diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index f41f7f2..2fa26d9 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -9,7 +9,7 @@ "name": "lore", "source": "./", "description": "Shared memory for Claude Code, Codex, and DOXA: capped curated facts, evidence-backed beliefs, FTS5 session search, staged review, learned skills, and signed sync.", - "version": "0.62.22" + "version": "0.62.23" } ] } diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index a3f06d9..034a889 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "lore", "description": "Memory for Claude Code that reasons about you and improves itself: Honcho-pattern belief store with deriver, dreamer and dialectic (/lore:ask), automatic skillification with a closed judge/update/retire loop, on a Hermes-pattern core \u2014 hard-capped curated memory, FTS5 session search, staged review.", - "version": "0.62.22", + "version": "0.62.23", "author": { "name": "Fabian Wilde", "email": "docwilde@proton.me" diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bdebde..69ff27a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 0.62.23 — 2026-10-19 + +**Background review history** + +- Fix **`worker::provider`** and **`run_claude`** saving background reviews as resumable user sessions; disable session persistence for Deriver, Dreamer and their shared review callers. +- Retain that setting during OAuth retries. Existing session files and Desktop imports require separate archival. + ## 0.62.22 — 2026-10-13 **Codex memory writes** diff --git a/Cargo.lock b/Cargo.lock index 75c0ca7..22207ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -612,7 +612,7 @@ checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" [[package]] name = "lore-core" -version = "0.62.22" +version = "0.62.23" dependencies = [ "fancy-regex", "hmac", diff --git a/lore_core/deriver.py b/lore_core/deriver.py index fbc99e5..fb53e77 100644 --- a/lore_core/deriver.py +++ b/lore_core/deriver.py @@ -142,7 +142,10 @@ def call(bare: bool) -> subprocess.CompletedProcess[str]: # Prompt via STDIN, never argv: a dreamer prompt over a large # belief store exceeds ARG_MAX (live E2BIG at 515 beliefs, # 2026-08-22). `claude -p` with no inline prompt reads stdin. - cmd += ["-p", "--model", model, "--allowedTools", ""] + # Internal reviews must stay out of provider/Desktop session history, + # including the OAuth retry without --bare. + cmd += ["-p", "--no-session-persistence", "--model", model, + "--allowedTools", ""] return subprocess.run( cmd, capture_output=True, text=True, timeout=600, input=prompt, diff --git a/plugin.json b/plugin.json index 94001d8..db2c606 100644 --- a/plugin.json +++ b/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", "name": "lore", - "version": "0.62.22", + "version": "0.62.23", "description": "Unified user, repo, and session memory for Claude Code, Codex, and DOXA", "author": { "name": "Dr. Fabian Wilde", diff --git a/rust/lore-core/Cargo.toml b/rust/lore-core/Cargo.toml index 0dbb885..010765c 100644 --- a/rust/lore-core/Cargo.toml +++ b/rust/lore-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lore-core" -version = "0.62.22" +version = "0.62.23" edition = "2021" license = "AGPL-3.0-only" description = "Native LORE memory, beliefs, review and session index" diff --git a/rust/lore-core/src/worker.rs b/rust/lore-core/src/worker.rs index 4a8bdf5..954aa85 100644 --- a/rust/lore-core/src/worker.rs +++ b/rust/lore-core/src/worker.rs @@ -147,7 +147,9 @@ fn provider( command.arg("--bare"); } command - .args(["-p", "--model", model, "--allowedTools", ""]) + // Background reviews must never become resumable user sessions, + // including when subscription auth requires the non-bare retry. + .args(["-p", "--no-session-persistence", "--model", model, "--allowedTools", ""]) .env("LORE_SKIP", "1") .env("LORE_DISABLE_REVIEW", "1") .stdin(Stdio::piped()) diff --git a/rust/lore-core/tests/worker.rs b/rust/lore-core/tests/worker.rs index 3edb9a8..3134793 100644 --- a/rust/lore-core/tests/worker.rs +++ b/rust/lore-core/tests/worker.rs @@ -29,7 +29,8 @@ const PROVIDER: &str = r#"#!/usr/bin/python3 import json, os, re, subprocess, sys, time from pathlib import Path root = Path(os.environ['OWNED_FIXTURE']) -assert sys.argv[1:] == ['--bare', '-p', '--model', 'owned-model', '--allowedTools', ''] +bare = '--bare' in sys.argv[1:] +assert sys.argv[1:] == (['--bare'] if bare else []) + ['-p', '--no-session-persistence', '--model', 'owned-model', '--allowedTools', ''] assert os.environ['LORE_SKIP'] == '1' and os.environ['LORE_DISABLE_REVIEW'] == '1' child = subprocess.Popen(['/bin/sleep', '30'], stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) (root / 'ready').write_text(json.dumps({'provider':os.getpid(), 'descendant':child.pid})) @@ -43,6 +44,9 @@ role = 'dreamer' if prompt.startswith('You are the dreamer') else 'deriver' (root / ('ready-' + role)).write_text(json.dumps({'provider':os.getpid(), 'descendant':child.pid})) with (root / 'calls').open('a') as output: output.write(json.dumps({'role':role,'argv':sys.argv[1:]}) + '\n') +if mode == 'dream_auth_retry' and bare: + sys.stderr.write('Not logged in') + sys.exit(1) if mode.startswith('dream_'): (root / ('prompt-' + role)).write_text(prompt) if role == 'dreamer': @@ -508,6 +512,7 @@ fn automatic_native_dream_merges_verified_pair_and_stages_promotions_for_review( json!([ "--bare", "-p", + "--no-session-persistence", "--model", "owned-model", "--allowedTools", @@ -571,6 +576,29 @@ fn automatic_native_dream_merges_verified_pair_and_stages_promotions_for_review( } } #[test] +fn native_review_oauth_retry_keeps_both_roles_out_of_session_history() { + let fixture = Fixture::new("codex", false); + fixture.seed_pair(); + let mut worker = fixture.spawn("codex", "dream_auth_retry"); + worker.observe_role(&fixture.home, "deriver"); + worker.observe_role(&fixture.home, "dreamer"); + assert!(worker.finish(Duration::from_secs(5)).unwrap().success()); + assert!(worker.stderr().is_empty() && worker.stdout().is_empty()); + let calls = fixture.calls(); + assert_eq!(calls.len(), 4); + assert_eq!( + calls.iter().map(|call| call["role"].as_str().unwrap()).collect::>(), + ["deriver", "deriver", "dreamer", "dreamer"] + ); + for (index, call) in calls.iter().enumerate() { + let args = call["argv"].as_array().unwrap(); + assert_eq!(args.contains(&json!("--bare")), index % 2 == 0); + assert!(args.contains(&json!("--no-session-persistence"))); + } + assert_eq!(fixture.pending().len(), 2); + fixture.assert_not_curated(); +} +#[test] fn failed_automatic_dream_preserves_landed_review_and_reports_no_safe_retry() { let fixture = Fixture::new("claude", false); let pair = fixture.seed_pair(); diff --git a/tests/test_worker_session_history.py b/tests/test_worker_session_history.py new file mode 100644 index 0000000..324a89d --- /dev/null +++ b/tests/test_worker_session_history.py @@ -0,0 +1,59 @@ +# SPDX-License-Identifier: AGPL-3.0-only +"""Background reviews must not create resumable provider sessions.""" +import contextlib +import io +import json +import os +from pathlib import Path +import sys +import tempfile +import unittest +from unittest import mock + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +from lore_core.deriver import run_claude + + +PROVIDER = """#!/usr/bin/python3 +import json, os, sys +from pathlib import Path +root = Path(os.environ['OWNED_FIXTURE']) +prompt = sys.stdin.read() +with (root / 'calls').open('a') as out: + out.write(json.dumps(sys.argv[1:]) + '\\n') +if '--no-session-persistence' not in sys.argv: + (root / 'user-session.jsonl').write_text(prompt) +if os.environ['OWNED_AUTH_RETRY'] == '1' and '--bare' in sys.argv: + sys.stderr.write('Not logged in') + sys.exit(1) +print(json.dumps({'reviewed': prompt, 'reentry_disabled': os.environ['LORE_SKIP']})) +""" + + +class WorkerSessionHistory(unittest.TestCase): + def test_deriver_and_dreamer_never_persist_even_after_oauth_retry(self): + for role in ('deriver', 'dreamer'): + for retry in (False, True): + with self.subTest(role=role, retry=retry), tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + provider = root / 'provider' + provider.write_text(PROVIDER) + provider.chmod(0o700) + with mock.patch.dict(os.environ, { + 'OWNED_FIXTURE': tmp, 'OWNED_AUTH_RETRY': '1' if retry else '0', + }), contextlib.redirect_stdout(io.StringIO()): + result = run_claude(str(provider), 'owned review prompt', 'owned-model', role) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(json.loads(result.stdout), { + 'reviewed': 'owned review prompt', 'reentry_disabled': '1', + }) + self.assertFalse((root / 'user-session.jsonl').exists()) + calls = [json.loads(line) for line in (root / 'calls').read_text().splitlines()] + self.assertEqual(len(calls), 2 if retry else 1) + self.assertIn('--bare', calls[0]) + if retry: + self.assertNotIn('--bare', calls[1]) + + +if __name__ == '__main__': + unittest.main()