runtime-memory 3.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. runtime_memory/__init__.py +28 -0
  2. runtime_memory/claude_code/__init__.py +48 -0
  3. runtime_memory/claude_code/commands.py +698 -0
  4. runtime_memory/claude_code/daemon.py +852 -0
  5. runtime_memory/claude_code/hooks.py +722 -0
  6. runtime_memory/cli/__init__.py +8 -0
  7. runtime_memory/cli/main.py +1936 -0
  8. runtime_memory/core/__init__.py +216 -0
  9. runtime_memory/core/config.py +473 -0
  10. runtime_memory/core/embeddings.py +908 -0
  11. runtime_memory/core/engine.py +1007 -0
  12. runtime_memory/core/exceptions.py +547 -0
  13. runtime_memory/core/legacy_env.py +39 -0
  14. runtime_memory/core/logging.py +160 -0
  15. runtime_memory/core/models.py +1051 -0
  16. runtime_memory/core/observability.py +725 -0
  17. runtime_memory/core/paths.py +30 -0
  18. runtime_memory/core/resilience.py +511 -0
  19. runtime_memory/core/retrieval.py +819 -0
  20. runtime_memory/core/storage.py +1105 -0
  21. runtime_memory/extraction/__init__.py +36 -0
  22. runtime_memory/extraction/extractor.py +1143 -0
  23. runtime_memory/hermes/__init__.py +39 -0
  24. runtime_memory/hermes/_base.py +154 -0
  25. runtime_memory/hermes/bridge.py +119 -0
  26. runtime_memory/hermes/plugin.yaml +13 -0
  27. runtime_memory/hermes/provider.py +536 -0
  28. runtime_memory/hermes/tools.py +230 -0
  29. runtime_memory/hermes/trace.py +177 -0
  30. runtime_memory/plugin/__init__.py +646 -0
  31. runtime_memory/sdk/__init__.py +97 -0
  32. runtime_memory/sdk/client.py +1577 -0
  33. runtime_memory/server/__init__.py +75 -0
  34. runtime_memory/server/api.py +1665 -0
  35. runtime_memory/server/mcp.py +1574 -0
  36. runtime_memory/server/static/css/styles.css +1110 -0
  37. runtime_memory/server/static/index.html +264 -0
  38. runtime_memory/server/static/js/api.js +294 -0
  39. runtime_memory/server/static/js/app.js +771 -0
  40. runtime_memory/tasks/__init__.py +114 -0
  41. runtime_memory/tasks/adapter.py +501 -0
  42. runtime_memory/tasks/claude_code_adapter.py +495 -0
  43. runtime_memory/tasks/claude_code_parser.py +339 -0
  44. runtime_memory/tasks/cli_bridge.py +415 -0
  45. runtime_memory/tasks/linking.py +397 -0
  46. runtime_memory/tasks/models.py +520 -0
  47. runtime_memory/tasks/outcomes.py +320 -0
  48. runtime_memory/tasks/parser.py +305 -0
  49. runtime_memory/tasks/unified_adapter.py +661 -0
  50. runtime_memory-3.0.0.dist-info/METADATA +497 -0
  51. runtime_memory-3.0.0.dist-info/RECORD +54 -0
  52. runtime_memory-3.0.0.dist-info/WHEEL +4 -0
  53. runtime_memory-3.0.0.dist-info/entry_points.txt +6 -0
  54. runtime_memory-3.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,536 @@
1
+ """memory-layer as a Hermes Agent memory provider.
2
+
3
+ Hermes discovers this through the ``hermes_agent.memory_providers`` entry point
4
+ and activates it with ``memory.provider: runtimememory``. Once active it replaces
5
+ the built-in note files rather than supplementing them, so recall stops being a
6
+ fixed block of text pasted into every prompt and becomes a per-turn retrieval
7
+ against the same SQLite store Claude Code and the MCP clients already share.
8
+
9
+ Two design choices are worth stating up front, because both differ from the
10
+ built-in provider:
11
+
12
+ Recall is synchronous. The Hermes contract expects ``prefetch()`` to hand back a
13
+ result warmed in the background on the previous turn, because the bundled
14
+ providers talk to network services. This store is local SQLite, so recall runs
15
+ against the turn's actual query instead of the one before it. That matters for
16
+ measurement as much as for quality: an off-by-one between question and retrieval
17
+ would confound any attempt to attribute an outcome to what was recalled.
18
+
19
+ Writes are explicit. Persisting every turn verbatim would fill a curated store
20
+ with conversational debris and degrade the retrieval it exists to serve. Memories
21
+ arrive from the ``runtimememory_remember`` tool, from mirrored built-in memory
22
+ writes, and - only when switched on - from end-of-session extraction.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import os
28
+ import time
29
+ import uuid
30
+ from pathlib import Path
31
+ from typing import TYPE_CHECKING, Any
32
+
33
+ from runtime_memory.core.logging import get_logger
34
+ from runtime_memory.core.models import (
35
+ MemoryCategory,
36
+ MemoryScope,
37
+ MemorySource,
38
+ Outcome,
39
+ )
40
+ from runtime_memory.hermes._base import (
41
+ INDICATOR_GLYPH,
42
+ MemoryProvider,
43
+ RecallStatus,
44
+ is_trivial_prompt,
45
+ )
46
+ from runtime_memory.hermes.bridge import run_sync, spawn
47
+ from runtime_memory.hermes.tools import TOOL_SCHEMAS, dispatch
48
+
49
+ if TYPE_CHECKING:
50
+ from runtime_memory.core.engine import EngineStats, MemoryEngine
51
+ from runtime_memory.core.models import Memory, SearchResult
52
+
53
+ logger = get_logger(__name__)
54
+
55
+ PROVIDER_NAME = "runtimememory"
56
+ PROVIDER_LABEL = "Runtime Memory"
57
+
58
+ DEFAULT_DB_PATH = "~/.memory-layer/memories.db"
59
+ DEFAULT_RECALL_LIMIT = 8
60
+ DEFAULT_MIN_SCORE = 0.0
61
+
62
+ _WRITE_CONTEXTS = frozenset({"primary", ""})
63
+ """Agent contexts allowed to write. Subagents, cron and flush runs read only."""
64
+
65
+ _MIRROR_CATEGORY = {
66
+ "memory": MemoryCategory.CONTEXT,
67
+ "user": MemoryCategory.PREFERENCE,
68
+ }
69
+ """Hermes built-in write targets mapped onto memory-layer categories."""
70
+
71
+
72
+ def _env_flag(name: str, default: bool = False) -> bool:
73
+ """Read a boolean environment variable."""
74
+ raw = os.environ.get(name)
75
+ if raw is None:
76
+ return default
77
+ return raw.strip().lower() in {"1", "true", "yes", "on"}
78
+
79
+
80
+ def _embedding_provider_name() -> str:
81
+ """Name the embedding backend for the engine to build.
82
+
83
+ ``local`` is the right answer even when ``sentence-transformers`` is
84
+ missing: the engine factory degrades it to the null provider, which indexes
85
+ no vectors and leaves retrieval on the BM25 half of the hybrid. Repeating
86
+ that check here would give the store two answers to the same question, and
87
+ picking ``mock`` writes hash-derived vectors alongside real ones.
88
+ """
89
+ return os.environ.get("RUNTIME_MEMORY_EMBEDDING") or "local"
90
+
91
+
92
+ class RuntimeMemoryProvider(MemoryProvider):
93
+ """Hermes memory provider backed by a local memory-layer engine."""
94
+
95
+ pre_compress_checkpoint_api_version = 1
96
+
97
+ def __init__(self) -> None:
98
+ """Create an inactive provider. Nothing is opened until ``initialize``."""
99
+ self._engine: MemoryEngine | None = None
100
+ self._session_id: str = ""
101
+ self._project: str | None = None
102
+ self._writes_allowed: bool = True
103
+
104
+ self._db_path: Path = Path(os.environ.get("RUNTIME_MEMORY_DB", DEFAULT_DB_PATH)).expanduser()
105
+ self._recall_limit: int = int(
106
+ os.environ.get("RUNTIME_MEMORY_RECALL_LIMIT", DEFAULT_RECALL_LIMIT)
107
+ )
108
+ self._min_score: float = float(os.environ.get("RUNTIME_MEMORY_MIN_SCORE", DEFAULT_MIN_SCORE))
109
+ self._mirror_builtin: bool = _env_flag("RUNTIME_MEMORY_MIRROR_WRITES", True)
110
+ self._extract_on_end: bool = _env_flag("RUNTIME_MEMORY_EXTRACT_ON_END", False)
111
+
112
+ # Last recall, kept so an outcome can be attributed without the model
113
+ # having to repeat the memory ids back to us.
114
+ self._turn_id: str = ""
115
+ self._last_ids: list[str] = []
116
+ self._last_count: int = 0
117
+
118
+ from runtime_memory.hermes.trace import TraceWriter # noqa: PLC0415
119
+
120
+ self._trace = TraceWriter()
121
+
122
+ # -- identity ------------------------------------------------------------
123
+
124
+ @property
125
+ def name(self) -> str:
126
+ """Provider name, matched against ``memory.provider``."""
127
+ return PROVIDER_NAME
128
+
129
+ def is_available(self) -> bool:
130
+ """Whether the store can be opened. Checks the filesystem only."""
131
+ try:
132
+ self._db_path.parent.mkdir(parents=True, exist_ok=True)
133
+ return os.access(self._db_path.parent, os.W_OK)
134
+ except OSError as exc:
135
+ logger.debug(f"memory-layer unavailable: {exc}")
136
+ return False
137
+
138
+ def unavailable_reason(self) -> str:
139
+ """Explain an unavailable store."""
140
+ return f"Cannot write to {self._db_path.parent}. Set RUNTIME_MEMORY_DB to a writable path."
141
+
142
+ # -- lifecycle -----------------------------------------------------------
143
+
144
+ def initialize(self, session_id: str, **kwargs: Any) -> None:
145
+ """Open the engine and warm it before the first turn.
146
+
147
+ Args:
148
+ session_id: The Hermes session this provider instance serves.
149
+ **kwargs: Hermes context. ``agent_context`` gates writes;
150
+ ``agent_workspace`` scopes memories to a project.
151
+ """
152
+ from runtime_memory.core.engine import EngineConfig, MemoryEngine # noqa: PLC0415
153
+
154
+ self._session_id = session_id
155
+
156
+ agent_context = kwargs.get("agent_context", "primary")
157
+ self._writes_allowed = agent_context in _WRITE_CONTEXTS
158
+ if not self._writes_allowed:
159
+ logger.info(f"Read-only in '{agent_context}' context")
160
+
161
+ workspace = kwargs.get("agent_workspace")
162
+ self._project = os.environ.get("RUNTIME_MEMORY_PROJECT") or (
163
+ Path(workspace).name if workspace else None
164
+ )
165
+
166
+ self._db_path.parent.mkdir(parents=True, exist_ok=True)
167
+ config = EngineConfig(
168
+ db_path=str(self._db_path),
169
+ embedding_provider=_embedding_provider_name(),
170
+ track_last_search=True,
171
+ )
172
+
173
+ engine = MemoryEngine(config=config)
174
+ run_sync(engine.initialize(), timeout=120.0)
175
+ self._engine = engine
176
+
177
+ # Pull the embedding model into memory now. It costs ~20s on first load,
178
+ # and paying that here rather than inside the user's first turn is the
179
+ # difference between a slow start and a stalled reply.
180
+ spawn(self._warm(), label="warmup")
181
+
182
+ logger.info(
183
+ f"memory-layer ready (db={self._db_path}, project={self._project}, "
184
+ f"writes={'on' if self._writes_allowed else 'off'})"
185
+ )
186
+
187
+ async def _warm(self) -> None:
188
+ """Touch the retrieval path once so the first real query is fast."""
189
+ if self._engine is not None:
190
+ await self._engine.search("warmup", limit=1, track_usage=False)
191
+
192
+ def shutdown(self) -> None:
193
+ """Close the engine. The shared event loop deliberately stays up."""
194
+ if self._engine is None:
195
+ return
196
+ try:
197
+ run_sync(self._engine.close(), timeout=10.0)
198
+ except Exception as exc: # shutdown must not raise
199
+ logger.warning(f"Engine close failed: {exc}")
200
+ finally:
201
+ self._engine = None
202
+
203
+ def on_session_switch(
204
+ self,
205
+ new_session_id: str,
206
+ *,
207
+ parent_session_id: str = "",
208
+ reset: bool = False,
209
+ rewound: bool = False,
210
+ **kwargs: Any,
211
+ ) -> None:
212
+ """Rebind to a new session id so later writes land in the right record."""
213
+ self._session_id = new_session_id
214
+ if reset:
215
+ self._turn_id, self._last_ids, self._last_count = "", [], 0
216
+
217
+ # -- recall --------------------------------------------------------------
218
+
219
+ def system_prompt_block(self) -> str:
220
+ """Static guidance. Recalled content is injected by ``prefetch``."""
221
+ return (
222
+ "You have persistent memory across sessions via Runtime Memory. "
223
+ "Relevant memories are retrieved automatically each turn. "
224
+ "Use runtimememory_remember to save a durable fact worth recalling "
225
+ "later, and runtimememory_outcome to report whether recalled memories "
226
+ "actually helped - that feedback decides what surfaces next time."
227
+ )
228
+
229
+ def prefetch(self, query: str, *, session_id: str = "") -> str:
230
+ """Retrieve memories for this turn and format them for injection.
231
+
232
+ Args:
233
+ query: The user's message for the upcoming turn.
234
+ session_id: Session scope, unused for a single shared store.
235
+
236
+ Returns:
237
+ A formatted memory block, or ``""`` when nothing is worth injecting.
238
+ """
239
+ self._turn_id = uuid.uuid4().hex
240
+ self._last_ids, self._last_count = [], 0
241
+
242
+ if self._engine is None or is_trivial_prompt(query):
243
+ return ""
244
+
245
+ started = time.perf_counter()
246
+ try:
247
+ results = run_sync(
248
+ self._engine.search(
249
+ query=query,
250
+ limit=self._recall_limit,
251
+ project=self._project,
252
+ min_score=self._min_score,
253
+ ),
254
+ timeout=15.0,
255
+ )
256
+ except Exception as exc: # a failed recall must never break the turn
257
+ logger.warning(f"Recall failed: {exc}")
258
+ return ""
259
+
260
+ if not results:
261
+ return ""
262
+
263
+ self._last_ids = [r.memory.id for r in results]
264
+ self._last_count = len(results)
265
+
266
+ self._trace.recall(
267
+ turn_id=self._turn_id,
268
+ session_id=session_id or self._session_id,
269
+ query=query,
270
+ results=results,
271
+ project=self._project,
272
+ latency_ms=(time.perf_counter() - started) * 1000,
273
+ )
274
+ return self._format(results)
275
+
276
+ def queue_prefetch(self, query: str, *, session_id: str = "") -> None:
277
+ """No-op. Recall is synchronous, so there is nothing to warm."""
278
+
279
+ def recall_status(self) -> RecallStatus | None:
280
+ """Report what the last recall injected, for the indicator line."""
281
+ if not self._last_count:
282
+ return None
283
+ return RecallStatus(
284
+ provider_label=PROVIDER_LABEL,
285
+ count=self._last_count,
286
+ glyph=INDICATOR_GLYPH,
287
+ )
288
+
289
+ def _format(self, results: list[SearchResult]) -> str:
290
+ """Render search results as a promptable block.
291
+
292
+ Each line carries its memory id so the model can name specific memories
293
+ when reporting an outcome, and its outcome score so a memory with a poor
294
+ track record reads as weaker evidence than one that keeps working.
295
+ """
296
+ lines = ["## Relevant memories", ""]
297
+ for result in results:
298
+ memory = result.memory
299
+ # Thresholds are inclusive so a single recorded outcome is enough to
300
+ # show: one `worked` lands exactly on +0.2, one `failed` on -0.3.
301
+ marker = ""
302
+ if memory.outcome_score >= 0.2:
303
+ marker = " (has worked before)"
304
+ elif memory.outcome_score <= -0.2:
305
+ marker = " (has failed before)"
306
+ lines.append(f"- [{memory.category.value}] {memory.content}{marker} `{memory.id}`")
307
+ return "\n".join(lines)
308
+
309
+ # -- writes --------------------------------------------------------------
310
+
311
+ def sync_turn(
312
+ self,
313
+ user_content: str,
314
+ assistant_content: str,
315
+ *,
316
+ session_id: str = "",
317
+ messages: list[dict[str, Any]] | None = None,
318
+ ) -> None:
319
+ """Called after every turn. Intentionally does not persist.
320
+
321
+ Storing raw turns would bury the curated memories that make retrieval
322
+ useful. Facts reach the store through the remember tool, mirrored
323
+ built-in writes, or opt-in end-of-session extraction.
324
+ """
325
+
326
+ def on_memory_write(
327
+ self,
328
+ action: str,
329
+ target: str,
330
+ content: str,
331
+ metadata: dict[str, Any] | None = None,
332
+ ) -> None:
333
+ """Mirror a built-in memory write into the shared store.
334
+
335
+ This is what lifts the built-in character cap in practice: Hermes keeps
336
+ its small always-injected note file, and the same fact also lands here,
337
+ where it is retrieved on relevance instead of pasted in wholesale.
338
+ """
339
+ if not (self._mirror_builtin and self._writes_allowed):
340
+ return
341
+ if action not in {"add", "replace"} or not content.strip():
342
+ return
343
+
344
+ category = _MIRROR_CATEGORY.get(target, MemoryCategory.CONTEXT)
345
+ spawn(
346
+ self._store(
347
+ content=content.strip(),
348
+ category=category,
349
+ source=MemorySource.IMPORTED,
350
+ tags=["hermes", f"builtin-{target}"],
351
+ metadata={"hermes_action": action, **(metadata or {})},
352
+ kind="mirror",
353
+ ),
354
+ label="mirror write",
355
+ )
356
+
357
+ def on_session_end(self, messages: list[dict[str, Any]]) -> None:
358
+ """Optionally extract durable facts when a session really ends."""
359
+ if not (self._extract_on_end and self._writes_allowed):
360
+ return
361
+ if self._engine is None or not messages:
362
+ return
363
+ logger.info(f"Session end: extraction over {len(messages)} messages")
364
+ spawn(self._extract(messages), label="extraction")
365
+
366
+ async def _extract(self, messages: list[dict[str, Any]]) -> None:
367
+ """Run LLM extraction over a finished session.
368
+
369
+ Requires the ``extraction`` extra and an API key. Failures are logged and
370
+ dropped: a missed extraction is a lost convenience, not a lost session.
371
+ """
372
+ from runtime_memory.extraction.extractor import MemoryExtractor # noqa: PLC0415
373
+
374
+ transcript = "\n".join(
375
+ f"{m.get('role', '?')}: {m.get('content', '')}" for m in messages if m.get("content")
376
+ )
377
+ result = await MemoryExtractor().extract_and_store(
378
+ transcript=transcript,
379
+ engine=self._require_engine(),
380
+ project=self._project,
381
+ )
382
+ if not result.success:
383
+ logger.warning(f"Extraction failed: {result.error}")
384
+ return
385
+
386
+ # extract_and_store writes through the engine without handing back the
387
+ # stored rows, so the trace records how many landed, not which.
388
+ if result.memory_count:
389
+ self._trace.write_turn(
390
+ turn_id=self._turn_id,
391
+ session_id=self._session_id,
392
+ memory_ids=[],
393
+ kind="extraction",
394
+ count=result.memory_count,
395
+ )
396
+
397
+ async def _store(
398
+ self,
399
+ *,
400
+ content: str,
401
+ category: MemoryCategory,
402
+ source: MemorySource,
403
+ tags: list[str],
404
+ metadata: dict[str, Any] | None = None,
405
+ kind: str = "tool",
406
+ ) -> Memory:
407
+ """Write one memory and trace it."""
408
+ memory = await self._engine.add( # type: ignore[union-attr]
409
+ content=content,
410
+ category=category,
411
+ project=self._project,
412
+ scope=MemoryScope.PROJECT if self._project else MemoryScope.GLOBAL,
413
+ source=source,
414
+ tags=tags,
415
+ metadata={"hermes_session": self._session_id, **(metadata or {})},
416
+ )
417
+ self._trace.write_turn(
418
+ turn_id=self._turn_id,
419
+ session_id=self._session_id,
420
+ memory_ids=[memory.id],
421
+ kind=kind,
422
+ )
423
+ return memory
424
+
425
+ # -- tools ---------------------------------------------------------------
426
+
427
+ def get_tool_schemas(self) -> list[dict[str, Any]]:
428
+ """Function-calling schemas for the tools this provider handles."""
429
+ return TOOL_SCHEMAS
430
+
431
+ def handle_tool_call(self, tool_name: str, args: dict[str, Any], **kwargs: Any) -> str:
432
+ """Handle one tool call, returning a JSON string."""
433
+ return dispatch(self, tool_name, args)
434
+
435
+ # -- synchronous helpers used by tool dispatch ---------------------------
436
+
437
+ def _require_engine(self) -> MemoryEngine:
438
+ """Return the engine or explain why there isn't one."""
439
+ if self._engine is None:
440
+ raise RuntimeError("Runtime Memory is not initialized")
441
+ return self._engine
442
+
443
+ def remember(self, *, content: str, category: MemoryCategory, tags: list[str]) -> Memory:
444
+ """Store a memory on the model's explicit instruction."""
445
+ self._require_engine()
446
+ if not self._writes_allowed:
447
+ raise RuntimeError("Writes are disabled in this agent context")
448
+ return run_sync(
449
+ self._store(
450
+ content=content,
451
+ category=category,
452
+ source=MemorySource.EXPLICIT,
453
+ tags=[*tags, "hermes"],
454
+ )
455
+ )
456
+
457
+ def recall(
458
+ self, *, query: str, limit: int, category: MemoryCategory | None
459
+ ) -> list[SearchResult]:
460
+ """Run an explicit search, separate from the automatic per-turn recall."""
461
+ engine = self._require_engine()
462
+ results = run_sync(
463
+ engine.search(query=query, limit=limit, category=category, project=self._project)
464
+ )
465
+ # Fold into the turn's recall set so an outcome can credit these too.
466
+ for result in results:
467
+ if result.memory.id not in self._last_ids:
468
+ self._last_ids.append(result.memory.id)
469
+ return results
470
+
471
+ def record_outcome(
472
+ self, *, outcome: Outcome, memory_ids: list[str] | None = None
473
+ ) -> list[Memory]:
474
+ """Apply outcome feedback, defaulting to this turn's recalled memories."""
475
+ engine = self._require_engine()
476
+ targets = memory_ids or self._last_ids
477
+ if not targets:
478
+ return []
479
+
480
+ updated = run_sync(engine.record_outcome(targets, outcome))
481
+ self._trace.outcome(
482
+ turn_id=self._turn_id,
483
+ session_id=self._session_id,
484
+ outcome=outcome.value,
485
+ memory_ids=[memory.id for memory in updated],
486
+ origin="tool" if memory_ids else "auto",
487
+ )
488
+ return updated
489
+
490
+ def stats(self) -> EngineStats:
491
+ """Return store statistics."""
492
+ return run_sync(self._require_engine().stats(project=self._project))
493
+
494
+ # -- configuration -------------------------------------------------------
495
+
496
+ def get_config_schema(self) -> list[dict[str, Any]]:
497
+ """Fields offered by ``hermes memory setup``."""
498
+ return [
499
+ {
500
+ "key": "db_path",
501
+ "description": "SQLite store shared with Claude Code and MCP clients",
502
+ "default": DEFAULT_DB_PATH,
503
+ "env_var": "RUNTIME_MEMORY_DB",
504
+ "type": "text",
505
+ },
506
+ {
507
+ "key": "recall_limit",
508
+ "description": "Memories injected per turn",
509
+ "default": DEFAULT_RECALL_LIMIT,
510
+ "env_var": "RUNTIME_MEMORY_RECALL_LIMIT",
511
+ "type": "integer",
512
+ "minimum": 1,
513
+ "maximum": 50,
514
+ },
515
+ {
516
+ "key": "mirror_writes",
517
+ "description": "Mirror built-in memory writes into the store",
518
+ "default": True,
519
+ "env_var": "RUNTIME_MEMORY_MIRROR_WRITES",
520
+ "type": "boolean",
521
+ },
522
+ {
523
+ "key": "extract_on_end",
524
+ "description": "Extract memories at session end (needs an API key)",
525
+ "default": False,
526
+ "env_var": "RUNTIME_MEMORY_EXTRACT_ON_END",
527
+ "type": "boolean",
528
+ },
529
+ ]
530
+
531
+ def save_config(self, values: dict[str, Any], hermes_home: str) -> None:
532
+ """No-op: every setting is an environment variable."""
533
+
534
+ def backup_paths(self) -> list[str]:
535
+ """The store lives outside HERMES_HOME, so name it for ``hermes backup``."""
536
+ return [str(self._db_path)]