pi-subagents 0.35.1 → 0.37.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 (89) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/README.md +224 -31
  3. package/agents/advisor.md +73 -0
  4. package/agents/delegate.md +2 -0
  5. package/agents/worker.md +2 -0
  6. package/package.json +11 -13
  7. package/skills/pi-subagents/SKILL.md +42 -13
  8. package/src/agents/agent-management.ts +7 -1
  9. package/src/agents/agents.ts +121 -17
  10. package/src/api/capability-ceiling.ts +17 -0
  11. package/src/api/delegation.ts +127 -0
  12. package/src/api/preflight.ts +399 -0
  13. package/src/extension/config.ts +7 -1
  14. package/src/extension/index.ts +52 -38
  15. package/src/extension/rpc.ts +31 -2
  16. package/src/extension/schemas.ts +29 -3
  17. package/src/extension/tool-description.ts +4 -2
  18. package/src/intercom/intercom-bridge.ts +1 -1
  19. package/src/intercom/native-supervisor-channel.ts +45 -6
  20. package/src/intercom/result-intercom.ts +7 -0
  21. package/src/runs/background/async-execution.ts +158 -17
  22. package/src/runs/background/async-job-tracker.ts +4 -0
  23. package/src/runs/background/async-resume.ts +47 -8
  24. package/src/runs/background/async-status.ts +94 -3
  25. package/src/runs/background/chain-append.ts +2 -0
  26. package/src/runs/background/completion-batcher.ts +6 -4
  27. package/src/runs/background/completion-dedupe.ts +2 -11
  28. package/src/runs/background/fleet-view.ts +9 -4
  29. package/src/runs/background/notify.ts +132 -120
  30. package/src/runs/background/process-terminal.ts +280 -0
  31. package/src/runs/background/result-watcher.ts +138 -78
  32. package/src/runs/background/run-status.ts +10 -2
  33. package/src/runs/background/scheduled-runs.ts +6 -1
  34. package/src/runs/background/stale-run-reconciler.ts +6 -0
  35. package/src/runs/background/subagent-runner.ts +406 -54
  36. package/src/runs/background/subagent-wait.ts +130 -4
  37. package/src/runs/background/wait-tool.ts +2 -2
  38. package/src/runs/foreground/chain-execution.ts +181 -111
  39. package/src/runs/foreground/execution.ts +147 -47
  40. package/src/runs/foreground/foreground-control.ts +90 -0
  41. package/src/runs/foreground/subagent-executor.ts +426 -165
  42. package/src/runs/shared/acceptance.ts +94 -41
  43. package/src/runs/shared/agent-contract.ts +38 -0
  44. package/src/runs/shared/capability-ceiling.ts +177 -0
  45. package/src/runs/shared/child-protocol.ts +1 -1
  46. package/src/runs/shared/completion-guard.ts +36 -5
  47. package/src/runs/shared/context-mode.ts +44 -0
  48. package/src/runs/shared/dynamic-fanout.ts +5 -5
  49. package/src/runs/shared/long-running-guard.ts +4 -0
  50. package/src/runs/shared/mcp-direct-tool-allowlist.ts +12 -6
  51. package/src/runs/shared/nested-events.ts +35 -3
  52. package/src/runs/shared/parallel-handoff.ts +154 -0
  53. package/src/runs/shared/parallel-utils.ts +11 -0
  54. package/src/runs/shared/pi-args.ts +148 -56
  55. package/src/runs/shared/run-history.ts +90 -5
  56. package/src/runs/shared/session-lease.ts +25 -5
  57. package/src/runs/shared/structured-output.ts +112 -7
  58. package/src/runs/shared/subagent-control.ts +4 -0
  59. package/src/runs/shared/subagent-prompt-runtime.ts +31 -19
  60. package/src/runs/shared/task-intent.ts +10 -5
  61. package/src/runs/shared/tool-availability.ts +21 -3
  62. package/src/runs/shared/tool-budget.ts +11 -5
  63. package/src/runs/shared/turn-budget.ts +2 -1
  64. package/src/runs/shared/worktree.ts +63 -14
  65. package/src/shared/accessible-dir.ts +25 -0
  66. package/src/shared/artifacts.ts +37 -7
  67. package/src/shared/atomic-json.ts +14 -42
  68. package/src/shared/child-transcript.ts +52 -0
  69. package/src/shared/file-system-retry.ts +47 -0
  70. package/src/shared/launch-contract.ts +123 -0
  71. package/src/shared/settings.ts +9 -1
  72. package/src/shared/types.ts +314 -28
  73. package/src/shared/utils.ts +17 -42
  74. package/src/slash/delegation-adapters.ts +153 -6
  75. package/src/slash/delegation-json.ts +108 -0
  76. package/src/slash/delegation-request.ts +182 -36
  77. package/src/slash/prompt-template-bridge.ts +222 -37
  78. package/src/slash/selector.ts +147 -0
  79. package/src/slash/slash-commands.ts +15 -6
  80. package/src/slash/slash-live-state.ts +2 -2
  81. package/src/slash/subagents-admin.ts +42 -42
  82. package/src/tui/fleet-status.ts +362 -0
  83. package/src/tui/fleet-transcript.ts +472 -0
  84. package/src/tui/fleet.ts +318 -59
  85. package/src/tui/render.ts +17 -15
  86. package/src/watchdog/change-signature.ts +105 -12
  87. package/src/watchdog/review.ts +7 -2
  88. package/src/watchdog/runtime.ts +5 -3
  89. package/src/slash/subagents-editor.ts +0 -86
@@ -1,12 +1,16 @@
1
1
  import {
2
2
  SUBAGENT_DELEGATION_CANCEL_EVENT,
3
3
  SUBAGENT_DELEGATION_PROTOCOL_VERSION,
4
+ SUBAGENT_DELEGATION_V2_PROTOCOL_VERSION,
4
5
  SUBAGENT_DELEGATION_REQUEST_EVENT,
5
6
  SUBAGENT_DELEGATION_RESPONSE_EVENT,
6
7
  SUBAGENT_DELEGATION_STARTED_EVENT,
7
8
  SUBAGENT_DELEGATION_UPDATE_EVENT,
8
9
  type SubagentDelegationRequest,
9
10
  type SubagentDelegationResponse,
11
+ type SubagentDelegationV2InvalidResponse,
12
+ type SubagentDelegationV2Request,
13
+ type SubagentDelegationV2Response,
10
14
  } from "../api/delegation.ts";
11
15
  import { parseSubagentDelegationRequest } from "./delegation-request.ts";
12
16
  import {
@@ -17,6 +21,9 @@ import {
17
21
  toSubagentDelegationExecutionParams,
18
22
  toSubagentDelegationResponse,
19
23
  toSubagentDelegationUpdate,
24
+ toSubagentDelegationV2ExecutionParams,
25
+ toSubagentDelegationV2Response,
26
+ toSubagentDelegationV2Update,
20
27
  type DelegatedSubagentExecutionParams,
21
28
  type PromptTemplateBridgeResult,
22
29
  type PromptTemplateDelegationRequest,
@@ -44,6 +51,17 @@ interface PromptTemplateBridgeOptions<Ctx extends { cwd?: string }> {
44
51
  ctx: Ctx,
45
52
  onUpdate: (result: PromptTemplateBridgeResult) => void,
46
53
  ) => Promise<PromptTemplateBridgeResult>;
54
+ /**
55
+ * Concurrent-safe executor for strict versioned delegation requests.
56
+ * Non-versioned prompt-template requests retain the ordinary single-dispatch guard.
57
+ */
58
+ executeVersioned?: (
59
+ requestId: string,
60
+ params: DelegatedSubagentExecutionParams,
61
+ signal: AbortSignal,
62
+ ctx: Ctx,
63
+ onUpdate: (result: PromptTemplateBridgeResult) => void,
64
+ ) => Promise<PromptTemplateBridgeResult>;
47
65
  }
48
66
 
49
67
  export function registerPromptTemplateDelegationBridge<Ctx extends { cwd?: string }>(
@@ -52,10 +70,17 @@ export function registerPromptTemplateDelegationBridge<Ctx extends { cwd?: strin
52
70
  cancelAll: () => void;
53
71
  dispose: () => void;
54
72
  } {
73
+ // Legacy and V1 retain requestId-only correlation. V2 attempts are isolated by
74
+ // their complete wire identity, while logical-node ownership is tracked separately.
55
75
  const controllers = new Map<string, AbortController>();
56
76
  const pendingCancels = new Map<string, true>();
77
+ const v2Controllers = new Map<string, AbortController>();
78
+ const pendingV2Cancels = new Map<string, true>();
79
+ const activeV2Nodes = new Map<string, { attemptKey: string; controller: AbortController }>();
80
+ const settledV2Attempts = new Map<string, true>();
57
81
  const subscriptions: Array<() => void> = [];
58
82
  let disposed = false;
83
+ let v2IdentitySaturated = false;
59
84
 
60
85
  const subscribe = (event: string, handler: (data: unknown) => void): void => {
61
86
  const unsubscribe = options.events.on(event, handler);
@@ -63,25 +88,63 @@ export function registerPromptTemplateDelegationBridge<Ctx extends { cwd?: strin
63
88
  };
64
89
  const ownsRequest = (requestId: string, controller: AbortController): boolean =>
65
90
  !disposed && controllers.get(requestId) === controller;
66
- const rememberPendingCancel = (requestId: string): void => {
67
- pendingCancels.delete(requestId);
68
- pendingCancels.set(requestId, true);
69
- while (pendingCancels.size > 256) {
70
- const oldest = pendingCancels.keys().next().value;
91
+ const ownsV2Attempt = (attemptKey: string, controller: AbortController): boolean =>
92
+ !disposed && v2Controllers.get(attemptKey) === controller;
93
+ const boundedRemember = (map: Map<string, true>, key: string): void => {
94
+ map.delete(key);
95
+ map.set(key, true);
96
+ while (map.size > 256) {
97
+ const oldest = map.keys().next().value;
71
98
  if (typeof oldest !== "string") break;
72
- pendingCancels.delete(oldest);
99
+ map.delete(oldest);
100
+ }
101
+ };
102
+ const rememberV2Identity = (map: Map<string, true>, key: string): void => {
103
+ if (map.has(key) || v2IdentitySaturated) return;
104
+ if (map.size >= 8_192) {
105
+ v2IdentitySaturated = true;
106
+ return;
107
+ }
108
+ map.set(key, true);
109
+ if (map.size === 8_192) {
110
+ // V2 identity facts are security state, not an LRU cache. Saturate as
111
+ // soon as the bounded history fills so no later untracked attempt can
112
+ // start, rather than evicting a cancellation or settled-attempt fact.
113
+ v2IdentitySaturated = true;
73
114
  }
74
115
  };
116
+ const v2NodeKey = (ownerRunId: string, nodeId: string): string => JSON.stringify([ownerRunId, nodeId]);
117
+ const v2AttemptKey = (requestId: string, ownerRunId: string, nodeId: string): string => JSON.stringify([requestId, ownerRunId, nodeId]);
118
+ const rememberPendingCancel = (requestId: string): void => {
119
+ boundedRemember(pendingCancels, requestId);
120
+ };
121
+ const emitV2Terminal = (attemptKey: string, payload: SubagentDelegationV2Response): void => {
122
+ if (disposed || settledV2Attempts.has(attemptKey)) return;
123
+ rememberV2Identity(settledV2Attempts, attemptKey);
124
+ options.events.emit(SUBAGENT_DELEGATION_RESPONSE_EVENT, payload);
125
+ };
75
126
 
76
127
  subscribe(PROMPT_TEMPLATE_SUBAGENT_CANCEL_EVENT, (data) => {
77
128
  if (!data || typeof data !== "object" || Array.isArray(data)) return;
78
- if (Object.hasOwn(data, "version")) {
79
- const value = data as Record<string, unknown>;
129
+ const value = data as Record<string, unknown>;
130
+ const requestId = value.requestId;
131
+ if (typeof requestId !== "string" || !requestId.trim() || requestId.length > 256 || /[\r\n]/.test(requestId)) return;
132
+ if (Object.hasOwn(value, "version")) {
133
+ if (value.version === SUBAGENT_DELEGATION_V2_PROTOCOL_VERSION) {
134
+ if (Object.keys(value).some((key) => key !== "version" && key !== "requestId" && key !== "ownerRunId" && key !== "nodeId")) return;
135
+ const ownerRunId = value.ownerRunId;
136
+ const nodeId = value.nodeId;
137
+ if (typeof ownerRunId !== "string" || !ownerRunId.trim() || ownerRunId.length > 256 || /[\r\n]/.test(ownerRunId)) return;
138
+ if (typeof nodeId !== "string" || !nodeId.trim() || nodeId.length > 256 || /[\r\n]/.test(nodeId)) return;
139
+ const attemptKey = v2AttemptKey(requestId, ownerRunId, nodeId);
140
+ const controller = v2Controllers.get(attemptKey);
141
+ if (controller) controller.abort();
142
+ else rememberV2Identity(pendingV2Cancels, attemptKey);
143
+ return;
144
+ }
80
145
  if (value.version !== SUBAGENT_DELEGATION_PROTOCOL_VERSION) return;
81
146
  if (Object.keys(value).some((key) => key !== "version" && key !== "requestId")) return;
82
147
  }
83
- const requestId = (data as { requestId?: unknown }).requestId;
84
- if (typeof requestId !== "string" || !requestId.trim() || requestId.length > 256 || /[\r\n]/.test(requestId)) return;
85
148
  const controller = controllers.get(requestId);
86
149
  if (controller) {
87
150
  controller.abort();
@@ -94,25 +157,50 @@ export function registerPromptTemplateDelegationBridge<Ctx extends { cwd?: strin
94
157
  const isVersioned = !!data && typeof data === "object" && Object.hasOwn(data, "version");
95
158
  let requestId: string;
96
159
  let params: DelegatedSubagentExecutionParams;
97
- let versionedRequest: SubagentDelegationRequest | undefined;
160
+ let versionedRequest: SubagentDelegationRequest | SubagentDelegationV2Request | undefined;
161
+ let v2Request: SubagentDelegationV2Request | undefined;
162
+ let v2Key: string | undefined;
98
163
  let legacyRequest: PromptTemplateDelegationRequest | undefined;
99
164
 
100
165
  if (isVersioned) {
101
166
  const parsed = parseSubagentDelegationRequest(data);
102
167
  if (parsed.ok === false) {
103
- if (!disposed && parsed.requestId && !controllers.has(parsed.requestId)) {
104
- options.events.emit(SUBAGENT_DELEGATION_RESPONSE_EVENT, {
105
- version: SUBAGENT_DELEGATION_PROTOCOL_VERSION,
106
- requestId: parsed.requestId,
107
- status: "invalid_request",
108
- error: parsed.error,
109
- } satisfies SubagentDelegationResponse);
168
+ if (!disposed && parsed.requestId) {
169
+ if (parsed.version === SUBAGENT_DELEGATION_V2_PROTOCOL_VERSION) {
170
+ const payload = {
171
+ version: SUBAGENT_DELEGATION_V2_PROTOCOL_VERSION,
172
+ requestId: parsed.requestId,
173
+ ...(parsed.ownerRunId ? { ownerRunId: parsed.ownerRunId } : {}),
174
+ ...(parsed.nodeId ? { nodeId: parsed.nodeId } : {}),
175
+ status: "invalid_request",
176
+ error: parsed.error,
177
+ } satisfies SubagentDelegationV2InvalidResponse;
178
+ if (parsed.ownerRunId && parsed.nodeId) {
179
+ const attemptKey = v2AttemptKey(parsed.requestId, parsed.ownerRunId, parsed.nodeId);
180
+ if (!v2Controllers.has(attemptKey)) emitV2Terminal(attemptKey, payload);
181
+ } else {
182
+ options.events.emit(SUBAGENT_DELEGATION_RESPONSE_EVENT, payload);
183
+ }
184
+ } else if (!controllers.has(parsed.requestId)) {
185
+ options.events.emit(SUBAGENT_DELEGATION_RESPONSE_EVENT, {
186
+ version: SUBAGENT_DELEGATION_PROTOCOL_VERSION,
187
+ requestId: parsed.requestId,
188
+ status: "invalid_request",
189
+ error: parsed.error,
190
+ } satisfies SubagentDelegationResponse);
191
+ }
110
192
  }
111
193
  return;
112
194
  }
113
195
  versionedRequest = parsed.request;
114
196
  requestId = parsed.request.requestId;
115
- params = toSubagentDelegationExecutionParams(parsed.request);
197
+ if (parsed.request.version === SUBAGENT_DELEGATION_V2_PROTOCOL_VERSION) {
198
+ v2Request = parsed.request;
199
+ v2Key = v2AttemptKey(requestId, parsed.request.ownerRunId, parsed.request.nodeId);
200
+ params = toSubagentDelegationV2ExecutionParams(parsed.request);
201
+ } else {
202
+ params = toSubagentDelegationExecutionParams(parsed.request);
203
+ }
116
204
  } else {
117
205
  legacyRequest = parsePromptTemplateRequest(data);
118
206
  if (!legacyRequest) return;
@@ -120,12 +208,56 @@ export function registerPromptTemplateDelegationBridge<Ctx extends { cwd?: strin
120
208
  params = toLegacyExecutionParams(legacyRequest);
121
209
  }
122
210
 
123
- // The first request owns its correlation ID until it settles. A duplicate
124
- // cannot receive a terminal response without stealing the original caller's.
125
- if (controllers.has(requestId)) return;
211
+ // Legacy and V1 keep requestId-only ownership. V2 retransmission and terminal
212
+ // suppression use the full tuple, independently from logical-node ownership.
213
+ if (!v2Request && controllers.has(requestId)) return;
214
+ if (v2Request && v2Key) {
215
+ if (v2Controllers.has(v2Key) || settledV2Attempts.has(v2Key)) return;
216
+ if (pendingV2Cancels.delete(v2Key)) {
217
+ emitV2Terminal(v2Key, {
218
+ version: SUBAGENT_DELEGATION_V2_PROTOCOL_VERSION,
219
+ requestId,
220
+ ownerRunId: v2Request.ownerRunId,
221
+ nodeId: v2Request.nodeId,
222
+ status: "cancelled",
223
+ });
224
+ return;
225
+ }
226
+ if (v2IdentitySaturated) {
227
+ options.events.emit(SUBAGENT_DELEGATION_RESPONSE_EVENT, {
228
+ version: SUBAGENT_DELEGATION_V2_PROTOCOL_VERSION,
229
+ requestId,
230
+ ownerRunId: v2Request.ownerRunId,
231
+ nodeId: v2Request.nodeId,
232
+ status: "unavailable_context",
233
+ error: "Delegation v2 identity capacity is exhausted for this extension context.",
234
+ } satisfies SubagentDelegationV2Response);
235
+ return;
236
+ }
237
+ const active = activeV2Nodes.get(v2NodeKey(v2Request.ownerRunId, v2Request.nodeId));
238
+ if (active) {
239
+ emitV2Terminal(v2Key, {
240
+ version: SUBAGENT_DELEGATION_V2_PROTOCOL_VERSION,
241
+ requestId,
242
+ ownerRunId: v2Request.ownerRunId,
243
+ nodeId: v2Request.nodeId,
244
+ status: "duplicate_node",
245
+ });
246
+ return;
247
+ }
248
+ }
126
249
  const ctx = options.getContext();
127
250
  if (!ctx) {
128
- if (versionedRequest) {
251
+ if (v2Request && v2Key) {
252
+ emitV2Terminal(v2Key, {
253
+ version: SUBAGENT_DELEGATION_V2_PROTOCOL_VERSION,
254
+ requestId,
255
+ ownerRunId: v2Request.ownerRunId,
256
+ nodeId: v2Request.nodeId,
257
+ status: "unavailable_context",
258
+ error: "No active extension context for delegated subagent execution.",
259
+ });
260
+ } else if (versionedRequest) {
129
261
  options.events.emit(SUBAGENT_DELEGATION_RESPONSE_EVENT, {
130
262
  version: SUBAGENT_DELEGATION_PROTOCOL_VERSION,
131
263
  requestId,
@@ -144,10 +276,24 @@ export function registerPromptTemplateDelegationBridge<Ctx extends { cwd?: strin
144
276
  }
145
277
 
146
278
  const controller = new AbortController();
147
- controllers.set(requestId, controller);
148
- if (pendingCancels.delete(requestId)) controller.abort();
279
+ if (v2Request && v2Key) {
280
+ v2Controllers.set(v2Key, controller);
281
+ activeV2Nodes.set(v2NodeKey(v2Request.ownerRunId, v2Request.nodeId), { attemptKey: v2Key, controller });
282
+ } else {
283
+ controllers.set(requestId, controller);
284
+ if (pendingCancels.delete(requestId)) controller.abort();
285
+ }
149
286
  if (controller.signal.aborted) {
150
- if (versionedRequest) {
287
+ if (v2Request && v2Key) {
288
+ emitV2Terminal(v2Key, {
289
+ version: SUBAGENT_DELEGATION_V2_PROTOCOL_VERSION,
290
+ requestId,
291
+ ownerRunId: v2Request.ownerRunId,
292
+ nodeId: v2Request.nodeId,
293
+ status: "cancelled",
294
+ });
295
+ activeV2Nodes.delete(v2NodeKey(v2Request.ownerRunId, v2Request.nodeId));
296
+ } else if (versionedRequest) {
151
297
  options.events.emit(SUBAGENT_DELEGATION_RESPONSE_EVENT, {
152
298
  version: SUBAGENT_DELEGATION_PROTOCOL_VERSION,
153
299
  requestId,
@@ -161,25 +307,36 @@ export function registerPromptTemplateDelegationBridge<Ctx extends { cwd?: strin
161
307
  errorText: "Delegated prompt cancelled.",
162
308
  } satisfies PromptTemplateDelegationResponse);
163
309
  }
164
- controllers.delete(requestId);
310
+ if (v2Key) v2Controllers.delete(v2Key);
311
+ else controllers.delete(requestId);
165
312
  return;
166
313
  }
167
314
 
168
315
  options.events.emit(
169
316
  versionedRequest ? SUBAGENT_DELEGATION_STARTED_EVENT : PROMPT_TEMPLATE_SUBAGENT_STARTED_EVENT,
170
- versionedRequest
171
- ? { version: SUBAGENT_DELEGATION_PROTOCOL_VERSION, requestId }
172
- : { requestId },
317
+ v2Request
318
+ ? { version: SUBAGENT_DELEGATION_V2_PROTOCOL_VERSION, requestId, ownerRunId: v2Request.ownerRunId, nodeId: v2Request.nodeId }
319
+ : versionedRequest
320
+ ? { version: SUBAGENT_DELEGATION_PROTOCOL_VERSION, requestId }
321
+ : { requestId },
173
322
  );
174
323
 
175
324
  try {
176
- const result = await options.execute(
325
+ const executeRequest = versionedRequest && options.executeVersioned
326
+ ? options.executeVersioned
327
+ : options.execute;
328
+ const result = await executeRequest(
177
329
  requestId,
178
330
  params,
179
331
  controller.signal,
180
332
  ctx,
181
333
  (update) => {
182
- if (!ownsRequest(requestId, controller)) return;
334
+ if (v2Key ? !ownsV2Attempt(v2Key, controller) : !ownsRequest(requestId, controller)) return;
335
+ if (v2Request) {
336
+ const payload = toSubagentDelegationV2Update(v2Request, update);
337
+ if (payload) options.events.emit(SUBAGENT_DELEGATION_UPDATE_EVENT, payload);
338
+ return;
339
+ }
183
340
  if (versionedRequest) {
184
341
  const payload = toSubagentDelegationUpdate(requestId, update);
185
342
  if (payload) options.events.emit(SUBAGENT_DELEGATION_UPDATE_EVENT, payload);
@@ -189,8 +346,10 @@ export function registerPromptTemplateDelegationBridge<Ctx extends { cwd?: strin
189
346
  if (payload) options.events.emit(PROMPT_TEMPLATE_SUBAGENT_UPDATE_EVENT, payload);
190
347
  },
191
348
  );
192
- if (!ownsRequest(requestId, controller)) return;
193
- if (versionedRequest) {
349
+ if (v2Key ? !ownsV2Attempt(v2Key, controller) : !ownsRequest(requestId, controller)) return;
350
+ if (v2Request && v2Key) {
351
+ emitV2Terminal(v2Key, toSubagentDelegationV2Response(v2Request, result, controller.signal.aborted));
352
+ } else if (versionedRequest) {
194
353
  options.events.emit(
195
354
  SUBAGENT_DELEGATION_RESPONSE_EVENT,
196
355
  toSubagentDelegationResponse(requestId, result, controller.signal.aborted),
@@ -204,8 +363,17 @@ export function registerPromptTemplateDelegationBridge<Ctx extends { cwd?: strin
204
363
  );
205
364
  }
206
365
  } catch (error) {
207
- if (!ownsRequest(requestId, controller)) return;
208
- if (versionedRequest) {
366
+ if (v2Key ? !ownsV2Attempt(v2Key, controller) : !ownsRequest(requestId, controller)) return;
367
+ if (v2Request && v2Key) {
368
+ emitV2Terminal(v2Key, {
369
+ version: SUBAGENT_DELEGATION_V2_PROTOCOL_VERSION,
370
+ requestId,
371
+ ownerRunId: v2Request.ownerRunId,
372
+ nodeId: v2Request.nodeId,
373
+ status: controller.signal.aborted ? "cancelled" : "failed",
374
+ ...(controller.signal.aborted ? {} : { error: error instanceof Error ? error.message : String(error) }),
375
+ });
376
+ } else if (versionedRequest) {
209
377
  options.events.emit(SUBAGENT_DELEGATION_RESPONSE_EVENT, {
210
378
  version: SUBAGENT_DELEGATION_PROTOCOL_VERSION,
211
379
  requestId,
@@ -221,23 +389,40 @@ export function registerPromptTemplateDelegationBridge<Ctx extends { cwd?: strin
221
389
  } satisfies PromptTemplateDelegationResponse);
222
390
  }
223
391
  } finally {
224
- if (controllers.get(requestId) === controller) controllers.delete(requestId);
392
+ if (v2Key) {
393
+ if (v2Controllers.get(v2Key) === controller) v2Controllers.delete(v2Key);
394
+ } else if (controllers.get(requestId) === controller) controllers.delete(requestId);
395
+ if (v2Request) {
396
+ const key = v2NodeKey(v2Request.ownerRunId, v2Request.nodeId);
397
+ if (activeV2Nodes.get(key)?.controller === controller) activeV2Nodes.delete(key);
398
+ }
225
399
  }
226
400
  });
227
401
 
228
402
  return {
229
403
  cancelAll: () => {
230
404
  for (const controller of controllers.values()) controller.abort();
405
+ for (const controller of v2Controllers.values()) controller.abort();
231
406
  controllers.clear();
407
+ v2Controllers.clear();
232
408
  pendingCancels.clear();
409
+ pendingV2Cancels.clear();
410
+ activeV2Nodes.clear();
411
+ settledV2Attempts.clear();
412
+ v2IdentitySaturated = false;
233
413
  },
234
414
  dispose: () => {
235
415
  disposed = true;
236
416
  for (const controller of controllers.values()) controller.abort();
417
+ for (const controller of v2Controllers.values()) controller.abort();
237
418
  controllers.clear();
419
+ v2Controllers.clear();
238
420
  for (const unsubscribe of subscriptions) unsubscribe();
239
421
  subscriptions.length = 0;
240
422
  pendingCancels.clear();
423
+ pendingV2Cancels.clear();
424
+ activeV2Nodes.clear();
425
+ settledV2Attempts.clear();
241
426
  },
242
427
  };
243
428
  }
@@ -0,0 +1,147 @@
1
+ import { DynamicBorder, keyHint, rawKeyHint, type Theme } from "@earendil-works/pi-coding-agent";
2
+ import { Container, fuzzyFilter, Input, type KeybindingsManager, Spacer, Text, type TUI } from "@earendil-works/pi-tui";
3
+
4
+ /** A single selectable row. `value` is returned on confirm; `label` is the primary text. */
5
+ export interface SelectorItem {
6
+ value: string;
7
+ label: string;
8
+ /** Optional dimmed badge shown after the label (e.g. a provider name). */
9
+ badge?: string;
10
+ /** Marks the current selection (rendered with a ✓ checkmark). */
11
+ current?: boolean;
12
+ }
13
+
14
+ export interface SelectorResult {
15
+ confirmed: boolean;
16
+ value?: string;
17
+ }
18
+
19
+ export interface SelectorOptions {
20
+ title: string;
21
+ /** Optional dimmed hint line under the title (e.g. the current value). */
22
+ subtitle?: string;
23
+ items: SelectorItem[];
24
+ done: (result: SelectorResult) => void;
25
+ }
26
+
27
+ const MAX_VISIBLE = 10;
28
+
29
+ /**
30
+ * A single-select list with a search field and a bounded scroll window that keeps the
31
+ * highlighted row on screen with an `(n/total)` indicator, so the selection never scrolls
32
+ * out of view when the option list is long. Composed from pi's own TUI primitives so it
33
+ * matches the built-in `/model` picker.
34
+ *
35
+ * Colors come from the `theme` passed to the factory: the module-level `theme` singleton
36
+ * is undefined under the extension's jiti module cache.
37
+ */
38
+ export class SelectorComponent extends Container {
39
+ private readonly tui: TUI;
40
+ private readonly theme: Theme;
41
+ private readonly keybindings: KeybindingsManager;
42
+ private readonly items: SelectorItem[];
43
+ private readonly done: (result: SelectorResult) => void;
44
+
45
+ private readonly searchInput: Input;
46
+ private readonly listContainer: Container;
47
+ private filtered: SelectorItem[];
48
+ private selectedIndex = 0;
49
+
50
+ constructor(tui: TUI, theme: Theme, keybindings: KeybindingsManager, options: SelectorOptions) {
51
+ super();
52
+ this.tui = tui;
53
+ this.theme = theme;
54
+ this.keybindings = keybindings;
55
+ this.items = options.items;
56
+ this.done = options.done;
57
+ this.filtered = [...this.items];
58
+
59
+ const border = () => new DynamicBorder((str) => theme.fg("border", str));
60
+ this.addChild(border());
61
+ this.addChild(new Spacer(1));
62
+ this.addChild(new Text(theme.fg("accent", theme.bold(options.title)), 0, 0));
63
+ if (options.subtitle) this.addChild(new Text(theme.fg("muted", options.subtitle), 0, 0));
64
+ this.addChild(new Spacer(1));
65
+
66
+ this.searchInput = new Input();
67
+ this.searchInput.focused = true;
68
+ this.searchInput.onSubmit = () => this.confirmSelection();
69
+ this.addChild(this.searchInput);
70
+ this.addChild(new Spacer(1));
71
+
72
+ this.listContainer = new Container();
73
+ this.addChild(this.listContainer);
74
+ this.addChild(new Spacer(1));
75
+
76
+ this.addChild(new Text(rawKeyHint("↑↓", "navigate") + " " + keyHint("tui.select.confirm", "select") + " " + keyHint("tui.select.cancel", "cancel"), 0, 0));
77
+ this.addChild(new Spacer(1));
78
+ this.addChild(border());
79
+
80
+ const currentIndex = this.filtered.findIndex((item) => item.current);
81
+ if (currentIndex >= 0) this.selectedIndex = currentIndex;
82
+ this.updateList();
83
+ }
84
+
85
+ private applyFilter(query: string): void {
86
+ const previous = this.filtered[this.selectedIndex]?.value;
87
+ this.filtered = query
88
+ ? fuzzyFilter(this.items, query, (item) => `${item.label} ${item.value} ${item.badge ?? ""}`)
89
+ : [...this.items];
90
+ const keepIndex = this.filtered.findIndex((item) => item.value === previous);
91
+ this.selectedIndex = keepIndex >= 0 ? keepIndex : Math.min(this.selectedIndex, Math.max(0, this.filtered.length - 1));
92
+ this.updateList();
93
+ }
94
+
95
+ private updateList(): void {
96
+ const th = this.theme;
97
+ this.listContainer.clear();
98
+ const startIndex = Math.max(0, Math.min(this.selectedIndex - Math.floor(MAX_VISIBLE / 2), this.filtered.length - MAX_VISIBLE));
99
+ const endIndex = Math.min(startIndex + MAX_VISIBLE, this.filtered.length);
100
+
101
+ for (let i = startIndex; i < endIndex; i++) {
102
+ const item = this.filtered[i]!;
103
+ const isSelected = i === this.selectedIndex;
104
+ const badge = item.badge ? ` ${th.fg("muted", `[${item.badge}]`)}` : "";
105
+ const checkmark = item.current ? th.fg("success", " ✓") : "";
106
+ const line = isSelected
107
+ ? `${th.fg("accent", "→ ")}${th.fg("accent", item.label)}${badge}${checkmark}`
108
+ : ` ${item.label}${badge}${checkmark}`;
109
+ this.listContainer.addChild(new Text(line, 0, 0));
110
+ }
111
+
112
+ if (startIndex > 0 || endIndex < this.filtered.length) {
113
+ this.listContainer.addChild(new Text(th.fg("muted", ` (${this.selectedIndex + 1}/${this.filtered.length})`), 0, 0));
114
+ }
115
+ if (this.filtered.length === 0) {
116
+ this.listContainer.addChild(new Text(th.fg("muted", " No matching entries"), 0, 0));
117
+ }
118
+ }
119
+
120
+ private confirmSelection(): void {
121
+ const selected = this.filtered[this.selectedIndex];
122
+ this.done(selected ? { confirmed: true, value: selected.value } : { confirmed: false });
123
+ }
124
+
125
+ handleInput(keyData: string): void {
126
+ const kb = this.keybindings;
127
+ if (kb.matches(keyData, "tui.select.up")) {
128
+ if (this.filtered.length === 0) return;
129
+ this.selectedIndex = this.selectedIndex === 0 ? this.filtered.length - 1 : this.selectedIndex - 1;
130
+ this.updateList();
131
+ this.tui.requestRender();
132
+ } else if (kb.matches(keyData, "tui.select.down")) {
133
+ if (this.filtered.length === 0) return;
134
+ this.selectedIndex = this.selectedIndex === this.filtered.length - 1 ? 0 : this.selectedIndex + 1;
135
+ this.updateList();
136
+ this.tui.requestRender();
137
+ } else if (kb.matches(keyData, "tui.select.confirm")) {
138
+ this.confirmSelection();
139
+ } else if (kb.matches(keyData, "tui.select.cancel")) {
140
+ this.done({ confirmed: false });
141
+ } else {
142
+ this.searchInput.handleInput(keyData);
143
+ this.applyFilter(this.searchInput.getValue());
144
+ this.tui.requestRender();
145
+ }
146
+ }
147
+ }
@@ -737,6 +737,13 @@ async function runSlashSubagent(
737
737
  }
738
738
  }
739
739
 
740
+ function launchSlashSubagent(
741
+ pi: ExtensionAPI,
742
+ ctx: ExtensionContext,
743
+ params: SubagentParamsLike,
744
+ ): void {
745
+ void runSlashSubagent(pi, ctx, params);
746
+ }
740
747
 
741
748
  export interface GroupConfig { concurrency?: number; failFast?: boolean; worktree?: boolean }
742
749
  export interface ParsedStep { kind: "step"; name: string; config: InlineConfig; task?: string }
@@ -1001,7 +1008,7 @@ function validateInlineAcceptanceInput(value: string, agent: string): void {
1001
1008
  const errors = validateAcceptanceInput(value, `acceptance for step '${agent}'`);
1002
1009
  if (errors.length > 0) throw new SlashParseError(errors[0]!);
1003
1010
  if (!INLINE_ACCEPTANCE_LEVELS.has(value)) {
1004
- throw new SlashParseError(`Inline acceptance for step '${agent}' supports auto, attested, or checked. Use the subagent tool API or a saved .chain.json file for none or verified acceptance contracts; reviewed is inferred-only.`);
1011
+ throw new SlashParseError(`Inline acceptance for step '${agent}' supports auto, attested, or checked. Use the subagent tool API or a saved .chain.json file for none, verified, or review requirements; reviewed is an achieved status, not an input level.`);
1005
1012
  }
1006
1013
  }
1007
1014
 
@@ -1189,7 +1196,7 @@ export function registerSlashCommands(
1189
1196
  if (inline.model) params.model = inline.model;
1190
1197
  if (bg) params.async = true;
1191
1198
  if (fork) params.context = "fork";
1192
- await runSlashSubagent(pi, ctx, params);
1199
+ launchSlashSubagent(pi, ctx, params);
1193
1200
  },
1194
1201
  });
1195
1202
 
@@ -1203,7 +1210,7 @@ export function registerSlashCommands(
1203
1210
  const params: SubagentParamsLike = { chain: built.chain, task: built.task, clarify: false, agentScope: "both" };
1204
1211
  if (bg) params.async = true;
1205
1212
  if (fork) params.context = "fork";
1206
- await runSlashSubagent(pi, ctx, params);
1213
+ launchSlashSubagent(pi, ctx, params);
1207
1214
  },
1208
1215
  });
1209
1216
 
@@ -1233,7 +1240,7 @@ export function registerSlashCommands(
1233
1240
  const params: SubagentParamsLike = { chain: mapSavedChainSteps(chain), task, clarify: false, agentScope: "both" };
1234
1241
  if (bg) params.async = true;
1235
1242
  if (fork) params.context = "fork";
1236
- await runSlashSubagent(pi, ctx, params);
1243
+ launchSlashSubagent(pi, ctx, params);
1237
1244
  },
1238
1245
  });
1239
1246
 
@@ -1257,7 +1264,7 @@ export function registerSlashCommands(
1257
1264
  const params: SubagentParamsLike = { tasks, clarify: false, agentScope: "both" };
1258
1265
  if (bg) params.async = true;
1259
1266
  if (fork) params.context = "fork";
1260
- await runSlashSubagent(pi, ctx, params);
1267
+ launchSlashSubagent(pi, ctx, params);
1261
1268
  },
1262
1269
  });
1263
1270
 
@@ -1331,7 +1338,9 @@ export function registerSlashCommands(
1331
1338
 
1332
1339
  registerPromptWorkflowCommands({
1333
1340
  pi,
1334
- run: (params, ctx) => runSlashSubagent(pi, ctx, params),
1341
+ run: async (params, ctx) => {
1342
+ launchSlashSubagent(pi, ctx, params);
1343
+ },
1335
1344
  });
1336
1345
 
1337
1346
  pi.registerCommand("subagents-models", {
@@ -193,8 +193,8 @@ function cloneResultsWithProgress(
193
193
  progress: NonNullable<Details["progress"]> | undefined,
194
194
  ): SingleResult[] {
195
195
  return results.map((result, index) => {
196
- const nextProgress = progress?.find((entry) => entry.index === index)
197
- ?? progress?.[index]
196
+ const nextProgress = progress?.find((entry) => entry.index === index
197
+ || (entry.index === undefined && entry.agent === result.agent))
198
198
  ?? result.progress;
199
199
  return nextProgress ? { ...result, progress: nextProgress } : result;
200
200
  });