Commit Graph
9 Commits
Author SHA1 Message Date
shadowdaoandClaude Opus 4.7 1b234069e3 feat(memory.search): optional minScore RRF threshold
Closes the long-standing backlog item from abfa5463. memory.search now
accepts an optional `minScore` parameter (Zod range 0..1) that drops hits
below the given Reciprocal Rank Fusion score. Default behavior is
unchanged — when minScore is unset, every fused result is returned, same
as today.

The original design memo suggested defaulting to 0.020, but that would
exclude valid pure-semantic matches (one ranker at rank 1 = 1/61 ≈
0.0164). Real-world Phase 2 testing surfaced exactly that case (the
"expose TLS" → HAProxy memory hit). Shipping unfiltered-by-default and
exposing the knob lets specific callers opt into stricter filtering
(e.g. ~0.025 to require two rankers to fire at rank 1) without
penalising legitimate semantic-only hits for the rest.

Tool description updated; per-source rank breakdown remains the primary
confidence signal for the model.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 17:04:00 -07:00
shadowdaoandClaude Opus 4.7 a44b834a78 fix: address Phase 4 code review (5 findings + symmetric snippet fix)
1. lib/snippets.ts + lib/memory-actions.ts (×2): shared-project key
   lookups were querying `projects` by key with no visibility scope. Since
   `projects.key` is unique per user (not global), the unscoped match
   could resolve another user's project entirely. Restricted the lookups
   to `readableProjectIds(userId, groupNames)` — own + shared only.

2. lib/access.ts getAccessibleProjects: a stray `if (existing) continue`
   inside the share-collapse loop short-circuited on the first match,
   killing the rw-beats-ro upgrade path. Two-group cases where one share
   was ro and another rw on the same project were incorrectly resolved
   as ro. Replaced with explicit owner/rw skip.

3. lib/mcp/tools.ts snippetPut: removed a dead `void exists` block that
   looked like an authorization pre-flight but was actually a no-op —
   real write authorization lives inside putSnippet, called next. Added
   a comment at the call site documenting where the check is.

4. memory.delete + snippet.delete: previously had no optimistic-lock
   CAS, so a concurrent peer edit could be silently overwritten by a
   delete on a stale view. Added optional `version` to MemoryDeleteInput
   (new) and SnippetDeleteInput (extended); UPDATE WHERE now CASes on
   version; 0-row response surfaces CONCURRENT_EDIT_ERROR. Web detail
   pages pass `version` through hidden form inputs. When the caller
   doesn't supply a version, falls back to the version we just read in
   the same handler for in-handler consistency.

5. lib/mcp/tools.ts memorySearch re-fetch: missing `isNull(deletedAt)`
   on the post-search row hydration left a TOCTOU window where a row
   soft-deleted between the search and the re-fetch would be returned.
   Visibility is still enforced by searchMemories itself.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 10:06:10 -07:00
shadowdaoandClaude Opus 4.7 d5823ca78c feat(sharing): project shares, co-edit safety, awareness UI (Phase 4c+d+e)
Adds project-level sharing via the new project_shares table plus the
infrastructure that makes multi-user editing safe and visible.

Authorization (lib/access.ts):
  - getAccessibleProjects / getProjectAccess centralise the predicate
    used by every read and write path.
  - readableProjectIds / writableProjectIds drive listing-style queries.
  - Web UI Server Actions and pages source group memberships from the
    user_groups table so authorization works without depending on
    Agent A's session callback shape.

Optimistic locking:
  - memories + snippets gain version + last_edited_by columns. Every
    UPDATE bumps version and stamps the editor; UPDATE WHERE clauses
    require the caller's pre-fetched version, surfacing a clear
    "refresh and try again" error on lost-write races rather than
    silently clobbering.
  - MemoryUpdateInput / SnippetPutInput accept an optional version
    token.

MCP tools:
  - memory.write / .update / .delete / .get / .list / .search,
    snippet.put / .get / .list / .delete now respect shared-project
    access (read = owner | any share, write = owner | rw share).
  - project, defaults to ctx.defaultProjectKey from the X-Project-Key
    header (populated by the MCP route — Agent A's wiring).
  - project.identify returns shared projects you have access to and
    prefers an owned project on key collision, audit-logging the
    collision so an operator can debug it.
  - Tool descriptions for memory.update, memory.write, snippet.put,
    and project.identify updated with the co-edit / shared-project
    notes.

Web UI:
  - Project detail page: ownership badge, shared-with-N-groups badge,
    owner-only "Manage sharing" section (add/flip/remove shares via
    lib/share-actions.ts). Add-share is constrained to groups the
    granter is already in.
  - "Shared" chips on memory cards in /memories and /dashboard.
  - "Last edited by ..." on memory + snippet detail pages, shown only
    when the last editor isn't the row's original author so the chip
    stays informative.
  - Read-only viewers (ro shares) lose Edit/Delete affordances on
    memories and snippets.

Migration 0004_project_shares.sql adds project_shares + the two new
columns on memories and snippets; it depends on Agent A's
0003_groups.sql for the groups, user_groups, and memory_access enum.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 09:50:59 -07:00
shadowdaoandClaude Opus 4.7 5b2bf7d19d fix: address code review on merged feature set
Five high-confidence findings from the post-merge reviewer pass:

1. memory.update MCP tool description claimed "Project is upserted if it
   doesn't exist", but the handler used resolveProjectId and would error.
   Matched the description to the actual behavior (call project.identify
   first) — keeps parity with memory.write.

2. updateMemoryAction's UPDATE statement was missing the userId guard.
   The preceding scoped SELECT made it not exploitable in practice, but
   it diverged from deleteMemoryAction's pattern. Added the guard for
   defense in depth.

3. putSnippet's UPDATE statement had the same missing userId guard —
   fixed the same way.

4. MemoryUpdateInput's refine for scope='user' accepted both
   project=undefined AND project=""; the snippets refine only accepted
   undefined. Tightened MemoryUpdateInput to require undefined, matching
   the snippets rule. Web actions already coerce "" → undefined before
   parsing, so no caller is affected.

5. 0002_snippets_scope.sql created two indexes unconditionally —
   replaced with CREATE INDEX IF NOT EXISTS so re-runs after a
   drizzle-kit push won't trip.

Also adds .claude/ to .gitignore so worktree directories from
multi-agent builds aren't accidentally committed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 06:53:32 -07:00
shadowdao 5e57538719 Merge: snippets feature end-to-end (Agent B) 2026-05-17 06:48:48 -07:00
shadowdaoandClaude Opus 4.7 fcc8708dd5 feat(snippets): named reusable artifacts with scope mirroring memories
Snippets are exact-name-keyed templates (PR formats, checklists, style
rules) — prescriptive artifacts the user wants applied consistently.
Distinct from memories, which are searched descriptive prose.

- Migration 0002 adds scope/project_id/deleted_at to snippets, partial
  unique indexes per scope, and a scope/project CHECK constraint
  mirroring memories_scope_project_chk.
- New @shared-memory/schemas: SnippetName/Put/Get/List/Delete inputs
  with shared scope-project refinement.
- lib/snippets.ts: get/put/list/softDelete helpers used by both the
  Web Server Actions and the MCP tool handlers.
- Four new MCP tools (snippet.put/get/list/delete) with directive
  descriptions contrasting against memory.* (exact-name lookup vs
  search; templates vs facts).
- /snippets pages: list, new, [name] with edit + delete and a
  scope-picker when a name lives in multiple scopes.
- Nav: Snippets link between Memories and Projects.

Design note: when a name exists in both user and project scope,
snippet.get without an explicit scope prefers project (if project key
given) then falls back to user. The detail page shows a picker when
the name is ambiguous and no scope query param was supplied.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 06:48:00 -07:00
shadowdao d65e1cb351 feat(memory): allow editing scope and project on memory.update
Extends both the Web UI edit form and the memory.update MCP tool so
existing memories can be reclassified between user-global and
project-attached scopes without delete+rewrite. Schema refines enforce
the user/project consistency invariants; audit log captures from/to
scope and projectKey on transitions.
2026-05-17 06:43:19 -07:00
shadowdaoandClaude Opus 4.7 9a8b504f51 feat: Phase 2 — embeddings + hybrid memory.search
Adds a small embedder sidecar (Xenova/bge-small-en-v1.5, ONNX, CPU-only)
that the web app calls inline on memory.write and memory.update, and on
demand from the new memory.search tool.

memory.search performs three candidate fetches in parallel — pgvector
cosine similarity, Postgres full-text via plainto_tsquery + ts_rank_cd,
and tag-set overlap — then fuses them with Reciprocal Rank Fusion
(k=60). Each result carries its per-source rank so the model can see
*why* a memory surfaced.

The migrator boot step gained an idempotent embedding backfill: any row
with embedding IS NULL is batched (32 at a time) through the embedder
after SQL migrations apply. Safe to run on every boot.

New tool memory.update fixes the missing edit path; centralises the
re-embed-on-content-change rule alongside write.

Stack additions:
- apps/embedder/ — Fastify server, persistent /data/models volume so the
  ~30 MB model only downloads once
- apps/web/lib/embedder.ts — typed HTTP client with batched embed +
  health probe
- packages/schemas — MemoryUpdateInput, MemorySearchInput
- docker-compose — embedder service, healthcheck, app + migrator both
  depend_on it healthy; EMBEDDER_URL promoted to a required env var

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 09:04:44 -07:00
shadowdaoandClaude Opus 4.7 077d0a0825 feat: Phase 1 — Authentik auth, MCP endpoint, persistent memory
End-to-end Phase 1 of shared-memory: a logged-in Authentik user can sign
into the Web UI (/me debug page), and an MCP client with an Authentik-
issued bearer token can call memory.write / memory.list / memory.get /
memory.delete plus project.identify against /api/mcp.

Stack:
- Next.js 15 (App Router) + React 19 + TypeScript, pnpm workspaces
- Drizzle ORM + Postgres 16 + pgvector + pg_trgm
- Auth.js v5 with Authentik provider (Web UI)
- jose + Authentik JWKS for MCP bearer-token validation
- JSON-RPC 2.0 dispatcher implementing the MCP wire protocol over plain
  HTTP POST (hand-rolled to fit Next.js App Router; switches to SSE in a
  later phase if server-initiated events are needed)
- bge-small embeddings sidecar deferred to Phase 2; the schema already
  reserves the vector(384) column + IVFFlat index, FTS via a STORED
  tsvector column, and the visibility enum (private/shared/team) so
  cross-user memory sharing can be added without a future migration

Deployment supports two modes (set in .env, never committed):
- Behind an external reverse proxy (HAProxy / nginx / Cloudflare Tunnel /
  Traefik) — DEFAULT; the app exposes APP_PORT on the host with
  X-Forwarded-* trusted, no in-container TLS
- Built-in TLS via Caddy — opt-in with `docker compose --profile tls up`

Discovery endpoint at /.well-known/oauth-protected-resource (RFC 9728)
points MCP clients at the Authentik authorization server after a 401.

README walks through both Authentik providers (Web UI + MCP resource
server), the audience scope mapping, redirect URIs, and includes a worked
HAProxy config snippet.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 07:04:11 -07:00