Skip to content

fix(native-chat): report empty background tasks on at-rest resume - #24239

Closed
ggbdpq wants to merge 2 commits into
stablyai:mainfrom
ggbdpq:fix/background-tasks-resume-null
Closed

ggbdpq wants to merge 2 commits into
stablyai:mainfrom
ggbdpq:fix/background-tasks-resume-null

Conversation

@ggbdpq

@ggbdpq ggbdpq commented Sep 30, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #24227. A finished background task stayed on the native-chat strip as "running" forever (timer still counting, Stop failing) when its chat pane was hidden while the task ended.

Root cause (all five steps verified against main):

  1. A hidden pane stops listening: use-structured-agent-session-read.ts gates owner.activate() on isVisible, so the task-end publish (backgroundTasks: null) reached zero subscribers.
  2. The idle sweep then stopped the provider child (STRUCTURED_AGENT_SESSION_IDLE_MS), after which adapter.backgroundTaskState answers undefined rather than null (claude-structured-session-adapter.ts: session ? backgroundTaskState(session) : undefined).
  3. Returning to the pane resumes from the saved cursor, and the subscribe catch-up omits the field when the value is undefined.
  4. The reducer treats a missing field as "no change", so the stale monitoring state survived every resume.
  5. Stop cannot help: no live process knows the task id anymore, and the failure leaves the row on screen.

Fix: StructuredAgentSessionBackgroundTaskChannel.subscribe and .history fold undefined into null before opening the subscriber / answering a history read. A conversation opened at rest has no live provider child, so it has no live background tasks - a resuming renderer receives the empty state and clears its copy. A live child still passes its real state through untouched (AgentSessionSubscribers already forwards explicit null). publish keeps its edge-driven semantics. The renderer backstop and Stop-settling variants from the issue are intentionally left alone: those only mask what the channel now reports correctly.

Verification

Check Result
New regression cases before the fix 2 failed (expected undefined to be null - subscribe resume + history read), 2 lock-in cases green
New regression cases after the fix 4/4 green (rebuilt after formatting)
Adjacent suites (channel, host x3, subscribers, reducer, renderer read owner) 85/85 green
tsc --noEmit -p config/tsconfig.tc.web.json 0 errors
oxlint (touched files) 0 warnings
oxfmt --check (touched files) clean
max-lines ratchet OK, no new bypasses

The two red-before cases pin the exact defect: a resume after the provider child is gone must deliver backgroundTasks: null, not omit the field.

AI use

Implemented by GLM-5.3-Flash (ZCode) from the issue's root-cause analysis; root-cause chain re-verified line-by-line in this codebase before writing the fix, and the fix direction (main-side empty-state reporting) follows the issue's option 1.

Checklist

  • Tests added/updated for the behavior change (red before, green after)
  • No UI/STYLEGUIDE-visible changes (wire semantics only)
  • No new dependencies

A finished background task stayed on the native-chat strip as "running"
forever when its pane was hidden while the task ended: the idle sweep
then stopped the provider child, so the adapter answered `undefined` for
`backgroundTaskState`. The subscribe/history path omitted the field on
`undefined`, and the renderer reducer reads a missing field as "no
change" - the stale monitoring state survived every resume, and Stop
failed against a process that no longer knew the task id.

The channel now folds `undefined` into `null` for subscribe and history
reads: a conversation opened at rest has no live provider child, so it
has no live background tasks. A resuming renderer receives the empty
state and clears its copy; a live child still passes its real state
through untouched. `publish` keeps its own edge-driven semantics.

Fixes stablyai#24227

Generated-by: GLM-5.3-Flash (ZCode)
@greptile-apps

greptile-apps Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

[Medium risk] Fixes background task state reporting on session resume.

The PR appears safe to merge, though the previously requested end-to-end stale-task clearing assertion remains absent.

Findings

  1. P2 Stale task clearing remains untested ▶

Summary

The PR makes at-rest history and subscription reads report backgroundTasks: null when no provider child is visible, and adds channel and reducer regression tests. The reducer test partially addresses the earlier test-coverage finding, but it constructs a null batch separately rather than applying the batch emitted by the channel to a renderer state containing a monitoring task.

  • Live background-task state remains unchanged, and publication semantics are not changed.
Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[Provider child unavailable] --> B[Channel reads undefined]
  B --> C[History or subscribe reports null]
  C --> D[Renderer reducer clears task roster]
Loading

Reviews (2) · Last reviewed commit: "test(native-chat): fix channel fixture t..."

close()

expect(batch?.type).toBe('batch')
expect(batch?.backgroundTasks).toBe(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.

P2 Stale task clearing remains untested The resume test confirms that the host emits backgroundTasks: null, but it never applies that batch to renderer state that already contains a monitoring task. That is the reported failure path, so a future change could leave the task on the strip while these tests still pass. Please assert that the emitted batch clears an existing task.

Knowledge Base Used: Agent workspaces and sessions

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fair gap - the channel tests only pin the host's half of the contract. Added the renderer half in src/shared/structured-agent-session-reducer.test.ts ("reads an omitted batch field as no change and an explicit null as the empty roster"): a state already holding a monitoring roster survives a batch that omits the field (identity no-op), and the same state clears its roster - backgroundTasks becomes null with transcript items untouched - when a batch carries the explicit empty state. Verified the new test bites by mutating the reducer's undefined check into a nullish coalesce (only the new test goes red), then reverting. Layered coverage is now: channel tests assert the host folds at-rest undefined into null on subscribe/history; the reducer test asserts the renderer actually clears on that null batch.

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

Important

The fix itself is correct, but pnpm tc:node fails on the new test file (5 TS errors). The PR's verification table only cites tc:web, which does not include src/main, so this went unnoticed. pnpm tc typechecks the node project (tsconfig.node.json includes src/main/**/*), so this blocks CI. Details inline.

Reviewed changes

  • Empty-state fold in the background-task channel — history and subscribe now coerce an undefined adapter answer to null, so an at-rest conversation reports an explicit empty background-task state instead of omitting the field (structured-agent-session-background-task-channel.ts:45 and :71). publish keeps its edge-driven semantics.
  • Regression coverage — a new structured-agent-session-background-task-channel.test.ts pins resume-after-child-gone (null), live-state passthrough, explicit adapter null passthrough, and the history-read empty state.

The implementation is sound: a conversation opened at rest has no live provider child, so null is the correct empty state, and a live owner returns a real state or null and passes through untouched. The tests pass at runtime (4/4) and the two red-before cases genuinely pin the defect — before the fold, the subscribe batch omitted the field entirely, so toBe(null) fails on undefined. No behavioral issue found in the implementation.

ℹ️ Nitpicks

  • After ?? null, backgroundTasks can no longer be undefined, so the conditional spread ...(backgroundTasks !== undefined ? { backgroundTasks } : {}) is always true at structured-agent-session-background-task-channel.ts:58 and :76. Leaving it signals an omit-path that no longer exists; the field can be spread directly.

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

@coderabbitai

coderabbitai Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Note

Repository guideline files applied to this review (1)
AGENTS.md — auto-discovered

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: 6b734b50-465b-42a0-bd1a-3632a39b285e

📥 Commits

Reviewing files that changed from the base of the PR and between f2d3e08 and f438525.

📒 Files selected for processing (2)
  • src/main/native-chat/agent-session-wire/structured-agent-session-background-task-channel.test.ts
  • src/shared/structured-agent-session-reducer.test.ts

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


📝 Walkthrough

Walkthrough

The background-task channel now includes backgroundTasks: null in history and subscription payloads when the adapter returns undefined. Tests cover both payload paths and verify that explicit null and a live monitoring state remain unchanged. Reducer tests verify that an omitted field preserves existing state and explicit null clears the roster.

Priority: ➖ Normal

Severity of issue fixed: Medium

Merge Risk: ⚪ Minimal · up to f4385

A resumed session whose provider is no longer live now clears stale background-task state. Live state and explicit null pass through unchanged, and the change is covered by channel and reducer tests. No merge-blocking risk remains.

Security Architecture Review

Security architecture risk: 🔵 Low · up to f4385

The change is narrowly scoped and preserves reported live tasks. No new security exposure was demonstrated, but the distinction between absent tasks and unavailable task information is not fully established across failure and recovery paths.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The demonstrated change affects per-session task metadata presented through history and subscription payloads. The changed methods do not add task-execution authority or a task-cancellation operation.

Trust Boundaries and Controls

  • observed — The two reported public-entrypoint ranges are confined to a test helper and a test case. They construct the production channel with injected dependencies and do not introduce a production route or authorization transition.

Resilience and Maintainability Implications

  • observed — The idle sweep derives owed work inside session serialization and includes live child-task state among its checks. This supports the intended normal transition from completed work to an at-rest conversation, but does not establish every partial-teardown or recovery outcome.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 3 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 main change: reporting empty background tasks when native chat resumes at rest.
Description check ✅ Passed The description clearly explains the problem, root cause, fix, scope, linked issue, testing, and AI use. It omits several template headings and does not explicitly provide the required visual-proof N/…
Linked Issues check ✅ Passed The pull request satisfies the coding objective in [#24227]. The background-task channel now converts an unavailable provider state (undefined) to null for subscription and history payloads. This …
Out of Scope Changes check ✅ Passed The changes remain within [#24227]. They modify the background-task channel and add focused channel and reducer regression tests. The pull request does not add unrelated behavior or implement the issu…
  • Fix all pre-merge checks with AI
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


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.

…, cover null-batch clear

Review follow-ups on stablyai#24239:

The new channel test did not compile under tsconfig.node.json (vitest
does not type-check, so the previous verification pass missed it): the
adapter stub's `() => unknown` and the store stub's `undefined` return
did not match the deps' signatures, and the deps literal lacked the
required `journalDatabase` and `claimKeyId`. The fixture now fills the
deps' shape the way the host-runtime-state test does - real per-directory
journal database, typed background-task read, record store stub cast.

`AgentSessionSubscribeEvent` is a discriminated union that includes the
terminal `{ type: 'end' }` frame, so `.backgroundTasks` is not a shared
property. The three assertions now narrow with an `Extract<..., { type:
'batch' }>` predicate instead of reading the last event blind.

The resume contract's renderer half was untested: the reducer must apply
a batch that carries `backgroundTasks: null` over a state that already
holds monitoring tasks, clearing the stale roster, while an omitted field
keeps meaning "no change". A reducer test locks both directions, so the
channel's null fold cannot silently regress on either side.

Generated-by: GLM-5.3-Flash (ZCode)

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

Reviewed changes

  • Test fixture now typechecks — the channel fixture types backgroundTaskState against the adapter's real read, returns null from getRecord, and fills the required journalDatabase/claimKeyId; pnpm tc:node exits clean.
  • Subscribe-event narrowing — all three assertion sites use a typed batch predicate rather than indexing the last event of the union.
  • Reducer lock-in case — a new case pins that an omitted batch field reads as no change (same object identity) while an explicit null replaces the stale roster without disturbing transcript identity.

Both prior review threads (fixture typecheck, union narrowing) are addressed and retired. Runtime tests verified green (channel 4/4, reducer 14/14).

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@ggbdpq

ggbdpq commented Oct 1, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by upstream 0b79720c2e (#22614): the channel's publish path now folds a vanished roster into an explicit null once one was sent (read === undefined && this.published.has(sessionId) ? null : read), which covers the at-rest stale-roster scenario this PR addressed on the subscribe/history read side - and the reducer's explicit-null semantics already clear the strip on receipt.

Closing in favor of the upstream fix. Thanks to greptile, pullfrog and coderabbit for the three review rounds that shaped the test coverage here; happy to revisit if the history read path turns out to still need the fold.

@ggbdpq ggbdpq closed this Oct 1, 2026
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