diff --git a/CHANGELOG.md b/CHANGELOG.md
index 61b9eac..9658f20 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@
## Unreleased
+### Changed
+- **Memory unification follow-up (aegis-oss#78)**: `consolidateEpisodicToSemantic` (`kernel/memory/consolidation.ts`) redesigned to write through `writeDreamFact()` (new `kernel/memory/dream-write.ts`) into the wiki's `dreams` scope, instead of writing raw fragments straight into the memory-worker fragment store — this was the deferred piece of the #457 wiki unification the original decision doc flagged as needing design work. Consolidation is now purely additive (extract 0-3 genuine facts per cycle, write each as its own dream page); the old ADD/UPDATE/DELETE-by-fragment-id model doesn't map cleanly onto wiki pages, so update/delete semantics are dropped in favor of the dreams lifecycle (promote on corroboration, archive if stale) already used by PRISM's cross-domain synthesis.
+- Removed `publishInsightsFromMemory()` from `kernel/scheduled/consolidation.ts` — this CRIX insight-publishing step called `publishInsight()`/`validateInsight()` against a D1 table literally named `memory`, which is never created by any migration in this repo or in aegis-daemon. Every invocation was silently failing (caught and logged as a warning) on every consolidation cycle. `insights.ts`'s public API (`publishInsight`, `validateInsight`, `promoteInsight`, `listInsights`, etc.) is left intact as OSS surface for downstream consumers — only the internal, broken call site was removed.
+
### Added
- OTDD contract test suites for all five bounded contexts — AgendaItem (48 tests), CCTask (53 tests), Goal (65 tests), ExecutorRouter I1/I2 violation paths (4 tests), and MemoryEntry/CRIX pipeline (33 tests). Tests are derived directly from `.contract.ts` schema, state machine, invariant, and authority declarations with no mocks.
diff --git a/web/package.json b/web/package.json
index f010716..4b58d2f 100755
--- a/web/package.json
+++ b/web/package.json
@@ -1,6 +1,6 @@
{
"name": "@stackbilt/aegis-core",
- "version": "0.8.4",
+ "version": "0.8.7",
"description": "Persistent AI agent framework for Cloudflare Workers. Multi-tier memory, autonomous goals, dreaming cycles, MCP native.",
"license": "Apache-2.0",
"publishConfig": {
diff --git a/web/src/email.ts b/web/src/email.ts
index 635467d..ace634d 100755
--- a/web/src/email.ts
+++ b/web/src/email.ts
@@ -280,7 +280,47 @@ export async function sendDailyDigest(
return `${s.toUpperCase()}`;
};
- // ── Section 1: WORK SHIPPED ──
+ // ── LEAD SIGNAL ──
+ // Single highest-priority banner. Only fires when truly actionable.
+ let leadSignal: { label: string; text: string; color: string } | null = null;
+
+ const critAlerts = sections.serviceAlerts.filter(a => a.severity === 'critical');
+ const highAlerts = sections.serviceAlerts.filter(a => a.severity === 'high');
+ const hasFailedPayment = sections.eventNotifications.some(e =>
+ e.event_type.includes('payment') && e.event_type.includes('fail'),
+ );
+ const revenueEvents = sections.eventNotifications.filter(e =>
+ e.event_type.includes('invoice.paid') || e.event_type.includes('checkout.session.completed'),
+ );
+ const deadlineAgenda = sections.agendaItems.filter(a => /^upcoming_deadlines:/i.test(a.item));
+ const failRate = sections.failedTasks.length / Math.max(1, sections.completedTasks.length + sections.failedTasks.length);
+ const critHealthChecks = sections.healthChecks.filter(h => h.severity === 'critical');
+
+ if (critAlerts.length > 0) {
+ leadSignal = { label: 'CRITICAL', text: critAlerts[0].summary, color: '#ef4444' };
+ } else if (critHealthChecks.length > 0) {
+ leadSignal = { label: 'CRITICAL', text: `System health: ${critHealthChecks[0].checks.find(c => c.status !== 'ok')?.name ?? 'check failed'}`, color: '#ef4444' };
+ } else if (hasFailedPayment) {
+ leadSignal = { label: 'PAYMENT FAILURE', text: 'Failed payment detected — check Stripe', color: '#ef4444' };
+ } else if (highAlerts.length > 0) {
+ leadSignal = { label: 'ALERT', text: highAlerts[0].summary, color: '#f5a623' };
+ } else if (failRate > 0.5 && sections.failedTasks.length >= 2) {
+ leadSignal = { label: 'FAIL RATE', text: `${Math.round(failRate * 100)}% task failure — review before queuing more`, color: '#f5a623' };
+ } else if (deadlineAgenda.length > 0) {
+ const text = deadlineAgenda[0].item.replace(/^upcoming_deadlines:\s*/i, '');
+ leadSignal = { label: 'DEADLINE', text, color: '#f5a623' };
+ } else if (revenueEvents.length > 0) {
+ leadSignal = { label: 'REVENUE', text: `${revenueEvents.length} payment${revenueEvents.length !== 1 ? 's' : ''} received`, color: '#2dd4a0' };
+ }
+
+ const leadHtml = leadSignal
+ ? `
+
${leadSignal.label}
+
${leadSignal.text}
+
`
+ : '';
+
+ // ── WORK SHIPPED ──
let workShippedHtml = '';
if (sections.failedTasks.length > 0 || sections.completedTasks.length > 0) {
const taskRow = (t: DigestTask) => {
@@ -303,122 +343,72 @@ export async function sendDailyDigest(
`;
}
- // ── Section 2: OPERATOR'S LOG ──
- let operatorLogHtml = '';
- if (sections.operatorLog) {
- const contentHtml = sections.operatorLog
- .split('\n\n')
- .filter(p => p.trim())
- .map(p => {
- if (p.startsWith('## ')) return `${p.slice(3)}
`;
- if (p.startsWith('### ')) return `${p.slice(4)}
`;
- const formatted = p.replace(/\*\*(.+?)\*\*/g, '$1');
- return `${formatted}
`;
- })
- .join('');
-
- operatorLogHtml = `
-
-
Operator's Log
-
- ${contentHtml}
-
-
`;
- }
-
- // ── Section 3: SYSTEM HEALTH ──
- let healthHtml = '';
- if (sections.healthChecks.length > 0) {
- const allChecks = sections.healthChecks.flatMap(h => h.checks.map(c => ({ ...c, severity: h.severity })));
- const checksRows = allChecks.map(c => {
- const color = c.status === 'alert' ? '#ff6b6b' : '#ffd93d';
- return `
+ // ── SERVICE ALERTS (high/critical only) ──
+ let serviceAlertsHtml = '';
+ const actionableAlerts = sections.serviceAlerts.filter(a =>
+ a.severity === 'critical' || a.severity === 'high',
+ );
+ if (actionableAlerts.length > 0) {
+ const sevColor = (s: string) => s === 'critical' ? '#ef4444' : '#f5a623';
+ const alertRows = actionableAlerts.map(a => `
- | ${c.name} |
- ${c.detail} |
-
`;
- }).join('');
-
- healthHtml = `
-
-
System Health
-
-
-
- | Check |
- Detail |
-
-
- ${checksRows}
-
-
`;
- }
+ ${a.severity.toUpperCase()} |
+ ${a.source} |
+ ${a.summary} |
+ `).join('');
- // ── Section 3b: ARGUS EVENTS ──
- let eventsHtml = '';
- if (sections.eventNotifications.length > 0) {
- const eventRows = sections.eventNotifications.map(e => {
- const color = e.priority === 'high' ? '#f5a623' : '#888';
- return `
-
- | ${e.source} |
- ${e.event_type} |
- ${e.summary} |
- ${e.ts.slice(0, 16)} |
-
`;
- }).join('');
-
- eventsHtml = `
+ serviceAlertsHtml = `
-
ARGUS Events (${sections.eventNotifications.length})
+
Alerts (${actionableAlerts.length})
+ | Sev |
Source |
- Event |
Summary |
- Time |
- ${eventRows}
+ ${alertRows}
`;
}
- // ── Section 3c: COGNITIVE SCORECARD ──
- let metricsHtml = '';
- if (sections.cognitiveMetrics) {
- const m = sections.cognitiveMetrics;
- const arrow = m.score_delta > 0 ? '▲' : m.score_delta < 0 ? '▼' : '▬';
- const arrowColor = m.score_delta > 0 ? '#2dd4a0' : m.score_delta < 0 ? '#ef4444' : '#888';
- const scoreColor = m.cognitive_score >= 75 ? '#2dd4a0' : m.cognitive_score >= 50 ? '#f5a623' : '#ef4444';
- const costDelta = m.avg_cost_prior_7d > 0
- ? ((m.avg_cost_7d - m.avg_cost_prior_7d) / m.avg_cost_prior_7d * 100).toFixed(0)
- : '0';
- const costArrow = Number(costDelta) <= 0 ? '#2dd4a0' : '#ef4444';
-
- metricsHtml = `
-
-
Cognitive Scorecard
-
-
- ${m.cognitive_score}
- ${arrow} ${Math.abs(m.score_delta)}
- /100
-
-
- | Dispatch success | ${Math.round(m.dispatch_success_rate_7d * 100)}% |
- | Procedures learned | ${Math.round(m.procedure_convergence_rate * 100)}% |
- | Tasks shipped (7d) | ${m.tasks_completed_7d} / ${m.tasks_completed_7d + m.tasks_failed_7d} |
- | Avg cost/dispatch | $${m.avg_cost_7d.toFixed(4)} (${Number(costDelta) <= 0 ? '' : '+'}${costDelta}%) |
- | Memory entries | ${m.memory_count} |
- ${m.top_failure_kind ? `| Top failure mode | ${m.top_failure_kind} |
` : ''}
+ // ── SYSTEM HEALTH (critical/high only) ──
+ let healthHtml = '';
+ const actionableHealthChecks = sections.healthChecks.filter(h =>
+ h.severity === 'critical' || h.severity === 'high',
+ );
+ if (actionableHealthChecks.length > 0) {
+ const nonOkChecks = actionableHealthChecks.flatMap(h =>
+ h.checks.filter(c => c.status !== 'ok').map(c => ({ ...c, severity: h.severity })),
+ );
+ if (nonOkChecks.length > 0) {
+ const checksRows = nonOkChecks.map(c => {
+ const color = c.status === 'alert' ? '#ff6b6b' : '#ffd93d';
+ return `
+
+ | ${c.name} |
+ ${c.detail} |
+
`;
+ }).join('');
+
+ healthHtml = `
+
+
System Health
+
+
+
+ | Check |
+ Detail |
+
+
+ ${checksRows}
-
- `;
+ `;
+ }
}
- // ── Section 3c2: ANALYTICS ──
+ // ── TRAFFIC (GA4) — numbers + tables, no auto-commentary ──
let analyticsHtml = '';
if (sections.analytics && sections.analytics.sessions_7d > 0) {
const a = sections.analytics;
@@ -442,12 +432,6 @@ export async function sendDailyDigest(
${s.sessions} |
`).join('');
- const insightsHtml = a.insights.length > 0
- ? `
-
${a.insights.map(i => `- ${i}
`).join('')}
-
`
- : '';
-
analyticsHtml = `
Traffic (GA4)
@@ -469,242 +453,118 @@ export async function sendDailyDigest(
Medium |
Sessions |
${sourcesRows}
` : ''}
- ${insightsHtml}
-
-
`;
- }
-
- // ── Section 3c3: DEVELOPER ACTIVITY ──
- let devActivityHtml = '';
- if (sections.devActivity) {
- const d = sections.devActivity;
- const tierBadges = d.tier_breakdown.map(t => {
- const colors: Record = { free: '#888', hobby: '#3dd6c8', pro: '#8b8bff', enterprise: '#f5a623' };
- return `${t.tier} ${t.count}`;
- }).join('');
-
- const signupRows = d.recent_signups.slice(0, 10).map(s => `
-
- | ${s.name || '(no name)'} |
- ${s.email} |
- ${s.tier} |
- ${s.created_at.slice(0, 10)} |
-
`).join('');
-
- devActivityHtml = `
-
-
Developer Activity
-
-
-
${d.total_users} users
-
${d.total_tenants} tenants
-
${d.keys_active_24h} active (24h)
-
-
- | Keys created (24h) | ${d.keys_created_24h} |
- | Keys created (7d) | ${d.keys_created_7d} |
- | Keys created (all-time) | ${d.keys_created_all_time} |
- | Active keys (7d) | ${d.keys_active_7d} |
-
-
${tierBadges || 'No active keys'}
- ${signupRows ? `
Recent Signups (7d)
-
-
- | Name |
- Email |
- Tier |
- Joined |
-
${signupRows}
` : ''}
-
-
`;
- }
-
- // ── Section 3b2: SERVICE ALERTS ──
- let serviceAlertsHtml = '';
- if (sections.serviceAlerts.length > 0) {
- const sevColor = (s: string) => s === 'critical' ? '#ef4444' : s === 'high' ? '#f5a623' : s === 'medium' ? '#ffd93d' : '#888';
- const alertRows = sections.serviceAlerts.map(a => `
-
- | ${a.severity.toUpperCase()} |
- ${a.source} |
- ${a.summary}${a.findingsCount > 0 ? ` (${a.findingsCount} findings)` : ''} |
-
`).join('');
-
- serviceAlertsHtml = `
-
-
Service Alerts (${sections.serviceAlerts.length})
-
-
-
- | Severity |
- Source |
- Summary |
-
-
- ${alertRows}
-
-
`;
- }
-
- // ── Section 3d: CO-FOUNDER'S TAKE ──
- // Opinionated synthesis: what matters, what's off track, what nobody's working on
- let cofounderHtml = '';
- {
- const takes: string[] = [];
-
- // Revenue signal
- const hasRevenue = sections.eventNotifications.some(e => e.event_type.includes('payment') || e.event_type.includes('checkout') || e.event_type.includes('invoice.paid'));
- const hasFailedPayment = sections.eventNotifications.some(e => e.event_type.includes('failed'));
- if (hasRevenue && !hasFailedPayment) {
- takes.push('Revenue is flowing. Keep shipping.');
- } else if (hasFailedPayment) {
- takes.push('Payment failures detected. Check Stripe dashboard before anything else today.');
- }
-
- // Task health
- const failRate = sections.failedTasks.length / Math.max(1, sections.completedTasks.length + sections.failedTasks.length);
- if (failRate > 0.4 && sections.failedTasks.length >= 3) {
- takes.push(`Task failure rate is ${Math.round(failRate * 100)}% — the taskrunner is burning cycles. Review failed tasks before queuing more.`);
- }
-
- // Stale proposals
- const staleProposals = sections.agendaItems.filter(a => {
- if (!a.item.startsWith('[PROPOSED ACTION]') || !a.created_at) return false;
- const ageDays = (Date.now() - new Date(a.created_at).getTime()) / 86_400_000;
- return ageDays > 4;
- });
- if (staleProposals.length > 0) {
- takes.push(`${staleProposals.length} proposed action${staleProposals.length > 1 ? 's' : ''} aging out. Approve or dismiss — stale proposals mean I'm doing work you're not reviewing.`);
- }
-
- // High-priority agenda items piling up
- const highItems = sections.agendaItems.filter(a => a.priority === 'high' && !a.item.startsWith('[PROPOSED'));
- if (highItems.length >= 4) {
- takes.push(`${highItems.length} high-priority agenda items. That's too many "high" items — either some aren't really high, or we need a focused triage session.`);
- }
-
- // Nothing shipped
- if (sections.completedTasks.length === 0 && sections.failedTasks.length === 0) {
- takes.push('No tasks ran in the last 24h. Is the taskrunner down, or is this intentional?');
- }
-
- // Health checks surfacing
- if (sections.healthChecks.length > 0) {
- const critChecks = sections.healthChecks.filter(h => h.severity === 'critical');
- if (critChecks.length > 0) {
- takes.push('Critical health checks in the system. This takes priority over feature work.');
- }
- }
-
- // Service alerts
- const critAlerts = sections.serviceAlerts.filter(a => a.severity === 'critical');
- if (critAlerts.length > 0) {
- takes.push(`${critAlerts.length} critical service alert${critAlerts.length > 1 ? 's' : ''} from ${[...new Set(critAlerts.map(a => a.source))].join(', ')}. Review immediately.`);
- }
-
- // Developer activity signals
- if (sections.devActivity) {
- if (sections.devActivity.recent_signups.length > 0) {
- takes.push(`${sections.devActivity.recent_signups.length} new signup${sections.devActivity.recent_signups.length !== 1 ? 's' : ''} this week. People are finding us.`);
- }
- if (sections.devActivity.keys_active_24h === 0 && sections.devActivity.keys_created_all_time > 0) {
- takes.push('No active API keys in the last 24h. Users signed up but aren\'t hitting endpoints — check onboarding friction.');
- }
- }
-
- // Memory reflection available
- if (sections.memoryReflection) {
- takes.push('Weekly reflection attached below — worth a 2-minute read to see what I\'m learning.');
- }
-
- if (takes.length > 0) {
- const takesHtml = takes.map(t => `${t}`).join('');
- cofounderHtml = `
- `;
- }
}
- // ── Section 3d: MEMORY REFLECTION ──
- let reflectionHtml = '';
- if (sections.memoryReflection) {
- const formatted = sections.memoryReflection
- .split('\n\n')
- .filter(p => p.trim())
- .map(p => `${p.replace(/\*\*(.+?)\*\*/g, '$1')}
`)
- .join('');
- reflectionHtml = `
-
-
Weekly Reflection
-
${formatted}
-
`;
- }
-
- // ── Section 4: AWAITING ACTION ──
+ // ── AWAITING ACTION — capped, grouped ──
let awaitingHtml = '';
- // Split agenda into proposed actions vs regular high-priority items
- const proposedAgenda = sections.agendaItems.filter(a => a.item.startsWith('[PROPOSED ACTION]'));
- const highAgenda = sections.agendaItems.filter(a => a.priority === 'high' && !a.item.startsWith('[PROPOSED ACTION]'));
- if (sections.proposedTasks.length > 0 || proposedAgenda.length > 0 || highAgenda.length > 0) {
- const proposedTaskRows = sections.proposedTasks.map(t => `
+ {
+ const isSkillEvo = (t: DigestTask) =>
+ /\[skill-evolution\]|prism synthesis|promote mindspring/i.test(t.title);
+ const skillEvoTasks = sections.proposedTasks.filter(isSkillEvo);
+ const otherProposedTasks = sections.proposedTasks.filter(t => !isSkillEvo(t)).slice(0, 3);
+
+ const isSentinel = (a: DigestAgendaItem) => /^\[SENTINEL/i.test(a.item);
+ const isDeadline = (a: DigestAgendaItem) => /^upcoming_deadlines:/i.test(a.item);
+ const isProposedAction = (a: DigestAgendaItem) => a.item.startsWith('[PROPOSED ACTION]');
+
+ const sentinelItems = sections.agendaItems.filter(isSentinel);
+ const proposedActionItems = sections.agendaItems.filter(isProposedAction).slice(0, 2);
+ const regularHigh = sections.agendaItems
+ .filter(a => a.priority === 'high' && !isSentinel(a) && !isDeadline(a) && !isProposedAction(a))
+ .slice(0, 3);
+
+ const hasContent =
+ otherProposedTasks.length > 0 || skillEvoTasks.length > 0 ||
+ sentinelItems.length > 0 || deadlineAgenda.length > 0 ||
+ proposedActionItems.length > 0 || regularHigh.length > 0;
+
+ if (hasContent) {
+ // Deadlines first — time-sensitive
+ const deadlineRows = deadlineAgenda.map(a => {
+ const text = a.item.replace(/^upcoming_deadlines:\s*/i, '');
+ return `
+ `;
+ }).join('');
+
+ // Non-skill-evo proposed tasks (capped at 3)
+ const proposedTaskRows = otherProposedTasks.map(t => `
${statusBadge('pending')}${t.repo} · ${t.category}
${t.title}
Awaiting approval · ID: ${t.id.slice(0, 8)}
`).join('');
- // Proposed action agenda items with expiry countdown (auto-expire at 7d)
- const proposedAgendaRows = proposedAgenda.map(a => {
- const ageDays = Math.floor((Date.now() - new Date(a.created_at ?? Date.now()).getTime()) / 86_400_000);
- const daysLeft = Math.max(0, 7 - ageDays);
- const expiryNote = daysLeft <= 2 ? ` expires in ${daysLeft}d` : ` ${daysLeft}d until auto-expire`;
- return `
-
-
#${a.id} · proposed${expiryNote}
-
${a.item.replace('[PROPOSED ACTION] ', '')}
-
`;
- }).join('');
-
- const agendaRows = highAgenda.map(a => `
+ // Skill-evolution collapsed to one row
+ const skillEvoRow = skillEvoTasks.length > 0
+ ? `
+
${statusBadge('pending')}skill-evolution · batch
+
${skillEvoTasks.length} skill-evolution proposal${skillEvoTasks.length !== 1 ? 's' : ''} pending — use aegis_batch_approve to review
+
`
+ : '';
+
+ // Proposed action agenda items (capped at 2)
+ const proposedActionRows = proposedActionItems.map(a => {
+ const ageDays = Math.floor((Date.now() - new Date(a.created_at ?? Date.now()).getTime()) / 86_400_000);
+ const daysLeft = Math.max(0, 7 - ageDays);
+ const expiryNote = daysLeft <= 2
+ ? ` expires in ${daysLeft}d`
+ : ` ${daysLeft}d left`;
+ return `
+
+
#${a.id} · proposed${expiryNote}
+
${a.item.replace('[PROPOSED ACTION] ', '')}
+
`;
+ }).join('');
+
+ // Regular high-priority items (capped at 3)
+ const regularRows = regularHigh.map(a => `
-
#${a.id} · ${a.priority}
+
#${a.id} · high
${a.item}
`).join('');
- // Review prompt only when proposals exist
- const reviewPrompt = proposedAgenda.length > 0
- ? `${proposedAgenda.length} proposed action${proposedAgenda.length !== 1 ? 's' : ''} awaiting review. Unapproved proposals auto-expire after 7 days.
`
- : '';
-
- awaitingHtml = `
-
-
Awaiting Action
- ${proposedTaskRows}
- ${proposedAgendaRows}
- ${agendaRows}
- ${reviewPrompt}
-
`;
+ // Sentinel items — shown as chronic, not daily action items
+ const sentinelRows = sentinelItems.length > 0
+ ? `
+
Chronic (${sentinelItems.length})
+ ${sentinelItems.map(a => {
+ const ageDays = Math.floor((Date.now() - new Date(a.created_at ?? Date.now()).getTime()) / 86_400_000);
+ const label = a.item.replace(/^\[SENTINEL[^\]]*\]\s*/i, '').slice(0, 80);
+ return `
#${a.id} · day ${ageDays} · ${label}
`;
+ }).join('')}
+
`
+ : '';
+
+ awaitingHtml = `
+
+
Awaiting Action
+ ${deadlineRows}
+ ${proposedTaskRows}
+ ${skillEvoRow}
+ ${proposedActionRows}
+ ${regularRows}
+ ${sentinelRows}
+
`;
+ }
}
- // ── Section 5: STATS BAR ──
+ // ── STATS BAR ──
const stats = [
`${sections.completedTasks.length + sections.failedTasks.length} tasks`,
`${sections.proposedTasks.length} proposed`,
`${sections.agendaItems.length} agenda`,
- `${sections.healthChecks.length} health checks`,
];
- if (sections.eventNotifications.length > 0) stats.push(`${sections.eventNotifications.length} events`);
if (sections.serviceAlerts.length > 0) stats.push(`${sections.serviceAlerts.length} alerts`);
if (sections.devActivity) stats.push(`${sections.devActivity.total_users} users`);
- if (sections.bizopsInteractions !== null) stats.push(`${sections.bizopsInteractions} interactions`);
const statsHtml = `${stats.join(' · ')}
`;
- const hasContent = metricsHtml || cofounderHtml || workShippedHtml || operatorLogHtml || healthHtml || eventsHtml || serviceAlertsHtml || analyticsHtml || devActivityHtml || reflectionHtml || awaitingHtml;
+ const hasContent = leadHtml || workShippedHtml || serviceAlertsHtml || healthHtml || analyticsHtml || awaitingHtml;
const html = `
@@ -715,7 +575,7 @@ export async function sendDailyDigest(
AEGIS — Co-Founder Brief
${date}
- ${hasContent ? `${metricsHtml}${cofounderHtml}${workShippedHtml}${operatorLogHtml}${healthHtml}${eventsHtml}${serviceAlertsHtml}${analyticsHtml}${devActivityHtml}${reflectionHtml}${awaitingHtml}` : 'No significant activity in the last 24 hours.
'}
+ ${hasContent ? `${leadHtml}${workShippedHtml}${serviceAlertsHtml}${healthHtml}${analyticsHtml}${awaitingHtml}` : 'No significant activity in the last 24 hours.
'}
${statsHtml}
aegis-web · daily digest · ${new Date().toISOString()}
diff --git a/web/src/kernel/memory/consolidation.ts b/web/src/kernel/memory/consolidation.ts
index d68f092..623e490 100755
--- a/web/src/kernel/memory/consolidation.ts
+++ b/web/src/kernel/memory/consolidation.ts
@@ -1,194 +1,109 @@
-import { askGroq } from '../../groq.js';
-import { normalizeTopic, factHash } from './semantic.js';
-import { extractNodes, createEdges } from './graph.js';
-
-// ─── Memory Consolidation ───────────────────────────────────
-
-const CONSOLIDATION_SYSTEM = `You are AEGIS memory consolidation. Analyze recent episodes against existing memory and produce structured operations.
-
-Operations:
-- ADD: a genuinely new fact not covered by existing memory
-- UPDATE: an existing fact that needs correction or has new information (reference its id via supersedes_id)
-- DELETE: an existing fact that is no longer accurate (reference its id via target_id, provide reason)
-- NOOP: nothing to do — return []
-
-Rules:
-1. Every fact MUST contain at least one specific detail: a name, date, number, ID, URL, or version. Never write vague observations.
-2. If episodes merely confirm existing facts, return [].
-3. Prefer UPDATE over ADD when a fact evolves (e.g., "Phase 2 planned" → "Phase 2 complete").
-4. DELETE facts that are provably wrong or obsolete based on episode evidence.
-5. Max 3 operations per run. Quality over quantity.
-6. Good facts: "Delaware PBC franchise tax filed 2026-03-03, 2 days late" or "BizOps dashboard_summary tool fails when org has no projects (undefined.id)".
-7. Bad facts: "Financial metrics are important" or "Document gaps exist and require attention".
-
-KNOWN TOPICS — reuse these whenever the fact fits. Only create a new topic if genuinely none of these apply:
-- aegis: cognitive kernel internals, architecture, infrastructure, versioning, deployment
-- img_forge: img-forge product, economics, integrations, API
-- bizops: BizOps tool, MCP tools, operational improvements
-- content: roundtable, dispatch, column generation pipelines
-- self_improvement: self-improvement analysis, outcomes, patterns
-- organization: org-level priorities, governance, go-to-market
-- auth: auth product, Better Auth, API key formats, middleware chain, auth-contract
-- product_strategy: product positioning, competitive landscape
-- compliance: legal, tax, compliance deadlines, regulatory
-- finance: financial metrics, costs, revenue, billing
-- operator_preferences: operator preferences, workflow choices
-- milestones: key dates, launches, completed phases
-- mcp_strategy: MCP protocol, OAuth, remote MCP, tool design
-
-Return ONLY a JSON array (no markdown):
-[
- { "operation": "ADD", "topic": "aegis", "fact": "specific fact", "confidence": 0.8 },
- { "operation": "UPDATE", "supersedes_id": 42, "topic": "auth", "fact": "updated fact", "confidence": 0.9 },
- { "operation": "DELETE", "target_id": 37, "reason": "no longer accurate" }
-]
-
-Return [] if nothing needs to change.`;
-
-export async function consolidateEpisodicToSemantic(
- db: D1Database,
- groqApiKey: string,
- groqModel: string,
- groqBaseUrl?: string,
- memoryBinding?: import('../../types.js').MemoryServiceBinding,
-): Promise {
- // High-water mark: only process episodes since last consolidation (not a rolling 24h window)
- const lastRun = await db.prepare(
- "SELECT received_at FROM web_events WHERE event_id = 'last_consolidation_at'"
- ).first<{ received_at: string }>();
- const since = lastRun?.received_at ?? new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString().replace('T', ' ').slice(0, 19);
-
- const result = await db.prepare(
- 'SELECT id, intent_class, channel, summary, outcome, cost FROM episodic_memory WHERE created_at > ? ORDER BY created_at DESC LIMIT 20'
- ).bind(since).all();
-
- const episodes = result.results as unknown as Array<{
- id: number;
- intent_class: string;
- channel: string;
- summary: string;
- outcome: string;
- cost: number;
- }>;
-
- // Guard: skip if not enough new signal
- if (episodes.length < 3) return;
-
- // Fetch existing active memory with IDs so the model can reference them for UPDATE/DELETE
- // Memory Worker is the sole knowledge store — D1 reads removed
- let existingMemoryList: Array<{ id: string | number; topic: string; fact: string }> = [];
- if (memoryBinding) {
- try {
- const fragments = await memoryBinding.recall('aegis', { limit: 40 });
- existingMemoryList = fragments.map(f => ({ id: f.id, topic: f.topic, fact: f.content }));
- } catch (err) {
- console.error('[consolidation] Memory Worker recall failed:', err instanceof Error ? err.message : String(err));
- }
- }
-
- const memoryContext = existingMemoryList.length > 0
- ? `\n\nExisting memory (reference by id for UPDATE/DELETE operations):\n${existingMemoryList.map(m => `- [id=${m.id}] [${m.topic}] ${m.fact}`).join('\n')}`
- : '';
-
- const userPrompt = `Recent agent episodes (since last consolidation):\n\n${episodes.map((e, i) =>
- `${i + 1}. [${e.intent_class}/${e.outcome}] ${e.summary}`
- ).join('\n')}${memoryContext}`;
-
- let rawResponse: string;
- try {
- rawResponse = await askGroq(groqApiKey, groqModel, CONSOLIDATION_SYSTEM, userPrompt, groqBaseUrl);
- } catch {
- return; // Groq failure — skip silently, will retry next cron cycle
- }
-
- if (!rawResponse) return;
- const cleaned = rawResponse.replace(/```json\s*/g, '').replace(/```\s*/g, '').trim();
- let ops: Array<{
- operation: 'ADD' | 'UPDATE' | 'DELETE' | 'NOOP';
- topic?: string; fact?: string; confidence?: number;
- supersedes_id?: string | number;
- target_id?: string | number; reason?: string;
- }>;
- try {
- ops = JSON.parse(cleaned);
- if (!Array.isArray(ops)) return;
- } catch {
- return;
- }
-
- for (const op of ops.slice(0, 3)) { // Hard cap: max 3 per run
- switch (op.operation) {
- case 'ADD': {
- if (!op.topic || !op.fact) continue;
- if (op.fact.length < 30) continue;
- if (!/\d/.test(op.fact) && !/[A-Z][a-z]/.test(op.fact.slice(1))) continue;
- if (!memoryBinding) continue;
- await memoryBinding.store('aegis', [{ content: op.fact, topic: op.topic, confidence: op.confidence ?? 0.7, source: 'episodic_consolidation' }]);
- console.log(`[consolidation] ADD [${normalizeTopic(op.topic)}] "${op.fact.slice(0, 80)}" (conf:${op.confidence ?? 0.7})`);
- // Phase 2: Extract knowledge graph nodes + edges from new fact
- try {
- const nodeIds = await extractNodes(db, op.fact, normalizeTopic(op.topic));
- if (nodeIds.length >= 2) {
- await createEdges(db, nodeIds);
- }
- } catch (err) {
- console.warn('[consolidation] Graph extraction failed:', err instanceof Error ? err.message : String(err));
- }
- break;
- }
- case 'UPDATE': {
- if (!op.supersedes_id || !op.topic || !op.fact) continue;
- if (op.fact.length < 30) continue;
- if (!/\d/.test(op.fact) && !/[A-Z][a-z]/.test(op.fact.slice(1))) continue;
- if (memoryBinding) {
- // Forget old → store new
- await memoryBinding.forget('aegis', { ids: [String(op.supersedes_id)] });
- await memoryBinding.store('aegis', [{ content: op.fact, topic: op.topic, confidence: op.confidence ?? 0.8, source: 'episodic_consolidation' }]);
- } else {
- await db.prepare(
- "UPDATE memory_entries SET valid_until = datetime('now'), updated_at = datetime('now') WHERE id = ? AND valid_until IS NULL"
- ).bind(op.supersedes_id).run();
- const topic = normalizeTopic(op.topic);
- const hash = factHash(topic, op.fact);
- const insertResult = await db.prepare(
- 'INSERT INTO memory_entries (topic, fact, fact_hash, confidence, source, superseded_by) VALUES (?, ?, ?, ?, ?, ?)'
- ).bind(topic, op.fact, hash, op.confidence ?? 0.8, 'episodic_consolidation', op.supersedes_id).run();
- if (insertResult.meta.last_row_id) {
- await db.prepare(
- 'UPDATE memory_entries SET superseded_by = ? WHERE id = ?'
- ).bind(insertResult.meta.last_row_id, op.supersedes_id).run();
- }
- }
- console.log(`[consolidation] UPDATE ${op.supersedes_id} → [${normalizeTopic(op.topic)}] "${op.fact.slice(0, 80)}" (conf:${op.confidence ?? 0.8})`);
- // Phase 2: Extract knowledge graph nodes + edges from updated fact
- try {
- const nodeIds = await extractNodes(db, op.fact, normalizeTopic(op.topic));
- if (nodeIds.length >= 2) {
- await createEdges(db, nodeIds);
- }
- } catch (err) {
- console.warn('[consolidation] Graph extraction failed:', err instanceof Error ? err.message : String(err));
- }
- break;
- }
- case 'DELETE': {
- if (!op.target_id) continue;
- if (memoryBinding) {
- await memoryBinding.forget('aegis', { ids: [String(op.target_id)] });
- } else {
- await db.prepare(
- "UPDATE memory_entries SET valid_until = datetime('now'), updated_at = datetime('now') WHERE id = ? AND valid_until IS NULL"
- ).bind(op.target_id).run();
- }
- console.log(`[consolidation] Soft-deleted memory ${op.target_id}: ${op.reason ?? 'no reason'}`);
- break;
- }
- // NOOP — skip
- }
- }
-
- // Advance the high-water mark — these episodes won't be re-processed
- await db.prepare(
- "INSERT OR REPLACE INTO web_events (event_id, received_at) VALUES ('last_consolidation_at', datetime('now'))"
- ).run();
-}
+import { askGroq } from '../../groq.js';
+import { extractNodes, createEdges } from './graph.js';
+import { writeDreamFact } from './dream-write.js';
+import type { WikiClientEnv } from '../../wiki/client.js';
+
+// ─── Memory Consolidation ───────────────────────────────────
+// Redesigned for the #457 wiki unification (aegis-oss#78): writes go to
+// the wiki's `dreams` scope instead of the memory-worker fragment store.
+// Dreams are additive, provisional pages (confidence: drifting) with their
+// own lifecycle rules (promote on corroboration, archive if stale) — that
+// supersedes the old ADD/UPDATE/DELETE-by-id fragment model, which relied
+// on reading back an existing fragment list to pick a target id. There is
+// no equivalent "patch this specific prior fact" operation for wiki pages
+// written this way; letting the dreams lifecycle handle staleness is the
+// same tradeoff PRISM's synthesis.ts already makes in production.
+
+const CONSOLIDATION_SYSTEM = `You are AEGIS memory consolidation. Analyze recent agent episodes and extract genuinely new, specific facts worth remembering long-term.
+
+Rules:
+1. Every fact MUST contain at least one specific detail: a name, date, number, ID, URL, or version. Never write vague observations.
+2. Max 3 facts per run. Quality over quantity.
+3. Good facts: "Delaware PBC franchise tax filed 2026-03-03, 2 days late" or "BizOps dashboard_summary tool fails when org has no projects (undefined.id)".
+4. Bad facts: "Financial metrics are important" or "Document gaps exist and require attention".
+5. If episodes contain nothing worth remembering, return [].
+
+Return ONLY a JSON array (no markdown):
+[
+ { "topic": "aegis", "fact": "specific fact", "confidence": 0.8 }
+]
+
+Return [] if nothing needs to change.`;
+
+export async function consolidateEpisodicToSemantic(
+ db: D1Database,
+ groqApiKey: string,
+ groqModel: string,
+ groqBaseUrl?: string,
+ wikiEnv?: WikiClientEnv,
+): Promise {
+ if (!wikiEnv?.wikiBinding || !wikiEnv.wikiToken) return;
+
+ // High-water mark: only process episodes since last consolidation (not a rolling 24h window)
+ const lastRun = await db.prepare(
+ "SELECT received_at FROM web_events WHERE event_id = 'last_consolidation_at'"
+ ).first<{ received_at: string }>();
+ const since = lastRun?.received_at ?? new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString().replace('T', ' ').slice(0, 19);
+
+ const result = await db.prepare(
+ 'SELECT id, intent_class, channel, summary, outcome, cost FROM episodic_memory WHERE created_at > ? ORDER BY created_at DESC LIMIT 20'
+ ).bind(since).all();
+
+ const episodes = result.results as unknown as Array<{
+ id: number;
+ intent_class: string;
+ channel: string;
+ summary: string;
+ outcome: string;
+ cost: number;
+ }>;
+
+ // Guard: skip if not enough new signal
+ if (episodes.length < 3) return;
+
+ const userPrompt = `Recent agent episodes (since last consolidation):\n\n${episodes.map((e, i) =>
+ `${i + 1}. [${e.intent_class}/${e.outcome}] ${e.summary}`
+ ).join('\n')}`;
+
+ let rawResponse: string;
+ try {
+ rawResponse = await askGroq(groqApiKey, groqModel, CONSOLIDATION_SYSTEM, userPrompt, groqBaseUrl);
+ } catch {
+ return; // Groq failure — skip silently, will retry next cron cycle
+ }
+
+ if (!rawResponse) return;
+ const cleaned = rawResponse.replace(/```json\s*/g, '').replace(/```\s*/g, '').trim();
+ let facts: Array<{ topic?: string; fact?: string; confidence?: number }>;
+ try {
+ facts = JSON.parse(cleaned);
+ if (!Array.isArray(facts)) return;
+ } catch {
+ return;
+ }
+
+ for (const item of facts.slice(0, 3)) { // Hard cap: max 3 per run
+ if (!item.fact || item.fact.length < 30) continue;
+ if (!/\d/.test(item.fact) && !/[A-Z][a-z]/.test(item.fact.slice(1))) continue;
+
+ const topic = item.topic || 'aegis';
+ const confidence = item.confidence ?? 0.7;
+ await writeDreamFact(wikiEnv, { fact: item.fact, confidence, source: 'episodic_consolidation' });
+ console.log(`[consolidation] Wrote dream fact [${topic}] "${item.fact.slice(0, 80)}" (conf:${confidence})`);
+
+ // Knowledge graph extraction — decoupled from storage backend, keep as-is
+ try {
+ const nodeIds = await extractNodes(db, item.fact, topic);
+ if (nodeIds.length >= 2) {
+ await createEdges(db, nodeIds);
+ }
+ } catch (err) {
+ console.warn('[consolidation] Graph extraction failed:', err instanceof Error ? err.message : String(err));
+ }
+ }
+
+ // Advance the high-water mark — these episodes won't be re-processed
+ await db.prepare(
+ "INSERT OR REPLACE INTO web_events (event_id, received_at) VALUES ('last_consolidation_at', datetime('now'))"
+ ).run();
+}
diff --git a/web/src/kernel/memory/dream-write.ts b/web/src/kernel/memory/dream-write.ts
new file mode 100644
index 0000000..c3c1d74
--- /dev/null
+++ b/web/src/kernel/memory/dream-write.ts
@@ -0,0 +1,63 @@
+// ─── Dream-scope fact writer (aegis#684 / aegis-oss#78) ────────
+// Writes a fact into the wiki's `dreams` scope instead of the legacy
+// memory-worker fragment store. Mirrors the pattern the daemon's
+// synthesis.ts (PRISM) already proves in production: one wiki page per
+// fact, scope: dreams, type: synthesis, confidence: drifting, unique
+// timestamped slug — no merge/lint conflicts, subject to the same dreams
+// lifecycle rules (archive if uncorroborated).
+//
+// Never throws — write failures log and fall through, matching the
+// grounding-layer invariant that non-critical writes must not block the
+// caller's scheduled task.
+
+import { writePage } from '../../wiki/client.js';
+import type { WikiClientEnv } from '../../wiki/client.js';
+
+export interface DreamFactInput {
+ fact: string;
+ confidence: number;
+ source: string;
+}
+
+function slugify(value: string): string {
+ return value
+ .toLowerCase()
+ .replace(/[^a-z0-9]+/g, '-')
+ .replace(/^-|-$/g, '');
+}
+
+export async function writeDreamFact(env: WikiClientEnv, input: DreamFactInput): Promise {
+ if (!env.wikiBinding || !env.wikiToken) return;
+
+ const { fact, confidence, source } = input;
+ const sourceSlug = slugify(source);
+ const slug = `dream-${sourceSlug}-${Date.now()}`;
+
+ try {
+ await writePage(env, {
+ slug,
+ scope: 'dreams',
+ type: 'synthesis',
+ title: `${source}: ${fact.slice(0, 60)}`,
+ summary: fact.slice(0, 200),
+ body: [
+ '## Fact',
+ '',
+ fact,
+ '',
+ '## Confidence',
+ '',
+ `${(confidence * 100).toFixed(0)}%`,
+ '',
+ '## Source',
+ '',
+ source,
+ ].join('\n'),
+ confidence: 'drifting',
+ canonical: false,
+ sources: [{ type: 'core_scheduled_task', ref: source, verified_date: new Date().toISOString().slice(0, 10) }],
+ });
+ } catch (err) {
+ console.warn(`[dream-write] Failed to write dream page for source "${source}":`, err instanceof Error ? err.message : String(err));
+ }
+}
diff --git a/web/src/kernel/memory/graph.ts b/web/src/kernel/memory/graph.ts
index e068072..1ba2821 100755
--- a/web/src/kernel/memory/graph.ts
+++ b/web/src/kernel/memory/graph.ts
@@ -9,7 +9,18 @@
// ─── Node Type Classification ────────────────────────────────────────────────
-import { WasmGraph } from '@stackbilt/wasm-core';
+// Lazy import: @stackbilt/wasm-core calls wasm.__wbindgen_start() at module
+// init time, which fails CF Workers validation (wasm is a Module, not an
+// instantiated Instance, during the validation pass). Dynamic import defers
+// initialization until the function body runs under the actual CF runtime.
+let _WasmGraph: typeof import('@stackbilt/wasm-core').WasmGraph | null = null;
+async function getWasmGraph(): Promise {
+ if (!_WasmGraph) {
+ const mod = await import('@stackbilt/wasm-core');
+ _WasmGraph = mod.WasmGraph;
+ }
+ return _WasmGraph;
+}
import type { NodeType, SourceSystem } from '../../schema-enums.js';
export type { SourceSystem } from '../../schema-enums.js';
@@ -216,6 +227,7 @@ export async function activateGraph(
if (nodesResult.results.length === 0) return [];
+ const WasmGraph = await getWasmGraph();
const graph = WasmGraph.fromSnapshotArrays(nodesResult.results, edgesResult.results);
try {
const raw = graph.spreadActivation(query, hops, 10);
diff --git a/web/src/kernel/scheduled/consolidation.ts b/web/src/kernel/scheduled/consolidation.ts
index 069d4bc..13ee267 100755
--- a/web/src/kernel/scheduled/consolidation.ts
+++ b/web/src/kernel/scheduled/consolidation.ts
@@ -1,7 +1,6 @@
import { type EdgeEnv } from '../dispatch.js';
-import { consolidateEpisodicToSemantic, maintainProcedures, getAllProcedures, PROCEDURE_MIN_SUCCESSES, PROCEDURE_MIN_SUCCESS_RATE } from '../memory/index.js';
+import { consolidateEpisodicToSemantic, maintainProcedures } from '../memory/index.js';
import { garbageCollectTools, promoteHighUsageTools } from '../dynamic-tools.js';
-import { publishInsight, type InsightType } from '../memory/insights.js';
import { pruneMemory } from '../memory-adapter.js';
import { runCrossDomainSynthesis } from '../memory/synthesis.js';
import { maintainNarratives, detectStaleNarratives, precomputeCognitiveState, pruneNarratives, getCognitiveState, type ProductPortfolioEntry } from '../cognition.js';
@@ -13,7 +12,10 @@ import { McpClient } from '../../mcp-client.js';
import { operatorConfig } from '../../operator/index.js';
export async function runMemoryConsolidation(env: EdgeEnv): Promise {
- await consolidateEpisodicToSemantic(env.db, env.groqApiKey, env.groqModel, env.groqBaseUrl, env.memoryBinding);
+ await consolidateEpisodicToSemantic(env.db, env.groqApiKey, env.groqModel, env.groqBaseUrl, {
+ wikiBinding: env.wikiBinding,
+ wikiToken: env.wikiToken,
+ });
if (env.memoryBinding) {
await pruneMemory(env.memoryBinding, env.db);
}
@@ -49,9 +51,6 @@ export async function runMemoryConsolidation(env: EdgeEnv): Promise {
// Update active_context block from freshly computed CognitiveState
await refreshActiveContextBlock(env.db);
-
- // ─── CRIX Phase 2c: Publish insights from procedural + semantic memory ───
- await publishInsightsFromMemory(env);
}
export async function fetchProductPortfolio(env: EdgeEnv): Promise {
@@ -80,102 +79,6 @@ export async function fetchProductPortfolio(env: EdgeEnv): Promise {
- let published = 0;
-
- // Source 1: Learned procedures — patterns with ≥70% success rate and 3+ successes
- try {
- const procedures = await getAllProcedures(env.db);
- for (const proc of procedures) {
- if (published >= INSIGHT_RATE_LIMIT) break;
- if (proc.status !== 'learned') continue;
- if (proc.success_count < PROCEDURE_MIN_SUCCESSES) continue;
-
- const successRate = proc.success_count / (proc.success_count + proc.fail_count);
- if (successRate < PROCEDURE_MIN_SUCCESS_RATE) continue;
-
- // Already published check: handled by publishInsight() fact hash gate
- 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'}`;
-
- const result = await publishInsight(env.db, {
- fact,
- insight_type: 'pattern',
- origin_repo: 'aegis',
- keywords: [proc.task_pattern, proc.executor, 'routing', 'procedure'],
- confidence: Math.min(0.95, 0.75 + successRate * 0.2),
- }, env.memoryBinding);
-
- if (result.published) {
- published++;
- console.log(`[crix] Published procedure insight: ${proc.task_pattern}`);
- }
- }
- } catch (err) {
- console.warn('[crix] Procedure scan failed:', err instanceof Error ? err.message : String(err));
- }
-
- // Source 2: High-confidence memory entries from the last 24h via Memory Worker
- // Look for entries tagged with bug/perf/arch topics that could be cross-repo insights
- if (env.memoryBinding) {
- try {
- const INSIGHT_TOPICS = new Map([
- ['bug_signature', 'pattern'],
- ['perf_pattern', 'heuristic'],
- ['arch_improvement', 'principle'],
- ]);
-
- const fragments = await env.memoryBinding.recall('aegis', { limit: 20 });
- // Filter to high-confidence entries suitable for cross-repo insight publishing
- const recentHighConf = fragments.filter(f => f.confidence >= 0.85).slice(0, 10);
-
- for (const entry of recentHighConf) {
- if (published >= INSIGHT_RATE_LIMIT) break;
-
- // Infer insight type from topic
- let insightType: InsightType = 'pattern';
- for (const [topicFragment, type] of INSIGHT_TOPICS) {
- if (entry.topic.includes(topicFragment)) {
- insightType = type;
- break;
- }
- }
-
- // Extract keywords from the fact text
- const keywords = entry.content.toLowerCase()
- .replace(/[^a-z0-9\s]/g, ' ')
- .split(/\s+/)
- .filter(w => w.length > 4)
- .slice(0, 8);
-
- if (keywords.length < 2) continue; // Not enough signal
-
- const result = await publishInsight(env.db, {
- fact: entry.content,
- insight_type: insightType,
- origin_repo: 'aegis',
- keywords,
- confidence: entry.confidence,
- }, env.memoryBinding);
-
- if (result.published) {
- published++;
- }
- }
- } catch (err) {
- console.warn('[crix] Semantic scan failed:', err instanceof Error ? err.message : String(err));
- }
- }
-
- if (published > 0) {
- console.log(`[crix] Published ${published} insights during consolidation cycle`);
- }
-}
-
// ─── Active Context Block Refresh ────────────────────────────
async function refreshActiveContextBlock(db: D1Database): Promise {
diff --git a/web/src/version.ts b/web/src/version.ts
index ac4f50a..c40f136 100755
--- a/web/src/version.ts
+++ b/web/src/version.ts
@@ -1,3 +1,3 @@
// Semantic version - bump on each deploy
// Used in /health endpoint and changelog tracking
-export const VERSION = '0.8.0';
+export const VERSION = '0.8.6';
diff --git a/web/tests/memory.test.ts b/web/tests/memory.test.ts
index ade31c5..b8c1887 100755
--- a/web/tests/memory.test.ts
+++ b/web/tests/memory.test.ts
@@ -1174,8 +1174,11 @@ describe('insights.ts', () => {
// consolidation.ts
// ════════════════════════════════════════════════════════════════
-// consolidateEpisodicToSemantic depends on askGroq, so we test
-// the guards and parsing logic by controlling the DB mock returns.
+// consolidateEpisodicToSemantic depends on askGroq + writeDreamFact (wiki),
+// so we test the guards and parsing logic by controlling the DB mock
+// returns and mocking the dream-write module. Redesigned for the #457
+// wiki unification (aegis-oss#78) — writes route through scope:dreams
+// wiki pages instead of the memory-worker fragment store.
import { consolidateEpisodicToSemantic } from '../src/kernel/memory/consolidation.js';
@@ -1187,12 +1190,29 @@ vi.mock('../src/groq.js', () => ({
import { askGroq } from '../src/groq.js';
const mockAskGroq = vi.mocked(askGroq);
+vi.mock('../src/kernel/memory/dream-write.js', () => ({
+ writeDreamFact: vi.fn(),
+}));
+
+import { writeDreamFact } from '../src/kernel/memory/dream-write.js';
+const mockWriteDreamFact = vi.mocked(writeDreamFact);
+
+const testWikiEnv = { wikiBinding: {} as any, wikiToken: 'test-token' };
+
describe('consolidation.ts', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('consolidateEpisodicToSemantic', () => {
+ it('skips when no wiki env is configured', async () => {
+ const db = createMockDb({});
+
+ await consolidateEpisodicToSemantic(db, 'fake-key', 'llama3');
+
+ expect(mockAskGroq).not.toHaveBeenCalled();
+ });
+
it('skips when fewer than 3 episodes since last run', async () => {
const db = createMockDb({
// last consolidation watermark
@@ -1203,7 +1223,7 @@ describe('consolidation.ts', () => {
]],
});
- await consolidateEpisodicToSemantic(db, 'fake-key', 'llama3');
+ await consolidateEpisodicToSemantic(db, 'fake-key', 'llama3', undefined, testWikiEnv);
expect(mockAskGroq).not.toHaveBeenCalled();
});
@@ -1218,59 +1238,20 @@ describe('consolidation.ts', () => {
const db = createMockDb({
firstResults: [null], // no watermark
- allResults: [
- episodes, // episodes query
- [], // existing memory
- ],
+ allResults: [episodes], // episodes query
runMeta: [{ changes: 1 }], // watermark update
});
- await consolidateEpisodicToSemantic(db, 'fake-key', 'llama3');
+ await consolidateEpisodicToSemantic(db, 'fake-key', 'llama3', undefined, testWikiEnv);
expect(mockAskGroq).toHaveBeenCalledOnce();
});
- it('processes ADD operations from Groq response', async () => {
- const addOps = JSON.stringify([
- { operation: 'ADD', topic: 'aegis', fact: 'Version 1.30.1 deployed successfully on 2026-03-10', confidence: 0.85 },
+ it('writes a dream page for genuine facts from Groq response', async () => {
+ const facts = JSON.stringify([
+ { topic: 'aegis', fact: 'Version 1.30.1 deployed successfully on 2026-03-10', confidence: 0.85 },
]);
- mockAskGroq.mockResolvedValue(addOps);
-
- const episodes = Array.from({ length: 5 }, (_, i) => ({
- id: i + 1, intent_class: 'chat', channel: 'web',
- summary: `Episode ${i}`, outcome: 'success', cost: 0.01,
- }));
-
- const db = createMockDb({
- firstResults: [
- null, // watermark
- ],
- allResults: [
- episodes, // episodes query
- ],
- runMeta: [
- { changes: 1 }, // watermark update
- ],
- });
-
- // ADD operations require memoryBinding (source uses memoryBinding.store)
- const mockStore = vi.fn().mockResolvedValue(undefined);
- const mockRecallFn = vi.fn().mockResolvedValue([]);
- const memBinding = { store: mockStore, recall: mockRecallFn, forget: vi.fn(), health: vi.fn() };
-
- await consolidateEpisodicToSemantic(db, 'fake-key', 'llama3', undefined, memBinding as any);
-
- // Should have stored via memoryBinding
- expect(mockStore).toHaveBeenCalledWith('aegis', expect.arrayContaining([
- expect.objectContaining({ content: expect.stringContaining('Version 1.30.1'), topic: 'aegis' }),
- ]));
- });
-
- it('processes DELETE operations from Groq response', async () => {
- const deleteOps = JSON.stringify([
- { operation: 'DELETE', target_id: 42, reason: 'obsolete' },
- ]);
- mockAskGroq.mockResolvedValue(deleteOps);
+ mockAskGroq.mockResolvedValue(facts);
const episodes = Array.from({ length: 5 }, (_, i) => ({
id: i + 1, intent_class: 'chat', channel: 'web',
@@ -1279,27 +1260,23 @@ describe('consolidation.ts', () => {
const db = createMockDb({
firstResults: [null], // watermark
- allResults: [
- episodes,
- [], // existing memory
- ],
- runMeta: [
- { changes: 1 }, // soft delete
- { changes: 1 }, // watermark
- ],
+ allResults: [episodes], // episodes query
+ runMeta: [{ changes: 1 }], // watermark update
});
- await consolidateEpisodicToSemantic(db, 'fake-key', 'llama3');
+ await consolidateEpisodicToSemantic(db, 'fake-key', 'llama3', undefined, testWikiEnv);
- const deleteQuery = db._queries.find(q => q.sql.includes('valid_until') && q.sql.includes('UPDATE'));
- expect(deleteQuery).toBeDefined();
+ expect(mockWriteDreamFact).toHaveBeenCalledWith(testWikiEnv, expect.objectContaining({
+ fact: expect.stringContaining('Version 1.30.1'),
+ source: 'episodic_consolidation',
+ }));
});
- it('skips ADD operations with short facts (< 30 chars)', async () => {
- const ops = JSON.stringify([
- { operation: 'ADD', topic: 'aegis', fact: 'Too short', confidence: 0.8 },
+ it('skips facts with short text (< 30 chars)', async () => {
+ const facts = JSON.stringify([
+ { topic: 'aegis', fact: 'Too short', confidence: 0.8 },
]);
- mockAskGroq.mockResolvedValue(ops);
+ mockAskGroq.mockResolvedValue(facts);
const episodes = Array.from({ length: 5 }, (_, i) => ({
id: i + 1, intent_class: 'chat', channel: 'web',
@@ -1308,15 +1285,13 @@ describe('consolidation.ts', () => {
const db = createMockDb({
firstResults: [null],
- allResults: [episodes, []],
- runMeta: [{ changes: 1 }], // only watermark
+ allResults: [episodes],
+ runMeta: [{ changes: 1 }], // watermark only
});
- await consolidateEpisodicToSemantic(db, 'fake-key', 'llama3');
+ await consolidateEpisodicToSemantic(db, 'fake-key', 'llama3', undefined, testWikiEnv);
- // Should NOT have inserted any memory (short fact filtered)
- const insertQuery = db._queries.find(q => q.sql.includes('INSERT INTO memory_entries'));
- expect(insertQuery).toBeUndefined();
+ expect(mockWriteDreamFact).not.toHaveBeenCalled();
});
it('handles malformed Groq response gracefully', async () => {
@@ -1329,55 +1304,40 @@ describe('consolidation.ts', () => {
const db = createMockDb({
firstResults: [null],
- allResults: [episodes, []],
+ allResults: [episodes],
});
// Should not throw
await expect(
- consolidateEpisodicToSemantic(db, 'fake-key', 'llama3')
+ consolidateEpisodicToSemantic(db, 'fake-key', 'llama3', undefined, testWikiEnv)
).resolves.not.toThrow();
+
+ expect(mockWriteDreamFact).not.toHaveBeenCalled();
});
- it('caps operations at 3 per run', async () => {
- const ops = JSON.stringify([
- { operation: 'ADD', topic: 'a', fact: 'Fact about system A with version 1.0 deployed', confidence: 0.8 },
- { operation: 'ADD', topic: 'b', fact: 'Fact about system B with version 2.0 deployed', confidence: 0.8 },
- { operation: 'ADD', topic: 'c', fact: 'Fact about system C with version 3.0 deployed', confidence: 0.8 },
- { operation: 'ADD', topic: 'd', fact: 'Fact about system D with version 4.0 should be skipped', confidence: 0.8 },
+ it('caps facts at 3 per run', async () => {
+ const facts = JSON.stringify([
+ { topic: 'a', fact: 'Fact about system A with version 1.0 deployed', confidence: 0.8 },
+ { topic: 'b', fact: 'Fact about system B with version 2.0 deployed', confidence: 0.8 },
+ { topic: 'c', fact: 'Fact about system C with version 3.0 deployed', confidence: 0.8 },
+ { topic: 'd', fact: 'Fact about system D with version 4.0 should be skipped', confidence: 0.8 },
]);
- mockAskGroq.mockResolvedValue(ops);
+ mockAskGroq.mockResolvedValue(facts);
const episodes = Array.from({ length: 5 }, (_, i) => ({
id: i + 1, intent_class: 'chat', channel: 'web',
summary: `Episode ${i}`, outcome: 'success', cost: 0.01,
}));
- // We need enough mock results for 3 recordMemory calls
const db = createMockDb({
- firstResults: [
- null, // watermark
- null, null, null, // 3x recordMemory phase 1 (no hash dup)
- ],
- allResults: [
- episodes,
- [], // existing memory
- [], [], [], // 3x recordMemory phase 2
- ],
- runMeta: [
- { changes: 1, last_row_id: 50 },
- { changes: 1, last_row_id: 51 },
- { changes: 1, last_row_id: 52 },
- { changes: 1 }, // watermark
- ],
+ firstResults: [null], // watermark
+ allResults: [episodes],
+ runMeta: [{ changes: 1 }], // watermark update
});
- await consolidateEpisodicToSemantic(db, 'fake-key', 'llama3');
+ await consolidateEpisodicToSemantic(db, 'fake-key', 'llama3', undefined, testWikiEnv);
- // Count INSERT queries for memory_entries (not watermark)
- const inserts = db._queries.filter(q =>
- q.sql.includes('INSERT INTO memory_entries')
- );
- expect(inserts.length).toBeLessThanOrEqual(3);
+ expect(mockWriteDreamFact).toHaveBeenCalledTimes(3);
});
});
});