@bli-cockpit/cli 0.2.150 → 0.2.155

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.
Files changed (46) hide show
  1. package/dist/adapters/local-sources.js +1 -0
  2. package/dist/adapters/raw-evidence-collection-state.js +1 -0
  3. package/dist/adapters/raw-evidence-facts.js +1 -0
  4. package/dist/adapters/raw-evidence-manifest.js +5 -0
  5. package/dist/adapters/raw-evidence-sanitize.js +82 -20
  6. package/dist/adapters/raw-evidence-snapshot-supersede.js +82 -0
  7. package/dist/adapters/raw-evidence-transcript-file.js +10 -0
  8. package/dist/agent-rules.js +3 -3
  9. package/dist/commands/backfill-lock-wait.js +49 -0
  10. package/dist/commands/backfill-outside-roots.js +97 -0
  11. package/dist/commands/backfill-reasons.js +17 -0
  12. package/dist/commands/backfill-scan.js +8 -3
  13. package/dist/commands/backfill-scope.js +14 -1
  14. package/dist/commands/backfill-session-report.js +3 -0
  15. package/dist/commands/backfill-upload.js +6 -0
  16. package/dist/commands/backfill.js +11 -5
  17. package/dist/commands/clean.js +31 -13
  18. package/dist/commands/doctor-budget.js +69 -0
  19. package/dist/commands/doctor-disk-words.js +81 -3
  20. package/dist/commands/doctor-lock-wait.js +12 -3
  21. package/dist/commands/doctor-pipeline-verdicts.js +2 -0
  22. package/dist/commands/doctor-pipeline.js +49 -24
  23. package/dist/commands/doctor.js +26 -4
  24. package/dist/commands/heartbeat.js +1 -1
  25. package/dist/commands/local-arg-values.js +9 -0
  26. package/dist/commands/local-args-collector-setup.js +4 -2
  27. package/dist/commands/local-args-collector-work.js +9 -1
  28. package/dist/commands/local-help-commands.js +11 -8
  29. package/dist/commands/local-help.js +1 -1
  30. package/dist/commands/public-root.js +1 -1
  31. package/dist/commands/status-disk-line.js +22 -0
  32. package/dist/commands/status.js +5 -2
  33. package/dist/commands/sync-followups-staging.js +13 -10
  34. package/dist/commands/sync.js +7 -4
  35. package/dist/cursors/raw-evidence-cursor.js +14 -0
  36. package/dist/disk-prune.js +19 -23
  37. package/dist/disk-retention.js +12 -9
  38. package/dist/disk-usage-footprint.js +10 -4
  39. package/dist/evidence-reconcile-client.js +2 -2
  40. package/dist/log-rotation.js +31 -10
  41. package/dist/prune-receipt.js +87 -0
  42. package/dist/raw-evidence-gc.js +55 -15
  43. package/dist/state-dir-audit.js +138 -0
  44. package/dist/upload-envelope-build.js +9 -0
  45. package/dist/upload-sync.js +10 -22
  46. package/package.json +5 -5
@@ -10,27 +10,33 @@ const RAW_EVIDENCE_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
10
10
  * observed on the fleet is minutes, not hours.
11
11
  */
12
12
  const STAGING_ORPHAN_MS = 6 * 60 * 60 * 1000;
13
- // GC hashes every file in every old-but-kept dir to confirm uploads. Running
14
- // that on each 15-min sync would re-read the same gigabytes ~96x/day on
15
- // machines with unconfirmed evidence, so GC is throttled to once per day.
16
- const GC_MIN_INTERVAL_MS = 24 * 60 * 60 * 1000;
13
+ /**
14
+ * The retired once-a-day throttle marker (BLI-11106). The GC runs every tick
15
+ * now, and deletes this file on sight so it stops being state nobody reads.
16
+ *
17
+ * Why every tick is affordable: the GC used to hash every file of every old
18
+ * pack to confirm it was uploaded, which re-read gigabytes on a machine with
19
+ * unconfirmed evidence. It now asks the pack's own `manifest.json` first, and a
20
+ * pack whose manifest names ANY hash the ledger has not committed is skipped
21
+ * without reading a payload byte. Only a pack the manifest says is fully
22
+ * committed is hashed, once, on the way to being deleted.
23
+ */
24
+ export const LEGACY_GC_THROTTLE_MARKER = ".last-raw-evidence-gc";
17
25
  export async function runRawEvidenceLocalGc(paths, env = process.env, now = new Date()) {
18
26
  if (env["COCKPIT_DISABLE_GC"] === "1") {
19
27
  return { skipped: true, removed_dirs: 0, freed_bytes: 0 };
20
28
  }
21
- const throttleMarker = path.join(paths.state_dir, ".last-raw-evidence-gc");
22
- const lastRun = await fs.stat(throttleMarker).catch(() => null);
23
- if (lastRun && now.getTime() - lastRun.mtimeMs < GC_MIN_INTERVAL_MS) {
24
- return { skipped: true, removed_dirs: 0, freed_bytes: 0 };
25
- }
26
- await fs.mkdir(paths.state_dir, { recursive: true }).catch(() => undefined);
27
- await fs.writeFile(throttleMarker, now.toISOString()).catch(() => undefined);
29
+ await fs
30
+ .rm(path.join(paths.state_dir, LEGACY_GC_THROTTLE_MARKER), { force: true })
31
+ .catch(() => undefined);
28
32
  const cursor = await readRawEvidenceCursor(paths);
29
33
  const uploadedHashes = new Set(Object.keys(cursor.objects));
30
34
  const rawEvidenceRoot = path.join(paths.state_dir, "raw-evidence");
31
35
  const entries = await fs.readdir(rawEvidenceRoot, { withFileTypes: true }).catch(() => []);
32
36
  let removedDirs = 0;
33
37
  let freedBytes = 0;
38
+ let scannedDirs = 0;
39
+ let skippedByManifest = 0;
34
40
  for (const entry of entries) {
35
41
  if (!entry.isDirectory() || !entry.name.startsWith("work-"))
36
42
  continue;
@@ -40,6 +46,11 @@ export async function runRawEvidenceLocalGc(paths, env = process.env, now = new
40
46
  continue;
41
47
  if (now.getTime() - dirStats.mtimeMs < RAW_EVIDENCE_RETENTION_MS)
42
48
  continue;
49
+ scannedDirs += 1;
50
+ if (await manifestNamesUncommitted(dir, uploadedHashes)) {
51
+ skippedByManifest += 1;
52
+ continue;
53
+ }
43
54
  const inspection = await inspectRawEvidenceDirForGc(dir, uploadedHashes);
44
55
  if (!inspection.deletable)
45
56
  continue;
@@ -50,12 +61,18 @@ export async function runRawEvidenceLocalGc(paths, env = process.env, now = new
50
61
  freedBytes += inspection.byteSize;
51
62
  }
52
63
  // Log capping used to live here, and that is why it did not work: this GC
53
- // is throttled to once a day and skipped entirely by COCKPIT_DISABLE_GC,
64
+ // was throttled to once a day and is skipped entirely by COCKPIT_DISABLE_GC,
54
65
  // while sync.log grows ~8 MB an hour on an active machine (measured on the
55
66
  // reference Mac, 2026-09-04: 219,866,695 bytes with the last GC 19 hours
56
- // earlier). A daily truncate-to-zero of a file that reaches ~190 MB a day is
57
- // not a cap, and sync.err.log had no cap at all. One owner now, running in
58
- // the tick preamble: log-rotation.ts (BLI-3553).
67
+ // earlier). One owner now, running in the tick preamble: log-rotation.ts
68
+ // (BLI-3553).
69
+ console.error("[raw-evidence] gc sweep", JSON.stringify({
70
+ reason: removedDirs > 0 ? "gc_removed" : "nothing_to_prune",
71
+ scanned_dirs: scannedDirs,
72
+ skipped_by_manifest: skippedByManifest,
73
+ removed_dirs: removedDirs,
74
+ freed_bytes: freedBytes,
75
+ }));
59
76
  return {
60
77
  skipped: false,
61
78
  removed_dirs: removedDirs,
@@ -241,6 +258,29 @@ async function dirByteSize(dir) {
241
258
  }
242
259
  return total;
243
260
  }
261
+ /**
262
+ * True when the pack's own manifest names a content hash the ledger has not
263
+ * committed, so the pack cannot be deleted and no payload needs reading. A
264
+ * missing or unreadable manifest answers false and falls through to the full
265
+ * hash check, which is the old, conservative path.
266
+ */
267
+ async function manifestNamesUncommitted(dir, uploadedHashes) {
268
+ const manifest = await fs
269
+ .readFile(path.join(dir, "manifest.json"), "utf8")
270
+ .catch(() => null);
271
+ if (!manifest)
272
+ return false;
273
+ try {
274
+ const parsed = JSON.parse(manifest);
275
+ return (parsed.files ?? []).some((file) => typeof file.content_hash_sha256 === "string" &&
276
+ !uploadedHashes.has(file.content_hash_sha256));
277
+ }
278
+ catch {
279
+ // Not silent in effect: an unparseable manifest falls through to the full
280
+ // per-file hash check below, which decides on the bytes themselves.
281
+ return false;
282
+ }
283
+ }
244
284
  async function inspectRawEvidenceDirForGc(dir, uploadedHashes) {
245
285
  const files = await listFiles(dir).catch(() => null);
246
286
  if (!files)
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Everything the collector's state directory holds, sorted into who owns it
3
+ * and what bounds it (BLI-11106).
4
+ *
5
+ * `~/.local/state/bli-cockpit` (Windows: `%USERPROFILE%\.local\state\bli-cockpit`,
6
+ * the same layout with backslashes) is where the collector keeps its state.
7
+ * By 2026-09-30 it also held a gigabyte of study artifacts written by agents
8
+ * running benches and experiments (`memory-bench`, `memory-extract-*`,
9
+ * `chat-model-bench`, …). Those are not the collector's, the collector never
10
+ * reads them, and counting them as its footprint made `cockpit doctor`
11
+ * alarm about bytes no prune could ever explain.
12
+ *
13
+ * The rule: study artifacts belong in the SIBLING root
14
+ * `~/.local/state/bli-cockpit-studies` (same parent, same layout on Windows),
15
+ * see `studiesStateDir`. Existing study folders already under the state dir are
16
+ * recognised by name, left exactly where they are, and not counted. Anything
17
+ * the table below does not recognise is counted as `other`, so a new
18
+ * collector file that nobody registered still shows up in the footprint
19
+ * rather than hiding.
20
+ *
21
+ * Every collector entry carries the one-line reason it is bounded; the
22
+ * runbook (`docs/runbooks/cockpit-launchd-sync.md`, "What the state
23
+ * directory holds") prints the same table. Metadata only: entry names are
24
+ * fixed strings, never a person's path.
25
+ */
26
+ import fs from "node:fs/promises";
27
+ import path from "node:path";
28
+ import { directoryBytes } from "./disk-usage-files.js";
29
+ /** The sibling root where agent-written studies live, never under the collector. */
30
+ export const STUDIES_DIR_SUFFIX = "-studies";
31
+ /**
32
+ * The collector's own entries first (checked before the study prefixes), then
33
+ * the studies. Order matters only where a study prefix could shadow a
34
+ * collector name; none does today, and the test pins that.
35
+ */
36
+ export const STATE_DIR_RULES = [
37
+ { name: "raw-evidence", match: "exact", class: "staging", bound: "staged copies go the tick the server commits them; 256 MiB cap; uncommitted evidence is never deleted, only counted" },
38
+ { name: "sync.log", match: "prefix", class: "logs", bound: "rotated every tick at 10 MiB, at most 2 archives (20 MB) per stream" },
39
+ { name: "sync.err.log", match: "prefix", class: "logs", bound: "rotated every tick at 10 MiB, at most 2 archives (20 MB) per stream" },
40
+ { name: "spool", match: "exact", class: "spool", bound: "upload queue; each entry is deleted once the ingest door accepts it" },
41
+ { name: "cursors", match: "exact", class: "state", bound: "raw-evidence ledger and reconcile answers capped at 5,000 rows each; backfill cursors one small file each" },
42
+ { name: "raw-evidence-staging.json", match: "exact", class: "state", bound: "staged-object index and delivery attempts capped at 5,000 rows each" },
43
+ { name: "work-contexts", match: "exact", class: "state", bound: "one small file per worktree ever bound with `cockpit start`; overwritten, not pruned (a few KB each)" },
44
+ { name: "active-work-context.json", match: "exact", class: "state", bound: "one file, overwritten" },
45
+ { name: "root-work-scan.json", match: "exact", class: "state", bound: "one file, overwritten by each root scan" },
46
+ { name: "setup-receipt.json", match: "exact", class: "state", bound: "one file, overwritten" },
47
+ { name: "memory-install-receipt.json", match: "exact", class: "state", bound: "one file, overwritten" },
48
+ { name: "last-prune.json", match: "exact", class: "state", bound: "one file, overwritten every tick" },
49
+ { name: "last-turn-trace.json", match: "exact", class: "state", bound: "one file, overwritten" },
50
+ { name: "sync.lock", match: "exact", class: "state", bound: "lock file, removed when the tick ends" },
51
+ { name: "backfill.lock", match: "exact", class: "state", bound: "lock file, removed when the backfill ends" },
52
+ { name: ".last-", match: "prefix", class: "state", bound: "24-byte throttle markers for self-update, autostart repair and the memory install" },
53
+ { name: "read-cache", match: "exact", class: "state", bound: "one response file per distinct Tower read, overwritten on refresh" },
54
+ { name: "memory-hook-stats.json", match: "exact", class: "state", bound: "fixed-size histograms, overwritten (BLI Memory hooks)" },
55
+ { name: "memory-hook-samples", match: "exact", class: "state", bound: "hourly content-free timing samples (BLI Memory hooks); not yet pruned, a few MB a week" },
56
+ { name: "memory-recall-seen", match: "exact", class: "state", bound: "one file per agent session, deleted after 7 days (BLI Memory hooks)" },
57
+ { name: "memory-stop-anchors.json", match: "exact", class: "state", bound: "one offset per transcript, overwritten (BLI Memory hooks)" },
58
+ { name: "memory-daemon-spawn.json", match: "exact", class: "state", bound: "one spawn stamp, overwritten (BLI Memory daemon)" },
59
+ { name: "memory-daemon.sock", match: "exact", class: "state", bound: "the daemon's socket, zero bytes" },
60
+ // Studies written by agents and scripts, not by the collector. Counted by
61
+ // name only, never sized, never deleted.
62
+ { name: "memory-bench", match: "prefix", class: "study", bound: "agent study; belongs in bli-cockpit-studies" },
63
+ { name: "memory-extract-", match: "prefix", class: "study", bound: "agent study; belongs in bli-cockpit-studies" },
64
+ { name: "memory-questions", match: "prefix", class: "study", bound: "agent study; belongs in bli-cockpit-studies" },
65
+ { name: "memory-hook-context-sweep", match: "prefix", class: "study", bound: "agent study; belongs in bli-cockpit-studies" },
66
+ { name: "memory-anchor-probe", match: "prefix", class: "study", bound: "agent study; belongs in bli-cockpit-studies" },
67
+ { name: "memory-dup-threshold", match: "prefix", class: "study", bound: "agent study; belongs in bli-cockpit-studies" },
68
+ { name: "memory-judge-effort", match: "prefix", class: "study", bound: "agent study; belongs in bli-cockpit-studies" },
69
+ { name: "memory-recurrence-backfill", match: "prefix", class: "study", bound: "agent study; belongs in bli-cockpit-studies" },
70
+ { name: "memory-supersede-from-record", match: "prefix", class: "study", bound: "agent study; belongs in bli-cockpit-studies" },
71
+ { name: "memory-reconcile-precision", match: "prefix", class: "study", bound: "agent study; belongs in bli-cockpit-studies" },
72
+ { name: "memory-harvest-dedup", match: "prefix", class: "study", bound: "agent study; belongs in bli-cockpit-studies" },
73
+ { name: "chat-model-bench", match: "prefix", class: "study", bound: "agent study; belongs in bli-cockpit-studies" },
74
+ { name: "gpt6-bench-", match: "prefix", class: "study", bound: "agent study; belongs in bli-cockpit-studies" },
75
+ { name: "manual-study-evidence-vault", match: "prefix", class: "study", bound: "hand-made evidence vault; `cockpit clean --all-committed` removes it once every file is committed" },
76
+ { name: "issue-import", match: "exact", class: "study", bound: "one-off import receipts; belongs in bli-cockpit-studies" },
77
+ { name: "analysis", match: "exact", class: "study", bound: "local harvest-analysis output; belongs in bli-cockpit-studies" },
78
+ { name: "task-type-backlog", match: "prefix", class: "study", bound: "local harvest-analysis output; belongs in bli-cockpit-studies" },
79
+ ];
80
+ /** Which rule an entry name falls under, or null for `other`. */
81
+ export function classifyStateEntry(name) {
82
+ for (const rule of STATE_DIR_RULES) {
83
+ if (rule.match === "exact" ? name === rule.name : name.startsWith(rule.name)) {
84
+ return rule;
85
+ }
86
+ }
87
+ return null;
88
+ }
89
+ /**
90
+ * The sibling studies root, on either host family: the state dir's own path
91
+ * with `-studies` appended, so `/Users/x/.local/state/bli-cockpit` becomes
92
+ * `/Users/x/.local/state/bli-cockpit-studies` and
93
+ * `C:\Users\x\.local\state\bli-cockpit` becomes
94
+ * `C:\Users\x\.local\state\bli-cockpit-studies`. No separator is joined, so
95
+ * neither layout can be spelled wrong.
96
+ */
97
+ export function studiesStateDir(paths) {
98
+ return `${paths.state_dir.replace(/[\\/]+$/u, "")}${STUDIES_DIR_SUFFIX}`;
99
+ }
100
+ /**
101
+ * Sizes the `state` and `other` entries of the state directory. Staging,
102
+ * logs and spool are measured by their own readers and skipped here; studies
103
+ * are counted but never walked, because a gigabyte of somebody's bench run is
104
+ * not worth a stat per file on every doctor call.
105
+ */
106
+ export async function readStateDirFootprint(paths) {
107
+ const footprint = {
108
+ state_bytes: 0,
109
+ other_bytes: 0,
110
+ other_entries: 0,
111
+ study_entries: 0,
112
+ };
113
+ const entries = await fs
114
+ .readdir(paths.state_dir, { withFileTypes: true })
115
+ .catch(() => []);
116
+ for (const entry of entries) {
117
+ const rule = classifyStateEntry(entry.name);
118
+ const cls = rule?.class ?? "other";
119
+ if (cls === "staging" || cls === "logs" || cls === "spool")
120
+ continue;
121
+ if (cls === "study") {
122
+ footprint.study_entries += 1;
123
+ continue;
124
+ }
125
+ const full = path.join(paths.state_dir, entry.name);
126
+ const bytes = entry.isDirectory()
127
+ ? await directoryBytes(full)
128
+ : await fs.stat(full).then((info) => info.size, () => 0);
129
+ if (cls === "state") {
130
+ footprint.state_bytes += bytes;
131
+ }
132
+ else {
133
+ footprint.other_bytes += bytes;
134
+ footprint.other_entries += 1;
135
+ }
136
+ }
137
+ return footprint;
138
+ }
@@ -14,6 +14,7 @@
14
14
  * `upload-session-reports.ts`, which builds its own provenance without a
15
15
  * full envelope.
16
16
  */
17
+ import { priorSnapshotsFromCursor, } from "./adapters/raw-evidence-snapshot-supersede.js";
17
18
  import { TelemetryIngestEnvelopeSchema, } from "@bli-cockpit/telemetry-core";
18
19
  import path from "node:path";
19
20
  import { getCollectorRuntimePaths, LOCAL_COLLECTOR_VERSION, readLocalCollectorConfig, readLocalCollectorSessionFile, readLocalSessionReference, readLocalWorkContextForRepo, } from "./local-state.js";
@@ -120,6 +121,10 @@ export async function buildLocalAmbientEnvelope(options = {}) {
120
121
  operatorId: session.operator_id,
121
122
  workContextId: uploadContext.work_context_id,
122
123
  }),
124
+ rawEvidencePriorSnapshots: priorSnapshotsFromCursor(transcriptCursorObjects(options.cursorObjects), (objectKey) => rawEvidenceObjectKeyBelongsToWorkContext(objectKey, {
125
+ operatorId: session.operator_id,
126
+ workContextId: uploadContext.work_context_id,
127
+ })),
123
128
  rawEvidenceByteBudget: options.rawEvidenceByteBudget,
124
129
  rawEvidenceObjectBudget: options.rawEvidenceObjectBudget,
125
130
  rawEvidenceBudget: options.rawEvidenceBudget,
@@ -188,6 +193,10 @@ function reusableContentHashes(options, context) {
188
193
  .filter(([, entry]) => rawEvidenceObjectKeyBelongsToWorkContext(entry.object_key, context))
189
194
  .map(([hash]) => hash));
190
195
  }
196
+ /** Cursor entries that remember a transcript source (BLI-11088); the rest are ignored. */
197
+ function transcriptCursorObjects(objects) {
198
+ return Object.fromEntries(Object.entries(objects ?? {}).filter((pair) => "source_key" in pair[1] && Boolean(pair[1].source_key)));
199
+ }
191
200
  export function makeUploadWorkContext(options) {
192
201
  const provenance = makeCollectorProvenance({
193
202
  context: options.activeContext,
@@ -1,23 +1,3 @@
1
- /**
2
- * `syncLocalAmbientEnvelope`: one ambient sync, end to end.
3
- *
4
- * 1. Assemble the envelope — or record why this machine cannot upload at all
5
- * and rethrow, so `cockpit status` names the blocker.
6
- * 2. Deliver the raw evidence objects, skipping anything inside its delivery
7
- * backoff window (`upload-evidence-delivery.ts`).
8
- * 3. Reconcile the envelope with what actually became durable, POST it to
9
- * ingest, and refuse to believe a receipt we cannot read
10
- * (`upload-ingest-receipt.ts`).
11
- * 4. Advance the content cursor, report any agent images, and write the ledger
12
- * row — a retry when a later sync could still rescue the gap, a named
13
- * blocker when it never can, nothing extra when the sync was clean.
14
- * 5. On any failure, spool a retry that carries its own reason.
15
- *
16
- * The whole file is built around one rule from the fleet contract: a sync that
17
- * did not collect everything must never read green. Objects held by backoff are
18
- * reported as named failures, permanent rejections are logged and blocked
19
- * rather than silently retried forever, and the ledger row always says why.
20
- */
21
1
  import { getCollectorRuntimePaths } from "./local-state.js";
22
2
  import { uploadRawEvidenceFilesChunked, } from "./evidence-upload-client.js";
23
3
  import { markObjectCommitted, readRawEvidenceCursor, writeRawEvidenceCursor, } from "./cursors/raw-evidence-cursor.js";
@@ -80,7 +60,7 @@ export async function syncLocalAmbientEnvelope(options = {}) {
80
60
  built,
81
61
  envelope: applyRawEvidenceUploadOutcomes(built.envelope, uploadOutcomes),
82
62
  });
83
- advanceRawEvidenceCursor(cursor, uploadOutcomes, attemptedAt);
63
+ advanceRawEvidenceCursor(cursor, uploadOutcomes, attemptedAt, built.raw_evidence_upload_files);
84
64
  await writeRawEvidenceCursor(paths, cursor);
85
65
  await reportAgentImageArtifacts({
86
66
  fetchImpl,
@@ -221,17 +201,25 @@ async function buildEnvelopeOrRecordBlocker(paths, options, cursorObjects, attem
221
201
  * bytes instead of re-uploading them. A failed upload is deliberately absent:
222
202
  * recording it would make the next sync skip a file that never landed.
223
203
  */
224
- function advanceRawEvidenceCursor(cursor, outcomes, attemptedAt) {
204
+ function advanceRawEvidenceCursor(cursor, outcomes, attemptedAt, files = []) {
205
+ // BLI-11088: a transcript's source key and snapshot facts ride into the
206
+ // cursor, so the next sync can prove its snapshot rebuilds this one.
207
+ const snapshotByHash = new Map(files.filter((file) => file.snapshot && file.source_key && file.pointer.content_hash_sha256)
208
+ .map((file) => [file.pointer.content_hash_sha256, file]));
225
209
  for (const outcome of outcomes) {
226
210
  if (outcome.upload_state === "upload_failed")
227
211
  continue;
228
212
  const contentHash = outcome.pointer.content_hash_sha256;
229
213
  if (!contentHash)
230
214
  continue;
215
+ const source = snapshotByHash.get(contentHash);
231
216
  markObjectCommitted(cursor, contentHash, {
232
217
  object_key: outcome.object_key,
233
218
  byte_size: outcome.pointer.byte_size ?? 0,
234
219
  committed_at: attemptedAt,
220
+ ...(source
221
+ ? { source_key: source.source_key, checkpoints: source.snapshot.checkpoints }
222
+ : {}),
235
223
  });
236
224
  }
237
225
  cursor.updated_at = attemptedAt;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bli-cockpit/cli",
3
- "version": "0.2.150",
3
+ "version": "0.2.155",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
@@ -24,11 +24,11 @@
24
24
  "pretypecheck": "node ../../scripts/build-workspace-dep.mjs @bli-cockpit/cli",
25
25
  "typecheck": "node -e \"await import('./dist/commands/public-root.js')\"",
26
26
  "pretest": "node ../../scripts/build-workspace-dep.mjs @bli-cockpit/cli",
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-package-pack.mjs --workspace=@bli-cockpit/cli"
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.39",
31
- "@bli-cockpit/mcp": "0.1.73",
32
- "@bli-cockpit/telemetry-core": "0.1.51"
30
+ "@bli-cockpit/memory-mcp": "0.1.41",
31
+ "@bli-cockpit/mcp": "0.1.75",
32
+ "@bli-cockpit/telemetry-core": "0.1.53"
33
33
  }
34
34
  }