pi-crew 0.9.67 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +172 -0
- package/agents/analyst.md +1 -1
- package/agents/cold-verifier.md +3 -1
- package/agents/critic.md +1 -1
- package/agents/executor.md +1 -1
- package/agents/explorer.md +1 -1
- package/agents/planner.md +1 -1
- package/agents/reviewer.md +1 -1
- package/agents/security-reviewer.md +1 -1
- package/agents/test-engineer.md +1 -1
- package/agents/verifier.md +1 -1
- package/agents/writer.md +1 -1
- package/dist/index.mjs +65847 -60331
- package/docs/README.md +2 -0
- package/docs/actions-reference.md +31 -0
- package/docs/commands-reference.md +17 -6
- package/docs/resource-formats.md +13 -0
- package/package.json +9 -6
- package/schema.json +15 -1
- package/scripts/resource-sampler.mjs +36 -2
- package/skills/requirements-to-task-packet/SKILL.md +26 -0
- package/skills/widget-rendering/SKILL.md +7 -7
- package/src/agents/agent-config.ts +36 -14
- package/src/agents/discover-agents.ts +23 -14
- package/src/config/config-merge.ts +183 -0
- package/src/config/config-validation.ts +681 -0
- package/src/config/config.ts +22 -864
- package/src/config/defaults.ts +35 -1
- package/src/config/drift-detector.ts +1 -1
- package/src/config/env-vars.ts +691 -0
- package/src/config/role-tools.ts +8 -8
- package/src/config/sanitize-project-config.ts +172 -0
- package/src/config/types.ts +30 -0
- package/src/extension/async-notifier.ts +25 -2
- package/src/extension/crew-vibes/config.ts +2 -1
- package/src/extension/plan-orchestrate.ts +132 -0
- package/src/extension/registration/commands/dashboard.ts +158 -0
- package/src/extension/registration/commands/index.ts +35 -0
- package/src/extension/registration/commands/manage.ts +303 -0
- package/src/extension/registration/commands/run.ts +214 -0
- package/src/extension/registration/commands/shared.ts +631 -0
- package/src/extension/registration/commands/status.ts +60 -0
- package/src/extension/registration/commands.ts +13 -1224
- package/src/extension/registration/lifecycle-handlers.ts +126 -15
- package/src/extension/registration/subagent-tools.ts +218 -9
- package/src/extension/rpc-hmac.ts +5 -3
- package/src/extension/team-tool/api/heartbeat.ts +47 -10
- package/src/extension/team-tool/api/plan-approval.ts +9 -0
- package/src/extension/team-tool/api/task-claims.ts +109 -40
- package/src/extension/team-tool/cancel.ts +84 -50
- package/src/extension/team-tool/dispatch/index.ts +1 -0
- package/src/extension/team-tool/dispatch/run.ts +4 -1
- package/src/extension/team-tool/doctor.ts +103 -1
- package/src/extension/team-tool/orchestrate.ts +66 -1
- package/src/extension/team-tool/plans.ts +192 -0
- package/src/extension/team-tool/respond.ts +197 -65
- package/src/extension/team-tool/run-deadline.ts +16 -1
- package/src/extension/team-tool/run-intent.ts +63 -0
- package/src/extension/team-tool/run.ts +48 -18
- package/src/extension/team-tool/status.ts +84 -26
- package/src/extension/team-tool.ts +11 -2
- package/src/hooks/registry.ts +1 -6
- package/src/i18n.ts +9 -0
- package/src/prompt/prompt-runtime.ts +521 -2
- package/src/prompt/worker-events-channel.ts +173 -0
- package/src/runtime/README.md +8 -8
- package/src/runtime/async-runner.ts +7 -3
- package/src/runtime/background-runner.ts +42 -14
- package/src/runtime/broker/broker-issuer.ts +9 -2
- package/src/runtime/broker/crew-broker-tokens.ts +43 -6
- package/src/runtime/broker/crew-broker.ts +838 -10
- package/src/runtime/broker/wait-status-cache.ts +157 -0
- package/src/runtime/budget-enforcement.ts +281 -0
- package/src/runtime/child-pi/child-pi-spawn.ts +60 -14
- package/src/runtime/child-pi/child-pi-timers.ts +324 -0
- package/src/runtime/child-pi/child-pi.ts +97 -201
- package/src/runtime/child-pi/mock-fixtures.ts +16 -2
- package/src/runtime/crew-agent-records.ts +259 -14
- package/src/runtime/delegate-spawn.ts +148 -0
- package/src/runtime/deterministic-ast.ts +2 -1
- package/src/runtime/dispatch-batch.ts +945 -0
- package/src/runtime/finalize-run.ts +557 -0
- package/src/runtime/goal-workflow/adaptive-plan.ts +87 -13
- package/src/runtime/goal-workflow/dynamic-workflow-runner.ts +3 -2
- package/src/runtime/goal-workflow/goal-state-store.ts +1 -1
- package/src/runtime/group-join.ts +11 -125
- package/src/runtime/live-session/live-session-runtime.ts +26 -1
- package/src/runtime/merge-gate.ts +7 -1
- package/src/runtime/merge-loop.ts +130 -0
- package/src/runtime/model/model-budget-summary.ts +53 -0
- package/src/runtime/model/model-fallback.ts +32 -2
- package/src/runtime/model/pi-args.ts +10 -0
- package/src/runtime/model/provider-extensions.ts +10 -0
- package/src/runtime/orphan-worker-registry.ts +1 -1
- package/src/runtime/output/output-validator.ts +45 -0
- package/src/runtime/parent-guard.ts +3 -1
- package/src/runtime/peer-dep.ts +2 -1
- package/src/runtime/per-write-validator.ts +0 -5
- package/src/runtime/pi-spawn.ts +61 -15
- package/src/runtime/plan-approval.ts +125 -0
- package/src/runtime/plan-replan.ts +151 -0
- package/src/runtime/recovery/checkpoint.ts +0 -18
- package/src/runtime/recovery/crash-recovery.ts +95 -46
- package/src/runtime/scheduler-context.ts +98 -0
- package/src/runtime/scheduling/coalesce-tasks.ts +5 -0
- package/src/runtime/scheduling/global-worker-cap.ts +2 -1
- package/src/runtime/scheduling/nested-slots.ts +70 -0
- package/src/runtime/scheduling/run-coalesced-task-group.ts +64 -13
- package/src/runtime/scheduling/task-graph-scheduler.ts +0 -10
- package/src/runtime/scratchpad/README.md +10 -5
- package/src/runtime/scratchpad/guest.ts +103 -2
- package/src/runtime/scratchpad/protocol.ts +13 -1
- package/src/runtime/scratchpad/transform.ts +206 -12
- package/src/runtime/settings-store.ts +219 -0
- package/src/runtime/spawn-policy.ts +217 -0
- package/src/runtime/stale-reconciler.ts +87 -6
- package/src/runtime/subagent-manager.ts +25 -1
- package/src/runtime/task-output-context.ts +230 -9
- package/src/runtime/task-packet.ts +23 -1
- package/src/runtime/task-runner/child-executor.ts +106 -7
- package/src/runtime/task-runner/post-execution.ts +125 -1
- package/src/runtime/task-runner/pre-execution.ts +39 -1
- package/src/runtime/task-runner/prompt-builder.ts +51 -1
- package/src/runtime/task-runner/retrieval-orchestrator.ts +72 -18
- package/src/runtime/task-runner/spec-evidence.ts +403 -0
- package/src/runtime/task-runner/state-helpers.ts +26 -24
- package/src/runtime/task-runner.ts +11 -0
- package/src/runtime/team-runner.ts +129 -1671
- package/src/runtime/verification/spec-sandbox.ts +255 -0
- package/src/runtime/verification/verification-gates.ts +3 -2
- package/src/runtime/verification/verification-worktree.ts +2 -1
- package/src/runtime/workflow-phase-advance.ts +100 -0
- package/src/runtime/workspace-tree.ts +9 -0
- package/src/schema/config-schema.ts +65 -25
- package/src/schema/sensitive-config-paths.ts +64 -0
- package/src/schema/team-tool-schema.ts +13 -3
- package/src/state/README.md +4 -10
- package/src/state/atomic-write.ts +20 -3
- package/src/state/contracts.ts +38 -0
- package/src/state/coordination/mailbox.ts +12 -2
- package/src/state/event-log/cursor.ts +223 -0
- package/src/state/event-log/event-log-rotation.ts +12 -4
- package/src/state/event-log/event-log.ts +152 -369
- package/src/state/event-log/sequence-cache.ts +373 -0
- package/src/state/event-log/worker-atomic-writer.ts +2 -1
- package/src/state/stores/active-run-registry.ts +3 -2
- package/src/state/stores/manifest-io.ts +237 -0
- package/src/state/stores/ownership-map.ts +162 -0
- package/src/state/stores/plan-store.ts +241 -0
- package/src/state/stores/run-cache.ts +0 -90
- package/src/state/stores/spec-store.ts +189 -0
- package/src/state/stores/state-store.ts +66 -229
- package/src/state/types.ts +197 -0
- package/src/ui/dashboard-panes/plan-pane.ts +136 -0
- package/src/ui/dashboard-panes/progress-pane.ts +6 -0
- package/src/ui/dashboard-panes/transcript-pane.ts +31 -0
- package/src/ui/heartbeat-aggregator.ts +9 -1
- package/src/ui/keybinding-map.ts +54 -13
- package/src/ui/powerbar-publisher.ts +52 -1
- package/src/ui/run-dashboard.ts +31 -5
- package/src/ui/run-snapshot-cache.ts +57 -30
- package/src/ui/snapshot-types.ts +6 -1
- package/src/ui/widget/widget-renderer.ts +9 -1
- package/src/utils/file-coalescer.ts +0 -4
- package/src/utils/fs-errno.ts +66 -0
- package/src/utils/fs-watch.ts +1 -1
- package/src/utils/internal-error.ts +3 -1
- package/src/utils/paths.ts +11 -3
- package/src/utils/task-name-generator.ts +1 -8
- package/src/workflows/discover-workflows.ts +19 -2
- package/src/workflows/validate-workflow.ts +7 -1
- package/src/workflows/workflow-config.ts +10 -0
- package/src/workflows/workflow-serializer.ts +3 -0
- package/src/worktree/worktree-manager.ts +22 -0
- package/src/agents/agent-search.ts +0 -98
- package/src/benchmark/benchmark-runner.ts +0 -313
- package/src/benchmark/feedback-loop.ts +0 -73
- package/src/config/resilient-parser.ts +0 -117
- package/src/extension/crew-vibes/cat-frames.ts +0 -18
- package/src/extension/result-watcher.ts +0 -139
- package/src/observability/exporters/prometheus-exporter.ts +0 -54
- package/src/observability/metric-retention.ts +0 -64
- package/src/runtime/compaction/compaction-summary.ts +0 -278
- package/src/runtime/errors/crew-errors.ts +0 -162
- package/src/runtime/live-session/intercom-bridge.ts +0 -187
- package/src/runtime/loop-gates.ts +0 -128
- package/src/runtime/metric-parser.ts +0 -36
- package/src/runtime/output/stream-preview.ts +0 -184
- package/src/runtime/output/tool-progress.ts +0 -278
- package/src/runtime/phase-tracker.ts +0 -385
- package/src/runtime/pipeline-runner.ts +0 -523
- package/src/runtime/process/process-lifecycle.ts +0 -491
- package/src/runtime/recovery/retry-runner.ts +0 -330
- package/src/runtime/run-drift.ts +0 -219
- package/src/runtime/scratchpad/snapshot-hmac.ts +0 -167
- package/src/runtime/task-quality.ts +0 -199
- package/src/runtime/task-runner/run-projection.ts +0 -128
- package/src/runtime/verification/post-checks.ts +0 -142
- package/src/state/coordination/schedule.ts +0 -166
- package/src/state/event-log/jsonl-writer.ts +0 -115
- package/src/state/hook-instinct-bridge.ts +0 -94
- package/src/state/hook-integrations.ts +0 -51
- package/src/state/session-state-map.ts +0 -51
- package/src/state/stores/blob-store.ts +0 -308
- package/src/state/stores/instinct-store.ts +0 -275
- package/src/state/stores/observation-store.ts +0 -176
- package/src/state/tiered-eval.ts +0 -480
- package/src/state/types-eval.ts +0 -58
- package/src/tools/safe-bash-extension.ts +0 -54
- package/src/tools/safe-bash.ts +0 -505
- package/src/ui/agent-management-overlay.ts +0 -160
- package/src/ui/crew-footer.ts +0 -102
- package/src/ui/crew-select-list.ts +0 -114
- package/src/ui/dashboard-panes/capability-pane.ts +0 -77
- package/src/ui/transcript-entries.ts +0 -256
- package/src/utils/conflict-detect.ts +0 -721
- package/src/utils/fingerprint.ts +0 -180
- package/src/utils/gh-protocol.ts +0 -556
- package/src/utils/project-detector.ts +0 -160
- package/src/utils/sse-parser.ts +0 -131
- package/src/workflows/cost-estimator.ts +0 -34
- package/src/workflows/intermediate-store.ts +0 -166
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* T2/R4 (ADR-4 §4) — re-plan sweep: dropped-item soft-cancel.
|
|
3
|
+
*
|
|
4
|
+
* Every scheduler tick (selectDispatchBatch, after the ask-deadline sweep)
|
|
5
|
+
* checks the CURRENT plan revision for `dropped` items and reconciles their
|
|
6
|
+
* tasks:
|
|
7
|
+
* - QUEUED tasks are cancelled outright (never dispatched — the re-plan
|
|
8
|
+
* removed their reason to exist).
|
|
9
|
+
* - RUNNING / WAITING / NEEDS_ATTENTION tasks get a SOFT cancel: a wrap-up
|
|
10
|
+
* advisory appended to the worker's steering file (the same JSONL contract
|
|
11
|
+
* as `team steer` and the ChildPiSteeringController soft limit — advisory
|
|
12
|
+
* first, the existing maxTurns+grace machinery stays the hard enforcement).
|
|
13
|
+
* `task.replanDroppedAt` stamps the advisory as issued (exactly-once across
|
|
14
|
+
* ticks) and doubles as the terminal marker "cancelled-by-replan".
|
|
15
|
+
* - A `plan.item.dropped` event is appended per affected task.
|
|
16
|
+
*
|
|
17
|
+
* Reload+persist discipline mirrors sweepExpiredWaitingTasks: best-effort
|
|
18
|
+
* reload, all mutations under withRunLockSync with a fresh in-lock reload,
|
|
19
|
+
* save only when something changed. Never throws into the scheduler tick —
|
|
20
|
+
* failures are logged and the sweep returns undefined (next tick retries).
|
|
21
|
+
*/
|
|
22
|
+
import * as fs from "node:fs";
|
|
23
|
+
import * as path from "node:path";
|
|
24
|
+
import { withRunLockSync } from "../state/coordination/locks.ts";
|
|
25
|
+
import { appendEvent } from "../state/event-log/event-log.ts";
|
|
26
|
+
import { getCurrentPlanRecord } from "../state/stores/plan-store.ts";
|
|
27
|
+
import { loadRunManifestById, saveRunManifest, saveRunTasks } from "../state/stores/state-store.ts";
|
|
28
|
+
import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
29
|
+
import { logInternalError } from "../utils/internal-error.ts";
|
|
30
|
+
import { resolveRealContainedPath } from "../utils/safe-paths.ts";
|
|
31
|
+
|
|
32
|
+
export interface DroppedPlanSweepResult {
|
|
33
|
+
manifest: TeamRunManifest;
|
|
34
|
+
tasks: TeamTaskState[];
|
|
35
|
+
cancelledTaskIds: string[];
|
|
36
|
+
advisedTaskIds: string[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const WRAP_UP_ADVISORY =
|
|
40
|
+
"Plan item dropped by a newer plan revision (re-plan). Wrap up immediately — provide your best final answer now; this task will not be continued.";
|
|
41
|
+
|
|
42
|
+
function appendSteeringAdvisory(manifest: TeamRunManifest, taskId: string): boolean {
|
|
43
|
+
try {
|
|
44
|
+
const steeringDir = path.join(manifest.artifactsRoot, "steering");
|
|
45
|
+
fs.mkdirSync(steeringDir, { recursive: true });
|
|
46
|
+
const safePath = resolveRealContainedPath(steeringDir, `${taskId}.jsonl`);
|
|
47
|
+
// Same 256KB growth cap as the steer tool (subagent-tools.ts) — refuse
|
|
48
|
+
// loudly rather than poisoning the next incarnation's first poll.
|
|
49
|
+
try {
|
|
50
|
+
if (fs.statSync(safePath).size > 256 * 1024) {
|
|
51
|
+
logInternalError("plan-replan.steer-refused-too-large", new Error("steering file over cap"), `taskId=${taskId}`);
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
/* no file yet — fine */
|
|
56
|
+
}
|
|
57
|
+
fs.appendFileSync(
|
|
58
|
+
safePath,
|
|
59
|
+
`${JSON.stringify({ type: "steer", message: WRAP_UP_ADVISORY, ts: new Date().toISOString() })}\n`,
|
|
60
|
+
"utf-8",
|
|
61
|
+
);
|
|
62
|
+
return true;
|
|
63
|
+
} catch (error) {
|
|
64
|
+
logInternalError("plan-replan.steer-write-failed", error instanceof Error ? error : new Error(String(error)), `taskId=${taskId}`);
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Takes the scheduler's IN-MEMORY manifest+tasks (selectDispatchBatch already
|
|
71
|
+
* holds both) — NO disk load on the no-op path. The per-tick cost for runs
|
|
72
|
+
* without plan-linked tasks is one array scan; plans.json is only read when a
|
|
73
|
+
* task carries planItem, and the reload+persist below only runs when an
|
|
74
|
+
* affected task actually exists.
|
|
75
|
+
*/
|
|
76
|
+
export function sweepDroppedPlanItems(initialManifest: TeamRunManifest, initialTasks: TeamTaskState[]): DroppedPlanSweepResult | undefined {
|
|
77
|
+
// Cheap exits FIRST (no I/O): no plan-linked tasks at all.
|
|
78
|
+
if (!initialTasks.some((t) => t.planItem)) return undefined;
|
|
79
|
+
const record = getCurrentPlanRecord(initialManifest);
|
|
80
|
+
if (!record) return undefined;
|
|
81
|
+
const dropped = new Set(record.items.filter((i) => i.status === "dropped").map((i) => i.id));
|
|
82
|
+
if (dropped.size === 0) return undefined;
|
|
83
|
+
if (!initialTasks.some((t) => t.planItem && dropped.has(t.planItem))) return undefined;
|
|
84
|
+
|
|
85
|
+
const cwd = initialManifest.cwd;
|
|
86
|
+
const runId = initialManifest.runId;
|
|
87
|
+
const cancelledTaskIds: string[] = [];
|
|
88
|
+
const advisedTaskIds: string[] = [];
|
|
89
|
+
try {
|
|
90
|
+
const outcome = withRunLockSync(initialManifest, () => {
|
|
91
|
+
const fresh = loadRunManifestById(cwd, runId); // in-lock consistent read
|
|
92
|
+
if (!fresh) return null;
|
|
93
|
+
const freshRecord = getCurrentPlanRecord(fresh.manifest);
|
|
94
|
+
if (!freshRecord) return null;
|
|
95
|
+
const freshDropped = new Set(freshRecord.items.filter((i) => i.status === "dropped").map((i) => i.id));
|
|
96
|
+
if (freshDropped.size === 0) return null;
|
|
97
|
+
let tasks = fresh.tasks;
|
|
98
|
+
let changed = false;
|
|
99
|
+
for (const task of fresh.tasks) {
|
|
100
|
+
if (!task.planItem || !freshDropped.has(task.planItem)) continue;
|
|
101
|
+
if (task.status === "queued") {
|
|
102
|
+
// Never dispatched — cancel outright.
|
|
103
|
+
tasks = tasks.map((t) =>
|
|
104
|
+
t.id === task.id
|
|
105
|
+
? {
|
|
106
|
+
...t,
|
|
107
|
+
status: "cancelled" as const,
|
|
108
|
+
finishedAt: new Date().toISOString(),
|
|
109
|
+
error: "Plan item dropped by re-plan.",
|
|
110
|
+
}
|
|
111
|
+
: t,
|
|
112
|
+
);
|
|
113
|
+
cancelledTaskIds.push(task.id);
|
|
114
|
+
appendEvent(fresh.manifest.eventsPath, {
|
|
115
|
+
type: "plan.item.dropped",
|
|
116
|
+
runId,
|
|
117
|
+
taskId: task.id,
|
|
118
|
+
message: `Task ${task.id} cancelled: plan item '${task.planItem}' dropped by re-plan v${freshRecord.version}.`,
|
|
119
|
+
data: { itemId: task.planItem, planId: freshRecord.id, planVersion: freshRecord.version },
|
|
120
|
+
});
|
|
121
|
+
changed = true;
|
|
122
|
+
} else if (
|
|
123
|
+
(task.status === "running" || task.status === "waiting" || task.status === "needs_attention") &&
|
|
124
|
+
!task.replanDroppedAt
|
|
125
|
+
) {
|
|
126
|
+
// In flight — advisory once; maxTurns+grace stays the hard stop.
|
|
127
|
+
if (!appendSteeringAdvisory(fresh.manifest, task.id)) continue;
|
|
128
|
+
tasks = tasks.map((t) => (t.id === task.id ? { ...t, replanDroppedAt: new Date().toISOString() } : t));
|
|
129
|
+
advisedTaskIds.push(task.id);
|
|
130
|
+
appendEvent(fresh.manifest.eventsPath, {
|
|
131
|
+
type: "plan.item.dropped",
|
|
132
|
+
runId,
|
|
133
|
+
taskId: task.id,
|
|
134
|
+
message: `Wrap-up advisory delivered to ${task.id}: plan item '${task.planItem}' dropped by re-plan v${freshRecord.version}.`,
|
|
135
|
+
data: { itemId: task.planItem, planId: freshRecord.id, planVersion: freshRecord.version, softCancel: true },
|
|
136
|
+
});
|
|
137
|
+
changed = true;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (!changed) return { manifest: fresh.manifest, tasks: fresh.tasks, cancelledTaskIds, advisedTaskIds };
|
|
141
|
+
saveRunTasks(fresh.manifest, tasks);
|
|
142
|
+
const manifest = { ...fresh.manifest, updatedAt: new Date().toISOString() };
|
|
143
|
+
saveRunManifest(manifest);
|
|
144
|
+
return { manifest, tasks, cancelledTaskIds, advisedTaskIds };
|
|
145
|
+
});
|
|
146
|
+
return outcome ?? undefined;
|
|
147
|
+
} catch (error) {
|
|
148
|
+
logInternalError("plan-replan.sweep-failed", error instanceof Error ? error : new Error(String(error)), `runId=${runId}`);
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -166,13 +166,6 @@ export function getCheckpointStore(stateRoot: string): CheckpointStore {
|
|
|
166
166
|
return _stores.get(stateRoot)!;
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
-
/**
|
|
170
|
-
* Clear all checkpoint stores (for testing).
|
|
171
|
-
*/
|
|
172
|
-
export function clearCheckpointStores(): void {
|
|
173
|
-
_stores.clear();
|
|
174
|
-
}
|
|
175
|
-
|
|
176
169
|
/**
|
|
177
170
|
* Save a checkpoint during agent execution.
|
|
178
171
|
*/
|
|
@@ -219,17 +212,6 @@ export function loadCheckpoint(runId: string, taskId: string, cwd?: string): Che
|
|
|
219
212
|
return store.load(runId, taskId);
|
|
220
213
|
}
|
|
221
214
|
|
|
222
|
-
/**
|
|
223
|
-
* Delete a checkpoint after successful completion.
|
|
224
|
-
*/
|
|
225
|
-
export function clearCheckpoint(runId: string, taskId: string, cwd?: string): void {
|
|
226
|
-
assertSafePathId("runId", runId);
|
|
227
|
-
assertSafePathId("taskId", taskId);
|
|
228
|
-
const stateRoot = path.join(projectCrewRoot(cwd ?? process.cwd()), "state", "runs", runId);
|
|
229
|
-
const store = getCheckpointStore(stateRoot);
|
|
230
|
-
store.delete(runId, taskId);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
215
|
/**
|
|
234
216
|
* Check if a checkpoint exists for a task.
|
|
235
217
|
*/
|
|
@@ -6,7 +6,7 @@ import type { MetricRegistry } from "../../observability/metric-registry.ts";
|
|
|
6
6
|
import { withRunLockSync } from "../../state/coordination/locks.ts";
|
|
7
7
|
import { appendEvent, scanSequence } from "../../state/event-log/event-log.ts";
|
|
8
8
|
import { readActiveRunRegistry, unregisterActiveRun } from "../../state/stores/active-run-registry.ts";
|
|
9
|
-
import { loadRunManifestById, saveRunTasks, updateRunStatus } from "../../state/stores/state-store.ts";
|
|
9
|
+
import { loadRunManifestById, saveRunManifest, saveRunTasks, updateRunStatus } from "../../state/stores/state-store.ts";
|
|
10
10
|
import type { TeamTaskState } from "../../state/types.ts";
|
|
11
11
|
import { logInternalError } from "../../utils/internal-error.ts";
|
|
12
12
|
import { projectCrewRoot, userCrewRoot } from "../../utils/paths.ts";
|
|
@@ -17,7 +17,7 @@ import { isWorkerHeartbeatStale } from "../heartbeat/worker-heartbeat.ts";
|
|
|
17
17
|
import { terminateLiveAgentsForRun } from "../live-session/live-agent-manager.ts";
|
|
18
18
|
import type { ManifestCache } from "../manifest-cache.ts";
|
|
19
19
|
import { checkProcessLiveness } from "../process-status.ts";
|
|
20
|
-
import {
|
|
20
|
+
import { isIntentionalWait, isPlanApprovalPendingEffective, type ReconcileResult, reconcileStaleRun } from "../stale-reconciler.ts";
|
|
21
21
|
|
|
22
22
|
export interface RecoveryPlan {
|
|
23
23
|
runId: string;
|
|
@@ -114,7 +114,10 @@ export function detectInterruptedRuns(
|
|
|
114
114
|
for (const manifest of manifestCache.list(50)) {
|
|
115
115
|
if (manifest.status !== "running" && manifest.status !== "blocked") continue;
|
|
116
116
|
// Preserve runs intentionally blocked on plan approval — not crashes.
|
|
117
|
-
|
|
117
|
+
// B1 battery 2026-08-18 (case b): generalized to isIntentionalWait — a run
|
|
118
|
+
// parked on an ask answer is equally intentional and must not be auto-resumed
|
|
119
|
+
// out from under its parked worker.
|
|
120
|
+
if (isIntentionalWait(manifest)) continue;
|
|
118
121
|
if (manifest.async?.pid !== undefined && checkProcessLiveness(manifest.async.pid).alive) continue;
|
|
119
122
|
// Skip runs owned by the current live session — a live session B must NOT
|
|
120
123
|
// detect session A's still-running run as interrupted.
|
|
@@ -142,54 +145,87 @@ export async function applyRecoveryPlan(plan: RecoveryPlan, ctx: Pick<ExtensionC
|
|
|
142
145
|
runId: plan.runId,
|
|
143
146
|
cwd: ctx.cwd,
|
|
144
147
|
});
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
148
|
+
|
|
149
|
+
// R14-4: the hook await above is an arbitrary async gap — a concurrent writer
|
|
150
|
+
// may have completed/cancelled the run in the meantime. Re-read inside the
|
|
151
|
+
// lock and derive every write from the FRESH snapshot; never reset a run that
|
|
152
|
+
// reached a terminal status.
|
|
153
|
+
withRunLockSync(loaded.manifest, () => {
|
|
154
|
+
const fresh = loadRunManifestById(ctx.cwd, plan.runId); // NOTE: inside withRunLockSync - consistent read
|
|
155
|
+
if (!fresh) throw new Error(`Run '${plan.runId}' not found.`);
|
|
156
|
+
if (fresh.manifest.status === "completed" || fresh.manifest.status === "failed" || fresh.manifest.status === "cancelled") {
|
|
157
|
+
// Run reached a terminal status while the recovery hook was running —
|
|
158
|
+
// do NOT reset it (no task reset, no status change).
|
|
159
|
+
appendEvent(fresh.manifest.eventsPath, {
|
|
160
|
+
type: "crew.run.recovery_skipped",
|
|
161
|
+
runId: plan.runId,
|
|
162
|
+
message: `Recovery skipped: run is already '${fresh.manifest.status}'`,
|
|
163
|
+
data: { status: fresh.manifest.status },
|
|
164
|
+
});
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
appendHookEvent(fresh.manifest, hookReport);
|
|
168
|
+
if (hookReport.outcome === "block") {
|
|
169
|
+
appendEvent(fresh.manifest.eventsPath, {
|
|
170
|
+
type: "crew.run.recovery_blocked",
|
|
171
|
+
runId: plan.runId,
|
|
172
|
+
message: `Recovery blocked by hook: ${hookReport.reason ?? "run_recovery hook blocked the operation."}`,
|
|
173
|
+
data: { hookOutcome: "block", reason: hookReport.reason },
|
|
174
|
+
});
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const reset = new Set(plan.resumableTasks);
|
|
179
|
+
const tasks = fresh.tasks.map((task) =>
|
|
180
|
+
reset.has(task.id)
|
|
181
|
+
? {
|
|
182
|
+
...task,
|
|
183
|
+
status: "queued" as const,
|
|
184
|
+
startedAt: undefined,
|
|
185
|
+
finishedAt: undefined,
|
|
186
|
+
error: undefined,
|
|
187
|
+
heartbeat: undefined,
|
|
188
|
+
// WP-2/R2 (ADR-0 item 9): a requeued task must not resume parked
|
|
189
|
+
// state — drop any stale ask-park marker along with the other
|
|
190
|
+
// per-attempt transient fields. Tasks NOT reset (e.g. a task parked
|
|
191
|
+
// in `waiting`) pass through untouched, marker intact; the marker's
|
|
192
|
+
// deadline is then owned by the scheduler tick (dispatch-batch).
|
|
193
|
+
waiting: undefined,
|
|
194
|
+
}
|
|
195
|
+
: task,
|
|
196
|
+
);
|
|
197
|
+
saveRunTasks(fresh.manifest, tasks);
|
|
198
|
+
appendEvent(fresh.manifest.eventsPath, {
|
|
199
|
+
type: "crew.run.resumed",
|
|
149
200
|
runId: plan.runId,
|
|
150
|
-
message: `
|
|
151
|
-
data: {
|
|
201
|
+
message: `Recovered ${plan.resumableTasks.length} interrupted task(s).`,
|
|
202
|
+
data: {
|
|
203
|
+
recoveredFromSeq: plan.lastEventSeq,
|
|
204
|
+
resumableTasks: plan.resumableTasks,
|
|
205
|
+
},
|
|
152
206
|
});
|
|
153
|
-
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
const reset = new Set(plan.resumableTasks);
|
|
157
|
-
const tasks = loaded.tasks.map((task) =>
|
|
158
|
-
reset.has(task.id)
|
|
159
|
-
? {
|
|
160
|
-
...task,
|
|
161
|
-
status: "queued" as const,
|
|
162
|
-
startedAt: undefined,
|
|
163
|
-
finishedAt: undefined,
|
|
164
|
-
error: undefined,
|
|
165
|
-
heartbeat: undefined,
|
|
166
|
-
}
|
|
167
|
-
: task,
|
|
168
|
-
);
|
|
169
|
-
saveRunTasks(loaded.manifest, tasks);
|
|
170
|
-
appendEvent(loaded.manifest.eventsPath, {
|
|
171
|
-
type: "crew.run.resumed",
|
|
172
|
-
runId: plan.runId,
|
|
173
|
-
message: `Recovered ${plan.resumableTasks.length} interrupted task(s).`,
|
|
174
|
-
data: {
|
|
175
|
-
recoveredFromSeq: plan.lastEventSeq,
|
|
176
|
-
resumableTasks: plan.resumableTasks,
|
|
177
|
-
},
|
|
207
|
+
registry?.counter("crew.run.count", "Total runs by status").inc({ status: "resumed" });
|
|
178
208
|
});
|
|
179
|
-
registry?.counter("crew.run.count", "Total runs by status").inc({ status: "resumed" });
|
|
180
209
|
}
|
|
181
210
|
|
|
182
211
|
export function declineRecoveryPlan(plan: RecoveryPlan, ctx: Pick<ExtensionContext, "cwd">): void {
|
|
183
212
|
const loaded = loadRunManifestById(ctx.cwd, plan.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
|
|
184
213
|
if (!loaded) throw new Error(`Run '${plan.runId}' not found.`);
|
|
185
|
-
//
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
214
|
+
// R14-4: re-read inside the lock so the decline decision applies to the
|
|
215
|
+
// on-disk state, not the best-effort snapshot read above. Kept SYNC —
|
|
216
|
+
// withRunLockSync is the sync lock variant.
|
|
217
|
+
withRunLockSync(loaded.manifest, () => {
|
|
218
|
+
const fresh = loadRunManifestById(ctx.cwd, plan.runId); // NOTE: inside withRunLockSync - consistent read
|
|
219
|
+
if (!fresh) return;
|
|
220
|
+
// Log the event first — if appendEvent fails, state remains consistent.
|
|
221
|
+
appendEvent(fresh.manifest.eventsPath, {
|
|
222
|
+
type: "crew.run.recovery_declined",
|
|
223
|
+
runId: plan.runId,
|
|
224
|
+
message: "Interrupted run was not resumed.",
|
|
225
|
+
data: { recoveredFromSeq: plan.lastEventSeq },
|
|
226
|
+
});
|
|
227
|
+
updateRunStatus(fresh.manifest, "cancelled", "interrupted-not-resumed");
|
|
191
228
|
});
|
|
192
|
-
updateRunStatus(loaded.manifest, "cancelled", "interrupted-not-resumed");
|
|
193
229
|
}
|
|
194
230
|
|
|
195
231
|
/**
|
|
@@ -224,9 +260,14 @@ export function cancelOrphanedRuns(
|
|
|
224
260
|
// Phase 1: Scan project-level manifests via manifestCache
|
|
225
261
|
for (const manifest of manifestCache.list(50)) {
|
|
226
262
|
if (manifest.status !== "running" && manifest.status !== "blocked") continue;
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
|
|
263
|
+
// B1 battery 2026-08-18 (case b): preserve runs intentionally blocked on a
|
|
264
|
+
// HUMAN DECISION — plan approval (pre-v2 semantics) OR a pending ask answer
|
|
265
|
+
// within the waiting TTL (ADR-0 item 9: "not crashes and must not be
|
|
266
|
+
// stale-repaired or cancelled by reconciliation"). Live proof: a resumed
|
|
267
|
+
// run parked on ask (worker alive, waitState set, parked workers do NOT
|
|
268
|
+
// heartbeat) was orphan-cancelled 5m18s into a 10-minute park by a THIRD
|
|
269
|
+
// session's startup scan because the predicate only knew plan approval.
|
|
270
|
+
if (isIntentionalWait(manifest)) {
|
|
230
271
|
skipped.push(manifest.runId);
|
|
231
272
|
continue;
|
|
232
273
|
}
|
|
@@ -293,6 +334,14 @@ export function cancelOrphanedRuns(
|
|
|
293
334
|
});
|
|
294
335
|
|
|
295
336
|
saveRunTasks(fresh.manifest, repairedTasks);
|
|
337
|
+
// B1 battery 2026-08-18 (case b leak fix): a cancelled run must not keep
|
|
338
|
+
// its waitState pointer — observed live: orphan-cancel left waitState set,
|
|
339
|
+
// shielding the dead run from staleness repair for the full 24h TTL.
|
|
340
|
+
if (fresh.manifest.waitState) {
|
|
341
|
+
const cleared = { ...fresh.manifest, waitState: undefined, updatedAt: new Date(now).toISOString() };
|
|
342
|
+
saveRunManifest(cleared);
|
|
343
|
+
fresh.manifest = cleared;
|
|
344
|
+
}
|
|
296
345
|
for (const task of repairedTasks) {
|
|
297
346
|
try {
|
|
298
347
|
upsertCrewAgent(fresh.manifest, recordFromTask(fresh.manifest, task, "scaffold"));
|
|
@@ -655,7 +704,7 @@ export function reconcileAllStaleRuns(
|
|
|
655
704
|
// Belt-and-suspenders: reconcileStaleRun itself guards this, but the run
|
|
656
705
|
// may have flipped to blocked+plan-approval between cache-list and lock
|
|
657
706
|
// acquisition — re-check the freshest manifest under the lock.
|
|
658
|
-
if (
|
|
707
|
+
if (isPlanApprovalPendingEffective(fresh.manifest)) {
|
|
659
708
|
results.push({
|
|
660
709
|
runId,
|
|
661
710
|
verdict: "blocked_awaiting_approval",
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scheduler context types for the team-run scheduler loop.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from team-runner.ts (2026-08, Phase 2.6 maintainability split —
|
|
5
|
+
* CORE-4 scheduler context, resolves RT-15). Pure type motion: SettledUnit /
|
|
6
|
+
* PendingUnit / SchedulerContext / SchedulerDecision moved verbatim.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
10
|
+
import type { WorkflowConfig } from "../workflows/workflow-config.ts";
|
|
11
|
+
import type { CrewRuntimeKind } from "./crew-agent-runtime.ts";
|
|
12
|
+
import type { BatchConcurrencyDecision } from "./scheduling/concurrency.ts";
|
|
13
|
+
import type { TaskGraphIndex, TaskGraphSchedulerSnapshot } from "./scheduling/task-graph-scheduler.ts";
|
|
14
|
+
import type { ResultArtifactReadCache } from "./task-output-context.ts";
|
|
15
|
+
// ExecuteTeamRunInput is defined in team-runner.ts — import type ONLY so the
|
|
16
|
+
// type-level team-runner↔scheduler-context reference is erased at runtime
|
|
17
|
+
// (no runtime import cycle; team-runner.ts imports these types back).
|
|
18
|
+
import type { ExecuteTeamRunInput } from "./team-runner.ts";
|
|
19
|
+
import type { WorkflowStateMachine } from "./workflow-state.ts";
|
|
20
|
+
|
|
21
|
+
/** RT-12: result shape from a settled dispatch unit (pre-created wrapper). */
|
|
22
|
+
export type SettledUnit = {
|
|
23
|
+
unitKey: string;
|
|
24
|
+
result: { manifest: TeamRunManifest; tasks: TeamTaskState[] } | undefined;
|
|
25
|
+
error: Error | undefined;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* RT-12: in-flight dispatch unit. `wrapped` is a pre-created wrapper promise
|
|
30
|
+
* (try/catch → SettledUnit) so mergeUnitResult can Promise.race without
|
|
31
|
+
* allocating new async closures every loop iteration (O(C) total wrappers
|
|
32
|
+
* instead of O(C×T) churn).
|
|
33
|
+
*/
|
|
34
|
+
export type PendingUnit = {
|
|
35
|
+
taskIds: string[];
|
|
36
|
+
promise: Promise<{ manifest: TeamRunManifest; tasks: TeamTaskState[] }>;
|
|
37
|
+
wrapped: Promise<SettledUnit>;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// ── CORE-4: SchedulerContext state bag ─────────────────────────────
|
|
41
|
+
// A mutable bag of the closure locals used across executeTeamRunCore.
|
|
42
|
+
// Extracted scheduler functions receive this context and mutate it
|
|
43
|
+
// in-place. This enables incremental extraction of the ~1075-line god
|
|
44
|
+
// function into scheduler/ functions without changing control flow.
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Mutable state shared across the team-run scheduler loop.
|
|
48
|
+
*
|
|
49
|
+
* Fields mirror the closure locals of `executeTeamRunCore`. Extracted
|
|
50
|
+
* scheduler functions mutate these fields in-place; the caller keeps the
|
|
51
|
+
* local variables in sync by assigning back from `ctx` after each call.
|
|
52
|
+
*/
|
|
53
|
+
export interface SchedulerContext {
|
|
54
|
+
input: ExecuteTeamRunInput;
|
|
55
|
+
workflow: WorkflowConfig;
|
|
56
|
+
manifest: TeamRunManifest;
|
|
57
|
+
tasks: TeamTaskState[];
|
|
58
|
+
queueIndex: TaskGraphIndex;
|
|
59
|
+
wfMachine: WorkflowStateMachine;
|
|
60
|
+
pendingUnits: Map<string, PendingUnit>;
|
|
61
|
+
/** Task ids ever dispatched (grows monotonically; never removed). Used by
|
|
62
|
+
* terminaliseRunWithDrain to cancel — not skip — tasks that were in-flight
|
|
63
|
+
* even after their dispatch unit settled + left pendingUnits (RT-NEW-2 race). */
|
|
64
|
+
dispatchedTaskIds: Set<string>;
|
|
65
|
+
runController: AbortController;
|
|
66
|
+
runtimeKind: CrewRuntimeKind;
|
|
67
|
+
adaptivePlanInjected: boolean;
|
|
68
|
+
adaptivePlanMissing: boolean;
|
|
69
|
+
/** Outcome of the most recent mergeUnitResult call: the settled unit's
|
|
70
|
+
* taskIds and the merged result object. Read by the post-merge inline
|
|
71
|
+
* logic (cancel-during-exec check + batch summary). Set by extraction 5. */
|
|
72
|
+
settledMerge: { taskIds: string[]; result: { manifest: TeamRunManifest; tasks: TeamTaskState[] } } | null;
|
|
73
|
+
/** R10-1: per-run result-artifact read cache (single instance, created in
|
|
74
|
+
* executeTeamRunCore). Shared by closeout aggregation and — via baseInput
|
|
75
|
+
* in dispatch-batch.ts — dependency-context reads in prepareTaskExecutionContext. */
|
|
76
|
+
resultReadCache: ResultArtifactReadCache;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Discriminated union representing a scheduler sub-function's decision.
|
|
81
|
+
*
|
|
82
|
+
* - `continue`: proceed to the next phase of the loop body.
|
|
83
|
+
* - `return`: short-circuit the loop and return the given result.
|
|
84
|
+
* - `skip-dispatch`: skip the dispatch phase this iteration (reserved for
|
|
85
|
+
* future extractions).
|
|
86
|
+
*/
|
|
87
|
+
export type SchedulerDecision =
|
|
88
|
+
| { kind: "continue" }
|
|
89
|
+
| { kind: "return"; result: { manifest: TeamRunManifest; tasks: TeamTaskState[] } }
|
|
90
|
+
| { kind: "skip-dispatch" }
|
|
91
|
+
| {
|
|
92
|
+
kind: "dispatch";
|
|
93
|
+
batch: TeamTaskState[];
|
|
94
|
+
concurrency: BatchConcurrencyDecision;
|
|
95
|
+
snapshot: TaskGraphSchedulerSnapshot;
|
|
96
|
+
approvalPending: boolean;
|
|
97
|
+
coalesceEnabled: boolean;
|
|
98
|
+
};
|
|
@@ -91,6 +91,11 @@ export function planCoalescedGroups(
|
|
|
91
91
|
// doesn't compose in a multi-task worker. These flags set up side
|
|
92
92
|
// effects per-task that can't be combined.
|
|
93
93
|
if (step.worktree) continue;
|
|
94
|
+
// T4/R6 (round-1 P2): spec-carrying steps never coalesce — the coalesced
|
|
95
|
+
// path bypasses buildTaskPacket/finalizeTaskResult, which would silently
|
|
96
|
+
// drop the snapshot freeze, the SPEC contract prompt block, and the gate.
|
|
97
|
+
if (step.specRefs?.length) continue;
|
|
98
|
+
if (step.specStrict === true) continue;
|
|
94
99
|
if (step.preStepScript) continue;
|
|
95
100
|
if (step.verify) continue;
|
|
96
101
|
if (step.output !== undefined && step.output !== false && step.output !== "") continue;
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
24
|
import * as os from "node:os";
|
|
25
|
+
import { getCrewEnv } from "../../config/env-vars.ts";
|
|
25
26
|
import { Semaphore } from "./semaphore.ts";
|
|
26
27
|
|
|
27
28
|
/**
|
|
@@ -30,7 +31,7 @@ import { Semaphore } from "./semaphore.ts";
|
|
|
30
31
|
* rather than silently disabling the cap.
|
|
31
32
|
*/
|
|
32
33
|
function resolveCapacity(): number {
|
|
33
|
-
const env =
|
|
34
|
+
const env = getCrewEnv("PI_CREW_MAX_WORKERS");
|
|
34
35
|
if (env !== undefined && env !== "") {
|
|
35
36
|
const parsed = Number.parseInt(env, 10);
|
|
36
37
|
if (Number.isFinite(parsed) && parsed > 0) {
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nested-slot budget for delegate-spawned grandchildren (ADR-5 §2, WP-5 step 3).
|
|
3
|
+
*
|
|
4
|
+
* Grandchildren NEVER ride the global worker semaphore — a waiting parent
|
|
5
|
+
* worker keeps holding its own global slot, so ride-along spawn is the
|
|
6
|
+
* permanent-deadlock shape the repo already records for the judge
|
|
7
|
+
* (`scheduling/global-worker-cap.ts:14-19` MAJ#3). Instead the root-side
|
|
8
|
+
* delegate handler draws from THIS separate budget:
|
|
9
|
+
*
|
|
10
|
+
* max = max(1, floor(globalSem / 2)) (or an explicit config override)
|
|
11
|
+
*
|
|
12
|
+
* FAIL-FAST, NEVER QUEUE: when the budget is exhausted, `tryAcquire` returns
|
|
13
|
+
* false immediately — the caller rejects the delegate with a policy message
|
|
14
|
+
* ("nested spawn budget exhausted; N/M in flight"). No waiting, no queueing.
|
|
15
|
+
*
|
|
16
|
+
* The budget is owned by the single root-side delegate handler process, so an
|
|
17
|
+
* in-memory id-keyed map is the source of truth (no disk state).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export interface NestedSlotSnapshot {
|
|
21
|
+
used: number;
|
|
22
|
+
max: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Compute the nested-slot budget from the global worker semaphore. */
|
|
26
|
+
export function defaultNestedSlotBudget(globalWorkerSemaphore: number): number {
|
|
27
|
+
return Math.max(1, Math.floor(globalWorkerSemaphore / 2));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class NestedSlotBudget {
|
|
31
|
+
private readonly held = new Set<string>();
|
|
32
|
+
readonly max: number;
|
|
33
|
+
|
|
34
|
+
constructor(globalWorkerSemaphore: number, override?: number) {
|
|
35
|
+
if (override !== undefined && (!Number.isFinite(override) || override < 1)) {
|
|
36
|
+
throw new TypeError(`nesting.maxSlots must be a positive integer, got ${String(override)}`);
|
|
37
|
+
}
|
|
38
|
+
this.max = override ?? defaultNestedSlotBudget(globalWorkerSemaphore);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Try to reserve one nested slot for the given grandchild id.
|
|
43
|
+
* Fail-fast: returns false when the budget is exhausted — the caller MUST
|
|
44
|
+
* reject the delegate immediately (never queue).
|
|
45
|
+
*/
|
|
46
|
+
tryAcquire(grandchildId: string): boolean {
|
|
47
|
+
if (this.held.has(grandchildId)) return true; // idempotent re-acquire
|
|
48
|
+
if (this.held.size >= this.max) return false;
|
|
49
|
+
this.held.add(grandchildId);
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Release the slot held by a finished/dead grandchild. Idempotent. */
|
|
54
|
+
release(grandchildId: string): void {
|
|
55
|
+
this.held.delete(grandchildId);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
get used(): number {
|
|
59
|
+
return this.held.size;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Human-facing status line for policy messages: "N/M in flight". */
|
|
63
|
+
get statusLine(): string {
|
|
64
|
+
return `${this.held.size}/${this.max} in flight`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
snapshot(): NestedSlotSnapshot {
|
|
68
|
+
return { used: this.held.size, max: this.max };
|
|
69
|
+
}
|
|
70
|
+
}
|