@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,269 @@
1
+ // Normalize Claude CLI workflow progress into the bounded digest the relay
2
+ // ships on a background-task row (`workflowProgress`).
3
+ //
4
+ // Two sources feed the digest (verified against CLI 2.1.226):
5
+ //
6
+ // - `<sessionDir>/workflows/<runId>.json` — the run record, written only at
7
+ // workflow COMPLETION. Its `workflowProgress` array mixes
8
+ // `{type:'workflow_phase'}` and `{type:'workflow_agent'}` entries and is the
9
+ // authoritative tree once it exists.
10
+ // - `<sessionDir>/subagents/workflows/<runId>/journal.jsonl` — the live
11
+ // append-only source while the run is in flight: `{type:'started', key,
12
+ // agentId}` then `{type:'result', key, agentId, result}` per agent.
13
+ // started-without-result = running; result = done.
14
+ //
15
+ // The digest shape is a contract shared with the relay sanitizer and the
16
+ // client tree renderer — keep it EXACTLY this:
17
+ //
18
+ // { runId, workflowName, status, agentCount, totalTokens, durationMs,
19
+ // phases: [{index, title}], logs: [string], // last 5, ≤300 chars
20
+ // agents: [{index, label, phaseIndex, phaseTitle, model, state, attempt,
21
+ // lastToolName, tokens, toolCalls, durationMs, startedAt}],
22
+ // agentsOmitted } // agents ≤100, order kept
23
+ //
24
+ // Both file formats are undocumented CLI internals, so every field is
25
+ // presence-checked and clamped; malformed input degrades to omitted fields or
26
+ // a null digest — these functions NEVER throw.
27
+
28
+ const MAX_AGENTS = 100;
29
+ const MAX_PHASES = 50;
30
+ const MAX_LOGS = 5;
31
+ const MAX_LOG_CHARS = 300;
32
+ const MAX_LABEL_CHARS = 160;
33
+ const MAX_TOOL_NAME_CHARS = 160;
34
+ const MAX_TITLE_CHARS = 120;
35
+ const MAX_MODEL_CHARS = 80;
36
+ const MAX_STATE_CHARS = 32;
37
+ const MAX_RUN_ID_CHARS = 64;
38
+ const MAX_WORKFLOW_NAME_CHARS = 120;
39
+
40
+ /** Trimmed string clamped to `max` chars, or null for anything non-string. */
41
+ function clampString(value, max) {
42
+ if (typeof value !== 'string') return null;
43
+ const text = value.trim();
44
+ if (!text) return null;
45
+ return text.length > max ? text.slice(0, max) : text;
46
+ }
47
+
48
+ /** A finite number, or null — never NaN/Infinity/strings coerced. */
49
+ function finiteOrNull(value) {
50
+ return typeof value === 'number' && Number.isFinite(value) ? value : null;
51
+ }
52
+
53
+ /**
54
+ * Digest a completed (or partial) run record. Returns null when `record` is
55
+ * not an object carrying a usable `runId` — the one field every real record
56
+ * has and the digest cannot identify a run without.
57
+ */
58
+ export function digestFromRunRecord(record) {
59
+ try {
60
+ if (!record || typeof record !== 'object' || Array.isArray(record)) return null;
61
+ const runId = clampString(record.runId, MAX_RUN_ID_CHARS);
62
+ if (!runId) return null;
63
+
64
+ const progress = Array.isArray(record.workflowProgress) ? record.workflowProgress : [];
65
+ const phases = [];
66
+ const agents = [];
67
+ let agentTotal = 0;
68
+ for (const entry of progress) {
69
+ if (!entry || typeof entry !== 'object') continue;
70
+ if (entry.type === 'workflow_phase') {
71
+ if (phases.length < MAX_PHASES) {
72
+ phases.push({
73
+ index: finiteOrNull(entry.index) ?? phases.length + 1,
74
+ title: clampString(entry.title, MAX_TITLE_CHARS),
75
+ });
76
+ }
77
+ continue;
78
+ }
79
+ if (entry.type !== 'workflow_agent') continue;
80
+ agentTotal += 1;
81
+ if (agents.length >= MAX_AGENTS) continue;
82
+ // promptPreview/resultPreview are dropped deliberately: they are the
83
+ // multi-hundred-char fields that would balloon a 2s-cadence broadcast.
84
+ agents.push({
85
+ index: finiteOrNull(entry.index) ?? agentTotal,
86
+ label: clampString(entry.label, MAX_LABEL_CHARS),
87
+ phaseIndex: finiteOrNull(entry.phaseIndex),
88
+ phaseTitle: clampString(entry.phaseTitle, MAX_TITLE_CHARS),
89
+ model: clampString(entry.model, MAX_MODEL_CHARS),
90
+ state: clampString(entry.state, MAX_STATE_CHARS),
91
+ attempt: finiteOrNull(entry.attempt),
92
+ lastToolName: clampString(entry.lastToolName, MAX_TOOL_NAME_CHARS),
93
+ tokens: finiteOrNull(entry.tokens),
94
+ toolCalls: finiteOrNull(entry.toolCalls),
95
+ durationMs: finiteOrNull(entry.durationMs),
96
+ startedAt: finiteOrNull(entry.startedAt),
97
+ });
98
+ }
99
+
100
+ // The record also carries top-level `phases: [{title}]`; used only when
101
+ // workflowProgress had no phase entries to derive titles from.
102
+ if (!phases.length && Array.isArray(record.phases)) {
103
+ for (const phase of record.phases) {
104
+ if (phases.length >= MAX_PHASES) break;
105
+ if (!phase || typeof phase !== 'object') continue;
106
+ phases.push({ index: phases.length + 1, title: clampString(phase.title, MAX_TITLE_CHARS) });
107
+ }
108
+ }
109
+
110
+ const logs = (Array.isArray(record.logs) ? record.logs : [])
111
+ .filter((line) => typeof line === 'string' && line.trim())
112
+ .slice(-MAX_LOGS)
113
+ .map((line) => line.slice(0, MAX_LOG_CHARS));
114
+
115
+ return {
116
+ runId,
117
+ workflowName: clampString(record.workflowName, MAX_WORKFLOW_NAME_CHARS),
118
+ status: clampString(record.status, MAX_STATE_CHARS),
119
+ agentCount: finiteOrNull(record.agentCount) ?? (agentTotal || null),
120
+ totalTokens: finiteOrNull(record.totalTokens),
121
+ // Top-level run duration: only the completed run record knows it. The
122
+ // finished-task card renders it; renderers ignore it when null.
123
+ durationMs: finiteOrNull(record.durationMs),
124
+ phases,
125
+ logs,
126
+ agents,
127
+ agentsOmitted: Math.max(0, agentTotal - agents.length),
128
+ };
129
+ } catch {
130
+ return null;
131
+ }
132
+ }
133
+
134
+ // Shared-prefix stripping only kicks in when the common prefix is long enough
135
+ // to be boilerplate; short shared openers ("review: ") are kept as-is.
136
+ const MIN_COMMON_PREFIX_CHARS = 24;
137
+ // Leftovers a stripped tail may start with (the prefix often ends mid-
138
+ // separator): whitespace, dashes, and common punctuation.
139
+ const LABEL_LEADING_NOISE = /^[\s.,;:!?·…"'()[\]{}<>|/\\`~*_#=–—-]+/;
140
+
141
+ /**
142
+ * Strip the longest common prefix shared by the given labels. Live labels are
143
+ * prompt previews, and workflows often prefix every agent's prompt with the
144
+ * same boilerplate — N indistinguishable labels. When ≥2 non-empty string
145
+ * entries share a prefix of ≥24 chars, that prefix is stripped from each and
146
+ * the tail (trimmed of leading whitespace/punctuation, clamped to the label
147
+ * limit) becomes the label. Non-string entries pass through untouched and do
148
+ * not join the prefix computation; a label that was nothing but the shared
149
+ * prefix keeps its original text (an empty label would be strictly worse).
150
+ * Single label or short prefix ⇒ unchanged. Pure and throw-free.
151
+ */
152
+ export function stripCommonPromptPrefix(labels) {
153
+ try {
154
+ if (!Array.isArray(labels)) return [];
155
+ const strings = labels.filter((label) => typeof label === 'string' && label.length > 0);
156
+ if (strings.length < 2) return labels.slice();
157
+ let prefix = strings[0];
158
+ for (const label of strings) {
159
+ let shared = 0;
160
+ const limit = Math.min(prefix.length, label.length);
161
+ while (shared < limit && prefix[shared] === label[shared]) shared += 1;
162
+ prefix = prefix.slice(0, shared);
163
+ if (!prefix) break;
164
+ }
165
+ if (prefix.length < MIN_COMMON_PREFIX_CHARS) return labels.slice();
166
+ return labels.map((label) => {
167
+ if (typeof label !== 'string' || label.length === 0) return label;
168
+ const tail = label.slice(prefix.length).replace(LABEL_LEADING_NOISE, '').slice(0, MAX_LABEL_CHARS);
169
+ return tail || label;
170
+ });
171
+ } catch {
172
+ return Array.isArray(labels) ? labels.slice() : [];
173
+ }
174
+ }
175
+
176
+ /** Label lookup tolerant of a Map or a plain object (own keys only). */
177
+ function labelFor(labelsByAgentId, agentId) {
178
+ try {
179
+ if (!labelsByAgentId) return null;
180
+ if (typeof labelsByAgentId.get === 'function') return labelsByAgentId.get(agentId) ?? null;
181
+ if (typeof labelsByAgentId === 'object' && Object.hasOwn(labelsByAgentId, agentId)) {
182
+ return labelsByAgentId[agentId];
183
+ }
184
+ } catch {
185
+ // fall through to null
186
+ }
187
+ return null;
188
+ }
189
+
190
+ /**
191
+ * Digest the live journal while the run record does not exist yet. `entries`
192
+ * are the parsed journal lines in file order; `labelsByAgentId` (Map or plain
193
+ * object) supplies per-agent labels read from the first line of each
194
+ * `agent-<agentId>.jsonl`, falling back to `agent <n>`. The journal carries no
195
+ * phases, tokens, or timings, so those digest fields are empty/null; `runId`
196
+ * is optional (the caller knows it from the run directory's name).
197
+ * Returns null when no agent can be derived — an empty digest has nothing to
198
+ * render and would only churn the publish path.
199
+ */
200
+ export function digestFromJournal({ entries, labelsByAgentId, workflowName, runId } = {}) {
201
+ try {
202
+ if (!Array.isArray(entries)) return null;
203
+ const stateByAgentId = new Map();
204
+ for (const entry of entries) {
205
+ if (!entry || typeof entry !== 'object') continue;
206
+ const agentId = clampString(entry.agentId, 256);
207
+ if (!agentId) continue;
208
+ if (entry.type === 'started') {
209
+ if (!stateByAgentId.has(agentId)) stateByAgentId.set(agentId, 'running');
210
+ } else if (entry.type === 'result') {
211
+ // A result without a prior `started` still proves the agent ran.
212
+ stateByAgentId.set(agentId, 'done');
213
+ }
214
+ }
215
+ if (!stateByAgentId.size) return null;
216
+
217
+ // Live labels are prompt previews; when the workflow prefixed every
218
+ // agent's prompt with the same boilerplate, strip the shared prefix so
219
+ // each label shows the distinguishing tail instead of N identical strings.
220
+ // The strip runs on the UNCLAMPED strings — boilerplate longer than the
221
+ // label limit would otherwise clamp every label to the same prefix and
222
+ // leave nothing to distinguish — and the label clamp is applied to the
223
+ // results (stripped tails come back pre-clamped, but the keep-original
224
+ // degenerate paths do not).
225
+ const agentEntries = [...stateByAgentId];
226
+ const labels = stripCommonPromptPrefix(
227
+ agentEntries.map(([agentId]) => {
228
+ const label = labelFor(labelsByAgentId, agentId);
229
+ return typeof label === 'string' ? label.trim() || null : null;
230
+ }),
231
+ ).map((label) => clampString(label, MAX_LABEL_CHARS));
232
+
233
+ const agents = [];
234
+ let index = 0;
235
+ for (const [, state] of agentEntries) {
236
+ index += 1;
237
+ if (agents.length >= MAX_AGENTS) continue;
238
+ agents.push({
239
+ index,
240
+ label: (typeof labels[index - 1] === 'string' && labels[index - 1]) || `agent ${index}`,
241
+ phaseIndex: null,
242
+ phaseTitle: null,
243
+ model: null,
244
+ state,
245
+ attempt: null,
246
+ lastToolName: null,
247
+ tokens: null,
248
+ toolCalls: null,
249
+ durationMs: null,
250
+ startedAt: null,
251
+ });
252
+ }
253
+
254
+ return {
255
+ runId: clampString(runId, MAX_RUN_ID_CHARS),
256
+ workflowName: clampString(workflowName, MAX_WORKFLOW_NAME_CHARS),
257
+ status: 'running',
258
+ agentCount: stateByAgentId.size,
259
+ totalTokens: null,
260
+ durationMs: null,
261
+ phases: [],
262
+ logs: [],
263
+ agents,
264
+ agentsOmitted: Math.max(0, stateByAgentId.size - agents.length),
265
+ };
266
+ } catch {
267
+ return null;
268
+ }
269
+ }
@@ -0,0 +1,126 @@
1
+ 'use strict';
2
+
3
+ import fs from 'fs';
4
+ import os from 'os';
5
+ import path from 'path';
6
+ import { pathToFileURL } from 'url';
7
+
8
+ function dedupePaths(paths) {
9
+ const seen = new Set();
10
+ return paths.filter((value) => {
11
+ const candidate = String(value || '').trim();
12
+ if (!candidate) return false;
13
+ const normalized = path.normalize(candidate);
14
+ if (seen.has(normalized)) return false;
15
+ seen.add(normalized);
16
+ return true;
17
+ });
18
+ }
19
+
20
+ export function getCopilotBaseDirs({ env = process.env, platform = process.platform, homeDir = os.homedir() } = {}) {
21
+ const configured = String(env.COPILOT_PKG_DIR || '').trim();
22
+ if (platform === 'win32') {
23
+ return dedupePaths([configured, path.join(env.LOCALAPPDATA || path.join(homeDir, 'AppData', 'Local'), 'copilot', 'pkg')]);
24
+ }
25
+ if (platform === 'darwin') {
26
+ return dedupePaths([configured, path.join(homeDir, 'Library', 'Application Support', 'copilot', 'pkg')]);
27
+ }
28
+ return dedupePaths([
29
+ configured,
30
+ path.join(env.XDG_CACHE_HOME || path.join(homeDir, '.cache'), 'copilot', 'pkg'),
31
+ path.join(env.XDG_DATA_HOME || path.join(homeDir, '.local', 'share'), 'copilot', 'pkg'),
32
+ ]);
33
+ }
34
+
35
+ /**
36
+ * The runtime entry point inside a Copilot CLI version directory.
37
+ *
38
+ * It is `app.js` and never the sibling `index.js`: `index.js` is a
39
+ * self-updating launcher that resolves the newest version in the cache and
40
+ * `exec`s THAT version's `app.js`. Spawning it silently defeats pinning the
41
+ * runtime to the SDK bundle's own version, which matters because the event
42
+ * shapes the workers normalize are per-version. Exported so every caller pins
43
+ * the same file.
44
+ */
45
+ export function copilotRuntimeEntry(versionDir) {
46
+ return path.join(versionDir, 'app.js');
47
+ }
48
+
49
+ export function resolveInstalledCopilotPaths({ config = {}, env = process.env, platform = process.platform, arch = process.arch } = {}) {
50
+ if (config.sdkPath && config.cliPath) {
51
+ return { sdkPath: path.resolve(config.sdkPath), cliPath: path.resolve(config.cliPath), version: 'configured' };
52
+ }
53
+ const requestedVersion = String(config.sdkVersion || '').trim();
54
+ const subdirs = platform === 'universal' ? ['universal'] : [`${platform}-${arch}`, 'universal'];
55
+ const candidates = [];
56
+ for (const [baseDirIdx, baseDir] of getCopilotBaseDirs({ env, platform }).entries()) {
57
+ for (const [subdirIdx, subdir] of subdirs.entries()) {
58
+ let entries = [];
59
+ try { entries = fs.readdirSync(path.join(baseDir, subdir)); } catch { continue; }
60
+ for (const version of entries) {
61
+ if (!/^\d+\.\d+\.\d+$/.test(version) || (requestedVersion && version !== requestedVersion)) continue;
62
+ const versionDir = path.join(baseDir, subdir, version);
63
+ const sdkPath = path.join(versionDir, 'copilot-sdk', 'index.js');
64
+ const cliPath = copilotRuntimeEntry(versionDir);
65
+ if (fs.existsSync(sdkPath) && fs.existsSync(cliPath)) {
66
+ candidates.push({ version, baseDirIdx, subdirIdx, sdkPath, cliPath });
67
+ }
68
+ }
69
+ }
70
+ }
71
+ candidates.sort((a, b) => {
72
+ const aVersion = a.version.split('.').map(Number);
73
+ const bVersion = b.version.split('.').map(Number);
74
+ return bVersion[0] - aVersion[0] || bVersion[1] - aVersion[1] || bVersion[2] - aVersion[2]
75
+ || a.baseDirIdx - b.baseDirIdx || a.subdirIdx - b.subdirIdx;
76
+ });
77
+ if (candidates.length) return candidates[0];
78
+ const scope = requestedVersion ? `version ${requestedVersion}` : 'a compatible version';
79
+ throw new Error(`Copilot SDK runtime not found (${scope}). Install Copilot CLI or configure sdkPath and cliPath.`);
80
+ }
81
+
82
+ export function buildInstalledCopilotClientOptions({
83
+ cliPath,
84
+ cwd,
85
+ baseDirectory,
86
+ logLevel = 'debug',
87
+ } = {}) {
88
+ return {
89
+ connection: {
90
+ kind: 'stdio',
91
+ path: cliPath,
92
+ },
93
+ mode: 'empty',
94
+ baseDirectory,
95
+ useLoggedInUser: true,
96
+ logLevel,
97
+ workingDirectory: cwd,
98
+ };
99
+ }
100
+
101
+ export async function createInstalledCopilotClient({
102
+ config = {},
103
+ cwd,
104
+ baseDirectory,
105
+ logLevel = 'debug',
106
+ } = {}) {
107
+ const paths = resolveInstalledCopilotPaths({ config });
108
+ const sdk = await import(pathToFileURL(paths.sdkPath).href);
109
+ if (typeof sdk.CopilotClient !== 'function') throw new Error('CopilotClient not found in installed Copilot SDK exports');
110
+ const client = new sdk.CopilotClient(buildInstalledCopilotClientOptions({
111
+ cliPath: paths.cliPath,
112
+ cwd,
113
+ baseDirectory,
114
+ logLevel,
115
+ }));
116
+ await client.start();
117
+ return {
118
+ client,
119
+ approveAll: sdk.approveAll,
120
+ paths,
121
+ async dispose() {
122
+ await client.stop?.();
123
+ await client.dispose?.();
124
+ },
125
+ };
126
+ }
@@ -0,0 +1,97 @@
1
+ import fs from 'fs';
2
+
3
+ // Very large base64 payloads risk rejection by the runtime; larger images fall
4
+ // back to a file reference so the agent can read them from disk instead. Same
5
+ // threshold the Claude and Cursor workers use.
6
+ export const MAX_INLINE_IMAGE_BYTES = 5 * 1024 * 1024;
7
+
8
+ function isImageAttachment(att) {
9
+ return String(att?.type || '').toLowerCase().startsWith('image/');
10
+ }
11
+
12
+ function imageFromDataUrl(att) {
13
+ const dataUrl = String(att?.dataUrl || '').trim();
14
+ if (!dataUrl.startsWith('data:')) return null;
15
+ const match = dataUrl.match(/^data:([^;,]+);base64,(.+)$/i);
16
+ if (!match) return null;
17
+ const mimeType = String(match[1] || '').trim().toLowerCase();
18
+ const data = String(match[2] || '').trim();
19
+ if (!mimeType.startsWith('image/') || !data) return null;
20
+ return { data, mimeType };
21
+ }
22
+
23
+ /**
24
+ * Build the `MessageOptions` payload for `session.send()` from a relay
25
+ * message.
26
+ *
27
+ * Mirrors `buildCursorUserMessage` / `buildClaudeUserContent`: images small
28
+ * enough to embed travel inline, everything else becomes a reference the
29
+ * agent's file tools can open, and the server-provided
30
+ * `attachmentPromptContext` is appended to the prompt. What differs is the
31
+ * carrier — the Copilot SDK's `MessageOptions.attachments` is a typed union
32
+ * rather than content blocks:
33
+ *
34
+ * image, embeddable → `{ type: 'blob', data, mimeType, displayName }`
35
+ * image, too large → `{ type: 'file', path, displayName }`
36
+ * any other file → `{ type: 'file', path, displayName }`
37
+ *
38
+ * The note lines are kept even though the attachments are structured, because
39
+ * they are what tells the model the absolute path of a file it may want to
40
+ * re-read with its own tools — the same reason the siblings emit them.
41
+ *
42
+ * Text composition order (identical to the siblings): user text, note lines,
43
+ * `attachmentPromptContext`, joined by blank lines.
44
+ */
45
+ export function buildCopilotMessageOptions(message, {
46
+ fsImpl = fs,
47
+ maxInlineImageBytes = MAX_INLINE_IMAGE_BYTES,
48
+ } = {}) {
49
+ const text = String(message?.text || '').trim();
50
+ const input = Array.isArray(message?.attachments) ? message.attachments : [];
51
+ const attachments = [];
52
+ const noteLines = [];
53
+
54
+ for (const att of input) {
55
+ if (!att || typeof att !== 'object') continue;
56
+ const name = String(att.name || 'attachment').trim() || 'attachment';
57
+ const mime = String(att.type || 'application/octet-stream').trim() || 'application/octet-stream';
58
+ const filePath = String(att.path || '').trim();
59
+
60
+ if (isImageAttachment(att)) {
61
+ let blob = null;
62
+ if (filePath && fsImpl.existsSync(filePath)) {
63
+ try {
64
+ const bytes = fsImpl.readFileSync(filePath);
65
+ if (Buffer.isBuffer(bytes) && bytes.length && bytes.length <= maxInlineImageBytes) {
66
+ blob = {
67
+ data: bytes.toString('base64'),
68
+ mimeType: mime.toLowerCase().startsWith('image/') ? mime.toLowerCase() : 'image/png',
69
+ };
70
+ }
71
+ } catch {
72
+ blob = null;
73
+ }
74
+ }
75
+ if (!blob) blob = imageFromDataUrl(att);
76
+ if (blob) {
77
+ attachments.push({ type: 'blob', data: blob.data, mimeType: blob.mimeType, displayName: name });
78
+ noteLines.push(`Attached image "${name}" (${mime}) is embedded in this message.`);
79
+ } else if (filePath) {
80
+ attachments.push({ type: 'file', path: filePath, displayName: name });
81
+ noteLines.push(`Attached image "${name}" (${mime}): ${filePath}`);
82
+ }
83
+ continue;
84
+ }
85
+
86
+ if (filePath && fsImpl.existsSync(filePath)) {
87
+ attachments.push({ type: 'file', path: filePath, displayName: name });
88
+ noteLines.push(`Attached file "${name}" (${mime}): ${filePath}`);
89
+ }
90
+ }
91
+
92
+ const attachmentPromptContext = String(message?.attachmentPromptContext || '').trim();
93
+ const textParts = [text];
94
+ if (noteLines.length) textParts.push(noteLines.join('\n'));
95
+ if (attachmentPromptContext) textParts.push(attachmentPromptContext);
96
+ return { prompt: textParts.filter(Boolean).join('\n\n'), attachments };
97
+ }
@@ -0,0 +1,121 @@
1
+ // BYOK (bring-your-own-key) provider config for the SDK worker.
2
+ //
3
+ // Why this exists at all: on the EXTENSION path the `COPILOT_PROVIDER_*`
4
+ // environment variables are read by the `copilot` CLI's own startup layer,
5
+ // which turns them into the process-wide custom provider. Sessions this worker
6
+ // creates through the SDK never go through that layer — a live probe confirmed
7
+ // the runtime IGNORES those variables for SDK-created sessions — so the same
8
+ // configuration has to be re-expressed in-process as `SessionConfig.provider`
9
+ // (`ProviderConfig`, verified against the bundled runtime 1.0.82 types).
10
+ //
11
+ // The variable names are deliberately NOT re-invented here: they are exactly
12
+ // the ones `applyOpenAIProviderEnvironment()`
13
+ // (server/services/session-worker-launch-service.mjs) writes today, so one
14
+ // relay-side OpenAI configuration drives both engines and the existing
15
+ // secret-env-file plumbing (`WORKER_SECRET_ENV_VARS` already carries
16
+ // `COPILOT_PROVIDER_API_KEY`) keeps working untouched.
17
+ //
18
+ // Note on cost: BYOK usage events report `cost: 0` (the field is the premium
19
+ // multiplier, not money), so nothing here feeds a spend display.
20
+
21
+ import { resolveModelTokenCeilings } from '../../shared/context-window-fallbacks.mjs';
22
+
23
+ const DEFAULT_BASE_URL = 'https://api.openai.com/v1';
24
+
25
+ function text(value) {
26
+ return String(value ?? '').trim();
27
+ }
28
+
29
+ /**
30
+ * A token ceiling override read from the environment.
31
+ *
32
+ * `parseInt` is deliberately NOT used: it truncates at the first character it
33
+ * cannot read, so `1e6` would silently become **1** and `90000.5` would become
34
+ * 90000 — an escape hatch that quietly does the wrong thing is worse than one
35
+ * that refuses. An unusable value is logged and IGNORED, falling through to the
36
+ * shared table and then to the runtime's own default, rather than being
37
+ * truncated into a number nobody asked for.
38
+ */
39
+ function tokenCeilingOverride(name, raw, dbg) {
40
+ const value = text(raw);
41
+ if (!value) return null;
42
+ const numeric = Number(value);
43
+ if (!Number.isInteger(numeric) || numeric <= 0) {
44
+ dbg(`ignoring ${name}: expected a positive whole number of tokens, got ${JSON.stringify(value)}`);
45
+ return null;
46
+ }
47
+ return numeric;
48
+ }
49
+
50
+ /**
51
+ * Prompt/output token ceilings for the model this session will call.
52
+ *
53
+ * The runtime resolves these from its own model catalog and falls back to a
54
+ * synthesized 128k context for a model it does not recognise, which is wrong in
55
+ * the expensive direction for a large-context model: a long relay conversation
56
+ * would lose history it never needed to lose.
57
+ *
58
+ * The numbers themselves live in `shared/context-window-fallbacks.mjs` — the
59
+ * same module the Copilot snapshot service and the Cursor worker read — so this
60
+ * worker cannot disagree with the rest of the relay about how big a model's
61
+ * window is. The prompt ceiling is derived there as `window − output`.
62
+ *
63
+ * An unknown model is left UNSET on purpose: the runtime's own default is a
64
+ * safe answer, whereas a guessed ceiling that is too HIGH turns early
65
+ * compaction into hard API errors.
66
+ */
67
+ export function resolveOpenAiModelTokenLimits(model) {
68
+ const { maxPromptTokens = null, maxOutputTokens = null } = resolveModelTokenCeilings(model);
69
+ if (maxPromptTokens === null || maxOutputTokens === null) return {};
70
+ return { maxPromptTokens, maxOutputTokens };
71
+ }
72
+
73
+ /**
74
+ * Build `SessionConfig.provider` from the worker's environment, or null when
75
+ * this session is not a BYOK session.
76
+ *
77
+ * The signal is the same one the extension path uses: the launch service only
78
+ * writes `COPILOT_PROVIDER_TYPE` for a session whose relay provider is
79
+ * `openai`, and deletes the whole family otherwise. An API key is required
80
+ * because a provider config without credentials would silently fail every
81
+ * model call instead of failing to start.
82
+ *
83
+ * `modelId` is deliberately left UNSET. `ProviderConfig.modelId` falls back to
84
+ * `SessionConfig.model`, so leaving it out is what keeps `session.setModel()`
85
+ * authoritative for the rest of the session; pinning it here would freeze the
86
+ * session on the model it was created with and make every later switch a
87
+ * silent no-op.
88
+ *
89
+ * The ceilings are resolved against the model passed in, so a caller that
90
+ * switches models must rebuild this config — `SessionConfig.provider` is fixed
91
+ * at session creation and runtime 1.0.82 exposes no way to update it
92
+ * mid-session (see `copilot-sdk-session-process.mjs`).
93
+ */
94
+ export function resolveCopilotProviderConfig({ env = process.env, model = '', dbg = () => {} } = {}) {
95
+ const type = text(env.COPILOT_PROVIDER_TYPE).toLowerCase();
96
+ if (type !== 'openai') return null;
97
+ const apiKey = text(env.COPILOT_PROVIDER_API_KEY);
98
+ if (!apiKey) return null;
99
+ // The session's live model wins over the configured default: the token
100
+ // ceilings below have to describe the model that will actually be called.
101
+ const effectiveModel = text(model) || text(env.COPILOT_MODEL);
102
+ const wireApi = text(env.COPILOT_PROVIDER_WIRE_API).toLowerCase();
103
+ const limits = resolveOpenAiModelTokenLimits(effectiveModel);
104
+ const maxPromptTokens = tokenCeilingOverride(
105
+ 'COPILOT_PROVIDER_MAX_PROMPT_TOKENS', env.COPILOT_PROVIDER_MAX_PROMPT_TOKENS, dbg,
106
+ ) ?? limits.maxPromptTokens ?? null;
107
+ const maxOutputTokens = tokenCeilingOverride(
108
+ 'COPILOT_PROVIDER_MAX_OUTPUT_TOKENS', env.COPILOT_PROVIDER_MAX_OUTPUT_TOKENS, dbg,
109
+ ) ?? limits.maxOutputTokens ?? null;
110
+ return {
111
+ type: 'openai',
112
+ baseUrl: text(env.COPILOT_PROVIDER_BASE_URL) || DEFAULT_BASE_URL,
113
+ apiKey,
114
+ // Only the two documented values are forwarded; anything else is a typo in
115
+ // the relay config and the runtime's own default ("completions") is a
116
+ // better answer than a rejected session config.
117
+ ...(wireApi === 'responses' || wireApi === 'completions' ? { wireApi } : {}),
118
+ ...(maxPromptTokens ? { maxPromptTokens } : {}),
119
+ ...(maxOutputTokens ? { maxOutputTokens } : {}),
120
+ };
121
+ }