@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,161 @@
1
+ // ─── Cross-Domain Synthesis Engine ───────────────────────────
2
+ // Generates new knowledge by finding connections across memory topics.
3
+ // Uses knowledge graph (kg_nodes/kg_edges) spreading activation to
4
+ // discover cross-topic entity links, then prompts Groq 70B to
5
+ // synthesize emergent patterns.
6
+ //
7
+ // Cost: ~$0.001/cycle (one Groq 70B call)
8
+ // Rate limit: max 3 synthetic insights per cycle
9
+
10
+ import type { EdgeEnv } from '../dispatch.js';
11
+ import type { MemoryServiceBinding, MemoryFragmentResult } from '../../types.js';
12
+ import { activateGraph } from './graph.js';
13
+ import { recallForQuery } from './recall.js';
14
+ import { askGroqJson } from '../../groq.js';
15
+ import { recordMemory } from '../memory-adapter.js';
16
+
17
+ const TENANT = 'aegis';
18
+ const MIN_TOPICS = 3;
19
+ const MAX_INSIGHTS_PER_CYCLE = 3;
20
+ const CONFIDENCE_GATE = 0.75;
21
+ const TOP_ENTRIES_PER_TOPIC = 5;
22
+
23
+ const SYNTHESIS_SYSTEM_PROMPT = `You are a cross-domain knowledge synthesizer. You receive facts from different knowledge domains/topics and connected entities from a knowledge graph.
24
+
25
+ Your job: identify non-obvious connections, implications, and emergent patterns that span multiple source topics. Each insight must reference concepts from at least 2 different source topics.
26
+
27
+ Output a JSON object with a single key "insights" containing an array of objects:
28
+ { "insights": [{ "topic": "synthesis_<descriptive_slug>", "fact": "<the synthesized insight>", "confidence": <0.0-1.0>, "source_topics": ["topic_a", "topic_b"] }] }
29
+
30
+ Rules:
31
+ - Each fact must be a concrete, actionable insight — not a vague observation
32
+ - confidence reflects how well-supported the synthesis is by the source facts
33
+ - Only include insights where you see a genuine connection, not forced associations
34
+ - Maximum 3 insights
35
+ - topic should be "synthesis_" followed by a short descriptive slug (lowercase, underscores)`;
36
+
37
+ interface SynthesisInsight {
38
+ topic: string;
39
+ fact: string;
40
+ confidence: number;
41
+ source_topics: string[];
42
+ }
43
+
44
+ interface SynthesisResult {
45
+ insights: SynthesisInsight[];
46
+ }
47
+
48
+ export async function runCrossDomainSynthesis(env: EdgeEnv): Promise<void> {
49
+ if (!env.memoryBinding) {
50
+ console.log('[synthesis] Skipped: Memory Worker binding unavailable');
51
+ return;
52
+ }
53
+
54
+ try {
55
+ // Step 1: Get active topics from memory stats
56
+ const stats = await env.memoryBinding.stats(TENANT);
57
+ const activeTopics = stats.topics
58
+ .filter(t => t.count >= 2)
59
+ .sort((a, b) => b.count - a.count);
60
+
61
+ if (activeTopics.length < MIN_TOPICS) {
62
+ console.log(`[synthesis] Skipped: only ${activeTopics.length} active topics (need ${MIN_TOPICS})`);
63
+ return;
64
+ }
65
+
66
+ // Step 2: Pick 2-3 topics with the most entries (most active)
67
+ const selectedTopics = activeTopics.slice(0, 3).map(t => t.topic);
68
+
69
+ // Step 3: Fetch top entries from each topic by confidence
70
+ const topicEntries = new Map<string, MemoryFragmentResult[]>();
71
+ for (const topic of selectedTopics) {
72
+ const entries = await env.memoryBinding.recall(TENANT, {
73
+ topic,
74
+ min_confidence: 0.5,
75
+ limit: TOP_ENTRIES_PER_TOPIC,
76
+ });
77
+ if (entries.length > 0) {
78
+ topicEntries.set(topic, entries);
79
+ }
80
+ }
81
+
82
+ if (topicEntries.size < 2) {
83
+ console.log(`[synthesis] Skipped: only ${topicEntries.size} topics returned entries`);
84
+ return;
85
+ }
86
+
87
+ // Step 4: Recall pipeline — find cross-topic entity connections via unified recall
88
+ const allTopicLabels = [...topicEntries.keys()].join(' ');
89
+ const recallResult = await recallForQuery(allTopicLabels, { db: env.db, memoryBinding: env.memoryBinding, mindspringFetcher: env.mindspringFetcher, mindspringToken: env.mindspringToken }, { includeGraph: true });
90
+ const graphNodes = await activateGraph(env.db, allTopicLabels, 2);
91
+
92
+ // Step 5: Build prompt with facts and graph context
93
+ const factsBlock = buildFactsBlock(topicEntries);
94
+ const graphBlock = graphNodes.length > 0
95
+ ? `\n\nConnected entities from knowledge graph:\n${graphNodes.map(n => `- ${n.label} (${n.type}, activation: ${n.activation.toFixed(2)})`).join('\n')}`
96
+ : '';
97
+ const recallBlock = recallResult.facts.length > 0
98
+ ? `\n\nRecall pipeline results (${recallResult.facts.length} facts, expansions: ${recallResult.graphExpansions.join(', ')}):\n${recallResult.facts.slice(0, 5).map(f => `- ${f.text} (score: ${f.score.toFixed(3)}, source: ${f.source})`).join('\n')}`
99
+ : '';
100
+
101
+ const userPrompt = `Here are facts from ${topicEntries.size} different knowledge domains:\n\n${factsBlock}${graphBlock}${recallBlock}\n\nFind cross-domain connections and synthesize new insights.`;
102
+
103
+ // Step 6: Ask Groq 70B for synthesis
104
+ const { parsed } = await askGroqJson<SynthesisResult>(
105
+ env.groqApiKey,
106
+ env.groqModel,
107
+ SYNTHESIS_SYSTEM_PROMPT,
108
+ userPrompt,
109
+ env.groqBaseUrl,
110
+ { maxTokens: 800, temperature: 0.4 },
111
+ );
112
+
113
+ if (!parsed.insights || !Array.isArray(parsed.insights)) {
114
+ console.log('[synthesis] No valid insights returned from Groq');
115
+ return;
116
+ }
117
+
118
+ // Step 7: Filter and store insights
119
+ let stored = 0;
120
+ for (const insight of parsed.insights) {
121
+ if (stored >= MAX_INSIGHTS_PER_CYCLE) break;
122
+
123
+ // Validate structure
124
+ if (!insight.fact || !insight.topic || typeof insight.confidence !== 'number') continue;
125
+
126
+ // Confidence gate
127
+ if (insight.confidence < CONFIDENCE_GATE) continue;
128
+
129
+ // Must reference ≥2 source topics
130
+ if (!insight.source_topics || insight.source_topics.length < 2) continue;
131
+
132
+ // Normalize topic
133
+ const topic = insight.topic.startsWith('synthesis_')
134
+ ? insight.topic.toLowerCase().replace(/[^a-z0-9_]/g, '')
135
+ : `synthesis_${insight.topic.toLowerCase().replace(/[^a-z0-9_]/g, '')}`;
136
+
137
+ await recordMemory(
138
+ env.memoryBinding,
139
+ topic,
140
+ insight.fact,
141
+ insight.confidence,
142
+ 'synthesis',
143
+ );
144
+ stored++;
145
+ console.log(`[synthesis] Stored insight: ${topic} (confidence: ${insight.confidence})`);
146
+ }
147
+
148
+ console.log(`[synthesis] Cycle complete: ${stored} insights stored from ${topicEntries.size} topics`);
149
+ } catch (err) {
150
+ console.error('[synthesis] Cross-domain synthesis failed:', err instanceof Error ? err.message : String(err));
151
+ }
152
+ }
153
+
154
+ function buildFactsBlock(topicEntries: Map<string, MemoryFragmentResult[]>): string {
155
+ const sections: string[] = [];
156
+ for (const [topic, entries] of topicEntries) {
157
+ const facts = entries.map(e => ` - ${e.content} (confidence: ${e.confidence})`).join('\n');
158
+ sections.push(`### ${topic}\n${facts}`);
159
+ }
160
+ return sections.join('\n\n');
161
+ }
@@ -0,0 +1,369 @@
1
+ // ─── Memory Worker Adapter ──────────────────────────────────
2
+ // Wraps Stackbilt Memory Worker RPC calls with AEGIS's existing
3
+ // function signatures for minimal call-site disruption.
4
+ // All functions take a MemoryServiceBinding (from env.MEMORY or config.memoryBinding).
5
+ //
6
+ // Memory Worker is the sole semantic store. D1 is for operational state only.
7
+
8
+ import type { MemoryServiceBinding, MemoryFragmentResult, MemoryStatsResult, MemoryStoreRequest } from '../types.js';
9
+ import { estimateTokens } from './memory/episodic.js';
10
+ import { classifyMemoryTopic, type MemoryTopicClassification } from './classify-memory-topic.js';
11
+
12
+ const TENANT = 'aegis';
13
+ const MEMORY_CONTEXT_LIMIT = 50;
14
+ const MEMORY_TOKEN_BUDGET = 800;
15
+ // Jaccard similarity floor for treating a write as an update of an existing
16
+ // entry. Was 0.55 until Stackbilt-dev/aegis#437 — that threshold was aggressive
17
+ // enough to silently catch genuine content updates (e.g. replacing a fact that
18
+ // kept ~60% of the original words) and, combined with the old "skip write and
19
+ // return existing id" logic, caused silent data loss. Raised to 0.85 so only
20
+ // near-identical paraphrases are treated as updates; borderline cases store
21
+ // as new entries and the consolidation cycle can merge them later if needed.
22
+ const DEDUP_SIMILARITY_THRESHOLD = 0.85;
23
+
24
+ type MB = MemoryServiceBinding;
25
+
26
+ // ─── Jaccard token similarity ───────────────────────────────
27
+ function tokenize(text: string): Set<string> {
28
+ return new Set(text.toLowerCase().replace(/[^a-z0-9\s]/g, '').split(/\s+/).filter(t => t.length > 2));
29
+ }
30
+
31
+ function jaccardSimilarity(a: string, b: string): number {
32
+ const setA = tokenize(a);
33
+ const setB = tokenize(b);
34
+ if (setA.size === 0 && setB.size === 0) return 1;
35
+ let intersection = 0;
36
+ for (const t of setA) if (setB.has(t)) intersection++;
37
+ return intersection / (setA.size + setB.size - intersection);
38
+ }
39
+
40
+ // ─── recordMemory (Memory Worker only) ──────────────────────
41
+ export interface RecordMemoryResult {
42
+ fragment_id: string;
43
+ /**
44
+ * True when this call superseded an existing near-identical entry. The new
45
+ * content is stored under `fragment_id`; the superseded entry is forgotten.
46
+ * See Stackbilt-dev/aegis#437 for the bug this field exists to surface.
47
+ */
48
+ updated?: boolean;
49
+ /** When `updated` is true, the id of the entry that was superseded. */
50
+ superseded_id?: string;
51
+ }
52
+
53
+ export async function recordMemory(
54
+ mem: MB, topic: string, fact: string, confidence: number, source: string,
55
+ metadata?: Record<string, unknown>,
56
+ ): Promise<RecordMemoryResult> {
57
+ try {
58
+ // Pre-write dedup: find an existing entry that's near-identical to the
59
+ // incoming fact. If found, we'll treat this call as an update — store
60
+ // the new content first, then forget the old entry. This preserves
61
+ // operator upsert semantics without losing data if the forget step fails.
62
+ //
63
+ // Uses keyword recall + Jaccard similarity to catch rephrasings that the
64
+ // Memory Worker's hash-based dedup misses.
65
+ let existingMatch: { id: string; lifecycle: string } | null = null;
66
+ try {
67
+ const keywords = [...tokenize(fact)].slice(0, 5).join(' ');
68
+ if (keywords) {
69
+ const existing = await mem.recall(TENANT, { keywords, topic, limit: 5 });
70
+ for (const entry of existing) {
71
+ const sim = jaccardSimilarity(fact, entry.content);
72
+ if (sim >= DEDUP_SIMILARITY_THRESHOLD) {
73
+ existingMatch = { id: entry.id, lifecycle: entry.lifecycle };
74
+ break;
75
+ }
76
+ }
77
+ }
78
+ } catch {
79
+ // Dedup check failed — proceed with write anyway. Worst case: a duplicate
80
+ // lands in the store and consolidation cleans it up later. That's a much
81
+ // better failure mode than silently dropping the caller's write.
82
+ }
83
+
84
+ // Store the new content first. If this fails, the old entry (if any) is
85
+ // still intact — no data loss. Preserve lifecycle=core on upserts so
86
+ // persona/identity facts don't silently demote and get decayed away.
87
+ const storeReq: MemoryStoreRequest = { content: fact, topic, confidence, source };
88
+ if (metadata) storeReq.metadata = metadata;
89
+ if (existingMatch?.lifecycle === 'core') {
90
+ storeReq.lifecycle = 'core';
91
+ }
92
+ const result = await mem.store(TENANT, [storeReq]);
93
+ const newId = result.fragment_ids?.[0] ?? 'unknown';
94
+
95
+ // If we matched an existing entry and the store succeeded, forget the
96
+ // old one now that the replacement is safely in place. If forget fails
97
+ // here, log loudly but don't throw — the operator has their new content
98
+ // under a fresh id, and the stale entry can be cleaned up manually. This
99
+ // is strictly better than the old behavior, where the write was silently
100
+ // skipped and the operator thought their update landed.
101
+ if (existingMatch && newId !== existingMatch.id && newId !== 'unknown') {
102
+ try {
103
+ await mem.forget(TENANT, { ids: [existingMatch.id] });
104
+ console.log(`[memory-adapter] upsert: ${newId} superseded ${existingMatch.id}`);
105
+ } catch (err) {
106
+ console.error(
107
+ `[memory-adapter] upsert: store ok but forget failed (stale entry ${existingMatch.id} remains): ${err instanceof Error ? err.message : String(err)}`,
108
+ );
109
+ }
110
+ return { fragment_id: newId, updated: true, superseded_id: existingMatch.id };
111
+ }
112
+
113
+ return { fragment_id: newId };
114
+ } catch (err) {
115
+ const errorMessage = err instanceof Error ? err.message : String(err);
116
+ console.error('[memory-adapter] recordMemory failed:', errorMessage);
117
+ throw new Error(`Memory write failed: ${errorMessage}`);
118
+ }
119
+ }
120
+
121
+ // ─── recordMemoryWithAutoTopic ──────────────────────────────
122
+ // Opt-in convenience wrapper: classify the fact via TarotScript's
123
+ // memory-topic-classify spread, then write with the inferred topic.
124
+ //
125
+ // Callers pass the tarotscript service binding alongside the memory binding.
126
+ // If the classifier returns 'general' via the fallback path (classifier
127
+ // down, low confidence, unknown topic), the wrapper still writes the fact
128
+ // but with the 'general' topic — the caller can filter on the returned
129
+ // `classification.source === 'fallback'` to decide whether to re-tag later.
130
+ //
131
+ // Rollout note: this is additive, not a replacement for recordMemory. Existing
132
+ // call sites that pass an explicit topic continue to work unchanged. Callers
133
+ // wanting auto-topic opt in explicitly by switching to this wrapper. The
134
+ // feature flag lives at the call site (not in this helper) so each caller
135
+ // can independently decide when to flip on auto-topic inference.
136
+
137
+ export interface RecordMemoryWithTopicResult extends RecordMemoryResult {
138
+ classification: MemoryTopicClassification;
139
+ }
140
+
141
+ export async function recordMemoryWithAutoTopic(
142
+ mem: MB,
143
+ tarotscriptFetcher: Fetcher,
144
+ fact: string,
145
+ confidence: number,
146
+ source: string,
147
+ opts: { seed?: number } = {},
148
+ ): Promise<RecordMemoryWithTopicResult> {
149
+ const classification = await classifyMemoryTopic(tarotscriptFetcher, fact, opts);
150
+ const record = await recordMemory(mem, classification.topic, fact, confidence, source);
151
+ return { ...record, classification };
152
+ }
153
+
154
+ // ─── getMemoryEntries ───────────────────────────────────────
155
+ export async function getMemoryEntries(
156
+ mem: MB, topic?: string, limit = 50,
157
+ ): Promise<MemoryFragmentResult[]> {
158
+ try {
159
+ return await mem.recall(TENANT, { topic, limit });
160
+ } catch (err) {
161
+ console.error('[memory-adapter] getMemoryEntries failed:', err);
162
+ return [];
163
+ }
164
+ }
165
+
166
+ // ─── searchMemoryByKeywords (Memory Worker only) ─────────────
167
+ export async function searchMemoryByKeywords(
168
+ mem: MB, query: string, limit = 10,
169
+ ): Promise<Array<{ id: string; topic: string; fact: string; confidence: number }>> {
170
+ try {
171
+ const fragments = await mem.recall(TENANT, { keywords: query, limit });
172
+ return fragments.map(f => ({ id: f.id, topic: f.topic, fact: f.content, confidence: f.confidence }));
173
+ } catch (err) {
174
+ console.error('[memory-adapter] searchMemoryByKeywords failed:', err instanceof Error ? err.message : String(err));
175
+ return [];
176
+ }
177
+ }
178
+
179
+ // ─── getAllMemoryForContext (Memory Worker only) ─────────────
180
+ // Two-pass recall: core fragments (persona, identity) + general fragments.
181
+ // Memory Worker is the sole knowledge store — no D1 reads.
182
+ // Exocortex v2: TOON serialization + token budget cap + query-aware ranking.
183
+ const CORE_LIMIT = 15;
184
+ const GENERAL_LIMIT = MEMORY_CONTEXT_LIMIT - CORE_LIMIT; // 35
185
+
186
+ export async function getAllMemoryForContext(
187
+ mem: MB,
188
+ query?: string,
189
+ ): Promise<{ text: string; ids: string[] }> {
190
+ let coreFragments: MemoryFragmentResult[] = [];
191
+ let workerGeneral: MemoryFragmentResult[] = [];
192
+
193
+ // Fire both reads in parallel: Worker core + Worker general
194
+ // When query is provided, use keywords for relevance-ranked recall
195
+ const workerCorePromise = mem.recall(TENANT, { lifecycle: ['core'], limit: CORE_LIMIT })
196
+ .then(r => { coreFragments = r; })
197
+ .catch(err => {
198
+ console.error('[memory-adapter] getAllMemoryForContext core failed:', err instanceof Error ? err.message : String(err));
199
+ });
200
+
201
+ const generalRecallOpts = query
202
+ ? { keywords: query, limit: GENERAL_LIMIT + 10 }
203
+ : { limit: GENERAL_LIMIT + 10 };
204
+ const workerGeneralPromise = mem.recall(TENANT, generalRecallOpts)
205
+ .then(r => { workerGeneral = r; })
206
+ .catch(err => {
207
+ console.error('[memory-adapter] getAllMemoryForContext general failed:', err instanceof Error ? err.message : String(err));
208
+ });
209
+
210
+ await Promise.all([workerCorePromise, workerGeneralPromise]);
211
+
212
+ // Dedupe: remove core IDs from worker general
213
+ const coreIds = new Set(coreFragments.map(f => f.id));
214
+ workerGeneral = workerGeneral.filter(f => !coreIds.has(f.id));
215
+
216
+ // Take top GENERAL_LIMIT
217
+ const generalEntries = workerGeneral.slice(0, GENERAL_LIMIT);
218
+
219
+ if (coreFragments.length === 0 && generalEntries.length === 0) return { text: '', ids: [] };
220
+
221
+ // ─── TOON compact serialization ───────────────────────────
222
+ // Format: [P:dim] obs1 | obs2 (Profile/core)
223
+ // [M:topic] ●fact1 | ◐fact2 (Memory/general)
224
+ // Token budget: core always included, general fills remaining budget.
225
+
226
+ const lines: string[] = [];
227
+ const includedIds: string[] = [];
228
+ let tokenCount = 0;
229
+
230
+ // Section 1: Core fragments — always included (persona/identity)
231
+ if (coreFragments.length > 0) {
232
+ const byDimension = new Map<string, string[]>();
233
+ for (const f of coreFragments) {
234
+ const match = f.content.match(/^\[(\w+)\]\s+(.+)/);
235
+ if (match) {
236
+ const list = byDimension.get(match[1]) ?? [];
237
+ list.push(match[2]);
238
+ byDimension.set(match[1], list);
239
+ } else {
240
+ const list = byDimension.get(f.topic) ?? [];
241
+ list.push(f.content);
242
+ byDimension.set(f.topic, list);
243
+ }
244
+ includedIds.push(f.id);
245
+ }
246
+ for (const [dim, observations] of byDimension) {
247
+ lines.push(`[P:${dim}] ${observations.join(' | ')}`);
248
+ }
249
+ tokenCount = estimateTokens(lines.join('\n'));
250
+ }
251
+
252
+ // Section 2: General memory — budget-capped, grouped by topic
253
+ if (generalEntries.length > 0) {
254
+ const byTopic = new Map<string, MemoryFragmentResult[]>();
255
+ for (const f of generalEntries) {
256
+ const group = byTopic.get(f.topic) ?? [];
257
+ group.push(f);
258
+ byTopic.set(f.topic, group);
259
+ }
260
+
261
+ for (const [topic, entries] of byTopic) {
262
+ const facts = entries.map(e => {
263
+ const conf = e.confidence >= 0.8 ? '●' : e.confidence >= 0.5 ? '◐' : '○';
264
+ return `${conf}${e.content}`;
265
+ });
266
+ const line = `[M:${topic}] ${facts.join(' | ')}`;
267
+ const lineCost = estimateTokens(line);
268
+
269
+ if (tokenCount + lineCost > MEMORY_TOKEN_BUDGET) {
270
+ // Try adding individual facts from this topic until budget exhausted
271
+ for (let i = 0; i < facts.length; i++) {
272
+ const partial = `[M:${topic}] ${facts.slice(0, i + 1).join(' | ')}`;
273
+ if (tokenCount + estimateTokens(partial) > MEMORY_TOKEN_BUDGET) break;
274
+ if (i === facts.length - 1) {
275
+ lines.push(partial);
276
+ tokenCount += estimateTokens(partial);
277
+ for (const e of entries.slice(0, i + 1)) includedIds.push(e.id);
278
+ }
279
+ }
280
+ break; // Budget exhausted — stop processing topics
281
+ }
282
+
283
+ lines.push(line);
284
+ tokenCount += lineCost;
285
+ for (const e of entries) includedIds.push(e.id);
286
+ }
287
+ }
288
+
289
+ const total = coreFragments.length + generalEntries.length;
290
+ const included = includedIds.length;
291
+ if (included < total) {
292
+ console.log(`[memory-adapter] context budget: ${included}/${total} fragments (~${tokenCount} tokens, budget ${MEMORY_TOKEN_BUDGET})`);
293
+ }
294
+
295
+ return { text: '\n' + lines.join('\n'), ids: includedIds };
296
+ }
297
+
298
+ // ─── recallMemory ───────────────────────────────────────────
299
+ // No-op — Memory Worker auto-tracks recall in recall()
300
+ export async function recallMemory(_mem: MB, _ids: string[]): Promise<void> {}
301
+
302
+ // ─── pruneMemory ────────────────────────────────────────────
303
+ // Split: memory decay → Memory Worker; AEGIS-specific cleanup → local D1
304
+ export async function pruneMemory(mem: MB, db: D1Database): Promise<void> {
305
+ try {
306
+ const result = await mem.decay(TENANT);
307
+ console.log(`[memory-adapter] decay: decayed=${result.decayed} pruned=${result.pruned} promoted=${result.promoted}`);
308
+ } catch (err) {
309
+ console.error('[memory-adapter] decay failed:', err);
310
+ }
311
+
312
+ // AEGIS-specific cleanup (non-memory tables)
313
+ await db.prepare("DELETE FROM web_events WHERE received_at < datetime('now', '-48 hours')").run();
314
+ await db.prepare(`
315
+ DELETE FROM heartbeat_results
316
+ WHERE id NOT IN (SELECT id FROM heartbeat_results ORDER BY created_at DESC LIMIT 100)
317
+ AND created_at < datetime('now', '-30 days')
318
+ `).run();
319
+ }
320
+
321
+ // ─── getMemoryStats ─────────────────────────────────────────
322
+ export async function getMemoryStats(mem: MB): Promise<MemoryStatsResult> {
323
+ try {
324
+ return await mem.stats(TENANT);
325
+ } catch (err) {
326
+ console.error('[memory-adapter] getMemoryStats failed:', err);
327
+ return { total_active: 0, topics: [], recalled_last_24h: 0, strength_distribution: { low: 0, medium: 0, high: 0 } };
328
+ }
329
+ }
330
+
331
+ // ─── forgetMemory ───────────────────────────────────────────
332
+ export async function forgetMemory(mem: MB, ids: string[]): Promise<number> {
333
+ try {
334
+ return await mem.forget(TENANT, { ids });
335
+ } catch (err) {
336
+ console.error('[memory-adapter] forgetMemory failed:', err);
337
+ return 0;
338
+ }
339
+ }
340
+
341
+ // ─── getMemoryForConsolidation ──────────────────────────────
342
+ export async function getMemoryForConsolidation(
343
+ mem: MB,
344
+ ): Promise<Array<{ id: string; topic: string; fact: string }>> {
345
+ try {
346
+ const fragments = await mem.recall(TENANT, { limit: 40 });
347
+ return fragments.map(f => ({ id: f.id, topic: f.topic, fact: f.content }));
348
+ } catch (err) {
349
+ console.error('[memory-adapter] getMemoryForConsolidation failed:', err);
350
+ return [];
351
+ }
352
+ }
353
+
354
+ // ─── getAllMemoryForReflection ───────────────────────────────
355
+ export async function getAllMemoryForReflection(
356
+ mem: MB,
357
+ ): Promise<Array<{ id: string; topic: string; fact: string; confidence: number; source: string; strength: number; created_at: string; last_accessed_at: string }>> {
358
+ try {
359
+ const fragments = await mem.recall(TENANT, { limit: 100 });
360
+ return fragments.map(f => ({
361
+ id: f.id, topic: f.topic, fact: f.content, confidence: f.confidence,
362
+ source: 'memory_worker', strength: f.strength,
363
+ created_at: f.created_at, last_accessed_at: f.last_accessed_at,
364
+ }));
365
+ } catch (err) {
366
+ console.error('[memory-adapter] getAllMemoryForReflection failed:', err);
367
+ return [];
368
+ }
369
+ }
@@ -0,0 +1,76 @@
1
+ // Memory Guardrails — shared validation for LLM-generated memory writes.
2
+ // Used by both MCP handlers (external writes) and dreaming (internal writes).
3
+
4
+ // Topics that produce low-value synthesis noise
5
+ const BLOCKED_TOPIC_PREFIXES = ['synthesis_', 'cross_repo_insight'];
6
+
7
+ // Prompt injection / system override attempts
8
+ const BLOCKED_TOPIC_PATTERNS = [
9
+ /^system[_-]?prompt/i,
10
+ /^system[_-]?override/i,
11
+ /^system[_-]?instruction/i,
12
+ /^instruction[_-]?override/i,
13
+ /^admin[_-]?override/i,
14
+ ];
15
+
16
+ // Secrets that should never be stored in memory
17
+ const SECRET_PATTERNS = [
18
+ /\bsk-ant-[a-zA-Z0-9-]{50,}\b/,
19
+ /\bsk-[a-zA-Z0-9]{40,}\b/,
20
+ /\b(ANTHROPIC|OPENAI|GROQ|RESEND|STRIPE|BRAVE|GITHUB)_[A-Z_]*KEY[=:]\s*\S+/i,
21
+ /\bghp_[a-zA-Z0-9]{36}\b/,
22
+ /\bre_[a-zA-Z0-9]{20,}\b/,
23
+ ];
24
+
25
+ // Established topic taxonomy — add your project-specific topics here
26
+ const ALLOWED_TOPICS = new Set([
27
+ 'aegis', 'infrastructure', 'compliance', 'content',
28
+ 'bizops', 'finance', 'project', 'operator_preferences',
29
+ 'operator_persona', 'meta_insight', 'feed_intel',
30
+ 'symbolic_reflection', 'self_improvement_outcomes',
31
+ ]);
32
+
33
+ const MAX_FACT_LENGTH = 2000;
34
+
35
+ export interface GuardrailResult {
36
+ allowed: boolean;
37
+ reason?: string;
38
+ }
39
+
40
+ /**
41
+ * Validate a memory write — returns { allowed: true } or { allowed: false, reason }.
42
+ * Checks topic blocklists, secret patterns, fact length, and topic allowlist.
43
+ */
44
+ export function validateMemoryWrite(
45
+ topic: string,
46
+ fact: string,
47
+ options?: { enforceAllowlist?: boolean },
48
+ ): GuardrailResult {
49
+ if (!topic || !fact) return { allowed: false, reason: 'topic and fact are required' };
50
+ if (fact.length > MAX_FACT_LENGTH) return { allowed: false, reason: `Fact exceeds max length (${fact.length}/${MAX_FACT_LENGTH})` };
51
+ if (fact.length < 20) return { allowed: false, reason: 'Fact too short to be useful (<20 chars)' };
52
+
53
+ const topicLower = topic.toLowerCase().trim();
54
+
55
+ // Block prompt injection attempts
56
+ if (BLOCKED_TOPIC_PATTERNS.some(p => p.test(topic))) {
57
+ return { allowed: false, reason: `Topic "${topic}" is not allowed — reserved namespace` };
58
+ }
59
+
60
+ // Block polluting synthesis topics
61
+ if (BLOCKED_TOPIC_PREFIXES.some(p => topicLower.startsWith(p))) {
62
+ return { allowed: false, reason: `Topic "${topic}" is blocked — polluting prefix` };
63
+ }
64
+
65
+ // Block secrets
66
+ if (SECRET_PATTERNS.some(p => p.test(fact))) {
67
+ return { allowed: false, reason: 'Fact appears to contain a secret/credential — refusing to store' };
68
+ }
69
+
70
+ // Optional: enforce topic allowlist (stricter — used by dreaming)
71
+ if (options?.enforceAllowlist && !ALLOWED_TOPICS.has(topicLower)) {
72
+ return { allowed: false, reason: `Unknown topic '${topic}' — add to ALLOWED_TOPICS if legitimate` };
73
+ }
74
+
75
+ return { allowed: true };
76
+ }
@@ -0,0 +1,23 @@
1
+ // AegisExecutorPort — the sole contract between the kernel and all interface adapters.
2
+ // Adapters (voice, CLI, MCP) consume this port. The kernel never imports adapter types.
3
+
4
+ export interface AegisTurnInput {
5
+ sessionId: string;
6
+ userId: string;
7
+ text: string;
8
+ signal?: AbortSignal;
9
+ context?: Record<string, unknown>;
10
+ }
11
+
12
+ export type AegisTurnEvent =
13
+ | { type: 'text.delta'; text: string }
14
+ | { type: 'tool.call'; name: string; args: unknown }
15
+ | { type: 'tool.result'; name: string; result: unknown }
16
+ | { type: 'memory.write'; patch: unknown }
17
+ | { type: 'warning'; message: string }
18
+ | { type: 'done' };
19
+
20
+ export interface AegisExecutorPort {
21
+ dispatch(input: AegisTurnInput): AsyncIterable<AegisTurnEvent>;
22
+ cancel?(sessionId: string): Promise<void>;
23
+ }