@llblab/pi-actors 0.42.3 → 0.43.1
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 +127 -175
- package/BACKLOG.md +189 -1
- package/CHANGELOG.md +183 -292
- package/README.md +115 -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/index.js +1 -1
- package/dist/lib/async-runs.d.ts +15 -38
- package/dist/lib/async-runs.js +173 -111
- package/dist/lib/automatic-review-runtime.d.ts +1 -1
- package/dist/lib/automatic-review-runtime.js +5 -5
- package/dist/lib/command-templates.d.ts +2 -0
- package/dist/lib/command-templates.js +38 -4
- package/dist/lib/control-projection.d.ts +20 -0
- package/dist/lib/control-projection.js +66 -0
- package/dist/lib/control.d.ts +15 -0
- package/dist/lib/control.js +97 -0
- package/dist/lib/draft-sleep.js +3 -3
- package/dist/lib/execution-sessions.d.ts +17 -0
- package/dist/lib/execution-sessions.js +85 -0
- package/dist/lib/file-state.d.ts +2 -0
- package/dist/lib/file-state.js +114 -46
- 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 +54 -70
- package/dist/lib/inspector-overlay.js +576 -910
- package/dist/lib/inspector.d.ts +3 -71
- package/dist/lib/inspector.js +19 -665
- package/dist/lib/limits.d.ts +7 -3
- package/dist/lib/limits.js +7 -3
- 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 +3 -3
- package/dist/lib/recipe-control.d.ts +7 -0
- package/dist/lib/recipe-control.js +43 -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-control.d.ts +1 -1
- package/dist/lib/review-control.js +4 -5
- 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 +28 -0
- package/dist/lib/runs-control-delivery.js +150 -0
- package/dist/lib/runs-controls.d.ts +37 -0
- package/dist/lib/runs-controls.js +146 -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 +102 -0
- package/dist/lib/runtime-identity.d.ts +7 -0
- package/dist/lib/runtime-identity.js +35 -0
- package/dist/lib/runtime-notifier.d.ts +1 -1
- package/dist/lib/runtime-notifier.js +1 -1
- package/dist/lib/runtime-triage.d.ts +29 -0
- package/dist/lib/runtime-triage.js +76 -0
- package/dist/lib/tool-review-scheduler.js +7 -7
- package/dist/lib/tools-inspect.d.ts +3 -3
- package/dist/lib/tools-inspect.js +241 -707
- package/dist/lib/tools-local.js +2 -10
- package/dist/lib/tools-message.d.ts +6 -7
- package/dist/lib/tools-message.js +95 -396
- package/dist/lib/tools-response.d.ts +1 -5
- package/dist/lib/tools-response.js +5 -48
- package/dist/lib/tools-spawn.js +16 -28
- package/dist/lib/tools.d.ts +1 -1
- package/dist/lib/tools.js +2 -3
- package/dist/lib/trace-projection.d.ts +22 -0
- package/dist/lib/trace-projection.js +185 -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 +196 -69
- package/dist/scripts/music-player.mjs +162 -159
- package/dist/scripts/recipe-utils.mjs +6 -96
- package/dist/scripts/release-gates.mjs +91 -0
- package/dist/scripts/validate-recipe.mjs +8 -57
- package/dist/skills/actors/SKILL.md +57 -265
- package/dist/skills/swarm/SKILL.md +10 -34
- package/docs/0.43-baseline.md +39 -0
- package/docs/README.md +4 -6
- package/docs/actor-inspector.md +26 -64
- package/docs/async-runs.md +81 -328
- package/docs/command-templates.md +8 -118
- package/docs/recipe-library.md +55 -182
- package/docs/releasing.md +28 -0
- 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/index.ts +1 -1
- package/lib/async-runs.ts +218 -204
- package/lib/automatic-review-runtime.ts +7 -7
- package/lib/command-templates.ts +44 -4
- package/lib/control-projection.ts +105 -0
- package/lib/control.ts +117 -0
- package/lib/draft-sleep.ts +3 -3
- package/lib/execution-sessions.ts +111 -0
- package/lib/file-state.ts +84 -64
- package/lib/inspector-actions.ts +2 -2
- package/lib/inspector-command.ts +3 -3
- package/lib/inspector-overlay.ts +617 -1126
- package/lib/inspector.ts +46 -979
- package/lib/limits.ts +7 -3
- package/lib/observability.ts +60 -101
- package/lib/prompts.ts +3 -3
- package/lib/recipe-control.ts +52 -0
- package/lib/recipes-discovery.ts +2 -0
- package/lib/recipes-references.ts +9 -45
- package/lib/review-control.ts +4 -5
- package/lib/review-projection.ts +1 -5
- package/lib/run-ui-runtime.ts +2 -2
- package/lib/runs-control-delivery.ts +209 -0
- package/lib/runs-controls.ts +213 -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 +136 -0
- package/lib/runtime-identity.ts +39 -0
- package/lib/runtime-notifier.ts +1 -1
- package/lib/runtime-triage.ts +120 -0
- package/lib/tool-review-scheduler.ts +7 -7
- package/lib/tools-inspect.ts +283 -900
- package/lib/tools-local.ts +2 -12
- package/lib/tools-message.ts +112 -520
- package/lib/tools-response.ts +5 -64
- package/lib/tools-spawn.ts +16 -32
- package/lib/tools.ts +5 -6
- package/lib/trace-projection.ts +244 -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 +196 -69
- package/scripts/music-player.mjs +162 -159
- package/scripts/recipe-utils.mjs +6 -96
- package/scripts/release-gates.mjs +91 -0
- package/scripts/validate-recipe.mjs +8 -57
- package/skills/actors/SKILL.md +57 -265
- package/skills/swarm/SKILL.md +10 -34
- 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/docs/actors-deep-reference.md +0 -66
- package/docs/component-recipes.md +0 -148
- package/docs/task-first-recipes.md +0 -263
- 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
|
@@ -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
|
|
@@ -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/dist/scripts/locker.mjs
CHANGED
|
@@ -15,12 +15,26 @@ import {
|
|
|
15
15
|
existsSync,
|
|
16
16
|
mkdirSync,
|
|
17
17
|
readFileSync,
|
|
18
|
-
rmSync,
|
|
19
|
-
writeFileSync,
|
|
20
18
|
} from "node:fs";
|
|
21
19
|
import { createServer } from "node:net";
|
|
22
20
|
import { dirname, join, resolve } from "node:path";
|
|
23
21
|
import readline from "node:readline";
|
|
22
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
23
|
+
|
|
24
|
+
function packageRoot() {
|
|
25
|
+
return dirname(dirname(fileURLToPath(import.meta.url)));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function importRuntimeModule(name) {
|
|
29
|
+
const root = packageRoot();
|
|
30
|
+
const compiled = join(root, "dist", "lib", `${name}.js`);
|
|
31
|
+
const source = join(root, "lib", `${name}.ts`);
|
|
32
|
+
return await import(pathToFileURL(existsSync(compiled) ? compiled : source).href);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const { acquireFileMutationLock, writeJsonAtomic, writeTextAtomic } =
|
|
36
|
+
await importRuntimeModule("file-state");
|
|
37
|
+
const { appendRunTraceEvent } = await importRuntimeModule("runs-trace");
|
|
24
38
|
|
|
25
39
|
function parseArgs(argv) {
|
|
26
40
|
const args = { mode: "serve", stateDir: "", leaseMs: 600000, lines: 20 };
|
|
@@ -45,8 +59,9 @@ 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 controlsPath = join(stateDir, "controls.jsonl");
|
|
49
63
|
const controlPath = join(stateDir, "control.fifo");
|
|
64
|
+
let runInstanceId;
|
|
50
65
|
mkdirSync(stateDir, { recursive: true });
|
|
51
66
|
|
|
52
67
|
function readJson(path, fallback) {
|
|
@@ -59,8 +74,7 @@ async function runLocker(argv = process.argv.slice(2)) {
|
|
|
59
74
|
}
|
|
60
75
|
|
|
61
76
|
function writeJson(path, value) {
|
|
62
|
-
|
|
63
|
-
writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
77
|
+
writeJsonAtomic(path, value);
|
|
64
78
|
}
|
|
65
79
|
|
|
66
80
|
function getControlEndpoint() {
|
|
@@ -73,11 +87,12 @@ async function runLocker(argv = process.argv.slice(2)) {
|
|
|
73
87
|
}
|
|
74
88
|
|
|
75
89
|
function writeControlEndpoint(endpoint) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
90
|
+
if (!runInstanceId) throw new Error("Run generation unavailable for Control endpoint");
|
|
91
|
+
writeJson(join(stateDir, "control-endpoint.json"), {
|
|
92
|
+
...endpoint,
|
|
93
|
+
ready_at: new Date().toISOString(),
|
|
94
|
+
run_instance_id: runInstanceId,
|
|
95
|
+
});
|
|
81
96
|
}
|
|
82
97
|
|
|
83
98
|
function journal(event, data = {}) {
|
|
@@ -87,11 +102,14 @@ async function runLocker(argv = process.argv.slice(2)) {
|
|
|
87
102
|
);
|
|
88
103
|
}
|
|
89
104
|
|
|
90
|
-
function
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
105
|
+
function emitTrace(kind, summary, data = {}, level = "info") {
|
|
106
|
+
appendRunTraceEvent(stateDir, {
|
|
107
|
+
attention: "followup",
|
|
108
|
+
data,
|
|
109
|
+
kind,
|
|
110
|
+
level,
|
|
111
|
+
summary,
|
|
112
|
+
});
|
|
95
113
|
}
|
|
96
114
|
|
|
97
115
|
function now() {
|
|
@@ -108,16 +126,88 @@ async function runLocker(argv = process.argv.slice(2)) {
|
|
|
108
126
|
return kept;
|
|
109
127
|
}
|
|
110
128
|
|
|
111
|
-
function
|
|
129
|
+
function normalizeControl(line) {
|
|
112
130
|
const trimmed = line.trim();
|
|
113
131
|
if (!trimmed) return undefined;
|
|
114
|
-
|
|
115
|
-
|
|
132
|
+
try {
|
|
133
|
+
const control = JSON.parse(trimmed);
|
|
134
|
+
if (
|
|
135
|
+
!control ||
|
|
136
|
+
typeof control !== "object" ||
|
|
137
|
+
typeof control.id !== "string" ||
|
|
138
|
+
typeof control.action !== "string"
|
|
139
|
+
) {
|
|
140
|
+
throw new Error("invalid Control envelope");
|
|
141
|
+
}
|
|
142
|
+
return control;
|
|
143
|
+
} catch (error) {
|
|
144
|
+
throw new Error(
|
|
145
|
+
`Invalid Control envelope: ${error instanceof Error ? error.message : String(error)}`,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function acquireControlsLock() {
|
|
151
|
+
return acquireFileMutationLock(controlsPath);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function readControls() {
|
|
155
|
+
if (!existsSync(controlsPath)) return [];
|
|
156
|
+
return readFileSync(controlsPath, "utf8")
|
|
157
|
+
.split("\n")
|
|
158
|
+
.filter((line) => line.trim())
|
|
159
|
+
.map((line) => {
|
|
160
|
+
try {
|
|
161
|
+
return JSON.parse(line);
|
|
162
|
+
} catch {
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
})
|
|
166
|
+
.filter(Boolean);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function writeControls(controls) {
|
|
170
|
+
writeTextAtomic(
|
|
171
|
+
controlsPath,
|
|
172
|
+
controls.length
|
|
173
|
+
? `${controls.map((control) => JSON.stringify(control)).join("\n")}\n`
|
|
174
|
+
: "",
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function claimControl(id) {
|
|
179
|
+
const release = acquireControlsLock();
|
|
180
|
+
try {
|
|
181
|
+
const controls = readControls();
|
|
182
|
+
const control = controls.find((item) => item.id === id);
|
|
183
|
+
if (
|
|
184
|
+
!control ||
|
|
185
|
+
control.run_instance_id !== runInstanceId ||
|
|
186
|
+
(control.status !== "queued" && control.status !== "delivered")
|
|
187
|
+
) {
|
|
188
|
+
return undefined;
|
|
189
|
+
}
|
|
190
|
+
control.claimed_at = new Date().toISOString();
|
|
191
|
+
control.status = "claimed";
|
|
192
|
+
writeControls(controls);
|
|
193
|
+
return { ...control };
|
|
194
|
+
} finally {
|
|
195
|
+
release();
|
|
116
196
|
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function finalizeControl(id, status, error) {
|
|
200
|
+
const release = acquireControlsLock();
|
|
117
201
|
try {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
202
|
+
const controls = readControls();
|
|
203
|
+
const control = controls.find((item) => item.id === id);
|
|
204
|
+
if (!control || control.status !== "claimed") return;
|
|
205
|
+
control.status = status;
|
|
206
|
+
control[`${status}_at`] = new Date().toISOString();
|
|
207
|
+
if (error) control.error = error;
|
|
208
|
+
writeControls(controls);
|
|
209
|
+
} finally {
|
|
210
|
+
release();
|
|
121
211
|
}
|
|
122
212
|
}
|
|
123
213
|
|
|
@@ -165,21 +255,21 @@ async function runLocker(argv = process.argv.slice(2)) {
|
|
|
165
255
|
return items.splice(index, 1)[0];
|
|
166
256
|
}
|
|
167
257
|
|
|
168
|
-
function handle(
|
|
169
|
-
const
|
|
258
|
+
function handle(control) {
|
|
259
|
+
const action = control.action;
|
|
170
260
|
const body =
|
|
171
|
-
|
|
261
|
+
control.input && typeof control.input === "object" ? control.input : {};
|
|
172
262
|
let queue = readJson(queuePath, { items: [] });
|
|
173
263
|
let locks = cleanExpiredLocks(readJson(locksPath, {}));
|
|
174
|
-
if (
|
|
264
|
+
if (action === "stop") {
|
|
175
265
|
writeJson(locksPath, locks);
|
|
176
|
-
journal("
|
|
177
|
-
|
|
266
|
+
journal("lock.stopped", {});
|
|
267
|
+
emitTrace("lock.stopped", "Locker stopped", {
|
|
178
268
|
queueDepth: queue.items?.length ?? 0,
|
|
179
269
|
});
|
|
180
|
-
|
|
270
|
+
return true;
|
|
181
271
|
}
|
|
182
|
-
if (
|
|
272
|
+
if (action === "enqueue") {
|
|
183
273
|
const item = {
|
|
184
274
|
id: body.id || `task-${Date.now()}`,
|
|
185
275
|
task: body.task ?? body,
|
|
@@ -190,19 +280,20 @@ async function runLocker(argv = process.argv.slice(2)) {
|
|
|
190
280
|
writeJson(queuePath, queue);
|
|
191
281
|
writeJson(locksPath, locks);
|
|
192
282
|
journal("lock.enqueued", { id: item.id, resources: item.resources });
|
|
193
|
-
|
|
283
|
+
emitTrace("lock.enqueued", `Queued task ${item.id}`, {
|
|
194
284
|
id: item.id,
|
|
195
285
|
queueDepth: queue.items.length,
|
|
196
286
|
});
|
|
197
287
|
return;
|
|
198
288
|
}
|
|
199
|
-
if (
|
|
200
|
-
const owner = body.owner
|
|
289
|
+
if (action === "claim") {
|
|
290
|
+
const owner = body.owner;
|
|
291
|
+
if (!owner) throw new Error("claim input.owner is required");
|
|
201
292
|
const item = nextTask(queue, locks);
|
|
202
293
|
if (!item) {
|
|
203
294
|
writeJson(queuePath, queue);
|
|
204
295
|
writeJson(locksPath, locks);
|
|
205
|
-
|
|
296
|
+
emitTrace("lock.empty", "No claimable task", {
|
|
206
297
|
owner,
|
|
207
298
|
queueDepth: queue.items?.length ?? 0,
|
|
208
299
|
});
|
|
@@ -217,15 +308,16 @@ async function runLocker(argv = process.argv.slice(2)) {
|
|
|
217
308
|
owner,
|
|
218
309
|
resources: item.resources,
|
|
219
310
|
});
|
|
220
|
-
|
|
311
|
+
emitTrace("lock.assigned", `Assigned task ${item.id}`, { owner, task: item });
|
|
221
312
|
return;
|
|
222
313
|
}
|
|
223
|
-
if (
|
|
314
|
+
if (action === "acquire") {
|
|
224
315
|
const resource = body.resource;
|
|
225
|
-
const owner = body.owner
|
|
226
|
-
if (!resource
|
|
316
|
+
const owner = body.owner;
|
|
317
|
+
if (!resource || !owner)
|
|
318
|
+
throw new Error("acquire input.resource and input.owner are required");
|
|
227
319
|
if (locks[resource])
|
|
228
|
-
|
|
320
|
+
emitTrace(
|
|
229
321
|
"lock.denied",
|
|
230
322
|
`Lock denied ${resource}`,
|
|
231
323
|
{ resource, owner, current: locks[resource] },
|
|
@@ -233,25 +325,26 @@ async function runLocker(argv = process.argv.slice(2)) {
|
|
|
233
325
|
);
|
|
234
326
|
else {
|
|
235
327
|
locks[resource] = { owner, expiresAt: now() + leaseMs };
|
|
236
|
-
|
|
328
|
+
emitTrace("lock.granted", `Lock granted ${resource}`, { resource, owner });
|
|
237
329
|
}
|
|
238
330
|
writeJson(locksPath, locks);
|
|
239
331
|
return;
|
|
240
332
|
}
|
|
241
|
-
if (
|
|
333
|
+
if (action === "renew") {
|
|
242
334
|
const resource = body.resource;
|
|
243
|
-
const owner = body.owner
|
|
244
|
-
if (!resource
|
|
335
|
+
const owner = body.owner;
|
|
336
|
+
if (!resource || !owner)
|
|
337
|
+
throw new Error("renew input.resource and input.owner are required");
|
|
245
338
|
const current = locks[resource];
|
|
246
339
|
if (!current) {
|
|
247
|
-
|
|
340
|
+
emitTrace(
|
|
248
341
|
"lock.denied",
|
|
249
342
|
`Lock renew denied ${resource}`,
|
|
250
343
|
{ resource, owner, reason: "missing" },
|
|
251
344
|
"warning",
|
|
252
345
|
);
|
|
253
346
|
} else if (current.owner !== owner) {
|
|
254
|
-
|
|
347
|
+
emitTrace(
|
|
255
348
|
"lock.denied",
|
|
256
349
|
`Lock renew denied ${resource}`,
|
|
257
350
|
{ resource, owner, current },
|
|
@@ -259,29 +352,24 @@ async function runLocker(argv = process.argv.slice(2)) {
|
|
|
259
352
|
);
|
|
260
353
|
} else {
|
|
261
354
|
locks[resource] = { ...current, expiresAt: now() + leaseMs };
|
|
262
|
-
|
|
355
|
+
emitTrace("lock.renewed", `Lock renewed ${resource}`, { resource, owner });
|
|
263
356
|
}
|
|
264
357
|
writeJson(locksPath, locks);
|
|
265
358
|
return;
|
|
266
359
|
}
|
|
267
|
-
if (
|
|
360
|
+
if (action === "release") {
|
|
268
361
|
const resource = body.resource;
|
|
269
362
|
if (resource) delete locks[resource];
|
|
270
363
|
writeJson(locksPath, locks);
|
|
271
|
-
|
|
364
|
+
emitTrace("lock.released", `Lock released ${resource}`, { resource });
|
|
272
365
|
return;
|
|
273
366
|
}
|
|
274
367
|
if (
|
|
275
|
-
|
|
276
|
-
type === "lock.fail" ||
|
|
277
|
-
type === "coord.complete" ||
|
|
278
|
-
type === "coord.fail"
|
|
368
|
+
action === "complete" || action === "fail"
|
|
279
369
|
) {
|
|
280
|
-
const eventType =
|
|
281
|
-
? type.replace("coord.", "lock.")
|
|
282
|
-
: type;
|
|
370
|
+
const eventType = `lock.${action}`;
|
|
283
371
|
journal(eventType, body);
|
|
284
|
-
|
|
372
|
+
emitTrace(
|
|
285
373
|
eventType,
|
|
286
374
|
`${eventType} ${body.id ?? ""}`.trim(),
|
|
287
375
|
body,
|
|
@@ -291,8 +379,8 @@ async function runLocker(argv = process.argv.slice(2)) {
|
|
|
291
379
|
writeJson(queuePath, queue);
|
|
292
380
|
return;
|
|
293
381
|
}
|
|
294
|
-
journal("lock.unknown", {
|
|
295
|
-
|
|
382
|
+
journal("lock.unknown", { action, input: body });
|
|
383
|
+
emitTrace("lock.unknown", `Unknown Control ${action}`, { action, input: body }, "warning");
|
|
296
384
|
}
|
|
297
385
|
|
|
298
386
|
if (mode === "snapshot") {
|
|
@@ -300,15 +388,35 @@ async function runLocker(argv = process.argv.slice(2)) {
|
|
|
300
388
|
process.exit(0);
|
|
301
389
|
}
|
|
302
390
|
|
|
391
|
+
const startupRun = readJson(join(stateDir, "run.json"), undefined);
|
|
392
|
+
if (!startupRun || typeof startupRun.run_instance_id !== "string") {
|
|
393
|
+
throw new Error("Run generation unavailable for Control service");
|
|
394
|
+
}
|
|
395
|
+
runInstanceId = startupRun.run_instance_id;
|
|
396
|
+
|
|
303
397
|
function handleLine(line) {
|
|
304
|
-
const
|
|
305
|
-
if (!
|
|
398
|
+
const control = normalizeControl(line);
|
|
399
|
+
if (!control) return false;
|
|
400
|
+
const claimed = claimControl(control.id);
|
|
401
|
+
if (!claimed) {
|
|
402
|
+
emitTrace(
|
|
403
|
+
"lock.control_rejected",
|
|
404
|
+
"Rejected stale or unjournaled Control",
|
|
405
|
+
{ id: control.id },
|
|
406
|
+
"warning",
|
|
407
|
+
);
|
|
408
|
+
return false;
|
|
409
|
+
}
|
|
306
410
|
try {
|
|
307
|
-
handle(
|
|
411
|
+
const stopping = handle(claimed) === true;
|
|
412
|
+
finalizeControl(claimed.id, "handled");
|
|
413
|
+
return stopping;
|
|
308
414
|
} catch (error) {
|
|
309
415
|
const text = error instanceof Error ? error.message : String(error);
|
|
416
|
+
finalizeControl(claimed.id, "failed", text);
|
|
310
417
|
journal("lock.error", { error: text });
|
|
311
|
-
|
|
418
|
+
emitTrace("lock.error", text, { error: text }, "error");
|
|
419
|
+
return false;
|
|
312
420
|
}
|
|
313
421
|
}
|
|
314
422
|
|
|
@@ -320,19 +428,38 @@ async function runLocker(argv = process.argv.slice(2)) {
|
|
|
320
428
|
}
|
|
321
429
|
writeControlEndpoint(endpoint);
|
|
322
430
|
while (true) {
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
|
|
431
|
+
const fs = await import("node:fs");
|
|
432
|
+
const fd = fs.openSync(endpoint.path, fs.constants.O_RDWR);
|
|
433
|
+
const stream = fs.createReadStream(undefined, {
|
|
434
|
+
autoClose: false,
|
|
435
|
+
encoding: "utf8",
|
|
436
|
+
fd,
|
|
437
|
+
});
|
|
326
438
|
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
327
|
-
for await (const line of rl)
|
|
439
|
+
for await (const line of rl) {
|
|
440
|
+
if (handleLine(line)) {
|
|
441
|
+
fs.writeSync(fd, "\n");
|
|
442
|
+
stream.destroy();
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
fs.closeSync(fd);
|
|
328
447
|
}
|
|
329
448
|
}
|
|
330
449
|
|
|
331
450
|
async function serveNamedPipe(endpoint) {
|
|
332
|
-
|
|
451
|
+
let resolveStopped;
|
|
452
|
+
const stopped = new Promise((resolve) => {
|
|
453
|
+
resolveStopped = resolve;
|
|
454
|
+
});
|
|
333
455
|
const server = createServer((socket) => {
|
|
334
456
|
const rl = readline.createInterface({ input: socket, crlfDelay: Infinity });
|
|
335
|
-
rl.on("line",
|
|
457
|
+
rl.on("line", (line) => {
|
|
458
|
+
if (!handleLine(line)) return;
|
|
459
|
+
rl.close();
|
|
460
|
+
socket.destroy();
|
|
461
|
+
server.close(() => resolveStopped());
|
|
462
|
+
});
|
|
336
463
|
});
|
|
337
464
|
await new Promise((resolveReady, rejectReady) => {
|
|
338
465
|
server.once("error", rejectReady);
|
|
@@ -342,14 +469,14 @@ async function runLocker(argv = process.argv.slice(2)) {
|
|
|
342
469
|
resolveReady();
|
|
343
470
|
});
|
|
344
471
|
});
|
|
345
|
-
await
|
|
472
|
+
await stopped;
|
|
346
473
|
}
|
|
347
474
|
|
|
348
475
|
const endpoint = getControlEndpoint();
|
|
349
476
|
writeJson(queuePath, readJson(queuePath, { items: [] }));
|
|
350
477
|
writeJson(locksPath, cleanExpiredLocks(readJson(locksPath, {})));
|
|
351
478
|
journal("lock.started", { leaseMs, control: endpoint.type });
|
|
352
|
-
|
|
479
|
+
emitTrace("lock.started", "Locker ready", { leaseMs, control: endpoint.type });
|
|
353
480
|
if (endpoint.type === "named-pipe") await serveNamedPipe(endpoint);
|
|
354
481
|
else await serveFifo(endpoint);
|
|
355
482
|
}
|