refactor(native-chat): keep the provider resume handle opaque to shared code (#24991)

* refactor(native-chat): keep the provider resume handle opaque to shared code

Shared structured-chat code parsed each provider's resume handle: Claude's
session id and branch leaf, Codex's thread id, through a 'claude' | 'codex'
union every new agent had to widen. The in-memory handle is now
{ transport, agent, nativeId, providerData? }: shared readers use nativeId,
lease and handle-chain checks compare transport and agent, and only the
Claude adapter reads its leaf (providerData).

Stored and wire forms are unchanged for Claude and Codex. One encoding
module writes their typed shapes and decodes both those and the neutral
shape a new transport uses, which an older build refuses as unreadable
rather than reading as Codex. Key and root strings, which fork seeds,
superseded creations and resume offers persist, stay byte-identical.

The journal's own handle type becomes the journal-row and attach-wire
encoding of the same handle, and the journal identity carries the
neutral handle (null before the provider proves one).

No user-visible change.

* fix(native-chat): derive journal-row provider handles from the journal identity

The journal row converter now takes the identity every caller already holds,
so a row's handle has one obvious constructor. Tests that wrote the in-memory
handle straight into journal rows now build it through that converter, and the
processless Claude fixture names a not-yet-proved handle as null.

* fix(native-chat): refuse a stored provider handle written in both forms

A typed Claude or Codex handle that also carries the neutral form's
transport, agent, native id or provider data named two identities; it
was read as Claude or Codex and the next write dropped the other one.
Such a row now stays unreadable and is set aside untouched.

* test(native-chat): use opaque handle in queued rejection fixture

* test(native-chat): share one Codex journal identity in the integration suite

Main grew the suite to the 800-line limit; the opaque-handle import pushed it
over. The two tests built the same identity inline.

* refactor(agent-session): name the handle's adapter state resumeCursor

Rename the neutral provider handle's providerData to resumeCursor before any
row persists the neutral form: it is an adapter-owned resume position (Claude's
transcript leaf), never identity. Claude/Codex stored and wire bytes are
unchanged; their typed shapes never carried the field.

State the stored-form contract (a handle's field set is closed; later per-link
data goes on the chain link, which every build preserves) and pin it with a
record round-trip test. Document that transport records the id space the
native id was minted in, which can differ from the agent's current transport.
This commit is contained in:
Brennan Benson
2026-10-05 14:48:12 -07:00
committed by GitHub
parent f1ed355d06
commit e817b0e237
206 changed files with 1698 additions and 644 deletions
@@ -27,8 +27,10 @@ const identity = {
workspaceId: 'fixture',
hostId: 'local',
agent: 'codex',
providerHandle: { kind: 'codex', threadId: 'thread' }
providerHandle: { transport: 'codex-app-server', agent: 'codex', nativeId: 'thread' }
}
// The baseline predates the neutral handle and takes its journal identity in the typed form.
const baselineIdentity = { ...identity, providerHandle: { kind: 'codex', threadId: 'thread' } }
const fixture = await mkdtemp(join(tmpdir(), 'orca-journal-replay-bench-'))
// Released in `finally`, newest first: an open SQLite handle blocks the fixture's removal on Windows.
const releases = []
@@ -75,7 +77,10 @@ async function openArm(arm) {
let journal
let database
if (arm === 'baseline') {
journal = await implementation.openAgentSessionJournal({ identity, journalDir: stateDirectory })
journal = await implementation.openAgentSessionJournal({
identity: baselineIdentity,
journalDir: stateDirectory
})
} else {
database = implementation.JournalHostDatabase.open(stateDirectory)
releases.push(() => database.close())
@@ -11,6 +11,10 @@ import {
assertLegacyAiVaultResumeCommandAllowed,
projectStructuredAiVaultSessions
} from './structured-session-ownership'
import {
claudeProviderHandle,
codexProviderHandle
} from '../../shared/agent-session-provider-handle-encoding'
const PROVIDER_SESSION = '019fd532-7c11-7a90-b6de-4e1a2c3d5f60'
@@ -93,6 +97,7 @@ function installOwnership(overrides: Partial<StructuredProviderSessionOwnership>
...overrides
}
const record = agentSessionRecordFixture(ownership.lease)
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the ownership read touches only `deps.store.listRecords`; the rest of the host is never reached.
setStructuredAgentSessionHost({
deps: {
store: {
@@ -105,7 +110,10 @@ function installOwnership(overrides: Partial<StructuredProviderSessionOwnership>
providerHandleChain: [
{
...record.providerHandleChain[0]!,
handle: { provider: ownership.provider, threadId: ownership.providerSessionId }
handle:
ownership.provider === 'claude'
? claudeProviderHandle(ownership.providerSessionId, null)
: codexProviderHandle(ownership.providerSessionId)
}
],
lease: { ...ownership.lease, sessionId: ownership.sessionId }
@@ -19,6 +19,7 @@ import {
import { createClaudeJournalTranslator } from './claude-structured-journal-translation'
import { openTestJournalHostDatabase } from '../native-chat/agent-session-journal/journal-host-database-test-support'
import { createStructuredAgentSessionLogger } from '../native-chat/agent-session-wire/structured-agent-session-logger'
import { codexProviderHandle } from '../../shared/agent-session-provider-handle-encoding'
const SWEEP_MS = 5
const RETRY_GAP_MS = 10 * 60_000
@@ -63,7 +64,7 @@ beforeEach(async () => {
acquisitionGeneration: 'generation-1',
link: {
linkId: `link-${fence}`,
handle: { provider: 'codex', threadId: THREAD },
handle: codexProviderHandle(THREAD),
origin: 'created',
mintedAtFence: fence,
observedAt: NOW
+2 -1
View File
@@ -13,13 +13,14 @@ import { unhandledProviderFrameJournalItem } from '../native-chat/agent-session-
import { createClaudeJournalTranslator } from './claude-structured-journal-translation'
import { openTestJournalHostDatabase } from '../native-chat/agent-session-journal/journal-host-database-test-support'
import { testEventSinkLogging } from '../native-chat/agent-session-wire/structured-agent-session-logger-test-support'
import { claudeProviderHandle } from '../../shared/agent-session-provider-handle-encoding'
const IDENTITY: AgentSessionJournalIdentity = {
sessionId: 'session-1',
workspaceId: 'workspace-1',
hostId: 'host-1',
agent: 'claude',
providerHandle: { kind: 'claude', sessionId: 'provider-1', leafUuid: 'leaf-1' }
providerHandle: claudeProviderHandle('provider-1', 'leaf-1')
}
/** A frame as Claude Code sends it while it retries a refused request. */
@@ -11,6 +11,7 @@ import { readAgentJournalTurn } from '../../shared/agent-session-turn-record'
import { bindClaudeContextUsageCapture } from './claude-context-usage'
import { createClaudeJournalTranslator } from './claude-structured-journal-translation'
import { testEventSinkLogging } from '../native-chat/agent-session-wire/structured-agent-session-logger-test-support'
import { claudeProviderHandle } from '../../shared/agent-session-provider-handle-encoding'
const journals = createTrackedJournalOpener()
let root: string
@@ -104,7 +105,7 @@ async function openJournal(): Promise<AgentSessionJournal> {
workspaceId: 'workspace-1',
hostId: 'local',
agent: 'claude',
providerHandle: { kind: 'claude', sessionId: 'claude-session', leafUuid: null }
providerHandle: claudeProviderHandle('claude-session', null)
},
now: () => 9_000,
stateDirectory: join(root, 'orca-session')
@@ -28,6 +28,7 @@ import {
} from './claude-context-usage-test-support'
import { createClaudeJournalTranslator } from './claude-structured-journal-translation'
import { testEventSinkLogging } from '../native-chat/agent-session-wire/structured-agent-session-logger-test-support'
import { claudeProviderHandle } from '../../shared/agent-session-provider-handle-encoding'
const SESSION = 'orca-session'
const journals = createTrackedJournalOpener()
@@ -49,7 +50,7 @@ async function openJournal(): Promise<AgentSessionJournal> {
workspaceId: 'workspace-1',
hostId: 'local',
agent: 'claude',
providerHandle: { kind: 'claude', sessionId: 'claude-session', leafUuid: null }
providerHandle: claudeProviderHandle('claude-session', null)
},
now: () => 9_000,
stateDirectory: join(root, SESSION)
@@ -8,13 +8,14 @@ import { createDeferredStructuredAgentSessionEventSink } from '../native-chat/ag
import { createClaudeJournalTranslator } from './claude-structured-journal-translation'
import { openTestJournalHostDatabase } from '../native-chat/agent-session-journal/journal-host-database-test-support'
import { testEventSinkLogging } from '../native-chat/agent-session-wire/structured-agent-session-logger-test-support'
import { claudeProviderHandle } from '../../shared/agent-session-provider-handle-encoding'
const IDENTITY: AgentSessionJournalIdentity = {
sessionId: 'session-1',
workspaceId: 'workspace-1',
hostId: 'host-1',
agent: 'claude',
providerHandle: { kind: 'claude', sessionId: 'provider-1', leafUuid: 'leaf-1' }
providerHandle: claudeProviderHandle('provider-1', 'leaf-1')
}
/** A frame as Claude Code 2.1.280 sends it: a transcript note at a render level. */
@@ -22,6 +22,7 @@ import type { ClaudeSession } from './claude-structured-session-state'
import { CLAUDE_STRUCTURED_BASE_OPTIONS } from './claude-structured-launch-resolution'
import { openTestJournalHostDatabase } from '../native-chat/agent-session-journal/journal-host-database-test-support'
import { testEventSinkLogging } from '../native-chat/agent-session-wire/structured-agent-session-logger-test-support'
import { claudeProviderHandle } from '../../shared/agent-session-provider-handle-encoding'
// These drive the real SDK against the scripted fake CLI, so every assertion is
// about the environment, argv and frames a real child actually saw.
@@ -380,7 +381,7 @@ describe('Claude stream-json connection', () => {
workspaceId: 'workspace-1',
hostId: 'host-1',
agent: 'claude',
providerHandle: { kind: 'claude', sessionId: SESSION_ID, leafUuid: 'leaf-1' }
providerHandle: claudeProviderHandle(SESSION_ID, 'leaf-1')
},
database: openTestJournalHostDatabase(join(scenario.cwd, 'journal')),
now: () => 1_700_000_000_000,
@@ -1,3 +1,4 @@
import { claudeProviderHandle } from '../../shared/agent-session-provider-handle-encoding'
import {
AgentSessionAcquisitionExitUnprovenError,
AgentSessionPreSpawnError,
@@ -72,11 +73,10 @@ export async function resolveClaudeAcquisitionLaunch(args: {
const launchIdentity = resumeSession
? {
...input.identity,
providerHandle: {
kind: 'claude' as const,
sessionId: resumeSession.providerSessionId,
leafUuid: resumeSession.turnEndLeafUuid
}
providerHandle: claudeProviderHandle(
resumeSession.providerSessionId,
resumeSession.turnEndLeafUuid
)
}
: input.identity
const launch = await deps
@@ -15,6 +15,7 @@ import {
fakeClaude,
identityFor
} from './claude-structured-session-test-support'
import { claudeProviderHandle } from '../../shared/agent-session-provider-handle-encoding'
const SESSION_ID = 'orca-session-auth'
const IDENTITY = { sessionId: SESSION_ID } as Parameters<
@@ -60,11 +61,10 @@ function realResolverAdapter(
claude: ReturnType<typeof fakeClaude>,
authSwitchSettleTimeoutMs: number
): ClaudeStructuredSessionAdapter {
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the resolver reads only the chain head's handle from this partial record.
const resumable = {
...record(),
providerHandleChain: [
{ handle: { provider: 'claude', sessionId: PROVIDER_SESSION_ID, leafUuid: null } }
]
providerHandleChain: [{ handle: claudeProviderHandle(PROVIDER_SESSION_ID, null) }]
} as unknown as AgentSessionRecord
return new ClaudeStructuredSessionAdapter({
resolveLaunch: createClaudeStructuredLaunchResolver({
@@ -13,6 +13,7 @@ import {
claudeProviderHistoryWindowFromJsonl,
resolveClaudeProviderHistoryWindow
} from './claude-structured-history-window'
import { claudeProviderHandle } from '../../shared/agent-session-provider-handle-encoding'
const PROVIDER_SESSION = 'provider-1'
const ORCA_SESSION = 'session-1'
@@ -82,7 +83,7 @@ describe('claudeProviderHistoryWindowFromJsonl', () => {
workspaceId: 'workspace-1',
hostId: 'host-1',
agent: 'claude',
providerHandle: { kind: 'claude', sessionId: PROVIDER_SESSION, leafUuid: 'anchor' }
providerHandle: claudeProviderHandle(PROVIDER_SESSION, 'anchor')
},
accountHomePath: accountHome,
hasLiveSession: false
@@ -16,6 +16,11 @@
import { join } from 'node:path'
import type { AgentSessionJournalIdentity } from '../../shared/agent-session-journal-types'
import {
CLAUDE_STRUCTURED_HANDLE_NAMESPACE,
claudeProviderHandleLeafUuid,
isAgentSessionProviderHandleInNamespace
} from '../../shared/agent-session-provider-handle-encoding'
import { resolveSessionFilePath } from '../native-chat/session-file-resolver'
import type {
ProviderHistoryItem,
@@ -242,10 +247,13 @@ export async function resolveClaudeProviderHistoryWindow(input: {
hasLiveSession: boolean
}): Promise<ProviderHistoryWindow | null> {
const handle = input.identity.providerHandle
if (handle.kind !== 'claude') {
if (
!handle ||
!isAgentSessionProviderHandleInNamespace(handle, CLAUDE_STRUCTURED_HANDLE_NAMESPACE)
) {
return null
}
const transcriptPath = await resolveSessionFilePath('claude', handle.sessionId, {
const transcriptPath = await resolveSessionFilePath('claude', handle.nativeId, {
claudeProjectsDir: join(input.accountHomePath, 'projects')
})
if (!transcriptPath) {
@@ -253,8 +261,8 @@ export async function resolveClaudeProviderHistoryWindow(input: {
}
return readClaudeProviderHistoryWindow({
transcriptPath,
providerSessionId: handle.sessionId,
previousLeafUuid: handle.leafUuid,
providerSessionId: handle.nativeId,
previousLeafUuid: claudeProviderHandleLeafUuid(handle),
sessionId: input.identity.sessionId,
turnInFlight: input.hasLiveSession
})
@@ -24,6 +24,7 @@ import { readAgentJournalTurn } from '../../shared/agent-session-turn-record'
import { createClaudeJournalTranslator } from './claude-structured-journal-translation'
import { openTestJournalHostDatabase } from '../native-chat/agent-session-journal/journal-host-database-test-support'
import { testEventSinkLogging } from '../native-chat/agent-session-wire/structured-agent-session-logger-test-support'
import { claudeProviderHandle } from '../../shared/agent-session-provider-handle-encoding'
function sinkState() {
const items: { identity: AgentJournalItemIdentity; body: AgentJournalItemBody }[] = []
@@ -180,7 +181,7 @@ const JOURNAL_IDENTITY: AgentSessionJournalIdentity = {
workspaceId: 'workspace-1',
hostId: 'host-1',
agent: 'claude',
providerHandle: { kind: 'claude', sessionId: 'claude-session', leafUuid: 'leaf-1' }
providerHandle: claudeProviderHandle('claude-session', 'leaf-1')
}
let journalRoot = ''
@@ -16,6 +16,7 @@ import {
createClaudeStructuredLaunchResolver
} from './claude-structured-launch-resolution'
import { claudeStructuredPermissionModeForSettings } from './claude-structured-permission-mode'
import { claudeProviderHandle } from '../../shared/agent-session-provider-handle-encoding'
const SESSION_ID = 'orca-session-1'
const IDENTITY = { sessionId: SESSION_ID } as Parameters<
@@ -41,7 +42,7 @@ function record(overrides: Partial<AgentSessionRecord> = {}): AgentSessionRecord
function identityAt(leafUuid: string | null): typeof IDENTITY {
return {
...IDENTITY,
providerHandle: { kind: 'claude', sessionId: 'provider-current', leafUuid }
providerHandle: claudeProviderHandle('provider-current', leafUuid)
}
}
@@ -100,8 +101,9 @@ const WSL_ONLY_NORMALIZED: ClaudeManagedAccountGateSettings = {
}
const RESUMABLE = record({
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the resolver reads only each link's handle, so the link's other fields stay unset.
providerHandleChain: [
{ handle: { provider: 'claude', sessionId: 'provider-current', leafUuid: 'leaf-current' } }
{ handle: claudeProviderHandle('provider-current', 'leaf-current') }
] as AgentSessionRecord['providerHandleChain']
})
@@ -136,14 +138,11 @@ describe('claude structured launch resolution', () => {
it('resumes the durable chain head by session id and carries its leaf as bookkeeping', async () => {
const launch = await resolverFor(
record({
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the resolver reads only each link's handle, so the link's other fields stay unset.
providerHandleChain: [
{ handle: { provider: 'claude', sessionId: 'provider-old', leafUuid: 'leaf-old' } },
{ handle: claudeProviderHandle('provider-old', 'leaf-old') },
{
handle: {
provider: 'claude',
sessionId: 'provider-current',
leafUuid: 'leaf-current'
}
handle: claudeProviderHandle('provider-current', 'leaf-current')
}
] as AgentSessionRecord['providerHandleChain']
})
@@ -194,7 +193,7 @@ describe('claude structured launch resolution', () => {
resolve({
identity: {
...IDENTITY,
providerHandle: { kind: 'claude', sessionId: 'provider-other', leafUuid: 'leaf-current' }
providerHandle: claudeProviderHandle('provider-other', 'leaf-current')
}
})
).rejects.toThrow('durable resume identity changed before spawn')
@@ -203,13 +202,10 @@ describe('claude structured launch resolution', () => {
it('keeps session-only resume when the durable handle has no leaf', async () => {
const launch = await resolverFor(
record({
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the resolver reads only each link's handle, so the link's other fields stay unset.
providerHandleChain: [
{
handle: {
provider: 'claude',
sessionId: 'provider-current',
leafUuid: null
}
handle: claudeProviderHandle('provider-current', null)
}
] as AgentSessionRecord['providerHandleChain']
})
@@ -226,7 +222,7 @@ describe('claude structured launch resolution', () => {
record({
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the resolver reads only each link's handle.
providerHandleChain: [
{ handle: { provider: 'claude', sessionId: 'provider-current', leafUuid: null } }
{ handle: claudeProviderHandle('provider-current', null) }
] as AgentSessionRecord['providerHandleChain']
}),
undefined,
@@ -5,7 +5,11 @@ import type {
PermissionMode
} from '@anthropic-ai/claude-agent-sdk'
import type { AgentSessionJournalIdentity } from '../../shared/agent-session-journal-types'
import { agentSessionProviderHandleChainHead } from '../../shared/agent-session-provider-handle'
import {
agentSessionProviderHandleChainHead,
agentSessionProviderHandleRoot
} from '../../shared/agent-session-provider-handle'
import { claudeProviderHandleLeafUuid } from '../../shared/agent-session-provider-handle-encoding'
import { LOCAL_EXECUTION_HOST_ID } from '../../shared/execution-host'
import { withCliRuntimeOnPath } from '../../shared/node-cli-command-resolution'
import { structuredSessionChildIdentityEnv } from '../runtime/structured-session-child-identity-env'
@@ -260,24 +264,25 @@ export function createClaudeStructuredLaunchResolver(
gate && hasWslBoundClaudeAccount(gate) ? { reason: 'managedAccountUnsupported' } : {}
)
}
const head = agentSessionProviderHandleChainHead(record.providerHandleChain)
// A Claude record's chain holds only Claude handles; the record store refuses anything else.
const head = agentSessionProviderHandleChainHead(record.providerHandleChain)?.handle ?? null
if (
head?.handle.provider === 'claude' &&
(identity.providerHandle.kind !== 'claude' ||
identity.providerHandle.sessionId !== head.handle.sessionId)
head &&
(!identity.providerHandle ||
agentSessionProviderHandleRoot(identity.providerHandle) !==
agentSessionProviderHandleRoot(head))
) {
throw new Error('claude durable resume identity changed before spawn')
}
const providerSessionId =
head?.handle.provider === 'claude'
? head.handle.sessionId
: claudeSessionIdForOrcaSession(identity.sessionId)
const continuesChain = head?.handle.provider === 'claude'
const providerSessionId = head
? head.nativeId
: claudeSessionIdForOrcaSession(identity.sessionId)
const continuesChain = head !== null
// A start that failed before its first turn wrote no transcript, and `--resume` of an absent
// one exits; launch that id fresh instead. With a transcript, `--session-id` would collide.
const resumesTranscript =
head?.handle.provider === 'claude' &&
(head.handle.leafUuid !== null ||
head !== null &&
(claudeProviderHandleLeafUuid(head) !== null ||
(await (deps.hasTranscript ?? claudeTranscriptExists)({
providerSessionId,
claudeConfigDir: record.accountHome.path
@@ -308,8 +313,7 @@ export function createClaudeStructuredLaunchResolver(
env,
claudeConfigDir: record.accountHome.path,
providerSessionId,
resumeLeafUuid:
resumesTranscript && head?.handle.provider === 'claude' ? head.handle.leafUuid : null,
resumeLeafUuid: resumesTranscript && head ? claudeProviderHandleLeafUuid(head) : null,
resumesTranscript,
continuesChain
}
@@ -1,12 +1,13 @@
import { describe, expect, it, vi } from 'vitest'
import { CLAUDE_SPAWN_TOKEN_ENV, claudeProcessIdentity } from './claude-structured-owner-identity'
import { claudeProviderHandle } from '../../shared/agent-session-provider-handle-encoding'
const IDENTITY = {
sessionId: 'session-identity',
workspaceId: 'workspace-1',
hostId: 'local',
agent: 'claude' as const,
providerHandle: { kind: 'claude' as const, sessionId: 'session-1', leafUuid: 'leaf-1' }
providerHandle: claudeProviderHandle('session-1', 'leaf-1')
}
describe('claude structured owner identity', () => {
@@ -1,5 +1,6 @@
import type { AgentSessionJournalIdentity } from '../../shared/agent-session-journal-types'
import type { AgentSessionProviderHandleLink } from '../../shared/agent-session-provider-handle'
import { claudeProviderHandle } from '../../shared/agent-session-provider-handle-encoding'
import type { AgentSessionProcessIdentity } from '../../shared/agent-session-record'
import { readProcessStartTimeMs } from '../runtime/agent-session-process-identity-probe'
@@ -16,7 +17,7 @@ export function claudeProviderHandleLink(input: {
linkId:
input.linkId ??
`claude-${input.fence}-${input.sessionId}-${input.leafUuid ?? 'empty'}`.slice(0, 128),
handle: { provider: 'claude', sessionId: input.sessionId, leafUuid: input.leafUuid },
handle: claudeProviderHandle(input.sessionId, input.leafUuid),
origin: input.origin ?? (input.resumed ? 'resumed' : 'created'),
mintedAtFence: input.fence,
observedAt: input.observedAt
@@ -12,13 +12,14 @@ import { createClaudeJournalTranslator } from './claude-structured-journal-trans
import type { ClaudeStructuredSessionEvent } from './claude-structured-session-state'
import { openTestJournalHostDatabase } from '../native-chat/agent-session-journal/journal-host-database-test-support'
import { testEventSinkLogging } from '../native-chat/agent-session-wire/structured-agent-session-logger-test-support'
import { claudeProviderHandle } from '../../shared/agent-session-provider-handle-encoding'
const IDENTITY: AgentSessionJournalIdentity = {
sessionId: 'session-1',
workspaceId: 'workspace-1',
hostId: 'host-1',
agent: 'claude',
providerHandle: { kind: 'claude', sessionId: 'provider-1', leafUuid: 'leaf-1' }
providerHandle: claudeProviderHandle('provider-1', 'leaf-1')
}
let root = ''
@@ -27,6 +27,7 @@ import {
realClaudeCommand,
realClaudeLaunchHome
} from './claude-real-cli-availability-test-support'
import { claudeProviderHandle } from '../../shared/agent-session-provider-handle-encoding'
const SESSION_ID = 'real-cli-fold'
// Pins the live proof order (SessionStart hook frame before system/init) and lets the
@@ -42,7 +43,7 @@ function identity(providerSessionId: string): AgentSessionJournalIdentity {
workspaceId: 'real-cli-fold-workspace',
hostId: 'local',
agent: 'claude',
providerHandle: { kind: 'claude', sessionId: providerSessionId, leafUuid: null }
providerHandle: claudeProviderHandle(providerSessionId, null)
}
}
@@ -21,6 +21,7 @@ import {
type ClaudeStructuredSessionEvent
} from './claude-structured-session-adapter'
import type { ClaudeStructuredSessionAdapterDeps } from './claude-structured-session-state'
import { claudeProviderHandle } from '../../shared/agent-session-provider-handle-encoding'
const command = realClaudeCommand
const suiteTitle = `Claude structured real CLI handshake${realClaudeCliGate.skipReason ? ` (skipped: ${realClaudeCliGate.skipReason})` : ''}`
@@ -66,7 +67,7 @@ function identity(providerSessionId: string): AgentSessionJournalIdentity {
workspaceId: 'real-cli-workspace',
hostId: 'local',
agent: 'claude',
providerHandle: { kind: 'claude', sessionId: providerSessionId, leafUuid: null }
providerHandle: claudeProviderHandle(providerSessionId, null)
}
}
@@ -158,13 +159,9 @@ describe.skipIf(!realClaudeAvailable)(suiteTitle, () => {
event.type === 'message' ? [event.message.subtype] : []
)
expect(acquisition.link.handle).toMatchObject({
provider: 'claude',
sessionId: providerSessionId,
// Init/SessionStart UUIDs are protocol frames, not resumable
// main-transcript leaves; no cursor exists before the first user turn.
leafUuid: null
})
// Init/SessionStart UUIDs are protocol frames, not resumable
// main-transcript leaves; no cursor exists before the first user turn.
expect(acquisition.link.handle).toEqual(claudeProviderHandle(providerSessionId, null))
expect(observedSubtypes).toContain('hook_started')
expect(adapter.readCommands('real-cli-handshake')).toContainEqual(
expect.objectContaining({
@@ -88,7 +88,7 @@ describe('Claude resume point is the last completed turn on every exit path', ()
spawnToken: 'spawn-7',
events: recordingJournalSink()
})
expect(acquisition.link.handle).toMatchObject({ leafUuid: 'a3' })
expect(acquisition.link.handle).toMatchObject({ resumeCursor: 'a3' })
await expect(adapter.closeSession('session-1')).resolves.toBe(true)
expect(persisted).toEqual([expect.objectContaining({ leafUuid: 'a3' })])
})
@@ -13,7 +13,7 @@ const IDENTITY: AgentSessionJournalIdentity = {
workspaceId: 'workspace-1',
hostId: 'local',
agent: 'claude',
providerHandle: { kind: 'opaque', agent: 'claude', value: 'pending' }
providerHandle: null
}
describe('Claude structured processless acquisition', () => {
@@ -24,6 +24,7 @@ import {
type FakeConnection
} from './claude-structured-session-test-support'
import { invokeCanUseTool } from './claude-can-use-tool-test-support'
import { claudeProviderHandle } from '../../shared/agent-session-provider-handle-encoding'
describe('ClaudeStructuredSessionAdapter.acquire', () => {
it('pins the account and proves init without treating the system-frame uuid as a chain leaf', async () => {
@@ -57,7 +58,7 @@ describe('ClaudeStructuredSessionAdapter.acquire', () => {
})
expect(acquisition.link).toEqual({
linkId: `claude-7-${PROVIDER_SESSION_ID}-empty`,
handle: { provider: 'claude', sessionId: PROVIDER_SESSION_ID, leafUuid: null },
handle: claudeProviderHandle(PROVIDER_SESSION_ID, null),
origin: 'created',
mintedAtFence: 7,
observedAt: 1_700_000_000_500
@@ -424,11 +425,7 @@ describe('ClaudeStructuredSessionAdapter.acquire', () => {
spawnToken: 'spawn-9'
})
expect(acquisition.link.handle).toEqual({
provider: 'claude',
sessionId: PROVIDER_SESSION_ID,
leafUuid: null
})
expect(acquisition.link.handle).toEqual(claudeProviderHandle(PROVIDER_SESSION_ID, null))
expect(events[0]).toMatchObject({
type: 'message',
message: { subtype: 'hook_started', hook_name: 'SessionStart:startup' }
@@ -476,11 +473,9 @@ describe('ClaudeStructuredSessionAdapter.acquire', () => {
spawnToken: 'spawn-9'
})
expect(acquisition.link.origin).toBe('resumed')
expect(acquisition.link.handle).toEqual({
provider: 'claude',
sessionId: PROVIDER_SESSION_ID,
leafUuid: 'leaf-before'
})
expect(acquisition.link.handle).toEqual(
claudeProviderHandle(PROVIDER_SESSION_ID, 'leaf-before')
)
const wrongClaude = fakeClaude({ initSessionId: 'different-session' })
await expect(endedAtStartup(wrongClaude)).resolves.toMatchObject({
@@ -13,6 +13,10 @@ import {
tick
} from './claude-structured-session-test-support'
import { invokeCanUseTool } from './claude-can-use-tool-test-support'
import {
claudeProviderHandle,
claudeProviderHandleLeafUuid
} from '../../shared/agent-session-provider-handle-encoding'
describe('ClaudeStructuredSessionAdapter close and exit recovery', () => {
it('shares concurrent close finalization and emits lifecycle once', async () => {
@@ -296,9 +300,9 @@ describe('ClaudeStructuredSessionAdapter close and exit recovery', () => {
let durableLeafUuid: string | null = null
const resolveLaunch = vi.fn(async ({ identity }) => {
if (
identity.providerHandle.kind !== 'claude' ||
identity.providerHandle.sessionId !== PROVIDER_SESSION_ID ||
identity.providerHandle.leafUuid !== durableLeafUuid
!identity.providerHandle ||
identity.providerHandle.nativeId !== PROVIDER_SESSION_ID ||
claudeProviderHandleLeafUuid(identity.providerHandle) !== durableLeafUuid
) {
throw new Error('claude durable resume identity changed before spawn')
}
@@ -385,11 +389,7 @@ describe('ClaudeStructuredSessionAdapter close and exit recovery', () => {
const replacement = await adapter.acquire({
identity: {
...identityFor(),
providerHandle: {
kind: 'claude',
sessionId: PROVIDER_SESSION_ID,
leafUuid: 'observed-retained-leaf'
}
providerHandle: claudeProviderHandle(PROVIDER_SESSION_ID, 'observed-retained-leaf')
},
fence: 8,
spawnToken: 'spawn-10',
@@ -411,11 +411,7 @@ describe('ClaudeStructuredSessionAdapter close and exit recovery', () => {
expect(resolveLaunch).toHaveBeenNthCalledWith(2, {
identity: {
...identityFor(),
providerHandle: {
kind: 'claude',
sessionId: PROVIDER_SESSION_ID,
leafUuid: 'observed-retained-leaf'
}
providerHandle: claudeProviderHandle(PROVIDER_SESSION_ID, 'observed-retained-leaf')
}
})
expect(oldPrompt.settled()).toBe(true)
@@ -432,11 +428,7 @@ describe('ClaudeStructuredSessionAdapter close and exit recovery', () => {
}
])
expect(replacement.link).toMatchObject({
handle: {
provider: 'claude',
sessionId: PROVIDER_SESSION_ID,
leafUuid: 'observed-retained-leaf'
},
handle: claudeProviderHandle(PROVIDER_SESSION_ID, 'observed-retained-leaf'),
origin: 'resumed',
mintedAtFence: 8
})
@@ -15,6 +15,7 @@ import {
type ClaudeStructuredSessionEvent
} from './claude-structured-session-adapter'
import type { StructuredAgentSessionEventSink } from '../native-chat/agent-session-wire/structured-agent-session-event-sink'
import { claudeProviderHandle } from '../../shared/agent-session-provider-handle-encoding'
export const PROVIDER_SESSION_ID = '819cf9f8-e43c-4ad7-b50f-54aa158a726a'
@@ -30,7 +31,7 @@ export function identityFor(sessionId = 'session-1'): AgentSessionJournalIdentit
workspaceId: 'workspace-1',
hostId: 'host-1',
agent: 'claude',
providerHandle: { kind: 'claude', sessionId: PROVIDER_SESSION_ID, leafUuid: null }
providerHandle: claudeProviderHandle(PROVIDER_SESSION_ID, null)
}
}
@@ -6,7 +6,7 @@ import {
import type { AgentSessionRecord } from '../../shared/agent-session-record'
import { LOCAL_EXECUTION_HOST_ID } from '../../shared/execution-host'
import type { AgentSessionRecordStore } from '../runtime/agent-session-record-store'
import { reviseAgentSessionClaudeResumePoint } from '../runtime/agent-session-provider-handle-transition'
import { reviseAgentSessionProviderResumePoint } from '../runtime/agent-session-provider-handle-transition'
import { createClaudeStructuredLaunchResolver } from './claude-structured-launch-resolution'
import {
ClaudeStructuredSessionAdapter,
@@ -20,6 +20,7 @@ import {
recordingJournalSink,
tick
} from './claude-structured-session-test-support'
import { claudeProviderHandle } from '../../shared/agent-session-provider-handle-encoding'
const FENCE = 7
@@ -36,14 +37,14 @@ function liveRecord(): AgentSessionRecord {
origin: 'created',
mintedAtFence: 1,
observedAt: 500,
handle: { provider: 'claude', sessionId: PROVIDER_SESSION_ID, leafUuid: null }
handle: claudeProviderHandle(PROVIDER_SESSION_ID, null)
},
{
linkId: 'published-link',
origin: 'resumed',
mintedAtFence: FENCE,
observedAt: 1_000,
handle: { provider: 'claude', sessionId: PROVIDER_SESSION_ID, leafUuid: 'resumed-at' }
handle: claudeProviderHandle(PROVIDER_SESSION_ID, 'resumed-at')
}
]
}
@@ -78,11 +79,10 @@ async function liveOwner(
persistResumePoint:
persistResumePoint ??
(async ({ providerSessionId, leafUuid, fence }) => {
store.record = reviseAgentSessionClaudeResumePoint({
store.record = reviseAgentSessionProviderResumePoint({
record: store.record,
fence,
providerSessionId,
leafUuid,
handle: claudeProviderHandle(providerSessionId, leafUuid),
now: 2_000
})
})
@@ -115,12 +115,12 @@ describe('Claude durable resume point at turn end', () => {
const { adapter, store, turn } = await liveOwner()
try {
await turn('u1', 'a1')
expect(store.record.providerHandleChain.at(-1)?.handle).toMatchObject({ leafUuid: 'a1' })
expect(store.record.providerHandleChain.at(-1)?.handle).toMatchObject({ resumeCursor: 'a1' })
await turn('u2', 'a2')
expect(store.record.providerHandleChain).toHaveLength(2)
expect(store.record.providerHandleChain.at(-1)).toMatchObject({
linkId: 'published-link',
handle: { leafUuid: 'a2' }
handle: { resumeCursor: 'a2' }
})
} finally {
await adapter.closeAll()
@@ -135,7 +135,7 @@ describe('Claude durable resume point at turn end', () => {
frame({ type: 'user', uuid: 'u3' })
await tick()
const head = store.record.providerHandleChain.at(-1)!.handle
expect(head).toMatchObject({ leafUuid: 'a2' })
expect(head).toMatchObject({ resumeCursor: 'a2' })
const resolve = createClaudeStructuredLaunchResolver({
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the resolver reads only getRecord from its store.
@@ -147,7 +147,7 @@ describe('Claude durable resume point at turn end', () => {
const launch = await resolve({
identity: {
...identityFor(store.record.sessionId),
providerHandle: { kind: 'claude', sessionId: PROVIDER_SESSION_ID, leafUuid: 'a2' }
providerHandle: claudeProviderHandle(PROVIDER_SESSION_ID, 'a2')
}
})
// Bookkeeping only: Claude continues from the end of its own conversation.
@@ -17,6 +17,7 @@ import { createDeferredStructuredAgentSessionEventSink } from '../native-chat/ag
import { createClaudeJournalTranslator } from './claude-structured-journal-translation'
import { claudeSubagentGroupBody, claudeSubagentGroupIdentity } from './claude-subagent-group-row'
import { testEventSinkLogging } from '../native-chat/agent-session-wire/structured-agent-session-logger-test-support'
import { claudeProviderHandle } from '../../shared/agent-session-provider-handle-encoding'
// Frame orders are real sessions', scrubbed. A resumed agent's frames still name its ORIGINAL
// spawn call while the announcement names the message call that resumed it, and a new provider
@@ -129,7 +130,7 @@ async function openJournal(): Promise<AgentSessionJournal> {
workspaceId: 'workspace-1',
hostId: 'local',
agent: 'claude',
providerHandle: { kind: 'claude', sessionId: 'claude-session', leafUuid: null }
providerHandle: claudeProviderHandle('claude-session', null)
},
now: () => 9_000,
stateDirectory: root
@@ -19,6 +19,7 @@ import {
import { createCodexJournalTranslator } from './codex-structured-journal-translation'
import { openTestJournalHostDatabase } from '../native-chat/agent-session-journal/journal-host-database-test-support'
import { createStructuredAgentSessionLogger } from '../native-chat/agent-session-wire/structured-agent-session-logger'
import { codexProviderHandle } from '../../shared/agent-session-provider-handle-encoding'
const SWEEP_MS = 5
const RETRY_GAP_MS = 10 * 60_000
@@ -61,7 +62,7 @@ beforeEach(async () => {
acquisitionGeneration: 'generation-1',
link: {
linkId: `link-${fence}`,
handle: { provider: 'codex', threadId: THREAD },
handle: codexProviderHandle(THREAD),
origin: 'created',
mintedAtFence: fence,
observedAt: NOW
@@ -3,13 +3,14 @@ import { describe, expect, it, vi } from 'vitest'
import type { AgentSessionJournalIdentity } from '../../shared/agent-session-journal-types'
import type { CodexAppServerConnection } from './codex-app-server-connection-types'
import { CodexStructuredSessionAdapter } from './codex-structured-session-adapter'
import { codexProviderHandle } from '../../shared/agent-session-provider-handle-encoding'
const IDENTITY: AgentSessionJournalIdentity = {
sessionId: 'session-1',
workspaceId: 'workspace-1',
hostId: 'host-1',
agent: 'codex',
providerHandle: { kind: 'codex', threadId: 'thread-1' }
providerHandle: codexProviderHandle('thread-1')
}
describe('Codex failed-acquisition exit proof', () => {
@@ -14,6 +14,7 @@ import type { StructuredAgentSessionEventSink } from '../native-chat/agent-sessi
import { CodexStructuredSessionAdapter } from './codex-structured-session-adapter'
import { codexTurnLifecycleFake } from './codex-turn-lifecycle-fake'
import type { CodexStructuredSessionAdapterDeps } from './codex-structured-session-state'
import { codexProviderHandle } from '../../shared/agent-session-provider-handle-encoding'
export const CODEX_TEST_THREAD_ID = 'thread-abc'
@@ -114,7 +115,7 @@ export async function acquiredCodexAdapter(input: {
workspaceId: 'ws-1',
hostId: 'host-1',
agent: 'codex',
providerHandle: { kind: 'codex', threadId: CODEX_TEST_THREAD_ID }
providerHandle: codexProviderHandle(CODEX_TEST_THREAD_ID)
}
await adapter.acquire({
identity,
@@ -15,6 +15,7 @@ import { readAgentSessionHistory } from '../native-chat/agent-session-wire/agent
import { createDeferredStructuredAgentSessionEventSink } from '../native-chat/agent-session-wire/structured-agent-session-event-sink'
import { CodexJournalGoals } from './codex-structured-journal-goals'
import { testEventSinkLogging } from '../native-chat/agent-session-wire/structured-agent-session-logger-test-support'
import { codexProviderHandle } from '../../shared/agent-session-provider-handle-encoding'
const THREAD = '01a08cc2-f96e-76d0-bb74-88b9bc0b03fc'
const IDENTITY: AgentSessionJournalIdentity = {
@@ -22,7 +23,7 @@ const IDENTITY: AgentSessionJournalIdentity = {
workspaceId: 'workspace-1',
hostId: 'host-1',
agent: 'codex',
providerHandle: { kind: 'codex', threadId: THREAD }
providerHandle: codexProviderHandle(THREAD)
}
let root: string | null = null
@@ -22,6 +22,7 @@ import { createCodexJournalTranslator } from './codex-structured-journal-transla
import type { CodexThreadItem } from './codex-thread-item-identity'
import { openTestJournalHostDatabase } from '../native-chat/agent-session-journal/journal-host-database-test-support'
import { testEventSinkLogging } from '../native-chat/agent-session-wire/structured-agent-session-logger-test-support'
import { codexProviderHandle } from '../../shared/agent-session-provider-handle-encoding'
const SESSION = 'session-codex-children'
const PARENT = 'thread-parent'
@@ -43,7 +44,7 @@ async function openJournal(root: string): Promise<AgentSessionJournal> {
workspaceId: 'workspace-1',
hostId: 'local',
agent: 'codex',
providerHandle: { kind: 'codex', threadId: PARENT }
providerHandle: codexProviderHandle(PARENT)
},
database: openTestJournalHostDatabase(root),
now: () => 1_000
@@ -27,6 +27,7 @@ import { createCodexStructuredNotificationRetry } from './codex-structured-notif
import type { CodexStructuredSessionEvent } from './codex-structured-session-adapter'
import type { CodexSession } from './codex-structured-session-state'
import { testEventSinkLogging } from '../native-chat/agent-session-wire/structured-agent-session-logger-test-support'
import { codexProviderHandle } from '../../shared/agent-session-provider-handle-encoding'
const SESSION_ID = 'session-1'
const THREAD_ID = 'thread-abc'
@@ -219,7 +220,7 @@ describe('codex turn lifecycle rows', () => {
workspaceId: 'workspace-1',
hostId: 'local',
agent: 'codex',
providerHandle: { kind: 'codex', threadId: THREAD_ID }
providerHandle: codexProviderHandle(THREAD_ID)
},
now: () => 9_000,
stateDirectory: join(root, SESSION_ID)
@@ -14,6 +14,7 @@ import { createTrackedJournalOpener } from '../native-chat/agent-session-journal
import { createDeferredStructuredAgentSessionEventSink } from '../native-chat/agent-session-wire/structured-agent-session-event-sink'
import { createCodexJournalTranslator } from './codex-structured-journal-translation'
import { testEventSinkLogging } from '../native-chat/agent-session-wire/structured-agent-session-logger-test-support'
import { codexProviderHandle } from '../../shared/agent-session-provider-handle-encoding'
const SESSION = 'session-codex-failed-turn'
const THREAD = 'thread-abc'
@@ -35,7 +36,7 @@ async function session() {
workspaceId: 'workspace-1',
hostId: 'local',
agent: 'codex',
providerHandle: { kind: 'codex', threadId: THREAD }
providerHandle: codexProviderHandle(THREAD)
},
stateDirectory: root,
now: () => 1_000
@@ -5,6 +5,7 @@ import { LOCAL_EXECUTION_HOST_ID } from '../../shared/execution-host'
import type { AgentSessionRecordStore } from '../runtime/agent-session-record-store'
import { createCodexStructuredLaunchResolver } from './codex-structured-launch-resolution'
import { codexStructuredPermissionPolicyForSettings } from './codex-structured-permission-policy'
import { codexProviderHandle } from '../../shared/agent-session-provider-handle-encoding'
const SESSION_ID = 'session-1'
const IDENTITY = { sessionId: SESSION_ID } as Parameters<
@@ -105,9 +106,10 @@ describe('codex structured launch resolution', () => {
it('resumes the last thread this session actually proved, not one a caller names', async () => {
const launch = await resolverFor(
record({
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the resolver reads only each link's handle, so the link's other fields stay unset.
providerHandleChain: [
{ handle: { provider: 'codex', threadId: 'thread-old' } },
{ handle: { provider: 'codex', threadId: 'thread-current' } }
{ handle: codexProviderHandle('thread-old') },
{ handle: codexProviderHandle('thread-current') }
] as AgentSessionRecord['providerHandleChain']
})
)({ identity: IDENTITY })
@@ -121,7 +123,7 @@ describe('codex structured launch resolution', () => {
mintedAtFence: number
): AgentSessionProviderHandleLink => ({
linkId: `link-${mintedAtFence}`,
handle: { provider: 'codex', threadId: 't' },
handle: codexProviderHandle('t'),
origin,
mintedAtFence,
observedAt: 1
@@ -204,8 +206,9 @@ describe('codex structured launch resolution', () => {
const resolveRollout = vi.fn(async () => '/home/work/.codex/sessions/rollout.jsonl')
const launch = await resolverFor(
record({
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the resolver reads only each link's handle, so the link's other fields stay unset.
providerHandleChain: [
{ handle: { provider: 'codex', threadId: 'thread-current' } }
{ handle: codexProviderHandle('thread-current') }
] as AgentSessionRecord['providerHandleChain']
}),
async (id) => `/repos/${id}`,
@@ -93,7 +93,8 @@ export function createCodexStructuredLaunchResolver(
// concern, and the permission posture they used to smuggle in is derived per acquisition.
const permissionPolicy = deps.resolvePermissionPolicy?.()
const head = agentSessionProviderHandleChainHead(record.providerHandleChain)
const resumeThreadId = head?.handle.provider === 'codex' ? head.handle.threadId : null
// A Codex record's chain holds only Codex handles; the record store refuses anything else.
const resumeThreadId = head?.handle.nativeId ?? null
// The same saved options every turn sends, so the thread and its turns name one model.
const model = record.options?.model
return {
@@ -1,12 +1,13 @@
import { describe, expect, it, vi } from 'vitest'
import { codexProcessIdentity, codexProviderHandleLink } from './codex-structured-owner-identity'
import { codexProviderHandle } from '../../shared/agent-session-provider-handle-encoding'
const IDENTITY = {
sessionId: 'session-identity',
workspaceId: 'workspace-1',
hostId: 'local',
agent: 'codex' as const,
providerHandle: { kind: 'codex' as const, threadId: 'thread-1' }
providerHandle: codexProviderHandle('thread-1')
}
describe('codex process identity', () => {
@@ -3,6 +3,7 @@ import {
agentSessionProviderHandleKey,
type AgentSessionProviderHandleLink
} from '../../shared/agent-session-provider-handle'
import { codexProviderHandle } from '../../shared/agent-session-provider-handle-encoding'
import type { AgentSessionProcessIdentity } from '../../shared/agent-session-record'
import { readProcessStartTimeMs } from '../runtime/agent-session-process-identity-probe'
@@ -91,16 +92,15 @@ export function codexProviderHandleLink(
): AgentSessionProviderHandleLink {
return {
linkId: input.linkId ?? `codex-${input.fence}-${input.threadId}`.slice(0, 128),
handle: { provider: 'codex', threadId: input.threadId },
handle: codexProviderHandle(input.threadId),
origin: input.origin ?? (input.resumed ? 'resumed' : 'created'),
mintedAtFence: input.fence,
observedAt: input.observedAt,
...(input.supersedesThreadId
? {
supersedesKey: agentSessionProviderHandleKey({
provider: 'codex',
threadId: input.supersedesThreadId
})
supersedesKey: agentSessionProviderHandleKey(
codexProviderHandle(input.supersedesThreadId)
)
}
: {})
}
@@ -37,6 +37,7 @@ import {
} from '../../renderer/src/components/native-chat/structured-agent-question-projection'
import { openTestJournalHostDatabase } from '../native-chat/agent-session-journal/journal-host-database-test-support'
import { createStructuredAgentSessionLogger } from '../native-chat/agent-session-wire/structured-agent-session-logger'
import { codexProviderHandle } from '../../shared/agent-session-provider-handle-encoding'
const CALLER = { callerKey: 'client-1' }
@@ -79,7 +80,7 @@ beforeEach(async () => {
},
link: {
linkId: `link-${fence}`,
handle: { provider: 'codex', threadId: THREAD },
handle: codexProviderHandle(THREAD),
origin: 'created',
mintedAtFence: fence,
observedAt: HOST_TEST_NOW
@@ -1,3 +1,7 @@
import {
CODEX_STRUCTURED_HANDLE_NAMESPACE,
isAgentSessionProviderHandleInNamespace
} from '../../shared/agent-session-provider-handle-encoding'
import {
AgentSessionAcquisitionRefusal,
AgentSessionPreSpawnError,
@@ -71,9 +75,11 @@ export async function acquireCodexStructuredSession(input: {
const { previousAttempt, attempt } = acquisitions.start(sessionId)
const acquisition = attempt.window
let unbindReadingControl: (() => void) | undefined
const provenHandle = acquireInput.identity.providerHandle
let primaryThreadId =
acquireInput.identity.providerHandle.kind === 'codex'
? acquireInput.identity.providerHandle.threadId
provenHandle &&
isAgentSessionProviderHandleInNamespace(provenHandle, CODEX_STRUCTURED_HANDLE_NAMESPACE)
? provenHandle.nativeId
: null
const subagentExecutions = new CodexSubagentExecutions()
const dispatchEchoes = createCodexDispatchEchoes()
@@ -14,6 +14,7 @@ import {
type CodexStructuredSessionEvent
} from './codex-structured-session-adapter'
import type { CodexStructuredSessionAdapterDeps } from './codex-structured-session-state'
import { codexProviderHandle } from '../../shared/agent-session-provider-handle-encoding'
export const THREAD_ID = 'thread-abc'
@@ -23,7 +24,7 @@ export function identityFor(sessionId: string): AgentSessionJournalIdentity {
workspaceId: 'ws-1',
hostId: 'host-1',
agent: 'codex',
providerHandle: { kind: 'codex', threadId: THREAD_ID }
providerHandle: codexProviderHandle(THREAD_ID)
}
}
@@ -16,6 +16,7 @@ import {
type CodexStructuredLaunch,
type CodexStructuredSessionEvent
} from './codex-structured-session-adapter'
import { codexProviderHandle } from '../../shared/agent-session-provider-handle-encoding'
const THREAD_ID = 'thread-abc'
@@ -25,7 +26,7 @@ function identityFor(sessionId: string): AgentSessionJournalIdentity {
workspaceId: 'ws-1',
hostId: 'host-1',
agent: 'codex',
providerHandle: { kind: 'codex', threadId: THREAD_ID }
providerHandle: codexProviderHandle(THREAD_ID)
}
}
@@ -20,6 +20,7 @@ import {
fakeCodex,
identityFor
} from './codex-structured-session-adapter-fixture'
import { codexProviderHandle } from '../../shared/agent-session-provider-handle-encoding'
describe('CodexStructuredSessionAdapter.acquire', () => {
it('starts a new thread and reports the process and link the lease will prove', async () => {
@@ -58,7 +59,7 @@ describe('CodexStructuredSessionAdapter.acquire', () => {
})
expect(acquisition.link).toEqual({
linkId: `codex-7-${THREAD_ID}`,
handle: { provider: 'codex', threadId: THREAD_ID },
handle: codexProviderHandle(THREAD_ID),
origin: 'created',
mintedAtFence: 7,
observedAt: 1_700_000_000_500
@@ -102,7 +103,7 @@ describe('CodexStructuredSessionAdapter.acquire', () => {
})
})
expect(acquisition.link.origin).toBe('resumed')
expect(acquisition.link.handle).toEqual({ provider: 'codex', threadId: 'thread-proven' })
expect(acquisition.link.handle).toEqual(codexProviderHandle('thread-proven'))
})
it('starts a thread in place of a creation Codex never saved, and says which it replaced', async () => {
@@ -132,7 +133,7 @@ describe('CodexStructuredSessionAdapter.acquire', () => {
])
expect(acquisition.link).toEqual({
linkId: `codex-9-${THREAD_ID}`,
handle: { provider: 'codex', threadId: THREAD_ID },
handle: codexProviderHandle(THREAD_ID),
origin: 'created',
supersedesKey: 'codex:"thread-unsaved"',
mintedAtFence: 9,
@@ -10,6 +10,7 @@ import { CodexStructuredSessionAdapter } from './codex-structured-session-adapte
import { CodexBackgroundTaskTracker } from './codex-background-task-tracker'
import type { CodexStructuredSessionEvent } from './codex-structured-session-state'
import type { StructuredAgentSessionEventSink } from '../native-chat/agent-session-wire/structured-agent-session-event-sink'
import { codexProviderHandle } from '../../shared/agent-session-provider-handle-encoding'
// Proves the host's child records are actually REACHED from provider traffic: the tracker is
// unit-tested separately, and a producer that is correct but unwired publishes nothing while
@@ -51,7 +52,7 @@ function identity(sessionId: string): AgentSessionJournalIdentity {
workspaceId: 'ws-1',
hostId: 'host-1',
agent: 'codex',
providerHandle: { kind: 'codex', threadId: THREAD_ID }
providerHandle: codexProviderHandle(THREAD_ID)
}
}
@@ -47,6 +47,7 @@ import {
type CodexStructuredSessionAdapterDeps,
type CodexStructuredSessionEvent
} from './codex-structured-session-adapter'
import { codexProviderHandle } from '../../shared/agent-session-provider-handle-encoding'
const THREAD_ID = 'thread-abc'
const USER_MESSAGE: AgentJournalMessageItem = {
@@ -69,7 +70,7 @@ function identity(): AgentSessionJournalIdentity {
workspaceId: 'ws-1',
hostId: 'host-1',
agent: 'codex',
providerHandle: { kind: 'codex', threadId: THREAD_ID }
providerHandle: codexProviderHandle(THREAD_ID)
}
}
@@ -17,6 +17,7 @@ import { CodexPromptRegistry } from './codex-structured-prompt-replies'
import type { CodexSession } from './codex-structured-session-state'
import type { StructuredAgentSessionAdapter } from '../native-chat/agent-session-wire/structured-agent-session-adapter'
import { StructuredAgentSessionAdapterRouter } from '../native-chat/agent-session-wire/structured-agent-session-adapter-router'
import { codexProviderHandle } from '../../shared/agent-session-provider-handle-encoding'
const THREAD = 'thread-1'
@@ -26,7 +27,7 @@ function identity(sessionId: string): AgentSessionJournalIdentity {
workspaceId: 'workspace-1',
hostId: 'host-1',
agent: 'codex',
providerHandle: { kind: 'codex', threadId: THREAD }
providerHandle: codexProviderHandle(THREAD)
}
}
@@ -8,6 +8,7 @@ import {
CodexStructuredSessionAdapter,
type CodexStructuredLaunch
} from './codex-structured-session-adapter'
import { codexProviderHandle } from '../../shared/agent-session-provider-handle-encoding'
const SESSION_ID = 'session-1'
const THREAD_ID = 'thread-1'
@@ -25,7 +26,7 @@ function identity(): AgentSessionJournalIdentity {
workspaceId: 'workspace-1',
hostId: 'host-1',
agent: 'codex',
providerHandle: { kind: 'codex', threadId: THREAD_ID }
providerHandle: codexProviderHandle(THREAD_ID)
}
}
@@ -26,13 +26,14 @@ import {
type ProviderHistoryWindow
} from './journal-submission-reconciler'
import { createTrackedJournalOpener } from './journal-host-database-test-support'
import { codexProviderHandle } from '../../../shared/agent-session-provider-handle-encoding'
const IDENTITY: AgentSessionJournalIdentity = {
sessionId: 'session-1',
workspaceId: 'ws-1',
hostId: 'host-1',
agent: 'codex',
providerHandle: { kind: 'codex', threadId: 'thread-1' }
providerHandle: codexProviderHandle('thread-1')
}
const TURN_ID = '019fd8ca-edbe-7c43-b231-4c7aea3a2d89'
@@ -14,6 +14,7 @@ import {
type AgentJournalItemIdentity,
type AgentSessionJournalIdentity
} from '../../../shared/agent-session-journal-types'
import { codexProviderHandle } from '../../../shared/agent-session-provider-handle-encoding'
import type Database from '../../sqlite/sync-database'
import type { openAgentSessionJournal } from './journal-store-factory'
import {
@@ -31,7 +32,7 @@ const IDENTITY: AgentSessionJournalIdentity = {
workspaceId: 'ws-1',
hostId: 'host-1',
agent: 'codex',
providerHandle: { kind: 'codex', threadId: 'thread-1' }
providerHandle: codexProviderHandle('thread-1')
}
const UNLOADABLE = {
@@ -11,13 +11,14 @@ import {
import { DEFAULT_JOURNAL_PAYLOAD_LIMITS } from './journal-payload-bounds'
import type { openAgentSessionJournal } from './journal-store-factory'
import { createTrackedJournalOpener } from './journal-host-database-test-support'
import { codexProviderHandle } from '../../../shared/agent-session-provider-handle-encoding'
const IDENTITY: AgentSessionJournalIdentity = {
sessionId: 'session-1',
workspaceId: 'ws-1',
hostId: 'host-1',
agent: 'codex',
providerHandle: { kind: 'codex', threadId: 'thread-1' }
providerHandle: codexProviderHandle('thread-1')
}
const HUGE = 'x'.repeat(4 * DEFAULT_JOURNAL_PAYLOAD_LIMITS.inlineHeadBytes)
@@ -19,19 +19,20 @@ import {
openTestJournalHostDatabase,
readTestJournalRows
} from './journal-host-database-test-support'
import { codexProviderHandle } from '../../../shared/agent-session-provider-handle-encoding'
const IDENTITY: AgentSessionJournalIdentity = {
sessionId: 'session-1',
workspaceId: 'ws-1',
hostId: 'host-1',
agent: 'codex',
providerHandle: { kind: 'codex', threadId: 'thread-1' }
providerHandle: codexProviderHandle('thread-1')
}
const PEER: AgentSessionJournalIdentity = {
...IDENTITY,
sessionId: 'session-peer',
providerHandle: { kind: 'codex', threadId: 'thread-peer' }
providerHandle: codexProviderHandle('thread-peer')
}
let root: string
@@ -10,6 +10,7 @@ import type {
AgentJournalTurnScope,
AgentSessionJournalIdentity
} from '../../../shared/agent-session-journal-types'
import { agentSessionJournalProviderHandle } from '../../../shared/agent-session-provider-handle-encoding'
import type { JournalHostDatabase } from './journal-host-database'
import type { JournalLoad } from './journal-open'
import { applyJournalRow, createJournalReducerState } from './journal-reducer'
@@ -56,7 +57,7 @@ export function replaceJournalEpoch(input: {
const epochRow: JournalRow = {
kind: 'epoch',
reason: input.reason,
providerHandle: input.identity.providerHandle,
providerHandle: agentSessionJournalProviderHandle(input.identity),
...journalRowBase(epoch, 1, input.fence, input.now())
}
const rows: JournalRow[] = [epochRow]
@@ -6,6 +6,7 @@
import { journalRowSchemaVersion } from '../../../shared/agent-session-journal-types'
import type { AgentSessionJournalIdentity } from '../../../shared/agent-session-journal-types'
import { agentSessionJournalProviderHandle } from '../../../shared/agent-session-provider-handle-encoding'
import type { JournalHostDatabase } from './journal-host-database'
import type { JournalLoad } from './journal-open'
import { applyJournalRow, createJournalReducerState } from './journal-reducer'
@@ -30,7 +31,7 @@ export function publishNewEpoch(input: {
const row: JournalRow = {
kind: 'epoch',
reason: input.reason,
providerHandle: input.identity.providerHandle,
providerHandle: agentSessionJournalProviderHandle(input.identity),
// Carries no body: an older host must keep reading a turn-free session past row 1.
v: journalRowSchemaVersion([]),
epoch: input.epoch,
@@ -25,13 +25,14 @@ import {
liveTestJournalRows,
SAVED_BY_NEWER_ORCA
} from './journal-host-database-test-support'
import { codexProviderHandle } from '../../../shared/agent-session-provider-handle-encoding'
const IDENTITY: AgentSessionJournalIdentity = {
sessionId: 'session-1',
workspaceId: 'ws-1',
hostId: 'host-1',
agent: 'codex',
providerHandle: { kind: 'codex', threadId: 'thread-1' }
providerHandle: codexProviderHandle('thread-1')
}
const DISCLOSURE_ITEM_ID = agentJournalItemKey(JOURNAL_FILE_FORMAT_REMNANT_DISCLOSURE_IDENTITY)
@@ -20,19 +20,20 @@ import {
readTestJournalRows
} from './journal-host-database-test-support'
import type Database from '../../sqlite/sync-database'
import { codexProviderHandle } from '../../../shared/agent-session-provider-handle-encoding'
const IDENTITY: AgentSessionJournalIdentity = {
sessionId: 'session-1',
workspaceId: 'ws-1',
hostId: 'local',
agent: 'codex',
providerHandle: { kind: 'codex', threadId: 'thread-1' }
providerHandle: codexProviderHandle('thread-1')
}
const OTHER: AgentSessionJournalIdentity = {
...IDENTITY,
sessionId: 'session-2',
providerHandle: { kind: 'codex', threadId: 'thread-2' }
providerHandle: codexProviderHandle('thread-2')
}
const SUBMISSION = {
@@ -8,10 +8,8 @@ import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { agentJournalItemKey } from '../../../shared/agent-session-journal-item-key'
import type {
AgentSessionJournalIdentity,
AgentSessionProviderHandle
} from '../../../shared/agent-session-journal-types'
import type { AgentSessionJournalIdentity } from '../../../shared/agent-session-journal-types'
import type { AgentSessionProviderHandle } from '../../../shared/agent-session-provider-handle'
import type { NativeChatMessage } from '../../../shared/native-chat-types'
import type * as TranscriptLineDecoders from '../transcript-line-decoders'
import { createLegacyIdentityTracker } from './journal-legacy-identity'
@@ -20,6 +18,10 @@ import { DEFAULT_JOURNAL_PAYLOAD_LIMITS } from './journal-payload-bounds'
import { openAgentSessionJournal } from './journal-store-factory'
import type { AgentSessionJournal } from './journal-store'
import { openTestJournalHostDatabase } from './journal-host-database-test-support'
import {
claudeProviderHandle,
codexProviderHandle
} from '../../../shared/agent-session-provider-handle-encoding'
// No shipped decoder emits a subagent roster, so the roster bounds are reached by standing one in.
const decodedClaudeOverride = vi.hoisted((): { message: NativeChatMessage | null } => ({
@@ -50,11 +52,11 @@ type ImportAgent = 'claude' | 'codex' | 'grok' | 'omp'
function providerHandle(agent: ImportAgent, sessionId: string): AgentSessionProviderHandle {
if (agent === 'claude') {
return { kind: 'claude', sessionId, leafUuid: null }
return claudeProviderHandle(sessionId, null)
}
return agent === 'codex'
? { kind: 'codex', threadId: sessionId }
: { kind: 'opaque', agent, value: sessionId }
? codexProviderHandle(sessionId)
: { transport: 'terminal-transcript', agent, nativeId: sessionId }
}
function identity(agent: ImportAgent, sessionId: string): AgentSessionJournalIdentity {
@@ -33,6 +33,7 @@ import { journalDirectoryFor, legacyJournalDatabaseFile } from './journal-paths'
import { importPerSessionJournal, previewPerSessionJournal } from './journal-per-session-import'
import { readJournalSessionEpoch, type JournalStoredRow } from './journal-row-table'
import { createStructuredAgentSessionLogger } from '../agent-session-wire/structured-agent-session-logger'
import { codexProviderHandle } from '../../../shared/agent-session-provider-handle-encoding'
vi.mock('node:fs', async (importOriginal) => {
const actual = await importOriginal<typeof NodeFs>()
@@ -44,7 +45,7 @@ const IDENTITY: AgentSessionJournalIdentity = {
workspaceId: 'ws-1',
hostId: 'local',
agent: 'codex',
providerHandle: { kind: 'codex', threadId: 'thread-1' }
providerHandle: codexProviderHandle('thread-1')
}
let root: string
@@ -16,6 +16,7 @@ import {
journalLifecycleBatchRowBuilder
} from './journal-row-builders'
import { createTrackedJournalOpener } from './journal-host-database-test-support'
import { codexProviderHandle } from '../../../shared/agent-session-provider-handle-encoding'
// A row's producer is fixed by the write that created it. A revision naming no
// producer — a settlement, a prompt answer, a reopen sweep — keeps it; a
@@ -170,7 +171,7 @@ describe('producer inheritance across a reopen', () => {
workspaceId: 'workspace-1',
hostId: 'local',
agent: 'codex',
providerHandle: { kind: 'codex', threadId: 'root' }
providerHandle: codexProviderHandle('root')
},
stateDirectory: root,
now: () => 1_000
@@ -8,6 +8,7 @@ import { afterEach, beforeEach, expect, it } from 'vitest'
import { agentSessionFailureFact } from '../../../shared/agent-session-failure'
import { agentSessionFailureWords } from '../../../shared/agent-session-failure-words'
import { agentJournalSubmissionKey } from '../../../shared/agent-session-journal-item-key'
import { claudeProviderHandle } from '../../../shared/agent-session-provider-handle-encoding'
import {
AGENT_JOURNAL_THREAD_SCOPE,
type AgentSessionJournalIdentity
@@ -23,7 +24,7 @@ const IDENTITY: AgentSessionJournalIdentity = {
workspaceId: 'ws-1',
hostId: 'host-1',
agent: 'claude',
providerHandle: { kind: 'claude', sessionId: 'native-1', leafUuid: null }
providerHandle: claudeProviderHandle('native-1', null)
}
const START_FAILED = agentSessionFailureWords(agentSessionFailureFact('providerStartFailed'), {
@@ -19,13 +19,14 @@ import { createTrackedJournalOpener } from './journal-host-database-test-support
import { agentSessionFailureFact } from '../../../shared/agent-session-failure'
import { agentSessionFailureWords } from '../../../shared/agent-session-failure-words'
import { classifyDispatchRejection } from '../../../shared/structured-agent-session-dispatch-rejection'
import { claudeProviderHandle } from '../../../shared/agent-session-provider-handle-encoding'
const IDENTITY: AgentSessionJournalIdentity = {
sessionId: 'session-1',
workspaceId: 'ws-1',
hostId: 'host-1',
agent: 'claude',
providerHandle: { kind: 'claude', sessionId: 'provider-1', leafUuid: null }
providerHandle: claudeProviderHandle('provider-1', null)
}
function claudeIdentity(uuid: string): AgentJournalItemIdentity {
@@ -5,8 +5,10 @@ import type {
AgentJournalProducerLinkage,
AgentJournalRowAttribution,
AgentJournalTurnScope,
AgentSessionProviderHandle
AgentSessionJournalIdentity,
AgentSessionJournalProviderHandle
} from '../../../shared/agent-session-journal-types'
import { agentSessionJournalProviderHandle } from '../../../shared/agent-session-provider-handle-encoding'
import { journalRowSchemaVersion } from '../../../shared/agent-session-journal-types'
import {
agentJournalLinkageFields,
@@ -63,7 +65,7 @@ export function journalTombstoneRowBuilder(
export function journalSubmissionRowBuilder(
state: () => JournalReducerState,
providerHandle: AgentSessionProviderHandle,
identity: Pick<AgentSessionJournalIdentity, 'providerHandle' | 'agent'>,
input: {
clientMessageId: string
payloadFingerprint: string
@@ -85,7 +87,7 @@ export function journalSubmissionRowBuilder(
const queuedMessageId = consume?.messageId ?? input.queuedMessageId
return buildJournalSubmissionRow({
state: state(),
providerHandle,
providerHandle: agentSessionJournalProviderHandle(identity),
...input,
...(queuedMessageId !== undefined ? { queuedMessageId } : {}),
seq,
@@ -300,7 +302,7 @@ export function buildJournalSubmissionRow(input: {
state: JournalReducerState
clientMessageId: string
payloadFingerprint: string
providerHandle: AgentSessionProviderHandle
providerHandle: AgentSessionJournalProviderHandle
body: AgentJournalMessageItem
seq: number
fence: number
@@ -19,13 +19,14 @@ import {
liveTestJournalRows,
updateTestJournalRowJson
} from './journal-host-database-test-support'
import { claudeProviderHandle } from '../../../shared/agent-session-provider-handle-encoding'
const IDENTITY: AgentSessionJournalIdentity = {
sessionId: 'session-1',
workspaceId: 'ws-1',
hostId: 'host-1',
agent: 'claude',
providerHandle: { kind: 'claude', sessionId: 'claude-session', leafUuid: null }
providerHandle: claudeProviderHandle('claude-session', null)
}
const USAGE = {
@@ -9,6 +9,7 @@ import {
openTestJournalHostDatabase,
liveTestJournalRows
} from './journal-host-database-test-support'
import { codexProviderHandle } from '../../../shared/agent-session-provider-handle-encoding'
// Which rows an older host can still read: only rows that carry a turn item
// are stamped with the version it does not know, and the epoch row never is.
@@ -33,7 +34,7 @@ describe('journal row schema versions', () => {
workspaceId: 'workspace-1',
hostId: 'local',
agent: 'codex',
providerHandle: { kind: 'codex', threadId: 'thread-1' }
providerHandle: codexProviderHandle('thread-1')
},
now: () => 1_000,
stateDirectory: join(root, 'session-1')
@@ -29,7 +29,7 @@ import {
type AgentJournalMessageItem,
type AgentJournalProducerLinkage,
type AgentJournalTurnScope,
type AgentSessionProviderHandle
type AgentSessionJournalProviderHandle
} from '../../../shared/agent-session-journal-types'
import {
readAgentJournalItemBody,
@@ -67,7 +67,7 @@ type JournalRowBase = AgentJournalProducerLinkage & {
export type JournalEpochRow = JournalRowBase & {
kind: 'epoch'
reason: AgentJournalEpochReason
providerHandle: AgentSessionProviderHandle
providerHandle: AgentSessionJournalProviderHandle
}
export const AGENT_JOURNAL_EPOCH_REASONS = [
@@ -131,7 +131,7 @@ export type JournalSubmissionRow = JournalRowBase & {
kind: 'submission'
clientMessageId: string
payloadFingerprint: string
providerHandle: AgentSessionProviderHandle
providerHandle: AgentSessionJournalProviderHandle
body: AgentJournalMessageItem
/** Accepted to be handed over by a later `dispatch{pending}` row; absent on rows whose writer
* dispatched in the same step. Older readers keep the key and ignore it. */
@@ -23,13 +23,17 @@ import {
buildJournalQueueResumeRow,
buildJournalStopEventRow
} from './journal-stop-and-resume-rows'
import {
agentSessionJournalProviderHandle,
codexProviderHandle
} from '../../../shared/agent-session-provider-handle-encoding'
const IDENTITY: AgentSessionJournalIdentity = {
sessionId: 'session-s',
workspaceId: 'ws-1',
hostId: 'host-1',
agent: 'codex',
providerHandle: { kind: 'codex', threadId: 'thread-1' }
providerHandle: codexProviderHandle('thread-1')
}
const EPOCH = 'epoch-1'
@@ -64,7 +68,7 @@ function epochWith(
{
kind: 'epoch',
reason,
providerHandle: IDENTITY.providerHandle,
providerHandle: agentSessionJournalProviderHandle(IDENTITY),
...journalRowBase(EPOCH, 1, 1, 1)
}
]
@@ -27,13 +27,14 @@ import {
insertTestJournalRowJson,
SAVED_BY_NEWER_ORCA
} from './journal-host-database-test-support'
import { codexProviderHandle } from '../../../shared/agent-session-provider-handle-encoding'
const IDENTITY: AgentSessionJournalIdentity = {
sessionId: 'session-1',
workspaceId: 'ws-1',
hostId: 'host-1',
agent: 'codex',
providerHandle: { kind: 'codex', threadId: 'thread-1' }
providerHandle: codexProviderHandle('thread-1')
}
let root: string
@@ -29,13 +29,14 @@ import {
deleteTestJournalRow
} from './journal-host-database-test-support'
import type Database from '../../sqlite/sync-database'
import { codexProviderHandle } from '../../../shared/agent-session-provider-handle-encoding'
const IDENTITY: AgentSessionJournalIdentity = {
sessionId: 'session-1',
workspaceId: 'ws-1',
hostId: 'host-1',
agent: 'codex',
providerHandle: { kind: 'codex', threadId: 'thread-1' }
providerHandle: codexProviderHandle('thread-1')
}
let root: string
@@ -321,7 +321,7 @@ export class AgentSessionJournal {
receipt?: JournalOperationReceipt
): Promise<AgentJournalCursor> {
return this.rowWriter.append(
journalSubmissionRowBuilder(() => this.state, this.identity.providerHandle, input, consume),
journalSubmissionRowBuilder(() => this.state, this.identity, input, consume),
consume && queuedMessageConsumeHook(this.queuedMessages, input.clientMessageId, consume),
receipt
)
@@ -21,13 +21,14 @@ import {
import type { openAgentSessionJournal } from './journal-store-factory'
import { createTrackedJournalOpener } from './journal-host-database-test-support'
import { staleSubagentRosterRevisions } from './journal-subagent-liveness'
import { codexProviderHandle } from '../../../shared/agent-session-provider-handle-encoding'
const IDENTITY: AgentSessionJournalIdentity = {
sessionId: 'session-1',
workspaceId: 'ws-1',
hostId: 'host-1',
agent: 'codex',
providerHandle: { kind: 'codex', threadId: 'thread-1' }
providerHandle: codexProviderHandle('thread-1')
}
const GROUP_ID = 'thread-1:turn-1'
@@ -15,13 +15,17 @@ import { createJournalReducerState, applyJournalRow } from './journal-reducer'
import { parseJournalRow, serializeJournalRow, type JournalRow } from './journal-row-schema'
import type { AgentSessionJournal } from './journal-store'
import { createTrackedJournalOpener } from './journal-host-database-test-support'
import {
agentSessionJournalProviderHandle,
claudeProviderHandle
} from '../../../shared/agent-session-provider-handle-encoding'
const IDENTITY: AgentSessionJournalIdentity = {
sessionId: 'session-q',
workspaceId: 'ws-1',
hostId: 'host-1',
agent: 'claude',
providerHandle: { kind: 'claude', sessionId: 'native-1', leafUuid: null }
providerHandle: claudeProviderHandle('native-1', null)
}
const BODY: AgentJournalMessageItem = {
kind: 'message',
@@ -132,7 +136,7 @@ describe('the persisted row', () => {
ts: 1,
clientMessageId: 'handoff-1',
payloadFingerprint: 'fp',
providerHandle: IDENTITY.providerHandle,
providerHandle: agentSessionJournalProviderHandle(IDENTITY),
body: BODY,
queuedMessageId: 'draft-1'
}
@@ -13,6 +13,7 @@ import {
type AgentJournalItemIdentity,
type AgentSessionJournalIdentity
} from '../../../shared/agent-session-journal-types'
import { codexProviderHandle } from '../../../shared/agent-session-provider-handle-encoding'
import {
closeTestJournalHostDatabase,
createTrackedJournalOpener,
@@ -27,7 +28,7 @@ const IDENTITY: AgentSessionJournalIdentity = {
workspaceId: 'ws-1',
hostId: 'host-1',
agent: 'codex',
providerHandle: { kind: 'codex', threadId: 'thread-1' }
providerHandle: codexProviderHandle('thread-1')
}
const SCOPE = { fence: 1, turnScope: AGENT_JOURNAL_THREAD_SCOPE }
@@ -22,6 +22,10 @@ import {
SAVED_BY_NEWER_ORCA
} from './journal-host-database-test-support'
import { parseJournalRow, type JournalRow } from './journal-row-schema'
import {
agentSessionJournalProviderHandle,
codexProviderHandle
} from '../../../shared/agent-session-provider-handle-encoding'
const UNLOADABLE = {
refusal: { code: 'agent_session_journal_unreadable', details: { reason: 'journalCorrupt' } }
@@ -32,7 +36,7 @@ const IDENTITY: AgentSessionJournalIdentity = {
workspaceId: 'ws-1',
hostId: 'host-1',
agent: 'codex',
providerHandle: { kind: 'codex', threadId: 'thread-1' }
providerHandle: codexProviderHandle('thread-1')
}
const SCOPE = { fence: 1, turnScope: AGENT_JOURNAL_THREAD_SCOPE }
@@ -116,7 +120,7 @@ async function journalWithOneItem(): Promise<string> {
* fails to compile. */
function rowOfEveryKind(): { [Kind in JournalRow['kind']]: Extract<JournalRow, { kind: Kind }> } {
const base = { v: AGENT_SESSION_JOURNAL_SCHEMA_VERSION, epoch: 'epoch-1', fence: 1, ts: 1 }
const providerHandle = IDENTITY.providerHandle
const providerHandle = agentSessionJournalProviderHandle(IDENTITY)
return {
epoch: { ...base, seq: 1, kind: 'epoch', reason: 'session_created', providerHandle },
item: {
@@ -20,13 +20,14 @@ import { openJournalOwingImport } from './journal-owed-import-test-support'
import type { AgentSessionJournal } from './journal-store'
import { openAgentSessionJournal } from './journal-store-factory'
import { JournalWriteQueue } from './journal-write-queue'
import { codexProviderHandle } from '../../../shared/agent-session-provider-handle-encoding'
const IDENTITY = {
sessionId: 'session-1',
workspaceId: 'workspace-1',
hostId: 'host-1',
agent: 'codex',
providerHandle: { kind: 'codex', threadId: 'thread-1' }
providerHandle: codexProviderHandle('thread-1')
} as const
const reply = (text: string): AgentJournalMessageItem => ({
@@ -20,13 +20,14 @@ import {
closeTestJournalHostDatabases,
createTrackedJournalOpener
} from './journal-host-database-test-support'
import { claudeProviderHandle } from '../../../shared/agent-session-provider-handle-encoding'
const IDENTITY: AgentSessionJournalIdentity = {
sessionId: 'session-q',
workspaceId: 'ws-1',
hostId: 'host-1',
agent: 'claude',
providerHandle: { kind: 'claude', sessionId: 'native-1', leafUuid: null }
providerHandle: claudeProviderHandle('native-1', null)
}
const REFUSAL = agentSessionFailureWords(
agentSessionFailureFact('providerRejected', {
@@ -17,13 +17,14 @@ import { queuedMessageFingerprint } from '../agent-session-wire/structured-agent
import { JournalQueuedMessages } from './journal-queued-messages'
import type { AgentSessionJournal } from './journal-store'
import { createTrackedJournalOpener } from './journal-host-database-test-support'
import { claudeProviderHandle } from '../../../shared/agent-session-provider-handle-encoding'
const IDENTITY: AgentSessionJournalIdentity = {
sessionId: 'session-q',
workspaceId: 'ws-1',
hostId: 'host-1',
agent: 'claude',
providerHandle: { kind: 'claude', sessionId: 'native-1', leafUuid: null }
providerHandle: claudeProviderHandle('native-1', null)
}
const STOP_WITHDRAWAL = agentSessionFailureWords(agentSessionFailureFact('cancelled'), {
surface: 'rejection'
@@ -29,13 +29,14 @@ import {
} from './queued-message-pause'
import { agentSessionFailureFact } from '../../../shared/agent-session-failure'
import { agentSessionFailureWords } from '../../../shared/agent-session-failure-words'
import { claudeProviderHandle } from '../../../shared/agent-session-provider-handle-encoding'
const IDENTITY: AgentSessionJournalIdentity = {
sessionId: 'session-p',
workspaceId: 'ws-1',
hostId: 'host-1',
agent: 'claude',
providerHandle: { kind: 'claude', sessionId: 'native-1', leafUuid: null }
providerHandle: claudeProviderHandle('native-1', null)
}
const HOST = 'proc-1'
@@ -26,13 +26,14 @@ import {
closeTestJournalHostDatabases,
createTrackedJournalOpener
} from './journal-host-database-test-support'
import { claudeProviderHandle } from '../../../shared/agent-session-provider-handle-encoding'
const IDENTITY: AgentSessionJournalIdentity = {
sessionId: 'session-q',
workspaceId: 'ws-1',
hostId: 'host-1',
agent: 'claude',
providerHandle: { kind: 'claude', sessionId: 'native-1', leafUuid: null }
providerHandle: claudeProviderHandle('native-1', null)
}
let root: string
@@ -11,13 +11,14 @@ import type {
import { createTrackedJournalOpener } from '../agent-session-journal/journal-host-database-test-support'
import type { AgentSessionJournal } from '../agent-session-journal/journal-store'
import { readAgentSessionHistory } from './agent-session-history-page'
import { codexProviderHandle } from '../../../shared/agent-session-provider-handle-encoding'
const IDENTITY: AgentSessionJournalIdentity = {
sessionId: 'session-1',
workspaceId: 'ws-1',
hostId: 'host-1',
agent: 'codex',
providerHandle: { kind: 'codex', threadId: 'thread-1' }
providerHandle: codexProviderHandle('thread-1')
}
const journals = createTrackedJournalOpener()
@@ -19,6 +19,7 @@ import {
readAgentSessionHistory,
readAgentSessionHydrationPage
} from './agent-session-history-page'
import { codexProviderHandle } from '../../../shared/agent-session-provider-handle-encoding'
// A backward page windows over the session's own rows, and a subagent's rows in
// that range ride along, so a burst cannot crowd the conversation off the page.
@@ -28,7 +29,7 @@ const IDENTITY: AgentSessionJournalIdentity = {
workspaceId: 'ws-1',
hostId: 'host-1',
agent: 'codex',
providerHandle: { kind: 'codex', threadId: 'thread-1' }
providerHandle: codexProviderHandle('thread-1')
}
const child: AgentJournalProducerLinkage = { agentId: 'task-1', producerKind: 'agent' }
@@ -20,13 +20,17 @@ import {
} from '../agent-session-journal/journal-host-database-test-support'
import { AgentSessionSubscribers } from './structured-agent-session-subscribers'
import { readAgentSessionHistory } from './agent-session-history-page'
import {
agentSessionJournalProviderHandle,
codexProviderHandle
} from '../../../shared/agent-session-provider-handle-encoding'
const identity: AgentSessionJournalIdentity = {
sessionId: 'bounded-catch-up',
workspaceId: 'folder-workspace',
hostId: 'remote-host',
agent: 'codex',
providerHandle: { kind: 'codex', threadId: 'thread-1' }
providerHandle: codexProviderHandle('thread-1')
}
const journals = createTrackedJournalOpener()
let root: string | undefined
@@ -55,7 +59,7 @@ async function seedJournal(count: number) {
kind: 'epoch',
seq: 1,
reason: 'session_created',
providerHandle: identity.providerHandle
providerHandle: agentSessionJournalProviderHandle(identity)
})
for (let index = 0; index < count; index += 1) {
insertTestJournalRow(db, identity.sessionId, {
@@ -35,13 +35,14 @@ import type {
import type { AgentSessionJournal } from '../agent-session-journal/journal-store'
import { projectJournalBatch } from './agent-session-journal-batch'
import { readAgentSessionHistory, resolveHistoryLimit } from './agent-session-history-page'
import { codexProviderHandle } from '../../../shared/agent-session-provider-handle-encoding'
const IDENTITY: AgentSessionJournalIdentity = {
sessionId: 'session-1',
workspaceId: 'ws-1',
hostId: 'host-1',
agent: 'codex',
providerHandle: { kind: 'codex', threadId: 'thread-1' }
providerHandle: codexProviderHandle('thread-1')
}
const journals = createTrackedJournalOpener()
@@ -46,6 +46,7 @@ import { agentSessionFailureFact } from '../../../shared/agent-session-failure'
import { agentSessionFailureWords } from '../../../shared/agent-session-failure-words'
import { openTestJournalHostDatabase } from '../agent-session-journal/journal-host-database-test-support'
import { createStructuredAgentSessionLogger } from './structured-agent-session-logger'
import { codexProviderHandle } from '../../../shared/agent-session-provider-handle-encoding'
const CALLER = { callerKey: 'client-1' }
@@ -66,7 +67,7 @@ const spawnChild: StructuredAgentSessionAdapter['acquire'] = async ({ fence, spa
acquisitionGeneration: `generation-${acquire.mock.calls.length}`,
link: {
linkId: `link-${fence}`,
handle: { provider: 'codex' as const, threadId: THREAD },
handle: codexProviderHandle(THREAD),
origin: store.getRecord(SESSION)?.providerHandleChain.length
? ('resumed' as const)
: ('created' as const),
@@ -21,6 +21,7 @@ import { performAttach } from './structured-agent-session-attach-flow'
import type { AgentSessionCreatePhaseRecorder } from '../../observability/agent-session-instrumentation'
import { openTestJournalHostDatabase } from '../agent-session-journal/journal-host-database-test-support'
import { createStructuredAgentSessionLogger } from './structured-agent-session-logger'
import { codexProviderHandle } from '../../../shared/agent-session-provider-handle-encoding'
const NOW = 1_800_000_000_000
const SESSION = 'legacy-session'
@@ -90,7 +91,7 @@ function adapter(input: {
},
link: {
linkId: `${input.origin}-link`,
handle: { provider: 'codex', threadId: 'legacy-thread' },
handle: codexProviderHandle('legacy-thread'),
origin: input.origin,
mintedAtFence: fence,
observedAt: NOW
@@ -139,7 +140,7 @@ describe('structured session acquisition options', () => {
},
link: {
linkId: `${origin}-link`,
handle: { provider: 'codex', threadId: 'legacy-thread' },
handle: codexProviderHandle('legacy-thread'),
origin,
mintedAtFence: input.fence,
observedAt: NOW
@@ -460,7 +461,7 @@ describe('structured session acquisition options', () => {
},
link: {
linkId: `link-${fence}`,
handle: { provider: 'codex', threadId: 'legacy-thread' },
handle: codexProviderHandle('legacy-thread'),
origin: store.getRecord(SESSION)?.providerHandleChain.length ? 'resumed' : 'created',
mintedAtFence: fence,
observedAt: NOW
@@ -8,6 +8,7 @@ import type {
StructuredAgentSessionAdapter
} from './structured-agent-session-adapter'
import { StructuredAgentSessionAdapterRouter } from './structured-agent-session-adapter-router'
import { claudeProviderHandle } from '../../../shared/agent-session-provider-handle-encoding'
function claudeIdentity(sessionId: string): AgentSessionJournalIdentity {
return {
@@ -15,7 +16,7 @@ function claudeIdentity(sessionId: string): AgentSessionJournalIdentity {
workspaceId: 'workspace-1',
hostId: 'local',
agent: 'claude',
providerHandle: { kind: 'claude', sessionId: 'provider-session-1', leafUuid: null }
providerHandle: claudeProviderHandle('provider-session-1', null)
}
}
@@ -24,7 +25,7 @@ function acquisition(fence: number, spawnToken: string): AgentSessionAcquisition
process: { hostId: 'local', pid: 1, processStartTimeMs: 1, spawnToken },
link: {
linkId: `link-${fence}`,
handle: { provider: 'claude', sessionId: 'provider-session-1', leafUuid: null },
handle: claudeProviderHandle('provider-session-1', null),
origin: 'created',
mintedAtFence: fence,
observedAt: 1
@@ -20,6 +20,7 @@ import * as legacyImport from '../agent-session-journal/journal-legacy-import'
import { StructuredAgentSessionHost } from './structured-agent-session-host'
import { openTestJournalHostDatabase } from '../agent-session-journal/journal-host-database-test-support'
import { createStructuredAgentSessionLogger } from './structured-agent-session-logger'
import { codexProviderHandle } from '../../../shared/agent-session-provider-handle-encoding'
const NOW = 1_800_000_000_000
const SESSION = 'codex_adopting_session'
@@ -101,7 +102,7 @@ function adapter(): StructuredAgentSessionAdapter {
process: { hostId: 'local', pid: 4242, processStartTimeMs: NOW, spawnToken },
link: {
linkId: 'resumed-link',
handle: { provider: 'codex', threadId: THREAD },
handle: codexProviderHandle(THREAD),
origin: 'resumed',
mintedAtFence: fence,
observedAt: NOW
@@ -5,6 +5,7 @@ import {
type AgentSessionWireRefusal
} from '../../../shared/agent-session-wire'
import type { AgentSessionRecord } from '../../../shared/agent-session-record'
import { agentSessionProviderHandleFromWire } from '../../../shared/agent-session-provider-handle-encoding'
import type { AgentSessionAttachParams, AttachedJournal } from './structured-agent-session-attach'
import type { JournalReplacementItem } from '../agent-session-journal/journal-epoch-replacement'
import {
@@ -49,10 +50,7 @@ async function readAdoptedTranscript(
}
const prepared = await prepareLegacyTranscriptImport({
agent: params.agent,
sessionId:
adopt.providerHandle.kind === 'claude'
? adopt.providerHandle.sessionId
: adopt.providerHandle.threadId,
sessionId: agentSessionProviderHandleFromWire(adopt.providerHandle).nativeId,
options: { filePath: adopt.transcriptPath }
})
if (!prepared.ok) {
@@ -100,10 +98,7 @@ async function applyAdoptedTranscript(
const imported = await importLegacyTranscriptIntoJournal({
journal: attached.journal,
agent: params.agent,
sessionId:
adopt.providerHandle.kind === 'claude'
? adopt.providerHandle.sessionId
: adopt.providerHandle.threadId,
sessionId: agentSessionProviderHandleFromWire(adopt.providerHandle).nativeId,
fence: record.lease.runtimeFence,
options: { filePath: adopt.transcriptPath }
})
@@ -27,6 +27,7 @@ import {
} from './structured-agent-session-host-test-data'
import { openTestJournalHostDatabase } from '../agent-session-journal/journal-host-database-test-support'
import { createStructuredAgentSessionLogger } from './structured-agent-session-logger'
import { codexProviderHandle } from '../../../shared/agent-session-provider-handle-encoding'
const CALLER = { callerKey: 'client-1' }
const EXIT_REASON = 'Claude Code is not signed in. Sign in with the Claude CLI'
@@ -119,7 +120,7 @@ beforeEach(async () => {
process: { hostId: 'local', pid: 4242, processStartTimeMs: 1_700_000_000_000, spawnToken },
link: {
linkId: `link-${fence}`,
handle: { provider: 'codex', threadId: THREAD },
handle: codexProviderHandle(THREAD),
origin: generation === 0 ? ('created' as const) : ('resumed' as const),
mintedAtFence: fence,
observedAt: NOW
@@ -28,6 +28,7 @@ import {
resetHostTestOperationIds
} from './structured-agent-session-host-test-data'
import { openTestJournalHostDatabase } from '../agent-session-journal/journal-host-database-test-support'
import { claudeProviderHandle } from '../../../shared/agent-session-provider-handle-encoding'
const caller = { callerKey: 'desktop' }
const CLAUDE_SESSION = '819cf9f8-e43c-4ad7-b50f-54aa158a726a'
@@ -56,7 +57,7 @@ function adapter(catalog: ClaudeAtRestCommandCatalog): StructuredAgentSessionAda
mintedAtFence: input.fence,
observedAt: HOST_TEST_NOW,
origin: 'created' as const,
handle: { provider: 'claude' as const, sessionId: CLAUDE_SESSION, leafUuid: null }
handle: claudeProviderHandle(CLAUDE_SESSION, null)
}
})),
atRestCommands: catalog,
@@ -5,15 +5,17 @@
// the record store's compare-and-swap, which also owns the idempotency row, so
// a retried attach replays instead of reserving a second owner.
import type {
AgentSessionJournalIdentity,
AgentSessionProviderHandle
} from '../../../shared/agent-session-journal-types'
import type { AgentSessionJournalIdentity } from '../../../shared/agent-session-journal-types'
import type { AgentSessionOwnerProbe } from '../../../shared/agent-session-lease-adjudication'
import type {
AgentSessionHandleProvider,
AgentSessionProviderHandleLink
} from '../../../shared/agent-session-provider-handle'
import {
agentSessionProviderHandleBelongsTo,
agentSessionProviderHandleFromWire,
type AgentSessionWireProviderHandle
} from '../../../shared/agent-session-provider-handle-encoding'
import { claudeProviderHandleLink } from '../../claude/claude-structured-owner-identity'
import { codexProviderHandleLink } from '../../codex/codex-structured-owner-identity'
import type {
@@ -65,8 +67,9 @@ export type AgentSessionAttachParams = {
* attach fingerprint: which tab shows the chat is not which conversation it attaches to. */
surfaceTabId?: string
launchArgs?: string[]
/** Omitted only for create-by-intent; the adapter proves the durable handle. */
providerHandle?: Exclude<AgentSessionProviderHandle, { kind: 'opaque' }>
/** Omitted only for create-by-intent; the adapter proves the durable handle. In the wire's
* form, because the attach fingerprint covers it as the client sent it. */
providerHandle?: AgentSessionWireProviderHandle
/**
* Host-resolved only. Present when this create adopts an existing provider conversation rather
* than starting one: it seeds the handle chain so the adapter resumes instead of creating, and
@@ -76,7 +79,7 @@ export type AgentSessionAttachParams = {
* without adopting — presence of a handle must never be what triggers a resume.
*/
adopt?: {
providerHandle: Exclude<AgentSessionProviderHandle, { kind: 'opaque' }>
providerHandle: AgentSessionWireProviderHandle
/** Omitted only when the exact committed operation replays an already-imported journal. */
transcriptPath?: string
}
@@ -119,7 +122,13 @@ export function attachFingerprintFields(params: AgentSessionAttachParams): Recor
export function admitAttachOrRefuse(
params: AgentSessionAttachParams
): { ok: true; fingerprint: string } | { ok: false; refusal: AgentSessionWireRefusal } {
if (params.providerHandle && params.providerHandle.kind !== params.provider) {
if (
params.providerHandle &&
!agentSessionProviderHandleBelongsTo(
agentSessionProviderHandleFromWire(params.providerHandle),
params.provider
)
) {
return {
ok: false,
refusal: refuse(
@@ -143,16 +152,9 @@ export function journalIdentityFor(
params: AgentSessionAttachParams
): AgentSessionJournalIdentity {
const head = agentSessionProviderHandleChainHead(record.providerHandleChain)
const providerHandle: AgentSessionProviderHandle =
head?.handle.provider === 'codex'
? { kind: 'codex', threadId: head.handle.threadId }
: head?.handle.provider === 'claude'
? {
kind: 'claude',
sessionId: head.handle.sessionId,
leafUuid: head.handle.leafUuid
}
: (params.providerHandle ?? { kind: 'opaque', agent: params.agent, value: 'pending' })
const providerHandle =
head?.handle ??
(params.providerHandle ? agentSessionProviderHandleFromWire(params.providerHandle) : null)
return {
sessionId: record.sessionId,
workspaceId: params.location.workspaceId,
@@ -265,7 +267,7 @@ async function reconcileAgainstProviderHistory(input: {
const ADOPTED_HANDLE_FENCE = 1
function adoptedProviderHandleLink(
handle: Exclude<AgentSessionProviderHandle, { kind: 'opaque' }>,
handle: AgentSessionWireProviderHandle,
observedAt: number
): AgentSessionProviderHandleLink {
return handle.kind === 'claude'
@@ -17,6 +17,7 @@ import {
} from './structured-agent-session-host-test-data'
import { openTestJournalHostDatabase } from '../agent-session-journal/journal-host-database-test-support'
import { createStructuredAgentSessionLogger } from './structured-agent-session-logger'
import { claudeProviderHandle } from '../../../shared/agent-session-provider-handle-encoding'
const CALLER = { callerKey: 'client-claude' }
const CLAUDE_SESSION = '019fd532-7c11-7a90-b6de-4e1a2c3d5f61'
@@ -55,7 +56,7 @@ function adapter(): StructuredAgentSessionAdapter {
process: { hostId: 'local', pid: 4200, processStartTimeMs: NOW, spawnToken },
link: {
linkId: `claude-native-${fence}`,
handle: { provider: 'claude', sessionId: CLAUDE_SESSION, leafUuid: 'native-leaf' },
handle: claudeProviderHandle(CLAUDE_SESSION, 'native-leaf'),
origin: acquire.mock.calls.length === 1 ? 'created' : 'resumed',
mintedAtFence: fence,
observedAt: NOW
@@ -524,7 +524,7 @@ it('starts a new child for the next send after a Stop, on the same Claude conver
})
// The wake resumes the same Claude conversation; the first test pins the leaf it resumes after.
expect(store.getRecord(SESSION)?.providerHandleChain.at(-1)?.handle).toMatchObject({
sessionId: PROVIDER_SESSION_ID
nativeId: PROVIDER_SESSION_ID
})
expect(resumed.closed).toBe(false)
expect(await dispatch(next)).toMatchObject({ state: 'pending' })
@@ -27,6 +27,7 @@ import {
} from './structured-agent-session-host-test-data'
import { openTestJournalHostDatabase } from '../agent-session-journal/journal-host-database-test-support'
import { recordingStructuredAgentSessionLogger } from './structured-agent-session-logger-test-support'
import { codexProviderHandle } from '../../../shared/agent-session-provider-handle-encoding'
const CALLER = { callerKey: 'client-1' }
@@ -77,7 +78,7 @@ beforeEach(async () => {
acquisitionGeneration: 'generation-1',
link: {
linkId: `link-${fence}`,
handle: { provider: 'codex' as const, threadId: THREAD },
handle: codexProviderHandle(THREAD),
origin: 'created' as const,
mintedAtFence: fence,
observedAt: NOW
@@ -10,13 +10,14 @@ import { StructuredAgentSessionConversations } from './structured-agent-session-
import type { StructuredAgentSessionHostSession } from './structured-agent-session-host-types'
import { hostTestAttachParams } from './structured-agent-session-host-test-data'
import { recordingStructuredAgentSessionLogger } from './structured-agent-session-logger-test-support'
import { codexProviderHandle } from '../../../shared/agent-session-provider-handle-encoding'
const IDENTITY: AgentSessionJournalIdentity = {
sessionId: 'session-1',
workspaceId: 'workspace-1',
hostId: 'host-1',
agent: 'codex',
providerHandle: { kind: 'codex', threadId: 'thread-1' }
providerHandle: codexProviderHandle('thread-1')
}
const journals = createTrackedJournalOpener()
@@ -48,6 +48,7 @@ import {
} from './structured-agent-session-host-test-data'
import { createStructuredAgentSessionLogger } from './structured-agent-session-logger'
import { recordingStructuredAgentSessionLogger } from './structured-agent-session-logger-test-support'
import { claudeProviderHandle } from '../../../shared/agent-session-provider-handle-encoding'
const PROVIDER_SESSION = 'provider-session-alpha-1'
/** The tool call's row: the last thing the provider wrote before the crash. */
@@ -71,7 +72,7 @@ function crashedClaudeRecord(): AgentSessionRecord {
providerHandleChain: [
{
linkId,
handle: { provider: 'claude', sessionId: PROVIDER_SESSION, leafUuid: null },
handle: claudeProviderHandle(PROVIDER_SESSION, null),
origin: 'created',
mintedAtFence: 13,
observedAt: TOOL_STARTED_AT - 60_000
@@ -120,7 +121,7 @@ async function seedClaudeToolTurn(): Promise<void> {
workspaceId: LOCATION.workspaceId,
hostId: LOCATION.executionHostId,
agent: 'claude',
providerHandle: { kind: 'claude', sessionId: PROVIDER_SESSION, leafUuid: null }
providerHandle: claudeProviderHandle(PROVIDER_SESSION, null)
},
database: openTestJournalHostDatabase(root),
now: () => now
@@ -435,7 +436,7 @@ describe('a turn a read reached before the reconcile proved its owner dead', ()
process,
link: {
linkId: `claude-${fence}-link`,
handle: { provider: 'claude', sessionId: PROVIDER_SESSION, leafUuid: null },
handle: claudeProviderHandle(PROVIDER_SESSION, null),
origin: 'resumed',
mintedAtFence: fence,
observedAt: RELAUNCHED_AT
@@ -513,7 +514,7 @@ async function hostWithFailingFirstStart(failure: Error) {
process,
link: {
linkId: `claude-${fence}-link`,
handle: { provider: 'claude', sessionId: PROVIDER_SESSION, leafUuid: null },
handle: claudeProviderHandle(PROVIDER_SESSION, null),
origin: 'resumed',
mintedAtFence: fence,
observedAt: now
@@ -17,6 +17,7 @@ import {
unfinishedStructuredAgentSessionWorkWasInterrupted
} from './structured-agent-session-dead-generation-settlement'
import { openTestJournalHostDatabase } from '../agent-session-journal/journal-host-database-test-support'
import { codexProviderHandle } from '../../../shared/agent-session-provider-handle-encoding'
const UNEXPECTED_PROVIDER_EXIT_OUTCOME =
'The agent stopped while this response was in progress. You can continue in this conversation.'
@@ -34,7 +35,7 @@ beforeEach(async () => {
workspaceId: 'workspace-1',
hostId: 'local',
agent: 'codex',
providerHandle: { kind: 'codex', threadId: THREAD }
providerHandle: codexProviderHandle(THREAD)
},
database: openTestJournalHostDatabase(root),
now: () => 1_000
@@ -403,7 +404,7 @@ describe('dead structured-session generation settlement', () => {
workspaceId: 'workspace-1',
hostId: 'local',
agent: 'codex',
providerHandle: { kind: 'codex', threadId: THREAD }
providerHandle: codexProviderHandle(THREAD)
},
database: openTestJournalHostDatabase(root),
now: () => 1_000
@@ -25,6 +25,7 @@ import { openJournalOwingImport } from '../agent-session-journal/journal-owed-im
import { createCodexJournalTranslator } from '../../codex/codex-structured-journal-translation'
import { createStructuredAgentSessionLogger } from './structured-agent-session-logger'
import { testEventSinkLogging } from './structured-agent-session-logger-test-support'
import { codexProviderHandle } from '../../../shared/agent-session-provider-handle-encoding'
const BODY: AgentJournalItemBody = {
kind: 'message',
@@ -37,7 +38,7 @@ const JOURNAL_IDENTITY = {
workspaceId: 'workspace-1',
hostId: 'host-1',
agent: 'codex',
providerHandle: { kind: 'codex', threadId: 'thread-1' }
providerHandle: codexProviderHandle('thread-1')
} as const
function identity(ordinal: number): AgentJournalItemIdentity {
@@ -24,6 +24,7 @@ import {
} from './structured-agent-session-host-test-data'
import { openTestJournalHostDatabase } from '../agent-session-journal/journal-host-database-test-support'
import { createStructuredAgentSessionLogger } from './structured-agent-session-logger'
import { codexProviderHandle } from '../../../shared/agent-session-provider-handle-encoding'
const CALLER = { callerKey: 'client-1' }
const EXIT_REASON = 'claude stream-json exited (code 1): stderr tail'
@@ -44,7 +45,7 @@ beforeEach(async () => {
process: { hostId: 'local', pid: 4242, processStartTimeMs: 1_700_000_000_000, spawnToken },
link: {
linkId: `link-${fence}`,
handle: { provider: 'codex', threadId: THREAD },
handle: codexProviderHandle(THREAD),
origin: 'created',
mintedAtFence: fence,
observedAt: NOW
@@ -22,6 +22,7 @@ import {
} from './structured-agent-session-host-test-data'
import { openTestJournalHostDatabase } from '../agent-session-journal/journal-host-database-test-support'
import { createStructuredAgentSessionLogger } from './structured-agent-session-logger'
import { codexProviderHandle } from '../../../shared/agent-session-provider-handle-encoding'
const CALLER = { callerKey: 'client-1' }
const EXIT_REASON = 'claude stream-json exited (code 1): claude: not signed in'
@@ -38,7 +39,7 @@ beforeEach(async () => {
process: { hostId: 'local', pid: 4242, processStartTimeMs: 1_700_000_000_000, spawnToken },
link: {
linkId: `link-${fence}`,
handle: { provider: 'codex', threadId: THREAD },
handle: codexProviderHandle(THREAD),
// A resume continues the chain the first start created.
origin: store.getRecord(SESSION)?.providerHandleChain.length ? 'resumed' : 'created',
mintedAtFence: fence,
@@ -25,6 +25,7 @@ import type { StructuredAgentSessionAttachContext } from './structured-agent-ses
import type { StructuredAgentSessionHostSession } from './structured-agent-session-host-types'
import { StructuredAgentSessionStatusFeed } from './structured-agent-session-status-feed'
import { createStructuredAgentSessionLogger } from './structured-agent-session-logger'
import { codexProviderHandle } from '../../../shared/agent-session-provider-handle-encoding'
// Everything before the journal is out of scope here; what matters is what the orchestration does
// when the attach throws after acquisition.
@@ -43,7 +44,7 @@ const IDENTITY: AgentSessionJournalIdentity = {
workspaceId: 'repo-1::/workspace/app',
hostId: 'host-1',
agent: 'codex',
providerHandle: { kind: 'codex', threadId: SESSION }
providerHandle: codexProviderHandle(SESSION)
}
const SUBJECT = makeStructuredAgentStatusSubject(
@@ -25,6 +25,7 @@ import {
} from './structured-agent-session-host-test-data'
import { openTestJournalHostDatabase } from '../agent-session-journal/journal-host-database-test-support'
import { createStructuredAgentSessionLogger } from './structured-agent-session-logger'
import { codexProviderHandle } from '../../../shared/agent-session-provider-handle-encoding'
const CALLER = { callerKey: 'client-1' }
@@ -125,7 +126,7 @@ beforeEach(async () => {
},
link: {
linkId: `link-${fence}`,
handle: { provider: 'codex', threadId: THREAD },
handle: codexProviderHandle(THREAD),
origin: store.getRecord(SESSION)?.providerHandleChain.length ? 'resumed' : 'created',
mintedAtFence: fence,
observedAt: NOW

Some files were not shown because too many files have changed in this diff Show More