@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,497 @@
1
+ 'use strict';
2
+
3
+ function normalizeId(value) {
4
+ return String(value || '').trim();
5
+ }
6
+
7
+ function normalizeMessageRole(value) {
8
+ const role = String(value || '').trim().toLowerCase();
9
+ if (role === 'assistant' || role === 'user') return role;
10
+ return '';
11
+ }
12
+
13
+ function normalizeMessageTimestamp(value, fallbackIso) {
14
+ const text = String(value || '').trim();
15
+ if (!text) return fallbackIso;
16
+ const parsed = Date.parse(text);
17
+ if (!Number.isFinite(parsed)) return fallbackIso;
18
+ return new Date(parsed).toISOString();
19
+ }
20
+
21
+ function ensureArray(value) {
22
+ return Array.isArray(value) ? value : [];
23
+ }
24
+
25
+ function parseAttachments(value) {
26
+ if (Array.isArray(value)) return value.filter((attachment) => attachment && typeof attachment === 'object');
27
+ if (typeof value !== 'string' || !value.trim()) return [];
28
+ try {
29
+ const parsed = JSON.parse(value);
30
+ return Array.isArray(parsed) ? parsed.filter((attachment) => attachment && typeof attachment === 'object') : [];
31
+ } catch {
32
+ return [];
33
+ }
34
+ }
35
+
36
+ const SHA256_PATTERN = /^[a-f0-9]{64}$/;
37
+
38
+ function attachmentSha256(attachment) {
39
+ const sha256 = String(attachment?.sha256 || '').trim().toLowerCase();
40
+ return SHA256_PATTERN.test(sha256) ? sha256 : '';
41
+ }
42
+
43
+ function attachmentSizeBytes(attachment) {
44
+ const size = Number(attachment?.size ?? attachment?.size_bytes ?? attachment?.sizeBytes);
45
+ return Number.isFinite(size) && size > 0 ? Math.trunc(size) : 0;
46
+ }
47
+
48
+ function attachmentSignature(attachments, detail = { sha: true, size: true }) {
49
+ const parts = ensureArray(attachments)
50
+ .map((attachment) => {
51
+ const sha256 = detail.sha ? attachmentSha256(attachment) : '';
52
+ if (sha256) return `sha:${sha256}`;
53
+ const name = String(attachment?.name || attachment?.displayName || '').trim().toLowerCase();
54
+ const type = String(attachment?.type || attachment?.mimeType || '').trim().toLowerCase();
55
+ if (!name && !type) return '';
56
+ const size = detail.size ? attachmentSizeBytes(attachment) : 0;
57
+ return `${name}\u0000${type}${size ? `\u0000${size}` : ''}`;
58
+ })
59
+ .filter(Boolean)
60
+ .sort();
61
+ return parts.length ? parts.join('\u0001') : '';
62
+ }
63
+
64
+ // SDK hint attachments never carry a sha256 in the fallback matching path
65
+ // (sha-bearing attachments are persisted directly) and their reported sizes
66
+ // are unreliable, so matching only uses the fields the hints actually have.
67
+ function signatureDetailForHints(hintAttachments) {
68
+ const hints = ensureArray(hintAttachments).filter((attachment) => attachment && typeof attachment === 'object');
69
+ return {
70
+ sha: hints.length > 0 && hints.every((attachment) => !!attachmentSha256(attachment)),
71
+ size: false,
72
+ };
73
+ }
74
+
75
+ function attachmentTimestampDistance(left, right) {
76
+ const leftMs = Date.parse(String(left || ''));
77
+ const rightMs = Date.parse(String(right || ''));
78
+ if (!Number.isFinite(leftMs) || !Number.isFinite(rightMs)) return Number.POSITIVE_INFINITY;
79
+ return Math.abs(leftMs - rightMs);
80
+ }
81
+
82
+ function candidateSignatureForHints(candidateAttachments, hintAttachments) {
83
+ const hints = ensureArray(hintAttachments);
84
+ const imageOnlyHints = hints.length > 0
85
+ && hints.every((attachment) => String(attachment?.type || attachment?.mimeType || '').trim().toLowerCase().startsWith('image/'));
86
+ const comparable = imageOnlyHints
87
+ ? ensureArray(candidateAttachments).filter((attachment) => (
88
+ String(attachment?.type || attachment?.mimeType || '').trim().toLowerCase().startsWith('image/')
89
+ ))
90
+ : candidateAttachments;
91
+ return attachmentSignature(comparable, signatureDetailForHints(hintAttachments));
92
+ }
93
+
94
+ function normalizeActivityEntry(value) {
95
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
96
+ const text = String(value.text || '').trim();
97
+ if (!text) return null;
98
+ const subagentRunId = value.subagentRunId ? String(value.subagentRunId).trim() : null;
99
+ const metadata = (value.metadata && typeof value.metadata === 'object' && !Array.isArray(value.metadata))
100
+ ? value.metadata
101
+ : null;
102
+ return { text, subagentRunId, metadata };
103
+ }
104
+ const text = String(value || '').trim();
105
+ if (!text) return null;
106
+ return { text, subagentRunId: null, metadata: null };
107
+ }
108
+
109
+ function normalizeThoughtEntry(value, index) {
110
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
111
+ const text = String(value.text || '').trim();
112
+ if (!text) return null;
113
+ return {
114
+ reasoningId: String(value.reasoningId || '').trim() || `history-thought-${index + 1}`,
115
+ text,
116
+ done: value.done !== false,
117
+ subagentRunId: value.subagentRunId ? String(value.subagentRunId).trim() : null,
118
+ };
119
+ }
120
+
121
+ export function createSessionHistoryRefreshService({
122
+ db,
123
+ stmts,
124
+ parseSessionEventsToMessages = null,
125
+ inFlightStateForConversation = null,
126
+ isDeletedSdkSession = null,
127
+ } = {}) {
128
+ const messageColumns = new Set(
129
+ db.prepare(`PRAGMA table_info(messages)`).all().map((column) => String(column?.name || '').trim()),
130
+ );
131
+ const messagesSupportShareVisibility = messageColumns.has('hidden_from_shares')
132
+ && messageColumns.has('share_hidden_at');
133
+ const listExistingMessageAttachments = messageColumns.has('attachments')
134
+ ? db.prepare(`
135
+ SELECT id, attachments, timestamp
136
+ FROM messages
137
+ WHERE conversation_id = ? AND attachments IS NOT NULL
138
+ `)
139
+ : null;
140
+ const listHiddenMessageVisibility = messagesSupportShareVisibility
141
+ ? db.prepare(`
142
+ SELECT id, role, text, timestamp, share_hidden_at
143
+ FROM messages
144
+ WHERE conversation_id = ? AND hidden_from_shares = 1
145
+ `)
146
+ : null;
147
+ const restoreHiddenMessageVisibility = messagesSupportShareVisibility
148
+ ? db.prepare(`
149
+ UPDATE messages
150
+ SET hidden_from_shares = 1, share_hidden_at = ?
151
+ WHERE conversation_id = ? AND id = ?
152
+ `)
153
+ : null;
154
+ const countBusyQueueByConversation = db.prepare(`
155
+ SELECT COUNT(*) AS cnt
156
+ FROM queue
157
+ WHERE conversation_id = ?
158
+ AND status IN ('pending', 'processing', 'parked')
159
+ `);
160
+ const countMessagesByConversation = db.prepare(`
161
+ SELECT COUNT(*) AS cnt
162
+ FROM messages
163
+ WHERE conversation_id = ?
164
+ `);
165
+ const tableNames = new Set(
166
+ db.prepare(`SELECT name FROM sqlite_master WHERE type = 'table'`).all()
167
+ .map((row) => String(row?.name || '').trim()),
168
+ );
169
+ const queueColumns = tableNames.has('queue')
170
+ ? new Set(db.prepare(`PRAGMA table_info(queue)`).all().map((column) => String(column?.name || '').trim()))
171
+ : new Set();
172
+ const listQueuedAttachments = queueColumns.has('attachments') && queueColumns.has('timestamp')
173
+ ? db.prepare(`
174
+ SELECT id, attachments, timestamp
175
+ FROM queue
176
+ WHERE conversation_id = ? AND attachments IS NOT NULL
177
+ `)
178
+ : null;
179
+ const listReferencedAttachments = tableNames.has('upload_refs') && tableNames.has('uploaded_files')
180
+ ? db.prepare(`
181
+ SELECT
182
+ ref.message_id,
183
+ MIN(ref.created_at) AS timestamp,
184
+ file.sha256,
185
+ file.original_name,
186
+ file.mime_type,
187
+ file.size_bytes
188
+ FROM upload_refs ref
189
+ JOIN uploaded_files file ON file.sha256 = ref.file_sha256
190
+ WHERE ref.conversation_id = ?
191
+ GROUP BY ref.message_id, file.sha256, file.original_name, file.mime_type, file.size_bytes
192
+ ORDER BY ref.message_id, MIN(ref.created_at), file.original_name
193
+ `)
194
+ : null;
195
+ const insertUploadRef = tableNames.has('upload_refs')
196
+ ? db.prepare(`
197
+ INSERT OR IGNORE INTO upload_refs (file_sha256, conversation_id, message_id, created_at)
198
+ VALUES (?, ?, ?, ?)
199
+ `)
200
+ : null;
201
+
202
+ const deleteConversationMessages = typeof stmts?.deleteConvMsg?.run === 'function'
203
+ ? stmts.deleteConvMsg
204
+ : db.prepare(`DELETE FROM messages WHERE conversation_id = ?`);
205
+ const deleteConversationActivity = typeof stmts?.deleteConvActivity?.run === 'function'
206
+ ? stmts.deleteConvActivity
207
+ : db.prepare(`DELETE FROM relay_activity WHERE conversation_id = ?`);
208
+ const deleteConversationThoughts = typeof stmts?.deleteConvThoughts?.run === 'function'
209
+ ? stmts.deleteConvThoughts
210
+ : db.prepare(`DELETE FROM relay_thought WHERE conversation_id = ?`);
211
+ const deleteConversationStreamEvents = typeof stmts?.deleteConvStreamEvents?.run === 'function'
212
+ ? stmts.deleteConvStreamEvents
213
+ : db.prepare(`DELETE FROM relay_stream_events WHERE conversation_id = ?`);
214
+ const deleteConversationSubagentRuns = typeof stmts?.deleteConvSubagentRuns?.run === 'function'
215
+ ? stmts.deleteConvSubagentRuns
216
+ : db.prepare(`DELETE FROM subagent_runs WHERE conversation_id = ?`);
217
+ // workflow_runs rows key on assistant message ids; a history rebuild
218
+ // replaces those ids, so the rows must go with the messages they annotated
219
+ // (same lifecycle as subagent_runs). The fallback guards dbs opened before
220
+ // the table existed.
221
+ const deleteConversationWorkflowRuns = typeof stmts?.deleteConvWorkflowRuns?.run === 'function'
222
+ ? stmts.deleteConvWorkflowRuns
223
+ : db.prepare(`DELETE FROM workflow_runs WHERE conversation_id = ?`);
224
+
225
+ function collectAttachmentCandidates(conversationId) {
226
+ const candidates = new Map();
227
+ const addCandidate = (sourceMessageId, attachments, timestamp, priority) => {
228
+ const parsed = parseAttachments(attachments);
229
+ const signature = attachmentSignature(parsed);
230
+ if (!signature) return;
231
+ const key = normalizeId(sourceMessageId) || `${signature}:${timestamp || ''}`;
232
+ const existing = candidates.get(key);
233
+ if (existing && existing.priority <= priority) return;
234
+ candidates.set(key, {
235
+ sourceMessageId: key,
236
+ attachments: parsed,
237
+ signature,
238
+ timestamp: String(timestamp || '').trim(),
239
+ priority,
240
+ });
241
+ };
242
+
243
+ for (const row of listExistingMessageAttachments?.all(conversationId) || []) {
244
+ addCandidate(row.id, row.attachments, row.timestamp, 0);
245
+ }
246
+ for (const row of listQueuedAttachments?.all(conversationId) || []) {
247
+ addCandidate(row.id, row.attachments, row.timestamp, 1);
248
+ }
249
+
250
+ const referencedByMessage = new Map();
251
+ for (const row of listReferencedAttachments?.all(conversationId) || []) {
252
+ const messageId = normalizeId(row.message_id);
253
+ if (!messageId) continue;
254
+ const group = referencedByMessage.get(messageId) || {
255
+ timestamp: row.timestamp,
256
+ attachments: [],
257
+ };
258
+ group.attachments.push({
259
+ name: String(row.original_name || '').trim(),
260
+ type: String(row.mime_type || '').trim().toLowerCase(),
261
+ size: Number(row.size_bytes || 0),
262
+ sha256: String(row.sha256 || '').trim().toLowerCase(),
263
+ });
264
+ referencedByMessage.set(messageId, group);
265
+ }
266
+ for (const [messageId, group] of referencedByMessage) {
267
+ addCandidate(messageId, group.attachments, group.timestamp, 2);
268
+ }
269
+ return Array.from(candidates.values());
270
+ }
271
+
272
+ function resolveRebuiltAttachments(message, candidates, consumedCandidates) {
273
+ const messageAttachments = parseAttachments(message?.attachments);
274
+ const persistedMessageAttachments = messageAttachments.filter((attachment) => (
275
+ String(attachment?.sha256 || '').trim()
276
+ || String(attachment?.contentUrl || '').trim()
277
+ || String(attachment?.dataUrl || '').trim()
278
+ || attachment?.generatedImage
279
+ ));
280
+ if (persistedMessageAttachments.length) return persistedMessageAttachments;
281
+
282
+ const messageId = normalizeId(message?.id);
283
+ const sameIdCandidate = candidates.find((candidate) => candidate.sourceMessageId === messageId);
284
+ if (sameIdCandidate) {
285
+ consumedCandidates.add(sameIdCandidate.sourceMessageId);
286
+ return sameIdCandidate.attachments;
287
+ }
288
+
289
+ const signature = attachmentSignature(messageAttachments, signatureDetailForHints(messageAttachments));
290
+ if (!signature) return [];
291
+ const matching = candidates
292
+ .filter((candidate) => (
293
+ !consumedCandidates.has(candidate.sourceMessageId)
294
+ && candidateSignatureForHints(candidate.attachments, messageAttachments) === signature
295
+ ))
296
+ .sort((left, right) => (
297
+ attachmentTimestampDistance(left.timestamp, message?.timestamp)
298
+ - attachmentTimestampDistance(right.timestamp, message?.timestamp)
299
+ || left.priority - right.priority
300
+ ));
301
+ const selected = matching[0];
302
+ if (!selected) return [];
303
+ consumedCandidates.add(selected.sourceMessageId);
304
+ return selected.attachments;
305
+ }
306
+
307
+ function insertRebuiltMessages(conversationId, messages = [], attachmentCandidates = []) {
308
+ const nowIso = new Date().toISOString();
309
+ const consumedAttachmentCandidates = new Set();
310
+ for (const message of ensureArray(messages)) {
311
+ const role = normalizeMessageRole(message?.role);
312
+ if (!role) continue;
313
+ const messageId = normalizeId(message?.id);
314
+ if (!messageId) continue;
315
+ const timestamp = normalizeMessageTimestamp(message?.timestamp, nowIso);
316
+ const attachments = resolveRebuiltAttachments(message, attachmentCandidates, consumedAttachmentCandidates);
317
+ stmts.insertMsg.run(
318
+ messageId,
319
+ conversationId,
320
+ role,
321
+ String(message?.text || ''),
322
+ role === 'assistant' ? (String(message?.model || '').trim() || null) : null,
323
+ String(message?.mode || '').trim() || null,
324
+ attachments.length ? JSON.stringify(attachments) : null,
325
+ timestamp,
326
+ null,
327
+ role === 'assistant' ? (String(message?.model || '').trim() || null) : null,
328
+ null,
329
+ );
330
+ for (const attachment of attachments) {
331
+ const sha256 = String(attachment?.sha256 || '').trim().toLowerCase();
332
+ if (!/^[a-f0-9]{64}$/.test(sha256)) continue;
333
+ insertUploadRef?.run(sha256, conversationId, messageId, timestamp);
334
+ }
335
+ if (role !== 'assistant') continue;
336
+ const activities = ensureArray(message?.activities)
337
+ .map((value) => normalizeActivityEntry(value))
338
+ .filter(Boolean);
339
+ for (const activity of activities) {
340
+ stmts.insertActivity.run(
341
+ String(message?.sourceMessageId || messageId),
342
+ messageId,
343
+ conversationId,
344
+ String(message?.mode || 'agent').trim() || 'agent',
345
+ activity.text,
346
+ timestamp,
347
+ activity.subagentRunId,
348
+ activity.metadata ? JSON.stringify(activity.metadata) : null,
349
+ );
350
+ }
351
+ const thoughts = ensureArray(message?.thoughts)
352
+ .map((value, index) => normalizeThoughtEntry(value, index))
353
+ .filter(Boolean);
354
+ for (let index = 0; index < thoughts.length; index += 1) {
355
+ const thought = thoughts[index];
356
+ if (typeof stmts.insertThought?.run !== 'function') continue;
357
+ stmts.insertThought.run(
358
+ String(message?.sourceMessageId || messageId),
359
+ messageId,
360
+ conversationId,
361
+ String(message?.mode || 'agent').trim() || 'agent',
362
+ thought.reasoningId,
363
+ index + 1,
364
+ thought.text,
365
+ thought.done ? 1 : 0,
366
+ timestamp,
367
+ thought.subagentRunId,
368
+ );
369
+ }
370
+ }
371
+ }
372
+
373
+ function normalizeHiddenMatchText(value) {
374
+ return String(value || '').replace(/\s+/g, ' ').trim();
375
+ }
376
+
377
+ function resolveHiddenMessageId(hiddenMessage, rebuiltMessages, consumedIds) {
378
+ const hiddenId = normalizeId(hiddenMessage?.id);
379
+ const sameIdMatch = rebuiltMessages.find((message) => normalizeId(message?.id) === hiddenId);
380
+ if (sameIdMatch) {
381
+ consumedIds.add(hiddenId);
382
+ return hiddenId;
383
+ }
384
+ const role = normalizeMessageRole(hiddenMessage?.role);
385
+ const text = normalizeHiddenMatchText(hiddenMessage?.text);
386
+ if (!role || !text) return '';
387
+ const matching = rebuiltMessages
388
+ .filter((message) => {
389
+ const messageId = normalizeId(message?.id);
390
+ return messageId
391
+ && !consumedIds.has(messageId)
392
+ && normalizeMessageRole(message?.role) === role
393
+ && normalizeHiddenMatchText(message?.text) === text;
394
+ })
395
+ .sort((left, right) => (
396
+ attachmentTimestampDistance(left?.timestamp, hiddenMessage?.timestamp)
397
+ - attachmentTimestampDistance(right?.timestamp, hiddenMessage?.timestamp)
398
+ ));
399
+ const selected = matching[0];
400
+ if (!selected) return '';
401
+ const selectedId = normalizeId(selected.id);
402
+ consumedIds.add(selectedId);
403
+ return selectedId;
404
+ }
405
+
406
+ const clearRetrievableHistoryTx = db.transaction((conversationId) => {
407
+ deleteConversationMessages.run(conversationId);
408
+ deleteConversationActivity.run(conversationId);
409
+ deleteConversationThoughts.run(conversationId);
410
+ deleteConversationStreamEvents.run(conversationId);
411
+ deleteConversationSubagentRuns.run(conversationId);
412
+ deleteConversationWorkflowRuns.run(conversationId);
413
+ });
414
+
415
+ const persistRebuiltHistoryTx = db.transaction((conversationId, messages = []) => {
416
+ insertRebuiltMessages(conversationId, messages);
417
+ });
418
+
419
+ const replaceRetrievableHistoryTx = db.transaction((conversationId, messages = []) => {
420
+ const hiddenMessages = listHiddenMessageVisibility?.all(conversationId) || [];
421
+ const attachmentCandidates = collectAttachmentCandidates(conversationId);
422
+ deleteConversationMessages.run(conversationId);
423
+ deleteConversationActivity.run(conversationId);
424
+ deleteConversationThoughts.run(conversationId);
425
+ deleteConversationStreamEvents.run(conversationId);
426
+ deleteConversationSubagentRuns.run(conversationId);
427
+ deleteConversationWorkflowRuns.run(conversationId);
428
+ insertRebuiltMessages(conversationId, messages, attachmentCandidates);
429
+ const rebuiltMessages = ensureArray(messages);
430
+ const consumedHiddenIds = new Set();
431
+ for (const hiddenMessage of hiddenMessages) {
432
+ const resolvedId = resolveHiddenMessageId(hiddenMessage, rebuiltMessages, consumedHiddenIds);
433
+ if (!resolvedId) continue;
434
+ restoreHiddenMessageVisibility.run(
435
+ hiddenMessage.share_hidden_at || new Date().toISOString(),
436
+ conversationId,
437
+ resolvedId,
438
+ );
439
+ }
440
+ });
441
+
442
+ function evaluateRefreshIdleState(conversationId) {
443
+ const sid = normalizeId(conversationId);
444
+ if (!sid) return { idle: false, reason: 'missing-conversation-id' };
445
+ const queueBusyCount = Number(countBusyQueueByConversation.get(sid)?.cnt || 0);
446
+ if (queueBusyCount > 0) return { idle: false, reason: 'queue-busy' };
447
+ if (typeof inFlightStateForConversation === 'function') {
448
+ const inFlight = inFlightStateForConversation(sid);
449
+ const status = String(inFlight?.status || '').trim().toLowerCase();
450
+ if (status === 'processing' || status === 'pending' || status === 'parked') {
451
+ return { idle: false, reason: 'turn-processing' };
452
+ }
453
+ }
454
+ return { idle: true };
455
+ }
456
+
457
+ function mapSdkEventsToMessages(events = []) {
458
+ if (typeof parseSessionEventsToMessages !== 'function') return [];
459
+ return parseSessionEventsToMessages(ensureArray(events));
460
+ }
461
+
462
+ function countRetrievableMessages(conversationId) {
463
+ const sid = normalizeId(conversationId);
464
+ if (!sid) return 0;
465
+ return Number(countMessagesByConversation.get(sid)?.cnt || 0);
466
+ }
467
+
468
+ function clearRetrievableHistory(conversationId) {
469
+ const sid = normalizeId(conversationId);
470
+ if (!sid) return false;
471
+ clearRetrievableHistoryTx(sid);
472
+ return true;
473
+ }
474
+
475
+ function persistRebuiltHistory(conversationId, messages = []) {
476
+ const sid = normalizeId(conversationId);
477
+ if (!sid) return { insertedCount: 0 };
478
+ persistRebuiltHistoryTx(sid, messages);
479
+ return { insertedCount: ensureArray(messages).length };
480
+ }
481
+
482
+ function replaceRetrievableHistory(conversationId, messages = []) {
483
+ const sid = normalizeId(conversationId);
484
+ if (!sid) return { insertedCount: 0 };
485
+ replaceRetrievableHistoryTx(sid, messages);
486
+ return { insertedCount: ensureArray(messages).length };
487
+ }
488
+
489
+ return {
490
+ evaluateRefreshIdleState,
491
+ mapSdkEventsToMessages,
492
+ countRetrievableMessages,
493
+ clearRetrievableHistory,
494
+ persistRebuiltHistory,
495
+ replaceRetrievableHistory,
496
+ };
497
+ }