Skip to content
Merged
5 changes: 2 additions & 3 deletions docs/IMPLEMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,8 @@ src/
index.ts Session lifecycle
state.ts RunState JSON save/load
compactor.ts Context compactor
summarizer.ts Model-backed structured compaction summary (fails closed)
summary-excerpt.ts Token-budgeted archive excerpt for the summary call
compaction-archive.ts Primary-only authorized evidence archive (post-policy capture)
summarizer.ts Model-backed structured compaction summary (fails closed) + token-budgeted archive excerpt
compaction-archive.ts Primary-only authorized evidence archive (post-policy capture) + archive:// occurrence refs
compaction-archive-schema.ts Archive occurrence / completeness certificate schemas
run-sink.ts Run-level event sink
stream-consumer.ts Async stream consumer with error handling
Expand Down
2 changes: 1 addition & 1 deletion docs/TELEMETRY.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ recorded.

`auth_provider` is a separate property for that reason: it names which
provider's sign-in was rejected (`codex`, `xai`, `anthropic`, `other`),
chosen from a fixed first-party set in `src/tui/session-chrome.ts`. No
chosen from a fixed first-party set in `src/tui/chrome-state.ts`. No
part of the provider's rejection message is sent.

The mapping is `src/telemetry/classify.ts`, and the tests that feed each
Expand Down
2 changes: 1 addition & 1 deletion src/plugins/evidence-archive-search-plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import {
advertiseArchiveSurface,
evidenceArchiveSearchPlugin,
} from "./evidence-archive-search-plugin.js";
import { formatArchiveRef } from "../session/archive-uri.js";
import {
createCompactionArchive,
formatArchiveRef,
type CompactionArchive,
} from "../session/compaction-archive.js";
import { CATALOG_TOOL_NAMES, CORE_TOOL_NAMES } from "../agent/tool-search.js";
Expand Down
2 changes: 1 addition & 1 deletion src/plugins/evidence-archive-search-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
formatArchiveRef,
isArchiveLike,
parseArchiveTarget,
} from "../session/archive-uri.js";
} from "../session/compaction-archive.js";

const SEARCH_DEFAULT_MAX = 1000;
const GREP_DEFAULT_MAX = 500;
Expand Down
2 changes: 1 addition & 1 deletion src/plugins/path-escape-plugin.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { resolve } from "node:path";
import type { ToolPlugin } from "@intx/tools-posix";
import { isToolOutputLike } from "../util/tool-output-uri.js";
import { isArchiveLike } from "../session/archive-uri.js";
import { isArchiveLike } from "../session/compaction-archive.js";
import { resolveWorkspacePath } from "../permission/path-restriction.js";
import type { RootsProvider } from "../permission/worktree-roots.js";

Expand Down
26 changes: 0 additions & 26 deletions src/session/archive-uri.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
isArchiveLike,
parseArchiveRef,
parseArchiveTarget,
} from "./archive-uri.js";
} from "./compaction-archive.js";

describe("archive URI", () => {
test("formats and parses archive:/// occurrence refs", () => {
Expand Down
36 changes: 36 additions & 0 deletions src/session/compaction-archive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -925,3 +925,39 @@ export function wrapCompactorWithCompletenessGate(
},
};
}

// ---------------------------------------------------------------------------
// Archive occurrence refs (pure addressing)
// ---------------------------------------------------------------------------
// The archive's own addressing scheme: `archive:///<occurrenceId>` refs name
// where an occurrence's payload bytes live. Kept on the archive module so the
// URI scheme and the store that honors it cannot drift apart.

/** Prefix for every archive target, including the bare `archive:///` root. */
export const ARCHIVE_URI_PREFIX = "archive:";
const ARCHIVE_URI_CANONICAL = "archive:///";

/** Render the canonical ref for an occurrence id. */
export function formatArchiveRef(occurrenceId: string): string {
return `${ARCHIVE_URI_CANONICAL}${occurrenceId}`;
}

export function isArchiveLike(path: string): boolean {
return path.startsWith(ARCHIVE_URI_PREFIX);
}

/** Accept archive:///occ-… and common slashes; return the occurrence id or undefined. */
export function parseArchiveRef(value: string): string | undefined {
return parseArchiveTarget(value)?.occurrenceId;
}

/** Root `archive:///` has no occurrenceId; a ref includes one. */
export function parseArchiveTarget(
value: string,
): { occurrenceId?: string } | undefined {
if (!isArchiveLike(value)) return undefined;
const rest = value.slice(ARCHIVE_URI_PREFIX.length).replace(/^\/+/, "");
const occurrenceId = rest.split(/[/?#]/)[0] ?? "";
if (occurrenceId.length === 0) return {};
return { occurrenceId };
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { expect, test } from "bun:test";
import type { ArchiveOccurrence } from "./compaction-archive-schema.js";
import { buildArchiveSummaryExcerpt } from "./summary-excerpt.js";
import { buildArchiveSummaryExcerpt } from "./summarizer.js";

function occ(
partial: Pick<ArchiveOccurrence, "occurrenceId" | "kind"> &
Expand Down
103 changes: 100 additions & 3 deletions src/session/summarizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,110 @@ import {
import { LOG_NAMESPACE_ROOT } from "../branding.js";
import { NOOP_TELEMETRY, type Telemetry } from "../telemetry/index.js";
import {
buildArchiveSummaryExcerpt,
type SummaryExcerptArchive,
} from "./summary-excerpt.js";
formatArchiveRef,
type CompactionArchive,
} from "./compaction-archive.js";
import type {
ArchiveKind,
ArchiveOccurrence,
} from "./compaction-archive-schema.js";
import { readSourceCredentialMaterial } from "../config/source-credentials.js";

const logger = getLogger([LOG_NAMESPACE_ROOT, "session", "summarizer"]);

// Token-budgeted compaction excerpt from the evidence archive.
//
// The live transcript is a clipped view. The archive holds the authorized
// payloads compaction is about to drop, so the summary call should read those
// rather than 400-character stubs. Budget is the control: later kinds yield
// when earlier ones fill the window. Gap rows contribute metadata only.
export const SUMMARY_EXCERPT_DEFAULT_BUDGET_CHARS = 80_000;

const KIND_PRIORITY: readonly ArchiveKind[] = [
"user_message",
"attachment",
"assistant_text",
"tool_args",
"tool_failure",
"tool_result",
"overflow_blob",
];

export type SummaryExcerptArchive = Pick<
CompactionArchive,
"listOccurrences" | "readAuthorizedPayload"
>;

function heading(occ: ArchiveOccurrence): string {
const parts = [`### ${occ.kind} ${formatArchiveRef(occ.occurrenceId)}`];
if (occ.callId !== undefined) parts.push(`call=${occ.callId}`);
if (occ.lifecycle !== undefined) parts.push(`lifecycle=${occ.lifecycle}`);
if (occ.gap === true) parts.push("[gap]");
return parts.join(" ");
}

/**
* Build a budgeted, kind-prioritized excerpt for the compaction summary call.
* Empty archives return "" so the caller can fall back to the live transcript.
*/
export async function buildArchiveSummaryExcerpt(
archive: SummaryExcerptArchive,
budgetChars = SUMMARY_EXCERPT_DEFAULT_BUDGET_CHARS,
): Promise<string> {
const occurrences = await archive.listOccurrences();
if (occurrences.length === 0) return "";

const byKind = new Map<ArchiveKind, ArchiveOccurrence[]>();
for (const occ of occurrences) {
const list = byKind.get(occ.kind);
if (list !== undefined) list.push(occ);
else byKind.set(occ.kind, [occ]);
}

const sections: string[] = [];
let used = 0;
let omitted = 0;

for (const kind of KIND_PRIORITY) {
const group = byKind.get(kind);
if (group === undefined) continue;
for (const occ of group) {
const remaining = budgetChars - used;
if (remaining <= 0) {
omitted++;
continue;
}

let body: string | undefined;
if (occ.gap === true) {
body = "(payload not stored)";
} else {
try {
body = await archive.readAuthorizedPayload(occ.occurrenceId);
} catch {
omitted++;
continue;
}
}

const section = `${heading(occ)}\n${body}`;
const separator = sections.length > 0 ? 2 : 0;
if (section.length + separator > remaining) {
omitted++;
continue;
}
sections.push(section);
used += section.length + separator;
}
}

const excerpt = sections.join("\n\n");
if (omitted === 0) return excerpt;
const note = `${omitted} occurrence${omitted === 1 ? "" : "s"} omitted`;
if (excerpt.length === 0) return note;
return `${excerpt}\n\n${note}`;
}

// What the agent was doing when compaction fired. Lets the summary preserve
// the workflow contract ("we are at step 3/7 of /build") rather than dropping
// it into the compacted region.
Expand Down
100 changes: 0 additions & 100 deletions src/session/summary-excerpt.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
resolveTurnLabel,
sendFailureText,
shouldSettleUiAfterSendFailure,
} from "./session-chrome.js";
} from "./chrome-state.js";

// The load-bearing guarantee: whatever tool identifier, MCP server name, or
// plugin name the runtime hands us, the rendered ticker string must land in
Expand Down
Loading
Loading