@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,485 @@
1
+ import { sanitizeSubagentRunId } from '../../shared/subagent-run-id.mjs';
2
+ import { capThought } from '../../shared/thought-cap.mjs';
3
+ import { shouldEmitStreamUpdate } from '../../shared/stream-emit-gating.mjs';
4
+
5
+ const MAX_TOOL_DETAIL_LENGTH = 140;
6
+ const SUBAGENT_TOOL_NAMES = new Set(['task', 'agent']);
7
+
8
+ function truncate(text, maxLength = MAX_TOOL_DETAIL_LENGTH) {
9
+ const value = String(text || '').trim();
10
+ if (value.length <= maxLength) return value;
11
+ return `${value.slice(0, Math.max(0, maxLength - 1))}…`;
12
+ }
13
+
14
+ export function isSubagentToolName(name) {
15
+ return SUBAGENT_TOOL_NAMES.has(String(name || '').trim().toLowerCase());
16
+ }
17
+
18
+ export function summarizeToolInput(toolName, input) {
19
+ if (!input || typeof input !== 'object') return '';
20
+ const candidates = [
21
+ input.command,
22
+ input.file_path,
23
+ input.path,
24
+ input.pattern,
25
+ input.description,
26
+ input.url,
27
+ input.query,
28
+ input.prompt,
29
+ ];
30
+ for (const candidate of candidates) {
31
+ const value = String(candidate ?? '').trim();
32
+ if (value) return value;
33
+ }
34
+ try {
35
+ const serialized = JSON.stringify(input);
36
+ return serialized === '{}' ? '' : serialized;
37
+ } catch {
38
+ return '';
39
+ }
40
+ }
41
+
42
+ export function formatToolActivityText(toolName, input) {
43
+ const summary = summarizeToolInput(toolName, input);
44
+ return truncate(summary ? `Tool (${toolName}): ${summary}` : `Tool (${toolName})`);
45
+ }
46
+
47
+ // Re-exported (rather than defined here) so existing importers of this
48
+ // normalizer keep working; the rule itself lives in shared/.
49
+ export { shouldEmitStreamUpdate };
50
+
51
+ /**
52
+ * Stateful normalizer mapping the Cursor adapter's merged event stream onto
53
+ * relay channel actions. One instance per turn.
54
+ *
55
+ * `normalize(event)` consumes `{ source: 'delta', update }` (InteractionUpdate
56
+ * from send's onDelta) and `{ source: 'stream', message }` (SDKMessage from
57
+ * run.stream()) and returns `{ channel, payload }` actions on the same six
58
+ * channels as the Claude worker.
59
+ *
60
+ * Both surfaces can describe the same text/thinking. Ownership latches decide
61
+ * once per turn: the first `text-delta` (resp. `thinking-delta`) makes the
62
+ * delta surface authoritative, and stream-surface `assistant` text blocks
63
+ * (resp. `thinking` messages) are then only kept as a final-text fallback.
64
+ * Without deltas (degraded mode) the stream messages are the emission path.
65
+ * Tool lifecycle is owned by stream `tool_call` messages; the delta surface's
66
+ * tool-call updates are ignored.
67
+ *
68
+ * Subagent attribution rides `tool-call-delta` updates: each carries the task
69
+ * tool_call's `callId` (live-verified identical to the stream `call_id`,
70
+ * 2026-08-16 probe) plus one nested frame (`taskUpdate`) from inside the
71
+ * subagent — text, thinking, and nested tool starts publish under that
72
+ * `subagentRunId`. One nesting level only; deeper levels are dropped by the
73
+ * SDK at convert time.
74
+ */
75
+ export function createSdkMessageNormalizer() {
76
+ let initModel = '';
77
+ let streamText = ''; // text-delta accumulator
78
+ let fallbackText = ''; // assistant-message text accumulator
79
+ let lastEmittedStreamText = '';
80
+ let deltaOwnsText = false;
81
+ let deltaOwnsThinking = false;
82
+ let openThought = null; // { reasoningId, text }
83
+ let stepIndex = 0;
84
+ let thoughtIndex = 0; // monotonic across the turn, so ids never collide
85
+ let lastUsage = null;
86
+ let assistantMessageCount = 0; // degraded-mode stand-in for the step count
87
+ const knownSubagentRuns = new Map(); // call_id -> displayName
88
+ const openSubagentRuns = new Set(); // call_ids currently 'running'
89
+ const seenToolFrames = new Set(); // `${call_id}\u0000${status}` de-dupe
90
+ // Per-subagent nested-delta accumulators (tool-call-delta frames).
91
+ const subagentDeltas = new Map(); // runId -> { streamText, lastEmittedStreamText, openThought, thoughtIndex }
92
+
93
+ function subagentDeltaState(runId) {
94
+ if (!subagentDeltas.has(runId)) {
95
+ subagentDeltas.set(runId, {
96
+ streamText: '',
97
+ lastEmittedStreamText: '',
98
+ openThought: null,
99
+ thoughtIndex: 0,
100
+ });
101
+ }
102
+ return subagentDeltas.get(runId);
103
+ }
104
+
105
+ function allocateThoughtId() {
106
+ const id = `cursor-thought-main-${stepIndex}-${thoughtIndex}`;
107
+ thoughtIndex += 1;
108
+ return id;
109
+ }
110
+
111
+ function closeOpenThought(actions) {
112
+ if (!openThought) return;
113
+ if (openThought.text.trim()) {
114
+ actions.push({
115
+ channel: 'thought',
116
+ payload: {
117
+ reasoningId: openThought.reasoningId,
118
+ text: openThought.text,
119
+ done: true,
120
+ subagentRunId: null,
121
+ },
122
+ });
123
+ }
124
+ openThought = null;
125
+ }
126
+
127
+ function normalizeDelta(update) {
128
+ const actions = [];
129
+ const type = String(update?.type || '');
130
+
131
+ if (type === 'text-delta') {
132
+ deltaOwnsText = true;
133
+ streamText += String(update?.text || '');
134
+ if (shouldEmitStreamUpdate(streamText, lastEmittedStreamText)) {
135
+ lastEmittedStreamText = streamText;
136
+ actions.push({
137
+ channel: 'stream',
138
+ payload: { text: streamText, done: false, subagentRunId: null },
139
+ });
140
+ }
141
+ return actions;
142
+ }
143
+
144
+ if (type === 'thinking-delta') {
145
+ deltaOwnsThinking = true;
146
+ if (!openThought) openThought = { reasoningId: allocateThoughtId(), text: '' };
147
+ openThought.text = capThought(openThought.text + String(update?.text || ''));
148
+ actions.push({
149
+ channel: 'thought',
150
+ payload: {
151
+ reasoningId: openThought.reasoningId,
152
+ text: openThought.text,
153
+ done: false,
154
+ subagentRunId: null,
155
+ },
156
+ });
157
+ return actions;
158
+ }
159
+
160
+ if (type === 'thinking-completed') {
161
+ closeOpenThought(actions);
162
+ return actions;
163
+ }
164
+
165
+ if (type === 'step-started') {
166
+ stepIndex += 1;
167
+ return actions;
168
+ }
169
+
170
+ if (type === 'step-completed') {
171
+ // Steps can end without an explicit thinking-completed; never leave a
172
+ // thought dangling in "streaming" state across a step boundary.
173
+ closeOpenThought(actions);
174
+ return actions;
175
+ }
176
+
177
+ if (type === 'turn-ended') {
178
+ if (update?.usage && typeof update.usage === 'object') lastUsage = update.usage;
179
+ return actions;
180
+ }
181
+
182
+ if (type === 'tool-call-delta') {
183
+ // Nested interaction updates streamed from a task/subagent tool call.
184
+ // `callId` is the task tool_call's own id — live-verified byte-equal on
185
+ // 2026-08-16 (probe: tool_f68aee7f… on both surfaces) — which is
186
+ // exactly the subagentRunId the lifecycle chips already use, so these
187
+ // frames fill the bubbles that used to render header-and-nothing.
188
+ const runId = sanitizeSubagentRunId(update?.callId);
189
+ const nested = update?.taskUpdate && typeof update.taskUpdate === 'object' ? update.taskUpdate : null;
190
+ if (!runId || !nested) return actions;
191
+ const nestedType = String(nested.type || '');
192
+ const state = subagentDeltaState(runId);
193
+ if (nestedType === 'text-delta') {
194
+ state.streamText += String(nested.text || '');
195
+ if (shouldEmitStreamUpdate(state.streamText, state.lastEmittedStreamText)) {
196
+ state.lastEmittedStreamText = state.streamText;
197
+ actions.push({
198
+ channel: 'stream',
199
+ payload: { text: state.streamText, done: false, subagentRunId: runId },
200
+ });
201
+ }
202
+ return actions;
203
+ }
204
+ if (nestedType === 'thinking-delta') {
205
+ if (!state.openThought) {
206
+ state.openThought = { reasoningId: `cursor-thought-${runId}-${state.thoughtIndex++}`, text: '' };
207
+ }
208
+ state.openThought.text = capThought(state.openThought.text + String(nested.text || ''));
209
+ actions.push({
210
+ channel: 'thought',
211
+ payload: {
212
+ reasoningId: state.openThought.reasoningId,
213
+ text: state.openThought.text,
214
+ done: false,
215
+ subagentRunId: runId,
216
+ },
217
+ });
218
+ return actions;
219
+ }
220
+ if (nestedType === 'thinking-completed' || nestedType === 'step-completed') {
221
+ if (state.openThought && state.openThought.text.trim()) {
222
+ actions.push({
223
+ channel: 'thought',
224
+ payload: {
225
+ reasoningId: state.openThought.reasoningId,
226
+ text: state.openThought.text,
227
+ done: true,
228
+ subagentRunId: runId,
229
+ },
230
+ });
231
+ }
232
+ if (state.openThought) state.openThought = null;
233
+ // A finished subagent step flushes its streamed text as final so the
234
+ // bubble keeps the prose even though the run's own turn never emits a
235
+ // top-level stream terminator for it.
236
+ if (nestedType === 'step-completed' && state.streamText.trim() && state.streamText !== state.lastEmittedStreamText) {
237
+ state.lastEmittedStreamText = state.streamText;
238
+ actions.push({
239
+ channel: 'stream',
240
+ payload: { text: state.streamText, done: false, subagentRunId: runId },
241
+ });
242
+ }
243
+ return actions;
244
+ }
245
+ if (nestedType === 'tool-call-started' || nestedType === 'tool-call-completed') {
246
+ const nestedName = String(nested.name || nested.toolName || nested.title || 'tool').trim() || 'tool';
247
+ if (nestedType === 'tool-call-started') {
248
+ actions.push({
249
+ channel: 'activity',
250
+ payload: { text: formatToolActivityText(nestedName, nested.args || nested.input || null), subagentRunId: runId },
251
+ });
252
+ }
253
+ return actions;
254
+ }
255
+ return actions;
256
+ }
257
+
258
+ // tool-call-started/-completed, partial-tool-call, token-delta,
259
+ // summary*, user-message-appended, shell-output-delta: intentionally ignored.
260
+ return actions;
261
+ }
262
+
263
+ function actionsForToolCall(message) {
264
+ const actions = [];
265
+ // call_id becomes a relay row id; malformed ones (embedded newline, two
266
+ // concatenated ids) have been seen live from this SDK.
267
+ const callId = sanitizeSubagentRunId(message?.call_id) || '';
268
+ const status = String(message?.status || '').trim();
269
+ if (!callId || !status) return actions;
270
+ const frameKey = `${callId}\u0000${status}`;
271
+ if (seenToolFrames.has(frameKey)) return actions;
272
+ seenToolFrames.add(frameKey);
273
+
274
+ const toolName = String(message?.name || '').trim() || 'unknown';
275
+ const args = message?.args && typeof message.args === 'object' ? message.args : {};
276
+
277
+ if (status === 'running') {
278
+ if (isSubagentToolName(toolName)) {
279
+ const displayName = String(
280
+ args.description
281
+ || args.name
282
+ || args.subagent_type
283
+ || 'Subagent',
284
+ ).trim() || 'Subagent';
285
+ knownSubagentRuns.set(callId, displayName);
286
+ openSubagentRuns.add(callId);
287
+ actions.push({
288
+ channel: 'subagent',
289
+ payload: {
290
+ subagentRunId: callId,
291
+ parentSubagentId: null,
292
+ displayName,
293
+ status: 'running',
294
+ },
295
+ });
296
+ }
297
+ actions.push({
298
+ channel: 'activity',
299
+ payload: { text: formatToolActivityText(toolName, args), subagentRunId: null },
300
+ });
301
+ return actions;
302
+ }
303
+
304
+ if (status === 'completed') {
305
+ if (knownSubagentRuns.has(callId)) {
306
+ openSubagentRuns.delete(callId);
307
+ actions.push({
308
+ channel: 'subagent',
309
+ payload: {
310
+ subagentRunId: callId,
311
+ parentSubagentId: null,
312
+ displayName: knownSubagentRuns.get(callId),
313
+ status: 'completed',
314
+ },
315
+ });
316
+ }
317
+ return actions;
318
+ }
319
+
320
+ if (status === 'error') {
321
+ if (knownSubagentRuns.has(callId)) {
322
+ openSubagentRuns.delete(callId);
323
+ actions.push({
324
+ channel: 'subagent',
325
+ payload: {
326
+ subagentRunId: callId,
327
+ parentSubagentId: null,
328
+ displayName: knownSubagentRuns.get(callId),
329
+ status: 'failed',
330
+ },
331
+ });
332
+ }
333
+ const resultText = typeof message?.result === 'string'
334
+ ? message.result
335
+ : String(message?.result?.text ?? '');
336
+ actions.push({
337
+ channel: 'activity',
338
+ payload: {
339
+ text: truncate(`Tool failed: ${resultText.trim() || 'unknown error'}`),
340
+ subagentRunId: null,
341
+ },
342
+ });
343
+ return actions;
344
+ }
345
+
346
+ return actions;
347
+ }
348
+
349
+ function normalizeMessage(message) {
350
+ const actions = [];
351
+ const type = String(message?.type || '');
352
+
353
+ if (type === 'system' && message?.subtype === 'init') {
354
+ // `model` is a ModelSelection object ({id, params?}) in current SDK
355
+ // builds; older shapes and test fixtures carry a plain string.
356
+ initModel = String(message.model?.id ?? message.model ?? '').trim();
357
+ actions.push({
358
+ channel: 'init',
359
+ payload: {
360
+ sessionId: String(message.session_id ?? message.sessionId ?? '').trim(),
361
+ model: initModel,
362
+ },
363
+ });
364
+ return actions;
365
+ }
366
+
367
+ if (type === 'assistant') {
368
+ assistantMessageCount += 1;
369
+ const content = Array.isArray(message?.message?.content) ? message.message.content : [];
370
+ for (const block of content) {
371
+ if (block?.type !== 'text') continue; // tool_use lifecycle is owned by tool_call messages
372
+ const text = String(block?.text || '');
373
+ if (!text) continue;
374
+ fallbackText += text;
375
+ if (!deltaOwnsText && shouldEmitStreamUpdate(fallbackText, lastEmittedStreamText)) {
376
+ lastEmittedStreamText = fallbackText;
377
+ actions.push({
378
+ channel: 'stream',
379
+ payload: { text: fallbackText, done: false, subagentRunId: null },
380
+ });
381
+ }
382
+ }
383
+ return actions;
384
+ }
385
+
386
+ if (type === 'thinking') {
387
+ if (deltaOwnsThinking) return actions;
388
+ const text = capThought(String(message?.text || ''));
389
+ if (!text.trim()) return actions;
390
+ actions.push({
391
+ channel: 'thought',
392
+ payload: {
393
+ reasoningId: allocateThoughtId(),
394
+ text,
395
+ done: true,
396
+ subagentRunId: null,
397
+ },
398
+ });
399
+ return actions;
400
+ }
401
+
402
+ if (type === 'tool_call') {
403
+ return actionsForToolCall(message);
404
+ }
405
+
406
+ if (type === 'task') {
407
+ const status = String(message?.status || '').trim() || 'unknown';
408
+ return [{
409
+ channel: 'activity',
410
+ payload: {
411
+ text: truncate(`Background task ${status}: ${String(message?.text || '').trim()}`),
412
+ subagentRunId: null,
413
+ },
414
+ }];
415
+ }
416
+
417
+ if (type === 'usage') {
418
+ if (message?.usage && typeof message.usage === 'object') lastUsage = message.usage;
419
+ return actions;
420
+ }
421
+
422
+ if (type === 'status') {
423
+ const status = String(message?.status || '').trim().toUpperCase();
424
+ const terminal = {
425
+ FINISHED: { isError: false, subtype: 'finished' },
426
+ ERROR: { isError: true, subtype: 'error' },
427
+ EXPIRED: { isError: true, subtype: 'expired' },
428
+ CANCELLED: { isError: false, subtype: 'cancelled' },
429
+ }[status];
430
+ if (!terminal) return actions; // CREATING / RUNNING
431
+ // Errored turns may end before any text streamed; the status message is
432
+ // then the only human-readable explanation, so surface it as the result.
433
+ const errorMessage = terminal.isError ? String(message?.message || '').trim() : '';
434
+ const text = finalStreamText() || errorMessage;
435
+ actions.push({
436
+ channel: 'result',
437
+ payload: {
438
+ text,
439
+ isError: terminal.isError,
440
+ subtype: terminal.subtype,
441
+ // Kept separate from text (which streamed prose can shadow) so the
442
+ // runner can classify the failure from the status message itself.
443
+ errorMessage: errorMessage || null,
444
+ usage: lastUsage,
445
+ totalCostUsd: null,
446
+ },
447
+ });
448
+ return actions;
449
+ }
450
+
451
+ // user, request: intentionally ignored.
452
+ return actions;
453
+ }
454
+
455
+ function normalize(event) {
456
+ if (!event || typeof event !== 'object') return [];
457
+ if (event.source === 'delta') return normalizeDelta(event.update);
458
+ if (event.source === 'stream') return normalizeMessage(event.message);
459
+ return [];
460
+ }
461
+
462
+ function finalStreamText() {
463
+ return streamText || fallbackText;
464
+ }
465
+
466
+ return {
467
+ normalize,
468
+ finalStreamText,
469
+ // Subagent runs opened but not yet terminated — the auth-retry path
470
+ // closes them out before discarding this normalizer, otherwise their
471
+ // relay rows stay 'running' forever.
472
+ activeSubagentRuns: () => [...openSubagentRuns].map((callId) => ({
473
+ subagentRunId: callId,
474
+ displayName: knownSubagentRuns.get(callId) || 'Subagent',
475
+ })),
476
+ get model() { return initModel; },
477
+ // The turn's usage as last seen on either surface, so the runner can still
478
+ // publish context data when the run ends without a terminal status message.
479
+ get lastUsage() { return lastUsage; },
480
+ // Model calls made this turn: step boundaries when the delta surface is
481
+ // live, assistant messages in degraded mode. Divides the turn's aggregate
482
+ // usage into a per-call context estimate (see cursor-context-usage.mjs).
483
+ get modelCallCount() { return Math.max(stepIndex, assistantMessageCount, 1); },
484
+ };
485
+ }