Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions app/admin/memory/compaction/page.tsx
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=[
Comment on lines +1 to +2

Copy link
Copy Markdown

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 dynamic route 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 your page.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 all fetch requests 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]. While export 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 using force-dynamic, you may be encountering the client-side Router Cache [6]. You can control the duration of this cache in your next.config.js via the experimental.staleTimes configuration [7]. Additionally, in Next.js 15, you can opt into dynamic behavior at a more granular level by invoking dynamic APIs [8][3]. The connection() function from next/server can be used within an asynchronous function to signal that the execution should be dynamic, ensuring the route is rendered at request time [9]. Note: The dynamic configuration takes precedence over individual fetch options within the same segment [4]. If you need a more precise approach, prefer using { cache: 'no-store' } on specific fetch calls 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 and since timestamp 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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { resolvePandoraRuntimeSafetyConfig } from "@/lib/config/pandora-runtime-safety-config";
export default function MemoryCompactionAdminPage(){const r=resolvePandoraRuntimeSafetyConfig(); const safe=[
import { resolvePandoraRuntimeSafetyConfig } from "`@/lib/config/pandora-runtime-safety-config`";
export const dynamic = "force-dynamic";
export default function MemoryCompactionAdminPage(){const r=resolvePandoraRuntimeSafetyConfig(); const safe=[
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/admin/memory/compaction/page.tsx` around lines 1 - 2, The
MemoryCompactionAdminPage is currently rendered statically, which can freeze the
runtime safety gate data and the since timestamp at build time. Add a dynamic
rendering export alongside the existing MemoryCompactionAdminPage component so
this admin page always evaluates resolvePandoraRuntimeSafetyConfig at request
time and keeps its runtime checks current.

["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>}
8 changes: 8 additions & 0 deletions app/api/memory/jobs/daily-digest/route.ts
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()});

Copy link
Copy Markdown

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:

Zod 3.23.8 z.string().datetime offset true default

💡 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.ts

Repository: besfeng23/Memory

Length of output: 3179


Allow ISO since values with offsets
z.string().datetime() rejects offset timestamps by default, so inputs like 2026-06-28T10:00:00-07:00 fail here. If this field is meant to accept ISO datetimes, switch to datetime({ offset: true }).

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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()});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/memory/jobs/daily-digest/route.ts` at line 6, The daily digest input
schema rejects ISO timestamps with timezone offsets because the `schema`
definition uses `z.string().datetime()` for `since`. Update the `since`
validator in the `route.ts` schema to accept offset-aware ISO datetimes by using
the datetime option with offsets enabled, keeping the existing optional behavior
intact.

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});}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Do not accept compaction user_id from the request body

When the internal job token is accepted, this route falls back to parsed.data.user_id and then uses the bridge admin client, so any caller with that token can choose another user's id and write profiles, open loops, and context packs for that user. That bypasses the AGENTS.md requirement that memory writes be tied to server-derived or RLS-derived identity; use only a server-side configured user/session mapping instead of a request-body user_id.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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 runDailyMemoryCompaction returns ok:false with blockers such as compaction_failed. Job schedulers and alerts will treat failed compactions as successful.

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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});}
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},{status:result.ok?200:500});}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/memory/jobs/daily-digest/route.ts` at line 8, The POST handler in
daily-digest/route.ts always responds with HTTP 200 even when
runDailyMemoryCompaction returns ok:false, so update the final NextResponse.json
call to set an error status whenever the result indicates failure. Use the
existing runDailyMemoryCompaction result fields (ok, blockers, next_step) to
choose a non-2xx status for failed compactions while keeping success responses
unchanged.

19 changes: 19 additions & 0 deletions docs/pandora-memory-autopilot.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,22 @@ Phase 5A is deterministic and keyword-based. It does not implement the future re
## Phase 5B Candidate Review

Phase 5A queues durable memory candidates automatically, but it does not make them permanent memory by default. Phase 5B adds the operator review console at `/admin/memory/candidates`, where Joven can approve, reject, edit, mark duplicate, or capture candidates. This keeps Pandora teachable: autopilot surfaces what might matter, while human review prevents noisy, sensitive, wrong, duplicate, or wrong-namespace items from polluting durable memory.

## Phase 5C Daily Compaction

Phase 5A queues candidates. Phase 5B reviews and captures candidates. Phase 5C compacts reviewed/captured memory into versioned profiles, open loops, and daily context packs. This is the deterministic compounding layer that lets Pandora improve over time without enabling model calls, embeddings, semantic retrieval, public reads, public writes, or permanent auto-capture.

Recommended environment 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
```

`PANDORA_INTERNAL_JOB_TOKEN` must remain server-only and must never be exposed as a `NEXT_PUBLIC_*` variable. Do not enable model calls or embeddings just for compaction, and do not enable permanent auto-capture yet.
94 changes: 94 additions & 0 deletions docs/pandora-memory-compaction.md
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.
35 changes: 35 additions & 0 deletions lib/services/memory-compaction-service.ts
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);}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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 since and status in memory. Busy tables with many rejected/blocked/out-of-window rows can cause eligible records in the compaction window to be skipped. Push since/status filtering into the query before limit(100).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/services/memory-compaction-service.ts` at line 17, The list() helper in
memory-compaction-service is applying since/status filtering after limit(100),
which can skip eligible rows on busy tables. Move the source filters into the
query built from client.from(table) before the limit is applied, using the same
MemoryCompactionScope fields and statuses constraints, and keep the existing
blocked_secret/textOf filtering as needed for any remaining in-memory checks.

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."};}}
Loading
Loading