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.
This commit is contained in:
Brennan Benson
2026-09-25 04:17:28 -07:00
parent 82f9bfd7b3
commit f3e52d206b
3 changed files with 57 additions and 4 deletions
@@ -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')
@@ -60,13 +60,16 @@ export async function importAdoptedTranscript(
params: AgentSessionAttachParams,
attached: AttachedJournal,
record: AgentSessionRecord,
prepared: JournalReplacementItem[] | null
prepared: JournalReplacementItem[] | null,
ownsJournal: boolean
): Promise<void> {
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
}
}
@@ -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,