Skip to content

fix: don't resurrect completed sub-agents from history rescan (v0.2.4)#2

Merged
psh4607 merged 1 commit intomainfrom
fix/no-historical-resurrect
May 8, 2026
Merged

fix: don't resurrect completed sub-agents from history rescan (v0.2.4)#2
psh4607 merged 1 commit intomainfrom
fix/no-historical-resurrect

Conversation

@psh4607
Copy link
Copy Markdown
Owner

@psh4607 psh4607 commented May 8, 2026

Summary

  • Bug: After plugin load or TUI restart, the sidebar briefly displays phantom rows like `explore Done 1230m 35s` / `librarian Done 1231m 29s` for sub-agents that ran in earlier turns, before retention prunes them.
  • Fix: `upsertToolPart` and `handleBackgroundStatusText` no longer resurrect entries when no in-flight tracking exists.
  • Bumped to v0.2.4.

Root cause

`scanSessionState` re-walks every message part of the session on every reactive tick (it's invoked from inside the panel's `insert(...)` accessor, which depends on `now()` and `version()`). It was designed to bootstrap state on plugin load.

For a background tool part already in `status: "completed"` — meaning the launch finished before this scan ran — we never observed the prior `pending` / `running` events. The old code did three things in sequence on rescan:

  1. `upsertToolPart` (status="completed", isBackground=true): no existing entry → unconditionally created one with `status: "running"` and `startedAt = part.state.time.start` (potentially hours old).
  2. On the next tick `handleBackgroundStatusText` matched the same message's `[BACKGROUND TASK COMPLETED]` / `[ALL BACKGROUND TASKS COMPLETE]` system reminder text.
  3. It called `completeEntry(entry, "completed", completedAt)` with `completedAt = Date.now()` (system reminders have no `time` field, so the fallback chain landed on the current time).

Result: `elapsed = Date.now() - startedAt` ≈ 1230 minutes. The v0.2.3 "stamp once" fix kept the row from getting worse, but the very first stamp itself was bogus on history rescans.

Why early-return is safe

Live launches deterministically pass through `pending` → `running` → `completed`. By the time a `status: "completed"` event is observed live, the entry is already in the `active` map (created by the `pending` / `running` branch a few hundred ms earlier). The only path that hits "`completed` with no existing entry" is a history rescan, where the entry has no business existing on the sidebar in the first place.

The same reasoning applies to `handleBackgroundStatusText`'s prior fallback that fabricated a row from an `[ALL BACKGROUND TASKS COMPLETE]` summary line alone.

Change

```diff
const bgKey = makeKey("background", bgID);
const promoted = active.get(bgKey);
if (promoted) return touchEntry(promoted, agent, description);
-active.set(bgKey, {

  • key: bgKey,
  • sessionID,
  • kind: "background",
  • agent,
  • description,
  • status: "running",
  • startedAt,
  • bgID,
  • callID,
    -});
    -return true;
    +// Live launches always pass through pending/running before reaching
    +// completed, so observing status="completed" with no in-flight entry
    +// means scanSessionState is replaying a historical message. Resurrecting
    +// here would stamp startedAt to part.time.start (potentially hours old)
    +// and let handleBackgroundStatusText finalize completedAt = Date.now()
    +// on the next tick, producing a spurious "Done 1230m" row before
    +// retention prunes it.
    +return false;
    ```

Verification

  • `bun run typecheck` ✅
  • `bun run build` ✅ (dist/ regenerated)
  • LSP diagnostics clean on `src/tui.ts`
  • Foreground completed branch already had the same `if (!existing) return false` guard, so no symmetric change needed there.

Files changed

  • `src/tui.ts` — fix + version bump
  • `package.json` — 0.2.3 → 0.2.4
  • `dist/tui.js`, `dist/tui.js.map`, `dist/tui.d.ts.map` — regenerated build output

Related

scanSessionState re-walks every message part on every reactive tick.
For background tool parts already in status='completed' (i.e. the launch
already finished), we never observed the prior pending/running events
because either the plugin wasn't loaded yet or this is a TUI restart.
The previous code unconditionally set up an entry with status='running'
and startedAt = part.time.start (potentially hours old). On the next
tick handleBackgroundStatusText would match the same message's
[BACKGROUND TASK COMPLETED] / [ALL BACKGROUND TASKS COMPLETE] system
reminder text and call completeEntry with completedAt = Date.now()
(system reminders carry no time field). Result: a phantom 'Done 1230m'
row that lingered for retention before being pruned.

v0.2.3 froze re-stamping after the first stamp, but the very first
stamp itself was bogus on rescans. This patch closes that hole:

- upsertToolPart's background-completed branch now early-returns when
  no in-flight entry exists. Live launches always pass through
  pending/running first, so missing those states means historical
  replay; resurrecting is wrong by construction.
- handleBackgroundStatusText's [ALL BACKGROUND TASKS COMPLETE] new-entry
  fallback is removed for the same reason.
Copilot AI review requested due to automatic review settings May 8, 2026 07:12
@psh4607 psh4607 merged commit 001e998 into main May 8, 2026
3 checks passed
@psh4607 psh4607 deleted the fix/no-historical-resurrect branch May 8, 2026 07:13
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes a sidebar UI bug where completed background sub-agents could be temporarily “resurrected” during session history rescans (e.g., after plugin load / TUI restart), causing phantom “Done 1230m” rows until retention pruning removed them.

Changes:

  • Prevent upsertToolPart from creating a new background entry when it observes status="completed" without any in-flight tracking entry (treat as historical replay).
  • Prevent handleBackgroundStatusText from fabricating completed background entries from summary reminder text when no in-flight entry exists.
  • Bump plugin/package version to v0.2.4 and regenerate dist/ outputs.

Reviewed changes

Copilot reviewed 2 out of 5 changed files in this pull request and generated no comments.

Show a summary per file
File Description
src/tui.ts Adds guards to avoid resurrecting historical completed background entries during rescans; bumps plugin version constant.
package.json Version bump to 0.2.4.
dist/tui.js Regenerated build reflecting the rescans/guard changes and version bump.
dist/tui.js.map Regenerated sourcemap output.
dist/tui.d.ts.map Regenerated types sourcemap output.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

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.

2 participants