@bli-cockpit/cli 0.2.40 → 0.2.42
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 +51 -5
- package/dist/autostart.js +105 -5
- package/dist/commands/brief-edit.js +289 -0
- package/dist/commands/brief-rewrite.js +260 -0
- package/dist/commands/brief.js +11 -10
- package/dist/commands/cli-io.js +29 -0
- package/dist/commands/correct.js +4 -5
- package/dist/commands/editor.js +204 -0
- package/dist/commands/jarvis.js +7 -8
- package/dist/commands/local-args.js +36 -2
- package/dist/commands/local-discovery.js +20 -0
- package/dist/commands/local-help.js +13 -2
- package/dist/commands/local.js +47 -1
- package/dist/commands/notes.js +9 -2
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/scout-render.js +20 -18
- package/dist/commands/scout.js +4 -4
- package/dist/commands/sessions.js +45 -1
- package/dist/commands/settings-render.js +9 -4
- package/dist/commands/settings.js +6 -0
- package/dist/commands/text-width.js +108 -0
- package/dist/commands/workbook-render.js +34 -17
- package/dist/commands/workbook.js +6 -4
- package/dist/evidence-upload-client.js +42 -3
- package/dist/local-state.js +27 -2
- package/dist/upload-agent-artifacts.js +27 -2
- package/dist/upload-failure-reason.js +121 -0
- package/dist/upload-http.js +28 -0
- package/dist/upload.js +122 -21
- package/package.json +1 -1
package/dist/upload.js
CHANGED
|
@@ -7,7 +7,8 @@ import { buildLocalAmbientEnvelope, LocalUploadBlockedError, } from "./upload-en
|
|
|
7
7
|
import { applyRawEvidenceUploadOutcomes, hasRetryableEvidenceGap, logEvidenceBackoffBypassed, logEvidenceHeldByBackoff, logPermanentlyRejectedEvidence, partitionHeldEvidenceFiles, permanentEvidenceFailureReason, persistDeliveryAttempts, retrySourcesForFailedSync, retryableEvidenceGapReason, summarizeRawEvidenceDelivery, } from "./upload-evidence-delivery.js";
|
|
8
8
|
import { reportAgentImageArtifacts } from "./upload-agent-artifacts.js";
|
|
9
9
|
import { describeError } from "./health-detail.js";
|
|
10
|
-
import { isNonEmptyString, readResponseJson,
|
|
10
|
+
import { isNonEmptyString, readResponseJson, serverFailureDetail, } from "./upload-http.js";
|
|
11
|
+
import { SyncDeliveryError, classifySyncFailure, } from "./upload-failure-reason.js";
|
|
11
12
|
export { LocalUploadBlockedError, buildLocalAmbientEnvelope, } from "./upload-envelope.js";
|
|
12
13
|
export { partitionHeldEvidenceFiles } from "./upload-evidence-delivery.js";
|
|
13
14
|
export { reportAgentImageArtifacts } from "./upload-agent-artifacts.js";
|
|
@@ -99,6 +100,17 @@ export async function syncLocalAmbientEnvelope(options = {}) {
|
|
|
99
100
|
uploadOutcomes,
|
|
100
101
|
attemptedAt,
|
|
101
102
|
});
|
|
103
|
+
// The success branch logs too. A line that only fires on failure cannot
|
|
104
|
+
// answer "did anything land at all today?", which is the question that
|
|
105
|
+
// would have caught BLI-2528 in a week instead of 57 days.
|
|
106
|
+
console.error("[cockpit-sync] ingest accepted the envelope", JSON.stringify({
|
|
107
|
+
reason: "ingest_accepted",
|
|
108
|
+
http_status: response.status,
|
|
109
|
+
event_count: built.event_count,
|
|
110
|
+
raw_evidence_file_count: built.raw_evidence_upload_files.length,
|
|
111
|
+
uploaded_chunk_count: uploadedChunkCount,
|
|
112
|
+
failed_object_count: uploadOutcomes.filter((outcome) => outcome.upload_state === "upload_failed").length,
|
|
113
|
+
}));
|
|
102
114
|
return {
|
|
103
115
|
status: "uploaded",
|
|
104
116
|
dashboard_url: built.dashboard_url,
|
|
@@ -113,12 +125,26 @@ export async function syncLocalAmbientEnvelope(options = {}) {
|
|
|
113
125
|
};
|
|
114
126
|
}
|
|
115
127
|
catch (error) {
|
|
116
|
-
|
|
128
|
+
// The spool row used to store `error.message` verbatim, so every distinct
|
|
129
|
+
// server sentence became its own "reason" and nothing could count how many
|
|
130
|
+
// machines were failing the same way (BLI-3483). It now stores a label from
|
|
131
|
+
// a closed set plus a bounded, redacted cause.
|
|
132
|
+
const classified = classifySyncFailure(error);
|
|
117
133
|
// A committed content-addressed object may be shared by concurrent syncs.
|
|
118
134
|
// Never delete it from this error path: an ingest retry can reuse it, while
|
|
119
135
|
// client-side cleanup cannot prove exclusive ownership without racing a
|
|
120
136
|
// second sync that is about to index the same object.
|
|
121
|
-
const spooledFailureReason =
|
|
137
|
+
const spooledFailureReason = classified.failure_reason;
|
|
138
|
+
console.error("[cockpit-sync] sync spooled for retry", JSON.stringify({
|
|
139
|
+
reason: classified.reason,
|
|
140
|
+
...(classified.http_status === null
|
|
141
|
+
? {}
|
|
142
|
+
: { http_status: classified.http_status }),
|
|
143
|
+
...(classified.detail ? { detail: classified.detail } : {}),
|
|
144
|
+
event_count: built.event_count,
|
|
145
|
+
raw_evidence_file_count: built.raw_evidence_upload_files.length,
|
|
146
|
+
uploaded_chunk_count: uploadedChunkCount,
|
|
147
|
+
}));
|
|
122
148
|
const entry = await recordUploadFailure(paths, {
|
|
123
149
|
last_attempt_at: attemptedAt,
|
|
124
150
|
dashboard_url: built.dashboard_url,
|
|
@@ -182,17 +208,50 @@ async function buildEnvelopeOrRecordBlocker(paths, options, cursorObjects, attem
|
|
|
182
208
|
* row counts, which is what the receipt check demands.
|
|
183
209
|
*/
|
|
184
210
|
async function postEnvelopeToIngest(options) {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
"
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
211
|
+
let response;
|
|
212
|
+
try {
|
|
213
|
+
response = await options.fetchImpl(`${options.built.dashboard_url}/api/ambient/ingest`, {
|
|
214
|
+
method: "POST",
|
|
215
|
+
headers: {
|
|
216
|
+
"Authorization": `Bearer ${options.built.device_token}`,
|
|
217
|
+
"Content-Type": "application/json",
|
|
218
|
+
},
|
|
219
|
+
body: JSON.stringify(options.envelope),
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
// No status came back at all, so no server-side reason exists to read. The
|
|
224
|
+
// error's own name and code (`TypeError` / `ENOTFOUND` / `ECONNREFUSED`)
|
|
225
|
+
// are the whole answer, and they are what separates "this machine is
|
|
226
|
+
// offline" from "the dashboard refused us" — two words that used to be the
|
|
227
|
+
// same spool row.
|
|
228
|
+
const described = describeError(error);
|
|
229
|
+
console.error("[cockpit-sync] ingest request failed before a status came back", JSON.stringify({ reason: "ingest_transport_error", ...described }));
|
|
230
|
+
throw new SyncDeliveryError("ingest_transport_error", {
|
|
231
|
+
detail: [
|
|
232
|
+
described.error_name,
|
|
233
|
+
described.error_code,
|
|
234
|
+
described.cause_code ?? described.cause_name,
|
|
235
|
+
described.error_detail,
|
|
236
|
+
]
|
|
237
|
+
.filter(Boolean)
|
|
238
|
+
.join(" "),
|
|
239
|
+
cause: error,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
193
242
|
const responseBody = await readResponseJson(response);
|
|
194
243
|
if (!response.ok) {
|
|
195
|
-
|
|
244
|
+
const serverReason = serverFailureDetail(responseBody);
|
|
245
|
+
console.error("[cockpit-sync] ingest refused the envelope", JSON.stringify({
|
|
246
|
+
reason: "ingest_rejected",
|
|
247
|
+
http_status: response.status,
|
|
248
|
+
server_reason: serverReason ?? "none",
|
|
249
|
+
event_count: options.envelope.events.length,
|
|
250
|
+
}));
|
|
251
|
+
throw new SyncDeliveryError("ingest_rejected", {
|
|
252
|
+
httpStatus: response.status,
|
|
253
|
+
detail: `http ${response.status}; server reason ${serverReason ?? "none"}`,
|
|
254
|
+
});
|
|
196
255
|
}
|
|
197
256
|
assertAmbientIngestReceipt(response, responseBody, options.envelope);
|
|
198
257
|
return response;
|
|
@@ -275,11 +334,26 @@ async function recordIngestedSyncOutcome(options) {
|
|
|
275
334
|
*
|
|
276
335
|
* Anything short of a 202 whose counts match the envelope's own totals is
|
|
277
336
|
* treated as no receipt at all — a proxy's 200, a truncated body, or a
|
|
278
|
-
* dashboard that accepted fewer rows than were sent all land here.
|
|
337
|
+
* dashboard that accepted fewer rows than were sent all land here. They used to
|
|
338
|
+
* land here under the SAME sentence, which is the BLI-3483 complaint: a
|
|
339
|
+
* network appliance answering on the dashboard's behalf and a dashboard that
|
|
340
|
+
* genuinely persisted three of four rows are opposite repairs. They are now two
|
|
341
|
+
* reasons, and the second one names the field that disagreed and by how much.
|
|
279
342
|
*/
|
|
280
343
|
function assertAmbientIngestReceipt(response, value, envelope) {
|
|
281
344
|
if (response.status !== 202 || !value || typeof value !== "object") {
|
|
282
|
-
|
|
345
|
+
const detail = response.status !== 202
|
|
346
|
+
? `http ${response.status}; the ingest route answers 202, so something in front of it replied`
|
|
347
|
+
: "202 with a body that is not an object";
|
|
348
|
+
console.error("[cockpit-sync] ingest answered without a durable receipt", JSON.stringify({
|
|
349
|
+
reason: "ingest_receipt_not_202",
|
|
350
|
+
http_status: response.status,
|
|
351
|
+
body_is_object: Boolean(value) && typeof value === "object",
|
|
352
|
+
}));
|
|
353
|
+
throw new SyncDeliveryError("ingest_receipt_not_202", {
|
|
354
|
+
httpStatus: response.status,
|
|
355
|
+
detail,
|
|
356
|
+
});
|
|
283
357
|
}
|
|
284
358
|
const record = value;
|
|
285
359
|
const ingest = record["ingest"] && typeof record["ingest"] === "object"
|
|
@@ -288,12 +362,39 @@ function assertAmbientIngestReceipt(response, value, envelope) {
|
|
|
288
362
|
const expectedFactCount = envelope.events.length;
|
|
289
363
|
const expectedRiskFlagCount = envelope.events.reduce((total, event) => total + event.risk_flags.length, 0);
|
|
290
364
|
const expectedEvidenceRefCount = envelope.events.reduce((total, event) => total + event.raw_evidence_pointers.length, 0);
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
365
|
+
const mismatches = [];
|
|
366
|
+
if (record["ok"] !== true)
|
|
367
|
+
mismatches.push("ok not true");
|
|
368
|
+
if (!ingest)
|
|
369
|
+
mismatches.push("no ingest block");
|
|
370
|
+
if (ingest && !isNonEmptyString(ingest["work_session_id"])) {
|
|
371
|
+
mismatches.push("blank work_session_id");
|
|
372
|
+
}
|
|
373
|
+
if (ingest) {
|
|
374
|
+
mismatches.push(...countMismatch("fact_count", ingest["fact_count"], expectedFactCount), ...countMismatch("risk_flag_count", ingest["risk_flag_count"], expectedRiskFlagCount), ...countMismatch("evidence_ref_count", ingest["evidence_ref_count"], expectedEvidenceRefCount));
|
|
298
375
|
}
|
|
376
|
+
if (mismatches.length === 0)
|
|
377
|
+
return;
|
|
378
|
+
console.error("[cockpit-sync] ingest receipt does not match what was submitted", JSON.stringify({
|
|
379
|
+
reason: "ingest_receipt_incomplete",
|
|
380
|
+
http_status: response.status,
|
|
381
|
+
mismatch_count: mismatches.length,
|
|
382
|
+
mismatches,
|
|
383
|
+
}));
|
|
384
|
+
throw new SyncDeliveryError("ingest_receipt_incomplete", {
|
|
385
|
+
httpStatus: response.status,
|
|
386
|
+
detail: mismatches.join("; "),
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* `submitted 4, receipt 3` — the sentence that tells partial persistence from a
|
|
391
|
+
* receipt that never carried the field at all. Counts are numbers this
|
|
392
|
+
* collector computed and numbers the server echoed; neither is content.
|
|
393
|
+
*/
|
|
394
|
+
function countMismatch(field, received, expected) {
|
|
395
|
+
if (received === expected)
|
|
396
|
+
return [];
|
|
397
|
+
return [
|
|
398
|
+
`${field} submitted ${expected}, receipt ${typeof received === "number" ? received : "absent"}`,
|
|
399
|
+
];
|
|
299
400
|
}
|