@bli-cockpit/cli 0.2.54 → 0.2.56
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.
- package/dist/adapters/attribution-core-fallbacks.js +247 -0
- package/dist/adapters/attribution-core-paths.js +182 -0
- package/dist/adapters/attribution-core-score.js +159 -0
- package/dist/adapters/attribution-core-types.js +13 -0
- package/dist/adapters/attribution-core.js +13 -565
- package/dist/adapters/claude-attribution-discovery.js +186 -0
- package/dist/adapters/claude-attribution-score.js +204 -0
- package/dist/adapters/claude-attribution-signals.js +180 -0
- package/dist/adapters/claude-attribution-types.js +25 -0
- package/dist/adapters/claude-attribution.js +14 -569
- package/dist/commands/doctor-access.js +129 -0
- package/dist/commands/doctor-pipeline.js +326 -0
- package/dist/commands/doctor-registration.js +105 -0
- package/dist/commands/doctor-report.js +111 -0
- package/dist/commands/doctor-update.js +120 -0
- package/dist/commands/doctor.js +8 -753
- package/dist/commands/heartbeat.js +8 -0
- package/dist/commands/jarvis-contracts.js +8 -0
- package/dist/commands/jarvis-render.js +413 -0
- package/dist/commands/jarvis-turn.js +305 -0
- package/dist/commands/jarvis.js +23 -698
- package/dist/commands/local-args-collector-setup.js +250 -0
- package/dist/commands/local-args-collector-status.js +227 -0
- package/dist/commands/local-args-collector-work.js +175 -0
- package/dist/commands/local-args-collector.js +19 -624
- package/dist/commands/local-args-tower-admin.js +456 -0
- package/dist/commands/local-args-tower-chat.js +194 -0
- package/dist/commands/local-args-tower-pages.js +314 -0
- package/dist/commands/local-args-tower.js +13 -880
- package/dist/commands/local-help.js +10 -2
- package/dist/commands/onboard-completion.js +136 -0
- package/dist/commands/onboard-flows.js +165 -0
- package/dist/commands/onboard-setup.js +102 -0
- package/dist/commands/onboard.js +5 -392
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/session-sync-counters.js +55 -0
- package/dist/commands/session-sync-health.js +8 -1
- package/dist/commands/session-sync-plan.js +47 -7
- package/dist/commands/session-sync-scan.js +4 -4
- package/dist/commands/session-sync.js +6 -0
- package/dist/commands/settings-render.js +27 -0
- package/dist/commands/sync-followups.js +5 -1
- package/dist/commands/sync.js +5 -1
- package/dist/commands/team-device-reasons.js +16 -0
- package/dist/commands/team.js +87 -7
- package/dist/evidence-upload-client.js +14 -763
- package/dist/evidence-upload-object.js +181 -0
- package/dist/evidence-upload-plan.js +233 -0
- package/dist/evidence-upload-terminal.js +309 -0
- package/dist/evidence-upload-transport.js +104 -0
- package/dist/spool/local-spool-io.js +122 -0
- package/dist/spool/local-spool-mutations.js +174 -0
- package/dist/spool/local-spool-parse.js +143 -0
- package/dist/spool/local-spool-types.js +22 -0
- package/dist/spool/local-spool.js +20 -426
- package/dist/upload-evidence-delivery-offer.js +144 -0
- package/dist/upload-evidence-delivery-reconcile.js +134 -0
- package/dist/upload-evidence-delivery-summary.js +205 -0
- package/dist/upload-evidence-delivery.js +12 -482
- package/package.json +3 -3
|
@@ -20,486 +20,16 @@
|
|
|
20
20
|
* own terms. Confusing them is what kept a Mac in `retry_pending` through 13
|
|
21
21
|
* identical syncs (BLI-2528), and what let a transcript sit undelivered for nine
|
|
22
22
|
* days while the sync read clean (BLI-3066).
|
|
23
|
-
*/
|
|
24
|
-
import { EvidenceCompletenessPayloadSchema, isPermanentUploadFailure, } from "@bli-cockpit/telemetry-core";
|
|
25
|
-
import { clearDeliveryAttempt, DELIVERY_BACKOFF_BYPASS_REASON, DELIVERY_BACKOFF_HOLDING_REASON, deliveryBackoffApplies, deliveryHold, recordDeliveryFailure, summarizeStuckEvidence, writeRawEvidenceStagingState, } from "./raw-evidence-staging.js";
|
|
26
|
-
// --------------------------------------------------------------------------
|
|
27
|
-
// 1. What may we offer this sync?
|
|
28
|
-
// --------------------------------------------------------------------------
|
|
29
|
-
/**
|
|
30
|
-
* Split the pack's files into "offer these now" and "still in backoff".
|
|
31
|
-
*
|
|
32
|
-
* A held file becomes an `upload_failed` outcome labelled
|
|
33
|
-
* `delivery_backoff_holding`. That is deliberate rather than a quiet omission:
|
|
34
|
-
* the pointer gets pruned from the envelope (the object is genuinely not
|
|
35
|
-
* durable), the sync stays in `retry_pending`, and the reason travels to the
|
|
36
|
-
* status output. A hold that read as success would be the green-status-hiding-
|
|
37
|
-
* missing-collection failure the fleet contract forbids.
|
|
38
|
-
*/
|
|
39
|
-
export function partitionHeldEvidenceFiles(files, staging, now, mode) {
|
|
40
|
-
const deliverable = [];
|
|
41
|
-
const held = [];
|
|
42
|
-
const bypassed = [];
|
|
43
|
-
const backoffApplies = deliveryBackoffApplies(mode);
|
|
44
|
-
for (const file of files) {
|
|
45
|
-
const hold = deliveryHold(staging, file.pointer.content_hash_sha256, now);
|
|
46
|
-
if (hold && !backoffApplies) {
|
|
47
|
-
// BLI-3118: a person asked for this one now. Offering it is the whole
|
|
48
|
-
// point of the retry command Cockpit printed, and the bypass is logged
|
|
49
|
-
// rather than assumed.
|
|
50
|
-
bypassed.push(hold);
|
|
51
|
-
deliverable.push(file);
|
|
52
|
-
continue;
|
|
53
|
-
}
|
|
54
|
-
if (!hold) {
|
|
55
|
-
deliverable.push(file);
|
|
56
|
-
continue;
|
|
57
|
-
}
|
|
58
|
-
held.push({
|
|
59
|
-
pointer: file.pointer,
|
|
60
|
-
object_key: file.pointer.object_key ?? "",
|
|
61
|
-
codex_session_id: file.codex_session_id ?? null,
|
|
62
|
-
kind: file.kind ?? "raw_evidence",
|
|
63
|
-
...(file.artifact_metadata
|
|
64
|
-
? { artifact_metadata: file.artifact_metadata }
|
|
65
|
-
: {}),
|
|
66
|
-
upload_state: "upload_failed",
|
|
67
|
-
reason: DELIVERY_BACKOFF_HOLDING_REASON,
|
|
68
|
-
uploaded_chunk_count: 0,
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
return { deliverable, held, bypassed };
|
|
72
|
-
}
|
|
73
|
-
/**
|
|
74
|
-
* Say out loud that bytes were withheld on purpose.
|
|
75
23
|
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
/**
|
|
90
|
-
* Say out loud that an operator's retry ignored a live backoff window.
|
|
91
|
-
*
|
|
92
|
-
* The success branch of BLI-3118: without this line the only trace of the
|
|
93
|
-
* decision is an object that was held on one run and offered on the next, and
|
|
94
|
-
* nothing on the machine says which rule made the difference.
|
|
95
|
-
*/
|
|
96
|
-
export function logEvidenceBackoffBypassed(bypassed, attemptedAt) {
|
|
97
|
-
if (bypassed.length === 0)
|
|
98
|
-
return;
|
|
99
|
-
console.error("[cockpit-sync] raw evidence delivery backoff bypassed", JSON.stringify({
|
|
100
|
-
attempted_at: attemptedAt,
|
|
101
|
-
reason: DELIVERY_BACKOFF_BYPASS_REASON,
|
|
102
|
-
object_count: bypassed.length,
|
|
103
|
-
byte_size: bypassed.reduce((sum, entry) => sum + entry.byte_size, 0),
|
|
104
|
-
max_attempts: bypassed.reduce((max, entry) => Math.max(max, entry.attempts), 0),
|
|
105
|
-
last_reasons: [...new Set(bypassed.map((entry) => entry.last_reason))]
|
|
106
|
-
.sort(),
|
|
107
|
-
}));
|
|
108
|
-
}
|
|
109
|
-
// --------------------------------------------------------------------------
|
|
110
|
-
// 2. What just happened to each object?
|
|
111
|
-
// --------------------------------------------------------------------------
|
|
112
|
-
/**
|
|
113
|
-
* Count what just happened to each object, and when it may be offered again.
|
|
114
|
-
*
|
|
115
|
-
* Written immediately after the upload pass and before ingest, so an ingest
|
|
116
|
-
* failure cannot lose the attempt counts — losing them resets every backoff to
|
|
117
|
-
* zero and the fleet is back to 15-minute retries forever. A held outcome is
|
|
118
|
-
* not itself an attempt: counting it would push its own next attempt further
|
|
119
|
-
* out on every sync and eventually never retry at all.
|
|
120
|
-
*/
|
|
121
|
-
export async function persistDeliveryAttempts(stateDir, staging, outcomes, attemptedAt) {
|
|
122
|
-
let changed = false;
|
|
123
|
-
for (const outcome of outcomes) {
|
|
124
|
-
const contentHash = outcome.pointer.content_hash_sha256;
|
|
125
|
-
if (!contentHash)
|
|
126
|
-
continue;
|
|
127
|
-
if (outcome.reason === DELIVERY_BACKOFF_HOLDING_REASON)
|
|
128
|
-
continue;
|
|
129
|
-
if (outcome.upload_state === "upload_failed") {
|
|
130
|
-
const entry = recordDeliveryFailure(staging, contentHash, {
|
|
131
|
-
reason: outcome.reason ?? "upload_failed",
|
|
132
|
-
attemptedAt,
|
|
133
|
-
byteSize: outcome.pointer.byte_size ?? 0,
|
|
134
|
-
});
|
|
135
|
-
changed = true;
|
|
136
|
-
console.error("[cockpit-sync] raw evidence delivery failed", JSON.stringify({
|
|
137
|
-
reason: entry.last_reason,
|
|
138
|
-
kind: outcome.kind,
|
|
139
|
-
attempts: entry.attempts,
|
|
140
|
-
first_failed_at: entry.first_failed_at,
|
|
141
|
-
next_attempt_at: entry.next_attempt_at,
|
|
142
|
-
byte_size: entry.byte_size,
|
|
143
|
-
}));
|
|
144
|
-
continue;
|
|
145
|
-
}
|
|
146
|
-
if (clearDeliveryAttempt(staging, contentHash)) {
|
|
147
|
-
changed = true;
|
|
148
|
-
console.error("[cockpit-sync] raw evidence delivery recovered", JSON.stringify({
|
|
149
|
-
reason: "delivery_recovered",
|
|
150
|
-
kind: outcome.kind,
|
|
151
|
-
upload_state: outcome.upload_state,
|
|
152
|
-
}));
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
if (!changed)
|
|
156
|
-
return;
|
|
157
|
-
staging.updated_at = attemptedAt.toISOString();
|
|
158
|
-
await writeRawEvidenceStagingState(stateDir, staging).catch((error) => {
|
|
159
|
-
console.error("[cockpit-sync] delivery attempt state write failed", JSON.stringify({
|
|
160
|
-
reason: "staging_state_write_failed",
|
|
161
|
-
detail: error instanceof Error ? error.name : typeof error,
|
|
162
|
-
tracked_count: Object.keys(staging.delivery_attempts).length,
|
|
163
|
-
}));
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
// --------------------------------------------------------------------------
|
|
167
|
-
// 3. What do we still owe the operator?
|
|
168
|
-
// --------------------------------------------------------------------------
|
|
169
|
-
/**
|
|
170
|
-
* Which adapters a queued retry has to re-run.
|
|
171
|
-
*
|
|
172
|
-
* Anything this sync touched or tried to touch counts, including a directory it
|
|
173
|
-
* could not read — a source that failed to scan is exactly the one a retry must
|
|
174
|
-
* come back to.
|
|
175
|
-
*/
|
|
176
|
-
export function retrySourcesForFailedSync(options, facts) {
|
|
177
|
-
const sources = new Set();
|
|
178
|
-
if ((options.codexSessionFiles?.length ?? 0) > 0 ||
|
|
179
|
-
(options.codexAttributionScan?.directory_read_failed_count ?? 0) > 0 ||
|
|
180
|
-
(options.codexAttributionScan?.stat_failed_count ?? 0) > 0 ||
|
|
181
|
-
facts?.evidence_completeness.source_counts.some((count) => count.source.startsWith("codex_") &&
|
|
182
|
-
count.scanned_count + count.included_count + count.reused_count > 0)) {
|
|
183
|
-
sources.add("codex");
|
|
184
|
-
}
|
|
185
|
-
if ((options.claudeSessionFiles?.length ?? 0) > 0 ||
|
|
186
|
-
(options.claudeAttributionScan?.project_dir_read_failed_count ?? 0) > 0 ||
|
|
187
|
-
(options.claudeAttributionScan?.session_stat_failed_count ?? 0) > 0 ||
|
|
188
|
-
(options.claudeAttributionScan?.sidecar_dir_read_failed_count ?? 0) > 0 ||
|
|
189
|
-
(options.claudeAttributionScan?.sidecar_stat_failed_count ?? 0) > 0 ||
|
|
190
|
-
facts?.evidence_completeness.source_counts.some((count) => count.source.startsWith("claude_") &&
|
|
191
|
-
count.scanned_count + count.included_count + count.reused_count > 0)) {
|
|
192
|
-
sources.add("claude_code");
|
|
193
|
-
}
|
|
194
|
-
return [...sources];
|
|
195
|
-
}
|
|
196
|
-
/**
|
|
197
|
-
* The failed uploads a later attempt could still rescue.
|
|
198
|
-
*
|
|
199
|
-
* An object storage has already refused on its own terms is not one of them,
|
|
200
|
-
* and counting it as one is what kept Edward's Mac in `retry_pending` through
|
|
201
|
-
* 13 consecutive syncs that were never going to end differently (BLI-2528).
|
|
202
|
-
*/
|
|
203
|
-
function retryableFailedOutcomes(outcomes) {
|
|
204
|
-
return outcomes.filter((outcome) => outcome.upload_state === "upload_failed" &&
|
|
205
|
-
!isPermanentUploadFailure(outcome.reason));
|
|
206
|
-
}
|
|
207
|
-
/** Failed uploads that no retry can rescue, kept so they can still be named. */
|
|
208
|
-
function permanentFailedOutcomes(outcomes) {
|
|
209
|
-
return outcomes.filter((outcome) => outcome.upload_state === "upload_failed" &&
|
|
210
|
-
isPermanentUploadFailure(outcome.reason));
|
|
211
|
-
}
|
|
212
|
-
/**
|
|
213
|
-
* The reason to show for objects that failed for good.
|
|
214
|
-
*
|
|
215
|
-
* Returns null when there are none. These never queue a retry, but they must
|
|
216
|
-
* never disappear either: a machine with a permanently rejected object has
|
|
217
|
-
* missing collection, and a status that reads clean would hide it.
|
|
218
|
-
*/
|
|
219
|
-
export function permanentEvidenceFailureReason(outcomes) {
|
|
220
|
-
const reasons = new Set(permanentFailedOutcomes(outcomes).map((outcome) => outcome.reason ?? "unknown"));
|
|
221
|
-
if (reasons.size === 0)
|
|
222
|
-
return null;
|
|
223
|
-
return `raw_evidence_permanently_rejected:${[...reasons].sort().join(",")}`;
|
|
224
|
-
}
|
|
225
|
-
/**
|
|
226
|
-
* Leave a dated record of the objects this machine gave up on.
|
|
227
|
-
*
|
|
228
|
-
* stderr, which launchd captures to `sync.err.log`, so an unattended machine
|
|
229
|
-
* still says what it lost. Reason labels and counts only — never a path or a
|
|
230
|
-
* byte of content.
|
|
231
|
-
*/
|
|
232
|
-
export function logPermanentlyRejectedEvidence(options) {
|
|
233
|
-
console.error("[cockpit-sync] raw evidence permanently rejected", JSON.stringify({
|
|
234
|
-
attempted_at: options.attemptedAt,
|
|
235
|
-
reason: options.reason,
|
|
236
|
-
object_count: permanentFailedOutcomes(options.outcomes).length,
|
|
237
|
-
}));
|
|
238
|
-
}
|
|
239
|
-
/**
|
|
240
|
-
* Is there anything a later sync could still turn into evidence?
|
|
241
|
-
*
|
|
242
|
-
* Any one of these is enough: an upload that failed for a rescuable reason,
|
|
243
|
-
* evidence deferred by this sync's byte or object budget, a completeness
|
|
244
|
-
* payload that reports failure, or a skip a retry could undo.
|
|
245
|
-
*/
|
|
246
|
-
export function hasRetryableEvidenceGap(facts, outcomes) {
|
|
247
|
-
if (retryableFailedOutcomes(outcomes).length > 0) {
|
|
248
|
-
return true;
|
|
249
|
-
}
|
|
250
|
-
if (!facts)
|
|
251
|
-
return false;
|
|
252
|
-
if (facts.deferred_byte_budget_count > 0 ||
|
|
253
|
-
facts.deferred_object_budget_count > 0) {
|
|
254
|
-
return true;
|
|
255
|
-
}
|
|
256
|
-
if (facts.evidence_completeness.status === "failed" ||
|
|
257
|
-
facts.evidence_completeness.totals.failed_count > 0 ||
|
|
258
|
-
facts.evidence_completeness.failure_reasons.length > 0) {
|
|
259
|
-
return true;
|
|
260
|
-
}
|
|
261
|
-
return facts.evidence_completeness.skip_reasons.some(({ reason }) => isRetryableEvidenceSkipReason(reason));
|
|
262
|
-
}
|
|
263
|
-
/**
|
|
264
|
-
* Skip reasons a later sync can still turn into evidence.
|
|
265
|
-
*
|
|
266
|
-
* `delivery_backoff_holding` is one of them, and it has to be: a source held by
|
|
267
|
-
* backoff is missing collection right now. If it did not land here the sync
|
|
268
|
-
* would read clean while a transcript sat undelivered for nine days, which is
|
|
269
|
-
* the exact shape of BLI-3066.
|
|
270
|
-
*/
|
|
271
|
-
function isRetryableEvidenceSkipReason(reason) {
|
|
272
|
-
if (reason === DELIVERY_BACKOFF_HOLDING_REASON)
|
|
273
|
-
return true;
|
|
274
|
-
return /(?:read|stat|directory)_failed|session_limit_overflow/iu.test(reason);
|
|
275
|
-
}
|
|
276
|
-
/** Every distinct reason behind the gap, sorted, as one spool-ready label. */
|
|
277
|
-
export function retryableEvidenceGapReason(facts, outcomes) {
|
|
278
|
-
const reasons = new Set();
|
|
279
|
-
for (const outcome of retryableFailedOutcomes(outcomes)) {
|
|
280
|
-
reasons.add(outcome.reason ?? "upload_failed");
|
|
281
|
-
}
|
|
282
|
-
if (facts) {
|
|
283
|
-
if (facts.deferred_byte_budget_count > 0)
|
|
284
|
-
reasons.add("deferred_byte_budget");
|
|
285
|
-
if (facts.deferred_object_budget_count > 0) {
|
|
286
|
-
reasons.add("deferred_object_budget");
|
|
287
|
-
}
|
|
288
|
-
for (const { reason } of facts.evidence_completeness.failure_reasons) {
|
|
289
|
-
reasons.add(reason);
|
|
290
|
-
}
|
|
291
|
-
if (facts.evidence_completeness.status === "failed" &&
|
|
292
|
-
facts.evidence_completeness.failure_reasons.length === 0) {
|
|
293
|
-
reasons.add("evidence_completeness_failed");
|
|
294
|
-
}
|
|
295
|
-
for (const { reason } of facts.evidence_completeness.skip_reasons) {
|
|
296
|
-
if (isRetryableEvidenceSkipReason(reason)) {
|
|
297
|
-
reasons.add(reason);
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
return `partial_raw_evidence_retry_required:${[
|
|
302
|
-
...reasons,
|
|
303
|
-
].sort().join(",") || "unknown"}`;
|
|
304
|
-
}
|
|
305
|
-
/**
|
|
306
|
-
* The counts `cockpit sync --json` and `cockpit status` read.
|
|
307
|
-
*
|
|
308
|
-
* Two kinds of reuse are added together on purpose: files this machine skipped
|
|
309
|
-
* because its own cursor already had the bytes, and files the server recognised
|
|
310
|
-
* from their content hash.
|
|
311
|
-
*/
|
|
312
|
-
export function summarizeRawEvidenceDelivery(built, outcomes, uploadedChunkCount, cursor, staging, now) {
|
|
313
|
-
const stuck = summarizeStuckEvidence(staging, now);
|
|
314
|
-
const cursorReused = built.raw_evidence_facts?.reused ?? [];
|
|
315
|
-
const serverReusedCount = outcomes.filter((outcome) => outcome.upload_state === "reused_existing").length;
|
|
316
|
-
const failed = outcomes.filter((outcome) => outcome.upload_state === "upload_failed");
|
|
317
|
-
const retryRequired = hasRetryableEvidenceGap(built.raw_evidence_facts, outcomes);
|
|
318
|
-
const retryReason = retryRequired
|
|
319
|
-
? retryableEvidenceGapReason(built.raw_evidence_facts, outcomes)
|
|
320
|
-
: null;
|
|
321
|
-
const cursorReusedOutcomes = cursorReused.map((entry) => ({
|
|
322
|
-
object_key: cursor.objects[entry.content_hash_sha256]?.object_key ?? "",
|
|
323
|
-
raw_evidence_pointer_id: cursor.objects[entry.content_hash_sha256]?.object_key ?? "",
|
|
324
|
-
kind: entry.kind,
|
|
325
|
-
codex_session_id: entry.codex_session_id,
|
|
326
|
-
...(entry.artifact_metadata
|
|
327
|
-
? { artifact_metadata: entry.artifact_metadata }
|
|
328
|
-
: {}),
|
|
329
|
-
upload_state: "reused_existing",
|
|
330
|
-
reason: "cursor_content_match",
|
|
331
|
-
}));
|
|
332
|
-
return {
|
|
333
|
-
raw_evidence_file_count: built.raw_evidence_upload_files.length,
|
|
334
|
-
raw_evidence_uploaded_object_count: outcomes.filter((outcome) => outcome.upload_state === "uploaded").length,
|
|
335
|
-
raw_evidence_uploaded_chunk_count: uploadedChunkCount,
|
|
336
|
-
raw_evidence_reused_count: cursorReused.length + serverReusedCount,
|
|
337
|
-
raw_evidence_failed_count: failed.length,
|
|
338
|
-
raw_evidence_sanitized_count: built.raw_evidence_facts?.sanitized_count ?? 0,
|
|
339
|
-
raw_evidence_failure_reasons: [
|
|
340
|
-
...new Set(failed.map((outcome) => outcome.reason ?? "unknown")),
|
|
341
|
-
],
|
|
342
|
-
raw_evidence_retry_required: retryRequired,
|
|
343
|
-
raw_evidence_retry_reasons: retryReason
|
|
344
|
-
? retryReason
|
|
345
|
-
.replace(/^partial_raw_evidence_retry_required:/u, "")
|
|
346
|
-
.split(",")
|
|
347
|
-
.filter(Boolean)
|
|
348
|
-
: [],
|
|
349
|
-
raw_evidence_outcomes: [
|
|
350
|
-
...outcomes.map((outcome) => ({
|
|
351
|
-
object_key: outcome.object_key,
|
|
352
|
-
raw_evidence_pointer_id: outcome.pointer.raw_evidence_pointer_id,
|
|
353
|
-
kind: outcome.kind,
|
|
354
|
-
codex_session_id: outcome.codex_session_id,
|
|
355
|
-
...(outcome.artifact_metadata
|
|
356
|
-
? { artifact_metadata: outcome.artifact_metadata }
|
|
357
|
-
: {}),
|
|
358
|
-
upload_state: outcome.upload_state,
|
|
359
|
-
reason: outcome.reason,
|
|
360
|
-
})),
|
|
361
|
-
...cursorReusedOutcomes,
|
|
362
|
-
],
|
|
363
|
-
raw_evidence_deferred_byte_budget: built.raw_evidence_facts?.deferred_byte_budget_count ?? 0,
|
|
364
|
-
raw_evidence_deferred_object_budget: built.raw_evidence_facts?.deferred_object_budget_count ?? 0,
|
|
365
|
-
cursor_tracked_object_count: Object.keys(cursor.objects).length,
|
|
366
|
-
raw_evidence_delivery_held_count: outcomes.filter((outcome) => outcome.reason === DELIVERY_BACKOFF_HOLDING_REASON).length + (built.raw_evidence_facts?.delivery_held_count ?? 0),
|
|
367
|
-
raw_evidence_stuck_object_count: stuck.stuck_object_count,
|
|
368
|
-
raw_evidence_max_delivery_attempts: stuck.max_attempts,
|
|
369
|
-
raw_evidence_oldest_delivery_failure_at: stuck.oldest_first_failed_at,
|
|
370
|
-
};
|
|
371
|
-
}
|
|
372
|
-
// --------------------------------------------------------------------------
|
|
373
|
-
// 4. Reconciling the envelope with what actually became durable
|
|
374
|
-
// --------------------------------------------------------------------------
|
|
375
|
-
/**
|
|
376
|
-
* Ingest refuses pointers whose objects never became durable, so failed
|
|
377
|
-
* uploads are pruned from the envelope instead of failing the whole sync.
|
|
378
|
-
* Successful upload responses can also carry server-side sanitized hash and
|
|
379
|
-
* redaction metadata; apply those before ingest so refs describe the bytes
|
|
380
|
-
* actually stored in the durable bucket.
|
|
381
|
-
*/
|
|
382
|
-
export function applyRawEvidenceUploadOutcomes(envelope, outcomes) {
|
|
383
|
-
// Content-addressed keys mean one pointer id can carry several outcomes
|
|
384
|
-
// (byte-identical files); the pointer is durable if ANY outcome succeeded.
|
|
385
|
-
const durablePointers = new Map();
|
|
386
|
-
for (const outcome of outcomes) {
|
|
387
|
-
if (outcome.upload_state === "upload_failed")
|
|
388
|
-
continue;
|
|
389
|
-
durablePointers.set(outcome.pointer.raw_evidence_pointer_id, outcome.pointer);
|
|
390
|
-
}
|
|
391
|
-
const durablePointerIds = new Set([...durablePointers.keys()]);
|
|
392
|
-
const failedPointerIds = new Set(outcomes
|
|
393
|
-
.filter((outcome) => outcome.upload_state === "upload_failed" &&
|
|
394
|
-
!durablePointerIds.has(outcome.pointer.raw_evidence_pointer_id))
|
|
395
|
-
.map((outcome) => outcome.pointer.raw_evidence_pointer_id));
|
|
396
|
-
const failedOutcomes = outcomes.filter((outcome) => failedPointerIds.has(outcome.pointer.raw_evidence_pointer_id));
|
|
397
|
-
if (failedPointerIds.size === 0 && durablePointers.size === 0)
|
|
398
|
-
return envelope;
|
|
399
|
-
return {
|
|
400
|
-
...envelope,
|
|
401
|
-
events: envelope.events.map((event) => ({
|
|
402
|
-
...event,
|
|
403
|
-
metrics: failedPointerIds.size > 0
|
|
404
|
-
? {
|
|
405
|
-
...event.metrics,
|
|
406
|
-
evidence_failed_count: (event.metrics["evidence_failed_count"] ?? 0) +
|
|
407
|
-
failedOutcomes.length,
|
|
408
|
-
}
|
|
409
|
-
: event.metrics,
|
|
410
|
-
attributes: failedPointerIds.size > 0 && event.evidence_completeness
|
|
411
|
-
? {
|
|
412
|
-
...event.attributes,
|
|
413
|
-
evidence_completeness_schema_version: event.evidence_completeness.schema_version,
|
|
414
|
-
evidence_completeness_status: "partial",
|
|
415
|
-
evidence_incomplete: true,
|
|
416
|
-
}
|
|
417
|
-
: event.attributes,
|
|
418
|
-
evidence_completeness: failedPointerIds.size > 0 && event.evidence_completeness
|
|
419
|
-
? markCompletenessUploadFailures(event.evidence_completeness, failedOutcomes)
|
|
420
|
-
: event.evidence_completeness,
|
|
421
|
-
raw_evidence_pointers: event.raw_evidence_pointers.flatMap((pointer) => {
|
|
422
|
-
const pointerId = pointer.raw_evidence_pointer_id;
|
|
423
|
-
if (failedPointerIds.has(pointerId))
|
|
424
|
-
return [];
|
|
425
|
-
return [durablePointers.get(pointerId) ?? pointer];
|
|
426
|
-
}),
|
|
427
|
-
redaction: failedPointerIds.size > 0
|
|
428
|
-
? {
|
|
429
|
-
...event.redaction,
|
|
430
|
-
raw_evidence_pointer_ids: event.redaction.raw_evidence_pointer_ids.filter((pointerId) => !failedPointerIds.has(pointerId)),
|
|
431
|
-
}
|
|
432
|
-
: event.redaction,
|
|
433
|
-
})),
|
|
434
|
-
};
|
|
435
|
-
}
|
|
436
|
-
/**
|
|
437
|
-
* Fold upload failures back into the completeness payload the event carries, so
|
|
438
|
-
* a downstream reader sees `partial` and the per-source failure counts rather
|
|
439
|
-
* than a complete-looking payload with fewer pointers than it claims.
|
|
440
|
-
*/
|
|
441
|
-
function markCompletenessUploadFailures(completeness, failedOutcomes) {
|
|
442
|
-
const failureCounts = new Map();
|
|
443
|
-
for (const outcome of failedOutcomes) {
|
|
444
|
-
const source = outcome.kind ?? "raw_evidence";
|
|
445
|
-
failureCounts.set(source, (failureCounts.get(source) ?? 0) + 1);
|
|
446
|
-
}
|
|
447
|
-
const totalFailures = [...failureCounts.values()].reduce((sum, count) => sum + count, 0);
|
|
448
|
-
const sourceCounts = [...completeness.source_counts];
|
|
449
|
-
for (const [source, count] of failureCounts) {
|
|
450
|
-
const existingIndex = sourceCounts.findIndex((entry) => entry.source === source);
|
|
451
|
-
if (existingIndex === -1) {
|
|
452
|
-
sourceCounts.push({
|
|
453
|
-
source,
|
|
454
|
-
scanned_count: 0,
|
|
455
|
-
included_count: 0,
|
|
456
|
-
skipped_count: 0,
|
|
457
|
-
truncated_count: 0,
|
|
458
|
-
deferred_count: 0,
|
|
459
|
-
reused_count: 0,
|
|
460
|
-
failed_count: count,
|
|
461
|
-
});
|
|
462
|
-
continue;
|
|
463
|
-
}
|
|
464
|
-
const existing = sourceCounts[existingIndex];
|
|
465
|
-
if (!existing)
|
|
466
|
-
continue;
|
|
467
|
-
sourceCounts[existingIndex] = {
|
|
468
|
-
...existing,
|
|
469
|
-
failed_count: existing.failed_count + count,
|
|
470
|
-
};
|
|
471
|
-
}
|
|
472
|
-
const failureReasons = [...completeness.failure_reasons];
|
|
473
|
-
for (const [source, count] of failureCounts) {
|
|
474
|
-
const reason = "upload_failed";
|
|
475
|
-
const existingIndex = failureReasons.findIndex((entry) => entry.source === source && entry.reason === reason);
|
|
476
|
-
if (existingIndex === -1) {
|
|
477
|
-
failureReasons.push({ source, reason, count });
|
|
478
|
-
}
|
|
479
|
-
else {
|
|
480
|
-
const existing = failureReasons[existingIndex];
|
|
481
|
-
if (existing) {
|
|
482
|
-
failureReasons[existingIndex] = {
|
|
483
|
-
...existing,
|
|
484
|
-
count: existing.count + count,
|
|
485
|
-
};
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
return EvidenceCompletenessPayloadSchema.parse({
|
|
490
|
-
...completeness,
|
|
491
|
-
status: "partial",
|
|
492
|
-
source_counts: sourceCounts,
|
|
493
|
-
totals: {
|
|
494
|
-
...completeness.totals,
|
|
495
|
-
failed_count: completeness.totals.failed_count + totalFailures,
|
|
496
|
-
},
|
|
497
|
-
failure_reasons: failureReasons.sort((a, b) => `${a.source}:${a.reason}`.localeCompare(`${b.source}:${b.reason}`)),
|
|
498
|
-
notes: [
|
|
499
|
-
...new Set([
|
|
500
|
-
...completeness.notes,
|
|
501
|
-
"Some collected evidence did not become durable; downstream analysis should lower confidence.",
|
|
502
|
-
]),
|
|
503
|
-
],
|
|
504
|
-
});
|
|
505
|
-
}
|
|
24
|
+
* This file is the table of contents; every responsibility above is a
|
|
25
|
+
* `upload-evidence-delivery-*.ts` sibling and is re-exported here so the
|
|
26
|
+
* module's public surface never moves (BLI-3637):
|
|
27
|
+
* -offer questions 1 and 2 — what may we offer, what just happened
|
|
28
|
+
* -summary question 3 — retry sources, retryable/permanent reasons, and
|
|
29
|
+
* the counts `cockpit status` reads
|
|
30
|
+
* -reconcile question 4 — folding upload outcomes back into the envelope
|
|
31
|
+
* ingest actually sees
|
|
32
|
+
*/
|
|
33
|
+
export { logEvidenceBackoffBypassed, logEvidenceHeldByBackoff, partitionHeldEvidenceFiles, persistDeliveryAttempts, } from "./upload-evidence-delivery-offer.js";
|
|
34
|
+
export { hasRetryableEvidenceGap, logPermanentlyRejectedEvidence, permanentEvidenceFailureReason, retrySourcesForFailedSync, retryableEvidenceGapReason, summarizeRawEvidenceDelivery, } from "./upload-evidence-delivery-summary.js";
|
|
35
|
+
export { applyRawEvidenceUploadOutcomes } from "./upload-evidence-delivery-reconcile.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bli-cockpit/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.56",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"test": "node dist/cli.js --help && node ../../scripts/assert-public-cli-routing.mjs && node ../../scripts/assert-public-cli-runtime-files.mjs && node ../../scripts/assert-public-cli-no-fleet-posts.mjs && node ../../scripts/assert-public-package-pack.mjs --workspace=@bli-cockpit/cli"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@bli-cockpit/memory-mcp": "0.1.
|
|
31
|
-
"@bli-cockpit/telemetry-core": "0.1.
|
|
30
|
+
"@bli-cockpit/memory-mcp": "0.1.4",
|
|
31
|
+
"@bli-cockpit/telemetry-core": "0.1.28"
|
|
32
32
|
}
|
|
33
33
|
}
|