@the-open-engine/zeroshot 6.34.0 → 6.34.2

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.
@@ -11,8 +11,10 @@
11
11
  */
12
12
 
13
13
  const { spawn, spawnSync } = require('child_process');
14
+ const { createHash } = require('crypto');
14
15
  const path = require('path');
15
16
  const fs = require('fs');
17
+ const { StringDecoder } = require('string_decoder');
16
18
  const { getNestedExecutionRegistry, TaskExecutionHandle } = require('./task-execution-handle');
17
19
  const os = require('os');
18
20
  const { parseProviderChunk, getProvider } = require('../providers');
@@ -57,6 +59,13 @@ const {
57
59
  isStructuredOutputInvalidError,
58
60
  } = require('./structured-output-error');
59
61
  const TASK_TERMINAL_STATUSES = new Set(['completed', 'failed', 'killed', 'stale']);
62
+ const MAX_CONTROL_PLANE_RECORD_BYTES = 1024 * 1024;
63
+ const MAX_CONTROL_PLANE_OUTPUT_BYTES = 2 * 1024 * 1024;
64
+ const MAX_CONTROL_PLANE_OUTPUT_RECORDS = 1024;
65
+ const MAX_LIVE_OUTPUT_BYTES = 512 * 1024;
66
+ const MAX_LIVE_OUTPUT_RECORDS = 512;
67
+ const CONTROL_PLANE_OMISSION_MARKER_BYTES = 1024;
68
+ const LOG_READ_CHUNK_BYTES = 64 * 1024;
60
69
  function runCommandWithTimeout(command, args, options = {}, callback = null) {
61
70
  const timeout = options.timeout ?? 30000;
62
71
  if (timeout <= 0) {
@@ -1218,16 +1227,194 @@ async function waitForTaskReady(agent, taskId, maxRetries = 10, delayMs = 200) {
1218
1227
  const MAX_STATUS_FAILURES = 30;
1219
1228
 
1220
1229
  function createLogFollowState() {
1221
- return {
1222
- output: '',
1230
+ const state = {
1231
+ controlPlaneOutput: createControlPlaneOutputState(),
1223
1232
  logFilePath: null,
1224
1233
  lastSize: 0,
1225
1234
  pollInterval: null,
1226
1235
  statusCheckInterval: null,
1227
1236
  resolved: false,
1228
- lineBuffer: '',
1237
+ lineBuffer: createLogRecordBuffer(),
1238
+ logDecoder: new StringDecoder('utf8'),
1229
1239
  consecutiveExecFailures: 0,
1230
1240
  };
1241
+ defineControlPlaneOutputAccessor(state, 'output');
1242
+ return state;
1243
+ }
1244
+
1245
+ function createControlPlaneOutputState(options = {}) {
1246
+ const maxBytes = options.maxBytes || MAX_CONTROL_PLANE_OUTPUT_BYTES;
1247
+ const maxRecords = options.maxRecords || MAX_CONTROL_PLANE_OUTPUT_RECORDS;
1248
+ const liveByteLimit = options.liveByteLimit || MAX_LIVE_OUTPUT_BYTES;
1249
+ const liveRecordLimit = options.liveRecordLimit || MAX_LIVE_OUTPUT_RECORDS;
1250
+ if (maxBytes <= CONTROL_PLANE_OMISSION_MARKER_BYTES || maxRecords < 1) {
1251
+ throw new Error('Control-plane output bounds must retain space for at least one record');
1252
+ }
1253
+ return {
1254
+ records: [],
1255
+ head: 0,
1256
+ byteLength: 0,
1257
+ maxBytes,
1258
+ maxRecords,
1259
+ liveByteLimit,
1260
+ liveRecordLimit,
1261
+ liveBytes: 0,
1262
+ liveRecords: 0,
1263
+ nextSequence: 0,
1264
+ lastLiveSequence: -1,
1265
+ liveSuppressed: false,
1266
+ terminalFlushed: false,
1267
+ omittedBytes: 0,
1268
+ omittedRecords: 0,
1269
+ omittedDigest: createHash('sha256'),
1270
+ prefixOmitted: false,
1271
+ };
1272
+ }
1273
+
1274
+ function defineControlPlaneOutputAccessor(state, property) {
1275
+ Object.defineProperty(state, property, {
1276
+ configurable: true,
1277
+ enumerable: true,
1278
+ get: () => snapshotControlPlaneOutput(state.controlPlaneOutput),
1279
+ });
1280
+ }
1281
+
1282
+ function ensureControlPlaneOutputState(state = {}) {
1283
+ if (state.controlPlaneOutput) return state;
1284
+ let existingOutput = '';
1285
+ if (typeof state.output === 'string') {
1286
+ existingOutput = state.output;
1287
+ } else if (typeof state.fullOutput === 'string') {
1288
+ existingOutput = state.fullOutput;
1289
+ }
1290
+ state.controlPlaneOutput = createControlPlaneOutputState();
1291
+ if (Object.hasOwn(state, 'output')) delete state.output;
1292
+ if (Object.hasOwn(state, 'fullOutput')) delete state.fullOutput;
1293
+ defineControlPlaneOutputAccessor(state, 'output');
1294
+ if (existingOutput) {
1295
+ replayCompleteLogContent(existingOutput, (content) =>
1296
+ appendControlPlaneRecord(state.controlPlaneOutput, {
1297
+ content,
1298
+ timestamp: Date.now(),
1299
+ type: isValidJsonLine(content) ? 'json' : 'text',
1300
+ })
1301
+ );
1302
+ }
1303
+ return state;
1304
+ }
1305
+
1306
+ function retainedControlPlaneRecords(outputState) {
1307
+ return outputState.records.slice(outputState.head);
1308
+ }
1309
+
1310
+ function compactControlPlaneRecords(outputState) {
1311
+ if (outputState.head >= 64 && outputState.head * 2 >= outputState.records.length) {
1312
+ outputState.records = outputState.records.slice(outputState.head);
1313
+ outputState.head = 0;
1314
+ }
1315
+ }
1316
+
1317
+ function omitOldestControlPlaneRecord(outputState) {
1318
+ const record = outputState.records[outputState.head++];
1319
+ outputState.byteLength -= record.byteLength;
1320
+ outputState.omittedBytes += record.byteLength;
1321
+ outputState.omittedRecords++;
1322
+ outputState.omittedDigest.update(record.text);
1323
+ compactControlPlaneRecords(outputState);
1324
+ }
1325
+
1326
+ function appendControlPlaneRecord(outputState, { content, timestamp, type }) {
1327
+ const text = `${content}\n`;
1328
+ const record = {
1329
+ sequence: outputState.nextSequence++,
1330
+ content,
1331
+ timestamp,
1332
+ type,
1333
+ text,
1334
+ byteLength: Buffer.byteLength(text),
1335
+ };
1336
+ outputState.records.push(record);
1337
+ outputState.byteLength += record.byteLength;
1338
+
1339
+ const payloadLimit = outputState.maxBytes - CONTROL_PLANE_OMISSION_MARKER_BYTES;
1340
+ while (
1341
+ outputState.byteLength > payloadLimit ||
1342
+ outputState.records.length - outputState.head > outputState.maxRecords
1343
+ ) {
1344
+ omitOldestControlPlaneRecord(outputState);
1345
+ }
1346
+ return record;
1347
+ }
1348
+
1349
+ function controlPlaneOmissionRecord(outputState) {
1350
+ if (!outputState.prefixOmitted && outputState.omittedRecords === 0) return null;
1351
+ const digest = outputState.omittedRecords
1352
+ ? `, sha256=${outputState.omittedDigest.copy().digest('hex')}`
1353
+ : '';
1354
+ const known = outputState.omittedRecords
1355
+ ? `records=${outputState.omittedRecords}, byte_length=${outputState.omittedBytes}${digest}`
1356
+ : 'byte_length=unknown';
1357
+ const content =
1358
+ `[ZEROSHOT] Earlier provider output omitted from the bounded control-plane tail (${known}). ` +
1359
+ 'Complete output remains in the task log.';
1360
+ return {
1361
+ content,
1362
+ timestamp: Date.now(),
1363
+ type: 'text',
1364
+ text: `${content}\n`,
1365
+ byteLength: Buffer.byteLength(`${content}\n`),
1366
+ };
1367
+ }
1368
+
1369
+ function snapshotControlPlaneOutput(outputState) {
1370
+ const omission = controlPlaneOmissionRecord(outputState);
1371
+ const records = retainedControlPlaneRecords(outputState);
1372
+ return `${omission ? omission.text : ''}${records.map((record) => record.text).join('')}`;
1373
+ }
1374
+
1375
+ function resetControlPlaneTail(outputState, { prefixOmitted = false } = {}) {
1376
+ outputState.records = [];
1377
+ outputState.head = 0;
1378
+ outputState.byteLength = 0;
1379
+ outputState.omittedBytes = 0;
1380
+ outputState.omittedRecords = 0;
1381
+ outputState.omittedDigest = createHash('sha256');
1382
+ outputState.prefixOmitted = prefixOmitted;
1383
+ }
1384
+
1385
+ function publishLiveControlPlaneRecord(outputState, record, publish) {
1386
+ if (
1387
+ outputState.liveSuppressed ||
1388
+ outputState.liveBytes + record.byteLength > outputState.liveByteLimit ||
1389
+ outputState.liveRecords + 1 > outputState.liveRecordLimit
1390
+ ) {
1391
+ outputState.liveSuppressed = true;
1392
+ return;
1393
+ }
1394
+ publish(record);
1395
+ outputState.liveBytes += record.byteLength;
1396
+ outputState.liveRecords++;
1397
+ outputState.lastLiveSequence = record.sequence;
1398
+ }
1399
+
1400
+ function flushTerminalControlPlaneOutput(outputState, publish) {
1401
+ if (outputState.terminalFlushed) return;
1402
+ outputState.terminalFlushed = true;
1403
+ if (outputState.liveSuppressed) {
1404
+ const omission = controlPlaneOmissionRecord(outputState);
1405
+ if (omission) publish(omission);
1406
+ }
1407
+ for (const record of retainedControlPlaneRecords(outputState)) {
1408
+ if (record.sequence > outputState.lastLiveSequence) publish(record);
1409
+ }
1410
+ }
1411
+
1412
+ function createLogRecordBuffer() {
1413
+ return {
1414
+ byteLength: 0,
1415
+ fragments: [],
1416
+ oversized: null,
1417
+ };
1231
1418
  }
1232
1419
 
1233
1420
  function lookupLogFilePath(ctPath, taskId) {
@@ -1276,31 +1463,16 @@ function isValidJsonLine(content) {
1276
1463
  }
1277
1464
  }
1278
1465
 
1279
- function broadcastAgentLine({ agent, providerName, state, line }) {
1280
- if (!line.trim()) return;
1281
-
1282
- const { timestamp, content } = parseTimestampedLine(line);
1283
- if (shouldSkipLogLine(content)) {
1284
- return;
1285
- }
1286
-
1287
- const isValidJson = isValidJsonLine(content);
1288
- state.output += content + '\n';
1289
-
1290
- if (!state.nested) {
1291
- agent.lastOutputTime = Date.now();
1292
- }
1293
-
1466
+ function publishAgentOutputRecord(agent, providerName, record) {
1294
1467
  agent._publish({
1295
1468
  topic: 'AGENT_OUTPUT',
1296
1469
  receiver: 'broadcast',
1297
1470
  metadata: buildRawLogOnlyMetadata(),
1298
- timestamp,
1471
+ timestamp: record.timestamp,
1299
1472
  content: {
1300
- text: content,
1301
1473
  data: {
1302
- type: isValidJson ? 'json' : 'text',
1303
- line: content,
1474
+ type: record.type,
1475
+ line: record.content,
1304
1476
  agent: agent.id,
1305
1477
  role: agent.role,
1306
1478
  iteration: agent.iteration,
@@ -1310,15 +1482,114 @@ function broadcastAgentLine({ agent, providerName, state, line }) {
1310
1482
  });
1311
1483
  }
1312
1484
 
1313
- function appendContentToBuffer(state, content, onLine) {
1314
- state.lineBuffer += content;
1315
- const lines = state.lineBuffer.split('\n');
1485
+ function broadcastAgentLine({ agent, providerName, state, line }) {
1486
+ const followerState = ensureControlPlaneOutputState(state);
1487
+ if (!line.trim()) return;
1488
+
1489
+ const { timestamp, content } = parseTimestampedLine(line);
1490
+ if (shouldSkipLogLine(content)) {
1491
+ return;
1492
+ }
1493
+
1494
+ const isValidJson = isValidJsonLine(content);
1495
+ const record = appendControlPlaneRecord(followerState.controlPlaneOutput, {
1496
+ content,
1497
+ timestamp,
1498
+ type: isValidJson ? 'json' : 'text',
1499
+ });
1500
+
1501
+ if (!followerState.nested) {
1502
+ agent.lastOutputTime = Date.now();
1503
+ }
1504
+
1505
+ publishLiveControlPlaneRecord(followerState.controlPlaneOutput, record, (item) =>
1506
+ publishAgentOutputRecord(agent, providerName, item)
1507
+ );
1508
+ }
1509
+
1510
+ function flushAgentOutput(agent, providerName, state) {
1511
+ const followerState = ensureControlPlaneOutputState(state);
1512
+ flushTerminalControlPlaneOutput(followerState.controlPlaneOutput, (record) =>
1513
+ publishAgentOutputRecord(agent, providerName, record)
1514
+ );
1515
+ }
1516
+
1517
+ function appendLogRecordFragment(buffer, fragment) {
1518
+ const fragmentBytes = Buffer.byteLength(fragment);
1519
+ buffer.byteLength += fragmentBytes;
1520
+ if (buffer.oversized) {
1521
+ buffer.oversized.digest.update(fragment);
1522
+ return;
1523
+ }
1524
+ if (buffer.byteLength <= MAX_CONTROL_PLANE_RECORD_BYTES) {
1525
+ buffer.fragments.push(fragment);
1526
+ return;
1527
+ }
1528
+
1529
+ const retainedPrefix = buffer.fragments.join('');
1530
+ const prefixProbe = `${retainedPrefix}${fragment.slice(0, Math.max(0, 32 - retainedPrefix.length))}`;
1531
+ const timestampPrefix = prefixProbe.match(/^\[\d{13}\]/)?.[0] || '';
1532
+ const digest = createHash('sha256');
1533
+ for (const retained of buffer.fragments) digest.update(retained);
1534
+ digest.update(fragment);
1535
+ buffer.fragments = [];
1536
+ buffer.oversized = { digest, timestampPrefix };
1537
+ }
1538
+
1539
+ function completeLogRecord(buffer, onLine, skipEmpty = false) {
1540
+ let line;
1541
+ if (buffer.oversized) {
1542
+ const digest = buffer.oversized.digest.digest('hex');
1543
+ line =
1544
+ `${buffer.oversized.timestampPrefix}[ZEROSHOT] Provider output record retained in task log ` +
1545
+ `but omitted from the control plane (byte_length=${buffer.byteLength}, sha256=${digest})`;
1546
+ } else {
1547
+ line = buffer.fragments.join('');
1548
+ }
1549
+ buffer.byteLength = 0;
1550
+ buffer.fragments = [];
1551
+ buffer.oversized = null;
1552
+ if (!skipEmpty || line.trim()) onLine(line);
1553
+ }
1554
+
1555
+ function appendContentToBuffer(state, content, onLine, skipEmpty = false) {
1556
+ let offset = 0;
1557
+ while (offset < content.length) {
1558
+ const newline = content.indexOf('\n', offset);
1559
+ if (newline === -1) {
1560
+ appendLogRecordFragment(state.lineBuffer, content.slice(offset));
1561
+ return;
1562
+ }
1563
+ appendLogRecordFragment(state.lineBuffer, content.slice(offset, newline));
1564
+ completeLogRecord(state.lineBuffer, onLine, skipEmpty);
1565
+ offset = newline + 1;
1566
+ }
1567
+ }
1316
1568
 
1317
- for (let i = 0; i < lines.length - 1; i++) {
1318
- onLine(lines[i]);
1569
+ function replayCompleteLogContent(content, onLine) {
1570
+ const replayState = { lineBuffer: createLogRecordBuffer() };
1571
+ appendContentToBuffer(replayState, content, onLine, true);
1572
+ if (replayState.lineBuffer.byteLength > 0) {
1573
+ completeLogRecord(replayState.lineBuffer, onLine, true);
1319
1574
  }
1575
+ }
1320
1576
 
1321
- state.lineBuffer = lines[lines.length - 1];
1577
+ function readLogFileDelta({ fsModule, logFilePath, state, currentSize, onNewContent }) {
1578
+ const fd = fsModule.openSync(logFilePath, 'r');
1579
+ try {
1580
+ let offset = state.lastSize;
1581
+ const buffer = Buffer.allocUnsafe(LOG_READ_CHUNK_BYTES);
1582
+ while (offset < currentSize) {
1583
+ const requested = Math.min(buffer.length, currentSize - offset);
1584
+ const bytesRead = fsModule.readSync(fd, buffer, 0, requested, offset);
1585
+ if (bytesRead === 0) break;
1586
+ onNewContent(state.logDecoder.write(buffer.subarray(0, bytesRead)));
1587
+ offset += bytesRead;
1588
+ }
1589
+ state.lastSize = offset;
1590
+ } finally {
1591
+ fsModule.closeSync(fd);
1592
+ }
1322
1593
  }
1323
1594
 
1324
1595
  function pollLogFileForUpdates({ agent, fsModule, ctPath, taskId, state, onNewContent }) {
@@ -1340,13 +1611,13 @@ function pollLogFileForUpdates({ agent, fsModule, ctPath, taskId, state, onNewCo
1340
1611
  const currentSize = stats.size;
1341
1612
 
1342
1613
  if (currentSize > state.lastSize) {
1343
- const fd = fsModule.openSync(state.logFilePath, 'r');
1344
- const buffer = Buffer.alloc(currentSize - state.lastSize);
1345
- fsModule.readSync(fd, buffer, 0, buffer.length, state.lastSize);
1346
- fsModule.closeSync(fd);
1347
-
1348
- onNewContent(buffer.toString('utf-8'));
1349
- state.lastSize = currentSize;
1614
+ readLogFileDelta({
1615
+ fsModule,
1616
+ logFilePath: state.logFilePath,
1617
+ state,
1618
+ currentSize,
1619
+ onNewContent,
1620
+ });
1350
1621
  }
1351
1622
  } catch (err) {
1352
1623
  const error = /** @type {Error} */ (err);
@@ -1536,7 +1807,29 @@ function finalizeLogFollow(agent, state) {
1536
1807
  }
1537
1808
  }
1538
1809
 
1539
- function handleStatusExecError({ agent, state, ctPath, taskId, error, stderr, resolve }) {
1810
+ function settleHostStatusFailure({ agent, providerName, state, resolve, text, data, error }) {
1811
+ if (state.resolved) return;
1812
+ state.resolved = true;
1813
+ finalizeLogFollow(agent, state);
1814
+ flushAgentOutput(agent, providerName, state);
1815
+ agent._publish({
1816
+ topic: 'AGENT_ERROR',
1817
+ receiver: 'broadcast',
1818
+ content: { text, data },
1819
+ });
1820
+ resolve({ success: false, output: state.output, error });
1821
+ }
1822
+
1823
+ function handleStatusExecError({
1824
+ agent,
1825
+ providerName,
1826
+ state,
1827
+ ctPath,
1828
+ taskId,
1829
+ error,
1830
+ stderr,
1831
+ resolve,
1832
+ }) {
1540
1833
  if (!error) {
1541
1834
  return false;
1542
1835
  }
@@ -1561,30 +1854,20 @@ function handleStatusExecError({ agent, state, ctPath, taskId, error, stderr, re
1561
1854
  `[Agent ${agent.id}] ⚠️ Task ${taskId} not found - will restart to ensure completion`
1562
1855
  );
1563
1856
 
1564
- if (!state.resolved) {
1565
- state.resolved = true;
1566
- finalizeLogFollow(agent, state);
1567
-
1568
- agent._publish({
1569
- topic: 'AGENT_ERROR',
1570
- receiver: 'broadcast',
1571
- content: {
1572
- text: `Task ${taskId} not found - restarting for safety`,
1573
- data: {
1574
- taskId,
1575
- error: 'task_not_found',
1576
- role: agent.role,
1577
- iteration: agent.iteration,
1578
- },
1579
- },
1580
- });
1581
-
1582
- resolve({
1583
- success: false,
1584
- output: state.output,
1585
- error: `Task not found - restarting for safety`,
1586
- });
1587
- }
1857
+ settleHostStatusFailure({
1858
+ agent,
1859
+ providerName,
1860
+ state,
1861
+ resolve,
1862
+ text: `Task ${taskId} not found - restarting for safety`,
1863
+ data: {
1864
+ taskId,
1865
+ error: 'task_not_found',
1866
+ role: agent.role,
1867
+ iteration: agent.iteration,
1868
+ },
1869
+ error: 'Task not found - restarting for safety',
1870
+ });
1588
1871
 
1589
1872
  return true;
1590
1873
  }
@@ -1602,31 +1885,21 @@ function handleStatusExecError({ agent, state, ctPath, taskId, error, stderr, re
1602
1885
  console.error(` Stderr: ${stderr || 'none'}`);
1603
1886
  console.error(` This may indicate zeroshot is not in PATH or task storage is corrupted.`);
1604
1887
 
1605
- if (!state.resolved) {
1606
- state.resolved = true;
1607
- finalizeLogFollow(agent, state);
1608
-
1609
- agent._publish({
1610
- topic: 'AGENT_ERROR',
1611
- receiver: 'broadcast',
1612
- content: {
1613
- text: `Task ${taskId} polling failed after ${MAX_STATUS_FAILURES} consecutive failures`,
1614
- data: {
1615
- taskId,
1616
- error: 'polling_timeout',
1617
- attempts: state.consecutiveExecFailures,
1618
- role: agent.role,
1619
- iteration: agent.iteration,
1620
- },
1621
- },
1622
- });
1623
-
1624
- resolve({
1625
- success: false,
1626
- output: state.output,
1627
- error: `Status polling failed ${MAX_STATUS_FAILURES} times - task may not exist`,
1628
- });
1629
- }
1888
+ settleHostStatusFailure({
1889
+ agent,
1890
+ providerName,
1891
+ state,
1892
+ resolve,
1893
+ text: `Task ${taskId} polling failed after ${MAX_STATUS_FAILURES} consecutive failures`,
1894
+ data: {
1895
+ taskId,
1896
+ error: 'polling_timeout',
1897
+ attempts: state.consecutiveExecFailures,
1898
+ role: agent.role,
1899
+ iteration: agent.iteration,
1900
+ },
1901
+ error: `Status polling failed ${MAX_STATUS_FAILURES} times - task may not exist`,
1902
+ });
1630
1903
 
1631
1904
  return true;
1632
1905
  }
@@ -1681,6 +1954,7 @@ function handleStatusCompletion({
1681
1954
  state.resolved = true;
1682
1955
 
1683
1956
  finalizeLogFollow(agent, state);
1957
+ flushAgentOutput(agent, providerName, state);
1684
1958
 
1685
1959
  buildCompletionResult({
1686
1960
  agent,
@@ -1703,6 +1977,7 @@ function buildKillHandler({ agent, taskId, state, providerName, resolve }) {
1703
1977
  if (state.resolved) return;
1704
1978
  state.resolved = true;
1705
1979
  finalizeLogFollow(agent, state);
1980
+ flushAgentOutput(agent, providerName, state);
1706
1981
  if (!state.nested) {
1707
1982
  agent._stopLivenessCheck();
1708
1983
  }
@@ -1762,7 +2037,18 @@ function createLogFollower({
1762
2037
  (error, stdout, stderr) => {
1763
2038
  if (state.resolved) return;
1764
2039
 
1765
- if (handleStatusExecError({ agent, state, ctPath, taskId, error, stderr, resolve })) {
2040
+ if (
2041
+ handleStatusExecError({
2042
+ agent,
2043
+ providerName,
2044
+ state,
2045
+ ctPath,
2046
+ taskId,
2047
+ error,
2048
+ stderr,
2049
+ resolve,
2050
+ })
2051
+ ) {
1766
2052
  return;
1767
2053
  }
1768
2054
 
@@ -2203,7 +2489,7 @@ async function spawnClaudeTaskIsolatedExecution(agent, context, options = {}) {
2203
2489
  * - Result: 10-20% overall latency reduction
2204
2490
  */
2205
2491
  function createIsolatedLogState(skipStructuredResultCheck = false, nested = false) {
2206
- return {
2492
+ const state = {
2207
2493
  taskExited: false,
2208
2494
  resolved: false,
2209
2495
  terminationPromise: null,
@@ -2211,14 +2497,20 @@ function createIsolatedLogState(skipStructuredResultCheck = false, nested = fals
2211
2497
  durableTaskStatus: null,
2212
2498
  lifecycleHandle: null,
2213
2499
  logFilePath: null,
2214
- fullOutput: '',
2500
+ controlPlaneOutput: createControlPlaneOutputState(),
2215
2501
  tailProcess: null,
2502
+ rawBytesSeen: 0,
2503
+ ignoreTailOutput: false,
2216
2504
  statusCheckInterval: null,
2217
2505
  timeoutTimer: null,
2218
- lineBuffer: '',
2506
+ lineBuffer: createLogRecordBuffer(),
2507
+ tailDecoder: new StringDecoder('utf8'),
2219
2508
  skipStructuredResultCheck,
2220
2509
  nested,
2221
2510
  };
2511
+ defineControlPlaneOutputAccessor(state, 'output');
2512
+ defineControlPlaneOutputAccessor(state, 'fullOutput');
2513
+ return state;
2222
2514
  }
2223
2515
 
2224
2516
  function buildIsolatedCleanup(state) {
@@ -2341,6 +2633,41 @@ async function resolveIsolatedLogFilePath(manager, clusterId, taskId, state) {
2341
2633
  return state.logFilePath;
2342
2634
  }
2343
2635
 
2636
+ async function captureIsolatedFinalOutputTail(manager, clusterId, logFilePath, state) {
2637
+ stopIsolatedTailForSettlement(state);
2638
+ const sizeResult = await manager.execInContainer(clusterId, [
2639
+ 'sh',
2640
+ '-c',
2641
+ `wc -c < "${logFilePath}" 2>/dev/null || echo 0`,
2642
+ ]);
2643
+ const fileSize = Number.parseInt(sizeResult.stdout.trim(), 10);
2644
+ const missingBytes = Number.isSafeInteger(fileSize)
2645
+ ? Math.max(0, fileSize - state.rawBytesSeen)
2646
+ : 0;
2647
+
2648
+ if (missingBytes > 0) {
2649
+ const readBytes = Math.min(missingBytes, MAX_CONTROL_PLANE_OUTPUT_BYTES);
2650
+ const finalReadResult = await manager.execInContainer(clusterId, [
2651
+ 'sh',
2652
+ '-c',
2653
+ `tail -c ${readBytes} "${logFilePath}" 2>/dev/null || echo ""`,
2654
+ ]);
2655
+ if (finalReadResult.code === 0 && finalReadResult.stdout) {
2656
+ if (missingBytes > readBytes) {
2657
+ resetControlPlaneTail(state.controlPlaneOutput, { prefixOmitted: true });
2658
+ state.lineBuffer = createLogRecordBuffer();
2659
+ }
2660
+ appendIsolatedContent(state, finalReadResult.stdout, (line) =>
2661
+ retainIsolatedLine(state, line)
2662
+ );
2663
+ }
2664
+ }
2665
+
2666
+ if (state.lineBuffer.byteLength > 0) {
2667
+ completeLogRecord(state.lineBuffer, (line) => retainIsolatedLine(state, line), true);
2668
+ }
2669
+ }
2670
+
2344
2671
  function settleIsolatedTerminalStatus({
2345
2672
  agent,
2346
2673
  manager,
@@ -2353,7 +2680,6 @@ function settleIsolatedTerminalStatus({
2353
2680
  cleanup,
2354
2681
  resolve,
2355
2682
  reject,
2356
- onLine,
2357
2683
  }) {
2358
2684
  if (state.resolved) return Promise.resolve();
2359
2685
  if (state.terminalSettlementPromise) return state.terminalSettlementPromise;
@@ -2367,19 +2693,9 @@ function settleIsolatedTerminalStatus({
2367
2693
  const logFilePath = await resolveIsolatedLogFilePath(manager, clusterId, taskId, state);
2368
2694
  await new Promise((settle) => setTimeout(settle, 200));
2369
2695
  if (state.resolved) return;
2370
- const finalReadResult = await manager.execInContainer(clusterId, [
2371
- 'sh',
2372
- '-c',
2373
- `cat "${logFilePath}" 2>/dev/null || echo ""`,
2374
- ]);
2696
+ await captureIsolatedFinalOutputTail(manager, clusterId, logFilePath, state);
2375
2697
  if (state.resolved) return;
2376
-
2377
- if (finalReadResult.code === 0 && finalReadResult.stdout) {
2378
- state.fullOutput = finalReadResult.stdout;
2379
- for (const line of state.fullOutput.split('\n')) {
2380
- if (line.trim()) onLine(line);
2381
- }
2382
- }
2698
+ flushIsolatedOutput(agent, providerName, taskId, state);
2383
2699
 
2384
2700
  const vertexModelError =
2385
2701
  providerName === 'claude'
@@ -2463,9 +2779,9 @@ function buildIsolatedLifecycleHandle({
2463
2779
  cleanup,
2464
2780
  resolve,
2465
2781
  reject,
2466
- onLine,
2467
2782
  }) {
2468
- const settleCancellation = (reason, details) =>
2783
+ const settleCancellation = (reason, details) => {
2784
+ flushIsolatedOutput(agent, providerName, taskId, state);
2469
2785
  settleIsolatedFollower({
2470
2786
  agent,
2471
2787
  state,
@@ -2480,6 +2796,7 @@ function buildIsolatedLifecycleHandle({
2480
2796
  tokenUsage: extractTokenUsage(state.fullOutput, providerName),
2481
2797
  },
2482
2798
  });
2799
+ };
2483
2800
  const terminate = (reason = 'Task killed', details = {}) => {
2484
2801
  if (state.durableTaskTerminal) {
2485
2802
  if (state.nested) settleCancellation(reason, details);
@@ -2511,7 +2828,6 @@ function buildIsolatedLifecycleHandle({
2511
2828
  cleanup,
2512
2829
  resolve,
2513
2830
  reject,
2514
- onLine,
2515
2831
  });
2516
2832
  return termination;
2517
2833
  }
@@ -2539,11 +2855,14 @@ function buildIsolatedLifecycleHandle({
2539
2855
  };
2540
2856
  }
2541
2857
 
2542
- function broadcastIsolatedLine({ agent, providerName, taskId, state, line }) {
2858
+ function parseIsolatedLogLine(line) {
2543
2859
  const timestampMatch = line.match(/^\[(\d{4}-\d{2}-\d{2}T[^\]]+)\]\s*(.*)$/);
2544
2860
  const timestamp = timestampMatch ? new Date(timestampMatch[1]).getTime() : Date.now();
2545
2861
  const content = timestampMatch ? timestampMatch[2] : line;
2862
+ return { timestamp, content };
2863
+ }
2546
2864
 
2865
+ function publishIsolatedOutputRecord(agent, providerName, taskId, record) {
2547
2866
  agent.messageBus.publish({
2548
2867
  cluster_id: agent.cluster.id,
2549
2868
  topic: 'AGENT_OUTPUT',
@@ -2551,31 +2870,64 @@ function broadcastIsolatedLine({ agent, providerName, taskId, state, line }) {
2551
2870
  metadata: buildRawLogOnlyMetadata(),
2552
2871
  content: {
2553
2872
  data: {
2554
- line: content,
2873
+ line: record.content,
2555
2874
  taskId,
2556
2875
  iteration: agent.iteration,
2557
2876
  provider: providerName,
2558
2877
  },
2559
2878
  },
2879
+ timestamp: record.timestamp,
2880
+ });
2881
+ }
2882
+
2883
+ function retainIsolatedLine(state, line) {
2884
+ const { timestamp, content } = parseIsolatedLogLine(line);
2885
+ return appendControlPlaneRecord(state.controlPlaneOutput, {
2886
+ content,
2560
2887
  timestamp,
2888
+ type: isValidJsonLine(content) ? 'json' : 'text',
2561
2889
  });
2890
+ }
2891
+
2892
+ function broadcastIsolatedLine({ agent, providerName, taskId, state, line }) {
2893
+ const followerState = ensureControlPlaneOutputState(state);
2894
+ const record = retainIsolatedLine(followerState, line);
2895
+ publishLiveControlPlaneRecord(followerState.controlPlaneOutput, record, (item) =>
2896
+ publishIsolatedOutputRecord(agent, providerName, taskId, item)
2897
+ );
2562
2898
 
2563
- if (!state?.nested) {
2899
+ if (!followerState.nested) {
2564
2900
  agent.lastOutputTime = Date.now();
2565
2901
  }
2566
2902
  }
2567
2903
 
2904
+ function flushIsolatedOutput(agent, providerName, taskId, state) {
2905
+ const followerState = ensureControlPlaneOutputState(state);
2906
+ flushTerminalControlPlaneOutput(followerState.controlPlaneOutput, (record) =>
2907
+ publishIsolatedOutputRecord(agent, providerName, taskId, record)
2908
+ );
2909
+ }
2910
+
2568
2911
  function appendIsolatedContent(state, content, onLine) {
2569
- state.lineBuffer += content;
2570
- const lines = state.lineBuffer.split('\n');
2912
+ appendContentToBuffer(state, content, onLine, true);
2913
+ }
2571
2914
 
2572
- for (let i = 0; i < lines.length - 1; i++) {
2573
- if (lines[i].trim()) {
2574
- onLine(lines[i]);
2575
- }
2576
- }
2915
+ function consumeIsolatedTailChunk(state, data, onLine) {
2916
+ const chunk = typeof data === 'string' ? data : state.tailDecoder.write(data);
2917
+ if (!chunk || state.ignoreTailOutput) return;
2918
+ state.rawBytesSeen += Buffer.byteLength(chunk);
2919
+ appendIsolatedContent(state, chunk, onLine);
2920
+ }
2577
2921
 
2578
- state.lineBuffer = lines[lines.length - 1];
2922
+ function stopIsolatedTailForSettlement(state) {
2923
+ state.ignoreTailOutput = true;
2924
+ if (!state.tailProcess) return;
2925
+ try {
2926
+ state.tailProcess.kill('SIGTERM');
2927
+ } catch {
2928
+ // Ignore - process may already be dead.
2929
+ }
2930
+ state.tailProcess = null;
2579
2931
  }
2580
2932
 
2581
2933
  function startIsolatedTail({ agent, manager, clusterId, logFilePath, state, onLine }) {
@@ -2586,9 +2938,7 @@ function startIsolatedTail({ agent, manager, clusterId, logFilePath, state, onLi
2586
2938
  ]);
2587
2939
 
2588
2940
  state.tailProcess.stdout.on('data', (data) => {
2589
- const chunk = data.toString();
2590
- state.fullOutput += chunk;
2591
- appendIsolatedContent(state, chunk, onLine);
2941
+ consumeIsolatedTailChunk(state, data, onLine);
2592
2942
  });
2593
2943
 
2594
2944
  state.tailProcess.stderr.on('data', (data) => {
@@ -2599,6 +2949,7 @@ function startIsolatedTail({ agent, manager, clusterId, logFilePath, state, onLi
2599
2949
  });
2600
2950
 
2601
2951
  state.tailProcess.on('close', (exitCode) => {
2952
+ consumeIsolatedTailChunk(state, state.tailDecoder.end(), onLine);
2602
2953
  if (!state.taskExited) {
2603
2954
  agent._log(`[${agent.id}] tail process exited with code ${exitCode}`);
2604
2955
  }
@@ -2620,7 +2971,6 @@ async function checkIsolatedStatus({
2620
2971
  cleanup,
2621
2972
  resolve,
2622
2973
  reject,
2623
- onLine,
2624
2974
  }) {
2625
2975
  if (state.taskExited) return;
2626
2976
 
@@ -2673,7 +3023,6 @@ async function checkIsolatedStatus({
2673
3023
  cleanup,
2674
3024
  resolve,
2675
3025
  reject,
2676
- onLine,
2677
3026
  });
2678
3027
  }
2679
3028
 
@@ -2688,7 +3037,6 @@ function startIsolatedStatusChecks({
2688
3037
  cleanup,
2689
3038
  resolve,
2690
3039
  reject,
2691
- onLine,
2692
3040
  }) {
2693
3041
  state.statusCheckInterval = setInterval(() => {
2694
3042
  checkIsolatedStatus({
@@ -2702,7 +3050,6 @@ function startIsolatedStatusChecks({
2702
3050
  cleanup,
2703
3051
  resolve,
2704
3052
  reject,
2705
- onLine,
2706
3053
  }).catch((statusErr) => {
2707
3054
  agent._log(`[${agent.id}] Status check error (will retry): ${statusErr.message}`);
2708
3055
  });
@@ -2736,7 +3083,6 @@ function followClaudeTaskLogsIsolated(agent, taskId, options = {}) {
2736
3083
  cleanup,
2737
3084
  resolve,
2738
3085
  reject,
2739
- onLine,
2740
3086
  });
2741
3087
  // Only register the lifecycle handle on the agent for top-level tasks.
2742
3088
  if (!options.nested) {
@@ -2807,7 +3153,6 @@ function followClaudeTaskLogsIsolated(agent, taskId, options = {}) {
2807
3153
  cleanup,
2808
3154
  resolve,
2809
3155
  reject,
2810
- onLine,
2811
3156
  });
2812
3157
 
2813
3158
  if (agent.timeout > 0 && !agent.enableLivenessCheck && !options.nested) {
@@ -3091,4 +3436,19 @@ module.exports = {
3091
3436
  buildTaskRunArgs,
3092
3437
  rebuildProviderSessionAfterCommit,
3093
3438
  killTask,
3439
+ createLogFollowState,
3440
+ createIsolatedLogState,
3441
+ createControlPlaneOutputState,
3442
+ appendControlPlaneRecord,
3443
+ createLogRecordBuffer,
3444
+ appendContentToBuffer,
3445
+ consumeIsolatedTailChunk,
3446
+ flushAgentOutput,
3447
+ flushIsolatedOutput,
3448
+ CONTROL_PLANE_OUTPUT_LIMITS: Object.freeze({
3449
+ maxBytes: MAX_CONTROL_PLANE_OUTPUT_BYTES,
3450
+ maxRecords: MAX_CONTROL_PLANE_OUTPUT_RECORDS,
3451
+ liveBytes: MAX_LIVE_OUTPUT_BYTES,
3452
+ liveRecords: MAX_LIVE_OUTPUT_RECORDS,
3453
+ }),
3094
3454
  };