@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.
- package/cli/commands/providers.js +13 -13
- package/cli/index.js +77 -35
- package/cli/lib/first-run.js +12 -11
- package/cli/lib/update-checker.js +517 -132
- package/lib/agent-cli-provider/adapters/opencode.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/opencode.js +3 -0
- package/lib/agent-cli-provider/adapters/opencode.js.map +1 -1
- package/lib/agent-cli-provider/types.d.ts +1 -0
- package/lib/agent-cli-provider/types.d.ts.map +1 -1
- package/lib/agent-cli-provider/types.js.map +1 -1
- package/lib/cluster/client.cjs +146 -0
- package/lib/cluster/client.d.ts +44 -0
- package/lib/cluster/client.mjs +141 -0
- package/lib/cluster/connection.cjs +382 -0
- package/lib/cluster/connection.d.ts +41 -0
- package/lib/cluster/connection.mjs +378 -0
- package/lib/cluster/errors.cjs +44 -0
- package/lib/cluster/errors.d.ts +23 -0
- package/lib/cluster/errors.mjs +32 -0
- package/lib/cluster/frames.cjs +49 -0
- package/lib/cluster/frames.d.ts +12 -0
- package/lib/cluster/frames.mjs +45 -0
- package/lib/cluster/generated/protocol-schema.cjs +5 -0
- package/lib/cluster/generated/protocol-schema.d.ts +1 -0
- package/lib/cluster/generated/protocol-schema.mjs +2 -0
- package/lib/cluster/generated/protocol.cjs +22 -0
- package/lib/cluster/generated/protocol.d.ts +781 -0
- package/lib/cluster/generated/protocol.mjs +19 -0
- package/lib/cluster/index.cjs +40 -0
- package/lib/cluster/index.d.ts +10 -0
- package/lib/cluster/index.mjs +6 -0
- package/lib/cluster/queue.cjs +71 -0
- package/lib/cluster/queue.d.ts +15 -0
- package/lib/cluster/queue.mjs +67 -0
- package/lib/cluster/socket.cjs +15 -0
- package/lib/cluster/socket.d.ts +11 -0
- package/lib/cluster/socket.mjs +12 -0
- package/lib/cluster/subscriptions.cjs +270 -0
- package/lib/cluster/subscriptions.d.ts +69 -0
- package/lib/cluster/subscriptions.mjs +264 -0
- package/lib/cluster/validators.cjs +46 -0
- package/lib/cluster/validators.d.ts +3 -0
- package/lib/cluster/validators.mjs +42 -0
- package/lib/settings.js +195 -23
- package/lib/setup-apply.js +45 -32
- package/lib/setup-undo.js +25 -16
- package/package.json +25 -3
- package/scripts/build-cluster.js +43 -0
- package/scripts/generate-cluster-types.js +203 -0
- package/scripts/release-dry-run.js +6 -6
- package/scripts/rust-distribution.js +933 -0
- package/src/agent/agent-hook-executor.js +14 -6
- package/src/agent/agent-lifecycle.js +25 -8
- package/src/agent/agent-task-executor.js +711 -139
- package/src/agent/output-reformatter.js +54 -41
- package/src/agent/pr-verification.js +11 -3
- package/src/agent/task-execution-handle.js +373 -0
- package/src/agent-cli-provider/adapters/opencode.ts +3 -0
- package/src/agent-cli-provider/types.ts +1 -0
- package/src/agent-wrapper.js +5 -2
- package/src/cluster/client.ts +199 -0
- package/src/cluster/connection.ts +300 -0
- package/src/cluster/errors.ts +32 -0
- package/src/cluster/frames.ts +44 -0
- package/src/cluster/generated/protocol-schema.ts +2 -0
- package/src/cluster/generated/protocol.ts +183 -0
- package/src/cluster/index.ts +38 -0
- package/src/cluster/queue.ts +84 -0
- package/src/cluster/socket.ts +31 -0
- package/src/cluster/subscriptions.ts +256 -0
- package/src/cluster/validators.ts +56 -0
- package/src/cluster/ws.d.ts +7 -0
- package/src/isolation-manager.js +16 -0
- package/src/issue-providers/jira-provider.js +1 -1
- package/src/issue-providers/linear-provider.js +4 -4
- package/task-lib/runner.js +3 -0
|
@@ -11,8 +11,13 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
const { spawn, spawnSync } = require('child_process');
|
|
14
|
+
const { randomUUID } = require('crypto');
|
|
14
15
|
const path = require('path');
|
|
15
16
|
const fs = require('fs');
|
|
17
|
+
const {
|
|
18
|
+
getNestedExecutionRegistry,
|
|
19
|
+
TaskExecutionHandle,
|
|
20
|
+
} = require('./task-execution-handle');
|
|
16
21
|
const os = require('os');
|
|
17
22
|
const { parseProviderChunk, getProvider } = require('../providers');
|
|
18
23
|
const { getTask, getTaskBySpawnOwnershipToken } = require('../../task-lib/store.js');
|
|
@@ -48,6 +53,207 @@ const {
|
|
|
48
53
|
validateCompletedResumeIdentity,
|
|
49
54
|
} = require('./provider-session');
|
|
50
55
|
const { extractClaudeVertexModelError } = require('./output-extraction');
|
|
56
|
+
const TASK_TERMINAL_STATUSES = new Set(['completed', 'failed', 'killed', 'stale']);
|
|
57
|
+
const OPENCODE_CONFIG_CONTENT_ENV = 'OPENCODE_CONFIG_CONTENT';
|
|
58
|
+
const OPENCODE_AGENT_ENV = 'ZEROSHOT_OPENCODE_AGENT';
|
|
59
|
+
const REFORMATTER_AGENT_PREFIX = 'zeroshot-output-reformatter-';
|
|
60
|
+
|
|
61
|
+
function parseOpenCodeJsonc(content) {
|
|
62
|
+
let withoutComments = '';
|
|
63
|
+
let inString = false;
|
|
64
|
+
let escaped = false;
|
|
65
|
+
let cursor = 0;
|
|
66
|
+
while (cursor < content.length) {
|
|
67
|
+
const char = content[cursor];
|
|
68
|
+
const next = content[cursor + 1];
|
|
69
|
+
if (inString) {
|
|
70
|
+
withoutComments += char;
|
|
71
|
+
if (escaped) {
|
|
72
|
+
escaped = false;
|
|
73
|
+
} else if (char === '\\') {
|
|
74
|
+
escaped = true;
|
|
75
|
+
} else if (char === '"') {
|
|
76
|
+
inString = false;
|
|
77
|
+
}
|
|
78
|
+
cursor += 1;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (char === '"') {
|
|
82
|
+
inString = true;
|
|
83
|
+
withoutComments += char;
|
|
84
|
+
cursor += 1;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (char === '/' && next === '/') {
|
|
88
|
+
cursor += 2;
|
|
89
|
+
while (cursor < content.length && content[cursor] !== '\n') cursor += 1;
|
|
90
|
+
withoutComments += '\n';
|
|
91
|
+
cursor += 1;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (char === '/' && next === '*') {
|
|
95
|
+
cursor += 2;
|
|
96
|
+
while (
|
|
97
|
+
cursor < content.length &&
|
|
98
|
+
!(content[cursor] === '*' && content[cursor + 1] === '/')
|
|
99
|
+
) {
|
|
100
|
+
if (content[cursor] === '\n') withoutComments += '\n';
|
|
101
|
+
cursor += 1;
|
|
102
|
+
}
|
|
103
|
+
if (cursor >= content.length) {
|
|
104
|
+
throw new SyntaxError('Unterminated block comment in OpenCode inline config');
|
|
105
|
+
}
|
|
106
|
+
cursor += 2;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
withoutComments += char;
|
|
110
|
+
cursor += 1;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let withoutTrailingCommas = '';
|
|
114
|
+
inString = false;
|
|
115
|
+
escaped = false;
|
|
116
|
+
for (let index = 0; index < withoutComments.length; index++) {
|
|
117
|
+
const char = withoutComments[index];
|
|
118
|
+
if (inString) {
|
|
119
|
+
withoutTrailingCommas += char;
|
|
120
|
+
if (escaped) {
|
|
121
|
+
escaped = false;
|
|
122
|
+
} else if (char === '\\') {
|
|
123
|
+
escaped = true;
|
|
124
|
+
} else if (char === '"') {
|
|
125
|
+
inString = false;
|
|
126
|
+
}
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (char === '"') {
|
|
130
|
+
inString = true;
|
|
131
|
+
withoutTrailingCommas += char;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (char === ',') {
|
|
135
|
+
let nextIndex = index + 1;
|
|
136
|
+
while (/\s/.test(withoutComments[nextIndex] || '')) nextIndex++;
|
|
137
|
+
if (withoutComments[nextIndex] === '}' || withoutComments[nextIndex] === ']') {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
withoutTrailingCommas += char;
|
|
142
|
+
}
|
|
143
|
+
return JSON.parse(withoutTrailingCommas);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function ensureFormatterLaunchOptions(providerName, options) {
|
|
147
|
+
if (providerName !== 'opencode' || options.disableTools !== true) return options;
|
|
148
|
+
if (options.formatterAgentName) return options;
|
|
149
|
+
return {
|
|
150
|
+
...options,
|
|
151
|
+
formatterAgentName: `${REFORMATTER_AGENT_PREFIX}${randomUUID()}`,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function applyOpenCodeToolBoundary(env, providerName, options = {}) {
|
|
156
|
+
if (providerName !== 'opencode' || options.disableTools !== true) return env;
|
|
157
|
+
if (!options.formatterAgentName) {
|
|
158
|
+
throw new Error('Tool-disabled OpenCode formatter launch is missing its unique agent identity');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
let config = {};
|
|
162
|
+
const existingContent = env[OPENCODE_CONFIG_CONTENT_ENV];
|
|
163
|
+
if (existingContent) {
|
|
164
|
+
try {
|
|
165
|
+
config = parseOpenCodeJsonc(existingContent);
|
|
166
|
+
} catch (error) {
|
|
167
|
+
throw new Error(
|
|
168
|
+
`Cannot install tool-disabled OpenCode formatter profile: invalid ${OPENCODE_CONFIG_CONTENT_ENV}: ${error.message}`
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const existingAgents =
|
|
174
|
+
config.agent && typeof config.agent === 'object' && !Array.isArray(config.agent)
|
|
175
|
+
? config.agent
|
|
176
|
+
: {};
|
|
177
|
+
const existingModes =
|
|
178
|
+
config.mode && typeof config.mode === 'object' && !Array.isArray(config.mode)
|
|
179
|
+
? config.mode
|
|
180
|
+
: {};
|
|
181
|
+
const formatterProfile = {
|
|
182
|
+
description: 'Convert supplied text to schema-valid JSON without external actions',
|
|
183
|
+
mode: 'primary',
|
|
184
|
+
permission: { '*': 'deny' },
|
|
185
|
+
tools: { '*': false },
|
|
186
|
+
};
|
|
187
|
+
env[OPENCODE_AGENT_ENV] = options.formatterAgentName;
|
|
188
|
+
env[OPENCODE_CONFIG_CONTENT_ENV] = JSON.stringify({
|
|
189
|
+
...config,
|
|
190
|
+
default_agent: options.formatterAgentName,
|
|
191
|
+
permission: 'deny',
|
|
192
|
+
tools: { '*': false },
|
|
193
|
+
agent: {
|
|
194
|
+
...existingAgents,
|
|
195
|
+
[options.formatterAgentName]: formatterProfile,
|
|
196
|
+
},
|
|
197
|
+
mode: {
|
|
198
|
+
...existingModes,
|
|
199
|
+
[options.formatterAgentName]: formatterProfile,
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
return env;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function resolveIsolatedOpenCodeConfigContent(manager, clusterId, providerName) {
|
|
206
|
+
if (
|
|
207
|
+
providerName === 'opencode' &&
|
|
208
|
+
typeof manager.getContainerEnvironmentValue === 'function'
|
|
209
|
+
) {
|
|
210
|
+
return manager.getContainerEnvironmentValue(clusterId, OPENCODE_CONFIG_CONTENT_ENV);
|
|
211
|
+
}
|
|
212
|
+
return process.env[OPENCODE_CONFIG_CONTENT_ENV] || null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async function resolveIsolatedOpenCodeConfigUnderOwnership({
|
|
216
|
+
manager,
|
|
217
|
+
clusterId,
|
|
218
|
+
providerName,
|
|
219
|
+
executionHandle,
|
|
220
|
+
}) {
|
|
221
|
+
if (!executionHandle) {
|
|
222
|
+
return resolveIsolatedOpenCodeConfigContent(manager, clusterId, providerName);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
let setupPending = true;
|
|
226
|
+
let rejectSetup;
|
|
227
|
+
const setupFailure = new Promise((_resolve, reject) => {
|
|
228
|
+
rejectSetup = reject;
|
|
229
|
+
});
|
|
230
|
+
executionHandle.setCancelAction((reason, details = {}) => {
|
|
231
|
+
if (setupPending) {
|
|
232
|
+
const error = new Error(reason || 'Nested task setup cancelled');
|
|
233
|
+
error.code = details.code || 'REFORMAT_CANCELLED';
|
|
234
|
+
error.nestedExecutionCancellation = true;
|
|
235
|
+
error.nestedExecutionLifecycle = true;
|
|
236
|
+
rejectSetup(error);
|
|
237
|
+
}
|
|
238
|
+
return { forced: true, beforeLaunch: true };
|
|
239
|
+
});
|
|
240
|
+
executionHandle.setFailClosedAction((error) => {
|
|
241
|
+
if (setupPending) rejectSetup(error);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
const config = await Promise.race([
|
|
246
|
+
Promise.resolve(resolveIsolatedOpenCodeConfigContent(manager, clusterId, providerName)),
|
|
247
|
+
setupFailure,
|
|
248
|
+
]);
|
|
249
|
+
if (executionHandle.isCancelled) {
|
|
250
|
+
throw createNestedCancellationError(executionHandle);
|
|
251
|
+
}
|
|
252
|
+
return config;
|
|
253
|
+
} finally {
|
|
254
|
+
setupPending = false;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
51
257
|
|
|
52
258
|
function runCommandWithTimeout(command, args, options = {}, callback = null) {
|
|
53
259
|
const timeout = options.timeout ?? 30000;
|
|
@@ -449,19 +655,85 @@ function extractTokenUsage(output, providerName = 'claude') {
|
|
|
449
655
|
* Spawn claude-zeroshots process and stream output via message bus
|
|
450
656
|
* @param {Object} agent - Agent instance
|
|
451
657
|
* @param {String} context - Context to pass to Claude
|
|
658
|
+
* @param {{skipStructuredResultCheck?: boolean}} [options] - Internal nested-task controls
|
|
452
659
|
* @returns {Promise<Object>} Result object { success, output, error }
|
|
453
660
|
*/
|
|
454
|
-
|
|
661
|
+
function createExecutionHandle(agent, nested) {
|
|
662
|
+
const handle = new TaskExecutionHandle(agent.id);
|
|
663
|
+
if (!nested) return { handle, registry: null };
|
|
664
|
+
|
|
665
|
+
const registry = getNestedExecutionRegistry(agent);
|
|
666
|
+
registry.register(handle);
|
|
667
|
+
if (agent.timeout > 0) {
|
|
668
|
+
handle.armDeadline(agent.timeout);
|
|
669
|
+
}
|
|
670
|
+
return { handle, registry };
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function isNestedLifecycleError(error) {
|
|
674
|
+
return (
|
|
675
|
+
error?.nestedExecutionLifecycle === true ||
|
|
676
|
+
error?.retainTaskHandle === true ||
|
|
677
|
+
error?.terminationExhausted === true
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function isTerminationConfirmed(termination) {
|
|
682
|
+
return termination?.forced !== false || termination?.alreadyTerminal === true;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function createNestedCancellationError(handle) {
|
|
686
|
+
const error = new Error(handle.cancelReason || 'Nested task cancelled');
|
|
687
|
+
error.code = handle.cancelDetails.code || 'REFORMAT_CANCELLED';
|
|
688
|
+
error.taskId = handle.taskId;
|
|
689
|
+
error.nestedExecutionCancellation = true;
|
|
690
|
+
error.nestedExecutionLifecycle = true;
|
|
691
|
+
return error;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function retainUnconfirmedNestedTermination(handle, error, termination) {
|
|
695
|
+
error.nestedExecutionLifecycle = true;
|
|
696
|
+
handle.retainOwnership();
|
|
697
|
+
error.message += ` Nested task cleanup was not confirmed: ${
|
|
698
|
+
termination?.reason || 'termination status unavailable'
|
|
699
|
+
}`;
|
|
700
|
+
error.retainTaskHandle = true;
|
|
701
|
+
error.permanent = true;
|
|
702
|
+
error.restartExhausted = true;
|
|
703
|
+
error.terminationExhausted = true;
|
|
704
|
+
error.terminationAttempts = 1;
|
|
705
|
+
error.taskId = handle.taskId;
|
|
706
|
+
return error;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
async function terminateNestedSetupFailure(handle, error) {
|
|
710
|
+
let termination;
|
|
711
|
+
try {
|
|
712
|
+
termination = await handle.cancel(error.message, { code: 'NESTED_SETUP_FAILED' });
|
|
713
|
+
} catch (cleanupError) {
|
|
714
|
+
termination = { forced: false, reason: cleanupError.message };
|
|
715
|
+
}
|
|
716
|
+
if (!isTerminationConfirmed(termination)) {
|
|
717
|
+
retainUnconfirmedNestedTermination(handle, error, termination);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
async function settleRegisteredNestedHandle(registry, handle) {
|
|
722
|
+
await handle.waitForCancellation().catch(() => {
|
|
723
|
+
// A cleanup failure retains the handle for a later shutdown/kill retry.
|
|
724
|
+
});
|
|
725
|
+
handle.finishExecution();
|
|
726
|
+
if (handle.settled) {
|
|
727
|
+
registry.unregister(handle);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
async function spawnClaudeTask(agent, context, options = {}) {
|
|
455
732
|
const providerName = agent._resolveProvider ? agent._resolveProvider() : 'claude';
|
|
456
733
|
const modelSpec = resolveAgentModelSpec(agent);
|
|
457
734
|
|
|
458
|
-
const ctPath = getClaudeTasksPath();
|
|
735
|
+
const ctPath = agent.taskCliPath || getClaudeTasksPath();
|
|
459
736
|
const cwd = agent.config.cwd || process.cwd();
|
|
460
|
-
|
|
461
|
-
// Build zeroshot task run args.
|
|
462
|
-
// CRITICAL: Default to strict schema validation to prevent cluster crashes from parse failures
|
|
463
|
-
// strictSchema=true uses Claude CLI's native --json-schema enforcement (no streaming but guaranteed structure)
|
|
464
|
-
// strictSchema=false uses stream-json with post-run validation (live logs but fragile)
|
|
465
737
|
const { desiredOutputFormat, runOutputFormat } = resolveOutputFormatConfig(agent);
|
|
466
738
|
const args = buildTaskRunArgs({
|
|
467
739
|
agent,
|
|
@@ -470,28 +742,20 @@ async function spawnClaudeTask(agent, context) {
|
|
|
470
742
|
runOutputFormat,
|
|
471
743
|
});
|
|
472
744
|
|
|
473
|
-
// NOTE: maxRetries is handled by the agent wrapper's internal retry loop,
|
|
474
|
-
// not passed to the CLI. See _handleTrigger() for retry logic.
|
|
475
|
-
|
|
476
745
|
maybeLogStreamJsonNotice(agent, runOutputFormat);
|
|
477
746
|
|
|
478
|
-
// If schema enforcement is desired but we had to run stream-json for live logs,
|
|
479
|
-
// add explicit output instructions so the model still knows the required shape.
|
|
480
747
|
const finalContext = buildFinalContext({
|
|
481
748
|
agent,
|
|
482
749
|
context,
|
|
483
750
|
desiredOutputFormat,
|
|
484
751
|
runOutputFormat,
|
|
485
752
|
});
|
|
486
|
-
|
|
487
753
|
args.push(finalContext);
|
|
488
754
|
|
|
489
|
-
// MOCK SUPPORT: Use injected mock function if provided
|
|
490
755
|
if (agent.mockSpawnFn) {
|
|
491
|
-
return agent.mockSpawnFn(args, { context });
|
|
756
|
+
return agent.mockSpawnFn(args, { context, options });
|
|
492
757
|
}
|
|
493
758
|
|
|
494
|
-
// SAFETY: Fail hard if testMode=true but no mock (should be caught in constructor)
|
|
495
759
|
if (agent.testMode) {
|
|
496
760
|
throw new Error(
|
|
497
761
|
`AgentWrapper: testMode=true but attempting real Claude API call for agent '${agent.id}'. ` +
|
|
@@ -499,16 +763,11 @@ async function spawnClaudeTask(agent, context) {
|
|
|
499
763
|
);
|
|
500
764
|
}
|
|
501
765
|
|
|
502
|
-
// ISOLATION MODE: Run inside Docker container
|
|
503
766
|
if (agent.isolation?.enabled) {
|
|
504
|
-
return spawnClaudeTaskIsolated(agent, context);
|
|
767
|
+
return spawnClaudeTaskIsolated(agent, context, options);
|
|
505
768
|
}
|
|
769
|
+
options = ensureFormatterLaunchOptions(providerName, options);
|
|
506
770
|
|
|
507
|
-
// NON-ISOLATION MODE: Load safety hooks through an additional per-run settings file. Claude
|
|
508
|
-
// still reads the user's normal config and the repository's project/local config.
|
|
509
|
-
// AskUserQuestion blocking handled via:
|
|
510
|
-
// 1. Prompt injection (see agent-context-builder)
|
|
511
|
-
// 2. PreToolUse hook (defense-in-depth) - activated by ZEROSHOT_BLOCK_ASK_USER env var
|
|
512
771
|
const claudeSettingsPath =
|
|
513
772
|
providerName === 'claude'
|
|
514
773
|
? prepareClaudeSettingsOverlay({
|
|
@@ -516,61 +775,118 @@ async function spawnClaudeTask(agent, context) {
|
|
|
516
775
|
})
|
|
517
776
|
: null;
|
|
518
777
|
|
|
778
|
+
const nested = options.nested === true;
|
|
779
|
+
const { handle, registry } = createExecutionHandle(agent, nested);
|
|
519
780
|
let taskId;
|
|
520
781
|
let pendingLaunch;
|
|
521
782
|
try {
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
783
|
+
try {
|
|
784
|
+
const spawnEnv = buildSpawnEnv(agent, providerName, modelSpec, {
|
|
785
|
+
claudeSettingsPath,
|
|
786
|
+
disableTools: options.disableTools === true,
|
|
787
|
+
formatterAgentName: options.formatterAgentName,
|
|
788
|
+
});
|
|
789
|
+
taskId = await spawnTaskProcess({
|
|
790
|
+
agent,
|
|
791
|
+
ctPath,
|
|
792
|
+
args,
|
|
793
|
+
cwd,
|
|
794
|
+
spawnEnv,
|
|
795
|
+
handle,
|
|
796
|
+
nested,
|
|
797
|
+
});
|
|
798
|
+
pendingLaunch = nested ? handle : agent.currentTask;
|
|
799
|
+
} catch (error) {
|
|
800
|
+
cleanupCallerOwnedCommand(error, () => cleanupClaudeSettingsOverlay(claudeSettingsPath));
|
|
801
|
+
throw error;
|
|
802
|
+
}
|
|
535
803
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
804
|
+
if (!nested) {
|
|
805
|
+
agent._log(`📋 Agent ${agent.id}: Following zeroshot logs for ${taskId}`);
|
|
806
|
+
}
|
|
807
|
+
await waitForTaskReady(agent, taskId);
|
|
808
|
+
if (pendingLaunch?.cancelled || pendingLaunch?.isCancelled) {
|
|
809
|
+
throw nested
|
|
810
|
+
? createNestedCancellationError(handle)
|
|
811
|
+
: new Error(`Task launch cancelled: ${taskId}`);
|
|
812
|
+
}
|
|
542
813
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
814
|
+
const MAX_PID_POLLS = 30;
|
|
815
|
+
const PID_POLL_DELAY = 100;
|
|
816
|
+
let realPid = null;
|
|
817
|
+
let terminalBeforePidObservation = false;
|
|
547
818
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
819
|
+
for (let i = 0; i < MAX_PID_POLLS; i++) {
|
|
820
|
+
const taskInfo = getTask(taskId);
|
|
821
|
+
if (taskInfo?.pid) {
|
|
822
|
+
realPid = taskInfo.pid;
|
|
823
|
+
break;
|
|
824
|
+
}
|
|
825
|
+
if (taskInfo && TASK_TERMINAL_STATUSES.has(taskInfo.status)) {
|
|
826
|
+
terminalBeforePidObservation = true;
|
|
827
|
+
break;
|
|
828
|
+
}
|
|
829
|
+
await new Promise((resolve) => setTimeout(resolve, PID_POLL_DELAY));
|
|
557
830
|
}
|
|
558
|
-
await new Promise((r) => setTimeout(r, PID_POLL_DELAY));
|
|
559
|
-
}
|
|
560
831
|
|
|
561
|
-
|
|
832
|
+
if (pendingLaunch?.cancelled || pendingLaunch?.isCancelled) {
|
|
833
|
+
throw nested
|
|
834
|
+
? createNestedCancellationError(handle)
|
|
835
|
+
: new Error(`Task launch cancelled: ${taskId}`);
|
|
836
|
+
}
|
|
562
837
|
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
838
|
+
if (realPid) {
|
|
839
|
+
handle.assignPid(realPid);
|
|
840
|
+
if (!nested) {
|
|
841
|
+
agent.processPid = realPid;
|
|
842
|
+
agent._publishLifecycle('PROCESS_SPAWNED', { pid: realPid });
|
|
843
|
+
agent._log(`📋 Agent ${agent.id}: Process PID: ${realPid}`);
|
|
844
|
+
}
|
|
845
|
+
} else if (!nested && terminalBeforePidObservation) {
|
|
846
|
+
agent._log(`📋 Agent ${agent.id}: Task finished before PID observation`);
|
|
847
|
+
} else if (!nested) {
|
|
848
|
+
agent._log(`⚠️ Agent ${agent.id}: PID not available (task may use non-standard watcher)`);
|
|
849
|
+
}
|
|
850
|
+
const result = await followClaudeTaskLogs(agent, taskId, {
|
|
851
|
+
...options,
|
|
852
|
+
executionHandle: nested ? handle : null,
|
|
853
|
+
});
|
|
854
|
+
if (nested && !result.success && !handle.isCancelled) {
|
|
855
|
+
const failure = new Error(result.error || `Nested task ${taskId} failed`);
|
|
856
|
+
await terminateNestedSetupFailure(handle, failure);
|
|
857
|
+
if (failure.permanent) throw failure;
|
|
858
|
+
return result;
|
|
859
|
+
}
|
|
860
|
+
if (nested && handle.isCancelled) {
|
|
861
|
+
await handle.waitForCancellation();
|
|
862
|
+
throw createNestedCancellationError(handle);
|
|
863
|
+
}
|
|
864
|
+
return result;
|
|
865
|
+
} catch (error) {
|
|
866
|
+
if (error.terminationExhausted === true && error.retainTaskHandle === true) {
|
|
867
|
+
throw error;
|
|
868
|
+
}
|
|
869
|
+
if (
|
|
870
|
+
nested &&
|
|
871
|
+
handle.isCancelled &&
|
|
872
|
+
handle.cancelDetails.code !== 'NESTED_SETUP_FAILED'
|
|
873
|
+
) {
|
|
874
|
+
const termination = await handle.waitForCancellation();
|
|
875
|
+
const cancellationError = createNestedCancellationError(handle);
|
|
876
|
+
if (!isTerminationConfirmed(termination)) {
|
|
877
|
+
throw retainUnconfirmedNestedTermination(handle, cancellationError, termination);
|
|
878
|
+
}
|
|
879
|
+
throw cancellationError;
|
|
880
|
+
}
|
|
881
|
+
if (nested && taskId) {
|
|
882
|
+
await terminateNestedSetupFailure(handle, error);
|
|
883
|
+
}
|
|
884
|
+
throw error;
|
|
885
|
+
} finally {
|
|
886
|
+
if (nested) {
|
|
887
|
+
await settleRegisteredNestedHandle(registry, handle);
|
|
888
|
+
}
|
|
571
889
|
}
|
|
572
|
-
|
|
573
|
-
return followClaudeTaskLogs(agent, taskId);
|
|
574
890
|
}
|
|
575
891
|
|
|
576
892
|
function resolveAgentModelSpec(agent) {
|
|
@@ -740,7 +1056,7 @@ function buildSpawnEnv(agent, providerName, modelSpec, options = {}) {
|
|
|
740
1056
|
worktreePath: agent.worktree?.path || null,
|
|
741
1057
|
});
|
|
742
1058
|
|
|
743
|
-
return spawnEnv;
|
|
1059
|
+
return applyOpenCodeToolBoundary(spawnEnv, providerName, options);
|
|
744
1060
|
}
|
|
745
1061
|
|
|
746
1062
|
function parseTaskIdFromOutput(stdout) {
|
|
@@ -748,8 +1064,6 @@ function parseTaskIdFromOutput(stdout) {
|
|
|
748
1064
|
return match ? match[1] : null;
|
|
749
1065
|
}
|
|
750
1066
|
|
|
751
|
-
const TASK_TERMINAL_STATUSES = new Set(['completed', 'failed', 'killed', 'stale']);
|
|
752
|
-
|
|
753
1067
|
function assignDurableTaskId(agent, taskId) {
|
|
754
1068
|
if (!taskId || agent.currentTaskId === taskId) return;
|
|
755
1069
|
agent.currentTaskId = taskId;
|
|
@@ -766,6 +1080,7 @@ function createPendingTaskLaunchHandle({
|
|
|
766
1080
|
findPersistedTaskId,
|
|
767
1081
|
waitForWrapperClose,
|
|
768
1082
|
isWrapperClosed,
|
|
1083
|
+
assignTaskId = (taskId) => assignDurableTaskId(agent, taskId),
|
|
769
1084
|
}) {
|
|
770
1085
|
let cancellation = null;
|
|
771
1086
|
const handle = {
|
|
@@ -780,13 +1095,21 @@ function createPendingTaskLaunchHandle({
|
|
|
780
1095
|
let commandAttempted = false;
|
|
781
1096
|
const cancelTask = async (persistedTaskId) => {
|
|
782
1097
|
commandAttempted = true;
|
|
783
|
-
|
|
1098
|
+
assignTaskId(persistedTaskId);
|
|
784
1099
|
try {
|
|
785
1100
|
await runCommandWithTimeout(ctPath, ['kill', persistedTaskId], { timeout: 10000 });
|
|
786
1101
|
} catch (error) {
|
|
787
1102
|
commandError = error;
|
|
788
1103
|
}
|
|
789
1104
|
};
|
|
1105
|
+
const findLateTaskId = async () => {
|
|
1106
|
+
for (let attempt = 0; attempt < 10; attempt++) {
|
|
1107
|
+
const persistedTaskId = findPersistedTaskId();
|
|
1108
|
+
if (persistedTaskId) return persistedTaskId;
|
|
1109
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
1110
|
+
}
|
|
1111
|
+
return null;
|
|
1112
|
+
};
|
|
790
1113
|
if (taskId) await cancelTask(taskId);
|
|
791
1114
|
|
|
792
1115
|
if (!isWrapperClosed()) {
|
|
@@ -794,7 +1117,7 @@ function createPendingTaskLaunchHandle({
|
|
|
794
1117
|
await waitForWrapperClose;
|
|
795
1118
|
}
|
|
796
1119
|
|
|
797
|
-
taskId = taskId ||
|
|
1120
|
+
taskId = taskId || (await findLateTaskId());
|
|
798
1121
|
if (taskId && !commandAttempted) await cancelTask(taskId);
|
|
799
1122
|
if (taskId && commandError === null) {
|
|
800
1123
|
const task = getTask(taskId);
|
|
@@ -833,7 +1156,16 @@ function createPendingTaskLaunchHandle({
|
|
|
833
1156
|
return handle;
|
|
834
1157
|
}
|
|
835
1158
|
|
|
836
|
-
function spawnTaskProcess({
|
|
1159
|
+
function spawnTaskProcess({
|
|
1160
|
+
agent,
|
|
1161
|
+
ctPath,
|
|
1162
|
+
args,
|
|
1163
|
+
cwd,
|
|
1164
|
+
spawnEnv,
|
|
1165
|
+
spawnTimeoutMs = 30000,
|
|
1166
|
+
handle = null,
|
|
1167
|
+
nested = false,
|
|
1168
|
+
}) {
|
|
837
1169
|
// Timeout for spawn phase - if CLI hangs during init (e.g., opencode 429 bug), kill it.
|
|
838
1170
|
const SPAWN_TIMEOUT_MS = spawnTimeoutMs;
|
|
839
1171
|
// spawn() throws on null bytes in argv; strip them before they get there.
|
|
@@ -849,6 +1181,9 @@ function spawnTaskProcess({ agent, ctPath, args, cwd, spawnEnv, spawnTimeoutMs =
|
|
|
849
1181
|
windowsHide: true,
|
|
850
1182
|
});
|
|
851
1183
|
|
|
1184
|
+
// Nested launches retain their own immutable process identity instead of
|
|
1185
|
+
// overwriting the parent agent's lifecycle fields.
|
|
1186
|
+
handle?.attachProcess(proc);
|
|
852
1187
|
let wrapperClosed = false;
|
|
853
1188
|
let resolveWrapperClose;
|
|
854
1189
|
const waitForWrapperClose = new Promise((resolveClose) => {
|
|
@@ -868,8 +1203,14 @@ function spawnTaskProcess({ agent, ctPath, args, cwd, spawnEnv, spawnTimeoutMs =
|
|
|
868
1203
|
findPersistedTaskId,
|
|
869
1204
|
waitForWrapperClose,
|
|
870
1205
|
isWrapperClosed: () => wrapperClosed,
|
|
1206
|
+
assignTaskId: nested
|
|
1207
|
+
? (taskId) => handle?.assignTaskId(taskId)
|
|
1208
|
+
: (taskId) => assignDurableTaskId(agent, taskId),
|
|
871
1209
|
});
|
|
872
|
-
|
|
1210
|
+
if (nested) {
|
|
1211
|
+
handle.setCancelAction((reason) => pendingLaunch.kill(reason));
|
|
1212
|
+
}
|
|
1213
|
+
if (!nested) agent.currentTask = pendingLaunch;
|
|
873
1214
|
|
|
874
1215
|
// NOTE: Don't emit PROCESS_SPAWNED here - proc.pid is a wrapper that exits immediately.
|
|
875
1216
|
// Real PID comes from task store after watcher spawns the actual CLI process.
|
|
@@ -882,7 +1223,10 @@ function spawnTaskProcess({ agent, ctPath, args, cwd, spawnEnv, spawnTimeoutMs =
|
|
|
882
1223
|
const classifyCleanupOwnership = trackTaskWrapperCleanupOwnership(findPersistedTaskId);
|
|
883
1224
|
const rejectWithOwnership = async (error) => {
|
|
884
1225
|
const classifiedError = classifyCleanupOwnership(error);
|
|
885
|
-
if (
|
|
1226
|
+
if (
|
|
1227
|
+
!callerOwnsCommandCleanup(classifiedError) &&
|
|
1228
|
+
(nested || agent.currentTask === pendingLaunch)
|
|
1229
|
+
) {
|
|
886
1230
|
let termination;
|
|
887
1231
|
try {
|
|
888
1232
|
termination = await pendingLaunch.kill(classifiedError.message);
|
|
@@ -896,15 +1240,16 @@ function spawnTaskProcess({ agent, ctPath, args, cwd, spawnEnv, spawnTimeoutMs =
|
|
|
896
1240
|
classifiedError.restartExhausted = true;
|
|
897
1241
|
classifiedError.terminationExhausted = true;
|
|
898
1242
|
classifiedError.terminationAttempts = 1;
|
|
899
|
-
|
|
900
|
-
|
|
1243
|
+
if (nested) handle?.retainOwnership();
|
|
1244
|
+
classifiedError.taskId = nested ? handle?.taskId || null : agent.currentTaskId || null;
|
|
1245
|
+
} else if (!nested && agent.currentTask === pendingLaunch) {
|
|
901
1246
|
agent.currentTask = null;
|
|
902
1247
|
agent.currentTaskId = null;
|
|
903
1248
|
agent.processPid = null;
|
|
904
1249
|
agent.lastOutputTime = null;
|
|
905
1250
|
agent.taskStartedAt = null;
|
|
906
1251
|
}
|
|
907
|
-
} else if (wrapperClosed && agent.currentTask === pendingLaunch) {
|
|
1252
|
+
} else if (!nested && wrapperClosed && agent.currentTask === pendingLaunch) {
|
|
908
1253
|
agent.currentTask = null;
|
|
909
1254
|
}
|
|
910
1255
|
reject(classifiedError);
|
|
@@ -958,10 +1303,11 @@ function spawnTaskProcess({ agent, ctPath, args, cwd, spawnEnv, spawnTimeoutMs =
|
|
|
958
1303
|
return;
|
|
959
1304
|
}
|
|
960
1305
|
|
|
961
|
-
|
|
1306
|
+
handle?.assignTaskId(spawnedTaskId);
|
|
1307
|
+
if (!nested) assignDurableTaskId(agent, spawnedTaskId);
|
|
962
1308
|
|
|
963
|
-
// Start liveness monitoring
|
|
964
|
-
if (agent.enableLivenessCheck) {
|
|
1309
|
+
// Start liveness monitoring for top-level tasks only.
|
|
1310
|
+
if (!nested && agent.enableLivenessCheck) {
|
|
965
1311
|
agent.taskStartedAt = Date.now();
|
|
966
1312
|
agent.lastOutputTime = agent.taskStartedAt;
|
|
967
1313
|
agent._startLivenessCheck();
|
|
@@ -988,7 +1334,7 @@ function spawnTaskProcess({ agent, ctPath, args, cwd, spawnEnv, spawnTimeoutMs =
|
|
|
988
1334
|
* @returns {Promise<void>}
|
|
989
1335
|
*/
|
|
990
1336
|
async function waitForTaskReady(agent, taskId, maxRetries = 10, delayMs = 200) {
|
|
991
|
-
const ctPath = getClaudeTasksPath();
|
|
1337
|
+
const ctPath = agent.taskCliPath || getClaudeTasksPath();
|
|
992
1338
|
|
|
993
1339
|
for (let i = 0; i < maxRetries; i++) {
|
|
994
1340
|
let exists = false;
|
|
@@ -1085,7 +1431,9 @@ function broadcastAgentLine({ agent, providerName, state, line }) {
|
|
|
1085
1431
|
const isValidJson = isValidJsonLine(content);
|
|
1086
1432
|
state.output += content + '\n';
|
|
1087
1433
|
|
|
1088
|
-
|
|
1434
|
+
if (!state.nested) {
|
|
1435
|
+
agent.lastOutputTime = Date.now();
|
|
1436
|
+
}
|
|
1089
1437
|
|
|
1090
1438
|
agent._publish({
|
|
1091
1439
|
topic: 'AGENT_OUTPUT',
|
|
@@ -1199,10 +1547,19 @@ async function evaluateStructuredSuccess({ agent, taskId, state, success }) {
|
|
|
1199
1547
|
if (!success || !requiresStructuredResult(agent)) {
|
|
1200
1548
|
return { success, error: null };
|
|
1201
1549
|
}
|
|
1550
|
+
// Short-circuit: if a previous pass already validated and cached the parsed
|
|
1551
|
+
// result (e.g. recovery model call), reuse it without a second parse.
|
|
1552
|
+
if (state._cachedParsedResult) {
|
|
1553
|
+
return { success: true, error: null };
|
|
1554
|
+
}
|
|
1202
1555
|
try {
|
|
1203
|
-
|
|
1556
|
+
// Cache the validated parsed object so completion hooks and {{result.*}}
|
|
1557
|
+
// substitution consume it directly instead of re-parsing (which could
|
|
1558
|
+
// trigger a second recovery model call).
|
|
1559
|
+
state._cachedParsedResult = await agent._parseResultOutput(state.output);
|
|
1204
1560
|
return { success: true, error: null };
|
|
1205
1561
|
} catch (error) {
|
|
1562
|
+
if (isNestedLifecycleError(error)) throw error;
|
|
1206
1563
|
const errorContext = sanitizeErrorMessage(error.message);
|
|
1207
1564
|
console.warn(
|
|
1208
1565
|
`[Agent ${agent.id}] Task ${taskId} reported completed but produced invalid structured output; ` +
|
|
@@ -1238,7 +1595,9 @@ async function buildCompletionResult({
|
|
|
1238
1595
|
success,
|
|
1239
1596
|
taskInfo = getTask(taskId),
|
|
1240
1597
|
}) {
|
|
1241
|
-
const classified =
|
|
1598
|
+
const classified = state.skipStructuredResultCheck
|
|
1599
|
+
? { success, error: null }
|
|
1600
|
+
: await evaluateStructuredSuccess({ agent, taskId, state, success });
|
|
1242
1601
|
const resumeIdentityError = classified.success ? validateCompletedResumeIdentity(taskInfo) : null;
|
|
1243
1602
|
if (resumeIdentityError) {
|
|
1244
1603
|
classified.success = false;
|
|
@@ -1261,6 +1620,9 @@ async function buildCompletionResult({
|
|
|
1261
1620
|
return {
|
|
1262
1621
|
success: classified.success,
|
|
1263
1622
|
output: state.output,
|
|
1623
|
+
// Carry the validated parsed object (from recovery or direct extraction)
|
|
1624
|
+
// so downstream hooks and {{result.*}} substitution never re-parse.
|
|
1625
|
+
parsedResult: state._cachedParsedResult || null,
|
|
1264
1626
|
error: errorContext,
|
|
1265
1627
|
tokenUsage: extractTokenUsage(state.output, providerName),
|
|
1266
1628
|
providerSession: providerSessionFromCompletedTask({
|
|
@@ -1280,7 +1642,9 @@ function finalizeLogFollow(agent, state) {
|
|
|
1280
1642
|
if (state.statusCheckInterval) {
|
|
1281
1643
|
clearInterval(state.statusCheckInterval);
|
|
1282
1644
|
}
|
|
1283
|
-
|
|
1645
|
+
if (!state.nested) {
|
|
1646
|
+
agent.currentTask = null;
|
|
1647
|
+
}
|
|
1284
1648
|
}
|
|
1285
1649
|
|
|
1286
1650
|
function handleStatusExecError({ agent, state, ctPath, taskId, error, stderr, resolve }) {
|
|
@@ -1402,6 +1766,7 @@ function handleStatusCompletion({
|
|
|
1402
1766
|
stdout,
|
|
1403
1767
|
pollLogFile,
|
|
1404
1768
|
resolve,
|
|
1769
|
+
reject,
|
|
1405
1770
|
}) {
|
|
1406
1771
|
const cleanStdout = stripAnsiCodes(stdout);
|
|
1407
1772
|
const { isCompleted, isFailed, isStale, isKilled } = parseStatusFlags(cleanStdout);
|
|
@@ -1438,6 +1803,10 @@ function handleStatusCompletion({
|
|
|
1438
1803
|
})
|
|
1439
1804
|
.then(resolve)
|
|
1440
1805
|
.catch((error) => {
|
|
1806
|
+
if (isNestedLifecycleError(error)) {
|
|
1807
|
+
reject(error);
|
|
1808
|
+
return;
|
|
1809
|
+
}
|
|
1441
1810
|
resolve({
|
|
1442
1811
|
success: false,
|
|
1443
1812
|
output: state.output,
|
|
@@ -1456,7 +1825,9 @@ function buildKillHandler({ agent, taskId, state, providerName, resolve }) {
|
|
|
1456
1825
|
if (state.resolved) return;
|
|
1457
1826
|
state.resolved = true;
|
|
1458
1827
|
finalizeLogFollow(agent, state);
|
|
1459
|
-
|
|
1828
|
+
if (!state.nested) {
|
|
1829
|
+
agent._stopLivenessCheck();
|
|
1830
|
+
}
|
|
1460
1831
|
resolve({
|
|
1461
1832
|
success: false,
|
|
1462
1833
|
output: state.output,
|
|
@@ -1469,9 +1840,20 @@ function buildKillHandler({ agent, taskId, state, providerName, resolve }) {
|
|
|
1469
1840
|
};
|
|
1470
1841
|
}
|
|
1471
1842
|
|
|
1472
|
-
function createLogFollower({
|
|
1473
|
-
|
|
1843
|
+
function createLogFollower({
|
|
1844
|
+
agent,
|
|
1845
|
+
taskId,
|
|
1846
|
+
fsModule,
|
|
1847
|
+
ctPath,
|
|
1848
|
+
providerName,
|
|
1849
|
+
skipStructuredResultCheck = false,
|
|
1850
|
+
nested = false,
|
|
1851
|
+
executionHandle = null,
|
|
1852
|
+
}) {
|
|
1853
|
+
return new Promise((resolve, reject) => {
|
|
1474
1854
|
const state = createLogFollowState();
|
|
1855
|
+
state.skipStructuredResultCheck = skipStructuredResultCheck;
|
|
1856
|
+
state.nested = nested;
|
|
1475
1857
|
|
|
1476
1858
|
state.logFilePath = lookupLogFilePath(ctPath, taskId);
|
|
1477
1859
|
if (state.logFilePath) {
|
|
@@ -1516,12 +1898,34 @@ function createLogFollower({ agent, taskId, fsModule, ctPath, providerName }) {
|
|
|
1516
1898
|
stdout,
|
|
1517
1899
|
pollLogFile,
|
|
1518
1900
|
resolve,
|
|
1901
|
+
reject,
|
|
1519
1902
|
});
|
|
1520
1903
|
}
|
|
1521
1904
|
);
|
|
1522
1905
|
}, 1000);
|
|
1523
1906
|
|
|
1524
|
-
|
|
1907
|
+
const killHandler = buildKillHandler({ agent, taskId, state, providerName, resolve });
|
|
1908
|
+
if (nested && executionHandle) {
|
|
1909
|
+
executionHandle.setFailClosedAction((error) => {
|
|
1910
|
+
if (state.resolved) return;
|
|
1911
|
+
state.resolved = true;
|
|
1912
|
+
finalizeLogFollow(agent, state);
|
|
1913
|
+
reject(error);
|
|
1914
|
+
});
|
|
1915
|
+
let cancelPendingLaunch;
|
|
1916
|
+
const cancelExecution = async (reason, details) => {
|
|
1917
|
+
const termination = cancelPendingLaunch
|
|
1918
|
+
? await cancelPendingLaunch(reason, details)
|
|
1919
|
+
: { forced: true, taskId };
|
|
1920
|
+
if (isTerminationConfirmed(termination)) {
|
|
1921
|
+
killHandler.kill(reason, details);
|
|
1922
|
+
}
|
|
1923
|
+
return termination;
|
|
1924
|
+
};
|
|
1925
|
+
cancelPendingLaunch = executionHandle.setCancelAction(cancelExecution);
|
|
1926
|
+
} else {
|
|
1927
|
+
agent.currentTask = killHandler;
|
|
1928
|
+
}
|
|
1525
1929
|
});
|
|
1526
1930
|
}
|
|
1527
1931
|
|
|
@@ -1532,12 +1936,21 @@ function createLogFollower({ agent, taskId, fsModule, ctPath, providerName }) {
|
|
|
1532
1936
|
* @param {String} taskId - Task ID to follow
|
|
1533
1937
|
* @returns {Promise<Object>} Result object { success, output, error }
|
|
1534
1938
|
*/
|
|
1535
|
-
function followClaudeTaskLogs(agent, taskId) {
|
|
1939
|
+
function followClaudeTaskLogs(agent, taskId, options = {}) {
|
|
1536
1940
|
const fsModule = require('fs');
|
|
1537
1941
|
const ctPath = agent.taskCliPath || getClaudeTasksPath();
|
|
1538
1942
|
const providerName = agent._resolveProvider ? agent._resolveProvider() : 'claude';
|
|
1539
1943
|
|
|
1540
|
-
return createLogFollower({
|
|
1944
|
+
return createLogFollower({
|
|
1945
|
+
agent,
|
|
1946
|
+
taskId,
|
|
1947
|
+
fsModule,
|
|
1948
|
+
ctPath,
|
|
1949
|
+
providerName,
|
|
1950
|
+
skipStructuredResultCheck: options.skipStructuredResultCheck === true,
|
|
1951
|
+
nested: options.nested === true,
|
|
1952
|
+
executionHandle: options.executionHandle || null,
|
|
1953
|
+
});
|
|
1541
1954
|
}
|
|
1542
1955
|
|
|
1543
1956
|
// Cache zeroshot path at module load time (when PATH is correct)
|
|
@@ -1575,11 +1988,53 @@ function getClaudeTasksPath() {
|
|
|
1575
1988
|
* Runs Claude CLI inside the container for full isolation
|
|
1576
1989
|
* @param {Object} agent - Agent instance
|
|
1577
1990
|
* @param {String} context - Context to pass to Claude
|
|
1991
|
+
* @param {{skipStructuredResultCheck?: boolean}} [options] - Internal nested-task controls
|
|
1578
1992
|
* @returns {Promise<Object>} Result object { success, output, error }
|
|
1579
1993
|
*/
|
|
1580
|
-
async function spawnClaudeTaskIsolated(agent, context) {
|
|
1994
|
+
async function spawnClaudeTaskIsolated(agent, context, options = {}) {
|
|
1995
|
+
const nested = options.nested === true;
|
|
1996
|
+
if (!nested) {
|
|
1997
|
+
return spawnClaudeTaskIsolatedExecution(agent, context, options);
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
const { handle, registry } = createExecutionHandle(agent, true);
|
|
2001
|
+
try {
|
|
2002
|
+
const result = await spawnClaudeTaskIsolatedExecution(agent, context, {
|
|
2003
|
+
...options,
|
|
2004
|
+
executionHandle: handle,
|
|
2005
|
+
});
|
|
2006
|
+
if (!result.success && !handle.isCancelled) {
|
|
2007
|
+
const failure = new Error(result.error || `Nested isolated task ${handle.taskId} failed`);
|
|
2008
|
+
await terminateNestedSetupFailure(handle, failure);
|
|
2009
|
+
if (failure.permanent) throw failure;
|
|
2010
|
+
return result;
|
|
2011
|
+
}
|
|
2012
|
+
if (handle.isCancelled) {
|
|
2013
|
+
await handle.waitForCancellation();
|
|
2014
|
+
throw createNestedCancellationError(handle);
|
|
2015
|
+
}
|
|
2016
|
+
return result;
|
|
2017
|
+
} catch (error) {
|
|
2018
|
+
if (error.terminationExhausted === true && error.retainTaskHandle === true) {
|
|
2019
|
+
throw error;
|
|
2020
|
+
}
|
|
2021
|
+
if (handle.isCancelled && handle.cancelDetails.code !== 'NESTED_SETUP_FAILED') {
|
|
2022
|
+
await handle.waitForCancellation();
|
|
2023
|
+
throw createNestedCancellationError(handle);
|
|
2024
|
+
}
|
|
2025
|
+
if (handle.taskId) {
|
|
2026
|
+
await terminateNestedSetupFailure(handle, error);
|
|
2027
|
+
}
|
|
2028
|
+
throw error;
|
|
2029
|
+
} finally {
|
|
2030
|
+
await settleRegisteredNestedHandle(registry, handle);
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
async function spawnClaudeTaskIsolatedExecution(agent, context, options = {}) {
|
|
1581
2035
|
const { manager, clusterId } = agent.isolation;
|
|
1582
2036
|
const providerName = agent._resolveProvider ? agent._resolveProvider() : 'claude';
|
|
2037
|
+
options = ensureFormatterLaunchOptions(providerName, options);
|
|
1583
2038
|
const modelSpec = resolveAgentModelSpec(agent);
|
|
1584
2039
|
const modelSpecSource = agent._resolveModelSpecSource
|
|
1585
2040
|
? agent._resolveModelSpecSource()
|
|
@@ -1619,16 +2074,36 @@ async function spawnClaudeTaskIsolated(agent, context) {
|
|
|
1619
2074
|
const ownershipToken = createTaskSpawnOwnershipToken();
|
|
1620
2075
|
// Auth env vars are injected by IsolationManager; the launch token is the
|
|
1621
2076
|
// only authoritative bridge back to the detached task row in the container.
|
|
1622
|
-
const
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
2077
|
+
const effectiveOpenCodeConfig =
|
|
2078
|
+
options.disableTools === true
|
|
2079
|
+
? await resolveIsolatedOpenCodeConfigUnderOwnership({
|
|
2080
|
+
manager,
|
|
2081
|
+
clusterId,
|
|
2082
|
+
providerName,
|
|
2083
|
+
executionHandle: options.executionHandle,
|
|
2084
|
+
})
|
|
2085
|
+
: null;
|
|
2086
|
+
const isolatedEnv = applyOpenCodeToolBoundary(
|
|
2087
|
+
{
|
|
2088
|
+
...(providerName === 'claude' ? buildClaudeEnv(modelSpec, { includeAuth: false }) : {}),
|
|
2089
|
+
...(effectiveOpenCodeConfig
|
|
2090
|
+
? { [OPENCODE_CONFIG_CONTENT_ENV]: effectiveOpenCodeConfig }
|
|
2091
|
+
: {}),
|
|
2092
|
+
[TASK_SPAWN_OWNERSHIP_TOKEN_ENV]: ownershipToken,
|
|
2093
|
+
},
|
|
2094
|
+
providerName,
|
|
2095
|
+
options
|
|
2096
|
+
);
|
|
1626
2097
|
|
|
2098
|
+
if (options.executionHandle?.isCancelled) {
|
|
2099
|
+
throw createNestedCancellationError(options.executionHandle);
|
|
2100
|
+
}
|
|
1627
2101
|
let isolatedPendingLaunch = null;
|
|
1628
2102
|
const taskId = await new Promise((resolve, reject) => {
|
|
1629
2103
|
const proc = manager.spawnInContainer(clusterId, command, {
|
|
1630
2104
|
env: isolatedEnv,
|
|
1631
2105
|
});
|
|
2106
|
+
options.executionHandle?.attachProcess(proc);
|
|
1632
2107
|
|
|
1633
2108
|
let isolatedTaskId = null;
|
|
1634
2109
|
let wrapperClosed = false;
|
|
@@ -1656,10 +2131,19 @@ async function spawnClaudeTaskIsolated(agent, context) {
|
|
|
1656
2131
|
);
|
|
1657
2132
|
if (persistedTaskId) {
|
|
1658
2133
|
isolatedTaskId = persistedTaskId;
|
|
1659
|
-
|
|
2134
|
+
options.executionHandle?.assignTaskId(persistedTaskId);
|
|
2135
|
+
if (!options.nested) assignDurableTaskId(agent, persistedTaskId);
|
|
1660
2136
|
}
|
|
1661
2137
|
return persistedTaskId;
|
|
1662
2138
|
};
|
|
2139
|
+
const findLatePersistedTaskId = async () => {
|
|
2140
|
+
for (let attempt = 0; attempt < 10; attempt++) {
|
|
2141
|
+
const persistedTaskId = await findPersistedTaskId();
|
|
2142
|
+
if (persistedTaskId) return persistedTaskId;
|
|
2143
|
+
await new Promise((resolveDelay) => setTimeout(resolveDelay, 50));
|
|
2144
|
+
}
|
|
2145
|
+
return null;
|
|
2146
|
+
};
|
|
1663
2147
|
const rejectLaunch = (error, { retainHandle = false } = {}) => {
|
|
1664
2148
|
if (resolved) return;
|
|
1665
2149
|
resolved = true;
|
|
@@ -1671,8 +2155,10 @@ async function spawnClaudeTaskIsolated(agent, context) {
|
|
|
1671
2155
|
rejection.restartExhausted = true;
|
|
1672
2156
|
rejection.terminationExhausted = true;
|
|
1673
2157
|
rejection.terminationAttempts = 1;
|
|
1674
|
-
|
|
1675
|
-
|
|
2158
|
+
if (options.nested) options.executionHandle?.retainOwnership();
|
|
2159
|
+
rejection.taskId =
|
|
2160
|
+
isolatedTaskId || (!options.nested ? agent.currentTaskId : null) || null;
|
|
2161
|
+
} else if (!options.nested && agent.currentTask === isolatedPendingLaunch) {
|
|
1676
2162
|
agent.currentTask = null;
|
|
1677
2163
|
}
|
|
1678
2164
|
reject(rejection);
|
|
@@ -1704,7 +2190,8 @@ async function spawnClaudeTaskIsolated(agent, context) {
|
|
|
1704
2190
|
}
|
|
1705
2191
|
|
|
1706
2192
|
try {
|
|
1707
|
-
const persistedTaskId =
|
|
2193
|
+
const persistedTaskId =
|
|
2194
|
+
isolatedTaskId || (await findLatePersistedTaskId());
|
|
1708
2195
|
if (persistedTaskId && !termination) {
|
|
1709
2196
|
termination = await terminateIsolatedTask(manager, clusterId, persistedTaskId);
|
|
1710
2197
|
commandError = null;
|
|
@@ -1737,11 +2224,14 @@ async function spawnClaudeTaskIsolated(agent, context) {
|
|
|
1737
2224
|
return termination;
|
|
1738
2225
|
},
|
|
1739
2226
|
};
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
2227
|
+
if (options.nested && options.executionHandle) {
|
|
2228
|
+
options.executionHandle.setCancelAction((reason) => isolatedPendingLaunch.kill(reason));
|
|
2229
|
+
}
|
|
2230
|
+
if (!options.nested) {
|
|
2231
|
+
agent.currentTask = isolatedPendingLaunch;
|
|
2232
|
+
agent.processPid = proc.pid;
|
|
2233
|
+
agent._publishLifecycle('PROCESS_SPAWNED', { pid: proc.pid });
|
|
2234
|
+
}
|
|
1745
2235
|
|
|
1746
2236
|
// CRITICAL: Timeout to prevent infinite hang if provider CLI hangs. Timeout
|
|
1747
2237
|
// uses the same cancellation path so a durable child cannot outlive it.
|
|
@@ -1784,7 +2274,8 @@ async function spawnClaudeTaskIsolated(agent, context) {
|
|
|
1784
2274
|
persistedTaskId,
|
|
1785
2275
|
});
|
|
1786
2276
|
isolatedTaskId = spawnedTaskId;
|
|
1787
|
-
|
|
2277
|
+
options.executionHandle?.assignTaskId(spawnedTaskId);
|
|
2278
|
+
if (!options.nested) assignDurableTaskId(agent, spawnedTaskId);
|
|
1788
2279
|
resolved = true;
|
|
1789
2280
|
resolve(spawnedTaskId);
|
|
1790
2281
|
} catch (error) {
|
|
@@ -1806,12 +2297,14 @@ async function spawnClaudeTaskIsolated(agent, context) {
|
|
|
1806
2297
|
});
|
|
1807
2298
|
if (isolatedPendingLaunch?.cancelled) throw new Error(`Task launch cancelled: ${taskId}`);
|
|
1808
2299
|
|
|
1809
|
-
|
|
2300
|
+
if (!options.nested) {
|
|
2301
|
+
agent._log(`📋 Agent ${agent.id}: Following zeroshot logs for ${taskId} in container...`);
|
|
2302
|
+
}
|
|
1810
2303
|
|
|
1811
2304
|
// STEP 2: Install the lifecycle-owned handle before liveness monitoring can
|
|
1812
2305
|
// observe the task, then follow the task's log file inside the container.
|
|
1813
|
-
const execution = followClaudeTaskLogsIsolated(agent, taskId);
|
|
1814
|
-
if (agent.enableLivenessCheck) {
|
|
2306
|
+
const execution = followClaudeTaskLogsIsolated(agent, taskId, options);
|
|
2307
|
+
if (!options.nested && agent.enableLivenessCheck) {
|
|
1815
2308
|
agent.taskStartedAt = Date.now();
|
|
1816
2309
|
agent.lastOutputTime = agent.taskStartedAt;
|
|
1817
2310
|
agent._startLivenessCheck();
|
|
@@ -1842,11 +2335,13 @@ async function spawnClaudeTaskIsolated(agent, context) {
|
|
|
1842
2335
|
* - Status checks reduced to every 2 seconds (not every poll)
|
|
1843
2336
|
* - Result: 10-20% overall latency reduction
|
|
1844
2337
|
*/
|
|
1845
|
-
function createIsolatedLogState() {
|
|
2338
|
+
function createIsolatedLogState(skipStructuredResultCheck = false, nested = false) {
|
|
1846
2339
|
return {
|
|
1847
2340
|
taskExited: false,
|
|
1848
2341
|
resolved: false,
|
|
1849
2342
|
terminationPromise: null,
|
|
2343
|
+
durableTaskTerminal: false,
|
|
2344
|
+
durableTaskStatus: null,
|
|
1850
2345
|
lifecycleHandle: null,
|
|
1851
2346
|
logFilePath: null,
|
|
1852
2347
|
fullOutput: '',
|
|
@@ -1854,6 +2349,8 @@ function createIsolatedLogState() {
|
|
|
1854
2349
|
statusCheckInterval: null,
|
|
1855
2350
|
timeoutTimer: null,
|
|
1856
2351
|
lineBuffer: '',
|
|
2352
|
+
skipStructuredResultCheck,
|
|
2353
|
+
nested,
|
|
1857
2354
|
};
|
|
1858
2355
|
}
|
|
1859
2356
|
|
|
@@ -1879,6 +2376,7 @@ function buildIsolatedCleanup(state) {
|
|
|
1879
2376
|
}
|
|
1880
2377
|
|
|
1881
2378
|
function clearIsolatedLifecycleHandle(agent, state) {
|
|
2379
|
+
if (state.nested) return;
|
|
1882
2380
|
if (agent.currentTask === state.lifecycleHandle) {
|
|
1883
2381
|
agent.currentTask = null;
|
|
1884
2382
|
}
|
|
@@ -1952,9 +2450,9 @@ async function terminateIsolatedTask(manager, clusterId, taskId) {
|
|
|
1952
2450
|
}`
|
|
1953
2451
|
);
|
|
1954
2452
|
}
|
|
1955
|
-
|
|
1956
2453
|
return {
|
|
1957
|
-
alreadyTerminal:
|
|
2454
|
+
alreadyTerminal:
|
|
2455
|
+
Boolean(beforeStatus) || Boolean(afterStatus && afterStatus !== 'killed'),
|
|
1958
2456
|
forced: !beforeStatus && afterStatus === 'killed',
|
|
1959
2457
|
status: beforeStatus || afterStatus,
|
|
1960
2458
|
};
|
|
@@ -1995,14 +2493,20 @@ function settleIsolatedTerminalStatus({
|
|
|
1995
2493
|
if (state.terminalSettlementPromise) return state.terminalSettlementPromise;
|
|
1996
2494
|
|
|
1997
2495
|
state.taskExited = true;
|
|
2496
|
+
if (status) {
|
|
2497
|
+
state.durableTaskTerminal = true;
|
|
2498
|
+
state.durableTaskStatus = status;
|
|
2499
|
+
}
|
|
1998
2500
|
const settlement = (async () => {
|
|
1999
2501
|
const logFilePath = await resolveIsolatedLogFilePath(manager, clusterId, taskId, state);
|
|
2000
2502
|
await new Promise((settle) => setTimeout(settle, 200));
|
|
2503
|
+
if (state.resolved) return;
|
|
2001
2504
|
const finalReadResult = await manager.execInContainer(clusterId, [
|
|
2002
2505
|
'sh',
|
|
2003
2506
|
'-c',
|
|
2004
2507
|
`cat "${logFilePath}" 2>/dev/null || echo ""`,
|
|
2005
2508
|
]);
|
|
2509
|
+
if (state.resolved) return;
|
|
2006
2510
|
|
|
2007
2511
|
if (finalReadResult.code === 0 && finalReadResult.stdout) {
|
|
2008
2512
|
state.fullOutput = finalReadResult.stdout;
|
|
@@ -2036,7 +2540,10 @@ function settleIsolatedTerminalStatus({
|
|
|
2036
2540
|
},
|
|
2037
2541
|
})
|
|
2038
2542
|
: null;
|
|
2039
|
-
const parsedResult =
|
|
2543
|
+
const parsedResult =
|
|
2544
|
+
state.skipStructuredResultCheck || vertexModelError
|
|
2545
|
+
? null
|
|
2546
|
+
: await agent._parseResultOutput(state.fullOutput);
|
|
2040
2547
|
|
|
2041
2548
|
settleIsolatedFollower({
|
|
2042
2549
|
agent,
|
|
@@ -2047,7 +2554,7 @@ function settleIsolatedTerminalStatus({
|
|
|
2047
2554
|
success,
|
|
2048
2555
|
output: state.fullOutput,
|
|
2049
2556
|
taskId,
|
|
2050
|
-
|
|
2557
|
+
parsedResult,
|
|
2051
2558
|
error: errorContext,
|
|
2052
2559
|
tokenUsage: extractTokenUsage(state.fullOutput, providerName),
|
|
2053
2560
|
vertexModelError,
|
|
@@ -2072,12 +2579,40 @@ function buildIsolatedLifecycleHandle({
|
|
|
2072
2579
|
reject,
|
|
2073
2580
|
onLine,
|
|
2074
2581
|
}) {
|
|
2582
|
+
const settleCancellation = (reason, details) =>
|
|
2583
|
+
settleIsolatedFollower({
|
|
2584
|
+
agent,
|
|
2585
|
+
state,
|
|
2586
|
+
cleanup,
|
|
2587
|
+
resolve,
|
|
2588
|
+
result: {
|
|
2589
|
+
success: false,
|
|
2590
|
+
output: state.fullOutput,
|
|
2591
|
+
error: reason,
|
|
2592
|
+
code: details.code || null,
|
|
2593
|
+
taskId,
|
|
2594
|
+
tokenUsage: extractTokenUsage(state.fullOutput, providerName),
|
|
2595
|
+
},
|
|
2596
|
+
});
|
|
2075
2597
|
const terminate = (reason = 'Task killed', details = {}) => {
|
|
2076
|
-
if (state.
|
|
2598
|
+
if (state.durableTaskTerminal) {
|
|
2599
|
+
if (state.nested) settleCancellation(reason, details);
|
|
2600
|
+
return Promise.resolve({
|
|
2601
|
+
alreadyTerminal: true,
|
|
2602
|
+
forced: false,
|
|
2603
|
+
status: state.durableTaskStatus,
|
|
2604
|
+
});
|
|
2605
|
+
}
|
|
2077
2606
|
if (state.terminationPromise) return state.terminationPromise;
|
|
2078
2607
|
|
|
2079
2608
|
const terminationPromise = (async () => {
|
|
2080
2609
|
const termination = await terminateIsolatedTask(manager, clusterId, taskId);
|
|
2610
|
+
state.durableTaskTerminal = true;
|
|
2611
|
+
state.durableTaskStatus = termination.status;
|
|
2612
|
+
if (!termination.forced && state.nested) {
|
|
2613
|
+
settleCancellation(reason, details);
|
|
2614
|
+
return termination;
|
|
2615
|
+
}
|
|
2081
2616
|
if (!termination.forced) {
|
|
2082
2617
|
await settleIsolatedTerminalStatus({
|
|
2083
2618
|
agent,
|
|
@@ -2095,20 +2630,7 @@ function buildIsolatedLifecycleHandle({
|
|
|
2095
2630
|
return termination;
|
|
2096
2631
|
}
|
|
2097
2632
|
|
|
2098
|
-
|
|
2099
|
-
agent,
|
|
2100
|
-
state,
|
|
2101
|
-
cleanup,
|
|
2102
|
-
resolve,
|
|
2103
|
-
result: {
|
|
2104
|
-
success: false,
|
|
2105
|
-
output: state.fullOutput,
|
|
2106
|
-
error: reason,
|
|
2107
|
-
code: details.code || null,
|
|
2108
|
-
taskId,
|
|
2109
|
-
tokenUsage: extractTokenUsage(state.fullOutput, providerName),
|
|
2110
|
-
},
|
|
2111
|
-
});
|
|
2633
|
+
settleCancellation(reason, details);
|
|
2112
2634
|
return termination;
|
|
2113
2635
|
})();
|
|
2114
2636
|
state.terminationPromise = terminationPromise;
|
|
@@ -2131,7 +2653,7 @@ function buildIsolatedLifecycleHandle({
|
|
|
2131
2653
|
};
|
|
2132
2654
|
}
|
|
2133
2655
|
|
|
2134
|
-
function broadcastIsolatedLine({ agent, providerName, taskId, line }) {
|
|
2656
|
+
function broadcastIsolatedLine({ agent, providerName, taskId, state, line }) {
|
|
2135
2657
|
const timestampMatch = line.match(/^\[(\d{4}-\d{2}-\d{2}T[^\]]+)\]\s*(.*)$/);
|
|
2136
2658
|
const timestamp = timestampMatch ? new Date(timestampMatch[1]).getTime() : Date.now();
|
|
2137
2659
|
const content = timestampMatch ? timestampMatch[2] : line;
|
|
@@ -2152,7 +2674,9 @@ function broadcastIsolatedLine({ agent, providerName, taskId, line }) {
|
|
|
2152
2674
|
timestamp,
|
|
2153
2675
|
});
|
|
2154
2676
|
|
|
2155
|
-
|
|
2677
|
+
if (!state?.nested) {
|
|
2678
|
+
agent.lastOutputTime = Date.now();
|
|
2679
|
+
}
|
|
2156
2680
|
}
|
|
2157
2681
|
|
|
2158
2682
|
function appendIsolatedContent(state, content, onLine) {
|
|
@@ -2299,7 +2823,7 @@ function startIsolatedStatusChecks({
|
|
|
2299
2823
|
}, 2000);
|
|
2300
2824
|
}
|
|
2301
2825
|
|
|
2302
|
-
function followClaudeTaskLogsIsolated(agent, taskId) {
|
|
2826
|
+
function followClaudeTaskLogsIsolated(agent, taskId, options = {}) {
|
|
2303
2827
|
const { isolation } = agent;
|
|
2304
2828
|
if (!isolation?.manager) {
|
|
2305
2829
|
throw new Error('followClaudeTaskLogsIsolated: isolation manager not found');
|
|
@@ -2310,9 +2834,13 @@ function followClaudeTaskLogsIsolated(agent, taskId) {
|
|
|
2310
2834
|
const providerName = agent._resolveProvider ? agent._resolveProvider() : 'claude';
|
|
2311
2835
|
|
|
2312
2836
|
return new Promise((resolve, reject) => {
|
|
2313
|
-
const state = createIsolatedLogState(
|
|
2837
|
+
const state = createIsolatedLogState(
|
|
2838
|
+
options.skipStructuredResultCheck === true,
|
|
2839
|
+
options.nested === true
|
|
2840
|
+
);
|
|
2314
2841
|
const cleanup = buildIsolatedCleanup(state);
|
|
2315
|
-
const onLine = (line) =>
|
|
2842
|
+
const onLine = (line) =>
|
|
2843
|
+
broadcastIsolatedLine({ agent, providerName, taskId, state, line });
|
|
2316
2844
|
state.lifecycleHandle = buildIsolatedLifecycleHandle({
|
|
2317
2845
|
agent,
|
|
2318
2846
|
manager,
|
|
@@ -2325,7 +2853,18 @@ function followClaudeTaskLogsIsolated(agent, taskId) {
|
|
|
2325
2853
|
reject,
|
|
2326
2854
|
onLine,
|
|
2327
2855
|
});
|
|
2328
|
-
agent
|
|
2856
|
+
// Only register the lifecycle handle on the agent for top-level tasks.
|
|
2857
|
+
if (!options.nested) {
|
|
2858
|
+
agent.currentTask = state.lifecycleHandle;
|
|
2859
|
+
}
|
|
2860
|
+
if (options.nested && options.executionHandle) {
|
|
2861
|
+
options.executionHandle.setFailClosedAction((error) =>
|
|
2862
|
+
rejectIsolatedFollower({ agent, state, cleanup, reject, error })
|
|
2863
|
+
);
|
|
2864
|
+
options.executionHandle.setCancelAction((reason, details) =>
|
|
2865
|
+
state.lifecycleHandle.terminate(reason, details)
|
|
2866
|
+
);
|
|
2867
|
+
}
|
|
2329
2868
|
|
|
2330
2869
|
manager
|
|
2331
2870
|
.execInContainer(clusterId, ['sh', '-c', `zeroshot get-log-path ${taskId}`])
|
|
@@ -2353,7 +2892,11 @@ function followClaudeTaskLogsIsolated(agent, taskId) {
|
|
|
2353
2892
|
});
|
|
2354
2893
|
}
|
|
2355
2894
|
state.logFilePath = logFilePath;
|
|
2356
|
-
if (
|
|
2895
|
+
if (
|
|
2896
|
+
state.resolved ||
|
|
2897
|
+
state.taskExited ||
|
|
2898
|
+
(options.nested && options.executionHandle?.isCancelled)
|
|
2899
|
+
) {
|
|
2357
2900
|
return;
|
|
2358
2901
|
}
|
|
2359
2902
|
|
|
@@ -2382,7 +2925,7 @@ function followClaudeTaskLogsIsolated(agent, taskId) {
|
|
|
2382
2925
|
onLine,
|
|
2383
2926
|
});
|
|
2384
2927
|
|
|
2385
|
-
if (agent.timeout > 0 && !agent.enableLivenessCheck) {
|
|
2928
|
+
if (agent.timeout > 0 && !agent.enableLivenessCheck && !options.nested) {
|
|
2386
2929
|
state.timeoutTimer = setTimeout(() => {
|
|
2387
2930
|
state.lifecycleHandle
|
|
2388
2931
|
.terminate(`Task timed out after ${agent.timeout}ms`, {
|
|
@@ -2445,6 +2988,13 @@ async function parseResultOutput(agent, output) {
|
|
|
2445
2988
|
rawOutput: output,
|
|
2446
2989
|
schema: agent.config.jsonSchema,
|
|
2447
2990
|
providerName,
|
|
2991
|
+
isCancelled: () => agent.running === false || agent.state === 'stopped',
|
|
2992
|
+
runReformat: (prompt) =>
|
|
2993
|
+
agent._spawnClaudeTask(prompt, {
|
|
2994
|
+
skipStructuredResultCheck: true,
|
|
2995
|
+
nested: true,
|
|
2996
|
+
disableTools: true,
|
|
2997
|
+
}),
|
|
2448
2998
|
onAttempt: (attempt, lastError) => {
|
|
2449
2999
|
if (lastError) {
|
|
2450
3000
|
console.warn(`[Agent ${agent.id}] Reformat attempt ${attempt}: ${lastError}`);
|
|
@@ -2456,6 +3006,14 @@ async function parseResultOutput(agent, output) {
|
|
|
2456
3006
|
},
|
|
2457
3007
|
});
|
|
2458
3008
|
} catch (reformatError) {
|
|
3009
|
+
if (
|
|
3010
|
+
reformatError.code === 'REFORMAT_CANCELLED' ||
|
|
3011
|
+
reformatError.code === 'AGENT_TASK_TIMEOUT' ||
|
|
3012
|
+
reformatError.permanent === true ||
|
|
3013
|
+
isNestedLifecycleError(reformatError)
|
|
3014
|
+
) {
|
|
3015
|
+
throw reformatError;
|
|
3016
|
+
}
|
|
2459
3017
|
// Reformatting failed - fall through to error below
|
|
2460
3018
|
console.error(`[Agent ${agent.id}] Reformatting failed: ${reformatError.message}`);
|
|
2461
3019
|
}
|
|
@@ -2548,6 +3106,19 @@ async function killTask(agent, termination = 'Task killed') {
|
|
|
2548
3106
|
const { reason, code } = normalizeTermination(termination);
|
|
2549
3107
|
const currentTask = agent.currentTask;
|
|
2550
3108
|
const taskId = agent.currentTaskId;
|
|
3109
|
+
const nestedRegistry = agent.nestedExecutions;
|
|
3110
|
+
const hadNestedExecutions = nestedRegistry?.hasActive === true;
|
|
3111
|
+
let nestedTermination = null;
|
|
3112
|
+
|
|
3113
|
+
if (hadNestedExecutions) {
|
|
3114
|
+
try {
|
|
3115
|
+
nestedTermination = await nestedRegistry.cancelAll(reason, { code });
|
|
3116
|
+
} catch (error) {
|
|
3117
|
+
return { forced: false, reason: error.message };
|
|
3118
|
+
}
|
|
3119
|
+
if (nestedTermination?.forced === false) return nestedTermination;
|
|
3120
|
+
if (!currentTask) return nestedTermination;
|
|
3121
|
+
}
|
|
2551
3122
|
|
|
2552
3123
|
if (currentTask?.pendingLaunch && typeof currentTask.kill === 'function') {
|
|
2553
3124
|
const pendingTermination = await currentTask.kill(reason, { code });
|
|
@@ -2590,6 +3161,7 @@ async function killTask(agent, termination = 'Task killed') {
|
|
|
2590
3161
|
agent.processPid = null;
|
|
2591
3162
|
agent.lastOutputTime = null;
|
|
2592
3163
|
agent.taskStartedAt = null;
|
|
3164
|
+
return nestedTermination || undefined;
|
|
2593
3165
|
}
|
|
2594
3166
|
|
|
2595
3167
|
async function killIsolatedTask(agent, currentTask, taskId, reason, code) {
|