cool-workflow 0.1.81 → 0.1.83
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 +2 -2
- package/.codex-plugin/plugin.json +4 -4
- package/README.md +125 -121
- 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/candidate-scoring.js +20 -26
- package/dist/capability-core.js +80 -93
- package/dist/capability-registry.js +30 -3
- package/dist/cli.js +12 -1
- package/dist/commit.js +217 -204
- package/dist/coordinator/util.js +6 -9
- package/dist/dispatch.js +11 -3
- package/dist/doctor.js +153 -0
- package/dist/evidence-reasoning.js +4 -1
- package/dist/execution-backend/agent.js +11 -48
- package/dist/execution-backend.js +11 -31
- package/dist/gates.js +48 -0
- package/dist/mcp-server.js +11 -0
- package/dist/multi-agent/helpers.js +6 -10
- package/dist/multi-agent/ids.js +20 -0
- package/dist/multi-agent-eval.js +27 -1
- package/dist/multi-agent-host.js +53 -21
- package/dist/multi-agent-operator-ux.js +2 -1
- package/dist/multi-agent-trust.js +5 -5
- package/dist/node-projection.js +59 -0
- package/dist/node-snapshot.js +8 -18
- package/dist/orchestrator/lifecycle-operations.js +22 -1
- package/dist/orchestrator.js +29 -2
- package/dist/reclamation/hash.js +72 -0
- package/dist/reclamation.js +31 -112
- package/dist/run-registry/queue.js +6 -7
- package/dist/run-registry.js +35 -24
- package/dist/scheduler.js +112 -57
- package/dist/topology.js +25 -4
- package/dist/trust-audit.js +70 -38
- package/dist/validation.js +328 -0
- package/dist/version.js +1 -1
- package/dist/worker-isolation.js +163 -58
- package/docs/agent-delegation-drive.7.md +90 -85
- package/docs/agent-framework.md +33 -32
- package/docs/candidate-scoring.7.md +26 -24
- package/docs/canonical-workflow-apps.7.md +40 -40
- package/docs/capability-topology-registry.7.md +24 -24
- package/docs/cli-mcp-parity.7.md +227 -154
- package/docs/contract-migration-tooling.7.md +49 -41
- package/docs/control-plane-scheduling.7.md +45 -40
- package/docs/coordinator-blackboard.7.md +30 -30
- package/docs/dogfood-one-real-repo.7.md +44 -44
- package/docs/durable-state-and-locking.7.md +35 -30
- package/docs/end-to-end-golden-path.7.md +29 -29
- package/docs/error-feedback.7.md +27 -27
- package/docs/evidence-adoption-reasoning-chain.7.md +63 -58
- package/docs/execution-backends.7.md +84 -79
- package/docs/getting-started.md +35 -18
- package/docs/index.md +3 -3
- package/docs/mcp-app-surface.7.md +64 -64
- package/docs/multi-agent-cli-mcp-surface.7.md +83 -77
- package/docs/multi-agent-eval-replay-harness.7.md +59 -54
- package/docs/multi-agent-operator-ux.7.md +70 -65
- package/docs/multi-agent-runtime-core.7.md +39 -39
- package/docs/multi-agent-topologies.7.md +24 -24
- package/docs/multi-agent-trust-policy-audit.7.md +38 -38
- package/docs/node-snapshot-diff-replay.7.md +26 -21
- package/docs/observability-cost-accounting.7.md +50 -45
- package/docs/operator-ux.7.md +30 -30
- package/docs/pipeline-runner.7.md +31 -31
- package/docs/project-index.md +15 -6
- package/docs/real-execution-backends.7.md +47 -42
- package/docs/release-and-migration.7.md +43 -38
- package/docs/release-tooling.7.md +74 -39
- package/docs/routines.md +16 -16
- package/docs/run-registry-control-plane.7.md +120 -115
- package/docs/run-retention-reclamation.7.md +46 -41
- package/docs/sandbox-profiles.7.md +32 -32
- package/docs/scheduled-tasks.md +14 -14
- package/docs/security-trust-hardening.7.md +29 -29
- package/docs/source-context-profiles.7.md +28 -28
- package/docs/state-explosion-management.7.md +64 -59
- package/docs/state-node.7.md +8 -8
- package/docs/team-collaboration.7.md +63 -58
- package/docs/trust-model.md +126 -126
- package/docs/unix-principles.md +80 -80
- package/docs/vendor-manifest-loadability.7.md +20 -20
- package/docs/verifier-gated-commit.7.md +16 -16
- package/docs/web-desktop-workbench.7.md +70 -65
- package/docs/worker-isolation.7.md +34 -37
- package/docs/workflow-app-framework.7.md +38 -38
- package/manifest/plugin.manifest.json +4 -4
- package/manifest/source-context-profiles.json +1 -1
- package/package.json +3 -2
- package/scripts/canonical-apps.js +4 -4
- package/scripts/children/batch-delegate-child.js +58 -0
- package/scripts/children/http-delegate-child.js +39 -0
- package/scripts/dogfood-release.js +1 -1
- package/scripts/gen-parity-doc.js +106 -0
- package/scripts/golden-path.js +4 -4
- package/scripts/release-flow.js +181 -5
- package/dist/verifier-registry.js +0 -46
|
@@ -1,7 +1,4 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
3
|
exports.policyForRole = policyForRole;
|
|
7
4
|
exports.policyForGroup = policyForGroup;
|
|
@@ -16,7 +13,7 @@ exports.summarizeMultiAgentTrust = summarizeMultiAgentTrust;
|
|
|
16
13
|
exports.hasAcceptedJudgeRationale = hasAcceptedJudgeRationale;
|
|
17
14
|
exports.sourceForActor = sourceForActor;
|
|
18
15
|
exports.hashText = hashText;
|
|
19
|
-
const
|
|
16
|
+
const execution_backend_1 = require("./execution-backend");
|
|
20
17
|
const trust_audit_1 = require("./trust-audit");
|
|
21
18
|
function policyForRole(role) {
|
|
22
19
|
const topologyRole = String(role.metadata?.topologyRoleId || role.title || "").toLowerCase();
|
|
@@ -294,8 +291,11 @@ function sourceForActor(actor) {
|
|
|
294
291
|
return "runtime-derived";
|
|
295
292
|
return "cw-validated";
|
|
296
293
|
}
|
|
294
|
+
// Delegates to the shared execution-backend sha256 (F10 dedup). Byte-identical:
|
|
295
|
+
// both emit `sha256:<hex>` and Node's Hash.update(string) defaults to utf8, the
|
|
296
|
+
// same encoding the shared helper passes explicitly.
|
|
297
297
|
function hashText(value) {
|
|
298
|
-
return
|
|
298
|
+
return (0, execution_backend_1.sha256)(value);
|
|
299
299
|
}
|
|
300
300
|
function resolvePolicy(run, input) {
|
|
301
301
|
const membership = input.membershipId ? run.multiAgent?.memberships.find((entry) => entry.id === input.membershipId) : undefined;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Canonical StateNode projection (F9 de-triplication) — the SINGLE source of
|
|
3
|
+
// truth for which StateNode fields make up the derived, fingerprinted body.
|
|
4
|
+
//
|
|
5
|
+
// Previously this 13-field set was hand-copied in three places: node-snapshot.ts's
|
|
6
|
+
// snapshotBody, reclamation.ts's snapshotProjectionDigest, and reclamation.ts's
|
|
7
|
+
// rawNodeBody/nodeBodyDigest (whose comment literally said "Mirror
|
|
8
|
+
// node-snapshot.ts"). Any field added in one place but not the others would drift
|
|
9
|
+
// the projection — and because reclamation hashes are tombstone-chained, a drift
|
|
10
|
+
// would silently break the chain. This module collapses the list to ONE export so
|
|
11
|
+
// the field set can only change in one place.
|
|
12
|
+
//
|
|
13
|
+
// BYTE-IDENTITY [load-bearing]: the projection / digest OUTPUT here is identical to
|
|
14
|
+
// the prior in-line implementations. `normalizeValue` sorts object keys, so the
|
|
15
|
+
// literal field ORDER never affected the bytes — only the field SET ever mattered.
|
|
16
|
+
// `replayStableStringify(value) === JSON.stringify(normalizeValue(value))` and
|
|
17
|
+
// `normalizeValue` is idempotent, so the projected-body digest and the raw-body
|
|
18
|
+
// digest funnel through the same canonical bytes (node-snapshot-diff-replay +
|
|
19
|
+
// reclamation smokes verify this).
|
|
20
|
+
//
|
|
21
|
+
// Pure: no I/O, no wall-clock, no random — safe in core/state/replay logic.
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.rawNodeProjection = rawNodeProjection;
|
|
24
|
+
exports.projectNodeBody = projectNodeBody;
|
|
25
|
+
exports.nodeProjectionDigestInput = nodeProjectionDigestInput;
|
|
26
|
+
const multi_agent_eval_1 = require("./multi-agent-eval");
|
|
27
|
+
/** The raw (un-normalized) projection input: the canonical field set, copied off
|
|
28
|
+
* the StateNode with no scrubbing. `normalizeValue`/`replayStableStringify` apply
|
|
29
|
+
* the timestamp/path scrubbing downstream. This is the ONE place the field list
|
|
30
|
+
* lives — add or drop a projected field here and every consumer follows. */
|
|
31
|
+
function rawNodeProjection(node) {
|
|
32
|
+
return {
|
|
33
|
+
id: node.id,
|
|
34
|
+
kind: node.kind,
|
|
35
|
+
status: node.status,
|
|
36
|
+
loopStage: node.loopStage,
|
|
37
|
+
inputs: node.inputs,
|
|
38
|
+
outputs: node.outputs,
|
|
39
|
+
artifacts: node.artifacts,
|
|
40
|
+
evidence: node.evidence,
|
|
41
|
+
errors: node.errors,
|
|
42
|
+
parents: node.parents,
|
|
43
|
+
children: node.children,
|
|
44
|
+
contractId: node.contractId,
|
|
45
|
+
metadata: node.metadata
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** The normalized, derived projected body of one StateNode — timestamps/paths
|
|
49
|
+
* stripped via the eval normalizer, so it is byte-stable across captures of the
|
|
50
|
+
* same logical state. This is exactly node-snapshot.ts's historical snapshotBody. */
|
|
51
|
+
function projectNodeBody(node) {
|
|
52
|
+
return (0, multi_agent_eval_1.normalizeValue)(rawNodeProjection(node));
|
|
53
|
+
}
|
|
54
|
+
/** Stable digest input for the projected body: the canonical bytes that the
|
|
55
|
+
* snapshot/reconstruction digests bind. `replayStableStringify` re-normalizes, so
|
|
56
|
+
* feeding the raw projection or the already-normalized body yields identical bytes. */
|
|
57
|
+
function nodeProjectionDigestInput(node) {
|
|
58
|
+
return (0, multi_agent_eval_1.replayStableStringify)(rawNodeProjection(node));
|
|
59
|
+
}
|
package/dist/node-snapshot.js
CHANGED
|
@@ -37,7 +37,9 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
37
37
|
const pipeline_runner_1 = require("./pipeline-runner");
|
|
38
38
|
const state_1 = require("./state");
|
|
39
39
|
const multi_agent_eval_1 = require("./multi-agent-eval");
|
|
40
|
+
const node_projection_1 = require("./node-projection");
|
|
40
41
|
const state_explosion_1 = require("./state-explosion");
|
|
42
|
+
const validation_1 = require("./validation");
|
|
41
43
|
exports.NODE_SNAPSHOT_SCHEMA_VERSION = 1;
|
|
42
44
|
/** Structured fail-closed error (mirrors the PipelineContractError shape). */
|
|
43
45
|
class NodeSnapshotError extends Error {
|
|
@@ -64,23 +66,11 @@ const SNAPSHOT_SECTIONS = [
|
|
|
64
66
|
"metadata"
|
|
65
67
|
];
|
|
66
68
|
/** The normalized projection of a node — timestamps/paths stripped by the eval
|
|
67
|
-
* normalizer, so it is byte-stable across captures of the same logical state.
|
|
69
|
+
* normalizer, so it is byte-stable across captures of the same logical state. The
|
|
70
|
+
* canonical field set lives in node-projection.ts (shared with reclamation.ts so
|
|
71
|
+
* the projection can never drift across the two). */
|
|
68
72
|
function snapshotBody(node) {
|
|
69
|
-
return (0,
|
|
70
|
-
id: node.id,
|
|
71
|
-
kind: node.kind,
|
|
72
|
-
status: node.status,
|
|
73
|
-
loopStage: node.loopStage,
|
|
74
|
-
inputs: node.inputs,
|
|
75
|
-
outputs: node.outputs,
|
|
76
|
-
artifacts: node.artifacts,
|
|
77
|
-
evidence: node.evidence,
|
|
78
|
-
errors: node.errors,
|
|
79
|
-
parents: node.parents,
|
|
80
|
-
children: node.children,
|
|
81
|
-
contractId: node.contractId,
|
|
82
|
-
metadata: node.metadata
|
|
83
|
-
});
|
|
73
|
+
return (0, node_projection_1.projectNodeBody)(node);
|
|
84
74
|
}
|
|
85
75
|
/** RAW fingerprint (NOT normalized): any transition (updatedAt/status) or
|
|
86
76
|
* artifact/evidence change flips it, which is how drift is detected. */
|
|
@@ -113,7 +103,7 @@ function readNodeSnapshot(run, snapshotId) {
|
|
|
113
103
|
for (const nodeDir of node_fs_1.default.readdirSync(root)) {
|
|
114
104
|
const file = node_path_1.default.join(root, nodeDir, `${snapshotId}.json`);
|
|
115
105
|
if (node_fs_1.default.existsSync(file))
|
|
116
|
-
return JSON.parse(node_fs_1.default.readFileSync(file, "utf8"));
|
|
106
|
+
return (0, validation_1.validateNodeSnapshot)(JSON.parse(node_fs_1.default.readFileSync(file, "utf8")));
|
|
117
107
|
}
|
|
118
108
|
}
|
|
119
109
|
throw new NodeSnapshotError("snapshot-not-found", `Node snapshot ${snapshotId} not found in run ${run.id}`, { freshness: "absent" });
|
|
@@ -125,7 +115,7 @@ function readNodeReplay(run, replayId) {
|
|
|
125
115
|
for (const nodeDir of node_fs_1.default.readdirSync(root)) {
|
|
126
116
|
const file = node_path_1.default.join(root, nodeDir, "replays", `${replayId}.json`);
|
|
127
117
|
if (node_fs_1.default.existsSync(file))
|
|
128
|
-
return JSON.parse(node_fs_1.default.readFileSync(file, "utf8"));
|
|
118
|
+
return (0, validation_1.validateNodeReplayRun)(JSON.parse(node_fs_1.default.readFileSync(file, "utf8")));
|
|
129
119
|
}
|
|
130
120
|
}
|
|
131
121
|
throw new NodeSnapshotError("replay-not-found", `Node replay ${replayId} not found in run ${run.id}`, { freshness: "absent" });
|
|
@@ -17,6 +17,7 @@ exports.commit = commit;
|
|
|
17
17
|
// plan() receives an already-resolved workflow app record (the runner still owns
|
|
18
18
|
// app loading, which is instance-stateful). Behavior is identical to the inline
|
|
19
19
|
// implementations; only the location changed.
|
|
20
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
20
21
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
21
22
|
const node_path_1 = __importDefault(require("node:path"));
|
|
22
23
|
const state_1 = require("../state");
|
|
@@ -456,8 +457,28 @@ function renderPrompt(prompt, inputs) {
|
|
|
456
457
|
}
|
|
457
458
|
return rendered;
|
|
458
459
|
}
|
|
460
|
+
// Deterministic run id (replay-determinism self-audit): the wall-clock stamp is an
|
|
461
|
+
// edge timestamp (recorded once and stripped on replay), but the former
|
|
462
|
+
// Math.random() suffix made the run id itself non-reproducible — re-deriving the id
|
|
463
|
+
// for the SAME recorded run would never match. The suffix is now a content hash of
|
|
464
|
+
// the run's deterministic identity (workflowId + the recorded stamp), so the id is a
|
|
465
|
+
// pure function of inputs that already live in state. Distinct plan() invocations
|
|
466
|
+
// still get distinct ids because the per-millisecond stamp differs; replaying a
|
|
467
|
+
// recorded run reproduces the byte-identical id. Mirrors the de-clock done for
|
|
468
|
+
// worker ids in src/worker-isolation/paths.ts.
|
|
469
|
+
let runIdSequence = 0;
|
|
459
470
|
function createRunId(workflowId) {
|
|
460
471
|
const stamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\..+/, "Z");
|
|
461
|
-
|
|
472
|
+
// The stamp is second-resolution, so several runs of the same workflowId minted
|
|
473
|
+
// within one second would otherwise hash to the SAME id. process.pid + a monotonic
|
|
474
|
+
// counter break the tie across BOTH same-process (counter) and concurrent-process
|
|
475
|
+
// (pid) minting — deterministic-by-environment, not a PRNG, so it keeps the
|
|
476
|
+
// replay-determinism intent; the id is an edge stamp stripped on replay anyway.
|
|
477
|
+
runIdSequence += 1;
|
|
478
|
+
const suffix = node_crypto_1.default
|
|
479
|
+
.createHash("sha256")
|
|
480
|
+
.update(`${workflowId}:${stamp}:${process.pid}:${runIdSequence}`)
|
|
481
|
+
.digest("hex")
|
|
482
|
+
.slice(0, 6);
|
|
462
483
|
return `${workflowId}-${stamp}-${suffix}`;
|
|
463
484
|
}
|
package/dist/orchestrator.js
CHANGED
|
@@ -90,10 +90,24 @@ class CoolWorkflowRunner {
|
|
|
90
90
|
pluginRoot;
|
|
91
91
|
workflowsDir;
|
|
92
92
|
appsDir;
|
|
93
|
-
|
|
93
|
+
// F7: the directory a run is resolved against (replaces the former process.chdir
|
|
94
|
+
// bracket in capability-core). undefined => fall back to process.cwd(). The runner
|
|
95
|
+
// reads runs from disk per call (no in-memory run state), so withBaseDir hands back
|
|
96
|
+
// a cheap scoped clone instead of mutating the global process cwd.
|
|
97
|
+
baseDir;
|
|
98
|
+
constructor({ pluginRoot, baseDir }) {
|
|
94
99
|
this.pluginRoot = resolvePluginRoot(pluginRoot);
|
|
95
100
|
this.workflowsDir = node_path_1.default.join(this.pluginRoot, "workflows");
|
|
96
101
|
this.appsDir = node_path_1.default.join(this.pluginRoot, "apps");
|
|
102
|
+
this.baseDir = baseDir ? node_path_1.default.resolve(baseDir) : undefined;
|
|
103
|
+
}
|
|
104
|
+
/** Return a runner that resolves runs against `dir` instead of process.cwd(),
|
|
105
|
+
* WITHOUT chdir-ing the process (F7). Same instance when the dir is unchanged. */
|
|
106
|
+
withBaseDir(dir) {
|
|
107
|
+
const resolved = dir ? node_path_1.default.resolve(dir) : undefined;
|
|
108
|
+
if (resolved === this.baseDir)
|
|
109
|
+
return this;
|
|
110
|
+
return new CoolWorkflowRunner({ pluginRoot: this.pluginRoot, baseDir: resolved });
|
|
97
111
|
}
|
|
98
112
|
listWorkflows() {
|
|
99
113
|
return this.loadWorkflowApps().map((record) => {
|
|
@@ -700,7 +714,7 @@ class CoolWorkflowRunner {
|
|
|
700
714
|
return feedbackOps.resolveFeedback(this.loadRun(runId), feedbackId, options);
|
|
701
715
|
}
|
|
702
716
|
loadRun(runId) {
|
|
703
|
-
return (0, state_1.loadRunFromCwd)(runId);
|
|
717
|
+
return (0, state_1.loadRunFromCwd)(runId, this.baseDir);
|
|
704
718
|
}
|
|
705
719
|
loadWorkflowAppById(appId) {
|
|
706
720
|
const record = this.loadWorkflowApps().find((candidate) => candidate.app.id === appId);
|
|
@@ -807,6 +821,7 @@ function formatHelp() {
|
|
|
807
821
|
"",
|
|
808
822
|
"Commands:",
|
|
809
823
|
" list",
|
|
824
|
+
" doctor [--json] (check your setup: Node, agent backend, git, writable state)",
|
|
810
825
|
" init <workflow-id> [--title TEXT] [--output PATH]",
|
|
811
826
|
" quickstart [app-id] [--repo PATH] [--question TEXT] [--agent-command CMD] [--once] [--preview]",
|
|
812
827
|
" plan <workflow-id> [--repo PATH] [--question TEXT] [--invariant TEXT]",
|
|
@@ -859,6 +874,18 @@ function formatHelp() {
|
|
|
859
874
|
" run search|list|show|resume|archive|rerun|export|import|verify-import [run-id|archive] [--scope repo|home] [--json]",
|
|
860
875
|
" queue add|list|drain|show [queue-id] [--repo PATH] [--priority N]",
|
|
861
876
|
" history [--scope repo|home] [--app ID] [--status STATE] [--json]",
|
|
877
|
+
" audit-run <app-id> [--repo PATH] [--question TEXT] [--agent-command CMD]",
|
|
878
|
+
" metrics show|summary <run-id> [--scope repo|home] [--json]",
|
|
879
|
+
" telemetry verify <run-id> [--pubkey PEM|PATH] [--json]",
|
|
880
|
+
" gc plan|run|verify [run-id] [--json]",
|
|
881
|
+
" sched plan|lease|release|complete|reclaim|reset|policy [--json]",
|
|
882
|
+
" migration list|check|prove [target] [--json]",
|
|
883
|
+
" operator status|report <run-id> [--json]",
|
|
884
|
+
" review status|policy <run-id> [--json]",
|
|
885
|
+
" approve|reject|comment <kind> <run-id> <target-id> [--reason TEXT]",
|
|
886
|
+
" handoff <kind> <run-id> <target-id> [--to ROLE]",
|
|
887
|
+
" loop --prompt TEXT [--interval-minutes N]",
|
|
888
|
+
" demo tamper",
|
|
862
889
|
" workbench view <run-id> [--json]",
|
|
863
890
|
" workbench serve [--port N] [--scope repo|home] [--once|--json]",
|
|
864
891
|
""
|
|
@@ -0,0 +1,72 @@
|
|
|
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.sha256OfString = sha256OfString;
|
|
7
|
+
exports.sha256OfFile = sha256OfFile;
|
|
8
|
+
exports.dirBytes = dirBytes;
|
|
9
|
+
exports.contentDigest = contentDigest;
|
|
10
|
+
// Content addressing + byte measurement for run reclamation (NO `du` — in-process
|
|
11
|
+
// only). Carved out of reclamation.ts (FreeBSD-audit god-module carve) so the pure
|
|
12
|
+
// content-addressing leaf no longer sits inside the write-ahead reclamation
|
|
13
|
+
// transaction. These are pure functions of their path/string inputs — no run
|
|
14
|
+
// state, no module-level mutable state.
|
|
15
|
+
//
|
|
16
|
+
// BEHAVIOR-PRESERVING — pure code movement, zero logic change. reclamation.ts
|
|
17
|
+
// re-exports the public symbols (sha256OfString/sha256OfFile/dirBytes) so the
|
|
18
|
+
// module's surface stays byte-identical.
|
|
19
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
20
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
21
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
22
|
+
const compare_1 = require("../compare");
|
|
23
|
+
function sha256Hex(value) {
|
|
24
|
+
return node_crypto_1.default.createHash("sha256").update(value).digest("hex");
|
|
25
|
+
}
|
|
26
|
+
function sha256OfString(value) {
|
|
27
|
+
return `sha256:${sha256Hex(value)}`;
|
|
28
|
+
}
|
|
29
|
+
function sha256OfFile(file) {
|
|
30
|
+
return `sha256:${sha256Hex(node_fs_1.default.readFileSync(file))}`;
|
|
31
|
+
}
|
|
32
|
+
/** Walk a path and sum file sizes IN-PROCESS (no `du`). Returns 0 if absent. A
|
|
33
|
+
* file returns its own size; a dir returns the recursive sum. */
|
|
34
|
+
function dirBytes(p) {
|
|
35
|
+
let total = 0;
|
|
36
|
+
let stat;
|
|
37
|
+
try {
|
|
38
|
+
stat = node_fs_1.default.statSync(p);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
43
|
+
if (stat.isFile())
|
|
44
|
+
return stat.size;
|
|
45
|
+
if (!stat.isDirectory())
|
|
46
|
+
return 0;
|
|
47
|
+
for (const entry of node_fs_1.default.readdirSync(p, { withFileTypes: true })) {
|
|
48
|
+
total += dirBytes(node_path_1.default.join(p, entry.name));
|
|
49
|
+
}
|
|
50
|
+
return total;
|
|
51
|
+
}
|
|
52
|
+
/** Stable content digest of a path (file = its bytes; dir = digest over each
|
|
53
|
+
* member's relative path + bytes, sorted). Lets the freed-manifest record a
|
|
54
|
+
* single sha per freed dir. */
|
|
55
|
+
function contentDigest(p) {
|
|
56
|
+
const stat = node_fs_1.default.statSync(p);
|
|
57
|
+
if (stat.isFile())
|
|
58
|
+
return sha256OfFile(p);
|
|
59
|
+
const parts = [];
|
|
60
|
+
const walk = (dir, rel) => {
|
|
61
|
+
for (const entry of node_fs_1.default.readdirSync(dir, { withFileTypes: true }).sort((a, b) => (0, compare_1.compareBytes)(a.name, b.name))) {
|
|
62
|
+
const abs = node_path_1.default.join(dir, entry.name);
|
|
63
|
+
const r = node_path_1.default.join(rel, entry.name);
|
|
64
|
+
if (entry.isDirectory())
|
|
65
|
+
walk(abs, r);
|
|
66
|
+
else
|
|
67
|
+
parts.push(`${r}:${sha256OfFile(abs)}`);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
walk(p, "");
|
|
71
|
+
return sha256OfString(parts.join("\n"));
|
|
72
|
+
}
|
package/dist/reclamation.js
CHANGED
|
@@ -29,10 +29,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
29
29
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
30
30
|
};
|
|
31
31
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
32
|
-
exports.ReclamationError = exports.ReclamationAbort = exports.SKELETON_REQUIRED_KEYS = void 0;
|
|
33
|
-
exports.sha256OfString = sha256OfString;
|
|
34
|
-
exports.sha256OfFile = sha256OfFile;
|
|
35
|
-
exports.dirBytes = dirBytes;
|
|
32
|
+
exports.dirBytes = exports.sha256OfFile = exports.sha256OfString = exports.ReclamationError = exports.ReclamationAbort = exports.SKELETON_REQUIRED_KEYS = void 0;
|
|
36
33
|
exports.reclaimedLogPath = reclaimedLogPath;
|
|
37
34
|
exports.loadReclamationLog = loadReclamationLog;
|
|
38
35
|
exports.extractSkeleton = extractSkeleton;
|
|
@@ -49,10 +46,14 @@ exports.runReclamation = runReclamation;
|
|
|
49
46
|
exports.reconstructArtifact = reconstructArtifact;
|
|
50
47
|
exports.verifyReclamation = verifyReclamation;
|
|
51
48
|
exports.dominantFailureCode = dominantFailureCode;
|
|
52
|
-
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
53
49
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
54
50
|
const node_path_1 = __importDefault(require("node:path"));
|
|
51
|
+
const hash_1 = require("./reclamation/hash");
|
|
52
|
+
Object.defineProperty(exports, "dirBytes", { enumerable: true, get: function () { return hash_1.dirBytes; } });
|
|
53
|
+
Object.defineProperty(exports, "sha256OfFile", { enumerable: true, get: function () { return hash_1.sha256OfFile; } });
|
|
54
|
+
Object.defineProperty(exports, "sha256OfString", { enumerable: true, get: function () { return hash_1.sha256OfString; } });
|
|
55
55
|
const multi_agent_eval_1 = require("./multi-agent-eval");
|
|
56
|
+
const node_projection_1 = require("./node-projection");
|
|
56
57
|
const node_snapshot_1 = require("./node-snapshot");
|
|
57
58
|
const state_1 = require("./state");
|
|
58
59
|
const trust_audit_1 = require("./trust-audit");
|
|
@@ -94,59 +95,6 @@ class ReclamationError extends Error {
|
|
|
94
95
|
}
|
|
95
96
|
}
|
|
96
97
|
exports.ReclamationError = ReclamationError;
|
|
97
|
-
// ---------------------------------------------------------------------------
|
|
98
|
-
// Content addressing + byte measurement (NO `du` — in-process only).
|
|
99
|
-
// ---------------------------------------------------------------------------
|
|
100
|
-
function sha256Hex(value) {
|
|
101
|
-
return node_crypto_1.default.createHash("sha256").update(value).digest("hex");
|
|
102
|
-
}
|
|
103
|
-
function sha256OfString(value) {
|
|
104
|
-
return `sha256:${sha256Hex(value)}`;
|
|
105
|
-
}
|
|
106
|
-
function sha256OfFile(file) {
|
|
107
|
-
return `sha256:${sha256Hex(node_fs_1.default.readFileSync(file))}`;
|
|
108
|
-
}
|
|
109
|
-
/** Walk a path and sum file sizes IN-PROCESS (no `du`). Returns 0 if absent. A
|
|
110
|
-
* file returns its own size; a dir returns the recursive sum. */
|
|
111
|
-
function dirBytes(p) {
|
|
112
|
-
let total = 0;
|
|
113
|
-
let stat;
|
|
114
|
-
try {
|
|
115
|
-
stat = node_fs_1.default.statSync(p);
|
|
116
|
-
}
|
|
117
|
-
catch {
|
|
118
|
-
return 0;
|
|
119
|
-
}
|
|
120
|
-
if (stat.isFile())
|
|
121
|
-
return stat.size;
|
|
122
|
-
if (!stat.isDirectory())
|
|
123
|
-
return 0;
|
|
124
|
-
for (const entry of node_fs_1.default.readdirSync(p, { withFileTypes: true })) {
|
|
125
|
-
total += dirBytes(node_path_1.default.join(p, entry.name));
|
|
126
|
-
}
|
|
127
|
-
return total;
|
|
128
|
-
}
|
|
129
|
-
/** Stable content digest of a path (file = its bytes; dir = digest over each
|
|
130
|
-
* member's relative path + bytes, sorted). Lets the freed-manifest record a
|
|
131
|
-
* single sha per freed dir. */
|
|
132
|
-
function contentDigest(p) {
|
|
133
|
-
const stat = node_fs_1.default.statSync(p);
|
|
134
|
-
if (stat.isFile())
|
|
135
|
-
return sha256OfFile(p);
|
|
136
|
-
const parts = [];
|
|
137
|
-
const walk = (dir, rel) => {
|
|
138
|
-
for (const entry of node_fs_1.default.readdirSync(dir, { withFileTypes: true }).sort((a, b) => (0, compare_1.compareBytes)(a.name, b.name))) {
|
|
139
|
-
const abs = node_path_1.default.join(dir, entry.name);
|
|
140
|
-
const r = node_path_1.default.join(rel, entry.name);
|
|
141
|
-
if (entry.isDirectory())
|
|
142
|
-
walk(abs, r);
|
|
143
|
-
else
|
|
144
|
-
parts.push(`${r}:${sha256OfFile(abs)}`);
|
|
145
|
-
}
|
|
146
|
-
};
|
|
147
|
-
walk(p, "");
|
|
148
|
-
return sha256OfString(parts.join("\n"));
|
|
149
|
-
}
|
|
150
98
|
/** Persist a run's authoritative state.json DURABLY (atomic temp → fsync →
|
|
151
99
|
* rename). The re-point that scratch reclamation depends on MUST be persisted
|
|
152
100
|
* this way BEFORE any byte is freed — see prepareFree(). */
|
|
@@ -219,13 +167,13 @@ function digestEvidenceEntry(entry) {
|
|
|
219
167
|
try {
|
|
220
168
|
const stat = node_fs_1.default.statSync(candidatePath);
|
|
221
169
|
if (stat.isFile())
|
|
222
|
-
return { ref, digest: sha256OfFile(candidatePath) };
|
|
170
|
+
return { ref, digest: (0, hash_1.sha256OfFile)(candidatePath) };
|
|
223
171
|
}
|
|
224
172
|
catch {
|
|
225
173
|
/* fall through to locator digest */
|
|
226
174
|
}
|
|
227
175
|
}
|
|
228
|
-
return { ref, digest: sha256OfString(ref) };
|
|
176
|
+
return { ref, digest: (0, hash_1.sha256OfString)(ref) };
|
|
229
177
|
}
|
|
230
178
|
/** STEP 1: extract + seal the skeleton. Pure read over the run; never mutates. */
|
|
231
179
|
function extractSkeleton(run) {
|
|
@@ -263,7 +211,7 @@ function extractSkeleton(run) {
|
|
|
263
211
|
.map(([ref, digest]) => ({ ref, digest }))
|
|
264
212
|
.sort((a, b) => (0, compare_1.compareBytes)(a.ref, b.ref));
|
|
265
213
|
const eventLog = auditEventLogPath(run);
|
|
266
|
-
const auditLogDigest = node_fs_1.default.existsSync(eventLog) ? sha256OfFile(eventLog) : sha256OfString("");
|
|
214
|
+
const auditLogDigest = node_fs_1.default.existsSync(eventLog) ? (0, hash_1.sha256OfFile)(eventLog) : (0, hash_1.sha256OfString)("");
|
|
267
215
|
const events = node_fs_1.default.existsSync(eventLog)
|
|
268
216
|
? node_fs_1.default
|
|
269
217
|
.readFileSync(eventLog, "utf8")
|
|
@@ -283,18 +231,18 @@ function extractSkeleton(run) {
|
|
|
283
231
|
const metricsReport = node_path_1.default.join(run.paths.runDir, "metrics", "metrics-report.json");
|
|
284
232
|
const costRecord = {
|
|
285
233
|
tasks: (run.tasks || []).map((task) => ({ taskId: task.id, model: task.usage?.model, source: task.usage?.source })),
|
|
286
|
-
metricsDigest: node_fs_1.default.existsSync(metricsReport) ? sha256OfFile(metricsReport) : undefined
|
|
234
|
+
metricsDigest: node_fs_1.default.existsSync(metricsReport) ? (0, hash_1.sha256OfFile)(metricsReport) : undefined
|
|
287
235
|
};
|
|
288
236
|
const collaboration = run.collaboration;
|
|
289
237
|
const collaborationLog = {
|
|
290
|
-
digest: sha256OfString((0, multi_agent_eval_1.replayStableStringify)(collaboration || {})),
|
|
238
|
+
digest: (0, hash_1.sha256OfString)((0, multi_agent_eval_1.replayStableStringify)(collaboration || {})),
|
|
291
239
|
approvals: collaboration?.approvals?.length || 0,
|
|
292
240
|
comments: collaboration?.comments?.length || 0,
|
|
293
241
|
handoffs: collaboration?.handoffs?.length || 0
|
|
294
242
|
};
|
|
295
243
|
// Empty (not a hash-of-empty) when state.json is absent, so the skeleton fails
|
|
296
244
|
// closed — you cannot seal a run whose authoritative state is gone.
|
|
297
|
-
const stateDigest = node_fs_1.default.existsSync(run.paths.state) ? sha256OfFile(run.paths.state) : "";
|
|
245
|
+
const stateDigest = node_fs_1.default.existsSync(run.paths.state) ? (0, hash_1.sha256OfFile)(run.paths.state) : "";
|
|
298
246
|
return {
|
|
299
247
|
schemaVersion: 1,
|
|
300
248
|
runId: run.id,
|
|
@@ -397,46 +345,17 @@ function buildReferenceGraph(run) {
|
|
|
397
345
|
add(message.id);
|
|
398
346
|
return refs;
|
|
399
347
|
}
|
|
348
|
+
/** expectDigest of a node's deterministic projection. Re-uses the SHARED
|
|
349
|
+
* node-projection field set (node-projection.ts) so reconstruction matches
|
|
350
|
+
* node-snapshot.ts's body byte-for-byte — the projection can no longer drift. */
|
|
400
351
|
function snapshotProjectionDigest(node) {
|
|
401
|
-
|
|
402
|
-
const body = (0, multi_agent_eval_1.normalizeValue)({
|
|
403
|
-
id: node.id,
|
|
404
|
-
kind: node.kind,
|
|
405
|
-
status: node.status,
|
|
406
|
-
loopStage: node.loopStage,
|
|
407
|
-
inputs: node.inputs,
|
|
408
|
-
outputs: node.outputs,
|
|
409
|
-
artifacts: node.artifacts,
|
|
410
|
-
evidence: node.evidence,
|
|
411
|
-
errors: node.errors,
|
|
412
|
-
parents: node.parents,
|
|
413
|
-
children: node.children,
|
|
414
|
-
contractId: node.contractId,
|
|
415
|
-
metadata: node.metadata
|
|
416
|
-
});
|
|
417
|
-
return sha256OfString((0, multi_agent_eval_1.replayStableStringify)(body));
|
|
352
|
+
return (0, hash_1.sha256OfString)((0, node_projection_1.nodeProjectionDigestInput)(node));
|
|
418
353
|
}
|
|
419
354
|
/** Body digest of the RETAINED node (lives in state.json). The reconstruction
|
|
420
|
-
* verifier re-derives the projection from this retained input.
|
|
355
|
+
* verifier re-derives the projection from this retained input. Same shared field
|
|
356
|
+
* set / canonical bytes as snapshotProjectionDigest. */
|
|
421
357
|
function nodeBodyDigest(node) {
|
|
422
|
-
return sha256OfString((0,
|
|
423
|
-
}
|
|
424
|
-
function rawNodeBody(node) {
|
|
425
|
-
return {
|
|
426
|
-
id: node.id,
|
|
427
|
-
kind: node.kind,
|
|
428
|
-
status: node.status,
|
|
429
|
-
loopStage: node.loopStage,
|
|
430
|
-
inputs: node.inputs,
|
|
431
|
-
outputs: node.outputs,
|
|
432
|
-
artifacts: node.artifacts,
|
|
433
|
-
evidence: node.evidence,
|
|
434
|
-
errors: node.errors,
|
|
435
|
-
parents: node.parents,
|
|
436
|
-
children: node.children,
|
|
437
|
-
contractId: node.contractId,
|
|
438
|
-
metadata: node.metadata
|
|
439
|
-
};
|
|
358
|
+
return (0, hash_1.sha256OfString)((0, node_projection_1.nodeProjectionDigestInput)(node));
|
|
440
359
|
}
|
|
441
360
|
/** Build the retention plan: which paths are freeable under `policy`, of what
|
|
442
361
|
* kind, how many bytes, and the resulting capability downgrade. */
|
|
@@ -459,7 +378,7 @@ function planReclamation(run, policy = {}) {
|
|
|
459
378
|
const resultsCopy = task?.resultPath;
|
|
460
379
|
if (!resultNodeId || !resultsCopy || !node_fs_1.default.existsSync(resultsCopy))
|
|
461
380
|
continue;
|
|
462
|
-
const bytes = dirBytes(workerDir);
|
|
381
|
+
const bytes = (0, hash_1.dirBytes)(workerDir);
|
|
463
382
|
if (bytes <= 0)
|
|
464
383
|
continue;
|
|
465
384
|
freeable.push({
|
|
@@ -506,14 +425,14 @@ function planReclamation(run, policy = {}) {
|
|
|
506
425
|
continue; // source node gone → cannot reconstruct → retain
|
|
507
426
|
if (repointNodeIds.has(node.id))
|
|
508
427
|
continue; // body will be re-pointed → retain
|
|
509
|
-
const bytes = dirBytes(snapFile);
|
|
428
|
+
const bytes = (0, hash_1.dirBytes)(snapFile);
|
|
510
429
|
if (bytes <= 0)
|
|
511
430
|
continue;
|
|
512
431
|
const inputDigest = nodeBodyDigest(node);
|
|
513
432
|
const recipe = {
|
|
514
433
|
recipeKind: "node-snapshot-projection",
|
|
515
434
|
inputDigests: [inputDigest],
|
|
516
|
-
inputsDigest: sha256OfString((0, multi_agent_eval_1.replayStableStringify)([inputDigest])),
|
|
435
|
+
inputsDigest: (0, hash_1.sha256OfString)((0, multi_agent_eval_1.replayStableStringify)([inputDigest])),
|
|
517
436
|
expectDigest: snapshotProjectionDigest(node),
|
|
518
437
|
sourceRef: node.id
|
|
519
438
|
};
|
|
@@ -562,11 +481,11 @@ function planReclamation(run, policy = {}) {
|
|
|
562
481
|
return { freeable, bytesToFree, byKind, capability, capabilityReason };
|
|
563
482
|
}
|
|
564
483
|
function policyDigestOf(policy) {
|
|
565
|
-
return sha256OfString((0, multi_agent_eval_1.replayStableStringify)(policy));
|
|
484
|
+
return (0, hash_1.sha256OfString)((0, multi_agent_eval_1.replayStableStringify)(policy));
|
|
566
485
|
}
|
|
567
486
|
/** genesis prevTombstoneHash = sha256 of the sealed skeleton. */
|
|
568
487
|
function genesisPrevHash(skeleton) {
|
|
569
|
-
return sha256OfString((0, multi_agent_eval_1.replayStableStringify)(skeleton));
|
|
488
|
+
return (0, hash_1.sha256OfString)((0, multi_agent_eval_1.replayStableStringify)(skeleton));
|
|
570
489
|
}
|
|
571
490
|
/** The canonical bytes a tombstoneHash binds: freed-manifest + sealed skeleton +
|
|
572
491
|
* prevTombstoneHash + capability. Recomputed independently by `gc verify`. */
|
|
@@ -579,14 +498,14 @@ function tombstoneHashInput(t) {
|
|
|
579
498
|
policyDigest: t.policyDigest,
|
|
580
499
|
freed: t.freed.map((f) => ({ path: f.path, kind: f.kind, bytes: f.bytes, sha256: f.sha256, recipe: f.recipe || null })),
|
|
581
500
|
bytesFreed: t.bytesFreed,
|
|
582
|
-
skeletonDigest: sha256OfString((0, multi_agent_eval_1.replayStableStringify)(t.skeleton)),
|
|
501
|
+
skeletonDigest: (0, hash_1.sha256OfString)((0, multi_agent_eval_1.replayStableStringify)(t.skeleton)),
|
|
583
502
|
capability: t.capability,
|
|
584
503
|
capabilityReason: t.capabilityReason,
|
|
585
504
|
prevTombstoneHash: t.prevTombstoneHash
|
|
586
505
|
});
|
|
587
506
|
}
|
|
588
507
|
function computeTombstoneHash(t) {
|
|
589
|
-
return sha256OfString(tombstoneHashInput(t));
|
|
508
|
+
return (0, hash_1.sha256OfString)(tombstoneHashInput(t));
|
|
590
509
|
}
|
|
591
510
|
function tombstoneId(seq) {
|
|
592
511
|
// Deterministic (FreeBSD-audit L13): the chain POSITION, not a process-global
|
|
@@ -604,7 +523,7 @@ function buildTombstone(run, skeleton, plan, options = {}) {
|
|
|
604
523
|
path: entry.path,
|
|
605
524
|
kind: entry.kind,
|
|
606
525
|
bytes: entry.bytes,
|
|
607
|
-
sha256: contentDigest(entry.absPath),
|
|
526
|
+
sha256: (0, hash_1.contentDigest)(entry.absPath),
|
|
608
527
|
recipe: entry.recipe
|
|
609
528
|
}));
|
|
610
529
|
const base = {
|
|
@@ -713,7 +632,7 @@ function freeBulk(run, tombstone) {
|
|
|
713
632
|
let freedBytes = 0;
|
|
714
633
|
for (const entry of tombstone.freed) {
|
|
715
634
|
const abs = node_path_1.default.join(runDir, entry.path);
|
|
716
|
-
const before = dirBytes(abs);
|
|
635
|
+
const before = (0, hash_1.dirBytes)(abs);
|
|
717
636
|
node_fs_1.default.rmSync(abs, { recursive: true, force: true });
|
|
718
637
|
freedBytes += before;
|
|
719
638
|
}
|
|
@@ -802,15 +721,15 @@ function reconstructArtifact(run, recipe) {
|
|
|
802
721
|
if (recipe.recipeKind === "node-snapshot-projection") {
|
|
803
722
|
const node = (run.nodes || []).find((n) => n.id === recipe.sourceRef);
|
|
804
723
|
if (!node) {
|
|
805
|
-
return { inputsDigest: sha256OfString("absent"), expectDigest: sha256OfString("absent") };
|
|
724
|
+
return { inputsDigest: (0, hash_1.sha256OfString)("absent"), expectDigest: (0, hash_1.sha256OfString)("absent") };
|
|
806
725
|
}
|
|
807
726
|
const inputDigest = nodeBodyDigest(node);
|
|
808
|
-
const inputsDigest = sha256OfString((0, multi_agent_eval_1.replayStableStringify)([inputDigest]));
|
|
727
|
+
const inputsDigest = (0, hash_1.sha256OfString)((0, multi_agent_eval_1.replayStableStringify)([inputDigest]));
|
|
809
728
|
const expectDigest = snapshotProjectionDigest(node);
|
|
810
729
|
return { inputsDigest, expectDigest };
|
|
811
730
|
}
|
|
812
731
|
// Unknown recipe kind → fail closed (digest can't match expectDigest).
|
|
813
|
-
return { inputsDigest: sha256OfString("unknown-recipe"), expectDigest: sha256OfString("unknown-recipe") };
|
|
732
|
+
return { inputsDigest: (0, hash_1.sha256OfString)("unknown-recipe"), expectDigest: (0, hash_1.sha256OfString)("unknown-recipe") };
|
|
814
733
|
}
|
|
815
734
|
/** Re-prove the whole reclamation chain for a run: skeleton schema-complete,
|
|
816
735
|
* tombstoneHash/prevTombstoneHash chain recomputed-and-untampered, and each
|
|
@@ -33,15 +33,14 @@ function queueFilePath(host) {
|
|
|
33
33
|
}
|
|
34
34
|
function loadQueue(host) {
|
|
35
35
|
const file = queueFilePath(host);
|
|
36
|
+
// Absent => empty queue. A present-but-corrupt queue must FAIL CLOSED rather
|
|
37
|
+
// than read as empty: silently draining to [] would lose every queued run and
|
|
38
|
+
// let scheduling/lease ops proceed as if the store were clean. readJson throws
|
|
39
|
+
// `Invalid JSON in <file>` on a present, unparseable store; let it propagate.
|
|
36
40
|
if (!node_fs_1.default.existsSync(file))
|
|
37
41
|
return [];
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
return Array.isArray(parsed.entries) ? parsed.entries : [];
|
|
41
|
-
}
|
|
42
|
-
catch {
|
|
43
|
-
return [];
|
|
44
|
-
}
|
|
42
|
+
const parsed = (0, state_1.readJson)(file);
|
|
43
|
+
return Array.isArray(parsed.entries) ? parsed.entries : [];
|
|
45
44
|
}
|
|
46
45
|
function saveQueue(host, entries) {
|
|
47
46
|
(0, state_1.writeJson)(queueFilePath(host), { schemaVersion: 1, entries }, { durable: true });
|