@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,216 @@
1
+ // Bridges the Copilot runtime's two blocking interactive surfaces onto the
2
+ // relay's question cards:
3
+ //
4
+ // * `onUserInputRequest` — the model called `ask_user`. Phase 1 answered these
5
+ // in-band with a "not supported" note; this asks the human.
6
+ // * `onPermissionRequest` in **ask** mode — the model wants to run a mutating
7
+ // tool. Phase 1 blanket-rejected; this offers approve/deny.
8
+ //
9
+ // Both are wired to the SAME relay endpoints the extension's
10
+ // `skills/question-bridge.mjs` uses, so a question raised by the SDK engine
11
+ // renders as the identical card, answers through the identical route, and
12
+ // honours the identical timeout as one raised by the extension engine.
13
+ //
14
+ // The polling/timeout/abort loop is NOT re-implemented here: it is
15
+ // `createAskUserBridge(...).waitForRelayQuestionAnswer` from
16
+ // `shared/ask-user-bridge.mjs`, reused verbatim. Only the CREATE payload is
17
+ // local, because Copilot's request shape carries things the shared
18
+ // `handleAskUserQuestion` cannot express — a real `allowFreeform` (the shared
19
+ // helper hardcodes `true`) and a single question rather than a Claude-style
20
+ // question array. Composing the waiter instead of copying it keeps one
21
+ // implementation of the abort semantics.
22
+ import { createAskUserBridge } from '../../shared/ask-user-bridge.mjs';
23
+ import {
24
+ DEFAULT_QUESTION_TIMEOUT_MS,
25
+ QUESTION_TIMEOUT_CONTINUATION_TEXT,
26
+ } from '../../shared/question-timeout.mjs';
27
+ import { describePermissionRequest } from './copilot-sdk-adapter.mjs';
28
+
29
+ /** Choice labels for a permission card. Freeform text is treated as denial. */
30
+ export const PERMISSION_APPROVE_CHOICE = 'Approve';
31
+ export const PERMISSION_DENY_CHOICE = 'Deny';
32
+
33
+ /**
34
+ * Normalize the runtime's `choices` into a string array.
35
+ *
36
+ * `choices` is documented as nullable — `null` means "no choices, free text
37
+ * only" and must NOT become `[]`-with-`allowFreeform:false`, which would render
38
+ * a card the user cannot answer at all.
39
+ */
40
+ export function normalizeUserInputChoices(choices) {
41
+ if (!Array.isArray(choices)) return [];
42
+ return choices
43
+ .map((choice) => (typeof choice === 'string' ? choice : String(choice?.label ?? choice?.value ?? '')))
44
+ .map((choice) => choice.trim())
45
+ .filter(Boolean);
46
+ }
47
+
48
+ /**
49
+ * Did the human type their own answer rather than pick an offered choice?
50
+ *
51
+ * The relay wire path is identical for both (the UI posts the chosen label as
52
+ * `answer`), so this is derived rather than reported. An answer that exactly
53
+ * matches an offered choice is a selection; anything else — including the
54
+ * timeout continuation text — is freeform. `wasFreeform` is REQUIRED by
55
+ * `UserInputResponse` and the runtime's deserializer is strict, so it must
56
+ * always be a real boolean.
57
+ */
58
+ export function deriveWasFreeform(answer, choices) {
59
+ const normalized = normalizeUserInputChoices(choices);
60
+ if (!normalized.length) return true;
61
+ const value = String(answer ?? '').trim();
62
+ return !normalized.some((choice) => choice === value);
63
+ }
64
+
65
+ export function createCopilotQuestionBridge({
66
+ api,
67
+ sdkSessionId = '',
68
+ getActiveMessage,
69
+ questionPollMs = 1500,
70
+ questionTimeoutMs = DEFAULT_QUESTION_TIMEOUT_MS,
71
+ sleep,
72
+ dbg = () => {},
73
+ } = {}) {
74
+ const shared = createAskUserBridge({
75
+ api,
76
+ sdkSessionId,
77
+ getActiveMessage,
78
+ questionPollMs,
79
+ questionTimeoutMs,
80
+ ...(sleep ? { sleep } : {}),
81
+ dbg,
82
+ });
83
+
84
+ // Question ids this worker created and has not yet seen settle. Shutdown
85
+ // walks these so a pending card is timed out deliberately rather than left
86
+ // spinning in the UI until the relay's 10s expiry sweeper notices.
87
+ const pendingQuestionIds = new Set();
88
+
89
+ async function createQuestion({ prompt, choices, allowFreeform, source, rationale, extra = {} }) {
90
+ const activeMsg = typeof getActiveMessage === 'function' ? getActiveMessage() : null;
91
+ const payload = {
92
+ // The relay 409s ("No active relay turn") unless this queue row is
93
+ // `processing`, which is exactly the state a blocking handler runs in.
94
+ queueId: activeMsg?.id,
95
+ messageId: activeMsg?.id,
96
+ conversationId: activeMsg?.conversationId,
97
+ mode: activeMsg?.relayMode || 'agent',
98
+ prompt,
99
+ choices,
100
+ allowFreeform,
101
+ sdk_session_id: sdkSessionId || undefined,
102
+ timeout_ms: questionTimeoutMs,
103
+ context: {
104
+ source,
105
+ rationale,
106
+ queueMessageId: activeMsg?.id || null,
107
+ conversationId: activeMsg?.conversationId || null,
108
+ relayMode: activeMsg?.relayMode || 'agent',
109
+ ...extra,
110
+ },
111
+ };
112
+ const created = await api('POST', '/api/relay-question', payload);
113
+ const questionId = created?.question?.id;
114
+ if (!questionId) throw new Error('Relay question could not be created');
115
+ return questionId;
116
+ }
117
+
118
+ async function ask(spec, { signal } = {}) {
119
+ const questionId = await createQuestion(spec);
120
+ pendingQuestionIds.add(questionId);
121
+ dbg('relay question created', questionId, spec.source, spec.prompt.slice(0, 80));
122
+ try {
123
+ return await shared.waitForRelayQuestionAnswer(questionId, { signal });
124
+ } finally {
125
+ pendingQuestionIds.delete(questionId);
126
+ }
127
+ }
128
+
129
+ /**
130
+ * `onUserInputRequest` → a relay question card → `{ answer, wasFreeform }`.
131
+ *
132
+ * Never throws: a handler that throws is auto-answered `user-not-available`
133
+ * by the SDK for permissions, and for user input it would fail the tool call
134
+ * silently. A relay that is unreachable therefore degrades to the same
135
+ * in-band note phase 1 always returned, which lets the model continue.
136
+ */
137
+ async function askUserInput(request, { signal } = {}) {
138
+ const question = String(request?.question || '').trim();
139
+ const choices = normalizeUserInputChoices(request?.choices);
140
+ // `allowFreeform` is only meaningful alongside choices; with none, the card
141
+ // must accept free text or it cannot be answered.
142
+ const allowFreeform = choices.length ? request?.allowFreeform !== false : true;
143
+ const result = await ask({
144
+ prompt: question || 'Copilot asked for input to continue this turn.',
145
+ choices,
146
+ allowFreeform,
147
+ source: 'onUserInputRequest',
148
+ rationale: 'Copilot requested clarification to continue this turn.',
149
+ extra: { requestId: String(request?.requestId || '') || undefined },
150
+ }, { signal });
151
+ const answer = String(result?.answer ?? '');
152
+ return {
153
+ answer,
154
+ wasFreeform: result?.timedOut ? true : deriveWasFreeform(answer, choices),
155
+ timedOut: result?.timedOut === true,
156
+ };
157
+ }
158
+
159
+ /**
160
+ * `onPermissionRequest` in ask mode → an approve/deny card.
161
+ *
162
+ * Freeform is allowed and anything that is not exactly "Approve" denies —
163
+ * so a human can type *why* they are refusing and the model receives it as
164
+ * feedback instead of a bare refusal.
165
+ */
166
+ async function askToolApproval(request, { signal } = {}) {
167
+ const description = describePermissionRequest(request);
168
+ const result = await ask({
169
+ prompt: `Copilot wants to run:\n\n${description}\n\nApprove this action?`,
170
+ choices: [PERMISSION_APPROVE_CHOICE, PERMISSION_DENY_CHOICE],
171
+ allowFreeform: true,
172
+ source: 'onPermissionRequest',
173
+ rationale: 'Copilot requested permission to run a tool in ask mode.',
174
+ extra: {
175
+ requestId: String(request?.requestId || '') || undefined,
176
+ permissionKind: String(request?.kind || '') || undefined,
177
+ },
178
+ }, { signal });
179
+ const answer = String(result?.answer ?? '').trim();
180
+ const approved = !result?.timedOut && answer.toLowerCase() === PERMISSION_APPROVE_CHOICE.toLowerCase();
181
+ // The denial note is composed HERE rather than in the permission handler so
182
+ // the handler never needs to know the card's choice labels — that would be
183
+ // an import cycle (this module already imports the request describer from
184
+ // the adapter). A freeform denial carries the human's own reason; a plain
185
+ // "Deny" click does not, so echoing the button label back at the model as
186
+ // though it were an explanation is avoided.
187
+ const feedback = answer && answer.toLowerCase() !== PERMISSION_DENY_CHOICE.toLowerCase()
188
+ ? answer
189
+ : 'The user declined this action.';
190
+ return { approved, answer, feedback, timedOut: result?.timedOut === true, description };
191
+ }
192
+
193
+ /**
194
+ * Settle every card this worker is still waiting on. Called on shutdown: the
195
+ * process is about to stop polling, so a card left `pending` would sit in the
196
+ * UI inviting an answer that nothing will ever read.
197
+ */
198
+ async function cancelPendingQuestions() {
199
+ const ids = [...pendingQuestionIds];
200
+ pendingQuestionIds.clear();
201
+ await Promise.all(ids.map((questionId) => api('POST', `/api/relay-question/${questionId}/timeout`, {})
202
+ .catch(() => {})));
203
+ return ids.length;
204
+ }
205
+
206
+ return {
207
+ askUserInput,
208
+ askToolApproval,
209
+ cancelPendingQuestions,
210
+ pendingQuestionCount: () => pendingQuestionIds.size,
211
+ // Re-exported so the runner can wait on a question id it created itself.
212
+ waitForRelayQuestionAnswer: shared.waitForRelayQuestionAnswer,
213
+ };
214
+ }
215
+
216
+ export { QUESTION_TIMEOUT_CONTINUATION_TEXT };