fix(task-session): fill false-completed task parts with child session real output (#863) - #912
Conversation
… real output (alvinunreal#863) When a foreground task's primary model halts on a non-retryable error (e.g. 403 quota exhausted), opencode's halt produces an empty assistant message and runTask settles the BackgroundJob as completed with an empty output (result.parts.findLast(text)?.text ?? ""). omos's tryFallback then re-prompts with the fallback model on an orphan runLoop that produces the real result, but the parent task part already says completed with an empty <task_result>. The orchestrator reads the empty output, mis-judges the task as failed/empty, and self-amplifies by launching redundant replacement tasks (alvinunreal#863). Fix: in the messages.transform hook, after stabilizeRunningTaskParts, scan task tool parts with status:completed whose parsed <task_result> is empty. For each, read the child session's real assistant text via the existing extractSessionResult helper and in-place rewrite the part output with renderTaskCompletedWithText (mirrors opencode renderOutput). Idempotent and non-blocking: if the child session has not yet produced non-empty text (fallback model still running), extractSessionResult returns empty and the part is left unchanged — the next transform turn re-evaluates naturally, so no explicit wait/poll is needed. The transform hook only mutates the in-memory output.messages (not the persisted DB), so the rewrite is per-turn but real output eventually lands in history once the child session completes and opencode writes it itself. Gated: only acts on completed task parts whose parsed result is empty. True completions with real text are preserved unchanged. Non-task tool parts are not touched. This is the false-complete counterpart to PR alvinunreal#880's false-cancel fix: - alvinunreal#880 rewrites error+cancelled parts using board record (false-cancel) - this rewrites completed+empty parts using child session text (false-complete) Root cause is in opencode runTask's "?? "" fallback, which omos cannot patch as a plugin. This is the minimal omos-only reconciliation. Co-developed-with: oracle (GLM-5.2)
Greptile SummaryThis PR repairs false-completed foreground task results using output recovered from the child session.
Confidence Score: 2/5The PR should not merge until child-session lookup failures are contained and recovered text cannot prematurely terminate the task-result wrapper. The new transform path allows session read errors to abort valid parent turns, while its renderer permits literal closing tags in assistant output to truncate the recovered task result. Files Needing Attention: src/hooks/task-session-manager/board-injection.ts, src/hooks/task-session-manager/index.ts, src/utils/task.ts Important Files Changed
Sequence DiagramsequenceDiagram
participant Parent as Parent transform
participant Board as Background job board
participant Child as Child session
Parent->>Board: Resolve empty completed task ID
Board-->>Parent: Job metadata
Parent->>Child: Read assistant messages
alt Non-empty child output
Child-->>Parent: Recovered result
Parent->>Parent: Rewrite task output in memory
else No output yet
Child-->>Parent: Empty result
Parent->>Parent: Preserve part for later turn
else Request fails
Child--xParent: Error propagates and aborts transform
end
Reviews (1): Last reviewed commit: "fix(task-session): fill false-completed ..." | Re-trigger Greptile |
| const extracted = await extractSessionResult(client, childSessionId, { | ||
| directory, | ||
| includeReasoning: false, | ||
| }); |
There was a problem hiding this comment.
Child lookup aborts transforms
When the child-session messages request fails because of a transient client error, unavailable service, or missing session, the uncaught exception propagates through the new reconciliation pass and aborts the parent message transform instead of preserving the empty result for a later retry.
| `<task id="${taskID}" state="completed">`, | ||
| `<summary>${summary}</summary>`, | ||
| '<task_result>', | ||
| text, |
There was a problem hiding this comment.
Embedded tags truncate recovered results
When recovered assistant text contains a literal </task_result>—for example in code or an audit report—this unescaped interpolation closes the wrapper early, and parseTaskResultFromOutput extracts only through that first closing tag, causing the remaining task result to be omitted.
…gate) Address Greptile/council P0-P1 on PR alvinunreal#912: - try/catch around extractSessionResult so child read failures never abort the parent messages transform - sanitize embedded </task_result|/task_error> in recovered body so non-greedy parseTaskResultFromOutput cannot truncate - skip rewrite while board job is still running or cancelled, avoiding mid-generation partial promotion into a completed part Tests cover throw fail-open, running-board skip, and embedded close-tag round-trip. Existing suite still green (28 pass).
…gate) Address Greptile/council P0-P1 on PR alvinunreal#912: - try/catch around extractSessionResult so child read failures never abort the parent messages transform - sanitize embedded </task_result|/task_error> in recovered body so non-greedy parseTaskResultFromOutput cannot truncate - skip rewrite while board job is still running or cancelled, avoiding mid-generation partial promotion into a completed part Tests cover throw fail-open, running-board skip, and embedded close-tag round-trip. Existing suite still green (28 pass).
Problem
When a foreground task's primary model halts on a non-retryable error (e.g. 403 quota exhausted), the orchestrator receives a completed task tool part with an empty
<task_result>, even though the fallback model continues on an orphan runLoop and produces the real result minutes later. The orchestrator reads the empty output, mis-judges the task as failed/empty, and self-amplifies by launching redundant replacement tasks (#863).Verified root cause (DB + opencode 1.18.5 source)
Complete chain, anchored in opencode core:
isRetryable:false→ opencodehalt(processor.ts:599) setsctx.assistantMessage.error, publishessession.error, sets statusidleprocessreturns"stop"(processor.ts:680:if (ctx.assistantMessage.error) return "stop")ops.promptreturns the last assistant message, which is empty (finish=undefined,parts=[]) because halt produced no contentsession.errorhandler triggerstryFallback(non-abort path) →promptAsyncre-prompts with model 2runTask(task.ts:213):result.parts.findLast(text)?.text ?? ""→ empty stringExit.succeed("")(background-job.ts:148) →status:"completed",output:""completedwith empty<task_result>— 12s after launchtryFallback'spromptAsynctakes effect only after model 1's runLoop ends → model 2 runs on an orphan runLoop, producing 9346 chars over 855s — but the parent part already says completed+emptyRoot cause is in opencode
runTask's?? ""fallback, which treats a halt-empty assistant message as success. omos cannot patch this as a plugin (no hook to interceptrunTasksettle). This is the false-complete counterpart to the false-cancel bug in PR #880.DB evidence (reproduced session)
completed+ empty output<task_result></task_result>Fix
In the
experimental.chat.messages.transformhook, afterstabilizeRunningTaskParts, scan task tool parts withstatus:"completed"whose parsed<task_result>is empty. For each, read the child session's real assistant text via the existingextractSessionResulthelper and in-place rewrite the part output withrenderTaskCompletedWithText(mirrors opencoderenderOutputtask.ts:64-76).Idempotent and non-blocking: if the child session has not yet produced non-empty text (fallback model still running),
extractSessionResultreturns empty and the part is left unchanged — the next transform turn re-evaluates naturally, so no explicit wait/poll is needed. The transform hook only mutates the in-memoryoutput.messages(not the persisted DB), so the rewrite is per-turn but the real output eventually lands in history once the child session completes and opencode writes it itself.Gated: only acts on
completedtask parts whose parsed result is empty. True completions with real text are preserved unchanged. Non-task tool parts are not touched.Tests
New
false-complete-fallback.test.ts(5 cases):Full suite: 1719 pass, 0 fail. Typecheck: pass.
Relation to other work
reconcile_tasktool (operation layer)error+cancelledparts rewritten from board recordAI assistance
Co-developed-with:
?? ""on halt-empty assistant), recommended this approach over idle-reconcile竞态 alternatives