@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.
- package/dist/adapters/local-sources.js +1 -0
- package/dist/adapters/raw-evidence-collection-state.js +1 -0
- package/dist/adapters/raw-evidence-facts.js +1 -0
- package/dist/adapters/raw-evidence-manifest.js +5 -0
- package/dist/adapters/raw-evidence-sanitize.js +82 -20
- package/dist/adapters/raw-evidence-snapshot-supersede.js +82 -0
- package/dist/adapters/raw-evidence-transcript-file.js +10 -0
- package/dist/agent-rules.js +3 -3
- package/dist/commands/backfill-lock-wait.js +49 -0
- package/dist/commands/backfill-outside-roots.js +97 -0
- package/dist/commands/backfill-reasons.js +17 -0
- package/dist/commands/backfill-scan.js +8 -3
- package/dist/commands/backfill-scope.js +14 -1
- package/dist/commands/backfill-session-report.js +3 -0
- package/dist/commands/backfill-upload.js +6 -0
- package/dist/commands/backfill.js +11 -5
- package/dist/commands/clean.js +31 -13
- package/dist/commands/doctor-budget.js +69 -0
- package/dist/commands/doctor-disk-words.js +81 -3
- package/dist/commands/doctor-lock-wait.js +12 -3
- package/dist/commands/doctor-pipeline-verdicts.js +2 -0
- package/dist/commands/doctor-pipeline.js +49 -24
- package/dist/commands/doctor.js +26 -4
- package/dist/commands/heartbeat.js +1 -1
- package/dist/commands/local-arg-values.js +9 -0
- package/dist/commands/local-args-collector-setup.js +4 -2
- package/dist/commands/local-args-collector-work.js +9 -1
- package/dist/commands/local-help-commands.js +11 -8
- package/dist/commands/local-help.js +1 -1
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/status-disk-line.js +22 -0
- package/dist/commands/status.js +5 -2
- package/dist/commands/sync-followups-staging.js +13 -10
- package/dist/commands/sync.js +7 -4
- package/dist/cursors/raw-evidence-cursor.js +14 -0
- package/dist/disk-prune.js +19 -23
- package/dist/disk-retention.js +12 -9
- package/dist/disk-usage-footprint.js +10 -4
- package/dist/evidence-reconcile-client.js +2 -2
- package/dist/log-rotation.js +31 -10
- package/dist/prune-receipt.js +87 -0
- package/dist/raw-evidence-gc.js +55 -15
- package/dist/state-dir-audit.js +138 -0
- package/dist/upload-envelope-build.js +9 -0
- package/dist/upload-sync.js +10 -22
- package/package.json +5 -5
package/dist/raw-evidence-gc.js
CHANGED
|
@@ -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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
//
|
|
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).
|
|
57
|
-
//
|
|
58
|
-
|
|
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,
|
package/dist/upload-sync.js
CHANGED
|
@@ -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.
|
|
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.
|
|
31
|
-
"@bli-cockpit/mcp": "0.1.
|
|
32
|
-
"@bli-cockpit/telemetry-core": "0.1.
|
|
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
|
}
|