@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.
Files changed (148) hide show
  1. package/package.json +96 -0
  2. package/schema.sql +586 -0
  3. package/src/adapters/voice/cloudflare-agent.ts +34 -0
  4. package/src/auth.ts +124 -0
  5. package/src/bluesky.ts +464 -0
  6. package/src/claude-tools/content.ts +188 -0
  7. package/src/claude-tools/email.ts +69 -0
  8. package/src/claude-tools/github.ts +440 -0
  9. package/src/claude-tools/goals.ts +116 -0
  10. package/src/claude-tools/index.ts +353 -0
  11. package/src/claude-tools/web.ts +59 -0
  12. package/src/claude.ts +406 -0
  13. package/src/codebeast.ts +200 -0
  14. package/src/composite.ts +715 -0
  15. package/src/content/column.ts +80 -0
  16. package/src/content/hero-image.ts +47 -0
  17. package/src/content/index.ts +27 -0
  18. package/src/content/journal.ts +91 -0
  19. package/src/content/roundtable.ts +163 -0
  20. package/src/core.ts +309 -0
  21. package/src/dashboard.ts +620 -0
  22. package/src/decision-docs.ts +284 -0
  23. package/src/dispatch.ts +13 -0
  24. package/src/edge-env.ts +58 -0
  25. package/src/email.ts +850 -0
  26. package/src/exports.ts +156 -0
  27. package/src/github-projects.ts +312 -0
  28. package/src/github.ts +670 -0
  29. package/src/groq.ts +247 -0
  30. package/src/health-page.ts +578 -0
  31. package/src/index.ts +89 -0
  32. package/src/kernel/argus-actions.ts +397 -0
  33. package/src/kernel/argus-correlation.ts +639 -0
  34. package/src/kernel/board.ts +91 -0
  35. package/src/kernel/briefing.ts +177 -0
  36. package/src/kernel/classify-memory-topic.ts +166 -0
  37. package/src/kernel/cognition.ts +377 -0
  38. package/src/kernel/court-cards.ts +163 -0
  39. package/src/kernel/dispatch.ts +587 -0
  40. package/src/kernel/domain.ts +50 -0
  41. package/src/kernel/dynamic-tools.ts +322 -0
  42. package/src/kernel/executor-port.ts +45 -0
  43. package/src/kernel/executors/claude.ts +73 -0
  44. package/src/kernel/executors/direct.ts +237 -0
  45. package/src/kernel/executors/groq.ts +18 -0
  46. package/src/kernel/executors/index.ts +87 -0
  47. package/src/kernel/executors/tarotscript.ts +104 -0
  48. package/src/kernel/executors/workers-ai.ts +54 -0
  49. package/src/kernel/insight-cache.ts +76 -0
  50. package/src/kernel/memory/agenda.ts +200 -0
  51. package/src/kernel/memory/blocks.ts +188 -0
  52. package/src/kernel/memory/consolidation.ts +194 -0
  53. package/src/kernel/memory/episodic.ts +241 -0
  54. package/src/kernel/memory/goals.ts +156 -0
  55. package/src/kernel/memory/graph.ts +290 -0
  56. package/src/kernel/memory/index.ts +11 -0
  57. package/src/kernel/memory/insights.ts +316 -0
  58. package/src/kernel/memory/procedural.ts +467 -0
  59. package/src/kernel/memory/pruning.ts +67 -0
  60. package/src/kernel/memory/recall.ts +367 -0
  61. package/src/kernel/memory/semantic.ts +315 -0
  62. package/src/kernel/memory/synthesis.ts +161 -0
  63. package/src/kernel/memory-adapter.ts +369 -0
  64. package/src/kernel/memory-guardrails.ts +76 -0
  65. package/src/kernel/port.ts +23 -0
  66. package/src/kernel/resilience.ts +322 -0
  67. package/src/kernel/router.ts +471 -0
  68. package/src/kernel/scheduled/agent-dispatch.ts +252 -0
  69. package/src/kernel/scheduled/argus-analytics.ts +247 -0
  70. package/src/kernel/scheduled/argus-heartbeat.ts +320 -0
  71. package/src/kernel/scheduled/argus-notify.ts +348 -0
  72. package/src/kernel/scheduled/board-sync.ts +110 -0
  73. package/src/kernel/scheduled/ci-watcher.ts +125 -0
  74. package/src/kernel/scheduled/cognitive-metrics.ts +377 -0
  75. package/src/kernel/scheduled/consolidation.ts +229 -0
  76. package/src/kernel/scheduled/content-drip.ts +47 -0
  77. package/src/kernel/scheduled/content.ts +6 -0
  78. package/src/kernel/scheduled/conversation-facts.ts +204 -0
  79. package/src/kernel/scheduled/cost-report.ts +84 -0
  80. package/src/kernel/scheduled/curiosity.ts +219 -0
  81. package/src/kernel/scheduled/dev-activity.ts +44 -0
  82. package/src/kernel/scheduled/digest.ts +317 -0
  83. package/src/kernel/scheduled/dreaming/agenda-triage.ts +115 -0
  84. package/src/kernel/scheduled/dreaming/facts.ts +239 -0
  85. package/src/kernel/scheduled/dreaming/index.ts +8 -0
  86. package/src/kernel/scheduled/dreaming/llm.ts +33 -0
  87. package/src/kernel/scheduled/dreaming/pattern-synthesis.ts +124 -0
  88. package/src/kernel/scheduled/dreaming/persona.ts +75 -0
  89. package/src/kernel/scheduled/dreaming/symbolic.ts +31 -0
  90. package/src/kernel/scheduled/dreaming/task-proposals.ts +80 -0
  91. package/src/kernel/scheduled/dreaming.ts +66 -0
  92. package/src/kernel/scheduled/entropy.ts +149 -0
  93. package/src/kernel/scheduled/escalation.ts +192 -0
  94. package/src/kernel/scheduled/feed-watcher.ts +206 -0
  95. package/src/kernel/scheduled/goals.ts +214 -0
  96. package/src/kernel/scheduled/governance.ts +41 -0
  97. package/src/kernel/scheduled/heartbeat.ts +220 -0
  98. package/src/kernel/scheduled/inbox-processor.ts +174 -0
  99. package/src/kernel/scheduled/index.ts +245 -0
  100. package/src/kernel/scheduled/issue-proposer.ts +478 -0
  101. package/src/kernel/scheduled/issue-watcher.ts +128 -0
  102. package/src/kernel/scheduled/pr-automerge.ts +213 -0
  103. package/src/kernel/scheduled/product-health.ts +107 -0
  104. package/src/kernel/scheduled/reflection.ts +373 -0
  105. package/src/kernel/scheduled/self-improvement.ts +114 -0
  106. package/src/kernel/scheduled/social-engage.ts +175 -0
  107. package/src/kernel/scheduled/task-audit.ts +60 -0
  108. package/src/kernel/symbolic.ts +156 -0
  109. package/src/kernel/types.ts +145 -0
  110. package/src/landing.ts +1190 -0
  111. package/src/lib/audit-chain/chain.ts +28 -0
  112. package/src/lib/audit-chain/types.ts +12 -0
  113. package/src/lib/observability/errors.ts +55 -0
  114. package/src/markdown.ts +164 -0
  115. package/src/mcp/handlers.ts +647 -0
  116. package/src/mcp/server.ts +184 -0
  117. package/src/mcp/tools.ts +316 -0
  118. package/src/mcp-client.ts +275 -0
  119. package/src/mcp-server.ts +2 -0
  120. package/src/operator/config.example.ts +60 -0
  121. package/src/operator/config.ts +60 -0
  122. package/src/operator/index.ts +46 -0
  123. package/src/operator/persona.example.ts +34 -0
  124. package/src/operator/persona.ts +34 -0
  125. package/src/operator/prompt-builder.ts +190 -0
  126. package/src/operator/types.ts +43 -0
  127. package/src/pulse.ts +1179 -0
  128. package/src/routes/bluesky.ts +116 -0
  129. package/src/routes/cc-tasks.ts +328 -0
  130. package/src/routes/codebeast.ts +1 -0
  131. package/src/routes/content.ts +194 -0
  132. package/src/routes/conversations.ts +25 -0
  133. package/src/routes/dynamic-tools.ts +111 -0
  134. package/src/routes/feedback.ts +192 -0
  135. package/src/routes/health.ts +147 -0
  136. package/src/routes/messages.ts +228 -0
  137. package/src/routes/observability.ts +82 -0
  138. package/src/routes/operator-logs.ts +42 -0
  139. package/src/routes/pages.ts +96 -0
  140. package/src/routes/sessions.ts +54 -0
  141. package/src/sanitize.ts +73 -0
  142. package/src/schema-enums.ts +155 -0
  143. package/src/search.ts +112 -0
  144. package/src/task-intelligence.ts +497 -0
  145. package/src/types.ts +194 -0
  146. package/src/ui.ts +5 -0
  147. package/src/version.ts +3 -0
  148. package/src/workers-ai-chat.ts +333 -0
@@ -0,0 +1,229 @@
1
+ import { type EdgeEnv } from '../dispatch.js';
2
+ import { consolidateEpisodicToSemantic, maintainProcedures, getAllProcedures, PROCEDURE_MIN_SUCCESSES, PROCEDURE_MIN_SUCCESS_RATE } from '../memory/index.js';
3
+ import { garbageCollectTools, promoteHighUsageTools } from '../dynamic-tools.js';
4
+ import { publishInsight, type InsightType } from '../memory/insights.js';
5
+ import { pruneMemory } from '../memory-adapter.js';
6
+ import { runCrossDomainSynthesis } from '../memory/synthesis.js';
7
+ import { maintainNarratives, detectStaleNarratives, precomputeCognitiveState, pruneNarratives, getCognitiveState, type ProductPortfolioEntry } from '../cognition.js';
8
+ import { updateBlock } from '../memory/blocks.js';
9
+ // topic-discovery is an extension point — consumers can provide their own
10
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
11
+ async function discoverEmergentTopics(_db: D1Database, _binding: any): Promise<void> { /* no-op in core */ }
12
+ import { McpClient } from '../../mcp-client.js';
13
+ import { operatorConfig } from '../../operator/index.js';
14
+
15
+ export async function runMemoryConsolidation(env: EdgeEnv): Promise<void> {
16
+ await consolidateEpisodicToSemantic(env.db, env.groqApiKey, env.groqModel, env.groqBaseUrl, env.memoryBinding);
17
+ if (env.memoryBinding) {
18
+ await pruneMemory(env.memoryBinding, env.db);
19
+ }
20
+ await maintainProcedures(env.db);
21
+
22
+ // Dynamic tools lifecycle: expire TTL'd tools, retire unused, promote high-use
23
+ try {
24
+ const gc = await garbageCollectTools(env.db);
25
+ const promoted = await promoteHighUsageTools(env.db);
26
+ if (gc.expired > 0 || gc.unused > 0 || promoted > 0) {
27
+ console.log(`[consolidation] Dynamic tools: ${gc.expired} expired, ${gc.unused} unused retired, ${promoted} promoted`);
28
+ }
29
+ } catch {
30
+ // Non-fatal — table may not exist yet
31
+ }
32
+
33
+ // Emergent topic discovery: find orphaned facts that cluster into new topics
34
+ if (env.memoryBinding) {
35
+ await discoverEmergentTopics(env.db, env.memoryBinding);
36
+ }
37
+
38
+ // Cross-domain synthesis: find connections across memory topics
39
+ await runCrossDomainSynthesis(env);
40
+
41
+ // Cognitive layer: narratives + state precomputation
42
+ await maintainNarratives(env.db, env.groqApiKey, env.groqModel, env.groqBaseUrl);
43
+ await detectStaleNarratives(env.db);
44
+ await pruneNarratives(env.db);
45
+
46
+ // Fetch product portfolio from BizOps (1 MCP call, hourly cadence)
47
+ const portfolio = await fetchProductPortfolio(env);
48
+ await precomputeCognitiveState(env.db, portfolio.length > 0 ? portfolio : undefined, env.memoryBinding, env.mindspringFetcher, env.mindspringToken);
49
+
50
+ // Update active_context block from freshly computed CognitiveState
51
+ await refreshActiveContextBlock(env.db);
52
+
53
+ // ─── CRIX Phase 2c: Publish insights from procedural + semantic memory ───
54
+ await publishInsightsFromMemory(env);
55
+ }
56
+
57
+ export async function fetchProductPortfolio(env: EdgeEnv): Promise<ProductPortfolioEntry[]> {
58
+ if (!env.bizopsToken) return [];
59
+ try {
60
+ const client = new McpClient({
61
+ url: operatorConfig.integrations.bizops.fallbackUrl,
62
+ token: env.bizopsToken,
63
+ prefix: 'bizops',
64
+ fetcher: env.bizopsFetcher,
65
+ rpcPath: '/rpc',
66
+ });
67
+ const raw = await client.callTool('list_projects', {});
68
+ const projects = typeof raw === 'string' ? JSON.parse(raw) : raw;
69
+ if (!Array.isArray(projects)) return [];
70
+ return projects.map((p: Record<string, unknown>) => ({
71
+ name: String(p.name ?? ''),
72
+ description: String(p.description ?? ''),
73
+ model: String(p.repo_kind ?? 'unknown'),
74
+ status: String(p.status ?? p.last_seen_at ? 'active' : 'unknown'),
75
+ revenue: p.revenue ? String(p.revenue) : undefined,
76
+ })).filter((p: ProductPortfolioEntry) => p.name);
77
+ } catch (err) {
78
+ console.warn('[scheduled] Product portfolio fetch failed:', err instanceof Error ? err.message : String(err));
79
+ return [];
80
+ }
81
+ }
82
+
83
+ // ─── CRIX Phase 2c: Insight Publishing ────────────────────────
84
+
85
+ const INSIGHT_RATE_LIMIT = 5; // max per consolidation cycle
86
+ const WATCH_REPOS = ['my-agent'] // Add your repos here;
87
+
88
+ async function publishInsightsFromMemory(env: EdgeEnv): Promise<void> {
89
+ let published = 0;
90
+
91
+ // Source 1: Learned procedures — patterns with ≥70% success rate and 3+ successes
92
+ try {
93
+ const procedures = await getAllProcedures(env.db);
94
+ for (const proc of procedures) {
95
+ if (published >= INSIGHT_RATE_LIMIT) break;
96
+ if (proc.status !== 'learned') continue;
97
+ if (proc.success_count < PROCEDURE_MIN_SUCCESSES) continue;
98
+
99
+ const successRate = proc.success_count / (proc.success_count + proc.fail_count);
100
+ if (successRate < PROCEDURE_MIN_SUCCESS_RATE) continue;
101
+
102
+ // Already published check: handled by publishInsight() fact hash gate
103
+ const fact = `Learned procedure: "${proc.task_pattern}" routes to ${proc.executor} executor with ${(successRate * 100).toFixed(0)}% success rate (${proc.success_count} successes). Config: ${proc.executor_config?.slice(0, 200) ?? 'default'}`;
104
+
105
+ const result = await publishInsight(env.db, {
106
+ fact,
107
+ insight_type: 'pattern',
108
+ origin_repo: 'aegis',
109
+ keywords: [proc.task_pattern, proc.executor, 'routing', 'procedure'],
110
+ confidence: Math.min(0.95, 0.75 + successRate * 0.2),
111
+ }, env.memoryBinding);
112
+
113
+ if (result.published) {
114
+ published++;
115
+ console.log(`[crix] Published procedure insight: ${proc.task_pattern}`);
116
+ }
117
+ }
118
+ } catch (err) {
119
+ console.warn('[crix] Procedure scan failed:', err instanceof Error ? err.message : String(err));
120
+ }
121
+
122
+ // Source 2: High-confidence memory entries from the last 24h via Memory Worker
123
+ // Look for entries tagged with bug/perf/arch topics that could be cross-repo insights
124
+ if (env.memoryBinding) {
125
+ try {
126
+ const INSIGHT_TOPICS = new Map<string, InsightType>([
127
+ ['bug_signature', 'pattern'],
128
+ ['perf_pattern', 'heuristic'],
129
+ ['arch_improvement', 'principle'],
130
+ ]);
131
+
132
+ const fragments = await env.memoryBinding.recall('aegis', { limit: 20 });
133
+ // Filter to high-confidence entries suitable for cross-repo insight publishing
134
+ const recentHighConf = fragments.filter(f => f.confidence >= 0.85).slice(0, 10);
135
+
136
+ for (const entry of recentHighConf) {
137
+ if (published >= INSIGHT_RATE_LIMIT) break;
138
+
139
+ // Infer insight type from topic
140
+ let insightType: InsightType = 'pattern';
141
+ for (const [topicFragment, type] of INSIGHT_TOPICS) {
142
+ if (entry.topic.includes(topicFragment)) {
143
+ insightType = type;
144
+ break;
145
+ }
146
+ }
147
+
148
+ // Extract keywords from the fact text
149
+ const keywords = entry.content.toLowerCase()
150
+ .replace(/[^a-z0-9\s]/g, ' ')
151
+ .split(/\s+/)
152
+ .filter(w => w.length > 4)
153
+ .slice(0, 8);
154
+
155
+ if (keywords.length < 2) continue; // Not enough signal
156
+
157
+ const result = await publishInsight(env.db, {
158
+ fact: entry.content,
159
+ insight_type: insightType,
160
+ origin_repo: 'aegis',
161
+ keywords,
162
+ confidence: entry.confidence,
163
+ }, env.memoryBinding);
164
+
165
+ if (result.published) {
166
+ published++;
167
+ }
168
+ }
169
+ } catch (err) {
170
+ console.warn('[crix] Semantic scan failed:', err instanceof Error ? err.message : String(err));
171
+ }
172
+ }
173
+
174
+ if (published > 0) {
175
+ console.log(`[crix] Published ${published} insights during consolidation cycle`);
176
+ }
177
+ }
178
+
179
+ // ─── Active Context Block Refresh ────────────────────────────
180
+
181
+ async function refreshActiveContextBlock(db: D1Database): Promise<void> {
182
+ try {
183
+ const state = await getCognitiveState(db);
184
+ if (!state) return;
185
+
186
+ // Build active_context content from CognitiveState (skip self-model — that's the identity block)
187
+ const parts: string[] = [];
188
+
189
+ if (state.narratives.length > 0) {
190
+ parts.push('## Active Narratives');
191
+ for (const n of state.narratives) {
192
+ const tag = n.status === 'stalled' ? ' [STALLED]' : '';
193
+ parts.push(`### ${n.title}${tag}`);
194
+ parts.push(n.summary);
195
+ if (n.tension) parts.push(`**Tension**: ${n.tension}`);
196
+ if (n.last_beat) parts.push(`**Latest**: ${n.last_beat}`);
197
+ }
198
+ }
199
+
200
+ parts.push('\n## Operational Pulse');
201
+ parts.push(`- Memory: ${state.memory_count} active entries`);
202
+ parts.push(`- Last 24h: ${state.episode_count_24h} episodes`);
203
+ parts.push(`- Agenda: ${state.open_threads} open threads, ${state.proposed_actions} pending actions`);
204
+ if (state.last_heartbeat_severity) {
205
+ parts.push(`- Last heartbeat: ${state.last_heartbeat_severity}`);
206
+ }
207
+
208
+ if (state.activated_nodes.length > 0) {
209
+ parts.push('\n## Active Concepts');
210
+ for (const node of state.activated_nodes) {
211
+ parts.push(`- ${node.label} (${node.type}, activation: ${node.activation.toFixed(2)})`);
212
+ }
213
+ }
214
+
215
+ if (state.product_portfolio?.length > 0) {
216
+ parts.push('\n## Stackbilt Product Portfolio');
217
+ for (const p of state.product_portfolio) {
218
+ const rev = p.revenue ? ` | Revenue: ${p.revenue}` : '';
219
+ parts.push(`- **${p.name}** [${p.status}] — ${p.description}${rev}`);
220
+ }
221
+ }
222
+
223
+ const content = parts.join('\n');
224
+ await updateBlock(db, 'active_context', content, 'consolidation');
225
+ console.log('[blocks] Refreshed active_context block');
226
+ } catch (err) {
227
+ console.warn('[blocks] Failed to refresh active_context:', err instanceof Error ? err.message : String(err));
228
+ }
229
+ }
@@ -0,0 +1,47 @@
1
+ // Content Drip — publishes scheduled posts to Bluesky
2
+ // Runs in the hourly cron. Checks for posts where scheduled_at <= now and status = 'scheduled'.
3
+
4
+ import { type EdgeEnv } from '../dispatch.js';
5
+ import { postToBluesky } from '../../bluesky.js';
6
+
7
+ export async function runContentDrip(env: EdgeEnv): Promise<void> {
8
+ if (!env.blueskyAppPassword) {
9
+ console.log('[content-drip] No BLUESKY_APP_PASSWORD configured, skipping');
10
+ return;
11
+ }
12
+
13
+ const due = await env.db.prepare(`
14
+ SELECT id, content, media_url, link_url, platform
15
+ FROM content_queue
16
+ WHERE status = 'scheduled' AND scheduled_at <= datetime('now') AND platform = 'bluesky'
17
+ ORDER BY scheduled_at ASC
18
+ LIMIT 5
19
+ `).all<{ id: string; content: string; media_url: string | null; link_url: string | null; platform: string }>();
20
+
21
+ if (due.results.length === 0) return;
22
+
23
+ const handle = env.blueskyHandle ?? 'your-handle.bsky.social';
24
+
25
+ for (const post of due.results) {
26
+ try {
27
+ const result = await postToBluesky(handle, env.blueskyAppPassword, {
28
+ text: post.content,
29
+ imageUrl: post.media_url ?? undefined,
30
+ });
31
+
32
+ await env.db.prepare(`
33
+ UPDATE content_queue SET status = 'published', published_at = datetime('now'), post_url = ?
34
+ WHERE id = ?
35
+ `).bind(result.url, post.id).run();
36
+
37
+ console.log(`[content-drip] Published: ${result.url}`);
38
+ } catch (err) {
39
+ const msg = err instanceof Error ? err.message : String(err);
40
+ await env.db.prepare(`
41
+ UPDATE content_queue SET status = 'failed', error = ?
42
+ WHERE id = ?
43
+ `).bind(msg, post.id).run();
44
+ console.error(`[content-drip] Failed to post ${post.id}: ${msg}`);
45
+ }
46
+ }
47
+ }
@@ -0,0 +1,6 @@
1
+ // Stub — full implementation not yet extracted to OSS
2
+ import type { EdgeEnv } from '../dispatch.js';
3
+
4
+ export async function runRoundtableContentGeneration(_env: EdgeEnv): Promise<void> {}
5
+ export async function runDispatchContentGeneration(_env: EdgeEnv): Promise<void> {}
6
+ export async function runColumnContentGeneration(_env: EdgeEnv): Promise<void> {}
@@ -0,0 +1,204 @@
1
+ // Per-conversation fact extraction (#324)
2
+ // Complements the dreaming cycle (daily, batch) with near-real-time
3
+ // fact capture from operator chat sessions. Runs every 2 hours,
4
+ // processes conversations updated since last run. Uses Workers AI (free).
5
+
6
+ import { type EdgeEnv } from '../dispatch.js';
7
+ import { recordMemory as recordMemoryAdapter } from '../memory-adapter.js';
8
+ import { askGroq } from '../../groq.js';
9
+
10
+ const WATERMARK_KEY = 'conversation_facts_watermark';
11
+ const MAX_CONVERSATIONS = 5;
12
+ const MAX_MESSAGES_PER_CONV = 20;
13
+ const MAX_FACTS_PER_CONV = 5;
14
+ const RUN_INTERVAL_MS = 2 * 60 * 60 * 1000; // 2 hours
15
+
16
+ const BLOCKED_TOPIC_PREFIXES = ['synthesis_', 'cross_repo_insight'];
17
+ const ALLOWED_TOPICS = new Set([
18
+ 'aegis',
19
+ 'auth',
20
+ 'bizops',
21
+ 'charter',
22
+ 'codebeast',
23
+ 'compliance',
24
+ 'content',
25
+ 'feed_intel',
26
+ 'finance',
27
+ 'img_forge',
28
+ 'infrastructure',
29
+ 'mcp_strategy',
30
+ 'meta_insight',
31
+ 'operator_persona',
32
+ 'operator_preferences',
33
+ 'organization',
34
+ 'project',
35
+ 'self_improvement',
36
+ 'tarotscript',
37
+ ]);
38
+
39
+ const TOPIC_ALIASES: Record<string, string> = {
40
+ operator_persona: 'operator_persona',
41
+ operator_preferences: 'operator_preferences',
42
+ stackbilt: 'project',
43
+ stackbilt_auth: 'auth',
44
+ roundtable: 'content',
45
+ memory_worker: 'aegis',
46
+ colonyos: 'project',
47
+ edgestack: 'project',
48
+ wip_techhuman: 'project',
49
+ };
50
+
51
+ const EXTRACTION_SYSTEM = `You extract durable facts from an AI assistant conversation. Focus on:
52
+ - Business decisions, commitments, deadlines
53
+ - Technical architecture changes or constraints
54
+ - User preferences and corrections
55
+ - New information not derivable from code
56
+
57
+ Return ONLY valid JSON (no markdown fences):
58
+ {
59
+ "facts": [
60
+ { "topic": "category", "fact": "specific durable fact", "confidence": 0.8 }
61
+ ]
62
+ }
63
+
64
+ Rules:
65
+ - Max 5 facts. Fewer is better if conversation is routine.
66
+ - Return {"facts":[]} if nothing worth remembering.
67
+ - topic must be one of: aegis, auth, bizops, charter, codebeast, compliance, content, feed_intel, finance, img_forge, infrastructure, mcp_strategy, meta_insight, operator_persona, operator_preferences, organization, project, self_improvement, tarotscript
68
+ - Each fact must be a complete sentence with concrete details (names, numbers, dates).
69
+ - Do NOT extract: greetings, tool outputs, code snippets, or ephemeral task state.
70
+ - confidence: 0.9 for explicit statements, 0.7 for inferences.`;
71
+
72
+ interface ExtractedFact {
73
+ topic: string;
74
+ fact: string;
75
+ confidence: number;
76
+ }
77
+
78
+ function normalizeTopic(topic: string): string {
79
+ const normalized = topic.toLowerCase().trim().replace(/\s+/g, '_');
80
+ return TOPIC_ALIASES[normalized] ?? normalized;
81
+ }
82
+
83
+ async function askAi(
84
+ env: EdgeEnv,
85
+ system: string,
86
+ user: string,
87
+ ): Promise<string> {
88
+ if (env.ai) {
89
+ const result = await env.ai.run(
90
+ '@cf/meta/llama-3.3-70b-instruct-fp8-fast' as Parameters<Ai['run']>[0],
91
+ { messages: [{ role: 'system', content: system }, { role: 'user', content: user }] },
92
+ );
93
+ if (typeof result === 'string') return result;
94
+ const obj = result as { response?: string; choices?: Array<{ message?: { content?: string } }> };
95
+ return obj.choices?.[0]?.message?.content ?? obj.response ?? '';
96
+ }
97
+ return askGroq(env.groqApiKey, env.groqResponseModel, system, user, env.groqBaseUrl);
98
+ }
99
+
100
+ export async function runConversationFactExtraction(env: EdgeEnv): Promise<void> {
101
+ // Time gate: run every 2 hours (uses web_events for watermark, same as other scheduled tasks)
102
+ const lastRun = await env.db.prepare(
103
+ 'SELECT received_at FROM web_events WHERE event_id = ?',
104
+ ).bind(WATERMARK_KEY).first<{ received_at: string }>().catch(() => null);
105
+
106
+ if (lastRun) {
107
+ const elapsed = Date.now() - new Date(lastRun.received_at + 'Z').getTime();
108
+ if (elapsed < RUN_INTERVAL_MS) return;
109
+ }
110
+
111
+ // Find conversations with recent activity (since last run or last 2h)
112
+ const since = lastRun?.received_at ?? new Date(Date.now() - RUN_INTERVAL_MS).toISOString().replace('Z', '');
113
+
114
+ const conversations = await env.db.prepare(`
115
+ SELECT c.id, c.title, COUNT(m.id) as msg_count
116
+ FROM conversations c
117
+ JOIN messages m ON m.conversation_id = c.id
118
+ WHERE m.created_at > ? AND m.role = 'user'
119
+ GROUP BY c.id
120
+ HAVING msg_count >= 2
121
+ ORDER BY MAX(m.created_at) DESC
122
+ LIMIT ?
123
+ `).bind(since, MAX_CONVERSATIONS).all<{ id: string; title: string; msg_count: number }>();
124
+
125
+ if (conversations.results.length === 0) {
126
+ await advanceWatermark(env.db);
127
+ return;
128
+ }
129
+
130
+ let totalFacts = 0;
131
+
132
+ for (const conv of conversations.results) {
133
+ const messages = await env.db.prepare(`
134
+ SELECT role, content FROM messages
135
+ WHERE conversation_id = ?
136
+ ORDER BY created_at DESC
137
+ LIMIT ?
138
+ `).bind(conv.id, MAX_MESSAGES_PER_CONV).all<{ role: string; content: string }>();
139
+
140
+ // Reverse to chronological order (queried DESC for recency limit)
141
+ const thread = messages.results.reverse();
142
+ if (thread.length < 2) continue;
143
+
144
+ const transcript = thread
145
+ .map((message) => `${message.role === 'user' ? 'operator' : 'AEGIS'}: ${message.content.slice(0, 1000)}`)
146
+ .join('\n\n');
147
+
148
+ let rawResponse: string;
149
+ try {
150
+ rawResponse = await askAi(env, EXTRACTION_SYSTEM, transcript.slice(0, 12000));
151
+ } catch (err) {
152
+ console.warn(`[conv-facts] LLM failed for ${conv.id}:`, err instanceof Error ? err.message : String(err));
153
+ continue;
154
+ }
155
+
156
+ if (!rawResponse) continue;
157
+
158
+ const cleaned = rawResponse.replace(/```json\s*/g, '').replace(/```\s*/g, '').trim();
159
+ let parsed: { facts?: ExtractedFact[] };
160
+ try {
161
+ parsed = JSON.parse(cleaned) as { facts?: ExtractedFact[] };
162
+ } catch {
163
+ console.warn(`[conv-facts] Failed to parse response for ${conv.id}: ${cleaned.slice(0, 100)}`);
164
+ continue;
165
+ }
166
+
167
+ if (!parsed.facts || !Array.isArray(parsed.facts)) continue;
168
+
169
+ for (const fact of parsed.facts.slice(0, MAX_FACTS_PER_CONV)) {
170
+ if (!fact.topic || !fact.fact || fact.fact.length < 20) continue;
171
+
172
+ const topicLower = normalizeTopic(fact.topic);
173
+ if (BLOCKED_TOPIC_PREFIXES.some((prefix) => topicLower.startsWith(prefix))) continue;
174
+ if (!ALLOWED_TOPICS.has(topicLower)) {
175
+ console.log(`[conv-facts] Blocked unknown topic '${fact.topic}'`);
176
+ continue;
177
+ }
178
+
179
+ try {
180
+ if (!env.memoryBinding) continue;
181
+ await recordMemoryAdapter(
182
+ env.memoryBinding,
183
+ topicLower,
184
+ fact.fact,
185
+ fact.confidence ?? 0.8,
186
+ 'conversation_extraction',
187
+ );
188
+ totalFacts++;
189
+ console.log(`[conv-facts] Extracted: [${topicLower}] ${fact.fact.slice(0, 80)}`);
190
+ } catch (err) {
191
+ console.warn('[conv-facts] Failed to record:', err instanceof Error ? err.message : String(err));
192
+ }
193
+ }
194
+ }
195
+
196
+ await advanceWatermark(env.db);
197
+ console.log(`[conv-facts] Processed ${conversations.results.length} conversations, extracted ${totalFacts} facts`);
198
+ }
199
+
200
+ async function advanceWatermark(db: D1Database): Promise<void> {
201
+ await db.prepare(
202
+ "INSERT OR REPLACE INTO web_events (event_id, received_at) VALUES (?, datetime('now'))",
203
+ ).bind(WATERMARK_KEY).run().catch(() => {});
204
+ }
@@ -0,0 +1,84 @@
1
+ import { type EdgeEnv } from '../dispatch.js';
2
+
3
+ // --- Cost Report ---
4
+ // Weekly cost aggregation by executor tier. Runs at 07 UTC on Mondays.
5
+ // Stores snapshot in digest_sections for inclusion in the daily digest.
6
+ // No LLM calls -- pure D1 queries.
7
+
8
+ interface ExecutorCost {
9
+ executor: string;
10
+ count: number;
11
+ total_cost: number;
12
+ avg_cost: number;
13
+ avg_latency_ms: number;
14
+ }
15
+
16
+ export async function runCostReport(env: EdgeEnv): Promise<void> {
17
+ // Gate: Monday at 07 UTC
18
+ const now = new Date();
19
+ if (now.getUTCDay() !== 1 || now.getUTCHours() !== 7) return;
20
+
21
+ // Cooldown: 6 days
22
+ const lastRun = await env.db.prepare(
23
+ "SELECT received_at FROM web_events WHERE event_id = 'cost_report'"
24
+ ).first<{ received_at: string }>();
25
+
26
+ if (lastRun) {
27
+ const elapsed = Date.now() - new Date(lastRun.received_at + 'Z').getTime();
28
+ if (elapsed < 6 * 24 * 60 * 60 * 1000) return;
29
+ }
30
+
31
+ // Aggregate costs by executor for the past 7 days
32
+ const byExecutor = await env.db.prepare(`
33
+ SELECT
34
+ COALESCE(executor, 'unknown') as executor,
35
+ COUNT(*) as count,
36
+ ROUND(SUM(cost), 4) as total_cost,
37
+ ROUND(AVG(cost), 4) as avg_cost,
38
+ ROUND(AVG(latency_ms), 0) as avg_latency_ms
39
+ FROM episodic_memory
40
+ WHERE created_at > datetime('now', '-7 days')
41
+ GROUP BY executor
42
+ ORDER BY total_cost DESC
43
+ `).all<ExecutorCost>();
44
+
45
+ if (!byExecutor.results?.length) return;
46
+
47
+ // Total across all executors
48
+ const totalCost = byExecutor.results.reduce((sum, r) => sum + r.total_cost, 0);
49
+ const totalDispatches = byExecutor.results.reduce((sum, r) => sum + r.count, 0);
50
+
51
+ // Top intent classes by cost
52
+ const topIntents = await env.db.prepare(`
53
+ SELECT
54
+ intent_class,
55
+ COUNT(*) as count,
56
+ ROUND(SUM(cost), 4) as total_cost
57
+ FROM episodic_memory
58
+ WHERE created_at > datetime('now', '-7 days')
59
+ GROUP BY intent_class
60
+ ORDER BY total_cost DESC
61
+ LIMIT 5
62
+ `).all<{ intent_class: string; count: number; total_cost: number }>();
63
+
64
+ const snapshot = {
65
+ period: '7d',
66
+ generated_at: now.toISOString(),
67
+ total_cost: Math.round(totalCost * 10000) / 10000,
68
+ total_dispatches: totalDispatches,
69
+ by_executor: byExecutor.results,
70
+ top_intents: topIntents.results || [],
71
+ };
72
+
73
+ // Store for digest
74
+ await env.db.prepare(
75
+ "INSERT INTO digest_sections (section, payload) VALUES ('cost_report', ?)"
76
+ ).bind(JSON.stringify(snapshot)).run();
77
+
78
+ // Update watermark
79
+ await env.db.prepare(
80
+ "INSERT OR REPLACE INTO web_events (event_id, received_at) VALUES ('cost_report', datetime('now'))"
81
+ ).run();
82
+
83
+ console.log(`[cost-report] Weekly: $${totalCost.toFixed(4)} across ${totalDispatches} dispatches, ${byExecutor.results.length} executors`);
84
+ }