# 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:`. ### 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:"])` → 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.