Add OMP as a built-in importable provider (#1388)

* Support imports from Pi-compatible providers

* Add OMP as a built-in provider

* Address Pi session scanner review

* Restore Pi session import discovery

* Update provider test for disabled OMP
This commit is contained in:
Mohamed Boudra
2026-06-08 08:17:57 +00:00
committed by GitHub
parent 7d748436f4
commit a1fbc91163
29 changed files with 781 additions and 88 deletions
@@ -5737,24 +5737,6 @@ test.each([
codex: { enabled: false, derivedFromProviderId: null },
},
],
[
"derived",
"claude",
"zai",
{
claude: { enabled: true, derivedFromProviderId: null },
zai: { enabled: true, derivedFromProviderId: "claude" },
},
],
[
"outside importable allowlist",
"claude",
"gemini",
{
claude: { enabled: true, derivedFromProviderId: null },
gemini: { enabled: true, derivedFromProviderId: null },
},
],
])(
"listImportablePersistedAgents skips %s providers in fan-out",
async (_reason, includedProvider, skippedProvider, providerDefinitions) => {
@@ -5774,6 +5756,25 @@ test.each([
},
);
test("listImportablePersistedAgents includes derived providers that list persisted agents", async () => {
const claudeClient = new RecordingPersistedAgentsClient("claude");
const ompClient = new RecordingPersistedAgentsClient("omp");
const manager = new AgentManager({
clients: { claude: claudeClient, omp: ompClient },
providerDefinitions: {
claude: { enabled: true, derivedFromProviderId: null },
omp: { enabled: true, derivedFromProviderId: "pi" },
},
logger,
});
const result = await manager.listImportablePersistedAgents();
expect(claudeClient.calls).toBe(1);
expect(ompClient.calls).toBe(1);
expect(result.map((d) => d.provider).sort()).toEqual(["claude", "omp"]);
});
test("listImportablePersistedAgents narrows to the providerFilter when supplied", async () => {
const claudeClient = new RecordingPersistedAgentsClient("claude");
const codexClient = new RecordingPersistedAgentsClient("codex");
@@ -54,7 +54,6 @@ import {
} from "./agent-stream-coalescer.js";
import { ForegroundRunState, type ForegroundTurnWaiter } from "./foreground-run-state.js";
import { getAgentProviderDefinition } from "@getpaseo/protocol/provider-manifest";
import { IMPORTABLE_PROVIDERS } from "./provider-registry.js";
import { invokeRewindCapability, type RewindMode } from "./rewind/rewind.js";
import { isSystemInjectedEnvelope } from "./agent-prompt.js";
import { stripInternalPaseoMcpServer, withRuntimePaseoMcpServer } from "./runtime-mcp-config.js";
@@ -419,7 +418,6 @@ function buildExplicitTimelineSeedForRegister(
export class AgentManager {
private readonly clients = new Map<AgentProvider, AgentClient>();
private readonly providerEnabled = new Map<AgentProvider, boolean>();
private readonly providerDerivedFromId = new Map<AgentProvider, string | null>();
private readonly agents = new Map<string, LiveManagedAgent>();
private readonly timelineStore = new InMemoryAgentTimelineStore();
private readonly agentsAwaitingInitialSnapshotPersist = new Set<string>();
@@ -478,7 +476,6 @@ export class AgentManager {
for (const [provider, definition] of Object.entries(input.providerDefinitions)) {
if (definition) {
this.providerEnabled.set(provider, definition.enabled);
this.providerDerivedFromId.set(provider, definition.derivedFromProviderId ?? null);
}
}
for (const [provider, client] of Object.entries(input.clients)) {
@@ -651,15 +648,9 @@ export class AgentManager {
provider: AgentProvider,
providerFilter: Set<string> | undefined,
): boolean {
if (!IMPORTABLE_PROVIDERS.includes(provider as (typeof IMPORTABLE_PROVIDERS)[number])) {
return false;
}
if (this.providerEnabled.get(provider) === false) {
return false;
}
if (this.providerDerivedFromId.get(provider) != null) {
return false;
}
if (providerFilter && !providerFilter.has(provider)) {
return false;
}
@@ -320,7 +320,7 @@ describe("ProviderOverrideSchema", () => {
});
describe("migrateProviderSettings", () => {
const builtinProviderIds = ["claude", "codex", "copilot", "opencode", "pi"];
const builtinProviderIds = ["claude", "codex", "copilot", "opencode", "pi", "omp"];
test("passes through entries already in the new format", () => {
const migrated = migrateProviderSettings(
@@ -6,6 +6,7 @@ import type { AgentModelDefinition } from "./agent-sdk-types.js";
const mockState = vi.hoisted(() => {
interface ConstructorEntry {
runtimeSettings?: unknown;
providerParams?: unknown;
}
return {
@@ -206,10 +207,11 @@ vi.mock("./providers/pi/agent.js", () => ({
readonly provider = "pi";
readonly runtimeSettings?: unknown;
constructor(options: { runtimeSettings?: unknown }) {
constructor(options: { runtimeSettings?: unknown; providerParams?: unknown }) {
this.runtimeSettings = options.runtimeSettings;
mockState.constructorArgs.pi.push({
runtimeSettings: options.runtimeSettings,
providerParams: options.providerParams,
});
}
@@ -412,6 +414,39 @@ test("built-in override applies env", () => {
});
});
test("OMP is a disabled built-in backed by the Pi adapter", () => {
const registry = buildProviderRegistry(logger);
expect(registry.omp).toMatchObject({
id: "omp",
label: "OMP",
enabled: false,
derivedFromProviderId: null,
});
expect(registry.omp.createClient(logger).provider).toBe("omp");
expect(mockState.constructorArgs.pi.at(-1)).toEqual({
runtimeSettings: {
command: {
mode: "replace",
argv: ["omp"],
},
},
providerParams: {
sessionDir: "~/.omp/agent/sessions",
},
});
});
test("OMP can be enabled without custom provider boilerplate", () => {
const registry = buildProviderRegistry(logger, {
providerOverrides: {
omp: { enabled: true },
},
});
expect(registry.omp.enabled).toBe(true);
});
test("new provider extending claude appears in registry", () => {
const registry = buildProviderRegistry(logger, {
providerOverrides: {
@@ -429,6 +464,36 @@ test("new provider extending claude appears in registry", () => {
expect(registry.zai.createClient(logger).provider).toBe("zai");
});
test("new provider extending pi passes params to the base provider constructor", () => {
const registry = buildProviderRegistry(logger, {
providerOverrides: {
omp: {
extends: "pi",
label: "OMP",
command: ["omp"],
params: {
sessionDir: "~/.omp/agent/sessions",
},
},
},
});
expect(registry.omp.createClient(logger).provider).toBe("omp");
expect(mockState.constructorArgs.pi.at(-1)).toEqual({
runtimeSettings: {
command: {
mode: "replace",
argv: ["omp"],
},
env: undefined,
disallowedTools: undefined,
},
providerParams: {
sessionDir: "~/.omp/agent/sessions",
},
});
});
test("new provider extending acp uses GenericACPAgentClient", () => {
const registry = buildProviderRegistry(logger, {
providerOverrides: {
@@ -69,8 +69,6 @@ export interface ProviderDefinition extends AgentProviderDefinition {
fetchModes: (options: ListModesOptions) => Promise<AgentMode[]>;
}
export { IMPORTABLE_PROVIDERS } from "@getpaseo/protocol/importable-providers";
export interface BuildProviderRegistryOptions {
runtimeSettings?: AgentProviderRuntimeSettingsMap;
providerOverrides?: Record<string, ProviderOverride>;
@@ -82,6 +80,7 @@ interface ProviderClientFactoryOptions extends Pick<
BuildProviderRegistryOptions,
"workspaceGitService"
> {
providerParams?: unknown;
customProvider?: {
id: string;
label: string;
@@ -103,6 +102,7 @@ interface ResolvedProvider {
profileModelsAreAdditive: boolean;
enabled: boolean;
derivedFromProviderId: string | null;
providerParams?: unknown;
createBaseClient: (logger: Logger) => AgentClient;
}
@@ -129,10 +129,27 @@ const PROVIDER_CLIENT_FACTORIES: Record<string, ProviderClientFactory> = {
env: runtimeSettings?.env,
}),
opencode: (logger, runtimeSettings) => new OpenCodeAgentClient(logger, runtimeSettings),
pi: (logger, runtimeSettings) =>
pi: (logger, runtimeSettings, options) =>
new PiRpcAgentClient({
logger,
runtimeSettings,
providerParams: options?.providerParams,
}),
omp: (logger, runtimeSettings, options) =>
new PiRpcAgentClient({
logger,
runtimeSettings: mergeRuntimeSettings(
{
command: {
mode: "replace",
argv: ["omp"],
},
},
runtimeSettings,
),
providerParams: options?.providerParams ?? {
sessionDir: "~/.omp/agent/sessions",
},
}),
mock: (logger) => new MockLoadTestAgentClient(logger),
"mock-slow": () => new MockSlowProviderClient(),
@@ -524,11 +541,13 @@ function buildResolvedBuiltinProviders(
profileModels: override?.models ?? [],
additionalModels: override?.additionalModels ?? [],
profileModelsAreAdditive: false,
enabled: override?.enabled !== false,
enabled: override?.enabled ?? definition.enabledByDefault ?? true,
derivedFromProviderId: null,
providerParams: override?.params,
createBaseClient: (logger) =>
factory(logger, mergedRuntimeSettings, {
workspaceGitService: options.workspaceGitService,
providerParams: override?.params,
}),
});
}
@@ -574,6 +593,7 @@ function addDerivedProviders(
profileModelsAreAdditive: false,
enabled: override.enabled !== false,
derivedFromProviderId: null,
providerParams: override.params,
createBaseClient: (logger) =>
providerId === "cursor"
? new CursorACPAgentClient({
@@ -608,6 +628,7 @@ function addDerivedProviders(
);
const baseDefinition = baseProvider.definition;
const baseFactory = getProviderClientFactory(baseProviderId);
const providerParams = override.params ?? baseProvider.providerParams;
resolvedProviders.set(providerId, {
definition: createDerivedDefinition(providerId, baseDefinition, override),
@@ -617,8 +638,10 @@ function addDerivedProviders(
profileModelsAreAdditive: false,
enabled: override.enabled !== false,
derivedFromProviderId: baseProviderId,
providerParams,
createBaseClient: (logger) =>
baseFactory(logger, mergedRuntimeSettings, {
providerParams,
customProvider: {
id: providerId,
label: override.label ?? providerId,
@@ -53,7 +53,9 @@ describe("ProviderSnapshotManager public surface", () => {
const manager = new ProviderSnapshotManager({ logger: createTestLogger() });
try {
const ids = manager.listRegisteredProviderIds();
expect(ids).toEqual(expect.arrayContaining(["claude", "codex", "opencode", "copilot", "pi"]));
expect(ids).toEqual(
expect.arrayContaining(["claude", "codex", "opencode", "copilot", "pi", "omp"]),
);
} finally {
manager.destroy();
}
@@ -254,7 +256,7 @@ describe("ProviderSnapshotManager public surface", () => {
try {
const entries = await manager.listProviders({ cwd: "/tmp/project", wait: true });
const providers = entries.map((entry) => entry.provider).sort();
expect(providers).toEqual(["claude", "codex", "copilot", "opencode", "pi"]);
expect(providers).toEqual(["claude", "codex", "copilot", "omp", "opencode", "pi"]);
for (const entry of entries) {
expect(entry.enabled).toBe(false);
expect(entry.status).toBe("unavailable");
@@ -1,4 +1,15 @@
import { closeSync, existsSync, fstatSync, openSync, readSync } from "node:fs";
import {
closeSync,
existsSync,
fstatSync,
mkdirSync,
mkdtempSync,
openSync,
readSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import pino from "pino";
import { describe, expect, test } from "vitest";
@@ -660,6 +671,128 @@ describe("PiRpcAgentSession", () => {
});
describe("PiRpcAgentClient", () => {
test("lists JSONL persisted sessions from configured provider params", async () => {
const root = mkdtempSync(path.join(tmpdir(), "paseo-pi-sessions-"));
const cwd = path.join(root, "workspace");
const otherCwd = path.join(root, "other");
const sessionsDir = path.join(root, "sessions");
mkdirSync(sessionsDir, { recursive: true });
const sessionFile = path.join(sessionsDir, "20260101_session.jsonl");
writeFileSync(
sessionFile,
[
JSON.stringify({
type: "session",
version: 3,
id: "pi-session-jsonl",
timestamp: "2026-01-01T00:00:00.000Z",
cwd,
}),
JSON.stringify({
type: "message",
id: "entry-1",
timestamp: "2026-01-01T00:00:01.000Z",
message: { role: "user", content: "first prompt" },
}),
JSON.stringify({
type: "session_info",
id: "info-1",
timestamp: "2026-01-01T00:00:02.000Z",
name: "Imported Pi session",
}),
JSON.stringify({
type: "message",
id: "entry-2",
timestamp: "2026-01-01T00:00:03.000Z",
message: { role: "user", content: [{ type: "text", text: "last prompt" }] },
}),
].join("\n") + "\n",
"utf8",
);
writeFileSync(
path.join(sessionsDir, "other.jsonl"),
`${JSON.stringify({ type: "session", version: 3, id: "other", cwd: otherCwd })}\n`,
"utf8",
);
const client = new PiRpcAgentClient({
logger: pino({ level: "silent" }),
runtime: new FakePi(),
providerParams: { sessionDir: sessionsDir },
});
await expect(client.listPersistedAgents({ cwd })).resolves.toEqual([
{
provider: "pi",
sessionId: "pi-session-jsonl",
cwd,
title: "Imported Pi session",
lastActivityAt: new Date("2026-01-01T00:00:03.000Z"),
persistence: {
provider: "pi",
sessionId: "pi-session-jsonl",
nativeHandle: sessionFile,
metadata: { provider: "pi", cwd },
},
timeline: [
{ type: "user_message", text: "first prompt" },
{ type: "user_message", text: "last prompt" },
],
},
]);
});
test("lists JSONL persisted sessions from Pi's configured agent directory", async () => {
const root = mkdtempSync(path.join(tmpdir(), "paseo-pi-default-sessions-"));
const cwd = path.join(root, "workspace");
const agentDir = path.join(root, ".pi", "agent");
const sessionsDir = path.join(agentDir, "sessions");
mkdirSync(sessionsDir, { recursive: true });
const sessionFile = path.join(sessionsDir, "20260102_session.jsonl");
writeFileSync(
sessionFile,
[
JSON.stringify({
type: "session",
version: 3,
id: "pi-default-session",
timestamp: "2026-01-02T00:00:00.000Z",
cwd,
}),
JSON.stringify({
type: "message",
id: "entry-1",
timestamp: "2026-01-02T00:00:01.000Z",
message: { role: "user", content: "default dir prompt" },
}),
].join("\n") + "\n",
"utf8",
);
const client = new PiRpcAgentClient({
logger: pino({ level: "silent" }),
runtime: new FakePi(),
runtimeSettings: {
env: {
PI_CODING_AGENT_DIR: agentDir,
},
},
});
await expect(client.listPersistedAgents({ cwd })).resolves.toMatchObject([
{
provider: "pi",
sessionId: "pi-default-session",
cwd,
title: "default dir prompt",
persistence: {
provider: "pi",
sessionId: "pi-default-session",
nativeHandle: sessionFile,
metadata: { provider: "pi", cwd },
},
},
]);
});
test("lists models from a short-lived Pi session in the requested cwd", async () => {
const pi = new FakePi();
const client = createClient(pi);
@@ -3,6 +3,7 @@ import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { homedir, tmpdir } from "node:os";
import { join } from "node:path";
import type { Logger } from "pino";
import { z } from "zod";
import {
type AgentCapabilityFlags,
@@ -52,6 +53,7 @@ import {
} from "./history-mapper.js";
import { PiCliRuntime } from "./cli-runtime.js";
import { revertPiConversation } from "./rewind.js";
import { listPiPersistedAgents } from "./session-descriptor.js";
import type { PiRuntime, PiRuntimeSession } from "./runtime.js";
import type {
PiAgentSessionEvent,
@@ -86,6 +88,14 @@ const QUESTION_COMMENT_HEADER = "Comment";
const PI_ASK_USER_FREEFORM_SENTINEL = "✏️ Type custom response...";
const COMBINED_ASK_USER_METADATA = "ask_user_select_optional_comment";
export const PiProviderParamsSchema = z
.object({
sessionDir: z.string().min(1).optional(),
})
.strict();
type PiProviderParams = z.infer<typeof PiProviderParamsSchema>;
const PI_HANDLED_BUILTIN_SLASH_COMMANDS: AgentSlashCommand[] = [
{
name: "compact",
@@ -128,6 +138,7 @@ const PI_THINKING_OPTIONS: ReadonlyArray<{
interface PiRpcAgentClientOptions {
logger: Logger;
runtimeSettings?: ProviderRuntimeSettings;
providerParams?: unknown;
runtime?: PiRuntime;
}
@@ -1826,11 +1837,13 @@ export class PiRpcAgentClient implements AgentClient {
private readonly logger: Logger;
private readonly runtimeSettings?: ProviderRuntimeSettings;
private readonly providerParams: PiProviderParams;
private readonly runtime: PiRuntime;
constructor(options: PiRpcAgentClientOptions) {
this.logger = options.logger;
this.runtimeSettings = options.runtimeSettings;
this.providerParams = PiProviderParamsSchema.parse(options.providerParams ?? {});
this.runtime = options.runtime ?? createRuntime(options.logger, options.runtimeSettings);
}
@@ -1940,9 +1953,14 @@ export class PiRpcAgentClient implements AgentClient {
}
async listPersistedAgents(
_options?: ListPersistedAgentsOptions,
options?: ListPersistedAgentsOptions,
): Promise<PersistedAgentDescriptor[]> {
return [];
return await listPiPersistedAgents({
...options,
provider: PI_PROVIDER,
sessionDir: this.providerParams.sessionDir,
runtimeSettings: this.runtimeSettings,
});
}
async isAvailable(): Promise<boolean> {
@@ -121,6 +121,36 @@ describe("PiCliRuntime", () => {
]);
});
test("uses the configured command when resuming a session", async () => {
const child = createPiChild();
replyToCommands(child, () => ({}));
const launches: PiRuntimeLaunch[] = [];
const runtime = new PiCliRuntime({
logger: pino({ level: "silent" }),
command: ["pi"],
runtimeSettings: {
command: {
mode: "replace",
argv: ["omp"],
},
},
spawnProcess: (launch) => {
launches.push(launch);
return child;
},
});
await runtime.startSession({ cwd: "/workspace/project", session: "/tmp/omp-session.jsonl" });
expect(launches).toEqual([
expect.objectContaining({
cwd: "/workspace/project",
session: "/tmp/omp-session.jsonl",
argv: ["omp", "--mode", "rpc", "--session", "/tmp/omp-session.jsonl"],
}),
]);
});
test("passes an appended system prompt to Pi", async () => {
const child = createPiChild();
replyToCommands(child, () => ({}));
@@ -0,0 +1,383 @@
import { open, readdir, readFile, stat } from "node:fs/promises";
import { homedir } from "node:os";
import path from "node:path";
import type {
AgentPersistenceHandle,
AgentTimelineItem,
ListPersistedAgentsOptions,
PersistedAgentDescriptor,
} from "../../agent-sdk-types.js";
import type { ProviderRuntimeSettings } from "../../provider-launch-config.js";
import { createRealpathAwarePathMatcher } from "../../../../utils/path.js";
const PI_PROVIDER = "pi";
const PI_CONFIG_DIR_NAME = ".pi";
const PI_AGENT_DIR_ENV = "PI_CODING_AGENT_DIR";
const PI_SESSION_DIR_ENV = "PI_CODING_AGENT_SESSION_DIR";
const HEAD_BYTES = 64 * 1024;
const TAIL_BYTES = 256 * 1024;
const FULL_SCAN_LINE_LIMIT = 2_000;
interface PiSessionDescriptorOptions extends ListPersistedAgentsOptions {
provider?: string;
sessionDir?: string;
runtimeSettings?: ProviderRuntimeSettings;
env?: NodeJS.ProcessEnv;
homeDir?: string;
}
interface PiSessionHeader {
sessionId: string;
cwd: string;
createdAt: Date | null;
}
interface PiSessionTail {
title: string | null;
lastActivityAt: Date | null;
lastUserMessage: string | null;
}
export async function listPiPersistedAgents(
options: PiSessionDescriptorOptions = {},
): Promise<PersistedAgentDescriptor[]> {
const provider = options.provider ?? PI_PROVIDER;
const sessionsDir = await resolvePiSessionsDir(options);
const files = await walkJsonlFiles(sessionsDir);
const matchesCwd = options.cwd ? createRealpathAwarePathMatcher(options.cwd) : null;
const limit = options.limit ?? 20;
const descriptors: PersistedAgentDescriptor[] = [];
for (const file of files) {
const descriptor = await readPiSessionDescriptor(file, provider);
if (!descriptor) continue;
if (matchesCwd && !matchesCwd(descriptor.cwd)) continue;
descriptors.push(descriptor);
}
return descriptors
.sort((left, right) => right.lastActivityAt.getTime() - left.lastActivityAt.getTime())
.slice(0, limit);
}
async function resolvePiSessionsDir(options: PiSessionDescriptorOptions): Promise<string> {
const env = options.env ?? process.env;
const homeDir = options.homeDir ?? homedir();
const baseDir = options.cwd ?? process.cwd();
if (options.sessionDir?.trim()) {
return resolveConfigPath(options.sessionDir, { baseDir, homeDir });
}
const agentDir = resolvePiAgentDir({ runtimeSettings: options.runtimeSettings, env, homeDir });
const envSessionDir =
options.runtimeSettings?.env?.[PI_SESSION_DIR_ENV] ?? env[PI_SESSION_DIR_ENV];
if (envSessionDir?.trim()) {
return resolveConfigPath(envSessionDir, { baseDir, homeDir });
}
const settingsSessionDir = await readConfiguredSessionDir({
agentDir,
cwd: options.cwd,
});
if (settingsSessionDir?.trim()) {
return resolveConfigPath(settingsSessionDir, { baseDir, homeDir });
}
return path.join(agentDir, "sessions");
}
function resolvePiAgentDir(input: {
runtimeSettings?: ProviderRuntimeSettings;
env: NodeJS.ProcessEnv;
homeDir: string;
}): string {
const configured = input.runtimeSettings?.env?.[PI_AGENT_DIR_ENV] ?? input.env[PI_AGENT_DIR_ENV];
if (configured?.trim()) {
return resolveConfigPath(configured, { baseDir: process.cwd(), homeDir: input.homeDir });
}
return path.join(input.homeDir, PI_CONFIG_DIR_NAME, "agent");
}
async function readConfiguredSessionDir(input: {
agentDir: string;
cwd: string | undefined;
}): Promise<string | null> {
const values = await Promise.all([
readSessionDirFromSettings(path.join(input.agentDir, "settings.json")),
input.cwd
? readSessionDirFromSettings(path.join(input.cwd, PI_CONFIG_DIR_NAME, "settings.json"))
: null,
]);
return values[1] ?? values[0] ?? null;
}
async function readSessionDirFromSettings(settingsPath: string): Promise<string | null> {
try {
const parsed = JSON.parse(await readFile(settingsPath, "utf8")) as unknown;
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
return null;
}
const sessionDir = Reflect.get(parsed, "sessionDir");
return typeof sessionDir === "string" && sessionDir.trim() ? sessionDir : null;
} catch {
return null;
}
}
function resolveConfigPath(value: string, options: { baseDir: string; homeDir: string }): string {
if (value === "~") {
return options.homeDir;
}
if (value.startsWith("~/")) {
return path.join(options.homeDir, value.slice(2));
}
return path.isAbsolute(value) ? value : path.resolve(options.baseDir, value);
}
async function walkJsonlFiles(root: string): Promise<string[]> {
let entries: import("node:fs").Dirent[];
try {
entries = await readdir(root, { withFileTypes: true });
} catch {
return [];
}
const files = await Promise.all(
entries.map(async (entry) => {
const entryPath = path.join(root, entry.name);
if (entry.isDirectory()) {
return await walkJsonlFiles(entryPath);
}
return entry.isFile() && entry.name.endsWith(".jsonl") ? [entryPath] : [];
}),
);
return files.flat();
}
async function readPiSessionDescriptor(
filePath: string,
provider: string,
): Promise<PersistedAgentDescriptor | null> {
const firstLine = await readFirstLine(filePath);
if (!firstLine) return null;
const header = parseSessionHeader(firstLine);
if (!header) return null;
const tail = await readTail(filePath).catch(() => "");
const tailInfo = parseSessionTail(tail);
const headInfo = await scanSessionHead(filePath);
const title = tailInfo.title ?? headInfo.title ?? headInfo.firstUserMessage;
const lastActivityAt =
tailInfo.lastActivityAt ?? (await readFileMtime(filePath)) ?? header.createdAt ?? new Date(0);
const timeline = buildPreviewTimeline({
firstUserMessage: headInfo.firstUserMessage,
lastUserMessage: tailInfo.lastUserMessage,
});
const persistence: AgentPersistenceHandle = {
provider,
sessionId: header.sessionId,
nativeHandle: filePath,
metadata: {
provider,
cwd: header.cwd,
},
};
return {
provider,
sessionId: header.sessionId,
cwd: header.cwd,
title,
lastActivityAt,
persistence,
timeline,
};
}
async function readFirstLine(filePath: string): Promise<string | null> {
const handle = await open(filePath, "r").catch(() => null);
if (!handle) return null;
try {
const buffer = Buffer.alloc(HEAD_BYTES);
const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
if (bytesRead <= 0) return null;
const chunk = buffer.subarray(0, bytesRead).toString("utf8");
const newlineIndex = chunk.indexOf("\n");
return (newlineIndex === -1 ? chunk : chunk.slice(0, newlineIndex)).trim();
} finally {
await handle.close().catch(() => undefined);
}
}
async function readTail(filePath: string): Promise<string> {
const fileStats = await stat(filePath);
const start = Math.max(0, fileStats.size - TAIL_BYTES);
const length = fileStats.size - start;
const handle = await open(filePath, "r");
try {
const buffer = Buffer.alloc(length);
const { bytesRead } = await handle.read(buffer, 0, buffer.length, start);
return buffer.subarray(0, bytesRead).toString("utf8");
} finally {
await handle.close().catch(() => undefined);
}
}
async function readFileMtime(filePath: string): Promise<Date | null> {
try {
return (await stat(filePath)).mtime;
} catch {
return null;
}
}
function parseSessionHeader(firstLine: string): PiSessionHeader | null {
const entry = parseJsonRecord(firstLine);
if (!entry || entry.type !== "session") return null;
const sessionId = typeof entry.id === "string" ? entry.id : null;
const cwd = typeof entry.cwd === "string" ? entry.cwd : null;
if (!sessionId || !cwd) return null;
const createdAt = parseDate(entry.timestamp);
return { sessionId, cwd, createdAt };
}
function parseSessionTail(tail: string): PiSessionTail {
const lines = tail.split(/\r?\n/u);
let title: string | null = null;
let lastActivityAt: Date | null = null;
let fallbackTimestamp: Date | null = null;
let lastUserMessage: string | null = null;
for (let index = lines.length - 1; index >= 0; index -= 1) {
const entry = parseJsonRecord(lines[index].trim());
if (!entry) continue;
if (!title && entry.type === "session_info") {
title = readNonEmptyString(entry.name);
}
const entryTimestamp = parseDate(entry.timestamp);
if (!fallbackTimestamp && entryTimestamp) {
fallbackTimestamp = entryTimestamp;
}
if (entry.type !== "message") {
continue;
}
if (!lastActivityAt && entryTimestamp) {
lastActivityAt = entryTimestamp;
}
if (!lastUserMessage && isRecord(entry.message) && entry.message.role === "user") {
lastUserMessage = extractMessageText(entry.message.content);
}
if (title && lastActivityAt && lastUserMessage) {
break;
}
}
return { title, lastActivityAt: lastActivityAt ?? fallbackTimestamp, lastUserMessage };
}
async function scanSessionHead(filePath: string): Promise<{
title: string | null;
firstUserMessage: string | null;
}> {
let content: string;
try {
content = await readFile(filePath, "utf8");
} catch {
return { title: null, firstUserMessage: null };
}
let title: string | null = null;
let firstUserMessage: string | null = null;
let lineCount = 0;
for (const rawLine of content.split(/\r?\n/u)) {
lineCount += 1;
const entry = parseJsonRecord(rawLine.trim());
if (!entry) continue;
if (entry.type === "session_info") {
title = readNonEmptyString(entry.name) ?? title;
}
if (!firstUserMessage && entry.type === "message" && isRecord(entry.message)) {
if (entry.message.role === "user") {
firstUserMessage = extractMessageText(entry.message.content);
}
}
if (title && firstUserMessage) {
break;
}
if (lineCount >= FULL_SCAN_LINE_LIMIT && firstUserMessage) {
break;
}
}
return { title, firstUserMessage };
}
function buildPreviewTimeline(input: {
firstUserMessage: string | null;
lastUserMessage: string | null;
}): AgentTimelineItem[] {
const items: AgentTimelineItem[] = [];
if (input.firstUserMessage) {
items.push({ type: "user_message", text: input.firstUserMessage });
}
if (input.lastUserMessage && input.lastUserMessage !== input.firstUserMessage) {
items.push({ type: "user_message", text: input.lastUserMessage });
}
return items;
}
function parseJsonRecord(line: string): Record<string, unknown> | null {
if (!line) return null;
try {
const parsed = JSON.parse(line) as unknown;
return isRecord(parsed) ? parsed : null;
} catch {
return null;
}
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function readNonEmptyString(value: unknown): string | null {
return typeof value === "string" && value.trim() ? value.trim() : null;
}
function parseDate(value: unknown): Date | null {
if (typeof value !== "string" && typeof value !== "number") {
return null;
}
const date = new Date(value);
return Number.isNaN(date.getTime()) ? null : date;
}
function extractMessageText(content: unknown): string | null {
if (typeof content === "string") {
return content.trim() || null;
}
if (!Array.isArray(content)) {
return null;
}
const text = content
.flatMap((part) =>
isRecord(part) && part.type === "text" && typeof part.text === "string" ? [part.text] : [],
)
.join("\n\n")
.trim();
return text || null;
}
@@ -151,7 +151,7 @@ const AgentMetadataGenerationSchema = z
})
.strict();
const BUILTIN_PROVIDER_IDS = ["claude", "codex", "copilot", "opencode", "pi"] as const;
const BUILTIN_PROVIDER_IDS = ["claude", "codex", "copilot", "opencode", "pi", "omp"] as const;
function isLegacyProviderEntry(value: unknown): boolean {
if (!value || typeof value !== "object" || Array.isArray(value)) {