Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
88c4c5d
Finish structured agent readiness before delivering held work
brennanb2025 Oct 12, 2026
a40ff4d
Merge main into agent startup host readiness
brennanb2025 Oct 12, 2026
1ab0a7d
Merge main into startup host readiness
brennanb2025 Oct 12, 2026
765a4ee
Adapt permission fixtures to startup lifecycle
brennanb2025 Oct 12, 2026
0737f63
Keep model discovery for chats before their first start
brennanb2025 Oct 13, 2026
86d0829
Use one host readiness promise for provider operations
brennanb2025 Oct 13, 2026
2264369
Await provider opening in the execution host
brennanb2025 Oct 13, 2026
57e7bde
Await Claude initialization in host-owned open
brennanb2025 Oct 13, 2026
9711350
Observe Claude readiness through the host status
brennanb2025 Oct 13, 2026
6d3de21
Integrate host-awaited Claude opening
brennanb2025 Oct 13, 2026
9b0ba03
Keep optional readback behind host readiness
brennanb2025 Oct 13, 2026
98bf1ac
Recheck launch permission after host readiness
brennanb2025 Oct 13, 2026
226c2a9
Preserve the awaited child during permission revalidation
brennanb2025 Oct 13, 2026
e1cc15d
Record failed owned opening before the first message
brennanb2025 Oct 13, 2026
b57b016
Abort a matching provider open when its child exits
brennanb2025 Oct 13, 2026
8e8fcd2
Cover provider exit during an unanswered host open
brennanb2025 Oct 13, 2026
bfd8e50
Cover explicit failure facts on a host-stopped child
brennanb2025 Oct 13, 2026
7ba38c7
Keep startup waits cancellable and preserve operation order
brennanb2025 Oct 13, 2026
ed35c02
Keep live choices and delivery order while agents start
brennanb2025 Oct 13, 2026
24d2a86
Apply startup picks and let parked messages release operations [skip ci]
brennanb2025 Oct 13, 2026
349b201
Save a waiting startup pick after Stop ends its opening [skip ci]
brennanb2025 Oct 13, 2026
2b3db6d
Save choices when startup ends or a replacement is still opening
brennanb2025 Oct 13, 2026
6f5bfb1
Merge main into host startup readiness
brennanb2025 Oct 13, 2026
f09f33d
Check saved choices separately from Claude startup reports
brennanb2025 Oct 13, 2026
eae9500
Align startup fixtures and preserve observed Claude exits
brennanb2025 Oct 13, 2026
7501b4f
Order startup picks before later messages and retain flow regressions
brennanb2025 Oct 13, 2026
29c4a81
Keep reviewed lifecycle contracts while pruning duplicate tests
brennanb2025 Oct 13, 2026
ad1e0d8
Save picks after a proven exit without requiring a child
brennanb2025 Oct 13, 2026
776a0db
Type the startup reply fixture against the Claude control contract
brennanb2025 Oct 13, 2026
70dbe14
Honor the before-input callback in the startup reply fixture
brennanb2025 Oct 13, 2026
85908e2
Keep startup option menus independent and show the saved choice
brennanb2025 Oct 13, 2026
7a4103d
Exercise startup picks across real menus and the runtime
brennanb2025 Oct 13, 2026
a4c51a1
Keep saved effort visible when picking a model during startup
brennanb2025 Oct 13, 2026
0ecfbbe
Merge current main into startup host readiness
brennanb2025 Oct 13, 2026
d467ebd
Include the running-turn scope in repeated Stop fixture
brennanb2025 Oct 13, 2026
df2a984
Use the current attach contract in import fixtures
brennanb2025 Oct 14, 2026
70f6f75
Merge main and retain independent option saves in combined picker
brennanb2025 Oct 14, 2026
734303f
Merge main daemon startup changes into startup readiness
brennanb2025 Oct 14, 2026
8e3842a
Merge main message selections with ordered startup option writes
brennanb2025 Oct 14, 2026
2e874f6
Align carried-message fixture with startup event contract
brennanb2025 Oct 14, 2026
415b772
Merge separate permission storage while keeping saved startup choices
brennanb2025 Oct 14, 2026
a473f03
Keep launch-only permission picks usable while prior writes wait
brennanb2025 Oct 14, 2026
2a46bff
ci: rerun on main with the locale fix
brennanb2025 Oct 14, 2026
bc3da66
Merge main and keep main's clock in the shared option writer
brennanb2025 Oct 14, 2026
0fbb96a
Merge main into chat startup readiness and retain current-work ownership
brennanb2025 Oct 15, 2026
d604b35
Preserve goal retries and chat launch selections
brennanb2025 Oct 16, 2026
76b1c74
Give launch regression its request identity
brennanb2025 Oct 16, 2026
6a0687e
Merge main into chat startup readiness
brennanb2025 Oct 16, 2026
21dcd01
Merge main into chat startup readiness
brennanb2025 Oct 16, 2026
4fcdc9d
test(native-chat): type the provisional-launch hook's session prop
brennanb2025 Oct 16, 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
5 changes: 5 additions & 0 deletions config/scripts/vitest-sqlite-runtime-files.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { SQLITE_DATABASE_RUNTIME_INCLUDE } from './vitest-sqlite-database-runtim

// Real SQLite fixtures keep Node publication and close semantics.
export const SQLITE_RUNTIME_INCLUDE = [
'tests/e2e/native-chat-startup-option-pickers.unit.test.ts',
...SQLITE_DATABASE_RUNTIME_INCLUDE,
'src/main/ipc/repos/project-host-setup-wsl-move-fence.test.ts',
'src/main/ipc/repos/repo-ipc-wsl-move-fence.test.ts',
Expand Down Expand Up @@ -141,6 +142,8 @@ export const SQLITE_RUNTIME_INCLUDE = [
'src/main/native-chat/agent-session-wire/structured-agent-session-provider-restore.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-provider-started-catalog.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-provider-started.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-provider-open.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-ready-operation-order.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-queued-action-receipts.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-queued-gate.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-queued-newer-orca.test.ts',
Expand Down Expand Up @@ -185,6 +188,8 @@ export const SQLITE_RUNTIME_INCLUDE = [
'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-attempt-host.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-startup-attempt-registrations.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-startup-picks.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-startup-permission-mode.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-startup-reconcile-failure.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-status-feed-child-work.test.ts',
'src/main/native-chat/agent-session-wire/structured-agent-session-status-feed-child.test.ts',
Expand Down
9 changes: 7 additions & 2 deletions src/main/acp/acp-structured-host-option-hang.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,21 +68,26 @@ describe('a Grok model pick Grok never answers', () => {
})

it('fails at its bound, frees the chat, and keeps what Grok answers late', async () => {
const { rig, host } = await openAttachedHostRig({ optionWriteTimeoutMs: 100 })
const { rig, host, store, fence } = await openAttachedHostRig({ optionWriteTimeoutMs: 100 })
const picking = pick(host, 'grok-4.6')
await waitFor(() => expect(framesOf(rig.child(), 'session/set_config_option')).toHaveLength(1))
expect(failed(await within(picking, 2_000))).toBe(true)
// The chat's queue moved on: a later operation runs.
expect(await within(host.readOptions(SESSION), 1_000)).not.toBe('pending')
const [frame] = framesOf(rig.child(), 'session/set_config_option')
const savedModel = store.getRecord(SESSION)?.options?.model
rig.child().agent.reply(frame!, {
configOptions: GROK_CONFIG_OPTIONS.map((option) =>
option.id === 'model' ? { ...option, currentValue: 'grok-4.6' } : option
)
})
await waitFor(async () =>
expect((await host.readOptions(SESSION)).current).toMatchObject({ model: 'grok-4.6' })
expect(
(await rig.adapter.readOptions({ sessionId: SESSION, fence: fence() })).current.model
).toBe('grok-4.6')
)
expect((await host.readOptions(SESSION)).current.model).toBe(savedModel)
expect(store.getRecord(SESSION)?.options?.model).toBe(savedModel)
await host.close(SESSION, 'user-close')
})
})
2 changes: 1 addition & 1 deletion src/main/acp/acp-structured-session-adapter-resume.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,7 @@ describe('reattaching a Grok chat the journal holds', () => {
handle: { nativeId: PROVIDER_SESSION },
replaces: { key: 'acp-key-saved-1', reason: 'restore-failed', replacedAt: 5_000 }
})
expect(acquired.link.supersedesKey).toBeUndefined()
expect(acquired.link?.supersedesKey).toBeUndefined()
expect(rig.sent('session/new')).toHaveLength(1)
const rows = await notRestoredRows(rig)
expect(rows).toHaveLength(1)
Expand Down
13 changes: 6 additions & 7 deletions src/main/claude/claude-structured-applied-options.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import {
acquired,
adapterFor,
fakeClaude,
identityFor
identityFor,
claudeOpened
} from './claude-structured-session-test-support'

/** Verbatim rows from Claude Code 2.1.280's list_models: `default` is the CLI's own
Expand Down Expand Up @@ -134,12 +135,10 @@ describe('Claude model before the first turn', () => {
const events: ClaudeStructuredSessionEvent[] = []
const adapter = await acquired(startedWithoutATurn(SETTINGS.noOverride), {}, events)

const started = events.find((event) => event.type === 'started')
const started = await claudeOpened(adapter, 'session-1')
expect(started).toMatchObject({ reportedOptions: { model: 'opus[1m]' } })
// Saved, it would be restored on every reopen past a later settings.json change.
expect(started?.type === 'started' ? started.reportedOptions : null).not.toHaveProperty(
'effort'
)
expect(started.reportedOptions).not.toHaveProperty('effort')
const reported = events.find((event) => event.type === 'options-reported')
expect(
reported?.type === 'options-reported' ? reported.reportedOptions : null
Expand Down Expand Up @@ -174,8 +173,8 @@ describe('Claude model before the first turn', () => {
})

// `started` is persisted as the session's options; opus's effort must not ride along onto haiku.
const started = events.find((event) => event.type === 'started')
const started = await claudeOpened(adapter, 'session-1')
expect(started).toMatchObject({ reportedOptions: { model: 'haiku' } })
expect(started?.type === 'started' ? started.reportedOptions.effort : null).toBeUndefined()
expect(started.reportedOptions.effort).toBeUndefined()
})
})
17 changes: 15 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,8 @@ 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 { createClaudeSessionStartup } from './claude-structured-session-startup-state'
import { createClaudeInitProof } from './claude-structured-init-proof'
import { claudeProviderHandleLink } from './claude-structured-owner-identity'

export function sessionFor(send: Mock = vi.fn().mockResolvedValue(undefined)): ClaudeSession {
return {
Expand Down Expand Up @@ -34,7 +35,19 @@ export function sessionFor(send: Mock = vi.fn().mockResolvedValue(undefined)): C
capabilities: [],
events: undefined,
translator: null,
startup: { ...createClaudeSessionStartup(), state: 'proven' }
link: claudeProviderHandleLink({
sessionId: 'provider-session',
leafUuid: null,
resumed: false,
fence: 1,
observedAt: 1
}),
initialization: {
proof: createClaudeInitProof(),
account: undefined,
resumesTranscript: false,
onLateRefusal: () => {}
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/main/claude/claude-structured-dispatch-waiters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export function forgetRetiredWaiter(session: ClaudeSession, waiter: ClaudeDispat
* starts — an interval bounded only by the previous turn. Elapsed time is
* therefore not evidence about delivery, and nothing here expires. Waiters end
* by provider and process facts instead: an echo, a `command_lifecycle` frame,
* the CLI's idle after `started` (`claude-command-lifecycle.ts`), a failed
* the CLI's idle after open (`claude-command-lifecycle.ts`), a failed
* write, or child exit.
*/
export function waitForReplay(
Expand Down
13 changes: 0 additions & 13 deletions src/main/claude/claude-structured-dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,6 @@ import {
claudeUserMessageWasProvablyUnwritten
} from './claude-agent-sdk-user-message-queue'
import { AgentSessionPreDispatchError } from '../native-chat/agent-session-wire/structured-agent-session-operation-settlement'
import {
claudeStartupFailureFact,
failClaudeStartup
} from './claude-structured-session-startup-state'

const MAX_ACTIVE_DISPATCH_WAITERS = 64

Expand Down Expand Up @@ -99,7 +95,6 @@ export function releaseClaudeDispatchWaitersInDoubt(
* CLI's idle has not. Retired rather than dropped: their identities stay joinable, bounded by
* `MAX_RETIRED_DISPATCH_WAITERS`. */
export function retireClaudeDispatchWaiters(session: ClaudeSession): void {
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 Down Expand Up @@ -132,14 +127,6 @@ export async function dispatchClaudeTurn(
if (session.dispatchWaiters.length >= MAX_ACTIVE_DISPATCH_WAITERS) {
return { state: 'rejected', ...claudeDispatchRejection(agentSessionFailureFact('queueFull')) }
}
const startupFailure = claudeStartupFailureFact(session)
if (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.
const acceptsResult = claudeDispatchInvokesSlashCommand(content)
Expand Down
10 changes: 5 additions & 5 deletions src/main/claude/claude-structured-effort-default-at-rest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ import {
fakeClaude,
identityFor,
recordingJournalSink,
claudeStartupSettled
claudeStartupSettled,
claudeOpened
} from './claude-structured-session-test-support'

const SESSION = 'session-1'
Expand Down Expand Up @@ -396,14 +397,13 @@ describe('Claude effort default at rest', () => {
it("never saves the applied effort as the chat's pick, yet shows it at rest", async () => {
const store = new AgentModelCatalogStore()
const events: ClaudeStructuredSessionEvent[] = []
await startChild(store, undefined, events)
const adapter = await startChild(store, undefined, events)

const started = events.find((event) => event.type === 'started')
const reported = started?.type === 'started' ? started.reportedOptions : null
const reported = (await claudeOpened(adapter, SESSION)).reportedOptions
expect(reported).not.toHaveProperty('effort')
// The record the start persists names the listed row the catalog learned under.
const record = restingRecord(
nativeSessionOptionsFromReport({ reported: reported!, restoreSkipped: [] })
nativeSessionOptionsFromReport({ reported: reported, restoreSkipped: [] })
)
expect(record.options).toEqual({ model: 'opus[1m]' })
expect((await readAtRest(store, record)).current.effort).toBe('medium')
Expand Down
7 changes: 0 additions & 7 deletions src/main/claude/claude-structured-event-delivery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,6 @@ export function emitClaudeStructuredSessionEvent({
const retired =
event.type === 'message' && session ? retireClaudeLaunchedModel(session, event.message) : null
if (session && retired !== null) {
deps.onEvent?.({
type: 'options-skipped',
sessionId: event.sessionId,
fence: session.fence,
acquisitionGeneration: session.acquisitionGeneration,
options: { model: retired }
})
resetClaudeRetiredModel(session, deps, event.sessionId, retired)
}
publishChildWork(event.sessionId, session, event.type === 'message' ? event.message : null)
Expand Down
17 changes: 15 additions & 2 deletions src/main/claude/claude-structured-options.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { ClaudeControlRequestTimeoutError } from './claude-agent-sdk-control-req
import { ClaudeBackgroundTaskTracker } from './claude-background-task-tracker'
import { ClaudeChildWorkDecoder } from './claude-child-work-decoder'
import { ClaudeSlashCommandCatalog } from './claude-slash-command-catalog'
import { createClaudeSessionStartup } from './claude-structured-session-startup-state'
import { createClaudeInitProof } from './claude-structured-init-proof'
import { claudeProviderHandleLink } from './claude-structured-owner-identity'
import {
observeClaudeFastModeFacts,
readClaudeStructuredSessionOptions
Expand Down Expand Up @@ -43,7 +44,19 @@ function sessionFor(setModel: ClaudeSession['connection']['setModel']): ClaudeSe
capabilities: [],
events: undefined,
translator: null,
startup: { ...createClaudeSessionStartup(), state: 'proven' }
link: claudeProviderHandleLink({
sessionId: 'provider-session',
leafUuid: null,
resumed: false,
fence: 1,
observedAt: 1
}),
initialization: {
proof: createClaudeInitProof(),
account: undefined,
resumesTranscript: false,
onLateRefusal: () => {}
}
}
}

Expand Down
9 changes: 0 additions & 9 deletions src/main/claude/claude-structured-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,6 @@ export function setClaudeStructuredSessionOption(
input: { key: string; value: string },
timeoutMs: number | undefined
): Promise<Readonly<Record<string, string>>> {
// Each write is a control request the CLI answers only after initialize.
if (session.startup.state !== 'proven') {
return Promise.reject(
new AgentSessionOptionRejectedError(
'Claude is still starting; options can be changed once it is ready.',
'providerStarting'
)
)
}
return setClaudeStructuredOption(session, input, timeoutMs)
}

Expand Down
2 changes: 1 addition & 1 deletion src/main/claude/claude-structured-prompt-ownership.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export async function cancelClaudeStructuredTurn(input: {
const acquisitionGeneration = session.acquisitionGeneration
// A CLI still starting answers no interrupt: Stop ends its child next (`stopEndsSession`), and
// that end settles every message it was handed and never echoed as stopped.
if (request.prompt || session.startup.state === 'pending') {
if (request.prompt) {
return { cancelled: false }
}
const requestedTurnId = request.turnId
Expand Down
Loading
Loading