Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions src/main/claude/claude-adapter-capture-replay.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ import {
identityFor,
PROVIDER_SESSION_ID
} from './claude-structured-session-test-support'
import { recordingStructuredAgentSessionLogger } from '../native-chat/agent-session-wire/structured-agent-session-logger-test-support'

const TEST_LOGGER = recordingStructuredAgentSessionLogger().logger

type CapturedEvent =
| { at: number; kind: 'meta'; providerSessionId: string }
Expand Down Expand Up @@ -108,6 +111,7 @@ async function replayCapture(name: string): Promise<Replay> {
// The capture supplies every frame, startup proof included.
const claude = fakeClaude({ initProof: 'none', replayUuid: null })
const deps: ClaudeStructuredSessionAdapterDeps = {
logger: TEST_LOGGER,
resolveLaunch: async () => ({
pathToClaudeCodeExecutable: 'claude',
options: {},
Expand Down
4 changes: 4 additions & 0 deletions src/main/claude/claude-agent-sdk-contract-pins.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ import { LOCAL_EXECUTION_HOST_ID } from '../../shared/execution-host'
import type { AgentSessionRecordStore } from '../runtime/agent-session-record-store'
import { claudeQuerySettingsReader } from './claude-agent-sdk-control-requests'
import { createClaudeStructuredLaunchResolver } from './claude-structured-launch-resolution'
import { recordingStructuredAgentSessionLogger } from '../native-chat/agent-session-wire/structured-agent-session-logger-test-support'

const TEST_LOGGER = recordingStructuredAgentSessionLogger().logger

// Contract pins for @anthropic-ai/claude-agent-sdk, run against the real SDK
// driving a scripted fake CLI (never the real Claude binary). These tests exist
Expand Down Expand Up @@ -159,6 +162,7 @@ function resolvedLaunch(permissionMode: PermissionMode, launchArgs: string[] = [
launchArgs
} as unknown as AgentSessionRecord
return createClaudeStructuredLaunchResolver({
logger: TEST_LOGGER,
store: { getRecord: () => record } as unknown as AgentSessionRecordStore,
resolveWorkspacePath: async () => '/repos/workspace-1',
resolveCommand: () => FAKE_CLI,
Expand Down
8 changes: 7 additions & 1 deletion src/main/claude/claude-at-rest-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { structuredSlashCommands } from '../../shared/structured-agent-session-c
import { discoverSkills } from '../skills/discovery'
import { scanClaudeCommandFolders } from './claude-command-folder-scan'
import { supportsClaudeStructuredLocation } from './claude-structured-location-support'
import type { StructuredAgentSessionLogger } from '../native-chat/agent-session-wire/structured-agent-session-logger'

/** How long one scan answers for a workspace and account before the next read scans again. */
export const CLAUDE_AT_REST_COMMANDS_TTL_MS = 10_000
Expand All @@ -14,6 +15,7 @@ const SCAN_OVERDUE_MS = 30_000
const MAX_SCANS_IN_FLIGHT = 2

export type ClaudeAtRestCommandsDeps = {
logger: StructuredAgentSessionLogger
resolveWorkspacePath: (workspaceId: string) => Promise<string>
now?: () => number
discover?: typeof discoverSkills
Expand Down Expand Up @@ -101,7 +103,11 @@ export class ClaudeAtRestCommandCatalog {
(error: unknown) => {
// Waits out the window before trying again, but leaves room for an older scan's answer.
entry.scannedAt = this.now()
console.warn('[claude] reading the at-rest `/` commands failed:', error)
this.deps.logger.warn('reading the at-rest `/` commands failed', {
scope: 'claude-at-rest-commands',
sessionId: record.sessionId,
error
})
}
)
.finally(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ import {
identityFor,
PROVIDER_SESSION_ID
} from './claude-structured-session-test-support'
import { recordingStructuredAgentSessionLogger } from '../native-chat/agent-session-wire/structured-agent-session-logger-test-support'

const TEST_LOGGER = recordingStructuredAgentSessionLogger().logger

export const parent = makeStructuredAgentStatusSubject(
{
Expand Down Expand Up @@ -115,6 +118,7 @@ export async function producer(host?: AgentHookServer) {
const evidenceLog: AgentChildWorkEvidence[][] = []
const ingested: (AgentChildWorkReconcileOutcome | null)[] = []
const adapter = new ClaudeStructuredSessionAdapter({
logger: TEST_LOGGER,
resolveLaunch: async () => ({
pathToClaudeCodeExecutable: 'claude',
options: {},
Expand Down
4 changes: 4 additions & 0 deletions src/main/claude/claude-command-lifecycle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ import {
PROVIDER_SESSION_ID,
type FakeConnection
} from './claude-structured-session-test-support'
import { recordingStructuredAgentSessionLogger } from '../native-chat/agent-session-wire/structured-agent-session-logger-test-support'

const TEST_LOGGER = recordingStructuredAgentSessionLogger().logger

type CapturedEvent =
| { kind: 'meta'; providerSessionId: string }
Expand Down Expand Up @@ -115,6 +118,7 @@ async function replayCapture(
// The capture supplies every frame, startup proof included.
const claude = fakeClaude({ initProof: 'none', replayUuid: null })
const adapter = new ClaudeStructuredSessionAdapter({
logger: TEST_LOGGER,
resolveLaunch: async () => ({
pathToClaudeCodeExecutable: 'claude',
options: {},
Expand Down
4 changes: 4 additions & 0 deletions src/main/claude/claude-fold-steer-receipt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ import {
twoSteersCapture,
TWO_STEERS_SEND_AT
} from './claude-captured-fold-steer-frames.test-fixture'
import { recordingStructuredAgentSessionLogger } from '../native-chat/agent-session-wire/structured-agent-session-logger-test-support'

const TEST_LOGGER = recordingStructuredAgentSessionLogger().logger

const T0 = 1_700_000_100_000

Expand Down Expand Up @@ -80,6 +83,7 @@ async function riggedAdapter(
const settled = vi.fn()
const claude = fakeClaude({ replayUuid: null, ...claudeOptions })
const deps: ClaudeStructuredSessionAdapterDeps = {
logger: TEST_LOGGER,
resolveLaunch: async () => ({
pathToClaudeCodeExecutable: 'claude',
options: {},
Expand Down
4 changes: 4 additions & 0 deletions src/main/claude/claude-history-window-source-budget.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import { tmpdir } from 'node:os'
import type { FileHandle } from 'node:fs/promises'
import type * as FsPromises from 'node:fs/promises'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { recordingStructuredAgentSessionLogger } from '../native-chat/agent-session-wire/structured-agent-session-logger-test-support'

const TEST_LOGGER = recordingStructuredAgentSessionLogger().logger

const state = vi.hoisted(() => ({
path: '',
Expand Down Expand Up @@ -95,6 +98,7 @@ let directory = ''

const read = (previousLeafUuid: string | null = 'anchor') =>
readClaudeProviderHistoryWindow({
logger: TEST_LOGGER,
transcriptPath: state.path,
providerSessionId: 'provider',
previousLeafUuid,
Expand Down
2 changes: 2 additions & 0 deletions src/main/claude/claude-stop-ack-race.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
import { stopClaudeBackgroundTasks } from './claude-structured-control-actions'
import { CLAUDE_STRUCTURED_BASE_OPTIONS } from './claude-structured-launch-resolution'
import type { ClaudeSession } from './claude-structured-session-state'
import { testEventSinkLogging } from '../native-chat/agent-session-wire/structured-agent-session-logger-test-support'

const FAKE_CLI = join(__dirname, '__fixtures__', 'claude-agent-sdk-scripted-cli.mjs')
const SESSION_ID = '5348c19f-6a54-4c2e-9c68-9c2b1a3d4e5f'
Expand Down Expand Up @@ -107,6 +108,7 @@ async function stoppedAsItCompleted(ahead: number) {
}
},
{
...testEventSinkLogging(),
onMessage: (message) => {
seen.push(`${String(message.type)}:${String(message.subtype ?? '')}`)
decoder.observe(message)
Expand Down
58 changes: 55 additions & 3 deletions src/main/claude/claude-stream-json-connection-close.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ import {
openClaudeStreamJsonConnection,
type ClaudeStreamJsonLaunch
} from './claude-stream-json-connection'
import {
recordingStructuredAgentSessionLogger,
testEventSinkLogging
} from '../native-chat/agent-session-wire/structured-agent-session-logger-test-support'

const mocks = vi.hoisted(() => {
const refresh = vi.fn()
Expand Down Expand Up @@ -66,6 +70,7 @@ describe('Claude stream-json close ordering', () => {
connection = await openClaudeStreamJsonConnection(
{ pathToClaudeCodeExecutable: 'claude', options: {}, cwd: '/work/repo' },
{
...testEventSinkLogging(),
onMessage: (message) => {
seen.push(String(message.type))
if (message.type === 'first') {
Expand Down Expand Up @@ -115,6 +120,7 @@ describe('Claude stream-json close ordering', () => {
const connection = await openClaudeStreamJsonConnection(
{ pathToClaudeCodeExecutable: 'claude', options: {}, cwd: '/work/repo' },
{
...testEventSinkLogging(),
onMessage: (message) => events.push(`message:${String(message.type)}`),
onExit: () => events.push('exit')
},
Expand Down Expand Up @@ -153,7 +159,7 @@ describe('Claude stream-json close ordering', () => {
}) as unknown as typeof query
const connection = await openClaudeStreamJsonConnection(
{ pathToClaudeCodeExecutable: 'claude', options: {}, cwd: '/work/repo' },
{},
testEventSinkLogging(),
() => child,
queryImpl
)
Expand Down Expand Up @@ -190,7 +196,12 @@ describe('Claude stream-json close ordering', () => {
})()
return (async function* () {})()
}) as typeof query
const connection = await openClaudeStreamJsonConnection(launch, {}, () => child, queryImpl)
const connection = await openClaudeStreamJsonConnection(
launch,
testEventSinkLogging(),
() => child,
queryImpl
)

const closing = connection.close()
await new Promise((resolve) => setImmediate(resolve))
Expand Down Expand Up @@ -231,7 +242,12 @@ describe('Claude stream-json close ordering', () => {
})()
return (async function* () {})()
}) as typeof query
const connection = await openClaudeStreamJsonConnection(launch, {}, () => child, queryImpl)
const connection = await openClaudeStreamJsonConnection(
launch,
testEventSinkLogging(),
() => child,
queryImpl
)

child.stderr.emit('data', 'output')
await vi.waitFor(() => expect(mocks.refresh).toHaveBeenCalledTimes(1))
Expand All @@ -248,4 +264,40 @@ describe('Claude stream-json close ordering', () => {
await expect(closing).resolves.toBe(true)
expect(child.stdin.writableEnded).toBe(true)
})

it('reports a descendant that outlived an exited root through the chat logger', async () => {
mocks.refresh.mockReset()
mocks.proveClaudeChildExit.mockReset()
mocks.refresh.mockResolvedValue(undefined)
mocks.proveClaudeChildExit.mockResolvedValueOnce(false)
const child = fakeChild()
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: This injected query only has to spawn the child; the close never reads a message.
const queryImpl = ((params: Parameters<typeof query>[0]) => {
params.options?.spawnClaudeCodeProcess?.({
command: 'claude',
args: [],
env: {},
signal: new AbortController().signal
})
return (async function* () {})()
}) as typeof query
const logged = recordingStructuredAgentSessionLogger()
const connection = await openClaudeStreamJsonConnection(
{ pathToClaudeCodeExecutable: 'claude', options: {}, cwd: '/work/repo' },
{ sessionId: 'session-1', logger: logged.logger },
() => child,
queryImpl
)
child.emit('exit', 0, null)
Object.assign(mocks.tree, { treeVerdict: 'live' })
try {
await expect(connection.close()).resolves.toBe(false)
} finally {
Object.assign(mocks.tree, { treeVerdict: 'unverifiable' })
}

expect(logged.entries.map((entry) => entry.fields)).toEqual([
{ scope: 'claude-close-descendant', sessionId: 'session-1', pid: 424242 }
])
})
})
6 changes: 3 additions & 3 deletions src/main/claude/claude-stream-json-connection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,12 +91,12 @@ const spawnedChildren: SpawnedProcess[] = []

async function open(
launch: ClaudeStreamJsonLaunch,
handlers: Parameters<typeof openClaudeStreamJsonConnection>[1] = {},
handlers: Omit<Parameters<typeof openClaudeStreamJsonConnection>[1], 'sessionId' | 'logger'> = {},
queryImpl?: typeof query
): Promise<ClaudeStreamJsonConnection> {
const connection = await openClaudeStreamJsonConnection(
launch,
handlers,
{ ...testEventSinkLogging(), ...handlers },
(spec) => {
spawned.push(spec)
const child = spawnProcess(spec)
Expand Down Expand Up @@ -804,7 +804,7 @@ describe('the managed-auth live gate', () => {

try {
await expect(
openClaudeStreamJsonConnection(launchFor(scenario), {}, (spec) => {
openClaudeStreamJsonConnection(launchFor(scenario), testEventSinkLogging(), (spec) => {
const child = spawnProcess(spec)
started = child
const attach = child.stderr.on.bind(child.stderr)
Expand Down
10 changes: 8 additions & 2 deletions src/main/claude/claude-stream-json-connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
createClaudeUserMessageQueue
} from './claude-agent-sdk-user-message-queue'
import type { ClaudeStructuredSdkOptions } from './claude-structured-launch-resolution'
import type { StructuredAgentSessionLogger } from '../native-chat/agent-session-wire/structured-agent-session-logger'

export { ClaudeControlRequestError }

Expand Down Expand Up @@ -53,6 +54,9 @@ export type ClaudeStreamJsonLaunch = {
}

export type ClaudeStreamJsonConnectionHandlers = {
/** The chat this connection serves, and where its close reports what it could not reap. */
sessionId: string
logger: StructuredAgentSessionLogger
onMessage?: (message: Record<string, unknown>) => void
/**
* The SDK owns inbound permission control: it hands `can_use_tool` to this callback with
Expand Down Expand Up @@ -114,7 +118,7 @@ function exitError(stderrTail: string, status: ExitStatus | null, cause?: Error)

export async function openClaudeStreamJsonConnection(
launch: ClaudeStreamJsonLaunch,
handlers: ClaudeStreamJsonConnectionHandlers = {},
handlers: ClaudeStreamJsonConnectionHandlers,
spawnImpl: typeof spawnProcess = spawnProcess,
queryImpl?: typeof ClaudeAgentSdk.query
): Promise<ClaudeStreamJsonConnection> {
Expand Down Expand Up @@ -323,7 +327,9 @@ export async function openClaudeStreamJsonConnection(
inbox.fail(new Error('claude stream-json connection closed'))
if (!proven) {
if (exited && tree.treeVerdict === 'live') {
console.warn('[claude-stream-json] root exited but a descendant survived the close:', {
handlers.logger.warn('the Claude process exited but a descendant survived the close', {
scope: 'claude-close-descendant',
sessionId: handlers.sessionId,
pid: spawner.pid
})
}
Expand Down
6 changes: 6 additions & 0 deletions src/main/claude/claude-structured-auth-parity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ import {
fakeClaude,
identityFor
} from './claude-structured-session-test-support'
import { recordingStructuredAgentSessionLogger } from '../native-chat/agent-session-wire/structured-agent-session-logger-test-support'

const TEST_LOGGER = recordingStructuredAgentSessionLogger().logger

const SESSION_ID = 'orca-session-auth'
const IDENTITY = { sessionId: SESSION_ID } as Parameters<
Expand Down Expand Up @@ -42,6 +45,7 @@ function resolverFor(options: {
authSwitchSettleTimeoutMs?: number
}): ReturnType<typeof createClaudeStructuredLaunchResolver> {
return createClaudeStructuredLaunchResolver({
logger: TEST_LOGGER,
store: { getRecord: () => record() } as unknown as AgentSessionRecordStore,
resolveWorkspacePath: async (id) => `/repos/${id}`,
resolveCommand: () => '/usr/local/bin/claude',
Expand All @@ -67,7 +71,9 @@ function realResolverAdapter(
]
} as unknown as AgentSessionRecord
return new ClaudeStructuredSessionAdapter({
logger: TEST_LOGGER,
resolveLaunch: createClaudeStructuredLaunchResolver({
logger: TEST_LOGGER,
store: { getRecord: () => resumable } as unknown as AgentSessionRecordStore,
resolveWorkspacePath: async (id) => `/repos/${id}`,
resolveCommand: () => '/usr/local/bin/claude',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,8 @@ describe('Claude structured dispatch attachment rejections', () => {
})

it('rejects an attachment it cannot read with the generic sentence and no path', async () => {
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
const session = sessionFor()
const warn = vi.spyOn(session.logger, 'warn')
const path = join(tmpdir(), 'orca-claude-image-missing', 'gone.png')

await expect(
Expand All @@ -169,9 +169,12 @@ describe('Claude structured dispatch attachment rejections', () => {
expect(session.connection.send).not.toHaveBeenCalled()
// The row drops the error, so the log is the only place left to find why.
expect(warn).toHaveBeenCalledWith(
'[claude-dispatch] attachment could not be read:',
expect.objectContaining({ code: 'ENOENT' })
'an attachment could not be read',
expect.objectContaining({
scope: 'claude-attachment-read',
sessionId: session.sessionId,
error: expect.objectContaining({ code: 'ENOENT' })
})
)
warn.mockRestore()
})
})
Loading
Loading