@llblab/pi-actors 0.41.0 → 0.42.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 +13 -8
- package/BACKLOG.md +2 -81
- package/CHANGELOG.md +22 -0
- package/README.md +22 -4
- package/dist/index.js +29 -82
- package/dist/lib/async-runs.d.ts +25 -5
- package/dist/lib/async-runs.js +136 -47
- package/dist/lib/automatic-review-runtime.d.ts +18 -0
- package/dist/lib/automatic-review-runtime.js +96 -0
- 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 +29 -1
- package/dist/lib/inspector-overlay.js +537 -90
- package/dist/lib/inspector.d.ts +9 -0
- package/dist/lib/inspector.js +52 -1
- package/dist/lib/observability.d.ts +39 -3
- package/dist/lib/observability.js +162 -34
- 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 +8 -2
- package/dist/lib/runs-control.js +23 -6
- 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 +1 -1
- 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 +1 -1
- 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 +6 -48
- package/dist/lib/runs-status.d.ts +5 -3
- package/dist/lib/runs-status.js +4 -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 +26 -3
- package/dist/lib/tools-local.js +4 -2
- package/dist/lib/tools-message.d.ts +1 -0
- package/dist/lib/tools-message.js +29 -17
- package/dist/lib/tools-response.d.ts +1 -1
- package/dist/lib/tools-response.js +5 -7
- package/dist/lib/tools-spawn.js +4 -1
- 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/release-gates.mjs +165 -0
- package/dist/skills/actors/SKILL.md +11 -14
- package/dist/skills/swarm/SKILL.md +1 -1
- package/docs/actor-inspector.md +32 -18
- package/docs/async-runs.md +10 -2
- package/docs/recipe-library.md +7 -3
- package/docs/template-recipes.md +4 -11
- package/docs/tool-registry.md +11 -4
- package/index.ts +29 -103
- package/lib/async-runs.ts +218 -62
- package/lib/automatic-review-runtime.ts +135 -0
- 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 +675 -105
- package/lib/inspector.ts +78 -3
- package/lib/observability.ts +219 -40
- 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 +49 -5
- 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 +1 -1
- package/lib/runs-parent-teardown.ts +257 -0
- package/lib/runs-process.ts +1 -1
- package/lib/runs-retention.ts +1 -1
- package/lib/runs-start.ts +12 -68
- package/lib/runs-status.ts +12 -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 +33 -3
- package/lib/tools-local.ts +8 -2
- package/lib/tools-message.ts +45 -30
- package/lib/tools-response.ts +5 -6
- package/lib/tools-spawn.ts +8 -1
- package/lib/tools.ts +5 -0
- package/package.json +3 -2
- package/recipes/draft-review.json +24 -0
- package/recipes/tool-review.json +24 -0
- package/scripts/release-gates.mjs +165 -0
- package/skills/actors/SKILL.md +11 -14
- package/skills/swarm/SKILL.md +1 -1
package/lib/inspector.ts
CHANGED
|
@@ -74,6 +74,7 @@ export interface ActorInspectorPreviewReadOptions {
|
|
|
74
74
|
|
|
75
75
|
export interface ActorInspectorRunItem {
|
|
76
76
|
run: string;
|
|
77
|
+
runInstanceId?: string;
|
|
77
78
|
status: string;
|
|
78
79
|
updatedAt?: string;
|
|
79
80
|
}
|
|
@@ -89,6 +90,10 @@ export function readActorInspectorRuns(
|
|
|
89
90
|
.flatMap((entry) => {
|
|
90
91
|
const stateDir = path.join(stateRoot, entry.name);
|
|
91
92
|
if (!matchesOwner(stateDir, ownerId)) return [];
|
|
93
|
+
const runMeta = readJsonFileResilient<Record<string, unknown>>(
|
|
94
|
+
path.join(stateDir, "run.json"),
|
|
95
|
+
{},
|
|
96
|
+
).value;
|
|
92
97
|
const progress = readJsonFileResilient<Record<string, unknown>>(
|
|
93
98
|
path.join(stateDir, "progress.json"),
|
|
94
99
|
{},
|
|
@@ -99,6 +104,9 @@ export function readActorInspectorRuns(
|
|
|
99
104
|
).value;
|
|
100
105
|
return [{
|
|
101
106
|
run: entry.name,
|
|
107
|
+
...(typeof runMeta.run_instance_id === "string"
|
|
108
|
+
? { runInstanceId: runMeta.run_instance_id }
|
|
109
|
+
: {}),
|
|
102
110
|
status: String(progress.phase ?? (Object.keys(result).length ? "terminal" : "unknown")),
|
|
103
111
|
...(typeof progress.updatedAt === "string"
|
|
104
112
|
? { updatedAt: progress.updatedAt }
|
|
@@ -107,15 +115,82 @@ export function readActorInspectorRuns(
|
|
|
107
115
|
: {}),
|
|
108
116
|
}];
|
|
109
117
|
})
|
|
110
|
-
.sort((left, right) =>
|
|
111
|
-
String(
|
|
112
|
-
|
|
118
|
+
.sort((left, right) => {
|
|
119
|
+
const recency = String(right.updatedAt ?? "").localeCompare(
|
|
120
|
+
String(left.updatedAt ?? ""),
|
|
121
|
+
);
|
|
122
|
+
return recency || right.run.localeCompare(left.run);
|
|
123
|
+
});
|
|
113
124
|
} catch (error) {
|
|
114
125
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
|
|
115
126
|
return [];
|
|
116
127
|
}
|
|
117
128
|
}
|
|
118
129
|
|
|
130
|
+
export interface ActorInspectorRecipeView {
|
|
131
|
+
composition: Array<Record<string, unknown>>;
|
|
132
|
+
definition?: Record<string, unknown>;
|
|
133
|
+
diagnostics: string[];
|
|
134
|
+
identity: Record<string, unknown>;
|
|
135
|
+
launch: Record<string, unknown>;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function readActorInspectorRecipe(
|
|
139
|
+
stateDir: string,
|
|
140
|
+
): ActorInspectorRecipeView {
|
|
141
|
+
const result = readJsonFileResilient<Record<string, unknown>>(
|
|
142
|
+
path.join(stateDir, "run.json"),
|
|
143
|
+
{},
|
|
144
|
+
);
|
|
145
|
+
const meta = result.value;
|
|
146
|
+
const contexts = Array.isArray(meta.recipe_context_records)
|
|
147
|
+
? meta.recipe_context_records
|
|
148
|
+
.filter((entry): entry is Record<string, unknown> =>
|
|
149
|
+
Boolean(entry && typeof entry === "object" && !Array.isArray(entry)),
|
|
150
|
+
)
|
|
151
|
+
: [];
|
|
152
|
+
const primary = contexts.find((entry) => entry.depth === 0) ?? contexts[0];
|
|
153
|
+
const definition = primary?.recipe && typeof primary.recipe === "object" &&
|
|
154
|
+
!Array.isArray(primary.recipe)
|
|
155
|
+
? primary.recipe as Record<string, unknown>
|
|
156
|
+
: undefined;
|
|
157
|
+
const redacted = (value: unknown): unknown =>
|
|
158
|
+
SessionEvidence.redactSessionEvidenceValue(value);
|
|
159
|
+
const redactedRecord = (value: Record<string, unknown>): Record<string, unknown> =>
|
|
160
|
+
redacted(Object.fromEntries(
|
|
161
|
+
Object.entries(value).filter(([, entry]) => entry !== undefined),
|
|
162
|
+
)) as Record<string, unknown>;
|
|
163
|
+
return {
|
|
164
|
+
composition: contexts.map((entry) => redactedRecord({
|
|
165
|
+
...(typeof entry.alias === "string" ? { alias: entry.alias } : {}),
|
|
166
|
+
depth: entry.depth,
|
|
167
|
+
file: entry.file,
|
|
168
|
+
import_path: entry.import_path,
|
|
169
|
+
name: entry.name,
|
|
170
|
+
})),
|
|
171
|
+
...(definition
|
|
172
|
+
? { definition: redactedRecord(definition) }
|
|
173
|
+
: {}),
|
|
174
|
+
diagnostics: result.diagnostics.map((diagnostic) => diagnostic.message),
|
|
175
|
+
identity: redactedRecord({
|
|
176
|
+
run: meta.run,
|
|
177
|
+
recipe: meta.recipe,
|
|
178
|
+
recipe_file: meta.recipe_file,
|
|
179
|
+
launch_source: meta.launch_source,
|
|
180
|
+
}),
|
|
181
|
+
launch: redactedRecord({
|
|
182
|
+
cwd: meta.cwd,
|
|
183
|
+
template: meta.template,
|
|
184
|
+
values: meta.values,
|
|
185
|
+
model_policy: meta.model_policy,
|
|
186
|
+
mailbox: meta.mailbox,
|
|
187
|
+
artifacts: meta.artifacts,
|
|
188
|
+
notification_policy: meta.notification_policy,
|
|
189
|
+
retire_when: meta.retire_when,
|
|
190
|
+
}),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
119
194
|
export interface ActorInspectorTurnItem extends SessionEvidence.SessionEvidenceTurn {
|
|
120
195
|
commandId: string;
|
|
121
196
|
diagnostics: string[];
|
package/lib/observability.ts
CHANGED
|
@@ -43,6 +43,7 @@ export interface RunObservation {
|
|
|
43
43
|
artifacts?: Record<string, string>;
|
|
44
44
|
launchSource?: AsyncRuns.AsyncRunLaunchSource;
|
|
45
45
|
modelPolicy?: Record<string, unknown>;
|
|
46
|
+
notificationPolicy?: "normal" | "silent";
|
|
46
47
|
recipeFile?: string;
|
|
47
48
|
terminalHandled?: boolean;
|
|
48
49
|
retireWhen?: string;
|
|
@@ -101,15 +102,19 @@ export function createRunUiObservationState(): RunUiObservationState {
|
|
|
101
102
|
export function readRunUiSnapshot(
|
|
102
103
|
state: RunUiObservationState,
|
|
103
104
|
ownerId: string,
|
|
105
|
+
options: { includeOutbox?: boolean; stateRoot?: string } = {},
|
|
104
106
|
): RunUiSnapshot {
|
|
105
|
-
const summary = summarizeRuns(
|
|
107
|
+
const summary = summarizeRuns(options.stateRoot, ownerId);
|
|
106
108
|
const status = renderRunStatus(summary, state.frame++);
|
|
107
109
|
return {
|
|
108
|
-
outboxEvents:
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
110
|
+
outboxEvents:
|
|
111
|
+
options.includeOutbox === false
|
|
112
|
+
? []
|
|
113
|
+
: detectRunOutboxEvents(
|
|
114
|
+
state.eventLines,
|
|
115
|
+
summary,
|
|
116
|
+
state.outboxEventIds,
|
|
117
|
+
),
|
|
113
118
|
status,
|
|
114
119
|
summary,
|
|
115
120
|
transitions: detectRunTransitions(state.observed, summary),
|
|
@@ -134,27 +139,55 @@ export function pruneRunUiObservationState(
|
|
|
134
139
|
export function deliverRunTransitionNotifications(
|
|
135
140
|
transitions: RunTransition[],
|
|
136
141
|
sink: RunUiNotificationSink,
|
|
142
|
+
inFlight: Set<string> = new Set(),
|
|
137
143
|
): void {
|
|
138
144
|
for (const transition of transitions) {
|
|
139
145
|
if (!shouldNotifyRunTransition(transition)) continue;
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
transition
|
|
152
|
-
|
|
153
|
-
)
|
|
146
|
+
const key = transition.stateDir ?? transition.run;
|
|
147
|
+
if (inFlight.has(key)) continue;
|
|
148
|
+
inFlight.add(key);
|
|
149
|
+
try {
|
|
150
|
+
const text = formatRunTransitionMessage(transition);
|
|
151
|
+
sink.notify(text, getRunTransitionNotificationType(transition));
|
|
152
|
+
if (!shouldSendRunTransitionFollowUp(transition)) continue;
|
|
153
|
+
sink.sendFollowUp({
|
|
154
|
+
customType: "pi-actors-run",
|
|
155
|
+
content: text,
|
|
156
|
+
display: true,
|
|
157
|
+
details: transition,
|
|
158
|
+
});
|
|
159
|
+
if (transition.stateDir) {
|
|
160
|
+
AsyncRuns.markRunTerminalNotificationHandled(
|
|
161
|
+
transition.stateDir,
|
|
162
|
+
transition.to,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
} finally {
|
|
166
|
+
inFlight.delete(key);
|
|
154
167
|
}
|
|
155
168
|
}
|
|
156
169
|
}
|
|
157
170
|
|
|
171
|
+
export function reconcileRunTerminalNotifications(input: {
|
|
172
|
+
inFlight?: Set<string>;
|
|
173
|
+
ownerId: string;
|
|
174
|
+
sink: RunUiNotificationSink;
|
|
175
|
+
state: RunUiObservationState;
|
|
176
|
+
stateRoot?: string;
|
|
177
|
+
}): RunUiSnapshot {
|
|
178
|
+
const snapshot = readRunUiSnapshot(input.state, input.ownerId, {
|
|
179
|
+
includeOutbox: false,
|
|
180
|
+
stateRoot: input.stateRoot,
|
|
181
|
+
});
|
|
182
|
+
deliverRunTransitionNotifications(
|
|
183
|
+
snapshot.transitions,
|
|
184
|
+
input.sink,
|
|
185
|
+
input.inFlight,
|
|
186
|
+
);
|
|
187
|
+
pruneRunUiObservationState(input.state, snapshot);
|
|
188
|
+
return snapshot;
|
|
189
|
+
}
|
|
190
|
+
|
|
158
191
|
export function deliverRunOutboxNotifications(
|
|
159
192
|
events: RunOutboxEvent[],
|
|
160
193
|
sink: RunUiNotificationSink,
|
|
@@ -189,18 +222,94 @@ export interface RunRetirementExecution {
|
|
|
189
222
|
stateDir: string;
|
|
190
223
|
}
|
|
191
224
|
|
|
225
|
+
export type RunStateWatcherDiagnosticCode =
|
|
226
|
+
| "attach_failed"
|
|
227
|
+
| "error"
|
|
228
|
+
| "removed"
|
|
229
|
+
| "rearmed";
|
|
230
|
+
|
|
231
|
+
export interface RunStateWatcherDiagnostic {
|
|
232
|
+
code: RunStateWatcherDiagnosticCode;
|
|
233
|
+
id: number;
|
|
234
|
+
message: string;
|
|
235
|
+
path: string;
|
|
236
|
+
scope: "root" | "run";
|
|
237
|
+
ts: string;
|
|
238
|
+
}
|
|
239
|
+
|
|
192
240
|
export interface RunStateWatcher {
|
|
193
241
|
close(): void;
|
|
242
|
+
getDiagnostics(): RunStateWatcherDiagnostic[];
|
|
194
243
|
refresh(): void;
|
|
195
244
|
}
|
|
196
245
|
|
|
246
|
+
const RUN_WATCHER_DIAGNOSTIC_LIMIT = 32;
|
|
247
|
+
|
|
197
248
|
export function createRunStateWatcher(input: {
|
|
198
|
-
|
|
249
|
+
exists?: (path: string) => boolean;
|
|
250
|
+
listDirectories?: (path: string) => string[];
|
|
199
251
|
onChange: () => void;
|
|
252
|
+
stateRoot?: string;
|
|
253
|
+
watchPath?: (path: string, onChange: () => void) => FSWatcher;
|
|
200
254
|
}): RunStateWatcher {
|
|
201
255
|
const stateRoot = input.stateRoot ?? Paths.getRunStateRoot();
|
|
256
|
+
const pathExists = input.exists ?? existsSync;
|
|
257
|
+
const listDirectories =
|
|
258
|
+
input.listDirectories ??
|
|
259
|
+
((path: string) =>
|
|
260
|
+
readdirSync(path, { withFileTypes: true })
|
|
261
|
+
.filter((entry) => entry.isDirectory())
|
|
262
|
+
.map((entry) => join(path, entry.name)));
|
|
263
|
+
const watchPath = input.watchPath ?? ((path, onChange) => watch(path, onChange));
|
|
264
|
+
let diagnosticId = 0;
|
|
265
|
+
let rootDegraded = false;
|
|
202
266
|
let stateRootWatcher: FSWatcher | undefined;
|
|
267
|
+
const degradedRunDirs = new Set<string>();
|
|
268
|
+
const diagnostics: RunStateWatcherDiagnostic[] = [];
|
|
269
|
+
const lastDiagnosticSignatures = new Map<string, string>();
|
|
203
270
|
const runDirWatchers = new Map<string, FSWatcher>();
|
|
271
|
+
const record = (
|
|
272
|
+
code: RunStateWatcherDiagnosticCode,
|
|
273
|
+
scope: "root" | "run",
|
|
274
|
+
path: string,
|
|
275
|
+
error?: unknown,
|
|
276
|
+
): void => {
|
|
277
|
+
const detail = error instanceof Error ? `: ${error.message}` : "";
|
|
278
|
+
const signature = `${code}${detail}`;
|
|
279
|
+
const signatureKey = `${scope}:${path}`;
|
|
280
|
+
if (lastDiagnosticSignatures.get(signatureKey) === signature) return;
|
|
281
|
+
lastDiagnosticSignatures.set(signatureKey, signature);
|
|
282
|
+
const recovery =
|
|
283
|
+
code === "rearmed"
|
|
284
|
+
? "; terminal watch acceleration restored"
|
|
285
|
+
: "; terminal reconciliation remains active and watcher rearm will retry";
|
|
286
|
+
diagnostics.push({
|
|
287
|
+
code,
|
|
288
|
+
id: ++diagnosticId,
|
|
289
|
+
message: `Run-state ${scope} watcher ${code.replace("_", " ")} for ${path}${detail}${recovery}`,
|
|
290
|
+
path,
|
|
291
|
+
scope,
|
|
292
|
+
ts: new Date().toISOString(),
|
|
293
|
+
});
|
|
294
|
+
if (diagnostics.length > RUN_WATCHER_DIAGNOSTIC_LIMIT) diagnostics.shift();
|
|
295
|
+
};
|
|
296
|
+
const removeRunWatcher = (
|
|
297
|
+
stateDir: string,
|
|
298
|
+
watcher: FSWatcher,
|
|
299
|
+
options: { degraded?: boolean; error?: unknown } = {},
|
|
300
|
+
): void => {
|
|
301
|
+
if (runDirWatchers.get(stateDir) !== watcher) return;
|
|
302
|
+
watcher.close();
|
|
303
|
+
runDirWatchers.delete(stateDir);
|
|
304
|
+
if (options.degraded === false) {
|
|
305
|
+
degradedRunDirs.delete(stateDir);
|
|
306
|
+
lastDiagnosticSignatures.delete(`run:${stateDir}`);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
degradedRunDirs.add(stateDir);
|
|
310
|
+
if (options.error) record("error", "run", stateDir, options.error);
|
|
311
|
+
record("removed", "run", stateDir);
|
|
312
|
+
};
|
|
204
313
|
const close = (): void => {
|
|
205
314
|
stateRootWatcher?.close();
|
|
206
315
|
stateRootWatcher = undefined;
|
|
@@ -208,37 +317,95 @@ export function createRunStateWatcher(input: {
|
|
|
208
317
|
runDirWatchers.clear();
|
|
209
318
|
};
|
|
210
319
|
const watchRunDir = (stateDir: string): void => {
|
|
211
|
-
if (runDirWatchers.has(stateDir) || !
|
|
320
|
+
if (runDirWatchers.has(stateDir) || !pathExists(stateDir)) return;
|
|
212
321
|
try {
|
|
213
|
-
const watcher =
|
|
214
|
-
watcher.on("error", () =>
|
|
215
|
-
watcher
|
|
216
|
-
|
|
217
|
-
});
|
|
322
|
+
const watcher = watchPath(stateDir, input.onChange);
|
|
323
|
+
watcher.on("error", (error) =>
|
|
324
|
+
removeRunWatcher(stateDir, watcher, { error }),
|
|
325
|
+
);
|
|
218
326
|
runDirWatchers.set(stateDir, watcher);
|
|
219
|
-
|
|
220
|
-
|
|
327
|
+
if (degradedRunDirs.delete(stateDir)) record("rearmed", "run", stateDir);
|
|
328
|
+
} catch (error) {
|
|
329
|
+
degradedRunDirs.add(stateDir);
|
|
330
|
+
record("attach_failed", "run", stateDir, error);
|
|
221
331
|
}
|
|
222
332
|
};
|
|
223
333
|
function refresh(): void {
|
|
224
|
-
if (!
|
|
334
|
+
if (!pathExists(stateRoot)) return;
|
|
225
335
|
if (!stateRootWatcher) {
|
|
226
336
|
try {
|
|
227
|
-
|
|
228
|
-
stateRootWatcher
|
|
229
|
-
|
|
337
|
+
const watcher = watchPath(stateRoot, input.onChange);
|
|
338
|
+
stateRootWatcher = watcher;
|
|
339
|
+
watcher.on("error", (error) => {
|
|
340
|
+
if (stateRootWatcher !== watcher) return;
|
|
341
|
+
watcher.close();
|
|
230
342
|
stateRootWatcher = undefined;
|
|
343
|
+
rootDegraded = true;
|
|
344
|
+
record("error", "root", stateRoot, error);
|
|
345
|
+
record("removed", "root", stateRoot);
|
|
231
346
|
});
|
|
232
|
-
|
|
233
|
-
|
|
347
|
+
if (rootDegraded) {
|
|
348
|
+
rootDegraded = false;
|
|
349
|
+
record("rearmed", "root", stateRoot);
|
|
350
|
+
}
|
|
351
|
+
} catch (error) {
|
|
352
|
+
rootDegraded = true;
|
|
353
|
+
record("attach_failed", "root", stateRoot, error);
|
|
234
354
|
}
|
|
235
355
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
356
|
+
let stateDirs: string[];
|
|
357
|
+
try {
|
|
358
|
+
stateDirs = listDirectories(stateRoot);
|
|
359
|
+
} catch (error) {
|
|
360
|
+
record("attach_failed", "root", stateRoot, error);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
const present = new Set(stateDirs);
|
|
364
|
+
for (const [stateDir, watcher] of runDirWatchers) {
|
|
365
|
+
if (present.has(stateDir) && pathExists(stateDir)) continue;
|
|
366
|
+
removeRunWatcher(stateDir, watcher, { degraded: false });
|
|
239
367
|
}
|
|
368
|
+
for (const stateDir of stateDirs) watchRunDir(stateDir);
|
|
240
369
|
}
|
|
241
|
-
return {
|
|
370
|
+
return {
|
|
371
|
+
close,
|
|
372
|
+
getDiagnostics: () => [...diagnostics],
|
|
373
|
+
refresh,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export interface RunTerminalReconciliationLoop {
|
|
378
|
+
close(): void;
|
|
379
|
+
reconcileNow(): void;
|
|
380
|
+
start(): void;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
export function createRunTerminalReconciliationLoop(input: {
|
|
384
|
+
intervalMs?: number;
|
|
385
|
+
onError?: (error: unknown) => void;
|
|
386
|
+
reconcile: () => void;
|
|
387
|
+
refreshWatcher: () => void;
|
|
388
|
+
}): RunTerminalReconciliationLoop {
|
|
389
|
+
const intervalMs = input.intervalMs ?? 10_000;
|
|
390
|
+
let interval: NodeJS.Timeout | undefined;
|
|
391
|
+
const reconcileNow = (): void => {
|
|
392
|
+
try {
|
|
393
|
+
input.refreshWatcher();
|
|
394
|
+
input.reconcile();
|
|
395
|
+
} catch (error) {
|
|
396
|
+
input.onError?.(error);
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
const close = (): void => {
|
|
400
|
+
if (interval) clearInterval(interval);
|
|
401
|
+
interval = undefined;
|
|
402
|
+
};
|
|
403
|
+
const start = (): void => {
|
|
404
|
+
close();
|
|
405
|
+
interval = setInterval(reconcileNow, intervalMs);
|
|
406
|
+
interval.unref?.();
|
|
407
|
+
};
|
|
408
|
+
return { close, reconcileNow, start };
|
|
242
409
|
}
|
|
243
410
|
|
|
244
411
|
export interface RunRetirementExecutorOptions {
|
|
@@ -365,6 +532,9 @@ function observeRun(stateDir: string): RunObservation | undefined {
|
|
|
365
532
|
!Array.isArray(status.model_policy)
|
|
366
533
|
? { modelPolicy: status.model_policy as Record<string, unknown> }
|
|
367
534
|
: {}),
|
|
535
|
+
...(status.notification_policy === "silent"
|
|
536
|
+
? { notificationPolicy: "silent" as const }
|
|
537
|
+
: {}),
|
|
368
538
|
...(typeof status.recipe_file === "string"
|
|
369
539
|
? { recipeFile: status.recipe_file }
|
|
370
540
|
: {}),
|
|
@@ -753,7 +923,11 @@ export function detectRunTransitions(
|
|
|
753
923
|
for (const run of summary.runs) {
|
|
754
924
|
const key = runObservationKey(run);
|
|
755
925
|
const old = previous.get(key);
|
|
756
|
-
if (
|
|
926
|
+
if (
|
|
927
|
+
run.notificationPolicy !== "silent" &&
|
|
928
|
+
!run.terminalHandled &&
|
|
929
|
+
TERMINAL.has(run.status)
|
|
930
|
+
) {
|
|
757
931
|
transitions.push({
|
|
758
932
|
from: old ?? "running",
|
|
759
933
|
run: run.run,
|
|
@@ -871,6 +1045,11 @@ export function detectRunOutboxEvents(
|
|
|
871
1045
|
const key = run.stateDir ?? run.run;
|
|
872
1046
|
const records = readOutboxRecords(run);
|
|
873
1047
|
const previousCount = previousLineCounts.get(key) ?? 0;
|
|
1048
|
+
if (run.notificationPolicy === "silent") {
|
|
1049
|
+
previousLineCounts.set(key, records.length);
|
|
1050
|
+
seenEventIds.set(key, seenEventIds.get(key) ?? new Set<string>());
|
|
1051
|
+
continue;
|
|
1052
|
+
}
|
|
874
1053
|
const start = Math.min(previousCount, records.length);
|
|
875
1054
|
const seen = seenEventIds.get(key) ?? new Set<string>();
|
|
876
1055
|
for (let index = start; index < records.length; index += 1) {
|
package/lib/paths.ts
CHANGED
|
@@ -17,6 +17,13 @@ export function getAgentDir(
|
|
|
17
17
|
: join(homedir(), ".pi", "agent");
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
export function isAutomaticRecipeReviewEnabled(
|
|
21
|
+
env: Record<string, string | undefined> = process.env,
|
|
22
|
+
): boolean {
|
|
23
|
+
const value = env.PI_ACTORS_AUTOMATIC_REVIEW?.trim().toLowerCase();
|
|
24
|
+
return value === undefined || !["0", "false", "off"].includes(value);
|
|
25
|
+
}
|
|
26
|
+
|
|
20
27
|
export interface ExtensionRuntimePaths {
|
|
21
28
|
configPath: string;
|
|
22
29
|
runStateRoot: string;
|
|
@@ -38,6 +45,42 @@ export function getRunStateRoot(agentDir = getAgentDir()): string {
|
|
|
38
45
|
return join(getExtensionTmpDir(agentDir), "runs");
|
|
39
46
|
}
|
|
40
47
|
|
|
48
|
+
export function getDraftSleepRoot(agentDir = getAgentDir()): string {
|
|
49
|
+
return join(getExtensionTmpDir(agentDir), "draft-sleep");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function getDraftSleepStatePath(agentDir = getAgentDir()): string {
|
|
53
|
+
return join(getDraftSleepRoot(agentDir), "state.json");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function getDraftSleepBatchDir(
|
|
57
|
+
batchId: string,
|
|
58
|
+
agentDir = getAgentDir(),
|
|
59
|
+
): string {
|
|
60
|
+
if (!/^[a-f0-9-]{36}$/u.test(batchId)) {
|
|
61
|
+
throw new Error("Invalid draft sleep batch id.");
|
|
62
|
+
}
|
|
63
|
+
return join(getDraftSleepRoot(agentDir), "batches", batchId);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function getToolReviewRoot(agentDir = getAgentDir()): string {
|
|
67
|
+
return join(getExtensionTmpDir(agentDir), "tool-review");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function getToolReviewStatePath(agentDir = getAgentDir()): string {
|
|
71
|
+
return join(getToolReviewRoot(agentDir), "state.json");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function getToolReviewBatchDir(
|
|
75
|
+
reviewId: string,
|
|
76
|
+
agentDir = getAgentDir(),
|
|
77
|
+
): string {
|
|
78
|
+
if (!/^[a-f0-9-]{36}$/u.test(reviewId)) {
|
|
79
|
+
throw new Error("Invalid tool review id.");
|
|
80
|
+
}
|
|
81
|
+
return join(getToolReviewRoot(agentDir), "batches", reviewId);
|
|
82
|
+
}
|
|
83
|
+
|
|
41
84
|
export function getExtensionRuntimePaths(
|
|
42
85
|
agentDir = getAgentDir(),
|
|
43
86
|
): ExtensionRuntimePaths {
|
package/lib/recipes-discovery.ts
CHANGED
|
@@ -703,32 +703,40 @@ function templatePreview(value: unknown): string | undefined {
|
|
|
703
703
|
|
|
704
704
|
export function listDraftRecipes(root: string): Array<Record<string, unknown>> {
|
|
705
705
|
return listRecipeFiles(root).map((path) => {
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
706
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
707
|
+
const id = RecipesReferences.getRecipeIdFromPath(path);
|
|
708
|
+
const bytes = readFileSync(path);
|
|
709
|
+
const fingerprint = createHash("sha256").update(bytes).digest("hex");
|
|
710
|
+
const stat = statSync(path);
|
|
711
|
+
const config = RecipesReferences.readRawRecipeConfig(path);
|
|
712
|
+
const resolved = RecipesReferences.readResolvedRecipeConfig(path);
|
|
713
|
+
const diagnostics = getRecipeConfigDiagnostics(path, resolved);
|
|
714
|
+
const after = readFileSync(path);
|
|
715
|
+
if (createHash("sha256").update(after).digest("hex") !== fingerprint) {
|
|
716
|
+
continue;
|
|
717
|
+
}
|
|
718
|
+
const sourceRun = String(config?.description ?? "").match(
|
|
719
|
+
/spawn run ([^\s]+)/,
|
|
720
|
+
)?.[1];
|
|
721
|
+
const preview = templatePreview(config?.template);
|
|
722
|
+
const riskLabels = getRecipeRiskLabels(resolved);
|
|
723
|
+
return {
|
|
724
|
+
id,
|
|
725
|
+
path,
|
|
726
|
+
sha256: fingerprint,
|
|
727
|
+
size: bytes.byteLength,
|
|
728
|
+
created_at: stat.birthtime.toISOString(),
|
|
729
|
+
modified_at: stat.mtime.toISOString(),
|
|
730
|
+
valid: Boolean(resolved),
|
|
731
|
+
diagnostics,
|
|
732
|
+
...(riskLabels.length ? { risk_labels: riskLabels } : {}),
|
|
733
|
+
...(config?.description ? { description: config.description } : {}),
|
|
734
|
+
...(sourceRun ? { source_run: sourceRun } : {}),
|
|
735
|
+
...(config?.async !== undefined ? { async: config.async } : {}),
|
|
736
|
+
...(preview ? { template_preview: preview } : {}),
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
throw new Error(`Draft changed repeatedly during inventory: ${path}`);
|
|
732
740
|
});
|
|
733
741
|
}
|
|
734
742
|
|