fix: remove automatic session JSON snapshots

This commit is contained in:
Teknium
2026-09-07 01:56:22 -07:00
parent bbbccd3935
commit 7a5fc1b2a9
15 changed files with 99 additions and 279 deletions

View File

@@ -210,7 +210,7 @@ hermes-agent/
| `~/.hermes/skills/` | Todas las habilidades activas (incluidas + instaladas desde hub + creadas por el agente) |
| `~/.hermes/memories/` | Memoria persistente (MEMORY.md, USER.md) |
| `~/.hermes/state.db` | Base de datos de sesiones SQLite |
| `~/.hermes/sessions/` | Índice de enrutamiento del gateway (`sessions.json`), migas de pan de solicitudes, transcripciones `*.jsonl` del gateway y (opcionalmente) snapshots JSON por sesión cuando `sessions.write_json_snapshots: true` está configurado. Los snapshots por sesión están desactivados por defecto; state.db es canónica. |
| `~/.hermes/sessions/` | Índice de enrutamiento del gateway (`sessions.json`), migas de pan de solicitudes, transcripciones `*.jsonl` del gateway y exportaciones explícitas con `/save`. Ya no se escriben snapshots JSON automáticos; los archivos existentes se conservan y state.db es canónica. |
| `~/.hermes/cron/` | Datos de trabajos programados |
| `~/.hermes/whatsapp/session/` | Credenciales del puente WhatsApp |

View File

@@ -300,7 +300,7 @@ hermes-agent/
| `~/.hermes/skills/` | All active skills (bundled + hub-installed + agent-created) |
| `~/.hermes/memories/` | Persistent memory (MEMORY.md, USER.md) |
| `~/.hermes/state.db` | SQLite session database |
| `~/.hermes/sessions/` | Gateway routing index (`sessions.json`), request-dump breadcrumbs, gateway `*.jsonl` transcripts, and (optionally) per-session JSON snapshots when `sessions.write_json_snapshots: true` is set. The per-session snapshots are off by default; state.db is canonical. |
| `~/.hermes/sessions/` | Gateway routing index (`sessions.json`), request-dump breadcrumbs, gateway `*.jsonl` transcripts, and explicit `/save` exports. Automatic per-session JSON snapshots are no longer written; state.db is canonical. |
| `~/.hermes/cron/` | Scheduled job data |
| `~/.hermes/whatsapp/session/` | WhatsApp bridge credentials |
@@ -329,7 +329,7 @@ User message → AIAgent._run_agent_loop()
- **Self-registering tools**: Each tool file calls `registry.register()` at import time. `model_tools.py` triggers discovery by importing all tool modules.
- **Toolset grouping**: Tools are grouped into toolsets (`web`, `terminal`, `file`, `browser`, etc.) that can be enabled/disabled per platform.
- **Session persistence**: All conversations are stored in SQLite (`hermes_state.py`) with full-text search and unique session titles. Per-session JSON snapshots in `~/.hermes/sessions/` were superseded by the SQLite store and are off by default; opt back in with `sessions.write_json_snapshots: true` if you have external tooling that consumes the JSON files directly.
- **Session persistence**: All conversations are stored in SQLite (`hermes_state.py`) with full-text search and unique session titles. Automatic per-session JSON snapshots have been removed. Existing files are left untouched; use `/save json` or `hermes sessions export` for an explicit export.
- **Ephemeral injection**: System prompts and prefill messages are injected at API call time, never persisted to the database or logs.
- **Provider abstraction**: The agent works with any OpenAI-compatible API. Provider resolution happens at init time (Nous Portal OAuth, OpenRouter API key, or custom endpoint).
- **Provider routing**: When using OpenRouter, `provider_routing` in config.yaml controls provider selection (sort by throughput/latency/price, allow/ignore specific providers, data retention policies). These are injected as `extra_body.provider` in API requests.

View File

@@ -1131,13 +1131,6 @@ def _init_session_state(agent, session_id, session_db, parent_session_id, reason
# ~/.hermes/sessions/ — kept unconditionally for request_dump_*.json debug breadcrumbs.
agent.logs_dir = get_hermes_home() / "sessions"
agent.logs_dir.mkdir(parents=True, exist_ok=True)
# Per-session JSON snapshot is opt-in (sessions.write_json_snapshots); state.db is canonical.
agent._session_json_enabled = False
with suppress(Exception):
from hermes_cli.config import load_config_readonly as _load_sess_cfg
_sess_cfg = (_load_sess_cfg().get("sessions") or {})
agent._session_json_enabled = bool(_sess_cfg.get("write_json_snapshots", False))
_set_defaults(agent, _SESSION_STATE)
# Filesystem checkpoint manager (transparent — not a tool)

View File

@@ -851,7 +851,7 @@ def build_cache_parity_fork(
# finalize the parent's still-active session row. suppress_status_output: fork status/warning
# emits go via _print_fn/status_callback, which bypass the stdout redirect.
review_agent._skip_mcp_refresh = review_agent._persist_disabled = review_agent.suppress_status_output = True
review_agent._session_json_enabled = review_agent._end_session_on_close = False
review_agent._end_session_on_close = False
review_agent._session_db = None
review_agent.session_id = agent.session_id
# Same model only: share the warm cached system prompt (~26% cost cut; a rebuilt prompt misses

View File

@@ -1,12 +1,12 @@
"""Durable transcript persistence for ``AIAgent`` (mixin; MRO-resolved from ``run_agent``): SQLite flush
with intrinsic ``_DB_PERSISTED_MARKER`` dedup, ephemeral-scaffolding filtering, optional JSON session log,
with intrinsic ``_DB_PERSISTED_MARKER`` dedup, ephemeral-scaffolding filtering, explicit
trajectory export."""
import hashlib
import json
import logging
import re
from contextlib import nullcontext
from datetime import datetime
from typing import Any, Dict, List, Optional
from agent.context_compressor import (
@@ -17,11 +17,11 @@ from agent.context_compressor import (
)
from agent.lazy_forward import forward as _forward, forward_static as _forward_static
from agent.memory_manager import sanitize_context
from agent.redact import redact_sensitive_text
from agent.tool_dispatch_helpers import _is_multimodal_tool_result, _multimodal_text_summary
from agent.trajectory import convert_scratchpad_to_think, save_trajectory as _save_trajectory_to_file
from agent.trajectory import save_trajectory as _save_trajectory_to_file
from agent.transcript_repair import sync_flushed_message_markers
from utils import atomic_json_write
logger = logging.getLogger("run_agent") # origin module's name: log records / caplog filters unchanged
@@ -262,34 +262,8 @@ def _db_flush_failed(agent, e: Exception, batch_rows: List[Dict[str, Any]], adop
return False
def _session_log_entry(agent, msg: Dict[str, Any]) -> Dict[str, Any]:
"""Copy of ``msg`` with scratchpad tags normalised and credentials redacted (honours HERMES_REDACT_SECRETS)."""
if "content" not in msg:
return msg
content = msg["content"]
if msg.get("role") == "assistant" and content:
content = agent._clean_session_content(content)
return {**msg, "content": agent._redact_message_content(content)}
def _existing_log_is_larger(log_file, count: int) -> bool:
"""Never overwrite a larger log with fewer messages (resumed agent with partial history); a corrupted
existing file allows the overwrite."""
if not log_file.exists():
return False
try:
existing = json.loads(log_file.read_text(encoding="utf-8"))
existing_count = existing.get("message_count", len(existing.get("messages", [])))
except Exception:
return False
if existing_count > count:
logging.debug("Skipping session log overwrite: existing has %d messages, current has %d", existing_count, count)
return True
return False
class SessionPersistenceMixin:
"""Session DB flush, session log and trajectory persistence (see module docstring)."""
"""Session DB flush and trajectory persistence (see module docstring)."""
def _apply_persist_user_message_override(self, messages: List[Dict]) -> None:
"""Rewrite the current-turn user message in place: some paths send an API-only variant that must not
@@ -311,7 +285,7 @@ class SessionPersistenceMixin:
msg["platform_message_id"] = platform_id
def _persist_session(self, messages: List[Dict], conversation_history: List[Dict] = None):
"""Save to JSON log and SQLite on any exit path. Trailing empty-response scaffolding is dropped from
"""Save to SQLite on any exit path. Trailing empty-response scaffolding is dropped from
the live list; the persist override is applied to the DB row only.
The persist user-message *override* is NOT applied here — it is resolved inside
@@ -322,7 +296,6 @@ class SessionPersistenceMixin:
with _persist_lock(self):
self._drop_trailing_empty_response_scaffolding(messages)
self._session_messages = messages
self._save_session_log(messages)
self._flush_messages_to_session_db(messages, conversation_history)
# Drain async token-accounting deltas at every persist point; cheap no-op when nothing queued.
if self._session_db is not None:
@@ -413,49 +386,3 @@ class SessionPersistenceMixin:
_extract_api_error_context = _forward_static("agent.agent_runtime_helpers", "extract_api_error_context")
_dump_api_request_debug = _forward("agent.agent_runtime_helpers", "dump_api_request_debug")
@staticmethod
def _clean_session_content(content: str) -> str:
"""Convert REASONING_SCRATCHPAD to think tags and clean up whitespace."""
if not content:
return content
content = re.sub(r'\n+(<think>)', r'\n\1', convert_scratchpad_to_think(content))
return re.sub(r'(</think>)\n+', r'\1\n', content).strip()
@staticmethod
def _redact_message_content(content):
"""Redact secrets in str or list-of-parts content (text fields only; honours HERMES_REDACT_SECRETS)."""
if isinstance(content, str):
return redact_sensitive_text(content)
if not isinstance(content, list):
return content
return [{**p, **{k: redact_sensitive_text(p[k]) for k in ("text", "content") if isinstance(p.get(k), str)}}
if isinstance(p, dict) else p for p in content]
def _save_session_log(self, messages: List[Dict[str, Any]] = None):
"""Optional per-session JSON snapshot (``sessions.write_json_snapshots``, default False) for external
tooling; state.db is canonical. Rewrites the full list after every persistence point."""
if not getattr(self, "_session_json_enabled", False):
return
messages = messages or self._session_messages
if not messages:
return
try: # re-derive the path each call so /branch and /compress land in the right file
log_file = self.logs_dir / f"session_{_safe_session_filename_component(self.session_id)}.json"
except Exception:
return
try:
# Mirror the SQLite flush: scaffolding is never durable transcript content.
cleaned = [_session_log_entry(self, msg) for msg in messages if not _is_ephemeral_scaffolding(msg)]
if _existing_log_is_larger(log_file, len(cleaned)):
return
entry = {
"session_id": self.session_id, "model": self.model, "base_url": self.base_url, "platform": self.platform,
"session_start": self.session_start.isoformat(), "last_updated": datetime.now().isoformat(),
"system_prompt": redact_sensitive_text(self._cached_system_prompt or ""), "tools": self.tools or [],
"message_count": len(cleaned), "messages": cleaned,
}
atomic_json_write(log_file, entry, indent=2, default=str)
except Exception as e:
if self.verbose_logging:
logging.warning(f"Failed to save session log: {e}")

View File

@@ -0,0 +1,36 @@
"""Real config → SQLite persistence → explicit /save A/B, without provider calls.
Run with a clean HOME/HERMES_HOME and PYTHONPATH pointing at the tree under test.
"""
import json
import os
from pathlib import Path
from types import SimpleNamespace
home = Path(os.environ["HERMES_HOME"])
home.mkdir(parents=True, exist_ok=True)
(home / "config.yaml").write_text("sessions:\n write_json_snapshots: true\n", encoding="utf-8")
from agent.agent_init import _init_session_state
from agent.session_persistence import SessionPersistenceMixin
from hermes_cli.cli_session_mixin import CLISessionMixin
from hermes_state import SessionDB
with SessionDB(db_path=home / "state.db") as db:
agent = SessionPersistenceMixin()
agent.max_iterations = 1
_init_session_state(agent, "snapshot-probe", db, None, None, None, False, 1, 1, 1)
agent.model, agent.base_url, agent.platform = "fixture", "http://127.0.0.1:1/v1", "cli"
agent.tools, agent.verbose_logging = [], True
db.create_session(agent.session_id, source="cli", model=agent.model)
agent._session_db_created = True
messages = [{"role": "user", "content": "retained question"},
{"role": "assistant", "content": "retained answer"}]
agent._persist_session(messages)
snapshots = [p.name for p in agent.logs_dir.glob("session_*.json")]
cli = SimpleNamespace(_session_db=db, session_id=agent.session_id)
explicit = home / "explicit.json"
CLISessionMixin.save_conversation(cli, f"/save json {explicit}")
saved = json.loads(explicit.read_text(encoding="utf-8"))
print(json.dumps({"automatic_snapshots": snapshots,
"sqlite_messages": len(db.get_messages_as_conversation(agent.session_id)),
"explicit_saved_messages": [m["content"] for m in saved["messages"]]}))

View File

@@ -2023,10 +2023,7 @@ DEFAULT_CONFIG = {
# Minimum hours between auto-maintenance runs (tracked in state.db state_meta, shared across
# processes).
"min_interval_hours": 24,
# Legacy ~/.hermes/sessions/session_{sid}.json snapshots rewritten every turn. state.db is
# canonical (superset); snapshots consumed GBs on heavy users. Enable only for an external
# tool that reads the JSON files directly.
"write_json_snapshots": False,
# Notice about the compact FTS layout (reclaims ~60%+ of state.db). OPT-IN: legacy indexes
# stay until `hermes sessions optimize-storage` runs, since the rebuild is disk-heavy on
# large DBs. advise = `hermes update` prints a one-line notice with reclaimable size when a

View File

@@ -0,0 +1,42 @@
"""Automatic persistence stays in SQLite; explicit exports remain available."""
import json
from pathlib import Path
from types import SimpleNamespace
import pytest
from agent.agent_init import _init_session_state
from agent.session_persistence import SessionPersistenceMixin
from hermes_state import SessionDB
@pytest.mark.parametrize("legacy_enabled", [False, True])
def test_persistence_never_snapshots_but_explicit_save_works(tmp_path, monkeypatch, legacy_enabled):
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
(tmp_path / "config.yaml").write_text(
f"sessions:\n write_json_snapshots: {str(legacy_enabled).lower()}\n", encoding="utf-8")
db = SessionDB(db_path=tmp_path / "state.db")
agent = SessionPersistenceMixin()
agent.max_iterations = 1
_init_session_state(agent, "snapshot-removal", db, None, None, None, False, 1, 1, 1)
agent.model = "fixture"
agent.base_url = "http://127.0.0.1:1/v1"
agent.platform = "cli"
agent.tools = []
agent.verbose_logging = True
db.create_session(agent.session_id, source="cli", model=agent.model)
agent._session_db_created = True
messages = [{"role": "user", "content": "keep this conversation"},
{"role": "assistant", "content": "durable answer"}]
try:
agent._persist_session(messages)
assert len(db.get_messages_as_conversation(agent.session_id)) == 2
assert not list(agent.logs_dir.glob("session_*.json"))
from hermes_cli.cli_session_mixin import CLISessionMixin
cli = SimpleNamespace(_session_db=db, session_id=agent.session_id)
output = tmp_path / "explicit.json"
CLISessionMixin.save_conversation(cli, f"/save json {output}")
saved = json.loads(output.read_text(encoding="utf-8"))
assert [m["content"] for m in saved["messages"]] == [m["content"] for m in messages]
finally:
db.close()

View File

@@ -12,7 +12,6 @@ gets stripped from the durable transcript. This test file verifies:
- The JSON log drops only the nudge, keeping the assistant candidate.
"""
import json
import sys
from unittest.mock import MagicMock
@@ -85,7 +84,7 @@ def _make_agent(ra, session_id, tmp_path):
)
agent._session_db = MagicMock()
agent._session_db_created = True
agent._session_json_enabled = True
agent.logs_dir = tmp_path / "logs"
agent.logs_dir.mkdir(parents=True, exist_ok=True)
return agent
@@ -120,34 +119,3 @@ def test_db_flush_drops_only_nudge_keeps_candidate(tmp_path, monkeypatch):
assert "premature done" in persisted
# Only the nudge is dropped.
assert "[System: run tests]" not in persisted
def test_json_log_drops_only_nudge_keeps_candidate(tmp_path, monkeypatch):
"""The assistant candidate is NOT flagged synthetic, so it persists in the
JSON log. Only the nudge (flagged synthetic) is dropped."""
monkeypatch.setenv("HERMES_HOME", str(tmp_path / ".hermes"))
ra = _fresh_run_agent(tmp_path)
agent = _make_agent(ra, "sess_json", tmp_path)
messages = [
{"role": "user", "content": "hi"},
# Assistant candidate — NOT flagged synthetic, persists.
{"role": "assistant", "content": "premature done"},
# Nudge — flagged synthetic, gets dropped.
{"role": "user", "content": "[System: run tests]", "_pre_verify_synthetic": True},
{"role": "assistant", "content": "verified and clean"},
]
agent._save_session_log(messages)
log_file = agent.logs_dir / "session_sess_json.json"
assert log_file.exists()
data = json.loads(log_file.read_text(encoding="utf-8"))
contents = [m.get("content") for m in data["messages"]]
# The assistant candidate persists — it is real content.
assert "premature done" in contents
assert "verified and clean" in contents
assert "hi" in contents
# Only the nudge is dropped.
assert "[System: run tests]" not in contents
assert all(not m.get("_pre_verify_synthetic") for m in data["messages"])

View File

@@ -270,7 +270,6 @@ def test_chat_multimodal_note_persists_clean_input_once(tmp_path, monkeypatch):
agent._cached_system_prompt = "test system prompt"
agent._session_init_model_config = None
agent._parent_session_id = None
agent._session_json_enabled = False
agent._pending_cli_user_message = None
agent._session_persist_lock = threading.RLock()
agent._persist_user_message_idx = None
@@ -455,7 +454,6 @@ def test_close_waits_for_atomic_cli_staging_before_snapshot(tmp_path, monkeypatc
agent._cached_system_prompt = "test system prompt"
agent._session_init_model_config = None
agent._parent_session_id = None
agent._session_json_enabled = False
agent._pending_cli_user_message = None
agent._session_persist_lock = threading.RLock()
agent._persist_user_message_idx = None

View File

@@ -17,7 +17,6 @@ other tests keep their existing no-arg behaviour.
from __future__ import annotations
import threading
import types
from typing import Any
from unittest.mock import MagicMock, patch
@@ -98,7 +97,7 @@ def _real_agent(db, session_id, session_messages):
agent._cached_system_prompt = "test system prompt"
agent._session_init_model_config = None
agent._parent_session_id = None
agent._session_json_enabled = False
agent._pending_cli_user_message = None
agent._session_persist_lock = threading.RLock()
return agent

View File

@@ -5,12 +5,9 @@ pieces. The OpenAI client and tool loading are mocked so no network calls
are made.
"""
import ast
import inspect
import io
import json
import logging
import re
import threading
import time
import uuid
@@ -164,7 +161,7 @@ def test_direct_session_db_flushes_share_marker_claim(agent):
agent._persist_user_message_timestamp = None
agent._persist_disabled = False
agent._session_persist_lock = threading.RLock()
agent._session_json_enabled = False
message = {"role": "user", "content": "exactly once"}
normal = threading.Thread(target=lambda: agent._persist_session([message], []))
@@ -500,67 +497,7 @@ class TestExtractReasoning:
class TestSessionJsonSnapshotOptIn:
"""Regression: per-session JSON snapshot writer is opt-in via config.
state.db is canonical (PR #29182). ``sessions.write_json_snapshots``
defaults to False, so the agent must NOT write ``session_{sid}.json``
files by default — that behavior caused multi-GB sessions directories
on heavy users. Users can opt back in for external tooling that reads
the JSON files directly.
"""
def test_session_json_disabled_by_default(self, agent):
# Default config: writer is gated off.
assert getattr(agent, "_session_json_enabled", False) is False, (
"sessions.write_json_snapshots must default to False"
)
def test_save_session_log_noops_when_disabled(self, agent, tmp_path):
# When disabled, calling the method must not write any file even
# if logs_dir is writable and messages are non-empty.
agent._session_json_enabled = False
agent.logs_dir = tmp_path
agent._session_messages = [{"role": "user", "content": "hello"}]
agent._save_session_log()
# No session_*.json must appear under logs_dir.
assert list(tmp_path.glob("session_*.json")) == []
def test_save_session_log_writes_when_enabled(self, agent, tmp_path):
# Opt-in path: with the flag on and a session_id, the writer must
# produce ``session_{sid}.json`` under logs_dir.
agent._session_json_enabled = True
agent.logs_dir = tmp_path
messages = [{"role": "user", "content": "hello"}]
agent._save_session_log(messages)
expected = tmp_path / f"session_{agent.session_id}.json"
assert expected.exists(), (
"Opt-in writer must produce session_{sid}.json under logs_dir"
)
def test_logs_dir_retained_for_request_dumps(self, agent):
# logs_dir is kept unconditionally because
# agent_runtime_helpers.dump_api_request_debug still writes
# request_dump_*.json there (debug breadcrumb path), independent of
# the session JSON opt-in.
assert hasattr(agent, "logs_dir")
def test_traversal_session_id_cannot_escape_logs_dir(self, agent, tmp_path):
# Security regression (#5958): a traversal-shaped session ID (which can
# originate from the untrusted X-Hermes-Session-Id API header) must not
# redirect the session snapshot outside the sessions directory.
agent._session_json_enabled = True
agent.logs_dir = tmp_path
agent.session_id = "../../../../outside_dir/pwned"
agent._save_session_log([{"role": "user", "content": "hello"}])
# Exactly one snapshot, and it lives directly under logs_dir.
written = list(tmp_path.glob("session_*.json"))
assert len(written) == 1, "writer must produce a single contained snapshot"
assert written[0].resolve().parent == tmp_path.resolve()
# Nothing escaped to the traversal target.
assert not (tmp_path.parent.parent / "outside_dir").exists()
class TestSessionFilenameSafety:
def test_safe_session_filename_component_contains_traversal(self):
# The sanitizer is the chokepoint: every session-ID-derived artifact
# path goes through it, so it must always yield a single, traversal-free
@@ -574,76 +511,6 @@ class TestSessionJsonSnapshotOptIn:
assert f("../a") != f("../b")
class TestSaveSessionLogRedactsSecrets:
"""Regression: session_*.json must not contain plaintext credentials (#19798, #19845)."""
@pytest.fixture(autouse=True)
def _ensure_redaction_enabled(self, monkeypatch):
"""Force redaction on regardless of host HERMES_REDACT_SECRETS state.
The hermetic conftest blanks the env var; the module-level
``_REDACT_ENABLED`` constant is captured at import time, so we
flip it directly for the duration of these tests."""
monkeypatch.delenv("HERMES_REDACT_SECRETS", raising=False)
monkeypatch.setattr("agent.redact._REDACT_ENABLED", True)
def test_redacts_api_key_in_tool_content(self, agent, tmp_path):
agent._session_json_enabled = True
agent.logs_dir = tmp_path
messages = [
{"role": "user", "content": "Hello"},
{
"role": "tool",
"content": "Response: Authorization: Bearer sk-proj-abc123def456ghi789jkl012mno",
},
]
agent._save_session_log(messages)
snapshot = (tmp_path / f"session_{agent.session_id}.json").read_text(encoding="utf-8")
assert "sk-proj-abc123def456ghi789jkl012mno" not in snapshot
def test_redacts_api_key_in_user_message(self, agent, tmp_path):
agent._session_json_enabled = True
agent.logs_dir = tmp_path
messages = [
{"role": "user", "content": "My key is sk-ant-api03-abc123def456ghi789jkl012mno please use it"},
]
agent._save_session_log(messages)
snapshot = (tmp_path / f"session_{agent.session_id}.json").read_text(encoding="utf-8")
assert "sk-ant-api03-abc123def456ghi789jkl012mno" not in snapshot
def test_redacts_system_prompt_credentials(self, agent, tmp_path):
agent._session_json_enabled = True
agent.logs_dir = tmp_path
agent._cached_system_prompt = "Use key sk-proj-realkey1234567890123456 for API calls"
agent._save_session_log([{"role": "user", "content": "test"}])
snapshot = (tmp_path / f"session_{agent.session_id}.json").read_text(encoding="utf-8")
assert "sk-proj-realkey1234567890123456" not in snapshot
def test_redacts_list_type_multimodal_content(self, agent, tmp_path):
"""OpenAI/Anthropic multimodal shape: content = list of {type, text|image_url} parts."""
agent._session_json_enabled = True
agent.logs_dir = tmp_path
messages = [
{
"role": "user",
"content": [
{"type": "text", "text": "Key: gsk_abc123def456ghi789jkl012mno"},
{"type": "image_url", "image_url": {"url": "data:image/png;base64,abc"}},
],
},
]
agent._save_session_log(messages)
snapshot_text = (tmp_path / f"session_{agent.session_id}.json").read_text(encoding="utf-8")
snapshot = json.loads(snapshot_text)
parts = snapshot["messages"][0]["content"]
assert "gsk_abc123def456ghi789jkl012mno" not in parts[0]["text"]
# Image part preserved untouched
assert parts[1]["image_url"]["url"].startswith("data:image")
class TestGetMessagesUpToLastAssistant:
def test_empty_list(self, agent):
assert agent._get_messages_up_to_last_assistant([]) == []
@@ -6760,7 +6627,6 @@ class TestAnthropicInterruptHandler:
Replaces the former source-reading assertion (which asserted the old,
now-removed rebuild-on-interrupt behavior) with a behavior test.
"""
import threading
import time
from unittest.mock import MagicMock
from run_agent import AIAgent

View File

@@ -38,7 +38,7 @@ def agent(tmp_path, monkeypatch):
)
instance._cached_system_prompt = "stable test prompt"
instance._session_db = None
instance._session_json_enabled = False
instance.save_trajectories = False
instance.compression_enabled = False
instance._cleanup_task_resources = lambda *_a, **_kw: None
@@ -251,5 +251,3 @@ def test_streamed_interim_then_different_summary_not_marked_previewed(agent, mon
# CRITICAL: response_previewed must be False — the interim narration was
# NOT the final response, so the CLI must render the summary.
assert result["response_previewed"] is False

View File

@@ -26,7 +26,6 @@ import threading
from types import SimpleNamespace
from unittest.mock import MagicMock, patch
import pytest
import tools.delegate_tool as dt
@@ -83,7 +82,7 @@ def _make_real_child():
# Keep the test hermetic: no session persistence.
child._persist_disabled = True
child._session_db = None
child._session_json_enabled = False
return child

View File

@@ -12,10 +12,8 @@ Scenarios:
"""
import threading
import time
from unittest.mock import MagicMock, PropertyMock, patch
from unittest.mock import MagicMock, patch
import pytest
# ---------------------------------------------------------------------------
# Helpers
@@ -177,7 +175,7 @@ class TestFinalizeSessionPersistE2E:
agent._cached_system_prompt = None
agent._session_init_model_config = None
agent._parent_session_id = None
agent._session_json_enabled = False
agent.quiet_mode = True
# commit_memory_session runs heavy machinery we don't exercise here.
agent.commit_memory_session = lambda *a, **k: None
@@ -270,4 +268,3 @@ class TestOnSessionEndHook:
model="claude-sonnet-4",
platform="tui",
)