@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.
Files changed (68) hide show
  1. package/bin/zeroshot-cluster-worker.js +16 -0
  2. package/cli/commands/providers.js +4 -1
  3. package/cli/index.js +153 -24
  4. package/docs/openengine-cluster-protocol/v1/legacy-worker.md +117 -0
  5. package/lib/agent-cli-provider/adapters/claude.d.ts.map +1 -1
  6. package/lib/agent-cli-provider/adapters/claude.js +23 -10
  7. package/lib/agent-cli-provider/adapters/claude.js.map +1 -1
  8. package/lib/agent-cli-provider/adapters/codex.d.ts.map +1 -1
  9. package/lib/agent-cli-provider/adapters/codex.js +4 -0
  10. package/lib/agent-cli-provider/adapters/codex.js.map +1 -1
  11. package/lib/agent-cli-provider/adapters/common.d.ts.map +1 -1
  12. package/lib/agent-cli-provider/adapters/common.js +2 -1
  13. package/lib/agent-cli-provider/adapters/common.js.map +1 -1
  14. package/lib/agent-cli-provider/contract-options.d.ts.map +1 -1
  15. package/lib/agent-cli-provider/contract-options.js +2 -1
  16. package/lib/agent-cli-provider/contract-options.js.map +1 -1
  17. package/lib/agent-cli-provider/index.d.ts +1 -1
  18. package/lib/agent-cli-provider/index.d.ts.map +1 -1
  19. package/lib/agent-cli-provider/index.js.map +1 -1
  20. package/lib/agent-cli-provider/provider-registry.d.ts +1 -1
  21. package/lib/agent-cli-provider/provider-registry.js +1 -1
  22. package/lib/agent-cli-provider/provider-registry.js.map +1 -1
  23. package/lib/agent-cli-provider/single-agent-runtime.js +6 -2
  24. package/lib/agent-cli-provider/single-agent-runtime.js.map +1 -1
  25. package/lib/agent-cli-provider/types.d.ts +3 -1
  26. package/lib/agent-cli-provider/types.d.ts.map +1 -1
  27. package/lib/agent-cli-provider/types.js.map +1 -1
  28. package/lib/cluster-worker/contracts.js +194 -0
  29. package/lib/cluster-worker/engine-adapter.js +300 -0
  30. package/lib/cluster-worker/executable.js +229 -0
  31. package/lib/cluster-worker/index.d.ts +227 -0
  32. package/lib/cluster-worker/index.js +294 -0
  33. package/lib/cluster-worker/object-utils.js +17 -0
  34. package/lib/cluster-worker/process-stdio.js +37 -0
  35. package/lib/cluster-worker/profiles.js +81 -0
  36. package/lib/cluster-worker/runtime-engine.js +50 -0
  37. package/lib/cluster-worker/runtime-support.js +298 -0
  38. package/lib/cluster-worker/state-machine.js +147 -0
  39. package/lib/cluster-worker/terminal-normalizer.js +95 -0
  40. package/lib/cluster-worker/worker-internals.js +20 -0
  41. package/lib/detached-startup.js +127 -11
  42. package/lib/process-liveness.js +26 -0
  43. package/lib/settings.js +1 -0
  44. package/lib/start-cluster.js +93 -18
  45. package/package.json +8 -2
  46. package/protocol/openengine-cluster/v1/worker.schema.json +1174 -0
  47. package/scripts/assert-release-published.js +65 -11
  48. package/scripts/run-lint-staged-no-stash.js +68 -0
  49. package/src/agent/agent-lifecycle.js +368 -91
  50. package/src/agent/agent-task-executor.js +384 -101
  51. package/src/agent-cli-provider/adapters/claude.ts +29 -11
  52. package/src/agent-cli-provider/adapters/codex.ts +4 -0
  53. package/src/agent-cli-provider/adapters/common.ts +2 -1
  54. package/src/agent-cli-provider/contract-options.ts +2 -1
  55. package/src/agent-cli-provider/index.ts +1 -0
  56. package/src/agent-cli-provider/provider-registry.ts +1 -1
  57. package/src/agent-cli-provider/single-agent-runtime.ts +8 -2
  58. package/src/agent-cli-provider/types.ts +3 -1
  59. package/src/agent-wrapper.js +9 -2
  60. package/src/config-validator.js +10 -11
  61. package/src/orchestrator.js +482 -67
  62. package/task-lib/attachable-watcher.js +10 -1
  63. package/task-lib/commands/kill.js +34 -9
  64. package/task-lib/commands/status.js +13 -3
  65. package/task-lib/process-termination.js +202 -0
  66. package/task-lib/runner.js +8 -20
  67. package/task-lib/store.js +28 -6
  68. 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.lastOutputTime = Date.now(); // Initialize to spawn time
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: Follow the task's log file inside container (NOT the spawn stdout!)
1611
- return followClaudeTaskLogsIsolated(agent, taskId);
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 isSuccess = /Status:\s+completed/i.test(statusOutput);
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 (!isSuccess && !isError && !isNotFound) {
1994
+ if (!status && !isNotFound) {
1758
1995
  return;
1759
1996
  }
1760
1997
 
1761
- state.taskExited = true;
1762
- await new Promise((r) => setTimeout(r, 200));
1763
-
1764
- const finalReadResult = await manager.execInContainer(clusterId, [
1765
- 'sh',
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
- result: parsedResult,
1807
- error: errorContext,
1808
- tokenUsage: extractTokenUsage(state.fullOutput, providerName),
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
- cleanup();
1862
- return reject(
1863
- new Error(`Failed to get log path for ${taskId} inside container: ${stderr || stdout}`)
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
- cleanup();
1870
- return reject(new Error(`Empty log path returned for ${taskId}`));
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
- if (!state.taskExited) {
1900
- cleanup();
1901
- reject(new Error(`Task ${taskId} timeout after ${agent.timeout}ms (isolated mode)`));
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 killTask(agent) {
2046
- if (agent.currentTask) {
2047
- // currentTask may be either a ChildProcess or our custom { kill } object
2048
- if (typeof agent.currentTask.kill === 'function') {
2049
- agent.currentTask.kill('SIGTERM');
2050
- }
2051
- agent.currentTask = null;
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
- // Also kill the underlying zeroshot task if we have a task ID
2055
- // This ensures the task process is stopped, not just our polling intervals
2056
- if (agent.currentTaskId) {
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
- runCommandWithTimeout(
2059
- ctPath,
2060
- ['task', 'kill', agent.currentTaskId],
2061
- { timeout: 10000 },
2062
- (error) => {
2063
- if (error) {
2064
- // Task may have already completed or been killed, ignore errors
2065
- agent._log(`Note: Could not kill task ${agent.currentTaskId}: ${error.message}`);
2066
- } else {
2067
- agent._log(`Killed task ${agent.currentTaskId}`);
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
- agent.currentTaskId = null;
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,