pi-crew 0.9.41 → 0.9.44
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/CHANGELOG.md +253 -0
- package/README.md +14 -0
- package/dist/build-meta.json +6450 -12419
- package/dist/index.mjs +57998 -68417
- package/dist/index.mjs.map +4 -4
- package/docs/migration/atomic-write-v2-migration.md +2 -0
- package/docs/superpowers/plans/2026-06-09-fallow-patterns-adoption.md +2 -0
- package/package.json +4 -2
- package/scripts/README.md +6 -0
- package/scripts/build-bundle.mjs +2 -1
- package/scripts/check-all-skills.ts +3 -3
- package/scripts/dev-runner.mjs +106 -0
- package/scripts/postinstall.mjs +6 -2
- package/scripts/verify-skill.ts +2 -2
- package/src/agents/agent-config.ts +1 -1
- package/src/config/config.ts +20 -1
- package/src/config/drift-detector.ts +97 -15
- package/src/config/resilient-parser.ts +4 -4
- package/src/config/types.ts +0 -2
- package/src/extension/crew-cleanup.ts +17 -3
- package/src/extension/crew-vibes/config.ts +1 -1
- package/src/extension/crew-vibes/font-detect.ts +16 -3
- package/src/extension/crew-vibes/index.ts +1 -1
- package/src/extension/cross-extension-rpc.ts +26 -9
- package/src/extension/management.ts +6 -5
- package/src/extension/register.ts +58 -1573
- package/src/extension/registration/command-registration.ts +57 -0
- package/src/extension/registration/commands.ts +7 -2
- package/src/extension/registration/context-builder.ts +131 -0
- package/src/extension/registration/crash-recovery-cache.ts +54 -0
- package/src/extension/registration/foreground-run-controller.ts +238 -0
- package/src/extension/registration/hook-registration.ts +119 -0
- package/src/extension/registration/lazy-configurers.ts +124 -0
- package/src/extension/registration/lifecycle-handlers.ts +731 -0
- package/src/extension/registration/observability.ts +33 -4
- package/src/extension/registration/registration-types.ts +149 -0
- package/src/extension/registration/runtime-cleanup.ts +230 -0
- package/src/extension/registration/subagent-manager-setup.ts +211 -0
- package/src/extension/registration/tool-registration.ts +50 -0
- package/src/extension/registration/wire-cross-extension.ts +37 -0
- package/src/extension/rpc-hmac.ts +21 -1
- package/src/extension/run-import.ts +3 -3
- package/src/extension/team-tool/auto-summarize.ts +4 -4
- package/src/extension/team-tool/cancel.ts +5 -5
- package/src/extension/team-tool/chain-executor.ts +5 -4
- package/src/extension/team-tool/context.ts +11 -1
- package/src/extension/team-tool/doctor.ts +2 -1
- package/src/extension/team-tool/goal-wrap.ts +2 -2
- package/src/extension/team-tool/goal.ts +1 -1
- package/src/extension/team-tool/handle-schedule.ts +25 -36
- package/src/extension/team-tool/orchestrate.ts +0 -1
- package/src/extension/team-tool/parallel-dispatch.ts +5 -0
- package/src/extension/team-tool/run.ts +220 -214
- package/src/extension/team-tool/workflow-manage.ts +1 -1
- package/src/extension/team-tool.ts +69 -16
- package/src/hooks/registry.ts +7 -2
- package/src/observability/correlation.ts +4 -3
- package/src/observability/event-bus.ts +10 -1
- package/src/observability/event-to-metric.ts +8 -7
- package/src/observability/exporters/adapter.ts +6 -9
- package/src/observability/exporters/otlp-exporter.ts +81 -0
- package/src/observability/metric-retention.ts +4 -0
- package/src/observability/metric-sink.ts +14 -4
- package/src/prompt/prompt-runtime.ts +15 -0
- package/src/runtime/adaptive-plan.ts +1 -1
- package/src/runtime/async-runner.ts +13 -4
- package/src/runtime/background-runner.ts +32 -23
- package/src/runtime/capability-inventory.ts +1 -1
- package/src/runtime/chain-runner.ts +5 -5
- package/src/runtime/checkpoint.ts +6 -16
- package/src/runtime/child-pi-constants.ts +42 -0
- package/src/runtime/child-pi-kill.ts +180 -0
- package/src/runtime/child-pi-spawn.ts +234 -0
- package/src/runtime/child-pi-steering.ts +128 -0
- package/src/runtime/child-pi-streams.ts +296 -0
- package/src/runtime/child-pi-transcript.ts +169 -0
- package/src/runtime/child-pi.ts +80 -822
- package/src/runtime/coalesce-tasks.ts +1 -1
- package/src/runtime/compact-stages/tail-capture-stage.ts +1 -1
- package/src/runtime/crash-recovery.ts +63 -47
- package/src/runtime/delivery-coordinator.ts +1 -1
- package/src/runtime/deterministic-ast.ts +2 -2
- package/src/runtime/diagnostic-export.ts +2 -1
- package/src/runtime/dwf-state-store.ts +5 -0
- package/src/runtime/dynamic-workflow-context.ts +78 -18
- package/src/runtime/dynamic-workflow-runner.ts +18 -2
- package/src/runtime/foreground-control.ts +3 -2
- package/src/runtime/goal-evaluator.ts +59 -27
- package/src/runtime/goal-loop-runner.ts +46 -13
- package/src/runtime/goal-state-store.ts +1 -1
- package/src/runtime/handoff-manager.ts +0 -2
- package/src/runtime/intercom-bridge.ts +0 -2
- package/src/runtime/live-agent-manager.ts +30 -5
- package/src/runtime/live-extension-bridge.ts +0 -2
- package/src/runtime/live-session-runtime.ts +65 -57
- package/src/runtime/manifest-cache.ts +59 -0
- package/src/runtime/mcp-proxy.ts +1 -1
- package/src/runtime/orphan-worker-registry.ts +3 -3
- package/src/runtime/output-validator.ts +1 -1
- package/src/runtime/path-overlap.ts +1 -1
- package/src/runtime/pi-args.ts +3 -3
- package/src/runtime/pi-json-output.ts +4 -3
- package/src/runtime/pi-spawn.ts +7 -1
- package/src/runtime/pipeline-runner.ts +1 -4
- package/src/runtime/post-checks.ts +0 -1
- package/src/runtime/retry-runner.ts +1 -1
- package/src/runtime/run-coalesced-task-group.ts +18 -23
- package/src/runtime/scheduler.ts +2 -0
- package/src/runtime/session-resources.ts +1 -1
- package/src/runtime/settings-store.ts +31 -1
- package/src/runtime/skill-effectiveness.ts +2 -2
- package/src/runtime/stale-reconciler.ts +23 -53
- package/src/runtime/subagent-manager.ts +19 -3
- package/src/runtime/task-output-context.ts +2 -1
- package/src/runtime/task-runner/prompt-builder.ts +3 -3
- package/src/runtime/task-runner/run-projection.ts +3 -1
- package/src/runtime/task-runner/state-helpers.ts +13 -1
- package/src/runtime/task-runner.ts +31 -6
- package/src/runtime/team-runner.ts +8 -2
- package/src/runtime/verification-gates.ts +54 -5
- package/src/schema/config-schema.ts +19 -1
- package/src/schema/team-tool-schema.ts +2 -0
- package/src/skills/discover-skills.ts +25 -15
- package/src/state/active-run-registry.ts +0 -1
- package/src/state/atomic-write.ts +37 -11
- package/src/state/blob-store.ts +3 -9
- package/src/state/contracts.ts +5 -1
- package/src/state/crew-init.ts +3 -2
- package/src/state/decision-ledger.ts +2 -2
- package/src/state/event-log-rotation.ts +1 -1
- package/src/state/event-log.ts +4 -3
- package/src/state/gitignore-manager.ts +2 -1
- package/src/state/health-store.ts +5 -2
- package/src/state/instinct-store.ts +11 -2
- package/src/state/locks.ts +132 -49
- package/src/state/mailbox.ts +3 -4
- package/src/state/observation-store.ts +1 -13
- package/src/state/run-cache.ts +12 -12
- package/src/state/run-graph.ts +2 -1
- package/src/state/run-metrics.ts +1 -1
- package/src/state/schedule.ts +7 -1
- package/src/state/state-store.ts +9 -3
- package/src/state/tiered-eval.ts +1 -1
- package/src/state/worker-atomic-writer.ts +1 -0
- package/src/ui/dynamic-border.ts +13 -3
- package/src/ui/live-conversation-overlay.ts +7 -27
- package/src/ui/live-duration.ts +2 -2
- package/src/ui/live-run-sidebar.ts +56 -32
- package/src/ui/mascot.ts +19 -13
- package/src/ui/powerbar-publisher.ts +244 -178
- package/src/ui/render-coalescer.ts +44 -3
- package/src/ui/render-diff.ts +2 -2
- package/src/ui/run-dashboard.ts +122 -21
- package/src/ui/run-event-bus.ts +66 -0
- package/src/ui/run-snapshot-cache.ts +84 -81
- package/src/ui/settings-overlay.ts +3 -3
- package/src/ui/status-colors.ts +24 -0
- package/src/ui/tool-progress-formatter.ts +1 -1
- package/src/ui/tool-render.ts +3 -3
- package/src/ui/transcript-cache.ts +5 -10
- package/src/ui/transcript-entries.ts +68 -72
- package/src/ui/transcript-viewer.ts +54 -14
- package/src/ui/widget/index.ts +26 -15
- package/src/ui/widget/widget-model.ts +18 -9
- package/src/ui/widget/widget-renderer.ts +2 -5
- package/src/utils/fingerprint.ts +1 -1
- package/src/utils/paths.ts +1 -1
- package/src/utils/project-detector.ts +1 -1
- package/src/worktree/worktree-manager.ts +47 -19
- package/src/state/atomic-write-v2.ts +0 -108
|
@@ -78,7 +78,7 @@ export function planCoalescedGroups(
|
|
|
78
78
|
const buckets = new Map<string, TeamTaskState[]>();
|
|
79
79
|
for (const taskId of readyTaskIds) {
|
|
80
80
|
const task = taskById.get(taskId);
|
|
81
|
-
if (!task
|
|
81
|
+
if (!task?.stepId) continue;
|
|
82
82
|
const step = stepById.get(task.stepId);
|
|
83
83
|
if (!step) continue;
|
|
84
84
|
|
|
@@ -59,7 +59,7 @@ export class TailCaptureStage implements ICompactStage {
|
|
|
59
59
|
// never contains a partial multi-byte sequence.
|
|
60
60
|
if (Buffer.byteLength(text, "utf-8") <= this.maxBytes) return text;
|
|
61
61
|
let tail = text.slice(Math.max(0, text.length - this.maxBytes));
|
|
62
|
-
while (Buffer.byteLength(tail, "utf-8") > this.maxBytes) tail = tail.slice(
|
|
62
|
+
while (Buffer.byteLength(tail, "utf-8") > this.maxBytes) tail = tail.slice(0, -1);
|
|
63
63
|
return this.marker ? `${this.marker}\n${tail}` : tail;
|
|
64
64
|
}
|
|
65
65
|
// Char cap mode.
|
|
@@ -397,31 +397,43 @@ export function purgeStaleActiveRunIndex(staleThresholdMs = 300_000, now = Date.
|
|
|
397
397
|
if (manifest?.status === "running" && manifest.async?.pid !== undefined) {
|
|
398
398
|
const pidAlive = checkProcessLiveness(manifest.async.pid).alive;
|
|
399
399
|
if (!pidAlive && !hasRecentLifeEvidence(entry, manifest.updatedAt, now, staleThresholdMs)) {
|
|
400
|
-
// Dead PID + no recent life evidence → cancel the manifest and unregister
|
|
400
|
+
// Dead PID + no recent life evidence → cancel the manifest and unregister.
|
|
401
|
+
// RT-F5: wrap load+modify+save in withRunLockSync so concurrent writers
|
|
402
|
+
// (e.g. team-runner final saveRunTasks) cannot interleave between the
|
|
403
|
+
// repaired-tasks write and the manifest-status flip. terminateLiveAgentsForRun
|
|
404
|
+
// is fire-and-forget so it stays OUTSIDE the lock to avoid blocking the
|
|
405
|
+
// critical section on async IPC.
|
|
401
406
|
try {
|
|
402
|
-
const fullLoaded = loadRunManifestById(entry.cwd, entry.runId);
|
|
407
|
+
const fullLoaded = loadRunManifestById(entry.cwd, entry.runId);
|
|
403
408
|
if (fullLoaded) {
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
409
|
+
withRunLockSync(fullLoaded.manifest, () => {
|
|
410
|
+
// Re-read under lock so the stale-best-effort load above
|
|
411
|
+
// can't cause us to clobber a fresh status flip.
|
|
412
|
+
const fresh = loadRunManifestById(entry.cwd, entry.runId);
|
|
413
|
+
if (!fresh) return;
|
|
414
|
+
if (fresh.manifest.status !== "running") return;
|
|
415
|
+
const now_iso = new Date(now).toISOString();
|
|
416
|
+
const repairedTasks = fresh.tasks.map((task) => {
|
|
417
|
+
if (task.status === "running" || task.status === "queued" || task.status === "waiting") {
|
|
418
|
+
return {
|
|
419
|
+
...task,
|
|
420
|
+
status: "cancelled" as const,
|
|
421
|
+
finishedAt: now_iso,
|
|
422
|
+
error: "Orphaned run: worker process dead and no recent activity",
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
return task;
|
|
426
|
+
});
|
|
427
|
+
saveRunTasks(fresh.manifest, repairedTasks);
|
|
428
|
+
for (const task of repairedTasks) {
|
|
429
|
+
try {
|
|
430
|
+
upsertCrewAgent(fresh.manifest, recordFromTask(fresh.manifest, task, "scaffold"));
|
|
431
|
+
} catch {
|
|
432
|
+
/* non-critical */
|
|
433
|
+
}
|
|
413
434
|
}
|
|
414
|
-
|
|
435
|
+
updateRunStatus(fresh.manifest, "cancelled", "Orphaned run: worker process dead and no recent activity");
|
|
415
436
|
});
|
|
416
|
-
saveRunTasks(fullLoaded.manifest, repairedTasks);
|
|
417
|
-
for (const task of repairedTasks) {
|
|
418
|
-
try {
|
|
419
|
-
upsertCrewAgent(fullLoaded.manifest, recordFromTask(fullLoaded.manifest, task, "scaffold"));
|
|
420
|
-
} catch {
|
|
421
|
-
/* non-critical */
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
updateRunStatus(fullLoaded.manifest, "cancelled", "Orphaned run: worker process dead and no recent activity");
|
|
425
437
|
void terminateLiveAgentsForRun(
|
|
426
438
|
fullLoaded.manifest.runId,
|
|
427
439
|
"cancelled",
|
|
@@ -447,35 +459,39 @@ export function purgeStaleActiveRunIndex(staleThresholdMs = 300_000, now = Date.
|
|
|
447
459
|
if (manifest?.status === "running" && manifest.async === undefined) {
|
|
448
460
|
if (!hasRecentLifeEvidence(entry, manifest.updatedAt, now, staleThresholdMs)) {
|
|
449
461
|
try {
|
|
450
|
-
const fullLoaded = loadRunManifestById(entry.cwd, entry.runId);
|
|
462
|
+
const fullLoaded = loadRunManifestById(entry.cwd, entry.runId);
|
|
451
463
|
if (fullLoaded && fullLoaded.manifest.status === "running") {
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
if (
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
464
|
+
withRunLockSync(fullLoaded.manifest, () => {
|
|
465
|
+
const fresh = loadRunManifestById(entry.cwd, entry.runId);
|
|
466
|
+
if (fresh?.manifest.status !== "running") return;
|
|
467
|
+
const now_iso = new Date(now).toISOString();
|
|
468
|
+
const repairedTasks = fresh.tasks.map((task) => {
|
|
469
|
+
if (task.status === "running" || task.status === "queued" || task.status === "waiting") {
|
|
470
|
+
return {
|
|
471
|
+
...task,
|
|
472
|
+
status: "cancelled" as const,
|
|
473
|
+
finishedAt: now_iso,
|
|
474
|
+
error: "Orphaned run: workflow completed but manifest never updated to terminal status",
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
return task;
|
|
478
|
+
});
|
|
479
|
+
saveRunTasks(fresh.manifest, repairedTasks);
|
|
480
|
+
for (const task of repairedTasks) {
|
|
481
|
+
try {
|
|
482
|
+
upsertCrewAgent(fresh.manifest, recordFromTask(fresh.manifest, task, "scaffold"));
|
|
483
|
+
} catch {
|
|
484
|
+
/* non-critical */
|
|
485
|
+
}
|
|
461
486
|
}
|
|
462
|
-
|
|
487
|
+
updateRunStatus(
|
|
488
|
+
fresh.manifest,
|
|
489
|
+
"cancelled",
|
|
490
|
+
"Orphaned run: no async worker and no manifest update in over " +
|
|
491
|
+
Math.round(staleThresholdMs / 60000) +
|
|
492
|
+
" minutes",
|
|
493
|
+
);
|
|
463
494
|
});
|
|
464
|
-
saveRunTasks(fullLoaded.manifest, repairedTasks);
|
|
465
|
-
for (const task of repairedTasks) {
|
|
466
|
-
try {
|
|
467
|
-
upsertCrewAgent(fullLoaded.manifest, recordFromTask(fullLoaded.manifest, task, "scaffold"));
|
|
468
|
-
} catch {
|
|
469
|
-
/* non-critical */
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
updateRunStatus(
|
|
473
|
-
fullLoaded.manifest,
|
|
474
|
-
"cancelled",
|
|
475
|
-
"Orphaned run: no async worker and no manifest update in over " +
|
|
476
|
-
Math.round(staleThresholdMs / 60000) +
|
|
477
|
-
" minutes",
|
|
478
|
-
);
|
|
479
495
|
void terminateLiveAgentsForRun(
|
|
480
496
|
fullLoaded.manifest.runId,
|
|
481
497
|
"cancelled",
|
|
@@ -21,7 +21,6 @@ export interface DeliveryCoordinatorDeps {
|
|
|
21
21
|
const PENDING_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
22
22
|
|
|
23
23
|
export class DeliveryCoordinator {
|
|
24
|
-
private ownerSessionId: string | undefined;
|
|
25
24
|
private active = false;
|
|
26
25
|
private generation = 0;
|
|
27
26
|
private pending: PendingDelivery[] = [];
|
|
@@ -29,6 +28,7 @@ export class DeliveryCoordinator {
|
|
|
29
28
|
private readonly deps: DeliveryCoordinatorDeps;
|
|
30
29
|
private ttlTimer: ReturnType<typeof setInterval> | undefined;
|
|
31
30
|
private timerStarted = false;
|
|
31
|
+
private ownerSessionId: string | undefined = undefined;
|
|
32
32
|
|
|
33
33
|
constructor(deps: DeliveryCoordinatorDeps) {
|
|
34
34
|
this.deps = deps;
|
|
@@ -120,9 +120,9 @@ function isNewDateExpression(node: AstNode): boolean {
|
|
|
120
120
|
*/
|
|
121
121
|
function isMemberExpression(node: AstNode, childKey: string, objectName: string, propertyName: string): boolean {
|
|
122
122
|
const child = asAstNode(node[childKey]);
|
|
123
|
-
if (
|
|
123
|
+
if (child?.type !== "MemberExpression") return false;
|
|
124
124
|
const object = asAstNode(child.object);
|
|
125
|
-
if (
|
|
125
|
+
if (object?.type !== "Identifier" || object.name !== objectName) return false;
|
|
126
126
|
return propertyNameOf(child) === propertyName;
|
|
127
127
|
}
|
|
128
128
|
|
|
@@ -3,6 +3,7 @@ import * as path from "node:path";
|
|
|
3
3
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import type { MetricRegistry } from "../observability/metric-registry.ts";
|
|
5
5
|
import type { MetricSnapshot } from "../observability/metrics-primitives.ts";
|
|
6
|
+
import { atomicWriteFile } from "../state/atomic-write.ts";
|
|
6
7
|
import { readEvents, type TeamEvent } from "../state/event-log.ts";
|
|
7
8
|
import { loadRunManifestById } from "../state/state-store.ts";
|
|
8
9
|
import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
@@ -148,7 +149,7 @@ export async function exportDiagnostic(
|
|
|
148
149
|
const dir = path.join(loaded.manifest.artifactsRoot, "diagnostic");
|
|
149
150
|
fs.mkdirSync(dir, { recursive: true });
|
|
150
151
|
const filePath = path.join(dir, `diagnostic-${safeTimestamp}.json`);
|
|
151
|
-
|
|
152
|
+
atomicWriteFile(filePath, `${JSON.stringify(report, null, 2)}\n`);
|
|
152
153
|
return { path: filePath, report };
|
|
153
154
|
}
|
|
154
155
|
|
|
@@ -28,6 +28,11 @@ export interface DwfCheckpointState {
|
|
|
28
28
|
logs: string[]; // capped copy (≤1000); the events log (dwf.log) is the durable source of truth
|
|
29
29
|
spent: number; // budget accumulator (round-14 P1-2)
|
|
30
30
|
agentCount: number;
|
|
31
|
+
/** PERS-1: per-agent-call idempotency cache. Maps a deterministic call ID to the
|
|
32
|
+
* cached result so a DWF resume skips already-completed agent calls (avoids
|
|
33
|
+
* duplicating artifacts/mailbox/tokens). Uses Record (not Map) for JSON
|
|
34
|
+
* serialization safety. */
|
|
35
|
+
completedAgentCalls?: Record<string, { text: string; usage?: { input: number; output: number } }>;
|
|
31
36
|
updatedAt: string;
|
|
32
37
|
}
|
|
33
38
|
|
|
@@ -35,6 +35,7 @@ import { logInternalError } from "../utils/internal-error.ts";
|
|
|
35
35
|
import { cleanupAgentWorktreeAsync, prepareAgentWorktreeAsync } from "../worktree/worktree-manager.ts";
|
|
36
36
|
import { runChildPi } from "./child-pi.ts";
|
|
37
37
|
import type { DwfCheckpointState } from "./dwf-state-store.ts";
|
|
38
|
+
import { withWorkerSlot } from "./global-worker-cap.ts";
|
|
38
39
|
import { mapConcurrent } from "./parallel-utils.ts";
|
|
39
40
|
import { parsePiJsonOutput } from "./pi-json-output.ts";
|
|
40
41
|
import { renderPlanTemplate } from "./plan-templates.ts";
|
|
@@ -269,6 +270,11 @@ export function makeWorkflowCtx(manifest: TeamRunManifest, opts: MakeWorkflowCtx
|
|
|
269
270
|
logs: opts.resumedState ? [...opts.resumedState.logs].slice(0, 1000) : [],
|
|
270
271
|
args: opts.args ?? {},
|
|
271
272
|
};
|
|
273
|
+
// PERS-1: per-agent-call idempotency cache. Hydrated from resumedState so a DWF
|
|
274
|
+
// resume skips already-completed agent calls (avoids duplicating artifacts/mailbox/tokens).
|
|
275
|
+
// Uses Record (not Map) for JSON serialization safety.
|
|
276
|
+
const completedAgentCalls: Record<string, { text: string; usage?: { input: number; output: number } }> =
|
|
277
|
+
opts.resumedState?.completedAgentCalls ?? {};
|
|
272
278
|
// round-14 P1-2: frozen budget surface. The closures read wfState.spent so the
|
|
273
279
|
// object stays live after Object.freeze(ctx). total is a snapshot primitive.
|
|
274
280
|
const budget = Object.freeze({
|
|
@@ -290,10 +296,35 @@ export function makeWorkflowCtx(manifest: TeamRunManifest, opts: MakeWorkflowCtx
|
|
|
290
296
|
// regardless of which return/throw path is taken.
|
|
291
297
|
let worktreePath: string | undefined;
|
|
292
298
|
let worktreeBranch: string | undefined;
|
|
299
|
+
// BDG-2: declared before the try so the catch block can un-reserve on failure.
|
|
300
|
+
const ESTIMATE = 4096;
|
|
301
|
+
let reserved = false;
|
|
293
302
|
try {
|
|
294
|
-
//
|
|
295
|
-
//
|
|
296
|
-
|
|
303
|
+
// PERS-1: per-agent-call idempotency. Compute a deterministic call ID
|
|
304
|
+
// from the call arguments and check if this call was already completed
|
|
305
|
+
// (e.g. during a previous run before a crash). If so, return the cached
|
|
306
|
+
// result without re-spawning the agent.
|
|
307
|
+
const callId = JSON.stringify({
|
|
308
|
+
role: call.role,
|
|
309
|
+
agent: call.agent,
|
|
310
|
+
prompt: call.prompt,
|
|
311
|
+
model: call.model,
|
|
312
|
+
schema: call.schema ? "yes" : "no",
|
|
313
|
+
});
|
|
314
|
+
const cached = completedAgentCalls[callId];
|
|
315
|
+
if (cached) {
|
|
316
|
+
return {
|
|
317
|
+
ok: true,
|
|
318
|
+
text: cached.text,
|
|
319
|
+
usage: cached.usage,
|
|
320
|
+
durationMs: 0,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// BDG-2: reserve-then-adjust budget. Before spawning, estimate the cost and
|
|
325
|
+
// reserve it by adding to wfState.spent. This prevents N concurrent calls from
|
|
326
|
+
// all seeing the same remaining budget and overspending.
|
|
327
|
+
if (budget.total !== null && budget.remaining() < ESTIMATE) {
|
|
297
328
|
return {
|
|
298
329
|
ok: false,
|
|
299
330
|
text: "",
|
|
@@ -301,6 +332,10 @@ export function makeWorkflowCtx(manifest: TeamRunManifest, opts: MakeWorkflowCtx
|
|
|
301
332
|
durationMs: 0,
|
|
302
333
|
};
|
|
303
334
|
}
|
|
335
|
+
// Reserve the estimate before spawning.
|
|
336
|
+
wfState.spent += ESTIMATE;
|
|
337
|
+
reserved = true;
|
|
338
|
+
|
|
304
339
|
const agentConfig = resolveAgentForRole(call.role, {
|
|
305
340
|
explicitAgent: call.agent,
|
|
306
341
|
team: opts.team,
|
|
@@ -353,20 +388,25 @@ export function makeWorkflowCtx(manifest: TeamRunManifest, opts: MakeWorkflowCtx
|
|
|
353
388
|
}
|
|
354
389
|
}
|
|
355
390
|
|
|
356
|
-
const childResult = await
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
391
|
+
const childResult = await withWorkerSlot(() =>
|
|
392
|
+
runChildPi({
|
|
393
|
+
cwd: agentCwd,
|
|
394
|
+
task,
|
|
395
|
+
agent: effectiveAgent,
|
|
396
|
+
model: call.model ?? opts.modelOverride ?? agentConfig.model,
|
|
397
|
+
skillPaths: undefined, // skills resolved via agent config + team-role plumbing
|
|
398
|
+
maxTurns: call.maxTurns,
|
|
399
|
+
graceTurns: call.graceTurns,
|
|
400
|
+
signal: opts.signal,
|
|
401
|
+
artifactsRoot: manifest.artifactsRoot,
|
|
402
|
+
runId: manifest.runId,
|
|
403
|
+
role: call.role ?? call.agent,
|
|
404
|
+
}),
|
|
405
|
+
);
|
|
369
406
|
if (childResult.exitCode !== 0 || childResult.error) {
|
|
407
|
+
// BDG-2: un-reserve the estimate on spawn failure.
|
|
408
|
+
wfState.spent -= ESTIMATE;
|
|
409
|
+
reserved = false;
|
|
370
410
|
return {
|
|
371
411
|
ok: false,
|
|
372
412
|
text: "",
|
|
@@ -376,8 +416,10 @@ export function makeWorkflowCtx(manifest: TeamRunManifest, opts: MakeWorkflowCtx
|
|
|
376
416
|
}
|
|
377
417
|
const parsed = parsePiJsonOutput(childResult.stdout);
|
|
378
418
|
// round-14 P1-2: accumulate this run's token usage into the workflow budget.
|
|
379
|
-
//
|
|
380
|
-
|
|
419
|
+
// BDG-2: adjust the reserve — subtract the estimate, add the actual usage.
|
|
420
|
+
// This correctly reduces spent when actualUsage < ESTIMATE.
|
|
421
|
+
wfState.spent += (parsed.usage?.input ?? 0) + (parsed.usage?.output ?? 0) - ESTIMATE;
|
|
422
|
+
reserved = false;
|
|
381
423
|
let text = parsed.finalText ?? "";
|
|
382
424
|
// Round-11 test fix: parsePiJsonOutput only extracts text from pi event stream
|
|
383
425
|
// ({type:"message_end", message:{role:"assistant", content:[...]}}). When the
|
|
@@ -400,6 +442,7 @@ export function makeWorkflowCtx(manifest: TeamRunManifest, opts: MakeWorkflowCtx
|
|
|
400
442
|
producer: "dynamic-workflow",
|
|
401
443
|
});
|
|
402
444
|
if (call.schema !== undefined && !extracted.structured) {
|
|
445
|
+
// BDG-2: un-reserve was already done above (reserved = false after adjust).
|
|
403
446
|
return {
|
|
404
447
|
ok: false,
|
|
405
448
|
text,
|
|
@@ -409,6 +452,11 @@ export function makeWorkflowCtx(manifest: TeamRunManifest, opts: MakeWorkflowCtx
|
|
|
409
452
|
durationMs: Date.now() - started,
|
|
410
453
|
};
|
|
411
454
|
}
|
|
455
|
+
// PERS-1: cache the successful result for idempotency on resume.
|
|
456
|
+
completedAgentCalls[callId] = {
|
|
457
|
+
text,
|
|
458
|
+
usage: { input: parsed.usage?.input ?? 0, output: parsed.usage?.output ?? 0 },
|
|
459
|
+
};
|
|
412
460
|
return {
|
|
413
461
|
ok: true,
|
|
414
462
|
text,
|
|
@@ -418,6 +466,8 @@ export function makeWorkflowCtx(manifest: TeamRunManifest, opts: MakeWorkflowCtx
|
|
|
418
466
|
durationMs: Date.now() - started,
|
|
419
467
|
};
|
|
420
468
|
} catch (error) {
|
|
469
|
+
// BDG-2: un-reserve the estimate on failure (only if still reserved).
|
|
470
|
+
if (reserved) wfState.spent -= ESTIMATE;
|
|
421
471
|
logInternalError("dynamic-workflow-context.agent", error, `runId=${manifest.runId}`);
|
|
422
472
|
return {
|
|
423
473
|
ok: false,
|
|
@@ -450,6 +500,7 @@ export function makeWorkflowCtx(manifest: TeamRunManifest, opts: MakeWorkflowCtx
|
|
|
450
500
|
logs: wfState.logs.slice(0, 1000),
|
|
451
501
|
spent: wfState.spent,
|
|
452
502
|
agentCount,
|
|
503
|
+
completedAgentCalls,
|
|
453
504
|
updatedAt: new Date().toISOString(),
|
|
454
505
|
});
|
|
455
506
|
} catch (checkpointError) {
|
|
@@ -709,6 +760,12 @@ export function makeWorkflowCtx(manifest: TeamRunManifest, opts: MakeWorkflowCtx
|
|
|
709
760
|
get: () => agentCount,
|
|
710
761
|
enumerable: false,
|
|
711
762
|
});
|
|
763
|
+
// PERS-1: completedAgentCalls is read-only from the runner; the agent() method
|
|
764
|
+
// is the only writer. Exposed so getWorkflowCheckpoint() can include it.
|
|
765
|
+
Object.defineProperty(ctx, "__completedAgentCalls", {
|
|
766
|
+
get: () => completedAgentCalls,
|
|
767
|
+
enumerable: false,
|
|
768
|
+
});
|
|
712
769
|
return ctx;
|
|
713
770
|
}
|
|
714
771
|
|
|
@@ -757,6 +814,9 @@ export function getWorkflowCheckpoint(ctx: WorkflowCtx): DwfCheckpointState {
|
|
|
757
814
|
logs: logs ?? [],
|
|
758
815
|
spent: ctx.budget.spent(),
|
|
759
816
|
agentCount: (ctx as unknown as { __agentCount?: number }).__agentCount ?? 0,
|
|
817
|
+
completedAgentCalls:
|
|
818
|
+
(ctx as unknown as { __completedAgentCalls?: Record<string, { text: string; usage?: { input: number; output: number } }> })
|
|
819
|
+
.__completedAgentCalls ?? {},
|
|
760
820
|
updatedAt: new Date().toISOString(),
|
|
761
821
|
};
|
|
762
822
|
}
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
import { readFileSync } from "node:fs";
|
|
21
21
|
import { join } from "node:path";
|
|
22
|
+
import { transformSync } from "esbuild";
|
|
22
23
|
import { writeArtifact } from "../state/artifact-store.ts";
|
|
23
24
|
import { appendEvent } from "../state/event-log.ts";
|
|
24
25
|
import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
@@ -113,7 +114,11 @@ async function loadWorkflowModule(scriptPath: string): Promise<DynamicWorkflowSc
|
|
|
113
114
|
// raw .dwf.ts file. This is the same source jiti will execute.
|
|
114
115
|
const scriptSource = readFileSync(scriptPath, "utf-8");
|
|
115
116
|
if (isDeterminismCheckEnabled()) {
|
|
116
|
-
|
|
117
|
+
// DISC-1: acorn can only parse JavaScript, not TypeScript. Real .dwf.ts files
|
|
118
|
+
// contain type annotations/imports that cause a silent parse error, so the
|
|
119
|
+
// determinism check never runs. Transpile to JS via esbuild first.
|
|
120
|
+
const js = transformSync(scriptSource, { loader: "ts", format: "esm" }).code;
|
|
121
|
+
assertDeterministicScript(js);
|
|
117
122
|
}
|
|
118
123
|
// jiti is the same loader async-runner.ts uses (resolveTypeScriptLoader). We require it
|
|
119
124
|
// lazily so this module stays importable in environments without jiti (type-only consumers).
|
|
@@ -166,9 +171,16 @@ export async function runDynamicWorkflow(input: RunDynamicWorkflowInput): Promis
|
|
|
166
171
|
});
|
|
167
172
|
}
|
|
168
173
|
|
|
174
|
+
// ERR-1: Create a timeout AbortController so we can abort spawned children on script timeout.
|
|
175
|
+
// Combine the input signal with the timeout controller's signal so EITHER source
|
|
176
|
+
// (external abort OR our timeout) propagates to runChildPi via the ctx.
|
|
177
|
+
// AbortSignal.any is available since Node 20.3 — the project requires Node 20+.
|
|
178
|
+
const timeoutController = new AbortController();
|
|
179
|
+
const combinedSignal = AbortSignal.any([signal, timeoutController.signal]);
|
|
180
|
+
|
|
169
181
|
const ctx = makeWorkflowCtx(manifest, {
|
|
170
182
|
concurrency: input.concurrency ?? workflow.maxConcurrency ?? 4,
|
|
171
|
-
signal,
|
|
183
|
+
signal: combinedSignal,
|
|
172
184
|
team: input.team,
|
|
173
185
|
modelOverride: input.modelOverride,
|
|
174
186
|
tokenBudget: input.tokenBudget ?? workflow.maxTokenBudget,
|
|
@@ -200,6 +212,10 @@ export async function runDynamicWorkflow(input: RunDynamicWorkflowInput): Promis
|
|
|
200
212
|
let timeoutHandle: NodeJS.Timeout | undefined;
|
|
201
213
|
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
202
214
|
timeoutHandle = setTimeout(() => {
|
|
215
|
+
// ERR-1: abort the timeout controller so the abort propagates to
|
|
216
|
+
// runChildPi (via the combined signal passed to makeWorkflowCtx),
|
|
217
|
+
// which kills spawned children instead of letting them run.
|
|
218
|
+
timeoutController.abort();
|
|
203
219
|
reject(
|
|
204
220
|
new Error(
|
|
205
221
|
`Dynamic workflow script timed out after ${SCRIPT_TIMEOUT_MS}ms. The script may have spawned a child process that did not exit. Check for spawn/exec calls without proper stdio handling.`,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
+
import { atomicWriteFile } from "../state/atomic-write.ts";
|
|
3
4
|
import { appendEvent } from "../state/event-log.ts";
|
|
4
5
|
import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
5
6
|
import { logInternalError } from "../utils/internal-error.ts";
|
|
@@ -82,7 +83,7 @@ export function writeForegroundInterruptRequest(
|
|
|
82
83
|
try {
|
|
83
84
|
fs.mkdirSync(lockDir, { recursive: true });
|
|
84
85
|
try {
|
|
85
|
-
|
|
86
|
+
atomicWriteFile(pidFile, String(process.pid));
|
|
86
87
|
} catch {
|
|
87
88
|
/* best-effort */
|
|
88
89
|
}
|
|
@@ -178,7 +179,7 @@ export function writeForegroundInterruptRequest(
|
|
|
178
179
|
acknowledged: false,
|
|
179
180
|
};
|
|
180
181
|
fs.mkdirSync(path.dirname(controlPath), { recursive: true });
|
|
181
|
-
|
|
182
|
+
atomicWriteFile(controlPath, `${JSON.stringify({ requests: [...requests, request] }, null, 2)}\n`);
|
|
182
183
|
appendEvent(manifest.eventsPath, {
|
|
183
184
|
type: "foreground.interrupt_requested",
|
|
184
185
|
runId: manifest.runId,
|
|
@@ -188,7 +188,9 @@ function tryParseDirectVerdict(stdout: string): { achieved: boolean; reason: str
|
|
|
188
188
|
* Returns a GoalVerdict. On any failure (non-zero exit, non-JSON, invalid shape),
|
|
189
189
|
* returns a `BLOCKED:`-prefixed verdict so the loop stops (§0c C6 fallback).
|
|
190
190
|
*/
|
|
191
|
-
export async function evaluateGoal(
|
|
191
|
+
export async function evaluateGoal(
|
|
192
|
+
input: EvaluateGoalInput,
|
|
193
|
+
): Promise<{ verdict: GoalVerdict; judgeUsage?: { totalTokens: number; inputTokens?: number; outputTokens?: number } }> {
|
|
192
194
|
const agent = synthesizeJudgeAgentConfig();
|
|
193
195
|
const task = buildJudgeTask(input);
|
|
194
196
|
const evaluatedAt = new Date().toISOString();
|
|
@@ -212,12 +214,15 @@ export async function evaluateGoal(input: EvaluateGoalInput): Promise<GoalVerdic
|
|
|
212
214
|
});
|
|
213
215
|
|
|
214
216
|
if (result.exitCode !== 0 || result.error) {
|
|
215
|
-
return
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
217
|
+
return {
|
|
218
|
+
verdict: blockedVerdict(
|
|
219
|
+
input.turn,
|
|
220
|
+
input.model,
|
|
221
|
+
evaluatedAt,
|
|
222
|
+
`judge spawn failed (exit=${result.exitCode}): ${result.error ?? result.stderr.slice(0, 200)}`,
|
|
223
|
+
),
|
|
224
|
+
judgeUsage: undefined,
|
|
225
|
+
};
|
|
221
226
|
}
|
|
222
227
|
|
|
223
228
|
const parsed = parsePiJsonOutput(result.stdout);
|
|
@@ -231,16 +236,22 @@ export async function evaluateGoal(input: EvaluateGoalInput): Promise<GoalVerdic
|
|
|
231
236
|
const direct = !finalText.trim() ? tryParseDirectVerdict(result.stdout) : undefined;
|
|
232
237
|
if (direct) {
|
|
233
238
|
return {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
239
|
+
verdict: {
|
|
240
|
+
turn: input.turn,
|
|
241
|
+
achieved: direct.achieved,
|
|
242
|
+
reason: direct.reason,
|
|
243
|
+
evidenceRefs: direct.evidenceRefs,
|
|
244
|
+
evaluatorModel: input.model,
|
|
245
|
+
evaluatedAt,
|
|
246
|
+
},
|
|
247
|
+
judgeUsage: undefined,
|
|
240
248
|
};
|
|
241
249
|
}
|
|
242
250
|
if (!finalText.trim()) {
|
|
243
|
-
return
|
|
251
|
+
return {
|
|
252
|
+
verdict: blockedVerdict(input.turn, input.model, evaluatedAt, "judge produced no output"),
|
|
253
|
+
judgeUsage: undefined,
|
|
254
|
+
};
|
|
244
255
|
}
|
|
245
256
|
|
|
246
257
|
const extracted = extractStructuredResult(finalText);
|
|
@@ -252,27 +263,48 @@ export async function evaluateGoal(input: EvaluateGoalInput): Promise<GoalVerdic
|
|
|
252
263
|
})
|
|
253
264
|
: undefined;
|
|
254
265
|
if (!data || typeof data.achieved !== "boolean" || typeof data.reason !== "string") {
|
|
255
|
-
return
|
|
266
|
+
return {
|
|
267
|
+
verdict: blockedVerdict(
|
|
268
|
+
input.turn,
|
|
269
|
+
input.model,
|
|
270
|
+
evaluatedAt,
|
|
271
|
+
`judge output not valid verdict JSON: ${truncate(finalText, 200)}`,
|
|
272
|
+
),
|
|
273
|
+
judgeUsage: undefined,
|
|
274
|
+
};
|
|
256
275
|
}
|
|
257
276
|
const evidenceRefs = Array.isArray(data.evidenceRefs)
|
|
258
277
|
? data.evidenceRefs.filter((r): r is string => typeof r === "string")
|
|
259
278
|
: undefined;
|
|
279
|
+
const judgeUsage = parsed.usage
|
|
280
|
+
? {
|
|
281
|
+
totalTokens: (parsed.usage.input ?? 0) + (parsed.usage.output ?? 0),
|
|
282
|
+
inputTokens: parsed.usage.input,
|
|
283
|
+
outputTokens: parsed.usage.output,
|
|
284
|
+
}
|
|
285
|
+
: undefined;
|
|
260
286
|
return {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
287
|
+
verdict: {
|
|
288
|
+
turn: input.turn,
|
|
289
|
+
achieved: data.achieved,
|
|
290
|
+
reason: data.reason,
|
|
291
|
+
evidenceRefs,
|
|
292
|
+
evaluatorModel: input.model,
|
|
293
|
+
evaluatedAt,
|
|
294
|
+
},
|
|
295
|
+
judgeUsage,
|
|
267
296
|
};
|
|
268
297
|
} catch (error) {
|
|
269
298
|
logInternalError("goal-evaluator.evaluateGoal", error, `turn=${input.turn}`);
|
|
270
|
-
return
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
299
|
+
return {
|
|
300
|
+
verdict: blockedVerdict(
|
|
301
|
+
input.turn,
|
|
302
|
+
input.model,
|
|
303
|
+
evaluatedAt,
|
|
304
|
+
`judge threw: ${error instanceof Error ? error.message : String(error)}`,
|
|
305
|
+
),
|
|
306
|
+
judgeUsage: undefined,
|
|
307
|
+
};
|
|
276
308
|
}
|
|
277
309
|
}
|
|
278
310
|
|