@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,317 @@
1
+ 'use strict';
2
+
3
+ const IMAGE_SCHEMA_VERSION = 1;
4
+
5
+ export function migrateImageConversationSchema(db) {
6
+ db.exec(`
7
+ CREATE TABLE IF NOT EXISTS image_sessions (
8
+ id TEXT PRIMARY KEY,
9
+ conversation_id TEXT NOT NULL,
10
+ origin TEXT NOT NULL CHECK (origin IN ('native', 'legacy_verified', 'legacy_reconstructed')),
11
+ status TEXT NOT NULL DEFAULT 'active' CHECK (status = 'active'),
12
+ schema_version INTEGER NOT NULL DEFAULT ${IMAGE_SCHEMA_VERSION},
13
+ created_at TEXT NOT NULL,
14
+ updated_at TEXT NOT NULL,
15
+ FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE
16
+ );
17
+
18
+ CREATE INDEX IF NOT EXISTS idx_image_sessions_conversation_updated
19
+ ON image_sessions(conversation_id, updated_at DESC);
20
+
21
+ CREATE TABLE IF NOT EXISTS image_operations (
22
+ id TEXT PRIMARY KEY,
23
+ image_session_id TEXT NOT NULL,
24
+ source_message_id TEXT NOT NULL UNIQUE,
25
+ queue_message_id TEXT NOT NULL UNIQUE,
26
+ kind TEXT NOT NULL CHECK (kind IN ('generate', 'edit', 'legacy_edit')),
27
+ parent_node_id TEXT,
28
+ prompt TEXT NOT NULL,
29
+ selected_image_model TEXT NOT NULL,
30
+ orchestration_model TEXT,
31
+ provider TEXT NOT NULL,
32
+ execution_mode TEXT NOT NULL CHECK (execution_mode IN ('native_responses', 'direct_images')),
33
+ parameters_json TEXT NOT NULL,
34
+ request_fingerprint TEXT NOT NULL,
35
+ idempotency_key TEXT NOT NULL,
36
+ replaces_operation_id TEXT,
37
+ created_at TEXT NOT NULL,
38
+ FOREIGN KEY (image_session_id) REFERENCES image_sessions(id) ON DELETE CASCADE,
39
+ FOREIGN KEY (source_message_id) REFERENCES messages(id) ON DELETE CASCADE,
40
+ FOREIGN KEY (replaces_operation_id) REFERENCES image_operations(id) ON DELETE SET NULL
41
+ );
42
+
43
+ CREATE INDEX IF NOT EXISTS idx_image_operations_session_created
44
+ ON image_operations(image_session_id, created_at);
45
+
46
+ CREATE TABLE IF NOT EXISTS image_nodes (
47
+ id TEXT PRIMARY KEY,
48
+ image_session_id TEXT NOT NULL,
49
+ operation_id TEXT NOT NULL,
50
+ assistant_message_id TEXT NOT NULL,
51
+ attachment_image_id TEXT NOT NULL,
52
+ output_index INTEGER NOT NULL CHECK (output_index >= 0),
53
+ created_at TEXT NOT NULL,
54
+ UNIQUE (operation_id, output_index),
55
+ UNIQUE (assistant_message_id, attachment_image_id),
56
+ FOREIGN KEY (image_session_id) REFERENCES image_sessions(id) ON DELETE CASCADE,
57
+ FOREIGN KEY (operation_id) REFERENCES image_operations(id) ON DELETE CASCADE,
58
+ FOREIGN KEY (assistant_message_id) REFERENCES messages(id) ON DELETE CASCADE
59
+ );
60
+
61
+ CREATE INDEX IF NOT EXISTS idx_image_nodes_session_created
62
+ ON image_nodes(image_session_id, created_at);
63
+
64
+ CREATE TABLE IF NOT EXISTS image_edges (
65
+ image_session_id TEXT NOT NULL,
66
+ parent_node_id TEXT NOT NULL,
67
+ child_node_id TEXT NOT NULL UNIQUE,
68
+ operation_id TEXT NOT NULL,
69
+ relation TEXT NOT NULL CHECK (relation = 'edit'),
70
+ created_at TEXT NOT NULL,
71
+ PRIMARY KEY (parent_node_id, child_node_id),
72
+ CHECK (parent_node_id != child_node_id),
73
+ FOREIGN KEY (image_session_id) REFERENCES image_sessions(id) ON DELETE CASCADE,
74
+ FOREIGN KEY (parent_node_id) REFERENCES image_nodes(id) ON DELETE CASCADE,
75
+ FOREIGN KEY (child_node_id) REFERENCES image_nodes(id) ON DELETE CASCADE,
76
+ FOREIGN KEY (operation_id) REFERENCES image_operations(id) ON DELETE CASCADE
77
+ );
78
+
79
+ CREATE TABLE IF NOT EXISTS image_operation_assets (
80
+ operation_id TEXT NOT NULL,
81
+ ordinal INTEGER NOT NULL CHECK (ordinal >= 0),
82
+ role TEXT NOT NULL CHECK (role IN ('reference', 'mask')),
83
+ upload_id TEXT NOT NULL,
84
+ content_sha256 TEXT NOT NULL,
85
+ media_type TEXT NOT NULL,
86
+ created_at TEXT NOT NULL,
87
+ PRIMARY KEY (operation_id, ordinal),
88
+ FOREIGN KEY (operation_id) REFERENCES image_operations(id) ON DELETE CASCADE,
89
+ FOREIGN KEY (content_sha256) REFERENCES uploaded_files(sha256) ON DELETE RESTRICT
90
+ );
91
+
92
+ CREATE TABLE IF NOT EXISTS image_operation_attempts (
93
+ id TEXT PRIMARY KEY,
94
+ operation_id TEXT NOT NULL,
95
+ attempt_number INTEGER NOT NULL CHECK (attempt_number > 0),
96
+ status TEXT NOT NULL CHECK (status IN ('started', 'succeeded', 'failed_terminal', 'failed_retryable', 'uncertain')),
97
+ provider TEXT NOT NULL,
98
+ capability_snapshot_json TEXT NOT NULL,
99
+ provider_request_id TEXT,
100
+ provider_conversation_id TEXT,
101
+ provider_response_id TEXT,
102
+ http_status INTEGER,
103
+ error_code TEXT,
104
+ error_message TEXT,
105
+ started_at TEXT NOT NULL,
106
+ completed_at TEXT,
107
+ UNIQUE (operation_id, attempt_number),
108
+ FOREIGN KEY (operation_id) REFERENCES image_operations(id) ON DELETE CASCADE
109
+ );
110
+
111
+ CREATE INDEX IF NOT EXISTS idx_image_attempts_operation
112
+ ON image_operation_attempts(operation_id, attempt_number DESC);
113
+
114
+ CREATE TABLE IF NOT EXISTS image_provider_state (
115
+ id TEXT PRIMARY KEY,
116
+ image_session_id TEXT NOT NULL,
117
+ head_node_id TEXT NOT NULL,
118
+ provider TEXT NOT NULL,
119
+ endpoint_fingerprint TEXT NOT NULL,
120
+ provider_conversation_id TEXT,
121
+ provider_response_id TEXT,
122
+ context_version INTEGER NOT NULL CHECK (context_version > 0),
123
+ reconstruction_mode TEXT NOT NULL CHECK (reconstruction_mode IN ('native', 'verified_replay', 'image_only')),
124
+ safe_state_json TEXT NOT NULL,
125
+ created_at TEXT NOT NULL,
126
+ updated_at TEXT NOT NULL,
127
+ UNIQUE (head_node_id, provider, endpoint_fingerprint),
128
+ FOREIGN KEY (image_session_id) REFERENCES image_sessions(id) ON DELETE CASCADE,
129
+ FOREIGN KEY (head_node_id) REFERENCES image_nodes(id) ON DELETE CASCADE
130
+ );
131
+
132
+ CREATE TABLE IF NOT EXISTS image_provider_capabilities (
133
+ endpoint_fingerprint TEXT PRIMARY KEY,
134
+ provider TEXT NOT NULL,
135
+ supports_responses INTEGER,
136
+ supports_conversations INTEGER,
137
+ supports_conversation_delete INTEGER,
138
+ supports_guaranteed_idempotency INTEGER NOT NULL DEFAULT 0,
139
+ probe_status TEXT NOT NULL CHECK (probe_status IN ('supported', 'partial', 'unsupported', 'error')),
140
+ safe_reason_code TEXT,
141
+ checked_at TEXT NOT NULL,
142
+ expires_at TEXT NOT NULL
143
+ );
144
+
145
+ CREATE TABLE IF NOT EXISTS image_provider_deletion_tombstones (
146
+ id TEXT PRIMARY KEY,
147
+ provider TEXT NOT NULL,
148
+ endpoint_fingerprint TEXT NOT NULL,
149
+ provider_conversation_id TEXT NOT NULL,
150
+ status TEXT NOT NULL CHECK (status IN ('pending', 'retrying', 'deleted', 'unsupported', 'abandoned')),
151
+ attempt_count INTEGER NOT NULL DEFAULT 0,
152
+ last_error_code TEXT,
153
+ next_attempt_at TEXT NOT NULL,
154
+ created_at TEXT NOT NULL,
155
+ updated_at TEXT NOT NULL,
156
+ UNIQUE (provider, endpoint_fingerprint, provider_conversation_id)
157
+ );
158
+ `);
159
+
160
+ const queueColumns = new Set(db.prepare(`PRAGMA table_info(queue)`).all().map((column) => column.name));
161
+ if (!queueColumns.has('image_operation_id')) {
162
+ db.exec(`ALTER TABLE queue ADD COLUMN image_operation_id TEXT`);
163
+ }
164
+ db.exec(`
165
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_queue_image_operation
166
+ ON queue(image_operation_id)
167
+ WHERE image_operation_id IS NOT NULL;
168
+ `);
169
+ }
170
+
171
+ export function createImageConversationRepository(db) {
172
+ const statements = {
173
+ insertSession: db.prepare(`
174
+ INSERT INTO image_sessions (id, conversation_id, origin, status, schema_version, created_at, updated_at)
175
+ VALUES (@id, @conversationId, @origin, 'active', @schemaVersion, @createdAt, @updatedAt)
176
+ `),
177
+ touchSession: db.prepare(`UPDATE image_sessions SET updated_at = ? WHERE id = ?`),
178
+ getSession: db.prepare(`SELECT * FROM image_sessions WHERE id = ?`),
179
+ insertOperation: db.prepare(`
180
+ INSERT INTO image_operations (
181
+ id, image_session_id, source_message_id, queue_message_id, kind, parent_node_id,
182
+ prompt, selected_image_model, orchestration_model, provider, execution_mode,
183
+ parameters_json, request_fingerprint, idempotency_key, replaces_operation_id, created_at
184
+ ) VALUES (
185
+ @id, @imageSessionId, @sourceMessageId, @queueMessageId, @kind, @parentNodeId,
186
+ @prompt, @selectedImageModel, @orchestrationModel, @provider, @executionMode,
187
+ @parametersJson, @requestFingerprint, @idempotencyKey, @replacesOperationId, @createdAt
188
+ )
189
+ `),
190
+ getOperation: db.prepare(`
191
+ SELECT operation.*, session.conversation_id
192
+ FROM image_operations operation
193
+ JOIN image_sessions session ON session.id = operation.image_session_id
194
+ WHERE operation.id = ?
195
+ `),
196
+ getOperationByQueueMessageId: db.prepare(`
197
+ SELECT operation.*, session.conversation_id
198
+ FROM image_operations operation
199
+ JOIN image_sessions session ON session.id = operation.image_session_id
200
+ WHERE operation.queue_message_id = ?
201
+ `),
202
+ linkQueueOperation: db.prepare(`UPDATE queue SET image_operation_id = ? WHERE id = ? AND image_operation_id IS NULL`),
203
+ insertAsset: db.prepare(`
204
+ INSERT INTO image_operation_assets (
205
+ operation_id, ordinal, role, upload_id, content_sha256, media_type, created_at
206
+ ) VALUES (@operationId, @ordinal, @role, @uploadId, @contentSha256, @mediaType, @createdAt)
207
+ `),
208
+ listAssets: db.prepare(`
209
+ SELECT asset.*, file.original_name, file.size_bytes
210
+ FROM image_operation_assets asset
211
+ JOIN uploaded_files file ON file.sha256 = asset.content_sha256
212
+ WHERE asset.operation_id = ?
213
+ ORDER BY asset.ordinal ASC
214
+ `),
215
+ getNode: db.prepare(`
216
+ SELECT node.*, session.conversation_id
217
+ FROM image_nodes node
218
+ JOIN image_sessions session ON session.id = node.image_session_id
219
+ WHERE node.id = ?
220
+ `),
221
+ getNodeByAttachment: db.prepare(`
222
+ SELECT node.*, session.conversation_id
223
+ FROM image_nodes node
224
+ JOIN image_sessions session ON session.id = node.image_session_id
225
+ WHERE node.assistant_message_id = ? AND node.attachment_image_id = ?
226
+ `),
227
+ insertNode: db.prepare(`
228
+ INSERT INTO image_nodes (
229
+ id, image_session_id, operation_id, assistant_message_id, attachment_image_id, output_index, created_at
230
+ ) VALUES (@id, @imageSessionId, @operationId, @assistantMessageId, @attachmentImageId, @outputIndex, @createdAt)
231
+ `),
232
+ insertEdge: db.prepare(`
233
+ INSERT INTO image_edges (
234
+ image_session_id, parent_node_id, child_node_id, operation_id, relation, created_at
235
+ ) VALUES (@imageSessionId, @parentNodeId, @childNodeId, @operationId, 'edit', @createdAt)
236
+ `),
237
+ findPathToNode: db.prepare(`
238
+ WITH RECURSIVE descendants(node_id) AS (
239
+ SELECT child_node_id FROM image_edges WHERE parent_node_id = ?
240
+ UNION
241
+ SELECT edge.child_node_id
242
+ FROM image_edges edge
243
+ JOIN descendants ON edge.parent_node_id = descendants.node_id
244
+ )
245
+ SELECT 1 AS found FROM descendants WHERE node_id = ? LIMIT 1
246
+ `),
247
+ getLatestAttempt: db.prepare(`
248
+ SELECT * FROM image_operation_attempts
249
+ WHERE operation_id = ?
250
+ ORDER BY attempt_number DESC
251
+ LIMIT 1
252
+ `),
253
+ insertAttempt: db.prepare(`
254
+ INSERT INTO image_operation_attempts (
255
+ id, operation_id, attempt_number, status, provider, capability_snapshot_json, started_at
256
+ ) VALUES (@id, @operationId, @attemptNumber, 'started', @provider, @capabilitySnapshotJson, @startedAt)
257
+ `),
258
+ finishAttempt: db.prepare(`
259
+ UPDATE image_operation_attempts
260
+ SET status = @status,
261
+ provider_request_id = @providerRequestId,
262
+ provider_conversation_id = @providerConversationId,
263
+ provider_response_id = @providerResponseId,
264
+ http_status = @httpStatus,
265
+ error_code = @errorCode,
266
+ error_message = @errorMessage,
267
+ completed_at = @completedAt
268
+ WHERE id = @id AND status = 'started'
269
+ `),
270
+ listNodesForOperation: db.prepare(`
271
+ SELECT node.*, message.attachments, message.text, message.model, message.mode, message.timestamp
272
+ FROM image_nodes node
273
+ JOIN messages message ON message.id = node.assistant_message_id
274
+ WHERE node.operation_id = ?
275
+ ORDER BY node.output_index ASC
276
+ `),
277
+ getParentEdge: db.prepare(`SELECT * FROM image_edges WHERE child_node_id = ?`),
278
+ listProviderStatesForConversation: db.prepare(`
279
+ SELECT DISTINCT state.provider, state.endpoint_fingerprint, state.provider_conversation_id
280
+ FROM image_provider_state state
281
+ JOIN image_sessions session ON session.id = state.image_session_id
282
+ WHERE session.conversation_id = ? AND state.provider_conversation_id IS NOT NULL
283
+ `),
284
+ insertDeletionTombstone: db.prepare(`
285
+ INSERT INTO image_provider_deletion_tombstones (
286
+ id, provider, endpoint_fingerprint, provider_conversation_id, status,
287
+ attempt_count, next_attempt_at, created_at, updated_at
288
+ ) VALUES (@id, @provider, @endpointFingerprint, @providerConversationId, 'pending', 0, @nextAttemptAt, @createdAt, @updatedAt)
289
+ ON CONFLICT(provider, endpoint_fingerprint, provider_conversation_id) DO NOTHING
290
+ `),
291
+ };
292
+
293
+ function insertEdgeChecked(edge) {
294
+ const parent = statements.getNode.get(edge.parentNodeId);
295
+ const child = statements.getNode.get(edge.childNodeId);
296
+ if (!parent || !child) throw new Error('Image lineage node not found');
297
+ if (parent.image_session_id !== child.image_session_id || parent.image_session_id !== edge.imageSessionId) {
298
+ throw new Error('Image lineage nodes must belong to the same image session');
299
+ }
300
+ if (parent.conversation_id !== child.conversation_id) {
301
+ throw new Error('Image lineage nodes must belong to the same conversation');
302
+ }
303
+ if (Date.parse(parent.created_at) > Date.parse(child.created_at)) {
304
+ throw new Error('Image lineage parent cannot postdate its child');
305
+ }
306
+ if (statements.findPathToNode.get(edge.childNodeId, edge.parentNodeId)) {
307
+ throw new Error('Image lineage cycle detected');
308
+ }
309
+ return statements.insertEdge.run(edge);
310
+ }
311
+
312
+ return {
313
+ ...statements,
314
+ imageSchemaVersion: IMAGE_SCHEMA_VERSION,
315
+ insertEdgeChecked,
316
+ };
317
+ }