Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
d438377
feat(native-chat): record which agent run wrote each journal item
brennanb2025 Oct 12, 2026
db574ae
feat(native-chat): one current-work projection for every host reader;…
brennanb2025 Oct 12, 2026
7f946e4
refactor(native-chat): keep structuredAgentSessionHandsOver file-loca…
brennanb2025 Oct 13, 2026
d6925c4
test(native-chat): fewer provenance tests: fold reopen and unstated-o…
brennanb2025 Oct 13, 2026
625f5ea
Merge branch 'brennanb2025/settle-split-1' into brennanb2025/settle-s…
brennanb2025 Oct 13, 2026
362532e
test(native-chat): drop current-work mirrors: the viewKey unit case a…
brennanb2025 Oct 13, 2026
f0a45c2
test(native-chat): an unstamped roster entry from an older build read…
brennanb2025 Oct 13, 2026
3f47bac
Merge branch 'brennanb2025/settle-split-1' into brennanb2025/settle-s…
brennanb2025 Oct 13, 2026
2e19393
Merge remote-tracking branch 'origin/main' into brennanb2025/settle-s…
brennanb2025 Oct 14, 2026
ada76fe
test(native-chat): build the older-build roster row inline, after mai…
brennanb2025 Oct 14, 2026
75f2c36
Merge remote-tracking branch 'origin/main' into brennanb2025/settle-s…
brennanb2025 Oct 14, 2026
d4471e8
refactor(native-chat): provenance is per item only; drop per-entry st…
brennanb2025 Oct 14, 2026
7cace4b
Merge branch 'brennanb2025/settle-split-1' into brennanb2025/settle-s…
brennanb2025 Oct 14, 2026
efa2676
fix(native-chat): an item's provenance is set when it is created; rev…
brennanb2025 Oct 14, 2026
28edc8d
test(native-chat): keep the provenance test below the production line…
brennanb2025 Oct 14, 2026
00e7f56
Merge branch 'brennanb2025/settle-split-1' into brennanb2025/settle-s…
brennanb2025 Oct 14, 2026
387d313
Merge remote-tracking branch 'origin/main' into brennanb2025/settle-s…
brennanb2025 Oct 14, 2026
a9b74f4
Merge branch 'brennanb2025/settle-split-1' into brennanb2025/settle-s…
brennanb2025 Oct 14, 2026
a7c5af9
Merge remote-tracking branch 'origin/main' into brennanb2025/settle-s…
brennanb2025 Oct 14, 2026
d8095a4
Merge origin/main (6d519f7835a) into settle-split part 2
brennanb2025 Oct 15, 2026
674dbed
Merge origin/main (1bdbc236480) into settle-split part 2
brennanb2025 Oct 15, 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
2 changes: 2 additions & 0 deletions config/scripts/vitest-sqlite-runtime-files.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ export const SQLITE_RUNTIME_INCLUDE = [
'src/main/native-chat/agent-session-wire/structured-agent-session-event-sink.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-failed-create-owner-verdict.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-failed-create-sink-release.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-current-work.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-forget-status.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-grouped-prompt.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-held-child-lease-renewal.test.ts',
Expand Down Expand Up @@ -166,6 +167,7 @@ export const SQLITE_RUNTIME_INCLUDE = [
'src/main/native-chat/agent-session-wire/structured-agent-session-restart-stream-duration.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-rewind-at-rest.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-rewind-live-send.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-rewind-provenance.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-rewind.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-clear-rewind.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-send-idempotency.test.ts',
Expand Down
14 changes: 10 additions & 4 deletions src/main/agent-hooks/first-work-branch-rename.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,12 +102,15 @@ describe('maybeAutoRenameBranchOnFirstWork', () => {
// A real journal's sequence only ever advances, so the feed's projection
// cache must miss on every publish here: this test is about the rename.
let sequence = 0
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the status feed reads only cursor(), lastActivityAt(), stopMarks and snapshot() of a journal.
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the status feed reads only cursor(), lastActivityAt(), stopMarks, snapshot(), the provenance reads and runningTurn() of a journal.
const journal = {
snapshot: () => ({ items, submissions: [] }),
stopMarks: { latest: () => null, revision: () => 0 },
lastActivityAt: () => 1,
cursor: () => ({ epoch: 1, sequence: (sequence += 1) })
cursor: () => ({ epoch: 1, sequence: (sequence += 1) }),
itemFence: () => undefined,
wroteBeforeOpen: () => false,
runningTurn: () => null
} as unknown as AgentSessionJournal
const pending: Promise<void>[] = []
const observe = vi.fn((summary, options) => {
Expand Down Expand Up @@ -191,7 +194,7 @@ describe('maybeAutoRenameBranchOnFirstWork', () => {
const { deps, setDisplayName, setRenameError } = makeDeps({
getRepo: () => ({ id: REPO_ID, kind: 'folder', path: '/workspace/platform' }) as Repo
})
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the status feed reads only cursor(), lastActivityAt(), stopMarks and snapshot() of a journal.
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the status feed reads only cursor(), lastActivityAt(), stopMarks, snapshot(), the provenance reads and runningTurn() of a journal.
const journal = {
lastActivityAt: () => 1,
stopMarks: { latest: () => null, revision: () => 0 },
Expand All @@ -208,7 +211,10 @@ describe('maybeAutoRenameBranchOnFirstWork', () => {
}
],
submissions: []
})
}),
itemFence: () => undefined,
wroteBeforeOpen: () => false,
runningTurn: () => null
} as unknown as AgentSessionJournal
const location = {
executionHostId: 'local' as const,
Expand Down
9 changes: 8 additions & 1 deletion src/main/claude/claude-background-stop-records.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,14 @@ async function droppedFromRoster() {
{
sessionId: 'session-1',
fence: 7,
journal: { snapshot: () => ({ items: [] }), submissions: () => [] },
journal: {
snapshot: () => ({ items: [] }),
submissions: () => [],
runningTurn: () => null,
itemFence: () => undefined,
visitItems: () => undefined,
wroteBeforeOpen: () => false
},
adapter: run.adapter
},
RECORD,
Expand Down
7 changes: 6 additions & 1 deletion src/main/claude/claude-background-wake-retention.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ async function wiredSession() {
},
// No Stop was ever pressed here.
stopMarks: { latest: () => null, revision: () => 0 },
// One generation produced everything here, and every send was accepted by this process.
itemFence: () => undefined,
wroteBeforeOpen: () => false,
// The live generation's own turns are never a died turn, so this is never consulted for one.
runningTurn: () => null,
snapshot: () => ({
items: [...run.journalItems.values()]
.sort((a, b) => a.sequence - b.sequence)
Expand All @@ -53,7 +58,7 @@ async function wiredSession() {
[
parent.sessionId,
{
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the fixture supplies the feed's cursor, clock, Stop marks, snapshot, submissions and item reads.
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the fixture supplies the feed's cursor, clock, Stop marks, snapshot, submissions, item and provenance reads.
journal: journal as unknown as Journal,
params: {
location: {
Expand Down
6 changes: 5 additions & 1 deletion src/main/claude/claude-subagent-permission-request.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { AgentHookServer } from '../agent-hooks/server'
import type { AgentSessionJournal } from '../native-chat/agent-session-journal/journal-store'
import { createTrackedJournalOpener } from '../native-chat/agent-session-journal/journal-host-database-test-support'
import { settleStructuredAgentSessionDeadGeneration } from '../native-chat/agent-session-wire/structured-agent-session-dead-generation-settlement'
import { StructuredAgentSessionCurrentWork } from '../native-chat/agent-session-wire/structured-agent-session-current-work'
import { createDeferredStructuredAgentSessionEventSink } from '../native-chat/agent-session-wire/structured-agent-session-event-sink'
import { testEventSinkLogging } from '../native-chat/agent-session-wire/structured-agent-session-logger-test-support'
import { createStructuredAgentSessionLogger } from '../native-chat/agent-session-wire/structured-agent-session-logger'
Expand Down Expand Up @@ -243,7 +244,10 @@ async function pipeline() {
await journal.appendItem(
identity,
{ ...card.body, resolution: { ...resolution, resolvedBy: 'client-1', resolvedAt: now() } },
{ fence: FENCE, turnScope: journal.liveTurnScope() }
{
fence: FENCE,
turnScope: new StructuredAgentSessionCurrentWork(journal, FENCE).turnScope()
}
)
}
const cardId = (): string => {
Expand Down
22 changes: 10 additions & 12 deletions src/main/codex/codex-structured-journal-exit-convergence.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { afterEach, expect, it, vi } from 'vitest'
import { AGENT_JOURNAL_THREAD_SCOPE } from '../../shared/agent-session-journal-types'
import { codexProviderHandle } from '../../shared/agent-session-provider-handle-encoding'
import { readAgentJournalTurn } from '../../shared/agent-session-turn-record'
import { isStructuredAgentSessionMainAgentWorking } from '../../shared/structured-agent-session-main-agent-working'
import type { AgentSessionRecord } from '../../shared/agent-session-record'
import {
agentSessionLeaseFixture,
Expand All @@ -23,7 +22,7 @@ import {
type StructuredAgentSessionChildExitSession
} from '../native-chat/agent-session-wire/structured-agent-session-child-exit'
import type { StructuredAgentSessionEndedEvent } from '../native-chat/agent-session-wire/structured-agent-session-adapter'
import { pendingPromptExists } from '../native-chat/agent-session-wire/structured-agent-session-queued-messages'
import { StructuredAgentSessionCurrentWork } from '../native-chat/agent-session-wire/structured-agent-session-current-work'
import { createCodexJournalTranslator } from './codex-structured-journal-translation'
import { handleCodexSessionExit } from './codex-structured-session-close'
import type { CodexSession, CodexStructuredSessionEvent } from './codex-structured-session-state'
Expand All @@ -46,15 +45,14 @@ afterEach(async () => {
}
})

function userVisible(journal: AgentSessionJournal) {
/** What the chat shows with the live generation at `liveFence`: 7 while the child runs, none once
* its exit released it (`structuredAgentSessionCurrentWork`). */
function userVisible(journal: AgentSessionJournal, liveFence: number | null) {
const items = journal.snapshot().items
const work = new StructuredAgentSessionCurrentWork(journal, liveFence)
return {
working: isStructuredAgentSessionMainAgentWorking(
journal.activeTurnId(),
journal.submissions(),
8
),
prompt: pendingPromptExists(journal),
working: work.working(),
prompt: work.hasActionablePrompt(),
turnStates: items.flatMap((item) => readAgentJournalTurn(item.body)?.state ?? []),
runningCalls: items.filter(
(item) => item.body.kind === 'tool-call' && item.body.state === 'running'
Expand Down Expand Up @@ -149,7 +147,7 @@ it('settles a refused Codex exit through the host in one commit, then permits a
).toEqual({ accepted: true })
await expect(deferred.drained()).resolves.toEqual({ ok: true })
expect(deferred.state().queuedBytes).toBe(0)
expect(userVisible(journal)).toEqual({
expect(userVisible(journal, 7)).toEqual({
working: true,
prompt: true,
turnStates: ['running'],
Expand Down Expand Up @@ -234,13 +232,13 @@ it('settles a refused Codex exit through the host in one commit, then permits a
},
sessions: new Map([[SESSION, hostSession]]),
flushLifecycle: () => deferred.lifecycleBarrier(),
publishFence: vi.fn(),
generationEnded: vi.fn(),
serialize: async (_sessionId, task) => task(),
now: () => 5_000
},
event
)
expect(userVisible(journal)).toEqual({
expect(userVisible(journal, null)).toEqual({
working: false,
prompt: false,
turnStates: ['interrupted'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ describe('atomic terminal settlement', () => {
return result
})
)
expect(await settle()).toEqual({ ok: true })
expect(await settle()).toMatchObject({ ok: true })
vi.restoreAllMocks()
const settled = journal.snapshot()
expect(published).toEqual([settled])
Expand All @@ -209,7 +209,7 @@ describe('atomic terminal settlement', () => {
expect(frames.at(-1)).toMatchObject({ type: 'batch', batch: { cursor: settled.cursor } })
const frameCount = frames.length
expect(settled.submissions[0]).toMatchObject({ dispatchState: 'unknown', recovered: true })
expect(journal.activeTurnId()).toBeNull()
expect(journal.runningTurn()?.turnId ?? null).toBeNull()
const calls = settled.items.filter((item) => item.body.kind === 'tool-call')
expect(calls).toHaveLength(201)
expect(
Expand All @@ -230,15 +230,15 @@ describe('atomic terminal settlement', () => {
'lifecycle-batch'
])
expect(committed.every((row) => row.recovered)).toBe(true)
expect(await settle()).toEqual({ ok: true })
expect(await settle()).toMatchObject({ ok: true })
expect(journal.snapshot()).toEqual(settled)
expect(rows().slice(diskBefore.length)).toEqual(committed)
expect(published).toEqual([settled])
expect(frames).toHaveLength(frameCount)
await journal.close()
journal = await open()
expect(journal.snapshot()).toEqual(settled)
expect(await settle()).toEqual({ ok: true })
expect(await settle()).toMatchObject({ ok: true })
expect(journal.snapshot()).toEqual(settled)
}
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import { journalStopEventRowBuilder } from './journal-stop-and-resume-rows'
export type JournalStopAcceptanceInput = {
event: Omit<JournalStopEvent, 'at'>
fence: number
/** The turn live as the event is written, by the host's read of what runs. */
liveTurnId: () => string | null
}

// Only host-held sends are withdrawn, so each carries proof it never reached the agent.
Expand All @@ -36,7 +38,6 @@ export class JournalStopAcceptor {
private readonly deps: {
writer: Pick<JournalRowWriter, 'enqueueRows'>
state: () => JournalReducerState
liveTurnId: () => string | null
}
) {}

Expand All @@ -56,7 +57,7 @@ export class JournalStopAcceptor {
// The same rows the chat's queued withdrawal always wrote, then the event, naming the turn
// live as it is written unless the Stop named one.
() => {
turnId = input.event.turnId ?? this.deps.liveTurnId()
turnId = input.event.turnId ?? input.liveTurnId()
const event = { ...input.event, ...(turnId !== null ? { turnId } : {}) }
return [
...journalQueuedRejectionRowBuilders(state, input.fence, STOP_WITHDRAWAL),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,11 +132,7 @@ export function createJournalStoreCollaborators(host: JournalStoreHost): Journal
writer: rowWriter,
cards: queuedMessages
}),
stops: new JournalStopAcceptor({
writer: rowWriter,
state: host.state,
liveTurnId: () => host.journal().activeTurnId()
}),
stops: new JournalStopAcceptor({ writer: rowWriter, state: host.state }),
// Behind the stored fact: settles drafts whose consumed submission the loaded journal shows
// refused (a downgrade wrote no hook), then prunes. Bookkeeping, never failing the open.
restore: () =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ describe('sequences', () => {
const rendered = (): string | null =>
activeStructuredAgentSessionTurnId(journal.snapshot().items)
const bothAgreeOn = async (turnId: string | null): Promise<void> => {
expect(journal.activeTurnId()).toBe(turnId)
expect(journal.runningTurn()?.turnId ?? null).toBe(turnId)
expect(rendered()).toBe(turnId)
}

Expand Down
24 changes: 9 additions & 15 deletions src/main/native-chat/agent-session-journal/journal-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,15 @@ import type {
AgentJournalSubmission,
AgentJournalThreadGoal,
AgentJournalTurnLifecycle,
AgentJournalTurnScope,
AgentSessionJournalIdentity
} from '../../../shared/agent-session-journal-types'
import { agentJournalItemKey } from '../../../shared/agent-session-journal-item-key'
import { currentAgentSessionThreadGoalBySequence } from '../../../shared/agent-session-thread-goal'
import type { AgentSessionContextUsage } from '../../../shared/agent-session-context-usage'
import { latestStructuredAgentContextFacts } from '../../../shared/structured-agent-session-context-usage'
import {
activeStructuredAgentSessionTurnIdBySequence,
liveStructuredAgentSessionTurnScope,
newestStructuredAgentSessionTurnBySequence
newestStructuredAgentSessionTurnBySequence,
runningStructuredAgentSessionTurnItemBySequence
} from '../../../shared/structured-agent-session-live-turn'
import type { JournalReplacementItem } from './journal-epoch-replacement'
import { readJournalSince } from './journal-cursor'
Expand Down Expand Up @@ -227,14 +225,11 @@ export class AgentSessionJournal {
}
}

/** The turn this journal has published as running — the same read a client's snapshot gives,
* without materialising one. */
activeTurnId = (): string | null =>
activeStructuredAgentSessionTurnIdBySequence(this.state.items.values())

/** Where a row written now belongs: the running turn, or the conversation. */
liveTurnScope = (): AgentJournalTurnScope =>
liveStructuredAgentSessionTurnScope(this.state.items.values())
/** The turn this journal records as running, with its record's item, whichever generation
* opened it — the same read a client's snapshot gives, without materialising one. Whether it is
* current work is the host projection's to say (`structuredAgentSessionCurrentWork`). */
runningTurn = (): { item: AgentJournalRenderItem; turnId: string } | null =>
runningStructuredAgentSessionTurnItemBySequence(this.state.items.values())

/** The newest turn record whatever state it settled in, for readers that need the outcome. */
newestTurn = (): AgentJournalTurnLifecycle | null =>
Expand All @@ -251,12 +246,11 @@ export class AgentSessionJournal {
/** Includes revisions and completion tombstones, whose timestamps disappear from render items. */
lastActivityAt = (): number => this.state.lastActivityAt

/** Fence of the writer that created the item, while it is in the timeline. */
/** The item's execution provenance (`JournalItemRow.ownerFence`), while it is in the timeline. */
itemFence = (itemId: string): number | undefined => this.state.itemFences.get(itemId)

/** Latest saved output at this fence, excluding client actions and recovery bookkeeping. */
lastProviderActivityAt = (fence: number): number | undefined =>
this.state.providerActivityAt.get(fence)
lastProviderActivityAt = (fence: number) => this.state.providerActivityAt.get(fence)

submissions = (): AgentJournalSubmission[] => [...this.state.submissions.values()]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ it.each([
turnId: 'old',
state: 'interrupted'
})
expect(journal.activeTurnId()).toBeNull()
expect(journal.runningTurn()?.turnId ?? null).toBeNull()
const ended = journal.snapshot()
await settle(observedAt, lastProvenAliveAt)
expect(journal.snapshot()).toEqual(ended)
Expand Down Expand Up @@ -145,7 +145,7 @@ it.each([{ kind: 'tool-call' }, { providerTurnId: '' }])(
state: 'interrupted',
completedAt: 1_000
})
expect(journal.activeTurnId()).toBeNull()
expect(journal.runningTurn()?.turnId ?? null).toBeNull()
await expectSendStillWritable()
}
)
Expand Down Expand Up @@ -329,7 +329,7 @@ it.each([
}
expect(journal.itemBody('orca:extension-7')).toHaveProperty('laterField', { kind: 'tool-call' })
expect(journal.itemBody('orca:extension-6')).toHaveProperty('contextUsage', laterContextUsage)
expect(journal.activeTurnId()).toBeNull()
expect(journal.runningTurn()?.turnId ?? null).toBeNull()
expect(journal.snapshot().items).toHaveLength(inputs.length + 1)
const ended = journal.snapshot()
await settle(observedAt, lastProvenAliveAt)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@ function buildPage(input: {
// A catch-up run pages one snapshot many times; its scan back to the turn record is paid once.
const latestTurnBySnapshot = new WeakMap<AgentJournalSnapshot, AgentSessionLatestTurn | null>()

function snapshotLatestTurn(snapshot: AgentJournalSnapshot): AgentSessionLatestTurn | null {
export function snapshotLatestTurn(snapshot: AgentJournalSnapshot): AgentSessionLatestTurn | null {
let latest = latestTurnBySnapshot.get(snapshot)
if (latest === undefined) {
latest = latestStructuredAgentSessionTurn(snapshot.items)
Expand Down
Loading
Loading