funolio-agent 1.0.75 → 1.1.65

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (236) hide show
  1. package/dist/auth/credential-reader.d.ts.map +1 -1
  2. package/dist/auth/credential-reader.js +4 -3
  3. package/dist/auth/credential-reader.js.map +1 -1
  4. package/dist/auth/token-refresh.d.ts +8 -0
  5. package/dist/auth/token-refresh.d.ts.map +1 -1
  6. package/dist/auth/token-refresh.js +82 -52
  7. package/dist/auth/token-refresh.js.map +1 -1
  8. package/dist/auto-organizer.d.ts.map +1 -1
  9. package/dist/auto-organizer.js +6 -7
  10. package/dist/auto-organizer.js.map +1 -1
  11. package/dist/bench-prefix.d.ts +16 -0
  12. package/dist/bench-prefix.d.ts.map +1 -0
  13. package/dist/bench-prefix.js +25 -0
  14. package/dist/bench-prefix.js.map +1 -0
  15. package/dist/bot-manager.d.ts.map +1 -1
  16. package/dist/bot-manager.js +23 -14
  17. package/dist/bot-manager.js.map +1 -1
  18. package/dist/chat-sync.d.ts +42 -0
  19. package/dist/chat-sync.d.ts.map +1 -0
  20. package/dist/chat-sync.js +95 -0
  21. package/dist/chat-sync.js.map +1 -0
  22. package/dist/clerk-model.d.ts +7 -0
  23. package/dist/clerk-model.d.ts.map +1 -1
  24. package/dist/clerk-model.js +42 -8
  25. package/dist/clerk-model.js.map +1 -1
  26. package/dist/cli-bootstrap-history.d.ts +10 -0
  27. package/dist/cli-bootstrap-history.d.ts.map +1 -0
  28. package/dist/cli-bootstrap-history.js +112 -0
  29. package/dist/cli-bootstrap-history.js.map +1 -0
  30. package/dist/cli-models.d.ts +8 -0
  31. package/dist/cli-models.d.ts.map +1 -0
  32. package/dist/cli-models.js +91 -0
  33. package/dist/cli-models.js.map +1 -0
  34. package/dist/cli-session-epoch.d.ts +13 -3
  35. package/dist/cli-session-epoch.d.ts.map +1 -1
  36. package/dist/cli-session-epoch.js +53 -4
  37. package/dist/cli-session-epoch.js.map +1 -1
  38. package/dist/codex-app-server-manager.d.ts +64 -4
  39. package/dist/codex-app-server-manager.d.ts.map +1 -1
  40. package/dist/codex-app-server-manager.js +755 -55
  41. package/dist/codex-app-server-manager.js.map +1 -1
  42. package/dist/commands/pool.d.ts +32 -0
  43. package/dist/commands/pool.d.ts.map +1 -1
  44. package/dist/commands/pool.js +145 -66
  45. package/dist/commands/pool.js.map +1 -1
  46. package/dist/commands/start.d.ts +21 -0
  47. package/dist/commands/start.d.ts.map +1 -1
  48. package/dist/commands/start.js +484 -63
  49. package/dist/commands/start.js.map +1 -1
  50. package/dist/commands/status.d.ts.map +1 -1
  51. package/dist/commands/status.js +5 -2
  52. package/dist/commands/status.js.map +1 -1
  53. package/dist/config.d.ts +1 -0
  54. package/dist/config.d.ts.map +1 -1
  55. package/dist/config.js +170 -58
  56. package/dist/config.js.map +1 -1
  57. package/dist/context-window.d.ts +37 -1
  58. package/dist/context-window.d.ts.map +1 -1
  59. package/dist/context-window.js +202 -16
  60. package/dist/context-window.js.map +1 -1
  61. package/dist/live-activity.d.ts +3 -1
  62. package/dist/live-activity.d.ts.map +1 -1
  63. package/dist/live-activity.js.map +1 -1
  64. package/dist/local-chat-execution.d.ts +114 -0
  65. package/dist/local-chat-execution.d.ts.map +1 -0
  66. package/dist/local-chat-execution.js +349 -0
  67. package/dist/local-chat-execution.js.map +1 -0
  68. package/dist/local-cli-pty-manager.d.ts +138 -3
  69. package/dist/local-cli-pty-manager.d.ts.map +1 -1
  70. package/dist/local-cli-pty-manager.js +1415 -111
  71. package/dist/local-cli-pty-manager.js.map +1 -1
  72. package/dist/local-conversation-gateway.d.ts +110 -0
  73. package/dist/local-conversation-gateway.d.ts.map +1 -0
  74. package/dist/local-conversation-gateway.js +175 -0
  75. package/dist/local-conversation-gateway.js.map +1 -0
  76. package/dist/local-data.d.ts +235 -5
  77. package/dist/local-data.d.ts.map +1 -1
  78. package/dist/local-data.js +1066 -87
  79. package/dist/local-data.js.map +1 -1
  80. package/dist/local-db.d.ts +6 -0
  81. package/dist/local-db.d.ts.map +1 -1
  82. package/dist/local-db.js +376 -4
  83. package/dist/local-db.js.map +1 -1
  84. package/dist/local-funnel.d.ts.map +1 -1
  85. package/dist/local-funnel.js +6 -5
  86. package/dist/local-funnel.js.map +1 -1
  87. package/dist/local-server.d.ts +30 -0
  88. package/dist/local-server.d.ts.map +1 -1
  89. package/dist/local-server.js +2898 -319
  90. package/dist/local-server.js.map +1 -1
  91. package/dist/managed-process-registry.d.ts +59 -0
  92. package/dist/managed-process-registry.d.ts.map +1 -0
  93. package/dist/managed-process-registry.js +390 -0
  94. package/dist/managed-process-registry.js.map +1 -0
  95. package/dist/mcp/claude-config-writer.d.ts +5 -5
  96. package/dist/mcp/claude-config-writer.d.ts.map +1 -1
  97. package/dist/mcp/claude-config-writer.js +19 -11
  98. package/dist/mcp/claude-config-writer.js.map +1 -1
  99. package/dist/mcp/index.d.ts +4 -2
  100. package/dist/mcp/index.d.ts.map +1 -1
  101. package/dist/mcp/index.js.map +1 -1
  102. package/dist/mcp/sync-cli-config.d.ts +42 -4
  103. package/dist/mcp/sync-cli-config.d.ts.map +1 -1
  104. package/dist/mcp/sync-cli-config.js +497 -17
  105. package/dist/mcp/sync-cli-config.js.map +1 -1
  106. package/dist/message-loop.d.ts.map +1 -1
  107. package/dist/message-loop.js +43 -1
  108. package/dist/message-loop.js.map +1 -1
  109. package/dist/mqtt-client.d.ts +34 -0
  110. package/dist/mqtt-client.d.ts.map +1 -1
  111. package/dist/mqtt-client.js +270 -45
  112. package/dist/mqtt-client.js.map +1 -1
  113. package/dist/mqtt-data-relay.d.ts +44 -0
  114. package/dist/mqtt-data-relay.d.ts.map +1 -0
  115. package/dist/mqtt-data-relay.js +106 -0
  116. package/dist/mqtt-data-relay.js.map +1 -0
  117. package/dist/orchestration/capabilities.d.ts +13 -0
  118. package/dist/orchestration/capabilities.d.ts.map +1 -0
  119. package/dist/orchestration/capabilities.js +152 -0
  120. package/dist/orchestration/capabilities.js.map +1 -0
  121. package/dist/orchestration/dispatch-executor.d.ts +83 -0
  122. package/dist/orchestration/dispatch-executor.d.ts.map +1 -0
  123. package/dist/orchestration/dispatch-executor.js +266 -0
  124. package/dist/orchestration/dispatch-executor.js.map +1 -0
  125. package/dist/orchestration/dispatch-hint.d.ts +134 -0
  126. package/dist/orchestration/dispatch-hint.d.ts.map +1 -0
  127. package/dist/orchestration/dispatch-hint.js +247 -0
  128. package/dist/orchestration/dispatch-hint.js.map +1 -0
  129. package/dist/orchestration/dispatch-runner.d.ts +106 -0
  130. package/dist/orchestration/dispatch-runner.d.ts.map +1 -0
  131. package/dist/orchestration/dispatch-runner.js +604 -0
  132. package/dist/orchestration/dispatch-runner.js.map +1 -0
  133. package/dist/orchestration/dispatch-tools.d.ts +167 -0
  134. package/dist/orchestration/dispatch-tools.d.ts.map +1 -0
  135. package/dist/orchestration/dispatch-tools.js +328 -0
  136. package/dist/orchestration/dispatch-tools.js.map +1 -0
  137. package/dist/orchestration/front-door-policy.d.ts +35 -10
  138. package/dist/orchestration/front-door-policy.d.ts.map +1 -1
  139. package/dist/orchestration/front-door-policy.js +30 -267
  140. package/dist/orchestration/front-door-policy.js.map +1 -1
  141. package/dist/orchestration/orchestrator-dispatch-prompt.d.ts +43 -0
  142. package/dist/orchestration/orchestrator-dispatch-prompt.d.ts.map +1 -0
  143. package/dist/orchestration/orchestrator-dispatch-prompt.js +267 -0
  144. package/dist/orchestration/orchestrator-dispatch-prompt.js.map +1 -0
  145. package/dist/orchestration/orchestrator-operating-prompt.d.ts +14 -0
  146. package/dist/orchestration/orchestrator-operating-prompt.d.ts.map +1 -1
  147. package/dist/orchestration/orchestrator-operating-prompt.js +157 -31
  148. package/dist/orchestration/orchestrator-operating-prompt.js.map +1 -1
  149. package/dist/orchestration/plan-import.d.ts +39 -0
  150. package/dist/orchestration/plan-import.d.ts.map +1 -0
  151. package/dist/orchestration/plan-import.js +547 -0
  152. package/dist/orchestration/plan-import.js.map +1 -0
  153. package/dist/orchestration/worker-operating-prompt.d.ts +2 -0
  154. package/dist/orchestration/worker-operating-prompt.d.ts.map +1 -1
  155. package/dist/orchestration/worker-operating-prompt.js +36 -46
  156. package/dist/orchestration/worker-operating-prompt.js.map +1 -1
  157. package/dist/orchestrator.d.ts +195 -3
  158. package/dist/orchestrator.d.ts.map +1 -1
  159. package/dist/orchestrator.js +1970 -432
  160. package/dist/orchestrator.js.map +1 -1
  161. package/dist/providers/anthropic.d.ts.map +1 -1
  162. package/dist/providers/anthropic.js +8 -4
  163. package/dist/providers/anthropic.js.map +1 -1
  164. package/dist/providers/claude-cli.d.ts.map +1 -1
  165. package/dist/providers/claude-cli.js +28 -3
  166. package/dist/providers/claude-cli.js.map +1 -1
  167. package/dist/providers/codex-cli.d.ts +10 -6
  168. package/dist/providers/codex-cli.d.ts.map +1 -1
  169. package/dist/providers/codex-cli.js +190 -17
  170. package/dist/providers/codex-cli.js.map +1 -1
  171. package/dist/providers/google.d.ts.map +1 -1
  172. package/dist/providers/google.js +15 -5
  173. package/dist/providers/google.js.map +1 -1
  174. package/dist/providers/index.d.ts +15 -1
  175. package/dist/providers/index.d.ts.map +1 -1
  176. package/dist/providers/index.js.map +1 -1
  177. package/dist/providers/openai.d.ts +1 -1
  178. package/dist/providers/openai.d.ts.map +1 -1
  179. package/dist/providers/openai.js +13 -5
  180. package/dist/providers/openai.js.map +1 -1
  181. package/dist/server-adapter.d.ts +8 -0
  182. package/dist/server-adapter.d.ts.map +1 -1
  183. package/dist/server-adapter.js +7 -0
  184. package/dist/server-adapter.js.map +1 -1
  185. package/dist/service-mode.d.ts +1 -1
  186. package/dist/service-mode.d.ts.map +1 -1
  187. package/dist/service-mode.js +64 -1
  188. package/dist/service-mode.js.map +1 -1
  189. package/dist/service-setup-only.d.ts +8 -0
  190. package/dist/service-setup-only.d.ts.map +1 -0
  191. package/dist/service-setup-only.js +37 -0
  192. package/dist/service-setup-only.js.map +1 -0
  193. package/dist/slash-commands.d.ts +21 -0
  194. package/dist/slash-commands.d.ts.map +1 -0
  195. package/dist/slash-commands.js +99 -0
  196. package/dist/slash-commands.js.map +1 -0
  197. package/dist/subagent/index.d.ts +4 -2
  198. package/dist/subagent/index.d.ts.map +1 -1
  199. package/dist/subagent/index.js.map +1 -1
  200. package/dist/summarization-pipeline.d.ts.map +1 -1
  201. package/dist/summarization-pipeline.js +1 -9
  202. package/dist/summarization-pipeline.js.map +1 -1
  203. package/dist/token-counter.d.ts.map +1 -1
  204. package/dist/token-counter.js +11 -4
  205. package/dist/token-counter.js.map +1 -1
  206. package/dist/tool-filter.d.ts.map +1 -1
  207. package/dist/tool-filter.js +10 -6
  208. package/dist/tool-filter.js.map +1 -1
  209. package/dist/tools/admin-tools.d.ts.map +1 -1
  210. package/dist/tools/admin-tools.js +13 -4
  211. package/dist/tools/admin-tools.js.map +1 -1
  212. package/dist/tools/run-command.d.ts.map +1 -1
  213. package/dist/tools/run-command.js +5 -1
  214. package/dist/tools/run-command.js.map +1 -1
  215. package/dist/tools/search-conversation-history.d.ts.map +1 -1
  216. package/dist/tools/search-conversation-history.js +12 -2
  217. package/dist/tools/search-conversation-history.js.map +1 -1
  218. package/dist/tools/todo-tasks.d.ts.map +1 -1
  219. package/dist/tools/todo-tasks.js +77 -5
  220. package/dist/tools/todo-tasks.js.map +1 -1
  221. package/dist/usage-log.d.ts +62 -0
  222. package/dist/usage-log.d.ts.map +1 -0
  223. package/dist/usage-log.js +98 -0
  224. package/dist/usage-log.js.map +1 -0
  225. package/dist/wizard-state.d.ts +13 -0
  226. package/dist/wizard-state.d.ts.map +1 -1
  227. package/dist/wizard-state.js +61 -3
  228. package/dist/wizard-state.js.map +1 -1
  229. package/dist/wizard-support.d.ts.map +1 -1
  230. package/dist/wizard-support.js +27 -1
  231. package/dist/wizard-support.js.map +1 -1
  232. package/dist/workflow-engine.d.ts +40 -1
  233. package/dist/workflow-engine.d.ts.map +1 -1
  234. package/dist/workflow-engine.js +753 -93
  235. package/dist/workflow-engine.js.map +1 -1
  236. package/package.json +2 -2
@@ -34,10 +34,22 @@ 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;
37
45
  exports.stripAnsi = stripAnsi;
38
46
  exports.resolveConptyOverwrites = resolveConptyOverwrites;
47
+ exports.getProviderSessionRootForTest = getProviderSessionRootForTest;
48
+ exports.emitAssistantChunkSequenceForTest = emitAssistantChunkSequenceForTest;
39
49
  exports.parseClaudeSessionRecord = parseClaudeSessionRecord;
40
50
  exports.parseCodexSessionRecord = parseCodexSessionRecord;
51
+ exports.trimPassthroughSlashChromeForTest = trimPassthroughSlashChromeForTest;
52
+ exports.finalizePassthroughContentForTest = finalizePassthroughContentForTest;
41
53
  exports.getLocalCliPtySessionManager = getLocalCliPtySessionManager;
42
54
  exports.runLocalCliPtyHealthCheck = runLocalCliPtyHealthCheck;
43
55
  exports.runLocalCliPtyTurnHealthCheck = runLocalCliPtyTurnHealthCheck;
@@ -49,8 +61,17 @@ const module_1 = require("module");
49
61
  const claude_cli_prompt_1 = require("./providers/claude-cli-prompt");
50
62
  const live_activity_1 = require("./live-activity");
51
63
  const completion_marker_1 = require("./completion-marker");
52
- const CLAUDE_FRESH_SESSION_STARTUP_TIMEOUT_MS = 7_000;
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;
53
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;
54
75
  function getPtyInactivityFailTimeoutMs(provider) {
55
76
  if (provider === 'claude-cli')
56
77
  return CLAUDE_PTY_INACTIVITY_FAIL_TIMEOUT_MS;
@@ -61,18 +82,275 @@ let _manager = null;
61
82
  function delay(ms) {
62
83
  return new Promise((resolve) => setTimeout(resolve, ms));
63
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
+ }
64
159
  function buildAbortError() {
65
160
  const abortErr = new Error('PTY turn aborted');
66
161
  abortErr.name = 'AbortError';
67
162
  return abortErr;
68
163
  }
69
- function buildClaudeFreshSessionStartupError(sessionId) {
70
- const err = new Error(`claude-cli fresh session startup timed out after ${Math.floor(CLAUDE_FRESH_SESSION_STARTUP_TIMEOUT_MS / 1000)}s waiting for transcript file`
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`
71
226
  + (sessionId ? ` (${sessionId})` : ''));
72
227
  err.name = 'ClaudeFreshSessionStartupTimeoutError';
73
228
  err.code = 'CLAUDE_FRESH_SESSION_STARTUP_TIMEOUT';
74
229
  return err;
75
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
+ }
76
354
  function throwIfAborted(signal) {
77
355
  if (!signal?.aborted)
78
356
  return;
@@ -129,7 +407,6 @@ function loadNodePtyModule() {
129
407
  if (_ptyModule)
130
408
  return _ptyModule;
131
409
  const dynamicRequire = eval('require');
132
- const fileRequire = (0, module_1.createRequire)(path.join(process.cwd(), 'sea-entry.js'));
133
410
  try {
134
411
  _ptyModule = dynamicRequire('@homebridge/node-pty-prebuilt-multiarch');
135
412
  return _ptyModule;
@@ -144,8 +421,18 @@ function loadNodePtyModule() {
144
421
  for (const candidate of candidates) {
145
422
  if (!fs.existsSync(candidate))
146
423
  continue;
147
- _ptyModule = fileRequire(candidate);
148
- 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
+ }
149
436
  }
150
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)}`);
151
438
  }
@@ -256,24 +543,181 @@ function buildCodexSeedPrompt(systemPrompt, messages) {
256
543
  }
257
544
  return fullPrompt;
258
545
  }
259
- 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 = '.') {
260
611
  if (!freshSession) {
261
612
  const lastMessage = messages.length > 0 ? messages[messages.length - 1] : null;
262
613
  if (lastMessage?.role === 'user') {
263
- const prompt = typeof lastMessage.content === 'string'
264
- ? lastMessage.content
265
- : JSON.stringify(lastMessage.content);
266
- return `${prompt}\n\nRequired final line when the response is fully complete: ${completion_marker_1.CLI_COMPLETION_SENTINEL}`;
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}`;
267
616
  }
268
617
  }
618
+ // For fresh sessions, flatten images before passing to prompt builders
619
+ const { messages: flatMessages, imageNote } = flattenMessagesForImages(provider, messages);
269
620
  if (provider === 'claude-cli') {
270
- return (0, claude_cli_prompt_1.buildClaudeCliStylePrompt)({
621
+ const prompt = (0, claude_cli_prompt_1.buildClaudeCliStylePrompt)({
271
622
  system: systemPrompt,
272
- messages,
623
+ messages: flatMessages,
273
624
  runtimeMode: 'local_desktop',
274
625
  });
626
+ return imageNote ? `${prompt}${imageNote}` : prompt;
275
627
  }
276
- return buildCodexSeedPrompt(systemPrompt, messages);
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
+ };
277
721
  }
278
722
  function stripAnsi(text) {
279
723
  if (!text)
@@ -310,9 +754,42 @@ function resolveConptyOverwrites(text) {
310
754
  }
311
755
  function normalizeTerminalChunk(text) {
312
756
  return resolveConptyOverwrites(stripAnsi(text))
757
+ .replace(/[\x01-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '')
758
+ .replace(/[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏✳✺✹✸✷✶✵✴✻✽⏵⏸●◐◑◒◓▉▊▋▌▍▎▏▐█▓▒░■□▪▫◼◻◾◽]+/g, ' ')
313
759
  .replace(/\u00a0/g, ' ')
314
760
  .replace(/[^\S\n]+/g, ' ');
315
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
+ }
316
793
  function isSyntheticPromptEchoLine(line) {
317
794
  const trimmed = line.trim();
318
795
  if (!trimmed)
@@ -326,12 +803,19 @@ function isSyntheticPromptEchoLine(line) {
326
803
  }
327
804
  function isAutomationNoiseLine(provider, line) {
328
805
  const trimmed = line.trim();
806
+ const normalizedLead = trimmed.replace(/^[>›â»âµâ–¸â–¶]+\s*/, '');
329
807
  if (!trimmed)
330
808
  return false;
809
+ if (isCliBootstrapChromeLine(provider, trimmed))
810
+ return true;
331
811
  if (/^Pasting text/i.test(trimmed))
332
812
  return true;
333
813
  if (/^\[Pasted.*lines?\]/i.test(trimmed))
334
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;
335
819
  if (/^ctrl\+g to edit in notepad$/i.test(trimmed))
336
820
  return true;
337
821
  if (/^\? for shortcuts$/i.test(trimmed))
@@ -340,7 +824,15 @@ function isAutomationNoiseLine(provider, line) {
340
824
  return true;
341
825
  if (/^Improve documentation in @filename$/i.test(trimmed))
342
826
  return true;
343
- if (/^permissions on \(shift-tab to cycle\)$/i.test(trimmed))
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))
344
836
  return true;
345
837
  if (/^[-_=]{4,}$/.test(trimmed))
346
838
  return true;
@@ -503,10 +995,23 @@ async function emitPtyChunk(session, chunk) {
503
995
  const visibleText = sanitizeVisibleChunk(session.provider, echoTrimmed.text, activeTurn.recentChromeLines);
504
996
  if (!visibleText.trim())
505
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
+ }
506
1010
  const terminalView = extractTerminalFacingText(visibleText, activeTurn.assistantOutputDetected);
507
1011
  activeTurn.assistantOutputDetected = terminalView.assistantOutputDetected;
508
1012
  if (terminalView.terminalText) {
509
1013
  activeTurn.lastMeaningfulPtyDataAtMs = Date.now();
1014
+ activeTurn.sawVisibleData = true;
510
1015
  const terminalText = terminalView.terminalText.replace(/\n/g, '\r\n');
511
1016
  activeTurn.callbackChain = activeTurn.callbackChain
512
1017
  .catch(() => undefined)
@@ -515,14 +1020,23 @@ async function emitPtyChunk(session, chunk) {
515
1020
  await activeTurn.callbackChain;
516
1021
  }
517
1022
  }
518
- function getProviderSessionRoot(provider) {
1023
+ function getDefaultProviderHome(provider) {
519
1024
  if (provider === 'claude-cli') {
520
- return path.join(os.homedir(), '.claude', 'projects');
1025
+ return path.join(os.homedir(), '.claude');
521
1026
  }
522
- return path.join(os.homedir(), '.codex', 'sessions');
1027
+ return path.join(os.homedir(), '.codex');
1028
+ }
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);
523
1037
  }
524
- function listSessionFiles(provider) {
525
- const root = getProviderSessionRoot(provider);
1038
+ function listSessionFiles(provider, sessionRoot) {
1039
+ const root = sessionRoot || getProviderSessionRoot(provider);
526
1040
  if (!fs.existsSync(root))
527
1041
  return [];
528
1042
  const results = [];
@@ -623,8 +1137,8 @@ function recentFileContainsPrompt(provider, filePath, promptText, startedAtMs) {
623
1137
  }
624
1138
  return false;
625
1139
  }
626
- function discoverSessionFile(provider, launchSnapshot, startedAtMs, promptLocator) {
627
- const candidates = listSessionFiles(provider)
1140
+ function discoverSessionFile(provider, launchSnapshot, startedAtMs, promptLocator, sessionRoot) {
1141
+ const candidates = listSessionFiles(provider, sessionRoot)
628
1142
  .map((candidate) => {
629
1143
  let mtimeMs = 0;
630
1144
  try {
@@ -699,6 +1213,24 @@ function findCodexSessionFileBySessionId(sessionId) {
699
1213
  }
700
1214
  return null;
701
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
+ }
702
1234
  function formatToolUseDetail(toolName, input) {
703
1235
  if (!input || typeof input !== 'object')
704
1236
  return `🔧 ${toolName}`;
@@ -818,16 +1350,49 @@ async function emitAssistantChunk(tracker, nextText, cb) {
818
1350
  if (delta) {
819
1351
  await cb(delta);
820
1352
  }
1353
+ return;
821
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;
822
1379
  }
823
1380
  function parseClaudeSessionRecord(record) {
824
1381
  const sessionId = typeof record?.sessionId === 'string' ? record.sessionId : undefined;
825
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;
826
1390
  const usage = record.message.usage
827
1391
  ? {
828
- inputTokens: (record.message.usage.input_tokens || 0)
829
- + (record.message.usage.cache_read_input_tokens || 0)
830
- + (record.message.usage.cache_creation_input_tokens || 0),
1392
+ inputTokens: fresh + cacheCreation + cacheRead,
1393
+ inputTokensFresh: fresh,
1394
+ inputTokensCacheCreation: cacheCreation,
1395
+ inputTokensCacheRead: cacheRead,
831
1396
  outputTokens: record.message.usage.output_tokens || 0,
832
1397
  }
833
1398
  : undefined;
@@ -847,6 +1412,14 @@ function parseClaudeSessionRecord(record) {
847
1412
  if (typeof block.id === 'string' && block.id.trim()) {
848
1413
  openedToolUseIds.push(block.id);
849
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
+ });
850
1423
  // Emit structured live activity events
851
1424
  if (toolName === 'Agent') {
852
1425
  const activityLabel = (0, live_activity_1.agentToolUseToLabel)(block.input || {});
@@ -911,8 +1484,39 @@ function parseClaudeSessionRecord(record) {
911
1484
  const resolvedToolUseIds = record.message.content
912
1485
  .map((block) => (typeof block?.tool_use_id === 'string' ? block.tool_use_id : ''))
913
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) {
1490
+ if (!block || typeof block !== 'object')
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);
1499
+ }
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
+ });
1513
+ }
914
1514
  if (resolvedToolUseIds.length > 0) {
915
- return { sessionId, resolvedToolUseIds };
1515
+ return {
1516
+ sessionId,
1517
+ resolvedToolUseIds,
1518
+ ...(resultActivities.length > 0 ? { activities: resultActivities } : {}),
1519
+ };
916
1520
  }
917
1521
  }
918
1522
  // system/turn_duration is a fallback completion signal from the CLI
@@ -957,18 +1561,31 @@ function parseCodexSessionRecord(record) {
957
1561
  if (record.type === 'event_msg' && record.payload?.type === 'token_count' && record.payload?.info?.total_token_usage) {
958
1562
  const lastUsage = record.payload.info.last_token_usage;
959
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;
960
1571
  return {
961
1572
  usage: {
962
- inputTokens: (lastUsage.input_tokens || 0)
963
- + (lastUsage.cached_input_tokens || 0),
1573
+ inputTokens: fresh + cacheRead,
1574
+ inputTokensFresh: fresh,
1575
+ inputTokensCacheCreation: 0,
1576
+ inputTokensCacheRead: cacheRead,
964
1577
  outputTokens: lastUsage.output_tokens || 0,
965
1578
  },
966
1579
  };
967
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;
968
1583
  return {
969
1584
  usage: {
970
- inputTokens: (record.payload.info.total_token_usage.input_tokens || 0)
971
- + (record.payload.info.total_token_usage.cached_input_tokens || 0),
1585
+ inputTokens: fresh + cacheRead,
1586
+ inputTokensFresh: fresh,
1587
+ inputTokensCacheCreation: 0,
1588
+ inputTokensCacheRead: cacheRead,
972
1589
  outputTokens: record.payload.info.total_token_usage.output_tokens || 0,
973
1590
  },
974
1591
  };
@@ -992,7 +1609,66 @@ function parseCodexSessionRecord(record) {
992
1609
  }
993
1610
  async function waitForFirstTerminalData(session) {
994
1611
  await Promise.race([session.readyPromise, delay(5000)]);
995
- 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';
996
1672
  }
997
1673
  async function writePromptInChunks(pty, promptText, chunkSize = 512, chunkDelayMs = 15) {
998
1674
  for (let offset = 0; offset < promptText.length; offset += chunkSize) {
@@ -1025,41 +1701,389 @@ async function writeInteractivePrompt(session, promptText) {
1025
1701
  await delay(session.submitDelayMs);
1026
1702
  session.pty.write('\r');
1027
1703
  }
1028
- const SESSION_IDLE_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
1029
- const SESSION_REAPER_INTERVAL_MS = 60 * 1000; // check every 60 seconds
1704
+ async function writeNakedPrompt(session, text) {
1705
+ await writePromptInChunks(session.pty, text);
1706
+ await delay(session.submitDelayMs);
1707
+ session.pty.write('\r');
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
+ }
1030
1894
  class LocalCliPtySessionManager {
1031
1895
  sessions = new Map();
1032
- reaperTimer = null;
1033
- constructor() {
1034
- this.reaperTimer = setInterval(() => this.reapIdleSessions(), SESSION_REAPER_INTERVAL_MS);
1035
- }
1036
- reapIdleSessions() {
1037
- const now = Date.now();
1038
- for (const [key, session] of this.sessions) {
1039
- if (session.activeTurn)
1040
- continue; // don't kill active sessions
1041
- if (now - session.lastUsedAtMs > SESSION_IDLE_TIMEOUT_MS) {
1042
- console.log(`[pty-reaper] Closing idle session: ${key} (idle ${Math.floor((now - session.lastUsedAtMs) / 1000)}s)`);
1043
- this.closeSession(key);
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();
1044
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);
1045
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
+ };
1046
1985
  }
1047
1986
  async runTurn(opts) {
1048
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;
1049
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;
1050
2000
  if (session
1051
- && (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
+ }
1052
2014
  this.closeSession(key);
1053
2015
  session = undefined;
1054
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
+ }
1055
2063
  if (!session) {
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
+ });
1056
2072
  try {
1057
- session = this.createSession(key, opts.provider, opts.cwd, opts.resumeSessionId, opts.newSessionId);
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);
1058
2078
  }
1059
2079
  catch (firstErr) {
1060
2080
  console.warn(`[pty] Session creation failed, retrying in 1s: ${firstErr instanceof Error ? firstErr.message : String(firstErr)}`);
1061
2081
  await delay(1000);
1062
- session = this.createSession(key, opts.provider, opts.cwd, opts.resumeSessionId, opts.newSessionId);
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);
1063
2087
  }
1064
2088
  this.sessions.set(key, session);
1065
2089
  }
@@ -1068,9 +2092,107 @@ class LocalCliPtySessionManager {
1068
2092
  session.chain = queued.then(() => undefined, () => undefined);
1069
2093
  return queued;
1070
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
+ }
1071
2138
  closeSessionByConversation(conversationId, botId) {
1072
2139
  this.closeSession(sessionKey(conversationId, botId));
1073
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
+ }
1074
2196
  closeAll() {
1075
2197
  for (const key of [...this.sessions.keys()]) {
1076
2198
  this.closeSession(key);
@@ -1081,50 +2203,86 @@ class LocalCliPtySessionManager {
1081
2203
  if (!session)
1082
2204
  return;
1083
2205
  session.closed = true;
1084
- try {
1085
- 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;
1086
2212
  }
1087
- catch {
1088
- // best effort
2213
+ else {
2214
+ try {
2215
+ session.pty.kill();
2216
+ }
2217
+ catch {
2218
+ closeFailed = true;
2219
+ }
1089
2220
  }
1090
2221
  this.sessions.delete(key);
2222
+ (0, managed_process_registry_1.unregisterProcess)(key, false, closeFailed);
2223
+ }
2224
+ closeSessionByKey(key) {
2225
+ this.closeSession(key);
1091
2226
  }
1092
- createSession(key, provider, cwd, resumeSessionId, newSessionId) {
2227
+ createSession(key, botId, conversationId, provider, botSettings, cwd, useConpty, toolEnv, resumeSessionId, newSessionId) {
1093
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);
1094
2233
  const cleanEnv = { ...process.env };
1095
2234
  for (const envKey of Object.keys(cleanEnv)) {
1096
2235
  if (envKey === 'CLAUDECODE' || envKey.startsWith('CLAUDE_CODE_')) {
1097
2236
  delete cleanEnv[envKey];
1098
2237
  }
1099
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
+ }
1100
2264
  // Snapshot existing session files BEFORE spawning the PTY.
1101
2265
  // Only needed for Pattern B providers (no --session-id support).
1102
2266
  // Pattern A providers know their file path from the generated ID.
1103
- const preSpawnSnapshot = new Set(listSessionFiles(provider));
2267
+ const preSpawnSnapshot = new Set(listSessionFiles(provider, sessionFilesRoot));
1104
2268
  let readyResolve = null;
1105
2269
  const readyPromise = new Promise((resolve) => {
1106
2270
  readyResolve = resolve;
1107
2271
  });
1108
2272
  // Build Claude CLI args with session control:
1109
- // Pattern A: --session-id for new, --resume for existing
1110
- // Pattern B (Codex): no session flags on new, codex resume <id> for existing
1111
- const claudeArgs = ['--dangerously-skip-permissions'];
1112
- if (provider === 'claude-cli') {
1113
- if (resumeSessionId) {
1114
- claudeArgs.push('--resume', resumeSessionId);
1115
- }
1116
- else if (newSessionId) {
1117
- claudeArgs.push('--session-id', newSessionId);
1118
- }
1119
- }
1120
- // Determine known session info for Pattern A
1121
- const knownSessionId = resumeSessionId || newSessionId || null;
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;
1122
2280
  let knownSessionFilePath = null;
1123
2281
  if (knownSessionId && provider === 'claude-cli') {
1124
2282
  // Search all project directories for the session file.
1125
2283
  // Don't try to derive the directory name — Claude's naming convention
1126
2284
  // (e.g., C--Projects-Funolio) doesn't match simple path normalization.
1127
- const projectRoot = getProviderSessionRoot(provider);
2285
+ const projectRoot = sessionFilesRoot;
1128
2286
  if (fs.existsSync(projectRoot)) {
1129
2287
  try {
1130
2288
  const projectDirs = fs.readdirSync(projectRoot);
@@ -1144,28 +2302,14 @@ class LocalCliPtySessionManager {
1144
2302
  else if (knownSessionId && provider === 'codex-cli') {
1145
2303
  knownSessionFilePath = findCodexSessionFileBySessionId(knownSessionId);
1146
2304
  }
1147
- const codexArgs = resumeSessionId
1148
- ? [
1149
- 'resume',
1150
- resumeSessionId,
1151
- '--no-alt-screen',
1152
- '--dangerously-bypass-approvals-and-sandbox',
1153
- '-c',
1154
- 'shell_environment_policy.inherit=all',
1155
- ]
1156
- : [
1157
- '--no-alt-screen',
1158
- '--dangerously-bypass-approvals-and-sandbox',
1159
- '-c',
1160
- 'shell_environment_policy.inherit=all',
1161
- ];
2305
+ const codexArgs = buildCodexCliArgs(resumeSessionId, botSettings?.codex);
1162
2306
  const pty = provider === 'codex-cli'
1163
2307
  ? ptyModule.spawn(findExecutableOnPath('codex.cmd') || 'codex.cmd', codexArgs, {
1164
2308
  cwd,
1165
2309
  cols: 160,
1166
2310
  rows: 48,
1167
2311
  env: cleanEnv,
1168
- useConpty: true,
2312
+ useConpty,
1169
2313
  name: 'xterm-color',
1170
2314
  })
1171
2315
  : ptyModule.spawn(findExecutableOnPath('claude.exe') || findExecutableOnPath('claude.cmd') || 'claude', claudeArgs, {
@@ -1173,35 +2317,66 @@ class LocalCliPtySessionManager {
1173
2317
  cols: 160,
1174
2318
  rows: 48,
1175
2319
  env: cleanEnv,
1176
- useConpty: true,
2320
+ useConpty,
1177
2321
  name: 'xterm-color',
1178
2322
  });
1179
2323
  const session = {
1180
2324
  key,
2325
+ conversationId,
2326
+ botId,
2327
+ topicId: null,
2328
+ warmRuntimeMode: null,
1181
2329
  provider,
1182
2330
  cwd,
2331
+ useConpty,
1183
2332
  pty,
1184
2333
  createdAtMs: Date.now(),
1185
2334
  lastUsedAtMs: Date.now(),
1186
2335
  launchSnapshot: preSpawnSnapshot,
2336
+ sessionFilesRoot,
1187
2337
  sessionId: knownSessionId,
1188
2338
  sessionFilePath: knownSessionFilePath,
1189
2339
  sessionFileOffset: 0,
1190
2340
  sessionFileCarry: '',
1191
2341
  readyPromise,
1192
2342
  readyResolved: false,
1193
- waitForNextSendMs: 250,
2343
+ waitForNextSendMs: 100,
1194
2344
  startupDelayMs: 1200,
1195
- submitDelayMs: provider === 'codex-cli' ? 350 : 400,
2345
+ startupDelayApplied: false,
2346
+ submitDelayMs: provider === 'codex-cli' ? 175 : 200,
1196
2347
  currentPromptLocator: null,
1197
2348
  currentPromptStartedAtMs: 0,
1198
2349
  activeTurn: null,
2350
+ warmPromise: null,
2351
+ warmRequestedAtMs: null,
2352
+ warmReadyAtMs: null,
2353
+ recentTerminalOutput: '',
1199
2354
  closed: false,
2355
+ exitReason: null,
1200
2356
  chain: Promise.resolve(),
1201
2357
  childFollowers: new Map(),
1202
2358
  childSnapshot: new Set(),
2359
+ claudeAuthFingerprint: provider === 'claude-cli' ? currentClaudeAuthFingerprint() : null,
2360
+ runtimeHomeDir: claudeSessionHome,
1203
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
+ }
1204
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
+ }
1205
2380
  if (!session.readyResolved && chunk && chunk.trim()) {
1206
2381
  session.readyResolved = true;
1207
2382
  readyResolve?.();
@@ -1210,12 +2385,88 @@ class LocalCliPtySessionManager {
1210
2385
  void emitPtyChunk(session, chunk);
1211
2386
  }
1212
2387
  });
1213
- pty.on('exit', () => {
2388
+ pty.on('exit', (...exitArgs) => {
1214
2389
  session.closed = true;
2390
+ session.exitReason = describePtyExit(exitArgs);
2391
+ console.warn(`[local-cli-pty] ${provider} PTY exited (${session.exitReason}) cwd=${cwd}`);
1215
2392
  this.sessions.delete(key);
2393
+ (0, managed_process_registry_1.unregisterProcess)(key, false);
1216
2394
  });
1217
2395
  return session;
1218
2396
  }
2397
+ async runPassthroughInternal(session, opts) {
2398
+ const abortSignal = opts.abortSignal;
2399
+ const abortHandler = () => {
2400
+ this.closeSession(session.key);
2401
+ };
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);
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
+ }
1219
2470
  async runTurnInternal(session, opts) {
1220
2471
  const abortSignal = opts.abortSignal;
1221
2472
  const abortHandler = () => {
@@ -1224,27 +2475,33 @@ class LocalCliPtySessionManager {
1224
2475
  abortSignal?.addEventListener('abort', abortHandler, { once: true });
1225
2476
  try {
1226
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');
1227
2480
  throwIfAborted(abortSignal);
1228
2481
  await waitForFirstTerminalData(session);
1229
2482
  throwIfAborted(abortSignal);
1230
2483
  if (session.closed) {
1231
- throw new Error(`${session.provider} PTY session closed before prompt was sent`);
2484
+ throw formatClosedSessionError(session, 'closed before prompt was sent');
1232
2485
  }
1233
- // For resumed sessions with a known file, set the read offset NOW (after CLI is ready)
1234
- // so we only read new content appended after this point. Setting it at spawn time is
1235
- // too early CLI may still be loading session history into the file.
1236
- if (session.sessionFilePath && session.sessionFileOffset === 0 && session.sessionId) {
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) {
1237
2491
  try {
1238
2492
  const currentSize = fs.statSync(session.sessionFilePath).size;
1239
2493
  session.sessionFileOffset = currentSize;
2494
+ session.sessionFileCarry = '';
1240
2495
  }
1241
2496
  catch {
1242
- // File may not exist yet for new sessions — that's fine
2497
+ // File may not exist yet for new sessions — that's fine.
1243
2498
  }
1244
2499
  }
1245
2500
  if (session.waitForNextSendMs > 0) {
1246
2501
  await delayWithAbort(session.waitForNextSendMs, abortSignal);
1247
2502
  }
2503
+ const timeoutMs = opts.timeoutMs ?? 10 * 60 * 1000;
2504
+ const startedAtMs = Date.now();
1248
2505
  const tracker = {
1249
2506
  done: false,
1250
2507
  sawExplicitCompletion: false,
@@ -1255,18 +2512,19 @@ class LocalCliPtySessionManager {
1255
2512
  pendingToolUseIds: new Set(),
1256
2513
  lastRecordAtMs: Date.now(),
1257
2514
  sawCompletionSentinel: false,
2515
+ turnStartedAtMs: startedAtMs,
2516
+ expectedUserPromptSnippet: extractCurrentUserPromptSnippet(opts.messages),
2517
+ sawCurrentTurnUserRecord: false,
1258
2518
  };
1259
- const timeoutMs = opts.timeoutMs ?? 10 * 60 * 1000;
1260
- const startedAtMs = Date.now();
1261
- const freshClaudeStartupDeadlineMs = opts.forceFreshSession && session.provider === 'claude-cli'
1262
- ? startedAtMs + CLAUDE_FRESH_SESSION_STARTUP_TIMEOUT_MS
1263
- : null;
1264
- const promptText = buildTurnPrompt(opts.provider, opts.systemPrompt, opts.messages, opts.forceFreshSession || !session.sessionFilePath);
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);
1265
2521
  session.currentPromptLocator = promptText.trim();
1266
2522
  session.currentPromptStartedAtMs = startedAtMs;
1267
2523
  const activeTurn = {
2524
+ mode: 'default',
1268
2525
  promptEchoRemainder: normalizeTerminalChunk(promptText),
1269
2526
  rawOutput: '',
2527
+ visibleOutput: '',
1270
2528
  lastDataAtMs: startedAtMs,
1271
2529
  lastMeaningfulPtyDataAtMs: startedAtMs,
1272
2530
  callbackChain: Promise.resolve(),
@@ -1274,6 +2532,7 @@ class LocalCliPtySessionManager {
1274
2532
  onRawChunk: opts.onRawChunk,
1275
2533
  recentChromeLines: [],
1276
2534
  assistantOutputDetected: false,
2535
+ sawVisibleData: false,
1277
2536
  };
1278
2537
  session.activeTurn = activeTurn;
1279
2538
  // Snapshot existing child subagent files before this turn
@@ -1296,6 +2555,7 @@ class LocalCliPtySessionManager {
1296
2555
  }
1297
2556
  throwIfAborted(abortSignal);
1298
2557
  await writeInteractivePrompt(session, promptText);
2558
+ (0, managed_process_registry_1.markTurnActivity)(session.key, 'prompt_sent');
1299
2559
  let lastHeartbeatAtMs = Date.now();
1300
2560
  const HEARTBEAT_INTERVAL_MS = 120_000; // 2 minutes
1301
2561
  while (!tracker.done) {
@@ -1303,6 +2563,14 @@ class LocalCliPtySessionManager {
1303
2563
  if (Date.now() - startedAtMs > timeoutMs) {
1304
2564
  throw new Error(`${opts.provider} PTY session timed out waiting for a response`);
1305
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
+ }
1306
2574
  const inactivityFailTimeoutMs = getPtyInactivityFailTimeoutMs(session.provider);
1307
2575
  if (inactivityFailTimeoutMs != null
1308
2576
  && Date.now() - activeTurn.lastMeaningfulPtyDataAtMs > inactivityFailTimeoutMs) {
@@ -1323,7 +2591,7 @@ class LocalCliPtySessionManager {
1323
2591
  if (!session.sessionFilePath) {
1324
2592
  // Pattern A (known session ID): search for our specific file by ID
1325
2593
  if (session.sessionId && session.provider === 'claude-cli') {
1326
- const projectRoot = getProviderSessionRoot(session.provider);
2594
+ const projectRoot = session.sessionFilesRoot;
1327
2595
  if (fs.existsSync(projectRoot)) {
1328
2596
  try {
1329
2597
  for (const dir of fs.readdirSync(projectRoot)) {
@@ -1341,7 +2609,7 @@ class LocalCliPtySessionManager {
1341
2609
  }
1342
2610
  // Pattern B (unknown ID): discover by snapshot diff
1343
2611
  if (!session.sessionFilePath) {
1344
- const discovered = discoverSessionFile(session.provider, session.launchSnapshot, session.currentPromptStartedAtMs || session.createdAtMs, session.currentPromptLocator);
2612
+ const discovered = discoverSessionFile(session.provider, session.launchSnapshot, session.currentPromptStartedAtMs || session.createdAtMs, session.currentPromptLocator, session.sessionFilesRoot);
1345
2613
  if (discovered) {
1346
2614
  session.sessionFilePath = discovered;
1347
2615
  session.sessionFileOffset = 0;
@@ -1352,11 +2620,33 @@ class LocalCliPtySessionManager {
1352
2620
  if (session.sessionFilePath && fs.existsSync(session.sessionFilePath)) {
1353
2621
  await this.consumeSessionFile(session, tracker, opts.onChunk, opts.onDetail);
1354
2622
  }
1355
- if (freshClaudeStartupDeadlineMs
1356
- && Date.now() >= freshClaudeStartupDeadlineMs
2623
+ if (waitForFreshClaudeTranscript
1357
2624
  && !(session.sessionFilePath && fs.existsSync(session.sessionFilePath))) {
1358
- this.closeSession(session.key);
1359
- throw buildClaudeFreshSessionStartupError(session.sessionId);
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
+ }
1360
2650
  }
1361
2651
  if (session._pendingTitle) {
1362
2652
  session._pendingTitle = null;
@@ -1366,21 +2656,29 @@ class LocalCliPtySessionManager {
1366
2656
  await this.consumeChildSubagentFiles(session, tracker, opts.onDetail);
1367
2657
  }
1368
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
+ }
1369
2667
  if (canFinalizeClaudeTurnOnSessionExit(session, tracker)) {
1370
2668
  tracker.done = true;
1371
2669
  tracker.finalContent = tracker.finalContent || tracker.lastAssistantText;
1372
2670
  break;
1373
2671
  }
1374
- throw new Error(`${opts.provider} PTY session exited while waiting for a response`);
2672
+ throw formatClosedSessionError(session, 'exited while waiting for a response');
1375
2673
  }
1376
2674
  if (!tracker.done) {
1377
- await delayWithAbort(1000, abortSignal);
2675
+ await delayWithAbort(200, abortSignal);
1378
2676
  }
1379
2677
  }
1380
2678
  const settleStartedAt = Date.now();
1381
- while (Date.now() - Math.max(activeTurn.lastDataAtMs, settleStartedAt) < 250) {
2679
+ while (Date.now() - Math.max(activeTurn.lastDataAtMs, settleStartedAt) < 100) {
1382
2680
  throwIfAborted(abortSignal);
1383
- if (Date.now() - settleStartedAt > 1500)
2681
+ if (Date.now() - settleStartedAt > 500)
1384
2682
  break;
1385
2683
  await delayWithAbort(50, abortSignal);
1386
2684
  }
@@ -1389,7 +2687,7 @@ class LocalCliPtySessionManager {
1389
2687
  }
1390
2688
  await activeTurn.callbackChain;
1391
2689
  session.lastUsedAtMs = Date.now();
1392
- session.waitForNextSendMs = 400;
2690
+ session.waitForNextSendMs = 150;
1393
2691
  return {
1394
2692
  content: (tracker.finalContent || tracker.lastAssistantText).trim(),
1395
2693
  sessionId: session.sessionId,
@@ -1398,7 +2696,9 @@ class LocalCliPtySessionManager {
1398
2696
  };
1399
2697
  }
1400
2698
  finally {
2699
+ syncClaudeSessionCredentialsBackToCanonical(session);
1401
2700
  session.activeTurn = null;
2701
+ (0, managed_process_registry_1.markTurnFinished)(session.key, 'pty_turn_finished');
1402
2702
  abortSignal?.removeEventListener('abort', abortHandler);
1403
2703
  }
1404
2704
  }
@@ -1414,6 +2714,7 @@ class LocalCliPtySessionManager {
1414
2714
  }
1415
2715
  if (stat.size <= session.sessionFileOffset)
1416
2716
  return;
2717
+ (0, managed_process_registry_1.markTurnActivity)(session.key, 'transcript_data');
1417
2718
  let fh = null;
1418
2719
  try {
1419
2720
  fh = await fs.promises.open(session.sessionFilePath, 'r');
@@ -1544,6 +2845,9 @@ class LocalCliPtySessionManager {
1544
2845
  }
1545
2846
  }
1546
2847
  async applyRecord(session, tracker, record, onChunk, onDetail) {
2848
+ if (session.provider === 'claude-cli' && shouldIgnoreClaudeRecordForCurrentTurn(tracker, record)) {
2849
+ return;
2850
+ }
1547
2851
  const parsed = session.provider === 'claude-cli'
1548
2852
  ? parseClaudeSessionRecord(record)
1549
2853
  : parseCodexSessionRecord(record);