@bli-cockpit/cli 0.2.56 → 0.2.58
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/commands/agent-door.js +85 -0
- package/dist/commands/docs.js +227 -0
- package/dist/commands/issue-contracts.js +99 -0
- package/dist/commands/issue-write.js +129 -0
- package/dist/commands/issue.js +189 -0
- package/dist/commands/local-args-tower-docs-msg.js +126 -0
- package/dist/commands/local-args-tower-work.js +178 -0
- package/dist/commands/local-args-tower.js +7 -1
- package/dist/commands/local-args.js +10 -2
- package/dist/commands/local-help.js +70 -0
- package/dist/commands/local.js +12 -0
- package/dist/commands/mcp-bin-resolve.js +102 -0
- package/dist/commands/memory-install-claude.js +13 -5
- package/dist/commands/memory-install-config.js +140 -0
- package/dist/commands/memory-install-report.js +89 -0
- package/dist/commands/memory-install.js +51 -362
- package/dist/commands/msg.js +188 -0
- package/dist/commands/notes-door.js +120 -0
- package/dist/commands/notes-reads.js +134 -0
- package/dist/commands/notes-writes.js +208 -0
- package/dist/commands/notes.js +16 -442
- package/dist/commands/ops-render.js +18 -2
- package/dist/commands/ops.js +9 -2
- package/dist/commands/project.js +38 -0
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/tower-mcp-claude.js +30 -0
- package/dist/commands/tower-mcp-codex.js +100 -0
- package/dist/commands/tower-mcp-contract.js +39 -0
- package/dist/commands/tower-mcp-install.js +75 -0
- package/dist/repo-identity-fingerprint.js +88 -0
- package/dist/repo-identity-git.js +76 -0
- package/dist/repo-identity-linked-worktrees.js +81 -0
- package/dist/repo-identity.js +5 -222
- package/dist/upload-envelope-build.js +240 -0
- package/dist/upload-envelope-event.js +198 -0
- package/dist/upload-envelope.js +16 -427
- package/dist/upload-ingest-receipt.js +121 -0
- package/dist/upload-session-reports-queue.js +156 -0
- package/dist/upload-session-reports-wire.js +275 -0
- package/dist/upload-session-reports.js +14 -425
- package/dist/upload-sync.js +291 -0
- package/dist/upload.js +24 -396
- package/package.json +6 -5
|
@@ -8,430 +8,19 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Table of contents:
|
|
10
10
|
*
|
|
11
|
-
* - `
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* wire: chunk at the endpoint's
|
|
19
|
-
*
|
|
11
|
+
* - `upload-session-reports-queue.ts` — `queueCodexSessionReport` writes the
|
|
12
|
+
* rows down first, so a report survives its source file and an endpoint
|
|
13
|
+
* outage cannot age the evidence out; `flushPendingCodexSessionReports`
|
|
14
|
+
* drains that queue with current credentials, deleting each report only
|
|
15
|
+
* against a receipt it can read.
|
|
16
|
+
* - `upload-session-reports-wire.ts` — `postCodexSessionReport`, the one-shot
|
|
17
|
+
* path for a harvest that already has a repo in hand, and
|
|
18
|
+
* `reportCodexSessionAttributions`, the wire itself: chunk at the endpoint's
|
|
19
|
+
* session cap, then attempt each chunk under a decision table of "final
|
|
20
|
+
* answer" versus "worth another attempt".
|
|
20
21
|
*
|
|
21
|
-
* Nothing here throws at the caller. Every failure comes back as a reason
|
|
22
|
-
* so a dashboard that is old, down or unreachable cannot fail a harvest.
|
|
22
|
+
* Nothing here throws at the caller. Every failure comes back as a reason
|
|
23
|
+
* label so a dashboard that is old, down or unreachable cannot fail a harvest.
|
|
23
24
|
*/
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
import { getCollectorRuntimePaths, LOCAL_COLLECTOR_VERSION, readLocalCollectorConfig, readLocalCollectorSessionFile, readLocalSessionReference, readLocalWorkContextForRepo, } from "./local-state.js";
|
|
27
|
-
import { readLocalUploadSpoolState, recordPendingSessionReport, recordSessionReportFailure, recordSessionReportSuccess, } from "./spool/local-spool.js";
|
|
28
|
-
import { makeCollectorProvenance, makeUploadWorkContext, safeRepoLabel, } from "./upload-envelope.js";
|
|
29
|
-
import { normalizeDashboardUrl, readResponseJson } from "./upload-http.js";
|
|
30
|
-
import { describeError } from "./health-detail.js";
|
|
31
|
-
/** Attempts per chunk, and the linear 250ms-per-attempt backoff between them. */
|
|
32
|
-
const DEFAULT_REPORT_ATTEMPTS = 3;
|
|
33
|
-
const REPORT_RETRY_BACKOFF_STEP_MS = 250;
|
|
34
|
-
/**
|
|
35
|
-
* Posts a Codex session attribution report using the paired collector
|
|
36
|
-
* credentials and the work context of a representative repo. Failures come
|
|
37
|
-
* back as reason labels so the harvest never hard-fails on reporting.
|
|
38
|
-
*/
|
|
39
|
-
export async function postCodexSessionReport(options) {
|
|
40
|
-
if (options.sessions.length === 0) {
|
|
41
|
-
return emptyCodexSessionReportResult("no_sessions_observed");
|
|
42
|
-
}
|
|
43
|
-
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
44
|
-
if (!fetchImpl) {
|
|
45
|
-
throw new Error("global fetch is unavailable; use Node.js 20 or newer.");
|
|
46
|
-
}
|
|
47
|
-
try {
|
|
48
|
-
const paths = getCollectorRuntimePaths(options.homeDir);
|
|
49
|
-
const config = await readLocalCollectorConfig(paths);
|
|
50
|
-
const sessionFile = await readLocalCollectorSessionFile(paths);
|
|
51
|
-
const session = await readLocalSessionReference(paths);
|
|
52
|
-
if (session.session_state !== "valid") {
|
|
53
|
-
return emptyCodexSessionReportResult("collector_not_paired");
|
|
54
|
-
}
|
|
55
|
-
const repoRoot = path.resolve(options.repoRoot ?? process.cwd());
|
|
56
|
-
const activeContext = await readLocalWorkContextForRepo(paths, repoRoot);
|
|
57
|
-
const repoLabel = safeRepoLabel(activeContext.repo_label ?? repoRoot);
|
|
58
|
-
const provenance = makeCollectorProvenance({
|
|
59
|
-
context: makeUploadWorkContext({
|
|
60
|
-
activeContext,
|
|
61
|
-
session,
|
|
62
|
-
repoLabel,
|
|
63
|
-
now: options.now ?? new Date(),
|
|
64
|
-
}),
|
|
65
|
-
session,
|
|
66
|
-
repoLabel,
|
|
67
|
-
});
|
|
68
|
-
return await reportCodexSessionAttributions({
|
|
69
|
-
fetchImpl,
|
|
70
|
-
dashboardUrl: normalizeDashboardUrl(options.dashboardUrl ?? sessionFile.dashboard_url ?? config.dashboard_url),
|
|
71
|
-
deviceToken: sessionFile.device_token,
|
|
72
|
-
provenance,
|
|
73
|
-
generatedAt: (options.now ?? new Date()).toISOString(),
|
|
74
|
-
sessions: options.sessions,
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
catch (error) {
|
|
78
|
-
// `collector_not_ready` covers this whole block — config read, session
|
|
79
|
-
// read, work-context read AND the report request itself. The label is
|
|
80
|
-
// kept (it is what the receipt contract carries) but it is broader than
|
|
81
|
-
// its name suggests: a network failure inside `reportCodexSessionAttributions`
|
|
82
|
-
// also lands here and reads as "this machine is not set up". The detail is
|
|
83
|
-
// now the only way to tell those apart. Flagged, not reclassified —
|
|
84
|
-
// narrowing the label is a contract change (BLI-3238).
|
|
85
|
-
console.error("[session-reports] could not report session attributions", JSON.stringify({
|
|
86
|
-
reason: "collector_not_ready",
|
|
87
|
-
session_count: options.sessions.length,
|
|
88
|
-
...describeError(error),
|
|
89
|
-
}));
|
|
90
|
-
return emptyCodexSessionReportResult("collector_not_ready");
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
/**
|
|
94
|
-
* Queues the safe session-attribution rows before the network request. The
|
|
95
|
-
* queue is merged per work context, so an endpoint outage retains every
|
|
96
|
-
* observed session without growing one duplicate report per scheduler pass.
|
|
97
|
-
*/
|
|
98
|
-
export async function queueCodexSessionReport(options) {
|
|
99
|
-
if (options.sessions.length === 0)
|
|
100
|
-
return null;
|
|
101
|
-
const paths = getCollectorRuntimePaths(options.homeDir);
|
|
102
|
-
return await recordPendingSessionReport(paths, {
|
|
103
|
-
attempted_at: options.generatedAt,
|
|
104
|
-
dashboard_url: normalizeDashboardUrl(options.dashboardUrl),
|
|
105
|
-
generated_at: options.generatedAt,
|
|
106
|
-
work_context_id: options.workContextId,
|
|
107
|
-
repo_label: safeRepoLabel(options.repoLabel),
|
|
108
|
-
branch: options.branch,
|
|
109
|
-
repo_fingerprint: options.repoFingerprint,
|
|
110
|
-
repo_origin_url: options.repoOriginUrl,
|
|
111
|
-
worktree_label: options.worktreeLabel,
|
|
112
|
-
worktree_fingerprint: options.worktreeFingerprint,
|
|
113
|
-
worktree_is_primary: options.worktreeIsPrimary,
|
|
114
|
-
sessions: options.sessions,
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
|
-
/**
|
|
118
|
-
* Flushes every durable session-attribution report using current credentials.
|
|
119
|
-
* Report payloads do not depend on the source files still being inside the live
|
|
120
|
-
* scan window, so a transient endpoint failure cannot silently age them out.
|
|
121
|
-
*/
|
|
122
|
-
export async function flushPendingCodexSessionReports(options) {
|
|
123
|
-
const paths = getCollectorRuntimePaths(options.homeDir);
|
|
124
|
-
const state = await readLocalUploadSpoolState(paths);
|
|
125
|
-
if (state.pending_session_reports.length === 0) {
|
|
126
|
-
return emptyCodexSessionReportResult("no_pending_session_reports");
|
|
127
|
-
}
|
|
128
|
-
const attemptedAt = (options.now ?? new Date()).toISOString();
|
|
129
|
-
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
130
|
-
if (!fetchImpl) {
|
|
131
|
-
throw new Error("global fetch is unavailable; use Node.js 20 or newer.");
|
|
132
|
-
}
|
|
133
|
-
let sessionFile;
|
|
134
|
-
let session;
|
|
135
|
-
try {
|
|
136
|
-
[sessionFile, session] = await Promise.all([
|
|
137
|
-
readLocalCollectorSessionFile(paths),
|
|
138
|
-
readLocalSessionReference(paths),
|
|
139
|
-
]);
|
|
140
|
-
}
|
|
141
|
-
catch (error) {
|
|
142
|
-
// This marks every queued report as failed. The count is what makes it
|
|
143
|
-
// worth a line: an unreadable session file here strands N reports at once
|
|
144
|
-
// and the only visible trace is a spool that stops draining (BLI-3238).
|
|
145
|
-
console.error("[session-reports] session file unreadable, failing the pending reports", JSON.stringify({
|
|
146
|
-
reason: "collector_not_ready",
|
|
147
|
-
pending_report_count: state.pending_session_reports.length,
|
|
148
|
-
...describeError(error),
|
|
149
|
-
}));
|
|
150
|
-
return await failPendingSessionReports(paths, state.pending_session_reports, attemptedAt, "collector_not_ready");
|
|
151
|
-
}
|
|
152
|
-
if (session.session_state !== "valid") {
|
|
153
|
-
return await failPendingSessionReports(paths, state.pending_session_reports, attemptedAt, "collector_not_paired");
|
|
154
|
-
}
|
|
155
|
-
const results = [];
|
|
156
|
-
for (const pending of state.pending_session_reports) {
|
|
157
|
-
const result = await reportCodexSessionAttributions({
|
|
158
|
-
fetchImpl,
|
|
159
|
-
dashboardUrl: normalizeDashboardUrl(pending.dashboard_url || sessionFile.dashboard_url),
|
|
160
|
-
deviceToken: sessionFile.device_token,
|
|
161
|
-
provenance: provenanceForPendingReport(pending, session),
|
|
162
|
-
generatedAt: pending.generated_at,
|
|
163
|
-
sessions: pending.sessions,
|
|
164
|
-
maxAttemptsPerRequest: options.maxAttemptsPerRequest,
|
|
165
|
-
sleep: options.sleep,
|
|
166
|
-
});
|
|
167
|
-
results.push(result);
|
|
168
|
-
if (result.posted) {
|
|
169
|
-
await recordSessionReportSuccess(paths, {
|
|
170
|
-
reportId: pending.report_id,
|
|
171
|
-
attemptedAt,
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
else {
|
|
175
|
-
await recordSessionReportFailure(paths, {
|
|
176
|
-
reportId: pending.report_id,
|
|
177
|
-
attemptedAt,
|
|
178
|
-
reason: result.reason,
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
return combineCodexSessionReportResults(results);
|
|
183
|
-
}
|
|
184
|
-
/**
|
|
185
|
-
* A queued report carries its own repo and worktree identity, captured when the
|
|
186
|
-
* sessions were observed. Only the operator and device session come from the
|
|
187
|
-
* credentials in use now — the report may be days old and from another branch.
|
|
188
|
-
*/
|
|
189
|
-
function provenanceForPendingReport(pending, session) {
|
|
190
|
-
return {
|
|
191
|
-
capture_source: "collector_runtime",
|
|
192
|
-
capture_adapter_version: LOCAL_COLLECTOR_VERSION,
|
|
193
|
-
collector_version: LOCAL_COLLECTOR_VERSION,
|
|
194
|
-
repo: pending.repo_label,
|
|
195
|
-
branch: pending.branch,
|
|
196
|
-
repo_label: pending.repo_label,
|
|
197
|
-
repo_fingerprint: pending.repo_fingerprint,
|
|
198
|
-
...(pending.repo_origin_url
|
|
199
|
-
? { repo_origin_url: pending.repo_origin_url }
|
|
200
|
-
: {}),
|
|
201
|
-
worktree_label: pending.worktree_label,
|
|
202
|
-
worktree_fingerprint: pending.worktree_fingerprint,
|
|
203
|
-
worktree_is_primary: pending.worktree_is_primary,
|
|
204
|
-
operator_id: session.operator_id,
|
|
205
|
-
session_id: session.session_id,
|
|
206
|
-
work_context_id: pending.work_context_id,
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
async function failPendingSessionReports(paths, pendingReports, attemptedAt, reason) {
|
|
210
|
-
for (const pending of pendingReports) {
|
|
211
|
-
await recordSessionReportFailure(paths, {
|
|
212
|
-
reportId: pending.report_id,
|
|
213
|
-
attemptedAt,
|
|
214
|
-
reason,
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
|
-
return {
|
|
218
|
-
posted: false,
|
|
219
|
-
reason,
|
|
220
|
-
chunk_count: 0,
|
|
221
|
-
recorded_count: 0,
|
|
222
|
-
failed_count: pendingReports.length,
|
|
223
|
-
chunks: [],
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
|
-
function combineCodexSessionReportResults(results) {
|
|
227
|
-
if (results.length === 0) {
|
|
228
|
-
return emptyCodexSessionReportResult("no_pending_session_reports");
|
|
229
|
-
}
|
|
230
|
-
const failed = results.filter((result) => !result.posted);
|
|
231
|
-
return {
|
|
232
|
-
posted: failed.length === 0,
|
|
233
|
-
reason: failed[0]?.reason ??
|
|
234
|
-
(results.every((result) => result.reason === "recorded")
|
|
235
|
-
? "recorded"
|
|
236
|
-
: results[0]?.reason ?? "recorded"),
|
|
237
|
-
chunk_count: results.reduce((total, result) => total + result.chunk_count, 0),
|
|
238
|
-
recorded_count: results.reduce((total, result) => total + result.recorded_count, 0),
|
|
239
|
-
failed_count: results.reduce((total, result) => total + result.failed_count, 0),
|
|
240
|
-
chunks: results.flatMap((result) => result.chunks),
|
|
241
|
-
};
|
|
242
|
-
}
|
|
243
|
-
/**
|
|
244
|
-
* Reports Codex session attribution outcomes after sync. Non-fatal by design:
|
|
245
|
-
* older dashboards without the endpoint must not fail the harvest, so the
|
|
246
|
-
* caller receives a posted/skipped label instead of an exception.
|
|
247
|
-
*/
|
|
248
|
-
export async function reportCodexSessionAttributions(options) {
|
|
249
|
-
if (options.sessions.length === 0) {
|
|
250
|
-
return emptyCodexSessionReportResult("no_sessions_observed");
|
|
251
|
-
}
|
|
252
|
-
const chunks = [];
|
|
253
|
-
const sessionChunks = chunkArray(options.sessions, CODEX_SESSION_REPORT_MAX_SESSIONS);
|
|
254
|
-
for (const [index, sessions] of sessionChunks.entries()) {
|
|
255
|
-
const chunk = await postCodexSessionAttributionChunk({
|
|
256
|
-
...options,
|
|
257
|
-
sessions,
|
|
258
|
-
batchIndex: index + 1,
|
|
259
|
-
});
|
|
260
|
-
chunks.push(chunk);
|
|
261
|
-
}
|
|
262
|
-
const failedCount = chunks.filter((chunk) => !chunk.posted).length;
|
|
263
|
-
const recordedCount = chunks.reduce((sum, chunk) => sum + chunk.recorded_count, 0);
|
|
264
|
-
return {
|
|
265
|
-
posted: failedCount === 0,
|
|
266
|
-
reason: failedCount === 0
|
|
267
|
-
? "recorded"
|
|
268
|
-
: chunks.find((chunk) => !chunk.posted)?.reason ?? "report_failed",
|
|
269
|
-
chunk_count: chunks.length,
|
|
270
|
-
recorded_count: recordedCount,
|
|
271
|
-
failed_count: failedCount,
|
|
272
|
-
chunks,
|
|
273
|
-
};
|
|
274
|
-
}
|
|
275
|
-
/**
|
|
276
|
-
* One chunk, up to three attempts, as a decision table on what came back:
|
|
277
|
-
*
|
|
278
|
-
* | what came back | verdict |
|
|
279
|
-
* |-------------------------------------|----------------------------------------|
|
|
280
|
-
* | 404 | this dashboard has no endpoint; stop |
|
|
281
|
-
* | 2xx with a readable, full receipt | recorded; stop |
|
|
282
|
-
* | 2xx malformed or short of the count | keep the reason, try again |
|
|
283
|
-
* | 4xx other than 429 | the server's final answer; stop |
|
|
284
|
-
* | 429 or 5xx | keep the reason, try again |
|
|
285
|
-
* | transport error | keep the reason, try again, no status |
|
|
286
|
-
*
|
|
287
|
-
* The row that looks wrong is the third one, and it is the important one: a 2xx
|
|
288
|
-
* we cannot read is retried because the server may have died between persisting
|
|
289
|
-
* the rows and answering. Only a schema-valid receipt that accounts for every
|
|
290
|
-
* distinct session is safe to delete a durable pending report against.
|
|
291
|
-
*/
|
|
292
|
-
async function postCodexSessionAttributionChunk(options) {
|
|
293
|
-
const maxAttempts = options.maxAttemptsPerRequest ?? DEFAULT_REPORT_ATTEMPTS;
|
|
294
|
-
const sleep = options.sleep ?? defaultReportSleep;
|
|
295
|
-
const requiredAcknowledgements = requiredCodexSessionAcknowledgementCount(options.sessions);
|
|
296
|
-
let lastStatus = null;
|
|
297
|
-
let lastFailureReason = null;
|
|
298
|
-
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
299
|
-
try {
|
|
300
|
-
const response = await postSessionAttributionRequest(options);
|
|
301
|
-
lastStatus = response.status;
|
|
302
|
-
const body = await readResponseJson(response);
|
|
303
|
-
if (response.status === 404) {
|
|
304
|
-
return codexSessionReportChunkResult(options, {
|
|
305
|
-
posted: false,
|
|
306
|
-
reason: "codex_session_api_unavailable",
|
|
307
|
-
httpStatus: response.status,
|
|
308
|
-
recordedCount: 0,
|
|
309
|
-
});
|
|
310
|
-
}
|
|
311
|
-
if (response.ok) {
|
|
312
|
-
const receipt = readSessionReportReceipt(body, requiredAcknowledgements);
|
|
313
|
-
if (receipt.accepted) {
|
|
314
|
-
return codexSessionReportChunkResult(options, {
|
|
315
|
-
posted: true,
|
|
316
|
-
reason: "recorded",
|
|
317
|
-
httpStatus: response.status,
|
|
318
|
-
recordedCount: receipt.recorded_count,
|
|
319
|
-
});
|
|
320
|
-
}
|
|
321
|
-
lastFailureReason = receipt.reason;
|
|
322
|
-
}
|
|
323
|
-
else {
|
|
324
|
-
lastFailureReason = `report_failed_http_${response.status}`;
|
|
325
|
-
if (isFinalHttpFailure(response.status)) {
|
|
326
|
-
return codexSessionReportChunkResult(options, {
|
|
327
|
-
posted: false,
|
|
328
|
-
reason: lastFailureReason,
|
|
329
|
-
httpStatus: response.status,
|
|
330
|
-
recordedCount: 0,
|
|
331
|
-
});
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
catch (error) {
|
|
336
|
-
// Logged per attempt, on purpose: the interesting failure is the one
|
|
337
|
-
// that repeats. Three identical ECONNRESETs and one DNS failure followed
|
|
338
|
-
// by two resets are different stories, and `report_network_error` alone
|
|
339
|
-
// tells neither (BLI-3238).
|
|
340
|
-
console.error("[session-reports] report attempt failed before a status came back", JSON.stringify({
|
|
341
|
-
reason: "report_network_error",
|
|
342
|
-
attempt,
|
|
343
|
-
max_attempts: maxAttempts,
|
|
344
|
-
batch_index: options.batchIndex,
|
|
345
|
-
session_count: options.sessions.length,
|
|
346
|
-
...describeError(error),
|
|
347
|
-
}));
|
|
348
|
-
lastStatus = null;
|
|
349
|
-
lastFailureReason = "report_network_error";
|
|
350
|
-
}
|
|
351
|
-
if (attempt < maxAttempts) {
|
|
352
|
-
await sleep(REPORT_RETRY_BACKOFF_STEP_MS * attempt);
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
return codexSessionReportChunkResult(options, {
|
|
356
|
-
posted: false,
|
|
357
|
-
reason: lastFailureReason ??
|
|
358
|
-
(lastStatus === null
|
|
359
|
-
? "report_network_error"
|
|
360
|
-
: `report_failed_http_${lastStatus}`),
|
|
361
|
-
httpStatus: lastStatus,
|
|
362
|
-
recordedCount: 0,
|
|
363
|
-
});
|
|
364
|
-
}
|
|
365
|
-
function postSessionAttributionRequest(options) {
|
|
366
|
-
return options.fetchImpl(`${options.dashboardUrl}/api/ambient/codex-sessions`, {
|
|
367
|
-
method: "POST",
|
|
368
|
-
headers: {
|
|
369
|
-
"Authorization": `Bearer ${options.deviceToken}`,
|
|
370
|
-
"Content-Type": "application/json",
|
|
371
|
-
},
|
|
372
|
-
body: JSON.stringify({
|
|
373
|
-
schema_version: "ambient-codex-session-attributions.v1",
|
|
374
|
-
generated_at: options.generatedAt,
|
|
375
|
-
provenance: options.provenance,
|
|
376
|
-
sessions: options.sessions,
|
|
377
|
-
}),
|
|
378
|
-
});
|
|
379
|
-
}
|
|
380
|
-
/** Is this status the server's final answer, or worth another attempt? */
|
|
381
|
-
function isFinalHttpFailure(status) {
|
|
382
|
-
return status < 500 && status !== 429;
|
|
383
|
-
}
|
|
384
|
-
/**
|
|
385
|
-
* Can we take this 2xx body as proof the rows are durable?
|
|
386
|
-
*
|
|
387
|
-
* Only if it parses as the receipt schema AND accounts for every distinct
|
|
388
|
-
* session in the chunk. Anything less is treated as no receipt at all.
|
|
389
|
-
*/
|
|
390
|
-
function readSessionReportReceipt(body, requiredAcknowledgements) {
|
|
391
|
-
const parsed = CodexSessionAttributionReportResponseSchema.safeParse(body);
|
|
392
|
-
if (!parsed.success) {
|
|
393
|
-
return { accepted: false, reason: "report_invalid_response" };
|
|
394
|
-
}
|
|
395
|
-
if (parsed.data.recorded_count < requiredAcknowledgements) {
|
|
396
|
-
return { accepted: false, reason: "report_incomplete_acknowledgement" };
|
|
397
|
-
}
|
|
398
|
-
return { accepted: true, recorded_count: parsed.data.recorded_count };
|
|
399
|
-
}
|
|
400
|
-
function codexSessionReportChunkResult(options, result) {
|
|
401
|
-
return {
|
|
402
|
-
batch_index: options.batchIndex,
|
|
403
|
-
session_count: options.sessions.length,
|
|
404
|
-
posted: result.posted,
|
|
405
|
-
reason: result.reason,
|
|
406
|
-
http_status: result.httpStatus,
|
|
407
|
-
recorded_count: result.recordedCount,
|
|
408
|
-
};
|
|
409
|
-
}
|
|
410
|
-
function emptyCodexSessionReportResult(reason) {
|
|
411
|
-
return {
|
|
412
|
-
posted: false,
|
|
413
|
-
reason,
|
|
414
|
-
chunk_count: 0,
|
|
415
|
-
recorded_count: 0,
|
|
416
|
-
failed_count: 0,
|
|
417
|
-
chunks: [],
|
|
418
|
-
};
|
|
419
|
-
}
|
|
420
|
-
function chunkArray(items, size) {
|
|
421
|
-
const chunks = [];
|
|
422
|
-
for (let offset = 0; offset < items.length; offset += size) {
|
|
423
|
-
chunks.push(items.slice(offset, offset + size));
|
|
424
|
-
}
|
|
425
|
-
return chunks;
|
|
426
|
-
}
|
|
427
|
-
/**
|
|
428
|
-
* How many rows the server must acknowledge, counted per distinct session
|
|
429
|
-
* rather than per row — the same session observed twice in one chunk is still
|
|
430
|
-
* one row on the far side.
|
|
431
|
-
*/
|
|
432
|
-
function requiredCodexSessionAcknowledgementCount(sessions) {
|
|
433
|
-
return new Set(sessions.map((session) => `${session.source ?? "codex"}:${session.codex_session_id}`)).size;
|
|
434
|
-
}
|
|
435
|
-
function defaultReportSleep(milliseconds) {
|
|
436
|
-
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
437
|
-
}
|
|
25
|
+
export { flushPendingCodexSessionReports, queueCodexSessionReport, } from "./upload-session-reports-queue.js";
|
|
26
|
+
export { postCodexSessionReport, reportCodexSessionAttributions, } from "./upload-session-reports-wire.js";
|