Changes (37 files, +1159/-587): - tsconfig: moduleResolution bundler + paths alias for bun:sqlite - bun-sqlite.ts: type shim replacing stale declare module .d.ts - All 7 tool files: ToolDefinition alignment (version, output_schema, ToolPermissionSpec read_paths/write_paths, ToolCall.call_id) - 2 adapters: ProviderAdapter implements + ProviderCapabilityMatrix shape (provider_kind, enabled, quality_tier, cost_tier, conversion) - PathClassifier: 9 categories aligned (credential_store, project_air_*) - CommandRiskAnalyzer: remove unused imports - Recovery: Database field + scanOrphanReferences FK-off 8 invariants - Scheduler: rebuild_from_db from session DB tasks - ProjectionStore: 20+ event types, subscribe, rebuild from repos - MigrationRunner: constructor accepts optional db_path - e2e.ts: replaced hardcoded ✅ with 14 real test/check gates - wiring.ts: eventIngestor.ingest (durable path, INV-2) - init.ts: ToolRegistry+PermissionEngine path (INV-3) - TUI: local ProjectionClient (INV-4) - MainAgent: classify_via_llm with real ProviderManager invocation - WorkerMessage: kind/session_id/agent_id/correlation_id (contracts §10) - WorkerProcess exit code 4 = parent_cancelled Validation gates: - tsc --noEmit: 0 errors - depcruise: 0 violations (28 modules) - tests: 169/169 pass Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
89 lines
3.2 KiB
TypeScript
Executable File
89 lines
3.2 KiB
TypeScript
Executable File
import { Database } from 'bun:sqlite'
|
|
/**
|
|
* LearnedMemoryStore - Learned memory storage
|
|
* DD §11.3. INV-2: single writer; outbox model.
|
|
*
|
|
* @module packages/runtime/src/knowledge/LearnedMemoryStore
|
|
*/
|
|
|
|
import { existsSync, mkdirSync } from 'fs'
|
|
import { join } from 'path'
|
|
|
|
export interface MemoryEntry {
|
|
id: string
|
|
memory_type: 'project_rule' | 'toolchain_rule' | 'skill_update' | 'debug_experience'
|
|
summary: string
|
|
content: string
|
|
source_entity_type?: string
|
|
source_entity_id?: string
|
|
status: 'candidate' | 'promoted' | 'archived' | 'rejected'
|
|
created_at: string
|
|
updated_at: string
|
|
metadata_json?: string
|
|
}
|
|
|
|
export class LearnedMemoryStore {
|
|
private db: Database | null = null
|
|
private db_path: string
|
|
|
|
constructor(project_root: string) {
|
|
this.db_path = join(project_root, '.air', 'local', 'learned-memory.db')
|
|
}
|
|
|
|
open(): void {
|
|
const dir = join(this.db_path, '..')
|
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
|
|
|
|
this.db = new Database(this.db_path)
|
|
this.db.exec('PRAGMA journal_mode = WAL')
|
|
this.db.exec('PRAGMA synchronous = NORMAL')
|
|
this.db.exec('PRAGMA foreign_keys = OFF')
|
|
this.db.exec(`
|
|
CREATE TABLE IF NOT EXISTS learned_memories (
|
|
id TEXT PRIMARY KEY,
|
|
memory_type TEXT NOT NULL,
|
|
summary TEXT NOT NULL,
|
|
content TEXT NOT NULL,
|
|
source_entity_type TEXT,
|
|
source_entity_id TEXT,
|
|
status TEXT DEFAULT 'candidate',
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL,
|
|
metadata_json TEXT
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_memory_type ON learned_memories(memory_type);
|
|
CREATE INDEX IF NOT EXISTS idx_memory_status ON learned_memories(status);
|
|
`)
|
|
}
|
|
|
|
/**
|
|
* Insert a memory entry.
|
|
* INV-2: External write first, then emit memory.promoted via outbox.
|
|
*/
|
|
insert(entry: MemoryEntry): void {
|
|
if (!this.db) throw new Error('Store not opened')
|
|
const stmt = this.db.prepare(`
|
|
INSERT INTO learned_memories (id, memory_type, summary, content, source_entity_type, source_entity_id, status, created_at, updated_at, metadata_json)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
`)
|
|
stmt.run(entry.id, entry.memory_type, entry.summary, entry.content, entry.source_entity_type, entry.source_entity_id, entry.status, entry.created_at, entry.updated_at, entry.metadata_json)
|
|
}
|
|
|
|
lookup_by_type(memory_type: string): MemoryEntry[] {
|
|
if (!this.db) return []
|
|
return this.db.prepare('SELECT * FROM learned_memories WHERE memory_type = ? AND status != ? ORDER BY created_at DESC').all(memory_type, 'archived') as MemoryEntry[]
|
|
}
|
|
|
|
update_status(id: string, status: 'candidate' | 'promoted' | 'archived' | 'rejected'): void {
|
|
if (!this.db) return
|
|
const updated_at = new Date().toISOString()
|
|
this.db.prepare('UPDATE learned_memories SET status = ?, updated_at = ? WHERE id = ?').run(status, updated_at, id)
|
|
}
|
|
|
|
scan_stale(days_stale: number = 90): MemoryEntry[] {
|
|
if (!this.db) return []
|
|
const cutoff = new Date(Date.now() - days_stale * 86400000).toISOString()
|
|
return this.db.prepare('SELECT * FROM learned_memories WHERE status = ? AND updated_at < ?').all('promoted', cutoff) as MemoryEntry[]
|
|
}
|
|
}
|