@bli-cockpit/cli 0.2.16 → 0.2.17

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.
@@ -15,7 +15,7 @@ export async function runCockpitCli(argv, io) {
15
15
  }
16
16
 
17
17
  if (command === "--version" || command === "-V" || command === "version") {
18
- writeLine(io?.stdout ?? process.stdout, "0.2.16");
18
+ writeLine(io?.stdout ?? process.stdout, "0.2.17");
19
19
  return 0;
20
20
  }
21
21
 
@@ -140,6 +140,7 @@ export async function uploadRawEvidenceFilesChunked(options) {
140
140
  continue;
141
141
  }
142
142
  const outcome = await uploadOneObject(options, entry, disposition, chunkSizeBytes);
143
+ await reportAbandonedUpload(options, disposition, outcome);
143
144
  outcomes.push(outcome);
144
145
  for (const duplicate of entry.duplicates) {
145
146
  outcomes.push(duplicateOutcome(outcome, duplicate));
@@ -171,6 +172,72 @@ function duplicateOutcome(primary, duplicate) {
171
172
  uploaded_chunk_count: 0,
172
173
  };
173
174
  }
175
+ /**
176
+ * Tell the server why we gave up on a row `begin` already opened (BLI-2539).
177
+ *
178
+ * `begin` opens a ledger row per object and the chunks then go one object at a
179
+ * time, so one object failing leaves its siblings untouched — the production
180
+ * shape is a lone failure among committed rows. This function is the only thing
181
+ * standing between that failure and a row that sits `pending` with a null
182
+ * reason until a drain relabels it `staging_incomplete` weeks later, which
183
+ * names the shape and not the cause. 275 rows reached that state by 2026-08-14.
184
+ *
185
+ * The server records the reason on the still-open row (for a reason classified
186
+ * permanent it fails the row closed), so reporting a give-up never costs the
187
+ * staged chunks the next sync resumes from.
188
+ *
189
+ * Deliberately best-effort: the upload has already failed and the caller's
190
+ * outcome is the answer that matters. Losing the abort as well leaves exactly
191
+ * the row we had before this existed, so a delivery failure must not throw —
192
+ * but it is named on stderr rather than swallowed, because a reason the server
193
+ * never received is invisible precisely where BLI-2539 needed it visible.
194
+ */
195
+ async function reportAbandonedUpload(options, disposition, outcome) {
196
+ if (outcome.upload_state !== "upload_failed")
197
+ return;
198
+ if (!outcome.reason)
199
+ return;
200
+ // Only a row this call actually opened or resumed. `conflict` never allocated
201
+ // one for us, and `already_committed` names durable content an abort must not
202
+ // touch.
203
+ if (disposition.disposition !== "new" && disposition.disposition !== "resume") {
204
+ return;
205
+ }
206
+ if (!disposition.upload_id)
207
+ return;
208
+ const reason = conformReasonLabel(outcome.reason);
209
+ const response = await requestJson(options, "/api/ambient/evidence/upload/abort", {
210
+ schema_version: "ambient-raw-evidence-upload-abort.v1",
211
+ generated_at: options.generatedAt,
212
+ provenance: options.provenance,
213
+ upload_id: disposition.upload_id,
214
+ object_key: outcome.object_key,
215
+ reason,
216
+ uploaded_chunk_count: outcome.uploaded_chunk_count,
217
+ });
218
+ if (!response.ok) {
219
+ // status 404 is an old dashboard without the abort route; status 0 means
220
+ // the request itself never completed. Metadata only — ids, status, labels.
221
+ console.error("[evidence-abort] delivery failed; the server did not record the reason", JSON.stringify({
222
+ upload_id: disposition.upload_id,
223
+ reason,
224
+ http_status: response.status,
225
+ }));
226
+ }
227
+ }
228
+ /**
229
+ * Force a reason into the label shape the abort route accepts.
230
+ *
231
+ * Every reason this client composes already fits, and this exists so that stays
232
+ * true without anyone having to remember it. A label the server rejects comes
233
+ * back 400 and the reason is lost — which is the precise failure BLI-2539 is
234
+ * about, arriving through the code meant to fix it. A mangled label that lands
235
+ * beats a perfect one that does not.
236
+ */
237
+ function conformReasonLabel(reason) {
238
+ const conformed = reason.replace(/[^a-z0-9_:.-]/gi, "_").slice(0, 120);
239
+ return conformed.length > 0 ? conformed : "upload_failed_unlabelled";
240
+ }
174
241
  async function uploadOneObject(options, entry, disposition, chunkSizeBytes) {
175
242
  const objectKey = entry.file.pointer.object_key ?? "";
176
243
  if (disposition.disposition === "already_committed") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bli-cockpit/cli",
3
- "version": "0.2.16",
3
+ "version": "0.2.17",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
@@ -26,6 +26,6 @@
26
26
  "test": "node dist/cli.js --help && node ../../scripts/assert-public-cli-routing.mjs && node ../../scripts/assert-public-package-pack.mjs --workspace=@bli-cockpit/cli"
27
27
  },
28
28
  "dependencies": {
29
- "@bli-cockpit/telemetry-core": "0.1.18"
29
+ "@bli-cockpit/telemetry-core": "0.1.19"
30
30
  }
31
31
  }