Skip to content

preserve accepted harness memories across concurrent saves - #2133

Closed
kevinjosethomas wants to merge 21 commits into
mainfrom
fix/concurrent-harness-memories
Closed

kevinjosethomas wants to merge 21 commits into
mainfrom
fix/concurrent-harness-memories

Conversation

@kevinjosethomas

@kevinjosethomas kevinjosethomas commented Sep 8, 2026 •

Copy link
Copy Markdown
Member

Concurrent kernel memory writes and host /refine saves could both report success, then the later save replaced the entire file from an older snapshot and silently discarded the earlier accepted memory. Fixes ENG-6007, independently reproduced from the symptoms reported in discussion #2009.

Both writers now acquire the same adjacent file lock, reread the latest state, and merge only changes since their load snapshot before replacing the file atomically. Unrelated memories and refinement events survive; conflicting edits to an entry, schema, or replaced history fail with a reload-and-retry error. Python also preserves the stored schema, generates distinct default refinement IDs, rejects new non-finite JSON values, and normalizes legacy invalid numbers while retaining valid data.

Validation on commit 32bf15dcb885310d6e7eec0bc04342e8eb254896:

  • 116 TypeScript tests, 41 Python harness controls, and npm run check pass locally and in a Linux Prime sandbox. The TypeScript set includes 30 concurrency regressions and existing refinement/literal-prompt controls.
  • The original bug reproduced against baseline in both save orders. New schema, refinement-history, identifier, and invalid-number regressions also failed before their respective fixes and pass afterward.
  • 29 installed-artifact cases pass using the packaged host writer and shipped Python runtime, with no model calls. The installed version is 0.9.4-eng6007.32bf15dcb8; the loaded Python module matches the installed artifact and pinned source by SHA-256.
  • Current CI passes, including all test shards and build/check. CodeQL, changelog, and Linear ticket checks also pass.

CI setup now scopes apt dependency installation to Ubuntu sources after repeated unrelated Chrome repository hash failures. The shared literal-prompt test correction checks the final provider user message, allowing the persistent-memory context added on main while preserving the exact prompt and transcript assertions.

Limits: both writers must be updated. A killed writer can leave a lock that must be removed only after all writers stop. Controlled interleavings establish the tested persistence behavior, not production failure frequency. Automatic stale-lock recovery, power-loss durability, and separate audit-log transactions are outside this change. Live inference is not part of this validation.

Linear: ENG-6007


Note

Medium Risk
Changes shared continual-harness persistence used by refinement and the Python RLM kernel; incorrect merge/validation logic could still lose or block memories, though conflicts now fail closed and coverage is extensive.

Overview
Fixes ENG-6007, where concurrent host /refine saves and Python kernel writes could both succeed but the last full-file write dropped the other writer’s accepted memories.

TypeScript and Python now share an adjacent harness_state.json.lock, re-read disk under the lock, and merge only deltas from each writer’s load snapshot (mergeHarnessStateChanges / _merge_harness_changes). Unrelated entries and compatible refinement appends survive; conflicts on the same entry, schema, or rewritten refinement history fail with reload-and-retry and persist nothing from that save. Python also serializes mutations on a shared HarnessState instance with an RLock.

Save behavior change: corrupt, malformed schema-1, unsupported schema, or lossy/non-finite JSON files stay read-only—loads still degrade for session use, but saves refuse to overwrite instead of self-healing. Writes must match schema 1; default Python refinement IDs are UUID-based to avoid duplicates under concurrency. Protocol and ops notes are in harness-persistence.md, with a large 6007 regression suite covering host/Python interleavings.

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

Note

Preserve harness memories across concurrent TypeScript and Python saves

  • Adds a shared cross-runtime lock directory that serializes harness saves between TypeScript and Python, failing after a 10-second timeout. Lock is removed unconditionally on success or failure.
  • Introduces three-way state merging in harness-persistence.ts and harness.py: saves re-read the latest on-disk state, preserve unrelated entries and compatible appended refinement events, and reject conflicting changes to the same entry, history, or schema.
  • Makes malformed, non-finite, or unsupported-schema harness files read-only: reads still return an empty view, but saves now refuse to overwrite instead of self-healing the file.
  • Changes default refinement event IDs from sequence-derived to UUID-based to avoid duplicates under concurrent writes.
  • Documents the full protocol in harness-persistence.md.
  • Behavioral Change: saves to a malformed or unsupported-schema file now throw invalidHarnessStateError / _invalid_harness_state_error rather than silently rewriting it; saves now block on the shared lock and may fail with a timeout error after 10 seconds if another writer holds it.

Changes since #2133 opened

  • Added schema validation to reject non-numeric and non-finite values before saving harness state [5e4293a]
  • Updated and added tests to verify schema validation behavior [5e4293a]
  • Added comprehensive validation infrastructure for harness state writes [d1b8199]
  • Replaced non-finite number checks with unsafe JSON number detection [d1b8199]
  • Modified harness state loading to tolerate malformed data while marking it invalid for writes [d1b8199]
  • Modified harness state saving to validate before writing and preserve existing content on validation failure [d1b8199]
  • Added regression test verifying concurrent save protection for corrupted harness files [d1b8199]
  • Clarified malformed file handling in harness persistence documentation [d1b8199]
  • Updated rlm.harness._contains_unsafe_json_number utility to detect integer-valued floats exceeding the maximum safe JSON integer [23bb98d]
  • Modified concurrent harness memory persistence test to corrupt the memory map container instead of individual entries [23bb98d]
  • Added per-instance reentrant locking to HarnessState in Python to serialize concurrent method calls [0bb2d6b]
  • Added validation to reject harness state JSON files containing numeric literals that lose precision when parsed [0bb2d6b]
  • Added tests verifying concurrent mutation serialization and rejection of lossy decimal literals in harness state files [0bb2d6b]
  • Updated documentation and changelog noting the concurrent mutation fix and Python serialization behavior [0bb2d6b]
  • Modified readHarnessStateResult function to preserve harness state entries and refinements when unsafe JSON numbers are detected [1c114e0]
  • Expanded concurrent harness memory persistence test coverage to include parameterized tests for both writer types and multiple unsafe number scenarios [1c114e0]
  • Added validation to reject negative zero and invalid decimal representations during JSON parsing in Python harness state loading [96f2fef]
  • Modified normalizeJsonNumberToken utility to preserve the sign prefix when normalized significant digits collapse to empty after trimming leading zeros [96f2fef]
  • Extended test coverage to verify rejection of extreme exponent forms and signed zero tokens across Python and host writers [96f2fef]
  • Modified JSON safety validation utilities to reject negative zero [6ae7772]
  • Added test coverage for negative zero rejection in harness memory persistence [6ae7772]
  • Modified saveHarnessState within the refinement module to select merge baseline based on whether the snapshot's recorded statePath matches the current target path, using the prior snapshot state only when paths match and falling back to an empty harness state otherwise [d06ef04]
  • Extended the harnessSnapshots module-level WeakMap value type to include a statePath property, introduced resolveHarnessStateTarget utility function for computing canonical target paths by realpath-resolving parent directories, and updated loadHarnessState and saveHarnessState to record resolved state paths in snapshots [d06ef04]
  • Added regression test case verifying that unchanged entries are copied when a loaded harness state is saved to a different directory [d06ef04]

Macroscope summarized 4744ae2.

@kevinjosethomas
kevinjosethomas marked this pull request as ready for review September 8, 2026 23:57
Comment thread prime-agent-runtime/src/rlm/harness.py Resolved
Comment thread prime-agent-runtime/src/rlm/harness.py Resolved
Comment thread prime-agent-runtime/src/rlm/harness.py Resolved
Comment thread prime-agent-runtime/src/rlm/harness.py Resolved
Comment thread prime-agent-runtime/src/rlm/harness.py Resolved
return state;
}

function readHarnessState(statePath: string, scope: HarnessScope): HarnessState {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The TS side still wipes a legacy NaN-poisoned harness file: readHarnessState JSON.parse-fails, degrades to empty state, and the next refinement save silently overwrites the whole store with that empty state. The recovery added here lives only in the Python writer, so whichever writer touches the file first decides whether the user keeps their harness — the exact legacy scenario this PR fixes on the other writer. Wiring the same recovery into the TS reader — with a cross-writer round-trip test (poisoned file → TS load/save → Python load, and the reverse) so the two recoveries can't drift — would make the outcome writer-independent.

[written by prime-agent, checked by snimu]

}
merged.refinements = structuredClone(proposed.refinements);
}
if (proposed.schema !== baseline.schema) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Both readers accept any finite schema number, so a future-schema file loads fine — and an old Python load->save then strips its unknown entry fields and kinds while keeping the new schema number, leaving the file silently corrupted yet still stamped with that newer number. A schema > known guard that preserves the file untouched or refuses the save would close this. The round-trips are also asymmetric — TS preserves unknown entry fields while Python strips them — worth aligning while the schema handling is open.

[written by prime-agent, checked by snimu]

@github-actions

github-actions Bot commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor

Prime Agent performance — completed

PR d06ef048 compared with main 5255a6b8.

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

Metric Main This PR Change
Cold startup 2,899.4 ms 2,896.1 ms ≈ -3.3 ms (-0.11%)
Warm startup 1,671.3 ms 1,669.6 ms ≈ -1.7 ms (-0.10%)
Installation 27.37 s 27.15 s ≈ -0.22 s (-0.81%)
Compressed release artifacts 11.13 MB 11.14 MB ≈ +0.01 MB (+0.12%)
Installed footprint 597.55 MB 597.67 MB ≈ +0.12 MB (+0.02%)
Idle memory, summed RSS 1,043.41 MB 1,036.43 MB ≈ -6.98 MB (-0.67%)

Python runtime

Metric Main This PR Change
Python kernel startup 123.7 ms 125.4 ms ≈ +1.7 ms (+1.40%)
Python cell round trip 0.505 ms 0.485 ms ≈ -0.020 ms (-3.95%)
Empty bash command 10.7 ms 10.8 ms ≈ +0.099 ms (+0.92%)
Bash git status 16.8 ms 16.0 ms ≈ -0.8 ms (-4.67%)
Bash 32 KiB output 11.6 ms 11.3 ms ≈ -0.3 ms (-2.61%)
35 cells / 9 shell calls 156.0 ms 161.8 ms ≈ +5.8 ms (+3.74%)
Python interrupt to done 1.543 ms 1.463 ms ≈ -0.080 ms (-5.19%)
Python state snapshot 24.2 ms 25.1 ms ≈ +0.9 ms (+3.77%)
Python state restore 339.9 ms 337.5 ms ≈ -2.3 ms (-0.69%)
Python idle RSS 36.21 MB 36.11 MB ≈ -0.10 MB (-0.28%)
Python RSS after pandas workload 98.35 MB 98.81 MB ≈ +0.46 MB (+0.47%)

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

Methodology and samples

Main resolved at 2026-09-11T20:08:48.296453+00:00. Harness 5255a6b8.
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.
Costs estimate full sandbox lifetimes at configured rates, including setup and build.
Budget target: $1; not a billing cap. Checks are informational.

Metric Main successful/attempted PR successful/attempted Main spread PR spread
Cold startup 10/10 10/10 IQR 81.6 ms IQR 37.9 ms
Warm startup 10/10 10/10 IQR 36.9 ms IQR 48.6 ms
Installation 3/3 3/3 range 0.89 s range 0.28 s
Compressed release artifacts 1/1 1/1 — —
Installed footprint 1/1 1/1 — —
Idle memory, summed RSS 10/10 10/10 IQR 13.53 MB IQR 16.61 MB
Python kernel startup 10/10 10/10 IQR 4.3 ms IQR 5.0 ms
Python cell round trip 10/10 10/10 IQR 0.117 ms IQR 0.024 ms
Empty bash command 10/10 10/10 IQR 0.9 ms IQR 0.3 ms
Bash git status 10/10 10/10 IQR 1.1 ms IQR 0.9 ms
Bash 32 KiB output 10/10 10/10 IQR 0.9 ms IQR 0.5 ms
35 cells / 9 shell calls 10/10 10/10 IQR 5.3 ms IQR 3.6 ms
Python interrupt to done 10/10 10/10 IQR 0.120 ms IQR 0.089 ms
Python state snapshot 10/10 10/10 IQR 1.5 ms IQR 1.6 ms
Python state restore 10/10 10/10 IQR 13.8 ms IQR 14.4 ms
Python idle RSS 10/10 10/10 IQR 1.89 MB IQR 4.31 MB
Python RSS after pandas workload 10/10 10/10 IQR 2.44 MB IQR 4.51 MB

Comment thread prime-agent-runtime/src/rlm/harness.py Resolved
Comment thread prime-agent-runtime/src/rlm/harness.py Resolved

@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 prime-agent-runtime/src/rlm/harness.py Resolved

@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 96f2fef. Configure here.

Comment thread prime-agent-runtime/src/rlm/harness.py Resolved
@sethkarten sethkarten closed this Sep 17, 2026
@sethkarten
sethkarten deleted the fix/concurrent-harness-memories branch September 17, 2026 07:01
@sethkarten
sethkarten restored the fix/concurrent-harness-memories branch September 17, 2026 19:04
@kevinjosethomas
kevinjosethomas deleted the fix/concurrent-harness-memories branch September 30, 2026 01: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.

4 participants