@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,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
|
+
}
|