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.
Files changed (236) hide show
  1. package/dist/auth/credential-reader.d.ts.map +1 -1
  2. package/dist/auth/credential-reader.js +4 -3
  3. package/dist/auth/credential-reader.js.map +1 -1
  4. package/dist/auth/token-refresh.d.ts +8 -0
  5. package/dist/auth/token-refresh.d.ts.map +1 -1
  6. package/dist/auth/token-refresh.js +82 -52
  7. package/dist/auth/token-refresh.js.map +1 -1
  8. package/dist/auto-organizer.d.ts.map +1 -1
  9. package/dist/auto-organizer.js +6 -7
  10. package/dist/auto-organizer.js.map +1 -1
  11. package/dist/bench-prefix.d.ts +16 -0
  12. package/dist/bench-prefix.d.ts.map +1 -0
  13. package/dist/bench-prefix.js +25 -0
  14. package/dist/bench-prefix.js.map +1 -0
  15. package/dist/bot-manager.d.ts.map +1 -1
  16. package/dist/bot-manager.js +23 -14
  17. package/dist/bot-manager.js.map +1 -1
  18. package/dist/chat-sync.d.ts +42 -0
  19. package/dist/chat-sync.d.ts.map +1 -0
  20. package/dist/chat-sync.js +95 -0
  21. package/dist/chat-sync.js.map +1 -0
  22. package/dist/clerk-model.d.ts +7 -0
  23. package/dist/clerk-model.d.ts.map +1 -1
  24. package/dist/clerk-model.js +42 -8
  25. package/dist/clerk-model.js.map +1 -1
  26. package/dist/cli-bootstrap-history.d.ts +10 -0
  27. package/dist/cli-bootstrap-history.d.ts.map +1 -0
  28. package/dist/cli-bootstrap-history.js +112 -0
  29. package/dist/cli-bootstrap-history.js.map +1 -0
  30. package/dist/cli-models.d.ts +8 -0
  31. package/dist/cli-models.d.ts.map +1 -0
  32. package/dist/cli-models.js +91 -0
  33. package/dist/cli-models.js.map +1 -0
  34. package/dist/cli-session-epoch.d.ts +13 -3
  35. package/dist/cli-session-epoch.d.ts.map +1 -1
  36. package/dist/cli-session-epoch.js +53 -4
  37. package/dist/cli-session-epoch.js.map +1 -1
  38. package/dist/codex-app-server-manager.d.ts +64 -4
  39. package/dist/codex-app-server-manager.d.ts.map +1 -1
  40. package/dist/codex-app-server-manager.js +755 -55
  41. package/dist/codex-app-server-manager.js.map +1 -1
  42. package/dist/commands/pool.d.ts +32 -0
  43. package/dist/commands/pool.d.ts.map +1 -1
  44. package/dist/commands/pool.js +145 -66
  45. package/dist/commands/pool.js.map +1 -1
  46. package/dist/commands/start.d.ts +21 -0
  47. package/dist/commands/start.d.ts.map +1 -1
  48. package/dist/commands/start.js +484 -63
  49. package/dist/commands/start.js.map +1 -1
  50. package/dist/commands/status.d.ts.map +1 -1
  51. package/dist/commands/status.js +5 -2
  52. package/dist/commands/status.js.map +1 -1
  53. package/dist/config.d.ts +1 -0
  54. package/dist/config.d.ts.map +1 -1
  55. package/dist/config.js +170 -58
  56. package/dist/config.js.map +1 -1
  57. package/dist/context-window.d.ts +37 -1
  58. package/dist/context-window.d.ts.map +1 -1
  59. package/dist/context-window.js +202 -16
  60. package/dist/context-window.js.map +1 -1
  61. package/dist/live-activity.d.ts +3 -1
  62. package/dist/live-activity.d.ts.map +1 -1
  63. package/dist/live-activity.js.map +1 -1
  64. package/dist/local-chat-execution.d.ts +114 -0
  65. package/dist/local-chat-execution.d.ts.map +1 -0
  66. package/dist/local-chat-execution.js +349 -0
  67. package/dist/local-chat-execution.js.map +1 -0
  68. package/dist/local-cli-pty-manager.d.ts +138 -3
  69. package/dist/local-cli-pty-manager.d.ts.map +1 -1
  70. package/dist/local-cli-pty-manager.js +1415 -111
  71. package/dist/local-cli-pty-manager.js.map +1 -1
  72. package/dist/local-conversation-gateway.d.ts +110 -0
  73. package/dist/local-conversation-gateway.d.ts.map +1 -0
  74. package/dist/local-conversation-gateway.js +175 -0
  75. package/dist/local-conversation-gateway.js.map +1 -0
  76. package/dist/local-data.d.ts +235 -5
  77. package/dist/local-data.d.ts.map +1 -1
  78. package/dist/local-data.js +1066 -87
  79. package/dist/local-data.js.map +1 -1
  80. package/dist/local-db.d.ts +6 -0
  81. package/dist/local-db.d.ts.map +1 -1
  82. package/dist/local-db.js +376 -4
  83. package/dist/local-db.js.map +1 -1
  84. package/dist/local-funnel.d.ts.map +1 -1
  85. package/dist/local-funnel.js +6 -5
  86. package/dist/local-funnel.js.map +1 -1
  87. package/dist/local-server.d.ts +30 -0
  88. package/dist/local-server.d.ts.map +1 -1
  89. package/dist/local-server.js +2898 -319
  90. package/dist/local-server.js.map +1 -1
  91. package/dist/managed-process-registry.d.ts +59 -0
  92. package/dist/managed-process-registry.d.ts.map +1 -0
  93. package/dist/managed-process-registry.js +390 -0
  94. package/dist/managed-process-registry.js.map +1 -0
  95. package/dist/mcp/claude-config-writer.d.ts +5 -5
  96. package/dist/mcp/claude-config-writer.d.ts.map +1 -1
  97. package/dist/mcp/claude-config-writer.js +19 -11
  98. package/dist/mcp/claude-config-writer.js.map +1 -1
  99. package/dist/mcp/index.d.ts +4 -2
  100. package/dist/mcp/index.d.ts.map +1 -1
  101. package/dist/mcp/index.js.map +1 -1
  102. package/dist/mcp/sync-cli-config.d.ts +42 -4
  103. package/dist/mcp/sync-cli-config.d.ts.map +1 -1
  104. package/dist/mcp/sync-cli-config.js +497 -17
  105. package/dist/mcp/sync-cli-config.js.map +1 -1
  106. package/dist/message-loop.d.ts.map +1 -1
  107. package/dist/message-loop.js +43 -1
  108. package/dist/message-loop.js.map +1 -1
  109. package/dist/mqtt-client.d.ts +34 -0
  110. package/dist/mqtt-client.d.ts.map +1 -1
  111. package/dist/mqtt-client.js +270 -45
  112. package/dist/mqtt-client.js.map +1 -1
  113. package/dist/mqtt-data-relay.d.ts +44 -0
  114. package/dist/mqtt-data-relay.d.ts.map +1 -0
  115. package/dist/mqtt-data-relay.js +106 -0
  116. package/dist/mqtt-data-relay.js.map +1 -0
  117. package/dist/orchestration/capabilities.d.ts +13 -0
  118. package/dist/orchestration/capabilities.d.ts.map +1 -0
  119. package/dist/orchestration/capabilities.js +152 -0
  120. package/dist/orchestration/capabilities.js.map +1 -0
  121. package/dist/orchestration/dispatch-executor.d.ts +83 -0
  122. package/dist/orchestration/dispatch-executor.d.ts.map +1 -0
  123. package/dist/orchestration/dispatch-executor.js +266 -0
  124. package/dist/orchestration/dispatch-executor.js.map +1 -0
  125. package/dist/orchestration/dispatch-hint.d.ts +134 -0
  126. package/dist/orchestration/dispatch-hint.d.ts.map +1 -0
  127. package/dist/orchestration/dispatch-hint.js +247 -0
  128. package/dist/orchestration/dispatch-hint.js.map +1 -0
  129. package/dist/orchestration/dispatch-runner.d.ts +106 -0
  130. package/dist/orchestration/dispatch-runner.d.ts.map +1 -0
  131. package/dist/orchestration/dispatch-runner.js +604 -0
  132. package/dist/orchestration/dispatch-runner.js.map +1 -0
  133. package/dist/orchestration/dispatch-tools.d.ts +167 -0
  134. package/dist/orchestration/dispatch-tools.d.ts.map +1 -0
  135. package/dist/orchestration/dispatch-tools.js +328 -0
  136. package/dist/orchestration/dispatch-tools.js.map +1 -0
  137. package/dist/orchestration/front-door-policy.d.ts +35 -10
  138. package/dist/orchestration/front-door-policy.d.ts.map +1 -1
  139. package/dist/orchestration/front-door-policy.js +30 -267
  140. package/dist/orchestration/front-door-policy.js.map +1 -1
  141. package/dist/orchestration/orchestrator-dispatch-prompt.d.ts +43 -0
  142. package/dist/orchestration/orchestrator-dispatch-prompt.d.ts.map +1 -0
  143. package/dist/orchestration/orchestrator-dispatch-prompt.js +267 -0
  144. package/dist/orchestration/orchestrator-dispatch-prompt.js.map +1 -0
  145. package/dist/orchestration/orchestrator-operating-prompt.d.ts +14 -0
  146. package/dist/orchestration/orchestrator-operating-prompt.d.ts.map +1 -1
  147. package/dist/orchestration/orchestrator-operating-prompt.js +157 -31
  148. package/dist/orchestration/orchestrator-operating-prompt.js.map +1 -1
  149. package/dist/orchestration/plan-import.d.ts +39 -0
  150. package/dist/orchestration/plan-import.d.ts.map +1 -0
  151. package/dist/orchestration/plan-import.js +547 -0
  152. package/dist/orchestration/plan-import.js.map +1 -0
  153. package/dist/orchestration/worker-operating-prompt.d.ts +2 -0
  154. package/dist/orchestration/worker-operating-prompt.d.ts.map +1 -1
  155. package/dist/orchestration/worker-operating-prompt.js +36 -46
  156. package/dist/orchestration/worker-operating-prompt.js.map +1 -1
  157. package/dist/orchestrator.d.ts +195 -3
  158. package/dist/orchestrator.d.ts.map +1 -1
  159. package/dist/orchestrator.js +1970 -432
  160. package/dist/orchestrator.js.map +1 -1
  161. package/dist/providers/anthropic.d.ts.map +1 -1
  162. package/dist/providers/anthropic.js +8 -4
  163. package/dist/providers/anthropic.js.map +1 -1
  164. package/dist/providers/claude-cli.d.ts.map +1 -1
  165. package/dist/providers/claude-cli.js +28 -3
  166. package/dist/providers/claude-cli.js.map +1 -1
  167. package/dist/providers/codex-cli.d.ts +10 -6
  168. package/dist/providers/codex-cli.d.ts.map +1 -1
  169. package/dist/providers/codex-cli.js +190 -17
  170. package/dist/providers/codex-cli.js.map +1 -1
  171. package/dist/providers/google.d.ts.map +1 -1
  172. package/dist/providers/google.js +15 -5
  173. package/dist/providers/google.js.map +1 -1
  174. package/dist/providers/index.d.ts +15 -1
  175. package/dist/providers/index.d.ts.map +1 -1
  176. package/dist/providers/index.js.map +1 -1
  177. package/dist/providers/openai.d.ts +1 -1
  178. package/dist/providers/openai.d.ts.map +1 -1
  179. package/dist/providers/openai.js +13 -5
  180. package/dist/providers/openai.js.map +1 -1
  181. package/dist/server-adapter.d.ts +8 -0
  182. package/dist/server-adapter.d.ts.map +1 -1
  183. package/dist/server-adapter.js +7 -0
  184. package/dist/server-adapter.js.map +1 -1
  185. package/dist/service-mode.d.ts +1 -1
  186. package/dist/service-mode.d.ts.map +1 -1
  187. package/dist/service-mode.js +64 -1
  188. package/dist/service-mode.js.map +1 -1
  189. package/dist/service-setup-only.d.ts +8 -0
  190. package/dist/service-setup-only.d.ts.map +1 -0
  191. package/dist/service-setup-only.js +37 -0
  192. package/dist/service-setup-only.js.map +1 -0
  193. package/dist/slash-commands.d.ts +21 -0
  194. package/dist/slash-commands.d.ts.map +1 -0
  195. package/dist/slash-commands.js +99 -0
  196. package/dist/slash-commands.js.map +1 -0
  197. package/dist/subagent/index.d.ts +4 -2
  198. package/dist/subagent/index.d.ts.map +1 -1
  199. package/dist/subagent/index.js.map +1 -1
  200. package/dist/summarization-pipeline.d.ts.map +1 -1
  201. package/dist/summarization-pipeline.js +1 -9
  202. package/dist/summarization-pipeline.js.map +1 -1
  203. package/dist/token-counter.d.ts.map +1 -1
  204. package/dist/token-counter.js +11 -4
  205. package/dist/token-counter.js.map +1 -1
  206. package/dist/tool-filter.d.ts.map +1 -1
  207. package/dist/tool-filter.js +10 -6
  208. package/dist/tool-filter.js.map +1 -1
  209. package/dist/tools/admin-tools.d.ts.map +1 -1
  210. package/dist/tools/admin-tools.js +13 -4
  211. package/dist/tools/admin-tools.js.map +1 -1
  212. package/dist/tools/run-command.d.ts.map +1 -1
  213. package/dist/tools/run-command.js +5 -1
  214. package/dist/tools/run-command.js.map +1 -1
  215. package/dist/tools/search-conversation-history.d.ts.map +1 -1
  216. package/dist/tools/search-conversation-history.js +12 -2
  217. package/dist/tools/search-conversation-history.js.map +1 -1
  218. package/dist/tools/todo-tasks.d.ts.map +1 -1
  219. package/dist/tools/todo-tasks.js +77 -5
  220. package/dist/tools/todo-tasks.js.map +1 -1
  221. package/dist/usage-log.d.ts +62 -0
  222. package/dist/usage-log.d.ts.map +1 -0
  223. package/dist/usage-log.js +98 -0
  224. package/dist/usage-log.js.map +1 -0
  225. package/dist/wizard-state.d.ts +13 -0
  226. package/dist/wizard-state.d.ts.map +1 -1
  227. package/dist/wizard-state.js +61 -3
  228. package/dist/wizard-state.js.map +1 -1
  229. package/dist/wizard-support.d.ts.map +1 -1
  230. package/dist/wizard-support.js +27 -1
  231. package/dist/wizard-support.js.map +1 -1
  232. package/dist/workflow-engine.d.ts +40 -1
  233. package/dist/workflow-engine.d.ts.map +1 -1
  234. package/dist/workflow-engine.js +753 -93
  235. package/dist/workflow-engine.js.map +1 -1
  236. 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: 'none',
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 scrubCodexAppServerEnv(baseEnv, extraEnv) {
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
- return [
108
- {
109
- type: 'text',
110
- text: extractLatestUserText(messages),
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
- const inputTokens = Number(last.inputTokens || 0) + Number(last.cachedInputTokens || 0);
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 { inputTokens, outputTokens };
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: String(settings?.reasoningEffort || DEFAULT_CODEX_SETTINGS.reasoningEffort),
189
- reasoningSummary: String(settings?.reasoningSummary || DEFAULT_CODEX_SETTINGS.reasoningSummary),
190
- personality: String(settings?.personality || DEFAULT_CODEX_SETTINGS.personality),
191
- serviceTier: String(settings?.serviceTier || DEFAULT_CODEX_SETTINGS.serviceTier),
192
- sandboxPolicy: String(settings?.sandboxPolicy || DEFAULT_CODEX_SETTINGS.sandboxPolicy),
193
- approvalPolicy: String(settings?.approvalPolicy || DEFAULT_CODEX_SETTINGS.approvalPolicy),
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.0.74' };
527
+ this.clientInfo = deps.clientInfo || { name: 'funolio-agent', version: '1.1.5' };
257
528
  }
258
- async runTurn(opts) {
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
- todoTaskId: opts.currentTodoTaskId,
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.forceFreshSession
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
- try {
302
- session.child.kill();
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
- catch {
305
- // best effort
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
- await this.prepareCliConfig();
319
- const child = this.spawnProcess(process.platform === 'win32' ? 'codex' : 'codex', ['app-server'], {
320
- stdio: 'pipe',
321
- shell: process.platform === 'win32',
322
- windowsHide: true,
323
- cwd,
324
- env,
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
- async runTurnInternal(session, opts) {
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.threadId = null;
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 turnStartResponse = await this.sendRequest(session, 'turn/start', {
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
- summary: codexSettings.reasoningSummary,
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 = normalizeDetail(detail);
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,