Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions ARCHITECTURE_PLAN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Reliability hardening plan
Baseline: upstream 4c9df39. Work only in this branch; do not install, modify Hermes configuration or touch user memory.
Keep Rust core + local CLI/MCP. No service, cloud, Docker or new AI provider is required for this native library repair.
Priority 1: propagate storage errors through CLI nonzero exit and MCP isError; never synthesize empty state on corruption.
Priority 2: OS advisory lock held throughout mutation, stable lock inode, no elapsed-age stealing; internal unlocked helpers. Rust std file locking preferred if toolchain supports it; declare MSRV.
Priority 3: durable atomic save, parent sync, temporary-file cleanup, bounded decoding/validation, explicit backup/check/restore with overwrite confirmation. Preserve legacy readable snapshots and document integrity limits.
Priority 4: runner stores observations rather than causal facts, correlates only identical command/project, avoids raw log/argument retention and does not auto-ingest fixes.
Priority 5: machine-readable memory CLI and verified native MCP, strict inputs and complete text output.
Priority 6: honest limitations and reproducible isolated load/recall measurements, no universal recall or memory-size claims.
Acceptance: real files and subprocesses, failure propagation, restart, lock contention/long holder/process exit, multi-process RMW, recover/invalid-backup, legacy roundtrip, Unicode/full text. No offensive reproduction or live-data tests. Independent defensive review, fix/re-review, measured stress evidence, branch PR (not direct main push).
92 changes: 92 additions & 0 deletions P1_REGRESSION.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
$ cargo test -p continuum-cli --test errors_api --locked --offline init_preserves_existing_memory_and_configuration -- --exact
Compiling continuum-core v0.1.0 (/Users/mymac/continuum-hardening/crates/continuum-core)
Compiling continuum-cli v0.1.0 (/Users/mymac/continuum-hardening/crates/continuum-cli)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.42s
Running tests/errors_api.rs (target/debug/deps/errors_api-06085ea841f4ed0b)

running 1 test
test init_preserves_existing_memory_and_configuration ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 14 filtered out; finished in 0.54s


exit=0
$ cargo test -p continuum-cli --test errors_api --locked --offline json_options_preserve_literal_text_and_paths -- --exact
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.00s
Running tests/errors_api.rs (target/debug/deps/errors_api-06085ea841f4ed0b)

running 1 test
test json_options_preserve_literal_text_and_paths ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 14 filtered out; finished in 0.58s


exit=0
$ cargo test -p continuum-cli --test errors_api --locked --offline unicode_surrogates_follow_json_semantics -- --exact
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.00s
Running tests/errors_api.rs (target/debug/deps/errors_api-06085ea841f4ed0b)

running 1 test
test unicode_surrogates_follow_json_semantics ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 14 filtered out; finished in 0.86s


exit=0
$ cargo test -p continuum-cli --test errors_api --locked --offline storage_errors_are_nonzero_and_never_claim_success -- --exact
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.00s
Running tests/errors_api.rs (target/debug/deps/errors_api-06085ea841f4ed0b)

running 1 test
test storage_errors_are_nonzero_and_never_claim_success ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 14 filtered out; finished in 0.46s


exit=0
$ cargo test -p continuum-cli --test errors_api --locked --offline mcp_storage_failures_are_tool_errors_without_empty_fallback -- --exact
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.00s
Running tests/errors_api.rs (target/debug/deps/errors_api-06085ea841f4ed0b)

running 1 test
test mcp_storage_failures_are_tool_errors_without_empty_fallback ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 14 filtered out; finished in 0.43s


exit=0
$ cargo test -p continuum-cli --test errors_api --locked --offline recovery_check_backup_and_confirmed_restore -- --exact
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.00s
Running tests/errors_api.rs (target/debug/deps/errors_api-06085ea841f4ed0b)

running 1 test
test recovery_check_backup_and_confirmed_restore ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 14 filtered out; finished in 0.55s


exit=0
$ cargo test -p continuum-core --test storage --locked --offline
Compiling continuum-core v0.1.0 (/Users/mymac/continuum-hardening/crates/continuum-core)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.13s
Running tests/storage.rs (target/debug/deps/storage-58850af08a49fd5f)

running 13 tests
test child_worker ... ok
test incomplete_snapshot_is_not_reinitialized ... ok
test save_creates_parent_and_failed_mutation_preserves_snapshot ... ok
test save_rejects_inconsistent_state_without_overwrite ... ok
test failed_atomic_publication_cleans_own_temp ... ok
test legacy_empty_snapshot_roundtrips_byte_exact ... ok
test invalid_normal_state_is_rejected_before_save ... ok
test ordinary_truncation_and_trailing_data_fail_closed ... ok
test stable_lock_times_out_and_reacquires ... ok
test backup_check_restore_are_validated_and_no_clobber ... ok
test process_rmw_has_no_lost_updates ... ok
test thread_rmw_has_no_lost_updates ... ok
test long_holder_is_not_stolen_and_killed_holder_releases ... ok

test result: ok. 13 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 6.18s


exit=0
17 changes: 17 additions & 0 deletions P1_REGRESSION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# P1 regression checkpoint

No further code edits or broad lint/workspace runs at this checkpoint.

Exact independent-review findings:
- [x] P1-1: init uses the transactional writer lock and preserves existing memory. Test: init_preserves_existing_memory_and_configuration. Repeated init is tested; simultaneous init specifically is not separately tested.
- [x] P1-2: output-option parsing preserves literal text/path arguments. Test: json_options_preserve_literal_text_and_paths.
- [x] P1-3: JSON decodes valid UTF-16 surrogate pairs. Test: unicode_surrogates_follow_json_semantics.

Dependent reliability regressions:
- [x] CLI/MCP storage failures never claim success: storage_errors_are_nonzero_and_never_claim_success, mcp_storage_failures_are_tool_errors_without_empty_fallback.
- [x] Recovery CLI: recovery_check_backup_and_confirmed_restore.
- [x] Storage integration suite: stable OS lock, long holder/process exit, process/thread RMW, invalid-state preservation, atomic publication and backup/restore.

Raw execution evidence: P1_REGRESSION.log. Each invocation must exit zero before committing.
Scope: CLI memory/MCP/error handling, JSON compatibility, core lock/persistence/recovery, minimum Rust version, and related tests. Runner changes are excluded from this checkpoint commit and remain uncommitted. No installation, Hermes configuration changes, or live-memory access.
This checkpoint is not a production-readiness certification or a performance benchmark. CTNM0001 has no checksum; old and new lock protocols must not run concurrently.
1 change: 1 addition & 0 deletions crates/continuum-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
name = "continuum-cli"
version = "0.1.0"
edition = "2024"
rust-version = "1.89"

[dependencies]
continuum-core = { path = "../continuum-core" }
Expand Down
96 changes: 77 additions & 19 deletions crates/continuum-cli/src/json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,20 @@ impl JsonValue {
pub fn to_json_string(&self) -> String {
match self {
JsonValue::Null => "null".to_string(),
JsonValue::Bool(b) => if *b { "true".to_string() } else { "false".to_string() },
JsonValue::Bool(b) => {
if *b {
"true".to_string()
} else {
"false".to_string()
}
}
JsonValue::Number(n) => {
if n.fract() == 0.0 && !n.is_infinite() && !n.is_nan() && *n >= (i64::MIN as f64) && *n <= (i64::MAX as f64) {
if n.fract() == 0.0
&& !n.is_infinite()
&& !n.is_nan()
&& *n >= (i64::MIN as f64)
&& *n <= (i64::MAX as f64)
{
format!("{}", *n as i64)
} else {
format!("{}", n)
Expand Down Expand Up @@ -186,7 +197,10 @@ impl<'a> JsonParser<'a> {
return Err(format!(
"Unexpected trailing characters at pos {}: '{}'",
parser.pos,
parser.chars[parser.pos..].iter().take(20).collect::<String>()
parser.chars[parser.pos..]
.iter()
.take(20)
.collect::<String>()
));
}
Ok(val)
Expand Down Expand Up @@ -216,15 +230,20 @@ impl<'a> JsonParser<'a> {

fn parse_value(&mut self) -> Result<JsonValue, String> {
self.skip_whitespace();
let c = self.peek().ok_or_else(|| "Unexpected EOF while parsing JSON value".to_string())?;
let c = self
.peek()
.ok_or_else(|| "Unexpected EOF while parsing JSON value".to_string())?;
match c {
'n' => self.parse_null(),
't' | 'f' => self.parse_bool(),
'"' => self.parse_string().map(JsonValue::String),
'[' => self.parse_array(),
'{' => self.parse_object(),
'-' | '0'..='9' => self.parse_number(),
other => Err(format!("Unexpected character '{}' at pos {}", other, self.pos)),
other => Err(format!(
"Unexpected character '{}' at pos {}",
other, self.pos
)),
}
}

Expand Down Expand Up @@ -261,6 +280,18 @@ impl<'a> JsonParser<'a> {
}
}

fn parse_hex_quad(&mut self) -> Result<u32, String> {
let mut code = 0;
for _ in 0..4 {
let digit = self
.next_char()
.and_then(|c| c.to_digit(16))
.ok_or_else(|| "Expected four hexadecimal digits in Unicode escape".to_string())?;
code = (code << 4) | digit;
}
Ok(code)
}

fn parse_string(&mut self) -> Result<String, String> {
if self.next_char() != Some('"') {
return Err(format!("Expected '\"' at pos {}", self.pos));
Expand All @@ -270,7 +301,9 @@ impl<'a> JsonParser<'a> {
match c {
'"' => return Ok(s),
'\\' => {
let esc = self.next_char().ok_or_else(|| "Unexpected EOF after escape".to_string())?;
let esc = self
.next_char()
.ok_or_else(|| "Unexpected EOF after escape".to_string())?;
match esc {
'"' => s.push('"'),
'\\' => s.push('\\'),
Expand All @@ -281,12 +314,18 @@ impl<'a> JsonParser<'a> {
'r' => s.push('\r'),
't' => s.push('\t'),
'u' => {
let mut hex = String::with_capacity(4);
for _ in 0..4 {
hex.push(self.next_char().ok_or_else(|| "Unexpected EOF in \\u escape".to_string())?);
let mut code = self.parse_hex_quad()?;
if (0xd800..=0xdbff).contains(&code) {
if self.next_char() != Some('\\') || self.next_char() != Some('u') {
return Err("High surrogate requires a low surrogate escape"
.to_string());
}
let low = self.parse_hex_quad()?;
if !(0xdc00..=0xdfff).contains(&low) {
return Err("Invalid low surrogate".to_string());
}
code = 0x10000 + ((code - 0xd800) << 10) + (low - 0xdc00);
}
let code = u32::from_str_radix(&hex, 16)
.map_err(|e| format!("Invalid hex escape \\u{}: {}", hex, e))?;
let decoded = char::from_u32(code)
.ok_or_else(|| format!("Invalid unicode code point: {:x}", code))?;
s.push(decoded);
Expand Down Expand Up @@ -325,8 +364,8 @@ impl<'a> JsonParser<'a> {
}
}
}
if let Some(c) = self.peek() {
if c == 'e' || c == 'E' {
if let Some(c) = self.peek()
&& (c == 'e' || c == 'E') {
self.pos += 1;
if self.peek() == Some('+') || self.peek() == Some('-') {
self.pos += 1;
Expand All @@ -339,9 +378,10 @@ impl<'a> JsonParser<'a> {
}
}
}
}
let raw: String = self.chars[start..self.pos].iter().collect();
let num: f64 = raw.parse().map_err(|e| format!("Failed to parse number '{}': {}", raw, e))?;
let num: f64 = raw
.parse()
.map_err(|e| format!("Failed to parse number '{}': {}", raw, e))?;
Ok(JsonValue::Number(num))
}

Expand Down Expand Up @@ -435,7 +475,10 @@ mod tests {
assert_eq!(parse_json("false").unwrap(), JsonValue::Bool(false));
assert_eq!(parse_json("42").unwrap(), JsonValue::Number(42.0));
assert_eq!(parse_json("-17.5").unwrap(), JsonValue::Number(-17.5));
assert_eq!(parse_json("\"hello world\"").unwrap(), JsonValue::String("hello world".to_string()));
assert_eq!(
parse_json("\"hello world\"").unwrap(),
JsonValue::String("hello world".to_string())
);
}

#[test]
Expand Down Expand Up @@ -463,9 +506,24 @@ mod tests {
assert_eq!(v.get("jsonrpc").unwrap().as_str().unwrap(), "2.0");
assert_eq!(v.get("id").unwrap().to_raw_id_string(), "\"msg_01J8K9\"");
assert_eq!(v.get("method").unwrap().as_str().unwrap(), "tools/call");
assert_eq!(v.get_path(&["params", "name"]).unwrap().as_str().unwrap(), "continuum_recall");
assert_eq!(v.get_path(&["params", "arguments", "query"]).unwrap().as_str().unwrap(), "database pool cap");
assert_eq!(v.get_path(&["params", "arguments", "top_k"]).unwrap().as_u64().unwrap(), 5);
assert_eq!(
v.get_path(&["params", "name"]).unwrap().as_str().unwrap(),
"continuum_recall"
);
assert_eq!(
v.get_path(&["params", "arguments", "query"])
.unwrap()
.as_str()
.unwrap(),
"database pool cap"
);
assert_eq!(
v.get_path(&["params", "arguments", "top_k"])
.unwrap()
.as_u64()
.unwrap(),
5
);
}

#[test]
Expand Down
Loading