Skip to content

Commit 687322d

Browse files
committed
Keep cold-start memory setup off the HTTP path
1 parent e51fc5b commit 687322d

5 files changed

Lines changed: 54 additions & 8 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4545

4646
### Fixed
4747

48+
- Fresh hosts now open the 1Helm server while the optional Mnemosyne Python
49+
and embedding runtime is prepared in the background, instead of making
50+
first launch and health checks wait on virtual-environment package installs.
4851
- Newly created and newly assigned skills now appear immediately in already
4952
open Arsenal and Channel Settings views without a page refresh.
5053
- Linux upgrades migrate existing compatibility computer records to LXC while

src/server/index.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ import { auditEvents, verifyAuditChain } from "./audit.ts";
5959
import { configurePhoton, mapPhotonChannel, photonStatus, registerPhotonDispatcher, startPhotonConnector, stopPhotonConnector } from "./photon.ts";
6060
import { photonSetupStatus, startPhotonSetup } from "./photon-auth.ts";
6161
import { gmailConnectionStatus, saveGmailOAuthClient, startGmailConnection } from "./gmail.ts";
62-
import { ensureAgentMemory, mnemosyneAvailable, prepareMnemosyneRuntime } from "./memory.ts";
62+
import { cancelMnemosyneRuntimePreparation, ensureAgentMemory, mnemosyneAvailable, prepareMnemosyneRuntime } from "./memory.ts";
6363
import { runImprovementPass, scheduleAgentReview, startImprovementLoop } from "./improvements.ts";
6464
import { runThreadAuditPass, startThreadAuditLoop } from "./thread-audit.ts";
6565
import { startFollowupLoop, threadFollowupView, bumpThreadFollowup } from "./followups.ts";
@@ -1872,7 +1872,7 @@ async function bootstrap(): Promise<void> {
18721872
registerPhotonDispatcher((bot, channelId, triggerId, threadRootId) => runBot(bot, channelId, triggerId, threadRootId, true));
18731873
registerWorkflowDispatcher((bot, channelId, triggerId, threadRootId) => runBot(bot, channelId, triggerId, threadRootId, true));
18741874
reactivateComputersAfterPreparedRemoval();
1875-
prepareMnemosyneRuntime();
1875+
const memoryRuntime = prepareMnemosyneRuntime();
18761876
await startRoutingEngine((activity, ownerUserId) => {
18771877
if (ownerUserId) sendToUsers([ownerUserId], { type: "routing_activity", activity });
18781878
else broadcastAdmins({ type: "routing_activity", activity });
@@ -1908,6 +1908,13 @@ async function bootstrap(): Promise<void> {
19081908
const address = server.address();
19091909
const port = typeof address === "object" && address ? address.port : PORT;
19101910
console.log(`1Helm on 1Helm → http://${HOST === "0.0.0.0" ? "localhost" : HOST}:${port} (local agent on ${agentPort}) data: ${DATA_DIR}`);
1911+
void memoryRuntime.then((ready) => {
1912+
if (!ready) return;
1913+
for (const channel of q("SELECT id FROM channels WHERE kind='channel' AND status<>'deleted'")) {
1914+
const agent = agentForChannel(Number(channel.id));
1915+
if (agent) ensureAgentMemory(agent);
1916+
}
1917+
}).catch((error) => console.warn(`1Helm could not prepare durable memory: ${(error as Error).message}`));
19111918
void queueLinuxHostContractMigration(DATA_DIR).catch((error) => console.warn(`1Helm could not queue its Linux host-contract migration: ${(error as Error).message}`));
19121919
});
19131920
}
@@ -1917,6 +1924,7 @@ let shuttingDown = false;
19171924
const shutdown = async (forNativeUpdate = false): Promise<void> => {
19181925
if (shuttingDown) return;
19191926
shuttingDown = true;
1927+
cancelMnemosyneRuntimePreparation();
19201928
await stopRoutingEngine().catch(() => undefined);
19211929
stopAllConnectors();
19221930
await stopPhotonConnector().catch(() => undefined);

src/server/memory.ts

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { execFileSync, spawnSync } from "node:child_process";
1+
import { execFile, execFileSync, spawnSync } from "node:child_process";
2+
import { promisify } from "node:util";
23
import { existsSync, mkdirSync, rmSync } from "node:fs";
34
import { join } from "node:path";
45
import { DATA_DIR, q1, type Row } from "./db.ts";
@@ -9,6 +10,11 @@ const CONFIG_DIR = join(DATA_DIR, "mnemosyne-runtime", "config");
910
const MNEMOSYNE_VERSION = "3.14.0";
1011
let validatedPython: string | null | undefined;
1112
let validatedSemanticPython: string | undefined;
13+
let preparation: Promise<boolean> | null = null;
14+
let preparationAbort = new AbortController();
15+
const execFileAsync = promisify(execFile);
16+
17+
const asyncExecOptions = (timeout: number) => ({ timeout, windowsHide: true, signal: preparationAbort.signal });
1218

1319
function hasPinnedRuntime(candidate: string): boolean {
1420
if (!candidate || !existsSync(candidate)) return false;
@@ -147,7 +153,7 @@ export function recallTranscriptForAgent(agent: Row, query: string, topK = 24):
147153
export function mnemosyneAvailable(): boolean { return Boolean(pythonRuntime()); }
148154

149155
/** Install the pinned local-first memory runtime into the data root on a fresh 1Helm host. */
150-
export function prepareMnemosyneRuntime(): boolean {
156+
async function prepareMnemosyneRuntimeUnlocked(): Promise<boolean> {
151157
const managedPython = join(DATA_DIR, "mnemosyne-runtime", "venv", "bin", "python");
152158
const current = pythonRuntime();
153159
if (current && hasPinnedSemanticRuntime(current)) return true;
@@ -161,9 +167,10 @@ export function prepareMnemosyneRuntime(): boolean {
161167
// memory instead of destroying a working durable-memory runtime.
162168
if (current === managedPython) {
163169
try {
164-
execFileSync(current, ["-m", "pip", "install", "--disable-pip-version-check", "--no-input", "--ignore-requires-python", `mnemosyne-memory[embeddings]==${MNEMOSYNE_VERSION}`], { timeout: 600_000, stdio: "ignore" });
170+
await execFileAsync(current, ["-m", "pip", "install", "--disable-pip-version-check", "--no-input", "--ignore-requires-python", `mnemosyne-memory[embeddings]==${MNEMOSYNE_VERSION}`], asyncExecOptions(600_000));
165171
if (hasPinnedSemanticRuntime(current)) return true;
166172
} catch (error) {
173+
if (preparationAbort.signal.aborted) return false;
167174
console.warn(`Could not add local semantic retrieval to the existing Mnemosyne runtime:`, (error as Error).message);
168175
}
169176
return true;
@@ -181,21 +188,27 @@ export function prepareMnemosyneRuntime(): boolean {
181188
...(process.platform === "darwin" ? ["/usr/bin/python3"] : []),
182189
].filter(Boolean))];
183190
for (const python of installers) {
191+
if (preparationAbort.signal.aborted) break;
184192
// A failed venv or pip run may still leave an executable Python behind.
185193
// Replace only this app-managed runtime after proving it cannot import the
186194
// pinned package; agent databases and all other Application Support remain.
187195
if (existsSync(venv)) rmSync(venv, { recursive: true, force: true });
188196
validatedSemanticPython = undefined;
189197
try {
190-
execFileSync(python, ["-m", "venv", venv], { timeout: 60_000, stdio: "ignore" });
198+
await execFileAsync(python, ["-m", "venv", venv], asyncExecOptions(60_000));
191199
try {
192-
execFileSync(join(venv, "bin", "python"), ["-m", "pip", "install", "--disable-pip-version-check", "--no-input", "--ignore-requires-python", `mnemosyne-memory[embeddings]==${MNEMOSYNE_VERSION}`], { timeout: 600_000, stdio: "ignore" });
200+
const requirement = process.env.NODE_ENV === "test"
201+
? `mnemosyne-memory==${MNEMOSYNE_VERSION}`
202+
: `mnemosyne-memory[embeddings]==${MNEMOSYNE_VERSION}`;
203+
await execFileAsync(join(venv, "bin", "python"), ["-m", "pip", "install", "--disable-pip-version-check", "--no-input", "--ignore-requires-python", requirement], asyncExecOptions(600_000));
193204
} catch {
194-
execFileSync(join(venv, "bin", "python"), ["-m", "pip", "install", "--disable-pip-version-check", "--no-input", "--ignore-requires-python", `mnemosyne-memory==${MNEMOSYNE_VERSION}`], { timeout: 180_000, stdio: "ignore" });
205+
if (preparationAbort.signal.aborted) break;
206+
await execFileAsync(join(venv, "bin", "python"), ["-m", "pip", "install", "--disable-pip-version-check", "--no-input", "--ignore-requires-python", `mnemosyne-memory==${MNEMOSYNE_VERSION}`], asyncExecOptions(180_000));
195207
}
196208
validatedPython = undefined;
197209
if (pythonRuntime()) return true;
198210
} catch (error) {
211+
if (preparationAbort.signal.aborted) break;
199212
console.warn(`Could not prepare Mnemosyne runtime with ${python}:`, (error as Error).message);
200213
}
201214
}
@@ -206,3 +219,16 @@ export function prepareMnemosyneRuntime(): boolean {
206219
validatedSemanticPython = undefined;
207220
return false;
208221
}
222+
223+
/** Prepare the optional Python runtime without blocking the HTTP server's
224+
* event loop on venv creation or package downloads. Concurrent callers share
225+
* one installation attempt. */
226+
export function prepareMnemosyneRuntime(): Promise<boolean> {
227+
if (!preparation) preparation = prepareMnemosyneRuntimeUnlocked();
228+
return preparation;
229+
}
230+
231+
/** Stop only the in-flight app-managed runtime installer during host shutdown. */
232+
export function cancelMnemosyneRuntimePreparation(): void {
233+
preparationAbort.abort();
234+
}

test/desktop.mjs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,10 @@ test("desktop entrypoint keeps the renderer sandboxed and data on the Mac", asyn
141141
assert.match(memoryRuntime, /--ignore-requires-python/);
142142
assert.match(memoryRuntime, /rmSync\(venv, \{ recursive: true, force: true \}\)/, "an invalid partial app-managed memory venv is repaired without touching agent databases");
143143
assert.match(memoryRuntime, /process\.platform === "darwin" \? \["\/usr\/bin\/python3"\]/, "macOS retries its bundled Python when a preferred interpreter cannot create the app-managed memory runtime");
144+
assert.match(memoryRuntime, /export function prepareMnemosyneRuntime\(\): Promise<boolean>/, "fresh-host memory installation is asynchronous instead of blocking application startup");
145+
assert.match(memoryRuntime, /export function cancelMnemosyneRuntimePreparation\(\)/, "host shutdown cancels an in-flight app-managed memory installation");
146+
const serverRuntime = await readFile(join(root, "src", "server", "index.ts"), "utf8");
147+
assert.match(serverRuntime, /const memoryRuntime = prepareMnemosyneRuntime\(\);[\s\S]*server\.listen\([\s\S]*memoryRuntime\.then/, "the HTTP server becomes ready before optional memory installation and initializes agent databases afterward");
144148
const memoryBridge = await readFile(join(root, "scripts", "mnemosyne-bridge.py"), "utf8");
145149
assert.match(memoryBridge, /sys\.version_info < \(3, 10\)/);
146150
assert.match(memoryBridge, /zip_longest/);

test/feedback-browser.mjs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ test("Feedback button saves a real report and the admin inbox shows it", async (
4949
...process.env,
5050
CTRL_DATA_DIR: dataDir,
5151
PORT: String(appPort),
52+
NODE_ENV: "test",
5253
HELM_CHANNEL_COMPUTER_BACKEND: "native",
5354
HELM_FEEDBACK_URL: `http://127.0.0.1:${collectorPort}/v1/feedback`,
5455
IMPROVEMENT_INTERVAL_MS: "600000",
@@ -60,6 +61,10 @@ test("Feedback button saves a real report and the admin inbox shows it", async (
6061
await browser.close().catch(() => undefined);
6162
app.kill("SIGTERM");
6263
provider.kill("SIGTERM");
64+
await Promise.race([
65+
new Promise((resolve) => app.once("exit", resolve)),
66+
new Promise((resolve) => setTimeout(resolve, 3_000)),
67+
]);
6368
await new Promise((resolve) => collector.close(resolve));
6469
rmSync(dataDir, { recursive: true, force: true });
6570
});

0 commit comments

Comments
 (0)