@the-open-engine/zeroshot 6.10.2 → 6.12.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 (76) hide show
  1. package/cli/commands/providers.js +13 -13
  2. package/cli/index.js +77 -35
  3. package/cli/lib/first-run.js +12 -11
  4. package/cli/lib/update-checker.js +517 -132
  5. package/lib/agent-cli-provider/adapters/opencode.d.ts.map +1 -1
  6. package/lib/agent-cli-provider/adapters/opencode.js +3 -0
  7. package/lib/agent-cli-provider/adapters/opencode.js.map +1 -1
  8. package/lib/agent-cli-provider/types.d.ts +1 -0
  9. package/lib/agent-cli-provider/types.d.ts.map +1 -1
  10. package/lib/agent-cli-provider/types.js.map +1 -1
  11. package/lib/cluster/client.cjs +146 -0
  12. package/lib/cluster/client.d.ts +44 -0
  13. package/lib/cluster/client.mjs +141 -0
  14. package/lib/cluster/connection.cjs +382 -0
  15. package/lib/cluster/connection.d.ts +41 -0
  16. package/lib/cluster/connection.mjs +378 -0
  17. package/lib/cluster/errors.cjs +44 -0
  18. package/lib/cluster/errors.d.ts +23 -0
  19. package/lib/cluster/errors.mjs +32 -0
  20. package/lib/cluster/frames.cjs +49 -0
  21. package/lib/cluster/frames.d.ts +12 -0
  22. package/lib/cluster/frames.mjs +45 -0
  23. package/lib/cluster/generated/protocol-schema.cjs +5 -0
  24. package/lib/cluster/generated/protocol-schema.d.ts +1 -0
  25. package/lib/cluster/generated/protocol-schema.mjs +2 -0
  26. package/lib/cluster/generated/protocol.cjs +22 -0
  27. package/lib/cluster/generated/protocol.d.ts +781 -0
  28. package/lib/cluster/generated/protocol.mjs +19 -0
  29. package/lib/cluster/index.cjs +40 -0
  30. package/lib/cluster/index.d.ts +10 -0
  31. package/lib/cluster/index.mjs +6 -0
  32. package/lib/cluster/queue.cjs +71 -0
  33. package/lib/cluster/queue.d.ts +15 -0
  34. package/lib/cluster/queue.mjs +67 -0
  35. package/lib/cluster/socket.cjs +15 -0
  36. package/lib/cluster/socket.d.ts +11 -0
  37. package/lib/cluster/socket.mjs +12 -0
  38. package/lib/cluster/subscriptions.cjs +270 -0
  39. package/lib/cluster/subscriptions.d.ts +69 -0
  40. package/lib/cluster/subscriptions.mjs +264 -0
  41. package/lib/cluster/validators.cjs +46 -0
  42. package/lib/cluster/validators.d.ts +3 -0
  43. package/lib/cluster/validators.mjs +42 -0
  44. package/lib/settings.js +195 -23
  45. package/lib/setup-apply.js +45 -32
  46. package/lib/setup-undo.js +25 -16
  47. package/package.json +25 -3
  48. package/scripts/build-cluster.js +43 -0
  49. package/scripts/generate-cluster-types.js +203 -0
  50. package/scripts/release-dry-run.js +6 -6
  51. package/scripts/rust-distribution.js +933 -0
  52. package/src/agent/agent-hook-executor.js +14 -6
  53. package/src/agent/agent-lifecycle.js +25 -8
  54. package/src/agent/agent-task-executor.js +711 -139
  55. package/src/agent/output-reformatter.js +54 -41
  56. package/src/agent/pr-verification.js +11 -3
  57. package/src/agent/task-execution-handle.js +373 -0
  58. package/src/agent-cli-provider/adapters/opencode.ts +3 -0
  59. package/src/agent-cli-provider/types.ts +1 -0
  60. package/src/agent-wrapper.js +5 -2
  61. package/src/cluster/client.ts +199 -0
  62. package/src/cluster/connection.ts +300 -0
  63. package/src/cluster/errors.ts +32 -0
  64. package/src/cluster/frames.ts +44 -0
  65. package/src/cluster/generated/protocol-schema.ts +2 -0
  66. package/src/cluster/generated/protocol.ts +183 -0
  67. package/src/cluster/index.ts +38 -0
  68. package/src/cluster/queue.ts +84 -0
  69. package/src/cluster/socket.ts +31 -0
  70. package/src/cluster/subscriptions.ts +256 -0
  71. package/src/cluster/validators.ts +56 -0
  72. package/src/cluster/ws.d.ts +7 -0
  73. package/src/isolation-manager.js +16 -0
  74. package/src/issue-providers/jira-provider.js +1 -1
  75. package/src/issue-providers/linear-provider.js +4 -4
  76. package/task-lib/runner.js +3 -0
@@ -4,16 +4,20 @@
4
4
  * When an LLM outputs markdown/text instead of JSON despite schema instructions,
5
5
  * this module attempts to extract/reformat the content into valid JSON.
6
6
  *
7
- * STATUS: SDK NOT IMPLEMENTED - Reformatting is not available.
8
- * This module exists for future extension when SDK support is added.
9
- *
10
- * To enable reformatting:
11
- * 1. Implement SDK support in the provider (getSDKEnvVar, callSimple)
12
- * 2. The reformatOutput() function will then work automatically
7
+ * Opencode output can be reformatted through its CLI when direct JSON extraction fails.
8
+ * Other providers remain on their own extraction paths and never depend on the opencode binary.
13
9
  */
14
10
 
15
11
  const DEFAULT_MAX_ATTEMPTS = 3;
16
12
 
13
+
14
+ function createCancellationError() {
15
+ const error = new Error('Output reformatting cancelled');
16
+ error.code = 'REFORMAT_CANCELLED';
17
+ return error;
18
+ }
19
+
20
+
17
21
  /**
18
22
  * Build the reformatting prompt
19
23
  *
@@ -27,7 +31,9 @@ function buildReformatPrompt(rawOutput, schema, previousError = null) {
27
31
  // Truncate long outputs to avoid context limits
28
32
  const truncatedOutput = rawOutput.length > 4000 ? rawOutput.slice(-4000) : rawOutput;
29
33
 
30
- let prompt = `Convert this text into a JSON object matching the schema.
34
+ let prompt = `CRITICAL: Do NOT use any tools. Do NOT read, write, or edit any files. Do NOT explore the codebase. This is a pure text-to-JSON transformation respond with JSON only.
35
+
36
+ Convert this text into a JSON object matching the schema.
31
37
 
32
38
  ## SCHEMA
33
39
  \`\`\`json
@@ -58,44 +64,45 @@ Fix this issue in your response.`;
58
64
  }
59
65
 
60
66
  /**
61
- * Attempt to reformat non-JSON output into valid JSON
62
- *
63
- * STATUS: SDK NOT IMPLEMENTED - This function always throws.
64
- * When SDK support is added to providers, this will work automatically.
67
+ * This fallback is intentionally scoped to opencode agents. It participates in agent
68
+ * cancellation so retries cannot outlive cluster shutdown.
65
69
  *
66
70
  * @param {Object} options
67
71
  * @param {string} options.rawOutput - The non-JSON output to reformat
68
72
  * @param {Object} options.schema - Target JSON schema
69
- * @param {string} options.providerName - Provider name (claude, codex, gemini, opencode)
73
+ * @param {string} options.providerName - Active provider name
70
74
  * @param {number} [options.maxAttempts=3] - Maximum reformatting attempts
71
75
  * @param {Function} [options.onAttempt] - Callback for each attempt (attempt, error)
76
+ * @param {Function} [options.isCancelled] - Returns true after agent cancellation
77
+ * @param {Function} options.runReformat - Runs the prompt in the active agent execution context
72
78
  * @returns {Promise<Object>} The reformatted JSON object
73
- * @throws {Error} Always throws - SDK not implemented
79
+ * @throws {Error} If the provider is unsupported, cancellation occurs, or attempts fail
74
80
  */
75
- function reformatOutput({
81
+ async function reformatOutput({
76
82
  rawOutput,
77
- schema: _schema,
83
+ schema,
78
84
  providerName,
79
- maxAttempts: _maxAttempts = DEFAULT_MAX_ATTEMPTS,
80
- onAttempt: _onAttempt,
85
+ maxAttempts = DEFAULT_MAX_ATTEMPTS,
86
+ onAttempt,
87
+ isCancelled = () => false,
88
+ runReformat,
81
89
  }) {
82
- // SDK not implemented - reformatting not available
83
- // When SDK support is added, uncomment the implementation below
84
- return Promise.reject(
85
- new Error(
86
- `Output reformatting not available: SDK not implemented for provider "${providerName}". ` +
90
+ if (providerName !== 'opencode') {
91
+ throw new Error(
92
+ `Output reformatting not available for provider "${providerName}". ` +
87
93
  `Agent output must be valid JSON. Raw output (last 200 chars): ${(rawOutput || '').slice(-200)}`
88
- )
89
- );
94
+ );
95
+ }
96
+ if (typeof runReformat !== 'function') {
97
+ throw new Error('Output reformatting requires the active agent execution context');
98
+ }
90
99
 
91
- // FUTURE: When SDK support is added to providers, uncomment this:
92
- /*
93
- const { getProvider } = require('../providers');
94
- const provider = getProvider(providerName);
95
100
 
101
+ const { extractJsonFromOutput } = require('./output-extraction');
96
102
  let lastError = null;
97
103
 
98
104
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
105
+ if (isCancelled()) throw createCancellationError();
99
106
  if (onAttempt) {
100
107
  onAttempt(attempt, lastError);
101
108
  }
@@ -103,23 +110,19 @@ function reformatOutput({
103
110
  const prompt = buildReformatPrompt(rawOutput, schema, lastError);
104
111
 
105
112
  try {
106
- const result = await provider.callSimple(prompt, {
107
- level: 'level1',
108
- maxTokens: 2000,
109
- });
110
-
113
+ const result = await runReformat(prompt);
114
+ if (isCancelled()) throw createCancellationError();
111
115
  if (!result?.success) {
112
- lastError = result?.error || 'API call failed';
116
+ lastError = result?.error || 'reformat task failed';
113
117
  continue;
114
118
  }
115
-
116
- if (!result?.text) {
117
- lastError = 'Empty response from reformatting model';
119
+ const output = result.output;
120
+ if (!output) {
121
+ lastError = 'reformat task returned no output';
118
122
  continue;
119
123
  }
120
124
 
121
- const parsed = extractJsonFromOutput(result.text, providerName);
122
-
125
+ const parsed = extractJsonFromOutput(output, 'opencode');
123
126
  if (!parsed) {
124
127
  lastError = 'Could not extract JSON from reformatted output';
125
128
  continue;
@@ -133,14 +136,24 @@ function reformatOutput({
133
136
 
134
137
  return parsed;
135
138
  } catch (err) {
139
+ if (
140
+ err.code === 'REFORMAT_CANCELLED' ||
141
+ err.code === 'AGENT_TASK_TIMEOUT' ||
142
+ err.nestedExecutionLifecycle === true ||
143
+ err.retainTaskHandle === true ||
144
+ err.permanent === true ||
145
+ err.terminationExhausted === true
146
+ ) {
147
+ throw err;
148
+ }
136
149
  lastError = err.message;
137
150
  }
138
151
  }
139
152
 
140
153
  throw new Error(
141
- `Failed to reformat output after ${maxAttempts} attempts. Last error: ${lastError}`
154
+ `Failed to reformat output after ${maxAttempts} attempts (provider "${providerName}"). ` +
155
+ `Last error: ${lastError}. Raw output (last 200 chars): ${(rawOutput || '').slice(-200)}`
142
156
  );
143
- */
144
157
  }
145
158
 
146
159
  /**
@@ -335,9 +335,12 @@ function resolveFieldValue(structuredOutput, fieldNames) {
335
335
  return null;
336
336
  }
337
337
 
338
- function resolvePrClaimsFromOutput({ output, providerName, adapter }) {
338
+ function resolvePrClaimsFromOutput({ output, parsedResult, providerName, adapter }) {
339
339
  const { extractJsonFromOutput } = require('./output-extraction');
340
- const structuredOutput = extractJsonFromOutput(output, providerName) || {};
340
+ const structuredOutput =
341
+ parsedResult && typeof parsedResult === 'object'
342
+ ? parsedResult
343
+ : extractJsonFromOutput(output, providerName) || {};
341
344
 
342
345
  const structuredUrl = resolveFieldValue(structuredOutput, getClaimUrlFieldCandidates(adapter));
343
346
  const structuredNumber = resolveFieldValue(
@@ -586,7 +589,12 @@ async function verifyPullRequest({ result, agent, autoMerge }) {
586
589
  const adapter = getVerificationAdapter(platform);
587
590
  const providerName =
588
591
  typeof agent?._resolveProvider === 'function' ? agent._resolveProvider() : 'claude';
589
- const claims = resolvePrClaimsFromOutput({ output: result.output, providerName, adapter });
592
+ const claims = resolvePrClaimsFromOutput({
593
+ output: result.output,
594
+ parsedResult: result.parsedResult,
595
+ providerName,
596
+ adapter,
597
+ });
590
598
 
591
599
  if (handleBlockedPusherOutcome({ claims, platform, agent })) {
592
600
  return;
@@ -0,0 +1,373 @@
1
+ function isTerminationConfirmed(termination) {
2
+ return termination?.forced !== false || termination?.alreadyTerminal === true;
3
+ }
4
+ const MAX_DEADLINE_CLEANUP_ATTEMPTS = 3;
5
+
6
+ /**
7
+ * TaskExecutionHandle — first-class reentrant task-execution handle.
8
+ *
9
+ * Created BEFORE the provider process is spawned so cancellation is possible
10
+ * even before a task ID or PID is known. Owns the process tree, tracks late
11
+ * task-ID / PID assignment, and supports nested (child) executions that never
12
+ * overwrite the parent's identity.
13
+ */
14
+ class TaskExecutionHandle {
15
+ constructor(agentId) {
16
+ /** @type {string} Owning agent id (immutable). */
17
+ this.agentId = agentId;
18
+ /** @type {string|null} Zeroshot task id, assigned once after registration. */
19
+ this._taskId = null;
20
+ /** @type {number|null} Real CLI process pid, assigned once after registration. */
21
+ this._pid = null;
22
+ /** @type {import('child_process').ChildProcess|null} Launch wrapper process. */
23
+ this._proc = null;
24
+ this._cancelled = false;
25
+ this._cancelReason = null;
26
+ this._cancelDetails = {};
27
+ this._cancelAction = null;
28
+ this._failClosedAction = null;
29
+ this._failClosedError = null;
30
+ this._invokedCancelActions = new Set();
31
+ this._cancelActionPromises = [];
32
+ this._lastCancellationResult = undefined;
33
+ this._executionFinished = false;
34
+ this._retainOwnership = false;
35
+ this.settled = false;
36
+ this._resolveSettle = null;
37
+ this._settlePromise = new Promise((resolve) => {
38
+ this._resolveSettle = resolve;
39
+ });
40
+ /** @type {ReturnType<typeof setTimeout>|null} */
41
+ this._killTimer = null;
42
+ /** @type {ReturnType<typeof setTimeout>|null} */
43
+ this._deadlineTimer = null;
44
+ }
45
+
46
+ get taskId() {
47
+ return this._taskId;
48
+ }
49
+
50
+ get pid() {
51
+ return this._pid;
52
+ }
53
+
54
+ get isCancelled() {
55
+ return this._cancelled;
56
+ }
57
+
58
+ get cancelReason() {
59
+ return this._cancelReason;
60
+ }
61
+
62
+ get cancelDetails() {
63
+ return this._cancelDetails;
64
+ }
65
+
66
+ attachProcess(proc) {
67
+ this._proc = proc;
68
+ const clearWrapperKill = () => this._clearKillTimer();
69
+ proc.once('close', clearWrapperKill);
70
+ proc.once('error', clearWrapperKill);
71
+ if (this._cancelled) {
72
+ this._killProcess();
73
+ }
74
+ }
75
+
76
+ assignTaskId(taskId) {
77
+ if (this._taskId && this._taskId !== taskId) {
78
+ throw new Error(`Nested execution task ID changed from ${this._taskId} to ${taskId}`);
79
+ }
80
+ this._taskId = taskId;
81
+ if (this._cancelled) {
82
+ this._killProcess();
83
+ this._invokeCancelAction();
84
+ }
85
+ }
86
+
87
+ assignPid(pid) {
88
+ if (this._pid && this._pid !== pid) {
89
+ throw new Error(`Nested execution PID changed from ${this._pid} to ${pid}`);
90
+ }
91
+ this._pid = pid;
92
+ }
93
+
94
+ setCancelAction(action) {
95
+ const previousAction = this._cancelAction;
96
+ this._cancelAction = action;
97
+ if (this._cancelled) {
98
+ this._invokeCancelAction();
99
+ }
100
+ return previousAction;
101
+ }
102
+
103
+ setFailClosedAction(action) {
104
+ this._failClosedAction = action;
105
+ if (this._failClosedError) {
106
+ action(this._failClosedError);
107
+ }
108
+ }
109
+
110
+ failClosed(error) {
111
+ if (this.settled || this._failClosedError) return false;
112
+ this._retainOwnership = true;
113
+ this._failClosedError = error;
114
+ if (!error.taskId && this._taskId) {
115
+ error.taskId = this._taskId;
116
+ }
117
+ if (this._failClosedAction) {
118
+ this._failClosedAction(error);
119
+ }
120
+ return true;
121
+ }
122
+
123
+ cancel(reason = 'Task cancelled', details = {}) {
124
+ if (!this._cancelled) {
125
+ this._cancelled = true;
126
+ this._cancelReason = reason;
127
+ this._cancelDetails = details;
128
+ }
129
+ this._clearDeadlineTimer();
130
+ this._killProcess();
131
+ this._invokeCancelAction();
132
+ return this.waitForCancellation();
133
+ }
134
+
135
+ async waitForCancellation() {
136
+ let observedCount = -1;
137
+ let results = [];
138
+ try {
139
+ while (observedCount !== this._cancelActionPromises.length) {
140
+ observedCount = this._cancelActionPromises.length;
141
+ results = await Promise.all(this._cancelActionPromises);
142
+ }
143
+ } catch (error) {
144
+ this._retainOwnership = true;
145
+ this._invokedCancelActions.delete(this._cancelAction);
146
+ this._cancelActionPromises = [];
147
+ throw error;
148
+ }
149
+ const observedTermination = results.at(-1);
150
+ if (observedTermination !== undefined && observedTermination !== null) {
151
+ this._lastCancellationResult = observedTermination;
152
+ }
153
+ const termination =
154
+ observedTermination !== undefined && observedTermination !== null
155
+ ? observedTermination
156
+ : this._lastCancellationResult;
157
+ if (termination === undefined || termination === null) {
158
+ return termination;
159
+ }
160
+ if (!isTerminationConfirmed(termination)) {
161
+ this._retainOwnership = true;
162
+ this._invokedCancelActions.delete(this._cancelAction);
163
+ this._cancelActionPromises = [];
164
+ } else {
165
+ this._retainOwnership = false;
166
+ if (this._executionFinished) {
167
+ this.markSettled();
168
+ }
169
+ }
170
+ return termination;
171
+ }
172
+
173
+ armDeadline(timeoutMs) {
174
+ if (timeoutMs <= 0 || this.settled) return;
175
+ this._clearDeadlineTimer();
176
+ this._deadlineTimer = setTimeout(() => {
177
+ this._deadlineTimer = null;
178
+ this._cancelAtDeadline(timeoutMs).catch(() => {
179
+ // failClosed records the permanent error before notifying the execution owner.
180
+ });
181
+ }, timeoutMs);
182
+ }
183
+
184
+ async _cancelAtDeadline(timeoutMs) {
185
+ const reason = `Nested task timed out after ${timeoutMs}ms`;
186
+ let lastError = null;
187
+ let termination = null;
188
+ let attempts = 0;
189
+ for (attempts = 1; attempts <= MAX_DEADLINE_CLEANUP_ATTEMPTS; attempts++) {
190
+ try {
191
+ termination = await this.cancel(reason, { code: 'AGENT_TASK_TIMEOUT' });
192
+ if (termination && isTerminationConfirmed(termination)) return;
193
+ } catch (error) {
194
+ lastError = error;
195
+ }
196
+ }
197
+
198
+ const detail =
199
+ lastError?.message || termination?.reason || 'task termination was not confirmed';
200
+ const error = new Error(
201
+ `Failed to terminate nested task ${this._taskId || 'before task ID assignment'} ` +
202
+ `after ${attempts - 1} deadline cleanup attempts: ${detail}`
203
+ );
204
+ error.code = 'NESTED_TASK_TERMINATION_EXHAUSTED';
205
+ error.taskId = this._taskId;
206
+ error.permanent = true;
207
+ error.restartExhausted = true;
208
+ error.terminationExhausted = true;
209
+ error.terminationAttempts = attempts - 1;
210
+ error.retainTaskHandle = true;
211
+ error.nestedExecutionLifecycle = true;
212
+ this.failClosed(error);
213
+ }
214
+
215
+ retainOwnership() {
216
+ this._retainOwnership = true;
217
+ }
218
+
219
+ finishExecution() {
220
+ this._executionFinished = true;
221
+ if (!this._retainOwnership) {
222
+ this.markSettled();
223
+ }
224
+ }
225
+
226
+ markSettled() {
227
+ if (this.settled) return;
228
+ this.settled = true;
229
+ this._clearKillTimer();
230
+ this._clearDeadlineTimer();
231
+ this._resolveSettle();
232
+ }
233
+
234
+ async settle() {
235
+ await this._settlePromise;
236
+ }
237
+
238
+ _invokeCancelAction() {
239
+ const action = this._cancelAction;
240
+ if (!action || this._invokedCancelActions.has(action)) return;
241
+ this._invokedCancelActions.add(action);
242
+ const cancellation = Promise.resolve().then(() =>
243
+ action(this._cancelReason, this._cancelDetails)
244
+ );
245
+ this._cancelActionPromises.push(cancellation);
246
+ }
247
+
248
+ _clearDeadlineTimer() {
249
+ if (this._deadlineTimer) {
250
+ clearTimeout(this._deadlineTimer);
251
+ this._deadlineTimer = null;
252
+ }
253
+ }
254
+
255
+ _clearKillTimer() {
256
+ if (this._killTimer) {
257
+ clearTimeout(this._killTimer);
258
+ this._killTimer = null;
259
+ }
260
+ }
261
+
262
+ _killProcess() {
263
+ if (!this._proc || this.settled) return;
264
+ try {
265
+ this._proc.kill('SIGTERM');
266
+ const proc = this._proc;
267
+ this._clearKillTimer();
268
+ this._killTimer = setTimeout(() => {
269
+ this._killTimer = null;
270
+ if (!this.settled) {
271
+ try {
272
+ proc.kill('SIGKILL');
273
+ } catch {
274
+ // Process already exited.
275
+ }
276
+ }
277
+ }, 3000);
278
+ } catch {
279
+ // Process already exited.
280
+ }
281
+ }
282
+ }
283
+
284
+ class NestedExecutionRegistry {
285
+ constructor(agentId) {
286
+ this.agentId = agentId;
287
+ this._handles = new Set();
288
+ this._cancellation = null;
289
+ }
290
+
291
+ get size() {
292
+ return this._handles.size;
293
+ }
294
+
295
+ get hasActive() {
296
+ return this._handles.size > 0;
297
+ }
298
+
299
+ get activeTaskIds() {
300
+ return [...this._handles].map((handle) => handle.taskId).filter(Boolean);
301
+ }
302
+
303
+ register(handle) {
304
+ if (this._cancellation) {
305
+ const error = new Error(this._cancellation.reason);
306
+ error.code = this._cancellation.details.code || 'REFORMAT_CANCELLED';
307
+ error.nestedExecutionCancellation = true;
308
+ error.nestedExecutionLifecycle = true;
309
+ throw error;
310
+ }
311
+ if (!(handle instanceof TaskExecutionHandle)) {
312
+ throw new TypeError('Nested execution registry accepts TaskExecutionHandle instances only');
313
+ }
314
+ if (handle.agentId !== this.agentId) {
315
+ throw new Error(
316
+ `Nested execution owner mismatch: registry ${this.agentId}, handle ${handle.agentId}`
317
+ );
318
+ }
319
+ this._handles.add(handle);
320
+ return handle;
321
+ }
322
+
323
+ unregister(handle) {
324
+ if (!handle.settled) {
325
+ throw new Error('Cannot unregister a nested execution before settlement');
326
+ }
327
+ this._handles.delete(handle);
328
+ }
329
+
330
+ failClosed(error) {
331
+ let dispatched = false;
332
+ for (const handle of this._handles) {
333
+ dispatched = handle.failClosed(error) || dispatched;
334
+ }
335
+ return dispatched;
336
+ }
337
+
338
+ async cancelAll(reason = 'Task cancelled', details = {}) {
339
+ this._cancellation = { reason, details };
340
+ try {
341
+ const handles = [...this._handles];
342
+ const terminations = await Promise.all(
343
+ handles.map(async (handle) => {
344
+ const termination = await handle.cancel(reason, details);
345
+ if (isTerminationConfirmed(termination)) {
346
+ await handle.settle();
347
+ this.unregister(handle);
348
+ }
349
+ return termination;
350
+ })
351
+ );
352
+ const failed = terminations.find(
353
+ (termination) => !isTerminationConfirmed(termination)
354
+ );
355
+ return failed || { forced: true, nested: terminations };
356
+ } finally {
357
+ this._cancellation = null;
358
+ }
359
+ }
360
+ }
361
+
362
+ function getNestedExecutionRegistry(agent) {
363
+ if (!agent.nestedExecutions) {
364
+ agent.nestedExecutions = new NestedExecutionRegistry(agent.id);
365
+ }
366
+ return agent.nestedExecutions;
367
+ }
368
+
369
+ module.exports = {
370
+ getNestedExecutionRegistry,
371
+ NestedExecutionRegistry,
372
+ TaskExecutionHandle,
373
+ };
@@ -55,6 +55,9 @@ function addOpencodeOptionalArgs(args: string[], options: BuildProviderCommandOp
55
55
  ) {
56
56
  args.push('--format', 'json');
57
57
  }
58
+ if (options.agentName) {
59
+ args.push('--agent', options.agentName);
60
+ }
58
61
 
59
62
  if (options.modelSpec?.model) {
60
63
  args.push('--model', options.modelSpec.model);
@@ -268,6 +268,7 @@ export interface BuildProviderCommandOptions {
268
268
  readonly outputFormat?: OutputFormat;
269
269
  readonly jsonSchema?: unknown;
270
270
  readonly cwd?: string;
271
+ readonly agentName?: string;
271
272
  readonly autoApprove?: boolean;
272
273
  readonly resumeSessionId?: string;
273
274
  readonly continueSession?: boolean;
@@ -25,6 +25,7 @@ const {
25
25
  const { findMatchingTrigger, evaluateTrigger } = require('./agent/agent-trigger-evaluator');
26
26
  const { executeHook } = require('./agent/agent-hook-executor');
27
27
  const { injectInput: injectAgentInput } = require('./agent/agent-input-injector');
28
+ const { NestedExecutionRegistry } = require('./agent/task-execution-handle');
28
29
  const {
29
30
  spawnClaudeTask,
30
31
  followClaudeTaskLogs,
@@ -78,6 +79,8 @@ class AgentWrapper {
78
79
  this.currentPromptIdentity = null;
79
80
  /** @type {number | null} */
80
81
  this.processPid = null; // Track process PID for resource monitoring
82
+ /** Active child executions never overwrite the parent task identity fields. */
83
+ this.nestedExecutions = new NestedExecutionRegistry(this.id);
81
84
  this.running = false;
82
85
  /** @type {Function | null} */
83
86
  this.unsubscribe = null;
@@ -512,8 +515,8 @@ class AgentWrapper {
512
515
  * Spawn claude-zeroshots process and stream output via message bus
513
516
  * @private
514
517
  */
515
- _spawnClaudeTask(context) {
516
- return spawnClaudeTask(this, context);
518
+ _spawnClaudeTask(context, options = {}) {
519
+ return spawnClaudeTask(this, context, options);
517
520
  }
518
521
 
519
522
  /**