mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 16:02:50 +00:00
fix(claude): bound background task stop requests
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
import type { Query } from '@anthropic-ai/claude-agent-sdk'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createClaudeControlSurface } from './claude-agent-sdk-control-requests'
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
describe('createClaudeControlSurface stopTask', () => {
|
||||
it('bounds a lost reply and permits a later stop request', async () => {
|
||||
vi.useFakeTimers()
|
||||
const stopTask = vi
|
||||
.fn<() => Promise<void>>()
|
||||
.mockImplementationOnce(() => new Promise(() => {}))
|
||||
.mockResolvedValueOnce()
|
||||
const controls = createClaudeControlSurface({ stopTask } as unknown as Query)
|
||||
const timedOut = expect(controls.stopTask('task-1', { timeoutMs: 25 })).rejects.toThrow(
|
||||
'claude stop_task request timed out'
|
||||
)
|
||||
|
||||
await vi.advanceTimersByTimeAsync(25)
|
||||
await timedOut
|
||||
await expect(controls.stopTask('task-2', { timeoutMs: 25 })).resolves.toBeUndefined()
|
||||
expect(stopTask).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
})
|
||||
@@ -91,6 +91,7 @@ export type ClaudeControlSurface = {
|
||||
settings: Parameters<Query['applyFlagSettings']>[0],
|
||||
options?: ClaudeControlOptions
|
||||
) => Promise<void>
|
||||
stopTask: (taskId: string, options?: ClaudeControlOptions) => Promise<void>
|
||||
supportedModels: (options?: ClaudeControlOptions) => Promise<unknown[]>
|
||||
initializationResult: (options?: ClaudeControlOptions) => Promise<unknown>
|
||||
getSettings: (options?: ClaudeControlOptions) => Promise<unknown>
|
||||
@@ -135,6 +136,10 @@ export function createClaudeControlSurface(query: Query): ClaudeControlSurface {
|
||||
() => query.applyFlagSettings(settings),
|
||||
options?.timeoutMs
|
||||
).then(() => {}),
|
||||
stopTask: (taskId, options) =>
|
||||
runClaudeControl('stop_task', () => query.stopTask(taskId), options?.timeoutMs).then(
|
||||
() => {}
|
||||
),
|
||||
supportedModels: (options) =>
|
||||
runClaudeControl('list_models', () => query.supportedModels(), options?.timeoutMs),
|
||||
initializationResult: (options) =>
|
||||
|
||||
@@ -76,7 +76,6 @@ export type ClaudeStreamJsonConnection = ClaudeControlSurface & {
|
||||
/** What the ladder has observed so far; read after a `close()` that returned false. */
|
||||
readonly exitVerdict: ClaudeChildExitVerdict
|
||||
send: (message: Record<string, unknown>) => Promise<void>
|
||||
stopTask: (taskId: string) => Promise<void>
|
||||
/** Resolves true after processless settlement, or root exit plus observed tree exit. */
|
||||
close: () => Promise<boolean>
|
||||
}
|
||||
@@ -279,7 +278,6 @@ export async function openClaudeStreamJsonConnection(
|
||||
} as const
|
||||
},
|
||||
send,
|
||||
stopTask: (taskId) => session.stopTask(taskId),
|
||||
close
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,11 +128,14 @@ describe('stopClaudeBackgroundTasks', () => {
|
||||
{ task_id: 'task-bash', task_type: 'local_bash', description: 'bash' }
|
||||
]
|
||||
})
|
||||
const stopTask = vi.fn(async (_taskId: string) => {})
|
||||
const stopTask = vi.fn(async (_taskId: string, _options?: { timeoutMs?: number }) => {})
|
||||
const session = { backgroundTasks, connection: { stopTask } } as unknown as ClaudeSession
|
||||
|
||||
await expect(stopClaudeBackgroundTasks(session)).resolves.toEqual({ cancelled: true })
|
||||
expect(stopTask.mock.calls.map(([taskId]) => taskId)).toEqual(['task-agent', 'task-bash'])
|
||||
await expect(stopClaudeBackgroundTasks(session, 5_000)).resolves.toEqual({ cancelled: true })
|
||||
expect(stopTask.mock.calls).toEqual([
|
||||
['task-agent', { timeoutMs: 5_000 }],
|
||||
['task-bash', { timeoutMs: 5_000 }]
|
||||
])
|
||||
})
|
||||
|
||||
it('stops issuing requests when ownership changes between tasks', async () => {
|
||||
@@ -152,7 +155,7 @@ describe('stopClaudeBackgroundTasks', () => {
|
||||
})
|
||||
const session = { backgroundTasks, connection: { stopTask } } as unknown as ClaudeSession
|
||||
|
||||
await stopClaudeBackgroundTasks(session, () => current)
|
||||
await stopClaudeBackgroundTasks(session, undefined, () => current)
|
||||
expect(stopTask).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -45,6 +45,7 @@ export async function cancelClaudeTurn(
|
||||
|
||||
export async function stopClaudeBackgroundTasks(
|
||||
session: ClaudeSession,
|
||||
timeoutMs: number | undefined,
|
||||
isCurrent: ClaudeTurnCancellationGuard = () => true
|
||||
): Promise<{ cancelled: boolean }> {
|
||||
const taskIds = session.backgroundTasks.stoppableTaskIds
|
||||
@@ -54,7 +55,7 @@ export async function stopClaudeBackgroundTasks(
|
||||
break
|
||||
}
|
||||
try {
|
||||
await session.connection.stopTask(taskId)
|
||||
await session.connection.stopTask(taskId, { timeoutMs })
|
||||
cancelled = true
|
||||
} catch (error) {
|
||||
if (!(error instanceof ClaudeControlRequestError)) {
|
||||
|
||||
@@ -207,7 +207,7 @@ export class ClaudeStructuredSessionAdapter implements StructuredAgentSessionAda
|
||||
stopBackgroundTasks: StructuredAgentSessionAdapter['stopBackgroundTasks'] = (input) => {
|
||||
const session = this.session(input.sessionId)
|
||||
const acquisitionGeneration = session.acquisitionGeneration
|
||||
return stopClaudeBackgroundTasks(session, () =>
|
||||
return stopClaudeBackgroundTasks(session, this.deps.requestTimeoutMs, () =>
|
||||
Boolean(
|
||||
this.sessions.get(input.sessionId) === session &&
|
||||
session.fence === input.fence &&
|
||||
|
||||
Reference in New Issue
Block a user