funolio-agent 1.0.53 → 1.1.65
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/dist/approval.d.ts +1 -6
- package/dist/approval.d.ts.map +1 -1
- package/dist/approval.js +2 -7
- package/dist/approval.js.map +1 -1
- package/dist/auth/credential-reader.d.ts.map +1 -1
- package/dist/auth/credential-reader.js +4 -3
- package/dist/auth/credential-reader.js.map +1 -1
- package/dist/auth/token-refresh.d.ts +8 -0
- package/dist/auth/token-refresh.d.ts.map +1 -1
- package/dist/auth/token-refresh.js +82 -52
- package/dist/auth/token-refresh.js.map +1 -1
- package/dist/auto-organizer.d.ts.map +1 -1
- package/dist/auto-organizer.js +6 -7
- package/dist/auto-organizer.js.map +1 -1
- package/dist/bench-prefix.d.ts +16 -0
- package/dist/bench-prefix.d.ts.map +1 -0
- package/dist/bench-prefix.js +25 -0
- package/dist/bench-prefix.js.map +1 -0
- package/dist/bot-manager.d.ts +5 -1
- package/dist/bot-manager.d.ts.map +1 -1
- package/dist/bot-manager.js +46 -27
- package/dist/bot-manager.js.map +1 -1
- package/dist/chat-sync.d.ts +42 -0
- package/dist/chat-sync.d.ts.map +1 -0
- package/dist/chat-sync.js +95 -0
- package/dist/chat-sync.js.map +1 -0
- package/dist/clerk-model.d.ts +7 -0
- package/dist/clerk-model.d.ts.map +1 -1
- package/dist/clerk-model.js +42 -8
- package/dist/clerk-model.js.map +1 -1
- package/dist/cli-bootstrap-history.d.ts +10 -0
- package/dist/cli-bootstrap-history.d.ts.map +1 -0
- package/dist/cli-bootstrap-history.js +112 -0
- package/dist/cli-bootstrap-history.js.map +1 -0
- package/dist/cli-models.d.ts +8 -0
- package/dist/cli-models.d.ts.map +1 -0
- package/dist/cli-models.js +91 -0
- package/dist/cli-models.js.map +1 -0
- package/dist/cli-session-epoch.d.ts +13 -3
- package/dist/cli-session-epoch.d.ts.map +1 -1
- package/dist/cli-session-epoch.js +53 -4
- package/dist/cli-session-epoch.js.map +1 -1
- package/dist/cli-session-registry.d.ts +35 -0
- package/dist/cli-session-registry.d.ts.map +1 -0
- package/dist/cli-session-registry.js +177 -0
- package/dist/cli-session-registry.js.map +1 -0
- package/dist/cli.js +62 -0
- package/dist/cli.js.map +1 -1
- package/dist/codex-app-server-manager.d.ts +189 -0
- package/dist/codex-app-server-manager.d.ts.map +1 -0
- package/dist/codex-app-server-manager.js +1468 -0
- package/dist/codex-app-server-manager.js.map +1 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +8 -30
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/pool.d.ts +32 -0
- package/dist/commands/pool.d.ts.map +1 -1
- package/dist/commands/pool.js +145 -66
- package/dist/commands/pool.js.map +1 -1
- package/dist/commands/setup.d.ts +4 -1
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +9 -25
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/start.d.ts +21 -0
- package/dist/commands/start.d.ts.map +1 -1
- package/dist/commands/start.js +559 -63
- package/dist/commands/start.js.map +1 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +5 -2
- package/dist/commands/status.js.map +1 -1
- package/dist/completion-marker.d.ts +7 -0
- package/dist/completion-marker.d.ts.map +1 -0
- package/dist/completion-marker.js +28 -0
- package/dist/completion-marker.js.map +1 -0
- package/dist/config.d.ts +7 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +184 -60
- package/dist/config.js.map +1 -1
- package/dist/context-window.d.ts +37 -1
- package/dist/context-window.d.ts.map +1 -1
- package/dist/context-window.js +210 -17
- package/dist/context-window.js.map +1 -1
- package/dist/live-activity.d.ts +31 -0
- package/dist/live-activity.d.ts.map +1 -0
- package/dist/live-activity.js +36 -0
- package/dist/live-activity.js.map +1 -0
- package/dist/local-chat-execution.d.ts +114 -0
- package/dist/local-chat-execution.d.ts.map +1 -0
- package/dist/local-chat-execution.js +349 -0
- package/dist/local-chat-execution.js.map +1 -0
- package/dist/local-cli-pty-manager.d.ts +186 -0
- package/dist/local-cli-pty-manager.d.ts.map +1 -1
- package/dist/local-cli-pty-manager.js +2581 -164
- package/dist/local-cli-pty-manager.js.map +1 -1
- package/dist/local-conversation-gateway.d.ts +110 -0
- package/dist/local-conversation-gateway.d.ts.map +1 -0
- package/dist/local-conversation-gateway.js +175 -0
- package/dist/local-conversation-gateway.js.map +1 -0
- package/dist/local-data.d.ts +276 -5
- package/dist/local-data.d.ts.map +1 -1
- package/dist/local-data.js +1201 -86
- package/dist/local-data.js.map +1 -1
- package/dist/local-db.d.ts +6 -0
- package/dist/local-db.d.ts.map +1 -1
- package/dist/local-db.js +428 -2
- package/dist/local-db.js.map +1 -1
- package/dist/local-funnel.d.ts.map +1 -1
- package/dist/local-funnel.js +6 -5
- package/dist/local-funnel.js.map +1 -1
- package/dist/local-server.d.ts +55 -0
- package/dist/local-server.d.ts.map +1 -1
- package/dist/local-server.js +3281 -441
- package/dist/local-server.js.map +1 -1
- package/dist/managed-process-registry.d.ts +59 -0
- package/dist/managed-process-registry.d.ts.map +1 -0
- package/dist/managed-process-registry.js +390 -0
- package/dist/managed-process-registry.js.map +1 -0
- package/dist/mcp/claude-config-writer.d.ts +5 -5
- package/dist/mcp/claude-config-writer.d.ts.map +1 -1
- package/dist/mcp/claude-config-writer.js +19 -11
- package/dist/mcp/claude-config-writer.js.map +1 -1
- package/dist/mcp/index.d.ts +4 -2
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/sync-cli-config.d.ts +42 -4
- package/dist/mcp/sync-cli-config.d.ts.map +1 -1
- package/dist/mcp/sync-cli-config.js +497 -17
- package/dist/mcp/sync-cli-config.js.map +1 -1
- package/dist/message-loop.d.ts +6 -0
- package/dist/message-loop.d.ts.map +1 -1
- package/dist/message-loop.js +281 -89
- package/dist/message-loop.js.map +1 -1
- package/dist/mqtt-client.d.ts +44 -1
- package/dist/mqtt-client.d.ts.map +1 -1
- package/dist/mqtt-client.js +284 -46
- package/dist/mqtt-client.js.map +1 -1
- package/dist/mqtt-data-relay.d.ts +44 -0
- package/dist/mqtt-data-relay.d.ts.map +1 -0
- package/dist/mqtt-data-relay.js +106 -0
- package/dist/mqtt-data-relay.js.map +1 -0
- package/dist/oauth.d.ts.map +1 -1
- package/dist/oauth.js +69 -29
- package/dist/oauth.js.map +1 -1
- package/dist/orchestration/capabilities.d.ts +13 -0
- package/dist/orchestration/capabilities.d.ts.map +1 -0
- package/dist/orchestration/capabilities.js +152 -0
- package/dist/orchestration/capabilities.js.map +1 -0
- package/dist/orchestration/dispatch-executor.d.ts +83 -0
- package/dist/orchestration/dispatch-executor.d.ts.map +1 -0
- package/dist/orchestration/dispatch-executor.js +266 -0
- package/dist/orchestration/dispatch-executor.js.map +1 -0
- package/dist/orchestration/dispatch-hint.d.ts +134 -0
- package/dist/orchestration/dispatch-hint.d.ts.map +1 -0
- package/dist/orchestration/dispatch-hint.js +247 -0
- package/dist/orchestration/dispatch-hint.js.map +1 -0
- package/dist/orchestration/dispatch-runner.d.ts +106 -0
- package/dist/orchestration/dispatch-runner.d.ts.map +1 -0
- package/dist/orchestration/dispatch-runner.js +604 -0
- package/dist/orchestration/dispatch-runner.js.map +1 -0
- package/dist/orchestration/dispatch-tools.d.ts +167 -0
- package/dist/orchestration/dispatch-tools.d.ts.map +1 -0
- package/dist/orchestration/dispatch-tools.js +328 -0
- package/dist/orchestration/dispatch-tools.js.map +1 -0
- package/dist/orchestration/front-door-policy.d.ts +35 -10
- package/dist/orchestration/front-door-policy.d.ts.map +1 -1
- package/dist/orchestration/front-door-policy.js +30 -267
- package/dist/orchestration/front-door-policy.js.map +1 -1
- package/dist/orchestration/orchestrator-dispatch-prompt.d.ts +43 -0
- package/dist/orchestration/orchestrator-dispatch-prompt.d.ts.map +1 -0
- package/dist/orchestration/orchestrator-dispatch-prompt.js +267 -0
- package/dist/orchestration/orchestrator-dispatch-prompt.js.map +1 -0
- package/dist/orchestration/orchestrator-operating-prompt.d.ts +15 -0
- package/dist/orchestration/orchestrator-operating-prompt.d.ts.map +1 -1
- package/dist/orchestration/orchestrator-operating-prompt.js +206 -20
- package/dist/orchestration/orchestrator-operating-prompt.js.map +1 -1
- package/dist/orchestration/plan-import.d.ts +39 -0
- package/dist/orchestration/plan-import.d.ts.map +1 -0
- package/dist/orchestration/plan-import.js +547 -0
- package/dist/orchestration/plan-import.js.map +1 -0
- package/dist/orchestration/validation.d.ts +40 -0
- package/dist/orchestration/validation.d.ts.map +1 -0
- package/dist/orchestration/validation.js +203 -0
- package/dist/orchestration/validation.js.map +1 -0
- package/dist/orchestration/worker-operating-prompt.d.ts +2 -0
- package/dist/orchestration/worker-operating-prompt.d.ts.map +1 -1
- package/dist/orchestration/worker-operating-prompt.js +36 -46
- package/dist/orchestration/worker-operating-prompt.js.map +1 -1
- package/dist/orchestrator.d.ts +214 -33
- package/dist/orchestrator.d.ts.map +1 -1
- package/dist/orchestrator.js +2200 -1100
- package/dist/orchestrator.js.map +1 -1
- package/dist/providers/anthropic.d.ts.map +1 -1
- package/dist/providers/anthropic.js +8 -4
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/claude-cli-prompt.d.ts.map +1 -1
- package/dist/providers/claude-cli-prompt.js +49 -5
- package/dist/providers/claude-cli-prompt.js.map +1 -1
- package/dist/providers/claude-cli.d.ts.map +1 -1
- package/dist/providers/claude-cli.js +81 -5
- package/dist/providers/claude-cli.js.map +1 -1
- package/dist/providers/codex-cli.d.ts +10 -6
- package/dist/providers/codex-cli.d.ts.map +1 -1
- package/dist/providers/codex-cli.js +204 -26
- package/dist/providers/codex-cli.js.map +1 -1
- package/dist/providers/google.d.ts.map +1 -1
- package/dist/providers/google.js +15 -5
- package/dist/providers/google.js.map +1 -1
- package/dist/providers/index.d.ts +15 -1
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/openai.d.ts +1 -1
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +13 -5
- package/dist/providers/openai.js.map +1 -1
- package/dist/response-guard.js +1 -1
- package/dist/response-guard.js.map +1 -1
- package/dist/server-adapter.d.ts +8 -0
- package/dist/server-adapter.d.ts.map +1 -1
- package/dist/server-adapter.js +7 -0
- package/dist/server-adapter.js.map +1 -1
- package/dist/service-mode.d.ts +1 -1
- package/dist/service-mode.d.ts.map +1 -1
- package/dist/service-mode.js +64 -1
- package/dist/service-mode.js.map +1 -1
- package/dist/service-setup-only.d.ts +8 -0
- package/dist/service-setup-only.d.ts.map +1 -0
- package/dist/service-setup-only.js +37 -0
- package/dist/service-setup-only.js.map +1 -0
- package/dist/slash-commands.d.ts +21 -0
- package/dist/slash-commands.d.ts.map +1 -0
- package/dist/slash-commands.js +99 -0
- package/dist/slash-commands.js.map +1 -0
- package/dist/subagent/index.d.ts +4 -2
- package/dist/subagent/index.d.ts.map +1 -1
- package/dist/subagent/index.js.map +1 -1
- package/dist/summarization-pipeline.d.ts.map +1 -1
- package/dist/summarization-pipeline.js +1 -9
- package/dist/summarization-pipeline.js.map +1 -1
- package/dist/token-counter.d.ts.map +1 -1
- package/dist/token-counter.js +11 -4
- package/dist/token-counter.js.map +1 -1
- package/dist/tool-filter.d.ts.map +1 -1
- package/dist/tool-filter.js +10 -6
- package/dist/tool-filter.js.map +1 -1
- package/dist/tools/admin-tools.d.ts.map +1 -1
- package/dist/tools/admin-tools.js +20 -5
- package/dist/tools/admin-tools.js.map +1 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +2 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/run-command.d.ts.map +1 -1
- package/dist/tools/run-command.js +5 -1
- package/dist/tools/run-command.js.map +1 -1
- package/dist/tools/search-conversation-history.d.ts +16 -0
- package/dist/tools/search-conversation-history.d.ts.map +1 -0
- package/dist/tools/search-conversation-history.js +334 -0
- package/dist/tools/search-conversation-history.js.map +1 -0
- package/dist/tools/todo-tasks.d.ts.map +1 -1
- package/dist/tools/todo-tasks.js +77 -5
- package/dist/tools/todo-tasks.js.map +1 -1
- package/dist/usage-log.d.ts +62 -0
- package/dist/usage-log.d.ts.map +1 -0
- package/dist/usage-log.js +98 -0
- package/dist/usage-log.js.map +1 -0
- package/dist/wizard-state.d.ts +20 -0
- package/dist/wizard-state.d.ts.map +1 -1
- package/dist/wizard-state.js +90 -3
- package/dist/wizard-state.js.map +1 -1
- package/dist/wizard-support.d.ts.map +1 -1
- package/dist/wizard-support.js +27 -1
- package/dist/wizard-support.js.map +1 -1
- package/dist/workflow-engine.d.ts +44 -2
- package/dist/workflow-engine.d.ts.map +1 -1
- package/dist/workflow-engine.js +932 -111
- package/dist/workflow-engine.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,1468 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.__codexAppServerTestUtils = exports.CodexAppServerManager = void 0;
|
|
4
|
+
exports.getCodexAppServerManager = getCodexAppServerManager;
|
|
5
|
+
exports.runCodexAppServerTurnForTest = runCodexAppServerTurnForTest;
|
|
6
|
+
const child_process_1 = require("child_process");
|
|
7
|
+
const crypto_1 = require("crypto");
|
|
8
|
+
const runtime_context_1 = require("./runtime-context");
|
|
9
|
+
const sync_cli_config_1 = require("./mcp/sync-cli-config");
|
|
10
|
+
const managed_process_registry_1 = require("./managed-process-registry");
|
|
11
|
+
const LOCAL_ONLY_ERROR = 'Codex app-server manager is local_desktop only';
|
|
12
|
+
const CODEX_APP_SERVER_WARM_TIMEOUT_MS = 10_000;
|
|
13
|
+
const DEFAULT_CODEX_SETTINGS = {
|
|
14
|
+
reasoningEffort: 'low',
|
|
15
|
+
reasoningSummary: 'detailed',
|
|
16
|
+
personality: 'friendly',
|
|
17
|
+
serviceTier: 'fast',
|
|
18
|
+
sandboxPolicy: 'danger-full-access',
|
|
19
|
+
approvalPolicy: 'never',
|
|
20
|
+
};
|
|
21
|
+
const VALID_CODEX_REASONING_EFFORTS = new Set(['low', 'medium', 'high', 'xhigh']);
|
|
22
|
+
const VALID_CODEX_REASONING_SUMMARIES = new Set(['auto', 'concise', 'detailed', 'none']);
|
|
23
|
+
const VALID_CODEX_PERSONALITIES = new Set(['friendly', 'pragmatic']);
|
|
24
|
+
const VALID_CODEX_SERVICE_TIERS = new Set(['fast', 'flex']);
|
|
25
|
+
const VALID_CODEX_SANDBOX_POLICIES = new Set(['read-only', 'workspace-write', 'danger-full-access']);
|
|
26
|
+
const VALID_CODEX_APPROVAL_POLICIES = new Set(['untrusted', 'on-failure', 'on-request', 'never']);
|
|
27
|
+
let _manager = null;
|
|
28
|
+
let _prepareCodexCliConfigChain = Promise.resolve();
|
|
29
|
+
function delay(ms) {
|
|
30
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
31
|
+
}
|
|
32
|
+
function buildAbortError() {
|
|
33
|
+
const err = new Error('Codex app-server turn aborted');
|
|
34
|
+
err.name = 'AbortError';
|
|
35
|
+
return err;
|
|
36
|
+
}
|
|
37
|
+
function throwIfNotLocalDesktop(runtimeMode) {
|
|
38
|
+
if (runtimeMode && !(0, runtime_context_1.isLocalDesktopRuntime)(runtimeMode)) {
|
|
39
|
+
throw new Error(LOCAL_ONLY_ERROR);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function sessionKey(conversationId, botId) {
|
|
43
|
+
return `${conversationId}::${botId}`;
|
|
44
|
+
}
|
|
45
|
+
function hasProcessExited(child) {
|
|
46
|
+
const exitCode = child?.exitCode;
|
|
47
|
+
const signalCode = child?.signalCode;
|
|
48
|
+
return ((exitCode !== null && exitCode !== undefined)
|
|
49
|
+
|| (signalCode !== null && signalCode !== undefined));
|
|
50
|
+
}
|
|
51
|
+
function collectProcessRestartReasons(session, cwd, requestedEnvFingerprint, expectedThreadId) {
|
|
52
|
+
const reasons = [];
|
|
53
|
+
if (session.cwd !== cwd)
|
|
54
|
+
reasons.push('cwd_mismatch');
|
|
55
|
+
if (session.closed || hasProcessExited(session.child))
|
|
56
|
+
reasons.push('process_unhealthy');
|
|
57
|
+
if (session.envFingerprint !== requestedEnvFingerprint)
|
|
58
|
+
reasons.push('env_mismatch');
|
|
59
|
+
if (expectedThreadId && session.threadId && session.threadId !== expectedThreadId) {
|
|
60
|
+
reasons.push('incompatible_thread_request');
|
|
61
|
+
}
|
|
62
|
+
return reasons;
|
|
63
|
+
}
|
|
64
|
+
function buildWarmTimeoutError(timeoutMs) {
|
|
65
|
+
const err = new Error(`codex app-server warm session timed out after ${Math.floor(timeoutMs / 1000)}s`);
|
|
66
|
+
err.code = 'CODEX_APP_SERVER_WARM_TIMEOUT';
|
|
67
|
+
err.name = 'CodexAppServerWarmTimeoutError';
|
|
68
|
+
return err;
|
|
69
|
+
}
|
|
70
|
+
function withWarmTimeout(promise, timeoutMs, onTimeout) {
|
|
71
|
+
let timeout = null;
|
|
72
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
73
|
+
timeout = setTimeout(() => {
|
|
74
|
+
try {
|
|
75
|
+
onTimeout();
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
reject(buildWarmTimeoutError(timeoutMs));
|
|
79
|
+
}
|
|
80
|
+
}, timeoutMs);
|
|
81
|
+
});
|
|
82
|
+
return Promise.race([promise, timeoutPromise]).finally(() => {
|
|
83
|
+
if (timeout)
|
|
84
|
+
clearTimeout(timeout);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
function createEventLog() {
|
|
88
|
+
return {
|
|
89
|
+
schema: 'funolio.codex-app-server.event-log@1',
|
|
90
|
+
events: [],
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function appendEvent(eventLog, direction, kind, payload) {
|
|
94
|
+
if (!eventLog)
|
|
95
|
+
return;
|
|
96
|
+
eventLog.events.push({
|
|
97
|
+
ts: new Date().toISOString(),
|
|
98
|
+
direction,
|
|
99
|
+
kind,
|
|
100
|
+
payload,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
function serializeEventLog(eventLog) {
|
|
104
|
+
return JSON.stringify(eventLog);
|
|
105
|
+
}
|
|
106
|
+
function normalizeDetail(text) {
|
|
107
|
+
return String(text || '').replace(/\s+/g, ' ').trim();
|
|
108
|
+
}
|
|
109
|
+
function isImportantLiveDetail(text) {
|
|
110
|
+
return /\b(ERROR|Error|failed|failure|Exit code:\s*[1-9]|authentication|rate limit|timeout|timed out|denied|unauthorized)\b/i.test(text);
|
|
111
|
+
}
|
|
112
|
+
function isGeneratedOrDependencyNoise(text) {
|
|
113
|
+
return (/\.map(?::|\b)/i.test(text)
|
|
114
|
+
|| /(?:^|[\\/])node_modules[\\/]/i.test(text)
|
|
115
|
+
|| /[\\/]?\.cargo[\\/]registry[\\/]/i.test(text)
|
|
116
|
+
|| /src-tauri[\\/]gen[\\/]schemas/i.test(text)
|
|
117
|
+
|| /\bpackage-lock\.json\b/i.test(text)
|
|
118
|
+
|| /\bCargo\.lock\b/i.test(text)
|
|
119
|
+
|| /\bdist[\\/]assets[\\/][^ ]+\.(?:js|css)\b/i.test(text));
|
|
120
|
+
}
|
|
121
|
+
function looksLikeMinifiedBlob(text) {
|
|
122
|
+
if (text.length < 1000)
|
|
123
|
+
return false;
|
|
124
|
+
const whitespaceCount = (text.match(/\s/g) || []).length;
|
|
125
|
+
const punctuationCount = (text.match(/[{}()[\],;:=]/g) || []).length;
|
|
126
|
+
return whitespaceCount / text.length < 0.08 && punctuationCount > 80;
|
|
127
|
+
}
|
|
128
|
+
function looksLikeRawSourceLine(text) {
|
|
129
|
+
return /^\s*(const|let|function|import|export|type|interface|class|return|if |for |while |case |await |async |try |catch|}\)|}}|\]|\)|\{)\b/.test(text);
|
|
130
|
+
}
|
|
131
|
+
function looksLikeSearchOutput(text) {
|
|
132
|
+
return /^(?:src|packages|prisma|scripts|public|app|C:\\|\.github)[\\/].*:\d+/.test(text)
|
|
133
|
+
|| /^\s*[\w.\-/\\]+:\d+/.test(text);
|
|
134
|
+
}
|
|
135
|
+
function truncateLiveDetail(text, maxLength) {
|
|
136
|
+
if (text.length <= maxLength)
|
|
137
|
+
return text;
|
|
138
|
+
return `${text.slice(0, maxLength)} ...[truncated raw output]`;
|
|
139
|
+
}
|
|
140
|
+
function prepareLiveDetailForUser(activeTurn, detail, source) {
|
|
141
|
+
const normalized = normalizeDetail(detail);
|
|
142
|
+
if (!normalized)
|
|
143
|
+
return null;
|
|
144
|
+
if (source === 'status') {
|
|
145
|
+
return truncateLiveDetail(normalized, 600);
|
|
146
|
+
}
|
|
147
|
+
if (isImportantLiveDetail(normalized)) {
|
|
148
|
+
return truncateLiveDetail(normalized, 900);
|
|
149
|
+
}
|
|
150
|
+
if (isGeneratedOrDependencyNoise(normalized) || looksLikeMinifiedBlob(normalized)) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
if (looksLikeSearchOutput(normalized)) {
|
|
154
|
+
const count = activeTurn.liveDetailCategoryCounts.get('search_output') || 0;
|
|
155
|
+
activeTurn.liveDetailCategoryCounts.set('search_output', count + 1);
|
|
156
|
+
if (count >= 25)
|
|
157
|
+
return null;
|
|
158
|
+
return truncateLiveDetail(normalized, 500);
|
|
159
|
+
}
|
|
160
|
+
if (normalized.length > 1000) {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
if ((source === 'command_output' || source === 'file_output') && looksLikeRawSourceLine(normalized)) {
|
|
164
|
+
const count = activeTurn.liveDetailCategoryCounts.get('raw_source_line') || 0;
|
|
165
|
+
activeTurn.liveDetailCategoryCounts.set('raw_source_line', count + 1);
|
|
166
|
+
if (count >= 40)
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
return truncateLiveDetail(normalized, 600);
|
|
170
|
+
}
|
|
171
|
+
function scrubCodexAppServerEnv(baseEnv, extraEnv, sessionKey) {
|
|
172
|
+
const env = {};
|
|
173
|
+
for (const [key, value] of Object.entries(baseEnv)) {
|
|
174
|
+
if (value == null)
|
|
175
|
+
continue;
|
|
176
|
+
if (key === 'CODEX_THREAD_ID')
|
|
177
|
+
continue;
|
|
178
|
+
if (key === 'FUNOLIO_TOOL_TODO_ID')
|
|
179
|
+
continue;
|
|
180
|
+
env[key] = value;
|
|
181
|
+
}
|
|
182
|
+
for (const [key, value] of Object.entries(extraEnv)) {
|
|
183
|
+
if (!value)
|
|
184
|
+
continue;
|
|
185
|
+
env[key] = value;
|
|
186
|
+
}
|
|
187
|
+
if (sessionKey) {
|
|
188
|
+
const marker = (0, managed_process_registry_1.getMarkerEnv)(sessionKey);
|
|
189
|
+
for (const [k, v] of Object.entries(marker)) {
|
|
190
|
+
env[k] = v;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return env;
|
|
194
|
+
}
|
|
195
|
+
const VOLATILE_ENV_KEYS = new Set(['FUNOLIO_TOOL_TODO_ID', 'FUNOLIO_MANAGED_SESSION']);
|
|
196
|
+
function computeEnvFingerprint(env) {
|
|
197
|
+
return JSON.stringify(Object.entries(env)
|
|
198
|
+
.filter(([key, value]) => typeof value === 'string' && !VOLATILE_ENV_KEYS.has(key))
|
|
199
|
+
.sort(([a], [b]) => a.localeCompare(b)));
|
|
200
|
+
}
|
|
201
|
+
function buildCodexAppServerToolEnv(input) {
|
|
202
|
+
const env = {};
|
|
203
|
+
const actorId = input.botName?.trim() || input.botId.trim();
|
|
204
|
+
if (actorId)
|
|
205
|
+
env.FUNOLIO_TOOL_ACTOR_ID = actorId;
|
|
206
|
+
if (input.projectId !== undefined && input.projectId !== null)
|
|
207
|
+
env.FUNOLIO_TOOL_PROJECT_ID = String(input.projectId);
|
|
208
|
+
return env;
|
|
209
|
+
}
|
|
210
|
+
function buildTurnMcpToolEnv(sessionEnv, currentTodoTaskId) {
|
|
211
|
+
const toolEnv = {};
|
|
212
|
+
for (const k of ['FUNOLIO_TOOL_ACTOR_ID', 'FUNOLIO_TOOL_PROJECT_ID']) {
|
|
213
|
+
if (sessionEnv[k])
|
|
214
|
+
toolEnv[k] = sessionEnv[k];
|
|
215
|
+
}
|
|
216
|
+
if (currentTodoTaskId !== undefined && currentTodoTaskId !== null) {
|
|
217
|
+
toolEnv.FUNOLIO_TOOL_TODO_ID = String(currentTodoTaskId);
|
|
218
|
+
}
|
|
219
|
+
return toolEnv;
|
|
220
|
+
}
|
|
221
|
+
function extractLatestUserText(messages) {
|
|
222
|
+
const renderContent = (content) => (typeof content === 'string'
|
|
223
|
+
? content
|
|
224
|
+
: content.map((part) => (part.type === 'text'
|
|
225
|
+
? part.text
|
|
226
|
+
: `[image:${part.mimeType};${Math.ceil((part.data?.length || 0) * 0.75)} bytes]`)).join('\n'));
|
|
227
|
+
const lastMessage = messages.length > 0 ? messages[messages.length - 1] : null;
|
|
228
|
+
if (lastMessage?.role === 'user') {
|
|
229
|
+
return renderContent(lastMessage.content);
|
|
230
|
+
}
|
|
231
|
+
if (messages.length === 0)
|
|
232
|
+
return '';
|
|
233
|
+
const rendered = messages.map((message) => {
|
|
234
|
+
const label = message.role === 'user' ? 'USER'
|
|
235
|
+
: message.role === 'assistant' ? 'ASSISTANT'
|
|
236
|
+
: message.role === 'tool' ? `TOOL (${message.toolName || 'tool'})`
|
|
237
|
+
: String(message.role || 'MESSAGE').toUpperCase();
|
|
238
|
+
return `${label}:\n${renderContent(message.content)}`;
|
|
239
|
+
});
|
|
240
|
+
return rendered.join('\n\n');
|
|
241
|
+
}
|
|
242
|
+
function buildCodexAppServerInput(messages) {
|
|
243
|
+
const lastMessage = messages.length > 0 ? messages[messages.length - 1] : null;
|
|
244
|
+
const content = lastMessage?.role === 'user' ? lastMessage.content : null;
|
|
245
|
+
// If the last user message has multimodal content, build image + text blocks
|
|
246
|
+
if (content && typeof content !== 'string') {
|
|
247
|
+
const blocks = [];
|
|
248
|
+
for (const part of content) {
|
|
249
|
+
if (part.type === 'text') {
|
|
250
|
+
blocks.push({ type: 'text', text: part.text });
|
|
251
|
+
}
|
|
252
|
+
else if (part.type === 'image' && part.data) {
|
|
253
|
+
blocks.push({
|
|
254
|
+
type: 'image',
|
|
255
|
+
url: `data:${part.mimeType || 'image/png'};base64,${part.data}`,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (blocks.length > 0)
|
|
260
|
+
return blocks;
|
|
261
|
+
}
|
|
262
|
+
// Fallback: text-only
|
|
263
|
+
return [{ type: 'text', text: extractLatestUserText(messages) }];
|
|
264
|
+
}
|
|
265
|
+
function buildTurnUsage(tokenUsage) {
|
|
266
|
+
const last = tokenUsage?.last;
|
|
267
|
+
if (!last || typeof last !== 'object')
|
|
268
|
+
return undefined;
|
|
269
|
+
// token.txt Change 1: keep inputTokens sum + add breakdown.
|
|
270
|
+
const fresh = Number(last.inputTokens || 0);
|
|
271
|
+
const cacheRead = Number(last.cachedInputTokens || 0);
|
|
272
|
+
const inputTokens = fresh + cacheRead;
|
|
273
|
+
const outputTokens = Number(last.outputTokens || 0) + Number(last.reasoningOutputTokens || 0);
|
|
274
|
+
if (!Number.isFinite(inputTokens) || !Number.isFinite(outputTokens))
|
|
275
|
+
return undefined;
|
|
276
|
+
return {
|
|
277
|
+
inputTokens,
|
|
278
|
+
inputTokensFresh: fresh,
|
|
279
|
+
inputTokensCacheCreation: 0,
|
|
280
|
+
inputTokensCacheRead: cacheRead,
|
|
281
|
+
outputTokens,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
function extractThreadId(payload) {
|
|
285
|
+
const id = payload?.thread?.id ?? payload?.threadId ?? null;
|
|
286
|
+
return typeof id === 'string' && id.trim() ? id.trim() : null;
|
|
287
|
+
}
|
|
288
|
+
function extractTurnId(payload) {
|
|
289
|
+
const id = payload?.turn?.id ?? payload?.turnId ?? null;
|
|
290
|
+
return typeof id === 'string' && id.trim() ? id.trim() : null;
|
|
291
|
+
}
|
|
292
|
+
function extractTurnStatus(payload) {
|
|
293
|
+
const status = payload?.turn?.status ?? null;
|
|
294
|
+
return typeof status === 'string' ? status : null;
|
|
295
|
+
}
|
|
296
|
+
function extractTurnFailureMessage(payload) {
|
|
297
|
+
const message = payload?.turn?.error?.message ?? payload?.error?.message ?? null;
|
|
298
|
+
return typeof message === 'string' && message.trim() ? message.trim() : null;
|
|
299
|
+
}
|
|
300
|
+
function isResponseMessage(value) {
|
|
301
|
+
return !!value && typeof value === 'object' && value.id !== undefined && ('result' in value || 'error' in value);
|
|
302
|
+
}
|
|
303
|
+
function isServerRequestMessage(value) {
|
|
304
|
+
return !!value && typeof value === 'object' && value.id !== undefined && typeof value.method === 'string';
|
|
305
|
+
}
|
|
306
|
+
function isNotificationMessage(value) {
|
|
307
|
+
return !!value && typeof value === 'object' && value.id === undefined && typeof value.method === 'string';
|
|
308
|
+
}
|
|
309
|
+
function isTurnMatch(activeTurn, params) {
|
|
310
|
+
if (!activeTurn)
|
|
311
|
+
return false;
|
|
312
|
+
if (activeTurn.threadId && params?.threadId && activeTurn.threadId !== params.threadId)
|
|
313
|
+
return false;
|
|
314
|
+
if (activeTurn.turnId && params?.turnId && activeTurn.turnId !== params.turnId)
|
|
315
|
+
return false;
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
318
|
+
function safeJsonParse(line) {
|
|
319
|
+
try {
|
|
320
|
+
return JSON.parse(line);
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
function describeStartupStatus(params) {
|
|
327
|
+
const name = typeof params?.name === 'string' ? params.name : 'MCP server';
|
|
328
|
+
const status = typeof params?.status === 'string' ? params.status : 'unknown';
|
|
329
|
+
const error = typeof params?.error === 'string' && params.error.trim() ? ` (${params.error.trim()})` : '';
|
|
330
|
+
return `${name}: ${status}${error}`;
|
|
331
|
+
}
|
|
332
|
+
function extractItemId(params) {
|
|
333
|
+
const id = params?.item?.id ?? params?.itemId ?? null;
|
|
334
|
+
return typeof id === 'string' && id.trim() ? id.trim() : null;
|
|
335
|
+
}
|
|
336
|
+
function extractAgentMessagePhase(params) {
|
|
337
|
+
if (params?.item?.type !== 'agentMessage')
|
|
338
|
+
return null;
|
|
339
|
+
const phase = params?.item?.phase ?? null;
|
|
340
|
+
return typeof phase === 'string' && phase.trim() ? phase.trim().toLowerCase() : null;
|
|
341
|
+
}
|
|
342
|
+
function describeItemProgress(method, params) {
|
|
343
|
+
if (!params?.item || typeof params.item !== 'object')
|
|
344
|
+
return null;
|
|
345
|
+
const itemType = typeof params.item.type === 'string' ? params.item.type : 'item';
|
|
346
|
+
const label = typeof params.item.name === 'string' ? params.item.name
|
|
347
|
+
: typeof params.item.title === 'string' ? params.item.title
|
|
348
|
+
: typeof params.item.serverName === 'string' ? params.item.serverName
|
|
349
|
+
: typeof params.item.toolName === 'string' ? params.item.toolName
|
|
350
|
+
: '';
|
|
351
|
+
const status = typeof params.item.status === 'string' ? ` (${params.item.status})`
|
|
352
|
+
: '';
|
|
353
|
+
const prefix = method === 'item/started' ? 'Started' : 'Completed';
|
|
354
|
+
return `${prefix} ${itemType}${label ? `: ${label}` : ''}${status}`;
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Build a structured tool event from a Codex app-server item/started or
|
|
358
|
+
* item/completed notification, when the item type represents a tool-like
|
|
359
|
+
* operation. Returns null for non-tool items (e.g. agent_message, reasoning).
|
|
360
|
+
*
|
|
361
|
+
* Recognized tool item types:
|
|
362
|
+
* - command_execution → shell command (PTY exec)
|
|
363
|
+
* - file_change → file edit / write / delete
|
|
364
|
+
* - mcpToolCall → MCP tool invocation
|
|
365
|
+
* - applyPatch → patch apply
|
|
366
|
+
*/
|
|
367
|
+
function buildToolEventFromItem(kind, params) {
|
|
368
|
+
const item = params?.item;
|
|
369
|
+
if (!item || typeof item !== 'object')
|
|
370
|
+
return null;
|
|
371
|
+
const itemType = typeof item.type === 'string' ? item.type : '';
|
|
372
|
+
const toolItemTypes = new Set([
|
|
373
|
+
'command_execution',
|
|
374
|
+
'file_change',
|
|
375
|
+
'mcpToolCall',
|
|
376
|
+
'mcp_tool_call',
|
|
377
|
+
'applyPatch',
|
|
378
|
+
'apply_patch',
|
|
379
|
+
]);
|
|
380
|
+
if (!toolItemTypes.has(itemType))
|
|
381
|
+
return null;
|
|
382
|
+
const toolName = (typeof item.name === 'string' && item.name)
|
|
383
|
+
|| (typeof item.toolName === 'string' && item.toolName)
|
|
384
|
+
|| (typeof item.serverName === 'string' && item.serverName)
|
|
385
|
+
|| itemType;
|
|
386
|
+
const toolCallId = (typeof item.id === 'string' && item.id)
|
|
387
|
+
|| (typeof item.itemId === 'string' && item.itemId)
|
|
388
|
+
|| `codex-${itemType}-${Date.now()}`;
|
|
389
|
+
const event = {
|
|
390
|
+
kind,
|
|
391
|
+
toolName,
|
|
392
|
+
toolCallId,
|
|
393
|
+
};
|
|
394
|
+
if (kind === 'call') {
|
|
395
|
+
if (item.command || item.input || item.arguments || item.path || item.changes) {
|
|
396
|
+
event.arguments = {
|
|
397
|
+
...(item.command ? { command: item.command } : {}),
|
|
398
|
+
...(item.input ? { input: item.input } : {}),
|
|
399
|
+
...(item.arguments ? item.arguments : {}),
|
|
400
|
+
...(item.path ? { path: item.path } : {}),
|
|
401
|
+
...(item.changes ? { changes: item.changes } : {}),
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
else {
|
|
406
|
+
// result
|
|
407
|
+
const output = (typeof item.output === 'string' && item.output)
|
|
408
|
+
|| (typeof item.stdout === 'string' && item.stdout)
|
|
409
|
+
|| (typeof item.result === 'string' && item.result)
|
|
410
|
+
|| '';
|
|
411
|
+
if (output)
|
|
412
|
+
event.output = output;
|
|
413
|
+
const status = typeof item.status === 'string' ? item.status.toLowerCase() : '';
|
|
414
|
+
event.isError = status === 'failed' || status === 'error' || !!item.error;
|
|
415
|
+
}
|
|
416
|
+
return event;
|
|
417
|
+
}
|
|
418
|
+
function normalizeCodexSettings(settings) {
|
|
419
|
+
const reasoningEffort = String(settings?.reasoningEffort || '').trim().toLowerCase();
|
|
420
|
+
const reasoningSummary = String(settings?.reasoningSummary || '').trim().toLowerCase();
|
|
421
|
+
const personality = String(settings?.personality || '').trim().toLowerCase();
|
|
422
|
+
const serviceTier = String(settings?.serviceTier || '').trim().toLowerCase();
|
|
423
|
+
const sandboxPolicy = String(settings?.sandboxPolicy || '').trim().toLowerCase();
|
|
424
|
+
const approvalPolicy = String(settings?.approvalPolicy || '').trim().toLowerCase();
|
|
425
|
+
return {
|
|
426
|
+
reasoningEffort: VALID_CODEX_REASONING_EFFORTS.has(reasoningEffort)
|
|
427
|
+
? reasoningEffort
|
|
428
|
+
: DEFAULT_CODEX_SETTINGS.reasoningEffort,
|
|
429
|
+
reasoningSummary: VALID_CODEX_REASONING_SUMMARIES.has(reasoningSummary)
|
|
430
|
+
? reasoningSummary
|
|
431
|
+
: DEFAULT_CODEX_SETTINGS.reasoningSummary,
|
|
432
|
+
personality: VALID_CODEX_PERSONALITIES.has(personality)
|
|
433
|
+
? personality
|
|
434
|
+
: DEFAULT_CODEX_SETTINGS.personality,
|
|
435
|
+
serviceTier: VALID_CODEX_SERVICE_TIERS.has(serviceTier)
|
|
436
|
+
? serviceTier
|
|
437
|
+
: DEFAULT_CODEX_SETTINGS.serviceTier,
|
|
438
|
+
sandboxPolicy: VALID_CODEX_SANDBOX_POLICIES.has(sandboxPolicy)
|
|
439
|
+
? sandboxPolicy
|
|
440
|
+
: DEFAULT_CODEX_SETTINGS.sandboxPolicy,
|
|
441
|
+
approvalPolicy: VALID_CODEX_APPROVAL_POLICIES.has(approvalPolicy)
|
|
442
|
+
? approvalPolicy
|
|
443
|
+
: DEFAULT_CODEX_SETTINGS.approvalPolicy,
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
function buildThreadSandbox(sandboxPolicy) {
|
|
447
|
+
switch (sandboxPolicy) {
|
|
448
|
+
case 'read-only':
|
|
449
|
+
return 'read-only';
|
|
450
|
+
case 'workspace-write':
|
|
451
|
+
return 'workspace-write';
|
|
452
|
+
default:
|
|
453
|
+
return 'danger-full-access';
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
function buildTurnSandboxPolicy(sandboxPolicy) {
|
|
457
|
+
switch (sandboxPolicy) {
|
|
458
|
+
case 'read-only':
|
|
459
|
+
return { type: 'readOnly' };
|
|
460
|
+
case 'workspace-write':
|
|
461
|
+
return { type: 'workspaceWrite' };
|
|
462
|
+
default:
|
|
463
|
+
return { type: 'dangerFullAccess' };
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* SHA-256 of the params that, together, define a unique Codex thread.
|
|
468
|
+
* If any of these inputs differ between warm-up and Send, the warmed
|
|
469
|
+
* thread is stale and must be invalidated so a fresh one is created
|
|
470
|
+
* with the current settings. systemPrompt is hashed rather than stored
|
|
471
|
+
* directly to avoid keeping a second copy of a potentially large string.
|
|
472
|
+
*/
|
|
473
|
+
function computeThreadBootstrapFingerprint(opts) {
|
|
474
|
+
const settings = normalizeCodexSettings(opts.codexSettings);
|
|
475
|
+
const parts = [
|
|
476
|
+
opts.systemPrompt || '',
|
|
477
|
+
opts.model || '',
|
|
478
|
+
settings.approvalPolicy,
|
|
479
|
+
settings.sandboxPolicy,
|
|
480
|
+
settings.personality,
|
|
481
|
+
settings.reasoningEffort,
|
|
482
|
+
settings.serviceTier,
|
|
483
|
+
opts.resumeSessionId || '',
|
|
484
|
+
];
|
|
485
|
+
return (0, crypto_1.createHash)('sha256').update(parts.join('\u0000')).digest('hex');
|
|
486
|
+
}
|
|
487
|
+
function buildApprovalResponse(method, params) {
|
|
488
|
+
switch (method) {
|
|
489
|
+
case 'item/commandExecution/requestApproval':
|
|
490
|
+
return { decision: 'accept' };
|
|
491
|
+
case 'item/fileChange/requestApproval':
|
|
492
|
+
return { decision: 'accept' };
|
|
493
|
+
case 'item/permissions/requestApproval':
|
|
494
|
+
return { permissions: params?.permissions || {}, scope: 'session' };
|
|
495
|
+
case 'applyPatchApproval':
|
|
496
|
+
return { decision: 'approved' };
|
|
497
|
+
case 'execCommandApproval':
|
|
498
|
+
return { decision: 'approved' };
|
|
499
|
+
case 'item/tool/requestUserInput':
|
|
500
|
+
return { answers: {} };
|
|
501
|
+
case 'mcpServer/elicitation/request':
|
|
502
|
+
return { action: 'decline' };
|
|
503
|
+
default:
|
|
504
|
+
return null;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
async function defaultPrepareCliConfig() {
|
|
508
|
+
_prepareCodexCliConfigChain = _prepareCodexCliConfigChain.then(async () => {
|
|
509
|
+
await (0, sync_cli_config_1.syncMcpToCliConfig)('codex-cli');
|
|
510
|
+
});
|
|
511
|
+
return _prepareCodexCliConfigChain;
|
|
512
|
+
}
|
|
513
|
+
class CodexAppServerManager {
|
|
514
|
+
sessions = new Map();
|
|
515
|
+
now;
|
|
516
|
+
spawnProcess;
|
|
517
|
+
prepareCliConfig;
|
|
518
|
+
logInfo;
|
|
519
|
+
logWarn;
|
|
520
|
+
clientInfo;
|
|
521
|
+
constructor(deps = {}) {
|
|
522
|
+
this.now = deps.now || (() => Date.now());
|
|
523
|
+
this.spawnProcess = deps.spawnProcess || ((command, args, options) => (0, child_process_1.spawn)(command, args, options));
|
|
524
|
+
this.prepareCliConfig = deps.prepareCliConfig || defaultPrepareCliConfig;
|
|
525
|
+
this.logInfo = deps.logInfo || ((message) => console.log(message));
|
|
526
|
+
this.logWarn = deps.logWarn || ((message) => console.warn(message));
|
|
527
|
+
this.clientInfo = deps.clientInfo || { name: 'funolio-agent', version: '1.1.5' };
|
|
528
|
+
}
|
|
529
|
+
async warmSession(opts) {
|
|
530
|
+
throwIfNotLocalDesktop(opts.runtimeMode);
|
|
531
|
+
const key = sessionKey(opts.conversationId, opts.botId);
|
|
532
|
+
const requestedEnv = scrubCodexAppServerEnv(process.env, buildCodexAppServerToolEnv({
|
|
533
|
+
botId: opts.botId,
|
|
534
|
+
botName: opts.botName,
|
|
535
|
+
projectId: opts.projectId,
|
|
536
|
+
}), key);
|
|
537
|
+
const requestedEnvFingerprint = computeEnvFingerprint(requestedEnv);
|
|
538
|
+
const wantsThreadBootstrap = typeof opts.systemPrompt === 'string' && opts.systemPrompt.length > 0;
|
|
539
|
+
let hiddenPrimerCompleted = false;
|
|
540
|
+
const requestedBootstrapFingerprint = wantsThreadBootstrap
|
|
541
|
+
? computeThreadBootstrapFingerprint({
|
|
542
|
+
systemPrompt: opts.systemPrompt,
|
|
543
|
+
model: opts.model,
|
|
544
|
+
codexSettings: opts.codexSettings,
|
|
545
|
+
resumeSessionId: opts.resumeSessionId,
|
|
546
|
+
})
|
|
547
|
+
: null;
|
|
548
|
+
let session = this.sessions.get(key);
|
|
549
|
+
if (session
|
|
550
|
+
&& (session.cwd !== opts.cwd
|
|
551
|
+
|| session.closed
|
|
552
|
+
|| session.envFingerprint !== requestedEnvFingerprint)) {
|
|
553
|
+
this.closeSession(key);
|
|
554
|
+
session = undefined;
|
|
555
|
+
}
|
|
556
|
+
if (session
|
|
557
|
+
&& wantsThreadBootstrap
|
|
558
|
+
&& session.threadId
|
|
559
|
+
&& session.threadBootstrapFingerprint
|
|
560
|
+
&& session.threadBootstrapFingerprint !== requestedBootstrapFingerprint) {
|
|
561
|
+
session.threadId = null;
|
|
562
|
+
session.threadBootstrapFingerprint = null;
|
|
563
|
+
session.bootstrapPromise = null;
|
|
564
|
+
session.warmPromise = null;
|
|
565
|
+
session.warmRequestedAtMs = null;
|
|
566
|
+
session.warmReadyAtMs = null;
|
|
567
|
+
session.bootstrapGeneration++;
|
|
568
|
+
}
|
|
569
|
+
const reusedExistingSession = !!session;
|
|
570
|
+
if (!session) {
|
|
571
|
+
session = await this.createSession(key, opts.cwd, requestedEnv, opts.currentTodoTaskId);
|
|
572
|
+
this.sessions.set(key, session);
|
|
573
|
+
}
|
|
574
|
+
// Fast path: session exists, handshake already complete, AND either
|
|
575
|
+
// (a) the caller didn't ask for a thread bootstrap, or (b) the thread
|
|
576
|
+
// is already bootstrapped with the current fingerprint.
|
|
577
|
+
const bootstrapAlreadyCorrect = !wantsThreadBootstrap
|
|
578
|
+
|| (!!session.threadId && session.threadBootstrapFingerprint === requestedBootstrapFingerprint);
|
|
579
|
+
if (reusedExistingSession
|
|
580
|
+
&& !session.warmPromise
|
|
581
|
+
&& !session.warmReadyAtMs
|
|
582
|
+
&& session.readyResolved
|
|
583
|
+
&& bootstrapAlreadyCorrect) {
|
|
584
|
+
(0, managed_process_registry_1.markReused)(key);
|
|
585
|
+
return {
|
|
586
|
+
reusedExistingSession,
|
|
587
|
+
readyAgeMs: Math.max(0, this.now() - session.lastUsedAtMs),
|
|
588
|
+
sessionId: session.threadId || null,
|
|
589
|
+
hiddenPrimerCompleted: false,
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
if (!session.warmPromise && !session.warmReadyAtMs) {
|
|
593
|
+
session.warmRequestedAtMs = this.now();
|
|
594
|
+
session.warmPromise = withWarmTimeout((async () => {
|
|
595
|
+
await session.readyPromise;
|
|
596
|
+
if (session.closed) {
|
|
597
|
+
throw new Error('Codex app-server warm session closed before it was ready');
|
|
598
|
+
}
|
|
599
|
+
if (wantsThreadBootstrap && !session.threadId && !session.bootstrapPromise) {
|
|
600
|
+
session.bootstrapPromise = this.bootstrapWarmThread(session, opts, requestedBootstrapFingerprint);
|
|
601
|
+
const primerMessages = Array.isArray(opts.primerMessages) && opts.primerMessages.length > 0
|
|
602
|
+
? opts.primerMessages
|
|
603
|
+
: null;
|
|
604
|
+
if (primerMessages) {
|
|
605
|
+
session.bootstrapPromise = session.bootstrapPromise.then(async () => {
|
|
606
|
+
if (!session || session.closed || !session.threadId)
|
|
607
|
+
return;
|
|
608
|
+
try {
|
|
609
|
+
await this.runTurnInternal(session, {
|
|
610
|
+
runtimeMode: opts.runtimeMode,
|
|
611
|
+
conversationId: opts.conversationId,
|
|
612
|
+
botId: opts.botId,
|
|
613
|
+
botName: opts.botName,
|
|
614
|
+
cwd: opts.cwd,
|
|
615
|
+
systemPrompt: opts.systemPrompt || '',
|
|
616
|
+
messages: primerMessages,
|
|
617
|
+
model: opts.model || null,
|
|
618
|
+
projectId: opts.projectId,
|
|
619
|
+
currentTodoTaskId: opts.currentTodoTaskId,
|
|
620
|
+
codexSettings: {
|
|
621
|
+
...(opts.codexSettings || {}),
|
|
622
|
+
reasoningEffort: 'low',
|
|
623
|
+
reasoningSummary: 'none',
|
|
624
|
+
},
|
|
625
|
+
resumeSessionId: null,
|
|
626
|
+
}, {
|
|
627
|
+
skipBootstrapWait: true,
|
|
628
|
+
suppressSendPathLog: true,
|
|
629
|
+
});
|
|
630
|
+
hiddenPrimerCompleted = true;
|
|
631
|
+
this.logInfo(`[cli-warm] codex hidden primer completed threadId=${JSON.stringify(session.threadId)}`);
|
|
632
|
+
}
|
|
633
|
+
catch (err) {
|
|
634
|
+
this.logWarn(`[cli-warm] codex hidden primer failed: ${err?.message || String(err)}`);
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
if (session.bootstrapPromise) {
|
|
640
|
+
await session.bootstrapPromise;
|
|
641
|
+
session.bootstrapPromise = null;
|
|
642
|
+
}
|
|
643
|
+
session.warmReadyAtMs = this.now();
|
|
644
|
+
})(), opts.timeoutMs ?? CODEX_APP_SERVER_WARM_TIMEOUT_MS, () => this.closeSession(key)).catch((err) => {
|
|
645
|
+
if (this.sessions.get(key) === session) {
|
|
646
|
+
this.closeSession(key);
|
|
647
|
+
}
|
|
648
|
+
throw err;
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
await session.warmPromise;
|
|
652
|
+
return {
|
|
653
|
+
reusedExistingSession,
|
|
654
|
+
readyAgeMs: session.warmReadyAtMs ? Math.max(0, this.now() - session.warmReadyAtMs) : 0,
|
|
655
|
+
sessionId: session.threadId || null,
|
|
656
|
+
hiddenPrimerCompleted,
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Issues thread/start (or thread/resume if resumeSessionId is provided)
|
|
661
|
+
* during warm-up, populating session.threadId so runTurn can skip the
|
|
662
|
+
* call. Failure leaves the session with no thread id — runTurn will
|
|
663
|
+
* retry from scratch via its normal path.
|
|
664
|
+
*/
|
|
665
|
+
async bootstrapWarmThread(session, opts, fingerprint) {
|
|
666
|
+
const settings = normalizeCodexSettings(opts.codexSettings);
|
|
667
|
+
const gen = session.bootstrapGeneration;
|
|
668
|
+
const startMs = this.now();
|
|
669
|
+
const BOOTSTRAP_TIMEOUT_MS = 30_000;
|
|
670
|
+
try {
|
|
671
|
+
const rpcParams = opts.resumeSessionId
|
|
672
|
+
? {
|
|
673
|
+
threadId: opts.resumeSessionId,
|
|
674
|
+
cwd: opts.cwd,
|
|
675
|
+
developerInstructions: opts.systemPrompt || null,
|
|
676
|
+
approvalPolicy: settings.approvalPolicy,
|
|
677
|
+
sandbox: buildThreadSandbox(settings.sandboxPolicy),
|
|
678
|
+
model: opts.model || null,
|
|
679
|
+
personality: settings.personality,
|
|
680
|
+
modelProvider: 'openai',
|
|
681
|
+
}
|
|
682
|
+
: {
|
|
683
|
+
cwd: opts.cwd,
|
|
684
|
+
developerInstructions: opts.systemPrompt || null,
|
|
685
|
+
approvalPolicy: settings.approvalPolicy,
|
|
686
|
+
sandbox: buildThreadSandbox(settings.sandboxPolicy),
|
|
687
|
+
model: opts.model || null,
|
|
688
|
+
personality: settings.personality,
|
|
689
|
+
modelProvider: 'openai',
|
|
690
|
+
ephemeral: false,
|
|
691
|
+
};
|
|
692
|
+
const method = opts.resumeSessionId ? 'thread/resume' : 'thread/start';
|
|
693
|
+
const response = await Promise.race([
|
|
694
|
+
this.sendRequest(session, method, rpcParams),
|
|
695
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('Thread bootstrap timed out')), BOOTSTRAP_TIMEOUT_MS)),
|
|
696
|
+
]);
|
|
697
|
+
if (session.bootstrapGeneration !== gen)
|
|
698
|
+
return;
|
|
699
|
+
session.threadId = extractThreadId(response);
|
|
700
|
+
session.threadBootstrapFingerprint = fingerprint;
|
|
701
|
+
this.logInfo(`[cli-warm] codex thread bootstrap completed in ${this.now() - startMs}ms threadId=${JSON.stringify(session.threadId)}`);
|
|
702
|
+
}
|
|
703
|
+
catch (err) {
|
|
704
|
+
if (session.bootstrapGeneration !== gen)
|
|
705
|
+
return;
|
|
706
|
+
session.threadId = null;
|
|
707
|
+
session.threadBootstrapFingerprint = null;
|
|
708
|
+
this.logWarn(`[cli-warm] codex thread bootstrap failed: ${err?.message || String(err)}`);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
async runTurn(opts) {
|
|
712
|
+
throwIfNotLocalDesktop(opts.runtimeMode);
|
|
713
|
+
const key = sessionKey(opts.conversationId, opts.botId);
|
|
714
|
+
const requestedEnv = scrubCodexAppServerEnv(process.env, buildCodexAppServerToolEnv({
|
|
715
|
+
botId: opts.botId,
|
|
716
|
+
botName: opts.botName,
|
|
717
|
+
projectId: opts.projectId,
|
|
718
|
+
}), key);
|
|
719
|
+
const requestedEnvFingerprint = computeEnvFingerprint(requestedEnv);
|
|
720
|
+
let session = this.sessions.get(key);
|
|
721
|
+
const expectedThreadId = opts.forceFreshSession ? null : (opts.resumeSessionId || null);
|
|
722
|
+
const restartReasons = session
|
|
723
|
+
? collectProcessRestartReasons(session, opts.cwd, requestedEnvFingerprint, expectedThreadId)
|
|
724
|
+
: [];
|
|
725
|
+
if (session && restartReasons.length > 0) {
|
|
726
|
+
this.logInfo(`[codex-app-server] codex_process_restarted conversationId=${JSON.stringify(opts.conversationId)} botId=${JSON.stringify(opts.botId)} pid=${session.child.pid ?? 'unknown'} reasons=${JSON.stringify(restartReasons)} requestedThreadId=${JSON.stringify(expectedThreadId)} existingThreadId=${JSON.stringify(session.threadId)}`);
|
|
727
|
+
this.closeSession(key);
|
|
728
|
+
session = undefined;
|
|
729
|
+
}
|
|
730
|
+
// Stale warm-thread invalidation. If warmSession bootstrapped a thread
|
|
731
|
+
// with a different systemPrompt / model / settings than this turn is
|
|
732
|
+
// using, the thread is wrong and must be discarded. Keeping the
|
|
733
|
+
// session + handshake alive; only the threadId is cleared so the
|
|
734
|
+
// existing "no thread → create one" branch of runTurnInternal fires.
|
|
735
|
+
if (session && session.threadId && session.threadBootstrapFingerprint) {
|
|
736
|
+
const currentFingerprint = computeThreadBootstrapFingerprint({
|
|
737
|
+
systemPrompt: opts.systemPrompt,
|
|
738
|
+
model: opts.model,
|
|
739
|
+
codexSettings: opts.codexSettings,
|
|
740
|
+
resumeSessionId: opts.forceFreshSession ? null : (opts.resumeSessionId || null),
|
|
741
|
+
});
|
|
742
|
+
if (currentFingerprint !== session.threadBootstrapFingerprint) {
|
|
743
|
+
session.threadId = null;
|
|
744
|
+
session.threadBootstrapFingerprint = null;
|
|
745
|
+
session.bootstrapPromise = null;
|
|
746
|
+
session.bootstrapGeneration++;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
if (session?.warmPromise) {
|
|
750
|
+
try {
|
|
751
|
+
await session.warmPromise;
|
|
752
|
+
this.logInfo(`[cli-warm] warm_reused conversationId=${JSON.stringify(opts.conversationId)} botId=${JSON.stringify(opts.botId)} provider="codex-cli" ageMs=${session.warmReadyAtMs ? this.now() - session.warmReadyAtMs : 0}`);
|
|
753
|
+
session.warmPromise = null;
|
|
754
|
+
session.warmRequestedAtMs = null;
|
|
755
|
+
session.warmReadyAtMs = null;
|
|
756
|
+
}
|
|
757
|
+
catch {
|
|
758
|
+
if (this.sessions.get(key) === session) {
|
|
759
|
+
this.closeSession(key);
|
|
760
|
+
}
|
|
761
|
+
session = undefined;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
else if (session?.warmReadyAtMs) {
|
|
765
|
+
this.logInfo(`[cli-warm] warm_reused conversationId=${JSON.stringify(opts.conversationId)} botId=${JSON.stringify(opts.botId)} provider="codex-cli" ageMs=${this.now() - session.warmReadyAtMs}`);
|
|
766
|
+
session.warmPromise = null;
|
|
767
|
+
session.warmRequestedAtMs = null;
|
|
768
|
+
session.warmReadyAtMs = null;
|
|
769
|
+
}
|
|
770
|
+
if (!session) {
|
|
771
|
+
this.logInfo(`[cli-warm] send_cold conversationId=${JSON.stringify(opts.conversationId)} botId=${JSON.stringify(opts.botId)} provider="codex-cli"`);
|
|
772
|
+
session = await this.createSession(key, opts.cwd, requestedEnv, opts.currentTodoTaskId);
|
|
773
|
+
this.sessions.set(key, session);
|
|
774
|
+
}
|
|
775
|
+
else {
|
|
776
|
+
(0, managed_process_registry_1.markReused)(key);
|
|
777
|
+
this.logInfo(`[codex-app-server] codex_process_reuse conversationId=${JSON.stringify(opts.conversationId)} botId=${JSON.stringify(opts.botId)} pid=${session.child.pid ?? 'unknown'} forceFreshSession=${opts.forceFreshSession ? 'true' : 'false'} threadId=${JSON.stringify(session.threadId)}`);
|
|
778
|
+
}
|
|
779
|
+
const run = async () => this.runTurnInternal(session, opts);
|
|
780
|
+
const queued = session.chain.then(run, run);
|
|
781
|
+
session.chain = queued.then(() => undefined, () => undefined);
|
|
782
|
+
return queued;
|
|
783
|
+
}
|
|
784
|
+
hasActiveSession(conversationId, botId) {
|
|
785
|
+
const session = this.sessions.get(sessionKey(conversationId, botId));
|
|
786
|
+
return !!session && !session.closed;
|
|
787
|
+
}
|
|
788
|
+
closeSessionByConversation(conversationId, botId) {
|
|
789
|
+
this.closeSession(sessionKey(conversationId, botId));
|
|
790
|
+
}
|
|
791
|
+
closeSessionsByConversation(conversationId) {
|
|
792
|
+
const prefix = `${conversationId}::`;
|
|
793
|
+
let closed = 0;
|
|
794
|
+
for (const key of [...this.sessions.keys()]) {
|
|
795
|
+
if (key.startsWith(prefix)) {
|
|
796
|
+
this.closeSession(key);
|
|
797
|
+
closed++;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
return closed;
|
|
801
|
+
}
|
|
802
|
+
closeWarmSessionsByConversation(conversationId) {
|
|
803
|
+
const prefix = `${conversationId}::`;
|
|
804
|
+
let closed = 0;
|
|
805
|
+
for (const [key, session] of [...this.sessions.entries()]) {
|
|
806
|
+
if (key.startsWith(prefix) && (session.warmPromise || session.warmReadyAtMs)) {
|
|
807
|
+
this.closeSession(key);
|
|
808
|
+
closed++;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
return closed;
|
|
812
|
+
}
|
|
813
|
+
closeSessionsByBotId(botId) {
|
|
814
|
+
const suffix = `::${botId}`;
|
|
815
|
+
for (const key of [...this.sessions.keys()]) {
|
|
816
|
+
if (key.endsWith(suffix)) {
|
|
817
|
+
this.closeSession(key);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
closeAll() {
|
|
822
|
+
for (const key of [...this.sessions.keys()]) {
|
|
823
|
+
this.closeSession(key);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
closeSessionByKey(key) {
|
|
827
|
+
this.closeSession(key);
|
|
828
|
+
}
|
|
829
|
+
closeSession(key) {
|
|
830
|
+
const session = this.sessions.get(key);
|
|
831
|
+
if (!session)
|
|
832
|
+
return;
|
|
833
|
+
session.closed = true;
|
|
834
|
+
session.bootstrapPromise = null;
|
|
835
|
+
session.bootstrapGeneration++;
|
|
836
|
+
const pid = session.child.pid;
|
|
837
|
+
let closeFailed = false;
|
|
838
|
+
if (pid) {
|
|
839
|
+
const killResult = (0, managed_process_registry_1.killProcessTreeDetailed)(pid);
|
|
840
|
+
closeFailed = !killResult.killed || !!killResult.error;
|
|
841
|
+
}
|
|
842
|
+
else {
|
|
843
|
+
try {
|
|
844
|
+
session.child.kill();
|
|
845
|
+
}
|
|
846
|
+
catch {
|
|
847
|
+
closeFailed = true;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
for (const pending of session.pendingRequests.values()) {
|
|
851
|
+
pending.reject(new Error('Codex app-server session closed'));
|
|
852
|
+
}
|
|
853
|
+
session.pendingRequests.clear();
|
|
854
|
+
if (session.activeTurn) {
|
|
855
|
+
session.activeTurn.rejectCompletion(new Error('Codex app-server session closed during active turn'));
|
|
856
|
+
session.activeTurn = null;
|
|
857
|
+
}
|
|
858
|
+
this.sessions.delete(key);
|
|
859
|
+
(0, managed_process_registry_1.unregisterProcess)(key, false, closeFailed);
|
|
860
|
+
}
|
|
861
|
+
async createSession(key, cwd, env, currentTodoTaskId) {
|
|
862
|
+
// Serialize config-write + spawn as an atomic unit through the global
|
|
863
|
+
// chain so concurrent Codex sessions can't interleave config.toml writes.
|
|
864
|
+
// Codex reads config.toml at startup to discover MCP servers and their env,
|
|
865
|
+
// so the write must complete before spawn AND no other write can happen
|
|
866
|
+
// between this write and the spawned process reading its config.
|
|
867
|
+
const toolEnv = buildTurnMcpToolEnv(env, currentTodoTaskId);
|
|
868
|
+
const child = await new Promise((resolve, reject) => {
|
|
869
|
+
_prepareCodexCliConfigChain = _prepareCodexCliConfigChain.then(async () => {
|
|
870
|
+
await (0, sync_cli_config_1.syncMcpToCliConfig)('codex-cli', toolEnv);
|
|
871
|
+
resolve(this.spawnProcess(process.platform === 'win32' ? 'codex' : 'codex', ['app-server'], {
|
|
872
|
+
stdio: 'pipe',
|
|
873
|
+
shell: process.platform === 'win32',
|
|
874
|
+
windowsHide: true,
|
|
875
|
+
cwd,
|
|
876
|
+
env: env,
|
|
877
|
+
}));
|
|
878
|
+
}).catch(reject);
|
|
879
|
+
});
|
|
880
|
+
const session = {
|
|
881
|
+
key,
|
|
882
|
+
cwd,
|
|
883
|
+
env,
|
|
884
|
+
envFingerprint: computeEnvFingerprint(env),
|
|
885
|
+
child,
|
|
886
|
+
stdoutCarry: '',
|
|
887
|
+
createdAtMs: this.now(),
|
|
888
|
+
lastUsedAtMs: this.now(),
|
|
889
|
+
nextRequestId: 1,
|
|
890
|
+
pendingRequests: new Map(),
|
|
891
|
+
activeTurn: null,
|
|
892
|
+
threadId: null,
|
|
893
|
+
readyPromise: Promise.resolve(),
|
|
894
|
+
readyResolved: false,
|
|
895
|
+
warmPromise: null,
|
|
896
|
+
warmRequestedAtMs: null,
|
|
897
|
+
warmReadyAtMs: null,
|
|
898
|
+
threadBootstrapFingerprint: null,
|
|
899
|
+
bootstrapPromise: null,
|
|
900
|
+
bootstrapGeneration: 0,
|
|
901
|
+
closed: false,
|
|
902
|
+
chain: Promise.resolve(),
|
|
903
|
+
};
|
|
904
|
+
if (child.pid) {
|
|
905
|
+
this.logInfo(`[codex-app-server] spawned key=${key} pid=${child.pid} cwd=${cwd}`);
|
|
906
|
+
const keyParts = key.split('::');
|
|
907
|
+
(0, managed_process_registry_1.registerProcess)({
|
|
908
|
+
sessionKey: key,
|
|
909
|
+
provider: 'codex-app-server',
|
|
910
|
+
conversationId: keyParts[0] || '',
|
|
911
|
+
botId: keyParts[1] || '',
|
|
912
|
+
pid: child.pid,
|
|
913
|
+
cwd,
|
|
914
|
+
createdAt: new Date(session.createdAtMs).toISOString(),
|
|
915
|
+
lastUsedAt: new Date(session.lastUsedAtMs).toISOString(),
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
child.stdout.on('data', (chunk) => {
|
|
919
|
+
this.handleStdout(session, chunk.toString());
|
|
920
|
+
});
|
|
921
|
+
child.stderr.on('data', (chunk) => {
|
|
922
|
+
this.handleStderr(session, chunk.toString());
|
|
923
|
+
});
|
|
924
|
+
child.on('error', (err) => {
|
|
925
|
+
this.handleProcessError(session, err);
|
|
926
|
+
});
|
|
927
|
+
child.on('close', (code, signal) => {
|
|
928
|
+
this.handleProcessClose(session, code, signal);
|
|
929
|
+
});
|
|
930
|
+
session.readyPromise = this.initializeSession(session);
|
|
931
|
+
return session;
|
|
932
|
+
}
|
|
933
|
+
async initializeSession(session) {
|
|
934
|
+
await this.sendRequest(session, 'initialize', {
|
|
935
|
+
clientInfo: this.clientInfo,
|
|
936
|
+
});
|
|
937
|
+
this.sendNotification(session, 'initialized');
|
|
938
|
+
session.readyResolved = true;
|
|
939
|
+
}
|
|
940
|
+
syncTurnMcpConfig(session, currentTodoTaskId) {
|
|
941
|
+
const toolEnv = buildTurnMcpToolEnv(session.env, currentTodoTaskId);
|
|
942
|
+
_prepareCodexCliConfigChain = _prepareCodexCliConfigChain.then(async () => {
|
|
943
|
+
await (0, sync_cli_config_1.syncMcpToCliConfig)('codex-cli', toolEnv);
|
|
944
|
+
});
|
|
945
|
+
return _prepareCodexCliConfigChain;
|
|
946
|
+
}
|
|
947
|
+
async runTurnInternal(session, opts, internalOpts) {
|
|
948
|
+
await session.readyPromise;
|
|
949
|
+
session.lastUsedAtMs = this.now();
|
|
950
|
+
(0, managed_process_registry_1.updateLastUsed)(session.key);
|
|
951
|
+
(0, managed_process_registry_1.markTurnStarted)(session.key, 'codex_turn');
|
|
952
|
+
const codexSettings = normalizeCodexSettings(opts.codexSettings);
|
|
953
|
+
await this.syncTurnMcpConfig(session, opts.currentTodoTaskId);
|
|
954
|
+
const eventLog = createEventLog();
|
|
955
|
+
const completionPromise = new Promise((resolve, reject) => {
|
|
956
|
+
const activeTurn = {
|
|
957
|
+
threadId: session.threadId,
|
|
958
|
+
turnId: null,
|
|
959
|
+
finalContent: '',
|
|
960
|
+
thinkingText: '',
|
|
961
|
+
commentaryItemIds: new Set(),
|
|
962
|
+
commentaryTextByItemId: new Map(),
|
|
963
|
+
lastCommentaryText: '',
|
|
964
|
+
done: false,
|
|
965
|
+
aborted: false,
|
|
966
|
+
callbackChain: Promise.resolve(),
|
|
967
|
+
detailFingerprints: new Set(),
|
|
968
|
+
liveDetailCategoryCounts: new Map(),
|
|
969
|
+
eventLog,
|
|
970
|
+
onChunk: opts.onChunk,
|
|
971
|
+
onCommentary: opts.onCommentary,
|
|
972
|
+
onThinkingChunk: opts.onThinkingChunk,
|
|
973
|
+
onDetail: opts.onDetail,
|
|
974
|
+
onToolEvent: opts.onToolEvent,
|
|
975
|
+
resolveCompletion: resolve,
|
|
976
|
+
rejectCompletion: reject,
|
|
977
|
+
};
|
|
978
|
+
session.activeTurn = activeTurn;
|
|
979
|
+
});
|
|
980
|
+
const activeTurn = session.activeTurn;
|
|
981
|
+
const abortHandler = async () => {
|
|
982
|
+
activeTurn.aborted = true;
|
|
983
|
+
if (activeTurn.turnId) {
|
|
984
|
+
try {
|
|
985
|
+
await this.sendRequest(session, 'turn/interrupt', { turnId: activeTurn.turnId });
|
|
986
|
+
}
|
|
987
|
+
catch {
|
|
988
|
+
// best effort; close handler will reject if needed
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
activeTurn.rejectCompletion(buildAbortError());
|
|
992
|
+
session.activeTurn = null;
|
|
993
|
+
};
|
|
994
|
+
opts.abortSignal?.addEventListener('abort', abortHandler, { once: true });
|
|
995
|
+
try {
|
|
996
|
+
const requestedThreadId = opts.forceFreshSession ? null : (opts.resumeSessionId || null);
|
|
997
|
+
if (opts.forceFreshSession) {
|
|
998
|
+
const hasWarmBootstrap = !!session.threadBootstrapFingerprint || !!session.bootstrapPromise;
|
|
999
|
+
if (!hasWarmBootstrap) {
|
|
1000
|
+
session.threadId = null;
|
|
1001
|
+
session.bootstrapPromise = null;
|
|
1002
|
+
session.bootstrapGeneration++;
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
let awaitedBootstrap = false;
|
|
1006
|
+
let bootstrapTimedOut = false;
|
|
1007
|
+
if (!internalOpts?.skipBootstrapWait && session.bootstrapPromise) {
|
|
1008
|
+
awaitedBootstrap = true;
|
|
1009
|
+
try {
|
|
1010
|
+
await session.bootstrapPromise;
|
|
1011
|
+
}
|
|
1012
|
+
catch {
|
|
1013
|
+
bootstrapTimedOut = true;
|
|
1014
|
+
}
|
|
1015
|
+
session.bootstrapPromise = null;
|
|
1016
|
+
}
|
|
1017
|
+
let coldThreadCreated = false;
|
|
1018
|
+
if (!session.threadId) {
|
|
1019
|
+
if (requestedThreadId) {
|
|
1020
|
+
const response = await this.sendRequest(session, 'thread/resume', {
|
|
1021
|
+
threadId: requestedThreadId,
|
|
1022
|
+
cwd: opts.cwd,
|
|
1023
|
+
developerInstructions: opts.systemPrompt || null,
|
|
1024
|
+
approvalPolicy: codexSettings.approvalPolicy,
|
|
1025
|
+
sandbox: buildThreadSandbox(codexSettings.sandboxPolicy),
|
|
1026
|
+
model: opts.model || null,
|
|
1027
|
+
personality: codexSettings.personality,
|
|
1028
|
+
modelProvider: 'openai',
|
|
1029
|
+
});
|
|
1030
|
+
session.threadId = extractThreadId(response);
|
|
1031
|
+
coldThreadCreated = true;
|
|
1032
|
+
}
|
|
1033
|
+
else {
|
|
1034
|
+
const response = await this.sendRequest(session, 'thread/start', {
|
|
1035
|
+
cwd: opts.cwd,
|
|
1036
|
+
developerInstructions: opts.systemPrompt || null,
|
|
1037
|
+
approvalPolicy: codexSettings.approvalPolicy,
|
|
1038
|
+
sandbox: buildThreadSandbox(codexSettings.sandboxPolicy),
|
|
1039
|
+
model: opts.model || null,
|
|
1040
|
+
personality: codexSettings.personality,
|
|
1041
|
+
modelProvider: 'openai',
|
|
1042
|
+
ephemeral: false,
|
|
1043
|
+
});
|
|
1044
|
+
session.threadId = extractThreadId(response);
|
|
1045
|
+
coldThreadCreated = true;
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
if (!internalOpts?.suppressSendPathLog) {
|
|
1049
|
+
if (bootstrapTimedOut) {
|
|
1050
|
+
this.logInfo(`[cli-warm] send_path=bootstrap_timeout fallback=cold`);
|
|
1051
|
+
}
|
|
1052
|
+
else if (awaitedBootstrap && session.threadId && !coldThreadCreated) {
|
|
1053
|
+
this.logInfo(`[cli-warm] send_path=waited_on_warm`);
|
|
1054
|
+
}
|
|
1055
|
+
else if (session.threadId && !awaitedBootstrap && !coldThreadCreated) {
|
|
1056
|
+
this.logInfo(`[cli-warm] send_path=reused_warm_thread`);
|
|
1057
|
+
}
|
|
1058
|
+
else {
|
|
1059
|
+
this.logInfo(`[cli-warm] send_path=cold_start`);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
if (session.threadId && !coldThreadCreated) {
|
|
1063
|
+
const reuseMode = awaitedBootstrap
|
|
1064
|
+
? 'awaited_bootstrap'
|
|
1065
|
+
: session.threadBootstrapFingerprint
|
|
1066
|
+
? 'warm_bootstrap'
|
|
1067
|
+
: 'existing_thread';
|
|
1068
|
+
this.logInfo(`[codex-app-server] codex_thread_reuse conversationId=${JSON.stringify(opts.conversationId)} botId=${JSON.stringify(opts.botId)} threadId=${JSON.stringify(session.threadId)} mode=${reuseMode} forceFreshSession=${opts.forceFreshSession ? 'true' : 'false'}`);
|
|
1069
|
+
}
|
|
1070
|
+
const threadId = session.threadId;
|
|
1071
|
+
if (!threadId) {
|
|
1072
|
+
throw new Error('Codex app-server did not return a thread id');
|
|
1073
|
+
}
|
|
1074
|
+
activeTurn.threadId = threadId;
|
|
1075
|
+
const turnStartParams = {
|
|
1076
|
+
threadId,
|
|
1077
|
+
cwd: opts.cwd,
|
|
1078
|
+
approvalPolicy: codexSettings.approvalPolicy,
|
|
1079
|
+
effort: codexSettings.reasoningEffort,
|
|
1080
|
+
sandboxPolicy: buildTurnSandboxPolicy(codexSettings.sandboxPolicy),
|
|
1081
|
+
personality: codexSettings.personality,
|
|
1082
|
+
input: buildCodexAppServerInput(opts.messages),
|
|
1083
|
+
model: opts.model || null,
|
|
1084
|
+
serviceTier: codexSettings.serviceTier,
|
|
1085
|
+
};
|
|
1086
|
+
if (codexSettings.reasoningSummary && codexSettings.reasoningSummary !== 'none') {
|
|
1087
|
+
turnStartParams.summary = codexSettings.reasoningSummary;
|
|
1088
|
+
}
|
|
1089
|
+
const turnStartResponse = await this.sendRequest(session, 'turn/start', turnStartParams);
|
|
1090
|
+
activeTurn.turnId = extractTurnId(turnStartResponse);
|
|
1091
|
+
(0, managed_process_registry_1.markTurnActivity)(session.key, 'codex_turn_started');
|
|
1092
|
+
return await completionPromise;
|
|
1093
|
+
}
|
|
1094
|
+
finally {
|
|
1095
|
+
opts.abortSignal?.removeEventListener('abort', abortHandler);
|
|
1096
|
+
(0, managed_process_registry_1.markTurnFinished)(session.key, 'codex_turn_finished');
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
sendNotification(session, method, params) {
|
|
1100
|
+
const payload = params === undefined
|
|
1101
|
+
? { jsonrpc: '2.0', method }
|
|
1102
|
+
: { jsonrpc: '2.0', method, params };
|
|
1103
|
+
appendEvent(session.activeTurn?.eventLog, 'out', 'notification', payload);
|
|
1104
|
+
session.child.stdin.write(`${JSON.stringify(payload)}\n`);
|
|
1105
|
+
}
|
|
1106
|
+
sendResponse(session, id, result, error) {
|
|
1107
|
+
const payload = error
|
|
1108
|
+
? { jsonrpc: '2.0', id, error }
|
|
1109
|
+
: { jsonrpc: '2.0', id, result };
|
|
1110
|
+
appendEvent(session.activeTurn?.eventLog, 'out', 'response', payload);
|
|
1111
|
+
session.child.stdin.write(`${JSON.stringify(payload)}\n`);
|
|
1112
|
+
}
|
|
1113
|
+
sendRequest(session, method, params) {
|
|
1114
|
+
if (session.closed) {
|
|
1115
|
+
return Promise.reject(new Error('Codex app-server session is closed'));
|
|
1116
|
+
}
|
|
1117
|
+
const id = session.nextRequestId++;
|
|
1118
|
+
const payload = params === undefined
|
|
1119
|
+
? { jsonrpc: '2.0', id, method }
|
|
1120
|
+
: { jsonrpc: '2.0', id, method, params };
|
|
1121
|
+
appendEvent(session.activeTurn?.eventLog, 'out', 'request', payload);
|
|
1122
|
+
return new Promise((resolve, reject) => {
|
|
1123
|
+
session.pendingRequests.set(id, { method, resolve, reject });
|
|
1124
|
+
session.child.stdin.write(`${JSON.stringify(payload)}\n`);
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
handleStdout(session, chunk) {
|
|
1128
|
+
if (chunk)
|
|
1129
|
+
(0, managed_process_registry_1.markTurnActivity)(session.key, 'codex_stdout');
|
|
1130
|
+
session.stdoutCarry += chunk;
|
|
1131
|
+
const lines = session.stdoutCarry.split(/\r?\n/);
|
|
1132
|
+
session.stdoutCarry = lines.pop() || '';
|
|
1133
|
+
for (const line of lines) {
|
|
1134
|
+
const trimmed = line.trim();
|
|
1135
|
+
if (!trimmed)
|
|
1136
|
+
continue;
|
|
1137
|
+
const parsed = safeJsonParse(trimmed);
|
|
1138
|
+
if (!parsed) {
|
|
1139
|
+
appendEvent(session.activeTurn?.eventLog, 'in', 'parse_error', trimmed);
|
|
1140
|
+
this.logWarn(`[codex-app-server] Non-JSON stdout line: ${trimmed}`);
|
|
1141
|
+
continue;
|
|
1142
|
+
}
|
|
1143
|
+
if (isResponseMessage(parsed)) {
|
|
1144
|
+
appendEvent(session.activeTurn?.eventLog, 'in', 'response', parsed);
|
|
1145
|
+
this.handleResponse(session, parsed);
|
|
1146
|
+
continue;
|
|
1147
|
+
}
|
|
1148
|
+
if (isServerRequestMessage(parsed)) {
|
|
1149
|
+
appendEvent(session.activeTurn?.eventLog, 'in', 'server_request', parsed);
|
|
1150
|
+
void this.handleServerRequest(session, parsed);
|
|
1151
|
+
continue;
|
|
1152
|
+
}
|
|
1153
|
+
if (isNotificationMessage(parsed)) {
|
|
1154
|
+
appendEvent(session.activeTurn?.eventLog, 'in', 'notification', parsed);
|
|
1155
|
+
void this.handleNotification(session, parsed);
|
|
1156
|
+
continue;
|
|
1157
|
+
}
|
|
1158
|
+
appendEvent(session.activeTurn?.eventLog, 'in', 'parse_error', parsed);
|
|
1159
|
+
this.logWarn('[codex-app-server] Unrecognized stdout payload');
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
handleResponse(session, message) {
|
|
1163
|
+
const pending = session.pendingRequests.get(message.id);
|
|
1164
|
+
if (!pending)
|
|
1165
|
+
return;
|
|
1166
|
+
session.pendingRequests.delete(message.id);
|
|
1167
|
+
if (message.error) {
|
|
1168
|
+
pending.reject(new Error(`${pending.method} failed: ${message.error.message || 'Unknown error'}`));
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
pending.resolve(message.result);
|
|
1172
|
+
}
|
|
1173
|
+
async handleServerRequest(session, message) {
|
|
1174
|
+
const approvalResponse = buildApprovalResponse(message.method, message.params);
|
|
1175
|
+
if (approvalResponse !== null) {
|
|
1176
|
+
await this.emitDetail(session.activeTurn, `Auto-handled ${message.method}`);
|
|
1177
|
+
this.sendResponse(session, message.id, approvalResponse);
|
|
1178
|
+
return;
|
|
1179
|
+
}
|
|
1180
|
+
this.sendResponse(session, message.id, undefined, {
|
|
1181
|
+
code: -32601,
|
|
1182
|
+
message: `Unsupported server request: ${message.method}`,
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
async handleNotification(session, message) {
|
|
1186
|
+
const params = message.params || {};
|
|
1187
|
+
if (message.method === 'thread/started') {
|
|
1188
|
+
const threadId = extractThreadId(params);
|
|
1189
|
+
if (threadId) {
|
|
1190
|
+
session.threadId = threadId;
|
|
1191
|
+
if (session.activeTurn)
|
|
1192
|
+
session.activeTurn.threadId = threadId;
|
|
1193
|
+
}
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
if (message.method === 'turn/started') {
|
|
1197
|
+
const turnId = extractTurnId(params);
|
|
1198
|
+
if (session.activeTurn && turnId) {
|
|
1199
|
+
session.activeTurn.turnId = turnId;
|
|
1200
|
+
}
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1203
|
+
if (message.method === 'thread/tokenUsage/updated' && isTurnMatch(session.activeTurn, params)) {
|
|
1204
|
+
const usage = buildTurnUsage(params.tokenUsage);
|
|
1205
|
+
if (usage && session.activeTurn) {
|
|
1206
|
+
session.activeTurn.usage = usage;
|
|
1207
|
+
}
|
|
1208
|
+
return;
|
|
1209
|
+
}
|
|
1210
|
+
if (message.method === 'item/agentMessage/delta' && isTurnMatch(session.activeTurn, params) && session.activeTurn) {
|
|
1211
|
+
const delta = typeof params.delta === 'string' ? params.delta : '';
|
|
1212
|
+
const itemId = extractItemId(params);
|
|
1213
|
+
if (delta) {
|
|
1214
|
+
if (itemId && session.activeTurn.commentaryItemIds.has(itemId)) {
|
|
1215
|
+
const existing = session.activeTurn.commentaryTextByItemId.get(itemId) || '';
|
|
1216
|
+
session.activeTurn.commentaryTextByItemId.set(itemId, existing + delta);
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1219
|
+
session.activeTurn.finalContent += delta;
|
|
1220
|
+
await this.emitChunk(session.activeTurn, delta);
|
|
1221
|
+
}
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
if (message.method === 'item/reasoning/summaryTextDelta' && isTurnMatch(session.activeTurn, params) && session.activeTurn) {
|
|
1225
|
+
const delta = typeof params.delta === 'string' ? params.delta : '';
|
|
1226
|
+
if (delta) {
|
|
1227
|
+
session.activeTurn.thinkingText += delta;
|
|
1228
|
+
await this.emitThinkingChunk(session.activeTurn, delta);
|
|
1229
|
+
}
|
|
1230
|
+
return;
|
|
1231
|
+
}
|
|
1232
|
+
if (message.method === 'item/commandExecution/outputDelta' && isTurnMatch(session.activeTurn, params)) {
|
|
1233
|
+
const delta = typeof params.delta === 'string' ? params.delta : '';
|
|
1234
|
+
if (delta) {
|
|
1235
|
+
await this.emitDetail(session.activeTurn, delta, 'command_output');
|
|
1236
|
+
}
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
if (message.method === 'item/fileChange/outputDelta' && isTurnMatch(session.activeTurn, params)) {
|
|
1240
|
+
const delta = typeof params.delta === 'string' ? params.delta : '';
|
|
1241
|
+
if (delta) {
|
|
1242
|
+
await this.emitDetail(session.activeTurn, delta, 'file_output');
|
|
1243
|
+
}
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
if (message.method === 'item/mcpToolCall/progress' && isTurnMatch(session.activeTurn, params)) {
|
|
1247
|
+
if (typeof params.message === 'string') {
|
|
1248
|
+
await this.emitDetail(session.activeTurn, params.message, 'mcp_progress');
|
|
1249
|
+
}
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
if (message.method === 'mcpServer/startupStatus/updated') {
|
|
1253
|
+
const detail = describeStartupStatus(params);
|
|
1254
|
+
if (detail)
|
|
1255
|
+
await this.emitDetail(session.activeTurn, detail);
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1258
|
+
if (message.method === 'item/started' && isTurnMatch(session.activeTurn, params)) {
|
|
1259
|
+
const commentaryItemId = extractItemId(params);
|
|
1260
|
+
if (session.activeTurn && commentaryItemId && extractAgentMessagePhase(params) === 'commentary') {
|
|
1261
|
+
session.activeTurn.commentaryItemIds.add(commentaryItemId);
|
|
1262
|
+
session.activeTurn.commentaryTextByItemId.set(commentaryItemId, '');
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
// Emit structured tool call event for tool-like items (command_execution,
|
|
1266
|
+
// file_change, mcpToolCall, applyPatch). Also emit a textual detail line
|
|
1267
|
+
// for the live activity log.
|
|
1268
|
+
const toolEvent = buildToolEventFromItem('call', params);
|
|
1269
|
+
if (toolEvent && session.activeTurn?.onToolEvent) {
|
|
1270
|
+
try {
|
|
1271
|
+
await session.activeTurn.onToolEvent(toolEvent);
|
|
1272
|
+
}
|
|
1273
|
+
catch { /* swallow */ }
|
|
1274
|
+
}
|
|
1275
|
+
const detail = describeItemProgress(message.method, params);
|
|
1276
|
+
if (detail)
|
|
1277
|
+
await this.emitDetail(session.activeTurn, detail);
|
|
1278
|
+
return;
|
|
1279
|
+
}
|
|
1280
|
+
if (message.method === 'item/completed' && isTurnMatch(session.activeTurn, params)) {
|
|
1281
|
+
const commentaryItemId = extractItemId(params);
|
|
1282
|
+
if (session.activeTurn && commentaryItemId && session.activeTurn.commentaryItemIds.has(commentaryItemId)) {
|
|
1283
|
+
const completedText = (typeof params?.item?.text === 'string' && params.item.text)
|
|
1284
|
+
|| session.activeTurn.commentaryTextByItemId.get(commentaryItemId)
|
|
1285
|
+
|| '';
|
|
1286
|
+
session.activeTurn.commentaryItemIds.delete(commentaryItemId);
|
|
1287
|
+
session.activeTurn.commentaryTextByItemId.delete(commentaryItemId);
|
|
1288
|
+
await this.emitCommentary(session.activeTurn, completedText);
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1291
|
+
// Emit structured tool result event for tool-like items.
|
|
1292
|
+
const toolEvent = buildToolEventFromItem('result', params);
|
|
1293
|
+
if (toolEvent && session.activeTurn?.onToolEvent) {
|
|
1294
|
+
try {
|
|
1295
|
+
await session.activeTurn.onToolEvent(toolEvent);
|
|
1296
|
+
}
|
|
1297
|
+
catch { /* swallow */ }
|
|
1298
|
+
}
|
|
1299
|
+
const detail = describeItemProgress(message.method, params);
|
|
1300
|
+
if (detail)
|
|
1301
|
+
await this.emitDetail(session.activeTurn, detail);
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1304
|
+
if (message.method === 'turn/completed' && isTurnMatch(session.activeTurn, params) && session.activeTurn) {
|
|
1305
|
+
const activeTurn = session.activeTurn;
|
|
1306
|
+
activeTurn.done = true;
|
|
1307
|
+
try {
|
|
1308
|
+
await activeTurn.callbackChain;
|
|
1309
|
+
}
|
|
1310
|
+
catch {
|
|
1311
|
+
// Callback failures are already logged by the emit helpers.
|
|
1312
|
+
}
|
|
1313
|
+
const status = extractTurnStatus(params);
|
|
1314
|
+
if (status === 'failed') {
|
|
1315
|
+
const messageText = extractTurnFailureMessage(params) || 'Codex app-server turn failed';
|
|
1316
|
+
session.activeTurn = null;
|
|
1317
|
+
activeTurn.rejectCompletion(new Error(messageText));
|
|
1318
|
+
return;
|
|
1319
|
+
}
|
|
1320
|
+
if (status === 'interrupted' && activeTurn.aborted) {
|
|
1321
|
+
session.activeTurn = null;
|
|
1322
|
+
activeTurn.rejectCompletion(buildAbortError());
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1325
|
+
const result = {
|
|
1326
|
+
content: activeTurn.finalContent.trim(),
|
|
1327
|
+
sessionId: session.threadId,
|
|
1328
|
+
usage: activeTurn.usage,
|
|
1329
|
+
rawOutput: serializeEventLog(activeTurn.eventLog),
|
|
1330
|
+
eventLog: activeTurn.eventLog,
|
|
1331
|
+
thinking: activeTurn.thinkingText || undefined,
|
|
1332
|
+
};
|
|
1333
|
+
session.activeTurn = null;
|
|
1334
|
+
activeTurn.resolveCompletion(result);
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
handleStderr(session, chunk) {
|
|
1338
|
+
if (chunk)
|
|
1339
|
+
(0, managed_process_registry_1.markTurnActivity)(session.key, 'codex_stderr');
|
|
1340
|
+
appendEvent(session.activeTurn?.eventLog, 'stderr', 'stderr', chunk);
|
|
1341
|
+
void this.emitDetail(session.activeTurn, chunk, 'stderr');
|
|
1342
|
+
}
|
|
1343
|
+
handleProcessError(session, err) {
|
|
1344
|
+
this.logWarn(`[codex-app-server] Process error for ${session.key}: ${err.stack || err.message}`);
|
|
1345
|
+
if (session.activeTurn) {
|
|
1346
|
+
const activeTurn = session.activeTurn;
|
|
1347
|
+
session.activeTurn = null;
|
|
1348
|
+
activeTurn.rejectCompletion(err);
|
|
1349
|
+
}
|
|
1350
|
+
for (const pending of session.pendingRequests.values()) {
|
|
1351
|
+
pending.reject(err);
|
|
1352
|
+
}
|
|
1353
|
+
session.pendingRequests.clear();
|
|
1354
|
+
session.closed = true;
|
|
1355
|
+
this.sessions.delete(session.key);
|
|
1356
|
+
(0, managed_process_registry_1.unregisterProcess)(session.key, false);
|
|
1357
|
+
}
|
|
1358
|
+
handleProcessClose(session, code, signal) {
|
|
1359
|
+
this.logWarn(`[codex-app-server] Process closed for ${session.key}: code=${code ?? 'null'} signal=${signal ?? 'null'} activeTurn=${session.activeTurn ? 'yes' : 'no'} pendingRequests=${session.pendingRequests.size}`);
|
|
1360
|
+
if (session.stdoutCarry.trim()) {
|
|
1361
|
+
const parsed = safeJsonParse(session.stdoutCarry.trim());
|
|
1362
|
+
if (parsed) {
|
|
1363
|
+
if (isResponseMessage(parsed)) {
|
|
1364
|
+
appendEvent(session.activeTurn?.eventLog, 'in', 'response', parsed);
|
|
1365
|
+
this.handleResponse(session, parsed);
|
|
1366
|
+
}
|
|
1367
|
+
else if (isServerRequestMessage(parsed)) {
|
|
1368
|
+
appendEvent(session.activeTurn?.eventLog, 'in', 'server_request', parsed);
|
|
1369
|
+
void this.handleServerRequest(session, parsed);
|
|
1370
|
+
}
|
|
1371
|
+
else if (isNotificationMessage(parsed)) {
|
|
1372
|
+
appendEvent(session.activeTurn?.eventLog, 'in', 'notification', parsed);
|
|
1373
|
+
void this.handleNotification(session, parsed);
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
session.closed = true;
|
|
1378
|
+
this.sessions.delete(session.key);
|
|
1379
|
+
(0, managed_process_registry_1.unregisterProcess)(session.key, false);
|
|
1380
|
+
const closeError = new Error(`Codex app-server exited${code !== null ? ` with code ${code}` : ''}${signal ? ` (${signal})` : ''}`);
|
|
1381
|
+
for (const pending of session.pendingRequests.values()) {
|
|
1382
|
+
pending.reject(closeError);
|
|
1383
|
+
}
|
|
1384
|
+
session.pendingRequests.clear();
|
|
1385
|
+
if (session.activeTurn) {
|
|
1386
|
+
const activeTurn = session.activeTurn;
|
|
1387
|
+
session.activeTurn = null;
|
|
1388
|
+
if (!activeTurn.done) {
|
|
1389
|
+
activeTurn.rejectCompletion(closeError);
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
async emitChunk(activeTurn, chunk) {
|
|
1394
|
+
if (!activeTurn?.onChunk || !chunk)
|
|
1395
|
+
return;
|
|
1396
|
+
activeTurn.callbackChain = activeTurn.callbackChain.then(async () => {
|
|
1397
|
+
await activeTurn.onChunk?.(chunk);
|
|
1398
|
+
}).catch((err) => {
|
|
1399
|
+
this.logWarn(`[codex-app-server] onChunk callback failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1400
|
+
});
|
|
1401
|
+
await activeTurn.callbackChain;
|
|
1402
|
+
}
|
|
1403
|
+
async emitCommentary(activeTurn, text) {
|
|
1404
|
+
if (!activeTurn?.onCommentary)
|
|
1405
|
+
return;
|
|
1406
|
+
const normalized = normalizeDetail(text);
|
|
1407
|
+
if (!normalized || normalized === activeTurn.lastCommentaryText)
|
|
1408
|
+
return;
|
|
1409
|
+
activeTurn.lastCommentaryText = normalized;
|
|
1410
|
+
activeTurn.callbackChain = activeTurn.callbackChain.then(async () => {
|
|
1411
|
+
await activeTurn.onCommentary?.(normalized);
|
|
1412
|
+
}).catch((err) => {
|
|
1413
|
+
this.logWarn(`[codex-app-server] onCommentary callback failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1414
|
+
});
|
|
1415
|
+
await activeTurn.callbackChain;
|
|
1416
|
+
}
|
|
1417
|
+
async emitThinkingChunk(activeTurn, chunk) {
|
|
1418
|
+
if (!activeTurn?.onThinkingChunk || !chunk)
|
|
1419
|
+
return;
|
|
1420
|
+
activeTurn.callbackChain = activeTurn.callbackChain.then(async () => {
|
|
1421
|
+
await activeTurn.onThinkingChunk?.(chunk);
|
|
1422
|
+
}).catch((err) => {
|
|
1423
|
+
this.logWarn(`[codex-app-server] onThinkingChunk callback failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1424
|
+
});
|
|
1425
|
+
await activeTurn.callbackChain;
|
|
1426
|
+
}
|
|
1427
|
+
async emitDetail(activeTurn, detail, source = 'status') {
|
|
1428
|
+
if (!activeTurn?.onDetail)
|
|
1429
|
+
return;
|
|
1430
|
+
const normalized = prepareLiveDetailForUser(activeTurn, detail, source);
|
|
1431
|
+
if (!normalized)
|
|
1432
|
+
return;
|
|
1433
|
+
if (activeTurn.detailFingerprints.has(normalized))
|
|
1434
|
+
return;
|
|
1435
|
+
activeTurn.detailFingerprints.add(normalized);
|
|
1436
|
+
activeTurn.callbackChain = activeTurn.callbackChain.then(async () => {
|
|
1437
|
+
await activeTurn.onDetail?.(normalized);
|
|
1438
|
+
}).catch((err) => {
|
|
1439
|
+
this.logWarn(`[codex-app-server] onDetail callback failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1440
|
+
});
|
|
1441
|
+
await activeTurn.callbackChain;
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
exports.CodexAppServerManager = CodexAppServerManager;
|
|
1445
|
+
function getCodexAppServerManager() {
|
|
1446
|
+
if (!_manager) {
|
|
1447
|
+
_manager = new CodexAppServerManager();
|
|
1448
|
+
}
|
|
1449
|
+
return _manager;
|
|
1450
|
+
}
|
|
1451
|
+
async function runCodexAppServerTurnForTest(manager, opts) {
|
|
1452
|
+
return manager.runTurn(opts);
|
|
1453
|
+
}
|
|
1454
|
+
exports.__codexAppServerTestUtils = {
|
|
1455
|
+
LOCAL_ONLY_ERROR,
|
|
1456
|
+
buildCodexAppServerInput,
|
|
1457
|
+
buildCodexAppServerToolEnv,
|
|
1458
|
+
buildTurnMcpToolEnv,
|
|
1459
|
+
scrubCodexAppServerEnv,
|
|
1460
|
+
computeEnvFingerprint,
|
|
1461
|
+
buildApprovalResponse,
|
|
1462
|
+
buildTurnUsage,
|
|
1463
|
+
createEventLog,
|
|
1464
|
+
serializeEventLog,
|
|
1465
|
+
sessionKey,
|
|
1466
|
+
delay,
|
|
1467
|
+
};
|
|
1468
|
+
//# sourceMappingURL=codex-app-server-manager.js.map
|