@llblab/pi-actors 0.22.5 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -0
- package/BACKLOG.md +0 -220
- package/CHANGELOG.md +18 -1
- package/README.md +1 -1
- package/dist/index.js +15 -0
- package/dist/lib/actor-inspector-tui.d.ts +4 -0
- package/dist/lib/actor-inspector-tui.js +53 -12
- package/dist/lib/actor-rooms.d.ts +8 -0
- package/dist/lib/actor-rooms.js +61 -10
- package/dist/lib/async-runs.d.ts +34 -2
- package/dist/lib/async-runs.js +215 -22
- package/dist/lib/command-templates.js +5 -5
- package/dist/lib/limits.d.ts +11 -0
- package/dist/lib/limits.js +11 -0
- package/dist/lib/observability.js +6 -4
- package/dist/lib/output.js +4 -5
- package/dist/lib/recipe-discovery.js +1 -0
- package/dist/lib/recipe-references.d.ts +11 -2
- package/dist/lib/recipe-references.js +4 -2
- package/dist/lib/recipe-usage.d.ts +2 -1
- package/dist/lib/recipe-usage.js +15 -3
- package/dist/lib/runtime.js +12 -2
- package/dist/lib/tools.js +197 -38
- package/docs/async-runs.md +2 -1
- package/docs/template-recipes.md +1 -1
- package/docs/tool-registry.md +0 -1
- package/index.ts +18 -0
- package/lib/actor-inspector-tui.ts +131 -29
- package/lib/actor-rooms.ts +155 -44
- package/lib/async-runs.ts +290 -31
- package/lib/command-templates.ts +5 -5
- package/lib/limits.ts +12 -0
- package/lib/observability.ts +10 -4
- package/lib/output.ts +4 -6
- package/lib/recipe-discovery.ts +1 -0
- package/lib/recipe-references.ts +20 -4
- package/lib/recipe-usage.ts +31 -4
- package/lib/runtime.ts +31 -7
- package/lib/tools.ts +296 -57
- package/package.json +2 -1
- package/scripts/async-runner.mjs +6 -0
- package/scripts/conformance.mjs +47 -0
- package/scripts/coordinator.mjs +13 -0
- package/scripts/locker.mjs +13 -0
- package/scripts/music-player.mjs +21 -2
- package/scripts/recipe-utils.mjs +13 -0
- package/scripts/validate-recipe.mjs +13 -0
- package/skills/actors/SKILL.md +3 -3
- package/skills/swarm/SKILL.md +1 -1
package/dist/lib/actor-rooms.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import * as fs from "node:fs";
|
|
7
7
|
import { randomUUID } from "node:crypto";
|
|
8
8
|
import * as path from "node:path";
|
|
9
|
+
import * as Limits from "./limits.js";
|
|
9
10
|
import { notifyRuntimeWake } from "./runtime-notifier.js";
|
|
10
11
|
const STATE_LOCK_MAX_AGE_MS = 5 * 60 * 1000;
|
|
11
12
|
const STATE_LOCK_TIMEOUT_MS = 5000;
|
|
@@ -128,8 +129,23 @@ function compactRoomMessages(stateDir, room) {
|
|
|
128
129
|
return;
|
|
129
130
|
const kept = lines.slice(-maxMessages);
|
|
130
131
|
fs.writeFileSync(file, `${kept.join("\n")}\n`);
|
|
132
|
+
const keptMessages = kept.flatMap((line) => {
|
|
133
|
+
try {
|
|
134
|
+
return [JSON.parse(line)];
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
});
|
|
131
140
|
writeJsonFile(path.join(roomDir(stateDir, room), "compaction.json"), {
|
|
132
141
|
compacted_at: new Date().toISOString(),
|
|
142
|
+
dropped_count: lines.length - kept.length,
|
|
143
|
+
...(keptMessages[0]?.received_at
|
|
144
|
+
? { first_kept_at: keptMessages[0].received_at }
|
|
145
|
+
: {}),
|
|
146
|
+
...(keptMessages.at(-1)?.received_at
|
|
147
|
+
? { last_kept_at: keptMessages.at(-1)?.received_at }
|
|
148
|
+
: {}),
|
|
133
149
|
max_messages: maxMessages,
|
|
134
150
|
});
|
|
135
151
|
}
|
|
@@ -250,20 +266,44 @@ function updateRosterForMessage(stateDir, room, message, receivedAt) {
|
|
|
250
266
|
last_seen: receivedAt,
|
|
251
267
|
...(current?.caps !== undefined ? { caps: current.caps } : {}),
|
|
252
268
|
...(current?.claim !== undefined ? { claim: current.claim } : {}),
|
|
253
|
-
...(current?.display !== undefined
|
|
269
|
+
...(current?.display !== undefined
|
|
270
|
+
? { display: current.display }
|
|
271
|
+
: {}),
|
|
254
272
|
...(current?.parent !== undefined ? { parent: current.parent } : {}),
|
|
255
|
-
...(current?.role !== undefined
|
|
273
|
+
...(current?.role !== undefined
|
|
274
|
+
? { role: current.role }
|
|
275
|
+
: { role: "actor" }),
|
|
256
276
|
status: String(body.status ?? "left"),
|
|
257
277
|
}
|
|
258
278
|
: {
|
|
259
279
|
address: message.from,
|
|
260
280
|
joined_at: current?.joined_at ?? receivedAt,
|
|
261
281
|
last_seen: receivedAt,
|
|
262
|
-
...(body.caps !== undefined
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
282
|
+
...(body.caps !== undefined
|
|
283
|
+
? { caps: body.caps }
|
|
284
|
+
: current?.caps !== undefined
|
|
285
|
+
? { caps: current.caps }
|
|
286
|
+
: {}),
|
|
287
|
+
...(body.claim !== undefined
|
|
288
|
+
? { claim: body.claim }
|
|
289
|
+
: current?.claim !== undefined
|
|
290
|
+
? { claim: current.claim }
|
|
291
|
+
: {}),
|
|
292
|
+
...(body.display !== undefined
|
|
293
|
+
? { display: body.display }
|
|
294
|
+
: current?.display !== undefined
|
|
295
|
+
? { display: current.display }
|
|
296
|
+
: {}),
|
|
297
|
+
...(body.parent !== undefined
|
|
298
|
+
? { parent: body.parent }
|
|
299
|
+
: current?.parent !== undefined
|
|
300
|
+
? { parent: current.parent }
|
|
301
|
+
: {}),
|
|
302
|
+
...(body.role !== undefined
|
|
303
|
+
? { role: body.role }
|
|
304
|
+
: current?.role !== undefined
|
|
305
|
+
? { role: current.role }
|
|
306
|
+
: { role: "actor" }),
|
|
267
307
|
status: String(body.status ?? current?.status ?? "present"),
|
|
268
308
|
};
|
|
269
309
|
roster[message.from] = next;
|
|
@@ -368,7 +408,14 @@ export function updateBranchInboxMessageStatus(stateDir, run, address, id, statu
|
|
|
368
408
|
if (!("message" in record) || record.message.id !== id)
|
|
369
409
|
return record;
|
|
370
410
|
changed = true;
|
|
371
|
-
return {
|
|
411
|
+
return {
|
|
412
|
+
message: {
|
|
413
|
+
...record.message,
|
|
414
|
+
...metadata,
|
|
415
|
+
[timestampKey]: new Date().toISOString(),
|
|
416
|
+
status,
|
|
417
|
+
},
|
|
418
|
+
};
|
|
372
419
|
});
|
|
373
420
|
if (!changed)
|
|
374
421
|
return false;
|
|
@@ -430,7 +477,7 @@ export function readRoomMessages(stateDir, room, limit = 40) {
|
|
|
430
477
|
throw error;
|
|
431
478
|
}
|
|
432
479
|
}
|
|
433
|
-
function previewValue(value, maxLength =
|
|
480
|
+
function previewValue(value, maxLength = Limits.ROOM_MESSAGE_PREVIEW_CHARS) {
|
|
434
481
|
if (value === undefined)
|
|
435
482
|
return undefined;
|
|
436
483
|
const text = typeof value === "string" ? value : JSON.stringify(value);
|
|
@@ -443,7 +490,9 @@ function previewValue(value, maxLength = 120) {
|
|
|
443
490
|
}
|
|
444
491
|
export function readRoomMessagePreviews(stateDir, room, limit = 40) {
|
|
445
492
|
return readRoomMessages(stateDir, room, limit).map((message) => ({
|
|
446
|
-
...(previewValue(message.body)
|
|
493
|
+
...(previewValue(message.body)
|
|
494
|
+
? { body_preview: previewValue(message.body) }
|
|
495
|
+
: {}),
|
|
447
496
|
...(message.from ? { from: message.from } : {}),
|
|
448
497
|
...(message.summary ? { summary: message.summary } : {}),
|
|
449
498
|
timestamp: message.received_at,
|
|
@@ -454,6 +503,7 @@ export function readRoomMessagePreviews(stateDir, room, limit = 40) {
|
|
|
454
503
|
export function getRoomStatus(stateDir, room) {
|
|
455
504
|
const messageCount = readRoomMessageCount(stateDir, room);
|
|
456
505
|
const [last] = readRoomMessages(stateDir, room, 1);
|
|
506
|
+
const compaction = readJsonFile(path.join(roomDir(stateDir, room), "compaction.json"), undefined);
|
|
457
507
|
return {
|
|
458
508
|
...(last
|
|
459
509
|
? {
|
|
@@ -463,6 +513,7 @@ export function getRoomStatus(stateDir, room) {
|
|
|
463
513
|
last_message_type: last.type,
|
|
464
514
|
}
|
|
465
515
|
: {}),
|
|
516
|
+
...(compaction ? { compaction } : {}),
|
|
466
517
|
message_count: messageCount,
|
|
467
518
|
room,
|
|
468
519
|
roster_count: Object.keys(readRoomRoster(stateDir, room)).length,
|
package/dist/lib/async-runs.d.ts
CHANGED
|
@@ -29,7 +29,7 @@ export interface AsyncRunStartParams {
|
|
|
29
29
|
timeout?: number | string;
|
|
30
30
|
delay?: number | string;
|
|
31
31
|
output?: string;
|
|
32
|
-
artifacts?: Record<string,
|
|
32
|
+
artifacts?: Record<string, RunArtifactDeclaration>;
|
|
33
33
|
mailbox?: RecipeReferences.TemplateRecipeMailbox;
|
|
34
34
|
retire_when?: "children_terminal";
|
|
35
35
|
retry?: number | string;
|
|
@@ -76,15 +76,43 @@ export interface AsyncRunMeta {
|
|
|
76
76
|
tool?: string;
|
|
77
77
|
template: CommandTemplateValue;
|
|
78
78
|
values: Record<string, unknown>;
|
|
79
|
-
artifacts?: Record<string,
|
|
79
|
+
artifacts?: Record<string, RunArtifactDeclaration>;
|
|
80
80
|
control?: AsyncRunControlEndpoint;
|
|
81
81
|
mailbox?: RecipeReferences.TemplateRecipeMailbox;
|
|
82
82
|
recipe_context_records?: RecipeReferences.TemplateRecipeContextRecord[];
|
|
83
83
|
retire_when?: "children_terminal";
|
|
84
84
|
}
|
|
85
|
+
export type RunArtifactDeclaration = string | {
|
|
86
|
+
path: string;
|
|
87
|
+
kind?: string;
|
|
88
|
+
media_type?: string;
|
|
89
|
+
required?: boolean;
|
|
90
|
+
};
|
|
91
|
+
export interface RunArtifactManifestEntry {
|
|
92
|
+
exists: boolean;
|
|
93
|
+
kind?: string;
|
|
94
|
+
media_type?: string;
|
|
95
|
+
path: string;
|
|
96
|
+
required?: boolean;
|
|
97
|
+
sha256?: string;
|
|
98
|
+
size?: number;
|
|
99
|
+
}
|
|
100
|
+
export declare function resolveArtifactManifest(artifacts: Record<string, RunArtifactDeclaration> | undefined): Record<string, RunArtifactManifestEntry> | undefined;
|
|
85
101
|
export declare function startRun(params: AsyncRunStartParams, cwd: string): AsyncRunMeta;
|
|
86
102
|
export declare function parseRunOutboxEventLine(line: string, run: string, stateDir: string, index: number): RunOutboxEvent | undefined;
|
|
87
103
|
export declare function getRunStatus(runOrDir: string): Record<string, unknown>;
|
|
104
|
+
export interface RunStateIndexEntry {
|
|
105
|
+
ownerId?: string;
|
|
106
|
+
recipe?: string;
|
|
107
|
+
run: string;
|
|
108
|
+
state_dir: string;
|
|
109
|
+
status: AsyncRunStatus;
|
|
110
|
+
tool?: string;
|
|
111
|
+
updated_at?: string;
|
|
112
|
+
}
|
|
113
|
+
export declare function listRunStateDirs(stateRoot?: string, depth?: number, seen?: Set<string>): string[];
|
|
114
|
+
export declare function rebuildRunStateIndex(stateRoot?: string): RunStateIndexEntry[];
|
|
115
|
+
export declare function readRunStateIndex(stateRoot?: string): RunStateIndexEntry[] | undefined;
|
|
88
116
|
export declare function listRuns(stateRoot?: string, statusFilter?: string): Array<Record<string, unknown>>;
|
|
89
117
|
export declare function tailRun(runOrDir: string, lines?: number): string;
|
|
90
118
|
export declare function readRunEvents(runOrDir: string, lines?: number): RunOutboxEvent[];
|
|
@@ -132,4 +160,8 @@ export interface RunProcessSignalPlan {
|
|
|
132
160
|
}
|
|
133
161
|
export declare function getRunProcessSignalPlan(pid: number, signal: NodeJS.Signals, runtimePlatform?: NodeJS.Platform): RunProcessSignalPlan;
|
|
134
162
|
export declare function cancelRun(runOrDir: string): Record<string, unknown>;
|
|
163
|
+
export declare function archiveRun(runOrDir: string): Record<string, unknown>;
|
|
164
|
+
export declare function pruneRun(runOrDir: string, options?: {
|
|
165
|
+
preserveArtifacts?: boolean;
|
|
166
|
+
}): Record<string, unknown>;
|
|
135
167
|
export declare function killRun(runOrDir: string): Record<string, unknown>;
|
package/dist/lib/async-runs.js
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* Owns detached run state, observation, log tailing, listing, and cancellation safety
|
|
5
5
|
*/
|
|
6
6
|
import { spawn, spawnSync } from "node:child_process";
|
|
7
|
-
import { randomUUID } from "node:crypto";
|
|
8
|
-
import { closeSync, constants, existsSync, mkdirSync, openSync, readdirSync, readFileSync, readlinkSync, rmSync, statSync, writeFileSync, writeSync, } from "node:fs";
|
|
7
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
8
|
+
import { closeSync, constants, existsSync, mkdirSync, openSync, readdirSync, readFileSync, cpSync, readlinkSync, renameSync, rmSync, statSync, writeFileSync, writeSync, } from "node:fs";
|
|
9
9
|
import { createConnection } from "node:net";
|
|
10
10
|
import { platform } from "node:os";
|
|
11
11
|
import { basename, dirname, extname, join, resolve } from "node:path";
|
|
@@ -48,10 +48,60 @@ function resolveArtifactPaths(artifacts, values) {
|
|
|
48
48
|
for (const [key, value] of Object.entries(artifacts)) {
|
|
49
49
|
if (!key.trim())
|
|
50
50
|
continue;
|
|
51
|
-
|
|
51
|
+
if (typeof value === "string") {
|
|
52
|
+
resolved[key] = substituteCommandTemplateToken(value, values, `recipe artifacts.${key}`);
|
|
53
|
+
}
|
|
54
|
+
else if (value &&
|
|
55
|
+
typeof value === "object" &&
|
|
56
|
+
typeof value.path === "string") {
|
|
57
|
+
resolved[key] = {
|
|
58
|
+
...value,
|
|
59
|
+
path: substituteCommandTemplateToken(value.path, values, `recipe artifacts.${key}.path`),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
52
62
|
}
|
|
53
63
|
return Object.keys(resolved).length > 0 ? resolved : undefined;
|
|
54
64
|
}
|
|
65
|
+
export function resolveArtifactManifest(artifacts) {
|
|
66
|
+
if (!artifacts)
|
|
67
|
+
return undefined;
|
|
68
|
+
const manifest = {};
|
|
69
|
+
for (const [name, artifact] of Object.entries(artifacts)) {
|
|
70
|
+
const declaration = typeof artifact === "string" ? { path: artifact } : artifact;
|
|
71
|
+
if (!declaration?.path)
|
|
72
|
+
continue;
|
|
73
|
+
try {
|
|
74
|
+
const content = readFileSync(declaration.path);
|
|
75
|
+
manifest[name] = {
|
|
76
|
+
exists: true,
|
|
77
|
+
...(declaration.kind ? { kind: declaration.kind } : {}),
|
|
78
|
+
...(declaration.media_type
|
|
79
|
+
? { media_type: declaration.media_type }
|
|
80
|
+
: {}),
|
|
81
|
+
path: declaration.path,
|
|
82
|
+
...(declaration.required !== undefined
|
|
83
|
+
? { required: declaration.required }
|
|
84
|
+
: {}),
|
|
85
|
+
sha256: createHash("sha256").update(content).digest("hex"),
|
|
86
|
+
size: content.byteLength,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
manifest[name] = {
|
|
91
|
+
exists: false,
|
|
92
|
+
...(declaration.kind ? { kind: declaration.kind } : {}),
|
|
93
|
+
...(declaration.media_type
|
|
94
|
+
? { media_type: declaration.media_type }
|
|
95
|
+
: {}),
|
|
96
|
+
path: declaration.path,
|
|
97
|
+
...(declaration.required !== undefined
|
|
98
|
+
? { required: declaration.required }
|
|
99
|
+
: {}),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return Object.keys(manifest).length > 0 ? manifest : undefined;
|
|
104
|
+
}
|
|
55
105
|
function resolveRunTemplate(params) {
|
|
56
106
|
if (!params.template)
|
|
57
107
|
throw new Error("spawn requires file or template.");
|
|
@@ -270,7 +320,7 @@ export function startRun(params, cwd) {
|
|
|
270
320
|
? RecipeReferences.buildRecipeContextRecords(recipeFile)
|
|
271
321
|
: undefined;
|
|
272
322
|
if (recipeFile && isMutableUsageRecipeFile(recipeFile)) {
|
|
273
|
-
RecipeUsage.recordRecipeLaunch(recipeFile);
|
|
323
|
+
RecipeUsage.recordRecipeLaunch(recipeFile, new Date(), startParams.launch_source === "tool" ? "tool" : "spawn");
|
|
274
324
|
}
|
|
275
325
|
const outFd = openSync(stdout, "a");
|
|
276
326
|
const errFd = openSync(stderr, "a");
|
|
@@ -440,31 +490,111 @@ function matchesStatusFilter(status, filter) {
|
|
|
440
490
|
return status !== "running";
|
|
441
491
|
return status === filter;
|
|
442
492
|
}
|
|
443
|
-
export function
|
|
444
|
-
if (!existsSync(stateRoot))
|
|
493
|
+
export function listRunStateDirs(stateRoot = DEFAULT_STATE_ROOT, depth = 0, seen = new Set()) {
|
|
494
|
+
if (!existsSync(stateRoot) || seen.has(resolve(stateRoot)))
|
|
495
|
+
return [];
|
|
496
|
+
seen.add(resolve(stateRoot));
|
|
497
|
+
let entries;
|
|
498
|
+
try {
|
|
499
|
+
entries = readdirSync(stateRoot, { withFileTypes: true });
|
|
500
|
+
}
|
|
501
|
+
catch {
|
|
445
502
|
return [];
|
|
446
|
-
|
|
447
|
-
|
|
503
|
+
}
|
|
504
|
+
const result = [];
|
|
505
|
+
for (const entry of entries) {
|
|
448
506
|
if (!entry.isDirectory())
|
|
449
507
|
continue;
|
|
508
|
+
const child = join(stateRoot, entry.name);
|
|
509
|
+
if (existsSync(join(child, "run.json")))
|
|
510
|
+
result.push(child);
|
|
511
|
+
if (depth + 1 < 8)
|
|
512
|
+
result.push(...listRunStateDirs(child, depth + 1, seen));
|
|
513
|
+
}
|
|
514
|
+
return result;
|
|
515
|
+
}
|
|
516
|
+
function runIndexPath(stateRoot) {
|
|
517
|
+
return join(stateRoot, "index.json");
|
|
518
|
+
}
|
|
519
|
+
function indexEntryFromStatus(status) {
|
|
520
|
+
const progress = status.progress && typeof status.progress === "object"
|
|
521
|
+
? status.progress
|
|
522
|
+
: {};
|
|
523
|
+
return {
|
|
524
|
+
...(typeof status.ownerId === "string" ? { ownerId: status.ownerId } : {}),
|
|
525
|
+
...(typeof status.recipe === "string" ? { recipe: status.recipe } : {}),
|
|
526
|
+
run: String(status.run),
|
|
527
|
+
state_dir: String(status.state_dir),
|
|
528
|
+
status: status.status,
|
|
529
|
+
...(typeof status.tool === "string" ? { tool: status.tool } : {}),
|
|
530
|
+
updated_at: typeof progress.updatedAt === "string"
|
|
531
|
+
? progress.updatedAt
|
|
532
|
+
: typeof status.createdAt === "string"
|
|
533
|
+
? status.createdAt
|
|
534
|
+
: new Date(0).toISOString(),
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
export function rebuildRunStateIndex(stateRoot = DEFAULT_STATE_ROOT) {
|
|
538
|
+
mkdirSync(stateRoot, { recursive: true });
|
|
539
|
+
const entries = listRunStateDirs(stateRoot)
|
|
540
|
+
.flatMap((stateDir) => {
|
|
450
541
|
try {
|
|
451
|
-
|
|
452
|
-
const status = getRunStatus(stateDir);
|
|
453
|
-
if (!matchesStatusFilter(status.status, statusFilter))
|
|
454
|
-
continue;
|
|
455
|
-
runs.push({
|
|
456
|
-
run: status.run,
|
|
457
|
-
state_dir: stateDir,
|
|
458
|
-
status: status.status,
|
|
459
|
-
...(typeof status.tool === "string" ? { tool: status.tool } : {}),
|
|
460
|
-
...(typeof status.recipe === "string" ? { recipe: status.recipe } : {}),
|
|
461
|
-
});
|
|
542
|
+
return [indexEntryFromStatus(getRunStatus(stateDir))];
|
|
462
543
|
}
|
|
463
544
|
catch {
|
|
464
|
-
|
|
545
|
+
return [];
|
|
465
546
|
}
|
|
547
|
+
})
|
|
548
|
+
.sort((a, b) => (b.updated_at ?? "").localeCompare(a.updated_at ?? ""));
|
|
549
|
+
writeJsonAtomic(runIndexPath(stateRoot), {
|
|
550
|
+
entries,
|
|
551
|
+
rebuilt_at: new Date().toISOString(),
|
|
552
|
+
});
|
|
553
|
+
return entries;
|
|
554
|
+
}
|
|
555
|
+
export function readRunStateIndex(stateRoot = DEFAULT_STATE_ROOT) {
|
|
556
|
+
const index = readJson(runIndexPath(stateRoot));
|
|
557
|
+
if (!index || typeof index !== "object")
|
|
558
|
+
return undefined;
|
|
559
|
+
const entries = index.entries;
|
|
560
|
+
if (!Array.isArray(entries))
|
|
561
|
+
return undefined;
|
|
562
|
+
const valid = entries.filter((entry) => {
|
|
563
|
+
const record = entry && typeof entry === "object"
|
|
564
|
+
? entry
|
|
565
|
+
: {};
|
|
566
|
+
return (typeof record.run === "string" &&
|
|
567
|
+
typeof record.state_dir === "string" &&
|
|
568
|
+
typeof record.status === "string");
|
|
569
|
+
});
|
|
570
|
+
if (valid.some((entry) => !existsSync(join(entry.state_dir, "run.json"))))
|
|
571
|
+
return undefined;
|
|
572
|
+
return valid;
|
|
573
|
+
}
|
|
574
|
+
export function listRuns(stateRoot = DEFAULT_STATE_ROOT, statusFilter) {
|
|
575
|
+
if (!existsSync(stateRoot))
|
|
576
|
+
return [];
|
|
577
|
+
const indexed = readRunStateIndex(stateRoot);
|
|
578
|
+
if (indexed) {
|
|
579
|
+
return indexed
|
|
580
|
+
.filter((entry) => matchesStatusFilter(entry.status, statusFilter))
|
|
581
|
+
.map((entry) => ({
|
|
582
|
+
run: entry.run,
|
|
583
|
+
state_dir: entry.state_dir,
|
|
584
|
+
status: entry.status,
|
|
585
|
+
...(entry.tool ? { tool: entry.tool } : {}),
|
|
586
|
+
...(entry.recipe ? { recipe: entry.recipe } : {}),
|
|
587
|
+
}));
|
|
466
588
|
}
|
|
467
|
-
return
|
|
589
|
+
return rebuildRunStateIndex(stateRoot)
|
|
590
|
+
.filter((entry) => matchesStatusFilter(entry.status, statusFilter))
|
|
591
|
+
.map((entry) => ({
|
|
592
|
+
run: entry.run,
|
|
593
|
+
state_dir: entry.state_dir,
|
|
594
|
+
status: entry.status,
|
|
595
|
+
...(entry.tool ? { tool: entry.tool } : {}),
|
|
596
|
+
...(entry.recipe ? { recipe: entry.recipe } : {}),
|
|
597
|
+
}));
|
|
468
598
|
}
|
|
469
599
|
export function tailRun(runOrDir, lines = 40) {
|
|
470
600
|
const status = getRunStatus(runOrDir);
|
|
@@ -630,11 +760,18 @@ export function appendRunOutboxEvent(runOrDir, event) {
|
|
|
630
760
|
const run = String(status.run ?? runOrDir);
|
|
631
761
|
const type = event.type || event.event || "run.message";
|
|
632
762
|
const to = event.to || "coordinator";
|
|
763
|
+
const metadata = event.metadata ?? {};
|
|
764
|
+
const requiresResponse = metadata.requires_response === true;
|
|
633
765
|
const payload = {
|
|
634
766
|
...(event.body !== undefined ? { body: event.body } : {}),
|
|
635
767
|
...(event.correlation_id ? { correlation_id: event.correlation_id } : {}),
|
|
636
768
|
...(event.data !== undefined ? { data: event.data } : {}),
|
|
637
|
-
delivery: normalizeRunOutboxDelivery(event.delivery ??
|
|
769
|
+
delivery: normalizeRunOutboxDelivery(event.delivery ??
|
|
770
|
+
(requiresResponse
|
|
771
|
+
? "followup"
|
|
772
|
+
: to === "coordinator"
|
|
773
|
+
? "notify"
|
|
774
|
+
: "log")),
|
|
638
775
|
event: type,
|
|
639
776
|
from: event.from || `run:${run}`,
|
|
640
777
|
level: normalizeRunOutboxLevel(event.level),
|
|
@@ -905,6 +1042,62 @@ export function cancelRun(runOrDir) {
|
|
|
905
1042
|
? { cancelled: result.stopped, ...result }
|
|
906
1043
|
: result;
|
|
907
1044
|
}
|
|
1045
|
+
function assertTerminalRun(runOrDir) {
|
|
1046
|
+
const status = getRunStatus(runOrDir);
|
|
1047
|
+
if (status.status === "running") {
|
|
1048
|
+
throw new Error("Only terminal runs can be archived or pruned.");
|
|
1049
|
+
}
|
|
1050
|
+
return status;
|
|
1051
|
+
}
|
|
1052
|
+
function archivePathFor(run, stateDir) {
|
|
1053
|
+
const archiveRoot = join(dirname(stateDir), "archived");
|
|
1054
|
+
mkdirSync(archiveRoot, { recursive: true });
|
|
1055
|
+
return join(archiveRoot, `${safeRunId(run)}-${new Date().toISOString().replace(/[:.]/g, "-")}`);
|
|
1056
|
+
}
|
|
1057
|
+
export function archiveRun(runOrDir) {
|
|
1058
|
+
const status = assertTerminalRun(runOrDir);
|
|
1059
|
+
const stateDir = String(status.state_dir);
|
|
1060
|
+
const run = String(status.run ?? basename(stateDir));
|
|
1061
|
+
const archiveDir = archivePathFor(run, stateDir);
|
|
1062
|
+
renameSync(stateDir, archiveDir);
|
|
1063
|
+
mkdirSync(stateDir, { recursive: true });
|
|
1064
|
+
const tombstone = {
|
|
1065
|
+
archived: true,
|
|
1066
|
+
archive_dir: archiveDir,
|
|
1067
|
+
original_state_dir: stateDir,
|
|
1068
|
+
run,
|
|
1069
|
+
status: status.status,
|
|
1070
|
+
ts: new Date().toISOString(),
|
|
1071
|
+
};
|
|
1072
|
+
writeJsonAtomic(join(stateDir, "archive-tombstone.json"), tombstone);
|
|
1073
|
+
return tombstone;
|
|
1074
|
+
}
|
|
1075
|
+
export function pruneRun(runOrDir, options = {}) {
|
|
1076
|
+
const status = assertTerminalRun(runOrDir);
|
|
1077
|
+
const stateDir = String(status.state_dir);
|
|
1078
|
+
const run = String(status.run ?? basename(stateDir));
|
|
1079
|
+
const manifest = resolveArtifactManifest(status.artifacts);
|
|
1080
|
+
const preserved = {};
|
|
1081
|
+
if (options.preserveArtifacts && manifest) {
|
|
1082
|
+
const preserveRoot = join(dirname(stateDir), "preserved-artifacts", safeRunId(run));
|
|
1083
|
+
mkdirSync(preserveRoot, { recursive: true });
|
|
1084
|
+
for (const [name, artifact] of Object.entries(manifest)) {
|
|
1085
|
+
if (!artifact.exists)
|
|
1086
|
+
continue;
|
|
1087
|
+
const target = join(preserveRoot, basename(artifact.path));
|
|
1088
|
+
cpSync(artifact.path, target, { force: true });
|
|
1089
|
+
preserved[name] = target;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
rmSync(stateDir, { recursive: true, force: true });
|
|
1093
|
+
return {
|
|
1094
|
+
pruned: true,
|
|
1095
|
+
preserved_artifacts: preserved,
|
|
1096
|
+
run,
|
|
1097
|
+
state_dir: stateDir,
|
|
1098
|
+
ts: new Date().toISOString(),
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
908
1101
|
export function killRun(runOrDir) {
|
|
909
1102
|
const result = stopRun(runOrDir, "SIGKILL", "run.kill");
|
|
910
1103
|
return Object.hasOwn(result, "stopped")
|
|
@@ -102,23 +102,23 @@ function getLeafCommandTemplateWarnings(config) {
|
|
|
102
102
|
const shellContent = hasAnyFlag(args, ["-c"])
|
|
103
103
|
? "shell command strings"
|
|
104
104
|
: "shell scripts";
|
|
105
|
-
warnings.push(`${config.label ?? command}: invokes ${command}; ${shellContent} are trusted executable content and are not sandboxed by command-template argv splitting.`);
|
|
105
|
+
warnings.push(`${config.label ?? command}: invokes ${command}; ${shellContent} are trusted executable content and are not sandboxed by command-template argv splitting. Mitigation: keep scripts local, reviewed, and parameterized with explicit placeholders.`);
|
|
106
106
|
}
|
|
107
107
|
if (["node", "deno", "bun"].includes(command) &&
|
|
108
108
|
hasAnyFlag(args, ["-e", "--eval"])) {
|
|
109
|
-
warnings.push(`${config.label ?? command}: invokes ${command} eval mode; code strings are trusted executable content and are not sandboxed.`);
|
|
109
|
+
warnings.push(`${config.label ?? command}: invokes ${command} eval mode; code strings are trusted executable content and are not sandboxed. Mitigation: prefer a checked-in script file or keep eval input fixed and reviewed.`);
|
|
110
110
|
}
|
|
111
111
|
if (["python", "python3", "perl", "ruby"].includes(command) &&
|
|
112
112
|
hasAnyFlag(args, ["-c", "-e"])) {
|
|
113
|
-
warnings.push(`${config.label ?? command}: invokes ${command} code-eval mode; code strings are trusted executable content and are not sandboxed.`);
|
|
113
|
+
warnings.push(`${config.label ?? command}: invokes ${command} code-eval mode; code strings are trusted executable content and are not sandboxed. Mitigation: prefer a checked-in script file or keep eval input fixed and reviewed.`);
|
|
114
114
|
}
|
|
115
115
|
if (command === "rm" &&
|
|
116
116
|
(args.some((arg) => /^-[^-]*r/.test(arg) || /^-[^-]*f/.test(arg)) ||
|
|
117
117
|
hasRiskyPathArg(args))) {
|
|
118
|
-
warnings.push(`${config.label ?? command}: removes filesystem paths; verify placeholders and paths before running trusted destructive commands.`);
|
|
118
|
+
warnings.push(`${config.label ?? command}: removes filesystem paths; verify placeholders and paths before running trusted destructive commands. Mitigation: constrain path placeholders and consider dry-run or explicit confirmation.`);
|
|
119
119
|
}
|
|
120
120
|
if (["mv", "cp", "rsync"].includes(command) && hasRiskyPathArg(args)) {
|
|
121
|
-
warnings.push(`${config.label ?? command}: mutates broad filesystem paths; verify placeholders and paths before running trusted commands.`);
|
|
121
|
+
warnings.push(`${config.label ?? command}: mutates broad filesystem paths; verify placeholders and paths before running trusted commands. Mitigation: constrain path placeholders and prefer narrow source/destination paths.`);
|
|
122
122
|
}
|
|
123
123
|
return warnings;
|
|
124
124
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared output and preview size limits.
|
|
3
|
+
* Zones: output governance, actor-message previews, inspect defaults
|
|
4
|
+
*/
|
|
5
|
+
export declare const DEFAULT_INSPECT_LINES = 40;
|
|
6
|
+
export declare const TOOL_OUTPUT_MAX_BYTES: number;
|
|
7
|
+
export declare const TOOL_OUTPUT_MAX_LINES = 2000;
|
|
8
|
+
export declare const COMPACT_PREVIEW_CHARS = 160;
|
|
9
|
+
export declare const INSPECTOR_BODY_PREVIEW_CHARS = 320;
|
|
10
|
+
export declare const ROOM_MESSAGE_PREVIEW_CHARS = 320;
|
|
11
|
+
export declare const DOCTOR_ACTION_PREVIEW_CHARS = 72;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared output and preview size limits.
|
|
3
|
+
* Zones: output governance, actor-message previews, inspect defaults
|
|
4
|
+
*/
|
|
5
|
+
export const DEFAULT_INSPECT_LINES = 40;
|
|
6
|
+
export const TOOL_OUTPUT_MAX_BYTES = 50 * 1024;
|
|
7
|
+
export const TOOL_OUTPUT_MAX_LINES = 2_000;
|
|
8
|
+
export const COMPACT_PREVIEW_CHARS = 160;
|
|
9
|
+
export const INSPECTOR_BODY_PREVIEW_CHARS = 320;
|
|
10
|
+
export const ROOM_MESSAGE_PREVIEW_CHARS = 320;
|
|
11
|
+
export const DOCTOR_ACTION_PREVIEW_CHARS = 72;
|
|
@@ -35,7 +35,7 @@ function getUpdatedAt(status) {
|
|
|
35
35
|
? status.createdAt
|
|
36
36
|
: undefined;
|
|
37
37
|
}
|
|
38
|
-
function
|
|
38
|
+
function scanRunStateDirs(stateRoot, depth = 0, seen = new Set()) {
|
|
39
39
|
if (!existsSync(stateRoot) || seen.has(stateRoot))
|
|
40
40
|
return [];
|
|
41
41
|
seen.add(stateRoot);
|
|
@@ -54,7 +54,7 @@ function listRunStateDirs(stateRoot, depth = 0, seen = new Set()) {
|
|
|
54
54
|
if (existsSync(join(child, "run.json")))
|
|
55
55
|
result.push(child);
|
|
56
56
|
if (depth + 1 < RUN_STATE_DISCOVERY_MAX_DEPTH)
|
|
57
|
-
result.push(...
|
|
57
|
+
result.push(...scanRunStateDirs(child, depth + 1, seen));
|
|
58
58
|
}
|
|
59
59
|
return result;
|
|
60
60
|
}
|
|
@@ -114,7 +114,8 @@ export function summarizeRuns(stateRoot = Paths.getRunStateRoot(), ownerId) {
|
|
|
114
114
|
total: 0,
|
|
115
115
|
};
|
|
116
116
|
}
|
|
117
|
-
const runs =
|
|
117
|
+
const runs = (AsyncRuns.readRunStateIndex(stateRoot)?.map((entry) => entry.state_dir) ??
|
|
118
|
+
scanRunStateDirs(stateRoot))
|
|
118
119
|
.map((stateDir) => observeRun(stateDir))
|
|
119
120
|
.filter((run) => Boolean(run))
|
|
120
121
|
.filter((run) => ownerId === undefined || run.ownerId === ownerId)
|
|
@@ -190,7 +191,8 @@ function summarizeRunsWithoutSubagents(stateRoot, ownerId) {
|
|
|
190
191
|
runs: [],
|
|
191
192
|
total: 0,
|
|
192
193
|
};
|
|
193
|
-
const runs =
|
|
194
|
+
const runs = (AsyncRuns.readRunStateIndex(stateRoot)?.map((entry) => entry.state_dir) ??
|
|
195
|
+
scanRunStateDirs(stateRoot))
|
|
194
196
|
.map((stateDir) => observeRun(stateDir))
|
|
195
197
|
.filter((run) => Boolean(run))
|
|
196
198
|
.filter((run) => ownerId === undefined || run.ownerId === ownerId)
|
package/dist/lib/output.js
CHANGED
|
@@ -6,9 +6,8 @@
|
|
|
6
6
|
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
import { sanitizeFilePart } from "./identity.js";
|
|
9
|
+
import * as Limits from "./limits.js";
|
|
9
10
|
import * as Paths from "./paths.js";
|
|
10
|
-
const MAX_OUTPUT_BYTES = 50 * 1024;
|
|
11
|
-
const MAX_OUTPUT_LINES = 2_000;
|
|
12
11
|
export function writeFullOutput(toolName, stream, content) {
|
|
13
12
|
try {
|
|
14
13
|
const outputRoot = join(Paths.getExtensionTmpDir(), "outputs");
|
|
@@ -45,10 +44,10 @@ export function truncateTailContent(content) {
|
|
|
45
44
|
const totalBytes = byteLength(content);
|
|
46
45
|
const lines = content.split("\n");
|
|
47
46
|
const totalLines = lines.length;
|
|
48
|
-
let output = totalLines >
|
|
49
|
-
? lines.slice(-
|
|
47
|
+
let output = totalLines > Limits.TOOL_OUTPUT_MAX_LINES
|
|
48
|
+
? lines.slice(-Limits.TOOL_OUTPUT_MAX_LINES).join("\n")
|
|
50
49
|
: content;
|
|
51
|
-
output = trimToTailBytes(output,
|
|
50
|
+
output = trimToTailBytes(output, Limits.TOOL_OUTPUT_MAX_BYTES);
|
|
52
51
|
const outputBytes = byteLength(output);
|
|
53
52
|
const outputLines = output ? output.split("\n").length : 0;
|
|
54
53
|
return {
|
|
@@ -351,6 +351,7 @@ export function summarizeDiscovery(result) {
|
|
|
351
351
|
disabled: entry.disabled,
|
|
352
352
|
invalid: entry.invalid,
|
|
353
353
|
shadows: entry.shadows,
|
|
354
|
+
...(entry.config?.imports ? { imports: entry.config.imports } : {}),
|
|
354
355
|
...(recipeUsage(entry.config)
|
|
355
356
|
? { usage: recipeUsage(entry.config) }
|
|
356
357
|
: {}),
|
|
@@ -11,9 +11,18 @@ export interface TemplateRecipeImportBinding {
|
|
|
11
11
|
values?: Record<string, unknown>;
|
|
12
12
|
}
|
|
13
13
|
export type TemplateRecipeImport = string | TemplateRecipeImportBinding;
|
|
14
|
+
export type TemplateRecipeMailboxEntry = string | {
|
|
15
|
+
type: string;
|
|
16
|
+
body_schema?: unknown;
|
|
17
|
+
ack?: boolean | string;
|
|
18
|
+
idempotency?: string;
|
|
19
|
+
requires_response?: boolean;
|
|
20
|
+
level?: string;
|
|
21
|
+
summary?: string;
|
|
22
|
+
};
|
|
14
23
|
export interface TemplateRecipeMailbox {
|
|
15
|
-
accepts?:
|
|
16
|
-
emits?:
|
|
24
|
+
accepts?: TemplateRecipeMailboxEntry[];
|
|
25
|
+
emits?: TemplateRecipeMailboxEntry[];
|
|
17
26
|
}
|
|
18
27
|
export interface TemplateRecipeDefinition {
|
|
19
28
|
name?: string;
|
|
@@ -630,12 +630,14 @@ export function readResolvedRecipeConfig(file, stack = [], options = {}) {
|
|
|
630
630
|
mailbox: {
|
|
631
631
|
...(Array.isArray(substituted.mailbox.accepts)
|
|
632
632
|
? {
|
|
633
|
-
accepts: substituted.mailbox.accepts.filter((value) => typeof value === "string"
|
|
633
|
+
accepts: substituted.mailbox.accepts.filter((value) => typeof value === "string" ||
|
|
634
|
+
(isRecord(value) && typeof value.type === "string")),
|
|
634
635
|
}
|
|
635
636
|
: {}),
|
|
636
637
|
...(Array.isArray(substituted.mailbox.emits)
|
|
637
638
|
? {
|
|
638
|
-
emits: substituted.mailbox.emits.filter((value) => typeof value === "string"
|
|
639
|
+
emits: substituted.mailbox.emits.filter((value) => typeof value === "string" ||
|
|
640
|
+
(isRecord(value) && typeof value.type === "string")),
|
|
639
641
|
}
|
|
640
642
|
: {}),
|
|
641
643
|
},
|
|
@@ -3,4 +3,5 @@
|
|
|
3
3
|
* Zones: recipe telemetry, muscle-memory cleanup evidence
|
|
4
4
|
* Owns lightweight launch counters for user-owned recipe files
|
|
5
5
|
*/
|
|
6
|
-
export
|
|
6
|
+
export type RecipeLaunchKind = "direct" | "spawn" | "tool";
|
|
7
|
+
export declare function recordRecipeLaunch(path: string, now?: Date, kind?: RecipeLaunchKind): boolean;
|