@openchambery/web 1.18.2-beta.9 → 1.18.3-beta.1

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 (148) hide show
  1. package/dist/assets/AgentAvatar-BvhXxFvg.js +1 -0
  2. package/dist/assets/AssistantView-Cqqwrumr.js +1 -0
  3. package/dist/assets/{ChatContainer-BQJX_lbm.js → ChatContainer-DmDmufVy.js} +43 -43
  4. package/dist/assets/{DiagramView-CUcZB4JV.js → DiagramView-BdutEd2B.js} +1 -1
  5. package/dist/assets/{FadeInOnReveal-oeyB_nIi.js → FadeInOnReveal-98buIVdv.js} +1 -1
  6. package/dist/assets/{Icon-DHaR0peD.js → Icon-BhSwbLaF.js} +1 -1
  7. package/dist/assets/{MarkdownRendererImpl-CEfEkrko.js → MarkdownRendererImpl-D3rG27vo.js} +2 -2
  8. package/dist/assets/{MobileDetailNavigation-X3uGiCOg.js → MobileDetailNavigation-8Na-l3IA.js} +1 -1
  9. package/dist/assets/MobileShareBridge-BJNmoLdX.js +4 -0
  10. package/dist/assets/MobileSurface-DtB0737A.js +2 -0
  11. package/dist/assets/{MultiRunWindow-TMVAyy27.js → MultiRunWindow-BaOXHVax.js} +1 -1
  12. package/dist/assets/{OnboardingScreen-BZHN7C2r.js → OnboardingScreen-D4jsavBK.js} +1 -1
  13. package/dist/assets/OverlayScrollbar-CXFN6Cqq.js +2 -0
  14. package/dist/assets/SettingsView-DGh9nGVd.js +25 -0
  15. package/dist/assets/{SettingsWindow-DpG6TZAJ.js → SettingsWindow-BAdWKqH6.js} +1 -1
  16. package/dist/assets/{TerminalView-DE-69Gjv.js → TerminalView-BwsbvoCj.js} +1 -1
  17. package/dist/assets/{ToolOutputDialog-BnSnqmPr.js → ToolOutputDialog-p7xe_D4A.js} +1 -1
  18. package/dist/assets/appThemeRegistry-CSZVlqLO.js +2 -0
  19. package/dist/assets/assistant-guide-hero-wide-DiFIThB0.jpg +0 -0
  20. package/dist/assets/{button-DCUMIXDC.js → button-BQY5ugjh.js} +1 -1
  21. package/dist/assets/capgoAdapter-DCowZ2ab.js +1 -0
  22. package/dist/assets/{clipboard-DF5tFLDb.js → clipboard-CSaKrCWk.js} +1 -1
  23. package/dist/assets/{collapsible-DNg5bLKV.js → collapsible-CqcZGw3W.js} +1 -1
  24. package/dist/assets/{context-menu-BsA0lfWU.js → context-menu-zEQM3m-S.js} +1 -1
  25. package/dist/assets/{desktopHosts-DSgGxDpI.js → desktopHosts-aA40MHzB.js} +1 -1
  26. package/dist/assets/{desktopRelayRestore-DVIx7jsz.js → desktopRelayRestore-CxUYRC6v.js} +1 -1
  27. package/dist/assets/{diagram-cjvFqVa8.js → diagram-B8D9Klct.js} +1 -1
  28. package/dist/assets/{dialog-DaSIYgrj.js → dialog-DS65fQJH.js} +1 -1
  29. package/dist/assets/{es-DCoGZljP.js → es-CUu9UM-5.js} +4 -4
  30. package/dist/assets/{fileQueries-DXvhIL_U.js → fileQueries-BSE4ycQt.js} +1 -1
  31. package/dist/assets/{fr-dkEoZpPz.js → fr-D74z6dA0.js} +4 -4
  32. package/dist/assets/gitApi-DfM5TpNW.js +7 -0
  33. package/dist/assets/{input-QS3IWgY5.js → input-CgIoxUJc.js} +1 -1
  34. package/dist/assets/insertionBoundaries-BkDH-P9U.js +1 -0
  35. package/dist/assets/{ja-bQwSsvRr.js → ja-CEiZgxEa.js} +4 -4
  36. package/dist/assets/{ko-Kq5C3hsv.js → ko-BwT91AGB.js} +4 -4
  37. package/dist/assets/main-C8Y3zrIO.js +2 -0
  38. package/dist/assets/main-Cm4Spw2a.js +172 -0
  39. package/dist/assets/{markdown-worker-D-OR2e_G.js → markdown-worker-C5Om4TVK.js} +1 -1
  40. package/dist/assets/{markdownTheme-y_Rf6mLa.js → markdownTheme-BdF4C4-o.js} +1 -1
  41. package/dist/assets/miniChat-CZjvn38r.js +2 -0
  42. package/dist/assets/mobile-C3finp5s.js +2 -0
  43. package/dist/assets/multirun-RWCgJ2Sf.js +4 -0
  44. package/dist/assets/persistence-DwXJK4-j.js +3 -0
  45. package/dist/assets/pl-DxxarF_s.js +15 -0
  46. package/dist/assets/platform-CXJwZIZe.js +2 -0
  47. package/dist/assets/{projectMeta-C1espVUO.js → projectMeta-CI-agdGd.js} +2 -2
  48. package/dist/assets/{pt-BR-MSPTLfTm.js → pt-BR-BKo7iaa0.js} +4 -4
  49. package/dist/assets/radio-DqwJGxRE.js +1 -0
  50. package/dist/assets/renderElectronMiniChatApp-BFQSHOJt.js +2 -0
  51. package/dist/assets/renderMobileApp-B0IBEDa2.js +23 -0
  52. package/dist/assets/runtimeConfig-B2b1-Y8N.js +2 -0
  53. package/dist/assets/runtimeEndpointReset-Ci4hHJWD.js +147 -0
  54. package/dist/assets/{sortable-tabs-strip-0XQFWr_M.js → sortable-tabs-strip-ZyYqprbu.js} +1 -1
  55. package/dist/assets/src-D8ZPwFWd.css +2 -0
  56. package/dist/assets/{store-OURVnLSN.js → store-BFYITbNu.js} +4 -4
  57. package/dist/assets/terminalApi-jfes-HIU.js +1 -0
  58. package/dist/assets/{tooltip-xEnco9xY.js → tooltip-rN8LrxMF.js} +1 -1
  59. package/dist/assets/types-BuiqMmh_.js +1 -0
  60. package/dist/assets/{uk-cSkRAYFY.js → uk-COMEIAWc.js} +4 -4
  61. package/dist/assets/{url-DIZgk80M.js → url-B0Ak7X6b.js} +1 -1
  62. package/dist/assets/useAppFontEffects-D4VpDoYq.js +7 -0
  63. package/dist/assets/{useAssistantUIStore-Bq3cPj2x.js → useAssistantUIStore-SPh025NE.js} +1 -1
  64. package/dist/assets/useDesktopWindowControlsLayout-CdQWzNss.js +1 -0
  65. package/dist/assets/useEffectiveDirectory-BsdXP5uD.js +1 -0
  66. package/dist/assets/{useFontPreferences-B-QHyR3C.js → useFontPreferences-Bz3crRRO.js} +1 -1
  67. package/dist/assets/useMobileNavigationStore-DUmxzvX1.js +476 -0
  68. package/dist/assets/useSessionAutoCleanup-DSAI3g76.js +1 -0
  69. package/dist/assets/{useUIStore-5agS8bsW.js → useUIStore-BJpkPwky.js} +1 -1
  70. package/dist/assets/{useWorkerHighlightedLines-CzF7WMk3.js → useWorkerHighlightedLines-C3e2tW2K.js} +1 -1
  71. package/dist/assets/{utils-Dxsi3J3l.js → utils-DfPDhqys.js} +1 -1
  72. package/dist/assets/{zh-CN-BKqeT_uz.js → zh-CN-3PwbVrPm.js} +4 -4
  73. package/dist/assets/{zh-TW-BaNjJ8Y4.js → zh-TW-CU3zh3yX.js} +4 -4
  74. package/dist/index.html +13 -11
  75. package/dist/mini-chat.html +11 -9
  76. package/dist/mobile.html +11 -9
  77. package/package.json +1 -1
  78. package/server/index.js +7 -0
  79. package/server/lib/assistants/DOCUMENTATION.md +2 -2
  80. package/server/lib/assistants/history-state.js +23 -0
  81. package/server/lib/assistants/history-state.test.js +57 -0
  82. package/server/lib/assistants/service.js +44 -15
  83. package/server/lib/assistants/service.test.js +120 -5
  84. package/server/lib/event-stream/DOCUMENTATION.md +2 -2
  85. package/server/lib/event-stream/diff-summary.js +28 -19
  86. package/server/lib/event-stream/diff-summary.test.js +37 -10
  87. package/server/lib/event-stream/protocol.test.js +66 -3
  88. package/server/lib/message-queue/DOCUMENTATION.md +1 -1
  89. package/server/lib/notifications/DOCUMENTATION.md +2 -2
  90. package/server/lib/notifications/runtime.js +68 -27
  91. package/server/lib/notifications/runtime.test.js +176 -0
  92. package/server/lib/opencode/DOCUMENTATION.md +2 -2
  93. package/server/lib/opencode/settings-helpers.js +2 -2
  94. package/server/lib/opencode/settings-helpers.test.js +42 -0
  95. package/server/lib/opencode/settings-normalization-runtime.js +7 -2
  96. package/server/lib/session-index/DOCUMENTATION.md +12 -6
  97. package/server/lib/session-index/routes.js +34 -0
  98. package/server/lib/session-index/routes.test.js +76 -0
  99. package/server/lib/session-index/service.js +53 -4
  100. package/server/lib/session-index/service.test.js +72 -2
  101. package/server/lib/session-title/DOCUMENTATION.md +19 -14
  102. package/server/lib/session-title/runtime.js +8 -8
  103. package/server/lib/session-title/runtime.test.js +231 -0
  104. package/server/lib/session-turn-pages/DOCUMENTATION.md +4 -4
  105. package/server/lib/session-turn-pages/reconcile.routes.test.js +6 -2
  106. package/server/lib/session-turn-pages/reconcile.service.test.js +6 -2
  107. package/server/lib/session-turn-pages/routes.js +3 -3
  108. package/server/lib/session-turn-pages/routes.test.js +41 -13
  109. package/server/lib/session-turn-pages/service.js +4 -2
  110. package/server/lib/session-turn-pages/service.test.js +13 -3
  111. package/server/lib/small-model/DOCUMENTATION.md +93 -38
  112. package/server/lib/small-model/catalog.js +10 -2
  113. package/server/lib/small-model/catalog.test.js +3 -1
  114. package/server/lib/small-model/index.js +52 -28
  115. package/server/lib/small-model/opencode-session.js +339 -0
  116. package/server/lib/small-model/opencode-session.test.js +177 -0
  117. package/server/lib/small-model/resolve.js +116 -5
  118. package/server/lib/small-model/resolve.test.js +76 -10
  119. package/server/lib/small-model/summary-settings.test.js +60 -3
  120. package/dist/assets/AgentAvatar-BzRb9sP6.js +0 -1
  121. package/dist/assets/AssistantView-DjG3NioW.js +0 -1
  122. package/dist/assets/MobileShareBridge-Cp9Mbus8.js +0 -4
  123. package/dist/assets/MobileSurface-CPfPdhR2.js +0 -2
  124. package/dist/assets/OverlayScrollbar-Bk-JHgkU.js +0 -1
  125. package/dist/assets/SettingsView-CEWseYV4.js +0 -25
  126. package/dist/assets/appThemeRegistry-CtmzwV4x.js +0 -2
  127. package/dist/assets/gitApi-DpQOg2UF.js +0 -7
  128. package/dist/assets/gitApiHttp-SLrnr8a5.js +0 -2
  129. package/dist/assets/insertionBoundaries-0grn8v_H.js +0 -1
  130. package/dist/assets/main-BhOjZs9d.js +0 -172
  131. package/dist/assets/main-wiv-Mcf-.js +0 -2
  132. package/dist/assets/miniChat-BmZzFlCY.js +0 -2
  133. package/dist/assets/mobile-BTW_GWJo.js +0 -2
  134. package/dist/assets/multirun-BEx2Gajj.js +0 -4
  135. package/dist/assets/persistence-oSFWyKgV.js +0 -3
  136. package/dist/assets/pl-CYpMAgVn.js +0 -15
  137. package/dist/assets/radio-snHzR8-Z.js +0 -1
  138. package/dist/assets/renderElectronMiniChatApp-DyJSVgf1.js +0 -2
  139. package/dist/assets/renderMobileApp-CmzB08pd.js +0 -23
  140. package/dist/assets/runtimeConfig-n4QAk1lU.js +0 -1
  141. package/dist/assets/runtimeEndpointReset-JATNf8W2.js +0 -145
  142. package/dist/assets/src-BhvNwAr-.css +0 -2
  143. package/dist/assets/terminalApi-_NnfkxgP.js +0 -1
  144. package/dist/assets/useAppFontEffects-D1lxdZjh.js +0 -7
  145. package/dist/assets/useDesktopWindowControlsLayout-DSAN4xkb.js +0 -1
  146. package/dist/assets/useEffectiveDirectory-BTrz9jc0.js +0 -1
  147. package/dist/assets/useMobileNavigationStore-Rel8oStT.js +0 -476
  148. package/dist/assets/useSessionAutoCleanup-CGvCs8rJ.js +0 -1
@@ -2,8 +2,9 @@
2
2
  // Outbound FileDiff summarization for message-stream fan-out and Host L1.
3
3
  //
4
4
  // L1 (ordinary first-packet / SSE / exact message GET):
5
- // project `info.summary.diffs` arrays to `{ diffCount, hasDiffs }` and remove
6
- // the file array entirely. Never ship patch bodies on these paths.
5
+ // keep `info.summary.diffs` as a slim file list
6
+ // `{ file, status?, additions, deletions }` plus additive
7
+ // `diffCount` / `hasDiffs` markers. Never ship patch bodies on these paths.
7
8
  //
8
9
  // Explicit L2/L3 APIs may still use summarizeFileDiff(s) for file-list / patch
9
10
  // responses. Pure JS — do not import UI TypeScript from the server package.
@@ -28,9 +29,6 @@ const hasHeavyDiffBody = (diff) => {
28
29
  * Reduce a FileDiff / SnapshotFileDiff to preview-safe scalars only.
29
30
  * Drops patch / before / after / from / to and any other large body fields.
30
31
  * Already-summary objects keep identity.
31
- *
32
- * Retained for explicit L2 file-list APIs; L1 message paths must use
33
- * `projectMessageSummaryDiffCounts` instead and never keep a file array.
34
32
  */
35
33
  export function summarizeFileDiff(diff) {
36
34
  if (!diff || typeof diff !== 'object' || Array.isArray(diff)) {
@@ -69,13 +67,14 @@ export function summarizeFileDiffs(diffs) {
69
67
  }
70
68
 
71
69
  /**
72
- * L1 projection: replace `summary.diffs` with count/marker scalars.
70
+ * L1 projection: keep `summary.diffs` as a slim file-list array and set
71
+ * additive `diffCount` / `hasDiffs` markers. Drops patch/before/after/from/to.
73
72
  * Keeps every other lightweight summary field. Identity when unchanged.
74
73
  *
75
74
  * @param {unknown} owner message info / session info / summary-bearing object
76
75
  * @returns {unknown}
77
76
  */
78
- export const projectMessageSummaryDiffCounts = (owner) => {
77
+ export const projectMessageSummaryDiffSlim = (owner) => {
79
78
  if (!owner || typeof owner !== 'object' || Array.isArray(owner)) {
80
79
  return owner;
81
80
  }
@@ -89,23 +88,33 @@ export const projectMessageSummaryDiffCounts = (owner) => {
89
88
  return owner;
90
89
  }
91
90
 
92
- const diffs = summary.diffs;
93
- const diffCount = Array.isArray(diffs) ? diffs.length : 0;
91
+ const rawDiffs = summary.diffs;
92
+ const slimDiffs = Array.isArray(rawDiffs) ? summarizeFileDiffs(rawDiffs) : [];
93
+ const diffCount = slimDiffs.length;
94
94
  const hasDiffs = diffCount > 0;
95
95
 
96
- const nextSummary = { ...summary };
97
- delete nextSummary.diffs;
98
- nextSummary.diffCount = diffCount;
99
- nextSummary.hasDiffs = hasDiffs;
96
+ const diffsUnchanged = slimDiffs === rawDiffs;
97
+ const markersMatch = summary.diffCount === diffCount && summary.hasDiffs === hasDiffs;
98
+ if (diffsUnchanged && markersMatch) {
99
+ return owner;
100
+ }
100
101
 
101
102
  return {
102
103
  ...owner,
103
- summary: nextSummary,
104
+ summary: {
105
+ ...summary,
106
+ diffs: slimDiffs,
107
+ diffCount,
108
+ hasDiffs,
109
+ },
104
110
  };
105
111
  };
106
112
 
107
- /** @deprecated Prefer projectMessageSummaryDiffCounts — L1 removes the file array. */
108
- export const summarizeMessageDiffSnapshots = projectMessageSummaryDiffCounts;
113
+ /** @deprecated Prefer projectMessageSummaryDiffSlimsame L1 slim-list behavior. */
114
+ export const projectMessageSummaryDiffCounts = projectMessageSummaryDiffSlim;
115
+
116
+ /** @deprecated Prefer projectMessageSummaryDiffSlim — L1 keeps slim file list + markers. */
117
+ export const summarizeMessageDiffSnapshots = projectMessageSummaryDiffSlim;
109
118
 
110
119
  /**
111
120
  * Summarize FileDiff bodies on outbound event payloads.
@@ -114,7 +123,7 @@ export const summarizeMessageDiffSnapshots = projectMessageSummaryDiffCounts;
114
123
  * - `session.diff` with legacy `properties.diff` or current `data.diff`
115
124
  * (keeps file/status/additions/deletions; drops patch bodies)
116
125
  * - message/session objects carrying `summary.diffs` under properties/data info
117
- * (L1: projects to diffCount/hasDiffs and removes the array)
126
+ * (L1: slim file list + diffCount/hasDiffs; never patch bodies)
118
127
  *
119
128
  * Non-diff events keep identity.
120
129
  */
@@ -173,7 +182,7 @@ export function summarizeOutboundEventPayload(payload) {
173
182
 
174
183
  // Common shape: properties.info.summary.diffs
175
184
  if (body.info && typeof body.info === 'object' && !Array.isArray(body.info)) {
176
- const nextInfo = projectMessageSummaryDiffCounts(body.info);
185
+ const nextInfo = projectMessageSummaryDiffSlim(body.info);
177
186
  if (nextInfo !== body.info) {
178
187
  next = {
179
188
  ...next,
@@ -188,7 +197,7 @@ export function summarizeOutboundEventPayload(payload) {
188
197
  }
189
198
 
190
199
  // Rare: properties.summary.diffs directly on the properties/data body
191
- const nextBody = projectMessageSummaryDiffCounts(body);
200
+ const nextBody = projectMessageSummaryDiffSlim(body);
192
201
  if (nextBody !== body) {
193
202
  next = {
194
203
  ...next,
@@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest';
2
2
 
3
3
  import {
4
4
  projectMessageSummaryDiffCounts,
5
+ projectMessageSummaryDiffSlim,
5
6
  summarizeFileDiff,
6
7
  summarizeFileDiffs,
7
8
  summarizeOutboundEventPayload,
@@ -49,34 +50,41 @@ describe('summarizeFileDiffs', () => {
49
50
  });
50
51
  });
51
52
 
52
- describe('projectMessageSummaryDiffCounts', () => {
53
- it('replaces summary.diffs with diffCount/hasDiffs and drops the array', () => {
53
+ describe('projectMessageSummaryDiffSlim', () => {
54
+ it('keeps slim summary.diffs and sets diffCount/hasDiffs', () => {
54
55
  const owner = {
55
56
  id: 'msg_1',
56
57
  summary: {
57
58
  title: 'turn',
58
59
  diffs: [
59
60
  { file: 'a.ts', additions: 1, deletions: 0, patch: '@@' },
60
- { file: 'b.ts', additions: 2, deletions: 1 },
61
+ { file: 'b.ts', additions: 2, deletions: 1, before: 'x', after: 'y' },
61
62
  ],
62
63
  },
63
64
  };
64
65
 
65
- const next = projectMessageSummaryDiffCounts(owner);
66
+ const next = projectMessageSummaryDiffSlim(owner);
66
67
  expect(next).not.toBe(owner);
67
68
  expect(next.summary).toEqual({
68
69
  title: 'turn',
70
+ diffs: [
71
+ { file: 'a.ts', additions: 1, deletions: 0 },
72
+ { file: 'b.ts', additions: 2, deletions: 1 },
73
+ ],
69
74
  diffCount: 2,
70
75
  hasDiffs: true,
71
76
  });
72
- expect(next.summary.diffs).toBeUndefined();
77
+ expect(JSON.stringify(next)).not.toContain('patch');
78
+ expect(JSON.stringify(next)).not.toContain('"before"');
79
+ expect(JSON.stringify(next)).not.toContain('"after"');
73
80
  });
74
81
 
75
- it('projects empty arrays to diffCount 0 / hasDiffs false', () => {
82
+ it('projects empty arrays to slim list + diffCount 0 / hasDiffs false', () => {
76
83
  const owner = {
77
84
  summary: { diffs: [] },
78
85
  };
79
- expect(projectMessageSummaryDiffCounts(owner).summary).toEqual({
86
+ expect(projectMessageSummaryDiffSlim(owner).summary).toEqual({
87
+ diffs: [],
80
88
  diffCount: 0,
81
89
  hasDiffs: false,
82
90
  });
@@ -84,7 +92,22 @@ describe('projectMessageSummaryDiffCounts', () => {
84
92
 
85
93
  it('keeps identity when summary has no diffs key', () => {
86
94
  const owner = { summary: { title: 'x', diffCount: 3, hasDiffs: true } };
87
- expect(projectMessageSummaryDiffCounts(owner)).toBe(owner);
95
+ expect(projectMessageSummaryDiffSlim(owner)).toBe(owner);
96
+ });
97
+
98
+ it('keeps identity when diffs already slim and markers match', () => {
99
+ const owner = {
100
+ summary: {
101
+ diffs: [{ file: 'a.ts', additions: 1, deletions: 0 }],
102
+ diffCount: 1,
103
+ hasDiffs: true,
104
+ },
105
+ };
106
+ expect(projectMessageSummaryDiffSlim(owner)).toBe(owner);
107
+ });
108
+
109
+ it('exports projectMessageSummaryDiffCounts as a compatible alias', () => {
110
+ expect(projectMessageSummaryDiffCounts).toBe(projectMessageSummaryDiffSlim);
88
111
  });
89
112
  });
90
113
 
@@ -137,7 +160,7 @@ describe('summarizeOutboundEventPayload', () => {
137
160
  }]);
138
161
  });
139
162
 
140
- it('projects message.updated info.summary.diffs to L1 count/marker', () => {
163
+ it('projects message.updated info.summary.diffs to L1 slim list + markers', () => {
141
164
  const payload = {
142
165
  type: 'message.updated',
143
166
  properties: {
@@ -149,6 +172,8 @@ describe('summarizeOutboundEventPayload', () => {
149
172
  additions: 1,
150
173
  deletions: 0,
151
174
  patch: '@@',
175
+ from: 'blob-a',
176
+ to: 'blob-b',
152
177
  }],
153
178
  },
154
179
  },
@@ -157,10 +182,12 @@ describe('summarizeOutboundEventPayload', () => {
157
182
 
158
183
  const next = summarizeOutboundEventPayload(payload);
159
184
  expect(next.properties.info.summary).toEqual({
185
+ diffs: [{ file: 'a.ts', additions: 1, deletions: 0 }],
160
186
  diffCount: 1,
161
187
  hasDiffs: true,
162
188
  });
163
- expect(next.properties.info.summary.diffs).toBeUndefined();
189
+ expect(JSON.stringify(next)).not.toContain('patch');
190
+ expect(JSON.stringify(next)).not.toContain('blob-a');
164
191
  });
165
192
 
166
193
  it('returns identity for session.status', () => {
@@ -223,7 +223,7 @@ describe('event stream protocol helpers', () => {
223
223
  expect(rawPayload.length).toBeLessThan(64 * 1024);
224
224
  });
225
225
 
226
- it('projects message.updated summary.diffs to L1 count/marker on outbound frames', () => {
226
+ it('projects message.updated summary.diffs to L1 slim list + markers on outbound frames', () => {
227
227
  let rawPayload = null;
228
228
  const socket = {
229
229
  readyState: 1,
@@ -246,6 +246,8 @@ describe('event stream protocol helpers', () => {
246
246
  additions: 2,
247
247
  deletions: 1,
248
248
  patch: '@@ heavy patch body @@\n' + 'y'.repeat(1000),
249
+ before: 'old',
250
+ after: 'new',
249
251
  }],
250
252
  },
251
253
  },
@@ -255,12 +257,73 @@ describe('event stream protocol helpers', () => {
255
257
  expect(sent).toBe(true);
256
258
  const frame = JSON.parse(rawPayload);
257
259
  expect(frame.payload.properties.info.summary).toEqual({
260
+ diffs: [{
261
+ file: 'a.ts',
262
+ status: 'modified',
263
+ additions: 2,
264
+ deletions: 1,
265
+ }],
258
266
  diffCount: 1,
259
267
  hasDiffs: true,
260
268
  });
261
- expect(frame.payload.properties.info.summary.diffs).toBeUndefined();
262
269
  expect(JSON.stringify(frame)).not.toContain('patch');
263
- expect(JSON.stringify(frame)).not.toContain('a.ts');
270
+ expect(JSON.stringify(frame)).not.toContain('"before"');
271
+ expect(JSON.stringify(frame)).not.toContain('"after"');
272
+ });
273
+
274
+ it('keeps L1 message.updated serialization under 256KiB for a 463-file / ~14MB patch summary', () => {
275
+ let rawPayload = null;
276
+ const socket = {
277
+ readyState: 1,
278
+ send(payload) {
279
+ rawPayload = payload;
280
+ },
281
+ };
282
+
283
+ const patchBody = 'P'.repeat(Math.floor((14 * 1024 * 1024) / 463));
284
+ const diffs = Array.from({ length: 463 }, (_, i) => ({
285
+ file: `src/generated/file-${i}.ts`,
286
+ status: 'modified',
287
+ additions: i % 7,
288
+ deletions: i % 3,
289
+ patch: `${patchBody}-${i}`,
290
+ before: `before-${i}`,
291
+ after: `after-${i}`,
292
+ from: `from-${i}`,
293
+ to: `to-${i}`,
294
+ }));
295
+
296
+ const sent = sendMessageStreamWsEvent(socket, {
297
+ type: 'message.updated',
298
+ properties: {
299
+ info: {
300
+ id: 'msg_huge',
301
+ sessionID: 'ses_huge',
302
+ role: 'assistant',
303
+ summary: { title: 'huge', diffs },
304
+ },
305
+ },
306
+ });
307
+
308
+ expect(sent).toBe(true);
309
+ const frame = JSON.parse(rawPayload);
310
+ const summary = frame.payload.properties.info.summary;
311
+ expect(summary.diffCount).toBe(463);
312
+ expect(summary.hasDiffs).toBe(true);
313
+ expect(summary.diffs).toHaveLength(463);
314
+ expect(summary.diffs[0]).toEqual({
315
+ file: 'src/generated/file-0.ts',
316
+ status: 'modified',
317
+ additions: 0,
318
+ deletions: 0,
319
+ });
320
+ expect(rawPayload).not.toContain(patchBody.slice(0, 64));
321
+ expect(rawPayload).not.toContain('"patch"');
322
+ expect(rawPayload).not.toContain('"before"');
323
+ expect(rawPayload).not.toContain('"after"');
324
+ expect(rawPayload).not.toContain('"from"');
325
+ expect(rawPayload).not.toContain('"to"');
326
+ expect(Buffer.byteLength(rawPayload, 'utf8')).toBeLessThan(256 * 1024);
264
327
  });
265
328
 
266
329
  it('leaves ordinary session.status events unchanged', () => {
@@ -67,7 +67,7 @@ The root snapshot contains a catalog of scope identity, revision, lifecycle stat
67
67
 
68
68
  ## Manual send and optimistic chip projection
69
69
 
70
- Manual send stores a one-time durable dispatch intent at any moment (waiting statuses only). Scope and item DTOs expose the non-sensitive authoritative `manualDispatchRequested` field. Authoritative tracking rows—`manualDispatchRequested === true`, `sending`, and `reconciling`—remain durable until exact confirmation/tombstone, but UI chips hide them as already-sent tracking. `failed` / `unresolved` restore chip visibility and retry. Pending client send mutations hide the target chip immediately (same optimistic filter as edit/remove); a definitive mutation failure clears the overlay so the authoritative waiting/failed/unresolved row reappears. After the mutation leaves `pending`, a successful send keeps a **committed-ack-revision shadow**: the client result carries `committedRevision` from the action receipt even when post-commit scope reload fails, and chips continue hiding that exact runtime generation/scope send target while the authoritative scope revision is still behind the ack. Shadow ends when scope revision reaches the ack, the runtime generation changes, or the success mutation is cleaned from the MutationCache. The overlay never writes the revision-pinned Query cache.
70
+ Manual send stores a one-time durable dispatch intent at any moment (waiting statuses only). Scope and item DTOs expose the non-sensitive authoritative `manualDispatchRequested` field. Authoritative tracking rows—`manualDispatchRequested === true`, `sending`, and `reconciling`—remain durable until exact confirmation/tombstone. UI chips keep pending client send, committed-ack send shadows, and authoritative `manualDispatchRequested` visible in a **"Sending…"** state (parallel to pending-admission "Queuing…"); chips hide only after authoritative `sending`/`reconciling` (or removal/confirm). `failed` / `unresolved` restore normal Send/Edit. Pending client send mutations leave the target chip visible; a definitive mutation failure clears the overlay so Send/Edit return. After the mutation leaves `pending`, a successful send keeps a **committed-ack-revision shadow**: the client result carries `committedRevision` from the action receipt even when post-commit scope reload fails, and the shadow continues selecting that exact runtime generation/scope send target for "Sending…" UI ownership while the authoritative scope revision is still behind the ack. Shadow ends when scope revision reaches the ack, the runtime generation changes, or the success mutation is cleaned from the MutationCache. Client send-pending presentation times out after 8s (`SERVER_QUEUE_SEND_PENDING_TIMEOUT_MS`) and restores clickable Send when stuck without reaching sending/reconciling/removal. The overlay never writes the revision-pinned Query cache.
71
71
 
72
72
  `beginAttempt` consumes the manual intent atomically with its message ID and attempt history; `sending` and `reconciling` status then represent active delivery tracking. Pre-attempt release and retry preserve the intent. Pause, expired-sending recovery, and ambiguity reconciliation clear the intent because the persisted attempt owns delivery certainty. Compact mutation acknowledgements retain their existing identity and revision fields. Dual-client replay of the same manual-send `requestID` returns the stored receipt once.
73
73
 
@@ -45,8 +45,8 @@ This module provides notification message preparation utilities for the web serv
45
45
  - Returned API:
46
46
  - `maybeSendPushForTrigger(payload)`
47
47
  - Owns:
48
- - top-level completion/question/permission trigger routing; completion is the sole task-status notification and child sessions are suppressed
49
- - session parent cache for child-session suppression
48
+ - top-level completion/question/permission trigger routing; completion is the sole task-status notification and child sessions plus small-model system sessions (`metadata.openchamber.smallModel.purpose`) are suppressed
49
+ - session meta cache for child-session and small-model suppression
50
50
  - template resolution and fallback behavior
51
51
  - native notification fanout and web push payload fanout
52
52
  - push suppression while any fresh UI visibility heartbeat reports a focused client
@@ -111,8 +111,8 @@ export const createNotificationTriggerRuntime = (deps) => {
111
111
  const notifiedPermissionRequests = new Set();
112
112
  const lastReadyNotificationAt = new Map();
113
113
 
114
- const sessionParentIdCache = new Map();
115
- const SESSION_PARENT_CACHE_TTL_MS = 60 * 1000;
114
+ const sessionMetaCache = new Map();
115
+ const SESSION_META_CACHE_TTL_MS = 60 * 1000;
116
116
 
117
117
  // Sessions where the client has enabled Permission Auto-Accept. Mirrored
118
118
  // from the client-side permissionStore via POST /api/notifications/auto-accept
@@ -136,43 +136,67 @@ export const createNotificationTriggerRuntime = (deps) => {
136
136
  return `/?session=${encodeURIComponent(sessionId)}`;
137
137
  };
138
138
 
139
- const getSessionParentCacheKey = (sessionId, directory) => `${directory || ''}\0${sessionId}`;
139
+ const getSessionMetaCacheKey = (sessionId, directory) => `${directory || ''}\0${sessionId}`;
140
140
 
141
- const getCachedSessionParentId = (sessionId, directory) => {
142
- const cacheKey = getSessionParentCacheKey(sessionId, directory);
143
- const entry = sessionParentIdCache.get(cacheKey);
141
+ const normalizeParentID = (value) => (
142
+ typeof value === 'string' && value.length > 0 ? value : null
143
+ );
144
+
145
+ const normalizeSmallModelPurpose = (value) => (
146
+ typeof value === 'string' && value.length > 0 ? value : null
147
+ );
148
+
149
+ const getCachedSessionMeta = (sessionId, directory) => {
150
+ const cacheKey = getSessionMetaCacheKey(sessionId, directory);
151
+ const entry = sessionMetaCache.get(cacheKey);
144
152
  if (!entry) return undefined;
145
- if (Date.now() - entry.at > SESSION_PARENT_CACHE_TTL_MS) {
146
- sessionParentIdCache.delete(cacheKey);
153
+ if (Date.now() - entry.at > SESSION_META_CACHE_TTL_MS) {
154
+ sessionMetaCache.delete(cacheKey);
147
155
  return undefined;
148
156
  }
149
- return entry.parentID;
157
+ return entry;
150
158
  };
151
159
 
152
- const setCachedSessionParentId = (sessionId, directory, parentID) => {
153
- sessionParentIdCache.set(getSessionParentCacheKey(sessionId, directory), { parentID: parentID ?? null, at: Date.now() });
160
+ const setCachedSessionMeta = (sessionId, directory, meta) => {
161
+ const previous = getCachedSessionMeta(sessionId, directory);
162
+ sessionMetaCache.set(getSessionMetaCacheKey(sessionId, directory), {
163
+ parentID: meta.parentID !== undefined ? meta.parentID : (previous?.parentID ?? null),
164
+ smallModelPurpose: meta.smallModelPurpose !== undefined
165
+ ? meta.smallModelPurpose
166
+ : (previous?.smallModelPurpose ?? null),
167
+ at: Date.now(),
168
+ });
154
169
  };
155
170
 
156
171
  const getParentIdFromPayload = (payload) => {
157
172
  if (!payload || typeof payload !== 'object') return undefined;
158
173
  if (payload.type !== 'session.created' && payload.type !== 'session.updated') return undefined;
159
- const parentID = payload.properties?.info?.parentID ?? null;
160
- return typeof parentID === 'string' && parentID.length > 0 ? parentID : null;
174
+ return normalizeParentID(payload.properties?.info?.parentID);
175
+ };
176
+
177
+ const getSmallModelPurposeFromPayload = (payload) => {
178
+ if (!payload || typeof payload !== 'object') return undefined;
179
+ if (payload.type !== 'session.created' && payload.type !== 'session.updated') return undefined;
180
+ return normalizeSmallModelPurpose(payload.properties?.info?.metadata?.openchamber?.smallModel?.purpose);
161
181
  };
162
182
 
163
- const maybeCacheSessionParentFromPayload = (payload) => {
183
+ const maybeCacheSessionMetaFromPayload = (payload) => {
164
184
  const sessionId = extractSessionIdFromPayload(payload);
165
185
  if (typeof sessionId !== 'string' || sessionId.length === 0) return;
166
186
  const directory = extractDirectoryFromPayload(payload);
167
187
  const parentID = getParentIdFromPayload(payload);
168
- if (parentID === undefined) return;
169
- setCachedSessionParentId(sessionId, directory, parentID);
188
+ const smallModelPurpose = getSmallModelPurposeFromPayload(payload);
189
+ if (parentID === undefined && smallModelPurpose === undefined) return;
190
+ setCachedSessionMeta(sessionId, directory, {
191
+ ...(parentID !== undefined ? { parentID } : {}),
192
+ ...(smallModelPurpose !== undefined ? { smallModelPurpose } : {}),
193
+ });
170
194
  };
171
195
 
172
- const fetchSessionParentId = async (sessionId, directory) => {
196
+ const fetchSessionMeta = async (sessionId, directory) => {
173
197
  if (!sessionId) return undefined;
174
198
 
175
- const cached = getCachedSessionParentId(sessionId, directory);
199
+ const cached = getCachedSessionMeta(sessionId, directory);
176
200
  if (cached !== undefined) return cached;
177
201
 
178
202
  try {
@@ -194,16 +218,22 @@ export const createNotificationTriggerRuntime = (deps) => {
194
218
  return undefined;
195
219
  }
196
220
 
197
- const parentID = typeof session.parentID === 'string' && session.parentID.length > 0
198
- ? session.parentID
199
- : null;
200
- setCachedSessionParentId(sessionId, directory, parentID);
201
- return parentID;
221
+ const meta = {
222
+ parentID: normalizeParentID(session.parentID),
223
+ smallModelPurpose: normalizeSmallModelPurpose(session.metadata?.openchamber?.smallModel?.purpose),
224
+ };
225
+ setCachedSessionMeta(sessionId, directory, meta);
226
+ return meta;
202
227
  } catch {
203
228
  return undefined;
204
229
  }
205
230
  };
206
231
 
232
+ const fetchSessionParentId = async (sessionId, directory) => {
233
+ const meta = await fetchSessionMeta(sessionId, directory);
234
+ return meta?.parentID;
235
+ };
236
+
207
237
  // Mirrors client-side autoRespondsPermission: a session auto-accepts if it
208
238
  // OR any ancestor is flagged. Walks the parent chain via fetchSessionParentId.
209
239
  const isSessionAutoAccepting = async (sessionId, directory) => {
@@ -225,6 +255,17 @@ export const createNotificationTriggerRuntime = (deps) => {
225
255
  return typeof parentID === 'string' && parentID.length > 0;
226
256
  };
227
257
 
258
+ const isSmallModelSession = async (sessionId, directory) => {
259
+ const meta = await fetchSessionMeta(sessionId, directory);
260
+ return typeof meta?.smallModelPurpose === 'string' && meta.smallModelPurpose.length > 0;
261
+ };
262
+
263
+ const shouldSkipSystemSessionNotification = async (sessionId, directory) => {
264
+ if (await isSubtaskSession(sessionId, directory)) return true;
265
+ if (await isSmallModelSession(sessionId, directory)) return true;
266
+ return false;
267
+ };
268
+
228
269
  const extractSessionIdFromPayload = (payload) => {
229
270
  if (!payload || typeof payload !== 'object') return null;
230
271
  const props = payload.properties;
@@ -309,7 +350,7 @@ export const createNotificationTriggerRuntime = (deps) => {
309
350
  return;
310
351
  }
311
352
 
312
- maybeCacheSessionParentFromPayload(payload);
353
+ maybeCacheSessionMetaFromPayload(payload);
313
354
 
314
355
  const sessionId = extractSessionIdFromPayload(payload);
315
356
  const notificationDirectory = extractDirectoryFromPayload(payload);
@@ -340,7 +381,7 @@ export const createNotificationTriggerRuntime = (deps) => {
340
381
  sessionId
341
382
  && info?.role === 'assistant'
342
383
  && (info?.finish === 'stop' || info?.finish === 'error')
343
- && await isSubtaskSession(sessionId, notificationDirectory)
384
+ && await shouldSkipSystemSessionNotification(sessionId, notificationDirectory)
344
385
  ) {
345
386
  return;
346
387
  }
@@ -433,7 +474,7 @@ export const createNotificationTriggerRuntime = (deps) => {
433
474
  }
434
475
 
435
476
  if (payload.type === 'question.asked' && sessionId) {
436
- if (await isSubtaskSession(sessionId, notificationDirectory)) {
477
+ if (await shouldSkipSystemSessionNotification(sessionId, notificationDirectory)) {
437
478
  return;
438
479
  }
439
480
  const existingTimer = pushQuestionDebounceTimers.get(sessionId);
@@ -534,7 +575,7 @@ export const createNotificationTriggerRuntime = (deps) => {
534
575
  }
535
576
 
536
577
  if (payload.type === 'permission.asked' && sessionId) {
537
- if (await isSubtaskSession(sessionId, notificationDirectory)) {
578
+ if (await shouldSkipSystemSessionNotification(sessionId, notificationDirectory)) {
538
579
  return;
539
580
  }
540
581
  const requestId = payload.properties?.id ?? payload.properties?.requestID ?? payload.properties?.requestId;