Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
124 lines
6.4 KiB
Markdown
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.
|