@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
+ // Pure signal extraction for self-initiated (continuation) turns.
2
+ //
3
+ // The Copilot runtime can start a turn with nobody asking it to. The shape that
4
+ // motivated this module, captured live on 2026-08-31 (session `10a1a9ad`,
5
+ // gpt-5.6-luna, "set a timer to 1 minute"):
6
+ //
7
+ // user prompt → bash{mode:"async", detach:true} → tool.execution_complete
8
+ // "<command started in detached background with shellId: 1>"
9
+ // → assistant.message "Timer set for 1 minute…" → session.idle
10
+ // ── the worker settles the row here, correctly ──
11
+ // 60s later: system.notification{kind:{type:"shell_detached_completed", shellId:"1"}}
12
+ // → assistant.turn_start → read_bash{shellId:"1"}
13
+ // → assistant.message "⏰ The 1-minute timer fired." → session.idle
14
+ //
15
+ // Everything after the notification arrived with **no active turn**, so the
16
+ // worker dropped it and the user never saw the reply. Worse, the 10-minute idle
17
+ // shutdown would have killed the detached shell outright had the timer been
18
+ // longer. This module supplies the three signals the runner needs to fix that,
19
+ // with no I/O and no SDK import so they can be unit-tested against real event
20
+ // payloads:
21
+ //
22
+ // 1. `createBackgroundShellTracker` — which detached shells are still live
23
+ // (lifecycle pinning), and when one settles (a continuation is due).
24
+ // 2. `isContinuationOpeningEvent` — the runtime has started work of its own.
25
+ // 3. `createReplayGate` — the events a `session.resume` replays are history,
26
+ // not new work, and must never open a continuation row.
27
+ //
28
+ // ## Why detached shells rather than `session.background_tasks_changed`
29
+ //
30
+ // `BackgroundTasksChangedEvent` exists and fires (~23 times during a single
31
+ // bash call), but its `data` is documented as an **empty payload** —
32
+ // "indicating background task state changed", with no task list, no id and no
33
+ // state. It cannot answer "is anything still running?", which is the only
34
+ // question lifecycle pinning has. The typed, id-carrying signals are the shell
35
+ // ones, so those are what this tracks:
36
+ //
37
+ // - opened: a `bash` `tool.execution_complete` whose telemetry says
38
+ // `detached: "true"` and/or whose result text names the new shell id;
39
+ // - settled: `system.notification` with a `shell_detached_completed` /
40
+ // `shell_completed` `kind` — a discriminated union carrying `shellId`;
41
+ // - closed by hand: a `read_bash` result reporting the exit code, or a
42
+ // successful `stop_bash`.
43
+ //
44
+ // The set is a best-effort superset-free view: a shell this module never learns
45
+ // about simply is not pinned (the old behaviour), and a shell it never learns
46
+ // the end of is released by the runner's cap. Neither can wedge a turn.
47
+
48
+ /** `system.notification` kinds that mean "a shell this session started is done". */
49
+ export const SHELL_SETTLED_NOTIFICATION_KINDS = Object.freeze([
50
+ 'shell_detached_completed',
51
+ 'shell_completed',
52
+ ]);
53
+
54
+ /** Tools that address an already-running shell by id. */
55
+ export const SHELL_HANDLE_TOOLS = Object.freeze(['read_bash', 'write_bash', 'stop_bash']);
56
+
57
+ // `<command started in detached background with shellId: 1>`
58
+ const SHELL_OPENED_RE = /detached\s+(?:background|command)[^<>]*?shellId:\s*([^\s>,]+)/i;
59
+ // `<detached command with shellId: 1 completed with exit code 0>`
60
+ const SHELL_EXITED_RE = /shellId:\s*([^\s>,]+)[^<>]*?completed with exit code/i;
61
+
62
+ /** Cap on the toolCallId → shell bookkeeping, so a long session cannot grow it forever. */
63
+ const TOOL_CALL_MEMORY = 200;
64
+
65
+ function eventData(event) {
66
+ return event && typeof event.data === 'object' && event.data ? event.data : {};
67
+ }
68
+
69
+ function toolResultText(data) {
70
+ const result = data?.result;
71
+ if (!result || typeof result !== 'object') return '';
72
+ return `${String(result.content || '')}\n${String(result.detailedContent || '')}`;
73
+ }
74
+
75
+ function normalizeShellId(value) {
76
+ const id = String(value ?? '').trim();
77
+ return id && id !== 'undefined' && id !== 'null' ? id : '';
78
+ }
79
+
80
+ /**
81
+ * The events that mean "the runtime has started doing something".
82
+ *
83
+ * An allowlist, not a denylist: the SDK event set grows between CLI releases
84
+ * (a live probe already found an undocumented `subagent.configured`), and the
85
+ * cost of the two directions is not symmetric. A missing opener costs one
86
+ * dropped continuation — today's behaviour. A spurious opener creates a
87
+ * synthetic queue row for a housekeeping event and leaves it open until the
88
+ * stall watchdog fails it, in the user's transcript.
89
+ *
90
+ * `session.idle` / `session.error` are deliberately absent: they terminate
91
+ * turns, and a stray terminator arriving with no turn must stay a no-op rather
92
+ * than open a row it would immediately close. So are `session.start`,
93
+ * `session.resume`, `session.shutdown`, `session.background_tasks_changed`,
94
+ * `pending_messages.modified`, `capabilities.changed` and the `model.*` family,
95
+ * which is all connection bookkeeping.
96
+ */
97
+ export const CONTINUATION_OPENING_EVENT_TYPES = Object.freeze([
98
+ // The runtime picked up a prompt on its own (a resume with
99
+ // `continuePendingWork`, or a prompt queued inside the runtime that outlived
100
+ // the interaction it was sent into).
101
+ 'user.message',
102
+ 'assistant.turn_start',
103
+ 'assistant.message_start',
104
+ 'assistant.message_delta',
105
+ 'assistant.message',
106
+ 'assistant.reasoning',
107
+ 'assistant.reasoning_delta',
108
+ 'assistant.intent',
109
+ 'tool.execution_start',
110
+ 'tool.user_requested',
111
+ 'permission.requested',
112
+ 'user_input.requested',
113
+ 'elicitation.requested',
114
+ 'exit_plan_mode.requested',
115
+ 'subagent.started',
116
+ 'skill.invoked',
117
+ ]);
118
+
119
+ const OPENING_TYPES = new Set(CONTINUATION_OPENING_EVENT_TYPES);
120
+
121
+ export function isContinuationOpeningEvent(event) {
122
+ return OPENING_TYPES.has(String(event?.type || ''));
123
+ }
124
+
125
+ /**
126
+ * Suppress the historical events a `session.resume` replays through the live
127
+ * event callback.
128
+ *
129
+ * `ResumeData` gives both halves of the discriminator, and they are used
130
+ * together because either alone is unsafe:
131
+ *
132
+ * - `resumeTime` — "ISO 8601 timestamp when the session was resumed". A
133
+ * persisted event carries its ORIGINAL `timestamp` (verified against a real
134
+ * `events.jsonl`), so anything older than `resumeTime` is history. Alone,
135
+ * this would suppress forever if the runtime ever emitted a stale timestamp.
136
+ * - `eventCount` — "Total number of persisted events in the session at the
137
+ * time of resume". That is an exact ceiling on how many events a replay can
138
+ * produce. Alone, a count is fragile: ephemeral events interleave with the
139
+ * replay (the resume fixture has 20+ of them in the first second), and
140
+ * miscounting by one would eat a live event or admit a replayed one.
141
+ *
142
+ * So: suppress events older than `resumeTime`, at most `eventCount` of them,
143
+ * and disarm at the first event that is not older — the replay is contiguous
144
+ * and precedes live traffic, so the first live event proves it is over.
145
+ *
146
+ * A resume with no `resumeTime`, or `eventCount: 0`, arms nothing at all: a
147
+ * fresh session has no history to replay, and guessing would be worse than the
148
+ * status quo.
149
+ */
150
+ export function createReplayGate() {
151
+ let window = null;
152
+
153
+ function arm(event) {
154
+ const data = eventData(event);
155
+ const resumeTime = Date.parse(String(data.resumeTime || ''));
156
+ const eventCount = Number(data.eventCount);
157
+ if (!Number.isFinite(resumeTime) || !Number.isFinite(eventCount) || eventCount <= 0) {
158
+ window = null;
159
+ return;
160
+ }
161
+ window = { resumeTime, remaining: Math.floor(eventCount) };
162
+ }
163
+
164
+ /** True when `event` is a replayed historical event and must be dropped. */
165
+ function isReplay(event) {
166
+ if (String(event?.type || '') === 'session.resume') {
167
+ // The resume event itself is live — it is the thing that arms the window.
168
+ arm(event);
169
+ return false;
170
+ }
171
+ if (!window) return false;
172
+ const timestamp = Date.parse(String(event?.timestamp || ''));
173
+ if (!Number.isFinite(timestamp)) return false;
174
+ if (timestamp >= window.resumeTime) {
175
+ window = null;
176
+ return false;
177
+ }
178
+ window.remaining -= 1;
179
+ if (window.remaining <= 0) window = null;
180
+ return true;
181
+ }
182
+
183
+ return {
184
+ isReplay,
185
+ reset: () => { window = null; },
186
+ /** Test seam: whether a replay window is currently open. */
187
+ armed: () => !!window,
188
+ };
189
+ }
190
+
191
+ /**
192
+ * Track the detached shells this session has running.
193
+ *
194
+ * `observe(event)` returns what the event changed, so the caller can pin the
195
+ * runtime's lifecycle and note that a continuation is due without re-parsing.
196
+ */
197
+ export function createBackgroundShellTracker({ now = () => Date.now() } = {}) {
198
+ const shells = new Map(); // shellId -> { shellId, description, startedAt }
199
+ const toolCalls = new Map(); // toolCallId -> { toolName, shellId, description }
200
+
201
+ function rememberToolCall(toolCallId, entry) {
202
+ if (!toolCallId) return;
203
+ toolCalls.set(toolCallId, entry);
204
+ // Bounded: a long conversation makes thousands of tool calls and only the
205
+ // most recent handful are ever looked up.
206
+ while (toolCalls.size > TOOL_CALL_MEMORY) {
207
+ const oldest = toolCalls.keys().next().value;
208
+ toolCalls.delete(oldest);
209
+ }
210
+ }
211
+
212
+ function open(shellId, description) {
213
+ const id = normalizeShellId(shellId);
214
+ if (!id || shells.has(id)) return null;
215
+ const shell = { shellId: id, description: String(description || '').trim(), startedAt: now() };
216
+ shells.set(id, shell);
217
+ return shell;
218
+ }
219
+
220
+ function close(shellId) {
221
+ const id = normalizeShellId(shellId);
222
+ if (!id) return null;
223
+ const shell = shells.get(id) || null;
224
+ shells.delete(id);
225
+ // A shell can settle before this tracker ever saw it open (the worker
226
+ // reconnected mid-flight); report the id anyway so the caller still knows a
227
+ // continuation is due.
228
+ return shell || { shellId: id, description: '', startedAt: 0 };
229
+ }
230
+
231
+ /**
232
+ * `heralded` is true only for the `system.notification` path.
233
+ *
234
+ * That notification is the runtime telling ITSELF a shell finished, and it is
235
+ * what makes it re-invoke the model — so it, and only it, means "a
236
+ * continuation is imminent". A `read_bash` that reports an exit code closes
237
+ * the same shell, but it happens INSIDE a turn that already knows: treating
238
+ * it as a herald would hold the runtime open for the grace window after every
239
+ * ordinary background command.
240
+ */
241
+ function observe(event) {
242
+ const type = String(event?.type || '');
243
+ const data = eventData(event);
244
+ const opened = [];
245
+ const settled = [];
246
+ let heralded = false;
247
+
248
+ if (type === 'tool.execution_start') {
249
+ const toolCallId = String(data.toolCallId || '').trim();
250
+ const toolName = String(data.toolName || '').trim();
251
+ const args = data.arguments && typeof data.arguments === 'object' ? data.arguments : {};
252
+ rememberToolCall(toolCallId, {
253
+ toolName,
254
+ shellId: normalizeShellId(args.shellId),
255
+ description: String(args.description || '').trim(),
256
+ });
257
+ return { opened, settled, heralded };
258
+ }
259
+
260
+ if (type === 'tool.execution_complete') {
261
+ const toolCallId = String(data.toolCallId || '').trim();
262
+ const call = toolCalls.get(toolCallId) || null;
263
+ toolCalls.delete(toolCallId);
264
+ const text = toolResultText(data);
265
+ const detached = String(data.toolTelemetry?.properties?.detached || '') === 'true';
266
+
267
+ // The exit report wins over the open report: a `read_bash` whose result
268
+ // says the command exited is a close, even though its text also matches
269
+ // the "detached … shellId" shape.
270
+ const exited = SHELL_EXITED_RE.exec(text);
271
+ if (exited) {
272
+ const shell = close(exited[1]);
273
+ if (shell) settled.push(shell);
274
+ return { opened, settled, heralded };
275
+ }
276
+
277
+ if (call?.toolName === 'stop_bash' && data.success !== false) {
278
+ const shell = close(call.shellId);
279
+ if (shell) settled.push(shell);
280
+ return { opened, settled, heralded };
281
+ }
282
+
283
+ const started = SHELL_OPENED_RE.exec(text);
284
+ if (started && data.success !== false) {
285
+ const shell = open(started[1], call?.description);
286
+ if (shell) opened.push(shell);
287
+ return { opened, settled, heralded };
288
+ }
289
+ // Telemetry says detached but the result text named no id — nothing to
290
+ // key on, so it is deliberately not tracked rather than tracked wrongly.
291
+ return { opened, settled, heralded };
292
+ }
293
+
294
+ if (type === 'system.notification') {
295
+ const kind = data.kind && typeof data.kind === 'object' ? data.kind : {};
296
+ if (!SHELL_SETTLED_NOTIFICATION_KINDS.includes(String(kind.type || ''))) {
297
+ return { opened, settled, heralded };
298
+ }
299
+ // The runtime is about to re-invoke the model off the back of this.
300
+ heralded = true;
301
+ const shell = close(kind.shellId);
302
+ if (shell) {
303
+ settled.push({
304
+ ...shell,
305
+ description: shell.description || String(kind.description || '').trim(),
306
+ });
307
+ }
308
+ return { opened, settled, heralded };
309
+ }
310
+
311
+ return { opened, settled, heralded };
312
+ }
313
+
314
+ return {
315
+ observe,
316
+ size: () => shells.size,
317
+ live: () => [...shells.values()],
318
+ /**
319
+ * Drop every shell that has been running longer than `capMs`, so a forgotten
320
+ * background command cannot pin the runtime forever. Returns what it forgot.
321
+ */
322
+ expireOlderThan(capMs) {
323
+ if (!(capMs > 0)) return [];
324
+ const deadline = now() - capMs;
325
+ const expired = [];
326
+ for (const shell of [...shells.values()]) {
327
+ if (shell.startedAt <= deadline) {
328
+ shells.delete(shell.shellId);
329
+ expired.push(shell);
330
+ }
331
+ }
332
+ return expired;
333
+ },
334
+ reset() {
335
+ shells.clear();
336
+ toolCalls.clear();
337
+ },
338
+ };
339
+ }
340
+
341
+ /** A one-line transcript note for a settled shell, posted into its continuation. */
342
+ export function describeSettledShell(shell) {
343
+ const id = normalizeShellId(shell?.shellId);
344
+ const description = String(shell?.description || '').trim();
345
+ if (!id) return '';
346
+ return description
347
+ ? `Background shell ${id} finished: ${description}`
348
+ : `Background shell ${id} finished`;
349
+ }
@@ -0,0 +1,110 @@
1
+ // The `plan_ready` board: the card that gives the UI its autopilot handoff
2
+ // buttons when a plan-mode turn produces a plan.
3
+ //
4
+ // The payload is byte-compatible with the Claude, Cursor and Grok workers'
5
+ // (`PLAN_BOARD_ACTIONS`, `boardType`, `title`, `recommendedAction`, `context`
6
+ // shape), because the UI renders whatever `actions` the board carries and the
7
+ // relay's action handler maps those ids onto relay modes. Diverging here would
8
+ // mean a Copilot plan card whose buttons behave differently from every other
9
+ // provider's.
10
+ //
11
+ // Copilot has something the Cursor and Grok workers do not: a real
12
+ // `onExitPlanModeRequest` hook, so the plan text can be taken from the runtime
13
+ // at the moment the agent finishes planning rather than only guessed from the
14
+ // shape of the final prose. Both paths are wired — the hook is primary, the
15
+ // text heuristic is the fallback — which is the Claude worker's arrangement.
16
+ import { countPlanLikeLines } from '../../shared/plan-lines.mjs';
17
+
18
+ /**
19
+ * Kept identical across workers. `exit_only` dismisses; `autopilot` and
20
+ * `interactive` queue a follow-up user message in the mapped relay mode.
21
+ */
22
+ export const PLAN_BOARD_ACTIONS = [
23
+ { id: 'autopilot', label: 'Implement in autopilot', mode: 'autopilot' },
24
+ { id: 'interactive', label: 'Stop here and prompt myself', mode: 'agent' },
25
+ { id: 'exit_only', label: 'Stop here', mode: 'agent' },
26
+ ];
27
+
28
+ /**
29
+ * How many list-ish lines a final message needs before it is treated as a plan.
30
+ * Same threshold as the siblings; `countPlanLikeLines` is the shared predicate.
31
+ */
32
+ export const PLAN_LINE_THRESHOLD = 2;
33
+
34
+ /** Returns null when there is nothing worth posting — the caller then skips. */
35
+ export function buildCopilotPlanReadyBoardPayload({
36
+ message,
37
+ planText = '',
38
+ source = 'plan-mode-fallback',
39
+ } = {}) {
40
+ const summary = String(planText || '').trim();
41
+ if (!summary) return null;
42
+ return {
43
+ queueId: message?.id,
44
+ messageId: message?.id,
45
+ conversationId: message?.conversationId,
46
+ mode: message?.relayMode || 'agent',
47
+ boardType: 'plan_ready',
48
+ title: 'Plan ready for review',
49
+ body: summary,
50
+ actions: PLAN_BOARD_ACTIONS,
51
+ recommendedAction: null,
52
+ context: {
53
+ source,
54
+ queueMessageId: message?.id || null,
55
+ conversationId: message?.conversationId || null,
56
+ relayMode: message?.relayMode || 'agent',
57
+ },
58
+ };
59
+ }
60
+
61
+ /**
62
+ * Should the fallback path post a board for this finished turn?
63
+ *
64
+ * Gated on plan AND ask, which is one mode wider than the Cursor worker (plan
65
+ * only). The board's real precondition is "the work is described but not
66
+ * started", and in plan mode that is guaranteed — mutating tools are denied
67
+ * outright. In ask mode it is NOT: the user can approve a tool, so a turn there
68
+ * can genuinely do the work and then summarise it in bullets. Offering
69
+ * "Implement in autopilot" for work already done would be worse than offering
70
+ * nothing, so ask mode additionally requires that nothing was actually
71
+ * approved (`acted`).
72
+ */
73
+ export function shouldPostPlanBoard({
74
+ relayMode,
75
+ finalText,
76
+ alreadyPosted = false,
77
+ acted = false,
78
+ } = {}) {
79
+ if (alreadyPosted) return false;
80
+ const mode = String(relayMode || '').trim().toLowerCase();
81
+ if (mode !== 'plan' && mode !== 'ask') return false;
82
+ if (mode === 'ask' && acted) return false;
83
+ return countPlanLikeLines(finalText) >= PLAN_LINE_THRESHOLD;
84
+ }
85
+
86
+ /**
87
+ * The text the runtime handed to `onExitPlanModeRequest`. `planContent` is the
88
+ * full plan when the runtime has it; `summary` is always present.
89
+ */
90
+ export function planTextFromExitRequest(request) {
91
+ return String(request?.planContent || request?.summary || '').trim();
92
+ }
93
+
94
+ /**
95
+ * Returned to the runtime after a board is posted.
96
+ *
97
+ * `approved: false` deliberately — approving exit-plan-mode tells the runtime
98
+ * the user accepted the plan, and the SAME turn then rolls straight into
99
+ * implementing it while the board is still sitting unanswered in the UI. The
100
+ * feedback explains the situation so the agent closes out instead of treating
101
+ * the refusal as an obstacle to route around. Mirrors the Claude worker's
102
+ * `EXIT_PLAN_DENY_MESSAGE`.
103
+ */
104
+ export const EXIT_PLAN_BOARD_POSTED_FEEDBACK =
105
+ 'Plan received — it is shown to the user in the relay for review. End the turn now with a short '
106
+ + 'closing message and do not start implementing; the user\'s decision will arrive as a new message.';
107
+
108
+ export const EXIT_PLAN_NO_BOARD_FEEDBACK =
109
+ 'Plan received, but it could not be shown for review. Restate the complete plan as your final '
110
+ + 'message, then end the turn without implementing; the user\'s decision will arrive as a new message.';
@@ -0,0 +1,86 @@
1
+ // Relay prompt context for the SDK worker: the mode marker, the mode's
2
+ // standing instructions, the relay tool guidance, and the live preview-lane
3
+ // block.
4
+ //
5
+ // This mirrors the extension's `createRelayPromptBuilder`
6
+ // (`.github/extensions/web-relay/skills/prompt-context.mjs`) and reuses its
7
+ // text verbatim through `buildModePrompt`, so a conversation reads the same
8
+ // guidance whichever engine ran it. It does NOT reuse the extension's builder
9
+ // wholesale for one reason: that builder also composes the message BODY via its
10
+ // own `buildPrompt`, which renders attachments as a text note. This worker
11
+ // passes real `MessageOptions.attachments` to the runtime
12
+ // (`buildCopilotMessageOptions`), so only the PREFIX is wanted here.
13
+ //
14
+ // Why the relay tool doc applies to this worker at all: `server/relay-tools.md`
15
+ // tells the model to ask through `ask_user` rather than in plain prose, and to
16
+ // restart the relay through the authenticated localhost API. Both are now true
17
+ // on the SDK path — `onUserInputRequest` is bridged to the relay question
18
+ // cards. The preview section is the part that must be live rather than static,
19
+ // which is exactly what `applyPreviewInstructions` swaps in.
20
+ import path from 'path';
21
+ import { fileURLToPath } from 'url';
22
+
23
+ import {
24
+ applyPreviewInstructions,
25
+ createPreviewInstructionsProvider,
26
+ } from '../../shared/preview-instructions.mjs';
27
+ import { buildModePrompt } from '../../.github/extensions/web-relay/skills/prompt-context.mjs';
28
+ import { loadRelayInstructionsFromFile } from '../../.github/extensions/web-relay/runtime/config-loader.mjs';
29
+
30
+ export { createPreviewInstructionsProvider, loadRelayInstructionsFromFile };
31
+
32
+ /**
33
+ * Read `server/relay-tools.md`, honouring the same `COPILOT_WEB_RELAY_TOOLS`
34
+ * override the extension uses.
35
+ *
36
+ * The path is resolved relative to THIS module rather than through the
37
+ * extension's `resolveRelayPaths`, whose `../../../server` walk is anchored to
38
+ * the extension's own directory depth and resolves to the wrong place from
39
+ * `server/copilot-worker/`. A missing file yields '' (the loader swallows the
40
+ * error), which degrades to "no tool guidance", never to a failed turn.
41
+ */
42
+ export function loadDefaultRelayToolInstructions({ env = process.env } = {}) {
43
+ const override = String(env.COPILOT_WEB_RELAY_TOOLS || '').trim();
44
+ const toolsPath = override
45
+ || path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'relay-tools.md');
46
+ return loadRelayInstructionsFromFile(toolsPath);
47
+ }
48
+
49
+ /**
50
+ * Build the per-turn relay context prefix.
51
+ *
52
+ * The heavy guidance rides along only when the relay mode CHANGED, matching the
53
+ * extension: repeat turns in one mode stay cheap, and the model is not re-read
54
+ * its standing instructions every message. The mode marker itself is always
55
+ * present.
56
+ *
57
+ * `agentMode` is threaded to the runtime natively as well
58
+ * (`copilotAgentModeForRelayMode`); the text is not a substitute for it but a
59
+ * reinforcement, same as on the extension path.
60
+ */
61
+ export function createCopilotPromptContextBuilder({
62
+ toolInstructions = '',
63
+ getPreviewInstructions = null,
64
+ } = {}) {
65
+ let lastPromptedRelayMode = null;
66
+ return async function buildRelayContextPrefix(message) {
67
+ const relayMode = String(message?.relayMode || 'agent').trim().toLowerCase() || 'agent';
68
+ const includeInstructions = lastPromptedRelayMode !== relayMode;
69
+ let instructions = '';
70
+ if (includeInstructions) {
71
+ // Advisory: a preview lookup that fails costs the block, never the turn.
72
+ const previewBlock = typeof getPreviewInstructions === 'function'
73
+ ? await Promise.resolve().then(getPreviewInstructions).catch(() => '')
74
+ : '';
75
+ instructions = applyPreviewInstructions(toolInstructions, previewBlock);
76
+ }
77
+ const prefix = buildModePrompt(relayMode, instructions, { includeInstructions });
78
+ lastPromptedRelayMode = relayMode;
79
+ return prefix;
80
+ };
81
+ }
82
+
83
+ /** Prepend the relay context prefix to a message body built elsewhere. */
84
+ export function withRelayContext(prefix, body) {
85
+ return [String(prefix || '').trim(), String(body || '').trim()].filter(Boolean).join(' ');
86
+ }