@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/lib/async-runs.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
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
|
|
|
6
6
|
import { spawn } from "node:child_process";
|
|
7
|
+
import { randomUUID } from "node:crypto";
|
|
7
8
|
import {
|
|
8
9
|
closeSync,
|
|
9
10
|
existsSync,
|
|
@@ -15,7 +16,7 @@ import {
|
|
|
15
16
|
statSync,
|
|
16
17
|
writeFileSync,
|
|
17
18
|
} from "node:fs";
|
|
18
|
-
import { basename, dirname, extname, join, relative, resolve } from "node:path";
|
|
19
|
+
import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
|
|
19
20
|
import { fileURLToPath } from "node:url";
|
|
20
21
|
|
|
21
22
|
import type {
|
|
@@ -60,6 +61,7 @@ import {
|
|
|
60
61
|
} from "./runs-process.ts";
|
|
61
62
|
import * as RunsStart from "./runs-start.ts";
|
|
62
63
|
import * as RunsIndex from "./runs-index.ts";
|
|
64
|
+
import * as RunsParentTeardown from "./runs-parent-teardown.ts";
|
|
63
65
|
import {
|
|
64
66
|
claimRunInboxMessageInStateDir,
|
|
65
67
|
parseRunInboxLine,
|
|
@@ -75,7 +77,12 @@ import {
|
|
|
75
77
|
deliverRunMessage,
|
|
76
78
|
type SendRunMessageOptions,
|
|
77
79
|
} from "./runs-messages.ts";
|
|
78
|
-
import {
|
|
80
|
+
import {
|
|
81
|
+
buildRunStatus,
|
|
82
|
+
tailFile,
|
|
83
|
+
tailLines,
|
|
84
|
+
type AsyncRunStatus,
|
|
85
|
+
} from "./runs-status.ts";
|
|
79
86
|
import { readJsonFileResilient } from "./state-readers.ts";
|
|
80
87
|
|
|
81
88
|
const RUNNER_IDENTITY_GRACE_MS = 5000;
|
|
@@ -87,11 +94,41 @@ export interface AsyncRunControlEndpoint {
|
|
|
87
94
|
type: "fifo" | "mailbox" | "named-pipe";
|
|
88
95
|
}
|
|
89
96
|
|
|
97
|
+
export function normalizeRunTransportContext(
|
|
98
|
+
value: unknown,
|
|
99
|
+
): Record<string, string | number | boolean> | undefined {
|
|
100
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
101
|
+
const normalized: Record<string, string | number | boolean> = {};
|
|
102
|
+
for (const [key, item] of Object.entries(
|
|
103
|
+
value as Record<string, unknown>,
|
|
104
|
+
).slice(0, 16)) {
|
|
105
|
+
const safeKey = key.trim().slice(0, 64);
|
|
106
|
+
if (!safeKey) continue;
|
|
107
|
+
if (typeof item === "string") {
|
|
108
|
+
normalized[safeKey] = item.trim().slice(0, 256);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (typeof item === "number" && Number.isFinite(item)) {
|
|
112
|
+
normalized[safeKey] = item;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (typeof item === "boolean") normalized[safeKey] = item;
|
|
116
|
+
}
|
|
117
|
+
return Object.keys(normalized).length ? normalized : undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
90
120
|
export interface AsyncRunStartParams {
|
|
91
121
|
async?: boolean;
|
|
92
122
|
control?: AsyncRunControlEndpoint;
|
|
93
123
|
file?: string;
|
|
94
124
|
launch_source?: AsyncRunLaunchSource;
|
|
125
|
+
lifecycleHooks?: {
|
|
126
|
+
onLockContention?(): void;
|
|
127
|
+
};
|
|
128
|
+
launch_correlation?: {
|
|
129
|
+
correlation_id?: string;
|
|
130
|
+
tool_call_id?: string;
|
|
131
|
+
};
|
|
95
132
|
name?: string;
|
|
96
133
|
ownerId?: string;
|
|
97
134
|
run_id?: string;
|
|
@@ -111,10 +148,12 @@ export interface AsyncRunStartParams {
|
|
|
111
148
|
output?: string;
|
|
112
149
|
artifacts?: Record<string, RunArtifactDeclaration>;
|
|
113
150
|
mailbox?: RecipesReferences.TemplateRecipeMailbox;
|
|
151
|
+
notification_policy?: "normal" | "silent";
|
|
114
152
|
retire_when?: "children_terminal";
|
|
115
153
|
retry?: number | string;
|
|
116
154
|
failure?: CommandTemplateFailureScope;
|
|
117
155
|
recover?: CommandTemplateValue;
|
|
156
|
+
transport_context?: Record<string, unknown>;
|
|
118
157
|
repeat?: number;
|
|
119
158
|
values?: Record<string, unknown>;
|
|
120
159
|
policy_values?: Record<string, unknown>;
|
|
@@ -122,24 +161,23 @@ export interface AsyncRunStartParams {
|
|
|
122
161
|
cwd?: string;
|
|
123
162
|
}
|
|
124
163
|
|
|
125
|
-
export type AsyncRunStatus
|
|
126
|
-
| "running"
|
|
127
|
-
| "done"
|
|
128
|
-
| "failed"
|
|
129
|
-
| "exited"
|
|
130
|
-
| "cancelled"
|
|
131
|
-
| "killed";
|
|
164
|
+
export type { AsyncRunStatus } from "./runs-status.ts";
|
|
132
165
|
|
|
133
166
|
export interface AsyncRunMeta {
|
|
134
167
|
argv: string[];
|
|
135
168
|
createdAt: string;
|
|
136
169
|
cwd: string;
|
|
137
170
|
launch_source?: AsyncRunLaunchSource;
|
|
171
|
+
launch_correlation?: {
|
|
172
|
+
correlation_id?: string;
|
|
173
|
+
tool_call_id?: string;
|
|
174
|
+
};
|
|
138
175
|
ownerId?: string;
|
|
139
176
|
pid: number;
|
|
140
177
|
recipe?: string;
|
|
141
178
|
recipe_file?: string;
|
|
142
179
|
run: string;
|
|
180
|
+
run_instance_id: string;
|
|
143
181
|
state_dir: string;
|
|
144
182
|
status: AsyncRunStatus;
|
|
145
183
|
tool?: string;
|
|
@@ -149,9 +187,11 @@ export interface AsyncRunMeta {
|
|
|
149
187
|
control?: AsyncRunControlEndpoint;
|
|
150
188
|
mailbox?: RecipesReferences.TemplateRecipeMailbox;
|
|
151
189
|
model_policy?: CurrentPolicyProvenance;
|
|
190
|
+
notification_policy?: "normal" | "silent";
|
|
152
191
|
process_identity?: RunProcessIdentity;
|
|
153
192
|
recipe_context_records?: RecipesReferences.TemplateRecipeContextRecord[];
|
|
154
193
|
retire_when?: "children_terminal";
|
|
194
|
+
transport_context?: Record<string, unknown>;
|
|
155
195
|
}
|
|
156
196
|
|
|
157
197
|
const DEFAULT_STATE_ROOT = Paths.getRunStateRoot();
|
|
@@ -235,7 +275,8 @@ function resolveRecipeFile(file: string): string {
|
|
|
235
275
|
function isMutableUsageRecipeFile(file: string): boolean {
|
|
236
276
|
const userRoot = resolve(DEFAULT_RECIPE_ROOT);
|
|
237
277
|
const resolved = resolve(file);
|
|
238
|
-
|
|
278
|
+
const relation = relative(userRoot, resolved);
|
|
279
|
+
return relation !== "" && !relation.startsWith("..") && !isAbsolute(relation);
|
|
239
280
|
}
|
|
240
281
|
|
|
241
282
|
function readRecipeFile(file: string): AsyncRunStartParams {
|
|
@@ -411,8 +452,11 @@ function assertCurrentPlaceholderReferencesResolved(
|
|
|
411
452
|
}
|
|
412
453
|
}
|
|
413
454
|
|
|
414
|
-
function acquireStateStartLock(
|
|
415
|
-
|
|
455
|
+
function acquireStateStartLock(
|
|
456
|
+
stateDir: string,
|
|
457
|
+
options: import("./file-state.ts").FileMutationLockOptions = {},
|
|
458
|
+
): () => void {
|
|
459
|
+
return RunsStart.acquireStateStartLock(stateDir, options);
|
|
416
460
|
}
|
|
417
461
|
|
|
418
462
|
function prepareStateDirForStart(stateDir: string): void {
|
|
@@ -448,7 +492,9 @@ export function startRun(
|
|
|
448
492
|
);
|
|
449
493
|
assertNoActiveRunState(stateDir);
|
|
450
494
|
mkdirSync(stateDir, { recursive: true });
|
|
451
|
-
const releaseStartLock = acquireStateStartLock(stateDir
|
|
495
|
+
const releaseStartLock = acquireStateStartLock(stateDir, {
|
|
496
|
+
onContention: startParams.lifecycleHooks?.onLockContention,
|
|
497
|
+
});
|
|
452
498
|
try {
|
|
453
499
|
claimRunStateDirectory(stateDir, run);
|
|
454
500
|
assertNoActiveRunState(stateDir);
|
|
@@ -466,11 +512,17 @@ export function startRun(
|
|
|
466
512
|
recipeFile && includeActorRecipeContext
|
|
467
513
|
? RecipesReferences.buildRecipeContextRecords(recipeFile)
|
|
468
514
|
: undefined;
|
|
469
|
-
if (
|
|
470
|
-
|
|
515
|
+
if (
|
|
516
|
+
recipeFile &&
|
|
517
|
+
isMutableUsageRecipeFile(recipeFile) &&
|
|
518
|
+
!RecipesUsage.recordRecipeLaunch(
|
|
471
519
|
recipeFile,
|
|
472
520
|
new Date(),
|
|
473
521
|
startParams.launch_source === "tool" ? "tool" : "spawn",
|
|
522
|
+
)
|
|
523
|
+
) {
|
|
524
|
+
throw new Error(
|
|
525
|
+
`Recipe launch rejected because its source changed during activation: ${recipeFile}. Reload recipe tools and retry.`,
|
|
474
526
|
);
|
|
475
527
|
}
|
|
476
528
|
const outFd = openSync(stdout, "a");
|
|
@@ -480,6 +532,9 @@ export function startRun(
|
|
|
480
532
|
...(startParams.defaults || {}),
|
|
481
533
|
...values,
|
|
482
534
|
};
|
|
535
|
+
const transportContext = normalizeRunTransportContext(
|
|
536
|
+
startParams.transport_context,
|
|
537
|
+
);
|
|
483
538
|
const artifacts = resolveArtifactPaths(startParams.artifacts, outputValues);
|
|
484
539
|
const meta: AsyncRunMeta = {
|
|
485
540
|
argv: [process.execPath, ...argv],
|
|
@@ -488,11 +543,14 @@ export function startRun(
|
|
|
488
543
|
...(startParams.launch_source
|
|
489
544
|
? { launch_source: startParams.launch_source }
|
|
490
545
|
: {}),
|
|
546
|
+
...(startParams.launch_correlation
|
|
547
|
+
? { launch_correlation: startParams.launch_correlation } : {}),
|
|
491
548
|
...(startParams.ownerId ? { ownerId: startParams.ownerId } : {}),
|
|
492
549
|
pid: 0,
|
|
493
550
|
...(recipe ? { recipe } : {}),
|
|
494
551
|
...(recipeFile ? { recipe_file: recipeFile } : {}),
|
|
495
552
|
run,
|
|
553
|
+
run_instance_id: randomUUID(),
|
|
496
554
|
state_dir: stateDir,
|
|
497
555
|
status: "running",
|
|
498
556
|
...(startParams.tool ? { tool: startParams.tool } : {}),
|
|
@@ -502,12 +560,17 @@ export function startRun(
|
|
|
502
560
|
...(artifacts ? { artifacts } : {}),
|
|
503
561
|
...(startParams.control ? { control: startParams.control } : {}),
|
|
504
562
|
...(startParams.mailbox ? { mailbox: startParams.mailbox } : {}),
|
|
563
|
+
...(startParams.notification_policy === "silent"
|
|
564
|
+
? { notification_policy: "silent" as const }
|
|
565
|
+
: {}),
|
|
505
566
|
...(recipeContextRecords && recipeContextRecords.length > 0
|
|
506
567
|
? { recipe_context_records: recipeContextRecords }
|
|
507
568
|
: {}),
|
|
508
569
|
...(startParams.retire_when === "children_terminal"
|
|
509
570
|
? { retire_when: "children_terminal" as const }
|
|
510
571
|
: {}),
|
|
572
|
+
...(transportContext
|
|
573
|
+
? { transport_context: transportContext } : {}),
|
|
511
574
|
};
|
|
512
575
|
writeJsonAtomic(join(stateDir, "run.json"), meta);
|
|
513
576
|
const child = spawn(process.execPath, argv, {
|
|
@@ -535,7 +598,7 @@ export function startRun(
|
|
|
535
598
|
});
|
|
536
599
|
writeFileSync(
|
|
537
600
|
join(stateDir, "events.jsonl"),
|
|
538
|
-
`${JSON.stringify({ event: "run.start", run, pid: meta.pid, ts: new Date().toISOString() })}\n`,
|
|
601
|
+
`${JSON.stringify({ event: "run.start", run, run_instance_id: meta.run_instance_id, pid: meta.pid, ts: new Date().toISOString() })}\n`,
|
|
539
602
|
{ flag: "a" },
|
|
540
603
|
);
|
|
541
604
|
child.unref();
|
|
@@ -552,12 +615,16 @@ export type {
|
|
|
552
615
|
RunOutboxLevel,
|
|
553
616
|
} from "./runs-outbox.ts";
|
|
554
617
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
618
|
+
function resolveRunStateDir(runOrDir: string): string {
|
|
619
|
+
return resolve(
|
|
620
|
+
/[\\/]/u.test(runOrDir)
|
|
558
621
|
? runOrDir
|
|
559
622
|
: join(DEFAULT_STATE_ROOT, safeRunId(runOrDir)),
|
|
560
623
|
);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
export function getRunStatus(runOrDir: string): Record<string, unknown> {
|
|
627
|
+
const stateDir = resolveRunStateDir(runOrDir);
|
|
561
628
|
const meta = readJson(join(stateDir, "run.json"));
|
|
562
629
|
if (!meta) throw new Error(`Run not found: ${runOrDir}`);
|
|
563
630
|
return buildRunStatus(
|
|
@@ -574,10 +641,8 @@ export type { RunStateIndexEntry } from "./runs-index.ts";
|
|
|
574
641
|
|
|
575
642
|
export function listRunStateDirs(
|
|
576
643
|
stateRoot = DEFAULT_STATE_ROOT,
|
|
577
|
-
depth = 0,
|
|
578
|
-
seen = new Set<string>(),
|
|
579
644
|
): string[] {
|
|
580
|
-
return RunsIndex.listRunStateDirs(stateRoot
|
|
645
|
+
return RunsIndex.listRunStateDirs(stateRoot);
|
|
581
646
|
}
|
|
582
647
|
|
|
583
648
|
export function rebuildRunStateIndex(
|
|
@@ -599,6 +664,103 @@ export function listRuns(
|
|
|
599
664
|
return RunsIndex.listRuns(stateRoot, getRunStatus, readJson, statusFilter);
|
|
600
665
|
}
|
|
601
666
|
|
|
667
|
+
export type {
|
|
668
|
+
ParentRunTeardownAttempt,
|
|
669
|
+
ParentRunTeardownDiscoveryFailure,
|
|
670
|
+
ParentRunTeardownResult,
|
|
671
|
+
} from "./runs-parent-teardown.ts";
|
|
672
|
+
|
|
673
|
+
export interface ParentRunsTeardownSummaryResult
|
|
674
|
+
extends RunsParentTeardown.ParentRunTeardownResult {
|
|
675
|
+
summaryPath?: string;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
export function teardownRunsOwnedByParent(
|
|
679
|
+
ownerId: string | undefined,
|
|
680
|
+
stateRoot = DEFAULT_STATE_ROOT,
|
|
681
|
+
options: { trigger?: string } = {},
|
|
682
|
+
): ParentRunsTeardownSummaryResult {
|
|
683
|
+
const result = RunsParentTeardown.teardownParentRuns(ownerId, {
|
|
684
|
+
getRunStatus,
|
|
685
|
+
killRun,
|
|
686
|
+
listRunStatuses: () => {
|
|
687
|
+
const discovered = RunsIndex.discoverRunStateDirs(
|
|
688
|
+
stateRoot,
|
|
689
|
+
Number.POSITIVE_INFINITY,
|
|
690
|
+
);
|
|
691
|
+
const failures: RunsParentTeardown.ParentRunTeardownDiscoveryFailure[] =
|
|
692
|
+
discovered.issues.map((issue) => ({
|
|
693
|
+
path: issue.path,
|
|
694
|
+
reason: issue.reason.replaceAll("_", " "),
|
|
695
|
+
}));
|
|
696
|
+
const statuses = discovered.stateDirs.flatMap((stateDir) => {
|
|
697
|
+
try {
|
|
698
|
+
return [getRunStatus(stateDir)];
|
|
699
|
+
} catch (error) {
|
|
700
|
+
failures.push({
|
|
701
|
+
path: stateDir,
|
|
702
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
703
|
+
});
|
|
704
|
+
return [];
|
|
705
|
+
}
|
|
706
|
+
});
|
|
707
|
+
return { failures, statuses };
|
|
708
|
+
},
|
|
709
|
+
recordAttempt: (attempt) => {
|
|
710
|
+
const current = getRunStatus(attempt.stateDir);
|
|
711
|
+
if (
|
|
712
|
+
!attempt.runInstanceId ||
|
|
713
|
+
current.run_instance_id !== attempt.runInstanceId
|
|
714
|
+
) {
|
|
715
|
+
throw new Error("run generation changed before teardown evidence");
|
|
716
|
+
}
|
|
717
|
+
writeFileSync(
|
|
718
|
+
join(attempt.stateDir, "events.jsonl"),
|
|
719
|
+
`${JSON.stringify({
|
|
720
|
+
event: "run.parent_teardown",
|
|
721
|
+
ownerId: attempt.ownerId,
|
|
722
|
+
outcome: attempt.outcome,
|
|
723
|
+
reason: attempt.reason,
|
|
724
|
+
run: attempt.run,
|
|
725
|
+
run_instance_id: attempt.runInstanceId,
|
|
726
|
+
type: "control.kill",
|
|
727
|
+
trigger: options.trigger ?? "parent_shutdown",
|
|
728
|
+
ts: new Date().toISOString(),
|
|
729
|
+
})}\n`,
|
|
730
|
+
{ flag: "a" },
|
|
731
|
+
);
|
|
732
|
+
},
|
|
733
|
+
});
|
|
734
|
+
const summaryPath = join(
|
|
735
|
+
stateRoot,
|
|
736
|
+
"teardown",
|
|
737
|
+
`${Date.now()}-${randomUUID()}.json`,
|
|
738
|
+
);
|
|
739
|
+
try {
|
|
740
|
+
writeJsonAtomic(
|
|
741
|
+
summaryPath,
|
|
742
|
+
RunsParentTeardown.buildBoundedParentTeardownSummary(
|
|
743
|
+
result,
|
|
744
|
+
ownerId ?? "unknown",
|
|
745
|
+
options.trigger ?? "parent_shutdown",
|
|
746
|
+
new Date().toISOString(),
|
|
747
|
+
),
|
|
748
|
+
);
|
|
749
|
+
return { ...result, summaryPath };
|
|
750
|
+
} catch (error) {
|
|
751
|
+
const failure = {
|
|
752
|
+
path: summaryPath,
|
|
753
|
+
reason: `summary: ${error instanceof Error ? error.message : String(error)}`,
|
|
754
|
+
};
|
|
755
|
+
return {
|
|
756
|
+
...result,
|
|
757
|
+
discoveryFailed: result.discoveryFailed + 1,
|
|
758
|
+
discoveryFailures: [...result.discoveryFailures, failure],
|
|
759
|
+
failed: result.failed + 1,
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
602
764
|
export function tailRun(runOrDir: string, lines = 40): string {
|
|
603
765
|
const status = getRunStatus(runOrDir);
|
|
604
766
|
const stateDir = String(status.state_dir);
|
|
@@ -785,11 +947,11 @@ function finalizeInterruptedReviewEvidence(
|
|
|
785
947
|
return {
|
|
786
948
|
attempt: index + 1,
|
|
787
949
|
stdout: {
|
|
788
|
-
path: relative(stateDir, stdoutFile),
|
|
950
|
+
path: relative(stateDir, stdoutFile).replaceAll("\\", "/"),
|
|
789
951
|
bytes: existsSync(stdoutFile) ? statSync(stdoutFile).size : 0,
|
|
790
952
|
},
|
|
791
953
|
stderr: {
|
|
792
|
-
path: relative(stateDir, stderrFile),
|
|
954
|
+
path: relative(stateDir, stderrFile).replaceAll("\\", "/"),
|
|
793
955
|
bytes: existsSync(stderrFile) ? statSync(stderrFile).size : 0,
|
|
794
956
|
},
|
|
795
957
|
};
|
|
@@ -815,26 +977,58 @@ function finalizeInterruptedReviewEvidence(
|
|
|
815
977
|
});
|
|
816
978
|
}
|
|
817
979
|
|
|
980
|
+
export interface RunControlExpectation {
|
|
981
|
+
onLocked?(): void;
|
|
982
|
+
ownerId?: string;
|
|
983
|
+
runInstanceId?: string;
|
|
984
|
+
}
|
|
985
|
+
|
|
818
986
|
function stopRun(
|
|
819
987
|
runOrDir: string,
|
|
820
988
|
signal: NodeJS.Signals,
|
|
821
989
|
event: string,
|
|
990
|
+
expected: RunControlExpectation = {},
|
|
822
991
|
): Record<string, unknown> {
|
|
823
|
-
const
|
|
824
|
-
const
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
}
|
|
829
|
-
const identity = verifyRunProcessIdentity(
|
|
830
|
-
pid,
|
|
831
|
-
status.process_identity as RunProcessIdentity | undefined,
|
|
832
|
-
);
|
|
833
|
-
if (status.status === "exited") {
|
|
992
|
+
const stateDir = resolveRunStateDir(runOrDir);
|
|
993
|
+
const releaseControlLock = RunsStart.acquireStateStartLock(stateDir);
|
|
994
|
+
try {
|
|
995
|
+
expected.onLocked?.();
|
|
996
|
+
const status = getRunStatus(stateDir);
|
|
834
997
|
if (
|
|
835
|
-
|
|
836
|
-
|
|
998
|
+
expected.ownerId !== undefined &&
|
|
999
|
+
status.ownerId !== expected.ownerId
|
|
1000
|
+
) {
|
|
1001
|
+
return { stopped: false, reason: "ownership changed", status };
|
|
1002
|
+
}
|
|
1003
|
+
if (
|
|
1004
|
+
expected.runInstanceId !== undefined &&
|
|
1005
|
+
status.run_instance_id !== expected.runInstanceId
|
|
837
1006
|
) {
|
|
1007
|
+
return { stopped: false, reason: "run generation changed", status };
|
|
1008
|
+
}
|
|
1009
|
+
const pid = Number(status.pid || 0);
|
|
1010
|
+
if (status.status !== "running" && status.status !== "exited") {
|
|
1011
|
+
return { stopped: false, reason: "not running", status };
|
|
1012
|
+
}
|
|
1013
|
+
const identity = verifyRunProcessIdentity(
|
|
1014
|
+
pid,
|
|
1015
|
+
status.process_identity as RunProcessIdentity | undefined,
|
|
1016
|
+
);
|
|
1017
|
+
if (status.status === "exited") {
|
|
1018
|
+
if (
|
|
1019
|
+
identity.status === "owner_mismatch" ||
|
|
1020
|
+
identity.status === "unsupported_proof"
|
|
1021
|
+
) {
|
|
1022
|
+
return {
|
|
1023
|
+
stopped: false,
|
|
1024
|
+
reason: identity.status.replaceAll("_", " "),
|
|
1025
|
+
process_identity_status: identity.status,
|
|
1026
|
+
status,
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
return { stopped: false, reason: "not running", status };
|
|
1030
|
+
}
|
|
1031
|
+
if (!identity.valid) {
|
|
838
1032
|
return {
|
|
839
1033
|
stopped: false,
|
|
840
1034
|
reason: identity.status.replaceAll("_", " "),
|
|
@@ -842,32 +1036,29 @@ function stopRun(
|
|
|
842
1036
|
status,
|
|
843
1037
|
};
|
|
844
1038
|
}
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
finalizeInterruptedReviewEvidence(stateDir, "cancelled", signal);
|
|
868
|
-
markTerminalProgress(stateDir, "cancelled");
|
|
1039
|
+
const signalResult = signalOwnedRunProcess(
|
|
1040
|
+
pid,
|
|
1041
|
+
signal,
|
|
1042
|
+
status.process_identity as RunProcessIdentity,
|
|
1043
|
+
);
|
|
1044
|
+
writeFileSync(
|
|
1045
|
+
join(stateDir, "events.jsonl"),
|
|
1046
|
+
`${JSON.stringify({ event, pid, signal, ...signalResult, ts: new Date().toISOString() })}\n`,
|
|
1047
|
+
{ flag: "a" },
|
|
1048
|
+
);
|
|
1049
|
+
markTerminalHandled(stateDir, { event, signal });
|
|
1050
|
+
if (event === "run.kill") {
|
|
1051
|
+
finalizeInterruptedReviewEvidence(stateDir, "killed", signal);
|
|
1052
|
+
markTerminalProgress(stateDir, "killed");
|
|
1053
|
+
}
|
|
1054
|
+
if (event === "run.cancel") {
|
|
1055
|
+
finalizeInterruptedReviewEvidence(stateDir, "cancelled", signal);
|
|
1056
|
+
markTerminalProgress(stateDir, "cancelled");
|
|
1057
|
+
}
|
|
1058
|
+
return { stopped: true, pid, signal, ...signalResult, state_dir: stateDir };
|
|
1059
|
+
} finally {
|
|
1060
|
+
releaseControlLock();
|
|
869
1061
|
}
|
|
870
|
-
return { stopped: true, pid, signal, ...signalResult, state_dir: stateDir };
|
|
871
1062
|
}
|
|
872
1063
|
|
|
873
1064
|
export function markRunTerminalNotificationHandled(
|
|
@@ -880,8 +1071,30 @@ export function markRunTerminalNotificationHandled(
|
|
|
880
1071
|
});
|
|
881
1072
|
}
|
|
882
1073
|
|
|
883
|
-
export function
|
|
884
|
-
|
|
1074
|
+
export function recordRunTerminalDeliveryFailure(
|
|
1075
|
+
stateDir: string,
|
|
1076
|
+
status: string,
|
|
1077
|
+
error: unknown,
|
|
1078
|
+
): void {
|
|
1079
|
+
const path = join(stateDir, "terminal-delivery-failure.json");
|
|
1080
|
+
const previous = readJson(path);
|
|
1081
|
+
const message = (error instanceof Error ? error.message : String(error))
|
|
1082
|
+
.replaceAll(/\s+/g, " ")
|
|
1083
|
+
.trim()
|
|
1084
|
+
.slice(0, 500);
|
|
1085
|
+
writeJsonAtomic(path, {
|
|
1086
|
+
attempts: Math.max(0, Number(previous?.attempts ?? 0)) + 1,
|
|
1087
|
+
error: message || "unknown delivery failure",
|
|
1088
|
+
status,
|
|
1089
|
+
ts: new Date().toISOString(),
|
|
1090
|
+
});
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
export function cancelRun(
|
|
1094
|
+
runOrDir: string,
|
|
1095
|
+
expected: RunControlExpectation = {},
|
|
1096
|
+
): Record<string, unknown> {
|
|
1097
|
+
const result = stopRun(runOrDir, "SIGTERM", "run.cancel", expected);
|
|
885
1098
|
return Object.hasOwn(result, "stopped")
|
|
886
1099
|
? { cancelled: result.stopped, ...result }
|
|
887
1100
|
: result;
|
|
@@ -906,8 +1119,11 @@ export function pruneRun(
|
|
|
906
1119
|
return pruneTerminalRun(assertTerminalRun(runOrDir), options);
|
|
907
1120
|
}
|
|
908
1121
|
|
|
909
|
-
export function killRun(
|
|
910
|
-
|
|
1122
|
+
export function killRun(
|
|
1123
|
+
runOrDir: string,
|
|
1124
|
+
expected: RunControlExpectation = {},
|
|
1125
|
+
): Record<string, unknown> {
|
|
1126
|
+
const result = stopRun(runOrDir, "SIGKILL", "run.kill", expected);
|
|
911
1127
|
return Object.hasOwn(result, "stopped")
|
|
912
1128
|
? { killed: result.stopped, ...result }
|
|
913
1129
|
: result;
|
|
@@ -0,0 +1,135 @@
|
|
|
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
|
+
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
|
|
9
|
+
import * as AsyncRuns from "./async-runs.ts";
|
|
10
|
+
import * as DraftSleep from "./draft-sleep.ts";
|
|
11
|
+
import * as ModelContext from "./model-context.ts";
|
|
12
|
+
import * as Paths from "./paths.ts";
|
|
13
|
+
import type * as Pi from "./pi.ts";
|
|
14
|
+
import * as ReviewControl from "./review-control.ts";
|
|
15
|
+
import * as ToolReviewScheduler from "./tool-review-scheduler.ts";
|
|
16
|
+
|
|
17
|
+
export interface AutomaticReviewRuntime {
|
|
18
|
+
close(): void;
|
|
19
|
+
handleMessage(type: string, body: unknown): Record<string, unknown>;
|
|
20
|
+
schedule(): void;
|
|
21
|
+
start(ctx: Pi.ExtensionContext): void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface AutomaticReviewRuntimeDeps {
|
|
25
|
+
getActiveContext(): Pi.ExtensionContext | undefined;
|
|
26
|
+
getRunOwnerId(ctx: Pi.ExtensionContext): string;
|
|
27
|
+
getThinkingLevel(): unknown;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function createAutomaticReviewRuntime(
|
|
31
|
+
deps: AutomaticReviewRuntimeDeps,
|
|
32
|
+
): AutomaticReviewRuntime {
|
|
33
|
+
let draftScheduler: DraftSleep.DraftSleepScheduler | undefined;
|
|
34
|
+
let toolScheduler: ToolReviewScheduler.ToolReviewScheduler | undefined;
|
|
35
|
+
|
|
36
|
+
const close = (): void => {
|
|
37
|
+
draftScheduler?.close();
|
|
38
|
+
draftScheduler = undefined;
|
|
39
|
+
toolScheduler?.close();
|
|
40
|
+
toolScheduler = undefined;
|
|
41
|
+
};
|
|
42
|
+
const hasActiveActors = (): boolean =>
|
|
43
|
+
AsyncRuns.listRuns(Paths.EXTENSION_RUNTIME_PATHS.runStateRoot, "running").length > 0;
|
|
44
|
+
const policyValues = (ctx: Pi.ExtensionContext): Record<string, unknown> =>
|
|
45
|
+
ModelContext.withCurrentModelValues(
|
|
46
|
+
{},
|
|
47
|
+
{
|
|
48
|
+
...(ctx as ModelContext.CurrentModelContext),
|
|
49
|
+
getThinkingLevel: deps.getThinkingLevel,
|
|
50
|
+
},
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
close,
|
|
55
|
+
handleMessage(type, body) {
|
|
56
|
+
if (type !== "review.retry" && type !== "review.reset") {
|
|
57
|
+
throw new Error("tool:pi-actors accepts review.retry or review.reset messages.");
|
|
58
|
+
}
|
|
59
|
+
if (type === "review.retry" && !Paths.isAutomaticRecipeReviewEnabled()) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
"Automatic recipe review is disabled by PI_ACTORS_AUTOMATIC_REVIEW.",
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
return ReviewControl.controlAutomaticReview(
|
|
65
|
+
type,
|
|
66
|
+
ReviewControl.parseAutomaticReviewScope(body),
|
|
67
|
+
{
|
|
68
|
+
scheduleDraft: () => draftScheduler?.schedule(),
|
|
69
|
+
scheduleTool: () => toolScheduler?.schedule(),
|
|
70
|
+
},
|
|
71
|
+
);
|
|
72
|
+
},
|
|
73
|
+
schedule() {
|
|
74
|
+
draftScheduler?.schedule();
|
|
75
|
+
toolScheduler?.schedule();
|
|
76
|
+
},
|
|
77
|
+
start(ctx) {
|
|
78
|
+
close();
|
|
79
|
+
if (!Paths.isAutomaticRecipeReviewEnabled()) return;
|
|
80
|
+
ToolReviewScheduler.applyApprovedToolReviewAtSessionBoundary({
|
|
81
|
+
recipeRoot: Paths.getRecipeRoot(),
|
|
82
|
+
});
|
|
83
|
+
draftScheduler = DraftSleep.createDraftSleepScheduler({
|
|
84
|
+
hasActiveActors,
|
|
85
|
+
launch: (batch) => {
|
|
86
|
+
if (deps.getActiveContext() !== ctx) {
|
|
87
|
+
throw new Error("Draft review session changed before launch.");
|
|
88
|
+
}
|
|
89
|
+
return AsyncRuns.startRun(
|
|
90
|
+
{
|
|
91
|
+
file: join(Paths.getPackagedRecipeRoot(), "draft-review.json"),
|
|
92
|
+
launch_source: "tool",
|
|
93
|
+
notification_policy: "silent",
|
|
94
|
+
ownerId: deps.getRunOwnerId(ctx),
|
|
95
|
+
policy_values: policyValues(ctx),
|
|
96
|
+
run_id: DraftSleep.draftSleepRunId(batch.batchId),
|
|
97
|
+
values: { input_path: batch.reviewerInputPath },
|
|
98
|
+
},
|
|
99
|
+
ctx.cwd,
|
|
100
|
+
);
|
|
101
|
+
},
|
|
102
|
+
process: (state) =>
|
|
103
|
+
DraftSleep.processDraftSleepReview(state, {
|
|
104
|
+
getRunStatus: AsyncRuns.getRunStatus,
|
|
105
|
+
recipeRoot: Paths.getRecipeRoot(),
|
|
106
|
+
}),
|
|
107
|
+
});
|
|
108
|
+
toolScheduler = ToolReviewScheduler.createToolReviewScheduler({
|
|
109
|
+
hasActiveActors,
|
|
110
|
+
launch: (batch) => {
|
|
111
|
+
if (deps.getActiveContext() !== ctx) {
|
|
112
|
+
throw new Error("Tool review session changed before launch.");
|
|
113
|
+
}
|
|
114
|
+
return AsyncRuns.startRun(
|
|
115
|
+
{
|
|
116
|
+
file: join(Paths.getPackagedRecipeRoot(), "tool-review.json"),
|
|
117
|
+
launch_source: "tool",
|
|
118
|
+
notification_policy: "silent",
|
|
119
|
+
ownerId: deps.getRunOwnerId(ctx),
|
|
120
|
+
policy_values: policyValues(ctx),
|
|
121
|
+
run_id: ToolReviewScheduler.toolReviewRunId(batch.reviewId),
|
|
122
|
+
values: { input_path: batch.reviewerInputPath },
|
|
123
|
+
},
|
|
124
|
+
ctx.cwd,
|
|
125
|
+
);
|
|
126
|
+
},
|
|
127
|
+
process: (state) =>
|
|
128
|
+
ToolReviewScheduler.processToolReviewResult(state, {
|
|
129
|
+
getRunStatus: AsyncRuns.getRunStatus,
|
|
130
|
+
recipeRoot: Paths.getRecipeRoot(),
|
|
131
|
+
}),
|
|
132
|
+
});
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|