fix(historian): defer child session cleanup - #337
Conversation
Keep successful historian children until the age-gated orphan sweep so OpenCode's detached writers cannot race session deletion. Extend the default prompt budget to ten minutes and surface empty Task completions instead of silently returning nothing.
There was a problem hiding this comment.
1 issue found across 15 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/plugin/src/hooks/magic-context/hook-handlers.ts">
<violation number="1" location="packages/plugin/src/hooks/magic-context/hook-handlers.ts:537">
P2: `annotateEmptyTaskOutput` appends the "completed without a final text response" message to any `task` output whose string is empty or whitespace, even when it is not a completed task wrapper. The empty-string branch (`trim().length === 0`) falls through past the `EMPTY_COMPLETED_TASK_RESULT` guard, so a task that failed, aborted, or never produced a `<task state="completed">` wrapper is still labeled as a completed-but-empty subagent. That contradicts the stated scope (surface completed `task_result` that is empty) and feeds the model a misleading annotation. Make the guard consistent with the completed-task regex, or handle the bare-empty case separately.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| // boundary. The queue helper re-checks the read-only mid-turn signal, | ||
| // so this is a no-op until the assistant is actually idle. | ||
| await flushIgnoredMessages(typedInput.sessionID); | ||
| annotateEmptyTaskOutput(typedInput.tool, output); |
There was a problem hiding this comment.
P2: annotateEmptyTaskOutput appends the "completed without a final text response" message to any task output whose string is empty or whitespace, even when it is not a completed task wrapper. The empty-string branch (trim().length === 0) falls through past the EMPTY_COMPLETED_TASK_RESULT guard, so a task that failed, aborted, or never produced a <task state="completed"> wrapper is still labeled as a completed-but-empty subagent. That contradicts the stated scope (surface completed task_result that is empty) and feeds the model a misleading annotation. Make the guard consistent with the completed-task regex, or handle the bare-empty case separately.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/plugin/src/hooks/magic-context/hook-handlers.ts, line 537:
<comment>`annotateEmptyTaskOutput` appends the "completed without a final text response" message to any `task` output whose string is empty or whitespace, even when it is not a completed task wrapper. The empty-string branch (`trim().length === 0`) falls through past the `EMPTY_COMPLETED_TASK_RESULT` guard, so a task that failed, aborted, or never produced a `<task state="completed">` wrapper is still labeled as a completed-but-empty subagent. That contradicts the stated scope (surface completed `task_result` that is empty) and feeds the model a misleading annotation. Make the guard consistent with the completed-task regex, or handle the bare-empty case separately.</comment>
<file context>
@@ -533,6 +534,7 @@ export function createToolExecuteAfterHook(args: {
// boundary. The queue helper re-checks the read-only mid-turn signal,
// so this is a no-op until the assistant is actually idle.
await flushIgnoredMessages(typedInput.sessionID);
+ annotateEmptyTaskOutput(typedInput.tool, output);
if (typedInput.tool === "ctx_reduce") {
</file context>
Summary
keep_subagentsis enabled<task_result>is emptyCloses #336.
Defensively addresses the silent empty-result symptom in #270.
Root cause
OpenCode prompt completion and
session.idleare not global session-writer barriers. Detached step/summary writes can persist after either event. Immediatesession.deletetherefore races foreign-key-backedpartinserts.Verification
Notes
The sweep retains a minimum safety factor of 3 after accounting for all configured prompt candidates and retry layers. Recent/live children are not eligible.
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.Summary by cubic
Defers historian child-session cleanup to an age-gated orphan sweep to avoid racing OpenCode’s detached writes and FK insert failures. Separates historian vs. privacy sweeps so privacy windows stay short while historian cleanup uses its own retry/fallback budget. Also raises the historian timeout and surfaces empty native Task completions for debugging.
keep_subagentsis true.historian_timeout_ms × MAX_HISTORIAN_PROMPT_ATTEMPTS × (primary+suggested) × fallback_count.resolveFallbackChain) and derives privacy timeouts before scheduling.historian_timeout_msis600000; exportsMAX_HISTORIAN_PROMPT_ATTEMPTS = 3.<task_result>using a sentinel via the tool-execute-after hook.historian_timeout_msto300000. Expect historian child sessions to persist until their sweep; privacy-only children retain their shorter sweep window.Closes #336. Defensively addresses the empty-result symptom in #270.
Written for commit 61ec04c. Summary will update on new commits.
Greptile Summary
This PR defers historian child-session deletion to an age-gated maintenance sweep, preventing deletion from racing detached OpenCode writes.
keep_subagents.<task_result>is empty.Confidence Score: 5/5
The PR appears safe to merge.
No blocking failure remains.
Important Files Changed
Sequence Diagram
sequenceDiagram participant H as Historian runner participant O as OpenCode session participant W as Detached writers participant T as Maintenance timer participant D as OpenCode DB H->>O: Create and prompt child session O-->>H: Prompt completes H-->>O: Preserve child session W->>D: Persist remaining step/summary parts T->>D: Find historian children older than stale threshold T->>O: Delete stale child unless keep_subagentsReviews (3): Last reviewed commit: "fix(historian): preserve child privacy w..." | Re-trigger Greptile
Context used: