Skip to content

fix(native-chat): a chat you return to drops background tasks that finished while it was hidden - #24305

Merged
brennanb2025 merged 8 commits into
mainfrom
brennanb2025/native-chat-resumed-strip-drops-finished-tasks
Oct 6, 2026
Merged

brennanb2025 merged 8 commits into
mainfrom
brennanb2025/native-chat-resumed-strip-drops-finished-tasks

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 8 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​349 $\color{#cf222e}{\Huge{\mathbf{−}}}$​54 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​295
Prod 10 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​125 $\color{#cf222e}{\Huge{\mathbf{−}}}$​101 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​24

ELI5

If a chat was off screen when its background command finished, coming back to it still showed the command as running. The timer kept counting for hours, and Stop failed. Now the chat shows what is actually running when you come back.

What Changed

The bug (#24227). A Claude chat starts a background command (here a 10-minute CI watcher), then you switch to another tab or workspace. The command finishes while the chat is hidden. Later, the chat has been idle for 30 minutes, so Orca stops its Claude process. When you return, the strip above the composer still says 1 shell command — 2h 21m with a spinner. Clicking Stop shows "The background task wasn't stopped," because nothing is left to stop.

Why it happened. A hidden chat pane stops listening for updates, so it missed the host's "no tasks running" update. When the pane came back, the host reopened its subscription from where the pane had left off and sent no task list at all, because with Claude stopped it reported "I can't say" instead of "none." The client reads a missing task list as "unchanged," so it kept the stale "running" row forever. The host's own records (its journal and its child-task store) already had the task as done. Only the delivery to the returning pane lost it.

After this change. When you return to a chat, the strip shows exactly what the host's records say is running. A task that finished while you were away is gone.

Mechanism. The task list now travels in the frames the host sends each open chat pane (a frame is one update message), the way the slash-command list and the message queue already do:

  • The host reads the list from its child-task records for a pane's first frame (including when the pane resumes from a saved position) and for every snapshot, the frame that re-sends the pane the whole conversation from scratch (a "resync"). When those records change, it re-sends the list to each open pane whose last copy differs. Ordinary frames carrying new transcript text never read it, so streaming output costs nothing extra.
  • The host always gives an answer. A conversation with no running child is "none" (null), whether or not a Claude process is attached. Its records are the source of truth either way. The old "no live agent → say nothing" rule is gone, along with the session-wide "last sent" cache that remembered what it had sent but not who received it.
  • On the client, the first batch of a resumed subscription is applied on its own, never merged with later batches, and it replaces the task list. A pane reconnecting to an older remote host that still leaves the list out therefore drops its stale copy too. Older hosts leave the list out only when their records hold no running child, so on a resume, clearing matches what a fresh open of that host already shows.

Also fixed, same cause in the other direction (found by reading the code, not reproduced in the app). When the host re-sent a snapshot to a pane that was already open (after recovering from an interrupted rewind, or after the agent's process exited on its own), the frame carried no task list. The client treats a snapshot without one as "none," so a task that was still running vanished from the strip. The host's "already sent" cache then blocked a re-send until the task list changed again. Snapshots now carry the current list.

Why

The bug isn't specific to one path. Every live field a host publishes (commands, queue, activity) is stated explicitly when a subscription opens, except the task list, which the host could leave out. "Left out" meant "unchanged" to the client, so any gap where the client wasn't listening could leave it stuck. Tracking the list separately for each open pane, and putting it in every opening and resync frame, removes that gap for every way a pane can miss an update: hidden pane, dropped connection, host resync.

Alternatives considered:

  • Send null instead of nothing when the subscription opens (the approach in fix(native-chat): report empty background tasks on at-rest resume #24239). It fixes the reported path. It leaves the "already sent" cache shared by the whole session rather than tracked per subscriber, and it leaves snapshots without a list, so a running task can still disappear after a resync. It also doesn't help a new client talking to an older remote host.
  • Clear the list on the client whenever a pane hides. The strip would flicker empty on every tab switch until the host answered, and it still wouldn't fix the resync case.
  • Make Stop clear the row when the host doesn't know the task. That hides the symptom only after the user clicks, and the timer still runs until they do.

How this differs from recording task progress in the conversation log itself. The common pattern records each background task's start and finish in the conversation log, so a client resuming from its saved position replays the finish. This PR keeps the task list derived from the host's child-task records (the single store everything reads since #22614) and restates it at every subscription open and resync. That is intended: writing task lifecycle into the conversation log would create a second copy of facts the child-task store already owns. Restating the full list on open delivers the same guarantee, that a resuming client sees the current truth, without that second copy. The common pattern also explicitly marks a stopped agent's tasks finished; here the provider's own end handling settles the child records and the task list is derived from them, which is intended because a separate "mark finished on stop" step would be a second writer of facts those records already hold.

Linked Issue

Fixes #24227

Visual Proof

Not captured. Reproducing in the app needs a background task to finish while the chat is hidden, then 30 idle minutes before the agent is stopped. The issue's own reproduction, run through the real subscriber registry and the real client reducer, is now a regression test: a pane resuming from its cursor drops a task that ended while it was away (#24227) in structured-agent-session-subscriber-background-tasks.test.ts.

Testing

  • I manually tested these changes locally. Not done in the running app; see Visual Proof.
  • Automated tests added/updated

New tests, each confirmed to fail with its half of the fix removed:

  • Host, structured-agent-session-subscriber-background-tasks.test.ts:
    • the issue's repro: open, hide, the task ends, the agent is stopped, resume from the saved position, and the pane shows no tasks
    • a caught-up resume still carries the list
    • snapshot frames carry the current list
    • an ordinary transcript batch neither reads nor re-sends the list
    • a re-send goes only to subscribers whose copy differs
    • With the first-frame read removed, the repro and the caught-up tests fail.
  • Host, structured-agent-session-background-task-channel.test.ts: the channel answers "none" for a conversation no agent holds (it used to answer "unknown").
  • Client, structured-agent-session-reducer.test.ts: a resumed subscription's first batch that leaves the list out clears the held copy, and an ordinary batch keeps it.
  • Client, structured-agent-session-read-transport.test.ts: each subscription's first batch is applied alone and marked, and later batches are still coalesced.
  • With the client rule removed, both client tests fail.

Updated tests: the subscriber, command-publication and outbound-admission tests now use the per-subscriber re-send instead of the removed push method. structured-agent-session-child-work-selection.test.ts asserted the old "no agent → no answer" rule and now expects "none."

Runs (macOS):

  • 778 related test files: all native-chat, agent-session, structured-session, child-work and agent-status tests under src/main, src/renderer and src/shared, the Claude/Codex adapter tests that touch background tasks (real-CLI files excluded), and the end-to-end folder's read-owner unit test. 8039 passed.
  • tests/e2e/cross-version-wire/cross-version-agent-session-wire.unit.test.ts: 17/17.
  • pnpm tc:node and pnpm tc:web pass.
  • oxlint on the changed files: 0 findings.
  • check:code-quality:changed passes.
  • After merging current main and the review follow-ups: 62 related test files (544 tests, including the cross-version file above) pass, tc:node and tc:web pass, and lint is clean.

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

  • Mixed versions. The wire shape is unchanged. What the host sends changes: the task list now appears on subscription-open, snapshot and history frames where it used to be left out at rest.
    • An old client reads the new null as "no tasks," which already means hiding the strip, so its stale row clears too.
    • A new client against an old host is covered by the client rule above.
    • The existing per-client filter for readers that predate child views or row Stop applies to every frame type, the new ones included.
  • Mobile. Mobile has no background-task display and always subscribes without a saved position, so it is unaffected.
  • SSH/remote. The fix is in the execution host's delivery and the shared client reducer, so remote hosts behave the same once updated, and the client rule covers ones that aren't.
  • Performance. The list is read only on opening and snapshot frames and when child-task records change. It is never read per streamed token.
  • Not changed. Stop still reports failure for a task the host doesn't know. With the list now correct, the strip no longer shows such a row.

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. Not captured; reason under Visual Proof.
  • 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). Ran: changed-file lint, tc:node and tc:web, and 778 related test files. CI covers the full suite and build.

…nished while it was hidden

A background command that finished while its chat pane was hidden stayed in
the strip as running, its timer counting for hours, and Stop failed with "The
background task wasn't stopped." The pane stops listening while hidden, so it
missed the "no tasks" update; once the idle sweep stopped the agent, the host
reopened the pane's subscription without any roster at all, and the client
reads a missing roster as "unchanged".

The roster now rides each subscriber's frames the way the slash-command list
and the message queue already do. The subscriber registry reads it from the
host's child records on a subscriber's first frame and on every snapshot and
reset, and re-sends it to each subscriber whose last copy differs when the
records change. The channel always answers: a conversation with no running
child is "none" whether or not an agent holds it. Ordinary journal batches
never read it.

This also fixes the reverse case: a snapshot or reset sent to a live pane
carried no roster, which cleared a still-running task from the strip until the
roster next changed.

The client treats a resumed subscription's first batch as stating the roster,
so a pane reconnecting to an older host that still omits it drops its stale
copy too.

Fixes #24227
@greptile-apps

greptile-apps Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 4/5

[Medium risk] Fixes background task roster delivery when returning to a chat.

The PR is not yet safe to merge because a temporary child-work read failure can hide running tasks.

Findings

  1. P1 Failed reads hide running tasks ▶

Summary

The PR restates the host’s background-task roster on subscription opening and resynchronization, and clears stale client state on a resumed subscription’s first batch.

  • The latest change replaces an explicit reconcileLeases function type with the factory’s equivalent return type.
  • The previously reported failed-read behavior remains.

Reviews (3) · Last reviewed commit: "refactor(native-chat): keep structured-a..."

Comment on lines +93 to +95
const views = structuredRunningChildWork((session && this.readChildWork(sessionId)) ?? [])
if (!session || views.length === 0) {
return null

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Failed reads hide running tasks

If the child-work store read fails temporarily when a pane subscribes or resyncs, readChildWork returns undefined. This code treats that as an empty roster and sends backgroundTasks: null, hiding tasks that may still be running. Ordinary transcript batches do not read the roster again, so the strip can stay empty after the store recovers until another child-work change or resync. An unavailable read needs to remain distinct from a confirmed empty roster.

Knowledge Base Used: Agent workspaces and sessions

@coderabbitai

coderabbitai Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 26d1f0c2-9f0e-4cba-9cce-513ee8018b27
📥 Commits

Reviewing files that changed from the base of the PR and between 75446b8 and f662242.

📒 Files selected for processing (3)
  • src/main/native-chat/agent-session-wire/structured-agent-session-subscriber-background-tasks.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-subscribers.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-wire-admission.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 2 remain after this review.


📝 Walkthrough

Walkthrough

The background-task channel now provides roster state through a reader. Subscriber frames select and fingerprint rosters, and subscribers republish roster changes only when their saved fingerprint differs. The read transport marks the first batch of each subscription. The reducer clears a previously saved roster when that marked batch omits it.

Priority: ➖ Normal

Severity of issue fixed: Medium

Merge Risk: ⚪ Minimal · up to f6622

The supplied evidence identifies no issue that needs correction before merge. The reported checks remain subject to normal CI completion.

Security Architecture Review

Security architecture risk: 🔵 Low · up to f6622

The change refreshes task status when returning to a chat, without an identified expansion of access or command privileges. Remaining uncertainty concerns failure recovery rather than a demonstrated security flaw.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The inspected publication path is scoped to one session's roster and its registered subscribers. Compared with the base, the change alters delivery and omission semantics rather than adding execution authority or a new cross-session data source. This does not establish complete coverage of transport admission.

Trust Boundaries and Controls

  • observed — Roster stop-capability flags remain derived from the provider adapter, as in the base implementation. Subscription-opening semantics are computed locally, and callbacks from stale subscription generations are rejected before they can change renderer state.

Resilience and Maintainability Implications

  • observed — A frame-building or emission exception drops the affected subscriber without advancing its roster fingerprint. This contains a failed delivery to that subscriber rather than propagating the exception through the publication loop.

Hardening Proposals

  • proposed — Consider distinguishing an unavailable child-record read from a confirmed empty roster, preserving visibility of uncertain running work during storage failures. This is a resilience proposal, not a verified security finding.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 26.67% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 15 functions across 18 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the native-chat background-task issue and the main fix. It is specific and related, though somewhat long.
Description check ✅ Passed The description is detailed and covers the change, rationale, linked issue, testing, compatibility, and performance. Visual proof is not attached for this behavior change; the author explains why and …
Linked Issues check ✅ Passed The PR meets [#24227]. The host sends the current task roster on subscription-open and snapshot frames, and resends it to subscribers whose copy differs. The client clears a stale roster when the firs…
Out of Scope Changes check ✅ Passed The whole-PR changes support [#24227]. Host delivery, client resume handling, and their tests address stale task rows after a pane resumes. Snapshot handling covers the same delivery gap. No unrelated…
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🛠️ Fix failing CI checks 💡
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.

🧰 Additional context used
📚 Code guidelines (1)
AGENTS.md — auto-discovered

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ℹ️ No critical issues — one small cleanup inline.

Reviewed changes

  • Per-subscriber roster delivery. The background-task roster now rides each subscriber's frames: read from the host's child records on a subscriber's first frame and every snapshot/reset, deduplicated against that subscriber's last sent fingerprint, and re-sent on child-record changes (agent-session-subscriber-frame-fields.ts, structured-agent-session-subscribers.ts).
  • The host always answers. StructuredAgentSessionBackgroundTaskChannel.read returns null for "none" instead of undefined for "unknown", whether or not a provider holds the session; publish now delegates to republishBackgroundTasks and the session-wide published cache is gone.
  • Client clears a resumed roster. The reducer clears a held roster when the first batch of a subscription omits it, and the read transport applies that first batch alone and flags it opensSubscription, so a hidden pane that missed the host's "no tasks" update drops its stale row (new client against both new and old hosts).

Verified by reading the changed tests, the full agent-session-wire suite (148 files), tc:node, and tc:web. The client snapshot/reset paths replace state wholesale, so only batch frames need the marker; the host's roster attach covers snapshots/resets. No correctness issues found.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

…op the unused close observers

The host's delivery layer now requires every hook it is built with, so
production wiring cannot leave out the roster read (an opening frame
without it reads as "no tasks" to current clients). The conversations
map's close observers lost their only caller in this PR and are removed.
The reducer test comment states the old-host omission rule precisely.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ No new issues found. The prior review's only note is addressed and the delta is a clean tightening of the same design.

Reviewed changes

  • Made the subscriber-delivery callbacks required. StructuredAgentSessionClientDelivery now takes onJournalActivity, onAgentStarted, onChildWorkChanged, and readBackgroundTasks as required parameters, with the status feed and subscriber hooks wired unconditionally; the sole host call site already passed all four.
  • Removed the dead close-observer API. StructuredAgentSessionConversations.observeClose, its closeObservers set, and the loop in delete() are gone — the background-task channel was their only caller, and this PR removed that call, so the hook had nothing left to do.
  • Reworded a reducer-test comment to match the new "old host omits the roster only when its records hold no running child" rule.

The merge of main (551c0c3) contributed the injected logger, agentStart namespace import, wakeDelivery, and finishOwedStop changes in structured-agent-session-host.ts; those are base branch, not authored here.

Technical details
# Verification of the incremental cleanup

## Affected sites
- `src/main/native-chat/agent-session-wire/structured-agent-session-client-delivery.ts:36-41,63` — required params; only construction is `structured-agent-session-host.ts:77`, which passes all.
- `src/main/native-chat/agent-session-wire/structured-agent-session-conversations.ts:63-67` — `delete()` reduced to `activity.delete` + `super.delete`.

## Required outcome
- No call site omits a now-required arg, and nothing still references `observeClose`/`closeObservers`/`onDeliveryError`.

## Evidence
- `grep` over `src/` finds zero references to `observeClose`, `closeObservers`, or `onDeliveryError`; one constructor call site, fully supplied.
- 6 affected test files (37 tests) pass; `pnpm tc:node` passes.

Pullfrog  | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

…he line limit

Main brought the host file to the 300-line lint limit, and this PR's
roster wiring adds one line. Name the lease reconciler's type by its
factory, as the neighbouring fields do.
…hat-resumed-strip-drops-finished-tasks

# Conflicts:
#	src/main/native-chat/agent-session-wire/structured-agent-session-subscribers.test.ts
#	src/main/native-chat/agent-session-wire/structured-agent-session-subscribers.ts
…h claudeProviderHandle

Main made the provider handle opaque (#24991); build it with the existing
helper, as main's own tests now do.
@brennanb2025
brennanb2025 merged commit ee91720 into main Oct 6, 2026
26 of 28 checks passed
@brennanb2025
brennanb2025 deleted the brennanb2025/native-chat-resumed-strip-drops-finished-tasks branch October 25, 2026 22:22
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.

[Bug]: Native chat keeps a finished background task "running" forever after the pane was hidden; Stop fails

1 participant