funolio-agent 1.0.75 → 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/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.map +1 -1
- package/dist/bot-manager.js +23 -14
- 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/codex-app-server-manager.d.ts +64 -4
- package/dist/codex-app-server-manager.d.ts.map +1 -1
- package/dist/codex-app-server-manager.js +755 -55
- package/dist/codex-app-server-manager.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/start.d.ts +21 -0
- package/dist/commands/start.d.ts.map +1 -1
- package/dist/commands/start.js +484 -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/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +170 -58
- 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 +202 -16
- package/dist/context-window.js.map +1 -1
- package/dist/live-activity.d.ts +3 -1
- package/dist/live-activity.d.ts.map +1 -1
- package/dist/live-activity.js.map +1 -1
- 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 +138 -3
- package/dist/local-cli-pty-manager.d.ts.map +1 -1
- package/dist/local-cli-pty-manager.js +1415 -111
- 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 +235 -5
- package/dist/local-data.d.ts.map +1 -1
- package/dist/local-data.js +1066 -87
- 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 +376 -4
- 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 +30 -0
- package/dist/local-server.d.ts.map +1 -1
- package/dist/local-server.js +2898 -319
- 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.map +1 -1
- package/dist/message-loop.js +43 -1
- package/dist/message-loop.js.map +1 -1
- package/dist/mqtt-client.d.ts +34 -0
- package/dist/mqtt-client.d.ts.map +1 -1
- package/dist/mqtt-client.js +270 -45
- 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/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 +14 -0
- package/dist/orchestration/orchestrator-operating-prompt.d.ts.map +1 -1
- package/dist/orchestration/orchestrator-operating-prompt.js +157 -31
- 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/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 +195 -3
- package/dist/orchestrator.d.ts.map +1 -1
- package/dist/orchestrator.js +1970 -432
- 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.d.ts.map +1 -1
- package/dist/providers/claude-cli.js +28 -3
- 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 +190 -17
- 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/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 +13 -4
- package/dist/tools/admin-tools.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.map +1 -1
- package/dist/tools/search-conversation-history.js +12 -2
- package/dist/tools/search-conversation-history.js.map +1 -1
- 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 +13 -0
- package/dist/wizard-state.d.ts.map +1 -1
- package/dist/wizard-state.js +61 -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 +40 -1
- package/dist/workflow-engine.d.ts.map +1 -1
- package/dist/workflow-engine.js +753 -93
- package/dist/workflow-engine.js.map +1 -1
- package/package.json +2 -2
|
@@ -4,17 +4,26 @@ exports.__codexAppServerTestUtils = exports.CodexAppServerManager = void 0;
|
|
|
4
4
|
exports.getCodexAppServerManager = getCodexAppServerManager;
|
|
5
5
|
exports.runCodexAppServerTurnForTest = runCodexAppServerTurnForTest;
|
|
6
6
|
const child_process_1 = require("child_process");
|
|
7
|
+
const crypto_1 = require("crypto");
|
|
7
8
|
const runtime_context_1 = require("./runtime-context");
|
|
8
9
|
const sync_cli_config_1 = require("./mcp/sync-cli-config");
|
|
10
|
+
const managed_process_registry_1 = require("./managed-process-registry");
|
|
9
11
|
const LOCAL_ONLY_ERROR = 'Codex app-server manager is local_desktop only';
|
|
12
|
+
const CODEX_APP_SERVER_WARM_TIMEOUT_MS = 10_000;
|
|
10
13
|
const DEFAULT_CODEX_SETTINGS = {
|
|
11
|
-
reasoningEffort: '
|
|
14
|
+
reasoningEffort: 'low',
|
|
12
15
|
reasoningSummary: 'detailed',
|
|
13
16
|
personality: 'friendly',
|
|
14
17
|
serviceTier: 'fast',
|
|
15
18
|
sandboxPolicy: 'danger-full-access',
|
|
16
19
|
approvalPolicy: 'never',
|
|
17
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']);
|
|
18
27
|
let _manager = null;
|
|
19
28
|
let _prepareCodexCliConfigChain = Promise.resolve();
|
|
20
29
|
function delay(ms) {
|
|
@@ -33,6 +42,48 @@ function throwIfNotLocalDesktop(runtimeMode) {
|
|
|
33
42
|
function sessionKey(conversationId, botId) {
|
|
34
43
|
return `${conversationId}::${botId}`;
|
|
35
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
|
+
}
|
|
36
87
|
function createEventLog() {
|
|
37
88
|
return {
|
|
38
89
|
schema: 'funolio.codex-app-server.event-log@1',
|
|
@@ -55,13 +106,77 @@ function serializeEventLog(eventLog) {
|
|
|
55
106
|
function normalizeDetail(text) {
|
|
56
107
|
return String(text || '').replace(/\s+/g, ' ').trim();
|
|
57
108
|
}
|
|
58
|
-
function
|
|
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) {
|
|
59
172
|
const env = {};
|
|
60
173
|
for (const [key, value] of Object.entries(baseEnv)) {
|
|
61
174
|
if (value == null)
|
|
62
175
|
continue;
|
|
63
176
|
if (key === 'CODEX_THREAD_ID')
|
|
64
177
|
continue;
|
|
178
|
+
if (key === 'FUNOLIO_TOOL_TODO_ID')
|
|
179
|
+
continue;
|
|
65
180
|
env[key] = value;
|
|
66
181
|
}
|
|
67
182
|
for (const [key, value] of Object.entries(extraEnv)) {
|
|
@@ -69,11 +184,18 @@ function scrubCodexAppServerEnv(baseEnv, extraEnv) {
|
|
|
69
184
|
continue;
|
|
70
185
|
env[key] = value;
|
|
71
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
|
+
}
|
|
72
193
|
return env;
|
|
73
194
|
}
|
|
195
|
+
const VOLATILE_ENV_KEYS = new Set(['FUNOLIO_TOOL_TODO_ID', 'FUNOLIO_MANAGED_SESSION']);
|
|
74
196
|
function computeEnvFingerprint(env) {
|
|
75
197
|
return JSON.stringify(Object.entries(env)
|
|
76
|
-
.filter(([, value]) => typeof value === 'string')
|
|
198
|
+
.filter(([key, value]) => typeof value === 'string' && !VOLATILE_ENV_KEYS.has(key))
|
|
77
199
|
.sort(([a], [b]) => a.localeCompare(b)));
|
|
78
200
|
}
|
|
79
201
|
function buildCodexAppServerToolEnv(input) {
|
|
@@ -83,14 +205,28 @@ function buildCodexAppServerToolEnv(input) {
|
|
|
83
205
|
env.FUNOLIO_TOOL_ACTOR_ID = actorId;
|
|
84
206
|
if (input.projectId !== undefined && input.projectId !== null)
|
|
85
207
|
env.FUNOLIO_TOOL_PROJECT_ID = String(input.projectId);
|
|
86
|
-
if (input.todoTaskId !== undefined && input.todoTaskId !== null)
|
|
87
|
-
env.FUNOLIO_TOOL_TODO_ID = String(input.todoTaskId);
|
|
88
208
|
return env;
|
|
89
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
|
+
}
|
|
90
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'));
|
|
91
227
|
const lastMessage = messages.length > 0 ? messages[messages.length - 1] : null;
|
|
92
228
|
if (lastMessage?.role === 'user') {
|
|
93
|
-
return lastMessage.content
|
|
229
|
+
return renderContent(lastMessage.content);
|
|
94
230
|
}
|
|
95
231
|
if (messages.length === 0)
|
|
96
232
|
return '';
|
|
@@ -99,27 +235,51 @@ function extractLatestUserText(messages) {
|
|
|
99
235
|
: message.role === 'assistant' ? 'ASSISTANT'
|
|
100
236
|
: message.role === 'tool' ? `TOOL (${message.toolName || 'tool'})`
|
|
101
237
|
: String(message.role || 'MESSAGE').toUpperCase();
|
|
102
|
-
return `${label}:\n${message.content
|
|
238
|
+
return `${label}:\n${renderContent(message.content)}`;
|
|
103
239
|
});
|
|
104
240
|
return rendered.join('\n\n');
|
|
105
241
|
}
|
|
106
242
|
function buildCodexAppServerInput(messages) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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) }];
|
|
113
264
|
}
|
|
114
265
|
function buildTurnUsage(tokenUsage) {
|
|
115
266
|
const last = tokenUsage?.last;
|
|
116
267
|
if (!last || typeof last !== 'object')
|
|
117
268
|
return undefined;
|
|
118
|
-
|
|
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;
|
|
119
273
|
const outputTokens = Number(last.outputTokens || 0) + Number(last.reasoningOutputTokens || 0);
|
|
120
274
|
if (!Number.isFinite(inputTokens) || !Number.isFinite(outputTokens))
|
|
121
275
|
return undefined;
|
|
122
|
-
return {
|
|
276
|
+
return {
|
|
277
|
+
inputTokens,
|
|
278
|
+
inputTokensFresh: fresh,
|
|
279
|
+
inputTokensCacheCreation: 0,
|
|
280
|
+
inputTokensCacheRead: cacheRead,
|
|
281
|
+
outputTokens,
|
|
282
|
+
};
|
|
123
283
|
}
|
|
124
284
|
function extractThreadId(payload) {
|
|
125
285
|
const id = payload?.thread?.id ?? payload?.threadId ?? null;
|
|
@@ -169,6 +329,16 @@ function describeStartupStatus(params) {
|
|
|
169
329
|
const error = typeof params?.error === 'string' && params.error.trim() ? ` (${params.error.trim()})` : '';
|
|
170
330
|
return `${name}: ${status}${error}`;
|
|
171
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
|
+
}
|
|
172
342
|
function describeItemProgress(method, params) {
|
|
173
343
|
if (!params?.item || typeof params.item !== 'object')
|
|
174
344
|
return null;
|
|
@@ -183,14 +353,94 @@ function describeItemProgress(method, params) {
|
|
|
183
353
|
const prefix = method === 'item/started' ? 'Started' : 'Completed';
|
|
184
354
|
return `${prefix} ${itemType}${label ? `: ${label}` : ''}${status}`;
|
|
185
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
|
+
}
|
|
186
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();
|
|
187
425
|
return {
|
|
188
|
-
reasoningEffort:
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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,
|
|
194
444
|
};
|
|
195
445
|
}
|
|
196
446
|
function buildThreadSandbox(sandboxPolicy) {
|
|
@@ -213,6 +463,27 @@ function buildTurnSandboxPolicy(sandboxPolicy) {
|
|
|
213
463
|
return { type: 'dangerFullAccess' };
|
|
214
464
|
}
|
|
215
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
|
+
}
|
|
216
487
|
function buildApprovalResponse(method, params) {
|
|
217
488
|
switch (method) {
|
|
218
489
|
case 'item/commandExecution/requestApproval':
|
|
@@ -253,56 +524,328 @@ class CodexAppServerManager {
|
|
|
253
524
|
this.prepareCliConfig = deps.prepareCliConfig || defaultPrepareCliConfig;
|
|
254
525
|
this.logInfo = deps.logInfo || ((message) => console.log(message));
|
|
255
526
|
this.logWarn = deps.logWarn || ((message) => console.warn(message));
|
|
256
|
-
this.clientInfo = deps.clientInfo || { name: 'funolio-agent', version: '1.
|
|
527
|
+
this.clientInfo = deps.clientInfo || { name: 'funolio-agent', version: '1.1.5' };
|
|
257
528
|
}
|
|
258
|
-
async
|
|
529
|
+
async warmSession(opts) {
|
|
259
530
|
throwIfNotLocalDesktop(opts.runtimeMode);
|
|
260
531
|
const key = sessionKey(opts.conversationId, opts.botId);
|
|
261
532
|
const requestedEnv = scrubCodexAppServerEnv(process.env, buildCodexAppServerToolEnv({
|
|
262
533
|
botId: opts.botId,
|
|
263
534
|
botName: opts.botName,
|
|
264
535
|
projectId: opts.projectId,
|
|
265
|
-
|
|
266
|
-
}));
|
|
536
|
+
}), key);
|
|
267
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;
|
|
268
548
|
let session = this.sessions.get(key);
|
|
269
|
-
const expectedThreadId = opts.forceFreshSession ? null : (opts.resumeSessionId || null);
|
|
270
549
|
if (session
|
|
271
|
-
&& (opts.
|
|
272
|
-
|| session.cwd !== opts.cwd
|
|
550
|
+
&& (session.cwd !== opts.cwd
|
|
273
551
|
|| session.closed
|
|
274
|
-
|| session.envFingerprint !== requestedEnvFingerprint
|
|
275
|
-
|| (expectedThreadId && session.threadId && session.threadId !== expectedThreadId))) {
|
|
552
|
+
|| session.envFingerprint !== requestedEnvFingerprint)) {
|
|
276
553
|
this.closeSession(key);
|
|
277
554
|
session = undefined;
|
|
278
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;
|
|
279
570
|
if (!session) {
|
|
280
|
-
session = await this.createSession(key, opts.cwd, requestedEnv);
|
|
571
|
+
session = await this.createSession(key, opts.cwd, requestedEnv, opts.currentTodoTaskId);
|
|
281
572
|
this.sessions.set(key, session);
|
|
282
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
|
+
}
|
|
283
779
|
const run = async () => this.runTurnInternal(session, opts);
|
|
284
780
|
const queued = session.chain.then(run, run);
|
|
285
781
|
session.chain = queued.then(() => undefined, () => undefined);
|
|
286
782
|
return queued;
|
|
287
783
|
}
|
|
784
|
+
hasActiveSession(conversationId, botId) {
|
|
785
|
+
const session = this.sessions.get(sessionKey(conversationId, botId));
|
|
786
|
+
return !!session && !session.closed;
|
|
787
|
+
}
|
|
288
788
|
closeSessionByConversation(conversationId, botId) {
|
|
289
789
|
this.closeSession(sessionKey(conversationId, botId));
|
|
290
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
|
+
}
|
|
291
821
|
closeAll() {
|
|
292
822
|
for (const key of [...this.sessions.keys()]) {
|
|
293
823
|
this.closeSession(key);
|
|
294
824
|
}
|
|
295
825
|
}
|
|
826
|
+
closeSessionByKey(key) {
|
|
827
|
+
this.closeSession(key);
|
|
828
|
+
}
|
|
296
829
|
closeSession(key) {
|
|
297
830
|
const session = this.sessions.get(key);
|
|
298
831
|
if (!session)
|
|
299
832
|
return;
|
|
300
833
|
session.closed = true;
|
|
301
|
-
|
|
302
|
-
|
|
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;
|
|
303
841
|
}
|
|
304
|
-
|
|
305
|
-
|
|
842
|
+
else {
|
|
843
|
+
try {
|
|
844
|
+
session.child.kill();
|
|
845
|
+
}
|
|
846
|
+
catch {
|
|
847
|
+
closeFailed = true;
|
|
848
|
+
}
|
|
306
849
|
}
|
|
307
850
|
for (const pending of session.pendingRequests.values()) {
|
|
308
851
|
pending.reject(new Error('Codex app-server session closed'));
|
|
@@ -313,15 +856,26 @@ class CodexAppServerManager {
|
|
|
313
856
|
session.activeTurn = null;
|
|
314
857
|
}
|
|
315
858
|
this.sessions.delete(key);
|
|
859
|
+
(0, managed_process_registry_1.unregisterProcess)(key, false, closeFailed);
|
|
316
860
|
}
|
|
317
|
-
async createSession(key, cwd, env) {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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);
|
|
325
879
|
});
|
|
326
880
|
const session = {
|
|
327
881
|
key,
|
|
@@ -338,9 +892,29 @@ class CodexAppServerManager {
|
|
|
338
892
|
threadId: null,
|
|
339
893
|
readyPromise: Promise.resolve(),
|
|
340
894
|
readyResolved: false,
|
|
895
|
+
warmPromise: null,
|
|
896
|
+
warmRequestedAtMs: null,
|
|
897
|
+
warmReadyAtMs: null,
|
|
898
|
+
threadBootstrapFingerprint: null,
|
|
899
|
+
bootstrapPromise: null,
|
|
900
|
+
bootstrapGeneration: 0,
|
|
341
901
|
closed: false,
|
|
342
902
|
chain: Promise.resolve(),
|
|
343
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
|
+
}
|
|
344
918
|
child.stdout.on('data', (chunk) => {
|
|
345
919
|
this.handleStdout(session, chunk.toString());
|
|
346
920
|
});
|
|
@@ -363,10 +937,20 @@ class CodexAppServerManager {
|
|
|
363
937
|
this.sendNotification(session, 'initialized');
|
|
364
938
|
session.readyResolved = true;
|
|
365
939
|
}
|
|
366
|
-
|
|
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) {
|
|
367
948
|
await session.readyPromise;
|
|
368
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');
|
|
369
952
|
const codexSettings = normalizeCodexSettings(opts.codexSettings);
|
|
953
|
+
await this.syncTurnMcpConfig(session, opts.currentTodoTaskId);
|
|
370
954
|
const eventLog = createEventLog();
|
|
371
955
|
const completionPromise = new Promise((resolve, reject) => {
|
|
372
956
|
const activeTurn = {
|
|
@@ -374,14 +958,20 @@ class CodexAppServerManager {
|
|
|
374
958
|
turnId: null,
|
|
375
959
|
finalContent: '',
|
|
376
960
|
thinkingText: '',
|
|
961
|
+
commentaryItemIds: new Set(),
|
|
962
|
+
commentaryTextByItemId: new Map(),
|
|
963
|
+
lastCommentaryText: '',
|
|
377
964
|
done: false,
|
|
378
965
|
aborted: false,
|
|
379
966
|
callbackChain: Promise.resolve(),
|
|
380
967
|
detailFingerprints: new Set(),
|
|
968
|
+
liveDetailCategoryCounts: new Map(),
|
|
381
969
|
eventLog,
|
|
382
970
|
onChunk: opts.onChunk,
|
|
971
|
+
onCommentary: opts.onCommentary,
|
|
383
972
|
onThinkingChunk: opts.onThinkingChunk,
|
|
384
973
|
onDetail: opts.onDetail,
|
|
974
|
+
onToolEvent: opts.onToolEvent,
|
|
385
975
|
resolveCompletion: resolve,
|
|
386
976
|
rejectCompletion: reject,
|
|
387
977
|
};
|
|
@@ -405,8 +995,26 @@ class CodexAppServerManager {
|
|
|
405
995
|
try {
|
|
406
996
|
const requestedThreadId = opts.forceFreshSession ? null : (opts.resumeSessionId || null);
|
|
407
997
|
if (opts.forceFreshSession) {
|
|
408
|
-
session.
|
|
998
|
+
const hasWarmBootstrap = !!session.threadBootstrapFingerprint || !!session.bootstrapPromise;
|
|
999
|
+
if (!hasWarmBootstrap) {
|
|
1000
|
+
session.threadId = null;
|
|
1001
|
+
session.bootstrapPromise = null;
|
|
1002
|
+
session.bootstrapGeneration++;
|
|
1003
|
+
}
|
|
409
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;
|
|
410
1018
|
if (!session.threadId) {
|
|
411
1019
|
if (requestedThreadId) {
|
|
412
1020
|
const response = await this.sendRequest(session, 'thread/resume', {
|
|
@@ -420,6 +1028,7 @@ class CodexAppServerManager {
|
|
|
420
1028
|
modelProvider: 'openai',
|
|
421
1029
|
});
|
|
422
1030
|
session.threadId = extractThreadId(response);
|
|
1031
|
+
coldThreadCreated = true;
|
|
423
1032
|
}
|
|
424
1033
|
else {
|
|
425
1034
|
const response = await this.sendRequest(session, 'thread/start', {
|
|
@@ -433,14 +1042,37 @@ class CodexAppServerManager {
|
|
|
433
1042
|
ephemeral: false,
|
|
434
1043
|
});
|
|
435
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`);
|
|
436
1060
|
}
|
|
437
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
|
+
}
|
|
438
1070
|
const threadId = session.threadId;
|
|
439
1071
|
if (!threadId) {
|
|
440
1072
|
throw new Error('Codex app-server did not return a thread id');
|
|
441
1073
|
}
|
|
442
1074
|
activeTurn.threadId = threadId;
|
|
443
|
-
const
|
|
1075
|
+
const turnStartParams = {
|
|
444
1076
|
threadId,
|
|
445
1077
|
cwd: opts.cwd,
|
|
446
1078
|
approvalPolicy: codexSettings.approvalPolicy,
|
|
@@ -450,13 +1082,18 @@ class CodexAppServerManager {
|
|
|
450
1082
|
input: buildCodexAppServerInput(opts.messages),
|
|
451
1083
|
model: opts.model || null,
|
|
452
1084
|
serviceTier: codexSettings.serviceTier,
|
|
453
|
-
|
|
454
|
-
|
|
1085
|
+
};
|
|
1086
|
+
if (codexSettings.reasoningSummary && codexSettings.reasoningSummary !== 'none') {
|
|
1087
|
+
turnStartParams.summary = codexSettings.reasoningSummary;
|
|
1088
|
+
}
|
|
1089
|
+
const turnStartResponse = await this.sendRequest(session, 'turn/start', turnStartParams);
|
|
455
1090
|
activeTurn.turnId = extractTurnId(turnStartResponse);
|
|
1091
|
+
(0, managed_process_registry_1.markTurnActivity)(session.key, 'codex_turn_started');
|
|
456
1092
|
return await completionPromise;
|
|
457
1093
|
}
|
|
458
1094
|
finally {
|
|
459
1095
|
opts.abortSignal?.removeEventListener('abort', abortHandler);
|
|
1096
|
+
(0, managed_process_registry_1.markTurnFinished)(session.key, 'codex_turn_finished');
|
|
460
1097
|
}
|
|
461
1098
|
}
|
|
462
1099
|
sendNotification(session, method, params) {
|
|
@@ -488,6 +1125,8 @@ class CodexAppServerManager {
|
|
|
488
1125
|
});
|
|
489
1126
|
}
|
|
490
1127
|
handleStdout(session, chunk) {
|
|
1128
|
+
if (chunk)
|
|
1129
|
+
(0, managed_process_registry_1.markTurnActivity)(session.key, 'codex_stdout');
|
|
491
1130
|
session.stdoutCarry += chunk;
|
|
492
1131
|
const lines = session.stdoutCarry.split(/\r?\n/);
|
|
493
1132
|
session.stdoutCarry = lines.pop() || '';
|
|
@@ -570,7 +1209,13 @@ class CodexAppServerManager {
|
|
|
570
1209
|
}
|
|
571
1210
|
if (message.method === 'item/agentMessage/delta' && isTurnMatch(session.activeTurn, params) && session.activeTurn) {
|
|
572
1211
|
const delta = typeof params.delta === 'string' ? params.delta : '';
|
|
1212
|
+
const itemId = extractItemId(params);
|
|
573
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
|
+
}
|
|
574
1219
|
session.activeTurn.finalContent += delta;
|
|
575
1220
|
await this.emitChunk(session.activeTurn, delta);
|
|
576
1221
|
}
|
|
@@ -587,20 +1232,20 @@ class CodexAppServerManager {
|
|
|
587
1232
|
if (message.method === 'item/commandExecution/outputDelta' && isTurnMatch(session.activeTurn, params)) {
|
|
588
1233
|
const delta = typeof params.delta === 'string' ? params.delta : '';
|
|
589
1234
|
if (delta) {
|
|
590
|
-
await this.emitDetail(session.activeTurn, delta);
|
|
1235
|
+
await this.emitDetail(session.activeTurn, delta, 'command_output');
|
|
591
1236
|
}
|
|
592
1237
|
return;
|
|
593
1238
|
}
|
|
594
1239
|
if (message.method === 'item/fileChange/outputDelta' && isTurnMatch(session.activeTurn, params)) {
|
|
595
1240
|
const delta = typeof params.delta === 'string' ? params.delta : '';
|
|
596
1241
|
if (delta) {
|
|
597
|
-
await this.emitDetail(session.activeTurn, delta);
|
|
1242
|
+
await this.emitDetail(session.activeTurn, delta, 'file_output');
|
|
598
1243
|
}
|
|
599
1244
|
return;
|
|
600
1245
|
}
|
|
601
1246
|
if (message.method === 'item/mcpToolCall/progress' && isTurnMatch(session.activeTurn, params)) {
|
|
602
1247
|
if (typeof params.message === 'string') {
|
|
603
|
-
await this.emitDetail(session.activeTurn, params.message);
|
|
1248
|
+
await this.emitDetail(session.activeTurn, params.message, 'mcp_progress');
|
|
604
1249
|
}
|
|
605
1250
|
return;
|
|
606
1251
|
}
|
|
@@ -611,12 +1256,46 @@ class CodexAppServerManager {
|
|
|
611
1256
|
return;
|
|
612
1257
|
}
|
|
613
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
|
+
}
|
|
614
1275
|
const detail = describeItemProgress(message.method, params);
|
|
615
1276
|
if (detail)
|
|
616
1277
|
await this.emitDetail(session.activeTurn, detail);
|
|
617
1278
|
return;
|
|
618
1279
|
}
|
|
619
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
|
+
}
|
|
620
1299
|
const detail = describeItemProgress(message.method, params);
|
|
621
1300
|
if (detail)
|
|
622
1301
|
await this.emitDetail(session.activeTurn, detail);
|
|
@@ -656,11 +1335,13 @@ class CodexAppServerManager {
|
|
|
656
1335
|
}
|
|
657
1336
|
}
|
|
658
1337
|
handleStderr(session, chunk) {
|
|
1338
|
+
if (chunk)
|
|
1339
|
+
(0, managed_process_registry_1.markTurnActivity)(session.key, 'codex_stderr');
|
|
659
1340
|
appendEvent(session.activeTurn?.eventLog, 'stderr', 'stderr', chunk);
|
|
660
|
-
void this.emitDetail(session.activeTurn, chunk);
|
|
1341
|
+
void this.emitDetail(session.activeTurn, chunk, 'stderr');
|
|
661
1342
|
}
|
|
662
1343
|
handleProcessError(session, err) {
|
|
663
|
-
this.logWarn(`[codex-app-server] Process error for ${session.key}: ${err.message}`);
|
|
1344
|
+
this.logWarn(`[codex-app-server] Process error for ${session.key}: ${err.stack || err.message}`);
|
|
664
1345
|
if (session.activeTurn) {
|
|
665
1346
|
const activeTurn = session.activeTurn;
|
|
666
1347
|
session.activeTurn = null;
|
|
@@ -672,8 +1353,10 @@ class CodexAppServerManager {
|
|
|
672
1353
|
session.pendingRequests.clear();
|
|
673
1354
|
session.closed = true;
|
|
674
1355
|
this.sessions.delete(session.key);
|
|
1356
|
+
(0, managed_process_registry_1.unregisterProcess)(session.key, false);
|
|
675
1357
|
}
|
|
676
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}`);
|
|
677
1360
|
if (session.stdoutCarry.trim()) {
|
|
678
1361
|
const parsed = safeJsonParse(session.stdoutCarry.trim());
|
|
679
1362
|
if (parsed) {
|
|
@@ -693,6 +1376,7 @@ class CodexAppServerManager {
|
|
|
693
1376
|
}
|
|
694
1377
|
session.closed = true;
|
|
695
1378
|
this.sessions.delete(session.key);
|
|
1379
|
+
(0, managed_process_registry_1.unregisterProcess)(session.key, false);
|
|
696
1380
|
const closeError = new Error(`Codex app-server exited${code !== null ? ` with code ${code}` : ''}${signal ? ` (${signal})` : ''}`);
|
|
697
1381
|
for (const pending of session.pendingRequests.values()) {
|
|
698
1382
|
pending.reject(closeError);
|
|
@@ -716,6 +1400,20 @@ class CodexAppServerManager {
|
|
|
716
1400
|
});
|
|
717
1401
|
await activeTurn.callbackChain;
|
|
718
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
|
+
}
|
|
719
1417
|
async emitThinkingChunk(activeTurn, chunk) {
|
|
720
1418
|
if (!activeTurn?.onThinkingChunk || !chunk)
|
|
721
1419
|
return;
|
|
@@ -726,10 +1424,10 @@ class CodexAppServerManager {
|
|
|
726
1424
|
});
|
|
727
1425
|
await activeTurn.callbackChain;
|
|
728
1426
|
}
|
|
729
|
-
async emitDetail(activeTurn, detail) {
|
|
1427
|
+
async emitDetail(activeTurn, detail, source = 'status') {
|
|
730
1428
|
if (!activeTurn?.onDetail)
|
|
731
1429
|
return;
|
|
732
|
-
const normalized =
|
|
1430
|
+
const normalized = prepareLiveDetailForUser(activeTurn, detail, source);
|
|
733
1431
|
if (!normalized)
|
|
734
1432
|
return;
|
|
735
1433
|
if (activeTurn.detailFingerprints.has(normalized))
|
|
@@ -757,7 +1455,9 @@ exports.__codexAppServerTestUtils = {
|
|
|
757
1455
|
LOCAL_ONLY_ERROR,
|
|
758
1456
|
buildCodexAppServerInput,
|
|
759
1457
|
buildCodexAppServerToolEnv,
|
|
1458
|
+
buildTurnMcpToolEnv,
|
|
760
1459
|
scrubCodexAppServerEnv,
|
|
1460
|
+
computeEnvFingerprint,
|
|
761
1461
|
buildApprovalResponse,
|
|
762
1462
|
buildTurnUsage,
|
|
763
1463
|
createEventLog,
|