From 3c0bf516e657632a8248cf1b8e0ea2fb7f5777e6 Mon Sep 17 00:00:00 2001 From: Joseph Yaksich Date: Sun, 2 Aug 2026 00:53:53 +0000 Subject: [PATCH] fix: keep Windows cold starts responsive Signed-off-by: Joseph Yaksich --- CHANGELOG.md | 14 +++++++++++++- README.md | 2 +- desktop/main.cjs | 3 ++- package-lock.json | 4 ++-- package.json | 2 +- src/server/agents.ts | 8 +++++--- src/server/channel-computers.ts | 2 +- src/server/db.ts | 2 +- src/server/index.ts | 7 ++++++- test/autonomy-platform.mjs | 11 +++++++++++ test/channel-computers.mjs | 2 +- test/desktop.mjs | 2 ++ 12 files changed, 46 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 867282f..babbebf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.0.36] - 2026-08-02 + +### Fixed + +- Kept the Windows desktop control plane available across a genuine cold WSL + reboot by deferring retained OCI directory maintenance until runtime access + or fleet reconciliation actually needs it, instead of synchronously waking + WSL before the HTTP server can listen. +- Gave the Windows desktop a bounded three-minute cold-runtime readiness window + while preserving the existing 30-second startup budget on other platforms. + ## [0.0.35] - 2026-08-01 ### Fixed @@ -989,7 +1000,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 notarization, stapled tickets, Gatekeeper verification, persistent Application Support, and isolated Apple container machines. -[Unreleased]: https://github.com/gitcommit90/1Helm/compare/v0.0.35...HEAD +[Unreleased]: https://github.com/gitcommit90/1Helm/compare/v0.0.36...HEAD +[0.0.36]: https://github.com/gitcommit90/1Helm/compare/v0.0.35...v0.0.36 [0.0.35]: https://github.com/gitcommit90/1Helm/compare/v0.0.34...v0.0.35 [0.0.34]: https://github.com/gitcommit90/1Helm/compare/v0.0.33...v0.0.34 [0.0.33]: https://github.com/gitcommit90/1Helm/compare/v0.0.32...v0.0.33 diff --git a/README.md b/README.md index dc61b41..8a616fd 100644 --- a/README.md +++ b/README.md @@ -313,7 +313,7 @@ A fresh data directory opens first-run setup. The source runtime defaults to | `PORT` | `8123` | HTTP/WebSocket control-plane port. | | `CTRL_DATA_DIR` | `./data` | Databases, routing state, uploads, and non-OCI development/Apple workspace mirrors. | | `HELM_CHANNEL_COMPUTER_BACKEND` | `apple` on macOS, `oci` on Linux and Windows | Host isolation backend; `native` and `mock` are explicit development/test overrides. | -| `HELM_CHANNEL_MACHINE_IMAGE` | `local/1helm-channel-machine:0.0.35` | Versioned channel-machine image contract. | +| `HELM_CHANNEL_MACHINE_IMAGE` | `local/1helm-channel-machine:0.0.36` | Versioned channel-machine image contract. | ### Agent-first JSON CLI diff --git a/desktop/main.cjs b/desktop/main.cjs index 365d07c..c052e7d 100644 --- a/desktop/main.cjs +++ b/desktop/main.cjs @@ -15,6 +15,7 @@ const LOOPBACK = "127.0.0.1"; // installation's Application Support/AppData tree untouched and never import // it implicitly into this runtime generation. const DATA_NAMESPACE = "1Helm-OCI-v1"; +const SERVER_READY_TIMEOUT_MS = process.platform === "win32" ? 3 * 60_000 : 30_000; app.setPath("userData", path.join(app.getPath("appData"), DATA_NAMESPACE)); let mainWindow = null; let authWindow = null; @@ -95,7 +96,7 @@ function freePort() { }); } -async function waitForServer(origin, timeoutMs = 30_000) { +async function waitForServer(origin, timeoutMs = SERVER_READY_TIMEOUT_MS) { const deadline = Date.now() + timeoutMs; let lastError = null; while (Date.now() < deadline) { diff --git a/package-lock.json b/package-lock.json index 6977b4f..6ecdcd0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "1helm", - "version": "0.0.35", + "version": "0.0.36", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "1helm", - "version": "0.0.35", + "version": "0.0.36", "hasInstallScript": true, "license": "AGPL-3.0-only", "dependencies": { diff --git a/package.json b/package.json index 7c454bd..4ba988b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "1helm", "productName": "1Helm", - "version": "0.0.35", + "version": "0.0.36", "private": true, "type": "module", "license": "AGPL-3.0-only", diff --git a/src/server/agents.ts b/src/server/agents.ts index 70544f4..f9394a6 100644 --- a/src/server/agents.ts +++ b/src/server/agents.ts @@ -62,7 +62,7 @@ export const channelRoot = hostChannelRoot; export const channelWorkspace = channelWorkspacePath; export const channelFiles = channelFilesPath; -export function ensureChannelWorkspace(channelId: number): string { +export function ensureChannelWorkspace(channelId: number, options: { initializeRuntimeStorage?: boolean } = {}): string { const channel = q1("SELECT id,name,personal_main_owner_id FROM channels WHERE id=? AND status<>'deleted'", channelId); if (!channel) throw new Error("Channel workspace not found."); const root = channelRoot(channelId); @@ -71,8 +71,10 @@ export function ensureChannelWorkspace(channelId: number): string { if (channelUsesRuntimeStorage(channelId)) { // OCI workspace/files live in runtime storage. On Windows that path is // \\wsl.localhost\\... and host mkdir there fails; create via the WSL runtime. - try { ensureOciChannelWorkspaceDirs(channelId); } - catch { /* provision path creates these once the computer exists */ } + if (options.initializeRuntimeStorage !== false) { + try { ensureOciChannelWorkspaceDirs(channelId); } + catch { /* provision path creates these once the computer exists */ } + } } else { mkdirSync(channelWorkspace(channelId), { recursive: true }); mkdirSync(channelFiles(channelId), { recursive: true }); diff --git a/src/server/channel-computers.ts b/src/server/channel-computers.ts index e990914..a40c16c 100644 --- a/src/server/channel-computers.ts +++ b/src/server/channel-computers.ts @@ -68,7 +68,7 @@ const APPLE_RUNTIME_VERSION = "1.1.0"; export const APPLE_RUNTIME_PACKAGE = `container-${APPLE_RUNTIME_VERSION}-installer-signed.pkg`; export const APPLE_RUNTIME_URL = `https://github.com/apple/container/releases/download/${APPLE_RUNTIME_VERSION}/${APPLE_RUNTIME_PACKAGE}`; export const APPLE_RUNTIME_SHA256 = "0ca1c42a2269c2557efb1d82b1b38ac553e6a3a3da1b1179c439bcee1e7d6714"; -export const DEFAULT_CHANNEL_IMAGE = process.env.HELM_CHANNEL_MACHINE_IMAGE || "local/1helm-channel-machine:0.0.35"; +export const DEFAULT_CHANNEL_IMAGE = process.env.HELM_CHANNEL_MACHINE_IMAGE || "local/1helm-channel-machine:0.0.36"; const CONTAINER_CANDIDATES = [process.env.HELM_CONTAINER_CLI, "/usr/local/bin/container", "/opt/homebrew/bin/container", "container"].filter(Boolean) as string[]; const OCI_RUNTIME_VERSION = "1helm-oci-runtime-v1"; const OCI_HELPER_CANDIDATES = [ diff --git a/src/server/db.ts b/src/server/db.ts index 0e8d30d..56d4c0c 100644 --- a/src/server/db.ts +++ b/src/server/db.ts @@ -939,7 +939,7 @@ export function migrate(): void { const platformBackend = process.platform === "darwin" ? "apple" : "oci"; const configuredBackend = String(process.env.HELM_CHANNEL_COMPUTER_BACKEND || platformBackend); const backend = ["apple", "oci", "native", "mock"].includes(configuredBackend) ? configuredBackend : platformBackend; - const image = String(process.env.HELM_CHANNEL_MACHINE_IMAGE || "local/1helm-channel-machine:0.0.35"); + const image = String(process.env.HELM_CHANNEL_MACHINE_IMAGE || "local/1helm-channel-machine:0.0.36"); for (const channel of q(`SELECT c.id FROM channels c JOIN agent_channels ac ON ac.channel_id=c.id WHERE c.kind='channel' AND c.status<>'deleted'`)) { const channelId = Number(channel.id); diff --git a/src/server/index.ts b/src/server/index.ts index bb76baf..5a37545 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -2287,7 +2287,12 @@ async function bootstrap(): Promise { // Resident agents own their Linux machine, never the Captain's native Mac. run("DELETE FROM bot_computers WHERE computer_id=? AND bot_id IN (SELECT bot_id FROM agents WHERE kind='channel')", computerId); for (const channel of q("SELECT id FROM channels WHERE kind='channel' AND status<>'deleted'")) { - ensureChannelWorkspace(Number(channel.id)); + // Retained OCI storage is already authoritative. In particular, a cold + // Windows login can take minutes to wake WSL, and that maintenance must + // never hold the HTTP control plane behind a synchronous startup probe. + // Provisioning, Files/Cowork access, Terminal, and the fleet reconciler + // initialize or verify runtime directories when they actually need them. + ensureChannelWorkspace(Number(channel.id), { initializeRuntimeStorage: false }); } startImprovementLoop(); startThreadAuditLoop(); diff --git a/test/autonomy-platform.mjs b/test/autonomy-platform.mjs index 236f4b3..dc3309b 100644 --- a/test/autonomy-platform.mjs +++ b/test/autonomy-platform.mjs @@ -200,6 +200,17 @@ test("Cowork contracts survive follow-ups and reject only newly-created incompat assert.equal(cowork.enforceCoworkCommandOutput(channelId, null, whiteboards, boardBefore), null); }); +test("retained OCI channel metadata can initialize without synchronously touching runtime storage", () => { + seed(); + const channelId = run("INSERT INTO channels (name,slug,kind,topic,purpose,status,created) VALUES ('retained-cold-start','retained-cold-start','channel','','','active',?)", now()).lastInsertRowid; + const botId = run("INSERT INTO bots (name,model,prompt,created) VALUES ('retained-cold-agent','mock','Resident.',?)", now()).lastInsertRowid; + const agentId = run("INSERT INTO agents (bot_id,kind,name,status,created) VALUES (?,'channel','retained-cold-agent','ready',?)", botId, now()).lastInsertRowid; + run("INSERT INTO agent_channels (agent_id,channel_id,bound_at) VALUES (?,?,?)", agentId, channelId, now()); + run("INSERT INTO agent_profiles (agent_id,purpose,instructions,updated) VALUES (?,'Cold start','Resident.',?)", agentId, now()); + assert.doesNotThrow(() => agents.ensureChannelWorkspace(channelId, { initializeRuntimeStorage: false })); + assert.equal(q1("SELECT root_ref FROM channel_workspaces WHERE channel_id=?", channelId).root_ref, `channels/${channelId}`); +}); + test("resident raw transcript search is semantic, exact, readable, and channel-isolated", () => { const ownerId = run("INSERT INTO users (username,pass,display,is_admin,created) VALUES ('history-owner','x','History Owner',1,?)", now()).lastInsertRowid; const channelId = run("INSERT INTO channels (name,slug,kind,topic,purpose,status,created_by,created) VALUES ('history-home','history-home','channel','','Recall prior sessions','active',?,?)", ownerId, now()).lastInsertRowid; diff --git a/test/channel-computers.mjs b/test/channel-computers.mjs index d6f096e..8c21a13 100644 --- a/test/channel-computers.mjs +++ b/test/channel-computers.mjs @@ -181,7 +181,7 @@ test("Apple channel-computer contract preserves isolation, files, wakes, archive test("runtime digest and packaged image recipe stay pinned", async () => { assert.equal(computers.APPLE_RUNTIME_SHA256, "0ca1c42a2269c2557efb1d82b1b38ac553e6a3a3da1b1179c439bcee1e7d6714"); assert.match(computers.APPLE_RUNTIME_URL, /\/1\.1\.0\/container-1\.1\.0-installer-signed\.pkg$/); - assert.equal(computers.DEFAULT_CHANNEL_IMAGE, "local/1helm-channel-machine:0.0.35"); + assert.equal(computers.DEFAULT_CHANNEL_IMAGE, "local/1helm-channel-machine:0.0.36"); const packaging = await readFile(join(root, "scripts", "package-mac-dmg.cjs"), "utf8"); assert.match(packaging, /container\(\?:\$\|\\\/\)/, "release packaging includes container/ image assets"); const image = await readFile(join(root, "container", "Containerfile"), "utf8"); diff --git a/test/desktop.mjs b/test/desktop.mjs index 5c1c696..0bb29ac 100644 --- a/test/desktop.mjs +++ b/test/desktop.mjs @@ -112,12 +112,14 @@ test("desktop entrypoint keeps the renderer sandboxed and data on the Mac", asyn const bootstrap = server.slice(server.indexOf("async function bootstrap"), server.indexOf("void bootstrap()")); assert.doesNotMatch(bootstrap, /ensureAgentMemory\(/, "retained resident memory initialization cannot block the event loop during a bounded host update"); assert.match(bootstrap, /void memoryRuntime\.catch/, "optional memory-runtime preparation remains asynchronous during startup"); + assert.match(bootstrap, /ensureChannelWorkspace\(Number\(channel\.id\), \{ initializeRuntimeStorage: false \}\)/, "retained OCI directories cannot synchronously wake WSL before the HTTP control plane is ready"); assert.match(server, /\["native-macos", "native-windows"\]\.includes\(update\.mode\)/, "both packaged host updaters quiesce the local server before replacement"); assert.match(nativeUpdater, /update\.electronjs\.org\/gitcommit90\/1Helm\/\$\{feedPlatform\}/); assert.match(nativeUpdater, /win32-x64/, "Windows checks and installs on its host through the native updater"); assert.match(source, /handleSquirrelEvent/); assert.match(source, /setAppUserModelId\("com\.squirrel\.1Helm\.1Helm"\)/, "Windows uses Squirrel's stable taskbar identity"); assert.match(source, /DATA_NAMESPACE = "1Helm-OCI-v1"/, "the clean-slate build uses a fresh durable application-data namespace"); + assert.match(source, /SERVER_READY_TIMEOUT_MS = process\.platform === "win32" \? 3 \* 60_000 : 30_000/, "Windows gives a bounded cold WSL host enough time without weakening other desktop startup budgets"); assert.match(source, /windows-removal\.cjs/, "Windows uninstall invokes ownership-checked shared-runtime cleanup before removing shortcuts"); const onboardingClient = await readFile(join(root, "src", "client", "onboarding.ts"), "utf8"); const clientApi = await readFile(join(root, "src", "client", "api.ts"), "utf8");