pi-subagents 0.80.0 → 0.81.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.
Files changed (87) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/bundle/agents.js +18 -0
  3. package/bundle/background-work.js +16 -0
  4. package/bundle/capability-ceiling.js +22 -0
  5. package/bundle/child-tool-plan.js +11 -0
  6. package/bundle/chunk-26C3WLYQ.js +8388 -0
  7. package/bundle/chunk-2XYWN7QJ.js +128 -0
  8. package/bundle/chunk-2ZSNH26H.js +4164 -0
  9. package/bundle/chunk-3DC5KBVP.js +235 -0
  10. package/bundle/chunk-3IF2PGW7.js +380 -0
  11. package/bundle/chunk-4QFZZV3O.js +57 -0
  12. package/bundle/chunk-563BHAZ5.js +1365 -0
  13. package/bundle/chunk-5LFLVSTM.js +260 -0
  14. package/bundle/chunk-62OCY2S4.js +14 -0
  15. package/bundle/chunk-74RT5YQE.js +326 -0
  16. package/bundle/chunk-7F6RKT6W.js +132 -0
  17. package/bundle/chunk-7J4ZXMY7.js +18 -0
  18. package/bundle/chunk-ARRKBB2B.js +179 -0
  19. package/bundle/chunk-BJN5LUQY.js +458 -0
  20. package/bundle/chunk-BPS2AGBZ.js +5421 -0
  21. package/bundle/chunk-CEKS3TFO.js +1337 -0
  22. package/bundle/chunk-CTPAE36Y.js +94 -0
  23. package/bundle/chunk-D4EHT5SJ.js +14 -0
  24. package/bundle/chunk-EYNJ6VL6.js +526 -0
  25. package/bundle/chunk-G5DKFLLI.js +1185 -0
  26. package/bundle/chunk-KSWQVGDW.js +1027 -0
  27. package/bundle/chunk-L5ZHE4L4.js +946 -0
  28. package/bundle/chunk-LSRUTHT7.js +57 -0
  29. package/bundle/chunk-N2BN7BF5.js +159 -0
  30. package/bundle/chunk-NF7IEKBH.js +490 -0
  31. package/bundle/chunk-NIXWDZZV.js +9 -0
  32. package/bundle/chunk-NNLASTQE.js +4082 -0
  33. package/bundle/chunk-TUWLXTAM.js +935 -0
  34. package/bundle/chunk-UKG4V45O.js +228 -0
  35. package/bundle/chunk-V2JWDB5U.js +753 -0
  36. package/bundle/chunk-V3BIUPLU.js +9163 -0
  37. package/bundle/chunk-V6MR5TOQ.js +270 -0
  38. package/bundle/chunk-VJHOELQP.js +8245 -0
  39. package/bundle/chunk-WSBNENFS.js +732 -0
  40. package/bundle/chunk-XMFLCKMQ.js +16664 -0
  41. package/bundle/chunk-YFCK7H2S.js +129 -0
  42. package/bundle/chunk-YYREO77V.js +137 -0
  43. package/bundle/chunk-Z7KVHQHL.js +173 -0
  44. package/bundle/chunk-ZBC44JJ3.js +8606 -0
  45. package/bundle/chunk-ZP3HODFH.js +175 -0
  46. package/bundle/control-channel.js +11 -0
  47. package/bundle/delegation.js +14 -0
  48. package/bundle/external-job-provider.js +20 -0
  49. package/bundle/external-runs.js +21 -0
  50. package/bundle/index.js +30 -0
  51. package/bundle/inspectors.js +8 -0
  52. package/bundle/intercom-bridge.js +11 -0
  53. package/bundle/preflight.js +425 -0
  54. package/bundle/project-panes.js +28 -0
  55. package/bundle/required-child-extensions.js +6 -0
  56. package/bundle/shared-types.js +8 -0
  57. package/bundle/workflow-resources.js +7 -0
  58. package/docs/extension-api.md +27 -0
  59. package/docs/observability.md +6 -2
  60. package/package.json +18 -18
  61. package/src/agents/frontmatter.d.ts +2 -0
  62. package/src/agents/frontmatter.js +9 -2
  63. package/src/api/delegation.d.ts +20 -0
  64. package/src/extension/index.js +27 -7
  65. package/src/runs/background/run-child-session.js +18 -0
  66. package/src/runs/background/stale-run-reconciler.js +2 -0
  67. package/src/runs/foreground/execution.js +39 -2
  68. package/src/runs/shared/structured-output.d.ts +1 -0
  69. package/src/runs/shared/structured-output.js +1 -0
  70. package/src/shared/accessible-dir.js +4 -1
  71. package/src/shared/provider-diagnostics.d.ts +9 -0
  72. package/src/shared/provider-diagnostics.js +75 -0
  73. package/src/shared/types.d.ts +3 -0
  74. package/src/slash/delegation-adapters.d.ts +3 -1
  75. package/src/slash/delegation-adapters.js +7 -0
  76. package/src/slash/prompt-template-bridge.js +3 -1
  77. package/src/slash/subagents-admin.d.ts +1 -0
  78. package/src/slash/subagents-admin.js +1 -1
  79. package/src/tui/fleet-layout.d.ts +20 -0
  80. package/src/tui/fleet-layout.js +17 -0
  81. package/src/tui/fleet-viewport.d.ts +12 -0
  82. package/src/tui/fleet-viewport.js +35 -0
  83. package/src/tui/fleet.d.ts +5 -6
  84. package/src/tui/fleet.js +69 -51
  85. package/src/workflows/structured-workflow-scripts.d.ts +1 -0
  86. package/src/workflows/structured-workflow-scripts.js +5 -4
  87. package/src/workflows/workflow-resources.js +2 -2
@@ -0,0 +1,732 @@
1
+ import {
2
+ checkPidLiveness,
3
+ currentPidNamespaceScope,
4
+ isTerminalAsyncState,
5
+ normalizeWorktreeBranchPrefix,
6
+ readProcessTerminal,
7
+ readWorkflowChildEvidence,
8
+ readWorkflowChildProcessEvidence,
9
+ validateAuthorityPolicy,
10
+ validateMissionStoreConfig,
11
+ validateModelResponseAliases
12
+ } from "./chunk-VJHOELQP.js";
13
+ import {
14
+ getAgentDir,
15
+ readStatus,
16
+ validatePermissionConfig,
17
+ writePrivateAtomicJson
18
+ } from "./chunk-ZBC44JJ3.js";
19
+ import {
20
+ FLEET_KEYBINDING_ACTIONS,
21
+ RUNNER_LAUNCHER_NAME_PATTERN,
22
+ RUNNER_LAUNCHER_NAME_RULE,
23
+ TEMP_ROOT_DIR
24
+ } from "./chunk-5LFLVSTM.js";
25
+
26
+ // src/shared/disabled-features.ts
27
+ var SUBAGENT_FEATURES = {
28
+ "agent-management": {
29
+ actions: ["create", "update", "delete", "eject", "disable", "enable", "reset", "refine", "refine.show", "refine.rollback"],
30
+ params: ["config"]
31
+ },
32
+ watchdog: {
33
+ actions: ["watchdog.status", "watchdog.check", "watchdog.configure", "watchdog.recommend-model"],
34
+ params: ["scope", "target", "thinking"]
35
+ },
36
+ panes: {
37
+ actions: ["inspector.open", "inspector.command", "inspector.status", "inspector.close", "project.open", "project.status", "project.close"],
38
+ params: ["focus"]
39
+ },
40
+ missions: {
41
+ actions: ["mission.create", "mission.list", "mission.show", "mission.update", "mission.resolve-decision", "mission.attach-run", "mission.close"],
42
+ params: ["mission", "missionUpdate", "missionStatus", "missionScope", "missionId", "runMode", "runStatus", "summary"]
43
+ },
44
+ "lane-management": {
45
+ actions: ["lane.status", "lane.recordMerge", "lane.recordSupersession", "worktree.discard", "worktree.cleanup"],
46
+ params: ["handoffPath", "laneId", "merge", "supersession", "repo", "planId"]
47
+ },
48
+ "spawn-budget-grants": { actions: ["grant-spawn-budget"], params: ["additional"] },
49
+ preflight: { actions: [], params: ["preflight"] },
50
+ "lane-metadata": { actions: [], params: ["lane"] },
51
+ gates: { actions: [], params: ["gate"] },
52
+ "usage-budgets": { actions: [], params: ["usageBudget"] },
53
+ "tool-budgets": { actions: [], params: ["toolBudget"] },
54
+ "control-overrides": { actions: [], params: ["control"] },
55
+ "extension-bindings": { actions: [], params: ["extensionBindings"] },
56
+ "external-machines": { actions: [], params: ["machine"] },
57
+ "workflow-scripts": { actions: ["validate"], params: ["workflow", "args", "preflight", "globalConcurrencyLimit", "maxSubagentSpawnsPerRun"] }
58
+ };
59
+ var SCHEDULE_SURFACE = {
60
+ actions: ["schedule.create", "schedule.list", "schedule.show", "schedule.history", "schedule.pause", "schedule.resume", "schedule.run", "schedule.run-due", "schedule.delete"],
61
+ params: ["name", "at", "every", "sessionOnly", "quiet", "on", "timezone", "overlap", "catchUp"]
62
+ };
63
+ function validateDisabledFeatures(value) {
64
+ if (value === void 0) return;
65
+ if (!Array.isArray(value)) throw new Error("config.disabledFeatures must be an array of feature names");
66
+ const seen = /* @__PURE__ */ new Set();
67
+ for (const entry of value) {
68
+ if (entry === "schedules") throw new Error(`config.disabledFeatures does not accept "schedules"; set config.scheduledRuns.enabled to false instead`);
69
+ if (typeof entry !== "string" || !Object.hasOwn(SUBAGENT_FEATURES, entry)) {
70
+ throw new Error(`config.disabledFeatures entry ${JSON.stringify(entry)} is not one of: ${Object.keys(SUBAGENT_FEATURES).join(", ")}`);
71
+ }
72
+ if (seen.has(entry)) throw new Error(`config.disabledFeatures lists "${entry}" more than once`);
73
+ seen.add(entry);
74
+ }
75
+ }
76
+ function featureSurface(feature) {
77
+ if (feature === "schedules") return { ...SCHEDULE_SURFACE, disabledBy: "scheduledRuns.enabled=false" };
78
+ return { ...SUBAGENT_FEATURES[feature], disabledBy: `disabledFeatures "${feature}"` };
79
+ }
80
+ function resolveDisabledFeatureSurface(config) {
81
+ const features = new Set(config.disabledFeatures);
82
+ if (config.scheduledRuns?.enabled === false) features.add("schedules");
83
+ const params = /* @__PURE__ */ new Map();
84
+ const actions = /* @__PURE__ */ new Map();
85
+ for (const feature of features) {
86
+ const surface = featureSurface(feature);
87
+ for (const param of surface.params) if (feature === "workflow-scripts" || !params.has(param)) params.set(param, surface.disabledBy);
88
+ for (const action of surface.actions) actions.set(action, surface.disabledBy);
89
+ }
90
+ return { features, params, actions };
91
+ }
92
+ function disabledFeatureUseError(request, surface, label = "subagent") {
93
+ const params = request;
94
+ const action = typeof params.action === "string" ? params.action.trim() : void 0;
95
+ const disabledAction = action === void 0 ? void 0 : surface.actions.get(action);
96
+ if (disabledAction) return `${label} action '${action}' is disabled by config ${disabledAction}.`;
97
+ for (const [param, disabledBy] of surface.params) {
98
+ if (params[param] !== void 0) return `${label} option '${param}' is disabled by config ${disabledBy}.`;
99
+ }
100
+ if (params.workflowScript !== void 0 && surface.features.has("workflow-scripts")) {
101
+ return `${label} workflow scripts are disabled by config ${featureSurface("workflow-scripts").disabledBy}.`;
102
+ }
103
+ return void 0;
104
+ }
105
+ function disabledFeatureNotice(surface) {
106
+ if (surface.features.size === 0) return void 0;
107
+ const lines = [...surface.features].map((feature) => {
108
+ const { actions, params, disabledBy } = featureSurface(feature);
109
+ const parts = [`options ${params.join(", ")}`, ...actions.length > 0 ? [`actions ${actions.join(", ")}`] : []];
110
+ return `- ${disabledBy}: ${parts.join("; ")}`;
111
+ });
112
+ return `Disabled by config in this session. The reference below still lists these, but calls that use them are rejected:
113
+ ${lines.join("\n")}`;
114
+ }
115
+
116
+ // src/runs/background/active-async-capacity.ts
117
+ import { createHash, randomUUID } from "node:crypto";
118
+ import * as fs from "node:fs";
119
+ import * as path from "node:path";
120
+ var ACTIVE_ASYNC_CAPACITY_DIR = path.join(TEMP_ROOT_DIR, "session-active-async-capacity");
121
+ var DEFAULT_ABANDONED_SLOT_RELEASE_AFTER_MS = 20 * 60 * 1e3;
122
+ var MIN_ABANDONED_SLOT_RELEASE_AFTER_MS = 5 * 60 * 1e3;
123
+ var MAX_ABANDONED_SLOT_RELEASE_AFTER_MS = 24 * 60 * 60 * 1e3;
124
+ var ActiveAsyncCapacityError = class extends Error {
125
+ snapshot;
126
+ constructor(snapshot) {
127
+ super(`Active async run capacity exhausted: ${snapshot.used}/${snapshot.limit} used.`);
128
+ this.name = "ActiveAsyncCapacityError";
129
+ this.snapshot = snapshot;
130
+ }
131
+ };
132
+ function resolveMaxActiveAsyncRunsPerSession(value) {
133
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 0) return void 0;
134
+ return value === 0 ? void 0 : value;
135
+ }
136
+ function resolveAbandonedSlotReleaseAfterMs(value) {
137
+ if (value === false) return false;
138
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 1) return DEFAULT_ABANDONED_SLOT_RELEASE_AFTER_MS;
139
+ return value;
140
+ }
141
+ function activeAsyncCapacitySessionKey(sessionId) {
142
+ return createHash("sha256").update(sessionId).digest("hex");
143
+ }
144
+ function sessionDir(sessionId, rootDir) {
145
+ return path.join(rootDir, activeAsyncCapacitySessionKey(sessionId));
146
+ }
147
+ function slotDir(poolDir, slot) {
148
+ return path.join(poolDir, `slot-${slot}`);
149
+ }
150
+ function parseOwner(value) {
151
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
152
+ const owner = value;
153
+ if (owner.version !== 1 || typeof owner.reservationToken !== "string" || !owner.reservationToken || typeof owner.ownerSessionId !== "string" || !owner.ownerSessionId || typeof owner.ownerSessionKey !== "string" || !owner.ownerSessionKey || typeof owner.slot !== "number" || !Number.isInteger(owner.slot) || owner.slot < 0 || typeof owner.runId !== "string" || !owner.runId || typeof owner.generation !== "number" || !Number.isInteger(owner.generation) || owner.generation < 0 || owner.kind !== "runner" && owner.kind !== "workflow" || typeof owner.asyncDir !== "string" || !owner.asyncDir || typeof owner.reservedAt !== "number" || !Number.isFinite(owner.reservedAt)) return void 0;
154
+ if (owner.sourceRunId !== void 0 && typeof owner.sourceRunId !== "string") return void 0;
155
+ if (owner.runnerProcessInstanceId !== void 0 && (typeof owner.runnerProcessInstanceId !== "string" || !owner.runnerProcessInstanceId)) return void 0;
156
+ if (owner.runnerStartedAt !== void 0 && (typeof owner.runnerStartedAt !== "number" || !Number.isFinite(owner.runnerStartedAt))) return void 0;
157
+ return owner;
158
+ }
159
+ function readOwner(dir) {
160
+ try {
161
+ return parseOwner(JSON.parse(fs.readFileSync(path.join(dir, "owner.json"), "utf-8")));
162
+ } catch {
163
+ return void 0;
164
+ }
165
+ }
166
+ function occupiedSlots(poolDir) {
167
+ try {
168
+ return fs.readdirSync(poolDir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && /^slot-\d+$/.test(entry.name)).map((entry) => path.join(poolDir, entry.name));
169
+ } catch (error) {
170
+ if (error.code === "ENOENT") return [];
171
+ throw error;
172
+ }
173
+ }
174
+ function snapshotFor(sessionId, limit, rootDir) {
175
+ return { used: occupiedSlots(sessionDir(sessionId, rootDir)).length, limit: limit ?? 0 };
176
+ }
177
+ function matchingOwner(dir, expected) {
178
+ const owner = readOwner(dir);
179
+ return owner?.reservationToken === expected.reservationToken && owner.runId === expected.runId && owner.generation === expected.generation ? owner : void 0;
180
+ }
181
+ function withSlotClaim(dir, operation) {
182
+ const claimPath = path.join(dir, "capacity.claim");
183
+ const claimToken = randomUUID();
184
+ let claim;
185
+ try {
186
+ claim = fs.openSync(claimPath, "wx", 384);
187
+ fs.writeFileSync(claim, claimToken, "utf-8");
188
+ fs.closeSync(claim);
189
+ claim = void 0;
190
+ } catch (error) {
191
+ if (claim !== void 0) fs.closeSync(claim);
192
+ if (error.code === "EEXIST" || error.code === "ENOENT") return { acquired: false };
193
+ throw error;
194
+ }
195
+ try {
196
+ return { acquired: true, value: operation() };
197
+ } finally {
198
+ try {
199
+ if (fs.readFileSync(claimPath, "utf-8") === claimToken) fs.rmSync(claimPath, { force: true });
200
+ } catch (error) {
201
+ if (error.code !== "ENOENT") throw error;
202
+ }
203
+ }
204
+ }
205
+ function removeOwnedSlot(dir, expected, options, requireUnstarted = false, release) {
206
+ if (requireUnstarted && (expected.runnerProcessInstanceId || expected.runnerStartedAt)) return false;
207
+ const claimed = withSlotClaim(dir, () => {
208
+ const current = matchingOwner(dir, expected);
209
+ if (!current || requireUnstarted && (current.runnerProcessInstanceId || current.runnerStartedAt)) return false;
210
+ const releasedDir = path.join(path.dirname(dir), `.${path.basename(dir)}.released-${randomUUID()}`);
211
+ fs.renameSync(dir, releasedDir);
212
+ options.afterSlotRename?.(releasedDir);
213
+ if (release?.state === "releasable" && release.evidence) {
214
+ appendAbandonedReleaseEvent(expected.asyncDir, expected, release.evidence, options.now?.() ?? Date.now());
215
+ }
216
+ fs.rmSync(releasedDir, { recursive: true, force: true });
217
+ return true;
218
+ });
219
+ return claimed.acquired && claimed.value;
220
+ }
221
+ function appendAbandonedReleaseEvent(asyncDir, owner, evidence, now) {
222
+ try {
223
+ const eventsPath = path.join(asyncDir, "events.jsonl");
224
+ fs.mkdirSync(path.dirname(eventsPath), { recursive: true });
225
+ fs.appendFileSync(eventsPath, `${JSON.stringify({
226
+ type: "subagent.capacity.released",
227
+ ts: now,
228
+ runId: owner.runId,
229
+ sessionId: owner.ownerSessionId,
230
+ ...evidence
231
+ })}
232
+ `, "utf-8");
233
+ } catch {
234
+ }
235
+ }
236
+ function runnerReleaseVerdict(owner, status, options) {
237
+ if (!status) return { state: "retained", reason: "status file is missing or unreadable" };
238
+ if (!owner.runnerProcessInstanceId) return { state: "retained", reason: "runner process identity has not been recorded" };
239
+ if (status.sessionId !== owner.ownerSessionId) return { state: "retained", reason: `status session ${status.sessionId ?? "unknown"} does not match owner session ${owner.ownerSessionId}` };
240
+ if (status.runId !== owner.runId) return { state: "retained", reason: `status run ${status.runId} does not match owner run ${owner.runId}` };
241
+ if (!isTerminalAsyncState(status.state)) return { state: "retained", reason: `run is still ${status.state}` };
242
+ if (status.processTerminal?.state === "not-started" && status.processTerminal.runId === owner.runId && status.processTerminal.runnerProcessInstanceId === owner.runnerProcessInstanceId && typeof status.error === "string" && status.error) return { state: "releasable", reason: "run failed before child startup completed" };
243
+ const proof = readProcessTerminal(owner.asyncDir, {
244
+ runId: owner.runId,
245
+ runnerProcessInstanceId: owner.runnerProcessInstanceId
246
+ });
247
+ return proof?.state === "observed" && proof.runId === owner.runId && proof.runnerProcessInstanceId === owner.runnerProcessInstanceId ? { state: "releasable", reason: "matching observed process-terminal proof is present" } : abandonedRunnerReleaseVerdict(status, proof?.state ?? "missing", options);
248
+ }
249
+ function abandonedRunnerReleaseVerdict(status, proofState, options, anyTerminalState = false) {
250
+ const proofReason = `process-terminal proof is ${proofState}`;
251
+ const thresholdMs = resolveAbandonedSlotReleaseAfterMs(options.abandonedSlotReleaseAfterMs);
252
+ if (thresholdMs === false) return { state: "retained", reason: `${proofReason}; abandoned-timeout policy is disabled` };
253
+ if (anyTerminalState ? !isTerminalAsyncState(status.state) : status.state !== "failed") return { state: "retained", reason: `${proofReason}; abandoned-timeout policy requires a ${anyTerminalState ? "terminal" : "failed"} run, not ${status.state}` };
254
+ if (typeof status.pid !== "number" || !Number.isInteger(status.pid) || status.pid < 1) return { state: "retained", reason: `${proofReason}; runner PID is missing or invalid` };
255
+ if (status.pidNamespaceScope !== void 0 && status.pidNamespaceScope !== (options.pidNamespaceScope ?? currentPidNamespaceScope)()) return { state: "retained", reason: `${proofReason}; runner PID namespace differs from this process, so liveness is unknown` };
256
+ const liveness = (options.pidLiveness ?? checkPidLiveness)(status.pid);
257
+ if (liveness !== "dead") return { state: "retained", reason: `${proofReason}; runner PID liveness is ${liveness}` };
258
+ const lastActivityAt = status.lastActivityAt ?? status.lastUpdate ?? status.endedAt;
259
+ if (typeof lastActivityAt !== "number" || !Number.isFinite(lastActivityAt)) return { state: "retained", reason: `${proofReason}; last activity timestamp is missing or invalid` };
260
+ const now = options.now?.() ?? Date.now();
261
+ const lastActivityAgeMs = Math.max(0, now - lastActivityAt);
262
+ if (lastActivityAgeMs <= thresholdMs) return { state: "retained", reason: `${proofReason}; last activity age ${lastActivityAgeMs}ms has not exceeded abandoned-timeout ${thresholdMs}ms` };
263
+ const evidence = {
264
+ releasedBy: "abandoned-timeout",
265
+ processProof: "unknown",
266
+ runnerPid: "gone",
267
+ lastActivityAgeMs,
268
+ abandonedSlotReleaseAfterMs: thresholdMs
269
+ };
270
+ return {
271
+ state: "releasable",
272
+ reason: `${evidence.releasedBy}: ${proofReason}; runner PID is gone; process proof unknown; last activity age ${lastActivityAgeMs}ms exceeds ${thresholdMs}ms`,
273
+ evidence
274
+ };
275
+ }
276
+ function workflowReleaseVerdict(owner, status, liveWorkflowRunIds, options) {
277
+ if (!status) return { state: "retained", reason: "status file is missing or unreadable" };
278
+ if (status.sessionId !== owner.ownerSessionId) return { state: "retained", reason: `status session ${status.sessionId ?? "unknown"} does not match owner session ${owner.ownerSessionId}` };
279
+ if (status.runId !== owner.runId) return { state: "retained", reason: `status run ${status.runId} does not match owner run ${owner.runId}` };
280
+ if (status.mode !== "workflow") return { state: "retained", reason: `status mode is ${status.mode}, not workflow` };
281
+ if (!isTerminalAsyncState(status.state)) return { state: "retained", reason: `workflow is still ${status.state}` };
282
+ if (liveWorkflowRunIds.has(owner.runId)) return { state: "retained", reason: "workflow controller is still live" };
283
+ const evidence = readWorkflowChildProcessEvidence(owner.asyncDir, status.steps);
284
+ if (evidence.state === "observed") return { state: "releasable", reason: "workflow is terminal, controller is gone, and async children have observed proof" };
285
+ return abandonedWorkflowReleaseVerdict(owner, status, evidence.reason, options);
286
+ }
287
+ function abandonedWorkflowReleaseVerdict(owner, status, proofReason, options) {
288
+ const thresholdMs = resolveAbandonedSlotReleaseAfterMs(options.abandonedSlotReleaseAfterMs);
289
+ if (thresholdMs === false) return { state: "retained", reason: `${proofReason}; abandoned-timeout policy is disabled` };
290
+ if (!status.steps) return { state: "retained", reason: proofReason };
291
+ if (typeof status.endedAt !== "number" || !Number.isFinite(status.endedAt)) return { state: "retained", reason: `${proofReason}; workflow end timestamp is missing or invalid` };
292
+ const workflowEndedAgeMs = (options.now?.() ?? Date.now()) - status.endedAt;
293
+ if (workflowEndedAgeMs <= thresholdMs) return { state: "retained", reason: `${proofReason}; workflow ended ${workflowEndedAgeMs}ms ago, within abandoned-timeout ${thresholdMs}ms` };
294
+ const abandonedChildren = [];
295
+ for (const step of status.steps) {
296
+ const child = readWorkflowChildEvidence(owner.asyncDir, step);
297
+ if (!child || child.state === "resolved") continue;
298
+ const unresolved = child.unresolvedProof;
299
+ if (!unresolved) return { state: "retained", reason: child.reason };
300
+ if (unresolved.status.sessionId !== owner.ownerSessionId) return { state: "retained", reason: `${child.reason}; child session ${unresolved.status.sessionId ?? "unknown"} does not match owner session ${owner.ownerSessionId}` };
301
+ const verdict = abandonedRunnerReleaseVerdict(unresolved.status, unresolved.proofState, options, true);
302
+ if (verdict.state !== "releasable") return { state: "retained", reason: `async workflow child ${unresolved.status.runId}: ${verdict.reason}` };
303
+ abandonedChildren.push({ runId: unresolved.status.runId, state: unresolved.status.state, lastActivityAgeMs: verdict.evidence.lastActivityAgeMs });
304
+ }
305
+ const evidence = {
306
+ releasedBy: "abandoned-timeout",
307
+ processProof: "unknown",
308
+ controller: "unregistered",
309
+ workflowEndedAgeMs,
310
+ abandonedChildren,
311
+ abandonedSlotReleaseAfterMs: thresholdMs
312
+ };
313
+ return {
314
+ state: "releasable",
315
+ reason: `${evidence.releasedBy}: ${proofReason}; workflow controller is unregistered; terminal async children ${abandonedChildren.map((child) => child.runId).join(", ")} have dead runner PIDs; process proof unknown; workflow ended ${workflowEndedAgeMs}ms ago, over ${thresholdMs}ms`,
316
+ evidence
317
+ };
318
+ }
319
+ function ownerReleaseVerdict(owner, liveWorkflowRunIds, options) {
320
+ const status = readStatus(owner.asyncDir);
321
+ return owner.kind === "runner" ? runnerReleaseVerdict(owner, status, options) : workflowReleaseVerdict(owner, status, liveWorkflowRunIds, options);
322
+ }
323
+ function capacitySessionDirs(rootDir, sessionId) {
324
+ if (sessionId) return [sessionDir(sessionId, rootDir)];
325
+ try {
326
+ return fs.readdirSync(rootDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => path.join(rootDir, entry.name));
327
+ } catch (error) {
328
+ if (error.code === "ENOENT") return [];
329
+ throw error;
330
+ }
331
+ }
332
+ function inspectActiveAsyncCapacityOwner(input, options = {}) {
333
+ const rootDir = options.rootDir ?? ACTIVE_ASYNC_CAPACITY_DIR;
334
+ const liveWorkflowRunIds = options.liveWorkflowRunIds ?? /* @__PURE__ */ new Set();
335
+ for (const poolDir of capacitySessionDirs(rootDir, input.sessionId)) {
336
+ for (const dir of occupiedSlots(poolDir)) {
337
+ const owner = readOwner(dir);
338
+ if (!owner) continue;
339
+ const sameRun = owner.runId === input.runId || input.asyncDir !== void 0 && path.resolve(owner.asyncDir) === path.resolve(input.asyncDir);
340
+ const sourceRun = owner.sourceRunId === input.runId;
341
+ if (!sameRun && !sourceRun) continue;
342
+ if (sourceRun && !sameRun) {
343
+ return {
344
+ owner,
345
+ relation: "source",
346
+ slotDir: dir,
347
+ release: { state: "not-owned", reason: `slot was transferred to ${owner.runId}` }
348
+ };
349
+ }
350
+ return { owner, relation: "current", slotDir: dir, release: ownerReleaseVerdict(owner, liveWorkflowRunIds, options) };
351
+ }
352
+ }
353
+ return { relation: "none", release: { state: "not-owned", reason: "no active-capacity slot records this run" } };
354
+ }
355
+ function reconcileActiveAsyncCapacity(sessionId, limit, options = {}) {
356
+ const rootDir = options.rootDir ?? ACTIVE_ASYNC_CAPACITY_DIR;
357
+ const poolDir = sessionDir(sessionId, rootDir);
358
+ const liveWorkflowRunIds = options.liveWorkflowRunIds ?? /* @__PURE__ */ new Set();
359
+ for (const dir of occupiedSlots(poolDir)) {
360
+ const owner = readOwner(dir);
361
+ if (!owner || owner.ownerSessionId !== sessionId || owner.ownerSessionKey !== activeAsyncCapacitySessionKey(sessionId) || path.basename(dir) !== `slot-${owner.slot}`) continue;
362
+ const release = ownerReleaseVerdict(owner, liveWorkflowRunIds, options);
363
+ if (release.state !== "releasable") continue;
364
+ removeOwnedSlot(dir, owner, options, false, release);
365
+ }
366
+ return snapshotFor(sessionId, limit, rootDir);
367
+ }
368
+ function getActiveAsyncCapacitySnapshot(sessionId, limit, options = {}) {
369
+ return reconcileActiveAsyncCapacity(sessionId, limit, options);
370
+ }
371
+ function createSlot(poolDir, owner) {
372
+ const destination = slotDir(poolDir, owner.slot);
373
+ fs.mkdirSync(poolDir, { recursive: true, mode: 448 });
374
+ try {
375
+ fs.mkdirSync(destination, { mode: 448 });
376
+ } catch (error) {
377
+ if (error.code === "EEXIST") return false;
378
+ throw error;
379
+ }
380
+ fs.writeFileSync(path.join(destination, "owner.json"), `${JSON.stringify(owner, null, 2)}
381
+ `, { encoding: "utf-8", mode: 384 });
382
+ return true;
383
+ }
384
+ function handleFor(owner, limit, options, rollbackOwner) {
385
+ const rootDir = options.rootDir ?? ACTIVE_ASYNC_CAPACITY_DIR;
386
+ const writeOwner = options.writeOwner ?? writePrivateAtomicJson;
387
+ const dir = slotDir(sessionDir(owner.ownerSessionId, rootDir), owner.slot);
388
+ return {
389
+ owner,
390
+ markStarted(runnerProcessInstanceId) {
391
+ const claimed = withSlotClaim(dir, () => {
392
+ const current = matchingOwner(dir, owner);
393
+ if (!current) return false;
394
+ const next = { ...current, runnerProcessInstanceId, runnerStartedAt: options.now?.() ?? Date.now() };
395
+ Object.assign(owner, next);
396
+ writeOwner(path.join(dir, "owner.json"), next);
397
+ return true;
398
+ });
399
+ if (!claimed.acquired || !claimed.value) throw new Error(`Active async capacity ownership changed for run '${owner.runId}'.`);
400
+ },
401
+ markWorkflowStarted() {
402
+ const claimed = withSlotClaim(dir, () => {
403
+ const current = matchingOwner(dir, owner);
404
+ if (!current || current.kind !== "workflow") return false;
405
+ const next = { ...current, runnerStartedAt: options.now?.() ?? Date.now() };
406
+ Object.assign(owner, next);
407
+ try {
408
+ writePrivateAtomicJson(path.join(dir, "owner.json"), next);
409
+ } catch (error) {
410
+ console.error(`Failed to mark async workflow '${owner.runId}' started; capacity will remain occupied:`, error);
411
+ }
412
+ return true;
413
+ });
414
+ if (!claimed.acquired || !claimed.value) throw new Error(`Active async capacity ownership changed for workflow '${owner.runId}'.`);
415
+ },
416
+ rollback() {
417
+ if (!rollbackOwner) return removeOwnedSlot(dir, owner, options, true);
418
+ if (owner.runnerProcessInstanceId || owner.runnerStartedAt) return false;
419
+ const claimed = withSlotClaim(dir, () => {
420
+ const current = matchingOwner(dir, owner);
421
+ if (!current || current.runnerProcessInstanceId || current.runnerStartedAt) return false;
422
+ writePrivateAtomicJson(path.join(dir, "owner.json"), rollbackOwner);
423
+ Object.assign(owner, rollbackOwner);
424
+ return true;
425
+ });
426
+ return claimed.acquired && claimed.value;
427
+ },
428
+ rollbackBeforeRunnerProceed(runnerProcessInstanceId) {
429
+ const claimed = withSlotClaim(dir, () => {
430
+ const current = matchingOwner(dir, owner);
431
+ if (!current) return false;
432
+ const boundToRunner = current.runnerProcessInstanceId === runnerProcessInstanceId;
433
+ const bindingFailedBeforeProceed = current.runnerProcessInstanceId === void 0 && current.runnerStartedAt === void 0 && owner.runnerProcessInstanceId === runnerProcessInstanceId;
434
+ if (!boundToRunner && !bindingFailedBeforeProceed) return false;
435
+ if (rollbackOwner) {
436
+ writePrivateAtomicJson(path.join(dir, "owner.json"), rollbackOwner);
437
+ Object.assign(owner, rollbackOwner);
438
+ return true;
439
+ }
440
+ const releasedDir = path.join(path.dirname(dir), `.${path.basename(dir)}.released-${randomUUID()}`);
441
+ fs.renameSync(dir, releasedDir);
442
+ options.afterSlotRename?.(releasedDir);
443
+ fs.rmSync(releasedDir, { recursive: true, force: true });
444
+ return true;
445
+ });
446
+ return claimed.acquired && claimed.value;
447
+ },
448
+ reconcile(liveWorkflowRunIds) {
449
+ return reconcileActiveAsyncCapacity(owner.ownerSessionId, limit, { ...options, rootDir, liveWorkflowRunIds });
450
+ }
451
+ };
452
+ }
453
+ function acquireActiveAsyncCapacity(input, options = {}) {
454
+ if (input.limit === void 0) return void 0;
455
+ const rootDir = options.rootDir ?? ACTIVE_ASYNC_CAPACITY_DIR;
456
+ const reconciled = reconcileActiveAsyncCapacity(input.sessionId, input.limit, options);
457
+ if (reconciled.used >= input.limit) throw new ActiveAsyncCapacityError(reconciled);
458
+ const poolDir = sessionDir(input.sessionId, rootDir);
459
+ const token = options.token?.() ?? randomUUID();
460
+ for (let slot = 0; slot < input.limit; slot++) {
461
+ const owner = {
462
+ version: 1,
463
+ reservationToken: token,
464
+ ownerSessionId: input.sessionId,
465
+ ownerSessionKey: activeAsyncCapacitySessionKey(input.sessionId),
466
+ slot,
467
+ runId: input.runId,
468
+ generation: 0,
469
+ kind: input.kind,
470
+ asyncDir: input.asyncDir,
471
+ reservedAt: options.now?.() ?? Date.now()
472
+ };
473
+ if (createSlot(poolDir, owner)) return handleFor(owner, input.limit, { ...options, rootDir });
474
+ }
475
+ throw new ActiveAsyncCapacityError(snapshotFor(input.sessionId, input.limit, rootDir));
476
+ }
477
+ function transferActiveAsyncCapacity(input, options = {}) {
478
+ const limit = input.limit ?? 0;
479
+ const rootDir = options.rootDir ?? ACTIVE_ASYNC_CAPACITY_DIR;
480
+ const poolDir = sessionDir(input.sessionId, rootDir);
481
+ for (const dir of occupiedSlots(poolDir)) {
482
+ const source = readOwner(dir);
483
+ if (!source || source.ownerSessionId !== input.sessionId || source.runId !== input.sourceRunId) continue;
484
+ const claimed = withSlotClaim(dir, () => {
485
+ const current = matchingOwner(dir, source);
486
+ const status = readStatus(source.asyncDir);
487
+ if (!current || !status || status.runId !== input.sourceRunId || status.state === "queued" || status.state === "running") {
488
+ throw new Error(`Active async capacity source '${input.sourceRunId}' is not transferable.`);
489
+ }
490
+ const next = {
491
+ ...current,
492
+ runId: input.runId,
493
+ sourceRunId: input.sourceRunId,
494
+ generation: current.generation + 1,
495
+ kind: "runner",
496
+ asyncDir: input.asyncDir,
497
+ reservedAt: options.now?.() ?? Date.now()
498
+ };
499
+ delete next.runnerProcessInstanceId;
500
+ delete next.runnerStartedAt;
501
+ writePrivateAtomicJson(path.join(dir, "owner.json"), next);
502
+ return handleFor(next, limit, { ...options, rootDir }, current);
503
+ });
504
+ if (!claimed.acquired) throw new Error(`Active async capacity transfer is already in progress for run '${input.sourceRunId}'.`);
505
+ return claimed.value;
506
+ }
507
+ return acquireActiveAsyncCapacity({ sessionId: input.sessionId, limit: input.limit, runId: input.runId, kind: "runner", asyncDir: input.asyncDir }, options);
508
+ }
509
+
510
+ // src/extension/config.ts
511
+ import * as fs2 from "node:fs";
512
+ import * as os from "node:os";
513
+ import * as path2 from "node:path";
514
+ import { Key } from "@earendil-works/pi-tui";
515
+ var FAIL_CLOSED_CONFIG_KEYS = ["worktreeProvider", "worktreeBranchPrefix", "modelResponseAliases", "modelExclusions", "checkpointBeforeDeadlineMs", "disabledFeatures", "scheduledRuns", "toolActivation", "authorityPolicy", "permissions", "toolBudget", "runnerLaunchers"];
516
+ var ARTIFACT_DIR_PREFERENCES = /* @__PURE__ */ new Set(["project", "session", "temp"]);
517
+ var FLEET_KEYBINDING_ACTION_SET = new Set(FLEET_KEYBINDING_ACTIONS);
518
+ var KEY_MODIFIERS = /* @__PURE__ */ new Set(["ctrl", "shift", "alt", "super"]);
519
+ var BASE_KEY_IDS = /* @__PURE__ */ new Set([
520
+ ..."abcdefghijklmnopqrstuvwxyz0123456789",
521
+ ...Object.values(Key).flatMap((value) => typeof value === "string" ? [value.toLowerCase()] : [])
522
+ ]);
523
+ var PrunedForkConfigError = class extends Error {
524
+ };
525
+ function validateForkContextConfig(value) {
526
+ if (value === void 0) return;
527
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("config.forkContext must be a JSON object");
528
+ const config = value;
529
+ if (config.mode !== void 0 && config.mode !== "full" && config.mode !== "pruned") {
530
+ throw new Error('config.forkContext.mode must be "full" or "pruned"');
531
+ }
532
+ if (config.model !== void 0 && (typeof config.model !== "string" || !config.model.trim())) {
533
+ throw new PrunedForkConfigError("config.forkContext.model must be a non-empty string");
534
+ }
535
+ if (config.mode === "pruned" && config.model === void 0) {
536
+ throw new PrunedForkConfigError('config.forkContext.model is required when config.forkContext.mode is "pruned"');
537
+ }
538
+ }
539
+ function isValidKeyId(value) {
540
+ if (value !== value.trim()) return false;
541
+ const parts = value.toLowerCase().split("+");
542
+ const base = parts.pop();
543
+ if (!base || !BASE_KEY_IDS.has(base)) return false;
544
+ return parts.length <= KEY_MODIFIERS.size && new Set(parts).size === parts.length && parts.every((modifier) => KEY_MODIFIERS.has(modifier));
545
+ }
546
+ function resolveScheduledStoreRoot(value) {
547
+ const expanded = value.startsWith("~/") ? path2.join(os.homedir(), value.slice(2)) : value;
548
+ if (!path2.isAbsolute(expanded)) throw new Error(`config.scheduledRuns.storeRoot must be an absolute path or "~/...", got ${JSON.stringify(value)}`);
549
+ return path2.normalize(expanded);
550
+ }
551
+ function validateScheduledRunsConfig(value) {
552
+ if (value === void 0) return;
553
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("config.scheduledRuns must be a JSON object");
554
+ const enabled = value.enabled;
555
+ if (enabled !== void 0 && typeof enabled !== "boolean") throw new Error("config.scheduledRuns.enabled must be a boolean");
556
+ const storeRoot = value.storeRoot;
557
+ if (storeRoot === void 0) return;
558
+ if (typeof storeRoot !== "string" || !storeRoot.trim()) throw new Error("config.scheduledRuns.storeRoot must be a non-empty string");
559
+ resolveScheduledStoreRoot(storeRoot);
560
+ }
561
+ function validateFleetKeybindingsConfig(value) {
562
+ if (value === void 0) return;
563
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("config.fleetKeybindings must be a JSON object");
564
+ for (const [action, bindings] of Object.entries(value)) {
565
+ if (!FLEET_KEYBINDING_ACTION_SET.has(action)) throw new Error(`config.fleetKeybindings.${action} is not a supported Fleet action`);
566
+ if (!Array.isArray(bindings) || bindings.length === 0) throw new Error(`config.fleetKeybindings.${action} must be a non-empty array of strings`);
567
+ for (const binding of bindings) {
568
+ if (typeof binding !== "string" || !binding.trim()) throw new Error(`config.fleetKeybindings.${action} entries must be non-empty strings`);
569
+ }
570
+ }
571
+ }
572
+ function validateArtifactConfig(value) {
573
+ if (value === void 0) return;
574
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("config.artifactConfig must be a JSON object");
575
+ const cleanupDays = value.cleanupDays;
576
+ if (cleanupDays !== void 0 && (typeof cleanupDays !== "number" || !Number.isInteger(cleanupDays) || cleanupDays < 0)) {
577
+ throw new Error("config.artifactConfig.cleanupDays must be a non-negative integer");
578
+ }
579
+ }
580
+ function validateCapacityConfig(value) {
581
+ if (value === void 0) return;
582
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("config.capacity must be a JSON object");
583
+ const abandonedSlotReleaseAfterMs = value.abandonedSlotReleaseAfterMs;
584
+ if (abandonedSlotReleaseAfterMs !== void 0 && abandonedSlotReleaseAfterMs !== false && (typeof abandonedSlotReleaseAfterMs !== "number" || !Number.isInteger(abandonedSlotReleaseAfterMs) || abandonedSlotReleaseAfterMs < MIN_ABANDONED_SLOT_RELEASE_AFTER_MS || abandonedSlotReleaseAfterMs > MAX_ABANDONED_SLOT_RELEASE_AFTER_MS)) {
585
+ throw new Error(`config.capacity.abandonedSlotReleaseAfterMs must be false or an integer from ${MIN_ABANDONED_SLOT_RELEASE_AFTER_MS} to ${MAX_ABANDONED_SLOT_RELEASE_AFTER_MS}`);
586
+ }
587
+ }
588
+ function validateRunnerLaunchersConfig(value) {
589
+ if (value === void 0) return;
590
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("config.runnerLaunchers must be a JSON object mapping launcher names to argv arrays");
591
+ for (const [name, argv] of Object.entries(value)) {
592
+ const label = `config.runnerLaunchers[${JSON.stringify(name)}]`;
593
+ if (!RUNNER_LAUNCHER_NAME_PATTERN.test(name)) throw new Error(`${label} has an invalid name; launcher names ${RUNNER_LAUNCHER_NAME_RULE}`);
594
+ if (!Array.isArray(argv) || argv.length === 0 || argv.some((arg) => typeof arg !== "string" || !arg.trim() || arg.includes("\0"))) {
595
+ throw new Error(`${label} must be a non-empty argv array of non-blank strings without NUL characters`);
596
+ }
597
+ }
598
+ }
599
+ function validateOrcaProgressTabsConfig(value) {
600
+ if (value === void 0) return;
601
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("config.orcaProgressTabs must be a JSON object");
602
+ const config = value;
603
+ for (const key of Object.keys(config)) {
604
+ if (key !== "enabled") throw new Error(`config.orcaProgressTabs.${key} is not supported`);
605
+ }
606
+ if (config.enabled !== void 0 && typeof config.enabled !== "boolean") {
607
+ throw new Error("config.orcaProgressTabs.enabled must be a boolean");
608
+ }
609
+ }
610
+ function validateMainWindowRendererConfig(value) {
611
+ if (value === void 0) return;
612
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("config.mainWindowRenderer must be a JSON object");
613
+ const rendererConfig = value;
614
+ if (rendererConfig.horizontalSpacing !== void 0 && (typeof rendererConfig.horizontalSpacing !== "number" || !Number.isInteger(rendererConfig.horizontalSpacing) || rendererConfig.horizontalSpacing < 0 || rendererConfig.horizontalSpacing > 4)) {
615
+ throw new Error("config.mainWindowRenderer.horizontalSpacing must be an integer from 0 to 4");
616
+ }
617
+ if (rendererConfig.compactResultMaxLines !== void 0 && (typeof rendererConfig.compactResultMaxLines !== "number" || !Number.isInteger(rendererConfig.compactResultMaxLines) || rendererConfig.compactResultMaxLines < 1)) {
618
+ throw new Error("config.mainWindowRenderer.compactResultMaxLines must be a positive integer");
619
+ }
620
+ }
621
+ function validateConfig(config) {
622
+ if (config.worktree !== void 0 && typeof config.worktree !== "boolean") {
623
+ throw new Error("config.worktree must be a boolean");
624
+ }
625
+ if (config.worktreeProvider !== void 0 && config.worktreeProvider !== "auto" && config.worktreeProvider !== "native" && config.worktreeProvider !== "worktrunk") {
626
+ throw new Error('config.worktreeProvider must be "auto", "native", or "worktrunk"');
627
+ }
628
+ if (config.worktreeBranchPrefix !== void 0) {
629
+ if (typeof config.worktreeBranchPrefix !== "string") throw new Error("config.worktreeBranchPrefix must be a string");
630
+ normalizeWorktreeBranchPrefix(config.worktreeBranchPrefix);
631
+ }
632
+ if (config.defaultSubagentContext !== void 0 && config.defaultSubagentContext !== "fresh" && config.defaultSubagentContext !== "fork") {
633
+ throw new Error('config.defaultSubagentContext must be "fresh" or "fork"');
634
+ }
635
+ validateForkContextConfig(config.forkContext);
636
+ if (config.checkpointBeforeDeadlineMs !== void 0 && (typeof config.checkpointBeforeDeadlineMs !== "number" || !Number.isInteger(config.checkpointBeforeDeadlineMs) || config.checkpointBeforeDeadlineMs <= 0 || config.checkpointBeforeDeadlineMs > 2147483647)) {
637
+ throw new Error("config.checkpointBeforeDeadlineMs must be a positive integer no larger than 2147483647");
638
+ }
639
+ if (config.foregroundDetachShortcut !== void 0 && (typeof config.foregroundDetachShortcut !== "string" || !isValidKeyId(config.foregroundDetachShortcut))) {
640
+ throw new Error('config.foregroundDetachShortcut must be a valid keybinding string such as "ctrl+b"');
641
+ }
642
+ if (config.artifactDir !== void 0 && !ARTIFACT_DIR_PREFERENCES.has(config.artifactDir)) {
643
+ throw new Error(`config.artifactDir must be "project", "session", or "temp"`);
644
+ }
645
+ if (config.maxActiveAsyncRunsPerSession !== void 0 && (typeof config.maxActiveAsyncRunsPerSession !== "number" || !Number.isInteger(config.maxActiveAsyncRunsPerSession) || config.maxActiveAsyncRunsPerSession < 0)) {
646
+ throw new Error("config.maxActiveAsyncRunsPerSession must be a non-negative integer");
647
+ }
648
+ if (config.resultScanLogging !== void 0 && config.resultScanLogging !== "all" && config.resultScanLogging !== "activity" && config.resultScanLogging !== "off") {
649
+ throw new Error('config.resultScanLogging must be "all", "activity", or "off"');
650
+ }
651
+ if (config.asyncWidgetCollapsed !== void 0 && typeof config.asyncWidgetCollapsed !== "boolean") {
652
+ throw new Error("config.asyncWidgetCollapsed must be a boolean");
653
+ }
654
+ if (config.asyncWidgetLayout !== void 0 && config.asyncWidgetLayout !== "adaptive" && config.asyncWidgetLayout !== "rows") {
655
+ throw new Error('config.asyncWidgetLayout must be "adaptive" or "rows"');
656
+ }
657
+ if (config.programStatus !== void 0 && typeof config.programStatus !== "boolean") {
658
+ throw new Error("config.programStatus must be a boolean");
659
+ }
660
+ if (config.activeRunsPrompt !== void 0 && typeof config.activeRunsPrompt !== "boolean") {
661
+ throw new Error("config.activeRunsPrompt must be a boolean");
662
+ }
663
+ if (config.pinToolDeclarations !== void 0 && typeof config.pinToolDeclarations !== "boolean") {
664
+ throw new Error("config.pinToolDeclarations must be a boolean");
665
+ }
666
+ if (config.enableSlashCommands !== void 0 && typeof config.enableSlashCommands !== "boolean") {
667
+ throw new Error("config.enableSlashCommands must be a boolean");
668
+ }
669
+ if (config.toolActivation !== void 0 && config.toolActivation !== "auto" && config.toolActivation !== "dynamic" && config.toolActivation !== "eager") {
670
+ throw new Error('config.toolActivation must be "auto", "dynamic", or "eager"');
671
+ }
672
+ validateMissionStoreConfig(config.missions);
673
+ validateAuthorityPolicy(config.authorityPolicy);
674
+ validatePermissionConfig(config.permissions);
675
+ validateScheduledRunsConfig(config.scheduledRuns);
676
+ validateDisabledFeatures(config.disabledFeatures);
677
+ validateFleetKeybindingsConfig(config.fleetKeybindings);
678
+ validateArtifactConfig(config.artifactConfig);
679
+ validateCapacityConfig(config.capacity);
680
+ if (config.modelExclusions !== void 0) throw new Error("config.modelExclusions was removed; model failures are no longer persisted or used for automatic switching");
681
+ validateModelResponseAliases(config.modelResponseAliases);
682
+ validateMainWindowRendererConfig(config.mainWindowRenderer);
683
+ validateOrcaProgressTabsConfig(config.orcaProgressTabs);
684
+ validateRunnerLaunchersConfig(config.runnerLaunchers);
685
+ }
686
+ function getConfigPath() {
687
+ return path2.join(getAgentDir(), "extensions", "subagent", "config.json");
688
+ }
689
+ function readConfigForUpdate(configPath = getConfigPath()) {
690
+ if (!fs2.existsSync(configPath)) return {};
691
+ const parsed = JSON.parse(fs2.readFileSync(configPath, "utf-8"));
692
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
693
+ throw new Error(`Subagent config at '${configPath}' must be a JSON object`);
694
+ }
695
+ validateConfig(parsed);
696
+ return parsed;
697
+ }
698
+ function resolveAsyncByDefault(config) {
699
+ return config.asyncByDefault !== false;
700
+ }
701
+ function loadConfig() {
702
+ const configPath = getConfigPath();
703
+ try {
704
+ return readConfigForUpdate(configPath);
705
+ } catch (error) {
706
+ if (error instanceof PrunedForkConfigError) throw error;
707
+ try {
708
+ const raw = JSON.parse(fs2.readFileSync(configPath, "utf-8"));
709
+ if (raw && typeof raw === "object" && !Array.isArray(raw) && FAIL_CLOSED_CONFIG_KEYS.some((key) => Object.hasOwn(raw, key))) throw error;
710
+ } catch (readError) {
711
+ if (readError === error) throw error;
712
+ }
713
+ console.error(`Failed to load subagent config from '${configPath}':`, error);
714
+ }
715
+ return {};
716
+ }
717
+
718
+ export {
719
+ resolveDisabledFeatureSurface,
720
+ disabledFeatureUseError,
721
+ disabledFeatureNotice,
722
+ ActiveAsyncCapacityError,
723
+ resolveMaxActiveAsyncRunsPerSession,
724
+ resolveAbandonedSlotReleaseAfterMs,
725
+ inspectActiveAsyncCapacityOwner,
726
+ getActiveAsyncCapacitySnapshot,
727
+ acquireActiveAsyncCapacity,
728
+ transferActiveAsyncCapacity,
729
+ resolveScheduledStoreRoot,
730
+ resolveAsyncByDefault,
731
+ loadConfig
732
+ };