From f3e52d206b3c529bb3e151cde5b42fa60591e211 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Fri, 25 Sep 2026 04:17:28 -0700 Subject: [PATCH] fix(native-chat): an adoption that fails to import keeps the conversation open The attach now writes into the conversation's own open journal, but a failed transcript import still closed it as if it were the attach's provisional one. The conversation stayed indexed with a closed journal, so every later send answered "could not be recorded" and every attach failed again until the app restarted. The import now closes only a journal the attach opened for itself. --- ...tured-agent-session-adopted-import.test.ts | 44 +++++++++++++++++++ ...structured-agent-session-adopted-import.ts | 9 ++-- .../structured-agent-session-attach-flow.ts | 8 +++- 3 files changed, 57 insertions(+), 4 deletions(-) diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-adopted-import.test.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-adopted-import.test.ts index 0248799980a..a8c19781d71 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-adopted-import.test.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-adopted-import.test.ts @@ -15,6 +15,7 @@ import { performAttach, type AttachFlowInput } from './structured-agent-session- import { AgentSessionJournal } from '../agent-session-journal/journal-store' import { agentSessionJournalCloseRetries } from '../agent-session-journal/journal-close-retry' import * as legacyImport from '../agent-session-journal/journal-legacy-import' +import { StructuredAgentSessionHost } from './structured-agent-session-host' const NOW = 1_800_000_000_000 const SESSION = 'codex_adopting_session' @@ -225,6 +226,49 @@ describe('adopting a provider conversation on create', () => { expect(close).toHaveBeenCalledTimes(1) }) + it('leaves the conversation writable when the import fails after acquiring', async () => { + root = await mkdtemp(join(tmpdir(), 'orca-adopt-host-failure-')) + const transcriptPath = join(root, 'rollout.jsonl') + await writeCodexRollout(transcriptPath, 'valid source') + store = await AgentSessionRecordStore.open({ directory: join(root, 'store'), hostId: 'local' }) + const host = new StructuredAgentSessionHost({ + store, + adapter: adapter(), + journalRoot: root, + claimKeyId: 'key-1', + mintSpawnToken: () => 'spawn-a', + now: () => NOW + }) + vi.spyOn(AgentSessionJournal.prototype, 'replaceEpochItems').mockRejectedValueOnce( + new Error('disk write failed') + ) + const attached = await host + .attach({ callerKey: 'client-1' }, attachParams(transcriptPath)) + .catch(() => null) + expect(attached?.ok).not.toBe(true) + + const body = { kind: 'message' as const, role: 'user' as const, blocks: [] } + const sent = await host.send( + { callerKey: 'client-1' }, + { + envelope: { + sessionId: SESSION, + clientOperationId: `${NOW}-${'2'.padStart(32, '0')}`, + expectedRuntimeFence: null, + payloadFingerprint: computeAgentSessionPayloadFingerprint({ + method: 'agentSession.send', + sessionId: SESSION, + fields: { body } + }) + }, + body + } + ) + // The failed attach kept the conversation's own journal open, so the send is recorded. + expect(sent).toMatchObject({ ok: true, value: { submission: { dispatchState: 'pending' } } }) + await host.flushAllStreamedEvents() + }) + it('prepares a valid source once before acquisition and imports those exact items', async () => { root = await mkdtemp(join(tmpdir(), 'orca-adopt-once-')) const transcriptPath = join(root, 'rollout.jsonl') diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-adopted-import.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-adopted-import.ts index 459d21b1f3b..63d00ef5398 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-adopted-import.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-adopted-import.ts @@ -60,13 +60,16 @@ export async function importAdoptedTranscript( params: AgentSessionAttachParams, attached: AttachedJournal, record: AgentSessionRecord, - prepared: JournalReplacementItem[] | null + prepared: JournalReplacementItem[] | null, + ownsJournal: boolean ): Promise { try { await applyAdoptedTranscript(params, attached, record, prepared) } catch (error) { - // Publication has not taken ownership of this provisional journal yet. - await agentSessionJournalCloseRetries.closeOrRetain(attached.journal) + // A journal the attach opened for itself has no other owner; the conversation's stays open. + if (ownsJournal) { + await agentSessionJournalCloseRetries.closeOrRetain(attached.journal) + } throw error } } diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-attach-flow.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-attach-flow.ts index 83555d63729..3498b6f5d88 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-attach-flow.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-attach-flow.ts @@ -212,7 +212,13 @@ export async function performAttach( ...(input.openConversation ? { openConversation: input.openConversation } : {}), providerHistoryWindow }) - await importAdoptedTranscript(params, attached, record, preparedTranscript.items) + await importAdoptedTranscript( + params, + attached, + record, + preparedTranscript.items, + input.openConversation === undefined + ) await input.onAttached(attached, acquisitionGeneration, acquiredOwner, providerChildPhase) await store.recordOperationOutcome({ callerKey: input.callerKey,