Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
e6e0fec
feat(native-chat): say in the chat why Orca stopped a reply, and offe…
brennanb2025 Oct 5, 2026
9d2fef0
test(native-chat): Continue is not held by a recovery file that never…
brennanb2025 Oct 5, 2026
6801f98
fix(native-chat): bind an Orca stop's cause to the runtime that held …
brennanb2025 Oct 5, 2026
b97a18e
Merge remote-tracking branch 'origin/main' into brennanb2025/rsc-resu…
brennanb2025 Oct 5, 2026
680d565
test(native-chat): type the Orca-stop test fixtures so the typecheck …
brennanb2025 Oct 5, 2026
9ea4744
fix(native-chat): call a cut a crash only when Orca's runtime started…
brennanb2025 Oct 5, 2026
13e4ba5
fix(native-chat): Continue answers once Orca accepts it, not once the…
brennanb2025 Oct 5, 2026
cdb457e
fix(native-chat): a reply the user steered, or a command run after th…
brennanb2025 Oct 5, 2026
00f24b0
fix(native-chat): a death proven after an earlier settle explains the…
brennanb2025 Oct 5, 2026
1a3a686
fix(native-chat): the cause row's words stay put, and Continue waits …
brennanb2025 Oct 5, 2026
3c06c11
fix(native-chat): a refused Continue says so once, in the composer
brennanb2025 Oct 5, 2026
3696cf2
fix(native-chat): the row naming Orca's stop carries its own presenta…
brennanb2025 Oct 5, 2026
9f1636b
test(native-chat): pass the session-end event without a type assertion
brennanb2025 Oct 5, 2026
d1377dc
test(native-chat): the row naming Orca's stop renders neutral, whoeve…
brennanb2025 Oct 5, 2026
37aa332
fix(native-chat): the "Couldn't continue" line goes once the chat is …
brennanb2025 Oct 5, 2026
1e83a16
fix(native-chat): Continue waits while an opted-in launch may still r…
brennanb2025 Oct 5, 2026
658fe70
fix(native-chat): a runtime's end survives a late reinstall, a failed…
brennanb2025 Oct 5, 2026
82ab348
test(native-chat): build the re-presented row as the plain status ite…
brennanb2025 Oct 5, 2026
7eac5a7
Merge remote-tracking branch 'origin/main' into brennanb2025/rsc-resu…
brennanb2025 Oct 5, 2026
9795b3a
fix(native-chat): a Continue click clears the composer's old error, s…
brennanb2025 Oct 5, 2026
7b8528f
fix(native-chat): a chat start compares the owner process, not the ru…
brennanb2025 Oct 5, 2026
26e614c
test(native-chat): count agentSession.continueInterrupted among the s…
brennanb2025 Oct 5, 2026
2264121
Merge remote-tracking branch 'origin/main' into brennanb2025/rsc-resu…
brennanb2025 Oct 6, 2026
4c91058
Merge remote-tracking branch 'origin/main' into brennanb2025/rsc-resu…
brennanb2025 Oct 6, 2026
8804487
Merge remote-tracking branch 'origin/main' into brennanb2025/rsc-resu…
brennanb2025 Oct 6, 2026
f13ed9d
Merge remote-tracking branch 'origin/main' into brennanb2025/rsc-resu…
brennanb2025 Oct 6, 2026
2913436
Merge remote-tracking branch 'origin/main' into brennanb2025/rsc-resu…
brennanb2025 Oct 6, 2026
624fe53
refactor(native-chat): derive the structured chat's transcript sessio…
brennanb2025 Oct 6, 2026
37e4efa
Merge remote-tracking branch 'origin/main' into brennanb2025/rsc-resu…
brennanb2025 Oct 6, 2026
0b26c86
refactor(native-chat): keep the Continue capability in its own module
brennanb2025 Oct 6, 2026
7dc49de
Merge remote-tracking branch 'origin/main' into brennanb2025/rsc-resu…
brennanb2025 Oct 6, 2026
87e7812
refactor(native-chat): keep two shared files within their line limit …
brennanb2025 Oct 6, 2026
2d0dbe0
Merge remote-tracking branch 'origin/main' into brennanb2025/rsc-resu…
brennanb2025 Oct 6, 2026
e7ec734
test(native-chat): compare the store directory's files without depend…
brennanb2025 Oct 6, 2026
cc773ab
Merge remote-tracking branch 'origin/main' into brennanb2025/rsc-resu…
brennanb2025 Oct 6, 2026
7948ec1
fix: share the path bound main's launch-directory check needs
brennanb2025 Oct 6, 2026
949802c
Merge remote-tracking branch 'origin/main' into brennanb2025/rsc-resu…
brennanb2025 Oct 6, 2026
bf8cac7
Merge remote-tracking branch 'origin/main' into brennanb2025/rsc-resu…
brennanb2025 Oct 6, 2026
abbe629
test: give the stop-row fold rows the draws flag main's fold now reads
brennanb2025 Oct 6, 2026
381c7e3
refactor: mark the launch's resume decision where the resume begins
brennanb2025 Oct 6, 2026
690c6c1
Merge remote-tracking branch 'origin/main' into brennanb2025/rsc-resu…
brennanb2025 Oct 6, 2026
0f571ef
Merge remote-tracking branch 'origin/main' into brennanb2025/rsc-resu…
brennanb2025 Oct 6, 2026
e18e074
test: count main's new structured method alongside agentSession.conti…
brennanb2025 Oct 6, 2026
1f389f3
Merge remote-tracking branch 'origin/main' into brennanb2025/rsc-resu…
brennanb2025 Oct 7, 2026
9d74884
Merge remote-tracking branch 'origin/main' into brennanb2025/rsc-resu…
brennanb2025 Oct 7, 2026
b7a5425
Merge remote-tracking branch 'origin/main' into brennanb2025/rsc-resu…
brennanb2025 Oct 7, 2026
05e8ae3
Merge remote-tracking branch 'origin/main' into brennanb2025/rsc-resu…
brennanb2025 Oct 7, 2026
806a136
fix: the journal database keeps its folder, where runtime end records…
brennanb2025 Oct 7, 2026
599bbdb
Merge remote-tracking branch 'origin/main' into brennanb2025/rsc-resu…
brennanb2025 Oct 7, 2026
dff0acd
Merge remote-tracking branch 'origin/main' into brennanb2025/rsc-resu…
brennanb2025 Oct 7, 2026
92b3c7d
Merge remote-tracking branch 'origin/main' into brennanb2025/rsc-resu…
brennanb2025 Oct 7, 2026
6784cc3
Merge remote-tracking branch 'origin/main' into brennanb2025/rsc-resu…
brennanb2025 Oct 7, 2026
174e589
Merge remote-tracking branch 'origin/main' into brennanb2025/rsc-resu…
brennanb2025 Oct 7, 2026
232128b
Merge remote-tracking branch 'origin/main' into brennanb2025/rsc-resu…
brennanb2025 Oct 7, 2026
5bc40fc
Merge remote-tracking branch 'origin/main' into brennanb2025/rsc-resu…
brennanb2025 Oct 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions config/scripts/vitest-sqlite-runtime-files.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export const SQLITE_RUNTIME_INCLUDE = [
'src/main/native-chat/agent-session-wire/agent-session-stop-note-projection.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-accept-then-deliver.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-acquisition-options.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-acquisition-runtime-stamp.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-adopted-import.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-append-delivery.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-at-rest-commands.test.ts',
Expand Down Expand Up @@ -118,6 +119,7 @@ export const SQLITE_RUNTIME_INCLUDE = [
'src/main/native-chat/agent-session-wire/structured-agent-session-send-preparation.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-send-restarts-failed-start.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-settled-attach-retry.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-shutdown-cut-row.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-stale-turn-verdict.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-start-failure-writer.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-startup-reconcile-failure.test.ts',
Expand Down Expand Up @@ -155,6 +157,7 @@ export const SQLITE_RUNTIME_INCLUDE = [
'src/main/runtime/agent-session-record-store-tab-table.test.ts',
'src/main/runtime/agent-session-record-store.test.ts',
'src/main/runtime/agent-session-released-reservation-replay.test.ts',
'src/main/runtime/agent-session-runtime-attribution.test.ts',
'src/main/runtime/agent-session-store-transactions.test.ts',
'src/main/runtime/agent-session-unsaved-creation-supersession.test.ts',
'src/main/runtime/automation-change-publication.test.ts',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ describe('grouping a Codex thread restored from full history', () => {
draws: true,
outlivesTurn: false,
reportsFailure: false,
reportsCompaction: false
explainsTurn: false
})),
settledTurnKeys: new Set(bars.settledTurns.keys()),
expandedTurnKeys: new Set()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,21 @@ export class JournalHostDatabase {
/** A failed transaction's ROLLBACK failed too, so the transaction may still be open. */
private stranded = false

private constructor(opened: OpenJournalDatabase) {
private constructor(
opened: OpenJournalDatabase,
/** Where the database lives; per-runtime end records sit beside it. */
readonly stateDirectory: string
) {
this.connection = opened.db
this.readOnly = opened.readOnly
}

static open(stateDirectory: string): JournalHostDatabase {
mkdirSync(stateDirectory, { recursive: true })
return new JournalHostDatabase(openJournalDatabase(journalDatabasePath(stateDirectory)))
return new JournalHostDatabase(
openJournalDatabase(journalDatabasePath(stateDirectory)),
stateDirectory
)
}

get isClosed(): boolean {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// A chat starts whatever the store stamped on its owner: the Orca runtime holding the process is
// recorded beside its identity, and the start compares the process, not the stamp.

import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, expect, it, vi } from 'vitest'
import { computeAgentSessionPayloadFingerprint } from '../../../shared/agent-session-mutation-envelope'
import { codexProviderHandle } from '../../../shared/agent-session-provider-handle-encoding'
import { openTestAgentSessionRecordStore } from '../../runtime/agent-session-record-store-test-harness'
import { agentSessionRuntimeIncarnation } from '../../runtime/agent-session-runtime-attribution'
import { openTestJournalHostDatabase } from '../agent-session-journal/journal-host-database-test-support'
import type { StructuredAgentSessionAdapter } from './structured-agent-session-adapter'
import {
attachFingerprintFields,
type AgentSessionAttachParams
} from './structured-agent-session-attach'
import { performAttach } from './structured-agent-session-attach-flow'
import { openTestAttachConversation } from './structured-agent-session-attach-test-conversation'
import { createStructuredAgentSessionLogger } from './structured-agent-session-logger'
import { NO_STRUCTURED_AGENTS } from './structured-agent-session-adapter-router-test-support'

const NOW = 1_800_000_000_000
const SESSION = 'stamped-owner-session'
const OPERATION = `${NOW}-${'1'.padStart(32, '0')}`
let root: string | null = null

afterEach(async () => {
if (root) {
await rm(root, { recursive: true, force: true })
}
root = null
})

function attachParams(): AgentSessionAttachParams {
const params: AgentSessionAttachParams = {
envelope: {
sessionId: SESSION,
clientOperationId: OPERATION,
expectedRuntimeFence: null,
payloadFingerprint: ''
},
location: {
executionHostId: 'local',
wslDistro: null,
workspaceId: 'workspace-1',
workspaceKind: 'folder'
},
provider: 'codex',
agent: 'codex',
accountHome: { variable: 'CODEX_HOME', path: '/home/dev/.codex' },
runtimeKind: 'native',
providerHandle: { kind: 'codex', threadId: 'thread-1' }
}
return {
...params,
envelope: {
...params.envelope,
payloadFingerprint: computeAgentSessionPayloadFingerprint({
method: 'agentSession.attach',
sessionId: SESSION,
fields: attachFingerprintFields(params)
})
}
}
}

it('starts a chat whose recorded owner carries the runtime stamp', async () => {
root = await mkdtemp(join(tmpdir(), 'orca-stamped-owner-'))
const store = await openTestAgentSessionRecordStore(root)
// The record as the store published it, stamp included, rather than the transition's own copy.
const commit = store.commitProcessIdentity.bind(store)
vi.spyOn(store, 'commitProcessIdentity').mockImplementation(async (input) => {
await commit(input)
const published = store.getRecord(input.sessionId)
if (!published) {
throw new Error('the committed record is missing')
}
return published
})
const adapter: StructuredAgentSessionAdapter = {
acquire: vi.fn<StructuredAgentSessionAdapter['acquire']>(
async ({ fence, spawnToken, onSpawned }) => {
const process = { hostId: 'local', pid: 4242, processStartTimeMs: NOW, spawnToken }
await onSpawned?.(process)
return {
process,
link: {
linkId: 'created-link',
handle: codexProviderHandle('thread-1'),
origin: 'created',
mintedAtFence: fence,
observedAt: NOW
}
}
}
),
dispatch: vi.fn(),
cancelTurn: vi.fn(),
answerPrompt: vi.fn(),
setOption: vi.fn()
}

const attached = await performAttach({
agents: NO_STRUCTURED_AGENTS,
logger: createStructuredAgentSessionLogger(),
store,
adapter,
openConversation: openTestAttachConversation(openTestJournalHostDatabase(root)),
authority: {
spawnToken: 'spawn-a',
claimKeyId: 'key-1',
handoffOperationId: OPERATION,
probe: { outcome: 'reservation-unused' }
},
callerKey: 'client-1',
params: attachParams(),
now: () => NOW,
onAttached: () => {}
})

expect(attached).toMatchObject({ ok: true })
expect(store.getRecord(SESSION)?.lease.ownerProcess).toMatchObject({
pid: 4242,
runtime: agentSessionRuntimeIncarnation()
})
})
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { isDeepStrictEqual } from 'node:util'
import type { AgentSessionRecord } from '../../../shared/agent-session-record'
import type {
AgentSessionProcessIdentity,
AgentSessionRecord
} from '../../../shared/agent-session-record'
import {
AgentSessionPreSpawnError,
isAgentSessionPreSpawnError,
Expand All @@ -11,6 +14,17 @@ import type { AttachFlowInput } from './structured-agent-session-attach-flow'
import { readNativeSessionOptions } from './structured-agent-session-option-restoration'
import { withAgentSessionCreatePhase } from '../../observability/agent-session-instrumentation'

/** The same process, whatever Orca runtime the store stamped on its record (`runtime`): that stamp
* is about who holds the process, not which process it is. */
function sameOwnerProcess(
stored: AgentSessionProcessIdentity,
acquired: AgentSessionProcessIdentity
): boolean {
const { runtime: _storedRuntime, ...storedProcess } = stored
const { runtime: _acquiredRuntime, ...acquiredProcess } = acquired
return isDeepStrictEqual(storedProcess, acquiredProcess)
}

/** A reservation with no process behind it is only a promise to spawn; the
* adapter makes it real and the store then grants the writer. */
export async function acquireOwner(
Expand Down Expand Up @@ -79,7 +93,7 @@ export async function acquireOwner(
process: acquired.process,
now: input.now()
})
} else if (!isDeepStrictEqual(record.lease.ownerProcess, acquired.process)) {
} else if (!sameOwnerProcess(record.lease.ownerProcess, acquired.process)) {
throw new Error('agent_session_ownership_unknown')
}
const proved = await input.store.proveOwner({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// What a continuation's send came to, read from its submission: the four dispatch states, never
// collapsed into transport success (`StructuredAgentSessionContinuationOutcome`).

import {
readAgentSessionFailureFact,
type UnreadAgentSessionFailureFact
} from '../../../shared/agent-session-failure'
import type { StructuredAgentSessionContinuationOutcome } from './structured-agent-session-restart-continuation'

export type ContinuationSubmission = {
dispatchState?: string
reason?: string | null
rejection?: UnreadAgentSessionFailureFact
}

export function refusedBy(
sessionId: string,
submission: ContinuationSubmission
): StructuredAgentSessionContinuationOutcome {
// A start the agent was refused files that refusal's code, which the failure guidance keys on.
const refusal = readAgentSessionFailureFact(submission.rejection)?.refusal
return {
sessionId,
outcome: 'refused',
reason: refusal?.code ?? submission.reason ?? 'agent_session_dispatch_rejected',
...(refusal ? { refusal } : {})
}
}

export function verdictOf(
sessionId: string,
submission: ContinuationSubmission | undefined
): StructuredAgentSessionContinuationOutcome {
const dispatch = submission?.dispatchState
if (submission && dispatch === 'rejected') {
return refusedBy(sessionId, submission)
}
if (dispatch === 'pending') {
// Still pending after settlement gave up: handed off, never confirmed.
return { sessionId, outcome: 'pending' }
}
// `unknown`, or a peer that reported no state at all: delivery is unverifiable, so this claims
// neither success nor failure — and writes no note saying the agent was asked to continue.
return dispatch === 'accepted'
? { sessionId, outcome: 'continued' }
: { sessionId, outcome: 'unknown' }
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ import {
import { resettleOpenStructuredAgentSessionConversation } from './structured-agent-session-conversation-open'
import { StructuredAgentSessionHost } from './structured-agent-session-host'
import { withNativeChatCutTurnNotices } from '../../../shared/native-chat-cut-turn-notice'
import { latestNativeChatOrcaStopCut } from '../../../shared/native-chat-orca-stop-cut'
import { beginAgentSessionRuntimeRecord } from '../../runtime/agent-session-runtime-end-record'
import { STRUCTURED_AGENT_SESSION_IDLE_MS } from './structured-agent-session-idle-sweep'
import type { StructuredAgentSessionHostDeps } from './structured-agent-session-host-types'
import {
Expand Down Expand Up @@ -360,6 +362,41 @@ describe('a turn a read reached before the reconcile proved its owner dead', ()
).toEqual({ key: 'workedFor', duration: '27s' })
})

it('explains the turn its proof revised, so the cut reads once and offers Continue', async () => {
// The Orca that crashed recorded that it started, and never that it ended.
const crashed = crashedClaudeRecord()
const ownerProcess = crashed.lease.ownerProcess
if (!ownerProcess) {
throw new Error('the crashed record has no owner')
}
await seedTestAgentSessionRecordStore(root, {
records: [
{
...crashed,
lease: { ...crashed.lease, ownerProcess: { ...ownerProcess, runtime: 'runtime-crashed' } }
}
]
})
beginAgentSessionRuntimeRecord(root, 'runtime-crashed', TOOL_STARTED_AT - 60_000)
store = await openTestAgentSessionRecordStore(root)
openHost({ probeOwner: async () => ({ outcome: 'pid-absent' }) })
expect(await settledTurn()).toEqual(UNVERIFIABLE_TURN)

await host.reconcileRestartLeases()
await drainSession()

const { items } = await host.journalSnapshot(SESSION)
const turnItemId = items.find((item) => item.body.kind === 'turn')?.itemId
expect(items.flatMap((item) => (item.body.kind === 'status' ? [item.turnScope] : []))).toEqual([
{ kind: 'turn', turnItemId }
])
expect(latestNativeChatOrcaStopCut(items, [])).toEqual({ turnItemId, cause: 'crash' })
const readerRows = withNativeChatCutTurnNotices(items, { agentName: 'Claude' }).filter(
(item) => item.body.kind === 'status'
)
expect(readerRows).toHaveLength(1)
})

it('revises nothing twice, whoever re-runs the settle', async () => {
openHost({ probeOwner: async () => ({ outcome: 'pid-absent' }) })
await host.history({ sessionId: SESSION, direction: 'tail' })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,9 @@ import {
} from './structured-agent-session-stale-turn-verdict'
import {
exitedRootTurnScope,
runningRootTurnScope
settledRootTurnScope
} from './structured-agent-session-exit-turn-scope'
import { orcaStopRowBody } from './structured-agent-session-orca-stop-row'
import {
hasUnfinishedStructuredAgentSessionWork,
isInProgressStructuredAgentSessionItem,
Expand Down Expand Up @@ -242,16 +243,9 @@ export async function settleStaleStructuredAgentSessionState(input: {
clientMessageId: `${STALE_SESSION_ROW_PREFIX}${input.sessionId}:death-${evidence.ownerFence ?? 'unowned'}-${evidence.observedAt}`
},
// The death evidence is Orca's log text, never a sentence for a person: the row says only
// that the provider stopped.
body: {
kind: 'status',
...agentSessionFailureWords(agentSessionFailureFact('providerExited'), {
...input.failureTextContext,
surface: 'row'
}),
tone: 'error'
},
turnScope: runningRootTurnScope(items)
// that the provider stopped, and how Orca ended when the provider died with it.
body: orcaStopRowBody(input.failureTextContext, evidence.runtimeEnd),
turnScope: settledRootTurnScope(items, turnEnds)
})
}
for (const chunk of partitionJournalLifecycleMutations(settlementId, mutations)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
type AgentJournalTurnScope
} from '../../../shared/agent-session-journal-types'
import { readAgentJournalTurn } from '../../../shared/agent-session-turn-record'
import { agentJournalItemKey } from '../../../shared/agent-session-journal-item-key'
import type { JournalLifecycleMutationInput } from '../agent-session-journal/journal-row-builders'
import type { StructuredAgentSessionTurnVerdict } from './structured-agent-session-stale-turn-verdict'

/** The turn the exit ended: still running, or already ended at the exit's instant by the child's
Expand All @@ -26,11 +28,13 @@ export function exitedRootTurnScope(
return ended ? { kind: 'turn', turnItemId: ended.itemId } : AGENT_JOURNAL_THREAD_SCOPE
}

export function runningRootTurnScope(
items: readonly AgentJournalRenderItem[]
/** The newest root turn a settle ends: one still running, or one an earlier settle left
* `unverifiable` that a proof now revises, whose row no longer reads running. */
export function settledRootTurnScope(
items: readonly AgentJournalRenderItem[],
turnEnds: readonly JournalLifecycleMutationInput[]
): AgentJournalTurnScope {
const running = items.findLast(
(item) => isRootAgentJournalItem(item) && readAgentJournalTurn(item.body)?.state === 'running'
)
return running ? { kind: 'turn', turnItemId: running.itemId } : AGENT_JOURNAL_THREAD_SCOPE
const ended = new Set(turnEnds.map((mutation) => agentJournalItemKey(mutation.identity)))
const settled = items.findLast((item) => isRootAgentJournalItem(item) && ended.has(item.itemId))
return settled ? { kind: 'turn', turnItemId: settled.itemId } : AGENT_JOURNAL_THREAD_SCOPE
}
Loading
Loading