funolio-agent 1.0.53 → 1.1.65

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (276) hide show
  1. package/dist/approval.d.ts +1 -6
  2. package/dist/approval.d.ts.map +1 -1
  3. package/dist/approval.js +2 -7
  4. package/dist/approval.js.map +1 -1
  5. package/dist/auth/credential-reader.d.ts.map +1 -1
  6. package/dist/auth/credential-reader.js +4 -3
  7. package/dist/auth/credential-reader.js.map +1 -1
  8. package/dist/auth/token-refresh.d.ts +8 -0
  9. package/dist/auth/token-refresh.d.ts.map +1 -1
  10. package/dist/auth/token-refresh.js +82 -52
  11. package/dist/auth/token-refresh.js.map +1 -1
  12. package/dist/auto-organizer.d.ts.map +1 -1
  13. package/dist/auto-organizer.js +6 -7
  14. package/dist/auto-organizer.js.map +1 -1
  15. package/dist/bench-prefix.d.ts +16 -0
  16. package/dist/bench-prefix.d.ts.map +1 -0
  17. package/dist/bench-prefix.js +25 -0
  18. package/dist/bench-prefix.js.map +1 -0
  19. package/dist/bot-manager.d.ts +5 -1
  20. package/dist/bot-manager.d.ts.map +1 -1
  21. package/dist/bot-manager.js +46 -27
  22. package/dist/bot-manager.js.map +1 -1
  23. package/dist/chat-sync.d.ts +42 -0
  24. package/dist/chat-sync.d.ts.map +1 -0
  25. package/dist/chat-sync.js +95 -0
  26. package/dist/chat-sync.js.map +1 -0
  27. package/dist/clerk-model.d.ts +7 -0
  28. package/dist/clerk-model.d.ts.map +1 -1
  29. package/dist/clerk-model.js +42 -8
  30. package/dist/clerk-model.js.map +1 -1
  31. package/dist/cli-bootstrap-history.d.ts +10 -0
  32. package/dist/cli-bootstrap-history.d.ts.map +1 -0
  33. package/dist/cli-bootstrap-history.js +112 -0
  34. package/dist/cli-bootstrap-history.js.map +1 -0
  35. package/dist/cli-models.d.ts +8 -0
  36. package/dist/cli-models.d.ts.map +1 -0
  37. package/dist/cli-models.js +91 -0
  38. package/dist/cli-models.js.map +1 -0
  39. package/dist/cli-session-epoch.d.ts +13 -3
  40. package/dist/cli-session-epoch.d.ts.map +1 -1
  41. package/dist/cli-session-epoch.js +53 -4
  42. package/dist/cli-session-epoch.js.map +1 -1
  43. package/dist/cli-session-registry.d.ts +35 -0
  44. package/dist/cli-session-registry.d.ts.map +1 -0
  45. package/dist/cli-session-registry.js +177 -0
  46. package/dist/cli-session-registry.js.map +1 -0
  47. package/dist/cli.js +62 -0
  48. package/dist/cli.js.map +1 -1
  49. package/dist/codex-app-server-manager.d.ts +189 -0
  50. package/dist/codex-app-server-manager.d.ts.map +1 -0
  51. package/dist/codex-app-server-manager.js +1468 -0
  52. package/dist/codex-app-server-manager.js.map +1 -0
  53. package/dist/commands/init.d.ts.map +1 -1
  54. package/dist/commands/init.js +8 -30
  55. package/dist/commands/init.js.map +1 -1
  56. package/dist/commands/pool.d.ts +32 -0
  57. package/dist/commands/pool.d.ts.map +1 -1
  58. package/dist/commands/pool.js +145 -66
  59. package/dist/commands/pool.js.map +1 -1
  60. package/dist/commands/setup.d.ts +4 -1
  61. package/dist/commands/setup.d.ts.map +1 -1
  62. package/dist/commands/setup.js +9 -25
  63. package/dist/commands/setup.js.map +1 -1
  64. package/dist/commands/start.d.ts +21 -0
  65. package/dist/commands/start.d.ts.map +1 -1
  66. package/dist/commands/start.js +559 -63
  67. package/dist/commands/start.js.map +1 -1
  68. package/dist/commands/status.d.ts.map +1 -1
  69. package/dist/commands/status.js +5 -2
  70. package/dist/commands/status.js.map +1 -1
  71. package/dist/completion-marker.d.ts +7 -0
  72. package/dist/completion-marker.d.ts.map +1 -0
  73. package/dist/completion-marker.js +28 -0
  74. package/dist/completion-marker.js.map +1 -0
  75. package/dist/config.d.ts +7 -2
  76. package/dist/config.d.ts.map +1 -1
  77. package/dist/config.js +184 -60
  78. package/dist/config.js.map +1 -1
  79. package/dist/context-window.d.ts +37 -1
  80. package/dist/context-window.d.ts.map +1 -1
  81. package/dist/context-window.js +210 -17
  82. package/dist/context-window.js.map +1 -1
  83. package/dist/live-activity.d.ts +31 -0
  84. package/dist/live-activity.d.ts.map +1 -0
  85. package/dist/live-activity.js +36 -0
  86. package/dist/live-activity.js.map +1 -0
  87. package/dist/local-chat-execution.d.ts +114 -0
  88. package/dist/local-chat-execution.d.ts.map +1 -0
  89. package/dist/local-chat-execution.js +349 -0
  90. package/dist/local-chat-execution.js.map +1 -0
  91. package/dist/local-cli-pty-manager.d.ts +186 -0
  92. package/dist/local-cli-pty-manager.d.ts.map +1 -1
  93. package/dist/local-cli-pty-manager.js +2581 -164
  94. package/dist/local-cli-pty-manager.js.map +1 -1
  95. package/dist/local-conversation-gateway.d.ts +110 -0
  96. package/dist/local-conversation-gateway.d.ts.map +1 -0
  97. package/dist/local-conversation-gateway.js +175 -0
  98. package/dist/local-conversation-gateway.js.map +1 -0
  99. package/dist/local-data.d.ts +276 -5
  100. package/dist/local-data.d.ts.map +1 -1
  101. package/dist/local-data.js +1201 -86
  102. package/dist/local-data.js.map +1 -1
  103. package/dist/local-db.d.ts +6 -0
  104. package/dist/local-db.d.ts.map +1 -1
  105. package/dist/local-db.js +428 -2
  106. package/dist/local-db.js.map +1 -1
  107. package/dist/local-funnel.d.ts.map +1 -1
  108. package/dist/local-funnel.js +6 -5
  109. package/dist/local-funnel.js.map +1 -1
  110. package/dist/local-server.d.ts +55 -0
  111. package/dist/local-server.d.ts.map +1 -1
  112. package/dist/local-server.js +3281 -441
  113. package/dist/local-server.js.map +1 -1
  114. package/dist/managed-process-registry.d.ts +59 -0
  115. package/dist/managed-process-registry.d.ts.map +1 -0
  116. package/dist/managed-process-registry.js +390 -0
  117. package/dist/managed-process-registry.js.map +1 -0
  118. package/dist/mcp/claude-config-writer.d.ts +5 -5
  119. package/dist/mcp/claude-config-writer.d.ts.map +1 -1
  120. package/dist/mcp/claude-config-writer.js +19 -11
  121. package/dist/mcp/claude-config-writer.js.map +1 -1
  122. package/dist/mcp/index.d.ts +4 -2
  123. package/dist/mcp/index.d.ts.map +1 -1
  124. package/dist/mcp/index.js.map +1 -1
  125. package/dist/mcp/sync-cli-config.d.ts +42 -4
  126. package/dist/mcp/sync-cli-config.d.ts.map +1 -1
  127. package/dist/mcp/sync-cli-config.js +497 -17
  128. package/dist/mcp/sync-cli-config.js.map +1 -1
  129. package/dist/message-loop.d.ts +6 -0
  130. package/dist/message-loop.d.ts.map +1 -1
  131. package/dist/message-loop.js +281 -89
  132. package/dist/message-loop.js.map +1 -1
  133. package/dist/mqtt-client.d.ts +44 -1
  134. package/dist/mqtt-client.d.ts.map +1 -1
  135. package/dist/mqtt-client.js +284 -46
  136. package/dist/mqtt-client.js.map +1 -1
  137. package/dist/mqtt-data-relay.d.ts +44 -0
  138. package/dist/mqtt-data-relay.d.ts.map +1 -0
  139. package/dist/mqtt-data-relay.js +106 -0
  140. package/dist/mqtt-data-relay.js.map +1 -0
  141. package/dist/oauth.d.ts.map +1 -1
  142. package/dist/oauth.js +69 -29
  143. package/dist/oauth.js.map +1 -1
  144. package/dist/orchestration/capabilities.d.ts +13 -0
  145. package/dist/orchestration/capabilities.d.ts.map +1 -0
  146. package/dist/orchestration/capabilities.js +152 -0
  147. package/dist/orchestration/capabilities.js.map +1 -0
  148. package/dist/orchestration/dispatch-executor.d.ts +83 -0
  149. package/dist/orchestration/dispatch-executor.d.ts.map +1 -0
  150. package/dist/orchestration/dispatch-executor.js +266 -0
  151. package/dist/orchestration/dispatch-executor.js.map +1 -0
  152. package/dist/orchestration/dispatch-hint.d.ts +134 -0
  153. package/dist/orchestration/dispatch-hint.d.ts.map +1 -0
  154. package/dist/orchestration/dispatch-hint.js +247 -0
  155. package/dist/orchestration/dispatch-hint.js.map +1 -0
  156. package/dist/orchestration/dispatch-runner.d.ts +106 -0
  157. package/dist/orchestration/dispatch-runner.d.ts.map +1 -0
  158. package/dist/orchestration/dispatch-runner.js +604 -0
  159. package/dist/orchestration/dispatch-runner.js.map +1 -0
  160. package/dist/orchestration/dispatch-tools.d.ts +167 -0
  161. package/dist/orchestration/dispatch-tools.d.ts.map +1 -0
  162. package/dist/orchestration/dispatch-tools.js +328 -0
  163. package/dist/orchestration/dispatch-tools.js.map +1 -0
  164. package/dist/orchestration/front-door-policy.d.ts +35 -10
  165. package/dist/orchestration/front-door-policy.d.ts.map +1 -1
  166. package/dist/orchestration/front-door-policy.js +30 -267
  167. package/dist/orchestration/front-door-policy.js.map +1 -1
  168. package/dist/orchestration/orchestrator-dispatch-prompt.d.ts +43 -0
  169. package/dist/orchestration/orchestrator-dispatch-prompt.d.ts.map +1 -0
  170. package/dist/orchestration/orchestrator-dispatch-prompt.js +267 -0
  171. package/dist/orchestration/orchestrator-dispatch-prompt.js.map +1 -0
  172. package/dist/orchestration/orchestrator-operating-prompt.d.ts +15 -0
  173. package/dist/orchestration/orchestrator-operating-prompt.d.ts.map +1 -1
  174. package/dist/orchestration/orchestrator-operating-prompt.js +206 -20
  175. package/dist/orchestration/orchestrator-operating-prompt.js.map +1 -1
  176. package/dist/orchestration/plan-import.d.ts +39 -0
  177. package/dist/orchestration/plan-import.d.ts.map +1 -0
  178. package/dist/orchestration/plan-import.js +547 -0
  179. package/dist/orchestration/plan-import.js.map +1 -0
  180. package/dist/orchestration/validation.d.ts +40 -0
  181. package/dist/orchestration/validation.d.ts.map +1 -0
  182. package/dist/orchestration/validation.js +203 -0
  183. package/dist/orchestration/validation.js.map +1 -0
  184. package/dist/orchestration/worker-operating-prompt.d.ts +2 -0
  185. package/dist/orchestration/worker-operating-prompt.d.ts.map +1 -1
  186. package/dist/orchestration/worker-operating-prompt.js +36 -46
  187. package/dist/orchestration/worker-operating-prompt.js.map +1 -1
  188. package/dist/orchestrator.d.ts +214 -33
  189. package/dist/orchestrator.d.ts.map +1 -1
  190. package/dist/orchestrator.js +2200 -1100
  191. package/dist/orchestrator.js.map +1 -1
  192. package/dist/providers/anthropic.d.ts.map +1 -1
  193. package/dist/providers/anthropic.js +8 -4
  194. package/dist/providers/anthropic.js.map +1 -1
  195. package/dist/providers/claude-cli-prompt.d.ts.map +1 -1
  196. package/dist/providers/claude-cli-prompt.js +49 -5
  197. package/dist/providers/claude-cli-prompt.js.map +1 -1
  198. package/dist/providers/claude-cli.d.ts.map +1 -1
  199. package/dist/providers/claude-cli.js +81 -5
  200. package/dist/providers/claude-cli.js.map +1 -1
  201. package/dist/providers/codex-cli.d.ts +10 -6
  202. package/dist/providers/codex-cli.d.ts.map +1 -1
  203. package/dist/providers/codex-cli.js +204 -26
  204. package/dist/providers/codex-cli.js.map +1 -1
  205. package/dist/providers/google.d.ts.map +1 -1
  206. package/dist/providers/google.js +15 -5
  207. package/dist/providers/google.js.map +1 -1
  208. package/dist/providers/index.d.ts +15 -1
  209. package/dist/providers/index.d.ts.map +1 -1
  210. package/dist/providers/index.js.map +1 -1
  211. package/dist/providers/openai.d.ts +1 -1
  212. package/dist/providers/openai.d.ts.map +1 -1
  213. package/dist/providers/openai.js +13 -5
  214. package/dist/providers/openai.js.map +1 -1
  215. package/dist/response-guard.js +1 -1
  216. package/dist/response-guard.js.map +1 -1
  217. package/dist/server-adapter.d.ts +8 -0
  218. package/dist/server-adapter.d.ts.map +1 -1
  219. package/dist/server-adapter.js +7 -0
  220. package/dist/server-adapter.js.map +1 -1
  221. package/dist/service-mode.d.ts +1 -1
  222. package/dist/service-mode.d.ts.map +1 -1
  223. package/dist/service-mode.js +64 -1
  224. package/dist/service-mode.js.map +1 -1
  225. package/dist/service-setup-only.d.ts +8 -0
  226. package/dist/service-setup-only.d.ts.map +1 -0
  227. package/dist/service-setup-only.js +37 -0
  228. package/dist/service-setup-only.js.map +1 -0
  229. package/dist/slash-commands.d.ts +21 -0
  230. package/dist/slash-commands.d.ts.map +1 -0
  231. package/dist/slash-commands.js +99 -0
  232. package/dist/slash-commands.js.map +1 -0
  233. package/dist/subagent/index.d.ts +4 -2
  234. package/dist/subagent/index.d.ts.map +1 -1
  235. package/dist/subagent/index.js.map +1 -1
  236. package/dist/summarization-pipeline.d.ts.map +1 -1
  237. package/dist/summarization-pipeline.js +1 -9
  238. package/dist/summarization-pipeline.js.map +1 -1
  239. package/dist/token-counter.d.ts.map +1 -1
  240. package/dist/token-counter.js +11 -4
  241. package/dist/token-counter.js.map +1 -1
  242. package/dist/tool-filter.d.ts.map +1 -1
  243. package/dist/tool-filter.js +10 -6
  244. package/dist/tool-filter.js.map +1 -1
  245. package/dist/tools/admin-tools.d.ts.map +1 -1
  246. package/dist/tools/admin-tools.js +20 -5
  247. package/dist/tools/admin-tools.js.map +1 -1
  248. package/dist/tools/index.d.ts.map +1 -1
  249. package/dist/tools/index.js +2 -1
  250. package/dist/tools/index.js.map +1 -1
  251. package/dist/tools/run-command.d.ts.map +1 -1
  252. package/dist/tools/run-command.js +5 -1
  253. package/dist/tools/run-command.js.map +1 -1
  254. package/dist/tools/search-conversation-history.d.ts +16 -0
  255. package/dist/tools/search-conversation-history.d.ts.map +1 -0
  256. package/dist/tools/search-conversation-history.js +334 -0
  257. package/dist/tools/search-conversation-history.js.map +1 -0
  258. package/dist/tools/todo-tasks.d.ts.map +1 -1
  259. package/dist/tools/todo-tasks.js +77 -5
  260. package/dist/tools/todo-tasks.js.map +1 -1
  261. package/dist/usage-log.d.ts +62 -0
  262. package/dist/usage-log.d.ts.map +1 -0
  263. package/dist/usage-log.js +98 -0
  264. package/dist/usage-log.js.map +1 -0
  265. package/dist/wizard-state.d.ts +20 -0
  266. package/dist/wizard-state.d.ts.map +1 -1
  267. package/dist/wizard-state.js +90 -3
  268. package/dist/wizard-state.js.map +1 -1
  269. package/dist/wizard-support.d.ts.map +1 -1
  270. package/dist/wizard-support.js +27 -1
  271. package/dist/wizard-support.js.map +1 -1
  272. package/dist/workflow-engine.d.ts +44 -2
  273. package/dist/workflow-engine.d.ts.map +1 -1
  274. package/dist/workflow-engine.js +932 -111
  275. package/dist/workflow-engine.js.map +1 -1
  276. package/package.json +2 -2
@@ -49,6 +49,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
49
49
  };
50
50
  Object.defineProperty(exports, "__esModule", { value: true });
51
51
  exports.WorkflowEngine = void 0;
52
+ exports.resolveWorkflowCliSessionTransportForTest = resolveWorkflowCliSessionTransportForTest;
53
+ exports.resolveWorkflowClaudeUseConptyForTest = resolveWorkflowClaudeUseConptyForTest;
54
+ exports.buildDeferredImageReferenceBlockForTest = buildDeferredImageReferenceBlockForTest;
55
+ exports.parseImageEscalationRequestForTest = parseImageEscalationRequestForTest;
52
56
  exports.getWorkflowEngine = getWorkflowEngine;
53
57
  const events_1 = require("events");
54
58
  const fs_1 = __importDefault(require("fs"));
@@ -59,39 +63,147 @@ const approval_1 = require("./approval");
59
63
  const clerk_model_1 = require("./clerk-model");
60
64
  const status_parser_1 = require("./orchestration/status-parser");
61
65
  const safeguards_1 = require("./orchestration/safeguards");
66
+ const config_1 = require("./config");
62
67
  const data = __importStar(require("./local-data"));
63
68
  const orchestrator_profile_1 = require("./orchestrator-profile");
64
69
  const subscription_runtime_1 = require("./auth/subscription-runtime");
65
70
  const runtime_context_1 = require("./runtime-context");
66
71
  const local_cli_pty_manager_1 = require("./local-cli-pty-manager");
72
+ const codex_app_server_manager_1 = require("./codex-app-server-manager");
67
73
  const cli_session_epoch_1 = require("./cli-session-epoch");
74
+ const cli_bootstrap_history_1 = require("./cli-bootstrap-history");
75
+ const context_window_1 = require("./context-window");
76
+ const capabilities_1 = require("./orchestration/capabilities");
68
77
  // ─── Workflow Engine ─────────────────────────────────────────────
69
78
  const MAX_STEP_ATTEMPTS = safeguards_1.SAFEGUARDS.MAX_AGENT_ATTEMPTS;
70
79
  const MAX_STEPS = safeguards_1.SAFEGUARDS.MAX_WORKFLOW_STEPS;
71
80
  function isInteractiveAuthFailure(text) {
72
- return /\b(not logged in|please run \/login|unauthorized|invalid api key|authentication required)\b/i.test(text);
81
+ return /\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.test(text);
73
82
  }
74
83
  const LOCAL_WORKFLOW_RUNTIME_RETRY_LIMIT = 2;
84
+ function shouldUseConptyForWorkflowClaudePty() {
85
+ // ConPTY can surface transient console-host flashes on Windows during
86
+ // long-running Claude PTY worker turns. Keep direct chat unchanged, but use
87
+ // the legacy hidden winpty backend for orchestration/workflow Claude runs.
88
+ return process.platform !== 'win32';
89
+ }
90
+ function isClaudeFreshWorkflowSessionStartupFailure(err) {
91
+ return err?.code === 'CLAUDE_FRESH_SESSION_STARTUP_TIMEOUT'
92
+ || err?.name === 'ClaudeFreshSessionStartupTimeoutError'
93
+ || /fresh session startup timed out/i.test(String(err?.message || err || ''));
94
+ }
95
+ function createWorkflowAbortError() {
96
+ const err = new Error('aborted');
97
+ err.name = 'AbortError';
98
+ err.code = 'ABORT_ERR';
99
+ return err;
100
+ }
101
+ function isWorkflowAbortError(err) {
102
+ return err?.name === 'AbortError'
103
+ || err?.code === 'ABORT_ERR'
104
+ || /\baborted\b/i.test(String(err?.message || err || ''));
105
+ }
106
+ function throwIfWorkflowAborted(abortSignal) {
107
+ if (!abortSignal?.aborted)
108
+ return;
109
+ throw createWorkflowAbortError();
110
+ }
75
111
  function shouldRetrySelectedWorkflowRuntime(err) {
76
112
  const text = String(err?.message || err || '').toLowerCase();
77
113
  if (!text)
78
114
  return false;
79
- if (/\b(no api key|configure one in settings|not available on this machine|not installed|please run \/login|not logged in|invalid api key)\b/i.test(text)) {
115
+ if (/\b(no api key|missing api key|configure one in settings|not available on this machine|not installed|please run \/login|login required|not logged in|unauthorized|invalid authorization|invalid api key|authentication required|invalid authentication credentials|invalid bearer token|token expired|session expired|credentials expired|expired credentials|reauthenticate)\b/i.test(text)) {
80
116
  return false;
81
117
  }
82
- return /\b(429|rate limit|timeout|timed out|temporar|temporarily|econnreset|etimedout|enotfound|econnrefused|socket hang up|network|try again|overloaded|busy)\b/i.test(text);
118
+ return /\b(429|rate limit|timeout|timed out|temporar|temporarily|econnreset|etimedout|enotfound|econnrefused|socket hang up|network|try again|overloaded|busy|no meaningful pty activity)\b/i.test(text);
83
119
  }
84
- async function pauseWorkflowRuntimeRetry(attempt) {
120
+ async function pauseWorkflowRuntimeRetry(attempt, abortSignal) {
85
121
  const delayMs = attempt <= 1 ? 750 : 1500;
86
- await new Promise((resolve) => setTimeout(resolve, delayMs));
122
+ throwIfWorkflowAborted(abortSignal);
123
+ await Promise.race([
124
+ new Promise((resolve) => setTimeout(resolve, delayMs)),
125
+ new Promise((_, reject) => {
126
+ if (!abortSignal)
127
+ return;
128
+ const onAbort = () => {
129
+ abortSignal.removeEventListener('abort', onAbort);
130
+ reject(createWorkflowAbortError());
131
+ };
132
+ abortSignal.addEventListener('abort', onAbort, { once: true });
133
+ }),
134
+ ]);
135
+ throwIfWorkflowAborted(abortSignal);
87
136
  }
88
- function buildWorkflowPtyConversationKey(conversationId, step, workflowContext) {
137
+ function buildWorkflowPtyConversationKey(conversationId, step, workflowContext, cliSessionScopeKey) {
138
+ const explicitScope = String(cliSessionScopeKey || '').trim();
139
+ if (explicitScope)
140
+ return explicitScope;
89
141
  if (conversationId)
90
142
  return conversationId;
91
143
  if (workflowContext?.workflowId)
92
144
  return `workflow:${workflowContext.workflowId}`;
93
145
  return `adhoc:${step.agentId}:${step.id}`;
94
146
  }
147
+ function clearFailedWorkflowCliSessionEpoch(conversationId, botId, sessionId, cliSessionScopeKey) {
148
+ const normalizedSessionId = String(sessionId || '').trim();
149
+ if (!conversationId || !normalizedSessionId)
150
+ return;
151
+ try {
152
+ const cleared = data.clearCliSessionEpochIfMatches(conversationId, botId, normalizedSessionId, cliSessionScopeKey);
153
+ if (cleared) {
154
+ console.warn(`[workflow-engine] cleared poisoned cli_session_epoch conversationId=${conversationId} botId=${botId} sessionId=${normalizedSessionId}`);
155
+ }
156
+ }
157
+ catch (err) {
158
+ console.warn(`[workflow-engine] failed to clear cli_session_epoch conversationId=${conversationId} botId=${botId} sessionId=${normalizedSessionId}: ${err?.message || err}`);
159
+ }
160
+ }
161
+ function resolveWorkflowCliSessionTransport(providerName, isLocalCliSession) {
162
+ if (!isLocalCliSession)
163
+ return 'none';
164
+ if (providerName === 'codex-cli')
165
+ return 'codex-app-server';
166
+ return 'pty';
167
+ }
168
+ function resolveWorkflowCliSessionTransportForTest(providerName, isLocalCliSession) {
169
+ return resolveWorkflowCliSessionTransport(providerName, isLocalCliSession);
170
+ }
171
+ function resolveWorkflowClaudeUseConptyForTest() {
172
+ return shouldUseConptyForWorkflowClaudePty();
173
+ }
174
+ function listImageAttachments(attachments) {
175
+ return (attachments || []).filter((attachment) => attachment.mimeType?.startsWith('image/'));
176
+ }
177
+ function buildDeferredImageReferenceBlock(attachments) {
178
+ const imageAttachments = listImageAttachments(attachments);
179
+ if (imageAttachments.length === 0)
180
+ return '';
181
+ const lines = [
182
+ '[Original Image Context Available]',
183
+ ...imageAttachments.map((attachment) => `- id: ${attachment.id} (${attachment.filename})`),
184
+ 'Use the assigned task, prior worker artifacts, and any available memory/history search tools first.',
185
+ 'If you still need the original image, reply exactly with one line:',
186
+ 'ESCALATE_IMAGE <id>',
187
+ 'Use one of the listed ids. Do not include any other text.',
188
+ ];
189
+ return lines.join('\n');
190
+ }
191
+ function buildDeferredImageReferenceBlockForTest(attachments) {
192
+ return buildDeferredImageReferenceBlock(attachments);
193
+ }
194
+ function parseImageEscalationRequest(text, attachments) {
195
+ const imageAttachments = listImageAttachments(attachments);
196
+ if (imageAttachments.length === 0)
197
+ return null;
198
+ const match = String(text || '').match(/^\s*ESCALATE_IMAGE\s+([A-Za-z0-9._:-]+)\s*$/m);
199
+ if (!match)
200
+ return null;
201
+ const requestedId = match[1].trim();
202
+ return imageAttachments.find((attachment) => attachment.id === requestedId) || null;
203
+ }
204
+ function parseImageEscalationRequestForTest(text, attachments) {
205
+ return parseImageEscalationRequest(text, attachments)?.id || null;
206
+ }
95
207
  class WorkflowEngine extends events_1.EventEmitter {
96
208
  projectDir;
97
209
  runtimeMode;
@@ -114,9 +226,16 @@ class WorkflowEngine extends events_1.EventEmitter {
114
226
  const effectiveRuntimeMode = (0, runtime_context_1.normalizeRuntimeMode)(opts?.runtimeMode || this.runtimeMode);
115
227
  const clerk = (0, clerk_model_1.getClerk)({ runtimeMode: effectiveRuntimeMode });
116
228
  const allProfiles = data.listAgentProfiles();
117
- // Filter out orchestrator profiles by default, but allow explicitly assigned specialists
118
- // and the selected agent back into the planning pool when needed.
119
- const agents = (0, orchestrator_profile_1.filterOutOrchestratorProfiles)(allProfiles);
229
+ // Filter out pure orchestrators but keep orchestrator bots that also have a worker role.
230
+ const PURE_ORCHESTRATOR_ROLES = new Set(['orchestrator', 'project manager']);
231
+ const agents = allProfiles.filter((agent) => {
232
+ const rc = String(agent.role_class || '').trim().toLowerCase();
233
+ if (agent.is_orchestrator === 1 && PURE_ORCHESTRATOR_ROLES.has(rc))
234
+ return false;
235
+ if (!agent.is_orchestrator && (0, orchestrator_profile_1.isOrchestratorProfile)(agent))
236
+ return false;
237
+ return true;
238
+ });
120
239
  const profile = data.getAgentProfile(agentId) || agents[0];
121
240
  if (!profile) {
122
241
  throw new Error('No agent profile configured');
@@ -126,7 +245,7 @@ class WorkflowEngine extends events_1.EventEmitter {
126
245
  let steps;
127
246
  if (clerk && planningAgents.length > 1 && !opts?.disableDecomposition) {
128
247
  // Ask clerk if this needs decomposition
129
- steps = await this.decomposeTask(clerk, prompt, planningAgents, profile, opts?.roleAssignments);
248
+ steps = await this.decomposeTask(clerk, prompt, planningAgents, profile, opts?.roleAssignments, effectiveRuntimeMode);
130
249
  }
131
250
  else {
132
251
  // Single agent, single step
@@ -323,8 +442,12 @@ class WorkflowEngine extends events_1.EventEmitter {
323
442
  throw new Error(`Workflow step ${index + 1} references missing bot ${templateStep.agent_id}`);
324
443
  }
325
444
  const instruction = templateStep.instruction?.trim() || `Complete stage ${index + 1} for this TODO item.`;
326
- const inferredRole = this.inferRoleForStep({ description: instruction });
327
- const isCheckpointLike = templateStep.is_checkpoint === 1 || inferredRole === 'qa';
445
+ // Role comes from the agent's structured role_class; checkpoint
446
+ // status comes from the template's explicit is_checkpoint flag
447
+ // or a QA-role agent. No prose scanning.
448
+ const agentRole = agent.role_class || undefined;
449
+ const agentIsQa = this.inferRoleForStep({ role: agentRole }) === 'qa';
450
+ const isCheckpointLike = templateStep.is_checkpoint === 1 || agentIsQa;
328
451
  return {
329
452
  id: templateStep.id,
330
453
  index,
@@ -334,6 +457,7 @@ class WorkflowEngine extends events_1.EventEmitter {
334
457
  agentName: agent.name,
335
458
  provider: agent.provider,
336
459
  model: agent.model,
460
+ role: agentRole,
337
461
  dependsOn: index === 0 ? [] : [index - 1],
338
462
  status: 'pending',
339
463
  attempts: 0,
@@ -458,8 +582,27 @@ class WorkflowEngine extends events_1.EventEmitter {
458
582
  /**
459
583
  * Use clerk to decompose a complex task into sub-steps with agent routing.
460
584
  */
461
- async decomposeTask(clerk, prompt, agents, defaultProfile, roleAssignments) {
585
+ async decomposeTask(clerk, prompt, agents, defaultProfile, roleAssignments, runtimeMode) {
462
586
  const deterministicPlan = this.buildDeterministicPlan(prompt, agents, defaultProfile, roleAssignments);
587
+ if ((0, runtime_context_1.normalizeRuntimeMode)(runtimeMode || this.runtimeMode) === 'local_desktop') {
588
+ if (deterministicPlan)
589
+ return deterministicPlan;
590
+ return [{
591
+ id: 'step-0',
592
+ index: 0,
593
+ description: prompt.slice(0, 100),
594
+ prompt,
595
+ agentId: defaultProfile.id,
596
+ agentName: defaultProfile.name,
597
+ provider: defaultProfile.provider,
598
+ model: defaultProfile.model,
599
+ role: defaultProfile.role_class || undefined,
600
+ dependsOn: [],
601
+ status: 'pending',
602
+ attempts: 0,
603
+ maxAttempts: MAX_STEP_ATTEMPTS,
604
+ }];
605
+ }
463
606
  try {
464
607
  const plannedSteps = await clerk.planWorkflow({
465
608
  prompt,
@@ -470,6 +613,11 @@ class WorkflowEngine extends events_1.EventEmitter {
470
613
  const mapped = plannedSteps.slice(0, MAX_STEPS).map((step, index) => {
471
614
  // Route to the named agent or default
472
615
  const targetAgent = this.resolveStepAgent(step, agents, defaultProfile, roleAssignments);
616
+ // Prefer an explicit role on the planned step; otherwise carry the
617
+ // resolved agent's role so downstream code doesn't fall back to
618
+ // regex classification on prose.
619
+ const explicitRole = typeof step.role === 'string' && step.role.trim() ? step.role.trim().toLowerCase() : undefined;
620
+ const agentRole = targetAgent.role_class || undefined;
473
621
  return {
474
622
  id: `step-${index}`,
475
623
  index,
@@ -479,6 +627,7 @@ class WorkflowEngine extends events_1.EventEmitter {
479
627
  agentName: targetAgent.name,
480
628
  provider: targetAgent.provider,
481
629
  model: targetAgent.model,
630
+ role: explicitRole || agentRole,
482
631
  dependsOn: step.dependsOn || [],
483
632
  status: 'pending',
484
633
  expectedOutput: step.expectedOutput || undefined,
@@ -504,7 +653,7 @@ class WorkflowEngine extends events_1.EventEmitter {
504
653
  step.loopCount = 0;
505
654
  }
506
655
  }
507
- return this.pruneTrailingPostQaSteps(mapped);
656
+ return mapped;
508
657
  }
509
658
  }
510
659
  catch {
@@ -554,6 +703,14 @@ class WorkflowEngine extends events_1.EventEmitter {
554
703
  }
555
704
  return steps.length >= 2 ? steps : null;
556
705
  }
706
+ /**
707
+ * @deprecated Phase B of orchestration-plan.txt.
708
+ * The LLM-tool-dispatch path in local_desktop creates TODOs directly with
709
+ * the LLM's explicit step list, so this regex-based role inference no
710
+ * longer drives dispatch. Still called from decomposeTask's fallback for
711
+ * non-local_desktop and legacy planner paths. Remove after those are
712
+ * migrated to tool dispatch.
713
+ */
557
714
  determineDesiredRoleSequence(prompt, roleAssignments) {
558
715
  const normalized = prompt.toLowerCase();
559
716
  const explicitResearch = /\b(brain|gpt|research(?:es|ed|ing)?|investigat|analy[sz]e|analysis|sound idea|review the idea|pressure[- ]?test|brainstorm(?:ing)?)\b/.test(normalized)
@@ -607,6 +764,7 @@ class WorkflowEngine extends events_1.EventEmitter {
607
764
  agentName: agent.name,
608
765
  provider: agent.provider,
609
766
  model: agent.model,
767
+ role: 'research',
610
768
  dependsOn,
611
769
  status: 'pending',
612
770
  expectedOutput: 'Clear implementation guidance, risks, and recommendations for the downstream coder.',
@@ -623,6 +781,7 @@ class WorkflowEngine extends events_1.EventEmitter {
623
781
  agentName: agent.name,
624
782
  provider: agent.provider,
625
783
  model: agent.model,
784
+ role: 'coding',
626
785
  dependsOn,
627
786
  status: 'pending',
628
787
  expectedOutput: 'Completed deliverable that satisfies the user request.',
@@ -639,6 +798,7 @@ class WorkflowEngine extends events_1.EventEmitter {
639
798
  agentName: agent.name,
640
799
  provider: agent.provider,
641
800
  model: agent.model,
801
+ role: 'qa',
642
802
  dependsOn,
643
803
  status: 'pending',
644
804
  expectedOutput: 'PASS if the deliverable fully satisfies the request, otherwise FAIL with actionable defects.',
@@ -685,17 +845,45 @@ class WorkflowEngine extends events_1.EventEmitter {
685
845
  : role === 'qa'
686
846
  ? 'Your role is QA. Verify the delivered work, then complete the TODO through the worker tool or create the next fix handoff.'
687
847
  : 'Your role is implementer. Do the assigned work, then complete the TODO through the worker tool.';
848
+ const botSettingsPrompt = this.buildBotSettingsPrompt(profile);
849
+ const capabilityInstructions = profile
850
+ ? (0, capabilities_1.buildCapabilityInstructionBlock)(data.getAgentRolePriorities(profile))
851
+ : '';
688
852
  return [
689
- `You are ${profile?.name || 'a workflow worker'}.`,
853
+ botSettingsPrompt || `You are ${profile?.name || 'a workflow worker'}.`,
854
+ '',
855
+ '[Orchestrated Worker Guardrails]',
690
856
  'You are executing one orchestrated TODO task.',
691
857
  roleLine,
692
858
  'Follow the assignment in the user message exactly.',
693
859
  'Use available tools when needed.',
694
860
  'Do not return a plain-text final answer until you have called complete_worker_task or block_worker_task.',
861
+ 'Do not create side TODOs with add_task while working an assigned TODO. Use complete_worker_task.handoff_prompt or insert_task when follow-up work is needed.',
695
862
  'Do not add workflow planning, orchestration narration, or TODO-management commentary outside those tools.',
696
863
  'If blocked after checking available context, call block_worker_task with blocker_summary, checked_context, and user_question.',
697
864
  'If the task is complete, call complete_worker_task with output_summary and any needed handoff prompt or inserted next task.',
698
- ].join('\n');
865
+ capabilityInstructions ? '' : null,
866
+ capabilityInstructions || null,
867
+ ].filter(Boolean).join('\n');
868
+ }
869
+ buildBotSettingsPrompt(profile) {
870
+ if (!profile)
871
+ return '';
872
+ const directPrompt = String(profile.final_prompt || profile.soul_md || '').trim();
873
+ if (directPrompt)
874
+ return directPrompt;
875
+ const sections = [`You are ${profile.name}.`];
876
+ const addSection = (label, value) => {
877
+ const text = String(value || '').trim();
878
+ if (text)
879
+ sections.push('', label, text);
880
+ };
881
+ addSection('PURPOSE', profile.purpose_md);
882
+ addSection('IDENTITY', profile.identity_summary);
883
+ addSection('SKILLS', profile.skills_md);
884
+ addSection('TOOLS', profile.tools_md);
885
+ addSection('MEMORY', profile.memory_md);
886
+ return sections.join('\n');
699
887
  }
700
888
  buildOrchestratedTodoHandledResult(taskId) {
701
889
  if (!taskId)
@@ -718,13 +906,18 @@ class WorkflowEngine extends events_1.EventEmitter {
718
906
  return Boolean(data.getTodoTask(taskId, 'completed')
719
907
  || data.getTodoTask(taskId, 'active')?.blocker_summary);
720
908
  }
721
- buildWorkflowStepPrompt(step, pinnedContext, handoffArtifacts) {
722
- const sections = [step.prompt.trim()];
909
+ buildWorkflowStepPrompt(step, pinnedContext, handoffArtifacts, handoffPrompts = [], localDesktopHandoffMode = false) {
910
+ const primaryPrompt = step.prompt.trim();
911
+ const sections = [primaryPrompt];
912
+ if (localDesktopHandoffMode && handoffPrompts.length > 0) {
913
+ const label = handoffPrompts.length === 1 ? 'Previous worker handoff' : 'Previous worker handoffs';
914
+ sections.push(`${label}:\n${handoffPrompts.join('\n\n')}`);
915
+ }
723
916
  if (pinnedContext.length > 0) {
724
917
  sections.push(`Pinned context:\n${pinnedContext.join('\n\n')}`);
725
918
  }
726
919
  if (handoffArtifacts.length > 0) {
727
- sections.push(`Input artifacts from previous steps:\n${handoffArtifacts.join('\n\n')}`);
920
+ sections.push(`${localDesktopHandoffMode ? 'Supporting artifacts from previous steps' : 'Input artifacts from previous steps'}:\n${handoffArtifacts.join('\n\n')}`);
728
921
  }
729
922
  return sections.join('\n\n');
730
923
  }
@@ -766,68 +959,39 @@ class WorkflowEngine extends events_1.EventEmitter {
766
959
  }
767
960
  return defaultProfile;
768
961
  }
962
+ /**
963
+ * Resolve a step's role. Prefers the structured `role` field set at plan
964
+ * creation (from an explicit planner decision or the bot's role_class).
965
+ * Falls through to a small normalizer that maps common role synonyms
966
+ * (e.g. "verify" → "qa") onto the three canonical workflow roles.
967
+ * Does NOT scan prose. Phase B of orchestration-plan.txt: pass/fail,
968
+ * routing, and task creation must come from structured data, not from
969
+ * English wording.
970
+ */
769
971
  inferRoleForStep(step) {
770
- const description = `${step.description || ''}`.toLowerCase();
771
- const prompt = `${step.prompt || ''} ${step.expectedOutput || ''}`.toLowerCase();
772
- const leadingVerbMatch = description.match(/^(?:[a-z0-9_.-]+\s+)?(qa|review(?:s|ed|ing)?|verif(?:y|ies|ied|ying)|validation|test(?:s|ed|ing)?|check(?:s|ed|ing)?|research(?:es|ed|ing)?|investigat(?:e|es|ed|ing)|analysis|analy[sz](?:e|es|ed|ing)|compar(?:e|es|ed|ing)|inspect(?:s|ed|ing)?|discover(?:s|ed|ing)?|defin(?:e|es|ed|ing)|build(?:s|ed|ing)?|creat(?:e|es|ed|ing)|write(?:s|n)?|writ(?:ing)?|fix(?:es|ed|ing)?|edit(?:s|ed|ing)?|updat(?:e|es|ed|ing)|refactor(?:s|ed|ing)?|implement(?:s|ed|ing)?|code(?:s|d|ing)?)\b/);
773
- const qaPattern = /\b(qa|quality assurance|review(?:s|ed|ing)?|verif(?:y|ies|ied|ying|ication)?|validation|test(?:s|ed|ing)?|check(?:s|ed|ing)?)\b/;
774
- const researchPattern = /\b(research(?:es|ed|ing)?|investigat(?:e|es|ed|ing)|analysis|analy[sz](?:e|es|ed|ing)|compar(?:e|es|ed|ing)|inspect(?:s|ed|ing)?|look up|discover(?:s|ed|ing)?|defin(?:e|es|ed|ing)|direction|spec(?:ification)?s?|requirements?)\b/;
775
- const codingPattern = /\b(code(?:s|d|ing)?|implement(?:s|ed|ing)?|build(?:s|ed|ing)?|creat(?:e|es|ed|ing)|write(?:s|n)?|writ(?:ing)?|fix(?:es|ed|ing)?|edit(?:s|ed|ing)?|updat(?:e|es|ed|ing)|refactor(?:s|ed|ing)?)\b/;
776
- if (leadingVerbMatch) {
777
- const verb = leadingVerbMatch[1];
778
- if (/(qa|review|verif|validation|test|check)/.test(verb))
779
- return 'qa';
780
- if (/(research|investigat|analysis|analy|compar|inspect|discover|defin)/.test(verb))
781
- return 'research';
782
- if (/(build|creat|write|writ|fix|edit|updat|refactor|implement|code)/.test(verb))
783
- return 'coding';
784
- }
785
- if (qaPattern.test(description) && !codingPattern.test(description))
972
+ const raw = typeof step.role === 'string' ? step.role.trim().toLowerCase() : '';
973
+ if (!raw)
974
+ return null;
975
+ if (raw === 'qa' || raw === 'review' || raw === 'reviewer' || raw === 'quality' || raw === 'quality assurance')
786
976
  return 'qa';
787
- if (codingPattern.test(description))
788
- return 'coding';
789
- if (researchPattern.test(description))
790
- return 'research';
791
- if (codingPattern.test(prompt) && !qaPattern.test(prompt))
792
- return 'coding';
793
- if (qaPattern.test(prompt))
977
+ if (raw === 'verify' || raw === 'verification')
978
+ return 'qa';
979
+ if (/\b(verify|verification)\b/.test(raw))
794
980
  return 'qa';
795
- if (researchPattern.test(prompt))
981
+ if (raw === 'research' || raw === 'researcher' || raw === 'analysis' || raw === 'analyst' || raw === 'planning' || raw === 'planner' || raw === 'brainstorming' || raw === 'design' || raw === 'designer')
796
982
  return 'research';
797
- if (codingPattern.test(prompt))
983
+ if (raw === 'coding' || raw === 'code' || raw === 'coder' || raw === 'build' || raw === 'building' || raw === 'builder' || raw === 'implement' || raw === 'implementation' || raw === 'dev' || raw === 'developer' || raw === 'engineering' || raw === 'commit' || raw === 'deploy')
798
984
  return 'coding';
985
+ // Comma-separated multi-roles (legacy) — take the first canonical match.
986
+ if (!/[,/]/.test(raw))
987
+ return null;
988
+ for (const part of raw.split(/[,/]/).map((s) => s.trim()).filter(Boolean)) {
989
+ const mapped = this.inferRoleForStep({ role: part });
990
+ if (mapped)
991
+ return mapped;
992
+ }
799
993
  return null;
800
994
  }
801
- pruneTrailingPostQaSteps(steps) {
802
- const firstQaIndex = steps.findIndex((step) => this.inferRoleForStep(step) === 'qa');
803
- if (firstQaIndex < 0)
804
- return steps;
805
- const shouldDrop = (step) => {
806
- if (step.index <= firstQaIndex)
807
- return false;
808
- const normalized = `${step.description || ''} ${step.prompt || ''}`.toLowerCase();
809
- return /\b(if applicable|based on qa feedback|based on feedback|revise|revision|finalize|confirm completion|compile instructions|ensure the file is accessible|ensure file is accessible)\b/.test(normalized);
810
- };
811
- const filtered = steps.filter((step) => !shouldDrop(step));
812
- if (filtered.length === steps.length)
813
- return steps;
814
- return filtered.map((step, newIndex) => {
815
- const oldIndex = step.index;
816
- const retainedPriorSteps = filtered
817
- .filter((candidate) => candidate.index < oldIndex)
818
- .map((candidate) => candidate.index);
819
- const reindexedDeps = (step.dependsOn || [])
820
- .filter((dep) => retainedPriorSteps.includes(dep))
821
- .map((dep) => filtered.findIndex((candidate) => candidate.index === dep))
822
- .filter((dep) => dep >= 0);
823
- return {
824
- ...step,
825
- index: newIndex,
826
- id: step.id || `step-${newIndex}`,
827
- dependsOn: reindexedDeps,
828
- };
829
- });
830
- }
831
995
  findPriorCodingStepIndex(steps, currentIndex) {
832
996
  const priorCodingStep = [...steps]
833
997
  .filter((candidate) => candidate.index < currentIndex)
@@ -868,6 +1032,15 @@ class WorkflowEngine extends events_1.EventEmitter {
868
1032
  const failed = new Set();
869
1033
  const allowMissingFooterForDirectStep = !opts?.isOrchestrated && !!opts?.disableDecomposition && steps.length === 1;
870
1034
  while (completed.size + failed.size < steps.length) {
1035
+ if (opts?.abortSignal?.aborted) {
1036
+ for (const step of steps) {
1037
+ if (step.status === 'pending' || step.status === 'running') {
1038
+ step.status = 'skipped';
1039
+ step.error = 'Cancelled by user';
1040
+ }
1041
+ }
1042
+ break;
1043
+ }
871
1044
  // Find steps that are ready to run (all deps met)
872
1045
  const ready = steps.filter(s => s.status === 'pending' &&
873
1046
  s.dependsOn.every(dep => completed.has(dep)));
@@ -883,10 +1056,22 @@ class WorkflowEngine extends events_1.EventEmitter {
883
1056
  }
884
1057
  // Execute ready steps in parallel
885
1058
  await Promise.all(ready.map(async (step) => {
1059
+ if (opts?.abortSignal?.aborted) {
1060
+ step.status = 'skipped';
1061
+ step.error = 'Cancelled by user';
1062
+ return;
1063
+ }
886
1064
  // Build context from completed dependency results — pass only RESULT artifact
887
1065
  const handoffArtifacts = [];
1066
+ const handoffPrompts = [];
1067
+ const localDesktopHandoffMode = (0, runtime_context_1.normalizeRuntimeMode)(opts?.runtimeMode || this.runtimeMode) === 'local_desktop'
1068
+ && opts?.isOrchestrated === true;
888
1069
  for (const depIndex of step.dependsOn) {
889
1070
  const depStep = steps[depIndex];
1071
+ const dependencyHandoff = String(depStep?._handoffPrompt || '').trim();
1072
+ if (localDesktopHandoffMode && dependencyHandoff) {
1073
+ handoffPrompts.push(dependencyHandoff);
1074
+ }
890
1075
  if (depStep?.result) {
891
1076
  const footer = (0, status_parser_1.parseStructuredFooter)(depStep.result);
892
1077
  const artifact = footer.result || depStep.result;
@@ -904,7 +1089,45 @@ class WorkflowEngine extends events_1.EventEmitter {
904
1089
  }
905
1090
  }
906
1091
  }
907
- const fullPrompt = this.buildWorkflowStepPrompt(step, pinnedContext, handoffArtifacts);
1092
+ const initialImageDeliveryMode = opts?.workerImageDeliveryMode || 'inline';
1093
+ const fullPrompt = this.buildWorkflowStepPrompt(step, pinnedContext, handoffArtifacts, handoffPrompts, localDesktopHandoffMode);
1094
+ // approvalRequired gate: when enabled, emit an approval request
1095
+ // event and wait for the listener to resolve before proceeding.
1096
+ const orchCfg = (0, config_1.loadConfig)();
1097
+ if (orchCfg.approvalRequired === true) {
1098
+ const approved = await new Promise((resolve) => {
1099
+ const timeout = setTimeout(() => resolve(false), 300_000); // 5-minute timeout
1100
+ this.emit('approval-request', {
1101
+ workflowId,
1102
+ step: { id: step.id, index: step.index, description: step.description, agentName: step.agentName },
1103
+ totalSteps: steps.length,
1104
+ resolve: (result) => {
1105
+ clearTimeout(timeout);
1106
+ resolve(result);
1107
+ },
1108
+ });
1109
+ // If there are no listeners for approval-request, auto-approve
1110
+ // to avoid hanging when no approval manager is wired up.
1111
+ if (this.listenerCount('approval-request') === 0) {
1112
+ clearTimeout(timeout);
1113
+ resolve(true);
1114
+ }
1115
+ });
1116
+ if (!approved) {
1117
+ step.status = 'failed';
1118
+ step.error = 'Workflow step rejected during approval';
1119
+ step.completedAt = Date.now();
1120
+ failed.add(step.index);
1121
+ for (const s of steps) {
1122
+ if (s.dependsOn.includes(step.index) && s.status === 'pending') {
1123
+ s.status = 'skipped';
1124
+ s.error = 'Stopped because a prior step was rejected';
1125
+ }
1126
+ }
1127
+ this.emitProgress(workflowId, step, steps, 'step-failed', opts?.onProgress);
1128
+ return;
1129
+ }
1130
+ }
908
1131
  step.status = 'running';
909
1132
  step.startedAt = Date.now();
910
1133
  this.emitProgress(workflowId, step, steps, 'step-started', opts?.onProgress, undefined, opts?.onWorkerChunk);
@@ -916,6 +1139,7 @@ class WorkflowEngine extends events_1.EventEmitter {
916
1139
  }
917
1140
  catch { /* best effort */ }
918
1141
  try {
1142
+ throwIfWorkflowAborted(opts?.abortSignal);
919
1143
  // Build previous steps summary for workflow context
920
1144
  const prevStepsText = steps
921
1145
  .filter(s => s.status === 'completed' && s.index < step.index)
@@ -934,10 +1158,43 @@ class WorkflowEngine extends events_1.EventEmitter {
934
1158
  retryCount: step.attempts,
935
1159
  loopCount: step.loopCount,
936
1160
  };
937
- const result = await this.executeStep(step, fullPrompt, conversationId, opts, opts?.apiKey, wfContext);
1161
+ let activeImageDeliveryMode = initialImageDeliveryMode;
1162
+ let result = await this.executeStep(step, fullPrompt, conversationId, { ...opts, workerImageDeliveryMode: activeImageDeliveryMode }, opts?.apiKey, wfContext);
1163
+ const requestedImage = activeImageDeliveryMode === 'reference'
1164
+ ? parseImageEscalationRequest(result, opts?.storedAttachments)
1165
+ : null;
1166
+ if (requestedImage) {
1167
+ throwIfWorkflowAborted(opts?.abortSignal);
1168
+ this.emitProgress(workflowId, step, steps, 'step-progress', opts?.onProgress, `${step.agentName} requested the original image for ${step.description}`);
1169
+ activeImageDeliveryMode = 'inline';
1170
+ result = await this.executeStep(step, fullPrompt, conversationId, { ...opts, workerImageDeliveryMode: activeImageDeliveryMode }, opts?.apiKey, wfContext);
1171
+ const repeatedEscalation = parseImageEscalationRequest(result, opts?.storedAttachments);
1172
+ if (repeatedEscalation) {
1173
+ step.status = 'failed';
1174
+ step.result = result;
1175
+ step.error = `Worker requested image escalation again after the original image was already provided (${repeatedEscalation.id}).`;
1176
+ step.completedAt = Date.now();
1177
+ failed.add(step.index);
1178
+ this.emitProgress(workflowId, step, steps, 'step-failed', opts?.onProgress, undefined, opts?.onWorkerChunk);
1179
+ try {
1180
+ const execId = step._execId;
1181
+ if (execId)
1182
+ data.updateStepExecution(execId, {
1183
+ status: 'failed',
1184
+ attempts: step.attempts,
1185
+ error: step.error,
1186
+ completedAt: new Date().toISOString().replace('T', ' ').replace('Z', ''),
1187
+ });
1188
+ }
1189
+ catch { /* best effort */ }
1190
+ return;
1191
+ }
1192
+ }
938
1193
  if (opts?.isOrchestrated && opts.taskId) {
1194
+ throwIfWorkflowAborted(opts?.abortSignal);
939
1195
  const completedTodo = data.getTodoTask(opts.taskId, 'completed');
940
1196
  if (completedTodo) {
1197
+ step._handoffPrompt = completedTodo.handoff_prompt || '';
941
1198
  step.status = 'completed';
942
1199
  step.result = completedTodo.output_summary || completedTodo.handoff_prompt || result;
943
1200
  step.completedAt = Date.now();
@@ -1214,6 +1471,31 @@ class WorkflowEngine extends events_1.EventEmitter {
1214
1471
  catch { /* best effort */ }
1215
1472
  }
1216
1473
  catch (err) {
1474
+ if (isWorkflowAbortError(err)) {
1475
+ step.status = 'skipped';
1476
+ step.error = 'Cancelled by user';
1477
+ step.completedAt = Date.now();
1478
+ for (const s of steps) {
1479
+ if (s.status === 'pending') {
1480
+ s.status = 'skipped';
1481
+ s.error = 'Cancelled by user';
1482
+ }
1483
+ }
1484
+ this.emitProgress(workflowId, step, steps, 'step-failed', opts?.onProgress, undefined, opts?.onWorkerChunk);
1485
+ try {
1486
+ const execId = step._execId;
1487
+ if (execId) {
1488
+ data.updateStepExecution(execId, {
1489
+ status: 'failed',
1490
+ attempts: step.attempts,
1491
+ error: step.error,
1492
+ completedAt: new Date().toISOString().replace('T', ' ').replace('Z', ''),
1493
+ });
1494
+ }
1495
+ }
1496
+ catch { /* best effort */ }
1497
+ return;
1498
+ }
1217
1499
  const authLikeFailure = typeof err?.message === 'string'
1218
1500
  && (err.message.startsWith('AUTH_REQUIRED:')
1219
1501
  || /invalid_grant/i.test(err.message)
@@ -1274,14 +1556,40 @@ class WorkflowEngine extends events_1.EventEmitter {
1274
1556
  }
1275
1557
  step.attempts++;
1276
1558
  step.error = err.message;
1277
- if (typeof err?.message === 'string' && this.isTerminalProviderFailure(err.message)) {
1559
+ const providerLimitFailure = typeof err?.message === 'string'
1560
+ ? this.classifyProviderLimitFailure(err.message)
1561
+ : null;
1562
+ const terminalProviderFailure = typeof err?.message === 'string' && this.isTerminalProviderFailure(err.message);
1563
+ if (providerLimitFailure || terminalProviderFailure) {
1278
1564
  step.status = 'failed';
1565
+ step.result = providerLimitFailure?.exactMessage || err.message;
1566
+ step.error = providerLimitFailure?.exactMessage || err.message;
1279
1567
  step.completedAt = Date.now();
1280
1568
  failed.add(step.index);
1281
1569
  for (const s of steps) {
1282
1570
  if (s.dependsOn.includes(step.index) && s.status === 'pending') {
1283
1571
  s.status = 'skipped';
1284
- s.error = 'Stopped because a prior step failed with a provider/runtime error';
1572
+ s.error = providerLimitFailure
1573
+ ? 'Stopped because a prior step hit a provider usage/rate limit'
1574
+ : 'Stopped because a prior step failed with a provider/runtime error';
1575
+ }
1576
+ }
1577
+ if (opts?.taskId) {
1578
+ try {
1579
+ data.markTodoTaskRuntimeBlocked(opts.taskId, {
1580
+ actor: { actorType: 'orchestrator', actorId: 'Workflow Engine' },
1581
+ blockerSummary: providerLimitFailure?.exactMessage || String(err?.message || err),
1582
+ checkedContext: providerLimitFailure
1583
+ ? 'Provider returned a quota/rate-limit error during worker execution. The exact provider message is preserved verbatim.'
1584
+ : 'Provider/runtime error during worker execution. The exact provider or PTY/app-server failure reason is preserved verbatim.',
1585
+ userQuestion: providerLimitFailure
1586
+ ? 'Fix the provider account limit or wait until the provider retry window resets, then resume this plan.'
1587
+ : 'Fix the provider/runtime issue, then resume this plan.',
1588
+ reasonCode: providerLimitFailure?.reasonCode || 'provider_runtime_error',
1589
+ });
1590
+ }
1591
+ catch (blockErr) {
1592
+ console.warn(`[workflow-engine] failed to mark TODO ${opts.taskId} blocked after provider/runtime failure: ${blockErr?.message || blockErr}`);
1285
1593
  }
1286
1594
  }
1287
1595
  this.emitProgress(workflowId, step, steps, 'step-failed', opts?.onProgress, undefined, opts?.onWorkerChunk);
@@ -1292,6 +1600,7 @@ class WorkflowEngine extends events_1.EventEmitter {
1292
1600
  status: 'failed',
1293
1601
  attempts: step.attempts,
1294
1602
  error: step.error,
1603
+ resultSummary: step.result?.slice(0, 200),
1295
1604
  completedAt: new Date().toISOString().replace('T', ' ').replace('Z', ''),
1296
1605
  });
1297
1606
  }
@@ -1424,20 +1733,216 @@ class WorkflowEngine extends events_1.EventEmitter {
1424
1733
  const effectivePrompt = opts?.isOrchestrated && opts?.taskId
1425
1734
  ? `CURRENT TODO ID: ${opts.taskId}\nUse this TODO id when calling complete_worker_task or block_worker_task.\n\n${prompt}`
1426
1735
  : prompt;
1427
- const messages = [{ role: 'user', content: effectivePrompt }];
1736
+ const workerImageDeliveryMode = opts?.workerImageDeliveryMode || 'inline';
1737
+ const deferredImageReferenceBlock = workerImageDeliveryMode === 'reference'
1738
+ ? buildDeferredImageReferenceBlock(opts?.storedAttachments)
1739
+ : '';
1740
+ if (deferredImageReferenceBlock) {
1741
+ systemPrompt = [
1742
+ systemPrompt,
1743
+ '',
1744
+ 'If the user message includes an "Original Image Context Available" section and you truly need the original image, reply exactly with ESCALATE_IMAGE <id> and no other text.',
1745
+ ].join('\n');
1746
+ }
1747
+ const promptWithDeferredImageNote = deferredImageReferenceBlock
1748
+ ? `${effectivePrompt}\n\n${deferredImageReferenceBlock}`
1749
+ : effectivePrompt;
1750
+ // Build user message — include native images if the worker's provider supports them
1751
+ const workerAttachments = opts?.storedAttachments;
1752
+ const workerSupportsImages = activeRuntime.providerName === 'anthropic'
1753
+ || activeRuntime.providerName === 'openai'
1754
+ || activeRuntime.providerName === 'google'
1755
+ || activeRuntime.providerName === 'codex-cli'
1756
+ || activeRuntime.providerName === 'claude-cli';
1757
+ let userContent = promptWithDeferredImageNote;
1758
+ if (workerImageDeliveryMode === 'inline' && workerAttachments && workerAttachments.length > 0 && workerSupportsImages) {
1759
+ const parts = [
1760
+ { type: 'text', text: promptWithDeferredImageNote },
1761
+ ];
1762
+ for (const att of workerAttachments) {
1763
+ if (att.storagePath && att.mimeType?.startsWith('image/')) {
1764
+ try {
1765
+ const imageData = fs_1.default.readFileSync(att.storagePath).toString('base64');
1766
+ parts.push({ type: 'image', mimeType: att.mimeType, data: imageData });
1767
+ }
1768
+ catch { /* file may not exist */ }
1769
+ }
1770
+ }
1771
+ if (parts.length > 1)
1772
+ userContent = parts;
1773
+ }
1774
+ const messages = [{ role: 'user', content: userContent }];
1428
1775
  const promptChars = effectivePrompt.length;
1429
1776
  const systemChars = systemPrompt.length;
1430
1777
  const isLocalCliSession = effectiveRuntimeMode === 'local_desktop'
1431
1778
  && (activeRuntime.providerName === 'claude-cli' || activeRuntime.providerName === 'codex-cli');
1779
+ const workflowCliSessionTransport = resolveWorkflowCliSessionTransport(activeRuntime.providerName, isLocalCliSession);
1780
+ const cliSessionScopeKey = String(opts?.cliSessionScopeKey || '').trim() || null;
1432
1781
  const hasPersistentConversation = isLocalCliSession && !!conversationId;
1433
1782
  const cliSessionEpochPlan = hasPersistentConversation
1434
- ? (0, cli_session_epoch_1.selectCliSessionEpoch)(conversationId, step.agentId, activeRuntime.providerName)
1435
- : { existing: undefined, resumeSessionId: null, resetReason: null };
1783
+ ? (0, cli_session_epoch_1.evaluateWarmSessionReuse)(conversationId, step.agentId, activeRuntime.providerName, undefined, cliSessionScopeKey)
1784
+ : {
1785
+ outcome: 'cold_start',
1786
+ existing: undefined,
1787
+ resumeSessionId: null,
1788
+ resetReason: null,
1789
+ topicId: null,
1790
+ };
1436
1791
  let activeCliSessionId = cliSessionEpochPlan.resumeSessionId;
1437
1792
  const cliEpochStartedAt = cliSessionEpochPlan.resumeSessionId
1438
1793
  ? (cliSessionEpochPlan.existing?.epoch_started_at || (0, cli_session_epoch_1.localTimestamp)())
1439
1794
  : (0, cli_session_epoch_1.localTimestamp)();
1440
- const ptyConversationKey = buildWorkflowPtyConversationKey(conversationId, step, workflowContext);
1795
+ const ptyConversationKey = buildWorkflowPtyConversationKey(conversationId, step, workflowContext, cliSessionScopeKey);
1796
+ let cliEpochResetReason = cliSessionEpochPlan.resetReason;
1797
+ // Resolve prompt context window early for session launch classification,
1798
+ // cross-bot context injection, and bootstrap decisions.
1799
+ const promptContextWindow = hasPersistentConversation
1800
+ ? (0, context_window_1.getPromptContextWindow)(conversationId, 5, {
1801
+ targetBotId: step.agentId,
1802
+ targetBotName: step.agentName,
1803
+ })
1804
+ : { summary: null, turns: [], sourceConversationId: conversationId || '', carriedForward: false, mode: 'new_topic', allowBootstrap: false, lastCrossBotTurn: null };
1805
+ // Session lifecycle logging — mirrors local-server.ts invariant plus
1806
+ // structured telemetry events for diagnostics.
1807
+ const sessionLaunchReason = cliSessionEpochPlan.resumeSessionId
1808
+ ? 'resumed'
1809
+ : promptContextWindow.mode === 'new_topic'
1810
+ ? 'new_topic/no_context'
1811
+ : cliSessionEpochPlan.resetReason
1812
+ ? cliSessionEpochPlan.resetReason
1813
+ : 'fresh_with_bootstrap';
1814
+ console.info(`[workflow-engine] session_launch_reason=${sessionLaunchReason} agent=${step.agentName} provider=${activeRuntime.providerName} conversationId=${conversationId || '(none)'} resumeSessionId=${cliSessionEpochPlan.resumeSessionId || '(none)'} mode=${promptContextWindow.mode || 'unknown'} bootstrap=${!!promptContextWindow.allowBootstrap}`);
1815
+ console.info(`[workflow-engine] cli_session_selected conversationId=${conversationId || '(none)'} botId=${step.agentId} provider=${activeRuntime.providerName} selectedSessionId=${cliSessionEpochPlan.resumeSessionId || '(none)'} resetReason=${cliSessionEpochPlan.resetReason || '(none)'}`);
1816
+ if (workflowCliSessionTransport === 'codex-app-server'
1817
+ && cliSessionEpochPlan.outcome === 'epoch_reset'
1818
+ && cliSessionEpochPlan.resetReason === 'token_limit') {
1819
+ console.info(`[workflow-engine] codex_thread_reset_token_limit conversationId=${conversationId || '(none)'} botId=${step.agentId} provider=${activeRuntime.providerName} previousSessionId=${cliSessionEpochPlan.existing?.session_id || '(none)'} topicId=${cliSessionEpochPlan.topicId || '(none)'}`);
1820
+ }
1821
+ if (cliSessionEpochPlan.resumeSessionId) {
1822
+ console.info(`[workflow-engine] cli_session_resuming conversationId=${conversationId || '(none)'} botId=${step.agentId} provider=${activeRuntime.providerName} sessionId=${cliSessionEpochPlan.resumeSessionId}`);
1823
+ }
1824
+ else if (promptContextWindow.mode === 'new_topic') {
1825
+ console.info(`[workflow-engine] cli_session_fresh_without_bootstrap_new_topic conversationId=${conversationId || '(none)'} botId=${step.agentId} provider=${activeRuntime.providerName}`);
1826
+ }
1827
+ else if (promptContextWindow.allowBootstrap) {
1828
+ console.info(`[workflow-engine] cli_session_fresh_bootstrap_applied conversationId=${conversationId || '(none)'} botId=${step.agentId} provider=${activeRuntime.providerName}`);
1829
+ }
1830
+ else {
1831
+ console.warn(`[workflow-engine] cli_session_fresh_without_bootstrap_unexpected conversationId=${conversationId || '(none)'} botId=${step.agentId} provider=${activeRuntime.providerName} mode=${promptContextWindow.mode || 'unknown'}`);
1832
+ }
1833
+ // For initial fresh sessions (not resumed) with existing context, write
1834
+ // bootstrap history and inject cross-bot context into the user message.
1835
+ // This ensures the invariant: fresh session for existing context = bootstrap required.
1836
+ if (hasPersistentConversation
1837
+ && !cliSessionEpochPlan.resumeSessionId
1838
+ && promptContextWindow.allowBootstrap
1839
+ && conversationId) {
1840
+ const topicId = data.getPrimaryTopicIdForConversation(conversationId) || undefined;
1841
+ const bootstrapHistoryFilePath = (0, cli_bootstrap_history_1.writeConversationBootstrapHistoryFile)({
1842
+ conversationId,
1843
+ topicId,
1844
+ botId: step.agentId,
1845
+ botName: step.agentName,
1846
+ });
1847
+ if (bootstrapHistoryFilePath) {
1848
+ const bootstrapInstruction = [
1849
+ `Please read the history file at: ${bootstrapHistoryFilePath}`,
1850
+ 'Use it for context only.',
1851
+ 'There is no need to mention the file, its path, or that you loaded context unless the user explicitly asks about it.',
1852
+ 'Then respond to the user request below.',
1853
+ '',
1854
+ ].join('\n');
1855
+ const currentUserContent = messages[0]?.content;
1856
+ const currentText = typeof currentUserContent === 'string'
1857
+ ? currentUserContent
1858
+ : Array.isArray(currentUserContent)
1859
+ ? currentUserContent.filter((p) => p?.type === 'text').map((p) => p.text).join('\n')
1860
+ : String(currentUserContent || '');
1861
+ messages[0] = { role: 'user', content: `${bootstrapInstruction}${currentText}` };
1862
+ }
1863
+ }
1864
+ // Inject cross-bot context for fresh sessions (not resumed) when the
1865
+ // previous turn was from a different bot. Do not rely on CLI session
1866
+ // memory for cross-bot context.
1867
+ if (hasPersistentConversation
1868
+ && !cliSessionEpochPlan.resumeSessionId
1869
+ && promptContextWindow.lastCrossBotTurn) {
1870
+ const crossBotBlock = (0, context_window_1.formatCrossBotPreviousTurn)(promptContextWindow.lastCrossBotTurn);
1871
+ const currentUserContent = messages[0]?.content;
1872
+ if (typeof currentUserContent === 'string') {
1873
+ messages[0] = { role: 'user', content: `${crossBotBlock}\n\n${currentUserContent}` };
1874
+ }
1875
+ }
1876
+ // For resumed sessions, inject cross-bot context into the user message
1877
+ // since the CLI session memory only has same-bot context.
1878
+ if (hasPersistentConversation
1879
+ && !!cliSessionEpochPlan.resumeSessionId
1880
+ && promptContextWindow.lastCrossBotTurn) {
1881
+ const crossBotBlock = (0, context_window_1.formatCrossBotPreviousTurn)(promptContextWindow.lastCrossBotTurn);
1882
+ const currentUserContent = messages[0]?.content;
1883
+ if (typeof currentUserContent === 'string') {
1884
+ messages[0] = { role: 'user', content: `${crossBotBlock}\n\n${currentUserContent}` };
1885
+ }
1886
+ }
1887
+ // Bootstrap fallback: when a resumed CLI session fails, rebuild the prompt
1888
+ // with bootstrap context (running summary + last 5 turns) before retrying
1889
+ // with a fresh session. Ensures prompt-building path and session runtime
1890
+ // reality always agree.
1891
+ let freshCliBootstrapFallbackApplied = false;
1892
+ let freshCliBootstrapFileWritten = false;
1893
+ const applyFreshCliBootstrapFallback = (reason) => {
1894
+ if (freshCliBootstrapFallbackApplied)
1895
+ return;
1896
+ freshCliBootstrapFallbackApplied = true;
1897
+ cliEpochResetReason = reason;
1898
+ if (!conversationId)
1899
+ return;
1900
+ const topicId = data.getPrimaryTopicIdForConversation(conversationId) || undefined;
1901
+ const freshPromptContextWindow = (0, context_window_1.getPromptContextWindow)(conversationId, 5, {
1902
+ targetBotId: step.agentId,
1903
+ targetBotName: step.agentName,
1904
+ });
1905
+ const bootstrapHistoryFilePath = freshPromptContextWindow.allowBootstrap
1906
+ ? (0, cli_bootstrap_history_1.writeConversationBootstrapHistoryFile)({
1907
+ conversationId,
1908
+ topicId,
1909
+ botId: step.agentId,
1910
+ botName: step.agentName,
1911
+ })
1912
+ : null;
1913
+ freshCliBootstrapFileWritten = !!bootstrapHistoryFilePath;
1914
+ if (bootstrapHistoryFilePath) {
1915
+ const bootstrapInstruction = [
1916
+ `Please read the history file at: ${bootstrapHistoryFilePath}`,
1917
+ 'Use it for context only.',
1918
+ 'There is no need to mention the file, its path, or that you loaded context unless the user explicitly asks about it.',
1919
+ 'Then respond to the user request below.',
1920
+ '',
1921
+ ].join('\n');
1922
+ const currentUserContent = messages[0]?.content;
1923
+ const currentText = typeof currentUserContent === 'string'
1924
+ ? currentUserContent
1925
+ : Array.isArray(currentUserContent)
1926
+ ? currentUserContent.filter((p) => p?.type === 'text').map((p) => p.text).join('\n')
1927
+ : String(currentUserContent || '');
1928
+ messages[0] = { role: 'user', content: `${bootstrapInstruction}${currentText}` };
1929
+ }
1930
+ if (freshPromptContextWindow.lastCrossBotTurn) {
1931
+ const crossBotBlock = (0, context_window_1.formatCrossBotPreviousTurn)(freshPromptContextWindow.lastCrossBotTurn);
1932
+ const currentUserContent = messages[0]?.content;
1933
+ if (typeof currentUserContent === 'string') {
1934
+ messages[0] = { role: 'user', content: `${crossBotBlock}\n\n${currentUserContent}` };
1935
+ }
1936
+ }
1937
+ console.info(`[workflow-engine] session_launch_reason=resume_failed agent=${step.agentName} provider=${activeRuntime.providerName} conversationId=${conversationId} bootstrap=${!!bootstrapHistoryFilePath}`);
1938
+ console.warn(`[workflow-engine] cli_session_resume_failed conversationId=${conversationId || '(none)'} botId=${step.agentId} provider=${activeRuntime.providerName} bootstrap=${!!bootstrapHistoryFilePath}`);
1939
+ if (bootstrapHistoryFilePath) {
1940
+ console.info(`[workflow-engine] cli_session_fresh_bootstrap_applied conversationId=${conversationId || '(none)'} botId=${step.agentId} provider=${activeRuntime.providerName} bootstrapReason=resume_failed`);
1941
+ }
1942
+ else {
1943
+ console.warn(`[workflow-engine] cli_session_fresh_without_bootstrap_unexpected conversationId=${conversationId || '(none)'} botId=${step.agentId} provider=${activeRuntime.providerName} bootstrapReason=resume_failed`);
1944
+ }
1945
+ };
1441
1946
  console.info(`[workflow-engine] step runtime: agent=${step.agentName} provider=${activeRuntime.providerName} model=${activeRuntime.model} runtime=${activeRuntime.runtimeLabel || 'unknown'} promptChars=${promptChars} systemChars=${systemChars} tools=${opts?.disableTools ? 0 : toolDefs.length}`);
1442
1947
  // Agentic loop
1443
1948
  let iteration = 0;
@@ -1445,6 +1950,7 @@ class WorkflowEngine extends events_1.EventEmitter {
1445
1950
  let incompleteExecutionRecoveries = 0;
1446
1951
  let selectedRuntimeRetryCount = 0;
1447
1952
  while (iteration < MAX_ITERATIONS) {
1953
+ throwIfWorkflowAborted(opts?.abortSignal);
1448
1954
  if (opts?.isOrchestrated && this.orchestratedTodoHandled(opts.taskId)) {
1449
1955
  return this.buildOrchestratedTodoHandledResult(opts.taskId);
1450
1956
  }
@@ -1464,38 +1970,285 @@ class WorkflowEngine extends events_1.EventEmitter {
1464
1970
  : 'auto';
1465
1971
  let response;
1466
1972
  try {
1467
- if (isLocalCliSession) {
1468
- if (cliSessionEpochPlan.resetReason && iteration === 1 && opts?.onWorkerChunk) {
1469
- const resetDetail = cliSessionEpochPlan.resetReason === 'turn_limit'
1470
- ? 'Resetting CLI session after reaching the turn limit.'
1471
- : cliSessionEpochPlan.resetReason === 'token_limit'
1472
- ? 'Resetting CLI session after reaching the context budget.'
1473
- : 'Resetting CLI session because the runtime changed.';
1474
- opts.onWorkerChunk({
1475
- type: opts?.isOrchestrated ? 'worker_chunk' : 'chunk',
1476
- stepId: step.id,
1477
- agentName: step.agentName,
1478
- description: step.description,
1479
- stepIndex: workflowContext?.stepIndex ?? 0,
1480
- totalSteps: workflowContext?.totalSteps ?? 1,
1481
- text: resetDetail,
1482
- });
1973
+ if (workflowCliSessionTransport === 'codex-app-server') {
1974
+ const codexAppServerManager = (0, codex_app_server_manager_1.getCodexAppServerManager)();
1975
+ let appServerAttempt = 0;
1976
+ while (true) {
1977
+ throwIfWorkflowAborted(opts?.abortSignal);
1978
+ appServerAttempt++;
1979
+ try {
1980
+ console.info(`[workflow-engine] codex_app_server_turn_start workflow=${workflowContext?.workflowId || '(none)'} step=${workflowContext?.stepIndex ?? 0} agent=${step.agentName} conversationId=${ptyConversationKey} taskId=${opts?.taskId ?? '(none)'} attempt=${appServerAttempt}`);
1981
+ const appServerResult = await codexAppServerManager.runTurn({
1982
+ runtimeMode: 'local_desktop',
1983
+ conversationId: ptyConversationKey,
1984
+ botId: step.agentId,
1985
+ botName: step.agentName,
1986
+ cwd: effectiveProjectDir,
1987
+ systemPrompt,
1988
+ messages,
1989
+ abortSignal: opts?.abortSignal,
1990
+ forceFreshSession: !hasPersistentConversation || (iteration === 1 && !codexAppServerManager.hasActiveSession(ptyConversationKey, step.agentId) && !cliSessionEpochPlan.resumeSessionId),
1991
+ resumeSessionId: cliSessionEpochPlan.resumeSessionId || undefined,
1992
+ model: activeRuntime.model,
1993
+ projectId: opts?.projectId ?? null,
1994
+ currentTodoTaskId: opts?.taskId,
1995
+ codexSettings: {
1996
+ reasoningEffort: profile?.codex_reasoning_effort,
1997
+ reasoningSummary: profile?.codex_reasoning_summary,
1998
+ personality: profile?.codex_personality,
1999
+ serviceTier: profile?.codex_service_tier,
2000
+ sandboxPolicy: profile?.codex_sandbox_policy,
2001
+ approvalPolicy: profile?.codex_approval_policy,
2002
+ },
2003
+ onChunk: async (chunk) => {
2004
+ streamedContent += chunk;
2005
+ if (opts?.onWorkerChunk) {
2006
+ opts.onWorkerChunk({
2007
+ type: opts?.isOrchestrated ? 'worker_chunk' : 'chunk',
2008
+ stepId: step.id,
2009
+ agentName: step.agentName,
2010
+ description: step.description,
2011
+ stepIndex: workflowContext?.stepIndex ?? 0,
2012
+ totalSteps: workflowContext?.totalSteps ?? 1,
2013
+ text: chunk,
2014
+ });
2015
+ }
2016
+ const now = Date.now();
2017
+ if (now - lastChunkProgressAt < 8000)
2018
+ return;
2019
+ lastChunkProgressAt = now;
2020
+ const activeSteps = workflowContext?.workflowId
2021
+ ? this.getActiveWorkflows().find((w) => w.id === workflowContext.workflowId)?.steps || [step]
2022
+ : [step];
2023
+ this.emitProgress(workflowContext?.workflowId || 'workflow', step, activeSteps, 'step-progress', opts?.onProgress, `${step.agentName} is still working on ${step.description}`);
2024
+ },
2025
+ // Forward Codex app-server tool/file events as worker_terminal_chunk-style detail lines.
2026
+ // This populates the worker child card details panel for Codex workers.
2027
+ onDetail: async (detail) => {
2028
+ if (!detail || !opts?.onWorkerChunk)
2029
+ return;
2030
+ opts.onWorkerChunk({
2031
+ type: 'worker_terminal_chunk',
2032
+ stepId: step.id,
2033
+ agentName: step.agentName,
2034
+ description: step.description,
2035
+ stepIndex: workflowContext?.stepIndex ?? 0,
2036
+ totalSteps: workflowContext?.totalSteps ?? 1,
2037
+ rawText: detail,
2038
+ });
2039
+ },
2040
+ // Forward structured tool call/result events from Codex app-server
2041
+ // (command_execution, file_change, mcpToolCall items) as
2042
+ // worker_tool_call / worker_tool_result events. This gives Codex
2043
+ // workers the same structured tool visibility as API-key workers.
2044
+ onToolEvent: async (event) => {
2045
+ if (!opts?.onWorkerChunk)
2046
+ return;
2047
+ if (event.kind === 'call') {
2048
+ opts.onWorkerChunk({
2049
+ type: opts?.isOrchestrated ? 'worker_tool_call' : 'tool_call',
2050
+ stepId: step.id,
2051
+ agentName: step.agentName,
2052
+ description: step.description,
2053
+ stepIndex: workflowContext?.stepIndex ?? 0,
2054
+ totalSteps: workflowContext?.totalSteps ?? 1,
2055
+ toolCallId: event.toolCallId,
2056
+ toolName: event.toolName,
2057
+ toolArguments: event.arguments,
2058
+ });
2059
+ }
2060
+ else {
2061
+ opts.onWorkerChunk({
2062
+ type: opts?.isOrchestrated ? 'worker_tool_result' : 'tool_result',
2063
+ stepId: step.id,
2064
+ agentName: step.agentName,
2065
+ description: step.description,
2066
+ stepIndex: workflowContext?.stepIndex ?? 0,
2067
+ totalSteps: workflowContext?.totalSteps ?? 1,
2068
+ toolCallId: event.toolCallId,
2069
+ toolName: event.toolName,
2070
+ toolOutput: event.output,
2071
+ toolIsError: event.isError,
2072
+ });
2073
+ }
2074
+ },
2075
+ });
2076
+ if (appServerResult.sessionId) {
2077
+ activeCliSessionId = appServerResult.sessionId;
2078
+ }
2079
+ console.info(`[workflow-engine] codex_app_server_turn_done workflow=${workflowContext?.workflowId || '(none)'} step=${workflowContext?.stepIndex ?? 0} agent=${step.agentName} conversationId=${ptyConversationKey} taskId=${opts?.taskId ?? '(none)'} contentChars=${String(appServerResult.content || '').length}`);
2080
+ response = {
2081
+ content: appServerResult.content || '',
2082
+ usage: appServerResult.usage,
2083
+ };
2084
+ break;
2085
+ }
2086
+ catch (appServerErr) {
2087
+ console.error(`[workflow-engine] codex_app_server_turn_error workflow=${workflowContext?.workflowId || '(none)'} step=${workflowContext?.stepIndex ?? 0} agent=${step.agentName} conversationId=${ptyConversationKey} taskId=${opts?.taskId ?? '(none)'} attempt=${appServerAttempt}: ${appServerErr?.stack || appServerErr?.message || String(appServerErr)}`);
2088
+ if (!isWorkflowAbortError(appServerErr)) {
2089
+ clearFailedWorkflowCliSessionEpoch(conversationId, step.agentId, activeCliSessionId || cliSessionEpochPlan.resumeSessionId, cliSessionScopeKey);
2090
+ codexAppServerManager.closeSessionByConversation(ptyConversationKey, step.agentId);
2091
+ }
2092
+ if (appServerAttempt >= LOCAL_WORKFLOW_RUNTIME_RETRY_LIMIT
2093
+ || !shouldRetrySelectedWorkflowRuntime(appServerErr)) {
2094
+ throw appServerErr;
2095
+ }
2096
+ if (cliSessionEpochPlan.resumeSessionId) {
2097
+ applyFreshCliBootstrapFallback('resume_failed');
2098
+ }
2099
+ console.warn(`[workflow-engine] ${step.agentName} selected runtime failed, retrying with a fresh session (${appServerAttempt + 1}/${LOCAL_WORKFLOW_RUNTIME_RETRY_LIMIT})`);
2100
+ await pauseWorkflowRuntimeRetry(appServerAttempt, opts?.abortSignal);
2101
+ }
2102
+ }
2103
+ }
2104
+ else if (workflowCliSessionTransport === 'pty') {
2105
+ if (activeRuntime.providerName !== 'claude-cli') {
2106
+ throw new Error(`Legacy PTY workflow sessions are Claude-only in local desktop mode; expected ${activeRuntime.providerName} to use Codex app-server.`);
1483
2107
  }
1484
2108
  const ptyManager = (0, local_cli_pty_manager_1.getLocalCliPtySessionManager)();
1485
2109
  let ptyAttempt = 0;
2110
+ let forceFreshInteractiveCliSession = false;
2111
+ let currentAttemptSessionId = null;
2112
+ let currentAttemptWasFreshSession = false;
1486
2113
  while (true) {
2114
+ throwIfWorkflowAborted(opts?.abortSignal);
1487
2115
  ptyAttempt++;
1488
2116
  try {
2117
+ const hasLivePtySession = ptyManager.hasActiveSession(ptyConversationKey, step.agentId);
2118
+ const isFreshSession = forceFreshInteractiveCliSession
2119
+ || !hasPersistentConversation
2120
+ || (iteration === 1 && !hasLivePtySession && !cliSessionEpochPlan.resumeSessionId);
2121
+ const newSessionId = activeRuntime.providerName === 'claude-cli' && isFreshSession
2122
+ ? data.generateNextSessionId()
2123
+ : undefined;
2124
+ currentAttemptWasFreshSession = isFreshSession;
2125
+ currentAttemptSessionId = newSessionId || cliSessionEpochPlan.resumeSessionId || activeCliSessionId || null;
1489
2126
  const ptyResult = await ptyManager.runTurn({
1490
2127
  conversationId: ptyConversationKey,
1491
2128
  botId: step.agentId,
1492
- provider: activeRuntime.providerName,
2129
+ provider: 'claude-cli',
2130
+ botSettings: {
2131
+ claude: {
2132
+ model: activeRuntime.model,
2133
+ effortLevel: profile?.claude_effort_level,
2134
+ outputStyle: profile?.claude_output_style,
2135
+ fastMode: profile?.claude_fast_mode === 1,
2136
+ permissionsJson: profile?.claude_permissions_json,
2137
+ },
2138
+ },
1493
2139
  cwd: effectiveProjectDir,
2140
+ toolActorId: step.agentName,
2141
+ toolProjectId: opts?.projectId ?? null,
2142
+ currentTodoTaskId: opts?.taskId,
2143
+ useConpty: shouldUseConptyForWorkflowClaudePty(),
1494
2144
  systemPrompt,
1495
2145
  messages,
1496
- forceFreshSession: !hasPersistentConversation || (iteration === 1 && !cliSessionEpochPlan.resumeSessionId),
2146
+ abortSignal: opts?.abortSignal,
2147
+ forceFreshSession: isFreshSession,
2148
+ resumeSessionId: isFreshSession ? undefined : (cliSessionEpochPlan.resumeSessionId || undefined),
2149
+ newSessionId,
2150
+ onRawChunk: async (chunk) => {
2151
+ if (opts?.onWorkerChunk) {
2152
+ opts.onWorkerChunk({
2153
+ type: 'worker_terminal_chunk',
2154
+ stepId: step.id,
2155
+ agentName: step.agentName,
2156
+ description: step.description,
2157
+ stepIndex: workflowContext?.stepIndex ?? 0,
2158
+ totalSteps: workflowContext?.totalSteps ?? 1,
2159
+ rawText: chunk,
2160
+ });
2161
+ }
2162
+ },
2163
+ // Forward Claude PTY tool_use detail markers as worker_terminal_chunk
2164
+ // detail lines so they appear in the worker child card details panel.
2165
+ // Also detect __ACTIVITY__ markers and emit structured worker_tool_call
2166
+ // events so PTY workers get the same tool visibility as API-key workers.
1497
2167
  onDetail: async (detail) => {
1498
- streamedContent += streamedContent ? `\n${detail}` : detail;
2168
+ if (!detail || !opts?.onWorkerChunk)
2169
+ return;
2170
+ // Always forward as terminal chunk for textual details panel.
2171
+ opts.onWorkerChunk({
2172
+ type: 'worker_terminal_chunk',
2173
+ stepId: step.id,
2174
+ agentName: step.agentName,
2175
+ description: step.description,
2176
+ stepIndex: workflowContext?.stepIndex ?? 0,
2177
+ totalSteps: workflowContext?.totalSteps ?? 1,
2178
+ rawText: detail,
2179
+ });
2180
+ // Detect __ACTIVITY__<JSON> markers from PTY parser and translate
2181
+ // tool activities into structured worker_tool_call / worker_tool_result events.
2182
+ if (detail.startsWith('__ACTIVITY__')) {
2183
+ try {
2184
+ const json = detail.slice('__ACTIVITY__'.length);
2185
+ const activity = JSON.parse(json);
2186
+ if (!activity || typeof activity !== 'object')
2187
+ return;
2188
+ if (activity.type === 'tool_call' && typeof activity.label === 'string') {
2189
+ // Every tool_use block from the Claude PTY parser
2190
+ opts.onWorkerChunk({
2191
+ type: opts?.isOrchestrated ? 'worker_tool_call' : 'tool_call',
2192
+ stepId: step.id,
2193
+ agentName: step.agentName,
2194
+ description: step.description,
2195
+ stepIndex: workflowContext?.stepIndex ?? 0,
2196
+ totalSteps: workflowContext?.totalSteps ?? 1,
2197
+ toolCallId: typeof activity.key === 'string' ? activity.key : `claude-tool-${Date.now()}`,
2198
+ toolName: activity.label,
2199
+ toolArguments: undefined,
2200
+ });
2201
+ }
2202
+ else if (activity.type === 'tool_result' && typeof activity.label === 'string') {
2203
+ // Result block from the Claude PTY parser
2204
+ opts.onWorkerChunk({
2205
+ type: opts?.isOrchestrated ? 'worker_tool_result' : 'tool_result',
2206
+ stepId: step.id,
2207
+ agentName: step.agentName,
2208
+ description: step.description,
2209
+ stepIndex: workflowContext?.stepIndex ?? 0,
2210
+ totalSteps: workflowContext?.totalSteps ?? 1,
2211
+ toolCallId: typeof activity.key === 'string' ? activity.key : `claude-tool-result-${Date.now()}`,
2212
+ toolName: 'tool',
2213
+ toolOutput: activity.label,
2214
+ toolIsError: !!activity.isError,
2215
+ });
2216
+ }
2217
+ else if (activity.type === 'working' && typeof activity.label === 'string') {
2218
+ // Legacy 'working' activities (sub-agent updates from child files)
2219
+ opts.onWorkerChunk({
2220
+ type: opts?.isOrchestrated ? 'worker_tool_call' : 'tool_call',
2221
+ stepId: step.id,
2222
+ agentName: step.agentName,
2223
+ description: step.description,
2224
+ stepIndex: workflowContext?.stepIndex ?? 0,
2225
+ totalSteps: workflowContext?.totalSteps ?? 1,
2226
+ toolCallId: typeof activity.key === 'string' ? activity.key : `claude-tool-${Date.now()}`,
2227
+ toolName: activity.label,
2228
+ toolArguments: undefined,
2229
+ });
2230
+ }
2231
+ else if (activity.type === 'subagent_started' && typeof activity.label === 'string') {
2232
+ opts.onWorkerChunk({
2233
+ type: opts?.isOrchestrated ? 'worker_tool_call' : 'tool_call',
2234
+ stepId: step.id,
2235
+ agentName: step.agentName,
2236
+ description: step.description,
2237
+ stepIndex: workflowContext?.stepIndex ?? 0,
2238
+ totalSteps: workflowContext?.totalSteps ?? 1,
2239
+ toolCallId: typeof activity.key === 'string' ? activity.key : `claude-agent-${Date.now()}`,
2240
+ toolName: 'Agent',
2241
+ toolArguments: { description: activity.label },
2242
+ });
2243
+ }
2244
+ }
2245
+ catch {
2246
+ // malformed __ACTIVITY__ marker — ignore
2247
+ }
2248
+ }
2249
+ },
2250
+ onChunk: async (chunk) => {
2251
+ streamedContent += chunk;
1499
2252
  if (opts?.onWorkerChunk) {
1500
2253
  opts.onWorkerChunk({
1501
2254
  type: opts?.isOrchestrated ? 'worker_chunk' : 'chunk',
@@ -1504,7 +2257,7 @@ class WorkflowEngine extends events_1.EventEmitter {
1504
2257
  description: step.description,
1505
2258
  stepIndex: workflowContext?.stepIndex ?? 0,
1506
2259
  totalSteps: workflowContext?.totalSteps ?? 1,
1507
- text: detail,
2260
+ text: chunk,
1508
2261
  });
1509
2262
  }
1510
2263
  const now = Date.now();
@@ -1527,12 +2280,41 @@ class WorkflowEngine extends events_1.EventEmitter {
1527
2280
  break;
1528
2281
  }
1529
2282
  catch (ptyErr) {
2283
+ if (isWorkflowAbortError(ptyErr)) {
2284
+ throw ptyErr;
2285
+ }
2286
+ const startupRetry = isClaudeFreshWorkflowSessionStartupFailure(ptyErr);
2287
+ if (startupRetry || currentAttemptWasFreshSession || cliSessionEpochPlan.resumeSessionId) {
2288
+ ptyManager.logSessionFailureByConversation(ptyConversationKey, step.agentId, startupRetry
2289
+ ? 'workflow_fresh_session_startup_failure_before_kill'
2290
+ : currentAttemptWasFreshSession
2291
+ ? 'workflow_fresh_session_failure_before_kill'
2292
+ : 'workflow_resume_session_failure_before_kill', ptyErr);
2293
+ }
2294
+ clearFailedWorkflowCliSessionEpoch(conversationId, step.agentId, currentAttemptSessionId || activeCliSessionId || cliSessionEpochPlan.resumeSessionId, cliSessionScopeKey);
2295
+ if (startupRetry || currentAttemptWasFreshSession) {
2296
+ forceFreshInteractiveCliSession = true;
2297
+ ptyManager.closeSessionByConversation(ptyConversationKey, step.agentId);
2298
+ }
2299
+ const resumeFailureFallback = !!cliSessionEpochPlan.resumeSessionId && !currentAttemptWasFreshSession;
2300
+ if (resumeFailureFallback) {
2301
+ forceFreshInteractiveCliSession = true;
2302
+ applyFreshCliBootstrapFallback('resume_failed');
2303
+ ptyManager.closeSessionByConversation(ptyConversationKey, step.agentId);
2304
+ }
1530
2305
  if (ptyAttempt >= LOCAL_WORKFLOW_RUNTIME_RETRY_LIMIT
1531
2306
  || !shouldRetrySelectedWorkflowRuntime(ptyErr)) {
1532
2307
  throw ptyErr;
1533
2308
  }
1534
- console.warn(`[workflow-engine] ${step.agentName} selected runtime failed, retrying the same connection (${ptyAttempt + 1}/${LOCAL_WORKFLOW_RUNTIME_RETRY_LIMIT})`);
1535
- await pauseWorkflowRuntimeRetry(ptyAttempt);
2309
+ const retryDetail = startupRetry
2310
+ ? `[workflow-engine] Fresh ${activeRuntime.providerName} session ${currentAttemptSessionId || '(unknown)'} did not finish startup before the transcript became available; killing it and retrying with a new session (${ptyAttempt + 1}/${LOCAL_WORKFLOW_RUNTIME_RETRY_LIMIT})`
2311
+ : resumeFailureFallback
2312
+ ? `[workflow-engine] Stored ${activeRuntime.providerName} session ${currentAttemptSessionId || '(unknown)'} failed to resume (${ptyErr?.message || ptyErr}); retrying with a fresh bootstrapped session (${ptyAttempt + 1}/${LOCAL_WORKFLOW_RUNTIME_RETRY_LIMIT})`
2313
+ : currentAttemptWasFreshSession
2314
+ ? `[workflow-engine] Fresh ${activeRuntime.providerName} session ${currentAttemptSessionId || '(unknown)'} failed (${ptyErr?.message || ptyErr}); killing it and retrying with a new session (${ptyAttempt + 1}/${LOCAL_WORKFLOW_RUNTIME_RETRY_LIMIT})`
2315
+ : `[workflow-engine] ${step.agentName} selected runtime failed (${ptyErr?.message || ptyErr}), retrying the same connection (${ptyAttempt + 1}/${LOCAL_WORKFLOW_RUNTIME_RETRY_LIMIT})`;
2316
+ console.warn(retryDetail);
2317
+ await pauseWorkflowRuntimeRetry(ptyAttempt, opts?.abortSignal);
1536
2318
  }
1537
2319
  }
1538
2320
  }
@@ -1579,6 +2361,7 @@ class WorkflowEngine extends events_1.EventEmitter {
1579
2361
  },
1580
2362
  tools: opts?.disableTools ? undefined : toolDefs,
1581
2363
  cwd: effectiveProjectDir,
2364
+ abortSignal: opts?.abortSignal,
1582
2365
  });
1583
2366
  // Flush remaining chunk buffer
1584
2367
  if (opts?.onWorkerChunk && chunkBuffer) {
@@ -1603,7 +2386,7 @@ class WorkflowEngine extends events_1.EventEmitter {
1603
2386
  && shouldRetrySelectedWorkflowRuntime(err)) {
1604
2387
  selectedRuntimeRetryCount++;
1605
2388
  console.warn(`[workflow-engine] ${step.agentName} selected runtime failed, retrying the same connection (${selectedRuntimeRetryCount + 1}/${LOCAL_WORKFLOW_RUNTIME_RETRY_LIMIT})`);
1606
- await pauseWorkflowRuntimeRetry(selectedRuntimeRetryCount);
2389
+ await pauseWorkflowRuntimeRetry(selectedRuntimeRetryCount, opts?.abortSignal);
1607
2390
  iteration--;
1608
2391
  continue;
1609
2392
  }
@@ -1631,29 +2414,43 @@ class WorkflowEngine extends events_1.EventEmitter {
1631
2414
  throw err;
1632
2415
  }
1633
2416
  if (hasPersistentConversation && activeCliSessionId && !response.toolCalls?.length) {
2417
+ if (cliSessionEpochPlan.resumeSessionId && !freshCliBootstrapFallbackApplied) {
2418
+ console.info(`[workflow-engine] cli_session_resume_succeeded conversationId=${conversationId} botId=${step.agentId} provider=${activeRuntime.providerName} sessionId=${activeCliSessionId}`);
2419
+ }
2420
+ const usedBootstrap = freshCliBootstrapFileWritten
2421
+ || (!cliSessionEpochPlan.resumeSessionId && !!promptContextWindow.allowBootstrap);
2422
+ const bootstrapReason = freshCliBootstrapFileWritten
2423
+ ? 'resume_failed'
2424
+ : (!cliSessionEpochPlan.resumeSessionId && !!promptContextWindow.allowBootstrap)
2425
+ ? 'fresh_with_context'
2426
+ : null;
2427
+ console.info(`[workflow-engine] cli_turn_telemetry conversationId=${conversationId || '(none)'} botId=${step.agentId} provider=${activeRuntime.providerName} selectedSessionId=${cliSessionEpochPlan.resumeSessionId || '(none)'} actualSessionId=${activeCliSessionId} promptContextMode=${promptContextWindow.mode || 'unknown'} usedBootstrap=${usedBootstrap} bootstrapReason=${bootstrapReason || '(none)'} resetReason=${cliEpochResetReason || '(none)'}`);
1634
2428
  const nextEpochTurnCount = cliSessionEpochPlan.resumeSessionId
1635
2429
  ? ((cliSessionEpochPlan.existing?.epoch_turn_count || 0) + 1)
1636
2430
  : 1;
1637
2431
  data.upsertCliSessionEpoch({
1638
2432
  conversationId: conversationId,
2433
+ sessionScopeKey: cliSessionScopeKey,
1639
2434
  botId: step.agentId,
1640
2435
  provider: activeRuntime.providerName,
1641
2436
  sessionId: activeCliSessionId,
1642
2437
  epochTurnCount: nextEpochTurnCount,
1643
2438
  lastInputTokens: response.usage?.inputTokens ?? null,
1644
2439
  lastOutputTokens: response.usage?.outputTokens ?? null,
1645
- resetReason: cliSessionEpochPlan.resetReason,
2440
+ resetReason: cliEpochResetReason,
1646
2441
  epochStartedAt: cliEpochStartedAt,
1647
2442
  lastUsedAt: (0, cli_session_epoch_1.localTimestamp)(),
1648
2443
  });
1649
2444
  }
1650
2445
  if (response.toolCalls && response.toolCalls.length > 0) {
2446
+ throwIfWorkflowAborted(opts?.abortSignal);
1651
2447
  messages.push({
1652
2448
  role: 'assistant',
1653
2449
  content: response.content || '',
1654
2450
  toolCalls: response.toolCalls,
1655
2451
  });
1656
2452
  for (const tc of response.toolCalls) {
2453
+ throwIfWorkflowAborted(opts?.abortSignal);
1657
2454
  const permMode = unrestrictedCliProvider
1658
2455
  ? 'autopilot'
1659
2456
  : (profile?.permission_mode || 'autopilot');
@@ -1677,6 +2474,7 @@ class WorkflowEngine extends events_1.EventEmitter {
1677
2474
  }
1678
2475
  const result = await (0, index_2.executeAndVerify)({ id: tc.id, name: tc.name, arguments: tc.arguments }, toolCtx);
1679
2476
  const output = result.success ? result.output : `ERROR: ${result.error || 'Unknown'}`;
2477
+ throwIfWorkflowAborted(opts?.abortSignal);
1680
2478
  messages.push({ role: 'tool', content: output, toolCallId: tc.id, toolName: tc.name });
1681
2479
  // Emit tool_result event
1682
2480
  if (opts?.onWorkerChunk) {
@@ -2130,15 +2928,38 @@ class WorkflowEngine extends events_1.EventEmitter {
2130
2928
  return count >= ERROR_LOOP_THRESHOLD;
2131
2929
  }
2132
2930
  isTerminalProviderFailure(errorMessage) {
2931
+ const normalized = String(errorMessage || '').toLowerCase();
2932
+ return Boolean(this.classifyProviderLimitFailure(errorMessage))
2933
+ || this.isProviderAuthFailure(errorMessage)
2934
+ || /\b(pty session|app-server|fresh session startup timed out|transcript file|no meaningful pty activity|session exited while waiting|failed to resume|session closed during active turn)\b/i.test(normalized);
2935
+ }
2936
+ isProviderAuthFailure(errorMessage) {
2133
2937
  const normalized = errorMessage.toLowerCase();
2134
- return normalized.includes('api error 429')
2938
+ return normalized.includes('refresh_token_reused')
2939
+ || normalized.includes('invalid_grant')
2940
+ || normalized.includes('oauth token refresh failed');
2941
+ }
2942
+ classifyProviderLimitFailure(errorMessage) {
2943
+ const exactMessage = String(errorMessage || '').trim();
2944
+ const normalized = exactMessage.toLowerCase();
2945
+ if (!exactMessage)
2946
+ return null;
2947
+ if (normalized.includes('rate limit')
2135
2948
  || normalized.includes('rate limited')
2949
+ || normalized.includes('api error 429')
2950
+ || normalized.includes('current session limit')) {
2951
+ return { reasonCode: 'provider_rate_limit', exactMessage };
2952
+ }
2953
+ if (normalized.includes("you've hit your org's monthly usage limit")
2954
+ || normalized.includes('monthly usage limit')
2955
+ || normalized.includes('usage limit reached')
2956
+ || normalized.includes('quota exceeded')
2136
2957
  || normalized.includes('exceeded your current quota')
2137
2958
  || normalized.includes('insufficient_quota')
2138
- || normalized.includes('billing details')
2139
- || normalized.includes('refresh_token_reused')
2140
- || normalized.includes('invalid_grant')
2141
- || normalized.includes('oauth token refresh failed');
2959
+ || normalized.includes('billing details')) {
2960
+ return { reasonCode: 'provider_quota_limit', exactMessage };
2961
+ }
2962
+ return null;
2142
2963
  }
2143
2964
  /**
2144
2965
  * Replan remaining work when retries are exhausted.