Skip to content

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

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

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

1 participant