mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 16:02:50 +00:00
fix(orchestration): decide a self-targeted --inject dispatch before agent detection
The no-recognized-agent rejection advised dispatching without --inject and sending the prompt manually, which is exactly what the coordinator guard forbids, so the same self-target input got opposite instructions depending on whether an agent happened to be detected in the pane. The coordinator guard now runs first. The flat dispatch-authority mock in manual-dispatch-observation gave the coordinator handle the worker's pane, which authority never does per handle.
This commit is contained in:
@@ -70,14 +70,6 @@ export const ORCHESTRATION_DISPATCH_METHODS: RpcMethod[] = [
|
||||
throw new Error(`Task ${params.task} is ${task.status}; only ready tasks can be dispatched`)
|
||||
}
|
||||
|
||||
// Why: injecting the preamble into a bare shell dumps it as shell commands (gibberish), so require a detected agent first.
|
||||
if (params.inject) {
|
||||
const hasAgent = await runtime.isTerminalRunningAgent(to)
|
||||
if (!hasAgent) {
|
||||
throw new Error(buildInjectRejectionMessage(to))
|
||||
}
|
||||
}
|
||||
|
||||
const dispatchAuthority = runtime.getOrchestrationDispatchAuthority(to)
|
||||
const assigneePaneKey =
|
||||
dispatchAuthority?.paneKey ?? runtime.getTerminalPaneKey(to) ?? undefined
|
||||
@@ -85,12 +77,6 @@ export const ORCHESTRATION_DISPATCH_METHODS: RpcMethod[] = [
|
||||
dispatchAuthority?.paneKey && dispatchAuthority.processIncarnation
|
||||
? dispatchAuthority.processIncarnation
|
||||
: undefined
|
||||
if (params.inject && (!assigneePaneKey || !processIncarnation)) {
|
||||
throw new OrchestrationError(
|
||||
'stable_pane_required',
|
||||
`Terminal ${to} has no stable pane/process incarnation for lifecycle authority.`
|
||||
)
|
||||
}
|
||||
// Why: the assignee side prefers dispatch authority, so the caller side must too — getTerminalPaneKey
|
||||
// alone returns null for a handle reachable only through the window-graph leaf, going inert here.
|
||||
const callerPane = params.from
|
||||
@@ -112,6 +98,21 @@ export const ORCHESTRATION_DISPATCH_METHODS: RpcMethod[] = [
|
||||
)
|
||||
}
|
||||
|
||||
// Why: injecting the preamble into a bare shell dumps it as shell commands (gibberish), so require a detected agent first.
|
||||
if (params.inject) {
|
||||
const hasAgent = await runtime.isTerminalRunningAgent(to)
|
||||
if (!hasAgent) {
|
||||
throw new Error(buildInjectRejectionMessage(to))
|
||||
}
|
||||
}
|
||||
|
||||
if (params.inject && (!assigneePaneKey || !processIncarnation)) {
|
||||
throw new OrchestrationError(
|
||||
'stable_pane_required',
|
||||
`Terminal ${to} has no stable pane/process incarnation for lifecycle authority.`
|
||||
)
|
||||
}
|
||||
|
||||
revalidateLegacyCoordinator?.()
|
||||
const ctx = db.createDispatchContext({
|
||||
taskId: params.task,
|
||||
|
||||
+10
-5
@@ -18,11 +18,16 @@ describe('manual Dispatch observation', () => {
|
||||
vi.spyOn(runtime, 'getTerminalPaneKey').mockImplementation((handle) =>
|
||||
handle === 'term_coord' ? coordinatorPaneKey : workerPaneKey
|
||||
)
|
||||
vi.spyOn(runtime, 'getOrchestrationDispatchAuthority').mockReturnValue({
|
||||
terminalHandle: 'term_worker',
|
||||
paneKey: workerPaneKey,
|
||||
processIncarnation: 'runtime_test:term_worker:1'
|
||||
} as never)
|
||||
// Authority is per handle in the real runtime; a flat mock would give the coordinator the worker's pane.
|
||||
vi.spyOn(runtime, 'getOrchestrationDispatchAuthority').mockImplementation(
|
||||
(handle) =>
|
||||
({
|
||||
terminalHandle: handle,
|
||||
paneKey: handle === 'term_coord' ? coordinatorPaneKey : workerPaneKey,
|
||||
processIncarnation:
|
||||
handle === 'term_coord' ? 'runtime_test:term_coord:1' : 'runtime_test:term_worker:1'
|
||||
}) as never
|
||||
)
|
||||
vi.spyOn(runtime, 'isTerminalRunningAgent').mockResolvedValue(true)
|
||||
vi.spyOn(runtime, 'sendTerminalAgentPrompt').mockResolvedValue({
|
||||
handle: 'term_worker',
|
||||
|
||||
+33
@@ -114,6 +114,39 @@ describe('orchestration.dispatch --to the caller', () => {
|
||||
expect(harness.runtime.sendTerminalAgentPrompt).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
// The rejection for a missing agent tells the caller to dispatch without --inject, which the
|
||||
// coordinator guard forbids; the self-target answer must not depend on agent presence.
|
||||
it.each([
|
||||
['the coordinator handle', 'term_coord'],
|
||||
['an alias of the coordinator pane', 'term_coord_alias']
|
||||
])('refuses %s even when no agent is detected', async (_label, target) => {
|
||||
vi.spyOn(harness.runtime, 'isTerminalRunningAgent').mockResolvedValue(false)
|
||||
vi.spyOn(harness.runtime, 'getOrchestrationDispatchAuthority').mockImplementation(
|
||||
(handle) =>
|
||||
(handle === 'term_coord_alias'
|
||||
? {
|
||||
terminalHandle: handle,
|
||||
paneKey: harness.coordinatorPaneKey,
|
||||
processIncarnation: 'runtime_test:term_coord:1'
|
||||
}
|
||||
: null) as never
|
||||
)
|
||||
const task = harness.db.createTask({
|
||||
spec: `self inject ${target}`,
|
||||
runId: harness.activeRunId
|
||||
})
|
||||
|
||||
await expect(
|
||||
harness.call('orchestration.dispatch', {
|
||||
task: task.id,
|
||||
from: 'term_coord',
|
||||
to: target,
|
||||
inject: true
|
||||
})
|
||||
).rejects.toMatchObject({ code: 'terminal_is_coordinator' })
|
||||
expect(harness.db.getDispatchContext(task.id)).toBeUndefined()
|
||||
})
|
||||
|
||||
it('still dispatches to a different pane', async () => {
|
||||
const task = harness.db.createTask({ spec: 'peer dispatch', runId: harness.activeRunId })
|
||||
const result = (await harness.call('orchestration.dispatch', {
|
||||
|
||||
Reference in New Issue
Block a user