agents-bridge 0.1.0 → 0.1.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.
Files changed (95) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +162 -131
  3. package/bin/agent-bridge.mjs +11 -11
  4. package/config.example.json +22 -55
  5. package/config.multi-agent.example.json +67 -0
  6. package/dist/agents/agent-resolver.d.ts +12 -0
  7. package/dist/agents/agent-resolver.d.ts.map +1 -0
  8. package/dist/agents/agent-resolver.js +114 -0
  9. package/dist/agents/agent-resolver.js.map +1 -0
  10. package/dist/agents/agent-router.d.ts +8 -0
  11. package/dist/agents/agent-router.d.ts.map +1 -0
  12. package/dist/agents/agent-router.js +72 -0
  13. package/dist/agents/agent-router.js.map +1 -0
  14. package/dist/agents/cli-backends.d.ts +20 -27
  15. package/dist/agents/cli-backends.d.ts.map +1 -1
  16. package/dist/agents/cli-backends.js +29 -4
  17. package/dist/agents/cli-backends.js.map +1 -1
  18. package/dist/agents/cli-output.d.ts +35 -0
  19. package/dist/agents/cli-output.d.ts.map +1 -0
  20. package/dist/agents/cli-output.js +277 -0
  21. package/dist/agents/cli-output.js.map +1 -0
  22. package/dist/agents/cli-output.test.d.ts +2 -0
  23. package/dist/agents/cli-output.test.d.ts.map +1 -0
  24. package/dist/agents/cli-output.test.js +56 -0
  25. package/dist/agents/cli-output.test.js.map +1 -0
  26. package/dist/agents/cli-runner.d.ts +23 -0
  27. package/dist/agents/cli-runner.d.ts.map +1 -1
  28. package/dist/agents/cli-runner.js +244 -153
  29. package/dist/agents/cli-runner.js.map +1 -1
  30. package/dist/agents/cli-runner.test.d.ts +2 -0
  31. package/dist/agents/cli-runner.test.d.ts.map +1 -0
  32. package/dist/agents/cli-runner.test.js +119 -0
  33. package/dist/agents/cli-runner.test.js.map +1 -0
  34. package/dist/agents/cli-session-store.d.ts +47 -0
  35. package/dist/agents/cli-session-store.d.ts.map +1 -1
  36. package/dist/agents/cli-session-store.js +72 -0
  37. package/dist/agents/cli-session-store.js.map +1 -1
  38. package/dist/agents/cli-session-store.test.d.ts +2 -0
  39. package/dist/agents/cli-session-store.test.d.ts.map +1 -0
  40. package/dist/agents/cli-session-store.test.js +30 -0
  41. package/dist/agents/cli-session-store.test.js.map +1 -0
  42. package/dist/agents/codex-app-server-runtime.d.ts +72 -0
  43. package/dist/agents/codex-app-server-runtime.d.ts.map +1 -0
  44. package/dist/agents/codex-app-server-runtime.js +772 -0
  45. package/dist/agents/codex-app-server-runtime.js.map +1 -0
  46. package/dist/agents/codex-app-server-runtime.test.d.ts +2 -0
  47. package/dist/agents/codex-app-server-runtime.test.d.ts.map +1 -0
  48. package/dist/agents/codex-app-server-runtime.test.js +164 -0
  49. package/dist/agents/codex-app-server-runtime.test.js.map +1 -0
  50. package/dist/agents/model-selection.d.ts +3 -2
  51. package/dist/agents/model-selection.js +1 -1
  52. package/dist/channels/botlink.d.ts +2 -0
  53. package/dist/channels/botlink.js +73 -60
  54. package/dist/channels/botlink.js.map +1 -1
  55. package/dist/cli/cmd-doctor.d.ts.map +1 -1
  56. package/dist/cli/cmd-doctor.js +41 -5
  57. package/dist/cli/cmd-doctor.js.map +1 -1
  58. package/dist/cli/cmd-logs.js +1 -1
  59. package/dist/cli/cmd-logs.js.map +1 -1
  60. package/dist/cli/cmd-onboard.d.ts.map +1 -1
  61. package/dist/cli/cmd-onboard.js +16 -10
  62. package/dist/cli/cmd-onboard.js.map +1 -1
  63. package/dist/cli/cmd-start.d.ts.map +1 -1
  64. package/dist/cli/cmd-start.js +205 -32
  65. package/dist/cli/cmd-start.js.map +1 -1
  66. package/dist/cli/run-main.js +27 -27
  67. package/dist/config/agent-dirs.d.ts +12 -0
  68. package/dist/config/agent-dirs.d.ts.map +1 -1
  69. package/dist/config/agent-dirs.js +43 -2
  70. package/dist/config/agent-dirs.js.map +1 -1
  71. package/dist/config/config.js +1 -1
  72. package/dist/config/config.js.map +1 -1
  73. package/dist/config/merge-config.d.ts.map +1 -1
  74. package/dist/config/merge-config.js +16 -4
  75. package/dist/config/merge-config.js.map +1 -1
  76. package/dist/config/paths.d.ts +4 -0
  77. package/dist/config/paths.d.ts.map +1 -1
  78. package/dist/config/paths.js +7 -0
  79. package/dist/config/paths.js.map +1 -1
  80. package/dist/config/zod-schema.d.ts +2158 -239
  81. package/dist/config/zod-schema.d.ts.map +1 -1
  82. package/dist/config/zod-schema.js +100 -1
  83. package/dist/config/zod-schema.js.map +1 -1
  84. package/dist/entry.d.ts +1 -1
  85. package/dist/logger.d.ts +1 -0
  86. package/dist/logger.d.ts.map +1 -1
  87. package/dist/logger.js +28 -5
  88. package/dist/logger.js.map +1 -1
  89. package/dist/types/index.d.ts +130 -3
  90. package/dist/types/index.d.ts.map +1 -1
  91. package/package.json +55 -54
  92. package/dist/console-encoding.d.ts +0 -6
  93. package/dist/console-encoding.d.ts.map +0 -1
  94. package/dist/console-encoding.js +0 -35
  95. package/dist/console-encoding.js.map +0 -1
@@ -1,16 +1,11 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { spawn } from 'node:child_process';
3
3
  import { CLI_FRESH_WATCHDOG_DEFAULTS, CLI_RESUME_WATCHDOG_DEFAULTS, CLI_WATCHDOG_MIN_TIMEOUT_MS, normalizeProviderId, resolveCliBackendConfig, } from './cli-backends.js';
4
- import { CliSessionStore } from './cli-session-store.js';
4
+ import { createCliOutputProbe, isSessionExpiredError, parseCliOutput, } from './cli-output.js';
5
+ import { CodexAppServerRuntime, } from './codex-app-server-runtime.js';
6
+ import { CliSessionStore, parseAgentSessionKey } from './cli-session-store.js';
5
7
  const CLI_RUN_QUEUES = new Map();
6
8
  const DEFAULT_TIMEOUT_MS = 15 * 60 * 1_000;
7
- const SESSION_EXPIRED_PATTERNS = [
8
- /session expired/i,
9
- /invalid session/i,
10
- /conversation not found/i,
11
- /thread not found/i,
12
- /resume session not found/i,
13
- ];
14
9
  class CliExecutionError extends Error {
15
10
  reason;
16
11
  stdout;
@@ -27,28 +22,41 @@ class CliExecutionError extends Error {
27
22
  }
28
23
  export class LocalCliTurnExecutor {
29
24
  options;
25
+ codexRuntime = null;
30
26
  constructor(options) {
31
27
  this.options = options;
32
28
  }
33
29
  async run(params) {
34
30
  const callbacks = params.callbacks ?? {};
35
31
  const providerId = normalizeProviderId(params.provider);
36
- const backendResolved = resolveCliBackendConfig(providerId, this.options.config);
32
+ const backendResolved = resolveCliBackendConfig(providerId, this.options.config, params.cliBackends);
37
33
  if (!backendResolved) {
38
34
  throw new Error(`Unknown CLI backend: ${params.provider}`);
39
35
  }
40
36
  const sessionStore = this.options.sessionStore ?? new CliSessionStore();
41
37
  const envelope = params.envelope;
38
+ let existingSessionEntry = await sessionStore.get(envelope.sessionId);
39
+ const parsedAgentSession = parseAgentSessionKey(envelope.sessionId);
40
+ if (!existingSessionEntry && parsedAgentSession) {
41
+ existingSessionEntry = await sessionStore.getWithLegacyFallback(envelope.sessionId, parsedAgentSession.originalKey);
42
+ }
42
43
  const sessionEntry = await sessionStore.ensureSession(envelope.sessionId, {
43
44
  provider: providerId,
44
45
  model: params.model,
45
46
  });
46
47
  const existingCliSessionId = await sessionStore.getCliSessionId(envelope.sessionId, providerId);
48
+ if (providerId === 'codex-cli' && !existingCliSessionId && existingSessionEntry?.provider === providerId) {
49
+ this.options.logger.warn({
50
+ provider: providerId,
51
+ sessionId: envelope.sessionId,
52
+ runtimeSessionId: sessionEntry.runtimeSessionId,
53
+ }, 'No persisted Codex thread id found for existing botlink session; next turn will start fresh');
54
+ }
47
55
  await this.emitProcessing(callbacks);
48
56
  const queueKey = backendResolved.config.serialize === false
49
57
  ? `${backendResolved.id}:${randomUUID()}`
50
58
  : `${backendResolved.id}:${envelope.sessionId}`;
51
- return enqueueCliRun(queueKey, async () => {
59
+ return enqueueCliRun(queueKey, async ({ queueWaitMs }) => {
52
60
  try {
53
61
  const result = await this.executeTurn({
54
62
  envelope,
@@ -59,13 +67,25 @@ export class LocalCliTurnExecutor {
59
67
  systemPrompt: params.systemPrompt,
60
68
  backend: backendResolved.config,
61
69
  cliSessionId: existingCliSessionId,
70
+ queueWaitMs,
62
71
  runtimeSessionId: sessionEntry.runtimeSessionId,
72
+ agentEnv: params.agentEnv,
73
+ agentClearEnv: params.agentClearEnv,
63
74
  });
64
75
  if (result.cliSessionId) {
65
76
  await sessionStore.setCliSessionId(envelope.sessionId, providerId, result.cliSessionId, {
66
77
  model: params.model,
67
78
  });
68
79
  }
80
+ else if (providerId === 'codex-cli') {
81
+ this.options.logger.warn({
82
+ provider: providerId,
83
+ sessionId: envelope.sessionId,
84
+ runtimeSessionId: sessionEntry.runtimeSessionId,
85
+ isResume: result.isResume,
86
+ diagnostics: result.diagnostics,
87
+ }, 'Codex turn completed without a persisted thread id; future turns will remain fresh');
88
+ }
69
89
  else {
70
90
  await sessionStore.ensureSession(envelope.sessionId, {
71
91
  provider: providerId,
@@ -77,8 +97,8 @@ export class LocalCliTurnExecutor {
77
97
  catch (error) {
78
98
  if (existingCliSessionId &&
79
99
  isSessionExpiredError(error) &&
80
- backendResolved.config.resumeArgs &&
81
- backendResolved.config.resumeArgs.length > 0) {
100
+ (providerId === 'codex-cli' ||
101
+ Boolean(backendResolved.config.resumeArgs && backendResolved.config.resumeArgs.length > 0))) {
82
102
  this.options.logger.warn({ provider: providerId, sessionId: envelope.sessionId }, 'CLI session expired, clearing provider mapping and retrying fresh turn');
83
103
  await sessionStore.clearCliSessionId(envelope.sessionId, providerId, { model: params.model });
84
104
  const retried = await this.executeTurn({
@@ -90,7 +110,10 @@ export class LocalCliTurnExecutor {
90
110
  systemPrompt: params.systemPrompt,
91
111
  backend: backendResolved.config,
92
112
  cliSessionId: undefined,
113
+ queueWaitMs,
93
114
  runtimeSessionId: sessionEntry.runtimeSessionId,
115
+ agentEnv: params.agentEnv,
116
+ agentClearEnv: params.agentClearEnv,
94
117
  });
95
118
  if (retried.cliSessionId) {
96
119
  await sessionStore.setCliSessionId(envelope.sessionId, providerId, retried.cliSessionId, {
@@ -107,6 +130,65 @@ export class LocalCliTurnExecutor {
107
130
  await callbacks.onProcessing?.();
108
131
  }
109
132
  async executeTurn(params) {
133
+ if (params.providerId === 'codex-cli' && params.backend.runtimeMode === 'app-server') {
134
+ return this.executeCodexAppServerTurn(params);
135
+ }
136
+ return this.executeCliExecTurn(params);
137
+ }
138
+ async teardown() {
139
+ await this.codexRuntime?.teardown();
140
+ }
141
+ getCodexRuntime(backend) {
142
+ if (!this.codexRuntime) {
143
+ this.codexRuntime = this.options.createCodexRuntime?.(backend) ?? new CodexAppServerRuntime({
144
+ backend,
145
+ logger: this.options.logger,
146
+ });
147
+ }
148
+ return this.codexRuntime;
149
+ }
150
+ async executeCodexAppServerTurn(params) {
151
+ const normalizedModel = normalizeCliModel(params.model, params.backend);
152
+ const existingCliSessionId = params.cliSessionId?.trim() || undefined;
153
+ const isResume = Boolean(existingCliSessionId);
154
+ const promptPayload = buildPromptPayload(params.envelope, params.backend, true);
155
+ this.options.logger.info({
156
+ provider: params.providerId,
157
+ model: normalizedModel,
158
+ sessionId: params.envelope.sessionId,
159
+ cliSessionId: existingCliSessionId,
160
+ runtimeSessionId: params.runtimeSessionId,
161
+ isResume,
162
+ runtimeMode: 'app-server',
163
+ }, 'Starting Codex app-server turn');
164
+ const runtime = this.getCodexRuntime(params.backend);
165
+ const result = await runtime.run({
166
+ sessionId: params.envelope.sessionId,
167
+ runtimeSessionId: params.runtimeSessionId,
168
+ existingThreadId: existingCliSessionId,
169
+ model: normalizedModel,
170
+ workspaceDir: params.workspaceDir,
171
+ timeoutMs: params.timeoutMs,
172
+ noProgressTimeoutMs: resolveCliNoOutputTimeoutMs(params.backend, params.timeoutMs, isResume),
173
+ systemPrompt: resolveAppServerSystemPrompt(params.backend, params.systemPrompt, !existingCliSessionId),
174
+ prompt: promptPayload.prompt,
175
+ imagePaths: promptPayload.imagePaths,
176
+ });
177
+ return {
178
+ text: result.text,
179
+ provider: params.providerId,
180
+ model: params.model,
181
+ cliSessionId: result.cliSessionId,
182
+ isResume: result.isResume,
183
+ usage: result.usage,
184
+ diagnostics: {
185
+ ...result.diagnostics,
186
+ queueWaitMs: params.queueWaitMs,
187
+ totalElapsedMs: result.diagnostics.totalElapsedMs + params.queueWaitMs,
188
+ },
189
+ };
190
+ }
191
+ async executeCliExecTurn(params) {
110
192
  const normalizedModel = normalizeCliModel(params.model, params.backend);
111
193
  const existingCliSessionId = params.cliSessionId?.trim() || undefined;
112
194
  const useResume = Boolean(existingCliSessionId &&
@@ -116,7 +198,7 @@ export class LocalCliTurnExecutor {
116
198
  backend: params.backend,
117
199
  cliSessionId: existingCliSessionId,
118
200
  });
119
- const promptPayload = buildPromptPayload(params.envelope, params.backend);
201
+ const promptPayload = buildPromptPayload(params.envelope, params.backend, Boolean(params.backend.imageArg));
120
202
  const promptInput = resolvePromptInput({
121
203
  backend: params.backend,
122
204
  prompt: promptPayload.prompt,
@@ -148,10 +230,18 @@ export class LocalCliTurnExecutor {
148
230
  runtimeSessionId: params.runtimeSessionId,
149
231
  isResume: useResume,
150
232
  }, 'Starting CLI turn');
151
- const env = { ...process.env, ...params.backend.env };
233
+ // Build env per design: process.env → backend.env → agent.env → clear
234
+ const env = { ...process.env, ...params.backend.env, ...(params.agentEnv ?? {}) };
152
235
  for (const key of params.backend.clearEnv ?? []) {
153
236
  delete env[key];
154
237
  }
238
+ for (const key of params.agentClearEnv ?? []) {
239
+ delete env[key];
240
+ }
241
+ const probe = createCliOutputProbe({
242
+ backend: params.backend,
243
+ mode: outputMode,
244
+ });
155
245
  const output = await executeCliCommand({
156
246
  command: params.backend.command,
157
247
  args,
@@ -160,22 +250,56 @@ export class LocalCliTurnExecutor {
160
250
  stdin: promptInput.stdin,
161
251
  timeoutMs: params.timeoutMs,
162
252
  noOutputTimeoutMs: resolveCliNoOutputTimeoutMs(params.backend, params.timeoutMs, useResume),
253
+ onStdoutChunk(chunk, atMs) {
254
+ probe.onStdout(chunk, atMs);
255
+ },
256
+ onStderrChunk(chunk, atMs) {
257
+ probe.onStderr(chunk, atMs);
258
+ },
163
259
  });
260
+ const probeSnapshot = probe.snapshot();
164
261
  const parsed = parseCliOutput(output.stdout.trim(), outputMode, params.backend) ?? {
165
- text: output.stdout.trim(),
262
+ text: probeSnapshot.lastText ?? output.stdout.trim(),
263
+ cliSessionId: probeSnapshot.cliSessionId,
264
+ usage: probeSnapshot.usage,
166
265
  };
167
- const cliSessionId = parsed.cliSessionId ?? existingCliSessionId ?? sessionIdToSend.sessionId;
266
+ const cliSessionId = parsed.cliSessionId ?? probeSnapshot.cliSessionId ?? existingCliSessionId ?? sessionIdToSend.sessionId;
267
+ const diagnostics = buildRunDiagnostics({
268
+ queueWaitMs: params.queueWaitMs,
269
+ commandElapsedMs: output.elapsedMs,
270
+ outputMode,
271
+ stdout: output.stdout,
272
+ stderr: output.stderr,
273
+ cliSessionId,
274
+ existingCliSessionId,
275
+ runtimeSessionId: sessionIdToSend.sessionId,
276
+ probe: probeSnapshot,
277
+ });
278
+ if (params.providerId === 'codex-cli' && !cliSessionId) {
279
+ this.options.logger.warn({
280
+ provider: params.providerId,
281
+ sessionId: params.envelope.sessionId,
282
+ runtimeSessionId: params.runtimeSessionId,
283
+ isResume: useResume,
284
+ diagnostics,
285
+ stdoutPreview: trimOutputPreview(output.stdout),
286
+ stderrPreview: trimOutputPreview(output.stderr),
287
+ }, 'Unable to extract Codex thread id from CLI output');
288
+ }
289
+ const text = parsed.text.trim() || probeSnapshot.lastText?.trim() || output.stdout.trim();
168
290
  return {
169
- text: parsed.text.trim(),
291
+ text,
170
292
  provider: params.providerId,
171
293
  model: params.model,
172
294
  cliSessionId,
173
295
  isResume: useResume,
174
296
  usage: parsed.usage,
297
+ diagnostics,
175
298
  };
176
299
  }
177
300
  }
178
301
  async function enqueueCliRun(key, task) {
302
+ const queuedAt = Date.now();
179
303
  const previous = CLI_RUN_QUEUES.get(key) ?? Promise.resolve();
180
304
  let release;
181
305
  const current = new Promise((resolve) => {
@@ -185,7 +309,9 @@ async function enqueueCliRun(key, task) {
185
309
  CLI_RUN_QUEUES.set(key, tail);
186
310
  await previous.catch(() => undefined);
187
311
  try {
188
- return await task();
312
+ return await task({
313
+ queueWaitMs: Date.now() - queuedAt,
314
+ });
189
315
  }
190
316
  finally {
191
317
  release();
@@ -216,6 +342,20 @@ function resolveSystemPrompt(backend, systemPrompt, isNewSession, useResume) {
216
342
  }
217
343
  return trimmed;
218
344
  }
345
+ function resolveAppServerSystemPrompt(backend, systemPrompt, isNewThread) {
346
+ const trimmed = systemPrompt?.trim();
347
+ if (!trimmed) {
348
+ return undefined;
349
+ }
350
+ const when = backend.systemPromptWhen ?? 'first';
351
+ if (when === 'never') {
352
+ return undefined;
353
+ }
354
+ if (when === 'first' && !isNewThread) {
355
+ return undefined;
356
+ }
357
+ return trimmed;
358
+ }
219
359
  function resolveSessionIdToSend(params) {
220
360
  const mode = params.backend.sessionMode ?? 'always';
221
361
  const existing = params.cliSessionId?.trim();
@@ -240,7 +380,7 @@ function resolvePromptInput(params) {
240
380
  }
241
381
  return { promptArg: params.prompt };
242
382
  }
243
- function buildPromptPayload(envelope, backend) {
383
+ function buildPromptPayload(envelope, backend, useSeparateImageInputs = Boolean(backend.imageArg)) {
244
384
  const imagePaths = envelope.content.attachments
245
385
  .filter((attachment) => attachment.type === 'photo' && attachment.localPath)
246
386
  .map((attachment) => attachment.localPath);
@@ -249,14 +389,14 @@ function buildPromptPayload(envelope, backend) {
249
389
  return `Attachment: ${attachment.localPath}${name}`;
250
390
  });
251
391
  const promptParts = [envelope.content.text.trim()];
252
- if (!backend.imageArg && imagePaths.length > 0) {
392
+ if (!useSeparateImageInputs && imagePaths.length > 0) {
253
393
  promptParts.push(...imagePaths.map((imagePath) => `Image: ${imagePath}`));
254
394
  }
255
395
  const nonImageFiles = fileLines.filter((line) => !imagePaths.some((imagePath) => line.includes(imagePath)));
256
396
  promptParts.push(...nonImageFiles);
257
397
  return {
258
398
  prompt: promptParts.filter(Boolean).join('\n\n').trim(),
259
- imagePaths: backend.imageArg && imagePaths.length > 0 ? imagePaths : undefined,
399
+ imagePaths: useSeparateImageInputs && imagePaths.length > 0 ? imagePaths : undefined,
260
400
  };
261
401
  }
262
402
  function buildCliArgs(params) {
@@ -309,10 +449,11 @@ function resolveCliNoOutputTimeoutMs(backend, timeoutMs, useResume) {
309
449
  }
310
450
  async function executeCliCommand(params) {
311
451
  return new Promise((resolve, reject) => {
312
- let stdout = '';
313
- let stderr = '';
452
+ const stdoutChunks = [];
453
+ const stderrChunks = [];
314
454
  let settled = false;
315
455
  let timedOutReason = null;
456
+ const startedAt = Date.now();
316
457
  const child = spawn(params.command, params.args, {
317
458
  cwd: params.cwd,
318
459
  env: params.env,
@@ -330,7 +471,11 @@ async function executeCliCommand(params) {
330
471
  reject(error);
331
472
  return;
332
473
  }
333
- resolve(result ?? { stdout, stderr });
474
+ resolve(result ?? {
475
+ stdout: decodeCliOutput(Buffer.concat(stdoutChunks)),
476
+ stderr: decodeCliOutput(Buffer.concat(stderrChunks)),
477
+ elapsedMs: Date.now() - startedAt,
478
+ });
334
479
  };
335
480
  const armNoOutputTimer = () => {
336
481
  clearTimeout(noOutputTimer);
@@ -346,21 +491,26 @@ async function executeCliCommand(params) {
346
491
  let noOutputTimer = setTimeout(() => undefined, params.noOutputTimeoutMs);
347
492
  armNoOutputTimer();
348
493
  child.on('error', (error) => {
349
- finish(new CliExecutionError(error.message, 'spawn-error', stdout, stderr));
494
+ finish(new CliExecutionError(error.message, 'spawn-error', decodeCliOutput(Buffer.concat(stdoutChunks)), decodeCliOutput(Buffer.concat(stderrChunks))));
350
495
  });
351
- child.stdout?.setEncoding('utf8');
352
- child.stderr?.setEncoding('utf8');
353
496
  child.stdout?.on('data', (chunk) => {
354
- stdout += chunk;
497
+ const buffer = typeof chunk === 'string' ? Buffer.from(chunk) : chunk;
498
+ stdoutChunks.push(buffer);
499
+ params.onStdoutChunk?.(decodeCliOutput(buffer), Date.now() - startedAt);
355
500
  armNoOutputTimer();
356
501
  });
357
502
  child.stderr?.on('data', (chunk) => {
358
- stderr += chunk;
503
+ const buffer = typeof chunk === 'string' ? Buffer.from(chunk) : chunk;
504
+ stderrChunks.push(buffer);
505
+ params.onStderrChunk?.(decodeCliOutput(buffer), Date.now() - startedAt);
359
506
  armNoOutputTimer();
360
507
  });
361
508
  child.on('close', (code) => {
509
+ const stdout = decodeCliOutput(Buffer.concat(stdoutChunks));
510
+ const stderr = decodeCliOutput(Buffer.concat(stderrChunks));
362
511
  const trimmedStdout = stdout.trim();
363
512
  const trimmedStderr = stderr.trim();
513
+ const elapsedMs = Date.now() - startedAt;
364
514
  if (timedOutReason) {
365
515
  const timeoutMessage = timedOutReason === 'overall-timeout'
366
516
  ? `CLI exceeded timeout (${Math.round(params.timeoutMs / 1000)}s) and was terminated.`
@@ -372,7 +522,7 @@ async function executeCliCommand(params) {
372
522
  finish(new CliExecutionError(trimmedStderr || trimmedStdout || `CLI failed with exit code ${code ?? 'unknown'}.`, 'exit', trimmedStdout, trimmedStderr, code));
373
523
  return;
374
524
  }
375
- finish(undefined, { stdout: trimmedStdout, stderr: trimmedStderr });
525
+ finish(undefined, { stdout: trimmedStdout, stderr: trimmedStderr, elapsedMs });
376
526
  });
377
527
  if (params.stdin !== undefined && child.stdin) {
378
528
  child.stdin.write(params.stdin);
@@ -380,145 +530,86 @@ async function executeCliCommand(params) {
380
530
  child.stdin?.end();
381
531
  });
382
532
  }
383
- function parseCliOutput(raw, mode, backend) {
384
- if (!raw.trim()) {
385
- return null;
533
+ function decodeCliOutput(buffer) {
534
+ if (buffer.length === 0) {
535
+ return '';
536
+ }
537
+ const utf8 = buffer.toString('utf8');
538
+ if (!shouldTryWindowsLegacyDecode(utf8)) {
539
+ return utf8;
386
540
  }
387
- if (mode === 'text') {
388
- return { text: raw.trim() };
541
+ for (const encoding of ['gb18030', 'gbk']) {
542
+ const decoded = tryDecodeWithEncoding(buffer, encoding);
543
+ if (decoded && decoded !== utf8) {
544
+ return decoded;
545
+ }
546
+ }
547
+ return utf8;
548
+ }
549
+ function shouldTryWindowsLegacyDecode(decoded) {
550
+ if (process.platform !== 'win32') {
551
+ return false;
389
552
  }
390
- if (mode === 'jsonl') {
391
- return parseCliJsonl(raw, backend);
553
+ const replacementCount = (decoded.match(/\uFFFD/g) ?? []).length;
554
+ if (replacementCount > 0) {
555
+ return true;
392
556
  }
393
- return parseCliJson(raw, backend);
557
+ // Common mojibake markers when UTF-8/Chinese text is decoded with the wrong code page.
558
+ return ['鈥', '銆', '锛', '鏈', '鍦', '浠', '闂'].some((marker) => decoded.includes(marker));
394
559
  }
395
- function parseCliJson(raw, backend) {
560
+ function tryDecodeWithEncoding(buffer, encoding) {
396
561
  try {
397
- const parsed = JSON.parse(raw);
398
- if (!isRecord(parsed)) {
399
- return null;
400
- }
401
- return {
402
- text: (collectText(parsed.message) ||
403
- collectText(parsed.content) ||
404
- collectText(parsed.result) ||
405
- collectText(parsed)).trim(),
406
- cliSessionId: pickCliSessionId(parsed, backend),
407
- usage: isRecord(parsed.usage) ? toUsage(parsed.usage) : undefined,
408
- };
562
+ // TextDecoder coverage depends on the Node/ICU build; guard runtime support.
563
+ return new TextDecoder(encoding, { fatal: false }).decode(buffer);
409
564
  }
410
565
  catch {
411
566
  return null;
412
567
  }
413
568
  }
414
- function parseCliJsonl(raw, backend) {
415
- const lines = raw
416
- .split(/\r?\n/g)
417
- .map((line) => line.trim())
418
- .filter(Boolean);
419
- if (lines.length === 0) {
420
- return null;
421
- }
422
- let cliSessionId;
423
- let usage;
424
- let lastText = '';
425
- for (const line of lines) {
426
- try {
427
- const parsed = JSON.parse(line);
428
- if (!isRecord(parsed)) {
429
- continue;
430
- }
431
- cliSessionId = cliSessionId ?? pickCliSessionId(parsed, backend) ?? getString(parsed.thread_id);
432
- if (isRecord(parsed.usage)) {
433
- usage = toUsage(parsed.usage) ?? usage;
434
- }
435
- const candidate = (collectText(parsed.item) ||
436
- collectText(parsed.message) ||
437
- collectText(parsed.delta) ||
438
- collectText(parsed.result)).trim();
439
- if (candidate) {
440
- lastText = candidate;
441
- }
569
+ function buildRunDiagnostics(params) {
570
+ let cliSessionIdSource;
571
+ if (params.cliSessionId) {
572
+ if (params.probe.cliSessionId && params.cliSessionId === params.probe.cliSessionId) {
573
+ cliSessionIdSource = 'stream';
442
574
  }
443
- catch {
444
- continue;
575
+ else if (params.existingCliSessionId && params.cliSessionId === params.existingCliSessionId) {
576
+ cliSessionIdSource = 'existing';
445
577
  }
446
- }
447
- if (!lastText) {
448
- return null;
449
- }
450
- return { text: lastText, cliSessionId, usage };
451
- }
452
- function pickCliSessionId(parsed, backend) {
453
- const fields = backend.sessionIdFields ?? ['session_id', 'sessionId', 'conversation_id', 'conversationId'];
454
- for (const field of fields) {
455
- const value = getString(parsed[field]);
456
- if (value) {
457
- return value;
578
+ else if (params.runtimeSessionId && params.cliSessionId === params.runtimeSessionId) {
579
+ cliSessionIdSource = 'runtime';
458
580
  }
459
- }
460
- return undefined;
461
- }
462
- function collectText(value) {
463
- if (!value) {
464
- return '';
465
- }
466
- if (typeof value === 'string') {
467
- return value;
468
- }
469
- if (Array.isArray(value)) {
470
- return value.map((entry) => collectText(entry)).filter(Boolean).join('\n');
471
- }
472
- if (!isRecord(value)) {
473
- return '';
474
- }
475
- if (typeof value.text === 'string') {
476
- return value.text;
477
- }
478
- if (typeof value.content === 'string') {
479
- return value.content;
480
- }
481
- if (Array.isArray(value.content)) {
482
- return value.content.map((entry) => collectText(entry)).filter(Boolean).join('\n');
483
- }
484
- if (isRecord(value.message)) {
485
- return collectText(value.message);
486
- }
487
- if (isRecord(value.item)) {
488
- return collectText(value.item);
489
- }
490
- return '';
491
- }
492
- function toUsage(raw) {
493
- const pick = (...keys) => {
494
- for (const key of keys) {
495
- const value = raw[key];
496
- if (typeof value === 'number' && value >= 0) {
497
- return value;
498
- }
581
+ else {
582
+ cliSessionIdSource = 'parsed';
499
583
  }
500
- return undefined;
501
- };
502
- const usage = {
503
- input: pick('input_tokens', 'inputTokens'),
504
- output: pick('output_tokens', 'outputTokens'),
505
- cacheRead: pick('cache_read_input_tokens', 'cached_input_tokens', 'cacheRead'),
506
- cacheWrite: pick('cache_write_input_tokens', 'cacheWrite'),
507
- total: pick('total_tokens', 'total'),
584
+ }
585
+ return {
586
+ queueWaitMs: params.queueWaitMs,
587
+ commandElapsedMs: params.commandElapsedMs,
588
+ totalElapsedMs: params.queueWaitMs + params.commandElapsedMs,
589
+ outputMode: params.outputMode,
590
+ runtimeMode: 'exec',
591
+ hadStdout: Boolean(params.stdout),
592
+ hadStderr: Boolean(params.stderr),
593
+ firstStdoutMs: params.probe.firstStdoutMs,
594
+ firstStderrMs: params.probe.firstStderrMs,
595
+ threadStartedMs: params.probe.threadStartedMs,
596
+ firstAgentMessageMs: params.probe.firstAgentMessageMs,
597
+ turnCompletedMs: params.probe.turnCompletedMs,
598
+ reconnectCount: params.probe.reconnectCount,
599
+ fellBackToHttps: params.probe.fellBackToHttps,
600
+ threadOperation: params.existingCliSessionId ? 'resume' : 'start',
601
+ threadOperationElapsedMs: params.probe.threadStartedMs,
602
+ cliSessionIdSource,
508
603
  };
509
- if (Object.values(usage).every((value) => value === undefined)) {
604
+ }
605
+ function trimOutputPreview(raw, limit = 1200) {
606
+ const normalized = raw.trim();
607
+ if (!normalized) {
510
608
  return undefined;
511
609
  }
512
- return usage;
513
- }
514
- function isSessionExpiredError(error) {
515
- const message = error instanceof Error ? error.message : String(error);
516
- return SESSION_EXPIRED_PATTERNS.some((pattern) => pattern.test(message));
517
- }
518
- function isRecord(value) {
519
- return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
520
- }
521
- function getString(value) {
522
- return typeof value === 'string' && value.trim() ? value.trim() : undefined;
610
+ if (normalized.length <= limit) {
611
+ return normalized;
612
+ }
613
+ return `${normalized.slice(0, limit)}...`;
523
614
  }
524
615
  //# sourceMappingURL=cli-runner.js.map