@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,145 @@
1
+ #!/usr/bin/env node
2
+ // Grok session worker: a per-conversation Node process that speaks the same
3
+ // relay contracts as the Claude/Cursor workers but executes turns through the
4
+ // Grok CLI ACP surface (grok agent stdio).
5
+ import path from 'path';
6
+ import { fileURLToPath } from 'url';
7
+
8
+ import {
9
+ loadTokenFromConfig,
10
+ resolveRelayServerUrl,
11
+ } from '../../.github/extensions/web-relay/runtime/config-loader.mjs';
12
+ import { createApiClient } from '../../.github/extensions/web-relay/runtime/api-client.mjs';
13
+ import { createWorkerWebSocketLink } from '../../.github/extensions/web-relay/runtime/worker-websocket-link.mjs';
14
+ import { createHeartbeatController } from '../../.github/extensions/web-relay/polling/heartbeat.mjs';
15
+ import { createControlPoller } from '../../shared/control-poller.mjs';
16
+ import { installWorkerCrashGuard } from '../../shared/worker-crash-guard.mjs';
17
+ import { createGrokTurnRunner } from './grok-turn-runner.mjs';
18
+
19
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
20
+ const HEARTBEAT_MS = 10_000;
21
+
22
+ function parseSessionIdArg(argv = process.argv) {
23
+ const index = argv.indexOf('--session-id');
24
+ if (index !== -1 && argv[index + 1]) return String(argv[index + 1]).trim();
25
+ const inline = argv.find((arg) => String(arg || '').startsWith('--session-id='));
26
+ if (inline) return String(inline.split('=')[1] || '').trim();
27
+ return String(process.env.SESSION_ID || '').trim();
28
+ }
29
+
30
+ function dbg(...parts) {
31
+ const timestamp = new Date().toISOString();
32
+ console.log(`[grok-worker ${timestamp}]`, ...parts);
33
+ }
34
+
35
+ async function main() {
36
+ const sdkSessionId = parseSessionIdArg();
37
+ if (!sdkSessionId) {
38
+ console.error('grok-session-worker: missing --session-id');
39
+ process.exit(2);
40
+ }
41
+
42
+ const configPath = String(process.env.COPILOT_WEB_RELAY_CONFIG || '').trim()
43
+ || path.resolve(__dirname, '..', 'config.json');
44
+ const serverUrl = resolveRelayServerUrl({ configPath });
45
+ const token = loadTokenFromConfig(configPath);
46
+ const cwd = String(process.env.COPILOT_WORKSPACE_ROOT || '').trim() || process.cwd();
47
+ const defaultModel = String(process.env.GROK_RELAY_MODEL || '').trim();
48
+ const command = String(process.env.GROK_CLI_COMMAND || process.env.GROK_COMMAND || 'grok').trim() || 'grok';
49
+ const alwaysApprove = String(process.env.GROK_ALWAYS_APPROVE || '1').trim() !== '0';
50
+
51
+ const api = createApiClient({
52
+ serverUrl,
53
+ token,
54
+ getHeaders: () => ({
55
+ 'X-Relay-Process-Pid': String(process.pid),
56
+ 'X-Relay-Parent-Pid': String(process.ppid),
57
+ 'X-Relay-Session-Id': sdkSessionId,
58
+ 'X-Relay-Conversation-Id': sdkSessionId,
59
+ }),
60
+ });
61
+
62
+ const controlPoller = createControlPoller({
63
+ api,
64
+ sdkSessionId,
65
+ abortAckNote: 'grok run cancelled',
66
+ dbg,
67
+ });
68
+ const turnRunner = createGrokTurnRunner({
69
+ api,
70
+ sdkSessionId,
71
+ cwd,
72
+ defaultModel,
73
+ command,
74
+ alwaysApprove,
75
+ controlPoller,
76
+ dbg,
77
+ });
78
+
79
+ let heartbeatTimer = null;
80
+ const heartbeat = createHeartbeatController({
81
+ api,
82
+ pollMs: HEARTBEAT_MS,
83
+ getSessionReady: () => true,
84
+ getHeartbeatTimer: () => heartbeatTimer,
85
+ setHeartbeatTimer: (timer) => { heartbeatTimer = timer; },
86
+ getActiveQueueMessageId: () => turnRunner.getActiveQueueMessageId(),
87
+ });
88
+
89
+ const wsLink = createWorkerWebSocketLink({
90
+ serverUrl,
91
+ token,
92
+ dbg,
93
+ getSessionReady: () => true,
94
+ getSessionId: () => sdkSessionId,
95
+ getPid: () => process.pid,
96
+ onDeliver: async (pending, reason) => {
97
+ dbg('queue.deliver received', `reason=${reason}`, `msgId=${pending?.message?.id || 'none'}`);
98
+ const deliveredCeiling = Number(pending?.settings?.turnCeilingMs);
99
+ if (Number.isFinite(deliveredCeiling) && deliveredCeiling >= 0) {
100
+ turnRunner.setTurnCeilingMs?.(deliveredCeiling);
101
+ }
102
+ try {
103
+ return await turnRunner.handlePendingPayload(pending);
104
+ } catch (error) {
105
+ dbg('turn handling failed', error?.message || String(error));
106
+ return false;
107
+ }
108
+ },
109
+ });
110
+
111
+ const shutdown = async (signal) => {
112
+ dbg(`shutting down (${signal})`);
113
+ try { wsLink.stop(); } catch {}
114
+ try { heartbeat.stopHeartbeat(); } catch {}
115
+ try { controlPoller.stop(); } catch {}
116
+ // Bounded: a hung ACP agent teardown must never leave the process
117
+ // ignoring the supervisor's SIGTERM.
118
+ try {
119
+ await Promise.race([
120
+ turnRunner.dispose(),
121
+ new Promise((resolve) => {
122
+ const timer = setTimeout(resolve, 3_000);
123
+ timer.unref?.();
124
+ }),
125
+ ]);
126
+ } catch {}
127
+ process.exit(0);
128
+ };
129
+ process.on('SIGTERM', () => { shutdown('SIGTERM'); });
130
+ process.on('SIGINT', () => { shutdown('SIGINT'); });
131
+ installWorkerCrashGuard({
132
+ api,
133
+ workerName: 'grok-session-worker',
134
+ getActiveQueueMessageIds: () => [turnRunner.getActiveQueueMessageId()],
135
+ });
136
+
137
+ dbg(`starting session=${sdkSessionId.slice(0, 8)} server=${serverUrl} cwd=${cwd} model=${defaultModel || 'default'}`);
138
+ heartbeat.startHeartbeat();
139
+ wsLink.start();
140
+ }
141
+
142
+ main().catch((error) => {
143
+ console.error('grok-session-worker fatal:', error?.stack || error?.message || error);
144
+ process.exit(1);
145
+ });
@@ -0,0 +1,523 @@
1
+ import {
2
+ createGrokAgentHandle,
3
+ startGrokTurn,
4
+ classifyGrokError,
5
+ } from './grok-sdk-adapter.mjs';
6
+ import { buildGrokContextUsage, resolveGrokContextWindow } from './grok-context-usage.mjs';
7
+ import { extractGrokUsageFromPromptResult, normalizeGrokTurnUsage } from '../services/plan-usage-grok.mjs';
8
+ import { createPreviewInstructionsProvider } from '../../shared/preview-instructions.mjs';
9
+ import { countPlanLikeLines } from '../../shared/plan-lines.mjs';
10
+ import { EMPTY_TURN_COMPLETION_NOTE } from '../../shared/empty-turn-completion.mjs';
11
+
12
+ const PLAN_BOARD_ACTIONS = [
13
+ { id: 'autopilot', label: 'Implement in autopilot', mode: 'autopilot' },
14
+ { id: 'interactive', label: 'Stop here and prompt myself', mode: 'agent' },
15
+ { id: 'exit_only', label: 'Stop here', mode: 'agent' },
16
+ ];
17
+
18
+ // Grok has no exit-plan tool, so the only board source is the plan-mode
19
+ // text-shape fallback; the payload matches the Claude/Cursor workers'.
20
+ export function buildGrokPlanReadyBoardPayload({
21
+ message,
22
+ planText = '',
23
+ source = 'plan-mode-fallback',
24
+ } = {}) {
25
+ const summary = String(planText || '').trim();
26
+ if (!summary) return null;
27
+ return {
28
+ queueId: message?.id,
29
+ messageId: message?.id,
30
+ conversationId: message?.conversationId,
31
+ mode: message?.relayMode || 'agent',
32
+ boardType: 'plan_ready',
33
+ title: 'Plan ready for review',
34
+ body: summary,
35
+ actions: PLAN_BOARD_ACTIONS,
36
+ recommendedAction: null,
37
+ context: {
38
+ source,
39
+ queueMessageId: message?.id || null,
40
+ conversationId: message?.conversationId || null,
41
+ relayMode: message?.relayMode || 'agent',
42
+ },
43
+ };
44
+ }
45
+
46
+ function buildGrokTerminalError(classified, message) {
47
+ return {
48
+ kind: 'grok-turn-failed',
49
+ code: classified.code,
50
+ stableCode: classified.code,
51
+ message: classified.message,
52
+ failedAt: new Date().toISOString(),
53
+ queueMessageId: String(message?.id || '') || null,
54
+ };
55
+ }
56
+
57
+ const MODE_NUDGES = {
58
+ ask: '[Relay mode: ask] Prioritize clarification questions before implementation work; '
59
+ + 'do not make broad assumptions when a question would materially change the result.',
60
+ autopilot: '[Relay mode: autopilot] Keep moving unless user input is truly blocking; avoid unnecessary questions.',
61
+ plan: '[Relay mode: plan] Produce a concrete plan before making code changes; prefer outlining steps over implementing.',
62
+ };
63
+
64
+ export function grokModeNudge(relayMode, previousMode = '') {
65
+ const mode = String(relayMode || 'agent').trim().toLowerCase();
66
+ const previous = String(previousMode || '').trim().toLowerCase();
67
+ if (mode === previous) return '';
68
+ if (MODE_NUDGES[mode]) return MODE_NUDGES[mode];
69
+ if (MODE_NUDGES[previous]) return `[Relay mode: ${mode}] Previous relay-mode instructions no longer apply.`;
70
+ return '';
71
+ }
72
+
73
+ export function buildGrokUserText(message = {}) {
74
+ const text = String(message?.text || message?.content || '').trim();
75
+ const attachments = Array.isArray(message?.attachments) ? message.attachments : [];
76
+ const notes = [];
77
+ for (const attachment of attachments) {
78
+ const name = String(attachment?.name || attachment?.filename || '').trim();
79
+ const pathValue = String(attachment?.path || attachment?.absolutePath || '').trim();
80
+ if (pathValue) notes.push(`[Attached file: ${pathValue}]`);
81
+ else if (name) notes.push(`[Attached file: ${name}]`);
82
+ }
83
+ return [text, ...notes].filter(Boolean).join('\n\n');
84
+ }
85
+
86
+ /**
87
+ * Execute one delivered relay turn against the Grok CLI ACP surface.
88
+ */
89
+ export function createGrokTurnRunner({
90
+ api,
91
+ sdkSessionId,
92
+ cwd,
93
+ defaultModel = '',
94
+ command = 'grok',
95
+ alwaysApprove = true,
96
+ controlPoller,
97
+ createAgentHandleImpl = createGrokAgentHandle,
98
+ startGrokTurnImpl = startGrokTurn,
99
+ classifyErrorImpl = classifyGrokError,
100
+ getPreviewInstructions = createPreviewInstructionsProvider({ api }),
101
+ dbg = () => {},
102
+ } = {}) {
103
+ let agentHandle = null;
104
+ let grokNativeSessionId = '';
105
+ let activeMessage = null;
106
+ let waitingForTurn = false;
107
+ let currentAbortController = null;
108
+ let lastNudgedRelayMode = '';
109
+ // ACP has no system-prompt seam, so capability guidance rides the prompt
110
+ // prefix like the mode nudge — once per worker, since the Grok session keeps
111
+ // the history it was told in.
112
+ let previewInstructionsSent = false;
113
+ // The user's max-turn-duration ceiling (0 = no limit), piggybacked on queue
114
+ // deliveries; null means no delivery has told us yet and the ACP defaults
115
+ // apply.
116
+ let turnCeilingMs = null;
117
+
118
+ function getActiveQueueMessageId() {
119
+ return waitingForTurn ? String(activeMessage?.id || '') : '';
120
+ }
121
+
122
+ function isTurnActive() {
123
+ return waitingForTurn;
124
+ }
125
+
126
+ async function dispose() {
127
+ try {
128
+ await agentHandle?.close?.();
129
+ } catch (error) {
130
+ dbg('grok agent close failed on dispose', error?.message || String(error));
131
+ }
132
+ agentHandle = null;
133
+ }
134
+
135
+ async function persistNativeSessionId(message, sessionId) {
136
+ const normalized = String(sessionId || '').trim();
137
+ if (!normalized || normalized === grokNativeSessionId) return;
138
+ try {
139
+ await api('POST', '/api/grok-native-session', {
140
+ conversationId: message.conversationId,
141
+ grokNativeSessionId: normalized,
142
+ });
143
+ grokNativeSessionId = normalized;
144
+ } catch (error) {
145
+ dbg('grok native session persist failed', error?.message || String(error));
146
+ }
147
+ }
148
+
149
+ async function ensureAgentHandle(message, model) {
150
+ if (agentHandle) return agentHandle;
151
+ const resumeId = String(message.grokNativeSessionId || grokNativeSessionId || '').trim();
152
+ agentHandle = await createAgentHandleImpl({
153
+ command,
154
+ cwd,
155
+ alwaysApprove,
156
+ nativeSessionId: resumeId,
157
+ model,
158
+ dbg,
159
+ });
160
+ if (agentHandle.sessionId) {
161
+ await persistNativeSessionId(message, agentHandle.sessionId);
162
+ }
163
+ // A silent fresh start after a failed session/load looks like the
164
+ // conversation lost its memory for no reason — say so in the transcript.
165
+ if (agentHandle.resumeFailed && message?.id) {
166
+ await postActivity(message, 'System note: the previous Grok session could not be restored; this turn starts a fresh agent session without the earlier context.');
167
+ }
168
+ return agentHandle;
169
+ }
170
+
171
+ async function postActivity(message, text, subagentRunId = null) {
172
+ if (!text) return;
173
+ await api('POST', '/api/activity', {
174
+ messageId: message.id,
175
+ conversationId: message.conversationId,
176
+ mode: message.relayMode || 'agent',
177
+ text,
178
+ ...(subagentRunId ? { subagentRunId } : {}),
179
+ }).catch(() => {});
180
+ }
181
+
182
+ async function dispatchAction(message, action, state) {
183
+ const { channel, payload } = action;
184
+ if (channel === 'init') {
185
+ state.responseModel = payload.model || state.responseModel;
186
+ await persistNativeSessionId(message, payload.sessionId);
187
+ return;
188
+ }
189
+ if (channel === 'stream') {
190
+ if (!payload.subagentRunId) state.lastStreamedText = payload.text;
191
+ await api('POST', '/api/stream', {
192
+ messageId: message.id,
193
+ conversationId: message.conversationId,
194
+ mode: message.relayMode || 'agent',
195
+ text: payload.text,
196
+ done: payload.done === true,
197
+ ...(payload.subagentRunId ? { subagentRunId: payload.subagentRunId } : {}),
198
+ }).catch(() => {});
199
+ return;
200
+ }
201
+ if (channel === 'thought') {
202
+ await api('POST', '/api/thought', {
203
+ messageId: message.id,
204
+ conversationId: message.conversationId,
205
+ mode: message.relayMode || 'agent',
206
+ reasoningId: payload.reasoningId,
207
+ text: payload.text,
208
+ done: payload.done === true,
209
+ ...(payload.subagentRunId ? { subagentRunId: payload.subagentRunId } : {}),
210
+ }).catch(() => {});
211
+ return;
212
+ }
213
+ if (channel === 'activity') {
214
+ await postActivity(message, payload.text, payload.subagentRunId);
215
+ return;
216
+ }
217
+ if (channel === 'subagent') {
218
+ await api('POST', '/api/subagent-run', {
219
+ messageId: message.id,
220
+ conversationId: message.conversationId,
221
+ subagentRunId: payload.subagentRunId,
222
+ parentSubagentId: payload.parentSubagentId || undefined,
223
+ displayName: payload.displayName || undefined,
224
+ status: payload.status,
225
+ }).catch(() => {});
226
+ return;
227
+ }
228
+ if (channel === 'result') {
229
+ state.result = payload;
230
+ state.responseModel = payload.model || state.responseModel;
231
+ // Prefer the prompt-result usage blob; fall back to a normalized stream usage.
232
+ if (payload?.usage) {
233
+ state.turnUsage = normalizeGrokTurnUsage(payload.usage)
234
+ || extractGrokUsageFromPromptResult({ _meta: payload.usage })
235
+ || state.turnUsage;
236
+ }
237
+ }
238
+ }
239
+
240
+ /**
241
+ * Ship the turn's token usage to the relay as a context-window breakdown.
242
+ * Advisory data: a failure here must not disturb the turn's response.
243
+ */
244
+ async function publishContextUsage(message, state, model) {
245
+ if (!state.turnUsage) return;
246
+ const usageModel = state.responseModel || state.turnUsage.modelId || model || '';
247
+ const contextWindow = resolveGrokContextWindow(
248
+ usageModel,
249
+ agentHandle?.discovered?.contextWindowsByModel || {},
250
+ );
251
+ const built = buildGrokContextUsage({
252
+ usage: state.turnUsage,
253
+ model: usageModel,
254
+ contextWindow,
255
+ });
256
+ if (!built) return;
257
+ await api('POST', '/api/grok-context-usage', {
258
+ conversationId: message.conversationId,
259
+ sdkSessionId,
260
+ model: usageModel,
261
+ contextUsage: built.contextUsage,
262
+ modelUsage: built.modelUsage,
263
+ }).catch((error) => {
264
+ dbg('context usage publish failed', error?.message || String(error));
265
+ });
266
+ }
267
+
268
+ /**
269
+ * Ship per-turn token/cost totals for the Check Usage Grok card. Advisory:
270
+ * failures never disturb the turn reply.
271
+ */
272
+ async function publishPlanUsage(message, state) {
273
+ if (!state.turnUsage) return;
274
+ await api('POST', '/api/grok-plan-usage', {
275
+ conversationId: message.conversationId,
276
+ sdkSessionId,
277
+ usage: state.turnUsage,
278
+ model: state.responseModel || state.turnUsage.modelId || '',
279
+ capturedAt: state.turnUsage.capturedAt || new Date().toISOString(),
280
+ }).catch((error) => {
281
+ dbg('plan usage publish failed', error?.message || String(error));
282
+ });
283
+ }
284
+
285
+ async function publishFinalStream(message, text) {
286
+ await api('POST', '/api/stream', {
287
+ messageId: message.id,
288
+ conversationId: message.conversationId,
289
+ mode: message.relayMode || 'agent',
290
+ text: String(text || ''),
291
+ done: true,
292
+ }).catch(() => {});
293
+ }
294
+
295
+ async function publishResponse(message, { text, model, terminalError = null, modelOrigin }) {
296
+ await api('POST', '/api/response', {
297
+ messageId: message.id,
298
+ conversationId: message.conversationId,
299
+ text: String(text || ''),
300
+ model: model || null,
301
+ modelOrigin: modelOrigin
302
+ || (String(message?.model || '').trim().toLowerCase() === 'auto' ? 'auto' : 'manual'),
303
+ ...(terminalError ? { terminalError } : {}),
304
+ }).catch(async () => {
305
+ await api('POST', '/api/requeue', { messageId: message.id }).catch(() => {});
306
+ });
307
+ }
308
+
309
+ async function runTurn(message) {
310
+ currentAbortController = new AbortController();
311
+ const abortController = currentAbortController;
312
+ const state = {
313
+ result: null,
314
+ lastStreamedText: '',
315
+ responseModel: '',
316
+ turnUsage: null,
317
+ };
318
+ let aborted = false;
319
+
320
+ // The conversation's Grok model is pinned at bootstrap; the relay 409s a
321
+ // per-message switch before delivery, so the request here can only match
322
+ // the pinned model (or 'auto'). The handle keeps the model the session
323
+ // was created with — it is never relabeled to a merely requested one.
324
+ const requestedModel = String(message.model || '').trim();
325
+ const perTurnModel = requestedModel && requestedModel.toLowerCase() !== 'auto'
326
+ ? requestedModel
327
+ : '';
328
+ const model = perTurnModel
329
+ || String(message.providerModel || '').trim()
330
+ || defaultModel;
331
+
332
+ await ensureAgentHandle(message, model);
333
+
334
+ const modeNudge = grokModeNudge(message.relayMode, lastNudgedRelayMode);
335
+ const userText = buildGrokUserText(message);
336
+ // Advisory: a preview lookup that fails costs the guidance, never the turn.
337
+ const previewBlock = previewInstructionsSent
338
+ ? ''
339
+ : await Promise.resolve()
340
+ .then(() => getPreviewInstructions?.())
341
+ .catch(() => '');
342
+ const promptText = [previewBlock, modeNudge, userText].filter(Boolean).join('\n\n');
343
+ const pendingNudgedRelayMode = String(message.relayMode || 'agent').trim().toLowerCase();
344
+
345
+ let turn = null;
346
+ let planBoardPosted = false;
347
+ const controlState = controlPoller?.start?.({
348
+ queueMessageId: message.id,
349
+ onAbortTurn: async () => {
350
+ aborted = true;
351
+ abortController.abort();
352
+ await turn?.cancel?.();
353
+ },
354
+ });
355
+
356
+ try {
357
+ let busyRetries = 0;
358
+ let actionsDispatched = false;
359
+ while (true) {
360
+ try {
361
+ turn = startGrokTurnImpl({
362
+ handle: agentHandle,
363
+ text: promptText,
364
+ reasoningEffort: message.reasoningEffort || '',
365
+ abortSignal: abortController.signal,
366
+ // The user's max-turn-duration setting overrides the worker-local
367
+ // absolute cap: "No limit" (0) must not be silently trimmed to
368
+ // the 30-minute default. The inactivity watchdog stays — it
369
+ // detects a dead transport, not a long turn.
370
+ watchdog: turnCeilingMs !== null
371
+ ? { maxTurnMs: turnCeilingMs > 0 ? turnCeilingMs : 0 }
372
+ : null,
373
+ dbg,
374
+ });
375
+ for await (const action of turn) {
376
+ actionsDispatched = true;
377
+ await dispatchAction(message, action, state);
378
+ }
379
+ lastNudgedRelayMode = pendingNudgedRelayMode;
380
+ if (previewBlock) previewInstructionsSent = true;
381
+ break;
382
+ } catch (error) {
383
+ // A stale agent whose previous prompt never settled reports busy;
384
+ // one close-and-resume usually clears it — but only before anything
385
+ // streamed: replaying the whole prompt over already-dispatched
386
+ // frames would duplicate text the user has seen. A busy after
387
+ // dispatch (or a second busy) is terminal.
388
+ if (busyRetries === 0 && !actionsDispatched && classifyErrorImpl(error).isBusy) {
389
+ busyRetries += 1;
390
+ dbg('grok agent busy, recreating handle for one retry');
391
+ await dispose();
392
+ await ensureAgentHandle(message, model);
393
+ continue;
394
+ }
395
+ throw error;
396
+ }
397
+ }
398
+ } catch (error) {
399
+ if (aborted || abortController.signal.aborted) {
400
+ dbg('turn aborted', message.id);
401
+ await publishFinalStream(message, state.lastStreamedText);
402
+ return true;
403
+ }
404
+ // A stalled turn keeps whatever streamed before the hang so the user
405
+ // sees the partial reply next to the stall system note.
406
+ if (classifyErrorImpl(error).isStalled && state.lastStreamedText) {
407
+ await publishFinalStream(message, state.lastStreamedText);
408
+ }
409
+ throw error;
410
+ } finally {
411
+ controlPoller?.stop?.(controlState);
412
+ // Publish on every exit path that may have captured usage (including
413
+ // abort after a partial result). Fire-and-forget: advisory data must
414
+ // not gate the reply below — the comment used to claim this while the
415
+ // awaits made it false.
416
+ void publishContextUsage(message, state, model).catch(() => {});
417
+ void publishPlanUsage(message, state).catch(() => {});
418
+ }
419
+
420
+ if (aborted) {
421
+ await publishFinalStream(message, state.lastStreamedText);
422
+ return true;
423
+ }
424
+
425
+ const result = state.result;
426
+ const responseModel = state.responseModel || model || null;
427
+ if (!result) {
428
+ // The turn ended without a terminal result; surface what streamed, or
429
+ // requeue so a transient hiccup is retried instead of burning the
430
+ // queue entry on a user-visible failure (Claude/Cursor parity).
431
+ const fallbackText = String(state.lastStreamedText || '').trim();
432
+ if (fallbackText) {
433
+ await publishFinalStream(message, fallbackText);
434
+ await publishResponse(message, { text: fallbackText, model: responseModel });
435
+ } else {
436
+ await api('POST', '/api/requeue', { messageId: message.id }).catch(() => {});
437
+ }
438
+ return true;
439
+ }
440
+
441
+ if (result.isError) {
442
+ const classified = classifyErrorImpl(new Error(result.errorMessage || result.stopReason || 'turn error'));
443
+ const errorText = classified.isAuth
444
+ ? `System note: the Grok agent could not authenticate (${classified.message})`
445
+ : String(state.lastStreamedText || result.text || '').trim();
446
+ await publishFinalStream(message, state.lastStreamedText);
447
+ await publishResponse(message, {
448
+ text: errorText,
449
+ model: responseModel,
450
+ terminalError: buildGrokTerminalError(classified, message),
451
+ });
452
+ return true;
453
+ }
454
+
455
+ const finalText = String(result.text || state.lastStreamedText || '').trim();
456
+ if (
457
+ !planBoardPosted
458
+ && String(message.relayMode || '').trim().toLowerCase() === 'plan'
459
+ && countPlanLikeLines(finalText) >= 2
460
+ ) {
461
+ const boardPayload = buildGrokPlanReadyBoardPayload({ message, planText: finalText });
462
+ if (boardPayload) {
463
+ await api('POST', '/api/relay-board', boardPayload).catch(() => {});
464
+ planBoardPosted = true;
465
+ }
466
+ }
467
+ // A terminal, non-error result carrying no prose is a COMPLETED turn, not
468
+ // a failed delivery: requeueing re-billed a deterministically-empty turn
469
+ // on every retry until the cap failed it (Claude/Cursor parity via the
470
+ // shared note).
471
+ const publishedText = finalText || EMPTY_TURN_COMPLETION_NOTE;
472
+ await publishFinalStream(message, publishedText);
473
+ await publishResponse(message, { text: publishedText, model: responseModel });
474
+ return true;
475
+ }
476
+
477
+ async function handlePendingPayload(pending) {
478
+ const message = pending?.message || pending;
479
+ if (!message?.id) return false;
480
+ if (waitingForTurn) {
481
+ dbg('turn already active; rejecting concurrent deliver', message.id);
482
+ return false;
483
+ }
484
+ waitingForTurn = true;
485
+ activeMessage = message;
486
+ try {
487
+ return await runTurn(message);
488
+ } catch (error) {
489
+ const classified = classifyErrorImpl(error);
490
+ dbg('turn failed', classified.code, classified.message);
491
+ // The guidance rides on the response text — there is no separate
492
+ // system-note channel (Claude/Cursor parity).
493
+ const noteText = classified.isAuth
494
+ ? `System note: the Grok agent could not authenticate (${classified.message})`
495
+ : classified.isStalled
496
+ ? `System note: the Grok turn stalled and was stopped (${classified.message}). Send a new message to retry.`
497
+ : `System note: the Grok turn failed (${classified.message}).`;
498
+ await publishResponse(message, {
499
+ text: noteText,
500
+ model: message.providerModel || defaultModel || null,
501
+ terminalError: buildGrokTerminalError(classified, message),
502
+ });
503
+ // Recreate handle after hard failures so the next turn is clean.
504
+ try { await dispose(); } catch { /* ignore */ }
505
+ return true;
506
+ } finally {
507
+ waitingForTurn = false;
508
+ activeMessage = null;
509
+ currentAbortController = null;
510
+ }
511
+ }
512
+
513
+ return {
514
+ handlePendingPayload,
515
+ getActiveQueueMessageId,
516
+ isTurnActive,
517
+ dispose,
518
+ setTurnCeilingMs(value) {
519
+ const ms = Number(value);
520
+ turnCeilingMs = Number.isFinite(ms) && ms >= 0 ? ms : null;
521
+ },
522
+ };
523
+ }