@the-open-engine/zeroshot 6.6.0 → 6.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/zeroshot-cluster-worker.js +16 -0
- package/cli/commands/providers.js +4 -1
- package/cli/index.js +153 -24
- package/docs/openengine-cluster-protocol/v1/legacy-worker.md +117 -0
- package/lib/agent-cli-provider/adapters/claude.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/claude.js +23 -10
- package/lib/agent-cli-provider/adapters/claude.js.map +1 -1
- package/lib/agent-cli-provider/adapters/codex.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/codex.js +4 -0
- package/lib/agent-cli-provider/adapters/codex.js.map +1 -1
- package/lib/agent-cli-provider/adapters/common.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/common.js +2 -1
- package/lib/agent-cli-provider/adapters/common.js.map +1 -1
- package/lib/agent-cli-provider/contract-options.d.ts.map +1 -1
- package/lib/agent-cli-provider/contract-options.js +2 -1
- package/lib/agent-cli-provider/contract-options.js.map +1 -1
- package/lib/agent-cli-provider/index.d.ts +1 -1
- package/lib/agent-cli-provider/index.d.ts.map +1 -1
- package/lib/agent-cli-provider/index.js.map +1 -1
- package/lib/agent-cli-provider/provider-registry.d.ts +1 -1
- package/lib/agent-cli-provider/provider-registry.js +1 -1
- package/lib/agent-cli-provider/provider-registry.js.map +1 -1
- package/lib/agent-cli-provider/single-agent-runtime.js +6 -2
- package/lib/agent-cli-provider/single-agent-runtime.js.map +1 -1
- package/lib/agent-cli-provider/types.d.ts +3 -1
- package/lib/agent-cli-provider/types.d.ts.map +1 -1
- package/lib/agent-cli-provider/types.js.map +1 -1
- package/lib/cluster-worker/contracts.js +194 -0
- package/lib/cluster-worker/engine-adapter.js +300 -0
- package/lib/cluster-worker/executable.js +229 -0
- package/lib/cluster-worker/index.d.ts +227 -0
- package/lib/cluster-worker/index.js +294 -0
- package/lib/cluster-worker/object-utils.js +17 -0
- package/lib/cluster-worker/process-stdio.js +37 -0
- package/lib/cluster-worker/profiles.js +81 -0
- package/lib/cluster-worker/runtime-engine.js +50 -0
- package/lib/cluster-worker/runtime-support.js +298 -0
- package/lib/cluster-worker/state-machine.js +147 -0
- package/lib/cluster-worker/terminal-normalizer.js +95 -0
- package/lib/cluster-worker/worker-internals.js +20 -0
- package/lib/detached-startup.js +127 -11
- package/lib/process-liveness.js +26 -0
- package/lib/settings.js +1 -0
- package/lib/start-cluster.js +93 -18
- package/package.json +8 -2
- package/protocol/openengine-cluster/v1/worker.schema.json +1174 -0
- package/scripts/assert-release-published.js +65 -11
- package/scripts/run-lint-staged-no-stash.js +68 -0
- package/src/agent/agent-lifecycle.js +368 -91
- package/src/agent/agent-task-executor.js +384 -101
- package/src/agent-cli-provider/adapters/claude.ts +29 -11
- package/src/agent-cli-provider/adapters/codex.ts +4 -0
- package/src/agent-cli-provider/adapters/common.ts +2 -1
- package/src/agent-cli-provider/contract-options.ts +2 -1
- package/src/agent-cli-provider/index.ts +1 -0
- package/src/agent-cli-provider/provider-registry.ts +1 -1
- package/src/agent-cli-provider/single-agent-runtime.ts +8 -2
- package/src/agent-cli-provider/types.ts +3 -1
- package/src/agent-wrapper.js +9 -2
- package/src/config-validator.js +10 -11
- package/src/orchestrator.js +482 -67
- package/task-lib/attachable-watcher.js +10 -1
- package/task-lib/commands/kill.js +34 -9
- package/task-lib/commands/status.js +13 -3
- package/task-lib/process-termination.js +202 -0
- package/task-lib/runner.js +8 -20
- package/task-lib/store.js +28 -6
- package/task-lib/watcher.js +14 -2
|
@@ -673,6 +673,7 @@ async function spawnClaudeTask(agent, context) {
|
|
|
673
673
|
const MAX_PID_POLLS = 30; // 3 seconds max
|
|
674
674
|
const PID_POLL_DELAY = 100;
|
|
675
675
|
let realPid = null;
|
|
676
|
+
let terminalBeforePidObservation = false;
|
|
676
677
|
|
|
677
678
|
for (let i = 0; i < MAX_PID_POLLS; i++) {
|
|
678
679
|
const taskInfo = getTask(taskId);
|
|
@@ -680,6 +681,10 @@ async function spawnClaudeTask(agent, context) {
|
|
|
680
681
|
realPid = taskInfo.pid;
|
|
681
682
|
break;
|
|
682
683
|
}
|
|
684
|
+
if (taskInfo && ['completed', 'failed', 'killed', 'stale'].includes(taskInfo.status)) {
|
|
685
|
+
terminalBeforePidObservation = true;
|
|
686
|
+
break;
|
|
687
|
+
}
|
|
683
688
|
await new Promise((r) => setTimeout(r, PID_POLL_DELAY));
|
|
684
689
|
}
|
|
685
690
|
|
|
@@ -687,6 +692,8 @@ async function spawnClaudeTask(agent, context) {
|
|
|
687
692
|
agent.processPid = realPid;
|
|
688
693
|
agent._publishLifecycle('PROCESS_SPAWNED', { pid: realPid });
|
|
689
694
|
agent._log(`📋 Agent ${agent.id}: Process PID: ${realPid}`);
|
|
695
|
+
} else if (terminalBeforePidObservation) {
|
|
696
|
+
agent._log(`📋 Agent ${agent.id}: Task finished before PID observation`);
|
|
690
697
|
} else {
|
|
691
698
|
agent._log(`⚠️ Agent ${agent.id}: PID not available (task may use non-standard watcher)`);
|
|
692
699
|
}
|
|
@@ -928,7 +935,8 @@ function spawnTaskProcess({ agent, ctPath, args, cwd, spawnEnv }) {
|
|
|
928
935
|
|
|
929
936
|
// Start liveness monitoring
|
|
930
937
|
if (agent.enableLivenessCheck) {
|
|
931
|
-
agent.
|
|
938
|
+
agent.taskStartedAt = Date.now();
|
|
939
|
+
agent.lastOutputTime = agent.taskStartedAt;
|
|
932
940
|
agent._startLivenessCheck();
|
|
933
941
|
}
|
|
934
942
|
|
|
@@ -1131,6 +1139,7 @@ function parseStatusFlags(cleanStdout) {
|
|
|
1131
1139
|
isCompleted: /Status:\s+completed/i.test(cleanStdout),
|
|
1132
1140
|
isFailed: /Status:\s+failed/i.test(cleanStdout),
|
|
1133
1141
|
isStale: /Status:\s+stale/i.test(cleanStdout),
|
|
1142
|
+
isKilled: /Status:\s+killed/i.test(cleanStdout),
|
|
1134
1143
|
};
|
|
1135
1144
|
}
|
|
1136
1145
|
|
|
@@ -1329,9 +1338,9 @@ function handleStatusCompletion({
|
|
|
1329
1338
|
resolve,
|
|
1330
1339
|
}) {
|
|
1331
1340
|
const cleanStdout = stripAnsiCodes(stdout);
|
|
1332
|
-
const { isCompleted, isFailed, isStale } = parseStatusFlags(cleanStdout);
|
|
1341
|
+
const { isCompleted, isFailed, isStale, isKilled } = parseStatusFlags(cleanStdout);
|
|
1333
1342
|
|
|
1334
|
-
if (!isCompleted && !isFailed && !isStale) {
|
|
1343
|
+
if (!isCompleted && !isFailed && !isStale && !isKilled) {
|
|
1335
1344
|
return false;
|
|
1336
1345
|
}
|
|
1337
1346
|
|
|
@@ -1370,9 +1379,9 @@ function handleStatusCompletion({
|
|
|
1370
1379
|
return true;
|
|
1371
1380
|
}
|
|
1372
1381
|
|
|
1373
|
-
function buildKillHandler({ agent, state, providerName, resolve }) {
|
|
1382
|
+
function buildKillHandler({ agent, taskId, state, providerName, resolve }) {
|
|
1374
1383
|
return {
|
|
1375
|
-
kill: (reason = 'Task killed') => {
|
|
1384
|
+
kill: (reason = 'Task killed', details = {}) => {
|
|
1376
1385
|
if (state.resolved) return;
|
|
1377
1386
|
state.resolved = true;
|
|
1378
1387
|
finalizeLogFollow(agent, state);
|
|
@@ -1381,6 +1390,8 @@ function buildKillHandler({ agent, state, providerName, resolve }) {
|
|
|
1381
1390
|
success: false,
|
|
1382
1391
|
output: state.output,
|
|
1383
1392
|
error: reason,
|
|
1393
|
+
code: details.code || null,
|
|
1394
|
+
taskId,
|
|
1384
1395
|
tokenUsage: extractTokenUsage(state.output, providerName),
|
|
1385
1396
|
});
|
|
1386
1397
|
},
|
|
@@ -1438,7 +1449,7 @@ function createLogFollower({ agent, taskId, fsModule, ctPath, providerName }) {
|
|
|
1438
1449
|
);
|
|
1439
1450
|
}, 1000);
|
|
1440
1451
|
|
|
1441
|
-
agent.currentTask = buildKillHandler({ agent, state, providerName, resolve });
|
|
1452
|
+
agent.currentTask = buildKillHandler({ agent, taskId, state, providerName, resolve });
|
|
1442
1453
|
});
|
|
1443
1454
|
}
|
|
1444
1455
|
|
|
@@ -1582,12 +1593,6 @@ async function spawnClaudeTaskIsolated(agent, context) {
|
|
|
1582
1593
|
taskId: spawnedTaskId,
|
|
1583
1594
|
});
|
|
1584
1595
|
|
|
1585
|
-
// Start liveness monitoring
|
|
1586
|
-
if (agent.enableLivenessCheck) {
|
|
1587
|
-
agent.lastOutputTime = Date.now(); // Initialize to spawn time
|
|
1588
|
-
agent._startLivenessCheck();
|
|
1589
|
-
}
|
|
1590
|
-
|
|
1591
1596
|
resolve(spawnedTaskId);
|
|
1592
1597
|
} else {
|
|
1593
1598
|
reject(new Error(`Could not parse task ID from output: ${stdout}`));
|
|
@@ -1607,8 +1612,15 @@ async function spawnClaudeTaskIsolated(agent, context) {
|
|
|
1607
1612
|
|
|
1608
1613
|
agent._log(`📋 Agent ${agent.id}: Following zeroshot logs for ${taskId} in container...`);
|
|
1609
1614
|
|
|
1610
|
-
// STEP 2:
|
|
1611
|
-
|
|
1615
|
+
// STEP 2: Install the lifecycle-owned handle before liveness monitoring can
|
|
1616
|
+
// observe the task, then follow the task's log file inside the container.
|
|
1617
|
+
const execution = followClaudeTaskLogsIsolated(agent, taskId);
|
|
1618
|
+
if (agent.enableLivenessCheck) {
|
|
1619
|
+
agent.taskStartedAt = Date.now();
|
|
1620
|
+
agent.lastOutputTime = agent.taskStartedAt;
|
|
1621
|
+
agent._startLivenessCheck();
|
|
1622
|
+
}
|
|
1623
|
+
return execution;
|
|
1612
1624
|
}
|
|
1613
1625
|
|
|
1614
1626
|
/**
|
|
@@ -1637,9 +1649,14 @@ async function spawnClaudeTaskIsolated(agent, context) {
|
|
|
1637
1649
|
function createIsolatedLogState() {
|
|
1638
1650
|
return {
|
|
1639
1651
|
taskExited: false,
|
|
1652
|
+
resolved: false,
|
|
1653
|
+
terminationPromise: null,
|
|
1654
|
+
lifecycleHandle: null,
|
|
1655
|
+
logFilePath: null,
|
|
1640
1656
|
fullOutput: '',
|
|
1641
1657
|
tailProcess: null,
|
|
1642
1658
|
statusCheckInterval: null,
|
|
1659
|
+
timeoutTimer: null,
|
|
1643
1660
|
lineBuffer: '',
|
|
1644
1661
|
};
|
|
1645
1662
|
}
|
|
@@ -1658,6 +1675,226 @@ function buildIsolatedCleanup(state) {
|
|
|
1658
1675
|
clearInterval(state.statusCheckInterval);
|
|
1659
1676
|
state.statusCheckInterval = null;
|
|
1660
1677
|
}
|
|
1678
|
+
if (state.timeoutTimer) {
|
|
1679
|
+
clearTimeout(state.timeoutTimer);
|
|
1680
|
+
state.timeoutTimer = null;
|
|
1681
|
+
}
|
|
1682
|
+
};
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
function clearIsolatedLifecycleHandle(agent, state) {
|
|
1686
|
+
if (agent.currentTask === state.lifecycleHandle) {
|
|
1687
|
+
agent.currentTask = null;
|
|
1688
|
+
}
|
|
1689
|
+
agent._stopLivenessCheck?.();
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
function settleIsolatedFollower({ agent, state, cleanup, resolve, result }) {
|
|
1693
|
+
if (state.resolved) return;
|
|
1694
|
+
state.resolved = true;
|
|
1695
|
+
state.taskExited = true;
|
|
1696
|
+
cleanup();
|
|
1697
|
+
clearIsolatedLifecycleHandle(agent, state);
|
|
1698
|
+
resolve(result);
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
function rejectIsolatedFollower({ agent, state, cleanup, reject, error }) {
|
|
1702
|
+
if (state.resolved) return;
|
|
1703
|
+
state.resolved = true;
|
|
1704
|
+
state.taskExited = true;
|
|
1705
|
+
cleanup();
|
|
1706
|
+
clearIsolatedLifecycleHandle(agent, state);
|
|
1707
|
+
reject(error);
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
function parseIsolatedStatus(output) {
|
|
1711
|
+
return output.match(/Status:\s+(completed|failed|killed|stale|cancelled)/i)?.[1].toLowerCase();
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
async function terminateIsolatedTask(manager, clusterId, taskId) {
|
|
1715
|
+
const before = await manager.execInContainer(clusterId, ['zeroshot', 'status', taskId]);
|
|
1716
|
+
const beforeStatus = before.code === 0 ? parseIsolatedStatus(before.stdout) : null;
|
|
1717
|
+
if (beforeStatus) {
|
|
1718
|
+
return { alreadyTerminal: true, forced: false, status: beforeStatus };
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
const result = await manager.execInContainer(clusterId, ['zeroshot', 'kill', taskId]);
|
|
1722
|
+
const status = await manager.execInContainer(clusterId, ['zeroshot', 'status', taskId]);
|
|
1723
|
+
const afterStatus = status.code === 0 ? parseIsolatedStatus(status.stdout) : null;
|
|
1724
|
+
if (!afterStatus) {
|
|
1725
|
+
throw new Error(
|
|
1726
|
+
`Failed to terminate isolated task ${taskId}: ${result.stderr || result.stdout || `exit ${result.code}`}`
|
|
1727
|
+
);
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
return {
|
|
1731
|
+
alreadyTerminal: false,
|
|
1732
|
+
forced: afterStatus === 'killed',
|
|
1733
|
+
status: afterStatus,
|
|
1734
|
+
};
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
async function resolveIsolatedLogFilePath(manager, clusterId, taskId, state) {
|
|
1738
|
+
if (state.logFilePath) return state.logFilePath;
|
|
1739
|
+
|
|
1740
|
+
const result = await manager.execInContainer(clusterId, [
|
|
1741
|
+
'sh',
|
|
1742
|
+
'-c',
|
|
1743
|
+
`zeroshot get-log-path ${taskId}`,
|
|
1744
|
+
]);
|
|
1745
|
+
if (result.code !== 0 || !result.stdout.trim()) {
|
|
1746
|
+
throw new Error(
|
|
1747
|
+
`Failed to get log path for ${taskId} inside container: ${result.stderr || result.stdout}`
|
|
1748
|
+
);
|
|
1749
|
+
}
|
|
1750
|
+
state.logFilePath = result.stdout.trim();
|
|
1751
|
+
return state.logFilePath;
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
function settleIsolatedTerminalStatus({
|
|
1755
|
+
agent,
|
|
1756
|
+
manager,
|
|
1757
|
+
clusterId,
|
|
1758
|
+
taskId,
|
|
1759
|
+
providerName,
|
|
1760
|
+
status,
|
|
1761
|
+
isNotFound = false,
|
|
1762
|
+
state,
|
|
1763
|
+
cleanup,
|
|
1764
|
+
resolve,
|
|
1765
|
+
reject,
|
|
1766
|
+
onLine,
|
|
1767
|
+
}) {
|
|
1768
|
+
if (state.resolved) return Promise.resolve();
|
|
1769
|
+
if (state.terminalSettlementPromise) return state.terminalSettlementPromise;
|
|
1770
|
+
|
|
1771
|
+
state.taskExited = true;
|
|
1772
|
+
const settlement = (async () => {
|
|
1773
|
+
const logFilePath = await resolveIsolatedLogFilePath(manager, clusterId, taskId, state);
|
|
1774
|
+
await new Promise((settle) => setTimeout(settle, 200));
|
|
1775
|
+
const finalReadResult = await manager.execInContainer(clusterId, [
|
|
1776
|
+
'sh',
|
|
1777
|
+
'-c',
|
|
1778
|
+
`cat "${logFilePath}" 2>/dev/null || echo ""`,
|
|
1779
|
+
]);
|
|
1780
|
+
|
|
1781
|
+
if (finalReadResult.code === 0 && finalReadResult.stdout) {
|
|
1782
|
+
state.fullOutput = finalReadResult.stdout;
|
|
1783
|
+
for (const line of state.fullOutput.split('\n')) {
|
|
1784
|
+
if (line.trim()) onLine(line);
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
const success = status === 'completed';
|
|
1789
|
+
const errorContext = !success
|
|
1790
|
+
? extractErrorContext({
|
|
1791
|
+
output: state.fullOutput,
|
|
1792
|
+
statusOutput: status ? `Status: ${status}` : '',
|
|
1793
|
+
taskId,
|
|
1794
|
+
isNotFound,
|
|
1795
|
+
debug: {
|
|
1796
|
+
agentId: agent.id,
|
|
1797
|
+
providerName,
|
|
1798
|
+
pid: agent.processPid,
|
|
1799
|
+
cwd: agent.config.cwd || process.cwd(),
|
|
1800
|
+
worktreePath: agent.worktree?.path || null,
|
|
1801
|
+
isolation: true,
|
|
1802
|
+
clusterId,
|
|
1803
|
+
logFilePath,
|
|
1804
|
+
},
|
|
1805
|
+
})
|
|
1806
|
+
: null;
|
|
1807
|
+
const parsedResult = await agent._parseResultOutput(state.fullOutput);
|
|
1808
|
+
|
|
1809
|
+
settleIsolatedFollower({
|
|
1810
|
+
agent,
|
|
1811
|
+
state,
|
|
1812
|
+
cleanup,
|
|
1813
|
+
resolve,
|
|
1814
|
+
result: {
|
|
1815
|
+
success,
|
|
1816
|
+
output: state.fullOutput,
|
|
1817
|
+
taskId,
|
|
1818
|
+
result: parsedResult,
|
|
1819
|
+
error: errorContext,
|
|
1820
|
+
tokenUsage: extractTokenUsage(state.fullOutput, providerName),
|
|
1821
|
+
},
|
|
1822
|
+
});
|
|
1823
|
+
})().catch((error) => {
|
|
1824
|
+
rejectIsolatedFollower({ agent, state, cleanup, reject, error });
|
|
1825
|
+
});
|
|
1826
|
+
state.terminalSettlementPromise = settlement;
|
|
1827
|
+
return settlement;
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
function buildIsolatedLifecycleHandle({
|
|
1831
|
+
agent,
|
|
1832
|
+
manager,
|
|
1833
|
+
clusterId,
|
|
1834
|
+
taskId,
|
|
1835
|
+
providerName,
|
|
1836
|
+
state,
|
|
1837
|
+
cleanup,
|
|
1838
|
+
resolve,
|
|
1839
|
+
reject,
|
|
1840
|
+
onLine,
|
|
1841
|
+
}) {
|
|
1842
|
+
const terminate = (reason = 'Task killed', details = {}) => {
|
|
1843
|
+
if (state.resolved || state.taskExited) return;
|
|
1844
|
+
if (state.terminationPromise) return state.terminationPromise;
|
|
1845
|
+
|
|
1846
|
+
const terminationPromise = (async () => {
|
|
1847
|
+
const termination = await terminateIsolatedTask(manager, clusterId, taskId);
|
|
1848
|
+
if (!termination.forced) {
|
|
1849
|
+
await settleIsolatedTerminalStatus({
|
|
1850
|
+
agent,
|
|
1851
|
+
manager,
|
|
1852
|
+
clusterId,
|
|
1853
|
+
taskId,
|
|
1854
|
+
providerName,
|
|
1855
|
+
status: termination.status,
|
|
1856
|
+
state,
|
|
1857
|
+
cleanup,
|
|
1858
|
+
resolve,
|
|
1859
|
+
reject,
|
|
1860
|
+
onLine,
|
|
1861
|
+
});
|
|
1862
|
+
return termination;
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
settleIsolatedFollower({
|
|
1866
|
+
agent,
|
|
1867
|
+
state,
|
|
1868
|
+
cleanup,
|
|
1869
|
+
resolve,
|
|
1870
|
+
result: {
|
|
1871
|
+
success: false,
|
|
1872
|
+
output: state.fullOutput,
|
|
1873
|
+
error: reason,
|
|
1874
|
+
code: details.code || null,
|
|
1875
|
+
taskId,
|
|
1876
|
+
tokenUsage: extractTokenUsage(state.fullOutput, providerName),
|
|
1877
|
+
},
|
|
1878
|
+
});
|
|
1879
|
+
return termination;
|
|
1880
|
+
})();
|
|
1881
|
+
state.terminationPromise = terminationPromise;
|
|
1882
|
+
terminationPromise.catch(() => {
|
|
1883
|
+
if (state.terminationPromise === terminationPromise) {
|
|
1884
|
+
state.terminationPromise = null;
|
|
1885
|
+
}
|
|
1886
|
+
});
|
|
1887
|
+
|
|
1888
|
+
return terminationPromise;
|
|
1889
|
+
};
|
|
1890
|
+
|
|
1891
|
+
return {
|
|
1892
|
+
isolated: true,
|
|
1893
|
+
terminate,
|
|
1894
|
+
kill: terminate,
|
|
1895
|
+
failClosed(error) {
|
|
1896
|
+
rejectIsolatedFollower({ agent, state, cleanup, reject, error });
|
|
1897
|
+
},
|
|
1661
1898
|
};
|
|
1662
1899
|
}
|
|
1663
1900
|
|
|
@@ -1739,6 +1976,7 @@ async function checkIsolatedStatus({
|
|
|
1739
1976
|
state,
|
|
1740
1977
|
cleanup,
|
|
1741
1978
|
resolve,
|
|
1979
|
+
reject,
|
|
1742
1980
|
onLine,
|
|
1743
1981
|
}) {
|
|
1744
1982
|
if (state.taskExited) return;
|
|
@@ -1750,62 +1988,27 @@ async function checkIsolatedStatus({
|
|
|
1750
1988
|
]);
|
|
1751
1989
|
|
|
1752
1990
|
const statusOutput = statusResult.stdout;
|
|
1753
|
-
const
|
|
1754
|
-
const isError = /Status:\s+failed/i.test(statusOutput);
|
|
1991
|
+
const status = parseIsolatedStatus(statusOutput);
|
|
1755
1992
|
const isNotFound = statusOutput.includes('not_found');
|
|
1756
1993
|
|
|
1757
|
-
if (!
|
|
1994
|
+
if (!status && !isNotFound) {
|
|
1758
1995
|
return;
|
|
1759
1996
|
}
|
|
1760
1997
|
|
|
1761
|
-
state.
|
|
1762
|
-
await
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
'-c',
|
|
1767
|
-
`cat "${logFilePath}" 2>/dev/null || echo ""`,
|
|
1768
|
-
]);
|
|
1769
|
-
|
|
1770
|
-
if (finalReadResult.code === 0 && finalReadResult.stdout) {
|
|
1771
|
-
state.fullOutput = finalReadResult.stdout;
|
|
1772
|
-
const remainingLines = state.fullOutput.split('\n');
|
|
1773
|
-
for (const line of remainingLines) {
|
|
1774
|
-
if (line.trim()) {
|
|
1775
|
-
onLine(line);
|
|
1776
|
-
}
|
|
1777
|
-
}
|
|
1778
|
-
}
|
|
1779
|
-
|
|
1780
|
-
cleanup();
|
|
1781
|
-
|
|
1782
|
-
const success = isSuccess && !isError;
|
|
1783
|
-
const errorContext = !success
|
|
1784
|
-
? extractErrorContext({
|
|
1785
|
-
output: state.fullOutput,
|
|
1786
|
-
taskId,
|
|
1787
|
-
isNotFound,
|
|
1788
|
-
debug: {
|
|
1789
|
-
agentId: agent.id,
|
|
1790
|
-
providerName,
|
|
1791
|
-
pid: agent.processPid,
|
|
1792
|
-
cwd: agent.config.cwd || process.cwd(),
|
|
1793
|
-
worktreePath: agent.worktree?.path || null,
|
|
1794
|
-
isolation: true,
|
|
1795
|
-
clusterId,
|
|
1796
|
-
logFilePath,
|
|
1797
|
-
},
|
|
1798
|
-
})
|
|
1799
|
-
: null;
|
|
1800
|
-
const parsedResult = await agent._parseResultOutput(state.fullOutput);
|
|
1801
|
-
|
|
1802
|
-
resolve({
|
|
1803
|
-
success,
|
|
1804
|
-
output: state.fullOutput,
|
|
1998
|
+
state.logFilePath = logFilePath;
|
|
1999
|
+
await settleIsolatedTerminalStatus({
|
|
2000
|
+
agent,
|
|
2001
|
+
manager,
|
|
2002
|
+
clusterId,
|
|
1805
2003
|
taskId,
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
2004
|
+
providerName,
|
|
2005
|
+
status,
|
|
2006
|
+
isNotFound,
|
|
2007
|
+
state,
|
|
2008
|
+
cleanup,
|
|
2009
|
+
resolve,
|
|
2010
|
+
reject,
|
|
2011
|
+
onLine,
|
|
1809
2012
|
});
|
|
1810
2013
|
}
|
|
1811
2014
|
|
|
@@ -1819,6 +2022,7 @@ function startIsolatedStatusChecks({
|
|
|
1819
2022
|
state,
|
|
1820
2023
|
cleanup,
|
|
1821
2024
|
resolve,
|
|
2025
|
+
reject,
|
|
1822
2026
|
onLine,
|
|
1823
2027
|
}) {
|
|
1824
2028
|
state.statusCheckInterval = setInterval(() => {
|
|
@@ -1832,6 +2036,7 @@ function startIsolatedStatusChecks({
|
|
|
1832
2036
|
state,
|
|
1833
2037
|
cleanup,
|
|
1834
2038
|
resolve,
|
|
2039
|
+
reject,
|
|
1835
2040
|
onLine,
|
|
1836
2041
|
}).catch((statusErr) => {
|
|
1837
2042
|
agent._log(`[${agent.id}] Status check error (will retry): ${statusErr.message}`);
|
|
@@ -1853,21 +2058,48 @@ function followClaudeTaskLogsIsolated(agent, taskId) {
|
|
|
1853
2058
|
const state = createIsolatedLogState();
|
|
1854
2059
|
const cleanup = buildIsolatedCleanup(state);
|
|
1855
2060
|
const onLine = (line) => broadcastIsolatedLine({ agent, providerName, taskId, line });
|
|
2061
|
+
state.lifecycleHandle = buildIsolatedLifecycleHandle({
|
|
2062
|
+
agent,
|
|
2063
|
+
manager,
|
|
2064
|
+
clusterId,
|
|
2065
|
+
taskId,
|
|
2066
|
+
providerName,
|
|
2067
|
+
state,
|
|
2068
|
+
cleanup,
|
|
2069
|
+
resolve,
|
|
2070
|
+
reject,
|
|
2071
|
+
onLine,
|
|
2072
|
+
});
|
|
2073
|
+
agent.currentTask = state.lifecycleHandle;
|
|
1856
2074
|
|
|
1857
2075
|
manager
|
|
1858
2076
|
.execInContainer(clusterId, ['sh', '-c', `zeroshot get-log-path ${taskId}`])
|
|
1859
2077
|
.then(({ stdout, stderr, code }) => {
|
|
1860
2078
|
if (code !== 0) {
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
2079
|
+
return rejectIsolatedFollower({
|
|
2080
|
+
agent,
|
|
2081
|
+
state,
|
|
2082
|
+
cleanup,
|
|
2083
|
+
reject,
|
|
2084
|
+
error: new Error(
|
|
2085
|
+
`Failed to get log path for ${taskId} inside container: ${stderr || stdout}`
|
|
2086
|
+
),
|
|
2087
|
+
});
|
|
1865
2088
|
}
|
|
1866
2089
|
|
|
1867
2090
|
const logFilePath = stdout.trim();
|
|
1868
2091
|
if (!logFilePath) {
|
|
1869
|
-
|
|
1870
|
-
|
|
2092
|
+
return rejectIsolatedFollower({
|
|
2093
|
+
agent,
|
|
2094
|
+
state,
|
|
2095
|
+
cleanup,
|
|
2096
|
+
reject,
|
|
2097
|
+
error: new Error(`Empty log path returned for ${taskId}`),
|
|
2098
|
+
});
|
|
2099
|
+
}
|
|
2100
|
+
state.logFilePath = logFilePath;
|
|
2101
|
+
if (state.resolved || state.taskExited) {
|
|
2102
|
+
return;
|
|
1871
2103
|
}
|
|
1872
2104
|
|
|
1873
2105
|
agent._log(`[${agent.id}] Following isolated task logs (streaming): ${logFilePath}`);
|
|
@@ -1891,21 +2123,26 @@ function followClaudeTaskLogsIsolated(agent, taskId) {
|
|
|
1891
2123
|
state,
|
|
1892
2124
|
cleanup,
|
|
1893
2125
|
resolve,
|
|
2126
|
+
reject,
|
|
1894
2127
|
onLine,
|
|
1895
2128
|
});
|
|
1896
2129
|
|
|
1897
|
-
if (agent.timeout > 0) {
|
|
1898
|
-
setTimeout(() => {
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
2130
|
+
if (agent.timeout > 0 && !agent.enableLivenessCheck) {
|
|
2131
|
+
state.timeoutTimer = setTimeout(() => {
|
|
2132
|
+
state.lifecycleHandle
|
|
2133
|
+
.terminate(`Task timed out after ${agent.timeout}ms`, {
|
|
2134
|
+
code: 'AGENT_TASK_TIMEOUT',
|
|
2135
|
+
})
|
|
2136
|
+
.catch((error) => {
|
|
2137
|
+
agent._log(
|
|
2138
|
+
`[${agent.id}] Failed to terminate timed-out isolated task: ${error.message}`
|
|
2139
|
+
);
|
|
2140
|
+
});
|
|
1903
2141
|
}, agent.timeout);
|
|
1904
2142
|
}
|
|
1905
2143
|
})
|
|
1906
2144
|
.catch((err) => {
|
|
1907
|
-
cleanup
|
|
1908
|
-
reject(err);
|
|
2145
|
+
rejectIsolatedFollower({ agent, state, cleanup, reject, error: err });
|
|
1909
2146
|
});
|
|
1910
2147
|
});
|
|
1911
2148
|
}
|
|
@@ -2042,40 +2279,86 @@ async function parseResultOutput(agent, output) {
|
|
|
2042
2279
|
* Kill current task
|
|
2043
2280
|
* @param {Object} agent - Agent instance
|
|
2044
2281
|
*/
|
|
2045
|
-
function
|
|
2046
|
-
if (
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
}
|
|
2051
|
-
|
|
2282
|
+
function normalizeTermination(termination) {
|
|
2283
|
+
if (termination && typeof termination === 'object') {
|
|
2284
|
+
return {
|
|
2285
|
+
reason: termination.reason || 'Task killed',
|
|
2286
|
+
code: termination.code || null,
|
|
2287
|
+
};
|
|
2288
|
+
}
|
|
2289
|
+
return { reason: termination || 'Task killed', code: null };
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
async function killTask(agent, termination = 'Task killed') {
|
|
2293
|
+
const { reason, code } = normalizeTermination(termination);
|
|
2294
|
+
const currentTask = agent.currentTask;
|
|
2295
|
+
const taskId = agent.currentTaskId;
|
|
2296
|
+
|
|
2297
|
+
if (agent.isolation?.enabled && taskId) {
|
|
2298
|
+
return killIsolatedTask(agent, currentTask, taskId, reason, code);
|
|
2052
2299
|
}
|
|
2053
2300
|
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2301
|
+
agent._stopLivenessCheck?.();
|
|
2302
|
+
|
|
2303
|
+
// Kill the underlying task before resolving the local follower. This keeps
|
|
2304
|
+
// retries from racing a provider process that is still shutting down.
|
|
2305
|
+
if (taskId) {
|
|
2057
2306
|
const ctPath = getClaudeTasksPath();
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2307
|
+
try {
|
|
2308
|
+
// `kill` is a top-level smart command. `task kill` has never existed.
|
|
2309
|
+
await runCommandWithTimeout(ctPath, ['kill', taskId], { timeout: 10000 });
|
|
2310
|
+
agent._log?.(`Killed task ${taskId}`);
|
|
2311
|
+
} catch (error) {
|
|
2312
|
+
// Resolve the local follower even if the task is already terminal or the
|
|
2313
|
+
// management CLI is unavailable; shutdown state must still reconcile.
|
|
2314
|
+
agent._log?.(`Note: Could not kill task ${taskId}: ${error.message}`);
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
|
|
2318
|
+
if (currentTask && typeof currentTask.kill === 'function') {
|
|
2319
|
+
currentTask.kill(reason, { code });
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
agent.currentTask = null;
|
|
2323
|
+
agent.currentTaskId = null;
|
|
2324
|
+
agent.processPid = null;
|
|
2325
|
+
agent.lastOutputTime = null;
|
|
2326
|
+
agent.taskStartedAt = null;
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2329
|
+
async function killIsolatedTask(agent, currentTask, taskId, reason, code) {
|
|
2330
|
+
let termination;
|
|
2331
|
+
if (currentTask && typeof currentTask.terminate === 'function') {
|
|
2332
|
+
termination = await currentTask.terminate(reason, { code });
|
|
2333
|
+
} else {
|
|
2334
|
+
termination = await terminateIsolatedTask(
|
|
2335
|
+
agent.isolation.manager,
|
|
2336
|
+
agent.isolation.clusterId,
|
|
2337
|
+
taskId
|
|
2070
2338
|
);
|
|
2071
|
-
|
|
2339
|
+
if (currentTask && typeof currentTask.kill === 'function') {
|
|
2340
|
+
currentTask.kill(reason, { code });
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
|
|
2344
|
+
agent._stopLivenessCheck?.();
|
|
2345
|
+
if (termination?.forced === false) {
|
|
2346
|
+
return termination;
|
|
2072
2347
|
}
|
|
2348
|
+
|
|
2349
|
+
agent.currentTask = null;
|
|
2350
|
+
agent.currentTaskId = null;
|
|
2351
|
+
agent.processPid = null;
|
|
2352
|
+
agent.lastOutputTime = null;
|
|
2353
|
+
agent.taskStartedAt = null;
|
|
2354
|
+
return termination;
|
|
2073
2355
|
}
|
|
2074
2356
|
|
|
2075
2357
|
module.exports = {
|
|
2076
2358
|
ensureAskUserQuestionHook,
|
|
2077
2359
|
spawnClaudeTask,
|
|
2078
2360
|
followClaudeTaskLogs,
|
|
2361
|
+
followClaudeTaskLogsIsolated,
|
|
2079
2362
|
waitForTaskReady,
|
|
2080
2363
|
spawnClaudeTaskIsolated,
|
|
2081
2364
|
getClaudeTasksPath,
|