-
Notifications
You must be signed in to change notification settings - Fork 1
Add Phase 5C Daily Compaction and Profile Synthesis #107
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| import { resolvePandoraRuntimeSafetyConfig } from "@/lib/config/pandora-runtime-safety-config"; | ||
| export default function MemoryCompactionAdminPage(){const r=resolvePandoraRuntimeSafetyConfig(); const safe=[ | ||
| ["Distillation gate",r.config.memoryDistillationEnabled], | ||
| ["Model calls",r.config.modelCallsEnabled], | ||
| ["Embeddings",r.config.embeddingsEnabled], | ||
| ["Semantic retrieval",r.config.semanticRetrievalEnabled], | ||
| ["Public memory read",r.config.publicMemoryReadEnabled], | ||
| ["Public persistence",r.config.publicMemoryPersistenceEnabled], | ||
| ]; return <main style={{padding:24,maxWidth:900}}><h1>Phase 5C Memory Compaction</h1><p>Daily compaction deterministically turns reviewed/captured memory into versioned profiles, open loops, and daily context packs. It does not call models, create embeddings, enable retrieval, or expose public reads/writes.</p><h2>Safety status</h2><ul>{safe.map(([label,value])=><li key={String(label)}><strong>{label}:</strong> {value?"enabled":"disabled"}</li>)}</ul><h2>Internal job</h2><p>POST <code>/api/memory/jobs/daily-digest</code> with <code>Authorization: Bearer $PANDORA_INTERNAL_JOB_TOKEN</code>.</p><pre>{JSON.stringify({namespace:"real_life",dry_run:true,since:new Date(Date.now()-86400000).toISOString()},null,2)}</pre><h2>Operator notes</h2><ul><li>Keep model calls, embeddings, semantic retrieval, public read/write, and permanent auto-capture off.</li><li>Run dry runs first, inspect profiles/open loops/context pack output, then run per namespace.</li></ul></main>} | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,8 @@ | ||||||
| import { NextRequest, NextResponse } from "next/server"; | ||||||
| import { z } from "zod"; | ||||||
| import { resolvePandoraRuntimeSafetyConfig } from "@/lib/config/pandora-runtime-safety-config"; | ||||||
| import { createSupabaseBridgeAdminClient } from "@/lib/supabase/bridge-admin"; | ||||||
| import { runDailyMemoryCompaction } from "@/lib/services/memory-compaction-service"; | ||||||
| const schema=z.object({namespace:z.enum(["real_life","au"]),since:z.string().datetime().optional(),dry_run:z.boolean().optional(),user_id:z.string().min(1).optional()}); | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🌐 Web query:
💡 Result: In Zod 3.23.8, the default value for the offset option in z.string.datetime({ offset:... }) is false [1][2][3]. When calling z.string.datetime, Zod enforces ISO 8601 strings and, by default, strictly requires UTC format (usually ending in "Z") and prohibits timezone offsets [1][2][3]. Setting { offset: true } explicitly enables the validation to accept strings that include timezone offsets (e.g., +02:00) [1][2][3]. Summary of datetime validation behavior: - Default: No timezone offsets are allowed [1][3]. - Enabled: { offset: true } allows timezone offsets [1][2]. - Precision: Arbitrary sub-second decimal precision is supported by default [1][3]. - Unqualified/Local: To allow datetimes without a timezone or "Z" designator, you must use the { local: true } option, which was added in Zod 3.23.0 [4][5][6]. Citations:
🏁 Script executed: #!/bin/bash
set -euo pipefail
# Locate Zod version declarations and the target route
git ls-files | rg '(^|/)(package\.json|pnpm-lock\.yaml|package-lock\.json|yarn\.lock|bun\.lockb|app/api/memory/jobs/daily-digest/route\.ts)$'
printf '\n--- package.json mentions ---\n'
rg -n '"zod"|zod@' package.json **/package.json 2>/dev/null || true
printf '\n--- lockfile mentions ---\n'
rg -n 'zod' pnpm-lock.yaml package-lock.json yarn.lock bun.lockb 2>/dev/null || true
printf '\n--- route file ---\n'
cat -n app/api/memory/jobs/daily-digest/route.tsRepository: besfeng23/Memory Length of output: 3179 Allow ISO Proposed fix-const schema=z.object({namespace:z.enum(["real_life","au"]),since:z.string().datetime().optional(),dry_run:z.boolean().optional(),user_id:z.string().min(1).optional()});
+const schema=z.object({namespace:z.enum(["real_life","au"]),since:z.string().datetime({offset:true}).optional(),dry_run:z.boolean().optional(),user_id:z.string().min(1).optional()});📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
| function bearer(request:NextRequest){const [scheme,token]=(request.headers.get("authorization")??"").split(" "); return scheme?.toLowerCase()==="bearer"?token:undefined;} | ||||||
| export async function POST(request:NextRequest){const token=process.env.PANDORA_INTERNAL_JOB_TOKEN; if(!token||bearer(request)!==token)return NextResponse.json({ok:false,blockers:["internal_job_token_required"]},{status:401}); const runtime=resolvePandoraRuntimeSafetyConfig(); if(!runtime.config.memoryDistillationEnabled)return NextResponse.json({ok:false,blockers:["memoryDistillationEnabled_disabled"],warnings:[],next_step:"Set PANDORA_ENABLE_MEMORY_DISTILLATION=true in a reviewed environment."},{status:403}); const parsed=schema.safeParse(await request.json().catch(()=>({}))); if(!parsed.success)return NextResponse.json({ok:false,blockers:["invalid_request"],issues:parsed.error.flatten()},{status:400}); const user_id=parsed.data.user_id??process.env.PANDORA_MEMORY_BRIDGE_USER_ID; if(!user_id)return NextResponse.json({ok:false,blockers:["job_user_id_required"],next_step:"Send user_id with the internal token or configure PANDORA_MEMORY_BRIDGE_USER_ID."},{status:400}); const result=await runDailyMemoryCompaction(createSupabaseBridgeAdminClient() as never,{user_id,namespace:parsed.data.namespace,since:parsed.data.since,dry_run:parsed.data.dry_run}); return NextResponse.json({ok:result.ok,namespace:result.namespace,dry_run:result.dry_run,daily_pack:result.daily_pack,updated_profiles:result.updated_profiles,opened_or_updated_loops:result.opened_or_updated_loops,warnings:result.warnings,blockers:result.blockers,next_step:result.next_step});} | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the internal job token is accepted, this route falls back to Useful? React with 👍 / 👎. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win Return a failure HTTP status when compaction fails. Line 8 always returns default HTTP 200, even when Proposed fix- const result=await runDailyMemoryCompaction(createSupabaseBridgeAdminClient() as never,{user_id,namespace:parsed.data.namespace,since:parsed.data.since,dry_run:parsed.data.dry_run}); return NextResponse.json({ok:result.ok,namespace:result.namespace,dry_run:result.dry_run,daily_pack:result.daily_pack,updated_profiles:result.updated_profiles,opened_or_updated_loops:result.opened_or_updated_loops,warnings:result.warnings,blockers:result.blockers,next_step:result.next_step});}
+ const result=await runDailyMemoryCompaction(createSupabaseBridgeAdminClient() as never,{user_id,namespace:parsed.data.namespace,since:parsed.data.since,dry_run:parsed.data.dry_run}); return NextResponse.json({ok:result.ok,namespace:result.namespace,dry_run:result.dry_run,daily_pack:result.daily_pack,updated_profiles:result.updated_profiles,opened_or_updated_loops:result.opened_or_updated_loops,warnings:result.warnings,blockers:result.blockers,next_step:result.next_step},{status:result.ok?200:500});}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| # Pandora Memory Phase 5C: Daily Compaction | ||
|
|
||
| Phase 5C adds deterministic daily compaction for Pandora Memory. It turns reviewed and captured source-backed memory into clean operating context: versioned profiles, open loops, and daily context packs. | ||
|
|
||
| ## Why compaction matters | ||
|
|
||
| Phase 5A queues candidates. Phase 5B lets an operator approve, reject, edit, duplicate, or capture them. Phase 5C makes the reviewed/captured layer compound over time instead of becoming an infinite event log. | ||
|
|
||
| ## Data layers | ||
|
|
||
| - **Candidates** are proposed memory items awaiting review or capture. | ||
| - **Memory events** are captured, source-backed memory rows. | ||
| - **Session digests** summarize sessions and can feed compaction wrappers. | ||
| - **Profiles** store durable operating, project, risk, relationship, people, deployment, and AU canon state. | ||
| - **Open loops** track unresolved blockers, risks, review backlog, and follow-up actions. | ||
| - **Context packs** are generated daily or master summaries for downstream private context use. | ||
|
|
||
| ## Deterministic profile synthesis | ||
|
|
||
| `runDailyMemoryCompaction` scans recent rows by `user_id`, `namespace`, and ISO `since`. It uses deterministic keyword signals only. It does not call models, create embeddings, or use semantic retrieval. | ||
|
|
||
| Profile updates are versioned. Existing active profiles are marked `superseded`, and a new active row is inserted with merged evidence references. | ||
|
|
||
| ## Feedback-aware compaction | ||
|
|
||
| Approved and captured candidates are eligible profile/open-loop inputs. Rejected and duplicate candidates remain visible in the compaction result but are not promoted as source material. Edited candidate fields are preferred because compaction reads the reviewed candidate title/summary/type/sensitivity fields. | ||
|
|
||
| Blocked secrets are excluded and redacted before pack creation. | ||
|
|
||
| ## Open-loop updates | ||
|
|
||
| The open-loop engine creates or updates loops by `user_id`, `namespace`, `loop_type`, and `subject_key`. Existing open/acknowledged loops are updated, evidence references are merged, and severity can increase but not downgrade. | ||
|
|
||
| ## Daily context packs | ||
|
|
||
| Daily compaction creates a `memory_context_packs` row with `pack_type=daily`, key points, projects, people, decisions, risks, open loops, and generated event ids. Older active daily packs for the same user/namespace are superseded when possible. | ||
|
|
||
| ## Internal job endpoint | ||
|
|
||
| `POST /api/memory/jobs/daily-digest` | ||
|
|
||
| Headers: | ||
|
|
||
| ```http | ||
| Authorization: Bearer <PANDORA_INTERNAL_JOB_TOKEN> | ||
| ``` | ||
|
|
||
| Body: | ||
|
|
||
| ```json | ||
| { "namespace": "real_life", "since": "2026-06-27T00:00:00.000Z", "dry_run": true } | ||
| ``` | ||
|
|
||
| The endpoint requires `PANDORA_ENABLE_MEMORY_DISTILLATION=true` and is internal-token protected. If needed for the first single-user rollout, send `user_id` only through this internal-token route or configure `PANDORA_MEMORY_BRIDGE_USER_ID` server-side. | ||
|
|
||
| ## Env posture | ||
|
|
||
| ```bash | ||
| PANDORA_ENABLE_MEMORY_DISTILLATION=true | ||
| PANDORA_INTERNAL_JOB_TOKEN=<server-only-secret> | ||
| PANDORA_ENABLE_MODEL_CALLS=false | ||
| PANDORA_ENABLE_EMBEDDINGS=false | ||
| PANDORA_ENABLE_SEMANTIC_RETRIEVAL=false | ||
| PANDORA_ENABLE_PUBLIC_MEMORY_READ=false | ||
| PANDORA_ENABLE_PUBLIC_MEMORY_PERSISTENCE=false | ||
| PANDORA_ENABLE_AUTO_CAPTURE=false | ||
| ``` | ||
|
|
||
| Never expose `PANDORA_INTERNAL_JOB_TOKEN` as `NEXT_PUBLIC_*`. | ||
|
|
||
| ## Safety boundaries | ||
|
|
||
| - deterministic only | ||
| - no model calls required | ||
| - no embeddings required | ||
| - no semantic retrieval required | ||
| - no public memory reads/writes | ||
| - blocked secrets excluded/redacted | ||
| - `real_life` and `au` remain separated | ||
| - permanent auto-capture remains disabled by default | ||
|
|
||
| ## Rollout | ||
|
|
||
| 1. Deploy with `PANDORA_INTERNAL_JOB_TOKEN` configured. | ||
| 2. Keep model calls, embeddings, semantic retrieval, public read/write, and auto-capture off. | ||
| 3. Enable `PANDORA_ENABLE_MEMORY_DISTILLATION=true`. | ||
| 4. Run the daily digest endpoint with `dry_run=true`. | ||
| 5. Inspect profile, loop, and pack output. | ||
| 6. Run for `real_life`. | ||
| 7. Run for `au`. | ||
| 8. Verify daily context packs appear. | ||
| 9. Verify active profiles version correctly. | ||
| 10. Verify open loops update without duplicate spam. | ||
| 11. Only then schedule the job externally. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| /* eslint-disable @typescript-eslint/no-explicit-any */ | ||
| import type { MemoryBridgeDbClient, MemoryBridgeNamespace, MemoryEvent } from "@/lib/services/memory-bridge-service"; | ||
| import { buildDailyContextPack } from "@/lib/services/memory-distillation-service"; | ||
| import { upsertVersionedMemoryProfile } from "@/lib/services/memory-profile-service"; | ||
| import { upsertOpenLoop } from "@/lib/services/memory-open-loop-service"; | ||
| import { redactSecrets } from "@/lib/services/memory-redaction-service"; | ||
| export type MemoryCompactionScope={user_id:string;namespace:MemoryBridgeNamespace;since?:string;dry_run?:boolean}; | ||
| export type MemoryCompactionResult={ok:boolean;namespace:MemoryBridgeNamespace;dry_run:boolean;daily_pack?:unknown;updated_profiles:unknown[];opened_or_updated_loops:unknown[];reviewed_candidates:unknown[];captured_events:unknown[];feedback_events:unknown[];blockers:string[];warnings:string[];next_step?:string}; | ||
| const projects=["Pandora Memory","PLP","Hatid","Speedcash","SpeedyPay","RetargetOS","AI GrowthOS","Red-Apple"]; | ||
| const personNames=["Patty","Melodee","Mang Rudy","Mang Bert","Ate Dhes"]; | ||
| const technicalWords=["GitHub","PR","merge","Vercel","Supabase","migration","deployment","build","CI","repo"]; | ||
| const relationshipWords=["relationship loop","emotional loop","relapse","unresolved emotional pattern"]; | ||
| const auOnlyNames=["Melodee","Mang Rudy","Mang Bert","Ate Dhes"]; | ||
| const blocked="[REDACTED_SECRET]"; | ||
| const clean=(s:string)=>redactSecrets(s).replace(/\s+/g," ").trim().slice(0,500); | ||
| const textOf=(x:any)=>clean([x.extracted_summary,x.summary,x.title,x.redacted_excerpt,x.raw_text,x.raw_excerpt].filter(Boolean).join(" — ")); | ||
| const has=(t:string,words:string[])=>words.some(w=>t.toLowerCase().includes(w.toLowerCase())); | ||
| const ref=(kind:string,x:any)=>({kind,id:x.id,source:x.source,source_ref:x.source_ref,created_at:x.created_at??x.reviewed_at}); | ||
| const subject=(s:string)=>s.toLowerCase().replace(/[^a-z0-9]+/g,"_").replace(/^_|_$/g,"")||"global"; | ||
| async function list(client:MemoryBridgeDbClient,table:string,input:MemoryCompactionScope,statuses?:string[]){const q:any=client.from(table).select("*").eq("user_id",input.user_id).eq("namespace",input.namespace).order("created_at",{ascending:false}).limit(100);const r=await (q as Promise<{data:any[]|null;error:{message:string}|null}>); if(r.error)throw new Error(`${table}: ${r.error.message}`); return (r.data??[]).filter(x=>(!input.since||String(x.created_at??x.reviewed_at??"")>=input.since!)&&(!statuses||statuses.includes(x.status))).filter(x=>x.status!=="blocked_secret"&&textOf(x)!==blocked);} | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win Apply source filters before the 100-row limit. Line 17 fetches the latest 100 rows first, then filters by 🤖 Prompt for AI Agents |
||
| function profileInputs(user_id:string,namespace:MemoryBridgeNamespace,sources:any[]){const out:any[]=[];const add=(profile_type:string,subject_key:string,title:string,items:any[],bucket:string)=>{if(!items.length)return; out.push({user_id,namespace,profile_type,subject_key,title,summary:items.map(textOf).filter(Boolean).slice(0,8).join("\n"),[bucket]:items.map((x:any)=>({text:textOf(x),ref:ref(x.raw_text?"event":"candidate",x)})),evidence_refs:items.map((x:any)=>ref(x.raw_text?"event":"candidate",x)),confidence:Math.min(0.95,0.55+items.length*0.05)});}; | ||
| add("operating_profile","global","Global operating profile",sources.filter(x=>has(textOf(x),["I prefer","from now on","always","don't","dont","be blunt","be concise","don't overpraise","check repo first","analyze automatically","maximize Codex task"])),"preferences"); | ||
| add("style_profile","global","Global style profile",sources.filter(x=>has(textOf(x),["tone","style","format","writing","prompt style","direct","Taglish","cinematic"])),"preferences"); | ||
| add("risk_profile","global","Global risk profile",sources.filter(x=>has(textOf(x),["gambling","money risk","reputation risk","avoidance","fantasy-vs-execution","legal risk","deployment claim not verified","fake progress","risk"])),"risks"); | ||
| for(const p of projects){const projectHits=sources.filter(x=>has(textOf(x),[p])); add("project_profile",subject(p),`${p} project profile`,projectHits,"facts"); add("technical_deployment_profile",subject(p),`${p} deployment profile`,projectHits.filter(x=>has(textOf(x),technicalWords)),"facts");} | ||
| add("technical_activity_profile","global","Global technical activity profile",sources.filter(x=>has(textOf(x),technicalWords)),"facts"); | ||
| for(const n of personNames){if(namespace==="real_life"&&auOnlyNames.includes(n))continue; const personHits=sources.filter(x=>has(textOf(x),[n])); add(namespace==="au"?"people_profile":"relationship_loop_profile",subject(n),`${n} profile`,personHits,"patterns");} | ||
| add("relationship_loop_profile","global","Global relationship/emotional loop profile",sources.filter(x=>has(textOf(x),relationshipWords)),"patterns"); | ||
| if(namespace==="au")add("writing_canon_profile","global","AU writing canon",sources.filter(x=>has(textOf(x),["AU","canon","chapter","story","fictional continuity","Melodee","Mang Rudy","Mang Bert","Ate Dhes"])),"facts"); | ||
| return out.filter(p=>p.summary&&!p.summary.includes(blocked));} | ||
| function loopInputs(user_id:string,namespace:MemoryBridgeNamespace,sources:any[]){const specs=[ ["deployment_blocker","deployment","Deployment blocker",["deployment blocked","Vercel failed","CI failed","failed migration","blocked"],8], ["review_backlog","candidate_review","Candidate review backlog",["candidate review backlog","pending review"],5], ["risk","global","Risk follow-up",["gambling","money risk","reputation risk","legal risk"],9], ["relationship_loop","relationship","Relationship/emotional loop",["relationship loop","emotional loop","relapse","unresolved"],7], ["project_followup","project","Project follow-up",["what next","next action","todo","follow up","pending"],5] ] as const; return specs.flatMap(([loop_type,sk,title,words,severity])=>{const items=sources.filter(x=>has(textOf(x),words as unknown as string[]));return items.length?[{user_id,namespace,loop_type,subject_key:sk,title,description:items.map(textOf).slice(0,5).join("\n"),severity,evidence_refs:items.map((x:any)=>ref(x.raw_text?"event":"candidate",x)),next_action:"Review and close when source evidence is resolved."}]:[];});} | ||
| export async function runDailyMemoryCompaction(client:MemoryBridgeDbClient,input:MemoryCompactionScope):Promise<MemoryCompactionResult>{const since=input.since??new Date(Date.now()-24*60*60*1000).toISOString(); const base={ok:false,namespace:input.namespace,dry_run:!!input.dry_run,updated_profiles:[],opened_or_updated_loops:[],reviewed_candidates:[],captured_events:[],feedback_events:[],blockers:[],warnings:[]} as MemoryCompactionResult; try{const events=await list(client,"memory_events",{...input,since},["captured","reviewed","promoted"]);const candidates=await list(client,"memory_capture_candidates",{...input,since},["approved","captured","rejected","duplicate"]);const feedback=await list(client,"memory_feedback_events",{...input,since});base.reviewed_candidates=candidates;base.captured_events=events;base.feedback_events=feedback;const usable=[...events,...candidates.filter(c=>["approved","captured"].includes(c.status))]; if(!usable.length){return {...base,ok:true,warnings:["no_source_data_available"],next_step:"Capture or approve memory before compaction."};} | ||
| for(const p of profileInputs(input.user_id,input.namespace,usable)){const r=await upsertVersionedMemoryProfile(client,{...p,dry_run:input.dry_run}); if(r.ok)base.updated_profiles.push(r.profile); else base.warnings.push(...r.warnings,...r.blockers);} | ||
| for(const l of loopInputs(input.user_id,input.namespace,usable)){const r=await upsertOpenLoop(client,{...l,dry_run:input.dry_run}); if(r.ok)base.opened_or_updated_loops.push(r.open_loop); else base.warnings.push(...r.warnings,...r.blockers);} | ||
| const safeEvents=events.map((e:MemoryEvent)=>({...e,raw_text:clean(e.raw_text),extracted_summary:e.extracted_summary?clean(e.extracted_summary):e.extracted_summary})); const pack={...buildDailyContextPack(input.namespace,input.user_id,safeEvents),open_loops:base.opened_or_updated_loops,risks:(base.updated_profiles as any[]).filter(p=>p.profile_type==="risk_profile")}; if(input.dry_run)base.daily_pack=pack; else {await client.from("memory_context_packs").update({status:"superseded",updated_at:new Date().toISOString()}).eq("user_id",input.user_id).eq("namespace",input.namespace).eq("pack_type","daily").eq("status","active").select("*").single(); const ins=await client.from("memory_context_packs").insert({...pack,status:"active"}).select("*").single(); if(ins.error||!ins.data)base.warnings.push(ins.error?.message??"context_pack_write_failed"); else base.daily_pack=ins.data;} return {...base,ok:true};}catch(e){return {...base,blockers:["compaction_failed"],warnings:[e instanceof Error?e.message:"unknown failure"],next_step:"Check compaction source table schemas and RLS."};}} | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
Next.js 15 App Router page static rendering force-dynamic route segment config💡 Result:
In Next.js 15, the
dynamicroute segment configuration option remains the primary mechanism to explicitly control the rendering mode of a page or layout [1][2]. To force a route to be rendered dynamically (on every request), you can export the following from yourpage.tsx,layout.tsx, or Route Handler: export const dynamic = 'force-dynamic'; Effects of 'force-dynamic': - It forces the entire route segment to be rendered at request time [2][3]. - It is equivalent to setting allfetchrequests within that segment to{ cache: 'no-store' }[2][4]. - It ensures the route is not pre-rendered at build time [5]. Distinction between Rendering and Caching: It is important to distinguish between server-side rendering (the generation of the page) and the Next.js Router Cache (a client-side cache) [4][6]. Whileexport const dynamic = 'force-dynamic'forces the page to be rendered dynamically on the server for each request, the client-side Router Cache may still store the resulting page navigation to improve perceived performance [7][6]. If you observe that a page seems to be "cached" despite usingforce-dynamic, you may be encountering the client-side Router Cache [6]. You can control the duration of this cache in yournext.config.jsvia theexperimental.staleTimesconfiguration [7]. Additionally, in Next.js 15, you can opt into dynamic behavior at a more granular level by invoking dynamic APIs [8][3]. Theconnection()function fromnext/servercan be used within an asynchronous function to signal that the execution should be dynamic, ensuring the route is rendered at request time [9]. Note: Thedynamicconfiguration takes precedence over individualfetchoptions within the same segment [4]. If you need a more precise approach, prefer using{ cache: 'no-store' }on specificfetchcalls rather than forcing the entire route to be dynamic [4].Citations:
Force this admin page to render dynamically. Add
export const dynamic = "force-dynamic";so the runtime safety gates andsincetimestamp aren’t frozen at build time.Proposed fix
import { resolvePandoraRuntimeSafetyConfig } from "`@/lib/config/pandora-runtime-safety-config`"; + +export const dynamic = "force-dynamic"; + export default function MemoryCompactionAdminPage(){const r=resolvePandoraRuntimeSafetyConfig(); const safe=[📝 Committable suggestion
🤖 Prompt for AI Agents