Skip to content
Merged
1 change: 1 addition & 0 deletions packages/coding-agent/.changes/perf-skill-sync-marker.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Fixed short-lived sessions re-paying the whole Python kernel skill sync or wiping and rebuilding the venv after being killed mid-sync: the bootstrap version marker is now written atomically and persisted incrementally (base first, then after every installed skill), so the next session resumes only the remaining skills instead of starting over.
72 changes: 66 additions & 6 deletions packages/coding-agent/src/core/kernel/bootstrap.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createHash, randomUUID } from "node:crypto";
import { constants, existsSync, readdirSync, readFileSync } from "node:fs";
import { access, mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
import { access, mkdir, readdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { stderr, stdin } from "node:process";
Expand Down Expand Up @@ -97,8 +97,13 @@ const REQUIRED_HARNESS_METHODS = [
];
export const RUNTIME_READY_CHECK = `import inspect; import rlm; from rlm import McpIntegration; import rlm.mcp as mcp; from rlm.harness import HarnessEntry; _harness_methods = ${JSON.stringify(REQUIRED_HARNESS_METHODS)}; assert callable(mcp.list_tools); assert callable(mcp.call_tool); assert all(callable(getattr(mcp, _m, None)) for _m in ${JSON.stringify(REQUIRED_MCP_DISCOVERY_METHODS)}), "rlm.mcp is missing MCP discovery methods (list_plugins, search_plugins, list_connections, search_tools, describe_tool); the kernel venv needs a current prime-agent-runtime"; assert callable(rlm.spawn); assert hasattr(rlm, 'rlm'); assert callable(rlm.rlm.spawn); assert inspect.signature(rlm.spawn).parameters['name'].default is inspect.Parameter.empty; assert not hasattr(rlm, 'run'); assert not hasattr(rlm.rlm, 'run'); assert callable(rlm.host_request); assert callable(rlm.find_models); assert callable(rlm.rlm.find_models); assert callable(rlm.create_session); assert callable(rlm.rlm.create_session); assert callable(rlm.progress_note); assert callable(rlm.rlm.progress_note); assert hasattr(rlm, 'harness'); assert hasattr(rlm, 'get_harness_state'); assert hasattr(rlm.rlm, 'harness'); assert hasattr(rlm.rlm, 'get_harness_state'); assert all(callable(getattr(_harness, _method, None)) for _harness in (rlm.harness, rlm.rlm.harness) for _method in _harness_methods); assert 'reference' in HarnessEntry.__dataclass_fields__; assert 'scope' in HarnessEntry.__dataclass_fields__; assert 'reference' in inspect.signature(rlm.harness.create_skill).parameters; assert 'reference' in inspect.signature(rlm.harness.update_skill).parameters; assert 'global_' in inspect.signature(rlm.harness.create_memory).parameters; assert 'global_' in inspect.signature(rlm.get_harness_state).parameters; assert not hasattr(rlm, 'background'); assert not hasattr(rlm.rlm, 'background'); from rlm.bash import BashHandle, BashResult; assert callable(rlm.bash); assert all(callable(getattr(BashHandle, _m, None)) for _m in ('tail', 'output', 'poll', 'kill')); assert {'exit_code', 'output', 'duration'} <= set(BashResult.__dataclass_fields__); import rlm.repl as _repl; assert callable(_repl.main); assert callable(_repl.emit); assert callable(_repl.host_request); assert callable(_repl.is_active); assert _repl.PROTOCOL_VERSION == 3; assert callable(rlm.emit); assert not hasattr(rlm, 'HOST_COMM_TARGET'); assert not hasattr(mcp, 'install_shutdown_hook')`;
const BOOTSTRAP_VERSION_FILE = ".bootstrap-version";
const BOOTSTRAP_VERSION_TMP_FILE = `${BOOTSTRAP_VERSION_FILE}.tmp`;
const BOOTSTRAP_LOCK_NAME = ".bootstrap.lock";
const BOOTSTRAP_LOCK_RETRY_MS = 100;
// Bounded retry for the atomic marker swap: replacing an existing marker can
// fail while a scanner or editor holds the file open.
const BOOTSTRAP_MARKER_SWAP_ATTEMPTS = 3;
const BOOTSTRAP_MARKER_SWAP_RETRY_MS = 50;
const BOOTSTRAP_LOCK_STALE_WITHOUT_PID_MS = 30_000;

let inFlightEnsureKernelPython: { key: string; promise: Promise<string> } | null = null;
Expand Down Expand Up @@ -689,7 +694,51 @@ async function writeBootstrapVersion(
extraUvArgs: DEFAULT_RLM_EXTRA_UV_ARGS,
pythonSkills: [...pythonSkills],
};
await writeFile(path.join(venv, BOOTSTRAP_VERSION_FILE), `${JSON.stringify(version)}\n`, "utf8");
const filePath = path.join(venv, BOOTSTRAP_VERSION_FILE);
const tmpPath = path.join(venv, BOOTSTRAP_VERSION_TMP_FILE);
const serialized = `${JSON.stringify(version)}\n`;
// Write-then-rename is atomic: a kill mid-write can never leave a partial
// marker, which would read as absent and force a rebuild. Replacing an
// existing marker can fail transiently while another process holds it, so
// retry the swap. A marker that stays unwritten is only stale: the next
// startup re-syncs skills, whereas an in-place overwrite truncated by a
// failure or a kill would read as absent and rebuild the whole venv.
let lastError: unknown;
for (let attempt = 1; attempt <= BOOTSTRAP_MARKER_SWAP_ATTEMPTS; attempt += 1) {
try {
await writeFile(tmpPath, serialized, "utf8");
await rename(tmpPath, filePath);
return;
} catch (error) {
lastError = error;
// Retry below; the previous marker is still intact.
}
if (attempt < BOOTSTRAP_MARKER_SWAP_ATTEMPTS) await sleep(BOOTSTRAP_MARKER_SWAP_RETRY_MS);
}
// Give up without overwriting the marker in place: a write truncated there
// by a failure or a kill reads as absent and forces a full venv rebuild,
// while the untouched previous marker stays valid. The failure still
// surfaces: a marker this process cannot write is one the next startup
// cannot trust.
await rm(tmpPath, { force: true }).catch(() => undefined);
throw lastError;
}

// Incremental marker write that merges fresh entries into the skills already
// recorded on disk. A missing or corrupt marker contributes no base entries.
async function writeMergedBootstrapVersion(
venv: string,
runtimeIdentity: string,
pythonSkills: readonly BootstrapPythonSkill[],
): Promise<void> {
const version = await readBootstrapVersion(venv);
const merged = new Map(
(version?.pythonSkills ?? []).map((skill) => [`${skill.importName}\0${skill.packagePath}`, skill]),
);
for (const skill of pythonSkills) {
merged.set(`${skill.importName}\0${skill.packagePath}`, skill);
}
await writeBootstrapVersion(venv, runtimeIdentity, [...merged.values()]);
}

function runtimeCandidateDirs(): string[] {
Expand Down Expand Up @@ -781,6 +830,10 @@ async function bootstrapVenv(
STATE_SNAPSHOT_REQUIREMENT,
...DEFAULT_RLM_EXTRA_UV_ARGS,
]);
// Land the base marker before the skill sync: a session killed mid-sync
// must leave the next one on the skills-only path instead of wiping the
// venv and re-paying the runtime install.
await writeBootstrapVersion(venv, runtimeIdentity, []);
await syncPythonSkills(uv, venv, python, runtimeIdentity, pythonSkills, options);
}

Expand Down Expand Up @@ -848,16 +901,23 @@ async function syncPythonSkills(
...formatPythonSkillInstallArgs(skill),
...localDependencyArgs,
]);
installedPythonSkills.push(
skill,
...localDependencies.filter((dependency) => !installedPythonSkills.includes(dependency)),
);
} catch (error) {
reportProgress(
options,
`Warning: Python skill ${skill.importName} failed to install and will be unavailable: ${errorMessage(error)}`,
);
continue;
}
installedPythonSkills.push(
skill,
...localDependencies.filter((dependency) => !installedPythonSkills.includes(dependency)),
);
// Persist progress after every completed install so a killed session
// resumes at the first missing skill instead of re-syncing from scratch.
// Merge with the on-disk marker so skills already recorded but not yet
// visited this sync (they sit later in install order) survive this
// incremental write; the final write below stays an authoritative replace.
await writeMergedBootstrapVersion(venv, runtimeIdentity, installedPythonSkills);
}
await writeBootstrapVersion(venv, runtimeIdentity, installedPythonSkills);
}
Expand Down
11 changes: 11 additions & 0 deletions packages/coding-agent/test/helpers/kernel-sync-child.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { ensureKernelPython, type KernelPythonSkill } from "../../src/core/kernel/bootstrap.js";

// Runs one ensureKernelPython pass as its own process so a test can SIGKILL it
// mid-sync and prove that the next pass resumes instead of rebuilding.
const skillsArg = process.env.KERNEL_SYNC_CHILD_SKILLS;
if (!skillsArg) {
throw new Error("KERNEL_SYNC_CHILD_SKILLS is required");
}
const pythonSkills: readonly KernelPythonSkill[] = JSON.parse(skillsArg);
const python = await ensureKernelPython({ pythonSkills });
console.log(`KERNEL-SYNC-CHILD-DONE ${python}`);
Loading
Loading