Skip to content

feat(coding-agent): ACP semantic-edges-v1 provenance producer - #1885

Open
snimu wants to merge 14 commits into
mainfrom
feat/acp-lineage-v1
Open

feat(coding-agent): ACP semantic-edges-v1 provenance producer#1885
snimu wants to merge 14 commits into
mainfrom
feat/acp-lineage-v1

Conversation

@snimu

@snimu snimu commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add an ACP semantic-edges-v1 provenance producer to the coding agent: every provider model call carries one opaque correlation ID in X-ACP-Model-Request-ID and Idempotency-Key (same value), and the recursive execution graph is recorded in a durable per-session ledger
  • record events in an append-only semantic-edges.jsonl beside the session's RLM artifacts: session_registered, request_started, request_finished, request_failed, compaction_begun, compaction_finished, child_returned
  • events are written before the action they describe: a provider can never observe a request ID that is absent from the ledger
  • derive the sparse edge set (continuation, subagent_call, subagent_return, compaction) on demand with a pure, order-independent fold over any set of per-session ledgers — edges publish only after their target request commits
  • reuse one request ID across provider-SDK retries for free and across session-level auto-retries only when the request body is identical (eager sha256 over provider, model, system prompt, and messages), so one Idempotency-Key never covers two different bodies
  • propagate spawn ancestry (parent session, spawning request ID) through all three subagent hosts (inline, runtime, daemon), snapshotted synchronously at the spawn entry point and restored across parent resume
  • tolerate crash windows: torn final ledger lines are repaired on first append only, reads never mutate the file, and an in-flight request at a torn tail derives zero edges

The contract matches the producer in nano-rlm PR #153 and the consumer in verifiers PR #2449 (ai.prime.acp/semantic-edges-v1). No publication channel is included: this PR produces the ledger and the derivation; delivery (ACP metadata, traces upload) is follow-up work. Existing provenance (X-Trace-Id, agent traces) is untouched.

Linear: ENG-5686

Merge dependency

Merge after PrimeIntellect-ai/verifiers#2449 (which pins nano-rlm #153); the wire contract this PR produces is defined there.

Validation

  • new suites: test/semantic-edges.test.ts (unit: recorder, fold, wrapper) and test/agent-session-semantic-edges.test.ts (integration: headers, retry identity, compaction, spawn ancestry across hosts, child return)
  • host coverage: runtime and daemon subagent hosts assert ancestry pass-through; inline host covered end to end
  • mutation-tested: 35 mechanics mutants (ledger ordering, retry identity, ancestry, torn-tail, epoch gating) plus 12 semantic-edge mutants (commit gating, pending return, spawn deferral, continuation dedupe, compaction gates, returned-once, fold order independence, wrapper failure paths, header name) all killed by named tests
  • full npm run check (biome, tsgo, installer render, browser smoke) and the compaction/recursion/retry regression suites pass

Note

Medium Risk
Changes core provider request headers, compaction ordering, and subagent spawn/return wiring; failures degrade to headerless calls rather than blocking sessions, but idempotency-key reuse and ledger-before-transcript ordering affect retry and compaction correctness if mis-implemented.

Overview
Adds an ACP semantic-edges-v1 producer that records model-call provenance without changing existing trace behavior. Each session writes an append-only semantic-edges.jsonl ledger next to its artifacts; SemanticEdgeRecorder logs registration, request start/finish/fail, compaction, and child-return events before the effects they describe.

AgentSession wraps the agent streamFn so normal turns mint one opaque ID on X-ACP-Model-Request-ID and Idempotency-Key, commit or fail when the stream settles, and reuse that ID on auto-retry only when the wire body hash matches (skipped when before_provider_request hooks can mutate payloads). Side questions unwrap the wrapper so they stay out of the chain.

Compaction routes summary provider calls through a SummaryCallRunner so split-turn summaries get distinct request IDs; completed compactions are ledger-finished before transcript append. RLM subagents receive semanticParentSessionId / semanticSpawnedByRequestId (snapshotted at spawn entry) across inline, runtime, and daemon hosts; parents record child_returned on successful or failed child runs with a last commit, not on cancel.

deriveSemanticEdges is a pure fold for commit-gated continuation, subagent_call, subagent_return, and compaction edges; nothing in-product reads or publishes the ledger yet. Ledger I/O failures disable provenance for the session while agent work continues.

Reviewed by Cursor Bugbot for commit 3b44c95. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Add semantic-edges-v1 provenance producer to coding agent

  • Introduces SemanticEdgeRecorder in semantic-edges.ts to durably log request, compaction, and subagent lifecycle events to an append-only JSONL ledger.
  • AgentSession wraps streamFn to attach request ID headers and record outcomes; auto-retries reuse the idempotency key when no payload-mutating extensions are installed.
  • Compaction in compaction.ts routes summary calls through a SummaryCallRunner callback to tag them with unique request IDs.
  • Subagent runtime creation forwards semanticParentSessionId and semanticSpawnedByRequestId to child sessions to track ancestry.
  • Behavioral Change: AgentSession wraps streamFn via wrapStreamFnWithSemanticEdges in agent-session.ts; side questions bypass it via unwrapSemanticEdgeStreamFn. If the ledger becomes unwritable, _disable stops all provenance logging and header attachment but operations continue.

Macroscope summarized 3b44c95.

snimu added 7 commits August 28, 2026 10:30
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.
Comment thread packages/coding-agent/src/core/agent-session.ts
Comment thread packages/coding-agent/src/core/agent-session.ts
Comment thread packages/coding-agent/src/core/agent-session.ts
Comment thread packages/coding-agent/src/core/semantic-edges.ts
Comment thread packages/coding-agent/src/core/agent-session.ts
Comment thread packages/coding-agent/src/core/agent-session.ts Outdated
…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.
Comment thread packages/coding-agent/src/core/agent-session.ts
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.
Comment thread packages/coding-agent/src/core/agent-session.ts Outdated
Comment thread packages/coding-agent/src/core/semantic-edges.ts Outdated

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

There are 3 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 4f53497. Configure here.

Comment thread packages/coding-agent/src/core/agent-session.ts
Comment thread packages/coding-agent/src/core/agent-session.ts Outdated
Comment thread packages/coding-agent/src/core/agent-session.ts
…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.
Comment thread packages/coding-agent/src/core/agent-session.ts
snimu added 3 commits August 29, 2026 10:29
…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
spawnCode?: string,
): Promise<RlmSpawnHandle> {
// Snapshot before any await: the spawning request is the turn whose tool call is executing now.
const spawnedByRequestId = this._semanticEdges.lastTurnRequestId;

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.

🟡 Medium core/agent-session.ts:10479

runRlmChild() records an idle/out-of-band child as spawned by the previous lastTurnRequestId, so the semantic ledger attributes that child to a completed model request that did not spawn it. Only capture the request ID while the parent is actively processing the spawning turn; otherwise pass undefined.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/coding-agent/src/core/agent-session.ts around line 10479:

`runRlmChild()` records an idle/out-of-band child as spawned by the previous `lastTurnRequestId`, so the semantic ledger attributes that child to a completed model request that did not spawn it. Only capture the request ID while the parent is actively processing the spawning turn; otherwise pass `undefined`.

Evidence trail:
packages/coding-agent/src/core/agent-session.ts:10473-10479,10560-10570,10862-10868 @ 3b44c9580af265fb36cbcd6d56ab318fe5a4865f
packages/coding-agent/src/core/semantic-edges.ts:205-238,436-464,577-594 @ 3b44c9580af265fb36cbcd6d56ab318fe5a4865f
packages/coding-agent/test/agent-session-semantic-edges.test.ts:321-365,371-391 @ 3b44c9580af265fb36cbcd6d56ab318fe5a4865f

if (signal.aborted) {
this._semanticEdges.failRequest(requestId);
} else {
this._semanticEdges.finishRequest(requestId);

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.

🟡 Medium core/agent-session.ts:7579

When either split-turn summary request rejects, _performCompaction marks the compaction as failed and returns while the other summaryCall can still later call finishRequest at line 7579. That records a successful request for a summary that was never committed to the transcript, so the next turn can inherit a continuation edge from an uncommitted compaction instead of the pre-compaction turn. Ensure both concurrent calls settle and invalidate/fail every request when the compaction fails before allowing any finishRequest to commit.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/coding-agent/src/core/agent-session.ts around line 7579:

When either split-turn summary request rejects, `_performCompaction` marks the compaction as failed and returns while the other `summaryCall` can still later call `finishRequest` at line 7579. That records a successful request for a summary that was never committed to the transcript, so the next turn can inherit a continuation edge from an uncommitted compaction instead of the pre-compaction turn. Ensure both concurrent calls settle and invalidate/fail every request when the compaction fails before allowing any `finishRequest` to commit.

Evidence trail:
packages/coding-agent/src/core/compaction/compaction.ts:700-730 @ 3b44c958
packages/coding-agent/src/core/agent-session.ts:7561-7618 @ 3b44c958
packages/coding-agent/src/core/semantic-edges.ts:250-284, 448-517 @ 3b44c958

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.

1 participant