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
@@ -36,14 +36,25 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.UNASSIGNED_PROJECT_NAME = void 0;
37
37
  exports.getDb = getDb;
38
38
  exports.closeDb = closeDb;
39
+ exports.getAgentOrchestrationRoleClass = getAgentOrchestrationRoleClass;
40
+ exports.getAgentOrchestrationRoleLabel = getAgentOrchestrationRoleLabel;
41
+ exports.getAgentRolePriorities = getAgentRolePriorities;
42
+ exports.rolesMatch = rolesMatch;
39
43
  exports.createAgentProfile = createAgentProfile;
40
44
  exports.getAgentProfile = getAgentProfile;
41
45
  exports.getDefaultAgentProfile = getDefaultAgentProfile;
42
46
  exports.getOrchestratorBot = getOrchestratorBot;
47
+ exports.getCurrentOrchestratorSelection = getCurrentOrchestratorSelection;
48
+ exports.getResolvedClerkConfigInfo = getResolvedClerkConfigInfo;
43
49
  exports.listAgentProfiles = listAgentProfiles;
44
50
  exports.migrateAgentProfilesToDefaultToolProfile = migrateAgentProfilesToDefaultToolProfile;
45
51
  exports.updateAgentProfile = updateAgentProfile;
46
52
  exports.setOrchestratorBot = setOrchestratorBot;
53
+ exports.setDefaultAgentProfile = setDefaultAgentProfile;
54
+ exports.isClerkOrchestratorEnabled = isClerkOrchestratorEnabled;
55
+ exports.isOrchestrationToolDispatchEnabled = isOrchestrationToolDispatchEnabled;
56
+ exports.setOrchestrationToolDispatchEnabled = setOrchestrationToolDispatchEnabled;
57
+ exports.setClerkAsOrchestrator = setClerkAsOrchestrator;
47
58
  exports.createProviderConnection = createProviderConnection;
48
59
  exports.listProviderConnections = listProviderConnections;
49
60
  exports.getProviderConnection = getProviderConnection;
@@ -55,26 +66,47 @@ exports.upsertOauthProviderConnection = upsertOauthProviderConnection;
55
66
  exports.deleteProviderConnection = deleteProviderConnection;
56
67
  exports.deleteAgentProfile = deleteAgentProfile;
57
68
  exports.createConversation = createConversation;
69
+ exports.getConversationParticipantBotIds = getConversationParticipantBotIds;
70
+ exports.getConversationPrimaryBotId = getConversationPrimaryBotId;
58
71
  exports.getConversation = getConversation;
59
72
  exports.listConversations = listConversations;
60
73
  exports.countConversations = countConversations;
61
74
  exports.countMessagesForBot = countMessagesForBot;
75
+ exports.countMessagesForBotSince = countMessagesForBotSince;
76
+ exports.countDecisionsForBot = countDecisionsForBot;
77
+ exports.countBotActiveDays = countBotActiveDays;
78
+ exports.countBotConversationsUpdatedBetween = countBotConversationsUpdatedBetween;
79
+ exports.sumBotTokenUsageSince = sumBotTokenUsageSince;
62
80
  exports.listBotConversationActivity = listBotConversationActivity;
63
81
  exports.updateConversation = updateConversation;
82
+ exports.syncConversationParticipants = syncConversationParticipants;
64
83
  exports.touchConversationActivity = touchConversationActivity;
84
+ exports.bumpConversationSyncRevision = bumpConversationSyncRevision;
65
85
  exports.deleteConversation = deleteConversation;
86
+ exports.toWarmSessionBindingKey = toWarmSessionBindingKey;
87
+ exports.parseWarmSessionBindingKey = parseWarmSessionBindingKey;
88
+ exports.epochRowToWarmInfo = epochRowToWarmInfo;
66
89
  exports.addMessage = addMessage;
67
90
  exports.updateMessage = updateMessage;
68
91
  exports.createChatJob = createChatJob;
69
92
  exports.getCliSessionEpoch = getCliSessionEpoch;
70
93
  exports.upsertCliSessionEpoch = upsertCliSessionEpoch;
71
94
  exports.deleteCliSessionEpoch = deleteCliSessionEpoch;
95
+ exports.clearCliSessionEpochIfMatches = clearCliSessionEpochIfMatches;
96
+ exports.cleanupPoisonedClaudePrewarmSessionRows = cleanupPoisonedClaudePrewarmSessionRows;
97
+ exports.getCliSessionEpochsByConversation = getCliSessionEpochsByConversation;
98
+ exports.generateNextSessionId = generateNextSessionId;
72
99
  exports.getChatJob = getChatJob;
73
100
  exports.getLatestConversationChatJob = getLatestConversationChatJob;
101
+ exports.getLatestConversationBotChatJob = getLatestConversationBotChatJob;
74
102
  exports.listQueuedChatJobs = listQueuedChatJobs;
103
+ exports.listRunningChatJobs = listRunningChatJobs;
104
+ exports.listActiveAssistantMessageIds = listActiveAssistantMessageIds;
75
105
  exports.countRunningChatJobs = countRunningChatJobs;
76
106
  exports.updateChatJob = updateChatJob;
77
107
  exports.markRunningChatJobsInterrupted = markRunningChatJobsInterrupted;
108
+ exports.recoverInterruptedChatJobs = recoverInterruptedChatJobs;
109
+ exports.recoverInterruptedWorkflowRuns = recoverInterruptedWorkflowRuns;
78
110
  exports.createMessageActivity = createMessageActivity;
79
111
  exports.attachMessageActivitiesToMessage = attachMessageActivitiesToMessage;
80
112
  exports.listMessageActivities = listMessageActivities;
@@ -109,6 +141,16 @@ exports.updateImportJob = updateImportJob;
109
141
  exports.createImportJobItem = createImportJobItem;
110
142
  exports.getQueuedImportItems = getQueuedImportItems;
111
143
  exports.updateImportJobItem = updateImportJobItem;
144
+ exports.createImportedPlanRun = createImportedPlanRun;
145
+ exports.getImportedPlanRun = getImportedPlanRun;
146
+ exports.listImportedPlanRunsForConversation = listImportedPlanRunsForConversation;
147
+ exports.getLatestImportedPlanRunForConversation = getLatestImportedPlanRunForConversation;
148
+ exports.updateImportedPlanRun = updateImportedPlanRun;
149
+ exports.touchImportedPlanRun = touchImportedPlanRun;
150
+ exports.getImportedPlanRunTasks = getImportedPlanRunTasks;
151
+ exports.getImportedPlanRunRemainingTaskIds = getImportedPlanRunRemainingTaskIds;
152
+ exports.getImportedPlanRunCurrentTask = getImportedPlanRunCurrentTask;
153
+ exports.deleteImportedPlanRunSelectedTasks = deleteImportedPlanRunSelectedTasks;
112
154
  exports.createOutboxEvent = createOutboxEvent;
113
155
  exports.getPendingOutboxEvents = getPendingOutboxEvents;
114
156
  exports.markOutboxEventSent = markOutboxEventSent;
@@ -148,6 +190,7 @@ exports.completeTodoTaskByUser = completeTodoTaskByUser;
148
190
  exports.listTodoArtifactsForTask = listTodoArtifactsForTask;
149
191
  exports.completeTodoTaskByWorker = completeTodoTaskByWorker;
150
192
  exports.blockTodoTaskByWorker = blockTodoTaskByWorker;
193
+ exports.markTodoTaskRuntimeBlocked = markTodoTaskRuntimeBlocked;
151
194
  exports.deleteTodoTask = deleteTodoTask;
152
195
  exports.reorderTodoTask = reorderTodoTask;
153
196
  exports.listTodoAudit = listTodoAudit;
@@ -155,6 +198,7 @@ exports.moveNullTodoTasksToUnassigned = moveNullTodoTasksToUnassigned;
155
198
  exports.getLatestActiveTodoForConversation = getLatestActiveTodoForConversation;
156
199
  exports.getNextActiveTodoForConversation = getNextActiveTodoForConversation;
157
200
  exports.listCompletedTodoTasksForConversation = listCompletedTodoTasksForConversation;
201
+ exports.listActiveTodoTasksForConversation = listActiveTodoTasksForConversation;
158
202
  exports.createProject = createProject;
159
203
  exports.upsertProject = upsertProject;
160
204
  exports.getProject = getProject;
@@ -229,6 +273,7 @@ const crypto = __importStar(require("crypto"));
229
273
  const default_tool_profile_1 = require("./default-tool-profile");
230
274
  const approval_1 = require("./approval");
231
275
  let _db = null;
276
+ const CLERK_IS_ORCHESTRATOR_KEY = 'clerk_is_orchestrator';
232
277
  /** Get or open the shared database connection */
233
278
  function getDb() {
234
279
  if (!_db) {
@@ -249,24 +294,121 @@ function newId() {
249
294
  function now() {
250
295
  return new Date().toISOString().replace('T', ' ').replace('Z', '');
251
296
  }
297
+ function defaultOrchestrationIncludeUserPrompt(_fields) {
298
+ // Every worker role gets the original user prompt by default.
299
+ // april19fixes.txt item 3 fix (IV): previously only verify/verification
300
+ // workers received it, which meant Ben/John/Brain flew blind on a
301
+ // generic "Complete X's workflow step" task whenever the handoff
302
+ // lacked detail. The orchestrator's task_instructions is still the
303
+ // primary context source; this default just ensures the original
304
+ // user message is always available as a fallback reference.
305
+ return true;
306
+ }
307
+ function getAgentOrchestrationRoleClass(agent) {
308
+ const roleClass = String(agent?.orchestration_role_class
309
+ || agent?.orchestration_role_label
310
+ || agent?.role_class
311
+ || agent?.role_label
312
+ || '').trim();
313
+ return roleClass || null;
314
+ }
315
+ function getAgentOrchestrationRoleLabel(agent) {
316
+ const roleLabel = String(agent?.orchestration_role_label
317
+ || agent?.orchestration_role_class
318
+ || agent?.role_label
319
+ || agent?.role_class
320
+ || '').trim();
321
+ return roleLabel || null;
322
+ }
323
+ /**
324
+ * Read a bot's role priority list, dual-reading new and old columns.
325
+ * Used everywhere role-based dispatch or display is needed.
326
+ *
327
+ * Reads `orchestration_roles_json` first (authoritative). If empty/missing,
328
+ * falls back to parsing `orchestration_role_class` (comma-separated, left-
329
+ * to-right priority). Returns empty array if neither source has content.
330
+ *
331
+ * Normalization (per Decision 2): trim each entry, drop empties. Does NOT
332
+ * lower-case or canonicalize the role text — comparisons are the caller's
333
+ * responsibility.
334
+ */
335
+ function getAgentRolePriorities(row) {
336
+ const raw = (row.orchestration_roles_json || '').trim();
337
+ if (raw) {
338
+ try {
339
+ const parsed = JSON.parse(raw);
340
+ if (Array.isArray(parsed)) {
341
+ return parsed.map((entry) => String(entry || '').trim()).filter(Boolean);
342
+ }
343
+ }
344
+ catch {
345
+ // fall through to legacy parse
346
+ }
347
+ }
348
+ const legacy = (row.orchestration_role_class || '').trim();
349
+ if (!legacy)
350
+ return [];
351
+ return legacy.split(',').map((s) => s.trim()).filter(Boolean);
352
+ }
353
+ /**
354
+ * Compare two role strings for equality per Decision 2 matching rules:
355
+ * case-insensitive + whitespace-normalized. NO alias table.
356
+ * "QA" == "qa" == " QA " == "qa ". "QA" != "Quality Assurance".
357
+ */
358
+ function rolesMatch(a, b) {
359
+ const na = String(a || '').trim().replace(/\s+/g, ' ').toLowerCase();
360
+ const nb = String(b || '').trim().replace(/\s+/g, ' ').toLowerCase();
361
+ if (!na || !nb)
362
+ return false;
363
+ return na === nb;
364
+ }
365
+ const VALID_CODEX_REASONING_SUMMARIES = new Set(['auto', 'concise', 'detailed', 'none']);
366
+ const VALID_CLAUDE_OUTPUT_STYLES = new Set(['default', 'explanatory']);
367
+ function normalizeCodexReasoningSummary(value) {
368
+ const normalized = String(value || '').trim().toLowerCase();
369
+ return VALID_CODEX_REASONING_SUMMARIES.has(normalized) ? normalized : 'detailed';
370
+ }
371
+ function normalizeClaudeOutputStyle(value) {
372
+ const normalized = String(value || '').trim().toLowerCase();
373
+ return VALID_CLAUDE_OUTPUT_STYLES.has(normalized) ? normalized : 'default';
374
+ }
252
375
  function createAgentProfile(p) {
253
376
  const db = getDb();
254
- const id = newId();
377
+ const id = p.id ?? newId();
255
378
  const ts = now();
256
379
  const insert = db.prepare(`
257
380
  INSERT INTO agent_profile (
258
381
  id, provider, model, name, soul_md, memory_md, tools_md, skills_md, api_key_enc,
259
- permission_mode, is_default, role_label, role_class, is_active, priority,
382
+ permission_mode, is_default, role_label, role_class, orchestration_role_label, orchestration_role_class,
383
+ orchestration_include_user_prompt, is_active, priority,
260
384
  provider_connection_id, color, purpose_md, identity_summary, final_prompt,
261
- enabled_builtin_tools_json, enabled_mcp_tools_json, show_thinking, is_orchestrator, created_at, updated_at
385
+ enabled_builtin_tools_json, enabled_mcp_tools_json, show_thinking, is_orchestrator,
386
+ codex_reasoning_effort, codex_reasoning_summary, codex_personality,
387
+ codex_service_tier, codex_sandbox_policy, codex_approval_policy,
388
+ claude_effort_level, claude_output_style, claude_fast_mode, claude_permissions_json,
389
+ orchestration_roles_json,
390
+ created_at, updated_at
262
391
  )
263
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?, ?)
392
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
264
393
  `);
265
394
  const transaction = db.transaction(() => {
395
+ if (p.isDefault) {
396
+ db.prepare("UPDATE agent_profile SET is_default = 0, updated_at = datetime('now') WHERE is_default = 1").run();
397
+ }
266
398
  if (p.isOrchestrator) {
267
399
  db.prepare("UPDATE agent_profile SET is_orchestrator = 0, updated_at = datetime('now') WHERE is_orchestrator = 1").run();
400
+ setSetting(CLERK_IS_ORCHESTRATOR_KEY, 'false');
268
401
  }
269
- insert.run(id, p.provider, p.model, p.name, p.soulMd ?? null, p.memoryMd ?? null, p.toolsMd ?? null, p.skillsMd ?? null, p.apiKeyEnc ?? null, (0, approval_1.normalizePermissionMode)(p.permissionMode ?? default_tool_profile_1.DEFAULT_PERMISSION_MODE), p.isDefault ? 1 : 0, p.roleLabel ?? null, p.roleClass ?? null, p.isActive !== false ? 1 : 0, p.priority ?? 100, p.providerConnectionId ?? null, p.color ?? null, p.purposeMd ?? null, p.identitySummary ?? null, p.finalPrompt ?? null, p.enabledBuiltinToolsJson ?? null, p.enabledMcpToolsJson ?? null, p.isOrchestrator ? 1 : 0, ts, ts);
402
+ // Dual-write: if caller provided a role priority list, also derive the
403
+ // legacy comma-separated `orchestration_role_class` value for compat.
404
+ // Priority 0 item becomes the primary for downstream code that still
405
+ // reads the old column directly.
406
+ const rolesList = Array.isArray(p.orchestrationRolesJson)
407
+ ? p.orchestrationRolesJson.map((s) => String(s || '').trim()).filter(Boolean)
408
+ : null;
409
+ const rolesJsonStored = rolesList && rolesList.length > 0 ? JSON.stringify(rolesList) : null;
410
+ const derivedRoleClass = rolesList && rolesList.length > 0 ? rolesList.join(', ') : null;
411
+ insert.run(id, p.provider, p.model, p.name, p.soulMd ?? null, p.memoryMd ?? null, p.toolsMd ?? null, p.skillsMd ?? null, p.apiKeyEnc ?? null, (0, approval_1.normalizePermissionMode)(p.permissionMode ?? default_tool_profile_1.DEFAULT_PERMISSION_MODE), p.isDefault ? 1 : 0, p.roleLabel ?? null, p.roleClass ?? null, p.orchestrationRoleLabel ?? p.roleLabel ?? null, p.orchestrationRoleClass ?? derivedRoleClass ?? p.roleClass ?? null, (p.orchestrationIncludeUserPrompt ?? defaultOrchestrationIncludeUserPrompt(p)) ? 1 : 0, p.isActive !== false ? 1 : 0, p.priority ?? 100, p.providerConnectionId ?? null, p.color ?? null, p.purposeMd ?? null, p.identitySummary ?? null, p.finalPrompt ?? null, p.enabledBuiltinToolsJson ?? null, p.enabledMcpToolsJson ?? null, 0, p.isOrchestrator ? 1 : 0, p.codexReasoningEffort ?? 'low', normalizeCodexReasoningSummary(p.codexReasoningSummary), p.codexPersonality ?? 'friendly', p.codexServiceTier ?? 'fast', p.codexSandboxPolicy ?? 'danger-full-access', p.codexApprovalPolicy ?? 'never', p.claudeEffortLevel ?? 'auto', normalizeClaudeOutputStyle(p.claudeOutputStyle), p.claudeFastMode ? 1 : 0, p.claudePermissionsJson ?? null, rolesJsonStored, ts, ts);
270
412
  });
271
413
  transaction();
272
414
  return db.prepare('SELECT * FROM agent_profile WHERE id = ?').get(id);
@@ -280,6 +422,47 @@ function getDefaultAgentProfile() {
280
422
  function getOrchestratorBot() {
281
423
  return getDb().prepare('SELECT * FROM agent_profile WHERE is_orchestrator = 1 ORDER BY updated_at DESC LIMIT 1').get();
282
424
  }
425
+ function getCurrentOrchestratorSelection() {
426
+ if (isClerkOrchestratorEnabled()) {
427
+ const clerk = getResolvedClerkConfigInfo();
428
+ return {
429
+ type: 'clerk',
430
+ provider: clerk.provider,
431
+ model: clerk.model,
432
+ };
433
+ }
434
+ const bot = getOrchestratorBot();
435
+ if (bot) {
436
+ return { type: 'bot', botId: bot.id, botName: bot.name };
437
+ }
438
+ return { type: 'none' };
439
+ }
440
+ function getResolvedClerkConfigInfo() {
441
+ const clerkConfig = getJsonSetting('clerk.config');
442
+ const providerConnectionId = clerkConfig?.providerConnectionId
443
+ ? String(clerkConfig.providerConnectionId).trim()
444
+ : '';
445
+ const connection = providerConnectionId ? getProviderConnection(providerConnectionId) : undefined;
446
+ const provider = String(connection?.provider_id
447
+ || clerkConfig?.providerId
448
+ || getSetting('clerk_provider')
449
+ || '').trim() || null;
450
+ const model = String(clerkConfig?.model
451
+ || connection?.default_model
452
+ || getSetting('clerk_model')
453
+ || '').trim() || null;
454
+ const hasSecret = Boolean(connection?.access_mode === 'cli'
455
+ || connection?.api_key_enc
456
+ || connection?.oauth_token
457
+ || getSetting('clerk_api_key'));
458
+ return {
459
+ provider,
460
+ model,
461
+ hasSecret,
462
+ configured: Boolean(provider && model && hasSecret),
463
+ providerConnectionId: providerConnectionId || null,
464
+ };
465
+ }
283
466
  function listAgentProfiles() {
284
467
  return getDb().prepare('SELECT * FROM agent_profile ORDER BY is_default DESC, created_at ASC').all();
285
468
  }
@@ -379,6 +562,18 @@ function updateAgentProfile(id, fields) {
379
562
  sets.push('role_class = ?');
380
563
  vals.push(fields.roleClass);
381
564
  }
565
+ if (fields.orchestrationRoleLabel !== undefined) {
566
+ sets.push('orchestration_role_label = ?');
567
+ vals.push(fields.orchestrationRoleLabel);
568
+ }
569
+ if (fields.orchestrationRoleClass !== undefined) {
570
+ sets.push('orchestration_role_class = ?');
571
+ vals.push(fields.orchestrationRoleClass);
572
+ }
573
+ if (fields.orchestrationIncludeUserPrompt !== undefined) {
574
+ sets.push('orchestration_include_user_prompt = ?');
575
+ vals.push(fields.orchestrationIncludeUserPrompt ? 1 : 0);
576
+ }
382
577
  if (fields.isActive !== undefined) {
383
578
  sets.push('is_active = ?');
384
579
  vals.push(fields.isActive ? 1 : 0);
@@ -423,11 +618,72 @@ function updateAgentProfile(id, fields) {
423
618
  sets.push('is_orchestrator = ?');
424
619
  vals.push(fields.isOrchestrator ? 1 : 0);
425
620
  }
621
+ if (fields.codexReasoningEffort !== undefined) {
622
+ sets.push('codex_reasoning_effort = ?');
623
+ vals.push(fields.codexReasoningEffort);
624
+ }
625
+ if (fields.codexReasoningSummary !== undefined) {
626
+ sets.push('codex_reasoning_summary = ?');
627
+ vals.push(normalizeCodexReasoningSummary(fields.codexReasoningSummary));
628
+ }
629
+ if (fields.codexPersonality !== undefined) {
630
+ sets.push('codex_personality = ?');
631
+ vals.push(fields.codexPersonality);
632
+ }
633
+ if (fields.codexServiceTier !== undefined) {
634
+ sets.push('codex_service_tier = ?');
635
+ vals.push(fields.codexServiceTier);
636
+ }
637
+ if (fields.codexSandboxPolicy !== undefined) {
638
+ sets.push('codex_sandbox_policy = ?');
639
+ vals.push(fields.codexSandboxPolicy);
640
+ }
641
+ if (fields.codexApprovalPolicy !== undefined) {
642
+ sets.push('codex_approval_policy = ?');
643
+ vals.push(fields.codexApprovalPolicy);
644
+ }
645
+ if (fields.claudeEffortLevel !== undefined) {
646
+ sets.push('claude_effort_level = ?');
647
+ vals.push(fields.claudeEffortLevel);
648
+ }
649
+ if (fields.claudeOutputStyle !== undefined) {
650
+ sets.push('claude_output_style = ?');
651
+ vals.push(normalizeClaudeOutputStyle(fields.claudeOutputStyle));
652
+ }
653
+ if (fields.claudeFastMode !== undefined) {
654
+ sets.push('claude_fast_mode = ?');
655
+ vals.push(fields.claudeFastMode ? 1 : 0);
656
+ }
657
+ if (fields.claudePermissionsJson !== undefined) {
658
+ sets.push('claude_permissions_json = ?');
659
+ vals.push(fields.claudePermissionsJson);
660
+ }
661
+ if (fields.orchestrationRolesJson !== undefined) {
662
+ // Dual-write: update both the new JSON column and the legacy comma-separated
663
+ // orchestration_role_class so any code still reading the old column stays
664
+ // consistent until Phase B drops it.
665
+ const rolesList = Array.isArray(fields.orchestrationRolesJson)
666
+ ? fields.orchestrationRolesJson.map((s) => String(s || '').trim()).filter(Boolean)
667
+ : null;
668
+ const rolesJsonStored = rolesList && rolesList.length > 0 ? JSON.stringify(rolesList) : null;
669
+ const derivedRoleClass = rolesList && rolesList.length > 0 ? rolesList.join(', ') : null;
670
+ sets.push('orchestration_roles_json = ?');
671
+ vals.push(rolesJsonStored);
672
+ // Only override orchestration_role_class if the caller didn't explicitly pass one this update.
673
+ if (fields.orchestrationRoleClass === undefined) {
674
+ sets.push('orchestration_role_class = ?');
675
+ vals.push(derivedRoleClass);
676
+ }
677
+ }
426
678
  if (sets.length === 0)
427
679
  return row;
428
680
  const transaction = db.transaction(() => {
681
+ if (fields.isDefault === true) {
682
+ db.prepare("UPDATE agent_profile SET is_default = 0, updated_at = datetime('now') WHERE is_default = 1 AND id <> ?").run(id);
683
+ }
429
684
  if (fields.isOrchestrator === true) {
430
685
  db.prepare("UPDATE agent_profile SET is_orchestrator = 0, updated_at = datetime('now') WHERE is_orchestrator = 1 AND id <> ?").run(id);
686
+ setSetting(CLERK_IS_ORCHESTRATOR_KEY, 'false');
431
687
  }
432
688
  sets.push("updated_at = datetime('now')");
433
689
  vals.push(id);
@@ -444,12 +700,57 @@ function setOrchestratorBot(botId) {
444
700
  const transaction = db.transaction(() => {
445
701
  db.prepare("UPDATE agent_profile SET is_orchestrator = 0, updated_at = datetime('now') WHERE is_orchestrator = 1").run();
446
702
  db.prepare("UPDATE agent_profile SET is_orchestrator = 1, updated_at = datetime('now') WHERE id = ?").run(botId);
703
+ setSetting(CLERK_IS_ORCHESTRATOR_KEY, 'false');
704
+ });
705
+ transaction();
706
+ return db.prepare('SELECT * FROM agent_profile WHERE id = ?').get(botId);
707
+ }
708
+ function setDefaultAgentProfile(botId) {
709
+ const db = getDb();
710
+ const row = db.prepare('SELECT * FROM agent_profile WHERE id = ?').get(botId);
711
+ if (!row)
712
+ return undefined;
713
+ const transaction = db.transaction(() => {
714
+ db.prepare("UPDATE agent_profile SET is_default = 0, updated_at = datetime('now') WHERE is_default = 1").run();
715
+ db.prepare("UPDATE agent_profile SET is_default = 1, updated_at = datetime('now') WHERE id = ?").run(botId);
447
716
  });
448
717
  transaction();
449
718
  return db.prepare('SELECT * FROM agent_profile WHERE id = ?').get(botId);
450
719
  }
720
+ function isClerkOrchestratorEnabled() {
721
+ return getSetting(CLERK_IS_ORCHESTRATOR_KEY) === 'true';
722
+ }
723
+ /**
724
+ * Phase B of orchestration-plan.txt: LLM-tool-dispatch is the only
725
+ * orchestration path in local_desktop mode. The feature flag introduced
726
+ * in Phase A has been retired — new path is always active, no user toggle.
727
+ * These helpers are kept for a brief deprecation window so any external
728
+ * callers (settings UI, HTTP endpoints) still compile while they are
729
+ * removed. Do not use in new code.
730
+ *
731
+ * @deprecated Phase B — tool dispatch is the only path.
732
+ */
733
+ function isOrchestrationToolDispatchEnabled() {
734
+ return true;
735
+ }
736
+ /**
737
+ * @deprecated Phase B — no-op. Tool dispatch is the only path.
738
+ */
739
+ function setOrchestrationToolDispatchEnabled(_enabled) {
740
+ // no-op after Phase B
741
+ }
742
+ function setClerkAsOrchestrator(enabled) {
743
+ const db = getDb();
744
+ const transaction = db.transaction(() => {
745
+ setSetting(CLERK_IS_ORCHESTRATOR_KEY, enabled ? 'true' : 'false');
746
+ if (enabled) {
747
+ db.prepare("UPDATE agent_profile SET is_orchestrator = 0, updated_at = datetime('now') WHERE is_orchestrator = 1").run();
748
+ }
749
+ });
750
+ transaction();
751
+ }
451
752
  const CLI_PROVIDER_IDS = new Set(['claude-cli', 'codex-cli']);
452
- const SUPPORTED_PROVIDER_ACCESS_MODES = new Set(['api', 'cli']);
753
+ const SUPPORTED_PROVIDER_ACCESS_MODES = new Set(['api', 'apiKey', 'cli']);
453
754
  function createProviderConnection(p) {
454
755
  const db = getDb();
455
756
  const id = newId();
@@ -650,12 +951,40 @@ function deleteAgentProfile(id) {
650
951
  tx();
651
952
  return true;
652
953
  }
954
+ function normalizeConversationBotIds(botIds, fallbackBotId) {
955
+ const next = new Set();
956
+ for (const botId of botIds) {
957
+ const trimmed = String(botId || '').trim();
958
+ if (trimmed)
959
+ next.add(trimmed);
960
+ }
961
+ const fallback = String(fallbackBotId || '').trim();
962
+ if (next.size === 0 && fallback)
963
+ next.add(fallback);
964
+ return Array.from(next);
965
+ }
966
+ function parseConversationBotIds(raw) {
967
+ if (!raw)
968
+ return [];
969
+ try {
970
+ const parsed = JSON.parse(raw);
971
+ if (!Array.isArray(parsed))
972
+ return [];
973
+ return normalizeConversationBotIds(parsed.map((value) => (typeof value === 'string' ? value : null)));
974
+ }
975
+ catch {
976
+ return [];
977
+ }
978
+ }
653
979
  function createConversation(agentId, title, source, opts) {
654
980
  const db = getDb();
655
981
  const id = newId();
656
982
  const ts = now();
657
983
  let projectId = opts?.projectId ?? null;
658
984
  let projectName = opts?.projectName ?? null;
985
+ const initialBotId = String(opts?.initialBotId || agentId || '').trim() || agentId;
986
+ const activeBotIds = normalizeConversationBotIds(opts?.botIds || [], initialBotId);
987
+ const activeBotIdsJson = JSON.stringify(activeBotIds);
659
988
  if (projectId) {
660
989
  const project = getProject(projectId);
661
990
  if (project) {
@@ -673,10 +1002,10 @@ function createConversation(agentId, title, source, opts) {
673
1002
  projectName = unassigned.name;
674
1003
  }
675
1004
  db.prepare(`
676
- INSERT INTO conversation (id, agent_id, title, source, project_id, project_name, created_at, updated_at)
677
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
678
- `).run(id, agentId, title ?? null, source ?? 'local', projectId, projectName, ts, ts);
679
- return db.prepare('SELECT * FROM conversation WHERE id = ?').get(id);
1005
+ INSERT INTO conversation (id, agent_id, initial_bot_id, active_bot_ids_json, title, source, project_id, project_name, sync_revision, created_at, updated_at)
1006
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?)
1007
+ `).run(id, agentId, initialBotId, activeBotIdsJson, title ?? null, source ?? 'local', projectId, projectName, ts, ts);
1008
+ return getConversation(id);
680
1009
  }
681
1010
  function latestConversationSummarySql(conversationAlias) {
682
1011
  return `(
@@ -770,14 +1099,42 @@ function conversationSelectSql(conversationAlias) {
770
1099
  function hydrateConversationRow(row) {
771
1100
  if (!row)
772
1101
  return undefined;
1102
+ const activeBotIds = parseConversationBotIds(row.active_bot_ids_json);
773
1103
  return {
774
1104
  ...row,
1105
+ initial_bot_id: row.initial_bot_id ?? null,
1106
+ active_bot_ids_json: row.active_bot_ids_json ?? null,
1107
+ active_bot_ids: activeBotIds,
775
1108
  title: row.resolved_title ?? row.title,
776
1109
  summary: row.resolved_summary ?? row.summary,
777
1110
  created_at: row.effective_created_at ?? row.created_at,
778
1111
  updated_at: row.effective_updated_at ?? row.updated_at,
779
1112
  };
780
1113
  }
1114
+ function getConversationParticipantBotIds(conversationId, conversation) {
1115
+ const conv = conversation || getConversation(conversationId);
1116
+ const fromConversation = normalizeConversationBotIds(conv?.active_bot_ids || []);
1117
+ const rows = getDb().prepare(`
1118
+ SELECT DISTINCT bot_id
1119
+ FROM message
1120
+ WHERE conversation_id = ?
1121
+ AND bot_id IS NOT NULL
1122
+ AND trim(bot_id) <> ''
1123
+ ORDER BY bot_id
1124
+ `).all(conversationId);
1125
+ return normalizeConversationBotIds([...fromConversation, ...rows.map((row) => row.bot_id)], conv?.initial_bot_id || conv?.agent_id || null);
1126
+ }
1127
+ function getConversationPrimaryBotId(conversationId, conversation) {
1128
+ const conv = conversation || getConversation(conversationId);
1129
+ const initialBotId = String(conv?.initial_bot_id || '').trim();
1130
+ if (initialBotId)
1131
+ return initialBotId;
1132
+ const participantIds = getConversationParticipantBotIds(conversationId, conv);
1133
+ if (participantIds.length > 0)
1134
+ return participantIds[0];
1135
+ const legacyBotId = String(conv?.agent_id || '').trim();
1136
+ return legacyBotId || null;
1137
+ }
781
1138
  function getConversation(id) {
782
1139
  return hydrateConversationRow(getDb().prepare(`${conversationSelectSql('c')} WHERE c.id = ?`).get(id));
783
1140
  }
@@ -790,15 +1147,18 @@ function listConversations(opts) {
790
1147
  const wheres = [];
791
1148
  if (opts?.agentId) {
792
1149
  wheres.push(`(
793
- c.agent_id = ?
1150
+ coalesce(c.active_bot_ids_json, '[]') LIKE ?
794
1151
  OR EXISTS (
795
1152
  SELECT 1
796
1153
  FROM message m
797
1154
  WHERE m.conversation_id = c.id
798
1155
  AND m.bot_id = ?
799
1156
  )
1157
+ OR c.initial_bot_id = ?
1158
+ OR c.agent_id = ?
800
1159
  )`);
801
- params.push(opts.agentId, opts.agentId);
1160
+ const botNeedle = `%\"${opts.agentId}\"%`;
1161
+ params.push(botNeedle, opts.agentId, opts.agentId, opts.agentId);
802
1162
  }
803
1163
  if (opts?.search) {
804
1164
  wheres.push(`(
@@ -828,14 +1188,16 @@ function countConversations(agentId) {
828
1188
  return db.prepare(`
829
1189
  SELECT count(DISTINCT c.id) as cnt
830
1190
  FROM conversation c
831
- WHERE c.agent_id = ?
1191
+ WHERE coalesce(c.active_bot_ids_json, '[]') LIKE ?
832
1192
  OR EXISTS (
833
1193
  SELECT 1
834
1194
  FROM message m
835
1195
  WHERE m.conversation_id = c.id
836
1196
  AND m.bot_id = ?
837
1197
  )
838
- `).get(agentId, agentId).cnt;
1198
+ OR c.initial_bot_id = ?
1199
+ OR c.agent_id = ?
1200
+ `).get(`%\"${agentId}\"%`, agentId, agentId, agentId).cnt;
839
1201
  }
840
1202
  return db.prepare('SELECT count(*) as cnt FROM conversation').get().cnt;
841
1203
  }
@@ -846,6 +1208,74 @@ function countMessagesForBot(botId) {
846
1208
  WHERE bot_id = ?
847
1209
  `).get(botId).cnt;
848
1210
  }
1211
+ function botConversationMembershipSql(alias) {
1212
+ return `
1213
+ (
1214
+ coalesce(${alias}.active_bot_ids_json, '[]') LIKE ?
1215
+ OR EXISTS (
1216
+ SELECT 1
1217
+ FROM message m
1218
+ WHERE m.conversation_id = ${alias}.id
1219
+ AND m.bot_id = ?
1220
+ )
1221
+ OR ${alias}.initial_bot_id = ?
1222
+ OR ${alias}.agent_id = ?
1223
+ )
1224
+ `;
1225
+ }
1226
+ function botConversationMembershipParams(botId) {
1227
+ return [`%\"${botId}\"%`, botId, botId, botId];
1228
+ }
1229
+ function countMessagesForBotSince(botId, since) {
1230
+ return getDb().prepare(`
1231
+ SELECT count(*) as cnt
1232
+ FROM message
1233
+ WHERE bot_id = ?
1234
+ AND datetime(created_at) >= datetime(?)
1235
+ `).get(botId, since).cnt;
1236
+ }
1237
+ function countDecisionsForBot(botId) {
1238
+ const membershipSql = botConversationMembershipSql('c');
1239
+ const params = botConversationMembershipParams(botId);
1240
+ return getDb().prepare(`
1241
+ SELECT count(*) as cnt
1242
+ FROM decision d
1243
+ JOIN conversation c ON c.id = d.conversation_id
1244
+ WHERE ${membershipSql}
1245
+ `).get(...params).cnt;
1246
+ }
1247
+ function countBotActiveDays(botId) {
1248
+ const membershipSql = botConversationMembershipSql('c');
1249
+ const params = botConversationMembershipParams(botId);
1250
+ return getDb().prepare(`
1251
+ SELECT count(DISTINCT date(c.updated_at)) as cnt
1252
+ FROM conversation c
1253
+ WHERE ${membershipSql}
1254
+ `).get(...params).cnt;
1255
+ }
1256
+ function countBotConversationsUpdatedBetween(botId, from, to) {
1257
+ const membershipSql = botConversationMembershipSql('c');
1258
+ const params = [...botConversationMembershipParams(botId), from];
1259
+ let sql = `
1260
+ SELECT count(*) as cnt
1261
+ FROM conversation c
1262
+ WHERE ${membershipSql}
1263
+ AND datetime(c.updated_at) >= datetime(?)
1264
+ `;
1265
+ if (to) {
1266
+ sql += ` AND datetime(c.updated_at) < datetime(?)`;
1267
+ params.push(to);
1268
+ }
1269
+ return getDb().prepare(sql).get(...params).cnt;
1270
+ }
1271
+ function sumBotTokenUsageSince(botId, since) {
1272
+ return Number(getDb().prepare(`
1273
+ SELECT coalesce(sum(token_count), 0) as total
1274
+ FROM message
1275
+ WHERE bot_id = ?
1276
+ AND datetime(created_at) >= datetime(?)
1277
+ `).get(botId, since).total ?? 0);
1278
+ }
849
1279
  function listBotConversationActivity(botId, limit = 10) {
850
1280
  const db = getDb();
851
1281
  return db.prepare(`
@@ -866,18 +1296,20 @@ function listBotConversationActivity(botId, limit = 10) {
866
1296
  FROM (
867
1297
  ${conversationSelectSql('c')}
868
1298
  WHERE (
869
- c.agent_id = ?
1299
+ coalesce(c.active_bot_ids_json, '[]') LIKE ?
870
1300
  OR EXISTS (
871
1301
  SELECT 1
872
1302
  FROM message m
873
1303
  WHERE m.conversation_id = c.id
874
1304
  AND m.bot_id = ?
875
1305
  )
1306
+ OR c.initial_bot_id = ?
1307
+ OR c.agent_id = ?
876
1308
  )
877
1309
  ) conv
878
1310
  ORDER BY COALESCE(bot_last_message_at, conv.updated_at, conv.created_at) DESC
879
1311
  LIMIT ?
880
- `).all(botId, botId, botId, botId, limit).map((row) => hydrateConversationRow(row));
1312
+ `).all(botId, botId, `%\"${botId}\"%`, botId, botId, botId, limit).map((row) => hydrateConversationRow(row));
881
1313
  }
882
1314
  function updateConversation(id, fields) {
883
1315
  const db = getDb();
@@ -919,6 +1351,8 @@ function updateConversation(id, fields) {
919
1351
  processingError: ['processing_error', fields.processingError],
920
1352
  botId: ['bot_id', fields.botId],
921
1353
  forkedFromId: ['forked_from_id', fields.forkedFromId],
1354
+ initialBotId: ['initial_bot_id', fields.initialBotId],
1355
+ activeBotIdsJson: ['active_bot_ids_json', fields.activeBotIdsJson],
922
1356
  createdAt: ['created_at', fields.createdAt],
923
1357
  updatedAt: ['updated_at', fields.updatedAt],
924
1358
  };
@@ -944,6 +1378,21 @@ function updateConversation(id, fields) {
944
1378
  vals.push(id);
945
1379
  db.prepare(`UPDATE conversation SET ${sets.join(', ')} WHERE id = ?`).run(...vals);
946
1380
  }
1381
+ function syncConversationParticipants(conversationId, botIds, opts) {
1382
+ const conversation = getConversation(conversationId);
1383
+ if (!conversation)
1384
+ return;
1385
+ const normalized = normalizeConversationBotIds(botIds, opts?.initialBotId || conversation.initial_bot_id || conversation.agent_id || null);
1386
+ if (normalized.length === 0)
1387
+ return;
1388
+ const current = normalizeConversationBotIds(conversation.active_bot_ids || [], conversation.initial_bot_id || conversation.agent_id || null);
1389
+ const nextIds = opts?.replace ? normalized : Array.from(new Set([...current, ...normalized]));
1390
+ const nextInitialBotId = String(opts?.initialBotId || conversation.initial_bot_id || conversation.agent_id || nextIds[0] || '').trim() || null;
1391
+ updateConversation(conversationId, {
1392
+ activeBotIdsJson: JSON.stringify(nextIds),
1393
+ initialBotId: nextInitialBotId || undefined,
1394
+ });
1395
+ }
947
1396
  function touchConversationActivity(id, ts) {
948
1397
  const stamp = ts?.trim() || now();
949
1398
  getDb().prepare(`
@@ -955,10 +1404,46 @@ function touchConversationActivity(id, ts) {
955
1404
  WHERE id = ?
956
1405
  `).run(stamp, stamp, id);
957
1406
  }
1407
+ function bumpConversationSyncRevision(id, ts) {
1408
+ const stamp = ts?.trim() || now();
1409
+ getDb().prepare(`
1410
+ UPDATE conversation
1411
+ SET sync_revision = coalesce(sync_revision, 0) + 1,
1412
+ updated_at = CASE
1413
+ WHEN datetime(updated_at) > datetime(?) THEN updated_at
1414
+ ELSE ?
1415
+ END
1416
+ WHERE id = ?
1417
+ `).run(stamp, stamp, id);
1418
+ const row = getDb().prepare('SELECT sync_revision FROM conversation WHERE id = ?').get(id);
1419
+ return Number(row?.sync_revision ?? 0);
1420
+ }
958
1421
  function deleteConversation(id) {
959
1422
  return getDb().prepare('DELETE FROM conversation WHERE id = ?').run(id).changes > 0;
960
1423
  }
961
- function addMessage(conversationId, role, content, model, toolCallsJson, botId, agentName, createdAt) {
1424
+ function toWarmSessionBindingKey(conversationId, botId) {
1425
+ return `${conversationId}::${botId}`;
1426
+ }
1427
+ function parseWarmSessionBindingKey(key) {
1428
+ const idx = key.indexOf('::');
1429
+ if (idx < 0)
1430
+ return null;
1431
+ return { conversationId: key.slice(0, idx), botId: key.slice(idx + 2) };
1432
+ }
1433
+ function epochRowToWarmInfo(row) {
1434
+ return {
1435
+ conversationId: row.conversation_id,
1436
+ sessionScopeKey: row.session_scope_key,
1437
+ botId: row.bot_id,
1438
+ provider: row.provider,
1439
+ sessionId: row.session_id,
1440
+ topicId: row.topic_id,
1441
+ epochTurnCount: row.epoch_turn_count,
1442
+ lastUsedAt: row.last_used_at,
1443
+ resetReason: row.reset_reason,
1444
+ };
1445
+ }
1446
+ function addMessage(conversationId, role, content, model, toolCallsJson, botId, agentName, createdAt, attachmentsJson) {
962
1447
  const db = getDb();
963
1448
  const id = newId();
964
1449
  const ts = createdAt?.trim() || now();
@@ -966,9 +1451,9 @@ function addMessage(conversationId, role, content, model, toolCallsJson, botId,
966
1451
  const maxSeq = db.prepare('SELECT COALESCE(MAX(seq), 0) as m FROM message WHERE conversation_id = ?').get(conversationId);
967
1452
  const seq = maxSeq.m + 1;
968
1453
  db.prepare(`
969
- INSERT INTO message (id, conversation_id, seq, role, content, model, tool_calls_json, bot_id, agent_name, created_at)
970
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
971
- `).run(id, conversationId, seq, role, content, model ?? null, toolCallsJson ?? null, botId ?? null, agentName ?? null, ts);
1454
+ INSERT INTO message (id, conversation_id, seq, role, content, attachments_json, model, tool_calls_json, bot_id, agent_name, created_at)
1455
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1456
+ `).run(id, conversationId, seq, role, content, attachmentsJson ?? null, model ?? null, toolCallsJson ?? null, botId ?? null, agentName ?? null, ts);
972
1457
  // Update conversation message_count and updated_at
973
1458
  db.prepare(`
974
1459
  UPDATE conversation
@@ -992,6 +1477,10 @@ function updateMessage(id, fields) {
992
1477
  sets.push('content = ?');
993
1478
  vals.push(fields.content);
994
1479
  }
1480
+ if (fields.attachmentsJson !== undefined) {
1481
+ sets.push('attachments_json = ?');
1482
+ vals.push(fields.attachmentsJson);
1483
+ }
995
1484
  if (fields.model !== undefined) {
996
1485
  sets.push('model = ?');
997
1486
  vals.push(fields.model);
@@ -1039,29 +1528,37 @@ function createChatJob(input) {
1039
1528
  `).run(id, input.conversationId, input.userMessageId, input.assistantMessageId, input.botId, input.status ?? 'queued', input.requestJson ?? null, ts, ts);
1040
1529
  return db.prepare('SELECT * FROM chat_job WHERE id = ?').get(id);
1041
1530
  }
1042
- function getCliSessionEpoch(conversationId, botId) {
1531
+ function normalizeCliSessionScopeKey(conversationId, sessionScopeKey) {
1532
+ const trimmed = String(sessionScopeKey || '').trim();
1533
+ return trimmed || conversationId;
1534
+ }
1535
+ function getCliSessionEpoch(conversationId, botId, sessionScopeKey) {
1536
+ const scopeKey = normalizeCliSessionScopeKey(conversationId, sessionScopeKey);
1043
1537
  return getDb().prepare(`
1044
1538
  SELECT *
1045
1539
  FROM cli_session_epoch
1046
- WHERE conversation_id = ? AND bot_id = ?
1540
+ WHERE session_scope_key = ? AND bot_id = ?
1047
1541
  LIMIT 1
1048
- `).get(conversationId, botId);
1542
+ `).get(scopeKey, botId);
1049
1543
  }
1050
1544
  function upsertCliSessionEpoch(input) {
1051
1545
  const db = getDb();
1052
- const existing = getCliSessionEpoch(input.conversationId, input.botId);
1546
+ const scopeKey = normalizeCliSessionScopeKey(input.conversationId, input.sessionScopeKey);
1547
+ const existing = getCliSessionEpoch(input.conversationId, input.botId, scopeKey);
1053
1548
  const id = existing?.id || newId();
1054
1549
  const epochStartedAt = input.epochStartedAt || existing?.epoch_started_at || now();
1055
1550
  const lastUsedAt = input.lastUsedAt || now();
1551
+ const topicId = input.topicId !== undefined ? (input.topicId || null) : (existing?.topic_id ?? null);
1056
1552
  db.prepare(`
1057
1553
  INSERT INTO cli_session_epoch (
1058
- id, conversation_id, bot_id, provider, session_id, epoch_turn_count,
1554
+ id, conversation_id, session_scope_key, bot_id, provider, session_id, topic_id, epoch_turn_count,
1059
1555
  last_input_tokens, last_output_tokens, epoch_started_at, last_used_at,
1060
1556
  reset_reason, created_at, updated_at
1061
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1062
- ON CONFLICT(conversation_id, bot_id) DO UPDATE SET
1557
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1558
+ ON CONFLICT(session_scope_key, bot_id) DO UPDATE SET
1063
1559
  provider = excluded.provider,
1064
1560
  session_id = excluded.session_id,
1561
+ topic_id = excluded.topic_id,
1065
1562
  epoch_turn_count = excluded.epoch_turn_count,
1066
1563
  last_input_tokens = excluded.last_input_tokens,
1067
1564
  last_output_tokens = excluded.last_output_tokens,
@@ -1069,14 +1566,54 @@ function upsertCliSessionEpoch(input) {
1069
1566
  last_used_at = excluded.last_used_at,
1070
1567
  reset_reason = excluded.reset_reason,
1071
1568
  updated_at = excluded.updated_at
1072
- `).run(id, input.conversationId, input.botId, input.provider, input.sessionId, input.epochTurnCount, input.lastInputTokens ?? null, input.lastOutputTokens ?? null, epochStartedAt, lastUsedAt, input.resetReason ?? null, existing?.created_at || now(), now());
1073
- return getCliSessionEpoch(input.conversationId, input.botId);
1569
+ `).run(id, input.conversationId, scopeKey, input.botId, input.provider, input.sessionId, topicId, input.epochTurnCount, input.lastInputTokens ?? null, input.lastOutputTokens ?? null, epochStartedAt, lastUsedAt, input.resetReason ?? null, existing?.created_at || now(), now());
1570
+ return getCliSessionEpoch(input.conversationId, input.botId, scopeKey);
1571
+ }
1572
+ function deleteCliSessionEpoch(conversationId, botId, sessionScopeKey) {
1573
+ const scopeKey = normalizeCliSessionScopeKey(conversationId, sessionScopeKey);
1574
+ return getDb().prepare(`
1575
+ DELETE FROM cli_session_epoch
1576
+ WHERE session_scope_key = ? AND bot_id = ?
1577
+ `).run(scopeKey, botId).changes > 0;
1578
+ }
1579
+ function clearCliSessionEpochIfMatches(conversationId, botId, sessionId, sessionScopeKey) {
1580
+ const normalizedSessionId = String(sessionId || '').trim();
1581
+ if (!normalizedSessionId)
1582
+ return false;
1583
+ const scopeKey = normalizeCliSessionScopeKey(conversationId, sessionScopeKey);
1584
+ return getDb().prepare(`
1585
+ DELETE FROM cli_session_epoch
1586
+ WHERE session_scope_key = ? AND bot_id = ? AND session_id = ?
1587
+ `).run(scopeKey, botId, normalizedSessionId).changes > 0;
1074
1588
  }
1075
- function deleteCliSessionEpoch(conversationId, botId) {
1589
+ function cleanupPoisonedClaudePrewarmSessionRows() {
1076
1590
  return getDb().prepare(`
1077
1591
  DELETE FROM cli_session_epoch
1078
- WHERE conversation_id = ? AND bot_id = ?
1079
- `).run(conversationId, botId).changes > 0;
1592
+ WHERE provider = 'claude-cli'
1593
+ AND epoch_turn_count = 0
1594
+ AND reset_reason = 'prewarm'
1595
+ AND session_id LIKE 'fff00000-%'
1596
+ `).run().changes;
1597
+ }
1598
+ function getCliSessionEpochsByConversation(conversationId) {
1599
+ return getDb().prepare(`
1600
+ SELECT *
1601
+ FROM cli_session_epoch
1602
+ WHERE conversation_id = ?
1603
+ ORDER BY last_used_at DESC
1604
+ `).all(conversationId);
1605
+ }
1606
+ /**
1607
+ * Generate the next sequential Funolio session ID.
1608
+ * Format: fff00000-0000-4000-8000-NNNNNNNNNNNN
1609
+ * Used by Pattern A providers (Claude CLI) that support --session-id.
1610
+ */
1611
+ function generateNextSessionId() {
1612
+ getDb().prepare('UPDATE funolio_session_counter SET last_value = last_value + 1 WHERE id = 1').run();
1613
+ const row = getDb().prepare('SELECT last_value FROM funolio_session_counter WHERE id = 1').get();
1614
+ const counter = row?.last_value ?? 1;
1615
+ const hex = counter.toString(16).padStart(12, '0');
1616
+ return `fff00000-0000-4000-8000-${hex}`;
1080
1617
  }
1081
1618
  function getChatJob(id) {
1082
1619
  return getDb().prepare('SELECT * FROM chat_job WHERE id = ?').get(id);
@@ -1090,6 +1627,16 @@ function getLatestConversationChatJob(conversationId) {
1090
1627
  LIMIT 1
1091
1628
  `).get(conversationId);
1092
1629
  }
1630
+ function getLatestConversationBotChatJob(conversationId, botId) {
1631
+ return getDb().prepare(`
1632
+ SELECT *
1633
+ FROM chat_job
1634
+ WHERE conversation_id = ?
1635
+ AND bot_id = ?
1636
+ ORDER BY datetime(created_at) DESC, rowid DESC
1637
+ LIMIT 1
1638
+ `).get(conversationId, botId);
1639
+ }
1093
1640
  function listQueuedChatJobs(limit = 50) {
1094
1641
  return getDb().prepare(`
1095
1642
  SELECT *
@@ -1099,6 +1646,25 @@ function listQueuedChatJobs(limit = 50) {
1099
1646
  LIMIT ?
1100
1647
  `).all(limit);
1101
1648
  }
1649
+ function listRunningChatJobs(limit = 50) {
1650
+ return getDb().prepare(`
1651
+ SELECT *
1652
+ FROM chat_job
1653
+ WHERE status = 'running'
1654
+ ORDER BY datetime(started_at) ASC, datetime(created_at) ASC, rowid ASC
1655
+ LIMIT ?
1656
+ `).all(limit);
1657
+ }
1658
+ function listActiveAssistantMessageIds(conversationId) {
1659
+ return getDb().prepare(`
1660
+ SELECT assistant_message_id
1661
+ FROM chat_job
1662
+ WHERE conversation_id = ?
1663
+ AND status IN ('queued', 'running')
1664
+ AND assistant_message_id IS NOT NULL
1665
+ AND trim(assistant_message_id) <> ''
1666
+ `).all(conversationId).map((row) => String(row.assistant_message_id || '')).filter(Boolean);
1667
+ }
1102
1668
  function countRunningChatJobs() {
1103
1669
  return getDb().prepare(`
1104
1670
  SELECT count(*) AS cnt
@@ -1161,6 +1727,119 @@ function markRunningChatJobsInterrupted(reason = 'Interrupted when app closed')
1161
1727
  `).run(reason, ts, ts);
1162
1728
  return result.changes;
1163
1729
  }
1730
+ function recoverInterruptedChatJobs() {
1731
+ const db = getDb();
1732
+ const ts = now();
1733
+ const INTERRUPTED_NOTE = '\n\n[Response interrupted - retry to continue]';
1734
+ const ERROR_MSG = 'Agent was restarted during processing';
1735
+ const orphanedJobs = db.prepare(`
1736
+ SELECT id, status, assistant_message_id
1737
+ FROM chat_job
1738
+ WHERE status IN ('running', 'pending')
1739
+ `).all();
1740
+ if (orphanedJobs.length === 0) {
1741
+ return { recoveredJobs: [], totalRecovered: 0 };
1742
+ }
1743
+ const updateJob = db.prepare(`
1744
+ UPDATE chat_job
1745
+ SET status = 'interrupted',
1746
+ error = ?,
1747
+ updated_at = ?
1748
+ WHERE id = ?
1749
+ `);
1750
+ const getMsg = db.prepare('SELECT id, content FROM message WHERE id = ?');
1751
+ const appendNote = db.prepare('UPDATE message SET content = content || ? WHERE id = ?');
1752
+ const recoveredJobs = [];
1753
+ const runAll = db.transaction(() => {
1754
+ for (const job of orphanedJobs) {
1755
+ updateJob.run(ERROR_MSG, ts, job.id);
1756
+ if (job.assistant_message_id) {
1757
+ const msg = getMsg.get(job.assistant_message_id);
1758
+ if (msg && msg.content && msg.content.trim().length > 0) {
1759
+ appendNote.run(INTERRUPTED_NOTE, msg.id);
1760
+ }
1761
+ }
1762
+ recoveredJobs.push({ jobId: job.id, previousStatus: job.status });
1763
+ }
1764
+ });
1765
+ runAll();
1766
+ return { recoveredJobs, totalRecovered: recoveredJobs.length };
1767
+ }
1768
+ function recoverInterruptedWorkflowRuns(reason = 'Agent was restarted during workflow execution') {
1769
+ const db = getDb();
1770
+ const ts = now();
1771
+ const runningWorkflowRows = db.prepare(`
1772
+ SELECT id, conversation_id, status
1773
+ FROM workflow_execution
1774
+ WHERE status = 'running'
1775
+ `).all();
1776
+ const runningImportedRuns = db.prepare(`
1777
+ SELECT *
1778
+ FROM imported_plan_run
1779
+ WHERE status = 'running'
1780
+ `).all();
1781
+ const updateSteps = db.prepare(`
1782
+ UPDATE workflow_step_execution
1783
+ SET status = 'failed',
1784
+ error = CASE
1785
+ WHEN error IS NULL OR trim(error) = '' THEN ?
1786
+ ELSE error
1787
+ END,
1788
+ completed_at = COALESCE(completed_at, ?)
1789
+ WHERE workflow_execution_id = ?
1790
+ AND status = 'running'
1791
+ `);
1792
+ const updateWorkflow = db.prepare(`
1793
+ UPDATE workflow_execution
1794
+ SET status = 'failed',
1795
+ error = CASE
1796
+ WHEN error IS NULL OR trim(error) = '' THEN ?
1797
+ ELSE error
1798
+ END,
1799
+ completed_at = COALESCE(completed_at, ?)
1800
+ WHERE id = ?
1801
+ AND status = 'running'
1802
+ `);
1803
+ const updateImportedRun = db.prepare(`
1804
+ UPDATE imported_plan_run
1805
+ SET status = ?,
1806
+ completed_at = CASE WHEN ? = 'completed' THEN COALESCE(completed_at, ?) ELSE completed_at END,
1807
+ last_error = ?,
1808
+ updated_at = datetime('now')
1809
+ WHERE id = ?
1810
+ `);
1811
+ const recoveredWorkflowExecutions = [];
1812
+ const recoveredImportedPlanRuns = [];
1813
+ const runAll = db.transaction(() => {
1814
+ for (const workflow of runningWorkflowRows) {
1815
+ updateSteps.run(reason, ts, workflow.id);
1816
+ updateWorkflow.run(reason, ts, workflow.id);
1817
+ recoveredWorkflowExecutions.push({
1818
+ workflowId: workflow.id,
1819
+ conversationId: workflow.conversation_id,
1820
+ previousStatus: workflow.status,
1821
+ });
1822
+ }
1823
+ for (const rawRun of runningImportedRuns) {
1824
+ const run = normalizeImportedPlanRunRow(rawRun);
1825
+ const remainingTaskCount = getImportedPlanRunRemainingTaskIds(run.id).length;
1826
+ const nextStatus = remainingTaskCount > 0 ? 'paused' : 'completed';
1827
+ updateImportedRun.run(nextStatus, nextStatus, ts, remainingTaskCount > 0 ? reason : null, run.id);
1828
+ recoveredImportedPlanRuns.push({
1829
+ runId: run.id,
1830
+ conversationId: run.conversation_id,
1831
+ previousStatus: run.status,
1832
+ remainingTaskCount,
1833
+ });
1834
+ }
1835
+ });
1836
+ runAll();
1837
+ return {
1838
+ recoveredWorkflowExecutions,
1839
+ recoveredImportedPlanRuns,
1840
+ totalRecovered: recoveredWorkflowExecutions.length + recoveredImportedPlanRuns.length,
1841
+ };
1842
+ }
1164
1843
  function createMessageActivity(activity) {
1165
1844
  const db = getDb();
1166
1845
  const id = newId();
@@ -1739,6 +2418,274 @@ function updateImportJobItem(id, fields) {
1739
2418
  vals.push(id);
1740
2419
  db.prepare(`UPDATE import_job_item SET ${sets.join(', ')} WHERE id = ?`).run(...vals);
1741
2420
  }
2421
+ function getImportedPlanFirstKnownOrder(run) {
2422
+ const knownTasks = run.task_ids
2423
+ .map((taskId) => getTodoTask(taskId, 'active') || getTodoTask(taskId, 'completed'))
2424
+ .filter((task) => !!task);
2425
+ if (knownTasks.length === 0)
2426
+ return null;
2427
+ return Math.min(...knownTasks.map((task) => task.created_order));
2428
+ }
2429
+ function doesTodoBelongToImportedPlanRun(run, task) {
2430
+ if (!task.conversation_id || task.conversation_id !== run.conversation_id)
2431
+ return false;
2432
+ if (run.task_ids.includes(task.id))
2433
+ return true;
2434
+ const firstImportedOrder = getImportedPlanFirstKnownOrder(run);
2435
+ if (firstImportedOrder == null)
2436
+ return false;
2437
+ return task.created_order >= firstImportedOrder;
2438
+ }
2439
+ function touchImportedPlanRunForTask(task) {
2440
+ if (!task.conversation_id)
2441
+ return undefined;
2442
+ const rows = getDb().prepare(`
2443
+ SELECT *
2444
+ FROM imported_plan_run
2445
+ WHERE conversation_id = ?
2446
+ AND status IN ('running', 'paused')
2447
+ ORDER BY datetime(updated_at) DESC, id DESC
2448
+ `).all(task.conversation_id);
2449
+ for (const row of rows) {
2450
+ const run = normalizeImportedPlanRunRow(row);
2451
+ if (!doesTodoBelongToImportedPlanRun(run, task))
2452
+ continue;
2453
+ getDb().prepare(`
2454
+ UPDATE imported_plan_run
2455
+ SET updated_at = datetime('now')
2456
+ WHERE id = ?
2457
+ `).run(run.id);
2458
+ return getImportedPlanRun(run.id);
2459
+ }
2460
+ return undefined;
2461
+ }
2462
+ function parseImportedPlanStageArray(raw) {
2463
+ try {
2464
+ const parsed = JSON.parse(String(raw || '[]'));
2465
+ if (!Array.isArray(parsed))
2466
+ return [];
2467
+ return parsed
2468
+ .map((entry) => ({
2469
+ role: String(entry?.role || '').trim(),
2470
+ botId: String(entry?.botId || '').trim(),
2471
+ }))
2472
+ .filter((entry) => entry.role && entry.botId);
2473
+ }
2474
+ catch {
2475
+ return [];
2476
+ }
2477
+ }
2478
+ function parseImportedPlanOptions(raw) {
2479
+ try {
2480
+ const parsed = JSON.parse(String(raw || '{}'));
2481
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
2482
+ return {};
2483
+ return {
2484
+ plannerInstructions: typeof parsed.plannerInstructions === 'string' ? parsed.plannerInstructions : null,
2485
+ executionOrder: parsed.executionOrder === 'batch_by_stage' ? 'batch_by_stage'
2486
+ : parsed.executionOrder === 'per_item_pipeline' ? 'per_item_pipeline'
2487
+ : null,
2488
+ };
2489
+ }
2490
+ catch {
2491
+ return {};
2492
+ }
2493
+ }
2494
+ function parseImportedPlanTaskIds(raw) {
2495
+ try {
2496
+ const parsed = JSON.parse(String(raw || '[]'));
2497
+ if (!Array.isArray(parsed))
2498
+ return [];
2499
+ return parsed
2500
+ .map((value) => Number(value))
2501
+ .filter((value) => Number.isFinite(value) && value > 0);
2502
+ }
2503
+ catch {
2504
+ return [];
2505
+ }
2506
+ }
2507
+ function normalizeImportedPlanRunRow(row) {
2508
+ return {
2509
+ id: String(row.id),
2510
+ conversation_id: String(row.conversation_id),
2511
+ project_id: row.project_id ? String(row.project_id) : null,
2512
+ source_file_path: String(row.source_file_path || ''),
2513
+ source_file_name: String(row.source_file_name || ''),
2514
+ planner_bot_id: row.planner_bot_id ? String(row.planner_bot_id) : null,
2515
+ planner_agent_name: row.planner_agent_name ? String(row.planner_agent_name) : null,
2516
+ planner_provider: row.planner_provider ? String(row.planner_provider) : null,
2517
+ planner_model: row.planner_model ? String(row.planner_model) : null,
2518
+ requested_stages: parseImportedPlanStageArray(row.requested_stages_json),
2519
+ requested_options: parseImportedPlanOptions(row.requested_options_json),
2520
+ task_ids: parseImportedPlanTaskIds(row.task_ids_json),
2521
+ status: String(row.status || 'paused'),
2522
+ created_at: String(row.created_at || ''),
2523
+ updated_at: String(row.updated_at || ''),
2524
+ started_at: row.started_at ? String(row.started_at) : null,
2525
+ completed_at: row.completed_at ? String(row.completed_at) : null,
2526
+ discarded_at: row.discarded_at ? String(row.discarded_at) : null,
2527
+ last_error: row.last_error ? String(row.last_error) : null,
2528
+ };
2529
+ }
2530
+ function createImportedPlanRun(params) {
2531
+ const db = getDb();
2532
+ const id = newId();
2533
+ const ts = now();
2534
+ db.prepare(`
2535
+ INSERT INTO imported_plan_run (
2536
+ id, conversation_id, project_id, source_file_path, source_file_name,
2537
+ planner_bot_id, planner_agent_name, planner_provider, planner_model,
2538
+ requested_stages_json, requested_options_json, task_ids_json, status,
2539
+ created_at, updated_at, started_at, completed_at, discarded_at, last_error
2540
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2541
+ `).run(id, params.conversationId, params.projectId ?? null, params.sourceFilePath, params.sourceFileName, params.plannerBotId ?? null, params.plannerAgentName ?? null, params.plannerProvider ?? null, params.plannerModel ?? null, JSON.stringify(params.requestedStages || []), JSON.stringify(params.requestedOptions || {}), JSON.stringify((params.taskIds || []).filter((taskId) => Number.isFinite(taskId) && taskId > 0)), params.status || 'paused', ts, ts, params.status === 'running' ? ts : null, params.status === 'completed' ? ts : null, params.status === 'discarded' ? ts : null, null);
2542
+ return getImportedPlanRun(id);
2543
+ }
2544
+ function getImportedPlanRun(id) {
2545
+ const row = getDb().prepare('SELECT * FROM imported_plan_run WHERE id = ?').get(id);
2546
+ if (!row)
2547
+ return undefined;
2548
+ return normalizeImportedPlanRunRow(row);
2549
+ }
2550
+ function listImportedPlanRunsForConversation(conversationId) {
2551
+ const rows = getDb().prepare('SELECT * FROM imported_plan_run WHERE conversation_id = ? ORDER BY datetime(created_at) DESC, id DESC').all(conversationId);
2552
+ return rows.map(normalizeImportedPlanRunRow);
2553
+ }
2554
+ function getLatestImportedPlanRunForConversation(conversationId, opts) {
2555
+ const clauses = ['conversation_id = ?'];
2556
+ const params = [conversationId];
2557
+ if (!opts?.includeDiscarded)
2558
+ clauses.push("status <> 'discarded'");
2559
+ if (!opts?.includeCompleted)
2560
+ clauses.push("status <> 'completed'");
2561
+ const row = getDb().prepare(`SELECT * FROM imported_plan_run WHERE ${clauses.join(' AND ')} ORDER BY datetime(updated_at) DESC, id DESC LIMIT 1`).get(...params);
2562
+ if (!row)
2563
+ return undefined;
2564
+ return normalizeImportedPlanRunRow(row);
2565
+ }
2566
+ function updateImportedPlanRun(id, fields) {
2567
+ const db = getDb();
2568
+ const sets = [];
2569
+ const vals = [];
2570
+ if (fields.taskIds !== undefined) {
2571
+ sets.push('task_ids_json = ?');
2572
+ vals.push(JSON.stringify(fields.taskIds));
2573
+ }
2574
+ if (fields.status !== undefined) {
2575
+ sets.push('status = ?');
2576
+ vals.push(fields.status);
2577
+ }
2578
+ if (fields.startedAt !== undefined) {
2579
+ sets.push('started_at = ?');
2580
+ vals.push(fields.startedAt);
2581
+ }
2582
+ if (fields.completedAt !== undefined) {
2583
+ sets.push('completed_at = ?');
2584
+ vals.push(fields.completedAt);
2585
+ }
2586
+ if (fields.discardedAt !== undefined) {
2587
+ sets.push('discarded_at = ?');
2588
+ vals.push(fields.discardedAt);
2589
+ }
2590
+ if (fields.lastError !== undefined) {
2591
+ sets.push('last_error = ?');
2592
+ vals.push(fields.lastError);
2593
+ }
2594
+ if (sets.length === 0)
2595
+ return getImportedPlanRun(id);
2596
+ sets.push("updated_at = datetime('now')");
2597
+ vals.push(id);
2598
+ db.prepare(`UPDATE imported_plan_run SET ${sets.join(', ')} WHERE id = ?`).run(...vals);
2599
+ return getImportedPlanRun(id);
2600
+ }
2601
+ function touchImportedPlanRun(id) {
2602
+ getDb().prepare(`
2603
+ UPDATE imported_plan_run
2604
+ SET updated_at = datetime('now')
2605
+ WHERE id = ?
2606
+ `).run(id);
2607
+ return getImportedPlanRun(id);
2608
+ }
2609
+ function getImportedPlanRunTasks(id) {
2610
+ const run = getImportedPlanRun(id);
2611
+ if (!run)
2612
+ return [];
2613
+ const knownTasks = run.task_ids
2614
+ .map((taskId) => getTodoTask(taskId, 'active') || getTodoTask(taskId, 'completed'))
2615
+ .filter((task) => !!task);
2616
+ if (knownTasks.length === 0) {
2617
+ return run.task_ids
2618
+ .map((taskId) => getTodoTask(taskId, 'active') || getTodoTask(taskId, 'completed'))
2619
+ .filter((task) => !!task);
2620
+ }
2621
+ const firstImportedOrder = Math.min(...knownTasks.map((task) => task.created_order));
2622
+ const db = getDb();
2623
+ const activeRows = db.prepare(`SELECT * FROM todo_active WHERE conversation_id = ? AND COALESCE(internal_only, 0) = 0 AND created_order >= ? ORDER BY created_order ASC, id ASC`).all(run.conversation_id, firstImportedOrder);
2624
+ const completedRows = db.prepare(`SELECT * FROM todo_completed WHERE conversation_id = ? AND COALESCE(internal_only, 0) = 0 AND created_order >= ? ORDER BY created_order ASC, id ASC`).all(run.conversation_id, firstImportedOrder);
2625
+ const allTasks = [
2626
+ ...activeRows.map((row) => normalizeTodoRow(row, 'active')),
2627
+ ...completedRows.map((row) => normalizeTodoRow(row, 'completed')),
2628
+ ].sort((a, b) => (a.created_order - b.created_order) || (a.id - b.id));
2629
+ return allTasks;
2630
+ }
2631
+ function getImportedPlanRunRemainingTaskIds(id) {
2632
+ const run = getImportedPlanRun(id);
2633
+ if (!run)
2634
+ return [];
2635
+ const knownTasks = run.task_ids
2636
+ .map((taskId) => getTodoTask(taskId, 'active') || getTodoTask(taskId, 'completed'))
2637
+ .filter((task) => !!task);
2638
+ if (knownTasks.length === 0) {
2639
+ return run.task_ids.filter((taskId) => !!getTodoTask(taskId, 'active'));
2640
+ }
2641
+ // Resume must follow the current TODO queue, not just the original imported
2642
+ // IDs. QA-fail fix tasks and re-QA companions are inserted into the same
2643
+ // conversation/order after import; if the app resumes later, those inserted
2644
+ // active tasks must run before later original tasks.
2645
+ const firstImportedOrder = Math.min(...knownTasks.map((task) => task.created_order));
2646
+ const rows = getDb().prepare(`
2647
+ SELECT id
2648
+ FROM todo_active
2649
+ WHERE conversation_id = ?
2650
+ AND COALESCE(internal_only, 0) = 0
2651
+ AND created_order >= ?
2652
+ ORDER BY created_order ASC, id ASC
2653
+ `).all(run.conversation_id, firstImportedOrder);
2654
+ return rows.map((row) => Number(row.id)).filter((taskId) => Number.isFinite(taskId) && taskId > 0);
2655
+ }
2656
+ function getImportedPlanRunCurrentTask(id) {
2657
+ return getImportedPlanRunTasks(id).find((task) => task.state === 'active');
2658
+ }
2659
+ function deleteImportedPlanRunSelectedTasks(id, selectedTaskIds, actor) {
2660
+ const db = getDb();
2661
+ const tx = db.transaction(() => {
2662
+ const current = getImportedPlanRun(id);
2663
+ if (!current)
2664
+ throw new Error(`Imported plan run ${id} not found`);
2665
+ if (current.status !== 'paused') {
2666
+ throw new Error('Imported plan tasks can only be edited while the plan is paused.');
2667
+ }
2668
+ const selected = new Set((selectedTaskIds || [])
2669
+ .map((taskId) => Number(taskId))
2670
+ .filter((taskId) => Number.isFinite(taskId) && current.task_ids.includes(taskId)));
2671
+ if (selected.size === 0)
2672
+ return current;
2673
+ for (const taskId of selected) {
2674
+ if (getTodoTask(taskId, 'active')) {
2675
+ deleteTodoTask(taskId, { actor, state: 'active' });
2676
+ }
2677
+ }
2678
+ const remainingTaskIds = current.task_ids.filter((taskId) => !selected.has(taskId) && !!getTodoTask(taskId, 'active'));
2679
+ const nextStatus = remainingTaskIds.length > 0 ? 'paused' : 'discarded';
2680
+ return updateImportedPlanRun(id, {
2681
+ taskIds: remainingTaskIds,
2682
+ status: nextStatus,
2683
+ discardedAt: remainingTaskIds.length > 0 ? null : now(),
2684
+ lastError: null,
2685
+ });
2686
+ });
2687
+ return tx();
2688
+ }
1742
2689
  function createOutboxEvent(e) {
1743
2690
  const db = getDb();
1744
2691
  const id = newId();
@@ -1781,7 +2728,7 @@ function setJsonSetting(key, value) {
1781
2728
  setSetting(key, JSON.stringify(value));
1782
2729
  }
1783
2730
  function getAllSettings() {
1784
- const rows = getDb().prepare("SELECT key, value FROM settings WHERE key NOT LIKE '_%'").all();
2731
+ const rows = getDb().prepare("SELECT key, value FROM settings WHERE substr(key, 1, 1) != '_'").all();
1785
2732
  const result = {};
1786
2733
  for (const r of rows)
1787
2734
  result[r.key] = r.value;
@@ -2194,6 +3141,7 @@ function normalizeTodoRow(row, state) {
2194
3141
  return {
2195
3142
  id: Number(row.id),
2196
3143
  project_id: row.project_id ? String(row.project_id) : null,
3144
+ topic_id: row.topic_id ? String(row.topic_id) : null,
2197
3145
  conversation_id: row.conversation_id ? String(row.conversation_id) : null,
2198
3146
  origin: row.origin ? String(row.origin) : null,
2199
3147
  created_by_bot_id: row.created_by_bot_id ? String(row.created_by_bot_id) : null,
@@ -2233,9 +3181,9 @@ function roleClassToTaskType(roleClass) {
2233
3181
  const normalized = String(roleClass || '').trim().toLowerCase();
2234
3182
  if (!normalized)
2235
3183
  return null;
2236
- if (normalized === 'code' || normalized === 'coding' || normalized === 'developer')
3184
+ if (normalized === 'code' || normalized === 'coding' || normalized === 'developer' || normalized === 'builder' || normalized === 'engineer')
2237
3185
  return 'coding';
2238
- if (normalized === 'qa' || normalized === 'review')
3186
+ if (normalized === 'qa' || normalized === 'review' || normalized === 'reviewer')
2239
3187
  return 'qa';
2240
3188
  if (normalized === 'research' || normalized === 'brainstorm' || normalized === 'planning' || normalized === 'plan')
2241
3189
  return 'research';
@@ -2255,7 +3203,11 @@ function inferTaskTypeForAgent(agent, requested) {
2255
3203
  const explicit = String(requested || '').trim();
2256
3204
  if (explicit)
2257
3205
  return explicit;
2258
- return roleClassToTaskType(agent?.role_class) || roleClassToTaskType(agent?.role_label) || null;
3206
+ return roleClassToTaskType(agent?.orchestration_role_class)
3207
+ || roleClassToTaskType(agent?.orchestration_role_label)
3208
+ || roleClassToTaskType(agent?.role_class)
3209
+ || roleClassToTaskType(agent?.role_label)
3210
+ || null;
2259
3211
  }
2260
3212
  function buildQueuedTaskContext(task) {
2261
3213
  const lines = [
@@ -2268,21 +3220,21 @@ function buildQueuedTaskContext(task) {
2268
3220
  return lines.join('\n');
2269
3221
  }
2270
3222
  function injectWorkerHandoffIntoPrompt(existingPrompt, handoffText, nextTask) {
2271
- const taskReplacement = [
2272
- 'Task:',
3223
+ const safeHandoff = String(handoffText || '');
3224
+ const handoffSection = [
2273
3225
  'PREVIOUS WORKER HANDOFF',
2274
- handoffText,
3226
+ safeHandoff,
2275
3227
  '',
2276
3228
  buildQueuedTaskContext(nextTask),
2277
- '',
2278
- 'RUNTIME CONTEXT',
2279
3229
  ].join('\n');
2280
3230
  if (existingPrompt.includes('\nRUNTIME CONTEXT\n')) {
2281
- return existingPrompt.replace(/Task:\s*[\s\S]*?\nRUNTIME CONTEXT\n/, `${taskReplacement}\n`);
3231
+ return existingPrompt.replace(/\nRUNTIME CONTEXT\n/, `\n${handoffSection}\n\nRUNTIME CONTEXT\n`);
2282
3232
  }
2283
3233
  return [
3234
+ existingPrompt,
3235
+ '',
2284
3236
  'PREVIOUS WORKER HANDOFF',
2285
- handoffText,
3237
+ safeHandoff,
2286
3238
  '',
2287
3239
  buildQueuedTaskContext(nextTask),
2288
3240
  ].join('\n');
@@ -2330,6 +3282,7 @@ function moveActiveTaskToCompletedTx(db, taskId, actor, reason) {
2330
3282
  const row = db.prepare('SELECT * FROM todo_active WHERE id = ?').get(taskId);
2331
3283
  if (!row)
2332
3284
  throw new Error(`Task ${taskId} not found`);
3285
+ const activeTask = normalizeTodoRow(row, 'active');
2333
3286
  const completedAt = now();
2334
3287
  db.prepare(`
2335
3288
  INSERT INTO todo_completed (
@@ -2343,7 +3296,9 @@ function moveActiveTaskToCompletedTx(db, taskId, actor, reason) {
2343
3296
  db.prepare('DELETE FROM todo_active WHERE id = ?').run(taskId);
2344
3297
  logTodoAuditTx(db, taskId, 'completed', 'complete', actor, { reason });
2345
3298
  const completed = db.prepare('SELECT * FROM todo_completed WHERE id = ?').get(taskId);
2346
- return normalizeTodoRow(completed, 'completed');
3299
+ const completedTask = normalizeTodoRow(completed, 'completed');
3300
+ touchImportedPlanRunForTask(activeTask);
3301
+ return completedTask;
2347
3302
  }
2348
3303
  function determineInsertPositionTx(db, projectId, insertAfterTaskId) {
2349
3304
  if (insertAfterTaskId) {
@@ -2417,43 +3372,35 @@ function resolveAllowedWorkerNamesForInsertion(currentTask) {
2417
3372
  allowed.add(previous.owner_name.trim().toLowerCase());
2418
3373
  return Array.from(allowed);
2419
3374
  }
2420
- function resolvePreviousWorkerName(currentTask) {
3375
+ function resolvePreviousWorkerTask(currentTask) {
2421
3376
  const db = getDb();
2422
3377
  const previous = currentTask.project_id
2423
3378
  ? db.prepare(`
2424
- SELECT owner_name
3379
+ SELECT owner_name, task_type
2425
3380
  FROM (
2426
- SELECT owner_name, created_order, internal_only FROM todo_active WHERE project_id = ?
3381
+ SELECT owner_name, task_type, created_order, internal_only FROM todo_active WHERE project_id = ?
2427
3382
  UNION ALL
2428
- SELECT owner_name, created_order, internal_only FROM todo_completed WHERE project_id = ?
3383
+ SELECT owner_name, task_type, created_order, internal_only FROM todo_completed WHERE project_id = ?
2429
3384
  )
2430
3385
  WHERE created_order < ? AND COALESCE(internal_only, 0) = 0 AND owner_name IS NOT NULL
2431
3386
  ORDER BY created_order DESC
2432
3387
  LIMIT 1
2433
3388
  `).get(currentTask.project_id, currentTask.project_id, currentTask.position)
2434
3389
  : db.prepare(`
2435
- SELECT owner_name
3390
+ SELECT owner_name, task_type
2436
3391
  FROM (
2437
- SELECT owner_name, created_order, internal_only FROM todo_active WHERE project_id IS NULL
3392
+ SELECT owner_name, task_type, created_order, internal_only FROM todo_active WHERE project_id IS NULL
2438
3393
  UNION ALL
2439
- SELECT owner_name, created_order, internal_only FROM todo_completed WHERE project_id IS NULL
3394
+ SELECT owner_name, task_type, created_order, internal_only FROM todo_completed WHERE project_id IS NULL
2440
3395
  )
2441
3396
  WHERE created_order < ? AND COALESCE(internal_only, 0) = 0 AND owner_name IS NOT NULL
2442
3397
  ORDER BY created_order DESC
2443
3398
  LIMIT 1
2444
3399
  `).get(currentTask.position);
2445
- return previous?.owner_name?.trim() || null;
3400
+ return previous || null;
2446
3401
  }
2447
- function shouldAutoCreateQaFixTask(task, outputSummary, handoffPrompt) {
2448
- if (String(task.task_type || '').trim().toLowerCase() !== 'qa')
2449
- return false;
2450
- const combined = `${outputSummary || ''}\n${handoffPrompt || ''}`.trim().toLowerCase();
2451
- if (!combined)
2452
- return false;
2453
- if (/(^|\b)(qa passed|all checks passed|no issues found|looks good to ship)(\b|$)/i.test(combined)) {
2454
- return false;
2455
- }
2456
- return /\b(fix|defect|issue|problem|error|not ready|fails?|failed|remaining problem|remaining issue|validator|re-qa)\b/i.test(combined);
3402
+ function resolvePreviousWorkerName(currentTask) {
3403
+ return resolvePreviousWorkerTask(currentTask)?.owner_name?.trim() || null;
2457
3404
  }
2458
3405
  function normalizeArtifactInput(ref) {
2459
3406
  if (typeof ref === 'string') {
@@ -2519,13 +3466,22 @@ function listTodoTasks(opts) {
2519
3466
  const limit = opts?.limit ?? 200;
2520
3467
  const offset = opts?.offset ?? 0;
2521
3468
  const table = state === 'active' ? 'todo_active' : 'todo_completed';
3469
+ const topicSelect = `
3470
+ (
3471
+ SELECT ts.topic_id
3472
+ FROM topic_segment ts
3473
+ WHERE ts.conversation_id = t.conversation_id
3474
+ ORDER BY datetime(ts.created_at) DESC, ts.id DESC
3475
+ LIMIT 1
3476
+ ) AS topic_id
3477
+ `;
2522
3478
  const orderBy = state === 'active'
2523
- ? 'ORDER BY created_order ASC, id ASC'
2524
- : 'ORDER BY datetime(completed_at) DESC, id DESC';
3479
+ ? 'ORDER BY t.created_order ASC, t.id ASC'
3480
+ : 'ORDER BY datetime(t.completed_at) DESC, t.id DESC';
2525
3481
  const hasProjectFilter = Object.prototype.hasOwnProperty.call(opts || {}, 'projectId') && opts?.projectId !== undefined;
2526
3482
  const sql = hasProjectFilter
2527
- ? `SELECT * FROM ${table} WHERE project_id ${opts?.projectId ? '= ?' : 'IS NULL'} ${orderBy} LIMIT ? OFFSET ?`
2528
- : `SELECT * FROM ${table} ${orderBy} LIMIT ? OFFSET ?`;
3483
+ ? `SELECT t.*, ${topicSelect} FROM ${table} t WHERE t.project_id ${opts?.projectId ? '= ?' : 'IS NULL'} ${orderBy} LIMIT ? OFFSET ?`
3484
+ : `SELECT t.*, ${topicSelect} FROM ${table} t ${orderBy} LIMIT ? OFFSET ?`;
2529
3485
  const rows = hasProjectFilter
2530
3486
  ? (opts?.projectId
2531
3487
  ? db.prepare(sql).all(opts.projectId, limit, offset)
@@ -2698,7 +3654,9 @@ function editTodoTask(taskId, params) {
2698
3654
  return moveActiveTaskToCompletedTx(db, taskId, params.actor, 'all_llms_complete');
2699
3655
  }
2700
3656
  const updated = db.prepare('SELECT * FROM todo_active WHERE id = ?').get(taskId);
2701
- return normalizeTodoRow(updated, 'active');
3657
+ const updatedTask = normalizeTodoRow(updated, 'active');
3658
+ touchImportedPlanRunForTask(updatedTask);
3659
+ return updatedTask;
2702
3660
  });
2703
3661
  return tx();
2704
3662
  }
@@ -2762,7 +3720,9 @@ function checkTodoTask(taskId, params) {
2762
3720
  return moveActiveTaskToCompletedTx(db, taskId, params.actor, 'all_llms_complete');
2763
3721
  }
2764
3722
  const updated = db.prepare('SELECT * FROM todo_active WHERE id = ?').get(taskId);
2765
- return normalizeTodoRow(updated, 'active');
3723
+ const updatedTask = normalizeTodoRow(updated, 'active');
3724
+ touchImportedPlanRunForTask(updatedTask);
3725
+ return updatedTask;
2766
3726
  });
2767
3727
  return tx();
2768
3728
  }
@@ -2817,6 +3777,23 @@ function completeTodoTaskByWorker(taskId, params) {
2817
3777
  if (!isAssignedWorker && !isOrchestratorActor) {
2818
3778
  throw new Error(`Actor "${actorName || 'unknown'}" is not the owner of task ${taskId}`);
2819
3779
  }
3780
+ // QA tasks MUST declare qa_result. Codex QA 2026-04-19 (HIGH): the
3781
+ // prompt says so, but the runtime was not enforcing it — a QA
3782
+ // worker that omitted qa_result was silently treated as "pass"
3783
+ // because control fell through to the next_worker handoff branch.
3784
+ // Now rejected at the edge. Orchestrator actors (admin override)
3785
+ // bypass this check — that's for cleanup operations only.
3786
+ const isQaTask = String(currentTask.task_type || '').trim().toLowerCase() === 'qa';
3787
+ if (isQaTask && !isOrchestratorActor) {
3788
+ const incomingQaResult = params.qaResult;
3789
+ if (incomingQaResult !== 'pass' && incomingQaResult !== 'fail') {
3790
+ throw new Error(`QA task ${taskId} must be completed with qa_result='pass' or qa_result='fail'. `
3791
+ + `Got ${incomingQaResult === undefined ? 'no qa_result' : `qa_result='${String(incomingQaResult)}'`}. `
3792
+ + `'not_applicable' is not valid on QA tasks. `
3793
+ + `If QA passed with no defects, set qa_result='pass'. `
3794
+ + `If defects were found, set qa_result='fail' and include findings.`);
3795
+ }
3796
+ }
2820
3797
  const existingArtifactRows = loadArtifactRowsByIdsTx(db, currentTask.artifact_ids);
2821
3798
  const newArtifactRows = resolveArtifactRowsTx(db, params.artifactRefs || [], {
2822
3799
  projectId: currentTask.project_id,
@@ -2843,6 +3820,9 @@ function completeTodoTaskByWorker(taskId, params) {
2843
3820
  outputSummary,
2844
3821
  artifactIds,
2845
3822
  inserted: Boolean(params.insertTask),
3823
+ qaResult: params.qaResult ?? null,
3824
+ findings: params.findings ?? null,
3825
+ requiresFixTask: params.requiresFixTask ?? null,
2846
3826
  });
2847
3827
  const completedTask = moveActiveTaskToCompletedTx(db, taskId, params.actor, 'all_llms_complete');
2848
3828
  let insertedTask;
@@ -2897,26 +3877,46 @@ function completeTodoTaskByWorker(taskId, params) {
2897
3877
  }
2898
3878
  }
2899
3879
  else {
2900
- if (shouldAutoCreateQaFixTask(completedTask, outputSummary, handoffPrompt)) {
2901
- const previousWorkerName = resolvePreviousWorkerName(completedTask);
3880
+ const wantsQaFixTask = params.qaResult === 'fail' && params.requiresFixTask !== false;
3881
+ if (wantsQaFixTask) {
3882
+ const previousWorkerTask = resolvePreviousWorkerTask(completedTask);
3883
+ const previousWorkerName = previousWorkerTask?.owner_name?.trim() || null;
3884
+ const previousTaskType = String(previousWorkerTask?.task_type || '').trim().toLowerCase();
2902
3885
  const previousWorker = resolveAgentByName(previousWorkerName);
2903
- if (previousWorkerName && previousWorker) {
3886
+ const findingsText = (params.findings || []).map((s) => String(s || '').trim()).filter(Boolean).join('\n- ');
3887
+ const fixPrompt = findingsText
3888
+ ? `QA findings to address:\n- ${findingsText}\n\n${handoffPrompt ? handoffPrompt + '\n\n' : ''}Address the findings above and keep the approved parts unchanged.`
3889
+ : (handoffPrompt || outputSummary || 'Address the QA findings and keep the approved parts unchanged.');
3890
+ if (previousTaskType === 'qa') {
3891
+ returnedToOrchestrator = true;
3892
+ logTodoAuditTx(db, taskId, 'completed', 'return_to_orchestrator', params.actor, {
3893
+ handoffPrompt,
3894
+ qaResult: 'fail',
3895
+ findings: params.findings || null,
3896
+ skippedQaFixLoop: true,
3897
+ previousWorkerName,
3898
+ previousTaskType,
3899
+ });
3900
+ }
3901
+ else if (previousWorkerName && previousWorker) {
2904
3902
  const participants = [previousWorker.name];
2905
3903
  insertedTask = insertTodoTaskTx(db, {
2906
3904
  projectId: completedTask.project_id,
2907
3905
  conversationId: completedTask.conversation_id,
2908
3906
  title: 'Fix QA findings',
2909
- details: handoffPrompt || outputSummary || 'Address the QA findings and update the implementation.',
3907
+ details: findingsText
3908
+ ? `QA findings:\n- ${findingsText}`
3909
+ : (handoffPrompt || outputSummary || 'Address the QA findings and update the implementation.'),
2910
3910
  participants,
2911
3911
  successCriteria: 'Address the QA findings and return the updated work for re-QA.',
2912
3912
  ownerBotId: previousWorker.id,
2913
3913
  ownerName: previousWorker.name,
2914
- taskType: inferTaskTypeForAgent(previousWorker, null),
3914
+ taskType: previousWorkerTask?.task_type || inferTaskTypeForAgent(previousWorker, null),
2915
3915
  profileName: completedTask.profile_name || 'Programming',
2916
3916
  nextWorkerBotId: currentTask.owner_bot_id,
2917
3917
  nextWorkerName: currentTask.owner_name,
2918
3918
  nextWorkerRole: actorBot?.role_label || actorBot?.role_class || null,
2919
- taskPrompt: handoffPrompt || outputSummary || 'Address the QA findings and keep the approved parts unchanged.',
3919
+ taskPrompt: fixPrompt,
2920
3920
  artifactIds,
2921
3921
  insertAfterTaskId: completedTask.id,
2922
3922
  actor: params.actor,
@@ -2924,13 +3924,54 @@ function completeTodoTaskByWorker(taskId, params) {
2924
3924
  logTodoAuditTx(db, insertedTask.id, 'active', 'worker_insert', params.actor, {
2925
3925
  insertedAfterTaskId: completedTask.id,
2926
3926
  requestedWorkerName: previousWorker.name,
2927
- autoCreatedFromQaFailure: true,
3927
+ qaFailureFromStructuredField: true,
3928
+ findings: params.findings || null,
2928
3929
  });
3930
+ // Companion re-QA TODO: after the previous worker addresses the
3931
+ // findings, the QA worker must re-review. Without this follow-up
3932
+ // task, the fix TODO's next_worker_bot_id points at a QA worker
3933
+ // with no active TODO, and the dispatcher silently skips re-QA
3934
+ // and runs whatever sits next in the queue (item 5 bug).
3935
+ // Chain: fix -> re-QA -> [QA's original next_worker].
3936
+ const currentOwnerId = currentTask.owner_bot_id;
3937
+ const currentOwnerName = currentTask.owner_name;
3938
+ if (currentOwnerId && currentOwnerName) {
3939
+ const reQaTask = insertTodoTaskTx(db, {
3940
+ projectId: completedTask.project_id,
3941
+ conversationId: completedTask.conversation_id,
3942
+ title: `Re-QA after fix`,
3943
+ details: findingsText
3944
+ ? `Re-review the previous worker's fix for these findings:\n- ${findingsText}`
3945
+ : 'Re-review the previous worker\'s fix against the original handoff.',
3946
+ participants: [currentOwnerName],
3947
+ successCriteria: 'Re-QA confirms the findings were addressed and the work meets the original spec.',
3948
+ ownerBotId: currentOwnerId,
3949
+ ownerName: currentOwnerName,
3950
+ taskType: 'qa',
3951
+ profileName: completedTask.profile_name || 'Programming',
3952
+ nextWorkerBotId: completedTask.next_worker_bot_id,
3953
+ nextWorkerName: completedTask.next_worker_name,
3954
+ nextWorkerRole: completedTask.next_worker_role,
3955
+ taskPrompt: findingsText
3956
+ ? `Re-review the previous worker's fix.\n\nOriginal findings:\n- ${findingsText}\n\nConfirm each finding has been addressed and the work meets the original success criteria.`
3957
+ : 'Re-review the previous worker\'s fix against the original handoff and success criteria.',
3958
+ artifactIds,
3959
+ insertAfterTaskId: insertedTask.id,
3960
+ actor: params.actor,
3961
+ });
3962
+ logTodoAuditTx(db, reQaTask.id, 'active', 'worker_insert', params.actor, {
3963
+ insertedAfterTaskId: insertedTask.id,
3964
+ requestedWorkerName: currentOwnerName,
3965
+ qaReQueueFromStructuredField: true,
3966
+ });
3967
+ }
2929
3968
  }
2930
3969
  else {
2931
3970
  returnedToOrchestrator = true;
2932
3971
  logTodoAuditTx(db, taskId, 'completed', 'return_to_orchestrator', params.actor, {
2933
3972
  handoffPrompt,
3973
+ qaResult: 'fail',
3974
+ findings: params.findings || null,
2934
3975
  missingPreviousWorkerForQaFix: true,
2935
3976
  });
2936
3977
  }
@@ -2997,6 +4038,11 @@ function completeTodoTaskByWorker(taskId, params) {
2997
4038
  }
2998
4039
  }
2999
4040
  }
4041
+ touchImportedPlanRunForTask(completedTask);
4042
+ if (insertedTask)
4043
+ touchImportedPlanRunForTask(insertedTask);
4044
+ if (handedOffTask)
4045
+ touchImportedPlanRunForTask(handedOffTask);
3000
4046
  return {
3001
4047
  completedTask,
3002
4048
  insertedTask,
@@ -3092,6 +4138,7 @@ function blockTodoTaskByWorker(taskId, params) {
3092
4138
  checkedContext,
3093
4139
  userQuestion,
3094
4140
  });
4141
+ touchImportedPlanRunForTask(blockedTask);
3095
4142
  return {
3096
4143
  blockedTask,
3097
4144
  artifacts: artifactRows,
@@ -3101,6 +4148,39 @@ function blockTodoTaskByWorker(taskId, params) {
3101
4148
  });
3102
4149
  return tx();
3103
4150
  }
4151
+ function markTodoTaskRuntimeBlocked(taskId, params) {
4152
+ const blockerSummary = String(params.blockerSummary || '').trim();
4153
+ if (!blockerSummary)
4154
+ throw new Error('Runtime blocked task requires blockerSummary');
4155
+ const checkedContext = String(params.checkedContext || 'Runtime/provider error was captured while executing the worker task.').trim();
4156
+ const userQuestion = String(params.userQuestion || 'Fix the provider/runtime issue, then resume the plan.').trim();
4157
+ const db = getDb();
4158
+ const tx = db.transaction(() => {
4159
+ const row = db.prepare('SELECT * FROM todo_active WHERE id = ?').get(taskId);
4160
+ if (!row)
4161
+ throw new Error(`Task ${taskId} not found`);
4162
+ db.prepare(`
4163
+ UPDATE todo_active
4164
+ SET blocker_summary = ?,
4165
+ blocker_checked = ?,
4166
+ blocker_question = ?,
4167
+ last_updated = ?,
4168
+ version = version + 1
4169
+ WHERE id = ?
4170
+ `).run(blockerSummary, checkedContext, userQuestion, now(), taskId);
4171
+ logTodoAuditTx(db, taskId, 'active', 'worker_blocked', params.actor, {
4172
+ blockerSummary,
4173
+ checkedContext,
4174
+ userQuestion,
4175
+ reasonCode: params.reasonCode || null,
4176
+ runtimeBlocked: true,
4177
+ });
4178
+ const blockedTask = normalizeTodoRow(db.prepare('SELECT * FROM todo_active WHERE id = ?').get(taskId), 'active');
4179
+ touchImportedPlanRunForTask(blockedTask);
4180
+ return blockedTask;
4181
+ });
4182
+ return tx();
4183
+ }
3104
4184
  function deleteTodoTask(taskId, params) {
3105
4185
  if (params.actor.actorType !== 'user' && params.actor.actorType !== 'orchestrator')
3106
4186
  throw new Error('Only user or orchestrator can delete tasks');
@@ -3154,7 +4234,11 @@ function reorderTodoTask(taskId, toPosition, actor) {
3154
4234
  }
3155
4235
  logTodoAuditTx(db, taskId, 'active', 'reorder', actor, { toPosition: clamped });
3156
4236
  const updated = db.prepare('SELECT * FROM todo_active ORDER BY created_order ASC, id ASC').all();
3157
- return updated.map((r) => normalizeTodoRow(r, 'active'));
4237
+ const normalized = updated.map((r) => normalizeTodoRow(r, 'active'));
4238
+ const reorderedTask = normalized.find((task) => task.id === taskId);
4239
+ if (reorderedTask)
4240
+ touchImportedPlanRunForTask(reorderedTask);
4241
+ return normalized;
3158
4242
  });
3159
4243
  return tx();
3160
4244
  }
@@ -3191,6 +4275,15 @@ function listCompletedTodoTasksForConversation(conversationId) {
3191
4275
  const rows = getDb().prepare('SELECT * FROM todo_completed WHERE conversation_id = ? AND COALESCE(internal_only, 0) = 0 ORDER BY created_order ASC, id ASC').all(conversationId);
3192
4276
  return rows.map((row) => normalizeTodoRow(row, 'completed'));
3193
4277
  }
4278
+ /**
4279
+ * List all active (non-completed) TODOs for a conversation in creation order.
4280
+ * Used by the orchestration dispatch-runner to feed `existing_todos` into
4281
+ * the orchestrator hint so the LLM can see what's already queued.
4282
+ */
4283
+ function listActiveTodoTasksForConversation(conversationId) {
4284
+ const rows = getDb().prepare('SELECT * FROM todo_active WHERE conversation_id = ? AND COALESCE(internal_only, 0) = 0 ORDER BY created_order ASC, id ASC').all(conversationId);
4285
+ return rows.map((row) => normalizeTodoRow(row, 'active'));
4286
+ }
3194
4287
  exports.UNASSIGNED_PROJECT_NAME = 'unassigned';
3195
4288
  function createProject(p) {
3196
4289
  const db = getDb();
@@ -3265,12 +4358,22 @@ function getProjectOverview(projectId) {
3265
4358
  const workflowTemplates = listWorkflowTemplates(projectId);
3266
4359
  const roleAssignments = getJsonSetting(`project:${projectId}:role_assignments`)
3267
4360
  || (() => {
3268
- try {
3269
- return JSON.parse(projectSettings.orchestrator_role_assignments || '{}');
3270
- }
3271
- catch {
3272
- return {};
4361
+ const derived = {};
4362
+ for (const bot of bots) {
4363
+ const roleClass = (bot.role_class || '').toLowerCase();
4364
+ if (!roleClass)
4365
+ continue;
4366
+ if (!derived.coding && ['code', 'coding', 'coder', 'builder', 'developer', 'engineer'].includes(roleClass)) {
4367
+ derived.coding = bot.name;
4368
+ }
4369
+ else if (!derived.qa && ['qa', 'review', 'reviewer'].includes(roleClass)) {
4370
+ derived.qa = bot.name;
4371
+ }
4372
+ else if (!derived.research && ['research', 'researcher', 'analyst', 'advisor'].includes(roleClass)) {
4373
+ derived.research = bot.name;
4374
+ }
3273
4375
  }
4376
+ return derived;
3274
4377
  })();
3275
4378
  let preferredWorkflow = null;
3276
4379
  const rawWorkflow = projectSettings.orchestrator_preferred_workflow || projectSettings.preferred_workflow;
@@ -3290,6 +4393,7 @@ function getProjectOverview(projectId) {
3290
4393
  defaultModel: connection.default_model || null,
3291
4394
  hasSecret: connection.access_mode === 'cli' || Boolean(connection.api_key_enc),
3292
4395
  }));
4396
+ const clerkConfig = getResolvedClerkConfigInfo();
3293
4397
  const userPolicy = getUserOrchestrationPolicy() || null;
3294
4398
  const projectPolicyOverride = getProjectOrchestrationPolicyOverride(projectId) || null;
3295
4399
  return {
@@ -3305,9 +4409,9 @@ function getProjectOverview(projectId) {
3305
4409
  preferred_workflow: preferredWorkflow,
3306
4410
  provider_summary: providerSummary,
3307
4411
  clerk: {
3308
- provider: getSetting('clerk_provider') || null,
3309
- model: getSetting('clerk_model') || null,
3310
- configured: Boolean(getSetting('clerk_provider') && getSetting('clerk_model') && getSetting('clerk_api_key')),
4412
+ provider: clerkConfig.provider,
4413
+ model: clerkConfig.model,
4414
+ configured: clerkConfig.configured,
3311
4415
  },
3312
4416
  user_policy: userPolicy,
3313
4417
  project_policy_override: projectPolicyOverride,
@@ -3439,7 +4543,18 @@ function updateTopic(id, fields) {
3439
4543
  return db.prepare('SELECT * FROM topic WHERE id = ?').get(id);
3440
4544
  }
3441
4545
  function deleteTopic(id) {
3442
- return getDb().prepare('DELETE FROM topic WHERE id = ?').run(id).changes > 0;
4546
+ const db = getDb();
4547
+ const tx = db.transaction((topicId) => {
4548
+ const topicExists = db.prepare('SELECT id FROM topic WHERE id = ?').get(topicId);
4549
+ if (!topicExists)
4550
+ return false;
4551
+ const linkedConversationIds = db.prepare('SELECT DISTINCT conversation_id FROM topic_segment WHERE topic_id = ?').all(topicId).map((row) => row.conversation_id);
4552
+ for (const conversationId of linkedConversationIds) {
4553
+ db.prepare('DELETE FROM conversation WHERE id = ?').run(conversationId);
4554
+ }
4555
+ return db.prepare('DELETE FROM topic WHERE id = ?').run(topicId).changes > 0;
4556
+ });
4557
+ return tx(id);
3443
4558
  }
3444
4559
  function createTopicSegment(s) {
3445
4560
  const db = getDb();
@@ -3580,7 +4695,7 @@ function getPreviousConversationInTopic(conversationId) {
3580
4695
  ORDER BY ts.created_at DESC, c.updated_at DESC
3581
4696
  LIMIT 1
3582
4697
  `).get(topicId, conversationId);
3583
- return row || null;
4698
+ return hydrateConversationRow(row) || null;
3584
4699
  }
3585
4700
  function getTopicMessages(topicId) {
3586
4701
  const db = getDb();