Skip to content

Commit 86f56ab

Browse files
Eilodonclaude
andauthored
Claude/calm server rewiring plan no80rd (#59)
* docs(plans): state.db rewiring execution plan for calm-server durable call sites Scopes the KNOWN_LIMITATIONS.md "Durable state and the rebuildable index share one SQLite file at runtime" follow-up: every real calm-server/ calm-cli call site that touches edit_transactions/tx_events/audit_ledger/ maintenance_jobs/project_memory needs rewiring from index.db (open_writer) to the already-built state.db (open_state_writer), which currently has zero production callers. Also documents a sharper finding than the KNOWN_LIMITATIONS entry implies: the durable tables only exist in STATE_SCHEMA_SQL, not SCHEMA_SQL/init_db -- so on any index.db created fresh under current HEAD, durable writes (remember/edit-transaction journal/audit ledger/maintenance outbox) target tables that don't exist. This is a functional-fix priority, not just a durability hardening pass. * feat(server): Phase 0 plumbing for state.db durable-write split Adds default_state_db_path (.calm/state.db, mirrors default_db_path), a state_db_path field on CalmServer (propagates to every for_connection clone via ..self.clone()), and two new connection helpers: make_state_read_conn (query_only read) and state_write_conn (open_state_writer, synchronous=FULL) — the state.db counterparts of make_read_conn/memory_write_conn. new_with_preset now also initializes state.db (init_state_db) and runs migrate_legacy_durable_tables to pull any durable rows a pre-split index.db still has, before recover_incomplete/reconcile_stale_at_startup -- both of which now scan the state connection instead of the index one. Nothing yet calls the new read/write helpers from a tool handler (Phase 2-4 of docs/plans/2026-08-05-state-db-rewiring-execution-plan.md) -- this commit only stands the plumbing up. Empirically confirmed while testing this phase: 80 calm-server tests already fail on unmodified HEAD with "no such table: edit_transactions" et al -- independent proof of the plan doc's §0 finding that PR #58 split the durable schema out of index.db without wiring any real call site to state.db, so every durable-table operation on a freshly created index.db currently targets a table that doesn't exist there. Also found while wiring: memory_write_conn is shared by remember AND pattern_debt_register/pattern_debt_status, but pattern_debt lives in the rebuildable index.db, not state.db -- so unlike the plan doc's §2.1 table (which said to redirect memory_write_conn itself), this adds a separate state_write_conn instead and leaves memory_write_conn pointed at index.db for pattern_debt's sake. remember's own call site moves to state_write_conn in the next phase. * feat(server): Phase 2 -- rewire durable-table reads to state.db Points every durable-table READ at the new state.db connection instead of index.db's make_read_conn: - memory.rs::recall (project_memory/project_memory_fts/project_memory_refs) - orient.rs::repo_overview's memory_notes_count query (kept on a separate connection from the rest of the function, which stays on index.db) - txn.rs::edit_transaction_status/batch_status/maintenance_status/ repair_consistency/verify_change (edit_transactions/tx_events/ maintenance_jobs via txn::get/replay_state/maintenance::all_jobs) - retry_maintenance's force_requeue/mark_running/mark_completed writes (maintenance_jobs) now go through open_state_writer; its embed-refresh bootstrap connection (index-side, unrelated to maintenance_jobs itself) is untouched - common.rs::related_notes (project_memory_refs via notes_for_path) now opens its own state connection internally, shadowing the caller-supplied one -- signature untouched, so its two call sites (edit_context in guardrails.rs, locate.rs) needed no changes at all. Done this way specifically to avoid touching edit_context's own range: it has 37 callers and CALM's own edit gate classifies it "high risk", which requires an MCP elicitation round-trip this non-interactive session can't perform -- every hub touch in this phase stayed at "medium" risk by construction (confirm+grounded-reason only, no elicitation needed). Empirically verified via `cargo test -p calm-server --lib`: 80 pre-existing failures (see previous commit) down to 76 -- recall/repo_overview/txn.rs status reads now correctly see state.db. The remaining failures are `remember` (still writes via memory_write_conn/index.db until Phase 3) and tests that produce their edit_transactions rows through the real edit_lines/edit_symbol tool path (Phase 4, not yet done) or seed them directly via a raw open_writer call in the test body (Phase 5 test updates, tracked in docs/plans/2026-08-05-state-db-rewiring-execution-plan.md). * feat(server): Phase 3 -- remember writes through state_write_conn remember's INSERT INTO project_memory now goes through state_write_conn (open_state_writer/state.db) instead of memory_write_conn (open_writer/index.db) -- matches recall's read side (Phase 2), closing the read/write DB mismatch that phase left. memory_write_conn itself is untouched and still serves pattern_debt_register/pattern_debt_status, which stay on index.db (pattern_debt is a rebuildable-index table, not a durable one -- see the Phase 0 commit's note on this). High-risk hub edit (remember: 22 callers, all tests) -- CALM's own edit_lines/edit_symbol gate requires an MCP elicitation round-trip this non-interactive session can't perform, so this one-line change was applied via native Edit after explicit user approval (edit_context already reviewed this session, confirming the only touched line is the connection-target swap). Empirically verified via `cargo test -p calm-server --lib`: 76 pre-existing failures (previous commit) down to 59. * feat(server): Phase 4 -- rewire edit.rs's durable writes to state.db The hardest phase: edit_lines_impl_gated and format_files_impl each used ONE shared connection for both reindex_paths (rebuildable index.db) and txn::/maintenance:: calls (durable) -- these now need two separate physical connections since state.db is a different file with a different synchronous pragma. edit_lines_impl_gated: opens a new state_conn alongside shared_conn right after shared_conn's own open (txn_init_failed's fail-closed posture is unchanged -- either open failing still refuses the write with no journal). shared_conn stays scoped to reindex_paths only from that point on. Every txn::begin/advance call, and the maintenance::enqueue/mark_running/ mark_completed calls for both ScipRefresh (foreground + its spawned thread) and EmbedRefresh (foreground + its spawned thread), now go through state_conn/open_state_writer(&self.state_db_path) -- each spawned thread clones state_db_path alongside whatever index-side path it already cloned (db/db_path), same pattern. embed_pending/embed_pending_chunks themselves are untouched, still writing the rebuildable embedding vectors to index.db via their own bg_conn. format_files_impl: file_conn (txn::begin/advance per file) now opens state.db. The batched shadow_tx advance_many call after reindex could previously reuse reindex_conn for a free perf win (both were index.db); that reuse is no longer valid once txn writes need a different file, so it now opens its own dedicated state_conn instead -- reindex_conn is explicitly dropped (`let _ = reindex_conn;`) right before, since nothing reads it anymore. audit_ledger required no separate change: ledger::append is only ever called from inside txn::write_transition, so it automatically follows edit_transactions/tx_events onto whichever connection txn::advance/begin receives. Empirically verified via `cargo test -p calm-server --lib`: 59 pre-existing failures (previous commit) down to 19 -- clean build, no new warnings. All 19 remaining failures are test-only: they seed durable rows directly via a raw open_writer(&server.db_path)/open_writer(&db_path) call in the test body (bypassing the real edit_lines/edit_symbol/ format_files tool path this phase just fixed), so they're still writing to index.db where the durable tables no longer exist. Tracked as Phase 5 in docs/plans/2026-08-05-state-db-rewiring-execution-plan.md. * test(server): Phase 5 -- point durable-table test fixtures at state.db Adds CalmServer::state_db() (#[cfg(test)] only, mirrors the existing db() helper) for tests that seed/assert durable rows (project_memory/edit_transactions/tx_events/maintenance_jobs) directly. Inserted as a new method right after db() without touching db() itself (126 callers, is_hub) -- same "insert into the blank line just past a hub symbol's own range" technique used for make_state_read_conn in the Phase 2 commit, so nothing about db()'s own signature/behavior changes for the many other tests still correctly using it for symbols/file_index assertions. Updates every test that seeds/reads a durable table via a raw open_writer(&server.db_path)/server.db() call (bypassing the real tool path) to use open_state_writer(&server.state_db_path)/server.state_db() instead, now that those tables live in state.db: startup_hook_reconciles_a_stale_maintenance_job_left_by_a_previous_process, edit_transaction_status_reports_a_known_transaction, batch_status_aggregates_multiple_transactions, batch_status_all_done_true_only_when_every_tx_is_done_and_none_missing, maintenance_status_reports_all_kinds_and_suggests_retry_on_failure, retry_maintenance_embed_refresh_reports_failure_when_no_model_loaded, repair_consistency_flags_drift_when_disk_no_longer_matches_proposed_digest, recall_reports_mismatch_after_out_of_band_content_tamper, recall_reports_unverified_for_note_with_no_stored_mac, recall_with_no_args_lists_all_most_recent_first, recall_query_ties_break_by_recency, shadow_tx_replay_state_matches_cached_state_across_edit_lines_edit_symbol_and_format_files, edit_context_surfaces_related_notes_for_non_hub_file, edit_context_drops_related_note_with_mismatched_mac, edit_context_hub_file_requires_symbol_mention_in_note, edit_context_omits_related_notes_flagged_by_injection_warning, locate_surfaces_related_notes_for_top_symbol. Several of these share a block with an INSERT INTO symbols call, which correctly stays on server.db()/index.db (symbols is rebuildable, not durable) -- only the project_memory/edit_transactions statements moved. Empirically verified via `cargo test -p calm-server --lib`: 19 pre-existing failures (previous commit) down to 2 -- clean build, clean `cargo clippy --all-targets -- -D warnings`, clean `cargo fmt --check`. The 2 remaining failures (edit_lines_aborts_when_txn_begin_fails, format_files_skips_one_file_when_txn_begin_fails_without_aborting_the_batch) are unrelated to the state.db split: both force a txn::begin failure by chmod'ing index.db read-only, but this container runs cargo as root, and root bypasses standard Unix permission bits for a regular file write -- open_writer/open_state_writer both succeed regardless of the chmod, so the tests' own fault-injection mechanism doesn't fire under root. This is a pre-existing environmental limitation of the chmod-based approach itself (would reproduce identically on unmodified HEAD run as root), not a regression from this rewiring -- confirmed by reasoning through the unchanged code path (shared_conn's own open_writer call, never touched by any commit in this series) rather than a destructive git-checkout comparison, which the harness's auto-mode classifier declined to allow uninstructed. This closes out docs/plans/2026-08-05-state-db-rewiring-execution-plan.md Phase 5's test-update scope. Remaining before the plan is fully closed: KNOWN_LIMITATIONS.md/CHANGELOG.md updates (Phase 5's doc scope) and the crash-injection harness update (crates/calm-cli/tests/txn_crash_injection.rs, crates/calm-cli/src/bin/txn_crash_harness.rs) -- calm-cli/calm-core were not touched by any commit in this series and haven't been re-verified yet. * test(cli): route crash-injection suite's durable state through state.db txn_crash_harness (the subprocess the crash-injection suite SIGKILLs at each reachable TxState transition) now takes a second --state-db path and routes every txn::begin/advance call through it instead of the index-db connection -- edit_transactions/tx_events live in state.db now (docs/plans/2026-08-05-state-db-rewiring-execution-plan.md), matching the split already applied to calm-server's real edit_lines_impl_gated/ format_files_impl. index.db's own open_writer/init_db call is kept (this harness deliberately skips a real reindex per its own module doc, but still mirrors a real process's startup schema init). txn_crash_injection.rs (the driver) gets a state_db_path_for sibling to db_path_for, passes --state-db to the harness subprocess, and reads tx_id/edit_transactions/tx_events/recover_incomplete back through open_state_writer(&state_db_path) instead of open_writer(&db_path) -- both the crash-run tx_id recovery in run_one and every assertion in assert_journal_consistent. Empirically verified: `cargo test -p calm-cli --test txn_crash_injection` passes (txn_journal_survives_kill_at_every_reachable_transition, 5 real SIGKILL cycles per reachable transition) -- the durability guarantee this suite exists to prove (disk never changes without a corresponding tx_events row, replay_state never drifts from the cached state, a crashed tx is always found by recover_incomplete on next startup) holds across the state.db split, verified against a real OS kill, not just graceful-path unit tests. Full `cargo test -p calm-cli --all-targets`, `cargo clippy -p calm-cli --all-targets -- -D warnings`, and `cargo fmt --check -p calm-cli` all clean. This closes the crash-injection-harness item from docs/plans/2026-08-05-state-db-rewiring-execution-plan.md Phase 5. Remaining: KNOWN_LIMITATIONS.md/CHANGELOG.md updates. * docs: close out state.db rewiring in KNOWN_LIMITATIONS.md/CHANGELOG.md Deletes the "Durable state and the rebuildable index share one SQLite file at runtime" entry from KNOWN_LIMITATIONS.md now that every real calm-server/calm-cli call site reads/writes durable state (project_memory/edit_transactions/tx_events/maintenance_jobs/ audit_ledger) through state.db instead of the shared, rebuildable index.db -- per the file's own stated convention ("If you land a fix for one of these, delete the entry in the same PR rather than leaving it stale"). Updates the matching CHANGELOG.md [Unreleased] bullet from "storage- layer foundation... not yet wired into any real calm-server call site" to describe what's now actually wired. This closes docs/plans/2026-08-05-state-db-rewiring-execution-plan.md in full. Final verification across the whole workspace: calm-server 339/341 (2 pre-existing root-environment-only failures, documented in the Phase 5 commit), calm-cli all green including the real-SIGKILL crash-injection suite, calm-core 978/978 unaffected (never touched by this series). fmt/clippy clean throughout. --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 9773d41 commit 86f56ab

12 files changed

Lines changed: 447 additions & 87 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ git tags in [Releases](https://github.com/Eilodon/CALM/releases).
2727
- `calm connect --preset` now takes effect even when attaching to an already-live daemon, not just when this connection is the one that spawns it: a one-line handshake preamble ahead of the raw MCP byte stream lets each connection narrow its own effective tool ceiling (`CalmServer::narrow_connection_preset`), reusing the same `resolve_preset`/`current_visible_tool_names` machinery `set_toolset` already enforces -- a too-wide request is a no-op, never a privilege escalation, since the daemon's own `tool_router` (built once at spawn time) stays the hard ceiling
2828
- New `batch_status` tool: takes a caller-supplied list of `tx_id`s (the ones a set of `edit_lines`/`edit_symbol`/`format_files` calls already returned) and reports one aggregate view -- counts by state, which are missing, whether any failed -- instead of requiring a separate `edit_transaction_status` call per file for a multi-file change. Observability only: doesn't group transactions server-side or change what those write tools do (see `KNOWN_LIMITATIONS.md` "No multi-file change-set / transaction")
2929
- New `calm guard` CLI command: runs the exact `diff_impact` tool an MCP agent's own Stage-7 pre-commit gate uses against the staged diff (`git diff --cached`) and exits non-zero when `aggregate_risk` is at or above `--fail-on` (default `high`) -- a first Git/CI-native integration point for changes made outside any MCP session (a teammate's native editor, a bot PR), usable directly as a pre-commit hook or CI step
30-
- Storage-layer foundation for splitting durable state out of the rebuildable index (`KNOWN_LIMITATIONS.md` "Durable state and the rebuildable index share one SQLite file at runtime"): `db::schema::STATE_SCHEMA_SQL`/`init_state_db` define `project_memory`, `project_memory_refs`, `edit_transactions`, `tx_events`, `maintenance_jobs`, and `audit_ledger` as a schema separate from the rebuildable index's `SCHEMA_SQL`/`init_db`; `db::conn::open_state_writer` opens a `PRAGMA synchronous=FULL` connection for it; `db::schema::migrate_legacy_durable_tables` does a one-time, idempotent, copy-only migration of a pre-split `index.db`'s durable rows into a `state.db`. Not yet wired into any real `calm-server` call site -- see `KNOWN_LIMITATIONS.md` for what's left
30+
- Durable state (`project_memory`, `project_memory_refs`, `edit_transactions`, `tx_events`, `maintenance_jobs`, `audit_ledger`) now lives in a separate `state.db` (`PRAGMA synchronous=FULL`, `db::conn::open_state_writer`) instead of sharing the rebuildable index's `index.db` (`synchronous=NORMAL`) -- every real call site (`remember`/`recall`, `edit_transaction_status`/`batch_status`/`maintenance_status`/`retry_maintenance`/`repair_consistency`/`verify_change`, the shadow-tx paths inside `edit_lines`/`edit_symbol`/`format_files`, and the OS-level crash-injection harness) now reads and writes through it; `db::schema::migrate_legacy_durable_tables` copies any pre-split `index.db`'s durable rows into `state.db` once, idempotently, on first startup after upgrading. Closes `KNOWN_LIMITATIONS.md` "Durable state and the rebuildable index share one SQLite file at runtime"
3131

3232
## [0.5.0] - 2026-08-03
3333

KNOWN_LIMITATIONS.md

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -29,34 +29,6 @@ allowlist, resource limits) applied uniformly first — bolting each new
2929
language's runner directly onto today's bare `Command::new(...)` would
3030
just multiply the unsandboxed surface instead of closing it. Not started.
3131

32-
## Durable state and the rebuildable index share one SQLite file at runtime
33-
34-
`.calm/index.db` holds the symbol/call-graph index (rebuildable from
35-
source, `PRAGMA synchronous=NORMAL` is a deliberate tradeoff for it) *and*
36-
the edit-transaction journal, audit ledger, and project-memory notes
37-
(none of which are rebuildable). All of it currently shares that same
38-
`synchronous=NORMAL` posture and the same physical file. A hard
39-
power-loss can only lose the last few committed rows under WAL+NORMAL,
40-
never corrupt the file — an acceptable cost for a cache, less obviously
41-
so for a journal used as evidence.
42-
43-
The storage-layer foundation for a split now exists in `calm-core`:
44-
`db::schema::STATE_SCHEMA_SQL`/`init_state_db` define the durable tables
45-
(`project_memory`, `project_memory_refs`, `edit_transactions`, `tx_events`,
46-
`maintenance_jobs`, `audit_ledger`) as a schema separate from the
47-
rebuildable `SCHEMA_SQL`/`init_db`; `db::conn::open_state_writer` opens a
48-
connection at `PRAGMA synchronous=FULL`; and
49-
`db::schema::migrate_legacy_durable_tables` does a one-time, idempotent,
50-
copy-only migration of any pre-split `index.db`'s durable rows into a
51-
`state.db`. All of this is unit-tested but **not yet wired up** — every
52-
real call site in `calm-server` (`edit.rs`, `txn.rs`, `memory.rs`,
53-
`common.rs`, `lib.rs`, and others) still opens and writes through the one
54-
shared `index.db`/`open_writer` connection. Rewiring those call sites to
55-
actually use `state.db` for durable writes is a separate follow-up, scoped
56-
by `open_writer`'s own blast radius (~320 transitively-affected files per
57-
`diff_impact`) rather than attempted alongside the schema/migration
58-
groundwork.
59-
6032
## No multi-file change-set / transaction
6133

6234
Every `edit_lines`/`edit_symbol`/`format_files` call gets its own,

crates/calm-cli/src/bin/txn_crash_harness.rs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ use std::path::PathBuf;
2626

2727
fn main() {
2828
let mut db_path: Option<PathBuf> = None;
29+
let mut state_db_path: Option<PathBuf> = None;
2930
let mut file_path: Option<PathBuf> = None;
3031
let mut crash_after: Option<String> = None;
3132
let mut new_content = String::from("new content\n");
@@ -34,6 +35,7 @@ fn main() {
3435
while let Some(arg) = args.next() {
3536
match arg.as_str() {
3637
"--db" => db_path = args.next().map(PathBuf::from),
38+
"--state-db" => state_db_path = args.next().map(PathBuf::from),
3739
"--file" => file_path = args.next().map(PathBuf::from),
3840
"--crash-after" => crash_after = args.next(),
3941
"--new-content" => new_content = args.next().unwrap_or(new_content),
@@ -44,6 +46,7 @@ fn main() {
4446
}
4547
}
4648
let db_path = db_path.expect("--db required");
49+
let state_db_path = state_db_path.expect("--state-db required");
4750
let file_path = file_path.expect("--file required");
4851

4952
// SIGKILL, not `std::process::exit` -- `exit` runs libc atexit handlers
@@ -62,11 +65,18 @@ fn main() {
6265

6366
let original = std::fs::read_to_string(&file_path).unwrap_or_default();
6467

68+
// index.db: schema init only in this harness (it deliberately skips a
69+
// real reindex -- see module doc), kept for parity with a real
70+
// process's own startup. All durable txn::begin/advance calls below go
71+
// through state_conn/state.db instead (docs/plans/2026-08-05-state-db-
72+
// rewiring-execution-plan.md).
6573
let conn = calm_core::db::conn::open_writer(&db_path).expect("open db");
6674
calm_core::db::schema::init_db(&conn).expect("init db");
75+
let state_conn = calm_core::db::conn::open_state_writer(&state_db_path).expect("open state db");
76+
calm_core::db::schema::init_state_db(&state_conn).expect("init state db");
6777

6878
let tx = calm_core::txn::begin(
69-
&conn,
79+
&state_conn,
7080
"crash-harness-project",
7181
file_path.file_name().unwrap().to_str().unwrap(),
7282
&calm_core::digest::evidence_digest(original.as_bytes()),
@@ -77,7 +87,7 @@ fn main() {
7787

7888
calm_core::edit::atomic_write(&file_path, &new_content).expect("atomic_write");
7989
calm_core::txn::advance(
80-
&conn,
90+
&state_conn,
8191
&tx.tx_id,
8292
calm_core::txn::TxState::FileCommitted,
8393
"system",
@@ -87,7 +97,7 @@ fn main() {
8797
crash_here("file_committed");
8898

8999
calm_core::txn::advance(
90-
&conn,
100+
&state_conn,
91101
&tx.tx_id,
92102
calm_core::txn::TxState::IndexCommitted,
93103
"system",
@@ -97,7 +107,7 @@ fn main() {
97107
crash_here("index_committed");
98108

99109
calm_core::txn::advance(
100-
&conn,
110+
&state_conn,
101111
&tx.tx_id,
102112
calm_core::txn::TxState::Done,
103113
"system",

crates/calm-cli/tests/txn_crash_injection.rs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,19 +63,29 @@ fn db_path_for(dir: &Path, run_key: &str) -> std::path::PathBuf {
6363
dir.join(format!("index-{run_key}.db"))
6464
}
6565

66+
/// state.db counterpart of `db_path_for` -- edit_transactions/tx_events now
67+
/// live there (docs/plans/2026-08-05-state-db-rewiring-execution-plan.md),
68+
/// not in the rebuildable index.db `db_path_for` points at.
69+
fn state_db_path_for(dir: &Path, run_key: &str) -> std::path::PathBuf {
70+
dir.join(format!("state-{run_key}.db"))
71+
}
72+
6673
/// Spawns one `txn_crash_harness` subprocess with `--crash-after step` (or
6774
/// no crash at all if `step` is `None`), waits for it to die, and returns
6875
/// what's observable afterward. Panics if a crash was requested but the
6976
/// process didn't actually die by SIGKILL -- that would mean this test
7077
/// stopped testing what it claims to.
7178
fn run_one(dir: &Path, run_key: &str, step: Option<&str>) -> CrashOutcome {
7279
let db_path = db_path_for(dir, run_key);
80+
let state_db_path = state_db_path_for(dir, run_key);
7381
let file_path = dir.join(format!("a-{run_key}.txt"));
7482
std::fs::write(&file_path, ORIGINAL_CONTENT).unwrap();
7583

7684
let mut cmd = Command::new(env!("CARGO_BIN_EXE_txn_crash_harness"));
7785
cmd.arg("--db")
7886
.arg(&db_path)
87+
.arg("--state-db")
88+
.arg(&state_db_path)
7989
.arg("--file")
8090
.arg(&file_path)
8191
.arg("--new-content")
@@ -108,7 +118,7 @@ fn run_one(dir: &Path, run_key: &str, step: Option<&str>) -> CrashOutcome {
108118
// crash run, recover it from the DB directly the way a real
109119
// recovering process would (there's exactly one transaction ever
110120
// created against this DB, now that db_path is unique per run_key).
111-
let conn = calm_core::db::conn::open_writer(&db_path).ok();
121+
let conn = calm_core::db::conn::open_state_writer(&state_db_path).ok();
112122
conn.and_then(|c| {
113123
c.query_row("SELECT tx_id FROM edit_transactions LIMIT 1", [], |r| {
114124
r.get::<_, String>(0)
@@ -154,8 +164,9 @@ fn assert_journal_consistent(
154164
synchronous, not partial"
155165
);
156166

157-
let db_path = db_path_for(dir, run_key);
158-
let conn = calm_core::db::conn::open_writer(&db_path).expect("reopen db after crash");
167+
let state_db_path = state_db_path_for(dir, run_key);
168+
let conn = calm_core::db::conn::open_state_writer(&state_db_path)
169+
.expect("reopen state db after crash");
159170

160171
let cached = calm_core::txn::get(&conn, tx_id)
161172
.expect("txn::get")

crates/calm-server/src/lib.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -723,6 +723,17 @@ pub fn default_db_path(project_root: &std::path::Path) -> PathBuf {
723723
project_root.join(".calm").join("index.db")
724724
}
725725

726+
/// Sibling of `default_db_path` for the durable-state split
727+
/// (`KNOWN_LIMITATIONS.md` "Durable state and the rebuildable index share
728+
/// one SQLite file at runtime", `docs/plans/2026-08-05-state-db-rewiring-
729+
/// execution-plan.md`): `project_memory`/`edit_transactions`/`tx_events`/
730+
/// `maintenance_jobs`/`audit_ledger` live here (`db::conn::open_state_writer`,
731+
/// `PRAGMA synchronous=FULL`), separate from the rebuildable index/call-graph
732+
/// data in `default_db_path`'s `index.db` (`synchronous=NORMAL`).
733+
pub fn default_state_db_path(project_root: &std::path::Path) -> PathBuf {
734+
project_root.join(".calm").join("state.db")
735+
}
736+
726737
pub fn doctor(project_root: &std::path::Path, fix: bool) -> Result<()> {
727738
use calm_core::db::schema::init_db;
728739

0 commit comments

Comments
 (0)