@llblab/pi-actors 0.41.1 → 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 +11 -6
- package/BACKLOG.md +2 -62
- package/CHANGELOG.md +15 -0
- package/README.md +22 -4
- package/dist/index.js +25 -121
- 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 +20 -3
- package/dist/lib/inspector-overlay.js +275 -74
- package/dist/lib/inspector.d.ts +9 -0
- package/dist/lib/inspector.js +48 -0
- package/dist/lib/observability.d.ts +1 -0
- package/dist/lib/observability.js +11 -1
- 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-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 +9 -8
- package/dist/skills/swarm/SKILL.md +1 -1
- package/docs/actor-inspector.md +20 -11
- package/docs/async-runs.md +9 -1
- package/docs/recipe-library.md +7 -3
- package/docs/template-recipes.md +4 -11
- package/docs/tool-registry.md +11 -4
- package/index.ts +27 -142
- 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 +325 -91
- package/lib/inspector.ts +72 -0
- package/lib/observability.ts +14 -1
- 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-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 +9 -8
- package/skills/swarm/SKILL.md +1 -1
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Automatic draft-sleep scheduling.
|
|
3
|
+
* Zones: twelve-draft trigger, immutable batch capture, silent post-turn launch
|
|
4
|
+
* Owns non-blocking review admission and persisted launch evidence, not reviewer decisions or mutation.
|
|
5
|
+
*/
|
|
6
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
7
|
+
import { existsSync, readFileSync, rmSync } from "node:fs";
|
|
8
|
+
import { basename, dirname, join } from "node:path";
|
|
9
|
+
import * as DraftConsolidationTransaction from "./draft-consolidation-transaction.js";
|
|
10
|
+
import { parseDraftReviewResult, validateDraftReviewInput, validateDraftReviewResult, } from "./draft-review.js";
|
|
11
|
+
import { withFileMutationLock, writeJsonAtomic } from "./file-state.js";
|
|
12
|
+
import * as Paths from "./paths.js";
|
|
13
|
+
import * as RecipesDiscovery from "./recipes-discovery.js";
|
|
14
|
+
import * as RecipesReferences from "./recipes-references.js";
|
|
15
|
+
import * as RecipesUsage from "./recipes-usage.js";
|
|
16
|
+
import * as ReviewProjection from "./review-projection.js";
|
|
17
|
+
export const DRAFT_SLEEP_THRESHOLD = 12;
|
|
18
|
+
const DEFAULT_DELAY_MS = 25;
|
|
19
|
+
function sha256(path) {
|
|
20
|
+
return createHash("sha256").update(readFileSync(path)).digest("hex");
|
|
21
|
+
}
|
|
22
|
+
function readState(path) {
|
|
23
|
+
try {
|
|
24
|
+
const value = JSON.parse(readFileSync(path, "utf8"));
|
|
25
|
+
return value && typeof value === "object" ? value : undefined;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function defaultActiveTools(recipeRoot) {
|
|
32
|
+
const discovered = RecipesDiscovery.discoverRecipeSources([
|
|
33
|
+
{ root: recipeRoot, defaultTool: true, mutableUsage: true },
|
|
34
|
+
{ root: Paths.getPackagedRecipeRoot() },
|
|
35
|
+
]);
|
|
36
|
+
return [...discovered.active.values()]
|
|
37
|
+
.filter((entry) => existsSync(entry.path))
|
|
38
|
+
.map((entry) => ({
|
|
39
|
+
name: entry.id,
|
|
40
|
+
path: entry.path,
|
|
41
|
+
sha256: sha256(entry.path),
|
|
42
|
+
}))
|
|
43
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
44
|
+
}
|
|
45
|
+
function draftFromRecord(record, recipeRoot, now) {
|
|
46
|
+
if (typeof record.path !== "string" ||
|
|
47
|
+
typeof record.sha256 !== "string" ||
|
|
48
|
+
typeof record.valid !== "boolean") {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
RecipesUsage.ensureRecipeLineage(record.path, now, recipeRoot);
|
|
52
|
+
const riskLabels = Array.isArray(record.risk_labels)
|
|
53
|
+
? record.risk_labels.filter((label) => typeof label === "string")
|
|
54
|
+
: [];
|
|
55
|
+
const secretTouching = riskLabels.includes("risk.secret_touching");
|
|
56
|
+
const recipe = secretTouching || !record.valid
|
|
57
|
+
? undefined
|
|
58
|
+
: RecipesReferences.readRawRecipeConfig(record.path);
|
|
59
|
+
const usage = RecipesUsage.readRecipeUsage(record.path, recipeRoot);
|
|
60
|
+
if (typeof usage?.demoted_fingerprint === "string" &&
|
|
61
|
+
usage.demoted_fingerprint === usage.fingerprint) {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
...(record.diagnostics !== undefined
|
|
66
|
+
? { diagnostics: record.diagnostics }
|
|
67
|
+
: {}),
|
|
68
|
+
path: record.path,
|
|
69
|
+
...(recipe ? { recipe } : {}),
|
|
70
|
+
riskLabels,
|
|
71
|
+
sha256: record.sha256,
|
|
72
|
+
...(usage ? { usage } : {}),
|
|
73
|
+
valid: record.valid && !secretTouching,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function captureDraftSleepBatchLocked(options, draftRoot, recipeRoot, statePath) {
|
|
77
|
+
const state = readState(statePath);
|
|
78
|
+
if (state?.phase === "captured" ||
|
|
79
|
+
state?.phase === "launched" ||
|
|
80
|
+
state?.phase === "processing_failed")
|
|
81
|
+
return undefined;
|
|
82
|
+
return withFileMutationLock(draftRoot, () => {
|
|
83
|
+
const records = RecipesDiscovery.listDraftRecipes(draftRoot);
|
|
84
|
+
const capturedAt = (options.now ?? (() => new Date()))();
|
|
85
|
+
const candidates = records.flatMap((record) => {
|
|
86
|
+
const draft = draftFromRecord(record, recipeRoot, capturedAt);
|
|
87
|
+
return draft ? [draft] : [];
|
|
88
|
+
});
|
|
89
|
+
if (candidates.length < DRAFT_SLEEP_THRESHOLD)
|
|
90
|
+
return undefined;
|
|
91
|
+
const batchId = (options.createBatchId ?? randomUUID)();
|
|
92
|
+
const batchDir = (options.batchRoot ?? Paths.getDraftSleepBatchDir)(batchId);
|
|
93
|
+
const inputPath = join(batchDir, "input.json");
|
|
94
|
+
const drafts = candidates.slice(0, DRAFT_SLEEP_THRESHOLD);
|
|
95
|
+
const input = {
|
|
96
|
+
activeTools: options.activeTools?.() ?? defaultActiveTools(recipeRoot),
|
|
97
|
+
batchId,
|
|
98
|
+
createdAt: capturedAt.toISOString(),
|
|
99
|
+
drafts,
|
|
100
|
+
};
|
|
101
|
+
const validation = validateDraftReviewInput(input);
|
|
102
|
+
if (!validation.ok) {
|
|
103
|
+
throw new Error(`Draft sleep input rejected: ${validation.errors.join("; ")}`);
|
|
104
|
+
}
|
|
105
|
+
writeJsonAtomic(inputPath, input);
|
|
106
|
+
const reviewerInputPath = join(batchDir, "review-input.json");
|
|
107
|
+
writeJsonAtomic(reviewerInputPath, ReviewProjection.projectDraftReviewInput(input));
|
|
108
|
+
const sourcePaths = drafts.map((draft) => draft.path);
|
|
109
|
+
writeJsonAtomic(statePath, {
|
|
110
|
+
attempts: 0,
|
|
111
|
+
batchId,
|
|
112
|
+
inputPath,
|
|
113
|
+
phase: "captured",
|
|
114
|
+
reviewerInputPath,
|
|
115
|
+
sourcePaths,
|
|
116
|
+
updatedAt: input.createdAt,
|
|
117
|
+
});
|
|
118
|
+
return { batchId, inputPath, reviewerInputPath, sourcePaths };
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
export function captureDraftSleepBatch(options = {}) {
|
|
122
|
+
const draftRoot = options.draftRoot ?? Paths.getRecipeDraftRoot();
|
|
123
|
+
const recipeRoot = options.recipeRoot ?? Paths.getRecipeRoot();
|
|
124
|
+
const statePath = options.statePath ?? Paths.getDraftSleepStatePath();
|
|
125
|
+
return withFileMutationLock(statePath, () => captureDraftSleepBatchLocked(options, draftRoot, recipeRoot, statePath));
|
|
126
|
+
}
|
|
127
|
+
function completeDraftSleepTransaction(transaction, state, deps, cycleDir, recipeRoot) {
|
|
128
|
+
try {
|
|
129
|
+
if (transaction.phase !== "committed") {
|
|
130
|
+
return {
|
|
131
|
+
error: `draft transaction remained ${transaction.phase}`,
|
|
132
|
+
outcome: "processing_failed",
|
|
133
|
+
stage: "transaction",
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
const committedDecisions = transaction.plan.drafts;
|
|
137
|
+
for (const decision of committedDecisions) {
|
|
138
|
+
if (decision.action === "promote") {
|
|
139
|
+
const targetPath = join(recipeRoot, `${decision.target}.json`);
|
|
140
|
+
if (!RecipesUsage.moveRecipeUsage(decision.draft, targetPath, recipeRoot)) {
|
|
141
|
+
throw new Error(`Could not transfer promoted recipe lineage: ${decision.draft}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
else if (!RecipesUsage.retireRecipeUsage(decision.draft, recipeRoot)) {
|
|
145
|
+
throw new Error(`Could not retire discarded recipe lineage: ${decision.draft}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const evidencePath = join(cycleDir, "sleep-evidence.json");
|
|
149
|
+
writeJsonAtomic(evidencePath, {
|
|
150
|
+
batchId: state.batchId,
|
|
151
|
+
decisions: committedDecisions,
|
|
152
|
+
garbageCollection: {
|
|
153
|
+
reviewedSources: committedDecisions.length,
|
|
154
|
+
status: "pending",
|
|
155
|
+
},
|
|
156
|
+
transactionEvidencePath: transaction.evidencePath,
|
|
157
|
+
ts: (deps.now ?? (() => new Date()))().toISOString(),
|
|
158
|
+
});
|
|
159
|
+
return {
|
|
160
|
+
cleanup: () => {
|
|
161
|
+
rmSync(join(cycleDir, "quarantine"), { recursive: true, force: true });
|
|
162
|
+
rmSync(join(cycleDir, "backups"), { recursive: true, force: true });
|
|
163
|
+
writeJsonAtomic(evidencePath, {
|
|
164
|
+
batchId: state.batchId,
|
|
165
|
+
decisions: committedDecisions,
|
|
166
|
+
garbageCollection: {
|
|
167
|
+
reviewedSources: committedDecisions.length,
|
|
168
|
+
status: "completed",
|
|
169
|
+
},
|
|
170
|
+
transactionEvidencePath: transaction.evidencePath,
|
|
171
|
+
ts: (deps.now ?? (() => new Date()))().toISOString(),
|
|
172
|
+
});
|
|
173
|
+
},
|
|
174
|
+
evidencePath,
|
|
175
|
+
outcome: "completed",
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
return {
|
|
180
|
+
error: error instanceof Error ? error.message : String(error),
|
|
181
|
+
outcome: "processing_failed",
|
|
182
|
+
stage: "transaction_or_lineage",
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
export function processDraftSleepReview(state, deps) {
|
|
187
|
+
const recipeRoot = deps.recipeRoot ?? Paths.getRecipeRoot();
|
|
188
|
+
const cycleDir = dirname(state.inputPath);
|
|
189
|
+
if (existsSync(join(cycleDir, "journal.json"))) {
|
|
190
|
+
const firstSource = state.sourcePaths[0];
|
|
191
|
+
if (!firstSource) {
|
|
192
|
+
return {
|
|
193
|
+
error: "captured draft source is unavailable for transaction recovery",
|
|
194
|
+
outcome: "processing_failed",
|
|
195
|
+
stage: "transaction",
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
try {
|
|
199
|
+
const transaction = DraftConsolidationTransaction.recoverDraftConsolidationCycle({
|
|
200
|
+
cycleDir,
|
|
201
|
+
draftRoot: dirname(firstSource),
|
|
202
|
+
recipeRoot,
|
|
203
|
+
});
|
|
204
|
+
return completeDraftSleepTransaction(transaction, state, deps, cycleDir, recipeRoot);
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
return {
|
|
208
|
+
error: error instanceof Error ? error.message : String(error),
|
|
209
|
+
outcome: "processing_failed",
|
|
210
|
+
stage: "transaction_or_lineage",
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (!state.runId) {
|
|
215
|
+
return { error: "review run id is unavailable", outcome: "review_failed", stage: "review_state" };
|
|
216
|
+
}
|
|
217
|
+
let status;
|
|
218
|
+
try {
|
|
219
|
+
status = deps.getRunStatus(state.runId);
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
return {
|
|
223
|
+
error: error instanceof Error ? error.message : String(error),
|
|
224
|
+
outcome: "review_failed",
|
|
225
|
+
stage: "review_state",
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
if (status.status === "running")
|
|
229
|
+
return { outcome: "pending" };
|
|
230
|
+
if (status.status !== "done" || typeof status.state_dir !== "string") {
|
|
231
|
+
return {
|
|
232
|
+
error: `review run ended with status ${String(status.status ?? "unknown")}`,
|
|
233
|
+
outcome: "review_failed",
|
|
234
|
+
stage: "review_run",
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
let input;
|
|
238
|
+
let result;
|
|
239
|
+
try {
|
|
240
|
+
input = JSON.parse(readFileSync(state.inputPath, "utf8"));
|
|
241
|
+
result = parseDraftReviewResult(readFileSync(join(status.state_dir, "stdout.log"), "utf8"));
|
|
242
|
+
result = ReviewProjection.restoreDraftReviewResult(input, result);
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
return {
|
|
246
|
+
error: error instanceof Error ? error.message : String(error),
|
|
247
|
+
outcome: "review_failed",
|
|
248
|
+
stage: "result_parse",
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
const validation = validateDraftReviewResult(input, result);
|
|
252
|
+
if (!validation.ok) {
|
|
253
|
+
return {
|
|
254
|
+
error: validation.errors.join("; "),
|
|
255
|
+
outcome: "review_failed",
|
|
256
|
+
stage: "result_validation",
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
const draftRoot = dirname(input.drafts[0].path);
|
|
260
|
+
const capturedDrafts = new Map(input.drafts.map((draft) => [draft.path, draft]));
|
|
261
|
+
const plan = {
|
|
262
|
+
createdAt: result.createdAt,
|
|
263
|
+
cycleId: state.batchId,
|
|
264
|
+
drafts: result.decisions.map((decision) => {
|
|
265
|
+
const source = capturedDrafts.get(decision.draft);
|
|
266
|
+
if (decision.action === "promote" && !source?.recipe) {
|
|
267
|
+
throw new Error(`Promoted draft source recipe is unavailable: ${decision.draft}`);
|
|
268
|
+
}
|
|
269
|
+
return {
|
|
270
|
+
action: decision.action,
|
|
271
|
+
draft: decision.draft,
|
|
272
|
+
rationale: decision.rationale,
|
|
273
|
+
...(decision.action === "promote" ? { recipe: source.recipe } : {}),
|
|
274
|
+
sha256: decision.sha256,
|
|
275
|
+
...(decision.target ? { target: decision.target } : {}),
|
|
276
|
+
...(decision.action === "promote" ? { targetSha256: null } : {}),
|
|
277
|
+
};
|
|
278
|
+
}),
|
|
279
|
+
};
|
|
280
|
+
const inventory = input.drafts.map((draft) => ({
|
|
281
|
+
id: basename(draft.path).replace(/\.(?:json|md)$/u, ""),
|
|
282
|
+
path: draft.path,
|
|
283
|
+
sha256: draft.sha256,
|
|
284
|
+
valid: draft.valid,
|
|
285
|
+
...(draft.diagnostics !== undefined ? { diagnostics: draft.diagnostics } : {}),
|
|
286
|
+
...(draft.riskLabels.length > 0 ? { riskLabels: draft.riskLabels } : {}),
|
|
287
|
+
}));
|
|
288
|
+
try {
|
|
289
|
+
const transaction = DraftConsolidationTransaction.applyDraftConsolidationPlan(plan, {
|
|
290
|
+
cycleDir,
|
|
291
|
+
draftRoot,
|
|
292
|
+
inventory,
|
|
293
|
+
recipeRoot,
|
|
294
|
+
sourceScope: "batch",
|
|
295
|
+
});
|
|
296
|
+
return completeDraftSleepTransaction(transaction, state, deps, cycleDir, recipeRoot);
|
|
297
|
+
}
|
|
298
|
+
catch (error) {
|
|
299
|
+
return {
|
|
300
|
+
error: error instanceof Error ? error.message : String(error),
|
|
301
|
+
outcome: "processing_failed",
|
|
302
|
+
stage: "transaction_or_lineage",
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
function cleanupCompletedState(state) {
|
|
307
|
+
const cycleDir = dirname(state.inputPath);
|
|
308
|
+
rmSync(join(cycleDir, "quarantine"), { recursive: true, force: true });
|
|
309
|
+
rmSync(join(cycleDir, "backups"), { recursive: true, force: true });
|
|
310
|
+
if (!state.evidencePath || !existsSync(state.evidencePath))
|
|
311
|
+
return;
|
|
312
|
+
try {
|
|
313
|
+
const evidence = JSON.parse(readFileSync(state.evidencePath, "utf8"));
|
|
314
|
+
const garbageCollection = evidence.garbageCollection;
|
|
315
|
+
if (garbageCollection?.status === "completed")
|
|
316
|
+
return;
|
|
317
|
+
writeJsonAtomic(state.evidencePath, {
|
|
318
|
+
...evidence,
|
|
319
|
+
garbageCollection: {
|
|
320
|
+
...garbageCollection,
|
|
321
|
+
status: "completed",
|
|
322
|
+
},
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
/* bounded diagnostic evidence remains available for later inspection */
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
export function createDraftSleepScheduler(deps) {
|
|
330
|
+
const statePath = deps.statePath ?? Paths.getDraftSleepStatePath();
|
|
331
|
+
const draftRoot = deps.draftRoot ?? Paths.getRecipeDraftRoot();
|
|
332
|
+
const recipeRoot = deps.recipeRoot ?? Paths.getRecipeRoot();
|
|
333
|
+
const now = deps.now ?? (() => new Date());
|
|
334
|
+
let timer;
|
|
335
|
+
let closed = false;
|
|
336
|
+
const close = () => {
|
|
337
|
+
closed = true;
|
|
338
|
+
if (timer)
|
|
339
|
+
clearTimeout(timer);
|
|
340
|
+
timer = undefined;
|
|
341
|
+
};
|
|
342
|
+
const schedule = () => {
|
|
343
|
+
if (closed || timer)
|
|
344
|
+
return;
|
|
345
|
+
timer = setTimeout(() => {
|
|
346
|
+
timer = undefined;
|
|
347
|
+
if (closed || deps.hasActiveActors())
|
|
348
|
+
return;
|
|
349
|
+
try {
|
|
350
|
+
withFileMutationLock(statePath, () => {
|
|
351
|
+
const existing = readState(statePath);
|
|
352
|
+
if (existing?.phase === "completed") {
|
|
353
|
+
cleanupCompletedState(existing);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
if (existing?.phase === "launched" ||
|
|
357
|
+
existing?.phase === "processing_failed") {
|
|
358
|
+
if (!deps.process || (existing.processingAttempts ?? 0) >= 3)
|
|
359
|
+
return;
|
|
360
|
+
let processed;
|
|
361
|
+
try {
|
|
362
|
+
processed = deps.process(existing);
|
|
363
|
+
}
|
|
364
|
+
catch (error) {
|
|
365
|
+
processed = {
|
|
366
|
+
error: error instanceof Error ? error.message : String(error),
|
|
367
|
+
outcome: "processing_failed",
|
|
368
|
+
stage: "result_processing",
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
if (processed.outcome === "pending")
|
|
372
|
+
return;
|
|
373
|
+
if (processed.outcome === "completed") {
|
|
374
|
+
const completed = {
|
|
375
|
+
...existing,
|
|
376
|
+
...(processed.evidencePath
|
|
377
|
+
? { evidencePath: processed.evidencePath }
|
|
378
|
+
: {}),
|
|
379
|
+
failedStage: undefined,
|
|
380
|
+
lastError: undefined,
|
|
381
|
+
nextAction: undefined,
|
|
382
|
+
phase: "completed",
|
|
383
|
+
updatedAt: now().toISOString(),
|
|
384
|
+
};
|
|
385
|
+
writeJsonAtomic(statePath, completed);
|
|
386
|
+
processed.cleanup?.();
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
if (processed.outcome === "processing_failed") {
|
|
390
|
+
writeJsonAtomic(statePath, {
|
|
391
|
+
...existing,
|
|
392
|
+
failedStage: processed.stage ?? "result_processing",
|
|
393
|
+
lastError: processed.error ?? "automatic draft review processing failed",
|
|
394
|
+
nextAction: "message to=tool:pi-actors type=review.retry body={\"scope\":\"draft\"}",
|
|
395
|
+
phase: "processing_failed",
|
|
396
|
+
processingAttempts: (existing.processingAttempts ?? 0) + 1,
|
|
397
|
+
updatedAt: now().toISOString(),
|
|
398
|
+
});
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
writeJsonAtomic(statePath, {
|
|
402
|
+
...existing,
|
|
403
|
+
failedStage: processed.stage ?? "review_result",
|
|
404
|
+
lastError: processed.error ?? "automatic draft reviewer failed",
|
|
405
|
+
nextAction: "message to=tool:pi-actors type=review.retry body={\"scope\":\"draft\"}",
|
|
406
|
+
phase: "failed",
|
|
407
|
+
runId: undefined,
|
|
408
|
+
updatedAt: now().toISOString(),
|
|
409
|
+
});
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
let batch;
|
|
413
|
+
try {
|
|
414
|
+
batch = existing?.phase === "captured" || existing?.phase === "failed"
|
|
415
|
+
? {
|
|
416
|
+
batchId: existing.batchId,
|
|
417
|
+
inputPath: existing.inputPath,
|
|
418
|
+
reviewerInputPath: existing.reviewerInputPath,
|
|
419
|
+
sourcePaths: existing.sourcePaths,
|
|
420
|
+
}
|
|
421
|
+
: captureDraftSleepBatchLocked(deps, draftRoot, recipeRoot, statePath);
|
|
422
|
+
if (!batch)
|
|
423
|
+
return;
|
|
424
|
+
const attempts = (existing?.batchId === batch.batchId ? existing.attempts : 0) + 1;
|
|
425
|
+
if (attempts > 3)
|
|
426
|
+
return;
|
|
427
|
+
const launched = deps.launch(batch);
|
|
428
|
+
writeJsonAtomic(statePath, {
|
|
429
|
+
attempts,
|
|
430
|
+
batchId: batch.batchId,
|
|
431
|
+
inputPath: batch.inputPath,
|
|
432
|
+
phase: "launched",
|
|
433
|
+
reviewerInputPath: batch.reviewerInputPath,
|
|
434
|
+
runId: launched.run,
|
|
435
|
+
sourcePaths: batch.sourcePaths,
|
|
436
|
+
updatedAt: now().toISOString(),
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
catch (error) {
|
|
440
|
+
if (!batch)
|
|
441
|
+
return;
|
|
442
|
+
const prior = readState(statePath);
|
|
443
|
+
writeJsonAtomic(statePath, {
|
|
444
|
+
attempts: (prior?.batchId === batch.batchId ? prior.attempts : 0) + 1,
|
|
445
|
+
failedStage: "review_launch",
|
|
446
|
+
lastError: error instanceof Error ? error.message : String(error),
|
|
447
|
+
nextAction: "message to=tool:pi-actors type=review.retry body={\"scope\":\"draft\"}",
|
|
448
|
+
batchId: batch.batchId,
|
|
449
|
+
inputPath: batch.inputPath,
|
|
450
|
+
phase: "failed",
|
|
451
|
+
reviewerInputPath: batch.reviewerInputPath,
|
|
452
|
+
sourcePaths: batch.sourcePaths,
|
|
453
|
+
updatedAt: now().toISOString(),
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
catch {
|
|
459
|
+
/* another session owns the durable admission transition */
|
|
460
|
+
}
|
|
461
|
+
}, deps.delayMs ?? DEFAULT_DELAY_MS);
|
|
462
|
+
timer.unref?.();
|
|
463
|
+
};
|
|
464
|
+
return { close, schedule };
|
|
465
|
+
}
|
|
466
|
+
export function draftSleepRunId(batchId) {
|
|
467
|
+
return `draft-sleep-${basename(batchId)}`;
|
|
468
|
+
}
|
package/dist/lib/file-state.d.ts
CHANGED
|
@@ -3,5 +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
|
-
export declare function
|
|
6
|
+
export declare function mutationLockPath(path: string): string;
|
|
7
|
+
export interface FileMutationLockOptions {
|
|
8
|
+
onBeforeReclaimRemove?(): void;
|
|
9
|
+
onContention?(): void;
|
|
10
|
+
onRemovalContention?(): void;
|
|
11
|
+
}
|
|
12
|
+
export declare function acquireFileMutationLock(path: string, options?: FileMutationLockOptions): () => void;
|
|
13
|
+
export declare function withFileMutationLock<T>(path: string, mutate: () => T, options?: FileMutationLockOptions): T;
|
|
7
14
|
export declare function writeJsonAtomic(path: string, value: unknown): void;
|
package/dist/lib/file-state.js
CHANGED
|
@@ -27,39 +27,118 @@ function canonicalMutationPath(path) {
|
|
|
27
27
|
const canonical = resolve(canonicalAncestor, ...suffix);
|
|
28
28
|
return process.platform === "win32" ? canonical.toLowerCase() : canonical;
|
|
29
29
|
}
|
|
30
|
-
function mutationLockPath(path) {
|
|
30
|
+
export function mutationLockPath(path) {
|
|
31
31
|
const key = createHash("sha256")
|
|
32
32
|
.update(canonicalMutationPath(path))
|
|
33
33
|
.digest("hex");
|
|
34
34
|
return join(FILE_MUTATION_LOCK_ROOT, `${key}.lock`);
|
|
35
35
|
}
|
|
36
|
-
function
|
|
36
|
+
function lockOwnerStatus(lockPath) {
|
|
37
37
|
try {
|
|
38
38
|
const owner = JSON.parse(readFileSync(join(lockPath, "owner.json"), "utf8"));
|
|
39
39
|
const pid = Number(owner.pid);
|
|
40
40
|
if (!Number.isInteger(pid) || pid <= 0)
|
|
41
|
-
return
|
|
41
|
+
return "unknown";
|
|
42
42
|
try {
|
|
43
43
|
process.kill(pid, 0);
|
|
44
|
-
return
|
|
44
|
+
return "alive";
|
|
45
45
|
}
|
|
46
46
|
catch (error) {
|
|
47
|
-
return error.code === "ESRCH"
|
|
47
|
+
return error.code === "ESRCH"
|
|
48
|
+
? "dead"
|
|
49
|
+
: "unknown";
|
|
48
50
|
}
|
|
49
51
|
}
|
|
50
52
|
catch {
|
|
53
|
+
return "unknown";
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function readLockToken(lockPath) {
|
|
57
|
+
try {
|
|
58
|
+
const owner = JSON.parse(readFileSync(join(lockPath, "owner.json"), "utf8"));
|
|
59
|
+
return typeof owner.token === "string" ? owner.token : undefined;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function tryReclaimRemovalBoundary(reclaimPath) {
|
|
66
|
+
if (!existsSync(reclaimPath))
|
|
67
|
+
return;
|
|
68
|
+
try {
|
|
69
|
+
const inspectedToken = readLockToken(reclaimPath);
|
|
70
|
+
const age = Date.now() - statSync(reclaimPath).mtimeMs;
|
|
71
|
+
const ownerStatus = lockOwnerStatus(reclaimPath);
|
|
72
|
+
if ((ownerStatus === "dead" ||
|
|
73
|
+
(ownerStatus === "unknown" && age > FILE_MUTATION_LOCK_STALE_MS)) &&
|
|
74
|
+
readLockToken(reclaimPath) === inspectedToken) {
|
|
75
|
+
rmSync(reclaimPath, { recursive: true, force: true });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
/* another contender changed the boundary */
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function withRemovalBoundary(lockPath, action) {
|
|
83
|
+
const reclaimPath = `${lockPath}.reclaim`;
|
|
84
|
+
const token = randomUUID();
|
|
85
|
+
try {
|
|
86
|
+
mkdirSync(reclaimPath);
|
|
87
|
+
try {
|
|
88
|
+
writeFileSync(join(reclaimPath, "owner.json"), `${JSON.stringify({ pid: process.pid, token, acquired_at: new Date().toISOString() })}\n`, "utf8");
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
rmSync(reclaimPath, { recursive: true, force: true });
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
tryReclaimRemovalBoundary(reclaimPath);
|
|
51
97
|
return false;
|
|
52
98
|
}
|
|
99
|
+
try {
|
|
100
|
+
action();
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
if (readLockToken(reclaimPath) === token) {
|
|
105
|
+
rmSync(reclaimPath, { recursive: true, force: true });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
53
108
|
}
|
|
54
|
-
|
|
109
|
+
function tryReclaimMutationLock(lockPath, options) {
|
|
110
|
+
let reclaimed = false;
|
|
111
|
+
if (!withRemovalBoundary(lockPath, () => {
|
|
112
|
+
if (!existsSync(lockPath)) {
|
|
113
|
+
reclaimed = true;
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const inspectedToken = readLockToken(lockPath);
|
|
117
|
+
const age = Date.now() - statSync(lockPath).mtimeMs;
|
|
118
|
+
const ownerStatus = lockOwnerStatus(lockPath);
|
|
119
|
+
if ((ownerStatus === "dead" ||
|
|
120
|
+
(ownerStatus === "unknown" && age > FILE_MUTATION_LOCK_STALE_MS)) &&
|
|
121
|
+
readLockToken(lockPath) === inspectedToken) {
|
|
122
|
+
options.onBeforeReclaimRemove?.();
|
|
123
|
+
rmSync(lockPath, { recursive: true, force: true });
|
|
124
|
+
reclaimed = true;
|
|
125
|
+
}
|
|
126
|
+
})) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
return reclaimed;
|
|
130
|
+
}
|
|
131
|
+
export function acquireFileMutationLock(path, options = {}) {
|
|
55
132
|
mkdirSync(FILE_MUTATION_LOCK_ROOT, { recursive: true });
|
|
56
133
|
const lockPath = mutationLockPath(path);
|
|
57
134
|
const deadline = Date.now() + FILE_MUTATION_LOCK_TIMEOUT_MS;
|
|
135
|
+
const token = randomUUID();
|
|
136
|
+
let contentionReported = false;
|
|
58
137
|
for (;;) {
|
|
59
138
|
try {
|
|
60
139
|
mkdirSync(lockPath);
|
|
61
140
|
try {
|
|
62
|
-
writeFileSync(join(lockPath, "owner.json"), `${JSON.stringify({ pid: process.pid, acquired_at: new Date().toISOString() })}\n`, "utf8");
|
|
141
|
+
writeFileSync(join(lockPath, "owner.json"), `${JSON.stringify({ pid: process.pid, token, acquired_at: new Date().toISOString() })}\n`, "utf8");
|
|
63
142
|
}
|
|
64
143
|
catch (error) {
|
|
65
144
|
rmSync(lockPath, { recursive: true, force: true });
|
|
@@ -68,17 +147,11 @@ export function withFileMutationLock(path, mutate) {
|
|
|
68
147
|
break;
|
|
69
148
|
}
|
|
70
149
|
catch (error) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
lockOwnerIsDead(lockPath)) {
|
|
75
|
-
rmSync(lockPath, { recursive: true, force: true });
|
|
76
|
-
continue;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
catch {
|
|
80
|
-
continue;
|
|
150
|
+
if (!contentionReported) {
|
|
151
|
+
contentionReported = true;
|
|
152
|
+
options.onContention?.();
|
|
81
153
|
}
|
|
154
|
+
tryReclaimMutationLock(lockPath, options);
|
|
82
155
|
if (Date.now() >= deadline) {
|
|
83
156
|
throw new Error(`Timed out waiting for file mutation lock: ${canonicalMutationPath(path)}`, {
|
|
84
157
|
cause: error,
|
|
@@ -87,11 +160,35 @@ export function withFileMutationLock(path, mutate) {
|
|
|
87
160
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
|
|
88
161
|
}
|
|
89
162
|
}
|
|
163
|
+
let released = false;
|
|
164
|
+
return () => {
|
|
165
|
+
if (released)
|
|
166
|
+
return;
|
|
167
|
+
released = true;
|
|
168
|
+
const deadline = Date.now() + FILE_MUTATION_LOCK_TIMEOUT_MS;
|
|
169
|
+
let removalContentionReported = false;
|
|
170
|
+
while (!withRemovalBoundary(lockPath, () => {
|
|
171
|
+
if (readLockToken(lockPath) === token) {
|
|
172
|
+
rmSync(lockPath, { recursive: true, force: true });
|
|
173
|
+
}
|
|
174
|
+
})) {
|
|
175
|
+
if (!removalContentionReported) {
|
|
176
|
+
removalContentionReported = true;
|
|
177
|
+
options.onRemovalContention?.();
|
|
178
|
+
}
|
|
179
|
+
if (Date.now() >= deadline)
|
|
180
|
+
return;
|
|
181
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
export function withFileMutationLock(path, mutate, options = {}) {
|
|
186
|
+
const release = acquireFileMutationLock(path, options);
|
|
90
187
|
try {
|
|
91
188
|
return mutate();
|
|
92
189
|
}
|
|
93
190
|
finally {
|
|
94
|
-
|
|
191
|
+
release();
|
|
95
192
|
}
|
|
96
193
|
}
|
|
97
194
|
export function writeJsonAtomic(path, value) {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Actor Inspector run actions.
|
|
3
|
+
* Owns: exact-session run-action authorization, canonical kill routing, and bounded operator feedback.
|
|
4
|
+
*/
|
|
5
|
+
export interface ActorInspectorKillResult {
|
|
6
|
+
ok: boolean;
|
|
7
|
+
message: string;
|
|
8
|
+
}
|
|
9
|
+
export interface ActorInspectorKillDeps {
|
|
10
|
+
getRunStatus: (runOrDir: string) => Record<string, unknown>;
|
|
11
|
+
killRun: (runOrDir: string, expected: {
|
|
12
|
+
ownerId: string;
|
|
13
|
+
runInstanceId: string;
|
|
14
|
+
}) => Record<string, unknown>;
|
|
15
|
+
}
|
|
16
|
+
export declare function killOwnedInspectorRun(ownerId: string, run: string, stateRoot: string, expectedRunInstanceId: string, deps: ActorInspectorKillDeps): ActorInspectorKillResult;
|