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

6.4 KiB

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.