funolio-agent 1.0.53 → 1.1.65

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (276) hide show
  1. package/dist/approval.d.ts +1 -6
  2. package/dist/approval.d.ts.map +1 -1
  3. package/dist/approval.js +2 -7
  4. package/dist/approval.js.map +1 -1
  5. package/dist/auth/credential-reader.d.ts.map +1 -1
  6. package/dist/auth/credential-reader.js +4 -3
  7. package/dist/auth/credential-reader.js.map +1 -1
  8. package/dist/auth/token-refresh.d.ts +8 -0
  9. package/dist/auth/token-refresh.d.ts.map +1 -1
  10. package/dist/auth/token-refresh.js +82 -52
  11. package/dist/auth/token-refresh.js.map +1 -1
  12. package/dist/auto-organizer.d.ts.map +1 -1
  13. package/dist/auto-organizer.js +6 -7
  14. package/dist/auto-organizer.js.map +1 -1
  15. package/dist/bench-prefix.d.ts +16 -0
  16. package/dist/bench-prefix.d.ts.map +1 -0
  17. package/dist/bench-prefix.js +25 -0
  18. package/dist/bench-prefix.js.map +1 -0
  19. package/dist/bot-manager.d.ts +5 -1
  20. package/dist/bot-manager.d.ts.map +1 -1
  21. package/dist/bot-manager.js +46 -27
  22. package/dist/bot-manager.js.map +1 -1
  23. package/dist/chat-sync.d.ts +42 -0
  24. package/dist/chat-sync.d.ts.map +1 -0
  25. package/dist/chat-sync.js +95 -0
  26. package/dist/chat-sync.js.map +1 -0
  27. package/dist/clerk-model.d.ts +7 -0
  28. package/dist/clerk-model.d.ts.map +1 -1
  29. package/dist/clerk-model.js +42 -8
  30. package/dist/clerk-model.js.map +1 -1
  31. package/dist/cli-bootstrap-history.d.ts +10 -0
  32. package/dist/cli-bootstrap-history.d.ts.map +1 -0
  33. package/dist/cli-bootstrap-history.js +112 -0
  34. package/dist/cli-bootstrap-history.js.map +1 -0
  35. package/dist/cli-models.d.ts +8 -0
  36. package/dist/cli-models.d.ts.map +1 -0
  37. package/dist/cli-models.js +91 -0
  38. package/dist/cli-models.js.map +1 -0
  39. package/dist/cli-session-epoch.d.ts +13 -3
  40. package/dist/cli-session-epoch.d.ts.map +1 -1
  41. package/dist/cli-session-epoch.js +53 -4
  42. package/dist/cli-session-epoch.js.map +1 -1
  43. package/dist/cli-session-registry.d.ts +35 -0
  44. package/dist/cli-session-registry.d.ts.map +1 -0
  45. package/dist/cli-session-registry.js +177 -0
  46. package/dist/cli-session-registry.js.map +1 -0
  47. package/dist/cli.js +62 -0
  48. package/dist/cli.js.map +1 -1
  49. package/dist/codex-app-server-manager.d.ts +189 -0
  50. package/dist/codex-app-server-manager.d.ts.map +1 -0
  51. package/dist/codex-app-server-manager.js +1468 -0
  52. package/dist/codex-app-server-manager.js.map +1 -0
  53. package/dist/commands/init.d.ts.map +1 -1
  54. package/dist/commands/init.js +8 -30
  55. package/dist/commands/init.js.map +1 -1
  56. package/dist/commands/pool.d.ts +32 -0
  57. package/dist/commands/pool.d.ts.map +1 -1
  58. package/dist/commands/pool.js +145 -66
  59. package/dist/commands/pool.js.map +1 -1
  60. package/dist/commands/setup.d.ts +4 -1
  61. package/dist/commands/setup.d.ts.map +1 -1
  62. package/dist/commands/setup.js +9 -25
  63. package/dist/commands/setup.js.map +1 -1
  64. package/dist/commands/start.d.ts +21 -0
  65. package/dist/commands/start.d.ts.map +1 -1
  66. package/dist/commands/start.js +559 -63
  67. package/dist/commands/start.js.map +1 -1
  68. package/dist/commands/status.d.ts.map +1 -1
  69. package/dist/commands/status.js +5 -2
  70. package/dist/commands/status.js.map +1 -1
  71. package/dist/completion-marker.d.ts +7 -0
  72. package/dist/completion-marker.d.ts.map +1 -0
  73. package/dist/completion-marker.js +28 -0
  74. package/dist/completion-marker.js.map +1 -0
  75. package/dist/config.d.ts +7 -2
  76. package/dist/config.d.ts.map +1 -1
  77. package/dist/config.js +184 -60
  78. package/dist/config.js.map +1 -1
  79. package/dist/context-window.d.ts +37 -1
  80. package/dist/context-window.d.ts.map +1 -1
  81. package/dist/context-window.js +210 -17
  82. package/dist/context-window.js.map +1 -1
  83. package/dist/live-activity.d.ts +31 -0
  84. package/dist/live-activity.d.ts.map +1 -0
  85. package/dist/live-activity.js +36 -0
  86. package/dist/live-activity.js.map +1 -0
  87. package/dist/local-chat-execution.d.ts +114 -0
  88. package/dist/local-chat-execution.d.ts.map +1 -0
  89. package/dist/local-chat-execution.js +349 -0
  90. package/dist/local-chat-execution.js.map +1 -0
  91. package/dist/local-cli-pty-manager.d.ts +186 -0
  92. package/dist/local-cli-pty-manager.d.ts.map +1 -1
  93. package/dist/local-cli-pty-manager.js +2581 -164
  94. package/dist/local-cli-pty-manager.js.map +1 -1
  95. package/dist/local-conversation-gateway.d.ts +110 -0
  96. package/dist/local-conversation-gateway.d.ts.map +1 -0
  97. package/dist/local-conversation-gateway.js +175 -0
  98. package/dist/local-conversation-gateway.js.map +1 -0
  99. package/dist/local-data.d.ts +276 -5
  100. package/dist/local-data.d.ts.map +1 -1
  101. package/dist/local-data.js +1201 -86
  102. package/dist/local-data.js.map +1 -1
  103. package/dist/local-db.d.ts +6 -0
  104. package/dist/local-db.d.ts.map +1 -1
  105. package/dist/local-db.js +428 -2
  106. package/dist/local-db.js.map +1 -1
  107. package/dist/local-funnel.d.ts.map +1 -1
  108. package/dist/local-funnel.js +6 -5
  109. package/dist/local-funnel.js.map +1 -1
  110. package/dist/local-server.d.ts +55 -0
  111. package/dist/local-server.d.ts.map +1 -1
  112. package/dist/local-server.js +3281 -441
  113. package/dist/local-server.js.map +1 -1
  114. package/dist/managed-process-registry.d.ts +59 -0
  115. package/dist/managed-process-registry.d.ts.map +1 -0
  116. package/dist/managed-process-registry.js +390 -0
  117. package/dist/managed-process-registry.js.map +1 -0
  118. package/dist/mcp/claude-config-writer.d.ts +5 -5
  119. package/dist/mcp/claude-config-writer.d.ts.map +1 -1
  120. package/dist/mcp/claude-config-writer.js +19 -11
  121. package/dist/mcp/claude-config-writer.js.map +1 -1
  122. package/dist/mcp/index.d.ts +4 -2
  123. package/dist/mcp/index.d.ts.map +1 -1
  124. package/dist/mcp/index.js.map +1 -1
  125. package/dist/mcp/sync-cli-config.d.ts +42 -4
  126. package/dist/mcp/sync-cli-config.d.ts.map +1 -1
  127. package/dist/mcp/sync-cli-config.js +497 -17
  128. package/dist/mcp/sync-cli-config.js.map +1 -1
  129. package/dist/message-loop.d.ts +6 -0
  130. package/dist/message-loop.d.ts.map +1 -1
  131. package/dist/message-loop.js +281 -89
  132. package/dist/message-loop.js.map +1 -1
  133. package/dist/mqtt-client.d.ts +44 -1
  134. package/dist/mqtt-client.d.ts.map +1 -1
  135. package/dist/mqtt-client.js +284 -46
  136. package/dist/mqtt-client.js.map +1 -1
  137. package/dist/mqtt-data-relay.d.ts +44 -0
  138. package/dist/mqtt-data-relay.d.ts.map +1 -0
  139. package/dist/mqtt-data-relay.js +106 -0
  140. package/dist/mqtt-data-relay.js.map +1 -0
  141. package/dist/oauth.d.ts.map +1 -1
  142. package/dist/oauth.js +69 -29
  143. package/dist/oauth.js.map +1 -1
  144. package/dist/orchestration/capabilities.d.ts +13 -0
  145. package/dist/orchestration/capabilities.d.ts.map +1 -0
  146. package/dist/orchestration/capabilities.js +152 -0
  147. package/dist/orchestration/capabilities.js.map +1 -0
  148. package/dist/orchestration/dispatch-executor.d.ts +83 -0
  149. package/dist/orchestration/dispatch-executor.d.ts.map +1 -0
  150. package/dist/orchestration/dispatch-executor.js +266 -0
  151. package/dist/orchestration/dispatch-executor.js.map +1 -0
  152. package/dist/orchestration/dispatch-hint.d.ts +134 -0
  153. package/dist/orchestration/dispatch-hint.d.ts.map +1 -0
  154. package/dist/orchestration/dispatch-hint.js +247 -0
  155. package/dist/orchestration/dispatch-hint.js.map +1 -0
  156. package/dist/orchestration/dispatch-runner.d.ts +106 -0
  157. package/dist/orchestration/dispatch-runner.d.ts.map +1 -0
  158. package/dist/orchestration/dispatch-runner.js +604 -0
  159. package/dist/orchestration/dispatch-runner.js.map +1 -0
  160. package/dist/orchestration/dispatch-tools.d.ts +167 -0
  161. package/dist/orchestration/dispatch-tools.d.ts.map +1 -0
  162. package/dist/orchestration/dispatch-tools.js +328 -0
  163. package/dist/orchestration/dispatch-tools.js.map +1 -0
  164. package/dist/orchestration/front-door-policy.d.ts +35 -10
  165. package/dist/orchestration/front-door-policy.d.ts.map +1 -1
  166. package/dist/orchestration/front-door-policy.js +30 -267
  167. package/dist/orchestration/front-door-policy.js.map +1 -1
  168. package/dist/orchestration/orchestrator-dispatch-prompt.d.ts +43 -0
  169. package/dist/orchestration/orchestrator-dispatch-prompt.d.ts.map +1 -0
  170. package/dist/orchestration/orchestrator-dispatch-prompt.js +267 -0
  171. package/dist/orchestration/orchestrator-dispatch-prompt.js.map +1 -0
  172. package/dist/orchestration/orchestrator-operating-prompt.d.ts +15 -0
  173. package/dist/orchestration/orchestrator-operating-prompt.d.ts.map +1 -1
  174. package/dist/orchestration/orchestrator-operating-prompt.js +206 -20
  175. package/dist/orchestration/orchestrator-operating-prompt.js.map +1 -1
  176. package/dist/orchestration/plan-import.d.ts +39 -0
  177. package/dist/orchestration/plan-import.d.ts.map +1 -0
  178. package/dist/orchestration/plan-import.js +547 -0
  179. package/dist/orchestration/plan-import.js.map +1 -0
  180. package/dist/orchestration/validation.d.ts +40 -0
  181. package/dist/orchestration/validation.d.ts.map +1 -0
  182. package/dist/orchestration/validation.js +203 -0
  183. package/dist/orchestration/validation.js.map +1 -0
  184. package/dist/orchestration/worker-operating-prompt.d.ts +2 -0
  185. package/dist/orchestration/worker-operating-prompt.d.ts.map +1 -1
  186. package/dist/orchestration/worker-operating-prompt.js +36 -46
  187. package/dist/orchestration/worker-operating-prompt.js.map +1 -1
  188. package/dist/orchestrator.d.ts +214 -33
  189. package/dist/orchestrator.d.ts.map +1 -1
  190. package/dist/orchestrator.js +2200 -1100
  191. package/dist/orchestrator.js.map +1 -1
  192. package/dist/providers/anthropic.d.ts.map +1 -1
  193. package/dist/providers/anthropic.js +8 -4
  194. package/dist/providers/anthropic.js.map +1 -1
  195. package/dist/providers/claude-cli-prompt.d.ts.map +1 -1
  196. package/dist/providers/claude-cli-prompt.js +49 -5
  197. package/dist/providers/claude-cli-prompt.js.map +1 -1
  198. package/dist/providers/claude-cli.d.ts.map +1 -1
  199. package/dist/providers/claude-cli.js +81 -5
  200. package/dist/providers/claude-cli.js.map +1 -1
  201. package/dist/providers/codex-cli.d.ts +10 -6
  202. package/dist/providers/codex-cli.d.ts.map +1 -1
  203. package/dist/providers/codex-cli.js +204 -26
  204. package/dist/providers/codex-cli.js.map +1 -1
  205. package/dist/providers/google.d.ts.map +1 -1
  206. package/dist/providers/google.js +15 -5
  207. package/dist/providers/google.js.map +1 -1
  208. package/dist/providers/index.d.ts +15 -1
  209. package/dist/providers/index.d.ts.map +1 -1
  210. package/dist/providers/index.js.map +1 -1
  211. package/dist/providers/openai.d.ts +1 -1
  212. package/dist/providers/openai.d.ts.map +1 -1
  213. package/dist/providers/openai.js +13 -5
  214. package/dist/providers/openai.js.map +1 -1
  215. package/dist/response-guard.js +1 -1
  216. package/dist/response-guard.js.map +1 -1
  217. package/dist/server-adapter.d.ts +8 -0
  218. package/dist/server-adapter.d.ts.map +1 -1
  219. package/dist/server-adapter.js +7 -0
  220. package/dist/server-adapter.js.map +1 -1
  221. package/dist/service-mode.d.ts +1 -1
  222. package/dist/service-mode.d.ts.map +1 -1
  223. package/dist/service-mode.js +64 -1
  224. package/dist/service-mode.js.map +1 -1
  225. package/dist/service-setup-only.d.ts +8 -0
  226. package/dist/service-setup-only.d.ts.map +1 -0
  227. package/dist/service-setup-only.js +37 -0
  228. package/dist/service-setup-only.js.map +1 -0
  229. package/dist/slash-commands.d.ts +21 -0
  230. package/dist/slash-commands.d.ts.map +1 -0
  231. package/dist/slash-commands.js +99 -0
  232. package/dist/slash-commands.js.map +1 -0
  233. package/dist/subagent/index.d.ts +4 -2
  234. package/dist/subagent/index.d.ts.map +1 -1
  235. package/dist/subagent/index.js.map +1 -1
  236. package/dist/summarization-pipeline.d.ts.map +1 -1
  237. package/dist/summarization-pipeline.js +1 -9
  238. package/dist/summarization-pipeline.js.map +1 -1
  239. package/dist/token-counter.d.ts.map +1 -1
  240. package/dist/token-counter.js +11 -4
  241. package/dist/token-counter.js.map +1 -1
  242. package/dist/tool-filter.d.ts.map +1 -1
  243. package/dist/tool-filter.js +10 -6
  244. package/dist/tool-filter.js.map +1 -1
  245. package/dist/tools/admin-tools.d.ts.map +1 -1
  246. package/dist/tools/admin-tools.js +20 -5
  247. package/dist/tools/admin-tools.js.map +1 -1
  248. package/dist/tools/index.d.ts.map +1 -1
  249. package/dist/tools/index.js +2 -1
  250. package/dist/tools/index.js.map +1 -1
  251. package/dist/tools/run-command.d.ts.map +1 -1
  252. package/dist/tools/run-command.js +5 -1
  253. package/dist/tools/run-command.js.map +1 -1
  254. package/dist/tools/search-conversation-history.d.ts +16 -0
  255. package/dist/tools/search-conversation-history.d.ts.map +1 -0
  256. package/dist/tools/search-conversation-history.js +334 -0
  257. package/dist/tools/search-conversation-history.js.map +1 -0
  258. package/dist/tools/todo-tasks.d.ts.map +1 -1
  259. package/dist/tools/todo-tasks.js +77 -5
  260. package/dist/tools/todo-tasks.js.map +1 -1
  261. package/dist/usage-log.d.ts +62 -0
  262. package/dist/usage-log.d.ts.map +1 -0
  263. package/dist/usage-log.js +98 -0
  264. package/dist/usage-log.js.map +1 -0
  265. package/dist/wizard-state.d.ts +20 -0
  266. package/dist/wizard-state.d.ts.map +1 -1
  267. package/dist/wizard-state.js +90 -3
  268. package/dist/wizard-state.js.map +1 -1
  269. package/dist/wizard-support.d.ts.map +1 -1
  270. package/dist/wizard-support.js +27 -1
  271. package/dist/wizard-support.js.map +1 -1
  272. package/dist/workflow-engine.d.ts +44 -2
  273. package/dist/workflow-engine.d.ts.map +1 -1
  274. package/dist/workflow-engine.js +932 -111
  275. package/dist/workflow-engine.js.map +1 -1
  276. package/package.json +2 -2
@@ -34,18 +34,344 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.LocalCliPtySessionManager = void 0;
37
+ exports.detectCliInteractiveAuthFailureForTest = detectCliInteractiveAuthFailureForTest;
38
+ exports.normalizeClaudeFreshSessionAuthErrorForTest = normalizeClaudeFreshSessionAuthErrorForTest;
39
+ exports.shouldResetClaudeSessionForAuthChangeForTest = shouldResetClaudeSessionForAuthChangeForTest;
40
+ exports.shouldRecycleClaudeSessionForFreshAuthForTest = shouldRecycleClaudeSessionForFreshAuthForTest;
41
+ exports.resolveClaudeLaunchSessionIdsForTest = resolveClaudeLaunchSessionIdsForTest;
42
+ exports.shouldContinueWaitingForFreshClaudeSessionForTest = shouldContinueWaitingForFreshClaudeSessionForTest;
43
+ exports.buildTurnPromptForTest = buildTurnPromptForTest;
44
+ exports.shouldIgnoreClaudeRecordForCurrentTurnForTest = shouldIgnoreClaudeRecordForCurrentTurnForTest;
45
+ exports.stripAnsi = stripAnsi;
46
+ exports.resolveConptyOverwrites = resolveConptyOverwrites;
47
+ exports.getProviderSessionRootForTest = getProviderSessionRootForTest;
48
+ exports.emitAssistantChunkSequenceForTest = emitAssistantChunkSequenceForTest;
37
49
  exports.parseClaudeSessionRecord = parseClaudeSessionRecord;
38
50
  exports.parseCodexSessionRecord = parseCodexSessionRecord;
51
+ exports.trimPassthroughSlashChromeForTest = trimPassthroughSlashChromeForTest;
52
+ exports.finalizePassthroughContentForTest = finalizePassthroughContentForTest;
39
53
  exports.getLocalCliPtySessionManager = getLocalCliPtySessionManager;
54
+ exports.runLocalCliPtyHealthCheck = runLocalCliPtyHealthCheck;
55
+ exports.runLocalCliPtyTurnHealthCheck = runLocalCliPtyTurnHealthCheck;
56
+ exports.runLocalCliPtyProbe = runLocalCliPtyProbe;
40
57
  const fs = __importStar(require("fs"));
41
58
  const os = __importStar(require("os"));
42
59
  const path = __importStar(require("path"));
60
+ const module_1 = require("module");
43
61
  const claude_cli_prompt_1 = require("./providers/claude-cli-prompt");
62
+ const live_activity_1 = require("./live-activity");
63
+ const completion_marker_1 = require("./completion-marker");
64
+ const sync_cli_config_1 = require("./mcp/sync-cli-config");
65
+ const credential_reader_1 = require("./auth/credential-reader");
66
+ const token_refresh_1 = require("./auth/token-refresh");
67
+ const managed_process_registry_1 = require("./managed-process-registry");
68
+ const CLAUDE_FRESH_SESSION_STARTUP_INITIAL_TIMEOUT_MS = 10_000;
69
+ const CLAUDE_FRESH_SESSION_STARTUP_PROGRESS_STALL_MS = 10_000;
70
+ const CLAUDE_FRESH_SESSION_STARTUP_MAX_TIMEOUT_MS = 30_000;
71
+ const CLAUDE_PTY_INACTIVITY_FAIL_TIMEOUT_MS = 60_000;
72
+ const PASSTHROUGH_FIRST_BYTE_TIMEOUT_MS = 10_000;
73
+ const PASSTHROUGH_TRAILING_IDLE_MS = 2_000;
74
+ const CLI_WARM_TIMEOUT_MS = 10_000;
75
+ function getPtyInactivityFailTimeoutMs(provider) {
76
+ if (provider === 'claude-cli')
77
+ return CLAUDE_PTY_INACTIVITY_FAIL_TIMEOUT_MS;
78
+ return null;
79
+ }
44
80
  let _ptyModule = null;
45
81
  let _manager = null;
46
82
  function delay(ms) {
47
83
  return new Promise((resolve) => setTimeout(resolve, ms));
48
84
  }
85
+ function fileFingerprint(filePath) {
86
+ if (!filePath)
87
+ return null;
88
+ try {
89
+ const stat = fs.statSync(filePath);
90
+ return [stat.dev, stat.ino, stat.size, Math.floor(stat.mtimeMs)].join(':');
91
+ }
92
+ catch {
93
+ return null;
94
+ }
95
+ }
96
+ function currentClaudeAuthFingerprint() {
97
+ return fileFingerprint((0, sync_cli_config_1.resolveClaudeCredentialsSourcePath)());
98
+ }
99
+ function appendRecentTerminalOutput(existing, chunk) {
100
+ const combined = `${existing || ''}${normalizeTerminalChunk(chunk || '')}`;
101
+ return combined.length > 4000 ? combined.slice(-4000) : combined;
102
+ }
103
+ const CLI_INTERACTIVE_AUTH_FAILURE_RE = /\b(not logged in|please run \/login|login required|unauthorized|invalid authorization|invalid api key|missing api key|authentication required|invalid authentication credentials|invalid bearer token|token expired|session expired|credentials expired|expired credentials|reauthenticate)\b/i;
104
+ function isCliInteractiveAuthFailureText(text) {
105
+ return CLI_INTERACTIVE_AUTH_FAILURE_RE.test(text);
106
+ }
107
+ function detectCliInteractiveAuthFailure(provider, terminalOutput) {
108
+ if (!isCliInteractiveAuthFailureText(terminalOutput))
109
+ return null;
110
+ if (provider === 'claude-cli') {
111
+ return {
112
+ message: 'Claude authentication is required before this request can continue.',
113
+ detail: 'Claude authentication is required. Opening subscription login...',
114
+ providerId: 'claude-cli',
115
+ cli: 'claude',
116
+ };
117
+ }
118
+ if (provider === 'codex-cli') {
119
+ return {
120
+ message: 'Codex authentication is required before this request can continue.',
121
+ detail: 'Codex authentication is required. Opening subscription login...',
122
+ providerId: 'codex-cli',
123
+ cli: 'codex',
124
+ };
125
+ }
126
+ return null;
127
+ }
128
+ function detectCliInteractiveAuthFailureAcrossOutputs(provider, ...terminalOutputs) {
129
+ for (const output of terminalOutputs) {
130
+ const auth = detectCliInteractiveAuthFailure(provider, String(output || ''));
131
+ if (auth)
132
+ return auth;
133
+ }
134
+ return null;
135
+ }
136
+ function buildCliInteractiveAuthError(provider, terminalOutput) {
137
+ const auth = detectCliInteractiveAuthFailure(provider, terminalOutput);
138
+ if (!auth)
139
+ return null;
140
+ const err = new Error(auth.message);
141
+ err.authRequired = true;
142
+ err.providerId = auth.providerId;
143
+ err.cli = auth.cli;
144
+ return err;
145
+ }
146
+ function buildCliInteractiveAuthErrorFromOutputs(provider, ...terminalOutputs) {
147
+ const auth = detectCliInteractiveAuthFailureAcrossOutputs(provider, ...terminalOutputs);
148
+ if (!auth)
149
+ return null;
150
+ const err = new Error(auth.message);
151
+ err.authRequired = true;
152
+ err.providerId = auth.providerId;
153
+ err.cli = auth.cli;
154
+ return err;
155
+ }
156
+ function detectCliInteractiveAuthFailureForTest(provider, ...terminalOutputs) {
157
+ return detectCliInteractiveAuthFailureAcrossOutputs(provider, ...terminalOutputs);
158
+ }
159
+ function buildAbortError() {
160
+ const abortErr = new Error('PTY turn aborted');
161
+ abortErr.name = 'AbortError';
162
+ return abortErr;
163
+ }
164
+ function buildCliAuthRequiredError(provider) {
165
+ if (provider === 'claude-cli') {
166
+ const err = new Error('Claude authentication is required before this request can continue.');
167
+ err.authRequired = true;
168
+ err.providerId = 'claude-cli';
169
+ err.cli = 'claude';
170
+ return err;
171
+ }
172
+ const err = new Error('Codex authentication is required before this request can continue.');
173
+ err.authRequired = true;
174
+ err.providerId = 'codex-cli';
175
+ err.cli = 'codex';
176
+ return err;
177
+ }
178
+ function normalizeClaudeFreshSessionAuthError(err) {
179
+ const message = String(err?.message || '').trim();
180
+ if (!message)
181
+ return null;
182
+ const normalized = message.toLowerCase();
183
+ if (err instanceof token_refresh_1.TokenRefreshError
184
+ || normalized.includes('invalid_grant')
185
+ || normalized.includes('refresh token not found or invalid')
186
+ || normalized.includes('oauth token refresh failed')
187
+ || normalized.includes('invalid authentication credentials')
188
+ || normalized.includes('failed to authenticate')) {
189
+ return buildCliAuthRequiredError('claude-cli');
190
+ }
191
+ return null;
192
+ }
193
+ function normalizeClaudeFreshSessionAuthErrorForTest(err) {
194
+ const normalized = normalizeClaudeFreshSessionAuthError(err);
195
+ if (!normalized)
196
+ return null;
197
+ return {
198
+ message: normalized.message,
199
+ providerId: normalized.providerId,
200
+ cli: normalized.cli,
201
+ };
202
+ }
203
+ async function ensureClaudeAuthReadyForFreshSession() {
204
+ const credential = (0, credential_reader_1.readClaudeCredentials)();
205
+ if (!credential?.accessToken) {
206
+ throw buildCliAuthRequiredError('claude-cli');
207
+ }
208
+ if (!(0, credential_reader_1.isExpired)(credential)) {
209
+ return;
210
+ }
211
+ if (!credential.refreshToken) {
212
+ throw buildCliAuthRequiredError('claude-cli');
213
+ }
214
+ try {
215
+ await (0, token_refresh_1.refreshToken)(credential);
216
+ }
217
+ catch (err) {
218
+ const authError = normalizeClaudeFreshSessionAuthError(err);
219
+ if (authError)
220
+ throw authError;
221
+ throw err;
222
+ }
223
+ }
224
+ function buildClaudeFreshSessionStartupError(sessionId, elapsedMs) {
225
+ const err = new Error(`claude-cli fresh session startup timed out after ${Math.floor(elapsedMs / 1000)}s waiting for transcript file`
226
+ + (sessionId ? ` (${sessionId})` : ''));
227
+ err.name = 'ClaudeFreshSessionStartupTimeoutError';
228
+ err.code = 'CLAUDE_FRESH_SESSION_STARTUP_TIMEOUT';
229
+ return err;
230
+ }
231
+ function getTerminalAuthFailure(session, activeTurn) {
232
+ const visibleFailure = buildCliInteractiveAuthError(session.provider, activeTurn.visibleOutput);
233
+ if (visibleFailure)
234
+ return visibleFailure;
235
+ return buildCliInteractiveAuthErrorFromOutputs(session.provider, normalizeTerminalChunk(activeTurn.rawOutput), session.recentTerminalOutput);
236
+ }
237
+ function hasClaudeAuthFingerprintChanged(provider, storedFingerprint, currentFingerprint = currentClaudeAuthFingerprint()) {
238
+ if (provider !== 'claude-cli')
239
+ return false;
240
+ return storedFingerprint !== currentFingerprint;
241
+ }
242
+ function shouldResetClaudeSessionForAuthChange(session) {
243
+ return hasClaudeAuthFingerprintChanged(session.provider, session.claudeAuthFingerprint);
244
+ }
245
+ function syncClaudeSessionCredentialsBackToCanonical(session) {
246
+ if (session.provider !== 'claude-cli')
247
+ return;
248
+ try {
249
+ const botHomeDir = path.dirname(session.sessionFilesRoot);
250
+ (0, sync_cli_config_1.promoteClaudeBotHomeCredentialsToCanonical)(botHomeDir);
251
+ }
252
+ catch {
253
+ // best effort
254
+ }
255
+ session.claudeAuthFingerprint = currentClaudeAuthFingerprint();
256
+ }
257
+ function clearSessionWarmState(session) {
258
+ session.warmPromise = null;
259
+ session.warmRequestedAtMs = null;
260
+ session.warmReadyAtMs = null;
261
+ }
262
+ function isIdleClaudeSession(session) {
263
+ return session.provider === 'claude-cli' && !session.closed && session.activeTurn == null;
264
+ }
265
+ function shouldRecycleClaudeSessionForFreshAuth(session) {
266
+ // A healthy idle Claude PTY is exactly what we want to reuse. Recycling it
267
+ // here forced the next turn down the expensive --resume/fresh-session path.
268
+ return false;
269
+ }
270
+ function resolveClaudeLaunchSessionIds(resumeSessionId, newSessionId) {
271
+ const normalizedResumeSessionId = String(resumeSessionId || '').trim() || null;
272
+ const normalizedNewSessionId = String(newSessionId || '').trim() || null;
273
+ // Preserve Claude-side session history whenever Funolio has a stored session
274
+ // id. A fresh --session-id is only valid for a genuinely new epoch; recurring
275
+ // turns must relaunch with --resume if the live PTY is no longer available.
276
+ return {
277
+ resumeSessionId: normalizedResumeSessionId,
278
+ newSessionId: normalizedResumeSessionId ? null : normalizedNewSessionId,
279
+ knownSessionId: normalizedResumeSessionId || normalizedNewSessionId,
280
+ };
281
+ }
282
+ function shouldResetClaudeSessionForAuthChangeForTest(provider, storedFingerprint, currentFingerprint) {
283
+ return hasClaudeAuthFingerprintChanged(provider, storedFingerprint, currentFingerprint);
284
+ }
285
+ function shouldRecycleClaudeSessionForFreshAuthForTest(input) {
286
+ return shouldRecycleClaudeSessionForFreshAuth({
287
+ key: 'test::session',
288
+ conversationId: 'conversation',
289
+ botId: 'bot',
290
+ topicId: null,
291
+ warmRuntimeMode: null,
292
+ provider: input.provider,
293
+ cwd: process.cwd(),
294
+ useConpty: false,
295
+ pty: { write() { }, kill() { }, on() { } },
296
+ createdAtMs: 0,
297
+ lastUsedAtMs: 0,
298
+ launchSnapshot: new Set(),
299
+ sessionFilesRoot: '',
300
+ sessionId: null,
301
+ sessionFilePath: null,
302
+ sessionFileOffset: 0,
303
+ sessionFileCarry: '',
304
+ readyPromise: Promise.resolve(),
305
+ readyResolved: true,
306
+ waitForNextSendMs: 0,
307
+ startupDelayMs: 0,
308
+ startupDelayApplied: false,
309
+ submitDelayMs: 0,
310
+ currentPromptLocator: null,
311
+ currentPromptStartedAtMs: 0,
312
+ activeTurn: input.hasActiveTurn ? {} : null,
313
+ warmPromise: input.hasWarmPromise ? Promise.resolve() : null,
314
+ warmRequestedAtMs: input.hasWarmPromise ? Date.now() : null,
315
+ warmReadyAtMs: input.hasWarmReadyAtMs ? Date.now() : null,
316
+ recentTerminalOutput: '',
317
+ closed: !!input.closed,
318
+ exitReason: null,
319
+ childFollowers: new Map(),
320
+ childSnapshot: new Set(),
321
+ claudeAuthFingerprint: null,
322
+ runtimeHomeDir: null,
323
+ chain: Promise.resolve(),
324
+ });
325
+ }
326
+ function resolveClaudeLaunchSessionIdsForTest(resumeSessionId, newSessionId) {
327
+ return resolveClaudeLaunchSessionIds(resumeSessionId, newSessionId);
328
+ }
329
+ function shouldContinueWaitingForFreshClaudeSession(session, activeTurn, startedAtMs, now) {
330
+ const elapsedMs = now - startedAtMs;
331
+ if (elapsedMs < CLAUDE_FRESH_SESSION_STARTUP_INITIAL_TIMEOUT_MS)
332
+ return true;
333
+ if (elapsedMs >= CLAUDE_FRESH_SESSION_STARTUP_MAX_TIMEOUT_MS)
334
+ return false;
335
+ if (session.closed)
336
+ return false;
337
+ const postPromptProgressAtMs = Math.max(activeTurn.lastDataAtMs, activeTurn.lastMeaningfulPtyDataAtMs);
338
+ if (postPromptProgressAtMs > startedAtMs) {
339
+ return now - postPromptProgressAtMs <= CLAUDE_FRESH_SESSION_STARTUP_PROGRESS_STALL_MS;
340
+ }
341
+ return true;
342
+ }
343
+ function shouldContinueWaitingForFreshClaudeSessionForTest(input) {
344
+ const startedAtMs = input.startedAtMs;
345
+ return shouldContinueWaitingForFreshClaudeSession({
346
+ provider: 'claude-cli',
347
+ closed: !!input.sessionClosed,
348
+ readyResolved: !!input.readyResolved,
349
+ }, {
350
+ lastDataAtMs: input.lastDataAtMs ?? startedAtMs,
351
+ lastMeaningfulPtyDataAtMs: input.lastMeaningfulPtyDataAtMs ?? startedAtMs,
352
+ }, startedAtMs, input.nowMs);
353
+ }
354
+ function throwIfAborted(signal) {
355
+ if (!signal?.aborted)
356
+ return;
357
+ throw buildAbortError();
358
+ }
359
+ async function delayWithAbort(ms, signal) {
360
+ if (!signal) {
361
+ await delay(ms);
362
+ return;
363
+ }
364
+ await Promise.race([
365
+ delay(ms),
366
+ new Promise((_, reject) => {
367
+ const onAbort = () => {
368
+ signal.removeEventListener('abort', onAbort);
369
+ reject(buildAbortError());
370
+ };
371
+ signal.addEventListener('abort', onAbort, { once: true });
372
+ }),
373
+ ]);
374
+ }
49
375
  function sessionKey(conversationId, botId) {
50
376
  return `${conversationId}::${botId}`;
51
377
  }
@@ -95,16 +421,99 @@ function loadNodePtyModule() {
95
421
  for (const candidate of candidates) {
96
422
  if (!fs.existsSync(candidate))
97
423
  continue;
98
- _ptyModule = dynamicRequire(candidate);
99
- return _ptyModule;
424
+ try {
425
+ // Use dynamic require with the absolute path — this preserves __dirname
426
+ // resolution inside the PTY module for finding native .node addons.
427
+ _ptyModule = dynamicRequire(candidate);
428
+ return _ptyModule;
429
+ }
430
+ catch {
431
+ // Try createRequire fallback if dynamic require fails
432
+ const ptyRequire = (0, module_1.createRequire)(candidate);
433
+ _ptyModule = ptyRequire(candidate);
434
+ return _ptyModule;
435
+ }
100
436
  }
101
437
  throw new Error(`Failed to load PTY runtime. Tried package import and packaged resource candidates. Original error: ${firstErr instanceof Error ? firstErr.message : String(firstErr)}`);
102
438
  }
103
439
  }
440
+ function parseBracketedSections(text) {
441
+ const lines = text.split('\n');
442
+ const sections = [];
443
+ let currentHeading = null;
444
+ let bodyLines = [];
445
+ const flush = () => {
446
+ if (!currentHeading)
447
+ return;
448
+ sections.push({
449
+ heading: currentHeading,
450
+ body: bodyLines.join('\n').trim(),
451
+ });
452
+ };
453
+ for (const line of lines) {
454
+ const trimmed = line.trim();
455
+ const match = /^\[(.+?)\]$/.exec(trimmed);
456
+ if (match) {
457
+ flush();
458
+ currentHeading = match[1];
459
+ bodyLines = [];
460
+ continue;
461
+ }
462
+ if (currentHeading) {
463
+ bodyLines.push(line);
464
+ }
465
+ }
466
+ flush();
467
+ return sections;
468
+ }
469
+ function trimSectionBody(body, maxChars) {
470
+ const trimmed = body.trim();
471
+ if (trimmed.length <= maxChars)
472
+ return trimmed;
473
+ return `${trimmed.slice(0, Math.max(0, maxChars - 32)).trim()}\n[Context trimmed for direct Codex PTY]`;
474
+ }
475
+ function compactCodexDirectSystemPrompt(systemPrompt) {
476
+ const trimmed = systemPrompt.trim();
477
+ if (!trimmed)
478
+ return '';
479
+ const sections = parseBracketedSections(trimmed);
480
+ if (sections.length === 0) {
481
+ return trimmed.length <= 900
482
+ ? trimmed
483
+ : `${trimmed.slice(0, 820).trim()}\n\n[Context trimmed for direct Codex PTY]`;
484
+ }
485
+ const priorities = [
486
+ { heading: 'Bot Identity', maxChars: 360 },
487
+ { heading: 'Project Overview', maxChars: 140 },
488
+ { heading: 'Recent Messages (Last 2 Turns)', maxChars: 120 },
489
+ { heading: 'Recent Messages (Last 3 Turns)', maxChars: 140 },
490
+ { heading: 'Recent Messages (Last 4 Turns)', maxChars: 160 },
491
+ { heading: 'Recent Messages (Last 5 Turns)', maxChars: 180 },
492
+ ];
493
+ const selected = [];
494
+ for (const priority of priorities) {
495
+ const section = sections.find((candidate) => candidate.heading === priority.heading);
496
+ if (!section)
497
+ continue;
498
+ const body = trimSectionBody(section.body, priority.maxChars);
499
+ if (!body)
500
+ continue;
501
+ selected.push(`[${section.heading}]\n${body}`);
502
+ }
503
+ const compacted = selected.join('\n\n').trim();
504
+ if (!compacted) {
505
+ return trimmed.length <= 900
506
+ ? trimmed
507
+ : `${trimmed.slice(0, 820).trim()}\n\n[Context trimmed for direct Codex PTY]`;
508
+ }
509
+ return compacted.length <= 900
510
+ ? compacted
511
+ : `${compacted.slice(0, 820).trim()}\n\n[Context trimmed for direct Codex PTY]`;
512
+ }
104
513
  function buildCodexSeedPrompt(systemPrompt, messages) {
105
514
  let fullPrompt = '';
106
515
  if (systemPrompt) {
107
- fullPrompt += `[System Instructions]\n${systemPrompt}\n\n`;
516
+ fullPrompt += `[System Instructions]\n${compactCodexDirectSystemPrompt(systemPrompt)}\n\n`;
108
517
  }
109
518
  const lastMessage = messages.length > 0 ? messages[messages.length - 1] : null;
110
519
  if (messages.length > 0) {
@@ -123,43 +532,511 @@ function buildCodexSeedPrompt(systemPrompt, messages) {
123
532
  }
124
533
  if (lastMessage?.role === 'user') {
125
534
  const prompt = typeof lastMessage.content === 'string' ? lastMessage.content : JSON.stringify(lastMessage.content);
126
- fullPrompt += `[User Request]\n${prompt}`;
535
+ fullPrompt += `[User Request]\n${prompt}\n\n${completion_marker_1.CLI_COMPLETION_INSTRUCTION}`;
127
536
  }
128
537
  else if (lastMessage) {
129
538
  const content = typeof lastMessage.content === 'string' ? lastMessage.content : JSON.stringify(lastMessage.content);
130
- fullPrompt += `[Latest Message]\n${content}\n\nContinue from the transcript above and respond appropriately.`;
539
+ fullPrompt += `[Latest Message]\n${content}\n\nContinue from the transcript above and respond appropriately. ${completion_marker_1.CLI_COMPLETION_INSTRUCTION}`;
131
540
  }
132
541
  else {
133
- fullPrompt += '[User Request]\n';
542
+ fullPrompt += `[User Request]\n\n${completion_marker_1.CLI_COMPLETION_INSTRUCTION}`;
134
543
  }
135
544
  return fullPrompt;
136
545
  }
137
- function buildTurnPrompt(provider, systemPrompt, messages, freshSession) {
546
+ function extractTextAndImagePaths(content) {
547
+ if (typeof content === 'string')
548
+ return { text: content, imagePaths: [] };
549
+ const textParts = [];
550
+ const imagePaths = [];
551
+ // Write images to OS temp dir, not project workspace
552
+ const tmpDir = path.join(os.tmpdir(), 'funolio-images');
553
+ for (const part of content) {
554
+ if (part.type === 'text') {
555
+ textParts.push(part.text);
556
+ }
557
+ else if (part.type === 'image' && part.data) {
558
+ const ext = (part.mimeType || 'image/png').split('/')[1] || 'png';
559
+ const filename = `funolio-img-${Date.now()}-${Math.random().toString(36).slice(2, 6)}.${ext}`;
560
+ const filePath = path.join(tmpDir, filename);
561
+ try {
562
+ if (!fs.existsSync(tmpDir))
563
+ fs.mkdirSync(tmpDir, { recursive: true });
564
+ fs.writeFileSync(filePath, Buffer.from(part.data, 'base64'));
565
+ imagePaths.push(filePath);
566
+ }
567
+ catch {
568
+ // best effort
569
+ }
570
+ }
571
+ }
572
+ return { text: textParts.join('\n'), imagePaths };
573
+ }
574
+ function buildClaudeImagePathText(imagePaths) {
575
+ if (imagePaths.length === 0)
576
+ return '';
577
+ if (imagePaths.length === 1)
578
+ return `\n\nAnalyze this image: ${imagePaths[0]}.`;
579
+ return `\n\n${imagePaths.map((p, index) => `Analyze image ${index + 1}: ${p}.`).join(' ')}`;
580
+ }
581
+ function buildImageNote(provider, imagePaths) {
582
+ if (provider === 'claude-cli') {
583
+ return buildClaudeImagePathText(imagePaths);
584
+ }
585
+ if (imagePaths.length === 0)
586
+ return '';
587
+ return `\n\n[The user attached ${imagePaths.length} image(s). Read them with your Read tool:\n${imagePaths.map((p) => ` - ${p}`).join('\n')}\n]`;
588
+ }
589
+ function flattenMessagesForImages(provider, messages) {
590
+ // Extract images from the last user message and convert content to text + image note
591
+ const lastMessage = messages.length > 0 ? messages[messages.length - 1] : null;
592
+ if (!lastMessage || lastMessage.role !== 'user' || typeof lastMessage.content === 'string') {
593
+ return { messages, imageNote: '' };
594
+ }
595
+ const { text, imagePaths } = extractTextAndImagePaths(lastMessage.content);
596
+ const flatMessages = [
597
+ ...messages.slice(0, -1),
598
+ {
599
+ ...lastMessage,
600
+ content: provider === 'claude-cli'
601
+ ? `${text}${buildClaudeImagePathText(imagePaths)}`
602
+ : text,
603
+ },
604
+ ];
605
+ return {
606
+ messages: flatMessages,
607
+ imageNote: provider === 'claude-cli' ? '' : buildImageNote(provider, imagePaths),
608
+ };
609
+ }
610
+ function buildTurnPrompt(provider, systemPrompt, messages, freshSession, cwd = '.') {
138
611
  if (!freshSession) {
139
612
  const lastMessage = messages.length > 0 ? messages[messages.length - 1] : null;
140
613
  if (lastMessage?.role === 'user') {
141
- return typeof lastMessage.content === 'string'
142
- ? lastMessage.content
143
- : JSON.stringify(lastMessage.content);
614
+ const { text, imagePaths } = extractTextAndImagePaths(lastMessage.content);
615
+ return `${text}${buildImageNote(provider, imagePaths)}\n\nRequired final line when the response is fully complete: ${completion_marker_1.CLI_COMPLETION_SENTINEL}`;
144
616
  }
145
617
  }
618
+ // For fresh sessions, flatten images before passing to prompt builders
619
+ const { messages: flatMessages, imageNote } = flattenMessagesForImages(provider, messages);
146
620
  if (provider === 'claude-cli') {
147
- return (0, claude_cli_prompt_1.buildClaudeCliStylePrompt)({
621
+ const prompt = (0, claude_cli_prompt_1.buildClaudeCliStylePrompt)({
148
622
  system: systemPrompt,
149
- messages,
623
+ messages: flatMessages,
150
624
  runtimeMode: 'local_desktop',
151
625
  });
626
+ return imageNote ? `${prompt}${imageNote}` : prompt;
627
+ }
628
+ const codexPrompt = buildCodexSeedPrompt(systemPrompt, flatMessages);
629
+ return imageNote ? `${codexPrompt}${imageNote}` : codexPrompt;
630
+ }
631
+ function buildTurnPromptForTest(provider, systemPrompt, messages, freshSession, cwd = '.') {
632
+ return buildTurnPrompt(provider, systemPrompt, messages, freshSession, cwd);
633
+ }
634
+ function normalizePromptSnippet(text) {
635
+ return String(text || '')
636
+ .replace(/\s+/g, ' ')
637
+ .trim()
638
+ .toLowerCase();
639
+ }
640
+ function extractCurrentUserPromptSnippet(messages) {
641
+ for (let i = messages.length - 1; i >= 0; i--) {
642
+ const msg = messages[i];
643
+ if (msg?.role !== 'user')
644
+ continue;
645
+ const { text } = extractTextAndImagePaths(msg.content);
646
+ const normalized = normalizePromptSnippet(text);
647
+ if (normalized)
648
+ return normalized.slice(0, 240);
649
+ }
650
+ return '';
651
+ }
652
+ function parseRecordTimestampMs(record) {
653
+ if (!record || typeof record !== 'object' || typeof record.timestamp !== 'string')
654
+ return null;
655
+ const parsed = Date.parse(record.timestamp);
656
+ return Number.isFinite(parsed) ? parsed : null;
657
+ }
658
+ function extractClaudeUserRecordText(record) {
659
+ if (!record || record.type !== 'user')
660
+ return '';
661
+ const content = record?.message?.content;
662
+ if (typeof content === 'string')
663
+ return content;
664
+ if (!Array.isArray(content))
665
+ return '';
666
+ return content
667
+ .map((block) => {
668
+ if (!block || typeof block !== 'object')
669
+ return '';
670
+ if (typeof block.text === 'string')
671
+ return block.text;
672
+ if (typeof block.content === 'string')
673
+ return block.content;
674
+ return '';
675
+ })
676
+ .filter(Boolean)
677
+ .join('\n');
678
+ }
679
+ function updateClaudeTurnPromptSeen(tracker, record) {
680
+ if (tracker.sawCurrentTurnUserRecord)
681
+ return;
682
+ const expected = tracker.expectedUserPromptSnippet;
683
+ if (!expected || !record || record.type !== 'user')
684
+ return;
685
+ const normalized = normalizePromptSnippet(extractClaudeUserRecordText(record));
686
+ if (normalized && normalized.includes(expected)) {
687
+ tracker.sawCurrentTurnUserRecord = true;
688
+ }
689
+ }
690
+ function shouldIgnoreClaudeRecordForCurrentTurn(tracker, record) {
691
+ const timestampMs = parseRecordTimestampMs(record);
692
+ if (timestampMs != null && timestampMs < tracker.turnStartedAtMs - 2_000) {
693
+ return true;
694
+ }
695
+ updateClaudeTurnPromptSeen(tracker, record);
696
+ if (record?.type === 'assistant' && tracker.expectedUserPromptSnippet && !tracker.sawCurrentTurnUserRecord) {
697
+ return true;
698
+ }
699
+ return false;
700
+ }
701
+ function shouldIgnoreClaudeRecordForCurrentTurnForTest(turnStartedAtMs, expectedUserPrompt, sawCurrentTurnUserRecord, record) {
702
+ const tracker = {
703
+ done: false,
704
+ sawExplicitCompletion: false,
705
+ finalContent: '',
706
+ usage: undefined,
707
+ lastAssistantText: '',
708
+ detailFingerprints: new Set(),
709
+ pendingToolUseIds: new Set(),
710
+ lastRecordAtMs: turnStartedAtMs,
711
+ sawCompletionSentinel: false,
712
+ turnStartedAtMs,
713
+ expectedUserPromptSnippet: normalizePromptSnippet(expectedUserPrompt).slice(0, 240),
714
+ sawCurrentTurnUserRecord,
715
+ };
716
+ const ignore = shouldIgnoreClaudeRecordForCurrentTurn(tracker, record);
717
+ return {
718
+ ignore,
719
+ sawCurrentTurnUserRecord: tracker.sawCurrentTurnUserRecord,
720
+ };
721
+ }
722
+ function stripAnsi(text) {
723
+ if (!text)
724
+ return '';
725
+ return text
726
+ .replace(/\x00/g, '')
727
+ .replace(/\x1B\][^\x07]*(?:\x07|\x1B\\)/g, '')
728
+ .replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, '');
729
+ }
730
+ function resolveConptyOverwrites(text) {
731
+ if (!text)
732
+ return '';
733
+ const normalized = text.replace(/\r\n/g, '\n');
734
+ const output = [];
735
+ let line = '';
736
+ for (const ch of normalized) {
737
+ if (ch === '\r') {
738
+ line = '';
739
+ continue;
740
+ }
741
+ if (ch === '\b') {
742
+ line = line.slice(0, -1);
743
+ continue;
744
+ }
745
+ if (ch === '\n') {
746
+ output.push(line);
747
+ line = '';
748
+ continue;
749
+ }
750
+ line += ch;
751
+ }
752
+ output.push(line);
753
+ return output.join('\n');
754
+ }
755
+ function normalizeTerminalChunk(text) {
756
+ return resolveConptyOverwrites(stripAnsi(text))
757
+ .replace(/[\x01-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '')
758
+ .replace(/[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏✳✺✹✸✷✶✵✴✻✽⏵⏸●◐◑◒◓▉▊▋▌▍▎▏▐█▓▒░■□▪▫◼◻◾◽]+/g, ' ')
759
+ .replace(/\u00a0/g, ' ')
760
+ .replace(/[^\S\n]+/g, ' ');
761
+ }
762
+ function isCliBootstrapChromeLine(provider, line) {
763
+ const normalized = String(line || '')
764
+ .replace(/[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏✳✺✹✸✷✶✵✴✻✽⏵⏸●◐◑◒◓▉▊▋▌▍▎▏▐█▓▒░■□▪▫◼◻◾◽]+/g, ' ')
765
+ .replace(/\s+/g, ' ')
766
+ .trim();
767
+ if (!normalized)
768
+ return false;
769
+ if (provider === 'claude-cli') {
770
+ let hints = 0;
771
+ if (/\bClaude Code v\d/i.test(normalized))
772
+ hints += 2;
773
+ if (/\bClaude Max\b/i.test(normalized))
774
+ hints += 1;
775
+ if (/\bBot Identity\b/i.test(normalized))
776
+ hints += 1;
777
+ if (/\b(?:Opus|Sonnet|Haiku)\b.*\b(?:effort|thinking)\b/i.test(normalized))
778
+ hints += 1;
779
+ if (/\bclaude\.md\b/i.test(normalized))
780
+ hints += 1;
781
+ if (/[A-Za-z]:\\/.test(normalized))
782
+ hints += 1;
783
+ if (hints >= 2)
784
+ return true;
785
+ }
786
+ if (provider === 'codex-cli') {
787
+ if (/\bcodex\b/i.test(normalized) && /\b(model|approval|sandbox|cwd)\b/i.test(normalized)) {
788
+ return true;
789
+ }
790
+ }
791
+ return false;
792
+ }
793
+ function isSyntheticPromptEchoLine(line) {
794
+ const trimmed = line.trim();
795
+ if (!trimmed)
796
+ return false;
797
+ return (/^\[(System Instructions|Recent Transcript|User Request|Latest Message|Bot Identity|Project Overview|Workflow State|Response Style|Input from Previous Step|Context trimmed.*)\]$/i.test(trimmed)
798
+ || /^Current user request:$/i.test(trimmed)
799
+ || /^Recent transcript \(for continuity\):$/i.test(trimmed)
800
+ || /^(Bot Name|Role Label|Project|Workspace):/i.test(trimmed)
801
+ || /^Responsibilities:?$/i.test(trimmed)
802
+ || /^(USER|ASSISTANT|TOOL(?:\s+\(.+\))?):$/i.test(trimmed));
803
+ }
804
+ function isAutomationNoiseLine(provider, line) {
805
+ const trimmed = line.trim();
806
+ const normalizedLead = trimmed.replace(/^[>›â»âµâ–¸â–¶]+\s*/, '');
807
+ if (!trimmed)
808
+ return false;
809
+ if (isCliBootstrapChromeLine(provider, trimmed))
810
+ return true;
811
+ if (/^Pasting text/i.test(trimmed))
812
+ return true;
813
+ if (/^\[Pasted.*lines?\]/i.test(trimmed))
814
+ return true;
815
+ if (/^\[?Pasted text #?\d+\b/i.test(normalizedLead))
816
+ return true;
817
+ if (/^(?:Cerebrating|Choreographing)\.\.\.?$/i.test(normalizedLead))
818
+ return true;
819
+ if (/^ctrl\+g to edit in notepad$/i.test(trimmed))
820
+ return true;
821
+ if (/^\? for shortcuts$/i.test(trimmed))
822
+ return true;
823
+ if (/^Use \/skills to list available skills$/i.test(trimmed))
824
+ return true;
825
+ if (/^Improve documentation in @filename$/i.test(trimmed))
826
+ return true;
827
+ if (/^permissions on \(shift(?:\+|-)tab to cycle\)$/i.test(trimmed))
828
+ return true;
829
+ if (/^bypass permissions on \(shift(?:\+|-)tab to cycle\)$/i.test(trimmed))
830
+ return true;
831
+ if (/^(?:[-+*]\s*)?Brewing(?:\.\.\.?|…)?(?:-+)?$/i.test(normalizedLead))
832
+ return true;
833
+ if (/^Reading \d+ file(?:s)?\.\.\.\s*\(ctrl\+o to expand\)/i.test(normalizedLead))
834
+ return true;
835
+ if (/^\[last-prompt\]$/i.test(normalizedLead))
836
+ return true;
837
+ if (/^[-_=]{4,}$/.test(trimmed))
838
+ return true;
839
+ if (/^[•◦·*✢✶✻✽●]+$/.test(trimmed))
840
+ return true;
841
+ if (/^(Wo|or|rk|ki|in|ng|Wng|Wog)$/.test(trimmed))
842
+ return true;
843
+ if (trimmed.length <= 3 && /^[A-Za-z0-9]+$/.test(trimmed))
844
+ return true;
845
+ if (provider === 'claude-cli' && /^>\s*$/.test(trimmed))
846
+ return true;
847
+ if (provider === 'codex-cli' && /^›\s*$/.test(trimmed))
848
+ return true;
849
+ return false;
850
+ }
851
+ function isRepeatedChromeLine(line) {
852
+ const trimmed = line.trim();
853
+ if (!trimmed)
854
+ return false;
855
+ return (/(?:gpt-\d|claude|% left|esc to interrupt|for shortcuts|Use \/skills|Quantumizing|Working)/i.test(trimmed)
856
+ || /^› /.test(trimmed)
857
+ || /^> /.test(trimmed));
858
+ }
859
+ function isLikelyAssistantAnswerLine(line) {
860
+ const trimmed = line.trim();
861
+ if (!trimmed)
862
+ return false;
863
+ if (/^(?:>|›|\$|\/)/.test(trimmed))
864
+ return false;
865
+ if (/^(Running|Working|Thinking|Brewing|Analyzing|Searching|Reading|Preparing|Loading|Using|Tool|Status|Task|Step)\b/i.test(trimmed))
866
+ return false;
867
+ if (/^[-*]\s+/.test(trimmed) && trimmed.length >= 48)
868
+ return true;
869
+ if (/^\d+\.\s+/.test(trimmed) && trimmed.length >= 48)
870
+ return true;
871
+ const words = trimmed.split(/\s+/).filter(Boolean);
872
+ if (words.length >= 8 && /[a-z]/.test(trimmed) && /[.:!?]$/.test(trimmed))
873
+ return true;
874
+ if (trimmed.length >= 72 && /[a-z]/.test(trimmed))
875
+ return true;
876
+ return false;
877
+ }
878
+ function extractTerminalFacingText(text, assistantOutputDetected) {
879
+ if (!text || assistantOutputDetected) {
880
+ return { terminalText: '', assistantOutputDetected };
881
+ }
882
+ const terminalLines = [];
883
+ let detected = assistantOutputDetected;
884
+ for (const rawLine of text.split('\n')) {
885
+ const trimmed = rawLine.trim();
886
+ if (!trimmed) {
887
+ if (terminalLines.length > 0 && terminalLines[terminalLines.length - 1] !== '') {
888
+ terminalLines.push('');
889
+ }
890
+ continue;
891
+ }
892
+ if (isLikelyAssistantAnswerLine(trimmed)) {
893
+ detected = true;
894
+ break;
895
+ }
896
+ terminalLines.push(rawLine);
897
+ }
898
+ return {
899
+ terminalText: terminalLines.join('\n').replace(/\n{3,}/g, '\n\n').trim(),
900
+ assistantOutputDetected: detected,
901
+ };
902
+ }
903
+ function sanitizeVisibleChunk(provider, text, recentChromeLines) {
904
+ const sanitizedLines = [];
905
+ for (const rawLine of text.split('\n')) {
906
+ const trimmedRight = rawLine.replace(/\s+$/g, '');
907
+ const trimmed = trimmedRight.trim();
908
+ if (!trimmed) {
909
+ if (sanitizedLines.length > 0 && sanitizedLines[sanitizedLines.length - 1] !== '') {
910
+ sanitizedLines.push('');
911
+ }
912
+ continue;
913
+ }
914
+ if (isSyntheticPromptEchoLine(trimmedRight))
915
+ continue;
916
+ if (isAutomationNoiseLine(provider, trimmedRight))
917
+ continue;
918
+ if (isRepeatedChromeLine(trimmedRight) && recentChromeLines.includes(trimmed))
919
+ continue;
920
+ sanitizedLines.push(trimmedRight);
921
+ if (isRepeatedChromeLine(trimmedRight)) {
922
+ recentChromeLines.push(trimmed);
923
+ if (recentChromeLines.length > 12) {
924
+ recentChromeLines.splice(0, recentChromeLines.length - 12);
925
+ }
926
+ }
927
+ }
928
+ return sanitizedLines.join('\n').replace(/\n{3,}/g, '\n\n');
929
+ }
930
+ function trimPromptEcho(text, promptEchoRemainder) {
931
+ if (!text || !promptEchoRemainder) {
932
+ return { text, promptEchoRemainder };
933
+ }
934
+ let output = '';
935
+ let remainingPrompt = promptEchoRemainder;
936
+ let index = 0;
937
+ const maxIterations = (text.length + promptEchoRemainder.length) * 2;
938
+ let iterations = 0;
939
+ while (index < text.length) {
940
+ if (++iterations > maxIterations) {
941
+ output += text.slice(index);
942
+ break;
943
+ }
944
+ if (!remainingPrompt) {
945
+ output += text.slice(index);
946
+ break;
947
+ }
948
+ const current = text.slice(index);
949
+ if (remainingPrompt.startsWith(current)) {
950
+ return { text: output, promptEchoRemainder: remainingPrompt.slice(current.length) };
951
+ }
952
+ if (current.startsWith(remainingPrompt)) {
953
+ index += remainingPrompt.length;
954
+ remainingPrompt = '';
955
+ continue;
956
+ }
957
+ const maxPrefix = Math.min(current.length, remainingPrompt.length);
958
+ let matched = 0;
959
+ while (matched < maxPrefix && current[matched] === remainingPrompt[matched]) {
960
+ matched++;
961
+ }
962
+ if (matched > 0) {
963
+ index += matched;
964
+ remainingPrompt = remainingPrompt.slice(matched);
965
+ continue;
966
+ }
967
+ output += text[index];
968
+ index += 1;
969
+ }
970
+ return { text: output, promptEchoRemainder: remainingPrompt };
971
+ }
972
+ async function emitPtyChunk(session, chunk) {
973
+ const activeTurn = session.activeTurn;
974
+ if (!activeTurn)
975
+ return;
976
+ activeTurn.rawOutput += chunk;
977
+ activeTurn.lastDataAtMs = Date.now();
978
+ // Fix 13: Extract terminal title updates (OSC sequences) for live activity
979
+ // Format: \x1b]0;<title>\x07 or \x1b]0;<title>\x1b\\
980
+ const titleMatch = chunk.match(/\x1b\]0;([^\x07\x1b]*?)(?:\x07|\x1b\\)/);
981
+ if (titleMatch && titleMatch[1]) {
982
+ const title = titleMatch[1].replace(/[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏✳✺✹✸✷✶✵✴✻✽⏵⏸●◐◑◒◓]/g, '').trim();
983
+ if (title && title !== session._lastPtyTitle && title.length > 3) {
984
+ session._lastPtyTitle = title;
985
+ activeTurn.lastMeaningfulPtyDataAtMs = Date.now();
986
+ // Store on session for the polling loop to emit via onDetail
987
+ session._pendingTitle = title;
988
+ }
989
+ }
990
+ const cleaned = normalizeTerminalChunk(chunk);
991
+ if (!cleaned.trim())
992
+ return;
993
+ const echoTrimmed = trimPromptEcho(cleaned, activeTurn.promptEchoRemainder);
994
+ activeTurn.promptEchoRemainder = echoTrimmed.promptEchoRemainder;
995
+ const visibleText = sanitizeVisibleChunk(session.provider, echoTrimmed.text, activeTurn.recentChromeLines);
996
+ if (!visibleText.trim())
997
+ return;
998
+ if (activeTurn.mode === 'passthrough') {
999
+ activeTurn.lastMeaningfulPtyDataAtMs = Date.now();
1000
+ activeTurn.sawVisibleData = true;
1001
+ const passthroughText = visibleText.replace(/\n/g, '\r\n');
1002
+ activeTurn.visibleOutput += passthroughText;
1003
+ activeTurn.callbackChain = activeTurn.callbackChain
1004
+ .catch(() => undefined)
1005
+ .then(() => activeTurn.onRawChunk?.(passthroughText))
1006
+ .then(() => undefined);
1007
+ await activeTurn.callbackChain;
1008
+ return;
1009
+ }
1010
+ const terminalView = extractTerminalFacingText(visibleText, activeTurn.assistantOutputDetected);
1011
+ activeTurn.assistantOutputDetected = terminalView.assistantOutputDetected;
1012
+ if (terminalView.terminalText) {
1013
+ activeTurn.lastMeaningfulPtyDataAtMs = Date.now();
1014
+ activeTurn.sawVisibleData = true;
1015
+ const terminalText = terminalView.terminalText.replace(/\n/g, '\r\n');
1016
+ activeTurn.callbackChain = activeTurn.callbackChain
1017
+ .catch(() => undefined)
1018
+ .then(() => activeTurn.onRawChunk?.(terminalText))
1019
+ .then(() => undefined);
1020
+ await activeTurn.callbackChain;
152
1021
  }
153
- return buildCodexSeedPrompt(systemPrompt, messages);
154
1022
  }
155
- function getProviderSessionRoot(provider) {
1023
+ function getDefaultProviderHome(provider) {
156
1024
  if (provider === 'claude-cli') {
157
- return path.join(os.homedir(), '.claude', 'projects');
1025
+ return path.join(os.homedir(), '.claude');
158
1026
  }
159
- return path.join(os.homedir(), '.codex', 'sessions');
1027
+ return path.join(os.homedir(), '.codex');
160
1028
  }
161
- function listSessionFiles(provider) {
162
- const root = getProviderSessionRoot(provider);
1029
+ function getProviderSessionRoot(provider, cliHomeDir) {
1030
+ const homeDir = cliHomeDir || getDefaultProviderHome(provider);
1031
+ return provider === 'claude-cli'
1032
+ ? path.join(homeDir, 'projects')
1033
+ : path.join(homeDir, 'sessions');
1034
+ }
1035
+ function getProviderSessionRootForTest(provider, cliHomeDir) {
1036
+ return getProviderSessionRoot(provider, cliHomeDir);
1037
+ }
1038
+ function listSessionFiles(provider, sessionRoot) {
1039
+ const root = sessionRoot || getProviderSessionRoot(provider);
163
1040
  if (!fs.existsSync(root))
164
1041
  return [];
165
1042
  const results = [];
@@ -198,8 +1075,24 @@ function extractUserPromptFromRecord(provider, record) {
198
1075
  }
199
1076
  return '';
200
1077
  }
201
- function recentFileContainsPrompt(provider, filePath, promptText) {
202
- const expected = promptText.trim();
1078
+ function normalizePromptMatchText(text) {
1079
+ return text.replace(/\r/g, '').trim();
1080
+ }
1081
+ function recordTimestampMs(record) {
1082
+ const raw = typeof record?.timestamp === 'string'
1083
+ ? record.timestamp
1084
+ : typeof record?.ts === 'number'
1085
+ ? record.ts * 1000
1086
+ : null;
1087
+ if (typeof raw === 'number')
1088
+ return Number.isFinite(raw) ? raw : null;
1089
+ if (!raw)
1090
+ return null;
1091
+ const parsed = Date.parse(raw);
1092
+ return Number.isFinite(parsed) ? parsed : null;
1093
+ }
1094
+ function recentFileContainsPrompt(provider, filePath, promptText, startedAtMs) {
1095
+ const expected = normalizePromptMatchText(promptText);
203
1096
  if (!expected)
204
1097
  return false;
205
1098
  let stat;
@@ -224,7 +1117,13 @@ function recentFileContainsPrompt(provider, filePath, promptText) {
224
1117
  continue;
225
1118
  try {
226
1119
  const record = JSON.parse(line);
227
- if (extractUserPromptFromRecord(provider, record) === expected) {
1120
+ if (provider === 'codex-cli' && startedAtMs) {
1121
+ const ts = recordTimestampMs(record);
1122
+ if (ts !== null && ts < (startedAtMs - 2000)) {
1123
+ continue;
1124
+ }
1125
+ }
1126
+ if (normalizePromptMatchText(extractUserPromptFromRecord(provider, record)) === expected) {
228
1127
  return true;
229
1128
  }
230
1129
  }
@@ -238,8 +1137,8 @@ function recentFileContainsPrompt(provider, filePath, promptText) {
238
1137
  }
239
1138
  return false;
240
1139
  }
241
- function discoverSessionFile(provider, launchSnapshot, startedAtMs, promptLocator) {
242
- const candidates = listSessionFiles(provider)
1140
+ function discoverSessionFile(provider, launchSnapshot, startedAtMs, promptLocator, sessionRoot) {
1141
+ const candidates = listSessionFiles(provider, sessionRoot)
243
1142
  .map((candidate) => {
244
1143
  let mtimeMs = 0;
245
1144
  try {
@@ -251,44 +1150,149 @@ function discoverSessionFile(provider, launchSnapshot, startedAtMs, promptLocato
251
1150
  return { candidate, mtimeMs, isNew: !launchSnapshot.has(candidate) };
252
1151
  })
253
1152
  .filter((item) => !!item)
254
- .filter((item) => item.mtimeMs >= (startedAtMs - 5000))
255
- .sort((a, b) => {
256
- if (a.isNew !== b.isNew)
257
- return a.isNew ? -1 : 1;
258
- return b.mtimeMs - a.mtimeMs;
259
- });
1153
+ .filter((item) => item.isNew && item.mtimeMs >= (startedAtMs - 5000))
1154
+ .sort((a, b) => b.mtimeMs - a.mtimeMs);
260
1155
  if (promptLocator?.trim()) {
261
- const matched = candidates.find((item) => recentFileContainsPrompt(provider, item.candidate, promptLocator));
1156
+ const matched = candidates.find((item) => recentFileContainsPrompt(provider, item.candidate, promptLocator, startedAtMs));
262
1157
  if (matched)
263
1158
  return matched.candidate;
1159
+ if (provider === 'codex-cli') {
1160
+ return null;
1161
+ }
264
1162
  }
265
1163
  return candidates[0]?.candidate || null;
266
1164
  }
267
- function extractClaudeText(record) {
268
- const blocks = Array.isArray(record?.message?.content) ? record.message.content : [];
269
- return blocks
270
- .map((block) => {
271
- if (!block || typeof block !== 'object')
272
- return '';
273
- return block.type === 'text' && typeof block.text === 'string' ? block.text : '';
274
- })
275
- .join('');
276
- }
277
- function extractClaudeToolNames(record) {
278
- const blocks = Array.isArray(record?.message?.content) ? record.message.content : [];
279
- return blocks
280
- .map((block) => (block?.type === 'tool_use' && typeof block?.name === 'string' ? block.name : ''))
281
- .filter((name) => !!name);
282
- }
283
- function extractCodexAssistantText(record) {
284
- if (!record || typeof record !== 'object')
285
- return '';
286
- if (record.type === 'event_msg') {
287
- if (record.payload?.type === 'agent_message' && typeof record.payload?.message === 'string') {
288
- return record.payload.message;
1165
+ function findCodexSessionFileBySessionId(sessionId) {
1166
+ if (!sessionId)
1167
+ return null;
1168
+ const candidates = listSessionFiles('codex-cli')
1169
+ .map((candidate) => {
1170
+ try {
1171
+ return { candidate, mtimeMs: fs.statSync(candidate).mtimeMs };
289
1172
  }
290
- if (record.payload?.type === 'task_complete' && typeof record.payload?.last_agent_message === 'string') {
291
- return record.payload.last_agent_message;
1173
+ catch {
1174
+ return null;
1175
+ }
1176
+ })
1177
+ .filter((item) => !!item)
1178
+ .sort((a, b) => b.mtimeMs - a.mtimeMs);
1179
+ for (const item of candidates) {
1180
+ let stat;
1181
+ try {
1182
+ stat = fs.statSync(item.candidate);
1183
+ }
1184
+ catch {
1185
+ continue;
1186
+ }
1187
+ const readLength = Math.min(stat.size, 64 * 1024);
1188
+ if (readLength <= 0)
1189
+ continue;
1190
+ const fd = fs.openSync(item.candidate, 'r');
1191
+ try {
1192
+ const buffer = Buffer.alloc(readLength);
1193
+ fs.readSync(fd, buffer, 0, readLength, 0);
1194
+ const text = buffer.toString('utf8');
1195
+ for (const rawLine of text.split('\n')) {
1196
+ const line = rawLine.trim();
1197
+ if (!line)
1198
+ continue;
1199
+ try {
1200
+ const record = JSON.parse(line);
1201
+ if (record?.type === 'session_meta' && record?.payload?.id === sessionId) {
1202
+ return item.candidate;
1203
+ }
1204
+ }
1205
+ catch {
1206
+ continue;
1207
+ }
1208
+ }
1209
+ }
1210
+ finally {
1211
+ fs.closeSync(fd);
1212
+ }
1213
+ }
1214
+ return null;
1215
+ }
1216
+ function describePtyExit(exitArgs) {
1217
+ const first = exitArgs[0];
1218
+ const code = first && typeof first === 'object'
1219
+ ? (first.exitCode ?? first.code)
1220
+ : first;
1221
+ const signal = first && typeof first === 'object'
1222
+ ? first.signal
1223
+ : exitArgs[1];
1224
+ const parts = [];
1225
+ if (code !== undefined && code !== null)
1226
+ parts.push(`exitCode=${String(code)}`);
1227
+ if (signal !== undefined && signal !== null)
1228
+ parts.push(`signal=${String(signal)}`);
1229
+ return parts.length > 0 ? parts.join(', ') : 'no exit details';
1230
+ }
1231
+ function formatClosedSessionError(session, context) {
1232
+ return new Error(`${session.provider} PTY session ${context}${session.exitReason ? ` (${session.exitReason})` : ''}`);
1233
+ }
1234
+ function formatToolUseDetail(toolName, input) {
1235
+ if (!input || typeof input !== 'object')
1236
+ return `🔧 ${toolName}`;
1237
+ // Format common tools nicely
1238
+ switch (toolName) {
1239
+ case 'Agent':
1240
+ return `🤖 Agent: ${input.description || input.subagent_type || 'working'}`;
1241
+ case 'Read':
1242
+ return `📄 Read: ${input.file_path || ''}`;
1243
+ case 'Write':
1244
+ return `✏️ Write: ${input.file_path || ''}`;
1245
+ case 'Edit':
1246
+ return `✏️ Edit: ${input.file_path || ''}`;
1247
+ case 'Bash':
1248
+ return `💻 Bash: ${String(input.command || '').slice(0, 200)}`;
1249
+ case 'Grep':
1250
+ return `🔍 Grep: "${input.pattern || ''}" ${input.path ? 'in ' + input.path : ''}`;
1251
+ case 'Glob':
1252
+ return `📁 Glob: ${input.pattern || ''}${input.path ? ' in ' + input.path : ''}`;
1253
+ case 'WebSearch':
1254
+ return `🌐 Search: ${input.query || ''}`;
1255
+ case 'WebFetch':
1256
+ return `🌐 Fetch: ${input.url || ''}`;
1257
+ default:
1258
+ return `🔧 ${toolName}: ${JSON.stringify(input).slice(0, 200)}`;
1259
+ }
1260
+ }
1261
+ function formatToolResultDetail(toolUseId, content) {
1262
+ const trimmedId = toolUseId.slice(-8);
1263
+ const lines = content.split('\n').length;
1264
+ const chars = content.length;
1265
+ if (chars <= 200) {
1266
+ return `✅ Result [${trimmedId}]: ${content}`;
1267
+ }
1268
+ const firstLine = content.split('\n')[0].slice(0, 120);
1269
+ return `✅ Result [${trimmedId}]: ${firstLine}... (${lines} lines, ${chars} chars)`;
1270
+ }
1271
+ function extractClaudeText(record) {
1272
+ const blocks = Array.isArray(record?.message?.content) ? record.message.content : [];
1273
+ return blocks
1274
+ .map((block) => {
1275
+ if (!block || typeof block !== 'object')
1276
+ return '';
1277
+ return block.type === 'text' && typeof block.text === 'string' ? block.text : '';
1278
+ })
1279
+ .join('');
1280
+ }
1281
+ function extractClaudeToolNames(record) {
1282
+ const blocks = Array.isArray(record?.message?.content) ? record.message.content : [];
1283
+ return blocks
1284
+ .map((block) => (block?.type === 'tool_use' && typeof block?.name === 'string' ? block.name : ''))
1285
+ .filter((name) => !!name);
1286
+ }
1287
+ function extractCodexAssistantText(record) {
1288
+ if (!record || typeof record !== 'object')
1289
+ return '';
1290
+ if (record.type === 'event_msg') {
1291
+ if (record.payload?.type === 'agent_message' && typeof record.payload?.message === 'string') {
1292
+ return record.payload.message;
1293
+ }
1294
+ if (record.payload?.type === 'task_complete' && typeof record.payload?.last_agent_message === 'string') {
1295
+ return record.payload.last_agent_message;
292
1296
  }
293
1297
  }
294
1298
  if (record.type === 'response_item' && record.payload?.type === 'message' && record.payload?.role === 'assistant') {
@@ -310,63 +1314,233 @@ function extractCodexAssistantText(record) {
310
1314
  function normalizeAssistantContent(text) {
311
1315
  return text.replace(/^\s+/, '').replace(/\r/g, '');
312
1316
  }
1317
+ function normalizeCompletionState(text) {
1318
+ const normalized = normalizeAssistantContent(text);
1319
+ if (!normalized)
1320
+ return { text: '', hasSentinel: false };
1321
+ const stripped = (0, completion_marker_1.stripCompletionSentinel)(normalized);
1322
+ return { text: stripped.text, hasSentinel: stripped.found };
1323
+ }
313
1324
  async function emitDetail(tracker, detail, cb) {
314
1325
  const trimmed = detail.trim();
315
1326
  if (!trimmed)
316
1327
  return;
317
- if (tracker.detailFingerprints.has(trimmed))
1328
+ // Only deduplicate consecutive identical details, not across the whole turn
1329
+ if (tracker.lastDetail === trimmed)
318
1330
  return;
319
- tracker.detailFingerprints.add(trimmed);
1331
+ tracker.lastDetail = trimmed;
320
1332
  await cb?.(trimmed);
321
1333
  }
1334
+ async function emitAssistantChunk(tracker, nextText, cb) {
1335
+ const normalized = normalizeCompletionState(nextText).text;
1336
+ if (!normalized)
1337
+ return;
1338
+ const previous = tracker.lastAssistantText;
1339
+ if (normalized === previous)
1340
+ return;
1341
+ tracker.lastAssistantText = normalized;
1342
+ if (!cb)
1343
+ return;
1344
+ if (!previous) {
1345
+ await cb(normalized);
1346
+ return;
1347
+ }
1348
+ if (normalized.startsWith(previous)) {
1349
+ const delta = normalized.slice(previous.length);
1350
+ if (delta) {
1351
+ await cb(delta);
1352
+ }
1353
+ return;
1354
+ }
1355
+ const separator = previous.match(/\n\s*\n\s*$/) ? '' : '\n\n';
1356
+ await cb(`${separator}${normalized}`);
1357
+ }
1358
+ async function emitAssistantChunkSequenceForTest(chunks) {
1359
+ const tracker = {
1360
+ done: false,
1361
+ sawExplicitCompletion: false,
1362
+ finalContent: '',
1363
+ detailFingerprints: new Set(),
1364
+ lastAssistantText: '',
1365
+ pendingToolUseIds: new Set(),
1366
+ lastRecordAtMs: 0,
1367
+ sawCompletionSentinel: false,
1368
+ turnStartedAtMs: 0,
1369
+ expectedUserPromptSnippet: '',
1370
+ sawCurrentTurnUserRecord: false,
1371
+ };
1372
+ const emitted = [];
1373
+ for (const chunk of chunks) {
1374
+ await emitAssistantChunk(tracker, chunk, async (delta) => {
1375
+ emitted.push(delta);
1376
+ });
1377
+ }
1378
+ return emitted;
1379
+ }
322
1380
  function parseClaudeSessionRecord(record) {
323
1381
  const sessionId = typeof record?.sessionId === 'string' ? record.sessionId : undefined;
324
1382
  if (record?.type === 'assistant' && record?.message) {
1383
+ // token.txt Change 1: keep `inputTokens` as the sum (back-compat
1384
+ // with downstream readers — SSE meta events, ChatJob persistence,
1385
+ // etc.) AND populate the three breakdown fields so the LlmUsageLog
1386
+ // path can record cache-aware costs.
1387
+ const fresh = record.message.usage?.input_tokens || 0;
1388
+ const cacheCreation = record.message.usage?.cache_creation_input_tokens || 0;
1389
+ const cacheRead = record.message.usage?.cache_read_input_tokens || 0;
325
1390
  const usage = record.message.usage
326
1391
  ? {
327
- inputTokens: (record.message.usage.input_tokens || 0)
328
- + (record.message.usage.cache_read_input_tokens || 0)
329
- + (record.message.usage.cache_creation_input_tokens || 0),
1392
+ inputTokens: fresh + cacheCreation + cacheRead,
1393
+ inputTokensFresh: fresh,
1394
+ inputTokensCacheCreation: cacheCreation,
1395
+ inputTokensCacheRead: cacheRead,
330
1396
  outputTokens: record.message.usage.output_tokens || 0,
331
1397
  }
332
1398
  : undefined;
333
- const toolNames = extractClaudeToolNames(record);
334
- if (toolNames.length > 0) {
335
- return {
336
- sessionId,
337
- detail: `Running ${toolNames.join(', ')}...`,
338
- usage,
339
- };
1399
+ // Build detail lines and live activity events from ALL content blocks
1400
+ const blocks = Array.isArray(record.message.content) ? record.message.content : [];
1401
+ const details = [];
1402
+ const activities = [];
1403
+ const openedToolUseIds = [];
1404
+ for (const block of blocks) {
1405
+ if (!block || typeof block !== 'object')
1406
+ continue;
1407
+ if (block.type === 'thinking' && typeof block.thinking === 'string' && block.thinking.trim()) {
1408
+ details.push(`💭 Thinking`);
1409
+ }
1410
+ else if (block.type === 'tool_use') {
1411
+ const toolName = typeof block.name === 'string' ? block.name : 'unknown';
1412
+ if (typeof block.id === 'string' && block.id.trim()) {
1413
+ openedToolUseIds.push(block.id);
1414
+ }
1415
+ // Always emit a structured tool_call activity for every tool_use block.
1416
+ // This is consumed by workflow-engine to produce worker_tool_call events.
1417
+ activities.push({
1418
+ type: 'tool_call',
1419
+ label: toolName,
1420
+ key: block.id || undefined,
1421
+ timestamp: Date.now(),
1422
+ });
1423
+ // Emit structured live activity events
1424
+ if (toolName === 'Agent') {
1425
+ const activityLabel = (0, live_activity_1.agentToolUseToLabel)(block.input || {});
1426
+ if (activityLabel) {
1427
+ activities.push({
1428
+ type: 'subagent_started',
1429
+ label: activityLabel,
1430
+ key: block.id || undefined,
1431
+ timestamp: Date.now(),
1432
+ });
1433
+ }
1434
+ }
1435
+ else {
1436
+ const activityLabel = (0, live_activity_1.toolUseToActivityLabel)(toolName);
1437
+ if (activityLabel) {
1438
+ activities.push({
1439
+ type: 'working',
1440
+ label: activityLabel,
1441
+ key: block.id || undefined,
1442
+ timestamp: Date.now(),
1443
+ });
1444
+ }
1445
+ }
1446
+ }
340
1447
  }
341
- const text = normalizeAssistantContent(extractClaudeText(record));
1448
+ const completionState = normalizeCompletionState(extractClaudeText(record));
1449
+ const text = completionState.text;
1450
+ const detailStr = details.length > 0 ? details.join('\n') : undefined;
1451
+ // Completion detection:
1452
+ // 1. Explicit stop_reason (end_turn, max_tokens, stop_sequence) = done immediately
1453
+ // 2. stop_reason: null or tool_use = NOT done (wait for system/turn_duration fallback)
1454
+ // Why not "text + no tool_use = done"? Because CLI writes text and tool_use as
1455
+ // separate records — text arrives first with stop_reason: null, then tool_use follows.
1456
+ // We'd mark done prematurely before the tool_use record arrives.
1457
+ const stopReason = record.message?.stop_reason;
1458
+ const isDone = !!stopReason && stopReason !== 'tool_use';
1459
+ const activityResult = activities.length > 0 ? { activities } : {};
1460
+ const toolUseResult = openedToolUseIds.length > 0 ? { openedToolUseIds } : {};
342
1461
  if (text) {
1462
+ const shouldFinalize = isDone || completionState.hasSentinel;
343
1463
  return {
344
1464
  sessionId,
345
- ...(record.message.stop_reason === 'end_turn'
1465
+ assistantText: text,
1466
+ ...(detailStr ? { detail: detailStr } : {}),
1467
+ ...(shouldFinalize
346
1468
  ? { finalContent: text, done: true, usage }
347
- : { detail: text, usage }),
1469
+ : { usage }),
1470
+ ...activityResult,
1471
+ ...toolUseResult,
348
1472
  };
349
1473
  }
350
- return { sessionId, usage };
1474
+ return {
1475
+ sessionId,
1476
+ ...(detailStr ? { detail: detailStr } : {}),
1477
+ ...(completionState.hasSentinel ? { done: true } : {}),
1478
+ usage,
1479
+ ...activityResult,
1480
+ ...toolUseResult,
1481
+ };
351
1482
  }
352
1483
  if (record?.type === 'user' && Array.isArray(record?.message?.content)) {
353
- const toolResultText = record.message.content
354
- .map((block) => {
1484
+ const resolvedToolUseIds = record.message.content
1485
+ .map((block) => (typeof block?.tool_use_id === 'string' ? block.tool_use_id : ''))
1486
+ .filter((toolUseId) => !!toolUseId);
1487
+ // Build tool_result activities so workflow-engine can emit worker_tool_result events
1488
+ const resultActivities = [];
1489
+ for (const block of record.message.content) {
355
1490
  if (!block || typeof block !== 'object')
356
- return '';
357
- if (typeof block.tool_use_id === 'string' && typeof block.content === 'string') {
358
- return `Tool result (${block.tool_use_id}): ${String(block.content).slice(0, 240)}`;
1491
+ continue;
1492
+ if (typeof block.tool_use_id !== 'string' || !block.tool_use_id)
1493
+ continue;
1494
+ const isError = !!block.is_error;
1495
+ // Extract a short summary from the result content (could be string or array of blocks)
1496
+ let summary = '';
1497
+ if (typeof block.content === 'string') {
1498
+ summary = block.content.replace(/\s+/g, ' ').trim().slice(0, 200);
359
1499
  }
360
- return '';
361
- })
362
- .filter(Boolean)
363
- .join('\n');
364
- if (toolResultText) {
365
- return { sessionId, detail: toolResultText };
1500
+ else if (Array.isArray(block.content)) {
1501
+ const textBlocks = block.content
1502
+ .map((c) => (c && typeof c === 'object' && typeof c.text === 'string' ? c.text : ''))
1503
+ .filter(Boolean);
1504
+ summary = textBlocks.join(' ').replace(/\s+/g, ' ').trim().slice(0, 200);
1505
+ }
1506
+ resultActivities.push({
1507
+ type: 'tool_result',
1508
+ label: summary || (isError ? 'tool error' : 'tool completed'),
1509
+ key: block.tool_use_id,
1510
+ isError,
1511
+ timestamp: Date.now(),
1512
+ });
366
1513
  }
1514
+ if (resolvedToolUseIds.length > 0) {
1515
+ return {
1516
+ sessionId,
1517
+ resolvedToolUseIds,
1518
+ ...(resultActivities.length > 0 ? { activities: resultActivities } : {}),
1519
+ };
1520
+ }
1521
+ }
1522
+ // system/turn_duration is a fallback completion signal from the CLI
1523
+ if (record?.type === 'system' && record?.subtype === 'turn_duration') {
1524
+ return { sessionId, done: true };
1525
+ }
1526
+ // Skip internal bookkeeping records, surface anything else
1527
+ const skipTypes = ['permission-mode', 'file-history-snapshot', 'attachment', 'system'];
1528
+ if (record?.type && !skipTypes.includes(record.type) && record.type !== 'assistant' && record.type !== 'user') {
1529
+ return { sessionId, detail: `[${record.type}]` };
367
1530
  }
368
1531
  return sessionId ? { sessionId } : {};
369
1532
  }
1533
+ function canFinalizeClaudeTurnOnSessionExit(session, tracker) {
1534
+ if (session.provider !== 'claude-cli')
1535
+ return false;
1536
+ if (tracker.sawExplicitCompletion || tracker.done)
1537
+ return false;
1538
+ if (!tracker.lastAssistantText.trim())
1539
+ return false;
1540
+ if (tracker.pendingToolUseIds.size > 0)
1541
+ return false;
1542
+ return true;
1543
+ }
370
1544
  function parseCodexSessionRecord(record) {
371
1545
  if (!record || typeof record !== 'object')
372
1546
  return {};
@@ -374,62 +1548,543 @@ function parseCodexSessionRecord(record) {
374
1548
  return { sessionId: record.payload.id };
375
1549
  }
376
1550
  if (record.type === 'event_msg' && record.payload?.type === 'task_started') {
377
- return { detail: 'Task started' };
1551
+ return {};
378
1552
  }
379
1553
  if (record.type === 'event_msg' && record.payload?.type === 'task_complete') {
380
- const finalContent = normalizeAssistantContent(typeof record.payload?.last_agent_message === 'string' ? record.payload.last_agent_message : '');
1554
+ const completionState = normalizeCompletionState(typeof record.payload?.last_agent_message === 'string' ? record.payload.last_agent_message : '');
381
1555
  return {
382
- finalContent,
1556
+ assistantText: completionState.text,
1557
+ finalContent: completionState.text,
383
1558
  done: true,
384
1559
  };
385
1560
  }
386
1561
  if (record.type === 'event_msg' && record.payload?.type === 'token_count' && record.payload?.info?.total_token_usage) {
1562
+ const lastUsage = record.payload.info.last_token_usage;
1563
+ if (lastUsage) {
1564
+ // token.txt Change 1: keep inputTokens sum + add breakdown.
1565
+ // Codex exposes only one cache field (cached_input_tokens) —
1566
+ // we map it to inputTokensCacheRead since "cached" semantically
1567
+ // means "served from cache." inputTokensCacheCreation is always
1568
+ // 0 for Codex (no separate write billing on OpenAI).
1569
+ const fresh = lastUsage.input_tokens || 0;
1570
+ const cacheRead = lastUsage.cached_input_tokens || 0;
1571
+ return {
1572
+ usage: {
1573
+ inputTokens: fresh + cacheRead,
1574
+ inputTokensFresh: fresh,
1575
+ inputTokensCacheCreation: 0,
1576
+ inputTokensCacheRead: cacheRead,
1577
+ outputTokens: lastUsage.output_tokens || 0,
1578
+ },
1579
+ };
1580
+ }
1581
+ const fresh = record.payload.info.total_token_usage.input_tokens || 0;
1582
+ const cacheRead = record.payload.info.total_token_usage.cached_input_tokens || 0;
387
1583
  return {
388
1584
  usage: {
389
- inputTokens: (record.payload.info.total_token_usage.input_tokens || 0)
390
- + (record.payload.info.total_token_usage.cached_input_tokens || 0),
1585
+ inputTokens: fresh + cacheRead,
1586
+ inputTokensFresh: fresh,
1587
+ inputTokensCacheCreation: 0,
1588
+ inputTokensCacheRead: cacheRead,
391
1589
  outputTokens: record.payload.info.total_token_usage.output_tokens || 0,
392
1590
  },
393
1591
  };
394
1592
  }
395
- const assistantText = normalizeAssistantContent(extractCodexAssistantText(record));
1593
+ const completionState = normalizeCompletionState(extractCodexAssistantText(record));
1594
+ const assistantText = completionState.text;
396
1595
  if (assistantText) {
397
- return { finalContent: assistantText };
1596
+ return {
1597
+ assistantText,
1598
+ finalContent: assistantText,
1599
+ ...(completionState.hasSentinel ? { done: true } : {}),
1600
+ };
398
1601
  }
399
1602
  if (record.type === 'event_msg' && typeof record.payload?.type === 'string') {
400
1603
  const eventType = record.payload.type;
401
1604
  if (!['user_message', 'agent_message', 'token_count', 'task_complete'].includes(eventType)) {
402
- return { detail: eventType.replace(/_/g, ' ') };
1605
+ return {};
403
1606
  }
404
1607
  }
405
1608
  return {};
406
1609
  }
407
1610
  async function waitForFirstTerminalData(session) {
408
1611
  await Promise.race([session.readyPromise, delay(5000)]);
409
- await delay(session.startupDelayMs);
1612
+ if (!session.startupDelayApplied) {
1613
+ await delay(session.startupDelayMs);
1614
+ session.startupDelayApplied = true;
1615
+ }
1616
+ }
1617
+ async function waitForOutputToSettle(session, quietMs = 500, maxMs = 4000) {
1618
+ const start = Date.now();
1619
+ let lastLen = session.recentTerminalOutput.length;
1620
+ while (Date.now() - start < maxMs) {
1621
+ await delay(quietMs);
1622
+ const currentLen = session.recentTerminalOutput.length;
1623
+ if (currentLen === lastLen)
1624
+ return;
1625
+ lastLen = currentLen;
1626
+ }
1627
+ }
1628
+ function buildWarmTimeoutError(provider, timeoutMs) {
1629
+ const err = new Error(`${provider} warm session timed out after ${Math.floor(timeoutMs / 1000)}s`);
1630
+ err.code = 'CLI_WARM_TIMEOUT';
1631
+ err.name = 'CliWarmTimeoutError';
1632
+ return err;
1633
+ }
1634
+ function withWarmTimeout(promise, timeoutMs, onTimeout, provider) {
1635
+ let timeout = null;
1636
+ const timeoutPromise = new Promise((_, reject) => {
1637
+ timeout = setTimeout(() => {
1638
+ try {
1639
+ onTimeout();
1640
+ }
1641
+ finally {
1642
+ reject(buildWarmTimeoutError(provider, timeoutMs));
1643
+ }
1644
+ }, timeoutMs);
1645
+ });
1646
+ return Promise.race([promise, timeoutPromise]).finally(() => {
1647
+ if (timeout)
1648
+ clearTimeout(timeout);
1649
+ });
1650
+ }
1651
+ function logCliWarmEvent(event, fields) {
1652
+ const suffix = Object.entries(fields)
1653
+ .filter(([, value]) => value !== undefined && value !== null && value !== '')
1654
+ .map(([key, value]) => `${key}=${JSON.stringify(value)}`)
1655
+ .join(' ');
1656
+ console.info(`[cli-warm] ${event}${suffix ? ` ${suffix}` : ''}`);
1657
+ }
1658
+ function buildCliWarmDiscardFailureReason(err) {
1659
+ const error = err;
1660
+ if (error?.code === 'CLI_WARM_TIMEOUT')
1661
+ return 'warm_timeout';
1662
+ if (error?.authRequired === true)
1663
+ return 'auth_required';
1664
+ if (error?.code === 'CLAUDE_FRESH_SESSION_STARTUP_TIMEOUT')
1665
+ return 'warm_startup_timeout';
1666
+ if (error?.name === 'AbortError')
1667
+ return 'warm_aborted';
1668
+ const message = String(error?.message || '').toLowerCase();
1669
+ if (message.includes('closed before it was ready'))
1670
+ return 'session_closed_before_ready';
1671
+ return 'warm_failed';
1672
+ }
1673
+ async function writePromptInChunks(pty, promptText, chunkSize = 512, chunkDelayMs = 15) {
1674
+ for (let offset = 0; offset < promptText.length; offset += chunkSize) {
1675
+ pty.write(promptText.slice(offset, offset + chunkSize));
1676
+ if (offset + chunkSize < promptText.length) {
1677
+ await delay(chunkDelayMs);
1678
+ }
1679
+ }
1680
+ }
1681
+ async function writeBracketedPaste(pty, promptText, chunkSize, chunkDelayMs) {
1682
+ pty.write('\x1b[200~');
1683
+ await writePromptInChunks(pty, promptText, chunkSize, chunkDelayMs);
1684
+ pty.write('\x1b[201~');
410
1685
  }
411
1686
  async function writeInteractivePrompt(session, promptText) {
412
1687
  if (session.provider === 'codex-cli') {
413
- session.pty.write(promptText);
1688
+ const multiline = promptText.includes('\n');
1689
+ if (multiline) {
1690
+ await writeBracketedPaste(session.pty, promptText, 512, 15);
1691
+ }
1692
+ else {
1693
+ await writePromptInChunks(session.pty, promptText);
1694
+ }
414
1695
  await delay(session.submitDelayMs);
415
1696
  session.pty.write('\r');
416
1697
  return;
417
1698
  }
418
- session.pty.write(promptText);
1699
+ // 512-byte chunks with 10ms delays — proven safe for Windows ConPTY pipe buffer.
1700
+ await writeBracketedPaste(session.pty, promptText, 512, 10);
1701
+ await delay(session.submitDelayMs);
1702
+ session.pty.write('\r');
1703
+ }
1704
+ async function writeNakedPrompt(session, text) {
1705
+ await writePromptInChunks(session.pty, text);
1706
+ await delay(session.submitDelayMs);
419
1707
  session.pty.write('\r');
420
1708
  }
1709
+ function parseJsonObject(raw) {
1710
+ const trimmed = String(raw || '').trim();
1711
+ if (!trimmed)
1712
+ return null;
1713
+ try {
1714
+ const parsed = JSON.parse(trimmed);
1715
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
1716
+ return parsed;
1717
+ }
1718
+ }
1719
+ catch {
1720
+ // ignore invalid JSON and fall back to the default CLI behavior
1721
+ }
1722
+ return null;
1723
+ }
1724
+ function buildClaudeCliArgs(resumeSessionId, newSessionId, settings) {
1725
+ const args = ['--dangerously-skip-permissions'];
1726
+ const model = String(settings?.model || '').trim();
1727
+ if (model && model.toLowerCase() !== 'default') {
1728
+ args.push('--model', model);
1729
+ }
1730
+ const effortLevel = String(settings?.effortLevel || '').trim().toLowerCase();
1731
+ if (['low', 'medium', 'high', 'max'].includes(effortLevel)) {
1732
+ args.push('--effort', effortLevel);
1733
+ }
1734
+ const runtimeSettings = {};
1735
+ const outputStyle = String(settings?.outputStyle || '').trim();
1736
+ if (outputStyle && outputStyle.toLowerCase() !== 'default')
1737
+ runtimeSettings.outputStyle = outputStyle;
1738
+ if (settings?.fastMode === true)
1739
+ runtimeSettings.fastMode = true;
1740
+ const permissions = parseJsonObject(settings?.permissionsJson);
1741
+ if (permissions)
1742
+ runtimeSettings.permissions = permissions;
1743
+ if (Object.keys(runtimeSettings).length > 0) {
1744
+ args.push('--settings', JSON.stringify(runtimeSettings));
1745
+ }
1746
+ if (resumeSessionId) {
1747
+ args.push('--resume', resumeSessionId);
1748
+ }
1749
+ else if (newSessionId) {
1750
+ args.push('--session-id', newSessionId);
1751
+ }
1752
+ return args;
1753
+ }
1754
+ function appendCodexConfigOverride(args, key, value) {
1755
+ const trimmed = String(value || '').trim();
1756
+ if (!trimmed)
1757
+ return;
1758
+ args.push('-c', `${key}=${JSON.stringify(trimmed)}`);
1759
+ }
1760
+ function buildCodexCliArgs(resumeSessionId, settings) {
1761
+ const model = String(settings?.model || '').trim();
1762
+ const sandboxPolicy = String(settings?.sandboxPolicy || '').trim() || 'danger-full-access';
1763
+ const approvalPolicy = String(settings?.approvalPolicy || '').trim() || 'never';
1764
+ const args = resumeSessionId ? ['resume', resumeSessionId] : [];
1765
+ args.push('--no-alt-screen');
1766
+ if (model && model.toLowerCase() !== 'default') {
1767
+ args.push('-m', model);
1768
+ }
1769
+ args.push('-s', sandboxPolicy, '-a', approvalPolicy);
1770
+ appendCodexConfigOverride(args, 'shell_environment_policy.inherit', 'all');
1771
+ appendCodexConfigOverride(args, 'model_reasoning_effort', settings?.reasoningEffort);
1772
+ appendCodexConfigOverride(args, 'personality', settings?.personality);
1773
+ appendCodexConfigOverride(args, 'service_tier', settings?.serviceTier);
1774
+ return args;
1775
+ }
1776
+ function trimPassthroughSlashChrome(command, rawText) {
1777
+ const normalized = rawText.replace(/\r\n/g, '\n').trim();
1778
+ if (!command.startsWith('/'))
1779
+ return normalized;
1780
+ const markers = [
1781
+ `\n> ${command}\n\n`,
1782
+ `\n${command}\n\n`,
1783
+ `\n> ${command}\n`,
1784
+ `\n${command}\n`,
1785
+ `> ${command}\n\n`,
1786
+ `${command}\n\n`,
1787
+ ];
1788
+ for (const marker of markers) {
1789
+ const idx = normalized.indexOf(marker);
1790
+ if (idx < 0)
1791
+ continue;
1792
+ const candidate = normalized.slice(idx + marker.length).trim();
1793
+ if (candidate)
1794
+ return cleanPassthroughSlashText(command, candidate);
1795
+ }
1796
+ return cleanPassthroughSlashText(command, normalized);
1797
+ }
1798
+ function finalizePassthroughContent(command, visibleOutput, rawOutput) {
1799
+ const visible = String(visibleOutput || '').trim();
1800
+ if (visible) {
1801
+ return trimPassthroughSlashChrome(command, visible);
1802
+ }
1803
+ const rawNormalized = normalizeTerminalChunk(rawOutput || '');
1804
+ return trimPassthroughSlashChrome(command, rawNormalized);
1805
+ }
1806
+ function cleanPassthroughSlashText(command, text) {
1807
+ const normalizedCommand = String(command || '').trim().toLowerCase();
1808
+ if (!text.trim()) {
1809
+ return text.trim();
1810
+ }
1811
+ const kept = [];
1812
+ let lastFingerprint = '';
1813
+ for (const rawLine of text.replace(/\r\n/g, '\n').split('\n')) {
1814
+ const cleanedLine = cleanNormalizedSlashPassthroughLine(normalizedCommand, rawLine);
1815
+ if (cleanedLine === null)
1816
+ continue;
1817
+ if (!cleanedLine) {
1818
+ if (kept.length > 0 && kept[kept.length - 1] !== '')
1819
+ kept.push('');
1820
+ continue;
1821
+ }
1822
+ const fingerprint = cleanedLine.toLowerCase();
1823
+ if (fingerprint === lastFingerprint)
1824
+ continue;
1825
+ kept.push(cleanedLine);
1826
+ lastFingerprint = fingerprint;
1827
+ }
1828
+ return kept.join('\n').replace(/\n{3,}/g, '\n\n').trim();
1829
+ }
1830
+ function cleanSlashPassthroughLine(command, rawLine) {
1831
+ const trimmedRight = rawLine.replace(/\s+$/g, '');
1832
+ const trimmed = trimmedRight.trim();
1833
+ if (!trimmed)
1834
+ return '';
1835
+ let withoutChrome = trimmedRight
1836
+ .replace(/^\s*Esc to cancel\s+/i, '')
1837
+ .replace(/(?:^|\s)(?:[>›â»âµâ–¸â–¶]+\s*)?bypass permissions on \(shift(?:\+|-)tab to cycle\).*$/i, '')
1838
+ .replace(/\s+[•●·-]+\s*\/effort\s*$/i, '')
1839
+ .replace(/\s+\/effort\s*$/i, '')
1840
+ .trim();
1841
+ if (!withoutChrome)
1842
+ return null;
1843
+ if (/^[─━│═┄┈]{4,}$/u.test(withoutChrome))
1844
+ return null;
1845
+ if (/^Status(?:\s+Config)?(?:\s+Usage)?(?:\s+Stats)?$/i.test(withoutChrome))
1846
+ return null;
1847
+ if (/^(?:Loading usage data|Scanning local sessions)$/i.test(withoutChrome))
1848
+ return null;
1849
+ if (command === '/usage' && /^[dw]\s+to\s+\w+/i.test(withoutChrome))
1850
+ return null;
1851
+ if (/^(?:[>›â»âµâ–¸â–¶]+\s*)?bypass permissions on \(shift(?:\+|-)tab to cycle\)/i.test(withoutChrome))
1852
+ return null;
1853
+ if (/^permissions on \(shift(?:\+|-)tab to cycle\)/i.test(withoutChrome))
1854
+ return null;
1855
+ return withoutChrome || null;
1856
+ }
1857
+ function cleanNormalizedSlashPassthroughLine(command, rawLine) {
1858
+ const trimmedRight = rawLine.replace(/\s+$/g, '');
1859
+ const trimmed = trimmedRight.trim();
1860
+ if (!trimmed)
1861
+ return '';
1862
+ let withoutChrome = trimmedRight
1863
+ .replace(/^\s*Esc to cancel\s+/i, '')
1864
+ .replace(/(?:^|\s)(?:>+\s*)?bypass permissions on \(shift(?:\+|-)tab to cycle\).*$/i, '')
1865
+ .replace(/\s+[•●·-]+\s*\/effort\s*$/i, '')
1866
+ .replace(/\s+\/effort\s*$/i, '')
1867
+ .trim();
1868
+ if (!withoutChrome)
1869
+ return null;
1870
+ if (/^[\u2500-\u257F\u2580-\u259F=_-]{4,}$/u.test(withoutChrome))
1871
+ return null;
1872
+ if (/^>+\s*$/.test(withoutChrome))
1873
+ return null;
1874
+ if (/^permissions on \(shift(?:\+|-)tab to cycle\)/i.test(withoutChrome))
1875
+ return null;
1876
+ if (/^bypass permissions on \(shift(?:\+|-)tab to cycle\)/i.test(withoutChrome))
1877
+ return null;
1878
+ if (command === '/cost' || command === '/usage') {
1879
+ if (/^Status(?:\s+Config)?(?:\s+Usage)?(?:\s+Stats)?$/i.test(withoutChrome))
1880
+ return null;
1881
+ if (/^(?:Loading usage data|Scanning local sessions)$/i.test(withoutChrome))
1882
+ return null;
1883
+ if (command === '/usage' && /^[dw]\s+to\s+\w+/i.test(withoutChrome))
1884
+ return null;
1885
+ }
1886
+ return withoutChrome || null;
1887
+ }
1888
+ function trimPassthroughSlashChromeForTest(command, rawText) {
1889
+ return trimPassthroughSlashChrome(command, rawText);
1890
+ }
1891
+ function finalizePassthroughContentForTest(command, visibleOutput, rawOutput) {
1892
+ return finalizePassthroughContent(command, visibleOutput, rawOutput);
1893
+ }
421
1894
  class LocalCliPtySessionManager {
422
1895
  sessions = new Map();
1896
+ async warmSession(opts) {
1897
+ const key = sessionKey(opts.conversationId, opts.botId);
1898
+ const isServiceMode = process.env.FUNOLIO_RUN_CONTEXT === 'windows-service'
1899
+ || process.argv.includes('--mode') && (process.argv.includes('service') || process.argv.includes('windows-service'));
1900
+ const requestedUseConpty = opts.useConpty ?? !isServiceMode;
1901
+ let session = this.sessions.get(key);
1902
+ if (session
1903
+ && (session.provider !== opts.provider
1904
+ || session.cwd !== opts.cwd
1905
+ || session.useConpty !== requestedUseConpty
1906
+ || shouldRecycleClaudeSessionForFreshAuth(session)
1907
+ || shouldResetClaudeSessionForAuthChange(session)
1908
+ || session.closed)) {
1909
+ if (session.provider === 'claude-cli' && shouldRecycleClaudeSessionForFreshAuth(session)) {
1910
+ console.info(`[claude-auth] recycling warm session before resume (${opts.botId} ${opts.conversationId})`);
1911
+ }
1912
+ else if (session.provider === 'claude-cli' && shouldResetClaudeSessionForAuthChange(session)) {
1913
+ console.info(`[claude-auth] recycling warm session after auth change (${opts.botId})`);
1914
+ }
1915
+ this.closeSession(key);
1916
+ session = undefined;
1917
+ }
1918
+ const reusedExistingSession = !!session;
1919
+ if (!session) {
1920
+ if (opts.provider === 'claude-cli') {
1921
+ await ensureClaudeAuthReadyForFreshSession();
1922
+ }
1923
+ session = this.createSession(key, opts.botId, opts.conversationId, opts.provider, opts.botSettings, opts.cwd, requestedUseConpty, {
1924
+ actorId: opts.toolActorId,
1925
+ projectId: opts.toolProjectId,
1926
+ todoTaskId: opts.currentTodoTaskId,
1927
+ }, opts.resumeSessionId, opts.newSessionId);
1928
+ this.sessions.set(key, session);
1929
+ }
1930
+ if (opts.topicId !== undefined) {
1931
+ session.topicId = opts.topicId || null;
1932
+ }
1933
+ if (opts.runtimeMode !== undefined) {
1934
+ session.warmRuntimeMode = opts.runtimeMode || null;
1935
+ }
1936
+ if (reusedExistingSession && !session.warmPromise && !session.warmReadyAtMs && session.readyResolved) {
1937
+ (0, managed_process_registry_1.markReused)(key);
1938
+ logCliWarmEvent('warm_reuse_existing', {
1939
+ conversationId: opts.conversationId,
1940
+ botId: opts.botId,
1941
+ provider: opts.provider,
1942
+ runtimeMode: session.warmRuntimeMode || opts.runtimeMode || 'local_desktop',
1943
+ topicId: session.topicId,
1944
+ reusedExistingSession,
1945
+ ageMs: Date.now() - session.lastUsedAtMs,
1946
+ });
1947
+ return {
1948
+ sessionId: session.sessionId,
1949
+ reusedExistingSession,
1950
+ readyAgeMs: Math.max(0, Date.now() - session.lastUsedAtMs),
1951
+ };
1952
+ }
1953
+ if (!session.warmPromise && !session.warmReadyAtMs) {
1954
+ const warmStartedAtMs = Date.now();
1955
+ session.warmRequestedAtMs = warmStartedAtMs;
1956
+ const warmTask = (async () => {
1957
+ await waitForFirstTerminalData(session);
1958
+ if (session.closed) {
1959
+ throw new Error(`${opts.provider} warm session closed before it was ready`);
1960
+ }
1961
+ if (!session.readyResolved) {
1962
+ throw new Error(`${opts.provider} warm session did not produce startup output`);
1963
+ }
1964
+ const authFailure = buildCliInteractiveAuthError(session.provider, session.recentTerminalOutput);
1965
+ if (authFailure) {
1966
+ throw authFailure;
1967
+ }
1968
+ await waitForOutputToSettle(session);
1969
+ session.warmReadyAtMs = Date.now();
1970
+ })();
1971
+ session.warmPromise = withWarmTimeout(warmTask, opts.timeoutMs ?? CLI_WARM_TIMEOUT_MS, () => this.closeSession(key), opts.provider).catch((err) => {
1972
+ if (this.sessions.get(key) === session) {
1973
+ this.closeSession(key);
1974
+ }
1975
+ throw err;
1976
+ });
1977
+ }
1978
+ await session.warmPromise;
1979
+ const readyAtMs = session.warmReadyAtMs || Date.now();
1980
+ return {
1981
+ sessionId: session.sessionId,
1982
+ reusedExistingSession,
1983
+ readyAgeMs: Math.max(0, Date.now() - readyAtMs),
1984
+ };
1985
+ }
423
1986
  async runTurn(opts) {
424
1987
  const key = sessionKey(opts.conversationId, opts.botId);
1988
+ // Default to WinPTY (useConpty: false) when running as a detached sidecar
1989
+ // without a console window. ConPTY requires a console host and fails silently
1990
+ // when the process was launched with CREATE_NO_WINDOW.
1991
+ const isServiceMode = process.env.FUNOLIO_RUN_CONTEXT === 'windows-service'
1992
+ || process.argv.includes('--mode') && (process.argv.includes('service') || process.argv.includes('windows-service'));
1993
+ const requestedUseConpty = opts.useConpty ?? !isServiceMode;
425
1994
  let session = this.sessions.get(key);
1995
+ const preserveFreshWarm = !!session
1996
+ && !!opts.forceFreshSession
1997
+ && session.provider === 'codex-cli'
1998
+ && !!session.warmReadyAtMs
1999
+ && !session.sessionId;
426
2000
  if (session
427
- && (opts.forceFreshSession || session.provider !== opts.provider || session.cwd !== opts.cwd || session.closed)) {
2001
+ && ((opts.forceFreshSession && !preserveFreshWarm)
2002
+ || session.provider !== opts.provider
2003
+ || session.cwd !== opts.cwd
2004
+ || session.useConpty !== requestedUseConpty
2005
+ || shouldRecycleClaudeSessionForFreshAuth(session)
2006
+ || shouldResetClaudeSessionForAuthChange(session)
2007
+ || session.closed)) {
2008
+ if (session.provider === 'claude-cli' && shouldRecycleClaudeSessionForFreshAuth(session)) {
2009
+ console.info(`[claude-auth] recycling turn session before resume (${opts.botId} ${opts.conversationId})`);
2010
+ }
2011
+ else if (session.provider === 'claude-cli' && shouldResetClaudeSessionForAuthChange(session)) {
2012
+ console.info(`[claude-auth] recycling turn session after auth change (${opts.botId})`);
2013
+ }
428
2014
  this.closeSession(key);
429
2015
  session = undefined;
430
2016
  }
2017
+ if (session?.warmPromise) {
2018
+ try {
2019
+ await session.warmPromise;
2020
+ logCliWarmEvent('warm_reused', {
2021
+ conversationId: opts.conversationId,
2022
+ botId: opts.botId,
2023
+ provider: opts.provider,
2024
+ runtimeMode: session.warmRuntimeMode || 'local_desktop',
2025
+ topicId: session.topicId,
2026
+ reusedExistingSession: true,
2027
+ ageMs: session.warmReadyAtMs ? Date.now() - session.warmReadyAtMs : 0,
2028
+ });
2029
+ session.warmPromise = null;
2030
+ session.warmRequestedAtMs = null;
2031
+ session.warmReadyAtMs = null;
2032
+ }
2033
+ catch (err) {
2034
+ logCliWarmEvent('warm_discarded', {
2035
+ conversationId: opts.conversationId,
2036
+ botId: opts.botId,
2037
+ provider: opts.provider,
2038
+ runtimeMode: session.warmRuntimeMode || 'local_desktop',
2039
+ topicId: session.topicId,
2040
+ reusedExistingSession: true,
2041
+ failureReason: buildCliWarmDiscardFailureReason(err),
2042
+ });
2043
+ if (this.sessions.get(key) === session) {
2044
+ this.closeSession(key);
2045
+ }
2046
+ session = undefined;
2047
+ }
2048
+ }
2049
+ else if (session?.warmReadyAtMs) {
2050
+ logCliWarmEvent('warm_reused', {
2051
+ conversationId: opts.conversationId,
2052
+ botId: opts.botId,
2053
+ provider: opts.provider,
2054
+ runtimeMode: session.warmRuntimeMode || 'local_desktop',
2055
+ topicId: session.topicId,
2056
+ reusedExistingSession: true,
2057
+ ageMs: Date.now() - session.warmReadyAtMs,
2058
+ });
2059
+ session.warmPromise = null;
2060
+ session.warmRequestedAtMs = null;
2061
+ session.warmReadyAtMs = null;
2062
+ }
431
2063
  if (!session) {
432
- session = this.createSession(key, opts.provider, opts.cwd);
2064
+ if (opts.provider === 'claude-cli') {
2065
+ await ensureClaudeAuthReadyForFreshSession();
2066
+ }
2067
+ logCliWarmEvent('send_cold', {
2068
+ conversationId: opts.conversationId,
2069
+ botId: opts.botId,
2070
+ provider: opts.provider,
2071
+ });
2072
+ try {
2073
+ session = this.createSession(key, opts.botId, opts.conversationId, opts.provider, opts.botSettings, opts.cwd, requestedUseConpty, {
2074
+ actorId: opts.toolActorId,
2075
+ projectId: opts.toolProjectId,
2076
+ todoTaskId: opts.currentTodoTaskId,
2077
+ }, opts.resumeSessionId, opts.newSessionId);
2078
+ }
2079
+ catch (firstErr) {
2080
+ console.warn(`[pty] Session creation failed, retrying in 1s: ${firstErr instanceof Error ? firstErr.message : String(firstErr)}`);
2081
+ await delay(1000);
2082
+ session = this.createSession(key, opts.botId, opts.conversationId, opts.provider, opts.botSettings, opts.cwd, requestedUseConpty, {
2083
+ actorId: opts.toolActorId,
2084
+ projectId: opts.toolProjectId,
2085
+ todoTaskId: opts.currentTodoTaskId,
2086
+ }, opts.resumeSessionId, opts.newSessionId);
2087
+ }
433
2088
  this.sessions.set(key, session);
434
2089
  }
435
2090
  const run = async () => this.runTurnInternal(session, opts);
@@ -437,9 +2092,107 @@ class LocalCliPtySessionManager {
437
2092
  session.chain = queued.then(() => undefined, () => undefined);
438
2093
  return queued;
439
2094
  }
2095
+ async runPassthroughCommand(opts) {
2096
+ const key = sessionKey(opts.conversationId, opts.botId);
2097
+ const isServiceMode = process.env.FUNOLIO_RUN_CONTEXT === 'windows-service'
2098
+ || process.argv.includes('--mode') && (process.argv.includes('service') || process.argv.includes('windows-service'));
2099
+ const requestedUseConpty = opts.useConpty ?? !isServiceMode;
2100
+ let session = this.sessions.get(key);
2101
+ if (session
2102
+ && (opts.forceFreshSession
2103
+ || session.provider !== opts.provider
2104
+ || session.cwd !== opts.cwd
2105
+ || session.useConpty !== requestedUseConpty
2106
+ || shouldRecycleClaudeSessionForFreshAuth(session)
2107
+ || shouldResetClaudeSessionForAuthChange(session)
2108
+ || session.closed)) {
2109
+ if (session.provider === 'claude-cli' && shouldRecycleClaudeSessionForFreshAuth(session)) {
2110
+ console.info(`[claude-auth] recycling passthrough session before resume (${opts.botId} ${opts.conversationId})`);
2111
+ }
2112
+ else if (session.provider === 'claude-cli' && shouldResetClaudeSessionForAuthChange(session)) {
2113
+ console.info(`[claude-auth] recycling passthrough session after auth change (${opts.botId})`);
2114
+ }
2115
+ this.closeSession(key);
2116
+ session = undefined;
2117
+ }
2118
+ if (!session) {
2119
+ if (opts.provider === 'claude-cli') {
2120
+ await ensureClaudeAuthReadyForFreshSession();
2121
+ }
2122
+ session = this.createSession(key, opts.botId, opts.conversationId, opts.provider, opts.botSettings, opts.cwd, requestedUseConpty, {
2123
+ actorId: opts.toolActorId,
2124
+ projectId: opts.toolProjectId,
2125
+ todoTaskId: opts.currentTodoTaskId,
2126
+ }, opts.resumeSessionId, opts.newSessionId);
2127
+ this.sessions.set(key, session);
2128
+ }
2129
+ const run = async () => this.runPassthroughInternal(session, opts);
2130
+ const queued = session.chain.then(run, run);
2131
+ session.chain = queued.then(() => undefined, () => undefined);
2132
+ return queued;
2133
+ }
2134
+ hasActiveSession(conversationId, botId) {
2135
+ const session = this.sessions.get(sessionKey(conversationId, botId));
2136
+ return !!session && !session.closed;
2137
+ }
440
2138
  closeSessionByConversation(conversationId, botId) {
441
2139
  this.closeSession(sessionKey(conversationId, botId));
442
2140
  }
2141
+ logSessionFailureByConversation(conversationId, botId, context, error) {
2142
+ const session = this.sessions.get(sessionKey(conversationId, botId));
2143
+ if (!session)
2144
+ return;
2145
+ const ptyBuffer = session.recentTerminalOutput || session.activeTurn?.rawOutput || '';
2146
+ const ptyPid = session.pty.pid;
2147
+ console.warn(`[local-cli-pty] ${context}`
2148
+ + ` | sessionId=${session.sessionId || 'unknown'}`
2149
+ + ` | botId=${botId}`
2150
+ + ` | pid=${ptyPid || 'unknown'}`
2151
+ + (error ? ` | error=${String(error?.message || error)}` : '')
2152
+ + (ptyBuffer
2153
+ ? `\n--- PTY buffer (last ${ptyBuffer.length} chars) ---\n${ptyBuffer}\n--- end PTY buffer ---`
2154
+ : ''));
2155
+ }
2156
+ closeSessionsByConversation(conversationId) {
2157
+ const prefix = `${conversationId}::`;
2158
+ let closed = 0;
2159
+ for (const key of [...this.sessions.keys()]) {
2160
+ if (key.startsWith(prefix)) {
2161
+ this.closeSession(key);
2162
+ closed++;
2163
+ }
2164
+ }
2165
+ return closed;
2166
+ }
2167
+ closeWarmSessionsByConversation(conversationId) {
2168
+ const prefix = `${conversationId}::`;
2169
+ let closed = 0;
2170
+ for (const [key, session] of [...this.sessions.entries()]) {
2171
+ if (key.startsWith(prefix) && (session.warmPromise || session.warmReadyAtMs)) {
2172
+ this.closeSession(key);
2173
+ closed++;
2174
+ }
2175
+ }
2176
+ return closed;
2177
+ }
2178
+ closeIdleClaudeSessions() {
2179
+ let closed = 0;
2180
+ for (const [key, session] of [...this.sessions.entries()]) {
2181
+ if (!isIdleClaudeSession(session))
2182
+ continue;
2183
+ this.closeSession(key);
2184
+ closed++;
2185
+ }
2186
+ return closed;
2187
+ }
2188
+ closeSessionsByBotId(botId) {
2189
+ const suffix = `::${botId}`;
2190
+ for (const key of [...this.sessions.keys()]) {
2191
+ if (key.endsWith(suffix)) {
2192
+ this.closeSession(key);
2193
+ }
2194
+ }
2195
+ }
443
2196
  closeAll() {
444
2197
  for (const key of [...this.sessions.keys()]) {
445
2198
  this.closeSession(key);
@@ -450,147 +2203,524 @@ class LocalCliPtySessionManager {
450
2203
  if (!session)
451
2204
  return;
452
2205
  session.closed = true;
453
- try {
454
- session.pty.kill();
2206
+ clearSessionWarmState(session);
2207
+ const ptyPid = session.pty.pid;
2208
+ let closeFailed = false;
2209
+ if (ptyPid) {
2210
+ const killResult = (0, managed_process_registry_1.killProcessTreeDetailed)(ptyPid);
2211
+ closeFailed = !killResult.killed || !!killResult.error;
455
2212
  }
456
- catch {
457
- // best effort
2213
+ else {
2214
+ try {
2215
+ session.pty.kill();
2216
+ }
2217
+ catch {
2218
+ closeFailed = true;
2219
+ }
458
2220
  }
459
2221
  this.sessions.delete(key);
2222
+ (0, managed_process_registry_1.unregisterProcess)(key, false, closeFailed);
2223
+ }
2224
+ closeSessionByKey(key) {
2225
+ this.closeSession(key);
460
2226
  }
461
- createSession(key, provider, cwd) {
2227
+ createSession(key, botId, conversationId, provider, botSettings, cwd, useConpty, toolEnv, resumeSessionId, newSessionId) {
462
2228
  const ptyModule = loadNodePtyModule();
2229
+ const claudeSessionHome = provider === 'claude-cli'
2230
+ ? (0, sync_cli_config_1.ensureCliSessionRuntimeHome)('claude-cli', botId, conversationId)
2231
+ : null;
2232
+ const sessionFilesRoot = getProviderSessionRoot(provider, claudeSessionHome || undefined);
463
2233
  const cleanEnv = { ...process.env };
464
- delete cleanEnv.CLAUDECODE;
465
- delete cleanEnv.CLAUDE_CODE_ENTRYPOINT;
466
- delete cleanEnv.CLAUDE_CODE_SESSION_ID;
2234
+ for (const envKey of Object.keys(cleanEnv)) {
2235
+ if (envKey === 'CLAUDECODE' || envKey.startsWith('CLAUDE_CODE_')) {
2236
+ delete cleanEnv[envKey];
2237
+ }
2238
+ }
2239
+ if (claudeSessionHome) {
2240
+ cleanEnv.CLAUDE_CONFIG_DIR = claudeSessionHome;
2241
+ }
2242
+ if (toolEnv?.actorId?.trim())
2243
+ cleanEnv.FUNOLIO_TOOL_ACTOR_ID = toolEnv.actorId.trim();
2244
+ if (toolEnv?.projectId !== undefined && toolEnv.projectId !== null)
2245
+ cleanEnv.FUNOLIO_TOOL_PROJECT_ID = String(toolEnv.projectId);
2246
+ if (toolEnv?.todoTaskId !== undefined && toolEnv.todoTaskId !== null)
2247
+ cleanEnv.FUNOLIO_TOOL_TODO_ID = String(toolEnv.todoTaskId);
2248
+ const markerEnv = (0, managed_process_registry_1.getMarkerEnv)(key);
2249
+ for (const [mk, mv] of Object.entries(markerEnv)) {
2250
+ cleanEnv[mk] = mv;
2251
+ }
2252
+ if (provider === 'claude-cli') {
2253
+ (0, sync_cli_config_1.syncClaudeJsonConfig)({
2254
+ ...(toolEnv?.actorId?.trim() ? { FUNOLIO_TOOL_ACTOR_ID: toolEnv.actorId.trim() } : {}),
2255
+ ...(toolEnv?.projectId !== undefined && toolEnv.projectId !== null ? { FUNOLIO_TOOL_PROJECT_ID: String(toolEnv.projectId) } : {}),
2256
+ }, (0, sync_cli_config_1.claudeJsonConfigPath)(claudeSessionHome || undefined));
2257
+ }
2258
+ else {
2259
+ void (0, sync_cli_config_1.syncMcpToCliConfig)('codex-cli', {
2260
+ ...(toolEnv?.actorId?.trim() ? { FUNOLIO_TOOL_ACTOR_ID: toolEnv.actorId.trim() } : {}),
2261
+ ...(toolEnv?.projectId !== undefined && toolEnv.projectId !== null ? { FUNOLIO_TOOL_PROJECT_ID: String(toolEnv.projectId) } : {}),
2262
+ }).catch(() => { });
2263
+ }
2264
+ // Snapshot existing session files BEFORE spawning the PTY.
2265
+ // Only needed for Pattern B providers (no --session-id support).
2266
+ // Pattern A providers know their file path from the generated ID.
2267
+ const preSpawnSnapshot = new Set(listSessionFiles(provider, sessionFilesRoot));
467
2268
  let readyResolve = null;
468
2269
  const readyPromise = new Promise((resolve) => {
469
2270
  readyResolve = resolve;
470
2271
  });
2272
+ // Build Claude CLI args with session control:
2273
+ // Claude: when launching a brand-new process, prefer a fresh Claude
2274
+ // session-id over reviving a dead Claude-side session with --resume.
2275
+ // Codex: no session flags on new, codex resume <id> for existing.
2276
+ const claudeLaunch = resolveClaudeLaunchSessionIds(resumeSessionId, newSessionId);
2277
+ const claudeArgs = buildClaudeCliArgs(claudeLaunch.resumeSessionId, claudeLaunch.newSessionId, botSettings?.claude);
2278
+ // Determine known session info for transcript discovery.
2279
+ const knownSessionId = claudeLaunch.knownSessionId;
2280
+ let knownSessionFilePath = null;
2281
+ if (knownSessionId && provider === 'claude-cli') {
2282
+ // Search all project directories for the session file.
2283
+ // Don't try to derive the directory name — Claude's naming convention
2284
+ // (e.g., C--Projects-Funolio) doesn't match simple path normalization.
2285
+ const projectRoot = sessionFilesRoot;
2286
+ if (fs.existsSync(projectRoot)) {
2287
+ try {
2288
+ const projectDirs = fs.readdirSync(projectRoot);
2289
+ for (const dir of projectDirs) {
2290
+ const candidate = path.join(projectRoot, dir, `${knownSessionId}.jsonl`);
2291
+ if (fs.existsSync(candidate)) {
2292
+ knownSessionFilePath = candidate;
2293
+ break;
2294
+ }
2295
+ }
2296
+ }
2297
+ catch { }
2298
+ }
2299
+ // For new sessions, file doesn't exist yet. We'll find it after Claude creates it
2300
+ // by searching again in the polling loop (or it'll be discovered via discoverSessionFile).
2301
+ }
2302
+ else if (knownSessionId && provider === 'codex-cli') {
2303
+ knownSessionFilePath = findCodexSessionFileBySessionId(knownSessionId);
2304
+ }
2305
+ const codexArgs = buildCodexCliArgs(resumeSessionId, botSettings?.codex);
471
2306
  const pty = provider === 'codex-cli'
472
- ? ptyModule.spawn(findExecutableOnPath('codex.cmd') || 'codex.cmd', ['--no-alt-screen'], {
2307
+ ? ptyModule.spawn(findExecutableOnPath('codex.cmd') || 'codex.cmd', codexArgs, {
473
2308
  cwd,
474
2309
  cols: 160,
475
2310
  rows: 48,
476
2311
  env: cleanEnv,
477
- useConpty: true,
2312
+ useConpty,
478
2313
  name: 'xterm-color',
479
2314
  })
480
- : ptyModule.spawn('cmd.exe', [], {
2315
+ : ptyModule.spawn(findExecutableOnPath('claude.exe') || findExecutableOnPath('claude.cmd') || 'claude', claudeArgs, {
481
2316
  cwd,
482
2317
  cols: 160,
483
2318
  rows: 48,
484
2319
  env: cleanEnv,
485
- useConpty: true,
2320
+ useConpty,
486
2321
  name: 'xterm-color',
487
2322
  });
488
2323
  const session = {
489
2324
  key,
2325
+ conversationId,
2326
+ botId,
2327
+ topicId: null,
2328
+ warmRuntimeMode: null,
490
2329
  provider,
491
2330
  cwd,
2331
+ useConpty,
492
2332
  pty,
493
2333
  createdAtMs: Date.now(),
494
2334
  lastUsedAtMs: Date.now(),
495
- launchSnapshot: new Set(listSessionFiles(provider)),
496
- sessionId: null,
497
- sessionFilePath: null,
2335
+ launchSnapshot: preSpawnSnapshot,
2336
+ sessionFilesRoot,
2337
+ sessionId: knownSessionId,
2338
+ sessionFilePath: knownSessionFilePath,
498
2339
  sessionFileOffset: 0,
499
2340
  sessionFileCarry: '',
500
2341
  readyPromise,
501
2342
  readyResolved: false,
502
- waitForNextSendMs: 250,
503
- startupDelayMs: provider === 'codex-cli' ? 9000 : 1200,
504
- submitDelayMs: provider === 'codex-cli' ? 350 : 0,
2343
+ waitForNextSendMs: 100,
2344
+ startupDelayMs: 1200,
2345
+ startupDelayApplied: false,
2346
+ submitDelayMs: provider === 'codex-cli' ? 175 : 200,
505
2347
  currentPromptLocator: null,
506
2348
  currentPromptStartedAtMs: 0,
2349
+ activeTurn: null,
2350
+ warmPromise: null,
2351
+ warmRequestedAtMs: null,
2352
+ warmReadyAtMs: null,
2353
+ recentTerminalOutput: '',
507
2354
  closed: false,
2355
+ exitReason: null,
508
2356
  chain: Promise.resolve(),
2357
+ childFollowers: new Map(),
2358
+ childSnapshot: new Set(),
2359
+ claudeAuthFingerprint: provider === 'claude-cli' ? currentClaudeAuthFingerprint() : null,
2360
+ runtimeHomeDir: claudeSessionHome,
509
2361
  };
2362
+ const ptyPid = pty.pid;
2363
+ if (ptyPid) {
2364
+ (0, managed_process_registry_1.registerProcess)({
2365
+ sessionKey: key,
2366
+ provider,
2367
+ conversationId,
2368
+ botId,
2369
+ pid: ptyPid,
2370
+ cwd,
2371
+ createdAt: new Date(session.createdAtMs).toISOString(),
2372
+ lastUsedAt: new Date(session.lastUsedAtMs).toISOString(),
2373
+ });
2374
+ }
510
2375
  pty.on('data', (chunk) => {
2376
+ if (chunk) {
2377
+ (0, managed_process_registry_1.markTurnActivity)(session.key, 'pty_data');
2378
+ session.recentTerminalOutput = appendRecentTerminalOutput(session.recentTerminalOutput, chunk);
2379
+ }
511
2380
  if (!session.readyResolved && chunk && chunk.trim()) {
512
2381
  session.readyResolved = true;
513
2382
  readyResolve?.();
514
2383
  }
2384
+ if (session.activeTurn && chunk) {
2385
+ void emitPtyChunk(session, chunk);
2386
+ }
515
2387
  });
516
- pty.on('exit', () => {
2388
+ pty.on('exit', (...exitArgs) => {
517
2389
  session.closed = true;
2390
+ session.exitReason = describePtyExit(exitArgs);
2391
+ console.warn(`[local-cli-pty] ${provider} PTY exited (${session.exitReason}) cwd=${cwd}`);
518
2392
  this.sessions.delete(key);
2393
+ (0, managed_process_registry_1.unregisterProcess)(key, false);
519
2394
  });
520
- if (provider === 'claude-cli') {
521
- pty.write('claude\r');
522
- }
523
2395
  return session;
524
2396
  }
525
- async runTurnInternal(session, opts) {
526
- session.lastUsedAtMs = Date.now();
527
- await waitForFirstTerminalData(session);
528
- if (session.closed) {
529
- throw new Error(`${session.provider} PTY session closed before prompt was sent`);
530
- }
531
- if (session.waitForNextSendMs > 0) {
532
- await delay(session.waitForNextSendMs);
533
- }
534
- const tracker = {
535
- done: false,
536
- finalContent: '',
537
- usage: undefined,
538
- lastAssistantText: '',
539
- detailFingerprints: new Set(),
2397
+ async runPassthroughInternal(session, opts) {
2398
+ const abortSignal = opts.abortSignal;
2399
+ const abortHandler = () => {
2400
+ this.closeSession(session.key);
540
2401
  };
541
- const timeoutMs = opts.timeoutMs ?? 10 * 60 * 1000;
542
- const startedAtMs = Date.now();
543
- const promptText = buildTurnPrompt(opts.provider, opts.systemPrompt, opts.messages, opts.forceFreshSession || !session.sessionFilePath);
544
- session.currentPromptLocator = promptText.trim();
545
- session.currentPromptStartedAtMs = startedAtMs;
546
- await writeInteractivePrompt(session, promptText);
547
- while (!tracker.done) {
548
- if (Date.now() - startedAtMs > timeoutMs) {
549
- throw new Error(`${opts.provider} PTY session timed out waiting for a response`);
2402
+ abortSignal?.addEventListener('abort', abortHandler, { once: true });
2403
+ try {
2404
+ session.lastUsedAtMs = Date.now();
2405
+ (0, managed_process_registry_1.updateLastUsed)(session.key);
2406
+ (0, managed_process_registry_1.markTurnStarted)(session.key, 'pty_passthrough');
2407
+ throwIfAborted(abortSignal);
2408
+ await waitForFirstTerminalData(session);
2409
+ throwIfAborted(abortSignal);
2410
+ if (session.closed) {
2411
+ throw formatClosedSessionError(session, `closed before ${opts.command} was sent`);
2412
+ }
2413
+ if (session.waitForNextSendMs > 0) {
2414
+ await delayWithAbort(session.waitForNextSendMs, abortSignal);
2415
+ }
2416
+ const timeoutMs = opts.timeoutMs ?? 30_000;
2417
+ const startedAtMs = Date.now();
2418
+ const activeTurn = {
2419
+ mode: 'passthrough',
2420
+ promptEchoRemainder: normalizeTerminalChunk(opts.command),
2421
+ rawOutput: '',
2422
+ visibleOutput: '',
2423
+ lastDataAtMs: startedAtMs,
2424
+ lastMeaningfulPtyDataAtMs: startedAtMs,
2425
+ callbackChain: Promise.resolve(),
2426
+ onRawChunk: opts.onRawChunk,
2427
+ recentChromeLines: [],
2428
+ assistantOutputDetected: false,
2429
+ sawVisibleData: false,
2430
+ };
2431
+ session.activeTurn = activeTurn;
2432
+ throwIfAborted(abortSignal);
2433
+ await writeNakedPrompt(session, opts.command);
2434
+ while (true) {
2435
+ throwIfAborted(abortSignal);
2436
+ const now = Date.now();
2437
+ const authFailure = getTerminalAuthFailure(session, activeTurn);
2438
+ if (authFailure) {
2439
+ if (session.provider === 'claude-cli')
2440
+ this.closeSession(session.key);
2441
+ throw authFailure;
2442
+ }
2443
+ if (!activeTurn.rawOutput) {
2444
+ if (now - startedAtMs > PASSTHROUGH_FIRST_BYTE_TIMEOUT_MS) {
2445
+ throw new Error(`${session.provider} PTY session returned no output for ${opts.command}`);
2446
+ }
2447
+ }
2448
+ else if (now - activeTurn.lastDataAtMs >= PASSTHROUGH_TRAILING_IDLE_MS) {
2449
+ break;
2450
+ }
2451
+ if (now - startedAtMs > timeoutMs) {
2452
+ throw new Error(`${session.provider} PTY session timed out waiting for ${opts.command}`);
2453
+ }
2454
+ await delayWithAbort(50, abortSignal);
550
2455
  }
2456
+ await activeTurn.callbackChain.catch(() => undefined);
2457
+ return {
2458
+ content: finalizePassthroughContent(opts.command, activeTurn.visibleOutput, activeTurn.rawOutput),
2459
+ sessionId: session.sessionId,
2460
+ rawOutput: activeTurn.rawOutput,
2461
+ };
2462
+ }
2463
+ finally {
2464
+ syncClaudeSessionCredentialsBackToCanonical(session);
2465
+ session.activeTurn = null;
2466
+ (0, managed_process_registry_1.markTurnFinished)(session.key, 'pty_passthrough_finished');
2467
+ abortSignal?.removeEventListener('abort', abortHandler);
2468
+ }
2469
+ }
2470
+ async runTurnInternal(session, opts) {
2471
+ const abortSignal = opts.abortSignal;
2472
+ const abortHandler = () => {
2473
+ this.closeSession(session.key);
2474
+ };
2475
+ abortSignal?.addEventListener('abort', abortHandler, { once: true });
2476
+ try {
2477
+ session.lastUsedAtMs = Date.now();
2478
+ (0, managed_process_registry_1.updateLastUsed)(session.key);
2479
+ (0, managed_process_registry_1.markTurnStarted)(session.key, 'pty_turn');
2480
+ throwIfAborted(abortSignal);
2481
+ await waitForFirstTerminalData(session);
2482
+ throwIfAborted(abortSignal);
551
2483
  if (session.closed) {
552
- throw new Error(`${opts.provider} PTY session exited while waiting for a response`);
2484
+ throw formatClosedSessionError(session, 'closed before prompt was sent');
553
2485
  }
554
- if (!session.sessionFilePath) {
555
- const discovered = discoverSessionFile(session.provider, session.launchSnapshot, session.currentPromptStartedAtMs || session.createdAtMs, session.currentPromptLocator);
556
- if (discovered) {
557
- session.sessionFilePath = discovered;
558
- session.sessionFileOffset = 0;
2486
+ // Reset the read offset at the start of every turn so we only consume records
2487
+ // appended after this prompt. A shared Claude session file can be advanced by
2488
+ // another caller between turns, and replaying that backlog can finalize the
2489
+ // wrong assistant response before the current prompt lands.
2490
+ if (session.sessionFilePath) {
2491
+ try {
2492
+ const currentSize = fs.statSync(session.sessionFilePath).size;
2493
+ session.sessionFileOffset = currentSize;
559
2494
  session.sessionFileCarry = '';
560
2495
  }
2496
+ catch {
2497
+ // File may not exist yet for new sessions — that's fine.
2498
+ }
561
2499
  }
562
- if (session.sessionFilePath && fs.existsSync(session.sessionFilePath)) {
563
- await this.consumeSessionFile(session, tracker, opts.onDetail);
2500
+ if (session.waitForNextSendMs > 0) {
2501
+ await delayWithAbort(session.waitForNextSendMs, abortSignal);
564
2502
  }
565
- if (!tracker.done) {
566
- await delay(250);
2503
+ const timeoutMs = opts.timeoutMs ?? 10 * 60 * 1000;
2504
+ const startedAtMs = Date.now();
2505
+ const tracker = {
2506
+ done: false,
2507
+ sawExplicitCompletion: false,
2508
+ finalContent: '',
2509
+ usage: undefined,
2510
+ lastAssistantText: '',
2511
+ detailFingerprints: new Set(),
2512
+ pendingToolUseIds: new Set(),
2513
+ lastRecordAtMs: Date.now(),
2514
+ sawCompletionSentinel: false,
2515
+ turnStartedAtMs: startedAtMs,
2516
+ expectedUserPromptSnippet: extractCurrentUserPromptSnippet(opts.messages),
2517
+ sawCurrentTurnUserRecord: false,
2518
+ };
2519
+ const waitForFreshClaudeTranscript = opts.forceFreshSession && session.provider === 'claude-cli';
2520
+ const promptText = buildTurnPrompt(opts.provider, opts.systemPrompt, opts.messages, opts.forceFreshSession || !session.sessionFilePath, opts.cwd);
2521
+ session.currentPromptLocator = promptText.trim();
2522
+ session.currentPromptStartedAtMs = startedAtMs;
2523
+ const activeTurn = {
2524
+ mode: 'default',
2525
+ promptEchoRemainder: normalizeTerminalChunk(promptText),
2526
+ rawOutput: '',
2527
+ visibleOutput: '',
2528
+ lastDataAtMs: startedAtMs,
2529
+ lastMeaningfulPtyDataAtMs: startedAtMs,
2530
+ callbackChain: Promise.resolve(),
2531
+ onChunk: opts.onChunk,
2532
+ onRawChunk: opts.onRawChunk,
2533
+ recentChromeLines: [],
2534
+ assistantOutputDetected: false,
2535
+ sawVisibleData: false,
2536
+ };
2537
+ session.activeTurn = activeTurn;
2538
+ // Snapshot existing child subagent files before this turn
2539
+ session.childFollowers.clear();
2540
+ if (session.sessionFilePath && session.provider === 'claude-cli') {
2541
+ const sessionDir = session.sessionFilePath.replace(/\.jsonl$/, '');
2542
+ const subagentsDir = path.join(sessionDir, 'subagents');
2543
+ if (fs.existsSync(subagentsDir)) {
2544
+ try {
2545
+ const existing = fs.readdirSync(subagentsDir).filter((f) => f.endsWith('.jsonl'));
2546
+ session.childSnapshot = new Set(existing.map((f) => path.join(subagentsDir, f)));
2547
+ }
2548
+ catch {
2549
+ session.childSnapshot = new Set();
2550
+ }
2551
+ }
2552
+ else {
2553
+ session.childSnapshot = new Set();
2554
+ }
2555
+ }
2556
+ throwIfAborted(abortSignal);
2557
+ await writeInteractivePrompt(session, promptText);
2558
+ (0, managed_process_registry_1.markTurnActivity)(session.key, 'prompt_sent');
2559
+ let lastHeartbeatAtMs = Date.now();
2560
+ const HEARTBEAT_INTERVAL_MS = 120_000; // 2 minutes
2561
+ while (!tracker.done) {
2562
+ throwIfAborted(abortSignal);
2563
+ if (Date.now() - startedAtMs > timeoutMs) {
2564
+ throw new Error(`${opts.provider} PTY session timed out waiting for a response`);
2565
+ }
2566
+ if (!activeTurn.assistantOutputDetected && !tracker.lastAssistantText.trim()) {
2567
+ const authFailure = getTerminalAuthFailure(session, activeTurn);
2568
+ if (authFailure) {
2569
+ if (session.provider === 'claude-cli')
2570
+ this.closeSession(session.key);
2571
+ throw authFailure;
2572
+ }
2573
+ }
2574
+ const inactivityFailTimeoutMs = getPtyInactivityFailTimeoutMs(session.provider);
2575
+ if (inactivityFailTimeoutMs != null
2576
+ && Date.now() - activeTurn.lastMeaningfulPtyDataAtMs > inactivityFailTimeoutMs) {
2577
+ if (session.sessionFilePath && fs.existsSync(session.sessionFilePath)) {
2578
+ await this.consumeSessionFile(session, tracker, opts.onChunk, opts.onDetail);
2579
+ }
2580
+ if (tracker.done) {
2581
+ break;
2582
+ }
2583
+ throw new Error(`${opts.provider} PTY session had no meaningful PTY activity for ${Math.floor(inactivityFailTimeoutMs / 1000)}s`);
2584
+ }
2585
+ // Heartbeat: if no detail emitted for 2+ minutes, send a status pulse
2586
+ if (opts.onDetail && Date.now() - lastHeartbeatAtMs > HEARTBEAT_INTERVAL_MS) {
2587
+ const elapsed = Math.floor((Date.now() - startedAtMs) / 1000);
2588
+ await opts.onDetail(`⏳ Still working... (${elapsed}s elapsed)`);
2589
+ lastHeartbeatAtMs = Date.now();
2590
+ }
2591
+ if (!session.sessionFilePath) {
2592
+ // Pattern A (known session ID): search for our specific file by ID
2593
+ if (session.sessionId && session.provider === 'claude-cli') {
2594
+ const projectRoot = session.sessionFilesRoot;
2595
+ if (fs.existsSync(projectRoot)) {
2596
+ try {
2597
+ for (const dir of fs.readdirSync(projectRoot)) {
2598
+ const candidate = path.join(projectRoot, dir, `${session.sessionId}.jsonl`);
2599
+ if (fs.existsSync(candidate)) {
2600
+ session.sessionFilePath = candidate;
2601
+ session.sessionFileOffset = 0;
2602
+ session.sessionFileCarry = '';
2603
+ break;
2604
+ }
2605
+ }
2606
+ }
2607
+ catch { }
2608
+ }
2609
+ }
2610
+ // Pattern B (unknown ID): discover by snapshot diff
2611
+ if (!session.sessionFilePath) {
2612
+ const discovered = discoverSessionFile(session.provider, session.launchSnapshot, session.currentPromptStartedAtMs || session.createdAtMs, session.currentPromptLocator, session.sessionFilesRoot);
2613
+ if (discovered) {
2614
+ session.sessionFilePath = discovered;
2615
+ session.sessionFileOffset = 0;
2616
+ session.sessionFileCarry = '';
2617
+ }
2618
+ }
2619
+ }
2620
+ if (session.sessionFilePath && fs.existsSync(session.sessionFilePath)) {
2621
+ await this.consumeSessionFile(session, tracker, opts.onChunk, opts.onDetail);
2622
+ }
2623
+ if (waitForFreshClaudeTranscript
2624
+ && !(session.sessionFilePath && fs.existsSync(session.sessionFilePath))) {
2625
+ const now = Date.now();
2626
+ if (!shouldContinueWaitingForFreshClaudeSession(session, activeTurn, startedAtMs, now)) {
2627
+ const ptyBuffer = session.recentTerminalOutput || activeTurn.rawOutput || '';
2628
+ const ptyPid = session.pty.pid;
2629
+ if (ptyBuffer) {
2630
+ console.warn(`[local-cli-pty] Failed fresh session PTY output before kill`
2631
+ + ` | sessionId=${session.sessionId || 'unknown'}`
2632
+ + ` | botId=${session.botId}`
2633
+ + ` | pid=${ptyPid || 'unknown'}`
2634
+ + `\n--- PTY buffer (last ${ptyBuffer.length} chars) ---\n${ptyBuffer}\n--- end PTY buffer ---`);
2635
+ }
2636
+ else {
2637
+ console.warn(`[local-cli-pty] Failed fresh session with no PTY output`
2638
+ + ` | sessionId=${session.sessionId || 'unknown'}`
2639
+ + ` | botId=${session.botId}`
2640
+ + ` | pid=${ptyPid || 'unknown'}`);
2641
+ }
2642
+ const authFailure = getTerminalAuthFailure(session, activeTurn);
2643
+ if (authFailure) {
2644
+ this.closeSession(session.key);
2645
+ throw authFailure;
2646
+ }
2647
+ this.closeSession(session.key);
2648
+ throw buildClaudeFreshSessionStartupError(session.sessionId, now - startedAtMs);
2649
+ }
2650
+ }
2651
+ if (session._pendingTitle) {
2652
+ session._pendingTitle = null;
2653
+ }
2654
+ // Fix 12: Follow child sub-agent JSON files for live progress
2655
+ if (session.sessionFilePath && session.provider === 'claude-cli' && opts.onDetail) {
2656
+ await this.consumeChildSubagentFiles(session, tracker, opts.onDetail);
2657
+ }
2658
+ if (session.closed) {
2659
+ if (!activeTurn.assistantOutputDetected && !tracker.lastAssistantText.trim()) {
2660
+ const authFailure = getTerminalAuthFailure(session, activeTurn);
2661
+ if (authFailure) {
2662
+ if (session.provider === 'claude-cli')
2663
+ this.closeSession(session.key);
2664
+ throw authFailure;
2665
+ }
2666
+ }
2667
+ if (canFinalizeClaudeTurnOnSessionExit(session, tracker)) {
2668
+ tracker.done = true;
2669
+ tracker.finalContent = tracker.finalContent || tracker.lastAssistantText;
2670
+ break;
2671
+ }
2672
+ throw formatClosedSessionError(session, 'exited while waiting for a response');
2673
+ }
2674
+ if (!tracker.done) {
2675
+ await delayWithAbort(200, abortSignal);
2676
+ }
2677
+ }
2678
+ const settleStartedAt = Date.now();
2679
+ while (Date.now() - Math.max(activeTurn.lastDataAtMs, settleStartedAt) < 100) {
2680
+ throwIfAborted(abortSignal);
2681
+ if (Date.now() - settleStartedAt > 500)
2682
+ break;
2683
+ await delayWithAbort(50, abortSignal);
567
2684
  }
2685
+ if (session.sessionFilePath && fs.existsSync(session.sessionFilePath)) {
2686
+ await this.consumeSessionFile(session, tracker, opts.onChunk, opts.onDetail);
2687
+ }
2688
+ await activeTurn.callbackChain;
2689
+ session.lastUsedAtMs = Date.now();
2690
+ session.waitForNextSendMs = 150;
2691
+ return {
2692
+ content: (tracker.finalContent || tracker.lastAssistantText).trim(),
2693
+ sessionId: session.sessionId,
2694
+ usage: tracker.usage,
2695
+ rawOutput: activeTurn.rawOutput,
2696
+ };
2697
+ }
2698
+ finally {
2699
+ syncClaudeSessionCredentialsBackToCanonical(session);
2700
+ session.activeTurn = null;
2701
+ (0, managed_process_registry_1.markTurnFinished)(session.key, 'pty_turn_finished');
2702
+ abortSignal?.removeEventListener('abort', abortHandler);
568
2703
  }
569
- session.lastUsedAtMs = Date.now();
570
- session.waitForNextSendMs = 400;
571
- return {
572
- content: tracker.finalContent.trim(),
573
- sessionId: session.sessionId,
574
- usage: tracker.usage,
575
- };
576
2704
  }
577
- async consumeSessionFile(session, tracker, onDetail) {
2705
+ async consumeSessionFile(session, tracker, onChunk, onDetail) {
578
2706
  if (!session.sessionFilePath)
579
2707
  return;
580
2708
  let stat;
581
2709
  try {
582
- stat = fs.statSync(session.sessionFilePath);
2710
+ stat = await fs.promises.stat(session.sessionFilePath);
583
2711
  }
584
2712
  catch {
585
2713
  return;
586
2714
  }
587
2715
  if (stat.size <= session.sessionFileOffset)
588
2716
  return;
589
- const fd = fs.openSync(session.sessionFilePath, 'r');
2717
+ (0, managed_process_registry_1.markTurnActivity)(session.key, 'transcript_data');
2718
+ let fh = null;
590
2719
  try {
2720
+ fh = await fs.promises.open(session.sessionFilePath, 'r');
591
2721
  const length = stat.size - session.sessionFileOffset;
592
2722
  const buffer = Buffer.alloc(length);
593
- fs.readSync(fd, buffer, 0, length, session.sessionFileOffset);
2723
+ await fh.read(buffer, 0, length, session.sessionFileOffset);
594
2724
  session.sessionFileOffset = stat.size;
595
2725
  const text = session.sessionFileCarry + buffer.toString('utf8');
596
2726
  const lines = text.split('\n');
@@ -606,14 +2736,118 @@ class LocalCliPtySessionManager {
606
2736
  catch {
607
2737
  continue;
608
2738
  }
609
- await this.applyRecord(session, tracker, record, onDetail);
2739
+ await this.applyRecord(session, tracker, record, onChunk, onDetail);
610
2740
  }
611
2741
  }
612
2742
  finally {
613
- fs.closeSync(fd);
2743
+ await fh?.close();
2744
+ }
2745
+ }
2746
+ /**
2747
+ * Fix 12: Watch for child sub-agent .jsonl files and emit curated progress.
2748
+ * Claude sub-agents write to: <parent-session-folder>/subagents/agent-<id>.jsonl
2749
+ * We discover new files by watching the directory (not by deriving from agentId,
2750
+ * which isn't available until the sub-agent completes).
2751
+ */
2752
+ async consumeChildSubagentFiles(session, tracker, onDetail) {
2753
+ if (!session.sessionFilePath)
2754
+ return;
2755
+ // Derive the subagents directory from the session file path
2756
+ const sessionDir = session.sessionFilePath.replace(/\.jsonl$/, '');
2757
+ const subagentsDir = path.join(sessionDir, 'subagents');
2758
+ if (!fs.existsSync(subagentsDir))
2759
+ return;
2760
+ // Scan for new child files
2761
+ try {
2762
+ const entries = fs.readdirSync(subagentsDir).filter((f) => f.endsWith('.jsonl'));
2763
+ for (const entry of entries) {
2764
+ const childPath = path.join(subagentsDir, entry);
2765
+ if (session.childSnapshot.has(childPath))
2766
+ continue; // Already known from before this turn
2767
+ // Start following this child file if not already
2768
+ if (!session.childFollowers.has(childPath)) {
2769
+ session.childFollowers.set(childPath, { offset: 0, carry: '' });
2770
+ }
2771
+ const follower = session.childFollowers.get(childPath);
2772
+ let stat;
2773
+ try {
2774
+ stat = fs.statSync(childPath);
2775
+ }
2776
+ catch {
2777
+ continue;
2778
+ }
2779
+ if (stat.size <= follower.offset)
2780
+ continue;
2781
+ // Read new content
2782
+ let fh = null;
2783
+ try {
2784
+ fh = await fs.promises.open(childPath, 'r');
2785
+ const length = stat.size - follower.offset;
2786
+ const buffer = Buffer.alloc(length);
2787
+ await fh.read(buffer, 0, length, follower.offset);
2788
+ follower.offset = stat.size;
2789
+ const text = follower.carry + buffer.toString('utf8');
2790
+ const lines = text.split('\n');
2791
+ follower.carry = lines.pop() || '';
2792
+ for (const line of lines) {
2793
+ const trimmed = line.trim();
2794
+ if (!trimmed)
2795
+ continue;
2796
+ let record;
2797
+ try {
2798
+ record = JSON.parse(trimmed);
2799
+ }
2800
+ catch {
2801
+ continue;
2802
+ }
2803
+ // Extract curated child progress
2804
+ if (record?.type === 'assistant' && record?.message?.content) {
2805
+ const blocks = Array.isArray(record.message.content) ? record.message.content : [];
2806
+ for (const block of blocks) {
2807
+ if (!block || typeof block !== 'object')
2808
+ continue;
2809
+ if (block.type === 'text' && typeof block.text === 'string') {
2810
+ const shortText = block.text.trim().slice(0, 150);
2811
+ if (shortText) {
2812
+ const activity = {
2813
+ type: 'subagent_working',
2814
+ label: shortText,
2815
+ key: childPath,
2816
+ timestamp: Date.now(),
2817
+ };
2818
+ await onDetail(`__ACTIVITY__${JSON.stringify(activity)}`);
2819
+ }
2820
+ }
2821
+ else if (block.type === 'tool_use') {
2822
+ const toolName = typeof block.name === 'string' ? block.name : 'tool';
2823
+ const activityLabel = (0, live_activity_1.toolUseToActivityLabel)(toolName);
2824
+ if (activityLabel) {
2825
+ const activity = {
2826
+ type: 'subagent_working',
2827
+ label: activityLabel,
2828
+ key: childPath,
2829
+ timestamp: Date.now(),
2830
+ };
2831
+ await onDetail(`__ACTIVITY__${JSON.stringify(activity)}`);
2832
+ }
2833
+ }
2834
+ }
2835
+ }
2836
+ }
2837
+ }
2838
+ finally {
2839
+ await fh?.close();
2840
+ }
2841
+ }
2842
+ }
2843
+ catch {
2844
+ // subagents dir not readable — skip silently
614
2845
  }
615
2846
  }
616
- async applyRecord(session, tracker, record, onDetail) {
2847
+ async applyRecord(session, tracker, record, onChunk, onDetail) {
2848
+ if (session.provider === 'claude-cli' && shouldIgnoreClaudeRecordForCurrentTurn(tracker, record)) {
2849
+ return;
2850
+ }
617
2851
  const parsed = session.provider === 'claude-cli'
618
2852
  ? parseClaudeSessionRecord(record)
619
2853
  : parseCodexSessionRecord(record);
@@ -623,14 +2857,40 @@ class LocalCliPtySessionManager {
623
2857
  if (parsed.usage) {
624
2858
  tracker.usage = parsed.usage;
625
2859
  }
2860
+ if (parsed.openedToolUseIds?.length) {
2861
+ for (const toolUseId of parsed.openedToolUseIds) {
2862
+ tracker.pendingToolUseIds.add(toolUseId);
2863
+ }
2864
+ tracker.lastRecordAtMs = Date.now();
2865
+ }
2866
+ if (parsed.resolvedToolUseIds?.length) {
2867
+ for (const toolUseId of parsed.resolvedToolUseIds) {
2868
+ tracker.pendingToolUseIds.delete(toolUseId);
2869
+ }
2870
+ tracker.lastRecordAtMs = Date.now();
2871
+ }
626
2872
  if (parsed.detail) {
2873
+ tracker.lastRecordAtMs = Date.now();
627
2874
  await emitDetail(tracker, parsed.detail, onDetail);
628
2875
  }
2876
+ // Emit structured live activity events as JSON-prefixed detail lines
2877
+ // Frontend can distinguish these from plain text details by the prefix
2878
+ if (parsed.activities && parsed.activities.length > 0 && onDetail) {
2879
+ for (const activity of parsed.activities) {
2880
+ const encoded = `__ACTIVITY__${JSON.stringify(activity)}`;
2881
+ await emitDetail(tracker, encoded, onDetail);
2882
+ }
2883
+ }
2884
+ if (parsed.assistantText) {
2885
+ tracker.lastRecordAtMs = Date.now();
2886
+ await emitAssistantChunk(tracker, parsed.assistantText, onChunk);
2887
+ }
629
2888
  if (parsed.finalContent) {
630
2889
  tracker.finalContent = parsed.finalContent;
631
2890
  tracker.lastAssistantText = parsed.finalContent;
632
2891
  }
633
2892
  if (parsed.done) {
2893
+ tracker.sawExplicitCompletion = true;
634
2894
  tracker.done = true;
635
2895
  }
636
2896
  }
@@ -642,4 +2902,161 @@ function getLocalCliPtySessionManager() {
642
2902
  }
643
2903
  return _manager;
644
2904
  }
2905
+ async function runLocalCliPtyHealthCheck() {
2906
+ const execDir = path.dirname(process.execPath);
2907
+ const appDir = getAppDirFromExec(execDir);
2908
+ const packagedIndexPath = path.join(appDir, 'resources', 'node-pty-prebuilt', 'lib', 'index.js');
2909
+ const outputChunks = [];
2910
+ try {
2911
+ const ptyModule = loadNodePtyModule();
2912
+ const pty = ptyModule.spawn('cmd.exe', [], {
2913
+ cwd: process.cwd(),
2914
+ cols: 80,
2915
+ rows: 24,
2916
+ env: { ...process.env },
2917
+ useConpty: true,
2918
+ name: 'xterm-color',
2919
+ });
2920
+ const result = await new Promise((resolve) => {
2921
+ let finished = false;
2922
+ const finish = (value) => {
2923
+ if (finished)
2924
+ return;
2925
+ finished = true;
2926
+ try {
2927
+ pty.kill();
2928
+ }
2929
+ catch { }
2930
+ resolve(value);
2931
+ };
2932
+ const timeout = setTimeout(() => {
2933
+ finish({
2934
+ ok: false,
2935
+ output: outputChunks.join(''),
2936
+ error: 'Timed out waiting for PTY echo response',
2937
+ });
2938
+ }, 5000);
2939
+ pty.on('data', (chunk) => {
2940
+ outputChunks.push(chunk);
2941
+ if (outputChunks.join('').includes('PTY_OK')) {
2942
+ clearTimeout(timeout);
2943
+ finish({ ok: true, output: outputChunks.join('') });
2944
+ }
2945
+ });
2946
+ pty.on('exit', () => {
2947
+ clearTimeout(timeout);
2948
+ if (!finished) {
2949
+ finish({
2950
+ ok: false,
2951
+ output: outputChunks.join(''),
2952
+ error: 'PTY session exited before echo response',
2953
+ });
2954
+ }
2955
+ });
2956
+ pty.write('echo PTY_OK\r');
2957
+ pty.write('exit\r');
2958
+ });
2959
+ return {
2960
+ ok: result.ok,
2961
+ execPath: process.execPath,
2962
+ packagedIndexPath,
2963
+ output: result.output,
2964
+ error: result.error,
2965
+ };
2966
+ }
2967
+ catch (error) {
2968
+ return {
2969
+ ok: false,
2970
+ execPath: process.execPath,
2971
+ packagedIndexPath,
2972
+ output: outputChunks.join(''),
2973
+ error: error instanceof Error ? error.message : String(error),
2974
+ };
2975
+ }
2976
+ }
2977
+ async function runLocalCliPtyTurnHealthCheck(provider, cwd) {
2978
+ const manager = getLocalCliPtySessionManager();
2979
+ const conversationId = `pty-health-${Date.now()}`;
2980
+ const botId = `${provider}-health`;
2981
+ const expected = provider === 'claude-cli' ? 'CLAUDE_PTY_TURN_OK' : 'CODEX_PTY_TURN_OK';
2982
+ try {
2983
+ const result = await manager.runTurn({
2984
+ conversationId,
2985
+ botId,
2986
+ provider,
2987
+ cwd,
2988
+ systemPrompt: '',
2989
+ messages: [
2990
+ {
2991
+ role: 'user',
2992
+ content: `Reply with exactly ${expected} and nothing else.`,
2993
+ },
2994
+ ],
2995
+ forceFreshSession: true,
2996
+ timeoutMs: 90_000,
2997
+ });
2998
+ manager.closeSessionByConversation(conversationId, botId);
2999
+ return {
3000
+ ok: result.content.trim() === expected,
3001
+ provider,
3002
+ cwd,
3003
+ content: result.content,
3004
+ error: result.content.trim() === expected
3005
+ ? undefined
3006
+ : `Unexpected response content: ${JSON.stringify(result.content)}`,
3007
+ };
3008
+ }
3009
+ catch (error) {
3010
+ manager.closeSessionByConversation(conversationId, botId);
3011
+ return {
3012
+ ok: false,
3013
+ provider,
3014
+ cwd,
3015
+ content: '',
3016
+ error: error instanceof Error ? error.message : String(error),
3017
+ };
3018
+ }
3019
+ }
3020
+ async function runLocalCliPtyProbe(provider, cwd, prompt, systemPrompt = '') {
3021
+ const manager = getLocalCliPtySessionManager();
3022
+ const conversationId = `pty-probe-${Date.now()}`;
3023
+ const botId = `${provider}-probe`;
3024
+ const startedAt = Date.now();
3025
+ try {
3026
+ const result = await manager.runTurn({
3027
+ conversationId,
3028
+ botId,
3029
+ provider,
3030
+ cwd,
3031
+ systemPrompt,
3032
+ messages: [
3033
+ {
3034
+ role: 'user',
3035
+ content: prompt,
3036
+ },
3037
+ ],
3038
+ forceFreshSession: true,
3039
+ timeoutMs: 180_000,
3040
+ });
3041
+ manager.closeSessionByConversation(conversationId, botId);
3042
+ return {
3043
+ ok: result.content.trim().length > 0,
3044
+ provider,
3045
+ cwd,
3046
+ content: result.content,
3047
+ elapsedMs: Date.now() - startedAt,
3048
+ };
3049
+ }
3050
+ catch (error) {
3051
+ manager.closeSessionByConversation(conversationId, botId);
3052
+ return {
3053
+ ok: false,
3054
+ provider,
3055
+ cwd,
3056
+ content: '',
3057
+ elapsedMs: Date.now() - startedAt,
3058
+ error: error instanceof Error ? error.message : String(error),
3059
+ };
3060
+ }
3061
+ }
645
3062
  //# sourceMappingURL=local-cli-pty-manager.js.map