Reviewed spec.md Phase 4 and relevant codebase files.
UnderstandStudied Architecture.md and environment setup for Supermemory.
UnderstandExamined package dependencies and conversation lifecycle.
UnderstandDrafted detailed implementation plan for Phase 4.
PlanIncorporated user feedback to refine timeout patterns and idempotency.
EditI'll start by reading spec.md to understand what phase four entails, then explore the current codebase to ground the plan.
Phase 4 is Layer 3 personalization (Supermemory) — cross-session founder profile that shapes answer ordering/emphasis without becoming the retrieval. Let me study the current codebase to ground the plan, especially the retrieval/assemble/synthesize path where this plugs in.
I have the spine. Let me read the answer-path files where personalization plugs in — assemble.ts, synthesize.ts, pipeline.ts, and the answer route — plus the synthesis prompt and env example.
Now let me check what Architecture.md says about Supermemory/Layer 3, and the env setup for the SUPERMEMORY key.
Let me read the Architecture.md Layer 3 detail and the synthesis prompt spec with the FOUNDER PROFILE block, plus check for any installed supermemory SDK.
Let me check the package.json deps and how the Ask UI tracks conversations (for the write-back path), plus the gateway model setup.
I now have a thorough picture. Let me quickly check the Ask UI to understand the conversation lifecycle (relevant to the write-back trigger).
I've now studied the full answer path. Here's what I found that shapes the plan for Phase 4 — Layer 3 personalization (Supermemory):
lib/retrieval/pipeline.ts (retrieve()) assembles context but never consults a profile; assembleContext() in assemble.ts already prepends a RELATIONSHIPS block (the exact pattern a FOUNDER PROFILE block should follow); synthesize.ts SYNTH_SYSTEM has no profile awareness yet; app/api/answer/route.ts already has an onFinish/onDone hook (used for tracing) — the natural place to fire write-back.app/ask/page.tsx) is single-shot Q→A with no conversation/session model and no DB persistence of Q&A — so "write takeaways after conversations" needs a defined trigger.package.json), but SUPERMEMORY_API_KEY is present in .env.local. Today/briefing surfaces aren't built yet (Phase 6/7), though the acceptance criterion references Today ordering.A few decisions genuinely change the build. Let me interview you.
Good — three locked, and the read path is the subtle one, so let me break it down properly before we decide. No code yet; just the concept.
Supermemory stores memories: small standalone text statements about the founder, written over time. Think of them as sticky notes:
At answer time, right before synthesis, we want to hand the model a short FOUNDER PROFILE: block (sitting next to the existing RELATIONSHIPS: and CONTEXT: blocks). The only question is: which memories go into that block for this particular question? That's the entire "read path" decision. Critically — and this is the design guardrail from the spec — the profile only shapes ordering and emphasis. It is never retrieval. It cannot add facts. If the profile says "investors first" but no investor items were retrieved, nothing gets invented.
There are two fundamentally different kinds of memory, and they answer two different questions:
1. Always-on standing priorities — "What does the founder ALWAYS care about, regardless of what they asked?"
These should influence every answer's ordering. Example: "investor threads first." On "What should I focus on today?" this is exactly what reorders the briefing. We fetch a small, capped set of these every time.
2. Question-scoped relevant memories — "For THIS specific question, is there a preference that applies?"
We do a semantic search in Supermemory using the question. Example: question = "who should I hire?" → surfaces "founder only wants senior eng hires, no juniors." That memory is useless on an investor question, so we only want it when it's relevant.
| Strategy | "What should I focus on today?" | "What's John Doe's email?" (precise lookup) |
|---|---|---|
| Always-on only | ✅ "investors first" fires correctly | ❌ injects "investors first" as noise into a lookup that shouldn't be reordered |
| Semantic-by-question only | ❌ generic question may match nothing, so "investors first" silently fails to fire | ✅ tight, returns nothing, no noise |
| Both (recommended) | ✅ standing priority fires | ✅ standing set is tiny + we can gate it off |
So Both is strictly richer — but it needs two safety rails to avoid the noise problem the table exposes:
intent and recency_weight. For ordering-sensitive intents (daily_briefing, cross_source, company_summary, investor_summary) we inject the full profile. For a precise lookup ("what's John's email"), personalization should not reorder — so we skip or heavily trim the profile. This reuses signal we already have for free.Operational rails (same pattern as graph expansion already uses): the Supermemory read runs in parallel with hybridSearch, is best-effort with a short timeout, and fails open to an empty profile — personalization can never break or slow an answer. Exactly how expandGraph is wrapped in pipeline.ts today.
Two sub-decisions remain on this read path:
All five decisions are locked. Let me write the full plan to a durable artifact in an isolated worktree, then summarize it here.
Plan written to plans/phase-4-personalization.md. Sanity check: the plan's wiring claims are grounded in files I read this session — pipeline.ts's expandGraph fail-open .catch (the pattern the profile read copies), assembleContext's existing RELATIONSHIPS block ordering, isThin() being citation-only, and the onDone hook in answer/route.ts (the enqueue point). The Supermemory SDK surface is flagged as verify-before-wire rather than assumed.
A 12-section implementation plan for Phase 4 — Layer 3 personalization (Supermemory), built on your five locked decisions:
getProfileBlock(userId, plan) hook fetches capped always-on standing priorities (by tag, deterministic) + capped question-scoped scoped memories (semantic), runs parallel to hybridSearch, and fails open to an empty profile exactly like expandGraph does today.personalizationEnabled(intent) injects the profile for ordering-sensitive intents and skips precise lookup, reusing the plan signal you already compute.FOUNDER PROFILE: block prepended before RELATIONSHIPS/CONTEXT; isThin() stays citation-only so the profile can never add facts, add citations, or rescue a thin answer.trigger/personalize.ts task (enqueued fire-and-forget from the answer route's onDone, never on the hot path), plus an explicit /api/remember route + an Ask UI "remember" affordance.assembleBriefingContext seam so Phase 6/7 Today just connects bolts, no personalization rework.scripts/seed-preference.ts for deterministic CI/acceptance + the UI affordance for live demo.pnpm add supermemory), Langfuse spans, vitest coverage, 6 acceptance criteria, and 4 independently-shippable sub-phases (4a–4d) with a reverse cut order.The plan lives on branch worktree-phase-4-personalization-plan (isolated worktree). It's a planning artifact only — no code changed in your working copy. Tell me if you want it merged to a feature/ branch, or to start on sub-phase 4a.
result: Wrote plans/phase-4-personalization.md — a full Phase 4 (Supermemory personalization) implementation plan grounded in the current answer-path code, reflecting all five interview decisions (hybrid learn path, both-with-rails read path, intent gating, Ask+Today reusable hook, script+UI seeding).
All six are fair and sharpen the plan. Quick verdict before I edit: (1), (3), and (5) are real correctness/product gaps; (2) and (6) fix latent bugs; (4) removes dead indirection. Updating the plan now.
Now the intent gate folds in the master switch (feedback 3):
Now the dedup/idempotency fix (feedback 2) — separate the two distinct concerns and pick one mechanism per concern:
Now wire the idempotencyKey into the enqueue call (5.3) and add the forget/correction surface (feedback 5) to 5.2:
Now drop the assembleBriefingContext wrapper (feedback 4) and fix the streaming-header note (feedback 6):
Two cross-references to keep consistent — add forgetPreference to the module surface (§3.1) and the new routes/UI to sub-phase 4c (§12):
Now add acceptance criterion 7 (the correction round-trip) referenced by 4c:
All six revisions are in. Here's my take on each and what changed:
1. Timeout pattern — agreed, this was hand-wavy. Pinned a single withTimeout helper (Promise.race + clearTimeout in finally) in §3.1 with a "copy nowhere else" rule. Also upgraded the two-read fan-out to Promise.allSettled so a slow standing read can't sink a healthy scoped read.
2. Dedup race — you were right that the hash was named but never stored/checked. I separated the two concerns that were tangled together and gave each exactly one mechanism: same-answer retry idempotency → Trigger.dev idempotencyKey at the trigger boundary (so step 3 can't race itself); cross-conversation duplicates → the semantic near-dup guard. Explicitly dropped the metadata-hash approach. The enqueue call in §5.3 now carries the key.
3. Master switch — folded into personalizationEnabled so one predicate covers both the kill switch and the intent gate; the write paths check the same flag. No two paths to diverge.
4. assembleBriefingContext wrapper — removed entirely. You're right it was indirection without safety. §6 now just pins the contract: Phase 7's briefing calls the existing getProfileBlock + assembleContext directly with intent: 'daily_briefing'. The only obligation is keeping those two side-effect-free, which they are.
5. Correction/forget path — the real gap. Added to 4c: /api/remember is now POST add / GET list / DELETE :memoryId, with an ownership check (memory must carry user:<id> before deletion — no cross-tenant delete), a forgetPreference + listStandingPreferences module surface, and a "Forget" control in the Ask UI. New acceptance criterion 7 covers the round-trip, the ownership refusal, and idempotency.
6. Streaming-header caveat — noted. §9 now flags that x-zrux-meta works for Ask (header read before stream drain) but Today must carry provenance on the cached briefing payload or a separate status call, not the header.
The updated plan is at plans/phase-4-personalization.md on the worktree branch. No source code touched — still a planning artifact.
result: Revised plans/phase-4-personalization.md per all six review points — pinned timeout helper, split retry-idempotency (Trigger.dev key) from cross-conversation dedup, folded the kill switch into the intent gate, dropped the needless briefing wrapper, added a full forget/correction surface with ownership checks, and corrected the Today provenance-delivery note.