@bli-cockpit/cli 0.2.268 → 0.2.270

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.
@@ -21,7 +21,7 @@ export async function runCockpitCli(argv, io) {
21
21
  }
22
22
 
23
23
  if (command === "--version" || command === "-V" || command === "version") {
24
- writeLine(io?.stdout ?? process.stdout, "0.2.268");
24
+ writeLine(io?.stdout ?? process.stdout, "0.2.270");
25
25
  return 0;
26
26
  }
27
27
 
@@ -1,11 +1,11 @@
1
- import { encodeEvidenceBytes } from "@bli-cockpit/telemetry-core/evidence-storage-encoding";
1
+ import { encodeChunkForStorage } from "./evidence-upload-encoding.js";
2
2
  import { fetchWithDeadline, isUploadDeadlineError, uploadDeadlineMs } from "./evidence-upload-deadline.js";
3
3
  import { noteSyncProgress } from "./sync-lock.js";
4
- export async function putSignedChunk(options, signedUrl, bytes, uploadId, index) {
5
- // Identical manifest + gzip framing to the server chunk route staging write.
6
- const encoded = encodeEvidenceBytes(bytes);
4
+ export async function putSignedChunk(options, signedUrl, bytes, uploadId, index, codec = "gzip") {
5
+ // Identical manifest + codec framing to the server chunk route staging write (gzip unless the server listed more).
6
+ const encoded = encodeChunkForStorage(bytes, codec);
7
7
  console.error("[evidence-upload] chunk encoded for storage", JSON.stringify({
8
- upload_id: uploadId, chunk_index: index, chunk_encoding: "gzip",
8
+ upload_id: uploadId, chunk_index: index, chunk_encoding: codec,
9
9
  bytes_before: bytes.length, bytes_after: encoded.length,
10
10
  }));
11
11
  const deadlineMs = uploadDeadlineMs(encoded.length);
@@ -0,0 +1,23 @@
1
+ import { EVIDENCE_CODEC_PREFERENCE, EVIDENCE_MANIFESTS, compressEvidenceBytes, zstdAvailable, } from "@bli-cockpit/telemetry-core/evidence-storage-encoding";
2
+ /** What this Node can write, most preferred first. Sent on `begin`. */
3
+ export function localEncodings(zstd = zstdAvailable()) {
4
+ return EVIDENCE_CODEC_PREFERENCE.filter(codec => codec !== "zstd" || zstd);
5
+ }
6
+ /**
7
+ * zstd when this Node has it and the server lists it, else brotli when listed, else gzip.
8
+ * A reply with no list (an old server) means gzip, which every server has always decoded.
9
+ */
10
+ export function chooseUploadEncoding(accepted, zstd = zstdAvailable()) {
11
+ if (!accepted || accepted.length === 0)
12
+ return { codec: "gzip", reason: "server_did_not_advertise" };
13
+ if (accepted.includes("zstd") && zstd)
14
+ return { codec: "zstd", reason: "server_accepts_zstd" };
15
+ if (accepted.includes("br")) {
16
+ return { codec: "br", reason: accepted.includes("zstd") ? "node_without_zstd" : "server_accepts_brotli_only" };
17
+ }
18
+ return { codec: "gzip", reason: accepted.includes("zstd") ? "node_without_zstd" : "server_did_not_advertise" };
19
+ }
20
+ /** The bytes the server stages for one chunk: the codec's manifest, then its stream. */
21
+ export function encodeChunkForStorage(bytes, codec) {
22
+ return Buffer.concat([EVIDENCE_MANIFESTS[codec], compressEvidenceBytes(bytes, codec)]);
23
+ }
@@ -1,5 +1,6 @@
1
1
  import { putSignedChunk } from "./evidence-upload-direct.js";
2
- import { gzipSync } from "node:zlib";
2
+ import { compressEvidenceBytes } from "@bli-cockpit/telemetry-core/evidence-storage-encoding";
3
+ import { chooseUploadEncoding } from "./evidence-upload-encoding.js";
3
4
  import { COMMIT_CRASHED_PLATFORM, RawEvidenceUploadCommitResponseSchema, isPermanentUploadFailure, } from "@bli-cockpit/telemetry-core";
4
5
  import { StagedFileError, readVerifiedChunk } from "./evidence-upload-staged.js";
5
6
  import { beginOneObject } from "./evidence-upload-terminal.js";
@@ -41,6 +42,12 @@ async function uploadObjectChunks(options, entry, disposition, chunkSizeBytes, o
41
42
  if (disposition.disposition === "already_committed" || disposition.commit_ready) {
42
43
  return { ok: true, uploadedChunks };
43
44
  }
45
+ // BLI-23334: the server listed what it decodes; an older one lists nothing and gets gzip.
46
+ const encoding = chooseUploadEncoding(disposition.accepted_encodings);
47
+ console.error("[evidence-upload] encoding chosen", JSON.stringify({
48
+ upload_id: disposition.upload_id, chunk_encoding: encoding.codec, reason: encoding.reason,
49
+ server_listed: disposition.accepted_encodings?.join(",") ?? "none",
50
+ }));
44
51
  for (let index = 0; index < entry.chunkCount; index += 1) {
45
52
  if (received.has(index))
46
53
  continue;
@@ -60,7 +67,7 @@ async function uploadObjectChunks(options, entry, disposition, chunkSizeBytes, o
60
67
  }
61
68
  const offer = disposition.signed_uploads?.find(upload => upload.chunk_index === index);
62
69
  const direct = offer
63
- ? await putSignedChunk(options, offer.signed_url, chunk, disposition.upload_id, index)
70
+ ? await putSignedChunk(options, offer.signed_url, chunk, disposition.upload_id, index, encoding.codec)
64
71
  : "refused";
65
72
  if (direct === "deadline") {
66
73
  // BLI-22801: the chunk route would push the same bytes over the same slow link.
@@ -75,26 +82,26 @@ async function uploadObjectChunks(options, entry, disposition, chunkSizeBytes, o
75
82
  uploadedChunks += 1;
76
83
  continue;
77
84
  }
78
- const failure = await uploadChunkThroughEndpoint(options, entry, disposition, objectKey, index, chunk, uploadedChunks, Boolean(offer));
85
+ const failure = await uploadChunkThroughEndpoint(options, entry, disposition, objectKey, index, chunk, uploadedChunks, Boolean(offer), encoding.codec);
79
86
  if (failure)
80
87
  return failure;
81
88
  uploadedChunks += 1;
82
89
  }
83
90
  return { ok: true, uploadedChunks };
84
91
  }
85
- async function uploadChunkThroughEndpoint(options, entry, disposition, objectKey, index, chunk, uploadedChunks, hadSignedOffer) {
92
+ async function uploadChunkThroughEndpoint(options, entry, disposition, objectKey, index, chunk, uploadedChunks, hadSignedOffer, codec) {
86
93
  console.error("[evidence-upload] using chunk route", JSON.stringify({
87
94
  reason: hadSignedOffer ? "signed_upload_refused_fallback" : "signed_upload_not_offered",
88
95
  upload_id: disposition.upload_id, chunk_index: index,
89
96
  }));
90
- const encodedChunk = gzipSync(chunk);
97
+ const encodedChunk = compressEvidenceBytes(chunk, codec);
91
98
  console.error("[evidence-upload] chunk encoded", JSON.stringify({
92
- upload_id: disposition.upload_id, chunk_index: index, chunk_encoding: "gzip",
99
+ upload_id: disposition.upload_id, chunk_index: index, chunk_encoding: codec,
93
100
  bytes_before: chunk.length, bytes_after: encodedChunk.length,
94
101
  }));
95
102
  const chunkResponse = await requestJson(options, "/api/ambient/evidence/upload/chunk", {
96
103
  schema_version: "ambient-raw-evidence-upload-chunk.v2",
97
- encoding: "gzip",
104
+ encoding: codec,
98
105
  generated_at: options.generatedAt,
99
106
  provenance: options.provenance,
100
107
  upload_id: disposition.upload_id,
@@ -243,6 +243,7 @@ export function readBeginDispositions(body) {
243
243
  signed_uploads: entry.signed_uploads,
244
244
  reason: entry.reason ?? null,
245
245
  raw_evidence_pointer_id: entry.raw_evidence_pointer_id,
246
+ ...(entry.accepted_encodings ? { accepted_encodings: entry.accepted_encodings } : {}),
246
247
  });
247
248
  }
248
249
  return map;
@@ -1,4 +1,6 @@
1
1
  import crypto from "node:crypto";
2
+ import { EVIDENCE_CODECS_HEADER } from "@bli-cockpit/telemetry-core";
3
+ import { localEncodings } from "./evidence-upload-encoding.js";
2
4
  import { describeError } from "./health-detail.js";
3
5
  import { fetchWithDeadline, isUploadDeadlineError, uploadDeadlineMs } from "./evidence-upload-deadline.js";
4
6
  import { noteSyncProgress } from "./sync-lock.js";
@@ -26,7 +28,7 @@ export async function requestJson(options, routePath, body) {
26
28
  "Authorization": `Bearer ${options.deviceToken}`,
27
29
  "Content-Type": "application/json",
28
30
  ...(routePath.endsWith("/evidence/upload/begin")
29
- ? { "x-tower-evidence-direct": "staged-gzip-v1" } : {}),
31
+ ? { "x-tower-evidence-direct": "staged-gzip-v1", [EVIDENCE_CODECS_HEADER]: localEncodings().join(",") } : {}),
30
32
  },
31
33
  body: serialized,
32
34
  }, deadlineMs);
@@ -14,17 +14,19 @@
14
14
  * every assistant line, holding no content.
15
15
  * - `toolUseResult` fields that repeat the tool_result or the tool input (claude-tool-results.ts).
16
16
  * - Attachment bookkeeping (claude-attachments.ts).
17
+ * - A thinking block's encrypted `signature`, replaced by a marker with its length (BLI-23334).
17
18
  *
18
19
  * Unknown shapes pass through untouched.
19
20
  */
20
21
  import { slimAttachmentLine, slimStopHookSummary } from "./claude-attachments.js";
21
22
  import { rememberToolInputs, slimToolUseResult, } from "./claude-tool-results.js";
22
- import { isJsonObject, stripKey } from "./slim-support.js";
23
+ import { dropCiphertext, isJsonObject, stripKey } from "./slim-support.js";
23
24
  export function newClaudeSlimState() {
24
25
  return { toolInputs: new Map() };
25
26
  }
26
27
  export function slimClaudeRecord(record, state, seen, edits) {
27
28
  stripKey(record, "wireToolInputs", "wireToolInputs", edits);
29
+ dropThinkingSignatures(record["message"], edits);
28
30
  switch (record["type"]) {
29
31
  case "assistant": {
30
32
  rememberToolInputs(record, state.toolInputs);
@@ -45,4 +47,20 @@ export function slimClaudeRecord(record, state, seen, edits) {
45
47
  default:
46
48
  return;
47
49
  }
50
+ }
51
+ /**
52
+ * BLI-23334: a thinking block's `signature` is a base64 blob Anthropic signs the block with; it
53
+ * compresses to about 75 percent and nothing in Tower reads it. The block, its `thinking` text and
54
+ * every other key stay. Assistant lines and `api-request-blob` lines carry the same blocks, so the
55
+ * rule keys on `message.content[]`, not on the line type. `redacted_thinking.data` is not touched:
56
+ * that block has no readable text, its data is all it is.
57
+ */
58
+ function dropThinkingSignatures(message, edits) {
59
+ if (!isJsonObject(message) || !Array.isArray(message["content"]))
60
+ return;
61
+ for (const block of message["content"]) {
62
+ if (isJsonObject(block) && block["type"] === "thinking") {
63
+ dropCiphertext(block, "signature", "claude.thinking.signature", edits);
64
+ }
65
+ }
48
66
  }
@@ -8,7 +8,20 @@
8
8
  * Guardian history and task_complete messages deduplicate only against earlier
9
9
  * response items; a copy appearing first is retained.
10
10
  */
11
- import { SLIM_COPY_MIN_CHARS, canonicalText, contentHash, isJsonObject, isSlimRef, slimRefFor, stripKey, } from "./slim-support.js";
11
+ import { SLIM_COPY_MIN_CHARS, canonicalText, contentHash, dropCiphertext, isJsonObject, isSlimRef, slimRefFor, stripKey, } from "./slim-support.js";
12
+ /**
13
+ * BLI-23334: `encrypted_content` on a reasoning or compaction item is ciphertext only OpenAI can
14
+ * read (it compresses to about 75 percent). It becomes a marker with its length; the item and its
15
+ * readable `summary` stay. Exactly two places carry it: a `response_item` payload of those types,
16
+ * and the same item types inside a `compacted` line's `replacement_history`. No other key and no
17
+ * other item type is touched, so nothing outside this contract can lose a field.
18
+ */
19
+ const CIPHERTEXT_ITEM_TYPES = new Set(["reasoning", "compaction"]);
20
+ function dropItemCiphertext(item, edits) {
21
+ if (isJsonObject(item) && typeof item["type"] === "string" && CIPHERTEXT_ITEM_TYPES.has(item["type"])) {
22
+ dropCiphertext(item, "encrypted_content", "codex.encrypted_content", edits);
23
+ }
24
+ }
12
25
  /** Strings from `response_item` lines remembered for comparison. Past this, new strings are not added. */
13
26
  const MAX_REMEMBERED_STRINGS = 300_000;
14
27
  /** Item types whose event copy is a pure repeat of a `response_item`. */
@@ -23,6 +36,7 @@ export function slimCodexRecord(record, state, seen, edits) {
23
36
  switch (record["type"]) {
24
37
  case "response_item":
25
38
  rememberLongStrings(payload, state, 0);
39
+ dropItemCiphertext(payload, edits);
26
40
  stripKey(payload, "internal_chat_message_metadata_passthrough", "codex.response_item.passthrough", edits);
27
41
  if (payload["type"] === "message" && payload["role"] === "developer") {
28
42
  seen.dedupeKey(payload, "content", "codex.developer_message.repeat", edits);
@@ -39,6 +53,10 @@ export function slimCodexRecord(record, state, seen, edits) {
39
53
  return;
40
54
  }
41
55
  case "compacted": {
56
+ const history = payload["replacement_history"];
57
+ if (Array.isArray(history))
58
+ for (const item of history)
59
+ dropItemCiphertext(item, edits);
42
60
  // guardian_history re-saves earlier turns; replacement_history is kept whole (part of it is found nowhere else).
43
61
  const guardian = payload["guardian_history"];
44
62
  if (Array.isArray(guardian))
@@ -58,6 +58,28 @@ export function stripKey(holder, key, rule, edits) {
58
58
  delete holder[key];
59
59
  return true;
60
60
  }
61
+ /** What an encrypted value becomes (BLI-23334): `[encrypted signature dropped, 1624 chars]`. Idempotent, prefix-stable. */
62
+ const CIPHERTEXT_MARKER_START = "[encrypted signature dropped, ";
63
+ export function ciphertextMarker(chars) {
64
+ return `${CIPHERTEXT_MARKER_START}${chars} chars]`;
65
+ }
66
+ /**
67
+ * Replace `holder[key]`, an opaque encrypted string nothing downstream can read (a Claude thinking
68
+ * `signature`, a Codex `encrypted_content`), with a marker that keeps its length. The surrounding
69
+ * block and its readable text are never touched. Shorter than its own marker, or already a marker,
70
+ * means no change.
71
+ */
72
+ export function dropCiphertext(holder, key, rule, edits) {
73
+ const value = holder[key];
74
+ if (typeof value !== "string" || value.startsWith(CIPHERTEXT_MARKER_START))
75
+ return false;
76
+ const marker = ciphertextMarker(value.length);
77
+ if (value.length <= marker.length)
78
+ return false;
79
+ holder[key] = marker;
80
+ edits.strip(rule, value.length - marker.length);
81
+ return true;
82
+ }
61
83
  /**
62
84
  * The one copy per transcript. The first time a block is seen it stays in the line; every time
63
85
  * after, the block is replaced by `{"slim_ref":"sha256:<16 hex>"}` naming the earlier copy. The
@@ -36,7 +36,7 @@ import { LineEdits, SeenBlocks, isJsonObject } from "./slim-support.js";
36
36
  * once, which is intended: the stored bytes are no longer what the new rules would produce.
37
37
  * `slim-rules-version.test.ts` fails when a rule changes without this moving.
38
38
  */
39
- export const SLIM_RULES_VERSION = "slim-rules.v1";
39
+ export const SLIM_RULES_VERSION = "slim-rules.v2";
40
40
  /** The text an image marker starts with; a line carrying one is a modified line (an image became a marker). */
41
41
  const IMAGE_MARKER_START = "[image sha256:";
42
42
  export function createSlimmer(kind, rulesForTests) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bli-cockpit/cli",
3
- "version": "0.2.268",
3
+ "version": "0.2.270",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
@@ -27,9 +27,9 @@
27
27
  "test": "node dist/cli.js --help && node ../../scripts/assert-public-cli-routing.mjs && node ../../scripts/assert-public-cli-verb-help.mjs && node ../../scripts/assert-public-cli-surface-doors.mjs && node ../../scripts/assert-public-cli-runtime-files.mjs && node ../../scripts/assert-public-cli-exit-contract.mjs && node ../../scripts/assert-public-cli-no-fleet-posts.mjs && node ../../scripts/assert-public-cli-doctor-json.mjs && node ../../scripts/assert-public-package-pack.mjs --workspace=@bli-cockpit/cli"
28
28
  },
29
29
  "dependencies": {
30
- "@bli-cockpit/memory-mcp": "0.1.79",
31
- "@bli-cockpit/mcp": "0.1.163",
32
- "@bli-cockpit/telemetry-core": "0.1.86"
30
+ "@bli-cockpit/memory-mcp": "0.1.81",
31
+ "@bli-cockpit/mcp": "0.1.165",
32
+ "@bli-cockpit/telemetry-core": "0.1.88"
33
33
  },
34
34
  "repository": {
35
35
  "type": "git",