@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.
- 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-overlay.js +73 -30
- package/dist/commands/cal.js +36 -23
- package/dist/commands/clean.js +4 -1
- package/dist/commands/docs-read.js +5 -2
- 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-write.js +70 -23
- package/dist/commands/issue.js +68 -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 +52 -50
- package/dist/commands/local-args-tower-careers.js +73 -54
- package/dist/commands/local-args-tower-docs-msg.js +55 -45
- 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-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-work.js +52 -33
- 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 +6 -2
- 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/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/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 +91 -67
- 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/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 +2 -2
package/dist/tower-client.js
CHANGED
|
@@ -1,27 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* today, the rest of the CLI-parity surface next — needs the same three
|
|
6
|
-
* things: the paired device session, a request that carries its device token,
|
|
7
|
-
* and a failure that names itself instead of throwing a transport stack trace
|
|
8
|
-
* at a person. Before this module each command grew its own copy; the copies
|
|
9
|
-
* drifted (one normalised the dashboard URL, one did not) and only one of them
|
|
10
|
-
* ever named a reason.
|
|
11
|
-
*
|
|
12
|
-
* Contract:
|
|
13
|
-
* - Nothing here throws for a transport error. `towerRequest` returns
|
|
14
|
-
* `{ ok: false, reason }` for every network-shaped failure, and the reason
|
|
15
|
-
* label is the thing that travels to the operator.
|
|
16
|
-
* - `loadPairedSession` still throws, because "this machine is not paired" is
|
|
17
|
-
* not a transport failure — it is a precondition the caller cannot proceed
|
|
18
|
-
* past, and the command's top level turns it into one plain sentence.
|
|
19
|
-
* - The device token is attached to the request and never returned, logged, or
|
|
20
|
-
* put in a reason. Log lines here carry metadata only.
|
|
21
|
-
*
|
|
22
|
-
* The HTTP vocabulary (`normalizeDashboardUrl`, `readResponseJson`,
|
|
23
|
-
* `responseErrorMessage`) is reused from `upload-http.ts` rather than copied —
|
|
24
|
-
* the upload path and the conversation path answer to the same rules.
|
|
2
|
+
* Share authenticated Tower requests across CLI capabilities.
|
|
3
|
+
* HTTP refusals remain responses for callers to interpret; transport failures
|
|
4
|
+
* carry named reasons. Warm reads reuse the daemon and writes invalidate cache.
|
|
25
5
|
*/
|
|
26
6
|
import { invalidateReadCacheSurface, readCacheSurfaceForPath, } from "@bli-cockpit/telemetry-core/node-read-cache";
|
|
27
7
|
import { errorMessage } from "./commands/cli-io.js";
|
|
@@ -109,36 +89,7 @@ export async function towerRequest(options) {
|
|
|
109
89
|
: null;
|
|
110
90
|
const response = warmResponse ?? (await options.fetch(url, init));
|
|
111
91
|
const respondedMs = Date.now() - startedAt;
|
|
112
|
-
|
|
113
|
-
// "log the success branch too" rule intact) — it is just that under
|
|
114
|
-
// `--json`/`--quiet` the reader already gets that decision back as the
|
|
115
|
-
// command's own stdout object, so a second copy on stderr is noise, not
|
|
116
|
-
// signal. The failure branch below never quiets: a request that reached
|
|
117
|
-
// no response at all is the one case AGENTS.md's logging rule protects.
|
|
118
|
-
if (!shouldSuppressTowerSuccessLog()) {
|
|
119
|
-
log(`[tower client] responded ${JSON.stringify({
|
|
120
|
-
request: options.label,
|
|
121
|
-
http_status: response.status,
|
|
122
|
-
content_type: response.headers.get("content-type") ?? "none",
|
|
123
|
-
streamed: (response.headers.get("content-type") ?? "").includes("application/x-ndjson"),
|
|
124
|
-
elapsed_ms: respondedMs,
|
|
125
|
-
})}`);
|
|
126
|
-
}
|
|
127
|
-
// BLI-10746: "any write through the CLI or MCP invalidates the affected
|
|
128
|
-
// cache". A successful (2xx) write to a covered surface throws away
|
|
129
|
-
// every cached read for that surface — coarse, but a stale row is never
|
|
130
|
-
// possible this way, only one avoidable extra request.
|
|
131
|
-
if (method !== "GET" && response.ok) {
|
|
132
|
-
const surface = readCacheSurfaceForPath(options.path);
|
|
133
|
-
if (surface) {
|
|
134
|
-
const stateDir = getCollectorRuntimePaths(options.homeDir).state_dir;
|
|
135
|
-
const invalidated = invalidateReadCacheSurface(stateDir, options.deviceToken, surface);
|
|
136
|
-
log(`[tower client] read cache invalidated ${JSON.stringify({
|
|
137
|
-
surface,
|
|
138
|
-
...(invalidated.ok ? { removed: invalidated.removed } : { reason: invalidated.reason }),
|
|
139
|
-
})}`);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
92
|
+
recordTowerResponse(options, response, method, respondedMs, log);
|
|
142
93
|
return { ok: true, response, startedAt, respondedMs };
|
|
143
94
|
}
|
|
144
95
|
catch (error) {
|
|
@@ -153,6 +104,38 @@ export async function towerRequest(options) {
|
|
|
153
104
|
return { ok: false, reason, detail: towerFailureDetail(reason, errorMessage(error)) };
|
|
154
105
|
}
|
|
155
106
|
}
|
|
107
|
+
function recordTowerResponse(options, response, method, respondedMs, log) {
|
|
108
|
+
// The success branch always decided something (BLI-10450 kept the
|
|
109
|
+
// "log the success branch too" rule intact) — it is just that under
|
|
110
|
+
// `--json`/`--quiet` the reader already gets that decision back as the
|
|
111
|
+
// command's own stdout object, so a second copy on stderr is noise, not
|
|
112
|
+
// signal. The failure branch below never quiets: a request that reached
|
|
113
|
+
// no response at all is the one case AGENTS.md's logging rule protects.
|
|
114
|
+
if (!shouldSuppressTowerSuccessLog()) {
|
|
115
|
+
log(`[tower client] responded ${JSON.stringify({
|
|
116
|
+
request: options.label,
|
|
117
|
+
http_status: response.status,
|
|
118
|
+
content_type: response.headers.get("content-type") ?? "none",
|
|
119
|
+
streamed: (response.headers.get("content-type") ?? "").includes("application/x-ndjson"),
|
|
120
|
+
elapsed_ms: respondedMs,
|
|
121
|
+
})}`);
|
|
122
|
+
}
|
|
123
|
+
// BLI-10746: "any write through the CLI or MCP invalidates the affected
|
|
124
|
+
// cache". A successful (2xx) write to a covered surface throws away
|
|
125
|
+
// every cached read for that surface — coarse, but a stale row is never
|
|
126
|
+
// possible this way, only one avoidable extra request.
|
|
127
|
+
if (method !== "GET" && response.ok) {
|
|
128
|
+
const surface = readCacheSurfaceForPath(options.path);
|
|
129
|
+
if (surface) {
|
|
130
|
+
const stateDir = getCollectorRuntimePaths(options.homeDir).state_dir;
|
|
131
|
+
const invalidated = invalidateReadCacheSurface(stateDir, options.deviceToken, surface);
|
|
132
|
+
log(`[tower client] read cache invalidated ${JSON.stringify({
|
|
133
|
+
surface,
|
|
134
|
+
...(invalidated.ok ? { removed: invalidated.removed } : { reason: invalidated.reason }),
|
|
135
|
+
})}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
156
139
|
/**
|
|
157
140
|
* `towerRequest` plus reading the body as JSON and turning a non-2xx into a
|
|
158
141
|
* named failure carrying the server's own words when it supplied any.
|
package/dist/tower-stream.js
CHANGED
|
@@ -1,28 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* (`application/x-ndjson`): `activity` events as tools start and settle,
|
|
6
|
-
* `token` events carrying the answer as it is written (BLI-3517), then exactly
|
|
7
|
-
* one `final` event carrying what the plain JSON response used to be.
|
|
8
|
-
* That is the same wire format the browser already reads
|
|
9
|
-
* (`apps/dashboard/src/lib/webchat/tool-trace.ts`); this is the terminal's
|
|
10
|
-
* reader for it.
|
|
11
|
-
*
|
|
12
|
-
* Three things it refuses to do:
|
|
13
|
-
* - Throw. A stream that dies mid-turn comes back as a named reason, never an
|
|
14
|
-
* exception in the middle of a person's answer.
|
|
15
|
-
* - Present a truncated turn as a complete one. No `final` event means the
|
|
16
|
-
* turn failed, and it says which way it failed
|
|
17
|
-
* (`turn_timed_out` when the dashboard's own 120s ceiling is the likely
|
|
18
|
-
* cause, `stream_ended_without_final` otherwise).
|
|
19
|
-
* - Require the stream. A dashboard that has not shipped streaming yet answers
|
|
20
|
-
* `application/json`; that single object is read as the final event and the
|
|
21
|
-
* fallback is named `stream_not_available` rather than passed off as a
|
|
22
|
-
* stream that happened to be quiet.
|
|
23
|
-
*
|
|
24
|
-
* A malformed line is skipped, named `ndjson_line_unparseable`, and the lines
|
|
25
|
-
* after it still count — one bad byte cannot swallow an answer.
|
|
2
|
+
* Read Tower stream frames without losing the final answer or partial timing.
|
|
3
|
+
* Activity, token and revision callbacks are optional; the final result retains
|
|
4
|
+
* the settled answer. A truncated stream reports its last meaningful reason.
|
|
26
5
|
*/
|
|
27
6
|
import { errorMessage } from "./commands/cli-io.js";
|
|
28
7
|
import { isAbortError } from "./tower-client.js";
|
|
@@ -131,6 +110,10 @@ export async function readTowerTurn(response, options = {}) {
|
|
|
131
110
|
activity.push(item.event);
|
|
132
111
|
options.onActivity?.(item.event);
|
|
133
112
|
}
|
|
113
|
+
return finishTowerTurn(options, { startedAt, activity, notes, streamed, final, draft, tokenFrames, firstByteMs, firstFrameMs, firstTokenMs });
|
|
114
|
+
}
|
|
115
|
+
function finishTowerTurn(options, state) {
|
|
116
|
+
const { startedAt, activity, notes, streamed, final, draft, tokenFrames, firstByteMs, firstFrameMs, firstTokenMs } = state;
|
|
134
117
|
const elapsedMs = Date.now() - startedAt;
|
|
135
118
|
const timing = { firstByteMs, firstFrameMs, firstTokenMs };
|
|
136
119
|
if (!final) {
|
|
@@ -1,14 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* A screenshot pasted into a Codex or Claude session is already uploaded as raw
|
|
5
|
-
* evidence like any other object; this is the second, separate report that says
|
|
6
|
-
* "that object is an image, from this session, at this turn" so the dashboard
|
|
7
|
-
* can queue it for redaction and OCR.
|
|
8
|
-
*
|
|
9
|
-
* Non-fatal by design: an older dashboard without the endpoint must not fail a
|
|
10
|
-
* harvest, so every path here returns a `{ posted, reason }` label instead of
|
|
11
|
-
* throwing.
|
|
2
|
+
* Register durable agent images so downstream OCR/redaction can find them.
|
|
3
|
+
* Reporting is best effort: return and log named outcomes without failing sync.
|
|
12
4
|
*/
|
|
13
5
|
import { AgentImageArtifactReportRequestSchema, } from "@bli-cockpit/telemetry-core";
|
|
14
6
|
import { describeError } from "./health-detail.js";
|
|
@@ -37,60 +29,9 @@ export async function reportAgentImageArtifacts(options) {
|
|
|
37
29
|
},
|
|
38
30
|
body: JSON.stringify(payload),
|
|
39
31
|
});
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if (!response.ok) {
|
|
44
|
-
const errorBody = await readResponseJson(response);
|
|
45
|
-
// A 404 is two different facts. "No such route" is the platform
|
|
46
|
-
// answering with an empty or HTML body; a JSON error envelope means the
|
|
47
|
-
// route answered and refused, and calling that "the dashboard has no
|
|
48
|
-
// agent-artifact route" hid a real refusal behind a shrug for as long as
|
|
49
|
-
// the door rejected ambient tickets.
|
|
50
|
-
if (response.status === 404 && !routeAnswered(errorBody)) {
|
|
51
|
-
console.error("[agent-artifacts] nothing answered at the agent-artifact path; these images stay unreported", JSON.stringify({
|
|
52
|
-
reason: "agent_artifact_api_unavailable",
|
|
53
|
-
http_status: 404,
|
|
54
|
-
artifact_count: artifacts.length,
|
|
55
|
-
}));
|
|
56
|
-
return {
|
|
57
|
-
posted: false,
|
|
58
|
-
reason: "agent_artifact_api_unavailable",
|
|
59
|
-
recorded_count: 0,
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
const serverReason = serverFailureDetail(errorBody);
|
|
63
|
-
console.error("[agent-artifacts] report refused", JSON.stringify({
|
|
64
|
-
reason: "report_failed",
|
|
65
|
-
http_status: response.status,
|
|
66
|
-
server_reason: serverReason ?? "none",
|
|
67
|
-
artifact_count: artifacts.length,
|
|
68
|
-
}));
|
|
69
|
-
return {
|
|
70
|
-
posted: false,
|
|
71
|
-
reason: `report_failed_http_${response.status}`,
|
|
72
|
-
recorded_count: 0,
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
const body = await readResponseJson(response);
|
|
76
|
-
const recordedCount = body && typeof body === "object"
|
|
77
|
-
? Number(body.recorded_count ?? 0)
|
|
78
|
-
: 0;
|
|
79
|
-
const recorded = Number.isFinite(recordedCount) ? recordedCount : 0;
|
|
80
|
-
// The success branch too: "some images reported" and "every image
|
|
81
|
-
// reported" are different facts, and a count that is short of
|
|
82
|
-
// `artifact_count` is the only warning an operator would ever get.
|
|
83
|
-
console.error("[agent-artifacts] report recorded", JSON.stringify({
|
|
84
|
-
reason: "recorded",
|
|
85
|
-
http_status: response.status,
|
|
86
|
-
artifact_count: artifacts.length,
|
|
87
|
-
recorded_count: recorded,
|
|
88
|
-
}));
|
|
89
|
-
return {
|
|
90
|
-
posted: true,
|
|
91
|
-
reason: "recorded",
|
|
92
|
-
recorded_count: recorded,
|
|
93
|
-
};
|
|
32
|
+
if (!response.ok)
|
|
33
|
+
return await reportRefusedArtifacts(response, artifacts.length);
|
|
34
|
+
return await recordAcceptedArtifacts(response, artifacts.length);
|
|
94
35
|
}
|
|
95
36
|
catch (error) {
|
|
96
37
|
// The label stays `report_network_error` — it is the wire contract. What
|
|
@@ -105,6 +46,59 @@ export async function reportAgentImageArtifacts(options) {
|
|
|
105
46
|
return { posted: false, reason: "report_network_error", recorded_count: 0 };
|
|
106
47
|
}
|
|
107
48
|
}
|
|
49
|
+
async function recordAcceptedArtifacts(response, artifactCount) {
|
|
50
|
+
const body = await readResponseJson(response);
|
|
51
|
+
const recordedCount = body && typeof body === "object"
|
|
52
|
+
? Number(body.recorded_count ?? 0)
|
|
53
|
+
: 0;
|
|
54
|
+
const recorded = Number.isFinite(recordedCount) ? recordedCount : 0;
|
|
55
|
+
// The success branch too: "some images reported" and "every image
|
|
56
|
+
// reported" are different facts, and a count that is short of
|
|
57
|
+
// `artifact_count` is the only warning an operator would ever get.
|
|
58
|
+
console.error("[agent-artifacts] report recorded", JSON.stringify({
|
|
59
|
+
reason: "recorded",
|
|
60
|
+
http_status: response.status,
|
|
61
|
+
artifact_count: artifactCount,
|
|
62
|
+
recorded_count: recorded,
|
|
63
|
+
}));
|
|
64
|
+
return {
|
|
65
|
+
posted: true,
|
|
66
|
+
reason: "recorded",
|
|
67
|
+
recorded_count: recorded,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
async function reportRefusedArtifacts(response, artifactCount) {
|
|
71
|
+
const errorBody = await readResponseJson(response);
|
|
72
|
+
// A 404 is two different facts. "No such route" is the platform
|
|
73
|
+
// answering with an empty or HTML body; a JSON error envelope means the
|
|
74
|
+
// route answered and refused, and calling that "the dashboard has no
|
|
75
|
+
// agent-artifact route" hid a real refusal behind a shrug for as long as
|
|
76
|
+
// the door rejected ambient tickets.
|
|
77
|
+
if (response.status === 404 && !routeAnswered(errorBody)) {
|
|
78
|
+
console.error("[agent-artifacts] nothing answered at the agent-artifact path; these images stay unreported", JSON.stringify({
|
|
79
|
+
reason: "agent_artifact_api_unavailable",
|
|
80
|
+
http_status: 404,
|
|
81
|
+
artifact_count: artifactCount,
|
|
82
|
+
}));
|
|
83
|
+
return {
|
|
84
|
+
posted: false,
|
|
85
|
+
reason: "agent_artifact_api_unavailable",
|
|
86
|
+
recorded_count: 0,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
const serverReason = serverFailureDetail(errorBody);
|
|
90
|
+
console.error("[agent-artifacts] report refused", JSON.stringify({
|
|
91
|
+
reason: "report_failed",
|
|
92
|
+
http_status: response.status,
|
|
93
|
+
server_reason: serverReason ?? "none",
|
|
94
|
+
artifact_count: artifactCount,
|
|
95
|
+
}));
|
|
96
|
+
return {
|
|
97
|
+
posted: false,
|
|
98
|
+
reason: `report_failed_http_${response.status}`,
|
|
99
|
+
recorded_count: 0,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
108
102
|
/**
|
|
109
103
|
* Did the route answer, or did something in front of it?
|
|
110
104
|
*
|
|
@@ -1,18 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* - `readPairedCollector` — prove the collector is installed and paired, with
|
|
6
|
-
* its own reason and retry command for each of the three ways a machine can
|
|
7
|
-
* fail to be ready. "sync failed" with no next step is what turns a
|
|
8
|
-
* five-second fix into a support thread.
|
|
9
|
-
* - `buildLocalAmbientEnvelope` — find the work context for this repo, run the
|
|
10
|
-
* source collectors, sanitize what came back and assemble the one event
|
|
11
|
-
* (`upload-envelope-event.ts`), then parse the envelope against its schema.
|
|
12
|
-
* - The work-context and provenance helpers (`makeUploadWorkContext`,
|
|
13
|
-
* `makeCollectorProvenance`, `safeRepoLabel`) are also called directly by
|
|
14
|
-
* `upload-session-reports.ts`, which builds its own provenance without a
|
|
15
|
-
* full envelope.
|
|
2
|
+
* Build a validated envelope only after proving the collector is paired.
|
|
3
|
+
* Read the workspace context, describe transcript images, collect sources,
|
|
4
|
+
* then sanitize and assemble the event. Blockers retain their retry commands.
|
|
16
5
|
*/
|
|
17
6
|
import { priorSnapshotsFromCursor, } from "./adapters/raw-evidence-snapshot-supersede.js";
|
|
18
7
|
import { TelemetryIngestEnvelopeSchema, } from "@bli-cockpit/telemetry-core";
|
|
@@ -82,18 +71,7 @@ export async function buildLocalAmbientEnvelope(options = {}) {
|
|
|
82
71
|
const collector = await readPairedCollector(paths);
|
|
83
72
|
const { config, sessionFile, session } = collector;
|
|
84
73
|
const repoRoot = path.resolve(options.repoRoot ?? process.cwd());
|
|
85
|
-
const activeContext = await
|
|
86
|
-
// The operator is told "run `cockpit start`", which is right when the file
|
|
87
|
-
// is simply absent and wrong when it exists and will not parse — the same
|
|
88
|
-
// advice, forever, on a machine that has already run it (BLI-3238).
|
|
89
|
-
if (!isMissingFileFailure(error)) {
|
|
90
|
-
console.error("[upload-envelope] work context present but unreadable, reporting it as missing", JSON.stringify({
|
|
91
|
-
reason: "missing_context",
|
|
92
|
-
...describeError(error),
|
|
93
|
-
}));
|
|
94
|
-
}
|
|
95
|
-
throw new LocalUploadBlockedError("missing_context", "Active work context missing. Run `cockpit start --workspace \"$PWD\"` before `cockpit sync`.", "cockpit start --workspace \"$PWD\"");
|
|
96
|
-
});
|
|
74
|
+
const activeContext = await readUploadWorkContext(paths, repoRoot);
|
|
97
75
|
const repoLabel = safeRepoLabel(activeContext.repo_label ?? repoRoot);
|
|
98
76
|
const uploadContext = makeUploadWorkContext({
|
|
99
77
|
activeContext,
|
|
@@ -110,39 +88,11 @@ export async function buildLocalAmbientEnvelope(options = {}) {
|
|
|
110
88
|
deviceToken: sessionFile.device_token,
|
|
111
89
|
fetchImpl: options.fetch,
|
|
112
90
|
});
|
|
113
|
-
const sourceCollection = await
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
sessionId: session.session_id,
|
|
119
|
-
workContextId: uploadContext.work_context_id,
|
|
120
|
-
activeWorkContext: activeContext,
|
|
121
|
-
rawEvidenceStateDir: paths.state_dir,
|
|
122
|
-
rawEvidenceSessionsDirs: defaultCodexSessionDirs(paths.home_dir),
|
|
123
|
-
claudeProjectsDir: path.join(paths.home_dir, ".claude", "projects"),
|
|
124
|
-
rawEvidenceIncludeCodexJsonl: options.rawEvidenceIncludeCodexJsonl,
|
|
125
|
-
rawEvidenceIncludeClaudeJsonl: options.rawEvidenceIncludeClaudeJsonl,
|
|
126
|
-
rawEvidenceCodexSessionFiles: options.codexSessionFiles,
|
|
127
|
-
rawEvidenceCodexAttributionScan: options.codexAttributionScan,
|
|
128
|
-
rawEvidenceClaudeSessionFiles: options.claudeSessionFiles,
|
|
129
|
-
rawEvidenceClaudeAttributionScan: options.claudeAttributionScan,
|
|
130
|
-
rawEvidenceSkipContentHashes: reusableContentHashes(options, {
|
|
131
|
-
operatorId: session.operator_id,
|
|
132
|
-
workContextId: uploadContext.work_context_id,
|
|
133
|
-
}),
|
|
134
|
-
rawEvidencePriorSnapshots: priorSnapshotsFromCursor(transcriptCursorObjects(options.cursorObjects), (objectKey) => rawEvidenceObjectKeyBelongsToWorkContext(objectKey, {
|
|
135
|
-
operatorId: session.operator_id,
|
|
136
|
-
workContextId: uploadContext.work_context_id,
|
|
137
|
-
})),
|
|
138
|
-
rawEvidenceStubReoffers: options.rawEvidenceStubReoffers,
|
|
139
|
-
rawEvidenceByteBudget: options.rawEvidenceByteBudget,
|
|
140
|
-
rawEvidenceObjectBudget: options.rawEvidenceObjectBudget,
|
|
141
|
-
rawEvidenceBudget: options.rawEvidenceBudget,
|
|
142
|
-
rawEvidenceImages,
|
|
143
|
-
rawEvidenceDeliveryMode: options.evidenceDeliveryMode,
|
|
144
|
-
now,
|
|
145
|
-
});
|
|
91
|
+
const sourceCollection = await collectEnvelopeSources(options, paths, repoRoot, uploadContext, session, activeContext, rawEvidenceImages, now);
|
|
92
|
+
return assembleLocalEnvelope(options, collector, uploadContext, repoLabel, sourceCollection, now);
|
|
93
|
+
}
|
|
94
|
+
function assembleLocalEnvelope(options, collector, uploadContext, repoLabel, sourceCollection, now) {
|
|
95
|
+
const { config, sessionFile, session } = collector;
|
|
146
96
|
const binding = sourceCollection.binding;
|
|
147
97
|
const ticketBinding = selectedTicketBindingCandidate(binding);
|
|
148
98
|
const uploadWorkContext = {
|
|
@@ -191,6 +141,57 @@ export async function buildLocalAmbientEnvelope(options = {}) {
|
|
|
191
141
|
raw_evidence_facts: sourceCollection.facts.raw_evidence,
|
|
192
142
|
};
|
|
193
143
|
}
|
|
144
|
+
async function collectEnvelopeSources(options, paths, repoRoot, uploadContext, session, activeContext, rawEvidenceImages, now) {
|
|
145
|
+
const sourceCollection = await runLocalSourceCollectors({
|
|
146
|
+
repoRoot,
|
|
147
|
+
branch: uploadContext.branch,
|
|
148
|
+
operatorId: session.operator_id,
|
|
149
|
+
operatorLabel: session.email ?? session.auth_subject_id,
|
|
150
|
+
sessionId: session.session_id,
|
|
151
|
+
workContextId: uploadContext.work_context_id,
|
|
152
|
+
activeWorkContext: activeContext,
|
|
153
|
+
rawEvidenceStateDir: paths.state_dir,
|
|
154
|
+
rawEvidenceSessionsDirs: defaultCodexSessionDirs(paths.home_dir),
|
|
155
|
+
claudeProjectsDir: path.join(paths.home_dir, ".claude", "projects"),
|
|
156
|
+
rawEvidenceIncludeCodexJsonl: options.rawEvidenceIncludeCodexJsonl,
|
|
157
|
+
rawEvidenceIncludeClaudeJsonl: options.rawEvidenceIncludeClaudeJsonl,
|
|
158
|
+
rawEvidenceCodexSessionFiles: options.codexSessionFiles,
|
|
159
|
+
rawEvidenceCodexAttributionScan: options.codexAttributionScan,
|
|
160
|
+
rawEvidenceClaudeSessionFiles: options.claudeSessionFiles,
|
|
161
|
+
rawEvidenceClaudeAttributionScan: options.claudeAttributionScan,
|
|
162
|
+
rawEvidenceSkipContentHashes: reusableContentHashes(options, {
|
|
163
|
+
operatorId: session.operator_id,
|
|
164
|
+
workContextId: uploadContext.work_context_id,
|
|
165
|
+
}),
|
|
166
|
+
rawEvidencePriorSnapshots: priorSnapshotsFromCursor(transcriptCursorObjects(options.cursorObjects), (objectKey) => rawEvidenceObjectKeyBelongsToWorkContext(objectKey, {
|
|
167
|
+
operatorId: session.operator_id,
|
|
168
|
+
workContextId: uploadContext.work_context_id,
|
|
169
|
+
})),
|
|
170
|
+
rawEvidenceStubReoffers: options.rawEvidenceStubReoffers,
|
|
171
|
+
rawEvidenceByteBudget: options.rawEvidenceByteBudget,
|
|
172
|
+
rawEvidenceObjectBudget: options.rawEvidenceObjectBudget,
|
|
173
|
+
rawEvidenceBudget: options.rawEvidenceBudget,
|
|
174
|
+
rawEvidenceImages,
|
|
175
|
+
rawEvidenceDeliveryMode: options.evidenceDeliveryMode,
|
|
176
|
+
now,
|
|
177
|
+
});
|
|
178
|
+
return sourceCollection;
|
|
179
|
+
}
|
|
180
|
+
async function readUploadWorkContext(paths, repoRoot) {
|
|
181
|
+
const activeContext = await readLocalWorkContextForRepo(paths, repoRoot).catch((error) => {
|
|
182
|
+
// The operator is told "run `cockpit start`", which is right when the file
|
|
183
|
+
// is simply absent and wrong when it exists and will not parse — the same
|
|
184
|
+
// advice, forever, on a machine that has already run it (BLI-3238).
|
|
185
|
+
if (!isMissingFileFailure(error)) {
|
|
186
|
+
console.error("[upload-envelope] work context present but unreadable, reporting it as missing", JSON.stringify({
|
|
187
|
+
reason: "missing_context",
|
|
188
|
+
...describeError(error),
|
|
189
|
+
}));
|
|
190
|
+
}
|
|
191
|
+
throw new LocalUploadBlockedError("missing_context", "Active work context missing. Run `cockpit start --workspace \"$PWD\"` before `cockpit sync`.", "cockpit start --workspace \"$PWD\"");
|
|
192
|
+
});
|
|
193
|
+
return activeContext;
|
|
194
|
+
}
|
|
194
195
|
/**
|
|
195
196
|
* Content hashes whose bytes are already durable and may be skipped this sync.
|
|
196
197
|
*
|
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
import { EvidenceCompletenessPayloadSchema, } from "@bli-cockpit/telemetry-core";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* Ingest refuses pointers whose objects never became durable, so failed
|
|
6
|
-
* uploads are pruned from the envelope instead of failing the whole sync.
|
|
7
|
-
* Successful upload responses can also carry server-side sanitized hash and
|
|
8
|
-
* redaction metadata; apply those before ingest so refs describe the bytes
|
|
9
|
-
* actually stored in the durable bucket.
|
|
3
|
+
* Prune failed pointers before ingest and apply server-sanitized metadata so
|
|
4
|
+
* refs describe the bytes actually stored. Any successful duplicate is durable.
|
|
10
5
|
*/
|
|
11
6
|
export function applyRawEvidenceUploadOutcomes(envelope, outcomes) {
|
|
12
7
|
// Content-addressed keys mean one pointer id can carry several outcomes
|
|
@@ -27,39 +22,42 @@ export function applyRawEvidenceUploadOutcomes(envelope, outcomes) {
|
|
|
27
22
|
return envelope;
|
|
28
23
|
return {
|
|
29
24
|
...envelope,
|
|
30
|
-
events: envelope.events.map((event) => (
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
: event.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
25
|
+
events: envelope.events.map((event) => reconcileEventEvidence(event, failedPointerIds, failedOutcomes, durablePointers)),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function reconcileEventEvidence(event, failedPointerIds, failedOutcomes, durablePointers) {
|
|
29
|
+
return {
|
|
30
|
+
...event,
|
|
31
|
+
metrics: failedPointerIds.size > 0
|
|
32
|
+
? {
|
|
33
|
+
...event.metrics,
|
|
34
|
+
evidence_failed_count: (event.metrics["evidence_failed_count"] ?? 0) +
|
|
35
|
+
failedOutcomes.length,
|
|
36
|
+
}
|
|
37
|
+
: event.metrics,
|
|
38
|
+
attributes: failedPointerIds.size > 0 && event.evidence_completeness
|
|
39
|
+
? {
|
|
40
|
+
...event.attributes,
|
|
41
|
+
evidence_completeness_schema_version: event.evidence_completeness.schema_version,
|
|
42
|
+
evidence_completeness_status: "partial",
|
|
43
|
+
evidence_incomplete: true,
|
|
44
|
+
}
|
|
45
|
+
: event.attributes,
|
|
46
|
+
evidence_completeness: failedPointerIds.size > 0 && event.evidence_completeness
|
|
47
|
+
? markCompletenessUploadFailures(event.evidence_completeness, failedOutcomes)
|
|
48
|
+
: event.evidence_completeness,
|
|
49
|
+
raw_evidence_pointers: event.raw_evidence_pointers.flatMap((pointer) => {
|
|
50
|
+
const pointerId = pointer.raw_evidence_pointer_id;
|
|
51
|
+
if (failedPointerIds.has(pointerId))
|
|
52
|
+
return [];
|
|
53
|
+
return [durablePointers.get(pointerId) ?? pointer];
|
|
54
|
+
}),
|
|
55
|
+
redaction: failedPointerIds.size > 0
|
|
56
|
+
? {
|
|
57
|
+
...event.redaction,
|
|
58
|
+
raw_evidence_pointer_ids: event.redaction.raw_evidence_pointer_ids.filter((pointerId) => !failedPointerIds.has(pointerId)),
|
|
59
|
+
}
|
|
60
|
+
: event.redaction,
|
|
63
61
|
};
|
|
64
62
|
}
|
|
65
63
|
/**
|
|
@@ -98,6 +96,25 @@ function markCompletenessUploadFailures(completeness, failedOutcomes) {
|
|
|
98
96
|
failed_count: existing.failed_count + count,
|
|
99
97
|
};
|
|
100
98
|
}
|
|
99
|
+
const failureReasons = mergeUploadFailureReasons(completeness, failureCounts);
|
|
100
|
+
return EvidenceCompletenessPayloadSchema.parse({
|
|
101
|
+
...completeness,
|
|
102
|
+
status: "partial",
|
|
103
|
+
source_counts: sourceCounts,
|
|
104
|
+
totals: {
|
|
105
|
+
...completeness.totals,
|
|
106
|
+
failed_count: completeness.totals.failed_count + totalFailures,
|
|
107
|
+
},
|
|
108
|
+
failure_reasons: failureReasons.sort((a, b) => `${a.source}:${a.reason}`.localeCompare(`${b.source}:${b.reason}`)),
|
|
109
|
+
notes: [
|
|
110
|
+
...new Set([
|
|
111
|
+
...completeness.notes,
|
|
112
|
+
"Some collected evidence did not become durable; downstream analysis should lower confidence.",
|
|
113
|
+
]),
|
|
114
|
+
],
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
function mergeUploadFailureReasons(completeness, failureCounts) {
|
|
101
118
|
const failureReasons = [...completeness.failure_reasons];
|
|
102
119
|
for (const [source, count] of failureCounts) {
|
|
103
120
|
const reason = "upload_failed";
|
|
@@ -115,20 +132,5 @@ function markCompletenessUploadFailures(completeness, failedOutcomes) {
|
|
|
115
132
|
}
|
|
116
133
|
}
|
|
117
134
|
}
|
|
118
|
-
return
|
|
119
|
-
...completeness,
|
|
120
|
-
status: "partial",
|
|
121
|
-
source_counts: sourceCounts,
|
|
122
|
-
totals: {
|
|
123
|
-
...completeness.totals,
|
|
124
|
-
failed_count: completeness.totals.failed_count + totalFailures,
|
|
125
|
-
},
|
|
126
|
-
failure_reasons: failureReasons.sort((a, b) => `${a.source}:${a.reason}`.localeCompare(`${b.source}:${b.reason}`)),
|
|
127
|
-
notes: [
|
|
128
|
-
...new Set([
|
|
129
|
-
...completeness.notes,
|
|
130
|
-
"Some collected evidence did not become durable; downstream analysis should lower confidence.",
|
|
131
|
-
]),
|
|
132
|
-
],
|
|
133
|
-
});
|
|
135
|
+
return failureReasons;
|
|
134
136
|
}
|