@stackbilt/aegis-core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +96 -0
- package/schema.sql +586 -0
- package/src/adapters/voice/cloudflare-agent.ts +34 -0
- package/src/auth.ts +124 -0
- package/src/bluesky.ts +464 -0
- package/src/claude-tools/content.ts +188 -0
- package/src/claude-tools/email.ts +69 -0
- package/src/claude-tools/github.ts +440 -0
- package/src/claude-tools/goals.ts +116 -0
- package/src/claude-tools/index.ts +353 -0
- package/src/claude-tools/web.ts +59 -0
- package/src/claude.ts +406 -0
- package/src/codebeast.ts +200 -0
- package/src/composite.ts +715 -0
- package/src/content/column.ts +80 -0
- package/src/content/hero-image.ts +47 -0
- package/src/content/index.ts +27 -0
- package/src/content/journal.ts +91 -0
- package/src/content/roundtable.ts +163 -0
- package/src/core.ts +309 -0
- package/src/dashboard.ts +620 -0
- package/src/decision-docs.ts +284 -0
- package/src/dispatch.ts +13 -0
- package/src/edge-env.ts +58 -0
- package/src/email.ts +850 -0
- package/src/exports.ts +156 -0
- package/src/github-projects.ts +312 -0
- package/src/github.ts +670 -0
- package/src/groq.ts +247 -0
- package/src/health-page.ts +578 -0
- package/src/index.ts +89 -0
- package/src/kernel/argus-actions.ts +397 -0
- package/src/kernel/argus-correlation.ts +639 -0
- package/src/kernel/board.ts +91 -0
- package/src/kernel/briefing.ts +177 -0
- package/src/kernel/classify-memory-topic.ts +166 -0
- package/src/kernel/cognition.ts +377 -0
- package/src/kernel/court-cards.ts +163 -0
- package/src/kernel/dispatch.ts +587 -0
- package/src/kernel/domain.ts +50 -0
- package/src/kernel/dynamic-tools.ts +322 -0
- package/src/kernel/executor-port.ts +45 -0
- package/src/kernel/executors/claude.ts +73 -0
- package/src/kernel/executors/direct.ts +237 -0
- package/src/kernel/executors/groq.ts +18 -0
- package/src/kernel/executors/index.ts +87 -0
- package/src/kernel/executors/tarotscript.ts +104 -0
- package/src/kernel/executors/workers-ai.ts +54 -0
- package/src/kernel/insight-cache.ts +76 -0
- package/src/kernel/memory/agenda.ts +200 -0
- package/src/kernel/memory/blocks.ts +188 -0
- package/src/kernel/memory/consolidation.ts +194 -0
- package/src/kernel/memory/episodic.ts +241 -0
- package/src/kernel/memory/goals.ts +156 -0
- package/src/kernel/memory/graph.ts +290 -0
- package/src/kernel/memory/index.ts +11 -0
- package/src/kernel/memory/insights.ts +316 -0
- package/src/kernel/memory/procedural.ts +467 -0
- package/src/kernel/memory/pruning.ts +67 -0
- package/src/kernel/memory/recall.ts +367 -0
- package/src/kernel/memory/semantic.ts +315 -0
- package/src/kernel/memory/synthesis.ts +161 -0
- package/src/kernel/memory-adapter.ts +369 -0
- package/src/kernel/memory-guardrails.ts +76 -0
- package/src/kernel/port.ts +23 -0
- package/src/kernel/resilience.ts +322 -0
- package/src/kernel/router.ts +471 -0
- package/src/kernel/scheduled/agent-dispatch.ts +252 -0
- package/src/kernel/scheduled/argus-analytics.ts +247 -0
- package/src/kernel/scheduled/argus-heartbeat.ts +320 -0
- package/src/kernel/scheduled/argus-notify.ts +348 -0
- package/src/kernel/scheduled/board-sync.ts +110 -0
- package/src/kernel/scheduled/ci-watcher.ts +125 -0
- package/src/kernel/scheduled/cognitive-metrics.ts +377 -0
- package/src/kernel/scheduled/consolidation.ts +229 -0
- package/src/kernel/scheduled/content-drip.ts +47 -0
- package/src/kernel/scheduled/content.ts +6 -0
- package/src/kernel/scheduled/conversation-facts.ts +204 -0
- package/src/kernel/scheduled/cost-report.ts +84 -0
- package/src/kernel/scheduled/curiosity.ts +219 -0
- package/src/kernel/scheduled/dev-activity.ts +44 -0
- package/src/kernel/scheduled/digest.ts +317 -0
- package/src/kernel/scheduled/dreaming/agenda-triage.ts +115 -0
- package/src/kernel/scheduled/dreaming/facts.ts +239 -0
- package/src/kernel/scheduled/dreaming/index.ts +8 -0
- package/src/kernel/scheduled/dreaming/llm.ts +33 -0
- package/src/kernel/scheduled/dreaming/pattern-synthesis.ts +124 -0
- package/src/kernel/scheduled/dreaming/persona.ts +75 -0
- package/src/kernel/scheduled/dreaming/symbolic.ts +31 -0
- package/src/kernel/scheduled/dreaming/task-proposals.ts +80 -0
- package/src/kernel/scheduled/dreaming.ts +66 -0
- package/src/kernel/scheduled/entropy.ts +149 -0
- package/src/kernel/scheduled/escalation.ts +192 -0
- package/src/kernel/scheduled/feed-watcher.ts +206 -0
- package/src/kernel/scheduled/goals.ts +214 -0
- package/src/kernel/scheduled/governance.ts +41 -0
- package/src/kernel/scheduled/heartbeat.ts +220 -0
- package/src/kernel/scheduled/inbox-processor.ts +174 -0
- package/src/kernel/scheduled/index.ts +245 -0
- package/src/kernel/scheduled/issue-proposer.ts +478 -0
- package/src/kernel/scheduled/issue-watcher.ts +128 -0
- package/src/kernel/scheduled/pr-automerge.ts +213 -0
- package/src/kernel/scheduled/product-health.ts +107 -0
- package/src/kernel/scheduled/reflection.ts +373 -0
- package/src/kernel/scheduled/self-improvement.ts +114 -0
- package/src/kernel/scheduled/social-engage.ts +175 -0
- package/src/kernel/scheduled/task-audit.ts +60 -0
- package/src/kernel/symbolic.ts +156 -0
- package/src/kernel/types.ts +145 -0
- package/src/landing.ts +1190 -0
- package/src/lib/audit-chain/chain.ts +28 -0
- package/src/lib/audit-chain/types.ts +12 -0
- package/src/lib/observability/errors.ts +55 -0
- package/src/markdown.ts +164 -0
- package/src/mcp/handlers.ts +647 -0
- package/src/mcp/server.ts +184 -0
- package/src/mcp/tools.ts +316 -0
- package/src/mcp-client.ts +275 -0
- package/src/mcp-server.ts +2 -0
- package/src/operator/config.example.ts +60 -0
- package/src/operator/config.ts +60 -0
- package/src/operator/index.ts +46 -0
- package/src/operator/persona.example.ts +34 -0
- package/src/operator/persona.ts +34 -0
- package/src/operator/prompt-builder.ts +190 -0
- package/src/operator/types.ts +43 -0
- package/src/pulse.ts +1179 -0
- package/src/routes/bluesky.ts +116 -0
- package/src/routes/cc-tasks.ts +328 -0
- package/src/routes/codebeast.ts +1 -0
- package/src/routes/content.ts +194 -0
- package/src/routes/conversations.ts +25 -0
- package/src/routes/dynamic-tools.ts +111 -0
- package/src/routes/feedback.ts +192 -0
- package/src/routes/health.ts +147 -0
- package/src/routes/messages.ts +228 -0
- package/src/routes/observability.ts +82 -0
- package/src/routes/operator-logs.ts +42 -0
- package/src/routes/pages.ts +96 -0
- package/src/routes/sessions.ts +54 -0
- package/src/sanitize.ts +73 -0
- package/src/schema-enums.ts +155 -0
- package/src/search.ts +112 -0
- package/src/task-intelligence.ts +497 -0
- package/src/types.ts +194 -0
- package/src/ui.ts +5 -0
- package/src/version.ts +3 -0
- package/src/workers-ai-chat.ts +333 -0
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
import type { ProceduralEntry, ProceduralStatus, Refinement } from '../types.js';
|
|
2
|
+
import {
|
|
3
|
+
getEpisodeStatsByComplexity,
|
|
4
|
+
getAllEpisodeStatsByComplexity,
|
|
5
|
+
type EpisodeStatsAggregate,
|
|
6
|
+
} from './episodic.js';
|
|
7
|
+
|
|
8
|
+
// ─── Constants ──────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
export const PROCEDURE_MIN_SUCCESSES = 3;
|
|
11
|
+
export const PROCEDURE_MIN_SUCCESS_RATE = 0.7;
|
|
12
|
+
const CIRCUIT_BREAKER_THRESHOLD = 2;
|
|
13
|
+
const PROCEDURE_STALENESS_DAYS = 14;
|
|
14
|
+
const UTILITY_MIN_INVOCATIONS = 5;
|
|
15
|
+
const UTILITY_MIN_SUCCESS_RATE = 0.5;
|
|
16
|
+
const CANDIDATE_PROMOTION_THRESHOLD = 3; // consecutive successes to promote candidate
|
|
17
|
+
|
|
18
|
+
// ─── Procedure Key Helpers ──────────────────────────────────
|
|
19
|
+
|
|
20
|
+
export function complexityTier(complexity: number | undefined): string {
|
|
21
|
+
const c = complexity ?? 2;
|
|
22
|
+
if (c <= 1) return 'low';
|
|
23
|
+
if (c >= 3) return 'high';
|
|
24
|
+
return 'mid';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function procedureKey(classification: string, complexity: number | undefined): string {
|
|
28
|
+
return `${classification}:${complexityTier(complexity)}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ─── Procedural Memory ──────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
export async function getProcedure(db: D1Database, taskPattern: string): Promise<ProceduralEntry | null> {
|
|
34
|
+
const row = await db.prepare(
|
|
35
|
+
'SELECT * FROM procedural_memory WHERE task_pattern = ?'
|
|
36
|
+
).bind(taskPattern).first<ProceduralEntry>();
|
|
37
|
+
return row ?? null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function getAllProcedures(db: D1Database): Promise<ProceduralEntry[]> {
|
|
41
|
+
const result = await db.prepare(
|
|
42
|
+
'SELECT * FROM procedural_memory ORDER BY last_used DESC'
|
|
43
|
+
).all();
|
|
44
|
+
return result.results as unknown as ProceduralEntry[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// aegis#564 — compat helper for callers that read the five cached
|
|
48
|
+
// aggregate columns (success_count, fail_count, avg_latency_ms, avg_cost,
|
|
49
|
+
// last_used). Phased migration:
|
|
50
|
+
// Phase 1: pass-through to getProcedure. Established the API.
|
|
51
|
+
// Phase 2 (here): opt-in shadow-read logging against
|
|
52
|
+
// getEpisodeStatsByComplexity. Still returns cached so behavior is
|
|
53
|
+
// identical; shadow_read_drift captures cached-vs-derived gaps so
|
|
54
|
+
// the Phase 3 cached-column drop is data-driven.
|
|
55
|
+
// Phase 3: flip to derived values and drop the cached columns from
|
|
56
|
+
// procedural_memory in the same move.
|
|
57
|
+
//
|
|
58
|
+
// Drift-log opts are opt-in per caller. Omitting opts entirely = pure
|
|
59
|
+
// pass-through. Pass `{ reader: '...' }` (default sample 1.0) for cold
|
|
60
|
+
// paths; `{ reader: '...', sample: 0.1 }` for hot paths.
|
|
61
|
+
export interface DriftLogOpts {
|
|
62
|
+
/** Label for shadow_read_drift.reader — router / dashboard / ... */
|
|
63
|
+
reader: string;
|
|
64
|
+
/** Sample rate in [0, 1]. Default 1.0. Omit `opts` entirely to skip logging. */
|
|
65
|
+
sample?: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function getProcedureWithDerivedStats(
|
|
69
|
+
db: D1Database,
|
|
70
|
+
taskPattern: string,
|
|
71
|
+
opts?: DriftLogOpts,
|
|
72
|
+
): Promise<ProceduralEntry | null> {
|
|
73
|
+
const row = await getProcedure(db, taskPattern);
|
|
74
|
+
if (!row) return null;
|
|
75
|
+
|
|
76
|
+
if (opts && Math.random() < (opts.sample ?? 1.0)) {
|
|
77
|
+
// Drift log failure must never break the read. Awaited + try/catch so
|
|
78
|
+
// observability is synchronous for tests and ordered for ctx.waitUntil.
|
|
79
|
+
try {
|
|
80
|
+
await logDriftSingle(db, row, opts.reader);
|
|
81
|
+
} catch (err) {
|
|
82
|
+
console.warn('[shadow-read] drift log failed (non-fatal):',
|
|
83
|
+
err instanceof Error ? err.message : String(err));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return row;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function getAllProceduresWithDerivedStats(
|
|
91
|
+
db: D1Database,
|
|
92
|
+
opts?: DriftLogOpts,
|
|
93
|
+
): Promise<ProceduralEntry[]> {
|
|
94
|
+
const procedures = await getAllProcedures(db);
|
|
95
|
+
|
|
96
|
+
if (opts && Math.random() < (opts.sample ?? 1.0)) {
|
|
97
|
+
try {
|
|
98
|
+
await logDriftBulk(db, procedures, opts.reader);
|
|
99
|
+
} catch (err) {
|
|
100
|
+
console.warn('[shadow-read] bulk drift log failed (non-fatal):',
|
|
101
|
+
err instanceof Error ? err.message : String(err));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return procedures;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ─── Shadow-read drift logging (Phase 2) ────────────────────────
|
|
109
|
+
|
|
110
|
+
function reconstructDerivedFields(
|
|
111
|
+
stats: EpisodeStatsAggregate['derived'][string] | undefined,
|
|
112
|
+
): {
|
|
113
|
+
count: number;
|
|
114
|
+
successCount: number;
|
|
115
|
+
failCount: number;
|
|
116
|
+
avgLatency: number;
|
|
117
|
+
avgCost: number;
|
|
118
|
+
lastUsed: string | null;
|
|
119
|
+
} {
|
|
120
|
+
if (!stats) {
|
|
121
|
+
return { count: 0, successCount: 0, failCount: 0, avgLatency: 0, avgCost: 0, lastUsed: null };
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
count: stats.count,
|
|
125
|
+
successCount: stats.successCount,
|
|
126
|
+
failCount: stats.failCount,
|
|
127
|
+
avgLatency: stats.avgLatency,
|
|
128
|
+
avgCost: stats.avgCost,
|
|
129
|
+
lastUsed: stats.lastUsed,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function parseProcedureKey(taskPattern: string): { intentClass: string; tier: string } | null {
|
|
134
|
+
const idx = taskPattern.lastIndexOf(':');
|
|
135
|
+
if (idx === -1) return null;
|
|
136
|
+
return {
|
|
137
|
+
intentClass: taskPattern.slice(0, idx),
|
|
138
|
+
tier: taskPattern.slice(idx + 1),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function logDriftSingle(
|
|
143
|
+
db: D1Database,
|
|
144
|
+
cached: ProceduralEntry,
|
|
145
|
+
reader: string,
|
|
146
|
+
): Promise<void> {
|
|
147
|
+
const parsed = parseProcedureKey(cached.task_pattern);
|
|
148
|
+
if (!parsed) return; // legacy non-conforming key — skip rather than pollute the log
|
|
149
|
+
|
|
150
|
+
const derivedStats = await getEpisodeStatsByComplexity(db, parsed.intentClass, parsed.tier);
|
|
151
|
+
const preTierRow = await db.prepare(
|
|
152
|
+
`SELECT COUNT(*) as c FROM episodic_memory
|
|
153
|
+
WHERE intent_class = ? AND complexity_tier IS NULL`
|
|
154
|
+
).bind(parsed.intentClass).first<{ c: number }>();
|
|
155
|
+
|
|
156
|
+
// Use exact successCount from SUM(CASE ...) rather than reconstructing
|
|
157
|
+
// via Math.round(count * rate). Float rounding on ugly rates would
|
|
158
|
+
// break strict-equality drift checks.
|
|
159
|
+
const derived = derivedStats
|
|
160
|
+
? {
|
|
161
|
+
count: derivedStats.count,
|
|
162
|
+
successCount: derivedStats.successCount,
|
|
163
|
+
failCount: derivedStats.count - derivedStats.successCount,
|
|
164
|
+
avgLatency: derivedStats.avgLatency,
|
|
165
|
+
avgCost: derivedStats.avgCost,
|
|
166
|
+
lastUsed: derivedStats.lastUsed,
|
|
167
|
+
}
|
|
168
|
+
: reconstructDerivedFields(undefined);
|
|
169
|
+
|
|
170
|
+
await db.prepare(
|
|
171
|
+
`INSERT INTO shadow_read_drift (
|
|
172
|
+
reader, task_pattern,
|
|
173
|
+
cached_count, cached_success_count, cached_fail_count,
|
|
174
|
+
cached_avg_latency_ms, cached_avg_cost, cached_last_used,
|
|
175
|
+
derived_count, derived_success_count, derived_fail_count,
|
|
176
|
+
derived_avg_latency_ms, derived_avg_cost, derived_last_used,
|
|
177
|
+
pre_tier_count
|
|
178
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
179
|
+
).bind(
|
|
180
|
+
reader, cached.task_pattern,
|
|
181
|
+
cached.success_count + cached.fail_count,
|
|
182
|
+
cached.success_count,
|
|
183
|
+
cached.fail_count,
|
|
184
|
+
cached.avg_latency_ms,
|
|
185
|
+
cached.avg_cost,
|
|
186
|
+
cached.last_used ?? null,
|
|
187
|
+
derived.count,
|
|
188
|
+
derived.successCount,
|
|
189
|
+
derived.failCount,
|
|
190
|
+
derived.avgLatency,
|
|
191
|
+
derived.avgCost,
|
|
192
|
+
derived.lastUsed,
|
|
193
|
+
preTierRow?.c ?? 0,
|
|
194
|
+
).run();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function logDriftBulk(
|
|
198
|
+
db: D1Database,
|
|
199
|
+
procedures: ProceduralEntry[],
|
|
200
|
+
reader: string,
|
|
201
|
+
): Promise<void> {
|
|
202
|
+
if (procedures.length === 0) return;
|
|
203
|
+
|
|
204
|
+
const aggregate = await getAllEpisodeStatsByComplexity(db);
|
|
205
|
+
|
|
206
|
+
const stmt = db.prepare(
|
|
207
|
+
`INSERT INTO shadow_read_drift (
|
|
208
|
+
reader, task_pattern,
|
|
209
|
+
cached_count, cached_success_count, cached_fail_count,
|
|
210
|
+
cached_avg_latency_ms, cached_avg_cost, cached_last_used,
|
|
211
|
+
derived_count, derived_success_count, derived_fail_count,
|
|
212
|
+
derived_avg_latency_ms, derived_avg_cost, derived_last_used,
|
|
213
|
+
pre_tier_count
|
|
214
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
const batch: D1PreparedStatement[] = [];
|
|
218
|
+
for (const cached of procedures) {
|
|
219
|
+
const parsed = parseProcedureKey(cached.task_pattern);
|
|
220
|
+
if (!parsed) continue;
|
|
221
|
+
const classStats = aggregate.get(parsed.intentClass);
|
|
222
|
+
const derived = reconstructDerivedFields(classStats?.derived[parsed.tier]);
|
|
223
|
+
const preTierCount = classStats?.preTierCount ?? 0;
|
|
224
|
+
|
|
225
|
+
batch.push(stmt.bind(
|
|
226
|
+
reader, cached.task_pattern,
|
|
227
|
+
cached.success_count + cached.fail_count,
|
|
228
|
+
cached.success_count,
|
|
229
|
+
cached.fail_count,
|
|
230
|
+
cached.avg_latency_ms,
|
|
231
|
+
cached.avg_cost,
|
|
232
|
+
cached.last_used ?? null,
|
|
233
|
+
derived.count,
|
|
234
|
+
derived.successCount,
|
|
235
|
+
derived.failCount,
|
|
236
|
+
derived.avgLatency,
|
|
237
|
+
derived.avgCost,
|
|
238
|
+
derived.lastUsed,
|
|
239
|
+
preTierCount,
|
|
240
|
+
));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// D1 caps batch size (~100 statements in practice). Chunk so a large
|
|
244
|
+
// procedural_memory table doesn't trip the cap and get swallowed by
|
|
245
|
+
// the non-fatal catch upstream, silently blinding the drift dashboard.
|
|
246
|
+
const BATCH = 100;
|
|
247
|
+
for (let i = 0; i < batch.length; i += BATCH) {
|
|
248
|
+
await db.batch(batch.slice(i, i + BATCH));
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export async function findNearMiss(db: D1Database, classification: string): Promise<string | null> {
|
|
253
|
+
const prefix = classification.split('_')[0];
|
|
254
|
+
if (!prefix) return null;
|
|
255
|
+
|
|
256
|
+
const row = await db.prepare(
|
|
257
|
+
"SELECT task_pattern FROM procedural_memory WHERE task_pattern != ? AND task_pattern LIKE ? || '_%' ORDER BY success_count DESC LIMIT 1"
|
|
258
|
+
).bind(classification, prefix).first<{ task_pattern: string }>();
|
|
259
|
+
|
|
260
|
+
return row?.task_pattern ?? null;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function computeStatus(successCount: number, failCount: number, currentStatus: ProceduralStatus): ProceduralStatus {
|
|
264
|
+
if (currentStatus === 'broken') return 'broken';
|
|
265
|
+
|
|
266
|
+
const total = successCount + failCount;
|
|
267
|
+
const rate = total > 0 ? successCount / total : 0;
|
|
268
|
+
|
|
269
|
+
if (successCount >= PROCEDURE_MIN_SUCCESSES && rate >= PROCEDURE_MIN_SUCCESS_RATE) {
|
|
270
|
+
return currentStatus === 'degraded' ? 'degraded' : 'learned';
|
|
271
|
+
}
|
|
272
|
+
return currentStatus === 'degraded' ? 'degraded' : 'learning';
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export async function upsertProcedure(
|
|
276
|
+
db: D1Database,
|
|
277
|
+
taskPattern: string,
|
|
278
|
+
executor: string,
|
|
279
|
+
executorConfig: string,
|
|
280
|
+
outcome: 'success' | 'failure' | 'partial_failure',
|
|
281
|
+
latencyMs: number,
|
|
282
|
+
cost: number,
|
|
283
|
+
): Promise<void> {
|
|
284
|
+
const existing = await getProcedure(db, taskPattern);
|
|
285
|
+
const succInc = outcome === 'success' ? 1 : 0;
|
|
286
|
+
const failInc = outcome !== 'success' ? 1 : 0;
|
|
287
|
+
|
|
288
|
+
if (!existing) {
|
|
289
|
+
// First time seeing this pattern — only record executor if it succeeded
|
|
290
|
+
await db.prepare(`
|
|
291
|
+
INSERT INTO procedural_memory (task_pattern, executor, executor_config, success_count, fail_count, avg_latency_ms, avg_cost, status, consecutive_failures, refinements, last_used)
|
|
292
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, 'learning', 0, '[]', datetime('now'))
|
|
293
|
+
`).bind(
|
|
294
|
+
taskPattern,
|
|
295
|
+
outcome === 'success' ? executor : 'pending',
|
|
296
|
+
outcome === 'success' ? executorConfig : '{}',
|
|
297
|
+
succInc, failInc, latencyMs, cost,
|
|
298
|
+
).run();
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const newSuccesses = existing.success_count + succInc;
|
|
303
|
+
const newFailures = existing.fail_count + failInc;
|
|
304
|
+
const total = existing.success_count + existing.fail_count;
|
|
305
|
+
const newAvgLatency = total > 0
|
|
306
|
+
? (existing.avg_latency_ms * total + latencyMs) / (total + 1)
|
|
307
|
+
: latencyMs;
|
|
308
|
+
const newAvgCost = total > 0
|
|
309
|
+
? (existing.avg_cost * total + cost) / (total + 1)
|
|
310
|
+
: cost;
|
|
311
|
+
|
|
312
|
+
const newConsecFailures = outcome !== 'success'
|
|
313
|
+
? existing.consecutive_failures + 1
|
|
314
|
+
: 0;
|
|
315
|
+
|
|
316
|
+
let newStatus = existing.status;
|
|
317
|
+
if (outcome === 'success' && existing.status === 'degraded') {
|
|
318
|
+
newStatus = 'learned';
|
|
319
|
+
} else if (newConsecFailures >= CIRCUIT_BREAKER_THRESHOLD) {
|
|
320
|
+
newStatus = newStatus === 'broken' ? 'broken' : 'degraded';
|
|
321
|
+
} else {
|
|
322
|
+
newStatus = computeStatus(newSuccesses, newFailures, existing.status);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// ─── Probation system ─────────────────────────────────────
|
|
326
|
+
// If the executor being used differs from the learned one, treat it as a
|
|
327
|
+
// candidate. Only promote after CANDIDATE_PROMOTION_THRESHOLD consecutive
|
|
328
|
+
// successes. If it fails, discard the candidate entirely.
|
|
329
|
+
const isSameExecutor = executor === existing.executor;
|
|
330
|
+
let candidateExecutor = existing.candidate_executor ?? null;
|
|
331
|
+
let candidateSuccesses = existing.candidate_successes ?? 0;
|
|
332
|
+
let newExecutor = existing.executor;
|
|
333
|
+
let newConfig = existing.executor_config;
|
|
334
|
+
|
|
335
|
+
if (isSameExecutor) {
|
|
336
|
+
// Using the trusted executor — update normally on success
|
|
337
|
+
if (outcome === 'success') {
|
|
338
|
+
newExecutor = executor;
|
|
339
|
+
newConfig = executorConfig;
|
|
340
|
+
}
|
|
341
|
+
// Clear any candidate since we reverted to trusted
|
|
342
|
+
candidateExecutor = null;
|
|
343
|
+
candidateSuccesses = 0;
|
|
344
|
+
} else {
|
|
345
|
+
// Using a different executor — probation logic
|
|
346
|
+
if (outcome === 'success') {
|
|
347
|
+
if (candidateExecutor === executor) {
|
|
348
|
+
candidateSuccesses += 1;
|
|
349
|
+
} else {
|
|
350
|
+
// New candidate, start probation
|
|
351
|
+
candidateExecutor = executor;
|
|
352
|
+
candidateSuccesses = 1;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Promote candidate after threshold
|
|
356
|
+
if (candidateSuccesses >= CANDIDATE_PROMOTION_THRESHOLD) {
|
|
357
|
+
console.log(`[procedures] Promoting candidate "${executor}" for ${taskPattern} (${candidateSuccesses} consecutive successes)`);
|
|
358
|
+
newExecutor = executor;
|
|
359
|
+
newConfig = executorConfig;
|
|
360
|
+
candidateExecutor = null;
|
|
361
|
+
candidateSuccesses = 0;
|
|
362
|
+
}
|
|
363
|
+
} else {
|
|
364
|
+
// Candidate failed — discard it, keep trusted executor
|
|
365
|
+
if (candidateExecutor === executor) {
|
|
366
|
+
console.log(`[procedures] Discarding candidate "${executor}" for ${taskPattern} (failed during probation)`);
|
|
367
|
+
candidateExecutor = null;
|
|
368
|
+
candidateSuccesses = 0;
|
|
369
|
+
}
|
|
370
|
+
// Do NOT update newExecutor — the trusted one stays
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
await db.prepare(`
|
|
375
|
+
UPDATE procedural_memory SET
|
|
376
|
+
executor = ?, executor_config = ?,
|
|
377
|
+
success_count = ?, fail_count = ?,
|
|
378
|
+
avg_latency_ms = ?, avg_cost = ?,
|
|
379
|
+
status = ?, consecutive_failures = ?,
|
|
380
|
+
candidate_executor = ?, candidate_successes = ?,
|
|
381
|
+
last_used = datetime('now')
|
|
382
|
+
WHERE task_pattern = ?
|
|
383
|
+
`).bind(
|
|
384
|
+
newExecutor, newConfig,
|
|
385
|
+
newSuccesses, newFailures,
|
|
386
|
+
newAvgLatency, newAvgCost,
|
|
387
|
+
newStatus, newConsecFailures,
|
|
388
|
+
candidateExecutor, candidateSuccesses,
|
|
389
|
+
taskPattern,
|
|
390
|
+
).run();
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export async function addRefinement(db: D1Database, taskPattern: string, refinement: Refinement): Promise<void> {
|
|
394
|
+
const procedure = await getProcedure(db, taskPattern);
|
|
395
|
+
if (!procedure) return;
|
|
396
|
+
|
|
397
|
+
const refinements: Refinement[] = JSON.parse(procedure.refinements);
|
|
398
|
+
refinements.push(refinement);
|
|
399
|
+
const trimmed = refinements.slice(-10);
|
|
400
|
+
|
|
401
|
+
await db.prepare(
|
|
402
|
+
'UPDATE procedural_memory SET refinements = ? WHERE task_pattern = ?'
|
|
403
|
+
).bind(JSON.stringify(trimmed), taskPattern).run();
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** Degrade a procedure retroactively (called by implicit feedback) */
|
|
407
|
+
export async function degradeProcedure(db: D1Database, taskPattern: string, executor: string): Promise<void> {
|
|
408
|
+
const procedure = await getProcedure(db, taskPattern);
|
|
409
|
+
if (!procedure) return;
|
|
410
|
+
|
|
411
|
+
const newFailures = procedure.fail_count + 1;
|
|
412
|
+
const newConsecFailures = procedure.consecutive_failures + 1;
|
|
413
|
+
const newStatus = newConsecFailures >= CIRCUIT_BREAKER_THRESHOLD ? 'degraded' : procedure.status;
|
|
414
|
+
|
|
415
|
+
await db.prepare(`
|
|
416
|
+
UPDATE procedural_memory SET
|
|
417
|
+
fail_count = ?, consecutive_failures = ?, status = ?,
|
|
418
|
+
last_used = datetime('now')
|
|
419
|
+
WHERE task_pattern = ?
|
|
420
|
+
`).bind(newFailures, newConsecFailures, newStatus, taskPattern).run();
|
|
421
|
+
|
|
422
|
+
await addRefinement(db, taskPattern, {
|
|
423
|
+
timestamp: Date.now(),
|
|
424
|
+
what: `Implicit feedback: user corrected ${executor} response`,
|
|
425
|
+
why: 'User correction detected — retroactive semantic failure',
|
|
426
|
+
impact: 'negative',
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
console.log(`[procedures] Retrograde degradation: ${taskPattern} (${executor}) → status=${newStatus}, consec_failures=${newConsecFailures}`);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// ─── Procedural Maintenance ─────────────────────────────────
|
|
433
|
+
|
|
434
|
+
// Reset stale learned procedures to learning — forces re-validation
|
|
435
|
+
// after long periods of inactivity (executor landscape may have changed)
|
|
436
|
+
export async function maintainProcedures(db: D1Database): Promise<void> {
|
|
437
|
+
const staleThreshold = `-${PROCEDURE_STALENESS_DAYS} days`;
|
|
438
|
+
const stale = await db.prepare(
|
|
439
|
+
"SELECT task_pattern FROM procedural_memory WHERE status = 'learned' AND last_used < datetime('now', ?)"
|
|
440
|
+
).bind(staleThreshold).all<{ task_pattern: string }>();
|
|
441
|
+
|
|
442
|
+
for (const { task_pattern } of stale.results) {
|
|
443
|
+
await db.prepare(
|
|
444
|
+
"UPDATE procedural_memory SET status = 'learning' WHERE task_pattern = ?"
|
|
445
|
+
).bind(task_pattern).run();
|
|
446
|
+
console.log(`[procedures] Stale procedure reset: ${task_pattern} (unused ${PROCEDURE_STALENESS_DAYS}+ days)`);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Utility-rate pruning (#53) — degrade procedures with poor success rates after sufficient invocations
|
|
450
|
+
const lowUtility = await db.prepare(
|
|
451
|
+
`SELECT task_pattern, success_count, fail_count
|
|
452
|
+
FROM procedural_memory
|
|
453
|
+
WHERE status IN ('learned', 'learning')
|
|
454
|
+
AND (success_count + fail_count) >= ?
|
|
455
|
+
AND CAST(success_count AS REAL) / (success_count + fail_count) < ?`
|
|
456
|
+
).bind(UTILITY_MIN_INVOCATIONS, UTILITY_MIN_SUCCESS_RATE).all<{
|
|
457
|
+
task_pattern: string; success_count: number; fail_count: number;
|
|
458
|
+
}>();
|
|
459
|
+
|
|
460
|
+
for (const proc of lowUtility.results) {
|
|
461
|
+
await db.prepare(
|
|
462
|
+
"UPDATE procedural_memory SET status = 'degraded' WHERE task_pattern = ?"
|
|
463
|
+
).bind(proc.task_pattern).run();
|
|
464
|
+
const rate = proc.success_count / (proc.success_count + proc.fail_count);
|
|
465
|
+
console.log(`[procedures] Utility-pruned: ${proc.task_pattern} (${(rate * 100).toFixed(0)}% success over ${proc.success_count + proc.fail_count} invocations)`);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { BASE_HALF_LIFE_DAYS } from './semantic.js';
|
|
2
|
+
|
|
3
|
+
const RETENTION_THRESHOLD = 0.1;
|
|
4
|
+
|
|
5
|
+
// Prune expired + low-confidence old entries, and cap per-topic row count (#4)
|
|
6
|
+
export async function pruneMemory(db: D1Database): Promise<void> {
|
|
7
|
+
// 1. Delete expired entries
|
|
8
|
+
await db.prepare(
|
|
9
|
+
"DELETE FROM memory_entries WHERE expires_at IS NOT NULL AND expires_at < datetime('now')"
|
|
10
|
+
).run();
|
|
11
|
+
|
|
12
|
+
// 2. Delete low-confidence active entries older than 30 days
|
|
13
|
+
await db.prepare(
|
|
14
|
+
"DELETE FROM memory_entries WHERE confidence < 0.5 AND valid_until IS NULL AND created_at < datetime('now', '-30 days')"
|
|
15
|
+
).run();
|
|
16
|
+
|
|
17
|
+
// 3. Cap each topic at 20 active entries — keep highest confidence, then most recent
|
|
18
|
+
const topics = await db.prepare(
|
|
19
|
+
'SELECT DISTINCT topic FROM memory_entries WHERE valid_until IS NULL'
|
|
20
|
+
).all<{ topic: string }>();
|
|
21
|
+
|
|
22
|
+
for (const { topic } of topics.results) {
|
|
23
|
+
await db.prepare(`
|
|
24
|
+
DELETE FROM memory_entries WHERE topic = ? AND valid_until IS NULL AND id NOT IN (
|
|
25
|
+
SELECT id FROM memory_entries WHERE topic = ? AND valid_until IS NULL
|
|
26
|
+
ORDER BY confidence DESC, updated_at DESC LIMIT 20
|
|
27
|
+
)
|
|
28
|
+
`).bind(topic, topic).run();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// 3b. Purge superseded entries older than 90 days (audit trail retention limit)
|
|
32
|
+
await db.prepare(
|
|
33
|
+
"DELETE FROM memory_entries WHERE valid_until IS NOT NULL AND valid_until < datetime('now', '-90 days')"
|
|
34
|
+
).run();
|
|
35
|
+
|
|
36
|
+
// 3c. Retention-based pruning (#53) — soft-delete entries with decayed retention below threshold
|
|
37
|
+
// Strength-1 entries prune after ~46 days; strength-5 after ~232 days
|
|
38
|
+
const activeEntries = await db.prepare(
|
|
39
|
+
'SELECT id, strength, last_recalled_at, created_at FROM memory_entries WHERE valid_until IS NULL'
|
|
40
|
+
).all<{ id: number; strength: number; last_recalled_at: string | null; created_at: string }>();
|
|
41
|
+
|
|
42
|
+
const now = Date.now();
|
|
43
|
+
for (const entry of activeEntries.results) {
|
|
44
|
+
const activeDate = entry.last_recalled_at ?? entry.created_at;
|
|
45
|
+
const ts = activeDate.endsWith('Z') ? activeDate : activeDate + 'Z';
|
|
46
|
+
const daysSince = Math.max(0, (now - new Date(ts).getTime()) / 86_400_000);
|
|
47
|
+
const retention = Math.pow(2, -daysSince / ((entry.strength ?? 1) * BASE_HALF_LIFE_DAYS));
|
|
48
|
+
if (retention < RETENTION_THRESHOLD) {
|
|
49
|
+
await db.prepare(
|
|
50
|
+
"UPDATE memory_entries SET valid_until = datetime('now'), updated_at = datetime('now') WHERE id = ?"
|
|
51
|
+
).bind(entry.id).run();
|
|
52
|
+
console.log(`[memory] Retention-pruned entry #${entry.id} (R=${retention.toFixed(4)}, strength=${entry.strength})`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 4. Prune stale event dedup entries (>48h)
|
|
57
|
+
await db.prepare(
|
|
58
|
+
"DELETE FROM web_events WHERE received_at < datetime('now', '-48 hours')"
|
|
59
|
+
).run();
|
|
60
|
+
|
|
61
|
+
// 5. Prune old heartbeat results (keep last 100 or 30 days)
|
|
62
|
+
await db.prepare(`
|
|
63
|
+
DELETE FROM heartbeat_results
|
|
64
|
+
WHERE id NOT IN (SELECT id FROM heartbeat_results ORDER BY created_at DESC LIMIT 100)
|
|
65
|
+
AND created_at < datetime('now', '-30 days')
|
|
66
|
+
`).run();
|
|
67
|
+
}
|