agents-bridge 0.1.1 → 0.1.3

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 (92) hide show
  1. package/README.md +12 -12
  2. package/config.example.json +116 -51
  3. package/dist/agents/agent-resolver.d.ts +12 -0
  4. package/dist/agents/agent-resolver.js +1 -0
  5. package/dist/agents/agent-resolver.js.map +1 -1
  6. package/dist/agents/agent-router.d.ts +8 -0
  7. package/dist/agents/cli-output.d.ts +35 -0
  8. package/dist/agents/cli-output.d.ts.map +1 -0
  9. package/dist/agents/cli-output.js +277 -0
  10. package/dist/agents/cli-output.js.map +1 -0
  11. package/dist/agents/cli-output.test.d.ts +2 -0
  12. package/dist/agents/cli-output.test.d.ts.map +1 -0
  13. package/dist/agents/cli-output.test.js +56 -0
  14. package/dist/agents/cli-output.test.js.map +1 -0
  15. package/dist/agents/cli-runner.d.ts +23 -0
  16. package/dist/agents/cli-runner.d.ts.map +1 -1
  17. package/dist/agents/cli-runner.js +196 -157
  18. package/dist/agents/cli-runner.js.map +1 -1
  19. package/dist/agents/cli-runner.test.d.ts +2 -0
  20. package/dist/agents/cli-runner.test.d.ts.map +1 -0
  21. package/dist/agents/cli-runner.test.js +119 -0
  22. package/dist/agents/cli-runner.test.js.map +1 -0
  23. package/dist/agents/cli-session-store.d.ts +47 -0
  24. package/dist/agents/cli-session-store.d.ts.map +1 -1
  25. package/dist/agents/cli-session-store.js +29 -0
  26. package/dist/agents/cli-session-store.js.map +1 -1
  27. package/dist/agents/cli-session-store.test.d.ts +2 -0
  28. package/dist/agents/cli-session-store.test.d.ts.map +1 -0
  29. package/dist/agents/cli-session-store.test.js +30 -0
  30. package/dist/agents/cli-session-store.test.js.map +1 -0
  31. package/dist/agents/codex-app-server-runtime.d.ts +72 -0
  32. package/dist/agents/codex-app-server-runtime.d.ts.map +1 -0
  33. package/dist/agents/codex-app-server-runtime.js +772 -0
  34. package/dist/agents/codex-app-server-runtime.js.map +1 -0
  35. package/dist/agents/codex-app-server-runtime.test.d.ts +2 -0
  36. package/dist/agents/codex-app-server-runtime.test.d.ts.map +1 -0
  37. package/dist/agents/codex-app-server-runtime.test.js +164 -0
  38. package/dist/agents/codex-app-server-runtime.test.js.map +1 -0
  39. package/dist/agents/model-selection.d.ts +3 -2
  40. package/dist/agents/model-selection.js +1 -1
  41. package/dist/channels/botlink.d.ts +2 -0
  42. package/dist/cli/cmd-onboard.d.ts.map +1 -1
  43. package/dist/cli/cmd-onboard.js +9 -78
  44. package/dist/cli/cmd-onboard.js.map +1 -1
  45. package/dist/cli/cmd-start.d.ts.map +1 -1
  46. package/dist/cli/cmd-start.js +154 -16
  47. package/dist/cli/cmd-start.js.map +1 -1
  48. package/dist/cli/cmd-version.d.ts +4 -0
  49. package/dist/cli/cmd-version.d.ts.map +1 -0
  50. package/dist/cli/cmd-version.js +12 -0
  51. package/dist/cli/cmd-version.js.map +1 -0
  52. package/dist/cli/run-main.d.ts +2 -1
  53. package/dist/cli/run-main.d.ts.map +1 -1
  54. package/dist/cli/run-main.js +42 -21
  55. package/dist/cli/run-main.js.map +1 -1
  56. package/dist/cli/run-main.test.d.ts +2 -0
  57. package/dist/cli/run-main.test.d.ts.map +1 -0
  58. package/dist/cli/run-main.test.js +17 -0
  59. package/dist/cli/run-main.test.js.map +1 -0
  60. package/dist/config/config.js +1 -1
  61. package/dist/config/config.js.map +1 -1
  62. package/dist/config/default-config.d.ts +9 -0
  63. package/dist/config/default-config.d.ts.map +1 -0
  64. package/dist/config/default-config.js +83 -0
  65. package/dist/config/default-config.js.map +1 -0
  66. package/dist/config/default-config.test.d.ts +2 -0
  67. package/dist/config/default-config.test.d.ts.map +1 -0
  68. package/dist/config/default-config.test.js +19 -0
  69. package/dist/config/default-config.test.js.map +1 -0
  70. package/dist/config/paths.d.ts +4 -0
  71. package/dist/config/paths.d.ts.map +1 -1
  72. package/dist/config/paths.js +7 -0
  73. package/dist/config/paths.js.map +1 -1
  74. package/dist/config/zod-schema.d.ts +656 -51
  75. package/dist/config/zod-schema.d.ts.map +1 -1
  76. package/dist/config/zod-schema.js +16 -0
  77. package/dist/config/zod-schema.js.map +1 -1
  78. package/dist/entry.d.ts +1 -1
  79. package/dist/entry.d.ts.map +1 -1
  80. package/dist/entry.js +12 -1
  81. package/dist/entry.js.map +1 -1
  82. package/dist/logger.d.ts +1 -0
  83. package/dist/logger.d.ts.map +1 -1
  84. package/dist/logger.js +28 -5
  85. package/dist/logger.js.map +1 -1
  86. package/dist/types/index.d.ts +39 -0
  87. package/dist/types/index.d.ts.map +1 -1
  88. package/package.json +2 -2
  89. package/dist/console-encoding.d.ts +0 -6
  90. package/dist/console-encoding.d.ts.map +0 -1
  91. package/dist/console-encoding.js +0 -35
  92. package/dist/console-encoding.js.map +0 -1
@@ -0,0 +1,56 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import { createCliOutputProbe, parseCliJsonl, } from './cli-output.js';
4
+ const CODEX_BACKEND = {
5
+ command: 'codex',
6
+ output: 'jsonl',
7
+ sessionIdFields: ['thread_id'],
8
+ };
9
+ test('parseCliJsonl extracts codex thread id and final agent text', () => {
10
+ const raw = [
11
+ '{"type":"thread.started","thread_id":"thread-123"}',
12
+ '{"type":"item.completed","item":{"id":"item_1","type":"agent_message","text":"OK"}}',
13
+ '{"type":"turn.completed","usage":{"input_tokens":10,"output_tokens":2}}',
14
+ ].join('\n');
15
+ const parsed = parseCliJsonl(raw, CODEX_BACKEND);
16
+ assert.ok(parsed);
17
+ assert.equal(parsed.cliSessionId, 'thread-123');
18
+ assert.equal(parsed.text, 'OK');
19
+ assert.deepEqual(parsed.usage, {
20
+ input: 10,
21
+ output: 2,
22
+ cacheRead: undefined,
23
+ cacheWrite: undefined,
24
+ total: undefined,
25
+ });
26
+ });
27
+ test('parseCliJsonl keeps codex thread id even when no final text is present', () => {
28
+ const raw = [
29
+ '{"type":"thread.started","thread_id":"thread-456"}',
30
+ '{"type":"turn.completed","usage":{"input_tokens":10,"output_tokens":0}}',
31
+ ].join('\n');
32
+ const parsed = parseCliJsonl(raw, CODEX_BACKEND);
33
+ assert.ok(parsed);
34
+ assert.equal(parsed.cliSessionId, 'thread-456');
35
+ assert.equal(parsed.text, '');
36
+ });
37
+ test('createCliOutputProbe tracks reconnects, fallback, and first message timing', () => {
38
+ const probe = createCliOutputProbe({
39
+ backend: CODEX_BACKEND,
40
+ mode: 'jsonl',
41
+ });
42
+ probe.onStdout('{"type":"thread.started","thread_id":"thread-789"}\n', 20);
43
+ probe.onStdout('{"type":"error","message":"Reconnecting... 2/5 (timeout waiting for child process to exit)"}\n', 40);
44
+ probe.onStdout('{"type":"item.completed","item":{"id":"item_0","type":"error","message":"Falling back from WebSockets to HTTPS transport. timeout waiting for child process to exit"}}\n', 60);
45
+ probe.onStdout('{"type":"item.completed","item":{"id":"item_1","type":"agent_message","text":"READY"}}\n', 80);
46
+ probe.onStdout('{"type":"turn.completed","usage":{"input_tokens":1,"output_tokens":1}}\n', 100);
47
+ const snapshot = probe.snapshot();
48
+ assert.equal(snapshot.cliSessionId, 'thread-789');
49
+ assert.equal(snapshot.threadStartedMs, 20);
50
+ assert.equal(snapshot.firstAgentMessageMs, 80);
51
+ assert.equal(snapshot.turnCompletedMs, 100);
52
+ assert.equal(snapshot.reconnectCount, 1);
53
+ assert.equal(snapshot.fellBackToHttps, true);
54
+ assert.equal(snapshot.lastText, 'READY');
55
+ });
56
+ //# sourceMappingURL=cli-output.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli-output.test.js","sourceRoot":"","sources":["../../src/agents/cli-output.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ import type { AppConfig } from '../config/zod-schema.js';
2
+ import type { CliBackendConfig, CliRunParams, CliRunResult, CliTurnExecutor, Logger } from '../types/index.js';
3
+ import { type CodexTurnRuntime } from './codex-app-server-runtime.js';
4
+ import { CliSessionStore } from './cli-session-store.js';
5
+ export declare class LocalCliTurnExecutor implements CliTurnExecutor {
6
+ private readonly options;
7
+ private codexRuntime;
8
+ constructor(options: {
9
+ config: Pick<AppConfig, 'agents'>;
10
+ logger: Logger;
11
+ sessionStore?: CliSessionStore;
12
+ defaultTimeoutMs?: number;
13
+ createCodexRuntime?: (backend: CliBackendConfig) => CodexTurnRuntime;
14
+ });
15
+ run(params: CliRunParams): Promise<CliRunResult>;
16
+ private emitProcessing;
17
+ private executeTurn;
18
+ teardown(): Promise<void>;
19
+ private getCodexRuntime;
20
+ private executeCodexAppServerTurn;
21
+ private executeCliExecTurn;
22
+ }
23
+ //# sourceMappingURL=cli-runner.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"cli-runner.d.ts","sourceRoot":"","sources":["../../src/agents/cli-runner.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAA;AACxD,OAAO,KAAK,EAGV,YAAY,EACZ,YAAY,EAEZ,eAAe,EACf,MAAM,EAEP,MAAM,mBAAmB,CAAA;AAQ1B,OAAO,EAAE,eAAe,EAAwB,MAAM,wBAAwB,CAAA;AA+B9E,qBAAa,oBAAqB,YAAW,eAAe;IAExD,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE;QACxB,MAAM,EAAE,IAAI,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAA;QACjC,MAAM,EAAE,MAAM,CAAA;QACd,YAAY,CAAC,EAAE,eAAe,CAAA;QAC9B,gBAAgB,CAAC,EAAE,MAAM,CAAA;KAC1B;IAGG,GAAG,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;YA8FxC,cAAc;YAId,WAAW;CA8F1B"}
1
+ {"version":3,"file":"cli-runner.d.ts","sourceRoot":"","sources":["../../src/agents/cli-runner.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAA;AACxD,OAAO,KAAK,EACV,gBAAgB,EAGhB,YAAY,EACZ,YAAY,EACZ,eAAe,EACf,MAAM,EAEP,MAAM,mBAAmB,CAAA;AAc1B,OAAO,EAEL,KAAK,gBAAgB,EACtB,MAAM,+BAA+B,CAAA;AACtC,OAAO,EAAE,eAAe,EAAwB,MAAM,wBAAwB,CAAA;AAkB9E,qBAAa,oBAAqB,YAAW,eAAe;IAIxD,OAAO,CAAC,QAAQ,CAAC,OAAO;IAH1B,OAAO,CAAC,YAAY,CAAgC;gBAGjC,OAAO,EAAE;QACxB,MAAM,EAAE,IAAI,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAA;QACjC,MAAM,EAAE,MAAM,CAAA;QACd,YAAY,CAAC,EAAE,eAAe,CAAA;QAC9B,gBAAgB,CAAC,EAAE,MAAM,CAAA;QACzB,kBAAkB,CAAC,EAAE,CAAC,OAAO,EAAE,gBAAgB,KAAK,gBAAgB,CAAA;KACrE;IAGG,GAAG,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;YAyHxC,cAAc;YAId,WAAW;IAqBnB,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAI/B,OAAO,CAAC,eAAe;YAUT,yBAAyB;YA+DzB,kBAAkB;CAwIjC"}
@@ -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 { createCliOutputProbe, isSessionExpiredError, parseCliOutput, } from './cli-output.js';
5
+ import { CodexAppServerRuntime, } from './codex-app-server-runtime.js';
4
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,6 +22,7 @@ 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
  }
@@ -39,21 +35,28 @@ export class LocalCliTurnExecutor {
39
35
  }
40
36
  const sessionStore = this.options.sessionStore ?? new CliSessionStore();
41
37
  const envelope = params.envelope;
42
- // Legacy fallback: if this is an agent-prefixed key, check for data under the old (unprefixed) key
43
- const parsed = parseAgentSessionKey(envelope.sessionId);
44
- if (parsed) {
45
- await sessionStore.getWithLegacyFallback(envelope.sessionId, parsed.originalKey);
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);
46
42
  }
47
43
  const sessionEntry = await sessionStore.ensureSession(envelope.sessionId, {
48
44
  provider: providerId,
49
45
  model: params.model,
50
46
  });
51
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
+ }
52
55
  await this.emitProcessing(callbacks);
53
56
  const queueKey = backendResolved.config.serialize === false
54
57
  ? `${backendResolved.id}:${randomUUID()}`
55
58
  : `${backendResolved.id}:${envelope.sessionId}`;
56
- return enqueueCliRun(queueKey, async () => {
59
+ return enqueueCliRun(queueKey, async ({ queueWaitMs }) => {
57
60
  try {
58
61
  const result = await this.executeTurn({
59
62
  envelope,
@@ -64,6 +67,7 @@ export class LocalCliTurnExecutor {
64
67
  systemPrompt: params.systemPrompt,
65
68
  backend: backendResolved.config,
66
69
  cliSessionId: existingCliSessionId,
70
+ queueWaitMs,
67
71
  runtimeSessionId: sessionEntry.runtimeSessionId,
68
72
  agentEnv: params.agentEnv,
69
73
  agentClearEnv: params.agentClearEnv,
@@ -73,6 +77,15 @@ export class LocalCliTurnExecutor {
73
77
  model: params.model,
74
78
  });
75
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
+ }
76
89
  else {
77
90
  await sessionStore.ensureSession(envelope.sessionId, {
78
91
  provider: providerId,
@@ -84,8 +97,8 @@ export class LocalCliTurnExecutor {
84
97
  catch (error) {
85
98
  if (existingCliSessionId &&
86
99
  isSessionExpiredError(error) &&
87
- backendResolved.config.resumeArgs &&
88
- backendResolved.config.resumeArgs.length > 0) {
100
+ (providerId === 'codex-cli' ||
101
+ Boolean(backendResolved.config.resumeArgs && backendResolved.config.resumeArgs.length > 0))) {
89
102
  this.options.logger.warn({ provider: providerId, sessionId: envelope.sessionId }, 'CLI session expired, clearing provider mapping and retrying fresh turn');
90
103
  await sessionStore.clearCliSessionId(envelope.sessionId, providerId, { model: params.model });
91
104
  const retried = await this.executeTurn({
@@ -97,6 +110,7 @@ export class LocalCliTurnExecutor {
97
110
  systemPrompt: params.systemPrompt,
98
111
  backend: backendResolved.config,
99
112
  cliSessionId: undefined,
113
+ queueWaitMs,
100
114
  runtimeSessionId: sessionEntry.runtimeSessionId,
101
115
  agentEnv: params.agentEnv,
102
116
  agentClearEnv: params.agentClearEnv,
@@ -116,6 +130,65 @@ export class LocalCliTurnExecutor {
116
130
  await callbacks.onProcessing?.();
117
131
  }
118
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) {
119
192
  const normalizedModel = normalizeCliModel(params.model, params.backend);
120
193
  const existingCliSessionId = params.cliSessionId?.trim() || undefined;
121
194
  const useResume = Boolean(existingCliSessionId &&
@@ -125,7 +198,7 @@ export class LocalCliTurnExecutor {
125
198
  backend: params.backend,
126
199
  cliSessionId: existingCliSessionId,
127
200
  });
128
- const promptPayload = buildPromptPayload(params.envelope, params.backend);
201
+ const promptPayload = buildPromptPayload(params.envelope, params.backend, Boolean(params.backend.imageArg));
129
202
  const promptInput = resolvePromptInput({
130
203
  backend: params.backend,
131
204
  prompt: promptPayload.prompt,
@@ -165,6 +238,10 @@ export class LocalCliTurnExecutor {
165
238
  for (const key of params.agentClearEnv ?? []) {
166
239
  delete env[key];
167
240
  }
241
+ const probe = createCliOutputProbe({
242
+ backend: params.backend,
243
+ mode: outputMode,
244
+ });
168
245
  const output = await executeCliCommand({
169
246
  command: params.backend.command,
170
247
  args,
@@ -173,22 +250,56 @@ export class LocalCliTurnExecutor {
173
250
  stdin: promptInput.stdin,
174
251
  timeoutMs: params.timeoutMs,
175
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
+ },
176
259
  });
260
+ const probeSnapshot = probe.snapshot();
177
261
  const parsed = parseCliOutput(output.stdout.trim(), outputMode, params.backend) ?? {
178
- text: output.stdout.trim(),
262
+ text: probeSnapshot.lastText ?? output.stdout.trim(),
263
+ cliSessionId: probeSnapshot.cliSessionId,
264
+ usage: probeSnapshot.usage,
179
265
  };
180
- 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();
181
290
  return {
182
- text: parsed.text.trim(),
291
+ text,
183
292
  provider: params.providerId,
184
293
  model: params.model,
185
294
  cliSessionId,
186
295
  isResume: useResume,
187
296
  usage: parsed.usage,
297
+ diagnostics,
188
298
  };
189
299
  }
190
300
  }
191
301
  async function enqueueCliRun(key, task) {
302
+ const queuedAt = Date.now();
192
303
  const previous = CLI_RUN_QUEUES.get(key) ?? Promise.resolve();
193
304
  let release;
194
305
  const current = new Promise((resolve) => {
@@ -198,7 +309,9 @@ async function enqueueCliRun(key, task) {
198
309
  CLI_RUN_QUEUES.set(key, tail);
199
310
  await previous.catch(() => undefined);
200
311
  try {
201
- return await task();
312
+ return await task({
313
+ queueWaitMs: Date.now() - queuedAt,
314
+ });
202
315
  }
203
316
  finally {
204
317
  release();
@@ -229,6 +342,20 @@ function resolveSystemPrompt(backend, systemPrompt, isNewSession, useResume) {
229
342
  }
230
343
  return trimmed;
231
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
+ }
232
359
  function resolveSessionIdToSend(params) {
233
360
  const mode = params.backend.sessionMode ?? 'always';
234
361
  const existing = params.cliSessionId?.trim();
@@ -253,7 +380,7 @@ function resolvePromptInput(params) {
253
380
  }
254
381
  return { promptArg: params.prompt };
255
382
  }
256
- function buildPromptPayload(envelope, backend) {
383
+ function buildPromptPayload(envelope, backend, useSeparateImageInputs = Boolean(backend.imageArg)) {
257
384
  const imagePaths = envelope.content.attachments
258
385
  .filter((attachment) => attachment.type === 'photo' && attachment.localPath)
259
386
  .map((attachment) => attachment.localPath);
@@ -262,14 +389,14 @@ function buildPromptPayload(envelope, backend) {
262
389
  return `Attachment: ${attachment.localPath}${name}`;
263
390
  });
264
391
  const promptParts = [envelope.content.text.trim()];
265
- if (!backend.imageArg && imagePaths.length > 0) {
392
+ if (!useSeparateImageInputs && imagePaths.length > 0) {
266
393
  promptParts.push(...imagePaths.map((imagePath) => `Image: ${imagePath}`));
267
394
  }
268
395
  const nonImageFiles = fileLines.filter((line) => !imagePaths.some((imagePath) => line.includes(imagePath)));
269
396
  promptParts.push(...nonImageFiles);
270
397
  return {
271
398
  prompt: promptParts.filter(Boolean).join('\n\n').trim(),
272
- imagePaths: backend.imageArg && imagePaths.length > 0 ? imagePaths : undefined,
399
+ imagePaths: useSeparateImageInputs && imagePaths.length > 0 ? imagePaths : undefined,
273
400
  };
274
401
  }
275
402
  function buildCliArgs(params) {
@@ -326,6 +453,7 @@ async function executeCliCommand(params) {
326
453
  const stderrChunks = [];
327
454
  let settled = false;
328
455
  let timedOutReason = null;
456
+ const startedAt = Date.now();
329
457
  const child = spawn(params.command, params.args, {
330
458
  cwd: params.cwd,
331
459
  env: params.env,
@@ -346,6 +474,7 @@ async function executeCliCommand(params) {
346
474
  resolve(result ?? {
347
475
  stdout: decodeCliOutput(Buffer.concat(stdoutChunks)),
348
476
  stderr: decodeCliOutput(Buffer.concat(stderrChunks)),
477
+ elapsedMs: Date.now() - startedAt,
349
478
  });
350
479
  };
351
480
  const armNoOutputTimer = () => {
@@ -365,11 +494,15 @@ async function executeCliCommand(params) {
365
494
  finish(new CliExecutionError(error.message, 'spawn-error', decodeCliOutput(Buffer.concat(stdoutChunks)), decodeCliOutput(Buffer.concat(stderrChunks))));
366
495
  });
367
496
  child.stdout?.on('data', (chunk) => {
368
- stdoutChunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);
497
+ const buffer = typeof chunk === 'string' ? Buffer.from(chunk) : chunk;
498
+ stdoutChunks.push(buffer);
499
+ params.onStdoutChunk?.(decodeCliOutput(buffer), Date.now() - startedAt);
369
500
  armNoOutputTimer();
370
501
  });
371
502
  child.stderr?.on('data', (chunk) => {
372
- stderrChunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);
503
+ const buffer = typeof chunk === 'string' ? Buffer.from(chunk) : chunk;
504
+ stderrChunks.push(buffer);
505
+ params.onStderrChunk?.(decodeCliOutput(buffer), Date.now() - startedAt);
373
506
  armNoOutputTimer();
374
507
  });
375
508
  child.on('close', (code) => {
@@ -377,6 +510,7 @@ async function executeCliCommand(params) {
377
510
  const stderr = decodeCliOutput(Buffer.concat(stderrChunks));
378
511
  const trimmedStdout = stdout.trim();
379
512
  const trimmedStderr = stderr.trim();
513
+ const elapsedMs = Date.now() - startedAt;
380
514
  if (timedOutReason) {
381
515
  const timeoutMessage = timedOutReason === 'overall-timeout'
382
516
  ? `CLI exceeded timeout (${Math.round(params.timeoutMs / 1000)}s) and was terminated.`
@@ -388,7 +522,7 @@ async function executeCliCommand(params) {
388
522
  finish(new CliExecutionError(trimmedStderr || trimmedStdout || `CLI failed with exit code ${code ?? 'unknown'}.`, 'exit', trimmedStdout, trimmedStderr, code));
389
523
  return;
390
524
  }
391
- finish(undefined, { stdout: trimmedStdout, stderr: trimmedStderr });
525
+ finish(undefined, { stdout: trimmedStdout, stderr: trimmedStderr, elapsedMs });
392
526
  });
393
527
  if (params.stdin !== undefined && child.stdin) {
394
528
  child.stdin.write(params.stdin);
@@ -432,145 +566,50 @@ function tryDecodeWithEncoding(buffer, encoding) {
432
566
  return null;
433
567
  }
434
568
  }
435
- function parseCliOutput(raw, mode, backend) {
436
- if (!raw.trim()) {
437
- return null;
438
- }
439
- if (mode === 'text') {
440
- return { text: raw.trim() };
441
- }
442
- if (mode === 'jsonl') {
443
- return parseCliJsonl(raw, backend);
444
- }
445
- return parseCliJson(raw, backend);
446
- }
447
- function parseCliJson(raw, backend) {
448
- try {
449
- const parsed = JSON.parse(raw);
450
- if (!isRecord(parsed)) {
451
- return null;
569
+ function buildRunDiagnostics(params) {
570
+ let cliSessionIdSource;
571
+ if (params.cliSessionId) {
572
+ if (params.probe.cliSessionId && params.cliSessionId === params.probe.cliSessionId) {
573
+ cliSessionIdSource = 'stream';
452
574
  }
453
- return {
454
- text: (collectText(parsed.message) ||
455
- collectText(parsed.content) ||
456
- collectText(parsed.result) ||
457
- collectText(parsed)).trim(),
458
- cliSessionId: pickCliSessionId(parsed, backend),
459
- usage: isRecord(parsed.usage) ? toUsage(parsed.usage) : undefined,
460
- };
461
- }
462
- catch {
463
- return null;
464
- }
465
- }
466
- function parseCliJsonl(raw, backend) {
467
- const lines = raw
468
- .split(/\r?\n/g)
469
- .map((line) => line.trim())
470
- .filter(Boolean);
471
- if (lines.length === 0) {
472
- return null;
473
- }
474
- let cliSessionId;
475
- let usage;
476
- let lastText = '';
477
- for (const line of lines) {
478
- try {
479
- const parsed = JSON.parse(line);
480
- if (!isRecord(parsed)) {
481
- continue;
482
- }
483
- cliSessionId = cliSessionId ?? pickCliSessionId(parsed, backend) ?? getString(parsed.thread_id);
484
- if (isRecord(parsed.usage)) {
485
- usage = toUsage(parsed.usage) ?? usage;
486
- }
487
- const candidate = (collectText(parsed.item) ||
488
- collectText(parsed.message) ||
489
- collectText(parsed.delta) ||
490
- collectText(parsed.result)).trim();
491
- if (candidate) {
492
- lastText = candidate;
493
- }
575
+ else if (params.existingCliSessionId && params.cliSessionId === params.existingCliSessionId) {
576
+ cliSessionIdSource = 'existing';
494
577
  }
495
- catch {
496
- continue;
578
+ else if (params.runtimeSessionId && params.cliSessionId === params.runtimeSessionId) {
579
+ cliSessionIdSource = 'runtime';
497
580
  }
498
- }
499
- if (!lastText) {
500
- return null;
501
- }
502
- return { text: lastText, cliSessionId, usage };
503
- }
504
- function pickCliSessionId(parsed, backend) {
505
- const fields = backend.sessionIdFields ?? ['session_id', 'sessionId', 'conversation_id', 'conversationId'];
506
- for (const field of fields) {
507
- const value = getString(parsed[field]);
508
- if (value) {
509
- return value;
581
+ else {
582
+ cliSessionIdSource = 'parsed';
510
583
  }
511
584
  }
512
- return undefined;
513
- }
514
- function collectText(value) {
515
- if (!value) {
516
- return '';
517
- }
518
- if (typeof value === 'string') {
519
- return value;
520
- }
521
- if (Array.isArray(value)) {
522
- return value.map((entry) => collectText(entry)).filter(Boolean).join('\n');
523
- }
524
- if (!isRecord(value)) {
525
- return '';
526
- }
527
- if (typeof value.text === 'string') {
528
- return value.text;
529
- }
530
- if (typeof value.content === 'string') {
531
- return value.content;
532
- }
533
- if (Array.isArray(value.content)) {
534
- return value.content.map((entry) => collectText(entry)).filter(Boolean).join('\n');
535
- }
536
- if (isRecord(value.message)) {
537
- return collectText(value.message);
538
- }
539
- if (isRecord(value.item)) {
540
- return collectText(value.item);
541
- }
542
- return '';
543
- }
544
- function toUsage(raw) {
545
- const pick = (...keys) => {
546
- for (const key of keys) {
547
- const value = raw[key];
548
- if (typeof value === 'number' && value >= 0) {
549
- return value;
550
- }
551
- }
552
- return undefined;
553
- };
554
- const usage = {
555
- input: pick('input_tokens', 'inputTokens'),
556
- output: pick('output_tokens', 'outputTokens'),
557
- cacheRead: pick('cache_read_input_tokens', 'cached_input_tokens', 'cacheRead'),
558
- cacheWrite: pick('cache_write_input_tokens', 'cacheWrite'),
559
- total: pick('total_tokens', 'total'),
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,
560
603
  };
561
- if (Object.values(usage).every((value) => value === undefined)) {
604
+ }
605
+ function trimOutputPreview(raw, limit = 1200) {
606
+ const normalized = raw.trim();
607
+ if (!normalized) {
562
608
  return undefined;
563
609
  }
564
- return usage;
565
- }
566
- function isSessionExpiredError(error) {
567
- const message = error instanceof Error ? error.message : String(error);
568
- return SESSION_EXPIRED_PATTERNS.some((pattern) => pattern.test(message));
569
- }
570
- function isRecord(value) {
571
- return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
572
- }
573
- function getString(value) {
574
- return typeof value === 'string' && value.trim() ? value.trim() : undefined;
610
+ if (normalized.length <= limit) {
611
+ return normalized;
612
+ }
613
+ return `${normalized.slice(0, limit)}...`;
575
614
  }
576
615
  //# sourceMappingURL=cli-runner.js.map