From c664cefe29c3bf7c890926187d19ef05d76087fc Mon Sep 17 00:00:00 2001 From: "Dr. Fabian Wilde" Date: Mon, 19 Oct 2026 11:18:42 +0200 Subject: [PATCH] fix(review): keep background workers out of session history Release LORE 0.62.23. Disable Claude session persistence in the native and Python review launchers, including OAuth fallback calls, so internal reviews are not offered as user conversations or imported into Desktop history. Cover Deriver and Dreamer calls and retain reviewed memory and process cleanup behavior. Existing saved sessions require separate archival. --- .claude-plugin/marketplace.json | 2 +- .claude-plugin/plugin.json | 2 +- CHANGELOG.md | 7 ++++ Cargo.lock | 2 +- lore_core/deriver.py | 5 ++- plugin.json | 2 +- rust/lore-core/Cargo.toml | 2 +- rust/lore-core/src/worker.rs | 4 +- rust/lore-core/tests/worker.rs | 30 +++++++++++++- tests/test_worker_session_history.py | 59 ++++++++++++++++++++++++++++ 10 files changed, 107 insertions(+), 8 deletions(-) create mode 100644 tests/test_worker_session_history.py 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()