@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,167 @@
1
+ #!/usr/bin/env node
2
+ // Copilot SDK session worker: a per-conversation Node process that speaks the
3
+ // same relay contracts as every other worker (worker WebSocket, heartbeat,
4
+ // control polling, activity channels) but executes turns through the Copilot
5
+ // SDK's headless runtime instead of the `copilot` TUI under a PTY.
6
+ //
7
+ // Dormant until phase 3 registers a worker kind for it. Run it by hand against
8
+ // a live relay with:
9
+ //
10
+ // COPILOT_WEB_RELAY_CONFIG=server/config.json \
11
+ // COPILOT_WORKSPACE_ROOT=/path/to/workspace \
12
+ // COPILOT_SDK_PATH=~/.cache/copilot/pkg/linux-x64/<version>/copilot-sdk \
13
+ // node server/copilot-worker/copilot-sdk-session-worker.mjs --session-id <sdk-session-id>
14
+ import path from 'path';
15
+ import { fileURLToPath } from 'url';
16
+
17
+ import {
18
+ loadTokenFromConfig,
19
+ resolveRelayServerUrl,
20
+ } from '../../.github/extensions/web-relay/runtime/config-loader.mjs';
21
+ import { createApiClient } from '../../.github/extensions/web-relay/runtime/api-client.mjs';
22
+ import { createWorkerWebSocketLink } from '../../.github/extensions/web-relay/runtime/worker-websocket-link.mjs';
23
+ import { createHeartbeatController } from '../../.github/extensions/web-relay/polling/heartbeat.mjs';
24
+ import { createControlPoller } from '../../shared/control-poller.mjs';
25
+ import { installWorkerCrashGuard } from '../../shared/worker-crash-guard.mjs';
26
+ import {
27
+ createWorkerDebug,
28
+ parseSessionIdArg,
29
+ readOptionalMs,
30
+ } from '../../shared/worker-bootstrap.mjs';
31
+ import { createCopilotSdkSessionRunner } from './copilot-sdk-session-process.mjs';
32
+
33
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
34
+ const HEARTBEAT_MS = 10_000;
35
+
36
+ const dbg = createWorkerDebug('copilot-sdk-worker');
37
+
38
+ async function main() {
39
+ const sdkSessionId = parseSessionIdArg();
40
+ if (!sdkSessionId) {
41
+ console.error('copilot-sdk-session-worker: missing --session-id');
42
+ process.exit(2);
43
+ }
44
+
45
+ const configPath = String(process.env.COPILOT_WEB_RELAY_CONFIG || '').trim()
46
+ || path.resolve(__dirname, '..', 'config.json');
47
+ // Two reads of the same small file. Collapsing them to one would mean
48
+ // re-implementing `resolveRelayServerUrl`'s env override, host
49
+ // normalisation and port defaulting inside this worker — a live contract
50
+ // duplicated to save a 1 KB read at process start. Not worth it; the
51
+ // extension helper stays the single source of the relay URL.
52
+ const serverUrl = resolveRelayServerUrl({ configPath });
53
+ const token = loadTokenFromConfig(configPath);
54
+ const cwd = String(process.env.COPILOT_WORKSPACE_ROOT || '').trim() || process.cwd();
55
+ const defaultModel = String(process.env.COPILOT_RELAY_MODEL || '').trim();
56
+
57
+ const api = createApiClient({
58
+ serverUrl,
59
+ token,
60
+ getHeaders: () => ({
61
+ 'X-Relay-Process-Pid': String(process.pid),
62
+ 'X-Relay-Parent-Pid': String(process.ppid),
63
+ 'X-Relay-Session-Id': sdkSessionId,
64
+ 'X-Relay-Conversation-Id': sdkSessionId,
65
+ }),
66
+ });
67
+
68
+ const controlPoller = createControlPoller({
69
+ api,
70
+ sdkSessionId,
71
+ abortAckNote: 'copilot session aborted',
72
+ dbg,
73
+ });
74
+ const turnRunner = createCopilotSdkSessionRunner({
75
+ api,
76
+ sdkSessionId,
77
+ cwd,
78
+ defaultModel,
79
+ controlPoller,
80
+ idleShutdownMs: readOptionalMs('COPILOT_SDK_RELAY_IDLE_SHUTDOWN_MS'),
81
+ turnStallTimeoutMs: readOptionalMs('COPILOT_SDK_RELAY_TURN_STALL_TIMEOUT_MS'),
82
+ // How long live detached shells alone may hold the runtime open (0 = no
83
+ // limit). Deliberately NOT the relay's `background_task_timeout_minutes`
84
+ // slider that rides every delivery payload: that slider defaults to
85
+ // 0/unlimited and governs Claude's background tasks, which the composer
86
+ // lists and can stop. A Copilot detached shell has neither, so consuming it
87
+ // would make "a forgotten shell pins a runtime forever" the default with
88
+ // nothing in the UI to reveal it. The runner's own 30-minute cap applies
89
+ // unless this says otherwise.
90
+ ...(() => {
91
+ const override = readOptionalMs('COPILOT_SDK_RELAY_BACKGROUND_TASK_TIMEOUT_MS');
92
+ return override === undefined ? {} : { getBackgroundTaskTimeoutMs: () => override };
93
+ })(),
94
+ dbg,
95
+ });
96
+
97
+ let heartbeatTimer = null;
98
+ const heartbeat = createHeartbeatController({
99
+ api,
100
+ pollMs: HEARTBEAT_MS,
101
+ getSessionReady: () => true,
102
+ getHeartbeatTimer: () => heartbeatTimer,
103
+ setHeartbeatTimer: (timer) => { heartbeatTimer = timer; },
104
+ getActiveQueueMessageId: () => turnRunner.getActiveQueueMessageId(),
105
+ getActiveQueueMessageIds: () => turnRunner.getActiveQueueMessageIds(),
106
+ });
107
+
108
+ const wsLink = createWorkerWebSocketLink({
109
+ serverUrl,
110
+ token,
111
+ dbg,
112
+ getSessionReady: () => true,
113
+ getSessionId: () => sdkSessionId,
114
+ getPid: () => process.pid,
115
+ onDeliver: async (pending, reason) => {
116
+ dbg('queue.deliver received', `reason=${reason}`, `msgId=${pending?.message?.id || 'none'}`);
117
+ try {
118
+ return await turnRunner.handlePendingPayload(pending);
119
+ } catch (error) {
120
+ dbg('turn handling failed', error?.message || String(error));
121
+ return false;
122
+ }
123
+ },
124
+ onControl: (control) => {
125
+ // Background-task controls have no Copilot SDK equivalent yet: the
126
+ // runtime's `session.background_tasks_changed` carries an empty payload
127
+ // (23 of them fire during a single bash call), so there is no task id to
128
+ // stop. Logged rather than silently dropped.
129
+ dbg('worker control ignored (unsupported by the SDK worker)', String(control?.type || '(none)'));
130
+ },
131
+ });
132
+
133
+ const shutdown = async (signal) => {
134
+ dbg(`shutting down (${signal})`);
135
+ try { wsLink.stop(); } catch {}
136
+ try { heartbeat.stopHeartbeat(); } catch {}
137
+ try { controlPoller.stop(); } catch {}
138
+ // Bounded, like the Cursor worker: stopping the runtime is an RPC, and a
139
+ // hung stop must never make the process ignore the supervisor's SIGTERM.
140
+ try {
141
+ await Promise.race([
142
+ turnRunner.dispose(),
143
+ new Promise((resolve) => {
144
+ const timer = setTimeout(resolve, 3_000);
145
+ timer.unref?.();
146
+ }),
147
+ ]);
148
+ } catch {}
149
+ process.exit(0);
150
+ };
151
+ process.on('SIGTERM', () => { shutdown('SIGTERM'); });
152
+ process.on('SIGINT', () => { shutdown('SIGINT'); });
153
+ installWorkerCrashGuard({
154
+ api,
155
+ workerName: 'copilot-sdk-session-worker',
156
+ getActiveQueueMessageIds: () => turnRunner.getActiveQueueMessageIds(),
157
+ });
158
+
159
+ dbg(`starting session=${sdkSessionId.slice(0, 8)} server=${serverUrl} cwd=${cwd} model=${defaultModel || 'default'}`);
160
+ heartbeat.startHeartbeat();
161
+ wsLink.start();
162
+ }
163
+
164
+ main().catch((error) => {
165
+ console.error('copilot-sdk-session-worker fatal:', error?.stack || error?.message || error);
166
+ process.exit(1);
167
+ });
@@ -0,0 +1,329 @@
1
+ // Spawn-free stand-ins for the Copilot SDK, per DEVELOPING.md: no test in this
2
+ // directory starts a runtime process, imports the installed SDK, or talks to a
3
+ // relay. The injection seam is a constructor option on
4
+ // `createCopilotSdkSessionRunner` (`startClientImpl`), not a module mock.
5
+ //
6
+ // Deliberately NOT named `*.test.mjs` so `node --test`'s glob skips it.
7
+ import fs from 'node:fs';
8
+ import path from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
10
+
11
+ import { createCopilotSdkSessionRunner } from './copilot-sdk-session-process.mjs';
12
+
13
+ const FIXTURE_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), 'fixtures');
14
+
15
+ /** Load a scrubbed phase-0 event dump as an array of `SessionEvent`s. */
16
+ export function loadFixture(name) {
17
+ return JSON.parse(fs.readFileSync(path.join(FIXTURE_DIR, `${name}.json`), 'utf8'));
18
+ }
19
+
20
+ export function tick(ms = 5) {
21
+ return new Promise((resolve) => setTimeout(resolve, ms));
22
+ }
23
+
24
+ export async function waitFor(predicate, { timeoutMs = 3000, label = 'condition' } = {}) {
25
+ const deadline = Date.now() + timeoutMs;
26
+ while (Date.now() < deadline) {
27
+ if (await predicate()) return true;
28
+ await tick(2);
29
+ }
30
+ throw new Error(`waitFor timed out: ${label}`);
31
+ }
32
+
33
+ /**
34
+ * Records every relay call; `failRoutes` makes a route throw, like a 500.
35
+ *
36
+ * `routeResponses` maps a route to its response body, or to a function of
37
+ * `(body, callIndexForThatRoute)` — which is how a test drives a route that
38
+ * answers differently per attempt (`/api/continuation-turn`'s retry loop).
39
+ * Unlisted routes answer `{}`, as before.
40
+ */
41
+ export function makeApiStub({ failRoutes = new Set(), routeResponses = {} } = {}) {
42
+ const calls = [];
43
+ const routeCounts = new Map();
44
+ async function api(method, routePath, body) {
45
+ calls.push({ method, routePath, body });
46
+ const route = String(routePath).split('?')[0];
47
+ if (failRoutes.has(route)) throw new Error(`stub route failure: ${route}`);
48
+ const attempt = routeCounts.get(route) || 0;
49
+ routeCounts.set(route, attempt + 1);
50
+ const canned = routeResponses[route];
51
+ if (typeof canned === 'function') return canned(body, attempt);
52
+ return canned === undefined ? {} : canned;
53
+ }
54
+ api.calls = calls;
55
+ api.bodiesFor = (routePath) => calls.filter((c) => c.routePath === routePath).map((c) => c.body);
56
+ return api;
57
+ }
58
+
59
+ /**
60
+ * An api stub that mints synthetic queue rows, so a test can exercise the
61
+ * continuation path end to end. `POST /api/continuation-turn` answers with a
62
+ * fresh `messageId`, exactly as the relay route does.
63
+ */
64
+ export function makeContinuationApiStub({
65
+ conversationId = 'conv-1',
66
+ messageIds = ['cont-1', 'cont-2', 'cont-3'],
67
+ ...options
68
+ } = {}) {
69
+ const minted = [];
70
+ const stub = makeApiStub({
71
+ ...options,
72
+ routeResponses: {
73
+ '/api/continuation-turn': (body, attempt) => {
74
+ const messageId = messageIds[Math.min(attempt, messageIds.length - 1)];
75
+ minted.push({ messageId, body });
76
+ return { ok: true, messageId, conversationId };
77
+ },
78
+ ...(options.routeResponses || {}),
79
+ },
80
+ });
81
+ stub.mintedContinuations = minted;
82
+ return stub;
83
+ }
84
+
85
+ /**
86
+ * A fake CopilotSession. `send()` accepts one `MessageOptions` object (the
87
+ * real signature: there is no second parameter) and, like the real SDK,
88
+ * resolves immediately — the turn's completion arrives on the event callback,
89
+ * which is what `onSend` is for.
90
+ */
91
+ export function createFakeCopilotSession({ config, onSend = null, onAbort = null }) {
92
+ const session = {
93
+ sessionId: config?.sessionId || 'fake-session',
94
+ config,
95
+ sends: [],
96
+ abortCalls: 0,
97
+ setModelCalls: [],
98
+ disconnected: false,
99
+ emit(event) {
100
+ config?.onEvent?.(event);
101
+ },
102
+ replay(events) {
103
+ for (const event of events) session.emit(event);
104
+ },
105
+ async send(options) {
106
+ session.sends.push(options);
107
+ // Scheduled rather than synchronous so the runner's `await session.send`
108
+ // resolves before the events land, matching the real ordering.
109
+ setTimeout(() => { onSend?.(session, options); }, 0);
110
+ return 'fake-message-id';
111
+ },
112
+ async abort() {
113
+ session.abortCalls += 1;
114
+ setTimeout(() => { onAbort?.(session); }, 0);
115
+ },
116
+ async setModel(model) {
117
+ session.setModelCalls.push(model);
118
+ },
119
+ async disconnect() {
120
+ session.disconnected = true;
121
+ },
122
+ };
123
+ return session;
124
+ }
125
+
126
+ /**
127
+ * The two ways `resumeSession` fails, which the runner MUST tell apart.
128
+ *
129
+ * - `missing` — the runtime has no state under this id. Shaped like the
130
+ * real thing: the runtime throws `Session not found: <id>`
131
+ * and JSON-RPC wraps it as a generic InternalError (-32603),
132
+ * so the message is the only signal.
133
+ * - `transient` — the call itself failed (connection dropped). Falling
134
+ * through to `createSession` here would blank a live
135
+ * conversation.
136
+ */
137
+ export function makeResumeFailure(flavor, sessionId = 'conv-1') {
138
+ if (flavor === 'transient') {
139
+ const error = new Error('Pending response rejected since connection got disposed');
140
+ error.code = -32097;
141
+ return error;
142
+ }
143
+ const error = new Error(`Request session.resume failed with message: Session not found: ${sessionId}`);
144
+ error.code = -32603;
145
+ return error;
146
+ }
147
+
148
+ /**
149
+ * A fake CopilotClient. `resumeAvailable: false` models a brand-new
150
+ * conversation (the runtime has no state to resume), which is what makes the
151
+ * runner fall through to `createSession`. `resumeFailure: 'transient'` models
152
+ * the RPC itself failing, which must NOT fall through.
153
+ */
154
+ export function createFakeCopilotClient({
155
+ resumeAvailable = false,
156
+ resumeFailure = 'missing',
157
+ onSend = null,
158
+ onAbort = null,
159
+ } = {}) {
160
+ const client = {
161
+ stopped: 0,
162
+ resumeAttempts: [],
163
+ createAttempts: [],
164
+ sessions: [],
165
+ resumeAvailable,
166
+ resumeFailure,
167
+ async resumeSession(sessionId, config) {
168
+ client.resumeAttempts.push({ sessionId, config });
169
+ if (!client.resumeAvailable) throw makeResumeFailure(client.resumeFailure, sessionId);
170
+ const session = createFakeCopilotSession({ config, onSend, onAbort });
171
+ session.resumed = true;
172
+ client.sessions.push(session);
173
+ // A session that exists once exists forever, so later reconnects resume.
174
+ client.resumeAvailable = true;
175
+ return session;
176
+ },
177
+ async createSession(config) {
178
+ client.createAttempts.push(config);
179
+ const session = createFakeCopilotSession({ config, onSend, onAbort });
180
+ client.sessions.push(session);
181
+ client.resumeAvailable = true;
182
+ return session;
183
+ },
184
+ async stop() {
185
+ client.stopped += 1;
186
+ return [];
187
+ },
188
+ };
189
+ Object.defineProperty(client, 'session', {
190
+ get: () => client.sessions[client.sessions.length - 1] || null,
191
+ });
192
+ return client;
193
+ }
194
+
195
+ export const FAKE_SDK_PATHS = {
196
+ sdkDir: '/opt/copilot/pkg/linux-x64/1.0.82/copilot-sdk',
197
+ sdkEntry: '/opt/copilot/pkg/linux-x64/1.0.82/copilot-sdk/index.js',
198
+ versionDir: '/opt/copilot/pkg/linux-x64/1.0.82',
199
+ runtimeEntry: '/opt/copilot/pkg/linux-x64/1.0.82/app.js',
200
+ version: '1.0.82',
201
+ };
202
+
203
+ /** Frozen delivery message; tests derive variants by spread. */
204
+ export const baseMessage = Object.freeze({
205
+ id: 'q-1',
206
+ conversationId: 'conv-1',
207
+ relayMode: 'agent',
208
+ text: 'hello',
209
+ model: 'gpt-5-mini',
210
+ });
211
+
212
+ /**
213
+ * A stand-in for the relay question bridge.
214
+ *
215
+ * `answers` maps a matcher to a canned reply, so a test can drive `ask_user`
216
+ * and ask-mode tool approvals without a relay. Every call is recorded.
217
+ */
218
+ export function makeFakeQuestionBridge({
219
+ userInputAnswer = 'the human answer',
220
+ userInputTimedOut = false,
221
+ approve = true,
222
+ approvalTimedOut = false,
223
+ approvalFeedback = 'The user declined this action.',
224
+ onAsk = null,
225
+ } = {}) {
226
+ const bridge = {
227
+ userInputCalls: [],
228
+ approvalCalls: [],
229
+ cancelledCount: 0,
230
+ async askUserInput(request, options) {
231
+ bridge.userInputCalls.push({ request, options });
232
+ if (onAsk) await onAsk(request, options);
233
+ const choices = Array.isArray(request?.choices) ? request.choices : [];
234
+ const answer = typeof userInputAnswer === 'function' ? userInputAnswer(request) : userInputAnswer;
235
+ return {
236
+ answer,
237
+ wasFreeform: userInputTimedOut ? true : !choices.includes(answer),
238
+ timedOut: userInputTimedOut,
239
+ };
240
+ },
241
+ async askToolApproval(request, options) {
242
+ bridge.approvalCalls.push({ request, options });
243
+ if (onAsk) await onAsk(request, options);
244
+ return {
245
+ approved: approvalTimedOut ? false : approve,
246
+ answer: approve ? 'Approve' : 'Deny',
247
+ feedback: approvalFeedback,
248
+ timedOut: approvalTimedOut,
249
+ description: 'a tool',
250
+ };
251
+ },
252
+ async cancelPendingQuestions() {
253
+ bridge.cancelledCount += 1;
254
+ return 0;
255
+ },
256
+ pendingQuestionCount: () => 0,
257
+ };
258
+ return bridge;
259
+ }
260
+
261
+ /**
262
+ * The relay-context prefix the runner prepends to every prompt, for tests that
263
+ * assert on the prompt. `relayToolInstructions` defaults to '' in the harness,
264
+ * so only the mode marker and the mode's standing instructions appear, and the
265
+ * instructions only on the FIRST turn of a given relay mode.
266
+ */
267
+ export function expectedPromptPrefix(mode = 'agent', { includeInstructions = true } = {}) {
268
+ const marker = `[Relay mode: ${mode}]`;
269
+ if (!includeInstructions) return marker;
270
+ const instructions = {
271
+ agent: 'Proceed as an interactive coding agent and use tools as needed. If you need clarification, pause and ask through the web relay instead of stalling silently. These instructions remain in effect until relay mode changes.',
272
+ plan: 'Draft a concise plan only. Use read-only inspection tools (glob, rg, view) only when they materially improve plan quality; otherwise draft from provided context. Do not edit repository files or run mutating commands unless the user explicitly asks for implementation. If clarification is required, pause and ask through the web relay. These instructions remain in effect until relay mode changes.',
273
+ ask: 'Prioritize clarification questions before doing any implementation work. If the request is ambiguous or underspecified, pause and ask through the web relay before making assumptions. Do not make broad assumptions when a question would materially change the result. These instructions remain in effect until relay mode changes.',
274
+ autopilot: 'Act directly on the request and use tools when needed. Keep moving unless user input is truly blocking. These instructions remain in effect until relay mode changes.',
275
+ }[mode];
276
+ return `${marker} ${instructions}`;
277
+ }
278
+
279
+ /** The message body with the relay-context prefix in front, as sent. */
280
+ export function promptWithPrefix(body, mode = 'agent', options = {}) {
281
+ return `${expectedPromptPrefix(mode, options)} ${body}`;
282
+ }
283
+
284
+ export function makeRunner({
285
+ stub,
286
+ client,
287
+ startWarning = null,
288
+ runtimeVersion = '1.0.82',
289
+ questionBridge = makeFakeQuestionBridge(),
290
+ ...overrides
291
+ } = {}) {
292
+ const started = [];
293
+ return {
294
+ started,
295
+ questionBridge,
296
+ runner: createCopilotSdkSessionRunner({
297
+ api: stub,
298
+ sdkSessionId: 'conv-1',
299
+ cwd: '/tmp/relay-fixture-workspace',
300
+ resolvePathsImpl: () => FAKE_SDK_PATHS,
301
+ // Spawn-free and relay-free by default: no test reads the real
302
+ // relay-tools.md off disk or asks a relay whether previews are on.
303
+ relayToolInstructions: '',
304
+ getPreviewInstructionsImpl: () => '',
305
+ createQuestionBridgeImpl: () => questionBridge,
306
+ startClientImpl: async (options) => {
307
+ started.push(options);
308
+ return {
309
+ sdk: {},
310
+ client,
311
+ paths: options.paths,
312
+ // The real adapter never blocks client start on the version probe;
313
+ // it hands back a promise the runner logs when it settles.
314
+ versionReady: Promise.resolve({ runtimeVersion, versionSkewWarning: startWarning }),
315
+ };
316
+ },
317
+ ...overrides,
318
+ }),
319
+ };
320
+ }
321
+
322
+ /**
323
+ * The events of a fixture up to and including the first one matching `type`.
324
+ * Lets a test replay "everything through session.idle" without hand-listing.
325
+ */
326
+ export function eventsThrough(events, type) {
327
+ const index = events.findIndex((event) => event.type === type);
328
+ return index === -1 ? events.slice() : events.slice(0, index + 1);
329
+ }