Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
115 commits
Select commit Hold shift + click to select a range
c867bd2
refactor(native-chat): remove the unused terminal handoff
brennanb2025 Sep 25, 2026
eab4ef1
fix(native-chat): never let the pre-stop snapshot hold a chat's stop
brennanb2025 Sep 25, 2026
abafbbc
refactor(native-chat): drop helpers only the terminal handoff called
brennanb2025 Sep 25, 2026
3cbf6b5
docs(native-chat): stop citing the removed handoff in lifecycle comments
brennanb2025 Sep 25, 2026
e048579
test(native-chat): type the stalled snapshot drain without a cast
brennanb2025 Sep 25, 2026
b50c574
test(native-chat): pin that a start dead before proving owes no settl…
brennanb2025 Sep 25, 2026
12efdef
fix(native-chat): keep the owner-status read behind an in-flight attach
brennanb2025 Sep 25, 2026
42d10ee
refactor(terminal): remove the agent-session PTY write gate
brennanb2025 Sep 25, 2026
0483915
refactor(native-chat): drop the transcript helpers only the handoff c…
brennanb2025 Sep 25, 2026
d0f4dae
fix(native-chat): stop calling a starting chat "mid-handoff"
brennanb2025 Sep 25, 2026
faecece
test(native-chat): type the stand-in roster decoder without a cast
brennanb2025 Sep 25, 2026
250250e
refactor(codex): name the pinned rollout lookup for what it does
brennanb2025 Sep 25, 2026
b588185
refactor(native-chat): type the owner-status reply as the host sends it
brennanb2025 Sep 25, 2026
35742fd
refactor(native-chat): normalize terminal-handoff lease values once a…
brennanb2025 Sep 25, 2026
54ce5fc
refactor(native-chat): stop threading the owner kind through a reserv…
brennanb2025 Sep 25, 2026
713b126
test(native-chat): pin the legacy-lease rewrite with a transaction th…
brennanb2025 Sep 25, 2026
44d7977
fix(native-chat): name a chat write by its target, not the owner gene…
brennanb2025 Sep 25, 2026
6dad39a
fix(native-chat): every journal append reaches the chats that are open
brennanb2025 Sep 25, 2026
97bf3bb
test(native-chat): an epoch replacement reaches the open chat
brennanb2025 Sep 25, 2026
427dc8d
test(native-chat): each row reaches an open chat once, and a live han…
brennanb2025 Sep 25, 2026
2bd44f5
test(native-chat): give the legacy-lease store test a tab id so the b…
brennanb2025 Sep 25, 2026
f79eb9b
test(worktree-activation): restore the OMP surfaced-agent resume test
brennanb2025 Sep 25, 2026
50d2e33
perf(native-chat): a publish behind a delivered commit reads nothing
brennanb2025 Sep 25, 2026
bd72b13
test(native-chat): state why the teardown test's fake journal is safe…
brennanb2025 Sep 25, 2026
fa9b76a
docs(native-chat): say mutation admission checks only the writer lease
brennanb2025 Sep 25, 2026
5435973
docs(native-chat): drop the send rebase from comments that still desc…
brennanb2025 Sep 25, 2026
8fd4558
Merge #22811 into the PR 1 base
brennanb2025 Sep 25, 2026
12dde37
Merge #22812 into the PR 1 base
brennanb2025 Sep 25, 2026
c753e8a
fix(native-chat): a message is accepted, then delivered
brennanb2025 Sep 25, 2026
82f9bfd
fix(native-chat): settle queued messages only for the child that ended
brennanb2025 Sep 25, 2026
f3e52d2
fix(native-chat): an adoption that fails to import keeps the conversa…
brennanb2025 Sep 25, 2026
29a340b
perf(native-chat): the recovering open reads the journal once
brennanb2025 Sep 25, 2026
c7a652f
fix(native-chat): an attach that fails after indexing its child leave…
brennanb2025 Sep 25, 2026
34d0bb0
fix(native-chat): a withdrawn message shows no error, and a rejection…
brennanb2025 Sep 25, 2026
46b6472
fix(orchestration): a structured worker whose agent outlasts the prea…
brennanb2025 Sep 25, 2026
283819d
fix(native-chat): a message rejected while its chat was closed reads …
brennanb2025 Sep 25, 2026
40dabe8
test(orchestration): name why the readiness settlement fakes are cast
brennanb2025 Sep 25, 2026
31444bf
fix(native-chat): keep each pane's own fence on frames so a failed re…
brennanb2025 Sep 25, 2026
494d8c7
docs(native-chat): drop the fence from the admission the send effects…
brennanb2025 Sep 25, 2026
0e74529
docs(native-chat): give the fence move on release the reason that sti…
brennanb2025 Sep 25, 2026
6c16f35
docs(native-chat): stop citing a write fence check in launch and mail…
brennanb2025 Sep 25, 2026
d2f646b
refactor(native-chat): the provider child is its own record
brennanb2025 Sep 25, 2026
25fa38a
Merge remote-tracking branch 'origin/main' into brennanb2025/chat-wri…
brennanb2025 Sep 25, 2026
aa56b50
fix(native-chat): the delivery loop alone settles a message its start…
brennanb2025 Sep 25, 2026
d23cca4
Merge #22812 and main into PR 1
brennanb2025 Sep 25, 2026
4993696
refactor(native-chat): a stopped child ends on the one reading of its…
brennanb2025 Sep 25, 2026
a2ad8f2
feat(native-chat): the host says it accepts a send before any agent h…
brennanb2025 Sep 25, 2026
48def96
refactor(native-chat): an attach never opens a journal of its own
brennanb2025 Sep 25, 2026
da65d57
fix(native-chat): a moved fence resends nothing on a host that accept…
brennanb2025 Sep 25, 2026
0184a3d
refactor(native-chat): a child's end says whether the user or the hos…
brennanb2025 Sep 25, 2026
12577d8
fix(native-chat): a chat whose only work is a queued message is not o…
brennanb2025 Sep 25, 2026
e751c7e
fix(native-chat): the conversation outlives its agent
brennanb2025 Sep 25, 2026
a59eed9
test(native-chat): type the queued-message fixtures in the resume-off…
brennanb2025 Sep 25, 2026
ccaf412
fix(native-chat): a start that dies while a message waits on it is th…
brennanb2025 Sep 25, 2026
0efca93
docs(native-chat): say what an attach's open conversation and unconfi…
brennanb2025 Sep 25, 2026
74d6506
test(native-chat): pin what a failed start settles, and what a resume…
brennanb2025 Sep 25, 2026
9c671f1
test(native-chat): the failed-start pins fail on what the message bec…
brennanb2025 Sep 25, 2026
eaf1436
fix(native-chat): a restart offer ends when the chat's agent starts a…
brennanb2025 Sep 25, 2026
5b93c44
fix(runtime): end a transcript stream when its client unsubscribes
brennanb2025 Sep 25, 2026
80ccb33
test(orchestration): the preamble's host stub is typed, not cast
brennanb2025 Sep 25, 2026
7805005
Merge remote-tracking branch 'origin/brennanb2025/chat-accept-then-de…
brennanb2025 Sep 25, 2026
0a3f255
fix(native-chat): one fact ends a restart offer: the chat moved on si…
brennanb2025 Sep 25, 2026
4c45865
test(native-chat): an older build reads the restart offer this build …
brennanb2025 Sep 25, 2026
1887bef
fix(native-chat): read a restart offer against where the journal stoo…
brennanb2025 Sep 26, 2026
a862d71
test(native-chat): wait for the listing's retire write before reading…
brennanb2025 Sep 26, 2026
0871b27
fix(native-chat): keep the terminal-backed chat's read error over its…
brennanb2025 Sep 26, 2026
6cde2a7
fix(native-chat): a start retries the exit settlement a failed journa…
brennanb2025 Sep 26, 2026
9530e8d
perf(native-chat): answer the owner check without opening the chat
brennanb2025 Sep 26, 2026
2f39e1b
fix(native-chat): a read waiting on the session lock opens nothing on…
brennanb2025 Sep 26, 2026
4ad06ca
fix(native-chat): read a failed resume's chat before calling it retry…
brennanb2025 Sep 26, 2026
1aaf776
test(native-chat): type the provider event sink the settlement test r…
brennanb2025 Sep 26, 2026
9a5e5e6
fix(native-chat): refuse a read of an unusable journal file with the …
brennanb2025 Sep 26, 2026
75d6902
Merge remote-tracking branch 'origin/brennanb2025/chat-conversation-o…
brennanb2025 Sep 26, 2026
daaf0cf
fix(native-chat): say the structured read keeps trying only where it …
brennanb2025 Sep 26, 2026
42307c8
test(native-chat): await the send's settlement instead of polling for…
brennanb2025 Sep 26, 2026
9d9dd98
fix(native-chat): a restart offer resumes any time after the quit, an…
brennanb2025 Sep 26, 2026
bab2cef
fix(orchestration): route no mail to a structured worker its orchestr…
brennanb2025 Sep 26, 2026
75f5607
fix(native-chat): a failed retry names the user's prompt, not Orca's …
brennanb2025 Sep 26, 2026
7a5e202
fix(orchestration): read the released row optionally, as the authorit…
brennanb2025 Sep 26, 2026
c0ea360
fix(native-chat): the status bar drops a restart offer the chat moved…
brennanb2025 Sep 26, 2026
e6dadd1
Merge main into PR 1
brennanb2025 Sep 26, 2026
6e319c4
feat(native-chat): list restored chats from durable state and their s…
brennanb2025 Sep 26, 2026
7e1e030
fix(native-chat): keep chat restart bookkeeping off the workspace's f…
brennanb2025 Sep 26, 2026
f6d9c8a
Merge PR 1's latest head (main merged in) into the conversation-outli…
brennanb2025 Sep 26, 2026
e10362b
test(native-chat): a roster of idle or finished children does not kee…
brennanb2025 Sep 26, 2026
c9a5576
test(native-chat): pin the startup pass's seeds, probe sidecars and f…
brennanb2025 Sep 26, 2026
453e701
Merge remote-tracking branch 'origin/brennanb2025/chat-conversation-o…
brennanb2025 Sep 26, 2026
20978d6
fix(orchestration): a task dispatched into a resting structured worke…
brennanb2025 Sep 26, 2026
d494b10
fix(native-chat): seed a store with no tab index from the saved sessi…
brennanb2025 Sep 26, 2026
35e5f5a
Merge remote-tracking branch 'origin/brennanb2025/chat-conversation-o…
brennanb2025 Sep 26, 2026
946170e
test(native-chat): state why the worker release stub's host cast is safe
brennanb2025 Sep 26, 2026
742bda7
docs(native-chat): comments stop describing the hold this PR removed
brennanb2025 Sep 26, 2026
b36d188
fix(native-chat): a restart offer keeps the start its own continuatio…
brennanb2025 Sep 26, 2026
30dabd9
Merge remote-tracking branch 'origin/brennanb2025/chat-conversation-o…
brennanb2025 Sep 26, 2026
ca3e68f
fix(native-chat): an agent gets a full idle window after its owed wor…
brennanb2025 Sep 26, 2026
194fc33
test(claude): the options-read fixture runs a live child
brennanb2025 Sep 26, 2026
86d7365
test(native-chat): host tests reach its collaborators through a typed…
brennanb2025 Sep 26, 2026
fb53224
refactor(orchestration): one owner answers a structured worker's custody
brennanb2025 Sep 26, 2026
ccb0610
Merge remote-tracking branch 'origin/brennanb2025/chat-conversation-o…
brennanb2025 Sep 26, 2026
21e02a4
Merge remote-tracking branch 'origin/brennanb2025/chat-conversation-o…
brennanb2025 Sep 27, 2026
0b975be
test(native-chat): the owner-status restart case runs the startup pas…
brennanb2025 Sep 27, 2026
1eea2d8
fix(native-chat): keep a legacy store's seeded tab list when another …
brennanb2025 Sep 27, 2026
ef47c7d
fix(native-chat): a saved-status flush that finds the file corrupt re…
brennanb2025 Sep 27, 2026
eb9c79b
fix(native-chat): a failed startup reconcile still settles every list…
brennanb2025 Sep 27, 2026
3240ce6
fix(native-chat): back off a saved-status flush that keeps failing
brennanb2025 Sep 27, 2026
c62e8a5
fix(native-chat): close the saved-status store when teardown's first …
brennanb2025 Sep 27, 2026
790361f
test(native-chat): reach the host's session map through its typed tes…
brennanb2025 Sep 27, 2026
2b189a8
test(native-chat): wait for the settled row and the cleared flag toge…
brennanb2025 Sep 27, 2026
d7ce43a
refactor(orchestration): owed work is an open dispatch on the worker'…
brennanb2025 Sep 27, 2026
31c0297
test(native-chat): crashed-turn tests assert the unverifiable settlem…
brennanb2025 Sep 27, 2026
9fa4452
fix(native-chat): a restart offer knows its continuations by a tag in…
brennanb2025 Sep 27, 2026
d09d941
Merge remote-tracking branch 'origin/brennanb2025/chat-conversation-o…
brennanb2025 Sep 27, 2026
0c02d8a
fix(native-chat): the chat tab sync subscribes even when the startup …
brennanb2025 Sep 27, 2026
5961a7d
fix(native-chat): saved statuses publish for the active workspace first
brennanb2025 Sep 27, 2026
efefb80
test(native-chat): give the unawaited post-reconcile settlement room …
brennanb2025 Sep 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
1 change: 1 addition & 0 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -757,6 +757,7 @@ jobs:
tests/e2e/cross-version-wire/cross-version-agent-session-wire.unit.test.ts
tests/e2e/cross-version-wire/cross-version-worktree-identity-downgrade.unit.test.ts
tests/e2e/cross-version-wire/cross-version-session-tabs-retirement-proof.unit.test.ts
tests/e2e/cross-version-wire/agent-session-resume-marker-downgrade.unit.test.ts

managed_hook_node18:
name: managed hooks on Node 18
Expand Down
2 changes: 2 additions & 0 deletions config/scripts/pr-code-change-scope.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,8 @@ const CROSS_VERSION_WIRE_PREFIXES = [
'src/main/native-chat/agent-session-journal/',
'src/main/native-chat/agent-session-wire/',
'src/main/runtime/agent-session-record-store',
'src/main/runtime/agent-session-recovery-capsule',
'src/shared/agent-session-resume-marker',
'src/main/runtime/rpc/dispatcher',
'src/main/runtime/rpc/methods/agent-launch',
'src/main/runtime/rpc/methods/ai-vault.ts',
Expand Down
6 changes: 5 additions & 1 deletion docs/reference/agent-status-store.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,11 @@ forgot the row would strand a permanently working-looking agent.
Two rules the ingest must keep:

- **Never persist a structured row.** The journal is the durable truth for a
structured session and the host republishes on restore. A structured row in
structured session and the host republishes on restore: for each listed chat,
from its saved settled status when its journal still stands at the position
that status was computed at, and otherwise from the journal. That saved copy
is the host's own cache (`agent-session-saved-status.db`), never this store's
file. A structured row in
`last-status.json` would hydrate as `restoredUnconfirmed` and then fight the
live republish. The serializer skips rows carrying `structuredHost`, and
hydrate drops any such row found on disk. Applying one therefore also skips
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { createElement } from 'react'
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
import type { AgentSessionSubscribeEvent } from '../../../src/shared/agent-session-wire'
import type { RpcClient } from '../transport/rpc-client'
import { useMobileStructuredAgentSession } from './use-mobile-structured-agent-session'

const asyncStorage = vi.hoisted(() => ({
getItem: vi.fn(async () => null),
setItem: vi.fn(async () => undefined),
removeItem: vi.fn(async () => undefined)
}))

vi.mock('@react-native-async-storage/async-storage', () => ({ default: asyncStorage }))

// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a literal snapshot frame; the hook reads its type, page and fence.
const SNAPSHOT: AgentSessionSubscribeEvent = {
type: 'snapshot',
sessionId: 'session-1',
fence: 3,
page: {
sessionId: 'session-1',
epoch: 'epoch-1',
direction: 'tail',
items: [
{
itemId: 'msg-1',
revision: 1,
sequence: 1,
observedAt: 10,
body: { kind: 'message', role: 'user', blocks: [{ type: 'text', text: 'still here' }] }
}
],
removedItemIds: [],
submissions: [],
window: {
oldest: { epoch: 'epoch-1', sequence: 1 },
newest: { epoch: 'epoch-1', sequence: 1 },
nextCursor: { epoch: 'epoch-1', sequence: 2 }
},
liveCursor: { epoch: 'epoch-1', sequence: 1 },
hasOlder: false,
hasNewer: false
}
} as AgentSessionSubscribeEvent

let renderer: ReactTestRenderer | null = null
let hook: ReturnType<typeof useMobileStructuredAgentSession> | null = null

afterEach(() => {
act(() => renderer?.unmount())
renderer = null
hook = null
})

beforeEach(() => {
vi.clearAllMocks()
})

it('subscribes when an older host refuses the hold, and shows the transcript (P2-24)', async () => {
const sendRequest = vi.fn(async (method: string) =>
method === 'agentSession.hold'
? {
ok: false,
error: { code: 'agent_session_owner_restart_failed', message: 'Codex could not start.' },
_meta: { runtimeId: 'runtime-1' }
}
: { ok: true, result: {}, _meta: { runtimeId: 'runtime-1' } }
)
const subscribe = vi.fn((_method: string, _params: unknown, onData: (value: unknown) => void) => {
onData(SNAPSHOT)
return () => undefined
})
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: an RPC client stub with the two members the hook calls.
const client = { sendRequest, subscribe } as unknown as RpcClient
function Harness(): null {
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the hook's options, of which this test supplies the ones it reads.
hook = useMobileStructuredAgentSession({
client,
sessionId: 'session-1',
sourceIdentity: 'host-a\0workspace-a',
enabled: true,
connected: true,
agent: 'codex',
onSendError: () => undefined
} as never)
return null
}

act(() => {
renderer = create(createElement(Harness))
})

await vi.waitFor(() =>
expect(subscribe).toHaveBeenCalledWith(
'agentSession.subscribe',
{ sessionId: 'session-1' },
expect.any(Function)
)
)
await vi.waitFor(() => expect(hook?.session.messages.length).toBeGreaterThan(0))
expect(hook?.session.status).not.toBe('error')
})
65 changes: 37 additions & 28 deletions mobile/src/session/use-mobile-structured-agent-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,33 @@ const MAX_RETAINED_SESSION_STATES = 32
/** Bounded so a busy stream cannot turn one Load-earlier tap into an endless read chain. */
const OLDER_PAGE_ANCHOR_ATTEMPTS = 3

/**
* Opens the transcript stream once the hold settles, either way: a refused hold is an older host
* saying it could not start the agent — which the next send does — never a reason to hide the
* transcript. Returns what ends the stream, opened or not yet. Outside the effect so its cleanup
* rule can see the stream is owned.
*/
function openTranscriptAfterHold(
client: RpcClient,
sessionId: string,
held: Promise<unknown>,
onFrame: (raw: unknown) => void
): () => void {
let ended = false
let close = (): void => {}
void held
.catch(() => undefined)
.then(() => {
if (!ended) {
close = client.subscribe('agentSession.subscribe', { sessionId }, onFrame)
}
})
return () => {
ended = true
close()
}
}

function isSubscribeEvent(value: unknown): value is AgentSessionSubscribeEvent {
if (typeof value !== 'object' || value === null) {
return false
Expand Down Expand Up @@ -101,39 +128,21 @@ export function useMobileStructuredAgentState(args: {
}
apply({ type: 'loading' })
const holderId = structuredAgentSessionHolderId('mobile-chat')
let cancelled = false
let unsubscribe = (): void => {}
const held = callAgentSession(client, 'agentSession.hold', {
sessionId,
holderId
})
void held
.then(() => {
if (cancelled) {
return
}
unsubscribe = client.subscribe('agentSession.subscribe', { sessionId }, (raw) => {
if (
typeof raw === 'object' &&
raw !== null &&
(raw as { type?: unknown }).type === 'error'
) {
apply({ type: 'error', message: String((raw as { message?: unknown }).message ?? '') })
return
}
if (isSubscribeEvent(raw)) {
apply({ type: 'event', event: raw })
}
})
})
.catch((error: unknown) => {
if (!cancelled) {
apply({ type: 'error', message: error instanceof Error ? error.message : String(error) })
}
})
const endStream = openTranscriptAfterHold(client, sessionId, held, (raw) => {
if (typeof raw === 'object' && raw !== null && 'type' in raw && raw.type === 'error') {
apply({ type: 'error', message: 'message' in raw ? String(raw.message ?? '') : '' })
return
}
if (isSubscribeEvent(raw)) {
apply({ type: 'event', event: raw })
}
})
return () => {
cancelled = true
unsubscribe()
endStream()
void held
.then(() =>
callAgentSession(
Expand Down
14 changes: 14 additions & 0 deletions mobile/src/transport/rpc-client-stream-registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,20 @@ describe('RpcClientStreamRegistry', () => {
])
})

it('ends one transcript stream on dispose and leaves a sibling on the same socket (U-03)', () => {
const { registry, sent } = createRegistry()
const disposeFirst = registry.subscribe('agentSession.subscribe', { sessionId: 's1' }, () => {})
registry.subscribe('agentSession.subscribe', { sessionId: 's2' }, () => {})
const [first] = sent

disposeFirst()
expect(sent.at(-1)).toMatchObject({
method: 'agentSession.unsubscribe',
params: { sessionId: 's1', subscriptionId: first!.id }
})
expect(sent.filter((request) => request.method === 'agentSession.unsubscribe')).toHaveLength(1)
})

it('keeps a disposed browser tombstone until ready can be unsubscribed', () => {
const { registry, sent } = createRegistry()
const dispose = registry.subscribe('browser.screencast', { page: 'page-1' }, () => {})
Expand Down
2 changes: 1 addition & 1 deletion mobile/src/transport/rpc-client-stream-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ export class RpcClientStreamRegistry {
this.sendRpc('terminal.unsubscribe', params)
}
} else {
const unsubscribe = buildStreamUnsubscribe(stream?.method, stream?.params)
const unsubscribe = buildStreamUnsubscribe(stream?.method, stream?.params, id)
if (unsubscribe) {
this.sendRpc(unsubscribe.method, unsubscribe.params)
}
Expand Down
8 changes: 8 additions & 0 deletions mobile/src/transport/rpc-client-terminal-subscription.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@ export function buildStreamUnsubscribe(
}
: null
}
if (method === 'agentSession.subscribe') {
const sessionId = (params as { sessionId?: unknown }).sessionId
// The host keys each transcript stream by its frame id; without it every stream of the
// session on this socket would end.
return typeof sessionId === 'string' && requestId
? { method: 'agentSession.unsubscribe', params: { sessionId, subscriptionId: requestId } }
: null
}
if (method === 'nativeChat.subscribe') {
const subscriptionId = (params as { subscriptionId?: unknown }).subscriptionId
if (typeof subscriptionId === 'string') {
Expand Down
14 changes: 9 additions & 5 deletions src/main/claude/claude-context-usage-unloaded-turn.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,8 @@ import { createTrackedJournalOpener } from '../native-chat/agent-session-journal
import { readAgentSessionHistory } from '../native-chat/agent-session-wire/agent-session-history-page'
import type { StructuredAgentSessionAdapter } from '../native-chat/agent-session-wire/structured-agent-session-adapter'
import { createDeferredStructuredAgentSessionEventSink } from '../native-chat/agent-session-wire/structured-agent-session-event-sink'
import {
readStructuredAgentSessionOptions,
type StructuredAgentSessionMutationContext
} from '../native-chat/agent-session-wire/structured-agent-session-host-mutations'
import type { StructuredAgentSessionMutationContext } from '../native-chat/agent-session-wire/structured-agent-session-host-mutations'
import { readStructuredAgentSessionOptions } from '../native-chat/agent-session-wire/structured-agent-session-options-read'
import {
assistantFrame,
initFrame,
Expand Down Expand Up @@ -85,6 +83,11 @@ function readOptions(
journal: AgentSessionJournal,
adapter: Partial<StructuredAgentSessionAdapter>
) {
const running = {
journal,
child: { fence: 1, generation: 'generation-1' },
params: { provider: 'claude' }
}
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the options read touches only these members.
const context = {
deps: {
Expand All @@ -95,7 +98,8 @@ function readOptions(
store: { getRecord: () => undefined }
},
serialize: (_sessionId: string, task: () => Promise<unknown>) => task(),
requireSession: () => ({ journal, fence: 1 })
openConversation: async () => running,
conversation: async () => running
} as unknown as StructuredAgentSessionMutationContext
return readStructuredAgentSessionOptions(context, SESSION)
}
Expand Down
4 changes: 2 additions & 2 deletions src/main/claude/claude-structured-dispatch-test-support.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { ClaudeSession } from './claude-structured-session-state'
import { ClaudeBackgroundTaskTracker } from './claude-background-task-tracker'
import { ClaudeChildWorkDecoder } from './claude-child-work-decoder'
import { ClaudeSlashCommandCatalog } from './claude-slash-command-catalog'
import { createClaudeSessionStartupGate } from './claude-structured-session-startup-gate'
import { createClaudeSessionStartup } from './claude-structured-session-startup-state'

export function sessionFor(send: Mock = vi.fn().mockResolvedValue(undefined)): ClaudeSession {
return {
Expand All @@ -32,7 +32,7 @@ export function sessionFor(send: Mock = vi.fn().mockResolvedValue(undefined)): C
capabilities: [],
events: undefined,
translator: null,
startup: { ...createClaudeSessionStartupGate(), state: 'proven' }
startup: { ...createClaudeSessionStartup(), state: 'proven' }
}
}

Expand Down
19 changes: 4 additions & 15 deletions src/main/claude/claude-structured-dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,8 @@ import {
import { AgentSessionPreDispatchError } from '../native-chat/agent-session-wire/structured-agent-session-operation-settlement'
import {
claudeStartupFailureReason,
claudeStartupHoldsWrites,
failClaudeStartupGate,
holdClaudeStartupWrite
} from './claude-structured-session-startup-gate'
failClaudeStartup
} from './claude-structured-session-startup-state'

const MAX_ACTIVE_DISPATCH_WAITERS = 64

Expand Down Expand Up @@ -226,7 +224,7 @@ export function settleCancelledClaudeDispatchWaiters(
* Retired rather than dropped: their identities stay joinable, bounded by
* `MAX_RETIRED_DISPATCH_WAITERS`. */
export function retireClaudeDispatchWaiters(session: ClaudeSession): void {
failClaudeStartupGate(session, new Error('claude stream-json ended before startup completed'))
failClaudeStartup(session, new Error('claude stream-json ended before startup completed'))
for (const waiter of session.dispatchWaiters.splice(0)) {
retireWaiter(session, waiter)
waiter.resolve(null)
Expand All @@ -236,8 +234,7 @@ export function retireClaudeDispatchWaiters(session: ClaudeSession): void {
export async function dispatchClaudeTurn(
session: ClaudeSession,
input: { clientMessageId?: string; body: AgentJournalMessageItem; requestedAt?: number },
beforeDispatch?: () => Promise<void>,
onSettledLate?: ClaudeLateDispatchSettlement
beforeDispatch?: () => Promise<void>
): Promise<AgentSessionDispatchOutcome> {
let content: unknown[]
try {
Expand Down Expand Up @@ -276,14 +273,6 @@ export async function dispatchClaudeTurn(
parent_tool_use_id: null,
session_id: session.providerSessionId
}
if (claudeStartupHoldsWrites(session)) {
return holdClaudeStartupWrite(session, {
message,
arm,
...(beforeDispatch ? { beforeDispatch } : {}),
...(onSettledLate ? { settleLate: onSettledLate } : {})
})
}
const pending = { replay: beforeDispatch ? undefined : arm() }
const authorize = beforeDispatch
? async () => {
Expand Down
4 changes: 2 additions & 2 deletions src/main/claude/claude-structured-options.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
import { ClaudeBackgroundTaskTracker } from './claude-background-task-tracker'
import { ClaudeChildWorkDecoder } from './claude-child-work-decoder'
import { ClaudeSlashCommandCatalog } from './claude-slash-command-catalog'
import { createClaudeSessionStartupGate } from './claude-structured-session-startup-gate'
import { createClaudeSessionStartup } from './claude-structured-session-startup-state'
import {
claudeStructuredSessionOptionsFrom,
observeClaudeFastModeFacts,
Expand Down Expand Up @@ -48,7 +48,7 @@ function sessionFor(setModel: ClaudeSession['connection']['setModel']): ClaudeSe
capabilities: [],
events: undefined,
translator: null,
startup: { ...createClaudeSessionStartupGate(), state: 'proven' }
startup: { ...createClaudeSessionStartup(), state: 'proven' }
}
}

Expand Down
7 changes: 7 additions & 0 deletions src/main/claude/claude-structured-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@ export function restoredClaudeStructuredSessionOptions(
)
}

/** The keys `setClaudeStructuredOption` writes; a pick made at rest is checked against these. */
const CLAUDE_STRUCTURED_OPTION_KEYS = new Set(['model', 'permissionMode', 'effort', 'fastMode'])

export function isClaudeStructuredOptionKey(key: string): boolean {
return CLAUDE_STRUCTURED_OPTION_KEYS.has(key)
}

/** A client's write; the startup restore writes through `setClaudeStructuredOption` directly. */
export function setClaudeStructuredSessionOption(
session: ClaudeSession,
Expand Down
Loading
Loading