@bli-cockpit/telemetry-core 0.1.16 → 0.1.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.
@@ -448,6 +448,7 @@ export declare const CodexSessionAttributionSchema: z.ZodObject<{
448
448
  upload_failed: "upload_failed";
449
449
  not_uploaded: "not_uploaded";
450
450
  }>>;
451
+ upload_reason: z.ZodOptional<z.ZodString>;
451
452
  }, z.core.$strict>;
452
453
  export type CodexSessionAttribution = z.infer<typeof CodexSessionAttributionSchema>;
453
454
  export declare const CodexSessionAttributionReportRequestSchema: z.ZodObject<{
@@ -516,6 +517,7 @@ export declare const CodexSessionAttributionReportRequestSchema: z.ZodObject<{
516
517
  upload_failed: "upload_failed";
517
518
  not_uploaded: "not_uploaded";
518
519
  }>>;
520
+ upload_reason: z.ZodOptional<z.ZodString>;
519
521
  }, z.core.$strict>>;
520
522
  }, z.core.$strict>;
521
523
  export type CodexSessionAttributionReportRequest = z.infer<typeof CodexSessionAttributionReportRequestSchema>;
@@ -217,6 +217,23 @@ export const CodexSessionAttributionSchema = z
217
217
  .optional(),
218
218
  raw_evidence_pointer_id: NonEmptyStringSchema.optional(),
219
219
  upload_state: CodexSessionUploadStateSchema.optional(),
220
+ /**
221
+ * Why this session has no durable pointer.
222
+ *
223
+ * `attribution_reason` is required, so a session that fails attribution
224
+ * always says why. Until BLI-2107 the upload half had no such field, and
225
+ * `not_uploaded` was written as the branch of last resort — the state
226
+ * meaning "we did not upload it and we are not saying why". 1,118 of the
227
+ * 2,400 sessions missing a durable pointer on 2026-07-28 sat in exactly
228
+ * that hole, 87 of them fully `attributed` with no ambiguity at all.
229
+ *
230
+ * That is unfixable and un-retryable by construction: you cannot tell a
231
+ * dropped network from a file we will never accept, so a retry either
232
+ * repeats a doomed attempt forever or gives up on a recoverable one.
233
+ * Optional only because already-published CLI versions do not send it;
234
+ * every write path in this repo sets it.
235
+ */
236
+ upload_reason: SafeLabelSchema.optional(),
220
237
  })
221
238
  .strict();
222
239
  export const CodexSessionAttributionReportRequestSchema = z
package/dist/index.d.ts CHANGED
@@ -13,4 +13,5 @@ export * from "./secret-guards.js";
13
13
  export * from "./source-adapter.js";
14
14
  export * from "./ticket-id.js";
15
15
  export * from "./trace-sink.js";
16
+ export * from "./upload-failure-class.js";
16
17
  export * from "./work-context.js";
package/dist/index.js CHANGED
@@ -13,4 +13,5 @@ export * from "./secret-guards.js";
13
13
  export * from "./source-adapter.js";
14
14
  export * from "./ticket-id.js";
15
15
  export * from "./trace-sink.js";
16
+ export * from "./upload-failure-class.js";
16
17
  export * from "./work-context.js";
@@ -0,0 +1,63 @@
1
+ /**
2
+ * What a caller should do about it.
3
+ *
4
+ * - `transient` — something outside the run interfered. The identical retry is
5
+ * the right retry.
6
+ * - `deterministic` — the run completed and the answer was no. Repeating it
7
+ * changes nothing; this needs a different method or a human.
8
+ * - `withheld_by_policy` — collection worked and we chose not to upload. Not a
9
+ * failure, and emphatically not something to "fix" by retrying: that would be
10
+ * defeating a redaction guard.
11
+ * - `unknown` — an unlisted reason. Not silently lumped in with the others,
12
+ * because the fleet contract's whole point is that a green status must never
13
+ * hide missing collection.
14
+ */
15
+ export type UploadFailureClass = "transient" | "deterministic" | "withheld_by_policy" | "unknown";
16
+ /**
17
+ * The reason written when nothing in the pipeline explained itself.
18
+ *
19
+ * This is the honest name for the hole BLI-2107 found. It is expected to be
20
+ * non-zero on the first release and to fall toward zero as each path that can
21
+ * reach it learns to say why — a session carrying this label means the collector
22
+ * declined to upload and no code on the way there recorded a cause.
23
+ */
24
+ export declare const NO_UPLOAD_ATTEMPT_RECORDED = "no_upload_attempt_recorded";
25
+ /**
26
+ * Storage refused the finished object because of its size.
27
+ *
28
+ * Named here rather than at the commit route so the label the server writes and
29
+ * the label the collector classifies are the same string by construction. They
30
+ * drifted apart once already: the collector could say `file_too_large` about a
31
+ * file it declined to send, and had no word at all for storage declining to
32
+ * keep one it had already received.
33
+ */
34
+ export declare const STORAGE_REJECTED_OBJECT_TOO_LARGE = "storage_rejected_object_too_large";
35
+ /** Storage refused the finished object because of its declared media type. */
36
+ export declare const STORAGE_REJECTED_MEDIA_TYPE = "storage_rejected_media_type";
37
+ /** Classify a persisted upload reason. Unlisted reasons stay `unknown`. */
38
+ export declare function classifyUploadFailure(reason: string | null | undefined): UploadFailureClass;
39
+ /**
40
+ * Whether repeating the identical attempt could plausibly succeed.
41
+ *
42
+ * `unknown` answers false. An unclassified reason is one nobody has reasoned
43
+ * about yet, and retrying on that basis is the loop this ticket exists to end;
44
+ * it should surface for a decision instead.
45
+ */
46
+ export declare function isRetryableUploadFailure(reason: string | null | undefined): boolean;
47
+ /**
48
+ * Whether repeating the attempt is known to be pointless.
49
+ *
50
+ * The deliberate mirror image of `isRetryableUploadFailure`, and not its
51
+ * negation: `unknown` answers false here and false there. The two questions
52
+ * are asked in different places and an unlisted reason has to answer them
53
+ * differently.
54
+ *
55
+ * `isRetryableUploadFailure` decides whether to *claim* a retry will help, so
56
+ * silence means no. This one decides whether to *stop* retrying, so silence
57
+ * means keep going — the labels the collector composes on the fly
58
+ * (`commit_failed_http_503`, `chunk_7_failed_http_500`) are unlisted by
59
+ * construction, and treating them as permanent would abandon a sync over a
60
+ * storage blip. Only a reason somebody has classified as deterministic or
61
+ * withheld ends the loop.
62
+ */
63
+ export declare function isPermanentUploadFailure(reason: string | null | undefined): boolean;
@@ -0,0 +1,109 @@
1
+ // BLI-2107: what to do about a session that has no durable pointer.
2
+ //
3
+ // A retry that repeats the same call is only correct when something outside the
4
+ // run interfered — the network dropped, a budget was exhausted, a lock was
5
+ // held. If the run completed and the answer was still no, repeating it produces
6
+ // the same no, forever, and the only signal that anything is wrong is a number
7
+ // that never improves. That is how 1,118 sessions reached 2026-07-28 with no
8
+ // pointer and no explanation.
9
+ //
10
+ // So the reason is not just recorded, it is classified, and the class decides
11
+ // the response. Deliberately a flat lookup rather than pattern matching on the
12
+ // label: a rule like "contains `failed` means retry" quietly reclassifies every
13
+ // future reason somebody adds, which is the same class of guessing the person
14
+ // resolver refuses to do. An unlisted reason is `unknown` and stays visible
15
+ // until a human decides which bucket it belongs in.
16
+ /**
17
+ * The reason written when nothing in the pipeline explained itself.
18
+ *
19
+ * This is the honest name for the hole BLI-2107 found. It is expected to be
20
+ * non-zero on the first release and to fall toward zero as each path that can
21
+ * reach it learns to say why — a session carrying this label means the collector
22
+ * declined to upload and no code on the way there recorded a cause.
23
+ */
24
+ export const NO_UPLOAD_ATTEMPT_RECORDED = "no_upload_attempt_recorded";
25
+ /**
26
+ * Storage refused the finished object because of its size.
27
+ *
28
+ * Named here rather than at the commit route so the label the server writes and
29
+ * the label the collector classifies are the same string by construction. They
30
+ * drifted apart once already: the collector could say `file_too_large` about a
31
+ * file it declined to send, and had no word at all for storage declining to
32
+ * keep one it had already received.
33
+ */
34
+ export const STORAGE_REJECTED_OBJECT_TOO_LARGE = "storage_rejected_object_too_large";
35
+ /** Storage refused the finished object because of its declared media type. */
36
+ export const STORAGE_REJECTED_MEDIA_TYPE = "storage_rejected_media_type";
37
+ const UPLOAD_FAILURE_CLASSES = {
38
+ // Budgets and locks: the file was fine and the run simply ran out of room or
39
+ // was beaten to it. The next pass has room.
40
+ deferred_object_budget: "transient",
41
+ deferred_byte_budget: "transient",
42
+ sync_already_running: "transient",
43
+ // Delivery. The bytes never reached the other end, or the run died on the way.
44
+ upload_failed: "transient",
45
+ collection_failed: "transient",
46
+ file_read_failed: "transient",
47
+ git_diff_failed: "transient",
48
+ // The file itself is the problem, and it will be the same size and the same
49
+ // shape on the next pass. Retrying is a promise nobody can keep.
50
+ file_too_large: "deterministic",
51
+ // Storage looked at the finished object and said no. The bytes are already
52
+ // final and content-addressed, so the next attempt presents the identical
53
+ // object to the identical rule (BLI-2528). Retrying these is what let 76
54
+ // objects retry for 57 days without one of them ever landing.
55
+ [STORAGE_REJECTED_OBJECT_TOO_LARGE]: "deterministic",
56
+ [STORAGE_REJECTED_MEDIA_TYPE]: "deterministic",
57
+ // The content-addressed key already holds different bytes. One of the two is
58
+ // wrong and repeating the write cannot decide which; a human has to look.
59
+ object_conflict: "deterministic",
60
+ // Attribution never produced a target, so there is nowhere to put the object.
61
+ // Widening discovery can change this; running the same scan again cannot.
62
+ cwd_outside_scanned_worktrees: "deterministic",
63
+ cwd_not_a_repo: "deterministic",
64
+ no_repo_signals: "deterministic",
65
+ multiple_worktrees_close_scores: "deterministic",
66
+ // Working as designed. Never retry these — a retry here is an attempt to get
67
+ // a redaction guard to change its mind.
68
+ secret_like_content_guard: "withheld_by_policy",
69
+ secret_like_file_name: "withheld_by_policy",
70
+ secret_redaction_failed: "withheld_by_policy",
71
+ // The server-side name for the same guard, returned by the commit route.
72
+ secret_guard_rejected: "withheld_by_policy",
73
+ };
74
+ /** Classify a persisted upload reason. Unlisted reasons stay `unknown`. */
75
+ export function classifyUploadFailure(reason) {
76
+ if (!reason)
77
+ return "unknown";
78
+ return UPLOAD_FAILURE_CLASSES[reason] ?? "unknown";
79
+ }
80
+ /**
81
+ * Whether repeating the identical attempt could plausibly succeed.
82
+ *
83
+ * `unknown` answers false. An unclassified reason is one nobody has reasoned
84
+ * about yet, and retrying on that basis is the loop this ticket exists to end;
85
+ * it should surface for a decision instead.
86
+ */
87
+ export function isRetryableUploadFailure(reason) {
88
+ return classifyUploadFailure(reason) === "transient";
89
+ }
90
+ /**
91
+ * Whether repeating the attempt is known to be pointless.
92
+ *
93
+ * The deliberate mirror image of `isRetryableUploadFailure`, and not its
94
+ * negation: `unknown` answers false here and false there. The two questions
95
+ * are asked in different places and an unlisted reason has to answer them
96
+ * differently.
97
+ *
98
+ * `isRetryableUploadFailure` decides whether to *claim* a retry will help, so
99
+ * silence means no. This one decides whether to *stop* retrying, so silence
100
+ * means keep going — the labels the collector composes on the fly
101
+ * (`commit_failed_http_503`, `chunk_7_failed_http_500`) are unlisted by
102
+ * construction, and treating them as permanent would abandon a sync over a
103
+ * storage blip. Only a reason somebody has classified as deterministic or
104
+ * withheld ends the loop.
105
+ */
106
+ export function isPermanentUploadFailure(reason) {
107
+ const failureClass = classifyUploadFailure(reason);
108
+ return failureClass === "deterministic" || failureClass === "withheld_by_policy";
109
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bli-cockpit/telemetry-core",
3
- "version": "0.1.16",
3
+ "version": "0.1.17",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",