@bli-cockpit/cli 0.2.237 → 0.2.239
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 +15 -11
- package/dist/adapters/attribution-core-score.js +111 -87
- package/dist/adapters/car-state.js +38 -35
- package/dist/adapters/claude-attribution-discovery.js +81 -56
- package/dist/adapters/claude-attribution-score.js +108 -94
- package/dist/adapters/claude-attribution-signals.js +52 -42
- package/dist/adapters/codex-attribution.js +42 -33
- package/dist/adapters/codex-session-signals.js +11 -9
- package/dist/adapters/git-state.js +56 -50
- package/dist/adapters/local-sources.js +52 -43
- package/dist/adapters/raw-evidence-git-diff.js +4 -2
- package/dist/adapters/raw-evidence-sanitize.js +15 -12
- package/dist/adapters/raw-evidence-transcript-file.js +32 -31
- package/dist/autostart-self-heal.js +18 -2
- package/dist/commands/agent-session-report.js +51 -59
- package/dist/commands/agent-setup-observed.js +30 -23
- package/dist/commands/agent-setup.js +96 -45
- package/dist/commands/autostart-heal.js +8 -2
- package/dist/commands/autostart-reading.js +3 -0
- package/dist/commands/backfill-evidence-outcomes.js +9 -6
- package/dist/commands/backfill-issues.js +3 -0
- package/dist/commands/backfill-scan.js +27 -22
- package/dist/commands/backfill-upload.js +69 -60
- package/dist/commands/backfill.js +26 -21
- package/dist/commands/brief-done.js +4 -1
- package/dist/commands/brief-edit.js +71 -46
- package/dist/commands/cal-detail.js +35 -0
- package/dist/commands/cal-direct.js +119 -0
- package/dist/commands/cal-overlay.js +73 -30
- package/dist/commands/cal.js +62 -25
- package/dist/commands/clean.js +4 -1
- package/dist/commands/docs-read.js +5 -2
- package/dist/commands/docs.js +38 -6
- package/dist/commands/doctor-backfill.js +88 -71
- package/dist/commands/doctor-pipeline.js +59 -46
- package/dist/commands/heartbeat-post.js +116 -0
- package/dist/commands/heartbeat-token-lock.js +46 -33
- package/dist/commands/heartbeat.js +43 -125
- package/dist/commands/install-update.js +17 -15
- package/dist/commands/issue-show.js +27 -28
- package/dist/commands/issue-versions.js +93 -0
- package/dist/commands/issue-write.js +70 -23
- package/dist/commands/issue.js +73 -37
- package/dist/commands/jarvis-live-answer.js +99 -0
- package/dist/commands/jarvis-render.js +1 -94
- package/dist/commands/jarvis-trace.js +30 -22
- package/dist/commands/jarvis-turn-report.js +124 -0
- package/dist/commands/jarvis-turn.js +108 -182
- package/dist/commands/jarvis.js +26 -17
- package/dist/commands/local-args-collector-setup.js +82 -84
- package/dist/commands/local-args-collector-work.js +37 -38
- package/dist/commands/local-args-tower-admin.js +155 -142
- package/dist/commands/local-args-tower-cal.js +69 -54
- package/dist/commands/local-args-tower-careers.js +73 -54
- package/dist/commands/local-args-tower-docs-msg.js +79 -51
- package/dist/commands/local-args-tower-github.js +26 -20
- package/dist/commands/local-args-tower-hits.js +32 -20
- package/dist/commands/local-args-tower-manager.js +85 -4
- package/dist/commands/local-args-tower-memory.js +139 -117
- package/dist/commands/local-args-tower-msg.js +24 -6
- package/dist/commands/local-args-tower-notes.js +346 -0
- package/dist/commands/local-args-tower-pages.js +100 -378
- package/dist/commands/local-args-tower-settings.js +28 -29
- package/dist/commands/local-args-tower-setup.js +11 -4
- package/dist/commands/local-args-tower-work.js +74 -37
- package/dist/commands/local-args.js +80 -129
- package/dist/commands/local-help-commands-tower-ops.js +15 -4
- package/dist/commands/local-help-commands-tower.js +20 -7
- package/dist/commands/local-help-commands.js +342 -341
- package/dist/commands/local-help.js +87 -83
- package/dist/commands/local-run-guards.js +100 -0
- package/dist/commands/local.js +137 -206
- package/dist/commands/login.js +13 -9
- package/dist/commands/logs.js +55 -38
- package/dist/commands/mail-attachments.js +44 -26
- package/dist/commands/manager-render-home.js +208 -0
- package/dist/commands/manager-render.js +7 -3
- package/dist/commands/manager-requests.js +33 -0
- package/dist/commands/manager.js +39 -5
- package/dist/commands/memory-consolidation.js +18 -9
- package/dist/commands/memory-daemon-probe.js +37 -28
- package/dist/commands/memory-install-config.js +64 -60
- package/dist/commands/memory-install.js +21 -41
- package/dist/commands/memory-requeue.js +26 -19
- package/dist/commands/memory-rules.js +26 -18
- package/dist/commands/memory-search-print.js +26 -0
- package/dist/commands/memory-store.js +14 -30
- package/dist/commands/memory-verb-list.js +49 -48
- package/dist/commands/msg-read-line.js +32 -0
- package/dist/commands/msg.js +45 -3
- package/dist/commands/notes-accounts.js +49 -32
- package/dist/commands/notes-sync-loop.js +115 -89
- package/dist/commands/notes-wisprflow-login.js +44 -23
- package/dist/commands/notes-writes.js +115 -100
- package/dist/commands/notes.js +33 -55
- package/dist/commands/onboard-flows.js +69 -51
- package/dist/commands/onboard.js +11 -9
- package/dist/commands/ops-render-memory.js +10 -7
- package/dist/commands/ops-render.js +33 -30
- package/dist/commands/ops.js +3 -0
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/session-sync-outside-roots.js +40 -36
- package/dist/commands/session-sync-plan.js +14 -10
- package/dist/commands/session-sync-upload.js +24 -22
- package/dist/commands/session-sync.js +19 -8
- package/dist/commands/setup.js +13 -0
- package/dist/commands/slack.js +25 -16
- package/dist/commands/sources-check.js +13 -7
- package/dist/commands/sources-discovery.js +25 -17
- package/dist/commands/sources-verdict.js +19 -18
- package/dist/commands/speed.js +23 -19
- package/dist/commands/start.js +20 -16
- package/dist/commands/sync-doctor.js +52 -48
- package/dist/commands/sync.js +22 -16
- package/dist/commands/usage-telemetry-toml.js +71 -45
- package/dist/commands/usage.js +94 -68
- package/dist/commands/workbook-render.js +10 -7
- package/dist/evidence-reconcile-client.js +75 -64
- package/dist/evidence-redelivery-plan.js +37 -34
- package/dist/evidence-redelivery.js +32 -29
- package/dist/evidence-transcripts-client.js +20 -20
- package/dist/evidence-upload-object.js +86 -89
- package/dist/evidence-upload-plan.js +94 -83
- package/dist/evidence-upload-terminal.js +45 -50
- package/dist/image-description-session.js +156 -159
- package/dist/local-state-pairing-code.js +39 -31
- package/dist/local-state-status.js +9 -2
- package/dist/onboarding-roots.js +5 -8
- package/dist/process-runner.js +8 -8
- package/dist/raw-evidence-gc.js +33 -38
- package/dist/raw-evidence-stuck-summary.js +7 -14
- package/dist/repo-identity.js +26 -23
- package/dist/scheduled-self-update.js +11 -15
- package/dist/second-install.js +3 -0
- package/dist/slim/codex-rules.js +23 -36
- package/dist/spool/local-spool-parse.js +13 -12
- package/dist/sync-lock.js +5 -15
- package/dist/tower-client.js +36 -53
- package/dist/tower-stream.js +7 -24
- package/dist/upload-agent-artifacts.js +58 -64
- package/dist/upload-envelope-build.js +60 -59
- package/dist/upload-evidence-delivery-reconcile.js +58 -56
- package/dist/upload-evidence-delivery-summary.js +32 -34
- package/dist/upload-ingest-receipt.js +20 -23
- package/dist/upload-session-reports-queue.js +4 -1
- package/dist/upload-session-reports-wire.js +64 -68
- package/dist/upload-sync.js +171 -157
- package/package.json +4 -4
|
@@ -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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
124
|
-
|
|
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
|
*
|