From 2d73b954adabf5efdaedea3cc8109987db5f9539 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Mon, 19 Oct 2026 01:06:47 -0700 Subject: [PATCH 1/4] refactor(native-chat): remove three chat mechanisms nothing drives - The adapter's optional startAnswered hook lost its only implementer in #27925; it was still forwarded through four layers and always read as answered. A start that never answered is read from the child's phase, as the exit already does. - The saved lease's journalCheckpoint is only ever written as null and read only by validation. Records an older build saved with it still load: it joins the retired lease fields dropped at load, so the next write sheds it. - The record store's setConversationCommand had no production caller. The tests that stood in for an older build with it now seed the saved row and reopen the store; three that only re-pinned another test are removed. --- .../agent-session-record-test-fixture.ts | 1 - ...-agent-session-accept-then-deliver.test.ts | 21 +--------- ...structured-agent-session-adapter-router.ts | 3 -- .../structured-agent-session-adapter.ts | 3 -- ...tured-agent-session-crash-turn-end.test.ts | 1 - ...ctured-agent-session-forget-status.test.ts | 1 - ...d-agent-session-host-runtime-state.test.ts | 1 - ...ructured-agent-session-queued-gate.test.ts | 25 +----------- ...ueued-message-rig-provider.test-fixture.ts | 3 -- ...ured-agent-session-queued-messages.test.ts | 22 +--------- ...tured-agent-session-restart-resume-host.ts | 1 - ...ent-session-restart-resume-test-harness.ts | 1 - ...ed-agent-session-restart-witnesses.test.ts | 1 - ...uctured-agent-session-restart-witnesses.ts | 2 - ...t-session-wedged-profile-migration.test.ts | 1 - ...tured-agent-session-working-at-teardown.ts | 5 +-- .../structured-conversation-command.test.ts | 34 +++++++++++----- ...structured-conversation-compaction.test.ts | 24 ++++++++--- ...ent-session-conversation-command-record.ts | 13 ------ .../runtime/agent-session-record-founding.ts | 1 - src/main/runtime/agent-session-record-rows.ts | 5 ++- .../agent-session-record-store.test.ts | 40 ++++++++++++------- .../runtime/agent-session-record-store.ts | 11 ----- .../agent-session-reservation-admission.ts | 1 - .../structured-agent-launch-parity.test.ts | 1 - .../structured-worker-identity.test.ts | 1 - .../agent-session-lease-adjudication.test.ts | 1 - .../agent-session-record.test-fixture.ts | 1 - src/shared/agent-session-record.ts | 18 --------- 29 files changed, 75 insertions(+), 168 deletions(-) diff --git a/src/main/native-chat/agent-session-record-test-fixture.ts b/src/main/native-chat/agent-session-record-test-fixture.ts index 21e7f1d5191e..13040eb99e31 100644 --- a/src/main/native-chat/agent-session-record-test-fixture.ts +++ b/src/main/native-chat/agent-session-record-test-fixture.ts @@ -32,7 +32,6 @@ export function agentSessionRecordFixture( leaseDeadlineAt: 0, lastRenewedAt: 0, handoffOperationId: null, - journalCheckpoint: null, claimKeyId: 'test-key', claimStatus: 'released', unreconciled: false, diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-accept-then-deliver.test.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-accept-then-deliver.test.ts index c64c31b988f7..f2c6589dbaa1 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-accept-then-deliver.test.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-accept-then-deliver.test.ts @@ -870,7 +870,7 @@ describe('a close that stops the child and then a wind-down step fails', () => { ) }) -describe('a compaction or rewind an earlier child left prepared', () => { +describe('a rewind an earlier child left prepared', () => { async function leftPrepared(prepare: (fence: number) => Promise): Promise { await host.close(SESSION, 'evict') await prepare(store.getRecord(SESSION)!.lease.runtimeFence) @@ -879,25 +879,6 @@ describe('a compaction or rewind an earlier child left prepared', () => { await startHost() } - it("ignores an older build's interrupted compaction, so a send is accepted and delivered (R16)", async () => { - await leftPrepared((fence) => - store.setConversationCommand(SESSION, fence, { - command: 'compact', - runtimeFence: fence, - operationId: 'compact-op', - callerKey: 'client-1', - phase: 'prepared', - state: 'unknown' - }) - ) - - const id = await accept('after the compaction') - - await eventually(async () => expect((await submission(id))?.dispatchState).toBe('accepted')) - // Nothing settles the record: it belongs to a child this host no longer runs. - expect(store.getRecord(SESSION)?.conversationCommand).toMatchObject({ phase: 'prepared' }) - }) - it('completes a rewind the provider already applied at open, so a send is accepted (R16)', async () => { await leftPrepared((fence) => persistRewindRecord(store, SESSION, fence, { diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-adapter-router.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-adapter-router.ts index 46c447319456..17f980bd1e6b 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-adapter-router.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-adapter-router.ts @@ -159,9 +159,6 @@ export class StructuredAgentSessionAdapterRouter implements StructuredAgentSessi setOption: StructuredAgentSessionAdapter['setOption'] = (input) => this.owner(input.sessionId).setOption(input) - startAnswered = (sessionId: string): boolean | undefined => - this.liveOwnerOrNull(sessionId)?.startAnswered?.(sessionId) - prepareReadOptions = (input: { sessionId: string; fence: number }) => this.liveOwnerOrNull(input.sessionId)?.prepareReadOptions?.(input) diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-adapter.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-adapter.ts index 3eaf8c236d6c..d7a6725348a6 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-adapter.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-adapter.ts @@ -396,9 +396,6 @@ export type StructuredAgentSessionAdapter = StructuredAgentSessionAdapterStop & setOption( input: StructuredAgentSessionSetOptionInput ): Promise>> - /** False while the live child has not answered its start, so it has run nothing it was handed. - * Absent or undefined reads as answered. */ - startAnswered?(sessionId: string): boolean | undefined /** Fetch off the lane, then apply the result under the same child's fence. */ prepareReadOptions?(input: { sessionId: string diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-crash-turn-end.test.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-crash-turn-end.test.ts index 0bdc715f292f..666afda85379 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-crash-turn-end.test.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-crash-turn-end.test.ts @@ -102,7 +102,6 @@ function crashedClaudeRecord(): AgentSessionRecord { leaseDeadlineAt: LAST_RENEWED_AT + 30_000, lastRenewedAt: LAST_RENEWED_AT, handoffOperationId: null, - journalCheckpoint: null, claimKeyId: 'key-1', claimStatus: 'live', unreconciled: false, diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-forget-status.test.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-forget-status.test.ts index 6fad454e993f..c03fef0e7c07 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-forget-status.test.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-forget-status.test.ts @@ -84,7 +84,6 @@ function ownerRecord(): AgentSessionRecord { leaseDeadlineAt: 100, lastRenewedAt: 1, handoffOperationId: null, - journalCheckpoint: null, claimKeyId: 'fixture-key', claimStatus: 'live', unreconciled: false, diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-host-runtime-state.test.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-host-runtime-state.test.ts index dd4d0976bd88..f75e7b7f29dc 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-host-runtime-state.test.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-host-runtime-state.test.ts @@ -49,7 +49,6 @@ function reservedRecord(): AgentSessionRecord { leaseDeadlineAt: NOW + 30_000, lastRenewedAt: NOW, handoffOperationId: 'op-1', - journalCheckpoint: null, claimKeyId: 'key-1', claimStatus: 'reserved', unreconciled: false, diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-queued-gate.test.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-queued-gate.test.ts index 742bf19a91f0..df0d83b7172f 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-queued-gate.test.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-queued-gate.test.ts @@ -1,7 +1,6 @@ // The one queue gate: admission, the drain step and Send-now consume a single -// typed hold decision, so the lists cannot drift — pinned here with a clear in -// doubt, Send-now's override set, and the replay-preference rule for a refused -// draft. +// typed hold decision, so the lists cannot drift — pinned here with Send-now's +// override set and the replay-preference rule for a refused draft. import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { AGENT_JOURNAL_THREAD_SCOPE } from '../../../shared/agent-session-journal-types' @@ -43,26 +42,6 @@ const settleRejected: QueuedMessageTestRig['settleRejected'] = (...args) => rig.settleRejected(...args) describe('the one queue gate', () => { - it('a clear an older build left prepared holds nothing: Send-now sends the draft', async () => { - await workingSend() - const queued = await send('queued behind the clear', 'queue-if-active').result - if (!queued.ok || !('queued' in queued.value)) { - throw new Error('expected a queued receipt') - } - const draftId = queued.value.queued.messageId - // An unfinished /clear changed nothing the chat reads, so it refuses no send. - await store.setConversationCommand(SESSION, 1, { - command: 'clear', - runtimeFence: 1, - operationId: hostTestOperationId(), - callerKey: CALLER.callerKey, - phase: 'prepared', - state: 'unknown' - }) - expect(await sendNow(draftId)).toMatchObject({ ok: true }) - await eventually(async () => expect(await rig.handoff(draftId)).toBeDefined()) - }) - it('Send-now refuses on a pending prompt and overrides a running turn', async () => { const working = await workingSend() const queued = await send('queued mid-turn', 'queue-if-active').result diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-queued-message-rig-provider.test-fixture.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-queued-message-rig-provider.test-fixture.ts index 5911f3bf7ab5..2154aedecf9c 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-queued-message-rig-provider.test-fixture.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-queued-message-rig-provider.test-fixture.ts @@ -25,8 +25,6 @@ export type QueuedRigProviderOptions = { starting?: true /** The provider's Stop ends its child, as Claude's does. */ stopEndsSession?: true - /** Its child never answers its start, so it runs nothing it is handed. */ - startUnanswered?: true } export function createQueuedRigProvider( @@ -99,7 +97,6 @@ export function createQueuedRigProvider( compact, cancelTurn, ...(options.stopEndsSession ? { stopEndsSession: () => true } : {}), - ...(options.startUnanswered ? { startAnswered: () => false } : {}), answerPrompt: vi.fn(async ({ commit }) => commit()), setOption: vi.fn(async () => undefined), ...(options.rewind diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-queued-messages.test.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-queued-messages.test.ts index b00a1e63752b..11a09ab7c012 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-queued-messages.test.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-queued-messages.test.ts @@ -41,7 +41,6 @@ let currentRig: QueuedMessageTestRig | undefined const observationClock = createStructuredSessionObservationClock(() => rig.host, SESSION) beforeEach(() => observationClock.start()) let host: QueuedMessageTestRig['host'] -let store: QueuedMessageTestRig['store'] let dispatch: QueuedMessageTestRig['dispatch'] beforeEach(async () => { @@ -49,7 +48,7 @@ beforeEach(async () => { // A Stop ends the agent's session, so the next send starts a child that resumes the thread. rig = await createQueuedMessageTestRig({ restartable: true }) currentRig = rig - ;({ host, store, dispatch } = rig) + ;({ host, dispatch } = rig) }) afterEach(async () => { @@ -738,25 +737,6 @@ describe('/clear', () => { expect(await rig.queuePause(replacementId)).toBeNull() }) - it('a clear an older build left prepared holds no draft: it drains when the turn settles', async () => { - const working = await workingSend() - const queued = await send('behind the clear', 'queue-if-active').result - if (!queued.ok || !('queued' in queued.value)) { - throw new Error('expected a queued receipt') - } - const draftId = queued.value.queued.messageId - await store.setConversationCommand(SESSION, 1, { - command: 'clear', - runtimeFence: 1, - operationId: hostTestOperationId(), - callerKey: CALLER.callerKey, - phase: 'prepared', - state: 'unknown' - }) - await settleAccepted(working, 'a') - await eventually(async () => expect(await rig.handoff(draftId)).toBeDefined()) - }) - it('a clear with no drafts keeps the existing conversation open', async () => { const cleared = await clear(hostTestOperationId()) expect(cleared).toMatchObject({ ok: true, value: { command: 'clear', state: 'completed' } }) diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume-host.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume-host.ts index 884655d8719a..bece13e0aa09 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume-host.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume-host.ts @@ -70,7 +70,6 @@ export function createStructuredAgentSessionRestartResume( getRecord: deps.store.getRecord, childWork: surfaces.readChildWork, childWorkOpen: surfaces.childWorkOpen, - startAnswered: (sessionId) => deps.adapter.startAnswered?.(sessionId), ...(deps.recoveryCapsule ? { capsule: deps.recoveryCapsule } : {}), teardownId: randomUUID(), now: surfaces.now, diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume-test-harness.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume-test-harness.ts index d2fcee941a45..0660ac95b8fd 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume-test-harness.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume-test-harness.ts @@ -75,7 +75,6 @@ export function record(overrides: { chain?: AgentSessionRecord['providerHandleCh leaseDeadlineAt: NOW, lastRenewedAt: NOW, handoffOperationId: null, - journalCheckpoint: null, claimKeyId: 'key-1', claimStatus: 'released', unreconciled: false, diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-restart-witnesses.test.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-restart-witnesses.test.ts index 07b0a547c929..7032e17c02be 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-restart-witnesses.test.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-restart-witnesses.test.ts @@ -21,7 +21,6 @@ function witnessed() { AGENTS[sessionId] ? ({ provider: AGENTS[sessionId] } as AgentSessionRecord) : null, childWorkOpen: () => false, childWork: () => undefined, - startAnswered: () => undefined, capsule: { record: async (markers) => void recorded.push([...markers]) }, teardownId: 'teardown', now: () => 1, diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-restart-witnesses.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-restart-witnesses.ts index bf3fe842e0a7..4453a250cce5 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-restart-witnesses.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-restart-witnesses.ts @@ -34,7 +34,6 @@ export function createStructuredAgentSessionRestartWitnesses(deps: { getRecord: (sessionId: string) => AgentSessionRecord | null childWork: WorkingAtStopInput['childWork'] childWorkOpen: WorkingAtStopInput['childWorkOpen'] - startAnswered: NonNullable capsule?: Pick teardownId: string now: () => number @@ -75,7 +74,6 @@ export function createStructuredAgentSessionRestartWitnesses(deps: { getRecord: deps.getRecord, childWork: deps.childWork, childWorkOpen: deps.childWorkOpen, - startAnswered: deps.startAnswered, trigger, teardownId: deps.teardownId, now: deps.now() diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-wedged-profile-migration.test.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-wedged-profile-migration.test.ts index 32a0b6c2c2b2..19822abedf41 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-wedged-profile-migration.test.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-wedged-profile-migration.test.ts @@ -112,7 +112,6 @@ function wedgedRecord(overrides: WedgeOverrides): PersistedAgentSessionRecord { leaseDeadlineAt: NOW - 9_000, lastRenewedAt: NOW - 10_000, handoffOperationId: overrides.handoffOperationId ?? null, - journalCheckpoint: null, claimKeyId: 'key-1', claimStatus: overrides.claimStatus, unreconciled: false, diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-working-at-teardown.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-working-at-teardown.ts index dded754b3dbc..3df8570ca843 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-working-at-teardown.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-working-at-teardown.ts @@ -193,8 +193,6 @@ export function structuredAgentSessionWorkingAtStop(input: { childWork: (sessionId: string) => readonly AgentChildWorkView[] | undefined /** Child work the provider reported running and not over, recorded or not. */ childWorkOpen: (sessionId: string) => boolean - /** False when the session's live child never answered its start; the exit reads the same fact. */ - startAnswered?: (sessionId: string) => boolean | undefined trigger: AgentSessionResumeTrigger /** Stable teardown identity for continuation deduplication, not launch ancestry. */ teardownId: string @@ -208,8 +206,7 @@ export function structuredAgentSessionWorkingAtStop(input: { const current = agentSessionCurrentContextRows(snapshot.items, snapshot.submissions) // A queued message reached no agent, and one handed to a start that never answered ran nowhere: // neither is work to resume. The next open or the exit keeps it as unsent. - const unanswered = - session.child.phase === 'starting' || input.startAnswered?.(sessionId) === false + const unanswered = session.child.phase === 'starting' const handedOver = current.submissions.filter( (submission) => !isQueuedAgentJournalSubmission(submission) && diff --git a/src/main/native-chat/agent-session-wire/structured-conversation-command.test.ts b/src/main/native-chat/agent-session-wire/structured-conversation-command.test.ts index 7191ada60a86..ca84e50608dc 100644 --- a/src/main/native-chat/agent-session-wire/structured-conversation-command.test.ts +++ b/src/main/native-chat/agent-session-wire/structured-conversation-command.test.ts @@ -8,7 +8,11 @@ import { computeAgentSessionPayloadFingerprint } from '../../../shared/agent-ses import type { AgentSessionConversationCommand } from '../../../shared/agent-session-conversation-command' import type { AgentSessionOwnerProbe } from '../../../shared/agent-session-lease-adjudication' import type { AgentSessionRecordStore } from '../../runtime/agent-session-record-store' -import { openTestAgentSessionRecordStore } from '../../runtime/agent-session-record-store-test-harness' +import { + editPersistedTestAgentSessionStore, + openTestAgentSessionRecordStore, + type PersistedTestAgentSessionStore +} from '../../runtime/agent-session-record-store-test-harness' import { StructuredAgentSessionHost } from './structured-agent-session-host' import { claudeAndCodexDeclared } from './structured-agent-session-adapter-router-test-support' import type { StructuredAgentSessionAdapter } from './structured-agent-session-adapter' @@ -79,8 +83,11 @@ async function openHost(): Promise { hosts.push(host) } -/** A crash and relaunch: the next host opens what the dying one had written, and nothing after. */ -async function restartHost(): Promise { +/** A crash and relaunch: the next host opens what the dying one had written, and nothing after. + * `olderBuild` edits that state first, as an older build would have left it. */ +async function restartHost( + olderBuild?: (persisted: PersistedTestAgentSessionStore) => void +): Promise { await store.renewLeases([]) const dying = generationRoot() generation++ @@ -88,6 +95,9 @@ async function restartHost(): Promise { recursive: true, filter: (source) => !source.endsWith('.tmp') && !source.includes('.lock') }) + if (olderBuild) { + await editPersistedTestAgentSessionStore(generationRoot(), olderBuild) + } await openHost() } @@ -347,15 +357,17 @@ describe('same-conversation clear', () => { }) it('allows independently sending a record with an old-build replacement pointer', async () => { - const source = store.getRecord(HOST_TEST_SESSION)! - await store.setConversationCommand(HOST_TEST_SESSION, source.lease.runtimeFence, { - command: 'clear', - phase: 'committed', - state: 'completed', - operationId: hostTestOperationId(), - callerKey: caller.callerKey, - replacementSessionId: 'old-clear-destination' + await restartHost((persisted) => { + persisted.records[HOST_TEST_SESSION].conversationCommand = { + command: 'clear', + phase: 'committed', + state: 'completed', + operationId: hostTestOperationId(), + callerKey: caller.callerKey, + replacementSessionId: 'old-clear-destination' + } }) + await host.restoreReadableSessions(store.listVisibleSessionIds()) expect(await host.send(caller, sendParams('still this conversation'))).toMatchObject({ ok: true }) diff --git a/src/main/native-chat/agent-session-wire/structured-conversation-compaction.test.ts b/src/main/native-chat/agent-session-wire/structured-conversation-compaction.test.ts index 807bd6127488..37bac51980c9 100644 --- a/src/main/native-chat/agent-session-wire/structured-conversation-compaction.test.ts +++ b/src/main/native-chat/agent-session-wire/structured-conversation-compaction.test.ts @@ -30,6 +30,10 @@ import { replaceHostTestState } from './structured-agent-session-host-test-harness' import { StructuredAgentSessionHost } from './structured-agent-session-host' +import { + editPersistedTestAgentSessionStore, + openTestAgentSessionRecordStore +} from '../../runtime/agent-session-record-store-test-harness' import { StructuredAgentRegistry } from './structured-agent-registry' import { CODEX_STRUCTURED_AGENT } from '../../codex/codex-structured-agent-definition' import { @@ -750,18 +754,26 @@ it('ignores a ledger-only compaction record with no command receipt (B15)', asyn hostFingerprint: older.envelope.payloadFingerprint, now: HOST_TEST_NOW }) - await state.store.setConversationCommand( - SESSION, - state.store.getRecord(SESSION)!.lease.runtimeFence, - { + const fence = state.store.getRecord(SESSION)!.lease.runtimeFence + await state.host.close(SESSION, 'evict') + await state.host.flushAllStreamedEvents() + await editPersistedTestAgentSessionStore(state.root, (persisted) => { + persisted.records[SESSION].conversationCommand = { command: 'compact', - runtimeFence: state.store.getRecord(SESSION)!.lease.runtimeFence, + runtimeFence: fence, operationId: older.envelope.clientOperationId, callerKey: CALLER.callerKey, phase: 'prepared', state: 'unknown' } - ) + }) + const store = await openTestAgentSessionRecordStore(state.root) + replaceHostTestState({ + store, + host: new StructuredAgentSessionHost({ ...state.host.deps, store }) + }) + state = hostTestState() + await state.host.restoreReadableSessions(store.listVisibleSessionIds()) await expect(state.host.send(CALLER, sendParams('still works'))).resolves.toMatchObject({ ok: true diff --git a/src/main/runtime/agent-session-conversation-command-record.ts b/src/main/runtime/agent-session-conversation-command-record.ts index c9f1ba52a26d..0ad4fd2ca5c2 100644 --- a/src/main/runtime/agent-session-conversation-command-record.ts +++ b/src/main/runtime/agent-session-conversation-command-record.ts @@ -4,19 +4,6 @@ import { agentSessionOperationKey } from '../../shared/agent-session-operation-l import type { AgentSessionStoreState } from './agent-session-store-state' import type { AgentSessionConversationCommandRecord } from '../../shared/agent-session-conversation-command' -export function commitConversationCommandRecord( - state: AgentSessionStoreState, - sessionId: string, - fence: number, - command: AgentSessionConversationCommandRecord -): void { - const record = state.records.get(sessionId) - if (!record || record.lease.runtimeFence !== fence) { - throw agentSessionRefusalError('agent_session_checkpoint_stale', { reason: 'leaseMoved' }) - } - state.records.set(sessionId, { ...record, conversationCommand: command }) -} - export type AgentSessionConversationClear = { sessionId: string fence: number diff --git a/src/main/runtime/agent-session-record-founding.ts b/src/main/runtime/agent-session-record-founding.ts index c79265f088e5..04602e32eb83 100644 --- a/src/main/runtime/agent-session-record-founding.ts +++ b/src/main/runtime/agent-session-record-founding.ts @@ -79,7 +79,6 @@ export function foundAgentSessionRecord( leaseDeadlineAt: lease.now, lastRenewedAt: lease.now, handoffOperationId: null, - journalCheckpoint: null, claimKeyId: lease.claimKeyId, claimStatus: 'released', unreconciled: false, diff --git a/src/main/runtime/agent-session-record-rows.ts b/src/main/runtime/agent-session-record-rows.ts index e819a1ebbf6c..7b72ac564052 100644 --- a/src/main/runtime/agent-session-record-rows.ts +++ b/src/main/runtime/agent-session-record-rows.ts @@ -27,12 +27,12 @@ import { AgentSessionTabTable, type PersistedAgentSessionTab } from './agent-ses * no row count can say which. */ const SESSION_TABS_RECORDED = 'session_tabs_recorded' -/** Latch fields older builds wrote. Nothing reads them, and dropping them keeps a lease this build - * writes back from carrying a stale latch to an older build after a downgrade. */ +/** Lease fields older builds wrote and nothing reads, dropped at load so no write carries them on. */ type RetiredAgentSessionLeaseFields = { processlessAt?: unknown settlementRetryRequired?: unknown settlementRetryId?: unknown + journalCheckpoint?: unknown } export function withoutRetiredLeaseLatches(lease: AgentSessionLease): AgentSessionLease { @@ -40,6 +40,7 @@ export function withoutRetiredLeaseLatches(lease: AgentSessionLease): AgentSessi processlessAt: _processlessAt, settlementRetryRequired: _settlementRetryRequired, settlementRetryId: _settlementRetryId, + journalCheckpoint: _journalCheckpoint, ...current }: AgentSessionLease & RetiredAgentSessionLeaseFields = lease return current diff --git a/src/main/runtime/agent-session-record-store.test.ts b/src/main/runtime/agent-session-record-store.test.ts index 0bd72c8f2cc9..67500e9d5fbc 100644 --- a/src/main/runtime/agent-session-record-store.test.ts +++ b/src/main/runtime/agent-session-record-store.test.ts @@ -738,27 +738,39 @@ describe('claim keys and unreadable rows', () => { expect(store.isClaimKeyVerifiable('key-unknown', NOW)).toBe(true) }) - it.each([ - [ - 'invalid checkpoint', - (record: PersistedAgentSessionRecord) => - Object.assign(record.lease, { journalCheckpoint: { epoch: 'bad', sequence: 1 } }) - ], - [ - // No proof yet is a live owner whose provider has not answered; a proof naming no link is not. - 'live proof naming no handle', - (record: PersistedAgentSessionRecord) => - Object.assign(record.lease, { provenHandleLinkId: 'link-never-minted' }) - ] - ])('quarantines a record with %s', async (_name, corrupt) => { + // No proof yet is a live owner whose provider has not answered; a proof naming no link is not. + it('quarantines a record with a live proof naming no handle', async () => { const first = await open() await establishOwner(first) await editPersistedTestAgentSessionStore(directory, (persisted) => { - corrupt(persisted.records['session-alpha']) + Object.assign(persisted.records['session-alpha'].lease, { + provenHandleLinkId: 'link-never-minted' + }) }) expect((await open()).isSessionUnreadable('session-alpha')).toBe(true) }) + // Nothing reads the checkpoint any more, so even one this build could never have parsed loads. + it.each([ + { epoch: 3, sequence: 0 }, + { epoch: 'bad', sequence: 1 } + ])( + "loads a record carrying an older build's journal checkpoint %o, and drops it on the next write", + async (journalCheckpoint) => { + const first = await open() + await establishOwner(first) + await editPersistedTestAgentSessionStore(directory, (persisted) => { + Object.assign(persisted.records['session-alpha'].lease, { journalCheckpoint }) + }) + const reopened = await open() + expect(reopened.isSessionUnreadable('session-alpha')).toBe(false) + await reopened.setConversationName('session-alpha', 'renamed') + const stored = (await readPersistedTestAgentSessionStore(directory)).records['session-alpha'] + expect(stored.conversationName).toBe('renamed') + expect(stored.lease).not.toHaveProperty('journalCheckpoint') + } + ) + it('sets aside a row whose handle is in both stored forms and never rewrites it', async () => { const first = await open() await establishOwner(first) diff --git a/src/main/runtime/agent-session-record-store.ts b/src/main/runtime/agent-session-record-store.ts index 6115678b5b7a..ce4728d10430 100644 --- a/src/main/runtime/agent-session-record-store.ts +++ b/src/main/runtime/agent-session-record-store.ts @@ -4,7 +4,6 @@ import { backfillAgentSessionPermissionModes } from './agent-session-permission-backfill' import { agentSessionRefusalError } from '../../shared/agent-session-wire-refusals' import { createAgentSessionConversationReceipts } from './agent-session-conversation-receipts' -import { commitConversationCommandRecord } from './agent-session-conversation-command-record' import { pinAgentSessionRecordLaunchDirectory } from './agent-session-record-launch-directory' import { agentSessionOperationKey, @@ -161,16 +160,6 @@ export class AgentSessionRecordStore { return this.listRecords().filter((record) => agentSessionScopeKey(record.location) === scope) } - setConversationCommand( - sessionId: string, - fence: number, - command: NonNullable - ): Promise { - return this.transact((draft) => - commitConversationCommandRecord(draft, sessionId, fence, command) - ) - } - /** Unfenced on purpose: the name is a durable note, so writing it never contends with the * writer lease. `null` clears it. */ setConversationName = (sessionId: string, name: string | null): Promise => diff --git a/src/main/runtime/agent-session-reservation-admission.ts b/src/main/runtime/agent-session-reservation-admission.ts index 31a1ece91ff5..6136ff327dc7 100644 --- a/src/main/runtime/agent-session-reservation-admission.ts +++ b/src/main/runtime/agent-session-reservation-admission.ts @@ -307,7 +307,6 @@ function createAgentSessionRecord( leaseDeadlineAt: reservation.now + reservation.leaseTtlMs, lastRenewedAt: reservation.now, handoffOperationId: reservation.handoffOperationId, - journalCheckpoint: null, claimKeyId: reservation.claimKeyId, claimStatus: 'reserved', unreconciled: false, diff --git a/src/main/runtime/structured-agent-launch-parity.test.ts b/src/main/runtime/structured-agent-launch-parity.test.ts index 5bd5ceac3404..d84efdd0cbfe 100644 --- a/src/main/runtime/structured-agent-launch-parity.test.ts +++ b/src/main/runtime/structured-agent-launch-parity.test.ts @@ -441,7 +441,6 @@ async function recordFor( leaseDeadlineAt: 0, lastRenewedAt: 0, handoffOperationId: null, - journalCheckpoint: null, claimKeyId: 'parity-key', claimStatus: 'released', unreconciled: false, diff --git a/src/main/runtime/structured-worker-identity.test.ts b/src/main/runtime/structured-worker-identity.test.ts index 45ff3e5199bd..555ce61aaee5 100644 --- a/src/main/runtime/structured-worker-identity.test.ts +++ b/src/main/runtime/structured-worker-identity.test.ts @@ -52,7 +52,6 @@ function record(overrides: { leaseDeadlineAt: 0, lastRenewedAt: 0, handoffOperationId: null, - journalCheckpoint: null, claimKeyId: 'k', claimStatus: overrides.claimStatus ?? 'live', unreconciled: false, diff --git a/src/shared/agent-session-lease-adjudication.test.ts b/src/shared/agent-session-lease-adjudication.test.ts index 35e794efd0d2..3e0b86ef4441 100644 --- a/src/shared/agent-session-lease-adjudication.test.ts +++ b/src/shared/agent-session-lease-adjudication.test.ts @@ -29,7 +29,6 @@ function lease(overrides: Partial = {}): AgentSessionLease { leaseDeadlineAt: 1_000, lastRenewedAt: 500, handoffOperationId: null, - journalCheckpoint: null, claimKeyId: 'key-1', claimStatus: 'live', unreconciled: false, diff --git a/src/shared/agent-session-record.test-fixture.ts b/src/shared/agent-session-record.test-fixture.ts index 68a190fbbcae..2a3df12d2e59 100644 --- a/src/shared/agent-session-record.test-fixture.ts +++ b/src/shared/agent-session-record.test-fixture.ts @@ -29,7 +29,6 @@ export function agentSessionLeaseFixture( leaseDeadlineAt: 60_000, lastRenewedAt: 30_000, handoffOperationId: null, - journalCheckpoint: null, claimKeyId: 'key-1', claimStatus: 'live', unreconciled: false, diff --git a/src/shared/agent-session-record.ts b/src/shared/agent-session-record.ts index 554630ee2412..57f74a1a302a 100644 --- a/src/shared/agent-session-record.ts +++ b/src/shared/agent-session-record.ts @@ -81,8 +81,6 @@ export type AgentSessionProcessIdentity = { runtime?: string } -export type AgentSessionJournalCheckpoint = { epoch: number; sequence: number } - /** * `released` means no owner: a durable record that outlives its owner needs a name for that. * `conflicted` is how a terminal owner an older build recorded loads: recovery waits it out and @@ -125,7 +123,6 @@ export type AgentSessionLease = { * `recovering` proves nothing, so it leaves this alone. */ lastRenewedAt: number handoffOperationId: string | null - journalCheckpoint: AgentSessionJournalCheckpoint | null /** Key id that minted the HMAC claim this lease was granted under. */ claimKeyId: string claimStatus: AgentSessionClaimStatus @@ -279,19 +276,6 @@ export function isAgentSessionLaunchEnv(value: unknown): value is AgentSessionLa ) } -function isAgentSessionJournalCheckpoint(value: unknown): value is AgentSessionJournalCheckpoint { - if (typeof value !== 'object' || value === null) { - return false - } - const checkpoint = value as Partial - return ( - Number.isSafeInteger(checkpoint.epoch) && - (checkpoint.epoch as number) >= 0 && - Number.isSafeInteger(checkpoint.sequence) && - (checkpoint.sequence as number) >= 0 - ) -} - function isAgentSessionDeathEvidence(value: unknown): value is AgentSessionDeathEvidence { if (typeof value !== 'object' || value === null) { return false @@ -334,8 +318,6 @@ function isPersistedAgentSessionLease(value: unknown): value is PersistedAgentSe Number.isSafeInteger(lease.lastRenewedAt) && (lease.handoffOperationId === null || isBoundedString(lease.handoffOperationId, MAX_ID_LENGTH)) && - (lease.journalCheckpoint === null || - isAgentSessionJournalCheckpoint(lease.journalCheckpoint)) && isBoundedString(lease.claimKeyId, MAX_ID_LENGTH) && (lease.claimStatus === 'reserved' || lease.claimStatus === 'live' || From 7d4e6ef43a259a28f0d53a99c951129da8982c82 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Mon, 19 Oct 2026 02:10:13 -0700 Subject: [PATCH 2/4] test(native-chat): an older build's null journal checkpoint also loads --- src/main/runtime/agent-session-record-store.test.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/main/runtime/agent-session-record-store.test.ts b/src/main/runtime/agent-session-record-store.test.ts index 67500e9d5fbc..82a047610108 100644 --- a/src/main/runtime/agent-session-record-store.test.ts +++ b/src/main/runtime/agent-session-record-store.test.ts @@ -751,10 +751,7 @@ describe('claim keys and unreadable rows', () => { }) // Nothing reads the checkpoint any more, so even one this build could never have parsed loads. - it.each([ - { epoch: 3, sequence: 0 }, - { epoch: 'bad', sequence: 1 } - ])( + it.each([null, { epoch: 3, sequence: 0 }, { epoch: 'bad', sequence: 1 }])( "loads a record carrying an older build's journal checkpoint %o, and drops it on the next write", async (journalCheckpoint) => { const first = await open() From a1a48364a1545b3fd49b9bf3c33e2d9b3d9f7ec4 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Mon, 19 Oct 2026 02:25:16 -0700 Subject: [PATCH 3/4] Keep startAnswered cleanup in its owning PR --- .../structured-agent-session-adapter-router.ts | 3 +++ .../agent-session-wire/structured-agent-session-adapter.ts | 3 +++ ...agent-session-queued-message-rig-provider.test-fixture.ts | 3 +++ .../structured-agent-session-restart-resume-host.ts | 1 + .../structured-agent-session-restart-witnesses.test.ts | 1 + .../structured-agent-session-restart-witnesses.ts | 2 ++ .../structured-agent-session-working-at-teardown.ts | 5 ++++- 7 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-adapter-router.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-adapter-router.ts index 17f980bd1e6b..46c447319456 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-adapter-router.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-adapter-router.ts @@ -159,6 +159,9 @@ export class StructuredAgentSessionAdapterRouter implements StructuredAgentSessi setOption: StructuredAgentSessionAdapter['setOption'] = (input) => this.owner(input.sessionId).setOption(input) + startAnswered = (sessionId: string): boolean | undefined => + this.liveOwnerOrNull(sessionId)?.startAnswered?.(sessionId) + prepareReadOptions = (input: { sessionId: string; fence: number }) => this.liveOwnerOrNull(input.sessionId)?.prepareReadOptions?.(input) diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-adapter.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-adapter.ts index d7a6725348a6..3eaf8c236d6c 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-adapter.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-adapter.ts @@ -396,6 +396,9 @@ export type StructuredAgentSessionAdapter = StructuredAgentSessionAdapterStop & setOption( input: StructuredAgentSessionSetOptionInput ): Promise>> + /** False while the live child has not answered its start, so it has run nothing it was handed. + * Absent or undefined reads as answered. */ + startAnswered?(sessionId: string): boolean | undefined /** Fetch off the lane, then apply the result under the same child's fence. */ prepareReadOptions?(input: { sessionId: string diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-queued-message-rig-provider.test-fixture.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-queued-message-rig-provider.test-fixture.ts index 2154aedecf9c..5911f3bf7ab5 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-queued-message-rig-provider.test-fixture.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-queued-message-rig-provider.test-fixture.ts @@ -25,6 +25,8 @@ export type QueuedRigProviderOptions = { starting?: true /** The provider's Stop ends its child, as Claude's does. */ stopEndsSession?: true + /** Its child never answers its start, so it runs nothing it is handed. */ + startUnanswered?: true } export function createQueuedRigProvider( @@ -97,6 +99,7 @@ export function createQueuedRigProvider( compact, cancelTurn, ...(options.stopEndsSession ? { stopEndsSession: () => true } : {}), + ...(options.startUnanswered ? { startAnswered: () => false } : {}), answerPrompt: vi.fn(async ({ commit }) => commit()), setOption: vi.fn(async () => undefined), ...(options.rewind diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume-host.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume-host.ts index bece13e0aa09..884655d8719a 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume-host.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume-host.ts @@ -70,6 +70,7 @@ export function createStructuredAgentSessionRestartResume( getRecord: deps.store.getRecord, childWork: surfaces.readChildWork, childWorkOpen: surfaces.childWorkOpen, + startAnswered: (sessionId) => deps.adapter.startAnswered?.(sessionId), ...(deps.recoveryCapsule ? { capsule: deps.recoveryCapsule } : {}), teardownId: randomUUID(), now: surfaces.now, diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-restart-witnesses.test.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-restart-witnesses.test.ts index 7032e17c02be..07b0a547c929 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-restart-witnesses.test.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-restart-witnesses.test.ts @@ -21,6 +21,7 @@ function witnessed() { AGENTS[sessionId] ? ({ provider: AGENTS[sessionId] } as AgentSessionRecord) : null, childWorkOpen: () => false, childWork: () => undefined, + startAnswered: () => undefined, capsule: { record: async (markers) => void recorded.push([...markers]) }, teardownId: 'teardown', now: () => 1, diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-restart-witnesses.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-restart-witnesses.ts index 4453a250cce5..bf3fe842e0a7 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-restart-witnesses.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-restart-witnesses.ts @@ -34,6 +34,7 @@ export function createStructuredAgentSessionRestartWitnesses(deps: { getRecord: (sessionId: string) => AgentSessionRecord | null childWork: WorkingAtStopInput['childWork'] childWorkOpen: WorkingAtStopInput['childWorkOpen'] + startAnswered: NonNullable capsule?: Pick teardownId: string now: () => number @@ -74,6 +75,7 @@ export function createStructuredAgentSessionRestartWitnesses(deps: { getRecord: deps.getRecord, childWork: deps.childWork, childWorkOpen: deps.childWorkOpen, + startAnswered: deps.startAnswered, trigger, teardownId: deps.teardownId, now: deps.now() diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-working-at-teardown.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-working-at-teardown.ts index 3df8570ca843..dded754b3dbc 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-working-at-teardown.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-working-at-teardown.ts @@ -193,6 +193,8 @@ export function structuredAgentSessionWorkingAtStop(input: { childWork: (sessionId: string) => readonly AgentChildWorkView[] | undefined /** Child work the provider reported running and not over, recorded or not. */ childWorkOpen: (sessionId: string) => boolean + /** False when the session's live child never answered its start; the exit reads the same fact. */ + startAnswered?: (sessionId: string) => boolean | undefined trigger: AgentSessionResumeTrigger /** Stable teardown identity for continuation deduplication, not launch ancestry. */ teardownId: string @@ -206,7 +208,8 @@ export function structuredAgentSessionWorkingAtStop(input: { const current = agentSessionCurrentContextRows(snapshot.items, snapshot.submissions) // A queued message reached no agent, and one handed to a start that never answered ran nowhere: // neither is work to resume. The next open or the exit keeps it as unsent. - const unanswered = session.child.phase === 'starting' + const unanswered = + session.child.phase === 'starting' || input.startAnswered?.(sessionId) === false const handedOver = current.submissions.filter( (submission) => !isQueuedAgentJournalSubmission(submission) && From be9be12bcbb0454e3bce48406612a1edac56579f Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Mon, 19 Oct 2026 02:51:38 -0700 Subject: [PATCH 4/4] Keep historical chat fixtures aligned with their readers --- config/scripts/vitest-sqlite-runtime-files.mjs | 1 + .../agent-session-handle-replacement-downgrade.unit.test.ts | 6 +++++- .../cross-version-registered-agents.unit.test.ts | 4 +++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/config/scripts/vitest-sqlite-runtime-files.mjs b/config/scripts/vitest-sqlite-runtime-files.mjs index 66873733aadf..4d0664ed3a6b 100644 --- a/config/scripts/vitest-sqlite-runtime-files.mjs +++ b/config/scripts/vitest-sqlite-runtime-files.mjs @@ -219,6 +219,7 @@ export const SQLITE_RUNTIME_INCLUDE = [ 'src/main/native-chat/agent-session-wire/structured-agent-session-wedged-profile-migration.test.ts', 'src/main/native-chat/agent-session-wire/structured-agent-session-wire-admission.test.ts', 'src/main/native-chat/agent-session-wire/structured-conversation-command.test.ts', + 'src/main/native-chat/agent-session-wire/structured-conversation-compaction.test.ts', 'src/main/opencode/opencode-acp-stored-messages.test.ts', 'src/main/orcad/orcad-automations.test.ts', 'src/main/orcad/orcad-local-serve-selection.test.ts', diff --git a/tests/e2e/cross-version-wire/agent-session-handle-replacement-downgrade.unit.test.ts b/tests/e2e/cross-version-wire/agent-session-handle-replacement-downgrade.unit.test.ts index ee103350cdf0..23b47fd3c5c9 100644 --- a/tests/e2e/cross-version-wire/agent-session-handle-replacement-downgrade.unit.test.ts +++ b/tests/e2e/cross-version-wire/agent-session-handle-replacement-downgrade.unit.test.ts @@ -57,6 +57,8 @@ function neutralRow(chain: AgentSessionProviderHandleLink[]): unknown { return JSON.parse( JSON.stringify({ ...fixture, + // Historical leases keep the checkpoint so old readers reach handle validation. + lease: { ...fixture.lease, journalCheckpoint: null }, provider: 'grok', accountHome: { variable: 'GROK_HOME', path: '/home/user/.grok' }, providerHandleChain: encodePersistedAgentSessionProviderHandleChain(chain) @@ -94,7 +96,7 @@ function legacyRow(provider: 'claude' | 'codex') { const fixture = agentSessionRecordFixture( agentSessionLeaseFixture({ sessionId: `chat-${provider}` }) ) - return encodeAgentSessionRecord({ + const stored = encodeAgentSessionRecord({ ...fixture, provider, accountHome: @@ -111,6 +113,8 @@ function legacyRow(provider: 'claude' | 'codex') { } ] }) + // Use the same historical lease format as the neutral rows above. + return { ...stored, lease: { ...stored.lease, journalCheckpoint: null } } } // A replacement is stored as it is held, which older builds would refuse; it is safe only because diff --git a/tests/e2e/cross-version-wire/cross-version-registered-agents.unit.test.ts b/tests/e2e/cross-version-wire/cross-version-registered-agents.unit.test.ts index 9ed7e4d56e13..4bca219b763e 100644 --- a/tests/e2e/cross-version-wire/cross-version-registered-agents.unit.test.ts +++ b/tests/e2e/cross-version-wire/cross-version-registered-agents.unit.test.ts @@ -114,8 +114,10 @@ function registersAgents(build: AgentSessionWireBuild): boolean { } function baselineReadsRecord(record: AgentSessionRecord): boolean { + const stored = encodeAgentSessionRecord(record) return baseline.isPersistedAgentSessionRecord( - encodeAgentSessionRecord(record), + // Historical leases keep the checkpoint so old readers reach agent validation. + { ...stored, lease: { ...stored.lease, journalCheckpoint: null } }, new Set(['claude', 'codex']) ) }