Runner memory search, read, propose tools

Task 5420 · Done · Coder · created · updated

Epic: Centralized project memory for runners, behind a per-task switch. This child is C4. It builds the four runner-facing control MCP tools — `memory_search`, `memory_read`, `memory_feedback`, `memory_propose` — and their CP routes and search. Design sources: the BA PRD (Task 5403, `prd`) and the settled architecture (Task 5404, `architecture`), via `task_read_artifact`. It depends on C1: `memory_active`, `ingest_candidate`, `record_events`, `unit_by_token`, `bump_bundle_revision`, the vocabulary and the tables. ## Epic requirement (verbatim) > Runners re-derive what other runners already learned: … Build a centralized project-memory layer per the attached architecture ("Jaah Project Memory") … The attachments are research input: the BA and Architect own the design and may depart from them with reasons. > 2. Capture mostly without runner effort: … plus an explicit propose tool. > 3. Delivery three ways: … and on-demand search/read/feedback tools. ## Settled requirements (verbatim) - R-CAP-1 Explicit: a `memory_propose` control MCP tool (kind, title, body, paths, personas) available to every runner persona on ON tasks; creates a `proposed` unit through the curation gate. - R-DEL-4 On-demand tools: `memory_search` (query, kind, paths, persona, limit), `memory_read` (id), `memory_feedback` (id, confirm|flag, note) control MCP tools; hybrid relevance search; same visibility as R-UNIT-4. - R-UNIT-4 … Reads obey the same visibility rule as `task_read`/`task_search` (launcher's visibility); writes are stamped with the calling task/session derived by the daemon (no caller-supplied task id). - R-SW-5 … Memory MCP tools, if visible in the tool list, refuse with a clear "Project memory is off for this task" and write nothing (D-3). - R-CUR-8 Trust: confirms … raise it; flags … lower it; one vote per (task, unit). ## Approach (architecture, binding) **Routes** (`apps/api/app/routes/agent_memory.py`) and verbs (`apps/jaahd/agent_vocabulary.py`: one `Verb` + `ToolSpec` row each, `task.search` is the precedent): | Verb | Method · path | Tool | Args → response | |---|---|---|---| | `memory.search` | GET `/api/agent/tasks/{task_id}/memory/search` | `memory_search` | q, kind, paths, persona, limit≤20 → `{units:[{id:"mem-N", kind, title, snippet≤240, paths, trust, status, injectable, source:{task_id, persona}}]}` | | `memory.read` | GET `/api/agent/tasks/{task_id}/memory/unit` | `memory_read` | `unit` → full unit + provenance; records a `read` event (channel `tool`) | | `memory.feedback` | POST `/api/agent/tasks/{task_id}/memory/feedback` | `memory_feedback` | `{unit, verdict: confirm\|flag, note≤500}` (note secret-gated) → `{trust, status}` | | `memory.propose` | POST `/api/agent/tasks/{task_id}/memory/propose` | `memory_propose` | `{kind, title, body, paths[], personas[]}` → `{id, status}` \| `{rejected}` | - **Refusals.** TASK routes refuse OFF or killed tasks (`not memory_active(task)`) with **409 `{"detail":"Project memory is off for this task"}`** and write nothing. No route takes a task id in its body (N-7). - **Search** (`project_memory/search.py`): - Lexical always: tsv `ts_rank` plus a path/keyword filter. - Vector added when `support_kb.embeddings.embedder_from_settings()` returns an embedder AND the query embeds within 1.5 s. - Returns every `accepted` unit, injectable or not, and stale ones marked stale. Never `proposed`, `flagged`, `retired` or `graduated` as facts. - Project-scoped. Launcher visibility: a unit is returned if ANY of its source tasks passes `routes/agent_peer_tasks._visible_peers`' clause, or it has no task source. - **Propose** calls `ingest_candidate(feed="propose", injectable_on_accept=False)`: free text is search-only until corroborated. - **Feedback** writes a `memory_votes` row: unique (unit, task), source `tool`. It updates `confirms`/`flags`/`trust` with `trust = (confirms + 1) / (confirms + flags + 2)`. A flag below `TRUST_FLOOR` makes the unit non-injectable. Any served-set change calls `bump_bundle_revision`. - The ToolSpec `kind` enum imports `KINDS`, so `decision` is not offered. ## Acceptance criteria (BA, verbatim — this child's legs) - AC-9 Visibility. A unit from a task the launcher cannot see is not returned. *Verify:* test mirroring `task_search` visibility cases. - AC-10 Propose tool. `memory_propose` from an ON task creates a proposed unit through the gates; from an OFF task refuses and writes nothing. *Verify:* agent-route tests. - AC-19b (tools leg) `kind=decision` is refused by the tools. *Verify:* test. - AC-5 (tools leg) With the kill switch engaged, memory tools refuse. *Verify:* route test with flag on. - AC-8 (tools leg) A task of project B never sees units of project A via the tools. *Verify:* negative test. - AC-6 (tool leg) A settled task's recon-derived accepted unit is found by `memory_search` from a later task in the same project, and `memory_read(id)` returns its body. *Verify:* route test with a seeded accepted unit. (The two-task end-to-end is C9.) - Quality bar: search ranking returns the seeded matching unit first for its own question text, and the refusal text is exactly the pinned string. Out of scope: Epic (verbatim): "moving OMC state to shared storage (ADR-0099); changing canon docs or the router's existing deterministic routes; org-wide (cross-project) memory beyond a scope field reserved for it; adopting an external memory SaaS." BA fences: - N-3: no cross-project results. - N-4: no external SaaS. - N-7: no caller-supplied task id. - N-9: no decision kind. Also binding: - No migration (C1). - No `memory.bundle`, `memory.ingest` or `memory.report` routes (C3/C6a). - No changes to existing verbs' behaviour. - No console UI (C8). Done means: - AC-10: route tests show an ON propose → `proposed` row via the gates; an OFF/killed propose → 409 with the exact detail and zero rows written (asserted on every memory table). - AC-9: visibility tests mirror the `task_search` cases (hidden, unsaved, other launcher). - AC-8: a project-B negative test. - AC-19b: the tool refuses `decision` (schema and route). - `memory_read` records exactly one `read` event per (task, unit). - Feedback: a duplicate vote from the same task is not double-counted, and trust follows the formula. - The four verbs appear in the vocabulary tests and the MCP tool list. - `./dc check full` green. This is an authz-touching change (new agent routes, visibility), reviewed per `docs/policies/branching-policy.md`. Required checks: gate

Outcome

Built memory_search/read/feedback/propose tools, routes and hybrid search (launcher visibility, 409 when off). CI gate green; PR #4716 merged; Jaah Deploy (dev) succeeded. Local full gate stopped by host memory pressure. Deferred: 3 C1 ingest follow-ups.

Pull request

Artifacts

Activity

  • Draft
  • Ready
  • Runner started
  • Runner ended (done)