@stackbilt/aegis-core 0.6.3 → 0.6.5
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 +1 -1
- package/src/auth.ts +6 -2
- package/src/groq.ts +7 -2
- package/src/kernel/dispatch.ts +1 -0
- package/src/kernel/memory/recall.ts +8 -2
- package/src/kernel/router.ts +8 -2
- package/src/kernel/scheduled/argus-heartbeat.ts +11 -2
- package/src/kernel/scheduled/conversation-facts.ts +22 -3
- package/src/kernel/scheduled/curiosity.ts +35 -1
- package/src/kernel/scheduled/dreaming/llm.ts +13 -7
- package/src/kernel/scheduled/mindspring-notebook.ts +132 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stackbilt/aegis-core",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.5",
|
|
4
4
|
"description": "Persistent AI agent framework for Cloudflare Workers. Multi-tier memory, autonomous goals, dreaming cycles, MCP native.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"publishConfig": {
|
package/src/auth.ts
CHANGED
|
@@ -31,8 +31,12 @@ export async function bearerAuth(c: Context<{ Bindings: Env }>, next: Next): Pro
|
|
|
31
31
|
const token = extractBearer(authHeader) ?? cookieToken ?? queryToken;
|
|
32
32
|
|
|
33
33
|
if (!token || token !== c.env.AEGIS_TOKEN) {
|
|
34
|
-
// UI pages — show login
|
|
35
|
-
|
|
34
|
+
// UI pages — show the login form for top-level HTML navigations (a GET
|
|
35
|
+
// whose Accept includes text/html) so the operator can enter a token.
|
|
36
|
+
// Path-agnostic on purpose: any page route — core or downstream-variant
|
|
37
|
+
// (e.g. the daemon's /lite) — gets the form without core enumerating it.
|
|
38
|
+
// API/fetch requests (Accept */* or application/json) get JSON 401.
|
|
39
|
+
if (c.req.method === 'GET' && (c.req.header('Accept') ?? '').includes('text/html')) {
|
|
36
40
|
return c.html(loginPage(), 401);
|
|
37
41
|
}
|
|
38
42
|
return c.json({ error: 'Unauthorized' }, 401);
|
package/src/groq.ts
CHANGED
|
@@ -30,11 +30,16 @@ export async function askGroq(
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
const data = await response.json<{
|
|
33
|
-
choices: { message: { content:
|
|
33
|
+
choices: { message: { content: unknown } }[];
|
|
34
34
|
usage?: { total_tokens: number };
|
|
35
35
|
}>();
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
const content = data.choices[0]?.message?.content;
|
|
38
|
+
if (typeof content === 'string') return content;
|
|
39
|
+
if (content == null) return '';
|
|
40
|
+
// Some Groq-routed models (notably gpt-oss tool-calling variants) return content
|
|
41
|
+
// as an array of content blocks. Coerce so downstream string operations don't crash.
|
|
42
|
+
return typeof content === 'object' ? JSON.stringify(content) : String(content);
|
|
38
43
|
}
|
|
39
44
|
|
|
40
45
|
// ─── Logprobs-enabled classification ─────────────────────────
|
package/src/kernel/dispatch.ts
CHANGED
|
@@ -43,6 +43,8 @@ interface MindSpringResult {
|
|
|
43
43
|
title: string;
|
|
44
44
|
text: string;
|
|
45
45
|
score: number;
|
|
46
|
+
notebook_id?: string;
|
|
47
|
+
notebook_title?: string;
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
// ─── RRF (Reciprocal Rank Fusion) ────────────────────────────
|
|
@@ -143,8 +145,12 @@ export async function recallForQuery(
|
|
|
143
145
|
: query;
|
|
144
146
|
|
|
145
147
|
const msResponse = await env.mindspringFetcher.fetch(
|
|
146
|
-
|
|
147
|
-
{
|
|
148
|
+
'https://mindspring/api/v2/workspaces/aegis-daemon/search',
|
|
149
|
+
{
|
|
150
|
+
method: 'POST',
|
|
151
|
+
headers: { 'Authorization': `Bearer ${env.mindspringToken}`, 'Content-Type': 'application/json' },
|
|
152
|
+
body: JSON.stringify({ query: msQuery, limit: 5, threshold: 0.4 }),
|
|
153
|
+
},
|
|
148
154
|
);
|
|
149
155
|
|
|
150
156
|
if (msResponse.ok) {
|
package/src/kernel/router.ts
CHANGED
|
@@ -79,8 +79,14 @@ async function classifyWithWorkersAI(
|
|
|
79
79
|
],
|
|
80
80
|
max_tokens: 200,
|
|
81
81
|
temperature: 0.1,
|
|
82
|
-
}) as { response?:
|
|
83
|
-
|
|
82
|
+
}) as { response?: unknown };
|
|
83
|
+
const raw = result.response;
|
|
84
|
+
if (typeof raw === 'string') return raw;
|
|
85
|
+
if (raw == null) return '';
|
|
86
|
+
// Workers AI sometimes returns structured responses (objects with tool_calls,
|
|
87
|
+
// arrays of segments, etc.). Coerce to string so downstream .trim()/JSON.parse
|
|
88
|
+
// callers don't crash on non-string payloads.
|
|
89
|
+
return typeof raw === 'object' ? JSON.stringify(raw) : String(raw);
|
|
84
90
|
}
|
|
85
91
|
|
|
86
92
|
|
|
@@ -21,7 +21,15 @@ import type { CorrelationResult, IncidentCluster, ArgusDiagnosis } from '../argu
|
|
|
21
21
|
// ─── Configuration ───────────────────────────────────────────
|
|
22
22
|
|
|
23
23
|
const RUN_CADENCE_HOURS = 3;
|
|
24
|
-
const COOLDOWN_MS = 12 * 60 * 60 * 1000; // 12h cooldown per pattern alert
|
|
24
|
+
const COOLDOWN_MS = 12 * 60 * 60 * 1000; // 12h default cooldown per pattern alert
|
|
25
|
+
|
|
26
|
+
// Per-pattern cooldown overrides. Drought patterns are expected to persist in
|
|
27
|
+
// pre-revenue or low-activity states — a longer cooldown prevents daily noise
|
|
28
|
+
// from a condition that isn't going to self-resolve on a 12h cycle.
|
|
29
|
+
const PATTERN_COOLDOWN_MS: Record<string, number> = {
|
|
30
|
+
drought_stripe: 72 * 60 * 60 * 1000, // 72h — expected in pre-revenue
|
|
31
|
+
drought_github: 48 * 60 * 60 * 1000, // 48h
|
|
32
|
+
};
|
|
25
33
|
|
|
26
34
|
// Pattern thresholds
|
|
27
35
|
const CI_FAILURE_WINDOW_HOURS = 6;
|
|
@@ -179,7 +187,8 @@ async function isOnCooldown(db: D1Database, pattern: string): Promise<boolean> {
|
|
|
179
187
|
).bind(key).first<{ received_at: string }>();
|
|
180
188
|
|
|
181
189
|
if (!last) return false;
|
|
182
|
-
|
|
190
|
+
const cooldown = PATTERN_COOLDOWN_MS[pattern] ?? COOLDOWN_MS;
|
|
191
|
+
return (Date.now() - new Date(last.received_at + 'Z').getTime()) < cooldown;
|
|
183
192
|
}
|
|
184
193
|
|
|
185
194
|
async function recordCooldown(db: D1Database, pattern: string): Promise<void> {
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
// Per-conversation fact extraction (#324)
|
|
2
2
|
// Complements the dreaming cycle (daily, batch) with near-real-time
|
|
3
3
|
// fact capture from operator chat sessions. Runs every 2 hours,
|
|
4
|
-
// processes conversations updated since last run. Uses
|
|
4
|
+
// processes conversations updated since last run. Uses Groq (free) with
|
|
5
|
+
// Workers AI llama-3.1-8b (free tier) as fallback.
|
|
5
6
|
|
|
6
7
|
import { type EdgeEnv } from '../dispatch.js';
|
|
7
8
|
import { recordMemory as recordMemoryAdapter } from '../memory-adapter.js';
|
|
8
9
|
import { askGroq } from '../../groq.js';
|
|
10
|
+
import { pushFactsToMindSpring, type FactEntry } from './mindspring-notebook.js';
|
|
9
11
|
|
|
10
12
|
const WATERMARK_KEY = 'conversation_facts_watermark';
|
|
11
13
|
const MAX_CONVERSATIONS = 5;
|
|
@@ -85,16 +87,25 @@ async function askAi(
|
|
|
85
87
|
system: string,
|
|
86
88
|
user: string,
|
|
87
89
|
): Promise<string> {
|
|
90
|
+
// Groq first — free, same 70B quality, no neuron consumption
|
|
91
|
+
if (env.groqApiKey) {
|
|
92
|
+
try {
|
|
93
|
+
return await askGroq(env.groqApiKey, env.groqResponseModel, system, user, env.groqBaseUrl);
|
|
94
|
+
} catch {
|
|
95
|
+
// fall through to Workers AI
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// Workers AI fallback — llama-3.1-8b is on the genuine free tier
|
|
88
99
|
if (env.ai) {
|
|
89
100
|
const result = await env.ai.run(
|
|
90
|
-
'@cf/meta/llama-3.
|
|
101
|
+
'@cf/meta/llama-3.1-8b-instruct' as Parameters<Ai['run']>[0],
|
|
91
102
|
{ messages: [{ role: 'system', content: system }, { role: 'user', content: user }] },
|
|
92
103
|
);
|
|
93
104
|
if (typeof result === 'string') return result;
|
|
94
105
|
const obj = result as { response?: string; choices?: Array<{ message?: { content?: string } }> };
|
|
95
106
|
return obj.choices?.[0]?.message?.content ?? obj.response ?? '';
|
|
96
107
|
}
|
|
97
|
-
|
|
108
|
+
throw new Error('[conv-facts] No LLM provider available (groqApiKey and env.ai both missing)');
|
|
98
109
|
}
|
|
99
110
|
|
|
100
111
|
export async function runConversationFactExtraction(env: EdgeEnv): Promise<void> {
|
|
@@ -128,6 +139,7 @@ export async function runConversationFactExtraction(env: EdgeEnv): Promise<void>
|
|
|
128
139
|
}
|
|
129
140
|
|
|
130
141
|
let totalFacts = 0;
|
|
142
|
+
const allFacts: FactEntry[] = [];
|
|
131
143
|
|
|
132
144
|
for (const conv of conversations.results) {
|
|
133
145
|
const messages = await env.db.prepare(`
|
|
@@ -185,6 +197,7 @@ export async function runConversationFactExtraction(env: EdgeEnv): Promise<void>
|
|
|
185
197
|
fact.confidence ?? 0.8,
|
|
186
198
|
'conversation_extraction',
|
|
187
199
|
);
|
|
200
|
+
allFacts.push({ topic: topicLower, fact: fact.fact, confidence: fact.confidence ?? 0.8 });
|
|
188
201
|
totalFacts++;
|
|
189
202
|
console.log(`[conv-facts] Extracted: [${topicLower}] ${fact.fact.slice(0, 80)}`);
|
|
190
203
|
} catch (err) {
|
|
@@ -195,6 +208,12 @@ export async function runConversationFactExtraction(env: EdgeEnv): Promise<void>
|
|
|
195
208
|
|
|
196
209
|
await advanceWatermark(env.db);
|
|
197
210
|
console.log(`[conv-facts] Processed ${conversations.results.length} conversations, extracted ${totalFacts} facts`);
|
|
211
|
+
|
|
212
|
+
// Push to MindSpring topic notebooks (non-blocking, never throws)
|
|
213
|
+
if (allFacts.length > 0) {
|
|
214
|
+
const runTag = `conv-facts-${Date.now().toString(36)}`;
|
|
215
|
+
await pushFactsToMindSpring(allFacts, runTag, env);
|
|
216
|
+
}
|
|
198
217
|
}
|
|
199
218
|
|
|
200
219
|
async function advanceWatermark(db: D1Database): Promise<void> {
|
|
@@ -8,11 +8,12 @@ import { type HeartbeatCheck } from './heartbeat.js';
|
|
|
8
8
|
export interface CuriosityCandidate {
|
|
9
9
|
topic: string;
|
|
10
10
|
reason: string;
|
|
11
|
-
source: 'memory_gap' | 'low_confidence' | 'failure_rate' | 'heartbeat_warn' | 'goal_failure' | 'self_interest';
|
|
11
|
+
source: 'memory_gap' | 'low_confidence' | 'failure_rate' | 'heartbeat_warn' | 'goal_failure' | 'self_interest' | 'conversation_gap';
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export async function gatherCuriosityTopics(env: EdgeEnv): Promise<CuriosityCandidate[]> {
|
|
15
15
|
const candidates: CuriosityCandidate[] = [];
|
|
16
|
+
const thinTopicSeeds: string[] = [];
|
|
16
17
|
|
|
17
18
|
// Source 1: Memory gaps — topics with few entries relative to others
|
|
18
19
|
if (env.memoryBinding) {
|
|
@@ -20,6 +21,7 @@ export async function gatherCuriosityTopics(env: EdgeEnv): Promise<CuriosityCand
|
|
|
20
21
|
const stats = await env.memoryBinding.stats('aegis');
|
|
21
22
|
const thinTopics = stats.topics.filter(t => t.count <= 2).slice(0, 5);
|
|
22
23
|
for (const t of thinTopics) {
|
|
24
|
+
thinTopicSeeds.push(t.topic);
|
|
23
25
|
candidates.push({
|
|
24
26
|
topic: `What more should I know about "${t.topic}"?`,
|
|
25
27
|
reason: `Only ${t.count} memory entries — thin coverage`,
|
|
@@ -154,6 +156,38 @@ export async function gatherCuriosityTopics(env: EdgeEnv): Promise<CuriosityCand
|
|
|
154
156
|
}
|
|
155
157
|
}
|
|
156
158
|
|
|
159
|
+
// Source 8: MindSpring conversation gap — topics MW barely knows but that appear in
|
|
160
|
+
// conversation history signal a consolidation pipeline failure, not just a knowledge gap.
|
|
161
|
+
const { mindspringFetcher, mindspringToken } = env;
|
|
162
|
+
if (mindspringFetcher && mindspringToken && thinTopicSeeds.length > 0) {
|
|
163
|
+
try {
|
|
164
|
+
const queryResults = await Promise.allSettled(
|
|
165
|
+
thinTopicSeeds.slice(0, 3).map(async (seed) => {
|
|
166
|
+
const res = await mindspringFetcher.fetch('https://mindspring/api/v2/workspaces/aegis-daemon/search', {
|
|
167
|
+
method: 'POST',
|
|
168
|
+
signal: AbortSignal.timeout(1500),
|
|
169
|
+
headers: { Authorization: `Bearer ${mindspringToken}`, 'Content-Type': 'application/json' },
|
|
170
|
+
body: JSON.stringify({ query: seed, limit: 5, threshold: 0.5 }),
|
|
171
|
+
});
|
|
172
|
+
if (!res.ok) return { seed, count: 0 };
|
|
173
|
+
const data = await res.json<{ results: Array<{ title: string; score: number }> }>();
|
|
174
|
+
return { seed, count: (data.results ?? []).length };
|
|
175
|
+
})
|
|
176
|
+
);
|
|
177
|
+
for (const r of queryResults) {
|
|
178
|
+
if (r.status === 'fulfilled' && r.value.count > 0) {
|
|
179
|
+
candidates.push({
|
|
180
|
+
topic: `"${r.value.seed}" appears in conversation history but has thin memory coverage`,
|
|
181
|
+
reason: `${r.value.count} MindSpring matches vs ≤2 memory entries — consolidation gap`,
|
|
182
|
+
source: 'conversation_gap',
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
} catch (err) {
|
|
187
|
+
console.warn('[curiosity] MindSpring gap scan failed:', err instanceof Error ? err.message : String(err));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
157
191
|
return candidates;
|
|
158
192
|
}
|
|
159
193
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Shared LLM helper —
|
|
1
|
+
// Shared LLM helper — Groq first (free, 70B quality), Workers AI 70B fallback
|
|
2
2
|
|
|
3
3
|
import type { EdgeEnv } from '../../dispatch.js';
|
|
4
4
|
import { askGroq } from '../../../groq.js';
|
|
@@ -9,18 +9,24 @@ export async function askWorkersAiOrGroq(
|
|
|
9
9
|
user: string,
|
|
10
10
|
useResponseModel = false,
|
|
11
11
|
): Promise<string> {
|
|
12
|
+
const groqModel = useResponseModel ? env.groqResponseModel : env.groqModel;
|
|
13
|
+
// Groq first — free tier, same 70B quality, eliminates neuron consumption
|
|
14
|
+
if (env.groqApiKey) {
|
|
15
|
+
try {
|
|
16
|
+
return await askGroq(env.groqApiKey, groqModel, system, user, env.groqBaseUrl);
|
|
17
|
+
} catch {
|
|
18
|
+
// fall through to Workers AI
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
// Workers AI fallback — only fires if Groq is unavailable or throws
|
|
12
22
|
if (env.ai) {
|
|
13
|
-
const model = useResponseModel
|
|
14
|
-
? '@cf/meta/llama-3.3-70b-instruct-fp8-fast'
|
|
15
|
-
: (env.gptOssModel || '@cf/meta/llama-3.3-70b-instruct-fp8-fast');
|
|
16
23
|
const result = await env.ai.run(
|
|
17
|
-
|
|
24
|
+
'@cf/meta/llama-3.3-70b-instruct-fp8-fast' as Parameters<Ai['run']>[0],
|
|
18
25
|
{ messages: [{ role: 'system', content: system }, { role: 'user', content: user }] },
|
|
19
26
|
) as { response?: string; choices?: Array<{ message?: { content?: string } }> };
|
|
20
27
|
return result.choices?.[0]?.message?.content ?? result.response ?? '';
|
|
21
28
|
}
|
|
22
|
-
|
|
23
|
-
return askGroq(env.groqApiKey, groqModel, system, user, env.groqBaseUrl);
|
|
29
|
+
throw new Error('[dreaming] No LLM provider available (groqApiKey and env.ai both missing)');
|
|
24
30
|
}
|
|
25
31
|
|
|
26
32
|
export function parseJsonResponse<T>(raw: string): T | null {
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// MindSpring v2 write pipeline — push extracted facts to topic notebooks
|
|
2
|
+
|
|
3
|
+
import type { EdgeEnv } from '../dispatch.js';
|
|
4
|
+
|
|
5
|
+
const WORKSPACE_ID = 'aegis-daemon';
|
|
6
|
+
const MS_BASE = 'https://mindspring';
|
|
7
|
+
|
|
8
|
+
interface MsNotebook { id: string; title: string }
|
|
9
|
+
interface UploadAccepted { uploadId: string; status: string }
|
|
10
|
+
|
|
11
|
+
export interface FactEntry {
|
|
12
|
+
topic: string;
|
|
13
|
+
fact: string;
|
|
14
|
+
confidence: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function msHeaders(token: string, extra?: Record<string, string>): Headers {
|
|
18
|
+
const h = new Headers(extra);
|
|
19
|
+
h.set('Authorization', `Bearer ${token}`);
|
|
20
|
+
return h;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function findOrCreateNotebook(topic: string, env: EdgeEnv): Promise<string> {
|
|
24
|
+
const token = env.mindspringIngestToken!;
|
|
25
|
+
const fetcher = env.mindspringFetcher!;
|
|
26
|
+
|
|
27
|
+
const listResp = await fetcher.fetch(
|
|
28
|
+
`${MS_BASE}/api/v2/workspaces/${WORKSPACE_ID}/notebooks`,
|
|
29
|
+
{ headers: msHeaders(token) },
|
|
30
|
+
);
|
|
31
|
+
if (listResp.ok) {
|
|
32
|
+
const data = await listResp.json<{ notebooks: MsNotebook[] }>();
|
|
33
|
+
const existing = data.notebooks?.find((nb) => nb.title === topic);
|
|
34
|
+
if (existing) return existing.id;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const createResp = await fetcher.fetch(
|
|
38
|
+
`${MS_BASE}/api/v2/workspaces/${WORKSPACE_ID}/notebooks`,
|
|
39
|
+
{
|
|
40
|
+
method: 'POST',
|
|
41
|
+
headers: msHeaders(token, { 'Content-Type': 'application/json' }),
|
|
42
|
+
body: JSON.stringify({ title: topic, type: 'research' }),
|
|
43
|
+
},
|
|
44
|
+
);
|
|
45
|
+
if (!createResp.ok) {
|
|
46
|
+
const msg = await createResp.text().catch(() => '');
|
|
47
|
+
throw new Error(`create notebook failed: ${createResp.status} ${msg.slice(0, 120)}`);
|
|
48
|
+
}
|
|
49
|
+
const nb = await createResp.json<MsNotebook>();
|
|
50
|
+
return nb.id;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function uploadContent(content: string, filename: string, env: EdgeEnv): Promise<string> {
|
|
54
|
+
const token = env.mindspringIngestToken!;
|
|
55
|
+
const fetcher = env.mindspringFetcher!;
|
|
56
|
+
|
|
57
|
+
const resp = await fetcher.fetch(`${MS_BASE}/api/uploads/simple`, {
|
|
58
|
+
method: 'POST',
|
|
59
|
+
headers: msHeaders(token, {
|
|
60
|
+
'Content-Type': 'text/plain',
|
|
61
|
+
'X-File-Name': filename,
|
|
62
|
+
}),
|
|
63
|
+
body: content,
|
|
64
|
+
});
|
|
65
|
+
if (!resp.ok) {
|
|
66
|
+
const msg = await resp.text().catch(() => '');
|
|
67
|
+
throw new Error(`upload failed: ${resp.status} ${msg.slice(0, 120)}`);
|
|
68
|
+
}
|
|
69
|
+
const { uploadId } = await resp.json<UploadAccepted>();
|
|
70
|
+
return uploadId;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function registerSource(notebookId: string, title: string, uploadId: string, env: EdgeEnv): Promise<void> {
|
|
74
|
+
const token = env.mindspringIngestToken!;
|
|
75
|
+
const fetcher = env.mindspringFetcher!;
|
|
76
|
+
|
|
77
|
+
const resp = await fetcher.fetch(
|
|
78
|
+
`${MS_BASE}/api/v2/workspaces/${WORKSPACE_ID}/notebooks/${notebookId}/sources`,
|
|
79
|
+
{
|
|
80
|
+
method: 'POST',
|
|
81
|
+
headers: msHeaders(token, { 'Content-Type': 'application/json' }),
|
|
82
|
+
body: JSON.stringify({ title, type: 'txt', sourceUploadId: uploadId, parserType: 'txt' }),
|
|
83
|
+
},
|
|
84
|
+
);
|
|
85
|
+
if (!resp.ok && resp.status !== 202) {
|
|
86
|
+
const msg = await resp.text().catch(() => '');
|
|
87
|
+
throw new Error(`register source failed: ${resp.status} ${msg.slice(0, 120)}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Push extracted facts to MindSpring v2 topic notebooks.
|
|
93
|
+
* Groups facts by topic, creates notebooks as needed.
|
|
94
|
+
* Never throws — all errors are logged as warnings.
|
|
95
|
+
*/
|
|
96
|
+
export async function pushFactsToMindSpring(
|
|
97
|
+
facts: FactEntry[],
|
|
98
|
+
sourceTag: string,
|
|
99
|
+
env: EdgeEnv,
|
|
100
|
+
): Promise<void> {
|
|
101
|
+
if (!env.mindspringFetcher || !env.mindspringIngestToken || facts.length === 0) return;
|
|
102
|
+
|
|
103
|
+
// Group by topic
|
|
104
|
+
const byTopic = new Map<string, string[]>();
|
|
105
|
+
for (const { topic, fact } of facts) {
|
|
106
|
+
const arr = byTopic.get(topic) ?? [];
|
|
107
|
+
arr.push(fact);
|
|
108
|
+
byTopic.set(topic, arr);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
112
|
+
|
|
113
|
+
for (const [topic, topicFacts] of byTopic.entries()) {
|
|
114
|
+
try {
|
|
115
|
+
const content = [
|
|
116
|
+
`Topic: ${topic}`,
|
|
117
|
+
`Source: ${sourceTag}`,
|
|
118
|
+
`Date: ${date}`,
|
|
119
|
+
'',
|
|
120
|
+
...topicFacts.map((f) => `- ${f}`),
|
|
121
|
+
].join('\n');
|
|
122
|
+
|
|
123
|
+
const notebookId = await findOrCreateNotebook(topic, env);
|
|
124
|
+
const uploadId = await uploadContent(content, `${topic}-facts.txt`, env);
|
|
125
|
+
await registerSource(notebookId, `facts-${date}-${sourceTag.slice(0, 12)}`, uploadId, env);
|
|
126
|
+
|
|
127
|
+
console.log(`[mindspring-nb] pushed ${topicFacts.length} fact(s) → notebook '${topic}' (${notebookId.slice(0, 8)})`);
|
|
128
|
+
} catch (err) {
|
|
129
|
+
console.warn(`[mindspring-nb] topic '${topic}' push failed:`, err instanceof Error ? err.message : String(err));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|