@openchambery/web 1.19.7-beta.9 → 1.19.7

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 (201) hide show
  1. package/dist/assets/AgentAvatar-CX0a1rKE.js +1 -0
  2. package/dist/assets/AssistantView-nVF0kdFh.js +2 -0
  3. package/dist/assets/DiagramView-B2UtQ8mw.js +1 -0
  4. package/dist/assets/FadeInOnReveal-CK4asqVR.js +1 -0
  5. package/dist/assets/Icon-BYM02QyU.js +1 -0
  6. package/dist/assets/MarkdownLoadingSkeleton-DWs4Jg7b.js +3 -0
  7. package/dist/assets/MarkdownRenderer-DT_GN1Ex.js +2 -0
  8. package/dist/assets/MarkdownRendererImpl-Cslh4XN_.js +7 -0
  9. package/dist/assets/MarkstreamRendererImpl-CsNwy1T5.css +1 -0
  10. package/dist/assets/MarkstreamRendererImpl-Ymm12x6J.js +1 -0
  11. package/dist/assets/MobileDetailNavigation-DD29ENgU.js +1 -0
  12. package/dist/assets/MobileShareBridge-CrTe_QND.js +4 -0
  13. package/dist/assets/MobileSurface-DjxchpUC.js +2 -0
  14. package/dist/assets/{MultiRunWindow-Ohj6UbWj.js → MultiRunWindow-jG5dCEMH.js} +1 -1
  15. package/dist/assets/OnboardingScreen-C3-wzeSX.js +1 -0
  16. package/dist/assets/OverlayScrollbar-KUpvYHdv.js +2 -0
  17. package/dist/assets/SettingsView-UKlEewdq.js +25 -0
  18. package/dist/assets/{SettingsWindow-B4lzhAbO.js → SettingsWindow-DO1aAplT.js} +1 -1
  19. package/dist/assets/TerminalView-BG1UPLsz.js +8 -0
  20. package/dist/assets/ToolOutputDialog-CWr5eVTK.js +10 -0
  21. package/dist/assets/appThemeRegistry-BI5drVpC.js +1 -0
  22. package/dist/assets/{button-z0GOBtWi.js → button-C0cth9tl.js} +1 -1
  23. package/dist/assets/clipboard-DJ4nQEuY.js +1 -0
  24. package/dist/assets/{collapsible-D9zJRpeB.js → collapsible-DFT9cE80.js} +1 -1
  25. package/dist/assets/context-menu-CAp74NfV.js +1 -0
  26. package/dist/assets/desktopHosts-CWxex1c3.js +1 -0
  27. package/dist/assets/desktopRelayRestore-9EbBYQ9s.js +1 -0
  28. package/dist/assets/{diagram-CoEqmReY.js → diagram-BryzMuta.js} +1 -1
  29. package/dist/assets/{dialog-x2hirWBd.js → dialog-CgbV5ljg.js} +1 -1
  30. package/dist/assets/dropdown-menu.styles-Bzo6L3FH.js +1 -0
  31. package/dist/assets/{es-C8Sqjsia.js → es-BHqnSjoZ.js} +1 -1
  32. package/dist/assets/fileQueries-BY-9OWzp.js +1 -0
  33. package/dist/assets/fileReferenceActions-BJJtX_S5.js +1 -0
  34. package/dist/assets/{fr-p-Zwm_j2.js → fr-BfrlJY9i.js} +1 -1
  35. package/dist/assets/{gitApi-CtS4kWoo.js → gitApi-D9hFlE3T.js} +3 -3
  36. package/dist/assets/{input-BG3VWTIF.js → input-DFLpqjGR.js} +1 -1
  37. package/dist/assets/insertionBoundaries-D2L7D0Oe.js +4 -0
  38. package/dist/assets/{ja-B6REu1qP.js → ja-DP-7qt-x.js} +1 -1
  39. package/dist/assets/{ko-CJBNH9IZ.js → ko-E2B5vGQP.js} +1 -1
  40. package/dist/assets/main-BRedOs_V.js +169 -0
  41. package/dist/assets/main-DlXpJ0Vw.js +2 -0
  42. package/dist/assets/markdown-worker-BuveaH3O.js +1 -0
  43. package/dist/assets/markdownTheme-CzZm3riB.js +2 -0
  44. package/dist/assets/miniChat-CBLbFpfy.js +2 -0
  45. package/dist/assets/mobile-CoZsWdWk.js +2 -0
  46. package/dist/assets/multirun-BF-I0r2_.js +4 -0
  47. package/dist/assets/outsideFileGrants-DnplRxr-.js +1 -0
  48. package/dist/assets/path-utils-jGozTg9d.js +1 -0
  49. package/dist/assets/persistence-DdUac14I.js +3 -0
  50. package/dist/assets/{pl-B2cBI_7K.js → pl-r8X5gRM5.js} +1 -1
  51. package/dist/assets/platform-B4KKLnZ5.js +2 -0
  52. package/dist/assets/projectMeta-2t4L5_4a.js +3 -0
  53. package/dist/assets/{pt-BR-CrCc9hmt.js → pt-BR-DcpdXCf5.js} +1 -1
  54. package/dist/assets/renderElectronMiniChatApp-fl7O3iHy.js +2 -0
  55. package/dist/assets/renderMobileApp-CKYDwf4U.js +28 -0
  56. package/dist/assets/runtimeConfig-B4eb4ZiU.js +2 -0
  57. package/dist/assets/runtimeEndpointReset-BHuTYyzc.js +147 -0
  58. package/dist/assets/sessionChangeSummary-DOq8EN_H.js +1 -0
  59. package/dist/assets/sessionLookup-DNc0ZeR3.js +1 -0
  60. package/dist/assets/sortable-tabs-strip-C8_9kHqu.js +1 -0
  61. package/dist/assets/src-DasvfkEg.css +2 -0
  62. package/dist/assets/{store-CL3WP48T.js → store-BDFdnk6l.js} +2 -2
  63. package/dist/assets/terminalApi-947l7Ra3.js +1 -0
  64. package/dist/assets/textarea-Cgo_nWUU.js +1 -0
  65. package/dist/assets/toolHelpers-DEvVP9U0.js +2 -0
  66. package/dist/assets/{tooltip-CtH61mQh.js → tooltip-BifLJ8o0.js} +1 -1
  67. package/dist/assets/{uk-C8YKAz0x.js → uk-Di0vl9V-.js} +1 -1
  68. package/dist/assets/url-Pk6tQ1EZ.js +1 -0
  69. package/dist/assets/{ChatContainer-CDExXwgn.js → useAppFontEffects-Dle8xs5h.js} +44 -39
  70. package/dist/assets/useAssistantUIStore-me6s63b2.js +1 -0
  71. package/dist/assets/useDesktopWindowControlsLayout-_QAwKcQn.js +1 -0
  72. package/dist/assets/useEffectiveDirectory-BSyuAgz6.js +1 -0
  73. package/dist/assets/useFontPreferences-BCOBlaAd.js +1 -0
  74. package/dist/assets/useMobileNavigationStore-f8xHJk7C.js +413 -0
  75. package/dist/assets/useSessionAutoCleanup-BHiXaW0J.js +1 -0
  76. package/dist/assets/useUIStore-CuaA8-Hp.js +1 -0
  77. package/dist/assets/useWorkerHighlightedLines-DuvXTSm_.js +7 -0
  78. package/dist/assets/utils-BiK3tBGM.js +1 -0
  79. package/dist/assets/{vendor-markstream-react-CwvbEg9R.js → vendor-markstream-react-C3K8YH0d.js} +4 -4
  80. package/dist/assets/{zh-CN-CCYwUnmB.js → zh-CN-coFCfeQE.js} +1 -1
  81. package/dist/assets/{zh-TW-D8Y6py08.js → zh-TW-oDRZ1-yb.js} +1 -1
  82. package/dist/index.html +9 -11
  83. package/dist/mini-chat.html +9 -11
  84. package/dist/mobile.html +15 -17
  85. package/package.json +2 -1
  86. package/server/index.js +2 -0
  87. package/server/lib/assistant-delivery-parts.js +3 -1
  88. package/server/lib/assistant-delivery-parts.test.js +11 -0
  89. package/server/lib/assistants/DOCUMENTATION.md +76 -3
  90. package/server/lib/assistants/assign.js +225 -0
  91. package/server/lib/assistants/assign.test.js +118 -0
  92. package/server/lib/assistants/bubbles.js +45 -0
  93. package/server/lib/assistants/bubbles.test.js +36 -0
  94. package/server/lib/assistants/cards.js +148 -0
  95. package/server/lib/assistants/cards.test.js +116 -0
  96. package/server/lib/assistants/contact-store.js +284 -0
  97. package/server/lib/assistants/contact-store.test.js +131 -0
  98. package/server/lib/assistants/contact-tools.js +761 -0
  99. package/server/lib/assistants/contact-tools.test.js +328 -0
  100. package/server/lib/assistants/contracts.js +20 -1
  101. package/server/lib/assistants/harness.js +551 -0
  102. package/server/lib/assistants/harness.test.js +607 -0
  103. package/server/lib/assistants/routes.js +44 -4
  104. package/server/lib/assistants/service.js +930 -17
  105. package/server/lib/assistants/service.test.js +1163 -45
  106. package/server/lib/event-stream/DOCUMENTATION.md +12 -1
  107. package/server/lib/event-stream/directory-ws-bridge.js +11 -2
  108. package/server/lib/event-stream/global-ws-bridge.js +19 -3
  109. package/server/lib/event-stream/protocol.js +11 -1
  110. package/server/lib/event-stream/reasoning-projection.js +425 -0
  111. package/server/lib/event-stream/reasoning-projection.test.js +300 -0
  112. package/server/lib/event-stream/runtime.js +10 -0
  113. package/server/lib/event-stream/runtime.test.js +53 -0
  114. package/server/lib/llm/DOCUMENTATION.md +84 -0
  115. package/server/lib/llm/catalog.js +100 -0
  116. package/server/lib/llm/catalog.test.js +78 -0
  117. package/server/lib/llm/completions.js +151 -0
  118. package/server/lib/llm/completions.test.js +127 -0
  119. package/server/lib/llm/generate.js +570 -0
  120. package/server/lib/llm/generate.test.js +459 -0
  121. package/server/lib/llm/routes.js +47 -0
  122. package/server/lib/llm/temp-directory.js +37 -0
  123. package/server/lib/notifications/APNS.md +19 -13
  124. package/server/lib/notifications/DOCUMENTATION.md +4 -3
  125. package/server/lib/notifications/runtime.js +148 -37
  126. package/server/lib/notifications/runtime.test.js +100 -0
  127. package/server/lib/opencode/DOCUMENTATION.md +2 -1
  128. package/server/lib/opencode/feature-routes-runtime.js +30 -0
  129. package/server/lib/opencode/feature-routes-runtime.test.js +20 -0
  130. package/server/lib/opencode/proxy.js +103 -9
  131. package/server/lib/opencode/proxy.test.js +177 -1
  132. package/server/lib/opencode/settings-runtime.js +2 -2
  133. package/server/lib/relay/reasoning-projection.e2e.test.ts +529 -0
  134. package/server/lib/session-goal/DOCUMENTATION.md +8 -1
  135. package/server/lib/session-goal/runtime.js +20 -0
  136. package/server/lib/session-index/DOCUMENTATION.md +3 -1
  137. package/server/lib/session-index/service.js +10 -4
  138. package/server/lib/session-index/service.test.js +37 -0
  139. package/server/lib/session-title/runtime.js +1 -0
  140. package/server/lib/session-title/runtime.test.js +11 -0
  141. package/server/lib/session-turn-pages/DOCUMENTATION.md +2 -1
  142. package/server/lib/session-turn-pages/routes.js +22 -3
  143. package/server/lib/session-turn-pages/routes.test.js +43 -0
  144. package/server/lib/transcript-cache/DOCUMENTATION.md +2 -2
  145. package/server/lib/transcript-cache/routes.js +9 -2
  146. package/server/lib/transcript-cache/routes.test.js +32 -0
  147. package/dist/assets/AgentAvatar-Di8F4QJh.js +0 -1
  148. package/dist/assets/AssistantView-DSxVAAb_.js +0 -1
  149. package/dist/assets/ContextPanel-DgwEIdqR.js +0 -285
  150. package/dist/assets/DiagramView-BKQO61y8.js +0 -1
  151. package/dist/assets/FadeInOnReveal-CV2s6YN8.js +0 -1
  152. package/dist/assets/Icon-CLpn1mIz.js +0 -1
  153. package/dist/assets/MarkdownRendererImpl-CTgmghSh.js +0 -7
  154. package/dist/assets/MarkstreamRendererImpl-BKsc-avF.js +0 -1
  155. package/dist/assets/MarkstreamRendererImpl-CxeWpDCK.css +0 -1
  156. package/dist/assets/MobileDetailNavigation-BcA6rd8Q.js +0 -1
  157. package/dist/assets/MobileShareBridge-w6T2ZrsW.js +0 -4
  158. package/dist/assets/MobileSurface-CG1n7v0h.js +0 -2
  159. package/dist/assets/OnboardingScreen-D6rtE7rj.js +0 -1
  160. package/dist/assets/OverlayScrollbar-DlNZho3q.js +0 -2
  161. package/dist/assets/SettingsView-CiF3r83e.js +0 -25
  162. package/dist/assets/TerminalView-Ctx_Jt_b.js +0 -8
  163. package/dist/assets/ToolOutputDialog-Dv9_fUYl.js +0 -10
  164. package/dist/assets/appThemeRegistry-DnwUIQB9.js +0 -2
  165. package/dist/assets/clipboard-D35XSwID.js +0 -1
  166. package/dist/assets/desktop-CfLlmafK.js +0 -1
  167. package/dist/assets/desktopHosts-C1Ns3DUu.js +0 -1
  168. package/dist/assets/desktopRelayRestore-8pvI_dSc.js +0 -1
  169. package/dist/assets/fileQueries-DGHZqZHc.js +0 -1
  170. package/dist/assets/insertionBoundaries-hK4mQj4W.js +0 -4
  171. package/dist/assets/main-BmbZFoO4.js +0 -29
  172. package/dist/assets/main-BwNhn8ml.js +0 -2
  173. package/dist/assets/markdown-worker-y3ewaqAK.js +0 -4
  174. package/dist/assets/markdownTheme-SPe4jOwX.js +0 -2
  175. package/dist/assets/miniChat-BN89siC3.js +0 -2
  176. package/dist/assets/mobile-DE9rVo5D.js +0 -2
  177. package/dist/assets/multirun-BiEuyDq3.js +0 -4
  178. package/dist/assets/persistence-w-AiX2i4.js +0 -3
  179. package/dist/assets/platform-4eCmsdol.js +0 -2
  180. package/dist/assets/projectMeta-DSLKcH1a.js +0 -3
  181. package/dist/assets/radio-HD5AB2Qf.js +0 -1
  182. package/dist/assets/renderElectronMiniChatApp-f_UxpFjv.js +0 -2
  183. package/dist/assets/renderMobileApp-BAhCai6d.js +0 -28
  184. package/dist/assets/runtime-switch-BkpTTbEi.js +0 -1
  185. package/dist/assets/runtimeConfig-qG_oNBoa.js +0 -2
  186. package/dist/assets/runtimeEndpointReset-BDeKD3EM.js +0 -4
  187. package/dist/assets/shikiHighlight-BphLZnac.js +0 -1
  188. package/dist/assets/sortable-tabs-strip-CNDgyX0B.js +0 -1
  189. package/dist/assets/src-DZt1nWf5.css +0 -2
  190. package/dist/assets/terminalApi-CGtOtc8I.js +0 -1
  191. package/dist/assets/url-KeihbE0d.js +0 -1
  192. package/dist/assets/useAppFontEffects--0dwVTSm.js +0 -7
  193. package/dist/assets/useAssistantUIStore-I0VWrB5V.js +0 -1
  194. package/dist/assets/useDesktopWindowControlsLayout-DsbeBLvv.js +0 -1
  195. package/dist/assets/useEffectiveDirectory-CHvvohcG.js +0 -1
  196. package/dist/assets/useFontPreferences-BLZ3y6mN.js +0 -1
  197. package/dist/assets/useMobileNavigationStore-peTDuC00.js +0 -412
  198. package/dist/assets/useSessionAutoCleanup-D_ZC9E5g.js +0 -1
  199. package/dist/assets/useUIStore-CNLmCoPU.js +0 -1
  200. package/dist/assets/useWorkerHighlightedLines-B1YzuMYR.js +0 -7
  201. package/dist/assets/utils-CTL16t2f.js +0 -1
@@ -10,9 +10,11 @@ This module contains the OpenChamber message-stream WebSocket protocol and runti
10
10
  - `packages/web/server/lib/event-stream/directory-ws-bridge.js`: browser-facing per-directory WS bridge that owns one scoped upstream reader per connection.
11
11
  - `packages/web/server/lib/event-stream/protocol.js`: path constants, SSE envelope parsing, and WebSocket frame serialization helpers.
12
12
  - `packages/web/server/lib/event-stream/diff-summary.js`: pure outbound FileDiff helpers. L1 message/session `summary.diffs` keep a slim file list `{ file, status?, additions, deletions }` plus additive `diffCount`/`hasDiffs` (never patch/before/after/from/to). `session.diff` event frames still keep file/status/additions/deletions and drop patch bodies. `summarizeFileDiff(s)` remains for explicit L2-style file lists.
13
+ - `packages/web/server/lib/event-stream/reasoning-projection.js`: OpenChamber outbound reasoning projection (`includeReasoning=false`). Shared pure snapshot helpers + per-connection stateful stream filter for HTTP parts, WS send/replay, and SSE proxy parse-filter paths. Never mutates hub/replay objects; never forwards the query param to OpenCode.
13
14
  - `packages/web/server/lib/event-stream/upstream-reader.js`: reusable upstream SSE reader with event-id tracking, stall recovery, and reconnect handling.
14
15
  - `packages/web/server/lib/event-stream/runtime.js`: thin WebSocket server runtime for upgrade handling and path dispatch to the global/directory bridges.
15
16
  - `packages/web/server/lib/event-stream/protocol.test.js`: unit tests for protocol helpers.
17
+ - `packages/web/server/lib/event-stream/reasoning-projection.test.js`: unit + 10k-delta performance guards for reasoning projection.
16
18
  - `packages/web/server/lib/event-stream/upstream-reader.test.js`: unit tests for upstream SSE reader behavior.
17
19
  - `packages/web/server/lib/event-stream/runtime.test.js`: unit tests for runtime-side broadcaster behavior.
18
20
 
@@ -26,7 +28,14 @@ The following APIs are exported by their owning modules. `event-stream/index.js`
26
28
  - `MESSAGE_STREAM_WS_HEARTBEAT_INTERVAL_MS`: heartbeat interval for browser-facing WS connections.
27
29
  - `parseSseEventEnvelope(block)`: parses an SSE block into `{ eventId, directory, payload }`.
28
30
  - `sendMessageStreamWsFrame(socket, payload)`: serializes and sends a JSON WS frame.
29
- - `sendMessageStreamWsEvent(socket, payload, options)`: sends an event frame with optional `eventId` and `directory`. Outbound `session.diff` keeps file/status/additions/deletions only (no full patch bodies). Message/session `summary.diffs` are L1-projected to a slim file list plus additive `diffCount`/`hasDiffs` (patch bodies never leave the host).
31
+ - `sendMessageStreamWsEvent(socket, payload, options)`: sends an event frame with optional `eventId`, `directory`, and per-connection `reasoningFilter`. Outbound `session.diff` keeps file/status/additions/deletions only (no full patch bodies). Message/session `summary.diffs` are L1-projected to a slim file list plus additive `diffCount`/`hasDiffs` (patch bodies never leave the host). When `reasoningFilter.projectEvent` returns `null`, the frame is dropped (still counts as a successful send so slow-client tracking is unchanged).
32
+
33
+ ### Reasoning projection helpers (`reasoning-projection.js`)
34
+ Import from the module file (not the event-stream barrel). Public surface:
35
+ - `shouldIncludeReasoning` / `readIncludeReasoningQuery` / `readIncludeReasoningFromUrl` / `stripIncludeReasoningParam`
36
+ - `projectMessagesPayloadForReasoning` / `projectMessagesPayloadWithoutReasoning` — array / `{ records }` / `{ record }` / `{ parts }` only
37
+ - `createReasoningOutboundFilter` — per-connection stream filter (version-suffix-aware type match; preserves original `type` on kept events)
38
+ - `filterSseBlock` / `createSseBlockSplitter` — SSE disabled path (CR-safe chunk boundaries)
30
39
 
31
40
  ### Runtime helpers
32
41
  - `createGlobalMessageStreamHub(...)`: creates a shared `/global/event` upstream SSE hub with event/status subscribers and bounded event-id replay.
@@ -45,6 +54,7 @@ The following APIs are exported by their owning modules. `event-stream/index.js`
45
54
  - The web server creates one shared global message-stream hub. OpenCode watcher side effects and global WS clients subscribe to that hub, so there is one upstream `/global/event` SSE reader for both server-side processing and browser fan-out.
46
55
  - The global hub keeps a bounded replay buffer keyed by SSE `eventId` so reconnecting browser clients can receive buffered events after their requested `Last-Event-ID`.
47
56
  - Outbound FileDiff payloads are summarized before hub fan-out/replay and again at WS send, so Relay never carries full patch frames larger than 64KB.
57
+ - **Reasoning projection (`includeReasoning=false`)**: per HTTP request or WS/SSE connection. The shared hub and internal subscribers always see full upstream events. Filtering runs only at the browser send boundary (live fan-out **and** replay) and on the direct SSE proxy parse path. HTTP turn-page / reconcile / exact message / official `session.messages` list / transcript-cache reads apply the same part strip. `tokens.reasoning` totals stay. The query param is OpenChamber-only and is stripped before OpenCode upstream calls.
48
58
  - Global WS reconnect protocol is **replay events → ready barrier**. On initial connect and on each first-ready for a client, the bridge synchronously sends any `replayAfter(lastEventId)` frames first, then emits `{ type: 'ready', scope: 'global' }`. Clients must not start HTTP compensation until they observe ready, so buffered history can merge first.
49
59
  - Directory WS clients still attach one upstream `/event?directory=...` SSE reader per connection because directory streams are scoped.
50
60
  - If an upstream SSE connection delays response headers or an attached stream stalls, the reader aborts that upstream fetch and reconnects upstream with `Last-Event-ID`, keeping the browser WS alive when recovery is fast.
@@ -66,6 +76,7 @@ The following APIs are exported by their owning modules. `event-stream/index.js`
66
76
 
67
77
  ## Testing
68
78
  - Run `vitest run packages/web/server/lib/event-stream/protocol.test.js`
79
+ - Run `vitest run packages/web/server/lib/event-stream/reasoning-projection.test.js`
69
80
  - Run `vitest run packages/web/server/lib/event-stream/upstream-reader.test.js`
70
81
  - Run `vitest run packages/web/server/lib/event-stream/runtime.test.js`
71
82
  - Run repo validation before finalizing: `bun run type-check`, `bun run lint`, `bun run build`
@@ -17,6 +17,7 @@ export function acceptDirectoryMessageStreamWsConnection({
17
17
  socket,
18
18
  requestedLastEventId,
19
19
  requestedDirectory,
20
+ reasoningFilter = null,
20
21
  buildOpenCodeUrl,
21
22
  getOpenCodeAuthHeaders,
22
23
  processForwardedEventPayload,
@@ -37,6 +38,7 @@ export function acceptDirectoryMessageStreamWsConnection({
37
38
  controller.abort();
38
39
  }
39
40
  reader?.stop();
41
+ reasoningFilter?.dispose?.();
40
42
  wsClients.delete(socket);
41
43
  };
42
44
 
@@ -56,7 +58,10 @@ export function acceptDirectoryMessageStreamWsConnection({
56
58
  return;
57
59
  }
58
60
 
59
- sendMessageStreamWsEvent(socket, { type: 'openchamber:heartbeat', timestamp: Date.now() }, { directory: 'global' });
61
+ sendMessageStreamWsEvent(socket, { type: 'openchamber:heartbeat', timestamp: Date.now() }, {
62
+ directory: 'global',
63
+ reasoningFilter,
64
+ });
60
65
  }, heartbeatIntervalMs);
61
66
 
62
67
  socket.on('close', () => {
@@ -77,10 +82,14 @@ export function acceptDirectoryMessageStreamWsConnection({
77
82
  sendMessageStreamWsEvent(socket, payload, {
78
83
  directory,
79
84
  eventId: typeof envelope?.eventId === 'string' && envelope.eventId.length > 0 ? envelope.eventId : undefined,
85
+ reasoningFilter,
80
86
  });
81
87
 
82
88
  processForwardedEventPayload(payload, (syntheticPayload) => {
83
- sendMessageStreamWsEvent(socket, syntheticPayload, { directory: 'global' });
89
+ sendMessageStreamWsEvent(socket, syntheticPayload, {
90
+ directory: 'global',
91
+ reasoningFilter,
92
+ });
84
93
  });
85
94
  };
86
95
 
@@ -22,9 +22,13 @@ export function createGlobalMessageStreamWsBridge({
22
22
  }) {
23
23
  const clients = new Set();
24
24
  const clientLastEventIds = new Map();
25
+ const clientReasoningFilters = new Map();
25
26
  const readyClients = new Set();
26
27
 
27
28
  const removeClient = (socket) => {
29
+ const filter = clientReasoningFilters.get(socket);
30
+ filter?.dispose?.();
31
+ clientReasoningFilters.delete(socket);
28
32
  clients.delete(socket);
29
33
  clientLastEventIds.delete(socket);
30
34
  readyClients.delete(socket);
@@ -32,10 +36,12 @@ export function createGlobalMessageStreamWsBridge({
32
36
  };
33
37
 
34
38
  const replayEvents = (socket, requestedLastEventId) => {
39
+ const reasoningFilter = clientReasoningFilters.get(socket);
35
40
  for (const entry of globalHub.replayAfter(requestedLastEventId)) {
36
41
  const sent = sendMessageStreamWsEvent(socket, entry.payload, {
37
42
  directory: entry.directory,
38
43
  eventId: entry.eventId,
44
+ reasoningFilter,
39
45
  });
40
46
  if (!sent) {
41
47
  removeClient(socket);
@@ -104,6 +110,7 @@ export function createGlobalMessageStreamWsBridge({
104
110
  const sent = sendMessageStreamWsEvent(socket, payload, {
105
111
  directory,
106
112
  eventId,
113
+ reasoningFilter: clientReasoningFilters.get(socket),
107
114
  });
108
115
  if (!sent) {
109
116
  removeClient(socket);
@@ -115,7 +122,10 @@ export function createGlobalMessageStreamWsBridge({
115
122
  if (!readyClients.has(socket)) {
116
123
  continue;
117
124
  }
118
- const sent = sendMessageStreamWsEvent(socket, syntheticPayload, { directory: 'global' });
125
+ const sent = sendMessageStreamWsEvent(socket, syntheticPayload, {
126
+ directory: 'global',
127
+ reasoningFilter: clientReasoningFilters.get(socket),
128
+ });
119
129
  if (!sent) {
120
130
  removeClient(socket);
121
131
  }
@@ -168,7 +178,7 @@ export function createGlobalMessageStreamWsBridge({
168
178
  }
169
179
  });
170
180
 
171
- const accept = (socket, { requestedLastEventId = '' } = {}) => {
181
+ const accept = (socket, { requestedLastEventId = '', reasoningFilter = null } = {}) => {
172
182
  const pingInterval = setInterval(() => {
173
183
  if (socket.readyState !== 1) {
174
184
  return;
@@ -185,7 +195,10 @@ export function createGlobalMessageStreamWsBridge({
185
195
  return;
186
196
  }
187
197
 
188
- sendMessageStreamWsEvent(socket, { type: 'openchamber:heartbeat', timestamp: Date.now() }, { directory: 'global' });
198
+ sendMessageStreamWsEvent(socket, { type: 'openchamber:heartbeat', timestamp: Date.now() }, {
199
+ directory: 'global',
200
+ reasoningFilter: clientReasoningFilters.get(socket),
201
+ });
189
202
  }, heartbeatIntervalMs);
190
203
 
191
204
  socket.on('close', () => {
@@ -201,6 +214,9 @@ export function createGlobalMessageStreamWsBridge({
201
214
 
202
215
  clients.add(socket);
203
216
  clientLastEventIds.set(socket, requestedLastEventId);
217
+ if (reasoningFilter) {
218
+ clientReasoningFilters.set(socket, reasoningFilter);
219
+ }
204
220
  globalHub.start();
205
221
  if (globalHub.isConnected()) {
206
222
  markReady(socket, requestedLastEventId);
@@ -126,9 +126,19 @@ export function sendMessageStreamWsFrame(socket, payload) {
126
126
  }
127
127
 
128
128
  export function sendMessageStreamWsEvent(socket, payload, options = {}) {
129
+ // Optional per-connection reasoning projector (includeReasoning=false).
130
+ // null from projectEvent means drop this frame entirely.
131
+ let candidate = payload;
132
+ if (options.reasoningFilter && typeof options.reasoningFilter.projectEvent === 'function') {
133
+ candidate = options.reasoningFilter.projectEvent(payload);
134
+ if (candidate == null) {
135
+ return true;
136
+ }
137
+ }
138
+
129
139
  // Summarize FileDiff bodies before fan-out so Relay/WS never carries full
130
140
  // patch frames. Identity is preserved for non-diff and already-summary events.
131
- const outbound = summarizeOutboundEventPayload(payload);
141
+ const outbound = summarizeOutboundEventPayload(candidate);
132
142
  return sendMessageStreamWsFrame(socket, {
133
143
  type: 'event',
134
144
  payload: outbound,
@@ -0,0 +1,425 @@
1
+ /**
2
+ * OpenChamber outbound reasoning projection.
3
+ *
4
+ * `includeReasoning=false` (strict string only) strips reasoning from Host→client
5
+ * HTTP/SSE/WS payloads. Missing/other values keep current behavior. OpenChamber
6
+ * projection only — never forward the param to OpenCode. Hub/upstream stay full.
7
+ *
8
+ * Shared by HTTP snapshots and stateful stream filters:
9
+ * drop type=reasoning parts, their deltas, session.next.reasoning.* events.
10
+ * Keep tokens.reasoning totals, tools, text, status, permissions/errors.
11
+ * Never mutate hub/replay objects.
12
+ */
13
+
14
+ const REASONING_DELTA_FIELDS = new Set([
15
+ 'reasoning',
16
+ 'reasoning_content',
17
+ 'reasoning_details',
18
+ ]);
19
+
20
+ /** Match UI opencode-event-normalizer: strip terminal `.N` version suffix for dispatch. */
21
+ const VERSIONED_TYPE = /^(.*)\.(\d+)$/;
22
+
23
+ const PART_TYPE_CACHE_LIMIT = 8192;
24
+
25
+ const isPlainObject = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
26
+
27
+ /** @param {string} type */
28
+ const baseEventType = (type) => {
29
+ const match = VERSIONED_TYPE.exec(type);
30
+ return match?.[1] ?? type;
31
+ };
32
+
33
+ /**
34
+ * Strict: only string `'false'` disables. Missing / other keep include.
35
+ * @param {unknown} value
36
+ * @returns {boolean}
37
+ */
38
+ export function shouldIncludeReasoning(value) {
39
+ if (Array.isArray(value) && value.length > 0) return value[0] !== 'false';
40
+ return value !== 'false';
41
+ }
42
+
43
+ /** @param {unknown} query */
44
+ export function readIncludeReasoningQuery(query) {
45
+ if (!query || typeof query !== 'object') return true;
46
+ return shouldIncludeReasoning(/** @type {Record<string, unknown>} */ (query).includeReasoning);
47
+ }
48
+
49
+ /** @param {string} rawUrl */
50
+ export function readIncludeReasoningFromUrl(rawUrl) {
51
+ if (typeof rawUrl !== 'string' || !rawUrl.includes('includeReasoning=')) return true;
52
+ try {
53
+ return shouldIncludeReasoning(new URL(rawUrl, 'http://openchamber.local').searchParams.get('includeReasoning'));
54
+ } catch {
55
+ return true;
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Strip OpenChamber-only `includeReasoning` before OpenCode upstream.
61
+ * @param {string} requestUrl
62
+ */
63
+ export function stripIncludeReasoningParam(requestUrl) {
64
+ if (typeof requestUrl !== 'string' || !requestUrl.includes('includeReasoning=')) return requestUrl;
65
+ try {
66
+ const url = new URL(requestUrl, 'http://openchamber.local');
67
+ if (!url.searchParams.has('includeReasoning')) return requestUrl;
68
+ url.searchParams.delete('includeReasoning');
69
+ const search = url.searchParams.toString();
70
+ if (requestUrl.startsWith('http://') || requestUrl.startsWith('https://')) {
71
+ return `${url.origin}${url.pathname}${search ? `?${search}` : ''}${url.hash || ''}`;
72
+ }
73
+ return `${url.pathname}${search ? `?${search}` : ''}${url.hash || ''}`;
74
+ } catch {
75
+ return requestUrl;
76
+ }
77
+ }
78
+
79
+ /** @param {unknown} parts */
80
+ function projectParts(parts) {
81
+ if (!Array.isArray(parts) || parts.length === 0) return parts;
82
+ let changed = false;
83
+ const next = [];
84
+ for (const part of parts) {
85
+ if (isPlainObject(part) && part.type === 'reasoning') {
86
+ changed = true;
87
+ continue;
88
+ }
89
+ next.push(part);
90
+ }
91
+ return changed ? next : parts;
92
+ }
93
+
94
+ /**
95
+ * One message record: `{ info, parts }` (turn-page / exact / cache / session.messages).
96
+ * @param {unknown} record
97
+ */
98
+ function projectRecord(record) {
99
+ if (!isPlainObject(record) || !Array.isArray(record.parts)) return record;
100
+ const parts = projectParts(record.parts);
101
+ return parts === record.parts ? record : { ...record, parts };
102
+ }
103
+
104
+ /**
105
+ * Real Host payload shapes only:
106
+ * - bare array of records
107
+ * - `{ records: [...] }` (turn-page / reconcile / cache session)
108
+ * - `{ record }` (cache message)
109
+ * - `{ info, parts }` (exact message)
110
+ *
111
+ * @param {unknown} payload
112
+ */
113
+ export function projectMessagesPayloadWithoutReasoning(payload) {
114
+ if (Array.isArray(payload)) {
115
+ let changed = false;
116
+ const next = payload.map((entry) => {
117
+ const projected = projectRecord(entry);
118
+ if (projected !== entry) changed = true;
119
+ return projected;
120
+ });
121
+ return changed ? next : payload;
122
+ }
123
+
124
+ if (!isPlainObject(payload)) return payload;
125
+
126
+ let next = payload;
127
+ let changed = false;
128
+
129
+ if (Array.isArray(payload.records)) {
130
+ const records = projectMessagesPayloadWithoutReasoning(payload.records);
131
+ if (records !== payload.records) {
132
+ next = { ...next, records };
133
+ changed = true;
134
+ }
135
+ }
136
+
137
+ if (isPlainObject(payload.record)) {
138
+ const record = projectRecord(payload.record);
139
+ if (record !== payload.record) {
140
+ next = { ...next, record };
141
+ changed = true;
142
+ }
143
+ }
144
+
145
+ // Exact message / single record body: top-level parts.
146
+ if (Array.isArray(payload.parts)) {
147
+ const projected = projectRecord(next);
148
+ if (projected !== next) {
149
+ next = projected;
150
+ changed = true;
151
+ }
152
+ }
153
+
154
+ return changed ? next : payload;
155
+ }
156
+
157
+ /**
158
+ * @param {unknown} payload
159
+ * @param {boolean} includeReasoning
160
+ */
161
+ export function projectMessagesPayloadForReasoning(payload, includeReasoning) {
162
+ if (includeReasoning) return payload;
163
+ return projectMessagesPayloadWithoutReasoning(payload);
164
+ }
165
+
166
+ const eventBodyKeys = /** @type {const} */ (['properties', 'data']);
167
+
168
+ /** @param {unknown} payload */
169
+ function eventBodyOf(payload) {
170
+ if (!isPlainObject(payload)) return null;
171
+ for (const key of eventBodyKeys) {
172
+ const body = payload[key];
173
+ if (isPlainObject(body)) return body;
174
+ }
175
+ return null;
176
+ }
177
+
178
+ /**
179
+ * Stateful per-connection outbound filter. Classification map is bounded.
180
+ * @returns {{ projectEvent: (payload: unknown) => unknown | null, knownPartCount: () => number, dispose: () => void }}
181
+ */
182
+ export function createReasoningOutboundFilter() {
183
+ /** @type {Map<string, string>} */
184
+ const partTypes = new Map();
185
+
186
+ const partKey = (messageID, partID) => `${messageID}\0${partID}`;
187
+
188
+ const rememberPartType = (messageID, partID, type) => {
189
+ if (typeof messageID !== 'string' || !messageID) return;
190
+ if (typeof partID !== 'string' || !partID) return;
191
+ if (typeof type !== 'string' || !type) return;
192
+ const key = partKey(messageID, partID);
193
+ if (partTypes.size >= PART_TYPE_CACHE_LIMIT && !partTypes.has(key)) {
194
+ const oldest = partTypes.keys().next().value;
195
+ if (oldest !== undefined) partTypes.delete(oldest);
196
+ }
197
+ partTypes.set(key, type);
198
+ };
199
+
200
+ const forgetPart = (messageID, partID) => {
201
+ if (typeof messageID === 'string' && typeof partID === 'string') {
202
+ partTypes.delete(partKey(messageID, partID));
203
+ }
204
+ };
205
+
206
+ const forgetMessage = (messageID) => {
207
+ if (typeof messageID !== 'string' || !messageID) return;
208
+ const prefix = `${messageID}\0`;
209
+ for (const key of Array.from(partTypes.keys())) {
210
+ if (key.startsWith(prefix)) partTypes.delete(key);
211
+ }
212
+ };
213
+
214
+ /**
215
+ * @param {unknown} payload
216
+ * @returns {unknown | null} null = drop; otherwise original or shallow-cloned payload (type string preserved)
217
+ */
218
+ const projectEvent = (payload) => {
219
+ if (!isPlainObject(payload)) return payload;
220
+
221
+ const rawType = typeof payload.type === 'string' ? payload.type : '';
222
+ const type = baseEventType(rawType);
223
+
224
+ // session.next.reasoning.* (incl. versioned …reasoning.delta.1)
225
+ if (type.startsWith('session.next.reasoning.')) return null;
226
+
227
+ if (type === 'message.part.updated') {
228
+ const body = eventBodyOf(payload);
229
+ const part = body && isPlainObject(body.part) ? body.part : null;
230
+ if (part) {
231
+ const messageID =
232
+ (typeof part.messageID === 'string' && part.messageID)
233
+ || (typeof body.messageID === 'string' && body.messageID)
234
+ || '';
235
+ const partID = typeof part.id === 'string' ? part.id : '';
236
+ if (typeof part.type === 'string') rememberPartType(messageID, partID, part.type);
237
+ if (part.type === 'reasoning') return null;
238
+ }
239
+ return payload;
240
+ }
241
+
242
+ if (type === 'message.part.delta') {
243
+ const body = eventBodyOf(payload);
244
+ if (!body) return null;
245
+
246
+ const messageID = typeof body.messageID === 'string' ? body.messageID : '';
247
+ const partID = typeof body.partID === 'string' ? body.partID : '';
248
+ const field = typeof body.field === 'string' ? body.field : '';
249
+
250
+ if (REASONING_DELTA_FIELDS.has(field)) return null;
251
+
252
+ const learned = messageID && partID ? partTypes.get(partKey(messageID, partID)) : undefined;
253
+ if (learned === 'reasoning' || learned === undefined) return null;
254
+ return payload;
255
+ }
256
+
257
+ if (type === 'message.part.removed') {
258
+ const body = eventBodyOf(payload);
259
+ if (body) {
260
+ forgetPart(
261
+ typeof body.messageID === 'string' ? body.messageID : '',
262
+ typeof body.partID === 'string' ? body.partID : '',
263
+ );
264
+ }
265
+ return payload;
266
+ }
267
+
268
+ if (type === 'message.removed') {
269
+ const body = eventBodyOf(payload);
270
+ if (body) {
271
+ const messageID =
272
+ (typeof body.messageID === 'string' && body.messageID)
273
+ || (isPlainObject(body.info) && typeof body.info.id === 'string' && body.info.id)
274
+ || '';
275
+ forgetMessage(messageID);
276
+ }
277
+ return payload;
278
+ }
279
+
280
+ // message.updated / message.created may embed parts on properties or data.
281
+ if (type === 'message.updated' || type === 'message.created') {
282
+ let next = payload;
283
+ let changed = false;
284
+ for (const key of eventBodyKeys) {
285
+ const body = next[key];
286
+ if (!isPlainObject(body) || !Array.isArray(body.parts)) continue;
287
+ const parts = projectParts(body.parts);
288
+ if (parts === body.parts) continue;
289
+ next = { ...next, [key]: { ...body, parts } };
290
+ changed = true;
291
+ }
292
+ return changed ? next : payload;
293
+ }
294
+
295
+ return payload;
296
+ };
297
+
298
+ return {
299
+ projectEvent,
300
+ knownPartCount: () => partTypes.size,
301
+ dispose: () => {
302
+ partTypes.clear();
303
+ },
304
+ };
305
+ }
306
+
307
+ /**
308
+ * Project SSE data JSON; preserve GlobalEvent `{ directory, payload }` wrap.
309
+ * @param {unknown} dataJson
310
+ * @param {{ projectEvent: (payload: unknown) => unknown | null }} filter
311
+ */
312
+ function projectSseDataJson(dataJson, filter) {
313
+ if (!isPlainObject(dataJson)) {
314
+ return { action: 'keep', value: dataJson, changed: false };
315
+ }
316
+
317
+ if (isPlainObject(dataJson.payload) && typeof dataJson.payload.type === 'string') {
318
+ const projected = filter.projectEvent(dataJson.payload);
319
+ if (projected == null) return { action: 'drop' };
320
+ if (projected === dataJson.payload) return { action: 'keep', value: dataJson, changed: false };
321
+ return { action: 'keep', value: { ...dataJson, payload: projected }, changed: true };
322
+ }
323
+
324
+ const projected = filter.projectEvent(dataJson);
325
+ if (projected == null) return { action: 'drop' };
326
+ if (projected === dataJson) return { action: 'keep', value: dataJson, changed: false };
327
+ return { action: 'keep', value: projected, changed: true };
328
+ }
329
+
330
+ /**
331
+ * Filter one SSE block (no trailing separator). null = drop.
332
+ * @param {string} block
333
+ * @param {{ projectEvent: (payload: unknown) => unknown | null }} filter
334
+ * @returns {string | null}
335
+ */
336
+ export function filterSseBlock(block, filter) {
337
+ if (typeof block !== 'string' || block.trim().length === 0) return null;
338
+ if (!block.includes('data:')) return `${block}\n\n`;
339
+
340
+ const lines = block.split('\n');
341
+ const dataLines = [];
342
+ const otherLines = [];
343
+ for (const line of lines) {
344
+ if (line.startsWith('data:')) dataLines.push(line.slice(5).replace(/^\s/, ''));
345
+ else otherLines.push(line);
346
+ }
347
+ if (dataLines.length === 0) return `${block}\n\n`;
348
+
349
+ const payloadText = dataLines.join('\n').trim();
350
+ if (!payloadText) return `${block}\n\n`;
351
+
352
+ let parsed;
353
+ try {
354
+ parsed = JSON.parse(payloadText);
355
+ } catch {
356
+ return `${block}\n\n`;
357
+ }
358
+
359
+ const result = projectSseDataJson(parsed, filter);
360
+ if (result.action === 'drop') return null;
361
+ if (!result.changed) return `${block}\n\n`;
362
+ return [...otherLines, `data: ${JSON.stringify(result.value)}`, '', ''].join('\n');
363
+ }
364
+
365
+ /**
366
+ * Incremental SSE block splitter. Holds a trailing CR so `\r` + `\n` across
367
+ * chunks never invents a false blank line (which would split one event in two).
368
+ */
369
+ export function createSseBlockSplitter() {
370
+ const decoder = new TextDecoder();
371
+ let buffer = '';
372
+ /** @type {string} trailing CR held until the next chunk clarifies `\r\n` vs lone `\r` */
373
+ let carryCr = '';
374
+
375
+ const appendNormalized = (text) => {
376
+ if (!text && !carryCr) return;
377
+ let input = carryCr + (text || '');
378
+ carryCr = '';
379
+ if (input.endsWith('\r')) {
380
+ carryCr = '\r';
381
+ input = input.slice(0, -1);
382
+ }
383
+ // Complete CRLF pairs first, then any remaining lone CR → LF.
384
+ buffer += input.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
385
+ };
386
+
387
+ const drainBlocks = () => {
388
+ const blocks = [];
389
+ while (true) {
390
+ const idx = buffer.indexOf('\n\n');
391
+ if (idx === -1) break;
392
+ const block = buffer.slice(0, idx);
393
+ buffer = buffer.slice(idx + 2);
394
+ if (block.trim().length > 0) blocks.push(block);
395
+ }
396
+ return blocks;
397
+ };
398
+
399
+ return {
400
+ /**
401
+ * @param {Uint8Array | string} chunk
402
+ * @returns {string[]}
403
+ */
404
+ push(chunk) {
405
+ const text = typeof chunk === 'string'
406
+ ? chunk
407
+ : decoder.decode(chunk, { stream: true });
408
+ if (!text) return [];
409
+ appendNormalized(text);
410
+ return drainBlocks();
411
+ },
412
+ /** @returns {string[]} */
413
+ finish() {
414
+ const tail = decoder.decode();
415
+ if (tail) appendNormalized(tail);
416
+ if (carryCr) {
417
+ buffer += '\n';
418
+ carryCr = '';
419
+ }
420
+ const remaining = buffer;
421
+ buffer = '';
422
+ return remaining.trim().length > 0 ? [remaining] : [];
423
+ },
424
+ };
425
+ }