Enable implicit phase-memory activation on every warden command.

Load coordination memory by default via ensure_memory_context on app bootstrap
and route/access flows; invalidate cache after episode writes. WARDEN_MEMORY=0
remains the opt-out. Document that warden memory activate is optional only.
This commit is contained in:
2026-07-03 00:49:36 +02:00
parent 04929e7981
commit 120de64bcb
6 changed files with 129 additions and 13 deletions

View File

@@ -161,15 +161,19 @@ get wrong.
ops-warden shares a **phase-memory** store across worker ticks, coding agent
sessions, and operator CLI use.
**At session start** (Claude Code, Codex, Grok, or future agents):
**Default:** phase-memory loads automatically on every `warden` command when
`phase-memory` is available. No separate activation step is required.
**Agent sessions** should set runtime identity once per session:
```bash
export WARDEN_AGENT_ID=grok # or claude, codex
warden memory activate --json
```
**During work:** use normal `warden route` / `warden access` / `warden sign`;
episodes are recorded automatically unless `WARDEN_MEMORY=0`.
Then use normal `warden route` / `warden access` / `warden sign` / `warden worker`.
Episodes are recorded automatically unless `WARDEN_MEMORY=0`.
`warden memory activate` is optional introspection/refresh, not a prerequisite.
**Store:** `~/.local/share/warden/memory/` (override: `WARDEN_MEMORY_STORE`).

View File

@@ -54,6 +54,19 @@ console = Console()
err = Console(stderr=True)
@app.callback()
def _bootstrap_memory(ctx: typer.Context) -> None:
"""Implicitly load phase-memory for every warden command (opt-out: WARDEN_MEMORY=0)."""
if ctx.invoked_subcommand is None:
return
try:
from warden import memory as warden_memory
warden_memory.ensure_memory_context(implicit=True)
except Exception: # noqa: BLE001 — memory must never block warden commands
return
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
@@ -783,6 +796,12 @@ def route_find(
limit: Annotated[int, typer.Option("--limit", help="Max matches")] = 5,
) -> None:
"""Rank routing scenarios by keyword overlap with the query."""
try:
from warden import memory as warden_memory
warden_memory.ensure_memory_context(need=query, implicit=True)
except Exception: # noqa: BLE001
pass
catalog = _load_catalog()
matches = catalog.find(query, include_draft=all_entries, limit=limit)
@@ -1026,6 +1045,12 @@ def access(
"""
from warden.access import expand_handoff, policy_gate_status
try:
from warden import memory as warden_memory
warden_memory.ensure_memory_context(need=need, implicit=True)
except Exception: # noqa: BLE001
pass
catalog = _load_catalog()
matches = catalog.find(need, include_draft=all_entries, limit=1)
if not matches:
@@ -1408,14 +1433,14 @@ def memory_activate(
] = None,
output_json: Annotated[bool, typer.Option("--json", help="Output JSON")] = False,
) -> None:
"""Activate bounded coordination memory for worker, operator, or agent sessions."""
"""Inspect or refresh coordination memory (optional — memory loads by default)."""
from warden import memory as warden_memory
if not warden_memory.memory_available():
err.print(f"[red]{warden_memory._PHASE_MEMORY_ERROR}[/red]")
raise typer.Exit(2)
try:
payload = warden_memory.activate(need=need, agent=agent)
payload = warden_memory.activate(need=need, agent=agent, implicit=False)
except RuntimeError as e:
err.print(f"[red]{e}[/red]")
raise typer.Exit(2)

View File

@@ -10,6 +10,17 @@ _PHASE_MEMORY_ERROR = (
"Install with: pip install phase-memory (or set PYTHONPATH to phase-memory/src)."
)
# In-process cache: implicit activation is default; no separate `warden memory activate`
# is required for normal route/access/worker/sign use within one CLI invocation tree.
_CONTEXT_CACHE: dict[str, Any] | None = None
_CONTEXT_CACHE_KEY: tuple[str, str] = ("", "")
def _invalidate_context_cache() -> None:
global _CONTEXT_CACHE, _CONTEXT_CACHE_KEY
_CONTEXT_CACHE = None
_CONTEXT_CACHE_KEY = ("", "")
def _phase_memory():
try:
@@ -46,24 +57,76 @@ def status(environ: Mapping[str, str] | None = None) -> dict[str, Any]:
return pm.OpsWardenMemoryStore.open(environ=environ).status()
def memory_context_summary(activation: dict[str, Any] | None) -> dict[str, Any]:
if not activation:
return {"enabled": False}
return {
"enabled": True,
"implicit": bool(activation.get("implicit")),
"session_kind": activation.get("session_kind", ""),
"episode_count": activation.get("episode_count", 0),
"stabilized_route_id": (activation.get("stabilized_route") or {}).get("route_id", ""),
"llm_calls_avoided": bool(activation.get("llm_calls_avoided")),
"selected_episode_count": len(activation.get("selected_episodes") or ()),
}
def ensure_memory_context(
need: str = "",
*,
agent: Optional[str] = None,
session_id: str = "",
environ: Mapping[str, str] | None = None,
implicit: bool = True,
) -> dict[str, Any] | None:
"""Load coordination memory for the current session (default, no extra command)."""
global _CONTEXT_CACHE, _CONTEXT_CACHE_KEY
if not enabled(environ):
return None
if not memory_available():
return None
pm = _phase_memory()
env = dict(environ or os.environ)
if agent:
env["WARDEN_AGENT_ID"] = agent
kind = pm.resolve_session_kind(env)
fingerprint = pm.need_fingerprint(need) if need else ""
cache_key = (kind, fingerprint)
if _CONTEXT_CACHE is not None and _CONTEXT_CACHE_KEY == cache_key:
return _CONTEXT_CACHE
try:
activation = activate(need=need, agent=agent, session_id=session_id, environ=env)
except RuntimeError:
return None
activation = {**activation, "implicit": implicit}
_CONTEXT_CACHE = activation
_CONTEXT_CACHE_KEY = cache_key
return activation
def activate(
*,
need: str = "",
agent: Optional[str] = None,
session_id: str = "",
environ: Mapping[str, str] | None = None,
implicit: bool = False,
) -> dict[str, Any]:
pm = _phase_memory()
env = dict(environ or os.environ)
if agent:
env["WARDEN_AGENT_ID"] = agent
kind = pm.resolve_session_kind(env)
return pm.activate_ops_warden_memory(
activation = pm.activate_ops_warden_memory(
pm.OpsWardenMemoryStore.open(environ=env),
session_kind=kind,
need=need,
session_id=session_id,
)
if implicit:
activation = {**activation, "implicit": True}
return activation
def record_command_episode(
@@ -91,13 +154,16 @@ def record_command_episode(
diagnostic_codes=diagnostic_codes,
metadata=metadata,
)
return pm.record_session_event(pm.OpsWardenMemoryStore.open(environ=env), event)
result = pm.record_session_event(pm.OpsWardenMemoryStore.open(environ=env), event)
if result.get("valid"):
_invalidate_context_cache()
return result
def worker_activation_context(need: str = "", environ: Mapping[str, str] | None = None) -> dict[str, Any]:
env = dict(environ or os.environ)
env["WARDEN_SESSION_KIND"] = "warden.worker"
return activate(need=need, environ=env)
return ensure_memory_context(need=need, environ=env, implicit=True) or activate(need=need, environ=env, implicit=True)
def stabilized_route_for_need(need: str, environ: Mapping[str, str] | None = None) -> Optional[dict[str, Any]]:

View File

@@ -602,7 +602,9 @@ def _memory_activation_for_message(message: dict) -> tuple[Optional[dict], str]:
return None, ""
query = str(message.get("subject", "") or message.get("body", ""))
try:
activation = warden_memory.worker_activation_context(query)
activation = warden_memory.ensure_memory_context(need=query, implicit=True)
if activation is None:
activation = warden_memory.worker_activation_context(query)
except RuntimeError:
return None, ""
from warden.memory import format_activation_summary

View File

@@ -126,4 +126,18 @@ def test_memory_disabled_skips_recording(monkeypatch) -> None:
def test_default_store_path_uses_xdg(monkeypatch) -> None:
monkeypatch.delenv("WARDEN_MEMORY_STORE", raising=False)
assert str(store_path()).endswith("warden/memory")
assert str(store_path()).endswith("warden/memory")
def test_route_find_implicitly_activates_memory_without_explicit_command(tmp_path, monkeypatch) -> None:
from warden.memory import ensure_memory_context
monkeypatch.setenv("WARDEN_MEMORY_STORE", str(tmp_path / "memory"))
monkeypatch.delenv("WARDEN_AGENT_ID", raising=False)
result = runner.invoke(app, ["route", "find", "ssh tunnel", "--json"])
assert result.exit_code == 0
activation = ensure_memory_context(need="ssh tunnel", implicit=True)
assert activation is not None
assert activation.get("implicit") is True
assert status()["episode_count"] >= 1

View File

@@ -19,13 +19,18 @@ ticks, coding agent sessions, and operator CLI use.
| Coding agent | `export WARDEN_AGENT_ID=claude` (or `codex`, `grok`, future ids) |
| Operator CLI | default `warden.operator` when `WARDEN_AGENT_ID` is unset |
## Default Behavior
phase-memory is **on by default** (`WARDEN_MEMORY=1`). Every `warden` command
implicitly loads the canonical store before route/access/worker/sign work. You do
not need a separate activation command for normal use.
## Agent Session Orientation
At the start of a Claude Code, Codex, or Grok session that will call warden:
For Claude Code, Codex, Grok, or future agents, set runtime identity once:
```bash
export WARDEN_AGENT_ID=grok # or claude, codex
warden memory activate --json
```
Then use normal `warden route` / `warden access` commands. Episodes are recorded