Skip to content

Commit 50a6b16

Browse files
committed
Merge pull request #1036 from corbitsdev/cl-7953-collapse-doubled-session-chrome-delivery-and-compaction
# Conflicts: # src/tui/runtime-bridge.test.ts
2 parents af62702 + 4a633bb commit 50a6b16

23 files changed

Lines changed: 433 additions & 427 deletions

docs/IMPLEMENTATION.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,8 @@ src/
7979
index.ts Session lifecycle
8080
state.ts RunState JSON save/load
8181
compactor.ts Context compactor
82-
summarizer.ts Model-backed structured compaction summary (fails closed)
83-
summary-excerpt.ts Token-budgeted archive excerpt for the summary call
84-
compaction-archive.ts Primary-only authorized evidence archive (post-policy capture)
82+
summarizer.ts Model-backed structured compaction summary (fails closed) + token-budgeted archive excerpt
83+
compaction-archive.ts Primary-only authorized evidence archive (post-policy capture) + archive:// occurrence refs
8584
compaction-archive-schema.ts Archive occurrence / completeness certificate schemas
8685
run-sink.ts Run-level event sink
8786
stream-consumer.ts Async stream consumer with error handling

docs/TELEMETRY.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ recorded.
107107

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

113113
The mapping is `src/telemetry/classify.ts`, and the tests that feed each

src/plugins/evidence-archive-search-plugin.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ import {
88
advertiseArchiveSurface,
99
evidenceArchiveSearchPlugin,
1010
} from "./evidence-archive-search-plugin.js";
11-
import { formatArchiveRef } from "../session/archive-uri.js";
1211
import {
1312
createCompactionArchive,
13+
formatArchiveRef,
1414
type CompactionArchive,
1515
} from "../session/compaction-archive.js";
1616
import { CATALOG_TOOL_NAMES, CORE_TOOL_NAMES } from "../agent/tool-search.js";

src/plugins/evidence-archive-search-plugin.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
formatArchiveRef,
1212
isArchiveLike,
1313
parseArchiveTarget,
14-
} from "../session/archive-uri.js";
14+
} from "../session/compaction-archive.js";
1515

1616
const SEARCH_DEFAULT_MAX = 1000;
1717
const GREP_DEFAULT_MAX = 500;

src/plugins/path-escape-plugin.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { resolve } from "node:path";
22
import type { ToolPlugin } from "@intx/tools-posix";
33
import { isToolOutputLike } from "../util/tool-output-uri.js";
4-
import { isArchiveLike } from "../session/archive-uri.js";
4+
import { isArchiveLike } from "../session/compaction-archive.js";
55
import { resolveWorkspacePath } from "../permission/path-restriction.js";
66
import type { RootsProvider } from "../permission/worktree-roots.js";
77

src/session/archive-uri.ts

Lines changed: 0 additions & 26 deletions
This file was deleted.

src/session/archive-uri.test.ts renamed to src/session/compaction-archive-refs.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
isArchiveLike,
66
parseArchiveRef,
77
parseArchiveTarget,
8-
} from "./archive-uri.js";
8+
} from "./compaction-archive.js";
99

1010
describe("archive URI", () => {
1111
test("formats and parses archive:/// occurrence refs", () => {

src/session/compaction-archive.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -925,3 +925,39 @@ export function wrapCompactorWithCompletenessGate(
925925
},
926926
};
927927
}
928+
929+
// ---------------------------------------------------------------------------
930+
// Archive occurrence refs (pure addressing)
931+
// ---------------------------------------------------------------------------
932+
// The archive's own addressing scheme: `archive:///<occurrenceId>` refs name
933+
// where an occurrence's payload bytes live. Kept on the archive module so the
934+
// URI scheme and the store that honors it cannot drift apart.
935+
936+
/** Prefix for every archive target, including the bare `archive:///` root. */
937+
export const ARCHIVE_URI_PREFIX = "archive:";
938+
const ARCHIVE_URI_CANONICAL = "archive:///";
939+
940+
/** Render the canonical ref for an occurrence id. */
941+
export function formatArchiveRef(occurrenceId: string): string {
942+
return `${ARCHIVE_URI_CANONICAL}${occurrenceId}`;
943+
}
944+
945+
export function isArchiveLike(path: string): boolean {
946+
return path.startsWith(ARCHIVE_URI_PREFIX);
947+
}
948+
949+
/** Accept archive:///occ-… and common slashes; return the occurrence id or undefined. */
950+
export function parseArchiveRef(value: string): string | undefined {
951+
return parseArchiveTarget(value)?.occurrenceId;
952+
}
953+
954+
/** Root `archive:///` has no occurrenceId; a ref includes one. */
955+
export function parseArchiveTarget(
956+
value: string,
957+
): { occurrenceId?: string } | undefined {
958+
if (!isArchiveLike(value)) return undefined;
959+
const rest = value.slice(ARCHIVE_URI_PREFIX.length).replace(/^\/+/, "");
960+
const occurrenceId = rest.split(/[/?#]/)[0] ?? "";
961+
if (occurrenceId.length === 0) return {};
962+
return { occurrenceId };
963+
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { expect, test } from "bun:test";
22
import type { ArchiveOccurrence } from "./compaction-archive-schema.js";
3-
import { buildArchiveSummaryExcerpt } from "./summary-excerpt.js";
3+
import { buildArchiveSummaryExcerpt } from "./summarizer.js";
44

55
function occ(
66
partial: Pick<ArchiveOccurrence, "occurrenceId" | "kind"> &

src/session/summarizer.ts

Lines changed: 100 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,110 @@ import {
2121
import { LOG_NAMESPACE_ROOT } from "../branding.js";
2222
import { NOOP_TELEMETRY, type Telemetry } from "../telemetry/index.js";
2323
import {
24-
buildArchiveSummaryExcerpt,
25-
type SummaryExcerptArchive,
26-
} from "./summary-excerpt.js";
24+
formatArchiveRef,
25+
type CompactionArchive,
26+
} from "./compaction-archive.js";
27+
import type {
28+
ArchiveKind,
29+
ArchiveOccurrence,
30+
} from "./compaction-archive-schema.js";
2731
import { readSourceCredentialMaterial } from "../config/source-credentials.js";
2832

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

35+
// Token-budgeted compaction excerpt from the evidence archive.
36+
//
37+
// The live transcript is a clipped view. The archive holds the authorized
38+
// payloads compaction is about to drop, so the summary call should read those
39+
// rather than 400-character stubs. Budget is the control: later kinds yield
40+
// when earlier ones fill the window. Gap rows contribute metadata only.
41+
export const SUMMARY_EXCERPT_DEFAULT_BUDGET_CHARS = 80_000;
42+
43+
const KIND_PRIORITY: readonly ArchiveKind[] = [
44+
"user_message",
45+
"attachment",
46+
"assistant_text",
47+
"tool_args",
48+
"tool_failure",
49+
"tool_result",
50+
"overflow_blob",
51+
];
52+
53+
export type SummaryExcerptArchive = Pick<
54+
CompactionArchive,
55+
"listOccurrences" | "readAuthorizedPayload"
56+
>;
57+
58+
function heading(occ: ArchiveOccurrence): string {
59+
const parts = [`### ${occ.kind} ${formatArchiveRef(occ.occurrenceId)}`];
60+
if (occ.callId !== undefined) parts.push(`call=${occ.callId}`);
61+
if (occ.lifecycle !== undefined) parts.push(`lifecycle=${occ.lifecycle}`);
62+
if (occ.gap === true) parts.push("[gap]");
63+
return parts.join(" ");
64+
}
65+
66+
/**
67+
* Build a budgeted, kind-prioritized excerpt for the compaction summary call.
68+
* Empty archives return "" so the caller can fall back to the live transcript.
69+
*/
70+
export async function buildArchiveSummaryExcerpt(
71+
archive: SummaryExcerptArchive,
72+
budgetChars = SUMMARY_EXCERPT_DEFAULT_BUDGET_CHARS,
73+
): Promise<string> {
74+
const occurrences = await archive.listOccurrences();
75+
if (occurrences.length === 0) return "";
76+
77+
const byKind = new Map<ArchiveKind, ArchiveOccurrence[]>();
78+
for (const occ of occurrences) {
79+
const list = byKind.get(occ.kind);
80+
if (list !== undefined) list.push(occ);
81+
else byKind.set(occ.kind, [occ]);
82+
}
83+
84+
const sections: string[] = [];
85+
let used = 0;
86+
let omitted = 0;
87+
88+
for (const kind of KIND_PRIORITY) {
89+
const group = byKind.get(kind);
90+
if (group === undefined) continue;
91+
for (const occ of group) {
92+
const remaining = budgetChars - used;
93+
if (remaining <= 0) {
94+
omitted++;
95+
continue;
96+
}
97+
98+
let body: string | undefined;
99+
if (occ.gap === true) {
100+
body = "(payload not stored)";
101+
} else {
102+
try {
103+
body = await archive.readAuthorizedPayload(occ.occurrenceId);
104+
} catch {
105+
omitted++;
106+
continue;
107+
}
108+
}
109+
110+
const section = `${heading(occ)}\n${body}`;
111+
const separator = sections.length > 0 ? 2 : 0;
112+
if (section.length + separator > remaining) {
113+
omitted++;
114+
continue;
115+
}
116+
sections.push(section);
117+
used += section.length + separator;
118+
}
119+
}
120+
121+
const excerpt = sections.join("\n\n");
122+
if (omitted === 0) return excerpt;
123+
const note = `${omitted} occurrence${omitted === 1 ? "" : "s"} omitted`;
124+
if (excerpt.length === 0) return note;
125+
return `${excerpt}\n\n${note}`;
126+
}
127+
31128
// What the agent was doing when compaction fired. Lets the summary preserve
32129
// the workflow contract ("we are at step 3/7 of /build") rather than dropping
33130
// it into the compacted region.

0 commit comments

Comments
 (0)