fix(coding-agent): coalesce child-usage attribution and gate agent-status persistence on real changes - #2050
fix(coding-agent): coalesce child-usage attribution and gate agent-status persistence on real changes#2050snimu wants to merge 7 commits into
Conversation
…dary Every child assistant message_end appended a durable child_usage_attributed entry to the parent journal and re-scanned the child transcript for the origin prompt, so one chatty tool-looping child grew the parent file per model request and kept it permanently changed for every metadata rescan. Completions now accumulate in memory (the parent aggregate still updates per completion) and flush one summed entry per child agent_end, with a settlement backstop for error and cancel paths. All consumers fold attribution entries linearly, so the coalesced entry reloads to the same own-spend. Fixes the defect reported in discussion #1788.
…lassifications The 25s summarizer sweep appended an agent_status entry on every idle pass: a session whose generation could not succeed (no auth, model resolution failure, persistent parse failure) fabricated a needs_input fallback whose empty summary re-armed the retry forever, appending one identical durable entry per sweep per idle session and re-paying the model call for parse failures. Fabricated fallbacks now stay in memory (the roster axis still settles), persistence requires a real generated status that differs from the latest persisted entry, and idle generations stop retrying after three failures on the same settled content until new activity arrives. Fixes the defect reported in discussion #1752.
…iling, key it to content Review fixes for the bookkeeping-append gates: pending child usage now accumulates per origin (steered agent_message usage is no longer mislabeled spawn_task) and a batch older than 60s flushes before it grows or a tool starts, so a process crash loses at most that window instead of a whole turn; the idle-generation retry ceiling keys on settled content (message count plus last timestamp, so a branch/edit back to the same length re-arms) and expires after a 30-minute backoff so transient outages and late credentials recover at one attempt per backoff instead of never; a forget() during an in-flight generation no longer repopulates the failure map for a closed session.
|
Addressed the review in 7bb135f, itemized:
All four new pins verified fail-unfixed against the previous head; suites (recursion 116, summarizer 26 + lifecycle) pass under a sanitized env; |
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 7bb135f. Configure here.
Message count plus last-message timestamp is not a branch identity: sibling branches can collide on both, so branch navigation could stay blocked by a failure record for different content. The leaf entry id is the branch tip identity - appends, edits, and branch navigation all move it - so the ceiling now keys on it, and navigation to a sibling branch re-arms classification.
|
Resolved the branch-navigation thread in 1566d09 + 601d8d0: the concern was valid. Trace: |
…wall-clock backstop Two review findings on the coalesced attribution flush: the stale-batch flush on message_end ran after attributeChildUsage folded the new completion, so the flushed entry's aggregateUsage included a completion whose childUsage was not yet durable and a replay of that prefix inflated the parent's own spend - the flush now runs before the fold, making every persisted aggregate exact for any prefix. And the staleness bound only fired at event checkpoints, so a crash during one long tool execution could still lose the pending batch - a 60s unref'd timer now flushes the batch when no checkpoint arrives.
|
Both new threads addressed in 1d1e233:
Recursion suite 116/116 under a sanitized env; |
…date its pins Comment blocks collapse to one- or two-line invariants. The tool-loop pins share one session builder, the ceiling re-arm pins become one table (leaf move and backoff expiry) whose warmup also carries the fabricated-fallback and never-persist assertions, and the forget() pin reuses the shared setup. Every fail-unfixed behavior keeps its assertion.
) * refactor(coding-agent): move the semantic-edge ledger onto the event-log substrate The recorder's private append/replay/repair IO is deleted; EventLog owns it, the same move #1987 made for the RLM spawn ledger. One durability rule is unified in the substrate rather than dropped: an unterminated final line is an uncommitted append, skipped on read and truncated before the next append — never newline-completed and never surfaced to a consumer whose next append destroys it. * fix(coding-agent): make the explicit ledger reader's ENOENT contract atomic readSemanticEdgeLedger probed with statSync before reading through EventLog, which swallows ENOENT; a ledger deleted between the two returned [] instead of throwing. The missing-file decision now lives at the single open (replaySync missingFileThrows), so no check-then-read window exists. * docs(coding-agent): state the event-log tail rule once The unterminated-tail contract was restated four times (module doc, replaySync doc, two test comments). It now lives once in the module doc; the method doc keeps only its own parse/missing-file semantics and the test comments reference the contract. * fix(coding-agent): write event-log appends fully and gate appends on tail repair writeSync may write short (ENOSPC after a prefix); appendSync now loops until the payload is fully on disk so write-before-action callers never act on a torn record reported as success. A tail-repair failure (e.g. append-only ACL permitting O_APPEND but not r+) now propagates instead of being swallowed: writing through an unrepaired torn tail would weld it to the new record as permanent interior corruption. ENOENT and the concurrent-writer instability path keep their existing semantics. * fix(coding-agent): reclaim short event-log writes instead of completing them The rlm spawn ledger is multi-writer by documented design (supervisor plus each worker over one file), so completing a short O_APPEND write with a second write could interleave with a rival append and weld two records. A short write now truncates its own torn prefix back off (only while this writer still owns the tail) and fails the append; a torn tail is read-tolerated, a weld is permanent corruption. The append fd opens a+ so the ownership check can read the tail. * fix(coding-agent): leave the torn tail on a short write instead of reclaiming it The tail-match reclaim could truncate a rival's committed record whose final bytes coincide with our torn prefix - committed-data loss, strictly worse than the torn tail it prevented. A short write now just fails the append: the torn tail is the one tolerated shape, skipped on read and truncated by any writer's next repair (verified for both topologies: a resumed single-writer recorder repairs on its first append; every rlm-ledger writer repairs before each append). * refactor(coding-agent): compress event-log comments * fix(ai): omit the default service tier, reprice cache writes from message_delta, repoint the zai default Incorporates #2032 at f82c7fa. * fix(tui,coding-agent): survive lone surrogates in table cells and terminate the WebP EXIF scan Incorporates #2033 at a3d1139. * fix(coding-agent): restart dead kernels on ensure() and read mcp>=2 tool schemas Incorporates #2034 at 749e216. * fix: one crash-safe owner for durable state writes Incorporates #2035 at f0f02d2. * fix(coding-agent): one zombie-aware process-liveness probe Incorporates #2041 at 92a0eac. * fix(coding-agent): snapshot transfer ids from the materialized cursor; mismatches settle the transfer, not the worker channel Incorporates #2044 at 5af3bbe. * fix(coding-agent): failed workers recover on touch; roster gaps answer a structured recovering error Incorporates #2047 at 77b747a. * fix(coding-agent): seven session and IO correctness defects Incorporates #2037 at 41b5d72. * fix(coding-agent): coalesce child-usage attribution and gate agent-status persistence on real changes Incorporates #2050 at 6b0af5d. * fix(coding-agent): incremental single-flight session metadata scans Incorporates #2043 at df032c1. * fix(coding-agent): memoize the passive RLM topology derivation Incorporates #2051 at 0ee114c. * fix(coding-agent): preserve accounting and metadata across deferred updates Keep durable child-usage aggregates separate from pending sibling usage. Retry optional topology metadata after transient reads. Completes #2050 and #2051 integration. * fix: preserve session accounting and read-only persistence boundaries --------- Co-authored-by: Seth <seth@primeintellect.ai>

Purpose
Two derived-bookkeeping paths appended durable journal entries at the wrong granularity, growing parent session files without bound and (for one of them) re-paying model calls forever:
message_endsynchronously appended achild_usage_attributedentry to the parent journal and re-scanned the child transcript for the origin prompt — one journal append per model request of every RLM child, keeping parent files permanently "changed" for every metadata rescan (feeds the amplification fixed in fix(coding-agent): incremental single-flight session metadata scans #2043).agent_statusentry on every idle pass: a session whose generation cannot succeed (no prime-inference auth, model resolution failure, persistent parse failure) fabricated aneeds_inputfallback whose empty summary re-armed the retry forever — one identical durable entry per idle session per 25s, a persisted verdict the model never produced, and a paid model call per sweep in the parse-failure case.Mechanism
agent_end, with a run-settlement backstop for error/cancel paths. All consumers fold attribution entries linearly (loader, scanner, context-tree), so the coalesced entry reloads to the same own-spend.getLatestAgentStatus); fabricated fallbacks stay in memory only (the roster activity axis still settles); idle generations stop retrying after three failures on the same settled content until new activity arrives.Measurement
SessionManager+ summarizer driver): main appends 3456agent_statusentries (+591KB journal, unbounded); this PR appends 0 and stops generation after 3 attempts (3456 -> 3 generate calls — for persistent parse failures those are paid calls).Validation
agent-session-recursion(115),daemon-session-summarizer(23),daemon-session-summarizer-lifecycle,session-manager/agent-status— all pass; rootnpm run checkpasses.LOC
Total src: +128/−26 (net +102); tests: +200/−42 (net +158).
Src +64/−18 (net +46): both changes wire persistence to existing boundaries (child
agent_end/ run settlement; the summarizer's existing latest-persisted truth) rather than adding machinery. Tests +79/−9.Squashes discussions #1788 and #1752; with #2043 this also removes the main journal-growth driver behind the #1503 worker OOM profile.
Linear: RES-1272 https://linear.app/primeintellect/issue/RES-1272
Note
Medium Risk
Touches durable session journal semantics for billing attribution and agent-status history; behavior is covered by new tests but incorrect coalescing or persistence gating could skew cost totals or dashboard status replay.
Overview
Stops unbounded parent session journal growth from two bookkeeping paths that were appending on every child model turn and every 25s idle sweep.
RLM child usage: Child assistant completions no longer call
appendChildUsageAttributionon eachmessage_end. Usage is accumulated in memory by origin (spawn_task,agent_message,direct_user) and flushed once per settle boundary (childagent_end, run cleanup, staleness after 60s, or a timer backstop). In-memory parent totals still update per completion; reload semantics stay consistent because consumers fold attribution entries linearly.Daemon idle status: The summarizer caps failed idle model calls (3 attempts per settled content key, 30-minute backoff keyed by leaf id + message count) and only persists
agent_statuswhen the model actually returned a classification that differs fromgetLatestAgentStatus. Fabricatedneeds_inputfallbacks remain in memory for the roster but no longer duplicate into the journal on unchanged idle sweeps.Reviewed by Cursor Bugbot for commit 6b0af5d. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Coalesce child-usage attribution in
AgentSessionand gate idle status persistence on real changesAgentSession.runRlmChildnow aggregates multiple per-request child usage records into one attribution entry per origin, flushing at child-turn settlement, cleanup, or after a 60-secondRLM_CHILD_USAGE_FLUSH_MAX_PENDING_MSbackstop. Failed bookkeeping appends no longer interrupt run settlement.DaemonSessionSummarizer.summarizenow caps idle-generation retries at three failed attempts per content key with a 30-minute backoff, and suppresses persisting fallbackneeds_inputverdicts or duplicate settled statuses. Idle statuses are persisted only when the summary, task state, or message count differs from the latest persisted status.runRlmChildis unref'ed, so it will not keep the process alive; pending attribution can be lost on crash beyond the 60s window. TheDaemonSessionSummarizernow relies ongetLeafIdfrom the session manager for content-key identity, so any caller not providing this mock data in tests will break.Macroscope summarized 6b0af5d.