Skip to content

[RSI, performance] Land the kernel skill-sync marker so killed sessions do not re-pay it - #2405

Merged
sethkarten merged 8 commits into
mainfrom
rsi/perf-skill-sync-marker
Sep 17, 2026
Merged

sethkarten merged 8 commits into
mainfrom
rsi/perf-skill-sync-marker

Conversation

@sethkarten

@sethkarten sethkarten commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

What

  • The kernel Python bootstrap now writes its venv .bootstrap-version marker atomically (temp file + rename in the same dir, direct-write fallback) instead of one plain write at the very end of the skill sync.
  • bootstrapVenv persists the base marker (schema, runtime identity, snapshot, extra uv args) immediately after the runtime install, before the Python-skill sync.
  • syncPythonSkills persists the marker after every successfully installed skill group, so the marker always reflects completed work. The final unconditional write is kept, so install-failure semantics are unchanged: a failed skill warns, continues, and retries next startup; the warning is now exclusively for real install failures (marker-write failures propagate as bootstrap errors, like the final write on main).
  • No behavior change to the ready gate (exact marker match still short-circuits with zero uv), the per-skill skip (pyproject path + hash match), PRIME_AGENT_KERNEL_PYTHON overrides, legacy-schema rebuilds, or the bootstrap dir-lock.

Why

PR #2398's idle-RSS profile caught uv pip install --editable re-running the bundled Python-skill sync inside the measurement window in 10/10 trials (~140 MB uv resident at idle). Root cause: the marker was written only once, after the LAST skill install, and no base marker existed before the skill sync - so any session killed mid-sync (the benchmark, and real short-lived sessions) recorded nothing. The next startup then failed kernelBaseReady and took the full wipe path: rm -rf the venv, re-install Python + prime-agent-runtime + extras, and re-run every editable skill install. Short-lived sessions re-pay this forever; the sync never converges.

With this change, a killed session leaves a valid base marker plus the skills that already finished. The next session takes the skills-only path and installs only the remainder; once any session completes the sync, all later sessions hit the ready gate with zero uv invocations.

Local before/after

Same protocol as the benchmark harness: real uv 0.12.12, the real bundled release skills (11 with pyproject.toml), a uv shim logging every invocation, a throwaway venv, kill the process group mid-sync after 3 editable installs, then rerun to completion in a fresh process. All polls with explicit deadlines.

metric main branch
venv rebuild in rerun after kill YES (rm -rf + venv --seed + runtime install) NO (0 seed lines)
editable installs in rerun 11 (all re-paid) 9 (2 recorded at kill; remainder only)
peak uv RSS during rerun 67.1 MB (70.6 MB concurrent) 52.8-53.5 MB
kill-rerun wall time (warm uv cache) 3.25 s 2.30-2.62 s
cold full sync (sanity) 3.27 s / 11 installs 3.06 s / 11 installs
steady state after a completed sync 0 uv 0 uv

Cumulative short-session demo (one venv, 6 consecutive sessions killed mid-sync): on the branch the marker grows 2->4->6->8->10 skills across sessions, the venv is never rebuilt, the 6th session completes the sync, and later sessions run zero uv. On main every killed session wipes the venv and re-pays the whole bootstrap; the sync never converges.

The warm-cache wall-time delta is modest; the structural win (no venv wipe, no runtime+extras reinstall, only the missing skills) scales with cache-cold rebuild cost, which is the real-world regression the #2398 profile caught (~140 MB uv resident per session, 10/10 trials).

Testing

  • kernel-bootstrap.test.ts: 27/27 green (23 pre-existing + 4 new):
    • "writes the version marker before syncing Python skills" - the fake uv probes the marker at every editable install; verified failing on pristine main (every probe sees no marker because main writes only after the last install).
    • "resumes a sync interrupted before all skills completed" - the exact post-kill marker state resumes without a rebuild and skips the completed skill.
    • "resumes a real killed mid-sync process without rebuilding the venv" - a real tsx child is SIGKILLed (process group) mid-install; the rerun steals the stale bootstrap lock via PID liveness, skips the completed skill, and never rebuilds the venv.
    • "re-syncs a changed skill alongside unsynced skills" - a changed pyproject re-syncs while an unsynced skill installs, with no rebuild.
  • Other suites importing the kernel bootstrap: ipython-provisioner (21), repl-kernel-parent-watchdog (8 + 2 skipped), kernel-windows-process (6), binary-runtime-source (4), kernel-bootstrap-windows (2) - all green.
  • npm run check (biome + tsgo + installer + browser-smoke) passes.

Note

Medium Risk
Changes kernel bootstrap persistence and skill-sync failure handling on disk; incorrect marker logic could cause unnecessary venv rebuilds or stale skill manifests, though atomic writes and existing ready gates limit worst-case impact.

Overview
Fixes short-lived or killed sessions rebuilding the kernel venv and re-running the full Python skill sync because .bootstrap-version was only written once at the end of sync (or could be truncated mid-write).

bootstrap.ts now lands a base marker (runtime identity, empty skill list) right after the runtime install and before editable skill sync. Each successful skill install triggers writeMergedBootstrapVersion, which merges progress into the on-disk record; the final pass still does an authoritative writeBootstrapVersion. Marker updates use write-temp + rename with bounded retries so a crash never leaves a corrupt partial file that looks “missing” and forces a wipe.

Failed editable installs are not recorded (only appended after success); the loop continues so later skills can still install. Tests add fake-uv marker probes, a fail-path assertion, and a real SIGKILL mid-sync child process that proves the next startup skips completed skills and never runs uv venv again.

Reviewed by Cursor Bugbot for commit f7b547a. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Write skill-sync marker atomically and incrementally in bootstrapVenv

  • writeBootstrapVersion in bootstrap.ts now serializes to a temp file and renames into place, retrying 3 times with a 50 ms delay so the prior marker is preserved on total failure.
  • New writeMergedBootstrapVersion helper merges existing marker skills with newly installed ones, allowing progress to persist across sync interruptions.
  • bootstrapVenv writes a base marker (empty skill list) right after runtime install and before Python skill sync, so a partially-synced venv is recognized as warm.
  • syncPythonSkills records each successful skill install incrementally and skips failed installs from the marker; the final write still replaces the full list authoritatively.
  • Added a child-process test helper and an integration test that kills a hanging skill install, then verifies resume behavior from the persisted marker.
  • Behavioral Change: failed skill installs are no longer recorded in the marker at any point; previously they could appear transiently before the final authoritative write.

Macroscope summarized f7b547a.

Test-line budget

This PR must pass the repo's test-line-budget gate (node scripts/check-test-policy.mjs, run in CI): net added test lines may not exceed meaningful added source lines. Per the fleet-wide audit directive ("probably needs to remove redundant and unnecessary tests throughout. Remove unnecessary tests and we should have enough.") and the owner's follow-up ("nah dont grandfather them in. we should fix them now"), the branch compresses test lines by consolidation and removal of probe-verified duplicate coverage, with a lost-coverage ledger below.

  • Before: net test additions 334 exceeded source additions 35, plus 6 policy-category violations (2 explicit per-test timeouts, vi.waitFor + wall-clock-sleep polls).
  • After: net test additions 32 vs source 35 (gate passes; measured with the repo's checker against the PR merge-base). The two remaining policy annotations are rationaled on the real-kill test only (real killed-process semantics; no in-process signal exists).

Lost-coverage ledger (executed cuts, probe-verified): this branch's ledger documents NO distinct vector loss — every cut is either a consolidation or duplicate scaffolding, and each surviving pin was verified by mutating the behavior it must catch:

  1. "writes the version marker before syncing Python skills" (branch test) merged into "writes the base marker before the first install and keeps a failed skill out of the record": same first-install marker probe (base marker before sync); the second-install incremental timing witness now lives in the kill test's at-kill marker. Probe: removing the base-marker write fails the merged test.
  2. "resumes a sync interrupted before all skills completed" (branch test) removed: the resume observable (only the missing skill installs, no venv rebuild) is pinned by the kill test's rerun phase (hanging skill 2x, stale skill 1x, recorded skill 0x, zero venv lines).
  3. The two real-kill tests merged into one warm-seeded kill test: the cold-kill variant's venv-line-count vector was redundant with the warm variant's zero-venv-line assert; the gated-admission machinery (vi.waitFor, 30s timeout, createRuntime gate) was scaffolding around vectors the ungated child pins. The merged test now kills via one group SIGKILL + exit-event await in the finally.
  4. "re-syncs a changed skill alongside unsynced skills" (branch test) removed: the stale+missing combination is pinned by the kill test (the child pass reinstalls the stale skill; the rerun installs the missing one).
  5. Pre-existing "syncs a warm venv when a Python skill pyproject changes" deleted: all three vectors (stale hash -> reinstall, fresh hash in the marker, no venv rebuild) survive in the kill test. Probe: making the stale check always-match fails the kill test at the at-kill fresh-hash assert.
  6. Pre-existing "continues when a Python skill editable install fails and retries it next startup" deleted: fail-continue and not-recording survive in the new cold fail-probe test (probes: a rethrowing catch makes it reject; recording the failed skill fails the final-marker assert). Its retry segment duplicated the marker-driven resume decision pinned by the kill test's rerun.
  7. Three bespoke fake-python builders (one warm/rebuild row, two PRIME_AGENT_KERNEL_PYTHON rows) consolidated into a deniedProbes parameter on the shared writeFakePython helper; every row asserts the identical error message as before.

@github-actions

github-actions Bot commented Sep 16, 2026

Copy link
Copy Markdown

Prime Agent performance — completed

PR f7b547aa compared with main fc9f2ae2.

Overall: 0 regressed · 0 improved · 41 no clear change.

Metric Main This PR Change
Cold startup 2,134.6 ms 1,940.3 ms ≈ -194.3 ms (-9.10%)
Warm startup 1,279.7 ms 1,121.0 ms ≈ -158.7 ms (-12.40%)
Installation 14.08 s 13.60 s ≈ -0.48 s (-3.38%)
Compressed release artifacts 72.61 MB 72.59 MB ≈ -0.02 MB (-0.03%)
Installed footprint 568.64 MB 568.65 MB ≈ +0.0087 MB (+0.00%)
Idle memory, summed RSS 1,272.80 MB 1,277.22 MB ≈ +4.43 MB (+0.35%)

Python runtime

Metric Main This PR Change
Python kernel startup 139.5 ms 140.8 ms ≈ +1.4 ms (+0.98%)
Python cell round trip 0.577 ms 0.613 ms ≈ +0.036 ms (+6.27%)
Empty bash command 12.1 ms 12.3 ms ≈ +0.2 ms (+1.68%)
Bash git status 18.4 ms 17.2 ms ≈ -1.2 ms (-6.32%)
Bash 32 KiB output 12.7 ms 12.1 ms ≈ -0.6 ms (-4.43%)
35 cells / 9 shell calls 180.0 ms 195.4 ms ≈ +15.4 ms (+8.53%)
Python interrupt to done 1.521 ms 1.610 ms ≈ +0.089 ms (+5.84%)
Python state snapshot 25.4 ms 26.3 ms ≈ +0.9 ms (+3.36%)
Python state restore 370.9 ms 365.4 ms ≈ -5.5 ms (-1.47%)
Python idle RSS 33.56 MB 35.57 MB ≈ +2.00 MB (+5.97%)
Python RSS after pandas workload 95.99 MB 97.80 MB ≈ +1.81 MB (+1.89%)

Session transport

Metric Main This PR Change
Private frame decode, 32 MiB in 8 KiB chunks 51.6 ms 48.8 ms ≈ -2.8 ms (-5.47%)

UI interactions

Metric Main This PR Change
Resume large session (cold) 4,335.9 ms 4,312.2 ms ≈ -23.7 ms (-0.55%)
CPU, resume large session 6,830.0 ms 7,000.0 ms ≈ +170.0 ms (+2.49%)
Switch into large session 5,316.3 ms 4,722.8 ms ≈ -593.6 ms (-11.16%)
CPU, switch into large session 7,350.0 ms 6,790.0 ms ≈ -560.0 ms (-7.62%)
Open agents view from a session 172.0 ms 177.9 ms ≈ +5.8 ms (+3.38%)
CPU, open agents view 360.0 ms 350.0 ms ≈ -10.0 ms (-2.78%)
Full agents roster, many sessions 4.46 s 4.46 s ≈ +0.00034 s (+0.01%)
CPU, full agents roster 3.11 s 3.56 s ≈ +0.45 s (+14.47%)
Open another session from agents view 3,018.3 ms 2,870.1 ms ≈ -148.2 ms (-4.91%)
CPU, open from agents view 3,320.0 ms 3,280.0 ms ≈ -40.0 ms (-1.20%)
Reopen resident large session 616.7 ms 614.1 ms ≈ -2.6 ms (-0.42%)
CPU, reopen resident session 1,040.0 ms 1,100.0 ms ≈ +60.0 ms (+5.77%)
Open subagent session at depth 6 21,855.6 ms 21,394.3 ms ≈ -461.4 ms (-2.11%)
CPU, open subagent at depth 6 11,390.0 ms 10,780.0 ms ≈ -610.0 ms (-5.36%)
Open chain parent from agents view 4,425.4 ms 4,334.7 ms ≈ -90.6 ms (-2.05%)
CPU, open chain parent 4,830.0 ms 4,220.0 ms ≈ -610.0 ms (-12.63%)
Scheduled catalog, first request 2,497.0 ms 2,002.5 ms ≈ -494.5 ms (-19.81%)
CPU, scheduled catalog 4,320.0 ms 3,350.0 ms ≈ -970.0 ms (-22.45%)
Scheduled catalog, repeated request 1,487.8 ms 1,189.4 ms ≈ -298.5 ms (-20.06%)
CPU, repeated catalog 2,040.0 ms 1,710.0 ms ≈ -330.0 ms (-16.18%)
Cold worker with three catalog scans 1,780.0 ms 1,592.5 ms ≈ -187.5 ms (-10.53%)
CPU, cold worker and scans 5,050.0 ms 4,500.0 ms ≈ -550.0 ms (-10.89%)
UI memory after interactions 2,642.71 MB 2,675.68 MB ≈ +32.96 MB (+1.25%)

Sandbox cost: ~$0.1605 — no inference calls.
Run, logs, and downloadable raw results

Methodology and samples

Main resolved at 2026-09-17T22:42:30.438198+00:00. Harness fc9f2ae2.
Linux x64, 4 vCPU, 8 GB RAM, 20 GB disk; region us.
Image: node:24-bookworm@sha256:be23f54a88d34e8824c741b19b91064094f92c1c97b194144bfc8b50d67258e2.
Stock tools, skills, daemon, and Python bootstrap enabled; fresh homes and a fixed Git fixture.
Onboarding is dismissed; the editor starts without a selected model or submitted prompt.
Medians shown. Arrows require a 20% timing/memory change plus absolute floors and IQR.
These practical noise floors are not a statistical significance test.
Cold means stopped Prime processes; OS filesystem caches are not flushed.
No model requests or credentials. Installation excludes build/setup time.
Installer tarballs use loopback; npm/Python downloads use the network with fresh caches.
Artifact size counts release tarballs; footprint after first use includes registry packages.
MB is decimal. Summed RSS can double-count shared pages; PSS is recorded when available.
Provisioning, setup, and build durations are recorded separately in the raw results.
Kernel probes use the installed JSONL runtime, outside the TUI/TypeScript host.
Per trial: 50 Python cells, 5 calls per shell case, and one 35-cell mix (9 git status calls).
Cell/shell values are batch means; other runtime timings are single operations.
State fixture: a 10,000-row × 8-column integer DataFrame and a 10,000-integer list.
Restore runs in a fresh kernel, including pandas imports; kernel startup is excluded.
Kernel RSS covers the isolated Python process; loaded RSS follows the pandas workload.
Transport benches run node against the prepared source build, outside the installed home.
Frame decode times one 32 MiB private frame, snapshot-chunk header, pushed in
8 KiB chunks; the wire shape of multi-MB frames on the daemon-worker channels.
UI trials use a fresh fixture set: 194 top-level sessions including one ~40 MB transcript,
40 ledger fan-out children, and a 6-deep subagent chain (~46 spawn edges).
Large fixtures hold 1,999 complete triples (~5 MB JSONL); medium 119; subagents 399 each.
Interactions: cold --resume of a large session, warm /resume switch, left-arrow to agents view,
roster settle with many saved sessions, search-and-open of another large session,
reattaching to that resident session, opening the chain parent, and drilling to depth 6.
Readiness is the rendered transcript tail plus a confirmed editor echo.
CPU metrics sum utime+stime across the whole benchmark-user process tree per interaction.
UI memory sums RSS after the interactions; PTY byte counts are in the raw results.
A separate catalog fixture has 2,300 sessions, 2,298 edges, and 13 paused scheduled-job owners.
Catalog timings cover first/repeated reads and cold worker creation under three pending scans.
All expected jobs and owner metadata are checked; worker readiness excludes TUI rendering.
Costs estimate full sandbox lifetimes at configured rates, including setup and build.
Budget target: $1; not a billing cap. Performance changes are informational.
Failed or incomplete execution fails the workflow; saved artifacts remain available.
Each side stops a phase after 2 identical consecutive failures.
Skipped trials are not attempted samples. Warm startup requires a successful cold launch.

Metric Main successful/attempted PR successful/attempted Main spread PR spread
Cold startup 10/10 10/10 IQR 224.7 ms IQR 197.9 ms
Warm startup 10/10 10/10 IQR 200.4 ms IQR 163.4 ms
Installation 3/3 3/3 range 0.99 s range 0.63 s
Compressed release artifacts 1/1 1/1
Installed footprint 1/1 1/1
Idle memory, summed RSS 10/10 10/10 IQR 8.53 MB IQR 103.25 MB
Python kernel startup 10/10 10/10 IQR 19.5 ms IQR 14.6 ms
Python cell round trip 10/10 10/10 IQR 0.074 ms IQR 0.114 ms
Empty bash command 10/10 10/10 IQR 2.3 ms IQR 1.7 ms
Bash git status 10/10 10/10 IQR 4.1 ms IQR 1.8 ms
Bash 32 KiB output 10/10 10/10 IQR 1.2 ms IQR 1.7 ms
35 cells / 9 shell calls 10/10 10/10 IQR 11.4 ms IQR 28.6 ms
Python interrupt to done 10/10 10/10 IQR 0.037 ms IQR 0.751 ms
Python state snapshot 10/10 10/10 IQR 11.1 ms IQR 5.6 ms
Python state restore 10/10 10/10 IQR 11.1 ms IQR 39.0 ms
Python idle RSS 10/10 10/10 IQR 6.13 MB IQR 2.86 MB
Python RSS after pandas workload 10/10 10/10 IQR 5.87 MB IQR 3.55 MB
Private frame decode, 32 MiB in 8 KiB chunks 10/10 10/10 IQR 6.5 ms IQR 15.2 ms
Resume large session (cold) 3/3 3/3 range 354.6 ms range 975.1 ms
CPU, resume large session 3/3 3/3 range 660.0 ms range 2,220.0 ms
Switch into large session 3/3 3/3 range 508.3 ms range 634.3 ms
CPU, switch into large session 3/3 3/3 range 1,050.0 ms range 1,180.0 ms
Open agents view from a session 3/3 3/3 range 29.2 ms range 22.7 ms
CPU, open agents view 3/3 3/3 range 260.0 ms range 130.0 ms
Full agents roster, many sessions 3/3 3/3 range 0.41 s range 0.41 s
CPU, full agents roster 3/3 3/3 range 0.48 s range 0.84 s
Open another session from agents view 3/3 3/3 range 309.5 ms range 266.5 ms
CPU, open from agents view 3/3 3/3 range 570.0 ms range 630.0 ms
Reopen resident large session 3/3 3/3 range 27.1 ms range 89.8 ms
CPU, reopen resident session 3/3 3/3 range 70.0 ms range 230.0 ms
Open subagent session at depth 6 3/3 3/3 range 478.7 ms range 252.9 ms
CPU, open subagent at depth 6 3/3 3/3 range 560.0 ms range 260.0 ms
Open chain parent from agents view 3/3 3/3 range 72.6 ms range 436.0 ms
CPU, open chain parent 3/3 3/3 range 210.0 ms range 460.0 ms
Scheduled catalog, first request 3/3 3/3 range 407.8 ms range 313.2 ms
CPU, scheduled catalog 3/3 3/3 range 980.0 ms range 590.0 ms
Scheduled catalog, repeated request 3/3 3/3 range 291.7 ms range 367.5 ms
CPU, repeated catalog 3/3 3/3 range 370.0 ms range 280.0 ms
Cold worker with three catalog scans 3/3 3/3 range 61.4 ms range 293.1 ms
CPU, cold worker and scans 3/3 3/3 range 80.0 ms range 720.0 ms
UI memory after interactions 3/3 3/3 range 52.64 MB range 57.67 MB

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread packages/coding-agent/src/core/kernel/bootstrap.ts Outdated

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit be144c8. Configure here.

Comment thread packages/coding-agent/src/core/kernel/bootstrap.ts
Comment thread packages/coding-agent/src/core/kernel/bootstrap.ts
@sethkarten
sethkarten force-pushed the rsi/perf-skill-sync-marker branch 2 times, most recently from 378932c to 27b432a Compare September 17, 2026 07:00
The kernel Python bootstrap wrote its whole-version marker only once, after the last Python skill install. A session killed during the skill sync (short-lived benchmark sessions hit this every run) never recorded the marker, so the next startup wiped the venv and re-paid the runtime install plus every editable skill install.

Write the marker atomically (temp file + rename, direct write fallback) and persist it incrementally: base fields right after the runtime install in bootstrapVenv, then the accumulated skill list after every successful install group in syncPythonSkills. A killed session now leaves a valid base marker so the next one takes the skills-only path and installs only the remainder, while the final unconditional write keeps the install-failure semantics (failed skills retry next startup) unchanged.
…arker writes

The incremental marker write in syncPythonSkills persisted only the skills visited so far this session, so a completed install earlier in install order dropped skills already recorded in the marker but later in install order. A kill mid-sync then lost those skills from the marker, and the next startup re-ran their unchanged editable installs.

Merge fresh entries into the on-disk marker for the incremental write; the final unconditional write stays an authoritative replace, and a missing or corrupt marker contributes no base entries.

Adds a regression test that kills a child mid-install and asserts the marker keeps the already-recorded skills, and that resume skips their installs.
…wap fails

The rename fallback overwrote the marker in place, so a failure or kill mid-write left a partial marker that reads as absent and forces a full venv rebuild. Retry the atomic swap a bounded number of times, then leave the previous marker alone and drop the temp file.
The SIGKILL/resume tests poll an external process (fake-uv log appends
and OS pid liveness) with bounded deadlines and a generous per-test
timeout, which the objective test policy flags. Mark each expression
with a test-policy allow comment stating the specific external-process
reason, following the precedent in mcp-connection-store.test.ts.
@sethkarten
sethkarten force-pushed the rsi/perf-skill-sync-marker branch from 27b432a to d02235e Compare September 17, 2026 07:17
@sethkarten
sethkarten enabled auto-merge (squash) September 17, 2026 21:20
@sethkarten
sethkarten merged commit d96a990 into main Sep 17, 2026
60 checks passed
@sethkarten
sethkarten deleted the rsi/perf-skill-sync-marker branch September 17, 2026 23:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants