@llblab/pi-actors 0.43.1 → 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 +6 -6
- package/BACKLOG.md +1 -189
- package/CHANGELOG.md +412 -402
- package/README.md +5 -5
- package/dist/lib/async-runs.js +8 -30
- package/dist/lib/file-state.d.ts +3 -1
- package/dist/lib/file-state.js +25 -7
- package/dist/lib/inspector-overlay.js +46 -30
- package/dist/lib/limits.d.ts +10 -0
- package/dist/lib/limits.js +10 -0
- package/dist/lib/observability.d.ts +4 -2
- package/dist/lib/observability.js +43 -36
- 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-controls.d.ts +7 -5
- package/dist/lib/runs-controls.js +186 -49
- package/dist/lib/runs-retention.js +27 -14
- package/dist/lib/runs-trace.d.ts +25 -1
- package/dist/lib/runs-trace.js +410 -21
- package/dist/lib/runtime-triage.js +6 -22
- package/dist/lib/tools-inspect.js +48 -14
- package/dist/lib/trace-projection.js +90 -44
- package/dist/scripts/conformance.mjs +5 -0
- package/dist/scripts/locker.mjs +31 -74
- package/dist/scripts/music-player.mjs +41 -129
- package/dist/scripts/release-gates.mjs +26 -4
- package/dist/skills/actors/SKILL.md +7 -6
- package/dist/skills/swarm/SKILL.md +2 -2
- package/docs/README.md +0 -1
- package/docs/actor-inspector.md +3 -2
- package/docs/async-runs.md +10 -8
- package/docs/recipe-library.md +2 -2
- package/lib/async-runs.ts +8 -45
- package/lib/file-state.ts +28 -7
- package/lib/inspector-overlay.ts +29 -17
- package/lib/limits.ts +10 -0
- package/lib/observability.ts +55 -57
- package/lib/run-evidence-policy.ts +242 -0
- package/lib/run-ui-runtime.ts +2 -0
- package/lib/runs-controls.ts +177 -108
- package/lib/runs-retention.ts +28 -20
- package/lib/runs-trace.ts +496 -21
- package/lib/runtime-triage.ts +11 -25
- package/lib/tools-inspect.ts +45 -15
- package/lib/trace-projection.ts +137 -75
- package/package.json +1 -1
- package/scripts/conformance.mjs +5 -0
- package/scripts/locker.mjs +31 -74
- package/scripts/music-player.mjs +41 -129
- package/scripts/release-gates.mjs +26 -4
- package/skills/actors/SKILL.md +7 -6
- package/skills/swarm/SKILL.md +2 -2
- package/dist/lib/runtime-notifier.d.ts +0 -48
- package/dist/lib/runtime-notifier.js +0 -138
- package/docs/0.43-baseline.md +0 -39
- package/lib/runtime-notifier.ts +0 -211
package/scripts/music-player.mjs
CHANGED
|
@@ -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,8 @@ 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");
|
|
55
54
|
const { appendRunTraceEvent } = await importRuntimeModule("runs-trace");
|
|
56
55
|
|
|
57
56
|
const AUDIO_EXTENSIONS = new Set([
|
|
@@ -408,8 +407,8 @@ function playerCommand(ctx, player, volume, track) {
|
|
|
408
407
|
}
|
|
409
408
|
}
|
|
410
409
|
|
|
411
|
-
function writeText(path, value
|
|
412
|
-
writeFileSync(path, value,
|
|
410
|
+
function writeText(path, value) {
|
|
411
|
+
writeFileSync(path, value, "utf8");
|
|
413
412
|
}
|
|
414
413
|
|
|
415
414
|
function readText(path) {
|
|
@@ -584,7 +583,24 @@ async function startControlServer(ctx, wakeControlLoop) {
|
|
|
584
583
|
: join(ctx.stateDir, "control.sock");
|
|
585
584
|
if (process.platform !== "win32") rmSync(path, { force: true });
|
|
586
585
|
const server = createServer((socket) => {
|
|
587
|
-
|
|
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
|
+
});
|
|
588
604
|
socket.resume();
|
|
589
605
|
});
|
|
590
606
|
await new Promise((resolveReady, rejectReady) => {
|
|
@@ -606,133 +622,38 @@ async function startControlServer(ctx, wakeControlLoop) {
|
|
|
606
622
|
};
|
|
607
623
|
}
|
|
608
624
|
|
|
609
|
-
function acquireControlsLock(ctx) {
|
|
610
|
-
return acquireFileMutationLock(ctx.controlsFile);
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
function readControls(ctx) {
|
|
614
|
-
if (!exists(ctx.controlsFile)) return [];
|
|
615
|
-
return readFileSync(ctx.controlsFile, "utf8")
|
|
616
|
-
.split("\n")
|
|
617
|
-
.filter((line) => line.trim())
|
|
618
|
-
.map((line) => {
|
|
619
|
-
try {
|
|
620
|
-
return JSON.parse(line);
|
|
621
|
-
} catch {
|
|
622
|
-
return undefined;
|
|
623
|
-
}
|
|
624
|
-
})
|
|
625
|
-
.filter(Boolean);
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
function writeControls(ctx, controls) {
|
|
629
|
-
writeTextAtomic(
|
|
630
|
-
ctx.controlsFile,
|
|
631
|
-
controls.length
|
|
632
|
-
? `${controls.map((control) => JSON.stringify(control)).join("\n")}\n`
|
|
633
|
-
: "",
|
|
634
|
-
);
|
|
635
|
-
}
|
|
636
|
-
|
|
637
625
|
function commandFromControl(control) {
|
|
638
626
|
if (typeof control.action !== "string") return undefined;
|
|
639
627
|
const action = control.action.trim();
|
|
640
628
|
return CONTROL_COMMANDS.has(action) ? action : undefined;
|
|
641
629
|
}
|
|
642
630
|
|
|
643
|
-
function runtimeWakeFile(ctx) {
|
|
644
|
-
return join(ctx.stateDir, "wake.jsonl");
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
function notifyControlWake(ctx, reason = "control.queued") {
|
|
648
|
-
try {
|
|
649
|
-
writeText(
|
|
650
|
-
runtimeWakeFile(ctx),
|
|
651
|
-
`${JSON.stringify({
|
|
652
|
-
actor: `run:${basename(ctx.stateDir)}`,
|
|
653
|
-
id: randomUUID(),
|
|
654
|
-
metadata: { command: "music-player" },
|
|
655
|
-
reason,
|
|
656
|
-
state_dir: ctx.stateDir,
|
|
657
|
-
ts: new Date().toISOString(),
|
|
658
|
-
})}\n`,
|
|
659
|
-
"a",
|
|
660
|
-
);
|
|
661
|
-
} catch {
|
|
662
|
-
// Wake records are advisory; the Control journal remains authoritative.
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
|
|
666
631
|
function appendControl(ctx, action) {
|
|
667
|
-
const
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
action,
|
|
673
|
-
id: randomUUID(),
|
|
674
|
-
queued_at: new Date().toISOString(),
|
|
675
|
-
run_instance_id: run.run_instance_id,
|
|
676
|
-
status: "queued",
|
|
677
|
-
});
|
|
678
|
-
writeControls(ctx, controls);
|
|
679
|
-
} finally {
|
|
680
|
-
release();
|
|
681
|
-
}
|
|
682
|
-
notifyControlWake(ctx);
|
|
632
|
+
const run = readJsonFile(runJsonFile(ctx), {});
|
|
633
|
+
appendRunControlInStateDir(ctx.stateDir, {
|
|
634
|
+
action,
|
|
635
|
+
run_instance_id: run.run_instance_id,
|
|
636
|
+
});
|
|
683
637
|
}
|
|
684
638
|
|
|
685
|
-
function
|
|
686
|
-
const
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
const command =
|
|
695
|
-
control.run_instance_id === ctx.runInstanceId
|
|
696
|
-
? commandFromControl(control)
|
|
697
|
-
: undefined;
|
|
698
|
-
if (!command) {
|
|
699
|
-
control.failed_at = claimedAt;
|
|
700
|
-
control.status = "failed";
|
|
701
|
-
control.error = "Unsupported or stale music-player Control";
|
|
702
|
-
changed = true;
|
|
703
|
-
continue;
|
|
704
|
-
}
|
|
705
|
-
control.claimed_at = claimedAt;
|
|
706
|
-
control.status = "claimed";
|
|
707
|
-
commands.push({ command, id: control.id });
|
|
708
|
-
changed = true;
|
|
709
|
-
}
|
|
710
|
-
if (changed) writeControls(ctx, controls);
|
|
711
|
-
return commands;
|
|
712
|
-
} finally {
|
|
713
|
-
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"]);
|
|
714
648
|
}
|
|
649
|
+
return command && control ? { command, id: control.id } : undefined;
|
|
715
650
|
}
|
|
716
651
|
|
|
717
652
|
function finalizeControl(ctx, id, status, error) {
|
|
718
653
|
if (!id) return;
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
const timestamp = new Date().toISOString();
|
|
723
|
-
let changed = false;
|
|
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;
|
|
730
|
-
changed = true;
|
|
731
|
-
}
|
|
732
|
-
if (changed) writeControls(ctx, controls);
|
|
733
|
-
} finally {
|
|
734
|
-
release();
|
|
735
|
-
}
|
|
654
|
+
updateRunControlStatusInStateDir(
|
|
655
|
+
ctx.stateDir, id, status, error ? { error } : {}, ["claimed"],
|
|
656
|
+
);
|
|
736
657
|
}
|
|
737
658
|
|
|
738
659
|
function controlsSignature(ctx) {
|
|
@@ -754,8 +675,7 @@ function startControlLoop(ctx) {
|
|
|
754
675
|
const name = file ? String(file) : "";
|
|
755
676
|
if (
|
|
756
677
|
!name ||
|
|
757
|
-
name === basename(ctx.controlsFile)
|
|
758
|
-
name === basename(runtimeWakeFile(ctx))
|
|
678
|
+
name === basename(ctx.controlsFile)
|
|
759
679
|
) {
|
|
760
680
|
dirty = true;
|
|
761
681
|
}
|
|
@@ -774,14 +694,6 @@ function startControlLoop(ctx) {
|
|
|
774
694
|
const signature = controlsSignature(ctx);
|
|
775
695
|
if (dirty || signature !== lastSignature) {
|
|
776
696
|
dirty = false;
|
|
777
|
-
for (const { command, id } of claimControls(ctx)) {
|
|
778
|
-
try {
|
|
779
|
-
handleControl(ctx, command);
|
|
780
|
-
finalizeControl(ctx, id, "handled");
|
|
781
|
-
} catch (error) {
|
|
782
|
-
finalizeControl(ctx, id, "failed", error.message);
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
697
|
lastSignature = controlsSignature(ctx);
|
|
786
698
|
continue;
|
|
787
699
|
}
|
|
@@ -165,8 +165,30 @@ try {
|
|
|
165
165
|
text.includes('importRuntimeModule("runs-trace")') && text.includes("appendRunTraceEvent"),
|
|
166
166
|
`first-party Trace writer bypasses canonical runtime module: ${path}`,
|
|
167
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
|
+
);
|
|
168
173
|
}
|
|
169
|
-
console.log("[release] canonical Trace writer residue checked");
|
|
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`);
|
|
170
192
|
|
|
171
193
|
check(
|
|
172
194
|
(stagedText("scripts/validate-recipe.mjs") ?? "").includes(
|
|
@@ -176,13 +198,13 @@ try {
|
|
|
176
198
|
);
|
|
177
199
|
console.log("[release] zero-warning Recipe QA gate checked");
|
|
178
200
|
|
|
179
|
-
const
|
|
201
|
+
const maximumShippedLines = 29_598;
|
|
180
202
|
const shippedPath = /^(?:lib\/|scripts\/|recipes\/|docs\/|skills\/)/u;
|
|
181
203
|
const shippedLines = files
|
|
182
204
|
.filter((path) => shippedPath.test(path))
|
|
183
205
|
.reduce((total, path) => total + (((stagedText(path) ?? "").match(/\n/gu) ?? []).length + 1), 0);
|
|
184
|
-
check(shippedLines
|
|
185
|
-
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}`);
|
|
186
208
|
|
|
187
209
|
const sources = files.filter((path) => path === "index.ts" || (path.startsWith("lib/") && path.endsWith(".ts")));
|
|
188
210
|
const sourceSet = new Set(sources);
|
package/skills/actors/SKILL.md
CHANGED
|
@@ -47,7 +47,7 @@ Trace records bounded structured observations in `trace.jsonl`:
|
|
|
47
47
|
}
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
-
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.
|
|
51
51
|
|
|
52
52
|
## Control
|
|
53
53
|
|
|
@@ -57,9 +57,9 @@ The public Control request is exact:
|
|
|
57
57
|
{ "target": "run:<id>", "action": "pause", "input": {}, "verbose": false }
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
-
Valid Controls persist in `controls.jsonl` before delivery; invalid envelopes remain outside
|
|
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.
|
|
61
61
|
|
|
62
|
-
`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.
|
|
63
63
|
|
|
64
64
|
## Run State and Safety
|
|
65
65
|
|
|
@@ -72,6 +72,8 @@ Run state lives under `~/.pi/agent/tmp/pi-actors/runs/<run>/`. Important evidenc
|
|
|
72
72
|
- `execution.json`: command/session provenance and bounded complete-capture references.
|
|
73
73
|
- `result.json`, logs, and declared artifacts.
|
|
74
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
|
+
|
|
75
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.
|
|
76
78
|
|
|
77
79
|
## Operating Pattern
|
|
@@ -79,8 +81,8 @@ Never bypass owner filtering, immutable generation fencing, process-identity ver
|
|
|
79
81
|
1. Inspect the Recipe before launch when its contract or policy matters.
|
|
80
82
|
2. Spawn with explicit values and retain the returned `run:<id>`.
|
|
81
83
|
3. Let short Runs finish; avoid polling.
|
|
82
|
-
4. Inspect Trace when evidence or attention requires it.
|
|
83
|
-
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.
|
|
84
86
|
6. Use runtime kill/cancel behavior for lifecycle termination.
|
|
85
87
|
7. Inspect artifacts and execution evidence for final validation.
|
|
86
88
|
|
|
@@ -98,6 +100,5 @@ If work may outlive the current turn, needs steering, produces artifacts, fans o
|
|
|
98
100
|
|
|
99
101
|
- [Recipe library](../../docs/recipe-library.md)
|
|
100
102
|
- [Async Runs](../../docs/async-runs.md)
|
|
101
|
-
- [Baseline and preservation gates](../../docs/0.43-baseline.md)
|
|
102
103
|
|
|
103
104
|
Read repository source and tests for exact contracts when changing pi-actors itself. Update this skill whenever durable Run mechanics change.
|
package/skills/swarm/SKILL.md
CHANGED
|
@@ -265,9 +265,9 @@ Report white spots, contradictions, evidence, and risks.
|
|
|
265
265
|
|
|
266
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.
|
|
267
267
|
|
|
268
|
-
`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.
|
|
269
269
|
|
|
270
|
-
`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.
|
|
271
271
|
|
|
272
272
|
`Cancellation boundary`: terminate only an owned active generation whose process identity the runtime can prove. Stale pid reuse must fail closed.
|
|
273
273
|
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Runtime wake notifications for actor state.
|
|
3
|
-
* Zones: advisory wake layer, file-backed runtime state, cross-platform notification boundary
|
|
4
|
-
* Owns best-effort live wake signals while durable Run state remains canonical.
|
|
5
|
-
*/
|
|
6
|
-
export interface RuntimeWakeEvent {
|
|
7
|
-
actor: string;
|
|
8
|
-
id: string;
|
|
9
|
-
metadata?: Record<string, unknown>;
|
|
10
|
-
reason: string;
|
|
11
|
-
state_dir: string;
|
|
12
|
-
ts: string;
|
|
13
|
-
}
|
|
14
|
-
export interface RuntimeNotifierSubscription {
|
|
15
|
-
close(): void;
|
|
16
|
-
}
|
|
17
|
-
export type RuntimeReconcileReason = "initial" | "poll" | "wake";
|
|
18
|
-
export interface RuntimeReconcileEvent {
|
|
19
|
-
actor: string;
|
|
20
|
-
reason: RuntimeReconcileReason;
|
|
21
|
-
state_dir: string;
|
|
22
|
-
ts: string;
|
|
23
|
-
}
|
|
24
|
-
export interface RuntimeNotifierSubscribeOptions {
|
|
25
|
-
onReconcile?: (event: RuntimeReconcileEvent) => void;
|
|
26
|
-
}
|
|
27
|
-
export interface FileRuntimeNotifierOptions {
|
|
28
|
-
pollIntervalMs?: number;
|
|
29
|
-
replay?: boolean;
|
|
30
|
-
watch?: boolean;
|
|
31
|
-
}
|
|
32
|
-
export interface RuntimeNotifier {
|
|
33
|
-
notify(event: {
|
|
34
|
-
actor: string;
|
|
35
|
-
metadata?: Record<string, unknown>;
|
|
36
|
-
reason: string;
|
|
37
|
-
}): RuntimeWakeEvent;
|
|
38
|
-
subscribe(actor: string, onWake: (event: RuntimeWakeEvent) => void, options?: RuntimeNotifierSubscribeOptions): RuntimeNotifierSubscription;
|
|
39
|
-
}
|
|
40
|
-
export declare function runtimeWakeFile(stateDir: string): string;
|
|
41
|
-
export declare function notifyRuntimeWake(stateDir: string, event: {
|
|
42
|
-
actor: string;
|
|
43
|
-
metadata?: Record<string, unknown>;
|
|
44
|
-
reason: string;
|
|
45
|
-
}): RuntimeWakeEvent;
|
|
46
|
-
export declare function parseRuntimeWakeEventLine(line: string): RuntimeWakeEvent | undefined;
|
|
47
|
-
export declare function readRuntimeWakeEvents(stateDir: string): RuntimeWakeEvent[];
|
|
48
|
-
export declare function createFileRuntimeNotifier(stateDir: string, options?: FileRuntimeNotifierOptions): RuntimeNotifier;
|
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Runtime wake notifications for actor state.
|
|
3
|
-
* Zones: advisory wake layer, file-backed runtime state, cross-platform notification boundary
|
|
4
|
-
* Owns best-effort live wake signals while durable Run state remains canonical.
|
|
5
|
-
*/
|
|
6
|
-
import { randomUUID } from "node:crypto";
|
|
7
|
-
import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync, watch, } from "node:fs";
|
|
8
|
-
import { basename, dirname, join } from "node:path";
|
|
9
|
-
import { readJsonlFileResilient } from "./state-readers.js";
|
|
10
|
-
const DEFAULT_POLL_INTERVAL_MS = 1000;
|
|
11
|
-
export function runtimeWakeFile(stateDir) {
|
|
12
|
-
return join(stateDir, "wake.jsonl");
|
|
13
|
-
}
|
|
14
|
-
function normalizeWakeEvent(stateDir, event) {
|
|
15
|
-
const actor = event.actor.trim();
|
|
16
|
-
const reason = event.reason.trim();
|
|
17
|
-
if (!actor)
|
|
18
|
-
throw new Error("Runtime wake event requires actor.");
|
|
19
|
-
if (!reason)
|
|
20
|
-
throw new Error("Runtime wake event requires reason.");
|
|
21
|
-
return {
|
|
22
|
-
actor,
|
|
23
|
-
id: randomUUID(),
|
|
24
|
-
...(event.metadata ? { metadata: event.metadata } : {}),
|
|
25
|
-
reason,
|
|
26
|
-
state_dir: stateDir,
|
|
27
|
-
ts: new Date().toISOString(),
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
export function notifyRuntimeWake(stateDir, event) {
|
|
31
|
-
const normalized = normalizeWakeEvent(stateDir, event);
|
|
32
|
-
const file = runtimeWakeFile(stateDir);
|
|
33
|
-
mkdirSync(dirname(file), { recursive: true });
|
|
34
|
-
appendFileSync(file, `${JSON.stringify(normalized)}\n`, "utf8");
|
|
35
|
-
return normalized;
|
|
36
|
-
}
|
|
37
|
-
export function parseRuntimeWakeEventLine(line) {
|
|
38
|
-
try {
|
|
39
|
-
const record = JSON.parse(line);
|
|
40
|
-
if (typeof record.actor !== "string" ||
|
|
41
|
-
typeof record.id !== "string" ||
|
|
42
|
-
typeof record.reason !== "string" ||
|
|
43
|
-
typeof record.state_dir !== "string" ||
|
|
44
|
-
typeof record.ts !== "string") {
|
|
45
|
-
return undefined;
|
|
46
|
-
}
|
|
47
|
-
return {
|
|
48
|
-
actor: record.actor,
|
|
49
|
-
id: record.id,
|
|
50
|
-
...(record.metadata &&
|
|
51
|
-
typeof record.metadata === "object" &&
|
|
52
|
-
!Array.isArray(record.metadata)
|
|
53
|
-
? { metadata: record.metadata }
|
|
54
|
-
: {}),
|
|
55
|
-
reason: record.reason,
|
|
56
|
-
state_dir: record.state_dir,
|
|
57
|
-
ts: record.ts,
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
catch {
|
|
61
|
-
return undefined;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
export function readRuntimeWakeEvents(stateDir) {
|
|
65
|
-
return readJsonlFileResilient(runtimeWakeFile(stateDir))
|
|
66
|
-
.records.map((record) => parseRuntimeWakeEventLine(JSON.stringify(record)))
|
|
67
|
-
.filter((event) => Boolean(event));
|
|
68
|
-
}
|
|
69
|
-
export function createFileRuntimeNotifier(stateDir, options = {}) {
|
|
70
|
-
const file = runtimeWakeFile(stateDir);
|
|
71
|
-
const pollIntervalMs = Math.max(25, Number(options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS));
|
|
72
|
-
return {
|
|
73
|
-
notify: (event) => notifyRuntimeWake(stateDir, event),
|
|
74
|
-
subscribe: (actor, onWake, subscribeOptions = {}) => {
|
|
75
|
-
mkdirSync(dirname(file), { recursive: true });
|
|
76
|
-
let position = options.replay || !existsSync(file) ? 0 : statSync(file).size;
|
|
77
|
-
let pending = "";
|
|
78
|
-
let closed = false;
|
|
79
|
-
const reconcile = (reason) => {
|
|
80
|
-
if (closed)
|
|
81
|
-
return;
|
|
82
|
-
subscribeOptions.onReconcile?.({
|
|
83
|
-
actor,
|
|
84
|
-
reason,
|
|
85
|
-
state_dir: stateDir,
|
|
86
|
-
ts: new Date().toISOString(),
|
|
87
|
-
});
|
|
88
|
-
};
|
|
89
|
-
const drain = () => {
|
|
90
|
-
if (closed || !existsSync(file))
|
|
91
|
-
return;
|
|
92
|
-
const buffer = readFileSync(file);
|
|
93
|
-
if (position > buffer.length) {
|
|
94
|
-
position = 0;
|
|
95
|
-
pending = "";
|
|
96
|
-
}
|
|
97
|
-
const chunk = pending + buffer.subarray(position).toString("utf8");
|
|
98
|
-
position = buffer.length;
|
|
99
|
-
const lines = chunk.split("\n");
|
|
100
|
-
pending = chunk.endsWith("\n") ? "" : (lines.pop() ?? "");
|
|
101
|
-
for (const line of lines) {
|
|
102
|
-
if (!line.trim())
|
|
103
|
-
continue;
|
|
104
|
-
const event = parseRuntimeWakeEventLine(line);
|
|
105
|
-
if (event && event.actor === actor) {
|
|
106
|
-
onWake(event);
|
|
107
|
-
reconcile("wake");
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
};
|
|
111
|
-
let watcher;
|
|
112
|
-
if (options.watch !== false) {
|
|
113
|
-
try {
|
|
114
|
-
watcher = watch(dirname(file), { persistent: false }, (_eventType, changedFile) => {
|
|
115
|
-
if (!changedFile || String(changedFile) === basename(file))
|
|
116
|
-
drain();
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
catch {
|
|
120
|
-
// fs.watch availability varies by platform/filesystem; polling below is the fallback.
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
reconcile("initial");
|
|
124
|
-
const timer = setInterval(() => {
|
|
125
|
-
drain();
|
|
126
|
-
reconcile("poll");
|
|
127
|
-
}, pollIntervalMs);
|
|
128
|
-
timer.unref?.();
|
|
129
|
-
return {
|
|
130
|
-
close: () => {
|
|
131
|
-
closed = true;
|
|
132
|
-
clearInterval(timer);
|
|
133
|
-
watcher?.close();
|
|
134
|
-
},
|
|
135
|
-
};
|
|
136
|
-
},
|
|
137
|
-
};
|
|
138
|
-
}
|
package/docs/0.43-baseline.md
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
# Released 0.43 Baseline
|
|
2
|
-
|
|
3
|
-
Release `0.43.0` is frozen at commit `0d6db30cd2e070c1d03ed1e60bef70538a3083c1`. Local and remote `main`, immutable tag `v0.43.0`, the GitHub Release, and `@llblab/pi-actors@0.43.0` all resolve to that commit. The npm artifact records the same `gitHead`, shasum `64b5982bc1cda5723ce2f80f4ed15476b6016d62`, and `latest` dist-tag.
|
|
4
|
-
|
|
5
|
-
The continuing `dev` line descends from release parent `f4e4e78891e0c3d570b31c35572aeaa226af6f91`, whose tree exactly matches the tagged merge tree. This preserves content equivalence without copying the content-neutral merge wrapper into `dev`.
|
|
6
|
-
|
|
7
|
-
## Reproduced Evidence
|
|
8
|
-
|
|
9
|
-
An isolated detached `v0.43.0` worktree produced this evidence on 2026-08-11:
|
|
10
|
-
|
|
11
|
-
- `npm ci` succeeded with 147 installed packages. Its generic audit summary included three peer-tree findings; the package-owned `--omit=peer` audit passed with zero vulnerabilities.
|
|
12
|
-
- `npm run test:preservation` passed 91 of 91 tests.
|
|
13
|
-
- `npm run release:validate` passed 525 tests with 5 platform skips, 106 conformance tests, 58 Recipe QA files, package dry-run, removed-surface checks, strict Domain DAG, and ABCd context validation.
|
|
14
|
-
- GitHub reported no open issue or pull request requiring post-release scope changes.
|
|
15
|
-
|
|
16
|
-
## Shipped-Line Ratchet
|
|
17
|
-
|
|
18
|
-
The released tree contains exactly **28,853** lines under the surfaces measured by `scripts/release-gates.mjs`:
|
|
19
|
-
|
|
20
|
-
- `lib/`
|
|
21
|
-
- `scripts/`
|
|
22
|
-
- `recipes/`
|
|
23
|
-
- `docs/`
|
|
24
|
-
- `skills/`
|
|
25
|
-
|
|
26
|
-
Release validation requires every retained post-`0.43.0` tree to remain strictly below 28,853 lines. Tests, fixtures, and workflows remain outside this existing metric. The ratchet prevents deleted communication-plane code from funding replacement bloat; it does not grant removed behavior preservation status.
|
|
27
|
-
|
|
28
|
-
## Retained Invariants
|
|
29
|
-
|
|
30
|
-
The preservation suite covers owner-filtered Run discovery, immutable generation fencing, process identity, lifecycle locking, shutdown and parent teardown, terminal reconciliation, bounded complete captures, Pi session provenance, path containment, redaction, review recovery, generation-bound Control, and canonical Trace. Rooms, routing, addressed messages, mailboxes, and communication topology are not retained.
|
|
31
|
-
|
|
32
|
-
## Validation
|
|
33
|
-
|
|
34
|
-
```bash
|
|
35
|
-
npm run test:preservation
|
|
36
|
-
npm run release:validate
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
Release gates also reject removed-surface residue, Domain DAG violations, ABCd context drift, stale package contents, and shipped-line growth at or above the released baseline.
|