agent-life-bridge 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +20 -0
- package/LICENSE +21 -0
- package/README.md +350 -0
- package/bin/agent-life-bridge.mjs +13 -0
- package/config.example.json +130 -0
- package/config.multi-agent.example.json +88 -0
- package/dist/agents/agent-life-session-map-store.d.ts +21 -0
- package/dist/agents/agent-life-session-map-store.d.ts.map +1 -0
- package/dist/agents/agent-life-session-map-store.js +78 -0
- package/dist/agents/agent-life-session-map-store.js.map +1 -0
- package/dist/agents/agent-resolver.d.ts +12 -0
- package/dist/agents/agent-resolver.d.ts.map +1 -0
- package/dist/agents/agent-resolver.js +114 -0
- package/dist/agents/agent-resolver.js.map +1 -0
- package/dist/agents/agent-router.d.ts +8 -0
- package/dist/agents/agent-router.d.ts.map +1 -0
- package/dist/agents/agent-router.js +72 -0
- package/dist/agents/agent-router.js.map +1 -0
- package/dist/agents/cli-backends.d.ts +25 -0
- package/dist/agents/cli-backends.d.ts.map +1 -0
- package/dist/agents/cli-backends.js +252 -0
- package/dist/agents/cli-backends.js.map +1 -0
- package/dist/agents/cli-output.d.ts +43 -0
- package/dist/agents/cli-output.d.ts.map +1 -0
- package/dist/agents/cli-output.js +352 -0
- package/dist/agents/cli-output.js.map +1 -0
- package/dist/agents/cli-runner.d.ts +32 -0
- package/dist/agents/cli-runner.d.ts.map +1 -0
- package/dist/agents/cli-runner.js +861 -0
- package/dist/agents/cli-runner.js.map +1 -0
- package/dist/agents/cli-session-store.d.ts +53 -0
- package/dist/agents/cli-session-store.d.ts.map +1 -0
- package/dist/agents/cli-session-store.js +263 -0
- package/dist/agents/cli-session-store.js.map +1 -0
- package/dist/agents/codex-app-server-runtime.d.ts +80 -0
- package/dist/agents/codex-app-server-runtime.d.ts.map +1 -0
- package/dist/agents/codex-app-server-runtime.js +1049 -0
- package/dist/agents/codex-app-server-runtime.js.map +1 -0
- package/dist/agents/fch-runtime-context.d.ts +28 -0
- package/dist/agents/fch-runtime-context.d.ts.map +1 -0
- package/dist/agents/fch-runtime-context.js +65 -0
- package/dist/agents/fch-runtime-context.js.map +1 -0
- package/dist/agents/model-selection.d.ts +28 -0
- package/dist/agents/model-selection.d.ts.map +1 -0
- package/dist/agents/model-selection.js +40 -0
- package/dist/agents/model-selection.js.map +1 -0
- package/dist/agents/models-config.d.ts +28 -0
- package/dist/agents/models-config.d.ts.map +1 -0
- package/dist/agents/models-config.js +43 -0
- package/dist/agents/models-config.js.map +1 -0
- package/dist/channels/agent-life.d.ts +9 -0
- package/dist/channels/agent-life.d.ts.map +1 -0
- package/dist/channels/agent-life.js +407 -0
- package/dist/channels/agent-life.js.map +1 -0
- package/dist/channels/command-gating.d.ts +20 -0
- package/dist/channels/command-gating.d.ts.map +1 -0
- package/dist/channels/command-gating.js +43 -0
- package/dist/channels/command-gating.js.map +1 -0
- package/dist/channels/draft-stream-controls.d.ts +21 -0
- package/dist/channels/draft-stream-controls.d.ts.map +1 -0
- package/dist/channels/draft-stream-controls.js +43 -0
- package/dist/channels/draft-stream-controls.js.map +1 -0
- package/dist/channels/draft-stream-loop.d.ts +31 -0
- package/dist/channels/draft-stream-loop.d.ts.map +1 -0
- package/dist/channels/draft-stream-loop.js +60 -0
- package/dist/channels/draft-stream-loop.js.map +1 -0
- package/dist/channels/mention-gating.d.ts +18 -0
- package/dist/channels/mention-gating.d.ts.map +1 -0
- package/dist/channels/mention-gating.js +20 -0
- package/dist/channels/mention-gating.js.map +1 -0
- package/dist/channels/registry.d.ts +5 -0
- package/dist/channels/registry.d.ts.map +1 -0
- package/dist/channels/registry.js +14 -0
- package/dist/channels/registry.js.map +1 -0
- package/dist/channels/run-state-machine.d.ts +21 -0
- package/dist/channels/run-state-machine.d.ts.map +1 -0
- package/dist/channels/run-state-machine.js +41 -0
- package/dist/channels/run-state-machine.js.map +1 -0
- package/dist/channels/session-envelope.d.ts +5 -0
- package/dist/channels/session-envelope.d.ts.map +1 -0
- package/dist/channels/session-envelope.js +16 -0
- package/dist/channels/session-envelope.js.map +1 -0
- package/dist/channels/session-id.d.ts +3 -0
- package/dist/channels/session-id.d.ts.map +1 -0
- package/dist/channels/session-id.js +11 -0
- package/dist/channels/session-id.js.map +1 -0
- package/dist/channels/session.d.ts +18 -0
- package/dist/channels/session.d.ts.map +1 -0
- package/dist/channels/session.js +35 -0
- package/dist/channels/session.js.map +1 -0
- package/dist/channels/typing-lifecycle.d.ts +16 -0
- package/dist/channels/typing-lifecycle.d.ts.map +1 -0
- package/dist/channels/typing-lifecycle.js +31 -0
- package/dist/channels/typing-lifecycle.js.map +1 -0
- package/dist/cli/cmd-add-agent.d.ts +19 -0
- package/dist/cli/cmd-add-agent.d.ts.map +1 -0
- package/dist/cli/cmd-add-agent.js +362 -0
- package/dist/cli/cmd-add-agent.js.map +1 -0
- package/dist/cli/cmd-config.d.ts +3 -0
- package/dist/cli/cmd-config.d.ts.map +1 -0
- package/dist/cli/cmd-config.js +16 -0
- package/dist/cli/cmd-config.js.map +1 -0
- package/dist/cli/cmd-doctor.d.ts +3 -0
- package/dist/cli/cmd-doctor.d.ts.map +1 -0
- package/dist/cli/cmd-doctor.js +127 -0
- package/dist/cli/cmd-doctor.js.map +1 -0
- package/dist/cli/cmd-logs.d.ts +3 -0
- package/dist/cli/cmd-logs.d.ts.map +1 -0
- package/dist/cli/cmd-logs.js +26 -0
- package/dist/cli/cmd-logs.js.map +1 -0
- package/dist/cli/cmd-onboard.d.ts +5 -0
- package/dist/cli/cmd-onboard.d.ts.map +1 -0
- package/dist/cli/cmd-onboard.js +53 -0
- package/dist/cli/cmd-onboard.js.map +1 -0
- package/dist/cli/cmd-restart.d.ts +3 -0
- package/dist/cli/cmd-restart.d.ts.map +1 -0
- package/dist/cli/cmd-restart.js +22 -0
- package/dist/cli/cmd-restart.js.map +1 -0
- package/dist/cli/cmd-start.d.ts +19 -0
- package/dist/cli/cmd-start.d.ts.map +1 -0
- package/dist/cli/cmd-start.js +783 -0
- package/dist/cli/cmd-start.js.map +1 -0
- package/dist/cli/cmd-status.d.ts +3 -0
- package/dist/cli/cmd-status.d.ts.map +1 -0
- package/dist/cli/cmd-status.js +16 -0
- package/dist/cli/cmd-status.js.map +1 -0
- package/dist/cli/cmd-stop.d.ts +9 -0
- package/dist/cli/cmd-stop.d.ts.map +1 -0
- package/dist/cli/cmd-stop.js +59 -0
- package/dist/cli/cmd-stop.js.map +1 -0
- package/dist/cli/cmd-update.d.ts +3 -0
- package/dist/cli/cmd-update.d.ts.map +1 -0
- package/dist/cli/cmd-update.js +127 -0
- package/dist/cli/cmd-update.js.map +1 -0
- package/dist/cli/cmd-version.d.ts +4 -0
- package/dist/cli/cmd-version.d.ts.map +1 -0
- package/dist/cli/cmd-version.js +12 -0
- package/dist/cli/cmd-version.js.map +1 -0
- package/dist/cli/pid-file.d.ts +23 -0
- package/dist/cli/pid-file.d.ts.map +1 -0
- package/dist/cli/pid-file.js +136 -0
- package/dist/cli/pid-file.js.map +1 -0
- package/dist/cli/run-main.d.ts +16 -0
- package/dist/cli/run-main.d.ts.map +1 -0
- package/dist/cli/run-main.js +114 -0
- package/dist/cli/run-main.js.map +1 -0
- package/dist/cli/update-check.d.ts +15 -0
- package/dist/cli/update-check.d.ts.map +1 -0
- package/dist/cli/update-check.js +187 -0
- package/dist/cli/update-check.js.map +1 -0
- package/dist/commands/prefix-router.d.ts +17 -0
- package/dist/commands/prefix-router.d.ts.map +1 -0
- package/dist/commands/prefix-router.js +79 -0
- package/dist/commands/prefix-router.js.map +1 -0
- package/dist/config/agent-dirs.d.ts +24 -0
- package/dist/config/agent-dirs.d.ts.map +1 -0
- package/dist/config/agent-dirs.js +71 -0
- package/dist/config/agent-dirs.js.map +1 -0
- package/dist/config/config.d.ts +4 -0
- package/dist/config/config.d.ts.map +1 -0
- package/dist/config/config.js +42 -0
- package/dist/config/config.js.map +1 -0
- package/dist/config/default-config.d.ts +9 -0
- package/dist/config/default-config.d.ts.map +1 -0
- package/dist/config/default-config.js +94 -0
- package/dist/config/default-config.js.map +1 -0
- package/dist/config/env-vars.d.ts +15 -0
- package/dist/config/env-vars.d.ts.map +1 -0
- package/dist/config/env-vars.js +16 -0
- package/dist/config/env-vars.js.map +1 -0
- package/dist/config/merge-config.d.ts +7 -0
- package/dist/config/merge-config.d.ts.map +1 -0
- package/dist/config/merge-config.js +45 -0
- package/dist/config/merge-config.js.map +1 -0
- package/dist/config/paths.d.ts +25 -0
- package/dist/config/paths.d.ts.map +1 -0
- package/dist/config/paths.js +31 -0
- package/dist/config/paths.js.map +1 -0
- package/dist/config/zod-schema.d.ts +3114 -0
- package/dist/config/zod-schema.d.ts.map +1 -0
- package/dist/config/zod-schema.js +217 -0
- package/dist/config/zod-schema.js.map +1 -0
- package/dist/entry.d.ts +3 -0
- package/dist/entry.d.ts.map +1 -0
- package/dist/entry.js +70 -0
- package/dist/entry.js.map +1 -0
- package/dist/logger.d.ts +5 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +42 -0
- package/dist/logger.js.map +1 -0
- package/dist/types/index.d.ts +215 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/package.json +62 -0
|
@@ -0,0 +1,861 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { CLI_FRESH_WATCHDOG_DEFAULTS, CLI_RESUME_WATCHDOG_DEFAULTS, CLI_WATCHDOG_MIN_TIMEOUT_MS, normalizeProviderId, resolveCliBackendConfig, } from './cli-backends.js';
|
|
5
|
+
import { createCliOutputProbe, isSessionExpiredError, parseCliOutput, parseCliStreamChunk, } from './cli-output.js';
|
|
6
|
+
import { CodexAppServerRuntime, } from './codex-app-server-runtime.js';
|
|
7
|
+
import { CliSessionStore, parseAgentSessionKey } from './cli-session-store.js';
|
|
8
|
+
import { buildCodexFchConfigArgs, buildFchEnv, makeFchContextKey, } from './fch-runtime-context.js';
|
|
9
|
+
const CLI_RUN_QUEUES = new Map();
|
|
10
|
+
const DEFAULT_TIMEOUT_MS = 15 * 60 * 1_000;
|
|
11
|
+
class CliExecutionError extends Error {
|
|
12
|
+
reason;
|
|
13
|
+
stdout;
|
|
14
|
+
stderr;
|
|
15
|
+
exitCode;
|
|
16
|
+
constructor(message, reason, stdout, stderr, exitCode) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.reason = reason;
|
|
19
|
+
this.stdout = stdout;
|
|
20
|
+
this.stderr = stderr;
|
|
21
|
+
this.exitCode = exitCode;
|
|
22
|
+
this.name = 'CliExecutionError';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export class LocalCliTurnExecutor {
|
|
26
|
+
options;
|
|
27
|
+
activeCodexRuntime = null;
|
|
28
|
+
drainingCodexRuntimes = new Set();
|
|
29
|
+
constructor(options) {
|
|
30
|
+
this.options = options;
|
|
31
|
+
}
|
|
32
|
+
async run(params) {
|
|
33
|
+
const callbacks = params.callbacks ?? {};
|
|
34
|
+
const providerId = normalizeProviderId(params.provider);
|
|
35
|
+
const backendResolved = resolveCliBackendConfig(providerId, this.options.config, params.cliBackends);
|
|
36
|
+
if (!backendResolved) {
|
|
37
|
+
throw new Error(`Unknown CLI backend: ${params.provider}`);
|
|
38
|
+
}
|
|
39
|
+
const sessionStore = this.options.sessionStore ?? new CliSessionStore();
|
|
40
|
+
const envelope = params.envelope;
|
|
41
|
+
let existingSessionEntry = await sessionStore.get(envelope.sessionId);
|
|
42
|
+
const parsedAgentSession = parseAgentSessionKey(envelope.sessionId);
|
|
43
|
+
if (!existingSessionEntry && parsedAgentSession) {
|
|
44
|
+
existingSessionEntry = await sessionStore.getWithLegacyFallback(envelope.sessionId, parsedAgentSession.originalKey);
|
|
45
|
+
}
|
|
46
|
+
const sessionEntry = await sessionStore.ensureSession(envelope.sessionId, {
|
|
47
|
+
provider: providerId,
|
|
48
|
+
model: params.model,
|
|
49
|
+
});
|
|
50
|
+
const existingCliSessionId = await sessionStore.getCliSessionId(envelope.sessionId, providerId);
|
|
51
|
+
if (providerId === 'codex-cli' && !existingCliSessionId && existingSessionEntry?.provider === providerId) {
|
|
52
|
+
this.options.logger.warn({
|
|
53
|
+
provider: providerId,
|
|
54
|
+
sessionId: envelope.sessionId,
|
|
55
|
+
runtimeSessionId: sessionEntry.runtimeSessionId,
|
|
56
|
+
}, 'No persisted Codex thread id found for existing agent-life session; next turn will start fresh');
|
|
57
|
+
}
|
|
58
|
+
await this.emitProcessing(callbacks);
|
|
59
|
+
const queueKey = backendResolved.config.serialize === false
|
|
60
|
+
? `${backendResolved.id}:${randomUUID()}`
|
|
61
|
+
: `${backendResolved.id}:${envelope.sessionId}`;
|
|
62
|
+
return enqueueCliRun(queueKey, async ({ queueWaitMs }) => {
|
|
63
|
+
try {
|
|
64
|
+
const result = await this.executeTurn({
|
|
65
|
+
envelope,
|
|
66
|
+
providerId,
|
|
67
|
+
model: params.model,
|
|
68
|
+
workspaceDir: params.workspaceDir,
|
|
69
|
+
timeoutMs: params.timeoutMs || this.options.defaultTimeoutMs || DEFAULT_TIMEOUT_MS,
|
|
70
|
+
systemPrompt: params.systemPrompt,
|
|
71
|
+
backend: backendResolved.config,
|
|
72
|
+
cliSessionId: existingCliSessionId,
|
|
73
|
+
queueWaitMs,
|
|
74
|
+
runtimeSessionId: sessionEntry.runtimeSessionId,
|
|
75
|
+
agentEnv: params.agentEnv,
|
|
76
|
+
agentClearEnv: params.agentClearEnv,
|
|
77
|
+
fchContext: params.fchContext,
|
|
78
|
+
callbacks,
|
|
79
|
+
});
|
|
80
|
+
if (result.cliSessionId) {
|
|
81
|
+
await sessionStore.setCliSessionId(envelope.sessionId, providerId, result.cliSessionId, {
|
|
82
|
+
model: params.model,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
else if (providerId === 'codex-cli') {
|
|
86
|
+
this.options.logger.warn({
|
|
87
|
+
provider: providerId,
|
|
88
|
+
sessionId: envelope.sessionId,
|
|
89
|
+
runtimeSessionId: sessionEntry.runtimeSessionId,
|
|
90
|
+
isResume: result.isResume,
|
|
91
|
+
diagnostics: result.diagnostics,
|
|
92
|
+
}, 'Codex turn completed without a persisted thread id; future turns will remain fresh');
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
await sessionStore.ensureSession(envelope.sessionId, {
|
|
96
|
+
provider: providerId,
|
|
97
|
+
model: params.model,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
if (existingCliSessionId &&
|
|
104
|
+
isSessionExpiredError(error) &&
|
|
105
|
+
(providerId === 'codex-cli' ||
|
|
106
|
+
Boolean(backendResolved.config.resumeArgs && backendResolved.config.resumeArgs.length > 0))) {
|
|
107
|
+
this.options.logger.warn({ provider: providerId, sessionId: envelope.sessionId }, 'CLI session expired, clearing provider mapping and retrying fresh turn');
|
|
108
|
+
await sessionStore.clearCliSessionId(envelope.sessionId, providerId, { model: params.model });
|
|
109
|
+
const retried = await this.executeTurn({
|
|
110
|
+
envelope,
|
|
111
|
+
providerId,
|
|
112
|
+
model: params.model,
|
|
113
|
+
workspaceDir: params.workspaceDir,
|
|
114
|
+
timeoutMs: params.timeoutMs || this.options.defaultTimeoutMs || DEFAULT_TIMEOUT_MS,
|
|
115
|
+
systemPrompt: params.systemPrompt,
|
|
116
|
+
backend: backendResolved.config,
|
|
117
|
+
cliSessionId: undefined,
|
|
118
|
+
queueWaitMs,
|
|
119
|
+
runtimeSessionId: sessionEntry.runtimeSessionId,
|
|
120
|
+
agentEnv: params.agentEnv,
|
|
121
|
+
agentClearEnv: params.agentClearEnv,
|
|
122
|
+
fchContext: params.fchContext,
|
|
123
|
+
callbacks,
|
|
124
|
+
});
|
|
125
|
+
if (retried.cliSessionId) {
|
|
126
|
+
await sessionStore.setCliSessionId(envelope.sessionId, providerId, retried.cliSessionId, {
|
|
127
|
+
model: params.model,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return retried;
|
|
131
|
+
}
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
async emitProcessing(callbacks) {
|
|
137
|
+
await callbacks.onProcessing?.();
|
|
138
|
+
}
|
|
139
|
+
async executeTurn(params) {
|
|
140
|
+
if (params.providerId === 'codex-cli' && params.backend.runtimeMode === 'app-server') {
|
|
141
|
+
return this.executeCodexAppServerTurn(params);
|
|
142
|
+
}
|
|
143
|
+
return this.executeCliExecTurn(params);
|
|
144
|
+
}
|
|
145
|
+
async teardown() {
|
|
146
|
+
const slots = [
|
|
147
|
+
...(this.activeCodexRuntime ? [this.activeCodexRuntime] : []),
|
|
148
|
+
...this.drainingCodexRuntimes,
|
|
149
|
+
];
|
|
150
|
+
this.activeCodexRuntime = null;
|
|
151
|
+
this.drainingCodexRuntimes.clear();
|
|
152
|
+
await Promise.all(slots.map(async (slot) => {
|
|
153
|
+
await slot.runtime.teardown().catch(() => undefined);
|
|
154
|
+
}));
|
|
155
|
+
}
|
|
156
|
+
async rotateCodexRuntime() {
|
|
157
|
+
const slot = this.activeCodexRuntime;
|
|
158
|
+
if (!slot) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
this.activeCodexRuntime = null;
|
|
162
|
+
if (slot.refCount > 0) {
|
|
163
|
+
this.drainingCodexRuntimes.add(slot);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
await slot.runtime.teardown().catch(() => undefined);
|
|
167
|
+
}
|
|
168
|
+
async acquireCodexRuntime(backend, fchContext) {
|
|
169
|
+
const fchContextKey = makeFchContextKey(fchContext);
|
|
170
|
+
if (this.activeCodexRuntime && this.activeCodexRuntime.fchContextKey !== fchContextKey) {
|
|
171
|
+
await this.rotateCodexRuntime();
|
|
172
|
+
}
|
|
173
|
+
if (!this.activeCodexRuntime) {
|
|
174
|
+
this.activeCodexRuntime = {
|
|
175
|
+
runtime: this.options.createCodexRuntime?.(backend, fchContext) ?? new CodexAppServerRuntime({
|
|
176
|
+
backend,
|
|
177
|
+
logger: this.options.logger,
|
|
178
|
+
fchContext,
|
|
179
|
+
}),
|
|
180
|
+
refCount: 0,
|
|
181
|
+
fchContextKey,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
return this.activeCodexRuntime;
|
|
185
|
+
}
|
|
186
|
+
async releaseCodexRuntime(slot) {
|
|
187
|
+
slot.refCount = Math.max(0, slot.refCount - 1);
|
|
188
|
+
if (!this.drainingCodexRuntimes.has(slot) || slot.refCount > 0) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
this.drainingCodexRuntimes.delete(slot);
|
|
192
|
+
await slot.runtime.teardown().catch(() => undefined);
|
|
193
|
+
}
|
|
194
|
+
async executeCodexAppServerTurn(params) {
|
|
195
|
+
const normalizedModel = normalizeCliModel(params.model, params.backend);
|
|
196
|
+
const existingCliSessionId = params.cliSessionId?.trim() || undefined;
|
|
197
|
+
const isResume = Boolean(existingCliSessionId);
|
|
198
|
+
const promptPayload = buildPromptPayload(params.envelope, params.backend, true);
|
|
199
|
+
this.options.logger.info({
|
|
200
|
+
provider: params.providerId,
|
|
201
|
+
model: normalizedModel,
|
|
202
|
+
sessionId: params.envelope.sessionId,
|
|
203
|
+
cliSessionId: existingCliSessionId,
|
|
204
|
+
runtimeSessionId: params.runtimeSessionId,
|
|
205
|
+
isResume,
|
|
206
|
+
runtimeMode: 'app-server',
|
|
207
|
+
}, 'Starting Codex app-server turn');
|
|
208
|
+
const slot = await this.acquireCodexRuntime(params.backend, params.fchContext);
|
|
209
|
+
slot.refCount += 1;
|
|
210
|
+
try {
|
|
211
|
+
const result = await slot.runtime.run({
|
|
212
|
+
sessionId: params.envelope.sessionId,
|
|
213
|
+
runtimeSessionId: params.runtimeSessionId,
|
|
214
|
+
existingThreadId: existingCliSessionId,
|
|
215
|
+
model: normalizedModel,
|
|
216
|
+
workspaceDir: params.workspaceDir,
|
|
217
|
+
timeoutMs: params.timeoutMs,
|
|
218
|
+
noProgressTimeoutMs: resolveCliNoOutputTimeoutMs(params.backend, params.timeoutMs, isResume),
|
|
219
|
+
systemPrompt: resolveAppServerSystemPrompt(params.backend, params.systemPrompt, !existingCliSessionId),
|
|
220
|
+
prompt: promptPayload.prompt,
|
|
221
|
+
imagePaths: promptPayload.imagePaths,
|
|
222
|
+
onProgress: async (text) => {
|
|
223
|
+
await params.callbacks?.onProgress?.(text);
|
|
224
|
+
},
|
|
225
|
+
onAssistantMessage: async (text) => {
|
|
226
|
+
await params.callbacks?.onAssistantMessage?.(text);
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
return {
|
|
230
|
+
text: result.text,
|
|
231
|
+
messages: result.messages,
|
|
232
|
+
provider: params.providerId,
|
|
233
|
+
model: params.model,
|
|
234
|
+
cliSessionId: result.cliSessionId,
|
|
235
|
+
isResume: result.isResume,
|
|
236
|
+
usage: result.usage,
|
|
237
|
+
diagnostics: {
|
|
238
|
+
...result.diagnostics,
|
|
239
|
+
queueWaitMs: params.queueWaitMs,
|
|
240
|
+
totalElapsedMs: result.diagnostics.totalElapsedMs + params.queueWaitMs,
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
finally {
|
|
245
|
+
await this.releaseCodexRuntime(slot);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
async executeCliExecTurn(params) {
|
|
249
|
+
const normalizedModel = normalizeCliModel(params.model, params.backend);
|
|
250
|
+
const existingCliSessionId = params.cliSessionId?.trim() || undefined;
|
|
251
|
+
const useResume = Boolean(existingCliSessionId &&
|
|
252
|
+
params.backend.resumeArgs &&
|
|
253
|
+
params.backend.resumeArgs.length > 0);
|
|
254
|
+
const sessionIdToSend = resolveSessionIdToSend({
|
|
255
|
+
backend: params.backend,
|
|
256
|
+
cliSessionId: existingCliSessionId,
|
|
257
|
+
});
|
|
258
|
+
const promptPayload = buildPromptPayload(params.envelope, params.backend, Boolean(params.backend.imageArg));
|
|
259
|
+
const promptInput = resolvePromptInput({
|
|
260
|
+
backend: params.backend,
|
|
261
|
+
prompt: promptPayload.prompt,
|
|
262
|
+
});
|
|
263
|
+
const outputMode = useResume
|
|
264
|
+
? (params.backend.resumeOutput ?? params.backend.output ?? 'json')
|
|
265
|
+
: (params.backend.output ?? 'json');
|
|
266
|
+
const baseArgs = useResume
|
|
267
|
+
? (params.backend.resumeArgs ?? params.backend.args ?? [])
|
|
268
|
+
: (params.backend.args ?? []);
|
|
269
|
+
const resolvedBaseArgs = useResume
|
|
270
|
+
? baseArgs.map((entry) => entry.replaceAll('{sessionId}', existingCliSessionId ?? ''))
|
|
271
|
+
: baseArgs;
|
|
272
|
+
const codexFchConfigArgs = params.providerId === 'codex-cli'
|
|
273
|
+
? buildCodexFchConfigArgs(params.fchContext)
|
|
274
|
+
: [];
|
|
275
|
+
const args = buildCliArgs({
|
|
276
|
+
backend: params.backend,
|
|
277
|
+
baseArgs: [...codexFchConfigArgs, ...resolvedBaseArgs],
|
|
278
|
+
modelId: normalizedModel,
|
|
279
|
+
sessionId: sessionIdToSend.sessionId,
|
|
280
|
+
systemPrompt: resolveSystemPrompt(params.backend, params.systemPrompt, sessionIdToSend.isNew, useResume),
|
|
281
|
+
imagePaths: promptPayload.imagePaths,
|
|
282
|
+
promptArg: promptInput.promptArg,
|
|
283
|
+
useResume,
|
|
284
|
+
});
|
|
285
|
+
this.options.logger.info({
|
|
286
|
+
provider: params.providerId,
|
|
287
|
+
model: normalizedModel,
|
|
288
|
+
sessionId: params.envelope.sessionId,
|
|
289
|
+
cliSessionId: existingCliSessionId,
|
|
290
|
+
runtimeSessionId: params.runtimeSessionId,
|
|
291
|
+
isResume: useResume,
|
|
292
|
+
}, 'Starting CLI turn');
|
|
293
|
+
// Build env per design: process.env → backend.env → agent.env → clear
|
|
294
|
+
const env = {
|
|
295
|
+
...process.env,
|
|
296
|
+
...params.backend.env,
|
|
297
|
+
...(params.agentEnv ?? {}),
|
|
298
|
+
...(params.fchContext ? buildFchEnv(params.fchContext) : {}),
|
|
299
|
+
};
|
|
300
|
+
for (const key of params.backend.clearEnv ?? []) {
|
|
301
|
+
delete env[key];
|
|
302
|
+
}
|
|
303
|
+
for (const key of params.agentClearEnv ?? []) {
|
|
304
|
+
delete env[key];
|
|
305
|
+
}
|
|
306
|
+
const probe = createCliOutputProbe({
|
|
307
|
+
backend: params.backend,
|
|
308
|
+
mode: outputMode,
|
|
309
|
+
});
|
|
310
|
+
let stdoutLineBuffer = '';
|
|
311
|
+
let lastProgressText = '';
|
|
312
|
+
let assistantStreamText = '';
|
|
313
|
+
const output = await executeCliCommand({
|
|
314
|
+
command: params.backend.command,
|
|
315
|
+
args,
|
|
316
|
+
cwd: params.workspaceDir,
|
|
317
|
+
env,
|
|
318
|
+
stdin: promptInput.stdin,
|
|
319
|
+
timeoutMs: params.timeoutMs,
|
|
320
|
+
noOutputTimeoutMs: resolveCliNoOutputTimeoutMs(params.backend, params.timeoutMs, useResume),
|
|
321
|
+
outputMode,
|
|
322
|
+
backend: params.backend,
|
|
323
|
+
onStdoutChunk(chunk, atMs) {
|
|
324
|
+
probe.onStdout(chunk, atMs);
|
|
325
|
+
if (outputMode !== 'jsonl') {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
stdoutLineBuffer += chunk;
|
|
329
|
+
const lines = stdoutLineBuffer.split(/\r?\n/g);
|
|
330
|
+
stdoutLineBuffer = lines.pop() ?? '';
|
|
331
|
+
for (const rawLine of lines) {
|
|
332
|
+
const parsedChunk = parseCliStreamChunk(rawLine, outputMode, params.backend);
|
|
333
|
+
const rawText = parsedChunk?.text;
|
|
334
|
+
const nextText = rawText?.trim();
|
|
335
|
+
if (parsedChunk?.kind === 'assistant') {
|
|
336
|
+
if (parsedChunk.incremental) {
|
|
337
|
+
if (rawText === undefined) {
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
assistantStreamText = `${assistantStreamText}${rawText}`;
|
|
341
|
+
if (!assistantStreamText.trim()) {
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
void Promise.resolve(params.callbacks?.onAssistantMessage?.(assistantStreamText)).catch(() => undefined);
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
if (!nextText) {
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
const assistantText = nextText;
|
|
351
|
+
assistantStreamText = assistantText;
|
|
352
|
+
void Promise.resolve(params.callbacks?.onAssistantMessage?.(assistantText)).catch(() => undefined);
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
if (!nextText) {
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
if (nextText === lastProgressText) {
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
lastProgressText = nextText;
|
|
362
|
+
void Promise.resolve(params.callbacks?.onProgress?.(nextText)).catch(() => undefined);
|
|
363
|
+
}
|
|
364
|
+
},
|
|
365
|
+
onStderrChunk(chunk, atMs) {
|
|
366
|
+
probe.onStderr(chunk, atMs);
|
|
367
|
+
},
|
|
368
|
+
});
|
|
369
|
+
const probeSnapshot = probe.snapshot();
|
|
370
|
+
if (outputMode === 'jsonl' && stdoutLineBuffer.trim()) {
|
|
371
|
+
const parsedChunk = parseCliStreamChunk(stdoutLineBuffer, outputMode, params.backend);
|
|
372
|
+
const rawText = parsedChunk?.text;
|
|
373
|
+
const nextText = rawText?.trim();
|
|
374
|
+
if (parsedChunk?.kind === 'assistant') {
|
|
375
|
+
if (parsedChunk.incremental) {
|
|
376
|
+
if (rawText !== undefined) {
|
|
377
|
+
assistantStreamText = `${assistantStreamText}${rawText}`;
|
|
378
|
+
if (assistantStreamText.trim()) {
|
|
379
|
+
await params.callbacks?.onAssistantMessage?.(assistantStreamText);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
else if (nextText) {
|
|
384
|
+
assistantStreamText = nextText;
|
|
385
|
+
await params.callbacks?.onAssistantMessage?.(nextText);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
else if (nextText && nextText !== lastProgressText) {
|
|
389
|
+
lastProgressText = nextText;
|
|
390
|
+
await params.callbacks?.onProgress?.(nextText);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
const parsed = parseCliOutput(output.stdout.trim(), outputMode, params.backend) ?? {
|
|
394
|
+
text: probeSnapshot.lastText ?? output.stdout.trim(),
|
|
395
|
+
cliSessionId: probeSnapshot.cliSessionId,
|
|
396
|
+
usage: probeSnapshot.usage,
|
|
397
|
+
};
|
|
398
|
+
const cliSessionId = parsed.cliSessionId ?? probeSnapshot.cliSessionId ?? existingCliSessionId ?? sessionIdToSend.sessionId;
|
|
399
|
+
const diagnostics = buildRunDiagnostics({
|
|
400
|
+
queueWaitMs: params.queueWaitMs,
|
|
401
|
+
commandElapsedMs: output.elapsedMs,
|
|
402
|
+
outputMode,
|
|
403
|
+
stdout: output.stdout,
|
|
404
|
+
stderr: output.stderr,
|
|
405
|
+
cliSessionId,
|
|
406
|
+
existingCliSessionId,
|
|
407
|
+
runtimeSessionId: sessionIdToSend.sessionId,
|
|
408
|
+
probe: probeSnapshot,
|
|
409
|
+
});
|
|
410
|
+
if (params.providerId === 'codex-cli' && !cliSessionId) {
|
|
411
|
+
this.options.logger.warn({
|
|
412
|
+
provider: params.providerId,
|
|
413
|
+
sessionId: params.envelope.sessionId,
|
|
414
|
+
runtimeSessionId: params.runtimeSessionId,
|
|
415
|
+
isResume: useResume,
|
|
416
|
+
diagnostics,
|
|
417
|
+
stdoutPreview: trimOutputPreview(output.stdout),
|
|
418
|
+
stderrPreview: trimOutputPreview(output.stderr),
|
|
419
|
+
}, 'Unable to extract Codex thread id from CLI output');
|
|
420
|
+
}
|
|
421
|
+
const text = parsed.text.trim() || probeSnapshot.lastText?.trim() || output.stdout.trim();
|
|
422
|
+
return {
|
|
423
|
+
text,
|
|
424
|
+
provider: params.providerId,
|
|
425
|
+
model: params.model,
|
|
426
|
+
cliSessionId,
|
|
427
|
+
isResume: useResume,
|
|
428
|
+
usage: parsed.usage,
|
|
429
|
+
diagnostics,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
async function enqueueCliRun(key, task) {
|
|
434
|
+
const queuedAt = Date.now();
|
|
435
|
+
const previous = CLI_RUN_QUEUES.get(key) ?? Promise.resolve();
|
|
436
|
+
let release;
|
|
437
|
+
const current = new Promise((resolve) => {
|
|
438
|
+
release = resolve;
|
|
439
|
+
});
|
|
440
|
+
const tail = previous.catch(() => undefined).then(() => current);
|
|
441
|
+
CLI_RUN_QUEUES.set(key, tail);
|
|
442
|
+
await previous.catch(() => undefined);
|
|
443
|
+
try {
|
|
444
|
+
return await task({
|
|
445
|
+
queueWaitMs: Date.now() - queuedAt,
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
finally {
|
|
449
|
+
release();
|
|
450
|
+
if (CLI_RUN_QUEUES.get(key) === tail) {
|
|
451
|
+
CLI_RUN_QUEUES.delete(key);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
function normalizeCliModel(model, backend) {
|
|
456
|
+
const trimmed = model.trim();
|
|
457
|
+
if (!trimmed) {
|
|
458
|
+
return trimmed;
|
|
459
|
+
}
|
|
460
|
+
const lower = trimmed.toLowerCase();
|
|
461
|
+
return backend.modelAliases?.[trimmed] ?? backend.modelAliases?.[lower] ?? trimmed;
|
|
462
|
+
}
|
|
463
|
+
function resolveSystemPrompt(backend, systemPrompt, isNewSession, useResume) {
|
|
464
|
+
const trimmed = systemPrompt?.trim();
|
|
465
|
+
if (!trimmed || !backend.systemPromptArg || useResume) {
|
|
466
|
+
return undefined;
|
|
467
|
+
}
|
|
468
|
+
const when = backend.systemPromptWhen ?? 'first';
|
|
469
|
+
if (when === 'never') {
|
|
470
|
+
return undefined;
|
|
471
|
+
}
|
|
472
|
+
if (when === 'first' && !isNewSession) {
|
|
473
|
+
return undefined;
|
|
474
|
+
}
|
|
475
|
+
return trimmed;
|
|
476
|
+
}
|
|
477
|
+
function resolveAppServerSystemPrompt(backend, systemPrompt, isNewThread) {
|
|
478
|
+
const trimmed = systemPrompt?.trim();
|
|
479
|
+
if (!trimmed) {
|
|
480
|
+
return undefined;
|
|
481
|
+
}
|
|
482
|
+
const when = backend.systemPromptWhen ?? 'first';
|
|
483
|
+
if (when === 'never') {
|
|
484
|
+
return undefined;
|
|
485
|
+
}
|
|
486
|
+
if (when === 'first' && !isNewThread) {
|
|
487
|
+
return undefined;
|
|
488
|
+
}
|
|
489
|
+
return trimmed;
|
|
490
|
+
}
|
|
491
|
+
function resolveSessionIdToSend(params) {
|
|
492
|
+
const mode = params.backend.sessionMode ?? 'always';
|
|
493
|
+
const existing = params.cliSessionId?.trim();
|
|
494
|
+
if (mode === 'none') {
|
|
495
|
+
return { sessionId: undefined, isNew: !existing };
|
|
496
|
+
}
|
|
497
|
+
if (mode === 'existing') {
|
|
498
|
+
return { sessionId: existing, isNew: !existing };
|
|
499
|
+
}
|
|
500
|
+
if (existing) {
|
|
501
|
+
return { sessionId: existing, isNew: false };
|
|
502
|
+
}
|
|
503
|
+
return { sessionId: randomUUID(), isNew: true };
|
|
504
|
+
}
|
|
505
|
+
function resolvePromptInput(params) {
|
|
506
|
+
const inputMode = params.backend.input ?? 'arg';
|
|
507
|
+
if (inputMode === 'stdin') {
|
|
508
|
+
return { stdin: params.prompt };
|
|
509
|
+
}
|
|
510
|
+
if (params.backend.maxPromptArgChars && params.prompt.length > params.backend.maxPromptArgChars) {
|
|
511
|
+
return { stdin: params.prompt };
|
|
512
|
+
}
|
|
513
|
+
return { promptArg: params.prompt };
|
|
514
|
+
}
|
|
515
|
+
export function buildGroupSenderContext(envelope) {
|
|
516
|
+
if (envelope.source.chatType === 'private') {
|
|
517
|
+
return undefined;
|
|
518
|
+
}
|
|
519
|
+
const senderName = envelope.source.senderName?.trim();
|
|
520
|
+
const senderUsername = envelope.source.username?.trim();
|
|
521
|
+
const payload = {
|
|
522
|
+
chat_type: envelope.source.chatType,
|
|
523
|
+
sender_id: String(envelope.source.userId),
|
|
524
|
+
sender_name: senderName || undefined,
|
|
525
|
+
sender_username: senderUsername || undefined,
|
|
526
|
+
};
|
|
527
|
+
if (!payload.sender_name && !payload.sender_username && !payload.sender_id) {
|
|
528
|
+
return undefined;
|
|
529
|
+
}
|
|
530
|
+
return [
|
|
531
|
+
'Conversation info (untrusted metadata):',
|
|
532
|
+
'```json',
|
|
533
|
+
JSON.stringify(payload, null, 2),
|
|
534
|
+
'```',
|
|
535
|
+
].join('\n');
|
|
536
|
+
}
|
|
537
|
+
export function buildPromptPayload(envelope, backend, useSeparateImageInputs = Boolean(backend.imageArg)) {
|
|
538
|
+
const imagePaths = envelope.content.attachments
|
|
539
|
+
.filter((attachment) => attachment.type === 'photo' && attachment.localPath)
|
|
540
|
+
.map((attachment) => attachment.localPath);
|
|
541
|
+
const fileLines = envelope.content.attachments.map((attachment) => {
|
|
542
|
+
const name = attachment.fileName ? ` (${attachment.fileName})` : '';
|
|
543
|
+
return `Attachment: ${attachment.localPath}${name}`;
|
|
544
|
+
});
|
|
545
|
+
const promptParts = envelope.content.promptMode === 'cli_passthrough'
|
|
546
|
+
? [envelope.content.text.trim()]
|
|
547
|
+
: [buildGroupSenderContext(envelope), envelope.content.text.trim()];
|
|
548
|
+
if (!useSeparateImageInputs && imagePaths.length > 0) {
|
|
549
|
+
promptParts.push(...imagePaths.map((imagePath) => `Image: ${imagePath}`));
|
|
550
|
+
}
|
|
551
|
+
const nonImageFiles = fileLines.filter((line) => !imagePaths.some((imagePath) => line.includes(imagePath)));
|
|
552
|
+
promptParts.push(...nonImageFiles);
|
|
553
|
+
return {
|
|
554
|
+
prompt: promptParts.filter(Boolean).join('\n\n').trim(),
|
|
555
|
+
imagePaths: useSeparateImageInputs && imagePaths.length > 0 ? imagePaths : undefined,
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
function buildCliArgs(params) {
|
|
559
|
+
const args = [...params.baseArgs];
|
|
560
|
+
if (!params.useResume && params.backend.modelArg && params.modelId) {
|
|
561
|
+
args.push(params.backend.modelArg, params.modelId);
|
|
562
|
+
}
|
|
563
|
+
if (!params.useResume && params.systemPrompt && params.backend.systemPromptArg) {
|
|
564
|
+
args.push(params.backend.systemPromptArg, params.systemPrompt);
|
|
565
|
+
}
|
|
566
|
+
if (!params.useResume && params.sessionId) {
|
|
567
|
+
if (params.backend.sessionArgs && params.backend.sessionArgs.length > 0) {
|
|
568
|
+
for (const entry of params.backend.sessionArgs) {
|
|
569
|
+
args.push(entry.replaceAll('{sessionId}', params.sessionId));
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
else if (params.backend.sessionArg) {
|
|
573
|
+
args.push(params.backend.sessionArg, params.sessionId);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
if (params.imagePaths && params.imagePaths.length > 0 && params.backend.imageArg) {
|
|
577
|
+
const imageMode = params.backend.imageMode ?? 'repeat';
|
|
578
|
+
if (imageMode === 'list') {
|
|
579
|
+
args.push(params.backend.imageArg, params.imagePaths.join(','));
|
|
580
|
+
}
|
|
581
|
+
else {
|
|
582
|
+
for (const imagePath of params.imagePaths) {
|
|
583
|
+
args.push(params.backend.imageArg, imagePath);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
if (params.promptArg !== undefined) {
|
|
588
|
+
args.push(params.promptArg);
|
|
589
|
+
}
|
|
590
|
+
return args;
|
|
591
|
+
}
|
|
592
|
+
function resolveCliNoOutputTimeoutMs(backend, timeoutMs, useResume) {
|
|
593
|
+
const defaults = useResume ? CLI_RESUME_WATCHDOG_DEFAULTS : CLI_FRESH_WATCHDOG_DEFAULTS;
|
|
594
|
+
const configured = useResume
|
|
595
|
+
? backend.reliability?.watchdog?.resume
|
|
596
|
+
: backend.reliability?.watchdog?.fresh;
|
|
597
|
+
if (configured?.noOutputTimeoutMs) {
|
|
598
|
+
return Math.max(configured.noOutputTimeoutMs, CLI_WATCHDOG_MIN_TIMEOUT_MS);
|
|
599
|
+
}
|
|
600
|
+
const ratio = configured?.noOutputTimeoutRatio ?? defaults.noOutputTimeoutRatio;
|
|
601
|
+
const minMs = configured?.minMs ?? defaults.minMs;
|
|
602
|
+
const maxMs = configured?.maxMs ?? defaults.maxMs;
|
|
603
|
+
const computed = Math.round(timeoutMs * ratio);
|
|
604
|
+
return Math.max(Math.min(computed, maxMs), Math.max(minMs, CLI_WATCHDOG_MIN_TIMEOUT_MS));
|
|
605
|
+
}
|
|
606
|
+
async function executeCliCommand(params) {
|
|
607
|
+
return new Promise((resolve, reject) => {
|
|
608
|
+
const stdoutChunks = [];
|
|
609
|
+
const stderrChunks = [];
|
|
610
|
+
let settled = false;
|
|
611
|
+
let timedOutReason = null;
|
|
612
|
+
const startedAt = Date.now();
|
|
613
|
+
const child = spawn(params.command, params.args, {
|
|
614
|
+
cwd: params.cwd,
|
|
615
|
+
env: params.env,
|
|
616
|
+
stdio: 'pipe',
|
|
617
|
+
shell: process.platform === 'win32',
|
|
618
|
+
windowsHide: true,
|
|
619
|
+
});
|
|
620
|
+
const finish = (error, result) => {
|
|
621
|
+
if (settled)
|
|
622
|
+
return;
|
|
623
|
+
settled = true;
|
|
624
|
+
clearTimeout(overallTimer);
|
|
625
|
+
clearTimeout(noOutputTimer);
|
|
626
|
+
if (error) {
|
|
627
|
+
reject(error);
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
resolve(result ?? {
|
|
631
|
+
stdout: decodeCliOutput(Buffer.concat(stdoutChunks)),
|
|
632
|
+
stderr: decodeCliOutput(Buffer.concat(stderrChunks)),
|
|
633
|
+
elapsedMs: Date.now() - startedAt,
|
|
634
|
+
});
|
|
635
|
+
};
|
|
636
|
+
const armNoOutputTimer = () => {
|
|
637
|
+
clearTimeout(noOutputTimer);
|
|
638
|
+
noOutputTimer = setTimeout(() => {
|
|
639
|
+
timedOutReason = 'no-output-timeout';
|
|
640
|
+
child.kill();
|
|
641
|
+
}, params.noOutputTimeoutMs);
|
|
642
|
+
};
|
|
643
|
+
const overallTimer = setTimeout(() => {
|
|
644
|
+
timedOutReason = 'overall-timeout';
|
|
645
|
+
child.kill();
|
|
646
|
+
}, params.timeoutMs);
|
|
647
|
+
let noOutputTimer = setTimeout(() => undefined, params.noOutputTimeoutMs);
|
|
648
|
+
armNoOutputTimer();
|
|
649
|
+
child.on('error', (error) => {
|
|
650
|
+
finish(new CliExecutionError(error.message, 'spawn-error', decodeCliOutput(Buffer.concat(stdoutChunks)), decodeCliOutput(Buffer.concat(stderrChunks))));
|
|
651
|
+
});
|
|
652
|
+
child.stdout?.on('data', (chunk) => {
|
|
653
|
+
const buffer = typeof chunk === 'string' ? Buffer.from(chunk) : chunk;
|
|
654
|
+
stdoutChunks.push(buffer);
|
|
655
|
+
params.onStdoutChunk?.(decodeCliOutput(buffer), Date.now() - startedAt);
|
|
656
|
+
armNoOutputTimer();
|
|
657
|
+
});
|
|
658
|
+
child.stderr?.on('data', (chunk) => {
|
|
659
|
+
const buffer = typeof chunk === 'string' ? Buffer.from(chunk) : chunk;
|
|
660
|
+
stderrChunks.push(buffer);
|
|
661
|
+
params.onStderrChunk?.(decodeCliOutput(buffer), Date.now() - startedAt);
|
|
662
|
+
armNoOutputTimer();
|
|
663
|
+
});
|
|
664
|
+
child.on('close', (code) => {
|
|
665
|
+
const stdout = decodeCliOutput(Buffer.concat(stdoutChunks));
|
|
666
|
+
const stderr = decodeCliOutput(Buffer.concat(stderrChunks));
|
|
667
|
+
const trimmedStdout = stdout.trim();
|
|
668
|
+
const trimmedStderr = stderr.trim();
|
|
669
|
+
const elapsedMs = Date.now() - startedAt;
|
|
670
|
+
if (timedOutReason) {
|
|
671
|
+
const timeoutMessage = timedOutReason === 'overall-timeout'
|
|
672
|
+
? `CLI exceeded timeout (${Math.round(params.timeoutMs / 1000)}s) and was terminated.`
|
|
673
|
+
: `CLI produced no output for ${Math.round(params.noOutputTimeoutMs / 1000)}s and was terminated.`;
|
|
674
|
+
finish(new CliExecutionError(timeoutMessage, timedOutReason, trimmedStdout, trimmedStderr, code));
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
if (code !== 0) {
|
|
678
|
+
finish(new CliExecutionError(buildCliFailureMessage({
|
|
679
|
+
stdout: trimmedStdout,
|
|
680
|
+
stderr: trimmedStderr,
|
|
681
|
+
exitCode: code,
|
|
682
|
+
outputMode: params.outputMode,
|
|
683
|
+
backend: params.backend,
|
|
684
|
+
}), 'exit', trimmedStdout, trimmedStderr, code));
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
finish(undefined, { stdout: trimmedStdout, stderr: trimmedStderr, elapsedMs });
|
|
688
|
+
});
|
|
689
|
+
if (params.stdin !== undefined && child.stdin) {
|
|
690
|
+
child.stdin.write(params.stdin);
|
|
691
|
+
}
|
|
692
|
+
child.stdin?.end();
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
function decodeCliOutput(buffer) {
|
|
696
|
+
if (buffer.length === 0) {
|
|
697
|
+
return '';
|
|
698
|
+
}
|
|
699
|
+
const utf8 = buffer.toString('utf8');
|
|
700
|
+
if (!shouldTryWindowsLegacyDecode(utf8)) {
|
|
701
|
+
return utf8;
|
|
702
|
+
}
|
|
703
|
+
for (const encoding of ['gb18030', 'gbk']) {
|
|
704
|
+
const decoded = tryDecodeWithEncoding(buffer, encoding);
|
|
705
|
+
if (decoded && decoded !== utf8) {
|
|
706
|
+
return decoded;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
return utf8;
|
|
710
|
+
}
|
|
711
|
+
function buildCliFailureMessage(params) {
|
|
712
|
+
const parsed = parseStructuredCliFailure(params.stdout, params.outputMode, params.backend)
|
|
713
|
+
?? parseStructuredCliFailure(params.stderr, params.outputMode, params.backend);
|
|
714
|
+
if (parsed) {
|
|
715
|
+
return parsed;
|
|
716
|
+
}
|
|
717
|
+
return params.stderr
|
|
718
|
+
|| params.stdout
|
|
719
|
+
|| `CLI failed with exit code ${params.exitCode ?? 'unknown'}.`;
|
|
720
|
+
}
|
|
721
|
+
function parseStructuredCliFailure(raw, outputMode, backend) {
|
|
722
|
+
const trimmed = raw.trim();
|
|
723
|
+
if (!trimmed || outputMode === 'text') {
|
|
724
|
+
return undefined;
|
|
725
|
+
}
|
|
726
|
+
if (outputMode === 'jsonl') {
|
|
727
|
+
const messages = [];
|
|
728
|
+
for (const line of trimmed.split(/\r?\n/g)) {
|
|
729
|
+
const message = extractCliFailureMessageFromJson(line, backend);
|
|
730
|
+
if (message) {
|
|
731
|
+
messages.push(message);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
return messages.at(-1);
|
|
735
|
+
}
|
|
736
|
+
return extractCliFailureMessageFromJson(trimmed, backend);
|
|
737
|
+
}
|
|
738
|
+
function extractCliFailureMessageFromJson(raw, backend) {
|
|
739
|
+
try {
|
|
740
|
+
const parsed = JSON.parse(raw);
|
|
741
|
+
if (!isPlainRecord(parsed)) {
|
|
742
|
+
return undefined;
|
|
743
|
+
}
|
|
744
|
+
const eventType = getPlainString(parsed.type)?.toLowerCase();
|
|
745
|
+
const subtype = getPlainString(parsed.subtype) ?? getPlainString(parsed.error_type);
|
|
746
|
+
const isError = parsed.is_error === true || eventType === 'error' || Boolean(subtype?.toLowerCase().includes('error'));
|
|
747
|
+
if (!isError) {
|
|
748
|
+
return undefined;
|
|
749
|
+
}
|
|
750
|
+
const message = pickCliFailureText(parsed);
|
|
751
|
+
if (!message) {
|
|
752
|
+
return subtype ? `CLI failed: ${subtype}` : undefined;
|
|
753
|
+
}
|
|
754
|
+
const providerName = backend?.command ? path.basename(backend.command) : 'CLI';
|
|
755
|
+
return `${providerName} failed${subtype ? ` (${subtype})` : ''}: ${message}`;
|
|
756
|
+
}
|
|
757
|
+
catch {
|
|
758
|
+
return undefined;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
function pickCliFailureText(parsed) {
|
|
762
|
+
const candidates = [
|
|
763
|
+
parsed.message,
|
|
764
|
+
parsed.error,
|
|
765
|
+
parsed.result,
|
|
766
|
+
parsed.data,
|
|
767
|
+
parsed.payload,
|
|
768
|
+
];
|
|
769
|
+
for (const candidate of candidates) {
|
|
770
|
+
const text = collectFailureText(candidate);
|
|
771
|
+
if (text) {
|
|
772
|
+
return text;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
return undefined;
|
|
776
|
+
}
|
|
777
|
+
function collectFailureText(value) {
|
|
778
|
+
if (typeof value === 'string') {
|
|
779
|
+
return value.trim() || undefined;
|
|
780
|
+
}
|
|
781
|
+
if (!isPlainRecord(value)) {
|
|
782
|
+
return undefined;
|
|
783
|
+
}
|
|
784
|
+
return getPlainString(value.message)
|
|
785
|
+
?? getPlainString(value.text)
|
|
786
|
+
?? getPlainString(value.details)
|
|
787
|
+
?? getPlainString(value.description);
|
|
788
|
+
}
|
|
789
|
+
function isPlainRecord(value) {
|
|
790
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
791
|
+
}
|
|
792
|
+
function getPlainString(value) {
|
|
793
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
794
|
+
}
|
|
795
|
+
function shouldTryWindowsLegacyDecode(decoded) {
|
|
796
|
+
if (process.platform !== 'win32') {
|
|
797
|
+
return false;
|
|
798
|
+
}
|
|
799
|
+
const replacementCount = (decoded.match(/\uFFFD/g) ?? []).length;
|
|
800
|
+
if (replacementCount > 0) {
|
|
801
|
+
return true;
|
|
802
|
+
}
|
|
803
|
+
// Common mojibake markers when UTF-8/Chinese text is decoded with the wrong code page.
|
|
804
|
+
return ['鈥', '銆', '锛', '鏈', '鍦', '浠', '闂'].some((marker) => decoded.includes(marker));
|
|
805
|
+
}
|
|
806
|
+
function tryDecodeWithEncoding(buffer, encoding) {
|
|
807
|
+
try {
|
|
808
|
+
// TextDecoder coverage depends on the Node/ICU build; guard runtime support.
|
|
809
|
+
return new TextDecoder(encoding, { fatal: false }).decode(buffer);
|
|
810
|
+
}
|
|
811
|
+
catch {
|
|
812
|
+
return null;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
function buildRunDiagnostics(params) {
|
|
816
|
+
let cliSessionIdSource;
|
|
817
|
+
if (params.cliSessionId) {
|
|
818
|
+
if (params.probe.cliSessionId && params.cliSessionId === params.probe.cliSessionId) {
|
|
819
|
+
cliSessionIdSource = 'stream';
|
|
820
|
+
}
|
|
821
|
+
else if (params.existingCliSessionId && params.cliSessionId === params.existingCliSessionId) {
|
|
822
|
+
cliSessionIdSource = 'existing';
|
|
823
|
+
}
|
|
824
|
+
else if (params.runtimeSessionId && params.cliSessionId === params.runtimeSessionId) {
|
|
825
|
+
cliSessionIdSource = 'runtime';
|
|
826
|
+
}
|
|
827
|
+
else {
|
|
828
|
+
cliSessionIdSource = 'parsed';
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
return {
|
|
832
|
+
queueWaitMs: params.queueWaitMs,
|
|
833
|
+
commandElapsedMs: params.commandElapsedMs,
|
|
834
|
+
totalElapsedMs: params.queueWaitMs + params.commandElapsedMs,
|
|
835
|
+
outputMode: params.outputMode,
|
|
836
|
+
runtimeMode: 'exec',
|
|
837
|
+
hadStdout: Boolean(params.stdout),
|
|
838
|
+
hadStderr: Boolean(params.stderr),
|
|
839
|
+
firstStdoutMs: params.probe.firstStdoutMs,
|
|
840
|
+
firstStderrMs: params.probe.firstStderrMs,
|
|
841
|
+
threadStartedMs: params.probe.threadStartedMs,
|
|
842
|
+
firstAgentMessageMs: params.probe.firstAgentMessageMs,
|
|
843
|
+
turnCompletedMs: params.probe.turnCompletedMs,
|
|
844
|
+
reconnectCount: params.probe.reconnectCount,
|
|
845
|
+
fellBackToHttps: params.probe.fellBackToHttps,
|
|
846
|
+
threadOperation: params.existingCliSessionId ? 'resume' : 'start',
|
|
847
|
+
threadOperationElapsedMs: params.probe.threadStartedMs,
|
|
848
|
+
cliSessionIdSource,
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
function trimOutputPreview(raw, limit = 1200) {
|
|
852
|
+
const normalized = raw.trim();
|
|
853
|
+
if (!normalized) {
|
|
854
|
+
return undefined;
|
|
855
|
+
}
|
|
856
|
+
if (normalized.length <= limit) {
|
|
857
|
+
return normalized;
|
|
858
|
+
}
|
|
859
|
+
return `${normalized.slice(0, limit)}...`;
|
|
860
|
+
}
|
|
861
|
+
//# sourceMappingURL=cli-runner.js.map
|