@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,110 @@
|
|
|
1
|
+
// --- Board Sync -- GitHub Projects v2 Reconciliation ---
|
|
2
|
+
// Hourly safety net: fetches all project items from GitHub and
|
|
3
|
+
// reconciles with D1 board_items cache. Catches manual moves,
|
|
4
|
+
// external additions, and any webhook events we missed.
|
|
5
|
+
//
|
|
6
|
+
// Runs every 2 hours (time-gated in scheduled/index.ts).
|
|
7
|
+
// Cost: 2-4 GraphQL API calls per run.
|
|
8
|
+
|
|
9
|
+
import { type EdgeEnv } from '../dispatch.js';
|
|
10
|
+
import { listProjectItems, type ProjectItem } from '../../github-projects.js';
|
|
11
|
+
import { upsertBoardItem, type BoardColumn, PROJECT_TITLE, ORG_LOGIN } from '../board.js';
|
|
12
|
+
import { findOrCreateProject } from '../../github-projects.js';
|
|
13
|
+
|
|
14
|
+
/** Map GitHub Projects Status field values to our internal column names. */
|
|
15
|
+
function resolveStatus(item: ProjectItem): BoardColumn {
|
|
16
|
+
const statusNode = item.fieldValues.nodes.find(n => n.field?.name === 'Status');
|
|
17
|
+
const statusName = statusNode?.name ?? '';
|
|
18
|
+
const map: Record<string, BoardColumn> = {
|
|
19
|
+
'Backlog': 'backlog',
|
|
20
|
+
'Queued': 'queued',
|
|
21
|
+
'In Progress': 'in_progress',
|
|
22
|
+
'Blocked': 'blocked',
|
|
23
|
+
'Shipped': 'shipped',
|
|
24
|
+
};
|
|
25
|
+
return map[statusName] ?? 'backlog';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function runBoardSync(env: EdgeEnv): Promise<void> {
|
|
29
|
+
if (!env.githubToken) {
|
|
30
|
+
console.log('[board-sync] No GITHUB_TOKEN -- skipping');
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Get project ID (cached in D1 via web_events)
|
|
35
|
+
const cachedId = await env.db.prepare(
|
|
36
|
+
"SELECT received_at FROM web_events WHERE event_id = 'board_project_id'"
|
|
37
|
+
).first<{ received_at: string }>();
|
|
38
|
+
|
|
39
|
+
let projectId = cachedId?.received_at;
|
|
40
|
+
if (!projectId) {
|
|
41
|
+
try {
|
|
42
|
+
projectId = await findOrCreateProject(env.githubToken, ORG_LOGIN, PROJECT_TITLE);
|
|
43
|
+
await env.db.prepare(
|
|
44
|
+
"INSERT OR REPLACE INTO web_events (event_id, received_at) VALUES ('board_project_id', ?)"
|
|
45
|
+
).bind(projectId).run();
|
|
46
|
+
} catch (err) {
|
|
47
|
+
console.error(`[board-sync] Failed to find/create project: ${err instanceof Error ? err.message : String(err)}`);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Fetch all items from the project (paginated)
|
|
53
|
+
const allItems: ProjectItem[] = [];
|
|
54
|
+
let cursor: string | null = null;
|
|
55
|
+
const maxPages = 4; // Safety: 50 items/page x 4 = 200 max
|
|
56
|
+
|
|
57
|
+
for (let page = 0; page < maxPages; page++) {
|
|
58
|
+
const result = await listProjectItems(env.githubToken, projectId, cursor ?? undefined);
|
|
59
|
+
allItems.push(...result.items);
|
|
60
|
+
if (!result.nextCursor) break;
|
|
61
|
+
cursor = result.nextCursor;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Track which repo+number combinations we see from GitHub
|
|
65
|
+
const seenKeys = new Set<string>();
|
|
66
|
+
|
|
67
|
+
// Upsert each item into D1
|
|
68
|
+
for (const item of allItems) {
|
|
69
|
+
if (!item.content) continue; // Draft items have no content
|
|
70
|
+
|
|
71
|
+
const repo = item.content.repository.nameWithOwner;
|
|
72
|
+
const number = item.content.number;
|
|
73
|
+
const key = `${repo}:${number}`;
|
|
74
|
+
seenKeys.add(key);
|
|
75
|
+
|
|
76
|
+
const labels = item.content.labels.nodes.map(l => l.name);
|
|
77
|
+
const assignee = item.content.assignees.nodes[0]?.login ?? null;
|
|
78
|
+
const status = resolveStatus(item);
|
|
79
|
+
|
|
80
|
+
await upsertBoardItem(env.db, {
|
|
81
|
+
id: item.id,
|
|
82
|
+
project_id: projectId,
|
|
83
|
+
content_type: item.content.__typename === 'PullRequest' ? 'pr' : 'issue',
|
|
84
|
+
content_node_id: '', // Not available from list query -- set on first add
|
|
85
|
+
repo,
|
|
86
|
+
number,
|
|
87
|
+
title: item.content.title,
|
|
88
|
+
status,
|
|
89
|
+
labels: JSON.stringify(labels),
|
|
90
|
+
assignee,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Remove D1 items that no longer exist on the project
|
|
95
|
+
// (deleted via GitHub UI or removed from project)
|
|
96
|
+
const localItems = await env.db.prepare(
|
|
97
|
+
"SELECT repo, number FROM board_items WHERE status != 'shipped'"
|
|
98
|
+
).all<{ repo: string; number: number }>();
|
|
99
|
+
|
|
100
|
+
for (const local of localItems.results ?? []) {
|
|
101
|
+
const key = `${local.repo}:${local.number}`;
|
|
102
|
+
if (!seenKeys.has(key)) {
|
|
103
|
+
await env.db.prepare(
|
|
104
|
+
'DELETE FROM board_items WHERE repo = ? AND number = ?'
|
|
105
|
+
).bind(local.repo, local.number).run();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
console.log(`[board-sync] Synced ${allItems.length} items, ${seenKeys.size} active`);
|
|
110
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// --- CI Failure Watcher ---
|
|
2
|
+
// Polls GitHub Actions workflow runs for watched repos and surfaces
|
|
3
|
+
// failures via the agenda system. Zero LM calls -- pure GitHub API
|
|
4
|
+
// + string matching. Runs hourly in the heartbeat phase.
|
|
5
|
+
|
|
6
|
+
import { type EdgeEnv } from '../dispatch.js';
|
|
7
|
+
import { listWorkflowRuns, resolveRepoName, type WorkflowRun } from '../../github.js';
|
|
8
|
+
import { addAgendaItem } from '../memory/agenda.js';
|
|
9
|
+
|
|
10
|
+
const WATCHED_REPO = 'aegis-oss';
|
|
11
|
+
const WATERMARK_KEY = 'last_ci_watcher_run';
|
|
12
|
+
const CHECK_INTERVAL_HOURS = 1;
|
|
13
|
+
|
|
14
|
+
// Keywords in workflow run names or commit messages that indicate
|
|
15
|
+
// breaking-change risk to key interfaces.
|
|
16
|
+
const BREAKING_CHANGE_KEYWORDS = [
|
|
17
|
+
'breaking',
|
|
18
|
+
'interface',
|
|
19
|
+
'type change',
|
|
20
|
+
'constructor',
|
|
21
|
+
'signature',
|
|
22
|
+
'schema',
|
|
23
|
+
'migration',
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
/** Check if a workflow run is relevant to breaking-change interfaces */
|
|
27
|
+
function isBreakingChangeRelated(run: WorkflowRun): boolean {
|
|
28
|
+
const text = `${run.name} ${run.branch}`.toLowerCase();
|
|
29
|
+
return BREAKING_CHANGE_KEYWORDS.some(kw => text.includes(kw));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function runCiWatcher(env: EdgeEnv): Promise<void> {
|
|
33
|
+
if (!env.githubToken) {
|
|
34
|
+
console.log('[ci-watcher] Skipped: missing githubToken');
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Rate-limit: run once per hour
|
|
39
|
+
const lastRun = await env.db.prepare(
|
|
40
|
+
`SELECT received_at FROM web_events WHERE event_id = ?`
|
|
41
|
+
).bind(WATERMARK_KEY).first<{ received_at: string }>();
|
|
42
|
+
|
|
43
|
+
if (lastRun) {
|
|
44
|
+
const hoursSince = (Date.now() - new Date(lastRun.received_at + 'Z').getTime()) / (1000 * 60 * 60);
|
|
45
|
+
if (hoursSince < CHECK_INTERVAL_HOURS) return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const repo = resolveRepoName(WATCHED_REPO);
|
|
49
|
+
|
|
50
|
+
// Fetch recent workflow runs (main branch + PRs)
|
|
51
|
+
let runs: WorkflowRun[];
|
|
52
|
+
try {
|
|
53
|
+
runs = await listWorkflowRuns(env.githubToken, repo, undefined, 10);
|
|
54
|
+
} catch (err) {
|
|
55
|
+
console.warn(`[ci-watcher] Failed to list workflow runs for ${repo}:`, err instanceof Error ? err.message : String(err));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (runs.length === 0) {
|
|
60
|
+
console.log(`[ci-watcher] ${repo}: no workflow runs found`);
|
|
61
|
+
await advanceWatermark(env.db);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Find failed runs that completed since our last check
|
|
66
|
+
const sinceDate = lastRun
|
|
67
|
+
? new Date(lastRun.received_at + 'Z')
|
|
68
|
+
: new Date(Date.now() - CHECK_INTERVAL_HOURS * 60 * 60 * 1000);
|
|
69
|
+
|
|
70
|
+
const newFailures = runs.filter(r =>
|
|
71
|
+
r.conclusion === 'failure' &&
|
|
72
|
+
r.status === 'completed' &&
|
|
73
|
+
new Date(r.created_at) > sinceDate
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
if (newFailures.length === 0) {
|
|
77
|
+
console.log(`[ci-watcher] ${repo}: no new failures`);
|
|
78
|
+
await advanceWatermark(env.db);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
console.log(`[ci-watcher] ${repo}: ${newFailures.length} new CI failure(s)`);
|
|
83
|
+
|
|
84
|
+
for (const run of newFailures) {
|
|
85
|
+
// Dedup: skip if we already created an agenda item for this run
|
|
86
|
+
const existing = await env.db.prepare(
|
|
87
|
+
`SELECT 1 FROM web_events WHERE event_id = ? LIMIT 1`
|
|
88
|
+
).bind(`ci-fail:${repo}:${run.id}`).first();
|
|
89
|
+
|
|
90
|
+
if (existing) continue;
|
|
91
|
+
|
|
92
|
+
const isBreaking = isBreakingChangeRelated(run);
|
|
93
|
+
const priority = run.branch === 'main'
|
|
94
|
+
? 'high'
|
|
95
|
+
: (isBreaking ? 'high' : 'medium');
|
|
96
|
+
|
|
97
|
+
const breakingTag = isBreaking ? ' [possible breaking change]' : '';
|
|
98
|
+
const branchTag = run.branch === 'main' ? ' (main branch)' : ` (branch: ${run.branch})`;
|
|
99
|
+
|
|
100
|
+
await addAgendaItem(
|
|
101
|
+
env.db,
|
|
102
|
+
`CI failure: "${run.name}"${branchTag}${breakingTag}`,
|
|
103
|
+
`Workflow run #${run.id} failed on ${run.branch} (event: ${run.event}). ` +
|
|
104
|
+
`Created: ${run.created_at}. ` +
|
|
105
|
+
`${isBreaking ? 'May affect key interfaces or schemas. ' : ''}` +
|
|
106
|
+
`URL: ${run.url}`,
|
|
107
|
+
priority,
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
// Mark this run as reported
|
|
111
|
+
await env.db.prepare(
|
|
112
|
+
`INSERT OR REPLACE INTO web_events (event_id, received_at) VALUES (?, datetime('now'))`
|
|
113
|
+
).bind(`ci-fail:${repo}:${run.id}`).run();
|
|
114
|
+
|
|
115
|
+
console.log(`[ci-watcher] Agenda item created for run #${run.id} (${priority} priority${breakingTag})`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
await advanceWatermark(env.db);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function advanceWatermark(db: D1Database): Promise<void> {
|
|
122
|
+
await db.prepare(
|
|
123
|
+
`INSERT OR REPLACE INTO web_events (event_id, received_at) VALUES (?, datetime('now'))`
|
|
124
|
+
).bind(WATERMARK_KEY).run();
|
|
125
|
+
}
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
// Cognitive Self-Measurement (#255)
|
|
2
|
+
// Computes whether AEGIS is getting smarter by aggregating existing data.
|
|
3
|
+
// Runs daily. Stores a snapshot to D1 for trend analysis.
|
|
4
|
+
// No LLM calls — pure D1 queries.
|
|
5
|
+
|
|
6
|
+
import { type EdgeEnv } from '../dispatch.js';
|
|
7
|
+
|
|
8
|
+
// ─── Metrics Schema ──────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
export interface CognitiveSnapshot {
|
|
11
|
+
// Dispatch quality
|
|
12
|
+
dispatch_success_rate_7d: number; // % success in last 7 days
|
|
13
|
+
dispatch_success_rate_prior_7d: number; // % success in prior 7 days (for delta)
|
|
14
|
+
dispatch_volume_7d: number;
|
|
15
|
+
avg_cost_7d: number;
|
|
16
|
+
avg_cost_prior_7d: number;
|
|
17
|
+
avg_latency_ms_7d: number;
|
|
18
|
+
avg_latency_ms_prior_7d: number;
|
|
19
|
+
reclassification_rate_7d: number; // % of dispatches that got reclassified
|
|
20
|
+
|
|
21
|
+
// Procedural learning
|
|
22
|
+
procedures_learned: number;
|
|
23
|
+
procedures_learning: number;
|
|
24
|
+
procedures_degraded: number;
|
|
25
|
+
procedures_broken: number;
|
|
26
|
+
procedure_convergence_rate: number; // learned / total
|
|
27
|
+
|
|
28
|
+
// Task execution
|
|
29
|
+
tasks_completed_7d: number;
|
|
30
|
+
tasks_failed_7d: number;
|
|
31
|
+
task_success_rate_7d: number;
|
|
32
|
+
task_success_rate_prior_7d: number;
|
|
33
|
+
top_failure_kind: string | null;
|
|
34
|
+
|
|
35
|
+
// Scheduled task health
|
|
36
|
+
scheduled_task_error_rate_24h: number;
|
|
37
|
+
|
|
38
|
+
// Classifier source breakdown (classify-cast monitoring)
|
|
39
|
+
classify_cast_rate_48h: number; // % of dispatches using classify-cast
|
|
40
|
+
user_correction_rate_48h: number; // % of dispatches followed by user_correction
|
|
41
|
+
classify_cast_correction_rate_48h: number; // user_correction rate specifically for classify-cast episodes
|
|
42
|
+
|
|
43
|
+
// Memory growth
|
|
44
|
+
memory_count: number;
|
|
45
|
+
memory_count_prior: number; // from last snapshot
|
|
46
|
+
|
|
47
|
+
// Composite score (0-100)
|
|
48
|
+
cognitive_score: number;
|
|
49
|
+
score_delta: number; // vs last snapshot
|
|
50
|
+
|
|
51
|
+
computed_at: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ─── Queries ─────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
async function queryDispatchMetrics(db: D1Database): Promise<{
|
|
57
|
+
success_rate_7d: number;
|
|
58
|
+
success_rate_prior_7d: number;
|
|
59
|
+
volume_7d: number;
|
|
60
|
+
avg_cost_7d: number;
|
|
61
|
+
avg_cost_prior_7d: number;
|
|
62
|
+
avg_latency_7d: number;
|
|
63
|
+
avg_latency_prior_7d: number;
|
|
64
|
+
reclassification_rate_7d: number;
|
|
65
|
+
}> {
|
|
66
|
+
const [current, prior, reclass] = await Promise.all([
|
|
67
|
+
db.prepare(`
|
|
68
|
+
SELECT COUNT(*) as total,
|
|
69
|
+
SUM(CASE WHEN outcome = 'success' THEN 1 ELSE 0 END) as successes,
|
|
70
|
+
AVG(cost) as avg_cost,
|
|
71
|
+
AVG(latency_ms) as avg_latency
|
|
72
|
+
FROM episodic_memory
|
|
73
|
+
WHERE created_at > datetime('now', '-7 days')
|
|
74
|
+
`).first<{ total: number; successes: number; avg_cost: number; avg_latency: number }>(),
|
|
75
|
+
|
|
76
|
+
db.prepare(`
|
|
77
|
+
SELECT COUNT(*) as total,
|
|
78
|
+
SUM(CASE WHEN outcome = 'success' THEN 1 ELSE 0 END) as successes,
|
|
79
|
+
AVG(cost) as avg_cost,
|
|
80
|
+
AVG(latency_ms) as avg_latency
|
|
81
|
+
FROM episodic_memory
|
|
82
|
+
WHERE created_at > datetime('now', '-14 days')
|
|
83
|
+
AND created_at <= datetime('now', '-7 days')
|
|
84
|
+
`).first<{ total: number; successes: number; avg_cost: number; avg_latency: number }>(),
|
|
85
|
+
|
|
86
|
+
db.prepare(`
|
|
87
|
+
SELECT COUNT(*) as total,
|
|
88
|
+
SUM(CASE WHEN reclassified = 1 THEN 1 ELSE 0 END) as reclassified
|
|
89
|
+
FROM episodic_memory
|
|
90
|
+
WHERE created_at > datetime('now', '-7 days')
|
|
91
|
+
`).first<{ total: number; reclassified: number }>(),
|
|
92
|
+
]);
|
|
93
|
+
|
|
94
|
+
const curTotal = current?.total ?? 0;
|
|
95
|
+
const priorTotal = prior?.total ?? 0;
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
success_rate_7d: curTotal > 0 ? (current?.successes ?? 0) / curTotal : 1,
|
|
99
|
+
success_rate_prior_7d: priorTotal > 0 ? (prior?.successes ?? 0) / priorTotal : 1,
|
|
100
|
+
volume_7d: curTotal,
|
|
101
|
+
avg_cost_7d: current?.avg_cost ?? 0,
|
|
102
|
+
avg_cost_prior_7d: prior?.avg_cost ?? 0,
|
|
103
|
+
avg_latency_7d: current?.avg_latency ?? 0,
|
|
104
|
+
avg_latency_prior_7d: prior?.avg_latency ?? 0,
|
|
105
|
+
reclassification_rate_7d: (reclass?.total ?? 0) > 0
|
|
106
|
+
? (reclass?.reclassified ?? 0) / (reclass?.total ?? 1)
|
|
107
|
+
: 0,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function queryProceduralMetrics(db: D1Database): Promise<{
|
|
112
|
+
learned: number;
|
|
113
|
+
learning: number;
|
|
114
|
+
degraded: number;
|
|
115
|
+
broken: number;
|
|
116
|
+
convergence_rate: number;
|
|
117
|
+
}> {
|
|
118
|
+
const result = await db.prepare(`
|
|
119
|
+
SELECT status, COUNT(*) as count
|
|
120
|
+
FROM procedural_memory
|
|
121
|
+
GROUP BY status
|
|
122
|
+
`).all<{ status: string; count: number }>();
|
|
123
|
+
|
|
124
|
+
const counts: Record<string, number> = {};
|
|
125
|
+
for (const row of result.results) {
|
|
126
|
+
counts[row.status] = row.count;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const total = Object.values(counts).reduce((a, b) => a + b, 0);
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
learned: counts['learned'] ?? 0,
|
|
133
|
+
learning: counts['learning'] ?? 0,
|
|
134
|
+
degraded: counts['degraded'] ?? 0,
|
|
135
|
+
broken: counts['broken'] ?? 0,
|
|
136
|
+
convergence_rate: total > 0 ? (counts['learned'] ?? 0) / total : 0,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function queryTaskMetrics(db: D1Database): Promise<{
|
|
141
|
+
completed_7d: number;
|
|
142
|
+
failed_7d: number;
|
|
143
|
+
success_rate_7d: number;
|
|
144
|
+
success_rate_prior_7d: number;
|
|
145
|
+
top_failure_kind: string | null;
|
|
146
|
+
}> {
|
|
147
|
+
const [current, prior, failures] = await Promise.all([
|
|
148
|
+
db.prepare(`
|
|
149
|
+
SELECT COUNT(*) as total,
|
|
150
|
+
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed,
|
|
151
|
+
SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) as failed
|
|
152
|
+
FROM cc_tasks
|
|
153
|
+
WHERE completed_at > datetime('now', '-7 days')
|
|
154
|
+
`).first<{ total: number; completed: number; failed: number }>(),
|
|
155
|
+
|
|
156
|
+
db.prepare(`
|
|
157
|
+
SELECT COUNT(*) as total,
|
|
158
|
+
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed
|
|
159
|
+
FROM cc_tasks
|
|
160
|
+
WHERE completed_at > datetime('now', '-14 days')
|
|
161
|
+
AND completed_at <= datetime('now', '-7 days')
|
|
162
|
+
`).first<{ total: number; completed: number }>(),
|
|
163
|
+
|
|
164
|
+
db.prepare(`
|
|
165
|
+
SELECT failure_kind, COUNT(*) as cnt
|
|
166
|
+
FROM cc_tasks
|
|
167
|
+
WHERE status = 'failed' AND completed_at > datetime('now', '-7 days')
|
|
168
|
+
AND failure_kind IS NOT NULL
|
|
169
|
+
GROUP BY failure_kind
|
|
170
|
+
ORDER BY cnt DESC
|
|
171
|
+
LIMIT 1
|
|
172
|
+
`).first<{ failure_kind: string; cnt: number }>(),
|
|
173
|
+
]);
|
|
174
|
+
|
|
175
|
+
const curTotal = (current?.completed ?? 0) + (current?.failed ?? 0);
|
|
176
|
+
const priorTotal = prior?.total ?? 0;
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
completed_7d: current?.completed ?? 0,
|
|
180
|
+
failed_7d: current?.failed ?? 0,
|
|
181
|
+
success_rate_7d: curTotal > 0 ? (current?.completed ?? 0) / curTotal : 1,
|
|
182
|
+
success_rate_prior_7d: priorTotal > 0 ? (prior?.completed ?? 0) / priorTotal : 1,
|
|
183
|
+
top_failure_kind: failures?.failure_kind ?? null,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function queryScheduledTaskHealth(db: D1Database): Promise<number> {
|
|
188
|
+
const result = await db.prepare(`
|
|
189
|
+
SELECT COUNT(*) as total,
|
|
190
|
+
SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END) as errors
|
|
191
|
+
FROM task_runs
|
|
192
|
+
WHERE created_at > datetime('now', '-24 hours')
|
|
193
|
+
`).first<{ total: number; errors: number }>();
|
|
194
|
+
|
|
195
|
+
const total = result?.total ?? 0;
|
|
196
|
+
return total > 0 ? (result?.errors ?? 0) / total : 0;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function queryClassifierMetrics(db: D1Database): Promise<{
|
|
200
|
+
classify_cast_rate: number;
|
|
201
|
+
user_correction_rate: number;
|
|
202
|
+
classify_cast_correction_rate: number;
|
|
203
|
+
}> {
|
|
204
|
+
const [totals, corrections] = await Promise.all([
|
|
205
|
+
db.prepare(`
|
|
206
|
+
SELECT COUNT(*) as total,
|
|
207
|
+
SUM(CASE WHEN summary LIKE '%[clf:classify-cast]%' THEN 1 ELSE 0 END) as classify_cast,
|
|
208
|
+
SUM(CASE WHEN intent_class = 'user_correction' THEN 1 ELSE 0 END) as user_corrections
|
|
209
|
+
FROM episodic_memory
|
|
210
|
+
WHERE created_at > datetime('now', '-48 hours')
|
|
211
|
+
AND channel != 'internal'
|
|
212
|
+
`).first<{ total: number; classify_cast: number; user_corrections: number }>(),
|
|
213
|
+
|
|
214
|
+
// Count user_corrections that follow a classify-cast episode in the same thread
|
|
215
|
+
db.prepare(`
|
|
216
|
+
SELECT COUNT(DISTINCT e2.id) as cc_corrections
|
|
217
|
+
FROM episodic_memory e2
|
|
218
|
+
INNER JOIN episodic_memory e1
|
|
219
|
+
ON e1.thread_id = e2.thread_id
|
|
220
|
+
AND e1.created_at < e2.created_at
|
|
221
|
+
WHERE e2.intent_class = 'user_correction'
|
|
222
|
+
AND e1.summary LIKE '%[clf:classify-cast]%'
|
|
223
|
+
AND e2.created_at > datetime('now', '-48 hours')
|
|
224
|
+
`).first<{ cc_corrections: number }>(),
|
|
225
|
+
]);
|
|
226
|
+
|
|
227
|
+
const total = totals?.total ?? 0;
|
|
228
|
+
const ccTotal = totals?.classify_cast ?? 0;
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
classify_cast_rate: total > 0 ? ccTotal / total : 0,
|
|
232
|
+
user_correction_rate: total > 0 ? (totals?.user_corrections ?? 0) / total : 0,
|
|
233
|
+
classify_cast_correction_rate: ccTotal > 0 ? (corrections?.cc_corrections ?? 0) / ccTotal : 0,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function queryMemoryCount(db: D1Database): Promise<number> {
|
|
238
|
+
const result = await db.prepare(
|
|
239
|
+
"SELECT COUNT(*) as cnt FROM memory_entries WHERE valid_until IS NULL"
|
|
240
|
+
).first<{ cnt: number }>();
|
|
241
|
+
return result?.cnt ?? 0;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ─── Composite Score ─────────────────────────────────────────
|
|
245
|
+
// Weighted average of key health indicators, 0-100 scale.
|
|
246
|
+
|
|
247
|
+
function computeScore(snapshot: Omit<CognitiveSnapshot, 'cognitive_score' | 'score_delta' | 'computed_at'>): number {
|
|
248
|
+
const weights = {
|
|
249
|
+
dispatch_success: 25, // most important — is the kernel routing correctly?
|
|
250
|
+
procedure_convergence: 20, // is learning working?
|
|
251
|
+
task_success: 20, // is autonomous work succeeding?
|
|
252
|
+
cost_efficiency: 15, // is cost trending down?
|
|
253
|
+
routing_stability: 10, // low reclassification = stable routing
|
|
254
|
+
scheduled_health: 10, // are cron tasks healthy?
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const scores = {
|
|
258
|
+
dispatch_success: snapshot.dispatch_success_rate_7d * 100,
|
|
259
|
+
procedure_convergence: snapshot.procedure_convergence_rate * 100,
|
|
260
|
+
task_success: snapshot.task_success_rate_7d * 100,
|
|
261
|
+
cost_efficiency: snapshot.avg_cost_prior_7d > 0
|
|
262
|
+
? Math.min(100, (1 - (snapshot.avg_cost_7d - snapshot.avg_cost_prior_7d) / snapshot.avg_cost_prior_7d) * 50 + 50)
|
|
263
|
+
: 50, // neutral if no prior data
|
|
264
|
+
routing_stability: (1 - snapshot.reclassification_rate_7d) * 100,
|
|
265
|
+
scheduled_health: (1 - snapshot.scheduled_task_error_rate_24h) * 100,
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
let weighted = 0;
|
|
269
|
+
let totalWeight = 0;
|
|
270
|
+
for (const [key, weight] of Object.entries(weights)) {
|
|
271
|
+
weighted += (scores[key as keyof typeof scores] ?? 50) * weight;
|
|
272
|
+
totalWeight += weight;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return Math.round(Math.max(0, Math.min(100, weighted / totalWeight)));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ─── Storage ─────────────────────────────────────────────────
|
|
279
|
+
|
|
280
|
+
// Store snapshots in digest_sections for the Co-Founder Brief
|
|
281
|
+
async function storeSnapshot(db: D1Database, snapshot: CognitiveSnapshot): Promise<void> {
|
|
282
|
+
const payload = JSON.stringify({
|
|
283
|
+
type: 'cognitive_metrics',
|
|
284
|
+
...snapshot,
|
|
285
|
+
});
|
|
286
|
+
await db.prepare(
|
|
287
|
+
"INSERT INTO digest_sections (section, payload) VALUES ('cognitive_metrics', ?)"
|
|
288
|
+
).bind(payload).run();
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async function getLastScore(db: D1Database): Promise<{ score: number; memory_count: number } | null> {
|
|
292
|
+
const row = await db.prepare(
|
|
293
|
+
"SELECT payload FROM digest_sections WHERE section = 'cognitive_metrics' ORDER BY created_at DESC LIMIT 1"
|
|
294
|
+
).first<{ payload: string }>();
|
|
295
|
+
|
|
296
|
+
if (!row) return null;
|
|
297
|
+
try {
|
|
298
|
+
const parsed = JSON.parse(row.payload);
|
|
299
|
+
return { score: parsed.cognitive_score ?? 50, memory_count: parsed.memory_count ?? 0 };
|
|
300
|
+
} catch { return null; }
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// ─── Main ────────────────────────────────────────────────────
|
|
304
|
+
|
|
305
|
+
export async function runCognitiveMetrics(env: EdgeEnv): Promise<void> {
|
|
306
|
+
// Daily gate — run once per day at 11 UTC (before the 12 UTC digest)
|
|
307
|
+
const hour = new Date().getUTCHours();
|
|
308
|
+
if (hour !== 11) return;
|
|
309
|
+
|
|
310
|
+
// Cooldown: 22 hours
|
|
311
|
+
const lastRun = await env.db.prepare(
|
|
312
|
+
"SELECT received_at FROM web_events WHERE event_id = 'cognitive_metrics'"
|
|
313
|
+
).first<{ received_at: string }>();
|
|
314
|
+
|
|
315
|
+
if (lastRun) {
|
|
316
|
+
const elapsed = Date.now() - new Date(lastRun.received_at + 'Z').getTime();
|
|
317
|
+
if (elapsed < 22 * 60 * 60 * 1000) return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Gather all metrics in parallel
|
|
321
|
+
const [dispatch, procedural, tasks, scheduledHealth, memoryCount, lastSnapshot, classifier] = await Promise.all([
|
|
322
|
+
queryDispatchMetrics(env.db),
|
|
323
|
+
queryProceduralMetrics(env.db),
|
|
324
|
+
queryTaskMetrics(env.db),
|
|
325
|
+
queryScheduledTaskHealth(env.db),
|
|
326
|
+
queryMemoryCount(env.db),
|
|
327
|
+
getLastScore(env.db),
|
|
328
|
+
queryClassifierMetrics(env.db),
|
|
329
|
+
]);
|
|
330
|
+
|
|
331
|
+
const partial = {
|
|
332
|
+
dispatch_success_rate_7d: dispatch.success_rate_7d,
|
|
333
|
+
dispatch_success_rate_prior_7d: dispatch.success_rate_prior_7d,
|
|
334
|
+
dispatch_volume_7d: dispatch.volume_7d,
|
|
335
|
+
avg_cost_7d: dispatch.avg_cost_7d,
|
|
336
|
+
avg_cost_prior_7d: dispatch.avg_cost_prior_7d,
|
|
337
|
+
avg_latency_ms_7d: dispatch.avg_latency_7d,
|
|
338
|
+
avg_latency_ms_prior_7d: dispatch.avg_latency_prior_7d,
|
|
339
|
+
reclassification_rate_7d: dispatch.reclassification_rate_7d,
|
|
340
|
+
procedures_learned: procedural.learned,
|
|
341
|
+
procedures_learning: procedural.learning,
|
|
342
|
+
procedures_degraded: procedural.degraded,
|
|
343
|
+
procedures_broken: procedural.broken,
|
|
344
|
+
procedure_convergence_rate: procedural.convergence_rate,
|
|
345
|
+
tasks_completed_7d: tasks.completed_7d,
|
|
346
|
+
tasks_failed_7d: tasks.failed_7d,
|
|
347
|
+
task_success_rate_7d: tasks.success_rate_7d,
|
|
348
|
+
task_success_rate_prior_7d: tasks.success_rate_prior_7d,
|
|
349
|
+
top_failure_kind: tasks.top_failure_kind,
|
|
350
|
+
scheduled_task_error_rate_24h: scheduledHealth,
|
|
351
|
+
classify_cast_rate_48h: classifier.classify_cast_rate,
|
|
352
|
+
user_correction_rate_48h: classifier.user_correction_rate,
|
|
353
|
+
classify_cast_correction_rate_48h: classifier.classify_cast_correction_rate,
|
|
354
|
+
memory_count: memoryCount,
|
|
355
|
+
memory_count_prior: lastSnapshot?.memory_count ?? memoryCount,
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
const score = computeScore(partial);
|
|
359
|
+
const scoreDelta = score - (lastSnapshot?.score ?? score);
|
|
360
|
+
|
|
361
|
+
const snapshot: CognitiveSnapshot = {
|
|
362
|
+
...partial,
|
|
363
|
+
cognitive_score: score,
|
|
364
|
+
score_delta: scoreDelta,
|
|
365
|
+
computed_at: new Date().toISOString(),
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
await storeSnapshot(env.db, snapshot);
|
|
369
|
+
|
|
370
|
+
await env.db.prepare(
|
|
371
|
+
"INSERT OR REPLACE INTO web_events (event_id, received_at) VALUES ('cognitive_metrics', datetime('now'))"
|
|
372
|
+
).run();
|
|
373
|
+
|
|
374
|
+
const arrow = scoreDelta > 0 ? '+' : scoreDelta < 0 ? '' : '=';
|
|
375
|
+
console.log(`[cognitive-metrics] Score: ${score}/100 (${arrow}${scoreDelta}) | dispatch: ${Math.round(dispatch.success_rate_7d * 100)}% | procedures: ${procedural.learned}/${procedural.learned + procedural.learning} learned | tasks: ${tasks.completed_7d}/${tasks.completed_7d + tasks.failed_7d}`);
|
|
376
|
+
console.log(`[cognitive-metrics] classifier: classify-cast=${Math.round(classifier.classify_cast_rate * 100)}% | correction_rate=${Math.round(classifier.user_correction_rate * 100)}% | cc_correction_rate=${Math.round(classifier.classify_cast_correction_rate * 100)}%`);
|
|
377
|
+
}
|