@bli-cockpit/cli 0.2.204 → 0.2.206

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Which images this machine has already sent to the describe door (BLI-16977).
3
+ *
4
+ * One small file in the collector state directory. It is how "each distinct image is sent once"
5
+ * survives from one 15-minute sync to the next. A key is the first 32 hex characters of the
6
+ * image's sha256 (128 bits: no two images on one laptop will ever share one); the file holds keys
7
+ * only, never a description, never a path, never a byte. If it is lost or corrupt the worst case
8
+ * is the images are sent again and the server answers "known" without calling a model.
9
+ */
10
+ import path from "node:path";
11
+ import { isMissingFileFailure } from "./health-detail.js";
12
+ import { readJsonFile, writeJsonFile } from "./local-state-files.js";
13
+ export const IMAGE_DESCRIPTION_STATE_FILENAME = "image-descriptions.json";
14
+ /** Oldest keys leave first past this many (about 5 MB on disk). */
15
+ export const IMAGE_DESCRIPTION_STATE_MAX_KEYS = 150_000;
16
+ const KEY_CHARS = 32;
17
+ export function imageStateKey(sha256) {
18
+ return sha256.slice(0, KEY_CHARS);
19
+ }
20
+ export function imageDescriptionStatePath(stateDir) {
21
+ return path.join(stateDir, IMAGE_DESCRIPTION_STATE_FILENAME);
22
+ }
23
+ /** The keys already described, oldest first. A missing or unreadable file is an empty list, said out loud when unreadable. */
24
+ export async function readDescribedImageKeys(stateDir) {
25
+ try {
26
+ const raw = (await readJsonFile(imageDescriptionStatePath(stateDir)));
27
+ if (!Array.isArray(raw?.described))
28
+ return [];
29
+ return raw.described.filter((key) => typeof key === "string" && key.length === KEY_CHARS);
30
+ }
31
+ catch (error) {
32
+ if (!isMissingFileFailure(error)) {
33
+ console.error("[image-descriptions] state file unreadable, starting from an empty list", JSON.stringify({ reason: "image_state_unreadable" }));
34
+ }
35
+ return [];
36
+ }
37
+ }
38
+ export async function writeDescribedImageKeys(stateDir, keys) {
39
+ const kept = keys.length > IMAGE_DESCRIPTION_STATE_MAX_KEYS ? keys.slice(keys.length - IMAGE_DESCRIPTION_STATE_MAX_KEYS) : keys;
40
+ await writeJsonFile(imageDescriptionStatePath(stateDir), {
41
+ schema_version: "cockpit-image-descriptions.v1",
42
+ described: kept,
43
+ });
44
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Claude Code `attachment` lines: the harness's bookkeeping, re-saved every few turns (BLI-16977).
3
+ * On one laptop attachment lines are 30% of Claude transcript bytes and almost none of it is
4
+ * conversation. The line always stays (Tower counts lines and reads every timestamp); what goes is
5
+ * the payload that is a second copy, an exact repeat of an earlier block, or text nobody reads.
6
+ *
7
+ * Three kinds of change, by attachment type:
8
+ *
9
+ * - PAYLOAD dropped: `hook_success` (except SessionStart, whose output is context the model saw),
10
+ * `total_tokens_reminder`, `deferred_tools_record`.
11
+ * - REPEATS become a reference after the first copy: the system prompt and tool list
12
+ * (`prompt_snapshot`), CLAUDE.md files (`instructions`), the skill listing, session context,
13
+ * nested memory, MCP and agent listings, the deferred-tools delta's description lines, invoked
14
+ * skills, hook-injected context.
15
+ * - The `rendered` and `renderedInHumanTurn` copies (the same attachment as the model saw it)
16
+ * are dropped for every type measured to be a template around fields that stay.
17
+ *
18
+ * Kept whole: `queued_command` (what the person typed while the agent was busy, often found
19
+ * nowhere else), `edited_text_file` snippets, SessionStart hook output, `hook_additional_context`
20
+ * (once per distinct text), `deferred_tools_delta` names (added AND removed), and every attachment
21
+ * type this file does not name.
22
+ */
23
+ import { isJsonObject, stripKey } from "./slim-support.js";
24
+ /** Fields dropped from the attachment, by type. */
25
+ const PAYLOAD_FIELDS_DROPPED = {
26
+ total_tokens_reminder: ["text"],
27
+ deferred_tools_record: ["entries", "toolInputCopies"],
28
+ };
29
+ /** Fields kept once per distinct text, by type. */
30
+ const REPEATED_FIELDS = {
31
+ prompt_snapshot: ["systemPrompt", "tools"],
32
+ skill_listing: ["content"],
33
+ session_context: ["context"],
34
+ nested_memory: ["content"],
35
+ mcp_instructions_delta: ["addedBlocks"],
36
+ agent_listing_delta: ["addedLines"],
37
+ deferred_tools_delta: ["addedLines", "surfacedDefinitions"],
38
+ invoked_skills: ["skills"],
39
+ hook_additional_context: ["content"],
40
+ };
41
+ /**
42
+ * Types whose `rendered` copy is a template around fields that stay, or around nothing that
43
+ * matters. `environment` is deliberately absent: its rendered text had 622 distinct versions in the
44
+ * survey and nobody proved they hold nothing else.
45
+ */
46
+ const RENDERED_IS_A_COPY = new Set([
47
+ "hook_success", "hook_additional_context", "hook_system_message", "instructions", "skill_listing",
48
+ "queued_command", "total_tokens_reminder", "mcp_instructions_delta", "agent_listing_delta",
49
+ "deferred_tools_delta", "deferred_tools_record", "session_context", "nested_memory", "invoked_skills",
50
+ "edited_text_file", "file", "bash_output_audience_note", "remote_session_change", "model",
51
+ "batching_reminder_sent", "date", "auto_mode", "credential_org", "silent_turn_reminder",
52
+ "thinking_drop", "read_truncation_notice", "structured_output",
53
+ ]);
54
+ export function slimAttachmentLine(record, seen, edits) {
55
+ const attachment = record["attachment"];
56
+ if (!isJsonObject(attachment))
57
+ return;
58
+ const type = typeof attachment["type"] === "string" ? attachment["type"] : "";
59
+ if (type === "hook_success")
60
+ dropHookOutput(attachment, edits);
61
+ for (const field of PAYLOAD_FIELDS_DROPPED[type] ?? [])
62
+ stripKey(attachment, field, `attachment.${type}.${field}`, edits);
63
+ for (const field of REPEATED_FIELDS[type] ?? [])
64
+ seen.dedupeKey(attachment, field, `attachment.${type}.repeat`, edits);
65
+ if (type === "instructions" && Array.isArray(attachment["files"])) {
66
+ for (const file of attachment["files"]) {
67
+ if (isJsonObject(file))
68
+ seen.dedupeKey(file, "content", "attachment.instructions.repeat", edits);
69
+ }
70
+ }
71
+ if (RENDERED_IS_A_COPY.has(type)) {
72
+ for (const copy of ["rendered", "renderedInHumanTurn"])
73
+ stripKey(record, copy, "attachment.rendered", edits);
74
+ }
75
+ }
76
+ /** A hook that ran fine and printed something: its output is bookkeeping unless it was the SessionStart hook. */
77
+ function dropHookOutput(attachment, edits) {
78
+ if (attachment["hookEvent"] === "SessionStart")
79
+ return;
80
+ const exitCode = attachment["exitCode"];
81
+ if (exitCode !== undefined && exitCode !== 0)
82
+ return;
83
+ for (const field of ["command", "content", "stderr", "stdout"]) {
84
+ stripKey(attachment, field, "attachment.hook_success.output", edits);
85
+ }
86
+ }
87
+ /** A `stop_hook_summary` system line with no error and no stop request: the per-hook list is bookkeeping. */
88
+ export function slimStopHookSummary(record, edits) {
89
+ if (record["subtype"] !== "stop_hook_summary")
90
+ return;
91
+ const errors = record["hookErrors"];
92
+ const hadErrors = Array.isArray(errors) ? errors.length > 0 : errors !== undefined && errors !== null && errors !== false;
93
+ if (hadErrors || record["preventedContinuation"] === true || record["stopReason"])
94
+ return;
95
+ stripKey(record, "hookInfos", "system.stop_hook_summary.hookInfos", edits);
96
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * The slim rules for one Claude Code transcript line (BLI-16977). Entry point: `slimClaudeRecord`.
3
+ *
4
+ * The line's skeleton is never touched: type, timestamp, uuid, parentUuid, sessionId, cwd,
5
+ * gitBranch, isSidechain, requestId, version and the rest of the envelope stay, because Tower reads
6
+ * the timestamp and the working folder of every line. `message.usage` stays whole too (the 1-hour
7
+ * versus 5-minute cache-write split is what the cost is priced on). Only payload goes.
8
+ *
9
+ * What goes, and why it is safe (measured on 160 local files; the per-rule table is in
10
+ * docs/architecture/image-descriptions.md):
11
+ *
12
+ * - `wireToolInputs`: a second copy of each tool call's input, keyed by tool_use id.
13
+ * - `message.input_transformations`: a list of "thinking was dropped here" pointers, re-listed on
14
+ * every assistant line, holding no content.
15
+ * - `toolUseResult` fields that repeat the tool_result or the tool input (claude-tool-results.ts).
16
+ * - Attachment bookkeeping (claude-attachments.ts).
17
+ *
18
+ * Unknown shapes pass through untouched.
19
+ */
20
+ import { slimAttachmentLine, slimStopHookSummary } from "./claude-attachments.js";
21
+ import { rememberToolInputs, slimToolUseResult, } from "./claude-tool-results.js";
22
+ import { isJsonObject, stripKey } from "./slim-support.js";
23
+ export function newClaudeSlimState() {
24
+ return { toolInputs: new Map() };
25
+ }
26
+ export function slimClaudeRecord(record, state, seen, edits) {
27
+ stripKey(record, "wireToolInputs", "wireToolInputs", edits);
28
+ switch (record["type"]) {
29
+ case "assistant": {
30
+ rememberToolInputs(record, state.toolInputs);
31
+ const message = record["message"];
32
+ if (isJsonObject(message))
33
+ stripKey(message, "input_transformations", "message.input_transformations", edits);
34
+ return;
35
+ }
36
+ case "user":
37
+ slimToolUseResult(record, state.toolInputs, edits);
38
+ return;
39
+ case "attachment":
40
+ slimAttachmentLine(record, seen, edits);
41
+ return;
42
+ case "system":
43
+ slimStopHookSummary(record, edits);
44
+ return;
45
+ default:
46
+ return;
47
+ }
48
+ }
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Claude Code `toolUseResult`: the structured copy of what a tool returned, written next to the
3
+ * `tool_result` block that holds the same thing for the model (BLI-16977).
4
+ *
5
+ * Measured on one laptop (80 largest plus 80 random files), 92% of `toolUseResult` bytes repeat
6
+ * something already on the same line (the tool_result text) or on the tool call's own input. This
7
+ * file strips exactly those repeats, and only after PROVING each one is a repeat:
8
+ *
9
+ * - Read `file.content`: the first and last non-blank lines of the file text appear in the
10
+ * tool_result. (A truncated tool_result fails the check and the field stays.)
11
+ * - Bash `stdout` and `stderr`: the text appears in the tool_result, and no `persistedOutputPath`
12
+ * says the tool_result is only a preview of a longer output.
13
+ * - Write `content`, Edit `oldString` and `newString`, Agent and Task `prompt` and `description`:
14
+ * the same sha256 as the tool call's input, which an earlier assistant line carried.
15
+ * - A string or list result (MCP tools): every long string in it appears in the tool_result.
16
+ *
17
+ * Kept on purpose, because each holds something found nowhere else: Bash `bashEditDiff`, the
18
+ * `originalFile` and `structuredPatch` of an Edit or a Write, full output behind a
19
+ * `persistedOutputPath`, Read `totalLines`, `originalSize` and `dimensions`, and every flag
20
+ * (`interrupted`, `isImage`, `userModified`, ...). The harvest extractor reads
21
+ * `toolUseResult.interrupted`; it is never touched.
22
+ */
23
+ import { canonicalText, contentHash, isJsonObject, stripKey, } from "./slim-support.js";
24
+ /** Tools whose result repeats a field of the tool call's input, and which input fields. */
25
+ const COPIED_INPUT_FIELDS = {
26
+ Write: ["content"],
27
+ Edit: ["old_string", "new_string"],
28
+ Agent: ["prompt", "description"],
29
+ Task: ["prompt", "description"],
30
+ };
31
+ /** Which result field repeats which input field. */
32
+ const RESULT_FIELD_FOR_INPUT = {
33
+ Write: { content: "content" },
34
+ Edit: { oldString: "old_string", newString: "new_string" },
35
+ Agent: { prompt: "prompt", description: "description" },
36
+ Task: { prompt: "prompt", description: "description" },
37
+ };
38
+ /** Tool calls remembered so a result can be matched to its input. Oldest dropped first. */
39
+ const MAX_REMEMBERED_TOOL_CALLS = 50_000;
40
+ /** A string shorter than this is not checked as a copy (it costs nothing to keep). */
41
+ const MIN_COPY_CHARS = 40;
42
+ /** Called for each assistant line: remember what the Write, Edit and Agent calls carried. */
43
+ export function rememberToolInputs(record, inputs) {
44
+ const message = record["message"];
45
+ const content = isJsonObject(message) ? message["content"] : undefined;
46
+ if (!Array.isArray(content))
47
+ return;
48
+ for (const block of content) {
49
+ if (!isJsonObject(block) || block["type"] !== "tool_use")
50
+ continue;
51
+ const id = block["id"];
52
+ const tool = block["name"];
53
+ const input = block["input"];
54
+ if (typeof id !== "string" || typeof tool !== "string" || !isJsonObject(input))
55
+ continue;
56
+ const fields = COPIED_INPUT_FIELDS[tool];
57
+ if (!fields)
58
+ continue;
59
+ const hashes = {};
60
+ for (const field of fields) {
61
+ const text = input[field];
62
+ if (typeof text === "string")
63
+ hashes[field] = contentHash(text);
64
+ }
65
+ inputs.set(id, { tool, hashes });
66
+ if (inputs.size > MAX_REMEMBERED_TOOL_CALLS) {
67
+ const oldest = inputs.keys().next().value;
68
+ if (oldest !== undefined)
69
+ inputs.delete(oldest);
70
+ }
71
+ }
72
+ }
73
+ /** Called for each user line that carries a `toolUseResult`. */
74
+ export function slimToolUseResult(record, inputs, edits) {
75
+ const result = record["toolUseResult"];
76
+ if (result === undefined)
77
+ return;
78
+ const blocks = toolResultBlocks(record);
79
+ // Several results on one line is not a shape we have seen; leave the line alone rather than guess which is which.
80
+ if (blocks.length !== 1)
81
+ return;
82
+ const block = blocks[0];
83
+ const toolUseId = block["tool_use_id"];
84
+ const digest = typeof toolUseId === "string" ? inputs.get(toolUseId) : undefined;
85
+ if (typeof toolUseId === "string")
86
+ inputs.delete(toolUseId);
87
+ if (isJsonObject(result)) {
88
+ stripCopiedFields(result, digest, toolResultText(block), edits);
89
+ return;
90
+ }
91
+ if (typeof result === "string" || Array.isArray(result)) {
92
+ if (everyLongStringAppearsIn(result, block["content"])) {
93
+ edits.strip("toolUseResult.copy_of_tool_result", canonicalText(result).length);
94
+ delete record["toolUseResult"];
95
+ }
96
+ }
97
+ }
98
+ function stripCopiedFields(result, digest, resultText, edits) {
99
+ const file = result["file"];
100
+ if (isJsonObject(file) && typeof file["content"] === "string" && fileLinesAppearIn(file["content"], resultText)) {
101
+ stripKey(file, "content", "toolUseResult.file.content", edits);
102
+ }
103
+ if (typeof result["stdout"] === "string" && result["persistedOutputPath"] === undefined && result["isImage"] !== true) {
104
+ for (const field of ["stdout", "stderr"]) {
105
+ const text = result[field];
106
+ if (typeof text === "string" && text.length >= MIN_COPY_CHARS && resultText.includes(text)) {
107
+ stripKey(result, field, `toolUseResult.${field}`, edits);
108
+ }
109
+ }
110
+ }
111
+ const fieldMap = digest ? RESULT_FIELD_FOR_INPUT[digest.tool] : undefined;
112
+ if (!digest || !fieldMap)
113
+ return;
114
+ for (const [resultField, inputField] of Object.entries(fieldMap)) {
115
+ const text = result[resultField];
116
+ // Same hash as the call's input is an exact repeat at any length, so no minimum here.
117
+ if (typeof text !== "string" || text.length === 0)
118
+ continue;
119
+ if (digest.hashes[inputField] === contentHash(text)) {
120
+ stripKey(result, resultField, `toolUseResult.${resultField}`, edits);
121
+ }
122
+ }
123
+ }
124
+ function toolResultBlocks(record) {
125
+ const message = record["message"];
126
+ const content = isJsonObject(message) ? message["content"] : undefined;
127
+ if (!Array.isArray(content))
128
+ return [];
129
+ return content.filter((block) => isJsonObject(block) && block["type"] === "tool_result");
130
+ }
131
+ /** The text of a tool_result block: a string as it is, a list of blocks as their text parts joined. */
132
+ function toolResultText(block) {
133
+ const content = block["content"];
134
+ if (typeof content === "string")
135
+ return content;
136
+ if (!Array.isArray(content))
137
+ return "";
138
+ const parts = [];
139
+ for (const part of content) {
140
+ if (isJsonObject(part) && typeof part["text"] === "string")
141
+ parts.push(part["text"]);
142
+ }
143
+ return parts.join("\n");
144
+ }
145
+ /** A Read puts `cat -n` numbering in front of each line, so compare by the first and last non-blank line. */
146
+ function fileLinesAppearIn(fileText, resultText) {
147
+ if (resultText.length === 0)
148
+ return false;
149
+ const lines = fileText.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
150
+ if (lines.length === 0)
151
+ return false;
152
+ return resultText.includes(lines[0]) && resultText.includes(lines[lines.length - 1]);
153
+ }
154
+ /** Every string of 40 or more characters in `value` appears (JSON-escaped) inside the tool_result content. */
155
+ function everyLongStringAppearsIn(value, toolResultContent) {
156
+ const haystack = JSON.stringify(toolResultContent) ?? "";
157
+ const longStrings = [];
158
+ collectLongStrings(value, longStrings, 0);
159
+ if (longStrings.length === 0)
160
+ return false;
161
+ return longStrings.every((text) => haystack.includes(JSON.stringify(text).slice(1, -1)));
162
+ }
163
+ function collectLongStrings(value, into, depth) {
164
+ if (depth > 6 || into.length > 200)
165
+ return;
166
+ if (typeof value === "string") {
167
+ if (value.length >= MIN_COPY_CHARS)
168
+ into.push(value);
169
+ }
170
+ else if (Array.isArray(value)) {
171
+ for (const item of value)
172
+ collectLongStrings(item, into, depth + 1);
173
+ }
174
+ else if (isJsonObject(value)) {
175
+ for (const item of Object.values(value))
176
+ collectLongStrings(item, into, depth + 1);
177
+ }
178
+ }
@@ -0,0 +1,179 @@
1
+ /**
2
+ * The slim rules for one Codex rollout line (BLI-16977). Entry point: `slimCodexRecord`.
3
+ *
4
+ * A Codex line is `{timestamp, type, payload}`. The envelope never changes; only payload goes.
5
+ * Measured on 80 local rollouts, almost all the bulk is images (turned into markers before this
6
+ * runs) and a second copy of each finished item (`event_msg` `item_completed`) next to the
7
+ * `response_item` the model saw. The survey and its skeptic agree on what is a copy:
8
+ *
9
+ * - `item_completed` CommandExecution: `formatted_output` and `parsed_cmd` go, and `stdout` goes
10
+ * when it equals `aggregated_output`. `aggregated_output` and `command` STAY: they are the only
11
+ * full copy of what the shell printed.
12
+ * - `item_completed` UserMessage, AgentMessage, Reasoning: any long string identical to one in an
13
+ * EARLIER `response_item` becomes a reference. A copy that came first in the file is kept.
14
+ * - McpToolCall results, FileChange contents, `compacted.replacement_history`, image generations:
15
+ * kept (their images are markers by now). `compacted.guardian_history` keeps only what an
16
+ * earlier `response_item` did not already hold.
17
+ * - `token_count`: `last_token_usage` and `rate_limits` go; `total_token_usage` and
18
+ * `model_context_window` stay (the session facts read them).
19
+ * - `token_usage_record`: payload emptied (17,964 of 18,109 were byte-equal to a `token_count`).
20
+ * - `thread_settings_applied`: reduced to `service_tier`.
21
+ * - `response_item`: `internal_chat_message_metadata_passthrough` goes; a developer message, the
22
+ * `session_meta` `base_instructions` and each `world_state` block are kept once per distinct
23
+ * text and become references after.
24
+ * - `task_complete.last_agent_message`: a reference when an earlier message holds the same text.
25
+ *
26
+ * `task_started`/`task_complete` durations, `session_meta.forked_from_id` and every unnamed shape
27
+ * pass through untouched.
28
+ */
29
+ import { SLIM_COPY_MIN_CHARS, canonicalText, contentHash, isJsonObject, isSlimRef, slimRefFor, stripKey, } from "./slim-support.js";
30
+ /** Strings from `response_item` lines remembered for comparison. Past this, new strings are not added. */
31
+ const MAX_REMEMBERED_STRINGS = 300_000;
32
+ /** Item types whose event copy is a pure repeat of a `response_item`. */
33
+ const ITEM_TYPES_THAT_REPEAT = new Set(["UserMessage", "AgentMessage", "Reasoning"]);
34
+ export function newCodexSlimState() {
35
+ return { earlierStrings: new Set() };
36
+ }
37
+ export function slimCodexRecord(record, state, seen, edits) {
38
+ const payload = record["payload"];
39
+ if (!isJsonObject(payload))
40
+ return;
41
+ switch (record["type"]) {
42
+ case "response_item":
43
+ rememberLongStrings(payload, state, 0);
44
+ stripKey(payload, "internal_chat_message_metadata_passthrough", "codex.response_item.passthrough", edits);
45
+ if (payload["type"] === "message" && payload["role"] === "developer") {
46
+ seen.dedupeKey(payload, "content", "codex.developer_message.repeat", edits);
47
+ }
48
+ return;
49
+ case "session_meta":
50
+ seen.dedupeKey(payload, "base_instructions", "codex.session_meta.base_instructions.repeat", edits);
51
+ return;
52
+ case "world_state": {
53
+ const blocks = payload["state"];
54
+ if (isJsonObject(blocks))
55
+ for (const key of Object.keys(blocks))
56
+ seen.dedupeKey(blocks, key, "codex.world_state.repeat", edits);
57
+ return;
58
+ }
59
+ case "compacted": {
60
+ // guardian_history re-saves earlier turns; replacement_history is kept whole (part of it is found nowhere else).
61
+ const guardian = payload["guardian_history"];
62
+ if (Array.isArray(guardian))
63
+ refEarlierCopiesIn(guardian, state, "codex.compacted.guardian_history.copy_of_response_item", edits, 0);
64
+ return;
65
+ }
66
+ case "token_usage_record":
67
+ emptyPayload(record, payload, "codex.token_usage_record", edits);
68
+ return;
69
+ case "event_msg":
70
+ slimEvent(payload, state, edits);
71
+ return;
72
+ default:
73
+ return;
74
+ }
75
+ }
76
+ function slimEvent(payload, state, edits) {
77
+ switch (payload["type"]) {
78
+ case "token_count": {
79
+ const info = payload["info"];
80
+ if (isJsonObject(info))
81
+ stripKey(info, "last_token_usage", "codex.token_count.last_token_usage", edits);
82
+ stripKey(payload, "rate_limits", "codex.token_count.rate_limits", edits);
83
+ return;
84
+ }
85
+ case "thread_settings_applied":
86
+ keepOnlyServiceTier(payload, edits);
87
+ return;
88
+ case "task_complete":
89
+ refIfEarlierCopy(payload, "last_agent_message", state, "codex.task_complete.last_agent_message", edits);
90
+ return;
91
+ case "item_completed":
92
+ slimFinishedItem(payload["item"], state, edits);
93
+ return;
94
+ default:
95
+ return;
96
+ }
97
+ }
98
+ function slimFinishedItem(item, state, edits) {
99
+ if (!isJsonObject(item))
100
+ return;
101
+ const type = item["type"];
102
+ if (type === "CommandExecution") {
103
+ stripKey(item, "formatted_output", "codex.item.command.formatted_output", edits);
104
+ stripKey(item, "parsed_cmd", "codex.item.command.parsed_cmd", edits);
105
+ const stdout = item["stdout"];
106
+ if (typeof stdout === "string" && stdout.length >= SLIM_COPY_MIN_CHARS && stdout === item["aggregated_output"]) {
107
+ stripKey(item, "stdout", "codex.item.command.stdout", edits);
108
+ }
109
+ return;
110
+ }
111
+ if (typeof type === "string" && ITEM_TYPES_THAT_REPEAT.has(type)) {
112
+ refEarlierCopiesIn(item, state, `codex.item.${type}.copy_of_response_item`, edits, 0);
113
+ }
114
+ }
115
+ /** The payload emptied, once. */
116
+ function emptyPayload(record, payload, rule, edits) {
117
+ if (Object.keys(payload).length === 0)
118
+ return;
119
+ edits.strip(rule, canonicalText(payload).length);
120
+ record["payload"] = {};
121
+ }
122
+ function keepOnlyServiceTier(payload, edits) {
123
+ const settings = payload["thread_settings"];
124
+ if (!isJsonObject(settings))
125
+ return;
126
+ const others = Object.keys(settings).filter((key) => key !== "service_tier");
127
+ if (others.length === 0)
128
+ return;
129
+ edits.strip("codex.thread_settings_applied", canonicalText(settings).length);
130
+ payload["thread_settings"] = settings["service_tier"] === undefined ? {} : { service_tier: settings["service_tier"] };
131
+ }
132
+ /** Walks a `response_item` payload and remembers each long string (reasoning ciphertext excluded). */
133
+ function rememberLongStrings(value, state, depth) {
134
+ if (depth > 8 || state.earlierStrings.size >= MAX_REMEMBERED_STRINGS)
135
+ return;
136
+ if (typeof value === "string") {
137
+ if (value.length >= SLIM_COPY_MIN_CHARS)
138
+ state.earlierStrings.add(contentHash(value));
139
+ }
140
+ else if (Array.isArray(value)) {
141
+ for (const item of value)
142
+ rememberLongStrings(item, state, depth + 1);
143
+ }
144
+ else if (isJsonObject(value)) {
145
+ for (const [key, item] of Object.entries(value)) {
146
+ if (key !== "encrypted_content")
147
+ rememberLongStrings(item, state, depth + 1);
148
+ }
149
+ }
150
+ }
151
+ function refIfEarlierCopy(holder, key, state, rule, edits) {
152
+ const value = holder[key];
153
+ if (typeof value !== "string" || value.length < SLIM_COPY_MIN_CHARS)
154
+ return;
155
+ const hash = contentHash(value);
156
+ if (!state.earlierStrings.has(hash))
157
+ return;
158
+ holder[key] = slimRefFor(hash);
159
+ edits.strip(rule, value.length);
160
+ }
161
+ /** Every long string in `node` that an earlier `response_item` already held becomes a reference. */
162
+ function refEarlierCopiesIn(node, state, rule, edits, depth) {
163
+ if (depth > 8)
164
+ return;
165
+ const keys = Array.isArray(node) ? node.map((_, index) => index) : Object.keys(node);
166
+ for (const key of keys) {
167
+ const holder = node;
168
+ const child = holder[key];
169
+ if (typeof child === "string") {
170
+ if (child.length >= SLIM_COPY_MIN_CHARS && state.earlierStrings.has(contentHash(child))) {
171
+ holder[key] = slimRefFor(contentHash(child));
172
+ edits.strip(rule, child.length);
173
+ }
174
+ }
175
+ else if ((Array.isArray(child) || isJsonObject(child)) && !isSlimRef(child)) {
176
+ refEarlierCopiesIn(child, state, rule, edits, depth + 1);
177
+ }
178
+ }
179
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Shared pieces of the slim transcript (BLI-16977): what a rule records, how a repeated block is
3
+ * swapped for a short reference, and the few JSON helpers every rule uses.
4
+ *
5
+ * The slim transcript is built one line at a time, in order, and each line may depend only on the
6
+ * lines before it and itself. That is the whole contract: it is why a growing transcript's slim
7
+ * form extends its earlier slim form byte for byte (the BLI-11088 prefix rule), and why the same
8
+ * code can slim a 700 MB file without holding it in memory.
9
+ *
10
+ * Nothing here reads a file, the network or the environment. Rules record sizes and rule names,
11
+ * never content.
12
+ */
13
+ import { createHash } from "node:crypto";
14
+ /** A block shorter than this is never worth swapping for a reference (the reference is about 40 characters). */
15
+ export const SLIM_REF_MIN_CHARS = 300;
16
+ /** A string shorter than this is not looked up against earlier copies. */
17
+ export const SLIM_COPY_MIN_CHARS = 80;
18
+ export function isJsonObject(value) {
19
+ return value !== null && typeof value === "object" && !Array.isArray(value);
20
+ }
21
+ export function isSlimRef(value) {
22
+ return isJsonObject(value) && typeof value["slim_ref"] === "string" && Object.keys(value).length === 1;
23
+ }
24
+ /** `sha256:` plus the first 16 hex characters of the sha256 of the value's text (a string as it is, anything else as compact JSON). */
25
+ export function contentHash(canonicalText) {
26
+ return createHash("sha256").update(canonicalText).digest("hex").slice(0, 16);
27
+ }
28
+ export function slimRefFor(hash) {
29
+ return { slim_ref: `sha256:${hash}` };
30
+ }
31
+ /** The text a value is hashed and measured by. */
32
+ export function canonicalText(value) {
33
+ if (typeof value === "string")
34
+ return value;
35
+ return JSON.stringify(value) ?? "";
36
+ }
37
+ /** What one line lost, by rule, in characters. A line with no entries was not touched. */
38
+ export class LineEdits {
39
+ charsByRule = new Map();
40
+ strip(rule, chars) {
41
+ this.charsByRule.set(rule, (this.charsByRule.get(rule) ?? 0) + chars);
42
+ }
43
+ get rules() {
44
+ return [...this.charsByRule.keys()];
45
+ }
46
+ get entries() {
47
+ return this.charsByRule.entries();
48
+ }
49
+ get isEmpty() {
50
+ return this.charsByRule.size === 0;
51
+ }
52
+ }
53
+ /** Remove `key` from `holder`, recording the rule and the characters it held. */
54
+ export function stripKey(holder, key, rule, edits) {
55
+ if (!(key in holder))
56
+ return false;
57
+ edits.strip(rule, canonicalText(holder[key]).length);
58
+ delete holder[key];
59
+ return true;
60
+ }
61
+ /**
62
+ * The one copy per transcript. The first time a block is seen it stays in the line; every time
63
+ * after, the block is replaced by `{"slim_ref":"sha256:<16 hex>"}` naming the earlier copy. The
64
+ * hash is of the block's text (a string as it is, anything else as compact JSON).
65
+ */
66
+ export class SeenBlocks {
67
+ hashes = new Set();
68
+ /** True when this block was seen before in the transcript; remembers it otherwise. */
69
+ seenBefore(hash) {
70
+ if (this.hashes.has(hash))
71
+ return true;
72
+ this.hashes.add(hash);
73
+ return false;
74
+ }
75
+ /** Swap `holder[key]` for a reference when an identical block came earlier. */
76
+ dedupeKey(holder, key, rule, edits) {
77
+ const value = holder[key];
78
+ if (value === undefined || isSlimRef(value))
79
+ return;
80
+ const text = canonicalText(value);
81
+ if (text.length < SLIM_REF_MIN_CHARS)
82
+ return;
83
+ const hash = contentHash(text);
84
+ if (!this.seenBefore(hash))
85
+ return;
86
+ const ref = slimRefFor(hash);
87
+ holder[key] = ref;
88
+ edits.strip(rule, Math.max(0, text.length - JSON.stringify(ref).length));
89
+ }
90
+ }