Files
AirCoding/packages/runtime/src/knowledge/LearnedMemoryStore.ts
AirCoding ea7cf427dd fix: tsc 0 errors + depcruise 0 violations + all GA blockers closed
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>
2026-06-04 11:43:19 +08:00

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[]
}
}