refactor(coding-agent): schedule agent-trace uploads through a disk-cursor outbox - #1957
Merged
Conversation
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 c90981d. Configure here.
…-cursor outbox Upload intent and uploaded-content cursors persist per session file in agent-traces-outbox.json; a once-per-process startup catch-up uploads whatever a previous process never finished and prunes cursors of deleted files. Unchanged sessions are never re-uploaded. 429s return immediately and reschedule via the controller instead of sleeping in-request, and session disposal no longer knows trace uploads exist. Linear: ENG-5838
…t, and process-exit safe One entry file per session (path-hashed) replaces the single-map file: concurrent writers cannot lose cursors and a bad read costs only its own entry. The pending marker is written synchronously at first persist. A failed cursor write after a successful PUT returns a retryable failure. 429 Retry-After is honored on the next scheduled cycle. All upload timers are unref-ed so telemetry never holds the process open.
snimu
force-pushed
the
refactor/agent-traces-outbox
branch
from
September 1, 2026 19:27
c90981d to
0e7da14
Compare
… the timer maximum A failed pending-marker write no longer marks the session as locally managed, so the next persist retries it. A Retry-After beyond Node's ~24.8-day setTimeout maximum is capped there instead of overflowing to an immediate retry loop.
sethkarten
self-requested a review
September 3, 2026 15:16
sethkarten
approved these changes
Sep 3, 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.

Problem
Agent-trace upload progress lived only in process memory (ENG-5837's investigation). That one premise produced the whole defect class:
retry-afterslept a flat 60s insidefetchWithRetry, holding whoever awaited the upload.Shape
Upload scheduling is now a disk-cursor outbox owned entirely by
agent-traces.ts:<agentDir>/agent-traces-outbox/, named by a hash of the session-file path, each written atomically (tmp+rename). An entry holds the session-file path plus the last uploaded content signature{size, mtimeMs}, or no signature for "scheduled but never uploaded".(size, mtimeMs)is the same append-only-content signaturereadSessionInfocaching already trusts. Per-entry files make cross-process lost updates and wipe-on-corrupt-read structurally impossible: concurrent writers touch different files (or write equivalent content to the same one), a bad read costs only its own entry, and pruning deletes one file.unref()ed: telemetry never keeps the process alive.catchUpAgentTraceUploads): iterates the outbox entries, prunes entries whose file is gone (deleted drafts/subagent catalogs), skips files owned by a live controller in this process, and sequentially uploads anything ahead of its cursor through the existing 5-per-minute request gate.failed(429)and the controller re-arms its timer at max(60s since the last start, the server's advertisedretry-after). Nothing ever sleeps inside a caller, and a fresh persist cannot re-arm inside an advertised window. In-requestretry-afteron 503 is still honored. A successful PUT whose cursor write fails returns a retryable failure instead of silently keeping a stale cursor (the PUT is idempotent; a resend is harmless).flushAgentTraceUpload,detachTraceFlush, and fix(coding-agent): don't block RLM subagent deletion on trace upload or a doomed kernel snapshot #1954's exit barrier (flushAllPendingAgentTraceUploads+ its four try/finally call sites in daemon shutdown, update restart, worker archive-and-shutdown, and in-process connection dispose) are deleted. A pending upload of a closed session fires from its still-armed timer, or from the next process start's catch-up.The cursor mechanism is deliberately file-kind-agnostic (path-keyed, "content ahead of cursor -> upload -> advance"); only the endpoint/payload construction in
performAgentTraceUploadis transcript-specific, so a second append-only file kind (e.g. the deferred semantic-edges ledger from #1885) can reuse the scheduling/catch-up/pruning later without a registry being built today.Ownership decision
The catch-up runs once per process, triggered by the first
installAgentTraceUploadcall — i.e. in whichever process actually hosts sessions (daemon workers, interactive/print/rpc processes). That is the only place trace upload is enabled and configured today (theauthStorage+settingsManagerpair from session services); the daemon supervisor hosts no sessions and stays trace-ignorant. Cross-worker duplication is bounded by the cursor compare + idempotent PUT.Server-semantics evidence (wire format unchanged)
Same endpoint, same auth, same full-file body; zero server-side changes. Evidence the server overwrites by session id: the method is
PUT(replace semantics) on/api/v1/agent-traces/sessions/<sessionId>;mainalready re-PUTs the entire raw session JSONL for the same id every <=60s while a session is active and the server answers each withbytes_storedfor the full body; the existing test pinsbody == readFileSync(sessionFile). The cursor only decides when an upload is due.Supersedes (#1954 overlap)
Builds on #1954 and deletes its interim exit-drain machinery — the startup catch-up makes exit draining unnecessary:
flushAllPendingAgentTraceUploads(+ 4 call sites),detachTraceFlush,logDetachedAgentTraceFlushFailure,flushAgentTraceUpload, and the barrier pins. Kept from #1954: thekernelSnapshot: falsedisposal threading (orthogonal) and its still-valid delete pins (delete never blocks, upload fires once, snapshot flags), reshaped onto the outbox scheduler.Numbers
Measured with the same drivers against the pre-#1954 in-memory scheduler (old) and this PR (new):
delete_subagentof a resident child with a pending upload (3s network)Honest note on volume: both schedulers are bounded by the same 60s min interval and the wire format is unchanged, so steady-state volume is similar by design; the new scheduler additionally coalesces while an upload is in flight and never re-POSTs unchanged content across restarts. The wins this PR is actually for are durability (crash loses nothing past the next startup) and the removal of all blocking.
The explicit
/traces uploadand/traces upload-allcommands intentionally bypass the cursor (an explicit command is a force re-send / escape hatch); only scheduled and catch-up uploads are cursor-gated.Pins (each fails on the old scheduler; verified by running them against the pre-rebase
origin/mainsources)unchangedwith zero POSTs; repeat catch-up uploads nothing — old: re-POSTs the full file (gotuploaded, 2 calls)delete_subagentwith a gated in-flight upload resolves immediately (kernelSnapshot: falsestill asserted) and the transcript upload completes afterward — old (pre-fix(coding-agent): don't block RLM subagent deletion on trace upload or a doomed kernel snapshot #1954): hangs on the gated fetch until the 20s test timeoutfailed(429)after exactly one attempt and the controller re-arms and retries on the next cycle without any caller waiting — old: retried in-request with 60s sleeps (test burned its 30s timeout)hasRef() === false) — fails withoutunref()Retry-Aftergates the next cycle (300s pin), and a fresh persist cannot re-arm earlier — fails when the header is droppeduploaded— fails on the swallowed cursor errorunchanged— fails on the single-map store (wipe + resend)Also reshaped: #1954's passivation-join delete pin (kept, minus the barrier drain), and its barrier pins deleted with the barrier.
Tests
packages/coding-agent: agent-traces (33), daemon-mode (183), agent-connection-in-process, agent-session-concurrent, ipython-provisioner all pass in a Prime sandbox; full suite is at the documented environmental baseline (82 failed / 14 files: extensions-, kernel--skill bridges, tools-EACCES-as-root, config, resource-loader, sdk-session-manager, agent-session-recursion, 4428, 4603 — identical list, no new failures). Rootnpm run checkclean.Linear: RES-1244
Note
Medium Risk
Changes telemetry durability and shutdown timing (traces may complete only after exit via catch-up), but wire format is unchanged and idempotent PUTs limit duplicate-send risk.
Overview
Replaces in-process trace upload draining with a durable
agent-traces-outbox/cursor so shutdown, session disposal, and subagent deletes no longer wait on telemetry. Upload intent and last-uploaded{size, mtimeMs}live in one hashed JSON entry per session; the firstinstallAgentTraceUploadper process runscatchUpAgentTraceUploadsto finish work from a prior crash and prune entries for deleted session files.Scheduled and catch-up uploads skip unchanged transcripts (
unchanged); explicit/traces uploadstill force-sends. 429 responses are not retried inside the HTTP client—the controller re-arms anunref()’d timer usingRetry-After(and the 60s min interval) instead of blocking callers. Successful PUTs advance the on-disk cursor; cursor write failures surface as retryable failures.Removes
flushAgentTraceUpload,flushAllPendingAgentTraceUploads, and detached flush hooks from runtime, daemon shutdown paths, in-process connection dispose, and RLM child updates. Interactive UI adds messaging forunchangeduploads.Reviewed by Cursor Bugbot for commit 2eb82f2. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Replace detached agent-trace flushes with disk-cursor outbox scheduling
AgentTraceUploadControllerschedules non-blocking uploads using unreferenced timers with debounce, throttle, andRetry-Afterrescheduling; content arriving mid-upload triggers a follow-up cycleRetry-AfterAgentTraceUploadControllertimer is unreferenced so it will not keep the process alive; cursor recording failure after a successful remote upload is reported as a failed result in agent-traces.tsMacroscope summarized 2eb82f2.