@bli-cockpit/cli 0.2.237 → 0.2.239
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/attribution-core-fallbacks.js +15 -11
- package/dist/adapters/attribution-core-score.js +111 -87
- package/dist/adapters/car-state.js +38 -35
- package/dist/adapters/claude-attribution-discovery.js +81 -56
- package/dist/adapters/claude-attribution-score.js +108 -94
- package/dist/adapters/claude-attribution-signals.js +52 -42
- package/dist/adapters/codex-attribution.js +42 -33
- package/dist/adapters/codex-session-signals.js +11 -9
- package/dist/adapters/git-state.js +56 -50
- package/dist/adapters/local-sources.js +52 -43
- package/dist/adapters/raw-evidence-git-diff.js +4 -2
- package/dist/adapters/raw-evidence-sanitize.js +15 -12
- package/dist/adapters/raw-evidence-transcript-file.js +32 -31
- package/dist/autostart-self-heal.js +18 -2
- package/dist/commands/agent-session-report.js +51 -59
- package/dist/commands/agent-setup-observed.js +30 -23
- package/dist/commands/agent-setup.js +96 -45
- package/dist/commands/autostart-heal.js +8 -2
- package/dist/commands/autostart-reading.js +3 -0
- package/dist/commands/backfill-evidence-outcomes.js +9 -6
- package/dist/commands/backfill-issues.js +3 -0
- package/dist/commands/backfill-scan.js +27 -22
- package/dist/commands/backfill-upload.js +69 -60
- package/dist/commands/backfill.js +26 -21
- package/dist/commands/brief-done.js +4 -1
- package/dist/commands/brief-edit.js +71 -46
- package/dist/commands/cal-detail.js +35 -0
- package/dist/commands/cal-direct.js +119 -0
- package/dist/commands/cal-overlay.js +73 -30
- package/dist/commands/cal.js +62 -25
- package/dist/commands/clean.js +4 -1
- package/dist/commands/docs-read.js +5 -2
- package/dist/commands/docs.js +38 -6
- package/dist/commands/doctor-backfill.js +88 -71
- package/dist/commands/doctor-pipeline.js +59 -46
- package/dist/commands/heartbeat-post.js +116 -0
- package/dist/commands/heartbeat-token-lock.js +46 -33
- package/dist/commands/heartbeat.js +43 -125
- package/dist/commands/install-update.js +17 -15
- package/dist/commands/issue-show.js +27 -28
- package/dist/commands/issue-versions.js +93 -0
- package/dist/commands/issue-write.js +70 -23
- package/dist/commands/issue.js +73 -37
- package/dist/commands/jarvis-live-answer.js +99 -0
- package/dist/commands/jarvis-render.js +1 -94
- package/dist/commands/jarvis-trace.js +30 -22
- package/dist/commands/jarvis-turn-report.js +124 -0
- package/dist/commands/jarvis-turn.js +108 -182
- package/dist/commands/jarvis.js +26 -17
- package/dist/commands/local-args-collector-setup.js +82 -84
- package/dist/commands/local-args-collector-work.js +37 -38
- package/dist/commands/local-args-tower-admin.js +155 -142
- package/dist/commands/local-args-tower-cal.js +69 -54
- package/dist/commands/local-args-tower-careers.js +73 -54
- package/dist/commands/local-args-tower-docs-msg.js +79 -51
- package/dist/commands/local-args-tower-github.js +26 -20
- package/dist/commands/local-args-tower-hits.js +32 -20
- package/dist/commands/local-args-tower-manager.js +85 -4
- package/dist/commands/local-args-tower-memory.js +139 -117
- package/dist/commands/local-args-tower-msg.js +24 -6
- package/dist/commands/local-args-tower-notes.js +346 -0
- package/dist/commands/local-args-tower-pages.js +100 -378
- package/dist/commands/local-args-tower-settings.js +28 -29
- package/dist/commands/local-args-tower-setup.js +11 -4
- package/dist/commands/local-args-tower-work.js +74 -37
- package/dist/commands/local-args.js +80 -129
- package/dist/commands/local-help-commands-tower-ops.js +15 -4
- package/dist/commands/local-help-commands-tower.js +20 -7
- package/dist/commands/local-help-commands.js +342 -341
- package/dist/commands/local-help.js +87 -83
- package/dist/commands/local-run-guards.js +100 -0
- package/dist/commands/local.js +137 -206
- package/dist/commands/login.js +13 -9
- package/dist/commands/logs.js +55 -38
- package/dist/commands/mail-attachments.js +44 -26
- package/dist/commands/manager-render-home.js +208 -0
- package/dist/commands/manager-render.js +7 -3
- package/dist/commands/manager-requests.js +33 -0
- package/dist/commands/manager.js +39 -5
- package/dist/commands/memory-consolidation.js +18 -9
- package/dist/commands/memory-daemon-probe.js +37 -28
- package/dist/commands/memory-install-config.js +64 -60
- package/dist/commands/memory-install.js +21 -41
- package/dist/commands/memory-requeue.js +26 -19
- package/dist/commands/memory-rules.js +26 -18
- package/dist/commands/memory-search-print.js +26 -0
- package/dist/commands/memory-store.js +14 -30
- package/dist/commands/memory-verb-list.js +49 -48
- package/dist/commands/msg-read-line.js +32 -0
- package/dist/commands/msg.js +45 -3
- package/dist/commands/notes-accounts.js +49 -32
- package/dist/commands/notes-sync-loop.js +115 -89
- package/dist/commands/notes-wisprflow-login.js +44 -23
- package/dist/commands/notes-writes.js +115 -100
- package/dist/commands/notes.js +33 -55
- package/dist/commands/onboard-flows.js +69 -51
- package/dist/commands/onboard.js +11 -9
- package/dist/commands/ops-render-memory.js +10 -7
- package/dist/commands/ops-render.js +33 -30
- package/dist/commands/ops.js +3 -0
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/session-sync-outside-roots.js +40 -36
- package/dist/commands/session-sync-plan.js +14 -10
- package/dist/commands/session-sync-upload.js +24 -22
- package/dist/commands/session-sync.js +19 -8
- package/dist/commands/setup.js +13 -0
- package/dist/commands/slack.js +25 -16
- package/dist/commands/sources-check.js +13 -7
- package/dist/commands/sources-discovery.js +25 -17
- package/dist/commands/sources-verdict.js +19 -18
- package/dist/commands/speed.js +23 -19
- package/dist/commands/start.js +20 -16
- package/dist/commands/sync-doctor.js +52 -48
- package/dist/commands/sync.js +22 -16
- package/dist/commands/usage-telemetry-toml.js +71 -45
- package/dist/commands/usage.js +94 -68
- package/dist/commands/workbook-render.js +10 -7
- package/dist/evidence-reconcile-client.js +75 -64
- package/dist/evidence-redelivery-plan.js +37 -34
- package/dist/evidence-redelivery.js +32 -29
- package/dist/evidence-transcripts-client.js +20 -20
- package/dist/evidence-upload-object.js +86 -89
- package/dist/evidence-upload-plan.js +94 -83
- package/dist/evidence-upload-terminal.js +45 -50
- package/dist/image-description-session.js +156 -159
- package/dist/local-state-pairing-code.js +39 -31
- package/dist/local-state-status.js +9 -2
- package/dist/onboarding-roots.js +5 -8
- package/dist/process-runner.js +8 -8
- package/dist/raw-evidence-gc.js +33 -38
- package/dist/raw-evidence-stuck-summary.js +7 -14
- package/dist/repo-identity.js +26 -23
- package/dist/scheduled-self-update.js +11 -15
- package/dist/second-install.js +3 -0
- package/dist/slim/codex-rules.js +23 -36
- package/dist/spool/local-spool-parse.js +13 -12
- package/dist/sync-lock.js +5 -15
- package/dist/tower-client.js +36 -53
- package/dist/tower-stream.js +7 -24
- package/dist/upload-agent-artifacts.js +58 -64
- package/dist/upload-envelope-build.js +60 -59
- package/dist/upload-evidence-delivery-reconcile.js +58 -56
- package/dist/upload-evidence-delivery-summary.js +32 -34
- package/dist/upload-ingest-receipt.js +20 -23
- package/dist/upload-session-reports-queue.js +4 -1
- package/dist/upload-session-reports-wire.js +64 -68
- package/dist/upload-sync.js +171 -157
- package/package.json +4 -4
|
@@ -32,20 +32,7 @@ export async function pairLocalCollectorViaLink(options = {}) {
|
|
|
32
32
|
if (!fetchImpl) {
|
|
33
33
|
throw new PairingCodeError("fetch_unavailable", "global fetch is unavailable; use Node.js 20 or newer.");
|
|
34
34
|
}
|
|
35
|
-
const minted = options
|
|
36
|
-
? null
|
|
37
|
-
: await mintPairingCode(fetchImpl, dashboardUrl, {
|
|
38
|
-
device_id: deviceId,
|
|
39
|
-
device_name: deviceName,
|
|
40
|
-
claimed_owner_email: options.claimedOwnerEmail,
|
|
41
|
-
collector_version: LOCAL_COLLECTOR_VERSION,
|
|
42
|
-
});
|
|
43
|
-
if (minted) {
|
|
44
|
-
options.onLinkReady?.({
|
|
45
|
-
connect_url: minted.connect_url,
|
|
46
|
-
expires_at: minted.expires_at,
|
|
47
|
-
});
|
|
48
|
-
}
|
|
35
|
+
const minted = await preparePairingLink(options, fetchImpl, dashboardUrl, deviceId, deviceName);
|
|
49
36
|
const session = await pollExchange(fetchImpl, dashboardUrl, {
|
|
50
37
|
paths,
|
|
51
38
|
code: options.pairCode ?? minted?.code ?? "",
|
|
@@ -66,6 +53,23 @@ export async function pairLocalCollectorViaLink(options = {}) {
|
|
|
66
53
|
direction: minted ? "cli_first" : "browser_first",
|
|
67
54
|
};
|
|
68
55
|
}
|
|
56
|
+
async function preparePairingLink(options, fetchImpl, dashboardUrl, deviceId, deviceName) {
|
|
57
|
+
const minted = options.pairCode
|
|
58
|
+
? null
|
|
59
|
+
: await mintPairingCode(fetchImpl, dashboardUrl, {
|
|
60
|
+
device_id: deviceId,
|
|
61
|
+
device_name: deviceName,
|
|
62
|
+
claimed_owner_email: options.claimedOwnerEmail,
|
|
63
|
+
collector_version: LOCAL_COLLECTOR_VERSION,
|
|
64
|
+
});
|
|
65
|
+
if (minted) {
|
|
66
|
+
options.onLinkReady?.({
|
|
67
|
+
connect_url: minted.connect_url,
|
|
68
|
+
expires_at: minted.expires_at,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return minted;
|
|
72
|
+
}
|
|
69
73
|
async function mintPairingCode(fetchImpl, dashboardUrl, body) {
|
|
70
74
|
const response = await fetchImpl(`${dashboardUrl}/api/devices/pair/code`, {
|
|
71
75
|
method: "POST",
|
|
@@ -107,23 +111,7 @@ async function pollExchange(fetchImpl, dashboardUrl, options) {
|
|
|
107
111
|
let attempts = 0;
|
|
108
112
|
while (Date.now() <= deadline) {
|
|
109
113
|
attempts += 1;
|
|
110
|
-
const
|
|
111
|
-
method: "POST",
|
|
112
|
-
headers: { "Content-Type": "application/json" },
|
|
113
|
-
body: JSON.stringify({
|
|
114
|
-
code: options.code,
|
|
115
|
-
...(options.clientSecret ? { client_secret: options.clientSecret } : {}),
|
|
116
|
-
device_id: options.deviceId,
|
|
117
|
-
device_name: options.deviceName,
|
|
118
|
-
collector_version: LOCAL_COLLECTOR_VERSION,
|
|
119
|
-
}),
|
|
120
|
-
});
|
|
121
|
-
const parsed = await readResponseJson(response, "exchange");
|
|
122
|
-
const record = asRecord(parsed);
|
|
123
|
-
const status = readString(record, "status");
|
|
124
|
-
if (!response.ok) {
|
|
125
|
-
throw refusalFrom(readString(record, "reason") ?? "pair_exchange_refused", response, parsed);
|
|
126
|
-
}
|
|
114
|
+
const { record, status } = await requestPairingExchange(fetchImpl, dashboardUrl, options);
|
|
127
115
|
if (status === "paired") {
|
|
128
116
|
const session = parseExchangedSession(record, options.paths.session_file);
|
|
129
117
|
await writeJsonFile(options.paths.session_file, session);
|
|
@@ -141,6 +129,26 @@ async function pollExchange(fetchImpl, dashboardUrl, options) {
|
|
|
141
129
|
console.error(`${TAG} gave up waiting`, JSON.stringify({ reason: "pair_exchange_timeout", attempts }));
|
|
142
130
|
throw new PairingCodeError("pair_exchange_timeout", "Timed out waiting for the browser sign-in. Run `cockpit login` again.");
|
|
143
131
|
}
|
|
132
|
+
async function requestPairingExchange(fetchImpl, dashboardUrl, options) {
|
|
133
|
+
const response = await fetchImpl(`${dashboardUrl}/api/devices/pair/exchange`, {
|
|
134
|
+
method: "POST",
|
|
135
|
+
headers: { "Content-Type": "application/json" },
|
|
136
|
+
body: JSON.stringify({
|
|
137
|
+
code: options.code,
|
|
138
|
+
...(options.clientSecret ? { client_secret: options.clientSecret } : {}),
|
|
139
|
+
device_id: options.deviceId,
|
|
140
|
+
device_name: options.deviceName,
|
|
141
|
+
collector_version: LOCAL_COLLECTOR_VERSION,
|
|
142
|
+
}),
|
|
143
|
+
});
|
|
144
|
+
const parsed = await readResponseJson(response, "exchange");
|
|
145
|
+
const record = asRecord(parsed);
|
|
146
|
+
const status = readString(record, "status");
|
|
147
|
+
if (!response.ok) {
|
|
148
|
+
throw refusalFrom(readString(record, "reason") ?? "pair_exchange_refused", response, parsed);
|
|
149
|
+
}
|
|
150
|
+
return { record, status };
|
|
151
|
+
}
|
|
144
152
|
function parseExchangedSession(record, sessionFilePath) {
|
|
145
153
|
const session = record?.["session"];
|
|
146
154
|
if (!session || typeof session !== "object") {
|
|
@@ -18,6 +18,9 @@ import { readLocalWorkContextForRepo, workDisplayLabel, } from "./local-state-wo
|
|
|
18
18
|
export async function inspectLocalCollectorStatus(options = {}) {
|
|
19
19
|
const now = options.now ?? new Date();
|
|
20
20
|
const reading = await readCollectorStatus(options, now);
|
|
21
|
+
return collectorStatusFromReading(reading, now);
|
|
22
|
+
}
|
|
23
|
+
function collectorStatusFromReading(reading, now) {
|
|
21
24
|
const { paths, config, session, identity, context, branch } = reading;
|
|
22
25
|
const { uploadSpool, healthOutbox, stuckEvidence } = reading;
|
|
23
26
|
const freshness = classifyCollectorFreshness(context, uploadSpool.last_upload_success_at, now);
|
|
@@ -132,7 +135,7 @@ function classifyUploadState(config, uploadSpool, session) {
|
|
|
132
135
|
* queue, and each names its own reason and count.
|
|
133
136
|
*/
|
|
134
137
|
function describeCollectorStatus(reading) {
|
|
135
|
-
const { paths, config, session, context, uploadSpool, healthOutbox
|
|
138
|
+
const { paths, config, session, context, uploadSpool, healthOutbox } = reading;
|
|
136
139
|
const details = [];
|
|
137
140
|
details.push(config
|
|
138
141
|
? `Config exists at ${paths.config_file}.`
|
|
@@ -158,6 +161,11 @@ function describeCollectorStatus(reading) {
|
|
|
158
161
|
if (healthOutbox.pending_count > 0) {
|
|
159
162
|
details.push(`Collector health retry pending: ${healthOutbox.pending_count} sanitized receipt(s) queued since ${healthOutbox.oldest_created_at ?? "unknown"}.`);
|
|
160
163
|
}
|
|
164
|
+
appendEvidenceStatus(details, reading);
|
|
165
|
+
return details;
|
|
166
|
+
}
|
|
167
|
+
function appendEvidenceStatus(details, reading) {
|
|
168
|
+
const { stuckEvidence } = reading;
|
|
161
169
|
// BLI-3797: the line above only sees objects whose delivery FAILED. This one
|
|
162
170
|
// sees evidence that is on this disk and not in the bucket, however it got
|
|
163
171
|
// that way — which is the only question that answers "has everything landed?".
|
|
@@ -176,7 +184,6 @@ function describeCollectorStatus(reading) {
|
|
|
176
184
|
if (stuckEvidence.ghost_record_count > 0) {
|
|
177
185
|
details.push(`${stuckEvidence.ghost_record_count} old delivery record(s) have nothing left to send; no action needed.`);
|
|
178
186
|
}
|
|
179
|
-
return details;
|
|
180
187
|
}
|
|
181
188
|
/**
|
|
182
189
|
* Whether this machine looks alive right now. A successful upload counts as
|
package/dist/onboarding-roots.js
CHANGED
|
@@ -11,14 +11,8 @@ import { isSamePath, normalizeCollectionRoots, } from "./root-normalization.js";
|
|
|
11
11
|
export { HOME_ROOT_DECLINE_PATH_PROMPT, HOME_ROOT_NO_FOLDER_CHOSEN_MESSAGE, homeRootConsentPrompt, missingCollectionRootMessage, rootRejectionExplanation, } from "./onboarding-root-guidance.js";
|
|
12
12
|
export const COLLECTION_ROOT_REQUIRED = "collection_root_required";
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* A type rather than a message prefix (BLI-3551): the health-receipt classifier
|
|
17
|
-
* used to recognise this by searching the error text, which is the same habit
|
|
18
|
-
* that filed `session_report_unposted` as an auth failure. The message is
|
|
19
|
-
* unchanged — `collection_root_required: <what to do about it>` — so every
|
|
20
|
-
* operator-facing string and every test that reads one still matches; what
|
|
21
|
-
* changed is that the classifier reads the type.
|
|
14
|
+
* Keep root failures typed so health classification never guesses from message text.
|
|
15
|
+
* The existing collection_root_required prefix remains a public contract.
|
|
22
16
|
*/
|
|
23
17
|
export class CollectionRootRequiredError extends Error {
|
|
24
18
|
reason = COLLECTION_ROOT_REQUIRED;
|
|
@@ -64,6 +58,9 @@ export async function resolveOnboardingRoots(options) {
|
|
|
64
58
|
}
|
|
65
59
|
return promptForRoots(options, "Collection root(s), comma-separated: ");
|
|
66
60
|
}
|
|
61
|
+
return resolveSuggestedOrLikelyRoots(options);
|
|
62
|
+
}
|
|
63
|
+
async function resolveSuggestedOrLikelyRoots(options) {
|
|
67
64
|
const suggestions = normalizeRootsDetailed(options.suggestedRoots ?? [], { homeDir: options.homeDir, allowHomeRoot: options.allowHomeRoot }).roots;
|
|
68
65
|
if (options.interactive && suggestions.length > 0 && await requirePrompt(options).confirm(`Sessions were found in:\n${suggestions.join("\n")}\nCollect from these folders? [Y/n] `)) {
|
|
69
66
|
return resolvedRoots(options, suggestions, "prompt", true);
|
package/dist/process-runner.js
CHANGED
|
@@ -37,14 +37,7 @@ export function createCapturedExecRunner(options = {}) {
|
|
|
37
37
|
windowsVerbatimArguments: invocation.windowsVerbatimArguments,
|
|
38
38
|
...(runOptions?.timeoutMs ? { timeout: runOptions.timeoutMs } : {}),
|
|
39
39
|
}, (error, stdout, stderr) => {
|
|
40
|
-
|
|
41
|
-
const code = error == null
|
|
42
|
-
? 0
|
|
43
|
-
: error.killed === true && runOptions?.timeoutMs
|
|
44
|
-
? 124
|
|
45
|
-
: typeof error.code === "number"
|
|
46
|
-
? error.code
|
|
47
|
-
: 1;
|
|
40
|
+
const code = capturedExitCode(error, runOptions?.timeoutMs);
|
|
48
41
|
resolve({
|
|
49
42
|
code,
|
|
50
43
|
stdout: stdout ?? "",
|
|
@@ -142,4 +135,11 @@ function escapeWindowsCmdArgument(value) {
|
|
|
142
135
|
escaped = `"${escaped}"`;
|
|
143
136
|
escaped = escaped.replace(CMD_META_CHARS, "^$1");
|
|
144
137
|
return escaped.replace(CMD_META_CHARS, "^$1");
|
|
138
|
+
}
|
|
139
|
+
function capturedExitCode(error, timeoutMs) {
|
|
140
|
+
if (error == null)
|
|
141
|
+
return 0;
|
|
142
|
+
if (error.killed === true && timeoutMs)
|
|
143
|
+
return 124;
|
|
144
|
+
return typeof error.code === "number" ? error.code : 1;
|
|
145
145
|
}
|
package/dist/raw-evidence-gc.js
CHANGED
|
@@ -83,23 +83,10 @@ export function rawEvidenceGcSummary(result) {
|
|
|
83
83
|
return `Cleaned up: removed ${result.removed_dirs} old folders, freed ~${formatMb(result.freed_bytes)} MB`;
|
|
84
84
|
}
|
|
85
85
|
/**
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
* copies of the same rollout (BLI-3066). The GC proper cannot: it only deletes
|
|
91
|
-
* packs whose every file is already committed, and these were never committed —
|
|
92
|
-
* that is precisely why they piled up.
|
|
93
|
-
*
|
|
94
|
-
* Deleting an uncommitted copy is safe here and only here: the survivor holds
|
|
95
|
-
* the identical bytes, by content hash, so no evidence is lost. Identity comes
|
|
96
|
-
* from the manifest's recorded file hashes — cheap (one small JSON per pack)
|
|
97
|
-
* and exact. A pack with no readable manifest is counted and left alone rather
|
|
98
|
-
* than guessed at.
|
|
99
|
-
*
|
|
100
|
-
* The survivor is the newest directory: it is the one the current content-keyed
|
|
101
|
-
* pack id resolves to, so a fleet machine converges in one extra sync instead
|
|
102
|
-
* of oscillating between an old name and a new one.
|
|
86
|
+
* Remove byte-identical staged packs without losing their surviving evidence.
|
|
87
|
+
* Fingerprint manifest hashes, never guess unreadable packs. Prefer a pack the
|
|
88
|
+
* staging index references, then the newest directory, so future collection
|
|
89
|
+
* reuses the survivor instead of recreating and deleting copies each tick.
|
|
103
90
|
*/
|
|
104
91
|
export async function sweepDuplicateStagedRawEvidence(paths, env = process.env, now = new Date()) {
|
|
105
92
|
const empty = {
|
|
@@ -123,6 +110,31 @@ export async function sweepDuplicateStagedRawEvidence(paths, env = process.env,
|
|
|
123
110
|
// that would look like the sweep working while the bytes moved every cycle.
|
|
124
111
|
const staging = await readRawEvidenceStagingState(paths.state_dir);
|
|
125
112
|
const referencedPackIds = new Set(Object.values(staging.staged).map((entry) => entry.pack_id));
|
|
113
|
+
const { packs, removedStagingDirs } = await inspectStagedPacks(rawEvidenceRoot, dirEntries, referencedPackIds, now);
|
|
114
|
+
const { duplicateGroups, removedDirs, freedBytes, unfingerprintable } = await removeDuplicatePacks(packs);
|
|
115
|
+
const result = {
|
|
116
|
+
skipped: false,
|
|
117
|
+
duplicate_groups: duplicateGroups,
|
|
118
|
+
removed_dirs: removedDirs,
|
|
119
|
+
freed_bytes: freedBytes,
|
|
120
|
+
removed_staging_dirs: removedStagingDirs,
|
|
121
|
+
unfingerprintable_dirs: unfingerprintable,
|
|
122
|
+
};
|
|
123
|
+
// stderr, which launchd captures to `sync.err.log`. Logged on every sweep,
|
|
124
|
+
// including the boring one: "0 duplicates today" is the only evidence that
|
|
125
|
+
// the drain is still running at all.
|
|
126
|
+
console.error("[raw-evidence] dedup sweep", JSON.stringify({
|
|
127
|
+
reason: removedDirs > 0 ? "dedup_removed" : "dedup_clean",
|
|
128
|
+
pack_count: packs.length,
|
|
129
|
+
duplicate_groups: duplicateGroups,
|
|
130
|
+
removed_dirs: removedDirs,
|
|
131
|
+
freed_bytes: freedBytes,
|
|
132
|
+
removed_staging_dirs: removedStagingDirs,
|
|
133
|
+
unfingerprintable_dirs: unfingerprintable,
|
|
134
|
+
}));
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
async function inspectStagedPacks(rawEvidenceRoot, dirEntries, referencedPackIds, now) {
|
|
126
138
|
let removedStagingDirs = 0;
|
|
127
139
|
const packs = [];
|
|
128
140
|
for (const entry of dirEntries) {
|
|
@@ -151,6 +163,9 @@ export async function sweepDuplicateStagedRawEvidence(paths, env = process.env,
|
|
|
151
163
|
preferred: referencedPackIds.has(entry.name),
|
|
152
164
|
});
|
|
153
165
|
}
|
|
166
|
+
return { packs, removedStagingDirs };
|
|
167
|
+
}
|
|
168
|
+
async function removeDuplicatePacks(packs) {
|
|
154
169
|
const groups = new Map();
|
|
155
170
|
let unfingerprintable = 0;
|
|
156
171
|
for (const pack of packs) {
|
|
@@ -186,27 +201,7 @@ export async function sweepDuplicateStagedRawEvidence(paths, env = process.env,
|
|
|
186
201
|
freedBytes += byteSize;
|
|
187
202
|
}
|
|
188
203
|
}
|
|
189
|
-
|
|
190
|
-
skipped: false,
|
|
191
|
-
duplicate_groups: duplicateGroups,
|
|
192
|
-
removed_dirs: removedDirs,
|
|
193
|
-
freed_bytes: freedBytes,
|
|
194
|
-
removed_staging_dirs: removedStagingDirs,
|
|
195
|
-
unfingerprintable_dirs: unfingerprintable,
|
|
196
|
-
};
|
|
197
|
-
// stderr, which launchd captures to `sync.err.log`. Logged on every sweep,
|
|
198
|
-
// including the boring one: "0 duplicates today" is the only evidence that
|
|
199
|
-
// the drain is still running at all.
|
|
200
|
-
console.error("[raw-evidence] dedup sweep", JSON.stringify({
|
|
201
|
-
reason: removedDirs > 0 ? "dedup_removed" : "dedup_clean",
|
|
202
|
-
pack_count: packs.length,
|
|
203
|
-
duplicate_groups: duplicateGroups,
|
|
204
|
-
removed_dirs: removedDirs,
|
|
205
|
-
freed_bytes: freedBytes,
|
|
206
|
-
removed_staging_dirs: removedStagingDirs,
|
|
207
|
-
unfingerprintable_dirs: unfingerprintable,
|
|
208
|
-
}));
|
|
209
|
-
return result;
|
|
204
|
+
return { duplicateGroups, removedDirs, freedBytes, unfingerprintable };
|
|
210
205
|
}
|
|
211
206
|
export function rawEvidenceDedupSummary(result) {
|
|
212
207
|
return `raw-evidence dedup: removed ${result.removed_dirs} duplicate pack(s) across ${result.duplicate_groups} group(s), freed ~${formatMb(result.freed_bytes)} MB`;
|
|
@@ -1,18 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* The distinction this file exists to keep: an object with a live failure
|
|
8
|
-
* history is STUCK, and an object whose next attempt is still in the future is
|
|
9
|
-
* HELD. Every held object is stuck and most stuck objects are held, but the two
|
|
10
|
-
* answer different questions — "has anything been failing for nine days" versus
|
|
11
|
-
* "what did this tick withhold, and when does it try again" — and BLI-4022
|
|
12
|
-
* needed the second one, which nothing computed.
|
|
13
|
-
*
|
|
14
|
-
* Metadata only, like the rest of this family: content hashes, byte sizes,
|
|
15
|
-
* reason labels, timestamps. Never content, never an absolute path.
|
|
2
|
+
* Report live delivery failures separately from evidence currently held by backoff.
|
|
3
|
+
* A hold carries its own oldest failure and next retry time. Delivery records
|
|
4
|
+
* without staged bytes are counted as ghosts, never as stuck evidence.
|
|
5
|
+
* Logs contain only metadata, never content or absolute paths.
|
|
16
6
|
*/
|
|
17
7
|
import { deliveryRetryAtMs, terminalDeliveryReason } from "./raw-evidence-staging.js";
|
|
18
8
|
/**
|
|
@@ -40,6 +30,9 @@ export function summarizeStuckEvidence(state, now) {
|
|
|
40
30
|
reason: "object_no_longer_staged",
|
|
41
31
|
}));
|
|
42
32
|
}
|
|
33
|
+
return summarizeLiveDeliveryAttempts(entries, ghostCount, now);
|
|
34
|
+
}
|
|
35
|
+
function summarizeLiveDeliveryAttempts(entries, ghostCount, now) {
|
|
43
36
|
const reasons = new Set();
|
|
44
37
|
let heldCount = 0;
|
|
45
38
|
let maxAttempts = 0;
|
package/dist/repo-identity.js
CHANGED
|
@@ -113,29 +113,7 @@ export async function discoverGitWorktreesWithStatus(root, options = {}) {
|
|
|
113
113
|
discovered.set(identity.worktree_fingerprint, identity);
|
|
114
114
|
continue;
|
|
115
115
|
}
|
|
116
|
-
|
|
117
|
-
// is missing from the scan, and the operator has to be able to see that.
|
|
118
|
-
let entries;
|
|
119
|
-
try {
|
|
120
|
-
entries = await fs.readdir(current.dir, { withFileTypes: true });
|
|
121
|
-
}
|
|
122
|
-
catch (error) {
|
|
123
|
-
if (unreadableDirs.length < MAX_REPORTED_UNREADABLE_DIRS) {
|
|
124
|
-
unreadableDirs.push({ path: current.dir, code: errorCode(error) });
|
|
125
|
-
}
|
|
126
|
-
continue;
|
|
127
|
-
}
|
|
128
|
-
if (current.depth >= maxDepth) {
|
|
129
|
-
if (entries.some((entry) => entry.isDirectory() && !shouldSkipDirectory(entry.name))) {
|
|
130
|
-
incompleteReasons.add("max_depth_reached");
|
|
131
|
-
}
|
|
132
|
-
continue;
|
|
133
|
-
}
|
|
134
|
-
for (const entry of entries) {
|
|
135
|
-
if (!entry.isDirectory() || shouldSkipDirectory(entry.name))
|
|
136
|
-
continue;
|
|
137
|
-
stack.push({ dir: path.join(current.dir, entry.name), depth: current.depth + 1 });
|
|
138
|
-
}
|
|
116
|
+
await enqueueChildDirectories(current, maxDepth, stack, unreadableDirs, incompleteReasons);
|
|
139
117
|
}
|
|
140
118
|
const expanded = await expandLinkedWorktrees([...discovered.values()].sort(compareIdentity), maxWorktrees, allowedRoots);
|
|
141
119
|
for (const reason of expanded.incomplete_reasons) {
|
|
@@ -150,6 +128,31 @@ export async function discoverGitWorktreesWithStatus(root, options = {}) {
|
|
|
150
128
|
unreadable_dirs: unreadableDirs,
|
|
151
129
|
};
|
|
152
130
|
}
|
|
131
|
+
async function enqueueChildDirectories(current, maxDepth, stack, unreadableDirs, incompleteReasons) {
|
|
132
|
+
// A failed read is recorded, never swallowed: anything beneath this folder
|
|
133
|
+
// is missing from the scan, and the operator has to be able to see that.
|
|
134
|
+
let entries;
|
|
135
|
+
try {
|
|
136
|
+
entries = await fs.readdir(current.dir, { withFileTypes: true });
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
if (unreadableDirs.length < MAX_REPORTED_UNREADABLE_DIRS) {
|
|
140
|
+
unreadableDirs.push({ path: current.dir, code: errorCode(error) });
|
|
141
|
+
}
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (current.depth >= maxDepth) {
|
|
145
|
+
if (entries.some((entry) => entry.isDirectory() && !shouldSkipDirectory(entry.name))) {
|
|
146
|
+
incompleteReasons.add("max_depth_reached");
|
|
147
|
+
}
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
for (const entry of entries) {
|
|
151
|
+
if (!entry.isDirectory() || shouldSkipDirectory(entry.name))
|
|
152
|
+
continue;
|
|
153
|
+
stack.push({ dir: path.join(current.dir, entry.name), depth: current.depth + 1 });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
153
156
|
/**
|
|
154
157
|
* Discovers and deduplicates git worktrees across the exact roots supplied by
|
|
155
158
|
* the caller. An approved root with no current git checkout intentionally
|
|
@@ -4,14 +4,8 @@ import { describeError } from "./health-detail.js";
|
|
|
4
4
|
const SELF_UPDATE_MIN_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
5
5
|
export const SELF_UPDATE_THROTTLE_MARKER = ".last-self-update-check";
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* bare `npm` spawn that works in every interactive shell resolves to nothing
|
|
10
|
-
* inside the tick, and every scheduled update would fail `npm_install_failed`
|
|
11
|
-
* on the whole Mac fleet while interactive `cockpit doctor` kept working. The
|
|
12
|
-
* npm shim ships beside the node binary in the standard layouts on both host
|
|
13
|
-
* families, so the running node's own directory is the one PATH entry that is
|
|
14
|
-
* always right.
|
|
7
|
+
* Put the running node binary's directory on PATH for scheduled npm commands.
|
|
8
|
+
* Schedulers use a bare PATH; npm's shim ships beside node on both host families.
|
|
15
9
|
*/
|
|
16
10
|
export function envWithNodeRuntimeOnPath(env, nodeExecutable = process.execPath) {
|
|
17
11
|
const nodeBinDir = path.dirname(nodeExecutable);
|
|
@@ -24,13 +18,9 @@ export function envWithNodeRuntimeOnPath(env, nodeExecutable = process.execPath)
|
|
|
24
18
|
};
|
|
25
19
|
}
|
|
26
20
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* Installation is verified with npm's installed-version probe. The sync caller
|
|
32
|
-
* re-executes the tick on `updated`, carrying a guard that skips another update
|
|
33
|
-
* attempt in the replacement process.
|
|
21
|
+
* Converge scheduled collectors on npm latest, throttled to one attempt per day.
|
|
22
|
+
* Verify npm's installed version before callers re-execute the tick. A server
|
|
23
|
+
* floor bypasses the throttle; an operator's incident freeze still wins.
|
|
34
24
|
*/
|
|
35
25
|
export async function runScheduledSelfUpdate(paths, deps, options = {}) {
|
|
36
26
|
const env = options.env ?? process.env;
|
|
@@ -72,6 +62,9 @@ export async function runScheduledSelfUpdate(paths, deps, options = {}) {
|
|
|
72
62
|
// this machine on purpose, and the server must not be able to unpin it.
|
|
73
63
|
return { status: "skipped", reason: "disabled_by_env", ...forcedFields };
|
|
74
64
|
}
|
|
65
|
+
return installScheduledUpdate(deps, tag, forced, minVersion, forcedFields);
|
|
66
|
+
}
|
|
67
|
+
async function installScheduledUpdate(deps, tag, forced, minVersion, forcedFields) {
|
|
75
68
|
const targetVersion = await latestCliVersionFromNpm(deps.exec, tag);
|
|
76
69
|
if (forced &&
|
|
77
70
|
targetVersion &&
|
|
@@ -105,6 +98,9 @@ export async function runScheduledSelfUpdate(paths, deps, options = {}) {
|
|
|
105
98
|
...forcedFields,
|
|
106
99
|
};
|
|
107
100
|
}
|
|
101
|
+
return verifyScheduledUpdate(deps, targetVersion, forcedFields);
|
|
102
|
+
}
|
|
103
|
+
async function verifyScheduledUpdate(deps, targetVersion, forcedFields) {
|
|
108
104
|
// Verify against what the platform returns, not the exit code npm handed
|
|
109
105
|
// back (BLI-2541 lesson: a green exit code is not proof). Read the
|
|
110
106
|
// globally installed package's version back off disk via `npm ls -g
|
package/dist/second-install.js
CHANGED
|
@@ -53,6 +53,9 @@ export async function detectSecondCockpitInstall(options) {
|
|
|
53
53
|
paths: installs,
|
|
54
54
|
};
|
|
55
55
|
}
|
|
56
|
+
return compareRunningInstall(installs, options, platform);
|
|
57
|
+
}
|
|
58
|
+
async function compareRunningInstall(installs, options, platform) {
|
|
56
59
|
// Exactly one on PATH. It can still be a DIFFERENT install from the one
|
|
57
60
|
// running now — the scheduler holds an absolute path to whichever prefix it
|
|
58
61
|
// was registered with. Only decidable where the bin is a symlink into the
|
package/dist/slim/codex-rules.js
CHANGED
|
@@ -1,30 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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.
|
|
2
|
+
* Remove only proven duplicate Codex payloads, preserving the envelope.
|
|
3
|
+
* Keep command/aggregated_output, unique file/tool content, replacement_history,
|
|
4
|
+
* images after marker replacement, durations, fork identity and unknown shapes.
|
|
5
|
+
* Remember earlier response_item strings; later long copies become references.
|
|
6
|
+
* Keep total usage/context window; drop repeated usage/rate-limit bookkeeping.
|
|
7
|
+
* Keep one distinct developer message, base instructions and world-state block.
|
|
8
|
+
* Guardian history and task_complete messages deduplicate only against earlier
|
|
9
|
+
* response items; a copy appearing first is retained.
|
|
28
10
|
*/
|
|
29
11
|
import { SLIM_COPY_MIN_CHARS, canonicalText, contentHash, isJsonObject, isSlimRef, slimRefFor, stripKey, } from "./slim-support.js";
|
|
30
12
|
/** Strings from `response_item` lines remembered for comparison. Past this, new strings are not added. */
|
|
@@ -136,16 +118,18 @@ function rememberLongStrings(value, state, depth) {
|
|
|
136
118
|
if (typeof value === "string") {
|
|
137
119
|
if (value.length >= SLIM_COPY_MIN_CHARS)
|
|
138
120
|
state.earlierStrings.add(contentHash(value));
|
|
121
|
+
return;
|
|
139
122
|
}
|
|
140
|
-
|
|
123
|
+
if (Array.isArray(value)) {
|
|
141
124
|
for (const item of value)
|
|
142
125
|
rememberLongStrings(item, state, depth + 1);
|
|
126
|
+
return;
|
|
143
127
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
128
|
+
if (!isJsonObject(value))
|
|
129
|
+
return;
|
|
130
|
+
for (const [key, item] of Object.entries(value)) {
|
|
131
|
+
if (key !== "encrypted_content")
|
|
132
|
+
rememberLongStrings(item, state, depth + 1);
|
|
149
133
|
}
|
|
150
134
|
}
|
|
151
135
|
function refIfEarlierCopy(holder, key, state, rule, edits) {
|
|
@@ -167,13 +151,16 @@ function refEarlierCopiesIn(node, state, rule, edits, depth) {
|
|
|
167
151
|
const holder = node;
|
|
168
152
|
const child = holder[key];
|
|
169
153
|
if (typeof child === "string") {
|
|
170
|
-
|
|
171
|
-
holder[key] = slimRefFor(contentHash(child));
|
|
172
|
-
edits.strip(rule, child.length);
|
|
173
|
-
}
|
|
154
|
+
refEarlierString(holder, key, child, state, rule, edits);
|
|
174
155
|
}
|
|
175
156
|
else if ((Array.isArray(child) || isJsonObject(child)) && !isSlimRef(child)) {
|
|
176
157
|
refEarlierCopiesIn(child, state, rule, edits, depth + 1);
|
|
177
158
|
}
|
|
178
159
|
}
|
|
160
|
+
}
|
|
161
|
+
function refEarlierString(holder, key, child, state, rule, edits) {
|
|
162
|
+
if (child.length < SLIM_COPY_MIN_CHARS || !state.earlierStrings.has(contentHash(child)))
|
|
163
|
+
return;
|
|
164
|
+
holder[key] = slimRefFor(contentHash(child));
|
|
165
|
+
edits.strip(rule, child.length);
|
|
179
166
|
}
|
|
@@ -65,24 +65,25 @@ function parseSourceRetryEntry(value, fieldPath) {
|
|
|
65
65
|
function parsePendingSessionReport(value, fieldPath) {
|
|
66
66
|
const record = requireRecord(value, fieldPath);
|
|
67
67
|
const sessions = parseMetadataOnlySessionReportRows(record["sessions"], `${fieldPath}.sessions`);
|
|
68
|
+
const text = (key) => requireString(record[key], `${fieldPath}.${key}`);
|
|
68
69
|
return {
|
|
69
|
-
report_id:
|
|
70
|
-
created_at:
|
|
71
|
-
last_attempt_at:
|
|
72
|
-
dashboard_url:
|
|
73
|
-
generated_at:
|
|
74
|
-
work_context_id:
|
|
75
|
-
repo_label:
|
|
76
|
-
branch:
|
|
77
|
-
repo_fingerprint:
|
|
70
|
+
report_id: text("report_id"),
|
|
71
|
+
created_at: text("created_at"),
|
|
72
|
+
last_attempt_at: text("last_attempt_at"),
|
|
73
|
+
dashboard_url: text("dashboard_url"),
|
|
74
|
+
generated_at: text("generated_at"),
|
|
75
|
+
work_context_id: text("work_context_id"),
|
|
76
|
+
repo_label: text("repo_label"),
|
|
77
|
+
branch: text("branch"),
|
|
78
|
+
repo_fingerprint: text("repo_fingerprint"),
|
|
78
79
|
repo_origin_url: record["repo_origin_url"] === undefined
|
|
79
80
|
? null
|
|
80
81
|
: requireNullableString(record["repo_origin_url"], `${fieldPath}.repo_origin_url`),
|
|
81
|
-
worktree_label:
|
|
82
|
-
worktree_fingerprint:
|
|
82
|
+
worktree_label: text("worktree_label"),
|
|
83
|
+
worktree_fingerprint: text("worktree_fingerprint"),
|
|
83
84
|
worktree_is_primary: requireBoolean(record["worktree_is_primary"], `${fieldPath}.worktree_is_primary`),
|
|
84
85
|
sessions,
|
|
85
|
-
failure_reason:
|
|
86
|
+
failure_reason: text("failure_reason"),
|
|
86
87
|
};
|
|
87
88
|
}
|
|
88
89
|
export function parseMetadataOnlySessionReportRows(value, fieldPath) {
|
package/dist/sync-lock.js
CHANGED
|
@@ -3,21 +3,8 @@ import fs from "node:fs/promises";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { describeError } from "./health-detail.js";
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* lets the second invocation exit cleanly with `sync_already_running` instead.
|
|
9
|
-
*
|
|
10
|
-
* A held lock refreshes a heartbeat every 30 s. Takeover happens only when the
|
|
11
|
-
* heartbeat is more than 2 minutes stale — a fixed short timeout would fire
|
|
12
|
-
* during a legitimate long first sync and recreate the interleaving it
|
|
13
|
-
* prevents (D33). The lock file stays local and stores a pid, a heartbeat and
|
|
14
|
-
* (BLI-12872) when the holder started and when it last finished a unit of
|
|
15
|
-
* work; no paths or content.
|
|
16
|
-
*
|
|
17
|
-
* The heartbeat only proves the process is alive. `started_ms` and
|
|
18
|
-
* `progress_ms` are what let `sync-lock-holder.ts` tell a slow tick from a
|
|
19
|
-
* stuck one: a holder that keeps finishing work is never called stuck, however
|
|
20
|
-
* long it has been running.
|
|
6
|
+
* Serialize collection ticks while tracking heartbeat ownership and progress.
|
|
7
|
+
* A stale owner may neither refresh nor release a replacement owner's lock.
|
|
21
8
|
*/
|
|
22
9
|
const LOCK_FILENAME = "sync.lock";
|
|
23
10
|
const HEARTBEAT_INTERVAL_MS = 30_000;
|
|
@@ -79,6 +66,9 @@ export async function acquireSyncLock(paths, now = new Date(), options = {}) {
|
|
|
79
66
|
}
|
|
80
67
|
logAcquired(held, takeover, existing, now);
|
|
81
68
|
heldStates.add(held);
|
|
69
|
+
return maintainOwnedLock(lockPath, token, held);
|
|
70
|
+
}
|
|
71
|
+
function maintainOwnedLock(lockPath, token, held) {
|
|
82
72
|
const timer = setInterval(() => {
|
|
83
73
|
// Refresh only while the lock is still ours; if we were taken over, stop
|
|
84
74
|
// touching the file so we cannot steal it back from the new owner.
|