@bli-cockpit/cli 0.2.242 → 0.2.244
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/git-state.js +1 -0
- package/dist/adapters/raw-evidence-budget-admission.js +63 -0
- package/dist/adapters/raw-evidence-collection-state.js +6 -50
- package/dist/adapters/raw-evidence-git-diff.js +1 -0
- package/dist/agent-rules.js +8 -0
- package/dist/autostart-contract.js +12 -0
- package/dist/autostart-darwin-plist.js +2 -2
- package/dist/autostart-darwin.js +1 -1
- package/dist/autostart-remind.js +245 -0
- package/dist/autostart-self-heal.js +17 -1
- package/dist/autostart-windows-scripts.js +7 -7
- package/dist/autostart.js +16 -15
- package/dist/commands/approvals-check-where.js +14 -0
- package/dist/commands/approvals-render.js +8 -1
- package/dist/commands/approvals.js +3 -2
- package/dist/commands/autostart-command.js +10 -0
- package/dist/commands/autostart-heal.js +36 -1
- package/dist/commands/brain.js +1 -1
- package/dist/commands/cal-agenda.js +44 -0
- package/dist/commands/cal-availability-share.js +97 -0
- package/dist/commands/cal-booking.js +147 -0
- package/dist/commands/cal-join.js +62 -0
- package/dist/commands/cal-links.js +68 -0
- package/dist/commands/cal-people.js +136 -0
- package/dist/commands/cal-remind.js +176 -0
- package/dist/commands/cal-reminders.js +33 -0
- package/dist/commands/cal-suggest.js +89 -0
- package/dist/commands/cal.js +41 -1
- package/dist/commands/cycle-args.js +127 -0
- package/dist/commands/cycles.js +117 -0
- package/dist/commands/docs-comments-args.js +85 -0
- package/dist/commands/docs-comments.js +128 -0
- package/dist/commands/docs-files-args.js +49 -0
- package/dist/commands/docs-files.js +179 -0
- package/dist/commands/docs-formatting.js +45 -0
- package/dist/commands/docs-inbox-args.js +93 -0
- package/dist/commands/docs-inbox.js +126 -0
- package/dist/commands/docs-notion.js +5 -0
- package/dist/commands/docs-read.js +2 -1
- package/dist/commands/docs-recovery.js +173 -0
- package/dist/commands/docs-refs-args.js +104 -0
- package/dist/commands/docs-refs.js +153 -0
- package/dist/commands/docs-search-args.js +79 -0
- package/dist/commands/docs-search.js +64 -0
- package/dist/commands/docs-sync-args.js +82 -0
- package/dist/commands/docs-sync.js +197 -0
- package/dist/commands/docs-tree.js +136 -0
- package/dist/commands/docs.js +85 -51
- package/dist/commands/doctor-update.js +4 -1
- package/dist/commands/inbox-args.js +81 -0
- package/dist/commands/inbox.js +88 -0
- package/dist/commands/issue-batch.js +61 -0
- package/dist/commands/issue-relations.js +151 -0
- package/dist/commands/issue-write.js +38 -5
- package/dist/commands/issue.js +32 -1
- package/dist/commands/local-args-collector-sources.js +2 -1
- package/dist/commands/local-args-tower-approvals.js +16 -4
- package/dist/commands/local-args-tower-cal-booking.js +81 -0
- package/dist/commands/local-args-tower-cal-links.js +42 -0
- package/dist/commands/local-args-tower-cal-remind.js +74 -0
- package/dist/commands/local-args-tower-cal-share.js +41 -0
- package/dist/commands/local-args-tower-cal.js +81 -8
- package/dist/commands/local-args-tower-docs-msg.js +98 -9
- package/dist/commands/local-args-tower-msg.js +222 -10
- package/dist/commands/local-args-tower-search.js +32 -5
- package/dist/commands/local-args-tower-work.js +110 -34
- package/dist/commands/local-auth.js +1 -1
- package/dist/commands/local-help-commands-tower-ops.js +6 -4
- package/dist/commands/local-help-commands-tower.js +47 -31
- package/dist/commands/local-help-commands.js +3 -1
- package/dist/commands/local-help-cycles.js +9 -0
- package/dist/commands/local-help-docs-files.js +10 -0
- package/dist/commands/local-help-docs-formatting.js +7 -0
- package/dist/commands/local-help-docs-inbox.js +10 -0
- package/dist/commands/local-help-docs-recovery.js +10 -0
- package/dist/commands/local-help-docs-refs.js +11 -0
- package/dist/commands/local-help-docs-sync.js +9 -0
- package/dist/commands/local-help-inbox.js +9 -0
- package/dist/commands/local-help-project.js +21 -0
- package/dist/commands/local-help-templates.js +9 -0
- package/dist/commands/local-help-views.js +10 -0
- package/dist/commands/local-help-work-notify.js +6 -0
- package/dist/commands/local-help.js +3 -3
- package/dist/commands/manager-hints.js +4 -2
- package/dist/commands/manager-limits.json +3 -1
- package/dist/commands/manager-read-reasons.js +1 -0
- package/dist/commands/manager-render-home.js +13 -5
- package/dist/commands/manager-render.js +3 -0
- package/dist/commands/manager.js +3 -1
- package/dist/commands/mcp-stdio-probe.js +1 -0
- package/dist/commands/msg-activity.js +134 -0
- package/dist/commands/msg-channel-args.js +92 -0
- package/dist/commands/msg-channel.js +125 -0
- package/dist/commands/msg-context-args.js +67 -0
- package/dist/commands/msg-context.js +65 -0
- package/dist/commands/msg-dm.js +28 -0
- package/dist/commands/msg-file.js +185 -0
- package/dist/commands/msg-mark-args.js +78 -0
- package/dist/commands/msg-mark.js +148 -0
- package/dist/commands/msg-notify-args.js +100 -0
- package/dist/commands/msg-notify.js +122 -0
- package/dist/commands/msg-schedule-args.js +114 -0
- package/dist/commands/msg-schedule.js +226 -0
- package/dist/commands/msg-search-args.js +71 -0
- package/dist/commands/msg-search.js +48 -0
- package/dist/commands/msg-write.js +15 -1
- package/dist/commands/msg.js +62 -39
- package/dist/commands/project-args.js +82 -0
- package/dist/commands/project.js +71 -7
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/search.js +23 -0
- package/dist/commands/session-sync-failures.js +17 -4
- package/dist/commands/session-sync-health.js +7 -2
- package/dist/commands/session-sync-hold.js +42 -1
- package/dist/commands/session-sync-outside-roots.js +72 -14
- package/dist/commands/session-sync-upload.js +2 -0
- package/dist/commands/session-sync.js +1 -0
- package/dist/commands/sources-check.js +63 -2
- package/dist/commands/sources-discovery.js +1 -1
- package/dist/commands/sources-reconciliation.js +87 -0
- package/dist/commands/sync-doctor.js +14 -1
- package/dist/commands/sync-report.js +1 -1
- package/dist/commands/template-args.js +122 -0
- package/dist/commands/templates.js +162 -0
- package/dist/commands/view-args.js +131 -0
- package/dist/commands/views.js +138 -0
- package/dist/commands/work-notify-args.js +67 -0
- package/dist/commands/work-notify.js +63 -0
- package/dist/delivery-hold-notice.js +34 -0
- package/dist/evidence-transcripts-client.js +1 -0
- package/dist/log-rotation.js +2 -2
- package/dist/notifier.js +83 -0
- package/dist/process-runner.js +4 -0
- package/dist/repo-identity-git.js +1 -0
- package/package.json +4 -4
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cockpit project` argument parsing (BLI-3716 list, BLI-21659 show/create/update/archive/restore).
|
|
3
|
+
*
|
|
4
|
+
* A description never travels on argv by default: `--file <path>` reads it from a local file (safest on Windows);
|
|
5
|
+
* `--description <text>` is the short form and `--description none` clears it. Write-safety flags
|
|
6
|
+
* (`--request-id`, `--expected-updated-at`) belong to the four write verbs.
|
|
7
|
+
*/
|
|
8
|
+
import { optionalNonEmpty, optionalPositiveInteger, optionalUrl, parseNamedArgs } from "./local-arg-values.js";
|
|
9
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
10
|
+
const ACTIONS = ["list", "show", "create", "update", "archive", "restore"];
|
|
11
|
+
const WRITES = ["create", "update", "archive", "restore"];
|
|
12
|
+
export function parseProjectArgs(args) {
|
|
13
|
+
const values = parseNamedArgs(args, {
|
|
14
|
+
allowedFlags: [
|
|
15
|
+
"--home", "--dashboard-url", "--archived", "--cursor", "--limit", "--json",
|
|
16
|
+
"--name", "--description", "--file", "--color", "--request-id", "--expected-updated-at",
|
|
17
|
+
],
|
|
18
|
+
valueFlags: [
|
|
19
|
+
"--home", "--dashboard-url", "--cursor", "--limit",
|
|
20
|
+
"--name", "--description", "--file", "--color", "--request-id", "--expected-updated-at",
|
|
21
|
+
],
|
|
22
|
+
});
|
|
23
|
+
const first = values.positionals[0];
|
|
24
|
+
const action = (first === undefined ? "list" : first);
|
|
25
|
+
if (!ACTIONS.includes(action)) {
|
|
26
|
+
throw new Error(`Unknown project command: ${first}. Try list, show, create, update, archive or restore.`);
|
|
27
|
+
}
|
|
28
|
+
const rest = values.positionals.slice(first === undefined ? 0 : 1);
|
|
29
|
+
const needsRef = action !== "list" && action !== "create";
|
|
30
|
+
if (needsRef && rest.length !== 1)
|
|
31
|
+
throw new Error(`project ${action} takes exactly one project id or name.`);
|
|
32
|
+
if (!needsRef && rest.length > 0)
|
|
33
|
+
throw new Error(`project ${action} does not take "${rest[0]}".`);
|
|
34
|
+
const given = (flag) => values.flags.has(flag);
|
|
35
|
+
const isWrite = WRITES.includes(action);
|
|
36
|
+
if (!isWrite && (given("--request-id") || given("--expected-updated-at"))) {
|
|
37
|
+
throw new Error("--request-id and --expected-updated-at belong to create, update, archive and restore.");
|
|
38
|
+
}
|
|
39
|
+
if (action !== "create" && action !== "update" && (given("--name") || given("--description") || given("--file"))) {
|
|
40
|
+
throw new Error("--name, --description and --file belong to project create and update.");
|
|
41
|
+
}
|
|
42
|
+
if (action !== "create" && given("--color"))
|
|
43
|
+
throw new Error("--color belongs to project create.");
|
|
44
|
+
if (action !== "list" && (given("--cursor") || given("--limit") || values.booleans.has("--archived"))) {
|
|
45
|
+
throw new Error("--archived, --cursor and --limit belong to project list.");
|
|
46
|
+
}
|
|
47
|
+
if (given("--description") && given("--file"))
|
|
48
|
+
throw new Error("Give --description or --file, not both.");
|
|
49
|
+
const requestId = optionalNonEmpty(values.flags.get("--request-id"));
|
|
50
|
+
if (requestId && !UUID_PATTERN.test(requestId))
|
|
51
|
+
throw new Error("--request-id must be a UUID.");
|
|
52
|
+
const expectedUpdatedAt = optionalNonEmpty(values.flags.get("--expected-updated-at"));
|
|
53
|
+
if (expectedUpdatedAt && Number.isNaN(Date.parse(expectedUpdatedAt))) {
|
|
54
|
+
throw new Error("--expected-updated-at must be the updated_at timestamp you last read.");
|
|
55
|
+
}
|
|
56
|
+
const name = optionalNonEmpty(values.flags.get("--name"));
|
|
57
|
+
if (action === "create" && !name)
|
|
58
|
+
throw new Error('project create needs --name "<name>".');
|
|
59
|
+
const rawDescription = values.flags.get("--description");
|
|
60
|
+
const description = rawDescription === undefined ? undefined : rawDescription.trim().toLowerCase() === "none" ? null : rawDescription;
|
|
61
|
+
const filePath = optionalNonEmpty(values.flags.get("--file"));
|
|
62
|
+
if (action === "update" && name === undefined && description === undefined && filePath === undefined) {
|
|
63
|
+
throw new Error("project update needs --name, --description (or none) or --file.");
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
kind: "project",
|
|
67
|
+
action,
|
|
68
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
69
|
+
dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
|
|
70
|
+
includeArchived: values.booleans.has("--archived"),
|
|
71
|
+
...(optionalNonEmpty(values.flags.get("--cursor")) ? { cursor: optionalNonEmpty(values.flags.get("--cursor")) } : {}),
|
|
72
|
+
...(values.flags.has("--limit") ? { limit: optionalPositiveInteger(values.flags.get("--limit"), "--limit") } : {}),
|
|
73
|
+
...(needsRef ? { ref: rest[0] } : {}),
|
|
74
|
+
...(name === undefined ? {} : { name }),
|
|
75
|
+
...(description === undefined ? {} : { description }),
|
|
76
|
+
...(filePath === undefined ? {} : { filePath }),
|
|
77
|
+
...(optionalNonEmpty(values.flags.get("--color")) ? { color: optionalNonEmpty(values.flags.get("--color")) } : {}),
|
|
78
|
+
...(requestId ? { requestId } : {}),
|
|
79
|
+
...(expectedUpdatedAt ? { expectedUpdatedAt } : {}),
|
|
80
|
+
json: values.booleans.has("--json"),
|
|
81
|
+
};
|
|
82
|
+
}
|
package/dist/commands/project.js
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `cockpit project list` — the projects issues are filed under
|
|
2
|
+
* `cockpit project list|show|create|update|archive|restore` — the projects issues are filed under
|
|
3
|
+
* (BLI-3716 list; BLI-21659 the rest).
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* command rather than `cockpit issue projects` because a person says
|
|
8
|
-
* "projects", and `cockpit issue list --project <name>` resolves a name
|
|
9
|
-
* against exactly this list.
|
|
5
|
+
* Its own command rather than `cockpit issue projects` because a person says "projects", and
|
|
6
|
+
* `cockpit issue list --project <name>` resolves a name against exactly this list. Writes go through
|
|
7
|
+
* `/api/work/projects` (receipt, `tower_only` delivery: nothing reaches Linear); no verb deletes a project.
|
|
10
8
|
*/
|
|
11
9
|
import { askAgentDoor, emitAgentDoor, failAgentDoor, openAgentDoor } from "./agent-door.js";
|
|
12
10
|
import { writeLine } from "./cli-io.js";
|
|
13
11
|
import { pageFieldsOf, pageFooter } from "./issue-list-page.js";
|
|
12
|
+
import { readBody } from "./issue-contracts.js";
|
|
14
13
|
const TAG = "[project cli]";
|
|
15
14
|
const READ_DEADLINE_MS = 30_000;
|
|
16
15
|
export async function runProject(command, io) {
|
|
17
16
|
const door = await openAgentDoor("project", command, io);
|
|
17
|
+
if (command.action !== "list")
|
|
18
|
+
return await runProjectVerb(command, door);
|
|
18
19
|
const params = new URLSearchParams();
|
|
19
20
|
if (command.includeArchived)
|
|
20
21
|
params.set("include_archived", "true");
|
|
@@ -47,4 +48,67 @@ export async function runProject(command, io) {
|
|
|
47
48
|
if (footer)
|
|
48
49
|
writeLine(door.io.stdout, footer);
|
|
49
50
|
return 0;
|
|
51
|
+
}
|
|
52
|
+
async function runProjectVerb(command, door) {
|
|
53
|
+
const base = "/api/work/projects";
|
|
54
|
+
const ref = command.ref ? `${base}/${encodeURIComponent(command.ref)}` : base;
|
|
55
|
+
if (command.action === "show") {
|
|
56
|
+
const answer = await askAgentDoor(door, { path: ref, method: "GET", label: "project show", timeoutMs: READ_DEADLINE_MS });
|
|
57
|
+
if (!answer.ok)
|
|
58
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
59
|
+
const body = answer.body;
|
|
60
|
+
if (door.json)
|
|
61
|
+
return emitAgentDoor(door, { ok: true, ...body, ...pageFieldsOf(body) });
|
|
62
|
+
const project = body.project;
|
|
63
|
+
const out = door.io.stdout;
|
|
64
|
+
writeLine(out, `${project?.name ?? command.ref} (${project?.archived_at ? "archived" : "active"}, ${project?.source ?? "native"})`);
|
|
65
|
+
writeLine(out, `id ${project?.id ?? ""} updated ${project?.updated_at ?? ""}`);
|
|
66
|
+
if (project?.description)
|
|
67
|
+
writeLine(out, project.description);
|
|
68
|
+
for (const issue of body.issues ?? [])
|
|
69
|
+
writeLine(out, `${issue.identifier ?? ""} ${issue.state ?? ""} ${issue.title ?? ""}`);
|
|
70
|
+
const footer = pageFooter(body, "tickets");
|
|
71
|
+
if (footer)
|
|
72
|
+
writeLine(out, footer);
|
|
73
|
+
if (body.href)
|
|
74
|
+
writeLine(out, `Filtered list: ${body.href}`);
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
77
|
+
const payload = {
|
|
78
|
+
...(command.requestId ? { request_id: command.requestId } : {}),
|
|
79
|
+
...(command.expectedUpdatedAt ? { expected_updated_at: command.expectedUpdatedAt } : {}),
|
|
80
|
+
};
|
|
81
|
+
if (command.action === "create" || command.action === "update") {
|
|
82
|
+
if (command.name !== undefined)
|
|
83
|
+
payload["name"] = command.name;
|
|
84
|
+
if (command.color !== undefined)
|
|
85
|
+
payload["color"] = command.color;
|
|
86
|
+
if (command.filePath) {
|
|
87
|
+
const read = await readBody({ filePath: command.filePath }, door.io);
|
|
88
|
+
if (!read.ok)
|
|
89
|
+
return failAgentDoor(door, TAG, read.reason, read.detail);
|
|
90
|
+
payload["description"] = read.text;
|
|
91
|
+
}
|
|
92
|
+
else if (command.description !== undefined) {
|
|
93
|
+
payload["description"] = command.description;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const path = command.action === "create" ? base : command.action === "update" ? ref : `${ref}/${command.action}`;
|
|
97
|
+
const answer = await askAgentDoor(door, {
|
|
98
|
+
path,
|
|
99
|
+
method: command.action === "update" ? "PATCH" : "POST",
|
|
100
|
+
label: `project ${command.action}`,
|
|
101
|
+
timeoutMs: READ_DEADLINE_MS,
|
|
102
|
+
body: payload,
|
|
103
|
+
});
|
|
104
|
+
if (!answer.ok) {
|
|
105
|
+
const mutation = answer.body?.mutation;
|
|
106
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail, mutation ? { mutation } : {});
|
|
107
|
+
}
|
|
108
|
+
const body = answer.body;
|
|
109
|
+
console.error(`${TAG} ${command.action} committed`, JSON.stringify({ project_id: body.project?.id, delivery: body.mutation?.delivery?.status }));
|
|
110
|
+
if (door.json)
|
|
111
|
+
return emitAgentDoor(door, { ok: true, project: body.project, mutation: body.mutation });
|
|
112
|
+
writeLine(door.io.stdout, `${command.action} committed: ${body.project?.name ?? ""} (${body.project?.id ?? ""}), delivery ${body.mutation?.delivery?.status ?? "tower_only"}.`);
|
|
113
|
+
return 0;
|
|
50
114
|
}
|
|
@@ -21,7 +21,7 @@ export async function runCockpitCli(argv, io) {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
if (command === "--version" || command === "-V" || command === "version") {
|
|
24
|
-
writeLine(io?.stdout ?? process.stdout, "0.2.
|
|
24
|
+
writeLine(io?.stdout ?? process.stdout, "0.2.244");
|
|
25
25
|
return 0;
|
|
26
26
|
}
|
|
27
27
|
|
package/dist/commands/search.js
CHANGED
|
@@ -26,6 +26,8 @@
|
|
|
26
26
|
*/
|
|
27
27
|
import { askAgentDoor, emitAgentDoor, failAgentDoor, openAgentDoor, } from "./agent-door.js";
|
|
28
28
|
import { writeLine } from "./cli-io.js";
|
|
29
|
+
import { appendDocSearchParams } from "./docs-search-args.js";
|
|
30
|
+
import { appendMsgSearchParams } from "./msg-search-args.js";
|
|
29
31
|
const TAG = "[search cli]";
|
|
30
32
|
const READ_DEADLINE_MS = 30_000;
|
|
31
33
|
/** Group headings, in the order a person reads them. Mirrors the overlay. */
|
|
@@ -56,6 +58,12 @@ export async function runSearch(command, io) {
|
|
|
56
58
|
params.set("kinds", command.kinds.join(","));
|
|
57
59
|
if (command.limit !== undefined)
|
|
58
60
|
params.set("limit", String(command.limit));
|
|
61
|
+
// BLI-21658: message filters ride the same door, which answers them only for kinds=msg.
|
|
62
|
+
if (command.msgFilters)
|
|
63
|
+
appendMsgSearchParams(params, command.msgFilters);
|
|
64
|
+
// BLI-21697: document filters ride the same door, which answers them only for kinds=doc.
|
|
65
|
+
if (command.docFilters)
|
|
66
|
+
appendDocSearchParams(params, command.docFilters);
|
|
59
67
|
const answer = await askAgentDoor(door, {
|
|
60
68
|
path: `/api/search?${params.toString()}`,
|
|
61
69
|
method: "GET",
|
|
@@ -139,6 +147,21 @@ function render(door, body) {
|
|
|
139
147
|
writeLine(door.io.stdout, `${KIND_LABELS[kind] ?? kind.toUpperCase()} did not answer (${reason}). Nothing from there is in this list.`);
|
|
140
148
|
}
|
|
141
149
|
}
|
|
150
|
+
// Named, so a filtered list is not read as the ranked, semantic one (BLI-21658).
|
|
151
|
+
if (body.degraded?.msg === "filtered_lexical_only") {
|
|
152
|
+
writeLine(door.io.stdout, "");
|
|
153
|
+
writeLine(door.io.stdout, "MESSAGES were filtered: keyword matching only (filtered_lexical_only), newest first, not ranked.");
|
|
154
|
+
if (body.msgPaging?.hasMore && body.msgPaging.nextCursor) {
|
|
155
|
+
writeLine(door.io.stdout, `More messages exist: \`cockpit msg search ... --before ${body.msgPaging.nextCursor}\` continues with the same filters.`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (body.degraded?.doc === "filtered_lexical_only") {
|
|
159
|
+
writeLine(door.io.stdout, "");
|
|
160
|
+
writeLine(door.io.stdout, "DOCUMENTS were filtered: keyword matching only (filtered_lexical_only), not ranked by meaning.");
|
|
161
|
+
if (body.docCoverage && body.docCoverage.coverage !== "exhausted") {
|
|
162
|
+
writeLine(door.io.stdout, `Documents coverage: ${body.docCoverage.coverage}${body.docCoverage.reason ? ` (${body.docCoverage.reason})` : ""}; ${body.docCoverage.matched} matched.`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
142
165
|
writeLine(door.io.stdout, "");
|
|
143
166
|
writeLine(door.io.stdout, `${hits.length} result(s) in ${body.elapsedMs ?? 0} ms.`);
|
|
144
167
|
return 0;
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* without saying why.
|
|
14
14
|
*/
|
|
15
15
|
import { claudeAttributionReadFailureCount, codexAttributionReadFailureCount, } from "./agent-session-report.js";
|
|
16
|
-
import { isDeliveryHoldReason } from "./session-sync-hold.js";
|
|
16
|
+
import { budgetStarved, isDeliveryHoldReason } from "./session-sync-hold.js";
|
|
17
17
|
import { sourceScanFailureReason } from "./session-sync-scan.js";
|
|
18
18
|
/**
|
|
19
19
|
* The label used when a sync fails and nothing on the way there said why.
|
|
@@ -55,8 +55,15 @@ export function createSyncFailureLedger() {
|
|
|
55
55
|
* hold in `session-sync-hold.ts` and rides the same receipt — and a tick that
|
|
56
56
|
* ALSO failed for a real reason still fails on that reason. Nothing is
|
|
57
57
|
* silenced; only the verdict changed.
|
|
58
|
+
*
|
|
59
|
+
* Ticket 21952 does the same for the per-tick size budget. Overflow defers to the next tick by design, so a tick that
|
|
60
|
+
* sent something and held the rest reports a named hold (`held_budget:` in `session-sync-hold.ts`) instead of failing:
|
|
61
|
+
* one machine was RED on every tick from 2026-10-15 while it uploaded 2 GiB each time. Only a STARVED tick, one that
|
|
62
|
+
* deferred evidence and let nothing through, still fails `deferred_byte_budget` / `deferred_object_budget`. Without
|
|
63
|
+
* `budgetUse` (an older caller) the old rule stands: any deferral fails.
|
|
58
64
|
*/
|
|
59
|
-
export function recordWorktreeDeliveryFailures(ledger, outcomes) {
|
|
65
|
+
export function recordWorktreeDeliveryFailures(ledger, outcomes, budgetUse) {
|
|
66
|
+
const budgetHolds = budgetUse !== undefined && !budgetStarved(budgetUse);
|
|
60
67
|
for (const outcome of outcomes) {
|
|
61
68
|
recordWorktreeSyncFailure(ledger, outcome);
|
|
62
69
|
const { sync } = outcome;
|
|
@@ -71,15 +78,21 @@ export function recordWorktreeDeliveryFailures(ledger, outcomes) {
|
|
|
71
78
|
for (const reason of sync.raw_evidence_retry_reasons ?? []) {
|
|
72
79
|
if (isDeliveryHoldReason(reason))
|
|
73
80
|
continue;
|
|
81
|
+
if (budgetHolds && isBudgetDeferralReason(reason))
|
|
82
|
+
continue;
|
|
74
83
|
ledger.add({
|
|
75
84
|
label: "raw_evidence_retry_required",
|
|
76
85
|
rendered: `raw_evidence_retry:${reason}`,
|
|
77
86
|
});
|
|
78
87
|
}
|
|
79
|
-
ledger.fail(sync.raw_evidence_deferred_byte_budget > 0, "deferred_byte_budget");
|
|
80
|
-
ledger.fail(sync.raw_evidence_deferred_object_budget > 0, "deferred_object_budget");
|
|
88
|
+
ledger.fail(!budgetHolds && sync.raw_evidence_deferred_byte_budget > 0, "deferred_byte_budget");
|
|
89
|
+
ledger.fail(!budgetHolds && sync.raw_evidence_deferred_object_budget > 0, "deferred_object_budget");
|
|
81
90
|
}
|
|
82
91
|
}
|
|
92
|
+
/** The two reasons the per-tick budget writes when it defers a file. Exact labels, never a pattern. */
|
|
93
|
+
function isBudgetDeferralReason(reason) {
|
|
94
|
+
return reason === "deferred_byte_budget" || reason === "deferred_object_budget";
|
|
95
|
+
}
|
|
83
96
|
function recordWorktreeSyncFailure(ledger, { worktree, sync }) {
|
|
84
97
|
if (sync.status === "uploaded")
|
|
85
98
|
return;
|
|
@@ -13,7 +13,7 @@ import { collectionRootLabel } from "./heartbeat.js";
|
|
|
13
13
|
import { workDirectories } from "../root-work.js";
|
|
14
14
|
import { createSyncFailureLedger, recordSourceScanFailures, recordUnexplainedFailure, recordUnpostedSessionReportFailure, recordWorktreeDeliveryFailures, } from "./session-sync-failures.js";
|
|
15
15
|
import { countSessionsNewThisTick, countSessionsPendingUpload, } from "./session-sync-counters.js";
|
|
16
|
-
import { logDeliveryHold, summarizeDeliveryHold } from "./session-sync-hold.js";
|
|
16
|
+
import { logBudgetHold, logDeliveryHold, summarizeBudgetHold, summarizeDeliveryHold } from "./session-sync-hold.js";
|
|
17
17
|
/**
|
|
18
18
|
* Decide whether this tick failed, and record every condition that decided it.
|
|
19
19
|
*
|
|
@@ -23,7 +23,7 @@ import { logDeliveryHold, summarizeDeliveryHold } from "./session-sync-hold.js";
|
|
|
23
23
|
export function decideSyncHealth(options) {
|
|
24
24
|
const { outcomes } = options.worktreePass;
|
|
25
25
|
const ledger = createSyncFailureLedger();
|
|
26
|
-
recordWorktreeDeliveryFailures(ledger, outcomes);
|
|
26
|
+
recordWorktreeDeliveryFailures(ledger, outcomes, options.worktreePass.budgetUse);
|
|
27
27
|
recordSourceScanFailures(ledger, options.scan);
|
|
28
28
|
const sessionsOutsideRoot = options.sessions.filter((session) => OUTSIDE_APPROVED_ROOT_REASONS.has(session.attribution_reason)).length;
|
|
29
29
|
const nothingInRoot = nothingInRootCount({
|
|
@@ -54,6 +54,10 @@ export function decideSyncHealth(options) {
|
|
|
54
54
|
const hold = summarizeDeliveryHold(outcomes);
|
|
55
55
|
if (hold && ok)
|
|
56
56
|
logDeliveryHold(hold);
|
|
57
|
+
// Ticket 21952: the size-budget hold, said on every tick that has one, ok or not.
|
|
58
|
+
const budgetHold = summarizeBudgetHold(options.worktreePass.budgetUse);
|
|
59
|
+
if (budgetHold)
|
|
60
|
+
logBudgetHold(budgetHold);
|
|
57
61
|
const records = ledger.sortedRecords();
|
|
58
62
|
return {
|
|
59
63
|
ok,
|
|
@@ -61,6 +65,7 @@ export function decideSyncHealth(options) {
|
|
|
61
65
|
failure_records: records,
|
|
62
66
|
notice,
|
|
63
67
|
hold,
|
|
68
|
+
budgetHold,
|
|
64
69
|
sessions_outside_root: sessionsOutsideRoot,
|
|
65
70
|
outside_root_directories: workDirectories([...(options.scan.codexAttribution.results ?? []), ...(options.scan.claudeAttribution.results ?? [])].filter(result => OUTSIDE_APPROVED_ROOT_REASONS.has(result.reason)))
|
|
66
71
|
.map(entry => ({ ...collectionRootLabel(entry.directory), count: entry.count }))
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* reads every `session-sync*.ts` sibling and demands that only the failure
|
|
12
12
|
* ledger writes one, and this file is not it.
|
|
13
13
|
*/
|
|
14
|
-
import { formatDeliveryHoldNotice, } from "../delivery-hold-notice.js";
|
|
14
|
+
import { formatBudgetHoldNotice, formatDeliveryHoldNotice, } from "../delivery-hold-notice.js";
|
|
15
15
|
import { DELIVERY_BACKOFF_HOLDING_REASON } from "../raw-evidence-staging.js";
|
|
16
16
|
/**
|
|
17
17
|
* Is this reason a delivery hold rather than a delivery failure?
|
|
@@ -87,4 +87,45 @@ export function logDeliveryHold(hold) {
|
|
|
87
87
|
last_reason: hold.lastReason,
|
|
88
88
|
next_action: "nothing until the next attempt is due; run `cockpit doctor` if it is already past",
|
|
89
89
|
}));
|
|
90
|
+
}
|
|
91
|
+
/** The token's leading word, owned with its grammar by `delivery-hold-notice.ts` so the fleet board parses what this writes. */
|
|
92
|
+
export { BUDGET_HOLD_NOTICE_PREFIX } from "../delivery-hold-notice.js";
|
|
93
|
+
/** Reads the shared budget's counters, or null when the pass carried none. */
|
|
94
|
+
export function budgetUseOf(budget) {
|
|
95
|
+
if (!budget)
|
|
96
|
+
return undefined;
|
|
97
|
+
return {
|
|
98
|
+
admittedBytes: budget.admittedBytes ?? 0,
|
|
99
|
+
admittedObjects: budget.admittedObjects ?? 0,
|
|
100
|
+
deferredBytes: budget.deferredBytes ?? 0,
|
|
101
|
+
deferredObjects: budget.deferredObjects ?? 0,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Did the budget hold anything back while still letting something through?
|
|
106
|
+
*
|
|
107
|
+
* The collector's design is that overflow defers and converges: the next tick starts with a fresh budget and picks up
|
|
108
|
+
* what this one held. A tick that admitted at least one object made progress, so what it deferred is a hold. A tick
|
|
109
|
+
* that deferred and admitted NOTHING made no progress at all, which stays a failure (`budgetStarved`).
|
|
110
|
+
*/
|
|
111
|
+
export function summarizeBudgetHold(use) {
|
|
112
|
+
if (!use || use.deferredObjects <= 0 || budgetStarved(use))
|
|
113
|
+
return null;
|
|
114
|
+
const notice = formatBudgetHoldNotice(use);
|
|
115
|
+
return notice ? { ...use, notice } : null;
|
|
116
|
+
}
|
|
117
|
+
/** True when the budget deferred evidence and let nothing through this tick. */
|
|
118
|
+
export function budgetStarved(use) {
|
|
119
|
+
return Boolean(use && use.deferredObjects > 0 && use.admittedObjects === 0);
|
|
120
|
+
}
|
|
121
|
+
/** The run's line for a size-budget hold: bytes sent, bytes held, and that the next tick takes the rest. */
|
|
122
|
+
export function logBudgetHold(hold) {
|
|
123
|
+
console.error("[session-sync] raw evidence held back by the per-tick size budget, the next tick takes the rest", JSON.stringify({
|
|
124
|
+
reason: hold.notice,
|
|
125
|
+
sent_bytes: hold.admittedBytes,
|
|
126
|
+
sent_objects: hold.admittedObjects,
|
|
127
|
+
held_bytes: hold.deferredBytes,
|
|
128
|
+
held_objects: hold.deferredObjects,
|
|
129
|
+
next_action: "none; the next tick starts with a fresh budget",
|
|
130
|
+
}));
|
|
90
131
|
}
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
*
|
|
23
23
|
* - A session already durable with unchanged content rides for free. It
|
|
24
24
|
* uploads nothing (the object cursor reuses it) and keeps its row stable.
|
|
25
|
-
* - A session that is new, changed or not yet uploaded is "owed". The
|
|
25
|
+
* - A session that is new, changed or not yet uploaded is "owed". The longest-waiting
|
|
26
26
|
* ones ride this tick up to a per-tick session cap. The rest wait, and their
|
|
27
27
|
* row says `deferred_object_budget`, the existing "next tick has room"
|
|
28
28
|
* label, instead of the old "not uploadable" one.
|
|
@@ -34,9 +34,9 @@
|
|
|
34
34
|
*
|
|
35
35
|
* The cap, the byte allowance and the object allowance are per tick, so a
|
|
36
36
|
* 1,800-session backlog drains across many ticks and never blows one tick's
|
|
37
|
-
* time or the machine's memory.
|
|
38
|
-
*
|
|
39
|
-
*
|
|
37
|
+
* time or the machine's memory. A persisted retry order rotates offered
|
|
38
|
+
* sessions behind waiting ones,
|
|
39
|
+
* including when their source keeps growing.
|
|
40
40
|
*
|
|
41
41
|
* A new sibling must also join `scripts/build-public-cli.mjs` `runtimeFiles`.
|
|
42
42
|
*/
|
|
@@ -45,12 +45,13 @@ import path from "node:path";
|
|
|
45
45
|
import { RAW_EVIDENCE_UPLOAD_MAX_FILE_BYTES } from "@bli-cockpit/telemetry-core";
|
|
46
46
|
import { isRedactionStubObjectKey } from "../cursors/raw-evidence-stub-entry.js";
|
|
47
47
|
import { describeError } from "../health-detail.js";
|
|
48
|
-
import {
|
|
48
|
+
import { getCollectorRuntimePaths } from "../local-state.js";
|
|
49
|
+
import { isMissingFileError, writeJsonFileAtomic, writeJsonFile } from "../local-state-files.js";
|
|
49
50
|
import { rootScopeForReason } from "./agent-session-root-scope.js";
|
|
50
51
|
import { ensureOutsideRootsWorkspace, isOutsideRootsWorkspace, outsideRootsWorkspace, } from "./backfill-outside-roots.js";
|
|
51
52
|
/** Most outside-root sessions that upload bytes in one tick. 4 ticks an hour, so up to 3,840 a day. */
|
|
52
53
|
export const OUTSIDE_ROOT_SESSIONS_PER_TICK = 40;
|
|
53
|
-
/**
|
|
54
|
+
/** Normal outside-root byte allowance; a fresh allowance can admit one wire-bounded file. */
|
|
54
55
|
export const OUTSIDE_ROOT_BYTES_PER_TICK = 256 * 1024 * 1024;
|
|
55
56
|
/** Files (a Claude main plus its sidecars count one each) an outside-root pass may collect in one tick. */
|
|
56
57
|
export const OUTSIDE_ROOT_OBJECTS_PER_TICK = 120;
|
|
@@ -165,7 +166,27 @@ export async function admitOutsideRootSessions(options) {
|
|
|
165
166
|
if (sightings.length === 0) {
|
|
166
167
|
return { ...scan, outsideRoots: null };
|
|
167
168
|
}
|
|
168
|
-
const
|
|
169
|
+
const queuePath = path.join(getCollectorRuntimePaths(options.homeDir).cursors_dir, "outside-root-admission.json");
|
|
170
|
+
const turns = await readAdmissionTurns(queuePath);
|
|
171
|
+
const { admitted, holds, counts, budget } = selectOutsideRootSessions(sightings, plan, turns);
|
|
172
|
+
// Offered work rotates even on a later disk/delivery refusal. New arrivals
|
|
173
|
+
// join behind existing waiters; growth never resets a session's place.
|
|
174
|
+
const offered = new Set(admitted.map((sighting) => outsideRootKey(sighting.source, sighting.sessionId)));
|
|
175
|
+
const next = Object.values(turns).reduce((max, turn) => Math.max(max, turn), 0) + 1;
|
|
176
|
+
const pending = {};
|
|
177
|
+
for (const sighting of sightings) {
|
|
178
|
+
const key = outsideRootKey(sighting.source, sighting.sessionId);
|
|
179
|
+
if (isDurableAndUnchanged(sighting.result, (sighting.source === "codex" ? plan.codexCursorBefore : plan.claudeCursorBefore).sessions[sighting.sessionId]) ||
|
|
180
|
+
sighting.result.byte_size > RAW_EVIDENCE_UPLOAD_MAX_FILE_BYTES)
|
|
181
|
+
continue;
|
|
182
|
+
pending[key] = offered.has(key) ? next + 1 : turns[key] ?? next;
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
await writeJsonFileAtomic(queuePath, pending);
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
console.error("[session-sync] outside-root admission order not saved", JSON.stringify({ reason: "admission_order_write_failed", ...describeError(error) }));
|
|
189
|
+
}
|
|
169
190
|
if (admitted.length === 0) {
|
|
170
191
|
console.error("[session-sync] outside-root raw upload", JSON.stringify({ reason: "nothing_to_upload_outside_roots", ...counts, ...budget }));
|
|
171
192
|
return { ...scan, outsideRoots: { workspace: null, holds, counts } };
|
|
@@ -203,7 +224,24 @@ export async function admitOutsideRootSessions(options) {
|
|
|
203
224
|
console.error("[session-sync] outside-root raw upload", JSON.stringify({ reason: "outside_root_sessions_admitted", ...counts, ...budget }));
|
|
204
225
|
return labelled;
|
|
205
226
|
}
|
|
206
|
-
|
|
227
|
+
/** Session ids and queue turns only; no source paths or transcript content. */
|
|
228
|
+
async function readAdmissionTurns(filePath) {
|
|
229
|
+
try {
|
|
230
|
+
const value = JSON.parse(await fs.readFile(filePath, "utf8"));
|
|
231
|
+
if (!value || typeof value !== "object" || Array.isArray(value) ||
|
|
232
|
+
Object.entries(value).some(([key, turn]) => !/^(codex|claude_code):/.test(key) || !Number.isSafeInteger(turn) || Number(turn) < 0)) {
|
|
233
|
+
throw new Error("invalid admission order");
|
|
234
|
+
}
|
|
235
|
+
return value;
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
if (!isMissingFileError(error)) {
|
|
239
|
+
console.error("[session-sync] outside-root admission order unavailable", JSON.stringify({ reason: "admission_order_read_failed", ...describeError(error) }));
|
|
240
|
+
}
|
|
241
|
+
return {};
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
function selectOutsideRootSessions(sightings, plan, turns) {
|
|
207
245
|
const cursorFor = (source) => source === "codex" ? plan.codexCursorBefore : plan.claudeCursorBefore;
|
|
208
246
|
const free = [];
|
|
209
247
|
const owed = [];
|
|
@@ -222,12 +260,28 @@ function selectOutsideRootSessions(sightings, plan) {
|
|
|
222
260
|
owed.push(sighting);
|
|
223
261
|
}
|
|
224
262
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
left.
|
|
229
|
-
|
|
230
|
-
|
|
263
|
+
const newTurn = Object.values(turns).reduce((max, turn) => Math.max(max, turn), 0) + 1;
|
|
264
|
+
owed.sort((left, right) => (turns[outsideRootKey(left.source, left.sessionId)] ?? newTurn) -
|
|
265
|
+
(turns[outsideRootKey(right.source, right.sessionId)] ?? newTurn) ||
|
|
266
|
+
left.result.session_file_mtime_ms - right.result.session_file_mtime_ms ||
|
|
267
|
+
outsideRootKey(left.source, left.sessionId).localeCompare(outsideRootKey(right.source, right.sessionId)));
|
|
268
|
+
// A main larger than the normal allowance gets a turn without other owed sessions. Otherwise a
|
|
269
|
+
// smaller main (or the other provider's reader, which runs first) could fill
|
|
270
|
+
// staging before the existing empty-staging exception ever sees it.
|
|
271
|
+
const riding = [];
|
|
272
|
+
let bytes = 0;
|
|
273
|
+
for (const sighting of owed) {
|
|
274
|
+
if (riding.length >= OUTSIDE_ROOT_SESSIONS_PER_TICK)
|
|
275
|
+
break;
|
|
276
|
+
if (bytes + sighting.result.byte_size > OUTSIDE_ROOT_BYTES_PER_TICK) {
|
|
277
|
+
if (riding.length === 0)
|
|
278
|
+
riding.push(sighting);
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
riding.push(sighting);
|
|
282
|
+
bytes += sighting.result.byte_size;
|
|
283
|
+
}
|
|
284
|
+
const waiting = owed.slice(riding.length);
|
|
231
285
|
for (const sighting of waiting) {
|
|
232
286
|
holds.set(outsideRootKey(sighting.source, sighting.sessionId), OUTSIDE_ROOT_WAITING_REASON);
|
|
233
287
|
}
|
|
@@ -273,6 +327,10 @@ export function applyOutsideRootHolds(rows, holds) {
|
|
|
273
327
|
* description session stay shared (they live on the same budget object).
|
|
274
328
|
*/
|
|
275
329
|
export function giveOutsideRootsTheirOwnAllowance(budget) {
|
|
330
|
+
budget.singleFileByteAllowance = {
|
|
331
|
+
normalBytes: OUTSIDE_ROOT_BYTES_PER_TICK,
|
|
332
|
+
maxFileBytes: RAW_EVIDENCE_UPLOAD_MAX_FILE_BYTES,
|
|
333
|
+
};
|
|
276
334
|
budget.remainingBytes = OUTSIDE_ROOT_BYTES_PER_TICK;
|
|
277
335
|
budget.remainingObjects = OUTSIDE_ROOT_OBJECTS_PER_TICK;
|
|
278
336
|
}
|
|
@@ -13,6 +13,7 @@ import { liveSyncTargetKey, liveSyncTargetWorktrees, matchesLiveSyncWorktree, }
|
|
|
13
13
|
import { isOutsideRootsWorkspace } from "./backfill-outside-roots.js";
|
|
14
14
|
import { giveOutsideRootsTheirOwnAllowance, outsideRootsLast, } from "./session-sync-outside-roots.js";
|
|
15
15
|
import { runYieldingAtBoundaries } from "./sync-yield.js";
|
|
16
|
+
import { budgetUseOf } from "./session-sync-hold.js";
|
|
16
17
|
const CLAUDE_DAMP_GROWTH_BYTES = 256 * 1024;
|
|
17
18
|
const CLAUDE_DAMP_MAX_AGE_MS = 6 * 60 * 60 * 1000;
|
|
18
19
|
/**
|
|
@@ -54,6 +55,7 @@ export async function syncEveryTargetWorktree(options) {
|
|
|
54
55
|
claudePriorDurablePointers: claudeDurablePointersFromCursor(options.plan.claudeCursorBefore),
|
|
55
56
|
growthDampedSessionCount: dampedClaudePointers.size,
|
|
56
57
|
yieldedWorktreesLeft: loop.left,
|
|
58
|
+
budgetUse: budgetUseOf(rawEvidenceBudget),
|
|
57
59
|
};
|
|
58
60
|
}
|
|
59
61
|
function syncTargetWithAllowance(options, worktree, syncWorktrees, rawEvidenceBudget, discoveredWorktreeKeys, dampedClaudePointers) {
|
|
@@ -190,6 +190,7 @@ async function finishAttributedSync(options, context) {
|
|
|
190
190
|
failure_records: health.failure_records,
|
|
191
191
|
notice: health.notice,
|
|
192
192
|
hold: health.hold,
|
|
193
|
+
budgetHold: health.budgetHold ?? null,
|
|
193
194
|
yielded_worktrees_left: worktreePass.yieldedWorktreesLeft,
|
|
194
195
|
sessions_observed: sessions.length,
|
|
195
196
|
sessions_outside_root: health.sessions_outside_root,
|