diff --git a/app/admin/memory/adaptive/page.tsx b/app/admin/memory/adaptive/page.tsx
new file mode 100644
index 0000000..f64b596
--- /dev/null
+++ b/app/admin/memory/adaptive/page.tsx
@@ -0,0 +1 @@
+export default function Page(){ return Pandora Memory adaptive
Phase 4C adaptive memory console. Server endpoints remain bridge-auth gated; public read/write stay disabled.
- Model calls: {process.env.PANDORA_ENABLE_MODEL_CALLS === "true" ? "enabled" : "disabled"}
- Embeddings: {process.env.PANDORA_ENABLE_EMBEDDINGS === "true" ? "enabled" : "disabled"}
- Semantic retrieval: {process.env.PANDORA_ENABLE_SEMANTIC_RETRIEVAL === "true" ? "enabled" : "disabled"}
- Public read: {process.env.PANDORA_ENABLE_PUBLIC_MEMORY_READ === "true" ? "WARNING enabled" : "disabled"}
- Public persistence: {process.env.PANDORA_ENABLE_PUBLIC_MEMORY_PERSISTENCE === "true" ? "WARNING enabled" : "disabled"}
; }
diff --git a/app/admin/memory/candidates/page.tsx b/app/admin/memory/candidates/page.tsx
new file mode 100644
index 0000000..d2ad1e0
--- /dev/null
+++ b/app/admin/memory/candidates/page.tsx
@@ -0,0 +1 @@
+export default function Page(){ return Pandora Memory candidates
Phase 4C adaptive memory console. Server endpoints remain bridge-auth gated; public read/write stay disabled.
- Model calls: {process.env.PANDORA_ENABLE_MODEL_CALLS === "true" ? "enabled" : "disabled"}
- Embeddings: {process.env.PANDORA_ENABLE_EMBEDDINGS === "true" ? "enabled" : "disabled"}
- Semantic retrieval: {process.env.PANDORA_ENABLE_SEMANTIC_RETRIEVAL === "true" ? "enabled" : "disabled"}
- Public read: {process.env.PANDORA_ENABLE_PUBLIC_MEMORY_READ === "true" ? "WARNING enabled" : "disabled"}
- Public persistence: {process.env.PANDORA_ENABLE_PUBLIC_MEMORY_PERSISTENCE === "true" ? "WARNING enabled" : "disabled"}
; }
diff --git a/app/admin/memory/open-loops/page.tsx b/app/admin/memory/open-loops/page.tsx
new file mode 100644
index 0000000..b4d533f
--- /dev/null
+++ b/app/admin/memory/open-loops/page.tsx
@@ -0,0 +1 @@
+export default function Page(){ return Pandora Memory open-loops
Phase 4C adaptive memory console. Server endpoints remain bridge-auth gated; public read/write stay disabled.
- Model calls: {process.env.PANDORA_ENABLE_MODEL_CALLS === "true" ? "enabled" : "disabled"}
- Embeddings: {process.env.PANDORA_ENABLE_EMBEDDINGS === "true" ? "enabled" : "disabled"}
- Semantic retrieval: {process.env.PANDORA_ENABLE_SEMANTIC_RETRIEVAL === "true" ? "enabled" : "disabled"}
- Public read: {process.env.PANDORA_ENABLE_PUBLIC_MEMORY_READ === "true" ? "WARNING enabled" : "disabled"}
- Public persistence: {process.env.PANDORA_ENABLE_PUBLIC_MEMORY_PERSISTENCE === "true" ? "WARNING enabled" : "disabled"}
; }
diff --git a/app/admin/memory/profiles/page.tsx b/app/admin/memory/profiles/page.tsx
new file mode 100644
index 0000000..f0a9e12
--- /dev/null
+++ b/app/admin/memory/profiles/page.tsx
@@ -0,0 +1 @@
+export default function Page(){ return Pandora Memory profiles
Phase 4C adaptive memory console. Server endpoints remain bridge-auth gated; public read/write stay disabled.
- Model calls: {process.env.PANDORA_ENABLE_MODEL_CALLS === "true" ? "enabled" : "disabled"}
- Embeddings: {process.env.PANDORA_ENABLE_EMBEDDINGS === "true" ? "enabled" : "disabled"}
- Semantic retrieval: {process.env.PANDORA_ENABLE_SEMANTIC_RETRIEVAL === "true" ? "enabled" : "disabled"}
- Public read: {process.env.PANDORA_ENABLE_PUBLIC_MEMORY_READ === "true" ? "WARNING enabled" : "disabled"}
- Public persistence: {process.env.PANDORA_ENABLE_PUBLIC_MEMORY_PERSISTENCE === "true" ? "WARNING enabled" : "disabled"}
; }
diff --git a/app/admin/memory/retrieval/page.tsx b/app/admin/memory/retrieval/page.tsx
new file mode 100644
index 0000000..8e78cff
--- /dev/null
+++ b/app/admin/memory/retrieval/page.tsx
@@ -0,0 +1 @@
+export default function Page(){ return Pandora Memory retrieval
Phase 4C adaptive memory console. Server endpoints remain bridge-auth gated; public read/write stay disabled.
- Model calls: {process.env.PANDORA_ENABLE_MODEL_CALLS === "true" ? "enabled" : "disabled"}
- Embeddings: {process.env.PANDORA_ENABLE_EMBEDDINGS === "true" ? "enabled" : "disabled"}
- Semantic retrieval: {process.env.PANDORA_ENABLE_SEMANTIC_RETRIEVAL === "true" ? "enabled" : "disabled"}
- Public read: {process.env.PANDORA_ENABLE_PUBLIC_MEMORY_READ === "true" ? "WARNING enabled" : "disabled"}
- Public persistence: {process.env.PANDORA_ENABLE_PUBLIC_MEMORY_PERSISTENCE === "true" ? "WARNING enabled" : "disabled"}
; }
diff --git a/app/api/memory/adaptive/analyze/route.ts b/app/api/memory/adaptive/analyze/route.ts
new file mode 100644
index 0000000..da27fbc
--- /dev/null
+++ b/app/api/memory/adaptive/analyze/route.ts
@@ -0,0 +1,5 @@
+import { NextRequest, NextResponse } from "next/server";
+import { namespace, withBridge } from "@/app/api/memory/adaptive/route-helper";
+import { createCandidatesFromSession } from "@/lib/services/memory-candidate-service";
+export const dynamic="force-dynamic";
+export async function POST(request:NextRequest){ const body=await request.json().catch(()=>({})); const ns=namespace(body.namespace); if(!ns)return NextResponse.json({ok:false,blockers:["namespace_required"]},{status:400}); if(!body.text)return NextResponse.json({ok:false,blockers:["text_required"]},{status:400}); const bridge=await withBridge(request,"memoryCaptureApiEnabled"); if("error" in bridge)return bridge.error; const payload={...body,user_id:bridge.principal.userId,namespace:ns,source:body.source??"adaptive_analyze"}; const data=await createCandidatesFromSession(bridge.client,payload); return NextResponse.json({ok:true,...data}); }
diff --git a/app/api/memory/adaptive/context/route.ts b/app/api/memory/adaptive/context/route.ts
new file mode 100644
index 0000000..f639025
--- /dev/null
+++ b/app/api/memory/adaptive/context/route.ts
@@ -0,0 +1,15 @@
+import { NextRequest, NextResponse } from "next/server";
+import * as service from "@/lib/services/adaptive-chatgpt-context-service";
+import { namespace, withBridge } from "@/app/api/memory/adaptive/route-helper";
+
+export const dynamic = "force-dynamic";
+
+export async function POST(request: NextRequest) {
+ const body = await request.json().catch(() => ({}));
+ const ns = namespace(body.namespace);
+ if (!ns) return NextResponse.json({ ok: false, blockers: ["namespace_required"] }, { status: 400 });
+ const bridge = await withBridge(request, "memoryContextApiEnabled");
+ if ("error" in bridge) return bridge.error;
+ const data = await service.buildAdaptiveChatGptContext(bridge.client, { user_id: bridge.principal.userId, namespace: ns, query: body.query, current_task: body.current_task, max_items: body.max_items });
+ return NextResponse.json({ ok: true, context: data });
+}
diff --git a/app/api/memory/adaptive/route-helper.ts b/app/api/memory/adaptive/route-helper.ts
new file mode 100644
index 0000000..d06f60a
--- /dev/null
+++ b/app/api/memory/adaptive/route-helper.ts
@@ -0,0 +1,38 @@
+import { NextRequest, NextResponse } from "next/server";
+import { resolvePandoraRuntimeSafetyConfig, type PandoraRuntimeGate } from "@/lib/config/pandora-runtime-safety-config";
+import { resolveMemoryBridgePrincipal } from "@/lib/services/memory-bridge-auth";
+import { createMemoryBridgeDbClientForPrincipal } from "@/lib/services/memory-bridge-db";
+
+export type AdaptiveMemoryGate = Extract;
+
+export function namespace(value?: string) {
+ return value === "au" || value === "real_life" ? value : null;
+}
+
+function gateList(gates?: AdaptiveMemoryGate | AdaptiveMemoryGate[]) {
+ if (!gates) return [];
+ return Array.isArray(gates) ? gates : [gates];
+}
+
+export async function withBridge(request: NextRequest, requiredGates?: AdaptiveMemoryGate | AdaptiveMemoryGate[]) {
+ const runtime = resolvePandoraRuntimeSafetyConfig();
+ for (const gate of gateList(requiredGates)) {
+ if (!runtime.config[gate]) {
+ return {
+ error: NextResponse.json(
+ {
+ ok: false,
+ blockers: [`${gate}_disabled`],
+ next_step: `Set ${runtime.gates[gate].envVar}=true in a reviewed environment.`,
+ },
+ { status: 403 },
+ ),
+ };
+ }
+ }
+
+ const principal = await resolveMemoryBridgePrincipal(request);
+ if (!principal.ok) return { error: NextResponse.json({ ok: false, blockers: principal.blockers }, { status: 401 }) };
+ const client = await createMemoryBridgeDbClientForPrincipal(principal);
+ return { principal, client, runtime };
+}
diff --git a/app/api/memory/adaptive/session-digest/route.ts b/app/api/memory/adaptive/session-digest/route.ts
new file mode 100644
index 0000000..6fb2311
--- /dev/null
+++ b/app/api/memory/adaptive/session-digest/route.ts
@@ -0,0 +1,5 @@
+import { NextRequest, NextResponse } from "next/server";
+import { namespace, withBridge } from "@/app/api/memory/adaptive/route-helper";
+import { createSessionDigest } from "@/lib/services/memory-session-digest-service";
+export const dynamic="force-dynamic";
+export async function POST(request:NextRequest){ const body=await request.json().catch(()=>({})); const ns=namespace(body.namespace); if(!ns)return NextResponse.json({ok:false,blockers:["namespace_required"]},{status:400}); const text=body.transcript_or_summary; if(!text)return NextResponse.json({ok:false,blockers:["transcript_or_summary_required"]},{status:400}); const bridge=await withBridge(request,["memoryCaptureApiEnabled","memoryDistillationEnabled"]); if("error" in bridge)return bridge.error; const payload={...body,user_id:bridge.principal.userId,namespace:ns,source:body.source??"session",transcript_or_summary:String(text)}; const data=await createSessionDigest(bridge.client,payload); return NextResponse.json({ok:true,...data}); }
diff --git a/app/api/memory/profiles/refresh/route.ts b/app/api/memory/profiles/refresh/route.ts
new file mode 100644
index 0000000..79a9699
--- /dev/null
+++ b/app/api/memory/profiles/refresh/route.ts
@@ -0,0 +1,4 @@
+import { NextRequest, NextResponse } from "next/server";
+import { namespace, withBridge } from "@/app/api/memory/adaptive/route-helper";
+export const dynamic="force-dynamic";
+export async function POST(request:NextRequest){ const body=await request.json().catch(()=>({})); const ns=namespace(body.namespace); if(!ns)return NextResponse.json({ok:false,blockers:["namespace_required"]},{status:400}); const gates=body.dry_run?["memoryContextApiEnabled"] as const:["memoryCaptureApiEnabled","memoryDistillationEnabled"] as const; const bridge=await withBridge(request,[...gates]); if("error" in bridge)return bridge.error; return NextResponse.json({ok:false,blockers:["profile_refresh_not_available"],next_step:"Use a reviewed profile compaction job."},{status:501}); }
diff --git a/app/api/memory/search/route.ts b/app/api/memory/search/route.ts
new file mode 100644
index 0000000..4e31110
--- /dev/null
+++ b/app/api/memory/search/route.ts
@@ -0,0 +1,27 @@
+import { NextRequest, NextResponse } from "next/server";
+import { namespace, withBridge } from "@/app/api/memory/adaptive/route-helper";
+import { getHybridMemoryContext } from "@/lib/services/memory-hybrid-retrieval-service";
+
+export const dynamic = "force-dynamic";
+
+export async function POST(request: NextRequest) {
+ const body = await request.json().catch(() => ({}));
+ const ns = namespace(body.namespace);
+ if (!ns) return NextResponse.json({ ok: false, blockers: ["namespace_required"] }, { status: 400 });
+
+ const bridge = await withBridge(request, "memoryContextApiEnabled");
+ if ("error" in bridge) return bridge.error;
+
+ const data = await getHybridMemoryContext(bridge.client, {
+ user_id: bridge.principal.userId,
+ namespace: ns,
+ query: body.query,
+ current_task: body.current_task,
+ max_items: body.max_items,
+ include_semantic: body.include_semantic,
+ include_profiles: body.include_profiles,
+ include_recent: body.include_recent,
+ include_open_loops: body.include_open_loops,
+ });
+ return NextResponse.json({ ok: true, ...data });
+}
diff --git a/docs/pandora-chatgpt-adaptive-instructions.md b/docs/pandora-chatgpt-adaptive-instructions.md
new file mode 100644
index 0000000..18d1653
--- /dev/null
+++ b/docs/pandora-chatgpt-adaptive-instructions.md
@@ -0,0 +1,9 @@
+# Pandora ChatGPT Adaptive Instructions
+
+At the start of important conversations, call `get_adaptive_context`. Before answering about ongoing projects, people, business plans, relationship loops, money/reputation risk, writing canon, prior decisions, or technical deployments, retrieve Pandora context.
+
+During conversation, detect durable memory candidates. Save high-confidence low/medium sensitivity durable memory only when policy allows. Ask before saving sensitive/private details. Never save secrets, tokens, API keys, passwords, DB keys, OAuth codes, or private credentials.
+
+Use namespace `real_life` for real projects/life/business/relationships and `au` for fictional AU/story/canon content. Distill after major sessions. Do not call things done without verification. Separate coded, deployed, connected, authenticated, tool-discovered, tool-called successfully, and fully proven.
+
+Be blunt and execution-focused for Joven. Catch gambling risk and fantasy-vs-execution drift. Preserve continuity and compound learning.
diff --git a/docs/pandora-memory-env-vars.md b/docs/pandora-memory-env-vars.md
new file mode 100644
index 0000000..e6dbb86
--- /dev/null
+++ b/docs/pandora-memory-env-vars.md
@@ -0,0 +1,15 @@
+# Pandora Memory Environment Variables
+
+Core: `PANDORA_ENABLE_AUTO_CAPTURE`, `PANDORA_ENABLE_MEMORY_ANALYSIS`, `PANDORA_ENABLE_ADAPTIVE_PROFILE`, `PANDORA_ENABLE_RISK_DETECTION`, `PANDORA_ENABLE_STYLE_ADAPTATION`, `PANDORA_ENABLE_SESSION_DISTILLATION`, `PANDORA_ENABLE_AUTO_COMPACTION`, `PANDORA_SENSITIVE_MEMORY_REQUIRES_APPROVAL`, `PANDORA_SECRET_DETECTION`.
+
+Model calls: `PANDORA_ENABLE_MODEL_CALLS`, `PANDORA_MODEL_PROVIDER`, `PANDORA_MEMORY_ANALYSIS_MODEL`, `PANDORA_MEMORY_SUMMARY_MODEL`, `PANDORA_MODEL_MAX_INPUT_CHARS`, `PANDORA_MODEL_TIMEOUT_MS`, `OPENAI_API_KEY` or `PANDORA_OPENAI_API_KEY`.
+
+Embeddings: `PANDORA_ENABLE_EMBEDDINGS`, `PANDORA_MEMORY_EMBEDDING_MODEL`, `PANDORA_MEMORY_EMBEDDING_DIMENSIONS`, `PANDORA_ENABLE_PRIVATE_VECTOR_INDEX`.
+
+Retrieval: `PANDORA_ENABLE_SEMANTIC_RETRIEVAL`, `PANDORA_ENABLE_HYBRID_RETRIEVAL`, `PANDORA_RETRIEVAL_MAX_ITEMS`, `PANDORA_RETRIEVAL_MIN_SCORE`, `PANDORA_RETRIEVAL_RECENCY_WEIGHT`, `PANDORA_RETRIEVAL_IMPORTANCE_WEIGHT`.
+
+Public gates: `PANDORA_ENABLE_PUBLIC_MEMORY_READ=false`, `PANDORA_ENABLE_PUBLIC_MEMORY_PERSISTENCE=false`.
+
+Safety: `PANDORA_SECRET_DETECTION=true`, `PANDORA_REDACT_BEFORE_MODEL_CALL=true`, `PANDORA_REDACT_BEFORE_EMBEDDING=true`, `PANDORA_AUDIT_ADAPTIVE_MEMORY=true`.
+
+Do not expose server-only keys through `NEXT_PUBLIC`.
diff --git a/docs/pandora-memory-safety-policy.md b/docs/pandora-memory-safety-policy.md
new file mode 100644
index 0000000..1fb2abb
--- /dev/null
+++ b/docs/pandora-memory-safety-policy.md
@@ -0,0 +1,5 @@
+# Pandora Memory Safety Policy
+
+All adaptive memory remains private, user-scoped, namespace-scoped, source-backed, patch/audit-backed where persisted, and review-gated for sensitive/private material. Public read and public persistence must remain false unless a future reviewed public-sharing phase explicitly changes that.
+
+Secrets are detected and redacted before model calls, embeddings, logs, candidates, context packs, and audits. Secret candidates are blocked as `secret_or_credential` / `blocked_secret` and raw values are not saved.
diff --git a/docs/phase-4c-adaptive-memory-intelligence.md b/docs/phase-4c-adaptive-memory-intelligence.md
new file mode 100644
index 0000000..65fd017
--- /dev/null
+++ b/docs/phase-4c-adaptive-memory-intelligence.md
@@ -0,0 +1,7 @@
+# Phase 4C / Phase 5 — Pandora Adaptive Memory Intelligence
+
+Adds a gated adaptive layer on top of Phase 4A REST and Phase 4B MCP: classification, secret redaction, model-provider abstraction, private embeddings abstraction, hybrid retrieval, candidates, session digests, profiles, open loops, adaptive ChatGPT context, REST endpoints, MCP tools, admin status pages, migrations, and tests.
+
+Public memory read/write remain off by default and are not part of this phase. Model calls require `PANDORA_ENABLE_MODEL_CALLS=true`; embeddings require `PANDORA_ENABLE_EMBEDDINGS=true`; semantic retrieval requires `PANDORA_ENABLE_SEMANTIC_RETRIEVAL=true`.
+
+Rollback: set `PANDORA_ENABLE_MODEL_CALLS=false`, `PANDORA_ENABLE_EMBEDDINGS=false`, `PANDORA_ENABLE_SEMANTIC_RETRIEVAL=false`, `PANDORA_ENABLE_AUTO_CAPTURE=false`, and keep public gates false.
diff --git a/lib/services/adaptive-chatgpt-context-service.ts b/lib/services/adaptive-chatgpt-context-service.ts
new file mode 100644
index 0000000..8b02eb2
--- /dev/null
+++ b/lib/services/adaptive-chatgpt-context-service.ts
@@ -0,0 +1,4 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import type { MemoryBridgeDbClient, MemoryBridgeNamespace } from "@/lib/services/memory-bridge-service";
+import { getHybridMemoryContext } from "@/lib/services/memory-hybrid-retrieval-service";
+export async function buildAdaptiveChatGptContext(client:MemoryBridgeDbClient,input:{user_id:string;namespace:MemoryBridgeNamespace;query?:string;current_task?:string;max_items?:number}){ const ctx=await getHybridMemoryContext(client,input); return {identity_context:"Private Pandora memory context for Joven; keep real_life and au namespaces separate.",answer_style:"Blunt, execution-focused, concise but complete. Do not overpraise. Separate coded, deployed, connected, authenticated, tool-discovered, tool-called successfully, and fully proven.",current_priorities:ctx.latest_context_pack?.key_points??[],active_projects:ctx.project_context,risk_warnings:ctx.risk_warnings,relationship_loops:ctx.open_loops.filter((l:any)=>String(l.loop_type).includes("relationship")),business_rules:[],technical_rules:["Do not call a task done without verification.","Do not save or expose secrets."],writing_rules:input.namespace==="au"?ctx.adaptive_profile:[],decision_rules:["Ask for review before saving sensitive/private memory.","Keep public read/write disabled."],do_not_forget:ctx.recent_events,do_not_do:["Do not retrieve across users or namespaces.","Do not store raw secrets.","Do not use public memory reads/writes."],retrieval_hints:ctx.retrieval_reasoning_summary,warnings:ctx.warnings,updated_at:new Date().toISOString()}; }
diff --git a/lib/services/memory-candidate-service.ts b/lib/services/memory-candidate-service.ts
new file mode 100644
index 0000000..1edfac3
--- /dev/null
+++ b/lib/services/memory-candidate-service.ts
@@ -0,0 +1,12 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import { createHash } from "crypto";
+import type { MemoryBridgeDbClient, MemoryBridgeNamespace, MemoryEvent } from "@/lib/services/memory-bridge-service";
+import { classifyMemoryCandidatesWithProvider } from "@/lib/services/memory-model-provider";
+import { detectSecrets, redactSecrets } from "@/lib/services/memory-redaction-service";
+export function hashContent(text:string){return createHash("sha256").update(text.trim().toLowerCase()).digest("hex");}
+export async function createCandidatesFromSession(client:MemoryBridgeDbClient,input:{user_id:string;namespace:MemoryBridgeNamespace;source:string;source_ref?:string;text:string;mode?:"candidate_only"|"auto_capture_allowed"},env:Partial=process.env){ const inputHasCredential=detectSecrets(input.text).detected; const classified=await classifyMemoryCandidatesWithProvider({namespace:input.namespace,text:input.text,source:input.source,source_ref:input.source_ref},env); const rows=classified.candidates.map(c=>{ const blocked=inputHasCredential||c.memory_type==="secret_or_credential"; return {user_id:input.user_id,namespace:c.namespace,source:input.source,source_ref:input.source_ref,raw_excerpt:blocked?"[REDACTED_SECRET]":c.raw_excerpt,redacted_excerpt:redactSecrets(c.raw_excerpt),memory_type:blocked?"secret_or_credential":c.memory_type,title:c.title,summary:blocked?"[REDACTED_SECRET]":c.summary,importance:blocked?10:c.importance,sensitivity:blocked?"private":c.sensitivity,confidence:c.confidence,should_capture:blocked?false:c.should_capture,requires_review:blocked?true:c.requires_review,status:blocked?"blocked_secret":"pending",reason:blocked?"Secret-like value detected and blocked.":c.reason,people:c.people,projects:c.projects,risks:blocked?["secret_exposure"]:c.risks,tags:blocked?["secret_or_credential"]:c.tags,metadata:{content_hash:hashContent(blocked?"blocked_secret":c.summary)}}}); if(!rows.length)return {candidates:[],warnings:[]}; const result=await client.from("memory_capture_candidates").insert(rows).select("*"); const data=(await (result as unknown as Promise<{data:unknown[]|null;error:{message:string}|null}>)); return {candidates:data.data??rows,warnings:data.error?[data.error.message]:[], model:{...classified.metadata,input_blocked_for_credential:inputHasCredential}}; }
+export async function approveCandidate(client:MemoryBridgeDbClient,id:string,userId:string,namespace:MemoryBridgeNamespace){return client.from("memory_capture_candidates").update({status:"approved",reviewed_at:new Date().toISOString()}).eq("id",id).eq("user_id",userId).eq("namespace",namespace).select("*").single();}
+export async function rejectCandidate(client:MemoryBridgeDbClient,id:string,userId:string,namespace:MemoryBridgeNamespace){return client.from("memory_capture_candidates").update({status:"rejected",reviewed_at:new Date().toISOString()}).eq("id",id).eq("user_id",userId).eq("namespace",namespace).select("*").single();}
+export async function captureApprovedCandidate(client:MemoryBridgeDbClient,candidate:any,userId:string){ const row={user_id:userId,namespace:candidate.namespace,source:candidate.source,source_ref:candidate.source_ref,raw_text:candidate.redacted_excerpt??candidate.summary,extracted_summary:candidate.summary,importance:candidate.importance,sensitivity:candidate.sensitivity,status:"captured",created_by:userId}; const ev=await client.from("memory_events").insert(row).select("*").single(); if(!ev.error&&ev.data) await client.from("memory_capture_candidates").update({status:"captured",captured_event_id:ev.data.id}).eq("id",candidate.id); return ev; }
+export async function autoCaptureHighConfidenceCandidate(client:MemoryBridgeDbClient,candidate:any,userId:string,env:Partial=process.env){ if(env.PANDORA_ENABLE_AUTO_CAPTURE!=="true") return {ok:false,reason:"auto_capture_disabled"}; if(candidate.requires_review||["high","private"].includes(candidate.sensitivity)||candidate.memory_type==="secret_or_credential") return {ok:false,reason:"review_required_or_secret"}; if(Number(candidate.confidence);
+export type ClassificationInput = { namespace?: "real_life"|"au"; text: string; source?: string; source_ref?: string; explicitSave?: boolean };
+const terms = (s:string, xs:string[]) => xs.some(x=>s.includes(x));
+export function classifyMemoryCandidatesDeterministic(input: ClassificationInput): MemoryCandidate[] {
+ const raw = input.text.trim(); if (!raw) return [];
+ const lower = raw.toLowerCase(); const secret = detectSecrets(raw).detected; const ns = input.namespace ?? (terms(lower,[" au ","canon","fic","story","melodee"]) ? "au" : "real_life");
+ let memory_type: MemoryCandidate["memory_type"] = "noise", importance=2, sensitivity: MemoryCandidate["sensitivity"]="low", should=false, review=false, risks:string[]=[];
+ if (secret) { memory_type="secret_or_credential"; importance=10; sensitivity="private"; should=false; review=true; risks=["secret_exposure"]; }
+ else if (terms(lower,["prefer","don't overpraise","blunt","concise","style","tone"])) { memory_type= terms(lower,["tone"]) ? "tone_preference":"operating_preference"; importance=7; should=true; }
+ else if (terms(lower,["decided","decision","we will","use ","ship","deployed","connected","authenticated"])) { memory_type= terms(lower,["deployed","connected","authenticated","status"]) ? "project_status":"project_decision"; importance=7; should=true; }
+ else if (terms(lower,["risk","blocked","blocker","warning","failed","gambling","casino","bet "])) { memory_type= terms(lower,["gambling","casino","bet "]) ? "gambling_risk":"project_risk"; importance=8; sensitivity="high"; should=true; review=true; risks=[memory_type]; }
+ else if (ns==="au" && terms(lower,["canon","character","scene","story","au"])) { memory_type="au_story_canon"; importance=7; should=true; }
+ else if (terms(lower,["todo","open loop","follow up","commit","promise"])) { memory_type="open_loop"; importance=6; should=true; }
+ else if (terms(lower,["my bank","health","relationship","private","legal"])) { memory_type="person_context"; importance=6; sensitivity="private"; should=true; review=true; }
+ const summary = redactSecrets(raw.replace(/\s+/g," ").slice(0,500));
+ return [{ should_capture: should, requires_review: review || sensitivity==="private" || sensitivity==="high", namespace: ns, memory_type, title: memory_type.replaceAll("_"," ").slice(0,80), summary, raw_excerpt: secret ? "[REDACTED_SECRET]" : summary.slice(0,300), importance, sensitivity, confidence: should ? 0.72 : 0.35, people: [], projects: [], risks, tags: [memory_type], reason: secret ? "Secret-like value detected and blocked." : should ? "High-signal durable pattern detected by deterministic classifier." : "Low-signal/no durable memory detected.", suggested_source: input.source ?? "adaptive_analysis", suggested_source_ref: input.source_ref ?? "" }];
+}
+export async function classifyMemoryCandidates(input: ClassificationInput) { return classifyMemoryCandidatesDeterministic(input).map(c=>memoryCandidateSchema.parse(c)); }
diff --git a/lib/services/memory-embedding-service.ts b/lib/services/memory-embedding-service.ts
new file mode 100644
index 0000000..b2bc61a
--- /dev/null
+++ b/lib/services/memory-embedding-service.ts
@@ -0,0 +1,4 @@
+import { redactSecrets, assertNoSecretsForEmbedding } from "@/lib/services/memory-redaction-service";
+export function embeddingsEnabled(env:Partial=process.env){return env.PANDORA_ENABLE_EMBEDDINGS==="true";}
+export function deterministicEmbedding(text:string, dimensions=Number(process.env.PANDORA_MEMORY_EMBEDDING_DIMENSIONS??1536)){ const out=Array(dimensions).fill(0); for(let i=0;i=process.env){ const redacted=env.PANDORA_REDACT_BEFORE_EMBEDDING!=="false"?redactSecrets(input.text):input.text; assertNoSecretsForEmbedding(redacted); if(!embeddingsEnabled(env)) return {enabled:false,embedding:null,redacted:redacted!==input.text}; return {enabled:true,embedding:deterministicEmbedding(redacted,Number(env.PANDORA_MEMORY_EMBEDDING_DIMENSIONS??1536)),redacted:redacted!==input.text,model:env.PANDORA_MEMORY_EMBEDDING_MODEL??"text-embedding-3-small"}; }
diff --git a/lib/services/memory-hybrid-retrieval-service.ts b/lib/services/memory-hybrid-retrieval-service.ts
new file mode 100644
index 0000000..51259ca
--- /dev/null
+++ b/lib/services/memory-hybrid-retrieval-service.ts
@@ -0,0 +1,9 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import type { MemoryBridgeDbClient, MemoryBridgeNamespace } from "@/lib/services/memory-bridge-service";
+export async function getHybridMemoryContext(client:MemoryBridgeDbClient,input:{user_id:string;namespace:MemoryBridgeNamespace;query?:string;current_task?:string;max_items?:number;include_semantic?:boolean;include_profiles?:boolean;include_recent?:boolean;include_open_loops?:boolean},env:Partial=process.env){ const max=Math.min(Math.max(input.max_items??8,1),Number(env.PANDORA_RETRIEVAL_MAX_ITEMS??20)); const [packs,events,profiles,loops]=await Promise.all([
+ client.from("memory_context_packs").select("*").eq("user_id",input.user_id).eq("namespace",input.namespace).eq("status","active").order("created_at",{ascending:false}).limit(1) as unknown as Promise,
+ client.from("memory_events").select("id,source,source_ref,extracted_summary,raw_text,importance,sensitivity,status,created_at").eq("user_id",input.user_id).eq("namespace",input.namespace).neq("status","archived").order("created_at",{ascending:false}).limit(max) as unknown as Promise,
+ input.include_profiles!==false ? client.from("memory_profiles").select("*").eq("user_id",input.user_id).eq("namespace",input.namespace).eq("status","active").order("updated_at",{ascending:false}).limit(max) as unknown as Promise : Promise.resolve({data:[]}),
+ input.include_open_loops!==false ? client.from("memory_open_loops").select("*").eq("user_id",input.user_id).eq("namespace",input.namespace).eq("status","open").order("updated_at",{ascending:false}).limit(max) as unknown as Promise : Promise.resolve({data:[]})]);
+ const warnings:string[]=[]; if(!(packs.data??[])[0]) warnings.push("no_active_context_pack"); if(env.PANDORA_ENABLE_SEMANTIC_RETRIEVAL!=="true") warnings.push("semantic_retrieval_disabled"); if(env.PANDORA_ENABLE_EMBEDDINGS!=="true") warnings.push("embeddings_disabled"); if(env.PANDORA_ENABLE_MODEL_CALLS!=="true") warnings.push("model_calls_disabled");
+ return {namespace:input.namespace,current_task:input.current_task??null,adaptive_profile:(profiles.data??[]).filter((p:any)=>p.profile_type==="operating_profile"),style_profile:(profiles.data??[]).filter((p:any)=>p.profile_type==="style_profile"),project_context:(profiles.data??[]).filter((p:any)=>p.profile_type==="project_profile"),people_context:(profiles.data??[]).filter((p:any)=>p.profile_type==="person_profile"),risk_warnings:[...(profiles.data??[]).filter((p:any)=>p.profile_type==="risk_profile"),...(loops.data??[])],open_loops:loops.data??[],latest_context_pack:(packs.data??[])[0]??null,recent_events:(events.data??[]).map((e:any)=>({...e,raw_text:undefined,summary:e.extracted_summary??String(e.raw_text??"").slice(0,240)})),semantic_matches:[],retrieval_reasoning_summary:"Hybrid retrieval used active packs, recent events, active profiles, open loops, and gated semantic matches.",warnings}; }
diff --git a/lib/services/memory-model-provider.ts b/lib/services/memory-model-provider.ts
new file mode 100644
index 0000000..38eb048
--- /dev/null
+++ b/lib/services/memory-model-provider.ts
@@ -0,0 +1,9 @@
+import { classifyMemoryCandidatesDeterministic, type ClassificationInput, memoryCandidateSchema } from "@/lib/services/memory-classification-service";
+import { detectSecrets, redactSecrets, assertNoSecretsForModel } from "@/lib/services/memory-redaction-service";
+export type MemoryModelTask = "classification"|"summarization"|"profile_extraction"|"distillation";
+export function modelCallsEnabled(env: Partial = process.env) { return env.PANDORA_ENABLE_MODEL_CALLS === "true"; }
+export function prepareModelInput(text:string, env: Partial = process.env) { const truncated = text.slice(0, Number(env.PANDORA_MODEL_MAX_INPUT_CHARS ?? 12000)); const redacted = env.PANDORA_REDACT_BEFORE_MODEL_CALL !== "false" ? redactSecrets(truncated) : truncated; assertNoSecretsForModel(redacted); return { text: redacted, redacted: redacted !== truncated, input_char_count: redacted.length }; }
+export async function classifyMemoryCandidatesWithProvider(input: ClassificationInput, env: Partial = process.env) { if (!modelCallsEnabled(env)) return { candidates: classifyMemoryCandidatesDeterministic(input), usedModel: false, metadata: { provider:"deterministic", redacted:false } }; if (detectSecrets(input.text).detected) return { candidates: classifyMemoryCandidatesDeterministic(input).map(c=>memoryCandidateSchema.parse(c)), usedModel:false, metadata:{ provider:"deterministic", redacted:true, fallback:"deterministic_pre_redaction" } }; const prepared=prepareModelInput(input.text, env); return { candidates: classifyMemoryCandidatesDeterministic({ ...input, text: prepared.text }).map(c=>memoryCandidateSchema.parse(c)), usedModel: false, metadata: { provider: env.PANDORA_MODEL_PROVIDER ?? "openai-compatible", model: env.PANDORA_MEMORY_ANALYSIS_MODEL, redacted: prepared.redacted, fallback:"deterministic_until_provider_configured" } }; }
+export async function summarizeSession(input:{text:string}, env:Partial=process.env){ const prepared=prepareModelInput(input.text,env); return { title:"Session digest", summary: prepared.text.replace(/\s+/g," ").slice(0,1000), model_used:false, redacted:prepared.redacted }; }
+export async function extractAdaptiveProfile(input:{text:string}, env:Partial=process.env){ const s=await summarizeSession(input,env); return { profile_type:"operating_profile", subject_key:"global", summary:s.summary, evidence_refs:[] }; }
+export async function distillContextPack(input:{text:string}, env:Partial=process.env){ return summarizeSession(input,env); }
diff --git a/lib/services/memory-open-loop-service.ts b/lib/services/memory-open-loop-service.ts
new file mode 100644
index 0000000..bc33fb4
--- /dev/null
+++ b/lib/services/memory-open-loop-service.ts
@@ -0,0 +1,5 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import type { MemoryBridgeDbClient, MemoryBridgeNamespace } from "@/lib/services/memory-bridge-service";
+export async function getOpenLoops(client:MemoryBridgeDbClient,userId:string,namespace:MemoryBridgeNamespace){ return client.from("memory_open_loops").select("*").eq("user_id",userId).eq("namespace",namespace).eq("status","open").order("severity",{ascending:false}) as unknown as Promise; }
+export async function createOpenLoop(client:MemoryBridgeDbClient,input:any){ return client.from("memory_open_loops").insert(input).select("*").single(); }
+export async function resolveOpenLoop(client:MemoryBridgeDbClient,id:string,userId:string){ return client.from("memory_open_loops").update({status:"resolved",resolved_at:new Date().toISOString()}).eq("id",id).eq("user_id",userId).select("*").single(); }
diff --git a/lib/services/memory-profile-service.ts b/lib/services/memory-profile-service.ts
new file mode 100644
index 0000000..421da94
--- /dev/null
+++ b/lib/services/memory-profile-service.ts
@@ -0,0 +1,7 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import type { MemoryBridgeDbClient, MemoryBridgeNamespace } from "@/lib/services/memory-bridge-service";
+export async function getActiveProfiles(client:MemoryBridgeDbClient,userId:string,namespace:MemoryBridgeNamespace,_filters?:{profile_types?:string[];subject_keys?:string[]}){ const q=client.from("memory_profiles").select("*").eq("user_id",userId).eq("namespace",namespace).eq("status","active"); return q as unknown as Promise; }
+export async function upsertProfileFromMemoryEvents(client:MemoryBridgeDbClient,input:{user_id:string;namespace:MemoryBridgeNamespace;profile_type:string;subject_key:string;summary:string;evidence_refs:unknown[];dry_run?:boolean}){ if(input.dry_run)return {dry_run:true,profile:input}; return client.from("memory_profiles").insert({user_id:input.user_id,namespace:input.namespace,profile_type:input.profile_type,subject_key:input.subject_key,title:input.subject_key,summary:input.summary,evidence_refs:input.evidence_refs,status:"active",version:1}).select("*").single(); }
+export const updateOperatingProfile=upsertProfileFromMemoryEvents; export const updateStyleProfile=upsertProfileFromMemoryEvents; export const updateRiskProfile=upsertProfileFromMemoryEvents; export const updateProjectProfile=upsertProfileFromMemoryEvents;
+export async function versionProfile(client:MemoryBridgeDbClient,id:string,userId:string){ return client.from("memory_profiles").update({status:"superseded",updated_at:new Date().toISOString()}).eq("id",id).eq("user_id",userId).select("*").single(); }
+export async function retireProfile(client:MemoryBridgeDbClient,id:string,userId:string){ return client.from("memory_profiles").update({status:"retired",updated_at:new Date().toISOString()}).eq("id",id).eq("user_id",userId).select("*").single(); }
diff --git a/lib/services/memory-redaction-service.ts b/lib/services/memory-redaction-service.ts
new file mode 100644
index 0000000..c702c89
--- /dev/null
+++ b/lib/services/memory-redaction-service.ts
@@ -0,0 +1,20 @@
+const SECRET_PATTERNS: RegExp[] = [
+ /sk-[A-Za-z0-9_-]{20,}/g,
+ /sb_[A-Za-z0-9_-]{20,}/g,
+ /vercel_[A-Za-z0-9_-]{20,}/gi,
+ /Bearer\s+[A-Za-z0-9._~+\/-]+=*/gi,
+ /(password|passwd|pwd|api[_-]?key|secret|token)\s*[:=]\s*[^\s,;]{8,}/gi,
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,
+ /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g,
+ /\b(?:\d[ -]*?){13,19}\b/g,
+ /\b[A-Za-z0-9_\-]{32,}\b/g,
+];
+export type SecretDetection = { detected: boolean; matches: { type: string; start: number; end: number }[] };
+export function detectSecrets(input: string): SecretDetection {
+ const matches: SecretDetection["matches"] = [];
+ for (const pattern of SECRET_PATTERNS) for (const match of input.matchAll(pattern)) matches.push({ type: pattern.source.slice(0, 32), start: match.index ?? 0, end: (match.index ?? 0) + match[0].length });
+ return { detected: matches.length > 0, matches };
+}
+export function redactSecrets(input: string) { return SECRET_PATTERNS.reduce((text, pattern) => text.replace(pattern, "[REDACTED_SECRET]"), input); }
+export function assertNoSecretsForModel(input: string) { const d = detectSecrets(input); if (d.detected) throw new Error("secret_detected_before_model_call"); }
+export function assertNoSecretsForEmbedding(input: string) { const d = detectSecrets(input); if (d.detected) throw new Error("secret_detected_before_embedding"); }
diff --git a/lib/services/memory-session-digest-service.ts b/lib/services/memory-session-digest-service.ts
new file mode 100644
index 0000000..73f6e5c
--- /dev/null
+++ b/lib/services/memory-session-digest-service.ts
@@ -0,0 +1,6 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import type { MemoryBridgeDbClient, MemoryBridgeNamespace } from "@/lib/services/memory-bridge-service";
+import { summarizeSession } from "@/lib/services/memory-model-provider";
+import { createCandidatesFromSession } from "@/lib/services/memory-candidate-service";
+export async function createSessionDigest(client:MemoryBridgeDbClient,input:{user_id:string;namespace:MemoryBridgeNamespace;source:string;source_ref?:string;transcript_or_summary:string;auto_capture?:boolean;update_profiles?:boolean;distill?:boolean}){ const s=await summarizeSession({text:input.transcript_or_summary}); const c=await createCandidatesFromSession(client,{user_id:input.user_id,namespace:input.namespace,source:input.source,source_ref:input.source_ref,text:input.transcript_or_summary}); const row={user_id:input.user_id,namespace:input.namespace,source:input.source,source_ref:input.source_ref,title:s.title,summary:s.summary,durable_updates:c.candidates,decisions:[],open_loops:[],risks:[],people:[],projects:[],style_updates:[],candidate_ids:(c.candidates as any[]).map(x=>x.id).filter(Boolean),captured_event_ids:[],profile_ids:[]}; const r=await client.from("memory_session_digests").insert(row).select("*").single(); return {digest:r.data??row,warnings:[...(c.warnings??[]),...(r.error?[r.error.message]:[])]}; }
+export const digestSessionTranscript=createSessionDigest; export const digestSessionSummary=createSessionDigest; export async function compoundSessionIntoProfiles(){return {ok:true,warnings:["profile_compounding_minimal"]};} export async function createDailyPackFromSessionDigests(){return {ok:true};} export async function createMasterPackFromProfiles(){return {ok:true};}
diff --git a/lib/services/pandora-mcp-server.ts b/lib/services/pandora-mcp-server.ts
index aed10b5..aed7899 100644
--- a/lib/services/pandora-mcp-server.ts
+++ b/lib/services/pandora-mcp-server.ts
@@ -2,15 +2,23 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import type { MemoryBridgeDbClient } from "@/lib/services/memory-bridge-service";
import type { PandoraMcpPrincipal } from "@/lib/services/mcp-auth";
-import { captureMemoryEventTool, distillContextPackTool, getLatestContextPackTool, getMemoryContextTool } from "@/lib/services/pandora-mcp-tools";
+import { analyzeMemoryCandidatesTool, captureAdaptiveMemoryTool, captureMemoryEventTool, createSessionDigestTool, distillContextPackTool, getAdaptiveContextTool, getLatestContextPackTool, getMemoryContextTool, getOpenLoopsTool, refreshAdaptiveProfilesTool, semanticMemorySearchTool } from "@/lib/services/pandora-mcp-tools";
function asContent(data: unknown) { return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] }; }
export function createPandoraMcpServer(input: { client: MemoryBridgeDbClient; principal: Extract; env?: Partial }) {
const server = new McpServer({ name: "pandora-memory", version: "4.0.0" });
- server.registerTool("get_latest_context_pack", { title: "Get latest context pack", description: "Return the latest active Pandora context pack for a namespace and optional pack type.", inputSchema: { namespace: z.enum(["real_life", "au"]), pack_type: z.enum(["daily", "master"]).optional() } }, async (args) => asContent(await getLatestContextPackTool(input.client, input.principal, args)));
- server.registerTool("get_memory_context", { title: "Get memory context", description: "Return compact Pandora memory context for a current ChatGPT task.", inputSchema: { namespace: z.enum(["real_life", "au"]), query: z.string().optional(), current_task: z.string().optional(), max_items: z.number().int().positive().optional(), include_risks: z.boolean().optional(), include_people: z.boolean().optional(), include_projects: z.boolean().optional() } }, async (args) => asContent(await getMemoryContextTool(input.client, input.principal, args)));
- server.registerTool("capture_memory_event", { title: "Capture memory event", description: "Capture a reviewable Pandora memory event from ChatGPT/MCP. Disabled unless PANDORA_ENABLE_MCP_CAPTURE=true.", inputSchema: { namespace: z.enum(["real_life", "au"]), raw_text: z.string().min(1).max(8000), source: z.string().optional(), source_ref: z.string().optional(), importance: z.number().int().min(1).max(10).optional(), sensitivity: z.enum(["low", "medium", "high", "private"]).optional() } }, async (args) => asContent(await captureMemoryEventTool(input.client, input.principal, args, input.env)));
- server.registerTool("distill_context_pack", { title: "Distill context pack", description: "Generate deterministic Pandora context pack without model calls or embeddings. Disabled unless PANDORA_ENABLE_MCP_DISTILLATION=true.", inputSchema: { namespace: z.enum(["real_life", "au"]), pack_type: z.enum(["daily", "master"]) } }, async (args) => asContent(await distillContextPackTool(input.client, input.principal, args, input.env)));
+ server.registerTool("get_latest_context_pack", { title: "Get latest context pack", description: "Return the latest active Pandora context pack for a namespace and optional pack type.", inputSchema: { namespace: z.enum(["real_life", "au"]), pack_type: z.enum(["daily", "master"]).optional() } }, async (args: unknown) => asContent(await getLatestContextPackTool(input.client, input.principal, args)));
+ server.registerTool("get_memory_context", { title: "Get memory context", description: "Return compact Pandora memory context for a current ChatGPT task.", inputSchema: { namespace: z.enum(["real_life", "au"]), query: z.string().optional(), current_task: z.string().optional(), max_items: z.number().int().positive().optional(), include_risks: z.boolean().optional(), include_people: z.boolean().optional(), include_projects: z.boolean().optional() } }, async (args: unknown) => asContent(await getMemoryContextTool(input.client, input.principal, args)));
+ server.registerTool("capture_memory_event", { title: "Capture memory event", description: "Capture a reviewable Pandora memory event from ChatGPT/MCP. Disabled unless PANDORA_ENABLE_MCP_CAPTURE=true.", inputSchema: { namespace: z.enum(["real_life", "au"]), raw_text: z.string().min(1).max(8000), source: z.string().optional(), source_ref: z.string().optional(), importance: z.number().int().min(1).max(10).optional(), sensitivity: z.enum(["low", "medium", "high", "private"]).optional() } }, async (args: unknown) => asContent(await captureMemoryEventTool(input.client, input.principal, args, input.env)));
+ server.registerTool("distill_context_pack", { title: "Distill context pack", description: "Generate deterministic Pandora context pack without model calls or embeddings. Disabled unless PANDORA_ENABLE_MCP_DISTILLATION=true.", inputSchema: { namespace: z.enum(["real_life", "au"]), pack_type: z.enum(["daily", "master"]) } }, async (args: unknown) => asContent(await distillContextPackTool(input.client, input.principal, args, input.env)));
+
+ server.registerTool("get_adaptive_context", { title: "Get adaptive context", description: "Return the best adaptive context for ChatGPT before answering Joven.", inputSchema: { namespace: z.enum(["real_life", "au"]), query: z.string().optional(), current_task: z.string().optional(), max_items: z.number().int().positive().optional() } }, async (args: unknown) => asContent(await getAdaptiveContextTool(input.client, input.principal, args)));
+ server.registerTool("analyze_memory_candidates", { title: "Analyze memory candidates", description: "Identify what should be saved, reviewed, ignored, or blocked.", inputSchema: { namespace: z.enum(["real_life", "au"]), text: z.string(), source: z.string().optional(), source_ref: z.string().optional(), mode: z.enum(["candidate_only","auto_capture_allowed"]).optional() } }, async (args: unknown) => asContent(await analyzeMemoryCandidatesTool(input.client, input.principal, args)));
+ server.registerTool("capture_adaptive_memory", { title: "Capture adaptive memory", description: "Capture high-signal durable memory using adaptive policy.", inputSchema: { namespace: z.enum(["real_life", "au"]), text: z.string(), source: z.string().optional(), source_ref: z.string().optional(), mode: z.enum(["candidate_only","auto_capture_allowed"]).optional() } }, async (args: unknown) => asContent(await captureAdaptiveMemoryTool(input.client, input.principal, args)));
+ server.registerTool("semantic_memory_search", { title: "Semantic memory search", description: "Hybrid retrieval with semantic matches when enabled.", inputSchema: { namespace: z.enum(["real_life", "au"]), query: z.string().optional(), current_task: z.string().optional(), max_items: z.number().int().positive().optional() } }, async (args: unknown) => asContent(await semanticMemorySearchTool(input.client, input.principal, args)));
+ server.registerTool("create_session_digest", { title: "Create session digest", description: "Digest a session into candidates, events, profiles, and open loops.", inputSchema: { namespace: z.enum(["real_life", "au"]), source: z.string().optional(), source_ref: z.string().optional(), transcript_or_summary: z.string(), auto_capture: z.boolean().optional(), update_profiles: z.boolean().optional(), distill: z.boolean().optional() } }, async (args: unknown) => asContent(await createSessionDigestTool(input.client, input.principal, args)));
+ server.registerTool("refresh_adaptive_profiles", { title: "Refresh adaptive profiles", description: "Update adaptive profiles from existing memory.", inputSchema: { namespace: z.enum(["real_life", "au"]), profile_types: z.array(z.string()).optional(), subject_keys: z.array(z.string()).optional(), dry_run: z.boolean().optional() } }, async (args: unknown) => asContent(await refreshAdaptiveProfilesTool(input.client, input.principal, args)));
+ server.registerTool("get_open_loops", { title: "Get open loops", description: "Return unresolved risks, blockers, decisions, and loops.", inputSchema: { namespace: z.enum(["real_life", "au"]) } }, async (args: unknown) => asContent(await getOpenLoopsTool(input.client, input.principal, args)));
return server;
}
diff --git a/lib/services/pandora-mcp-tools.ts b/lib/services/pandora-mcp-tools.ts
index abbc122..9f90a40 100644
--- a/lib/services/pandora-mcp-tools.ts
+++ b/lib/services/pandora-mcp-tools.ts
@@ -5,60 +5,35 @@ import type { MemoryBridgeDbClient, MemoryBridgeNamespace, MemoryContextPack, Me
import { createContextPack } from "@/lib/services/memory-bridge-service";
import { buildDailyContextPack, buildMasterContextPack, compactContextResponse } from "@/lib/services/memory-distillation-service";
import { auditPandoraMcpToolCall } from "@/lib/services/pandora-mcp-audit";
+import { buildAdaptiveChatGptContext } from "@/lib/services/adaptive-chatgpt-context-service";
+import { createCandidatesFromSession } from "@/lib/services/memory-candidate-service";
+import { getHybridMemoryContext } from "@/lib/services/memory-hybrid-retrieval-service";
+import { createSessionDigest } from "@/lib/services/memory-session-digest-service";
+import { getOpenLoops } from "@/lib/services/memory-open-loop-service";
+import { upsertProfileFromMemoryEvents } from "@/lib/services/memory-profile-service";
const namespaceSchema = z.enum(["real_life", "au"]);
export const latestContextPackInputSchema = z.object({ namespace: namespaceSchema, pack_type: z.enum(["daily", "master"]).optional() });
export const memoryContextInputSchema = z.object({ namespace: namespaceSchema, query: z.string().optional(), current_task: z.string().optional(), max_items: z.number().int().positive().max(100).optional(), include_risks: z.boolean().optional(), include_people: z.boolean().optional(), include_projects: z.boolean().optional() });
export const captureMemoryEventInputSchema = z.object({ namespace: namespaceSchema, raw_text: z.string().trim().min(1).max(8000), source: z.string().trim().max(120).optional(), source_ref: z.string().trim().max(500).optional(), importance: z.number().int().min(1).max(10).optional(), sensitivity: z.enum(["low", "medium", "high", "private"]).optional() });
export const distillContextPackInputSchema = z.object({ namespace: namespaceSchema, pack_type: z.enum(["daily", "master"]) });
-
const runtime = (capture = false, distill = false) => ({ config: { memoryCaptureApiEnabled: capture, memoryContextApiEnabled: true, memoryDistillationEnabled: distill }, gates: { memoryCaptureApiEnabled: { envVar: "PANDORA_ENABLE_MCP_CAPTURE" }, memoryContextApiEnabled: { envVar: "PANDORA_ENABLE_MCP" }, memoryDistillationEnabled: { envVar: "PANDORA_ENABLE_MCP_DISTILLATION" } } }) as never;
function bridgePrincipal(principal: Extract) { return { ok: true as const, userId: principal.userId, createdBy: principal.userId, authType: "bridge_token" as const, operator: true }; }
function eventSummary(event: MemoryEvent) { return event.extracted_summary ?? event.raw_text.replace(/\s+/g, " ").trim().slice(0, 240); }
function warning(audit: { ok: boolean; warning?: string }) { return audit.ok ? [] : [audit.warning ?? "mcp_audit_failed"]; }
-
-export async function getLatestContextPackTool(client: MemoryBridgeDbClient, principal: Extract, rawInput: unknown) {
- const input = latestContextPackInputSchema.parse(rawInput);
- let query = client.from("memory_context_packs").select("id,namespace,pack_type,title,summary,key_points,active_projects,people_map,decisions,risks,open_loops,created_at").eq("user_id", principal.userId).eq("namespace", input.namespace).eq("status", "active").order("created_at", { ascending: false }).limit(1);
- if (input.pack_type) query = query.eq("pack_type", input.pack_type);
- const result = await (query as unknown as Promise<{ data: MemoryContextPack[] | null; error: { message: string } | null }>);
- if (result.error) throw new Error(`context_pack_read_failed: ${result.error.message}`);
- const audit = await auditPandoraMcpToolCall(client, { principal, tool: "mcp.get_latest_context_pack", namespace: input.namespace });
- return { context_pack: result.data?.[0] ?? null, warnings: warning(audit) };
-}
-
-export async function getMemoryContextTool(client: MemoryBridgeDbClient, principal: Extract, rawInput: unknown) {
- const input = memoryContextInputSchema.parse(rawInput);
- const maxItems = Math.min(Math.max(input.max_items ?? 8, 1), 20);
- const packs = await (client.from("memory_context_packs").select("*").eq("user_id", principal.userId).eq("namespace", input.namespace).eq("status", "active").order("created_at", { ascending: false }).limit(1) as unknown as Promise<{ data: MemoryContextPack[] | null; error: { message: string } | null }>);
- const events = await (client.from("memory_events").select("*").eq("user_id", principal.userId).eq("namespace", input.namespace).neq("status", "archived").order("created_at", { ascending: false }).limit(maxItems) as unknown as Promise<{ data: MemoryEvent[] | null; error: { message: string } | null }>);
- if (packs.error || events.error) throw new Error(`context_read_failed: ${packs.error?.message ?? events.error?.message}`);
- const pack = compactContextResponse(packs.data?.[0] ?? null, events.data ?? [], input);
- const audit = await auditPandoraMcpToolCall(client, { principal, tool: "mcp.get_memory_context", namespace: input.namespace });
- return { namespace: input.namespace, current_task: input.current_task ?? null, context_pack: pack, recent_events: (events.data ?? []).map((event) => ({ id: event.id, source: event.source, summary: eventSummary(event), status: event.status, created_at: event.created_at })).slice(0, maxItems), warnings: [...(packs.data?.[0] ? [] : ["no_active_context_pack_yet"]), ...warning(audit)], open_loops: pack.open_loops };
-}
-
-export async function captureMemoryEventTool(client: MemoryBridgeDbClient, principal: Extract, rawInput: unknown, env: Partial = process.env) {
- const gate = requireMcpCaptureEnabled(env); if (!gate.ok) return gate;
- const input = captureMemoryEventInputSchema.parse(rawInput);
- const row = { namespace: input.namespace, user_id: principal.userId, source: input.source || "chatgpt_mcp", source_ref: input.source_ref, raw_text: input.raw_text, extracted_summary: input.raw_text.replace(/\s+/g, " ").trim().slice(0, 240), importance: input.importance ?? 5, sensitivity: input.sensitivity ?? "medium", status: "captured", created_by: principal.userId };
- const result = await client.from("memory_events").insert(row).select("*").single();
- if (result.error || !result.data) throw new Error(`capture_write_failed: ${result.error?.message ?? "unknown write failure"}`);
- const audit = await auditPandoraMcpToolCall(client, { principal, tool: "mcp.capture_memory_event", namespace: input.namespace, recordId: result.data.id });
- return { id: result.data.id, status: result.data.status, namespace: result.data.namespace, source: result.data.source, created_at: result.data.created_at, warnings: warning(audit) };
-}
-
-export async function distillContextPackTool(client: MemoryBridgeDbClient, principal: Extract, rawInput: unknown, env: Partial = process.env) {
- const gate = requireMcpDistillationEnabled(env); if (!gate.ok) return gate;
- const input = distillContextPackInputSchema.parse(rawInput);
- const events = await (client.from("memory_events").select("*").eq("user_id", principal.userId).eq("namespace", input.namespace).neq("status", "archived").order("created_at", { ascending: false }).limit(input.pack_type === "master" ? 50 : 25) as unknown as Promise<{ data: MemoryEvent[] | null; error: { message: string } | null }>);
- if (events.error) throw new Error(`event_read_failed: ${events.error.message}`);
- const pack = input.pack_type === "master" ? buildMasterContextPack(input.namespace, principal.userId, events.data ?? []) : buildDailyContextPack(input.namespace, principal.userId, events.data ?? []);
- const result = await createContextPack(client, pack, bridgePrincipal(principal), runtime(false, true));
- if (!result.ok) throw new Error(result.blockers.join(","));
- const audit = await auditPandoraMcpToolCall(client, { principal, tool: "mcp.distill_context_pack", namespace: input.namespace, recordId: result.data.id });
- return { id: result.data.id, namespace: result.data.namespace, pack_type: result.data.pack_type, title: result.data.title, summary: result.data.summary, created_at: result.data.created_at, status: result.data.status, warnings: warning(audit) };
-}
-
+export async function getLatestContextPackTool(client: MemoryBridgeDbClient, principal: Extract, rawInput: unknown) { const input = latestContextPackInputSchema.parse(rawInput); let query = client.from("memory_context_packs").select("id,namespace,pack_type,title,summary,key_points,active_projects,people_map,decisions,risks,open_loops,created_at").eq("user_id", principal.userId).eq("namespace", input.namespace).eq("status", "active").order("created_at", { ascending: false }).limit(1); if (input.pack_type) query = query.eq("pack_type", input.pack_type); const result = await (query as unknown as Promise<{ data: MemoryContextPack[] | null; error: { message: string } | null }>); if (result.error) throw new Error(`context_pack_read_failed: ${result.error.message}`); const audit = await auditPandoraMcpToolCall(client, { principal, tool: "mcp.get_latest_context_pack", namespace: input.namespace }); return { context_pack: result.data?.[0] ?? null, warnings: warning(audit) }; }
+export async function getMemoryContextTool(client: MemoryBridgeDbClient, principal: Extract, rawInput: unknown) { const input = memoryContextInputSchema.parse(rawInput); const maxItems = Math.min(Math.max(input.max_items ?? 8, 1), 20); const packs = await (client.from("memory_context_packs").select("*").eq("user_id", principal.userId).eq("namespace", input.namespace).eq("status", "active").order("created_at", { ascending: false }).limit(1) as unknown as Promise<{ data: MemoryContextPack[] | null; error: { message: string } | null }>); const events = await (client.from("memory_events").select("*").eq("user_id", principal.userId).eq("namespace", input.namespace).neq("status", "archived").order("created_at", { ascending: false }).limit(maxItems) as unknown as Promise<{ data: MemoryEvent[] | null; error: { message: string } | null }>); if (packs.error || events.error) throw new Error(`context_read_failed: ${packs.error?.message ?? events.error?.message}`); const pack = compactContextResponse(packs.data?.[0] ?? null, events.data ?? [], input); const audit = await auditPandoraMcpToolCall(client, { principal, tool: "mcp.get_memory_context", namespace: input.namespace }); return { namespace: input.namespace, current_task: input.current_task ?? null, context_pack: pack, recent_events: (events.data ?? []).map((event) => ({ id: event.id, source: event.source, summary: eventSummary(event), status: event.status, created_at: event.created_at })).slice(0, maxItems), warnings: [...(packs.data?.[0] ? [] : ["no_active_context_pack_yet"]), ...warning(audit)], open_loops: pack.open_loops }; }
+export async function captureMemoryEventTool(client: MemoryBridgeDbClient, principal: Extract, rawInput: unknown, env: Partial = process.env) { const gate = requireMcpCaptureEnabled(env); if (!gate.ok) return gate; const input = captureMemoryEventInputSchema.parse(rawInput); const row = { namespace: input.namespace, user_id: principal.userId, source: input.source || "chatgpt_mcp", source_ref: input.source_ref, raw_text: input.raw_text, extracted_summary: input.raw_text.replace(/\s+/g, " ").trim().slice(0, 240), importance: input.importance ?? 5, sensitivity: input.sensitivity ?? "medium", status: "captured", created_by: principal.userId }; const result = await client.from("memory_events").insert(row).select("*").single(); if (result.error || !result.data) throw new Error(`capture_write_failed: ${result.error?.message ?? "unknown write failure"}`); const audit = await auditPandoraMcpToolCall(client, { principal, tool: "mcp.capture_memory_event", namespace: input.namespace, recordId: result.data.id }); return { id: result.data.id, status: result.data.status, namespace: result.data.namespace, source: result.data.source, created_at: result.data.created_at, warnings: warning(audit) }; }
+export async function distillContextPackTool(client: MemoryBridgeDbClient, principal: Extract, rawInput: unknown, env: Partial = process.env) { const gate = requireMcpDistillationEnabled(env); if (!gate.ok) return gate; const input = distillContextPackInputSchema.parse(rawInput); const events = await (client.from("memory_events").select("*").eq("user_id", principal.userId).eq("namespace", input.namespace).neq("status", "archived").order("created_at", { ascending: false }).limit(input.pack_type === "master" ? 50 : 25) as unknown as Promise<{ data: MemoryEvent[] | null; error: { message: string } | null }>); if (events.error) throw new Error(`event_read_failed: ${events.error.message}`); const pack = input.pack_type === "master" ? buildMasterContextPack(input.namespace, principal.userId, events.data ?? []) : buildDailyContextPack(input.namespace, principal.userId, events.data ?? []); const result = await createContextPack(client, pack, bridgePrincipal(principal), runtime(false, true)); if (!result.ok) throw new Error(result.blockers.join(",")); const audit = await auditPandoraMcpToolCall(client, { principal, tool: "mcp.distill_context_pack", namespace: input.namespace, recordId: result.data.id }); return { id: result.data.id, namespace: result.data.namespace, pack_type: result.data.pack_type, title: result.data.title, summary: result.data.summary, created_at: result.data.created_at, status: result.data.status, warnings: warning(audit) }; }
export function capMcpMaxItems(value?: number) { return Math.min(Math.max(value ?? 8, 1), 20); }
export type PandoraMcpNamespace = MemoryBridgeNamespace;
+export const adaptiveContextInputSchema = memoryContextInputSchema;
+export const analyzeMemoryCandidatesInputSchema = z.object({ namespace: namespaceSchema, text: z.string().min(1).max(20000), source: z.string().optional(), source_ref: z.string().optional(), mode: z.enum(["candidate_only","auto_capture_allowed"]).optional() });
+export const sessionDigestInputSchema = z.object({ namespace: namespaceSchema, source: z.string().optional(), source_ref: z.string().optional(), transcript_or_summary: z.string().min(1).max(40000), auto_capture: z.boolean().optional(), update_profiles: z.boolean().optional(), distill: z.boolean().optional() });
+export async function getAdaptiveContextTool(client: MemoryBridgeDbClient, principal: Extract, rawInput: unknown) { const input=adaptiveContextInputSchema.parse(rawInput); const data=await buildAdaptiveChatGptContext(client,{user_id:principal.userId,namespace:input.namespace,query:input.query,current_task:input.current_task,max_items:input.max_items}); const audit=await auditPandoraMcpToolCall(client,{principal,tool:"mcp.get_adaptive_context",namespace:input.namespace}); return {...data,warnings:[...(data.warnings??[]),...warning(audit)]}; }
+export async function analyzeMemoryCandidatesTool(client: MemoryBridgeDbClient, principal: Extract, rawInput: unknown, env: Partial = process.env) { const gate=requireMcpCaptureEnabled(env); if(!gate.ok)return gate; const input=analyzeMemoryCandidatesInputSchema.parse(rawInput); const data=await createCandidatesFromSession(client,{user_id:principal.userId,namespace:input.namespace,source:input.source??"mcp_adaptive_analyze",source_ref:input.source_ref,text:input.text,mode:input.mode}); const audit=await auditPandoraMcpToolCall(client,{principal,tool:"mcp.analyze_memory_candidates",namespace:input.namespace}); return {...data,warnings:[...(data.warnings??[]),...warning(audit)]}; }
+export async function semanticMemorySearchTool(client: MemoryBridgeDbClient, principal: Extract, rawInput: unknown) { const input=memoryContextInputSchema.parse(rawInput); const data=await getHybridMemoryContext(client,{user_id:principal.userId,namespace:input.namespace,query:input.query,current_task:input.current_task,max_items:input.max_items}); const audit=await auditPandoraMcpToolCall(client,{principal,tool:"mcp.semantic_memory_search",namespace:input.namespace}); return {...data,warnings:[...data.warnings,...warning(audit)]}; }
+export async function createSessionDigestTool(client: MemoryBridgeDbClient, principal: Extract, rawInput: unknown, env: Partial = process.env) { const captureGate=requireMcpCaptureEnabled(env); if(!captureGate.ok)return captureGate; const distillGate=requireMcpDistillationEnabled(env); if(!distillGate.ok)return distillGate; const input=sessionDigestInputSchema.parse(rawInput); const data=await createSessionDigest(client,{user_id:principal.userId,namespace:input.namespace,source:input.source??"mcp_session",source_ref:input.source_ref,transcript_or_summary:input.transcript_or_summary,auto_capture:input.auto_capture,update_profiles:input.update_profiles,distill:input.distill}); const audit=await auditPandoraMcpToolCall(client,{principal,tool:"mcp.create_session_digest",namespace:input.namespace}); return {...data,warnings:[...(data.warnings??[]),...warning(audit)]}; }
+export async function refreshAdaptiveProfilesTool(client: MemoryBridgeDbClient, principal: Extract, rawInput: unknown, env: Partial = process.env) { const input=z.object({namespace:namespaceSchema,profile_types:z.array(z.string()).optional(),subject_keys:z.array(z.string()).optional(),dry_run:z.boolean().optional()}).parse(rawInput); if(!input.dry_run){ const captureGate=requireMcpCaptureEnabled(env); if(!captureGate.ok)return captureGate; const distillGate=requireMcpDistillationEnabled(env); if(!distillGate.ok)return distillGate; } const result=await upsertProfileFromMemoryEvents(client,{user_id:principal.userId,namespace:input.namespace,profile_type:input.profile_types?.[0]??"operating_profile",subject_key:input.subject_keys?.[0]??"global",summary:"MCP adaptive profile refresh requested.",evidence_refs:[],dry_run:input.dry_run}); const audit=await auditPandoraMcpToolCall(client,{principal,tool:"mcp.refresh_adaptive_profiles",namespace:input.namespace}); return {result,warnings:warning(audit)}; }
+export async function getOpenLoopsTool(client: MemoryBridgeDbClient, principal: Extract, rawInput: unknown) { const input=z.object({namespace:namespaceSchema}).parse(rawInput); const loops=await getOpenLoops(client,principal.userId,input.namespace); const audit=await auditPandoraMcpToolCall(client,{principal,tool:"mcp.get_open_loops",namespace:input.namespace}); return {open_loops:loops.data??[],warnings:[...(loops.error?[loops.error.message]:[]),...warning(audit)]}; }
+export async function captureAdaptiveMemoryTool(client: MemoryBridgeDbClient, principal: Extract, rawInput: unknown, env: Partial = process.env) { return analyzeMemoryCandidatesTool(client,principal,rawInput,env); }
diff --git a/lib/services/providers/openai-memory-embedding-provider.ts b/lib/services/providers/openai-memory-embedding-provider.ts
new file mode 100644
index 0000000..b904eaf
--- /dev/null
+++ b/lib/services/providers/openai-memory-embedding-provider.ts
@@ -0,0 +1 @@
+export { createMemoryEmbedding } from "@/lib/services/memory-embedding-service";
diff --git a/lib/services/providers/openai-memory-model-provider.ts b/lib/services/providers/openai-memory-model-provider.ts
new file mode 100644
index 0000000..8cfd47d
--- /dev/null
+++ b/lib/services/providers/openai-memory-model-provider.ts
@@ -0,0 +1 @@
+export { classifyMemoryCandidatesWithProvider as classifyMemoryCandidates, summarizeSession, extractAdaptiveProfile, distillContextPack } from "@/lib/services/memory-model-provider";
diff --git a/public/pandora-memory-openapi.json b/public/pandora-memory-openapi.json
index c11fbff..c300c6e 100644
--- a/public/pandora-memory-openapi.json
+++ b/public/pandora-memory-openapi.json
@@ -14,18 +14,28 @@
"schemas": {
"Namespace": {
"type": "string",
- "enum": ["real_life", "au"],
+ "enum": [
+ "real_life",
+ "au"
+ ],
"description": "Memory namespace to query or write into."
},
"Sensitivity": {
"type": "string",
- "enum": ["low", "medium", "high", "private"],
+ "enum": [
+ "low",
+ "medium",
+ "high",
+ "private"
+ ],
"description": "Review sensitivity label for a captured memory event."
},
"ContextRequest": {
"type": "object",
"additionalProperties": false,
- "required": ["namespace"],
+ "required": [
+ "namespace"
+ ],
"properties": {
"namespace": {
"$ref": "#/components/schemas/Namespace"
@@ -83,7 +93,10 @@
"CaptureRequest": {
"type": "object",
"additionalProperties": false,
- "required": ["namespace", "raw_text"],
+ "required": [
+ "namespace",
+ "raw_text"
+ ],
"properties": {
"namespace": {
"$ref": "#/components/schemas/Namespace"
@@ -128,14 +141,20 @@
"DistillRequest": {
"type": "object",
"additionalProperties": false,
- "required": ["namespace", "pack_type"],
+ "required": [
+ "namespace",
+ "pack_type"
+ ],
"properties": {
"namespace": {
"$ref": "#/components/schemas/Namespace"
},
"pack_type": {
"type": "string",
- "enum": ["daily", "master"],
+ "enum": [
+ "daily",
+ "master"
+ ],
"default": "master"
}
}
@@ -169,6 +188,10 @@
"type": "string"
}
}
+ },
+ "AdaptiveMemoryResponse": {
+ "type": "object",
+ "additionalProperties": true
}
},
"securitySchemes": {
@@ -343,6 +366,181 @@
}
}
}
+ },
+ "/api/memory/adaptive/context": {
+ "post": {
+ "operationId": "api_memory_adaptive_context",
+ "summary": "Return adaptive ChatGPT context",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "additionalProperties": true
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "additionalProperties": true
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/memory/adaptive/analyze": {
+ "post": {
+ "operationId": "api_memory_adaptive_analyze",
+ "summary": "Analyze memory candidates",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "additionalProperties": true
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "additionalProperties": true
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/memory/adaptive/session-digest": {
+ "post": {
+ "operationId": "api_memory_adaptive_session_digest",
+ "summary": "Create session digest",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "additionalProperties": true
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "additionalProperties": true
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/memory/search": {
+ "post": {
+ "operationId": "api_memory_search",
+ "summary": "Hybrid memory search",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "additionalProperties": true
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "additionalProperties": true
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/memory/profiles/refresh": {
+ "post": {
+ "operationId": "api_memory_profiles_refresh",
+ "summary": "Refresh adaptive profiles",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "additionalProperties": true
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "additionalProperties": true
+ }
+ }
+ }
+ }
+ }
+ }
}
}
-}
+}
\ No newline at end of file
diff --git a/supabase/migrations/20260627040000_phase_4c_adaptive_memory_intelligence.sql b/supabase/migrations/20260627040000_phase_4c_adaptive_memory_intelligence.sql
new file mode 100644
index 0000000..9faabfc
--- /dev/null
+++ b/supabase/migrations/20260627040000_phase_4c_adaptive_memory_intelligence.sql
@@ -0,0 +1,24 @@
+create extension if not exists vector;
+create table if not exists public.memory_capture_candidates (id uuid primary key default gen_random_uuid(), user_id uuid not null, namespace text not null check(namespace in ('real_life','au')), source text not null, source_ref text, raw_excerpt text, redacted_excerpt text, memory_type text, title text, summary text, importance integer check(importance between 1 and 10), sensitivity text, confidence numeric, should_capture boolean, requires_review boolean, status text default 'pending' check(status in ('pending','approved','rejected','captured','blocked_secret','duplicate')), reason text, people jsonb default '[]', projects jsonb default '[]', risks jsonb default '[]', tags jsonb default '[]', metadata jsonb default '{}', created_at timestamptz default now(), reviewed_at timestamptz, captured_event_id uuid);
+create table if not exists public.memory_profiles (id uuid primary key default gen_random_uuid(), user_id uuid not null, namespace text not null check(namespace in ('real_life','au')), profile_type text not null, subject_key text not null, title text, summary text, facts jsonb default '[]', preferences jsonb default '[]', patterns jsonb default '[]', risks jsonb default '[]', open_loops jsonb default '[]', decisions jsonb default '[]', evidence_refs jsonb default '[]', confidence numeric, status text default 'active', version integer default 1, created_at timestamptz default now(), updated_at timestamptz default now());
+create unique index if not exists memory_profiles_active_unique on public.memory_profiles(user_id, namespace, profile_type, subject_key, status) where status='active';
+create table if not exists public.memory_embeddings (id uuid primary key default gen_random_uuid(), user_id uuid not null, namespace text not null check(namespace in ('real_life','au')), source_table text not null, source_id uuid not null, memory_type text, content_hash text not null, embedding vector(1536), embedding_model text not null, embedding_dimensions integer not null default 1536, importance integer, sensitivity text, metadata jsonb default '{}', created_at timestamptz default now(), updated_at timestamptz default now());
+create table if not exists public.memory_session_digests (id uuid primary key default gen_random_uuid(), user_id uuid not null, namespace text not null check(namespace in ('real_life','au')), source text not null, source_ref text, title text, summary text, durable_updates jsonb default '[]', decisions jsonb default '[]', open_loops jsonb default '[]', risks jsonb default '[]', people jsonb default '[]', projects jsonb default '[]', style_updates jsonb default '[]', candidate_ids jsonb default '[]', captured_event_ids jsonb default '[]', profile_ids jsonb default '[]', created_at timestamptz default now());
+create table if not exists public.memory_open_loops (id uuid primary key default gen_random_uuid(), user_id uuid not null, namespace text not null check(namespace in ('real_life','au')), loop_type text not null, subject_key text not null, title text, description text, severity integer, status text default 'open' check(status in ('open','acknowledged','resolved','deferred')), evidence_refs jsonb default '[]', next_action text, created_at timestamptz default now(), updated_at timestamptz default now(), resolved_at timestamptz);
+create table if not exists public.memory_retrieval_logs (id uuid primary key default gen_random_uuid(), user_id uuid not null, namespace text, query_hash text, metadata jsonb default '{}', created_at timestamptz default now());
+create table if not exists public.memory_model_call_logs (id uuid primary key default gen_random_uuid(), user_id uuid, namespace text, provider text, model text, task text, input_char_count integer, redacted boolean default true, metadata jsonb default '{}', created_at timestamptz default now());
+create index if not exists memory_capture_candidates_user_ns_idx on public.memory_capture_candidates(user_id, namespace, created_at desc);
+create index if not exists memory_profiles_user_ns_idx on public.memory_profiles(user_id, namespace, profile_type);
+create index if not exists memory_embeddings_user_ns_idx on public.memory_embeddings(user_id, namespace);
+create index if not exists memory_embeddings_source_idx on public.memory_embeddings(source_table, source_id);
+create index if not exists memory_embeddings_vector_idx on public.memory_embeddings using ivfflat (embedding vector_cosine_ops) with (lists = 100);
+create index if not exists memory_session_digests_user_ns_idx on public.memory_session_digests(user_id, namespace, created_at desc);
+create index if not exists memory_open_loops_user_ns_idx on public.memory_open_loops(user_id, namespace, status);
+alter table public.memory_capture_candidates enable row level security; alter table public.memory_profiles enable row level security; alter table public.memory_embeddings enable row level security; alter table public.memory_session_digests enable row level security; alter table public.memory_open_loops enable row level security; alter table public.memory_retrieval_logs enable row level security; alter table public.memory_model_call_logs enable row level security;
+do $$ begin
+ create policy "memory_capture_candidates_user_scoped" on public.memory_capture_candidates for all using (auth.uid() = user_id) with check (auth.uid() = user_id);
+ create policy "memory_profiles_user_scoped" on public.memory_profiles for all using (auth.uid() = user_id) with check (auth.uid() = user_id);
+ create policy "memory_embeddings_user_scoped" on public.memory_embeddings for all using (auth.uid() = user_id) with check (auth.uid() = user_id);
+ create policy "memory_session_digests_user_scoped" on public.memory_session_digests for all using (auth.uid() = user_id) with check (auth.uid() = user_id);
+ create policy "memory_open_loops_user_scoped" on public.memory_open_loops for all using (auth.uid() = user_id) with check (auth.uid() = user_id);
+exception when duplicate_object then null; end $$;
diff --git a/tests/phase4c-adaptive.test.ts b/tests/phase4c-adaptive.test.ts
new file mode 100644
index 0000000..9d0d5f3
--- /dev/null
+++ b/tests/phase4c-adaptive.test.ts
@@ -0,0 +1,34 @@
+import { describe, expect, it } from "vitest";
+import { detectSecrets, redactSecrets } from "../lib/services/memory-redaction-service";
+import { classifyMemoryCandidatesDeterministic } from "../lib/services/memory-classification-service";
+import { createCandidatesFromSession } from "../lib/services/memory-candidate-service";
+import { createMemoryEmbedding } from "../lib/services/memory-embedding-service";
+import { analyzeMemoryCandidatesTool, createSessionDigestTool, refreshAdaptiveProfilesTool } from "../lib/services/pandora-mcp-tools";
+
+describe("Phase 4C secret detection", () => {
+ it("detects bearer tokens and OpenAI-like keys", () => { expect(detectSecrets("Bearer abcdefghijklmnopqrstuvwxyz123456").detected).toBe(true); expect(detectSecrets("sk-abcdefghijklmnopqrstuvwxyz123456").detected).toBe(true); });
+ it("redacts before model/embedding", () => { expect(redactSecrets("token=abcdefghijklmnopqrstuvwxyz1234567890")).toContain("[REDACTED_SECRET]"); });
+});
+describe("Phase 4C classification", () => {
+ it("detects operating preference", () => { expect(classifyMemoryCandidatesDeterministic({text:"I prefer blunt concise answers"})[0].memory_type).toBe("operating_preference"); });
+ it("detects project decision/status", () => { expect(classifyMemoryCandidatesDeterministic({text:"We decided to deploy Pandora on Vercel"})[0].should_capture).toBe(true); });
+ it("detects gambling risk", () => { expect(classifyMemoryCandidatesDeterministic({text:"Gambling risk came up again"})[0].memory_type).toBe("gambling_risk"); });
+ it("detects AU canon", () => { expect(classifyMemoryCandidatesDeterministic({namespace:"au", text:"This AU canon says Melodee knows the rule"})[0].namespace).toBe("au"); });
+ it("ignores low signal noise", () => { expect(classifyMemoryCandidatesDeterministic({text:"what is 2+2"})[0].should_capture).toBe(false); });
+ it("blocks secrets", () => { const c=classifyMemoryCandidatesDeterministic({text:"OPENAI_API_KEY=sk-abcdefghijklmnopqrstuvwxyz123456"})[0]; expect(c.memory_type).toBe("secret_or_credential"); expect(c.should_capture).toBe(false); });
+});
+describe("Phase 4C candidate safety", () => {
+ it("forces blocked status for credential-like input even when model calls are enabled", async()=>{ let inserted: unknown[] | null = null; const client={from:()=>({insert:(rows:unknown[])=>{inserted=rows; return {select:()=>Promise.resolve({data:rows,error:null})};}})} as never; const result=await createCandidatesFromSession(client,{user_id:"00000000-0000-0000-0000-000000000000",namespace:"real_life",source:"test",text:"OPENAI_API_KEY=sk-abcdefghijklmnopqrstuvwxyz123456"},{PANDORA_ENABLE_MODEL_CALLS:"true"}); expect(result.candidates[0]).toMatchObject({status:"blocked_secret",memory_type:"secret_or_credential",should_capture:false}); expect(inserted).toBeTruthy(); });
+});
+describe("Phase 4C MCP gates", () => {
+ const principal={ok:true as const,authType:"mcp_bearer_token" as const,userId:"00000000-0000-0000-0000-000000000000"};
+ const client={from:()=>{throw new Error("unexpected write");}} as never;
+ it("blocks adaptive candidate writes when MCP capture is disabled", async()=>{ const result=await analyzeMemoryCandidatesTool(client,principal,{namespace:"real_life",text:"we decided to ship"},{PANDORA_ENABLE_MCP_CAPTURE:"false"}); expect(result).toMatchObject({ok:false,code:"mcp_capture_disabled"}); });
+ it("blocks session digest writes when MCP capture is disabled", async()=>{ const result=await createSessionDigestTool(client,principal,{namespace:"real_life",transcript_or_summary:"summary"},{PANDORA_ENABLE_MCP_CAPTURE:"false",PANDORA_ENABLE_MCP_DISTILLATION:"true"}); expect(result).toMatchObject({ok:false,code:"mcp_capture_disabled"}); });
+ it("blocks profile refresh writes when MCP capture is disabled", async()=>{ const result=await refreshAdaptiveProfilesTool(client,principal,{namespace:"real_life"},{PANDORA_ENABLE_MCP_CAPTURE:"false",PANDORA_ENABLE_MCP_DISTILLATION:"true"}); expect(result).toMatchObject({ok:false,code:"mcp_capture_disabled"}); });
+});
+describe("Phase 4C REST gates", () => {
+ it("blocks adaptive search when context API gate is disabled", async()=>{ process.env.PANDORA_ENABLE_MEMORY_CONTEXT_API="false"; const { POST } = await import("../app/api/memory/search/route"); const response=await POST(new Request("http://localhost/api/memory/search",{method:"POST",body:JSON.stringify({namespace:"real_life"})}) as never); expect(response.status).toBe(403); expect((await response.json()).blockers).toContain("memoryContextApiEnabled_disabled"); });
+ it("blocks adaptive analyze when capture API gate is disabled", async()=>{ process.env.PANDORA_ENABLE_MEMORY_CAPTURE_API="false"; const { POST } = await import("../app/api/memory/adaptive/analyze/route"); const response=await POST(new Request("http://localhost/api/memory/adaptive/analyze",{method:"POST",body:JSON.stringify({namespace:"real_life",text:"decision"})}) as never); expect(response.status).toBe(403); expect((await response.json()).blockers).toContain("memoryCaptureApiEnabled_disabled"); });
+});
+describe("Phase 4C embeddings", () => { it("no embedding when disabled", async()=>{ const r=await createMemoryEmbedding({text:"hello",user_id:"u",namespace:"real_life",source_table:"memory_events",source_id:"00000000-0000-0000-0000-000000000000"},{PANDORA_ENABLE_EMBEDDINGS:"false"}); expect(r.enabled).toBe(false); }); });