Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
257 changes: 257 additions & 0 deletions apps/server/src/project/AgentSessionImporter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,263 @@ it.layer(NodeServices.layer)("AgentSessionImporter", (it) => {
}),
);

it.effect("repairs legacy context titles on already imported Codex threads", () =>
Effect.gen(function* () {
const thread = makeThread("codex");
const source = makeThreadOutcome(thread).source;
const threadId = ThreadId.make(
`import:${source.providerInstanceId}:${source.providerSessionId}`,
);
const commands: Array<OrchestrationCommand> = [];
const scanner = AgentSessionScanner.AgentSessionScanner.of({
scan: Effect.die("unused"),
recentThreads: () =>
Stream.succeed({
_tag: "AlreadyImported",
source,
canonicalTitle: "Prototype MetaApi trade replication",
}),
});
const engine = OrchestrationEngine.OrchestrationEngineService.of({
dispatch: (command) => Effect.sync(() => ({ sequence: commands.push(command) })),
readEvents: () => Stream.empty,
readThreadEvents: () => Stream.empty,
getThreadReplayStats: () => Effect.die("unused"),
streamDomainEvents: Stream.empty,
subscribeDomainEvents: Effect.succeed(Stream.empty),
latestSequence: Effect.succeed(0),
});
const directory = ProviderSessionDirectory.ProviderSessionDirectory.of({
upsert: () => Effect.die("must not replace an existing binding"),
getProvider: () => Effect.die("unused"),
recordImportedTranscript: () => Effect.die("must not rewrite completed history"),
getBinding: () => Effect.die("must not read a completed binding"),
listThreadIds: () => Effect.die("unused"),
listBindings: () => Effect.die("unused"),
});

const result = yield* runImport({
scanner,
engine,
directory,
snapshots: makeSnapshotsLayer({
project: makeProject(),
getThread: (id) =>
id === threadId
? Option.some({
...makeProjectedThread({ source: "codex", imported: true }),
title: "<recommended_plugins>",
})
: Option.none(),
}),
});

expect(result).toEqual({ importedCount: 1, skippedCount: 0 });
expect(commands).toMatchObject([
{
type: "thread.title.generate.complete",
threadId,
title: "Prototype MetaApi trade replication",
expectedTitle: "<recommended_plugins>",
expectedVersion: null,
needsRefinement: false,
},
]);

commands.length = 0;
expect(
yield* runImport({
scanner,
engine,
directory,
snapshots: makeSnapshotsLayer({
project: makeProject(),
getThread: () =>
Option.some({
...makeProjectedThread({ source: "codex", imported: true }),
title: "# AGENTS.md instructions for /tmp/project",
}),
}),
}),
).toEqual({ importedCount: 1, skippedCount: 0 });
expect(commands).toMatchObject([
{
type: "thread.title.generate.complete",
expectedTitle: "# AGENTS.md instructions for /tmp/project",
title: "Prototype MetaApi trade replication",
},
]);

commands.length = 0;
expect(
yield* runImport({
scanner,
engine,
directory,
snapshots: makeSnapshotsLayer({
project: makeProject(),
getThread: () =>
Option.some({
...makeProjectedThread({ source: "codex", imported: true }),
title: "My custom thread title",
titleState: {
source: "manual",
version: CommandId.make("manual-title"),
needsRefinement: false,
},
}),
}),
}),
).toEqual({ importedCount: 1, skippedCount: 0 });
expect(commands).toEqual([]);
}),
);

it.effect(
"repairs a legacy title from imported history when the Codex index has no title",
() =>
Effect.gen(function* () {
const thread = makeThread("codex");
const source = makeThreadOutcome(thread).source;
const threadId = ThreadId.make(
`import:${source.providerInstanceId}:${source.providerSessionId}`,
);
const commands: Array<OrchestrationCommand> = [];
const scanner = AgentSessionScanner.AgentSessionScanner.of({
scan: Effect.die("unused"),
recentThreads: () =>
Stream.succeed({ _tag: "AlreadyImported", source, canonicalTitle: null }),
});
const engine = OrchestrationEngine.OrchestrationEngineService.of({
dispatch: (command) => Effect.sync(() => ({ sequence: commands.push(command) })),
readEvents: () => Stream.empty,
readThreadEvents: () => Stream.empty,
getThreadReplayStats: () => Effect.die("unused"),
streamDomainEvents: Stream.empty,
subscribeDomainEvents: Effect.succeed(Stream.empty),
latestSequence: Effect.succeed(0),
});
const directory = ProviderSessionDirectory.ProviderSessionDirectory.of({
upsert: () => Effect.die("unused"),
getProvider: () => Effect.die("unused"),
recordImportedTranscript: () => Effect.die("must not rewrite completed history"),
getBinding: () => Effect.die("unused"),
listThreadIds: () => Effect.die("unused"),
listBindings: () => Effect.die("unused"),
});
const projected = makeProjectedThread({ source: "codex", imported: true });

const result = yield* runImport({
scanner,
engine,
directory,
snapshots: makeSnapshotsLayer({
project: makeProject(),
getThread: () =>
Option.some({
...projected,
title: "<user_instructions>",
messages: [
{
...projected.messages[0]!,
text: "<user_instructions>\nInternal setup instructions",
},
{
...projected.messages[0]!,
id: MessageId.make("user-followup"),
text: "Do not use this follow-up",
},
{
...projected.messages[0]!,
id: MessageId.make(`${threadId}:000001`),
text: "Recovered from imported history",
},
],
}),
}),
});

expect(result).toEqual({ importedCount: 1, skippedCount: 0 });
expect(commands).toMatchObject([
{
type: "thread.title.generate.complete",
threadId,
expectedTitle: "<user_instructions>",
title: "Recovered from imported history",
needsRefinement: false,
},
]);
}),
);

it.effect("records an existing import when Codex title repair fails", () =>
Effect.gen(function* () {
const thread = makeThread("codex");
const outcome = makeThreadOutcome(thread);
const threadId = ThreadId.make(
`import:${thread.providerInstanceId}:${thread.providerSessionId}`,
);
let recorded = 0;
const scanner = AgentSessionScanner.AgentSessionScanner.of({
scan: Effect.die("unused"),
recentThreads: () => Stream.succeed(outcome),
});
const engine = OrchestrationEngine.OrchestrationEngineService.of({
dispatch: (command) =>
command.type === "thread.title.generate.complete"
? Effect.fail(
new OrchestrationCommandInvariantError({
commandType: command.type,
detail: "Temporary title repair failure.",
}),
)
: Effect.die("must not dispatch another command"),
readEvents: () => Stream.empty,
readThreadEvents: () => Stream.empty,
getThreadReplayStats: () => Effect.die("unused"),
streamDomainEvents: Stream.empty,
subscribeDomainEvents: Effect.succeed(Stream.empty),
latestSequence: Effect.succeed(0),
});
const directory = ProviderSessionDirectory.ProviderSessionDirectory.of({
upsert: () => Effect.die("must not replace the existing binding"),
getProvider: () => Effect.die("unused"),
recordImportedTranscript: () => Effect.sync(() => void (recorded += 1)),
getBinding: () =>
Effect.succeed(
Option.some({
threadId,
provider: ProviderDriverKind.make("codex"),
providerInstanceId: ProviderInstanceId.make("codex"),
status: "stopped" as const,
resumeCursor: { threadId: thread.providerSessionId },
runtimeMode: "full-access" as const,
runtimePayload: { cwd: WORKSPACE_ROOT },
}),
),
listThreadIds: () => Effect.die("unused"),
listBindings: () => Effect.die("unused"),
});

const result = yield* runImport({
scanner,
engine,
directory,
snapshots: makeSnapshotsLayer({
project: makeProject(),
getThread: () =>
Option.some({
...makeProjectedThread({ source: "codex", imported: true }),
title: "<recommended_plugins>",
}),
}),
});

expect(result).toEqual({ importedCount: 1, skippedCount: 0 });
expect(recorded).toBe(1);
}),
);

it.effect("counts scanner skips without writing a thread or binding", () =>
Effect.gen(function* () {
const scanner = AgentSessionScanner.AgentSessionScanner.of({
Expand Down
66 changes: 66 additions & 0 deletions apps/server/src/project/AgentSessionImporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,26 @@ function hasImportedHistory(thread: OrchestrationThread): boolean {
return thread.messages.some((message) => isImportedAgentSessionMessageId(message.id));
}

/** Identify titles produced by the old first-line Codex import fallback. */
function hasLegacyCodexContextTitle(title: string): boolean {
return (
title === "<recommended_plugins>" ||
title === "<environment_context>" ||
title === "<user_instructions>" ||
/^# AGENTS\.md instructions(?:\s|$)/.test(title)
);
}

/** Recover a fallback title from the imported user messages already in the projection. */
function deriveImportedCodexTitle(thread: OrchestrationThread): string | null {
for (const message of thread.messages) {
if (message.role !== "user" || !isImportedAgentSessionMessageId(message.id)) continue;
const title = AgentSessionScanner.deriveImportedThreadTitle(message.text, "codex");
if (title !== null) return title;
}
return null;
}

function hasImportBlockingActivity(
thread: OrchestrationThread,
importedHistoryPresent: boolean,
Expand Down Expand Up @@ -136,6 +156,32 @@ export const importRecentAgentThreads = Effect.fn("importRecentAgentThreads")(fu
let importedCount = 0;
let skippedCount = 0;

const repairImportedCodexTitle = Effect.fn("repairImportedCodexTitle")(function* (
threadId: ThreadId,
canonicalTitle: string | null,
) {
const existingThread = yield* snapshots.getThreadDetailById(threadId);
if (Option.isNone(existingThread)) return;
const existingTitle = existingThread.value.title;
const replacementTitle = canonicalTitle ?? deriveImportedCodexTitle(existingThread.value);
if (
replacementTitle === null ||
!hasLegacyCodexContextTitle(existingTitle) ||
existingTitle === replacementTitle
) {
return;
}
yield* engine.dispatch({
type: "thread.title.generate.complete",
commandId: CommandId.make(yield* crypto.randomUUIDv4),
threadId,
title: replacementTitle,
expectedTitle: existingTitle,
expectedVersion: existingThread.value.titleState?.version ?? null,
needsRefinement: false,
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});

yield* Stream.runForEach(threads, (outcome) =>
Effect.gen(function* () {
if (outcome._tag === "Skipped") {
Expand All @@ -147,6 +193,16 @@ export const importRecentAgentThreads = Effect.fn("importRecentAgentThreads")(fu
`import:${outcome.source.providerInstanceId}:${outcome.source.providerSessionId}`,
);
if (outcome._tag === "AlreadyImported") {
if (outcome.source.provider === "codex") {
yield* repairImportedCodexTitle(threadId, outcome.canonicalTitle).pipe(
Effect.catch((cause) =>
Effect.logWarning("Could not repair an imported Codex thread title", {
threadId,
cause,
}),
),
);
}
importedThreadIds.add(threadId);
importedCount += 1;
} else if (importedThreadIds.has(threadId)) {
Expand Down Expand Up @@ -199,6 +255,16 @@ export const importRecentAgentThreads = Effect.fn("importRecentAgentThreads")(fu
importedHistoryPresent &&
Option.isSome(existingBinding)
) {
if (thread.source === "codex") {
yield* repairImportedCodexTitle(threadId, thread.title).pipe(
Effect.catch((cause) =>
Effect.logWarning("Could not repair an imported Codex thread title", {
threadId,
cause,
}),
),
);
}
yield* directory.recordImportedTranscript({ threadId, source: outcome.source });
return true;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Expand Down
Loading
Loading