@the-open-engine/zeroshot 6.39.2 → 6.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +12 -0
  2. package/cli/agent-provider-boundary.js +19 -0
  3. package/cli/agent-provider-boundary.ts +79 -0
  4. package/cli/export-stream.js +94 -0
  5. package/cli/export-stream.ts +118 -0
  6. package/cli/index.js +102 -54
  7. package/cli/json-export.js +2 -38
  8. package/cli/json-export.ts +2 -56
  9. package/cli/semantic-canonical.js +60 -0
  10. package/cli/semantic-canonical.ts +59 -0
  11. package/cli/semantic-contract.js +97 -0
  12. package/cli/semantic-contract.ts +153 -0
  13. package/cli/semantic-events.js +53 -0
  14. package/cli/semantic-events.ts +65 -0
  15. package/cli/semantic-evidence.js +63 -0
  16. package/cli/semantic-evidence.ts +65 -0
  17. package/cli/semantic-export.js +186 -0
  18. package/cli/semantic-export.ts +245 -0
  19. package/cli/semantic-json.js +69 -0
  20. package/cli/semantic-json.ts +69 -0
  21. package/cli/semantic-line-scanner.js +56 -0
  22. package/cli/semantic-line-scanner.ts +54 -0
  23. package/cli/semantic-parser.js +78 -0
  24. package/cli/semantic-parser.ts +95 -0
  25. package/cli/semantic-provider-line.js +91 -0
  26. package/cli/semantic-provider-line.ts +106 -0
  27. package/cli/trace-evidence.js +86 -0
  28. package/cli/trace-evidence.ts +115 -0
  29. package/cli/trace-export.js +120 -0
  30. package/cli/trace-export.ts +164 -0
  31. package/cli/trace-output-record.js +15 -0
  32. package/cli/trace-output-record.ts +18 -0
  33. package/cli/trace-output.js +98 -0
  34. package/cli/trace-output.ts +119 -0
  35. package/lib/agent-cli-provider/log-prefix.d.ts.map +1 -1
  36. package/lib/agent-cli-provider/log-prefix.js +8 -6
  37. package/lib/agent-cli-provider/log-prefix.js.map +1 -1
  38. package/lib/stream-json-parser.js +6 -5
  39. package/npm-shrinkwrap.json +2 -2
  40. package/package.json +6 -4
  41. package/src/agent/agent-task-executor.js +36 -40
  42. package/src/agent/output-extraction-json.js +5 -6
  43. package/src/agent/output-extraction-json.ts +4 -7
  44. package/src/agent-cli-provider/log-prefix.ts +12 -7
  45. package/src/claude-task-runner.js +9 -9
  46. package/src/legacy-lib/stream-json-parser.ts +10 -5
  47. package/src/providers/index.js +1 -19
  48. package/src/task-log-line.d.ts +20 -0
  49. package/src/task-log-line.js +80 -0
  50. package/task-lib/commands/logs.js +5 -8
  51. package/task-lib/sdk-watcher-output.js +31 -0
  52. package/task-lib/sdk-watcher.js +4 -11
  53. package/task-lib/watcher-output-runtime.js +30 -16
@@ -18,6 +18,7 @@ const { StringDecoder } = require('string_decoder');
18
18
  const { getNestedExecutionRegistry, TaskExecutionHandle } = require('./task-execution-handle');
19
19
  const os = require('os');
20
20
  const { parseProviderChunk, getProvider } = require('../providers');
21
+ const { decodeTaskLogLine } = require('../task-log-line');
21
22
  const { getTask, getTaskBySpawnOwnershipToken } = require('../../task-lib/store.js');
22
23
  const { OMP_SESSIONLESS_ENV } = require('../../task-lib/omp-storage-root.js');
23
24
  const { loadSettings } = require('../../lib/settings.js');
@@ -1268,9 +1269,10 @@ async function waitForTaskReady(agent, taskId, maxRetries = 10, delayMs = 200) {
1268
1269
 
1269
1270
  const MAX_STATUS_FAILURES = 30;
1270
1271
 
1271
- function createLogFollowState() {
1272
+ function createLogFollowState(taskId = null) {
1272
1273
  const state = {
1273
1274
  controlPlaneOutput: createControlPlaneOutputState(),
1275
+ taskId,
1274
1276
  logFilePath: null,
1275
1277
  lastSize: 0,
1276
1278
  pollInterval: null,
@@ -1471,28 +1473,26 @@ function lookupLogFilePath(ctPath, taskId) {
1471
1473
  }
1472
1474
 
1473
1475
  function parseTimestampedLine(line) {
1474
- let timestamp = Date.now();
1475
- let content = line.replace(/\r$/, '');
1476
- let timestamped = false;
1477
-
1478
- const timestampMatch = content.match(/^\[(\d{13})\](.*)$/);
1479
- if (timestampMatch) {
1480
- timestamp = parseInt(timestampMatch[1], 10);
1481
- content = timestampMatch[2];
1482
- timestamped = true;
1483
- }
1484
-
1485
- return { timestamp, content, timestamped };
1476
+ const decoded = decodeTaskLogLine(line);
1477
+ return {
1478
+ timestamp: decoded.timestamp ?? Date.now(),
1479
+ content: decoded.content,
1480
+ timestamped: decoded.timestamped,
1481
+ providerOutput: decoded.providerOutput,
1482
+ providerStdoutFramed: decoded.channel === 'provider_stdout',
1483
+ };
1486
1484
  }
1487
1485
 
1488
- function shouldSkipLogLine(content, providerName, timestamped) {
1486
+ function shouldSkipLogLine(
1487
+ content,
1488
+ providerName,
1489
+ timestamped,
1490
+ providerOutput,
1491
+ providerStdoutFramed = false
1492
+ ) {
1493
+ if (!providerOutput) return true;
1494
+ if (providerStdoutFramed) return false;
1489
1495
  if (providerName === 'pi') {
1490
- if (
1491
- content.startsWith('[ZEROSHOT][PROVIDER_STDERR] ') ||
1492
- content.startsWith('[ZEROSHOT][FATAL] ')
1493
- ) {
1494
- return true;
1495
- }
1496
1496
  if (timestamped) return false;
1497
1497
  return (
1498
1498
  /^={50}$/.test(content) ||
@@ -1504,7 +1504,6 @@ function shouldSkipLogLine(content, providerName, timestamped) {
1504
1504
  content.startsWith('===') ||
1505
1505
  content.startsWith('Finished:') ||
1506
1506
  content.startsWith('Exit code:') ||
1507
- content.startsWith('[ZEROSHOT][PROVIDER_STDERR] ') ||
1508
1507
  (content.includes('"type":"system"') && content.includes('"subtype":"init"'))
1509
1508
  );
1510
1509
  }
@@ -1522,7 +1521,7 @@ function isValidJsonLine(content) {
1522
1521
  }
1523
1522
  }
1524
1523
 
1525
- function publishAgentOutputRecord(agent, providerName, record) {
1524
+ function publishAgentOutputRecord(agent, providerName, taskId, record) {
1526
1525
  agent._publish({
1527
1526
  topic: 'AGENT_OUTPUT',
1528
1527
  receiver: 'broadcast',
@@ -1536,6 +1535,7 @@ function publishAgentOutputRecord(agent, providerName, record) {
1536
1535
  role: agent.role,
1537
1536
  iteration: agent.iteration,
1538
1537
  provider: providerName,
1538
+ ...(taskId ? { taskId } : {}),
1539
1539
  },
1540
1540
  },
1541
1541
  });
@@ -1545,8 +1545,9 @@ function broadcastAgentLine({ agent, providerName, state, line }) {
1545
1545
  const followerState = ensureControlPlaneOutputState(state);
1546
1546
  if (!line.trim()) return;
1547
1547
 
1548
- const { timestamp, content, timestamped } = parseTimestampedLine(line);
1549
- if (shouldSkipLogLine(content, providerName, timestamped)) {
1548
+ const { timestamp, content, timestamped, providerOutput, providerStdoutFramed } =
1549
+ parseTimestampedLine(line);
1550
+ if (shouldSkipLogLine(content, providerName, timestamped, providerOutput, providerStdoutFramed)) {
1550
1551
  return;
1551
1552
  }
1552
1553
  const controlPlaneContent = redactTerminalFailureForControlPlane(
@@ -1567,14 +1568,14 @@ function broadcastAgentLine({ agent, providerName, state, line }) {
1567
1568
  }
1568
1569
 
1569
1570
  publishLiveControlPlaneRecord(followerState.controlPlaneOutput, record, (item) =>
1570
- publishAgentOutputRecord(agent, providerName, item)
1571
+ publishAgentOutputRecord(agent, providerName, followerState.taskId, item)
1571
1572
  );
1572
1573
  }
1573
1574
 
1574
1575
  function flushAgentOutput(agent, providerName, state) {
1575
1576
  const followerState = ensureControlPlaneOutputState(state);
1576
1577
  flushTerminalControlPlaneOutput(followerState.controlPlaneOutput, (record) =>
1577
- publishAgentOutputRecord(agent, providerName, record)
1578
+ publishAgentOutputRecord(agent, providerName, followerState.taskId, record)
1578
1579
  );
1579
1580
  }
1580
1581
 
@@ -2117,7 +2118,7 @@ function createLogFollower({
2117
2118
  executionHandle = null,
2118
2119
  }) {
2119
2120
  return new Promise((resolve, reject) => {
2120
- const state = createLogFollowState();
2121
+ const state = createLogFollowState(taskId);
2121
2122
  state.skipStructuredResultCheck = skipStructuredResultCheck;
2122
2123
  state.nested = nested;
2123
2124
  state.logFilePath = lookupLogFilePath(ctPath, taskId);
@@ -3022,17 +3023,6 @@ function buildIsolatedLifecycleHandle({
3022
3023
  };
3023
3024
  }
3024
3025
 
3025
- function parseIsolatedLogLine(line) {
3026
- const timestampMatch = line.match(/^\[(\d{13}|\d{4}-\d{2}-\d{2}T[^\]]+)\]\s*(.*)$/);
3027
- const timestamp = timestampMatch
3028
- ? /^\d{13}$/.test(timestampMatch[1])
3029
- ? Number.parseInt(timestampMatch[1], 10)
3030
- : new Date(timestampMatch[1]).getTime()
3031
- : Date.now();
3032
- const content = timestampMatch ? timestampMatch[2] : line;
3033
- return { timestamp, content, timestamped: timestampMatch !== null };
3034
- }
3035
-
3036
3026
  function publishIsolatedOutputRecord(agent, providerName, taskId, record) {
3037
3027
  agent.messageBus.publish({
3038
3028
  cluster_id: agent.cluster.id,
@@ -3052,8 +3042,14 @@ function publishIsolatedOutputRecord(agent, providerName, taskId, record) {
3052
3042
  }
3053
3043
 
3054
3044
  function retainIsolatedLine(state, providerName, line) {
3055
- const { timestamp, content, timestamped } = parseIsolatedLogLine(line);
3056
- if (!content.trim() || shouldSkipLogLine(content, providerName, timestamped)) return null;
3045
+ const { timestamp, content, timestamped, providerOutput, providerStdoutFramed } =
3046
+ parseTimestampedLine(line);
3047
+ if (
3048
+ !content.trim() ||
3049
+ shouldSkipLogLine(content, providerName, timestamped, providerOutput, providerStdoutFramed)
3050
+ ) {
3051
+ return null;
3052
+ }
3057
3053
  const controlPlaneContent = redactTerminalFailureForControlPlane(state, providerName, content);
3058
3054
  return appendControlPlaneRecord(state.controlPlaneOutput, {
3059
3055
  content: controlPlaneContent,
@@ -1,4 +1,5 @@
1
1
  "use strict";
2
+ const task_log_line_1 = require("../task-log-line");
2
3
  function isObjectRecord(value) {
3
4
  return value !== null && typeof value === 'object' && !Array.isArray(value);
4
5
  }
@@ -14,10 +15,6 @@ if (!isProvidersParserBoundary(rawProviders)) {
14
15
  throw new TypeError('providers module must expose parseProviderChunk');
15
16
  }
16
17
  const { parseProviderChunk } = rawProviders;
17
- function timestampBody(text) {
18
- const match = /^\[(\d{13})\](.*)$/.exec(text);
19
- return match ? (match[2] || '').trimStart() : null;
20
- }
21
18
  function prefixedJsonBody(text) {
22
19
  if (text.startsWith('{') || text.startsWith('['))
23
20
  return null;
@@ -33,8 +30,10 @@ function stripTimestamp(line) {
33
30
  const normalized = line.trim().replace(/\r$/, '');
34
31
  if (!normalized)
35
32
  return '';
36
- const withoutTimestamp = timestampBody(normalized) ?? normalized;
37
- return prefixedJsonBody(withoutTimestamp) ?? withoutTimestamp;
33
+ const decoded = (0, task_log_line_1.decodeTaskLogLine)(normalized);
34
+ if (!decoded.providerOutput)
35
+ return '';
36
+ return prefixedJsonBody(decoded.content) ?? decoded.content;
38
37
  }
39
38
  function parseJsonRecordLine(line) {
40
39
  const content = stripTimestamp(line);
@@ -1,4 +1,5 @@
1
1
  import type { JsonRecord, ProvidersParserBoundary } from './output-extraction-types';
2
+ import { decodeTaskLogLine } from '../task-log-line';
2
3
 
3
4
  function isObjectRecord(value: unknown): value is JsonRecord {
4
5
  return value !== null && typeof value === 'object' && !Array.isArray(value);
@@ -20,11 +21,6 @@ if (!isProvidersParserBoundary(rawProviders)) {
20
21
  }
21
22
  const { parseProviderChunk } = rawProviders;
22
23
 
23
- function timestampBody(text: string): string | null {
24
- const match = /^\[(\d{13})\](.*)$/.exec(text);
25
- return match ? (match[2] || '').trimStart() : null;
26
- }
27
-
28
24
  function prefixedJsonBody(text: string): string | null {
29
25
  if (text.startsWith('{') || text.startsWith('[')) return null;
30
26
  const separatorIndex = text.indexOf('|');
@@ -38,8 +34,9 @@ function stripTimestamp(line: unknown): string {
38
34
  const normalized = line.trim().replace(/\r$/, '');
39
35
  if (!normalized) return '';
40
36
 
41
- const withoutTimestamp = timestampBody(normalized) ?? normalized;
42
- return prefixedJsonBody(withoutTimestamp) ?? withoutTimestamp;
37
+ const decoded = decodeTaskLogLine(normalized);
38
+ if (!decoded.providerOutput) return '';
39
+ return prefixedJsonBody(decoded.content) ?? decoded.content;
43
40
  }
44
41
 
45
42
  function parseJsonRecordLine(line: string): JsonRecord | null {
@@ -1,13 +1,18 @@
1
+ interface TaskLogDecoder {
2
+ decodeTaskLogLine(line: string): { readonly content: string; readonly providerOutput: boolean };
3
+ }
4
+
5
+ // The canonical decoder is maintained JavaScript and remains at the same source path after build.
6
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
7
+ const taskLogDecoder: TaskLogDecoder = require('../../src/task-log-line');
8
+ const { decodeTaskLogLine } = taskLogDecoder;
9
+
1
10
  export function stripTimestampPrefix(line: string): string {
2
- let trimmed = line.trim().replace(/\r$/, '');
11
+ const decoded = decodeTaskLogLine(line.trim());
12
+ if (!decoded.providerOutput) return '';
13
+ let trimmed = decoded.content;
3
14
  if (!trimmed) return '';
4
15
 
5
- const timestampMatch = /^\[(\d{13})\](.*)$/.exec(trimmed);
6
- const timestampRemainder = timestampMatch?.[2];
7
- if (typeof timestampRemainder === 'string') {
8
- trimmed = timestampRemainder.trimStart();
9
- }
10
-
11
16
  if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) {
12
17
  const pipeMatch = /^[^|]{1,40}\|\s*(.*)$/.exec(trimmed);
13
18
  const afterPipe = pipeMatch?.[1]?.trimStart();
@@ -11,6 +11,7 @@ const TaskRunner = require('./task-runner');
11
11
  const { loadSettings } = require('../lib/settings');
12
12
  const { normalizeProviderName, getDefaultProviderId } = require('../lib/provider-names');
13
13
  const { getProvider } = require('./providers');
14
+ const { decodeTaskLogLine } = require('./task-log-line');
14
15
  const { prependWorktreeToolBinToEnv } = require('./worktree-tooling-env');
15
16
  const { applyDarwinKeychainBoundaryToEnv } = require('./darwin-keychain-boundary');
16
17
  const { getTask, getTaskBySpawnOwnershipToken } = require('../task-lib/store.js');
@@ -556,18 +557,17 @@ class ClaudeTaskRunner extends TaskRunner {
556
557
  const broadcastLine = (line) => {
557
558
  if (!line.trim()) return;
558
559
 
559
- let content = line;
560
- const timestampMatch = line.match(/^\[(\d{13})\](.*)$/);
561
- if (timestampMatch) {
562
- content = timestampMatch[2];
563
- }
560
+ const decoded = decodeTaskLogLine(line);
561
+ if (!decoded.providerOutput) return;
562
+ const content = decoded.content;
564
563
 
565
564
  // Skip non-JSON patterns
566
565
  if (
567
- content.startsWith('===') ||
568
- content.startsWith('Finished:') ||
569
- content.startsWith('Exit code:') ||
570
- (content.includes('"type":"system"') && content.includes('"subtype":"init"'))
566
+ decoded.channel !== 'provider_stdout' &&
567
+ (content.startsWith('===') ||
568
+ content.startsWith('Finished:') ||
569
+ content.startsWith('Exit code:') ||
570
+ (content.includes('"type":"system"') && content.includes('"subtype":"init"')))
571
571
  ) {
572
572
  return;
573
573
  }
@@ -16,9 +16,16 @@ interface ProviderFacade {
16
16
  listProviders(): string[];
17
17
  }
18
18
 
19
+ interface TaskLogDecoder {
20
+ decodeTaskLogLine(line: string): { content: string; providerOutput: boolean };
21
+ }
22
+
19
23
  // The runtime facade is maintained JavaScript; keep its original emitted require path.
20
24
  // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
21
25
  const { getProvider, listProviders }: ProviderFacade = require('../src/providers');
26
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
27
+ const taskLogDecoder: TaskLogDecoder = require('../src/task-log-line');
28
+ const { decodeTaskLogLine } = taskLogDecoder;
22
29
 
23
30
  function createProviderParsers(): ProviderParser[] {
24
31
  return listProviders().map((name) => getProvider(name));
@@ -26,11 +33,9 @@ function createProviderParsers(): ProviderParser[] {
26
33
 
27
34
  function stripTimestampPrefix(line: unknown): string {
28
35
  if (!line || typeof line !== 'string') return '';
29
- let trimmed = line.trim().replace(/\r$/, '');
30
- if (!trimmed) return '';
31
-
32
- const tsMatch = /^\[(\d{13})\](.*)$/.exec(trimmed);
33
- if (tsMatch) trimmed = (tsMatch[2] || '').trimStart();
36
+ const decoded = decodeTaskLogLine(line.trim());
37
+ if (!decoded.providerOutput) return '';
38
+ const trimmed = decoded.content;
34
39
 
35
40
  if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) {
36
41
  const pipeMatch = /^[^|]{1,40}\|\s*(.*)$/.exec(trimmed);
@@ -6,6 +6,7 @@ const {
6
6
  } = require('../../lib/provider-names');
7
7
  const { commandExists, getCommandPath } = require('../../lib/provider-detection');
8
8
  const helper = require('../../lib/agent-cli-provider');
9
+ const { stripTimestampPrefix } = require('../../lib/agent-cli-provider/log-prefix');
9
10
 
10
11
  const warned = new Set();
11
12
 
@@ -218,25 +219,6 @@ function listProviders() {
218
219
  return helper.listProviderAdapters();
219
220
  }
220
221
 
221
- function stripTimestampPrefix(line) {
222
- if (!line || typeof line !== 'string') return '';
223
- let trimmed = line.trim().replace(/\r$/, '');
224
- if (!trimmed) return '';
225
-
226
- const tsMatch = trimmed.match(/^\[(\d{13})\](.*)$/);
227
- if (tsMatch) trimmed = (tsMatch[2] || '').trimStart();
228
-
229
- if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) {
230
- const pipeMatch = trimmed.match(/^[^|]{1,40}\|\s*(.*)$/);
231
- if (pipeMatch) {
232
- const afterPipe = (pipeMatch[1] || '').trimStart();
233
- if (afterPipe.startsWith('{') || afterPipe.startsWith('[')) return afterPipe;
234
- }
235
- }
236
-
237
- return trimmed;
238
- }
239
-
240
222
  function collectEvents(events, event) {
241
223
  if (!event) return;
242
224
  if (Array.isArray(event)) {
@@ -0,0 +1,20 @@
1
+ export const TASK_LOG_V2_MARKER: '[ZEROSHOT][LOG_FORMAT] channel-framed-v2';
2
+ export const TASK_LOG_STDOUT_PREFIX: '[ZEROSHOT][PROVIDER_STDOUT] ';
3
+ export const TASK_LOG_STDERR_PREFIX: '[ZEROSHOT][PROVIDER_STDERR] ';
4
+
5
+ export type TaskLogChannel = 'provider_stdout' | 'provider_stderr' | 'control' | 'legacy';
6
+ export type TaskLogFormat = 'stderr-tagged-v1' | 'channel-framed-v2' | null;
7
+
8
+ export interface DecodedTaskLogLine {
9
+ readonly channel: TaskLogChannel;
10
+ readonly content: string;
11
+ readonly format: TaskLogFormat;
12
+ readonly providerOutput: boolean;
13
+ readonly timestamp: number | null;
14
+ readonly timestamped: boolean;
15
+ }
16
+
17
+ export function decodeTaskLogLine(line: string): DecodedTaskLogLine;
18
+ export function formatTaskLogMarker(timestamp: number): string;
19
+ export function formatTaskLogStderr(timestamp: number, content: string): string;
20
+ export function formatTaskLogStdout(timestamp: number, content: string): string;
@@ -0,0 +1,80 @@
1
+ 'use strict';
2
+
3
+ const TASK_LOG_V2_MARKER = '[ZEROSHOT][LOG_FORMAT] channel-framed-v2';
4
+ const TASK_LOG_STDOUT_PREFIX = '[ZEROSHOT][PROVIDER_STDOUT] ';
5
+ const TASK_LOG_STDERR_PREFIX = '[ZEROSHOT][PROVIDER_STDERR] ';
6
+ const TASK_LOG_V1_MARKER = '[ZEROSHOT][LOG_FORMAT] stderr-tagged-v1';
7
+ const CONTROL_LINE =
8
+ /^\[(?:ATTACH|CLEANUP|CRASH|DETACH|ERROR|OMP-OWNERSHIP|SDK-DIAGNOSTIC|SYSTEM)\](?:\s|$)/;
9
+
10
+ function timestampParts(line) {
11
+ const normalized = line.replace(/\r$/, '');
12
+ const match = /^\[(\d{13}|\d{4}-\d{2}-\d{2}T[^\]]+)\](.*)$/.exec(normalized);
13
+ if (!match) return { content: normalized, timestamp: null };
14
+ const rawTimestamp = match[1];
15
+ const epochTimestamp = /^\d{13}$/.test(rawTimestamp);
16
+ const timestamp = epochTimestamp
17
+ ? Number.parseInt(rawTimestamp, 10)
18
+ : new Date(rawTimestamp).getTime();
19
+ return {
20
+ content: match[2].trimStart(),
21
+ timestamp: Number.isFinite(timestamp) ? timestamp : null,
22
+ };
23
+ }
24
+
25
+ function decodeTaskLogLine(line) {
26
+ const { content, timestamp } = timestampParts(line);
27
+ const common = { timestamp, timestamped: timestamp !== null };
28
+ if (content.startsWith(TASK_LOG_STDOUT_PREFIX)) {
29
+ return {
30
+ ...common,
31
+ channel: 'provider_stdout',
32
+ content: content.slice(TASK_LOG_STDOUT_PREFIX.length),
33
+ format: null,
34
+ providerOutput: true,
35
+ };
36
+ }
37
+ if (content.startsWith(TASK_LOG_STDERR_PREFIX)) {
38
+ return {
39
+ ...common,
40
+ channel: 'provider_stderr',
41
+ content: content.slice(TASK_LOG_STDERR_PREFIX.length),
42
+ format: null,
43
+ providerOutput: false,
44
+ };
45
+ }
46
+ let format = null;
47
+ if (content === TASK_LOG_V2_MARKER) format = 'channel-framed-v2';
48
+ else if (content === TASK_LOG_V1_MARKER) format = 'stderr-tagged-v1';
49
+ const control =
50
+ format !== null || content.startsWith('[ZEROSHOT][FATAL] ') || CONTROL_LINE.test(content);
51
+ return {
52
+ ...common,
53
+ channel: control ? 'control' : 'legacy',
54
+ content,
55
+ format,
56
+ providerOutput: !control,
57
+ };
58
+ }
59
+
60
+ function formatTaskLogMarker(timestamp) {
61
+ return `[${timestamp}]${TASK_LOG_V2_MARKER}\n`;
62
+ }
63
+
64
+ function formatTaskLogStdout(timestamp, content) {
65
+ return `[${timestamp}]${TASK_LOG_STDOUT_PREFIX}${content}\n`;
66
+ }
67
+
68
+ function formatTaskLogStderr(timestamp, content) {
69
+ return `[${timestamp}]${TASK_LOG_STDERR_PREFIX}${content}\n`;
70
+ }
71
+
72
+ module.exports = {
73
+ TASK_LOG_STDERR_PREFIX,
74
+ TASK_LOG_STDOUT_PREFIX,
75
+ TASK_LOG_V2_MARKER,
76
+ decodeTaskLogLine,
77
+ formatTaskLogMarker,
78
+ formatTaskLogStderr,
79
+ formatTaskLogStdout,
80
+ };
@@ -7,6 +7,7 @@ import { createRequire } from 'module';
7
7
  // Import cluster's stream parser (shared between task and cluster)
8
8
  const require = createRequire(import.meta.url);
9
9
  const { parseChunk } = require('../../lib/stream-json-parser');
10
+ const { decodeTaskLogLine } = require('../../src/task-log-line');
10
11
 
11
12
  // Tool icons for different tool types
12
13
  const TOOL_ICONS = {
@@ -271,16 +272,12 @@ function handleMulti(event) {
271
272
  }
272
273
 
273
274
  // Parse a raw log line (may have timestamp prefix)
274
- function parseLogLine(line) {
275
- let trimmed = line.trim();
275
+ export function parseLogLine(line) {
276
+ const decoded = decodeTaskLogLine(line);
277
+ if (!decoded.providerOutput) return [];
278
+ const trimmed = decoded.content.trim();
276
279
  if (!trimmed) return [];
277
280
 
278
- // Strip timestamp prefix if present: [1234567890]{...} -> {...}
279
- const timestampMatch = trimmed.match(/^\[\d+\](.*)$/);
280
- if (timestampMatch) {
281
- trimmed = timestampMatch[1];
282
- }
283
-
284
281
  // Non-JSON lines output as-is
285
282
  if (!trimmed.startsWith('{')) {
286
283
  return [{ type: 'text', text: trimmed + '\n' }];
@@ -0,0 +1,31 @@
1
+ import { createRequire } from 'module';
2
+
3
+ const require = createRequire(import.meta.url);
4
+ const {
5
+ formatTaskLogMarker,
6
+ formatTaskLogStderr,
7
+ formatTaskLogStdout,
8
+ } = require('../src/task-log-line.js');
9
+
10
+ export function markSdkTaskLog(log, timestamp = Date.now()) {
11
+ log(formatTaskLogMarker(timestamp));
12
+ }
13
+
14
+ function normalizedTerminalEvent(terminal) {
15
+ if (terminal.type === 'result') return terminal.event;
16
+ return {
17
+ type: 'result',
18
+ success: false,
19
+ error: { ...terminal.frame.error },
20
+ };
21
+ }
22
+
23
+ export function logSdkTerminal(log, result, now = Date.now) {
24
+ for (const frame of result.progress) {
25
+ log(formatTaskLogStdout(now(), JSON.stringify(frame)));
26
+ }
27
+ log(formatTaskLogStdout(now(), JSON.stringify(normalizedTerminalEvent(result.terminal))));
28
+ for (const line of (result.diagnosticStderr || '').replace(/\r/g, '').split('\n')) {
29
+ if (line) log(formatTaskLogStderr(now(), line));
30
+ }
31
+ }
@@ -7,6 +7,7 @@
7
7
  import { appendFileSync } from 'fs';
8
8
  import { getTask, updateTask } from './store.js';
9
9
  import { createCommandSpecCleanup } from './command-spec-cleanup.js';
10
+ import { logSdkTerminal, markSdkTaskLog } from './sdk-watcher-output.js';
10
11
  import {
11
12
  completePendingWatcherCancellation,
12
13
  completeWatcherFailure,
@@ -72,16 +73,6 @@ function completionFor(result) {
72
73
  };
73
74
  }
74
75
 
75
- function logTerminal(result) {
76
- for (const frame of result.progress) log(`[${Date.now()}]${JSON.stringify(frame)}\n`);
77
- const terminal =
78
- result.terminal.type === 'result' ? result.terminal.event : result.terminal.frame;
79
- log(`[${Date.now()}]${JSON.stringify(terminal)}\n`);
80
- if (result.diagnosticStderr) {
81
- log(`[${Date.now()}][SDK-DIAGNOSTIC] ${result.diagnosticStderr}\n`);
82
- }
83
- }
84
-
85
76
  async function terminateOwnedProviderBoundary() {
86
77
  if (terminalResult) return terminalResult.cleanupAttestation.clean === true;
87
78
  if (!running) return true;
@@ -116,6 +107,8 @@ process.on('unhandledRejection', (reason) => {
116
107
  void crashWithError(reason, 'unhandledRejection');
117
108
  });
118
109
 
110
+ markSdkTaskLog(log);
111
+
119
112
  if (
120
113
  await completePendingWatcherCancellation({
121
114
  taskId,
@@ -139,7 +132,7 @@ try {
139
132
  if (getTask(taskId)?.cancelRequested) running.cancel();
140
133
 
141
134
  terminalResult = await running.result;
142
- logTerminal(terminalResult);
135
+ logSdkTerminal(log, terminalResult);
143
136
  await completeWatcherTask({
144
137
  taskId,
145
138
  completion: completionFor(terminalResult),