@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,349 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ function collectTextCandidates(value, out) {
4
+ if (!value) return;
5
+ if (typeof value === "string") {
6
+ const text = value.trim();
7
+ if (text) out.push(text);
8
+ return;
9
+ }
10
+ if (Array.isArray(value)) {
11
+ for (const item of value) collectTextCandidates(item, out);
12
+ return;
13
+ }
14
+ if (typeof value === "object") {
15
+ collectTextCandidates(value.text, out);
16
+ collectTextCandidates(value.content, out);
17
+ collectTextCandidates(value.output_text, out);
18
+ collectTextCandidates(value.outputText, out);
19
+ collectTextCandidates(value.summary, out);
20
+ collectTextCandidates(value.message, out);
21
+ collectTextCandidates(value.result, out);
22
+ collectTextCandidates(value.response, out);
23
+ collectTextCandidates(value.output, out);
24
+ collectTextCandidates(value.answer, out);
25
+ collectTextCandidates(value.finalText, out);
26
+ collectTextCandidates(value.final_text, out);
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Deep-scans a response envelope for a specific named tool call and returns
32
+ * a field from its input. Used to extract task_complete({ summary }) when
33
+ * the agent closes via tool call instead of plain text.
34
+ */
35
+ function extractToolCallInput(value, toolName, field, depth = 0) {
36
+ if (!value || typeof value !== "object" || depth > 12) return null;
37
+ if (Array.isArray(value)) {
38
+ for (const item of value) {
39
+ const found = extractToolCallInput(item, toolName, field, depth + 1);
40
+ if (found) return found;
41
+ }
42
+ return null;
43
+ }
44
+ // Check if this node IS the target tool call
45
+ const name = value.name || value.tool || value.function_name || value.toolName || value.tool_name;
46
+ if (name === toolName) {
47
+ const input = value.input || value.arguments || value.params || value.args || {};
48
+ if (input && typeof input === "object") {
49
+ const text = input[field];
50
+ if (text && typeof text === "string" && text.trim()) return text.trim();
51
+ }
52
+ // Also try string-encoded JSON arguments
53
+ if (typeof value.arguments === "string") {
54
+ try {
55
+ const parsed = JSON.parse(value.arguments);
56
+ if (parsed && typeof parsed[field] === "string" && parsed[field].trim()) return parsed[field].trim();
57
+ } catch { /* ignore */ }
58
+ }
59
+ }
60
+ // Recurse into known envelope container keys only (avoids traversing arbitrary tool inputs)
61
+ const CONTAINER_KEYS = ["data", "output", "content", "tool_calls", "toolRequests",
62
+ "calls", "items", "results", "steps", "turns", "messages", "events"];
63
+ for (const key of CONTAINER_KEYS) {
64
+ const child = value[key];
65
+ if (child !== undefined && child !== null) {
66
+ const found = extractToolCallInput(child, toolName, field, depth + 1);
67
+ if (found) return found;
68
+ }
69
+ }
70
+ return null;
71
+ }
72
+
73
+ function extractFinalText(finalEvent, _dbg) {
74
+ // task_complete({ summary }) is the agent's explicit closing message — highest priority.
75
+ const taskSummary = extractToolCallInput(finalEvent, "task_complete", "summary");
76
+ if (taskSummary) return taskSummary;
77
+
78
+ const candidates = [];
79
+ collectTextCandidates(finalEvent?.data?.content, candidates);
80
+ collectTextCandidates(finalEvent?.data?.text, candidates);
81
+ collectTextCandidates(finalEvent?.data?.output_text, candidates);
82
+ collectTextCandidates(finalEvent?.data?.outputText, candidates);
83
+ collectTextCandidates(finalEvent?.data?.summary, candidates);
84
+ collectTextCandidates(finalEvent?.data?.message, candidates);
85
+ collectTextCandidates(finalEvent?.data?.result, candidates);
86
+ collectTextCandidates(finalEvent?.data?.response, candidates);
87
+ collectTextCandidates(finalEvent?.data?.output, candidates);
88
+ collectTextCandidates(finalEvent?.data?.answer, candidates);
89
+ collectTextCandidates(finalEvent?.data?.finalText, candidates);
90
+ collectTextCandidates(finalEvent?.data?.final_text, candidates);
91
+ collectTextCandidates(finalEvent?.content, candidates);
92
+ collectTextCandidates(finalEvent?.text, candidates);
93
+ collectTextCandidates(finalEvent?.output_text, candidates);
94
+ collectTextCandidates(finalEvent?.outputText, candidates);
95
+ collectTextCandidates(finalEvent?.summary, candidates);
96
+ collectTextCandidates(finalEvent?.message, candidates);
97
+ collectTextCandidates(finalEvent?.result, candidates);
98
+ collectTextCandidates(finalEvent?.response, candidates);
99
+ collectTextCandidates(finalEvent?.output, candidates);
100
+ collectTextCandidates(finalEvent?.answer, candidates);
101
+ collectTextCandidates(finalEvent?.finalText, candidates);
102
+ collectTextCandidates(finalEvent?.final_text, candidates);
103
+ return candidates.find((value) => typeof value === "string" && value.trim()) || "";
104
+ }
105
+
106
+ function parseGeneratedImageDataUrl(value) {
107
+ if (typeof value !== "string") return null;
108
+ const raw = value.trim();
109
+ if (!raw.startsWith("data:")) return null;
110
+ const match = raw.match(/^data:([^;,]+);base64,(.+)$/i);
111
+ if (!match) return null;
112
+ const mimeType = String(match[1] || "").trim().toLowerCase();
113
+ const data = String(match[2] || "").trim();
114
+ if (!mimeType.startsWith("image/") || !data) return null;
115
+ return { mimeType, data };
116
+ }
117
+
118
+ function readGeneratedImageStringField(value, keys = []) {
119
+ if (!value || typeof value !== "object") return "";
120
+ for (const key of keys) {
121
+ const candidate = value[key];
122
+ if (typeof candidate !== "string") continue;
123
+ const trimmed = candidate.trim();
124
+ if (trimmed) return trimmed;
125
+ }
126
+ return "";
127
+ }
128
+
129
+ function normalizeGeneratedImageCandidate(value = {}) {
130
+ if (!value || typeof value !== "object") return null;
131
+ const type = String(value.type || value.output_type || value.kind || value.event || "").trim().toLowerCase();
132
+ const mimeType = String(
133
+ value.mime_type
134
+ || value.mimeType
135
+ || value.content_type
136
+ || value.contentType
137
+ || "",
138
+ ).trim().toLowerCase();
139
+
140
+ const dataUrlCandidate = parseGeneratedImageDataUrl(
141
+ readGeneratedImageStringField(value, [
142
+ "data_url",
143
+ "dataUrl",
144
+ "image_data_url",
145
+ "imageDataUrl",
146
+ ]),
147
+ );
148
+ const base64Data = readGeneratedImageStringField(value, [
149
+ "b64_json",
150
+ "b64Json",
151
+ "image_base64",
152
+ "imageBase64",
153
+ "base64",
154
+ "data",
155
+ ]);
156
+
157
+ const hasKnownType = type.includes("image_generation")
158
+ || type.includes("generated_image")
159
+ || type === "output_image"
160
+ || type === "image";
161
+ const fromDataUrl = dataUrlCandidate && dataUrlCandidate.data
162
+ ? {
163
+ data: dataUrlCandidate.data,
164
+ mimeType: dataUrlCandidate.mimeType,
165
+ }
166
+ : null;
167
+ const fromBase64 = base64Data && (hasKnownType || !!mimeType || "b64_json" in value || "image_base64" in value || "imageBase64" in value)
168
+ ? {
169
+ data: base64Data,
170
+ mimeType: mimeType.startsWith("image/") ? mimeType : "image/png",
171
+ }
172
+ : null;
173
+ const resolved = fromDataUrl || fromBase64;
174
+ if (!resolved?.data) return null;
175
+ return {
176
+ data: resolved.data,
177
+ mimeType: resolved.mimeType,
178
+ name: String(value.name || value.filename || value.fileName || "generated-image").trim() || "generated-image",
179
+ };
180
+ }
181
+
182
+ function collectGeneratedImageCandidates(value, out, depth = 0) {
183
+ if (depth > 12 || value === null || value === undefined) return;
184
+ if (Array.isArray(value)) {
185
+ for (const item of value) collectGeneratedImageCandidates(item, out, depth + 1);
186
+ return;
187
+ }
188
+ if (!value || typeof value !== "object") return;
189
+ const direct = normalizeGeneratedImageCandidate(value);
190
+ if (direct) out.push(direct);
191
+
192
+ const CONTAINER_KEYS = [
193
+ "data",
194
+ "output",
195
+ "outputs",
196
+ "items",
197
+ "images",
198
+ "content",
199
+ "response",
200
+ "result",
201
+ "results",
202
+ "event",
203
+ "events",
204
+ ];
205
+ for (const key of CONTAINER_KEYS) {
206
+ if (!(key in value)) continue;
207
+ collectGeneratedImageCandidates(value[key], out, depth + 1);
208
+ }
209
+ }
210
+
211
+ function extractGeneratedImages(finalEvent) {
212
+ const candidates = [];
213
+ collectGeneratedImageCandidates(finalEvent, candidates);
214
+ const deduped = [];
215
+ const seen = new Set();
216
+ for (const item of candidates) {
217
+ const fingerprint = createHash("sha256").update(item.data).digest("hex");
218
+ const key = `${item.mimeType}:${fingerprint}`;
219
+ if (seen.has(key)) continue;
220
+ seen.add(key);
221
+ deduped.push(item);
222
+ }
223
+ return deduped.slice(0, 8);
224
+ }
225
+
226
+ export function createSessionIoHelpers({ getSession, sleep, dbg = () => {} }) {
227
+ const STREAM_WAIT_POLL_MS = 5_000;
228
+ const WAIT_TICK = Symbol("relay-stream-wait-tick");
229
+
230
+ function buildRelayStreamTimeoutError(timeoutMs) {
231
+ const error = new Error(`Hard timeout after ${timeoutMs}ms while streaming session.send`);
232
+ error.code = "RELAY_STREAM_TIMEOUT";
233
+ error.stableCode = "relay.stream-timeout";
234
+ return error;
235
+ }
236
+
237
+ async function sendAndWaitWithHardTimeout(payload, timeoutMs) {
238
+ // The loser timer is cancelled when the race settles: a fast turn must
239
+ // not leave a five-minute timer armed to fire a rejection into a settled
240
+ // race (bounded, but noisy and needlessly event-loop-pinning).
241
+ let timer = null;
242
+ try {
243
+ return await Promise.race([
244
+ getSession().sendAndWait(payload, timeoutMs),
245
+ new Promise((_resolve, reject) => {
246
+ timer = setTimeout(() => {
247
+ reject(new Error(`Hard timeout after ${timeoutMs}ms waiting for sendAndWait`));
248
+ }, timeoutMs + 5_000);
249
+ if (typeof timer.unref === "function") timer.unref();
250
+ }),
251
+ ]);
252
+ } finally {
253
+ if (timer) clearTimeout(timer);
254
+ }
255
+ }
256
+
257
+ async function sendWithBestEffortStreaming(payload, timeoutMs, onEvent, options = {}) {
258
+ const session = getSession();
259
+ if (!session) throw new Error("No active Copilot session");
260
+ if (typeof session.send !== "function") {
261
+ return sendAndWaitWithHardTimeout(payload, timeoutMs);
262
+ }
263
+
264
+ const streamOrResult = await session.send(payload);
265
+ if (streamOrResult && typeof streamOrResult[Symbol.asyncIterator] === "function") {
266
+ const deadline = Date.now() + timeoutMs + 5_000;
267
+ let finalEvent = null;
268
+ const iterator = streamOrResult[Symbol.asyncIterator]();
269
+ const onWaiting = typeof options?.onWaiting === "function" ? options.onWaiting : null;
270
+ const waitPollMs = Math.max(250, Math.min(Number(options?.waitPollMs) || STREAM_WAIT_POLL_MS, 30_000));
271
+
272
+ try {
273
+ while (true) {
274
+ const remainingMs = deadline - Date.now();
275
+ if (remainingMs <= 0) {
276
+ throw buildRelayStreamTimeoutError(timeoutMs);
277
+ }
278
+
279
+ const nextOrTick = await Promise.race([
280
+ iterator.next(),
281
+ sleep(Math.min(waitPollMs, remainingMs)).then(() => WAIT_TICK),
282
+ ]);
283
+
284
+ if (nextOrTick === WAIT_TICK) {
285
+ if (onWaiting) {
286
+ await onWaiting({
287
+ timeoutMs,
288
+ deadline,
289
+ remainingMs: Math.max(0, deadline - Date.now()),
290
+ });
291
+ }
292
+ continue;
293
+ }
294
+
295
+ if (nextOrTick?.done) break;
296
+ const event = nextOrTick?.value;
297
+ finalEvent = event;
298
+ if (typeof onEvent === "function") {
299
+ try { await onEvent(event); } catch {}
300
+ }
301
+ }
302
+ } finally {
303
+ if (typeof iterator?.return === "function") {
304
+ try { await iterator.return(); } catch {}
305
+ }
306
+ }
307
+ return finalEvent || {};
308
+ }
309
+
310
+ if (typeof onEvent === "function") {
311
+ try { await onEvent(streamOrResult); } catch {}
312
+ }
313
+ return streamOrResult;
314
+ }
315
+
316
+ function extractFinalTextWithLogging(finalEvent) {
317
+ const result = extractFinalText(finalEvent, dbg);
318
+ // Always log envelope structure so we can diagnose extraction misses
319
+ try {
320
+ const raw = JSON.stringify(finalEvent);
321
+ dbg("extractFinalText: result=", JSON.stringify(result).slice(0, 120),
322
+ "| envelope keys:", Object.keys(finalEvent || {}).join(","),
323
+ "| raw (first 600):", raw.slice(0, 600));
324
+ } catch {
325
+ dbg("extractFinalText: result=", JSON.stringify(result).slice(0, 120), "| envelope: [unserializable]");
326
+ }
327
+ return result;
328
+ }
329
+
330
+ function extractGeneratedImagesWithLogging(finalEvent) {
331
+ const images = extractGeneratedImages(finalEvent);
332
+ if (images.length) {
333
+ dbg(
334
+ "extractGeneratedImages: count=",
335
+ images.length,
336
+ "| mime=",
337
+ images.map((entry) => entry.mimeType).join(","),
338
+ );
339
+ }
340
+ return images;
341
+ }
342
+
343
+ return {
344
+ extractFinalText: extractFinalTextWithLogging,
345
+ extractGeneratedImages: extractGeneratedImagesWithLogging,
346
+ sendAndWaitWithHardTimeout,
347
+ sendWithBestEffortStreaming,
348
+ };
349
+ }
@@ -0,0 +1,44 @@
1
+ export function isRetryableSessionJoinConflict(error) {
2
+ const message = String(error?.message || error || '');
3
+ if (!message) return false;
4
+ return (
5
+ message.includes('Failed to connect to IDE MCP server')
6
+ && message.includes('Conflict: A connection for this session already exists')
7
+ );
8
+ }
9
+
10
+ export async function joinSessionWithRetry({
11
+ joinSessionImpl,
12
+ joinOptions,
13
+ dbg = () => {},
14
+ delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
15
+ retries = 4,
16
+ retryDelayMs = 1500,
17
+ } = {}) {
18
+ if (typeof joinSessionImpl !== 'function') {
19
+ throw new Error('joinSessionImpl must be a function');
20
+ }
21
+ const maxAttempts = Math.max(1, Number(retries || 1));
22
+ const waitMs = Math.max(0, Number(retryDelayMs || 0));
23
+ let lastError = null;
24
+
25
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
26
+ try {
27
+ return await joinSessionImpl(joinOptions);
28
+ } catch (error) {
29
+ lastError = error;
30
+ if (!isRetryableSessionJoinConflict(error) || attempt >= maxAttempts) {
31
+ throw error;
32
+ }
33
+ dbg(
34
+ 'joinSession conflict retry',
35
+ `attempt=${attempt}/${maxAttempts}`,
36
+ `delay=${waitMs}ms`,
37
+ error?.message || String(error),
38
+ );
39
+ await delay(waitMs);
40
+ }
41
+ }
42
+
43
+ throw lastError || new Error('joinSession failed');
44
+ }
@@ -0,0 +1,29 @@
1
+ let activeSession = null;
2
+
3
+ function normalizeId(value) {
4
+ const text = String(value ?? "").trim();
5
+ return text || null;
6
+ }
7
+
8
+ export function registerSession(sdkSessionId, conversationId) {
9
+ const nextSessionId = normalizeId(sdkSessionId);
10
+ if (!nextSessionId) {
11
+ activeSession = null;
12
+ return null;
13
+ }
14
+
15
+ activeSession = {
16
+ sdkSessionId: nextSessionId,
17
+ conversationId: normalizeId(conversationId),
18
+ };
19
+
20
+ return { ...activeSession };
21
+ }
22
+
23
+ export function getActiveSession() {
24
+ return activeSession ? { ...activeSession } : null;
25
+ }
26
+
27
+ export function clearSession() {
28
+ activeSession = null;
29
+ }
@@ -0,0 +1,156 @@
1
+ function normalizeReason(reason, fallback = "switch-call-failed") {
2
+ const text = String(reason || "").trim();
3
+ return text || fallback;
4
+ }
5
+
6
+ function isTransientSwitchError(message) {
7
+ const text = String(message || "").toLowerCase();
8
+ if (!text) return false;
9
+ return text.includes("timed out")
10
+ || text.includes("timeout")
11
+ || text.includes("tempor")
12
+ || text.includes("busy")
13
+ || text.includes("again");
14
+ }
15
+
16
+ function isInvalidTargetSwitchError(message) {
17
+ const text = String(message || "").toLowerCase();
18
+ if (!text) return false;
19
+ return text.includes("unknown session")
20
+ || text.includes("not found")
21
+ || text.includes("invalid session")
22
+ || text.includes("no session");
23
+ }
24
+
25
+ export function classifySessionSwitchFailure({
26
+ errors = [],
27
+ mismatched = 0,
28
+ candidateCount = 0,
29
+ } = {}) {
30
+ const normalizedErrors = Array.isArray(errors)
31
+ ? errors.map((value) => String(value || "").trim()).filter(Boolean)
32
+ : [];
33
+ const mismatchCount = Math.max(0, Math.trunc(Number(mismatched) || 0));
34
+ const candidates = Math.max(0, Math.trunc(Number(candidateCount) || 0));
35
+
36
+ if (candidates <= 0) {
37
+ return {
38
+ reason: "switch-api-missing",
39
+ retryable: false,
40
+ };
41
+ }
42
+ if (mismatchCount > 0) {
43
+ return {
44
+ reason: "switch-result-mismatch",
45
+ retryable: true,
46
+ };
47
+ }
48
+ if (normalizedErrors.some((message) => isInvalidTargetSwitchError(message))) {
49
+ return {
50
+ reason: "target-session-invalid",
51
+ retryable: false,
52
+ };
53
+ }
54
+ if (normalizedErrors.some((message) => isTransientSwitchError(message))) {
55
+ return {
56
+ reason: "switch-call-transient-failure",
57
+ retryable: true,
58
+ };
59
+ }
60
+ return {
61
+ reason: "switch-call-failed",
62
+ retryable: false,
63
+ };
64
+ }
65
+
66
+ export function createSessionRuntimeManager({
67
+ dbg = () => {},
68
+ getSession = () => null,
69
+ setSession = () => {},
70
+ } = {}) {
71
+ async function activateSession(targetSessionId, source = "unknown") {
72
+ const target = String(targetSessionId || "").trim();
73
+ const session = getSession?.() || null;
74
+ const switchFn = session?.switchSession;
75
+ if (!target) {
76
+ return {
77
+ ok: false,
78
+ reason: "target-session-invalid",
79
+ retryable: false,
80
+ source,
81
+ };
82
+ }
83
+ if (typeof switchFn !== "function") {
84
+ return {
85
+ ok: false,
86
+ reason: "switch-api-missing",
87
+ retryable: false,
88
+ source,
89
+ };
90
+ }
91
+
92
+ try {
93
+ const switched = await switchFn.call(session, target);
94
+ const switchedSessionId = String(switched?.sessionId || "").trim();
95
+ const liveSession = getSession?.() || session;
96
+ const activeSessionId = String(liveSession?.sessionId || switchedSessionId || "").trim();
97
+ const mismatch = activeSessionId && activeSessionId !== target ? 1 : 0;
98
+ const classified = classifySessionSwitchFailure({
99
+ errors: [],
100
+ mismatched: mismatch,
101
+ candidateCount: 1,
102
+ });
103
+ if (mismatch > 0) {
104
+ dbg(
105
+ "runtime switch mismatch",
106
+ `target=${target}`,
107
+ `active=${activeSessionId}`,
108
+ `source=${source}`,
109
+ );
110
+ return {
111
+ ok: false,
112
+ reason: normalizeReason(classified.reason),
113
+ retryable: classified.retryable === true,
114
+ source,
115
+ };
116
+ }
117
+ if (liveSession && typeof liveSession === "object" && activeSessionId && activeSessionId !== liveSession.sessionId) {
118
+ setSession?.({ ...liveSession, sessionId: activeSessionId });
119
+ } else {
120
+ setSession?.(liveSession);
121
+ }
122
+ return {
123
+ ok: true,
124
+ reason: null,
125
+ retryable: false,
126
+ activeSessionId: activeSessionId || target,
127
+ source,
128
+ };
129
+ } catch (error) {
130
+ const message = String(error?.message || error || "switch-call-failed");
131
+ const classified = classifySessionSwitchFailure({
132
+ errors: [message],
133
+ mismatched: 0,
134
+ candidateCount: 1,
135
+ });
136
+ dbg(
137
+ "runtime switch failed",
138
+ `target=${target}`,
139
+ `reason=${classified.reason}`,
140
+ `retryable=${classified.retryable === true ? "yes" : "no"}`,
141
+ `source=${source}`,
142
+ );
143
+ return {
144
+ ok: false,
145
+ reason: normalizeReason(classified.reason),
146
+ retryable: classified.retryable === true,
147
+ error: message,
148
+ source,
149
+ };
150
+ }
151
+ }
152
+
153
+ return {
154
+ activateSession,
155
+ };
156
+ }
@@ -0,0 +1,74 @@
1
+ let lastSyncedKey = "";
2
+
3
+ function normalizeId(value) {
4
+ const text = String(value ?? "").trim();
5
+ return text || null;
6
+ }
7
+
8
+ export async function syncSessionToServer(
9
+ sdkSessionId,
10
+ conversationId,
11
+ apiClient,
12
+ forceSync = false,
13
+ options = {},
14
+ ) {
15
+ const sessionId = normalizeId(sdkSessionId);
16
+ const nextConversationId = normalizeId(conversationId);
17
+ if (!sessionId || !nextConversationId || typeof apiClient !== "function") {
18
+ return false;
19
+ }
20
+ const workspaceRootPath = normalizeId(options?.workspaceRootPath || options?.cwd);
21
+
22
+ const syncKey = `${sessionId}::${nextConversationId || ""}::${workspaceRootPath || ""}`;
23
+ if (!forceSync && syncKey === lastSyncedKey) {
24
+ return true;
25
+ }
26
+
27
+ const orchestrator = options?.orchestrator && typeof options.orchestrator === "object"
28
+ ? options.orchestrator
29
+ : null;
30
+ const payload = {
31
+ sdk_session_id: sessionId,
32
+ conversation_id: nextConversationId,
33
+ };
34
+ if (workspaceRootPath) payload.workspace_root_path = workspaceRootPath;
35
+ if (orchestrator) {
36
+ const correlationId = normalizeId(
37
+ orchestrator.correlationId
38
+ || orchestrator.transactionId
39
+ || orchestrator.orchestratorCorrelationId,
40
+ );
41
+ const targetSessionId = normalizeId(
42
+ orchestrator.targetSessionId
43
+ || orchestrator.orchestratorTargetSessionId,
44
+ );
45
+ if (correlationId) payload.orchestrator_correlation_id = correlationId;
46
+ if (targetSessionId) payload.orchestrator_target_session_id = targetSessionId;
47
+ if (orchestrator.rebindCompleted === true) payload.rebind_completed = true;
48
+ if (orchestrator.rebindState) payload.rebind_state = String(orchestrator.rebindState);
49
+ if (orchestrator.rebindSignal) payload.rebind_signal = String(orchestrator.rebindSignal);
50
+ }
51
+
52
+ await apiClient("POST", "/api/session-sync", payload);
53
+
54
+ lastSyncedKey = syncKey;
55
+ return true;
56
+ }
57
+
58
+ export async function syncWorkspaceRootToServer(
59
+ workspaceRootPath,
60
+ apiClient,
61
+ options = {},
62
+ ) {
63
+ const nextWorkspaceRootPath = normalizeId(workspaceRootPath);
64
+ if (!nextWorkspaceRootPath || typeof apiClient !== "function") {
65
+ return false;
66
+ }
67
+
68
+ const payload = { workspace_root_path: nextWorkspaceRootPath };
69
+ const sessionId = normalizeId(options?.sdkSessionId || options?.sessionId);
70
+ if (sessionId) payload.sdk_session_id = sessionId;
71
+
72
+ await apiClient("POST", "/api/session-workspace-root", payload);
73
+ return true;
74
+ }