From 589f225a8f6a304292ef101fd1f5c446b168ac7b Mon Sep 17 00:00:00 2001 From: wolverinaton Date: Thu, 13 Aug 2026 21:10:26 -0300 Subject: [PATCH] feat: add release-first operational review --- CHANGELOG.md | 22 +- DOCS-MAP.md | 7 +- README.md | 504 +++++++++--------- docs/generated/release-truth.json | 4 +- docs/generated/release-truth.md | 4 +- memorymaster/operations/__init__.py | 1 + memorymaster/operations/operational_review.py | 305 +++++++++++ pyproject.toml | 2 +- .../install-windows-operational-review.ps1 | 52 ++ scripts/run_v47_operational_acceptance.py | 2 +- scripts/windows-operational-review.ps1 | 60 +++ tests/test_operational_review.py | 106 ++++ 12 files changed, 799 insertions(+), 270 deletions(-) create mode 100644 memorymaster/operations/__init__.py create mode 100644 memorymaster/operations/operational_review.py create mode 100644 scripts/install-windows-operational-review.ps1 create mode 100644 scripts/windows-operational-review.ps1 create mode 100644 tests/test_operational_review.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 54dcef0a..e6b62178 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ - + Covers user-visible changes, migrations, governance boundaries, and operational fixes for each release. Key terms: graph observations, compiled profile, governed capture, recall, sensitivity, lineage. Read when upgrading MemoryMaster, preparing release notes, or checking migration and rollback impact. @@ -8,6 +8,26 @@ Read when upgrading MemoryMaster, preparing release notes, or checking migration ## [Unreleased] +## [4.7.2] - 2026-08-13 + +### Changed + +- Rebuilt the README around the shipped 4.7 product: governed graph + observations, the evidence-bound compiled profile, Gemini plus GLM provider + activation, safe lifecycle semantics, and the real restart boundary. +- Release and activation no longer depend on a 24-hour calendar window. That + window is documented only as optional post-release operational evidence. + +### Added + +- A read-only operational reviewer and bounded Windows scheduled-task installer. + The default six-hour review records package/database integrity, observation + queues, compiled-profile support, recent private-context residue, and a + natural-language recall canary with distinct PASS/WARN/FAIL exits. +- Review artifacts explicitly record that work occurred and that database + mutations remained zero; scheduler transport never creates a feature-success + receipt or watermark. + ## [4.7.1] - 2026-08-13 ### Fixed diff --git a/DOCS-MAP.md b/DOCS-MAP.md index 6f292802..02b3e3b4 100644 --- a/DOCS-MAP.md +++ b/DOCS-MAP.md @@ -1,15 +1,16 @@ - + # DOCS-MAP - memorymaster # Covers: trust verdicts and replacements for every canonical documentation surface. # Key terms: CURRENT, SUPERSEDED, compiled user profile, roadmap, ADR. # Read when: locating authoritative project documentation before reading doc bodies. -# Updated: 2026-08-12 for the evidence-bound compiled user-profile implementation. +# Updated: 2026-08-13 for shipped graph observations, compiled profile, and scheduled review. # Rule: generated profiles are disposable projections; MemoryMaster remains authority. | File | Verdict | Last change | Reason | |---|---|---|---| -| CHANGELOG.md | CURRENT | 2026-08-04 | Public release history; v4.6.0 records governed universal capture, measured quality changes, security evidence, and known follow-ups. | +| CHANGELOG.md | CURRENT | 2026-08-13 | Public release history through v4.7.2, including observations, compiled profile, retrieval/privacy fixes, and scheduled operational review. | +| README.md | CURRENT | 2026-08-13 | Primary product overview and setup for governed claims, PPR-7 observations, compiled profile, Gemini plus GLM, and release-first scheduled review. | | ROADMAP.md | CURRENT | 2026-08-12 | Sole authoritative roadmap; P5 is merged and locally deployed, while isolated PPR-7 implementation is active. | | .planning/GRAPH-OBSERVATIONS-V1.md | CURRENT | 2026-08-12 | Bounded PPR-7 implementation ledger for deterministic supported components, governed synthesis, lifecycle, opt-in recall, and verification. | | .planning/COMPILED-USER-PROFILE-V1.md | CURRENT | 2026-08-12 | Implementation contract for the zero-curation, evidence-bound user profile projection and weekly GLM map/reduce job. | diff --git a/README.md b/README.md index b1147d1f..5d0ab932 100644 --- a/README.md +++ b/README.md @@ -1,221 +1,232 @@ + # MemoryMaster -# Covers: personal-first governed memory capture, recall, retirement, and improvement. -# Key terms: remember, recall, forget, improve, evidence lineage, trusted claims. -# Read when: installing MemoryMaster, evaluating its posture, or finding deeper docs. -# Default: local SQLite and private MCP; team/cloud operation is deferred. -# Safety: captures become candidates; only the steward confirms claims. -# Updated: 2026-07-30 after retiring a named legacy consumer integration. - -[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) -[![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/) -[![Tests](https://img.shields.io/badge/tests-3200%2B-green.svg)]() -[![Release truth](https://img.shields.io/badge/release%20truth-generated-purple.svg)](docs/generated/release-truth.md) -[![CLI Commands](https://img.shields.io/badge/CLI%20commands-106-orange.svg)]() +# Covers: installation, governed capture and recall, graph observations, compiled profile, scheduling, and safety. +# Key terms: claims, citations, steward, graph observations, compiled profile, Gemini, GLM, SQLite, MCP. +# Read when: evaluating, installing, operating, or upgrading MemoryMaster. +# Default: private local SQLite; observations and generated views never bypass claim governance. + + +[![CI](https://github.com/wolverin0/memorymaster/actions/workflows/ci.yml/badge.svg)](https://github.com/wolverin0/memorymaster/actions/workflows/ci.yml) [![PyPI](https://img.shields.io/pypi/v/memorymaster.svg)](https://pypi.org/project/memorymaster/) +[![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/) +[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) -MemoryMaster prevents the #1 problem with agent memory: **drift, stale assumptions, and unsafe disclosure**. It gives Claude Code, Codex, and any MCP-compatible agent persistent, verifiable memory with a full claim lifecycle, citation tracking, conflict detection, and human-in-the-loop governance. +MemoryMaster is persistent memory for coding agents where every durable fact is +a governed claim—not an opaque chunk that silently lives forever. -> **Product posture:** MemoryMaster is primarily a personal/local application. -> The default profile uses one SQLite database and a private stdio MCP process. -> Postgres/team operation is a deferred optional capability, and Qdrant is an -> optional semantic index—not a dependency or source of truth. +It captures evidence, extracts candidate claims, preserves citations, detects +conflicts, promotes trustworthy claims through a steward, and retires facts when +their support stops being current. Claude Code, Codex, Gemini-powered workers, +Hermes, and any MCP client can share the same local memory without surrendering +authority to a vector database or generated summary. -### How it's different +```text +evidence -> candidate claim -> steward -> confirmed claim -> governed recall + \-> supported graph + \-> candidate observation + \-> steward -> opt-in recall +``` -Agent-memory systems (mem0, Letta/MemGPT, Zep, cognee) have largely converged on strong **retrieval** — hybrid search, temporal and graph reasoning, ontologies. MemoryMaster competes on a different axis: **governance**. The wedge is **curation over accumulation** — every memory is a lifecycle-managed *claim* (status, citations, decay, conflict arbitration), not an opaque embedding that lingers until overwritten. Tellingly, the market moved the other way: mem0's 2026 model is explicitly *add-only with no conflict resolution* — the opposite of a steward. +## What shipped in 4.7 -| | mem0 / Letta / Zep | **MemoryMaster** | +| Capability | What it does | Default | |---|---|---| -| Unit of memory | text chunk / summary | **claim** with status, tier, citations, bitemporal validity | -| Stale / wrong facts | linger until overwritten | **decay → `stale`**, **conflict detection**, **supersession** | -| Contradictions | silently coexist | surfaced as **`conflicted`**, auto-resolved (5-tier) or queued for review | -| Provenance | usually none | **citation per claim** + per-agent provenance | -| Secret leakage | your problem | **sensitivity filter at ingest** (JWT/AWS/Bearer/SSH redaction) | -| Operator control | API only | **steward governance** + a **dashboard** you can *see* | - -If you want an agent that recalls more, any vector store works. If you want an agent that recalls *correctly* — and can prove where a fact came from and retire it when it goes stale — that's the gap MemoryMaster fills. - ---- - -## Architecture - -MemoryMaster is layered around MCP/CLI entry points, the `MemoryService` facade, authoritative -SQLite storage, an optional deferred Postgres/team backend, an optional Qdrant index, scheduled jobs, and an **optional** Obsidian wiki/vault -layer (opt-in, off by default — see below). The canonical capture path is: - -```text -producer -> source item -> evidence -> candidate claim -> steward -> confirmed claim -> supported graph +| Governed graph observations | Derives supported dependencies, constraints, recurring patterns and root causes from confirmed evidence | Generation enabled explicitly; recall opt-in | +| Compiled user profile | Builds a disposable, cited projection from multiple independent sessions | Explicitly enabled; never an instruction source | +| Fast conversational recall | Finds relevant claims on the local lexical path without an embedding, Qdrant or provider call | On | +| Private-context intake guard | Redacts RFC1918 topology and absolute Windows/UNC paths from durable claim fields while preserving useful prose | On | +| Governed skills | Stores reviewed reusable procedures separately from ordinary facts | Recall opt-in | +| Hermes integration | Provides exact session/project scoping, local HTTP/stdio compatibility and replay-safe outbox behavior | Optional integration | +| Operational review | Performs a read-only six-hour integrity, queue, profile, intake and retrieval review | Optional Windows task | + +MemoryMaster 4.7 uses the configured **Gemini extraction + GLM consolidation** +path for this installation. OpenAI and Anthropic remain optional provider +adapters; neither is required for graph-observation discovery or ordinary recall. + +## Why MemoryMaster exists + +Typical memory stacks optimize retrieval while leaving correctness and retirement +to the caller. MemoryMaster makes those properties explicit: + +- Every claim has lifecycle state, scope, provenance, confidence and validity. +- Trusted recall returns confirmed, authorized, non-sensitive claims only. +- Contradictions become visible conflicts instead of silently coexisting. +- Citations and support tables make derived output traceable back to evidence. +- `forget` previews logical retirement; it never implies an unsafe hard delete. +- SQLite is authoritative. Qdrant is optional and may only propose IDs that are + rehydrated and re-authorized from SQLite. +- Generated observations, profiles, skills and wiki pages never recursively + reinforce the claims that produced them. + +## Quick start + +Install the private local MCP profile: + +```powershell +python -m pip install "memorymaster[mcp,capture,security]" +memorymaster-setup --yes --profile minimal --no-full-stack --json ``` -The query path is: +Restart the agent session once so its long-lived MCP process loads the installed +package, then verify: ```text -query_memory -> MemoryService.query -> authorized SQLite rows -> lexical/local-hybrid ranking -> context -``` - -Qdrant candidate retrieval is disabled by default. When a user deliberately -enables the governed semantic profile, Qdrant may return only candidate IDs, -content hashes, and scores. Every candidate is rehydrated from the authoritative -store and rechecked for lifecycle, tenant, scope, visibility, sensitivity, and -exact content hash. The semantic profile remains unproven for production until -the separately documented authenticated/TLS gate passes. - -See [docs/architecture.md](docs/architecture.md) for the current module map, data-flow details, -recent PR status, and sensitivity-filter invariants. - -## Key features - -- **6-state lifecycle**: `candidate` → `confirmed` → `stale` → `superseded` → `conflicted` → `archived` -- **Universal governed capture**: inline text, URL references, Markdown, HTML, - optional PDF/DOCX, and explicit OCR/transcription providers through one - replay-safe source/evidence/job contract -- **Friendly public facade**: synchronous `remember`, governed `recall`, - preview-first `forget`, and budgeted `improve` across Python, CLI, and MCP -- **Citation tracking** with provenance for every claim -- **Hybrid retrieval**: authoritative SQLite claim rows ranked with FTS5, local/primary-store embedding signals, freshness, and confidence; governed Qdrant candidates are explicit and optional -- **Context optimizer**: `query_for_context(budget=4000)` returns auto-curated memory that fits your token budget -- **Entity graph** with typed relationships and alias resolution -- **Rule-shaped claims** (new in v3.21.0): prescriptive `when , do because ` claims (`ingest_rule` / `query_rules`) — the shape an agent needs to actually change behaviour next time, not just recall a fact -- **Correction mining** (new in v3.21.0): `mine-rules` scans the verbatim transcript archive for user corrections and distills them into rule claims; the Stop hook also mines each session's latest correction automatically -- **Versioned schema migrations** (new in v3.20.0): `migrate` applies SQLite/Postgres migrations with sha256 drift detection; incremental `export-delta` ships small claim deltas for cheap cross-machine sync -- **Retrieval quality** (new in v3.22.0): floor-ratio boost gate (`MEMORYMASTER_BOOST_FLOOR_RATIO`) stops fresh-but-wrong claims outranking the true match; `query --explain` shows per-stage score attribution; an opt-in correctness-safe query cache (`MEMORYMASTER_QUERY_CACHE`) with a generation gate -- **Semantic contradiction probe** (new in v3.22.0, wired as a steward phase in v3.23.0): `detect-contradictions` finds claims that genuinely contradict each other (beyond the deterministic same-subject conflict check) via an LLM judge with a Wilson-CI rate and verdict cache; in v3.23 the same probe runs inside `run-steward` and emits paste-ready `conflicted` proposals -- **Verbatim archive cleanup** (new in v3.23.0): `verbatim-cleanup` dedups the raw-transcript table and optionally purges pre-#128 junk rows, with a dry-run default and FTS5 mirror sync -- **Steward governance**: multi-probe validators (filesystem, format, citation, semantic, tool) with proposal review -- **Conflict resolution**: 5-tier auto (confidence > freshness > citations > LLM > manual) -- **Auto-redaction** at ingest: JWT, GitHub tokens, Bearer, AWS keys, SSH keys, custom patterns -- **LLM Wiki** *(opt-in, off by default — set `MEMORYMASTER_WIKI_ABSORB=1`)*: compiled-truth + append-only timeline articles with progressive-disclosure frontmatter, `explored: true|false` operator-review marker, and inline `> [!contradiction]` Obsidian callouts. A redundant human-browsable **view** of the claims DB — the DB + recall is the memory system; the Markdown wiki does not scale past a few hundred pages (Karpathy LLM-Wiki pattern) so it stays off unless you want to browse it -- **Atlas Inbox V1** (new in v3.13.0): WhatsApp ingestion → source/evidence/action proposal lifecycle → Super-Productivity export. Versioned API/CLI contract for external capture producers — see [`docs/atlas-api-contract-v1.md`](docs/atlas-api-contract-v1.md). Real provider adapters (`OpenAIWhisperTranscriptionProvider`, `TesseractOcrProvider`) behind `Protocol`s; mock providers stay default. -- **Optional local-path resolution** (new in v4.1.0): `resolve-project` / `local-search` (CLI + MCP) turn a fuzzy project/file name into its real on-disk path. Most useful for agents **without** strong native file search (e.g. Codex on Windows) and for cross-session "where was project X?" — for clients that already have a good file-glob this is marginal. Backed by [Everything](https://www.voidtools.com/)'s read-only `ES.exe` CLI via a backend-agnostic `LocalSearchProvider` Protocol (`memorymaster/bridges/local_search/`; `plocate`/`fd`/`mdfind` can drop in later). Resolution is read-only by default; use `resolve-project --remember` (or MCP `remember=true`) to persist only matches at the calibrated `0.85` confidence threshold. Use `local-search --exact` for whole-name lookup. **Requires Everything + the ES CLI with `MEMORYMASTER_EVERYTHING_ES_PATH` set; degrades to a no-op when absent.** Setup mirrors an existing configured ES path into generated Claude/Codex MCP environments. Paths are redacted to root-relative tokens, and usage telemetry stores only tool/status/latency — never queries or paths. -- **LLM typed-entity Atlas extractor** (new in v4.1.0): turns ingested evidence (WhatsApp / email / notes) into *typed*, cited life-knowledge claims (`person`/`project`/`commitment`/`decision`/`event`/…) via an LLM with strict subject/type validation — replacing the deterministic keyword matcher. The `subject` is always the real named entity, never the source app. -- **Bitemporal write-time guard** (new in v4.2.0): rejects malformed ISO-8601 and inverted `valid_until < valid_from` at ingest, so a durable-but-invisible claim can never be written. -- **`archive_by_source` + `checkpoint`** (new in v4.2.0): lifecycle-safe bulk source cleanup (archive, never hard-delete; dry-run default) and one-round-trip batch ingest through the same sensitivity filter. -- **Intent-aware ranking** (new in v4.2.0, opt-in): `retrieval_profile="auto"` routes query intent to a weight profile; RRF fusion (`MEMORYMASTER_RECALL_FUSION`) is available but `linear` stays default after A/B measurement. -- **Usage telemetry** (new in v4.2.0): per-agent recall counters + a `get_usage_rollup` MCP tool. -- **Guarded fuzzy entity resolver** (new in v4.2.0, opt-in `MEMORYMASTER_ENTITY_FUZZY_RESOLVE`): refuses ambiguous alias matches (anti-hallucination) instead of fragmenting entities. -- **Hebbian/Ebbinghaus entity edges** (new in v4.2.0, opt-in `MEMORYMASTER_HEBBIAN_DECAY`): usage strengthens, time decays entity-graph edge weights. -- **Proactive + tool-triggered recall** (new in v4.2.0): a `volunteer_context` MCP tool (confidence-gated, zero-LLM) and an opt-in PreToolUse hook (`MEMORYMASTER_PRETOOLUSE_RECALL`) that injects memory as `additionalContext` on Grep/Glob. -- **Belief `holder`** (new in v4.2.0): nullable per-claim `holder` for multi-holder beliefs (take/fact/bet/hunch reuse `claim_type`); SQLite+Postgres, ranking-neutral by default. -- **SQLite-first backend**: SQLite for the primary personal/local product; PostgreSQL team support is retained but deferred until a real multi-user use case and its external evidence exist -- **Dream Bridge** for bidirectional sync with Claude Code's Auto Dream -- **Hook stack**: recall, classify, validate-wiki, session-start, auto-ingest, precompact (settings.json) + steward-cycle (cron/schtasks) + opt-in `--pretooluse` grep/glob recall-inject - -Full feature index lives in [`docs/handbook.md`](docs/handbook.md). - -## Benchmarks - -The current committed LongMemEval-S retrieval result covers all 500 questions: - -| Metric | Current reproducible result | -|---|---:| -| Recall@5 | 0.9660 | -| Recall@10 | 0.9840 | -| MRR | 0.9021 | - -Reproduce with `python tests/bench_longmemeval.py --retrieval-only`. There is -no comparable current full-QA result: an OAuth-backed judge must be run before -and after a release candidate before making a QA-regression claim. See the -[benchmark record](docs/archive/longmemeval-results.md). - -## Prerequisites - -**Required (the package won't function without these)** - -- Python **3.10+** with `pip` -- Claude Code, Codex, or any MCP-compatible agent -- **An LLM provider** — pick one: Claude Code OAuth (free if you're a subscriber, set `MEMORYMASTER_LLM_PROVIDER=claude_cli`), a free Gemini API key from [aistudio.google.com](https://aistudio.google.com), OpenAI, Anthropic API, or local Ollama. The steward and auto-ingest cycles need an LLM — without one, claims pile up as `candidate` and never get validated or deduped. (The opt-in `wiki-absorb` needs one too, if you enable it.) - -**Strongly recommended (you'll lose ~80% of the value without these)** - -- **Node.js 18+** for [graphify](https://github.com/wolverin0/graphify) and [GitNexus](https://github.com/wolverin0/gitnexus) — these are the cached intelligence layers that make MemoryMaster cheap to query. Without them, every "what does this codebase do?" question burns tokens cold-exploring files the graph already mapped. The `intelligence-first` workflow in `CLAUDE.md` assumes both are installed. -- **Obsidian 1.6+** with the [Bases](https://help.obsidian.md/Plugins/Bases) core plugin — **only if you opt into the Markdown wiki** (`MEMORYMASTER_WIKI_ABSORB=1`, off by default). The claims DB + recall is the memory system and needs no editor; if you turn `wiki-absorb` on, Obsidian's backlinks/graph/Bases are how you'd browse that (redundant) view. Not needed for normal use. - -**Optional (nice to have)** - -- **Docker** only if you deliberately want local Ollama or the optional governed Qdrant semantic profile. SQLite remains authoritative and requires neither Docker nor Qdrant. - -## Quickstart: remember, recall, forget, improve - -**1. Install** - -```bash -pip install "memorymaster[mcp,capture]" +query_memory("What decisions have we made about storage?") ``` -Try the complete disposable flow without touching your active database: +Try the complete lifecycle in a disposable database: -```bash +```powershell memorymaster --json demo ``` -Use the four public operations: +## Four public operations -```bash -memorymaster --workspace . remember --text "Project Atlas uses SQLite WAL." -memorymaster --workspace . recall "What does Project Atlas use?" +The stable Python, CLI and MCP interface is intentionally small: + +```powershell +memorymaster --workspace . remember --text "Atlas uses SQLite WAL." +memorymaster --workspace . recall "What does Atlas use?" memorymaster --workspace . forget --source-item-id 1 -memorymaster --workspace . improve +memorymaster --workspace . improve --scope project:atlas ``` -`forget` previews by default; add `--apply` only after reviewing its lifecycle -effects. `improve` queues extraction and review work but never confirms a claim -inside the request. - -The same contract is available in Python: - ```python from memorymaster import forget, improve, recall, remember -receipt = remember(text="Project Atlas uses SQLite WAL.", scope="project:atlas") +receipt = remember(text="Atlas uses SQLite WAL.", scope="project:atlas") context = recall("What does Atlas use?", scope_allowlist=["project:atlas"]) preview = forget(source_item_id=receipt.source_item["id"]) queued = improve(scope="project:atlas") ``` -For file capture, configure `MEMORYMASTER_CAPTURE_ROOTS` and use local-trusted -mode; URL-only capture stores a visible reference and waits for producer-supplied -evidence. See [the public v1 guide](docs/public-v1.md). +- `remember` stores source/evidence lineage and queues governed extraction. +- `recall` is confirmed-only in trusted mode. +- `forget` previews by default; `--apply` performs lifecycle retirement. +- `improve` queues bounded work. It does not confirm or rewrite claims inside + the request. + +See [Public v1](docs/public-v1.md) for receipts and full parameter contracts. + +## Graph observations (PPR-7) + +Graph observations answer questions that individual facts cannot, such as: + +- Which three blockers form one dependency chain? +- What recurring pattern is supported by several independent episodes? +- Which root cause is cited by multiple confirmed claims? +- Which observation became stale when its supporting evidence was retired? + +Component membership is deterministic. Exact canonical graph signatures and +union-find decide which evidence belongs together; an LLM may summarize an +eligible component but cannot choose membership, invent support, promote its own +output, or feed the result back into graph extraction. + +Recall remains explicit: + +```powershell +memorymaster --workspace . recall "What dependencies keep recurring?" --include-observations +``` -**2. Let your agent configure MCP and hooks** +```python +result = recall( + "What dependencies keep recurring?", + include_observations=True, + observation_limit=2, +) +``` -Paste the contents of [`docs/AGENT-INSTALL.md`](docs/AGENT-INSTALL.md) into Claude Code or Codex. The agent will: -- run `memorymaster-setup --yes --profile minimal --no-full-stack --json` (wires the SQLite database and private MCP without starting Postgres, Qdrant, or Ollama) -- report what was wired, what was reused (brownfield), and what degraded -- run `memorymaster-setup --verify-only` and show the round-trip result +Trusted mode revalidates support at read time and returns confirmed observations +only. Exploratory mode can label candidates or stale observations. Ordinary +recall remains unchanged while `include_observations` is off. -**3. Restart your session** +## Evidence-bound compiled profile -Hooks and MCP load on session start. Restart Claude Code / Codex once. +The compiled profile turns repeated, independently supported transcript facts +into a bounded session-start briefing. It is a disposable projection: -**4. Verify** +- SQLite transcript/support rows remain authoritative. +- New or replacement facts need at least two independent sessions. +- Unknown support IDs, sensitive output, instructions and malformed provider + output fail closed. +- The injected profile is context about the user—not permission and not an + instruction hierarchy. -After restart, run in your agent: +Enable both 4.7 features for new processes: +```powershell +[Environment]::SetEnvironmentVariable("MEMORYMASTER_GRAPH_OBSERVATIONS", "1", "User") +[Environment]::SetEnvironmentVariable("MEMORYMASTER_COMPILED_PROFILE", "1", "User") ``` -query_memory("test") + +Hooks are re-read on every event and update immediately. MCP servers and other +long-lived daemons must be restarted to load newly installed package code or new +environment variables. + +## Scheduled operation—release first, review afterward + +There is **no 24-hour implementation or release prerequisite**. A long observation +window is useful only as later evidence about queue health, cost and lifecycle. +It must never keep working code out of `main` merely because the clock has not +elapsed. + +On Windows, install the bounded read-only review task after installing the +release: + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File scripts/install-windows-operational-review.ps1 ` + -PythonExe "" ` + -Database "" ` + -ExpectedVersion "4.7.2" ` + -EveryHours 6 ``` -You should get a recall response from the MCP server. Done. +`MemoryMaster-Operational-Review` then checks: + +- installed version, schema, SQLite `quick_check` and foreign keys; +- observation backlog, blocked jobs and expired leases; +- compiled-profile runs, facts and exact support counts; +- recent claim fields for private topology or absolute-path residue; +- the configured natural-language retrieval canary. + +It writes `latest.json` and append-only execution history under the user's local +application-data directory. The review opens SQLite read-only, performs no claim +or job mutation, and never turns task transport into a false success receipt. +Exit codes are `0=PASS`, `1=FAIL`, and `3=WARN`. ---- +## Architecture and governance -For manual setup, advanced flags (`--provider`, `--db`, `--no-cron`, `--no-full-stack`, `--verify-only`, `--json`, and more), Docker, Helm, and Postgres, see [INSTALLATION.md](INSTALLATION.md). +```text +producer + -> source_items + -> evidence_items + -> candidate claims + -> steward proposals / lifecycle events + -> confirmed claims + -> FTS5 recall + supported entity graph + -> optional observations / skills / compiled profile +``` -## Pick your LLM provider +The default product is one private local SQLite database in WAL mode plus a +stdio MCP server. No new database, cloud service or vector server is required. +PostgreSQL team operation remains explicitly deferred; Qdrant remains an +optional semantic accelerator rather than a source of truth. -| Provider | Env vars | Default model | Cost | -|----------|----------|---------------|------| -| **Claude Code OAuth** (recommended for subscribers) | `MEMORYMASTER_LLM_PROVIDER=claude_cli` (requires `claude` CLI on PATH) | `claude-haiku-4-5-20251001` | included in Claude Code plan | -| Google Gemini (default) | `MEMORYMASTER_LLM_PROVIDER=google` + `GEMINI_API_KEY=...` | `gemini-3.1-flash-lite-preview` | ~free | -| OpenAI | `MEMORYMASTER_LLM_PROVIDER=openai` + `OPENAI_API_KEY=...` | `gpt-4o-mini` | ~$0.001/call | -| Anthropic API | `MEMORYMASTER_LLM_PROVIDER=anthropic` + `ANTHROPIC_API_KEY=...` | `claude-haiku-4-5-20251001` | ~$0.001/call | -| Ollama (local) | `MEMORYMASTER_LLM_PROVIDER=ollama` + `OLLAMA_URL=http://localhost:11434` | `llama3.2:3b` | free | +Important boundaries: -The `claude_cli` provider shells out to your local `claude --print` binary, so it inherits the OAuth session you're already logged into in Claude Code — no API key, no rotator, no quota juggling. **Caveat**: cold-start adds 3-15s per call (subprocess spawn), so it's ideal for batched/cron paths (steward, wiki-absorb) and not for latency-sensitive recall. Override with `MEMORYMASTER_CLAUDE_CLI_BIN` and `MEMORYMASTER_CLAUDE_CLI_TIMEOUT`. On VM installs the OAuth token expires ~24h, so pair with `MEMORYMASTER_LLM_FALLBACK_PROVIDER=ollama`; desktop tokens don't expire. +- Claim fields pass through the shared sensitivity filter on every ingest path. +- Raw user-selected source/evidence remains governed by the separate preservation + boundary described in [ADR-0006](docs/adr/0006-sensitivity-filter-boundary.md). +- Generated observations cannot support future observations. +- Trusted graph traversal requires active, authorized support in the same scope + and tenant. +- Schema changes use immutable checksum-verified migrations. +- The Obsidian wiki is an opt-in human view, not the read layer. -For zero-cost offline use, install [Ollama](https://ollama.com), `ollama pull llama3.2:3b`, and set `MEMORYMASTER_LLM_PROVIDER=ollama`. +See [Architecture](docs/architecture.md), [Operations](docs/operations.md), and +the [Graph Observations ledger](.planning/GRAPH-OBSERVATIONS-V1.md). -## MCP server +## MCP configuration ```json { @@ -223,8 +234,8 @@ For zero-cost offline use, install [Ollama](https://ollama.com), `ollama pull ll "memorymaster": { "command": "memorymaster-mcp", "env": { - "MEMORYMASTER_DEFAULT_DB": "/path/to/memorymaster.db", - "MEMORYMASTER_WORKSPACE": "/path/to/your/project", + "MEMORYMASTER_DEFAULT_DB": "", + "MEMORYMASTER_WORKSPACE": "", "MEMORYMASTER_MCP_AUTH_MODE": "local-trusted" } } @@ -232,103 +243,76 @@ For zero-cost offline use, install [Ollama](https://ollama.com), `ollama pull ll } ``` -MCP authorization mode is mandatory. Use `local-trusted` only with SQLite in a -private stdio process controlled by one OS user. PostgreSQL application runtime -is team-only and requires an operator-configured principal, tenant, non-owner -application DSN, workspace, and explicit scope allowlist. Schema initialization -uses a distinct migrator DSN/role; never give that role to the MCP runtime. -Unverified host-wide and maintenance tools fail closed. Existing brownfield MCP -entries must add the mode or be regenerated with setup `--force`. - -MemoryMaster exposes `remember`, `recall`, `forget`, and `improve` plus advanced -setup, lifecycle, retrieval, graph, and governance tools. The -[generated release truth](docs/generated/release-truth.md) is the authoritative -inventory and count. - -See [`docs/MCP-TOOLS.md`](docs/MCP-TOOLS.md) for the grouped reference (one line per tool), and [`.mcp.json.example`](.mcp.json.example) for the full config template. - -## Backends - -| Backend | Install | Use case | -|---------|---------|----------| -| **SQLite** | Built-in | **Primary profile:** personal/local, private, zero-config | -| **Postgres 16.x** | `pip install "memorymaster[postgres]"` | **Deferred:** future authenticated team deployment with isolated app/migrator roles | - -Postgres is not required for normal MemoryMaster use and is not currently a -release target. The detailed contract below is retained so the dormant profile -fails closed and can be revisited safely if a genuine shared-service use case -appears. - -PostgreSQL v0011 enables and forces row-level security. Reads are tenant/scope -bounded and expose public claims or the principal's own private claims; writes -are owner-only, require a nonblank `source_agent` on every team claim, and are -limited to public/private rows. Migration v0012 makes public claim identities -tenant + exact-scope local; non-public idempotency keys, human IDs, and -confirmed tuples additionally include exact visibility and principal. A -tenant-derived hash-only function preserves the event chain across private -principals/scopes without exposing payloads. The application role must read and -append events but cannot update any event column or delete events. Unscoped -human-ID/idempotency-key reads fail when an identifier is ambiguous across -scopes. The supersession guard denies self- and cross-tenant/scope/visibility/ -principal links; the canonical lifecycle locks both claims and commits reciprocal -pointers plus one event atomically. Startup rejects drift in exact policy, -function, trigger, privilege, and identity-index catalogs. Brownfield -owner/duplicate/unsafe-supersession repair requires a reviewed external -maintenance action. Team action proposals and raw merge/sync paths remain disabled. -See [INSTALLATION.md](INSTALLATION.md#postgresql-team-runtime-security-boundary) -and [deployment profiles](docs/deployment_profiles.md) before enabling this -backend. Real PostgreSQL verification requires two distinct DSNs targeting a -disposable database; repository tests do not constitute a production proof. - -## Docker Compose - -For an explicit experimental semantic profile, run the optional Qdrant index -and Ollama stack with: - -```bash -docker compose up -d -``` +`local-trusted` is only for a private SQLite stdio process controlled by one OS +user. Regenerate old brownfield entries that do not declare an authorization +mode. The full tool inventory is generated from code in +[release truth](docs/generated/release-truth.md); operational examples are in +[MCP tools](docs/MCP-TOOLS.md). + +## Providers + +Provider calls are for extraction, consolidation and selected steward phases— +not for ordinary local recall. -Starting Qdrant alone does not enable governed retrieval. The minimal local -profile should not start this stack. +| Provider | Configuration | Typical use | +|---|---|---| +| Gemini | `MEMORYMASTER_LLM_PROVIDER=google` plus configured Google credentials | Activated extraction path | +| GLM through authenticated OpenCode | configured consolidation model | Activated Dreaming/profile consolidation path | +| Claude CLI OAuth | `MEMORYMASTER_LLM_PROVIDER=claude_cli` | Optional batch/steward path | +| Ollama | `MEMORYMASTER_LLM_PROVIDER=ollama` | Optional local provider | +| OpenAI / Anthropic APIs | corresponding provider and environment credential | Supported optional adapters | + +Never put credentials in claims, repository files, task arguments, logs or +generated profiles. -See [INSTALLATION.md](INSTALLATION.md) for Kubernetes / Helm. +## Useful commands -## Development +```powershell +# Trusted recall with score explanation +memorymaster query "topic" --explain -```bash -# Install with dev dependencies -pip install -e ".[dev,mcp,security,embeddings,qdrant]" +# One bounded steward cycle +python -m memorymaster --db memorymaster.db run-cycle -# Run tests -pytest tests/ -q +# Database checks +memorymaster --db memorymaster.db --json integrity --quick-check --fk-check -# Lint and format -ruff check memorymaster/ && ruff format memorymaster/ +# Dashboard +memorymaster-dashboard --db memorymaster.db -# Performance benchmarks -python benchmarks/perf_smoke.py +# Validate generated release inventory +python scripts/generate_release_truth.py --check ``` -See [CONTRIBUTING.md](CONTRIBUTING.md) for the full workflow. +## Upgrade and rollback + +Upgrade the package, then restart long-lived MCP/daemon processes: + +```powershell +python -m pip install --upgrade "memorymaster[mcp,capture,security]" +``` + +Feature rollback does not delete history: + +```powershell +[Environment]::SetEnvironmentVariable("MEMORYMASTER_GRAPH_OBSERVATIONS", "0", "User") +[Environment]::SetEnvironmentVariable("MEMORYMASTER_COMPILED_PROFILE", "0", "User") +``` -## Documentation +Disabling generation/recall leaves additive tables and audit history intact. +Candidates can be archived and confirmed generated observations made stale only +through the governed lifecycle—not by deleting database rows. -| Document | Description | -|----------|-------------| -| [docs/README.md](docs/README.md) | Documentation index — where to find each living doc | -| [docs/handbook.md](docs/handbook.md) | Full operator handbook — hooks, dashboard, steward, dream bridge, troubleshooting, one-prompt install | -| [docs/MCP-TOOLS.md](docs/MCP-TOOLS.md) | MCP usage guide; generated inventory and counts are linked from the document | -| [docs/INTEGRATING.md](docs/INTEGRATING.md) | Integration guide for embedding MemoryMaster in your agent | -| [docs/public-v1.md](docs/public-v1.md) | Stable remember/recall/forget/improve contract, capture limits, and trust boundary | -| [INSTALLATION.md](INSTALLATION.md) | Setup guide: pip, Docker, Helm, MCP config | -| [CONTRIBUTING.md](CONTRIBUTING.md) | Dev setup, testing, PR workflow | -| [ARCHITECTURE.md](ARCHITECTURE.md) | System design and subsystem details | -| [USER_GUIDE.md](USER_GUIDE.md) | Usage, MCP integration, troubleshooting | -| [CHANGELOG.md](CHANGELOG.md) | Version history and release notes | -| [ROADMAP.md](ROADMAP.md) | Release plan and future tracks | -| [docs/enabling-v2-systems.md](docs/enabling-v2-systems.md) | v3 statistical classifier + cadence policy opt-in | +## Project status and documentation -## License +- Stable release history: [CHANGELOG](CHANGELOG.md) +- Canonical documentation verdicts: [DOCS-MAP](DOCS-MAP.md) +- Install and agent wiring: [AGENT-INSTALL](docs/AGENT-INSTALL.md) +- Public API: [Public v1](docs/public-v1.md) +- Operational procedures: [Operations](docs/operations.md) +- Security and supply chain: [Security](docs/security_supply_chain.md) +- Full feature handbook: [Handbook](docs/handbook.md) -[MIT](LICENSE) — Built by [wolverin0](https://github.com/wolverin0) +MemoryMaster is MIT licensed. Contributions should preserve SQLite authority, +claim governance, exact support lineage, privacy boundaries and opt-in derived +recall. diff --git a/docs/generated/release-truth.json b/docs/generated/release-truth.json index 4d6ac222..a07aff90 100644 --- a/docs/generated/release-truth.json +++ b/docs/generated/release-truth.json @@ -134,7 +134,7 @@ "console_entrypoints": 8, "mcp_tools": 50, "ops_cli_commands": 5, - "pytest_test_functions": 3810 + "pytest_test_functions": 3814 }, "feature_profile_matrix": { "capture_hook": [ @@ -255,5 +255,5 @@ "mcp" ] }, - "version": "4.7.1" + "version": "4.7.2" } diff --git a/docs/generated/release-truth.md b/docs/generated/release-truth.md index 8b7a5075..8438dd3c 100644 --- a/docs/generated/release-truth.md +++ b/docs/generated/release-truth.md @@ -8,12 +8,12 @@ Do not edit this file by hand. Run `python scripts/generate_release_truth.py`. -- Package version: `4.7.1` +- Package version: `4.7.2` - MCP tools: **50** - Main CLI commands: **118** - Operations CLI commands: **5** - Console entrypoints: **8** -- Pytest source test functions: **3810** +- Pytest source test functions: **3814** ## MCP tools diff --git a/memorymaster/operations/__init__.py b/memorymaster/operations/__init__.py new file mode 100644 index 00000000..a85ae75b --- /dev/null +++ b/memorymaster/operations/__init__.py @@ -0,0 +1 @@ +"""Operational inspection tools that never mutate memory state.""" diff --git a/memorymaster/operations/operational_review.py b/memorymaster/operations/operational_review.py new file mode 100644 index 00000000..1c147ce7 --- /dev/null +++ b/memorymaster/operations/operational_review.py @@ -0,0 +1,305 @@ +"""Read-only operational review for a live personal MemoryMaster installation. + +The review is deliberately independent of release timing. It records current +runtime evidence and never mutates claims, jobs, lifecycle state, or a success +watermark. Exit codes are 0=PASS, 1=FAIL, and 3=WARN. +""" + +from __future__ import annotations + +import argparse +import importlib.metadata +import json +import os +import sqlite3 +from dataclasses import asdict, dataclass +from datetime import datetime, timedelta, timezone +from enum import Enum +from pathlib import Path +from typing import Callable, Iterable + + +ACTIVE_JOB_STATES = ("pending", "leased", "retryable", "blocked") +TRUE_VALUES = {"1", "true", "yes", "on"} + + +class Verdict(str, Enum): + PASS = "PASS" + WARN = "WARN" + FAIL = "FAIL" + + +@dataclass(frozen=True, slots=True) +class ReviewResult: + name: str + verdict: Verdict + detail: str + counts: dict[str, int] | None = None + human_ids: tuple[str, ...] = () + + +@dataclass(frozen=True, slots=True) +class ReviewConfig: + db: Path + expected_version: str | None = None + lookback_hours: int = 8 + canary_query: str | None = None + canary_human_id: str | None = None + + +def _connect_ro(db: Path) -> sqlite3.Connection: + connection = sqlite3.connect(f"file:{db.as_posix()}?mode=ro", uri=True, timeout=30) + connection.row_factory = sqlite3.Row + connection.execute("PRAGMA query_only=ON") + connection.execute("PRAGMA busy_timeout=30000") + return connection + + +def _enabled(name: str) -> bool: + return os.environ.get(name, "").strip().lower() in TRUE_VALUES + + +def check_runtime(config: ReviewConfig) -> ReviewResult: + try: + version = importlib.metadata.version("memorymaster") + except importlib.metadata.PackageNotFoundError: + return ReviewResult("runtime", Verdict.FAIL, "installed package unavailable") + expected = config.expected_version or version + verdict = Verdict.PASS if version == expected else Verdict.FAIL + return ReviewResult("runtime", verdict, f"installed={version} expected={expected}") + + +def check_database(config: ReviewConfig) -> ReviewResult: + try: + with _connect_ro(config.db) as connection: + quick = [str(row[0]) for row in connection.execute("PRAGMA quick_check")] + foreign_keys = sum(1 for _ in connection.execute("PRAGMA foreign_key_check")) + migration = int(connection.execute("SELECT MAX(version) FROM schema_versions").fetchone()[0] or 0) + except (OSError, sqlite3.Error) as exc: + return ReviewResult("database", Verdict.FAIL, f"probe_error={type(exc).__name__}") + ok = quick == ["ok"] and foreign_keys == 0 and migration >= 21 + return ReviewResult( + "database", + Verdict.PASS if ok else Verdict.FAIL, + f"quick_check={','.join(quick[:3])} foreign_key_errors={foreign_keys} migration={migration}", + ) + + +def check_feature_activation(_config: ReviewConfig) -> ReviewResult: + states = { + "graph_observations": int(_enabled("MEMORYMASTER_GRAPH_OBSERVATIONS")), + "compiled_profile": int(_enabled("MEMORYMASTER_COMPILED_PROFILE")), + } + enabled = sum(states.values()) + return ReviewResult( + "feature_activation", + Verdict.PASS if enabled == len(states) else Verdict.WARN, + f"enabled={enabled}/{len(states)}", + states, + ) + + +def check_graph_observations(config: ReviewConfig) -> ReviewResult: + marks = ",".join("?" for _ in ACTIVE_JOB_STATES) + try: + with _connect_ro(config.db) as connection: + rows = connection.execute( + f"SELECT status, COUNT(*) count FROM graph_observation_jobs " + f"WHERE status IN ({marks}) GROUP BY status", + ACTIVE_JOB_STATES, + ).fetchall() + counts = {state: 0 for state in ACTIVE_JOB_STATES} + counts.update({str(row["status"]): int(row["count"]) for row in rows}) + counts["completed_discovery"] = int(connection.execute( + "SELECT COUNT(*) FROM graph_observation_jobs WHERE stage='discover' AND status='completed'" + ).fetchone()[0]) + counts["observations"] = int(connection.execute( + "SELECT COUNT(*) FROM graph_observations" + ).fetchone()[0]) + expired_leases = int(connection.execute( + "SELECT COUNT(*) FROM graph_observation_jobs WHERE status='leased' " + "AND lease_expires_at IS NOT NULL AND datetime(lease_expires_at)<=datetime('now')" + ).fetchone()[0]) + counts["expired_leases"] = expired_leases + except (OSError, sqlite3.Error) as exc: + return ReviewResult("graph_observations", Verdict.FAIL, f"probe_error={type(exc).__name__}") + if counts["blocked"] or expired_leases: + verdict = Verdict.FAIL + elif counts["retryable"] or counts["pending"] > 100: + verdict = Verdict.WARN + else: + verdict = Verdict.PASS + return ReviewResult( + "graph_observations", + verdict, + "zero observations is valid when no eligible evidence component exists", + counts, + ) + + +def check_compiled_profile(config: ReviewConfig) -> ReviewResult: + try: + with _connect_ro(config.db) as connection: + completed = int(connection.execute( + "SELECT COUNT(*) FROM compiled_profile_runs WHERE status='completed'" + ).fetchone()[0]) + facts = int(connection.execute( + "SELECT COUNT(*) FROM compiled_profile_facts WHERE status='active'" + ).fetchone()[0]) + supports = int(connection.execute("SELECT COUNT(*) FROM compiled_profile_supports").fetchone()[0]) + mismatches = int(connection.execute(""" + SELECT COUNT(*) FROM compiled_profile_facts f + LEFT JOIN ( + SELECT fact_id, COUNT(*) support_count, COUNT(DISTINCT session_id) session_count + FROM compiled_profile_supports GROUP BY fact_id + ) s ON s.fact_id=f.id + WHERE f.status='active' AND ( + f.support_count<>COALESCE(s.support_count,0) + OR f.independent_sessions<>COALESCE(s.session_count,0) + ) + """).fetchone()[0]) + except (OSError, sqlite3.Error) as exc: + return ReviewResult("compiled_profile", Verdict.FAIL, f"probe_error={type(exc).__name__}") + counts = {"completed_runs": completed, "active_facts": facts, "supports": supports, "mismatches": mismatches} + if mismatches: + verdict = Verdict.FAIL + elif _enabled("MEMORYMASTER_COMPILED_PROFILE") and (completed == 0 or facts == 0): + verdict = Verdict.WARN + else: + verdict = Verdict.PASS + return ReviewResult("compiled_profile", verdict, "active facts must retain exact session support", counts) + + +def check_recent_private_context(config: ReviewConfig) -> ReviewResult: + from memorymaster.core.security import _CLAIM_ONLY_PATTERNS + + since = datetime.now(timezone.utc) - timedelta(hours=config.lookback_hours) + matches: list[str] = [] + scanned = 0 + try: + with _connect_ro(config.db) as connection: + rows = connection.execute( + "SELECT human_id, text, subject, predicate, object_value FROM claims " + "WHERE datetime(created_at)>=datetime(?)", + (since.isoformat(),), + ) + for row in rows: + scanned += 1 + content = "\n".join(str(row[key] or "") for key in ("text", "subject", "predicate", "object_value")) + if any(pattern.search(content) for _, pattern in _CLAIM_ONLY_PATTERNS): + matches.append(str(row["human_id"] or "")) + except (OSError, sqlite3.Error) as exc: + return ReviewResult("recent_private_context", Verdict.FAIL, f"probe_error={type(exc).__name__}") + counts = {"lookback_hours": config.lookback_hours, "claims_scanned": scanned, "matches": len(matches)} + return ReviewResult( + "recent_private_context", + Verdict.FAIL if matches else Verdict.PASS, + "claim fields only; raw source/evidence is intentionally outside this check", + counts, + tuple(matches[:10]), + ) + + +def _default_retrieval(db: Path, query: str) -> list[str]: + from memorymaster.core.service import MemoryService + from memorymaster.recall.planner import RetrievalRequest, build_retrieval_plan + + service = MemoryService(str(db), workspace_root=db.parent, read_only=True) + plan = build_retrieval_plan(RetrievalRequest(query_text=query, limit=5, trust_mode="trusted")) + rows = service.query_rows( + query_text=plan.search_text, + limit=plan.limit, + include_stale=False, + include_conflicted=False, + include_candidates=False, + retrieval_mode=plan.effective_mode, + allow_sensitive=False, + scope_allowlist=None, + record_accesses=False, + ) + return [str(row["claim"].human_id or "") for row in rows] + + +def check_retrieval( + config: ReviewConfig, + *, + retrieve: Callable[[Path, str], list[str]] = _default_retrieval, +) -> ReviewResult: + if not config.canary_query or not config.canary_human_id: + return ReviewResult("retrieval_canary", Verdict.WARN, "canary not configured") + try: + ranking = retrieve(config.db, config.canary_query) + except Exception as exc: # noqa: BLE001 - review converts probe errors into evidence + return ReviewResult("retrieval_canary", Verdict.FAIL, f"probe_error={type(exc).__name__}") + rank = ranking.index(config.canary_human_id) + 1 if config.canary_human_id in ranking else 0 + return ReviewResult( + "retrieval_canary", + Verdict.PASS if rank else Verdict.FAIL, + f"target={config.canary_human_id} rank={rank or 'missing'}", + human_ids=tuple(ranking), + ) + + +def exit_code(results: Iterable[ReviewResult]) -> int: + verdicts = {result.verdict for result in results} + if Verdict.FAIL in verdicts: + return 1 + if Verdict.WARN in verdicts: + return 3 + return 0 + + +def run_review(config: ReviewConfig) -> list[ReviewResult]: + return [ + check_runtime(config), + check_database(config), + check_feature_activation(config), + check_graph_observations(config), + check_compiled_profile(config), + check_recent_private_context(config), + check_retrieval(config), + ] + + +def _parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--db", required=True) + parser.add_argument("--expected-version") + parser.add_argument("--lookback-hours", type=int, default=8) + parser.add_argument("--canary-query") + parser.add_argument("--canary-human-id") + parser.add_argument("--json", action="store_true") + return parser + + +def main(argv: list[str] | None = None) -> int: + args = _parser().parse_args(argv) + config = ReviewConfig( + db=Path(args.db).expanduser().resolve(), + expected_version=args.expected_version, + lookback_hours=max(1, min(168, args.lookback_hours)), + canary_query=args.canary_query, + canary_human_id=args.canary_human_id, + ) + results = run_review(config) + code = exit_code(results) + payload = { + "schema": "memorymaster.operational-review.v1", + "observed_at": datetime.now(timezone.utc).isoformat(), + "review_performed": True, + "database_mutations": 0, + "verdict": {0: "PASS", 1: "FAIL", 3: "WARN"}[code], + "exit_code": code, + "checks": [{**asdict(item), "verdict": item.verdict.value} for item in results], + } + if args.json: + print(json.dumps(payload, indent=2)) + else: + for item in results: + print(f"{item.name}: {item.verdict.value} - {item.detail}") + print(f"overall: {payload['verdict']} (exit {code})") + return code + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/pyproject.toml b/pyproject.toml index f7ea1444..9e7fda40 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "memorymaster" -version = "4.7.1" +version = "4.7.2" description = "Production-grade memory reliability system for AI coding agents. Lifecycle-managed claims with citations, conflict detection, steward governance, and MCP integration." license = {text = "MIT"} authors = [{name = "wolverin0"}] diff --git a/scripts/install-windows-operational-review.ps1 b/scripts/install-windows-operational-review.ps1 new file mode 100644 index 00000000..faf89a88 --- /dev/null +++ b/scripts/install-windows-operational-review.ps1 @@ -0,0 +1,52 @@ +param( + [Parameter(Mandatory = $true)] + [string]$PythonExe, + [Parameter(Mandatory = $true)] + [string]$Database, + [string]$ExpectedVersion = "4.7.2", + [int]$EveryHours = 6, + [int]$LookbackHours = 8, + [string]$CanaryQuery = "why does wezterm cli time out from Node but not from bash", + [string]$CanaryHumanId = "mm-8aef", + [string]$TaskName = "MemoryMaster-Operational-Review" +) + +$ErrorActionPreference = "Stop" +if (-not (Test-Path -LiteralPath $PythonExe -PathType Leaf)) { throw "Python executable not found" } +if (-not (Test-Path -LiteralPath $Database -PathType Leaf)) { throw "Database not found" } +if ($EveryHours -lt 1 -or $EveryHours -gt 24) { throw "EveryHours must be between 1 and 24" } + +$installRoot = Join-Path $env:LOCALAPPDATA "MemoryMaster\operational-review" +$outputRoot = Join-Path $installRoot "results" +[IO.Directory]::CreateDirectory($installRoot) | Out-Null +[IO.Directory]::CreateDirectory($outputRoot) | Out-Null +$runner = Join-Path $installRoot "windows-operational-review.ps1" +$configPath = Join-Path $installRoot "config.json" +Copy-Item -LiteralPath (Join-Path $PSScriptRoot "windows-operational-review.ps1") -Destination $runner -Force + +$config = [ordered]@{ + python = [IO.Path]::GetFullPath($PythonExe) + db = [IO.Path]::GetFullPath($Database) + expected_version = $ExpectedVersion + lookback_hours = $LookbackHours + canary_query = $CanaryQuery + canary_human_id = $CanaryHumanId + output_root = $outputRoot +} +[IO.File]::WriteAllText($configPath, ($config | ConvertTo-Json), [Text.UTF8Encoding]::new($false)) + +$actionArgs = "-NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File `"$runner`" -ConfigPath `"$configPath`"" +$action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument $actionArgs +$trigger = New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(2) ` + -RepetitionInterval (New-TimeSpan -Hours $EveryHours) +$settings = New-ScheduledTaskSettingsSet -MultipleInstances IgnoreNew ` + -ExecutionTimeLimit (New-TimeSpan -Minutes 5) -StartWhenAvailable +Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger -Settings $settings -Force | Out-Null + +[pscustomobject]@{ + task_name = $TaskName + interval_hours = $EveryHours + config = $configPath + result = (Join-Path $outputRoot "latest.json") + history = (Join-Path $outputRoot "history.jsonl") +} | ConvertTo-Json -Compress diff --git a/scripts/run_v47_operational_acceptance.py b/scripts/run_v47_operational_acceptance.py index 0b1754ff..ae1145df 100644 --- a/scripts/run_v47_operational_acceptance.py +++ b/scripts/run_v47_operational_acceptance.py @@ -31,7 +31,7 @@ sys.path.insert(0, str(REPO_ROOT)) -EXPECTED_VERSION = "4.7.0" +EXPECTED_VERSION = "4.7.2" TARGET_HUMAN_ID = "mm-8aef" TARGET_QUERY = "why does wezterm cli time out from Node but not from bash" ACTIVE_OBSERVATION_STATES = ("pending", "leased", "retryable", "blocked") diff --git a/scripts/windows-operational-review.ps1 b/scripts/windows-operational-review.ps1 new file mode 100644 index 00000000..2e7ff0a1 --- /dev/null +++ b/scripts/windows-operational-review.ps1 @@ -0,0 +1,60 @@ +param( + [Parameter(Mandatory = $true)] + [string]$ConfigPath +) + +$ErrorActionPreference = "Stop" +$config = Get-Content -LiteralPath $ConfigPath -Raw | ConvertFrom-Json +$outputRoot = [IO.Path]::GetFullPath([string]$config.output_root) +[IO.Directory]::CreateDirectory($outputRoot) | Out-Null +$tempJson = Join-Path $outputRoot ("review-{0}.tmp" -f [guid]::NewGuid().ToString("N")) +$tempError = Join-Path $outputRoot ("review-{0}.stderr.tmp" -f [guid]::NewGuid().ToString("N")) +$latestJson = Join-Path $outputRoot "latest.json" +$latestError = Join-Path $outputRoot "latest.stderr.log" +$historyLog = Join-Path $outputRoot "history.jsonl" + +$arguments = @( + "-m", "memorymaster.operations.operational_review", + "--db", [string]$config.db, + "--expected-version", [string]$config.expected_version, + "--lookback-hours", [string]$config.lookback_hours, + "--canary-query", [string]$config.canary_query, + "--canary-human-id", [string]$config.canary_human_id, + "--json" +) + +try { + $output = & ([string]$config.python) @arguments 2> $tempError + $exitCode = $LASTEXITCODE + $jsonText = $output -join [Environment]::NewLine + $null = $jsonText | ConvertFrom-Json + [IO.File]::WriteAllText($tempJson, $jsonText, [Text.UTF8Encoding]::new($false)) + Move-Item -LiteralPath $tempJson -Destination $latestJson -Force + if ((Get-Item -LiteralPath $tempError).Length -gt 0) { + Move-Item -LiteralPath $tempError -Destination $latestError -Force + } + else { + Remove-Item -LiteralPath $tempError -Force + if (Test-Path -LiteralPath $latestError) { Remove-Item -LiteralPath $latestError -Force } + } + $record = [ordered]@{ + observed_at = [DateTimeOffset]::Now.ToString("o") + exit_code = $exitCode + artifact = $latestJson + review_performed = $true + } | ConvertTo-Json -Compress + [IO.File]::AppendAllText($historyLog, $record + [Environment]::NewLine, [Text.UTF8Encoding]::new($false)) + exit $exitCode +} +catch { + if (Test-Path -LiteralPath $tempJson) { Remove-Item -LiteralPath $tempJson -Force } + if (Test-Path -LiteralPath $tempError) { Move-Item -LiteralPath $tempError -Destination $latestError -Force } + $record = [ordered]@{ + observed_at = [DateTimeOffset]::Now.ToString("o") + exit_code = 9 + error_type = $_.Exception.GetType().Name + review_performed = $false + } | ConvertTo-Json -Compress + [IO.File]::AppendAllText($historyLog, $record + [Environment]::NewLine, [Text.UTF8Encoding]::new($false)) + exit 9 +} diff --git a/tests/test_operational_review.py b/tests/test_operational_review.py new file mode 100644 index 00000000..a891e03e --- /dev/null +++ b/tests/test_operational_review.py @@ -0,0 +1,106 @@ +from __future__ import annotations + +import sqlite3 +from pathlib import Path + +from memorymaster.operations import operational_review as review + + +def _db(path: Path) -> Path: + connection = sqlite3.connect(path) + connection.executescript(""" + CREATE TABLE schema_versions(version INTEGER); + INSERT INTO schema_versions VALUES (21); + CREATE TABLE graph_observation_jobs( + stage TEXT, status TEXT, lease_expires_at TEXT + ); + INSERT INTO graph_observation_jobs VALUES ('discover', 'completed', NULL); + CREATE TABLE graph_observations(observation_claim_id INTEGER); + CREATE TABLE compiled_profile_runs(status TEXT); + INSERT INTO compiled_profile_runs VALUES ('completed'); + CREATE TABLE compiled_profile_facts( + id INTEGER PRIMARY KEY, status TEXT, support_count INTEGER, independent_sessions INTEGER + ); + INSERT INTO compiled_profile_facts VALUES (1, 'active', 2, 2); + CREATE TABLE compiled_profile_supports(fact_id INTEGER, session_id TEXT); + INSERT INTO compiled_profile_supports VALUES (1, 'a'), (1, 'b'); + CREATE TABLE claims( + human_id TEXT, text TEXT, subject TEXT, predicate TEXT, + object_value TEXT, created_at TEXT + ); + INSERT INTO claims VALUES ( + 'mm-safe', 'Uses a repo-relative config file', 'MemoryMaster', 'uses', + 'scripts/config.json', datetime('now') + ); + """) + connection.commit() + connection.close() + return path + + +def test_clean_review_checks_pass(tmp_path: Path, monkeypatch) -> None: + config = review.ReviewConfig( + db=_db(tmp_path / "memory.db"), + expected_version="4.7.2", + canary_query="why", + canary_human_id="mm-target", + ) + monkeypatch.setattr(review.importlib.metadata, "version", lambda _name: "4.7.2") + monkeypatch.setenv("MEMORYMASTER_GRAPH_OBSERVATIONS", "1") + monkeypatch.setenv("MEMORYMASTER_COMPILED_PROFILE", "1") + + results = [ + review.check_runtime(config), + review.check_database(config), + review.check_feature_activation(config), + review.check_graph_observations(config), + review.check_compiled_profile(config), + review.check_recent_private_context(config), + review.check_retrieval(config, retrieve=lambda *_: ["mm-target"]), + ] + + assert review.exit_code(results) == 0 + assert {item.verdict for item in results} == {review.Verdict.PASS} + + +def test_private_context_and_blocked_job_fail(tmp_path: Path) -> None: + db = _db(tmp_path / "memory.db") + connection = sqlite3.connect(db) + connection.execute("INSERT INTO graph_observation_jobs VALUES ('synthesize', 'blocked', NULL)") + connection.execute( + "INSERT INTO claims VALUES (?, ?, '', '', '', datetime('now'))", + ("mm-private", "The service uses 10.1.2.3 internally"), + ) + connection.commit() + connection.close() + config = review.ReviewConfig(db=db) + + graph = review.check_graph_observations(config) + intake = review.check_recent_private_context(config) + + assert graph.verdict is review.Verdict.FAIL + assert intake.verdict is review.Verdict.FAIL + assert intake.human_ids == ("mm-private",) + assert review.exit_code([graph, intake]) == 1 + + +def test_missing_canary_and_disabled_features_warn(tmp_path: Path, monkeypatch) -> None: + config = review.ReviewConfig(db=_db(tmp_path / "memory.db")) + monkeypatch.delenv("MEMORYMASTER_GRAPH_OBSERVATIONS", raising=False) + monkeypatch.delenv("MEMORYMASTER_COMPILED_PROFILE", raising=False) + + results = [review.check_feature_activation(config), review.check_retrieval(config)] + + assert review.exit_code(results) == 3 + + +def test_powershell_scheduler_contract_is_bounded() -> None: + root = Path(__file__).resolve().parents[1] + installer = (root / "scripts" / "install-windows-operational-review.ps1").read_text(encoding="utf-8") + runner = (root / "scripts" / "windows-operational-review.ps1").read_text(encoding="utf-8") + + assert "RepetitionInterval" in installer + assert "ExecutionTimeLimit" in installer + assert "MultipleInstances IgnoreNew" in installer + assert "review_performed = $true" in runner + assert "work-receipt" not in runner.lower()