Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
e4fba20
feat(coding-agent): ACP lineage-v1 provenance producer
snimu Aug 28, 2026
d7e9a41
fix(coding-agent): harden lineage retry identity, spawn ancestry, and…
snimu Aug 28, 2026
cd18572
fix(coding-agent): eager lineage body hash and mutation-hardened tests
snimu Aug 28, 2026
e66d91e
test(coding-agent): kill the last two lineage mutants
snimu Aug 28, 2026
06b80dc
feat(coding-agent)!: convert the provenance producer to ACP semantic-…
snimu Aug 28, 2026
1e531cb
test(coding-agent): kill the five surviving semantic-edge mutants
snimu Aug 28, 2026
9f62820
feat(coding-agent): failed subagent runs also return their last commit
snimu Aug 28, 2026
fdfd5fa
fix(coding-agent): harden semantic-edge retry identity and ledger fau…
snimu Aug 28, 2026
7f8c934
fix(coding-agent): distinct request identity per split-turn summary call
snimu Aug 28, 2026
4f53497
merge: main into feat/acp-lineage-v1 (kernel protocol 2)
snimu Aug 28, 2026
5b9c8fb
fix(coding-agent): degrade the semantic-edge recorder instead of thro…
snimu Aug 29, 2026
d066a75
fix(coding-agent): flush pending edges to the last-committed summary …
snimu Aug 29, 2026
82241bc
fix(coding-agent): dedupe the terminal flush against generated contin…
snimu Aug 29, 2026
3b44c95
Merge main into local-acp-lineage-v1
snimu Aug 31, 2026
3c654e6
refactor(coding-agent): reschedule agent-trace uploads through a disk…
snimu Sep 1, 2026
0e7da14
fix(coding-agent): make the trace outbox per-entry, durable at persis…
snimu Sep 1, 2026
2eb82f2
fix(coding-agent): retry failed intent markers and cap Retry-After at…
snimu Sep 2, 2026
3e473f3
Merge remote-tracking branch 'origin/main' into feat/acp-lineage-v1
snimu Sep 2, 2026
ec01ea1
Merge remote-tracking branch 'origin/refactor/agent-traces-outbox' in…
snimu Sep 2, 2026
64ef1b6
fix(coding-agent): commit summary slices only when the compaction com…
snimu Sep 2, 2026
856a088
Merge branch 'feat/acp-lineage-v1' into feat/acp-lineage-delivery
snimu Sep 2, 2026
2b27521
feat(coding-agent): register the semantic-edge ledger with the agent-…
snimu Sep 2, 2026
2ac123e
fix(coding-agent): re-register the ledger intent when the outbox ledg…
snimu Sep 2, 2026
69ef139
Merge remote-tracking branch 'origin/main' into feat/acp-lineage-deli…
snimu Sep 3, 2026
22606d5
fix(coding-agent): consent-gate outbox intent at persist time and dro…
snimu Sep 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Registered the per-session semantic-edge ledger with the agent-traces outbox as its own kind-tagged entry: durable upload intent at persist, an append-only byte cursor that never re-counts unchanged ledgers, startup catch-up counting, and pruning when a ledger is deleted with its session. No delivery endpoint exists yet, so pending ledgers are counted but never sent.
5 changes: 5 additions & 0 deletions packages/coding-agent/src/core/agent-session-services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { ModelRegistry } from "./model-registry.js";
import { DefaultResourceLoader, type DefaultResourceLoaderOptions, type ResourceLoader } from "./resource-loader.js";
import type { SubagentRuntimeHost } from "./rlm-runtime.js";
import { type CreateAgentSessionResult, createAgentSession } from "./sdk.js";
import { semanticEdgeLedgerPath } from "./semantic-edges.js";
import type { SessionManager } from "./session-manager.js";
import { SettingsManager } from "./settings-manager.js";
import { installAgentTelemetry, isTelemetryEnabled } from "./telemetry.js";
Expand Down Expand Up @@ -225,6 +226,10 @@ export async function createAgentSessionFromServices(
installAgentTraceUpload(options.sessionManager, {
authStorage: options.services.authStorage,
settingsManager: options.services.settingsManager,
semanticEdgesLedgerPath: semanticEdgeLedgerPath({
rlmSessionDir: options.rlmSessionDir,
sessionArtifactDir: options.sessionManager.getSessionArtifactDir(),
}),
});
const result = await createAgentSession({
cwd: options.services.cwd,
Expand Down
8 changes: 5 additions & 3 deletions packages/coding-agent/src/core/agent-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,8 +233,8 @@ import {
} from "./rlm-runtime.js";
import {
modelRequestHeaders,
SEMANTIC_EDGES_LEDGER_FILENAME,
SemanticEdgeRecorder,
semanticEdgeLedgerPath,
wrapStreamFnWithSemanticEdges,
} from "./semantic-edges.js";
import {
Expand Down Expand Up @@ -1284,9 +1284,11 @@ export class AgentSession {
this._rlmSessionDir = config.rlmSessionDir;
this._rlmParentNodeId = config.rlmParentNodeId;
this._rlmParentAgent = config.rlmParentAgent;
const semanticEdgesDir = this._rlmSessionDir ?? this.sessionManager.getSessionArtifactDir();
this._semanticEdges = new SemanticEdgeRecorder({
ledgerPath: semanticEdgesDir ? join(semanticEdgesDir, SEMANTIC_EDGES_LEDGER_FILENAME) : undefined,
ledgerPath: semanticEdgeLedgerPath({
rlmSessionDir: this._rlmSessionDir,
sessionArtifactDir: this.sessionManager.getSessionArtifactDir(),
}),
sessionId: this.sessionManager.getSessionId(),
parentSessionId: config.semanticParentSessionId,
spawnedByRequestId: config.semanticSpawnedByRequestId,
Expand Down
84 changes: 74 additions & 10 deletions packages/coding-agent/src/core/agent-traces.ts
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ export interface AgentTraceUploadInstallOptions {
configPath?: string;
fetchFn?: typeof fetch;
requestTimeoutMs?: number;
/** The session's semantic-edge ledger; registered with the outbox as its own delivery kind. */
semanticEdgesLedgerPath?: string;
}

export type AgentTracePreviewResult =
Expand Down Expand Up @@ -639,8 +641,12 @@ interface AgentTraceUploadedSignature {
mtimeMs: number;
}

export const SEMANTIC_EDGES_OUTBOX_KIND = "semantic-edges";

export interface AgentTraceCatchUpResult {
pruned: number;
/** Registered semantic-edge ledgers with bytes beyond their cursor; no delivery endpoint exists yet. */
semanticEdgeLedgersPending: number;
results: Array<{ sessionFile: string; result: AgentTraceUploadResult }>;
}

Expand All @@ -654,9 +660,14 @@ function agentTraceOutboxEntryPath(sessionFile: string): string {
return join(getAgentTraceOutboxDir(), `${key}.json`);
}

function parseOutboxEntry(
raw: string,
): { sessionFile: string; uploaded: AgentTraceUploadedSignature | null } | undefined {
function parseOutboxEntry(raw: string):
| {
sessionFile: string;
kind?: string;
uploaded: AgentTraceUploadedSignature | null;
uploadedBytes?: number;
}
| undefined {
const parsed = parseResponseObject(raw);
if (!parsed || typeof parsed.sessionFile !== "string") {
return undefined;
Expand All @@ -665,7 +676,12 @@ function parseOutboxEntry(
typeof parsed.size === "number" && typeof parsed.mtimeMs === "number"
? { size: parsed.size, mtimeMs: parsed.mtimeMs }
: null;
return { sessionFile: parsed.sessionFile, uploaded };
return {
sessionFile: parsed.sessionFile,
kind: typeof parsed.kind === "string" ? parsed.kind : undefined,
uploaded,
uploadedBytes: typeof parsed.uploadedBytes === "number" ? parsed.uploadedBytes : undefined,
};
}

/** `undefined` = no usable cursor; `null` = scheduled but never uploaded. */
Expand All @@ -688,15 +704,19 @@ function signatureEquals(a: AgentTraceUploadedSignature | null | undefined, b: A
const locallyManagedSessionFiles = new Set<string>();

/** Best-effort and synchronous: upload intent must be on disk the moment the transcript persist returns. */
function markAgentTraceOutboxPendingSync(sessionFile: string): boolean {
function markAgentTraceOutboxPendingSync(sessionFile: string, kind?: string): boolean {
try {
const entryPath = agentTraceOutboxEntryPath(sessionFile);
if (existsSync(entryPath)) {
return true;
}
mkdirSync(getAgentTraceOutboxDir(), { recursive: true });
const tempPath = `${entryPath}.${process.pid}.${randomUUID()}.tmp`;
writeFileSync(tempPath, `${JSON.stringify({ sessionFile })}\n`, "utf8");
writeFileSync(
tempPath,
`${JSON.stringify(kind === undefined ? { sessionFile } : { sessionFile, kind })}\n`,
"utf8",
);
renameSync(tempPath, entryPath);
return true;
} catch {
Expand Down Expand Up @@ -724,7 +744,7 @@ async function recordAgentTraceOutboxUpload(
export async function catchUpAgentTraceUploads(
options: Omit<AgentTraceUploadOptions, "sessionFile">,
): Promise<AgentTraceCatchUpResult> {
const catchUp: AgentTraceCatchUpResult = { pruned: 0, results: [] };
const catchUp: AgentTraceCatchUpResult = { pruned: 0, semanticEdgeLedgersPending: 0, results: [] };
if (options.requireEnabled !== false && !(await getAgentTracesEnabled(options))) {
return catchUp;
}
Expand Down Expand Up @@ -756,6 +776,36 @@ export async function catchUpAgentTraceUploads(
catchUp.pruned += 1;
continue;
}
if (entry.kind === SEMANTIC_EDGES_OUTBOX_KIND) {
let ledgerStats: Awaited<ReturnType<typeof stat>>;
try {
ledgerStats = await stat(entry.sessionFile);
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
await unlink(entryPath).catch(() => undefined);
catchUp.pruned += 1;
}
continue;
}
if (!ledgerStats.isFile()) {
await unlink(entryPath).catch(() => undefined);
catchUp.pruned += 1;
continue;
}
// Append-only byte cursor: a ledger whose size equals its delivered offset has nothing new.
if (ledgerStats.size === entry.uploadedBytes) {
continue;
}
// No delivery endpoint exists yet (verifiers#2449 consumes edges in-band over ACP
// metadata; the trace server has no semantic-edges route). The delta and cursor stay
// untouched so the first real sender delivers the whole backlog.
catchUp.semanticEdgeLedgersPending += 1;
continue;
Comment thread
snimu marked this conversation as resolved.
}
if (entry.kind !== undefined) {
// A newer build may register kinds this one cannot deliver; leave their cursors alone.
continue;
}
if (locallyManagedSessionFiles.has(entry.sessionFile)) {
continue;
}
Expand Down Expand Up @@ -1030,9 +1080,23 @@ class AgentTraceUploadController {

schedule = (): void => {
this.pending = true;
const sessionFile = this.sessionManager.getSessionFile();
if (sessionFile && !locallyManagedSessionFiles.has(sessionFile) && markAgentTraceOutboxPendingSync(sessionFile)) {
locallyManagedSessionFiles.add(sessionFile);
// Intent is consent-gated at persist time: an entry created while sharing
// is off would turn a later enable into retroactive collection of
// opted-out sessions. Marking re-runs every persist (existsSync-cheap),
// so an entry pruned by a racing catch-up is re-registered.
if (this.options.settingsManager.getAgentTracesEnabled()) {
const sessionFile = this.sessionManager.getSessionFile();
if (
sessionFile &&
!locallyManagedSessionFiles.has(sessionFile) &&
markAgentTraceOutboxPendingSync(sessionFile)
) {
locallyManagedSessionFiles.add(sessionFile);
}
const ledgerPath = this.options.semanticEdgesLedgerPath;
if (ledgerPath) {
markAgentTraceOutboxPendingSync(ledgerPath, SEMANTIC_EDGES_OUTBOX_KIND);
}
}
this.arm();
};
Expand Down
11 changes: 10 additions & 1 deletion packages/coding-agent/src/core/semantic-edges.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createHash, randomUUID } from "node:crypto";
import { appendFileSync, existsSync, mkdirSync, readFileSync, truncateSync } from "node:fs";
import { dirname } from "node:path";
import { dirname, join } from "node:path";
import type { StreamFn } from "@earendil-works/pi-agent-core";

/**
Expand Down Expand Up @@ -94,6 +94,15 @@ export interface SemanticEdge {
type: SemanticEdgeType;
}

/** The one derivation of where a session's ledger lives; recorder and outbox must agree. */
export function semanticEdgeLedgerPath(options: {
rlmSessionDir?: string;
sessionArtifactDir?: string;
}): string | undefined {
const dir = options.rlmSessionDir ?? options.sessionArtifactDir;
return dir ? join(dir, SEMANTIC_EDGES_LEDGER_FILENAME) : undefined;
}

export function modelRequestHeaders(requestId: string): Record<string, string> {
return {
[MODEL_REQUEST_ID_HEADER]: requestId,
Expand Down
Loading
Loading