Skip to content

fix(historian): defer child session cleanup - #337

Closed
coleleavitt wants to merge 3 commits into
cortexkit:masterfrom
coleleavitt:fix/historian-session-retire
Closed

fix(historian): defer child session cleanup#337
coleleavitt wants to merge 3 commits into
cortexkit:masterfrom
coleleavitt:fix/historian-session-retire

Conversation

@coleleavitt

@coleleavitt coleleavitt commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Summary

  • stop deleting successful historian children on the prompt-return path
  • retire stale historian children through the existing age-gated orphan sweep, including dreamer-disabled installs
  • budget the stale threshold for outer retries, model-suggestion retry, and configured fallback models
  • preserve historian children when keep_subagents is enabled
  • raise the default historian prompt timeout from 5 to 10 minutes
  • surface current OpenCode native Task wrappers whose completed <task_result> is empty

Closes #336.
Defensively addresses the silent empty-result symptom in #270.

Root cause

OpenCode prompt completion and session.idle are not global session-writer barriers. Detached step/summary writes can persist after either event. Immediate session.delete therefore races foreign-key-backed part inserts.

Verification

  • red: successful historian lifecycle observed immediate deletion without a quiescence barrier
  • green: 105 affected tests, 0 failures
  • plugin build and declaration emit pass
  • plugin typecheck passes
  • Biome format check passes
  • Oracle race review: SHIP

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.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with 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.

  • Keeps historian children after prompt completion; the orphan sweep removes them after a window that covers outer retries, model-suggestion attempts, and the normalized fallback chain. Excludes historian children from the sweep when keep_subagents is true.
  • Preserves privacy windows: sweeps dreamer/privacy titles and historian titles separately. Privacy stale = task timeout × 3; historian stale = historian_timeout_ms × MAX_HISTORIAN_PROMPT_ATTEMPTS × (primary+suggested) × fallback_count.
  • Runs the orphan sweep even when Dreamer is disabled. Registers historian timeout and normalized fallback count (via resolveFallbackChain) and derives privacy timeouts before scheduling.
  • Updates configuration, docs, and schema: default historian_timeout_ms is 600000; exports MAX_HISTORIAN_PROMPT_ATTEMPTS = 3.
  • Diagnostics: annotates completed native Task outputs with an empty <task_result> using a sentinel via the tool-execute-after hook.
  • Migration: if you relied on the old 5-minute default, set historian_timeout_ms to 300000. 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.

Review in cubic

Greptile Summary

This PR defers historian child-session deletion to an age-gated maintenance sweep, preventing deletion from racing detached OpenCode writes.

  • Separates historian and Dreamer privacy sweeps so each uses its own runtime-derived stale window.
  • Runs historian cleanup when Dreamer is disabled while honoring keep_subagents.
  • Raises the default historian timeout to ten minutes and synchronizes the public configuration surfaces.
  • Annotates completed native Task wrappers whose <task_result> is empty.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
packages/plugin/src/hooks/magic-context/compartment-runner-historian.ts Removes immediate successful-child deletion and retains historian sessions for deferred cleanup.
packages/plugin/src/features/magic-context/dreamer/retrospective-orphan-sweep.ts Adds historian title matching and a stale threshold covering retries, suggestion attempts, and fallback models.
packages/plugin/src/plugin/dream-timer.ts Runs separate Dreamer and historian orphan sweeps, including historian-only maintenance configurations.
packages/plugin/src/hooks/magic-context/empty-task-output.ts Annotates completed native Task wrappers when their task-result body contains no final text.
packages/plugin/src/config/schema/magic-context.ts Raises the historian timeout default and centralizes the maximum prompt-attempt count.
packages/plugin/src/index.ts Supplies normalized historian timeout, fallback count, and retention settings to timer registration.

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_subagents
Loading

Reviews (3): Last reviewed commit: "fix(historian): preserve child privacy w..." | Re-trigger Greptile

Context used:

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.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread packages/plugin/src/plugin/dream-timer.ts Outdated
Comment thread packages/plugin/src/index.ts Outdated
// 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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

Comment thread packages/plugin/src/hooks/magic-context/empty-task-output.ts Outdated
ualtinok added a commit that referenced this pull request Aug 20, 2026
…raction pieces named; #312 remains open — timeout arm is observability, not isolation)

Co-Authored-By: Alfonso <alfonso@cortexkit.io>
@alfonso-magic-context

Copy link
Copy Markdown
Collaborator

Thank you, Cole — you diagnosed the important part of #336 early: prompt completion is not a barrier for OpenCode's detached child writes, so deleting the historian child inline can race the FK-backed part insert. You filed this before we landed our independent work, and the overlap is real.

In parallel we merged a fix for #336 (edd6e06): it keeps the child and retires it through the age-gated orphan sweep, adds the late-part FK regression repro, composes the stale threshold from the full retry envelope plus a detached-writer grace, covers the dreamer-disabled sweep path, and honors keep_subagents.

Two pieces of your PR stand on their own and we're extracting them into focused follow-ups with credit: the empty-task-result visibility hook (makes a completed native Task with an empty result visible to the caller — useful for #270-class diagnostics), and the fallback-chain-aware timer registration (narrows an over-retention window). Closing this PR as superseded on the main arm — the two extractions will land crediting you as co-author.

ualtinok added a commit that referenced this pull request Aug 20, 2026
…gnostics)

Extracted from PR #337 (Cole Leavitt). A completed native task tool can
return an empty <task_result> when its provider emitted reasoning only or
context-fill truncation dropped the final output. Annotate such outputs
with a diagnostic sentinel from the tool-execute-after hook so the caller
can distinguish an empty result from a genuinely-empty tool.

Co-Authored-By: Cole Leavitt <cole@unwrap.rs>

Co-Authored-By: Alfonso <alfonso@cortexkit.io>
ualtinok added a commit that referenced this pull request Aug 20, 2026
Extracted from PR #337 (Cole Leavitt). The historian child-sweep retention
budget previously counted raw fallback_models entries, which over-counted
blank, duplicate, or malformed entries and could retain a child longer than
necessary. Derive the count from resolveFallbackChain(...).length so the
budget reflects the actual normalized fallback chain.

Co-Authored-By: Cole Leavitt <cole@unwrap.rs>

Co-Authored-By: Alfonso <alfonso@cortexkit.io>
ualtinok added a commit that referenced this pull request Aug 20, 2026
…th Cole Leavitt (empty-task-output visibility, fallback-chain timer retention, 30s test-timeout lock observability, opentui TDZ warmup)

Co-Authored-By: Alfonso <alfonso@cortexkit.io>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Historian child cleanup races late OpenCode part writes

2 participants