Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
26c8701
Carry a person's stopped messages to the agent with their next send
brennanb2025 Oct 10, 2026
7e08ad7
Let a queued card's hand-off carry stopped history; test the exactly-…
brennanb2025 Oct 10, 2026
fb2f4d2
Keep a send that carried stopped history out of fingerprint reconcili…
brennanb2025 Oct 10, 2026
19a943d
Owe a stopped message only on proof it never reached the agent; carry…
brennanb2025 Oct 10, 2026
2d2733d
Owe stopped messages only where Orca held them; list only the history…
brennanb2025 Oct 10, 2026
86793da
Build the swept-send test's marked fact with the shared builder
brennanb2025 Oct 10, 2026
86fbc23
Keep a person's own words when a rewind rebuilds their message
brennanb2025 Oct 11, 2026
6a9a28e
Require the logger on the handover context
brennanb2025 Oct 11, 2026
855b261
Write a plain Claude send as soon as before when no stopped history r…
brennanb2025 Oct 11, 2026
4fba415
Keep stopped messages and their carriers' coverage through a rewind
brennanb2025 Oct 12, 2026
d610a83
Test that a rewind keeps stopped messages owed or covered
brennanb2025 Oct 12, 2026
90a5ad0
Owe stopped history again when a carrier provably never reached the a…
brennanb2025 Oct 12, 2026
048458f
Share the Pi adapter test harness with other Pi tests
brennanb2025 Oct 12, 2026
9696670
Prepare each send once with the stopped history that fits, in one bat…
brennanb2025 Oct 12, 2026
4141213
Keep a stopped or restarted queue paused through a rewind after a /clear
brennanb2025 Oct 12, 2026
2fc29aa
Restate a rewind's stopped messages in the order they were sent
brennanb2025 Oct 12, 2026
9edc5cc
Merge branch 'brennanb2025/snu-r2-delivery' into brennanb2025/snu-r2-…
brennanb2025 Oct 12, 2026
aaa115f
Merge branch 'brennanb2025/snu-r2-rewind' into brennanb2025/snu-r2-in…
brennanb2025 Oct 12, 2026
0e50a06
Fit the rewind tests to the prepared-dispatch frame
brennanb2025 Oct 12, 2026
98db061
Cap carried history at 64k characters and degrade only the oldest sto…
brennanb2025 Oct 12, 2026
827ff78
Owe stopped history again when ACP or Claude refused its carrier befo…
brennanb2025 Oct 12, 2026
9d79e17
Strip a carried-history frame before capping a Grok preview
brennanb2025 Oct 12, 2026
a5e6671
Leave a title that opens like a frame but never closes it unchanged
brennanb2025 Oct 12, 2026
db519bf
Drop the image labels Codex writes for carried history images from ti…
brennanb2025 Oct 12, 2026
ed3004a
Read a stopped message's images one by one so a refused image leaves …
brennanb2025 Oct 12, 2026
26ebce0
Settle a Pi prompt still waiting out a sign-in refusal as never reach…
brennanb2025 Oct 12, 2026
e6c4ffd
Name the journal rewind module after the rows it builds
brennanb2025 Oct 12, 2026
8e5113a
Type Pi's rejection helper with the facts that can reject a message
brennanb2025 Oct 12, 2026
c2159c8
Hide everything ahead of a carried-history frame within a bounded loo…
brennanb2025 Oct 12, 2026
8951827
Fit Pi's stopped history beside the line it is written, and settle an…
brennanb2025 Oct 12, 2026
ebd931c
Record stopped messages appended to the agent's own log in the journal
brennanb2025 Oct 12, 2026
a56bf60
Derive owed stopped messages from append records as well as ACP and P…
brennanb2025 Oct 12, 2026
9dd7c5d
Append owed stopped messages to the agent's own log at the next handover
brennanb2025 Oct 12, 2026
c2951ef
Write Claude's stopped messages as shouldQuery:false frames that run …
brennanb2025 Oct 12, 2026
20d604c
Inject Codex's stopped messages with its own turn_aborted record whil…
brennanb2025 Oct 12, 2026
5e05037
Carry stopped messages for ACP and Pi as plain text, whole messages only
brennanb2025 Oct 12, 2026
d3fa13a
Drop the stopped-history frame stripping from Session History scanners
brennanb2025 Oct 12, 2026
83b2319
Keep stopped messages and their append records through a Codex rewind
brennanb2025 Oct 12, 2026
b47474f
Keep an append record through a Codex rewind only where Codex keeps i…
brennanb2025 Oct 12, 2026
2fb06d2
Check, bound and settle each stopped-message append once
brennanb2025 Oct 12, 2026
0b8d2d5
Give ACP's carried stopped messages a text block of their own
brennanb2025 Oct 12, 2026
a1d9cf1
Hide Codex's own <turn_aborted> record in Session History
brennanb2025 Oct 12, 2026
e8d575f
Give carried stopped-message preparation one time limit, so a stalled…
brennanb2025 Oct 12, 2026
8f60460
Read Codex's stopped images within Orca's attachment limit, not Claude's
brennanb2025 Oct 12, 2026
c902ff3
Append only while the agent is idle by its own account, and record no…
brennanb2025 Oct 12, 2026
18b8438
Find a Codex rewind's cut in one place for kept rows and kept append …
brennanb2025 Oct 12, 2026
8e4057e
Drop the rewind send-order sort, share the interruption marker, and r…
brennanb2025 Oct 12, 2026
628a6f5
Test the append end to end through the router, and a late append echo…
brennanb2025 Oct 12, 2026
29d5c50
Leave out a stopped image whose read stalls, and never read that file…
brennanb2025 Oct 13, 2026
d56e58d
Inject Codex's stopped images only into a Codex that resizes injected…
brennanb2025 Oct 13, 2026
ca356f2
Skip and log a kept append record no rebuilt row precedes, instead of…
brennanb2025 Oct 13, 2026
c3a7bd9
Restate a rewound stopped send's rejection through the one dispatch-r…
brennanb2025 Oct 13, 2026
5cd9988
Keep a stopped image unread until every abandoned read of it settles,…
brennanb2025 Oct 13, 2026
5933f96
Merge remote-tracking branch 'origin/main' into brennanb2025/snu-stop…
brennanb2025 Oct 13, 2026
f799dff
Read stopped-history transcripts through the one projection every cli…
brennanb2025 Oct 13, 2026
82e5b3e
Merge remote-tracking branch 'origin/main' into brennanb2025/snu-stop…
brennanb2025 Oct 13, 2026
04a62cf
Deliver stopped messages with the next send: Codex by injection, ever…
brennanb2025 Oct 13, 2026
036e89d
Bound stopped history by one send's size beyond the newest, counting …
brennanb2025 Oct 13, 2026
9bfa8f5
Keep a whole stopped-history delivery within one send, counting eithe…
brennanb2025 Oct 13, 2026
535623e
Budget stopped history by the agent's remaining context beside the se…
brennanb2025 Oct 13, 2026
1bfe8ab
Bound stopped history by one send again, not the agent's context: eac…
brennanb2025 Oct 13, 2026
2e61ec4
Bound stopped history together with the send it rides with
brennanb2025 Oct 13, 2026
21f969a
Never owe a stopped message too large to deliver beside any send
brennanb2025 Oct 13, 2026
41341f0
Owe a stopped message only if it fits beside a one-letter send
brennanb2025 Oct 13, 2026
a6f2a4e
Merge origin/main: write the never-reached proof inside the Stop's save
brennanb2025 Oct 14, 2026
c051208
Deliver Codex's stopped messages inline, like every other agent
brennanb2025 Oct 14, 2026
4e5ac0b
Merge remote-tracking branch 'origin/main' into brennanb2025/snu-stop…
brennanb2025 Oct 14, 2026
3d4f04d
Owe a Codex send's stopped messages again when Codex declines its tur…
brennanb2025 Oct 14, 2026
50f3eb7
Merge commit '069bbaa4f98' into brennanb2025/snu-stopped-history
brennanb2025 Oct 14, 2026
5aa2e10
Merge remote-tracking branch 'origin/main' into brennanb2025/snu-stop…
brennanb2025 Oct 14, 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
9 changes: 7 additions & 2 deletions src/main/acp/acp-structured-session-adapter-lifecycle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,12 @@ describe('ACP Stop names a turn', () => {
expect(rig.sent('session/cancel')).toHaveLength(2)
expect(rig.settled).toEqual(
expect.arrayContaining([
expect.objectContaining({ clientMessageId: 'follow-up', state: 'rejected' })
// Held by Orca, never prompted: proof it never reached the agent.
expect.objectContaining({
clientMessageId: 'follow-up',
state: 'rejected',
rejection: { kind: 'cancelled', neverReachedAgent: true }
})
])
)
})
Expand Down Expand Up @@ -221,7 +226,7 @@ describe('ACP connection loss', () => {
).toEqual({
state: 'rejected',
reason: 'Grok stopped before this message was sent.',
rejection: { kind: 'providerExited' }
rejection: { kind: 'providerExited', neverReachedAgent: true }
})
// The next start's stop of the old child is one Orca asked for; it does not rename the crash.
await expect(rig.acquire()).rejects.toMatchObject({
Expand Down
3 changes: 2 additions & 1 deletion src/main/acp/acp-structured-session-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,13 +330,14 @@ export class AcpStructuredSessionAdapter implements StructuredAgentSessionAdapte
void this.stop(session.sessionId, false)
}

/** Only for a send refused before its prompt was written. */
private rejected(
session: AcpStructuredSession,
kind: 'providerExited'
): AgentSessionDispatchOutcome {
return {
state: 'rejected',
...agentSessionFailureWords(agentSessionFailureFact(kind), {
...agentSessionFailureWords(agentSessionFailureFact(kind, { neverReachedAgent: true }), {
surface: 'rejection',
agentName: acpAgentName(session.spec.agent)
})
Expand Down
5 changes: 4 additions & 1 deletion src/main/acp/acp-structured-turns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,10 @@ export class AcpStructuredTurns {
private withdrawSteers(): boolean {
const withdrawn = this.steers.splice(0)
for (const send of withdrawn) {
this.reject(send.clientMessageId, agentSessionFailureFact('cancelled'))
this.reject(
send.clientMessageId,
agentSessionFailureFact('cancelled', { neverReachedAgent: true })
)
}
return withdrawn.length > 0
}
Expand Down
6 changes: 5 additions & 1 deletion src/main/claude/claude-structured-dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,11 @@ export async function dispatchClaudeTurn(
}
const startupFailure = claudeStartupFailureFact(session)
if (startupFailure) {
return { state: 'rejected', ...claudeDispatchRejection(startupFailure) }
// Refused before the write, so the agent never saw it.
return {
state: 'rejected',
...claudeDispatchRejection({ ...startupFailure, neverReachedAgent: true })
}
}
// Read the sent content, not the journal blocks: only the mapped trailing prompt decides
// whether Claude runs a command, so the two cannot disagree about which frame settles this.
Expand Down
10 changes: 10 additions & 0 deletions src/main/claude/claude-structured-queued-stop.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,16 @@ describe('Stop with a follow-up Claude queued behind the running turn', () => {
expect(bySessionStart).toEqual(byInit)
})

// Claude may already hold a swept send in the stopped turn, so it is never re-sent as history.
it('settles the swept follow-up without proof it never reached Claude', async () => {
const { settlements } = await stopWithQueuedFollowUp(
claudeCli({ initProof: 'init', capabilities: CAPABILITIES }),
() => {}
)
expect(settlements[0]).toMatchObject({ state: 'rejected' })
expect(JSON.stringify(settlements[0])).not.toContain('neverReachedAgent')
})

it('keeps what a turn advertised when a later frame names nothing', async () => {
const result = await stopWithQueuedFollowUp(
claudeCli({ initProof: 'session-start' }),
Expand Down
3 changes: 2 additions & 1 deletion src/main/codex/codex-structured-dispatch-admission.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,8 @@ describe('codex dispatch admission', () => {
reason: "Codex didn't accept this message: thread not found.",
rejection: {
kind: 'providerRejected',
detail: { text: 'thread not found', audience: 'person' }
detail: { text: 'thread not found', audience: 'person' },
neverReachedAgent: true
}
})

Expand Down
2 changes: 1 addition & 1 deletion src/main/codex/codex-structured-session-adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -593,7 +593,7 @@ describe('CodexStructuredSessionAdapter.dispatch', () => {
// Built without Codex's own words, so nothing is quoted and no detail is invented.
state: 'rejected',
reason: "Codex didn't accept this message.",
rejection: { kind: 'providerRejected' }
rejection: { kind: 'providerRejected', neverReachedAgent: true }
})
})

Expand Down
4 changes: 3 additions & 1 deletion src/main/codex/codex-structured-turn-end-settlement.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,12 @@ describe('a Codex send its turn ended without echoing', () => {
rig.turns.end('interrupted')

expect(rig.settlements).toEqual([
// Codex took it into a turn: no proof it never reached Codex.
expect.objectContaining({
sessionId: 'session-1',
clientMessageId: 'client-1',
state: 'rejected'
state: 'rejected',
rejection: { kind: 'cancelled' }
})
])
expect(rig.categoryOf(rig.settlements[0])).toBe('withdrawn')
Expand Down
8 changes: 6 additions & 2 deletions src/main/codex/codex-structured-turn-start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,11 +170,15 @@ export async function dispatchCodexTurn(
if (isCodexAppServerRequestError(error) || isCodexAppServerUnsupportedError(error)) {
// Codex answered and declined, so no echo for this write can arrive.
session.dispatchEchoes.disarm(input.clientMessageId)
// Codex's own words, when it gave any, are the one part of the error a person can use.
// Codex's own words, when it gave any, are the one part of the error a person can use. A
// declined request recorded nothing, so stopped messages it carried are owed again.
return {
state: 'rejected',
...codexDispatchRejection(
agentSessionFailureFact('providerRejected', { detail: providerDiagnosticOf(error) })
agentSessionFailureFact('providerRejected', {
detail: providerDiagnosticOf(error),
neverReachedAgent: true
})
)
}
}
Expand Down
3 changes: 2 additions & 1 deletion src/main/codex/codex-structured-turn-steer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,8 @@ describe('a Codex send while a turn runs', () => {
reason: "Codex didn't accept this message: thread not found.",
rejection: {
kind: 'providerRejected',
detail: { text: 'thread not found', audience: 'person' }
detail: { text: 'thread not found', audience: 'person' },
neverReachedAgent: true
}
})
expect(methods()).toEqual(['thread/start', 'turn/steer', 'turn/start'])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export function applyJournalDispatchRow(
if (row.state === 'pending') {
submission.handedOverAt = row.ts
placeHandedOverMessage(state, submission, row)
foldStoppedHistoryCarried(state, row)
} else if (row.state === 'rejected') {
placeRejectedMessage(state, submission, row)
}
Expand All @@ -77,6 +78,20 @@ export function applyJournalDispatchRow(
})
}

/** A send is handed over once, so its one pending row names what it carries; a malformed id is
* dropped. */
function foldStoppedHistoryCarried(
state: JournalReducerState,
row: Extract<JournalRow, { kind: 'dispatch' }>
): void {
const carried = Array.isArray(row.carries)
? row.carries.filter((id): id is string => typeof id === 'string' && id.length > 0)
: []
if (carried.length > 0) {
state.stoppedHistoryCarried.set(row.clientMessageId, carried)
}
}

/** A stored answered turn. One malformed, or naming a way of joining this build does not know, is
* read as no turn: it was written knowing the field, so it is not an older row. */
function readAnsweredTurn(value: unknown): AgentJournalAnsweredTurn | null {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ export type JournalReducerState = {
latestAcceptedTurnSequence: number
/** The latest person's Stop event and Resume, what the queue's pause is derived from. */
queuePauseMarks: JournalQueuePauseMarks
/** Host-only: each handed-over send's stopped-message history, from its latest `pending` row. */
stoppedHistoryCarried: Map<string, readonly string[]>
}

export function createJournalReducerState(sessionId: string, epoch: string): JournalReducerState {
Expand All @@ -92,7 +94,8 @@ export function createJournalReducerState(sessionId: string, epoch: string): Jou
appliedSettlementIds: new Set(),
derivedTurnScope: new JournalDerivedTurnScope(),
latestAcceptedTurnSequence: 0,
queuePauseMarks: createJournalQueuePauseMarks()
queuePauseMarks: createJournalQueuePauseMarks(),
stoppedHistoryCarried: new Map()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,38 @@ describe('reconcileJournalSubmissionsAgainstHistory', () => {
expect(journal.submissions()[0]?.dispatchState).toBe('unknown')
})

it('leaves a send that carried stopped history unknown: its provider copy leads with that history', async () => {
const journal = await open()
for (const clientMessageId of ['stopped', 'cm_1']) {
await journal.appendSubmission({
clientMessageId,
payloadFingerprint: digestPayload(clientMessageId),
body: userMessage(clientMessageId),
fence: 1,
handoverRecorded: true
})
}
await journal.resolveDispatch({
clientMessageId: 'cm_1',
state: 'pending',
fence: 1,
turnScope: AGENT_JOURNAL_THREAD_SCOPE,
carries: ['stopped']
})
const restarted = await open()
await restarted.markPendingSubmissionsUnknown(2)

const settled = await reconcileJournalSubmissionsAgainstHistory({
journal: restarted,
fence: 2,
history: window([])
})

expect(settled).toEqual([])
expect(restarted.submission('cm_1')?.dispatchState).toBe('unknown')
expect(restarted.stoppedHistoryCarried().get('cm_1')).toEqual(['stopped'])
})

it('leaves multi-block text sends unknown because Claude joins them before recording history', async () => {
const journal = await reopenAfterCrash(
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,10 @@ function comparableSubmissions(journal: AgentSessionJournal): AgentJournalSubmis
if (
(submission.dispatchState !== 'pending' && submission.dispatchState !== 'unknown') ||
// Never handed over, so provider history cannot hold it.
isQueuedAgentJournalSubmission(submission)
isQueuedAgentJournalSubmission(submission) ||
// The provider's copy leads with the stopped history it carried, so its fingerprint cannot
// match; like an attachment's, its absence is no evidence and it stays `unknown`.
journal.stoppedHistoryCarried().has(submission.clientMessageId)
) {
return false
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,8 @@ export function journalDispatchRowBuilder(
: {}),
...journalRowBase(state().epoch, seq, input.fence, ts),
...(input.recovered ? { recovered: input.recovered } : {}),
...(input.state === 'pending' ? { turnScope: input.turnScope } : {})
...(input.state === 'pending' ? { turnScope: input.turnScope } : {}),
...(input.state === 'pending' && input.carries?.length ? { carries: [...input.carries] } : {})
})
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,9 @@ export type JournalDispatchRow = JournalRowBase & {
reason: string | null
/** On `pending`: the turn the message was handed into, which becomes its row's scope. */
turnScope?: AgentJournalTurnScope
/** On `pending`: the stopped messages this send carries to the agent as history, by client
* message id in the order they were sent. Host-only; older readers keep the key and ignore it. */
carries?: string[]
/** On `rejected`: why, typed. Older readers keep the key and ignore it; a malformed one is
* dropped when read, never the row. */
rejection?: AgentSessionFailureFact
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
// event, and the command receipt that names that event. Nothing of it is durable unless all of it
// is, so a Stop whose acceptance fails withdraws nothing and interrupts nothing.

import type { AgentJournalDispatchRejection } from '../../../shared/agent-session-failure-words'
import { agentSessionFailureFact } from '../../../shared/agent-session-failure'
import { agentSessionFailureWords } from '../../../shared/agent-session-failure-words'
import type { AgentJournalCursor } from '../../../shared/agent-session-journal-types'
import { journalQueuedRejectionRowBuilders } from './journal-pending-submission-recovery'
import type { JournalReducerState } from './journal-reducer'
Expand All @@ -13,10 +14,14 @@ import { journalStopEventRowBuilder } from './journal-stop-and-resume-rows'
export type JournalStopAcceptanceInput = {
event: Omit<JournalStopEvent, 'at'>
fence: number
/** What every queued send is rejected with. */
withdrawal: AgentJournalDispatchRejection
}

// Only host-held sends are withdrawn, so each carries proof it never reached the agent.
const STOP_WITHDRAWAL = agentSessionFailureWords(
agentSessionFailureFact('cancelled', { neverReachedAgent: true }),
{ surface: 'rejection' }
)

export type JournalStopAcceptance = {
/** Where the Stop event landed. */
mark: AgentJournalCursor
Expand Down Expand Up @@ -54,7 +59,7 @@ export class JournalStopAcceptor {
turnId = input.event.turnId ?? this.deps.liveTurnId()
const event = { ...input.event, ...(turnId !== null ? { turnId } : {}) }
return [
...journalQueuedRejectionRowBuilders(state, input.fence, input.withdrawal),
...journalQueuedRejectionRowBuilders(state, input.fence, STOP_WITHDRAWAL),
journalStopEventRowBuilder(state, event, input.fence)
]
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
AgentSessionJournalIdentity
} from '../../../shared/agent-session-journal-types'
import type { JournalHostDatabase } from './journal-host-database'
import { JournalContextController } from './journal-context-controller'
import { JournalEpochController } from './journal-epoch-controller'
import { JournalItemAppender } from './journal-item-appender'
import { JournalLifecycleBatchAppender } from './journal-lifecycle-batch-appender'
Expand Down Expand Up @@ -58,6 +59,7 @@ export type JournalStoreCollaborators = {
stepWriter: JournalStepWriter
queuedMessages: JournalQueuedMessages
stopMarks: JournalStopMarks
context: JournalContextController
stops: JournalStopAcceptor
/** Restores the store's state from disk. Owned here because it needs the same
* collaborators the constructor just built. */
Expand Down Expand Up @@ -125,6 +127,11 @@ export function createJournalStoreCollaborators(host: JournalStoreHost): Journal
epochController,
queuedMessages,
stopMarks: new JournalStopMarks({ state: host.state }),
context: new JournalContextController({
state: host.state,
writer: rowWriter,
cards: queuedMessages
}),
stops: new JournalStopAcceptor({
writer: rowWriter,
state: host.state,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,12 @@ export type ResolveDispatchInput = {
(
| { state: 'accepted'; providerIdentity: AgentJournalItemIdentity | null }
/** The turn the message is handed into — the live root turn, or `thread` when none runs. */
| { state: 'pending'; turnScope: AgentJournalTurnScope }
| {
state: 'pending'
turnScope: AgentJournalTurnScope
/** The stopped messages this send carries as history (`JournalDispatchRow.carries`). */
carries?: readonly string[]
}
/** `reason` is what released clients print, `rejection` what newer ones read: both from
* `agentSessionFailureWords`, never written by hand. */
| ({
Expand Down
12 changes: 6 additions & 6 deletions src/main/native-chat/agent-session-journal/journal-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ import type { JournalItemAppender } from './journal-item-appender'
import type { JournalLifecycleBatchAppender } from './journal-lifecycle-batch-appender'
import type { JournalStepWriter } from './journal-step-writer'
import type { JournalStopMarks } from './journal-stop-marks'
import { JournalContextController } from './journal-context-controller'
import type { JournalContextController } from './journal-context-controller'
import type { JournalStopAcceptor } from './journal-stop-acceptance'

export { AgentSessionJournalError } from './journal-write-guards'
Expand Down Expand Up @@ -146,11 +146,7 @@ export class AgentSessionJournal {
this.stopMarks = collaborators.stopMarks
this.stops = collaborators.stops
this.restore = collaborators.restore
this.context = new JournalContextController({
state: () => this.state,
writer: this.rowWriter,
cards: this.queuedMessages
})
this.context = collaborators.context
}

get epoch(): string {
Expand Down Expand Up @@ -216,6 +212,10 @@ export class AgentSessionJournal {
itemBody = (itemId: string): AgentJournalItemBody | null =>
this.state.items.get(itemId)?.body ?? null

/** Host-only: the stopped messages each handed-over send carried, by its client message id. */
stoppedHistoryCarried = (): ReadonlyMap<string, readonly string[]> =>
this.state.stoppedHistoryCarried

/** One reduced item with its attribution, for a writer that needs the turn a row joined. */
item = (itemId: string): AgentJournalRenderItem | null => this.state.items.get(itemId) ?? null

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@
// reachable only through `mutateWithChatStop`, which queues that step in the same synchronous call
// as the mutation. Once saved, a failure is answered from the Stop's receipt and noted on the chat.

import { agentSessionFailureFact } from '../../../shared/agent-session-failure'
import { agentSessionFailureWords } from '../../../shared/agent-session-failure-words'
import type {
AgentSessionCancelResult,
AgentSessionMutationEnvelope,
Expand Down Expand Up @@ -83,10 +81,7 @@ export function mutateWithChatStop<TValue>(
caller: caller.callerKey,
...(target.eventTurnId !== null ? { turnId: target.eventTurnId } : {})
},
fence: ctx.fence,
withdrawal: agentSessionFailureWords(agentSessionFailureFact('cancelled'), {
surface: 'rejection'
})
fence: ctx.fence
})
return accepted.ok
? { ok: true, value: { settled: target.queued, turnId: accepted.value.turnId } }
Expand Down
Loading
Loading