Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
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 throughredact_textbefore storing, then dedupes (same key → update in place, same content hash → refresh), enforces the org capMEMORY_ORG_CHAR_CAPby 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 underbudget_chars; countshits/last_used_at.on_task_finished— oneRoutingMode.CHEAPcompletion throughModelProviderPort, asking for ≤ 2 lessons + a code-map paragraph, stored withsource=reflection/confidence=0.5, priced in the usage ledger. A failure is recorded througherror_logand 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_idis 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 consultingModelGrantPort. An org with no grants still spends credits on its own reflection — flag if the grant filter should apply here too. _parse_reflectionis 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/factgo to the code-reading roles (planner,developer,programmer,reviewer,tester).
CHANGE REQUESTs
expire_memories()has no caller. The function is exposed onMemoryService.expire_memories(organization_id=None). Please call it from the existing billing worker (or a new tick inworkers/__main__.py) at integration.memory.changedis not in@agy/contracts'WsEventType— the page compares the raw string. Please add it when the contracts package is touched.build_memory_serviceis importable only fromcontainer_org. Re-export it fromcontainer.pyfor one import surface.