@openchambery/web 1.19.20 → 2.0.0-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 (189) hide show
  1. package/README.md +13 -11
  2. package/bin/cli-opencode.test.js +208 -0
  3. package/bin/cli.js +62 -9
  4. package/bin/lib/cli-startup.js +105 -2
  5. package/bin/lib/cli-startup.test.js +165 -0
  6. package/dist/assets/{AgentAvatar-NT957MuI.js → AgentAvatar-CLWWNu_Q.js} +1 -1
  7. package/dist/assets/AssistantView-BTCkm8yk.js +3 -0
  8. package/dist/assets/{DiagramView-C_JfbxNl.js → DiagramView-xuSJTnv7.js} +1 -1
  9. package/dist/assets/{FadeInOnReveal-CilRl4gc.js → FadeInOnReveal-Ov0D96d0.js} +1 -1
  10. package/dist/assets/{Icon-i5GiQRLq.js → Icon-DD-BuMfZ.js} +1 -1
  11. package/dist/assets/MarkdownRenderer-bc_p1H7l.js +2 -0
  12. package/dist/assets/{MarkdownRendererImpl-B1iUwzOz.js → MarkdownRendererImpl-SKi4oG0c.js} +1 -1
  13. package/dist/assets/{MarkstreamRendererImpl-BPtMj3-X.js → MarkstreamRendererImpl-CnkodGJW.js} +1 -1
  14. package/dist/assets/MobileDetailNavigation-Bnx59U_J.js +1 -0
  15. package/dist/assets/{MobileShareBridge-CKClHsWN.js → MobileShareBridge-BQlP4J-F.js} +1 -1
  16. package/dist/assets/MobileSurface-BXLeiZ4d.js +2 -0
  17. package/dist/assets/{MultiRunWindow-DiA24bnN.js → MultiRunWindow-ChZGob0v.js} +1 -1
  18. package/dist/assets/{OnboardingScreen-BGRLk4Wi.js → OnboardingScreen-DtQJxZG-.js} +1 -1
  19. package/dist/assets/{OverlayScrollbar-NuQI58XD.js → OverlayScrollbar-pMwxaCca.js} +1 -1
  20. package/dist/assets/SettingsView-B5I0KDTZ.js +25 -0
  21. package/dist/assets/{SettingsWindow-XlzRSS7E.js → SettingsWindow-DfnsFa8y.js} +1 -1
  22. package/dist/assets/{TerminalView-DdIaaLba.js → TerminalView-Dh9MWCpS.js} +1 -1
  23. package/dist/assets/{ToolOutputDialog-BbwuLdXU.js → ToolOutputDialog-BejmjrRk.js} +1 -1
  24. package/dist/assets/{appThemeRegistry-ChzfD7mc.js → appThemeRegistry-COGfxTFV.js} +1 -1
  25. package/dist/assets/{button-DtzFDvij.js → button-D-rATN7w.js} +1 -1
  26. package/dist/assets/{collapsible-D-nzhcyF.js → collapsible-BXRd5_Hr.js} +1 -1
  27. package/dist/assets/{context-menu-BzVSzHLG.js → context-menu-jGtH6fR9.js} +1 -1
  28. package/dist/assets/{diagram-2YWalSb5.js → diagram-DQf7Y_uR.js} +1 -1
  29. package/dist/assets/{dialog-DGV7DsZC.js → dialog-bfbvR4VH.js} +1 -1
  30. package/dist/assets/es-c52LTHHe.js +15 -0
  31. package/dist/assets/{fileMentionTouchSelection-DyjmxUEN.js → fileMentionTouchSelection-BvrNCroG.js} +1 -1
  32. package/dist/assets/{fr-DsBZeN8Y.js → fr-PisFbUE8.js} +3 -3
  33. package/dist/assets/gitApi-ButwzQ0N.js +9 -0
  34. package/dist/assets/{input-CbB2HdLg.js → input-8r6F_Fza.js} +1 -1
  35. package/dist/assets/insertionBoundaries-DGs7S5zv.js +1 -0
  36. package/dist/assets/ja-C8Ikun3Z.js +15 -0
  37. package/dist/assets/ko-ClFys5o2.js +15 -0
  38. package/dist/assets/main-CCE_4y5F.js +2 -0
  39. package/dist/assets/main-Dn9rpsgl.js +169 -0
  40. package/dist/assets/{markdownHeightCache-C9ym9Tsb.js → markdownHeightCache-1Qo6y6IJ.js} +1 -1
  41. package/dist/assets/miniChat-BpjxVqgg.js +2 -0
  42. package/dist/assets/mobile-nMJCaNq1.js +2 -0
  43. package/dist/assets/{multirun-8fkF0Ji_.js → multirun-BvSG94mW.js} +1 -1
  44. package/dist/assets/pl-CnaKWN5F.js +15 -0
  45. package/dist/assets/platform-CWTIp02q.js +4 -0
  46. package/dist/assets/{projectMeta--y6tyjrv.js → projectMeta-DOe49dM_.js} +2 -2
  47. package/dist/assets/pt-BR-s8OpfRI6.js +15 -0
  48. package/dist/assets/renderElectronMiniChatApp-R6dKh79f.js +2 -0
  49. package/dist/assets/renderMobileApp-y-S8y8v_.js +28 -0
  50. package/dist/assets/{runtimeConfig-DYWtBrkW.js → runtimeConfig-BMvqa5we.js} +1 -1
  51. package/dist/assets/{runtimeEndpointReset-BSWg99jc.js → runtimeEndpointReset-B6BxhvR0.js} +5 -5
  52. package/dist/assets/{sessionChangeSummary-BwupJF-Q.js → sessionChangeSummary-VVBlEXuh.js} +1 -1
  53. package/dist/assets/{sessionLookup-DP2X1qRR.js → sessionLookup-gh-hVRIc.js} +1 -1
  54. package/dist/assets/{sortable-tabs-strip-CfgEJfB0.js → sortable-tabs-strip-CudoSgV4.js} +1 -1
  55. package/dist/assets/{src-DZ60kHBP.css → src-BzX5UI-c.css} +1 -1
  56. package/dist/assets/{store-B6dImdlY.js → store-DJczmczv.js} +4 -4
  57. package/dist/assets/{terminalApi-xjqssKke.js → terminalApi-DiF6JmBP.js} +1 -1
  58. package/dist/assets/{textarea-BnD799LW.js → textarea-3XR8sUfy.js} +1 -1
  59. package/dist/assets/{tooltip-Djn7MSOX.js → tooltip-CQvpK-UV.js} +1 -1
  60. package/dist/assets/uk-CaX0iqkB.js +15 -0
  61. package/dist/assets/{useAppFontEffects-DWLxpWlS.js → useAppFontEffects-Cpmpo7Wb.js} +45 -45
  62. package/dist/assets/{useDesktopWindowControlsLayout-BGPR_rq6.js → useDesktopWindowControlsLayout-B1GX59-L.js} +1 -1
  63. package/dist/assets/{useEffectiveDirectory-CbV9_r1s.js → useEffectiveDirectory-CN6JoCFw.js} +1 -1
  64. package/dist/assets/{useFontPreferences-B1JYyzpK.js → useFontPreferences-DcbXQOkc.js} +1 -1
  65. package/dist/assets/useMobileNavigationStore-BVjFtXSy.js +414 -0
  66. package/dist/assets/{useSessionAutoCleanup-Dnors7hB.js → useSessionAutoCleanup-BLjymaFt.js} +1 -1
  67. package/dist/assets/{useWorkerHighlightedLines-DQqqrOBU.js → useWorkerHighlightedLines-Bqn4AiBL.js} +1 -1
  68. package/dist/assets/vendor-opencode-client-BOe2qpQb.js +12 -0
  69. package/dist/assets/{vendor-yaml-DIcAK3EC.js → vendor-yaml-C9KlJAlj.js} +37 -37
  70. package/dist/assets/zh-CN-20MYEjha.js +15 -0
  71. package/dist/assets/{zh-TW-BsOQbrwT.js → zh-TW-DiN04qTw.js} +3 -3
  72. package/dist/index.html +6 -6
  73. package/dist/mini-chat.html +6 -6
  74. package/dist/mobile.html +6 -6
  75. package/package.json +2 -2
  76. package/server/index.js +107 -0
  77. package/server/lib/assistants/DOCUMENTATION.md +18 -18
  78. package/server/lib/assistants/assign.js +2 -1
  79. package/server/lib/assistants/contact-store.js +37 -19
  80. package/server/lib/assistants/projection-history.test.js +207 -0
  81. package/server/lib/assistants/routes.js +5 -5
  82. package/server/lib/assistants/service.js +493 -123
  83. package/server/lib/assistants/service.test.js +203 -133
  84. package/server/lib/conversations/DOCUMENTATION.md +4 -4
  85. package/server/lib/conversations/service.js +86 -57
  86. package/server/lib/conversations/service.test.js +116 -100
  87. package/server/lib/event-stream/global-hub.js +2 -1
  88. package/server/lib/llm/DOCUMENTATION.md +71 -84
  89. package/server/lib/llm/catalog.js +111 -45
  90. package/server/lib/llm/catalog.test.js +121 -59
  91. package/server/lib/llm/completions.js +37 -12
  92. package/server/lib/llm/completions.test.js +45 -4
  93. package/server/lib/llm/generate.js +421 -450
  94. package/server/lib/llm/generate.test.js +553 -487
  95. package/server/lib/llm/routes.js +6 -4
  96. package/server/lib/message-queue/DOCUMENTATION.md +5 -5
  97. package/server/lib/message-queue/opencode-adapter.js +191 -104
  98. package/server/lib/message-queue/opencode-adapter.test.js +328 -58
  99. package/server/lib/message-queue/runtime.test.js +6 -6
  100. package/server/lib/message-queue/worker.js +7 -1
  101. package/server/lib/opencode/DOCUMENTATION.md +28 -5
  102. package/server/lib/opencode/config-entity-routes.js +84 -13
  103. package/server/lib/opencode/config-entity-routes.test.js +41 -10
  104. package/server/lib/opencode/ensure-cli.js +239 -0
  105. package/server/lib/opencode/ensure-cli.test.js +119 -0
  106. package/server/lib/opencode/env-runtime.js +193 -110
  107. package/server/lib/opencode/env-runtime.test.js +221 -17
  108. package/server/lib/opencode/feature-routes-runtime.js +37 -1
  109. package/server/lib/opencode/lifecycle.js +204 -70
  110. package/server/lib/opencode/lifecycle.test.js +376 -6
  111. package/server/lib/opencode/network-runtime.js +28 -16
  112. package/server/lib/opencode/network-runtime.test.js +68 -4
  113. package/server/lib/opencode/opencode2-pin.js +139 -0
  114. package/server/lib/opencode/opencode2-pin.test.js +87 -0
  115. package/server/lib/opencode/opencode2-sidecar.e2e.test.js +203 -0
  116. package/server/lib/opencode/plugin-compatibility.js +15 -0
  117. package/server/lib/opencode/plugin-compatibility.test.js +21 -0
  118. package/server/lib/opencode/plugins.js +4 -3
  119. package/server/lib/opencode/plugins.test.js +12 -0
  120. package/server/lib/opencode/proxy.js +153 -16
  121. package/server/lib/opencode/proxy.test.js +120 -0
  122. package/server/lib/opencode/routes.js +25 -32
  123. package/server/lib/opencode/routes.test.js +51 -52
  124. package/server/lib/opencode/server-utils-runtime.js +2 -0
  125. package/server/lib/opencode/skill-routes.js +6 -7
  126. package/server/lib/opencode/skill-routes.test.js +36 -5
  127. package/server/lib/opencode/v1-migration-gate.js +157 -0
  128. package/server/lib/opencode/v1-migration-gate.test.js +111 -0
  129. package/server/lib/opencode/v2-client.js +49 -0
  130. package/server/lib/opencode/watcher.js +2 -1
  131. package/server/lib/opencode/watcher.test.js +1 -1
  132. package/server/lib/scheduled-tasks/DOCUMENTATION.md +71 -39
  133. package/server/lib/scheduled-tasks/managed-tool-route.js +31 -1
  134. package/server/lib/scheduled-tasks/managed-tool-route.test.js +23 -0
  135. package/server/lib/scheduled-tasks/routes.js +27 -0
  136. package/server/lib/scheduled-tasks/routes.test.js +94 -0
  137. package/server/lib/scheduled-tasks/runtime.js +331 -308
  138. package/server/lib/scheduled-tasks/runtime.test.js +530 -318
  139. package/server/lib/session-goal/DOCUMENTATION.md +65 -14
  140. package/server/lib/session-goal/capability.js +57 -0
  141. package/server/lib/session-goal/capability.test.js +33 -0
  142. package/server/lib/session-goal/routes.js +231 -2
  143. package/server/lib/session-goal/routes.test.js +211 -0
  144. package/server/lib/session-goal/runtime.js +46 -4
  145. package/server/lib/session-goal/runtime.test.js +62 -1
  146. package/server/lib/session-index/sync-runtime.js +20 -17
  147. package/server/lib/session-index/sync-runtime.test.js +9 -8
  148. package/server/lib/session-metadata/DOCUMENTATION.md +41 -0
  149. package/server/lib/session-metadata/routes.js +94 -0
  150. package/server/lib/session-metadata/routes.test.js +111 -0
  151. package/server/lib/session-metadata/session-metadata-store.js +235 -0
  152. package/server/lib/session-metadata/session-metadata-store.test.js +184 -0
  153. package/server/lib/session-turn-pages/DOCUMENTATION.md +3 -3
  154. package/server/lib/session-turn-pages/reconcile.service.js +1 -1
  155. package/server/lib/session-turn-pages/reconcile.service.test.js +16 -0
  156. package/server/lib/session-turn-pages/routes.js +204 -71
  157. package/server/lib/session-turn-pages/routes.test.js +285 -0
  158. package/server/lib/session-turn-pages/service.js +6 -2
  159. package/server/lib/session-turn-pages/service.test.js +34 -0
  160. package/server/lib/small-model/DOCUMENTATION.md +5 -4
  161. package/server/lib/small-model/catalog.js +56 -6
  162. package/server/lib/small-model/catalog.test.js +47 -0
  163. package/server/lib/small-model/index.js +3 -3
  164. package/server/lib/small-model/opencode-session.js +50 -206
  165. package/server/lib/small-model/opencode-session.test.js +58 -88
  166. package/server/opencode-proxy.test.js +61 -12
  167. package/dist/assets/AssistantView-BjvfkSRS.js +0 -3
  168. package/dist/assets/MarkdownRenderer-Cg82L3fN.js +0 -2
  169. package/dist/assets/MobileDetailNavigation-s_vvUD_F.js +0 -1
  170. package/dist/assets/MobileSurface-DHpXHbJp.js +0 -2
  171. package/dist/assets/SettingsView-l0J7pfYo.js +0 -25
  172. package/dist/assets/es-rqMiRP7-.js +0 -15
  173. package/dist/assets/gitApi-CHpibtno.js +0 -7
  174. package/dist/assets/insertionBoundaries-Yd1Lorjw.js +0 -1
  175. package/dist/assets/ja-Bgr8-dOg.js +0 -15
  176. package/dist/assets/ko-B_Gxh-0l.js +0 -15
  177. package/dist/assets/main-BoRXxmB3.js +0 -2
  178. package/dist/assets/main-DCDcdV5o.js +0 -169
  179. package/dist/assets/miniChat-12fxk5KR.js +0 -2
  180. package/dist/assets/mobile-CkZem9Ad.js +0 -2
  181. package/dist/assets/pl-DW2XJGC4.js +0 -15
  182. package/dist/assets/platform-0w_Q9_ji.js +0 -2
  183. package/dist/assets/pt-BR-CIsanf4T.js +0 -15
  184. package/dist/assets/renderElectronMiniChatApp-COFc0ZDJ.js +0 -2
  185. package/dist/assets/renderMobileApp-BKzx7JL3.js +0 -28
  186. package/dist/assets/uk-BPmPEUYJ.js +0 -15
  187. package/dist/assets/useMobileNavigationStore-DrB_awgk.js +0 -413
  188. package/dist/assets/vendor-opencode-sdk-C07HQypa.js +0 -7
  189. package/dist/assets/zh-CN-R8-mDcIp.js +0 -15
@@ -1,6 +1,8 @@
1
1
  import { spawn, spawnSync } from 'node:child_process';
2
2
  import net from 'node:net';
3
3
  import { registerManagedProcess, unregisterManagedProcess, reapOrphanedProcesses } from './managed-process-registry.js';
4
+ import { evaluateOpenCodeHealthBody } from './opencode2-pin.js';
5
+ import { OPENCODE_V1_MIGRATION_PATH, fetchV1MigrationGate } from './v1-migration-gate.js';
4
6
 
5
7
  const parsePositiveInt = (value, fallback) => {
6
8
  const parsed = Number.parseInt(String(value ?? ''), 10);
@@ -14,7 +16,14 @@ const HEALTH_CHECK_MAX_CONSECUTIVE_FAILURES = parsePositiveInt(
14
16
  );
15
17
  const HEALTH_CHECK_INTERVAL_OVERRIDE_MS = parsePositiveInt(process.env.OPENCHAMBER_OPENCODE_HEALTH_INTERVAL_MS, 0);
16
18
  const HEALTH_CHECK_RESULT_CACHE_MS = parsePositiveInt(process.env.OPENCHAMBER_OPENCODE_HEALTH_CACHE_MS, 750);
17
- const OPENCODE_HEALTH_PATH = '/global/health';
19
+ // Official OpenCode 2.x exposes ServerInfo at GET /api/info (client.server.info).
20
+ const OPENCODE_HEALTH_PATH = '/api/info';
21
+ const OPENCODE_HEALTH_FALLBACK_PATH = '/global/health';
22
+ const DEFAULT_OPENCODE_SERVE_PORT = 4096;
23
+ // Last-used directory plus recently opened projects — deeper tails are unlikely
24
+ // to be the user's first click and just add background work.
25
+ const WARMUP_DIRECTORY_LIMIT = 4;
26
+ const WARMUP_REQUEST_TIMEOUT_MS = 30000;
18
27
 
19
28
  export const createOpenCodeLifecycleRuntime = (deps) => {
20
29
  const {
@@ -28,6 +37,7 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
28
37
  normalizeApiPrefix,
29
38
  applyOpencodeBinaryFromSettings,
30
39
  ensureOpencodeCliEnv,
40
+ ensurePinnedOpenCode2CliEnv,
31
41
  ensureLocalOpenCodeServerPassword,
32
42
  resolveManagedOpenCodeLaunchSpec,
33
43
  setOpenCodePort,
@@ -40,6 +50,7 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
40
50
  getManagedOpenCodeShellEnvSnapshot,
41
51
  managedCapabilitiesRuntime = null,
42
52
  getActiveSessionCount = () => 0,
53
+ getWarmupDirectories = async () => [],
43
54
  } = deps;
44
55
 
45
56
  const killProcessOnPort = (port) => {
@@ -306,8 +317,11 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
306
317
  stdout += chunk.toString();
307
318
  const lines = stdout.split('\n');
308
319
  for (const line of lines) {
309
- if (!line.startsWith('opencode server listening')) continue;
310
- const match = line.match(/on\s+(https?:\/\/[^\s]+)/);
320
+ const trimmed = line.trim();
321
+ // v2 prints `server listening on http://...` without the `opencode `
322
+ // prefix; 1.x still prints `opencode server listening on ...`.
323
+ if (!trimmed.startsWith('opencode server listening') && !trimmed.startsWith('server listening')) continue;
324
+ const match = trimmed.match(/on\s+(https?:\/\/[^\s]+)/);
311
325
  if (!match) {
312
326
  finish(reject, new Error(`Failed to parse server url from output: ${line}`));
313
327
  return;
@@ -403,23 +417,72 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
403
417
  });
404
418
  };
405
419
 
420
+ // Official v2 only publishes GET /api/experimental/migration/v1. Backfill
421
+ // runs inside opencode2; we poll status and never POST.
422
+ const probeV1MigrationGate = async (signal) => fetchV1MigrationGate({
423
+ url: buildOpenCodeUrl(OPENCODE_V1_MIGRATION_PATH, ''),
424
+ headers: getOpenCodeAuthHeaders(),
425
+ signal,
426
+ });
427
+
428
+ // Health ok is not enough to pull session messages. required/running/error
429
+ // keep isOpenCodeReady false so the proxy ready gate still blocks transcript.
430
+ const applyV1MigrationGate = (gate) => {
431
+ state.v1Migration = gate;
432
+ if (gate.admitTranscript) {
433
+ state.isOpenCodeReady = true;
434
+ state.openCodeNotReadySince = 0;
435
+ return true;
436
+ }
437
+ state.isOpenCodeReady = false;
438
+ if (!state.openCodeNotReadySince) {
439
+ state.openCodeNotReadySince = Date.now();
440
+ }
441
+ if (gate.phase === 'error' && gate.error) {
442
+ state.lastOpenCodeError = gate.error;
443
+ }
444
+ return false;
445
+ };
446
+
447
+ // v2 readiness lives at /api/info (ServerInfo.version); /global/health remains
448
+ // a probe fallback for older sidecars. Both require Basic auth from
449
+ // getOpenCodeAuthHeaders(). Version admission rejects 1.x / missing / noise.
450
+ const fetchOpenCodeHealthOk = async (urlForPath, signal, options = {}) => {
451
+ const headerCandidates = [
452
+ { Accept: 'application/json', ...getOpenCodeAuthHeaders() },
453
+ ];
454
+ if (options.allowUnauthenticated) {
455
+ headerCandidates.push({ Accept: 'application/json' });
456
+ }
457
+ for (const headers of headerCandidates) {
458
+ for (const healthPath of [OPENCODE_HEALTH_PATH, OPENCODE_HEALTH_FALLBACK_PATH]) {
459
+ try {
460
+ const response = await fetch(urlForPath(healthPath), {
461
+ method: 'GET',
462
+ headers,
463
+ signal,
464
+ });
465
+ if (!response.ok) continue;
466
+ const body = await response.json().catch(() => null);
467
+ const gate = evaluateOpenCodeHealthBody(body);
468
+ if (gate.ok) return true;
469
+ } catch {
470
+ }
471
+ }
472
+ }
473
+ return false;
474
+ };
475
+
406
476
  const isOpenCodeProcessHealthy = async () => {
407
477
  if (!state.openCodeProcess || !state.openCodePort) {
408
478
  return false;
409
479
  }
410
480
 
411
481
  try {
412
- const response = await fetch(buildOpenCodeUrl(OPENCODE_HEALTH_PATH, ''), {
413
- method: 'GET',
414
- headers: {
415
- Accept: 'application/json',
416
- ...getOpenCodeAuthHeaders(),
417
- },
418
- signal: AbortSignal.timeout(HEALTH_CHECK_TIMEOUT_MS),
419
- });
420
- if (!response.ok) return false;
421
- const body = await response.json().catch(() => null);
422
- return body?.healthy === true;
482
+ return await fetchOpenCodeHealthOk(
483
+ (healthPath) => buildOpenCodeUrl(healthPath, ''),
484
+ AbortSignal.timeout(HEALTH_CHECK_TIMEOUT_MS)
485
+ );
423
486
  } catch {
424
487
  return false;
425
488
  }
@@ -434,18 +497,13 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
434
497
  const controller = new AbortController();
435
498
  const timeout = setTimeout(() => controller.abort(), 3000);
436
499
  const base = origin ?? `http://127.0.0.1:${port}`;
437
- const response = await fetch(`${base}${OPENCODE_HEALTH_PATH}`, {
438
- method: 'GET',
439
- headers: {
440
- Accept: 'application/json',
441
- ...getOpenCodeAuthHeaders(),
442
- },
443
- signal: controller.signal,
444
- });
500
+ const healthy = await fetchOpenCodeHealthOk(
501
+ (healthPath) => `${base}${healthPath}`,
502
+ controller.signal,
503
+ { allowUnauthenticated: true },
504
+ );
445
505
  clearTimeout(timeout);
446
- if (!response.ok) return false;
447
- const body = await response.json().catch(() => null);
448
- return body?.healthy === true;
506
+ return healthy;
449
507
  } catch {
450
508
  return false;
451
509
  }
@@ -472,7 +530,12 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
472
530
  const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
473
531
 
474
532
  const startOpenCodeOnce = async () => {
475
- const desiredPort = env.ENV_CONFIGURED_OPENCODE_PORT ?? 0;
533
+ const requestedPort = env.ENV_CONFIGURED_OPENCODE_PORT;
534
+ const defaultPortFree = requestedPort
535
+ ? false
536
+ : await waitForPortRelease(DEFAULT_OPENCODE_SERVE_PORT, 200, env.ENV_CONFIGURED_OPENCODE_HOSTNAME);
537
+ const desiredPort = requestedPort
538
+ ?? (defaultPortFree ? DEFAULT_OPENCODE_SERVE_PORT : 0);
476
539
  const spawnPort = await resolveManagedOpenCodePort(desiredPort, env.ENV_CONFIGURED_OPENCODE_HOSTNAME);
477
540
  console.log(
478
541
  desiredPort > 0
@@ -481,6 +544,9 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
481
544
  );
482
545
 
483
546
  await applyOpencodeBinaryFromSettings({ strict: true });
547
+ if (typeof ensurePinnedOpenCode2CliEnv === 'function') {
548
+ await ensurePinnedOpenCode2CliEnv();
549
+ }
484
550
  ensureOpencodeCliEnv();
485
551
  const openCodePassword = await ensureLocalOpenCodeServerPassword({ rotateManaged: true });
486
552
  const envPath = typeof buildManagedOpenCodePath === 'function'
@@ -523,9 +589,10 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
523
589
  setOpenCodePort(port);
524
590
  setDetectedOpenCodeApiPrefix(prefix);
525
591
 
526
- state.isOpenCodeReady = true;
527
- state.lastOpenCodeError = null;
528
- state.openCodeNotReadySince = 0;
592
+ const gate = await probeV1MigrationGate(AbortSignal.timeout(HEALTH_CHECK_TIMEOUT_MS));
593
+ if (applyV1MigrationGate(gate)) {
594
+ state.lastOpenCodeError = null;
595
+ }
529
596
 
530
597
  return serverInstance;
531
598
  }
@@ -593,12 +660,14 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
593
660
  if (healthy) {
594
661
  console.log(`External OpenCode server on port ${probePort} is healthy`);
595
662
  setOpenCodePort(probePort);
596
- state.isOpenCodeReady = true;
597
- state.lastOpenCodeError = null;
598
- state.openCodeNotReadySince = 0;
663
+ const gate = await probeV1MigrationGate(AbortSignal.timeout(HEALTH_CHECK_TIMEOUT_MS));
664
+ if (applyV1MigrationGate(gate)) {
665
+ state.lastOpenCodeError = null;
666
+ }
599
667
  syncToHmrState();
600
668
  } else {
601
669
  state.lastOpenCodeError = `External OpenCode server on port ${probePort} is not responding`;
670
+ state.v1Migration = null;
602
671
  console.error(state.lastOpenCodeError);
603
672
  throw new Error(state.lastOpenCodeError);
604
673
  }
@@ -658,12 +727,16 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
658
727
  } catch (error) {
659
728
  console.error(`Failed to restart OpenCode: ${error.message}`);
660
729
  state.lastOpenCodeError = error.message;
661
- if (!env.ENV_CONFIGURED_OPENCODE_PORT) {
662
- state.openCodePort = null;
663
- syncToHmrState();
730
+ // External serve is read-only: never clear its port, flip ownership, or
731
+ // spawn a managed replacement when a re-probe fails.
732
+ if (!state.isExternalOpenCode) {
733
+ if (!env.ENV_CONFIGURED_OPENCODE_PORT) {
734
+ state.openCodePort = null;
735
+ syncToHmrState();
736
+ }
737
+ state.openCodeApiPrefixDetected = true;
738
+ state.openCodeApiPrefix = '';
664
739
  }
665
- state.openCodeApiPrefixDetected = true;
666
- state.openCodeApiPrefix = '';
667
740
  throw error;
668
741
  } finally {
669
742
  state.currentRestartPromise = null;
@@ -682,32 +755,31 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
682
755
  while (Date.now() < deadline) {
683
756
  let timeout = null;
684
757
  try {
758
+ // One timer covers health + migration for this attempt. Clearing after
759
+ // health alone let a hung migration outrun HEALTH_CHECK_TIMEOUT_MS and
760
+ // the outer deadline when the probe never settled.
685
761
  const controller = new AbortController();
686
762
  timeout = setTimeout(() => controller.abort(), HEALTH_CHECK_TIMEOUT_MS);
687
- const response = await fetch(buildOpenCodeUrl(OPENCODE_HEALTH_PATH, ''), {
688
- method: 'GET',
689
- headers: { Accept: 'application/json', ...getOpenCodeAuthHeaders() },
690
- signal: controller.signal,
691
- });
692
- clearTimeout(timeout);
693
- timeout = null;
763
+ const healthy = await fetchOpenCodeHealthOk(
764
+ (healthPath) => buildOpenCodeUrl(healthPath, ''),
765
+ controller.signal
766
+ );
694
767
 
695
- if (!response.ok) {
696
- lastError = new Error(`OpenCode health endpoint responded with status ${response.status}`);
768
+ if (!healthy) {
769
+ lastError = new Error('OpenCode health endpoint returned unhealthy response');
697
770
  await new Promise((resolve) => setTimeout(resolve, intervalMs));
698
771
  continue;
699
772
  }
700
773
 
701
- const body = await response.json().catch(() => null);
702
- if (body?.healthy !== true) {
703
- lastError = new Error('OpenCode health endpoint returned unhealthy response');
704
- await new Promise((resolve) => setTimeout(resolve, intervalMs));
705
- continue;
774
+ const gate = await probeV1MigrationGate(controller.signal);
775
+ if (applyV1MigrationGate(gate)) {
776
+ state.lastOpenCodeError = null;
777
+ return;
706
778
  }
707
779
 
708
- state.isOpenCodeReady = true;
709
- state.lastOpenCodeError = null;
710
- return;
780
+ lastError = new Error(gate.error || `V1 migration is ${gate.phase}`);
781
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
782
+ continue;
711
783
  } catch (error) {
712
784
  lastError = error;
713
785
  } finally {
@@ -828,35 +900,40 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
828
900
  console.log(`Using external OpenCode server at ${label} (skip-start mode)`);
829
901
  state.openCodeBaseUrl = env.ENV_CONFIGURED_OPENCODE_HOST?.origin ?? null;
830
902
  setOpenCodePort(env.ENV_EFFECTIVE_PORT);
831
- state.isOpenCodeReady = true;
903
+ // External attach is not ready until version + V1 migration admit.
904
+ state.isOpenCodeReady = false;
905
+ state.openCodeNotReadySince = Date.now();
832
906
  state.isExternalOpenCode = true;
833
907
  state.lastOpenCodeError = null;
834
- state.openCodeNotReadySince = 0;
908
+ state.v1Migration = null;
835
909
  syncToHmrState();
836
910
  } else if (await isOpenCodeProcessHealthy() && (managedCapabilitiesRuntime ? managedCapabilitiesRuntime.hasValidIdentity() : true)) {
837
911
  console.log(`[HMR] Reusing existing OpenCode process on port ${state.openCodePort}`);
838
912
  } else if (state.openCodeProcess && !state.isExternalOpenCode && await isOpenCodeProcessHealthy()) {
839
913
  console.log('[HMR] Restarting managed OpenCode because scheduled-task capability identity is stale');
840
914
  await restartOpenCode();
841
- } else if (env.ENV_EFFECTIVE_PORT && await probeExternalOpenCode(env.ENV_EFFECTIVE_PORT, env.ENV_CONFIGURED_OPENCODE_HOST?.origin)) {
842
- const label = env.ENV_CONFIGURED_OPENCODE_HOST ? env.ENV_CONFIGURED_OPENCODE_HOST.origin : `http://localhost:${env.ENV_EFFECTIVE_PORT}`;
843
- console.log(`Auto-detected existing OpenCode server at ${label}`);
915
+ } else if (
916
+ (env.ENV_EFFECTIVE_PORT || DEFAULT_OPENCODE_SERVE_PORT)
917
+ && await probeExternalOpenCode(
918
+ env.ENV_EFFECTIVE_PORT || DEFAULT_OPENCODE_SERVE_PORT,
919
+ env.ENV_CONFIGURED_OPENCODE_HOST?.origin,
920
+ )
921
+ ) {
922
+ const reusePort = env.ENV_EFFECTIVE_PORT || DEFAULT_OPENCODE_SERVE_PORT;
923
+ const label = env.ENV_CONFIGURED_OPENCODE_HOST ? env.ENV_CONFIGURED_OPENCODE_HOST.origin : `http://localhost:${reusePort}`;
924
+ console.log(`Reusing existing OpenCode serve at ${label}`);
844
925
  state.openCodeBaseUrl = env.ENV_CONFIGURED_OPENCODE_HOST?.origin ?? null;
845
- setOpenCodePort(env.ENV_EFFECTIVE_PORT);
846
- state.isOpenCodeReady = true;
926
+ setOpenCodePort(reusePort);
927
+ // Health ok is not transcript-ready; waitForOpenCodeReady applies the gate.
928
+ state.isOpenCodeReady = false;
929
+ state.openCodeNotReadySince = Date.now();
847
930
  state.isExternalOpenCode = true;
848
931
  state.lastOpenCodeError = null;
849
- state.openCodeNotReadySince = 0;
932
+ state.v1Migration = null;
850
933
  syncToHmrState();
851
934
  } else {
852
- // We never auto-attach to an arbitrary pre-existing OpenCode instance.
853
- // Attaching to an external server requires explicit opt-in via env
854
- // (OPENCODE_HOST / OPENCODE_PORT / OPENCODE_SKIP_START), handled by the
855
- // branches above. Without that opt-in we always start our OWN managed
856
- // instance on a freshly-allocated port. A blind probe of the default
857
- // port 4096 used to hijack a user's separately-running OpenCode (e.g.
858
- // the OpenCode desktop app), coupling our lifecycle to theirs and
859
- // breaking init against an unexpected server version/config.
935
+ // No healthy v2 serve on the requested/default port. Start a generic
936
+ // `opencode serve` so OpenChamber and other OpenCode clients can share it.
860
937
  state.isExternalOpenCode = false;
861
938
  state.openCodeBaseUrl = null;
862
939
  if (env.ENV_EFFECTIVE_PORT) {
@@ -874,6 +951,11 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
874
951
  await waitForOpenCodePort();
875
952
  try {
876
953
  await waitForOpenCodeReady();
954
+ // Warm only after readiness admits; a failed gate must not kick off
955
+ // directory-scoped traffic against an unready upstream.
956
+ if (state.isOpenCodeReady) {
957
+ void warmOpenCodeDirectories();
958
+ }
877
959
  } catch (error) {
878
960
  console.error(`OpenCode readiness check failed: ${error.message}`);
879
961
  }
@@ -884,6 +966,47 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
884
966
  }
885
967
  };
886
968
 
969
+ // OpenCode initializes each project directory lazily on its first
970
+ // directory-scoped request, and that initialization takes seconds on large
971
+ // session stores. Without warming, the user's first session open pays it
972
+ // interactively. Warm the most recently used directories right after
973
+ // readiness so the work overlaps UI startup instead. Sequential and
974
+ // best-effort: a failed or slow directory never blocks the others for long,
975
+ // and a restart invalidates the pass via the port/readiness guard.
976
+ const warmOpenCodeDirectories = async () => {
977
+ let directories = [];
978
+ try {
979
+ directories = await getWarmupDirectories();
980
+ } catch {
981
+ return;
982
+ }
983
+ if (!Array.isArray(directories) || directories.length === 0) return;
984
+
985
+ const warmedPort = state.openCodePort;
986
+ for (const directory of directories.slice(0, WARMUP_DIRECTORY_LIMIT)) {
987
+ if (typeof directory !== 'string' || !directory) continue;
988
+ if (!state.isOpenCodeReady || state.openCodePort !== warmedPort) return;
989
+ let timeout = null;
990
+ try {
991
+ const controller = new AbortController();
992
+ timeout = setTimeout(() => controller.abort(), WARMUP_REQUEST_TIMEOUT_MS);
993
+ // Warming a directory is the point, not the answer: any directory-scoped
994
+ // read makes OpenCode initialise it. `/api/session` is the cheapest one
995
+ // that takes a directory.
996
+ const url = `${buildOpenCodeUrl('/api/session', '')}?directory=${encodeURIComponent(directory)}&limit=1`;
997
+ await fetch(url, {
998
+ method: 'GET',
999
+ headers: { Accept: 'application/json', ...getOpenCodeAuthHeaders() },
1000
+ signal: controller.signal,
1001
+ });
1002
+ } catch {
1003
+ // Best-effort — the directory stays lazy and the UI's own request warms it.
1004
+ } finally {
1005
+ if (timeout) clearTimeout(timeout);
1006
+ }
1007
+ }
1008
+ };
1009
+
887
1010
  const bootstrapOpenCodeAtStartup = () => {
888
1011
  if (!bootstrapOpenCodePromise) {
889
1012
  bootstrapOpenCodePromise = runBootstrapOpenCodeAtStartup().finally(() => {
@@ -903,6 +1026,9 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
903
1026
 
904
1027
  clearResolvedOpenCodeBinary();
905
1028
  await applyOpencodeBinaryFromSettings({ strict: true });
1029
+ if (typeof ensurePinnedOpenCode2CliEnv === 'function') {
1030
+ await ensurePinnedOpenCode2CliEnv();
1031
+ }
906
1032
  const resolved = ensureOpencodeCliEnv();
907
1033
  if (!resolved) {
908
1034
  const error = new Error('OpenCode CLI could not be resolved');
@@ -992,6 +1118,9 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
992
1118
  };
993
1119
 
994
1120
  const runHealthCheckCycle = async (source) => {
1121
+ // External serve is read-only mount: never restart or spawn a second copy
1122
+ // because the user's instance looks unhealthy from our probe.
1123
+ if (state.isExternalOpenCode) return;
995
1124
  if (!state.openCodeProcess || state.isShuttingDown || state.isRestartingOpenCode) return;
996
1125
  if (healthCheckCyclePromise) return healthCheckCyclePromise;
997
1126
 
@@ -1017,6 +1146,11 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
1017
1146
  await restartOpenCode();
1018
1147
  } else {
1019
1148
  resetHealthFailureState();
1149
+ try {
1150
+ const gate = await probeV1MigrationGate(AbortSignal.timeout(HEALTH_CHECK_TIMEOUT_MS));
1151
+ applyV1MigrationGate(gate);
1152
+ } catch {
1153
+ }
1020
1154
  }
1021
1155
  })().finally(() => {
1022
1156
  healthCheckCyclePromise = null;