@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,124 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import crypto from "crypto";
4
+
5
+ function hashToken(value) {
6
+ const token = String(value || "").trim();
7
+ if (!token) return "";
8
+ return crypto.createHash("sha256").update(token).digest("hex");
9
+ }
10
+
11
+ function safeReadLock(lockPath) {
12
+ try {
13
+ const raw = fs.readFileSync(lockPath, "utf8");
14
+ const parsed = JSON.parse(raw);
15
+ // Prefer the hashed field; fall back to hashing a legacy plaintext `token`
16
+ // so a lock written by an older build still matches (and is rewritten 0600).
17
+ const tokenHash = typeof parsed?.tokenHash === "string" && parsed.tokenHash
18
+ ? parsed.tokenHash
19
+ : hashToken(parsed?.token);
20
+ return {
21
+ pid: Number.parseInt(String(parsed?.pid ?? ""), 10),
22
+ tokenHash,
23
+ startedAt: typeof parsed?.startedAt === "string" ? parsed.startedAt : null,
24
+ };
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
29
+
30
+ export function createRelaySingletonGuard({
31
+ lockPath,
32
+ pid,
33
+ token,
34
+ now = () => new Date().toISOString(),
35
+ isProcessAlive,
36
+ logger = console,
37
+ }) {
38
+ const lockFilePath = path.resolve(String(lockPath || ""));
39
+ const currentPid = Number.parseInt(String(pid || process.pid), 10);
40
+ const currentTokenHash = hashToken(token);
41
+ const processAlive = typeof isProcessAlive === "function"
42
+ ? isProcessAlive
43
+ : (candidatePid) => {
44
+ if (!Number.isInteger(candidatePid) || candidatePid <= 0) return false;
45
+ try {
46
+ process.kill(candidatePid, 0);
47
+ return true;
48
+ } catch {
49
+ return false;
50
+ }
51
+ };
52
+
53
+ function writeLock() {
54
+ fs.mkdirSync(path.dirname(lockFilePath), { recursive: true });
55
+ const payload = {
56
+ pid: currentPid,
57
+ // Store only a hash of the auth token: the guard needs equality for the
58
+ // self-ownership check, never the token itself. Keeps the master
59
+ // credential out of a file that has historically been world-readable.
60
+ tokenHash: currentTokenHash,
61
+ startedAt: now(),
62
+ };
63
+ const fd = fs.openSync(lockFilePath, "wx", 0o600);
64
+ try {
65
+ fs.writeFileSync(fd, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
66
+ } finally {
67
+ try { fs.closeSync(fd); } catch {}
68
+ }
69
+ return payload;
70
+ }
71
+
72
+ function acquire() {
73
+ try {
74
+ return writeLock();
75
+ } catch (error) {
76
+ if (error?.code !== "EEXIST") {
77
+ throw error;
78
+ }
79
+ }
80
+
81
+ const existingLock = safeReadLock(lockFilePath);
82
+ const existingPid = Number.isInteger(existingLock?.pid) ? existingLock.pid : null;
83
+ const existingTokenHash = typeof existingLock?.tokenHash === "string" ? existingLock.tokenHash : "";
84
+ const existingStartedAt = existingLock?.startedAt || "unknown";
85
+
86
+ if (existingPid && processAlive(existingPid)) {
87
+ if (existingTokenHash && existingTokenHash === currentTokenHash) {
88
+ throw new Error(`Relay already running (pid=${existingPid}, startedAt=${existingStartedAt}).`);
89
+ }
90
+ throw new Error(
91
+ `Relay appears to be owned by another live process (pid=${existingPid}, startedAt=${existingStartedAt}).`
92
+ );
93
+ }
94
+
95
+ try {
96
+ fs.unlinkSync(lockFilePath);
97
+ logger?.warn?.(`[server] Recovered stale singleton lock: ${lockFilePath}`);
98
+ } catch (unlinkError) {
99
+ if (unlinkError?.code !== "ENOENT") throw unlinkError;
100
+ }
101
+
102
+ return writeLock();
103
+ }
104
+
105
+ function release() {
106
+ const existingLock = safeReadLock(lockFilePath);
107
+ if (!existingLock) return false;
108
+ if (existingLock.pid !== currentPid) return false;
109
+ if (String(existingLock.tokenHash || "") !== currentTokenHash) return false;
110
+ try {
111
+ fs.unlinkSync(lockFilePath);
112
+ return true;
113
+ } catch (error) {
114
+ if (error?.code === "ENOENT") return true;
115
+ throw error;
116
+ }
117
+ }
118
+
119
+ return {
120
+ lockFilePath,
121
+ acquire,
122
+ release,
123
+ };
124
+ }
@@ -0,0 +1,57 @@
1
+ // Safe headers for streaming stored bytes (workspace files, uploads, shared
2
+ // attachments) back to the browser.
3
+ //
4
+ // The threat: a semi-trusted producer (an AI worker writing a workspace file, or
5
+ // an uploaded attachment) can supply content whose MIME type the browser will
6
+ // execute as markup/script on THIS origin — `text/html`, `image/svg+xml`, XML
7
+ // with embedded script. Served inline, that is stored XSS against the app (and,
8
+ // via the unauthenticated /api/shared/* routes, against share viewers). We
9
+ // neutralize such types to `text/plain` and always send `nosniff`, and we attach
10
+ // a locked-down CSP + `sandbox` so anything that still reaches the browser as a
11
+ // document can neither run script nor navigate.
12
+
13
+ // Types the browser renders as active documents on this origin.
14
+ const ACTIVE_MARKUP_PATTERN = /^text\/html\b|svg|xhtml|(^|\/)xml\b|\+xml\b/i;
15
+
16
+ // Types safe to preview inline with their real Content-Type.
17
+ const INLINE_SAFE_PREFIXES = ['image/', 'video/', 'audio/'];
18
+ const INLINE_SAFE_EXACT = new Set(['application/pdf']);
19
+
20
+ /**
21
+ * Resolve the Content-Type to send for stored bytes, neutralizing types the
22
+ * browser would execute. Returns the (possibly coerced) type plus whether it is
23
+ * safe to display inline.
24
+ */
25
+ export function safeServedContentType(mimeType) {
26
+ const type = String(mimeType || '').trim().toLowerCase().split(';')[0].slice(0, 127)
27
+ || 'application/octet-stream';
28
+ if (ACTIVE_MARKUP_PATTERN.test(type)) {
29
+ // Render markup/script content as its own source text instead of executing it.
30
+ return { contentType: 'text/plain; charset=utf-8', inlineSafe: false, neutralized: true };
31
+ }
32
+ const inlineSafe = INLINE_SAFE_PREFIXES.some((prefix) => type.startsWith(prefix))
33
+ || INLINE_SAFE_EXACT.has(type);
34
+ return { contentType: type, inlineSafe, neutralized: false };
35
+ }
36
+
37
+ /**
38
+ * Apply the safe Content-Type plus the hardening headers to a response that is
39
+ * about to stream stored bytes. Returns the resolved descriptor so callers can
40
+ * choose an inline vs. attachment disposition.
41
+ */
42
+ export function applySafeServedContentHeaders(res, mimeType, { fileName = '' } = {}) {
43
+ const resolved = safeServedContentType(mimeType);
44
+ res.setHeader('Content-Type', resolved.contentType);
45
+ res.setHeader('X-Content-Type-Options', 'nosniff');
46
+ // Belt-and-braces: a served file is never part of the app UI, so forbid it
47
+ // from loading anything or being framed, and sandbox it so it cannot run
48
+ // script or navigate even if a browser would otherwise treat it as active.
49
+ res.setHeader('Content-Security-Policy', "default-src 'none'; sandbox; frame-ancestors 'none'");
50
+ const safeName = String(fileName || '').replace(/[\r\n"\\]/g, '').slice(0, 255);
51
+ const disposition = resolved.inlineSafe ? 'inline' : 'attachment';
52
+ res.setHeader(
53
+ 'Content-Disposition',
54
+ safeName ? `${disposition}; filename="${safeName}"` : disposition,
55
+ );
56
+ return resolved;
57
+ }
@@ -0,0 +1,271 @@
1
+ 'use strict';
2
+
3
+ function text(value) {
4
+ return String(value || '').trim();
5
+ }
6
+
7
+ function iso(value, fallback = null) {
8
+ const parsed = value instanceof Date ? value.getTime() : Date.parse(String(value || ''));
9
+ return Number.isFinite(parsed) ? new Date(parsed).toISOString() : fallback;
10
+ }
11
+
12
+ function sessionIdOf(session) {
13
+ return text(session?.sessionId || session?.id || session?.session_id);
14
+ }
15
+
16
+ function sessionMetadata(session) {
17
+ return session?.metadata || session || {};
18
+ }
19
+
20
+ function sourceTimestamps(session) {
21
+ const metadata = sessionMetadata(session);
22
+ return {
23
+ startedAt: iso(
24
+ metadata.startTime
25
+ || metadata.start_time
26
+ || metadata.createdAt
27
+ || metadata.created_at
28
+ || session?.startTime
29
+ || session?.createdAt,
30
+ ),
31
+ modifiedAt: iso(
32
+ metadata.modifiedTime
33
+ || metadata.modified_time
34
+ || metadata.updatedAt
35
+ || metadata.updated_at
36
+ || session?.modifiedTime
37
+ || session?.updatedAt,
38
+ ),
39
+ };
40
+ }
41
+
42
+ function sessionTitle(session, messages) {
43
+ const metadata = session?.metadata || session || {};
44
+ const summary = text(metadata.summary || metadata.title || metadata.name);
45
+ if (summary) return summary.slice(0, 240);
46
+ const firstUser = (Array.isArray(messages) ? messages : []).find((message) => message?.role === 'user');
47
+ return text(firstUser?.text).replace(/\s+/g, ' ').slice(0, 240) || 'Session';
48
+ }
49
+
50
+ async function normalizeEvents(value) {
51
+ if (Array.isArray(value)) return value;
52
+ if (Array.isArray(value?.sessions)) return value.sessions;
53
+ if (Array.isArray(value?.events)) return value.events;
54
+ if (value?.[Symbol.asyncIterator]) {
55
+ const events = [];
56
+ for await (const event of value) events.push(event);
57
+ return events;
58
+ }
59
+ if (value?.[Symbol.iterator]) return [...value];
60
+ return [];
61
+ }
62
+
63
+ function boundedError(error) {
64
+ return text(error?.message || error).slice(0, 1000) || 'Unknown SDK session import failure';
65
+ }
66
+
67
+ export function createSdkSessionImportService({
68
+ db,
69
+ stmts,
70
+ createClient,
71
+ parseSessionEventsToMessages,
72
+ replaceRetrievableHistory,
73
+ ensureRuntimeSessionBinding,
74
+ logger = console,
75
+ } = {}) {
76
+ if (!db || !stmts || typeof createClient !== 'function') throw new Error('SDK session importer requires database, statements, and a client factory');
77
+ let runtime = null;
78
+ let activeRun = null;
79
+ const countConversationMessages = db.prepare(`
80
+ SELECT COUNT(*) AS count
81
+ FROM messages
82
+ WHERE conversation_id = ?
83
+ `);
84
+
85
+ const upsertConversation = db.prepare(`
86
+ INSERT INTO conversations (id, title, sdk_session_id, configured_workspace_root_path, runtime_workspace_root_path, created_at, updated_at)
87
+ VALUES (?, ?, ?, ?, ?, ?, ?)
88
+ ON CONFLICT(id) DO UPDATE SET
89
+ sdk_session_id = excluded.sdk_session_id,
90
+ title = CASE WHEN conversations.title_source = 'manual' THEN conversations.title ELSE excluded.title END,
91
+ configured_workspace_root_path = COALESCE(conversations.configured_workspace_root_path, excluded.configured_workspace_root_path),
92
+ runtime_workspace_root_path = COALESCE(conversations.runtime_workspace_root_path, excluded.runtime_workspace_root_path),
93
+ updated_at = excluded.updated_at
94
+ `);
95
+
96
+ async function getRuntime() {
97
+ if (!runtime) runtime = await createClient();
98
+ return runtime;
99
+ }
100
+
101
+ function isTombstoned(sdkSessionId) {
102
+ return !!stmts.getDeletedSdkSession.get(sdkSessionId);
103
+ }
104
+
105
+ function claim(sdkSessionId, session, { force = false } = {}) {
106
+ const now = new Date().toISOString();
107
+ return db.transaction(() => {
108
+ const existing = stmts.getSdkSessionImport.get(sdkSessionId);
109
+ const { modifiedAt } = sourceTimestamps(session);
110
+ const hasNewerSource = modifiedAt
111
+ && (!existing?.source_modified_at || modifiedAt > existing.source_modified_at);
112
+ if (!force && existing?.status === 'completed' && !hasNewerSource) {
113
+ return { claimed: null, category: 'unchanged' };
114
+ }
115
+ stmts.upsertSdkSessionImport.run(sdkSessionId, existing?.conversation_id || sdkSessionId, now);
116
+ const claimed = stmts.claimSdkSessionImport.run(
117
+ now,
118
+ now,
119
+ sdkSessionId,
120
+ force || hasNewerSource ? 1 : 0,
121
+ );
122
+ if (claimed.changes === 0) return { claimed: null, category: 'unchanged' };
123
+ return {
124
+ claimed: stmts.getSdkSessionImport.get(sdkSessionId),
125
+ category: existing?.status === 'completed' ? 'changed' : 'new',
126
+ };
127
+ })();
128
+ }
129
+
130
+ function persistCompletedImport({ sdkSessionId, session, messages }) {
131
+ const now = new Date().toISOString();
132
+ const metadata = sessionMetadata(session);
133
+ const { startedAt: sourceStartedAt, modifiedAt: sourceModifiedAt } = sourceTimestamps(session);
134
+ const updatedAt = sourceModifiedAt || now;
135
+ const createdAt = sourceStartedAt || updatedAt;
136
+ const workspaceRoot = text(metadata.workspaceRootPath || metadata.workspace_root_path || metadata.cwd) || null;
137
+ const title = sessionTitle(session, messages);
138
+ db.transaction(() => {
139
+ upsertConversation.run(sdkSessionId, title, sdkSessionId, workspaceRoot, workspaceRoot, createdAt, updatedAt);
140
+ ensureRuntimeSessionBinding(sdkSessionId, null, updatedAt, sdkSessionId);
141
+ replaceRetrievableHistory(sdkSessionId, messages);
142
+ stmts.completeSdkSessionImport.run(
143
+ sdkSessionId,
144
+ now,
145
+ sourceStartedAt,
146
+ sourceModifiedAt,
147
+ now,
148
+ sdkSessionId,
149
+ );
150
+ })();
151
+ }
152
+
153
+ async function importSession(session, { force = false } = {}) {
154
+ const sdkSessionId = sessionIdOf(session);
155
+ if (!sdkSessionId) return { status: 'skipped', category: 'unchanged', reason: 'missing-session-id' };
156
+ if (isTombstoned(sdkSessionId)) return { sdkSessionId, status: 'skipped', category: 'tombstoned', reason: 'tombstoned' };
157
+ // Sessions the relay created to execute an existing conversation's turns
158
+ // are vehicles, not conversations: their history already lives in the
159
+ // conversation they ran for. Importing them creates duplicate "shadow"
160
+ // conversations in the list.
161
+ const relayLink = stmts.getRelaySessionLink?.get?.(sdkSessionId) || null;
162
+ if (relayLink && text(relayLink.conversation_id) && text(relayLink.conversation_id) !== sdkSessionId) {
163
+ return { sdkSessionId, status: 'skipped', category: 'relay-owned', reason: 'relay-execution-session' };
164
+ }
165
+ // The SDK-engine workers (Copilot SDK, Claude, Cursor, Grok) run a
166
+ // conversation's turns in a CLI session that shares the conversation's OWN
167
+ // id — the exact equality the guard above reads as "external". A runtime
168
+ // binding for that conversation means the relay executes it and already
169
+ // owns its history: importing would overwrite relay messages with the raw
170
+ // runtime transcript, instruction preambles included (burn-in incident
171
+ // 2026-08-31: "[Relay mode: autopilot] …" surfaced as user bubbles and
172
+ // conversation titles after a restart). This also protects an imported
173
+ // conversation the user later CONTINUED in the relay — from that moment
174
+ // the relay's history is authoritative, not the CLI transcript.
175
+ const runtimeSession = stmts.getRuntimeSessionByConversation?.get?.(sdkSessionId) || null;
176
+ if (runtimeSession) {
177
+ return { sdkSessionId, status: 'skipped', category: 'relay-owned', reason: 'relay-execution-session' };
178
+ }
179
+ // Same protection when a live conversation already claims this session id
180
+ // as its transcript binding under a different conversation id.
181
+ const owningConversation = stmts.getConvBySdkSessionId?.get?.(sdkSessionId) || null;
182
+ if (owningConversation && text(owningConversation.id) && text(owningConversation.id) !== sdkSessionId) {
183
+ return { sdkSessionId, status: 'skipped', category: 'bound-elsewhere', reason: 'session-bound-to-existing-conversation' };
184
+ }
185
+ const claimed = claim(sdkSessionId, session, { force });
186
+ if (!claimed.claimed) return { sdkSessionId, status: 'skipped', category: claimed.category, reason: 'unchanged-or-active' };
187
+
188
+ let resumed = null;
189
+ try {
190
+ const client = await getRuntime();
191
+ resumed = await client.client.resumeSession(sdkSessionId, {
192
+ suppressResumeEvent: true,
193
+ availableTools: [],
194
+ });
195
+ const events = await normalizeEvents(await resumed.getEvents());
196
+ const messages = parseSessionEventsToMessages?.(events);
197
+ if (!Array.isArray(messages)) throw new Error('SDK session import returned an invalid history snapshot');
198
+ const existingMessageCount = Number(countConversationMessages.get(sdkSessionId)?.count || 0);
199
+ if (messages.length === 0 && existingMessageCount > 0) {
200
+ throw new Error('SDK session import returned an empty history snapshot for an existing conversation');
201
+ }
202
+ persistCompletedImport({ sdkSessionId, session, messages });
203
+ return { sdkSessionId, status: 'completed', category: claimed.category, messageCount: messages.length };
204
+ } catch (error) {
205
+ stmts.failSdkSessionImport.run(new Date().toISOString(), boundedError(error), sdkSessionId);
206
+ return { sdkSessionId, status: 'failed', category: 'failed', error: boundedError(error) };
207
+ } finally {
208
+ try { await resumed?.stop?.(); } catch {}
209
+ try { await resumed?.dispose?.(); } catch {}
210
+ }
211
+ }
212
+
213
+ async function runStartupImport() {
214
+ if (activeRun) return activeRun;
215
+ activeRun = (async () => {
216
+ const summary = {
217
+ listed: 0,
218
+ new: 0,
219
+ changed: 0,
220
+ unchanged: 0,
221
+ failed: 0,
222
+ tombstoned: 0,
223
+ 'relay-owned': 0,
224
+ 'bound-elsewhere': 0,
225
+ };
226
+ try {
227
+ stmts.resetInterruptedSdkSessionImports.run(new Date().toISOString());
228
+ const client = await getRuntime();
229
+ const sessions = await normalizeEvents(await client.client.listSessions());
230
+ summary.listed = sessions.length;
231
+ for (const session of sessions) {
232
+ const result = await importSession(session);
233
+ summary[result.category] = Number(summary[result.category] || 0) + 1;
234
+ }
235
+ } catch (error) {
236
+ summary.failed += 1;
237
+ summary.error = boundedError(error);
238
+ } finally {
239
+ activeRun = null;
240
+ }
241
+ logger.info?.(
242
+ `[sdk-session-import] listed=${summary.listed} new=${summary.new} changed=${summary.changed}`
243
+ + ` unchanged=${summary.unchanged} failed=${summary.failed} tombstoned=${summary.tombstoned}`
244
+ + ` relay-owned=${summary['relay-owned']} bound-elsewhere=${summary['bound-elsewhere']}`,
245
+ );
246
+ return summary;
247
+ })();
248
+ return activeRun;
249
+ }
250
+
251
+ async function refreshConversation(conversation) {
252
+ const sdkSessionId = text(conversation?.sdk_session_id || conversation?.sdkSessionId || conversation?.id);
253
+ if (!sdkSessionId || isTombstoned(sdkSessionId)) {
254
+ const error = new Error('Conversation not found');
255
+ error.statusCode = 404;
256
+ throw error;
257
+ }
258
+ return importSession({ sessionId: sdkSessionId, metadata: conversation }, { force: true });
259
+ }
260
+
261
+ return {
262
+ runStartupImport,
263
+ importSession,
264
+ refreshConversation,
265
+ async dispose() {
266
+ const current = runtime;
267
+ runtime = null;
268
+ await current?.dispose?.();
269
+ },
270
+ };
271
+ }
@@ -0,0 +1,207 @@
1
+ 'use strict';
2
+
3
+ import { v4 as uuidv4 } from 'uuid';
4
+
5
+ function normalizeId(value) {
6
+ return String(value || '').trim();
7
+ }
8
+
9
+ function makeError(message, statusCode) {
10
+ const error = new Error(message);
11
+ error.statusCode = statusCode;
12
+ return error;
13
+ }
14
+
15
+ export function createSdkSessionSyncService(db) {
16
+ const runtimeSessionColumns = new Set(
17
+ db.prepare(`PRAGMA table_info(runtime_sessions)`).all().map((column) => String(column?.name || '').trim()),
18
+ );
19
+ const runtimeSessionsSupportProviders = runtimeSessionColumns.has('provider_type')
20
+ && runtimeSessionColumns.has('provider_model');
21
+ const providerSelect = runtimeSessionsSupportProviders
22
+ ? 'provider_type, provider_model'
23
+ : `'github' AS provider_type, NULL AS provider_model`;
24
+ const getConversation = db.prepare(`
25
+ SELECT id, sdk_session_id, status
26
+ FROM conversations
27
+ WHERE id = ?
28
+ LIMIT 1
29
+ `);
30
+
31
+ const getConversationBySdkSessionId = db.prepare(`
32
+ SELECT id, sdk_session_id, status
33
+ FROM conversations
34
+ WHERE sdk_session_id = ?
35
+ AND id != ?
36
+ LIMIT 1
37
+ `);
38
+
39
+ const getRuntimeSessionByConversation = db.prepare(`
40
+ SELECT id, conversation_id, sdk_session_id, status, strategy, runtime_key, model, ${providerSelect}
41
+ FROM runtime_sessions
42
+ WHERE conversation_id = ?
43
+ LIMIT 1
44
+ `);
45
+
46
+ const getRuntimeSessionBySdkSessionId = db.prepare(`
47
+ SELECT id, conversation_id, sdk_session_id, status, strategy, runtime_key, model, ${providerSelect}
48
+ FROM runtime_sessions
49
+ WHERE sdk_session_id = ?
50
+ LIMIT 1
51
+ `);
52
+
53
+ const updateConversationSdkSession = db.prepare(`
54
+ UPDATE conversations
55
+ SET sdk_session_id = ?, updated_at = ?
56
+ WHERE id = ?
57
+ `);
58
+
59
+ const updateRuntimeSessionSdkSession = db.prepare(`
60
+ UPDATE runtime_sessions
61
+ SET conversation_id = ?,
62
+ sdk_session_id = ?,
63
+ last_used_at = ?,
64
+ status = COALESCE(status, 'active')
65
+ WHERE id = ?
66
+ `);
67
+
68
+ const migrateQueueOwnerSessionId = db.prepare(`
69
+ UPDATE queue
70
+ SET owner_sdk_session_id = ?
71
+ WHERE status = 'pending'
72
+ AND owner_sdk_session_id = ?
73
+ AND conversation_id = ?
74
+ `);
75
+
76
+ const insertRuntimeSession = db.prepare(runtimeSessionsSupportProviders
77
+ ? `
78
+ INSERT INTO runtime_sessions (
79
+ id, conversation_id, strategy, runtime_key, model, status, created_at, last_used_at, sdk_session_id, provider_type, provider_model
80
+ ) VALUES (?, ?, ?, ?, ?, 'active', ?, ?, ?, 'github', NULL)
81
+ `
82
+ : `
83
+ INSERT INTO runtime_sessions (
84
+ id, conversation_id, strategy, runtime_key, model, status, created_at, last_used_at, sdk_session_id
85
+ ) VALUES (?, ?, ?, ?, ?, 'active', ?, ?, ?)
86
+ `);
87
+
88
+ const syncSessionTx = db.transaction((sdkSessionIdRaw, conversationIdRaw) => {
89
+ const sdkSessionId = normalizeId(sdkSessionIdRaw);
90
+ const conversationId = normalizeId(conversationIdRaw);
91
+ const nowIso = new Date().toISOString();
92
+
93
+ if (!sdkSessionId || !conversationId) {
94
+ throw makeError('Missing sdk_session_id or conversation_id', 400);
95
+ }
96
+
97
+ const conversation = getConversation.get(conversationId);
98
+ if (!conversation || String(conversation.status || '').trim() === 'deleted') {
99
+ throw makeError('Conversation not found', 404);
100
+ }
101
+
102
+ const existingConversationSdkSessionId = normalizeId(conversation.sdk_session_id);
103
+ const placeholderConversationBinding = existingConversationSdkSessionId
104
+ && existingConversationSdkSessionId === conversationId
105
+ && existingConversationSdkSessionId !== sdkSessionId;
106
+ if (existingConversationSdkSessionId && existingConversationSdkSessionId !== sdkSessionId && !placeholderConversationBinding) {
107
+ throw makeError(
108
+ `Conversation ${conversationId} is already bound to SDK session ${existingConversationSdkSessionId}`,
109
+ 409,
110
+ );
111
+ }
112
+
113
+ const otherConversation = getConversationBySdkSessionId.get(sdkSessionId, conversationId);
114
+ if (otherConversation) {
115
+ throw makeError(
116
+ `SDK session ${sdkSessionId} is already bound to conversation ${otherConversation.id}`,
117
+ 409,
118
+ );
119
+ }
120
+
121
+ const runtimeSessionByConversation = getRuntimeSessionByConversation.get(conversationId) || null;
122
+ const runtimeSessionBySdkSessionId = getRuntimeSessionBySdkSessionId.get(sdkSessionId) || null;
123
+
124
+ if (runtimeSessionByConversation && runtimeSessionBySdkSessionId) {
125
+ const sameRuntimeSession = String(runtimeSessionByConversation.id || '') === String(runtimeSessionBySdkSessionId.id || '');
126
+ const sameConversation = String(runtimeSessionBySdkSessionId.conversation_id || '') === conversationId;
127
+ const sameSdkSession = normalizeId(runtimeSessionByConversation.sdk_session_id) === sdkSessionId;
128
+ const placeholderRuntimeBinding = normalizeId(runtimeSessionByConversation.sdk_session_id) === conversationId
129
+ && normalizeId(runtimeSessionByConversation.sdk_session_id) !== sdkSessionId;
130
+ if (!sameRuntimeSession && !sameConversation && !placeholderRuntimeBinding) {
131
+ throw makeError(
132
+ `SDK session ${sdkSessionId} is already bound to another runtime session`,
133
+ 409,
134
+ );
135
+ }
136
+ if (!sameSdkSession && normalizeId(runtimeSessionByConversation.sdk_session_id)) {
137
+ throw makeError(
138
+ `Runtime session ${runtimeSessionByConversation.id} is already bound to SDK session ${normalizeId(runtimeSessionByConversation.sdk_session_id)}`,
139
+ 409,
140
+ );
141
+ }
142
+ } else if (runtimeSessionBySdkSessionId && normalizeId(runtimeSessionBySdkSessionId.conversation_id) !== conversationId) {
143
+ throw makeError(
144
+ `SDK session ${sdkSessionId} is already bound to runtime session ${runtimeSessionBySdkSessionId.id}`,
145
+ 409,
146
+ );
147
+ }
148
+
149
+ updateConversationSdkSession.run(sdkSessionId, nowIso, conversationId);
150
+ if (placeholderConversationBinding) {
151
+ migrateQueueOwnerSessionId.run(sdkSessionId, conversationId, conversationId);
152
+ }
153
+
154
+ let runtimeSessionId = null;
155
+ let createdRuntimeSession = false;
156
+
157
+ if (runtimeSessionByConversation) {
158
+ const currentSdkSessionId = normalizeId(runtimeSessionByConversation.sdk_session_id);
159
+ if (currentSdkSessionId && currentSdkSessionId !== sdkSessionId && currentSdkSessionId !== conversationId) {
160
+ throw makeError(
161
+ `Runtime session ${runtimeSessionByConversation.id} is already bound to SDK session ${currentSdkSessionId}`,
162
+ 409,
163
+ );
164
+ }
165
+
166
+ updateRuntimeSessionSdkSession.run(conversationId, sdkSessionId, nowIso, runtimeSessionByConversation.id);
167
+ runtimeSessionId = runtimeSessionByConversation.id;
168
+ } else if (runtimeSessionBySdkSessionId) {
169
+ const currentConversationId = normalizeId(runtimeSessionBySdkSessionId.conversation_id);
170
+ if (currentConversationId && currentConversationId !== conversationId) {
171
+ throw makeError(
172
+ `SDK session ${sdkSessionId} is already bound to conversation ${currentConversationId}`,
173
+ 409,
174
+ );
175
+ }
176
+
177
+ updateRuntimeSessionSdkSession.run(conversationId, sdkSessionId, nowIso, runtimeSessionBySdkSessionId.id);
178
+ runtimeSessionId = runtimeSessionBySdkSessionId.id;
179
+ } else {
180
+ runtimeSessionId = uuidv4();
181
+ createdRuntimeSession = true;
182
+ insertRuntimeSession.run(
183
+ runtimeSessionId,
184
+ conversationId,
185
+ 'isolated',
186
+ runtimeSessionId,
187
+ null,
188
+ nowIso,
189
+ nowIso,
190
+ sdkSessionId,
191
+ );
192
+ }
193
+
194
+ return {
195
+ conversationId,
196
+ sdkSessionId,
197
+ runtimeSessionId,
198
+ createdRuntimeSession,
199
+ };
200
+ });
201
+
202
+ return {
203
+ syncSession({ sdk_session_id, conversation_id }) {
204
+ return syncSessionTx(sdk_session_id, conversation_id);
205
+ },
206
+ };
207
+ }