@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
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Packaged local music-player
|
|
4
|
+
* Packaged local music-player controlled-service helper.
|
|
5
5
|
*
|
|
6
6
|
* This script backs the standard music-player recipe. It scans local music
|
|
7
7
|
* sources, builds playback queues, launches an available backend player, and
|
|
8
|
-
* consumes
|
|
8
|
+
* consumes generation-bound Controls such as play, pause, next, previous,
|
|
9
9
|
* stop, and status.
|
|
10
10
|
*
|
|
11
|
-
* Keep the helper focused on one maintained
|
|
11
|
+
* Keep the helper focused on one maintained controlled Run implementation; Recipe
|
|
12
12
|
* metadata and invocation arguments choose source paths and backend behavior.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import { spawn } from "node:child_process";
|
|
15
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
16
16
|
import { randomUUID } from "node:crypto";
|
|
17
17
|
import {
|
|
18
18
|
accessSync,
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
watch,
|
|
27
27
|
writeFileSync,
|
|
28
28
|
} from "node:fs";
|
|
29
|
+
import { createServer } from "node:net";
|
|
29
30
|
import { homedir } from "node:os";
|
|
30
31
|
import {
|
|
31
32
|
basename,
|
|
@@ -36,6 +37,22 @@ import {
|
|
|
36
37
|
join,
|
|
37
38
|
resolve,
|
|
38
39
|
} from "node:path";
|
|
40
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
41
|
+
|
|
42
|
+
function packageRoot() {
|
|
43
|
+
return dirname(dirname(fileURLToPath(import.meta.url)));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function importRuntimeModule(name) {
|
|
47
|
+
const root = packageRoot();
|
|
48
|
+
const compiled = join(root, "dist", "lib", `${name}.js`);
|
|
49
|
+
const source = join(root, "lib", `${name}.ts`);
|
|
50
|
+
return await import(pathToFileURL(existsSync(compiled) ? compiled : source).href);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const { acquireFileMutationLock, writeTextAtomic } =
|
|
54
|
+
await importRuntimeModule("file-state");
|
|
55
|
+
const { appendRunTraceEvent } = await importRuntimeModule("runs-trace");
|
|
39
56
|
|
|
40
57
|
const AUDIO_EXTENSIONS = new Set([
|
|
41
58
|
".aac",
|
|
@@ -55,7 +72,6 @@ const CONTROL_COMMANDS = new Set([
|
|
|
55
72
|
"toggle",
|
|
56
73
|
"next",
|
|
57
74
|
"previous",
|
|
58
|
-
"prev",
|
|
59
75
|
"stop",
|
|
60
76
|
"status",
|
|
61
77
|
]);
|
|
@@ -66,9 +82,9 @@ function usage() {
|
|
|
66
82
|
music-player.mjs <pause|resume|toggle|next|previous|stop|status> <state-dir>
|
|
67
83
|
music-player.mjs control <state-dir> <play|pause|toggle|next|previous|stop|status>
|
|
68
84
|
|
|
69
|
-
Runs a
|
|
70
|
-
|
|
71
|
-
Prefer message
|
|
85
|
+
Runs a foreground music player so pi-actors can own it as a controlled Run.
|
|
86
|
+
Controls use canonical records in <state-dir>/controls.jsonl.
|
|
87
|
+
Prefer message target=run:<run> action=<command>, or use direct control commands below.
|
|
72
88
|
Supported players: auto, mpv, afplay, ffplay, cvlc, play, wmp.
|
|
73
89
|
`);
|
|
74
90
|
}
|
|
@@ -404,23 +420,13 @@ function readText(path) {
|
|
|
404
420
|
}
|
|
405
421
|
}
|
|
406
422
|
|
|
407
|
-
function emitPlayerEvent(ctx,
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
event: type,
|
|
415
|
-
from: `run:${basename(ctx.stateDir)}`,
|
|
416
|
-
level: "info",
|
|
417
|
-
summary,
|
|
418
|
-
to: "coordinator",
|
|
419
|
-
ts: new Date().toISOString(),
|
|
420
|
-
type,
|
|
421
|
-
})}\n`,
|
|
422
|
-
"a",
|
|
423
|
-
);
|
|
423
|
+
function emitPlayerEvent(ctx, kind, summary, data = {}) {
|
|
424
|
+
appendRunTraceEvent(ctx.stateDir, {
|
|
425
|
+
data,
|
|
426
|
+
kind,
|
|
427
|
+
level: "info",
|
|
428
|
+
summary,
|
|
429
|
+
});
|
|
424
430
|
}
|
|
425
431
|
|
|
426
432
|
function emitTrackEvent(ctx, index, count, track, player) {
|
|
@@ -468,13 +474,20 @@ function setState(ctx, state) {
|
|
|
468
474
|
function sendSignalToCurrent(ctx, signal) {
|
|
469
475
|
const pid = Number(readText(ctx.pidFile).trim());
|
|
470
476
|
if (!Number.isInteger(pid) || pid <= 0) return;
|
|
471
|
-
if (process.platform
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
+
if (process.platform === "win32") {
|
|
478
|
+
if (signal === "SIGTERM") {
|
|
479
|
+
spawnSync("taskkill", ["/PID", String(pid), "/T", "/F"], {
|
|
480
|
+
stdio: "ignore",
|
|
481
|
+
windowsHide: true,
|
|
482
|
+
});
|
|
477
483
|
}
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
try {
|
|
487
|
+
process.kill(-pid, signal);
|
|
488
|
+
return;
|
|
489
|
+
} catch {
|
|
490
|
+
// Fall through to the direct child fallback.
|
|
478
491
|
}
|
|
479
492
|
try {
|
|
480
493
|
process.kill(pid, signal);
|
|
@@ -539,6 +552,7 @@ function handleControl(ctx, input) {
|
|
|
539
552
|
controlCurrentPlayback(ctx, "stop");
|
|
540
553
|
break;
|
|
541
554
|
case "stop":
|
|
555
|
+
ctx.stopping = true;
|
|
542
556
|
writeText(ctx.commandFile, "stop");
|
|
543
557
|
controlCurrentPlayback(ctx, "stop");
|
|
544
558
|
break;
|
|
@@ -563,45 +577,42 @@ function writeJsonFile(path, value) {
|
|
|
563
577
|
writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
564
578
|
}
|
|
565
579
|
|
|
566
|
-
function
|
|
567
|
-
|
|
568
|
-
const
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
580
|
+
async function startControlServer(ctx, wakeControlLoop) {
|
|
581
|
+
if (!ctx.runInstanceId) return undefined;
|
|
582
|
+
const path = process.platform === "win32"
|
|
583
|
+
? `\\\\.\\pipe\\pi-actors-music-${String(process.env.run_id ?? process.pid).replaceAll(/[^A-Za-z0-9_-]/g, "-")}`
|
|
584
|
+
: join(ctx.stateDir, "control.sock");
|
|
585
|
+
if (process.platform !== "win32") rmSync(path, { force: true });
|
|
586
|
+
const server = createServer((socket) => {
|
|
587
|
+
socket.on("data", wakeControlLoop);
|
|
588
|
+
socket.resume();
|
|
573
589
|
});
|
|
590
|
+
await new Promise((resolveReady, rejectReady) => {
|
|
591
|
+
server.once("error", rejectReady);
|
|
592
|
+
server.listen(path, () => {
|
|
593
|
+
server.off("error", rejectReady);
|
|
594
|
+
writeJsonFile(join(ctx.stateDir, "control-endpoint.json"), {
|
|
595
|
+
path,
|
|
596
|
+
ready_at: new Date().toISOString(),
|
|
597
|
+
run_instance_id: ctx.runInstanceId,
|
|
598
|
+
type: "named-pipe",
|
|
599
|
+
});
|
|
600
|
+
resolveReady();
|
|
601
|
+
});
|
|
602
|
+
});
|
|
603
|
+
return () => {
|
|
604
|
+
server.close();
|
|
605
|
+
if (process.platform !== "win32") rmSync(path, { force: true });
|
|
606
|
+
};
|
|
574
607
|
}
|
|
575
608
|
|
|
576
|
-
function
|
|
577
|
-
|
|
578
|
-
const started = Date.now();
|
|
579
|
-
while (true) {
|
|
580
|
-
try {
|
|
581
|
-
mkdirSync(lockDir);
|
|
582
|
-
writeJsonFile(join(lockDir, "owner.json"), {
|
|
583
|
-
created_at: new Date().toISOString(),
|
|
584
|
-
pid: process.pid,
|
|
585
|
-
});
|
|
586
|
-
return () => rmSync(lockDir, { recursive: true, force: true });
|
|
587
|
-
} catch (error) {
|
|
588
|
-
try {
|
|
589
|
-
const stat = statSync(lockDir);
|
|
590
|
-
if (Date.now() - stat.mtimeMs > 5 * 60 * 1000) {
|
|
591
|
-
rmSync(lockDir, { recursive: true, force: true });
|
|
592
|
-
continue;
|
|
593
|
-
}
|
|
594
|
-
} catch {
|
|
595
|
-
continue;
|
|
596
|
-
}
|
|
597
|
-
if (Date.now() - started > 5000) throw error;
|
|
598
|
-
}
|
|
599
|
-
}
|
|
609
|
+
function acquireControlsLock(ctx) {
|
|
610
|
+
return acquireFileMutationLock(ctx.controlsFile);
|
|
600
611
|
}
|
|
601
612
|
|
|
602
|
-
function
|
|
603
|
-
if (!exists(ctx.
|
|
604
|
-
return readFileSync(ctx.
|
|
613
|
+
function readControls(ctx) {
|
|
614
|
+
if (!exists(ctx.controlsFile)) return [];
|
|
615
|
+
return readFileSync(ctx.controlsFile, "utf8")
|
|
605
616
|
.split("\n")
|
|
606
617
|
.filter((line) => line.trim())
|
|
607
618
|
.map((line) => {
|
|
@@ -614,45 +625,26 @@ function readInboxMessages(ctx) {
|
|
|
614
625
|
.filter(Boolean);
|
|
615
626
|
}
|
|
616
627
|
|
|
617
|
-
function
|
|
618
|
-
|
|
619
|
-
ctx.
|
|
620
|
-
|
|
621
|
-
? `${
|
|
628
|
+
function writeControls(ctx, controls) {
|
|
629
|
+
writeTextAtomic(
|
|
630
|
+
ctx.controlsFile,
|
|
631
|
+
controls.length
|
|
632
|
+
? `${controls.map((control) => JSON.stringify(control)).join("\n")}\n`
|
|
622
633
|
: "",
|
|
623
|
-
"utf8",
|
|
624
634
|
);
|
|
625
635
|
}
|
|
626
636
|
|
|
627
|
-
function
|
|
628
|
-
if (
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
if (match && CONTROL_COMMANDS.has(match[1])) return match[1];
|
|
632
|
-
return undefined;
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
function commandFromInboxMessage(message) {
|
|
636
|
-
if (typeof message.type === "string") {
|
|
637
|
-
const command = commandFromControlToken(message.type.trim());
|
|
638
|
-
if (command) return command;
|
|
639
|
-
}
|
|
640
|
-
if (typeof message.body === "string") {
|
|
641
|
-
const command = message.body.trim();
|
|
642
|
-
if (CONTROL_COMMANDS.has(command)) return command;
|
|
643
|
-
}
|
|
644
|
-
if (message.body && typeof message.body === "object") {
|
|
645
|
-
const command = String(message.body.command ?? "").trim();
|
|
646
|
-
if (CONTROL_COMMANDS.has(command)) return command;
|
|
647
|
-
}
|
|
648
|
-
return undefined;
|
|
637
|
+
function commandFromControl(control) {
|
|
638
|
+
if (typeof control.action !== "string") return undefined;
|
|
639
|
+
const action = control.action.trim();
|
|
640
|
+
return CONTROL_COMMANDS.has(action) ? action : undefined;
|
|
649
641
|
}
|
|
650
642
|
|
|
651
643
|
function runtimeWakeFile(ctx) {
|
|
652
644
|
return join(ctx.stateDir, "wake.jsonl");
|
|
653
645
|
}
|
|
654
646
|
|
|
655
|
-
function
|
|
647
|
+
function notifyControlWake(ctx, reason = "control.queued") {
|
|
656
648
|
try {
|
|
657
649
|
writeText(
|
|
658
650
|
runtimeWakeFile(ctx),
|
|
@@ -667,86 +659,85 @@ function notifyMailboxWake(ctx, reason = "run.message") {
|
|
|
667
659
|
"a",
|
|
668
660
|
);
|
|
669
661
|
} catch {
|
|
670
|
-
// Wake records are advisory; the
|
|
662
|
+
// Wake records are advisory; the Control journal remains authoritative.
|
|
671
663
|
}
|
|
672
664
|
}
|
|
673
665
|
|
|
674
|
-
function
|
|
675
|
-
const release =
|
|
666
|
+
function appendControl(ctx, action) {
|
|
667
|
+
const release = acquireControlsLock(ctx);
|
|
676
668
|
try {
|
|
677
|
-
const
|
|
678
|
-
const
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
from: "coordinator",
|
|
669
|
+
const run = readJsonFile(runJsonFile(ctx), {});
|
|
670
|
+
const controls = readControls(ctx);
|
|
671
|
+
controls.push({
|
|
672
|
+
action,
|
|
682
673
|
id: randomUUID(),
|
|
683
|
-
queued_at:
|
|
684
|
-
|
|
674
|
+
queued_at: new Date().toISOString(),
|
|
675
|
+
run_instance_id: run.run_instance_id,
|
|
685
676
|
status: "queued",
|
|
686
|
-
to: `run:${basename(ctx.stateDir)}`,
|
|
687
|
-
type: `player.${command}`,
|
|
688
677
|
});
|
|
689
|
-
|
|
678
|
+
writeControls(ctx, controls);
|
|
690
679
|
} finally {
|
|
691
680
|
release();
|
|
692
681
|
}
|
|
693
|
-
|
|
682
|
+
notifyControlWake(ctx);
|
|
694
683
|
}
|
|
695
684
|
|
|
696
|
-
function
|
|
697
|
-
const release =
|
|
685
|
+
function claimControls(ctx) {
|
|
686
|
+
const release = acquireControlsLock(ctx);
|
|
698
687
|
try {
|
|
699
|
-
const
|
|
688
|
+
const controls = readControls(ctx);
|
|
700
689
|
const commands = [];
|
|
701
690
|
let changed = false;
|
|
702
691
|
const claimedAt = new Date().toISOString();
|
|
703
|
-
for (const
|
|
704
|
-
if (
|
|
705
|
-
const command =
|
|
692
|
+
for (const control of controls) {
|
|
693
|
+
if (control.status !== "queued" && control.status !== "delivered") continue;
|
|
694
|
+
const command =
|
|
695
|
+
control.run_instance_id === ctx.runInstanceId
|
|
696
|
+
? commandFromControl(control)
|
|
697
|
+
: undefined;
|
|
706
698
|
if (!command) {
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
699
|
+
control.failed_at = claimedAt;
|
|
700
|
+
control.status = "failed";
|
|
701
|
+
control.error = "Unsupported or stale music-player Control";
|
|
710
702
|
changed = true;
|
|
711
703
|
continue;
|
|
712
704
|
}
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
commands.push({ command, id: message.id });
|
|
705
|
+
control.claimed_at = claimedAt;
|
|
706
|
+
control.status = "claimed";
|
|
707
|
+
commands.push({ command, id: control.id });
|
|
717
708
|
changed = true;
|
|
718
709
|
}
|
|
719
|
-
if (changed)
|
|
710
|
+
if (changed) writeControls(ctx, controls);
|
|
720
711
|
return commands;
|
|
721
712
|
} finally {
|
|
722
713
|
release();
|
|
723
714
|
}
|
|
724
715
|
}
|
|
725
716
|
|
|
726
|
-
function
|
|
717
|
+
function finalizeControl(ctx, id, status, error) {
|
|
727
718
|
if (!id) return;
|
|
728
|
-
const release =
|
|
719
|
+
const release = acquireControlsLock(ctx);
|
|
729
720
|
try {
|
|
730
|
-
const
|
|
721
|
+
const controls = readControls(ctx);
|
|
731
722
|
const timestamp = new Date().toISOString();
|
|
732
723
|
let changed = false;
|
|
733
|
-
for (const
|
|
734
|
-
if (
|
|
735
|
-
|
|
736
|
-
if (status === "handled")
|
|
737
|
-
else
|
|
738
|
-
if (error)
|
|
724
|
+
for (const control of controls) {
|
|
725
|
+
if (control.id !== id || control.status !== "claimed") continue;
|
|
726
|
+
control.status = status;
|
|
727
|
+
if (status === "handled") control.handled_at = timestamp;
|
|
728
|
+
else control.failed_at = timestamp;
|
|
729
|
+
if (error) control.error = error;
|
|
739
730
|
changed = true;
|
|
740
731
|
}
|
|
741
|
-
if (changed)
|
|
732
|
+
if (changed) writeControls(ctx, controls);
|
|
742
733
|
} finally {
|
|
743
734
|
release();
|
|
744
735
|
}
|
|
745
736
|
}
|
|
746
737
|
|
|
747
|
-
function
|
|
738
|
+
function controlsSignature(ctx) {
|
|
748
739
|
try {
|
|
749
|
-
const stat = statSync(ctx.
|
|
740
|
+
const stat = statSync(ctx.controlsFile);
|
|
750
741
|
return `${stat.size}:${stat.mtimeMs}`;
|
|
751
742
|
} catch {
|
|
752
743
|
return "missing";
|
|
@@ -757,19 +748,21 @@ function startControlLoop(ctx) {
|
|
|
757
748
|
let closed = false;
|
|
758
749
|
let dirty = true;
|
|
759
750
|
let watcher;
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
751
|
+
if (process.platform !== "win32") {
|
|
752
|
+
try {
|
|
753
|
+
watcher = watch(ctx.stateDir, { persistent: false }, (_eventType, file) => {
|
|
754
|
+
const name = file ? String(file) : "";
|
|
755
|
+
if (
|
|
756
|
+
!name ||
|
|
757
|
+
name === basename(ctx.controlsFile) ||
|
|
758
|
+
name === basename(runtimeWakeFile(ctx))
|
|
759
|
+
) {
|
|
760
|
+
dirty = true;
|
|
761
|
+
}
|
|
762
|
+
});
|
|
763
|
+
} catch {
|
|
764
|
+
// fs.watch is advisory; signature checks remain the portable fallback.
|
|
765
|
+
}
|
|
773
766
|
}
|
|
774
767
|
const close = () => {
|
|
775
768
|
closed = true;
|
|
@@ -778,37 +771,43 @@ function startControlLoop(ctx) {
|
|
|
778
771
|
const promise = (async () => {
|
|
779
772
|
let lastSignature = "";
|
|
780
773
|
while (!ctx.stopping && !closed) {
|
|
781
|
-
const signature =
|
|
774
|
+
const signature = controlsSignature(ctx);
|
|
782
775
|
if (dirty || signature !== lastSignature) {
|
|
783
776
|
dirty = false;
|
|
784
|
-
for (const { command, id } of
|
|
777
|
+
for (const { command, id } of claimControls(ctx)) {
|
|
785
778
|
try {
|
|
786
779
|
handleControl(ctx, command);
|
|
787
|
-
|
|
780
|
+
finalizeControl(ctx, id, "handled");
|
|
788
781
|
} catch (error) {
|
|
789
|
-
|
|
782
|
+
finalizeControl(ctx, id, "failed", error.message);
|
|
790
783
|
}
|
|
791
784
|
}
|
|
792
|
-
lastSignature =
|
|
785
|
+
lastSignature = controlsSignature(ctx);
|
|
793
786
|
continue;
|
|
794
787
|
}
|
|
795
788
|
await sleep(250);
|
|
796
789
|
}
|
|
797
790
|
})();
|
|
798
|
-
return { close, promise };
|
|
791
|
+
return { close, promise, wake: () => { dirty = true; } };
|
|
799
792
|
}
|
|
800
793
|
|
|
801
794
|
function playOne(ctx, player, volume, track, index, count) {
|
|
802
795
|
return new Promise((resolveDone) => {
|
|
796
|
+
if (ctx.stopping) {
|
|
797
|
+
resolveDone();
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
803
800
|
rmSync(ctx.playerControlFile, { force: true });
|
|
804
801
|
const [command, args] = playerCommand(ctx, player, volume, track);
|
|
805
802
|
writeStatus(ctx, "playing", index, count, track, player, "");
|
|
806
803
|
const child = spawn(command, args, {
|
|
804
|
+
detached: process.platform === "win32",
|
|
807
805
|
stdio: ["ignore", "inherit", "inherit"],
|
|
808
806
|
});
|
|
809
807
|
ctx.child = child;
|
|
810
808
|
const pid = child.pid || "";
|
|
811
809
|
if (pid) writeText(ctx.pidFile, String(pid));
|
|
810
|
+
if (ctx.stopping) controlCurrentPlayback(ctx, "stop");
|
|
812
811
|
writeStatus(ctx, "playing", index, count, track, player, pid);
|
|
813
812
|
emitTrackEvent(ctx, index, count, track, player);
|
|
814
813
|
child.once("error", (error) => {
|
|
@@ -851,13 +850,16 @@ async function playMain(args) {
|
|
|
851
850
|
),
|
|
852
851
|
);
|
|
853
852
|
mkdirSync(stateDir, { recursive: true });
|
|
853
|
+
const startupRun = readJsonFile(runJsonFile({ stateDir }), {});
|
|
854
854
|
const ctx = {
|
|
855
855
|
commandFile: join(stateDir, "command.txt"),
|
|
856
856
|
current: undefined,
|
|
857
|
-
|
|
858
|
-
inboxFile: join(stateDir, "inbox.jsonl"),
|
|
857
|
+
controlsFile: join(stateDir, "controls.jsonl"),
|
|
859
858
|
pidFile: join(stateDir, "current.pid"),
|
|
860
859
|
playerControlFile: join(stateDir, "player-control.txt"),
|
|
860
|
+
runInstanceId: typeof startupRun.run_instance_id === "string"
|
|
861
|
+
? startupRun.run_instance_id
|
|
862
|
+
: undefined,
|
|
861
863
|
stateDir,
|
|
862
864
|
stateFile: join(stateDir, "player-state.txt"),
|
|
863
865
|
statusFile: join(stateDir, "status.txt"),
|
|
@@ -872,6 +874,7 @@ async function playMain(args) {
|
|
|
872
874
|
const player = selectPlayer(playerArg);
|
|
873
875
|
const tracks = loadPlaylist(sourceArg);
|
|
874
876
|
let controlLoop;
|
|
877
|
+
let closeControlServer;
|
|
875
878
|
const cleanup = () => {
|
|
876
879
|
ctx.stopping = true;
|
|
877
880
|
writeText(ctx.stateFile, "stopped");
|
|
@@ -881,6 +884,7 @@ async function playMain(args) {
|
|
|
881
884
|
} catch {}
|
|
882
885
|
}
|
|
883
886
|
controlLoop?.close();
|
|
887
|
+
closeControlServer?.();
|
|
884
888
|
rmSync(ctx.pidFile, { force: true });
|
|
885
889
|
rmSync(ctx.playerControlFile, { force: true });
|
|
886
890
|
};
|
|
@@ -897,8 +901,8 @@ async function playMain(args) {
|
|
|
897
901
|
process.exit(129);
|
|
898
902
|
});
|
|
899
903
|
try {
|
|
900
|
-
updateRunControlMetadata(ctx);
|
|
901
904
|
controlLoop = startControlLoop(ctx);
|
|
905
|
+
closeControlServer = await startControlServer(ctx, controlLoop.wake);
|
|
902
906
|
setState(ctx, "playing");
|
|
903
907
|
console.error(
|
|
904
908
|
`music-player: player=${player} loop=${loop} volume=${volume} tracks=${tracks.length} state_dir=${stateDir}`,
|
|
@@ -952,8 +956,8 @@ function controlMain(args) {
|
|
|
952
956
|
);
|
|
953
957
|
return;
|
|
954
958
|
}
|
|
955
|
-
|
|
956
|
-
{
|
|
959
|
+
appendControl(
|
|
960
|
+
{ controlsFile: join(stateDir, "controls.jsonl"), stateDir },
|
|
957
961
|
command,
|
|
958
962
|
);
|
|
959
963
|
console.log(`music-player: command=${command} queued state_dir=${stateDir}`);
|
|
@@ -966,7 +970,6 @@ const directControlCommands = new Set([
|
|
|
966
970
|
"toggle",
|
|
967
971
|
"next",
|
|
968
972
|
"previous",
|
|
969
|
-
"prev",
|
|
970
973
|
"stop",
|
|
971
974
|
"status",
|
|
972
975
|
]);
|