Replace post-worker_done polling with idle behavior (#7509)

- Instruct workers to stop and idle or exit immediately after sending
  `worker_done`, rather than running a 10-minute polling loop.
- Distinguish instructions based on worker kind: prompt-returning
  agents should remain idle for re-engagement, while bare-shell
  workers should exit.
- Prevent infinite polling overhead since the coordinator re-engages
  workers via fresh terminal input instead of inbox polling.
This commit is contained in:
Jinjing 2026-07-05 23:10:20 -07:00 committed by GitHub
parent af98a72f3f
commit c97d3a4977
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 104 additions and 24 deletions

View File

@ -116,4 +116,16 @@ describe('orchestration skill guidance', () => {
expect(skill).toContain('post-review corrections and PR prep belong to that named owner')
expect(skill).toContain('the named owner edits files and creates the PR')
})
it('keeps worker_done post-completion guidance idle instead of polling', () => {
const skill = readSkill()
const agentGuidance = getSection(skill, 'Agent Guidance')
expect(agentGuidance).toContain('After sending `worker_done`, end your turn')
expect(agentGuidance).toContain('idle at the agent prompt')
expect(agentGuidance).toContain('Do not poll or keep calling `orca orchestration check`')
expect(agentGuidance).toContain('fresh preamble + TASK block delivered as new terminal input')
expect(skill).not.toContain('post-completion polling messages')
expect(skill).not.toContain('every 2 minutes')
})
})

View File

@ -46,7 +46,7 @@ Classify inherited context before sending lifecycle messages:
- Full handoff means ownership transfer, not supervised dispatch. The original actor is not monitoring a DAG, so do not create lifecycle obligations unless the user explicitly asks you to supervise.
- Classify requests containing "hand off", "handoff", "handover", "give this to another agent", "give this to another worktree", "another agent", or "another worktree" as full handoffs by default, even when the user names a custom model or reasoning effort.
- Use supervised orchestration only when the user explicitly asks you to "supervise", "monitor", "wait", "track completion", "wait for worker_done", return results, coordinate a DAG, use a decision gate, or manage ask/reply flow.
- Do not use `orca orchestration dispatch --inject` for full handoffs. It injects a coordinator preamble that tells the worker to send `worker_done`, heartbeat, `ask`, and post-completion polling messages back to the original terminal.
- Do not use `orca orchestration dispatch --inject` for full handoffs. It injects a coordinator preamble that tells the worker to send `worker_done`, heartbeat, and `ask` messages, then end its turn under the original terminal's dispatch lifecycle.
- Do not run `orca orchestration task-create`, `orca orchestration dispatch --inject`, or `orca orchestration check --wait` for full handoffs. Do not peek at terminal output after prompt delivery to monitor progress.
- A review-only `worker_done` reports findings; it does not authorize coordinator file edits. After a review-only completion, synthesize findings, ask a decision gate if ownership is unclear, and dispatch or hand off fixes unless the user explicitly asked the coordinator to own fixes.
- If the user's plan names a next owner agent (for example, "then use opencode to create a PR"), post-review corrections and PR prep belong to that named owner. The coordinator routes, synthesizes, asks decision gates when needed, and supervises; the named owner edits files and creates the PR.
@ -197,6 +197,7 @@ Wait for `tui-idle` before dispatching. Always pass `--timeout-ms`; real coding
- Workers with a valid live preamble must send `worker_done` exactly once, even on failure:
`orca orchestration send --to <coordinator_handle> --type worker_done --subject "<short status>" --body "<3-sentence summary: what you did, what you found, what's left>" --payload '{"taskId":"<task_id>","dispatchId":"<dispatch_id>","filesModified":["path/a"],"reportPath":"<optional>"}' --json`
- After sending `worker_done`, end your turn and idle at the agent prompt. Do not poll or keep calling `orca orchestration check`; the coordinator re-engages you with a fresh preamble + TASK block delivered as new terminal input.
- For long tasks, send heartbeat/status only when the preamble asks for it, including both IDs:
`orca orchestration send --to <coordinator_handle> --type heartbeat --subject "alive" --payload '{"taskId":"<task_id>","dispatchId":"<dispatch_id>","phase":"implementing"}' --json`
- If blocked before completion, use `ask`; use `escalation` only when ownership is valid and the coordinator must intervene.

View File

@ -73,15 +73,16 @@ Slack, GitHub comments, or any other channel to reach a human during the run.
=== AFTER YOU SEND worker_done ===
Keep the shell session open for a grace period (10 minutes) in case the
coordinator sends a follow-up or re-dispatches you. Poll with
\`orca orchestration check\` every 2 minutes during that window. If no
follow-up arrives, you may exit after the grace period — the coordinator
will not expect further output from you.
worker_done ends your turn for this task. Your dispatched work is complete:
stop, return to an idle prompt, and take no further actions — do NOT start
new or unrelated work, do NOT run a sleep/poll loop, and do NOT keep calling
\`orca orchestration check\`. The coordinator has already recorded your
completion and expects no further output.
If the coordinator re-dispatches you (you will receive a fresh preamble +
TASK block), reset your polling and start the new task. Do not respond
to the previous task's follow-ups after a re-dispatch.
Do not exit the shell. Your terminal stays available, and if the
coordinator has more for you it will re-engage this terminal with a fresh
preamble + TASK block, which arrives as new input. When that happens,
reset and start the new task; ignore the previous task's follow-ups.
=== TASK ===
TASK_BODY"

View File

@ -12,6 +12,16 @@ function baseParams(overrides: Partial<Parameters<typeof buildDispatchPreamble>[
}
}
function afterWorkerDoneSection(result: string) {
const sectionStart = result.indexOf('=== AFTER YOU SEND worker_done ===')
const sectionEnd = result.indexOf('=== TASK ===')
expect(sectionStart).toBeGreaterThan(-1)
expect(sectionEnd).toBeGreaterThan(sectionStart)
return result.slice(sectionStart, sectionEnd)
}
describe('buildDispatchPreamble', () => {
it('substitutes template variables', () => {
const result = buildDispatchPreamble(baseParams())
@ -93,11 +103,33 @@ describe('buildDispatchPreamble', () => {
expect(occurrences).toBe(3)
})
it('includes AFTER YOU SEND block with 2-minute poll cadence and release signal', () => {
it('tells prompt-returning workers to idle without post-done polling', () => {
const result = buildDispatchPreamble(baseParams())
expect(result).toContain('=== AFTER YOU SEND worker_done ===')
expect(result).toMatch(/2 minutes/)
expect(result).toMatch(/may exit/)
const section = afterWorkerDoneSection(result)
expect(section).toContain('=== AFTER YOU SEND worker_done ===')
expect(section).toContain('worker_done ends your turn for this task')
expect(section).toContain('return to an idle prompt')
expect(section).toContain('Do not exit the shell')
expect(section).toContain('do NOT run a sleep/poll loop')
expect(section).toContain('do NOT keep calling')
expect(section).toMatch(/fresh\s+preamble \+ TASK block/)
expect(section).not.toMatch(/2 minutes/)
expect(section).not.toMatch(/10 minutes/)
expect(section).not.toMatch(/may exit/)
expect(section).not.toMatch(/grace period/)
})
it('tells bare-shell workers to exit after worker_done', () => {
const result = buildDispatchPreamble(baseParams({ workerKind: 'bare-shell' }))
const section = afterWorkerDoneSection(result)
expect(section).toContain('Exit the shell after completion')
expect(section).toContain('Bare-shell workers have no idle agent')
expect(section).toContain('do NOT run a sleep/poll loop')
expect(section).not.toContain('Do not exit the shell')
expect(section).not.toMatch(/2 minutes/)
expect(section).not.toMatch(/may exit/)
})
it('uses === TASK === separator with the task spec appended', () => {

View File

@ -21,6 +21,9 @@ export type PreambleParams = {
behind: number
recentSubjects: string[]
}
// Why: prompt-returning agents should idle after worker_done, while bare
// shells have no agent prompt for Orca to reuse.
workerKind?: 'prompt-returning-agent' | 'bare-shell'
}
// Why: 5 minutes is frequent enough that the coordinator's stale-heartbeat
@ -39,6 +42,10 @@ export function buildDispatchPreamble(params: PreambleParams): string {
// socket. Without this, agents inside the dev Electron app would call the
// production CLI and talk to the wrong Orca instance (Section 6.4).
const cli = params.devMode ? 'orca-dev' : 'orca'
const postDoneInstructions = buildPostWorkerDoneInstructions({
cli,
workerKind: params.workerKind ?? 'prompt-returning-agent'
})
const header = `You are working inside Orca, a multi-agent IDE. You are a dispatched worker.
Your coordinator's terminal handle is: ${params.coordinatorHandle}
@ -110,17 +117,7 @@ Slack, GitHub comments, or any other channel to reach a human during the run.
# Check for messages from the coordinator:
${cli} orchestration check
=== AFTER YOU SEND worker_done ===
Keep the shell session open for a grace period (10 minutes) in case the
coordinator sends a follow-up or re-dispatches you. Poll with
\`${cli} orchestration check\` every 2 minutes during that window. If no
follow-up arrives, you may exit after the grace period the coordinator
will not expect further output from you.
If the coordinator re-dispatches you (you will receive a fresh preamble +
TASK block), reset your polling and start the new task. Do not respond
to the previous task's follow-ups after a re-dispatch.`
${postDoneInstructions}`
// Why: the drift section fires only when the coordinator allowed dispatch
// against a stale worktree (via `allow-stale-base: true` in the task spec,
@ -136,6 +133,43 @@ to the previous task's follow-ups after a re-dispatch.`
${params.taskSpec}`
}
function buildPostWorkerDoneInstructions({
cli,
workerKind
}: {
cli: string
workerKind: NonNullable<PreambleParams['workerKind']>
}): string {
// Why: re-dispatch reaches idle agents as terminal input; inbox polling
// after completion cannot receive that new TASK block and looks hung.
if (workerKind === 'bare-shell') {
return `=== AFTER YOU SEND worker_done ===
worker_done ends your turn for this task. Your dispatched work is complete:
stop and take no further actions do NOT start new or unrelated work,
do NOT run a sleep/poll loop, and do NOT keep calling
\`${cli} orchestration check\`. The coordinator has already recorded your
completion and expects no further output.
Exit the shell after completion. Bare-shell workers have no idle agent
prompt for Orca to reuse; if the coordinator has more for you it will
dispatch or prompt another worker with a fresh TASK block.`
}
return `=== AFTER YOU SEND worker_done ===
worker_done ends your turn for this task. Your dispatched work is complete:
stop, return to an idle prompt, and take no further actions do NOT start
new or unrelated work, do NOT run a sleep/poll loop, and do NOT keep calling
\`${cli} orchestration check\`. The coordinator has already recorded your
completion and expects no further output.
Do not exit the shell. Your terminal stays available, and if the
coordinator has more for you it will re-engage this terminal with a fresh
preamble + TASK block, which arrives as new input. When that happens,
reset and start the new task; ignore the previous task's follow-ups.`
}
function buildDriftSection(drift: NonNullable<PreambleParams['baseDrift']>): string {
const subjects = drift.recentSubjects.map((s) => ` - ${s}`).join('\n')
return `