Files
smart-org/docs/plan/reports/agent-1-phase-4.md
T

124 lines
6.4 KiB
Markdown

# Agent 1 — Phase 4 report (org memory)
Branch: `dev` · Continues the org-platform workstream (PLAN-v2 §6,
TODO-agent-1 Phase 4). Provenance: the phase was reconstructed from the sibling
`agent1/org-platform` branch and reconciled onto `dev`'s Phase-2/3 shapes, then
fully re-verified here.
## What I did
### 4.1 `app/domain/memory/` (new, pure Python)
`models.py` owns the whole vocabulary: `MemoryKind` (rule / preference / lesson /
fact / code_map), `MemorySource` (user / manager / system / reflection), the
`Memory` record, the limits (key ≤ 80, content ≤ 1,500), `normalise` +
`content_hash` for dedupe, `visible_kinds(role)` for the role filter,
`lesson_expiry` (30 days unless pinned or confirmed), `relevance` (keyword
overlap + recency) and `tokenise` (Persian/English stop words removed). No
FastAPI, no SQLAlchemy.
### 4.2 `org_memories` (in `models_org.py`, migration `f7a8b9c0d1e2`)
id, organization_id, kind, key, content, content_hash, source, evidence_task_id
(**real FK** to `tasks.id`, `ON DELETE SET NULL`), pinned, confidence 0..1,
expires_at, hits, last_used_at, created_by_user_id, created_at, updated_at,
deleted_at (soft). Indexes: `(organization_id, kind)`, `(organization_id, key)`,
`organization_id`.
### 4.3 `memory_service.py` (the real `MemoryPort`)
- `record` — validates, **redacts through `redact_text` before storing**, then
dedupes (same key → update in place, same content hash → refresh), enforces the
org cap `MEMORY_ORG_CHAR_CAP` by evicting the oldest **unpinned lesson** first,
and enforces the 3-writes-per-task ceiling for reflections.
- `build_context(role, query, budget)` — pinned rules then pinned preferences
first, then everything the role may see by relevance and recency, assembled as
`- [قاعده] …` lines under `budget_chars`; counts `hits`/`last_used_at`.
- `on_task_finished` — one `RoutingMode.CHEAP` completion through
`ModelProviderPort`, asking for ≤ 2 lessons + a code-map paragraph, stored with
`source=reflection`/`confidence=0.5`, priced in the usage ledger. A failure is
recorded through `error_log` and swallowed — it never raises into the engine.
- `expire_memories()` — soft-deletes expired unpinned lessons (no worker hook; see
CHANGE REQUEST).
- Plus the UI/gateway writes: `list`, `teach`, `edit`, `set_pinned`, `confirm`,
`delete`, `stats`.
### 4.4 Router `memory.py` (`/organizations/{org}/memory*`)
`GET` list (kind + text filter, with total/pinned/chars stats), `POST` teach
(member+, rule or preference only), `PATCH /{id}` edit, `POST /{id}/pin`,
`POST /{id}/confirm`, `DELETE /{id}` (all admin+). Every mutation is audited
(`memory.create|update|pin|confirm|delete`) and then publishes `memory.changed`
on `org:<id>`.
### 4.5
`knowledge.py` is untouched and still mounted; its sources appear as the memory
page's fourth tab, and `/knowledge` redirects to `/memory`.
### 4.6 `/memory`
Tabs «قوانین و ترجیحات», «درس‌های آموخته», «نقشهٔ کد», «منابع دانش»; inline edit
modal, pin/unpin, «تأیید درس», the «به سازمان یاد بده» dialog, source table, and
`useLiveChannel(["org:<id>"])` → refresh on `memory.changed`.
### 4.7
`container_org.build_memory` now returns the real `MemoryService`; a second
builder `build_memory_service(session, provider=…)` serves the router and lets a
test inject a provider.
## Files changed
New: `app/domain/memory/{__init__,models}.py`,
`app/infrastructure/repositories/memory.py`,
`app/application/services/memory_service.py`, `tests/test_memory.py`,
`apps/web/src/test/org-memory.test.tsx`,
`alembic/versions/f7a8b9c0d1e2_org_memory.py`.
Modified: `app/infrastructure/db/models_org.py` (+`OrgMemoryRow`),
`app/application/container_org.py`, `app/domain/identity/audit_actions.py`,
`app/interfaces/http/routers/memory.py`, `apps/web/src/lib/api-org.ts`,
`apps/web/src/app/(app)/memory/page.tsx`,
`apps/web/src/app/(app)/knowledge/page.tsx`.
## Test output summary
```
apps/api: pytest -q → 587 passed, 1 skipped (was 553; +21 memory +13 MCP)
apps/api: ruff check . → All checks passed!
apps/api: mypy app → Success: no issues found in 147 source files
apps/api: alembic upgrade → downgrade -1 → upgrade → check → clean, single head
apps/web: tsc --noEmit → clean
apps/web: next lint → No ESLint warnings or errors
apps/web: vitest run → 99 passed (13 files)
```
`tests/test_memory.py` (4.8, 21 tests): domain rules (normalisation, hash equality
across case/whitespace, both limits, role filtering, lesson expiry, stop words) ·
dedupe and key correction · the cap evicting oldest unpinned lessons first · the
3-writes-per-task ceiling · a provider key shape never stored · context (pinned
rule first, budget respected, role filtering, a hit counted) · reflection (two
lessons + one code map with `source=reflection`/`confidence=0.5`, priced in the
ledger; a failing provider raises nothing and records
`memory.reflection_failed`; an empty catalogue records
`memory.reflection_no_model`) · the expiry job · tenant isolation · HTTP
permissions · audit actions · the real port wired in the container.
## Deviations / notes
- **`evidence_task_id` is a real foreign key** (`ON DELETE SET NULL`), so a
memory cannot point at a task that never existed.
- **A reflection is not grant-checked**: it uses `pick_for_mode(CHEAP)` like the
engine, without consulting `ModelGrantPort`. An org with no grants still spends
credits on its own reflection — flag if the grant filter should apply here too.
- **`_parse_reflection` is deliberately conservative**: only lines starting with a
known marker become memories.
- **The context block has a fixed header** (`یادداشت‌های سازمان:`) that counts
toward the budget; a budget smaller than the header yields `""`.
- Preferences are manager-only; `code_map`/`fact` go to the code-reading roles
(`planner`, `developer`, `programmer`, `reviewer`, `tester`).
## CHANGE REQUESTs
1. **`expire_memories()` has no caller.** The function is exposed on
`MemoryService.expire_memories(organization_id=None)`. Please call it from the
existing billing worker (or a new tick in `workers/__main__.py`) at
integration.
2. **`memory.changed` is not in `@agy/contracts`' `WsEventType`** — the page
compares the raw string. Please add it when the contracts package is touched.
3. **`build_memory_service` is importable only from `container_org`.** Re-export
it from `container.py` for one import surface.