@openchambery/web 2.0.0-beta.3 → 2.0.0-beta.5

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 (178) hide show
  1. package/dist/assets/{AgentAvatar-B_JQoreg.js → AgentAvatar-BBmQXYDq.js} +1 -1
  2. package/dist/assets/AssistantView-BG8hK4y9.js +3 -0
  3. package/dist/assets/{DiagramView-BNnda8qO.js → DiagramView-CN0qWNU5.js} +1 -1
  4. package/dist/assets/{FadeInOnReveal-VYQTSAkr.js → FadeInOnReveal-w_BQ7K7K.js} +1 -1
  5. package/dist/assets/Icon-B9kZgYZn.js +1 -0
  6. package/dist/assets/{MarkdownRenderer-CJHSsWin.js → MarkdownRenderer-Ds61--LT.js} +2 -2
  7. package/dist/assets/{MarkdownRendererImpl-DwrHFGUp.js → MarkdownRendererImpl-DOMmiOH0.js} +1 -1
  8. package/dist/assets/{MarkstreamRendererImpl-CSnbQMu3.js → MarkstreamRendererImpl-F7MGg5ER.js} +1 -1
  9. package/dist/assets/{MobileDetailNavigation-BXr6AGLT.js → MobileDetailNavigation-DcmVOjkw.js} +1 -1
  10. package/dist/assets/{MobileShareBridge-D7OE9OP_.js → MobileShareBridge-C1Fc4ErB.js} +2 -2
  11. package/dist/assets/MobileSurface-BA7eS5Yd.js +2 -0
  12. package/dist/assets/{MultiRunWindow-DF3iIFCd.js → MultiRunWindow-BoTnH6Yp.js} +1 -1
  13. package/dist/assets/{OnboardingScreen-BjHmlwlw.js → OnboardingScreen-BxItVnnO.js} +1 -1
  14. package/dist/assets/{OverlayScrollbar-DLtWO_Gp.js → OverlayScrollbar-B4e0QD05.js} +1 -1
  15. package/dist/assets/SettingsView-CjesWNLw.js +25 -0
  16. package/dist/assets/{SettingsWindow-BI5swspb.js → SettingsWindow-CReMbg-w.js} +1 -1
  17. package/dist/assets/TerminalView-C64-QYOG.js +8 -0
  18. package/dist/assets/{ToolOutputDialog-BEWhBcRd.js → ToolOutputDialog-CJu8DNrt.js} +1 -1
  19. package/dist/assets/{appThemeRegistry-4MR0F7Jz.js → appThemeRegistry-CF4lIxNt.js} +1 -1
  20. package/dist/assets/{assistant-attachment-cache-C0aRnFkj.js → assistant-attachment-cache-DphJleHr.js} +1 -1
  21. package/dist/assets/{button-DKuz3L8l.js → button-CwXyux0a.js} +1 -1
  22. package/dist/assets/collapsible-BZ6_cYJd.js +1 -0
  23. package/dist/assets/context-menu-DHrRifPE.js +1 -0
  24. package/dist/assets/{desktopHosts-CoDkOnyS.js → desktopHosts-JGFqhlaM.js} +1 -1
  25. package/dist/assets/{desktopRelayRestore-DDlVKyOE.js → desktopRelayRestore-CHEwrI93.js} +1 -1
  26. package/dist/assets/{diagram-D7T8HQgj.js → diagram-BwnPNKWt.js} +1 -1
  27. package/dist/assets/{dialog-DQYl3W_o.js → dialog-Dp0lQv5K.js} +1 -1
  28. package/dist/assets/es-DicJ8lKD.js +15 -0
  29. package/dist/assets/{fileMentionTouchSelection-BZ6ovBYH.js → fileMentionTouchSelection-DPZRxNa2.js} +3 -4
  30. package/dist/assets/{fileQueries-JrpcsOCs.js → fileQueries-DN6gbLhM.js} +1 -1
  31. package/dist/assets/fr-lhgVcBW_.js +13 -0
  32. package/dist/assets/{gitApi-CM42qUed.js → gitApi-D8R7ic-T.js} +3 -3
  33. package/dist/assets/{imageSource-CIl0FL10.js → imageSource-BMztRu0c.js} +1 -1
  34. package/dist/assets/{input-CzQy8UhV.js → input-Biw8aNzR.js} +1 -1
  35. package/dist/assets/insertionBoundaries-DdwCD4S2.js +1 -0
  36. package/dist/assets/ja-B9EZwOcw.js +15 -0
  37. package/dist/assets/ko-Dxk-uJRb.js +15 -0
  38. package/dist/assets/main-BhlwwJ2c.js +169 -0
  39. package/dist/assets/main-D2BzpnUt.css +1 -0
  40. package/dist/assets/main-DSPCkT74.js +2 -0
  41. package/dist/assets/{markdownHeightCache-Cj1I5NDw.js → markdownHeightCache-BOOrGCuG.js} +1 -1
  42. package/dist/assets/markdownTheme-DM5h1i18.js +1 -0
  43. package/dist/assets/{mcpQueries-D5mQymgM.js → mcpQueries-CE9tk-Jz.js} +1 -1
  44. package/dist/assets/miniChat-Ckk9ABql.js +2 -0
  45. package/dist/assets/mobile-cLMRZqI9.js +2 -0
  46. package/dist/assets/multirun-DVnFRb1V.js +4 -0
  47. package/dist/assets/persistence-D0y7226-.js +3 -0
  48. package/dist/assets/pl-CFhwWLJj.js +15 -0
  49. package/dist/assets/platform-D_c-T5nO.js +6 -0
  50. package/dist/assets/projectMeta-CSQgHequ.js +3 -0
  51. package/dist/assets/pt-BR-Ba6GfZ6J.js +15 -0
  52. package/dist/assets/{relay-image-stream-DM7atqJs.js → relay-image-stream-DrdVj99c.js} +1 -1
  53. package/dist/assets/renderElectronMiniChatApp-gGFkwoL_.js +2 -0
  54. package/dist/assets/renderMobileApp-B2w50kAc.js +28 -0
  55. package/dist/assets/runtime-fetch-Fs9ZV2V8.js +1 -0
  56. package/dist/assets/{runtimeConfig-BIGBRGQI.js → runtimeConfig-BHH1hJDd.js} +1 -1
  57. package/dist/assets/runtimeEndpointReset-5uJ4sJk0.js +149 -0
  58. package/dist/assets/{sessionChangeSummary-eIbpSfrO.js → sessionChangeSummary-spf_atO8.js} +2 -2
  59. package/dist/assets/{sessionLookup-myHe5ltu.js → sessionLookup-BGjjwiCq.js} +1 -1
  60. package/dist/assets/{sortable-tabs-strip-BQtmHx9r.js → sortable-tabs-strip-CTY_wG46.js} +1 -1
  61. package/dist/assets/src-BjEyU6if.css +2 -0
  62. package/dist/assets/store-DmowK-GO.js +15 -0
  63. package/dist/assets/{terminalApi-BxcdiduX.js → terminalApi-Dhqqp4xi.js} +1 -1
  64. package/dist/assets/textarea-QxsmcBGj.js +1 -0
  65. package/dist/assets/{tooltip-Cx5gTbdY.js → tooltip-CpMLl5_k.js} +1 -1
  66. package/dist/assets/uk-ChchBzy6.js +15 -0
  67. package/dist/assets/{url-CDHnjlsm.js → url-CGbJbiJq.js} +1 -1
  68. package/dist/assets/{useAppFontEffects-CNeo0aZs.js → useAppFontEffects-B1h7ab_J.js} +55 -50
  69. package/dist/assets/{useAssistantUIStore-asNG6K7n.js → useAssistantUIStore-Bz4tRflF.js} +1 -1
  70. package/dist/assets/useDesktopWindowControlsLayout-Cxsx84NO.js +1 -0
  71. package/dist/assets/{useEffectiveDirectory-B4LvR8Gt.js → useEffectiveDirectory-DsqhqsJR.js} +1 -1
  72. package/dist/assets/{useFontPreferences-CUnQ4TVm.js → useFontPreferences-Xw1vSkmC.js} +1 -1
  73. package/dist/assets/useMobileNavigationStore-B1axS4g-.js +414 -0
  74. package/dist/assets/{useRuntimeAPIs-DwLtO0c_.js → useRuntimeAPIs-DSLXJybW.js} +1 -1
  75. package/dist/assets/useSessionAutoCleanup-B5FTG_qf.js +2 -0
  76. package/dist/assets/{useThemeSystem-BlNUWH47.js → useThemeSystem-DMzWVJaW.js} +1 -1
  77. package/dist/assets/useUIStore-syVmbuDA.js +1 -0
  78. package/dist/assets/{useWorkerHighlightedLines-CJZbJDW3.js → useWorkerHighlightedLines-DC_Bc-RV.js} +1 -1
  79. package/dist/assets/{vendor-base-ui-CDjyofeD.js → vendor-base-ui-CnHfUmDN.js} +1 -1
  80. package/dist/assets/{vendor-cmdk-BFH0UpXX.js → vendor-cmdk-CeAGfrLo.js} +1 -1
  81. package/dist/assets/{vendor-dnd-kit-accessibility-BrR3AKvM.js → vendor-dnd-kit-accessibility-BY5LhhUB.js} +1 -1
  82. package/dist/assets/{vendor-dnd-kit-core-CNfdvk6H.js → vendor-dnd-kit-core-CvWxUPdb.js} +1 -1
  83. package/dist/assets/{vendor-dnd-kit-sortable-DQUaTeGj.js → vendor-dnd-kit-sortable-hHdP9lLb.js} +1 -1
  84. package/dist/assets/{vendor-framer-motion-4dogsUk3.js → vendor-framer-motion-DFy_BSVd.js} +1 -1
  85. package/dist/assets/{vendor-legendapp-list-BU7MdPZR.js → vendor-legendapp-list-WE32lwNk.js} +1 -1
  86. package/dist/assets/{vendor-markstream-react-qHC2FykU.js → vendor-markstream-react-DX55XLGO.js} +3 -3
  87. package/dist/assets/vendor-opencode-client-C12gfjag.js +12 -0
  88. package/dist/assets/{vendor-pierre-diffs-CUfqC003.js → vendor-pierre-diffs-DX7XTPG3.js} +1 -1
  89. package/dist/assets/{vendor-react-C94A0AHl.js → vendor-react-DLPElpoQ.js} +1 -1
  90. package/dist/assets/{vendor-react-drawio-FVlWqKVE.js → vendor-react-drawio-ECtEwvbc.js} +1 -1
  91. package/dist/assets/{vendor-reactuses-core-C2A3EjUi.js → vendor-reactuses-core-B8parU1Y.js} +1 -1
  92. package/dist/assets/{vendor-sonner-DNqHmUQs.js → vendor-sonner-DVzioF3d.js} +1 -1
  93. package/dist/assets/{vendor-stream-diffs-CwbKsmlv.js → vendor-stream-diffs-DWy3xSmp.js} +2 -2
  94. package/dist/assets/{vendor-tanstack-react-query-D0iFK57C.js → vendor-tanstack-react-query-D7pys_c9.js} +1 -1
  95. package/dist/assets/{vendor-tanstack-react-virtual-B2kV6cnT.js → vendor-tanstack-react-virtual-tlDgH6PG.js} +1 -1
  96. package/dist/assets/{vendor-zustand-BqLKf6ZY.js → vendor-zustand-B4dlGEM2.js} +1 -1
  97. package/dist/assets/zh-CN-Z_bkUQk5.js +15 -0
  98. package/dist/assets/zh-TW-CZeTm7kd.js +15 -0
  99. package/dist/index.html +14 -14
  100. package/dist/mini-chat.html +14 -14
  101. package/dist/mobile.html +14 -14
  102. package/package.json +3 -2
  103. package/server/index.js +38 -7
  104. package/server/lib/browser-provider/DOCUMENTATION.md +141 -0
  105. package/server/lib/browser-provider/contract.js +312 -0
  106. package/server/lib/browser-provider/host.js +421 -0
  107. package/server/lib/browser-provider/host.test.js +304 -0
  108. package/server/lib/browser-provider/routes.js +122 -0
  109. package/server/lib/browser-provider/routes.test.js +120 -0
  110. package/server/lib/browser-provider/selection-store.js +43 -0
  111. package/server/lib/browser-provider/selection-store.test.js +35 -0
  112. package/server/lib/event-stream/DOCUMENTATION.md +3 -1
  113. package/server/lib/event-stream/reasoning-projection.js +16 -1
  114. package/server/lib/event-stream/reasoning-projection.test.js +15 -0
  115. package/server/lib/notifications/DOCUMENTATION.md +3 -1
  116. package/server/lib/notifications/runtime.js +128 -31
  117. package/server/lib/notifications/runtime.test.js +103 -0
  118. package/server/lib/notifications/template-runtime.js +130 -38
  119. package/server/lib/notifications/template-runtime.test.js +96 -0
  120. package/server/lib/opencode/DOCUMENTATION.md +8 -0
  121. package/server/lib/opencode/config-entity-routes.js +6 -1
  122. package/server/lib/opencode/config-entity-routes.test.js +25 -0
  123. package/server/lib/opencode/ensure-cli.d.ts +8 -0
  124. package/server/lib/opencode/feature-routes-runtime.js +10 -0
  125. package/server/lib/opencode/feature-routes-runtime.test.js +7 -0
  126. package/server/lib/opencode/proxy.js +6 -1
  127. package/server/lib/opencode/proxy.test.js +18 -3
  128. package/server/lib/opencode/routes.js +30 -0
  129. package/server/lib/opencode/upgrade-screen.d.ts +30 -0
  130. package/server/lib/opencode/upgrade-screen.js +299 -0
  131. package/server/lib/opencode/upgrade-screen.test.js +196 -0
  132. package/server/lib/session-metadata/DOCUMENTATION.md +27 -17
  133. package/server/lib/session-metadata/opencode-session-record.d.ts +57 -0
  134. package/server/lib/session-metadata/opencode-session-record.js +316 -0
  135. package/server/lib/session-metadata/opencode-session-record.test.js +242 -0
  136. package/server/lib/session-metadata/routes.js +5 -1
  137. package/server/lib/session-metadata/session-archive.js +1 -1
  138. package/server/lib/session-metadata/session-metadata-store.d.ts +2 -0
  139. package/server/lib/session-metadata/session-metadata-store.js +63 -16
  140. package/server/lib/session-metadata/session-projection.js +26 -2
  141. package/dist/assets/AssistantView-ClQAfGOT.js +0 -3
  142. package/dist/assets/Icon-BiEQEcih.js +0 -1
  143. package/dist/assets/MobileSurface-D2jUkX9l.js +0 -2
  144. package/dist/assets/SettingsView-BRxELjow.js +0 -25
  145. package/dist/assets/TerminalView-BU3x8-fr.js +0 -8
  146. package/dist/assets/collapsible-DXbDynN4.js +0 -3
  147. package/dist/assets/context-menu-CqolsLCm.js +0 -1
  148. package/dist/assets/es-CN3E_zXj.js +0 -15
  149. package/dist/assets/fr-9zZvQZD2.js +0 -13
  150. package/dist/assets/insertionBoundaries-CtkZ9720.js +0 -1
  151. package/dist/assets/ja-Dnjk_82N.js +0 -15
  152. package/dist/assets/ko-BxCEE99C.js +0 -15
  153. package/dist/assets/main-BILgBI3B.js +0 -169
  154. package/dist/assets/main-BeveiW6F.js +0 -2
  155. package/dist/assets/main-qSeeJRW1.css +0 -1
  156. package/dist/assets/markdownTheme-CKac3wKC.js +0 -1
  157. package/dist/assets/miniChat-BgrzM3u7.js +0 -2
  158. package/dist/assets/mobile-DtYoQE8C.js +0 -2
  159. package/dist/assets/multirun-jy8dEI96.js +0 -4
  160. package/dist/assets/persistence-C9LQpiOS.js +0 -3
  161. package/dist/assets/pl-CNNsWnPM.js +0 -15
  162. package/dist/assets/platform-BQghHRfp.js +0 -6
  163. package/dist/assets/pt-BR-BZB2DJMp.js +0 -15
  164. package/dist/assets/renderElectronMiniChatApp-BGrrmeME.js +0 -2
  165. package/dist/assets/renderMobileApp-u3c4GtFC.js +0 -28
  166. package/dist/assets/runtime-fetch-zIalUnbb.js +0 -1
  167. package/dist/assets/runtimeEndpointReset-D2savkuw.js +0 -147
  168. package/dist/assets/src-CLN7Z-zx.css +0 -2
  169. package/dist/assets/store-DLCiZaA6.js +0 -15
  170. package/dist/assets/textarea-CakKvqFw.js +0 -1
  171. package/dist/assets/uk-rwQrxeYM.js +0 -15
  172. package/dist/assets/useDesktopWindowControlsLayout-DBJ56qIo.js +0 -1
  173. package/dist/assets/useMobileNavigationStore-ZHJPxlHt.js +0 -414
  174. package/dist/assets/useSessionAutoCleanup-BIVmrnYU.js +0 -2
  175. package/dist/assets/useUIStore-CQYKJnrg.js +0 -1
  176. package/dist/assets/vendor-opencode-client-BOe2qpQb.js +0 -12
  177. package/dist/assets/zh-CN-BvmBDnkD.js +0 -15
  178. package/dist/assets/zh-TW-9wcHTX0y.js +0 -15
@@ -0,0 +1,299 @@
1
+ /**
2
+ * Startup upgrade screen for OpenCode below 2.0.15.
3
+ *
4
+ * This module does not own process lifecycle. It classifies the version the
5
+ * existing startup path already detected, and installs through the owned-cache
6
+ * installer (`installPinnedOpenCode2Cli`) plus the caller's restart hook.
7
+ * Success is a verified running version, never a download that has not started.
8
+ */
9
+
10
+ import {
11
+ PINNED_OPENCODE2_VERSION,
12
+ isAcceptableOpenCode2HealthVersion,
13
+ isOpenCode1xVersion,
14
+ isOpenCode2VersionAtLeast,
15
+ npmPackageForOpenCode2,
16
+ } from './opencode2-pin.js';
17
+
18
+ /** Oldest OpenCode this OpenChamber build will enter a session against. */
19
+ export const REQUIRED_OPENCODE_VERSION = '2.0.15';
20
+
21
+ const normalizeVersion = (value) => {
22
+ if (typeof value !== 'string') return null;
23
+ const trimmed = value.trim().replace(/^v/i, '');
24
+ return trimmed || null;
25
+ };
26
+
27
+ /**
28
+ * Install target is the pin when the pin already satisfies the screen, otherwise
29
+ * the screen minimum. A pin still at 2.0.12 must not be what this screen installs.
30
+ * @param {string} [pin]
31
+ */
32
+ export function resolveUpgradeScreenTarget(pin = PINNED_OPENCODE2_VERSION) {
33
+ if (isOpenCode2VersionAtLeast(pin, REQUIRED_OPENCODE_VERSION)) return pin;
34
+ return REQUIRED_OPENCODE_VERSION;
35
+ }
36
+
37
+ /** 2.x at or above the screen minimum. A future major is not this contract. */
38
+ function isUpgradeScreenSatisfied(version) {
39
+ const normalized = normalizeVersion(version);
40
+ if (!normalized || !isAcceptableOpenCode2HealthVersion(normalized)) return false;
41
+ return isOpenCode2VersionAtLeast(normalized, REQUIRED_OPENCODE_VERSION);
42
+ }
43
+
44
+ function isOlderThanUpgradeScreenMinimum(version) {
45
+ const normalized = normalizeVersion(version);
46
+ if (!normalized) return false;
47
+ if (isOpenCode1xVersion(normalized)) return true;
48
+ if (!isAcceptableOpenCode2HealthVersion(normalized)) return false;
49
+ return !isOpenCode2VersionAtLeast(normalized, REQUIRED_OPENCODE_VERSION);
50
+ }
51
+
52
+ /**
53
+ * @param {string | null | undefined} ownership
54
+ * @returns {'managed' | 'external' | 'bundled'}
55
+ */
56
+ function classifyUpgradeScreenInstallation(ownership) {
57
+ if (ownership === 'external-serve') return 'external';
58
+ if (ownership === 'bundled') return 'bundled';
59
+ return 'managed';
60
+ }
61
+
62
+ export function platformSupportsUpgradeScreenInstall(platform = process.platform, arch = process.arch) {
63
+ try {
64
+ npmPackageForOpenCode2(platform, arch);
65
+ return true;
66
+ } catch {
67
+ return false;
68
+ }
69
+ }
70
+
71
+ const pickDetectedVersion = (serveVersion, cliVersion) => {
72
+ const serve = normalizeVersion(serveVersion);
73
+ const cli = normalizeVersion(cliVersion);
74
+ if (serve && !isUpgradeScreenSatisfied(serve)) return serve;
75
+ if (serve) return serve;
76
+ return cli;
77
+ };
78
+
79
+ /**
80
+ * @param {{
81
+ * version?: string | null,
82
+ * installation?: 'managed' | 'external' | 'bundled',
83
+ * platformCanInstall?: boolean,
84
+ * guidance?: string | null,
85
+ * pin?: string,
86
+ * }} input
87
+ */
88
+ export function describeUpgradeScreen(input = {}) {
89
+ const version = normalizeVersion(input.version);
90
+ const installation = input.installation === 'external' || input.installation === 'bundled'
91
+ ? input.installation
92
+ : 'managed';
93
+ const state = version == null
94
+ ? 'unavailable'
95
+ : isUpgradeScreenSatisfied(version)
96
+ ? 'compatible'
97
+ : 'incompatible';
98
+ const older = version != null && isOlderThanUpgradeScreenMinimum(version);
99
+ // External processes are not replaced. Bundled and managed locals can receive
100
+ // an owned-cache install; that does not overwrite the user's other binaries.
101
+ const canInstall = state === 'incompatible'
102
+ && older
103
+ && installation !== 'external'
104
+ && input.platformCanInstall === true;
105
+ const guidance = canInstall
106
+ ? null
107
+ : (typeof input.guidance === 'string' && input.guidance.trim() ? input.guidance.trim() : null);
108
+ return {
109
+ state,
110
+ version,
111
+ installation,
112
+ minimumVersion: REQUIRED_OPENCODE_VERSION,
113
+ targetVersion: resolveUpgradeScreenTarget(input.pin),
114
+ canInstall,
115
+ guidance,
116
+ };
117
+ }
118
+
119
+ /**
120
+ * A running serve below the minimum, or a missing version, is not an upgrade.
121
+ * @param {{ targetVersion?: string | null, serveVersion?: string | null }} input
122
+ */
123
+ export function evaluateUpgradeScreenInstallResult(input = {}) {
124
+ const targetVersion = resolveUpgradeScreenTarget(input.targetVersion || REQUIRED_OPENCODE_VERSION);
125
+ const version = normalizeVersion(input.serveVersion);
126
+ if (!version || !isUpgradeScreenSatisfied(version) || !isOpenCode2VersionAtLeast(version, targetVersion)) {
127
+ const found = version || 'unknown';
128
+ return {
129
+ ok: false,
130
+ upgraded: false,
131
+ version,
132
+ targetVersion,
133
+ errorCode: 'UPGRADE_SCREEN_NOT_VERIFIED',
134
+ error: `OpenCode is still ${found}. OpenChamber requires ${REQUIRED_OPENCODE_VERSION} or newer.`,
135
+ };
136
+ }
137
+ return {
138
+ ok: true,
139
+ upgraded: true,
140
+ version,
141
+ targetVersion,
142
+ error: null,
143
+ errorCode: null,
144
+ };
145
+ }
146
+
147
+ async function readUpgradeScreenStatus(deps) {
148
+ const ownership = typeof deps.resolveOwnership === 'function'
149
+ ? await deps.resolveOwnership()
150
+ : { ownership: 'unknown', guidance: null };
151
+ const installation = classifyUpgradeScreenInstallation(ownership?.ownership);
152
+ const serveVersion = typeof deps.readServeVersion === 'function'
153
+ ? await Promise.resolve(deps.readServeVersion()).catch(() => null)
154
+ : null;
155
+ const cliVersion = typeof deps.readCliVersion === 'function'
156
+ ? await Promise.resolve(deps.readCliVersion()).catch(() => null)
157
+ : null;
158
+ return describeUpgradeScreen({
159
+ version: pickDetectedVersion(serveVersion, cliVersion),
160
+ installation,
161
+ platformCanInstall: typeof deps.platformCanInstall === 'boolean'
162
+ ? deps.platformCanInstall
163
+ : platformSupportsUpgradeScreenInstall(deps.platform, deps.arch),
164
+ guidance: ownership?.guidance ?? null,
165
+ pin: deps.pin,
166
+ });
167
+ }
168
+
169
+ const failure = (message, code, status = 500) => {
170
+ const error = new Error(message);
171
+ error.code = code;
172
+ error.status = status;
173
+ return error;
174
+ };
175
+
176
+ /**
177
+ * Install the screen target into the owned cache and restart through the
178
+ * caller's existing lifecycle hooks. Restores the previous binary selection
179
+ * when verification fails. Never returns upgraded:true for a failed install.
180
+ * @param {object} deps
181
+ */
182
+ export async function installRequiredOpenCode(deps) {
183
+ const status = await readUpgradeScreenStatus(deps);
184
+ if (status.state !== 'incompatible' || status.canInstall !== true) {
185
+ throw failure(
186
+ status.guidance || 'Automatic OpenCode installation is unavailable for this runtime.',
187
+ 'UPGRADE_SCREEN_UNAVAILABLE',
188
+ 409,
189
+ );
190
+ }
191
+ const target = status.targetVersion;
192
+ const previousBinary = typeof deps.getResolvedBinary === 'function' ? deps.getResolvedBinary() : null;
193
+ const previousSource = typeof deps.getResolvedBinarySource === 'function' ? deps.getResolvedBinarySource() : null;
194
+ const previousSetting = typeof deps.readConfiguredBinary === 'function'
195
+ ? await deps.readConfiguredBinary()
196
+ : undefined;
197
+ let persisted = false;
198
+ try {
199
+ if (typeof deps.install !== 'function') {
200
+ throw failure('OpenCode install is not configured.', 'UPGRADE_SCREEN_INSTALL_UNAVAILABLE', 500);
201
+ }
202
+ const installedPath = await deps.install({ version: target });
203
+ const diskVersion = typeof deps.readBinaryVersion === 'function'
204
+ ? await Promise.resolve(deps.readBinaryVersion(installedPath))
205
+ : null;
206
+ if (diskVersion !== target) {
207
+ throw failure(
208
+ `Installed OpenCode version mismatch: expected ${target}, got ${diskVersion || 'unknown'}`,
209
+ 'UPGRADE_SCREEN_BINARY_MISMATCH',
210
+ 500,
211
+ );
212
+ }
213
+ if (typeof deps.persistBinary === 'function') {
214
+ await deps.persistBinary(installedPath);
215
+ persisted = true;
216
+ }
217
+ if (typeof deps.forceBinary === 'function') {
218
+ deps.forceBinary(installedPath, 'installed');
219
+ }
220
+ if (typeof deps.restart === 'function') {
221
+ const shared = typeof deps.isSharedService === 'function' && deps.isSharedService() === true;
222
+ await deps.restart(shared ? { binaryPath: installedPath } : undefined);
223
+ }
224
+ if (typeof deps.waitReady === 'function') {
225
+ await deps.waitReady();
226
+ }
227
+ const serveVersion = typeof deps.readServeVersion === 'function'
228
+ ? await deps.readServeVersion()
229
+ : null;
230
+ const verification = evaluateUpgradeScreenInstallResult({ targetVersion: target, serveVersion });
231
+ if (!verification.ok) {
232
+ throw failure(verification.error, verification.errorCode, 500);
233
+ }
234
+ return verification;
235
+ } catch (error) {
236
+ if (persisted && typeof deps.persistBinary === 'function') {
237
+ try {
238
+ await deps.persistBinary(typeof previousSetting === 'string' ? previousSetting : '');
239
+ } catch {
240
+ // Keep the install error. A failed restore must not become success.
241
+ }
242
+ }
243
+ if (previousBinary && typeof deps.forceBinary === 'function') {
244
+ try {
245
+ deps.forceBinary(previousBinary, previousSource || 'discovered');
246
+ } catch {
247
+ // Same: report the install failure, not a restored-success.
248
+ }
249
+ }
250
+ throw error;
251
+ }
252
+ }
253
+
254
+ export function registerUpgradeScreenRoutes(app, deps) {
255
+ app.get('/api/opencode/compatibility', async (_req, res) => {
256
+ try {
257
+ res.json(await readUpgradeScreenStatus(deps));
258
+ } catch (error) {
259
+ res.status(503).json({
260
+ error: error instanceof Error ? error.message : 'Could not check OpenCode compatibility',
261
+ });
262
+ }
263
+ });
264
+
265
+ let inFlight = null;
266
+ app.post('/api/opencode/install-required', async (_req, res) => {
267
+ try {
268
+ if (!inFlight) {
269
+ inFlight = installRequiredOpenCode(deps).finally(() => {
270
+ inFlight = null;
271
+ });
272
+ }
273
+ const result = await inFlight;
274
+ if (!result || result.ok !== true || result.upgraded !== true || !result.version) {
275
+ return res.status(500).json({
276
+ success: false,
277
+ upgraded: false,
278
+ error: result?.error || 'OpenCode installation did not verify a new version',
279
+ errorCode: result?.errorCode || 'UPGRADE_SCREEN_NOT_VERIFIED',
280
+ version: result?.version ?? null,
281
+ });
282
+ }
283
+ return res.json({
284
+ success: true,
285
+ upgraded: true,
286
+ version: result.version,
287
+ targetVersion: result.targetVersion,
288
+ });
289
+ } catch (error) {
290
+ const status = Number.isInteger(error?.status) ? error.status : 500;
291
+ return res.status(status).json({
292
+ success: false,
293
+ upgraded: false,
294
+ error: error instanceof Error ? error.message : 'OpenCode installation failed',
295
+ errorCode: error?.code || 'UPGRADE_SCREEN_FAILED',
296
+ });
297
+ }
298
+ });
299
+ }
@@ -0,0 +1,196 @@
1
+ import express from 'express';
2
+ import request from 'supertest';
3
+ import { describe, expect, it, vi } from 'vitest';
4
+ import {
5
+ REQUIRED_OPENCODE_VERSION,
6
+ describeUpgradeScreen,
7
+ evaluateUpgradeScreenInstallResult,
8
+ installRequiredOpenCode,
9
+ registerUpgradeScreenRoutes,
10
+ resolveUpgradeScreenTarget,
11
+ } from './upgrade-screen.js';
12
+
13
+ describe('upgrade screen version gate', () => {
14
+ it('uses 2.0.15 even when the runtime pin is older', () => {
15
+ expect(REQUIRED_OPENCODE_VERSION).toBe('2.0.15');
16
+ expect(resolveUpgradeScreenTarget('2.0.12')).toBe('2.0.15');
17
+ expect(resolveUpgradeScreenTarget('2.0.16')).toBe('2.0.16');
18
+ });
19
+
20
+ it('does not open the gate at or above 2.0.15', () => {
21
+ expect(describeUpgradeScreen({
22
+ version: '2.0.15',
23
+ installation: 'managed',
24
+ platformCanInstall: true,
25
+ })).toMatchObject({ state: 'compatible', canInstall: false, minimumVersion: '2.0.15' });
26
+ expect(describeUpgradeScreen({
27
+ version: 'v2.0.16',
28
+ installation: 'managed',
29
+ platformCanInstall: true,
30
+ }).state).toBe('compatible');
31
+ });
32
+
33
+ it('opens the gate below 2.0.15 and offers install only when this runtime can switch', () => {
34
+ expect(describeUpgradeScreen({
35
+ version: '2.0.14',
36
+ installation: 'managed',
37
+ platformCanInstall: true,
38
+ })).toMatchObject({ state: 'incompatible', canInstall: true, version: '2.0.14' });
39
+ expect(describeUpgradeScreen({
40
+ version: '1.18.30',
41
+ installation: 'managed',
42
+ platformCanInstall: true,
43
+ })).toMatchObject({ state: 'incompatible', canInstall: true });
44
+ expect(describeUpgradeScreen({
45
+ version: '2.0.14',
46
+ installation: 'external',
47
+ platformCanInstall: true,
48
+ }).canInstall).toBe(false);
49
+ expect(describeUpgradeScreen({
50
+ version: '2.0.14',
51
+ installation: 'bundled',
52
+ platformCanInstall: true,
53
+ })).toMatchObject({ state: 'incompatible', canInstall: true, installation: 'bundled' });
54
+ expect(describeUpgradeScreen({
55
+ version: '2.0.14',
56
+ installation: 'managed',
57
+ platformCanInstall: false,
58
+ }).canInstall).toBe(false);
59
+ });
60
+
61
+ it('does not treat a missing version as an old install', () => {
62
+ expect(describeUpgradeScreen({
63
+ version: null,
64
+ installation: 'managed',
65
+ platformCanInstall: true,
66
+ }).state).toBe('unavailable');
67
+ });
68
+
69
+ it('does not report an unverified install as upgraded', () => {
70
+ expect(evaluateUpgradeScreenInstallResult({
71
+ targetVersion: '2.0.15',
72
+ serveVersion: '2.0.14',
73
+ })).toMatchObject({ ok: false, upgraded: false, version: '2.0.14' });
74
+ expect(evaluateUpgradeScreenInstallResult({
75
+ targetVersion: '2.0.15',
76
+ serveVersion: null,
77
+ }).upgraded).toBe(false);
78
+ expect(evaluateUpgradeScreenInstallResult({
79
+ targetVersion: '2.0.15',
80
+ serveVersion: '2.0.15',
81
+ })).toMatchObject({ ok: true, upgraded: true, version: '2.0.15' });
82
+ });
83
+ });
84
+
85
+ describe('installRequiredOpenCode', () => {
86
+ const createDeps = (overrides = {}) => {
87
+ const calls = [];
88
+ const deps = {
89
+ resolveOwnership: async () => ({ ownership: 'global-cli', guidance: null }),
90
+ readServeVersion: async () => '2.0.14',
91
+ readCliVersion: () => '2.0.14',
92
+ platformCanInstall: true,
93
+ install: vi.fn(async () => {
94
+ calls.push('install');
95
+ return '/cache/opencode';
96
+ }),
97
+ readBinaryVersion: vi.fn(() => '2.0.15'),
98
+ persistBinary: vi.fn(async () => {
99
+ calls.push('persist');
100
+ }),
101
+ forceBinary: vi.fn(() => {
102
+ calls.push('force');
103
+ }),
104
+ restart: vi.fn(async () => {
105
+ calls.push('restart');
106
+ }),
107
+ waitReady: vi.fn(async () => {
108
+ calls.push('ready');
109
+ }),
110
+ readConfiguredBinary: async () => null,
111
+ isSharedService: () => false,
112
+ ...overrides,
113
+ };
114
+ return { deps, calls };
115
+ };
116
+
117
+ it('installs 2.0.15 and only succeeds after the running serve is verified', async () => {
118
+ const { deps, calls } = createDeps({
119
+ readServeVersion: vi.fn()
120
+ .mockResolvedValueOnce('2.0.14')
121
+ .mockResolvedValueOnce('2.0.15'),
122
+ });
123
+
124
+ await expect(installRequiredOpenCode(deps)).resolves.toMatchObject({
125
+ ok: true,
126
+ upgraded: true,
127
+ version: '2.0.15',
128
+ targetVersion: '2.0.15',
129
+ });
130
+ expect(deps.install).toHaveBeenCalledWith({ version: '2.0.15' });
131
+ expect(calls).toEqual(['install', 'persist', 'force', 'restart', 'ready']);
132
+ });
133
+
134
+ it('returns the failure reason and does not claim an upgrade when verification fails', async () => {
135
+ const { deps } = createDeps({
136
+ readServeVersion: vi.fn()
137
+ .mockResolvedValueOnce('1.18.30')
138
+ .mockResolvedValueOnce('1.18.30'),
139
+ readCliVersion: () => '1.18.30',
140
+ readBinaryVersion: () => '2.0.15',
141
+ });
142
+
143
+ await expect(installRequiredOpenCode(deps)).rejects.toThrow(/still 1\.18\.30/);
144
+ expect(deps.persistBinary).toHaveBeenCalledWith('');
145
+ });
146
+
147
+ it('does not install when the runtime cannot switch', async () => {
148
+ const { deps } = createDeps({
149
+ resolveOwnership: async () => ({
150
+ ownership: 'external-serve',
151
+ guidance: 'Restart that server yourself.',
152
+ }),
153
+ });
154
+
155
+ await expect(installRequiredOpenCode(deps)).rejects.toMatchObject({
156
+ status: 409,
157
+ message: 'Restart that server yourself.',
158
+ });
159
+ expect(deps.install).not.toHaveBeenCalled();
160
+ });
161
+ });
162
+
163
+ describe('upgrade screen routes', () => {
164
+ it('hides the gate for 2.0.15 and reports install failure without upgraded:true', async () => {
165
+ const app = express();
166
+ app.use(express.json());
167
+ const readServeVersion = vi.fn()
168
+ .mockResolvedValueOnce('2.0.15')
169
+ .mockResolvedValueOnce('2.0.12')
170
+ .mockResolvedValueOnce('2.0.12');
171
+ registerUpgradeScreenRoutes(app, {
172
+ resolveOwnership: async () => ({ ownership: 'owned-cache', guidance: null }),
173
+ readServeVersion,
174
+ readCliVersion: () => '2.0.12',
175
+ platformCanInstall: true,
176
+ install: vi.fn(async () => {
177
+ throw new Error('registry unreachable');
178
+ }),
179
+ readBinaryVersion: () => '2.0.15',
180
+ persistBinary: vi.fn(),
181
+ forceBinary: vi.fn(),
182
+ restart: vi.fn(),
183
+ });
184
+
185
+ const compatible = await request(app).get('/api/opencode/compatibility').expect(200);
186
+ expect(compatible.body).toMatchObject({ state: 'compatible', version: '2.0.15', minimumVersion: '2.0.15' });
187
+
188
+ const failed = await request(app).post('/api/opencode/install-required').expect(500);
189
+ expect(failed.body).toMatchObject({
190
+ success: false,
191
+ upgraded: false,
192
+ error: 'registry unreachable',
193
+ });
194
+ expect(failed.body.upgraded).not.toBe(true);
195
+ });
196
+ });
@@ -1,32 +1,42 @@
1
1
  # Session metadata store
2
2
 
3
- OpenChamber-owned per-session metadata for Host features that used to live on
4
- OpenCode `session.metadata`.
3
+ OpenChamber session metadata whose durable copy lives on the OpenCode session
4
+ record. The side JSON file is the cache and the pre-migration source.
5
5
 
6
6
  ## Why
7
7
 
8
- Session goal progress, Host archive stamps, and later other Host state under the
9
- `openchamber` namespace are OpenChamber product state. The Host owns their
10
- authority: it serializes conditional writes, publishes only persisted values,
11
- and protects archive fields from generic patches. They therefore live in this
12
- Host store and are folded back onto proxy session list/detail/SSE responses so
13
- clients still read `session.metadata` and `time.archived`.
8
+ Session goal progress, Host archive stamps, titles, and other Host state under
9
+ the `openchamber` namespace travel with the OpenCode session. `session.update`
10
+ (`PATCH /api/session/:id`, body `{ title?, metadata?, permissions? }`) replaces
11
+ `metadata` wholesale. Archive is `metadata.openchamber.archive.archivedAt` on
12
+ that record and is still projected onto `time.archived`. Title is the record's
13
+ `title` field and is not sent when only metadata changes.
14
14
 
15
- Upstream capability is tracked per version and does not change that ownership:
15
+ `opencode-session-record.js` is the only module that knows that call. A client
16
+ whose types omit `session.update` metadata still goes through that module;
17
+ a missing API throws and is never an empty write.
18
+
19
+ The side file remains until each row has been copied or OpenCode has confirmed
20
+ the session is gone. Until that load succeeds, the file is not an authoritative
21
+ empty catalog: a failed read stays unwritable and does not clear other sessions.
22
+ A session OpenCode still has is listed even when the side file has no row.
23
+ One failed copy does not delete the other rows or the other OpenCode sessions.
24
+
25
+ Fork-specific fields (`goal`, `assistant`, `scheduledTask`, `llm`, `smallModel`,
26
+ and the rest of `openchamber`) are merged onto the record. They are not dropped
27
+ to match a narrower upstream metadata shape.
16
28
 
17
29
  | OpenCode client | Session metadata update |
18
30
  |---|---|
19
31
  | Earlier 2.x releases used during the migration | `metadata` accepted at create time only |
20
- | 2.0.14 | `PATCH /api/session/:id` with body `{ title, metadata, permissions }` (`session.update`) |
21
-
22
- Adopting the upstream update API for Host-owned fields requires a separate
23
- decision about authority, conditional writes, and archive protection.
32
+ | 2.0.14+ / 2.0.15 | `PATCH /api/session/:id` with body `{ title, metadata, permissions }` (`session.update`) |
24
33
 
25
34
  ## Layout
26
35
 
27
36
  | File | Role |
28
37
  |---|---|
29
- | `session-metadata-store.js` | Durable store: one JSON file per data dir, `{ [sessionID]: metadata }`, RFC 7386 merge patch, full RMW serialization, refuse-failed-read refuses write/read-as-empty |
38
+ | `opencode-session-record.js` | Read/write title and metadata on the OpenCode session record; migrate the side file without treating a failed load as empty or dropping other sessions |
39
+ | `session-metadata-store.js` | Side-file cache and migration source: `{ [sessionID]: metadata }`, RFC 7386 merge patch, full RMW serialization, refuse-failed-read refuses write/read-as-empty. When a record reader/writer is wired, commits the merged object onto the OpenCode session first |
30
40
  | `session-projection.js` | Pure Host authority projection (metadata deep-merge + archive → `time.archived`) |
31
41
  | `session-archive.js` | Domain archive/unarchive: validate upstream session + directory, persist, project, best-effort index/broadcast |
32
42
  | `system-session.js` | Isolation patches for system session creators: `buildScheduledTaskMetadata`, `buildLlmSessionMetadata`, `buildAssistantSessionMetadata`. Title prefixes are labels and never ownership. |
@@ -59,19 +69,19 @@ decision about authority, conditional writes, and archive protection.
59
69
 
60
70
  Wired from `packages/web/server/index.js` + `feature-routes-runtime.js`:
61
71
 
62
- 1. `createSessionMetadataStore({ dataDir: OPENCHAMBER_DATA_DIR })` + initial `load()`
72
+ 1. `createHttpSessionRecordClient` + `createSessionMetadataStore({ dataDir, recordReader, recordWriter })` + initial `load()`, then `startSideStoreMigration`
63
73
  2. proxy `getStoredSessionMetadata` / `getStoredSessionMetadataSync` → list/get + direct SSE overlay
64
74
  3. global hub `projectOutboundSessionPayload` → live + replay session lifecycle authority
65
75
  4. session-index sync `projectSessions` + event-ingest `projectSession` / `onSessionDeleted`
66
76
  5. `createSessionArchiveService` after session index exists
67
77
  6. session-goal `readSessionMetadata` / `mutateSessionMetadata` / `persistSessionGoal`
68
78
  7. `registerSessionMetadataRoutes` (metadata + archive)
69
- 8. assistants `archiveSessionHost` → shared Host archive (not upstream `session.update`)
79
+ 8. assistants `archiveSessionHost` → shared Host archive, which write-throughs `metadata` onto the OpenCode session record without clearing `title`
70
80
  9. system session creators persist isolation through `persistSessionMetadata`
71
81
  (`setSessionMetadata` + `openchamber:session-metadata` broadcast) and then
72
82
  `onSystemSessionPersisted` (index upsert that removes the row). Wired from
73
83
  `index.js` into scheduled-task runtime, LLM attachment generate, and
74
- Assistant binding create. OpenCode create-time `metadata` is a hint only.
84
+ Assistant binding create. Create-time `metadata` is merged onto the session record; it is not a second authority.
75
85
 
76
86
  Capability is open (`supported: true`). Manual UI metadata writes, session-goal
77
87
  routes, and scheduled-task goal create all persist `openchamber.goal` through
@@ -0,0 +1,57 @@
1
+ export function mergeOwnedMetadata(
2
+ openCodeMetadata: unknown,
3
+ sideMetadata: unknown,
4
+ ): Record<string, unknown>;
5
+
6
+ export function writeSessionRecordViaClient(
7
+ client: { session?: { update?: (input: unknown, requestOptions?: unknown) => Promise<unknown> } } | null | undefined,
8
+ input: {
9
+ sessionID: string;
10
+ metadata?: Record<string, unknown>;
11
+ title?: string;
12
+ requestOptions?: object;
13
+ },
14
+ ): Promise<void>;
15
+
16
+ export function readSessionRecordViaClient(
17
+ client: { session?: { get?: (input: unknown, requestOptions?: unknown) => Promise<unknown> } } | null | undefined,
18
+ input: { sessionID: string; requestOptions?: object },
19
+ ): Promise<Record<string, unknown> | null>;
20
+
21
+ export function createHttpSessionRecordClient(options: {
22
+ buildSessionUrl: (sessionID: string, directory?: string | null) => string;
23
+ getHeaders?: () => Record<string, string>;
24
+ fetchFn?: typeof fetch;
25
+ }): {
26
+ read: (sessionID: string, directory?: string | null) => Promise<Record<string, unknown> | null>;
27
+ write: (input: {
28
+ sessionID: string;
29
+ metadata?: Record<string, unknown>;
30
+ title?: string;
31
+ directory?: string | null;
32
+ }) => Promise<void>;
33
+ };
34
+
35
+ export function migrateLoadedSideStore(input: {
36
+ loadResult: { ok?: boolean; entries?: Record<string, object> | null } | null;
37
+ readSession: (sessionID: string) => Promise<object | null>;
38
+ writeMetadata: (sessionID: string, metadata: object, record: object) => Promise<void>;
39
+ }): Promise<{
40
+ status: 'unavailable' | 'partial' | 'complete';
41
+ migrated: string[];
42
+ failed: Array<{ id: string; error: string }>;
43
+ absent: string[];
44
+ }>;
45
+
46
+ export function retainOpenCodeSessions(
47
+ openCodeSessions: unknown,
48
+ sideById: Record<string, object> | null | undefined,
49
+ ): unknown[];
50
+
51
+ export function startSideStoreMigration(options: {
52
+ load: () => Promise<{ ok?: boolean; entries?: Record<string, object> }>;
53
+ readSession: (sessionID: string) => Promise<object | null>;
54
+ writeMetadata: (sessionID: string, metadata: object, record: object) => Promise<void>;
55
+ setTimer?: typeof setTimeout;
56
+ clearTimer?: typeof clearTimeout;
57
+ }): () => void;