@oar-sh/oar 0.9.0

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 (293) hide show
  1. package/.github/extensions/web-relay/README.md +190 -0
  2. package/.github/extensions/web-relay/extension.mjs +972 -0
  3. package/.github/extensions/web-relay/model-api/model-switching.mjs +338 -0
  4. package/.github/extensions/web-relay/polling/heartbeat.mjs +55 -0
  5. package/.github/extensions/web-relay/polling/polling-loop.mjs +1105 -0
  6. package/.github/extensions/web-relay/runtime/api-client.mjs +34 -0
  7. package/.github/extensions/web-relay/runtime/banner-state.mjs +126 -0
  8. package/.github/extensions/web-relay/runtime/config-loader.mjs +104 -0
  9. package/.github/extensions/web-relay/runtime/debug-log.mjs +27 -0
  10. package/.github/extensions/web-relay/runtime/send-and-wait-errors.mjs +170 -0
  11. package/.github/extensions/web-relay/runtime/session-binding.mjs +78 -0
  12. package/.github/extensions/web-relay/runtime/session-io.mjs +349 -0
  13. package/.github/extensions/web-relay/runtime/session-join-retry.mjs +44 -0
  14. package/.github/extensions/web-relay/runtime/session-registry.mjs +29 -0
  15. package/.github/extensions/web-relay/runtime/session-runtime-manager.mjs +156 -0
  16. package/.github/extensions/web-relay/runtime/session-sync-bridge.mjs +74 -0
  17. package/.github/extensions/web-relay/runtime/worker-websocket-link.mjs +325 -0
  18. package/.github/extensions/web-relay/runtime/workspace-root.mjs +55 -0
  19. package/.github/extensions/web-relay/server-lifecycle/managed-server.mjs +386 -0
  20. package/.github/extensions/web-relay/skills/prompt-context.mjs +160 -0
  21. package/.github/extensions/web-relay/skills/question-bridge.mjs +141 -0
  22. package/.github/extensions/web-relay/skills/question-routing-hooks.mjs +463 -0
  23. package/.github/extensions/web-relay/skills/reasoning-stream.mjs +285 -0
  24. package/.github/extensions/web-relay/skills/subagent-lifecycle.mjs +345 -0
  25. package/.github/extensions/web-relay/skills/tool-activity.mjs +243 -0
  26. package/.github/extensions/web-relay/utils/process-utils.mjs +25 -0
  27. package/.github/extensions/web-relay/utils/token-generation.mjs +58 -0
  28. package/CHANGELOG.md +25 -0
  29. package/LICENSE +21 -0
  30. package/README.md +717 -0
  31. package/bin/bat/oar.bat +46 -0
  32. package/bin/oar.js +665 -0
  33. package/package.json +68 -0
  34. package/server/README.md +1379 -0
  35. package/server/claude-worker/claude-attachments.mjs +134 -0
  36. package/server/claude-worker/claude-preview-tool.mjs +57 -0
  37. package/server/claude-worker/claude-sdk-adapter.mjs +420 -0
  38. package/server/claude-worker/claude-session-process.mjs +2391 -0
  39. package/server/claude-worker/claude-session-test-harness.mjs +236 -0
  40. package/server/claude-worker/claude-session-worker.mjs +186 -0
  41. package/server/claude-worker/claude-transcript-relocator.mjs +189 -0
  42. package/server/claude-worker/claude-turn-publisher.mjs +291 -0
  43. package/server/claude-worker/fixtures/workflow-run-record.json +1 -0
  44. package/server/claude-worker/sdk-message-normalizer.mjs +629 -0
  45. package/server/claude-worker/workflow-progress-digest.mjs +269 -0
  46. package/server/copilot-sdk-runtime.mjs +126 -0
  47. package/server/copilot-worker/copilot-attachments.mjs +97 -0
  48. package/server/copilot-worker/copilot-byok-provider.mjs +121 -0
  49. package/server/copilot-worker/copilot-continuation-signals.mjs +349 -0
  50. package/server/copilot-worker/copilot-plan-board.mjs +110 -0
  51. package/server/copilot-worker/copilot-prompt-context.mjs +86 -0
  52. package/server/copilot-worker/copilot-question-bridge.mjs +216 -0
  53. package/server/copilot-worker/copilot-sdk-adapter.mjs +494 -0
  54. package/server/copilot-worker/copilot-sdk-event-normalizer.mjs +800 -0
  55. package/server/copilot-worker/copilot-sdk-session-process.mjs +1729 -0
  56. package/server/copilot-worker/copilot-sdk-session-worker.mjs +167 -0
  57. package/server/copilot-worker/copilot-sdk-test-harness.mjs +329 -0
  58. package/server/copilot-worker/fixtures/abort-turn.json +588 -0
  59. package/server/copilot-worker/fixtures/ask-user-turn.json +1154 -0
  60. package/server/copilot-worker/fixtures/background-timer-continuation.json +445 -0
  61. package/server/copilot-worker/fixtures/background-timer-turn.json +497 -0
  62. package/server/copilot-worker/fixtures/happy-turn.json +566 -0
  63. package/server/copilot-worker/fixtures/quota-turn.json +647 -0
  64. package/server/copilot-worker/fixtures/reasoning-turn.json +625 -0
  65. package/server/copilot-worker/fixtures/resume-turn.json +700 -0
  66. package/server/copilot-worker/fixtures/subagent-turn.json +1378 -0
  67. package/server/copilot-worker/fixtures/tool-permission-turn.json +1293 -0
  68. package/server/cursor-worker/cursor-ask-user-tool.mjs +83 -0
  69. package/server/cursor-worker/cursor-attachments.mjs +88 -0
  70. package/server/cursor-worker/cursor-context-usage.mjs +85 -0
  71. package/server/cursor-worker/cursor-preview-tool.mjs +44 -0
  72. package/server/cursor-worker/cursor-sdk-adapter.mjs +554 -0
  73. package/server/cursor-worker/cursor-session-worker.mjs +145 -0
  74. package/server/cursor-worker/cursor-subagent-roster.mjs +81 -0
  75. package/server/cursor-worker/cursor-turn-runner.mjs +717 -0
  76. package/server/cursor-worker/sdk-message-normalizer.mjs +485 -0
  77. package/server/db-schema.mjs +932 -0
  78. package/server/features.mjs +106 -0
  79. package/server/grok-worker/acp-client.mjs +434 -0
  80. package/server/grok-worker/acp-host-services.mjs +277 -0
  81. package/server/grok-worker/grok-context-usage.mjs +77 -0
  82. package/server/grok-worker/grok-sdk-adapter.mjs +334 -0
  83. package/server/grok-worker/grok-session-worker.mjs +145 -0
  84. package/server/grok-worker/grok-turn-runner.mjs +523 -0
  85. package/server/grok-worker/sdk-message-normalizer.mjs +311 -0
  86. package/server/migrations/0001-add-structured-answer.mjs +80 -0
  87. package/server/migrations/0002-recent-workspace-roots-path-key.mjs +121 -0
  88. package/server/migrations/0003-push-subscriptions.mjs +87 -0
  89. package/server/public/app/action-confirmations.js +267 -0
  90. package/server/public/app/activity-replay-state.mjs +149 -0
  91. package/server/public/app/api-client.js +1082 -0
  92. package/server/public/app/ask-user-view.js +545 -0
  93. package/server/public/app/attachments-view.js +2112 -0
  94. package/server/public/app/auto-compact-window-options.mjs +59 -0
  95. package/server/public/app/background-tasks-view.mjs +494 -0
  96. package/server/public/app/bootstrap.js +4347 -0
  97. package/server/public/app/claude-auth-ui.js +394 -0
  98. package/server/public/app/claude-thinking-options.mjs +38 -0
  99. package/server/public/app/cli-install-ui.js +455 -0
  100. package/server/public/app/code-copy.mjs +85 -0
  101. package/server/public/app/composer-attachment-cache.mjs +151 -0
  102. package/server/public/app/composer-control-state.mjs +107 -0
  103. package/server/public/app/composer-paste.mjs +132 -0
  104. package/server/public/app/context-tier-options.mjs +68 -0
  105. package/server/public/app/context-usage-view.mjs +338 -0
  106. package/server/public/app/conversation-draft-timestamp-utils.mjs +11 -0
  107. package/server/public/app/conversation-preferences.mjs +91 -0
  108. package/server/public/app/conversation-provider-indicator.mjs +88 -0
  109. package/server/public/app/conversation-view.js +2898 -0
  110. package/server/public/app/cwd-menu-placement.mjs +85 -0
  111. package/server/public/app/cwd-picker.js +605 -0
  112. package/server/public/app/emoji-view.js +101 -0
  113. package/server/public/app/external-link-policy.mjs +80 -0
  114. package/server/public/app/file-preview-navigation.mjs +51 -0
  115. package/server/public/app/font-scaling.js +262 -0
  116. package/server/public/app/git-changes-view.js +339 -0
  117. package/server/public/app/git-diff-model.mjs +109 -0
  118. package/server/public/app/grok-auth-ui.js +387 -0
  119. package/server/public/app/image-downscale.mjs +174 -0
  120. package/server/public/app/in-flight-snapshot.mjs +40 -0
  121. package/server/public/app/infinite-loader.js +164 -0
  122. package/server/public/app/journal-view.js +1141 -0
  123. package/server/public/app/known-cwd-options.mjs +41 -0
  124. package/server/public/app/launch-cwd-path.mjs +23 -0
  125. package/server/public/app/live-message-dedupe.mjs +57 -0
  126. package/server/public/app/message-search-view.js +242 -0
  127. package/server/public/app/model-selector-options.mjs +31 -0
  128. package/server/public/app/model-vision-support.mjs +56 -0
  129. package/server/public/app/new-conversation-model-choice.mjs +67 -0
  130. package/server/public/app/plan-usage-view.mjs +307 -0
  131. package/server/public/app/preview-cards.mjs +273 -0
  132. package/server/public/app/preview-command.mjs +103 -0
  133. package/server/public/app/push-settings.js +370 -0
  134. package/server/public/app/pwa-install.js +456 -0
  135. package/server/public/app/question-schema-view.mjs +71 -0
  136. package/server/public/app/reasoning-effort-labels.mjs +42 -0
  137. package/server/public/app/relay-board-view.js +117 -0
  138. package/server/public/app/relay-dot-state.mjs +69 -0
  139. package/server/public/app/relay-error-ctas.mjs +52 -0
  140. package/server/public/app/relay-prompt-sanitizer.mjs +114 -0
  141. package/server/public/app/relay-thoughts.mjs +98 -0
  142. package/server/public/app/relay-watchdog-policy.mjs +29 -0
  143. package/server/public/app/repo-browser-preferences.mjs +79 -0
  144. package/server/public/app/repo-browser-tree-state.mjs +97 -0
  145. package/server/public/app/router.js +657 -0
  146. package/server/public/app/selection-guard.mjs +93 -0
  147. package/server/public/app/settings-modal.js +1484 -0
  148. package/server/public/app/settings-tabs.js +129 -0
  149. package/server/public/app/slash-autocomplete.mjs +188 -0
  150. package/server/public/app/slash-commands.mjs +183 -0
  151. package/server/public/app/socket-handlers.js +703 -0
  152. package/server/public/app/status-store.mjs +242 -0
  153. package/server/public/app/status-view.mjs +315 -0
  154. package/server/public/app/store.js +1454 -0
  155. package/server/public/app/stream-state.mjs +73 -0
  156. package/server/public/app/streaming-dom-patch.mjs +23 -0
  157. package/server/public/app/sync-outbox.mjs +208 -0
  158. package/server/public/app/tap-actions.js +62 -0
  159. package/server/public/app/thread-order.mjs +54 -0
  160. package/server/public/app/tmux-inspector-view.js +422 -0
  161. package/server/public/app/transcript-separators.mjs +282 -0
  162. package/server/public/app-icon-192.png +0 -0
  163. package/server/public/app-icon-512.png +0 -0
  164. package/server/public/app-icon.svg +4 -0
  165. package/server/public/favicon.ico +0 -0
  166. package/server/public/index.html +5743 -0
  167. package/server/public/manifest.webmanifest +32 -0
  168. package/server/public/sw.js +307 -0
  169. package/server/relay-exit-codes.mjs +4 -0
  170. package/server/relay-self-restart.mjs +170 -0
  171. package/server/relay-tools.md +27 -0
  172. package/server/relay.mjs +1026 -0
  173. package/server/repositories/image-conversation-repository.mjs +317 -0
  174. package/server/repositories/message-repository.mjs +434 -0
  175. package/server/repositories/question-repository.mjs +161 -0
  176. package/server/repositories/session-repository.mjs +320 -0
  177. package/server/repositories/share-visibility-statements.mjs +24 -0
  178. package/server/routes/ask-user-routes.mjs +292 -0
  179. package/server/routes/cache-routes.mjs +44 -0
  180. package/server/routes/git-routes.mjs +92 -0
  181. package/server/routes/messages-routes-test-harness.mjs +111 -0
  182. package/server/routes/messages-routes.mjs +7107 -0
  183. package/server/routes/preview-routes.mjs +108 -0
  184. package/server/routes/push-routes.mjs +59 -0
  185. package/server/routes/relay-board-routes.mjs +268 -0
  186. package/server/routes/sessions-routes.mjs +6150 -0
  187. package/server/server-runtime.mjs +7463 -0
  188. package/server/server.js +24 -0
  189. package/server/services/ask-user-routing-service.mjs +117 -0
  190. package/server/services/cache-rebuild-service.mjs +236 -0
  191. package/server/services/claude-auth-service.mjs +578 -0
  192. package/server/services/claude-context-usage.mjs +240 -0
  193. package/server/services/claude-session-root-service.mjs +228 -0
  194. package/server/services/cli-install-service.mjs +1152 -0
  195. package/server/services/cli-process-runner.mjs +181 -0
  196. package/server/services/cloudflared-tunnel-service.mjs +358 -0
  197. package/server/services/context-snapshot-service.mjs +499 -0
  198. package/server/services/context-usage-view.mjs +160 -0
  199. package/server/services/conversation-preferences-service.mjs +208 -0
  200. package/server/services/cursor-dashboard-usage.mjs +217 -0
  201. package/server/services/cursor-session-root-service.mjs +62 -0
  202. package/server/services/delete-archive-service.mjs +154 -0
  203. package/server/services/drives-path-helpers.mjs +66 -0
  204. package/server/services/fixtures/claude-auth-stub.sh +62 -0
  205. package/server/services/fixtures/cli-install-stub.sh +75 -0
  206. package/server/services/fixtures/grok-stub.sh +86 -0
  207. package/server/services/generated-image-cleanup-service.mjs +114 -0
  208. package/server/services/git-changes-service.mjs +195 -0
  209. package/server/services/github-billing-usage.mjs +133 -0
  210. package/server/services/grok-auth-service.mjs +593 -0
  211. package/server/services/grok-billing-usage.mjs +120 -0
  212. package/server/services/image-context-service.mjs +106 -0
  213. package/server/services/image-operation-service.mjs +135 -0
  214. package/server/services/keyed-mutex-service.mjs +82 -0
  215. package/server/services/mime-sniffer.mjs +118 -0
  216. package/server/services/oar-cli-helpers.mjs +109 -0
  217. package/server/services/oar-state-migration-service.mjs +243 -0
  218. package/server/services/plan-usage-claude.mjs +387 -0
  219. package/server/services/plan-usage-contract.mjs +271 -0
  220. package/server/services/plan-usage-copilot.mjs +421 -0
  221. package/server/services/plan-usage-cursor.mjs +566 -0
  222. package/server/services/plan-usage-grok.mjs +436 -0
  223. package/server/services/plan-usage-service.mjs +487 -0
  224. package/server/services/preview-health-probe.mjs +82 -0
  225. package/server/services/preview-proxy-server.mjs +498 -0
  226. package/server/services/preview-registry-service.mjs +457 -0
  227. package/server/services/preview-static-handler.mjs +237 -0
  228. package/server/services/provider-model-selection.mjs +91 -0
  229. package/server/services/provider-reasoning-effort.mjs +94 -0
  230. package/server/services/push-active-device-service.mjs +57 -0
  231. package/server/services/push-dispatch-service.mjs +459 -0
  232. package/server/services/rate-limit-service.mjs +63 -0
  233. package/server/services/relay-bridge-owner-service.mjs +84 -0
  234. package/server/services/relay-cli-launcher-service.mjs +209 -0
  235. package/server/services/relay-prompt-sanitizer.mjs +137 -0
  236. package/server/services/relay-queue-gate-service.mjs +38 -0
  237. package/server/services/relay-restart-orchestrator-service.mjs +935 -0
  238. package/server/services/relay-singleton-guard.mjs +124 -0
  239. package/server/services/safe-served-content.mjs +57 -0
  240. package/server/services/sdk-session-import-service.mjs +271 -0
  241. package/server/services/sdk-session-sync-service.mjs +207 -0
  242. package/server/services/session-history-refresh-service.mjs +497 -0
  243. package/server/services/session-transcript-service.mjs +419 -0
  244. package/server/services/session-worker-launch-service.mjs +786 -0
  245. package/server/services/session-worker-process-service.mjs +319 -0
  246. package/server/services/session-worker-registry-service.mjs +194 -0
  247. package/server/services/session-worker-stop-service.mjs +141 -0
  248. package/server/services/session-worker-supervisor-service.mjs +898 -0
  249. package/server/services/session-worker-websocket-service.mjs +483 -0
  250. package/server/services/ssh-tunnel-manager-service.mjs +423 -0
  251. package/server/services/status-event-service.mjs +158 -0
  252. package/server/services/tmux-inspector-access-policy.mjs +77 -0
  253. package/server/services/tmux-inspector-socket-service.mjs +191 -0
  254. package/server/services/tmux-inspector-stream-service.mjs +552 -0
  255. package/server/services/tunnel-worker-path-guard.mjs +107 -0
  256. package/server/services/upload-sweep.mjs +60 -0
  257. package/server/services/usage-snapshot-helpers.mjs +209 -0
  258. package/server/services/windows-autostart-service.mjs +115 -0
  259. package/server/services/workspace-root-defaults-service.mjs +34 -0
  260. package/server/services/workspace-root-path-policy.mjs +196 -0
  261. package/server/services/workspace-root-relaunch-service.mjs +132 -0
  262. package/server/services/workspace-symlink-guard.mjs +54 -0
  263. package/server/tools/capture-proxy.mjs +173 -0
  264. package/server/tty-console-bootstrap.mjs +75 -0
  265. package/server/uploads/README.md +36 -0
  266. package/server/uploads/image_input.md +540 -0
  267. package/server/windows-terminal-launcher.mjs +29 -0
  268. package/server/workspace-root.mjs +87 -0
  269. package/shared/ask-user-bridge.mjs +131 -0
  270. package/shared/auto-compact-window.mjs +116 -0
  271. package/shared/background-task-timeout.mjs +52 -0
  272. package/shared/claude-thinking.mjs +101 -0
  273. package/shared/context-window-fallbacks.mjs +148 -0
  274. package/shared/control-poller.mjs +115 -0
  275. package/shared/empty-turn-completion.mjs +20 -0
  276. package/shared/model-catalog-freshness.mjs +11 -0
  277. package/shared/model-descriptors.mjs +118 -0
  278. package/shared/model-id.mjs +147 -0
  279. package/shared/model-refresh.mjs +24 -0
  280. package/shared/openai-reasoning.mjs +13 -0
  281. package/shared/plan-lines.mjs +13 -0
  282. package/shared/preview-instructions.mjs +77 -0
  283. package/shared/preview-tool-core.mjs +191 -0
  284. package/shared/provider-routing.mjs +29 -0
  285. package/shared/question-schema.mjs +315 -0
  286. package/shared/question-timeout.mjs +18 -0
  287. package/shared/stream-emit-gating.mjs +28 -0
  288. package/shared/subagent-run-id.mjs +17 -0
  289. package/shared/thought-cap.mjs +13 -0
  290. package/shared/tool-activity.mjs +98 -0
  291. package/shared/turn-ceiling.mjs +53 -0
  292. package/shared/worker-bootstrap.mjs +44 -0
  293. package/shared/worker-crash-guard.mjs +60 -0
@@ -0,0 +1,434 @@
1
+ 'use strict';
2
+
3
+ import { createShareVisibilityStatements } from './share-visibility-statements.mjs';
4
+ import { sessionWorkerProviderSqlList } from '../../shared/provider-routing.mjs';
5
+
6
+ export function createMessageRepository(db) {
7
+ const shareVisibility = createShareVisibilityStatements(db);
8
+ const queueHasImageOperationId = db.prepare(`PRAGMA table_info(queue)`).all()
9
+ .some((column) => column.name === 'image_operation_id');
10
+ const insertQueueSql = queueHasImageOperationId
11
+ ? `INSERT INTO queue (id, conversation_id, runtime_session_id, is_new_conversation, model, model_variant_id, reasoning_effort, context_tier, relay_mode, text, attachments, status, timestamp, retry_count, next_attempt_at, owner_sdk_session_id, owner_assigned_at, owner_lease_expires_at, owner_last_claimed_at, image_operation_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', ?, 0, NULL, ?, ?, ?, ?, ?)`
12
+ : `INSERT INTO queue (id, conversation_id, runtime_session_id, is_new_conversation, model, model_variant_id, reasoning_effort, context_tier, relay_mode, text, attachments, status, timestamp, retry_count, next_attempt_at, owner_sdk_session_id, owner_assigned_at, owner_lease_expires_at, owner_last_claimed_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', ?, 0, NULL, ?, ?, ?, ?)`;
13
+ return {
14
+ // messages
15
+ getMessages: db.prepare(`SELECT * FROM messages WHERE conversation_id = ? ORDER BY timestamp ASC`),
16
+ getSharedMessages: shareVisibility.getSharedMessages,
17
+ getMessageByConversation: db.prepare(`SELECT * FROM messages WHERE id = ? AND conversation_id = ? LIMIT 1`),
18
+ setMessageShareVisibility: shareVisibility.setMessageShareVisibility,
19
+ getLatestConversationModel: db.prepare(`SELECT model FROM messages WHERE conversation_id = ? AND model IS NOT NULL AND model != '' ORDER BY timestamp DESC LIMIT 1`),
20
+ getRecentMessagesDesc: db.prepare(`SELECT role, text, timestamp FROM messages WHERE conversation_id = ? ORDER BY timestamp DESC LIMIT ?`),
21
+ insertMsg: db.prepare(`INSERT INTO messages (id, conversation_id, role, text, model, mode, attachments, timestamp, model_requested, model_actual, model_origin) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`),
22
+ // Provenance is derived from the authenticated responder identity at
23
+ // /api/response time, never from the response payload.
24
+ setMessageExecutedProvider: db.prepare(`UPDATE messages SET executed_provider = ? WHERE id = ?`),
25
+ searchMessagesCount: db.prepare(`
26
+ SELECT COUNT(*) AS cnt
27
+ FROM messages_fts fts
28
+ INNER JOIN messages m
29
+ ON m.rowid = fts.rowid
30
+ INNER JOIN conversations c
31
+ ON c.id = m.conversation_id
32
+ WHERE fts.text MATCH ?
33
+ AND c.status != 'deleted'
34
+ `),
35
+ searchMessagesPage: db.prepare(`
36
+ SELECT
37
+ m.id AS message_id,
38
+ m.conversation_id AS conversation_id,
39
+ c.title AS conversation_title,
40
+ m.role AS role,
41
+ m.timestamp AS timestamp,
42
+ m.text AS raw_text,
43
+ snippet(messages_fts, 0, '<mark>', '</mark>', '…', 18) AS snippet,
44
+ bm25(messages_fts) AS score
45
+ FROM messages_fts fts
46
+ INNER JOIN messages m
47
+ ON m.rowid = fts.rowid
48
+ INNER JOIN conversations c
49
+ ON c.id = m.conversation_id
50
+ WHERE fts.text MATCH ?
51
+ AND c.status != 'deleted'
52
+ ORDER BY score ASC, m.timestamp DESC, m.id DESC
53
+ LIMIT ? OFFSET ?
54
+ `),
55
+ upsertMessageUsageSnapshot: db.prepare(`
56
+ INSERT INTO message_usage_snapshots (
57
+ response_message_id,
58
+ queue_message_id,
59
+ conversation_id,
60
+ source,
61
+ stale,
62
+ premium_remaining,
63
+ premium_entitlement,
64
+ premium_used_percent,
65
+ premium_delta_used,
66
+ chat_remaining,
67
+ chat_entitlement,
68
+ chat_used_percent,
69
+ chat_delta_used,
70
+ plan_remaining,
71
+ plan_entitlement,
72
+ plan_used_percent,
73
+ plan_delta_used,
74
+ captured_at
75
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
76
+ ON CONFLICT(response_message_id) DO UPDATE SET
77
+ queue_message_id = excluded.queue_message_id,
78
+ conversation_id = excluded.conversation_id,
79
+ source = excluded.source,
80
+ stale = excluded.stale,
81
+ premium_remaining = excluded.premium_remaining,
82
+ premium_entitlement = excluded.premium_entitlement,
83
+ premium_used_percent = excluded.premium_used_percent,
84
+ premium_delta_used = excluded.premium_delta_used,
85
+ chat_remaining = excluded.chat_remaining,
86
+ chat_entitlement = excluded.chat_entitlement,
87
+ chat_used_percent = excluded.chat_used_percent,
88
+ chat_delta_used = excluded.chat_delta_used,
89
+ plan_remaining = excluded.plan_remaining,
90
+ plan_entitlement = excluded.plan_entitlement,
91
+ plan_used_percent = excluded.plan_used_percent,
92
+ plan_delta_used = excluded.plan_delta_used,
93
+ captured_at = excluded.captured_at
94
+ `),
95
+ getLatestMessageUsageSnapshotByConversation: db.prepare(`
96
+ SELECT *
97
+ FROM message_usage_snapshots
98
+ WHERE conversation_id = ?
99
+ ORDER BY captured_at DESC
100
+ LIMIT 1
101
+ `),
102
+ listMessageUsageSnapshotsByConversation: db.prepare(`
103
+ SELECT *
104
+ FROM message_usage_snapshots
105
+ WHERE conversation_id = ?
106
+ ORDER BY captured_at ASC
107
+ `),
108
+
109
+ // queue
110
+ insertQ: db.prepare(insertQueueSql),
111
+ queueHasImageOperationId,
112
+ // A background-continuation turn: born 'processing' and owned by its
113
+ // session worker — the CLI already started it on its own, so it must
114
+ // never be handed out as deliverable work.
115
+ insertContinuationQ: db.prepare(`INSERT INTO queue (id, conversation_id, runtime_session_id, is_new_conversation, model, relay_mode, text, status, kind, timestamp, processing_at, retry_count, owner_sdk_session_id, owner_assigned_at, owner_lease_expires_at, owner_last_claimed_at) VALUES (?, ?, ?, 0, ?, ?, ?, 'processing', 'continuation', ?, ?, 0, ?, ?, ?, ?)`),
116
+ setMessageKind: db.prepare(`UPDATE messages SET kind = ? WHERE id = ?`),
117
+ // Quiet teardown for a continuation whose worker died: there is no
118
+ // user to answer, so it fails without the terminal-failure ceremony.
119
+ dropStaleContinuation: db.prepare(`UPDATE queue SET status = 'failed', processing_at = NULL, next_attempt_at = NULL, owner_lease_expires_at = NULL WHERE id = ? AND status = 'processing' AND kind = 'continuation'`),
120
+ findPending: db.prepare(`SELECT * FROM queue WHERE status = 'pending' AND COALESCE(kind, '') != 'continuation' AND (next_attempt_at IS NULL OR next_attempt_at <= ?) ORDER BY retry_count ASC, CASE WHEN next_attempt_at IS NULL THEN 0 ELSE 1 END ASC, COALESCE(next_attempt_at, timestamp) ASC, timestamp ASC LIMIT 1`),
121
+ // Global fallback for requesters without a session identity (the legacy
122
+ // Copilot relay CLI). Conversations bound to a session-worker provider
123
+ // (claude/cursor/grok) are excluded outright: handing one of their turns
124
+ // to the relay executes it on the Copilot plan under whatever model the
125
+ // id happens to resolve to there, invisibly to the user.
126
+ //
127
+ // Owned rows are excluded too. An owner means a session worker is
128
+ // responsible for the turn, and the relay polls every 2s while a worker
129
+ // takes ~a minute to become ready — so without this the relay wins that
130
+ // race and fails a turn it cannot run (an OpenAI image model is not a
131
+ // Copilot model), costing a retry and a backoff. Owners are only
132
+ // assigned when session-worker routing is enabled
133
+ // (resolveInitialQueueOwnerSessionId returns null otherwise), so
134
+ // routing-disabled installs still hand the relay every row.
135
+ findPendingForLegacyRelay: db.prepare(`
136
+ SELECT q.*
137
+ FROM queue q
138
+ LEFT JOIN runtime_sessions rs
139
+ ON rs.id = q.runtime_session_id
140
+ LEFT JOIN runtime_sessions rsc
141
+ ON rsc.conversation_id = q.conversation_id
142
+ WHERE q.status = 'pending'
143
+ AND COALESCE(q.kind, '') != 'continuation'
144
+ AND (q.next_attempt_at IS NULL OR q.next_attempt_at <= ?)
145
+ AND NULLIF(q.owner_sdk_session_id, '') IS NULL
146
+ AND LOWER(COALESCE(
147
+ NULLIF(rs.provider_type, ''),
148
+ NULLIF(rsc.provider_type, ''),
149
+ 'github'
150
+ )) NOT IN (${sessionWorkerProviderSqlList()})
151
+ ORDER BY
152
+ q.retry_count ASC,
153
+ CASE WHEN q.next_attempt_at IS NULL THEN 0 ELSE 1 END ASC,
154
+ COALESCE(q.next_attempt_at, q.timestamp) ASC,
155
+ q.timestamp ASC
156
+ LIMIT 1
157
+ `),
158
+ findPendingForWorker: db.prepare(`
159
+ SELECT q.*
160
+ FROM queue q
161
+ LEFT JOIN runtime_sessions rs
162
+ ON rs.id = q.runtime_session_id
163
+ LEFT JOIN conversations c
164
+ ON c.id = q.conversation_id
165
+ WHERE q.status = 'pending'
166
+ AND COALESCE(q.kind, '') != 'continuation'
167
+ AND (q.next_attempt_at IS NULL OR q.next_attempt_at <= ?)
168
+ AND (
169
+ (
170
+ COALESCE(
171
+ NULLIF(q.owner_sdk_session_id, ''),
172
+ NULLIF(rs.sdk_session_id, ''),
173
+ NULLIF(c.sdk_session_id, '')
174
+ ) IS NULL
175
+ -- Defense in depth: session-worker-provider turns are always
176
+ -- born owned, so an unowned one is a routing anomaly — it must
177
+ -- never be claimable by an arbitrary worker of a different
178
+ -- provider (isolation would otherwise rest on owner assignment
179
+ -- alone).
180
+ AND LOWER(COALESCE(NULLIF(rs.provider_type, ''), 'github'))
181
+ NOT IN (${sessionWorkerProviderSqlList()})
182
+ )
183
+ OR COALESCE(
184
+ NULLIF(q.owner_sdk_session_id, ''),
185
+ NULLIF(rs.sdk_session_id, ''),
186
+ NULLIF(c.sdk_session_id, '')
187
+ ) = ?
188
+ )
189
+ ORDER BY
190
+ CASE
191
+ WHEN COALESCE(
192
+ NULLIF(q.owner_sdk_session_id, ''),
193
+ NULLIF(rs.sdk_session_id, ''),
194
+ NULLIF(c.sdk_session_id, '')
195
+ ) = ? THEN 0
196
+ ELSE 1
197
+ END ASC,
198
+ q.retry_count ASC,
199
+ CASE WHEN q.next_attempt_at IS NULL THEN 0 ELSE 1 END ASC,
200
+ COALESCE(q.next_attempt_at, q.timestamp) ASC,
201
+ q.timestamp ASC
202
+ LIMIT 1
203
+ `),
204
+ findPendingForSessionAffinity: db.prepare(`
205
+ SELECT q.*
206
+ FROM queue q
207
+ LEFT JOIN runtime_sessions rs
208
+ ON rs.id = q.runtime_session_id
209
+ LEFT JOIN conversations c
210
+ ON c.id = q.conversation_id
211
+ WHERE q.status = 'pending'
212
+ AND COALESCE(q.kind, '') != 'continuation'
213
+ AND (q.next_attempt_at IS NULL OR q.next_attempt_at <= ?)
214
+ AND COALESCE(
215
+ NULLIF(q.owner_sdk_session_id, ''),
216
+ NULLIF(rs.sdk_session_id, ''),
217
+ NULLIF(c.sdk_session_id, '')
218
+ ) = ?
219
+ ORDER BY
220
+ q.retry_count ASC,
221
+ CASE WHEN q.next_attempt_at IS NULL THEN 0 ELSE 1 END ASC,
222
+ COALESCE(q.next_attempt_at, q.timestamp) ASC,
223
+ q.timestamp ASC
224
+ LIMIT 1
225
+ `),
226
+ countQueueWorkForSessionAffinity: db.prepare(`
227
+ SELECT COUNT(*) AS cnt
228
+ FROM queue q
229
+ LEFT JOIN runtime_sessions rs
230
+ ON rs.id = q.runtime_session_id
231
+ LEFT JOIN conversations c
232
+ ON c.id = q.conversation_id
233
+ WHERE (
234
+ (q.status = 'pending' AND (q.next_attempt_at IS NULL OR q.next_attempt_at <= ?))
235
+ OR q.status = 'processing'
236
+ )
237
+ AND COALESCE(
238
+ NULLIF(q.owner_sdk_session_id, ''),
239
+ NULLIF(rs.sdk_session_id, ''),
240
+ NULLIF(c.sdk_session_id, '')
241
+ ) = ?
242
+ `),
243
+ listPendingWorkerOwnerSessionIds: db.prepare(`
244
+ SELECT COALESCE(
245
+ NULLIF(q.owner_sdk_session_id, ''),
246
+ NULLIF(rs.sdk_session_id, ''),
247
+ NULLIF(c.sdk_session_id, '')
248
+ ) AS sdk_session_id
249
+ FROM queue q
250
+ LEFT JOIN runtime_sessions rs
251
+ ON rs.id = q.runtime_session_id
252
+ LEFT JOIN conversations c
253
+ ON c.id = q.conversation_id
254
+ WHERE q.status = 'pending'
255
+ AND COALESCE(
256
+ NULLIF(q.owner_sdk_session_id, ''),
257
+ NULLIF(rs.sdk_session_id, ''),
258
+ NULLIF(c.sdk_session_id, '')
259
+ ) IS NOT NULL
260
+ GROUP BY COALESCE(
261
+ NULLIF(q.owner_sdk_session_id, ''),
262
+ NULLIF(rs.sdk_session_id, ''),
263
+ NULLIF(c.sdk_session_id, '')
264
+ )
265
+ ORDER BY MIN(q.timestamp) ASC
266
+ LIMIT ?
267
+ `),
268
+ countStatus: db.prepare(`SELECT status, COUNT(*) as cnt FROM queue WHERE status IN ('pending','processing','parked') GROUP BY status`),
269
+ countRuntimeSessions: db.prepare(`SELECT COUNT(*) AS cnt FROM runtime_sessions WHERE status = 'active'`),
270
+ setProcessing: db.prepare(`UPDATE queue SET status = 'processing', processing_at = ? WHERE id = ?`),
271
+ setProcessingWithWorkerLease: db.prepare(`
272
+ UPDATE queue
273
+ SET
274
+ status = 'processing',
275
+ processing_at = ?,
276
+ owner_sdk_session_id = COALESCE(NULLIF(owner_sdk_session_id, ''), ?),
277
+ owner_assigned_at = COALESCE(owner_assigned_at, ?),
278
+ owner_lease_expires_at = ?,
279
+ owner_last_claimed_at = ?
280
+ WHERE id = ?
281
+ `),
282
+ setQueueRuntimeSession: db.prepare(`UPDATE queue SET runtime_session_id = ? WHERE id = ?`),
283
+ setQueueResponseMessageId: db.prepare(`UPDATE queue SET response_message_id = ? WHERE id = ?`),
284
+ setDone: db.prepare(`UPDATE queue SET status = 'done', response = ?, processing_at = NULL, next_attempt_at = NULL, owner_lease_expires_at = NULL WHERE id = ? AND status IN ('processing', 'pending')`),
285
+ setFailed: db.prepare(`UPDATE queue SET status = 'failed', response = ?, processing_at = NULL, next_attempt_at = NULL, owner_lease_expires_at = NULL WHERE id = ?`),
286
+ deleteConvQ: db.prepare(`DELETE FROM queue WHERE conversation_id = ?`),
287
+ findQById: db.prepare(`SELECT * FROM queue WHERE id = ?`),
288
+ pruneQueue: db.prepare(`DELETE FROM queue WHERE status = 'done' AND id NOT IN (SELECT id FROM queue WHERE status = 'done' ORDER BY timestamp DESC LIMIT 200)`),
289
+ // Recovery clears only the lease, never the owner: a recovered row must
290
+ // stay routed to its provider worker so the primer respawns that worker
291
+ // instead of the row becoming claimable by the global relay poll.
292
+ recoverStale: db.prepare(`UPDATE queue SET status = 'pending', processing_at = NULL, next_attempt_at = ?, owner_lease_expires_at = NULL, owner_last_claimed_at = NULL WHERE status = 'processing' AND COALESCE(kind, '') != 'continuation' AND processing_at < ?`),
293
+ // Staleness is inactivity, not elapsed turn time: owner_last_claimed_at is
294
+ // refreshed by every worker heartbeat for the message it is working on, so a
295
+ // long-but-alive turn keeps moving the cutoff. processing_at is only the
296
+ // fallback for rows without an owner (session-worker routing disabled), and
297
+ // it is what the separate absolute ceiling measures.
298
+ //
299
+ // A turn blocked on an unanswered AskUserQuestion is never stale — it is
300
+ // waiting on the human, and relay_questions carries its own expiry.
301
+ listRecoverableProcessing: db.prepare(`
302
+ SELECT id, conversation_id, kind, relay_mode, model, retry_count
303
+ FROM queue
304
+ WHERE status = 'processing'
305
+ AND COALESCE(kind, '') != 'continuation'
306
+ AND (
307
+ COALESCE(owner_last_claimed_at, processing_at, timestamp) < @inactiveBefore
308
+ OR (@ceilingBefore IS NOT NULL AND COALESCE(processing_at, timestamp) < @ceilingBefore)
309
+ )
310
+ AND id NOT IN (SELECT queue_id FROM relay_questions WHERE status = 'pending')
311
+ `),
312
+ recoverProcessingBefore: db.prepare(`
313
+ UPDATE queue
314
+ SET status = 'pending',
315
+ processing_at = NULL,
316
+ next_attempt_at = @requeueAt,
317
+ owner_lease_expires_at = NULL,
318
+ owner_last_claimed_at = NULL,
319
+ retry_count = retry_count + 1
320
+ WHERE status = 'processing'
321
+ AND COALESCE(kind, '') != 'continuation'
322
+ AND (
323
+ COALESCE(owner_last_claimed_at, processing_at, timestamp) < @inactiveBefore
324
+ OR (@ceilingBefore IS NOT NULL AND COALESCE(processing_at, timestamp) < @ceilingBefore)
325
+ )
326
+ AND id NOT IN (SELECT queue_id FROM relay_questions WHERE status = 'pending')
327
+ `),
328
+ // Dead-worker recovery: the owner's process is provably gone, so its
329
+ // in-flight rows go back to pending — owner intact (the primer
330
+ // respawns that worker; the row must never become global prey) and
331
+ // retry_count incremented so a worker that dies on every delivery
332
+ // eventually exhausts the same budget as any other retry loop. The
333
+ // pending-question exemption deliberately does NOT apply here: a dead
334
+ // worker can never collect its answer, so holding the row would leak
335
+ // both the row and the question.
336
+ listProcessingRowsForOwner: db.prepare(`
337
+ SELECT id, conversation_id, kind, retry_count
338
+ FROM queue
339
+ WHERE status = 'processing'
340
+ AND NULLIF(owner_sdk_session_id, '') = ?
341
+ `),
342
+ recoverProcessingRowKeepOwner: db.prepare(`
343
+ UPDATE queue
344
+ SET status = 'pending',
345
+ processing_at = NULL,
346
+ next_attempt_at = ?,
347
+ owner_lease_expires_at = NULL,
348
+ owner_last_claimed_at = NULL,
349
+ retry_count = retry_count + 1
350
+ WHERE id = ? AND status = 'processing'
351
+ `),
352
+ listProcessingOwnerSessionIds: db.prepare(`
353
+ SELECT DISTINCT NULLIF(owner_sdk_session_id, '') AS sdk_session_id
354
+ FROM queue
355
+ WHERE status = 'processing'
356
+ AND NULLIF(owner_sdk_session_id, '') IS NOT NULL
357
+ LIMIT ?
358
+ `),
359
+ // A continuation with no live worker can only be torn down — replaying
360
+ // it as deliverable work would send the CLI's own bookkeeping text back
361
+ // to the CLI as a user prompt.
362
+ listStaleProcessingContinuations: db.prepare(`
363
+ SELECT id, conversation_id
364
+ FROM queue
365
+ WHERE status = 'processing'
366
+ AND kind = 'continuation'
367
+ AND (
368
+ COALESCE(owner_last_claimed_at, processing_at, timestamp) < @inactiveBefore
369
+ OR (@ceilingBefore IS NOT NULL AND COALESCE(processing_at, timestamp) < @ceilingBefore)
370
+ )
371
+ `),
372
+ listQueueForPauseDrop: db.prepare(`SELECT id, conversation_id FROM queue WHERE status IN ('pending', 'processing', 'parked')`),
373
+ // Authoritative "is a turn in flight" signal for the conversation list.
374
+ // Clients otherwise track this from one-shot message_status socket events,
375
+ // which are lost if the socket drops between the final status and delivery.
376
+ listConversationIdsWithActiveQueue: db.prepare(`SELECT DISTINCT conversation_id FROM queue WHERE status IN ('pending', 'processing', 'parked')`),
377
+ deleteQueueById: db.prepare(`DELETE FROM queue WHERE id = ?`),
378
+ getLatestProcessingQueueByConversation: db.prepare(`SELECT id, relay_mode, timestamp, processing_at FROM queue WHERE conversation_id = ? AND status = 'processing' ORDER BY COALESCE(processing_at, timestamp) DESC LIMIT 1`),
379
+ parkPendingQueueForRestart: db.prepare(`
380
+ UPDATE queue
381
+ SET
382
+ status = 'parked',
383
+ next_attempt_at = NULL,
384
+ parked_at = COALESCE(parked_at, @parkedAt),
385
+ parked_target_session_id = @targetSessionId,
386
+ parked_transaction_id = @transactionId,
387
+ parked_reason = @reason
388
+ WHERE status = 'pending'
389
+ `),
390
+ listParkedQueueForRelease: db.prepare(`
391
+ SELECT id, conversation_id
392
+ FROM queue
393
+ WHERE status = 'parked'
394
+ AND (
395
+ parked_transaction_id IS NULL
396
+ OR parked_transaction_id = ?
397
+ OR parked_target_session_id = ?
398
+ )
399
+ ORDER BY COALESCE(parked_at, timestamp) ASC, timestamp ASC
400
+ `),
401
+ listAllParkedQueueForRelease: db.prepare(`
402
+ SELECT id, conversation_id
403
+ FROM queue
404
+ WHERE status = 'parked'
405
+ ORDER BY COALESCE(parked_at, timestamp) ASC, timestamp ASC
406
+ `),
407
+ releaseParkedQueueByIds: db.prepare(`
408
+ UPDATE queue
409
+ SET
410
+ status = 'pending',
411
+ parked_at = NULL,
412
+ parked_target_session_id = NULL,
413
+ parked_transaction_id = NULL,
414
+ parked_reason = NULL
415
+ WHERE id = ? AND status = 'parked'
416
+ `),
417
+
418
+
419
+ // uploads
420
+ getUploadFile: db.prepare(`SELECT * FROM uploaded_files WHERE sha256 = ?`),
421
+ insertUploadFile: db.prepare(`INSERT OR IGNORE INTO uploaded_files (sha256, original_name, mime_type, size_bytes, created_at) VALUES (?, ?, ?, ?, ?)`),
422
+ insertUploadRef: db.prepare(`INSERT OR IGNORE INTO upload_refs (file_sha256, conversation_id, message_id, created_at) VALUES (?, ?, ?, ?)`),
423
+ listUploadHashesByConversation: db.prepare(`SELECT DISTINCT file_sha256 FROM upload_refs WHERE conversation_id = ?`),
424
+ deleteUploadRefsByConversation: db.prepare(`DELETE FROM upload_refs WHERE conversation_id = ?`),
425
+ countUploadRefsBySha: db.prepare(`SELECT COUNT(*) AS cnt FROM upload_refs WHERE file_sha256 = ?`),
426
+ deleteUploadFile: db.prepare(`DELETE FROM uploaded_files WHERE sha256 = ?`),
427
+
428
+ // Draft attachment references use a sentinel message id so a composer
429
+ // draft can hold a blob alive before any message exists.
430
+ listDraftUploadHashes: db.prepare(`SELECT file_sha256 FROM upload_refs WHERE conversation_id = ? AND message_id = '__draft__'`),
431
+ deleteDraftUploadRefs: db.prepare(`DELETE FROM upload_refs WHERE conversation_id = ? AND message_id = '__draft__'`),
432
+ deleteDraftUploadRef: db.prepare(`DELETE FROM upload_refs WHERE conversation_id = ? AND message_id = '__draft__' AND file_sha256 = ?`),
433
+ };
434
+ }
@@ -0,0 +1,161 @@
1
+ 'use strict';
2
+
3
+ export function createQuestionRepository(db) {
4
+ return {
5
+ // relay questions
6
+ insertQuestion: db.prepare(`INSERT INTO relay_questions (id, queue_id, conversation_id, message_id, relay_mode, prompt, choices, request, request_schema, status, answer, structured_answer, sdk_session_id, owner_worker_id, continuation_id, continuation_question_id, created_at, answered_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', NULL, NULL, ?, ?, ?, ?, ?, NULL, ?)`),
7
+ getQuestion: db.prepare(`SELECT * FROM relay_questions WHERE id = ?`),
8
+ findPendingQuestionByMessage: db.prepare(`SELECT * FROM relay_questions WHERE message_id = ? AND status = 'pending' ORDER BY created_at DESC LIMIT 1`),
9
+ listPendingQuestionsByMessage: db.prepare(`SELECT * FROM relay_questions WHERE message_id = ? AND status = 'pending' ORDER BY created_at ASC`),
10
+ findRecentlyAnsweredQuestionByMessage: db.prepare(`SELECT * FROM relay_questions WHERE message_id = ? AND status = 'answered' AND answered_at >= ? ORDER BY answered_at DESC LIMIT 1`),
11
+ listQuestions: db.prepare(`SELECT * FROM relay_questions WHERE status = ? AND (? IS NULL OR conversation_id = ?) ORDER BY created_at ASC`),
12
+ timeoutQuestion:db.prepare(`UPDATE relay_questions SET status = 'timed_out' WHERE id = ? AND status = 'pending'`),
13
+ cancelPendingQuestionsByMessage: db.prepare(`UPDATE relay_questions SET status = 'cancelled', answered_at = COALESCE(answered_at, ?) WHERE message_id = ? AND status = 'pending'`),
14
+ deleteConvQuestions: db.prepare(`DELETE FROM relay_questions WHERE conversation_id = ?`),
15
+ // RETURNING (run via .all) so the expiry log names exactly the rows
16
+ // that flipped — a separate pre-SELECT could race an answer landing
17
+ // between the two statements.
18
+ expireQuestions: db.prepare(`UPDATE relay_questions SET status = 'timed_out' WHERE status = 'pending' AND expires_at < ? RETURNING id, created_at`),
19
+
20
+ // relay activity
21
+ insertActivity: db.prepare(`INSERT INTO relay_activity (queue_message_id, response_message_id, conversation_id, relay_mode, text, created_at, subagent_run_id, metadata_json) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`),
22
+ linkActivityToResponse: db.prepare(`UPDATE relay_activity SET response_message_id = ? WHERE queue_message_id = ? AND response_message_id IS NULL`),
23
+ listActivityByResponse: db.prepare(`SELECT text, subagent_run_id, metadata_json FROM relay_activity WHERE response_message_id = ? ORDER BY id ASC`),
24
+ listActivityByQueueMessage: db.prepare(`SELECT text, subagent_run_id, metadata_json FROM relay_activity WHERE queue_message_id = ? ORDER BY id ASC`),
25
+ deleteConvActivity: db.prepare(`DELETE FROM relay_activity WHERE conversation_id = ?`),
26
+
27
+ // relay stream events — every update carries the full text-so-far
28
+ // (all workers publish cumulative snapshots), so the store keeps ONE
29
+ // row per (queue message, subagent thread) and replaces it in place.
30
+ getLastStreamSeqByQueueMessage: db.prepare(`SELECT COALESCE(MAX(seq), 0) AS max_seq FROM relay_stream_events WHERE queue_message_id = ?`),
31
+ getStreamEventByQueueAndThread: db.prepare(`SELECT id, seq, done FROM relay_stream_events WHERE queue_message_id = ? AND subagent_run_id IS ? LIMIT 1`),
32
+ insertStreamEvent: db.prepare(`INSERT INTO relay_stream_events (queue_message_id, response_message_id, conversation_id, relay_mode, seq, text, done, created_at, subagent_run_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`),
33
+ updateStreamEventByQueueAndThread: db.prepare(`
34
+ UPDATE relay_stream_events
35
+ SET response_message_id = COALESCE(response_message_id, ?),
36
+ conversation_id = ?,
37
+ relay_mode = ?,
38
+ seq = ?,
39
+ text = ?,
40
+ done = CASE WHEN done = 1 OR ? = 1 THEN 1 ELSE 0 END,
41
+ created_at = ?
42
+ WHERE queue_message_id = ?
43
+ AND subagent_run_id IS ?
44
+ `),
45
+ linkStreamEventsToResponse: db.prepare(`UPDATE relay_stream_events SET response_message_id = ? WHERE queue_message_id = ? AND response_message_id IS NULL`),
46
+ listStreamEventsByResponse: db.prepare(`SELECT seq, text, done, created_at, subagent_run_id FROM relay_stream_events WHERE response_message_id = ? ORDER BY seq ASC, id ASC`),
47
+ listStreamEventsByQueueMessage: db.prepare(`SELECT seq, text, done, created_at, subagent_run_id FROM relay_stream_events WHERE queue_message_id = ? ORDER BY seq ASC, id ASC`),
48
+ deleteConvStreamEvents: db.prepare(`DELETE FROM relay_stream_events WHERE conversation_id = ?`),
49
+
50
+ // relay thoughts (agent reasoning)
51
+ getLastThoughtSeqByQueueMessage: db.prepare(`SELECT COALESCE(MAX(seq), 0) AS max_seq FROM relay_thought WHERE queue_message_id = ?`),
52
+ getThoughtByQueueAndReasoning: db.prepare(`SELECT seq FROM relay_thought WHERE queue_message_id = ? AND reasoning_id = ? LIMIT 1`),
53
+ insertThought: db.prepare(`INSERT INTO relay_thought (queue_message_id, response_message_id, conversation_id, relay_mode, reasoning_id, seq, text, done, created_at, subagent_run_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`),
54
+ updateThoughtByQueueAndReasoning: db.prepare(`
55
+ UPDATE relay_thought
56
+ SET response_message_id = COALESCE(response_message_id, ?),
57
+ conversation_id = ?,
58
+ relay_mode = ?,
59
+ text = ?,
60
+ done = CASE WHEN done = 1 OR ? = 1 THEN 1 ELSE 0 END,
61
+ created_at = ?,
62
+ subagent_run_id = COALESCE(?, subagent_run_id)
63
+ WHERE queue_message_id = ?
64
+ AND reasoning_id = ?
65
+ `),
66
+ linkThoughtsToResponse: db.prepare(`UPDATE relay_thought SET response_message_id = ? WHERE queue_message_id = ? AND response_message_id IS NULL`),
67
+ listThoughtsByResponse: db.prepare(`SELECT reasoning_id, seq, text, done, created_at, subagent_run_id FROM relay_thought WHERE response_message_id = ? ORDER BY seq ASC, id ASC`),
68
+ listThoughtsByQueueMessage: db.prepare(`SELECT reasoning_id, seq, text, done, created_at, subagent_run_id FROM relay_thought WHERE queue_message_id = ? ORDER BY seq ASC, id ASC`),
69
+ deleteConvThoughts: db.prepare(`DELETE FROM relay_thought WHERE conversation_id = ?`),
70
+
71
+ // subagent runs
72
+ insertSubagentRun: db.prepare(`
73
+ INSERT INTO subagent_runs (
74
+ id, queue_message_id, conversation_id, parent_subagent_id, display_name, status, started_at, updated_at, completed_at
75
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULL)
76
+ `),
77
+ getSubagentRun: db.prepare(`SELECT * FROM subagent_runs WHERE id = ?`),
78
+ updateSubagentRunStatus: db.prepare(`
79
+ UPDATE subagent_runs
80
+ SET status = ?, updated_at = ?, completed_at = COALESCE(?, completed_at)
81
+ WHERE id = ?
82
+ `),
83
+ listSubagentRunsByQueueMessage: db.prepare(`
84
+ SELECT id, queue_message_id, conversation_id, parent_subagent_id, display_name, status, started_at, updated_at, completed_at
85
+ FROM subagent_runs
86
+ WHERE queue_message_id = ?
87
+ ORDER BY started_at ASC, id ASC
88
+ `),
89
+ // Terminal-state reconciliation: whatever ends a turn (response, fail,
90
+ // abort) also ends its subagent runs — nothing else ever will, and an
91
+ // un-reconciled row renders as a bubble stuck "running" forever.
92
+ listRunningSubagentRunsByQueueMessage: db.prepare(`
93
+ SELECT id, conversation_id, parent_subagent_id, display_name
94
+ FROM subagent_runs
95
+ WHERE queue_message_id = ? AND status = 'running'
96
+ `),
97
+ closeRunningSubagentRunsByQueueMessage: db.prepare(`
98
+ UPDATE subagent_runs
99
+ SET status = ?, updated_at = ?, completed_at = COALESCE(completed_at, ?)
100
+ WHERE queue_message_id = ? AND status = 'running'
101
+ `),
102
+ listSubagentRunsByResponse: db.prepare(`
103
+ SELECT sr.id, sr.queue_message_id, sr.conversation_id, sr.parent_subagent_id, sr.display_name, sr.status, sr.started_at, sr.updated_at, sr.completed_at
104
+ FROM subagent_runs sr
105
+ WHERE sr.queue_message_id IN (
106
+ SELECT id FROM queue WHERE response_message_id = ?
107
+ UNION
108
+ SELECT DISTINCT queue_message_id FROM relay_activity WHERE response_message_id = ? AND queue_message_id IS NOT NULL
109
+ UNION
110
+ SELECT DISTINCT queue_message_id FROM relay_thought WHERE response_message_id = ? AND queue_message_id IS NOT NULL
111
+ )
112
+ ORDER BY sr.started_at ASC, sr.id ASC
113
+ `),
114
+ deleteConvSubagentRuns: db.prepare(`DELETE FROM subagent_runs WHERE conversation_id = ?`),
115
+
116
+ // workflow runs — final digests of settled background workflows,
117
+ // attached to the assistant message that reports the completion.
118
+ // Inserted inside the /api/response finalize transaction, so rows key
119
+ // directly on the response message id (unlike subagent_runs, which are
120
+ // written before the response id exists and need queue-id resolution).
121
+ insertWorkflowRun: db.prepare(`
122
+ INSERT INTO workflow_runs (
123
+ id, response_message_id, conversation_id, run_index, digest_json, created_at
124
+ ) VALUES (?, ?, ?, ?, ?, ?)
125
+ `),
126
+ listWorkflowRunsByResponse: db.prepare(`
127
+ SELECT id, response_message_id, conversation_id, run_index, digest_json, created_at
128
+ FROM workflow_runs
129
+ WHERE response_message_id = ?
130
+ ORDER BY run_index ASC, id ASC
131
+ `),
132
+ deleteConvWorkflowRuns: db.prepare(`DELETE FROM workflow_runs WHERE conversation_id = ?`),
133
+
134
+ // preview cards — snapshots of previews published mid-turn, written
135
+ // with the queue id and linked to the response at finalize (the same
136
+ // two-step relay_activity uses, because the response id does not exist
137
+ // while the turn is still running).
138
+ insertPreviewCard: db.prepare(`
139
+ INSERT INTO preview_cards (
140
+ id, queue_message_id, response_message_id, conversation_id, preview_json, created_at
141
+ ) VALUES (?, ?, NULL, ?, ?, ?)
142
+ `),
143
+ linkPreviewCardsToResponse: db.prepare(`UPDATE preview_cards SET response_message_id = ? WHERE queue_message_id = ? AND response_message_id IS NULL`),
144
+ listPreviewCardsByResponse: db.prepare(`
145
+ SELECT id, preview_json, created_at
146
+ FROM preview_cards
147
+ WHERE response_message_id = ?
148
+ ORDER BY created_at ASC, id ASC
149
+ `),
150
+ deleteConvPreviewCards: db.prepare(`DELETE FROM preview_cards WHERE conversation_id = ?`),
151
+
152
+ // relay boards
153
+ insertBoard: db.prepare(`INSERT INTO relay_boards (id, queue_id, conversation_id, message_id, board_type, relay_mode, title, body, actions_json, recommended_action, context_json, status, selected_action, acted_at, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', NULL, NULL, ?, ?)`),
154
+ getBoard: db.prepare(`SELECT * FROM relay_boards WHERE id = ?`),
155
+ findBoardByMessageType: db.prepare(`SELECT * FROM relay_boards WHERE message_id = ? AND board_type = ? ORDER BY created_at DESC LIMIT 1`),
156
+ listBoards: db.prepare(`SELECT * FROM relay_boards WHERE status = ? AND (? IS NULL OR conversation_id = ?) ORDER BY created_at ASC`),
157
+ markBoardAction: db.prepare(`UPDATE relay_boards SET status = 'acted', selected_action = ?, acted_at = ?, updated_at = ? WHERE id = ? AND status = 'pending'`),
158
+ dismissBoard: db.prepare(`UPDATE relay_boards SET status = 'dismissed', selected_action = ?, acted_at = ?, updated_at = ? WHERE id = ? AND status = 'pending'`),
159
+ deleteConvBoards: db.prepare(`DELETE FROM relay_boards WHERE conversation_id = ?`),
160
+ };
161
+ }