@openchambery/web 1.19.21-beta.1 → 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.
- package/README.md +13 -11
- package/bin/cli-opencode.test.js +208 -0
- package/bin/cli.js +62 -9
- package/bin/lib/cli-startup.js +105 -2
- package/bin/lib/cli-startup.test.js +165 -0
- package/dist/assets/{AgentAvatar-NT957MuI.js → AgentAvatar-CLWWNu_Q.js} +1 -1
- package/dist/assets/AssistantView-BTCkm8yk.js +3 -0
- package/dist/assets/{DiagramView-Bh06xinl.js → DiagramView-xuSJTnv7.js} +1 -1
- package/dist/assets/{FadeInOnReveal-CilRl4gc.js → FadeInOnReveal-Ov0D96d0.js} +1 -1
- package/dist/assets/{Icon-i5GiQRLq.js → Icon-DD-BuMfZ.js} +1 -1
- package/dist/assets/MarkdownRenderer-bc_p1H7l.js +2 -0
- package/dist/assets/{MarkdownRendererImpl-f2agCc4q.js → MarkdownRendererImpl-SKi4oG0c.js} +1 -1
- package/dist/assets/{MarkstreamRendererImpl-ChD4WD2v.js → MarkstreamRendererImpl-CnkodGJW.js} +1 -1
- package/dist/assets/MobileDetailNavigation-Bnx59U_J.js +1 -0
- package/dist/assets/{MobileShareBridge-D7SG0ueZ.js → MobileShareBridge-BQlP4J-F.js} +1 -1
- package/dist/assets/MobileSurface-BXLeiZ4d.js +2 -0
- package/dist/assets/{MultiRunWindow-CjCw1X4n.js → MultiRunWindow-ChZGob0v.js} +1 -1
- package/dist/assets/{OnboardingScreen-BGRLk4Wi.js → OnboardingScreen-DtQJxZG-.js} +1 -1
- package/dist/assets/{OverlayScrollbar-NuQI58XD.js → OverlayScrollbar-pMwxaCca.js} +1 -1
- package/dist/assets/SettingsView-B5I0KDTZ.js +25 -0
- package/dist/assets/{SettingsWindow-C2DgbXJ3.js → SettingsWindow-DfnsFa8y.js} +1 -1
- package/dist/assets/{TerminalView-lubDPKMs.js → TerminalView-Dh9MWCpS.js} +1 -1
- package/dist/assets/{ToolOutputDialog-CzgW3-Cb.js → ToolOutputDialog-BejmjrRk.js} +1 -1
- package/dist/assets/{appThemeRegistry-BKc-qXbQ.js → appThemeRegistry-COGfxTFV.js} +1 -1
- package/dist/assets/{button-DtzFDvij.js → button-D-rATN7w.js} +1 -1
- package/dist/assets/{collapsible-D-nzhcyF.js → collapsible-BXRd5_Hr.js} +1 -1
- package/dist/assets/{context-menu-BzVSzHLG.js → context-menu-jGtH6fR9.js} +1 -1
- package/dist/assets/{diagram-2YWalSb5.js → diagram-DQf7Y_uR.js} +1 -1
- package/dist/assets/{dialog-DGV7DsZC.js → dialog-bfbvR4VH.js} +1 -1
- package/dist/assets/es-c52LTHHe.js +15 -0
- package/dist/assets/{fileMentionTouchSelection-BTkP7aYI.js → fileMentionTouchSelection-BvrNCroG.js} +1 -1
- package/dist/assets/{fr-DsBZeN8Y.js → fr-PisFbUE8.js} +3 -3
- package/dist/assets/gitApi-ButwzQ0N.js +9 -0
- package/dist/assets/{input-CbB2HdLg.js → input-8r6F_Fza.js} +1 -1
- package/dist/assets/insertionBoundaries-DGs7S5zv.js +1 -0
- package/dist/assets/ja-C8Ikun3Z.js +15 -0
- package/dist/assets/ko-ClFys5o2.js +15 -0
- package/dist/assets/main-CCE_4y5F.js +2 -0
- package/dist/assets/main-Dn9rpsgl.js +169 -0
- package/dist/assets/{markdownHeightCache-C9ym9Tsb.js → markdownHeightCache-1Qo6y6IJ.js} +1 -1
- package/dist/assets/miniChat-BpjxVqgg.js +2 -0
- package/dist/assets/mobile-nMJCaNq1.js +2 -0
- package/dist/assets/multirun-BvSG94mW.js +4 -0
- package/dist/assets/pl-CnaKWN5F.js +15 -0
- package/dist/assets/platform-CWTIp02q.js +4 -0
- package/dist/assets/projectMeta-DOe49dM_.js +3 -0
- package/dist/assets/pt-BR-s8OpfRI6.js +15 -0
- package/dist/assets/renderElectronMiniChatApp-R6dKh79f.js +2 -0
- package/dist/assets/renderMobileApp-y-S8y8v_.js +28 -0
- package/dist/assets/{runtimeConfig-PpnZiwcW.js → runtimeConfig-BMvqa5we.js} +1 -1
- package/dist/assets/{runtimeEndpointReset-BGjzNuQl.js → runtimeEndpointReset-B6BxhvR0.js} +6 -6
- package/dist/assets/sessionChangeSummary-VVBlEXuh.js +2 -0
- package/dist/assets/{sessionLookup-CbbzalcH.js → sessionLookup-gh-hVRIc.js} +1 -1
- package/dist/assets/{sortable-tabs-strip-CfgEJfB0.js → sortable-tabs-strip-CudoSgV4.js} +1 -1
- package/dist/assets/src-BzX5UI-c.css +2 -0
- package/dist/assets/{store-B6dImdlY.js → store-DJczmczv.js} +4 -4
- package/dist/assets/{terminalApi-xjqssKke.js → terminalApi-DiF6JmBP.js} +1 -1
- package/dist/assets/textarea-3XR8sUfy.js +1 -0
- package/dist/assets/{tooltip-Djn7MSOX.js → tooltip-CQvpK-UV.js} +1 -1
- package/dist/assets/uk-CaX0iqkB.js +15 -0
- package/dist/assets/{useAppFontEffects-BJBcy-84.js → useAppFontEffects-Cpmpo7Wb.js} +45 -45
- package/dist/assets/useDesktopWindowControlsLayout-B1GX59-L.js +1 -0
- package/dist/assets/{useEffectiveDirectory-CzHynoi_.js → useEffectiveDirectory-CN6JoCFw.js} +1 -1
- package/dist/assets/{useFontPreferences-B1JYyzpK.js → useFontPreferences-DcbXQOkc.js} +1 -1
- package/dist/assets/useMobileNavigationStore-BVjFtXSy.js +414 -0
- package/dist/assets/{useSessionAutoCleanup-DzV9N6G-.js → useSessionAutoCleanup-BLjymaFt.js} +1 -1
- package/dist/assets/{useWorkerHighlightedLines-Bi9ASKr_.js → useWorkerHighlightedLines-Bqn4AiBL.js} +1 -1
- package/dist/assets/vendor-opencode-client-BOe2qpQb.js +12 -0
- package/dist/assets/{vendor-yaml-DIcAK3EC.js → vendor-yaml-C9KlJAlj.js} +37 -37
- package/dist/assets/zh-CN-20MYEjha.js +15 -0
- package/dist/assets/{zh-TW-BsOQbrwT.js → zh-TW-DiN04qTw.js} +3 -3
- package/dist/index.html +6 -6
- package/dist/mini-chat.html +6 -6
- package/dist/mobile.html +6 -6
- package/package.json +2 -2
- package/server/index.js +107 -0
- package/server/lib/assistants/DOCUMENTATION.md +18 -18
- package/server/lib/assistants/assign.js +2 -1
- package/server/lib/assistants/contact-store.js +37 -19
- package/server/lib/assistants/projection-history.test.js +207 -0
- package/server/lib/assistants/routes.js +5 -5
- package/server/lib/assistants/service.js +493 -123
- package/server/lib/assistants/service.test.js +203 -133
- package/server/lib/conversations/DOCUMENTATION.md +4 -4
- package/server/lib/conversations/service.js +86 -57
- package/server/lib/conversations/service.test.js +116 -100
- package/server/lib/event-stream/global-hub.js +2 -1
- package/server/lib/llm/DOCUMENTATION.md +71 -84
- package/server/lib/llm/catalog.js +111 -45
- package/server/lib/llm/catalog.test.js +121 -59
- package/server/lib/llm/completions.js +37 -12
- package/server/lib/llm/completions.test.js +45 -4
- package/server/lib/llm/generate.js +421 -450
- package/server/lib/llm/generate.test.js +553 -487
- package/server/lib/llm/routes.js +6 -4
- package/server/lib/message-queue/DOCUMENTATION.md +5 -5
- package/server/lib/message-queue/opencode-adapter.js +191 -104
- package/server/lib/message-queue/opencode-adapter.test.js +328 -58
- package/server/lib/message-queue/runtime.test.js +6 -6
- package/server/lib/message-queue/worker.js +7 -1
- package/server/lib/opencode/DOCUMENTATION.md +28 -5
- package/server/lib/opencode/config-entity-routes.js +84 -13
- package/server/lib/opencode/config-entity-routes.test.js +41 -10
- package/server/lib/opencode/ensure-cli.js +239 -0
- package/server/lib/opencode/ensure-cli.test.js +119 -0
- package/server/lib/opencode/env-runtime.js +193 -110
- package/server/lib/opencode/env-runtime.test.js +221 -17
- package/server/lib/opencode/feature-routes-runtime.js +37 -1
- package/server/lib/opencode/lifecycle.js +204 -70
- package/server/lib/opencode/lifecycle.test.js +376 -6
- package/server/lib/opencode/network-runtime.js +28 -16
- package/server/lib/opencode/network-runtime.test.js +68 -4
- package/server/lib/opencode/opencode2-pin.js +139 -0
- package/server/lib/opencode/opencode2-pin.test.js +87 -0
- package/server/lib/opencode/opencode2-sidecar.e2e.test.js +203 -0
- package/server/lib/opencode/plugin-compatibility.js +15 -0
- package/server/lib/opencode/plugin-compatibility.test.js +21 -0
- package/server/lib/opencode/plugins.js +4 -3
- package/server/lib/opencode/plugins.test.js +12 -0
- package/server/lib/opencode/proxy.js +153 -16
- package/server/lib/opencode/proxy.test.js +120 -0
- package/server/lib/opencode/routes.js +25 -32
- package/server/lib/opencode/routes.test.js +51 -52
- package/server/lib/opencode/server-utils-runtime.js +2 -0
- package/server/lib/opencode/skill-routes.js +6 -7
- package/server/lib/opencode/skill-routes.test.js +36 -5
- package/server/lib/opencode/v1-migration-gate.js +157 -0
- package/server/lib/opencode/v1-migration-gate.test.js +111 -0
- package/server/lib/opencode/v2-client.js +49 -0
- package/server/lib/opencode/watcher.js +2 -1
- package/server/lib/opencode/watcher.test.js +1 -1
- package/server/lib/scheduled-tasks/DOCUMENTATION.md +71 -39
- package/server/lib/scheduled-tasks/managed-tool-route.js +31 -1
- package/server/lib/scheduled-tasks/managed-tool-route.test.js +23 -0
- package/server/lib/scheduled-tasks/routes.js +27 -0
- package/server/lib/scheduled-tasks/routes.test.js +94 -0
- package/server/lib/scheduled-tasks/runtime.js +331 -308
- package/server/lib/scheduled-tasks/runtime.test.js +530 -318
- package/server/lib/session-goal/DOCUMENTATION.md +65 -14
- package/server/lib/session-goal/capability.js +57 -0
- package/server/lib/session-goal/capability.test.js +33 -0
- package/server/lib/session-goal/routes.js +231 -2
- package/server/lib/session-goal/routes.test.js +211 -0
- package/server/lib/session-goal/runtime.js +46 -4
- package/server/lib/session-goal/runtime.test.js +62 -1
- package/server/lib/session-index/sync-runtime.js +20 -17
- package/server/lib/session-index/sync-runtime.test.js +9 -8
- package/server/lib/session-metadata/DOCUMENTATION.md +41 -0
- package/server/lib/session-metadata/routes.js +94 -0
- package/server/lib/session-metadata/routes.test.js +111 -0
- package/server/lib/session-metadata/session-metadata-store.js +235 -0
- package/server/lib/session-metadata/session-metadata-store.test.js +184 -0
- package/server/lib/session-turn-pages/DOCUMENTATION.md +3 -3
- package/server/lib/session-turn-pages/reconcile.service.js +1 -1
- package/server/lib/session-turn-pages/reconcile.service.test.js +16 -0
- package/server/lib/session-turn-pages/routes.js +204 -71
- package/server/lib/session-turn-pages/routes.test.js +285 -0
- package/server/lib/session-turn-pages/service.js +6 -2
- package/server/lib/session-turn-pages/service.test.js +34 -0
- package/server/lib/small-model/DOCUMENTATION.md +5 -4
- package/server/lib/small-model/catalog.js +56 -6
- package/server/lib/small-model/catalog.test.js +47 -0
- package/server/lib/small-model/index.js +3 -3
- package/server/lib/small-model/opencode-session.js +50 -206
- package/server/lib/small-model/opencode-session.test.js +58 -88
- package/server/opencode-proxy.test.js +61 -12
- package/dist/assets/AssistantView-BtGYjSW5.js +0 -3
- package/dist/assets/MarkdownRenderer-AwDKOCvD.js +0 -2
- package/dist/assets/MobileDetailNavigation-B6fGRYsg.js +0 -1
- package/dist/assets/MobileSurface-BY7eErHN.js +0 -2
- package/dist/assets/SettingsView-lcRi35Y3.js +0 -25
- package/dist/assets/es-rqMiRP7-.js +0 -15
- package/dist/assets/gitApi-CcIuekQ3.js +0 -7
- package/dist/assets/insertionBoundaries-BocNNP9Q.js +0 -1
- package/dist/assets/ja-Bgr8-dOg.js +0 -15
- package/dist/assets/ko-B_Gxh-0l.js +0 -15
- package/dist/assets/main-CFLmpNC5.js +0 -169
- package/dist/assets/main-Ckj_IpUy.js +0 -2
- package/dist/assets/miniChat-Dq1CTKNs.js +0 -2
- package/dist/assets/mobile-DlJ0Yr7h.js +0 -2
- package/dist/assets/multirun-BGUKIJys.js +0 -4
- package/dist/assets/pl-DW2XJGC4.js +0 -15
- package/dist/assets/platform-0w_Q9_ji.js +0 -2
- package/dist/assets/projectMeta-Bft2t6GF.js +0 -3
- package/dist/assets/pt-BR-CIsanf4T.js +0 -15
- package/dist/assets/renderElectronMiniChatApp-CTbXbB0Y.js +0 -2
- package/dist/assets/renderMobileApp-DLewUqoE.js +0 -28
- package/dist/assets/sessionChangeSummary-DiqwX3vZ.js +0 -2
- package/dist/assets/src-rS3zCcsC.css +0 -2
- package/dist/assets/textarea-DpK2Z0Gp.js +0 -1
- package/dist/assets/uk-BPmPEUYJ.js +0 -15
- package/dist/assets/useDesktopWindowControlsLayout-3Bm0rXCQ.js +0 -1
- package/dist/assets/useMobileNavigationStore-BIyCYi_z.js +0 -413
- package/dist/assets/vendor-opencode-sdk-C07HQypa.js +0 -7
- 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
|
-
|
|
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
|
-
|
|
310
|
-
|
|
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
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
527
|
-
|
|
528
|
-
|
|
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
|
-
|
|
597
|
-
|
|
598
|
-
|
|
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
|
-
|
|
662
|
-
|
|
663
|
-
|
|
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
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
});
|
|
692
|
-
clearTimeout(timeout);
|
|
693
|
-
timeout = null;
|
|
763
|
+
const healthy = await fetchOpenCodeHealthOk(
|
|
764
|
+
(healthPath) => buildOpenCodeUrl(healthPath, ''),
|
|
765
|
+
controller.signal
|
|
766
|
+
);
|
|
694
767
|
|
|
695
|
-
if (!
|
|
696
|
-
lastError = new Error(
|
|
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
|
|
702
|
-
if (
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
continue;
|
|
774
|
+
const gate = await probeV1MigrationGate(controller.signal);
|
|
775
|
+
if (applyV1MigrationGate(gate)) {
|
|
776
|
+
state.lastOpenCodeError = null;
|
|
777
|
+
return;
|
|
706
778
|
}
|
|
707
779
|
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
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
|
-
|
|
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.
|
|
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 (
|
|
842
|
-
|
|
843
|
-
|
|
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(
|
|
846
|
-
|
|
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.
|
|
932
|
+
state.v1Migration = null;
|
|
850
933
|
syncToHmrState();
|
|
851
934
|
} else {
|
|
852
|
-
//
|
|
853
|
-
//
|
|
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;
|