mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 16:02:50 +00:00
fix(claude): preserve SDK preset and close ownership
This commit is contained in:
@@ -4,7 +4,7 @@ import { join } from 'node:path'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { spawnProcess, type SpawnedProcess } from '../../shared/child-process/run-process'
|
||||
import type { ProcessSpec } from '../../shared/child-process/process-spec'
|
||||
import type { CanUseTool } from '@anthropic-ai/claude-agent-sdk'
|
||||
import { query, type CanUseTool, type Options } from '@anthropic-ai/claude-agent-sdk'
|
||||
import {
|
||||
openClaudeStreamJsonConnection,
|
||||
type ClaudeStreamJsonConnection,
|
||||
@@ -85,14 +85,20 @@ const spawnedChildren: SpawnedProcess[] = []
|
||||
|
||||
async function open(
|
||||
launch: ClaudeStreamJsonLaunch,
|
||||
handlers: Parameters<typeof openClaudeStreamJsonConnection>[1] = {}
|
||||
handlers: Parameters<typeof openClaudeStreamJsonConnection>[1] = {},
|
||||
queryImpl?: typeof query
|
||||
): Promise<ClaudeStreamJsonConnection> {
|
||||
const connection = await openClaudeStreamJsonConnection(launch, handlers, (spec) => {
|
||||
spawned.push(spec)
|
||||
const child = spawnProcess(spec)
|
||||
spawnedChildren.push(child)
|
||||
return child
|
||||
})
|
||||
const connection = await openClaudeStreamJsonConnection(
|
||||
launch,
|
||||
handlers,
|
||||
(spec) => {
|
||||
spawned.push(spec)
|
||||
const child = spawnProcess(spec)
|
||||
spawnedChildren.push(child)
|
||||
return child
|
||||
},
|
||||
queryImpl
|
||||
)
|
||||
openConnections.push(connection)
|
||||
return connection
|
||||
}
|
||||
@@ -121,6 +127,17 @@ function readReportSafely(scenario: { readReport: () => ScriptedCliReport }) {
|
||||
}
|
||||
|
||||
describe('Claude stream-json connection', () => {
|
||||
it('passes the Claude Code system-prompt preset through to SDK query', async () => {
|
||||
const scenario = scriptScenario([HOLD_OPEN])
|
||||
let captured: Options | undefined
|
||||
await open(launchFor(scenario), {}, (params) => {
|
||||
captured = params.options
|
||||
return query(params)
|
||||
})
|
||||
|
||||
expect(captured?.systemPrompt).toEqual({ type: 'preset', preset: 'claude_code' })
|
||||
})
|
||||
|
||||
it('hands the child a derived environment, the resolved CLI path, and keeps the pid', async () => {
|
||||
vi.stubEnv('ANTHROPIC_API_KEY', 'sk-ant-SHELL-LEAK')
|
||||
vi.stubEnv('CLAUDE_CODE_CHILD_SESSION', '1')
|
||||
|
||||
@@ -91,12 +91,13 @@ function exitError(stderrTail: string, status: ExitStatus | null, cause?: Error)
|
||||
export async function openClaudeStreamJsonConnection(
|
||||
launch: ClaudeStreamJsonLaunch,
|
||||
handlers: ClaudeStreamJsonConnectionHandlers = {},
|
||||
spawnImpl: typeof spawnProcess = spawnProcess
|
||||
spawnImpl: typeof spawnProcess = spawnProcess,
|
||||
queryImpl?: typeof ClaudeAgentSdk.query
|
||||
): Promise<ClaudeStreamJsonConnection> {
|
||||
const { query } = await loadClaudeAgentSdk()
|
||||
const spawner = createClaudeCodeProcessSpawn(spawnImpl)
|
||||
const inbox = createClaudeUserMessageQueue()
|
||||
const session = query({
|
||||
const session = (queryImpl ?? query)({
|
||||
prompt: inbox.messages,
|
||||
options: {
|
||||
...launch.options,
|
||||
|
||||
@@ -69,6 +69,7 @@ describe('claude structured launch resolution', () => {
|
||||
includePartialMessages: true,
|
||||
settingSources: [...CLAUDE_DEFAULT_SETTING_SOURCES],
|
||||
supportedDialogKinds: [],
|
||||
systemPrompt: { type: 'preset', preset: 'claude_code' },
|
||||
extraArgs: { 'replay-user-messages': null },
|
||||
sessionId: first.providerSessionId
|
||||
})
|
||||
|
||||
@@ -16,6 +16,7 @@ export type ClaudeStructuredSdkOptions = Pick<
|
||||
| 'settingSources'
|
||||
| 'supportedDialogKinds'
|
||||
| 'extraArgs'
|
||||
| 'systemPrompt'
|
||||
| 'model'
|
||||
| 'effort'
|
||||
| 'sessionId'
|
||||
@@ -34,6 +35,9 @@ export const CLAUDE_STRUCTURED_BASE_OPTIONS: ClaudeStructuredSdkOptions = {
|
||||
includePartialMessages: true,
|
||||
settingSources: [...CLAUDE_DEFAULT_SETTING_SOURCES],
|
||||
supportedDialogKinds: [],
|
||||
// Keep the SDK on Claude Code's own system-prompt contract. Without this
|
||||
// preset, query() can run with a generic/empty prompt under SDK defaults.
|
||||
systemPrompt: { type: 'preset', preset: 'claude_code' },
|
||||
extraArgs: { 'replay-user-messages': null }
|
||||
}
|
||||
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type { StructuredAgentSessionAdapter } from './structured-agent-session-adapter'
|
||||
import { StructuredAgentSessionAdapterRouter } from './structured-agent-session-adapter-router'
|
||||
|
||||
function adapterOf(
|
||||
closeSession: StructuredAgentSessionAdapter['closeSession']
|
||||
): StructuredAgentSessionAdapter {
|
||||
return {
|
||||
acquire: vi.fn(async () => ({ process: { pid: 1 } }) as never),
|
||||
closeSession,
|
||||
dispatch: vi.fn(),
|
||||
cancelTurn: vi.fn(),
|
||||
answerPrompt: vi.fn(),
|
||||
setOption: vi.fn()
|
||||
} as unknown as StructuredAgentSessionAdapter
|
||||
}
|
||||
|
||||
describe('StructuredAgentSessionAdapterRouter.closeSession', () => {
|
||||
it('retains the owner when provider exit is unproven so close can retry', async () => {
|
||||
const closeSession = vi.fn().mockResolvedValueOnce(false).mockResolvedValueOnce(true)
|
||||
const adapter = adapterOf(closeSession)
|
||||
const router = new StructuredAgentSessionAdapterRouter(
|
||||
{ claude: adapter, codex: adapter },
|
||||
async () => {}
|
||||
)
|
||||
const identity = { sessionId: 'session-1', agent: 'claude' } as never
|
||||
await router.acquire({ identity, fence: 1, spawnToken: 'spawn-1' })
|
||||
|
||||
await expect(router.closeSession('session-1')).resolves.toBe(false)
|
||||
await expect(router.closeSession('session-1')).resolves.toBe(true)
|
||||
expect(closeSession).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
})
|
||||
@@ -70,7 +70,11 @@ export class StructuredAgentSessionAdapterRouter implements StructuredAgentSessi
|
||||
return false
|
||||
}
|
||||
const closed = await adapter.closeSession?.(sessionId)
|
||||
this.owners.delete(sessionId)
|
||||
// Keep routing ownership while the provider cannot prove its child exited;
|
||||
// a retry must still reach the adapter that owns the live process.
|
||||
if (closed === true) {
|
||||
this.owners.delete(sessionId)
|
||||
}
|
||||
return closed === true
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user