@llblab/pi-actors 0.42.3 → 0.43.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/AGENTS.md +121 -175
- package/CHANGELOG.md +12 -0
- package/README.md +113 -276
- package/dist/fixtures/protocol/control-endpoint.json +6 -0
- package/dist/fixtures/protocol/control-record.json +9 -0
- package/dist/fixtures/protocol/recipe-summary.json +4 -12
- package/dist/fixtures/protocol/trace-event.json +9 -0
- package/dist/lib/async-runs.d.ts +14 -38
- package/dist/lib/async-runs.js +158 -108
- package/dist/lib/control.d.ts +12 -0
- package/dist/lib/control.js +84 -0
- package/dist/lib/execution-sessions.d.ts +17 -0
- package/dist/lib/execution-sessions.js +85 -0
- package/dist/lib/file-state.d.ts +1 -0
- package/dist/lib/file-state.js +17 -5
- package/dist/lib/inspector-actions.d.ts +2 -2
- package/dist/lib/inspector-actions.js +2 -2
- package/dist/lib/inspector-command.js +3 -3
- package/dist/lib/inspector-overlay.d.ts +52 -70
- package/dist/lib/inspector-overlay.js +532 -905
- package/dist/lib/inspector.d.ts +3 -71
- package/dist/lib/inspector.js +19 -665
- package/dist/lib/limits.d.ts +4 -2
- package/dist/lib/limits.js +4 -2
- package/dist/lib/observability.d.ts +16 -16
- package/dist/lib/observability.js +43 -82
- package/dist/lib/prompts.d.ts +1 -1
- package/dist/lib/prompts.js +2 -2
- package/dist/lib/recipe-control.d.ts +7 -0
- package/dist/lib/recipe-control.js +39 -0
- package/dist/lib/recipes-discovery.js +2 -0
- package/dist/lib/recipes-references.d.ts +1 -14
- package/dist/lib/recipes-references.js +6 -21
- package/dist/lib/review-projection.js +1 -5
- package/dist/lib/run-ui-runtime.js +2 -2
- package/dist/lib/runs-control-delivery.d.ts +21 -0
- package/dist/lib/runs-control-delivery.js +127 -0
- package/dist/lib/runs-controls.d.ts +35 -0
- package/dist/lib/runs-controls.js +144 -0
- package/dist/lib/runs-retention.d.ts +7 -0
- package/dist/lib/runs-retention.js +27 -3
- package/dist/lib/runs-start.js +4 -2
- package/dist/lib/runs-status.js +11 -6
- package/dist/lib/runs-trace.d.ts +24 -0
- package/dist/lib/runs-trace.js +98 -0
- package/dist/lib/runtime-notifier.d.ts +1 -1
- package/dist/lib/runtime-notifier.js +1 -1
- package/dist/lib/tools-inspect.d.ts +3 -3
- package/dist/lib/tools-inspect.js +203 -708
- package/dist/lib/tools-local.js +2 -10
- package/dist/lib/tools-message.d.ts +7 -7
- package/dist/lib/tools-message.js +95 -396
- package/dist/lib/tools-response.d.ts +1 -4
- package/dist/lib/tools-response.js +5 -39
- package/dist/lib/tools-spawn.js +16 -28
- package/dist/lib/tools.js +1 -2
- package/dist/lib/trace-projection.d.ts +22 -0
- package/dist/lib/trace-projection.js +165 -0
- package/dist/recipes/draft-review.json +0 -10
- package/dist/recipes/lens-swarm.json +0 -14
- package/dist/recipes/music-player.json +10 -19
- package/dist/recipes/pipeline-architect-coordinator.json +0 -11
- package/dist/recipes/pipeline-artifact-bundle.json +1 -22
- package/dist/recipes/pipeline-artifact-report.json +1 -18
- package/dist/recipes/pipeline-artifact-write.json +1 -18
- package/dist/recipes/pipeline-async-run-ops.json +0 -12
- package/dist/recipes/pipeline-checkpoint-continuation.json +0 -14
- package/dist/recipes/pipeline-development-tasking.json +0 -12
- package/dist/recipes/pipeline-docs-maintenance.json +0 -12
- package/dist/recipes/pipeline-media-library.json +0 -12
- package/dist/recipes/pipeline-quorum-review.json +0 -12
- package/dist/recipes/pipeline-release-readiness.json +0 -12
- package/dist/recipes/pipeline-release-summary.json +0 -12
- package/dist/recipes/pipeline-repo-health.json +0 -12
- package/dist/recipes/pipeline-research-synthesis.json +0 -11
- package/dist/recipes/pipeline-review-readiness.json +0 -12
- package/dist/recipes/resource-locker.json +27 -0
- package/dist/recipes/subagent-artifact.json +0 -9
- package/dist/recipes/subagent-checkpoint.json +0 -10
- package/dist/recipes/subagent-conflict-report.json +0 -11
- package/dist/recipes/subagent-contradiction-map.json +0 -11
- package/dist/recipes/subagent-critic.json +0 -11
- package/dist/recipes/subagent-evidence-map.json +0 -11
- package/dist/recipes/subagent-followup.json +0 -10
- package/dist/recipes/subagent-judge.json +0 -11
- package/dist/recipes/subagent-merge.json +0 -11
- package/dist/recipes/subagent-normalize.json +0 -11
- package/dist/recipes/subagent-plan.json +0 -11
- package/dist/recipes/subagent-preflight.json +0 -11
- package/dist/recipes/subagent-prompt.json +0 -10
- package/dist/recipes/subagent-quorum.json +0 -10
- package/dist/recipes/subagent-review-coordinator.json +0 -14
- package/dist/recipes/subagent-review.json +0 -11
- package/dist/recipes/subagent-task-card.json +0 -11
- package/dist/recipes/subagent-tools.json +0 -10
- package/dist/recipes/subagent-verify.json +0 -11
- package/dist/recipes/subagents-prompts.json +0 -10
- package/dist/recipes/tool-review.json +0 -10
- package/dist/scripts/async-runner.mjs +25 -25
- package/dist/scripts/conformance.mjs +4 -2
- package/dist/scripts/locker.mjs +200 -66
- package/dist/scripts/music-player.mjs +159 -150
- package/dist/scripts/recipe-utils.mjs +6 -96
- package/dist/scripts/release-gates.mjs +60 -0
- package/dist/scripts/validate-recipe.mjs +3 -53
- package/dist/skills/actors/SKILL.md +53 -266
- package/dist/skills/swarm/SKILL.md +11 -33
- package/docs/0.43-baseline.md +44 -0
- package/docs/README.md +3 -3
- package/docs/actor-inspector.md +26 -64
- package/docs/actors-deep-reference.md +92 -50
- package/docs/async-runs.md +81 -328
- package/docs/command-templates.md +2 -2
- package/docs/component-recipes.md +30 -133
- package/docs/recipe-library.md +57 -182
- package/docs/task-first-recipes.md +10 -12
- package/docs/template-recipes.md +76 -289
- package/docs/tool-registry.md +41 -161
- package/fixtures/protocol/control-endpoint.json +6 -0
- package/fixtures/protocol/control-record.json +9 -0
- package/fixtures/protocol/recipe-summary.json +4 -12
- package/fixtures/protocol/trace-event.json +9 -0
- package/lib/async-runs.ts +202 -201
- package/lib/control.ts +102 -0
- package/lib/execution-sessions.ts +111 -0
- package/lib/file-state.ts +17 -4
- package/lib/inspector-actions.ts +2 -2
- package/lib/inspector-command.ts +3 -3
- package/lib/inspector-overlay.ts +577 -1121
- package/lib/inspector.ts +46 -979
- package/lib/limits.ts +4 -2
- package/lib/observability.ts +60 -101
- package/lib/prompts.ts +2 -2
- package/lib/recipe-control.ts +45 -0
- package/lib/recipes-discovery.ts +2 -0
- package/lib/recipes-references.ts +9 -45
- package/lib/review-projection.ts +1 -5
- package/lib/run-ui-runtime.ts +2 -2
- package/lib/runs-control-delivery.ts +181 -0
- package/lib/runs-controls.ts +204 -0
- package/lib/runs-retention.ts +38 -3
- package/lib/runs-start.ts +4 -2
- package/lib/runs-status.ts +11 -6
- package/lib/runs-trace.ts +132 -0
- package/lib/runtime-notifier.ts +1 -1
- package/lib/tools-inspect.ts +240 -901
- package/lib/tools-local.ts +2 -12
- package/lib/tools-message.ts +112 -519
- package/lib/tools-response.ts +5 -52
- package/lib/tools-spawn.ts +16 -32
- package/lib/tools.ts +1 -2
- package/lib/trace-projection.ts +221 -0
- package/package.json +2 -1
- package/recipes/draft-review.json +0 -10
- package/recipes/lens-swarm.json +0 -14
- package/recipes/music-player.json +10 -19
- package/recipes/pipeline-architect-coordinator.json +0 -11
- package/recipes/pipeline-artifact-bundle.json +1 -22
- package/recipes/pipeline-artifact-report.json +1 -18
- package/recipes/pipeline-artifact-write.json +1 -18
- package/recipes/pipeline-async-run-ops.json +0 -12
- package/recipes/pipeline-checkpoint-continuation.json +0 -14
- package/recipes/pipeline-development-tasking.json +0 -12
- package/recipes/pipeline-docs-maintenance.json +0 -12
- package/recipes/pipeline-media-library.json +0 -12
- package/recipes/pipeline-quorum-review.json +0 -12
- package/recipes/pipeline-release-readiness.json +0 -12
- package/recipes/pipeline-release-summary.json +0 -12
- package/recipes/pipeline-repo-health.json +0 -12
- package/recipes/pipeline-research-synthesis.json +0 -11
- package/recipes/pipeline-review-readiness.json +0 -12
- package/recipes/resource-locker.json +27 -0
- package/recipes/subagent-artifact.json +0 -9
- package/recipes/subagent-checkpoint.json +0 -10
- package/recipes/subagent-conflict-report.json +0 -11
- package/recipes/subagent-contradiction-map.json +0 -11
- package/recipes/subagent-critic.json +0 -11
- package/recipes/subagent-evidence-map.json +0 -11
- package/recipes/subagent-followup.json +0 -10
- package/recipes/subagent-judge.json +0 -11
- package/recipes/subagent-merge.json +0 -11
- package/recipes/subagent-normalize.json +0 -11
- package/recipes/subagent-plan.json +0 -11
- package/recipes/subagent-preflight.json +0 -11
- package/recipes/subagent-prompt.json +0 -10
- package/recipes/subagent-quorum.json +0 -10
- package/recipes/subagent-review-coordinator.json +0 -14
- package/recipes/subagent-review.json +0 -11
- package/recipes/subagent-task-card.json +0 -11
- package/recipes/subagent-tools.json +0 -10
- package/recipes/subagent-verify.json +0 -11
- package/recipes/subagents-prompts.json +0 -10
- package/recipes/tool-review.json +0 -10
- package/scripts/async-runner.mjs +25 -25
- package/scripts/conformance.mjs +4 -2
- package/scripts/locker.mjs +200 -66
- package/scripts/music-player.mjs +159 -150
- package/scripts/recipe-utils.mjs +6 -96
- package/scripts/release-gates.mjs +60 -0
- package/scripts/validate-recipe.mjs +3 -53
- package/skills/actors/SKILL.md +53 -266
- package/skills/swarm/SKILL.md +11 -33
- package/dist/fixtures/protocol/actor-message-branch.json +0 -13
- package/dist/fixtures/protocol/mailbox-contract.json +0 -15
- package/dist/fixtures/protocol/room-message.json +0 -11
- package/dist/fixtures/protocol/room-roster.json +0 -11
- package/dist/fixtures/protocol/run-inbox-message.json +0 -9
- package/dist/fixtures/protocol/run-outbox-event.json +0 -9
- package/dist/lib/mailbox-loop.d.ts +0 -41
- package/dist/lib/mailbox-loop.js +0 -60
- package/dist/lib/messages.d.ts +0 -25
- package/dist/lib/messages.js +0 -122
- package/dist/lib/rooms.d.ts +0 -104
- package/dist/lib/rooms.js +0 -647
- package/dist/lib/runs-mailbox.d.ts +0 -25
- package/dist/lib/runs-mailbox.js +0 -146
- package/dist/lib/runs-messages.d.ts +0 -15
- package/dist/lib/runs-messages.js +0 -179
- package/dist/lib/runs-outbox.d.ts +0 -41
- package/dist/lib/runs-outbox.js +0 -87
- package/dist/lib/tools-mailbox.d.ts +0 -8
- package/dist/lib/tools-mailbox.js +0 -48
- package/dist/recipes/actor-worker.json +0 -39
- package/dist/recipes/coordinator-locker.json +0 -45
- package/dist/recipes/locker.json +0 -45
- package/dist/recipes/pipeline-room-swarm.json +0 -50
- package/dist/recipes/subagent-message.json +0 -32
- package/dist/recipes/utility-actor-message.json +0 -23
- package/dist/scripts/actor-worker.mjs +0 -214
- package/dist/scripts/coordinator.mjs +0 -799
- package/docs/actor-messages.md +0 -225
- package/fixtures/protocol/actor-message-branch.json +0 -13
- package/fixtures/protocol/mailbox-contract.json +0 -15
- package/fixtures/protocol/room-message.json +0 -11
- package/fixtures/protocol/room-roster.json +0 -11
- package/fixtures/protocol/run-inbox-message.json +0 -9
- package/fixtures/protocol/run-outbox-event.json +0 -9
- package/lib/mailbox-loop.ts +0 -144
- package/lib/messages.ts +0 -151
- package/lib/rooms.ts +0 -939
- package/lib/runs-mailbox.ts +0 -208
- package/lib/runs-messages.ts +0 -252
- package/lib/runs-outbox.ts +0 -144
- package/lib/tools-mailbox.ts +0 -56
- package/recipes/actor-worker.json +0 -39
- package/recipes/coordinator-locker.json +0 -45
- package/recipes/locker.json +0 -45
- package/recipes/pipeline-room-swarm.json +0 -50
- package/recipes/subagent-message.json +0 -32
- package/recipes/utility-actor-message.json +0 -23
- package/scripts/actor-worker.mjs +0 -214
- package/scripts/coordinator.mjs +0 -799
package/scripts/async-runner.mjs
CHANGED
|
@@ -43,6 +43,7 @@ const { execCommandTemplate } = await importRuntimeModule("command-templates");
|
|
|
43
43
|
const { applyOutputAcceptancePolicy, executeRegisteredTool } =
|
|
44
44
|
await importRuntimeModule("execution");
|
|
45
45
|
const { writeJsonAtomic } = await importRuntimeModule("file-state");
|
|
46
|
+
const { appendRunTraceEvent } = await importRuntimeModule("runs-trace");
|
|
46
47
|
|
|
47
48
|
function quoteCommandDetailPart(value) {
|
|
48
49
|
if (value === "") return "''";
|
|
@@ -77,25 +78,24 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
|
|
|
77
78
|
const runPath = join(stateDir, "run.json");
|
|
78
79
|
const progressPath = join(stateDir, "progress.json");
|
|
79
80
|
const resultPath = join(stateDir, "result.json");
|
|
80
|
-
const
|
|
81
|
-
const outboxPath = join(stateDir, "outbox.jsonl");
|
|
81
|
+
const tracePath = join(stateDir, "trace.jsonl");
|
|
82
82
|
const stdoutPath = join(stateDir, "stdout.log");
|
|
83
83
|
const stderrPath = join(stateDir, "stderr.log");
|
|
84
|
-
const
|
|
84
|
+
const executionPath = join(stateDir, "execution.json");
|
|
85
85
|
const meta = JSON.parse(readFileSync(runPath, "utf8"));
|
|
86
86
|
|
|
87
|
-
function event(
|
|
88
|
-
|
|
89
|
-
eventsPath,
|
|
90
|
-
`${JSON.stringify({ event: name, ts: new Date().toISOString(), ...data })}\n`,
|
|
91
|
-
);
|
|
87
|
+
function event(kind, data = {}) {
|
|
88
|
+
appendRunTraceEvent(stateDir, { kind, data });
|
|
92
89
|
}
|
|
93
90
|
|
|
94
|
-
function
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
91
|
+
function observation(kind, summary, data = {}, delivery = "log", level = "info") {
|
|
92
|
+
appendRunTraceEvent(stateDir, {
|
|
93
|
+
kind,
|
|
94
|
+
summary,
|
|
95
|
+
data,
|
|
96
|
+
...(delivery === "followup" ? { attention: "followup" } : {}),
|
|
97
|
+
...(level === "error" ? { level: "error" } : {}),
|
|
98
|
+
});
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
function progress(phase, extra = {}) {
|
|
@@ -116,8 +116,8 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
|
|
|
116
116
|
const stageOccurrences = new Map();
|
|
117
117
|
let reportEvidence;
|
|
118
118
|
|
|
119
|
-
function
|
|
120
|
-
writeJsonAtomic(
|
|
119
|
+
function writeExecutionManifest(status) {
|
|
120
|
+
writeJsonAtomic(executionPath, {
|
|
121
121
|
version: 1,
|
|
122
122
|
run: meta.run,
|
|
123
123
|
status,
|
|
@@ -279,10 +279,10 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
|
|
|
279
279
|
.filter((record) =>
|
|
280
280
|
["reviewer", "verifier", "merger", "judge"].includes(record.stage)
|
|
281
281
|
)
|
|
282
|
-
.map((record) => `
|
|
282
|
+
.map((record) => `execution.json#${record.id}`);
|
|
283
283
|
if (required.length === 0) return undefined;
|
|
284
284
|
const cited = [...new Set(
|
|
285
|
-
text.match(/
|
|
285
|
+
text.match(/execution\.json#command-\d{3}/g) || [],
|
|
286
286
|
)].sort();
|
|
287
287
|
const missing = required.filter((reference) => !cited.includes(reference));
|
|
288
288
|
const claimsComplete = /\bStatus\b[\s:*#_-]{0,40}\bcomplete\b/i.test(text);
|
|
@@ -358,7 +358,7 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
|
|
|
358
358
|
.filter((record) =>
|
|
359
359
|
["reviewer", "verifier", "merger", "judge"].includes(record.stage)
|
|
360
360
|
)
|
|
361
|
-
.map((record) => `ACTOR_EVIDENCE_REF:
|
|
361
|
+
.map((record) => `ACTOR_EVIDENCE_REF: execution.json#${record.id}`);
|
|
362
362
|
if (references.length > 0) {
|
|
363
363
|
appendFileSync(
|
|
364
364
|
materialized.promptFile,
|
|
@@ -379,7 +379,7 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
|
|
|
379
379
|
occurrence,
|
|
380
380
|
});
|
|
381
381
|
evidenceRecords.push(startedEvidence);
|
|
382
|
-
|
|
382
|
+
writeExecutionManifest("running");
|
|
383
383
|
event("command.start", {
|
|
384
384
|
activeSubagents,
|
|
385
385
|
command_id: commandId,
|
|
@@ -400,7 +400,7 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
|
|
|
400
400
|
);
|
|
401
401
|
result = {
|
|
402
402
|
...result,
|
|
403
|
-
evidenceRef: `
|
|
403
|
+
evidenceRef: `execution.json#${commandId}`,
|
|
404
404
|
};
|
|
405
405
|
const preflightDiagnostic = result.code !== 0
|
|
406
406
|
? buildReviewPreflightDiagnostic({
|
|
@@ -435,7 +435,7 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
|
|
|
435
435
|
occurrence,
|
|
436
436
|
startedAt: startedEvidence.started_at,
|
|
437
437
|
});
|
|
438
|
-
|
|
438
|
+
writeExecutionManifest("running");
|
|
439
439
|
activeSubagents = Math.max(0, activeSubagents - 1);
|
|
440
440
|
completedSubagents += 1;
|
|
441
441
|
if (result.code !== 0) {
|
|
@@ -463,13 +463,13 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
|
|
|
463
463
|
: {}),
|
|
464
464
|
...(preflightDiagnostic ? { preflight: preflightDiagnostic } : {}),
|
|
465
465
|
});
|
|
466
|
-
|
|
466
|
+
observation(
|
|
467
467
|
"command.done",
|
|
468
468
|
`Command ${summarizeCommandDetail(commandDetail)} completed with code ${result.code}`,
|
|
469
469
|
{
|
|
470
470
|
activeSubagents,
|
|
471
471
|
...(meta.artifacts ? { artifacts: meta.artifacts } : {}),
|
|
472
|
-
run_files: [stdoutPath, stderrPath, resultPath,
|
|
472
|
+
run_files: [stdoutPath, stderrPath, resultPath, tracePath, executionPath],
|
|
473
473
|
command_id: commandId,
|
|
474
474
|
code: result.code,
|
|
475
475
|
command: commandDetail,
|
|
@@ -517,7 +517,7 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
|
|
|
517
517
|
};
|
|
518
518
|
throw error;
|
|
519
519
|
}
|
|
520
|
-
|
|
520
|
+
writeExecutionManifest("done");
|
|
521
521
|
progress("done", {
|
|
522
522
|
completed: 1,
|
|
523
523
|
failures: result.details.nonCriticalFailures || [],
|
|
@@ -535,7 +535,7 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
|
|
|
535
535
|
const message = error instanceof Error ? error.message : String(error);
|
|
536
536
|
const details = error && typeof error === "object" ? error.details : undefined;
|
|
537
537
|
appendFileSync(stderrPath, `${message}\n`);
|
|
538
|
-
|
|
538
|
+
writeExecutionManifest("failed");
|
|
539
539
|
progress("failed", {
|
|
540
540
|
completed: 0,
|
|
541
541
|
failures: Array.isArray(details?.branches) && details.branches.length > 0
|
package/scripts/conformance.mjs
CHANGED
|
@@ -12,13 +12,15 @@ import { dirname } from "node:path";
|
|
|
12
12
|
import { fileURLToPath } from "node:url";
|
|
13
13
|
|
|
14
14
|
const conformanceSuites = [
|
|
15
|
-
"tests/
|
|
15
|
+
"tests/control.test.ts",
|
|
16
|
+
"tests/runs-controls.test.ts",
|
|
17
|
+
"tests/runs-trace.test.ts",
|
|
18
|
+
"tests/tools-inspect-kernel.test.ts",
|
|
16
19
|
"tests/recipes-discovery.test.ts",
|
|
17
20
|
"tests/review-swarm-dogfood.test.ts",
|
|
18
21
|
"tests/registry.test.ts",
|
|
19
22
|
"tests/runtime.test.ts",
|
|
20
23
|
"tests/async-runs.test.ts",
|
|
21
|
-
"tests/rooms.test.ts",
|
|
22
24
|
"tests/tools.test.ts",
|
|
23
25
|
];
|
|
24
26
|
|
package/scripts/locker.mjs
CHANGED
|
@@ -9,18 +9,32 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { spawnSync } from "node:child_process";
|
|
12
|
-
import { createHash } from "node:crypto";
|
|
12
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
13
13
|
import {
|
|
14
14
|
appendFileSync,
|
|
15
15
|
existsSync,
|
|
16
16
|
mkdirSync,
|
|
17
17
|
readFileSync,
|
|
18
18
|
rmSync,
|
|
19
|
-
writeFileSync,
|
|
20
19
|
} from "node:fs";
|
|
21
20
|
import { createServer } from "node:net";
|
|
22
21
|
import { dirname, join, resolve } from "node:path";
|
|
23
22
|
import readline from "node:readline";
|
|
23
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
24
|
+
|
|
25
|
+
function packageRoot() {
|
|
26
|
+
return dirname(dirname(fileURLToPath(import.meta.url)));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function importRuntimeModule(name) {
|
|
30
|
+
const root = packageRoot();
|
|
31
|
+
const compiled = join(root, "dist", "lib", `${name}.js`);
|
|
32
|
+
const source = join(root, "lib", `${name}.ts`);
|
|
33
|
+
return await import(pathToFileURL(existsSync(compiled) ? compiled : source).href);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const { acquireFileMutationLock, writeJsonAtomic, writeTextAtomic } =
|
|
37
|
+
await importRuntimeModule("file-state");
|
|
24
38
|
|
|
25
39
|
function parseArgs(argv) {
|
|
26
40
|
const args = { mode: "serve", stateDir: "", leaseMs: 600000, lines: 20 };
|
|
@@ -45,8 +59,10 @@ async function runLocker(argv = process.argv.slice(2)) {
|
|
|
45
59
|
const queuePath = join(stateDir, "queue.json");
|
|
46
60
|
const locksPath = join(stateDir, "locks.json");
|
|
47
61
|
const journalPath = join(stateDir, "journal.jsonl");
|
|
48
|
-
const
|
|
62
|
+
const tracePath = join(stateDir, "trace.jsonl");
|
|
63
|
+
const controlsPath = join(stateDir, "controls.jsonl");
|
|
49
64
|
const controlPath = join(stateDir, "control.fifo");
|
|
65
|
+
let runInstanceId;
|
|
50
66
|
mkdirSync(stateDir, { recursive: true });
|
|
51
67
|
|
|
52
68
|
function readJson(path, fallback) {
|
|
@@ -59,8 +75,7 @@ async function runLocker(argv = process.argv.slice(2)) {
|
|
|
59
75
|
}
|
|
60
76
|
|
|
61
77
|
function writeJson(path, value) {
|
|
62
|
-
|
|
63
|
-
writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
78
|
+
writeJsonAtomic(path, value);
|
|
64
79
|
}
|
|
65
80
|
|
|
66
81
|
function getControlEndpoint() {
|
|
@@ -73,11 +88,12 @@ async function runLocker(argv = process.argv.slice(2)) {
|
|
|
73
88
|
}
|
|
74
89
|
|
|
75
90
|
function writeControlEndpoint(endpoint) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
91
|
+
if (!runInstanceId) throw new Error("Run generation unavailable for Control endpoint");
|
|
92
|
+
writeJson(join(stateDir, "control-endpoint.json"), {
|
|
93
|
+
...endpoint,
|
|
94
|
+
ready_at: new Date().toISOString(),
|
|
95
|
+
run_instance_id: runInstanceId,
|
|
96
|
+
});
|
|
81
97
|
}
|
|
82
98
|
|
|
83
99
|
function journal(event, data = {}) {
|
|
@@ -87,10 +103,18 @@ async function runLocker(argv = process.argv.slice(2)) {
|
|
|
87
103
|
);
|
|
88
104
|
}
|
|
89
105
|
|
|
90
|
-
function
|
|
106
|
+
function emitTrace(kind, summary, data = {}, level = "info") {
|
|
91
107
|
appendFileSync(
|
|
92
|
-
|
|
93
|
-
`${JSON.stringify({
|
|
108
|
+
tracePath,
|
|
109
|
+
`${JSON.stringify({
|
|
110
|
+
attention: "followup",
|
|
111
|
+
data,
|
|
112
|
+
id: randomUUID(),
|
|
113
|
+
kind,
|
|
114
|
+
level,
|
|
115
|
+
summary,
|
|
116
|
+
ts: new Date().toISOString(),
|
|
117
|
+
})}\n`,
|
|
94
118
|
);
|
|
95
119
|
}
|
|
96
120
|
|
|
@@ -108,16 +132,88 @@ async function runLocker(argv = process.argv.slice(2)) {
|
|
|
108
132
|
return kept;
|
|
109
133
|
}
|
|
110
134
|
|
|
111
|
-
function
|
|
135
|
+
function normalizeControl(line) {
|
|
112
136
|
const trimmed = line.trim();
|
|
113
137
|
if (!trimmed) return undefined;
|
|
114
|
-
|
|
115
|
-
|
|
138
|
+
try {
|
|
139
|
+
const control = JSON.parse(trimmed);
|
|
140
|
+
if (
|
|
141
|
+
!control ||
|
|
142
|
+
typeof control !== "object" ||
|
|
143
|
+
typeof control.id !== "string" ||
|
|
144
|
+
typeof control.action !== "string"
|
|
145
|
+
) {
|
|
146
|
+
throw new Error("invalid Control envelope");
|
|
147
|
+
}
|
|
148
|
+
return control;
|
|
149
|
+
} catch (error) {
|
|
150
|
+
throw new Error(
|
|
151
|
+
`Invalid Control envelope: ${error instanceof Error ? error.message : String(error)}`,
|
|
152
|
+
);
|
|
116
153
|
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function acquireControlsLock() {
|
|
157
|
+
return acquireFileMutationLock(controlsPath);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function readControls() {
|
|
161
|
+
if (!existsSync(controlsPath)) return [];
|
|
162
|
+
return readFileSync(controlsPath, "utf8")
|
|
163
|
+
.split("\n")
|
|
164
|
+
.filter((line) => line.trim())
|
|
165
|
+
.map((line) => {
|
|
166
|
+
try {
|
|
167
|
+
return JSON.parse(line);
|
|
168
|
+
} catch {
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
})
|
|
172
|
+
.filter(Boolean);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function writeControls(controls) {
|
|
176
|
+
writeTextAtomic(
|
|
177
|
+
controlsPath,
|
|
178
|
+
controls.length
|
|
179
|
+
? `${controls.map((control) => JSON.stringify(control)).join("\n")}\n`
|
|
180
|
+
: "",
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function claimControl(id) {
|
|
185
|
+
const release = acquireControlsLock();
|
|
117
186
|
try {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
187
|
+
const controls = readControls();
|
|
188
|
+
const control = controls.find((item) => item.id === id);
|
|
189
|
+
if (
|
|
190
|
+
!control ||
|
|
191
|
+
control.run_instance_id !== runInstanceId ||
|
|
192
|
+
(control.status !== "queued" && control.status !== "delivered")
|
|
193
|
+
) {
|
|
194
|
+
return undefined;
|
|
195
|
+
}
|
|
196
|
+
control.claimed_at = new Date().toISOString();
|
|
197
|
+
control.status = "claimed";
|
|
198
|
+
writeControls(controls);
|
|
199
|
+
return { ...control };
|
|
200
|
+
} finally {
|
|
201
|
+
release();
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function finalizeControl(id, status, error) {
|
|
206
|
+
const release = acquireControlsLock();
|
|
207
|
+
try {
|
|
208
|
+
const controls = readControls();
|
|
209
|
+
const control = controls.find((item) => item.id === id);
|
|
210
|
+
if (!control || control.status !== "claimed") return;
|
|
211
|
+
control.status = status;
|
|
212
|
+
control[`${status}_at`] = new Date().toISOString();
|
|
213
|
+
if (error) control.error = error;
|
|
214
|
+
writeControls(controls);
|
|
215
|
+
} finally {
|
|
216
|
+
release();
|
|
121
217
|
}
|
|
122
218
|
}
|
|
123
219
|
|
|
@@ -165,21 +261,21 @@ async function runLocker(argv = process.argv.slice(2)) {
|
|
|
165
261
|
return items.splice(index, 1)[0];
|
|
166
262
|
}
|
|
167
263
|
|
|
168
|
-
function handle(
|
|
169
|
-
const
|
|
264
|
+
function handle(control) {
|
|
265
|
+
const action = control.action;
|
|
170
266
|
const body =
|
|
171
|
-
|
|
267
|
+
control.input && typeof control.input === "object" ? control.input : {};
|
|
172
268
|
let queue = readJson(queuePath, { items: [] });
|
|
173
269
|
let locks = cleanExpiredLocks(readJson(locksPath, {}));
|
|
174
|
-
if (
|
|
270
|
+
if (action === "stop") {
|
|
175
271
|
writeJson(locksPath, locks);
|
|
176
|
-
journal("
|
|
177
|
-
|
|
272
|
+
journal("lock.stopped", {});
|
|
273
|
+
emitTrace("lock.stopped", "Locker stopped", {
|
|
178
274
|
queueDepth: queue.items?.length ?? 0,
|
|
179
275
|
});
|
|
180
|
-
|
|
276
|
+
return true;
|
|
181
277
|
}
|
|
182
|
-
if (
|
|
278
|
+
if (action === "enqueue") {
|
|
183
279
|
const item = {
|
|
184
280
|
id: body.id || `task-${Date.now()}`,
|
|
185
281
|
task: body.task ?? body,
|
|
@@ -190,19 +286,20 @@ async function runLocker(argv = process.argv.slice(2)) {
|
|
|
190
286
|
writeJson(queuePath, queue);
|
|
191
287
|
writeJson(locksPath, locks);
|
|
192
288
|
journal("lock.enqueued", { id: item.id, resources: item.resources });
|
|
193
|
-
|
|
289
|
+
emitTrace("lock.enqueued", `Queued task ${item.id}`, {
|
|
194
290
|
id: item.id,
|
|
195
291
|
queueDepth: queue.items.length,
|
|
196
292
|
});
|
|
197
293
|
return;
|
|
198
294
|
}
|
|
199
|
-
if (
|
|
200
|
-
const owner = body.owner
|
|
295
|
+
if (action === "claim") {
|
|
296
|
+
const owner = body.owner;
|
|
297
|
+
if (!owner) throw new Error("claim input.owner is required");
|
|
201
298
|
const item = nextTask(queue, locks);
|
|
202
299
|
if (!item) {
|
|
203
300
|
writeJson(queuePath, queue);
|
|
204
301
|
writeJson(locksPath, locks);
|
|
205
|
-
|
|
302
|
+
emitTrace("lock.empty", "No claimable task", {
|
|
206
303
|
owner,
|
|
207
304
|
queueDepth: queue.items?.length ?? 0,
|
|
208
305
|
});
|
|
@@ -217,15 +314,16 @@ async function runLocker(argv = process.argv.slice(2)) {
|
|
|
217
314
|
owner,
|
|
218
315
|
resources: item.resources,
|
|
219
316
|
});
|
|
220
|
-
|
|
317
|
+
emitTrace("lock.assigned", `Assigned task ${item.id}`, { owner, task: item });
|
|
221
318
|
return;
|
|
222
319
|
}
|
|
223
|
-
if (
|
|
320
|
+
if (action === "acquire") {
|
|
224
321
|
const resource = body.resource;
|
|
225
|
-
const owner = body.owner
|
|
226
|
-
if (!resource
|
|
322
|
+
const owner = body.owner;
|
|
323
|
+
if (!resource || !owner)
|
|
324
|
+
throw new Error("acquire input.resource and input.owner are required");
|
|
227
325
|
if (locks[resource])
|
|
228
|
-
|
|
326
|
+
emitTrace(
|
|
229
327
|
"lock.denied",
|
|
230
328
|
`Lock denied ${resource}`,
|
|
231
329
|
{ resource, owner, current: locks[resource] },
|
|
@@ -233,25 +331,26 @@ async function runLocker(argv = process.argv.slice(2)) {
|
|
|
233
331
|
);
|
|
234
332
|
else {
|
|
235
333
|
locks[resource] = { owner, expiresAt: now() + leaseMs };
|
|
236
|
-
|
|
334
|
+
emitTrace("lock.granted", `Lock granted ${resource}`, { resource, owner });
|
|
237
335
|
}
|
|
238
336
|
writeJson(locksPath, locks);
|
|
239
337
|
return;
|
|
240
338
|
}
|
|
241
|
-
if (
|
|
339
|
+
if (action === "renew") {
|
|
242
340
|
const resource = body.resource;
|
|
243
|
-
const owner = body.owner
|
|
244
|
-
if (!resource
|
|
341
|
+
const owner = body.owner;
|
|
342
|
+
if (!resource || !owner)
|
|
343
|
+
throw new Error("renew input.resource and input.owner are required");
|
|
245
344
|
const current = locks[resource];
|
|
246
345
|
if (!current) {
|
|
247
|
-
|
|
346
|
+
emitTrace(
|
|
248
347
|
"lock.denied",
|
|
249
348
|
`Lock renew denied ${resource}`,
|
|
250
349
|
{ resource, owner, reason: "missing" },
|
|
251
350
|
"warning",
|
|
252
351
|
);
|
|
253
352
|
} else if (current.owner !== owner) {
|
|
254
|
-
|
|
353
|
+
emitTrace(
|
|
255
354
|
"lock.denied",
|
|
256
355
|
`Lock renew denied ${resource}`,
|
|
257
356
|
{ resource, owner, current },
|
|
@@ -259,29 +358,24 @@ async function runLocker(argv = process.argv.slice(2)) {
|
|
|
259
358
|
);
|
|
260
359
|
} else {
|
|
261
360
|
locks[resource] = { ...current, expiresAt: now() + leaseMs };
|
|
262
|
-
|
|
361
|
+
emitTrace("lock.renewed", `Lock renewed ${resource}`, { resource, owner });
|
|
263
362
|
}
|
|
264
363
|
writeJson(locksPath, locks);
|
|
265
364
|
return;
|
|
266
365
|
}
|
|
267
|
-
if (
|
|
366
|
+
if (action === "release") {
|
|
268
367
|
const resource = body.resource;
|
|
269
368
|
if (resource) delete locks[resource];
|
|
270
369
|
writeJson(locksPath, locks);
|
|
271
|
-
|
|
370
|
+
emitTrace("lock.released", `Lock released ${resource}`, { resource });
|
|
272
371
|
return;
|
|
273
372
|
}
|
|
274
373
|
if (
|
|
275
|
-
|
|
276
|
-
type === "lock.fail" ||
|
|
277
|
-
type === "coord.complete" ||
|
|
278
|
-
type === "coord.fail"
|
|
374
|
+
action === "complete" || action === "fail"
|
|
279
375
|
) {
|
|
280
|
-
const eventType =
|
|
281
|
-
? type.replace("coord.", "lock.")
|
|
282
|
-
: type;
|
|
376
|
+
const eventType = `lock.${action}`;
|
|
283
377
|
journal(eventType, body);
|
|
284
|
-
|
|
378
|
+
emitTrace(
|
|
285
379
|
eventType,
|
|
286
380
|
`${eventType} ${body.id ?? ""}`.trim(),
|
|
287
381
|
body,
|
|
@@ -291,8 +385,8 @@ async function runLocker(argv = process.argv.slice(2)) {
|
|
|
291
385
|
writeJson(queuePath, queue);
|
|
292
386
|
return;
|
|
293
387
|
}
|
|
294
|
-
journal("lock.unknown", {
|
|
295
|
-
|
|
388
|
+
journal("lock.unknown", { action, input: body });
|
|
389
|
+
emitTrace("lock.unknown", `Unknown Control ${action}`, { action, input: body }, "warning");
|
|
296
390
|
}
|
|
297
391
|
|
|
298
392
|
if (mode === "snapshot") {
|
|
@@ -300,15 +394,35 @@ async function runLocker(argv = process.argv.slice(2)) {
|
|
|
300
394
|
process.exit(0);
|
|
301
395
|
}
|
|
302
396
|
|
|
397
|
+
const startupRun = readJson(join(stateDir, "run.json"), undefined);
|
|
398
|
+
if (!startupRun || typeof startupRun.run_instance_id !== "string") {
|
|
399
|
+
throw new Error("Run generation unavailable for Control service");
|
|
400
|
+
}
|
|
401
|
+
runInstanceId = startupRun.run_instance_id;
|
|
402
|
+
|
|
303
403
|
function handleLine(line) {
|
|
304
|
-
const
|
|
305
|
-
if (!
|
|
404
|
+
const control = normalizeControl(line);
|
|
405
|
+
if (!control) return false;
|
|
406
|
+
const claimed = claimControl(control.id);
|
|
407
|
+
if (!claimed) {
|
|
408
|
+
emitTrace(
|
|
409
|
+
"lock.control_rejected",
|
|
410
|
+
"Rejected stale or unjournaled Control",
|
|
411
|
+
{ id: control.id },
|
|
412
|
+
"warning",
|
|
413
|
+
);
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
306
416
|
try {
|
|
307
|
-
handle(
|
|
417
|
+
const stopping = handle(claimed) === true;
|
|
418
|
+
finalizeControl(claimed.id, "handled");
|
|
419
|
+
return stopping;
|
|
308
420
|
} catch (error) {
|
|
309
421
|
const text = error instanceof Error ? error.message : String(error);
|
|
422
|
+
finalizeControl(claimed.id, "failed", text);
|
|
310
423
|
journal("lock.error", { error: text });
|
|
311
|
-
|
|
424
|
+
emitTrace("lock.error", text, { error: text }, "error");
|
|
425
|
+
return false;
|
|
312
426
|
}
|
|
313
427
|
}
|
|
314
428
|
|
|
@@ -320,19 +434,39 @@ async function runLocker(argv = process.argv.slice(2)) {
|
|
|
320
434
|
}
|
|
321
435
|
writeControlEndpoint(endpoint);
|
|
322
436
|
while (true) {
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
|
|
437
|
+
const fs = await import("node:fs");
|
|
438
|
+
const fd = fs.openSync(endpoint.path, fs.constants.O_RDWR);
|
|
439
|
+
const stream = fs.createReadStream(undefined, {
|
|
440
|
+
autoClose: false,
|
|
441
|
+
encoding: "utf8",
|
|
442
|
+
fd,
|
|
443
|
+
});
|
|
326
444
|
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
327
|
-
for await (const line of rl)
|
|
445
|
+
for await (const line of rl) {
|
|
446
|
+
if (handleLine(line)) {
|
|
447
|
+
fs.writeSync(fd, "\n");
|
|
448
|
+
stream.destroy();
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
fs.closeSync(fd);
|
|
328
453
|
}
|
|
329
454
|
}
|
|
330
455
|
|
|
331
456
|
async function serveNamedPipe(endpoint) {
|
|
332
457
|
rmSync(endpoint.path, { force: true });
|
|
458
|
+
let resolveStopped;
|
|
459
|
+
const stopped = new Promise((resolve) => {
|
|
460
|
+
resolveStopped = resolve;
|
|
461
|
+
});
|
|
333
462
|
const server = createServer((socket) => {
|
|
334
463
|
const rl = readline.createInterface({ input: socket, crlfDelay: Infinity });
|
|
335
|
-
rl.on("line",
|
|
464
|
+
rl.on("line", (line) => {
|
|
465
|
+
if (!handleLine(line)) return;
|
|
466
|
+
rl.close();
|
|
467
|
+
socket.destroy();
|
|
468
|
+
server.close(() => resolveStopped());
|
|
469
|
+
});
|
|
336
470
|
});
|
|
337
471
|
await new Promise((resolveReady, rejectReady) => {
|
|
338
472
|
server.once("error", rejectReady);
|
|
@@ -342,14 +476,14 @@ async function runLocker(argv = process.argv.slice(2)) {
|
|
|
342
476
|
resolveReady();
|
|
343
477
|
});
|
|
344
478
|
});
|
|
345
|
-
await
|
|
479
|
+
await stopped;
|
|
346
480
|
}
|
|
347
481
|
|
|
348
482
|
const endpoint = getControlEndpoint();
|
|
349
483
|
writeJson(queuePath, readJson(queuePath, { items: [] }));
|
|
350
484
|
writeJson(locksPath, cleanExpiredLocks(readJson(locksPath, {})));
|
|
351
485
|
journal("lock.started", { leaseMs, control: endpoint.type });
|
|
352
|
-
|
|
486
|
+
emitTrace("lock.started", "Locker ready", { leaseMs, control: endpoint.type });
|
|
353
487
|
if (endpoint.type === "named-pipe") await serveNamedPipe(endpoint);
|
|
354
488
|
else await serveFifo(endpoint);
|
|
355
489
|
}
|