@llblab/pi-actors 0.42.2 → 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 +16 -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 +17 -17
- package/dist/lib/observability.js +45 -84
- package/dist/lib/pi.d.ts +1 -1
- 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 +63 -104
- package/lib/pi.ts +1 -1
- 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
|
@@ -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,21 @@ 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");
|
|
39
55
|
|
|
40
56
|
const AUDIO_EXTENSIONS = new Set([
|
|
41
57
|
".aac",
|
|
@@ -55,7 +71,6 @@ const CONTROL_COMMANDS = new Set([
|
|
|
55
71
|
"toggle",
|
|
56
72
|
"next",
|
|
57
73
|
"previous",
|
|
58
|
-
"prev",
|
|
59
74
|
"stop",
|
|
60
75
|
"status",
|
|
61
76
|
]);
|
|
@@ -66,9 +81,9 @@ function usage() {
|
|
|
66
81
|
music-player.mjs <pause|resume|toggle|next|previous|stop|status> <state-dir>
|
|
67
82
|
music-player.mjs control <state-dir> <play|pause|toggle|next|previous|stop|status>
|
|
68
83
|
|
|
69
|
-
Runs a
|
|
70
|
-
|
|
71
|
-
Prefer message
|
|
84
|
+
Runs a foreground music player so pi-actors can own it as a controlled Run.
|
|
85
|
+
Controls use canonical records in <state-dir>/controls.jsonl.
|
|
86
|
+
Prefer message target=run:<run> action=<command>, or use direct control commands below.
|
|
72
87
|
Supported players: auto, mpv, afplay, ffplay, cvlc, play, wmp.
|
|
73
88
|
`);
|
|
74
89
|
}
|
|
@@ -404,20 +419,16 @@ function readText(path) {
|
|
|
404
419
|
}
|
|
405
420
|
}
|
|
406
421
|
|
|
407
|
-
function emitPlayerEvent(ctx,
|
|
422
|
+
function emitPlayerEvent(ctx, kind, summary, data = {}) {
|
|
408
423
|
writeText(
|
|
409
424
|
ctx.eventFile,
|
|
410
425
|
`${JSON.stringify({
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
event: type,
|
|
415
|
-
from: `run:${basename(ctx.stateDir)}`,
|
|
426
|
+
data,
|
|
427
|
+
id: randomUUID(),
|
|
428
|
+
kind,
|
|
416
429
|
level: "info",
|
|
417
430
|
summary,
|
|
418
|
-
to: "coordinator",
|
|
419
431
|
ts: new Date().toISOString(),
|
|
420
|
-
type,
|
|
421
432
|
})}\n`,
|
|
422
433
|
"a",
|
|
423
434
|
);
|
|
@@ -468,13 +479,20 @@ function setState(ctx, state) {
|
|
|
468
479
|
function sendSignalToCurrent(ctx, signal) {
|
|
469
480
|
const pid = Number(readText(ctx.pidFile).trim());
|
|
470
481
|
if (!Number.isInteger(pid) || pid <= 0) return;
|
|
471
|
-
if (process.platform
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
482
|
+
if (process.platform === "win32") {
|
|
483
|
+
if (signal === "SIGTERM") {
|
|
484
|
+
spawnSync("taskkill", ["/PID", String(pid), "/T", "/F"], {
|
|
485
|
+
stdio: "ignore",
|
|
486
|
+
windowsHide: true,
|
|
487
|
+
});
|
|
477
488
|
}
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
try {
|
|
492
|
+
process.kill(-pid, signal);
|
|
493
|
+
return;
|
|
494
|
+
} catch {
|
|
495
|
+
// Fall through to the direct child fallback.
|
|
478
496
|
}
|
|
479
497
|
try {
|
|
480
498
|
process.kill(pid, signal);
|
|
@@ -539,6 +557,7 @@ function handleControl(ctx, input) {
|
|
|
539
557
|
controlCurrentPlayback(ctx, "stop");
|
|
540
558
|
break;
|
|
541
559
|
case "stop":
|
|
560
|
+
ctx.stopping = true;
|
|
542
561
|
writeText(ctx.commandFile, "stop");
|
|
543
562
|
controlCurrentPlayback(ctx, "stop");
|
|
544
563
|
break;
|
|
@@ -563,45 +582,42 @@ function writeJsonFile(path, value) {
|
|
|
563
582
|
writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
564
583
|
}
|
|
565
584
|
|
|
566
|
-
function
|
|
567
|
-
|
|
568
|
-
const
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
585
|
+
async function startControlServer(ctx, wakeControlLoop) {
|
|
586
|
+
if (!ctx.runInstanceId) return undefined;
|
|
587
|
+
const path = process.platform === "win32"
|
|
588
|
+
? `\\\\.\\pipe\\pi-actors-music-${String(process.env.run_id ?? process.pid).replaceAll(/[^A-Za-z0-9_-]/g, "-")}`
|
|
589
|
+
: join(ctx.stateDir, "control.sock");
|
|
590
|
+
if (process.platform !== "win32") rmSync(path, { force: true });
|
|
591
|
+
const server = createServer((socket) => {
|
|
592
|
+
socket.on("data", wakeControlLoop);
|
|
593
|
+
socket.resume();
|
|
594
|
+
});
|
|
595
|
+
await new Promise((resolveReady, rejectReady) => {
|
|
596
|
+
server.once("error", rejectReady);
|
|
597
|
+
server.listen(path, () => {
|
|
598
|
+
server.off("error", rejectReady);
|
|
599
|
+
writeJsonFile(join(ctx.stateDir, "control-endpoint.json"), {
|
|
600
|
+
path,
|
|
601
|
+
ready_at: new Date().toISOString(),
|
|
602
|
+
run_instance_id: ctx.runInstanceId,
|
|
603
|
+
type: "named-pipe",
|
|
604
|
+
});
|
|
605
|
+
resolveReady();
|
|
606
|
+
});
|
|
573
607
|
});
|
|
608
|
+
return () => {
|
|
609
|
+
server.close();
|
|
610
|
+
if (process.platform !== "win32") rmSync(path, { force: true });
|
|
611
|
+
};
|
|
574
612
|
}
|
|
575
613
|
|
|
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
|
-
}
|
|
614
|
+
function acquireControlsLock(ctx) {
|
|
615
|
+
return acquireFileMutationLock(ctx.controlsFile);
|
|
600
616
|
}
|
|
601
617
|
|
|
602
|
-
function
|
|
603
|
-
if (!exists(ctx.
|
|
604
|
-
return readFileSync(ctx.
|
|
618
|
+
function readControls(ctx) {
|
|
619
|
+
if (!exists(ctx.controlsFile)) return [];
|
|
620
|
+
return readFileSync(ctx.controlsFile, "utf8")
|
|
605
621
|
.split("\n")
|
|
606
622
|
.filter((line) => line.trim())
|
|
607
623
|
.map((line) => {
|
|
@@ -614,45 +630,26 @@ function readInboxMessages(ctx) {
|
|
|
614
630
|
.filter(Boolean);
|
|
615
631
|
}
|
|
616
632
|
|
|
617
|
-
function
|
|
618
|
-
|
|
619
|
-
ctx.
|
|
620
|
-
|
|
621
|
-
? `${
|
|
633
|
+
function writeControls(ctx, controls) {
|
|
634
|
+
writeTextAtomic(
|
|
635
|
+
ctx.controlsFile,
|
|
636
|
+
controls.length
|
|
637
|
+
? `${controls.map((control) => JSON.stringify(control)).join("\n")}\n`
|
|
622
638
|
: "",
|
|
623
|
-
"utf8",
|
|
624
639
|
);
|
|
625
640
|
}
|
|
626
641
|
|
|
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;
|
|
642
|
+
function commandFromControl(control) {
|
|
643
|
+
if (typeof control.action !== "string") return undefined;
|
|
644
|
+
const action = control.action.trim();
|
|
645
|
+
return CONTROL_COMMANDS.has(action) ? action : undefined;
|
|
649
646
|
}
|
|
650
647
|
|
|
651
648
|
function runtimeWakeFile(ctx) {
|
|
652
649
|
return join(ctx.stateDir, "wake.jsonl");
|
|
653
650
|
}
|
|
654
651
|
|
|
655
|
-
function
|
|
652
|
+
function notifyControlWake(ctx, reason = "control.queued") {
|
|
656
653
|
try {
|
|
657
654
|
writeText(
|
|
658
655
|
runtimeWakeFile(ctx),
|
|
@@ -667,86 +664,85 @@ function notifyMailboxWake(ctx, reason = "run.message") {
|
|
|
667
664
|
"a",
|
|
668
665
|
);
|
|
669
666
|
} catch {
|
|
670
|
-
// Wake records are advisory; the
|
|
667
|
+
// Wake records are advisory; the Control journal remains authoritative.
|
|
671
668
|
}
|
|
672
669
|
}
|
|
673
670
|
|
|
674
|
-
function
|
|
675
|
-
const release =
|
|
671
|
+
function appendControl(ctx, action) {
|
|
672
|
+
const release = acquireControlsLock(ctx);
|
|
676
673
|
try {
|
|
677
|
-
const
|
|
678
|
-
const
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
from: "coordinator",
|
|
674
|
+
const run = readJsonFile(runJsonFile(ctx), {});
|
|
675
|
+
const controls = readControls(ctx);
|
|
676
|
+
controls.push({
|
|
677
|
+
action,
|
|
682
678
|
id: randomUUID(),
|
|
683
|
-
queued_at:
|
|
684
|
-
|
|
679
|
+
queued_at: new Date().toISOString(),
|
|
680
|
+
run_instance_id: run.run_instance_id,
|
|
685
681
|
status: "queued",
|
|
686
|
-
to: `run:${basename(ctx.stateDir)}`,
|
|
687
|
-
type: `player.${command}`,
|
|
688
682
|
});
|
|
689
|
-
|
|
683
|
+
writeControls(ctx, controls);
|
|
690
684
|
} finally {
|
|
691
685
|
release();
|
|
692
686
|
}
|
|
693
|
-
|
|
687
|
+
notifyControlWake(ctx);
|
|
694
688
|
}
|
|
695
689
|
|
|
696
|
-
function
|
|
697
|
-
const release =
|
|
690
|
+
function claimControls(ctx) {
|
|
691
|
+
const release = acquireControlsLock(ctx);
|
|
698
692
|
try {
|
|
699
|
-
const
|
|
693
|
+
const controls = readControls(ctx);
|
|
700
694
|
const commands = [];
|
|
701
695
|
let changed = false;
|
|
702
696
|
const claimedAt = new Date().toISOString();
|
|
703
|
-
for (const
|
|
704
|
-
if (
|
|
705
|
-
const command =
|
|
697
|
+
for (const control of controls) {
|
|
698
|
+
if (control.status !== "queued" && control.status !== "delivered") continue;
|
|
699
|
+
const command =
|
|
700
|
+
control.run_instance_id === ctx.runInstanceId
|
|
701
|
+
? commandFromControl(control)
|
|
702
|
+
: undefined;
|
|
706
703
|
if (!command) {
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
704
|
+
control.failed_at = claimedAt;
|
|
705
|
+
control.status = "failed";
|
|
706
|
+
control.error = "Unsupported or stale music-player Control";
|
|
710
707
|
changed = true;
|
|
711
708
|
continue;
|
|
712
709
|
}
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
commands.push({ command, id: message.id });
|
|
710
|
+
control.claimed_at = claimedAt;
|
|
711
|
+
control.status = "claimed";
|
|
712
|
+
commands.push({ command, id: control.id });
|
|
717
713
|
changed = true;
|
|
718
714
|
}
|
|
719
|
-
if (changed)
|
|
715
|
+
if (changed) writeControls(ctx, controls);
|
|
720
716
|
return commands;
|
|
721
717
|
} finally {
|
|
722
718
|
release();
|
|
723
719
|
}
|
|
724
720
|
}
|
|
725
721
|
|
|
726
|
-
function
|
|
722
|
+
function finalizeControl(ctx, id, status, error) {
|
|
727
723
|
if (!id) return;
|
|
728
|
-
const release =
|
|
724
|
+
const release = acquireControlsLock(ctx);
|
|
729
725
|
try {
|
|
730
|
-
const
|
|
726
|
+
const controls = readControls(ctx);
|
|
731
727
|
const timestamp = new Date().toISOString();
|
|
732
728
|
let changed = false;
|
|
733
|
-
for (const
|
|
734
|
-
if (
|
|
735
|
-
|
|
736
|
-
if (status === "handled")
|
|
737
|
-
else
|
|
738
|
-
if (error)
|
|
729
|
+
for (const control of controls) {
|
|
730
|
+
if (control.id !== id || control.status !== "claimed") continue;
|
|
731
|
+
control.status = status;
|
|
732
|
+
if (status === "handled") control.handled_at = timestamp;
|
|
733
|
+
else control.failed_at = timestamp;
|
|
734
|
+
if (error) control.error = error;
|
|
739
735
|
changed = true;
|
|
740
736
|
}
|
|
741
|
-
if (changed)
|
|
737
|
+
if (changed) writeControls(ctx, controls);
|
|
742
738
|
} finally {
|
|
743
739
|
release();
|
|
744
740
|
}
|
|
745
741
|
}
|
|
746
742
|
|
|
747
|
-
function
|
|
743
|
+
function controlsSignature(ctx) {
|
|
748
744
|
try {
|
|
749
|
-
const stat = statSync(ctx.
|
|
745
|
+
const stat = statSync(ctx.controlsFile);
|
|
750
746
|
return `${stat.size}:${stat.mtimeMs}`;
|
|
751
747
|
} catch {
|
|
752
748
|
return "missing";
|
|
@@ -757,19 +753,21 @@ function startControlLoop(ctx) {
|
|
|
757
753
|
let closed = false;
|
|
758
754
|
let dirty = true;
|
|
759
755
|
let watcher;
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
756
|
+
if (process.platform !== "win32") {
|
|
757
|
+
try {
|
|
758
|
+
watcher = watch(ctx.stateDir, { persistent: false }, (_eventType, file) => {
|
|
759
|
+
const name = file ? String(file) : "";
|
|
760
|
+
if (
|
|
761
|
+
!name ||
|
|
762
|
+
name === basename(ctx.controlsFile) ||
|
|
763
|
+
name === basename(runtimeWakeFile(ctx))
|
|
764
|
+
) {
|
|
765
|
+
dirty = true;
|
|
766
|
+
}
|
|
767
|
+
});
|
|
768
|
+
} catch {
|
|
769
|
+
// fs.watch is advisory; signature checks remain the portable fallback.
|
|
770
|
+
}
|
|
773
771
|
}
|
|
774
772
|
const close = () => {
|
|
775
773
|
closed = true;
|
|
@@ -778,37 +776,43 @@ function startControlLoop(ctx) {
|
|
|
778
776
|
const promise = (async () => {
|
|
779
777
|
let lastSignature = "";
|
|
780
778
|
while (!ctx.stopping && !closed) {
|
|
781
|
-
const signature =
|
|
779
|
+
const signature = controlsSignature(ctx);
|
|
782
780
|
if (dirty || signature !== lastSignature) {
|
|
783
781
|
dirty = false;
|
|
784
|
-
for (const { command, id } of
|
|
782
|
+
for (const { command, id } of claimControls(ctx)) {
|
|
785
783
|
try {
|
|
786
784
|
handleControl(ctx, command);
|
|
787
|
-
|
|
785
|
+
finalizeControl(ctx, id, "handled");
|
|
788
786
|
} catch (error) {
|
|
789
|
-
|
|
787
|
+
finalizeControl(ctx, id, "failed", error.message);
|
|
790
788
|
}
|
|
791
789
|
}
|
|
792
|
-
lastSignature =
|
|
790
|
+
lastSignature = controlsSignature(ctx);
|
|
793
791
|
continue;
|
|
794
792
|
}
|
|
795
793
|
await sleep(250);
|
|
796
794
|
}
|
|
797
795
|
})();
|
|
798
|
-
return { close, promise };
|
|
796
|
+
return { close, promise, wake: () => { dirty = true; } };
|
|
799
797
|
}
|
|
800
798
|
|
|
801
799
|
function playOne(ctx, player, volume, track, index, count) {
|
|
802
800
|
return new Promise((resolveDone) => {
|
|
801
|
+
if (ctx.stopping) {
|
|
802
|
+
resolveDone();
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
803
805
|
rmSync(ctx.playerControlFile, { force: true });
|
|
804
806
|
const [command, args] = playerCommand(ctx, player, volume, track);
|
|
805
807
|
writeStatus(ctx, "playing", index, count, track, player, "");
|
|
806
808
|
const child = spawn(command, args, {
|
|
809
|
+
detached: process.platform === "win32",
|
|
807
810
|
stdio: ["ignore", "inherit", "inherit"],
|
|
808
811
|
});
|
|
809
812
|
ctx.child = child;
|
|
810
813
|
const pid = child.pid || "";
|
|
811
814
|
if (pid) writeText(ctx.pidFile, String(pid));
|
|
815
|
+
if (ctx.stopping) controlCurrentPlayback(ctx, "stop");
|
|
812
816
|
writeStatus(ctx, "playing", index, count, track, player, pid);
|
|
813
817
|
emitTrackEvent(ctx, index, count, track, player);
|
|
814
818
|
child.once("error", (error) => {
|
|
@@ -851,13 +855,17 @@ async function playMain(args) {
|
|
|
851
855
|
),
|
|
852
856
|
);
|
|
853
857
|
mkdirSync(stateDir, { recursive: true });
|
|
858
|
+
const startupRun = readJsonFile(runJsonFile({ stateDir }), {});
|
|
854
859
|
const ctx = {
|
|
855
860
|
commandFile: join(stateDir, "command.txt"),
|
|
856
861
|
current: undefined,
|
|
857
|
-
eventFile: join(stateDir, "
|
|
858
|
-
|
|
862
|
+
eventFile: join(stateDir, "trace.jsonl"),
|
|
863
|
+
controlsFile: join(stateDir, "controls.jsonl"),
|
|
859
864
|
pidFile: join(stateDir, "current.pid"),
|
|
860
865
|
playerControlFile: join(stateDir, "player-control.txt"),
|
|
866
|
+
runInstanceId: typeof startupRun.run_instance_id === "string"
|
|
867
|
+
? startupRun.run_instance_id
|
|
868
|
+
: undefined,
|
|
861
869
|
stateDir,
|
|
862
870
|
stateFile: join(stateDir, "player-state.txt"),
|
|
863
871
|
statusFile: join(stateDir, "status.txt"),
|
|
@@ -872,6 +880,7 @@ async function playMain(args) {
|
|
|
872
880
|
const player = selectPlayer(playerArg);
|
|
873
881
|
const tracks = loadPlaylist(sourceArg);
|
|
874
882
|
let controlLoop;
|
|
883
|
+
let closeControlServer;
|
|
875
884
|
const cleanup = () => {
|
|
876
885
|
ctx.stopping = true;
|
|
877
886
|
writeText(ctx.stateFile, "stopped");
|
|
@@ -881,6 +890,7 @@ async function playMain(args) {
|
|
|
881
890
|
} catch {}
|
|
882
891
|
}
|
|
883
892
|
controlLoop?.close();
|
|
893
|
+
closeControlServer?.();
|
|
884
894
|
rmSync(ctx.pidFile, { force: true });
|
|
885
895
|
rmSync(ctx.playerControlFile, { force: true });
|
|
886
896
|
};
|
|
@@ -897,8 +907,8 @@ async function playMain(args) {
|
|
|
897
907
|
process.exit(129);
|
|
898
908
|
});
|
|
899
909
|
try {
|
|
900
|
-
updateRunControlMetadata(ctx);
|
|
901
910
|
controlLoop = startControlLoop(ctx);
|
|
911
|
+
closeControlServer = await startControlServer(ctx, controlLoop.wake);
|
|
902
912
|
setState(ctx, "playing");
|
|
903
913
|
console.error(
|
|
904
914
|
`music-player: player=${player} loop=${loop} volume=${volume} tracks=${tracks.length} state_dir=${stateDir}`,
|
|
@@ -952,8 +962,8 @@ function controlMain(args) {
|
|
|
952
962
|
);
|
|
953
963
|
return;
|
|
954
964
|
}
|
|
955
|
-
|
|
956
|
-
{
|
|
965
|
+
appendControl(
|
|
966
|
+
{ controlsFile: join(stateDir, "controls.jsonl"), stateDir },
|
|
957
967
|
command,
|
|
958
968
|
);
|
|
959
969
|
console.log(`music-player: command=${command} queued state_dir=${stateDir}`);
|
|
@@ -966,7 +976,6 @@ const directControlCommands = new Set([
|
|
|
966
976
|
"toggle",
|
|
967
977
|
"next",
|
|
968
978
|
"previous",
|
|
969
|
-
"prev",
|
|
970
979
|
"stop",
|
|
971
980
|
"status",
|
|
972
981
|
]);
|