docs: revise Phase 9 design after pre-implementation audit

↗ view on GitHub · Claude · 2026-05-16 · 84aafcd6

Audit of the existing codebase surfaced gaps the original design
glossed over. Corrected the design before writing code.

Architecture findings folded in:

- The four-layer system-prompt assembly described in
  01-architecture.md is NOT implemented today.
  chatTools.ts:85 hardcodes SYSTEM_PROMPT as a constant.
  users.custom_instructions is stored but never read into a
  prompt. workspaces.instructions is stored but never read.
  systemPromptExtra is only used by projectChat.ts for document
  context. Phase 9 now builds the full assembly chain in a new
  promptAssembly.ts helper, behind a use_layered_prompt feature
  flag (default true) on org_settings for emergency rollback.

- Two chat code paths exist (routes/chat.ts and
  routes/projectChat.ts). Both call the new builder.

- Auth uses res.locals.userId / res.locals.userRole, not req.user.
  Updated the API surface notes accordingly.

- Audit events are emitted inline (INSERT INTO audit_events ...)
  following the lib/users.ts pattern. The phantom lib/audit.ts
  reference is removed from the file list.

- pipApi exports flat functions (listProjects, createProject,
  ...). Updated the planned API to listMemories, createMemory,
  updateMemory, deleteMemory, clearMemories, exportMemories.

- The admin page is /admin/policy/, not /admin/ai-policy/.

Concrete safety corrections:

- LRU eviction under concurrent add_memory calls now runs inside
  a single transaction with SELECT ... FOR UPDATE on the user's
  rows, eliminating the race where two callers both pass the
  count check.

- Pin cap (10) is enforced by a Postgres trigger
  (enforce_user_memory_pin_cap) at INSERT and UPDATE OF pinned.
  Raises 23514 when exceeded. App layer translates to 409.

- Memory content is rendered as a fenced code block in the
  system prompt with explicit PINNED: / OTHER: subheadings.
  Hostile content (e.g. lines starting with ## or ---) cannot
  alter the document's markdown structure.

- DELETE /api/me/memories?confirm=1 replaced with
  POST /api/me/memories/clear body { confirm: true }. CSRF-safe:
  a JSON-body POST is not issuable cross-origin without explicit
  fetch.

- POST /api/me/memories forces source='user' server-side and
  ignores any source field in the body.

- Export endpoint returns chat_id as a bare uuid; does not
  hydrate chat title or other chat metadata, to avoid leaking
  metadata of chats the caller can no longer access.

- Audit actor for model-driven memory.create is the chat owner;
  metadata carries actor_is_system: true and the chat_id.

Migration corrected:

- Adds touch_updated_at trigger for the new table, matching the
  convention in 0002_users.sql, 0006_projects.sql, etc.
- Adds the pin-cap trigger function and trigger.
- Adds RLS enable + force + revoke from anon/authenticated, in
  line with 0011_rls.sql.
- Adds use_layered_prompt to org_settings as the rollback switch.
- Confirmed next migration number is 0022 (last is 0021_branding).

LlmPolicy interface gains allow_memory, memory_max_per_user,
memory_max_chars, and use_layered_prompt loaded from org_settings.
loadLlmPolicy() selects the new columns.
Repository cpatpa/PIP
Author Claude <noreply@anthropic.com>
Authored
Parents 5f2e7203
Stats 1 file changed , +177 , -65
Part of Phase 9 - persistent user memory and layered system-prompt assembly

Capture this commit into my fork

Download a Markdown prompt that tells Claude how to port this exact commit into your working tree. Run it via claude -p < capture-commit-84aafcd6.md from inside the repo you want the change in.

⬇ Download capture-commit-84aafcd6.md