@llblab/pi-actors 0.43.0 → 0.44.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 +16 -10
- package/CHANGELOG.md +425 -536
- package/README.md +13 -11
- package/dist/index.js +1 -1
- package/dist/lib/async-runs.d.ts +2 -1
- package/dist/lib/async-runs.js +24 -34
- 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 +3 -0
- package/dist/lib/control.js +27 -14
- package/dist/lib/draft-sleep.js +3 -3
- package/dist/lib/file-state.d.ts +4 -1
- package/dist/lib/file-state.js +118 -44
- package/dist/lib/inspector-overlay.d.ts +2 -0
- package/dist/lib/inspector-overlay.js +124 -69
- package/dist/lib/limits.d.ts +15 -3
- package/dist/lib/limits.js +15 -3
- package/dist/lib/observability.d.ts +4 -2
- package/dist/lib/observability.js +43 -36
- package/dist/lib/prompts.d.ts +1 -1
- package/dist/lib/prompts.js +1 -1
- package/dist/lib/recipe-control.js +6 -2
- package/dist/lib/review-control.d.ts +1 -1
- package/dist/lib/review-control.js +4 -5
- package/dist/lib/run-evidence-policy.d.ts +95 -0
- package/dist/lib/run-evidence-policy.js +177 -0
- package/dist/lib/run-ui-runtime.js +2 -0
- package/dist/lib/runs-control-delivery.d.ts +8 -1
- package/dist/lib/runs-control-delivery.js +38 -15
- package/dist/lib/runs-controls.d.ts +8 -4
- package/dist/lib/runs-controls.js +189 -50
- package/dist/lib/runs-retention.js +27 -14
- package/dist/lib/runs-trace.d.ts +26 -2
- package/dist/lib/runs-trace.js +411 -18
- package/dist/lib/runtime-identity.d.ts +7 -0
- package/dist/lib/runtime-identity.js +35 -0
- package/dist/lib/runtime-triage.d.ts +29 -0
- package/dist/lib/runtime-triage.js +60 -0
- package/dist/lib/tool-review-scheduler.js +7 -7
- package/dist/lib/tools-inspect.js +91 -18
- package/dist/lib/tools-message.d.ts +1 -2
- package/dist/lib/tools-message.js +6 -6
- package/dist/lib/tools-response.d.ts +0 -1
- package/dist/lib/tools-response.js +0 -9
- package/dist/lib/tools.d.ts +1 -1
- package/dist/lib/tools.js +1 -1
- package/dist/lib/trace-projection.js +107 -41
- package/dist/scripts/conformance.mjs +5 -0
- package/dist/scripts/locker.mjs +40 -90
- package/dist/scripts/music-player.mjs +48 -142
- package/dist/scripts/release-gates.mjs +56 -3
- package/dist/scripts/validate-recipe.mjs +5 -4
- package/dist/skills/actors/SKILL.md +17 -11
- package/dist/skills/swarm/SKILL.md +2 -4
- package/docs/README.md +1 -4
- package/docs/actor-inspector.md +6 -5
- package/docs/async-runs.md +11 -9
- package/docs/command-templates.md +6 -116
- package/docs/recipe-library.md +4 -6
- package/docs/releasing.md +28 -0
- package/docs/template-recipes.md +1 -1
- package/docs/tool-registry.md +2 -2
- package/index.ts +1 -1
- package/lib/async-runs.ts +26 -50
- 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 +33 -18
- package/lib/draft-sleep.ts +3 -3
- package/lib/file-state.ts +91 -63
- package/lib/inspector-overlay.ts +108 -61
- package/lib/limits.ts +15 -3
- package/lib/observability.ts +55 -57
- package/lib/prompts.ts +1 -1
- package/lib/recipe-control.ts +9 -2
- package/lib/review-control.ts +4 -5
- package/lib/run-evidence-policy.ts +242 -0
- package/lib/run-ui-runtime.ts +2 -0
- package/lib/runs-control-delivery.ts +45 -17
- package/lib/runs-controls.ts +180 -102
- package/lib/runs-retention.ts +28 -20
- package/lib/runs-trace.ts +499 -20
- package/lib/runtime-identity.ts +39 -0
- package/lib/runtime-triage.ts +106 -0
- package/lib/tool-review-scheduler.ts +7 -7
- package/lib/tools-inspect.ts +94 -20
- package/lib/tools-message.ts +7 -8
- package/lib/tools-response.ts +0 -12
- package/lib/tools.ts +4 -4
- package/lib/trace-projection.ts +156 -71
- package/package.json +1 -1
- package/scripts/conformance.mjs +5 -0
- package/scripts/locker.mjs +40 -90
- package/scripts/music-player.mjs +48 -142
- package/scripts/release-gates.mjs +56 -3
- package/scripts/validate-recipe.mjs +5 -4
- package/skills/actors/SKILL.md +17 -11
- package/skills/swarm/SKILL.md +2 -4
- package/dist/lib/runtime-notifier.d.ts +0 -48
- package/dist/lib/runtime-notifier.js +0 -138
- package/docs/0.43-baseline.md +0 -44
- package/docs/actors-deep-reference.md +0 -108
- package/docs/component-recipes.md +0 -45
- package/docs/task-first-recipes.md +0 -261
- package/lib/runtime-notifier.ts +0 -211
package/dist/scripts/locker.mjs
CHANGED
|
@@ -9,13 +9,11 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { spawnSync } from "node:child_process";
|
|
12
|
-
import { createHash
|
|
12
|
+
import { createHash } from "node:crypto";
|
|
13
13
|
import {
|
|
14
|
-
appendFileSync,
|
|
15
14
|
existsSync,
|
|
16
15
|
mkdirSync,
|
|
17
16
|
readFileSync,
|
|
18
|
-
rmSync,
|
|
19
17
|
} from "node:fs";
|
|
20
18
|
import { createServer } from "node:net";
|
|
21
19
|
import { dirname, join, resolve } from "node:path";
|
|
@@ -33,8 +31,13 @@ async function importRuntimeModule(name) {
|
|
|
33
31
|
return await import(pathToFileURL(existsSync(compiled) ? compiled : source).href);
|
|
34
32
|
}
|
|
35
33
|
|
|
36
|
-
const { acquireFileMutationLock, writeJsonAtomic, writeTextAtomic } =
|
|
37
|
-
|
|
34
|
+
const { acquireFileMutationLock, writeJsonAtomic, writeTextAtomic } = await importRuntimeModule("file-state");
|
|
35
|
+
const { readJsonlFileResilient } = await importRuntimeModule("state-readers");
|
|
36
|
+
const LOCKER_JOURNAL_MAX_RECORDS = 512;
|
|
37
|
+
const LOCKER_JOURNAL_MAX_BYTES = 1024 * 1024;
|
|
38
|
+
const { claimRunControlByIdInStateDir, updateRunControlStatusInStateDir } =
|
|
39
|
+
await importRuntimeModule("runs-controls");
|
|
40
|
+
const { appendRunTraceEvent } = await importRuntimeModule("runs-trace");
|
|
38
41
|
|
|
39
42
|
function parseArgs(argv) {
|
|
40
43
|
const args = { mode: "serve", stateDir: "", leaseMs: 600000, lines: 20 };
|
|
@@ -59,8 +62,6 @@ async function runLocker(argv = process.argv.slice(2)) {
|
|
|
59
62
|
const queuePath = join(stateDir, "queue.json");
|
|
60
63
|
const locksPath = join(stateDir, "locks.json");
|
|
61
64
|
const journalPath = join(stateDir, "journal.jsonl");
|
|
62
|
-
const tracePath = join(stateDir, "trace.jsonl");
|
|
63
|
-
const controlsPath = join(stateDir, "controls.jsonl");
|
|
64
65
|
const controlPath = join(stateDir, "control.fifo");
|
|
65
66
|
let runInstanceId;
|
|
66
67
|
mkdirSync(stateDir, { recursive: true });
|
|
@@ -97,25 +98,32 @@ async function runLocker(argv = process.argv.slice(2)) {
|
|
|
97
98
|
}
|
|
98
99
|
|
|
99
100
|
function journal(event, data = {}) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
101
|
+
const release = acquireFileMutationLock(journalPath);
|
|
102
|
+
try {
|
|
103
|
+
const records = [...readJsonlFileResilient(journalPath).records,
|
|
104
|
+
{ event, ts: new Date().toISOString(), ...data }]
|
|
105
|
+
.slice(-LOCKER_JOURNAL_MAX_RECORDS);
|
|
106
|
+
let content = encodeJournal(records);
|
|
107
|
+
while (records.length && Buffer.byteLength(content) > LOCKER_JOURNAL_MAX_BYTES) {
|
|
108
|
+
records.shift();
|
|
109
|
+
content = encodeJournal(records);
|
|
110
|
+
}
|
|
111
|
+
writeTextAtomic(journalPath, content);
|
|
112
|
+
} finally { release(); }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function encodeJournal(records) {
|
|
116
|
+
return records.length ? `${records.map((record) => JSON.stringify(record)).join("\n")}\n` : "";
|
|
104
117
|
}
|
|
105
118
|
|
|
106
119
|
function emitTrace(kind, summary, data = {}, level = "info") {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
level,
|
|
115
|
-
summary,
|
|
116
|
-
ts: new Date().toISOString(),
|
|
117
|
-
})}\n`,
|
|
118
|
-
);
|
|
120
|
+
appendRunTraceEvent(stateDir, {
|
|
121
|
+
attention: "followup",
|
|
122
|
+
data,
|
|
123
|
+
kind,
|
|
124
|
+
level,
|
|
125
|
+
summary,
|
|
126
|
+
});
|
|
119
127
|
}
|
|
120
128
|
|
|
121
129
|
function now() {
|
|
@@ -153,69 +161,6 @@ async function runLocker(argv = process.argv.slice(2)) {
|
|
|
153
161
|
}
|
|
154
162
|
}
|
|
155
163
|
|
|
156
|
-
function acquireControlsLock() {
|
|
157
|
-
return acquireFileMutationLock(controlsPath);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function readControls() {
|
|
161
|
-
if (!existsSync(controlsPath)) return [];
|
|
162
|
-
return readFileSync(controlsPath, "utf8")
|
|
163
|
-
.split("\n")
|
|
164
|
-
.filter((line) => line.trim())
|
|
165
|
-
.map((line) => {
|
|
166
|
-
try {
|
|
167
|
-
return JSON.parse(line);
|
|
168
|
-
} catch {
|
|
169
|
-
return undefined;
|
|
170
|
-
}
|
|
171
|
-
})
|
|
172
|
-
.filter(Boolean);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function writeControls(controls) {
|
|
176
|
-
writeTextAtomic(
|
|
177
|
-
controlsPath,
|
|
178
|
-
controls.length
|
|
179
|
-
? `${controls.map((control) => JSON.stringify(control)).join("\n")}\n`
|
|
180
|
-
: "",
|
|
181
|
-
);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function claimControl(id) {
|
|
185
|
-
const release = acquireControlsLock();
|
|
186
|
-
try {
|
|
187
|
-
const controls = readControls();
|
|
188
|
-
const control = controls.find((item) => item.id === id);
|
|
189
|
-
if (
|
|
190
|
-
!control ||
|
|
191
|
-
control.run_instance_id !== runInstanceId ||
|
|
192
|
-
(control.status !== "queued" && control.status !== "delivered")
|
|
193
|
-
) {
|
|
194
|
-
return undefined;
|
|
195
|
-
}
|
|
196
|
-
control.claimed_at = new Date().toISOString();
|
|
197
|
-
control.status = "claimed";
|
|
198
|
-
writeControls(controls);
|
|
199
|
-
return { ...control };
|
|
200
|
-
} finally {
|
|
201
|
-
release();
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function finalizeControl(id, status, error) {
|
|
206
|
-
const release = acquireControlsLock();
|
|
207
|
-
try {
|
|
208
|
-
const controls = readControls();
|
|
209
|
-
const control = controls.find((item) => item.id === id);
|
|
210
|
-
if (!control || control.status !== "claimed") return;
|
|
211
|
-
control.status = status;
|
|
212
|
-
control[`${status}_at`] = new Date().toISOString();
|
|
213
|
-
if (error) control.error = error;
|
|
214
|
-
writeControls(controls);
|
|
215
|
-
} finally {
|
|
216
|
-
release();
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
164
|
|
|
220
165
|
function tailJournal(count) {
|
|
221
166
|
if (!existsSync(journalPath)) return [];
|
|
@@ -403,7 +348,9 @@ async function runLocker(argv = process.argv.slice(2)) {
|
|
|
403
348
|
function handleLine(line) {
|
|
404
349
|
const control = normalizeControl(line);
|
|
405
350
|
if (!control) return false;
|
|
406
|
-
const claimed =
|
|
351
|
+
const claimed = claimRunControlByIdInStateDir(
|
|
352
|
+
stateDir, runInstanceId, control.id,
|
|
353
|
+
);
|
|
407
354
|
if (!claimed) {
|
|
408
355
|
emitTrace(
|
|
409
356
|
"lock.control_rejected",
|
|
@@ -415,11 +362,15 @@ async function runLocker(argv = process.argv.slice(2)) {
|
|
|
415
362
|
}
|
|
416
363
|
try {
|
|
417
364
|
const stopping = handle(claimed) === true;
|
|
418
|
-
|
|
365
|
+
updateRunControlStatusInStateDir(
|
|
366
|
+
stateDir, claimed.id, "handled", {}, ["claimed"],
|
|
367
|
+
);
|
|
419
368
|
return stopping;
|
|
420
369
|
} catch (error) {
|
|
421
370
|
const text = error instanceof Error ? error.message : String(error);
|
|
422
|
-
|
|
371
|
+
updateRunControlStatusInStateDir(
|
|
372
|
+
stateDir, claimed.id, "failed", { error: text }, ["claimed"],
|
|
373
|
+
);
|
|
423
374
|
journal("lock.error", { error: text });
|
|
424
375
|
emitTrace("lock.error", text, { error: text }, "error");
|
|
425
376
|
return false;
|
|
@@ -454,7 +405,6 @@ async function runLocker(argv = process.argv.slice(2)) {
|
|
|
454
405
|
}
|
|
455
406
|
|
|
456
407
|
async function serveNamedPipe(endpoint) {
|
|
457
|
-
rmSync(endpoint.path, { force: true });
|
|
458
408
|
let resolveStopped;
|
|
459
409
|
const stopped = new Promise((resolve) => {
|
|
460
410
|
resolveStopped = resolve;
|
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { spawn, spawnSync } from "node:child_process";
|
|
16
|
-
import { randomUUID } from "node:crypto";
|
|
17
16
|
import {
|
|
18
17
|
accessSync,
|
|
19
18
|
constants,
|
|
@@ -50,8 +49,9 @@ async function importRuntimeModule(name) {
|
|
|
50
49
|
return await import(pathToFileURL(existsSync(compiled) ? compiled : source).href);
|
|
51
50
|
}
|
|
52
51
|
|
|
53
|
-
const {
|
|
54
|
-
await importRuntimeModule("
|
|
52
|
+
const { appendRunControlInStateDir, claimRunControlByIdInStateDir,
|
|
53
|
+
updateRunControlStatusInStateDir } = await importRuntimeModule("runs-controls");
|
|
54
|
+
const { appendRunTraceEvent } = await importRuntimeModule("runs-trace");
|
|
55
55
|
|
|
56
56
|
const AUDIO_EXTENSIONS = new Set([
|
|
57
57
|
".aac",
|
|
@@ -407,8 +407,8 @@ function playerCommand(ctx, player, volume, track) {
|
|
|
407
407
|
}
|
|
408
408
|
}
|
|
409
409
|
|
|
410
|
-
function writeText(path, value
|
|
411
|
-
writeFileSync(path, value,
|
|
410
|
+
function writeText(path, value) {
|
|
411
|
+
writeFileSync(path, value, "utf8");
|
|
412
412
|
}
|
|
413
413
|
|
|
414
414
|
function readText(path) {
|
|
@@ -420,18 +420,12 @@ function readText(path) {
|
|
|
420
420
|
}
|
|
421
421
|
|
|
422
422
|
function emitPlayerEvent(ctx, kind, summary, data = {}) {
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
level: "info",
|
|
430
|
-
summary,
|
|
431
|
-
ts: new Date().toISOString(),
|
|
432
|
-
})}\n`,
|
|
433
|
-
"a",
|
|
434
|
-
);
|
|
423
|
+
appendRunTraceEvent(ctx.stateDir, {
|
|
424
|
+
data,
|
|
425
|
+
kind,
|
|
426
|
+
level: "info",
|
|
427
|
+
summary,
|
|
428
|
+
});
|
|
435
429
|
}
|
|
436
430
|
|
|
437
431
|
function emitTrackEvent(ctx, index, count, track, player) {
|
|
@@ -589,7 +583,24 @@ async function startControlServer(ctx, wakeControlLoop) {
|
|
|
589
583
|
: join(ctx.stateDir, "control.sock");
|
|
590
584
|
if (process.platform !== "win32") rmSync(path, { force: true });
|
|
591
585
|
const server = createServer((socket) => {
|
|
592
|
-
|
|
586
|
+
let content = "";
|
|
587
|
+
socket.setEncoding("utf8");
|
|
588
|
+
socket.on("data", (chunk) => { content += chunk; });
|
|
589
|
+
socket.on("end", () => {
|
|
590
|
+
for (const line of content.split("\n")) {
|
|
591
|
+
if (!line.trim()) continue;
|
|
592
|
+
try {
|
|
593
|
+
const claimed = claimControl(ctx, JSON.parse(line));
|
|
594
|
+
if (!claimed) continue;
|
|
595
|
+
handleControl(ctx, claimed.command);
|
|
596
|
+
finalizeControl(ctx, claimed.id, "handled");
|
|
597
|
+
} catch (error) {
|
|
598
|
+
const id = (() => { try { return JSON.parse(line).id; } catch { return undefined; } })();
|
|
599
|
+
finalizeControl(ctx, id, "failed", error instanceof Error ? error.message : String(error));
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
wakeControlLoop();
|
|
603
|
+
});
|
|
593
604
|
socket.resume();
|
|
594
605
|
});
|
|
595
606
|
await new Promise((resolveReady, rejectReady) => {
|
|
@@ -611,133 +622,38 @@ async function startControlServer(ctx, wakeControlLoop) {
|
|
|
611
622
|
};
|
|
612
623
|
}
|
|
613
624
|
|
|
614
|
-
function acquireControlsLock(ctx) {
|
|
615
|
-
return acquireFileMutationLock(ctx.controlsFile);
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
function readControls(ctx) {
|
|
619
|
-
if (!exists(ctx.controlsFile)) return [];
|
|
620
|
-
return readFileSync(ctx.controlsFile, "utf8")
|
|
621
|
-
.split("\n")
|
|
622
|
-
.filter((line) => line.trim())
|
|
623
|
-
.map((line) => {
|
|
624
|
-
try {
|
|
625
|
-
return JSON.parse(line);
|
|
626
|
-
} catch {
|
|
627
|
-
return undefined;
|
|
628
|
-
}
|
|
629
|
-
})
|
|
630
|
-
.filter(Boolean);
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
function writeControls(ctx, controls) {
|
|
634
|
-
writeTextAtomic(
|
|
635
|
-
ctx.controlsFile,
|
|
636
|
-
controls.length
|
|
637
|
-
? `${controls.map((control) => JSON.stringify(control)).join("\n")}\n`
|
|
638
|
-
: "",
|
|
639
|
-
);
|
|
640
|
-
}
|
|
641
|
-
|
|
642
625
|
function commandFromControl(control) {
|
|
643
626
|
if (typeof control.action !== "string") return undefined;
|
|
644
627
|
const action = control.action.trim();
|
|
645
628
|
return CONTROL_COMMANDS.has(action) ? action : undefined;
|
|
646
629
|
}
|
|
647
630
|
|
|
648
|
-
function runtimeWakeFile(ctx) {
|
|
649
|
-
return join(ctx.stateDir, "wake.jsonl");
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
function notifyControlWake(ctx, reason = "control.queued") {
|
|
653
|
-
try {
|
|
654
|
-
writeText(
|
|
655
|
-
runtimeWakeFile(ctx),
|
|
656
|
-
`${JSON.stringify({
|
|
657
|
-
actor: `run:${basename(ctx.stateDir)}`,
|
|
658
|
-
id: randomUUID(),
|
|
659
|
-
metadata: { command: "music-player" },
|
|
660
|
-
reason,
|
|
661
|
-
state_dir: ctx.stateDir,
|
|
662
|
-
ts: new Date().toISOString(),
|
|
663
|
-
})}\n`,
|
|
664
|
-
"a",
|
|
665
|
-
);
|
|
666
|
-
} catch {
|
|
667
|
-
// Wake records are advisory; the Control journal remains authoritative.
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
|
|
671
631
|
function appendControl(ctx, action) {
|
|
672
|
-
const
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
action,
|
|
678
|
-
id: randomUUID(),
|
|
679
|
-
queued_at: new Date().toISOString(),
|
|
680
|
-
run_instance_id: run.run_instance_id,
|
|
681
|
-
status: "queued",
|
|
682
|
-
});
|
|
683
|
-
writeControls(ctx, controls);
|
|
684
|
-
} finally {
|
|
685
|
-
release();
|
|
686
|
-
}
|
|
687
|
-
notifyControlWake(ctx);
|
|
632
|
+
const run = readJsonFile(runJsonFile(ctx), {});
|
|
633
|
+
appendRunControlInStateDir(ctx.stateDir, {
|
|
634
|
+
action,
|
|
635
|
+
run_instance_id: run.run_instance_id,
|
|
636
|
+
});
|
|
688
637
|
}
|
|
689
638
|
|
|
690
|
-
function
|
|
691
|
-
const
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
const command =
|
|
700
|
-
control.run_instance_id === ctx.runInstanceId
|
|
701
|
-
? commandFromControl(control)
|
|
702
|
-
: undefined;
|
|
703
|
-
if (!command) {
|
|
704
|
-
control.failed_at = claimedAt;
|
|
705
|
-
control.status = "failed";
|
|
706
|
-
control.error = "Unsupported or stale music-player Control";
|
|
707
|
-
changed = true;
|
|
708
|
-
continue;
|
|
709
|
-
}
|
|
710
|
-
control.claimed_at = claimedAt;
|
|
711
|
-
control.status = "claimed";
|
|
712
|
-
commands.push({ command, id: control.id });
|
|
713
|
-
changed = true;
|
|
714
|
-
}
|
|
715
|
-
if (changed) writeControls(ctx, controls);
|
|
716
|
-
return commands;
|
|
717
|
-
} finally {
|
|
718
|
-
release();
|
|
639
|
+
function claimControl(ctx, wire) {
|
|
640
|
+
const control = claimRunControlByIdInStateDir(
|
|
641
|
+
ctx.stateDir, ctx.runInstanceId, wire.id,
|
|
642
|
+
);
|
|
643
|
+
const command = control ? commandFromControl(control) : undefined;
|
|
644
|
+
if (!command && control) {
|
|
645
|
+
updateRunControlStatusInStateDir(ctx.stateDir, control.id, "failed", {
|
|
646
|
+
error: "Unsupported music-player Control",
|
|
647
|
+
}, ["claimed"]);
|
|
719
648
|
}
|
|
649
|
+
return command && control ? { command, id: control.id } : undefined;
|
|
720
650
|
}
|
|
721
651
|
|
|
722
652
|
function finalizeControl(ctx, id, status, error) {
|
|
723
653
|
if (!id) return;
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
const timestamp = new Date().toISOString();
|
|
728
|
-
let changed = false;
|
|
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;
|
|
735
|
-
changed = true;
|
|
736
|
-
}
|
|
737
|
-
if (changed) writeControls(ctx, controls);
|
|
738
|
-
} finally {
|
|
739
|
-
release();
|
|
740
|
-
}
|
|
654
|
+
updateRunControlStatusInStateDir(
|
|
655
|
+
ctx.stateDir, id, status, error ? { error } : {}, ["claimed"],
|
|
656
|
+
);
|
|
741
657
|
}
|
|
742
658
|
|
|
743
659
|
function controlsSignature(ctx) {
|
|
@@ -759,8 +675,7 @@ function startControlLoop(ctx) {
|
|
|
759
675
|
const name = file ? String(file) : "";
|
|
760
676
|
if (
|
|
761
677
|
!name ||
|
|
762
|
-
name === basename(ctx.controlsFile)
|
|
763
|
-
name === basename(runtimeWakeFile(ctx))
|
|
678
|
+
name === basename(ctx.controlsFile)
|
|
764
679
|
) {
|
|
765
680
|
dirty = true;
|
|
766
681
|
}
|
|
@@ -779,14 +694,6 @@ function startControlLoop(ctx) {
|
|
|
779
694
|
const signature = controlsSignature(ctx);
|
|
780
695
|
if (dirty || signature !== lastSignature) {
|
|
781
696
|
dirty = false;
|
|
782
|
-
for (const { command, id } of claimControls(ctx)) {
|
|
783
|
-
try {
|
|
784
|
-
handleControl(ctx, command);
|
|
785
|
-
finalizeControl(ctx, id, "handled");
|
|
786
|
-
} catch (error) {
|
|
787
|
-
finalizeControl(ctx, id, "failed", error.message);
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
697
|
lastSignature = controlsSignature(ctx);
|
|
791
698
|
continue;
|
|
792
699
|
}
|
|
@@ -859,7 +766,6 @@ async function playMain(args) {
|
|
|
859
766
|
const ctx = {
|
|
860
767
|
commandFile: join(stateDir, "command.txt"),
|
|
861
768
|
current: undefined,
|
|
862
|
-
eventFile: join(stateDir, "trace.jsonl"),
|
|
863
769
|
controlsFile: join(stateDir, "controls.jsonl"),
|
|
864
770
|
pidFile: join(stateDir, "current.pid"),
|
|
865
771
|
playerControlFile: join(stateDir, "player-control.txt"),
|
|
@@ -145,13 +145,66 @@ try {
|
|
|
145
145
|
}
|
|
146
146
|
console.log("[release] removed-surface and legacy-fallback allowlists checked");
|
|
147
147
|
|
|
148
|
-
const
|
|
148
|
+
const directTraceWrite = /(?:appendFileSync|writeFileSync|writeText(?:Atomic)?)\s*\(\s*[A-Za-z0-9_.]*?(?:trace|event)(?:Path|File)/iu;
|
|
149
|
+
for (const path of files.filter((candidate) =>
|
|
150
|
+
(candidate.startsWith("lib/") && candidate.endsWith(".ts")) ||
|
|
151
|
+
(candidate.startsWith("scripts/") && candidate.endsWith(".mjs"))
|
|
152
|
+
)) {
|
|
153
|
+
if (path === "lib/runs-trace.ts") continue;
|
|
154
|
+
check(!directTraceWrite.test(stagedText(path) ?? ""), `direct Trace writer outside canonical authority: ${path}`);
|
|
155
|
+
}
|
|
156
|
+
const canonicalTrace = stagedText("lib/runs-trace.ts") ?? "";
|
|
157
|
+
check(/withFileMutationLock\(path/u.test(canonicalTrace), "canonical Trace append lacks mutation lock");
|
|
158
|
+
for (const path of [
|
|
159
|
+
"scripts/async-runner.mjs",
|
|
160
|
+
"scripts/locker.mjs",
|
|
161
|
+
"scripts/music-player.mjs",
|
|
162
|
+
]) {
|
|
163
|
+
const text = stagedText(path) ?? "";
|
|
164
|
+
check(
|
|
165
|
+
text.includes('importRuntimeModule("runs-trace")') && text.includes("appendRunTraceEvent"),
|
|
166
|
+
`first-party Trace writer bypasses canonical runtime module: ${path}`,
|
|
167
|
+
);
|
|
168
|
+
if (path !== "scripts/async-runner.mjs") check(
|
|
169
|
+
text.includes('importRuntimeModule("runs-controls")') &&
|
|
170
|
+
!/function (?:read|write|claimControls|finalizeControls)\b/u.test(text),
|
|
171
|
+
`first-party Control writer bypasses canonical runtime module: ${path}`,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
console.log("[release] canonical Trace and Control writer residue checked");
|
|
175
|
+
|
|
176
|
+
// owner | class | lock | record bound | byte bound | recovery
|
|
177
|
+
const appendOnlyOwners = new Set([
|
|
178
|
+
"lib/runs-trace.ts", // canonical Run evidence | token lock | 2048 | 4 MiB | compact valid suffix
|
|
179
|
+
"lib/runs-retention.ts", // shared kernel evidence | token lock | 256 | 1 MiB | skip malformed
|
|
180
|
+
"scripts/locker.mjs", // first-party actor state | token lock | 512 | 1 MiB | skip malformed
|
|
181
|
+
"lib/command-templates.ts", // complete execution capture | command owner | n/a | artifact policy | preserve
|
|
182
|
+
"scripts/async-runner.mjs", // captures/prompts | Run owner | n/a | artifact policy | preserve
|
|
183
|
+
"scripts/recipe-utils.mjs", // user-declared artifact | caller owner | n/a | caller policy | preserve
|
|
184
|
+
"scripts/release-gates.mjs", // build/test-only source inventory | n/a | n/a | n/a | self-match
|
|
185
|
+
]);
|
|
186
|
+
const appendOnlyPattern = /\bappendFileSync\b|\b(?:writeFileSync|writeTextAtomic)\s*\([^)]*\{\s*flag:\s*["']a["']/su;
|
|
187
|
+
for (const path of files.filter((candidate) => /^(?:lib\/.*\.ts|scripts\/.*\.mjs)$/u.test(candidate)))
|
|
188
|
+
if (appendOnlyPattern.test(stagedText(path) ?? ""))
|
|
189
|
+
check(appendOnlyOwners.has(path), `unregistered shipped append-only writer: ${path}`);
|
|
190
|
+
for (const path of appendOnlyOwners) check(fileSet.has(path), `stale append-only owner inventory: ${path}`);
|
|
191
|
+
console.log(`[release] append-only owner inventory checked: ${appendOnlyOwners.size} source owners`);
|
|
192
|
+
|
|
193
|
+
check(
|
|
194
|
+
(stagedText("scripts/validate-recipe.mjs") ?? "").includes(
|
|
195
|
+
"qaReport.diagnostics.length === 0 && qaReport.warnings.length === 0",
|
|
196
|
+
),
|
|
197
|
+
"Recipe QA warnings are not release-blocking",
|
|
198
|
+
);
|
|
199
|
+
console.log("[release] zero-warning Recipe QA gate checked");
|
|
200
|
+
|
|
201
|
+
const maximumShippedLines = 29_598;
|
|
149
202
|
const shippedPath = /^(?:lib\/|scripts\/|recipes\/|docs\/|skills\/)/u;
|
|
150
203
|
const shippedLines = files
|
|
151
204
|
.filter((path) => shippedPath.test(path))
|
|
152
205
|
.reduce((total, path) => total + (((stagedText(path) ?? "").match(/\n/gu) ?? []).length + 1), 0);
|
|
153
|
-
check(shippedLines
|
|
154
|
-
console.log(`[release] shipped lines ${shippedLines}
|
|
206
|
+
check(shippedLines <= maximumShippedLines, `shipped lines ${shippedLines} exceed release maximum ${maximumShippedLines}`);
|
|
207
|
+
console.log(`[release] shipped lines ${shippedLines} <= release maximum ${maximumShippedLines}`);
|
|
155
208
|
|
|
156
209
|
const sources = files.filter((path) => path === "index.ts" || (path.startsWith("lib/") && path.endsWith(".ts")));
|
|
157
210
|
const sourceSet = new Set(sources);
|
|
@@ -29,7 +29,7 @@ export function validateRecipeUsage() {
|
|
|
29
29
|
return `Usage:
|
|
30
30
|
validate-recipe.mjs <recipe-file-or-dir> [--all] [--qa] [--summary]
|
|
31
31
|
|
|
32
|
-
Validates one template recipe file, or all *.json/*.md files in a directory when --all is set. Add --qa for packaged-recipe quality checks. Add --summary for compact CLI output.`;
|
|
32
|
+
Validates one template recipe file, or all *.json/*.md files in a directory when --all is set. Add --qa for packaged-recipe quality checks, where diagnostics and warnings fail validation. Add --summary for compact CLI output.`;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
function expandPath(value) {
|
|
@@ -120,8 +120,6 @@ function validateHelperPaths(file, config) {
|
|
|
120
120
|
function qaDiagnostics(file, config) {
|
|
121
121
|
const diagnostics = [];
|
|
122
122
|
const warnings = [];
|
|
123
|
-
if (typeof config.description !== "string" || !config.description.trim())
|
|
124
|
-
warnings.push("description: missing or empty");
|
|
125
123
|
if (config.mailbox !== undefined)
|
|
126
124
|
diagnostics.push("recipe.mailbox was removed; use control actions and Trace events");
|
|
127
125
|
diagnostics.push(...validateArtifactDeclarations(config));
|
|
@@ -138,7 +136,7 @@ function qaDiagnostics(file, config) {
|
|
|
138
136
|
}
|
|
139
137
|
|
|
140
138
|
function qaOk(qaReport) {
|
|
141
|
-
return qaReport.diagnostics.length === 0;
|
|
139
|
+
return qaReport.diagnostics.length === 0 && qaReport.warnings.length === 0;
|
|
142
140
|
}
|
|
143
141
|
|
|
144
142
|
function validateFile(file, qa = false) {
|
|
@@ -225,6 +223,9 @@ function summarizeReport(report) {
|
|
|
225
223
|
...(result.qa?.diagnostics?.length
|
|
226
224
|
? { diagnostics: result.qa.diagnostics }
|
|
227
225
|
: {}),
|
|
226
|
+
...(result.qa?.warnings?.length
|
|
227
|
+
? { warnings: result.qa.warnings }
|
|
228
|
+
: {}),
|
|
228
229
|
})),
|
|
229
230
|
};
|
|
230
231
|
}
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: actors
|
|
3
3
|
description: Required practical guide for non-trivial pi-actors use and Run-kernel work. Read before using or changing spawn, message, inspect, Runs, tools, Recipes, command templates, Control, Trace, artifacts, or lifecycle mechanics.
|
|
4
|
-
metadata:
|
|
5
|
-
version: 0.43.0
|
|
6
4
|
---
|
|
7
5
|
|
|
8
6
|
# Actors (pi-actors)
|
|
@@ -38,22 +36,30 @@ Prefer maintained packaged Recipes over ad hoc wrappers. Keep model, thinking, m
|
|
|
38
36
|
Trace records bounded structured observations in `trace.jsonl`:
|
|
39
37
|
|
|
40
38
|
```json
|
|
41
|
-
{
|
|
39
|
+
{
|
|
40
|
+
"id": "…",
|
|
41
|
+
"ts": "…",
|
|
42
|
+
"kind": "progress.update",
|
|
43
|
+
"summary": "…",
|
|
44
|
+
"data": {},
|
|
45
|
+
"level": "info",
|
|
46
|
+
"attention": "notify"
|
|
47
|
+
}
|
|
42
48
|
```
|
|
43
49
|
|
|
44
|
-
Trace never carries sender, recipient, route, reply, or message-envelope fields.
|
|
50
|
+
Trace never carries sender, recipient, route, reply, or message-envelope fields. It is a bounded retained suffix: the canonical lock appends within 2,048 events and 4 MiB or atomically keeps the newest suffix plus one warning-only `runtime.trace_compacted` marker. That marker means older history was discarded; terminal/result/execution/artifact evidence stays independently authoritative. `inspect view=trace` reports completeness. Equal timestamps use same-source physical order, fixed source rank, then stable id without exposing an ordinal or claiming cross-source causality. Attention is a wake hint, not a queue: persist durable state or an artifact first, use `notify` for visible status, and reserve `followup` for needed coordinator context. Compaction may discard old hints.
|
|
45
51
|
|
|
46
52
|
## Control
|
|
47
53
|
|
|
48
54
|
The public Control request is exact:
|
|
49
55
|
|
|
50
56
|
```json
|
|
51
|
-
{"target":"run:<id>","action":"pause","input":{},"verbose":false}
|
|
57
|
+
{ "target": "run:<id>", "action": "pause", "input": {}, "verbose": false }
|
|
52
58
|
```
|
|
53
59
|
|
|
54
|
-
Controls persist in `controls.jsonl` before delivery.
|
|
60
|
+
Valid Controls persist in `controls.jsonl` before delivery; invalid envelopes remain outside it. One token-owned lock rejects a 65th pending Control or 1 MiB rewrite before admission, fails closed on malformed or stale-generation evidence, and atomically admits one queued record. Exact-id claims/finalization preserve a 128-terminal tail, expected-state fencing, and 4 KiB errors. Admitted nonterminal Controls never expire automatically. `inspect view=control` reports capacity, saturation, stale work, bytes, and diagnostics. Endpoints carry immutable startup `run_instance_id`; FIFO and named pipe share limits of 64 action characters, 380 serialized input bytes, and 512 newline-terminated wire bytes. Partial writes fail. Put larger data in an artifact and send only its reference. Delivery revalidates owner, generation, state, and process identity.
|
|
55
61
|
|
|
56
|
-
`kill` remains
|
|
62
|
+
`kill` remains the runtime recovery path for a stuck saturated Run: it bypasses actor-local Control capacity and adds no synthetic Control. Use actor-local `stop` only when declared and implemented. Restart clears generation-local evidence; archive preserves the bounded terminal tree, while prune preserves only requested artifacts.
|
|
57
63
|
|
|
58
64
|
## Run State and Safety
|
|
59
65
|
|
|
@@ -66,6 +72,8 @@ Run state lives under `~/.pi/agent/tmp/pi-actors/runs/<run>/`. Important evidenc
|
|
|
66
72
|
- `execution.json`: command/session provenance and bounded complete-capture references.
|
|
67
73
|
- `result.json`, logs, and declared artifacts.
|
|
68
74
|
|
|
75
|
+
Trace/Control quotas do not constrain user-declared artifacts, repositories, media sources, complete captures, or actor-owned workload state. No public noun, tool, target, or view is added by bounded retention.
|
|
76
|
+
|
|
69
77
|
Never bypass owner filtering, immutable generation fencing, process-identity verification, path containment, redaction, terminal reconciliation, or shutdown kill behavior. Do not edit active Run state to force a result.
|
|
70
78
|
|
|
71
79
|
## Operating Pattern
|
|
@@ -73,8 +81,8 @@ Never bypass owner filtering, immutable generation fencing, process-identity ver
|
|
|
73
81
|
1. Inspect the Recipe before launch when its contract or policy matters.
|
|
74
82
|
2. Spawn with explicit values and retain the returned `run:<id>`.
|
|
75
83
|
3. Let short Runs finish; avoid polling.
|
|
76
|
-
4. Inspect Trace when evidence or attention requires it.
|
|
77
|
-
5.
|
|
84
|
+
4. Inspect Trace when evidence or attention requires it; its summary states whether retained history is complete.
|
|
85
|
+
5. Inspect Control capacity before diagnosing stale work or saturation, then send only declared actor-local Controls.
|
|
78
86
|
6. Use runtime kill/cancel behavior for lifecycle termination.
|
|
79
87
|
7. Inspect artifacts and execution evidence for final validation.
|
|
80
88
|
|
|
@@ -90,9 +98,7 @@ If work may outlive the current turn, needs steering, produces artifacts, fans o
|
|
|
90
98
|
|
|
91
99
|
## Deep References
|
|
92
100
|
|
|
93
|
-
- [Actors deep reference](../../docs/actors-deep-reference.md)
|
|
94
101
|
- [Recipe library](../../docs/recipe-library.md)
|
|
95
102
|
- [Async Runs](../../docs/async-runs.md)
|
|
96
|
-
- [Baseline and preservation gates](../../docs/0.43-baseline.md)
|
|
97
103
|
|
|
98
104
|
Read repository source and tests for exact contracts when changing pi-actors itself. Update this skill whenever durable Run mechanics change.
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: swarm
|
|
3
3
|
description: Subagent and actor orchestration with scoped locks, fanout, and quorum consensus. Use before launching multiple parallel actors or subagents for independent implementation, artifact generation, review, delegated audit, coordinated execution, or any workflow that needs autonomous coordinator decomposition and integration.
|
|
4
|
-
metadata:
|
|
5
|
-
version: 0.43.0
|
|
6
4
|
---
|
|
7
5
|
|
|
8
6
|
# Swarm
|
|
@@ -267,9 +265,9 @@ Report white spots, contradictions, evidence, and risks.
|
|
|
267
265
|
|
|
268
266
|
Detached execution is an adapter concern, not a portable Swarm-script requirement. When the host offers Runs, launch the composed Recipe, return its id, and rely on terminal follow-up rather than blocking or polling.
|
|
269
267
|
|
|
270
|
-
`Progress contract`: expose bounded structured Trace, logs, artifacts, status, timestamps, and final result evidence. Read
|
|
268
|
+
`Progress contract`: expose bounded structured Trace, logs, artifacts, status, timestamps, and final result evidence. Treat Trace as a retained suffix and attention as a wake hint; write durable task cards, checkpoints, and large evidence to artifacts before signaling attention. Read completeness and Control capacity through existing Run inspection rather than scraping output. Artifact size/lifecycle remains separate from Trace/Control quotas.
|
|
271
269
|
|
|
272
|
-
`Resumable checkpoint goal`: a controlled agent-backed Run may preserve context and accept a declared Control. When the host cannot preserve context, write a handoff artifact and launch a clean-context Run while marking the context loss explicitly.
|
|
270
|
+
`Resumable checkpoint goal`: a controlled agent-backed Run may preserve context and accept a declared Control. Control saturation rejects before admission and admitted work does not expire; lifecycle recovery remains host-owned. When the host cannot preserve context, write a handoff artifact and launch a clean-context Run while marking the context loss explicitly.
|
|
273
271
|
|
|
274
272
|
`Cancellation boundary`: terminate only an owned active generation whose process identity the runtime can prove. Stale pid reuse must fail closed.
|
|
275
273
|
|