feat(coding-agent): deliver the semantic-edge ledger through the agent-traces outbox - #1984
Merged
Conversation
One opaque request ID on the wire (X-ACP-Lineage-Request-ID + Idempotency-Key, minted before the call and stable across retries of the same call), a durable append-only lineage.jsonl ledger per agent session beside its artifacts, context epochs driven by compaction outcomes, cross-process ancestry for spawned subagents, and deriveLineageManifest folding a session tree's ledgers into the verifiers lineage-v1 manifest. Derivation only; nothing publishes or reads the ledger yet.
… ledger repair Review round 2 on the lineage-v1 producer: retry Idempotency-Key reuse now requires a byte-identical body (sha256 of model + system prompt + messages) instead of a shape proxy; spawn ancestry is snapshotted synchronously at the spawn entry point and survives resume via ledger replay; child terminal status is recorded once at actual release (reusable children stay running across follow-up runs, startup failures record failed); ledger construction never mutates the file (torn-tail repair deferred to first append) and only a malformed unterminated final line counts as torn; the completed-compaction ledger event now precedes the transcript commit. Adds host pass-through, real-summary-call wire, negative validator calibration, replayed-status, double-wrap, and failed/cancelled mapping coverage.
Round 3 on the lineage-v1 producer. The retry body hash is now computed eagerly at request creation, before the wire call, so mutating the live message objects between the failed call and the retry park can no longer alias two different bodies under one Idempotency-Key (TOCTOU regression test included; the lazy-thunk indirection is gone). Test hardening from the mutation campaign: hash keyed on provider/model/systemPrompt each proven independently; undefined-vs-undefined parked hashes never match; readLineageLedger proven read-only; torn-tail repair proven single-shot; spawn-ancestry snapshot proven to precede preflight awaits; the completed compaction ledger event proven durable before (and exactly once despite a failing) transcript commit. The invariant port checks compactions before requests and the negative calibration table asserts the named error per row, adding duplicate-context, session-owner, and per-predicate isolation rows.
readLineageLedger is proven to never open the ledger for writing (a missing path must throw ENOENT and stay missing; a zero-byte append-open would create it), and the calibration table gains a row where a fully valid child compaction is named by a root request, so only the request-side compaction owner check can reject it.
…edges-v1 The ecosystem replaced lineage-v1 (nano-rlm #153, verifiers #2449): one renamed correlation header (X-ACP-Model-Request-ID + Idempotency-Key) and sparse commit-gated semantic edges instead of a full session manifest. The durable per-session JSONL ledger keeps its hardened mechanics (ledger-before-wire, torn-vs-corrupt tails, repair-on-first-append, read-path purity, replay idempotence, eager body-hash retry identity, synchronous spawn snapshots) and now records request outcomes: the streamFn wrapper commits or fails each request when its stream resolves, compaction summaries commit around the real wire call only, and a parent claims a child_returned with the child's last committed request at the success point. deriveSemanticEdges is a pure order-independent fold to continuation/subagent_call/subagent_return/compaction edges: edges materialize only when their target commits, failed requests return their inbound edges to the session's pending set (prime-agent has no prompt rollback, so they attach to the next commit), spawn edges defer to the child's first committed request, and a completed compaction suppresses the summary's own continuation. Deleted: context epochs, session status recording, depth in ledger events, the manifest deriver, and the lineage-v1 validator port with its calibration table.
Two isolated compaction fold gates (a committed summary followed by a later commit, and a failed compaction after a committed summary, each producing no compaction edge), distinct wrapper outcome cases including a rejected stream promise and an aborted final message (both must fail the request), and the wire header asserted as the literal string X-ACP-Model-Request-ID so a renamed production constant cannot pass.
Aligns with nano-rlm a1d7856: a failed child still returns an error outcome the parent consumes, so the failure settle point now records child_returned with the child's last committed request. Cancelled runs and zero-commit children stay silent; the returned-once guard is unchanged. Tests cover the failed-with-commits return edge, failed zero-commit silence, and cancelled-with-commits silence.
…lt paths Review-bot round: the retry body hash now covers tools and the request-shaping stream options (reasoning, thinking budgets, temperature, max tokens, service tier); parking is forfeited entirely when a before_provider_request hook is registered, since payload hooks rewrite the wire body after the hash point; ledger write failures in the stream outcome observer and in compaction error paths are contained with a one-line warning instead of crashing or masking the original error (the completed-compaction ID is marked consumed before its write so a failed write propagates untouched); and side questions now use the unwrapped inner stream function, keeping their calls out of the session ledger and the continuation chain.
Split-turn compactions send two summary calls with different bodies; one shared Idempotency-Key gets rejected (or replayed) by key-honoring interceptors. compact() now runs every summary call through a host runner that mints its own request ID, headers, and finish/fail outcome, and the edge fold tracks all summary slices of a compaction, keying the compaction edge off whichever slice is the session's last commit. Earlier slices contribute ordinary continuation edges.
…wing Review-bot round 2, one mechanism for four findings: the first failed ledger operation (read at construction or any append) permanently disables the recorder with a single warning; every write becomes a no-op and the streamFn wrapper and compaction runner stop emitting request IDs on the wire, preserving ledger-before-wire instead of weakening it. This removes the scattered try/catch layers (recordOutcomeSafely and both compaction-path guards are deleted) and means an unwritable ledger can no longer break compaction, rewrite a successful child run to error, or fail a compaction whose summary succeeded. Appends are now durable before in-memory commit state advances, so a failed write never leaves a claimable commit. Aborted compaction summaries record request_failed rather than committing partial text into the chain. Compaction summary slices claim no pending edges and return none on failure (documented divergence from nano-rlm): pending defers to the post-compaction turn, the request that actually consumes subagent results.
…slice A completed compaction as the session's final activity permanently lost deferred pending edges (subagent_return, reclaimed continuations): the slices claimed nothing and no post-compaction turn ever arrived. The fold now flushes still-pending edges to the compaction's last-committed slice at completion — the same request that sources the compaction edge — so pending always lands on a committed request whether or not the session continues, restoring nano-equivalent single-call semantics while staying deterministic for split turns. Failed or cancelled compactions still leave pending for the next turn.
…uations The terminal flush appended pending edges after the last summary slice had already emitted its regenerated continuation, so a reclaimed continuation from the same source derived twice — duplicate edges the consumer rejects. The flush now applies nano's source-only suppression: a pending edge from X removes the slice's generated continuation from X regardless of the pending edge's type, then pending flushes once.
# Conflicts: # packages/coding-agent/src/core/agent-session.ts
…-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.
… 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.
…to feat/acp-lineage-delivery
…mits and gate spawn attribution on an active run A split-turn summary slice that succeeded on the wire no longer publishes request_finished at stream resolution: slices settle at the compaction outcome, so a failed or cancelled compaction leaves no committed summary request and the next turn's continuation edge stays on the pre-compaction request. Spawn attribution now requires an active agent run; an out-of-band spawn records no spawned_by_request_id instead of a stale one.
…traces outbox The per-session semantic-edges.jsonl becomes a second outbox kind: durable kind-tagged intent at the first transcript persist, an append-only byte cursor (uploadedBytes) whose unchanged files are never re-counted, catch-up inclusion after a kill, and pruning when the ledger is deleted with its session. No delivery endpoint exists yet (verifiers#2449 consumes edges in-band over ACP metadata; the trace server has no semantic-edges route), so pending ledgers are counted, never sent, and the cursor stays untouched for the first real sender. Unknown outbox kinds from newer builds are left alone. The ledger path derivation is shared between the recorder and the outbox registration.
snimu
marked this pull request as ready for review
September 2, 2026 11:08
…er path changes A controller update that changes semanticEdgesLedgerPath resets the intent memo, so a reused SessionManager registers the new ledger at its next persist instead of silently dropping it from catch-up. Also bullet-prefix the changelog fragment.
sethkarten
self-requested a review
September 3, 2026 15:22
sethkarten
previously approved these changes
Sep 3, 2026
…very # Conflicts: # packages/coding-agent/src/core/agent-session.ts # packages/coding-agent/src/core/agent-traces.ts # packages/coding-agent/src/core/semantic-edges.ts # packages/coding-agent/test/agent-traces.test.ts
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 69ef139. Configure here.
…p the ledger intent latch Outbox entries (transcript and semantic-edges alike) are only created while trace sharing is enabled: an entry recorded during an opted-out era would turn a later enable into retroactive collection. The ledger intent memo is deleted rather than patched — marking re-runs every persist behind an existsSync short-circuit, so an entry pruned by a racing catch-up is re-registered at the next persist, and the path-change reset it needed disappears with it.
sethkarten
self-requested a review
September 3, 2026 17:03
sethkarten
approved these changes
Sep 3, 2026
This was referenced 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.

The producer records causality in per-session semantic-edges ledgers; this PR is the groundwork for the shipper — the component that will get that recorded truth to the trace server. It registers each ledger with the agent-traces outbox as a second, kind-tagged delivery kind, and it deliberately sends nothing: no endpoint exists. The trace server exposes only a transcript route (
PUT /api/v1/agent-traces/sessions/{id}), and the semantic-edges contract is consumed in-band over ACP response metadata — there is no file-upload channel to ship to. So this PR lands everything the real sender will need, keeps the cursor untouched, and the first real sender delivers the whole backlog. Everything sits behind the existingagentTracesenablement.What is in place:
{ sessionFile, kind: "semantic-edges" }outbox entry, path-keyed in the same per-entry cursor store the transcript kind usesuploadedBytes): a ledger whose size equals its cursor is unchanged and is never re-counted or resentsemanticEdgeLedgersPending) and prunes entries whose ledger was deleted with its session — the same rules the transcript kind followssemanticEdgeLedgerPath()is shared by the recorder (writer) and the outbox registration (reader)Stacked draft — do not merge until both land, then retarget to main: the base branch is the semantic-edges producer (#1885,
feat/acp-lineage-v1), and the agent-traces outbox branch (#1957,refactor/agent-traces-outbox) is merged into this branch unchanged — no commits here touch its behavior.Validation: new pins in
agent-traces.test.ts, both verified fail-unfixed — kind-tagged durable intent at persist; catch-up counts appended bytes, stays quiet at the cursor, and prunes deleted ledgers. One existing outboxtoEqualshape updated for the newsemanticEdgeLedgersPendingcounter (twice, same literal). Suites green: agent-traces (41), semantic-edges (52), agent-session-semantic-edges (22), agent-session-services, daemon-mode (184), agent-connection-in-process — 315 tests, 0 failures. Fullnpm run check(biome, tsgo, installer render, browser smoke) passes.Linear: RES-1245 https://linear.app/primeintellect/issue/res-1245 (producer), RES-1244 https://linear.app/primeintellect/issue/res-1244 (outbox)
Note
Register semantic-edge ledger in agent-traces outbox with kind-tagged durable intents
semanticEdgeLedgerPathresolver in semantic-edges.ts and wires it into session construction (agent-session.ts) and trace-upload installation (agent-traces.ts).SEMANTIC_EDGES_OUTBOX_KINDso outbox entries carry akinddiscriminator anduploadedBytescursor;parseOutboxEntryandmarkAgentTraceOutboxPendingSyncare extended to read/write these fields while remaining compatible with existing untagged trace entries.catchUpAgentTraceUploadsnow counts pending semantic-edge ledgers, prunes missing or non-file ledger intents, and leaves undelivered cursors untouched since no delivery endpoint exists yet.AgentTraceUploadController.schedulesynchronously writes a kind-tagged outbox intent for the configured ledger path without issuing any network request;updateresets the per-path intent marker when the ledger path changes.AgentTraceCatchUpResultgains asemanticEdgeLedgersPendingfield; existing callers and tests must account for it. Outbox entry format is extended but backward-compatible with untagged entries.Changes since #1984 opened
settingsManager.getAgentTracesEnabled()guard withinAgentTraceUploadController.schedulemethod, removed memoization gating for ledger intent marking to allow re-registration on every persist when enabled, and removed theledgerIntentMarkedfield and its conditional reset logic fromAgentTraceUploadController.updatemethod [22606d5]Macroscope summarized 69ef139.
Note
Medium Risk
Changes agent-traces outbox persistence and consent gating alongside trace sharing; ledger delivery is stubbed but new durable intents affect privacy and restart catch-up behavior.
Overview
This PR wires per-session semantic-edge ledgers into the existing agent-traces outbox as a separate delivery kind (
semantic-edges), without sending ledger data yet.A shared
semanticEdgeLedgerPath()keeps the recorder and trace-upload install aligned on disk location. On each transcript persist (only whenagentTracessharing is enabled), the upload controller writes durable outbox intents for both the session JSONL and the ledger; disabled sharing writes no intents so later opt-in cannot backfill opted-out sessions.Outbox entries gain optional
kindand an append-onlyuploadedBytescursor for ledgers. StartupcatchUpAgentTraceUploadsprunes missing ledgers, skips unknown kinds, and incrementssemanticEdgeLedgersPendingwhen ledger size exceeds the cursor—no HTTP upload until a delivery route exists. Transcript upload behavior is unchanged aside from consent gating on outbox marking.Reviewed by Cursor Bugbot for commit 22606d5. Bugbot is set up for automated code reviews on this repo. Configure here.