Skip to content

fix(agent-status): keep restored spinner titles from reading as working - #43

Merged
nplez1 merged 2 commits into
nplez1/mainfrom
nplez1/Restored-Title-Working
Oct 1, 2026
Merged

nplez1 merged 2 commits into
nplez1/mainfrom
nplez1/Restored-Title-Working

Conversation

@nplez1

@nplez1 nplez1 commented Sep 29, 2026 •

Copy link
Copy Markdown
Owner
Files Added Deleted Net
Test 4 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​211 $\color{#cf222e}{\Huge{\mathbf{−}}}$​10 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​201
Prod 17 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​934 $\color{#cf222e}{\Huge{\mathbf{−}}}$​824 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​110

ELI5

After restarting the app, Orca could show a "working" spinner on worktrees whose agents were actually all done, then quietly correct itself a minute or two later. The app was mistaking an old saved terminal title — one caught mid-task by an automatic save — for proof that work was happening right now. Now Orca remembers where a title came from: a title restored from saved state can name the pane, but only titles observed live can claim the agent is working.

What Changed

Before: relaunching the app while every agent was idle could flip the sidebar worktree row, the worktree card, and the tab bar to "working" at the same time, for roughly 30 seconds to 2 minutes, then silently fall back to idle.

After: a restored title never reads as "working". As soon as a real title frame arrives from the terminal, the indicator shows exactly what the agent is doing again.

Mechanism: the terminal daemon checkpoints each session, including its last terminal title. Checkpoints are taken on a timer, so one can catch a mid-task spinner frame (real values from this machine: ⠧ π - Jenkins-Details, ⠧ π - Remove-Team, ⠴ π - orca). On relaunch that saved title was replayed to the UI as fresh state, and title normalization only standardizes spinner frames — it never removes them — so the restored title classified as "working" in every reader at once. We now track provenance in the title tracker: a title seeded from saved state is marked "restored, unconfirmed"; the first title observed live (real bytes or a hook frame) confirms it. The replayed title fact carries that stamp — mirroring the existing restoredUnconfirmed stamp on restored agent-status rows — and the renderer fact handler, the single point every reader's titles flow through, strips working indicators from an unconfirmed working claim before any UI can see it. Permission titles (π !) keep signalling, the OMP static state marker is rewritten through its own protocol table (π : → π >), and observed titles (park/unpark, worktree-switch replays) pass through untouched.

Why

Alternatives considered:

  1. Clear working indicators only when the replay falls back to the record title (the path with no live tracker). Verified not to fix the bug: the restore path seeds the tracker, so the restored title is served from the tracker branch and that fallback never fires — confirmed with a runtime probe before choosing the route.
  2. Blanket-clear every replayed title. Wrong: park/unpark and worktree-switch replays of observed titles are trusted evidence, and clearing them would hide genuinely working agents.

Provenance is the trust boundary the codebase already uses elsewhere: restored agent-status rows are stamped restoredUnconfirmed (never fresh until a live update) and seeded status is marked lastAgentStatusObservedLive: false. Accepted tradeoff (also in the commit body): an agent genuinely mid-turn across a relaunch keeps its indicator quiet until its next live title frame — the same treatment restored hook rows already receive.

Linked Issue

N/A — internal fix, no pre-existing issue.

Visual Proof

N/A — the change removes a transient wrong status after app relaunch (see Testing for the repro shape); no layout, styling, or interaction design change to capture.

Testing

  • I manually tested these changes locally
  • Automated tests added/updated, or explained why not below

Regression tests added where the harness runs them:

  • title-tracker provenance transitions (seeded → confirmed on first live frame; seeds never override observed titles),
  • the unit pairing of normalizeTerminalTitle / detectAgentStatusFromTitle / clearWorkingIndicators on the three real checkpoint titles (normalized form still classifies working — the bug's root gap — cleared form reads idle), plus OMP marker rewrite and permission-marker preservation,
  • the renderer fact handler: a restored spinner title no longer reads as working, observed replays pass through verbatim, restored permission markers keep signalling,
  • the runtime snapshot: restore-seeded titles are stamped unconfirmed, observed titles are not.

Repro before the fix requires an app relaunch with a mid-turn checkpoint title, so no manual run was performed in this branch (the app was intentionally not relaunched). Verified by full test sweep of src/main/runtime, src/shared, and src/renderer/src/components/terminal-pane (23,369 tests passing), pnpm tc, oxlint, oxfmt, and pnpm run check:code-quality:changed. Platform impact: the fix is host-agnostic — local-daemon and SSH PTYs share the same title tracker and fact handler, remote clients run the same handler code; the checkpoint wire format is unchanged.

AI Disclosure

AI-assisted (pi coding agent).

Review

Agent skill upstream boundary

  • Not applicable, or this change follows docs/reference/agent-skill-sharing-upstream-boundary.md and copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

  • Cross-platform: no paths, shortcuts, or platform-specific behavior touched.
  • SSH/remote: shared tracker + fact handler covers local-daemon and SSH hosts; remote clients run the same reader code. Verdict/status vocabulary untouched.
  • Mobile: mobile/src performs no title-activity classification (status comes from hook rows, which keep their own restoredUnconfirmed gate), so no impact.
  • Backwards compatibility: the fact channel only gains an optional field; old clients ignore it. Checkpoint files unchanged.
  • Security / performance: no new attack surface; the treatment runs only on stamped facts and does two memoized classifications at most.

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

Before: a cold restore handed checkpoint.json's lastTitle — written on
checkpoint ticks, so it can capture a mid-turn spinner frame (real values
on this machine: "⠴ π - orca", "⠧ π - Jenkins-Details",
"⠧ π - Remove-Team") — into the PTY record and title tracker via
seedTerminalRestoreTail -> applySeededAgentStatus. The (re)attach
snapshot in getTerminalSideEffectSnapshot replayed that title to every
surface, and because normalizeTerminalTitle canonicalizes braille frames
but never strips them, the restored title classified as working through
detectAgentStatusFromTitle. The sidebar worktree row, the worktree card,
and the tab bar all flipped to working together and only self-corrected
via the 30s cold-park delays and the 60s watchdog heal.

After: the title tracker records provenance — a seeded title is restored
state this process never observed on the wire, and the first live frame
(including synthetic hook frames) confirms it. The replay snapshot stamps
such facts restoredUnconfirmed (mirroring the agent-status hydration
stamp), and the renderer fact handler — the single choke point every
reader's titles flow through — strips working indicators from an
unconfirmed fact that asserts working, feeding readers a title that can
never claim working until a live frame re-asserts it. Both title fields
are cleared so the completion coordinator cannot arm on the restored raw
title and later mint a false task-complete. Permission claims ("π !")
keep signalling, the OMP static marker is rewritten through its protocol
table ("π :" -> "π >"), and observed titles — park/unpark and
worktree-switch replays — pass through untouched. The treatment is host
agnostic: local-daemon and SSH PTYs share the tracker and fact handler,
and remote clients run the same handler.

Route: clearing only the record-title fallback (where normalizedTitle is
null) would not have fixed the bug. The restore seed populates the
tracker, so the working title is served from the non-null tracker branch
(a runtime probe returned "⠋ π - Jenkins-Details" with a tracker entry
present). Since normalizedTitle being null does not identify a cold
restore, the fix takes the provenance route: explicit restoredUnconfirmed
state plus reader-side treatment, never a blanket clear on replay.

Accepted, as agreed: an agent genuinely mid-turn across a relaunch keeps
its indicator quiet until the next live frame re-asserts it — the same
treatment restored hook rows already receive (restoredUnconfirmed state
is never fresh). The same applies to the residual same-run reclaim whose
seed-only title asserts working (e.g. an OMP static working marker that
never repaints): quiet until its next title change. A stale working title
on a PTY that keeps producing output is still healed by main's 3s
stale-title timer exactly as before.

The checkpoint wire format is unchanged; the fact channel only gains an
optional field.
@nplez1
nplez1 force-pushed the nplez1/Restored-Title-Working branch from e4df419 to d22f94e Compare October 1, 2026 03:42
Before: `pnpm exec oxlint` fails with six max-lines errors, so static
analysis is red on nplez1/main and on every branch cut from it. The
upstream sync re-seated this fork's rate-limits, status-bar, sidebar and
identity patches onto upstream files that had grown in the meantime, and
the combined results crossed the 300/400-line caps.

After: the linter is clean again. Per repo rules there is no max-lines
disable and no cap bump anywhere — each over-cap file yields one cohesive
unit to a domain-named sibling module, and the behavior is unchanged:

- service-full-cycle-preparation.ts: the copilot stored-token-vs-gh
  credential selection moves to service-copilot-cycle-credentials.ts;
  the three repeated settled-result promise adapters move (as
  trackSettledProviderResult) onto service-sibling-provider-result.ts
- service-full-cycle-application.ts: the nine settled-result to
  error-snapshot fallback blocks collapse into providerResultSnapshot in
  service-provider-result-snapshot.ts, shapes preserved exactly,
  including opencode-go's explicit monthly slot
- tooltip.tsx: the rate-limit window unit (window derivation, usage
  urgency colors, per-window bar) moves to
  provider-rate-limit-window-sections.tsx
- use-status-bar-controller.ts: the usage visibility projection moves to
  status-bar-usage-projection.ts
- tui-agent-config.ts: the per-agent authoring table moves to
  tui-agent-config-table.ts
- worktree-activation.ts: the sidebar reveal group moves to
  worktree-activation-sidebar-reveal.ts

Pure refactor: no behavior changes, no test edits.
@nplez1
nplez1 merged commit ab0c9d9 into nplez1/main Oct 1, 2026
31 checks passed
nplez1 added a commit that referenced this pull request Oct 1, 2026
…ng (#43)

* fix(agent-status): keep restored spinner titles from reading as working

Before: a cold restore handed checkpoint.json's lastTitle — written on
checkpoint ticks, so it can capture a mid-turn spinner frame (real values
on this machine: "⠴ π - orca", "⠧ π - Jenkins-Details",
"⠧ π - Remove-Team") — into the PTY record and title tracker via
seedTerminalRestoreTail -> applySeededAgentStatus. The (re)attach
snapshot in getTerminalSideEffectSnapshot replayed that title to every
surface, and because normalizeTerminalTitle canonicalizes braille frames
but never strips them, the restored title classified as working through
detectAgentStatusFromTitle. The sidebar worktree row, the worktree card,
and the tab bar all flipped to working together and only self-corrected
via the 30s cold-park delays and the 60s watchdog heal.

After: the title tracker records provenance — a seeded title is restored
state this process never observed on the wire, and the first live frame
(including synthetic hook frames) confirms it. The replay snapshot stamps
such facts restoredUnconfirmed (mirroring the agent-status hydration
stamp), and the renderer fact handler — the single choke point every
reader's titles flow through — strips working indicators from an
unconfirmed fact that asserts working, feeding readers a title that can
never claim working until a live frame re-asserts it. Both title fields
are cleared so the completion coordinator cannot arm on the restored raw
title and later mint a false task-complete. Permission claims ("π !")
keep signalling, the OMP static marker is rewritten through its protocol
table ("π :" -> "π >"), and observed titles — park/unpark and
worktree-switch replays — pass through untouched. The treatment is host
agnostic: local-daemon and SSH PTYs share the tracker and fact handler,
and remote clients run the same handler.

Route: clearing only the record-title fallback (where normalizedTitle is
null) would not have fixed the bug. The restore seed populates the
tracker, so the working title is served from the non-null tracker branch
(a runtime probe returned "⠋ π - Jenkins-Details" with a tracker entry
present). Since normalizedTitle being null does not identify a cold
restore, the fix takes the provenance route: explicit restoredUnconfirmed
state plus reader-side treatment, never a blanket clear on replay.

Accepted, as agreed: an agent genuinely mid-turn across a relaunch keeps
its indicator quiet until the next live frame re-asserts it — the same
treatment restored hook rows already receive (restoredUnconfirmed state
is never fresh). The same applies to the residual same-run reclaim whose
seed-only title asserts working (e.g. an OMP static working marker that
never repaints): quiet until its next title change. A stale working title
on a PTY that keeps producing output is still healed by main's 3s
stale-title timer exactly as before.

The checkpoint wire format is unchanged; the fact channel only gains an
optional field.

* fix(lint): split the six files the sync left over the max-lines cap

Before: `pnpm exec oxlint` fails with six max-lines errors, so static
analysis is red on nplez1/main and on every branch cut from it. The
upstream sync re-seated this fork's rate-limits, status-bar, sidebar and
identity patches onto upstream files that had grown in the meantime, and
the combined results crossed the 300/400-line caps.

After: the linter is clean again. Per repo rules there is no max-lines
disable and no cap bump anywhere — each over-cap file yields one cohesive
unit to a domain-named sibling module, and the behavior is unchanged:

- service-full-cycle-preparation.ts: the copilot stored-token-vs-gh
  credential selection moves to service-copilot-cycle-credentials.ts;
  the three repeated settled-result promise adapters move (as
  trackSettledProviderResult) onto service-sibling-provider-result.ts
- service-full-cycle-application.ts: the nine settled-result to
  error-snapshot fallback blocks collapse into providerResultSnapshot in
  service-provider-result-snapshot.ts, shapes preserved exactly,
  including opencode-go's explicit monthly slot
- tooltip.tsx: the rate-limit window unit (window derivation, usage
  urgency colors, per-window bar) moves to
  provider-rate-limit-window-sections.tsx
- use-status-bar-controller.ts: the usage visibility projection moves to
  status-bar-usage-projection.ts
- tui-agent-config.ts: the per-agent authoring table moves to
  tui-agent-config-table.ts
- worktree-activation.ts: the sidebar reveal group moves to
  worktree-activation-sidebar-reveal.ts

Pure refactor: no behavior changes, no test edits.
nplez1 added a commit that referenced this pull request Oct 6, 2026
…ng (#43)

* fix(agent-status): keep restored spinner titles from reading as working

Before: a cold restore handed checkpoint.json's lastTitle — written on
checkpoint ticks, so it can capture a mid-turn spinner frame (real values
on this machine: "⠴ π - orca", "⠧ π - Jenkins-Details",
"⠧ π - Remove-Team") — into the PTY record and title tracker via
seedTerminalRestoreTail -> applySeededAgentStatus. The (re)attach
snapshot in getTerminalSideEffectSnapshot replayed that title to every
surface, and because normalizeTerminalTitle canonicalizes braille frames
but never strips them, the restored title classified as working through
detectAgentStatusFromTitle. The sidebar worktree row, the worktree card,
and the tab bar all flipped to working together and only self-corrected
via the 30s cold-park delays and the 60s watchdog heal.

After: the title tracker records provenance — a seeded title is restored
state this process never observed on the wire, and the first live frame
(including synthetic hook frames) confirms it. The replay snapshot stamps
such facts restoredUnconfirmed (mirroring the agent-status hydration
stamp), and the renderer fact handler — the single choke point every
reader's titles flow through — strips working indicators from an
unconfirmed fact that asserts working, feeding readers a title that can
never claim working until a live frame re-asserts it. Both title fields
are cleared so the completion coordinator cannot arm on the restored raw
title and later mint a false task-complete. Permission claims ("π !")
keep signalling, the OMP static marker is rewritten through its protocol
table ("π :" -> "π >"), and observed titles — park/unpark and
worktree-switch replays — pass through untouched. The treatment is host
agnostic: local-daemon and SSH PTYs share the tracker and fact handler,
and remote clients run the same handler.

Route: clearing only the record-title fallback (where normalizedTitle is
null) would not have fixed the bug. The restore seed populates the
tracker, so the working title is served from the non-null tracker branch
(a runtime probe returned "⠋ π - Jenkins-Details" with a tracker entry
present). Since normalizedTitle being null does not identify a cold
restore, the fix takes the provenance route: explicit restoredUnconfirmed
state plus reader-side treatment, never a blanket clear on replay.

Accepted, as agreed: an agent genuinely mid-turn across a relaunch keeps
its indicator quiet until the next live frame re-asserts it — the same
treatment restored hook rows already receive (restoredUnconfirmed state
is never fresh). The same applies to the residual same-run reclaim whose
seed-only title asserts working (e.g. an OMP static working marker that
never repaints): quiet until its next title change. A stale working title
on a PTY that keeps producing output is still healed by main's 3s
stale-title timer exactly as before.

The checkpoint wire format is unchanged; the fact channel only gains an
optional field.

* fix(lint): split the six files the sync left over the max-lines cap

Before: `pnpm exec oxlint` fails with six max-lines errors, so static
analysis is red on nplez1/main and on every branch cut from it. The
upstream sync re-seated this fork's rate-limits, status-bar, sidebar and
identity patches onto upstream files that had grown in the meantime, and
the combined results crossed the 300/400-line caps.

After: the linter is clean again. Per repo rules there is no max-lines
disable and no cap bump anywhere — each over-cap file yields one cohesive
unit to a domain-named sibling module, and the behavior is unchanged:

- service-full-cycle-preparation.ts: the copilot stored-token-vs-gh
  credential selection moves to service-copilot-cycle-credentials.ts;
  the three repeated settled-result promise adapters move (as
  trackSettledProviderResult) onto service-sibling-provider-result.ts
- service-full-cycle-application.ts: the nine settled-result to
  error-snapshot fallback blocks collapse into providerResultSnapshot in
  service-provider-result-snapshot.ts, shapes preserved exactly,
  including opencode-go's explicit monthly slot
- tooltip.tsx: the rate-limit window unit (window derivation, usage
  urgency colors, per-window bar) moves to
  provider-rate-limit-window-sections.tsx
- use-status-bar-controller.ts: the usage visibility projection moves to
  status-bar-usage-projection.ts
- tui-agent-config.ts: the per-agent authoring table moves to
  tui-agent-config-table.ts
- worktree-activation.ts: the sidebar reveal group moves to
  worktree-activation-sidebar-reveal.ts

Pure refactor: no behavior changes, no test edits.
nplez1 pushed a commit that referenced this pull request Oct 19, 2026
…iew off the deprecated owner resolver (stablyai#30562)

* refactor(renderer): fold browser creation, file actions and doc-preview callers off the deprecated owner resolver

The creation-action policy and browser creation (#3, #48) fold together, so the provider the
policy offers is the one creation uses. Browser panes, page lifecycle and conversion, tab
duplication, the explorer tree pane, doc-preview actions and editor path moves (#21, #43, #45-#47,
#49, #50) read runtimeEnvironmentIdWithLegacyFallbackUnlessTwin. File-explorer writes on a folder
(#51) take the owner from the rows alone, never from the focused server. Deprecated importers
16 -> 6.

* refactor(renderer): fold file preview off the deprecated owner resolver; Open in Orca Browser refuses a twin

A project two hosts publish reads as no server for the doc-preview plan, but the file:// path now
refuses it with its own message instead of opening this machine's copy (#7). Deprecated importers
6 -> 5.

* fix(renderer): a twin's file preview and open-externally refuse instead of opening this machine's copy

Codex P2 on the F7 folds: a folder twin read as no server, so the preview plan built a local
file:// tab and open-externally handed the path to this machine's OS. Both now check the owner
for ambiguity first. Also fix two test fixtures that broke the renderer typecheck.

* refactor(renderer): move the file:// open target out of file-preview.ts

file-preview.ts crossed its line limit after main grew it; the file:// target, its type and its two
refusal messages move to workspace-file-browser-open-target.ts, which they never depended on
file-preview for.

* test: mock the replacement resolver in the quick-action availability suite (Codex P2)
@nplez1
nplez1 deleted the nplez1/Restored-Title-Working branch October 24, 2026 03:23
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.

1 participant