Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
ea3ee4f
fix(native-chat): a turn a proven crash cut short reads interrupted, …
brennanb2025 Sep 27, 2026
9c46353
fix(native-chat): end a probe-proven turn at the last row the journal…
brennanb2025 Sep 27, 2026
498d539
test(native-chat): give the unexpected-exit fake journal its live-act…
brennanb2025 Sep 27, 2026
f82f3ed
refactor(native-chat): read the journal's live bound only for a probe…
brennanb2025 Sep 27, 2026
25e6f51
fix(native-chat): mark what a journal open settles for a gone host as…
brennanb2025 Sep 27, 2026
7076351
fix(native-chat): bound a probe-proven death with the last time Orca …
brennanb2025 Sep 27, 2026
577b9ab
test(native-chat): a failed acquisition parked in recovery keeps its …
brennanb2025 Sep 27, 2026
1afd727
Merge remote-tracking branch 'origin/main' into brennanb2025/crash-tu…
brennanb2025 Sep 28, 2026
65629a3
refactor(native-chat): move the submission dispatch folds out of the …
brennanb2025 Sep 28, 2026
27619ee
test(native-chat): a send or reader that opens a crashed chat settles…
brennanb2025 Sep 28, 2026
11b1a91
docs(native-chat): say which proofs of death record a last proof of life
brennanb2025 Sep 28, 2026
b22c1b8
fix(native-chat): a proof of life bounds only the owner that wrote th…
brennanb2025 Sep 28, 2026
514c9d1
test(native-chat): a crash with a send in doubt still ends at the las…
brennanb2025 Sep 28, 2026
63f6637
fix(native-chat): a proof of death judges only the turn its own owner…
brennanb2025 Sep 28, 2026
1784860
test(native-chat): every writer of a watched exit names the owner it …
brennanb2025 Sep 28, 2026
7ff03e3
test(native-chat): give the fake journal's cast its safety rationale
brennanb2025 Sep 28, 2026
5575b03
Merge remote-tracking branch 'origin/main' into brennanb2025/crash-tu…
brennanb2025 Sep 28, 2026
2254c6f
Merge remote-tracking branch 'origin/main' into brennanb2025/crash-tu…
brennanb2025 Sep 28, 2026
45b6d97
fix(native-chat): a proof of death written after a chat opened revise…
brennanb2025 Sep 28, 2026
db09dd4
test(native-chat): a chat read before the reconcile reads unverifiabl…
brennanb2025 Sep 28, 2026
54a4dd1
fix(native-chat): revise an open chat's turn wherever a proof of deat…
brennanb2025 Sep 28, 2026
e4ce520
test(native-chat): a failed exit settlement is retried in place once …
brennanb2025 Sep 28, 2026
1eca100
test(native-chat): tests that pin a send settling a failed exit refus…
brennanb2025 Sep 28, 2026
c32dd59
Merge remote-tracking branch 'origin/main' into brennanb2025/crash-tu…
brennanb2025 Sep 28, 2026
286b1b3
fix(native-chat): name the explanation row by the death it explains, …
brennanb2025 Sep 28, 2026
e5cf5a0
fix(native-chat): end a crashed turn at its owner's provider output, …
brennanb2025 Sep 28, 2026
ed7cce4
fix(native-chat): end a crashed turn at its last proof of life, never…
brennanb2025 Sep 28, 2026
316d64e
Merge remote-tracking branch 'origin/main' into brennanb2025/crash-tu…
brennanb2025 Sep 28, 2026
0b0beb5
test(native-chat): drop a stale reference to the removed live-activit…
brennanb2025 Sep 28, 2026
728d3ec
Merge remote-tracking branch 'origin/main' into brennanb2025/crash-tu…
brennanb2025 Sep 29, 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
20 changes: 16 additions & 4 deletions src/main/native-chat/agent-session-journal/journal-reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ export type JournalReducerState = {
oldestSequence: number
highestFence: number
items: Map<string, AgentJournalRenderItem>
/** Fence of the writer that created each item: the generation a running turn belongs to. */
itemFences: Map<string, number>
/** Revision of a removed item, so a late lower revision cannot resurrect it. */
tombstones: Map<string, number>
submissions: Map<string, AgentJournalSubmission>
Expand All @@ -61,6 +63,7 @@ export function createJournalReducerState(sessionId: string, epoch: string): Jou
oldestSequence: 1,
highestFence: 0,
items: new Map(),
itemFences: new Map(),
tombstones: new Map(),
submissions: new Map(),
receipts: new Map(),
Expand All @@ -82,7 +85,13 @@ export function applyJournalRow(state: JournalReducerState, row: JournalRow): vo
}
const itemId = resolveJournalItemId(state, row.itemId, row.body)
acceptSubmissionFromProviderItem(state, row.itemId, itemId, row)
upsertItem(state, itemId, row.revision, journalRenderItem(itemId, row.revision, row.body, row))
upsertItem(
state,
itemId,
row.revision,
journalRenderItem(itemId, row.revision, row.body, row),
row.fence
)
return
}
if (row.kind === 'tombstone') {
Expand All @@ -103,7 +112,7 @@ export function applyJournalRow(state: JournalReducerState, row: JournalRow): vo
acceptSubmissionFromProviderItem(state, mutation.itemId, itemId, row)
const producer = journalBatchMutationProducer(row, mutation)
const item = journalRenderItem(itemId, revision, body, row, producer, sequenceIndex)
upsertItem(state, itemId, revision, item)
upsertItem(state, itemId, revision, item, row.fence)
} else {
removeItem(state, resolveItemId(state, mutation.itemId), mutation.revision)
}
Expand Down Expand Up @@ -187,7 +196,8 @@ function upsertItem(
state: JournalReducerState,
itemId: string,
revision: number,
next: AgentJournalRenderItem
next: AgentJournalRenderItem,
fence: number
): void {
const tombstoned = state.tombstones.get(itemId)
if (tombstoned !== undefined && revision <= tombstoned) {
Expand All @@ -199,6 +209,7 @@ function upsertItem(
}
if (!existing) {
state.items.set(itemId, next)
state.itemFences.set(itemId, fence)
state.tombstones.delete(itemId)
return
}
Expand Down Expand Up @@ -237,6 +248,7 @@ function removeItem(state: JournalReducerState, itemId: string, revision: number
}
state.tombstones.set(itemId, revision)
state.items.delete(itemId)
state.itemFences.delete(itemId)
}

function applySubmission(
Expand All @@ -255,7 +267,7 @@ function applySubmission(
...(row.handoverRecorded ? { handoverRecorded: true, acceptedSequence: row.seq } : {})
})
const itemId = agentJournalSubmissionKey(row.clientMessageId)
upsertItem(state, itemId, 0, journalRenderItem(itemId, 0, row.body, row))
upsertItem(state, itemId, 0, journalRenderItem(itemId, 0, row.body, row), row.fence)
}

function acceptSubmissionFromProviderItem(
Expand Down
3 changes: 3 additions & 0 deletions src/main/native-chat/agent-session-journal/journal-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,9 @@ 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. */
itemFence = (itemId: string): number | undefined => this.state.itemFences.get(itemId)

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

pendingSubmissions = (): AgentJournalSubmission[] =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

import type { AgentJournalResetReason } from '../../../shared/agent-session-journal-types'
import { journalDirectoryFor } from '../agent-session-journal/journal-paths'
import type { AgentSessionJournal } from '../agent-session-journal/journal-store'
import { openAgentSessionJournalWithRecovery } from './agent-session-journal-recovery'
import { computeAgentSessionPayloadFingerprint } from '../../../shared/agent-session-mutation-envelope'
import type { AgentSessionRecord } from '../../../shared/agent-session-record'
Expand Down Expand Up @@ -103,30 +104,54 @@ export async function openStructuredAgentSessionConversationJournal(
} catch (error) {
deps.onEventSinkError?.({ sessionId, error })
}
try {
// No child in this process writes to a journal nobody had open, so whatever it shows running
// belongs to a generation that is gone, whatever the lease still claims. Settled before any
// reader or child sees it.
if (!options.acquisition) {
await settleStaleStructuredAgentSessionState({
journal: opened.journal,
sessionId,
fence,
acquisitionGeneration: null,
deathEvidence: record.lease.deathEvidence ?? null,
failureTextContext: structuredAgentSessionFailureWordsContext(record)
})
}
} catch (error) {
// Best effort: the next acquire re-derives it.
deps.onEventSinkError?.({ sessionId, error })
// No child in this process writes to a journal nobody had open, so whatever it shows running
// belongs to a generation that is gone, whatever the lease still claims. Settled before any
// reader or child sees it.
if (!options.acquisition) {
await settleGoneGeneration(deps, record, opened.journal)
}
return {
session: { journal: opened.journal, params, child: null },
reset: opened.recovery?.reset ?? null
}
}

/**
* The open's settle again, for a conversation already open: a proof of death written since it
* opened (the startup reconcile, a recovery) revises what the open could only call `unverifiable`.
* A record holds a proof only while released, so no child here is writing. A no-op once revised.
*/
export async function resettleOpenStructuredAgentSessionConversation(
deps: StructuredAgentSessionConversationOpenDeps,
sessionId: string,
session: StructuredAgentSessionHostSession | undefined
): Promise<void> {
const record = deps.store.getRecord(sessionId)
if (session && record?.lease.deathEvidence) {
await settleGoneGeneration(deps, record, session.journal)
}
}

async function settleGoneGeneration(
deps: Pick<StructuredAgentSessionConversationOpenDeps, 'onEventSinkError'>,
record: AgentSessionRecord,
journal: AgentSessionJournal
): Promise<void> {
try {
await settleStaleStructuredAgentSessionState({
journal,
sessionId: record.sessionId,
fence: record.lease.runtimeFence,
acquisitionGeneration: null,
deathEvidence: record.lease.deathEvidence ?? null,
failureTextContext: structuredAgentSessionFailureWordsContext(record)
})
} catch (error) {
// Best effort: the next open or acquire re-derives it.
deps.onEventSinkError?.({ sessionId: record.sessionId, error })
}
}

export function attachParamsForRecord(
record: AgentSessionRecord,
input: {
Expand Down
Loading
Loading