@llblab/pi-actors 0.52.0 → 0.53.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 +2 -1
- package/CHANGELOG.md +12 -0
- package/README.md +1 -1
- package/dist/lib/async-runs.d.ts +3 -0
- package/dist/lib/async-runs.js +14 -1
- package/dist/lib/command-templates.js +45 -3
- package/dist/lib/extension-runtime.js +1 -1
- package/dist/lib/observability.d.ts +16 -3
- package/dist/lib/observability.js +92 -7
- package/dist/lib/pi.d.ts +0 -1
- package/dist/lib/pi.js +15 -24
- package/dist/lib/run-delivery-lineage.d.ts +17 -0
- package/dist/lib/run-delivery-lineage.js +44 -0
- package/dist/lib/run-delivery.d.ts +4 -0
- package/dist/lib/run-delivery.js +102 -4
- package/dist/lib/run-ui-runtime.js +58 -39
- package/dist/lib/runtime.js +14 -6
- package/dist/skills/actors/SKILL.md +17 -7
- package/dist/skills/music-player/SKILL.md +3 -3
- package/dist/skills/music-player/genapps/music-player.mjs +6 -4
- package/dist/skills/music-player/scripts/playback.mjs +85 -18
- package/dist/skills/swarm/SKILL.md +2 -6
- package/dist/skills/swarm/references/development-swarm.md +2 -31
- package/docs/async-runs.md +1 -1
- package/docs/coordinator-delivery.md +18 -23
- package/docs/recipe-library.md +1 -1
- package/lib/async-runs.ts +18 -1
- package/lib/command-templates.ts +41 -3
- package/lib/extension-runtime.ts +1 -1
- package/lib/observability.ts +119 -5
- package/lib/pi.ts +15 -28
- package/lib/run-delivery-lineage.ts +68 -0
- package/lib/run-delivery.ts +120 -4
- package/lib/run-ui-runtime.ts +69 -44
- package/lib/runtime.ts +17 -6
- package/package.json +1 -1
- package/skills/actors/SKILL.md +17 -7
- package/skills/music-player/SKILL.md +3 -3
- package/skills/music-player/genapps/music-player.mjs +6 -4
- package/skills/music-player/scripts/playback.mjs +85 -18
- package/skills/swarm/SKILL.md +2 -6
- package/skills/swarm/references/development-swarm.md +2 -31
- package/dist/skills/music-player/scripts/playback-client.mjs +0 -143
- package/skills/music-player/scripts/playback-client.mjs +0 -143
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
watch,
|
|
28
28
|
writeFileSync,
|
|
29
29
|
} from "node:fs";
|
|
30
|
-
import { createServer } from "node:net";
|
|
30
|
+
import { createConnection, createServer } from "node:net";
|
|
31
31
|
import { homedir } from "node:os";
|
|
32
32
|
import {
|
|
33
33
|
basename,
|
|
@@ -66,7 +66,7 @@ let updateRunControlStatusInStateDir;
|
|
|
66
66
|
let isAlive;
|
|
67
67
|
let verifyRunProcessIdentity;
|
|
68
68
|
let appendRunTraceEvent = () => {};
|
|
69
|
-
|
|
69
|
+
async function loadActorAdapter() {
|
|
70
70
|
({
|
|
71
71
|
appendRunControlInStateDir,
|
|
72
72
|
claimRunControlByIdInStateDir,
|
|
@@ -110,8 +110,9 @@ function usage() {
|
|
|
110
110
|
playback.mjs control <state-dir> <play|pause|toggle|next|previous|seek|volume|stop|status> [percent]
|
|
111
111
|
|
|
112
112
|
Runs a foreground music player so pi-actors can own it as a controlled Run.
|
|
113
|
-
|
|
114
|
-
|
|
113
|
+
Actor-owned controls use canonical records in <state-dir>/controls.jsonl.
|
|
114
|
+
Standalone controls use the generation-fenced playback service endpoint.
|
|
115
|
+
Prefer message target=run:<run> action=<command> for Actors; external adapters use control <state-dir> <action>.
|
|
115
116
|
Supported players: auto, mpv, afplay, ffplay, cvlc, play, wmp.
|
|
116
117
|
`);
|
|
117
118
|
}
|
|
@@ -1134,6 +1135,7 @@ function readAndClearCommand(ctx) {
|
|
|
1134
1135
|
}
|
|
1135
1136
|
|
|
1136
1137
|
async function playMain(args) {
|
|
1138
|
+
if (actorAdapterEnabled) await loadActorAdapter();
|
|
1137
1139
|
const [
|
|
1138
1140
|
sourceArg,
|
|
1139
1141
|
loopArg = "true",
|
|
@@ -1333,30 +1335,74 @@ function projectCurrentProgress(status, nowMs = Date.now()) {
|
|
|
1333
1335
|
};
|
|
1334
1336
|
}
|
|
1335
1337
|
|
|
1336
|
-
function actorControlAvailability(stateDir) {
|
|
1338
|
+
async function actorControlAvailability(stateDir) {
|
|
1337
1339
|
const run = readJsonFile(join(stateDir, "run.json"), {});
|
|
1338
1340
|
const result = readJsonFile(join(stateDir, "result.json"), {});
|
|
1339
1341
|
const endpoint = readJsonFile(join(stateDir, "control-endpoint.json"), {});
|
|
1340
1342
|
const playerStatus = readJsonFile(join(stateDir, "player.json"), {});
|
|
1343
|
+
const runInstanceId = typeof run.run_instance_id === "string"
|
|
1344
|
+
? run.run_instance_id
|
|
1345
|
+
: undefined;
|
|
1346
|
+
// Inactive status must remain readable without an installed Actor runtime,
|
|
1347
|
+
// including standalone state beside metadata from a rejected Actor launch.
|
|
1348
|
+
if (!runInstanceId || typeof result.completedAt === "string" ||
|
|
1349
|
+
endpoint.run_instance_id !== runInstanceId ||
|
|
1350
|
+
!["playing", "paused"].includes(playerStatus.state)) {
|
|
1351
|
+
return { available: false, runInstanceId };
|
|
1352
|
+
}
|
|
1353
|
+
if (!verifyRunProcessIdentity) {
|
|
1354
|
+
await loadActorAdapter();
|
|
1355
|
+
// Re-read authority after the asynchronous import before admitting control.
|
|
1356
|
+
return actorControlAvailability(stateDir);
|
|
1357
|
+
}
|
|
1341
1358
|
const pid = Number(run.pid || 0);
|
|
1342
1359
|
const hasProcessIdentity = pid > 0 || run.process_identity !== undefined;
|
|
1343
1360
|
const processIdentity = pid > 0
|
|
1344
1361
|
? verifyRunProcessIdentity(pid, run.process_identity)
|
|
1345
1362
|
: { valid: false };
|
|
1346
|
-
const available =
|
|
1347
|
-
typeof run.run_instance_id === "string" &&
|
|
1348
|
-
typeof result.completedAt !== "string" &&
|
|
1349
|
-
(!hasProcessIdentity || (pid > 0 && isAlive(pid) && processIdentity.valid === true)) &&
|
|
1350
|
-
endpoint.run_instance_id === run.run_instance_id &&
|
|
1351
|
-
["playing", "paused"].includes(playerStatus.state);
|
|
1352
1363
|
return {
|
|
1353
|
-
available
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
: undefined,
|
|
1364
|
+
available: !hasProcessIdentity ||
|
|
1365
|
+
(pid > 0 && isAlive(pid) && processIdentity.valid === true),
|
|
1366
|
+
runInstanceId,
|
|
1357
1367
|
};
|
|
1358
1368
|
}
|
|
1359
1369
|
|
|
1370
|
+
async function sendPlaybackCommand(endpoint, action, input) {
|
|
1371
|
+
const payload = `${JSON.stringify({
|
|
1372
|
+
action,
|
|
1373
|
+
...(input !== undefined ? { input } : {}),
|
|
1374
|
+
service_instance_id: endpoint.service_instance_id,
|
|
1375
|
+
})}\n`;
|
|
1376
|
+
const response = await new Promise((resolveResponse, rejectResponse) => {
|
|
1377
|
+
const socket = createConnection(endpoint.path);
|
|
1378
|
+
let content = "";
|
|
1379
|
+
const timeout = setTimeout(() => {
|
|
1380
|
+
socket.destroy(new Error("playback service command timed out"));
|
|
1381
|
+
}, 5_000);
|
|
1382
|
+
timeout.unref?.();
|
|
1383
|
+
socket.setEncoding("utf8");
|
|
1384
|
+
socket.on("connect", () => socket.write(payload));
|
|
1385
|
+
socket.on("data", (chunk) => {
|
|
1386
|
+
content += chunk;
|
|
1387
|
+
if (Buffer.byteLength(content, "utf8") > 4096) {
|
|
1388
|
+
socket.destroy(new Error("playback service response is too large"));
|
|
1389
|
+
}
|
|
1390
|
+
});
|
|
1391
|
+
socket.on("close", () => clearTimeout(timeout));
|
|
1392
|
+
socket.on("error", rejectResponse);
|
|
1393
|
+
socket.on("end", () => {
|
|
1394
|
+
try {
|
|
1395
|
+
resolveResponse(JSON.parse(content.trim()));
|
|
1396
|
+
} catch {
|
|
1397
|
+
rejectResponse(new Error("playback service returned invalid JSON"));
|
|
1398
|
+
}
|
|
1399
|
+
});
|
|
1400
|
+
});
|
|
1401
|
+
if (response?.ok !== true) {
|
|
1402
|
+
throw new Error(response?.error || "playback service rejected the command");
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1360
1406
|
async function controlMain(args) {
|
|
1361
1407
|
const stateDir = expandPath(args[0] || "");
|
|
1362
1408
|
const command = args[1] || "status";
|
|
@@ -1365,13 +1411,20 @@ async function controlMain(args) {
|
|
|
1365
1411
|
usage();
|
|
1366
1412
|
process.exit(2);
|
|
1367
1413
|
}
|
|
1368
|
-
|
|
1414
|
+
if (!CONTROL_COMMANDS.has(command)) fail(`unsupported command: ${command}`, 2);
|
|
1415
|
+
const endpoint = readJsonFile(join(stateDir, "playback-endpoint.json"), {});
|
|
1416
|
+
// A standalone service retains authority even if an unsuccessful Actor launch
|
|
1417
|
+
// left Run metadata beside its endpoint. Clients never start or adopt it.
|
|
1418
|
+
const actorOwned = endpoint.owner_mode !== "standalone" &&
|
|
1419
|
+
existsSync(join(stateDir, "run.json"));
|
|
1369
1420
|
if (command === "status") {
|
|
1370
1421
|
const statusFile = join(stateDir, "player.json");
|
|
1371
1422
|
const status = exists(statusFile)
|
|
1372
1423
|
? readJsonFile(statusFile, { state: "unknown" })
|
|
1373
1424
|
: { state: "unknown" };
|
|
1374
|
-
const actor =
|
|
1425
|
+
const actor = actorOwned
|
|
1426
|
+
? await actorControlAvailability(stateDir)
|
|
1427
|
+
: { available: false };
|
|
1375
1428
|
process.stdout.write(`${JSON.stringify({
|
|
1376
1429
|
...projectCurrentProgress(status),
|
|
1377
1430
|
actor_available: actor.available,
|
|
@@ -1381,7 +1434,7 @@ async function controlMain(args) {
|
|
|
1381
1434
|
})}\n`);
|
|
1382
1435
|
return;
|
|
1383
1436
|
}
|
|
1384
|
-
if (!actorControlAvailability(stateDir).available) {
|
|
1437
|
+
if (actorOwned && !(await actorControlAvailability(stateDir)).available) {
|
|
1385
1438
|
fail(`Run playback is not active: ${stateDir}`, 3);
|
|
1386
1439
|
}
|
|
1387
1440
|
let input;
|
|
@@ -1396,6 +1449,20 @@ async function controlMain(args) {
|
|
|
1396
1449
|
fail(error instanceof Error ? error.message : String(error), 2);
|
|
1397
1450
|
}
|
|
1398
1451
|
}
|
|
1452
|
+
if (!actorOwned) {
|
|
1453
|
+
if (endpoint.owner_mode !== "standalone" ||
|
|
1454
|
+
typeof endpoint.path !== "string" || !endpoint.path ||
|
|
1455
|
+
typeof endpoint.service_instance_id !== "string" || !endpoint.service_instance_id) {
|
|
1456
|
+
fail(`standalone playback service is not active: ${stateDir}`, 3);
|
|
1457
|
+
}
|
|
1458
|
+
try {
|
|
1459
|
+
await sendPlaybackCommand(endpoint, command === "resume" ? "play" : command, input);
|
|
1460
|
+
console.log(`music-player: command=${command} handled state_dir=${stateDir}`);
|
|
1461
|
+
return;
|
|
1462
|
+
} catch (error) {
|
|
1463
|
+
fail(error instanceof Error ? error.message : String(error), 3);
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1399
1466
|
const queued = appendControl(
|
|
1400
1467
|
{ controlsFile: join(stateDir, "controls.jsonl"), stateDir },
|
|
1401
1468
|
command,
|
|
@@ -9,13 +9,9 @@ Use multi-actor execution only when at least two scopes or evidence lenses are m
|
|
|
9
9
|
|
|
10
10
|
Read `actors` first for generic Recipe, spawn, Run, Trace, Control, artifact, and lifecycle operation. This Skill owns only multi-actor methodology: decomposition, scope ownership, independence, synthesis, integration, and completion proof.
|
|
11
11
|
|
|
12
|
-
## Coordinator
|
|
12
|
+
## Coordinator and participants
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
This resembles gateway orchestration in dependency direction but not in ownership: the coordinator is itself an agent instance with inspectable Runs, not an infrastructure service that implicitly creates sessions. Preserve that distinction in prompts, docs, recovery, and target routing.
|
|
17
|
-
|
|
18
|
-
Once work is delegated, keep the coordinator available for decisions and integration instead of duplicating participant implementation. Wait for the settled completion batch by default; use meaningful attention or evidence-based timers for overdue work rather than a tight inspection loop.
|
|
14
|
+
The coordinator owns decomposition, shared contracts, integration order, and final validation. Participants own bounded tasks or evidence lenses. Keep the coordinator available for decisions instead of duplicating participant implementation; use `actors` for launch, observation, and lifecycle mechanics.
|
|
19
15
|
|
|
20
16
|
## Reasoning allocation
|
|
21
17
|
|
|
@@ -14,38 +14,9 @@ Use a development swarm only when all are true:
|
|
|
14
14
|
|
|
15
15
|
Do not parallelize implementation when tasks need the same central files, semantic ordering dominates wall-clock time, or the likely conflicts would invalidate the decomposition. Use planning or review first.
|
|
16
16
|
|
|
17
|
-
##
|
|
17
|
+
## Roles and reasoning
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
The coordinator should:
|
|
22
|
-
|
|
23
|
-
- Translate high-level intent into bounded task cards and dependency edges.
|
|
24
|
-
- Keep user authority, shared contracts, integration order, and final validation local.
|
|
25
|
-
- Remain available for checkpoints, permissions, conflicts, and changing evidence.
|
|
26
|
-
- Consume terminal handoffs, durable artifacts, and Trace attention instead of mirroring participant work.
|
|
27
|
-
- Check overdue work on an evidence-based timer; never replace event-driven completion with a rapid polling loop.
|
|
28
|
-
|
|
29
|
-
A participant should:
|
|
30
|
-
|
|
31
|
-
- Own one concrete execution or evidence boundary.
|
|
32
|
-
- Avoid global orchestration and undeclared participant creation.
|
|
33
|
-
- Return a bounded handoff that lets the coordinator decide without replaying the entire task.
|
|
34
|
-
|
|
35
|
-
Use one ordinary Run under `actors` when only one worker is delegated. Activate this development-swarm protocol when two or more participants, parallel ownership, or explicit integration edges exist. Keep trivial single-boundary work inline when delegation overhead has no compensating value.
|
|
36
|
-
|
|
37
|
-
## Reasoning profiles
|
|
38
|
-
|
|
39
|
-
| Role | Default | Raise or fan out when |
|
|
40
|
-
| --- | --- | --- |
|
|
41
|
-
| Bounded implementation author | Reasoning off | The card explicitly owns unresolved diagnosis or design judgement |
|
|
42
|
-
| Reviewer | Medium reasoning, clean context | Stakes require independent lenses or repeated judges |
|
|
43
|
-
| Synthesizer / integrator | Medium reasoning | Evidence conflicts, shared contracts move, or merge order is semantic |
|
|
44
|
-
| Coordinator | Sufficient for decomposition and decisions | Scope, authority, or architecture remains unresolved |
|
|
45
|
-
|
|
46
|
-
Prefer independent review after implementation over asking one author thread to implement, retain all local assumptions, and then certify itself. When risk justifies the cost, use multiple independent reviewers: different lenses increase breadth, while repeated judges increase confidence. Preserve minority high-impact findings and merge only evidence-backed conclusions.
|
|
47
|
-
|
|
48
|
-
More reviewers are not automatically better. Do not fan out when they would inspect unstable code, share contaminated context, repeat one unsupported claim, or exceed the value of the decision. Never change an already-running participant solely to enforce a newer profile; add a fresh review boundary if evidence remains open.
|
|
19
|
+
Apply [Swarm's coordinator and reasoning contract](../SKILL.md#reasoning-allocation). Task cards record those profiles and the owned execution boundary; participants return evidence without undeclared orchestration. Do not fan out review over unstable code or contaminated context. The sections below specify development-only task cards, ownership transfers, conflict reports, and integration.
|
|
49
20
|
|
|
50
21
|
## Decompose by ownership
|
|
51
22
|
|
package/docs/async-runs.md
CHANGED
|
@@ -110,7 +110,7 @@ Statuses include `running`, `done`, `failed`, `exited`, `cancelled`, and `killed
|
|
|
110
110
|
|
|
111
111
|
Ambient observation detects root terminal transitions and explicit retained Trace attention. Terminal transitions reconcile before semantic attention. Canonical attention is an in-memory wake hint, not a durable queue: observers prime retained ids at startup, deliver each later retained unseen id once, and bound memory to the current retained set across compaction. Persist durable recovery state or an artifact before emitting attention; compaction may discard older hints and its marker makes that history loss explicit. Terminal follow-up delivery persists handled/failure evidence so reloads retry unhandled transitions without duplicating completed notifications.
|
|
112
112
|
|
|
113
|
-
Ordinary finite Runs project
|
|
113
|
+
Ordinary finite Runs project terminal results through one root-coordinator completion scheduler. Every detached runner passes its delivery-root owner and exact parent Run generation to nested actor processes. A descendant terminal remains authoritative but ineligible until its containing top-level Run is terminal; descendant Pi sessions never schedule completion follow-ups. After `agent_settled`, session recovery, or an idle debounce, the root scheduler snapshots the ready trees plus concurrently completed top-level Runs into one owner-fenced immutable batch of at most 256 exact generations. One batch causes one automatic agent turn, renders one visible gray operator card, exposes at most 64 bounded parent-child rows with semantic outputs to the model, and marks member terminals handled after Pi durably accepts the exact batch. Pending send failures retain bounded retry evidence. The narrow crash boundary between acceptance and finalization remains recoverable from bounded active-session evidence, but normal hot delivery never waits on a later context callback and therefore cannot block subsequent epochs. Duplicate exact context envelopes collapse before presentation.
|
|
114
114
|
|
|
115
115
|
Sequence, parallel, repeat, and imported branches are internal execution topology and never own branch-level turns. Each separately launched Run owns its own generation and terminal lifecycle; compatible singleton reuse is not a new launch. Explicit semantic attention may intentionally add a checkpoint turn; Runs marked silent and synchronously acknowledged stop outcomes suppress automatic projection. Large semantic results stay outside compact completion rows and remain available in structured details, execution captures, or artifacts.
|
|
116
116
|
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
# Coordinator Delivery Scheduler
|
|
2
2
|
|
|
3
|
-
Status:
|
|
3
|
+
Status: Implemented coordinator-delivery contract.
|
|
4
4
|
|
|
5
5
|
## Goal
|
|
6
6
|
|
|
7
7
|
Separate durable Run completion truth from the scheduling of model turns:
|
|
8
8
|
|
|
9
9
|
```text
|
|
10
|
-
one Run generation -> one
|
|
11
|
-
one
|
|
10
|
+
one Run generation -> one terminal record with inherited tree lineage
|
|
11
|
+
one ready completion forest per bounded epoch -> one root-coordinator turn
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
Ordinary
|
|
14
|
+
Ordinary terminals accumulate across nested actor sessions while Pi is active. Descendants wait for their containing top-level Run, then all ready trees and concurrently completed top-level Runs reach only the root coordinator in one bounded batch after Pi settles. Only an explicitly actor-authored urgent semantic checkpoint may steer an active agent loop.
|
|
15
15
|
|
|
16
16
|
## Non-Goals
|
|
17
17
|
|
|
@@ -27,7 +27,7 @@ Ordinary root terminals accumulate while Pi is active and reach the coordinator
|
|
|
27
27
|
- `attention: "notify"` remains visible UI status without a model turn.
|
|
28
28
|
- `attention: "followup"` retains its existing explicit semantic follow-up behavior.
|
|
29
29
|
- New `attention: "steer"` requests urgent semantic delivery at Pi's next safe assistant/tool boundary.
|
|
30
|
-
-
|
|
30
|
+
- Terminal transitions inherit exact parent-generation and root-owner lineage; only the root session batches ready completion trees instead of sending one follow-up per Run or descendant session.
|
|
31
31
|
|
|
32
32
|
`command.done` remains non-projectable even if malformed or legacy Trace attaches any attention value.
|
|
33
33
|
|
|
@@ -81,11 +81,11 @@ Phases are monotonic:
|
|
|
81
81
|
|
|
82
82
|
Every journal mutation uses the canonical token-owned lock, expected-phase fencing, owner and generation validation, and atomic replacement. Repeated transitions are idempotent. Corrupt, oversized, foreign-owner, or stale-generation state fails closed with bounded diagnostics.
|
|
83
83
|
|
|
84
|
-
|
|
84
|
+
Completion and steer acknowledgments intentionally differ. Successful `sendMessage()` acceptance finalizes an ordinary completion batch immediately and marks its members through the existing terminal-handled authority; this prevents a missed context callback from blocking every later epoch. Urgent steer still requires exact model-context presentation. If a completion member was synchronously archived or pruned after queueing, the bounded delivery snapshot remains sufficient and the missing state write becomes a diagnostic rather than invalidating acceptance.
|
|
85
85
|
|
|
86
86
|
## Completion Collection
|
|
87
87
|
|
|
88
|
-
Reconciliation admits unhandled
|
|
88
|
+
Reconciliation admits unhandled terminal generations with status `done`, `failed`, `killed`, or `exited` that belong to the active root delivery owner. A descendant becomes eligible only when its exact parent chain reaches a terminal top-level Run.
|
|
89
89
|
|
|
90
90
|
It excludes:
|
|
91
91
|
|
|
@@ -97,7 +97,7 @@ It excludes:
|
|
|
97
97
|
|
|
98
98
|
Candidates sort by terminal timestamp, then stable Run identity, then `run_instance_id`. Replacement generations with the same logical Run id remain distinct internal members.
|
|
99
99
|
|
|
100
|
-
While `ctx.isIdle()` is false, candidates remain durable in their Run state and no terminal follow-up is sent. A flush snapshots eligible candidates into one immutable batch. While
|
|
100
|
+
While `ctx.isIdle()` is false, candidates remain durable in their Run state and no terminal follow-up is sent. A flush snapshots eligible candidates into one immutable batch. While transport acceptance is pending, newer terminals stay unhandled for the next bounded completion epoch.
|
|
101
101
|
|
|
102
102
|
## Batch Flush
|
|
103
103
|
|
|
@@ -107,25 +107,20 @@ Flush one batch when:
|
|
|
107
107
|
2. terminals arrive while Pi is already idle and survive one short debounce window;
|
|
108
108
|
3. session restoration discovers unhandled terminal generations or recoverable queued delivery state.
|
|
109
109
|
|
|
110
|
-
The
|
|
110
|
+
The custom message uses `customType: "pi-actors-run-batch"`, `display: true`, `deliverAs: "followUp"`, and `triggerTurn: true`. It restores the visible gray completion card for the operator while supplying the same single tree-compressed prompt to the model. The bounded content retains the exact batch identity for session evidence and deduplication even though Pi omits private message details from model context. It includes:
|
|
111
111
|
|
|
112
112
|
- batch ID and completion window;
|
|
113
113
|
- counts by terminal status;
|
|
114
|
-
- stable Run
|
|
114
|
+
- stable parent-child Run rows with status, compact semantic output, and bounded artifacts;
|
|
115
115
|
- explicit overflow evidence and the canonical runtime Inspect route.
|
|
116
116
|
|
|
117
117
|
The journal may retain at most 256 members and 1 MiB. Model-facing content lists at most 64 exact rows within the centralized model-output bound. Additional members remain represented by exact status counts and supported Inspect guidance. More than 256 unhandled generations form a later batch rather than being discarded.
|
|
118
118
|
|
|
119
119
|
Completion member details remain redacted through existing terminal projection rules: no raw model policy, secrets, private Recipe paths, or machine-local source paths enter the message.
|
|
120
120
|
|
|
121
|
-
## Presentation
|
|
121
|
+
## Acceptance, Presentation, And Recovery
|
|
122
122
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
1. moves the envelope to `presented`;
|
|
126
|
-
2. marks every still-present member generation terminal-handled;
|
|
127
|
-
3. records a non-attention `delivery.steer_presented` marker in the exact Run generation for a presented steer;
|
|
128
|
-
4. retains a bounded owner receipt sufficient for near-term deduplication and diagnostics.
|
|
123
|
+
Ordinary completion delivery atomically advances through queued acceptance to finalization immediately after `sendMessage()` returns, marks every still-present member generation terminal-handled, and retains a bounded owner receipt. A `context` lifecycle adapter remains for exact urgent-steer presentation and harmless completion deduplication; presented steer writes `delivery.steer_presented` in the exact Run generation.
|
|
129
124
|
|
|
130
125
|
The generation-fenced Trace marker prevents a retained historical steer from replaying after bounded owner receipts rotate: suffix compaction cannot retain the older steer while discarding its newer presentation marker. Missing, archived, pruned, or replaced Run state needs no marker because it can no longer replay that original generation.
|
|
131
126
|
|
|
@@ -133,9 +128,9 @@ Recovery rules:
|
|
|
133
128
|
|
|
134
129
|
- Send failure: keep `pending`, record failure evidence, and retry.
|
|
135
130
|
- Crash after queueing: inspect existing owned Pi session evidence for the exact custom message ID.
|
|
136
|
-
-
|
|
137
|
-
-
|
|
138
|
-
- Presented
|
|
131
|
+
- Completion send accepted: finalize immediately and release the next epoch.
|
|
132
|
+
- Crash leaves a queued completion: queued itself proves `sendMessage()` acceptance, so finalize without resend or content reformatting.
|
|
133
|
+
- Presented urgent steer: never resend.
|
|
139
134
|
- Session or context replacement: close timers and callbacks; never deliver through stale context.
|
|
140
135
|
- Owner mismatch: do not inspect, acknowledge, or deliver the envelope.
|
|
141
136
|
|
|
@@ -188,8 +183,8 @@ Implementation is complete only when source and packed-extension tests prove:
|
|
|
188
183
|
2. Idle completions inside the debounce window form one batch.
|
|
189
184
|
3. Completion/settled races project every generation exactly once.
|
|
190
185
|
4. Send failure and restart before queueing retry without a handled marker.
|
|
191
|
-
5. Restart
|
|
192
|
-
6.
|
|
186
|
+
5. Restart in the narrow queue-acceptance/finalization boundary neither loses nor duplicates the batch.
|
|
187
|
+
6. Successful completion acceptance acknowledges members atomically and idempotently; urgent steer still requires exact `context` presentation.
|
|
193
188
|
7. Replacement generations sharing a Run id remain distinct.
|
|
194
189
|
8. Silent, stopped, cancelled, handled, and foreign-owner Runs remain excluded.
|
|
195
190
|
9. Legacy or malformed `command.done` attention, including `steer`, remains non-projectable.
|
|
@@ -202,6 +197,6 @@ Focused observability and delivery tests precede TypeScript/build/import checks.
|
|
|
202
197
|
|
|
203
198
|
## Rollout
|
|
204
199
|
|
|
205
|
-
This is one minor release because durable batching,
|
|
200
|
+
This is one minor release because durable batching, acceptance acknowledgment, lifecycle ordering, and explicit steer share one model-delivery invariant. Completion acceptance must release later epochs without depending on a context callback; steer must not ship before its durable presentation-deduplication path exists.
|
|
206
201
|
|
|
207
202
|
Update README and Run documentation only when implementation establishes the new public behavior. Move the accepted outcome from BACKLOG to CHANGELOG only after complete validation.
|
package/docs/recipe-library.md
CHANGED
|
@@ -36,7 +36,7 @@ Artifact pipelines terminate in files/manifests and result evidence; they do not
|
|
|
36
36
|
|
|
37
37
|
### Music playback and controlled services
|
|
38
38
|
|
|
39
|
-
- `music-player/playback` — singleton playback service that resolves files, directories, URLs, explicit lists, and playlist files into one persistent queue; it exposes declared playback Controls including arbitrary absolute `volume` percentages, generation-fenced endpoint readiness, structured status, a player-owned continuity checkpoint, and playback Trace. Player selection is `player:enum(auto,mpv,afplay,ffplay,cvlc,play,wmp)=auto`.
|
|
39
|
+
- `music-player/playback` — singleton playback service that resolves files, directories, URLs, explicit lists, and playlist files into one persistent queue; it exposes declared playback Controls including arbitrary absolute `volume` percentages, generation-fenced endpoint readiness, structured status, a player-owned continuity checkpoint, and playback Trace. Player selection is `player:enum(auto,mpv,afplay,ffplay,cvlc,play,wmp)=auto`. The single `skills/music-player/scripts/playback.mjs` executable also supports standalone foreground `serve` and `control <state-dir> <action> [percent]`; control observes the existing owner, using canonical Control records for Actors and generation-fenced RPC for standalone playback. The maintained Generative App uses that same control entrypoint for Actor-owned playback.
|
|
40
40
|
- `actors/resource-locker` — optional queue/lease-lock service with explicit owner/resource input, lock Trace, and a 512-record/1 MiB atomically retained journal.
|
|
41
41
|
|
|
42
42
|
These Recipes declare actor-local Control. Ordinary one-shot Recipes omit it. Helper-backed Skill Recipes self-locate through runtime-owned `{skill_dir}`; callers do not pass package installation roots.
|
package/lib/async-runs.ts
CHANGED
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
type CurrentPolicyProvenance,
|
|
33
33
|
} from "./model-context.ts";
|
|
34
34
|
import * as Paths from "./paths.ts";
|
|
35
|
+
import * as RunDeliveryLineage from "./run-delivery-lineage.ts";
|
|
35
36
|
import * as RecipesReferences from "./recipes-references.ts";
|
|
36
37
|
import * as RecipesUsage from "./recipes-usage.ts";
|
|
37
38
|
import * as Schema from "./schema.ts";
|
|
@@ -186,6 +187,8 @@ export interface AsyncRunMeta {
|
|
|
186
187
|
artifacts?: Record<string, RunArtifactDeclaration>;
|
|
187
188
|
control?: string[];
|
|
188
189
|
control_endpoint?: AsyncRunControlEndpoint;
|
|
190
|
+
delivery_owner_id?: string;
|
|
191
|
+
delivery_parent?: RunDeliveryLineage.RunDeliveryParent;
|
|
189
192
|
model_policy?: CurrentPolicyProvenance;
|
|
190
193
|
notification_policy?: "normal" | "silent";
|
|
191
194
|
process_identity?: RunProcessIdentity;
|
|
@@ -666,6 +669,10 @@ export function startRun(
|
|
|
666
669
|
startParams.transport_context,
|
|
667
670
|
);
|
|
668
671
|
const artifacts = resolveArtifactPaths(startParams.artifacts, outputValues);
|
|
672
|
+
const runInstanceId = randomUUID();
|
|
673
|
+
const deliveryLineage = RunDeliveryLineage.inheritedRunDeliveryLineage(
|
|
674
|
+
startParams.ownerId,
|
|
675
|
+
);
|
|
669
676
|
const meta: AsyncRunMeta = {
|
|
670
677
|
argv: [process.execPath, ...argv],
|
|
671
678
|
createdAt: new Date().toISOString(),
|
|
@@ -683,7 +690,7 @@ export function startRun(
|
|
|
683
690
|
...(recipe ? { recipe } : {}),
|
|
684
691
|
...(recipeFile ? { recipe_file: recipeFile } : {}),
|
|
685
692
|
run,
|
|
686
|
-
run_instance_id:
|
|
693
|
+
run_instance_id: runInstanceId,
|
|
687
694
|
state_dir: stateDir,
|
|
688
695
|
state_schema: RuntimeIdentity.RUN_STATE_SCHEMA,
|
|
689
696
|
status: "running",
|
|
@@ -696,6 +703,7 @@ export function startRun(
|
|
|
696
703
|
...(startParams.control_endpoint
|
|
697
704
|
? { control_endpoint: startParams.control_endpoint }
|
|
698
705
|
: {}),
|
|
706
|
+
...(deliveryLineage ?? {}),
|
|
699
707
|
...(startParams.notification_policy === "silent"
|
|
700
708
|
? { notification_policy: "silent" as const }
|
|
701
709
|
: {}),
|
|
@@ -726,6 +734,15 @@ export function startRun(
|
|
|
726
734
|
const child = spawn(process.execPath, argv, {
|
|
727
735
|
cwd,
|
|
728
736
|
detached: true,
|
|
737
|
+
...(deliveryLineage
|
|
738
|
+
? {
|
|
739
|
+
env: RunDeliveryLineage.runDeliveryChildEnv(deliveryLineage, {
|
|
740
|
+
run,
|
|
741
|
+
run_instance_id: runInstanceId,
|
|
742
|
+
state_dir: stateDir,
|
|
743
|
+
}),
|
|
744
|
+
}
|
|
745
|
+
: {}),
|
|
729
746
|
stdio: ["ignore", outFd, errFd],
|
|
730
747
|
});
|
|
731
748
|
closeSync(outFd);
|
package/lib/command-templates.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Owns portable command-template parsing, expansion, risk checks, retries, timeouts, and direct execution.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { spawn } from "node:child_process";
|
|
7
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
8
8
|
import { accessSync, appendFileSync, constants, mkdirSync, mkdtempSync, statSync, writeFileSync } from "node:fs";
|
|
9
9
|
import { homedir, tmpdir } from "node:os";
|
|
10
10
|
import { basename, delimiter, extname, isAbsolute, join, resolve as resolvePath } from "node:path";
|
|
@@ -1010,12 +1010,50 @@ function execCommandTemplateOnce(
|
|
|
1010
1010
|
let settled = false;
|
|
1011
1011
|
let timeoutId: NodeJS.Timeout | undefined;
|
|
1012
1012
|
let killTimeoutId: NodeJS.Timeout | undefined;
|
|
1013
|
+
const signalProcessTree = (signal: NodeJS.Signals): void => {
|
|
1014
|
+
if (proc.pid === undefined) return;
|
|
1015
|
+
if (process.platform === "win32") {
|
|
1016
|
+
spawnSync("taskkill", [
|
|
1017
|
+
"/PID",
|
|
1018
|
+
String(proc.pid),
|
|
1019
|
+
"/T",
|
|
1020
|
+
...(signal === "SIGKILL" ? ["/F"] : []),
|
|
1021
|
+
]);
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
const snapshot = spawnSync("ps", ["-eo", "pid=,ppid="], { encoding: "utf8" });
|
|
1025
|
+
const children = new Map<number, number[]>();
|
|
1026
|
+
for (const line of snapshot.stdout?.split(/\r?\n/u) ?? []) {
|
|
1027
|
+
const [pidText, parentText] = line.trim().split(/\s+/u);
|
|
1028
|
+
const pid = Number(pidText);
|
|
1029
|
+
const parent = Number(parentText);
|
|
1030
|
+
if (!Number.isSafeInteger(pid) || !Number.isSafeInteger(parent)) continue;
|
|
1031
|
+
const siblings = children.get(parent) ?? [];
|
|
1032
|
+
siblings.push(pid);
|
|
1033
|
+
children.set(parent, siblings);
|
|
1034
|
+
}
|
|
1035
|
+
const descendants: number[] = [];
|
|
1036
|
+
const collect = (parent: number): void => {
|
|
1037
|
+
for (const child of children.get(parent) ?? []) {
|
|
1038
|
+
collect(child);
|
|
1039
|
+
descendants.push(child);
|
|
1040
|
+
}
|
|
1041
|
+
};
|
|
1042
|
+
collect(proc.pid);
|
|
1043
|
+
for (const pid of [...descendants, proc.pid]) {
|
|
1044
|
+
try {
|
|
1045
|
+
process.kill(pid, signal);
|
|
1046
|
+
} catch (error) {
|
|
1047
|
+
if ((error as NodeJS.ErrnoException).code !== "ESRCH") throw error;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
};
|
|
1013
1051
|
const killProcess = (): void => {
|
|
1014
1052
|
if (killed) return;
|
|
1015
1053
|
killed = true;
|
|
1016
|
-
|
|
1054
|
+
signalProcessTree("SIGTERM");
|
|
1017
1055
|
killTimeoutId = setTimeout(() => {
|
|
1018
|
-
if (!settled)
|
|
1056
|
+
if (!settled) signalProcessTree("SIGKILL");
|
|
1019
1057
|
}, options.killGrace ?? 5000);
|
|
1020
1058
|
};
|
|
1021
1059
|
const settle = (code: number): void => {
|
package/lib/extension-runtime.ts
CHANGED
|
@@ -143,7 +143,7 @@ export function createActorExtensionRuntime(
|
|
|
143
143
|
},
|
|
144
144
|
getRunOwnerId,
|
|
145
145
|
onAgentSettled(ctx) {
|
|
146
|
-
if (
|
|
146
|
+
if (!activeRunOwnerId || getRunOwnerId(ctx) !== activeRunOwnerId) return;
|
|
147
147
|
if (!runUiRuntime.flushCompletionBatch(ctx)) automaticReview.schedule();
|
|
148
148
|
},
|
|
149
149
|
onContext(messages, ctx) {
|