@bli-cockpit/telemetry-core 0.1.18 → 0.1.20

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.
@@ -377,6 +377,78 @@ export declare const RawEvidenceUploadCommitResponseSchema: z.ZodObject<{
377
377
  }, z.core.$strict>>;
378
378
  }, z.core.$strict>;
379
379
  export type RawEvidenceUploadCommitResponse = z.infer<typeof RawEvidenceUploadCommitResponseSchema>;
380
+ /**
381
+ * The collector giving up on an object it already opened (BLI-2539).
382
+ *
383
+ * `begin` writes a ledger row for every object in the batch, then chunks go one
384
+ * object at a time. When one object's chunks fail its siblings still finish, so
385
+ * the production shape is a single failure among committed rows rather than a
386
+ * truncated batch. The collector knows exactly why that object died — it
387
+ * composes labels like `chunk_0_failed_http_500` — and until this route existed
388
+ * it had nowhere to send them: begin/chunk/commit has no call meaning "I quit".
389
+ * The row stayed `pending` with a null reason until a drain relabelled it
390
+ * `staging_incomplete`, which describes the shape and not the cause. 275 rows
391
+ * reached that state by 2026-08-14.
392
+ *
393
+ * The reason is a label, never free text or an error body: it lands in
394
+ * `status_reason`, which is read by `classifyUploadFailure`, and an unlisted
395
+ * label classifies as `unknown` rather than being guessed into a bucket. That
396
+ * is also what keeps these rows recoverable — `unknown` is not permanent, so
397
+ * the row stays open (still `pending`, staged chunks intact) and the next
398
+ * `begin` resumes it. Only a reason classified permanent fails the row closed.
399
+ */
400
+ export declare const RawEvidenceUploadAbortRequestSchema: z.ZodObject<{
401
+ schema_version: z.ZodLiteral<"ambient-raw-evidence-upload-abort.v1">;
402
+ generated_at: z.ZodString;
403
+ provenance: z.ZodObject<{
404
+ capture_source: z.ZodEnum<{
405
+ unknown: "unknown";
406
+ codex_jsonl: "codex_jsonl";
407
+ codex_otel: "codex_otel";
408
+ car_state: "car_state";
409
+ git_state: "git_state";
410
+ github_state: "github_state";
411
+ linear_state: "linear_state";
412
+ mcp_local: "mcp_local";
413
+ claude_jsonl: "claude_jsonl";
414
+ claude_hooks: "claude_hooks";
415
+ manual_event: "manual_event";
416
+ collector_runtime: "collector_runtime";
417
+ }>;
418
+ capture_adapter_version: z.ZodString;
419
+ collector_version: z.ZodString;
420
+ repo: z.ZodString;
421
+ branch: z.ZodString;
422
+ repo_label: z.ZodOptional<z.ZodString>;
423
+ repo_fingerprint: z.ZodOptional<z.ZodString>;
424
+ repo_origin_url: z.ZodOptional<z.ZodString>;
425
+ worktree_label: z.ZodOptional<z.ZodString>;
426
+ worktree_fingerprint: z.ZodOptional<z.ZodString>;
427
+ worktree_is_primary: z.ZodOptional<z.ZodBoolean>;
428
+ operator_id: z.ZodString;
429
+ session_id: z.ZodString;
430
+ work_context_id: z.ZodString;
431
+ }, z.core.$strict>;
432
+ upload_id: z.ZodString;
433
+ object_key: z.ZodString;
434
+ reason: z.ZodString;
435
+ uploaded_chunk_count: z.ZodOptional<z.ZodNumber>;
436
+ }, z.core.$strict>;
437
+ export type RawEvidenceUploadAbortRequest = z.infer<typeof RawEvidenceUploadAbortRequestSchema>;
438
+ export declare const RawEvidenceUploadAbortResponseSchema: z.ZodObject<{
439
+ ok: z.ZodLiteral<true>;
440
+ status: z.ZodEnum<{
441
+ reason_recorded: "reason_recorded";
442
+ aborted: "aborted";
443
+ already_terminal: "already_terminal";
444
+ }>;
445
+ upload_id: z.ZodString;
446
+ object_key: z.ZodString;
447
+ status_reason: z.ZodString;
448
+ retryable: z.ZodBoolean;
449
+ will_reopen: z.ZodBoolean;
450
+ }, z.core.$strict>;
451
+ export type RawEvidenceUploadAbortResponse = z.infer<typeof RawEvidenceUploadAbortResponseSchema>;
380
452
  /**
381
453
  * Codex session attribution report.
382
454
  *
@@ -19,11 +19,22 @@ export const RAW_EVIDENCE_UPLOAD_DEFAULT_CHUNK_BYTES = RAW_EVIDENCE_UPLOAD_MAX_C
19
19
  // by Vercel's ~4.5MB request-body limit (see header comment). 192 chunks
20
20
  // * 3 MiB gives a 576 MiB protocol ceiling above the current file cap.
21
21
  export const RAW_EVIDENCE_UPLOAD_MAX_CHUNK_COUNT = 192;
22
- // Final-object cap. The commit route still assembles and redaction-scans text
23
- // in memory, so keep this below V8's string ceiling with practical headroom for
24
- // buffers and redaction copies. 256 MiB covers the largest observed fleet
25
- // session (118 MiB) with >2x headroom. Raising it requires a streaming commit.
26
- export const RAW_EVIDENCE_UPLOAD_MAX_FILE_BYTES = 256 * 1024 * 1024;
22
+ // Final-object cap. The server's Supabase project-level (global) storage
23
+ // limit was raised from 50 MiB to 512 MiB on 2026-07-04 (BLI-2528; see
24
+ // docs/runbooks/cockpit-stuck-evidence-uploads.md) after that ceiling silently
25
+ // rejected every larger raw-evidence object for 57 days, so this constant must
26
+ // track the server's real ceiling rather than trail it. It cannot simply BE
27
+ // 512 MiB, though: the commit route (apps/dashboard .../evidence/upload/commit)
28
+ // still assembles the object into one Buffer and calls
29
+ // `assembled.toString("utf8")` for the secret-content scan, and 512 MiB
30
+ // (536,870,912 bytes) is 24 bytes PAST V8's `MAX_STRING_LENGTH`
31
+ // (536,870,888 on Node 22 / this repo's pinned runtime) — a file at the literal
32
+ // 512 MiB ceiling would crash the commit route with a RangeError instead of
33
+ // committing, the same silent-loss failure mode BLI-2528 was about. 500 MiB
34
+ // keeps ~12.6 MiB of real headroom under that hard ceiling for the
35
+ // Buffer.concat + string-conversion overhead, while still fitting the chunk
36
+ // protocol's 576 MiB ceiling (192 chunks * 3 MiB).
37
+ export const RAW_EVIDENCE_UPLOAD_MAX_FILE_BYTES = 500 * 1024 * 1024;
27
38
  export const RAW_EVIDENCE_UPLOAD_MAX_OBJECTS_PER_BEGIN = 25;
28
39
  export const CODEX_SESSION_REPORT_MAX_SESSIONS = 200;
29
40
  const SafeLabelSchema = z
@@ -154,6 +165,72 @@ export const RawEvidenceUploadCommitResponseSchema = z
154
165
  redaction: RawEvidenceRedactionMetadataSchema.optional(),
155
166
  })
156
167
  .strict();
168
+ /**
169
+ * The collector giving up on an object it already opened (BLI-2539).
170
+ *
171
+ * `begin` writes a ledger row for every object in the batch, then chunks go one
172
+ * object at a time. When one object's chunks fail its siblings still finish, so
173
+ * the production shape is a single failure among committed rows rather than a
174
+ * truncated batch. The collector knows exactly why that object died — it
175
+ * composes labels like `chunk_0_failed_http_500` — and until this route existed
176
+ * it had nowhere to send them: begin/chunk/commit has no call meaning "I quit".
177
+ * The row stayed `pending` with a null reason until a drain relabelled it
178
+ * `staging_incomplete`, which describes the shape and not the cause. 275 rows
179
+ * reached that state by 2026-08-14.
180
+ *
181
+ * The reason is a label, never free text or an error body: it lands in
182
+ * `status_reason`, which is read by `classifyUploadFailure`, and an unlisted
183
+ * label classifies as `unknown` rather than being guessed into a bucket. That
184
+ * is also what keeps these rows recoverable — `unknown` is not permanent, so
185
+ * the row stays open (still `pending`, staged chunks intact) and the next
186
+ * `begin` resumes it. Only a reason classified permanent fails the row closed.
187
+ */
188
+ export const RawEvidenceUploadAbortRequestSchema = z
189
+ .object({
190
+ schema_version: z.literal("ambient-raw-evidence-upload-abort.v1"),
191
+ generated_at: IsoDateTimeSchema,
192
+ provenance: CaptureProvenanceSchema,
193
+ upload_id: NonEmptyStringSchema,
194
+ object_key: NonEmptyStringSchema,
195
+ reason: SafeLabelSchema,
196
+ // How far the object actually got before the collector gave up. The server
197
+ // already knows the received chunk count; this is what the CLIENT believed,
198
+ // and a disagreement between the two is itself worth being able to see.
199
+ uploaded_chunk_count: z.number().int().nonnegative().optional(),
200
+ })
201
+ .strict();
202
+ export const RawEvidenceUploadAbortResponseSchema = z
203
+ .object({
204
+ ok: z.literal(true),
205
+ // `reason_recorded` is the ordinary answer: the reason landed on the
206
+ // still-open row, whose staged chunks stay resumable by the next `begin`.
207
+ // `aborted` means the reason was classified permanent and the row failed
208
+ // closed. `already_terminal` covers a row the server had already settled —
209
+ // a drain that ran first, or a duplicate abort after a retry. Reporting
210
+ // these back distinguishes "we recorded your reason" from "someone beat
211
+ // you to it", which a bare 200 would hide.
212
+ status: z.enum(["reason_recorded", "aborted", "already_terminal"]),
213
+ upload_id: NonEmptyStringSchema,
214
+ object_key: NonEmptyStringSchema,
215
+ status_reason: SafeLabelSchema,
216
+ // Two booleans because the underlying questions are not each other's
217
+ // negation, and one field would have to lie about half of them.
218
+ //
219
+ // `retryable` is `isRetryableUploadFailure`: is repeating this KNOWN to
220
+ // help? An unclassified reason answers no — refusing to promise something
221
+ // nobody has reasoned about.
222
+ //
223
+ // `will_reopen` is `!isPermanentUploadFailure`: is this row still open (or
224
+ // re-openable) for the next `begin`? An unclassified reason answers yes —
225
+ // refusing to abandon an object over a word it cannot read.
226
+ //
227
+ // So a composed transport label like `chunk_0_failed_http_500` comes back
228
+ // `retryable: false, will_reopen: true`, which is the honest pair: we are
229
+ // not claiming a retry succeeds, and we are not writing the row off.
230
+ retryable: z.boolean(),
231
+ will_reopen: z.boolean(),
232
+ })
233
+ .strict();
157
234
  /**
158
235
  * Codex session attribution report.
159
236
  *
@@ -45,6 +45,12 @@ const UPLOAD_FAILURE_CLASSES = {
45
45
  collection_failed: "transient",
46
46
  file_read_failed: "transient",
47
47
  git_diff_failed: "transient",
48
+ // `begin` opens a ledger row per object in a loop with no transaction, so a
49
+ // throw partway through leaves the earlier rows written while the client sees
50
+ // one 500 for the batch (BLI-2539: 12 such batches in production). Those rows
51
+ // now name themselves instead of going silent. Transient because the object
52
+ // was never offered a chance to upload — nothing about it was refused.
53
+ begin_batch_server_error: "transient",
48
54
  // The file itself is the problem, and it will be the same size and the same
49
55
  // shape on the next pass. Retrying is a promise nobody can keep.
50
56
  file_too_large: "deterministic",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bli-cockpit/telemetry-core",
3
- "version": "0.1.18",
3
+ "version": "0.1.20",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",