@llblab/pi-actors 0.43.0 → 0.43.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 +12 -6
- package/BACKLOG.md +189 -1
- package/CHANGELOG.md +180 -301
- package/README.md +9 -7
- package/dist/index.js +1 -1
- package/dist/lib/async-runs.d.ts +2 -1
- package/dist/lib/async-runs.js +16 -4
- package/dist/lib/automatic-review-runtime.d.ts +1 -1
- package/dist/lib/automatic-review-runtime.js +5 -5
- package/dist/lib/command-templates.d.ts +2 -0
- package/dist/lib/command-templates.js +38 -4
- package/dist/lib/control-projection.d.ts +20 -0
- package/dist/lib/control-projection.js +66 -0
- package/dist/lib/control.d.ts +3 -0
- package/dist/lib/control.js +27 -14
- package/dist/lib/draft-sleep.js +3 -3
- package/dist/lib/file-state.d.ts +1 -0
- package/dist/lib/file-state.js +98 -42
- package/dist/lib/inspector-overlay.d.ts +2 -0
- package/dist/lib/inspector-overlay.js +92 -53
- package/dist/lib/limits.d.ts +5 -3
- package/dist/lib/limits.js +5 -3
- package/dist/lib/prompts.d.ts +1 -1
- package/dist/lib/prompts.js +1 -1
- package/dist/lib/recipe-control.js +6 -2
- package/dist/lib/review-control.d.ts +1 -1
- package/dist/lib/review-control.js +4 -5
- package/dist/lib/runs-control-delivery.d.ts +8 -1
- package/dist/lib/runs-control-delivery.js +38 -15
- package/dist/lib/runs-controls.d.ts +2 -0
- package/dist/lib/runs-controls.js +5 -3
- package/dist/lib/runs-trace.d.ts +2 -2
- package/dist/lib/runs-trace.js +24 -20
- package/dist/lib/runtime-identity.d.ts +7 -0
- package/dist/lib/runtime-identity.js +35 -0
- package/dist/lib/runtime-triage.d.ts +29 -0
- package/dist/lib/runtime-triage.js +76 -0
- package/dist/lib/tool-review-scheduler.js +7 -7
- package/dist/lib/tools-inspect.js +53 -14
- package/dist/lib/tools-message.d.ts +1 -2
- package/dist/lib/tools-message.js +6 -6
- package/dist/lib/tools-response.d.ts +0 -1
- package/dist/lib/tools-response.js +0 -9
- package/dist/lib/tools.d.ts +1 -1
- package/dist/lib/tools.js +1 -1
- package/dist/lib/trace-projection.js +30 -10
- package/dist/scripts/locker.mjs +9 -16
- package/dist/scripts/music-player.mjs +7 -13
- package/dist/scripts/release-gates.mjs +33 -2
- package/dist/scripts/validate-recipe.mjs +5 -4
- package/dist/skills/actors/SKILL.md +12 -7
- package/dist/skills/swarm/SKILL.md +0 -2
- package/docs/0.43-baseline.md +18 -23
- package/docs/README.md +2 -4
- package/docs/actor-inspector.md +5 -5
- package/docs/async-runs.md +2 -2
- package/docs/command-templates.md +6 -116
- package/docs/recipe-library.md +2 -4
- package/docs/releasing.md +28 -0
- package/docs/template-recipes.md +1 -1
- package/docs/tool-registry.md +2 -2
- package/index.ts +1 -1
- package/lib/async-runs.ts +18 -5
- package/lib/automatic-review-runtime.ts +7 -7
- package/lib/command-templates.ts +44 -4
- package/lib/control-projection.ts +105 -0
- package/lib/control.ts +33 -18
- package/lib/draft-sleep.ts +3 -3
- package/lib/file-state.ts +68 -61
- package/lib/inspector-overlay.ts +93 -58
- package/lib/limits.ts +5 -3
- package/lib/prompts.ts +1 -1
- package/lib/recipe-control.ts +9 -2
- package/lib/review-control.ts +4 -5
- package/lib/runs-control-delivery.ts +45 -17
- package/lib/runs-controls.ts +12 -3
- package/lib/runs-trace.ts +23 -19
- package/lib/runtime-identity.ts +39 -0
- package/lib/runtime-triage.ts +120 -0
- package/lib/tool-review-scheduler.ts +7 -7
- package/lib/tools-inspect.ts +60 -16
- package/lib/tools-message.ts +7 -8
- package/lib/tools-response.ts +0 -12
- package/lib/tools.ts +4 -4
- package/lib/trace-projection.ts +33 -10
- package/package.json +1 -1
- package/scripts/locker.mjs +9 -16
- package/scripts/music-player.mjs +7 -13
- package/scripts/release-gates.mjs +33 -2
- package/scripts/validate-recipe.mjs +5 -4
- package/skills/actors/SKILL.md +12 -7
- package/skills/swarm/SKILL.md +0 -2
- package/docs/actors-deep-reference.md +0 -108
- package/docs/component-recipes.md +0 -45
- package/docs/task-first-recipes.md +0 -261
package/README.md
CHANGED
|
@@ -53,8 +53,10 @@ Send one exact Control:
|
|
|
53
53
|
Run targets accept only actions declared by the captured Recipe. Runtime targets accept only reserved review actions:
|
|
54
54
|
|
|
55
55
|
```text
|
|
56
|
-
message target=runtime action=review.retry
|
|
57
|
-
message target=runtime action=review.
|
|
56
|
+
message target=runtime action=review.retry input={"scope":"draft"}
|
|
57
|
+
message target=runtime action=review.retry input={"scope":"tool"}
|
|
58
|
+
message target=runtime action=review.reset input={"scope":"draft"}
|
|
59
|
+
message target=runtime action=review.reset input={"scope":"tool"}
|
|
58
60
|
```
|
|
59
61
|
|
|
60
62
|
Lifecycle `kill` remains runtime-owned rather than Recipe-declared.
|
|
@@ -67,9 +69,9 @@ Inspect one exact management target:
|
|
|
67
69
|
inspect target=run:test view=recipe
|
|
68
70
|
inspect target=run:test view=trace source=lifecycle lines=40
|
|
69
71
|
inspect target=run:test view=control
|
|
70
|
-
inspect target=runtime
|
|
71
|
-
inspect target=recipes
|
|
72
|
-
inspect target=tool:my_tool
|
|
72
|
+
inspect target=runtime view=status
|
|
73
|
+
inspect target=recipes view=status
|
|
74
|
+
inspect target=tool:my_tool view=status
|
|
73
75
|
```
|
|
74
76
|
|
|
75
77
|
A Run exposes exactly `recipe`, `trace`, and `control` views.
|
|
@@ -119,7 +121,7 @@ String command-template leaves execute directly without shell interpretation. Us
|
|
|
119
121
|
}
|
|
120
122
|
```
|
|
121
123
|
|
|
122
|
-
Trace fields are exact: `id`, `ts`, `kind`, and optional `summary`, `data`, `level`, `attention`. Address, sender, recipient, reply, and routing fields fail validation.
|
|
124
|
+
Trace fields are exact: `id`, `ts`, `kind`, and optional `summary`, `data`, `level`, `attention`. Address, sender, recipient, reply, and routing fields fail validation. The canonical append authority validates and size-checks under a cross-process mutation lock before one append-only JSONL write; first-party scripts never append this file directly.
|
|
123
125
|
|
|
124
126
|
Use `attention: "notify"` for visible status and `attention: "followup"` only when the coordinator needs semantic follow-up context. Store large evidence in artifacts or bounded execution captures.
|
|
125
127
|
|
|
@@ -138,7 +140,7 @@ Long-lived services publish `control-endpoint.json` only when ready:
|
|
|
138
140
|
}
|
|
139
141
|
```
|
|
140
142
|
|
|
141
|
-
Supported transports are Unix FIFO and Windows named pipe.
|
|
143
|
+
Supported transports are Unix FIFO and Windows named pipe. Every actor-local Control uses the same portable envelope: action is at most 64 lowercase ASCII characters, serialized JSON input is at most 380 bytes, and the newline-terminated wire record is at most 512 bytes. Put larger data in a declared artifact/path and send only its bounded reference or instruction through Control. Delivery revalidates owner, generation, state, and process identity under the canonical lifecycle lock.
|
|
142
144
|
|
|
143
145
|
## Run State
|
|
144
146
|
|
package/dist/index.js
CHANGED
|
@@ -106,7 +106,7 @@ export default function toolRegistryExtension(pi) {
|
|
|
106
106
|
configPath: Paths.EXTENSION_RUNTIME_PATHS.configPath,
|
|
107
107
|
getActiveTools: () => pi.getActiveTools(),
|
|
108
108
|
getRuntimeTool: (name) => Tools.resolveActiveRuntimeTool(name, runtime.getTools(), (activeName) => actorToolDefinitions.get(activeName)),
|
|
109
|
-
|
|
109
|
+
handleRuntimeControl: automaticReview.handleControl,
|
|
110
110
|
registryRuntime: runtime,
|
|
111
111
|
setActiveTools: (toolNames) => pi.setActiveTools(toolNames),
|
|
112
112
|
}).map(withCurrentThinkingContext));
|
package/dist/lib/async-runs.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { type CurrentPolicyProvenance } from "./model-context.ts";
|
|
|
7
7
|
import * as RecipesReferences from "./recipes-references.ts";
|
|
8
8
|
import { type RunArtifactDeclaration } from "./runs-artifacts.ts";
|
|
9
9
|
import { type RunProcessIdentity } from "./runs-process.ts";
|
|
10
|
+
import * as RuntimeIdentity from "./runtime-identity.ts";
|
|
10
11
|
import * as RunsIndex from "./runs-index.ts";
|
|
11
12
|
import * as RunsParentTeardown from "./runs-parent-teardown.ts";
|
|
12
13
|
import { type DeliverRunControlOptions, type DeliverRunControlRequest } from "./runs-control-delivery.ts";
|
|
@@ -77,7 +78,7 @@ export interface AsyncRunMeta {
|
|
|
77
78
|
run: string;
|
|
78
79
|
run_instance_id: string;
|
|
79
80
|
state_dir: string;
|
|
80
|
-
state_schema:
|
|
81
|
+
state_schema: typeof RuntimeIdentity.RUN_STATE_SCHEMA;
|
|
81
82
|
status: AsyncRunStatus;
|
|
82
83
|
tool?: string;
|
|
83
84
|
template: CommandTemplateValue;
|
package/dist/lib/async-runs.js
CHANGED
|
@@ -18,6 +18,7 @@ import { buildTerminalProgress, markTerminalHandled, signalOwnedRunProcess, } fr
|
|
|
18
18
|
import { claimRunStateDirectory } from "./runs-ownership.js";
|
|
19
19
|
import { appendRunRetentionEvidence, archiveTerminalRun, pruneTerminalRun, } from "./runs-retention.js";
|
|
20
20
|
import { captureRunProcessIdentity, verifyRunProcessIdentity, } from "./runs-process.js";
|
|
21
|
+
import * as RuntimeIdentity from "./runtime-identity.js";
|
|
21
22
|
import * as RunsStart from "./runs-start.js";
|
|
22
23
|
import { appendRunTraceEvent } from "./runs-trace.js";
|
|
23
24
|
import * as RunsIndex from "./runs-index.js";
|
|
@@ -236,7 +237,21 @@ export function startRun(params, cwd) {
|
|
|
236
237
|
const resolved = resolveRunTemplate(startParams);
|
|
237
238
|
const run = safeRunId(startParams.run_id);
|
|
238
239
|
const stateDir = resolveStateDir(startParams, run);
|
|
240
|
+
const recipeFile = startParams.file
|
|
241
|
+
? resolveRecipeFile(startParams.file)
|
|
242
|
+
: undefined;
|
|
243
|
+
const packagedRecipeRoot = resolve(Paths.getPackagedRecipeRoot());
|
|
244
|
+
const recipeRelation = recipeFile
|
|
245
|
+
? relative(packagedRecipeRoot, recipeFile)
|
|
246
|
+
: undefined;
|
|
247
|
+
const packagedRepo = startParams.defaults?.repo === "~/.pi/agent/extensions/pi-actors" &&
|
|
248
|
+
recipeRelation &&
|
|
249
|
+
!recipeRelation.startsWith("..") &&
|
|
250
|
+
!isAbsolute(recipeRelation)
|
|
251
|
+
? dirname(packagedRecipeRoot)
|
|
252
|
+
: undefined;
|
|
239
253
|
const values = {
|
|
254
|
+
...(packagedRepo ? { repo: packagedRepo } : {}),
|
|
240
255
|
...(startParams.values || {}),
|
|
241
256
|
run_id: run,
|
|
242
257
|
state_dir: stateDir,
|
|
@@ -259,9 +274,6 @@ export function startRun(params, cwd) {
|
|
|
259
274
|
prepareStateDirForStart(stateDir);
|
|
260
275
|
const stdout = join(stateDir, "stdout.log");
|
|
261
276
|
const stderr = join(stateDir, "stderr.log");
|
|
262
|
-
const recipeFile = startParams.file
|
|
263
|
-
? resolveRecipeFile(startParams.file)
|
|
264
|
-
: undefined;
|
|
265
277
|
const recipe = startParams.name || getRunIdFromFile(recipeFile);
|
|
266
278
|
const includeActorRecipeContext = startParams.actor_context !== false &&
|
|
267
279
|
startParams.actor_context !== "off";
|
|
@@ -298,7 +310,7 @@ export function startRun(params, cwd) {
|
|
|
298
310
|
run,
|
|
299
311
|
run_instance_id: randomUUID(),
|
|
300
312
|
state_dir: stateDir,
|
|
301
|
-
state_schema:
|
|
313
|
+
state_schema: RuntimeIdentity.RUN_STATE_SCHEMA,
|
|
302
314
|
status: "running",
|
|
303
315
|
...(startParams.tool ? { tool: startParams.tool } : {}),
|
|
304
316
|
template: resolved.template,
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import type * as Pi from "./pi.ts";
|
|
7
7
|
export interface AutomaticReviewRuntime {
|
|
8
8
|
close(): void;
|
|
9
|
-
|
|
9
|
+
handleControl(action: string, input: unknown): Record<string, unknown>;
|
|
10
10
|
schedule(): void;
|
|
11
11
|
start(ctx: Pi.ExtensionContext): void;
|
|
12
12
|
}
|
|
@@ -26,14 +26,14 @@ export function createAutomaticReviewRuntime(deps) {
|
|
|
26
26
|
});
|
|
27
27
|
return {
|
|
28
28
|
close,
|
|
29
|
-
|
|
30
|
-
if (
|
|
31
|
-
throw new Error("
|
|
29
|
+
handleControl(action, input) {
|
|
30
|
+
if (action !== "review.retry" && action !== "review.reset") {
|
|
31
|
+
throw new Error("runtime accepts review.retry or review.reset Controls.");
|
|
32
32
|
}
|
|
33
|
-
if (
|
|
33
|
+
if (action === "review.retry" && !Paths.isAutomaticRecipeReviewEnabled()) {
|
|
34
34
|
throw new Error("Automatic recipe review is disabled by PI_ACTORS_AUTOMATIC_REVIEW.");
|
|
35
35
|
}
|
|
36
|
-
return ReviewControl.controlAutomaticReview(
|
|
36
|
+
return ReviewControl.controlAutomaticReview(action, ReviewControl.parseAutomaticReviewScope(input), {
|
|
37
37
|
scheduleDraft: () => draftScheduler?.schedule(),
|
|
38
38
|
scheduleTool: () => toolScheduler?.schedule(),
|
|
39
39
|
});
|
|
@@ -74,6 +74,8 @@ export declare function getCommandTemplateRiskLabels(config: CommandTemplateConf
|
|
|
74
74
|
export declare function getCommandTemplateDefaults(config: CommandTemplateConfig | undefined): Record<string, string>;
|
|
75
75
|
export declare function splitCommandTemplate(input: string): string[];
|
|
76
76
|
export declare function expandCommandTemplateExecutable(command: string, cwd: string): string;
|
|
77
|
+
export type CommandTemplateExecutableResolver = (name: string) => string | undefined;
|
|
78
|
+
export declare function resolveCommandTemplateScriptInvocation(invocation: CommandTemplateInvocation, resolveExecutable?: CommandTemplateExecutableResolver): CommandTemplateInvocation;
|
|
77
79
|
export declare function shouldRunCommandTemplateNode(value: boolean | string | undefined, values: Record<string, unknown>): boolean;
|
|
78
80
|
export declare function substituteCommandTemplateToken(token: string, values: Record<string, unknown>, missingLabel?: string, depth?: number): string;
|
|
79
81
|
export declare function execCommandTemplate(command: string, args: string[], options?: CommandTemplateExecOptions): Promise<CommandTemplateExecResult>;
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
* Owns portable command-template parsing, expansion, risk checks, retries, timeouts, and direct execution.
|
|
5
5
|
*/
|
|
6
6
|
import { spawn } from "node:child_process";
|
|
7
|
-
import { appendFileSync, mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { accessSync, appendFileSync, constants, mkdirSync, mkdtempSync, statSync, writeFileSync } from "node:fs";
|
|
8
8
|
import { homedir, tmpdir } from "node:os";
|
|
9
|
-
import { isAbsolute, join, resolve as resolvePath } from "node:path";
|
|
9
|
+
import { basename, delimiter, extname, isAbsolute, join, resolve as resolvePath } from "node:path";
|
|
10
10
|
const DEFAULT_COMMAND_CAPTURE_LIMIT_BYTES = 1024 * 1024;
|
|
11
11
|
const COMMAND_TEMPLATE_RISK_LABEL_ORDER = [
|
|
12
12
|
"risk.shell",
|
|
@@ -135,7 +135,7 @@ function getLeafCommandTemplateRiskLabels(config) {
|
|
|
135
135
|
const command = getExecutableName(parts[0]);
|
|
136
136
|
const args = parts.slice(1);
|
|
137
137
|
const labels = new Set();
|
|
138
|
-
if (["bash", "sh", "zsh", "fish"].includes(command)) {
|
|
138
|
+
if (["bash", "sh", "zsh", "fish"].includes(command) || command.endsWith(".sh")) {
|
|
139
139
|
labels.add("risk.shell");
|
|
140
140
|
if (hasAnyFlag(args, ["-c"]))
|
|
141
141
|
labels.add("risk.eval");
|
|
@@ -393,6 +393,40 @@ export function expandCommandTemplateExecutable(command, cwd) {
|
|
|
393
393
|
return resolvePath(cwd, command);
|
|
394
394
|
return command;
|
|
395
395
|
}
|
|
396
|
+
function resolveExecutableOnPath(name) {
|
|
397
|
+
if (name === "node" && /^node(?:\.exe)?$/i.test(basename(process.execPath)))
|
|
398
|
+
return process.execPath;
|
|
399
|
+
const pathValue = Object.entries(process.env)
|
|
400
|
+
.find(([key]) => key.toLowerCase() === "path")?.[1] ?? "";
|
|
401
|
+
const suffixes = process.platform === "win32" ? [".exe", ".com"] : [""];
|
|
402
|
+
for (const entry of pathValue.split(delimiter).filter(Boolean)) {
|
|
403
|
+
const directory = entry.replace(/^"|"$/g, "");
|
|
404
|
+
for (const suffix of suffixes) {
|
|
405
|
+
const candidate = join(directory, `${name}${suffix}`);
|
|
406
|
+
try {
|
|
407
|
+
accessSync(candidate, constants.X_OK);
|
|
408
|
+
if (statSync(candidate).isFile())
|
|
409
|
+
return candidate;
|
|
410
|
+
}
|
|
411
|
+
catch { }
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return undefined;
|
|
415
|
+
}
|
|
416
|
+
export function resolveCommandTemplateScriptInvocation(invocation, resolveExecutable = resolveExecutableOnPath) {
|
|
417
|
+
const extension = extname(invocation.command).toLowerCase();
|
|
418
|
+
const interpreters = extension === ".js" || extension === ".mjs"
|
|
419
|
+
? [["node"], ["bun"], ["deno", "run"]]
|
|
420
|
+
: extension === ".sh" ? [["bash"]] : [];
|
|
421
|
+
if (interpreters.length === 0)
|
|
422
|
+
return invocation;
|
|
423
|
+
for (const [name, ...prefix] of interpreters) {
|
|
424
|
+
const executable = resolveExecutable(name);
|
|
425
|
+
if (executable)
|
|
426
|
+
return { command: executable, args: [...prefix, invocation.command, ...invocation.args] };
|
|
427
|
+
}
|
|
428
|
+
throw new Error(`No interpreter available for command-template ${extension} script; tried ${interpreters.map(([name]) => name).join(", ")}.`);
|
|
429
|
+
}
|
|
396
430
|
function evaluateCommandTemplateExpression(expression, values) {
|
|
397
431
|
let index = 0;
|
|
398
432
|
const source = expression.replace(/\s+/g, "");
|
|
@@ -766,5 +800,5 @@ export function buildCommandTemplateInvocation(config, values, cwd, options = {}
|
|
|
766
800
|
.slice(1)
|
|
767
801
|
.map((part) => substituteCommandTemplateToken(part, resolvedValues, options.missingLabel))
|
|
768
802
|
.filter((part) => part !== "");
|
|
769
|
-
return { command, args };
|
|
803
|
+
return resolveCommandTemplateScriptInvocation({ command, args });
|
|
770
804
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model-facing Run Control projection.
|
|
3
|
+
* Zones: structured redaction, bounded Control input/error, decision-useful evidence
|
|
4
|
+
* Owns safe Control reads; durable journals and lifecycle transitions remain in runs-controls.
|
|
5
|
+
*/
|
|
6
|
+
import type { RunControlRecord, RunControlStatus } from "./runs-controls.ts";
|
|
7
|
+
export interface ProjectedRunControl {
|
|
8
|
+
action: string;
|
|
9
|
+
claimed_at?: string;
|
|
10
|
+
delivered_at?: string;
|
|
11
|
+
error?: string;
|
|
12
|
+
failed_at?: string;
|
|
13
|
+
handled_at?: string;
|
|
14
|
+
id: string;
|
|
15
|
+
input?: unknown;
|
|
16
|
+
queued_at: string;
|
|
17
|
+
run_instance_id: string;
|
|
18
|
+
status: RunControlStatus;
|
|
19
|
+
}
|
|
20
|
+
export declare function projectRunControl(control: RunControlRecord): ProjectedRunControl;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model-facing Run Control projection.
|
|
3
|
+
* Zones: structured redaction, bounded Control input/error, decision-useful evidence
|
|
4
|
+
* Owns safe Control reads; durable journals and lifecycle transitions remain in runs-controls.
|
|
5
|
+
*/
|
|
6
|
+
import * as Limits from "./limits.js";
|
|
7
|
+
import * as SessionEvidence from "./session-evidence.js";
|
|
8
|
+
const OPTIONAL_TIMESTAMP_FIELDS = [
|
|
9
|
+
"claimed_at",
|
|
10
|
+
"delivered_at",
|
|
11
|
+
"failed_at",
|
|
12
|
+
"handled_at",
|
|
13
|
+
];
|
|
14
|
+
function boundedRedactedString(value, maxChars) {
|
|
15
|
+
const redacted = SessionEvidence.redactSessionEvidenceValue(value, maxChars);
|
|
16
|
+
return typeof redacted === "string"
|
|
17
|
+
? redacted
|
|
18
|
+
: String(redacted).slice(0, maxChars);
|
|
19
|
+
}
|
|
20
|
+
function boundedRedactedValue(value) {
|
|
21
|
+
const redacted = SessionEvidence.redactSessionEvidenceValue(value, Limits.INSPECTOR_BODY_PREVIEW_CHARS);
|
|
22
|
+
let serialized;
|
|
23
|
+
try {
|
|
24
|
+
serialized = JSON.stringify(redacted);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return "[UNSERIALIZABLE]";
|
|
28
|
+
}
|
|
29
|
+
if (serialized === undefined)
|
|
30
|
+
return "[UNSERIALIZABLE]";
|
|
31
|
+
if (serialized.length <= Limits.INSPECTOR_BODY_PREVIEW_CHARS)
|
|
32
|
+
return redacted;
|
|
33
|
+
let preview = serialized.slice(0, Limits.INSPECTOR_BODY_PREVIEW_CHARS);
|
|
34
|
+
let bounded = { preview: `${preview}…`, truncated: true };
|
|
35
|
+
while (preview &&
|
|
36
|
+
JSON.stringify(bounded).length > Limits.INSPECTOR_BODY_PREVIEW_CHARS) {
|
|
37
|
+
const excess = JSON.stringify(bounded).length - Limits.INSPECTOR_BODY_PREVIEW_CHARS;
|
|
38
|
+
preview = preview.slice(0, Math.max(0, preview.length - Math.max(1, excess)));
|
|
39
|
+
bounded = { preview: `${preview}…`, truncated: true };
|
|
40
|
+
}
|
|
41
|
+
return bounded;
|
|
42
|
+
}
|
|
43
|
+
export function projectRunControl(control) {
|
|
44
|
+
const timestamps = Object.fromEntries(OPTIONAL_TIMESTAMP_FIELDS.flatMap((field) => {
|
|
45
|
+
const value = control[field];
|
|
46
|
+
return typeof value === "string"
|
|
47
|
+
? [[field, boundedRedactedString(value, Limits.COMPACT_PREVIEW_CHARS)]]
|
|
48
|
+
: [];
|
|
49
|
+
}));
|
|
50
|
+
return {
|
|
51
|
+
action: boundedRedactedString(control.action, Limits.COMPACT_PREVIEW_CHARS),
|
|
52
|
+
...timestamps,
|
|
53
|
+
...(typeof control.error === "string"
|
|
54
|
+
? {
|
|
55
|
+
error: boundedRedactedString(control.error, Limits.INSPECTOR_BODY_PREVIEW_CHARS),
|
|
56
|
+
}
|
|
57
|
+
: {}),
|
|
58
|
+
id: boundedRedactedString(control.id, Limits.COMPACT_PREVIEW_CHARS),
|
|
59
|
+
...(control.input !== undefined
|
|
60
|
+
? { input: boundedRedactedValue(control.input) }
|
|
61
|
+
: {}),
|
|
62
|
+
queued_at: boundedRedactedString(control.queued_at, Limits.COMPACT_PREVIEW_CHARS),
|
|
63
|
+
run_instance_id: boundedRedactedString(control.run_instance_id, Limits.COMPACT_PREVIEW_CHARS),
|
|
64
|
+
status: boundedRedactedString(control.status, Limits.COMPACT_PREVIEW_CHARS),
|
|
65
|
+
};
|
|
66
|
+
}
|
package/dist/lib/control.d.ts
CHANGED
|
@@ -9,4 +9,7 @@ export interface ControlRequest {
|
|
|
9
9
|
input?: unknown;
|
|
10
10
|
verbose?: boolean;
|
|
11
11
|
}
|
|
12
|
+
export declare function isControlAction(value: string): boolean;
|
|
13
|
+
export declare function normalizeControlAction(value: unknown): string;
|
|
14
|
+
export declare function normalizeControlInput(input: unknown): unknown;
|
|
12
15
|
export declare function normalizeControlRequest(input: unknown): ControlRequest;
|
package/dist/lib/control.js
CHANGED
|
@@ -17,7 +17,23 @@ const REMOVED_FIELDS = new Set([
|
|
|
17
17
|
"to",
|
|
18
18
|
"type",
|
|
19
19
|
]);
|
|
20
|
-
function
|
|
20
|
+
export function isControlAction(value) {
|
|
21
|
+
return ACTION_PATTERN.test(value);
|
|
22
|
+
}
|
|
23
|
+
export function normalizeControlAction(value) {
|
|
24
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
25
|
+
throw new Error("control.action is required");
|
|
26
|
+
}
|
|
27
|
+
const action = value.trim();
|
|
28
|
+
if (!isControlAction(action)) {
|
|
29
|
+
throw new Error(`invalid control action: ${action}`);
|
|
30
|
+
}
|
|
31
|
+
if (action.length > Limits.CONTROL_ACTION_MAX_LENGTH) {
|
|
32
|
+
throw new Error(`control.action exceeds ${Limits.CONTROL_ACTION_MAX_LENGTH} ASCII characters`);
|
|
33
|
+
}
|
|
34
|
+
return action;
|
|
35
|
+
}
|
|
36
|
+
export function normalizeControlInput(input) {
|
|
21
37
|
let serialized;
|
|
22
38
|
try {
|
|
23
39
|
serialized = JSON.stringify(input);
|
|
@@ -28,7 +44,11 @@ function serializedInputBytes(input) {
|
|
|
28
44
|
if (serialized === undefined) {
|
|
29
45
|
throw new Error("control.input must be JSON-serializable");
|
|
30
46
|
}
|
|
31
|
-
|
|
47
|
+
const bytes = Buffer.byteLength(serialized);
|
|
48
|
+
if (bytes > Limits.CONTROL_INPUT_MAX_BYTES) {
|
|
49
|
+
throw new Error(`control.input exceeds ${Limits.CONTROL_INPUT_MAX_BYTES} serialized bytes`);
|
|
50
|
+
}
|
|
51
|
+
return JSON.parse(serialized);
|
|
32
52
|
}
|
|
33
53
|
function normalizeTarget(value) {
|
|
34
54
|
if (typeof value !== "string" || !value.trim()) {
|
|
@@ -61,24 +81,17 @@ export function normalizeControlRequest(input) {
|
|
|
61
81
|
throw new Error(`unsupported control fields: ${unknown.sort().join(", ")}`);
|
|
62
82
|
}
|
|
63
83
|
const target = normalizeTarget(record.target);
|
|
64
|
-
|
|
65
|
-
throw new Error("control.action is required");
|
|
66
|
-
}
|
|
67
|
-
const action = record.action.trim();
|
|
68
|
-
if (!ACTION_PATTERN.test(action)) {
|
|
69
|
-
throw new Error(`invalid control action: ${action}`);
|
|
70
|
-
}
|
|
84
|
+
const action = normalizeControlAction(record.action);
|
|
71
85
|
if (record.verbose !== undefined && typeof record.verbose !== "boolean") {
|
|
72
86
|
throw new Error("control.verbose must be a boolean");
|
|
73
87
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
88
|
+
const normalizedInput = record.input === undefined
|
|
89
|
+
? undefined
|
|
90
|
+
: normalizeControlInput(record.input);
|
|
78
91
|
return {
|
|
79
92
|
target,
|
|
80
93
|
action,
|
|
81
|
-
...(
|
|
94
|
+
...(normalizedInput !== undefined ? { input: normalizedInput } : {}),
|
|
82
95
|
...(record.verbose !== undefined ? { verbose: record.verbose } : {}),
|
|
83
96
|
};
|
|
84
97
|
}
|
package/dist/lib/draft-sleep.js
CHANGED
|
@@ -391,7 +391,7 @@ export function createDraftSleepScheduler(deps) {
|
|
|
391
391
|
...existing,
|
|
392
392
|
failedStage: processed.stage ?? "result_processing",
|
|
393
393
|
lastError: processed.error ?? "automatic draft review processing failed",
|
|
394
|
-
nextAction: "message
|
|
394
|
+
nextAction: "message target=runtime action=review.retry input={\"scope\":\"draft\"}",
|
|
395
395
|
phase: "processing_failed",
|
|
396
396
|
processingAttempts: (existing.processingAttempts ?? 0) + 1,
|
|
397
397
|
updatedAt: now().toISOString(),
|
|
@@ -402,7 +402,7 @@ export function createDraftSleepScheduler(deps) {
|
|
|
402
402
|
...existing,
|
|
403
403
|
failedStage: processed.stage ?? "review_result",
|
|
404
404
|
lastError: processed.error ?? "automatic draft reviewer failed",
|
|
405
|
-
nextAction: "message
|
|
405
|
+
nextAction: "message target=runtime action=review.retry input={\"scope\":\"draft\"}",
|
|
406
406
|
phase: "failed",
|
|
407
407
|
runId: undefined,
|
|
408
408
|
updatedAt: now().toISOString(),
|
|
@@ -444,7 +444,7 @@ export function createDraftSleepScheduler(deps) {
|
|
|
444
444
|
attempts: (prior?.batchId === batch.batchId ? prior.attempts : 0) + 1,
|
|
445
445
|
failedStage: "review_launch",
|
|
446
446
|
lastError: error instanceof Error ? error.message : String(error),
|
|
447
|
-
nextAction: "message
|
|
447
|
+
nextAction: "message target=runtime action=review.retry input={\"scope\":\"draft\"}",
|
|
448
448
|
batchId: batch.batchId,
|
|
449
449
|
inputPath: batch.inputPath,
|
|
450
450
|
phase: "failed",
|
package/dist/lib/file-state.d.ts
CHANGED
package/dist/lib/file-state.js
CHANGED
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
* Zones: file persistence, atomic writes, runtime state support
|
|
4
4
|
* Owns generic durable JSON file writes shared by registry config and async run state.
|
|
5
5
|
*/
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
6
7
|
import { createHash, randomUUID } from "node:crypto";
|
|
7
|
-
import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync
|
|
8
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
8
9
|
import { tmpdir } from "node:os";
|
|
9
10
|
import { basename, dirname, join, parse, resolve } from "node:path";
|
|
10
|
-
const FILE_MUTATION_LOCK_TIMEOUT_MS = 5000;
|
|
11
|
+
const FILE_MUTATION_LOCK_TIMEOUT_MS = process.platform === "win32" ? 15000 : 5000;
|
|
11
12
|
const FILE_MUTATION_LOCK_STALE_MS = 30000;
|
|
12
13
|
const FILE_MUTATION_LOCK_ROOT = join(tmpdir(), "pi-actors-file-locks");
|
|
13
14
|
function canonicalMutationPath(path) {
|
|
@@ -42,6 +43,20 @@ export function mutationLockPath(path) {
|
|
|
42
43
|
.digest("hex");
|
|
43
44
|
return join(FILE_MUTATION_LOCK_ROOT, `${key}.lock`);
|
|
44
45
|
}
|
|
46
|
+
function isZombieProcess(pid) {
|
|
47
|
+
if (process.platform === "linux")
|
|
48
|
+
try {
|
|
49
|
+
const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
|
|
50
|
+
return stat.slice(stat.lastIndexOf(")") + 2).startsWith("Z ");
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
if (process.platform !== "darwin")
|
|
56
|
+
return false;
|
|
57
|
+
const result = spawnSync("ps", ["-p", String(pid), "-o", "stat="], { encoding: "utf8" });
|
|
58
|
+
return result.status === 0 && result.stdout.trim().startsWith("Z");
|
|
59
|
+
}
|
|
45
60
|
function lockOwnerStatus(lockPath) {
|
|
46
61
|
try {
|
|
47
62
|
const owner = JSON.parse(readFileSync(join(lockPath, "owner.json"), "utf8"));
|
|
@@ -50,6 +65,8 @@ function lockOwnerStatus(lockPath) {
|
|
|
50
65
|
return "unknown";
|
|
51
66
|
try {
|
|
52
67
|
process.kill(pid, 0);
|
|
68
|
+
if (Date.now() - statSync(lockPath).mtimeMs > 50 && isZombieProcess(pid))
|
|
69
|
+
return "dead";
|
|
53
70
|
return "alive";
|
|
54
71
|
}
|
|
55
72
|
catch (error) {
|
|
@@ -71,6 +88,46 @@ function readLockToken(lockPath) {
|
|
|
71
88
|
return undefined;
|
|
72
89
|
}
|
|
73
90
|
}
|
|
91
|
+
function prepareLockBoundary(lockPath, token, onBeforePublish) {
|
|
92
|
+
const pendingPath = `${lockPath}.${process.pid}.${token}.pending`;
|
|
93
|
+
try {
|
|
94
|
+
mkdirSync(pendingPath);
|
|
95
|
+
writeFileSync(join(pendingPath, "owner.json"), `${JSON.stringify({ pid: process.pid, token, acquired_at: new Date().toISOString() })}\n`, "utf8");
|
|
96
|
+
onBeforePublish?.();
|
|
97
|
+
return pendingPath;
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
try {
|
|
101
|
+
rmSync(pendingPath, { recursive: true, force: true });
|
|
102
|
+
}
|
|
103
|
+
catch { }
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function removeLockBoundary(lockPath, token) {
|
|
108
|
+
if (readLockToken(lockPath) !== token)
|
|
109
|
+
return false;
|
|
110
|
+
if (process.platform === "win32")
|
|
111
|
+
try {
|
|
112
|
+
rmSync(lockPath, { recursive: true, force: true, maxRetries: 20, retryDelay: 5 });
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
const removingPath = `${lockPath}.${process.pid}.${randomUUID()}.removing`;
|
|
119
|
+
try {
|
|
120
|
+
renameSync(lockPath, removingPath);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
rmSync(removingPath, { recursive: true, force: true });
|
|
127
|
+
}
|
|
128
|
+
catch { }
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
74
131
|
function tryReclaimRemovalBoundary(reclaimPath) {
|
|
75
132
|
if (!existsSync(reclaimPath))
|
|
76
133
|
return;
|
|
@@ -81,7 +138,7 @@ function tryReclaimRemovalBoundary(reclaimPath) {
|
|
|
81
138
|
if ((ownerStatus === "dead" ||
|
|
82
139
|
(ownerStatus === "unknown" && age > FILE_MUTATION_LOCK_STALE_MS)) &&
|
|
83
140
|
readLockToken(reclaimPath) === inspectedToken) {
|
|
84
|
-
|
|
141
|
+
removeLockBoundary(reclaimPath, inspectedToken);
|
|
85
142
|
}
|
|
86
143
|
}
|
|
87
144
|
catch {
|
|
@@ -91,28 +148,28 @@ function tryReclaimRemovalBoundary(reclaimPath) {
|
|
|
91
148
|
function withRemovalBoundary(lockPath, action) {
|
|
92
149
|
const reclaimPath = `${lockPath}.reclaim`;
|
|
93
150
|
const token = randomUUID();
|
|
151
|
+
const pendingPath = prepareLockBoundary(reclaimPath, token);
|
|
94
152
|
try {
|
|
95
|
-
mkdirSync(reclaimPath);
|
|
96
153
|
try {
|
|
97
|
-
|
|
154
|
+
renameSync(pendingPath, reclaimPath);
|
|
98
155
|
}
|
|
99
|
-
catch
|
|
100
|
-
|
|
101
|
-
|
|
156
|
+
catch {
|
|
157
|
+
tryReclaimRemovalBoundary(reclaimPath);
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
action();
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
finally {
|
|
165
|
+
removeLockBoundary(reclaimPath, token);
|
|
102
166
|
}
|
|
103
|
-
}
|
|
104
|
-
catch {
|
|
105
|
-
tryReclaimRemovalBoundary(reclaimPath);
|
|
106
|
-
return false;
|
|
107
|
-
}
|
|
108
|
-
try {
|
|
109
|
-
action();
|
|
110
|
-
return true;
|
|
111
167
|
}
|
|
112
168
|
finally {
|
|
113
|
-
|
|
114
|
-
rmSync(
|
|
169
|
+
try {
|
|
170
|
+
rmSync(pendingPath, { recursive: true, force: true });
|
|
115
171
|
}
|
|
172
|
+
catch { }
|
|
116
173
|
}
|
|
117
174
|
}
|
|
118
175
|
function tryReclaimMutationLock(lockPath, options) {
|
|
@@ -129,8 +186,7 @@ function tryReclaimMutationLock(lockPath, options) {
|
|
|
129
186
|
(ownerStatus === "unknown" && age > FILE_MUTATION_LOCK_STALE_MS)) &&
|
|
130
187
|
readLockToken(lockPath) === inspectedToken) {
|
|
131
188
|
options.onBeforeReclaimRemove?.();
|
|
132
|
-
|
|
133
|
-
reclaimed = true;
|
|
189
|
+
reclaimed = removeLockBoundary(lockPath, inspectedToken);
|
|
134
190
|
}
|
|
135
191
|
})) {
|
|
136
192
|
return false;
|
|
@@ -142,32 +198,34 @@ export function acquireFileMutationLock(path, options = {}) {
|
|
|
142
198
|
const lockPath = mutationLockPath(path);
|
|
143
199
|
const deadline = Date.now() + FILE_MUTATION_LOCK_TIMEOUT_MS;
|
|
144
200
|
const token = randomUUID();
|
|
201
|
+
const pendingPath = prepareLockBoundary(lockPath, token, options.onBeforeLockPublish);
|
|
145
202
|
let contentionReported = false;
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
mkdirSync(lockPath);
|
|
203
|
+
try {
|
|
204
|
+
for (;;) {
|
|
149
205
|
try {
|
|
150
|
-
|
|
206
|
+
renameSync(pendingPath, lockPath);
|
|
207
|
+
break;
|
|
151
208
|
}
|
|
152
209
|
catch (error) {
|
|
153
|
-
|
|
154
|
-
|
|
210
|
+
if (!contentionReported) {
|
|
211
|
+
contentionReported = true;
|
|
212
|
+
options.onContention?.();
|
|
213
|
+
}
|
|
214
|
+
tryReclaimMutationLock(lockPath, options);
|
|
215
|
+
if (Date.now() >= deadline) {
|
|
216
|
+
throw new Error(`Timed out waiting for file mutation lock: ${canonicalMutationPath(path)}`, {
|
|
217
|
+
cause: error,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
|
|
155
221
|
}
|
|
156
|
-
break;
|
|
157
222
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}
|
|
163
|
-
tryReclaimMutationLock(lockPath, options);
|
|
164
|
-
if (Date.now() >= deadline) {
|
|
165
|
-
throw new Error(`Timed out waiting for file mutation lock: ${canonicalMutationPath(path)}`, {
|
|
166
|
-
cause: error,
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
|
|
223
|
+
}
|
|
224
|
+
finally {
|
|
225
|
+
try {
|
|
226
|
+
rmSync(pendingPath, { recursive: true, force: true });
|
|
170
227
|
}
|
|
228
|
+
catch { }
|
|
171
229
|
}
|
|
172
230
|
let released = false;
|
|
173
231
|
return () => {
|
|
@@ -177,9 +235,7 @@ export function acquireFileMutationLock(path, options = {}) {
|
|
|
177
235
|
const deadline = Date.now() + FILE_MUTATION_LOCK_TIMEOUT_MS;
|
|
178
236
|
let removalContentionReported = false;
|
|
179
237
|
while (!withRemovalBoundary(lockPath, () => {
|
|
180
|
-
|
|
181
|
-
rmSync(lockPath, { recursive: true, force: true });
|
|
182
|
-
}
|
|
238
|
+
removeLockBoundary(lockPath, token);
|
|
183
239
|
})) {
|
|
184
240
|
if (!removalContentionReported) {
|
|
185
241
|
removalContentionReported = true;
|