Skip to content

Commit 87f132f

Browse files
committed
feat(memory): inject persistent markdown into prompt assembly + normalize Postgres DDL
Two memory-layer fixes that production wilds-ai depends on: 1. Persistent-memory injection. CognitiveMemoryManager.assemble now reads from an optional PersistentMemorySource and threads the trimmed text through to assembleStandardPrompt as persistentMemoryText. The source is plug-pointed via a new PersistentMemorySource interface on CognitiveMemoryConfig.read(): string | Promise<string>. Working memory remains the bounded cognitive focus; persistent memory is durable agent/user state (profile notes, preferences, identity anchors). Read failures are caught and treated as "no persistent memory available" rather than crashing prompt assembly. 2. Postgres DDL dialect normalization. Brain.ts ddlStatements were emitted in SQLite syntax; the storage adapter executed them verbatim against Postgres which rejects AUTOINCREMENT and BLOB. New _normalizeDdlForDialect rewrites at exec time: AUTOINCREMENT replaced via dialect.autoIncrementPrimaryKey(), BLOB replaced by BYTEA. SQLite path is unchanged. New unit test Brain.postgres-ddl.test.ts asserts the rewrite shape via a recording adapter. Plus reader-router.ts docstring updated to reflect the single-provider-OpenAI bench design (no behavior change). Verified: 14 memory tests pass (1 new + 13 integration).
1 parent 6eda803 commit 87f132f

6 files changed

Lines changed: 124 additions & 3 deletions

File tree

src/memory-router/reader-router.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,9 @@ import type { MemoryQueryCategory } from './routing-tables.js';
6767

6868
/**
6969
* Reader tier the router can dispatch to. Restricted to OpenAI models
70-
* the shipped presets were calibrated against. Future presets (e.g.
71-
* `claude-opus-4-7` + `gpt-5-mini`) would extend this union.
70+
* the shipped presets were calibrated against. The bench is single-
71+
* provider OpenAI for reader/classifier/judge by design; Cohere is
72+
* allowed only at the rerank stage.
7273
*/
7374
export type ReaderTier = 'gpt-4o' | 'gpt-5' | 'gpt-5-mini';
7475

src/memory/CognitiveMemoryManager.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -892,6 +892,17 @@ export class CognitiveMemoryManager implements ICognitiveMemoryManager {
892892
// Get working memory state
893893
const wmText = this.workingMemory.formatForPrompt();
894894

895+
let persistentMemoryText: string | undefined;
896+
if (this.config.persistentMemory) {
897+
try {
898+
const text = await this.config.persistentMemory.read();
899+
const trimmed = typeof text === 'string' ? text.trim() : '';
900+
persistentMemoryText = trimmed.length > 0 ? trimmed : undefined;
901+
} catch {
902+
/* non-critical */
903+
}
904+
}
905+
895906
// --- Batch 2: Check prospective memory ---
896907
const prospectiveAlerts: string[] = [];
897908
if (this.prospective) {
@@ -936,6 +947,7 @@ export class CognitiveMemoryManager implements ICognitiveMemoryManager {
936947
allocation: this.config.tokenBudget,
937948
traits: this.config.traits,
938949
workingMemoryText: wmText,
950+
persistentMemoryText,
939951
retrievedTraces: result.retrieved,
940952
prospectiveAlerts,
941953
graphContext,

src/memory/core/config.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,15 @@ export interface ReflectorConfig {
7878
llmInvoker?: (systemPrompt: string, userPrompt: string) => Promise<string>;
7979
}
8080

81+
export interface PersistentMemorySource {
82+
/**
83+
* Read markdown memory that should be injected into every assembled prompt.
84+
* Implementations are expected to handle missing/unreadable backing stores
85+
* and return an empty string when no persistent memory is available.
86+
*/
87+
read: () => string | Promise<string>;
88+
}
89+
8190
/**
8291
* Configuration for the memory graph subsystem.
8392
*
@@ -253,6 +262,15 @@ export interface CognitiveMemoryConfig {
253262
graph?: Partial<MemoryGraphConfig>;
254263
consolidation?: Partial<ConsolidationConfig>;
255264

265+
/**
266+
* Optional persistent markdown memory source injected into every prompt.
267+
*
268+
* This is separate from active working memory: working memory is the
269+
* bounded cognitive focus, while persistent memory is durable agent/user
270+
* state such as profile notes, preferences, and identity anchors.
271+
*/
272+
persistentMemory?: PersistentMemorySource;
273+
256274
// --- Cognitive Mechanisms (optional, no-op when absent) ---
257275
/** Optional per-mechanism cognitive science extensions (reconsolidation, RIF, FOK, etc.). */
258276
cognitiveMechanisms?: import('../mechanisms/types.js').CognitiveMechanismsConfig;

src/memory/retrieval/store/Brain.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -706,7 +706,7 @@ export class Brain {
706706
];
707707

708708
for (const statement of ddlStatements) {
709-
await this._adapter.exec(statement);
709+
await this._adapter.exec(this._normalizeDdlForDialect(statement));
710710
}
711711

712712
// FTS index via feature abstraction (FTS5 on SQLite, tsvector/GIN on Postgres).
@@ -727,6 +727,16 @@ export class Brain {
727727
}
728728
}
729729

730+
private _normalizeDdlForDialect(statement: string): string {
731+
if (this._features.dialect.name !== 'postgres') {
732+
return statement;
733+
}
734+
735+
return statement
736+
.replace(/\bINTEGER\s+PRIMARY\s+KEY\s+AUTOINCREMENT\b/g, this._features.dialect.autoIncrementPrimaryKey())
737+
.replace(/\bBLOB\b/g, 'BYTEA');
738+
}
739+
730740
/**
731741
* Seed `brain_meta` with mandatory keys on first creation.
732742
* Uses INSERT OR IGNORE to be idempotent on subsequent opens.
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { describe, expect, it } from 'vitest';
2+
import type {
3+
StorageAdapter,
4+
StorageParameters,
5+
StorageRunResult,
6+
} from '@framers/sql-storage-adapter';
7+
import { Brain } from '../Brain.js';
8+
9+
class RecordingPostgresAdapter implements StorageAdapter {
10+
readonly kind = 'postgres' as const;
11+
readonly execStatements: string[] = [];
12+
13+
async open(): Promise<void> {}
14+
15+
async close(): Promise<void> {}
16+
17+
async run(_sql: string, _params?: StorageParameters): Promise<StorageRunResult> {
18+
return { changes: 1 };
19+
}
20+
21+
async get<T = unknown>(sql: string, _params?: StorageParameters): Promise<T | null> {
22+
if (sql.includes('information_schema.tables') || sql.includes('information_schema.columns')) {
23+
return { exists: false } as T;
24+
}
25+
return null;
26+
}
27+
28+
async all<T = unknown>(_sql: string, _params?: StorageParameters): Promise<T[]> {
29+
return [];
30+
}
31+
32+
async exec(sql: string): Promise<void> {
33+
this.execStatements.push(sql);
34+
}
35+
36+
async transaction<T>(fn: (trx: StorageAdapter) => Promise<T>): Promise<T> {
37+
return fn(this);
38+
}
39+
}
40+
41+
describe('Brain Postgres schema initialization', () => {
42+
it('emits Postgres-compatible DDL for fresh brain schemas', async () => {
43+
const adapter = new RecordingPostgresAdapter();
44+
45+
await Brain.openWithAdapter(adapter, { brainId: 'pg-brain' });
46+
47+
const ddl = adapter.execStatements.join('\n');
48+
expect(ddl).not.toContain('AUTOINCREMENT');
49+
expect(ddl).not.toMatch(/\bBLOB\b/);
50+
expect(ddl).toContain('GENERATED ALWAYS AS IDENTITY PRIMARY KEY');
51+
expect(ddl).toContain('BYTEA');
52+
});
53+
});

tests/memory/CognitiveMemoryManager.integration.spec.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,33 @@ describe('CognitiveMemoryManager (integration)', () => {
201201
expect(assembled.contextText.length).toBeGreaterThan(0);
202202
expect(assembled.includedMemoryIds.length).toBeGreaterThan(0);
203203
});
204+
205+
it('injects configured persistent markdown memory into prompt context', async () => {
206+
const managerWithPersistentMemory = new CognitiveMemoryManager();
207+
await managerWithPersistentMemory.initialize({
208+
vectorStore: createMockVectorStore(),
209+
embeddingManager: createMockEmbeddingManager(),
210+
knowledgeGraph: createMockKnowledgeGraph(),
211+
workingMemory: createMockWorkingMemory(),
212+
agentId: 'test-agent-persistent',
213+
traits: { openness: 0.7, conscientiousness: 0.6, emotionality: 0.5 },
214+
moodProvider: () => neutralMood,
215+
featureDetectionStrategy: 'keyword',
216+
collectionPrefix: 'test-persistent',
217+
persistentMemory: {
218+
read: () => '# Persistent Self\n- User prefers compact, direct answers.',
219+
},
220+
});
221+
222+
const assembled = await managerWithPersistentMemory.assembleForPrompt(
223+
'answer style',
224+
1000,
225+
neutralMood,
226+
);
227+
228+
expect(assembled.contextText).toContain('## Persistent Memory');
229+
expect(assembled.contextText).toContain('User prefers compact, direct answers.');
230+
});
204231
});
205232

206233
describe('getMemoryHealth', () => {

0 commit comments

Comments
 (0)