cool-workflow 0.1.80 → 0.1.81
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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +42 -2
- package/apps/architecture-review/app.json +1 -1
- package/apps/architecture-review-fast/app.json +1 -1
- package/apps/end-to-end-golden-path/app.json +1 -1
- package/apps/pr-review-fix-ci/app.json +1 -1
- package/apps/release-cut/app.json +1 -1
- package/apps/research-synthesis/app.json +1 -1
- package/dist/agent-config.js +21 -7
- package/dist/candidate-scoring.js +42 -22
- package/dist/capability-core.js +94 -17
- package/dist/capability-registry.js +138 -171
- package/dist/cli.js +90 -100
- package/dist/collaboration.js +5 -6
- package/dist/commit.js +20 -6
- package/dist/compare.js +18 -0
- package/dist/coordinator/classify.js +45 -0
- package/dist/coordinator/paths.js +42 -0
- package/dist/coordinator/util.js +129 -0
- package/dist/coordinator.js +127 -300
- package/dist/dispatch.js +35 -0
- package/dist/drive.js +7 -7
- package/dist/error-feedback.js +8 -4
- package/dist/evidence-reasoning.js +1 -1
- package/dist/execution-backend/agent.js +331 -0
- package/dist/execution-backend/probes.js +96 -0
- package/dist/execution-backend/util.js +47 -0
- package/dist/execution-backend.js +67 -420
- package/dist/mcp-server.js +34 -173
- package/dist/multi-agent/graph.js +84 -0
- package/dist/multi-agent/helpers.js +145 -0
- package/dist/multi-agent/paths.js +22 -0
- package/dist/multi-agent-eval/format.js +194 -0
- package/dist/multi-agent-eval/normalize.js +51 -0
- package/dist/multi-agent-eval.js +39 -244
- package/dist/multi-agent-host.js +0 -19
- package/dist/multi-agent.js +125 -314
- package/dist/node-snapshot.js +3 -3
- package/dist/observability/format.js +61 -0
- package/dist/observability/intake.js +98 -0
- package/dist/observability.js +14 -160
- package/dist/operator-ux/format.js +364 -0
- package/dist/operator-ux.js +22 -363
- package/dist/orchestrator/report.js +8 -0
- package/dist/orchestrator.js +25 -8
- package/dist/reclamation.js +26 -21
- package/dist/run-export.js +138 -14
- package/dist/run-registry/derive.js +172 -0
- package/dist/run-registry/format.js +124 -0
- package/dist/run-registry/gc.js +251 -0
- package/dist/run-registry/policy.js +16 -0
- package/dist/run-registry/queue.js +116 -0
- package/dist/run-registry.js +78 -593
- package/dist/run-state-schema.js +1 -0
- package/dist/sandbox-profile.js +43 -2
- package/dist/state-explosion/format.js +159 -0
- package/dist/state-explosion/helpers.js +82 -0
- package/dist/state-explosion.js +65 -283
- package/dist/state-node.js +19 -4
- package/dist/telemetry-attestation.js +55 -0
- package/dist/telemetry-demo.js +15 -3
- package/dist/telemetry-ledger.js +60 -15
- package/dist/topology.js +25 -8
- package/dist/triggers.js +33 -14
- package/dist/trust-audit.js +145 -33
- package/dist/version.js +1 -1
- package/dist/worker-isolation/helpers.js +51 -0
- package/dist/worker-isolation/paths.js +46 -0
- package/dist/worker-isolation.js +39 -115
- package/docs/agent-delegation-drive.7.md +13 -0
- package/docs/cli-mcp-parity.7.md +4 -0
- package/docs/contract-migration-tooling.7.md +2 -0
- package/docs/control-plane-scheduling.7.md +2 -0
- package/docs/dogfood/resume-drive-real-agent-2026-06-14.md +40 -0
- package/docs/durable-state-and-locking.7.md +4 -0
- package/docs/evidence-adoption-reasoning-chain.7.md +2 -0
- package/docs/execution-backends.7.md +2 -0
- package/docs/index.md +1 -0
- package/docs/launch/launch-kit.md +46 -23
- package/docs/launch/pre-launch-checklist.md +14 -14
- package/docs/multi-agent-cli-mcp-surface.7.md +4 -0
- package/docs/multi-agent-eval-replay-harness.7.md +2 -0
- package/docs/multi-agent-operator-ux.7.md +2 -0
- package/docs/multi-agent-trust-policy-audit.7.md +27 -0
- package/docs/node-snapshot-diff-replay.7.md +2 -0
- package/docs/observability-cost-accounting.7.md +2 -0
- package/docs/project-index.md +18 -5
- package/docs/real-execution-backends.7.md +2 -0
- package/docs/release-and-migration.7.md +4 -0
- package/docs/release-tooling.7.md +2 -0
- package/docs/run-registry-control-plane.7.md +54 -8
- package/docs/run-retention-reclamation.7.md +4 -0
- package/docs/state-explosion-management.7.md +2 -0
- package/docs/team-collaboration.7.md +2 -0
- package/docs/trust-model.md +267 -0
- package/docs/vendor-manifest-loadability.7.md +43 -0
- package/docs/web-desktop-workbench.7.md +2 -0
- package/manifest/plugin.manifest.json +1 -1
- package/package.json +4 -2
- package/scripts/agents/builtin-templates.json +7 -0
- package/scripts/bump-version.js +5 -11
- package/scripts/canonical-apps-list.js +64 -0
- package/scripts/canonical-apps.js +19 -4
- package/scripts/dogfood-release.js +1 -1
- package/scripts/golden-path.js +4 -4
- package/scripts/parity-check.js +5 -0
- package/scripts/release-check.js +5 -1
- package/scripts/version-sync-check.js +5 -8
- package/dist/capability-dispatcher.js +0 -86
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.reclamationPolicy = reclamationPolicy;
|
|
4
|
+
exports.reclaimEligibility = reclaimEligibility;
|
|
5
|
+
exports.gcPlan = gcPlan;
|
|
6
|
+
exports.gcRun = gcRun;
|
|
7
|
+
exports.gcVerify = gcVerify;
|
|
8
|
+
const reclamation_1 = require("../reclamation");
|
|
9
|
+
const trust_audit_1 = require("../trust-audit");
|
|
10
|
+
const policy_1 = require("./policy");
|
|
11
|
+
/** Resolve the effective reclamation policy (defaults reclaim NOTHING). */
|
|
12
|
+
function reclamationPolicy(overrides = {}) {
|
|
13
|
+
return { ...policy_1.DEFAULT_RUN_REGISTRY_POLICY, ...overrides };
|
|
14
|
+
}
|
|
15
|
+
/** Fail-closed eligibility: terminal AND archived AND no open feedback AND past
|
|
16
|
+
* retention. Returns the matching refusal code, or null when eligible. Reads
|
|
17
|
+
* the live-source-derived record; order yields distinct, stable codes. */
|
|
18
|
+
function reclaimEligibility(record, policy, nowMs) {
|
|
19
|
+
if (record.tier === "reclaimed")
|
|
20
|
+
return "already-reclaimed";
|
|
21
|
+
const terminalStates = policy.reclaimStates && policy.reclaimStates.length ? policy.reclaimStates : ["completed", "failed"];
|
|
22
|
+
if (record.derivedLifecycle !== "completed" && record.derivedLifecycle !== "failed")
|
|
23
|
+
return "non-terminal";
|
|
24
|
+
if (!terminalStates.includes(record.derivedLifecycle))
|
|
25
|
+
return "non-terminal";
|
|
26
|
+
if (record.openFeedbackCount > 0)
|
|
27
|
+
return "open-feedback";
|
|
28
|
+
if (!record.archived)
|
|
29
|
+
return "not-archived";
|
|
30
|
+
const days = policy.reclaimAfterArchiveDays ?? 0;
|
|
31
|
+
if (days > 0) {
|
|
32
|
+
const archivedAtMs = record.archivedAt ? Date.parse(record.archivedAt) : NaN;
|
|
33
|
+
if (!Number.isFinite(archivedAtMs))
|
|
34
|
+
return "within-retention";
|
|
35
|
+
if (archivedAtMs > nowMs - days * 24 * 60 * 60 * 1000)
|
|
36
|
+
return "within-retention";
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
/** Resolve a single run to a one-element record list via locate() (repo-first),
|
|
41
|
+
* avoiding a full-registry scan for single-run gc plan/run. */
|
|
42
|
+
function recordsForRunId(host, runId, scope) {
|
|
43
|
+
const located = host.locate(runId, scope);
|
|
44
|
+
return located ? [located.record] : [];
|
|
45
|
+
}
|
|
46
|
+
/** Dry-run: compute eligible runs, per-kind bytes that WOULD be freed, and the
|
|
47
|
+
* capability downgrade. Frees NOTHING. */
|
|
48
|
+
function gcPlan(host, options = {}) {
|
|
49
|
+
const scope = options.scope || "home";
|
|
50
|
+
const policy = reclamationPolicy(options.policy);
|
|
51
|
+
const nowIso = options.now || new Date().toISOString();
|
|
52
|
+
const nowMs = Date.parse(nowIso);
|
|
53
|
+
// Fast, deterministic single-run path: resolve just that run via locate()
|
|
54
|
+
// (repo-first) so a home-scope plan never re-scans the whole registry.
|
|
55
|
+
const records = options.runId ? recordsForRunId(host, options.runId, scope) : host.buildIndex(scope).records;
|
|
56
|
+
const entries = [];
|
|
57
|
+
let bytesToFree = 0;
|
|
58
|
+
let eligibleCount = 0;
|
|
59
|
+
for (const record of records) {
|
|
60
|
+
const refusal = reclaimEligibility(record, policy, nowMs);
|
|
61
|
+
let plan;
|
|
62
|
+
try {
|
|
63
|
+
const run = host.loadRun(record.repo, record.runId);
|
|
64
|
+
plan = (0, reclamation_1.planReclamation)(run, { keepScratch: policy.keepScratch, keepSnapshots: policy.keepSnapshots });
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
entries.push({
|
|
68
|
+
runId: record.runId,
|
|
69
|
+
repo: record.repo,
|
|
70
|
+
eligible: false,
|
|
71
|
+
reason: "unreadable",
|
|
72
|
+
tier: record.tier || "live",
|
|
73
|
+
capability: record.capability || "re-runnable",
|
|
74
|
+
capabilityReason: record.capabilityReason || "live-full",
|
|
75
|
+
bytesToFree: 0,
|
|
76
|
+
byKind: {},
|
|
77
|
+
freeable: []
|
|
78
|
+
});
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const eligible = refusal === null;
|
|
82
|
+
const entry = {
|
|
83
|
+
runId: record.runId,
|
|
84
|
+
repo: record.repo,
|
|
85
|
+
eligible,
|
|
86
|
+
reason: eligible ? "eligible" : refusal,
|
|
87
|
+
tier: record.tier || "live",
|
|
88
|
+
capability: plan.capability,
|
|
89
|
+
capabilityReason: plan.capabilityReason,
|
|
90
|
+
bytesToFree: eligible ? plan.bytesToFree : 0,
|
|
91
|
+
byKind: eligible ? plan.byKind : {},
|
|
92
|
+
freeable: eligible ? plan.freeable.map((f) => ({ path: f.path, kind: f.kind, bytes: f.bytes })) : []
|
|
93
|
+
};
|
|
94
|
+
entries.push(entry);
|
|
95
|
+
if (eligible) {
|
|
96
|
+
eligibleCount += 1;
|
|
97
|
+
bytesToFree += plan.bytesToFree;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
schemaVersion: 1,
|
|
102
|
+
scope,
|
|
103
|
+
generatedAt: nowIso,
|
|
104
|
+
policy: {
|
|
105
|
+
reclaimAfterArchiveDays: policy.reclaimAfterArchiveDays ?? 0,
|
|
106
|
+
keepSnapshots: Boolean(policy.keepSnapshots),
|
|
107
|
+
keepScratch: Boolean(policy.keepScratch),
|
|
108
|
+
reclaimStates: policy.reclaimStates && policy.reclaimStates.length ? policy.reclaimStates : ["completed", "failed"]
|
|
109
|
+
},
|
|
110
|
+
total: entries.length,
|
|
111
|
+
eligibleCount,
|
|
112
|
+
bytesToFree,
|
|
113
|
+
entries,
|
|
114
|
+
nextAction: eligibleCount ? "node scripts/cw.js gc run" : "node scripts/cw.js run search"
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/** Execute the write-ahead reclamation transaction for eligible runs. Bounded
|
|
118
|
+
* (`maxReclaimRuns` / `maxReclaimBytes`), fail-closed on any incomplete
|
|
119
|
+
* skeleton. Produces a tombstone and frees the bulk. */
|
|
120
|
+
function gcRun(host, options = {}) {
|
|
121
|
+
const scope = options.scope || "home";
|
|
122
|
+
const policy = reclamationPolicy(options.policy);
|
|
123
|
+
const nowIso = options.now || new Date().toISOString();
|
|
124
|
+
const nowMs = Date.parse(nowIso);
|
|
125
|
+
const records = options.runId ? recordsForRunId(host, options.runId, scope) : host.buildIndex(scope).records;
|
|
126
|
+
const maxRuns = options.limit ?? (policy.maxReclaimRuns || 0);
|
|
127
|
+
const maxBytes = policy.maxReclaimBytes || 0;
|
|
128
|
+
const reclaimed = [];
|
|
129
|
+
const refused = [];
|
|
130
|
+
let totalBytesFreed = 0;
|
|
131
|
+
for (const record of records) {
|
|
132
|
+
const refusal = reclaimEligibility(record, policy, nowMs);
|
|
133
|
+
if (refusal) {
|
|
134
|
+
refused.push({ runId: record.runId, code: refusal });
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (maxRuns > 0 && reclaimed.length >= maxRuns)
|
|
138
|
+
break;
|
|
139
|
+
let run;
|
|
140
|
+
try {
|
|
141
|
+
run = host.loadRun(record.repo, record.runId);
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
refused.push({ runId: record.runId, code: "unreadable" });
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
const result = (0, reclamation_1.runReclamation)(run, {
|
|
149
|
+
now: nowIso,
|
|
150
|
+
actor: options.actor,
|
|
151
|
+
policy: { reclaimAfterArchiveDays: policy.reclaimAfterArchiveDays, keepScratch: policy.keepScratch, keepSnapshots: policy.keepSnapshots },
|
|
152
|
+
reclaimPolicy: { keepScratch: policy.keepScratch, keepSnapshots: policy.keepSnapshots }
|
|
153
|
+
});
|
|
154
|
+
// No post-free saveCheckpoint: runReclamation now DURABLY persists the
|
|
155
|
+
// result-node re-point inside the transaction (before any byte is freed),
|
|
156
|
+
// so state.json can never reference a freed path even on a crash here.
|
|
157
|
+
reclaimed.push({
|
|
158
|
+
runId: record.runId,
|
|
159
|
+
bytesFreed: result.bytesFreed,
|
|
160
|
+
tombstoneHash: result.tombstone.tombstoneHash,
|
|
161
|
+
capability: result.tombstone.capability,
|
|
162
|
+
capabilityReason: result.tombstone.capabilityReason
|
|
163
|
+
});
|
|
164
|
+
// Independent reclamation WITNESS in the tamper-evident trust-audit chain:
|
|
165
|
+
// proves this run WAS reclaimed even if reclaimed.json is later deleted — so
|
|
166
|
+
// `gc verify` can tell proof-deletion apart from never-reclaimed.
|
|
167
|
+
(0, trust_audit_1.recordTrustAuditEvent)(run, {
|
|
168
|
+
kind: "run.reclaimed",
|
|
169
|
+
decision: "recorded",
|
|
170
|
+
source: "cw-validated",
|
|
171
|
+
metadata: { tombstoneHash: result.tombstone.tombstoneHash, bytesFreed: result.bytesFreed, capability: result.tombstone.capability }
|
|
172
|
+
});
|
|
173
|
+
totalBytesFreed += result.bytesFreed;
|
|
174
|
+
if (maxBytes > 0 && totalBytesFreed >= maxBytes)
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
if (error instanceof reclamation_1.ReclamationError)
|
|
179
|
+
refused.push({ runId: record.runId, code: error.code });
|
|
180
|
+
else
|
|
181
|
+
throw error;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
schemaVersion: 1,
|
|
186
|
+
scope,
|
|
187
|
+
generatedAt: nowIso,
|
|
188
|
+
dryRun: false,
|
|
189
|
+
reclaimed,
|
|
190
|
+
refused,
|
|
191
|
+
totalBytesFreed,
|
|
192
|
+
nextAction: reclaimed.length ? "node scripts/cw.js gc verify <run-id>" : "node scripts/cw.js gc plan"
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
/** Re-prove a reclaimed run: skeleton schema-complete, tombstone chain
|
|
196
|
+
* recomputed-and-untampered, each reconstructable artifact re-derived from its
|
|
197
|
+
* RETAINED inputs to its expectDigest, and eligible-when-reclaimed. */
|
|
198
|
+
function gcVerify(host, runId, options = {}) {
|
|
199
|
+
const scope = options.scope || "home";
|
|
200
|
+
const located = host.locate(runId, scope);
|
|
201
|
+
if (!located) {
|
|
202
|
+
return {
|
|
203
|
+
schemaVersion: 1,
|
|
204
|
+
runId,
|
|
205
|
+
reclaimed: false,
|
|
206
|
+
verified: false,
|
|
207
|
+
tier: "live",
|
|
208
|
+
capability: "re-runnable",
|
|
209
|
+
chainLength: 0,
|
|
210
|
+
checks: [{ name: "located", pass: false, code: "not-reclaimed", detail: "run source not found" }],
|
|
211
|
+
nextAction: "node scripts/cw.js registry refresh" + (scope === "home" ? " --scope home" : "")
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
const run = host.loadRun(located.record.repo, runId);
|
|
215
|
+
const result = (0, reclamation_1.verifyReclamation)(run);
|
|
216
|
+
const checks = result.checks.map((c) => ({ name: c.name, pass: c.pass, code: c.code, detail: c.detail }));
|
|
217
|
+
// Eligible-when-reclaimed: each tombstone must have sealed a terminal verdict.
|
|
218
|
+
let eligibleWhenReclaimed = result.reclaimed;
|
|
219
|
+
for (const tombstone of result.tombstones) {
|
|
220
|
+
const terminal = tombstone.skeleton.finalVerdict?.terminal === true;
|
|
221
|
+
if (!terminal) {
|
|
222
|
+
eligibleWhenReclaimed = false;
|
|
223
|
+
checks.push({ name: `eligible-when-reclaimed:${tombstone.tombstoneId}`, pass: false, code: "ineligible-when-reclaimed", detail: "non-terminal verdict sealed" });
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
const last = result.tombstones[result.tombstones.length - 1];
|
|
227
|
+
// Independent witness: a trust-audit "run.reclaimed" event proves this run was
|
|
228
|
+
// reclaimed even if reclaimed.json was deleted. A present witness + missing proof
|
|
229
|
+
// = the proof was deleted/tampered (NOT "never reclaimed") — fail closed so
|
|
230
|
+
// `gc verify <run> && deploy` cannot pass on a wiped reclamation record.
|
|
231
|
+
const witnessed = (0, trust_audit_1.listTrustAuditEvents)(run).some((event) => event.kind === "run.reclaimed");
|
|
232
|
+
const proofDeleted = witnessed && !result.reclaimed;
|
|
233
|
+
if (proofDeleted) {
|
|
234
|
+
checks.push({ name: "reclaim-witness", pass: false, code: "reclaim-proof-deleted", detail: "trust-audit attests reclamation but reclaimed.json is missing/empty" });
|
|
235
|
+
}
|
|
236
|
+
const reclaimed = result.reclaimed || proofDeleted;
|
|
237
|
+
const verified = result.verified && eligibleWhenReclaimed && !proofDeleted;
|
|
238
|
+
return {
|
|
239
|
+
schemaVersion: 1,
|
|
240
|
+
runId,
|
|
241
|
+
reclaimed,
|
|
242
|
+
verified,
|
|
243
|
+
tier: located.record.tier || (reclaimed ? "reclaimed" : "live"),
|
|
244
|
+
capability: located.record.capability || "re-runnable",
|
|
245
|
+
capabilityReason: located.record.capabilityReason,
|
|
246
|
+
tombstoneHash: last?.tombstoneHash,
|
|
247
|
+
chainLength: result.tombstones.length,
|
|
248
|
+
checks,
|
|
249
|
+
nextAction: verified ? "node scripts/cw.js run show " + runId : "node scripts/cw.js gc plan"
|
|
250
|
+
};
|
|
251
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_RUN_REGISTRY_POLICY = exports.RUN_REGISTRY_SCHEMA_VERSION = void 0;
|
|
4
|
+
exports.RUN_REGISTRY_SCHEMA_VERSION = 1;
|
|
5
|
+
exports.DEFAULT_RUN_REGISTRY_POLICY = {
|
|
6
|
+
schemaVersion: 1,
|
|
7
|
+
archiveOlderThanDays: 0,
|
|
8
|
+
archiveStates: ["completed", "failed"],
|
|
9
|
+
defaultQueuePriority: 100,
|
|
10
|
+
reclaimAfterArchiveDays: 0,
|
|
11
|
+
reclaimStates: ["completed", "failed"],
|
|
12
|
+
keepSnapshots: false,
|
|
13
|
+
keepScratch: false,
|
|
14
|
+
maxReclaimRuns: 0,
|
|
15
|
+
maxReclaimBytes: 0
|
|
16
|
+
};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.queueFilePath = queueFilePath;
|
|
7
|
+
exports.loadQueue = loadQueue;
|
|
8
|
+
exports.saveQueue = saveQueue;
|
|
9
|
+
exports.queueAdd = queueAdd;
|
|
10
|
+
exports.queueList = queueList;
|
|
11
|
+
exports.queueShow = queueShow;
|
|
12
|
+
exports.queueDrain = queueDrain;
|
|
13
|
+
// Durable run-queue operations for the run registry (FreeBSD-audit R2 deep).
|
|
14
|
+
// Carved out of run-registry.ts so the RunRegistry class no longer bundles the
|
|
15
|
+
// stateful queue cluster; the class keeps the public methods as thin delegators.
|
|
16
|
+
//
|
|
17
|
+
// BEHAVIOR-PRESERVING — pure code movement, zero logic change. Each function
|
|
18
|
+
// takes a `QueueHost` (the registry, narrowed to exactly the file-access +
|
|
19
|
+
// repo-registration helpers the queue needs) so it stays a function of its
|
|
20
|
+
// inputs, matching the existing router pattern (orchestrator/*-operations.ts,
|
|
21
|
+
// run-registry/derive.ts + format.ts).
|
|
22
|
+
//
|
|
23
|
+
// The queue file lives beside the other home-registry plain files (EXPLICIT,
|
|
24
|
+
// INSPECTABLE STATE): readable, diffable, no hidden database. Cross-process
|
|
25
|
+
// read-modify-write is locked (v0.1.40, P1-D) so a concurrent add/drain can
|
|
26
|
+
// never drop or double-drain an entry.
|
|
27
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
28
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
29
|
+
const state_1 = require("../state");
|
|
30
|
+
const derive_1 = require("./derive");
|
|
31
|
+
function queueFilePath(host) {
|
|
32
|
+
return node_path_1.default.join(host.homeRegistryDir(), "queue.json");
|
|
33
|
+
}
|
|
34
|
+
function loadQueue(host) {
|
|
35
|
+
const file = queueFilePath(host);
|
|
36
|
+
if (!node_fs_1.default.existsSync(file))
|
|
37
|
+
return [];
|
|
38
|
+
try {
|
|
39
|
+
const parsed = (0, state_1.readJson)(file);
|
|
40
|
+
return Array.isArray(parsed.entries) ? parsed.entries : [];
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function saveQueue(host, entries) {
|
|
47
|
+
(0, state_1.writeJson)(queueFilePath(host), { schemaVersion: 1, entries }, { durable: true });
|
|
48
|
+
}
|
|
49
|
+
function queueAdd(host, options = {}) {
|
|
50
|
+
const repo = options.repo ? node_path_1.default.resolve(options.repo) : host.repoRoot;
|
|
51
|
+
// Cross-process read-modify-write on the home queue: lock so a concurrently
|
|
52
|
+
// added task can never vanish (v0.1.40, P1-D).
|
|
53
|
+
return (0, state_1.withFileLock)(queueFilePath(host), () => {
|
|
54
|
+
const entries = loadQueue(host);
|
|
55
|
+
const entry = {
|
|
56
|
+
schemaVersion: 1,
|
|
57
|
+
id: options.id || (0, derive_1.queueId)(),
|
|
58
|
+
runId: options.runId,
|
|
59
|
+
appId: options.appId,
|
|
60
|
+
workflowId: options.workflowId,
|
|
61
|
+
repo,
|
|
62
|
+
priority: Number.isFinite(options.priority) ? Number(options.priority) : host.defaultQueuePriority,
|
|
63
|
+
enqueuedAt: new Date().toISOString(),
|
|
64
|
+
status: "pending",
|
|
65
|
+
inputs: options.inputs,
|
|
66
|
+
note: options.note
|
|
67
|
+
};
|
|
68
|
+
entries.push(entry);
|
|
69
|
+
host.registerRepo(repo);
|
|
70
|
+
saveQueue(host, entries);
|
|
71
|
+
return entry;
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
function queueList(host, options = {}) {
|
|
75
|
+
let entries = loadQueue(host);
|
|
76
|
+
if (options.status)
|
|
77
|
+
entries = entries.filter((e) => e.status === options.status);
|
|
78
|
+
if (options.repo) {
|
|
79
|
+
const repo = node_path_1.default.resolve(options.repo);
|
|
80
|
+
entries = entries.filter((e) => node_path_1.default.resolve(e.repo) === repo);
|
|
81
|
+
}
|
|
82
|
+
entries = [...entries].sort(derive_1.compareQueue);
|
|
83
|
+
return { schemaVersion: 1, total: entries.length, entries };
|
|
84
|
+
}
|
|
85
|
+
function queueShow(host, id) {
|
|
86
|
+
const entry = loadQueue(host).find((e) => e.id === id);
|
|
87
|
+
if (!entry)
|
|
88
|
+
throw new Error(`Queue entry not found: ${id}`);
|
|
89
|
+
return entry;
|
|
90
|
+
}
|
|
91
|
+
/** Drain the next N ready/pending entries in policy order, marking them drained.
|
|
92
|
+
* CW records readiness/order; the HOST still executes the workers. */
|
|
93
|
+
function queueDrain(host, options = {}) {
|
|
94
|
+
const limit = (0, derive_1.clampInt)(options.limit, 1, 1);
|
|
95
|
+
const repoFilter = options.repo ? node_path_1.default.resolve(options.repo) : undefined;
|
|
96
|
+
// Lock the drain RMW so two hosts can never double-drain the same entry
|
|
97
|
+
// (v0.1.40, P1-D — the scheduling kernel's concurrency ceiling now holds
|
|
98
|
+
// across processes, not just within one).
|
|
99
|
+
return (0, state_1.withFileLock)(queueFilePath(host), () => {
|
|
100
|
+
const entries = loadQueue(host);
|
|
101
|
+
const drainable = entries
|
|
102
|
+
.filter((e) => e.status === "pending" || e.status === "ready")
|
|
103
|
+
.filter((e) => !repoFilter || node_path_1.default.resolve(e.repo) === repoFilter)
|
|
104
|
+
.sort(derive_1.compareQueue);
|
|
105
|
+
const drained = [];
|
|
106
|
+
const drainedAt = new Date().toISOString();
|
|
107
|
+
for (const entry of drainable.slice(0, limit)) {
|
|
108
|
+
entry.status = "drained";
|
|
109
|
+
entry.drainedAt = drainedAt;
|
|
110
|
+
drained.push(entry);
|
|
111
|
+
}
|
|
112
|
+
saveQueue(host, entries);
|
|
113
|
+
const remaining = entries.filter((e) => e.status === "pending" || e.status === "ready").length;
|
|
114
|
+
return { schemaVersion: 1, drained, remaining };
|
|
115
|
+
});
|
|
116
|
+
}
|