cloving 1.1.0 → 1.2.0
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/README.md +121 -21
- package/assets/watch-fonts/fira-code-OFL.md +93 -0
- package/assets/watch-fonts/fira-code-latin-400-normal.woff2 +0 -0
- package/assets/watch-fonts/fira-code-latin-500-normal.woff2 +0 -0
- package/assets/watch-fonts/heebo-OFL.md +93 -0
- package/assets/watch-fonts/heebo-latin-500-normal.woff2 +0 -0
- package/assets/watch-fonts/heebo-latin-700-normal.woff2 +0 -0
- package/assets/watch-fonts/heebo-latin-800-normal.woff2 +0 -0
- package/assets/watch-fonts/manifest.json +112 -0
- package/assets/watch-fonts/space-grotesk-OFL.md +93 -0
- package/assets/watch-fonts/space-grotesk-latin-400-normal.woff2 +0 -0
- package/assets/watch-fonts/space-grotesk-latin-500-normal.woff2 +0 -0
- package/assets/watch-fonts/space-grotesk-latin-600-normal.woff2 +0 -0
- package/assets/watch-fonts/space-grotesk-latin-700-normal.woff2 +0 -0
- package/dist/src/authority/commands.d.ts +1 -0
- package/dist/src/authority/commands.js +25 -4
- package/dist/src/authority/dispatch-decision.d.ts +1 -0
- package/dist/src/authority/dispatch-decision.js +7 -4
- package/dist/src/authority/events.js +2 -2
- package/dist/src/authority/handoff-chain.d.ts +24 -0
- package/dist/src/authority/handoff-chain.js +46 -0
- package/dist/src/authority/reconciliation.d.ts +1 -0
- package/dist/src/authority/reconciliation.js +18 -11
- package/dist/src/authority/transitions.d.ts +1 -1
- package/dist/src/authority/transitions.js +3 -0
- package/dist/src/broker/actor/authority-queries.d.ts +4 -0
- package/dist/src/broker/actor/authority-queries.js +51 -2
- package/dist/src/broker/actor/delivery-intents.d.ts +1 -0
- package/dist/src/broker/actor/effect-delivery.js +9 -0
- package/dist/src/broker/authority-admission.js +13 -5
- package/dist/src/chats/analysis-batches.d.ts +18 -0
- package/dist/src/chats/analysis-batches.js +15 -1
- package/dist/src/chats/analysis-checkpoint.d.ts +29 -0
- package/dist/src/chats/analysis-checkpoint.js +31 -0
- package/dist/src/chats/analysis-input.d.ts +7 -1
- package/dist/src/chats/analysis-input.js +20 -6
- package/dist/src/chats/analysis-merge.d.ts +16 -0
- package/dist/src/chats/analysis-merge.js +134 -0
- package/dist/src/chats/analysis-prompt.d.ts +8 -0
- package/dist/src/chats/analysis-prompt.js +27 -1
- package/dist/src/chats/analysis-proposal.d.ts +2 -0
- package/dist/src/chats/analysis-proposal.js +9 -0
- package/dist/src/chats/analysis-shape.d.ts +1 -0
- package/dist/src/chats/analysis-shape.js +11 -4
- package/dist/src/chats/reference-mapping.d.ts +16 -0
- package/dist/src/chats/reference-mapping.js +153 -0
- package/dist/src/chats/workflow-generation-projection.d.ts +8 -0
- package/dist/src/chats/workflow-generation-projection.js +75 -0
- package/dist/src/chats/workflow-generation-source.d.ts +25 -0
- package/dist/src/chats/workflow-generation-source.js +124 -0
- package/dist/src/chats/workflow-generation-text.d.ts +23 -0
- package/dist/src/chats/workflow-generation-text.js +74 -0
- package/dist/src/chats/workflow-generation.d.ts +69 -0
- package/dist/src/chats/workflow-generation.js +142 -0
- package/dist/src/cli/render.d.ts +1 -0
- package/dist/src/cli/render.js +60 -13
- package/dist/src/cli/usage.js +5 -2
- package/dist/src/cli/watch-board-view.d.ts +12 -1
- package/dist/src/cli/watch-board-view.js +57 -11
- package/dist/src/cli/watch-board.d.ts +7 -1
- package/dist/src/cli/watch-board.js +42 -10
- package/dist/src/cli/watch-client.d.ts +1 -1
- package/dist/src/cli/watch-client.js +7 -3
- package/dist/src/cli/watch-model.d.ts +10 -4
- package/dist/src/cli/watch-model.js +35 -58
- package/dist/src/cli/watch-page.js +21 -4
- package/dist/src/cli/watch-phases.js +3 -3
- package/dist/src/cli/watch-pr-command.d.ts +5 -2
- package/dist/src/cli/watch-pr-command.js +41 -4
- package/dist/src/cli/watch-pr-identity.d.ts +14 -2
- package/dist/src/cli/watch-pr-identity.js +25 -0
- package/dist/src/cli/watch-pr-observer.d.ts +1 -0
- package/dist/src/cli/watch-pr-observer.js +48 -6
- package/dist/src/cli/watch-pull-request.d.ts +10 -0
- package/dist/src/cli/watch-pull-request.js +130 -0
- package/dist/src/cli/watch-reads.d.ts +7 -1
- package/dist/src/cli/watch-reads.js +42 -20
- package/dist/src/cli/watch-render.d.ts +14 -1
- package/dist/src/cli/watch-render.js +2 -1
- package/dist/src/cli/watch-server.js +33 -2
- package/dist/src/cli/watch-turn.js +1 -1
- package/dist/src/cli/watch-view.d.ts +7 -0
- package/dist/src/cli/watch-view.js +51 -3
- package/dist/src/daemon/analysis-worker.d.ts +0 -2
- package/dist/src/daemon/analysis-worker.js +71 -19
- package/dist/src/daemon/chats-batch-binding.d.ts +23 -0
- package/dist/src/daemon/chats-batch-binding.js +99 -0
- package/dist/src/daemon/chats-capture.d.ts +18 -0
- package/dist/src/daemon/chats-capture.js +194 -0
- package/dist/src/daemon/chats-checkpoints.d.ts +41 -0
- package/dist/src/daemon/chats-checkpoints.js +186 -0
- package/dist/src/daemon/chats-files.d.ts +29 -2
- package/dist/src/daemon/chats-files.js +142 -3
- package/dist/src/daemon/chats-follow-up.d.ts +38 -0
- package/dist/src/daemon/chats-follow-up.js +180 -0
- package/dist/src/daemon/chats-question.d.ts +15 -0
- package/dist/src/daemon/chats-question.js +93 -0
- package/dist/src/daemon/chats-worker.d.ts +1 -0
- package/dist/src/daemon/chats-worker.js +52 -51
- package/dist/src/daemon/claude-turn.d.ts +2 -1
- package/dist/src/daemon/claude-turn.js +1 -1
- package/dist/src/daemon/claude-worker.d.ts +0 -2
- package/dist/src/daemon/claude-worker.js +6 -11
- package/dist/src/daemon/daemon.d.ts +5 -1
- package/dist/src/daemon/daemon.js +38 -10
- package/dist/src/daemon/delivery-router.js +6 -0
- package/dist/src/daemon/github-calls.d.ts +4 -0
- package/dist/src/daemon/github-calls.js +16 -7
- package/dist/src/daemon/github-ci.d.ts +6 -0
- package/dist/src/daemon/github-ci.js +170 -0
- package/dist/src/daemon/github-merge-call.d.ts +14 -0
- package/dist/src/daemon/github-merge-call.js +73 -0
- package/dist/src/daemon/github-merge.d.ts +3 -5
- package/dist/src/daemon/github-merge.js +52 -95
- package/dist/src/daemon/github-methods.d.ts +28 -0
- package/dist/src/daemon/github-methods.js +80 -16
- package/dist/src/daemon/github-operation.d.ts +2 -0
- package/dist/src/daemon/github-retry.d.ts +6 -0
- package/dist/src/daemon/github-retry.js +49 -0
- package/dist/src/daemon/github-ticket-status.js +27 -1
- package/dist/src/daemon/github-worker.js +2 -1
- package/dist/src/daemon/handoff-worker.d.ts +10 -0
- package/dist/src/daemon/handoff-worker.js +232 -0
- package/dist/src/daemon/issue-queue.d.ts +1 -0
- package/dist/src/daemon/issue-queue.js +94 -42
- package/dist/src/daemon/notify-journal.d.ts +18 -0
- package/dist/src/daemon/notify-journal.js +108 -0
- package/dist/src/daemon/notify-message.js +2 -1
- package/dist/src/daemon/notify.js +176 -37
- package/dist/src/daemon/retrying-turn.d.ts +3 -0
- package/dist/src/daemon/retrying-turn.js +55 -9
- package/dist/src/daemon/ticket-dependency-waits.d.ts +18 -0
- package/dist/src/daemon/ticket-dependency-waits.js +60 -0
- package/dist/src/daemon/ticket-registration.d.ts +8 -4
- package/dist/src/daemon/ticket-registration.js +76 -22
- package/dist/src/daemon/ticket-source.d.ts +4 -1
- package/dist/src/daemon/ticket-source.js +8 -2
- package/dist/src/decision-visibility.d.ts +2 -0
- package/dist/src/decision-visibility.js +10 -0
- package/dist/src/execution-pin.d.ts +4 -0
- package/dist/src/execution-pin.js +23 -2
- package/dist/src/host/start-delivery.d.ts +2 -1
- package/dist/src/host/start-delivery.js +1 -1
- package/dist/src/host/step-bridge-host.d.ts +1 -0
- package/dist/src/host/step-bridge-host.js +16 -0
- package/dist/src/host/world-reconciliation.d.ts +1 -0
- package/dist/src/host/world-reconciliation.js +3 -0
- package/dist/src/host/world-runs.js +5 -0
- package/dist/src/issue-identity.d.ts +6 -0
- package/dist/src/issue-identity.js +23 -0
- package/dist/src/service/queue-runs.d.ts +3 -0
- package/dist/src/service/queue-runs.js +39 -0
- package/dist/src/service/registry.d.ts +8 -2
- package/dist/src/service/registry.js +20 -8
- package/dist/src/service/replies.d.ts +2 -0
- package/dist/src/service/run-handlers.js +44 -12
- package/dist/src/service/service.d.ts +3 -0
- package/dist/src/service/service.js +3 -1
- package/dist/src/service/submission-handler.d.ts +1 -0
- package/dist/src/service/submission-handler.js +92 -13
- package/dist/src/service/worker-totals.js +25 -1
- package/dist/src/step-bridge.d.ts +9 -0
- package/dist/src/steps/handoff.d.ts +2 -0
- package/dist/src/steps/handoff.js +13 -0
- package/dist/src/ticket-dependencies.d.ts +1 -0
- package/dist/src/ticket-dependencies.js +15 -0
- package/dist/src/ticket-status.js +7 -4
- package/dist/src/token-prices.d.ts +2 -0
- package/dist/src/token-prices.js +41 -9
- package/dist/src/worker-usage.d.ts +9 -0
- package/dist/src/worker-usage.js +1 -1
- package/dist/src/workflow-admission.d.ts +1 -0
- package/dist/src/workflow-candidate.js +1 -0
- package/dist/src/workflow-contracts.d.ts +2 -0
- package/dist/src/workflow-contracts.js +8 -3
- package/dist/src/workflow-declaration.d.ts +11 -0
- package/dist/src/workflow-declaration.js +71 -15
- package/dist/src/workflow-discovery.d.ts +18 -0
- package/dist/src/workflow-discovery.js +33 -0
- package/dist/src/workflow-refusals.d.ts +6 -0
- package/dist/src/workflow-refusals.js +39 -0
- package/dist/src/workflows.d.ts +2 -1
- package/dist/src/workflows.js +1 -0
- package/dist/tools/admit-workflow.d.ts +13 -0
- package/dist/tools/admit-workflow.js +94 -0
- package/dist/tools/self-host-project.d.ts +17 -0
- package/dist/tools/self-host-project.js +177 -0
- package/dist/tools/self-host.js +27 -81
- package/dist/tools/workflow-bundle-dependencies.d.ts +5 -0
- package/dist/tools/workflow-bundle-dependencies.js +93 -0
- package/dist/tools/workflow-bundles.d.ts +20 -0
- package/dist/tools/workflow-bundles.js +173 -0
- package/dist/tools/workflow-candidate-counts.d.ts +8 -0
- package/dist/tools/workflow-candidate-counts.js +251 -0
- package/dist/tools/workflow-candidate-program.d.ts +11 -0
- package/dist/tools/workflow-candidate-program.js +78 -0
- package/dist/tools/workflow-candidate-source.d.ts +6 -0
- package/dist/tools/workflow-candidate-source.js +107 -0
- package/dist/tools/workflow-candidate-syntax.d.ts +15 -0
- package/dist/tools/workflow-candidate-syntax.js +219 -0
- package/dist/tools/workflow-folder-capture.d.ts +17 -0
- package/dist/tools/workflow-folder-capture.js +216 -0
- package/dist/tools/workflow-folders.d.ts +33 -0
- package/dist/tools/workflow-folders.js +150 -0
- package/dist/tools/workflow-source-effects.d.ts +10 -0
- package/dist/tools/workflow-source-effects.js +213 -0
- package/dist/workflows/analyze-chats.d.ts +6 -1
- package/dist/workflows/analyze-chats.declaration.json +14 -1
- package/dist/workflows/analyze-chats.js +188 -11
- package/dist/workflows/develop-prd.js +71 -3
- package/dist/workflows/develop-ticket.js +5 -0
- package/dist/workflows/ticket-prompts.js +12 -2
- package/package.json +21 -3
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
2
|
+
import { send } from "../cli/client.js";
|
|
3
|
+
import { record } from "../protocol.js";
|
|
4
|
+
import { runIdentity } from "../service/submission-handler.js";
|
|
5
|
+
import { located, readRecord, writeRecord } from "./operation-journal.js";
|
|
6
|
+
const attempts = 10;
|
|
7
|
+
const uncertain = [
|
|
8
|
+
"outcome-unknown",
|
|
9
|
+
"service-unavailable",
|
|
10
|
+
"service-busy",
|
|
11
|
+
"storage-failure",
|
|
12
|
+
"authority-unavailable",
|
|
13
|
+
"resolver-timeout",
|
|
14
|
+
"resolver-failure",
|
|
15
|
+
];
|
|
16
|
+
function own(intent) {
|
|
17
|
+
return (intent.kind === "protected-effect" &&
|
|
18
|
+
intent.capability === "handoff" &&
|
|
19
|
+
intent.method === "run" &&
|
|
20
|
+
intent.target !== undefined &&
|
|
21
|
+
intent.payload !== undefined &&
|
|
22
|
+
intent.leaseGeneration !== undefined);
|
|
23
|
+
}
|
|
24
|
+
function completed(runId) {
|
|
25
|
+
return {
|
|
26
|
+
kind: "completed",
|
|
27
|
+
worker: "handoff",
|
|
28
|
+
receipt: runId,
|
|
29
|
+
result: runId,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
async function saved(folder) {
|
|
33
|
+
const stored = record(await readRecord(folder, "result.json"));
|
|
34
|
+
if (stored === undefined)
|
|
35
|
+
return undefined;
|
|
36
|
+
if (stored["kind"] === "completed" && typeof stored["result"] === "string")
|
|
37
|
+
return completed(stored["result"]);
|
|
38
|
+
if (stored["kind"] === "failed" && typeof stored["reason"] === "string")
|
|
39
|
+
return { kind: "failed", reason: stored["reason"] };
|
|
40
|
+
throw new Error("invalid-handoff-journal");
|
|
41
|
+
}
|
|
42
|
+
async function retained(folder, outcome) {
|
|
43
|
+
if (["completed", "failed"].includes(outcome.kind))
|
|
44
|
+
await writeRecord(folder, {
|
|
45
|
+
name: "result.json",
|
|
46
|
+
value: outcome,
|
|
47
|
+
permissions: "private",
|
|
48
|
+
});
|
|
49
|
+
return outcome;
|
|
50
|
+
}
|
|
51
|
+
function outcome(reply) {
|
|
52
|
+
if (reply.kind !== "error") {
|
|
53
|
+
const runId = reply.result["runId"];
|
|
54
|
+
return reply.result["kind"] === "run-admitted" && typeof runId === "string"
|
|
55
|
+
? completed(runId)
|
|
56
|
+
: { kind: "unknown" };
|
|
57
|
+
}
|
|
58
|
+
if (reply.error.code === "workflow-active")
|
|
59
|
+
return undefined;
|
|
60
|
+
if (reply.error.code === "chain-limit")
|
|
61
|
+
return { kind: "failed", reason: "chain-limit" };
|
|
62
|
+
return uncertain.includes(reply.error.code)
|
|
63
|
+
? { kind: "unknown" }
|
|
64
|
+
: { kind: "failed", reason: "handoff-input-refused" };
|
|
65
|
+
}
|
|
66
|
+
async function input(settings, intent) {
|
|
67
|
+
const text = await settings.document(intent.payload);
|
|
68
|
+
try {
|
|
69
|
+
return record(JSON.parse(text ?? "null"));
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async function submit(settings, call) {
|
|
76
|
+
const { intent, signal } = call;
|
|
77
|
+
if (!own(intent))
|
|
78
|
+
return { kind: "failed", reason: "unsupported-effect" };
|
|
79
|
+
const { folder } = located(settings.journal, intent.operationId);
|
|
80
|
+
const known = await saved(folder);
|
|
81
|
+
if (known !== undefined)
|
|
82
|
+
return known;
|
|
83
|
+
const submission = await input(settings, intent);
|
|
84
|
+
if (submission === undefined)
|
|
85
|
+
return retained(folder, {
|
|
86
|
+
kind: "failed",
|
|
87
|
+
reason: "handoff-input-refused",
|
|
88
|
+
});
|
|
89
|
+
const previous = await busyAttempts(folder);
|
|
90
|
+
for (let busy = previous; busy < attempts; busy += 1) {
|
|
91
|
+
if (!(await allowed(settings, { intent, signal })))
|
|
92
|
+
return { kind: "unknown" };
|
|
93
|
+
const reply = await submitted(settings, {
|
|
94
|
+
intent,
|
|
95
|
+
submission,
|
|
96
|
+
signal,
|
|
97
|
+
folder,
|
|
98
|
+
});
|
|
99
|
+
if (reply === undefined)
|
|
100
|
+
return { kind: "unknown" };
|
|
101
|
+
const result = outcome(reply);
|
|
102
|
+
if (result !== undefined)
|
|
103
|
+
return retained(folder, result);
|
|
104
|
+
await writeRecord(folder, {
|
|
105
|
+
name: "attempts.json",
|
|
106
|
+
value: { busy: busy + 1 },
|
|
107
|
+
permissions: "private",
|
|
108
|
+
});
|
|
109
|
+
if (busy + 1 < attempts)
|
|
110
|
+
await delay(settings.retryMilliseconds ?? 1000, undefined, {
|
|
111
|
+
signal,
|
|
112
|
+
}).catch(() => undefined);
|
|
113
|
+
}
|
|
114
|
+
return retained(folder, { kind: "failed", reason: "handoff-start-timeout" });
|
|
115
|
+
}
|
|
116
|
+
async function submitted(settings, call) {
|
|
117
|
+
const { intent, submission, folder, signal } = call;
|
|
118
|
+
const marker = (status) => writeRecord(folder, {
|
|
119
|
+
name: "submission.json",
|
|
120
|
+
value: { processId: process.pid, pending: status === "pending" },
|
|
121
|
+
permissions: "private",
|
|
122
|
+
});
|
|
123
|
+
await marker("pending");
|
|
124
|
+
if (!(await allowed(settings, { intent, signal }))) {
|
|
125
|
+
await marker("settled");
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
const reply = await send(settings.runtimeDirectory, {
|
|
129
|
+
operation: "run",
|
|
130
|
+
parameters: {
|
|
131
|
+
workflowId: intent.target,
|
|
132
|
+
source: "input",
|
|
133
|
+
submission,
|
|
134
|
+
commandId: `handoff:${intent.operationId}`,
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
if (reply.kind !== "error" || reply.error.code !== "outcome-unknown")
|
|
138
|
+
await marker("settled");
|
|
139
|
+
return reply;
|
|
140
|
+
}
|
|
141
|
+
async function allowed(settings, call) {
|
|
142
|
+
try {
|
|
143
|
+
call.signal.throwIfAborted();
|
|
144
|
+
const status = await settings.runStatus(call.intent.runId);
|
|
145
|
+
call.signal.throwIfAborted();
|
|
146
|
+
return status === "running";
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
async function inFlight(folder) {
|
|
153
|
+
const marker = record(await readRecord(folder, "submission.json"));
|
|
154
|
+
if (marker?.["pending"] !== true)
|
|
155
|
+
return false;
|
|
156
|
+
const pid = marker["processId"];
|
|
157
|
+
if (typeof pid !== "number" || !Number.isSafeInteger(pid) || pid <= 0)
|
|
158
|
+
throw new Error("invalid-handoff-journal");
|
|
159
|
+
try {
|
|
160
|
+
process.kill(pid, 0);
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
return !(error instanceof Error &&
|
|
165
|
+
"code" in error &&
|
|
166
|
+
error.code === "ESRCH");
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
async function busyAttempts(folder) {
|
|
170
|
+
const previous = record(await readRecord(folder, "attempts.json"))?.["busy"] ?? 0;
|
|
171
|
+
if (typeof previous !== "number" ||
|
|
172
|
+
!Number.isSafeInteger(previous) ||
|
|
173
|
+
previous < 0 ||
|
|
174
|
+
previous > attempts)
|
|
175
|
+
throw new Error("invalid-handoff-journal");
|
|
176
|
+
return previous;
|
|
177
|
+
}
|
|
178
|
+
async function observed(settings, intent) {
|
|
179
|
+
if (!own(intent))
|
|
180
|
+
return { kind: "failed", reason: "unsupported-effect" };
|
|
181
|
+
const known = await saved(located(settings.journal, intent.operationId).folder);
|
|
182
|
+
if (known !== undefined)
|
|
183
|
+
return known;
|
|
184
|
+
const reply = await send(settings.runtimeDirectory, {
|
|
185
|
+
operation: "status",
|
|
186
|
+
parameters: { runId: runIdentity(`handoff:${intent.operationId}`) },
|
|
187
|
+
});
|
|
188
|
+
if (reply.kind === "error")
|
|
189
|
+
return reply.error.code === "unknown-run"
|
|
190
|
+
? {
|
|
191
|
+
kind: (await inFlight(located(settings.journal, intent.operationId).folder))
|
|
192
|
+
? "unknown"
|
|
193
|
+
: "absent",
|
|
194
|
+
}
|
|
195
|
+
: { kind: "unknown" };
|
|
196
|
+
const runId = record(reply.result["run"])?.["runId"];
|
|
197
|
+
return typeof runId === "string" ? completed(runId) : { kind: "unknown" };
|
|
198
|
+
}
|
|
199
|
+
export function handoffWorker(settings) {
|
|
200
|
+
const pending = new Map();
|
|
201
|
+
const deliver = (intent, signal) => {
|
|
202
|
+
const known = pending.get(intent.operationId);
|
|
203
|
+
if (known !== undefined)
|
|
204
|
+
return known.result;
|
|
205
|
+
const stopping = new AbortController();
|
|
206
|
+
const result = submit(settings, {
|
|
207
|
+
intent,
|
|
208
|
+
signal: AbortSignal.any([signal, stopping.signal]),
|
|
209
|
+
}).finally(() => pending.delete(intent.operationId));
|
|
210
|
+
pending.set(intent.operationId, { stopping, result });
|
|
211
|
+
return result;
|
|
212
|
+
};
|
|
213
|
+
return {
|
|
214
|
+
deliver,
|
|
215
|
+
reconcile: (intent) => observed(settings, intent),
|
|
216
|
+
retry: async (intent, signal) => {
|
|
217
|
+
if (!own(intent))
|
|
218
|
+
return { kind: "failed", reason: "unsupported-effect" };
|
|
219
|
+
const { folder } = located(settings.journal, intent.operationId);
|
|
220
|
+
if (!(await inFlight(folder)))
|
|
221
|
+
return { kind: "unknown" };
|
|
222
|
+
return deliver(intent, signal);
|
|
223
|
+
},
|
|
224
|
+
stop: async (intent) => {
|
|
225
|
+
const active = pending.get(intent.operationId);
|
|
226
|
+
active?.stopping.abort();
|
|
227
|
+
await active?.result;
|
|
228
|
+
const known = await observed(settings, intent);
|
|
229
|
+
return known.kind === "absent" ? { kind: "stopped" } : known;
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
}
|
|
@@ -76,6 +76,34 @@ async function recover(settings, pending) {
|
|
|
76
76
|
await pending.save();
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
|
+
async function trackedRuns(runtimeDirectory, commands) {
|
|
80
|
+
if (commands.length === 0)
|
|
81
|
+
return [];
|
|
82
|
+
if (commands.length === 1) {
|
|
83
|
+
const commandId = String(commands[0]);
|
|
84
|
+
const run = await runStatus(runtimeDirectory, commandId);
|
|
85
|
+
if (run === undefined)
|
|
86
|
+
throw new Error("queue-run-missing");
|
|
87
|
+
return [{ commandId, run }];
|
|
88
|
+
}
|
|
89
|
+
const reply = await send(runtimeDirectory, {
|
|
90
|
+
operation: "status",
|
|
91
|
+
parameters: { runIds: commands.map(runIdentity) },
|
|
92
|
+
});
|
|
93
|
+
if (reply.kind === "error")
|
|
94
|
+
throw new Error(reply.error.code);
|
|
95
|
+
const runs = reply.result["runs"];
|
|
96
|
+
if (!Array.isArray(runs))
|
|
97
|
+
throw new Error("invalid-run-list");
|
|
98
|
+
return commands.map((commandId) => {
|
|
99
|
+
const run = runs
|
|
100
|
+
.map(record)
|
|
101
|
+
.find((item) => item?.["runId"] === runIdentity(commandId));
|
|
102
|
+
if (run === undefined)
|
|
103
|
+
throw new Error("queue-run-missing");
|
|
104
|
+
return { commandId, run };
|
|
105
|
+
});
|
|
106
|
+
}
|
|
79
107
|
function handedOff(run) {
|
|
80
108
|
const outcome = record(run["outcome"]);
|
|
81
109
|
return (run["state"] === "completed" &&
|
|
@@ -88,33 +116,39 @@ function checkFailureLimit(pending) {
|
|
|
88
116
|
if (pending.failures.length >= 3)
|
|
89
117
|
throw new QueueFailureLimit(`3 runs in a row ended without handoff: ${pending.failures.join(", ")}`);
|
|
90
118
|
}
|
|
119
|
+
async function retired(pending, ending) {
|
|
120
|
+
const { commandId, run } = ending;
|
|
121
|
+
pending.commands.delete(commandId);
|
|
122
|
+
if (handedOff(run))
|
|
123
|
+
pending.failures.length = 0;
|
|
124
|
+
else
|
|
125
|
+
pending.failures.push(runIdentity(commandId));
|
|
126
|
+
await pending.save();
|
|
127
|
+
checkFailureLimit(pending);
|
|
128
|
+
if (handedOff(run))
|
|
129
|
+
return;
|
|
130
|
+
const outcome = record(run["outcome"]);
|
|
131
|
+
process.stderr.write(`Queue run ended ${String(outcome?.["kind"] ?? run["state"])}${outcome?.["issueUpdate"] === "failed" ? "; issueUpdate: failed" : ""}: ${runIdentity(commandId)}; continuing\n`);
|
|
132
|
+
}
|
|
91
133
|
async function completed(settings, pending) {
|
|
92
134
|
checkFailureLimit(pending);
|
|
93
|
-
let
|
|
135
|
+
let pendingRuns = 0;
|
|
136
|
+
const endings = [];
|
|
94
137
|
const tracked = [...pending.commands].filter((commandId) => commandId !== pending.retryCommandId);
|
|
95
|
-
for (const commandId of tracked) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
throw new Error("queue-run-missing");
|
|
99
|
-
if (!terminal(run)) {
|
|
100
|
-
pendingRun = true;
|
|
101
|
-
continue;
|
|
102
|
-
}
|
|
103
|
-
pending.commands.delete(commandId);
|
|
104
|
-
if (handedOff(run))
|
|
105
|
-
pending.failures.length = 0;
|
|
138
|
+
for (const { commandId, run } of await trackedRuns(settings.runtimeDirectory, tracked)) {
|
|
139
|
+
if (terminal(run))
|
|
140
|
+
endings.push({ commandId, run });
|
|
106
141
|
else
|
|
107
|
-
|
|
108
|
-
await pending.save();
|
|
109
|
-
checkFailureLimit(pending);
|
|
110
|
-
if (!handedOff(run)) {
|
|
111
|
-
const outcome = record(run["outcome"]);
|
|
112
|
-
process.stderr.write(`Queue run ended ${String(outcome?.["kind"] ?? run["state"])}${outcome?.["issueUpdate"] === "failed" ? "; issueUpdate: failed" : ""}: ${runIdentity(commandId)}; continuing\n`);
|
|
113
|
-
}
|
|
142
|
+
pendingRuns += 1;
|
|
114
143
|
}
|
|
115
|
-
|
|
144
|
+
endings.sort((first, second) => Number(first.run["terminalSequence"] ?? first.run["latestSequence"] ?? 0) -
|
|
145
|
+
Number(second.run["terminalSequence"] ?? second.run["latestSequence"] ?? 0));
|
|
146
|
+
for (const ending of endings)
|
|
147
|
+
await retired(pending, ending);
|
|
148
|
+
return pendingRuns;
|
|
116
149
|
}
|
|
117
150
|
async function active(runtimeDirectory) {
|
|
151
|
+
let count = 0;
|
|
118
152
|
let after = "";
|
|
119
153
|
do {
|
|
120
154
|
const reply = await send(runtimeDirectory, {
|
|
@@ -126,17 +160,16 @@ async function active(runtimeDirectory) {
|
|
|
126
160
|
const runs = reply.result["runs"];
|
|
127
161
|
if (!Array.isArray(runs))
|
|
128
162
|
throw new Error("invalid-run-list");
|
|
129
|
-
|
|
163
|
+
count += runs.filter((value) => {
|
|
130
164
|
const run = record(value);
|
|
131
165
|
return run?.["workflowId"] === "develop-issue" && !terminal(run);
|
|
132
|
-
})
|
|
133
|
-
return true;
|
|
166
|
+
}).length;
|
|
134
167
|
const next = reply.result["next"];
|
|
135
168
|
if (next !== null && (typeof next !== "string" || next <= after))
|
|
136
169
|
throw new Error("invalid-run-page");
|
|
137
170
|
after = next;
|
|
138
171
|
} while (after !== null);
|
|
139
|
-
return
|
|
172
|
+
return count;
|
|
140
173
|
}
|
|
141
174
|
async function submit(settings, pending) {
|
|
142
175
|
const commandId = pending.retryCommandId ?? randomUUID();
|
|
@@ -163,19 +196,38 @@ async function submit(settings, pending) {
|
|
|
163
196
|
process.stderr.write(`Queue ${reply.error.code}: command ${commandId}; will retry\n`);
|
|
164
197
|
return;
|
|
165
198
|
}
|
|
166
|
-
if (![
|
|
199
|
+
if (![
|
|
200
|
+
"no-ready-ticket",
|
|
201
|
+
"workflow-active",
|
|
202
|
+
"ticket-active",
|
|
203
|
+
"ticket-dependency-open",
|
|
204
|
+
].includes(reply.error.code))
|
|
167
205
|
throw new Error(reply.error.code);
|
|
168
206
|
pending.commands.delete(commandId);
|
|
169
207
|
await pending.save();
|
|
170
208
|
}
|
|
209
|
+
async function pendingUpdate(directory) {
|
|
210
|
+
return readFile(join(directory, "pending-self-update"), "utf8").catch((error) => {
|
|
211
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
212
|
+
return undefined;
|
|
213
|
+
throw error;
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
async function holdStarted(pending, observed) {
|
|
217
|
+
const { mark, activeRuns } = observed;
|
|
218
|
+
const timestamp = Number(mark);
|
|
219
|
+
if (!Number.isFinite(timestamp) || timestamp <= 0)
|
|
220
|
+
throw new Error("invalid-pending-self-update");
|
|
221
|
+
if (pending.selfUpdateHold?.mark !== mark || activeRuns > 0) {
|
|
222
|
+
pending.selfUpdateHold = { mark, startedAt: Date.now() };
|
|
223
|
+
await pending.save();
|
|
224
|
+
}
|
|
225
|
+
return pending.selfUpdateHold.startedAt;
|
|
226
|
+
}
|
|
171
227
|
function updateHold(directory, pending) {
|
|
172
228
|
let notice;
|
|
173
|
-
return async () => {
|
|
174
|
-
const mark = await
|
|
175
|
-
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
176
|
-
return undefined;
|
|
177
|
-
throw error;
|
|
178
|
-
});
|
|
229
|
+
return async (activeRuns = 0) => {
|
|
230
|
+
const mark = await pendingUpdate(directory);
|
|
179
231
|
if (mark === undefined && pending.selfUpdateHold !== undefined) {
|
|
180
232
|
pending.selfUpdateHold = undefined;
|
|
181
233
|
await pending.save();
|
|
@@ -184,14 +236,8 @@ function updateHold(directory, pending) {
|
|
|
184
236
|
notice = undefined;
|
|
185
237
|
return false;
|
|
186
238
|
}
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
throw new Error("invalid-pending-self-update");
|
|
190
|
-
if (pending.selfUpdateHold?.mark !== mark) {
|
|
191
|
-
pending.selfUpdateHold = { mark, startedAt: Date.now() };
|
|
192
|
-
await pending.save();
|
|
193
|
-
}
|
|
194
|
-
const holding = Date.now() - pending.selfUpdateHold.startedAt < 30 * 60 * 1000;
|
|
239
|
+
const startedAt = await holdStarted(pending, { mark, activeRuns });
|
|
240
|
+
const holding = activeRuns > 0 || Date.now() - startedAt < 30 * 60 * 1000;
|
|
195
241
|
const line = holding
|
|
196
242
|
? "Queue holding for pending self-update\n"
|
|
197
243
|
: "Queue continuing after pending self-update time limit\n";
|
|
@@ -227,11 +273,17 @@ async function cycle(settings, signal) {
|
|
|
227
273
|
await delay(settings.intervalMilliseconds, undefined, { signal }).catch(() => undefined);
|
|
228
274
|
if (stopped())
|
|
229
275
|
return;
|
|
230
|
-
|
|
276
|
+
const pendingRuns = await completed(settings, pending);
|
|
277
|
+
const lanes = settings.lanes ?? 1;
|
|
278
|
+
if (pendingRuns >= lanes) {
|
|
279
|
+
await holding(pendingRuns);
|
|
231
280
|
continue;
|
|
232
|
-
|
|
281
|
+
}
|
|
282
|
+
const activeRuns = await active(settings.runtimeDirectory);
|
|
283
|
+
const held = await holding(activeRuns);
|
|
284
|
+
if (activeRuns >= lanes || activeRuns < pendingRuns)
|
|
233
285
|
continue;
|
|
234
|
-
if (
|
|
286
|
+
if (held)
|
|
235
287
|
continue;
|
|
236
288
|
if (stopped())
|
|
237
289
|
return;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Notification } from "./notify-message.ts";
|
|
2
|
+
export interface DeliveryEntry {
|
|
3
|
+
readonly key: string;
|
|
4
|
+
readonly message: Notification | null;
|
|
5
|
+
readonly state: "pending" | "sent" | "failed";
|
|
6
|
+
readonly attempts: number;
|
|
7
|
+
readonly lastAttemptAt: number | null;
|
|
8
|
+
readonly lastFailure: string | null;
|
|
9
|
+
}
|
|
10
|
+
export interface RecordedNotifications {
|
|
11
|
+
readonly entries: Map<string, DeliveryEntry>;
|
|
12
|
+
readonly retiredKeys: Set<string>;
|
|
13
|
+
}
|
|
14
|
+
export declare function recordedNotifications(file: string): Promise<RecordedNotifications | undefined>;
|
|
15
|
+
export declare function saveNotifications(file: string, journal: {
|
|
16
|
+
readonly entries: ReadonlyMap<string, DeliveryEntry>;
|
|
17
|
+
readonly retiredKeys: ReadonlySet<string>;
|
|
18
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { open, readFile, rename } from "node:fs/promises";
|
|
2
|
+
import { record } from "../protocol.js";
|
|
3
|
+
export async function recordedNotifications(file) {
|
|
4
|
+
const text = await readFile(file, "utf8").catch((error) => {
|
|
5
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
6
|
+
return undefined;
|
|
7
|
+
throw error;
|
|
8
|
+
});
|
|
9
|
+
if (text === undefined)
|
|
10
|
+
return undefined;
|
|
11
|
+
const value = JSON.parse(text);
|
|
12
|
+
const journal = record(value);
|
|
13
|
+
if (journal?.["version"] === 1 && Array.isArray(journal["entries"])) {
|
|
14
|
+
const entries = journal["entries"].map(deliveryEntry);
|
|
15
|
+
const found = new Map(entries.map((entry) => [entry.key, entry]));
|
|
16
|
+
if (found.size !== entries.length)
|
|
17
|
+
throw new Error("invalid-notify-record");
|
|
18
|
+
return {
|
|
19
|
+
entries: found,
|
|
20
|
+
retiredKeys: recordedKeys(journal["retiredKeys"] ?? []),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
const keys = recordedKeys(value);
|
|
24
|
+
return {
|
|
25
|
+
retiredKeys: new Set(),
|
|
26
|
+
entries: new Map([...keys].map((key) => [
|
|
27
|
+
key,
|
|
28
|
+
{
|
|
29
|
+
key,
|
|
30
|
+
message: null,
|
|
31
|
+
state: "sent",
|
|
32
|
+
attempts: 0,
|
|
33
|
+
lastAttemptAt: Date.now(),
|
|
34
|
+
lastFailure: null,
|
|
35
|
+
},
|
|
36
|
+
])),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function recordedKeys(value) {
|
|
40
|
+
if (!Array.isArray(value) ||
|
|
41
|
+
!value.every((key) => typeof key === "string"))
|
|
42
|
+
throw new Error("invalid-notify-record");
|
|
43
|
+
return new Set(value.map(String));
|
|
44
|
+
}
|
|
45
|
+
function notification(value) {
|
|
46
|
+
const message = record(value);
|
|
47
|
+
if (message === undefined)
|
|
48
|
+
return false;
|
|
49
|
+
return (["decision", "blocked", "failed"].includes(String(message["event"])) &&
|
|
50
|
+
["event", "runId", "workflowId", "subject", "body"].every((field) => typeof message[field] === "string"));
|
|
51
|
+
}
|
|
52
|
+
function deliveryState(value) {
|
|
53
|
+
return value === "pending" || value === "sent" || value === "failed";
|
|
54
|
+
}
|
|
55
|
+
function attemptCount(value) {
|
|
56
|
+
return (typeof value === "number" &&
|
|
57
|
+
Number.isInteger(value) &&
|
|
58
|
+
value >= 0 &&
|
|
59
|
+
value <= 5);
|
|
60
|
+
}
|
|
61
|
+
function attemptTime(value) {
|
|
62
|
+
return (value === null ||
|
|
63
|
+
(typeof value === "number" && Number.isSafeInteger(value) && value >= 0));
|
|
64
|
+
}
|
|
65
|
+
function deliveryEntry(value) {
|
|
66
|
+
const { key, message, state, attempts, lastAttemptAt, lastFailure } = record(value) ?? {};
|
|
67
|
+
if (typeof key !== "string" ||
|
|
68
|
+
!deliveryState(state) ||
|
|
69
|
+
!attemptCount(attempts))
|
|
70
|
+
throw new Error("invalid-notify-record");
|
|
71
|
+
if (!attemptTime(lastAttemptAt) ||
|
|
72
|
+
!(lastFailure === null || typeof lastFailure === "string"))
|
|
73
|
+
throw new Error("invalid-notify-record");
|
|
74
|
+
const content = { message, state };
|
|
75
|
+
if (!entryMessage(content))
|
|
76
|
+
throw new Error("invalid-notify-record");
|
|
77
|
+
return {
|
|
78
|
+
key,
|
|
79
|
+
message: content.message,
|
|
80
|
+
state,
|
|
81
|
+
attempts,
|
|
82
|
+
lastAttemptAt,
|
|
83
|
+
lastFailure,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function entryMessage(entry) {
|
|
87
|
+
return entry.message === null
|
|
88
|
+
? entry.state === "sent"
|
|
89
|
+
: notification(entry.message);
|
|
90
|
+
}
|
|
91
|
+
export async function saveNotifications(file, journal) {
|
|
92
|
+
const temporary = await open(`${file}.tmp`, "w", 0o600);
|
|
93
|
+
try {
|
|
94
|
+
await temporary.chmod(0o600);
|
|
95
|
+
await temporary.writeFile(JSON.stringify({
|
|
96
|
+
version: 1,
|
|
97
|
+
entries: [...journal.entries.values()],
|
|
98
|
+
...(journal.retiredKeys.size > 0
|
|
99
|
+
? { retiredKeys: [...journal.retiredKeys] }
|
|
100
|
+
: {}),
|
|
101
|
+
}) + "\n");
|
|
102
|
+
await temporary.sync();
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
await temporary.close();
|
|
106
|
+
}
|
|
107
|
+
await rename(`${file}.tmp`, file);
|
|
108
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { operationalDecision } from "../decision-visibility.js";
|
|
1
2
|
import { decisionLines, runStatus } from "../cli/render.js";
|
|
2
3
|
import { record } from "../protocol.js";
|
|
3
4
|
function body(text, commands) {
|
|
@@ -17,7 +18,7 @@ function message(run, content) {
|
|
|
17
18
|
};
|
|
18
19
|
}
|
|
19
20
|
export function decisionNotification(run, decision) {
|
|
20
|
-
const lines = decisionLines(decision);
|
|
21
|
+
const lines = decisionLines(operationalDecision(decision, String(run["workflowId"])));
|
|
21
22
|
const allowed = decision["allowed"];
|
|
22
23
|
const count = Array.isArray(allowed) ? allowed.length : 0;
|
|
23
24
|
const split = lines.length - count;
|