@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
|
@@ -41,54 +41,20 @@ export async function runBriefEdit(command, io) {
|
|
|
41
41
|
const log = (line) => writeLine(io.stderr, line);
|
|
42
42
|
const startedAt = Date.now();
|
|
43
43
|
// ── 1. The page, as a document ────────────────────────────────────────────
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
deviceToken: session.device_token,
|
|
48
|
-
fetch: io.fetch,
|
|
49
|
-
method: "GET",
|
|
50
|
-
label: "brief:edit",
|
|
51
|
-
timeoutMs: REQUEST_DEADLINE_MS,
|
|
52
|
-
log,
|
|
53
|
-
});
|
|
54
|
-
if (!read.ok)
|
|
55
|
-
return fail(command, io, read.reason, read.detail);
|
|
56
|
-
const brief = read.body;
|
|
57
|
-
const personId = brief.page?.personId;
|
|
58
|
-
const original = brief.editable?.document;
|
|
59
|
-
const lines = (brief.editable?.lines ?? []).filter((line) => typeof line.claimId === "string" && typeof line.text === "string");
|
|
60
|
-
if (!brief.ok || !personId || typeof original !== "string" || lines.length === 0) {
|
|
61
|
-
return fail(command, io, brief.error ?? "no_page", brief.reply ?? "There is no page to edit.");
|
|
62
|
-
}
|
|
44
|
+
const page = await readEditablePage(command, io, session, dashboardUrl, log);
|
|
45
|
+
if (!page.ok)
|
|
46
|
+
return fail(command, io, page.reason, page.detail);
|
|
63
47
|
// ── 2. Their editor, or their stdin ───────────────────────────────────────
|
|
64
|
-
const edited = await collectEditedDocument(io, original, log);
|
|
48
|
+
const edited = await collectEditedDocument(io, page.original, log);
|
|
65
49
|
if (!edited.ok)
|
|
66
50
|
return fail(command, io, edited.reason, edited.detail);
|
|
67
51
|
// ── 3. The diff, here ─────────────────────────────────────────────────────
|
|
68
|
-
const diff = diffDocument(lines, edited.text);
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
return fail(command, io, "deleted_claim", `The line [${diff.deleted[0]}] is missing from what you saved. Put it back — a line you delete ` +
|
|
75
|
-
"reads as no opinion at all, not as a sentence to remove, and Tower cannot tell those apart. " +
|
|
76
|
-
"Nothing has been sent.");
|
|
77
|
-
}
|
|
78
|
-
if (diff.edits.length === 0) {
|
|
79
|
-
const styled = colorEnabled(io);
|
|
80
|
-
if (command.json) {
|
|
81
|
-
writeLine(io.stdout, JSON.stringify({ ok: true, saved: 0, reason: "nothing_changed" }));
|
|
82
|
-
}
|
|
83
|
-
else {
|
|
84
|
-
writeLine(io.stdout, dim("Nothing changed, so nothing was written.", styled));
|
|
85
|
-
}
|
|
86
|
-
writeLine(io.stderr, `[brief edit] nothing changed ${JSON.stringify({
|
|
87
|
-
lines: lines.length,
|
|
88
|
-
elapsed_ms: Date.now() - startedAt,
|
|
89
|
-
})}`);
|
|
90
|
-
return 0;
|
|
91
|
-
}
|
|
52
|
+
const diff = diffDocument(page.lines, edited.text);
|
|
53
|
+
const refusal = refuseBadDiff(diff);
|
|
54
|
+
if (refusal)
|
|
55
|
+
return fail(command, io, refusal.reason, refusal.detail);
|
|
56
|
+
if (diff.edits.length === 0)
|
|
57
|
+
return reportNothingChanged(command, io, page.lines.length, startedAt);
|
|
92
58
|
// ── 4. The same door the browser's save posts to ──────────────────────────
|
|
93
59
|
const saved = await towerJsonRequest({
|
|
94
60
|
dashboardUrl,
|
|
@@ -99,8 +65,8 @@ export async function runBriefEdit(command, io) {
|
|
|
99
65
|
timeoutMs: REQUEST_DEADLINE_MS,
|
|
100
66
|
log,
|
|
101
67
|
body: {
|
|
102
|
-
personId,
|
|
103
|
-
pageId:
|
|
68
|
+
personId: page.personId,
|
|
69
|
+
pageId: page.pageId,
|
|
104
70
|
edits: diff.edits,
|
|
105
71
|
...(command.reason ? { reason: command.reason } : {}),
|
|
106
72
|
},
|
|
@@ -122,6 +88,65 @@ export async function runBriefEdit(command, io) {
|
|
|
122
88
|
})}`);
|
|
123
89
|
return 0;
|
|
124
90
|
}
|
|
91
|
+
async function readEditablePage(command, io, session, dashboardUrl, log) {
|
|
92
|
+
const read = await towerJsonRequest({
|
|
93
|
+
dashboardUrl,
|
|
94
|
+
path: `/api/jarvis/brief${briefQuery(command)}`,
|
|
95
|
+
deviceToken: session.device_token,
|
|
96
|
+
fetch: io.fetch,
|
|
97
|
+
method: "GET",
|
|
98
|
+
label: "brief:edit",
|
|
99
|
+
timeoutMs: REQUEST_DEADLINE_MS,
|
|
100
|
+
log,
|
|
101
|
+
});
|
|
102
|
+
if (!read.ok)
|
|
103
|
+
return { ok: false, reason: read.reason, detail: read.detail };
|
|
104
|
+
const brief = read.body;
|
|
105
|
+
const personId = brief.page?.personId;
|
|
106
|
+
const original = brief.editable?.document;
|
|
107
|
+
const lines = (brief.editable?.lines ?? []).filter((line) => typeof line.claimId === "string" && typeof line.text === "string");
|
|
108
|
+
if (!brief.ok || !personId || typeof original !== "string" || lines.length === 0) {
|
|
109
|
+
return {
|
|
110
|
+
ok: false,
|
|
111
|
+
reason: brief.error ?? "no_page",
|
|
112
|
+
detail: brief.reply ?? "There is no page to edit.",
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
return { ok: true, personId, pageId: brief.page?.pageId ?? null, original, lines };
|
|
116
|
+
}
|
|
117
|
+
/** The refusal for a diff that invented or deleted a claim id, or null when the diff may be sent. */
|
|
118
|
+
function refuseBadDiff(diff) {
|
|
119
|
+
if (diff.invented.length > 0) {
|
|
120
|
+
return {
|
|
121
|
+
reason: "invented_claim",
|
|
122
|
+
detail: `The page has no line called “${diff.invented[0]}”. Change the words after an id, never the id ` +
|
|
123
|
+
"itself — nothing has been sent to Tower.",
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
if (diff.deleted.length > 0) {
|
|
127
|
+
return {
|
|
128
|
+
reason: "deleted_claim",
|
|
129
|
+
detail: `The line [${diff.deleted[0]}] is missing from what you saved. Put it back — a line you delete ` +
|
|
130
|
+
"reads as no opinion at all, not as a sentence to remove, and Tower cannot tell those apart. " +
|
|
131
|
+
"Nothing has been sent.",
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
function reportNothingChanged(command, io, lineCount, startedAt) {
|
|
137
|
+
const styled = colorEnabled(io);
|
|
138
|
+
if (command.json) {
|
|
139
|
+
writeLine(io.stdout, JSON.stringify({ ok: true, saved: 0, reason: "nothing_changed" }));
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
writeLine(io.stdout, dim("Nothing changed, so nothing was written.", styled));
|
|
143
|
+
}
|
|
144
|
+
writeLine(io.stderr, `[brief edit] nothing changed ${JSON.stringify({
|
|
145
|
+
lines: lineCount,
|
|
146
|
+
elapsed_ms: Date.now() - startedAt,
|
|
147
|
+
})}`);
|
|
148
|
+
return 0;
|
|
149
|
+
}
|
|
125
150
|
function briefQuery(command) {
|
|
126
151
|
const params = new URLSearchParams({ editable: "1" });
|
|
127
152
|
if (command.subject)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { askAgentDoor, emitAgentDoor, failAgentDoor } from "./agent-door.js";
|
|
2
|
+
import { writeLine } from "./cli-io.js";
|
|
3
|
+
export async function runCalDetail(command, door) {
|
|
4
|
+
const query = new URLSearchParams({ tz: command.timeZone ?? Intl.DateTimeFormat().resolvedOptions().timeZone });
|
|
5
|
+
if (command.occurrenceStart)
|
|
6
|
+
query.set("occurrence_start", command.occurrenceStart);
|
|
7
|
+
const path = `/api/cal/events/${encodeURIComponent(command.subjectId)}${command.action === "export" ? "/export" : ""}?${query}`;
|
|
8
|
+
const answer = await askAgentDoor(door, { method: "GET", path, label: `cal ${command.action}`, timeoutMs: 30000 });
|
|
9
|
+
if (!answer.ok)
|
|
10
|
+
return failAgentDoor(door, "[cal cli]", answer.reason, answer.detail);
|
|
11
|
+
if (door.json)
|
|
12
|
+
return emitAgentDoor(door, answer.body);
|
|
13
|
+
if (command.action === "export") {
|
|
14
|
+
writeLine(door.io.stdout, String(answer.body.content));
|
|
15
|
+
return 0;
|
|
16
|
+
}
|
|
17
|
+
const event = answer.body.event;
|
|
18
|
+
const zone = command.timeZone ?? Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
19
|
+
const date = new Date(event.starts_at).toLocaleDateString("en-CA", { timeZone: event.all_day ? "UTC" : zone });
|
|
20
|
+
const clock = (value) => new Intl.DateTimeFormat("en-GB", { timeZone: zone, hour: "2-digit", minute: "2-digit", hourCycle: "h23" }).format(new Date(value));
|
|
21
|
+
writeLine(door.io.stdout, `${event.title} · ${date} · ${event.all_day ? "All day" : `${clock(event.starts_at)}–${clock(event.ends_at)}`} · ${zone} · ${event.calendar_name}`);
|
|
22
|
+
writeLine(door.io.stdout, event.recurrence_summary);
|
|
23
|
+
if (event.recurrence_coverage_note)
|
|
24
|
+
writeLine(door.io.stdout, event.recurrence_coverage_note);
|
|
25
|
+
writeLine(door.io.stdout, `Organizer: ${event.organizer_detail?.name ?? "Unknown"}`);
|
|
26
|
+
for (const guest of event.guests)
|
|
27
|
+
writeLine(door.io.stdout, `${guest.name} · ${guest.rsvp}${guest.optional ? " · Optional" : ""}`);
|
|
28
|
+
if (event.meeting_url)
|
|
29
|
+
writeLine(door.io.stdout, `Join: ${event.meeting_url}`);
|
|
30
|
+
if (event.location)
|
|
31
|
+
writeLine(door.io.stdout, `Location: ${event.location}`);
|
|
32
|
+
if (event.capabilities.reason)
|
|
33
|
+
writeLine(door.io.stdout, event.capabilities.reason);
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
@@ -1,29 +1,51 @@
|
|
|
1
1
|
import { askAgentDoor, emitAgentDoor, failAgentDoor } from "./agent-door.js";
|
|
2
2
|
import { readPipedText, isInteractiveStdin, writeLine } from "./cli-io.js";
|
|
3
|
+
const FAIL_TAG = "[cal cli]";
|
|
4
|
+
const REQUEST_TIMEOUT_MS = 30000;
|
|
3
5
|
export async function runCalOverlay(command, door) {
|
|
4
|
-
if (command.action === "calendar-change")
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
return failAgentDoor(door, "[cal cli]", "invalid_body", "Pipe JSON preferences or use --color '#4285f4' (provider resets).");
|
|
15
|
-
}
|
|
6
|
+
if (command.action === "calendar-change")
|
|
7
|
+
return changeCalendarPreferences(command, door);
|
|
8
|
+
return showConflicts(command, door);
|
|
9
|
+
}
|
|
10
|
+
async function changeCalendarPreferences(command, door) {
|
|
11
|
+
let body = command.color ? { color_override: command.color === "provider" ? null : command.color } : {};
|
|
12
|
+
if (!command.color && !isInteractiveStdin(door.io)) {
|
|
13
|
+
const piped = await readPipedPreferences(door);
|
|
14
|
+
if (piped === null) {
|
|
15
|
+
return failAgentDoor(door, FAIL_TAG, "invalid_body", "Pipe JSON preferences or use --color '#4285f4' (provider resets).");
|
|
16
16
|
}
|
|
17
|
-
|
|
18
|
-
return failAgentDoor(door, "[cal cli]", "invalid_body", "Use --color '#4285f4', --color provider, or pipe JSON with color_override/selected.");
|
|
19
|
-
const answer = await askAgentDoor(door, { method: "PATCH", path: `/api/cal/calendars/${encodeURIComponent(command.subjectId)}`, body, label: "cal calendar-change", timeoutMs: 30000 });
|
|
20
|
-
if (!answer.ok)
|
|
21
|
-
return failAgentDoor(door, "[cal cli]", answer.reason, answer.detail);
|
|
22
|
-
if (door.json)
|
|
23
|
-
return emitAgentDoor(door, answer.body);
|
|
24
|
-
writeLine(door.io.stdout, "Calendar preferences saved. Provider sync preserves your colour override.");
|
|
25
|
-
return 0;
|
|
17
|
+
body = piped;
|
|
26
18
|
}
|
|
19
|
+
if (!Object.keys(body).length) {
|
|
20
|
+
return failAgentDoor(door, FAIL_TAG, "invalid_body", "Use --color '#4285f4', --color provider, or pipe JSON with color_override/selected.");
|
|
21
|
+
}
|
|
22
|
+
const answer = await askAgentDoor(door, {
|
|
23
|
+
method: "PATCH",
|
|
24
|
+
path: `/api/cal/calendars/${encodeURIComponent(command.subjectId)}`,
|
|
25
|
+
body,
|
|
26
|
+
label: "cal calendar-change",
|
|
27
|
+
timeoutMs: REQUEST_TIMEOUT_MS,
|
|
28
|
+
});
|
|
29
|
+
if (!answer.ok)
|
|
30
|
+
return failAgentDoor(door, FAIL_TAG, answer.reason, answer.detail);
|
|
31
|
+
if (door.json)
|
|
32
|
+
return emitAgentDoor(door, answer.body);
|
|
33
|
+
writeLine(door.io.stdout, "Calendar preferences saved. Provider sync preserves your colour override.");
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
/** Piped JSON preferences, or null when the pipe is unreadable or not a JSON object. */
|
|
37
|
+
async function readPipedPreferences(door) {
|
|
38
|
+
try {
|
|
39
|
+
const parsed = JSON.parse(await readPipedText(door.io.stdin, { maxChars: 4000, overflowMessage: "Preferences are too large." }));
|
|
40
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
41
|
+
return null;
|
|
42
|
+
return parsed;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async function showConflicts(command, door) {
|
|
27
49
|
const query = new URLSearchParams({ tz: command.timeZone ?? Intl.DateTimeFormat().resolvedOptions().timeZone });
|
|
28
50
|
if (command.from)
|
|
29
51
|
query.set("from", command.from);
|
|
@@ -33,31 +55,52 @@ export async function runCalOverlay(command, door) {
|
|
|
33
55
|
query.set("offset", String(command.offset));
|
|
34
56
|
if (command.includeDeselected)
|
|
35
57
|
query.set("all", "true");
|
|
36
|
-
const answer = await askAgentDoor(door, { method: "GET", path: `/api/cal/conflicts?${query}`, label: "cal conflicts", timeoutMs:
|
|
58
|
+
const answer = await askAgentDoor(door, { method: "GET", path: `/api/cal/conflicts?${query}`, label: "cal conflicts", timeoutMs: REQUEST_TIMEOUT_MS });
|
|
37
59
|
if (!answer.ok)
|
|
38
|
-
return failAgentDoor(door,
|
|
60
|
+
return failAgentDoor(door, FAIL_TAG, answer.reason, answer.detail);
|
|
39
61
|
if (door.json)
|
|
40
62
|
return emitAgentDoor(door, answer.body);
|
|
41
63
|
const result = answer.body;
|
|
42
64
|
const zone = result.window?.time_zone ?? command.timeZone ?? Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
65
|
+
writeConflictGroups(door, result, zone);
|
|
66
|
+
writeCoverage(door, result);
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
function writeConflictGroups(door, result, zone) {
|
|
43
70
|
writeLine(door.io.stdout, `${result.groups.length} double-bookings · own calendars`);
|
|
44
71
|
for (const group of result.groups) {
|
|
45
72
|
const parts = new Intl.DateTimeFormat("en-GB", { timeZone: zone, weekday: "short", day: "numeric", month: "short", hour: "2-digit", minute: "2-digit", hourCycle: "h23" }).formatToParts(new Date(group.from));
|
|
46
73
|
const part = (name) => parts.find(item => item.type === name)?.value;
|
|
47
|
-
|
|
74
|
+
const meetingTitles = group.meetings.map(meeting => `${meeting.event.title} · ${meeting.event.calendar_name}`).join(" / ");
|
|
75
|
+
writeLine(door.io.stdout, `${part("weekday")} ${part("day")} ${part("month")} ${part("hour")}:${part("minute")}: ${meetingTitles}`);
|
|
48
76
|
}
|
|
49
|
-
|
|
77
|
+
}
|
|
78
|
+
function describeSyncAge(checked) {
|
|
50
79
|
const oldest = Math.min(...checked.map(calendar => calendar.lastSyncAt ? Date.parse(calendar.lastSyncAt) : NaN));
|
|
51
80
|
const minutes = Math.max(0, Math.floor((Date.now() - oldest) / 60000));
|
|
52
|
-
|
|
53
|
-
|
|
81
|
+
if (!checked.length)
|
|
82
|
+
return "none checked";
|
|
83
|
+
if (!Number.isFinite(oldest))
|
|
84
|
+
return "sync age unknown";
|
|
85
|
+
if (minutes < 1)
|
|
86
|
+
return "just now";
|
|
87
|
+
if (minutes < 60)
|
|
88
|
+
return `${minutes} min ago`;
|
|
89
|
+
if (minutes < 1440)
|
|
90
|
+
return `${Math.floor(minutes / 60)} h ago`;
|
|
91
|
+
return `${Math.floor(minutes / 1440)} days ago`;
|
|
92
|
+
}
|
|
93
|
+
function writeCoverage(door, result) {
|
|
94
|
+
const checked = result.coverage.calendars.filter(calendar => calendar.checked);
|
|
95
|
+
writeLine(door.io.stdout, `Checked ${checked.length} calendar${checked.length === 1 ? "" : "s"} · ${describeSyncAge(checked)}`);
|
|
54
96
|
for (const calendar of result.coverage.calendars.filter(calendar => calendar.hidden || !calendar.checked)) {
|
|
55
97
|
writeLine(door.io.stdout, `${calendar.name}: ${calendar.hidden ? "hidden; " : ""}${calendar.checked ? "included by request" : `not checked (${(calendar.excludedReason ?? "unavailable").replaceAll("_", " ")})`}`);
|
|
56
98
|
}
|
|
57
|
-
for (const account of result.coverage.accounts ?? [])
|
|
99
|
+
for (const account of result.coverage.accounts ?? []) {
|
|
58
100
|
if (!account.calendars)
|
|
59
101
|
writeLine(door.io.stdout, `${account.label}: no discovered calendars (${account.status.replaceAll("_", " ")})`);
|
|
60
|
-
|
|
102
|
+
}
|
|
103
|
+
if (result.coverage.incomplete || result.coverage.truncated || result.coverage.notes.length) {
|
|
61
104
|
writeLine(door.io.stdout, "Incomplete window. This check cannot confirm free time.");
|
|
62
|
-
|
|
105
|
+
}
|
|
63
106
|
}
|
package/dist/commands/cal.js
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
*/
|
|
23
23
|
import { askAgentDoor, emitAgentDoor, failAgentDoor, openAgentDoor, } from "./agent-door.js";
|
|
24
24
|
import { runCalAddIcal, runCalDetach, runCalShare, runCalSync } from "./cal-calendars-write.js";
|
|
25
|
+
import { runCalDetail } from "./cal-detail.js";
|
|
25
26
|
import { runCalOverlay } from "./cal-overlay.js";
|
|
26
27
|
import { runCalDelete } from "./cal-delete.js";
|
|
27
28
|
import { runCalSyncStatus } from "./cal-sync-status.js";
|
|
@@ -33,6 +34,9 @@ const WRITE_DEADLINE_MS = 60_000;
|
|
|
33
34
|
export async function runCal(command, io) {
|
|
34
35
|
const door = await openAgentDoor("cal", command, io);
|
|
35
36
|
switch (command.action) {
|
|
37
|
+
case "event":
|
|
38
|
+
case "export":
|
|
39
|
+
return runCalDetail(command, door);
|
|
36
40
|
case "conflicts":
|
|
37
41
|
case "calendar-change":
|
|
38
42
|
return runCalOverlay(command, door);
|
|
@@ -95,6 +99,19 @@ function windowQuery(command) {
|
|
|
95
99
|
return query;
|
|
96
100
|
}
|
|
97
101
|
async function window(command, door) {
|
|
102
|
+
const answer = await askForWindow(command, door);
|
|
103
|
+
if (!answer.ok)
|
|
104
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
105
|
+
const body = answer.body;
|
|
106
|
+
if (door.json)
|
|
107
|
+
return emitAgentDoor(door, { ok: true, ...body, ...cacheCoverage(answer) });
|
|
108
|
+
if (answer.fromCache) {
|
|
109
|
+
writeLine(door.io.stderr, `(${cacheAgeLabel(answer.ageMs ?? 0)}, refreshing in the background)`);
|
|
110
|
+
}
|
|
111
|
+
writeWindow(body, door);
|
|
112
|
+
return 0;
|
|
113
|
+
}
|
|
114
|
+
async function askForWindow(command, door) {
|
|
98
115
|
// `--from`/`--to` beat a named window, on any of the three verbs: somebody
|
|
99
116
|
// who named two dates meant them.
|
|
100
117
|
const path = command.from && command.to ? "events" : command.action;
|
|
@@ -109,33 +126,27 @@ async function window(command, door) {
|
|
|
109
126
|
timeoutMs: READ_DEADLINE_MS,
|
|
110
127
|
warmRoute: cached,
|
|
111
128
|
});
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const body = answer.body;
|
|
127
|
-
if (door.json)
|
|
128
|
-
return emitAgentDoor(door, { ok: true, ...body, ...cacheCoverage(answer) });
|
|
129
|
-
if (answer.fromCache) {
|
|
130
|
-
writeLine(door.io.stderr, `(${cacheAgeLabel(answer.ageMs ?? 0)}, refreshing in the background)`);
|
|
131
|
-
}
|
|
129
|
+
if (!cached)
|
|
130
|
+
return { ...(await ask()), fromCache: false, ageMs: null };
|
|
131
|
+
return readThroughCache({
|
|
132
|
+
io: door.io,
|
|
133
|
+
homeDir: door.homeDir,
|
|
134
|
+
deviceToken: door.deviceToken,
|
|
135
|
+
surface: "cal",
|
|
136
|
+
label: "cal today",
|
|
137
|
+
path: apiPath,
|
|
138
|
+
json: door.json,
|
|
139
|
+
ask,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
function writeWindow(body, door) {
|
|
132
143
|
const zone = body.window?.time_zone ?? localTimeZone();
|
|
133
144
|
const events = body.events ?? [];
|
|
134
145
|
if (events.length === 0) {
|
|
135
146
|
writeLine(door.io.stdout, (body.calendars ?? []).length === 0
|
|
136
147
|
? "No calendars attached yet. `cockpit cal add-ical` for a personal one, or connect Google in the browser."
|
|
137
148
|
: `Nothing on ${describeWindow(body, zone)}.`);
|
|
138
|
-
return
|
|
149
|
+
return;
|
|
139
150
|
}
|
|
140
151
|
writeLine(door.io.stdout, describeWindow(body, zone));
|
|
141
152
|
writeLine(door.io.stdout, "");
|
|
@@ -156,7 +167,6 @@ async function window(command, door) {
|
|
|
156
167
|
for (const note of body.notes ?? []) {
|
|
157
168
|
writeLine(door.io.stdout, `Note: "${note.title}" repeats on a rule Tower does not expand (${note.detail.join(", ")}), so only its first occurrence is shown.`);
|
|
158
169
|
}
|
|
159
|
-
return 0;
|
|
160
170
|
}
|
|
161
171
|
function describeWindow(body, zone) {
|
|
162
172
|
if (!body.window)
|
|
@@ -297,6 +307,10 @@ async function create(command, door) {
|
|
|
297
307
|
const event = answer.body.event;
|
|
298
308
|
if (door.json)
|
|
299
309
|
return emitAgentDoor(door, { ok: true, event });
|
|
310
|
+
writeCreated(command, door, event);
|
|
311
|
+
return 0;
|
|
312
|
+
}
|
|
313
|
+
function writeCreated(command, door, event) {
|
|
300
314
|
const zone = command.timeZone ?? localTimeZone();
|
|
301
315
|
writeLine(door.io.stdout, `Created "${event?.title ?? command.title}" on ${event ? `${dayLabel(event.starts_at, zone)} ${clock(event.starts_at, zone)}` : "that calendar"}.`);
|
|
302
316
|
if (event?.event_id) {
|
|
@@ -317,7 +331,6 @@ async function create(command, door) {
|
|
|
317
331
|
if (command.attendees.length > 0) {
|
|
318
332
|
writeLine(door.io.stdout, `Invitations went to ${command.attendees.join(", ")} from Google.`);
|
|
319
333
|
}
|
|
320
|
-
return 0;
|
|
321
334
|
}
|
|
322
335
|
function meetingLinkText(event) {
|
|
323
336
|
const label = { meet: "Meet link", zoom: "Zoom link", teams: "Teams link" }[event.meeting_kind ?? ""] ?? "Meeting link";
|
package/dist/commands/clean.js
CHANGED
|
@@ -83,11 +83,14 @@ export async function runClean(command, io) {
|
|
|
83
83
|
writeLine(io.stdout, cleanJson(footprint, plan, vaults, pruned, reconciled));
|
|
84
84
|
return pruned.status === "fail" ? 1 : 0;
|
|
85
85
|
}
|
|
86
|
+
writeFreedSummary(io, pruned, removedVaults);
|
|
87
|
+
return pruned.status === "fail" ? 1 : 0;
|
|
88
|
+
}
|
|
89
|
+
function writeFreedSummary(io, pruned, removedVaults) {
|
|
86
90
|
writeLine(io.stdout, `Freed ${mb(pruned.deleted_bytes)} MB across ${pruned.deleted_files} file(s) and ${pruned.removed_packs} pack(s)${removedVaults > 0 ? `, plus ${removedVaults} evidence vault(s)` : ""}.`);
|
|
87
91
|
if (pruned.cap_blocked_by_uncommitted) {
|
|
88
92
|
writeLine(io.stdout, `Still ${mb(pruned.bytes_after)} MB over a ${mb(pruned.cap_bytes)} MB cap, held by ${pruned.cap_blocked_count} object(s) the upload ledger cannot vouch for. Run \`cockpit sync\` to try delivering them; nothing here will delete them.`);
|
|
89
93
|
}
|
|
90
|
-
return pruned.status === "fail" ? 1 : 0;
|
|
91
94
|
}
|
|
92
95
|
/** The bucket table a person reads before anything is deleted. */
|
|
93
96
|
export function cleanLines(footprint, plan, vaults, command, logArchives = []) {
|
|
@@ -166,8 +166,11 @@ export async function readDoc(command, door) {
|
|
|
166
166
|
writeDocumentBlock(door.io, result.document);
|
|
167
167
|
return 0;
|
|
168
168
|
}
|
|
169
|
-
|
|
170
|
-
|
|
169
|
+
return reportSeveralDocuments(door, results);
|
|
170
|
+
}
|
|
171
|
+
// Several refs: `{ ok, documents: [{ ref, ok, document|error }] }` — one bad
|
|
172
|
+
// ref never loses the rest of the batch.
|
|
173
|
+
function reportSeveralDocuments(door, results) {
|
|
171
174
|
const failed = results.filter((result) => !result.ok);
|
|
172
175
|
if (door.json) {
|
|
173
176
|
return emitAgentDoor(door, {
|
|
@@ -36,91 +36,108 @@ export async function checkBackfillState(context) {
|
|
|
36
36
|
*/
|
|
37
37
|
export async function fixBackfillState(context) {
|
|
38
38
|
const started = Date.now();
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
39
|
+
const run = {
|
|
40
|
+
started,
|
|
41
|
+
deadline: started + doctorBackfillBudgetSeconds(context, started) * 1000,
|
|
42
|
+
caughtUp: 0,
|
|
43
|
+
chunks: 0,
|
|
44
|
+
live: null,
|
|
45
|
+
remaining: null,
|
|
46
|
+
};
|
|
47
|
+
const stopProgress = startProgress((text) => context.io.stderr.write(text), progressReporter(run));
|
|
48
|
+
try {
|
|
49
|
+
return await catchUpChunkByChunk(context, run);
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
console.error("[doctor] backfill-complete failed", JSON.stringify({ reason: "backfill_threw", error_name: error instanceof Error ? error.name : "unknown" }));
|
|
53
|
+
return unfinishedRow(run, "backfill_threw", "backfill failed");
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
stopProgress();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function describeReceipt(run) {
|
|
60
|
+
return `caught up on ${run.caughtUp} old sessions in ${run.chunks} chunk${run.chunks === 1 ? "" : "s"} over ${clock(Date.now() - run.started)}`;
|
|
61
|
+
}
|
|
62
|
+
function unfinishedRow(run, code, reason) {
|
|
63
|
+
return {
|
|
64
|
+
...needsFix("backfill-complete", code, `${describeReceipt(run)}, ${run.remaining ?? "unknown"} still to go; ${reason}; run \`cockpit doctor\` again to continue`),
|
|
47
65
|
nextAction: "cockpit doctor",
|
|
48
|
-
}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function progressReporter(run) {
|
|
49
69
|
let lastLine = null;
|
|
50
|
-
|
|
70
|
+
return (elapsedMs) => {
|
|
71
|
+
const { live } = run;
|
|
51
72
|
const snapshot = {
|
|
52
|
-
caughtUp: live === null ? caughtUp : caughtUp + live.uploaded,
|
|
53
|
-
remaining: live?.remaining ?? remaining,
|
|
73
|
+
caughtUp: live === null ? run.caughtUp : run.caughtUp + live.uploaded,
|
|
74
|
+
remaining: live?.remaining ?? run.remaining,
|
|
54
75
|
};
|
|
55
76
|
console.error("[doctor] backfill-complete progress", JSON.stringify({
|
|
56
77
|
status: live === null ? "between_chunks" : "running", processed: live?.processed ?? null,
|
|
57
|
-
uploaded: live === null ? null : caughtUp + live.uploaded,
|
|
58
|
-
caught_up: snapshot.caughtUp, remaining: snapshot.remaining, chunks, elapsed_seconds: Math.floor(elapsedMs / 1000),
|
|
78
|
+
uploaded: live === null ? null : run.caughtUp + live.uploaded,
|
|
79
|
+
caught_up: snapshot.caughtUp, remaining: snapshot.remaining, chunks: run.chunks, elapsed_seconds: Math.floor(elapsedMs / 1000),
|
|
59
80
|
}));
|
|
60
81
|
const line = backfillProgressLine(snapshot, lastLine, elapsedMs);
|
|
61
82
|
lastLine = snapshot;
|
|
62
83
|
return line;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
chunks += 1;
|
|
72
|
-
const code = await runBackfillCommand({
|
|
73
|
-
homeDir: context.command.homeDir, repoRoot: context.command.repoRoot,
|
|
74
|
-
all: true, dryRun: false, yes: true, json: true,
|
|
75
|
-
deadlineMs: deadline,
|
|
76
|
-
// Doctor never waits for the collection lock (BLI-12872): the gate
|
|
77
|
-
// above looked at it, and a busy answer here is reported, not retried.
|
|
78
|
-
syncLockWaitMs: 0,
|
|
79
|
-
onProgress: (progress) => { live = progress; },
|
|
80
|
-
}, capture.io);
|
|
81
|
-
const parsed = parseDoctorBackfillJson(capture.stdout());
|
|
82
|
-
const counts = asRecord(parsed?.counts);
|
|
83
|
-
const count = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
|
|
84
|
-
caughtUp += count(counts?.["backfilled"]) ?? 0;
|
|
85
|
-
remaining = count(counts?.["remaining"]) ?? remaining;
|
|
86
|
-
live = null;
|
|
87
|
-
if (code === 0 && parsed?.status === "complete") {
|
|
88
|
-
// Reporting totals include permanent caps. Preserve their marker
|
|
89
|
-
// note instead of retrying history the collector says is complete.
|
|
90
|
-
const checked = await checkBackfillState(context);
|
|
91
|
-
return ok("backfill-complete", checked.status === "ok" ? checked.code : "complete", receipt() + (checked.status === "ok" ? `; ${checked.message}` : ""));
|
|
92
|
-
}
|
|
93
|
-
const verdict = backfillFixVerdict(parsed, jsonField(capture.stderr(), "failure_reason"));
|
|
94
|
-
if (parsed?.status === "partial" &&
|
|
95
|
-
(parsed.failure_reason === "deferred_budget_exhausted" || !parsed.failure_reason)) {
|
|
96
|
-
return needsFix("backfill-complete", "backfill_chunk_pending", "more history remains");
|
|
97
|
-
}
|
|
98
|
-
return { ...verdict, message: typeof parsed?.failure_reason === "string" ? parsed.failure_reason : "backfill failed without a valid completion receipt" };
|
|
99
|
-
});
|
|
100
|
-
if (row.status === "ok") {
|
|
101
|
-
console.error("[doctor] backfill-complete finished", JSON.stringify({ caught_up: caughtUp, remaining, chunks, elapsed_seconds: Math.floor((Date.now() - started) / 1000) }));
|
|
102
|
-
return row;
|
|
103
|
-
}
|
|
104
|
-
if (row.status === "skipped") {
|
|
105
|
-
// A running sync (or another catch-up) owns the lock. Doctor does not
|
|
106
|
-
// wait; it says what this run managed before it stood aside.
|
|
107
|
-
console.error("[doctor] backfill-complete stood aside", JSON.stringify({ reason: row.code, caught_up: caughtUp, remaining, chunks }));
|
|
108
|
-
return caughtUp > 0 ? { ...row, message: `${receipt()}; ${row.message}` } : row;
|
|
109
|
-
}
|
|
110
|
-
if (row.code === "sync_stuck_not_ended")
|
|
111
|
-
return row;
|
|
112
|
-
if (row.code !== "backfill_chunk_pending")
|
|
113
|
-
return unfinished(row.code, row.message);
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
async function catchUpChunkByChunk(context, run) {
|
|
87
|
+
while (Date.now() < run.deadline) {
|
|
88
|
+
const row = await withCollectionGate(context, "backfill-complete", () => runOneChunk(context, run));
|
|
89
|
+
if (row.status === "ok") {
|
|
90
|
+
console.error("[doctor] backfill-complete finished", JSON.stringify({ caught_up: run.caughtUp, remaining: run.remaining, chunks: run.chunks, elapsed_seconds: Math.floor((Date.now() - run.started) / 1000) }));
|
|
91
|
+
return row;
|
|
114
92
|
}
|
|
115
|
-
|
|
93
|
+
if (row.status === "skipped") {
|
|
94
|
+
// A running sync (or another catch-up) owns the lock. Doctor does not
|
|
95
|
+
// wait; it says what this run managed before it stood aside.
|
|
96
|
+
console.error("[doctor] backfill-complete stood aside", JSON.stringify({ reason: row.code, caught_up: run.caughtUp, remaining: run.remaining, chunks: run.chunks }));
|
|
97
|
+
return run.caughtUp > 0 ? { ...row, message: `${describeReceipt(run)}; ${row.message}` } : row;
|
|
98
|
+
}
|
|
99
|
+
if (row.code === "sync_stuck_not_ended")
|
|
100
|
+
return row;
|
|
101
|
+
if (row.code !== "backfill_chunk_pending")
|
|
102
|
+
return unfinishedRow(run, row.code, row.message);
|
|
116
103
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
104
|
+
return unfinishedRow(run, "backfill_budget_timeout", "backfill time budget reached");
|
|
105
|
+
}
|
|
106
|
+
async function runOneChunk(context, run) {
|
|
107
|
+
if (Date.now() >= run.deadline)
|
|
108
|
+
return unfinishedRow(run, "backfill_budget_timeout", "backfill time budget reached");
|
|
109
|
+
const capture = capturedIo(context.io, false);
|
|
110
|
+
run.live = null;
|
|
111
|
+
run.chunks += 1;
|
|
112
|
+
const code = await runBackfillCommand({
|
|
113
|
+
homeDir: context.command.homeDir, repoRoot: context.command.repoRoot,
|
|
114
|
+
all: true, dryRun: false, yes: true, json: true,
|
|
115
|
+
deadlineMs: run.deadline,
|
|
116
|
+
// Doctor never waits for the collection lock (BLI-12872): the gate
|
|
117
|
+
// above looked at it, and a busy answer here is reported, not retried.
|
|
118
|
+
syncLockWaitMs: 0,
|
|
119
|
+
onProgress: (progress) => { run.live = progress; },
|
|
120
|
+
}, capture.io);
|
|
121
|
+
const parsed = parseDoctorBackfillJson(capture.stdout());
|
|
122
|
+
const counts = asRecord(parsed?.counts);
|
|
123
|
+
run.caughtUp += nonNegativeCount(counts?.["backfilled"]) ?? 0;
|
|
124
|
+
run.remaining = nonNegativeCount(counts?.["remaining"]) ?? run.remaining;
|
|
125
|
+
run.live = null;
|
|
126
|
+
if (code === 0 && parsed?.status === "complete") {
|
|
127
|
+
// Reporting totals include permanent caps. Preserve their marker
|
|
128
|
+
// note instead of retrying history the collector says is complete.
|
|
129
|
+
const checked = await checkBackfillState(context);
|
|
130
|
+
return ok("backfill-complete", checked.status === "ok" ? checked.code : "complete", describeReceipt(run) + (checked.status === "ok" ? `; ${checked.message}` : ""));
|
|
120
131
|
}
|
|
121
|
-
|
|
122
|
-
|
|
132
|
+
const verdict = backfillFixVerdict(parsed, jsonField(capture.stderr(), "failure_reason"));
|
|
133
|
+
if (parsed?.status === "partial" &&
|
|
134
|
+
(parsed.failure_reason === "deferred_budget_exhausted" || !parsed.failure_reason)) {
|
|
135
|
+
return needsFix("backfill-complete", "backfill_chunk_pending", "more history remains");
|
|
123
136
|
}
|
|
137
|
+
return { ...verdict, message: typeof parsed?.failure_reason === "string" ? parsed.failure_reason : "backfill failed without a valid completion receipt" };
|
|
138
|
+
}
|
|
139
|
+
function nonNegativeCount(value) {
|
|
140
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
|
|
124
141
|
}
|
|
125
142
|
function capturedIo(io, forward) {
|
|
126
143
|
const stdoutChunks = [];
|