generated from coulomb/repo-seed
Implement WARDEN-WP-0024 experiential memory and agent sessions.
Add phase-memory bridge, warden memory CLI, route/access/sign recording, memory-aware worker planning with OpenRouter skip, tests, wiki, and AGENTS.md orientation for Claude, Codex, Grok, and future agent sessions.
This commit is contained in:
23
AGENTS.md
23
AGENTS.md
@@ -156,6 +156,29 @@ get wrong.
|
||||
<!-- Append repo-specific agent instructions below this marker.
|
||||
The state-hub template sync preserves content after this line. -->
|
||||
|
||||
## Experiential memory (WARDEN-WP-0024)
|
||||
|
||||
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):
|
||||
|
||||
```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`.
|
||||
|
||||
**Store:** `~/.local/share/warden/memory/` (override: `WARDEN_MEMORY_STORE`).
|
||||
|
||||
**Worker:** `warden worker run --brain llm` skips OpenRouter when stabilized
|
||||
routing memory matches. See `wiki/OpsWardenMemory.md`.
|
||||
|
||||
Requires `phase-memory` on `PYTHONPATH` or installed; contract in
|
||||
`phase-memory/docs/ops-warden-memory-contract.md`.
|
||||
|
||||
---
|
||||
|
||||
## Workplan Convention (ADR-001)
|
||||
|
||||
@@ -13,6 +13,9 @@ dependencies = [
|
||||
"httpx>=0.27",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
memory = []
|
||||
|
||||
[project.scripts]
|
||||
warden = "warden.cli:app"
|
||||
ops-ssh-wrapper = "warden.scripts.ops_ssh_wrapper:main"
|
||||
@@ -29,7 +32,7 @@ packages = ["src/warden"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
pythonpath = ["src"]
|
||||
pythonpath = ["src", "../phase-memory/src"]
|
||||
addopts = "-m 'not integration'"
|
||||
markers = ["integration: requires ssh-keygen binary; run with pytest -m integration"]
|
||||
|
||||
|
||||
@@ -44,6 +44,11 @@ activity_app = typer.Typer(
|
||||
no_args_is_help=True,
|
||||
)
|
||||
app.add_typer(activity_app, name="activity")
|
||||
memory_app = typer.Typer(
|
||||
help="Cross-runtime experiential memory via phase-memory (WARDEN-WP-0024)",
|
||||
no_args_is_help=True,
|
||||
)
|
||||
app.add_typer(memory_app, name="memory")
|
||||
|
||||
console = Console()
|
||||
err = Console(stderr=True)
|
||||
@@ -53,6 +58,30 @@ err = Console(stderr=True)
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _record_memory_episode(
|
||||
*,
|
||||
command: str,
|
||||
outcome: str,
|
||||
need: str = "",
|
||||
route_id: str = "",
|
||||
) -> None:
|
||||
try:
|
||||
from warden import memory as warden_memory
|
||||
except ImportError:
|
||||
return
|
||||
if not warden_memory.enabled() or not warden_memory.memory_available():
|
||||
return
|
||||
try:
|
||||
warden_memory.record_command_episode(
|
||||
command=command,
|
||||
outcome=outcome,
|
||||
need=need,
|
||||
route_id=route_id,
|
||||
)
|
||||
except RuntimeError:
|
||||
return
|
||||
|
||||
|
||||
def _load_cfg() -> WardenConfig:
|
||||
try:
|
||||
return load_config()
|
||||
@@ -128,6 +157,12 @@ def sign(
|
||||
|
||||
# cert_command interface: write cert text to stdout only
|
||||
print(record.cert_path.read_text().strip())
|
||||
_record_memory_episode(
|
||||
command="sign",
|
||||
outcome="resolved",
|
||||
need=f"ssh cert {actor_name}",
|
||||
route_id="ops-warden-ssh-cert",
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -753,14 +788,30 @@ def route_find(
|
||||
|
||||
if output_json:
|
||||
print(json.dumps([_entry_summary(e) for e in matches], indent=2))
|
||||
if matches:
|
||||
_record_memory_episode(
|
||||
command="route find",
|
||||
outcome="resolved",
|
||||
need=query,
|
||||
route_id=matches[0].id,
|
||||
)
|
||||
else:
|
||||
_record_memory_episode(command="route find", outcome="skipped", need=query)
|
||||
return
|
||||
|
||||
if not matches:
|
||||
_record_memory_episode(command="route find", outcome="skipped", need=query)
|
||||
console.print(
|
||||
f"No routing match for {query!r}. "
|
||||
"Try `warden route list --all` to browse all scenarios."
|
||||
)
|
||||
return
|
||||
_record_memory_episode(
|
||||
command="route find",
|
||||
outcome="resolved",
|
||||
need=query,
|
||||
route_id=matches[0].id,
|
||||
)
|
||||
_print_entry_table(matches, f"Matches for {query!r}")
|
||||
|
||||
|
||||
@@ -1003,8 +1054,20 @@ def access(
|
||||
|
||||
if output_json:
|
||||
print(json.dumps(_access_json(entry, expanded, gate, domain), indent=2))
|
||||
_record_memory_episode(
|
||||
command="access",
|
||||
outcome="resolved",
|
||||
need=need,
|
||||
route_id=entry.id,
|
||||
)
|
||||
return
|
||||
|
||||
_record_memory_episode(
|
||||
command="access",
|
||||
outcome="resolved",
|
||||
need=need,
|
||||
route_id=entry.id,
|
||||
)
|
||||
console.print(f"[bold]{entry.title}[/bold] ([cyan]{entry.id}[/cyan])")
|
||||
console.print(f" owner : {entry.owner_repo} ({entry.subsystem})")
|
||||
|
||||
@@ -1305,3 +1368,58 @@ def worker_status_cmd() -> None:
|
||||
console.print(f"timer : {st or 'unknown'}")
|
||||
except Exception: # noqa: BLE001 — systemd may be absent (cron/other host)
|
||||
console.print("timer : (systemd not available)")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# warden memory — cross-runtime experiential memory (WARDEN-WP-0024)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@memory_app.command("status")
|
||||
def memory_status(
|
||||
output_json: Annotated[bool, typer.Option("--json", help="Output JSON")] = False,
|
||||
) -> None:
|
||||
"""Show canonical phase-memory store status (metadata only)."""
|
||||
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.status()
|
||||
except RuntimeError as e:
|
||||
err.print(f"[red]{e}[/red]")
|
||||
raise typer.Exit(2)
|
||||
if output_json:
|
||||
print(json.dumps(payload, indent=2))
|
||||
return
|
||||
console.print(f"store_path : {payload.get('store_path', '')}")
|
||||
console.print(f"profile_id : {payload.get('profile_id', '')}")
|
||||
console.print(f"episode_count : {payload.get('episode_count', 0)}")
|
||||
console.print(f"session_kinds : {payload.get('episode_counts_by_session_kind', {})}")
|
||||
console.print(f"last_activation: {payload.get('last_activation_at') or '—'}")
|
||||
|
||||
|
||||
@memory_app.command("activate")
|
||||
def memory_activate(
|
||||
need: Annotated[str, typer.Option("--need", help="Optional routing need fingerprint source")] = "",
|
||||
agent: Annotated[
|
||||
Optional[str],
|
||||
typer.Option("--agent", help="Agent id for session_kind warden.agent.<id> (claude, codex, grok, …)"),
|
||||
] = None,
|
||||
output_json: Annotated[bool, typer.Option("--json", help="Output JSON")] = False,
|
||||
) -> None:
|
||||
"""Activate bounded coordination memory for worker, operator, or agent sessions."""
|
||||
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)
|
||||
except RuntimeError as e:
|
||||
err.print(f"[red]{e}[/red]")
|
||||
raise typer.Exit(2)
|
||||
if output_json:
|
||||
print(json.dumps(payload, indent=2))
|
||||
return
|
||||
console.print(warden_memory.format_activation_summary(payload))
|
||||
|
||||
122
src/warden/memory.py
Normal file
122
src/warden/memory.py
Normal file
@@ -0,0 +1,122 @@
|
||||
"""phase-memory bridge for ops-warden cross-runtime experiential memory."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Any, Mapping, Optional
|
||||
|
||||
_PHASE_MEMORY_ERROR = (
|
||||
"phase-memory is required for warden memory commands. "
|
||||
"Install with: pip install phase-memory (or set PYTHONPATH to phase-memory/src)."
|
||||
)
|
||||
|
||||
|
||||
def _phase_memory():
|
||||
try:
|
||||
import phase_memory.ops_warden as pm
|
||||
|
||||
return pm
|
||||
except ImportError as exc: # pragma: no cover - exercised via tests with PYTHONPATH
|
||||
raise RuntimeError(_PHASE_MEMORY_ERROR) from exc
|
||||
|
||||
|
||||
def memory_available() -> bool:
|
||||
try:
|
||||
_phase_memory()
|
||||
return True
|
||||
except RuntimeError:
|
||||
return False
|
||||
|
||||
|
||||
def enabled(environ: Mapping[str, str] | None = None) -> bool:
|
||||
environ = environ or os.environ
|
||||
return str(environ.get("WARDEN_MEMORY", "1")).strip().lower() not in {"0", "false", "no", "off"}
|
||||
|
||||
|
||||
def store_path(environ: Mapping[str, str] | None = None):
|
||||
return _phase_memory().default_memory_store_path(environ)
|
||||
|
||||
|
||||
def session_kind(environ: Mapping[str, str] | None = None) -> str:
|
||||
return _phase_memory().resolve_session_kind(environ)
|
||||
|
||||
|
||||
def status(environ: Mapping[str, str] | None = None) -> dict[str, Any]:
|
||||
pm = _phase_memory()
|
||||
return pm.OpsWardenMemoryStore.open(environ=environ).status()
|
||||
|
||||
|
||||
def activate(
|
||||
*,
|
||||
need: str = "",
|
||||
agent: Optional[str] = None,
|
||||
session_id: str = "",
|
||||
environ: Mapping[str, str] | None = None,
|
||||
) -> 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(
|
||||
pm.OpsWardenMemoryStore.open(environ=env),
|
||||
session_kind=kind,
|
||||
need=need,
|
||||
session_id=session_id,
|
||||
)
|
||||
|
||||
|
||||
def record_command_episode(
|
||||
*,
|
||||
command: str,
|
||||
outcome: str,
|
||||
need: str = "",
|
||||
route_id: str = "",
|
||||
diagnostic_codes: Optional[list[str]] = None,
|
||||
metadata: Optional[dict[str, Any]] = None,
|
||||
environ: Mapping[str, str] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
if not enabled(environ):
|
||||
return {"valid": True, "skipped": True, "reason": "WARDEN_MEMORY=0"}
|
||||
pm = _phase_memory()
|
||||
env = dict(environ or os.environ)
|
||||
event = pm.build_session_event(
|
||||
command=command,
|
||||
session_kind=pm.resolve_session_kind(env),
|
||||
outcome=outcome,
|
||||
need=need,
|
||||
route_id=route_id,
|
||||
agent_id=str(env.get("WARDEN_AGENT_ID") or ""),
|
||||
session_id=str(env.get("WARDEN_SESSION_ID") or ""),
|
||||
diagnostic_codes=diagnostic_codes,
|
||||
metadata=metadata,
|
||||
)
|
||||
return pm.record_session_event(pm.OpsWardenMemoryStore.open(environ=env), event)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
def stabilized_route_for_need(need: str, environ: Mapping[str, str] | None = None) -> Optional[dict[str, Any]]:
|
||||
pm = _phase_memory()
|
||||
store = pm.OpsWardenMemoryStore.open(environ=environ)
|
||||
return pm.stabilized_route_match(store.list_events(), need=need)
|
||||
|
||||
|
||||
def format_activation_summary(activation: dict[str, Any]) -> str:
|
||||
lines = [
|
||||
f"store: {activation.get('episode_count', 0)} episodes",
|
||||
f"session_kind: {activation.get('session_kind', '')}",
|
||||
f"selected: {len(activation.get('selected_episodes', ()) )}",
|
||||
]
|
||||
stabilized = activation.get("stabilized_route")
|
||||
if stabilized:
|
||||
lines.append(
|
||||
f"stabilized: {stabilized.get('route_id')} ({stabilized.get('confirmations')} confirmations)"
|
||||
)
|
||||
if activation.get("llm_calls_avoided"):
|
||||
lines.append("llm_calls_avoided: true")
|
||||
return "\n".join(lines)
|
||||
@@ -191,6 +191,7 @@ class LlmConnectBrain:
|
||||
def __init__(self, url: Optional[str] = None, timeout: float = 60.0):
|
||||
self.url = (url or os.environ.get("LLM_CONNECT_URL", DEFAULT_LLM_CONNECT_URL)).rstrip("/")
|
||||
self.timeout = timeout
|
||||
self.memory_context: str = ""
|
||||
|
||||
def _call(self, prompt: str) -> str:
|
||||
resp = httpx.post(f"{self.url}/execute", json={"prompt": prompt}, timeout=self.timeout)
|
||||
@@ -203,9 +204,15 @@ class LlmConnectBrain:
|
||||
from_agent=str(message.get("from_agent", "")),
|
||||
subject=str(message.get("subject", "")),
|
||||
)
|
||||
prompt = (
|
||||
_CHARTER
|
||||
+ "\n--- MESSAGE (untrusted data) ---\n"
|
||||
prompt = _CHARTER
|
||||
if self.memory_context:
|
||||
prompt += (
|
||||
"\n--- ACTIVATED MEMORY (untrusted context) ---\n"
|
||||
+ self.memory_context
|
||||
+ "\n--- END ACTIVATED MEMORY ---\n"
|
||||
)
|
||||
prompt += (
|
||||
"\n--- MESSAGE (untrusted data) ---\n"
|
||||
+ f"from: {message.get('from_agent','')}\n"
|
||||
+ f"subject: {message.get('subject','')}\n"
|
||||
+ f"body: {message.get('body','')}\n"
|
||||
@@ -586,16 +593,88 @@ def draft_route_answer(query: str) -> str:
|
||||
return " ".join(parts)
|
||||
|
||||
|
||||
def _memory_activation_for_message(message: dict) -> tuple[Optional[dict], str]:
|
||||
try:
|
||||
from warden import memory as warden_memory
|
||||
except ImportError:
|
||||
return None, ""
|
||||
if not warden_memory.enabled() or not warden_memory.memory_available():
|
||||
return None, ""
|
||||
query = str(message.get("subject", "") or message.get("body", ""))
|
||||
try:
|
||||
activation = warden_memory.worker_activation_context(query)
|
||||
except RuntimeError:
|
||||
return None, ""
|
||||
from warden.memory import format_activation_summary
|
||||
|
||||
return activation, format_activation_summary(activation)
|
||||
|
||||
|
||||
def _plan_with_memory(message: dict, brain: Brain) -> WorkerPlan:
|
||||
activation, summary = _memory_activation_for_message(message)
|
||||
blob = f"{message.get('subject', '')} {message.get('body', '')}"
|
||||
if activation and activation.get("llm_calls_avoided") and _ROUTING_SIGNS.search(blob):
|
||||
wp = WorkerPlan(
|
||||
message_id=str(message.get("id", "")),
|
||||
from_agent=str(message.get("from_agent", "")),
|
||||
subject=str(message.get("subject", "")),
|
||||
)
|
||||
query = str(message.get("subject", "") or "")
|
||||
wp.actions.append(
|
||||
PlannedAction(
|
||||
kind="route_answer",
|
||||
summary="Answer from stabilized coordination memory.",
|
||||
payload={
|
||||
"query": query,
|
||||
"answer": draft_route_answer(query),
|
||||
"memory_stabilized": True,
|
||||
},
|
||||
)
|
||||
)
|
||||
return wp
|
||||
if isinstance(brain, LlmConnectBrain) and summary:
|
||||
brain.memory_context = summary
|
||||
return brain.plan(message)
|
||||
|
||||
|
||||
def _record_worker_memory_outcome(plan: WorkerPlan) -> None:
|
||||
try:
|
||||
from warden import memory as warden_memory
|
||||
except ImportError:
|
||||
return
|
||||
if not warden_memory.enabled() or not warden_memory.memory_available():
|
||||
return
|
||||
outcome = "escalated" if plan.escalated else "resolved"
|
||||
route_id = ""
|
||||
for action in plan.actions:
|
||||
if action.kind == "route_answer" and action.payload.get("memory_stabilized"):
|
||||
stabilized = warden_memory.stabilized_route_for_need(plan.subject)
|
||||
if stabilized:
|
||||
route_id = str(stabilized.get("route_id") or "")
|
||||
try:
|
||||
warden_memory.record_command_episode(
|
||||
command="worker run",
|
||||
outcome=outcome,
|
||||
need=plan.subject,
|
||||
route_id=route_id,
|
||||
diagnostic_codes=["worker_escalated"] if plan.escalated else [],
|
||||
metadata={"message_id": plan.message_id, "action_kinds": [a.kind for a in plan.actions]},
|
||||
)
|
||||
except RuntimeError:
|
||||
return
|
||||
|
||||
|
||||
def build_plans(messages: List[dict], brain: Brain) -> List[WorkerPlan]:
|
||||
"""Plan every message, attach computed route answers, and apply the guardrail pass."""
|
||||
plans: List[WorkerPlan] = []
|
||||
for m in messages:
|
||||
plan = brain.plan(m)
|
||||
plan = _plan_with_memory(m, brain)
|
||||
plan.raw = m
|
||||
for a in plan.actions:
|
||||
if a.kind == "route_answer" and "answer" not in a.payload:
|
||||
a.payload["answer"] = draft_route_answer(a.payload.get("query", m.get("subject", "")))
|
||||
plans.append(_guardrail(plan, m))
|
||||
_record_worker_memory_outcome(plans[-1])
|
||||
return plans
|
||||
|
||||
|
||||
|
||||
11
tests/conftest.py
Normal file
11
tests/conftest.py
Normal file
@@ -0,0 +1,11 @@
|
||||
"""Test configuration for ops-warden."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
PHASE_MEMORY_SRC = Path(__file__).resolve().parents[2] / "phase-memory" / "src"
|
||||
if PHASE_MEMORY_SRC.exists() and str(PHASE_MEMORY_SRC) not in sys.path:
|
||||
sys.path.insert(0, str(PHASE_MEMORY_SRC))
|
||||
129
tests/test_memory.py
Normal file
129
tests/test_memory.py
Normal file
@@ -0,0 +1,129 @@
|
||||
"""Tests for ops-warden phase-memory bridge (WARDEN-WP-0024)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from warden.cli import app
|
||||
from warden.memory import activate, enabled, record_command_episode, status, store_path
|
||||
from warden.worker import RuleBrain, _plan_with_memory, build_plans
|
||||
|
||||
runner = CliRunner()
|
||||
|
||||
|
||||
def _msg(**over) -> dict:
|
||||
base = {
|
||||
"id": "m1",
|
||||
"from_agent": "someone",
|
||||
"subject": "Where do I get an npm token?",
|
||||
"body": "Which subsystem owns this credential — how do I obtain it?",
|
||||
}
|
||||
base.update(over)
|
||||
return base
|
||||
|
||||
|
||||
def test_memory_status_and_activate_round_trip(tmp_path, monkeypatch) -> None:
|
||||
monkeypatch.setenv("WARDEN_MEMORY_STORE", str(tmp_path / "memory"))
|
||||
monkeypatch.setenv("WARDEN_AGENT_ID", "grok")
|
||||
|
||||
first = record_command_episode(
|
||||
command="route find",
|
||||
outcome="resolved",
|
||||
need="npm token",
|
||||
route_id="openbao-api-key",
|
||||
)
|
||||
second = record_command_episode(
|
||||
command="route find",
|
||||
outcome="resolved",
|
||||
need="npm token",
|
||||
route_id="openbao-api-key",
|
||||
)
|
||||
payload = activate(need="npm token", agent="grok")
|
||||
|
||||
assert first["valid"] is True
|
||||
assert second["valid"] is True
|
||||
assert payload["session_kind"] == "warden.agent.grok"
|
||||
assert payload["stabilized_route"]["route_id"] == "openbao-api-key"
|
||||
assert status()["episode_count"] >= 3
|
||||
|
||||
|
||||
def test_worker_uses_stabilized_memory_without_llm(tmp_path, monkeypatch) -> None:
|
||||
monkeypatch.setenv("WARDEN_MEMORY_STORE", str(tmp_path / "memory"))
|
||||
monkeypatch.setenv("WARDEN_SESSION_KIND", "warden.worker")
|
||||
record_command_episode(
|
||||
command="route find",
|
||||
outcome="resolved",
|
||||
need="Where do I get an npm token?",
|
||||
route_id="openbao-api-key",
|
||||
)
|
||||
record_command_episode(
|
||||
command="route find",
|
||||
outcome="resolved",
|
||||
need="Where do I get an npm token?",
|
||||
route_id="openbao-api-key",
|
||||
)
|
||||
|
||||
plan = _plan_with_memory(_msg(), RuleBrain())
|
||||
|
||||
assert plan.actions
|
||||
assert plan.actions[0].kind == "route_answer"
|
||||
assert plan.actions[0].payload.get("memory_stabilized") is True
|
||||
|
||||
|
||||
def test_cross_runtime_continuity_agent_to_worker(tmp_path, monkeypatch) -> None:
|
||||
monkeypatch.setenv("WARDEN_MEMORY_STORE", str(tmp_path / "memory"))
|
||||
monkeypatch.setenv("WARDEN_AGENT_ID", "codex")
|
||||
record_command_episode(
|
||||
command="route find",
|
||||
outcome="resolved",
|
||||
need="openrouter api key",
|
||||
route_id="openrouter-llm-connect",
|
||||
)
|
||||
monkeypatch.setenv("WARDEN_SESSION_KIND", "warden.worker")
|
||||
monkeypatch.delenv("WARDEN_AGENT_ID", raising=False)
|
||||
activation = activate(need="openrouter api key")
|
||||
kinds = {item.get("session_kind") for item in activation["selected_episodes"] if item.get("kind") == "episode"}
|
||||
|
||||
assert "warden.agent.codex" in kinds
|
||||
|
||||
|
||||
def test_cli_memory_status_and_activate(tmp_path, monkeypatch) -> None:
|
||||
monkeypatch.setenv("WARDEN_MEMORY_STORE", str(tmp_path / "memory"))
|
||||
status_result = runner.invoke(app, ["memory", "status", "--json"])
|
||||
activate_result = runner.invoke(app, ["memory", "activate", "--agent", "claude", "--json"])
|
||||
|
||||
assert status_result.exit_code == 0
|
||||
assert activate_result.exit_code == 0
|
||||
assert json.loads(status_result.stdout)["episode_count"] >= 0
|
||||
assert json.loads(activate_result.stdout)["session_kind"] == "warden.agent.claude"
|
||||
|
||||
|
||||
def test_route_find_records_memory_episode(tmp_path, monkeypatch) -> None:
|
||||
monkeypatch.setenv("WARDEN_MEMORY_STORE", str(tmp_path / "memory"))
|
||||
result = runner.invoke(app, ["route", "find", "openrouter api key", "--json"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
payload = status()
|
||||
assert payload["episode_count"] >= 1
|
||||
|
||||
|
||||
def test_build_plans_records_worker_outcomes(tmp_path, monkeypatch) -> None:
|
||||
monkeypatch.setenv("WARDEN_MEMORY_STORE", str(tmp_path / "memory"))
|
||||
plans = build_plans([_msg()], RuleBrain())
|
||||
|
||||
assert plans[0].actions
|
||||
assert status()["episode_count"] >= 1
|
||||
|
||||
|
||||
def test_memory_disabled_skips_recording(monkeypatch) -> None:
|
||||
monkeypatch.setenv("WARDEN_MEMORY", "0")
|
||||
result = record_command_episode(command="route find", outcome="resolved", need="npm token")
|
||||
|
||||
assert result.get("skipped") is True
|
||||
|
||||
|
||||
def test_default_store_path_uses_xdg(monkeypatch) -> None:
|
||||
monkeypatch.delenv("WARDEN_MEMORY_STORE", raising=False)
|
||||
assert str(store_path()).endswith("warden/memory")
|
||||
52
wiki/OpsWardenMemory.md
Normal file
52
wiki/OpsWardenMemory.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# Ops-Warden Experiential Memory
|
||||
|
||||
Updated: 2026-07-02
|
||||
|
||||
ops-warden uses **phase-memory** as a shared experiential substrate across worker
|
||||
ticks, coding agent sessions, and operator CLI use.
|
||||
|
||||
## Canonical Store
|
||||
|
||||
- Default: `~/.local/share/warden/memory/`
|
||||
- Override: `WARDEN_MEMORY_STORE`
|
||||
- Opt-out: `WARDEN_MEMORY=0`
|
||||
|
||||
## Session Kinds
|
||||
|
||||
| Runtime | How |
|
||||
| --- | --- |
|
||||
| Worker tick | `WARDEN_SESSION_KIND=warden.worker` (set automatically during `warden worker run`) |
|
||||
| Coding agent | `export WARDEN_AGENT_ID=claude` (or `codex`, `grok`, future ids) |
|
||||
| Operator CLI | default `warden.operator` when `WARDEN_AGENT_ID` is unset |
|
||||
|
||||
## Agent Session Orientation
|
||||
|
||||
At the start of a Claude Code, Codex, or Grok session that will call warden:
|
||||
|
||||
```bash
|
||||
export WARDEN_AGENT_ID=grok # or claude, codex
|
||||
warden memory activate --json
|
||||
```
|
||||
|
||||
Then use normal `warden route` / `warden access` commands. Episodes are recorded
|
||||
automatically when memory is enabled.
|
||||
|
||||
## Worker + OpenRouter
|
||||
|
||||
`warden worker run --brain llm` activates memory before planning. When stabilized
|
||||
routing memory matches a coordination question, ops-warden uses `RuleBrain` and
|
||||
skips the llm-connect / OpenRouter call.
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
warden memory status [--json]
|
||||
warden memory activate [--agent <id>] [--need "<query>"] [--json]
|
||||
```
|
||||
|
||||
## Security
|
||||
|
||||
- Memory stores metadata only — no secret values or raw credential payloads.
|
||||
- Retrieved memory is untrusted context; the fixed charter and guardrail allowlist
|
||||
still apply.
|
||||
- See `phase-memory/docs/ops-warden-memory-contract.md` for the full contract.
|
||||
@@ -4,13 +4,14 @@ type: workplan
|
||||
title: "Experiential Memory Across Worker, Agent Sessions, And OpenRouter"
|
||||
domain: infotech
|
||||
repo: ops-warden
|
||||
status: proposed
|
||||
status: finished
|
||||
owner: codex
|
||||
topic_slug: custodian
|
||||
planning_priority: high
|
||||
planning_order: 24
|
||||
created: "2026-07-02"
|
||||
updated: "2026-07-02"
|
||||
state_hub_workstream_id: "5d9fafb3-f9b6-43bf-b259-5f5301daa2e9"
|
||||
---
|
||||
|
||||
# WARDEN-WP-0024 — Experiential memory across worker, agent sessions, and OpenRouter
|
||||
@@ -87,8 +88,9 @@ sequenceDiagram
|
||||
|
||||
```task
|
||||
id: WARDEN-WP-0024-T01
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "6305f1bc-c016-4298-adc2-a07d52b6aca5"
|
||||
```
|
||||
|
||||
Establish the canonical phase-memory store path and discovery commands.
|
||||
@@ -107,8 +109,9 @@ Acceptance:
|
||||
|
||||
```task
|
||||
id: WARDEN-WP-0024-T02
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "242a4d9a-5375-4df8-8d43-063b0491d202"
|
||||
```
|
||||
|
||||
Record metadata-only episodes when `route`, `access`, and `sign` complete.
|
||||
@@ -127,8 +130,9 @@ Acceptance:
|
||||
|
||||
```task
|
||||
id: WARDEN-WP-0024-T03
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "176fcae1-e4e5-481f-9a83-e9a7000fac1a"
|
||||
```
|
||||
|
||||
Retrieve coordination memory before `Brain.plan` and record outcomes after execute.
|
||||
@@ -147,8 +151,9 @@ Acceptance:
|
||||
|
||||
```task
|
||||
id: WARDEN-WP-0024-T04
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "16501557-6cde-44ea-bc6f-1726cb7ec070"
|
||||
```
|
||||
|
||||
Expose memory activation for coding agent sessions across Claude, Codex, Grok, and
|
||||
@@ -167,8 +172,9 @@ Acceptance:
|
||||
|
||||
```task
|
||||
id: WARDEN-WP-0024-T05
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "55ae679c-c08f-4afd-8646-9f5f3019f86e"
|
||||
```
|
||||
|
||||
Ensure worker ticks see agent session episodes and vice versa.
|
||||
@@ -187,8 +193,9 @@ Acceptance:
|
||||
|
||||
```task
|
||||
id: WARDEN-WP-0024-T06
|
||||
status: todo
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "fc2dffcf-7184-4f3d-8653-d26dc18a9afc"
|
||||
```
|
||||
|
||||
Skip or downgrade OpenRouter calls when stabilized memory provides a verified match.
|
||||
@@ -206,8 +213,9 @@ Acceptance:
|
||||
|
||||
```task
|
||||
id: WARDEN-WP-0024-T07
|
||||
status: todo
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "ca2aaf23-833f-49a6-a49b-a0b659208f5f"
|
||||
```
|
||||
|
||||
Document the memory workflow for operators and coding agents.
|
||||
@@ -249,4 +257,13 @@ Acceptance:
|
||||
|
||||
## Closure Review
|
||||
|
||||
Pending implementation.
|
||||
Integrated phase-memory across all ops-warden runtime surfaces:
|
||||
|
||||
- `warden memory status` and `warden memory activate` with canonical XDG store.
|
||||
- CLI recording hooks on `route find`, `access`, and `sign` (opt-out via
|
||||
`WARDEN_MEMORY=0`).
|
||||
- Memory-aware worker planning with stabilized-route RuleBrain short-circuit
|
||||
and LlmConnectBrain context injection.
|
||||
- Cross-runtime continuity verified in tests (agent session → worker activation).
|
||||
- `wiki/OpsWardenMemory.md` and AGENTS.md session orientation for Claude,
|
||||
Codex, Grok, and future agents.
|
||||
Reference in New Issue
Block a user