@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,338 @@
1
+ import { isValidModelId, normalizeModelIdCandidate } from "../../../../shared/model-id.mjs";
2
+ import {
3
+ extractModelDescriptors,
4
+ normalizeContextLimitTokens,
5
+ } from "../../../../shared/model-descriptors.mjs";
6
+
7
+ export { extractModelDescriptors };
8
+
9
+ export function createModelSwitchingService({
10
+ api,
11
+ dbg,
12
+ getSession,
13
+ modelSnapshotMinIntervalMs,
14
+ modelSwitchConfirmAttempts = 4,
15
+ modelSwitchConfirmDelayMs = 50,
16
+ modelSwitchPendingTtlMs = 10_000,
17
+ sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
18
+ }) {
19
+ let lastModelSnapshotMs = 0;
20
+ let lastConfirmedModel = null;
21
+ let pendingModel = null;
22
+ let pendingModelUntilMs = 0;
23
+ const cachedModelMetadataById = new Map();
24
+ const MODEL_ALIAS_CANDIDATES = {
25
+ "sonnet-4.6": ["sonnet-4.6", "claude-sonnet-4.6", "claude-sonnet-4-6", "anthropic/claude-sonnet-4.6"],
26
+ "haiku-4.5": ["haiku-4.5", "claude-haiku-4.5", "claude-haiku-4-5", "anthropic/claude-haiku-4.5"],
27
+ "gpt-5.4": ["gpt-5.4", "openai/gpt-5.4"],
28
+ "gpt-5.4-mini": ["gpt-5.4-mini", "gpt-5-mini", "openai/gpt-5.4-mini"],
29
+ "gpt-5.3-codex": ["gpt-5.3-codex", "codex-5.3", "codex-5", "gpt-codex-5", "openai/codex-5.3"],
30
+ };
31
+
32
+ function buildRequestedModelCandidates(requested) {
33
+ const key = String(requested || "").trim();
34
+ if (!key) return [];
35
+ const lower = key.toLowerCase();
36
+ const aliasList = MODEL_ALIAS_CANDIDATES[key] || MODEL_ALIAS_CANDIDATES[lower] || [];
37
+ return [...new Set([key, ...aliasList])];
38
+ }
39
+
40
+ function normalizeModelId(modelInfo) {
41
+ if (!modelInfo) return null;
42
+ if (typeof modelInfo === "string") {
43
+ const candidate = normalizeModelIdCandidate(modelInfo);
44
+ return isValidModelId(candidate) ? candidate : null;
45
+ }
46
+ const candidate = normalizeModelIdCandidate(modelInfo.modelId || modelInfo.id || modelInfo.model || null);
47
+ return isValidModelId(candidate) ? candidate : null;
48
+ }
49
+
50
+ function canonicalModelId(id) {
51
+ return String(id || "").toLowerCase().replace(/[^a-z0-9]+/g, "");
52
+ }
53
+
54
+ function appendDescriptorsWithPriority(raw, found, priority) {
55
+ const descriptors = [];
56
+ extractModelDescriptors(raw, descriptors);
57
+ for (const descriptor of descriptors) {
58
+ found.push({ ...descriptor, __priority: priority });
59
+ }
60
+ }
61
+
62
+ async function getAvailableModels() {
63
+ const session = getSession();
64
+ const modelApi = session?.rpc?.model;
65
+ if (!modelApi && !session?.connection?.sendRequest) {
66
+ if (!cachedModelMetadataById.size) return [];
67
+ return [...cachedModelMetadataById.values()];
68
+ }
69
+ const found = [];
70
+
71
+ const sendRequest = session?.connection?.sendRequest;
72
+ if (typeof sendRequest === "function") {
73
+ try {
74
+ const raw = await sendRequest.call(session.connection, "models.list", {});
75
+ appendDescriptorsWithPriority(raw, found, 3);
76
+ } catch {
77
+ // Keep going with session-scoped model APIs.
78
+ }
79
+ }
80
+
81
+ if (typeof modelApi?.list === "function") {
82
+ try {
83
+ const raw = await modelApi.list.call(modelApi, { skipCache: true });
84
+ appendDescriptorsWithPriority(raw, found, 2);
85
+ } catch {
86
+ // Continue with compatibility fallbacks below.
87
+ }
88
+ }
89
+
90
+ if (modelApi) {
91
+ for (const fnName of ["getAvailable", "available", "getAll", "list"]) {
92
+ const fn = modelApi[fnName];
93
+ if (typeof fn !== "function") continue;
94
+ try {
95
+ const raw = await fn.call(modelApi);
96
+ appendDescriptorsWithPriority(raw, found, 1);
97
+ } catch {
98
+ // Continue with other API shapes.
99
+ }
100
+ }
101
+ }
102
+
103
+ found.sort((a, b) => Number(b?.__priority || 0) - Number(a?.__priority || 0));
104
+ const byModelId = new Map();
105
+ for (const entry of found) {
106
+ const modelId = normalizeModelIdCandidate(entry?.modelId);
107
+ if (!isValidModelId(modelId)) continue;
108
+ const existing = byModelId.get(modelId);
109
+ const cached = cachedModelMetadataById.get(modelId);
110
+ byModelId.set(modelId, {
111
+ modelId,
112
+ contextLimitTokens: existing?.contextLimitTokens
113
+ ?? normalizeContextLimitTokens(entry?.contextLimitTokens)
114
+ ?? cached?.contextLimitTokens
115
+ ?? null,
116
+ longContextLimitTokens: existing?.longContextLimitTokens
117
+ ?? normalizeContextLimitTokens(entry?.longContextLimitTokens)
118
+ ?? cached?.longContextLimitTokens
119
+ ?? null,
120
+ pricing: existing?.pricing || entry?.pricing || cached?.pricing || null,
121
+ });
122
+ }
123
+ const models = byModelId.size ? [...byModelId.values()] : [...cachedModelMetadataById.values()];
124
+ for (const entry of models) cachedModelMetadataById.set(entry.modelId, entry);
125
+ return models;
126
+ }
127
+
128
+ async function getAvailableModelIds() {
129
+ const models = await getAvailableModels();
130
+ return models.map((entry) => entry.modelId);
131
+ }
132
+
133
+ function setConfirmedModel(modelId) {
134
+ const normalized = normalizeModelId(modelId);
135
+ if (!normalized) return null;
136
+ lastConfirmedModel = normalized;
137
+ pendingModel = null;
138
+ pendingModelUntilMs = 0;
139
+ return normalized;
140
+ }
141
+
142
+ function setPendingModel(modelId) {
143
+ const normalized = normalizeModelId(modelId);
144
+ if (!normalized) return null;
145
+ pendingModel = normalized;
146
+ pendingModelUntilMs = Date.now() + Math.max(250, Number(modelSwitchPendingTtlMs) || 10_000);
147
+ return normalized;
148
+ }
149
+
150
+ function activePendingModel() {
151
+ if (!pendingModel) return null;
152
+ if (Date.now() <= pendingModelUntilMs) return pendingModel;
153
+ pendingModel = null;
154
+ pendingModelUntilMs = 0;
155
+ return null;
156
+ }
157
+
158
+ async function readCurrentModelId() {
159
+ try {
160
+ const modelInfo = await getSession()?.rpc?.model?.getCurrent();
161
+ return normalizeModelId(modelInfo);
162
+ } catch {
163
+ return null;
164
+ }
165
+ }
166
+
167
+ async function getCurrentModelId() {
168
+ const current = await readCurrentModelId();
169
+ const pending = activePendingModel();
170
+ if (current) {
171
+ if (pending && canonicalModelId(current) !== canonicalModelId(pending)) {
172
+ return pending;
173
+ }
174
+ return setConfirmedModel(current);
175
+ }
176
+ return pending || lastConfirmedModel;
177
+ }
178
+
179
+ async function confirmRequestedModel(requested, targetModel) {
180
+ const attempts = Math.max(1, Number(modelSwitchConfirmAttempts) || 1);
181
+ let lastObserved = null;
182
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
183
+ const observed = await readCurrentModelId();
184
+ if (observed) lastObserved = observed;
185
+ if (
186
+ canonicalModelId(observed) === canonicalModelId(requested)
187
+ || canonicalModelId(observed) === canonicalModelId(targetModel)
188
+ ) {
189
+ return { confirmed: true, model: setConfirmedModel(observed) };
190
+ }
191
+ if (attempt + 1 < attempts) {
192
+ await sleep(Math.max(0, Number(modelSwitchConfirmDelayMs) || 0));
193
+ }
194
+ }
195
+ return { confirmed: false, model: lastObserved };
196
+ }
197
+
198
+ async function publishModelSnapshot(reason = "unspecified", force = false) {
199
+ const now = Date.now();
200
+ if (!force && (now - lastModelSnapshotMs) < modelSnapshotMinIntervalMs) return;
201
+ lastModelSnapshotMs = now;
202
+
203
+ try {
204
+ const currentModel = await getCurrentModelId();
205
+ const availableModels = await getAvailableModels();
206
+ const models = availableModels.map((entry) => entry.modelId);
207
+ const contextLimitsByModel = Object.fromEntries(
208
+ availableModels
209
+ .filter((entry) => entry.contextLimitTokens !== null)
210
+ .map((entry) => [entry.modelId, entry.contextLimitTokens]),
211
+ );
212
+ const modelMetadataByModel = Object.fromEntries(
213
+ availableModels.map((entry) => [entry.modelId, {
214
+ defaultContextLimitTokens: entry.contextLimitTokens,
215
+ longContextLimitTokens: entry.longContextLimitTokens,
216
+ pricing: entry.pricing,
217
+ }]),
218
+ );
219
+ const modelListWarning = (!models.length && !currentModel)
220
+ ? "CLI did not expose a usable model list."
221
+ : null;
222
+ const payload = {
223
+ source: `web-relay-extension:${reason}`,
224
+ models,
225
+ contextLimitsByModel,
226
+ modelMetadataByModel,
227
+ currentModel: currentModel || null,
228
+ defaultModel: currentModel || models[0] || null,
229
+ error: modelListWarning,
230
+ };
231
+ await api("POST", "/api/models/snapshot", payload);
232
+ dbg("model snapshot published", `reason=${reason}`, `models=${models.length}`, `contextLimits=${Object.keys(contextLimitsByModel).length}`, `current=${currentModel || "unknown"}`);
233
+ } catch (e) {
234
+ dbg("model snapshot publish failed", `reason=${reason}`, e?.message || String(e));
235
+ await api("POST", "/api/models/snapshot", {
236
+ source: `web-relay-extension:${reason}`,
237
+ models: [],
238
+ currentModel: null,
239
+ defaultModel: null,
240
+ error: e?.message || String(e),
241
+ }).catch(() => {});
242
+ }
243
+ }
244
+
245
+ function resolveRequestedModelId(requested, availableIds) {
246
+ const target = String(requested || "").trim();
247
+ if (!target) return null;
248
+ const candidates = buildRequestedModelCandidates(target);
249
+ if (!availableIds.length) return candidates[0] || target;
250
+ for (const candidate of candidates) {
251
+ const exact = availableIds.find((id) => id === candidate);
252
+ if (exact) return exact;
253
+ const canonical = canonicalModelId(candidate);
254
+ const canonicalMatch = availableIds.find((id) => canonicalModelId(id) === canonical);
255
+ if (canonicalMatch) return canonicalMatch;
256
+ }
257
+ return candidates[0] || target;
258
+ }
259
+
260
+ async function setModelForMessage(model, contextTier = 'default') {
261
+ const requested = String(model || "").trim();
262
+ if (!requested) return { requested, current: await getCurrentModelId(), switched: false };
263
+ if (requested.toLowerCase() === "auto") {
264
+ return {
265
+ requested,
266
+ current: await getCurrentModelId(),
267
+ switched: false,
268
+ requiresSessionBoundary: true,
269
+ error: "Auto model selection is only available when a new SDK session is created",
270
+ };
271
+ }
272
+
273
+ const current = await getCurrentModelId();
274
+ if (canonicalModelId(current) === canonicalModelId(requested) && contextTier === 'default') {
275
+ return { requested, current, switched: true, after: current, via: "already-active" };
276
+ }
277
+
278
+ const availableModels = await getAvailableModelIds();
279
+ const targetModel = resolveRequestedModelId(requested, availableModels);
280
+ if (!targetModel) {
281
+ return { requested, current, switched: false, error: "Requested model is not available in current Copilot CLI model list" };
282
+ }
283
+ const errors = [];
284
+
285
+ const candidates = [...new Set([targetModel, requested].filter(Boolean))];
286
+ for (const candidate of candidates) {
287
+ try {
288
+ const result = await getSession()?.rpc?.model?.switchTo({ modelId: candidate, contextTier });
289
+ const resultModel = normalizeModelId(result);
290
+ if (resultModel) {
291
+ if (
292
+ canonicalModelId(resultModel) === canonicalModelId(requested)
293
+ || canonicalModelId(resultModel) === canonicalModelId(targetModel)
294
+ ) {
295
+ const after = setConfirmedModel(resultModel);
296
+ return { requested, current, switched: true, after, via: `switchTo(${candidate})`, targetModel };
297
+ }
298
+ errors.push(`switchTo(${candidate}) returned active=${resultModel}`);
299
+ continue;
300
+ }
301
+ const confirmation = await confirmRequestedModel(requested, targetModel);
302
+ if (confirmation.confirmed) {
303
+ return {
304
+ requested,
305
+ current,
306
+ switched: true,
307
+ after: confirmation.model,
308
+ via: `switchTo(${candidate})`,
309
+ targetModel,
310
+ };
311
+ }
312
+ const after = setPendingModel(targetModel);
313
+ return {
314
+ requested,
315
+ current,
316
+ switched: true,
317
+ after,
318
+ via: `switchTo(${candidate})`,
319
+ targetModel,
320
+ confirmationPending: true,
321
+ observedModel: confirmation.model || null,
322
+ };
323
+ } catch (e) {
324
+ errors.push(`switchTo(${candidate}) failed: ${e?.message || String(e)}`);
325
+ }
326
+ }
327
+
328
+ const after = await getCurrentModelId();
329
+ return { requested, current, switched: false, after, targetModel, error: errors[0] || "Unknown switch failure" };
330
+ }
331
+
332
+ return {
333
+ getAvailableModels,
334
+ getCurrentModelId,
335
+ setModelForMessage,
336
+ publishModelSnapshot,
337
+ };
338
+ }
@@ -0,0 +1,55 @@
1
+ export function createHeartbeatController({
2
+ api,
3
+ pollMs,
4
+ getSessionReady,
5
+ getHeartbeatTimer,
6
+ setHeartbeatTimer,
7
+ getActiveQueueMessageId,
8
+ // Optional: every queue message the worker currently owns work for (a
9
+ // persistent-process worker can hold a running turn, a delivered message
10
+ // queued behind it, and a background continuation at once). Each reported
11
+ // id gets its owner lease refreshed; unreported owned rows are recovered.
12
+ getActiveQueueMessageIds,
13
+ }) {
14
+ async function pulseHeartbeat() {
15
+ if (!getSessionReady()) return false;
16
+ try {
17
+ const activeQueueMessageId = typeof getActiveQueueMessageId === "function"
18
+ ? String(getActiveQueueMessageId() || "").trim()
19
+ : "";
20
+ const activeQueueMessageIds = typeof getActiveQueueMessageIds === "function"
21
+ ? (getActiveQueueMessageIds() || []).map((id) => String(id || "").trim()).filter(Boolean)
22
+ : [];
23
+ const body = {
24
+ ...(activeQueueMessageId ? { activeQueueMessageId } : {}),
25
+ ...(activeQueueMessageIds.length ? { activeQueueMessageIds } : {}),
26
+ };
27
+ await api("POST", "/api/heartbeat", body);
28
+ return true;
29
+ } catch {
30
+ return false;
31
+ }
32
+ }
33
+
34
+ function startHeartbeat() {
35
+ if (getHeartbeatTimer()) return;
36
+ void pulseHeartbeat();
37
+ const timer = setInterval(() => {
38
+ void pulseHeartbeat();
39
+ }, pollMs);
40
+ setHeartbeatTimer(timer);
41
+ }
42
+
43
+ function stopHeartbeat() {
44
+ const timer = getHeartbeatTimer();
45
+ if (!timer) return;
46
+ clearInterval(timer);
47
+ setHeartbeatTimer(null);
48
+ }
49
+
50
+ return {
51
+ pulseHeartbeat,
52
+ startHeartbeat,
53
+ stopHeartbeat,
54
+ };
55
+ }