@bli-cockpit/telemetry-core 0.1.17 → 0.1.19
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
|
*
|
package/dist/evidence-upload.js
CHANGED
|
@@ -154,6 +154,72 @@ export const RawEvidenceUploadCommitResponseSchema = z
|
|
|
154
154
|
redaction: RawEvidenceRedactionMetadataSchema.optional(),
|
|
155
155
|
})
|
|
156
156
|
.strict();
|
|
157
|
+
/**
|
|
158
|
+
* The collector giving up on an object it already opened (BLI-2539).
|
|
159
|
+
*
|
|
160
|
+
* `begin` writes a ledger row for every object in the batch, then chunks go one
|
|
161
|
+
* object at a time. When one object's chunks fail its siblings still finish, so
|
|
162
|
+
* the production shape is a single failure among committed rows rather than a
|
|
163
|
+
* truncated batch. The collector knows exactly why that object died — it
|
|
164
|
+
* composes labels like `chunk_0_failed_http_500` — and until this route existed
|
|
165
|
+
* it had nowhere to send them: begin/chunk/commit has no call meaning "I quit".
|
|
166
|
+
* The row stayed `pending` with a null reason until a drain relabelled it
|
|
167
|
+
* `staging_incomplete`, which describes the shape and not the cause. 275 rows
|
|
168
|
+
* reached that state by 2026-08-14.
|
|
169
|
+
*
|
|
170
|
+
* The reason is a label, never free text or an error body: it lands in
|
|
171
|
+
* `status_reason`, which is read by `classifyUploadFailure`, and an unlisted
|
|
172
|
+
* label classifies as `unknown` rather than being guessed into a bucket. That
|
|
173
|
+
* is also what keeps these rows recoverable — `unknown` is not permanent, so
|
|
174
|
+
* the row stays open (still `pending`, staged chunks intact) and the next
|
|
175
|
+
* `begin` resumes it. Only a reason classified permanent fails the row closed.
|
|
176
|
+
*/
|
|
177
|
+
export const RawEvidenceUploadAbortRequestSchema = z
|
|
178
|
+
.object({
|
|
179
|
+
schema_version: z.literal("ambient-raw-evidence-upload-abort.v1"),
|
|
180
|
+
generated_at: IsoDateTimeSchema,
|
|
181
|
+
provenance: CaptureProvenanceSchema,
|
|
182
|
+
upload_id: NonEmptyStringSchema,
|
|
183
|
+
object_key: NonEmptyStringSchema,
|
|
184
|
+
reason: SafeLabelSchema,
|
|
185
|
+
// How far the object actually got before the collector gave up. The server
|
|
186
|
+
// already knows the received chunk count; this is what the CLIENT believed,
|
|
187
|
+
// and a disagreement between the two is itself worth being able to see.
|
|
188
|
+
uploaded_chunk_count: z.number().int().nonnegative().optional(),
|
|
189
|
+
})
|
|
190
|
+
.strict();
|
|
191
|
+
export const RawEvidenceUploadAbortResponseSchema = z
|
|
192
|
+
.object({
|
|
193
|
+
ok: z.literal(true),
|
|
194
|
+
// `reason_recorded` is the ordinary answer: the reason landed on the
|
|
195
|
+
// still-open row, whose staged chunks stay resumable by the next `begin`.
|
|
196
|
+
// `aborted` means the reason was classified permanent and the row failed
|
|
197
|
+
// closed. `already_terminal` covers a row the server had already settled —
|
|
198
|
+
// a drain that ran first, or a duplicate abort after a retry. Reporting
|
|
199
|
+
// these back distinguishes "we recorded your reason" from "someone beat
|
|
200
|
+
// you to it", which a bare 200 would hide.
|
|
201
|
+
status: z.enum(["reason_recorded", "aborted", "already_terminal"]),
|
|
202
|
+
upload_id: NonEmptyStringSchema,
|
|
203
|
+
object_key: NonEmptyStringSchema,
|
|
204
|
+
status_reason: SafeLabelSchema,
|
|
205
|
+
// Two booleans because the underlying questions are not each other's
|
|
206
|
+
// negation, and one field would have to lie about half of them.
|
|
207
|
+
//
|
|
208
|
+
// `retryable` is `isRetryableUploadFailure`: is repeating this KNOWN to
|
|
209
|
+
// help? An unclassified reason answers no — refusing to promise something
|
|
210
|
+
// nobody has reasoned about.
|
|
211
|
+
//
|
|
212
|
+
// `will_reopen` is `!isPermanentUploadFailure`: is this row still open (or
|
|
213
|
+
// re-openable) for the next `begin`? An unclassified reason answers yes —
|
|
214
|
+
// refusing to abandon an object over a word it cannot read.
|
|
215
|
+
//
|
|
216
|
+
// So a composed transport label like `chunk_0_failed_http_500` comes back
|
|
217
|
+
// `retryable: false, will_reopen: true`, which is the honest pair: we are
|
|
218
|
+
// not claiming a retry succeeds, and we are not writing the row off.
|
|
219
|
+
retryable: z.boolean(),
|
|
220
|
+
will_reopen: z.boolean(),
|
|
221
|
+
})
|
|
222
|
+
.strict();
|
|
157
223
|
/**
|
|
158
224
|
* Codex session attribution report.
|
|
159
225
|
*
|
package/dist/secret-guards.js
CHANGED
|
@@ -225,7 +225,30 @@ export function redactSecretLikeContent(value, options = {}) {
|
|
|
225
225
|
});
|
|
226
226
|
return { redacted: true, text, metadata };
|
|
227
227
|
}
|
|
228
|
+
/**
|
|
229
|
+
* Every region the detector considers a secret, redacted — precisely where a
|
|
230
|
+
* rule can pinpoint the value, coarsely where none can.
|
|
231
|
+
*
|
|
232
|
+
* `containsSecretLikeContent` and `SECRET_REDACTION_RULES` are two lists that
|
|
233
|
+
* have to agree, and nothing used to make them. When the detector matched and
|
|
234
|
+
* no rule produced a range, `redactSecretLikeContent` reported
|
|
235
|
+
* `redacted: false`, and the caller dropped the entire session file
|
|
236
|
+
* (`secret_like_content_guard`). Drift between the lists cost whole sessions:
|
|
237
|
+
* 78 fleet-wide as of 2026-08-14, 3 of Viet's 17 Claude sessions.
|
|
238
|
+
*
|
|
239
|
+
* The two lists can still drift — regexes are like that — but drift now costs
|
|
240
|
+
* precision instead of evidence. A detector hit with no matching rule redacts
|
|
241
|
+
* the whole matched span, so `redactSecretLikeContent` always reports the
|
|
242
|
+
* redaction it performed and the transcript survives with the value removed.
|
|
243
|
+
*/
|
|
228
244
|
function findSecretRedactionMatches(value) {
|
|
245
|
+
const accepted = acceptNonOverlapping(secretRuleCandidates(value));
|
|
246
|
+
const uncovered = detectorFallbackCandidates(value).filter((candidate) => !overlapsAny(candidate, accepted));
|
|
247
|
+
if (uncovered.length === 0)
|
|
248
|
+
return accepted;
|
|
249
|
+
return acceptNonOverlapping([...accepted, ...uncovered]);
|
|
250
|
+
}
|
|
251
|
+
function secretRuleCandidates(value) {
|
|
229
252
|
const candidates = [];
|
|
230
253
|
SECRET_REDACTION_RULES.forEach((rule, priority) => {
|
|
231
254
|
const pattern = cloneGlobalPattern(rule.pattern);
|
|
@@ -248,7 +271,36 @@ function findSecretRedactionMatches(value) {
|
|
|
248
271
|
});
|
|
249
272
|
}
|
|
250
273
|
});
|
|
251
|
-
candidates
|
|
274
|
+
return candidates;
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Ranked below every real rule, so a rule that can name the credential always
|
|
278
|
+
* wins and the coarse span is only used where nothing else reached.
|
|
279
|
+
*/
|
|
280
|
+
function detectorFallbackCandidates(value) {
|
|
281
|
+
const candidates = [];
|
|
282
|
+
const basePriority = SECRET_REDACTION_RULES.length;
|
|
283
|
+
SECRET_LIKE_CONTENT_PATTERNS.forEach((pattern, offset) => {
|
|
284
|
+
for (const match of value.matchAll(cloneGlobalPattern(pattern))) {
|
|
285
|
+
const fullMatch = match[0];
|
|
286
|
+
const matchIndex = match.index;
|
|
287
|
+
if (!fullMatch || matchIndex === undefined)
|
|
288
|
+
continue;
|
|
289
|
+
candidates.push({
|
|
290
|
+
start: matchIndex,
|
|
291
|
+
end: matchIndex + fullMatch.length,
|
|
292
|
+
ruleId: "unnamed_secret_shape",
|
|
293
|
+
priority: basePriority + offset,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
return candidates;
|
|
298
|
+
}
|
|
299
|
+
function overlapsAny(candidate, accepted) {
|
|
300
|
+
return accepted.some((other) => candidate.start < other.end && other.start < candidate.end);
|
|
301
|
+
}
|
|
302
|
+
function acceptNonOverlapping(candidates) {
|
|
303
|
+
const ordered = [...candidates].sort((a, b) => {
|
|
252
304
|
if (a.start !== b.start)
|
|
253
305
|
return a.start - b.start;
|
|
254
306
|
if (a.priority !== b.priority)
|
|
@@ -257,7 +309,7 @@ function findSecretRedactionMatches(value) {
|
|
|
257
309
|
});
|
|
258
310
|
const accepted = [];
|
|
259
311
|
let lastEnd = -1;
|
|
260
|
-
for (const candidate of
|
|
312
|
+
for (const candidate of ordered) {
|
|
261
313
|
if (candidate.start < lastEnd)
|
|
262
314
|
continue;
|
|
263
315
|
accepted.push(candidate);
|
|
@@ -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",
|