Skip to content
Merged
1 change: 1 addition & 0 deletions app/admin/memory/adaptive/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default function Page(){ return <main><h1>Pandora Memory adaptive</h1><p>Phase 4C adaptive memory console. Server endpoints remain bridge-auth gated; public read/write stay disabled.</p><ul><li>Model calls: {process.env.PANDORA_ENABLE_MODEL_CALLS === "true" ? "enabled" : "disabled"}</li><li>Embeddings: {process.env.PANDORA_ENABLE_EMBEDDINGS === "true" ? "enabled" : "disabled"}</li><li>Semantic retrieval: {process.env.PANDORA_ENABLE_SEMANTIC_RETRIEVAL === "true" ? "enabled" : "disabled"}</li><li>Public read: {process.env.PANDORA_ENABLE_PUBLIC_MEMORY_READ === "true" ? "WARNING enabled" : "disabled"}</li><li>Public persistence: {process.env.PANDORA_ENABLE_PUBLIC_MEMORY_PERSISTENCE === "true" ? "WARNING enabled" : "disabled"}</li></ul></main>; }
1 change: 1 addition & 0 deletions app/admin/memory/candidates/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default function Page(){ return <main><h1>Pandora Memory candidates</h1><p>Phase 4C adaptive memory console. Server endpoints remain bridge-auth gated; public read/write stay disabled.</p><ul><li>Model calls: {process.env.PANDORA_ENABLE_MODEL_CALLS === "true" ? "enabled" : "disabled"}</li><li>Embeddings: {process.env.PANDORA_ENABLE_EMBEDDINGS === "true" ? "enabled" : "disabled"}</li><li>Semantic retrieval: {process.env.PANDORA_ENABLE_SEMANTIC_RETRIEVAL === "true" ? "enabled" : "disabled"}</li><li>Public read: {process.env.PANDORA_ENABLE_PUBLIC_MEMORY_READ === "true" ? "WARNING enabled" : "disabled"}</li><li>Public persistence: {process.env.PANDORA_ENABLE_PUBLIC_MEMORY_PERSISTENCE === "true" ? "WARNING enabled" : "disabled"}</li></ul></main>; }
1 change: 1 addition & 0 deletions app/admin/memory/open-loops/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default function Page(){ return <main><h1>Pandora Memory open-loops</h1><p>Phase 4C adaptive memory console. Server endpoints remain bridge-auth gated; public read/write stay disabled.</p><ul><li>Model calls: {process.env.PANDORA_ENABLE_MODEL_CALLS === "true" ? "enabled" : "disabled"}</li><li>Embeddings: {process.env.PANDORA_ENABLE_EMBEDDINGS === "true" ? "enabled" : "disabled"}</li><li>Semantic retrieval: {process.env.PANDORA_ENABLE_SEMANTIC_RETRIEVAL === "true" ? "enabled" : "disabled"}</li><li>Public read: {process.env.PANDORA_ENABLE_PUBLIC_MEMORY_READ === "true" ? "WARNING enabled" : "disabled"}</li><li>Public persistence: {process.env.PANDORA_ENABLE_PUBLIC_MEMORY_PERSISTENCE === "true" ? "WARNING enabled" : "disabled"}</li></ul></main>; }
1 change: 1 addition & 0 deletions app/admin/memory/profiles/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default function Page(){ return <main><h1>Pandora Memory profiles</h1><p>Phase 4C adaptive memory console. Server endpoints remain bridge-auth gated; public read/write stay disabled.</p><ul><li>Model calls: {process.env.PANDORA_ENABLE_MODEL_CALLS === "true" ? "enabled" : "disabled"}</li><li>Embeddings: {process.env.PANDORA_ENABLE_EMBEDDINGS === "true" ? "enabled" : "disabled"}</li><li>Semantic retrieval: {process.env.PANDORA_ENABLE_SEMANTIC_RETRIEVAL === "true" ? "enabled" : "disabled"}</li><li>Public read: {process.env.PANDORA_ENABLE_PUBLIC_MEMORY_READ === "true" ? "WARNING enabled" : "disabled"}</li><li>Public persistence: {process.env.PANDORA_ENABLE_PUBLIC_MEMORY_PERSISTENCE === "true" ? "WARNING enabled" : "disabled"}</li></ul></main>; }
1 change: 1 addition & 0 deletions app/admin/memory/retrieval/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default function Page(){ return <main><h1>Pandora Memory retrieval</h1><p>Phase 4C adaptive memory console. Server endpoints remain bridge-auth gated; public read/write stay disabled.</p><ul><li>Model calls: {process.env.PANDORA_ENABLE_MODEL_CALLS === "true" ? "enabled" : "disabled"}</li><li>Embeddings: {process.env.PANDORA_ENABLE_EMBEDDINGS === "true" ? "enabled" : "disabled"}</li><li>Semantic retrieval: {process.env.PANDORA_ENABLE_SEMANTIC_RETRIEVAL === "true" ? "enabled" : "disabled"}</li><li>Public read: {process.env.PANDORA_ENABLE_PUBLIC_MEMORY_READ === "true" ? "WARNING enabled" : "disabled"}</li><li>Public persistence: {process.env.PANDORA_ENABLE_PUBLIC_MEMORY_PERSISTENCE === "true" ? "WARNING enabled" : "disabled"}</li></ul></main>; }
5 changes: 5 additions & 0 deletions app/api/memory/adaptive/analyze/route.ts
Original file line number Diff line number Diff line change
@@ -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}); }
15 changes: 15 additions & 0 deletions app/api/memory/adaptive/context/route.ts
Original file line number Diff line number Diff line change
@@ -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 });
}
38 changes: 38 additions & 0 deletions app/api/memory/adaptive/route-helper.ts
Original file line number Diff line number Diff line change
@@ -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<PandoraRuntimeGate, "memoryCaptureApiEnabled" | "memoryContextApiEnabled" | "memoryDistillationEnabled">;

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 };
}
5 changes: 5 additions & 0 deletions app/api/memory/adaptive/session-digest/route.ts
Original file line number Diff line number Diff line change
@@ -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}); }
4 changes: 4 additions & 0 deletions app/api/memory/profiles/refresh/route.ts
Original file line number Diff line number Diff line change
@@ -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}); }
27 changes: 27 additions & 0 deletions app/api/memory/search/route.ts
Original file line number Diff line number Diff line change
@@ -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 });
}
9 changes: 9 additions & 0 deletions docs/pandora-chatgpt-adaptive-instructions.md
Original file line number Diff line number Diff line change
@@ -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.
15 changes: 15 additions & 0 deletions docs/pandora-memory-env-vars.md
Original file line number Diff line number Diff line change
@@ -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`.
5 changes: 5 additions & 0 deletions docs/pandora-memory-safety-policy.md
Original file line number Diff line number Diff line change
@@ -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.
7 changes: 7 additions & 0 deletions docs/phase-4c-adaptive-memory-intelligence.md
Original file line number Diff line number Diff line change
@@ -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.
4 changes: 4 additions & 0 deletions lib/services/adaptive-chatgpt-context-service.ts
Original file line number Diff line number Diff line change
@@ -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()}; }
12 changes: 12 additions & 0 deletions lib/services/memory-candidate-service.ts
Original file line number Diff line number Diff line change
@@ -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<NodeJS.ProcessEnv>=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<MemoryEvent>("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<NodeJS.ProcessEnv>=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)<Number(env.PANDORA_AUTO_CAPTURE_CONFIDENCE_THRESHOLD??0.86)) return {ok:false,reason:"confidence_below_threshold"}; const ev=await captureApprovedCandidate(client,candidate,userId); return {ok:!ev.error,event:ev.data,error:ev.error?.message}; }
export async function detectDuplicateCandidate(){ return {duplicate:false,reason:"deterministic_hash_only"}; }
Loading
Loading