@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,214 @@
|
|
|
1
|
+
import { createIntent, dispatch, type EdgeEnv } from '../dispatch.js';
|
|
2
|
+
import { getActiveGoals, touchGoal, recordGoalAction, getGoalActions, downgradeGoalAuthority, type AgentGoal } from '../memory/index.js';
|
|
3
|
+
|
|
4
|
+
// ─── Autonomous Goal Loop (#14, #28) ─────────────────────────
|
|
5
|
+
|
|
6
|
+
// Standing Orders: structured context for goal execution.
|
|
7
|
+
// Stored in agent_goals.context_json. Defines scope, guardrails,
|
|
8
|
+
// and escalation rules so goals operate with clear boundaries.
|
|
9
|
+
export interface GoalContext {
|
|
10
|
+
scope?: string[]; // What this goal IS authorized to do
|
|
11
|
+
guardrails?: string[]; // What this goal must NOT do
|
|
12
|
+
escalation?: string[]; // When to stop and flag for human attention
|
|
13
|
+
triggers?: string[]; // Additional trigger conditions beyond schedule
|
|
14
|
+
deadline?: string; // ISO date — auto-complete goal after this date
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Coerce string fields to string[] — context_json in the DB may have been written
|
|
18
|
+
// with a plain string instead of an array, which passes the optional-chain length
|
|
19
|
+
// guard but causes .map() to throw at runtime (aegis#585).
|
|
20
|
+
function normalizeContext(raw: GoalContext): GoalContext {
|
|
21
|
+
const toArr = (v: unknown): string[] | undefined => {
|
|
22
|
+
if (!v) return undefined;
|
|
23
|
+
if (Array.isArray(v)) return v.map(String);
|
|
24
|
+
if (typeof v === 'string') return [v];
|
|
25
|
+
return undefined;
|
|
26
|
+
};
|
|
27
|
+
return {
|
|
28
|
+
...raw,
|
|
29
|
+
scope: toArr(raw.scope),
|
|
30
|
+
guardrails: toArr(raw.guardrails),
|
|
31
|
+
escalation: toArr(raw.escalation),
|
|
32
|
+
triggers: toArr(raw.triggers),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function formatStandingOrders(ctx: GoalContext): string {
|
|
37
|
+
const sections: string[] = [];
|
|
38
|
+
|
|
39
|
+
if (ctx.scope?.length) {
|
|
40
|
+
sections.push(`Scope (what you MAY do):\n${ctx.scope.map(s => ` - ${s}`).join('\n')}`);
|
|
41
|
+
}
|
|
42
|
+
if (ctx.guardrails?.length) {
|
|
43
|
+
sections.push(`Guardrails (what you must NOT do):\n${ctx.guardrails.map(g => ` - ${g}`).join('\n')}`);
|
|
44
|
+
}
|
|
45
|
+
if (ctx.escalation?.length) {
|
|
46
|
+
sections.push(`Escalation (stop and flag if):\n${ctx.escalation.map(e => ` - ${e}`).join('\n')}`);
|
|
47
|
+
}
|
|
48
|
+
if (ctx.triggers?.length) {
|
|
49
|
+
sections.push(`Additional triggers:\n${ctx.triggers.map(t => ` - ${t}`).join('\n')}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return sections.length > 0
|
|
53
|
+
? `\nStanding Orders:\n${sections.join('\n')}`
|
|
54
|
+
: '';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const MAX_GOALS_PER_CYCLE = 3;
|
|
58
|
+
|
|
59
|
+
// Approved tools for auto_low execution — reversible, low-risk actions only
|
|
60
|
+
// NOTE: Only list tools that ACTUALLY EXIST. Phantom tools cause silent correctness failures
|
|
61
|
+
// where error messages pass the >50 char check and get recorded as "success".
|
|
62
|
+
export const AUTO_LOW_APPROVED_TOOLS: ReadonlySet<string> = new Set([
|
|
63
|
+
'record_memory_entry',
|
|
64
|
+
'resolve_agenda_item',
|
|
65
|
+
'add_agenda_item',
|
|
66
|
+
]);
|
|
67
|
+
|
|
68
|
+
export const AUTO_LOW_MAX_ACTIONS_PER_RUN = 3;
|
|
69
|
+
export const AUTO_LOW_FAILURE_THRESHOLD = 2; // consecutive failures before downgrade
|
|
70
|
+
|
|
71
|
+
export async function runGoalLoop(env: EdgeEnv): Promise<void> {
|
|
72
|
+
const goals = await getActiveGoals(env.db);
|
|
73
|
+
if (goals.length === 0) return;
|
|
74
|
+
|
|
75
|
+
const nowMs = Date.now();
|
|
76
|
+
const seen = new Set<string | number>();
|
|
77
|
+
// Parse next_run_at to Date for correct comparison — D1 stores 'YYYY-MM-DD HH:MM:SS'
|
|
78
|
+
// while JS toISOString() uses 'YYYY-MM-DDTHH:MM:SS.sssZ'. Lexicographic comparison
|
|
79
|
+
// breaks because space (ASCII 32) < T (ASCII 84), making every goal appear overdue.
|
|
80
|
+
const due = goals.filter(g => {
|
|
81
|
+
if (g.next_run_at === null) return true;
|
|
82
|
+
const nextRunMs = new Date(g.next_run_at + 'Z').getTime();
|
|
83
|
+
return nextRunMs <= nowMs;
|
|
84
|
+
})
|
|
85
|
+
.filter(g => { if (seen.has(g.id)) return false; seen.add(g.id); return true; })
|
|
86
|
+
.sort((a, b) => {
|
|
87
|
+
const aMs = a.next_run_at ? new Date(a.next_run_at + 'Z').getTime() : 0;
|
|
88
|
+
const bMs = b.next_run_at ? new Date(b.next_run_at + 'Z').getTime() : 0;
|
|
89
|
+
return aMs - bMs;
|
|
90
|
+
})
|
|
91
|
+
.slice(0, MAX_GOALS_PER_CYCLE);
|
|
92
|
+
if (due.length === 0) return;
|
|
93
|
+
|
|
94
|
+
console.log(`[goals] ${due.length} goal(s) due for execution (capped at ${MAX_GOALS_PER_CYCLE})`);
|
|
95
|
+
for (const goal of due) {
|
|
96
|
+
// Check consecutive failures for auto_low goals before running
|
|
97
|
+
if (goal.authority_level === 'auto_low') {
|
|
98
|
+
const recentActions = await getGoalActions(env.db, goal.id, AUTO_LOW_FAILURE_THRESHOLD);
|
|
99
|
+
const consecutiveFailures = recentActions.filter(a => a.outcome === 'failure').length;
|
|
100
|
+
if (consecutiveFailures >= AUTO_LOW_FAILURE_THRESHOLD) {
|
|
101
|
+
await downgradeGoalAuthority(env.db, goal.id, `${consecutiveFailures} consecutive failures`);
|
|
102
|
+
goal.authority_level = 'propose'; // reflect downgrade for this run
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
await runSingleGoal(env, goal);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function runSingleGoal(env: EdgeEnv, goal: AgentGoal): Promise<void> {
|
|
110
|
+
// Parse standing orders from context_json
|
|
111
|
+
let goalContext: GoalContext | null = null;
|
|
112
|
+
if (goal.context_json) {
|
|
113
|
+
try {
|
|
114
|
+
goalContext = normalizeContext(JSON.parse(goal.context_json) as GoalContext);
|
|
115
|
+
} catch {
|
|
116
|
+
console.warn(`[goals] Failed to parse context_json for "${goal.title}"`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Check deadline — auto-complete expired goals
|
|
121
|
+
if (goalContext?.deadline) {
|
|
122
|
+
const deadlineMs = new Date(goalContext.deadline).getTime();
|
|
123
|
+
if (!isNaN(deadlineMs) && Date.now() > deadlineMs) {
|
|
124
|
+
console.log(`[goals] Goal "${goal.title}" past deadline (${goalContext.deadline}), marking completed`);
|
|
125
|
+
await env.db.prepare("UPDATE agent_goals SET status = 'completed', completed_at = datetime('now') WHERE id = ?").bind(goal.id).run();
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const isAutoLow = goal.authority_level === 'auto_low';
|
|
131
|
+
|
|
132
|
+
const authorityInstructions = isAutoLow
|
|
133
|
+
? `Authority: auto_low — you may DIRECTLY execute these approved actions without human approval:
|
|
134
|
+
- record_memory_entry (record observations)
|
|
135
|
+
- resolve_agenda_item (close stale agenda items)
|
|
136
|
+
- add_agenda_item (create new agenda items)
|
|
137
|
+
- Read-only BizOps tools (compliance_items, compliance_status, document_status, dashboard_summary)
|
|
138
|
+
|
|
139
|
+
For any action NOT in this list, create a [PROPOSED ACTION] agenda item instead.
|
|
140
|
+
Maximum ${AUTO_LOW_MAX_ACTIONS_PER_RUN} autonomous actions per run.`
|
|
141
|
+
: 'Authority: propose — create [PROPOSED ACTION] agenda items, do not execute autonomously.';
|
|
142
|
+
|
|
143
|
+
const standingOrders = goalContext ? formatStandingOrders(goalContext) : '';
|
|
144
|
+
|
|
145
|
+
const prompt = `You are running the autonomous goal loop. Evaluate the following goal and take appropriate action.
|
|
146
|
+
|
|
147
|
+
**Goal #${goal.id}: ${goal.title}**
|
|
148
|
+
${goal.description ? `Description: ${goal.description}` : ''}
|
|
149
|
+
${authorityInstructions}${standingOrders}
|
|
150
|
+
Run count: ${goal.run_count}
|
|
151
|
+
|
|
152
|
+
Procedure:
|
|
153
|
+
1. Use BizOps tools to check current state relevant to this goal
|
|
154
|
+
2. Determine if action is needed
|
|
155
|
+
3. ${isAutoLow
|
|
156
|
+
? 'If yes and action is in approved list: execute it directly. Otherwise: create [PROPOSED ACTION] agenda item.'
|
|
157
|
+
: 'If yes: call add_agenda_item with "[PROPOSED ACTION]" prefix and full reasoning in the context field'}
|
|
158
|
+
4. If no: call record_memory_entry noting what was checked and that no action is needed
|
|
159
|
+
5. ${isAutoLow ? `Maximum ${AUTO_LOW_MAX_ACTIONS_PER_RUN} actions per run.` : 'One proposed action max per run unless multiple urgent issues exist'}
|
|
160
|
+
|
|
161
|
+
Be concise. This is a scheduled check, not a full analysis.`;
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
const intent = createIntent(`goal-${goal.id}`, prompt, {
|
|
165
|
+
source: { channel: 'internal', threadId: `goal-${goal.id}` },
|
|
166
|
+
classified: 'goal_execution',
|
|
167
|
+
costCeiling: 'expensive',
|
|
168
|
+
raw: prompt,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const result = await dispatch(intent, env);
|
|
172
|
+
const partialMeta = result.meta as { partialFailure?: boolean; failedSubtasks?: number; subtasksPlanned?: number } | undefined;
|
|
173
|
+
const isPartial = partialMeta?.partialFailure;
|
|
174
|
+
// Partial failure is only a true failure if ALL subtasks failed or the response is empty.
|
|
175
|
+
// If some subtasks succeeded and returned data, treat as success (the data is usable).
|
|
176
|
+
// BUT: error messages are NOT usable data — check for known failure patterns.
|
|
177
|
+
const errorPatterns = ['Tool unavailable', 'Method not found', 'MCP RPC error', 'Unknown tool', 'Memory Worker binding unavailable', 'Memory write failed', 'Cannot read properties of undefined'];
|
|
178
|
+
const looksLikeError = errorPatterns.some(p => result.text.includes(p));
|
|
179
|
+
// Clean result patterns indicate the goal achieved its purpose even if subtasks errored
|
|
180
|
+
const cleanPatterns = ['no overdue', 'all clear', 'compliance posture is clean', 'no deadlines within', 'no action needed', 'recording the check', 'recording clean'];
|
|
181
|
+
const looksClean = cleanPatterns.some(p => result.text.toLowerCase().includes(p));
|
|
182
|
+
const hasUsableData = result.text.length > 50 && (!looksLikeError || looksClean);
|
|
183
|
+
const allFailed = isPartial && (partialMeta?.failedSubtasks ?? 0) >= (partialMeta?.subtasksPlanned ?? 1);
|
|
184
|
+
// A clean result overrides partial failure — the goal loop got the answer it needed
|
|
185
|
+
const goalOutcome = (!looksClean && (allFailed || !hasUsableData)) ? 'failure' : 'success';
|
|
186
|
+
const desc = isPartial
|
|
187
|
+
? `${result.text.slice(0, 400)} [partial: ${partialMeta.failedSubtasks} subtask(s) failed]`
|
|
188
|
+
: result.text.slice(0, 500);
|
|
189
|
+
await recordGoalAction(env.db, goal.id, 'executed', desc, goalOutcome, {
|
|
190
|
+
autoExecuted: isAutoLow,
|
|
191
|
+
authorityLevel: goal.authority_level,
|
|
192
|
+
});
|
|
193
|
+
console.log(`[goals] goal "${goal.title}" ${goalOutcome} (${goal.authority_level}) — ${result.text.slice(0, 100)}`);
|
|
194
|
+
} catch (err) {
|
|
195
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
196
|
+
try {
|
|
197
|
+
await recordGoalAction(env.db, goal.id, 'skipped', `Error: ${msg}`, 'failure', {
|
|
198
|
+
autoExecuted: false,
|
|
199
|
+
authorityLevel: goal.authority_level,
|
|
200
|
+
});
|
|
201
|
+
} catch { /* don't let recording failure prevent touchGoal */ }
|
|
202
|
+
console.error(`[goals] goal "${goal.title}" failed:`, msg);
|
|
203
|
+
|
|
204
|
+
// Failure downgrade: auto_low goal that fails reverts to propose
|
|
205
|
+
if (isAutoLow) {
|
|
206
|
+
await downgradeGoalAuthority(env.db, goal.id, `Execution failure: ${msg.slice(0, 200)}`);
|
|
207
|
+
}
|
|
208
|
+
} finally {
|
|
209
|
+
// Always advance schedule — prevents runaway re-execution on persistent errors (#83)
|
|
210
|
+
await touchGoal(env.db, goal.id, goal.schedule_hours).catch(err =>
|
|
211
|
+
console.error(`[goals] touchGoal failed for "${goal.title}":`, err instanceof Error ? err.message : String(err))
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Task Governance Limits — safety checks before auto-creating cc_tasks
|
|
2
|
+
// Prevents runaway task creation by enforcing caps on active AEGIS tasks.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Check whether AEGIS is allowed to create another task.
|
|
6
|
+
* Returns { allowed: true } or { allowed: false, reason: string }.
|
|
7
|
+
*/
|
|
8
|
+
export async function checkTaskGovernanceLimits(
|
|
9
|
+
db: D1Database,
|
|
10
|
+
opts: { repo: string; title: string; category: string },
|
|
11
|
+
): Promise<{ allowed: true } | { allowed: false; reason: string }> {
|
|
12
|
+
// Per-repo cap: max 5 active AEGIS tasks per repo
|
|
13
|
+
const repoPending = await db.prepare(
|
|
14
|
+
`SELECT COUNT(*) as c FROM cc_tasks WHERE status IN ('pending', 'running') AND created_by LIKE 'aegis%' AND repo = ?`
|
|
15
|
+
).bind(opts.repo).first<{ c: number }>();
|
|
16
|
+
|
|
17
|
+
if (repoPending && repoPending.c >= 5) {
|
|
18
|
+
return { allowed: false, reason: `Per-repo cap reached (${repoPending.c}/5 active AEGIS tasks for ${opts.repo})` };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Global active cap: max 20 pending/running AEGIS tasks across all repos
|
|
22
|
+
// Completed tasks don't block new work — cap is self-regulating via consumption
|
|
23
|
+
const activeCount = await db.prepare(
|
|
24
|
+
`SELECT COUNT(*) as c FROM cc_tasks WHERE status IN ('pending', 'running') AND created_by LIKE 'aegis%'`
|
|
25
|
+
).first<{ c: number }>();
|
|
26
|
+
|
|
27
|
+
if (activeCount && activeCount.c >= 20) {
|
|
28
|
+
return { allowed: false, reason: `Active task cap reached (${activeCount.c}/20 pending/running AEGIS tasks)` };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Duplicate title detection: prevent creating tasks with identical titles that are active or completed
|
|
32
|
+
const duplicate = await db.prepare(
|
|
33
|
+
`SELECT COUNT(*) as c FROM cc_tasks WHERE title = ? AND status IN ('pending', 'running', 'completed')`
|
|
34
|
+
).bind(opts.title).first<{ c: number }>();
|
|
35
|
+
|
|
36
|
+
if (duplicate && duplicate.c > 0) {
|
|
37
|
+
return { allowed: false, reason: `Duplicate task: "${opts.title}" is already pending` };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return { allowed: true };
|
|
41
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { createIntent, dispatch, type EdgeEnv } from '../dispatch.js';
|
|
2
|
+
import { getActiveAgendaItems, getRecentHeartbeats, PROPOSED_ACTION_PREFIX } from '../memory/index.js';
|
|
3
|
+
import { type StaleHighItem } from './escalation.js';
|
|
4
|
+
|
|
5
|
+
export const ALERT_SEVERITIES = new Set(['medium', 'high', 'critical']);
|
|
6
|
+
|
|
7
|
+
/** Hours between heartbeat runs (directive: reduce from 1h to 6h) */
|
|
8
|
+
const HEARTBEAT_CADENCE_HOURS = 6;
|
|
9
|
+
|
|
10
|
+
/** Consecutive medium runs before severity decays to LOW (directive: 10+ → downgrade) */
|
|
11
|
+
const CHRONIC_MEDIUM_THRESHOLD = 10;
|
|
12
|
+
|
|
13
|
+
/** Once decayed to LOW, only re-surface weekly (runs at 6h cadence = 28 runs/week) */
|
|
14
|
+
const LOW_RESURFACE_INTERVAL = 28;
|
|
15
|
+
|
|
16
|
+
export type CheckStatus = 'ok' | 'warn' | 'alert';
|
|
17
|
+
export interface HeartbeatCheck { name: string; status: CheckStatus; detail: string }
|
|
18
|
+
|
|
19
|
+
export async function runHeartbeat(env: EdgeEnv, staleHighItems: StaleHighItem[] = []): Promise<void> {
|
|
20
|
+
// Cadence gate: only run every 6 hours (0, 6, 12, 18 UTC)
|
|
21
|
+
// Skip at 12 UTC — daily digest covers that slot
|
|
22
|
+
const hour = new Date().getUTCHours();
|
|
23
|
+
if (hour % HEARTBEAT_CADENCE_HOURS !== 0 || hour === 12) return;
|
|
24
|
+
|
|
25
|
+
// Load prior heartbeat history for trend context (#6)
|
|
26
|
+
const priorRuns = await getRecentHeartbeats(env.db, 12);
|
|
27
|
+
const priorChecksMap = new Map<string, CheckStatus[]>();
|
|
28
|
+
for (const run of priorRuns) {
|
|
29
|
+
const checks = JSON.parse(run.checks_json) as HeartbeatCheck[];
|
|
30
|
+
for (const c of checks) {
|
|
31
|
+
const history = priorChecksMap.get(c.name) ?? [];
|
|
32
|
+
history.push(c.status);
|
|
33
|
+
priorChecksMap.set(c.name, history);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const intent = createIntent('scheduled', 'heartbeat check', {
|
|
38
|
+
source: { channel: 'internal', threadId: 'scheduled' },
|
|
39
|
+
classified: 'heartbeat',
|
|
40
|
+
costCeiling: 'free',
|
|
41
|
+
});
|
|
42
|
+
const result = await dispatch(intent, env);
|
|
43
|
+
const meta = result.meta as { actionable?: boolean; severity?: string; checks?: unknown[] } | undefined;
|
|
44
|
+
|
|
45
|
+
const actionable = meta?.actionable ?? false;
|
|
46
|
+
const severity = meta?.severity ?? 'none';
|
|
47
|
+
const checks = (meta?.checks ?? []) as HeartbeatCheck[];
|
|
48
|
+
|
|
49
|
+
const checksJson = JSON.stringify(checks);
|
|
50
|
+
|
|
51
|
+
await env.db.prepare(
|
|
52
|
+
'INSERT INTO heartbeat_results (actionable, severity, summary, checks_json) VALUES (?, ?, ?, ?)'
|
|
53
|
+
).bind(
|
|
54
|
+
actionable ? 1 : 0,
|
|
55
|
+
severity,
|
|
56
|
+
result.text.slice(0, 500),
|
|
57
|
+
checksJson,
|
|
58
|
+
).run();
|
|
59
|
+
|
|
60
|
+
// Payload dedup: if checks are identical to the last run, suppress all email/digest writes.
|
|
61
|
+
// Only recording the result above (for trend history) and agenda sync still proceed.
|
|
62
|
+
const lastRun = priorRuns[0];
|
|
63
|
+
if (lastRun && lastRun.checks_json === checksJson) {
|
|
64
|
+
console.log('[heartbeat] Payload identical to previous run — suppressing email/digest');
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Classify each check as new / persisting / escalated / resolved (#6)
|
|
69
|
+
// emailableChecks: new + escalated (triggers alert email)
|
|
70
|
+
// agendaChecks: new + escalated + persisting (synced to agenda, but no re-email for persisters)
|
|
71
|
+
const { emailableChecks, agendaChecks, resolved } = classifyCheckDeltas(checks, priorChecksMap);
|
|
72
|
+
|
|
73
|
+
// Per-check email cooldown (24h) — prevents re-alerting when checks flap due to
|
|
74
|
+
// transient BizOps timeouts or data fluctuations resetting the history window
|
|
75
|
+
const ALERT_COOLDOWN_MS = 24 * 60 * 60 * 1000;
|
|
76
|
+
const dedupedEmailChecks: HeartbeatCheck[] = [];
|
|
77
|
+
for (const check of emailableChecks) {
|
|
78
|
+
const eventKey = `heartbeat_alert_${check.name}`;
|
|
79
|
+
const lastAlert = await env.db.prepare(
|
|
80
|
+
"SELECT received_at FROM web_events WHERE event_id = ?"
|
|
81
|
+
).bind(eventKey).first<{ received_at: string }>();
|
|
82
|
+
if (lastAlert && (Date.now() - new Date(lastAlert.received_at + 'Z').getTime()) < ALERT_COOLDOWN_MS) {
|
|
83
|
+
continue; // already alerted within 24h
|
|
84
|
+
}
|
|
85
|
+
dedupedEmailChecks.push(check);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
await syncChecksToAgenda(env.db, agendaChecks);
|
|
89
|
+
|
|
90
|
+
// Auto-resolve agenda items for checks that cleared
|
|
91
|
+
await resolveAgendaForChecks(env.db, resolved);
|
|
92
|
+
|
|
93
|
+
// Inject stale escalation items as heartbeat checks so they appear in the same email
|
|
94
|
+
if (staleHighItems.length > 0) {
|
|
95
|
+
for (const s of staleHighItems) {
|
|
96
|
+
const alreadyTracked = dedupedEmailChecks.some(c => c.name === `stale_agenda_${s.id}`);
|
|
97
|
+
if (!alreadyTracked) {
|
|
98
|
+
dedupedEmailChecks.push({
|
|
99
|
+
name: `stale_agenda_${s.id}`,
|
|
100
|
+
status: 'warn',
|
|
101
|
+
detail: `${s.item.slice(0, 120)} (${Math.floor(s.ageDays)}d stale)`,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Ensure severity covers escalation items even if LLM heartbeat was clean
|
|
108
|
+
const effectiveSeverity = dedupedEmailChecks.length > 0 && !ALERT_SEVERITIES.has(severity)
|
|
109
|
+
? 'medium'
|
|
110
|
+
: severity;
|
|
111
|
+
|
|
112
|
+
// All heartbeat alerts — write to digest_sections for daily digest consolidation.
|
|
113
|
+
// No standalone emails. Critical ARGUS events (CI failures, payment issues) handle
|
|
114
|
+
// real-time alerting; heartbeat checks are not time-sensitive enough for inbox spam.
|
|
115
|
+
if (dedupedEmailChecks.length > 0 && ALERT_SEVERITIES.has(effectiveSeverity)) {
|
|
116
|
+
const payload = JSON.stringify({
|
|
117
|
+
severity: effectiveSeverity,
|
|
118
|
+
checks: dedupedEmailChecks,
|
|
119
|
+
timestamp: new Date().toISOString(),
|
|
120
|
+
});
|
|
121
|
+
await env.db.prepare(
|
|
122
|
+
"INSERT INTO digest_sections (section, payload) VALUES ('health_check', ?)"
|
|
123
|
+
).bind(payload).run();
|
|
124
|
+
// Record per-check cooldown timestamps
|
|
125
|
+
for (const check of dedupedEmailChecks) {
|
|
126
|
+
const eventKey = `heartbeat_alert_${check.name}`;
|
|
127
|
+
await env.db.prepare(
|
|
128
|
+
"INSERT OR REPLACE INTO web_events (event_id, received_at) VALUES (?, datetime('now'))"
|
|
129
|
+
).bind(eventKey).run();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function classifyCheckDeltas(
|
|
135
|
+
current: HeartbeatCheck[],
|
|
136
|
+
priorMap: Map<string, CheckStatus[]>,
|
|
137
|
+
): { emailableChecks: HeartbeatCheck[]; agendaChecks: HeartbeatCheck[]; resolved: string[] } {
|
|
138
|
+
const emailableChecks: HeartbeatCheck[] = []; // new + escalated → triggers email
|
|
139
|
+
const agendaChecks: HeartbeatCheck[] = []; // new + escalated + persisting → agenda sync
|
|
140
|
+
const resolved: string[] = [];
|
|
141
|
+
|
|
142
|
+
for (const check of current) {
|
|
143
|
+
const prior = priorMap.get(check.name) ?? [];
|
|
144
|
+
const lastStatus = prior[0]; // most recent prior run
|
|
145
|
+
|
|
146
|
+
if (check.status === 'ok') {
|
|
147
|
+
// Was previously warn/alert → now ok = resolved
|
|
148
|
+
if (lastStatus && lastStatus !== 'ok') resolved.push(check.name);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!lastStatus || lastStatus === 'ok') {
|
|
153
|
+
// New issue — email + agenda
|
|
154
|
+
emailableChecks.push(check);
|
|
155
|
+
agendaChecks.push(check);
|
|
156
|
+
} else if (check.status === 'alert' && lastStatus === 'warn') {
|
|
157
|
+
// Escalated warn→alert — email + agenda
|
|
158
|
+
emailableChecks.push(check);
|
|
159
|
+
agendaChecks.push(check);
|
|
160
|
+
} else {
|
|
161
|
+
// Persisting — check for chronic medium decay before deciding re-surface cadence
|
|
162
|
+
const allPrior = [lastStatus, ...prior.slice(1)];
|
|
163
|
+
const consecutivePersist = allPrior.filter(s => s !== 'ok').length;
|
|
164
|
+
|
|
165
|
+
// Severity decay: medium items persisting 10+ runs are accepted risks.
|
|
166
|
+
// Downgrade to LOW and only re-surface weekly (~28 runs at 6h cadence).
|
|
167
|
+
if (check.status === 'warn' && consecutivePersist >= CHRONIC_MEDIUM_THRESHOLD) {
|
|
168
|
+
if (consecutivePersist > 0 && consecutivePersist % LOW_RESURFACE_INTERVAL === 0) {
|
|
169
|
+
agendaChecks.push({ ...check, detail: `${check.detail} (known risk, weekly check-in — ${consecutivePersist + 1} runs)` });
|
|
170
|
+
}
|
|
171
|
+
// Otherwise fully suppress — operator is aware
|
|
172
|
+
} else if (consecutivePersist > 0 && consecutivePersist % 12 === 0) {
|
|
173
|
+
// Normal persisting items re-surface every 12 runs (~3 days at 6h cadence)
|
|
174
|
+
agendaChecks.push({ ...check, detail: `${check.detail} (persisting ${consecutivePersist + 1} runs)` });
|
|
175
|
+
}
|
|
176
|
+
// Otherwise suppress entirely — already on agenda
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return { emailableChecks, agendaChecks, resolved };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export async function syncChecksToAgenda(db: D1Database, checks: HeartbeatCheck[]): Promise<void> {
|
|
184
|
+
for (const check of checks) {
|
|
185
|
+
const existing = await db.prepare(
|
|
186
|
+
"SELECT id FROM agent_agenda WHERE status = 'active' AND item LIKE ?"
|
|
187
|
+
).bind(`%${check.name}%`).first();
|
|
188
|
+
if (existing) continue;
|
|
189
|
+
|
|
190
|
+
// Respect dismissed items — don't re-create if dismissed within the last 7 days.
|
|
191
|
+
// This prevents agenda churn where a known structural condition (e.g. low separation
|
|
192
|
+
// scores) gets re-created every heartbeat cycle after the operator dismisses it.
|
|
193
|
+
const recentlyDismissed = await db.prepare(
|
|
194
|
+
"SELECT id FROM agent_agenda WHERE status = 'dismissed' AND item LIKE ? AND resolved_at > datetime('now', '-7 days')"
|
|
195
|
+
).bind(`%${check.name}%`).first();
|
|
196
|
+
if (recentlyDismissed) continue;
|
|
197
|
+
|
|
198
|
+
// Alert-level checks become proposed actions — AEGIS has a specific recommended step.
|
|
199
|
+
// Warn-level checks are regular agenda items (flag only, no clear action yet).
|
|
200
|
+
const isProposed = check.status === 'alert';
|
|
201
|
+
const item = isProposed
|
|
202
|
+
? `${PROPOSED_ACTION_PREFIX} ${check.name}: ${check.detail}`
|
|
203
|
+
: `${check.name}: ${check.detail}`;
|
|
204
|
+
const context = isProposed
|
|
205
|
+
? `Auto-detected by heartbeat. Review in BizOps and resolve the underlying issue, then mark this item done.`
|
|
206
|
+
: 'Auto-detected by heartbeat';
|
|
207
|
+
|
|
208
|
+
await db.prepare(
|
|
209
|
+
'INSERT INTO agent_agenda (item, context, priority) VALUES (?, ?, ?)'
|
|
210
|
+
).bind(item, context, isProposed ? 'high' : 'medium').run();
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export async function resolveAgendaForChecks(db: D1Database, checkNames: string[]): Promise<void> {
|
|
215
|
+
for (const name of checkNames) {
|
|
216
|
+
await db.prepare(
|
|
217
|
+
"UPDATE agent_agenda SET status = 'done', resolved_at = datetime('now') WHERE status = 'active' AND item LIKE ?"
|
|
218
|
+
).bind(`%${name}%`).run();
|
|
219
|
+
}
|
|
220
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// --- Agent Inbox Processor ---
|
|
2
|
+
// Runs every hour in the heartbeat phase. Reads AEGIS's unread
|
|
3
|
+
// inbox messages, processes them by type, acks, and responds.
|
|
4
|
+
// Zero LLM cost -- all processing is D1 reads/writes + memory lookups.
|
|
5
|
+
// For context_request: pull relevant memory and post a reply.
|
|
6
|
+
// For task_proposal: create agenda item for operator approval.
|
|
7
|
+
// For alert: escalate to agenda.
|
|
8
|
+
// For status_update/message: ack and optionally record to memory.
|
|
9
|
+
|
|
10
|
+
import { type EdgeEnv } from '../dispatch.js';
|
|
11
|
+
import { searchMemoryByKeywords, recordMemory } from '../memory-adapter.js';
|
|
12
|
+
import { addAgendaItem } from '../memory/index.js';
|
|
13
|
+
|
|
14
|
+
interface InboxMessage {
|
|
15
|
+
id: string;
|
|
16
|
+
sender: string;
|
|
17
|
+
recipient: string;
|
|
18
|
+
channel: string;
|
|
19
|
+
msg_type: string;
|
|
20
|
+
subject: string;
|
|
21
|
+
body: string;
|
|
22
|
+
created_at: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function ackMessage(db: D1Database, id: string): Promise<void> {
|
|
26
|
+
await db.prepare("UPDATE agent_inbox SET acked_at = datetime('now') WHERE id = ?").bind(id).run();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function postReply(db: D1Database, sender: string, recipient: string, channel: string, msgType: string, subject: string, body: string): Promise<void> {
|
|
30
|
+
const id = crypto.randomUUID();
|
|
31
|
+
await db.prepare(`
|
|
32
|
+
INSERT INTO agent_inbox (id, sender, recipient, channel, msg_type, subject, body)
|
|
33
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
34
|
+
`).bind(id, sender, recipient, channel, msgType, subject, body).run();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function handleContextRequest(msg: InboxMessage, env: EdgeEnv): Promise<void> {
|
|
38
|
+
// Extract keywords from subject + body for memory search
|
|
39
|
+
const searchTerms = `${msg.subject} ${msg.body}`.slice(0, 500);
|
|
40
|
+
|
|
41
|
+
// Search semantic memory (requires Memory Worker binding)
|
|
42
|
+
const memories = env.memoryBinding
|
|
43
|
+
? await searchMemoryByKeywords(env.memoryBinding, searchTerms, 15)
|
|
44
|
+
: [];
|
|
45
|
+
|
|
46
|
+
// Also check episodic memory for recent conversations on the topic
|
|
47
|
+
const episodes = await env.db.prepare(`
|
|
48
|
+
SELECT intent, classification, outcome, created_at
|
|
49
|
+
FROM episodic_memory
|
|
50
|
+
WHERE intent LIKE ? OR classification LIKE ?
|
|
51
|
+
ORDER BY created_at DESC LIMIT 10
|
|
52
|
+
`).bind(`%${msg.subject.split(' ')[0]}%`, `%${msg.subject.split(' ')[0]}%`).all();
|
|
53
|
+
|
|
54
|
+
// Build response
|
|
55
|
+
const memorySection = memories.length > 0
|
|
56
|
+
? memories.map(m => `- [${m.topic}] ${m.fact} (confidence: ${m.confidence})`).join('\n')
|
|
57
|
+
: '(no relevant memories found)';
|
|
58
|
+
|
|
59
|
+
const episodeSection = (episodes.results?.length ?? 0) > 0
|
|
60
|
+
? (episodes.results as Array<{ intent: string; classification: string; outcome: string; created_at: string }>)
|
|
61
|
+
.map(e => `- [${e.created_at}] ${e.classification}: ${e.intent?.slice(0, 100)}`)
|
|
62
|
+
.join('\n')
|
|
63
|
+
: '(no recent episodes)';
|
|
64
|
+
|
|
65
|
+
const response = `## Context: ${msg.subject}
|
|
66
|
+
|
|
67
|
+
### Semantic Memory (${memories.length} entries)
|
|
68
|
+
${memorySection}
|
|
69
|
+
|
|
70
|
+
### Recent Episodes (${episodes.results?.length ?? 0})
|
|
71
|
+
${episodeSection}
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
_Auto-generated by AEGIS inbox processor in response to context_request from ${msg.sender}_`;
|
|
75
|
+
|
|
76
|
+
await postReply(env.db, 'aegis', msg.sender, msg.channel, 'message', `Re: ${msg.subject}`, response);
|
|
77
|
+
await ackMessage(env.db, msg.id);
|
|
78
|
+
console.log(`[inbox] Replied to context_request from ${msg.sender}: ${msg.subject}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function handleTaskProposal(msg: InboxMessage, env: EdgeEnv): Promise<void> {
|
|
82
|
+
// Create agenda item for operator approval
|
|
83
|
+
await addAgendaItem(
|
|
84
|
+
env.db,
|
|
85
|
+
`[${msg.sender.toUpperCase()}] Task proposal: ${msg.subject}`,
|
|
86
|
+
`From ${msg.sender} via agent inbox:\n\n${msg.body}`,
|
|
87
|
+
'medium',
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
await postReply(env.db, 'aegis', msg.sender, msg.channel, 'message',
|
|
91
|
+
`Re: ${msg.subject}`,
|
|
92
|
+
`Task proposal received and added to operator agenda for review. Will update when the operator responds.`);
|
|
93
|
+
|
|
94
|
+
await ackMessage(env.db, msg.id);
|
|
95
|
+
console.log(`[inbox] Task proposal from ${msg.sender} -> agenda: ${msg.subject}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function handleAlert(msg: InboxMessage, env: EdgeEnv): Promise<void> {
|
|
99
|
+
// Alerts go directly to high-priority agenda
|
|
100
|
+
await addAgendaItem(
|
|
101
|
+
env.db,
|
|
102
|
+
`[ALERT:${msg.sender.toUpperCase()}] ${msg.subject}`,
|
|
103
|
+
`Agent alert from ${msg.sender}:\n\n${msg.body}`,
|
|
104
|
+
'high',
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
await ackMessage(env.db, msg.id);
|
|
108
|
+
console.log(`[inbox] Alert from ${msg.sender} escalated to agenda: ${msg.subject}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function handleStatusUpdate(msg: InboxMessage, env: EdgeEnv): Promise<void> {
|
|
112
|
+
// Record significant status updates to memory
|
|
113
|
+
if (env.memoryBinding) {
|
|
114
|
+
await recordMemory(env.memoryBinding, 'aegis', `[${msg.sender}] ${msg.subject}: ${msg.body.slice(0, 500)}`, 0.7, 'agent_inbox');
|
|
115
|
+
}
|
|
116
|
+
await ackMessage(env.db, msg.id);
|
|
117
|
+
console.log(`[inbox] Status update from ${msg.sender} recorded: ${msg.subject}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function handleMessage(msg: InboxMessage, env: EdgeEnv): Promise<void> {
|
|
121
|
+
// Generic messages -- ack and log
|
|
122
|
+
await ackMessage(env.db, msg.id);
|
|
123
|
+
console.log(`[inbox] Message from ${msg.sender} acked: ${msg.subject}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function runInboxProcessor(env: EdgeEnv): Promise<void> {
|
|
127
|
+
// Read unread messages for AEGIS
|
|
128
|
+
const result = await env.db.prepare(`
|
|
129
|
+
SELECT id, sender, recipient, channel, msg_type, subject, body, created_at
|
|
130
|
+
FROM agent_inbox
|
|
131
|
+
WHERE (recipient = 'aegis' OR recipient = 'all')
|
|
132
|
+
AND acked_at IS NULL
|
|
133
|
+
ORDER BY created_at ASC
|
|
134
|
+
LIMIT 20
|
|
135
|
+
`).all<InboxMessage>();
|
|
136
|
+
|
|
137
|
+
const messages = result.results ?? [];
|
|
138
|
+
if (messages.length === 0) return;
|
|
139
|
+
|
|
140
|
+
console.log(`[inbox] Processing ${messages.length} unread message(s)`);
|
|
141
|
+
|
|
142
|
+
for (const msg of messages) {
|
|
143
|
+
// Don't process our own broadcasts
|
|
144
|
+
if (msg.sender === 'aegis' && msg.recipient === 'all') {
|
|
145
|
+
await ackMessage(env.db, msg.id);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
switch (msg.msg_type) {
|
|
151
|
+
case 'context_request':
|
|
152
|
+
await handleContextRequest(msg, env);
|
|
153
|
+
break;
|
|
154
|
+
case 'task_proposal':
|
|
155
|
+
await handleTaskProposal(msg, env);
|
|
156
|
+
break;
|
|
157
|
+
case 'alert':
|
|
158
|
+
await handleAlert(msg, env);
|
|
159
|
+
break;
|
|
160
|
+
case 'status_update':
|
|
161
|
+
await handleStatusUpdate(msg, env);
|
|
162
|
+
break;
|
|
163
|
+
case 'message':
|
|
164
|
+
default:
|
|
165
|
+
await handleMessage(msg, env);
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
} catch (err) {
|
|
169
|
+
console.error(`[inbox] Error processing message ${msg.id} from ${msg.sender}:`, err instanceof Error ? err.message : String(err));
|
|
170
|
+
// Ack anyway to prevent infinite reprocessing
|
|
171
|
+
await ackMessage(env.db, msg.id);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|