fix: heartbeat sessions are normal sessions — idle display and normal residency with durable wake - #1967
Merged
Conversation
An armed heartbeat between firings is residency, not work, so it no longer counts as a running signal anywhere: the roster classifier, the unified agents-view classifier, the ancestor-propagation overwrite, and the subagent count projections all drop the heartbeat clause. A firing heartbeat still lands in Running through its executing turn, and hasActiveHeartbeat stays on the wire for badges and labels. Idle rows with an armed heartbeat now label the next fire time (heartbeat - next 5m) when the aggregate knows it. Paused-only heartbeats, previously invisible, render a dimmed heartbeat badge without affecting the section. The delete confirmation warns when the targeted session or subagent still has an armed heartbeat; the daemon already cancels scheduled jobs in both delete paths. ENG-5847
…me due An armed heartbeat no longer pins a session resident forever. The hasRegisteredHeartbeat veto is deleted from the idle-eviction and child passivation policies (and from the empty-draft detach rule): heartbeat sessions follow the one existing idle rule (idleEvictionMinutes, default 90m) like every other session. The schedule survives passivation through a single wake owner split by residency: workers keep owning all firing and delivery for resident trees (their scheduler already revives passive children on due jobs), and the supervisor owns exactly one new concern - waking a session tree that is resident nowhere. It recomputes wake timers from durable truth (the saved-session catalog plus each session's scheduled-jobs artifact) at boot and on every heartbeat/residency change, and on fire relaunches the root worker through the same create path clients use; the fresh worker's scheduler then runs the due job with the existing catch-up semantics. Workers now also register passive descendants' scheduled-job artifacts when a root becomes resident, so a relaunched tree schedules its passivated children's heartbeats without hydrating them. Passivated sessions keep their heartbeats visible: the supervisor's heartbeats_list appends passive scheduled jobs, roster rows keep their registration marks across eviction, and the agents-view badge aggregate matches jobs by stable session id and file, not just the stale active id. ENG-5846
…opology Review round on the residency half: - Client-owned schedules now die with the registration, exactly like their roster rows: stopping an ephemeral worker cancels the scheduled jobs of its root and ledger descendants, so a private session is never listed in heartbeats_list or relaunched by the wake. Public sessions are unaffected because heartbeat_set/cron_add promote owned sessions before their jobs land. - The wake enumeration now reads topology from the RLM spawn ledger's family (roots plus ledger descendants, fork headers stripped) instead of the saved-session catalog, which missed nested subagent transcripts entirely and misassigned passivated forks to their former parent through header lineage. - The wake schedule stays disarmed while an update restart is being prepared instead of spinning a zero-delay timer on overdue jobs; the failed-preparation transition re-arms it once, and a successful restart re-arms at the next boot. - One corrupt descendant artifact no longer strands the remaining descendants' registrations in a fresh worker. - Managing a passivated heartbeat now works: heartbeat_manage resolves the job through the same durable enumeration and mutates its scheduled-jobs store directly, without waking the session. ENG-5846
Comment pass over both halves: every added comment either deleted or cut to one load-bearing line. Test pass: the paused/active heartbeat eviction test duplicated the sweep pin's heartbeat eviction (the distinction has no code path anymore) and the next-fire label pin merged into the heartbeat aggregation test it shares fixtures with.
…he wake The ephemeral-stop cancel derived descendant session ids from filenames, while scheduled-job artifacts key on the persisted session id: a child whose id differs from its file name kept armed jobs past its owner's departure. The cancel now walks the same ledger family() rows the wake enumeration uses, so both sides share one id resolution. The cancel also ran after the worker left the worker map, leaving a window (or, after a swallowed failure, forever) in which the tree looked non-resident with active jobs and could be woken publicly. It now runs before the map removal - every enumeration observed during the awaited cancel still sees the tree covered - and a failed cancel parks the root in pendingEphemeralCancels: enumeration retries the cancel first and keeps the tree excluded until it lands. ENG-5846
…tale cancel intents Unscoped cron_cancel probed only resident workers, so a passivated session's still-firing job could not be stopped without reopening the session; it now falls back to the passive enumeration and cancels through the per-job store API, broadcasting so the wake timer drops it. Unscoped cron_list merges the same passive jobs after the resident sweep (worker copies win). With these, no unscoped job surface in the protocol misses passive sessions. A parked ephemeral-cancel intent also outlived its premise: reopening the tree after a failed cancel would let the enumeration retry destroy the fresh worker's new schedules. The retry now drops the intent when a worker covers the tree again - the store is worker-owned from that moment and resident trees are excluded from enumeration anyway. RES-1250
This was referenced Sep 2, 2026
One malformed scheduled-jobs.json aborted collectPassiveScheduledJobs entirely, so every other passive session's jobs vanished from listings and their wakes never armed. Ref: RES-1250
A failed ephemeral cancel was parked only in memory, so a supervisor restart lost the intent and the next boot recovered the private tree's jobs wake-eligible. The stop now deletes the persisted tombstone only after the cancel settles; boot finishes the cancel from the tombstone, with the same covered-tree exemption that protects a reopened session's new schedules. Ref: RES-1251
Resident workers honor includeInactive but the passive merge only contributed active/paused jobs, so passivation silently shrank an inclusive listing. The wake path keeps considering active jobs only. Ref: RES-1250
promoteOwnedWorker can clear ownerClientId while the cancel awaits the ledger family read, so a successful promotion had its now-public schedules cancelled out from under it. The guard runs in the same synchronous turn as the store walk; promotion commits are synchronous single-mutation blocks, so a promote lands either wholly before the guard (cancel skipped) or wholly after the walk. Ref: RES-1251
… its jobs Passivity was judged by the top ancestor alone, so a job owned by a mid-tree session with its own resident worker still counted passive and the wake launched a competing root worker over an active subtree. The walk now checks findWorkerBySessionFile at every hop; only a fully uncovered chain stays wake-eligible. Ref: RES-1250
The round-5 stillWanted guard only ran when the family read succeeded: a promotion landing during a throwing read was still parked, and the enumeration retry cancelled the now-public schedules unguarded. The catch now settles instead of parking when ownership is gone, and the retry passes stillWanted from the parked worker's descriptor - the same object a late promotion mutates and persists, which the boot gate already reads. Ref: RES-1251
The in-memory park/retry map duplicated a truth that already lives on disk: the kept tombstoned client-owned descriptor. Enumeration now re-derives retry intents (and the still-pending tree exclusion) from descriptorDir alone, and the covering-worker exemption moves inside the destructive walk itself, checked per tree member - chain-complete where the old root-only retry check missed a replacement worker reopened on a child file - so stop-time, retry, and boot cancels share one guard. Promotion self-corrects in memory and on disk because promoteOwnedWorker replaces and persists the descriptor. Ref: RES-1251
The wake scan only enumerates the supervisor's sessions root, so a public worker whose root file lives in a custom session dir would passivate and never wake for its schedules. Eviction eligibility now matches wake enumerability: such a worker with a registered heartbeat or cron job stays resident, exactly the pre-PR behavior for the blind set. create.config.sessionDir is client-reachable (daemon CLI --session-dir). Ref: RES-1250
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 1723f5e. Configure here.
VincentBailly
pushed a commit
to VincentBailly/prime-agent
that referenced
this pull request
Sep 2, 2026
… section Fork-local workaround. Do not merge upstream. Delete it once upstream classifies finished agents correctly. Finished agents stay in the agents-view Running section forever. The supervisor ledger only re-publishes a roster row when the summarizer verdict text changes, so a finished agent keeps activity "working" (label "classifying") and a frozen rosterStatus "running". This guard trusts "running" only when a hard busy signal still backs it. Greppable marker sites (both carry the marker LOCAL PATCH(agents-view-done-in-running)): packages/coding-agent/src/modes/agents-view/agents-view-state.ts packages/coding-agent/test/agents-view-done-in-running.test.ts Drop test, one command: delete the guard block in agents-view-state.ts, then run npx vitest --run packages/coding-agent/test/agents-view-done-in-running.test.ts If it still passes without the guard, upstream fixed the bug: delete the guard block AND the whole test file. Upstream refs: PrimeIntellect-ai#1873, PrimeIntellect-ai#1872, PrimeIntellect-ai#1967. Caused by 8d5722e / 1d2e91d.
snimu
added a commit
that referenced
this pull request
Sep 2, 2026
The descendant tally read row sections after propagateHeartbeatStateToAncestors had promoted idle ancestors to running, so a heartbeat-active grandchild counted its promoted parent too. The tally now runs before the promotion pass and counts intrinsically busy rows only; once that propagation pass is deleted (#1967) the ordering is a no-op. RES-1253
The gate lived at the sweep's candidate build only, so the fenced recompute after the mutation drain and the last-detach empty eviction could still passivate a custom-dir worker into the blind spot. The fact moves into the WorkerEvictionSnapshot (hasWakeBlindSchedule, refused by canEvictWorker) so both sweep sites read one policy, and the last-detach path - a different input shape built from empty-draft summaries, not eviction snapshots - consults the same isWakeBlindScheduledWorker helper in its own eligibility predicate. Call-site check deleted. Ref: RES-1250
…ptor workerEvictionSnapshot now computes the required hasWakeBlindSchedule field, which walks descriptor.sessionFile ?? createCommand.sessionPath. createCommand is required on DaemonWorkerDescriptor and every real descriptor carries it; the fixture impersonated one through an unknown-cast without it. Fix the fixture, not the type contract.
sethkarten
self-requested a review
September 3, 2026 15:19
sethkarten
approved these changes
Sep 3, 2026
sethkarten
pushed a commit
that referenced
this pull request
Sep 3, 2026
…1986) * fix: count busy descendants in the agents-view subagents indicator The "N subagents running" indicator only tallied direct children whose row classified as running, so a busy grandchild under an idle intermediate child left every ancestor reading "N subagents" with a zero count. The tally is now a bottom-up pass over the built rows - the live per-descendant truth - replacing the direct-child increment. Indicator only, per policy: a session is Running when it works itself; idle ancestors of busy subtrees stay in Idle. Within Idle they now rank above plain idle rows, and a collapsed group's summary row renders its running count in the success color instead of dimmed so the busy subtree is discoverable without expanding. Diagnosis credit: Vincent Bailly (VincentBailly#9) traced the stale hasRunningRlmChildren snapshot and the row-walk mechanism; this adopts his walk for the indicator while rejecting the section promotion. RES-1253 * fix: tally busy descendants before heartbeat promotion inflates sections The descendant tally read row sections after propagateHeartbeatStateToAncestors had promoted idle ancestors to running, so a heartbeat-active grandchild counted its promoted parent too. The tally now runs before the promotion pass and counts intrinsically busy rows only; once that propagation pass is deleted (#1967) the ordering is a no-op. RES-1253 * fix: make the descendant tally iterative so deep chains cannot overflow The recursive tally threw RangeError on a deep child chain just by opening the agents view. The nesting loop assigns every row at most one parent, so the tree is a forest: a reverse breadth-first pass over one work list computes the same bottom-up counts iteratively, with the pre-promotion ordering kept. RES-1253 * fix: classify running from the session's own work, not delegated children Second half of the RES-1253 policy, user-approved: Running means the session you enter is doing work now. Delegated child work no longer classifies a session as running - the busy-descendant badge, count, and idle ranking from the first half carry the delegation signal. hasRunningRlmChildren loses its section-classification role in both owners: classifySessionRosterStatus composes busy from the session's own activity/isSessionActive, and isActiveSessionBusy (the worker's activity axis) no longer holds a settled parent at "working" for its children. The "subagents running" status label dies with the section it had to agree with. The field itself stays on the wire, and isSessionSummaryBusy keeps it on purpose for its residency and shutdown-safety consumers (worker eviction snapshots, empty-draft eviction, busy client-owned session counts, daemon stop confirmation). RES-1253 * chore: note the running-means-the-session policy in the changelog fragment * fix: split display activity from live-work safety in the busy predicates isActiveSessionBusy served two meanings after the classification change: the display activity axis (session's own work only, correct) and worker recovery plus draft-discard (where a running RLM child is live work that dies with the worker). The recovery journal recorded busy:false for a settled parent with a running child, so a worker death skipped its interruption record and notice. The shared predicate is deleted and each meaning gets one named owner: activeActivityForSession reads the session's own isSessionActive directly, and hasLiveSessionWork (own turn or running RLM child) backs recordWorkerRecoveryState and isDiscardableDraft. No caller can grab the wrong meaning blind. RES-1253 * fix: gate destructive agent actions on live work, not display section Since Running means the session's own work, an idle parent of a running crew was branching into the delete path: killSubagent chose stop-vs- delete from the display section and stopAgentForDeletion treated activity==='working' as the only live work. Destructive actions are safety consumers of the display/safety split: one row-level hasLiveWork (own section running, busy descendants, or the wire running-children flag) now drives the stop-first branch, the deletion flow, and the confirmation verb. Also trimmed the tally comment to current behavior. RES-1253 * fix: stop live descendants when cancelling a settled child run Stopping a subagent row whose own run already settled reported 'Subagent already finished' while a nested descendant kept running: cancelRlmChildRun only cancelled the targeted run itself, and a fully released child (run removed, session retained) was never matched by id at all. Cancellation now descends where the tree lives: a settled target stops every running or queued run in its retained session's subtree (cancelRunningRlmDescendants, mirroring hasRunningRlmChildren's walk), and the returned flag stays truthful so the UI says stopped only when something stopped. RES-1253 * chore: trim comments and fold overlapping pins on the indicator branch Comment pass: multi-line narration cut to one-line invariant guards (destructive live-work gate, residency-vs-section busy split, worker- death live work) and code-readable notes deleted. Test pass: the direct- child indicator pin folded into the grandchild pin (the general case), which now also carries the idle-label and wire-flag assertions. * fix: cancel and descend at every node of the rlm cancel walk Cancelling a live child run aborts it, and abort cascades into the child's ACTIVE runs - but running work retained under a settled descendant of that child was the end of the line: neither the cascade nor the cancel walk visited it. cancelRlmChildRun now descends into the target's session after cancelling it, and cancelRunningRlmDescendants descends at every node instead of treating running runs as leaves, so cancellation is exhaustive over the subtree regardless of each node's run state. The returned flag stays truthful. RES-1253
sethkarten
added a commit
that referenced
this pull request
Sep 3, 2026
* fix: count busy descendants in the agents-view subagents indicator The "N subagents running" indicator only tallied direct children whose row classified as running, so a busy grandchild under an idle intermediate child left every ancestor reading "N subagents" with a zero count. The tally is now a bottom-up pass over the built rows - the live per-descendant truth - replacing the direct-child increment. Indicator only, per policy: a session is Running when it works itself; idle ancestors of busy subtrees stay in Idle. Within Idle they now rank above plain idle rows, and a collapsed group's summary row renders its running count in the success color instead of dimmed so the busy subtree is discoverable without expanding. Diagnosis credit: Vincent Bailly (VincentBailly#9) traced the stale hasRunningRlmChildren snapshot and the row-walk mechanism; this adopts his walk for the indicator while rejecting the section promotion. RES-1253 * fix: tally busy descendants before heartbeat promotion inflates sections The descendant tally read row sections after propagateHeartbeatStateToAncestors had promoted idle ancestors to running, so a heartbeat-active grandchild counted its promoted parent too. The tally now runs before the promotion pass and counts intrinsically busy rows only; once that propagation pass is deleted (#1967) the ordering is a no-op. RES-1253 * fix: make the descendant tally iterative so deep chains cannot overflow The recursive tally threw RangeError on a deep child chain just by opening the agents view. The nesting loop assigns every row at most one parent, so the tree is a forest: a reverse breadth-first pass over one work list computes the same bottom-up counts iteratively, with the pre-promotion ordering kept. RES-1253 * fix: classify running from the session's own work, not delegated children Second half of the RES-1253 policy, user-approved: Running means the session you enter is doing work now. Delegated child work no longer classifies a session as running - the busy-descendant badge, count, and idle ranking from the first half carry the delegation signal. hasRunningRlmChildren loses its section-classification role in both owners: classifySessionRosterStatus composes busy from the session's own activity/isSessionActive, and isActiveSessionBusy (the worker's activity axis) no longer holds a settled parent at "working" for its children. The "subagents running" status label dies with the section it had to agree with. The field itself stays on the wire, and isSessionSummaryBusy keeps it on purpose for its residency and shutdown-safety consumers (worker eviction snapshots, empty-draft eviction, busy client-owned session counts, daemon stop confirmation). RES-1253 * chore: note the running-means-the-session policy in the changelog fragment * fix: split display activity from live-work safety in the busy predicates isActiveSessionBusy served two meanings after the classification change: the display activity axis (session's own work only, correct) and worker recovery plus draft-discard (where a running RLM child is live work that dies with the worker). The recovery journal recorded busy:false for a settled parent with a running child, so a worker death skipped its interruption record and notice. The shared predicate is deleted and each meaning gets one named owner: activeActivityForSession reads the session's own isSessionActive directly, and hasLiveSessionWork (own turn or running RLM child) backs recordWorkerRecoveryState and isDiscardableDraft. No caller can grab the wrong meaning blind. RES-1253 * feat: show token and cost totals on agents view rows Every agents view row with usage data now reads `<input>/<output> | $<own> ($<recursive>)` in the details cell, replacing the message-count detail. Own numbers are the session's whole-file spend (every branch, forks included, attributed child usage subtracted) - the money answer, deliberately broader than /usage's current-branch context answer, and identical for resident and on-disk rows so nothing shifts at passivation or revival. The recursive total rides the same reverse breadth-first traversal as the running-subagent tally, summing each descendant row's own cost exactly once. Producers: resident summaries compute the total from the already-loaded entries, memoized until entries change (0.27 ms measured cold at 10k entries); saved rows accumulate it inside the existing mtime/size-cached catalog scan, so old files get costs with no new IO and no new cache. Entries over the scan's max line length keep their existing skip, so a giant assistant message can undercount slightly. Schema revision 26 publishes the new optional summary and saved-row field. RES-1258 * feat: final row usage format with arrows and conditional recursive cost Details cell now reads `↑12k ↓1.2k · $0.42 ($1.10 w/ subagents)`; the parenthesized total renders only when descendants added spend visible at cent rounding, so leaf rows read `↑500 ↓50 · $0.68`. Arrow glyphs follow the existing TUI vocabulary (keybinding hints, token status) and the width pipeline already treats them as single cells. RES-1258 * fix: one own-spend truth across scan, loader, and filters Review round on the usage feature: - The catalog scan subtracted attributed child usage from disk values that (in append-only files) still carry the assistant's ORIGINAL model-response usage, double-subtracting and shifting numbers at passivation. Full-file rewrites (migrations, forks) can also persist the already-folded aggregates, so raw summation is wrong for those files instead. The scan now mirrors the loader exactly: fold each attribution's aggregate onto its target, then subtract the child usage - both disk representations cancel to the same own spend the resident computation reports. The equality pin now goes through a real v3 flushed-then-attributed fixture (the old fixture had no attribution, which is why it never caught this). - Recursive cost was computed over search-filtered rows, so a matching parent showed an incomplete "w/ subagents" total. The rollup now comes from the unfiltered record hierarchy (computeRecursiveCosts over the unified index) and filtering can no longer change the number. - Zero-spend sessions published 0/0/$0.00 live and nothing once passivated; sessionUsageSummaryFrom now returns undefined for zero totals, shared by both producers, so rows stay age-only until real spend exists. RES-1258 * fix: gate destructive agent actions on live work, not display section Since Running means the session's own work, an idle parent of a running crew was branching into the delete path: killSubagent chose stop-vs- delete from the display section and stopAgentForDeletion treated activity==='working' as the only live work. Destructive actions are safety consumers of the display/safety split: one row-level hasLiveWork (own section running, busy descendants, or the wire running-children flag) now drives the stop-first branch, the deletion flow, and the confirmation verb. Also trimmed the tally comment to current behavior. RES-1253 * fix: stop live descendants when cancelling a settled child run Stopping a subagent row whose own run already settled reported 'Subagent already finished' while a nested descendant kept running: cancelRlmChildRun only cancelled the targeted run itself, and a fully released child (run removed, session retained) was never matched by id at all. Cancellation now descends where the tree lives: a settled target stops every running or queued run in its retained session's subtree (cancelRunningRlmDescendants, mirroring hasRunningRlmChildren's walk), and the returned flag stays truthful so the UI says stopped only when something stopped. RES-1253 * feat: render the usage cell unconditionally, zeros included Every agents view row now renders the full details cell `↑<up> ↓<down> · $<own> ($<total> w/ subagents)` over four defaulted numbers - no presence branching, no thresholds, no conditional paren. A session that never spent reads `↑0 ↓0 · $0.00 ($0.00 w/ subagents)`, and a zero-usage parent with billed subagents shows its subtree spend instead of hiding it. RES-1258 * chore: trim comments and fold overlapping pins on the indicator branch Comment pass: multi-line narration cut to one-line invariant guards (destructive live-work gate, residency-vs-section busy split, worker- death live work) and code-readable notes deleted. Test pass: the direct- child indicator pin folded into the grandchild pin (the general case), which now also carries the idle-label and wire-flag assertions. * chore: cut the usage feature's comments and collapse its pins Comment pass (>80%): 24 added comment lines down to 4 one-line invariant guards (loader-fold cancellation in the scan, passivation-invariance on the resident getter, unfiltered rollup, the revision note). Test pass: the scan-accumulation and resident-equality pins merged onto one forked- and-attributed fixture; the descendant-rollup and filtered-total pins merged with a grandchild; the format pin asserts all four row shapes from one fixture; redundant zero-case and permutation tests deleted. * fix: cancel and descend at every node of the rlm cancel walk Cancelling a live child run aborts it, and abort cascades into the child's ACTIVE runs - but running work retained under a settled descendant of that child was the end of the line: neither the cascade nor the cancel walk visited it. cancelRlmChildRun now descends into the target's session after cancelling it, and cancelRunningRlmDescendants descends at every node instead of treating running runs as leaves, so cancellation is exhaustive over the subtree regardless of each node's run state. The returned flag stays truthful. RES-1253 * fix: passivated RLM descendants survive restarts as saved-catalog rows An RLM child's transcript lives in session-artifacts, which the saved- session scan never visits. While something resident remembered the child its row survived; after a supervisor restart an inactive parent's passivated descendants silently disappeared from the agents view, and their spend vanished from the parent's subtree total. Both list_saved_sessions composition points now append live spawn-ledger children the scan missed, through one shared walk: readSessionInfo per descendant, parentSessionPath/rlmDepth backfilled from the edge, deleted edges excluded. The view already renders saved-only descendants, merges resident duplicates by session identity, and rolls saved usage into recursive totals, so restored rows restore the money with no view change. Measured at 300 sessions with 36 passivated descendants: scan 33ms, merge +9ms (cold ledger, ~0.25ms per descendant). RES-1262 RES-1258 * feat: compaction and branch-summary calls bill the session they serve Summarization was invisible money: the compaction model call(s) - two on a split turn - and branch-summary calls returned usage that was dropped on the floor, so a session that compacted often under-reported what it actually cost. The summarizer responses' usage is now folded (both split-turn slices) and persisted on the compaction/branch_summary entry, and both own-spend producers add the same term: own spend = fold(assistant usage) + fold(summarization usage) - attributions, identical resident and scanned. Old entries without the field fold as zero; a failed compaction persists nothing, so failed-call billing is not recoverable - there is no committed entry to carry it. RES-1258 * fix: harden the passive-descendant catalog merge (round 2) Three review findings on the merge, all real: the transcript header's parentSession can point at a forked-away ancestor, so the ledger edge is now the authoritative topology (family() semantics) for parent and depth; ledgers are per sessions-dir family, so each catalog request now reads the ledger of the directory it lists instead of always the default one; and a broken ledger (unreadable, over-quota) no longer fails the whole list_saved_sessions response - the merge logs and returns the scanned catalog. RES-1262 * chore: consolidate the accumulated review-round pins The worker-daemon catalog pins from three rounds folded onto one fixture: restart rows, per-family ledger isolation, and broken-ledger degradation now share the two-family scaffold. Redundant assertions and narration comments cut; every behavior keeps exactly one pin. * test(coding-agent): align unified view regression with usage rows --------- Co-authored-by: Seth <seth@primeintellect.ai>
This was referenced Sep 5, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

A session with an armed heartbeat sat in the agents view's Running section forever — even when it had been quiet for hours — and its worker was pinned resident around the clock, because the daemon treated "has a heartbeat" as both a running signal and an idle-eviction veto. This PR makes heartbeat sessions completely normal sessions: truthful status in the view, and normal residency with a durable wake when the next beat comes due.
Part 1 — display. The contract is now: running = doing work now (an executing heartbeat turn still lands there through its normal prompt turn); idle = resident and quiet, including armed heartbeats between firings; inactive = not resident. A heartbeat count is never a running signal. The heartbeat-to-running rule is deleted from every place it lived — the roster classifier, the unified agents-view classifier, ancestor propagation, subagent count projections, and the daemon's agent-message summaries. hasActiveHeartbeat stays on the wire for badges and labels (no protocol change): idle rows label the next firing (
heartbeat · next 5m), paused-only heartbeats render a dimmed ♥ badge, and the delete confirmation warns when the targeted row still has an armed heartbeat.Part 2 — residency. The residency half of the same rule (hasRegisteredHeartbeat vetoing idle eviction, child passivation, and empty-draft eviction) is deleted too; heartbeat sessions passivate under the one existing idle rule,
idleEvictionMinutes(default 90 minutes, "off" supported). Ownership is split by residency: workers keep all firing and delivery for resident trees (existing scheduler revival, claim locks, and catch-up semantics unchanged), and the supervisor owns exactly one new concern — waking a tree that is resident nowhere. It recomputes a single unref'd wake timer (the soonest due job) from durable truth only — the spawn ledger'sfamily()plus each session'sscheduled-jobs.jsonartifact — at boot and whenever heartbeats or residency change; on fire it relaunches the root worker through the same create path clients use, and the fresh worker's scheduler runs the due job. Failed relaunches back off 60 seconds instead of hot-looping, and the schedule stays disarmed while an update restart is being prepared. The supervisor never dispatches or delivers, so firing ownership never overlaps.Client-owned (ephemeral) sessions stay private: stopping the worker cancels the scheduled jobs of its root and ledger descendants — schedules die with the registration exactly like their roster rows — and a failed cancel keeps the stop tombstone as durable intent, re-derived from the persisted descriptors at the next boot (public sessions are unaffected: heartbeat_set / cron_add promote owned sessions before their jobs land). One eligibility gate: the wake enumerator sees only the supervisor's sessions root, so an idle public worker whose root file lives outside that root keeps its residency while it has a registered heartbeat or cron job — passivating it would orphan schedules no wake scan can see. And a freshly launched root registers its passive descendants' scheduled-job artifacts on creation (no hydration; a corrupt artifact skips only itself), so a relaunched tree schedules its passivated children too.
Passivated heartbeats stay usable:
heartbeats_listappends passive scheduled jobs from the same durable enumeration the wake uses, roster rows keep their registration marks across eviction and rank first within the Inactive section, the badge aggregate matches jobs by stable session id and file (the active id goes stale after passivation), andheartbeat_managemutates a passive job's scheduled-jobs store directly — no wake just to pause or stop it.Validation: 14 display pins fail on main and pass here (next-fire label, dimmed paused badge, delete guard included); 6 residency pins fail on the display-only half and pass here, plus 4 review-round pins (client-owned schedule cancellation, ledger-topology wake, update-restart disarm, passive heartbeat management); resident-idle firing is already pinned by the unchanged daemon-mode heartbeat tests. daemon-mode (184), daemon-supervisor-* (including the process suite), session-action-store, daemon-session-list, cron-jobs, all heartbeat suites, and the full agents-view/roster/subagent display suites pass in a sanitized env; root
npm run checkgreen.Linear: RES-1251 (display) https://linear.app/primeintellect/issue/RES-1251/sessions-with-armed-heartbeats-show-as-running-forever-instead-of-idle
Linear: RES-1250 (residency) https://linear.app/primeintellect/issue/RES-1250/armed-heartbeats-pin-workers-resident-247-supervisor-owned-scheduler
Note
Medium Risk
Changes daemon supervisor scheduling, idle eviction, and session wake paths alongside agents-view status semantics; incorrect wake or eviction could miss heartbeats or drop schedules, though behavior is heavily test-covered.
Overview
Armed heartbeats no longer count as “running.” Roster and agents-view classification, ancestor propagation, and subagent counts now treat only real work (streaming, tools, children, etc.) as Running; quiet sessions with active heartbeats sit in Idle with badges and labels like
heartbeat · next 5m. Paused-only jobs get a dim ♥ badge; delete confirmations warn when a row still has an armed heartbeat.Residency matches normal idle rules.
hasRegisteredHeartbeatis removed from idle eviction, child passivation, and empty-session eviction. The supervisor instead scans durable scheduled-job artifacts and the spawn ledger, arms a single wake timer, and creates/reuses a root worker when the next active job is due (with backoff and disarm during update-restart). Workers whose session files sit outside the enumerable sessions root stay resident when they have schedules the wake scan cannot see (hasWakeBlindSchedule).Passivated trees stay operable:
heartbeats_list/cron_listmerge passive jobs;heartbeat_manageandcron_cancelcan hit durable stores without hydration; badge aggregation keys off session id/file when active ids go stale; passivated roster rows keep registration marks; new roots register passive descendants’ cron artifacts. Client-owned workers cancel scheduled jobs for the whole tree on stop, with tombstoned descriptors retried on boot if cancel fails.Reviewed by Cursor Bugbot for commit 281cfbf. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Treat heartbeat sessions as normal sessions for idle display and residency, add durable passive wake
classifyAgentStatus,classifySessionRosterStatus, and the agents-view/interactive classifiers now derive status from actual work, residency, and queued children only.wake-blindflag onWorkerEvictionSnapshotfor workers whose session file lives outside the enumerable sessions root. These workers are protected from idle and detach eviction because the supervisor cannot re-wake them.cron_list,heartbeats_list,heartbeat_manage, andcron_cancelcommand handlers to operate on passive scheduled jobs directly via durable artifacts, without hydrating or waking a worker.SessionEvictionSnapshotno longer carries a per-session heartbeat-registration field andclassifyAgentStatusno longer accepts a heartbeat activity flag.Macroscope summarized 281cfbf.