funolio-agent 1.0.7 → 1.0.48

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 (239) hide show
  1. package/dist/agent-config.d.ts +9 -1
  2. package/dist/agent-config.d.ts.map +1 -1
  3. package/dist/agent-config.js +4 -1
  4. package/dist/agent-config.js.map +1 -1
  5. package/dist/approval.d.ts +1 -0
  6. package/dist/approval.d.ts.map +1 -1
  7. package/dist/approval.js +12 -0
  8. package/dist/approval.js.map +1 -1
  9. package/dist/auth/auto-detect.d.ts +11 -3
  10. package/dist/auth/auto-detect.d.ts.map +1 -1
  11. package/dist/auth/auto-detect.js +136 -168
  12. package/dist/auth/auto-detect.js.map +1 -1
  13. package/dist/auth/subscription-runtime.js +1 -1
  14. package/dist/auth/subscription-runtime.js.map +1 -1
  15. package/dist/auto-organizer.d.ts.map +1 -1
  16. package/dist/auto-organizer.js +4 -3
  17. package/dist/auto-organizer.js.map +1 -1
  18. package/dist/backfill.d.ts.map +1 -1
  19. package/dist/backfill.js +34 -30
  20. package/dist/backfill.js.map +1 -1
  21. package/dist/bot-manager.d.ts +4 -8
  22. package/dist/bot-manager.d.ts.map +1 -1
  23. package/dist/bot-manager.js +31 -160
  24. package/dist/bot-manager.js.map +1 -1
  25. package/dist/clerk-model.d.ts +15 -7
  26. package/dist/clerk-model.d.ts.map +1 -1
  27. package/dist/clerk-model.js +78 -43
  28. package/dist/clerk-model.js.map +1 -1
  29. package/dist/cli-session-epoch.d.ts +10 -0
  30. package/dist/cli-session-epoch.d.ts.map +1 -0
  31. package/dist/cli-session-epoch.js +61 -0
  32. package/dist/cli-session-epoch.js.map +1 -0
  33. package/dist/cli.js +7 -2
  34. package/dist/cli.js.map +1 -1
  35. package/dist/commands/import-history.js +5 -1
  36. package/dist/commands/import-history.js.map +1 -1
  37. package/dist/commands/init.d.ts.map +1 -1
  38. package/dist/commands/init.js +30 -1
  39. package/dist/commands/init.js.map +1 -1
  40. package/dist/commands/pool.js +1 -1
  41. package/dist/commands/pool.js.map +1 -1
  42. package/dist/commands/setup.d.ts +37 -0
  43. package/dist/commands/setup.d.ts.map +1 -1
  44. package/dist/commands/setup.js +146 -43
  45. package/dist/commands/setup.js.map +1 -1
  46. package/dist/commands/start.d.ts.map +1 -1
  47. package/dist/commands/start.js +117 -255
  48. package/dist/commands/start.js.map +1 -1
  49. package/dist/config-cleanup.d.ts.map +1 -1
  50. package/dist/config-cleanup.js +2 -1
  51. package/dist/config-cleanup.js.map +1 -1
  52. package/dist/config.d.ts +6 -9
  53. package/dist/config.d.ts.map +1 -1
  54. package/dist/config.js +7 -18
  55. package/dist/config.js.map +1 -1
  56. package/dist/context-window.d.ts +33 -5
  57. package/dist/context-window.d.ts.map +1 -1
  58. package/dist/context-window.js +122 -21
  59. package/dist/context-window.js.map +1 -1
  60. package/dist/eval/orchestrator-front-door-replay.js +1 -1
  61. package/dist/eval/orchestrator-front-door-replay.js.map +1 -1
  62. package/dist/eval/policy-detection-replay.js +1 -1
  63. package/dist/eval/policy-detection-replay.js.map +1 -1
  64. package/dist/import-parser-core.d.ts.map +1 -1
  65. package/dist/import-parser-core.js +74 -8
  66. package/dist/import-parser-core.js.map +1 -1
  67. package/dist/integration-tokens.d.ts +1 -6
  68. package/dist/integration-tokens.d.ts.map +1 -1
  69. package/dist/integration-tokens.js +38 -40
  70. package/dist/integration-tokens.js.map +1 -1
  71. package/dist/local-cli-pty-manager.d.ts +50 -0
  72. package/dist/local-cli-pty-manager.d.ts.map +1 -0
  73. package/dist/local-cli-pty-manager.js +645 -0
  74. package/dist/local-cli-pty-manager.js.map +1 -0
  75. package/dist/local-data.d.ts +89 -6
  76. package/dist/local-data.d.ts.map +1 -1
  77. package/dist/local-data.js +600 -63
  78. package/dist/local-data.js.map +1 -1
  79. package/dist/local-db.d.ts.map +1 -1
  80. package/dist/local-db.js +74 -1
  81. package/dist/local-db.js.map +1 -1
  82. package/dist/local-funnel.d.ts +0 -7
  83. package/dist/local-funnel.d.ts.map +1 -1
  84. package/dist/local-funnel.js +22 -30
  85. package/dist/local-funnel.js.map +1 -1
  86. package/dist/local-import-worker.d.ts.map +1 -1
  87. package/dist/local-import-worker.js +49 -4
  88. package/dist/local-import-worker.js.map +1 -1
  89. package/dist/local-memory-search.d.ts +1 -0
  90. package/dist/local-memory-search.d.ts.map +1 -1
  91. package/dist/local-memory-search.js +107 -21
  92. package/dist/local-memory-search.js.map +1 -1
  93. package/dist/local-server.d.ts +21 -0
  94. package/dist/local-server.d.ts.map +1 -1
  95. package/dist/local-server.js +1057 -501
  96. package/dist/local-server.js.map +1 -1
  97. package/dist/mcp/bridge-server.d.ts.map +1 -1
  98. package/dist/mcp/bridge-server.js +2 -1
  99. package/dist/mcp/bridge-server.js.map +1 -1
  100. package/dist/mcp/local-memory-server.d.ts +6 -1
  101. package/dist/mcp/local-memory-server.d.ts.map +1 -1
  102. package/dist/mcp/local-memory-server.js +38 -13
  103. package/dist/mcp/local-memory-server.js.map +1 -1
  104. package/dist/mcp/manager.d.ts +3 -22
  105. package/dist/mcp/manager.d.ts.map +1 -1
  106. package/dist/mcp/manager.js +66 -320
  107. package/dist/mcp/manager.js.map +1 -1
  108. package/dist/memory-extraction.d.ts +2 -0
  109. package/dist/memory-extraction.d.ts.map +1 -1
  110. package/dist/memory-extraction.js +3 -1
  111. package/dist/memory-extraction.js.map +1 -1
  112. package/dist/message-loop.d.ts +1 -3
  113. package/dist/message-loop.d.ts.map +1 -1
  114. package/dist/message-loop.js +220 -437
  115. package/dist/message-loop.js.map +1 -1
  116. package/dist/mqtt-client.d.ts +2 -28
  117. package/dist/mqtt-client.d.ts.map +1 -1
  118. package/dist/mqtt-client.js +2 -2
  119. package/dist/mqtt-client.js.map +1 -1
  120. package/dist/oauth.d.ts +6 -0
  121. package/dist/oauth.d.ts.map +1 -1
  122. package/dist/oauth.js +91 -0
  123. package/dist/oauth.js.map +1 -1
  124. package/dist/orchestration/front-door-policy.d.ts +5 -2
  125. package/dist/orchestration/front-door-policy.d.ts.map +1 -1
  126. package/dist/orchestration/front-door-policy.js +25 -28
  127. package/dist/orchestration/front-door-policy.js.map +1 -1
  128. package/dist/orchestration/orchestrator-blocked-prompt.d.ts +2 -1
  129. package/dist/orchestration/orchestrator-blocked-prompt.d.ts.map +1 -1
  130. package/dist/orchestration/orchestrator-blocked-prompt.js +12 -1
  131. package/dist/orchestration/orchestrator-blocked-prompt.js.map +1 -1
  132. package/dist/orchestration/orchestrator-final-response-prompt.d.ts +4 -1
  133. package/dist/orchestration/orchestrator-final-response-prompt.d.ts.map +1 -1
  134. package/dist/orchestration/orchestrator-final-response-prompt.js +9 -7
  135. package/dist/orchestration/orchestrator-final-response-prompt.js.map +1 -1
  136. package/dist/orchestration/orchestrator-operating-prompt.d.ts +11 -0
  137. package/dist/orchestration/orchestrator-operating-prompt.d.ts.map +1 -1
  138. package/dist/orchestration/orchestrator-operating-prompt.js +67 -44
  139. package/dist/orchestration/orchestrator-operating-prompt.js.map +1 -1
  140. package/dist/orchestration/worker-operating-prompt.d.ts +2 -0
  141. package/dist/orchestration/worker-operating-prompt.d.ts.map +1 -1
  142. package/dist/orchestration/worker-operating-prompt.js +41 -2
  143. package/dist/orchestration/worker-operating-prompt.js.map +1 -1
  144. package/dist/orchestrator.d.ts +17 -0
  145. package/dist/orchestrator.d.ts.map +1 -1
  146. package/dist/orchestrator.js +328 -166
  147. package/dist/orchestrator.js.map +1 -1
  148. package/dist/prompt-template.js +3 -3
  149. package/dist/prompt-template.js.map +1 -1
  150. package/dist/providers/anthropic.d.ts +0 -5
  151. package/dist/providers/anthropic.d.ts.map +1 -1
  152. package/dist/providers/anthropic.js +29 -75
  153. package/dist/providers/anthropic.js.map +1 -1
  154. package/dist/providers/claude-cli-prompt.d.ts.map +1 -1
  155. package/dist/providers/claude-cli-prompt.js +22 -6
  156. package/dist/providers/claude-cli-prompt.js.map +1 -1
  157. package/dist/providers/claude-cli.d.ts.map +1 -1
  158. package/dist/providers/claude-cli.js +36 -142
  159. package/dist/providers/claude-cli.js.map +1 -1
  160. package/dist/providers/codex-cli.d.ts.map +1 -1
  161. package/dist/providers/codex-cli.js +148 -74
  162. package/dist/providers/codex-cli.js.map +1 -1
  163. package/dist/providers/google.d.ts.map +1 -1
  164. package/dist/providers/google.js +4 -2
  165. package/dist/providers/google.js.map +1 -1
  166. package/dist/providers/index.d.ts +13 -0
  167. package/dist/providers/index.d.ts.map +1 -1
  168. package/dist/providers/index.js.map +1 -1
  169. package/dist/providers/openai.d.ts.map +1 -1
  170. package/dist/providers/openai.js +27 -2
  171. package/dist/providers/openai.js.map +1 -1
  172. package/dist/runtime-context.d.ts +10 -0
  173. package/dist/runtime-context.d.ts.map +1 -0
  174. package/dist/runtime-context.js +30 -0
  175. package/dist/runtime-context.js.map +1 -0
  176. package/dist/storage-mode.d.ts +5 -0
  177. package/dist/storage-mode.d.ts.map +1 -0
  178. package/dist/storage-mode.js +21 -0
  179. package/dist/storage-mode.js.map +1 -0
  180. package/dist/subagent/queue.d.ts.map +1 -1
  181. package/dist/subagent/queue.js +1 -0
  182. package/dist/subagent/queue.js.map +1 -1
  183. package/dist/summarization-pipeline.d.ts +10 -0
  184. package/dist/summarization-pipeline.d.ts.map +1 -1
  185. package/dist/summarization-pipeline.js +147 -34
  186. package/dist/summarization-pipeline.js.map +1 -1
  187. package/dist/tool-permissions.d.ts +2 -0
  188. package/dist/tool-permissions.d.ts.map +1 -0
  189. package/dist/tool-permissions.js +25 -0
  190. package/dist/tool-permissions.js.map +1 -0
  191. package/dist/tools/analyze-image.js +2 -2
  192. package/dist/tools/analyze-image.js.map +1 -1
  193. package/dist/tools/edit-file.js +3 -3
  194. package/dist/tools/edit-file.js.map +1 -1
  195. package/dist/tools/index.d.ts +7 -8
  196. package/dist/tools/index.d.ts.map +1 -1
  197. package/dist/tools/index.js +106 -60
  198. package/dist/tools/index.js.map +1 -1
  199. package/dist/tools/list-directory.js +7 -4
  200. package/dist/tools/list-directory.js.map +1 -1
  201. package/dist/tools/read-file.js +3 -3
  202. package/dist/tools/read-file.js.map +1 -1
  203. package/dist/tools/run-command.js +3 -3
  204. package/dist/tools/run-command.js.map +1 -1
  205. package/dist/tools/sandbox.d.ts +10 -5
  206. package/dist/tools/sandbox.d.ts.map +1 -1
  207. package/dist/tools/sandbox.js +41 -13
  208. package/dist/tools/sandbox.js.map +1 -1
  209. package/dist/tools/search-codebase.js +2 -2
  210. package/dist/tools/search-codebase.js.map +1 -1
  211. package/dist/tools/search-local-memory.d.ts.map +1 -1
  212. package/dist/tools/search-local-memory.js +19 -8
  213. package/dist/tools/search-local-memory.js.map +1 -1
  214. package/dist/tools/search-memory.d.ts.map +1 -1
  215. package/dist/tools/search-memory.js +9 -3
  216. package/dist/tools/search-memory.js.map +1 -1
  217. package/dist/tools/spawn-subagent.d.ts.map +1 -1
  218. package/dist/tools/spawn-subagent.js +1 -0
  219. package/dist/tools/spawn-subagent.js.map +1 -1
  220. package/dist/tools/write-file.js +3 -3
  221. package/dist/tools/write-file.js.map +1 -1
  222. package/dist/types.d.ts +5 -0
  223. package/dist/types.d.ts.map +1 -1
  224. package/dist/types.js +0 -3
  225. package/dist/types.js.map +1 -1
  226. package/dist/verification/index.js +2 -2
  227. package/dist/verification/index.js.map +1 -1
  228. package/dist/wizard-state.d.ts.map +1 -1
  229. package/dist/wizard-state.js +16 -2
  230. package/dist/wizard-state.js.map +1 -1
  231. package/dist/wizard-support.d.ts +2 -2
  232. package/dist/wizard-support.d.ts.map +1 -1
  233. package/dist/wizard-support.js +88 -99
  234. package/dist/wizard-support.js.map +1 -1
  235. package/dist/workflow-engine.d.ts +9 -3
  236. package/dist/workflow-engine.d.ts.map +1 -1
  237. package/dist/workflow-engine.js +378 -82
  238. package/dist/workflow-engine.js.map +1 -1
  239. package/package.json +2 -1
@@ -58,15 +58,30 @@ exports.createConversation = createConversation;
58
58
  exports.getConversation = getConversation;
59
59
  exports.listConversations = listConversations;
60
60
  exports.countConversations = countConversations;
61
+ exports.countMessagesForBot = countMessagesForBot;
62
+ exports.listBotConversationActivity = listBotConversationActivity;
61
63
  exports.updateConversation = updateConversation;
64
+ exports.touchConversationActivity = touchConversationActivity;
62
65
  exports.deleteConversation = deleteConversation;
63
66
  exports.addMessage = addMessage;
67
+ exports.updateMessage = updateMessage;
68
+ exports.createChatJob = createChatJob;
69
+ exports.getCliSessionEpoch = getCliSessionEpoch;
70
+ exports.upsertCliSessionEpoch = upsertCliSessionEpoch;
71
+ exports.deleteCliSessionEpoch = deleteCliSessionEpoch;
72
+ exports.getChatJob = getChatJob;
73
+ exports.getLatestConversationChatJob = getLatestConversationChatJob;
74
+ exports.listQueuedChatJobs = listQueuedChatJobs;
75
+ exports.countRunningChatJobs = countRunningChatJobs;
76
+ exports.updateChatJob = updateChatJob;
77
+ exports.markRunningChatJobsInterrupted = markRunningChatJobsInterrupted;
64
78
  exports.createMessageActivity = createMessageActivity;
65
79
  exports.attachMessageActivitiesToMessage = attachMessageActivitiesToMessage;
66
80
  exports.listMessageActivities = listMessageActivities;
67
81
  exports.deleteExpiredMessageActivities = deleteExpiredMessageActivities;
68
82
  exports.getMessage = getMessage;
69
83
  exports.getMessages = getMessages;
84
+ exports.getMessagesInRange = getMessagesInRange;
70
85
  exports.getLastMessages = getLastMessages;
71
86
  exports.getRecentMessageRounds = getRecentMessageRounds;
72
87
  exports.getMessageRoundsBefore = getMessageRoundsBefore;
@@ -104,6 +119,7 @@ exports.setSetting = setSetting;
104
119
  exports.setJsonSetting = setJsonSetting;
105
120
  exports.getAllSettings = getAllSettings;
106
121
  exports.deleteSetting = deleteSetting;
122
+ exports.purgeLegacyExtractionDataOnce = purgeLegacyExtractionDataOnce;
107
123
  exports.defaultOrchestrationPolicy = defaultOrchestrationPolicy;
108
124
  exports.resolveCurrentUserIdentity = resolveCurrentUserIdentity;
109
125
  exports.getUserOrchestrationPolicy = getUserOrchestrationPolicy;
@@ -211,6 +227,7 @@ const memory_extraction_1 = require("./memory-extraction");
211
227
  const orchestrator_blocked_prompt_1 = require("./orchestration/orchestrator-blocked-prompt");
212
228
  const crypto = __importStar(require("crypto"));
213
229
  const default_tool_profile_1 = require("./default-tool-profile");
230
+ const approval_1 = require("./approval");
214
231
  let _db = null;
215
232
  /** Get or open the shared database connection */
216
233
  function getDb() {
@@ -249,7 +266,7 @@ function createAgentProfile(p) {
249
266
  if (p.isOrchestrator) {
250
267
  db.prepare("UPDATE agent_profile SET is_orchestrator = 0, updated_at = datetime('now') WHERE is_orchestrator = 1").run();
251
268
  }
252
- insert.run(id, p.provider, p.model, p.name, p.soulMd ?? null, p.memoryMd ?? null, p.toolsMd ?? null, p.skillsMd ?? null, p.apiKeyEnc ?? null, 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);
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);
253
270
  });
254
271
  transaction();
255
272
  return db.prepare('SELECT * FROM agent_profile WHERE id = ?').get(id);
@@ -286,7 +303,8 @@ function migrateAgentProfilesToDefaultToolProfile() {
286
303
  catch {
287
304
  tools = [];
288
305
  }
289
- const needsPermission = !row.permission_mode || row.permission_mode === 'autopilot' || row.permission_mode === 'default';
306
+ const normalizedPermission = (0, approval_1.normalizePermissionMode)(row.permission_mode);
307
+ const needsPermission = !row.permission_mode || row.permission_mode !== normalizedPermission;
290
308
  const needsTools = tools.length === 0;
291
309
  if (!needsPermission && !needsTools)
292
310
  continue;
@@ -294,7 +312,7 @@ function migrateAgentProfilesToDefaultToolProfile() {
294
312
  const vals = [now()];
295
313
  if (needsPermission) {
296
314
  sets.push('permission_mode = ?');
297
- vals.push(default_tool_profile_1.DEFAULT_PERMISSION_MODE);
315
+ vals.push(normalizedPermission);
298
316
  }
299
317
  if (needsTools) {
300
318
  sets.push('enabled_builtin_tools_json = ?');
@@ -347,7 +365,7 @@ function updateAgentProfile(id, fields) {
347
365
  }
348
366
  if (fields.permissionMode !== undefined) {
349
367
  sets.push('permission_mode = ?');
350
- vals.push(fields.permissionMode);
368
+ vals.push((0, approval_1.normalizePermissionMode)(fields.permissionMode));
351
369
  }
352
370
  if (fields.isDefault !== undefined) {
353
371
  sets.push('is_default = ?');
@@ -660,37 +678,207 @@ function createConversation(agentId, title, source, opts) {
660
678
  `).run(id, agentId, title ?? null, source ?? 'local', projectId, projectName, ts, ts);
661
679
  return db.prepare('SELECT * FROM conversation WHERE id = ?').get(id);
662
680
  }
681
+ function latestConversationSummarySql(conversationAlias) {
682
+ return `(
683
+ SELECT cs.summary_text
684
+ FROM conversation_summary cs
685
+ WHERE cs.conversation_id = ${conversationAlias}.id
686
+ AND coalesce(cs.summary_kind, 'rolling') = 'rolling'
687
+ ORDER BY cs.turn_range_end DESC, cs.created_at DESC
688
+ LIMIT 1
689
+ )`;
690
+ }
691
+ function primaryConversationTopicTitleSql(conversationAlias) {
692
+ return `(
693
+ SELECT t.title
694
+ FROM topic_segment ts
695
+ INNER JOIN topic t ON t.id = ts.topic_id
696
+ WHERE ts.conversation_id = ${conversationAlias}.id
697
+ ORDER BY ts.created_at ASC
698
+ LIMIT 1
699
+ )`;
700
+ }
701
+ function firstConversationUserMessageSql(conversationAlias, maxLength = 120) {
702
+ return `(
703
+ SELECT substr(trim(m.content), 1, ${maxLength})
704
+ FROM message m
705
+ WHERE m.conversation_id = ${conversationAlias}.id
706
+ AND m.role = 'user'
707
+ AND trim(coalesce(m.content, '')) <> ''
708
+ AND trim(coalesce(m.content, '')) NOT LIKE '[System Instructions]%'
709
+ AND trim(coalesce(m.content, '')) NOT LIKE '# AGENTS.md%'
710
+ AND trim(coalesce(m.content, '')) NOT LIKE '<INSTRUCTIONS%'
711
+ AND trim(coalesce(m.content, '')) NOT LIKE '<environment_context%'
712
+ AND trim(coalesce(m.content, '')) NOT LIKE 'CURRENT TODO ID:%'
713
+ ORDER BY m.seq ASC
714
+ LIMIT 1
715
+ )`;
716
+ }
717
+ function firstConversationAssistantMessageSql(conversationAlias, maxLength = 120) {
718
+ return `(
719
+ SELECT substr(trim(m.content), 1, ${maxLength})
720
+ FROM message m
721
+ WHERE m.conversation_id = ${conversationAlias}.id
722
+ AND m.role = 'assistant'
723
+ AND trim(coalesce(m.content, '')) <> ''
724
+ ORDER BY m.seq ASC
725
+ LIMIT 1
726
+ )`;
727
+ }
728
+ function conversationSelectSql(conversationAlias) {
729
+ const latestSummary = latestConversationSummarySql(conversationAlias);
730
+ const topicTitle = primaryConversationTopicTitleSql(conversationAlias);
731
+ const firstUserMessage = firstConversationUserMessageSql(conversationAlias);
732
+ const firstAssistantMessage = firstConversationAssistantMessageSql(conversationAlias);
733
+ return `
734
+ SELECT
735
+ ${conversationAlias}.*,
736
+ COALESCE(
737
+ CASE
738
+ WHEN ${conversationAlias}.title IS NOT NULL
739
+ AND trim(${conversationAlias}.title) <> ''
740
+ AND trim(${conversationAlias}.title) <> 'New Conversation'
741
+ AND trim(${conversationAlias}.title) NOT LIKE '[System Instructions]%'
742
+ AND trim(${conversationAlias}.title) NOT LIKE '# AGENTS.md%'
743
+ AND trim(${conversationAlias}.title) NOT LIKE '<INSTRUCTIONS%'
744
+ AND trim(${conversationAlias}.title) NOT LIKE '<environment_context%'
745
+ AND trim(${conversationAlias}.title) NOT LIKE 'CURRENT TODO ID:%'
746
+ THEN ${conversationAlias}.title
747
+ ELSE NULL
748
+ END,
749
+ ${topicTitle},
750
+ ${firstUserMessage},
751
+ ${firstAssistantMessage},
752
+ ${conversationAlias}.title
753
+ ) AS resolved_title,
754
+ COALESCE(${latestSummary}, ${conversationAlias}.summary) AS resolved_summary,
755
+ CASE
756
+ WHEN ${conversationAlias}.source = 'import'
757
+ AND ${conversationAlias}.conversation_started_at IS NOT NULL
758
+ THEN ${conversationAlias}.conversation_started_at
759
+ ELSE ${conversationAlias}.created_at
760
+ END AS effective_created_at,
761
+ CASE
762
+ WHEN ${conversationAlias}.source = 'import'
763
+ AND ${conversationAlias}.conversation_ended_at IS NOT NULL
764
+ THEN ${conversationAlias}.conversation_ended_at
765
+ ELSE ${conversationAlias}.updated_at
766
+ END AS effective_updated_at
767
+ FROM conversation ${conversationAlias}
768
+ `;
769
+ }
770
+ function hydrateConversationRow(row) {
771
+ if (!row)
772
+ return undefined;
773
+ return {
774
+ ...row,
775
+ title: row.resolved_title ?? row.title,
776
+ summary: row.resolved_summary ?? row.summary,
777
+ created_at: row.effective_created_at ?? row.created_at,
778
+ updated_at: row.effective_updated_at ?? row.updated_at,
779
+ };
780
+ }
663
781
  function getConversation(id) {
664
- return getDb().prepare('SELECT * FROM conversation WHERE id = ?').get(id);
782
+ return hydrateConversationRow(getDb().prepare(`${conversationSelectSql('c')} WHERE c.id = ?`).get(id));
665
783
  }
666
784
  function listConversations(opts) {
667
785
  const db = getDb();
668
786
  const limit = opts?.limit ?? 50;
669
787
  const offset = opts?.offset ?? 0;
670
- let sql = 'SELECT * FROM conversation';
788
+ let sql = conversationSelectSql('c');
671
789
  const params = [];
672
790
  const wheres = [];
673
791
  if (opts?.agentId) {
674
- wheres.push('agent_id = ?');
675
- params.push(opts.agentId);
792
+ wheres.push(`(
793
+ c.agent_id = ?
794
+ OR EXISTS (
795
+ SELECT 1
796
+ FROM message m
797
+ WHERE m.conversation_id = c.id
798
+ AND m.bot_id = ?
799
+ )
800
+ )`);
801
+ params.push(opts.agentId, opts.agentId);
676
802
  }
677
803
  if (opts?.search) {
678
- wheres.push('(title LIKE ? OR summary LIKE ?)');
679
- params.push(`%${opts.search}%`, `%${opts.search}%`);
804
+ wheres.push(`(
805
+ coalesce(c.title, '') LIKE ?
806
+ OR coalesce(c.summary, '') LIKE ?
807
+ OR coalesce(${latestConversationSummarySql('c')}, '') LIKE ?
808
+ OR coalesce(${primaryConversationTopicTitleSql('c')}, '') LIKE ?
809
+ OR coalesce(c.project_name, '') LIKE ?
810
+ OR coalesce(c.description, '') LIKE ?
811
+ OR coalesce(c.tags, '') LIKE ?
812
+ OR coalesce(c.topics_discussed, '') LIKE ?
813
+ OR coalesce(c.tech_stack, '') LIKE ?
814
+ OR coalesce(c.category, '') LIKE ?
815
+ )`);
816
+ const needle = `%${opts.search}%`;
817
+ params.push(needle, needle, needle, needle, needle, needle, needle, needle, needle, needle);
680
818
  }
681
819
  if (wheres.length)
682
820
  sql += ' WHERE ' + wheres.join(' AND ');
683
- sql += ' ORDER BY updated_at DESC LIMIT ? OFFSET ?';
821
+ sql += ' ORDER BY c.updated_at DESC LIMIT ? OFFSET ?';
684
822
  params.push(limit, offset);
685
- return db.prepare(sql).all(...params);
823
+ return db.prepare(sql).all(...params).map((row) => hydrateConversationRow(row));
686
824
  }
687
825
  function countConversations(agentId) {
688
826
  const db = getDb();
689
827
  if (agentId) {
690
- return db.prepare('SELECT count(*) as cnt FROM conversation WHERE agent_id = ?').get(agentId).cnt;
828
+ return db.prepare(`
829
+ SELECT count(DISTINCT c.id) as cnt
830
+ FROM conversation c
831
+ WHERE c.agent_id = ?
832
+ OR EXISTS (
833
+ SELECT 1
834
+ FROM message m
835
+ WHERE m.conversation_id = c.id
836
+ AND m.bot_id = ?
837
+ )
838
+ `).get(agentId, agentId).cnt;
691
839
  }
692
840
  return db.prepare('SELECT count(*) as cnt FROM conversation').get().cnt;
693
841
  }
842
+ function countMessagesForBot(botId) {
843
+ return getDb().prepare(`
844
+ SELECT count(*) as cnt
845
+ FROM message
846
+ WHERE bot_id = ?
847
+ `).get(botId).cnt;
848
+ }
849
+ function listBotConversationActivity(botId, limit = 10) {
850
+ const db = getDb();
851
+ return db.prepare(`
852
+ SELECT
853
+ conv.*,
854
+ COALESCE((
855
+ SELECT count(*)
856
+ FROM message bm
857
+ WHERE bm.conversation_id = conv.id
858
+ AND bm.bot_id = ?
859
+ ), 0) AS bot_message_count,
860
+ (
861
+ SELECT max(bm.created_at)
862
+ FROM message bm
863
+ WHERE bm.conversation_id = conv.id
864
+ AND bm.bot_id = ?
865
+ ) AS bot_last_message_at
866
+ FROM (
867
+ ${conversationSelectSql('c')}
868
+ WHERE (
869
+ c.agent_id = ?
870
+ OR EXISTS (
871
+ SELECT 1
872
+ FROM message m
873
+ WHERE m.conversation_id = c.id
874
+ AND m.bot_id = ?
875
+ )
876
+ )
877
+ ) conv
878
+ ORDER BY COALESCE(bot_last_message_at, conv.updated_at, conv.created_at) DESC
879
+ LIMIT ?
880
+ `).all(botId, botId, botId, botId, limit).map((row) => hydrateConversationRow(row));
881
+ }
694
882
  function updateConversation(id, fields) {
695
883
  const db = getDb();
696
884
  const sets = [];
@@ -731,6 +919,8 @@ function updateConversation(id, fields) {
731
919
  processingError: ['processing_error', fields.processingError],
732
920
  botId: ['bot_id', fields.botId],
733
921
  forkedFromId: ['forked_from_id', fields.forkedFromId],
922
+ createdAt: ['created_at', fields.createdAt],
923
+ updatedAt: ['updated_at', fields.updatedAt],
734
924
  };
735
925
  for (const [key, [col, val]] of Object.entries(map)) {
736
926
  if (fields[key] !== undefined) {
@@ -748,17 +938,30 @@ function updateConversation(id, fields) {
748
938
  }
749
939
  if (sets.length === 0)
750
940
  return;
751
- sets.push("updated_at = datetime('now')");
941
+ if (fields.updatedAt === undefined) {
942
+ sets.push("updated_at = datetime('now')");
943
+ }
752
944
  vals.push(id);
753
945
  db.prepare(`UPDATE conversation SET ${sets.join(', ')} WHERE id = ?`).run(...vals);
754
946
  }
947
+ function touchConversationActivity(id, ts) {
948
+ const stamp = ts?.trim() || now();
949
+ getDb().prepare(`
950
+ UPDATE conversation
951
+ SET updated_at = CASE
952
+ WHEN datetime(updated_at) > datetime(?) THEN updated_at
953
+ ELSE ?
954
+ END
955
+ WHERE id = ?
956
+ `).run(stamp, stamp, id);
957
+ }
755
958
  function deleteConversation(id) {
756
959
  return getDb().prepare('DELETE FROM conversation WHERE id = ?').run(id).changes > 0;
757
960
  }
758
- function addMessage(conversationId, role, content, model, toolCallsJson, botId, agentName) {
961
+ function addMessage(conversationId, role, content, model, toolCallsJson, botId, agentName, createdAt) {
759
962
  const db = getDb();
760
963
  const id = newId();
761
- const ts = now();
964
+ const ts = createdAt?.trim() || now();
762
965
  // Get next sequence number
763
966
  const maxSeq = db.prepare('SELECT COALESCE(MAX(seq), 0) as m FROM message WHERE conversation_id = ?').get(conversationId);
764
967
  const seq = maxSeq.m + 1;
@@ -767,9 +970,197 @@ function addMessage(conversationId, role, content, model, toolCallsJson, botId,
767
970
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
768
971
  `).run(id, conversationId, seq, role, content, model ?? null, toolCallsJson ?? null, botId ?? null, agentName ?? null, ts);
769
972
  // Update conversation message_count and updated_at
770
- db.prepare("UPDATE conversation SET message_count = message_count + 1, updated_at = datetime('now') WHERE id = ?").run(conversationId);
973
+ db.prepare(`
974
+ UPDATE conversation
975
+ SET message_count = message_count + 1,
976
+ updated_at = CASE
977
+ WHEN datetime(updated_at) > datetime(?) THEN updated_at
978
+ ELSE ?
979
+ END
980
+ WHERE id = ?
981
+ `).run(ts, ts, conversationId);
982
+ return db.prepare('SELECT * FROM message WHERE id = ?').get(id);
983
+ }
984
+ function updateMessage(id, fields) {
985
+ const db = getDb();
986
+ const row = db.prepare('SELECT * FROM message WHERE id = ?').get(id);
987
+ if (!row)
988
+ return undefined;
989
+ const sets = [];
990
+ const vals = [];
991
+ if (fields.content !== undefined) {
992
+ sets.push('content = ?');
993
+ vals.push(fields.content);
994
+ }
995
+ if (fields.model !== undefined) {
996
+ sets.push('model = ?');
997
+ vals.push(fields.model);
998
+ }
999
+ if (fields.toolCallsJson !== undefined) {
1000
+ sets.push('tool_calls_json = ?');
1001
+ vals.push(fields.toolCallsJson);
1002
+ }
1003
+ if (fields.botId !== undefined) {
1004
+ sets.push('bot_id = ?');
1005
+ vals.push(fields.botId);
1006
+ }
1007
+ if (fields.agentName !== undefined) {
1008
+ sets.push('agent_name = ?');
1009
+ vals.push(fields.agentName);
1010
+ }
1011
+ if (fields.resultArtifact !== undefined) {
1012
+ sets.push('result_artifact = ?');
1013
+ vals.push(fields.resultArtifact);
1014
+ }
1015
+ if (fields.resultSummary !== undefined) {
1016
+ sets.push('result_summary = ?');
1017
+ vals.push(fields.resultSummary);
1018
+ }
1019
+ if (fields.resultStatus !== undefined) {
1020
+ sets.push('result_status = ?');
1021
+ vals.push(fields.resultStatus);
1022
+ }
1023
+ if (sets.length === 0)
1024
+ return row;
1025
+ vals.push(id);
1026
+ db.prepare(`UPDATE message SET ${sets.join(', ')} WHERE id = ?`).run(...vals);
771
1027
  return db.prepare('SELECT * FROM message WHERE id = ?').get(id);
772
1028
  }
1029
+ function createChatJob(input) {
1030
+ const db = getDb();
1031
+ const id = newId();
1032
+ const ts = now();
1033
+ db.prepare(`
1034
+ INSERT INTO chat_job (
1035
+ id, conversation_id, user_message_id, assistant_message_id, bot_id,
1036
+ status, request_json, created_at, updated_at
1037
+ )
1038
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
1039
+ `).run(id, input.conversationId, input.userMessageId, input.assistantMessageId, input.botId, input.status ?? 'queued', input.requestJson ?? null, ts, ts);
1040
+ return db.prepare('SELECT * FROM chat_job WHERE id = ?').get(id);
1041
+ }
1042
+ function getCliSessionEpoch(conversationId, botId) {
1043
+ return getDb().prepare(`
1044
+ SELECT *
1045
+ FROM cli_session_epoch
1046
+ WHERE conversation_id = ? AND bot_id = ?
1047
+ LIMIT 1
1048
+ `).get(conversationId, botId);
1049
+ }
1050
+ function upsertCliSessionEpoch(input) {
1051
+ const db = getDb();
1052
+ const existing = getCliSessionEpoch(input.conversationId, input.botId);
1053
+ const id = existing?.id || newId();
1054
+ const epochStartedAt = input.epochStartedAt || existing?.epoch_started_at || now();
1055
+ const lastUsedAt = input.lastUsedAt || now();
1056
+ db.prepare(`
1057
+ INSERT INTO cli_session_epoch (
1058
+ id, conversation_id, bot_id, provider, session_id, epoch_turn_count,
1059
+ last_input_tokens, last_output_tokens, epoch_started_at, last_used_at,
1060
+ reset_reason, created_at, updated_at
1061
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1062
+ ON CONFLICT(conversation_id, bot_id) DO UPDATE SET
1063
+ provider = excluded.provider,
1064
+ session_id = excluded.session_id,
1065
+ epoch_turn_count = excluded.epoch_turn_count,
1066
+ last_input_tokens = excluded.last_input_tokens,
1067
+ last_output_tokens = excluded.last_output_tokens,
1068
+ epoch_started_at = excluded.epoch_started_at,
1069
+ last_used_at = excluded.last_used_at,
1070
+ reset_reason = excluded.reset_reason,
1071
+ 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);
1074
+ }
1075
+ function deleteCliSessionEpoch(conversationId, botId) {
1076
+ return getDb().prepare(`
1077
+ DELETE FROM cli_session_epoch
1078
+ WHERE conversation_id = ? AND bot_id = ?
1079
+ `).run(conversationId, botId).changes > 0;
1080
+ }
1081
+ function getChatJob(id) {
1082
+ return getDb().prepare('SELECT * FROM chat_job WHERE id = ?').get(id);
1083
+ }
1084
+ function getLatestConversationChatJob(conversationId) {
1085
+ return getDb().prepare(`
1086
+ SELECT *
1087
+ FROM chat_job
1088
+ WHERE conversation_id = ?
1089
+ ORDER BY datetime(created_at) DESC, rowid DESC
1090
+ LIMIT 1
1091
+ `).get(conversationId);
1092
+ }
1093
+ function listQueuedChatJobs(limit = 50) {
1094
+ return getDb().prepare(`
1095
+ SELECT *
1096
+ FROM chat_job
1097
+ WHERE status = 'queued'
1098
+ ORDER BY datetime(created_at) ASC, rowid ASC
1099
+ LIMIT ?
1100
+ `).all(limit);
1101
+ }
1102
+ function countRunningChatJobs() {
1103
+ return getDb().prepare(`
1104
+ SELECT count(*) AS cnt
1105
+ FROM chat_job
1106
+ WHERE status = 'running'
1107
+ `).get().cnt;
1108
+ }
1109
+ function updateChatJob(id, fields) {
1110
+ const db = getDb();
1111
+ const row = db.prepare('SELECT * FROM chat_job WHERE id = ?').get(id);
1112
+ if (!row)
1113
+ return undefined;
1114
+ const sets = [];
1115
+ const vals = [];
1116
+ if (fields.status !== undefined) {
1117
+ sets.push('status = ?');
1118
+ vals.push(fields.status);
1119
+ }
1120
+ if (fields.error !== undefined) {
1121
+ sets.push('error = ?');
1122
+ vals.push(fields.error);
1123
+ }
1124
+ if (fields.requestJson !== undefined) {
1125
+ sets.push('request_json = ?');
1126
+ vals.push(fields.requestJson);
1127
+ }
1128
+ if (fields.startedAt !== undefined) {
1129
+ sets.push('started_at = ?');
1130
+ vals.push(fields.startedAt);
1131
+ }
1132
+ if (fields.completedAt !== undefined) {
1133
+ sets.push('completed_at = ?');
1134
+ vals.push(fields.completedAt);
1135
+ }
1136
+ if (fields.cancelledAt !== undefined) {
1137
+ sets.push('cancelled_at = ?');
1138
+ vals.push(fields.cancelledAt);
1139
+ }
1140
+ if (sets.length === 0)
1141
+ return row;
1142
+ sets.push('updated_at = ?');
1143
+ vals.push(now());
1144
+ vals.push(id);
1145
+ db.prepare(`UPDATE chat_job SET ${sets.join(', ')} WHERE id = ?`).run(...vals);
1146
+ return db.prepare('SELECT * FROM chat_job WHERE id = ?').get(id);
1147
+ }
1148
+ function markRunningChatJobsInterrupted(reason = 'Interrupted when app closed') {
1149
+ const db = getDb();
1150
+ const ts = now();
1151
+ const result = db.prepare(`
1152
+ UPDATE chat_job
1153
+ SET status = 'failed',
1154
+ error = CASE
1155
+ WHEN error IS NULL OR trim(error) = '' THEN ?
1156
+ ELSE error
1157
+ END,
1158
+ completed_at = COALESCE(completed_at, ?),
1159
+ updated_at = ?
1160
+ WHERE status = 'running'
1161
+ `).run(reason, ts, ts);
1162
+ return result.changes;
1163
+ }
773
1164
  function createMessageActivity(activity) {
774
1165
  const db = getDb();
775
1166
  const id = newId();
@@ -829,6 +1220,12 @@ function getMessages(conversationId, opts) {
829
1220
  return db.prepare('SELECT * FROM message WHERE conversation_id = ? ORDER BY seq ASC LIMIT ? OFFSET ?')
830
1221
  .all(conversationId, limit, offset);
831
1222
  }
1223
+ function getMessagesInRange(conversationId, startSeq, endSeq) {
1224
+ const db = getDb();
1225
+ const start = Math.max(1, Math.floor(startSeq));
1226
+ const end = Math.max(start, Math.floor(endSeq));
1227
+ return db.prepare('SELECT * FROM message WHERE conversation_id = ? AND seq >= ? AND seq <= ? ORDER BY seq ASC').all(conversationId, start, end);
1228
+ }
832
1229
  function getLastMessages(conversationId, count) {
833
1230
  const db = getDb();
834
1231
  return db.prepare(`
@@ -1198,11 +1595,6 @@ function listMemoryFacts(opts) {
1198
1595
  function deleteMemoryFact(id) {
1199
1596
  return getDb().prepare('DELETE FROM memory_fact WHERE id = ?').run(id).changes > 0;
1200
1597
  }
1201
- /**
1202
- * Get user-profile facts (person entities) for injection into the system prompt.
1203
- * Returns high-confidence facts about people (address, preferences, etc.)
1204
- * limited to ~500 tokens worth of content.
1205
- */
1206
1598
  function getUserProfileFacts(agentId, limit = 15) {
1207
1599
  const db = getDb();
1208
1600
  return db.prepare(`
@@ -1398,6 +1790,19 @@ function getAllSettings() {
1398
1790
  function deleteSetting(key) {
1399
1791
  return getDb().prepare('DELETE FROM settings WHERE key = ?').run(key).changes > 0;
1400
1792
  }
1793
+ function purgeLegacyExtractionDataOnce() {
1794
+ if (getSetting('legacy_extraction_cleanup_v1') === 'done')
1795
+ return false;
1796
+ const db = getDb();
1797
+ const tx = db.transaction(() => {
1798
+ for (const table of ['entity_relationship', 'entity', 'memory_fact', 'decision', 'action_item']) {
1799
+ db.prepare(`DELETE FROM ${table}`).run();
1800
+ }
1801
+ setSetting('legacy_extraction_cleanup_v1', 'done');
1802
+ });
1803
+ tx();
1804
+ return true;
1805
+ }
1401
1806
  const AUTH_SESSION_KEY = 'auth.session';
1402
1807
  function defaultOrchestrationPolicy() {
1403
1808
  return {
@@ -1852,6 +2257,36 @@ function inferTaskTypeForAgent(agent, requested) {
1852
2257
  return explicit;
1853
2258
  return roleClassToTaskType(agent?.role_class) || roleClassToTaskType(agent?.role_label) || null;
1854
2259
  }
2260
+ function buildQueuedTaskContext(task) {
2261
+ const lines = [
2262
+ 'NEXT TASK CONTEXT',
2263
+ `Title: ${task.title}`,
2264
+ task.task_type ? `Task type: ${task.task_type}` : null,
2265
+ task.details?.trim() ? `Details: ${task.details.trim()}` : null,
2266
+ task.success_criteria?.trim() ? `Success criteria: ${task.success_criteria.trim()}` : null,
2267
+ ].filter(Boolean);
2268
+ return lines.join('\n');
2269
+ }
2270
+ function injectWorkerHandoffIntoPrompt(existingPrompt, handoffText, nextTask) {
2271
+ const taskReplacement = [
2272
+ 'Task:',
2273
+ 'PREVIOUS WORKER HANDOFF',
2274
+ handoffText,
2275
+ '',
2276
+ buildQueuedTaskContext(nextTask),
2277
+ '',
2278
+ 'RUNTIME CONTEXT',
2279
+ ].join('\n');
2280
+ if (existingPrompt.includes('\nRUNTIME CONTEXT\n')) {
2281
+ return existingPrompt.replace(/Task:\s*[\s\S]*?\nRUNTIME CONTEXT\n/, `${taskReplacement}\n`);
2282
+ }
2283
+ return [
2284
+ 'PREVIOUS WORKER HANDOFF',
2285
+ handoffText,
2286
+ '',
2287
+ buildQueuedTaskContext(nextTask),
2288
+ ].join('\n');
2289
+ }
1855
2290
  function normalizeArtifactRow(row) {
1856
2291
  return {
1857
2292
  id: Number(row.id),
@@ -1982,6 +2417,44 @@ function resolveAllowedWorkerNamesForInsertion(currentTask) {
1982
2417
  allowed.add(previous.owner_name.trim().toLowerCase());
1983
2418
  return Array.from(allowed);
1984
2419
  }
2420
+ function resolvePreviousWorkerName(currentTask) {
2421
+ const db = getDb();
2422
+ const previous = currentTask.project_id
2423
+ ? db.prepare(`
2424
+ SELECT owner_name
2425
+ FROM (
2426
+ SELECT owner_name, created_order, internal_only FROM todo_active WHERE project_id = ?
2427
+ UNION ALL
2428
+ SELECT owner_name, created_order, internal_only FROM todo_completed WHERE project_id = ?
2429
+ )
2430
+ WHERE created_order < ? AND COALESCE(internal_only, 0) = 0 AND owner_name IS NOT NULL
2431
+ ORDER BY created_order DESC
2432
+ LIMIT 1
2433
+ `).get(currentTask.project_id, currentTask.project_id, currentTask.position)
2434
+ : db.prepare(`
2435
+ SELECT owner_name
2436
+ FROM (
2437
+ SELECT owner_name, created_order, internal_only FROM todo_active WHERE project_id IS NULL
2438
+ UNION ALL
2439
+ SELECT owner_name, created_order, internal_only FROM todo_completed WHERE project_id IS NULL
2440
+ )
2441
+ WHERE created_order < ? AND COALESCE(internal_only, 0) = 0 AND owner_name IS NOT NULL
2442
+ ORDER BY created_order DESC
2443
+ LIMIT 1
2444
+ `).get(currentTask.position);
2445
+ return previous?.owner_name?.trim() || null;
2446
+ }
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);
2457
+ }
1985
2458
  function normalizeArtifactInput(ref) {
1986
2459
  if (typeof ref === 'string') {
1987
2460
  const trimmed = ref.trim();
@@ -2424,10 +2897,49 @@ function completeTodoTaskByWorker(taskId, params) {
2424
2897
  }
2425
2898
  }
2426
2899
  else {
2427
- const nextWorkerName = completedTask.next_worker_name?.trim();
2428
- if (nextWorkerName) {
2429
- const nextRow = completedTask.next_worker_bot_id
2430
- ? db.prepare(`
2900
+ if (shouldAutoCreateQaFixTask(completedTask, outputSummary, handoffPrompt)) {
2901
+ const previousWorkerName = resolvePreviousWorkerName(completedTask);
2902
+ const previousWorker = resolveAgentByName(previousWorkerName);
2903
+ if (previousWorkerName && previousWorker) {
2904
+ const participants = [previousWorker.name];
2905
+ insertedTask = insertTodoTaskTx(db, {
2906
+ projectId: completedTask.project_id,
2907
+ conversationId: completedTask.conversation_id,
2908
+ title: 'Fix QA findings',
2909
+ details: handoffPrompt || outputSummary || 'Address the QA findings and update the implementation.',
2910
+ participants,
2911
+ successCriteria: 'Address the QA findings and return the updated work for re-QA.',
2912
+ ownerBotId: previousWorker.id,
2913
+ ownerName: previousWorker.name,
2914
+ taskType: inferTaskTypeForAgent(previousWorker, null),
2915
+ profileName: completedTask.profile_name || 'Programming',
2916
+ nextWorkerBotId: currentTask.owner_bot_id,
2917
+ nextWorkerName: currentTask.owner_name,
2918
+ nextWorkerRole: actorBot?.role_label || actorBot?.role_class || null,
2919
+ taskPrompt: handoffPrompt || outputSummary || 'Address the QA findings and keep the approved parts unchanged.',
2920
+ artifactIds,
2921
+ insertAfterTaskId: completedTask.id,
2922
+ actor: params.actor,
2923
+ });
2924
+ logTodoAuditTx(db, insertedTask.id, 'active', 'worker_insert', params.actor, {
2925
+ insertedAfterTaskId: completedTask.id,
2926
+ requestedWorkerName: previousWorker.name,
2927
+ autoCreatedFromQaFailure: true,
2928
+ });
2929
+ }
2930
+ else {
2931
+ returnedToOrchestrator = true;
2932
+ logTodoAuditTx(db, taskId, 'completed', 'return_to_orchestrator', params.actor, {
2933
+ handoffPrompt,
2934
+ missingPreviousWorkerForQaFix: true,
2935
+ });
2936
+ }
2937
+ }
2938
+ else {
2939
+ const nextWorkerName = completedTask.next_worker_name?.trim();
2940
+ if (nextWorkerName) {
2941
+ const nextRow = completedTask.next_worker_bot_id
2942
+ ? db.prepare(`
2431
2943
  SELECT * FROM todo_active
2432
2944
  WHERE conversation_id = ?
2433
2945
  AND id <> ?
@@ -2437,7 +2949,7 @@ function completeTodoTaskByWorker(taskId, params) {
2437
2949
  ORDER BY created_order ASC, id ASC
2438
2950
  LIMIT 1
2439
2951
  `).get(completedTask.conversation_id, completedTask.id, completedTask.next_worker_bot_id, completedTask.created_order)
2440
- : db.prepare(`
2952
+ : db.prepare(`
2441
2953
  SELECT * FROM todo_active
2442
2954
  WHERE conversation_id = ?
2443
2955
  AND id <> ?
@@ -2447,21 +2959,15 @@ function completeTodoTaskByWorker(taskId, params) {
2447
2959
  ORDER BY created_order ASC, id ASC
2448
2960
  LIMIT 1
2449
2961
  `).get(completedTask.conversation_id, completedTask.id, nextWorkerName, completedTask.created_order);
2450
- if (nextRow) {
2451
- const nextTask = normalizeTodoRow(nextRow, 'active');
2452
- const handoffText = handoffPrompt?.trim()
2453
- || outputSummary?.trim()
2454
- || `Continue from the completed work summary: ${completedTask.title}`;
2455
- const existingPrompt = nextTask.task_prompt?.trim() || nextTask.details || nextTask.title;
2456
- const mergedArtifactIds = Array.from(new Set([...(nextTask.artifact_ids || []), ...artifactIds]));
2457
- const nextPrompt = [
2458
- 'PREVIOUS WORKER HANDOFF',
2459
- handoffText,
2460
- '',
2461
- 'ORIGINAL ASSIGNED TODO',
2462
- existingPrompt,
2463
- ].join('\n');
2464
- db.prepare(`
2962
+ if (nextRow) {
2963
+ const nextTask = normalizeTodoRow(nextRow, 'active');
2964
+ const handoffText = handoffPrompt?.trim()
2965
+ || outputSummary?.trim()
2966
+ || `Continue from the completed work summary: ${completedTask.title}`;
2967
+ const existingPrompt = nextTask.task_prompt?.trim() || nextTask.details || nextTask.title;
2968
+ const mergedArtifactIds = Array.from(new Set([...(nextTask.artifact_ids || []), ...artifactIds]));
2969
+ const nextPrompt = injectWorkerHandoffIntoPrompt(existingPrompt, handoffText, nextTask);
2970
+ db.prepare(`
2465
2971
  UPDATE todo_active
2466
2972
  SET task_prompt = ?,
2467
2973
  artifact_ids_json = ?,
@@ -2469,26 +2975,27 @@ function completeTodoTaskByWorker(taskId, params) {
2469
2975
  version = version + 1
2470
2976
  WHERE id = ?
2471
2977
  `).run(nextPrompt, JSON.stringify(mergedArtifactIds), now(), nextTask.id);
2472
- handedOffTask = normalizeTodoRow(db.prepare('SELECT * FROM todo_active WHERE id = ?').get(nextTask.id), 'active');
2473
- logTodoAuditTx(db, nextTask.id, 'active', 'edit', params.actor, {
2474
- previousTaskId: completedTask.id,
2475
- receivedHandoff: true,
2476
- });
2978
+ handedOffTask = normalizeTodoRow(db.prepare('SELECT * FROM todo_active WHERE id = ?').get(nextTask.id), 'active');
2979
+ logTodoAuditTx(db, nextTask.id, 'active', 'edit', params.actor, {
2980
+ previousTaskId: completedTask.id,
2981
+ receivedHandoff: true,
2982
+ });
2983
+ }
2984
+ else {
2985
+ returnedToOrchestrator = true;
2986
+ logTodoAuditTx(db, taskId, 'completed', 'return_to_orchestrator', params.actor, {
2987
+ handoffPrompt,
2988
+ missingQueuedNextWorker: nextWorkerName,
2989
+ });
2990
+ }
2477
2991
  }
2478
2992
  else {
2479
2993
  returnedToOrchestrator = true;
2480
2994
  logTodoAuditTx(db, taskId, 'completed', 'return_to_orchestrator', params.actor, {
2481
2995
  handoffPrompt,
2482
- missingQueuedNextWorker: nextWorkerName,
2483
2996
  });
2484
2997
  }
2485
2998
  }
2486
- else {
2487
- returnedToOrchestrator = true;
2488
- logTodoAuditTx(db, taskId, 'completed', 'return_to_orchestrator', params.actor, {
2489
- handoffPrompt,
2490
- });
2491
- }
2492
2999
  }
2493
3000
  return {
2494
3001
  completedTask,
@@ -3040,13 +3547,20 @@ function listTopicConversations(topicId, opts) {
3040
3547
  const db = getDb();
3041
3548
  const limit = opts?.limit ?? 50;
3042
3549
  const offset = opts?.offset ?? 0;
3043
- return db.prepare(`
3044
- SELECT DISTINCT c.* FROM conversation c
3045
- INNER JOIN topic_segment ts ON ts.conversation_id = c.id
3550
+ const rows = db.prepare(`
3551
+ SELECT DISTINCT conv.*
3552
+ FROM (
3553
+ ${conversationSelectSql('c')}
3554
+ ) conv
3555
+ INNER JOIN topic_segment ts ON ts.conversation_id = conv.id
3046
3556
  WHERE ts.topic_id = ?
3047
- ORDER BY c.updated_at DESC
3557
+ ORDER BY datetime(COALESCE(conv.effective_updated_at, conv.updated_at)) DESC,
3558
+ datetime(COALESCE(conv.effective_created_at, conv.created_at)) DESC
3048
3559
  LIMIT ? OFFSET ?
3049
3560
  `).all(topicId, limit, offset);
3561
+ return rows
3562
+ .map((row) => hydrateConversationRow(row))
3563
+ .filter((row) => !!row);
3050
3564
  }
3051
3565
  function getPrimaryTopicIdForConversation(conversationId) {
3052
3566
  const db = getDb();
@@ -3090,24 +3604,43 @@ function listTopicConversationsWithPreviews(topicId, opts) {
3090
3604
  const limit = opts?.limit ?? 50;
3091
3605
  const offset = opts?.offset ?? 0;
3092
3606
  // Primary: find conversations via topic_segment links
3093
- let convs = db.prepare(`
3094
- SELECT DISTINCT c.* FROM conversation c
3095
- INNER JOIN topic_segment ts ON ts.conversation_id = c.id
3607
+ const rawConvs = db.prepare(`
3608
+ SELECT DISTINCT conv.*
3609
+ FROM (
3610
+ ${conversationSelectSql('c')}
3611
+ ) conv
3612
+ INNER JOIN topic_segment ts ON ts.conversation_id = conv.id
3096
3613
  WHERE ts.topic_id = ?
3097
- ORDER BY c.updated_at DESC
3614
+ ORDER BY datetime(COALESCE(conv.effective_updated_at, conv.updated_at)) DESC,
3615
+ datetime(COALESCE(conv.effective_created_at, conv.created_at)) DESC
3098
3616
  LIMIT ? OFFSET ?
3099
3617
  `).all(topicId, limit, offset);
3618
+ const convs = rawConvs
3619
+ .map((row) => hydrateConversationRow(row))
3620
+ .filter((row) => !!row);
3100
3621
  // No fallback — topics with no segments have no conversations yet.
3101
3622
  // Backfill/import must create topic_segment rows explicitly.
3102
3623
  return convs.map(conv => {
3624
+ const latestJob = getLatestConversationChatJob(conv.id);
3625
+ const jobStatus = latestJob?.status === 'running'
3626
+ ? 'working'
3627
+ : latestJob?.status === 'completed'
3628
+ ? 'done'
3629
+ : latestJob?.status ?? null;
3103
3630
  // Get latest summary text
3104
- const summary = db.prepare('SELECT summary_text FROM conversation_summary WHERE conversation_id = ? ORDER BY turn_range_end DESC LIMIT 1').get(conv.id);
3631
+ const summary = db.prepare(`SELECT summary_text
3632
+ FROM conversation_summary
3633
+ WHERE conversation_id = ?
3634
+ AND coalesce(summary_kind, 'rolling') = 'rolling'
3635
+ ORDER BY turn_range_end DESC, created_at DESC
3636
+ LIMIT 1`).get(conv.id);
3105
3637
  // Get first user message, truncated to 200 chars
3106
3638
  const firstMsg = db.prepare("SELECT content FROM message WHERE conversation_id = ? AND role = 'user' ORDER BY seq ASC LIMIT 1").get(conv.id);
3107
3639
  return {
3108
3640
  ...conv,
3109
3641
  summary_text: summary?.summary_text ?? null,
3110
3642
  first_user_message: firstMsg ? firstMsg.content.slice(0, 200) : null,
3643
+ job_status: jobStatus,
3111
3644
  };
3112
3645
  });
3113
3646
  }
@@ -3159,6 +3692,10 @@ function updateWorkflowTemplate(id, fields) {
3159
3692
  const db = getDb();
3160
3693
  const sets = [];
3161
3694
  const vals = [];
3695
+ if (fields.projectId !== undefined) {
3696
+ sets.push('project_id = ?');
3697
+ vals.push(fields.projectId);
3698
+ }
3162
3699
  if (fields.name !== undefined) {
3163
3700
  sets.push('name = ?');
3164
3701
  vals.push(fields.name);