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/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-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-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-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-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-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..82a047610108 100644 --- a/src/main/runtime/agent-session-record-store.test.ts +++ b/src/main/runtime/agent-session-record-store.test.ts @@ -738,27 +738,36 @@ 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([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() + 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' || 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']) ) }