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
28 changes: 28 additions & 0 deletions web/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,37 @@ CREATE TABLE IF NOT EXISTS episodic_memory (
reclassified INTEGER NOT NULL DEFAULT 0,
thread_id TEXT, -- conversation thread for dreaming cycle
executor TEXT, -- which executor handled this
complexity_tier TEXT, -- aegis#563: procedureKey complement (low|mid|high); NULL for non-dispatcher producers
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);

-- aegis#564 Phase 2: shadow-read drift log for cached-vs-derived stats on
-- procedural_memory. Populated by getProcedureWithDerivedStats /
-- getAllProceduresWithDerivedStats. Phase 3 gate: drift row p95 within
-- tolerance across a 7-day window before dropping the cached aggregate
-- columns from procedural_memory.
CREATE TABLE IF NOT EXISTS shadow_read_drift (
id INTEGER PRIMARY KEY AUTOINCREMENT,
reader TEXT NOT NULL,
task_pattern TEXT NOT NULL,
cached_count INTEGER NOT NULL,
cached_success_count INTEGER NOT NULL,
cached_fail_count INTEGER NOT NULL,
cached_avg_latency_ms REAL NOT NULL,
cached_avg_cost REAL NOT NULL,
cached_last_used TEXT,
derived_count INTEGER NOT NULL,
derived_success_count INTEGER NOT NULL,
derived_fail_count INTEGER NOT NULL,
derived_avg_latency_ms REAL NOT NULL,
derived_avg_cost REAL NOT NULL,
derived_last_used TEXT,
pre_tier_count INTEGER NOT NULL DEFAULT 0,
sampled_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_shadow_read_drift_reader_sampled
ON shadow_read_drift(reader, sampled_at);

CREATE TABLE IF NOT EXISTS procedural_memory (
id INTEGER PRIMARY KEY AUTOINCREMENT,
task_pattern TEXT NOT NULL UNIQUE,
Expand Down
4 changes: 2 additions & 2 deletions web/src/dashboard.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Operator dashboard — server-rendered system health, memory, goals, cost tracking
// Auth-gated via existing bearerAuth middleware

import { getAllProcedures, getActiveAgendaItems, getActiveGoals } from './kernel/memory/index.js';
import { getAllProceduresWithDerivedStats, getActiveAgendaItems, getActiveGoals } from './kernel/memory/index.js';
import { getMemoryStats } from './kernel/memory-adapter.js';
import type { MemoryServiceBinding } from './types.js';
import type { ProceduralEntry } from './kernel/types.js';
Expand Down Expand Up @@ -115,7 +115,7 @@ export async function getDashboardData(db: D1Database, memoryBinding?: MemorySer
taskCompletedRows,
taskRecentRows,
] = await Promise.all([
getAllProcedures(db),
getAllProceduresWithDerivedStats(db, { reader: 'dashboard' }),
getActiveAgendaItems(db),
getActiveGoals(db),
memoryBinding ? getMemoryStats(memoryBinding) : Promise.resolve({ total_active: 0, topics: [], recalled_last_24h: 0, strength_distribution: { low: 0, medium: 0, high: 0 } }),
Expand Down
118 changes: 118 additions & 0 deletions web/src/kernel/memory/episodic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,124 @@ export async function getEpisodeStats(db: D1Database, intentClass: string): Prom
};
}

// ─── Stats by (intent_class, complexity_tier) — derived-stats path ──
// aegis#563 + aegis#564: stats keyed by (intent_class, complexity_tier) —
// matches procedural_memory's procedureKey shape so the projection-source
// path can derive procedural aggregates at read time.
//
// The helpers rely on episodic_memory.complexity_tier (added in the same
// schema migration that adds these functions). Rows without a tier value
// are excluded from derived results by the WHERE complexity_tier IS NOT
// NULL guard. Consumers that want stricter protection against retroactive
// backfills can add their own time-based filter in a wrapper.

export async function getEpisodeStatsByComplexity(
db: D1Database,
intentClass: string,
complexityTier: string,
): Promise<{
count: number;
successCount: number;
successRate: number;
avgCost: number;
avgLatency: number;
lastUsed: string | null;
} | null> {
// SUM(CASE outcome='success' ...) returns an exact integer — don't
// reconstruct successCount from count * avgSuccessRate downstream (FP
// rounding on ugly rates would break strict-equality drift checks).
const row = await db.prepare(`
SELECT
COUNT(*) as count,
SUM(CASE WHEN outcome = 'success' THEN 1 ELSE 0 END) as success_count,
AVG(cost) as avg_cost,
AVG(latency_ms) as avg_latency,
MAX(created_at) as last_used
FROM episodic_memory
WHERE intent_class = ?
AND complexity_tier = ?
`).bind(intentClass, complexityTier).first<{
count: number;
success_count: number;
avg_cost: number;
avg_latency: number;
last_used: string | null;
}>();

if (!row || row.count === 0) return null;
return {
count: row.count,
successCount: row.success_count,
successRate: row.count > 0 ? row.success_count / row.count : 0,
avgCost: row.avg_cost,
avgLatency: row.avg_latency,
lastUsed: row.last_used,
};
}

// aegis#564 Phase 2: bulk variant for dashboard / observability / decision-docs.
// One GROUP BY intent_class, complexity_tier scan covers both the derived
// slice (non-null tier) and the pre-tier ghost slice (NULL tier) so callers
// avoid N+1 queries. Returns a Map keyed on intent_class with nested
// derived-by-tier and the pre-tier count.
export interface EpisodeStatsAggregate {
derived: Record<string, {
count: number;
successCount: number;
failCount: number;
avgCost: number;
avgLatency: number;
lastUsed: string | null;
}>;
preTierCount: number;
}

export async function getAllEpisodeStatsByComplexity(
db: D1Database,
): Promise<Map<string, EpisodeStatsAggregate>> {
// Single scan: grouping on (intent_class, complexity_tier) folds both
// derived (non-null tier) and pre-tier (NULL tier) rows into one query.
const result = await db.prepare(`
SELECT
intent_class,
complexity_tier,
COUNT(*) as count,
SUM(CASE WHEN outcome = 'success' THEN 1 ELSE 0 END) as success_count,
AVG(cost) as avg_cost,
AVG(latency_ms) as avg_latency,
MAX(created_at) as last_used
FROM episodic_memory
GROUP BY intent_class, complexity_tier
`).all<{
intent_class: string;
complexity_tier: string | null;
count: number;
success_count: number;
avg_cost: number;
avg_latency: number;
last_used: string | null;
}>();

const byClass = new Map<string, EpisodeStatsAggregate>();
for (const row of result.results) {
const entry = byClass.get(row.intent_class) ?? { derived: {}, preTierCount: 0 };
if (row.complexity_tier === null) {
entry.preTierCount = row.count;
} else {
entry.derived[row.complexity_tier] = {
count: row.count,
successCount: row.success_count,
failCount: row.count - row.success_count,
avgCost: row.avg_cost,
avgLatency: row.avg_latency,
lastUsed: row.last_used,
};
}
byClass.set(row.intent_class, entry);
}
return byClass;
}

// ─── Conversation History ───────────────────────────────────

export async function getConversationHistory(
Expand Down
4 changes: 2 additions & 2 deletions web/src/kernel/memory/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export { recordEpisode, retrogradeEpisode, sanitizeEpisodicOutcome, getRecentEpisodes, getEpisodeStats, getConversationHistory, estimateTokens, budgetConversationHistory } from './episodic.js';
export { PROCEDURE_MIN_SUCCESSES, PROCEDURE_MIN_SUCCESS_RATE, complexityTier, procedureKey, getProcedure, getAllProcedures, findNearMiss, upsertProcedure, addRefinement, degradeProcedure, maintainProcedures } from './procedural.js';
export { recordEpisode, retrogradeEpisode, sanitizeEpisodicOutcome, getRecentEpisodes, getEpisodeStats, getEpisodeStatsByComplexity, getAllEpisodeStatsByComplexity, type EpisodeStatsAggregate, getConversationHistory, estimateTokens, budgetConversationHistory } from './episodic.js';
export { PROCEDURE_MIN_SUCCESSES, PROCEDURE_MIN_SUCCESS_RATE, complexityTier, procedureKey, getProcedure, getAllProcedures, getProcedureWithDerivedStats, getAllProceduresWithDerivedStats, type DriftLogOpts, findNearMiss, upsertProcedure, addRefinement, degradeProcedure, maintainProcedures } from './procedural.js';
export { normalizeTopic, tokenize, jaccardSimilarity, recordMemory, searchMemoryByKeywords, getMemoryEntries, recallMemory, computeEwaScore, getAllMemoryForContext } from './semantic.js';
export { pruneMemory } from './pruning.js';
export { consolidateEpisodicToSemantic } from './consolidation.js';
Expand Down
Loading