@llblab/pi-actors 0.41.1 → 0.42.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +11 -6
- package/BACKLOG.md +1 -65
- package/CHANGELOG.md +25 -0
- package/README.md +24 -4
- package/dist/index.js +25 -121
- package/dist/lib/async-runs.d.ts +37 -5
- package/dist/lib/async-runs.js +181 -51
- package/dist/lib/automatic-review-runtime.d.ts +18 -0
- package/dist/lib/automatic-review-runtime.js +96 -0
- package/dist/lib/command-templates.js +1 -1
- package/dist/lib/draft-consolidation-transaction.d.ts +65 -0
- package/dist/lib/draft-consolidation-transaction.js +610 -0
- package/dist/lib/draft-consolidation.d.ts +35 -0
- package/dist/lib/draft-consolidation.js +126 -0
- package/dist/lib/draft-review.d.ts +56 -0
- package/dist/lib/draft-review.js +254 -0
- package/dist/lib/draft-sleep.d.ts +65 -0
- package/dist/lib/draft-sleep.js +468 -0
- package/dist/lib/file-state.d.ts +8 -1
- package/dist/lib/file-state.js +115 -18
- package/dist/lib/inspector-actions.d.ts +16 -0
- package/dist/lib/inspector-actions.js +57 -0
- package/dist/lib/inspector-command.d.ts +7 -0
- package/dist/lib/inspector-command.js +37 -0
- package/dist/lib/inspector-overlay.d.ts +23 -3
- package/dist/lib/inspector-overlay.js +327 -70
- package/dist/lib/inspector.d.ts +9 -0
- package/dist/lib/inspector.js +49 -1
- package/dist/lib/observability.d.ts +13 -0
- package/dist/lib/observability.js +160 -6
- package/dist/lib/paths.d.ts +7 -0
- package/dist/lib/paths.js +28 -0
- package/dist/lib/recipes-discovery.js +32 -24
- package/dist/lib/recipes-usage.d.ts +18 -6
- package/dist/lib/recipes-usage.js +445 -34
- package/dist/lib/review-control.d.ts +14 -0
- package/dist/lib/review-control.js +111 -0
- package/dist/lib/review-diagnostics.d.ts +11 -0
- package/dist/lib/review-diagnostics.js +148 -0
- package/dist/lib/review-projection.d.ts +14 -0
- package/dist/lib/review-projection.js +170 -0
- package/dist/lib/run-ui-runtime.d.ts +18 -0
- package/dist/lib/run-ui-runtime.js +123 -0
- package/dist/lib/runs-artifacts.d.ts +1 -1
- package/dist/lib/runs-artifacts.js +1 -1
- package/dist/lib/runs-control.d.ts +10 -2
- package/dist/lib/runs-control.js +37 -7
- package/dist/lib/runs-identity.d.ts +1 -1
- package/dist/lib/runs-identity.js +1 -1
- package/dist/lib/runs-index.d.ts +11 -2
- package/dist/lib/runs-index.js +46 -23
- package/dist/lib/runs-mailbox.d.ts +1 -1
- package/dist/lib/runs-mailbox.js +1 -1
- package/dist/lib/runs-messages.d.ts +1 -1
- package/dist/lib/runs-messages.js +1 -1
- package/dist/lib/runs-outbox.d.ts +1 -1
- package/dist/lib/runs-outbox.js +1 -1
- package/dist/lib/runs-ownership.d.ts +1 -1
- package/dist/lib/runs-ownership.js +18 -4
- package/dist/lib/runs-parent-teardown.d.ts +51 -0
- package/dist/lib/runs-parent-teardown.js +172 -0
- package/dist/lib/runs-process.d.ts +1 -1
- package/dist/lib/runs-process.js +5 -4
- package/dist/lib/runs-retention.d.ts +1 -1
- package/dist/lib/runs-retention.js +1 -1
- package/dist/lib/runs-start.d.ts +5 -3
- package/dist/lib/runs-start.js +7 -48
- package/dist/lib/runs-status.d.ts +5 -3
- package/dist/lib/runs-status.js +7 -6
- package/dist/lib/runtime.d.ts +7 -1
- package/dist/lib/runtime.js +32 -18
- package/dist/lib/tool-review-lineage-transaction.d.ts +27 -0
- package/dist/lib/tool-review-lineage-transaction.js +597 -0
- package/dist/lib/tool-review-lineage.d.ts +24 -0
- package/dist/lib/tool-review-lineage.js +98 -0
- package/dist/lib/tool-review-scheduler.d.ts +80 -0
- package/dist/lib/tool-review-scheduler.js +494 -0
- package/dist/lib/tool-review-transaction.d.ts +50 -0
- package/dist/lib/tool-review-transaction.js +362 -0
- package/dist/lib/tool-review.d.ts +56 -0
- package/dist/lib/tool-review.js +197 -0
- package/dist/lib/tools-inspect.js +28 -4
- package/dist/lib/tools-local.js +21 -4
- package/dist/lib/tools-message.d.ts +1 -0
- package/dist/lib/tools-message.js +29 -17
- package/dist/lib/tools-spawn.js +14 -2
- package/dist/lib/tools.d.ts +1 -0
- package/dist/lib/tools.js +1 -0
- package/dist/recipes/draft-review.json +24 -0
- package/dist/recipes/tool-review.json +24 -0
- package/dist/scripts/async-runner.mjs +9 -9
- package/dist/scripts/build-dist.mjs +6 -1
- package/dist/scripts/conformance.mjs +6 -1
- package/dist/scripts/recipe-utils.mjs +3 -3
- package/dist/scripts/release-gates.mjs +165 -0
- package/dist/skills/actors/SKILL.md +9 -8
- package/dist/skills/swarm/SKILL.md +1 -1
- package/docs/actor-inspector.md +20 -11
- package/docs/async-runs.md +16 -2
- package/docs/recipe-library.md +7 -3
- package/docs/template-recipes.md +4 -11
- package/docs/tool-registry.md +13 -4
- package/index.ts +27 -142
- package/lib/async-runs.ts +283 -67
- package/lib/automatic-review-runtime.ts +135 -0
- package/lib/command-templates.ts +1 -1
- package/lib/draft-consolidation-transaction.ts +821 -0
- package/lib/draft-consolidation.ts +181 -0
- package/lib/draft-review.ts +325 -0
- package/lib/draft-sleep.ts +576 -0
- package/lib/file-state.ts +143 -19
- package/lib/inspector-actions.ts +79 -0
- package/lib/inspector-command.ts +54 -0
- package/lib/inspector-overlay.ts +377 -91
- package/lib/inspector.ts +73 -1
- package/lib/observability.ts +194 -5
- package/lib/paths.ts +43 -0
- package/lib/recipes-discovery.ts +34 -26
- package/lib/recipes-usage.ts +569 -40
- package/lib/review-control.ts +137 -0
- package/lib/review-diagnostics.ts +164 -0
- package/lib/review-projection.ts +200 -0
- package/lib/run-ui-runtime.ts +153 -0
- package/lib/runs-artifacts.ts +1 -1
- package/lib/runs-control.ts +69 -6
- package/lib/runs-identity.ts +1 -1
- package/lib/runs-index.ts +57 -21
- package/lib/runs-mailbox.ts +1 -1
- package/lib/runs-messages.ts +1 -1
- package/lib/runs-outbox.ts +1 -1
- package/lib/runs-ownership.ts +23 -4
- package/lib/runs-parent-teardown.ts +257 -0
- package/lib/runs-process.ts +5 -4
- package/lib/runs-retention.ts +1 -1
- package/lib/runs-start.ts +13 -68
- package/lib/runs-status.ts +17 -8
- package/lib/runtime.ts +34 -17
- package/lib/tool-review-lineage-transaction.ts +881 -0
- package/lib/tool-review-lineage.ts +145 -0
- package/lib/tool-review-scheduler.ts +635 -0
- package/lib/tool-review-transaction.ts +563 -0
- package/lib/tool-review.ts +270 -0
- package/lib/tools-inspect.ts +35 -4
- package/lib/tools-local.ts +29 -4
- package/lib/tools-message.ts +45 -30
- package/lib/tools-spawn.ts +22 -2
- package/lib/tools.ts +5 -0
- package/package.json +5 -3
- package/recipes/draft-review.json +24 -0
- package/recipes/tool-review.json +24 -0
- package/scripts/async-runner.mjs +9 -9
- package/scripts/build-dist.mjs +6 -1
- package/scripts/conformance.mjs +6 -1
- package/scripts/recipe-utils.mjs +3 -3
- package/scripts/release-gates.mjs +165 -0
- package/skills/actors/SKILL.md +9 -8
- package/skills/swarm/SKILL.md +1 -1
package/dist/lib/async-runs.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Command-template async run lifecycle facade.
|
|
3
|
-
* Owns launch, state observation, listing, message/control facade methods, and retention while runs-* subdomains own narrower run internals.
|
|
3
|
+
* Owns: launch, state observation, listing, message/control facade methods, and retention while runs-* subdomains own narrower run internals.
|
|
4
4
|
*/
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
6
7
|
import { closeSync, existsSync, mkdirSync, openSync, readdirSync, statSync, writeFileSync, } from "node:fs";
|
|
7
|
-
import { basename, dirname, extname, join, relative, resolve } from "node:path";
|
|
8
|
+
import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
|
|
8
9
|
import { fileURLToPath } from "node:url";
|
|
9
10
|
import { writeJsonAtomic } from "./file-state.js";
|
|
10
11
|
import { CURRENT_MODEL_VALUE_KEY, CURRENT_THINKING_VALUE_KEY, describeCurrentPolicyProvenance, } from "./model-context.js";
|
|
@@ -20,11 +21,33 @@ import { archiveTerminalRun, pruneTerminalRun } from "./runs-retention.js";
|
|
|
20
21
|
import { captureRunProcessIdentity, verifyRunProcessIdentity, } from "./runs-process.js";
|
|
21
22
|
import * as RunsStart from "./runs-start.js";
|
|
22
23
|
import * as RunsIndex from "./runs-index.js";
|
|
24
|
+
import * as RunsParentTeardown from "./runs-parent-teardown.js";
|
|
23
25
|
import { claimRunInboxMessageInStateDir, parseRunInboxLine, processRunInboxMessagesInStateDir, runInboxFile, updateRunInboxMessageStatusInStateDir, } from "./runs-mailbox.js";
|
|
24
26
|
import { deliverRunMessage, } from "./runs-messages.js";
|
|
25
|
-
import { buildRunStatus, tailFile, tailLines } from "./runs-status.js";
|
|
27
|
+
import { buildRunStatus, tailFile, tailLines, } from "./runs-status.js";
|
|
26
28
|
import { readJsonFileResilient } from "./state-readers.js";
|
|
27
29
|
const RUNNER_IDENTITY_GRACE_MS = 5000;
|
|
30
|
+
export function normalizeRunTransportContext(value) {
|
|
31
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
32
|
+
return undefined;
|
|
33
|
+
const normalized = {};
|
|
34
|
+
for (const [key, item] of Object.entries(value).slice(0, 16)) {
|
|
35
|
+
const safeKey = key.trim().slice(0, 64);
|
|
36
|
+
if (!safeKey)
|
|
37
|
+
continue;
|
|
38
|
+
if (typeof item === "string") {
|
|
39
|
+
normalized[safeKey] = item.trim().slice(0, 256);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (typeof item === "number" && Number.isFinite(item)) {
|
|
43
|
+
normalized[safeKey] = item;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (typeof item === "boolean")
|
|
47
|
+
normalized[safeKey] = item;
|
|
48
|
+
}
|
|
49
|
+
return Object.keys(normalized).length ? normalized : undefined;
|
|
50
|
+
}
|
|
28
51
|
const DEFAULT_STATE_ROOT = Paths.getRunStateRoot();
|
|
29
52
|
const DEFAULT_RECIPE_ROOT = Paths.getRecipeRoot();
|
|
30
53
|
function packageRoot() {
|
|
@@ -89,7 +112,8 @@ function resolveRecipeFile(file) {
|
|
|
89
112
|
function isMutableUsageRecipeFile(file) {
|
|
90
113
|
const userRoot = resolve(DEFAULT_RECIPE_ROOT);
|
|
91
114
|
const resolved = resolve(file);
|
|
92
|
-
|
|
115
|
+
const relation = relative(userRoot, resolved);
|
|
116
|
+
return relation !== "" && !relation.startsWith("..") && !isAbsolute(relation);
|
|
93
117
|
}
|
|
94
118
|
function readRecipeFile(file) {
|
|
95
119
|
const path = resolveRecipeFile(file);
|
|
@@ -201,8 +225,8 @@ function assertCurrentPlaceholderReferencesResolved(template, defaults, values,
|
|
|
201
225
|
throw Object.assign(new Error(`Template recipe requires the current Pi ${check.label} for inheritance, but no current ${check.label} was available. Pass explicit values or launch from a Pi session with a selected ${check.label}. unresolved=${refs.slice(0, 4).join(",")}`), { model_policy: modelPolicy });
|
|
202
226
|
}
|
|
203
227
|
}
|
|
204
|
-
function acquireStateStartLock(stateDir) {
|
|
205
|
-
return RunsStart.acquireStateStartLock(stateDir);
|
|
228
|
+
function acquireStateStartLock(stateDir, options = {}) {
|
|
229
|
+
return RunsStart.acquireStateStartLock(stateDir, options);
|
|
206
230
|
}
|
|
207
231
|
function prepareStateDirForStart(stateDir) {
|
|
208
232
|
RunsStart.prepareStateDirForStart(stateDir, readJson, RUNNER_PATH);
|
|
@@ -228,7 +252,9 @@ export function startRun(params, cwd) {
|
|
|
228
252
|
assertCurrentPlaceholderReferencesResolved(resolved.template, startParams.defaults, values, modelPolicy);
|
|
229
253
|
assertNoActiveRunState(stateDir);
|
|
230
254
|
mkdirSync(stateDir, { recursive: true });
|
|
231
|
-
const releaseStartLock = acquireStateStartLock(stateDir
|
|
255
|
+
const releaseStartLock = acquireStateStartLock(stateDir, {
|
|
256
|
+
onContention: startParams.lifecycleHooks?.onLockContention,
|
|
257
|
+
});
|
|
232
258
|
try {
|
|
233
259
|
claimRunStateDirectory(stateDir, run);
|
|
234
260
|
assertNoActiveRunState(stateDir);
|
|
@@ -244,8 +270,10 @@ export function startRun(params, cwd) {
|
|
|
244
270
|
const recipeContextRecords = recipeFile && includeActorRecipeContext
|
|
245
271
|
? RecipesReferences.buildRecipeContextRecords(recipeFile)
|
|
246
272
|
: undefined;
|
|
247
|
-
if (recipeFile &&
|
|
248
|
-
|
|
273
|
+
if (recipeFile &&
|
|
274
|
+
isMutableUsageRecipeFile(recipeFile) &&
|
|
275
|
+
!RecipesUsage.recordRecipeLaunch(recipeFile, new Date(), startParams.launch_source === "tool" ? "tool" : "spawn")) {
|
|
276
|
+
throw new Error(`Recipe launch rejected because its source changed during activation: ${recipeFile}. Reload recipe tools and retry.`);
|
|
249
277
|
}
|
|
250
278
|
const outFd = openSync(stdout, "a");
|
|
251
279
|
const errFd = openSync(stderr, "a");
|
|
@@ -254,6 +282,7 @@ export function startRun(params, cwd) {
|
|
|
254
282
|
...(startParams.defaults || {}),
|
|
255
283
|
...values,
|
|
256
284
|
};
|
|
285
|
+
const transportContext = normalizeRunTransportContext(startParams.transport_context);
|
|
257
286
|
const artifacts = resolveArtifactPaths(startParams.artifacts, outputValues);
|
|
258
287
|
const meta = {
|
|
259
288
|
argv: [process.execPath, ...argv],
|
|
@@ -262,11 +291,14 @@ export function startRun(params, cwd) {
|
|
|
262
291
|
...(startParams.launch_source
|
|
263
292
|
? { launch_source: startParams.launch_source }
|
|
264
293
|
: {}),
|
|
294
|
+
...(startParams.launch_correlation
|
|
295
|
+
? { launch_correlation: startParams.launch_correlation } : {}),
|
|
265
296
|
...(startParams.ownerId ? { ownerId: startParams.ownerId } : {}),
|
|
266
297
|
pid: 0,
|
|
267
298
|
...(recipe ? { recipe } : {}),
|
|
268
299
|
...(recipeFile ? { recipe_file: recipeFile } : {}),
|
|
269
300
|
run,
|
|
301
|
+
run_instance_id: randomUUID(),
|
|
270
302
|
state_dir: stateDir,
|
|
271
303
|
status: "running",
|
|
272
304
|
...(startParams.tool ? { tool: startParams.tool } : {}),
|
|
@@ -276,12 +308,17 @@ export function startRun(params, cwd) {
|
|
|
276
308
|
...(artifacts ? { artifacts } : {}),
|
|
277
309
|
...(startParams.control ? { control: startParams.control } : {}),
|
|
278
310
|
...(startParams.mailbox ? { mailbox: startParams.mailbox } : {}),
|
|
311
|
+
...(startParams.notification_policy === "silent"
|
|
312
|
+
? { notification_policy: "silent" }
|
|
313
|
+
: {}),
|
|
279
314
|
...(recipeContextRecords && recipeContextRecords.length > 0
|
|
280
315
|
? { recipe_context_records: recipeContextRecords }
|
|
281
316
|
: {}),
|
|
282
317
|
...(startParams.retire_when === "children_terminal"
|
|
283
318
|
? { retire_when: "children_terminal" }
|
|
284
319
|
: {}),
|
|
320
|
+
...(transportContext
|
|
321
|
+
? { transport_context: transportContext } : {}),
|
|
285
322
|
};
|
|
286
323
|
writeJsonAtomic(join(stateDir, "run.json"), meta);
|
|
287
324
|
const child = spawn(process.execPath, argv, {
|
|
@@ -303,7 +340,7 @@ export function startRun(params, cwd) {
|
|
|
303
340
|
phase: "starting",
|
|
304
341
|
updatedAt: new Date().toISOString(),
|
|
305
342
|
});
|
|
306
|
-
writeFileSync(join(stateDir, "events.jsonl"), `${JSON.stringify({ event: "run.start", run, pid: meta.pid, ts: new Date().toISOString() })}\n`, { flag: "a" });
|
|
343
|
+
writeFileSync(join(stateDir, "events.jsonl"), `${JSON.stringify({ event: "run.start", run, run_instance_id: meta.run_instance_id, pid: meta.pid, ts: new Date().toISOString() })}\n`, { flag: "a" });
|
|
307
344
|
child.unref();
|
|
308
345
|
return meta;
|
|
309
346
|
}
|
|
@@ -312,17 +349,20 @@ export function startRun(params, cwd) {
|
|
|
312
349
|
}
|
|
313
350
|
}
|
|
314
351
|
export { parseRunOutboxEventLine } from "./runs-outbox.js";
|
|
315
|
-
|
|
316
|
-
|
|
352
|
+
function resolveRunStateDir(runOrDir) {
|
|
353
|
+
return resolve(/[\\/]/u.test(runOrDir)
|
|
317
354
|
? runOrDir
|
|
318
355
|
: join(DEFAULT_STATE_ROOT, safeRunId(runOrDir)));
|
|
356
|
+
}
|
|
357
|
+
export function getRunStatus(runOrDir) {
|
|
358
|
+
const stateDir = resolveRunStateDir(runOrDir);
|
|
319
359
|
const meta = readJson(join(stateDir, "run.json"));
|
|
320
360
|
if (!meta)
|
|
321
361
|
throw new Error(`Run not found: ${runOrDir}`);
|
|
322
362
|
return buildRunStatus(stateDir, runOrDir, meta, readJson, RUNNER_PATH, RUNNER_IDENTITY_GRACE_MS);
|
|
323
363
|
}
|
|
324
|
-
export function listRunStateDirs(stateRoot = DEFAULT_STATE_ROOT
|
|
325
|
-
return RunsIndex.listRunStateDirs(stateRoot
|
|
364
|
+
export function listRunStateDirs(stateRoot = DEFAULT_STATE_ROOT) {
|
|
365
|
+
return RunsIndex.listRunStateDirs(stateRoot);
|
|
326
366
|
}
|
|
327
367
|
export function rebuildRunStateIndex(stateRoot = DEFAULT_STATE_ROOT) {
|
|
328
368
|
return RunsIndex.rebuildRunStateIndex(stateRoot, getRunStatus);
|
|
@@ -333,6 +373,67 @@ export function readRunStateIndex(stateRoot = DEFAULT_STATE_ROOT) {
|
|
|
333
373
|
export function listRuns(stateRoot = DEFAULT_STATE_ROOT, statusFilter) {
|
|
334
374
|
return RunsIndex.listRuns(stateRoot, getRunStatus, readJson, statusFilter);
|
|
335
375
|
}
|
|
376
|
+
export function teardownRunsOwnedByParent(ownerId, stateRoot = DEFAULT_STATE_ROOT, options = {}) {
|
|
377
|
+
const result = RunsParentTeardown.teardownParentRuns(ownerId, {
|
|
378
|
+
getRunStatus,
|
|
379
|
+
killRun,
|
|
380
|
+
listRunStatuses: () => {
|
|
381
|
+
const discovered = RunsIndex.discoverRunStateDirs(stateRoot, Number.POSITIVE_INFINITY);
|
|
382
|
+
const failures = discovered.issues.map((issue) => ({
|
|
383
|
+
path: issue.path,
|
|
384
|
+
reason: issue.reason.replaceAll("_", " "),
|
|
385
|
+
}));
|
|
386
|
+
const statuses = discovered.stateDirs.flatMap((stateDir) => {
|
|
387
|
+
try {
|
|
388
|
+
return [getRunStatus(stateDir)];
|
|
389
|
+
}
|
|
390
|
+
catch (error) {
|
|
391
|
+
failures.push({
|
|
392
|
+
path: stateDir,
|
|
393
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
394
|
+
});
|
|
395
|
+
return [];
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
return { failures, statuses };
|
|
399
|
+
},
|
|
400
|
+
recordAttempt: (attempt) => {
|
|
401
|
+
const current = getRunStatus(attempt.stateDir);
|
|
402
|
+
if (!attempt.runInstanceId ||
|
|
403
|
+
current.run_instance_id !== attempt.runInstanceId) {
|
|
404
|
+
throw new Error("run generation changed before teardown evidence");
|
|
405
|
+
}
|
|
406
|
+
writeFileSync(join(attempt.stateDir, "events.jsonl"), `${JSON.stringify({
|
|
407
|
+
event: "run.parent_teardown",
|
|
408
|
+
ownerId: attempt.ownerId,
|
|
409
|
+
outcome: attempt.outcome,
|
|
410
|
+
reason: attempt.reason,
|
|
411
|
+
run: attempt.run,
|
|
412
|
+
run_instance_id: attempt.runInstanceId,
|
|
413
|
+
type: "control.kill",
|
|
414
|
+
trigger: options.trigger ?? "parent_shutdown",
|
|
415
|
+
ts: new Date().toISOString(),
|
|
416
|
+
})}\n`, { flag: "a" });
|
|
417
|
+
},
|
|
418
|
+
});
|
|
419
|
+
const summaryPath = join(stateRoot, "teardown", `${Date.now()}-${randomUUID()}.json`);
|
|
420
|
+
try {
|
|
421
|
+
writeJsonAtomic(summaryPath, RunsParentTeardown.buildBoundedParentTeardownSummary(result, ownerId ?? "unknown", options.trigger ?? "parent_shutdown", new Date().toISOString()));
|
|
422
|
+
return { ...result, summaryPath };
|
|
423
|
+
}
|
|
424
|
+
catch (error) {
|
|
425
|
+
const failure = {
|
|
426
|
+
path: summaryPath,
|
|
427
|
+
reason: `summary: ${error instanceof Error ? error.message : String(error)}`,
|
|
428
|
+
};
|
|
429
|
+
return {
|
|
430
|
+
...result,
|
|
431
|
+
discoveryFailed: result.discoveryFailed + 1,
|
|
432
|
+
discoveryFailures: [...result.discoveryFailures, failure],
|
|
433
|
+
failed: result.failed + 1,
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
}
|
|
336
437
|
export function tailRun(runOrDir, lines = 40) {
|
|
337
438
|
const status = getRunStatus(runOrDir);
|
|
338
439
|
const stateDir = String(status.state_dir);
|
|
@@ -441,11 +542,11 @@ function finalizeInterruptedReviewEvidence(stateDir, phase, signal) {
|
|
|
441
542
|
return {
|
|
442
543
|
attempt: index + 1,
|
|
443
544
|
stdout: {
|
|
444
|
-
path: relative(stateDir, stdoutFile),
|
|
545
|
+
path: relative(stateDir, stdoutFile).replaceAll("\\", "/"),
|
|
445
546
|
bytes: existsSync(stdoutFile) ? statSync(stdoutFile).size : 0,
|
|
446
547
|
},
|
|
447
548
|
stderr: {
|
|
448
|
-
path: relative(stateDir, stderrFile),
|
|
549
|
+
path: relative(stateDir, stderrFile).replaceAll("\\", "/"),
|
|
449
550
|
bytes: existsSync(stderrFile) ? statSync(stderrFile).size : 0,
|
|
450
551
|
},
|
|
451
552
|
};
|
|
@@ -470,17 +571,38 @@ function finalizeInterruptedReviewEvidence(stateDir, phase, signal) {
|
|
|
470
571
|
updated_at: completedAt,
|
|
471
572
|
});
|
|
472
573
|
}
|
|
473
|
-
function stopRun(runOrDir, signal, event) {
|
|
474
|
-
const
|
|
475
|
-
const
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
574
|
+
function stopRun(runOrDir, signal, event, expected = {}) {
|
|
575
|
+
const stateDir = resolveRunStateDir(runOrDir);
|
|
576
|
+
const releaseControlLock = RunsStart.acquireStateStartLock(stateDir);
|
|
577
|
+
try {
|
|
578
|
+
expected.onLocked?.();
|
|
579
|
+
const status = getRunStatus(stateDir);
|
|
580
|
+
if (expected.ownerId !== undefined &&
|
|
581
|
+
status.ownerId !== expected.ownerId) {
|
|
582
|
+
return { stopped: false, reason: "ownership changed", status };
|
|
583
|
+
}
|
|
584
|
+
if (expected.runInstanceId !== undefined &&
|
|
585
|
+
status.run_instance_id !== expected.runInstanceId) {
|
|
586
|
+
return { stopped: false, reason: "run generation changed", status };
|
|
587
|
+
}
|
|
588
|
+
const pid = Number(status.pid || 0);
|
|
589
|
+
if (status.status !== "running" && status.status !== "exited") {
|
|
590
|
+
return { stopped: false, reason: "not running", status };
|
|
591
|
+
}
|
|
592
|
+
const identity = verifyRunProcessIdentity(pid, status.process_identity);
|
|
593
|
+
if (status.status === "exited") {
|
|
594
|
+
if (identity.status === "owner_mismatch" ||
|
|
595
|
+
identity.status === "unsupported_proof") {
|
|
596
|
+
return {
|
|
597
|
+
stopped: false,
|
|
598
|
+
reason: identity.status.replaceAll("_", " "),
|
|
599
|
+
process_identity_status: identity.status,
|
|
600
|
+
status,
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
return { stopped: false, reason: "not running", status };
|
|
604
|
+
}
|
|
605
|
+
if (!identity.valid) {
|
|
484
606
|
return {
|
|
485
607
|
stopped: false,
|
|
486
608
|
reason: identity.status.replaceAll("_", " "),
|
|
@@ -488,28 +610,22 @@ function stopRun(runOrDir, signal, event) {
|
|
|
488
610
|
status,
|
|
489
611
|
};
|
|
490
612
|
}
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
markTerminalHandled(stateDir, { event, signal });
|
|
504
|
-
if (event === "run.kill") {
|
|
505
|
-
finalizeInterruptedReviewEvidence(stateDir, "killed", signal);
|
|
506
|
-
markTerminalProgress(stateDir, "killed");
|
|
613
|
+
const signalResult = signalOwnedRunProcess(pid, signal, status.process_identity);
|
|
614
|
+
writeFileSync(join(stateDir, "events.jsonl"), `${JSON.stringify({ event, pid, signal, ...signalResult, ts: new Date().toISOString() })}\n`, { flag: "a" });
|
|
615
|
+
markTerminalHandled(stateDir, { event, signal });
|
|
616
|
+
if (event === "run.kill") {
|
|
617
|
+
finalizeInterruptedReviewEvidence(stateDir, "killed", signal);
|
|
618
|
+
markTerminalProgress(stateDir, "killed");
|
|
619
|
+
}
|
|
620
|
+
if (event === "run.cancel") {
|
|
621
|
+
finalizeInterruptedReviewEvidence(stateDir, "cancelled", signal);
|
|
622
|
+
markTerminalProgress(stateDir, "cancelled");
|
|
623
|
+
}
|
|
624
|
+
return { stopped: true, pid, signal, ...signalResult, state_dir: stateDir };
|
|
507
625
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
markTerminalProgress(stateDir, "cancelled");
|
|
626
|
+
finally {
|
|
627
|
+
releaseControlLock();
|
|
511
628
|
}
|
|
512
|
-
return { stopped: true, pid, signal, ...signalResult, state_dir: stateDir };
|
|
513
629
|
}
|
|
514
630
|
export function markRunTerminalNotificationHandled(stateDir, status) {
|
|
515
631
|
markTerminalHandled(stateDir, {
|
|
@@ -517,8 +633,22 @@ export function markRunTerminalNotificationHandled(stateDir, status) {
|
|
|
517
633
|
status,
|
|
518
634
|
});
|
|
519
635
|
}
|
|
520
|
-
export function
|
|
521
|
-
const
|
|
636
|
+
export function recordRunTerminalDeliveryFailure(stateDir, status, error) {
|
|
637
|
+
const path = join(stateDir, "terminal-delivery-failure.json");
|
|
638
|
+
const previous = readJson(path);
|
|
639
|
+
const message = (error instanceof Error ? error.message : String(error))
|
|
640
|
+
.replaceAll(/\s+/g, " ")
|
|
641
|
+
.trim()
|
|
642
|
+
.slice(0, 500);
|
|
643
|
+
writeJsonAtomic(path, {
|
|
644
|
+
attempts: Math.max(0, Number(previous?.attempts ?? 0)) + 1,
|
|
645
|
+
error: message || "unknown delivery failure",
|
|
646
|
+
status,
|
|
647
|
+
ts: new Date().toISOString(),
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
export function cancelRun(runOrDir, expected = {}) {
|
|
651
|
+
const result = stopRun(runOrDir, "SIGTERM", "run.cancel", expected);
|
|
522
652
|
return Object.hasOwn(result, "stopped")
|
|
523
653
|
? { cancelled: result.stopped, ...result }
|
|
524
654
|
: result;
|
|
@@ -536,8 +666,8 @@ export function archiveRun(runOrDir) {
|
|
|
536
666
|
export function pruneRun(runOrDir, options = {}) {
|
|
537
667
|
return pruneTerminalRun(assertTerminalRun(runOrDir), options);
|
|
538
668
|
}
|
|
539
|
-
export function killRun(runOrDir) {
|
|
540
|
-
const result = stopRun(runOrDir, "SIGKILL", "run.kill");
|
|
669
|
+
export function killRun(runOrDir, expected = {}) {
|
|
670
|
+
const result = stopRun(runOrDir, "SIGKILL", "run.kill", expected);
|
|
541
671
|
return Object.hasOwn(result, "stopped")
|
|
542
672
|
? { killed: result.stopped, ...result }
|
|
543
673
|
: result;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Automatic recipe-review runtime composition.
|
|
3
|
+
* Zones: session-bound review schedulers, silent reviewer launch adapters, review controls
|
|
4
|
+
* Owns automatic-review lifecycle wiring without owning review decisions or filesystem transactions.
|
|
5
|
+
*/
|
|
6
|
+
import type * as Pi from "./pi.ts";
|
|
7
|
+
export interface AutomaticReviewRuntime {
|
|
8
|
+
close(): void;
|
|
9
|
+
handleMessage(type: string, body: unknown): Record<string, unknown>;
|
|
10
|
+
schedule(): void;
|
|
11
|
+
start(ctx: Pi.ExtensionContext): void;
|
|
12
|
+
}
|
|
13
|
+
export interface AutomaticReviewRuntimeDeps {
|
|
14
|
+
getActiveContext(): Pi.ExtensionContext | undefined;
|
|
15
|
+
getRunOwnerId(ctx: Pi.ExtensionContext): string;
|
|
16
|
+
getThinkingLevel(): unknown;
|
|
17
|
+
}
|
|
18
|
+
export declare function createAutomaticReviewRuntime(deps: AutomaticReviewRuntimeDeps): AutomaticReviewRuntime;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Automatic recipe-review runtime composition.
|
|
3
|
+
* Zones: session-bound review schedulers, silent reviewer launch adapters, review controls
|
|
4
|
+
* Owns automatic-review lifecycle wiring without owning review decisions or filesystem transactions.
|
|
5
|
+
*/
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import * as AsyncRuns from "./async-runs.js";
|
|
8
|
+
import * as DraftSleep from "./draft-sleep.js";
|
|
9
|
+
import * as ModelContext from "./model-context.js";
|
|
10
|
+
import * as Paths from "./paths.js";
|
|
11
|
+
import * as ReviewControl from "./review-control.js";
|
|
12
|
+
import * as ToolReviewScheduler from "./tool-review-scheduler.js";
|
|
13
|
+
export function createAutomaticReviewRuntime(deps) {
|
|
14
|
+
let draftScheduler;
|
|
15
|
+
let toolScheduler;
|
|
16
|
+
const close = () => {
|
|
17
|
+
draftScheduler?.close();
|
|
18
|
+
draftScheduler = undefined;
|
|
19
|
+
toolScheduler?.close();
|
|
20
|
+
toolScheduler = undefined;
|
|
21
|
+
};
|
|
22
|
+
const hasActiveActors = () => AsyncRuns.listRuns(Paths.EXTENSION_RUNTIME_PATHS.runStateRoot, "running").length > 0;
|
|
23
|
+
const policyValues = (ctx) => ModelContext.withCurrentModelValues({}, {
|
|
24
|
+
...ctx,
|
|
25
|
+
getThinkingLevel: deps.getThinkingLevel,
|
|
26
|
+
});
|
|
27
|
+
return {
|
|
28
|
+
close,
|
|
29
|
+
handleMessage(type, body) {
|
|
30
|
+
if (type !== "review.retry" && type !== "review.reset") {
|
|
31
|
+
throw new Error("tool:pi-actors accepts review.retry or review.reset messages.");
|
|
32
|
+
}
|
|
33
|
+
if (type === "review.retry" && !Paths.isAutomaticRecipeReviewEnabled()) {
|
|
34
|
+
throw new Error("Automatic recipe review is disabled by PI_ACTORS_AUTOMATIC_REVIEW.");
|
|
35
|
+
}
|
|
36
|
+
return ReviewControl.controlAutomaticReview(type, ReviewControl.parseAutomaticReviewScope(body), {
|
|
37
|
+
scheduleDraft: () => draftScheduler?.schedule(),
|
|
38
|
+
scheduleTool: () => toolScheduler?.schedule(),
|
|
39
|
+
});
|
|
40
|
+
},
|
|
41
|
+
schedule() {
|
|
42
|
+
draftScheduler?.schedule();
|
|
43
|
+
toolScheduler?.schedule();
|
|
44
|
+
},
|
|
45
|
+
start(ctx) {
|
|
46
|
+
close();
|
|
47
|
+
if (!Paths.isAutomaticRecipeReviewEnabled())
|
|
48
|
+
return;
|
|
49
|
+
ToolReviewScheduler.applyApprovedToolReviewAtSessionBoundary({
|
|
50
|
+
recipeRoot: Paths.getRecipeRoot(),
|
|
51
|
+
});
|
|
52
|
+
draftScheduler = DraftSleep.createDraftSleepScheduler({
|
|
53
|
+
hasActiveActors,
|
|
54
|
+
launch: (batch) => {
|
|
55
|
+
if (deps.getActiveContext() !== ctx) {
|
|
56
|
+
throw new Error("Draft review session changed before launch.");
|
|
57
|
+
}
|
|
58
|
+
return AsyncRuns.startRun({
|
|
59
|
+
file: join(Paths.getPackagedRecipeRoot(), "draft-review.json"),
|
|
60
|
+
launch_source: "tool",
|
|
61
|
+
notification_policy: "silent",
|
|
62
|
+
ownerId: deps.getRunOwnerId(ctx),
|
|
63
|
+
policy_values: policyValues(ctx),
|
|
64
|
+
run_id: DraftSleep.draftSleepRunId(batch.batchId),
|
|
65
|
+
values: { input_path: batch.reviewerInputPath },
|
|
66
|
+
}, ctx.cwd);
|
|
67
|
+
},
|
|
68
|
+
process: (state) => DraftSleep.processDraftSleepReview(state, {
|
|
69
|
+
getRunStatus: AsyncRuns.getRunStatus,
|
|
70
|
+
recipeRoot: Paths.getRecipeRoot(),
|
|
71
|
+
}),
|
|
72
|
+
});
|
|
73
|
+
toolScheduler = ToolReviewScheduler.createToolReviewScheduler({
|
|
74
|
+
hasActiveActors,
|
|
75
|
+
launch: (batch) => {
|
|
76
|
+
if (deps.getActiveContext() !== ctx) {
|
|
77
|
+
throw new Error("Tool review session changed before launch.");
|
|
78
|
+
}
|
|
79
|
+
return AsyncRuns.startRun({
|
|
80
|
+
file: join(Paths.getPackagedRecipeRoot(), "tool-review.json"),
|
|
81
|
+
launch_source: "tool",
|
|
82
|
+
notification_policy: "silent",
|
|
83
|
+
ownerId: deps.getRunOwnerId(ctx),
|
|
84
|
+
policy_values: policyValues(ctx),
|
|
85
|
+
run_id: ToolReviewScheduler.toolReviewRunId(batch.reviewId),
|
|
86
|
+
values: { input_path: batch.reviewerInputPath },
|
|
87
|
+
}, ctx.cwd);
|
|
88
|
+
},
|
|
89
|
+
process: (state) => ToolReviewScheduler.processToolReviewResult(state, {
|
|
90
|
+
getRunStatus: AsyncRuns.getRunStatus,
|
|
91
|
+
recipeRoot: Paths.getRecipeRoot(),
|
|
92
|
+
}),
|
|
93
|
+
});
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
@@ -344,7 +344,7 @@ export function splitCommandTemplate(input) {
|
|
|
344
344
|
let active = false;
|
|
345
345
|
for (const char of input) {
|
|
346
346
|
if (escaped) {
|
|
347
|
-
current += char
|
|
347
|
+
current += /[\s'"\\]/u.test(char) ? char : `\\${char}`;
|
|
348
348
|
escaped = false;
|
|
349
349
|
active = true;
|
|
350
350
|
continue;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic draft consolidation transaction and crash recovery.
|
|
3
|
+
* Owns: cycle claim, source/target compare-and-swap, complete recipe persistence, source quarantine, journal transitions, rollback, and runtime evidence.
|
|
4
|
+
*/
|
|
5
|
+
import type { DraftConsolidationPlan } from "./draft-consolidation.ts";
|
|
6
|
+
import { type DraftConsolidationInventoryItem } from "./draft-consolidation.ts";
|
|
7
|
+
export type DraftConsolidationTransactionPhase = "prepared" | "targets_validated" | "sources_quarantined" | "committed_evidence_pending" | "committed" | "rolled_back" | "rollback_required";
|
|
8
|
+
interface TargetOperation {
|
|
9
|
+
backupPath?: string;
|
|
10
|
+
expectedSha256: string | null;
|
|
11
|
+
intendedSha256: string;
|
|
12
|
+
path: string;
|
|
13
|
+
recipe: Record<string, unknown>;
|
|
14
|
+
target: string;
|
|
15
|
+
}
|
|
16
|
+
interface SourceOperation {
|
|
17
|
+
path: string;
|
|
18
|
+
quarantinePath: string;
|
|
19
|
+
sha256: string;
|
|
20
|
+
}
|
|
21
|
+
interface ConsolidationRootIdentity {
|
|
22
|
+
dev: string;
|
|
23
|
+
ino: string;
|
|
24
|
+
path: string;
|
|
25
|
+
realpath: string;
|
|
26
|
+
}
|
|
27
|
+
interface ConsolidationRootIdentities {
|
|
28
|
+
cycleDir: ConsolidationRootIdentity;
|
|
29
|
+
draftRoot: ConsolidationRootIdentity;
|
|
30
|
+
recipeRoot: ConsolidationRootIdentity;
|
|
31
|
+
}
|
|
32
|
+
export interface DraftConsolidationJournal {
|
|
33
|
+
cycleId: string;
|
|
34
|
+
error?: string;
|
|
35
|
+
operationsSha256: string;
|
|
36
|
+
phase: DraftConsolidationTransactionPhase;
|
|
37
|
+
plan: DraftConsolidationPlan;
|
|
38
|
+
planSha256: string;
|
|
39
|
+
roots: ConsolidationRootIdentities;
|
|
40
|
+
sourceScope: "batch" | "complete";
|
|
41
|
+
sources: SourceOperation[];
|
|
42
|
+
targets: TargetOperation[];
|
|
43
|
+
updatedAt: string;
|
|
44
|
+
}
|
|
45
|
+
export type DraftConsolidationCheckpoint = "prepared" | "target_written" | "targets_validated" | "source_quarantined" | "sources_quarantined" | "committed_evidence_pending" | "evidence_written";
|
|
46
|
+
export interface DraftConsolidationTransactionOptions {
|
|
47
|
+
checkpoint?(point: DraftConsolidationCheckpoint): void;
|
|
48
|
+
cycleDir: string;
|
|
49
|
+
draftRoot: string;
|
|
50
|
+
inventory: DraftConsolidationInventoryItem[];
|
|
51
|
+
onLockContention?(): void;
|
|
52
|
+
sourceScope?: "batch" | "complete";
|
|
53
|
+
recipeRoot: string;
|
|
54
|
+
}
|
|
55
|
+
export interface DraftConsolidationTransactionResult {
|
|
56
|
+
cycleId: string;
|
|
57
|
+
evidencePath: string;
|
|
58
|
+
phase: "committed" | "rolled_back";
|
|
59
|
+
plan: DraftConsolidationPlan;
|
|
60
|
+
sources: number;
|
|
61
|
+
targets: number;
|
|
62
|
+
}
|
|
63
|
+
export declare function applyDraftConsolidationPlan(plan: DraftConsolidationPlan, options: DraftConsolidationTransactionOptions): DraftConsolidationTransactionResult;
|
|
64
|
+
export declare function recoverDraftConsolidationCycle(options: Pick<DraftConsolidationTransactionOptions, "cycleDir" | "draftRoot" | "recipeRoot">): DraftConsolidationTransactionResult;
|
|
65
|
+
export {};
|