@bli-cockpit/cli 0.2.237 → 0.2.238

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.
Files changed (139) hide show
  1. package/dist/adapters/attribution-core-fallbacks.js +15 -11
  2. package/dist/adapters/attribution-core-score.js +111 -87
  3. package/dist/adapters/car-state.js +38 -35
  4. package/dist/adapters/claude-attribution-discovery.js +81 -56
  5. package/dist/adapters/claude-attribution-score.js +108 -94
  6. package/dist/adapters/claude-attribution-signals.js +52 -42
  7. package/dist/adapters/codex-attribution.js +42 -33
  8. package/dist/adapters/codex-session-signals.js +11 -9
  9. package/dist/adapters/git-state.js +56 -50
  10. package/dist/adapters/local-sources.js +52 -43
  11. package/dist/adapters/raw-evidence-git-diff.js +4 -2
  12. package/dist/adapters/raw-evidence-sanitize.js +15 -12
  13. package/dist/adapters/raw-evidence-transcript-file.js +32 -31
  14. package/dist/autostart-self-heal.js +18 -2
  15. package/dist/commands/agent-session-report.js +51 -59
  16. package/dist/commands/agent-setup-observed.js +30 -23
  17. package/dist/commands/agent-setup.js +96 -45
  18. package/dist/commands/autostart-heal.js +8 -2
  19. package/dist/commands/autostart-reading.js +3 -0
  20. package/dist/commands/backfill-evidence-outcomes.js +9 -6
  21. package/dist/commands/backfill-issues.js +3 -0
  22. package/dist/commands/backfill-scan.js +27 -22
  23. package/dist/commands/backfill-upload.js +69 -60
  24. package/dist/commands/backfill.js +26 -21
  25. package/dist/commands/brief-done.js +4 -1
  26. package/dist/commands/brief-edit.js +71 -46
  27. package/dist/commands/cal-detail.js +35 -0
  28. package/dist/commands/cal-overlay.js +73 -30
  29. package/dist/commands/cal.js +36 -23
  30. package/dist/commands/clean.js +4 -1
  31. package/dist/commands/docs-read.js +5 -2
  32. package/dist/commands/doctor-backfill.js +88 -71
  33. package/dist/commands/doctor-pipeline.js +59 -46
  34. package/dist/commands/heartbeat-post.js +116 -0
  35. package/dist/commands/heartbeat-token-lock.js +46 -33
  36. package/dist/commands/heartbeat.js +43 -125
  37. package/dist/commands/install-update.js +17 -15
  38. package/dist/commands/issue-show.js +27 -28
  39. package/dist/commands/issue-write.js +70 -23
  40. package/dist/commands/issue.js +68 -37
  41. package/dist/commands/jarvis-live-answer.js +99 -0
  42. package/dist/commands/jarvis-render.js +1 -94
  43. package/dist/commands/jarvis-trace.js +30 -22
  44. package/dist/commands/jarvis-turn-report.js +124 -0
  45. package/dist/commands/jarvis-turn.js +108 -182
  46. package/dist/commands/jarvis.js +26 -17
  47. package/dist/commands/local-args-collector-setup.js +82 -84
  48. package/dist/commands/local-args-collector-work.js +37 -38
  49. package/dist/commands/local-args-tower-admin.js +155 -142
  50. package/dist/commands/local-args-tower-cal.js +52 -50
  51. package/dist/commands/local-args-tower-careers.js +73 -54
  52. package/dist/commands/local-args-tower-docs-msg.js +55 -45
  53. package/dist/commands/local-args-tower-github.js +26 -20
  54. package/dist/commands/local-args-tower-hits.js +32 -20
  55. package/dist/commands/local-args-tower-manager.js +85 -4
  56. package/dist/commands/local-args-tower-memory.js +139 -117
  57. package/dist/commands/local-args-tower-notes.js +346 -0
  58. package/dist/commands/local-args-tower-pages.js +100 -378
  59. package/dist/commands/local-args-tower-settings.js +28 -29
  60. package/dist/commands/local-args-tower-work.js +52 -33
  61. package/dist/commands/local-args.js +80 -129
  62. package/dist/commands/local-help-commands-tower-ops.js +15 -4
  63. package/dist/commands/local-help-commands-tower.js +6 -2
  64. package/dist/commands/local-help-commands.js +342 -341
  65. package/dist/commands/local-help.js +87 -83
  66. package/dist/commands/local-run-guards.js +100 -0
  67. package/dist/commands/local.js +137 -206
  68. package/dist/commands/login.js +13 -9
  69. package/dist/commands/logs.js +55 -38
  70. package/dist/commands/mail-attachments.js +44 -26
  71. package/dist/commands/manager-render-home.js +208 -0
  72. package/dist/commands/manager-render.js +7 -3
  73. package/dist/commands/manager-requests.js +33 -0
  74. package/dist/commands/manager.js +39 -5
  75. package/dist/commands/memory-consolidation.js +18 -9
  76. package/dist/commands/memory-daemon-probe.js +37 -28
  77. package/dist/commands/memory-install-config.js +64 -60
  78. package/dist/commands/memory-install.js +21 -41
  79. package/dist/commands/memory-requeue.js +26 -19
  80. package/dist/commands/memory-rules.js +26 -18
  81. package/dist/commands/memory-search-print.js +26 -0
  82. package/dist/commands/memory-store.js +14 -30
  83. package/dist/commands/memory-verb-list.js +49 -48
  84. package/dist/commands/notes-accounts.js +49 -32
  85. package/dist/commands/notes-sync-loop.js +115 -89
  86. package/dist/commands/notes-wisprflow-login.js +44 -23
  87. package/dist/commands/notes-writes.js +115 -100
  88. package/dist/commands/notes.js +33 -55
  89. package/dist/commands/onboard-flows.js +69 -51
  90. package/dist/commands/onboard.js +11 -9
  91. package/dist/commands/ops-render-memory.js +10 -7
  92. package/dist/commands/ops-render.js +33 -30
  93. package/dist/commands/ops.js +3 -0
  94. package/dist/commands/public-root.js +1 -1
  95. package/dist/commands/session-sync-outside-roots.js +40 -36
  96. package/dist/commands/session-sync-plan.js +14 -10
  97. package/dist/commands/session-sync-upload.js +24 -22
  98. package/dist/commands/session-sync.js +19 -8
  99. package/dist/commands/slack.js +25 -16
  100. package/dist/commands/sources-check.js +13 -7
  101. package/dist/commands/sources-discovery.js +25 -17
  102. package/dist/commands/sources-verdict.js +19 -18
  103. package/dist/commands/speed.js +23 -19
  104. package/dist/commands/start.js +20 -16
  105. package/dist/commands/sync-doctor.js +52 -48
  106. package/dist/commands/sync.js +22 -16
  107. package/dist/commands/usage-telemetry-toml.js +71 -45
  108. package/dist/commands/usage.js +91 -67
  109. package/dist/commands/workbook-render.js +10 -7
  110. package/dist/evidence-reconcile-client.js +75 -64
  111. package/dist/evidence-redelivery-plan.js +37 -34
  112. package/dist/evidence-redelivery.js +32 -29
  113. package/dist/evidence-transcripts-client.js +20 -20
  114. package/dist/evidence-upload-object.js +86 -89
  115. package/dist/evidence-upload-plan.js +94 -83
  116. package/dist/evidence-upload-terminal.js +45 -50
  117. package/dist/image-description-session.js +156 -159
  118. package/dist/local-state-pairing-code.js +39 -31
  119. package/dist/local-state-status.js +9 -2
  120. package/dist/onboarding-roots.js +5 -8
  121. package/dist/process-runner.js +8 -8
  122. package/dist/raw-evidence-gc.js +33 -38
  123. package/dist/raw-evidence-stuck-summary.js +7 -14
  124. package/dist/repo-identity.js +26 -23
  125. package/dist/scheduled-self-update.js +11 -15
  126. package/dist/second-install.js +3 -0
  127. package/dist/slim/codex-rules.js +23 -36
  128. package/dist/sync-lock.js +5 -15
  129. package/dist/tower-client.js +36 -53
  130. package/dist/tower-stream.js +7 -24
  131. package/dist/upload-agent-artifacts.js +58 -64
  132. package/dist/upload-envelope-build.js +60 -59
  133. package/dist/upload-evidence-delivery-reconcile.js +58 -56
  134. package/dist/upload-evidence-delivery-summary.js +32 -34
  135. package/dist/upload-ingest-receipt.js +20 -23
  136. package/dist/upload-session-reports-queue.js +4 -1
  137. package/dist/upload-session-reports-wire.js +64 -68
  138. package/dist/upload-sync.js +171 -157
  139. package/package.json +2 -2
@@ -55,70 +55,9 @@ export async function runEvidenceReconcile(options) {
55
55
  const batches = chunk(hashes, EVIDENCE_RECONCILE_MAX_BATCH);
56
56
  const bounded = options.maxBatches != null ? batches.slice(0, options.maxBatches) : batches;
57
57
  const cursor = await readReconcileCursor(paths);
58
- let committed = 0;
59
- let notCommitted = 0;
60
- let unknownToServer = 0;
61
- let failedBatches = 0;
62
- let firstFailureReason = null;
63
- let askedThisRun = 0;
64
- for (const batch of bounded) {
65
- const outcome = await sendReconcileBatch({
66
- dashboardUrl: options.dashboardUrl,
67
- deviceToken: session.device_token,
68
- hashes: batch,
69
- fetchImpl: doFetch,
70
- });
71
- if (!outcome.ok) {
72
- failedBatches += 1;
73
- firstFailureReason ??= outcome.reason;
74
- continue;
75
- }
76
- askedThisRun += batch.length;
77
- for (const item of outcome.results) {
78
- recordReconcileResult(cursor, item.content_hash_sha256, {
79
- verdict: item.verdict,
80
- checked_at: now.toISOString(),
81
- server_committed_at: item.verdict === "committed" ? item.committed_at : null,
82
- });
83
- if (item.verdict === "committed")
84
- committed += 1;
85
- else if (item.verdict === "not_committed")
86
- notCommitted += 1;
87
- else
88
- unknownToServer += 1;
89
- }
90
- }
91
- if (askedThisRun > 0) {
92
- cursor.updated_at = now.toISOString();
93
- await writeReconcileCursor(paths, cursor).catch((error) => {
94
- // The prune's own forgetStagedObjects follows the same rule: the
95
- // answer is not lost (the server has it), but a reader of THIS
96
- // machine will ask again next run rather than silently drift.
97
- console.error("[collector reconcile] reconcile cursor not written after answers landed", JSON.stringify({
98
- reason: "reconcile_cursor_write_failed",
99
- answered_count: askedThisRun,
100
- ...describeError(error),
101
- }));
102
- });
103
- }
104
- const result = {
105
- status: askedThisRun > 0 ? "ok" : "fail",
106
- reason: reconcileReason({
107
- askedThisRun,
108
- failedBatches,
109
- firstFailureReason,
110
- boundedShortOfAvailable: bounded.length < batches.length,
111
- }),
112
- asked: askedThisRun,
113
- committed,
114
- not_committed: notCommitted,
115
- unknown_to_server: unknownToServer,
116
- batches: bounded.length,
117
- failed_batches: failedBatches,
118
- total_batches: batches.length,
119
- };
120
- reportReconcile(result);
121
- return result;
58
+ const counts = await askReconcileBatches(bounded, cursor, options.dashboardUrl, session.device_token, doFetch, now);
59
+ await writeAnsweredReconcileCursor(paths, cursor, now, counts.askedThisRun);
60
+ return finishReconcilePass(counts, bounded.length, batches.length);
122
61
  }
123
62
  catch (error) {
124
63
  const result = {
@@ -129,6 +68,78 @@ export async function runEvidenceReconcile(options) {
129
68
  return result;
130
69
  }
131
70
  }
71
+ async function askReconcileBatches(bounded, cursor, dashboardUrl, deviceToken, doFetch, now) {
72
+ let committed = 0;
73
+ let notCommitted = 0;
74
+ let unknownToServer = 0;
75
+ let failedBatches = 0;
76
+ let firstFailureReason = null;
77
+ let askedThisRun = 0;
78
+ for (const batch of bounded) {
79
+ const outcome = await sendReconcileBatch({
80
+ dashboardUrl,
81
+ deviceToken,
82
+ hashes: batch,
83
+ fetchImpl: doFetch,
84
+ });
85
+ if (!outcome.ok) {
86
+ failedBatches += 1;
87
+ firstFailureReason ??= outcome.reason;
88
+ continue;
89
+ }
90
+ askedThisRun += batch.length;
91
+ for (const item of outcome.results) {
92
+ recordReconcileResult(cursor, item.content_hash_sha256, {
93
+ verdict: item.verdict,
94
+ checked_at: now.toISOString(),
95
+ server_committed_at: item.verdict === "committed" ? item.committed_at : null,
96
+ });
97
+ if (item.verdict === "committed")
98
+ committed += 1;
99
+ else if (item.verdict === "not_committed")
100
+ notCommitted += 1;
101
+ else
102
+ unknownToServer += 1;
103
+ }
104
+ }
105
+ return { committed, notCommitted, unknownToServer, failedBatches, firstFailureReason, askedThisRun };
106
+ }
107
+ async function writeAnsweredReconcileCursor(paths, cursor, now, askedThisRun) {
108
+ if (askedThisRun > 0) {
109
+ cursor.updated_at = now.toISOString();
110
+ await writeReconcileCursor(paths, cursor).catch((error) => {
111
+ // The prune's own forgetStagedObjects follows the same rule: the
112
+ // answer is not lost (the server has it), but a reader of THIS
113
+ // machine will ask again next run rather than silently drift.
114
+ console.error("[collector reconcile] reconcile cursor not written after answers landed", JSON.stringify({
115
+ reason: "reconcile_cursor_write_failed",
116
+ answered_count: askedThisRun,
117
+ ...describeError(error),
118
+ }));
119
+ });
120
+ }
121
+ }
122
+ function finishReconcilePass(counts, boundedCount, totalBatches) {
123
+ const { committed, notCommitted, unknownToServer, failedBatches, firstFailureReason, askedThisRun } = counts;
124
+ const result = {
125
+ status: askedThisRun > 0 ? "ok" : "fail",
126
+ reason: reconcileReason({
127
+ askedThisRun,
128
+ failedBatches,
129
+ firstFailureReason,
130
+ boundedShortOfAvailable: boundedCount < totalBatches,
131
+ }),
132
+ asked: askedThisRun,
133
+ committed,
134
+ not_committed: notCommitted,
135
+ unknown_to_server: unknownToServer,
136
+ batches: boundedCount,
137
+ failed_batches: failedBatches,
138
+ total_batches: totalBatches,
139
+ };
140
+ reportReconcile(result);
141
+ return result;
142
+ }
132
143
  function reconcileReason(input) {
133
144
  if (input.askedThisRun === 0) {
134
145
  return `reconcile_unavailable: ${input.firstFailureReason ?? "unknown"}`;
@@ -112,43 +112,46 @@ export async function planEvidenceRedelivery(options) {
112
112
  countUnplannable(plan, "no_manifest_row");
113
113
  continue;
114
114
  }
115
- let packPlan = packPlans.get(object.pack_id);
116
- if (!packPlan) {
117
- packPlan = {
118
- pack_id: object.pack_id,
119
- provenance: manifest.provenance,
120
- files: [],
121
- bytes: 0,
122
- };
123
- packPlans.set(object.pack_id, packPlan);
124
- plan.packs.push(packPlan);
125
- }
126
- packPlan.files.push({
127
- pointer: {
128
- raw_evidence_pointer_id: row.object_key,
129
- privacy_classification: "remote_durable_raw_evidence",
130
- retention_policy: {
131
- mode: RAW_EVIDENCE_RETENTION_MODE,
132
- privacy_classification: "remote_durable_raw_evidence",
133
- },
134
- storage_scope: "remote_object",
135
- storage_bucket: RAW_EVIDENCE_BUCKET,
136
- object_key: row.object_key,
137
- content_hash_sha256: row.content_hash_sha256,
138
- byte_size: row.byte_size,
139
- media_type: row.media_type,
140
- redacted_summary: row.redacted_summary,
141
- },
142
- local_path: path.join(root, object.pack_id, ...object.relative_path.split("/").filter(Boolean)),
143
- kind: row.kind,
144
- codex_session_id: null,
145
- });
146
- packPlan.bytes += object.byte_size;
147
- plan.object_count += 1;
148
- plan.bytes += object.byte_size;
115
+ addRedeliveryObject(plan, packPlans, manifest, row, object, root);
149
116
  }
150
117
  return plan;
151
118
  }
119
+ function addRedeliveryObject(plan, packPlans, manifest, row, object, root) {
120
+ let packPlan = packPlans.get(object.pack_id);
121
+ if (!packPlan) {
122
+ packPlan = {
123
+ pack_id: object.pack_id,
124
+ provenance: manifest.provenance,
125
+ files: [],
126
+ bytes: 0,
127
+ };
128
+ packPlans.set(object.pack_id, packPlan);
129
+ plan.packs.push(packPlan);
130
+ }
131
+ packPlan.files.push({
132
+ pointer: {
133
+ raw_evidence_pointer_id: row.object_key,
134
+ privacy_classification: "remote_durable_raw_evidence",
135
+ retention_policy: {
136
+ mode: RAW_EVIDENCE_RETENTION_MODE,
137
+ privacy_classification: "remote_durable_raw_evidence",
138
+ },
139
+ storage_scope: "remote_object",
140
+ storage_bucket: RAW_EVIDENCE_BUCKET,
141
+ object_key: row.object_key,
142
+ content_hash_sha256: row.content_hash_sha256,
143
+ byte_size: row.byte_size,
144
+ media_type: row.media_type,
145
+ redacted_summary: row.redacted_summary,
146
+ },
147
+ local_path: path.join(root, object.pack_id, ...object.relative_path.split("/").filter(Boolean)),
148
+ kind: row.kind,
149
+ codex_session_id: null,
150
+ });
151
+ packPlan.bytes += object.byte_size;
152
+ plan.object_count += 1;
153
+ plan.bytes += object.byte_size;
154
+ }
152
155
  /**
153
156
  * Every uncommitted object, oldest staged bytes first.
154
157
  *
@@ -78,41 +78,44 @@ export async function runEvidenceRedelivery(options) {
78
78
  reportRedelivery(result, plan);
79
79
  return result;
80
80
  }
81
- const outcome = await deliverPlan({
82
- plan,
83
- dashboardUrl: options.dashboardUrl,
84
- deviceToken: session.device_token,
85
- fetchImpl: options.fetch ?? fetch,
86
- now,
87
- });
88
- await recordOutcomes(paths, staging, outcome.outcomes, now);
89
- const terminalOutcomes = outcome.outcomes.filter(entry => entry.upload_state === "upload_failed" && terminalDeliveryReason(entry.reason));
90
- const result = {
91
- status: outcome.failed > 0 && outcome.uploaded === 0 ? "fail" : "ok",
92
- reason: "redelivered",
93
- terminal: (inventory.terminal_count ?? 0) + terminalOutcomes.length,
94
- terminal_bytes: (inventory.terminal_bytes ?? 0) + terminalOutcomes.reduce((bytes, entry) => bytes + (entry.pointer.byte_size ?? 0), 0),
95
- offered: plan.object_count,
96
- offered_bytes: plan.bytes,
97
- uploaded: outcome.uploaded,
98
- uploaded_bytes: outcome.uploadedBytes,
99
- reused: outcome.reused,
100
- reconciled_committed_by_409: outcome.outcomes.filter(entry => entry.reason === "reconciled_committed_by_409").length,
101
- failed: outcome.failed,
102
- held: plan.held_count,
103
- deferred: plan.deferred_count,
104
- deferred_bytes: plan.deferred_bytes,
105
- packs: plan.packs.length,
106
- failure_reasons: outcome.failureReasons,
107
- };
108
- reportRedelivery(result, plan);
109
- return result;
81
+ return await deliverAndRecordPlan(options, paths, staging, now, inventory, plan, session.device_token);
110
82
  }
111
83
  catch (error) {
112
84
  console.error("[evidence-redelivery] the drain could not run; nothing was re-offered", JSON.stringify({ reason: "redelivery_threw", ...describeError(error) }));
113
85
  return { ...emptyResult("redelivery_threw", "fail") };
114
86
  }
115
87
  }
88
+ async function deliverAndRecordPlan(options, paths, staging, now, inventory, plan, deviceToken) {
89
+ const outcome = await deliverPlan({
90
+ plan,
91
+ dashboardUrl: options.dashboardUrl,
92
+ deviceToken: deviceToken,
93
+ fetchImpl: options.fetch ?? fetch,
94
+ now,
95
+ });
96
+ await recordOutcomes(paths, staging, outcome.outcomes, now);
97
+ const terminalOutcomes = outcome.outcomes.filter(entry => entry.upload_state === "upload_failed" && terminalDeliveryReason(entry.reason));
98
+ const result = {
99
+ status: outcome.failed > 0 && outcome.uploaded === 0 ? "fail" : "ok",
100
+ reason: "redelivered",
101
+ terminal: (inventory.terminal_count ?? 0) + terminalOutcomes.length,
102
+ terminal_bytes: (inventory.terminal_bytes ?? 0) + terminalOutcomes.reduce((bytes, entry) => bytes + (entry.pointer.byte_size ?? 0), 0),
103
+ offered: plan.object_count,
104
+ offered_bytes: plan.bytes,
105
+ uploaded: outcome.uploaded,
106
+ uploaded_bytes: outcome.uploadedBytes,
107
+ reused: outcome.reused,
108
+ reconciled_committed_by_409: outcome.outcomes.filter(entry => entry.reason === "reconciled_committed_by_409").length,
109
+ failed: outcome.failed,
110
+ held: plan.held_count,
111
+ deferred: plan.deferred_count,
112
+ deferred_bytes: plan.deferred_bytes,
113
+ packs: plan.packs.length,
114
+ failure_reasons: outcome.failureReasons,
115
+ };
116
+ reportRedelivery(result, plan);
117
+ return result;
118
+ }
116
119
  /**
117
120
  * One `uploadRawEvidenceFilesChunked` call per pack, because provenance is a
118
121
  * PACK fact: the repo, branch, operator, session and work context these bytes
@@ -23,7 +23,7 @@ export async function fetchTranscriptObjects(options) {
23
23
  const files = new Map();
24
24
  const seen = new Set();
25
25
  let returned = 0;
26
- let unkeyed = 0;
26
+ const state = { unkeyed: 0 };
27
27
  let pages = 0;
28
28
  let after = null;
29
29
  for (;;) {
@@ -38,29 +38,13 @@ export async function fetchTranscriptObjects(options) {
38
38
  if (seen.has(object.ref_id))
39
39
  continue;
40
40
  seen.add(object.ref_id);
41
- addObject(object);
41
+ addTranscriptObject(object, files, state);
42
42
  }
43
43
  // Advance by what the server returned; a page that names no cursor is the last.
44
44
  if (!page.nextAfter)
45
45
  return finish("exhausted", "ok");
46
46
  after = page.nextAfter;
47
47
  }
48
- function addObject(object) {
49
- const identity = transcriptFileFromObjectKey(object.object_key);
50
- if (!identity) {
51
- unkeyed += 1;
52
- return;
53
- }
54
- const key = transcriptFileKey(identity);
55
- const list = files.get(key) ?? [];
56
- list.push({
57
- object_key: object.object_key,
58
- byte_size: object.byte_size,
59
- content_hash_sha256: object.content_hash_sha256,
60
- stub: object.stub,
61
- });
62
- files.set(key, list);
63
- }
64
48
  function finish(status, reason) {
65
49
  const result = {
66
50
  status,
@@ -69,7 +53,7 @@ export async function fetchTranscriptObjects(options) {
69
53
  requested: pages * pageSize,
70
54
  returned,
71
55
  unique: seen.size,
72
- unkeyed,
56
+ unkeyed: state.unkeyed,
73
57
  files,
74
58
  };
75
59
  console.error("[collector sources] transcript objects listed", JSON.stringify({
@@ -78,13 +62,29 @@ export async function fetchTranscriptObjects(options) {
78
62
  requested: result.requested,
79
63
  returned,
80
64
  unique: seen.size,
81
- unkeyed,
65
+ unkeyed: state.unkeyed,
82
66
  file_count: files.size,
83
67
  ...(status === "failed_partial" ? { failure: reason } : {}),
84
68
  }));
85
69
  return result;
86
70
  }
87
71
  }
72
+ function addTranscriptObject(object, files, state) {
73
+ const identity = transcriptFileFromObjectKey(object.object_key);
74
+ if (!identity) {
75
+ state.unkeyed += 1;
76
+ return;
77
+ }
78
+ const key = transcriptFileKey(identity);
79
+ const list = files.get(key) ?? [];
80
+ list.push({
81
+ object_key: object.object_key,
82
+ byte_size: object.byte_size,
83
+ content_hash_sha256: object.content_hash_sha256,
84
+ stub: object.stub,
85
+ });
86
+ files.set(key, list);
87
+ }
88
88
  async function requestPage(options) {
89
89
  const controller = new AbortController();
90
90
  const timeout = setTimeout(() => controller.abort(), TRANSCRIPTS_PAGE_TIMEOUT_MS);
@@ -56,60 +56,68 @@ async function uploadObjectChunks(options, entry, disposition, chunkSizeBytes, o
56
56
  uploadedChunks += 1;
57
57
  continue;
58
58
  }
59
- console.error("[evidence-upload] using chunk route", JSON.stringify({
60
- reason: offer ? "signed_upload_refused_fallback" : "signed_upload_not_offered",
61
- upload_id: disposition.upload_id, chunk_index: index,
62
- }));
63
- const encodedChunk = gzipSync(chunk);
64
- console.error("[evidence-upload] chunk encoded", JSON.stringify({
65
- upload_id: disposition.upload_id, chunk_index: index, chunk_encoding: "gzip",
66
- bytes_before: chunk.length, bytes_after: encodedChunk.length,
67
- }));
68
- const chunkResponse = await requestJson(options, "/api/ambient/evidence/upload/chunk", {
69
- schema_version: "ambient-raw-evidence-upload-chunk.v2",
70
- encoding: "gzip",
71
- generated_at: options.generatedAt,
72
- provenance: options.provenance,
73
- upload_id: disposition.upload_id,
74
- object_key: objectKey,
75
- chunk_index: index,
76
- chunk_count: entry.chunkCount,
77
- chunk_hash_sha256: sha256(chunk),
78
- content_base64: encodedChunk.toString("base64"),
79
- });
80
- if (!chunkResponse.ok) {
81
- // Same treatment the commit path has had since BLI-2528: the server's
82
- // own reason rides on the label, so `chunk_3_failed_http_413` becomes
83
- // `chunk_3_failed_http_413_object_too_large` and the ledger row names
84
- // the cause instead of the transport (BLI-3483).
85
- const chunkDetail = safeFailureDetail(chunkResponse.body);
86
- if (chunkResponse.status === 409 && chunkDetail === "upload_not_pending:committed") {
87
- console.error("[evidence-upload] committed receipt reconciled", JSON.stringify({
88
- reason: "reconciled_committed_by_409", upload_id: disposition.upload_id,
89
- }));
90
- return { ok: false, outcome: {
91
- ...failedOutcome(entry.file, "reconciled_committed_by_409", uploadedChunks),
92
- upload_state: "reused_existing",
93
- } };
94
- }
95
- console.error("[evidence-upload] chunk rejected", JSON.stringify({
96
- reason: "chunk_rejected",
97
- upload_id: disposition.upload_id,
98
- http_status: chunkResponse.status,
99
- server_reason: chunkDetail ?? "none",
100
- chunk_index: index,
101
- chunk_count: entry.chunkCount,
102
- uploaded_chunk_count: uploadedChunks,
103
- }));
104
- return {
105
- ok: false,
106
- outcome: failedOutcome(entry.file, `chunk_${index}_failed_http_${chunkResponse.status}${chunkDetail ? `_${chunkDetail}` : ""}`, uploadedChunks),
107
- };
108
- }
59
+ const failure = await uploadChunkThroughEndpoint(options, entry, disposition, objectKey, index, chunk, uploadedChunks, Boolean(offer));
60
+ if (failure)
61
+ return failure;
109
62
  uploadedChunks += 1;
110
63
  }
111
64
  return { ok: true, uploadedChunks };
112
65
  }
66
+ async function uploadChunkThroughEndpoint(options, entry, disposition, objectKey, index, chunk, uploadedChunks, hadSignedOffer) {
67
+ console.error("[evidence-upload] using chunk route", JSON.stringify({
68
+ reason: hadSignedOffer ? "signed_upload_refused_fallback" : "signed_upload_not_offered",
69
+ upload_id: disposition.upload_id, chunk_index: index,
70
+ }));
71
+ const encodedChunk = gzipSync(chunk);
72
+ console.error("[evidence-upload] chunk encoded", JSON.stringify({
73
+ upload_id: disposition.upload_id, chunk_index: index, chunk_encoding: "gzip",
74
+ bytes_before: chunk.length, bytes_after: encodedChunk.length,
75
+ }));
76
+ const chunkResponse = await requestJson(options, "/api/ambient/evidence/upload/chunk", {
77
+ schema_version: "ambient-raw-evidence-upload-chunk.v2",
78
+ encoding: "gzip",
79
+ generated_at: options.generatedAt,
80
+ provenance: options.provenance,
81
+ upload_id: disposition.upload_id,
82
+ object_key: objectKey,
83
+ chunk_index: index,
84
+ chunk_count: entry.chunkCount,
85
+ chunk_hash_sha256: sha256(chunk),
86
+ content_base64: encodedChunk.toString("base64"),
87
+ });
88
+ if (!chunkResponse.ok)
89
+ return rejectChunkResponse(chunkResponse, entry, disposition, index, uploadedChunks);
90
+ return null;
91
+ }
92
+ function rejectChunkResponse(chunkResponse, entry, disposition, index, uploadedChunks) {
93
+ // Same treatment the commit path has had since BLI-2528: the server's
94
+ // own reason rides on the label, so `chunk_3_failed_http_413` becomes
95
+ // `chunk_3_failed_http_413_object_too_large` and the ledger row names
96
+ // the cause instead of the transport (BLI-3483).
97
+ const chunkDetail = safeFailureDetail(chunkResponse.body);
98
+ if (chunkResponse.status === 409 && chunkDetail === "upload_not_pending:committed") {
99
+ console.error("[evidence-upload] committed receipt reconciled", JSON.stringify({
100
+ reason: "reconciled_committed_by_409", upload_id: disposition.upload_id,
101
+ }));
102
+ return { ok: false, outcome: {
103
+ ...failedOutcome(entry.file, "reconciled_committed_by_409", uploadedChunks),
104
+ upload_state: "reused_existing",
105
+ } };
106
+ }
107
+ console.error("[evidence-upload] chunk rejected", JSON.stringify({
108
+ reason: "chunk_rejected",
109
+ upload_id: disposition.upload_id,
110
+ http_status: chunkResponse.status,
111
+ server_reason: chunkDetail ?? "none",
112
+ chunk_index: index,
113
+ chunk_count: entry.chunkCount,
114
+ uploaded_chunk_count: uploadedChunks,
115
+ }));
116
+ return {
117
+ ok: false,
118
+ outcome: failedOutcome(entry.file, `chunk_${index}_failed_http_${chunkResponse.status}${chunkDetail ? `_${chunkDetail}` : ""}`, uploadedChunks),
119
+ };
120
+ }
113
121
  async function commitUploadedObject(options, entry, disposition, objectKey, uploadedChunks, directChunks) {
114
122
  const commit = await requestJson(options, "/api/ambient/evidence/upload/commit", {
115
123
  schema_version: "ambient-raw-evidence-upload-commit.v1",
@@ -119,45 +127,9 @@ async function commitUploadedObject(options, entry, disposition, objectKey, uplo
119
127
  object_key: objectKey,
120
128
  ...(disposition.signed_uploads ? { direct_chunks: directChunks } : {}),
121
129
  });
122
- if (!commit.ok) {
123
- // The server can tell us this object will be refused again. Take its reason
124
- // verbatim so the label names the cause ("storage rejected 116 MB") instead
125
- // of the transport ("commit_failed_http_500"), which is all the fleet could
126
- // say for the 57 days of BLI-2528.
127
- const permanent = permanentCommitRejection(commit.body);
128
- if (permanent) {
129
- return failedOutcome(entry.file, permanent, uploadedChunks);
130
- }
131
- // A 5xx whose body is not JSON did not come from the route. The commit
132
- // handler always answers `{ code, message, ... }`; an HTML error page means
133
- // the serverless process was killed — an out-of-memory on a large
134
- // assembly, or a hard timeout — so nothing server-side ran a catch, wrote a
135
- // ledger reason, or logged a line. Naming it separately is the only way an
136
- // operator reading upload reasons can tell "the server refused these bytes"
137
- // from "the server never survived them".
138
- //
139
- // The stem comes from telemetry-core so the label the collector writes and
140
- // the label `classifyUploadFailure` reads are the same string by
141
- // construction; the status rides on the end so a 502 gateway timeout can
142
- // still be told from a 500 process kill, and core strips it back off.
143
- if (commit.status >= 500 && isNonJsonResponseBody(commit.body)) {
144
- console.error("[evidence-commit] the server died before it could answer; the object is still pending", JSON.stringify({
145
- upload_id: disposition.upload_id,
146
- http_status: commit.status,
147
- byte_size: entry.bytes.byteLength,
148
- chunk_count: entry.chunkCount,
149
- uploaded_chunk_count: uploadedChunks,
150
- reason: COMMIT_CRASHED_PLATFORM,
151
- }));
152
- return failedOutcome(entry.file, `${COMMIT_CRASHED_PLATFORM}_http_${commit.status}`, uploadedChunks);
153
- }
154
- const detail = safeFailureDetail(commit.body);
155
- return failedOutcome(entry.file, `commit_failed_http_${commit.status}${detail ? `_${detail}` : ""}`, uploadedChunks);
156
- }
157
- // "already_committed" means a concurrent or earlier sync made these bytes
158
- // durable; this sync does not own them, so they must be reported as reuse —
159
- // a later ingest failure here must not clean up an object another sync's
160
- // indexed refs already point at.
130
+ if (!commit.ok)
131
+ return rejectCommit(commit, entry, disposition, uploadedChunks);
132
+ // Reused evidence belongs to another sync; later failures must not delete it.
161
133
  const parsedCommit = RawEvidenceUploadCommitResponseSchema.safeParse(commit.body);
162
134
  if (!parsedCommit.success) {
163
135
  return failedOutcome(entry.file, "commit_invalid_response", uploadedChunks);
@@ -189,6 +161,31 @@ async function commitUploadedObject(options, entry, disposition, objectKey, uplo
189
161
  uploaded_chunk_count: uploadedChunks,
190
162
  };
191
163
  }
164
+ function rejectCommit(commit, entry, disposition, uploadedChunks) {
165
+ // The server can tell us this object will be refused again. Take its reason
166
+ // verbatim so the label names the cause ("storage rejected 116 MB") instead
167
+ // of the transport ("commit_failed_http_500"), which is all the fleet could
168
+ // say for the 57 days of BLI-2528.
169
+ const permanent = permanentCommitRejection(commit.body);
170
+ if (permanent) {
171
+ return failedOutcome(entry.file, permanent, uploadedChunks);
172
+ }
173
+ // The route always answers JSON. A non-JSON 5xx is a platform failure;
174
+ // keep the shared reason stem and HTTP status so retries stay classifiable.
175
+ if (commit.status >= 500 && isNonJsonResponseBody(commit.body)) {
176
+ console.error("[evidence-commit] the server died before it could answer; the object is still pending", JSON.stringify({
177
+ upload_id: disposition.upload_id,
178
+ http_status: commit.status,
179
+ byte_size: entry.bytes.byteLength,
180
+ chunk_count: entry.chunkCount,
181
+ uploaded_chunk_count: uploadedChunks,
182
+ reason: COMMIT_CRASHED_PLATFORM,
183
+ }));
184
+ return failedOutcome(entry.file, `${COMMIT_CRASHED_PLATFORM}_http_${commit.status}`, uploadedChunks);
185
+ }
186
+ const detail = safeFailureDetail(commit.body);
187
+ return failedOutcome(entry.file, `commit_failed_http_${commit.status}${detail ? `_${detail}` : ""}`, uploadedChunks);
188
+ }
192
189
  /**
193
190
  * The server's verdict that repeating this commit is pointless, or null.
194
191
  *