@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,208 @@
1
+ 'use strict';
2
+
3
+ import { parseAutoCompactWindow } from '../../shared/auto-compact-window.mjs';
4
+ import {
5
+ DEFAULT_THINKING_DISPLAY,
6
+ DEFAULT_THINKING_ENABLED,
7
+ parseThinkingDisplay,
8
+ parseThinkingEnabled,
9
+ } from '../../shared/claude-thinking.mjs';
10
+
11
+ function normalizeModeWith(value, normalizeMode) {
12
+ if (typeof normalizeMode === 'function') {
13
+ const normalized = normalizeMode(value);
14
+ const text = String(normalized || '').trim();
15
+ return text || null;
16
+ }
17
+ const text = String(value || '').trim();
18
+ return text || null;
19
+ }
20
+
21
+ export function normalizePreferredModel(value) {
22
+ return String(value || '').trim();
23
+ }
24
+
25
+ export function normalizePreferredReasoningEffort(value) {
26
+ return String(value || '').trim().toLowerCase();
27
+ }
28
+
29
+ export function persistConversationPreferences({
30
+ db,
31
+ stmts,
32
+ conversationId = '',
33
+ preferredRelayMode = '',
34
+ preferredModel = '',
35
+ preferredReasoningEffort = '',
36
+ // `undefined` means "not part of this write" — the composer PATCHes the same
37
+ // route without ever mentioning the window, and must not clear it.
38
+ autoCompactWindow = undefined,
39
+ // Same contract for the thinking control: `undefined` leaves the stored
40
+ // value alone. Anything else is resolved through the shared parsers, whose
41
+ // fallback is the relay default (on / summarized).
42
+ thinkingEnabled = undefined,
43
+ thinkingDisplay = undefined,
44
+ updatedAt = new Date().toISOString(),
45
+ createIfMissing = false,
46
+ createTitle = 'Session',
47
+ tolerateMissingColumns = false,
48
+ } = {}) {
49
+ const convId = String(conversationId || '').trim();
50
+ const mode = String(preferredRelayMode || '').trim();
51
+ const model = normalizePreferredModel(preferredModel);
52
+ const reasoningEffort = normalizePreferredReasoningEffort(preferredReasoningEffort);
53
+ const writesAutoCompactWindow = autoCompactWindow !== undefined;
54
+ const window = writesAutoCompactWindow ? parseAutoCompactWindow(autoCompactWindow) : null;
55
+ const writesThinkingEnabled = thinkingEnabled !== undefined;
56
+ const enabled = writesThinkingEnabled ? parseThinkingEnabled(thinkingEnabled) : DEFAULT_THINKING_ENABLED;
57
+ const writesThinkingDisplay = thinkingDisplay !== undefined;
58
+ const display = writesThinkingDisplay ? parseThinkingDisplay(thinkingDisplay) : null;
59
+ if (!db || !stmts || !convId || !mode) {
60
+ return {
61
+ ok: false,
62
+ created: false,
63
+ preferredRelayMode: mode,
64
+ preferredModel: '',
65
+ preferredReasoningEffort: '',
66
+ autoCompactWindow: null,
67
+ thinkingEnabled: DEFAULT_THINKING_ENABLED,
68
+ thinkingDisplay: DEFAULT_THINKING_DISPLAY,
69
+ updatedAt,
70
+ };
71
+ }
72
+ const safeTitle = String(createTitle || '').trim() || 'Session';
73
+ const writePreferences = db.transaction(() => {
74
+ const existing = typeof stmts.getConvAnyStatus?.get === 'function'
75
+ ? (stmts.getConvAnyStatus.get(convId) || null)
76
+ : null;
77
+ if (!existing && createIfMissing && typeof stmts.insertConv?.run === 'function') {
78
+ stmts.insertConv.run(convId, safeTitle, updatedAt, updatedAt);
79
+ }
80
+ // Extra per-conversation setting columns this write mentions. The prepared
81
+ // statement can't carry them, and these writes are rare (one control
82
+ // change each) — the dynamic form keeps the hot composer path on the
83
+ // prepared statement.
84
+ const extraSets = [];
85
+ const extraValues = [];
86
+ if (writesAutoCompactWindow) {
87
+ extraSets.push('auto_compact_window = ?');
88
+ extraValues.push(window);
89
+ }
90
+ // Both axes store the RESOLVED value, never NULL-as-default: NULL means
91
+ // "never set" and is read back as the relay default, so canonicalizing a
92
+ // default choice to NULL would silently re-point those rows if the relay
93
+ // default ever changed.
94
+ if (writesThinkingEnabled) {
95
+ extraSets.push('thinking_enabled = ?');
96
+ extraValues.push(enabled ? 1 : 0);
97
+ }
98
+ if (writesThinkingDisplay) {
99
+ extraSets.push('thinking_display = ?');
100
+ extraValues.push(display);
101
+ }
102
+ try {
103
+ if (extraSets.length) {
104
+ db.prepare(`
105
+ UPDATE conversations
106
+ SET preferred_relay_mode = ?, preferred_model = ?, preferred_reasoning_effort = ?,
107
+ ${extraSets.join(', ')}, updated_at = ?
108
+ WHERE id = ?
109
+ `).run(mode, model || null, reasoningEffort || null, ...extraValues, updatedAt, convId);
110
+ } else if (typeof stmts.updateConvPreferences?.run === 'function') {
111
+ stmts.updateConvPreferences.run(mode, model || null, reasoningEffort || null, updatedAt, convId);
112
+ } else {
113
+ db.prepare(`
114
+ UPDATE conversations
115
+ SET preferred_relay_mode = ?, preferred_model = ?, preferred_reasoning_effort = ?, updated_at = ?
116
+ WHERE id = ?
117
+ `).run(mode, model || null, reasoningEffort || null, updatedAt, convId);
118
+ }
119
+ } catch (error) {
120
+ if (!tolerateMissingColumns) throw error;
121
+ }
122
+ return {
123
+ ok: true,
124
+ created: !existing,
125
+ preferredRelayMode: mode,
126
+ preferredModel: model,
127
+ preferredReasoningEffort: reasoningEffort,
128
+ // Always echoed, so a write that didn't touch the window still tells the
129
+ // client what the stored value is.
130
+ autoCompactWindow: writesAutoCompactWindow
131
+ ? window
132
+ : parseAutoCompactWindow(existing?.auto_compact_window),
133
+ thinkingEnabled: writesThinkingEnabled
134
+ ? enabled
135
+ : parseThinkingEnabled(existing?.thinking_enabled),
136
+ thinkingDisplay: writesThinkingDisplay
137
+ ? display
138
+ : parseThinkingDisplay(existing?.thinking_display),
139
+ updatedAt,
140
+ };
141
+ });
142
+ return writePreferences();
143
+ }
144
+
145
+ export function persistConversationModelPreference({
146
+ db,
147
+ stmts,
148
+ conversationId = '',
149
+ relayMode = '',
150
+ model = '',
151
+ reasoningEffort = '',
152
+ normalizeMode,
153
+ fallbackRelayMode = 'agent',
154
+ updatedAt = new Date().toISOString(),
155
+ createIfMissing = false,
156
+ createTitle = 'Session',
157
+ tolerateMissingColumns = false,
158
+ } = {}) {
159
+ const convId = String(conversationId || '').trim();
160
+ const modelText = normalizePreferredModel(model);
161
+ const normalizedRelayMode = normalizeModeWith(relayMode, normalizeMode);
162
+ const fallbackMode = normalizeModeWith(fallbackRelayMode, normalizeMode) || String(fallbackRelayMode || '').trim() || 'agent';
163
+ const mode = normalizedRelayMode || fallbackMode;
164
+ const effortText = normalizePreferredReasoningEffort(reasoningEffort);
165
+ if (!db || !stmts || !convId || !mode || !modelText) {
166
+ return {
167
+ ok: false,
168
+ created: false,
169
+ preferredRelayMode: mode || fallbackMode,
170
+ preferredModel: '',
171
+ preferredReasoningEffort: '',
172
+ updatedAt,
173
+ };
174
+ }
175
+
176
+ const safeTitle = String(createTitle || '').trim() || 'Session';
177
+ const writeModelPreference = db.transaction(() => {
178
+ const existing = typeof stmts.getConvAnyStatus?.get === 'function'
179
+ ? (stmts.getConvAnyStatus.get(convId) || null)
180
+ : null;
181
+ if (!existing && createIfMissing && typeof stmts.insertConv?.run === 'function') {
182
+ stmts.insertConv.run(convId, safeTitle, updatedAt, updatedAt);
183
+ }
184
+ const effort = effortText || normalizePreferredReasoningEffort(existing?.preferred_reasoning_effort);
185
+ try {
186
+ if (typeof stmts.updateConvPreferences?.run === 'function') {
187
+ stmts.updateConvPreferences.run(mode, modelText, effort || null, updatedAt, convId);
188
+ } else {
189
+ db.prepare(`
190
+ UPDATE conversations
191
+ SET preferred_relay_mode = ?, preferred_model = ?, preferred_reasoning_effort = ?, updated_at = ?
192
+ WHERE id = ?
193
+ `).run(mode, modelText, effort || null, updatedAt, convId);
194
+ }
195
+ } catch (error) {
196
+ if (!tolerateMissingColumns) throw error;
197
+ }
198
+ return {
199
+ ok: true,
200
+ created: !existing,
201
+ preferredRelayMode: mode,
202
+ preferredModel: modelText,
203
+ preferredReasoningEffort: effort,
204
+ updatedAt,
205
+ };
206
+ });
207
+ return writeModelPreference();
208
+ }
@@ -0,0 +1,217 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Live Cursor plan quota for the Check Usage card.
5
+ *
6
+ * The Cursor API key exposes no plan/usage surface (`/v1/agents/:id/usage` is
7
+ * feature-gated, everything else 404s). The percentages shown on
8
+ * cursor.com/dashboard ("Included in Ultra": Total / Auto / API) come from the
9
+ * dashboard's own web API, which authenticates with the browser session
10
+ * cookie:
11
+ *
12
+ * POST https://cursor.com/api/dashboard/get-current-period-usage
13
+ * Cookie: WorkosCursorSessionToken=<token>
14
+ * Origin: https://cursor.com
15
+ *
16
+ * The token resolves automatically from the relay host's Cursor IDE login
17
+ * (state.vscdb `cursorAuth/accessToken`, same host-login trust model as the
18
+ * Grok CLI's auth.json) — the cookie is `<sub-user-id>::<access JWT>`. A
19
+ * manually pasted cookie (DevTools → Application → Cookies → cursor.com)
20
+ * takes precedence for headless hosts without an IDE install. On 401/expiry
21
+ * the card silently degrades to the local estimated view. Tokens never leave
22
+ * the relay and are never echoed back to clients.
23
+ */
24
+
25
+ import fs from 'node:fs';
26
+ import os from 'node:os';
27
+ import path from 'node:path';
28
+ import { createRequire } from 'node:module';
29
+
30
+ const requireModule = createRequire(import.meta.url);
31
+
32
+ export const CURSOR_DASHBOARD_USAGE_URL = 'https://cursor.com/api/dashboard/get-current-period-usage';
33
+ const FETCH_TIMEOUT_MS = 6_000;
34
+ // The IDE access token lives for months; re-reading the multi-MB state.vscdb
35
+ // on every Check Usage open would be wasteful.
36
+ const IDE_TOKEN_CACHE_MS = 5 * 60_000;
37
+
38
+ /**
39
+ * Default locations of the Cursor IDE's state database per platform.
40
+ */
41
+ export function defaultCursorStateDbPath({ platform = process.platform, env = process.env, homeDir = os.homedir() } = {}) {
42
+ if (platform === 'win32') {
43
+ const appData = String(env.APPDATA || '').trim() || path.join(homeDir, 'AppData', 'Roaming');
44
+ return path.join(appData, 'Cursor', 'User', 'globalStorage', 'state.vscdb');
45
+ }
46
+ if (platform === 'darwin') {
47
+ return path.join(homeDir, 'Library', 'Application Support', 'Cursor', 'User', 'globalStorage', 'state.vscdb');
48
+ }
49
+ return path.join(homeDir, '.config', 'Cursor', 'User', 'globalStorage', 'state.vscdb');
50
+ }
51
+
52
+ /**
53
+ * Build the dashboard session cookie value from an IDE access token:
54
+ * `WorkosCursorSessionToken=<userId>%3A%3A<jwt>` where userId is the JWT
55
+ * `sub` claim with its provider prefix stripped. Pure; null when the token
56
+ * is malformed or expired.
57
+ */
58
+ export function sessionTokenFromAccessToken(accessToken, { now = () => Date.now() } = {}) {
59
+ const token = String(accessToken || '').trim();
60
+ const parts = token.split('.');
61
+ if (parts.length !== 3) return null;
62
+ let payload;
63
+ try {
64
+ payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
65
+ } catch {
66
+ return null;
67
+ }
68
+ const sub = String(payload?.sub || '').trim();
69
+ if (!sub) return null;
70
+ const exp = Number(payload?.exp);
71
+ if (Number.isFinite(exp) && exp * 1000 <= now()) return null;
72
+ const userId = sub.includes('|') ? sub.split('|').pop() : sub;
73
+ return `${userId}%3A%3A${token}`;
74
+ }
75
+
76
+ /**
77
+ * Read the Cursor IDE's login from state.vscdb and build the dashboard
78
+ * session token from it — the same host-login trust model as the Grok CLI's
79
+ * auth.json. The db is opened READ-ONLY in place: it can be multiple GB, so
80
+ * copying it is not an option, and SQLite's WAL mode supports concurrent
81
+ * readers next to the running IDE. Never throws; null when the IDE is
82
+ * absent, logged out, or the db is momentarily locked.
83
+ */
84
+ export function readCursorIdeSessionToken({
85
+ stateDbPath = defaultCursorStateDbPath(),
86
+ fsImpl = fs,
87
+ loadDatabase = null,
88
+ now = () => Date.now(),
89
+ } = {}) {
90
+ try {
91
+ if (!fsImpl.existsSync(stateDbPath)) return null;
92
+ // Lazy-required so importing this module never pays the native-module cost.
93
+ const openDatabase = loadDatabase
94
+ || ((file) => requireModule('better-sqlite3')(file, { readonly: true, fileMustExist: true, timeout: 1500 }));
95
+ const db = openDatabase(stateDbPath);
96
+ try {
97
+ const row = db.prepare("SELECT value FROM ItemTable WHERE key = 'cursorAuth/accessToken'").get();
98
+ return sessionTokenFromAccessToken(row?.value, { now });
99
+ } finally {
100
+ try { db.close(); } catch { /* ignore */ }
101
+ }
102
+ } catch {
103
+ return null;
104
+ }
105
+ }
106
+
107
+ function toPercent(value) {
108
+ const numeric = Number(value);
109
+ return Number.isFinite(numeric) && numeric >= 0 ? numeric : null;
110
+ }
111
+
112
+ function toCents(value) {
113
+ const numeric = Number(value);
114
+ return Number.isFinite(numeric) && numeric >= 0 ? numeric : null;
115
+ }
116
+
117
+ function toIso(value) {
118
+ if (value === null || value === undefined || value === '') return null;
119
+ // The endpoint has shipped both ISO strings and unix-ms strings.
120
+ const asNumber = Number(value);
121
+ const timestamp = Number.isFinite(asNumber) && asNumber > 10_000_000_000
122
+ ? asNumber
123
+ : Date.parse(String(value));
124
+ return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : null;
125
+ }
126
+
127
+ /**
128
+ * Normalize the dashboard payload into the fields the plan card needs.
129
+ * Handles both the flat `planUsage` shape and the older
130
+ * `individualUsage.plan` shape from /api/usage-summary. Pure; null when
131
+ * nothing usable is present.
132
+ */
133
+ export function normalizeCursorDashboardUsage(payload = null) {
134
+ if (!payload || typeof payload !== 'object') return null;
135
+ const plan = (payload.planUsage && typeof payload.planUsage === 'object')
136
+ ? payload.planUsage
137
+ : (payload.individualUsage?.plan && typeof payload.individualUsage.plan === 'object'
138
+ ? payload.individualUsage.plan
139
+ : null);
140
+ if (!plan) return null;
141
+
142
+ const normalized = {
143
+ totalPercentUsed: toPercent(plan.totalPercentUsed),
144
+ autoPercentUsed: toPercent(plan.autoPercentUsed),
145
+ apiPercentUsed: toPercent(plan.apiPercentUsed),
146
+ includedSpendCents: toCents(plan.includedSpend ?? plan.totalSpend),
147
+ limitCents: toCents(plan.limit),
148
+ remainingCents: toCents(plan.remaining),
149
+ billingCycleStart: toIso(payload.billingCycleStart),
150
+ billingCycleEnd: toIso(payload.billingCycleEnd),
151
+ membershipType: String(payload.membershipType || '').trim() || null,
152
+ displayMessage: String(payload.displayMessage || '').trim() || null,
153
+ onDemand: (() => {
154
+ const spend = payload.spendLimitUsage && typeof payload.spendLimitUsage === 'object'
155
+ ? payload.spendLimitUsage
156
+ : (payload.individualUsage?.onDemand && typeof payload.individualUsage.onDemand === 'object'
157
+ ? payload.individualUsage.onDemand
158
+ : null);
159
+ if (!spend) return null;
160
+ const usedCents = toCents(spend.individualUsed ?? spend.totalSpend ?? spend.used);
161
+ const limitCents = toCents(spend.individualLimit ?? spend.limit);
162
+ if (usedCents === null && limitCents === null) return null;
163
+ return { usedCents, limitCents };
164
+ })(),
165
+ };
166
+
167
+ const hasSignal = normalized.totalPercentUsed !== null
168
+ || normalized.autoPercentUsed !== null
169
+ || normalized.apiPercentUsed !== null
170
+ || normalized.includedSpendCents !== null;
171
+ return hasSignal ? normalized : null;
172
+ }
173
+
174
+ /**
175
+ * Build the best-effort fetcher the usage route calls. Token resolution: a
176
+ * manually stored session token wins (headless hosts), otherwise the host's
177
+ * Cursor IDE login is used automatically. Errors, missing or expired tokens
178
+ * yield `null` so the card degrades to the estimated view.
179
+ */
180
+ export function createCursorDashboardUsageFetcher({
181
+ getSessionToken = () => '',
182
+ readIdeTokenImpl = readCursorIdeSessionToken,
183
+ fetchImpl = globalThis.fetch,
184
+ url = CURSOR_DASHBOARD_USAGE_URL,
185
+ timeoutMs = FETCH_TIMEOUT_MS,
186
+ now = () => Date.now(),
187
+ } = {}) {
188
+ let cachedIdeToken = null;
189
+ let cachedIdeTokenAt = 0;
190
+ const resolveIdeToken = () => {
191
+ if (cachedIdeToken && now() - cachedIdeTokenAt < IDE_TOKEN_CACHE_MS) return cachedIdeToken;
192
+ cachedIdeToken = readIdeTokenImpl() || null;
193
+ cachedIdeTokenAt = now();
194
+ return cachedIdeToken;
195
+ };
196
+ return async function fetchCursorDashboardUsage() {
197
+ if (typeof fetchImpl !== 'function') return null;
198
+ const token = String(getSessionToken() || '').trim() || resolveIdeToken();
199
+ if (!token) return null;
200
+ try {
201
+ const response = await fetchImpl(url, {
202
+ method: 'POST',
203
+ headers: {
204
+ Cookie: `WorkosCursorSessionToken=${token}`,
205
+ Origin: 'https://cursor.com',
206
+ 'Content-Type': 'application/json',
207
+ },
208
+ body: '{}',
209
+ signal: AbortSignal.timeout?.(timeoutMs),
210
+ });
211
+ if (!response?.ok) return null;
212
+ return normalizeCursorDashboardUsage(await response.json());
213
+ } catch {
214
+ return null;
215
+ }
216
+ };
217
+ }
@@ -0,0 +1,62 @@
1
+ 'use strict';
2
+
3
+ import nodeFs from 'fs';
4
+ import nodePath from 'path';
5
+
6
+ // Resolve the on-disk folder the Cursor SDK integration keeps for one session,
7
+ // so the file explorer's Session root works on `cursor`-provider conversations.
8
+ //
9
+ // The Cursor worker roots its per-session agent store at
10
+ // `<storeDir>/<sdkSessionId>/` (see cursor-session-worker.mjs / the storePath
11
+ // in cursor-sdk-adapter.mjs), where storeDir is `$CURSOR_AGENT_STORE_DIR` or
12
+ // `<serverDir>/data/cursor-agents`. The directory is created on the session's
13
+ // first turn, so a missing directory means "nothing to browse yet" rather than
14
+ // an error — the resolver returns null and the Session button stays disabled
15
+ // until the first turn lands.
16
+
17
+ const SESSION_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
18
+
19
+ function normalizeText(value) {
20
+ const text = String(value || '').trim();
21
+ return text || '';
22
+ }
23
+
24
+ export function resolveCursorAgentStoreDir({
25
+ env = process.env,
26
+ serverDir = '',
27
+ path = nodePath,
28
+ } = {}) {
29
+ const override = normalizeText(env?.CURSOR_AGENT_STORE_DIR);
30
+ if (override) return override;
31
+ const base = normalizeText(serverDir);
32
+ if (!base) return '';
33
+ return path.join(base, 'data', 'cursor-agents');
34
+ }
35
+
36
+ export function createCursorSessionRootResolver({
37
+ fs = nodeFs,
38
+ path = nodePath,
39
+ env = process.env,
40
+ serverDir = '',
41
+ } = {}) {
42
+ /**
43
+ * @returns {{ sessionRootPath: string, sessionRootName: string } | null}
44
+ */
45
+ function resolveCursorSessionRoot({ sdkSessionId = '' } = {}) {
46
+ const sessionId = normalizeText(sdkSessionId);
47
+ // The id is joined into a filesystem path, so it is validated before any
48
+ // syscall (mirrors the Claude session-root resolver).
49
+ if (!SESSION_ID_PATTERN.test(sessionId)) return null;
50
+ const storeDir = resolveCursorAgentStoreDir({ env, serverDir, path });
51
+ if (!storeDir) return null;
52
+ const sessionRootPath = path.join(storeDir, sessionId);
53
+ try {
54
+ if (!fs.statSync(sessionRootPath).isDirectory()) return null;
55
+ } catch {
56
+ return null;
57
+ }
58
+ return { sessionRootPath, sessionRootName: 'Session' };
59
+ }
60
+
61
+ return { resolveCursorSessionRoot };
62
+ }
@@ -0,0 +1,154 @@
1
+ const DEFAULT_DELETE_RETRIES = 3;
2
+ const RETRY_DELAYS_MS = [250, 500];
3
+
4
+ function sleep(ms) {
5
+ return new Promise((resolve) => setTimeout(resolve, ms));
6
+ }
7
+
8
+ function normalizeConversationId(value) {
9
+ return String(value || '').trim();
10
+ }
11
+
12
+ function createSdkDeleteSessionCaller(sdkClient) {
13
+ if (sdkClient && typeof sdkClient.deleteSession === 'function') {
14
+ return sdkClient.deleteSession.bind(sdkClient);
15
+ }
16
+ let warned = false;
17
+ return async (sessionId) => {
18
+ if (!warned) {
19
+ warned = true;
20
+ console.warn(`[delete-archive] SDK deleteSession() is unavailable; skipping remote delete lifecycle.`);
21
+ }
22
+ return { ok: true, skipped: true, sessionId };
23
+ };
24
+ }
25
+
26
+ import { cleanupGeneratedImagesForConversation } from './generated-image-cleanup-service.mjs';
27
+
28
+ export function createDeleteArchiveService(db, sdkClient, { resolveSessionStateRoot = null } = {}) {
29
+ const callDeleteSession = createSdkDeleteSessionCaller(sdkClient);
30
+
31
+ const stmts = {
32
+ getConversation: db.prepare(`
33
+ SELECT c.id, c.status, c.sdk_session_id
34
+ FROM conversations c
35
+ WHERE c.id = ?
36
+ LIMIT 1
37
+ `),
38
+ markDeleted: db.prepare(`UPDATE conversations SET status = 'deleted', updated_at = ? WHERE id = ?`),
39
+ markArchived: db.prepare(`UPDATE conversations SET archived = 1, updated_at = ? WHERE id = ?`),
40
+ listDeletedWithSdkSession: db.prepare(`
41
+ SELECT c.id, c.sdk_session_id
42
+ FROM conversations c
43
+ WHERE c.status = 'deleted'
44
+ ORDER BY c.updated_at ASC
45
+ `),
46
+ listConversationMessages: db.prepare(`
47
+ SELECT attachments
48
+ FROM messages
49
+ WHERE conversation_id = ?
50
+ ORDER BY timestamp ASC
51
+ `),
52
+ hardDeleteConversation: db.transaction((conversationId) => {
53
+ db.prepare(`DELETE FROM relay_questions WHERE conversation_id = ?`).run(conversationId);
54
+ db.prepare(`DELETE FROM relay_boards WHERE conversation_id = ?`).run(conversationId);
55
+ db.prepare(`DELETE FROM queue WHERE conversation_id = ?`).run(conversationId);
56
+ db.prepare(`DELETE FROM messages WHERE conversation_id = ?`).run(conversationId);
57
+ db.prepare(`DELETE FROM runtime_sessions WHERE conversation_id = ?`).run(conversationId);
58
+ db.prepare(`DELETE FROM conversations WHERE id = ?`).run(conversationId);
59
+ }),
60
+ };
61
+
62
+ async function deleteSdkSessionWithRetries(sdkSessionId, conversationId) {
63
+ let lastError = null;
64
+ for (let attempt = 1; attempt <= DEFAULT_DELETE_RETRIES; attempt += 1) {
65
+ try {
66
+ await callDeleteSession(sdkSessionId);
67
+ return { ok: true };
68
+ } catch (error) {
69
+ lastError = error;
70
+ const suffix = attempt >= DEFAULT_DELETE_RETRIES ? ' (no more retries)' : '';
71
+ console.warn(
72
+ `[delete-archive] SDK delete failed for conversation=${conversationId}, session=${sdkSessionId}, attempt=${attempt}/${DEFAULT_DELETE_RETRIES}${suffix}: ${error?.message || error}`
73
+ );
74
+ if (attempt < DEFAULT_DELETE_RETRIES) {
75
+ await sleep(RETRY_DELAYS_MS[attempt - 1] || RETRY_DELAYS_MS[RETRY_DELAYS_MS.length - 1] || 0);
76
+ }
77
+ }
78
+ }
79
+ return { ok: false, error: lastError };
80
+ }
81
+
82
+ async function hardDeleteConversation(conversationId) {
83
+ const row = stmts.getConversation.get(conversationId);
84
+ cleanupGeneratedImagesForConversation({
85
+ conversationId,
86
+ sdkSessionId: String(row?.sdk_session_id || '').trim() || null,
87
+ messageRows: stmts.listConversationMessages.all(conversationId),
88
+ resolveSessionStateRoot,
89
+ });
90
+ stmts.hardDeleteConversation(conversationId);
91
+ }
92
+
93
+ async function deleteConversation(conversationId) {
94
+ const id = normalizeConversationId(conversationId);
95
+ if (!id) return { ok: false, error: 'Missing conversation id' };
96
+
97
+ const row = stmts.getConversation.get(id);
98
+ if (!row) return { ok: true, alreadyDeleted: true };
99
+ if (row.status === 'deleted') return { ok: true, alreadyDeleted: true, tombstoned: true };
100
+
101
+ stmts.markDeleted.run(new Date().toISOString(), id);
102
+
103
+ const sdkSessionId = String(row.sdk_session_id || '').trim();
104
+
105
+ if (!sdkSessionId) {
106
+ await hardDeleteConversation(id);
107
+ return { ok: true, deleted: true, tombstoned: true };
108
+ }
109
+
110
+ const sdkDeleteResult = await deleteSdkSessionWithRetries(sdkSessionId, id);
111
+ if (sdkDeleteResult.ok) {
112
+ await hardDeleteConversation(id);
113
+ return { ok: true, deleted: true, tombstoned: true, sdkDeleted: true };
114
+ }
115
+
116
+ console.warn(`[delete-archive] Leaving tombstoned conversation=${id}; SDK delete failed. Will retry on next startup.`);
117
+ return { ok: true, deleted: false, tombstoned: true, sdkDeleted: false };
118
+ }
119
+
120
+ async function archiveConversation(conversationId) {
121
+ const id = normalizeConversationId(conversationId);
122
+ if (!id) return { ok: false, error: 'Missing conversation id' };
123
+
124
+ const row = stmts.getConversation.get(id);
125
+ if (!row) return { ok: true, alreadyDeleted: true };
126
+ if (row.status === 'deleted') return { ok: true, alreadyDeleted: true, tombstoned: true };
127
+
128
+ stmts.markArchived.run(new Date().toISOString(), id);
129
+ return { ok: true, archived: true };
130
+ }
131
+
132
+ async function retryPendingDeletesOnStartup() {
133
+ const pendingRows = stmts.listDeletedWithSdkSession.all();
134
+ for (const row of pendingRows) {
135
+ if (!row || !row.id) continue;
136
+ const id = String(row.id).trim();
137
+ if (!id) continue;
138
+ const sdkSessionId = String(row.sdk_session_id || '').trim();
139
+ if (!sdkSessionId) {
140
+ await hardDeleteConversation(id);
141
+ continue;
142
+ }
143
+ const sdkDeleteResult = await deleteSdkSessionWithRetries(sdkSessionId, id);
144
+ if (sdkDeleteResult.ok) {
145
+ await hardDeleteConversation(id);
146
+ } else {
147
+ console.warn(`[delete-archive] Startup retry failed for conversation=${id}; tombstone retained for next restart.`);
148
+ }
149
+ }
150
+ return { ok: true, pendingCount: pendingRows.length };
151
+ }
152
+
153
+ return { deleteConversation, archiveConversation, retryPendingDeletesOnStartup };
154
+ }