sensorium-mcp 3.0.54 → 3.0.56
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/dist/data/file-storage.d.ts +2 -1
- package/dist/data/file-storage.d.ts.map +1 -1
- package/dist/data/file-storage.js +24 -6
- package/dist/data/file-storage.js.map +1 -1
- package/dist/data/memory/narrative.d.ts +5 -5
- package/dist/data/memory/narrative.d.ts.map +1 -1
- package/dist/data/memory/narrative.js +475 -71
- package/dist/data/memory/narrative.js.map +1 -1
- package/dist/response-builders.d.ts.map +1 -1
- package/dist/response-builders.js +2 -1
- package/dist/response-builders.js.map +1 -1
- package/dist/services/agent-spawn.service.d.ts.map +1 -1
- package/dist/services/agent-spawn.service.js +22 -14
- package/dist/services/agent-spawn.service.js.map +1 -1
- package/dist/services/dispatcher/lock.d.ts +17 -2
- package/dist/services/dispatcher/lock.d.ts.map +1 -1
- package/dist/services/dispatcher/lock.js +125 -13
- package/dist/services/dispatcher/lock.js.map +1 -1
- package/dist/services/dispatcher/poller.d.ts.map +1 -1
- package/dist/services/dispatcher/poller.js +9 -8
- package/dist/services/dispatcher/poller.js.map +1 -1
- package/dist/services/keeper.service.d.ts.map +1 -1
- package/dist/services/keeper.service.js +7 -1
- package/dist/services/keeper.service.js.map +1 -1
- package/dist/services/process.service.d.ts.map +1 -1
- package/dist/services/process.service.js +32 -9
- package/dist/services/process.service.js.map +1 -1
- package/dist/services/self-update.service.d.ts.map +1 -1
- package/dist/services/self-update.service.js +2 -26
- package/dist/services/self-update.service.js.map +1 -1
- package/dist/telegram.d.ts.map +1 -1
- package/dist/telegram.js +19 -8
- package/dist/telegram.js.map +1 -1
- package/dist/tools/session-tools.js +2 -4
- package/dist/tools/session-tools.js.map +1 -1
- package/dist/tools/wait/poll-loop.d.ts +1 -1
- package/dist/tools/wait/poll-loop.d.ts.map +1 -1
- package/dist/tools/wait/poll-loop.js +15 -0
- package/dist/tools/wait/poll-loop.js.map +1 -1
- package/package.json +2 -1
- package/templates/azure-devops-securevault.default.md +12 -0
- package/templates/md-to-pdf.default.md +36 -0
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
* Temporal Narrative Generator
|
|
3
3
|
*
|
|
4
4
|
* Produces multi-resolution narratives from episodes and semantic notes:
|
|
5
|
-
* - day: detailed events (~
|
|
6
|
-
* - week: key decisions and progress (~
|
|
7
|
-
* - month: high-level arc (~
|
|
8
|
-
* - quarter: strategic 3-month arc (~
|
|
9
|
-
* - half_year: bird's-eye 6-month arc (~
|
|
5
|
+
* - day: detailed events (~400 tokens)
|
|
6
|
+
* - week: key decisions and progress (~800 tokens)
|
|
7
|
+
* - month: high-level arc (~1200 tokens)
|
|
8
|
+
* - quarter: strategic 3-month arc (~2000 tokens)
|
|
9
|
+
* - half_year: bird's-eye 6-month arc (~2500 tokens)
|
|
10
10
|
*
|
|
11
11
|
* These narratives replace raw note dumps in bootstrap and give the agent
|
|
12
12
|
* coherent temporal awareness across long-running sessions.
|
|
@@ -25,7 +25,7 @@ const NARRATIVE_FILLER_PHRASES = [
|
|
|
25
25
|
/\bseveral enhancements\b/i,
|
|
26
26
|
/\bsignificant (evolution|strides|developments?)\b/i,
|
|
27
27
|
/\boverall.{0,20}(positive|good|well)\b/i,
|
|
28
|
-
/\bpivotal moments
|
|
28
|
+
/\bpivotal (moments?|decisions?|turning)\b/i,
|
|
29
29
|
/\bcrucial (step|milestone|decision)\b/i,
|
|
30
30
|
/\bnotable (milestone|achievement|development)\b/i,
|
|
31
31
|
/\bsubstantial (progress|improvement)\b/i,
|
|
@@ -34,6 +34,22 @@ const NARRATIVE_FILLER_PHRASES = [
|
|
|
34
34
|
/\bas I (navigated|reflected|observed|witnessed)\b/i,
|
|
35
35
|
/\bthis (prompted|led) me to reflect\b/i,
|
|
36
36
|
/\bI (noticed|observed|witnessed) a (critical|pivotal|key)\b/i,
|
|
37
|
+
/\bmarked by a series of\b/i,
|
|
38
|
+
/\bpart of a broader effort\b/i,
|
|
39
|
+
/\bshaped the (direction|trajectory)\b/i,
|
|
40
|
+
/\bturning points? that\b/i,
|
|
41
|
+
/\bset the stage for\b/i,
|
|
42
|
+
/\bnot only .{5,40} but also\b/i,
|
|
43
|
+
/\bculminating in\b/i,
|
|
44
|
+
/\bthe focus remains on\b/i,
|
|
45
|
+
/\bdriven by a commitment\b/i,
|
|
46
|
+
/\bthis decision to\b/i,
|
|
47
|
+
/\bthe current status reflects\b/i,
|
|
48
|
+
/\bresulting in a more\b/i,
|
|
49
|
+
/\bleading to more\b/i,
|
|
50
|
+
/\baddress(ed|ing) the root causes? of\b/i,
|
|
51
|
+
/\benhancing overall\b/i,
|
|
52
|
+
/\baligning with\b/i,
|
|
37
53
|
];
|
|
38
54
|
function findFillerPhrase(text) {
|
|
39
55
|
for (const pattern of NARRATIVE_FILLER_PHRASES) {
|
|
@@ -43,6 +59,64 @@ function findFillerPhrase(text) {
|
|
|
43
59
|
}
|
|
44
60
|
return null;
|
|
45
61
|
}
|
|
62
|
+
function findDateViolation(text, periodStart, periodEnd) {
|
|
63
|
+
const startDate = new Date(periodStart);
|
|
64
|
+
const endDate = new Date(periodEnd);
|
|
65
|
+
const validYears = new Set();
|
|
66
|
+
for (let y = startDate.getFullYear(); y <= endDate.getFullYear(); y++)
|
|
67
|
+
validYears.add(y);
|
|
68
|
+
// Check standalone year references (e.g. "in 2025", "2025–2026")
|
|
69
|
+
const yearMatches = text.matchAll(/\b(20\d{2})\b/g);
|
|
70
|
+
for (const m of yearMatches) {
|
|
71
|
+
const year = parseInt(m[1], 10);
|
|
72
|
+
if (!validYears.has(year))
|
|
73
|
+
return `year ${year} outside valid range ${[...validYears].join("–")}`;
|
|
74
|
+
}
|
|
75
|
+
// Check month+year combos (e.g. "March 2026", "Apr 2025")
|
|
76
|
+
const monthNames = ["january", "february", "march", "april", "may", "june", "july", "august", "september", "october", "november", "december"];
|
|
77
|
+
const monthPattern = /\b(Jan(?:uary)?|Feb(?:ruary)?|Mar(?:ch)?|Apr(?:il)?|May|June?|July?|Aug(?:ust)?|Sep(?:tember)?|Oct(?:ober)?|Nov(?:ember)?|Dec(?:ember)?)\s+(20\d{2})\b/gi;
|
|
78
|
+
for (const m of text.matchAll(monthPattern)) {
|
|
79
|
+
const monthStr = m[1].toLowerCase().slice(0, 3);
|
|
80
|
+
const monthIdx = monthNames.findIndex((mn) => mn.startsWith(monthStr));
|
|
81
|
+
const year = parseInt(m[2], 10);
|
|
82
|
+
if (monthIdx === -1)
|
|
83
|
+
continue;
|
|
84
|
+
const refDate = new Date(year, monthIdx, 15);
|
|
85
|
+
const windowStart = new Date(startDate);
|
|
86
|
+
windowStart.setDate(windowStart.getDate() - 7);
|
|
87
|
+
const windowEnd = new Date(endDate);
|
|
88
|
+
windowEnd.setDate(windowEnd.getDate() + 7);
|
|
89
|
+
if (refDate < windowStart || refDate > windowEnd) {
|
|
90
|
+
return `"${m[0]}" falls outside the period ${periodStart.slice(0, 10)} to ${periodEnd.slice(0, 10)}`;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
function findLowDensitySentences(text) {
|
|
96
|
+
const sentences = text.split(/(?<=[.!?])\s+/).filter(s => s.length > 20);
|
|
97
|
+
if (sentences.length === 0)
|
|
98
|
+
return { count: 0, total: 0, examples: [] };
|
|
99
|
+
const identifierPattern = /\b(?:\d{4}[-/]\d{2}[-/]\d{2}|\d{1,2}:\d{2}|v\d+\.\d+|#\d+|ID\s*\d+|\d{3,}|(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\w*\s+\d{1,2}|(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday))/i;
|
|
100
|
+
const quotedNamePattern = /['"][^'"]+['"]|`[^`]+`/;
|
|
101
|
+
const namedEntityPattern = /thread\s*\d+|ID\s*\d+|(?:MCP|API|LLM|GPT|SQL|WAL|CLI|SDK|PR|CI|CD)\b/i;
|
|
102
|
+
const midSentenceProperNoun = (s) => {
|
|
103
|
+
const afterFirst = s.replace(/^\S+\s+/, "");
|
|
104
|
+
return /\b[A-Z][a-z]{2,}/.test(afterFirst);
|
|
105
|
+
};
|
|
106
|
+
const hasNumber = (s) => /\d+/.test(s);
|
|
107
|
+
const lowDensity = [];
|
|
108
|
+
for (const sentence of sentences) {
|
|
109
|
+
const hasIdentifier = identifierPattern.test(sentence);
|
|
110
|
+
const hasProperNoun = midSentenceProperNoun(sentence);
|
|
111
|
+
const hasQuotedName = quotedNamePattern.test(sentence);
|
|
112
|
+
const hasEntity = namedEntityPattern.test(sentence);
|
|
113
|
+
const hasNum = hasNumber(sentence);
|
|
114
|
+
if (!hasIdentifier && !hasProperNoun && !hasQuotedName && !hasEntity && !hasNum) {
|
|
115
|
+
lowDensity.push(sentence.slice(0, 80));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return { count: lowDensity.length, total: sentences.length, examples: lowDensity.slice(0, 3) };
|
|
119
|
+
}
|
|
46
120
|
/** Cooldown per resolution before regenerating */
|
|
47
121
|
const COOLDOWNS = {
|
|
48
122
|
day: 2 * 60 * 60 * 1000, // 2 hours
|
|
@@ -53,11 +127,23 @@ const COOLDOWNS = {
|
|
|
53
127
|
};
|
|
54
128
|
/** Target output token count per resolution */
|
|
55
129
|
const OUTPUT_TOKEN_TARGETS = {
|
|
56
|
-
day:
|
|
57
|
-
week:
|
|
58
|
-
month:
|
|
59
|
-
quarter:
|
|
60
|
-
half_year:
|
|
130
|
+
day: 400,
|
|
131
|
+
week: 800,
|
|
132
|
+
month: 1600,
|
|
133
|
+
quarter: 3000,
|
|
134
|
+
half_year: 4000,
|
|
135
|
+
};
|
|
136
|
+
const INPUT_CHAR_BUDGETS = {
|
|
137
|
+
day: { episodes: 30_000, notes: 10_000 },
|
|
138
|
+
week: { episodes: 50_000, notes: 15_000 },
|
|
139
|
+
month: { episodes: 60_000, notes: 20_000 },
|
|
140
|
+
quarter: { episodes: 80_000, notes: 25_000 },
|
|
141
|
+
half_year: { episodes: 100_000, notes: 30_000 },
|
|
142
|
+
};
|
|
143
|
+
const CHILD_RESOLUTION = {
|
|
144
|
+
month: "week",
|
|
145
|
+
quarter: "month",
|
|
146
|
+
half_year: "quarter",
|
|
61
147
|
};
|
|
62
148
|
const NARRATIVE_MODEL = process.env.NARRATIVE_MODEL || process.env.CONSOLIDATION_MODEL || "gpt-4o";
|
|
63
149
|
// ─── Period Calculation ──────────────────────────────────────────────────────
|
|
@@ -96,6 +182,12 @@ function getPeriodBounds(resolution) {
|
|
|
96
182
|
const _exhaustive = resolution;
|
|
97
183
|
throw new Error(`Unhandled narrative resolution: ${_exhaustive}`);
|
|
98
184
|
}
|
|
185
|
+
function getEarliestEpisodeDate(db, threadId) {
|
|
186
|
+
const row = db
|
|
187
|
+
.prepare(`SELECT MIN(timestamp) as earliest FROM episodes WHERE thread_id = ?`)
|
|
188
|
+
.get(threadId);
|
|
189
|
+
return row?.earliest ?? null;
|
|
190
|
+
}
|
|
99
191
|
// ─── Source Data Collection ──────────────────────────────────────────────────
|
|
100
192
|
function getEpisodesInPeriod(db, threadId, start, end) {
|
|
101
193
|
const rows = db
|
|
@@ -149,33 +241,95 @@ function getNotesInPeriod(db, threadId, start) {
|
|
|
149
241
|
qualityScore: r.quality_score ?? null,
|
|
150
242
|
}));
|
|
151
243
|
}
|
|
244
|
+
// ─── Child Narrative Retrieval ──────────────────────────────────────────────
|
|
245
|
+
function getChildNarratives(db, threadId, resolution, start, end) {
|
|
246
|
+
const child = CHILD_RESOLUTION[resolution];
|
|
247
|
+
if (!child)
|
|
248
|
+
return [];
|
|
249
|
+
const rows = db
|
|
250
|
+
.prepare(`SELECT * FROM temporal_narratives
|
|
251
|
+
WHERE thread_id = ? AND resolution = ? AND period_start >= ? AND period_start <= ?
|
|
252
|
+
ORDER BY period_start ASC`)
|
|
253
|
+
.all(threadId, child, start, end);
|
|
254
|
+
return rows.map((r) => ({
|
|
255
|
+
id: r.id,
|
|
256
|
+
threadId: r.thread_id,
|
|
257
|
+
resolution: r.resolution,
|
|
258
|
+
periodStart: r.period_start,
|
|
259
|
+
periodEnd: r.period_end,
|
|
260
|
+
narrative: r.narrative,
|
|
261
|
+
sourceEpisodeCount: r.source_episode_count,
|
|
262
|
+
sourceNoteCount: r.source_note_count,
|
|
263
|
+
model: r.model,
|
|
264
|
+
createdAt: r.created_at,
|
|
265
|
+
}));
|
|
266
|
+
}
|
|
267
|
+
function formatChildNarrativesForLLM(narratives) {
|
|
268
|
+
return narratives
|
|
269
|
+
.map((n) => {
|
|
270
|
+
const fmtDate = (d) => new Date(d).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
|
|
271
|
+
return `--- ${n.resolution} narrative (${fmtDate(n.periodStart)} – ${fmtDate(n.periodEnd)}, ${n.sourceEpisodeCount} episodes) ---\n${n.narrative}`;
|
|
272
|
+
})
|
|
273
|
+
.join("\n\n");
|
|
274
|
+
}
|
|
152
275
|
// ─── Content Extraction ──────────────────────────────────────────────────────
|
|
153
276
|
function extractEpisodeText(ep) {
|
|
154
277
|
const content = ep.content;
|
|
155
|
-
const text = (content.text || content.caption || content.message || "");
|
|
156
|
-
return text.slice(0,
|
|
278
|
+
const text = (content.text || content.raw || content.caption || content.message || "");
|
|
279
|
+
return text.slice(0, 1500);
|
|
280
|
+
}
|
|
281
|
+
function formatEpisodeLine(ep, text) {
|
|
282
|
+
const ts = ep.timestamp.slice(0, 16).replace("T", " ");
|
|
283
|
+
const imp = ep.importance != null && ep.importance !== 0.5 ? ` [imp: ${ep.importance.toFixed(1)}]` : "";
|
|
284
|
+
const tags = ep.topicTags.length > 0 ? ` [tags: ${ep.topicTags.join(", ")}]` : "";
|
|
285
|
+
return `[${ts}] (${ep.type}/${ep.modality})${imp}${tags} ${text}`;
|
|
157
286
|
}
|
|
158
287
|
function formatEpisodesForLLM(episodes, maxChars) {
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
288
|
+
const importanceBudget = Math.round(maxChars * 0.3);
|
|
289
|
+
// Pool 1: top episodes by importance (guaranteed inclusion)
|
|
290
|
+
const byImportance = [...episodes].sort((a, b) => (b.importance ?? 0.5) - (a.importance ?? 0.5));
|
|
291
|
+
const selected = new Set();
|
|
292
|
+
const pool1 = [];
|
|
293
|
+
let pool1Chars = 0;
|
|
294
|
+
for (const ep of byImportance) {
|
|
162
295
|
const text = extractEpisodeText(ep);
|
|
163
296
|
if (!text.trim())
|
|
164
297
|
continue;
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
if (chars + line.length > maxChars)
|
|
298
|
+
const line = formatEpisodeLine(ep, text);
|
|
299
|
+
if (pool1Chars + line.length > importanceBudget)
|
|
168
300
|
break;
|
|
169
|
-
|
|
170
|
-
|
|
301
|
+
pool1.push({ ep, line });
|
|
302
|
+
selected.add(ep.episodeId);
|
|
303
|
+
pool1Chars += line.length;
|
|
171
304
|
}
|
|
172
|
-
|
|
305
|
+
// Pool 2: chronological fill (remaining budget)
|
|
306
|
+
const remainingBudget = maxChars - pool1Chars;
|
|
307
|
+
const chronological = [...episodes]
|
|
308
|
+
.filter(ep => !selected.has(ep.episodeId))
|
|
309
|
+
.sort((a, b) => (a.timestamp < b.timestamp ? -1 : 1));
|
|
310
|
+
const pool2 = [];
|
|
311
|
+
let pool2Chars = 0;
|
|
312
|
+
for (const ep of chronological) {
|
|
313
|
+
const text = extractEpisodeText(ep);
|
|
314
|
+
if (!text.trim())
|
|
315
|
+
continue;
|
|
316
|
+
const line = formatEpisodeLine(ep, text);
|
|
317
|
+
if (pool2Chars + line.length > remainingBudget)
|
|
318
|
+
break;
|
|
319
|
+
pool2.push({ ep, line });
|
|
320
|
+
pool2Chars += line.length;
|
|
321
|
+
}
|
|
322
|
+
// Merge both pools and sort chronologically for coherent reading
|
|
323
|
+
return [...pool1, ...pool2]
|
|
324
|
+
.sort((a, b) => (a.ep.timestamp < b.ep.timestamp ? -1 : 1))
|
|
325
|
+
.map(x => x.line)
|
|
326
|
+
.join("\n");
|
|
173
327
|
}
|
|
174
328
|
function formatNotesForLLM(notes, maxChars) {
|
|
175
329
|
const lines = [];
|
|
176
330
|
let chars = 0;
|
|
177
331
|
for (const n of notes) {
|
|
178
|
-
const line = `- [${n.type}] ${n.content.slice(0,
|
|
332
|
+
const line = `- [${n.type}] ${n.content.slice(0, 600)} (conf: ${n.confidence.toFixed(2)})`;
|
|
179
333
|
if (chars + line.length > maxChars)
|
|
180
334
|
break;
|
|
181
335
|
lines.push(line);
|
|
@@ -188,28 +342,63 @@ function buildPrompt(resolution, episodesText, notesText, episodeCount, periodLa
|
|
|
188
342
|
const startYear = new Date(periodStart).getFullYear();
|
|
189
343
|
const endYear = new Date().getFullYear();
|
|
190
344
|
const instructions = {
|
|
191
|
-
day: `Write a
|
|
192
|
-
week: `Write a
|
|
193
|
-
month: `Write a
|
|
194
|
-
quarter: `Write a narrative
|
|
195
|
-
half_year: `Write a
|
|
345
|
+
day: `Write a narrative of what happened today (${periodLabel}). Tell the story chronologically — what happened, why, what it caused. Include timestamps (hours:minutes) for each event. Write in flowing prose, connecting events with cause and effect. Be specific: name systems, threads, versions, IDs. Target ~400 tokens.`,
|
|
346
|
+
week: `Write a narrative of the key developments this week (${periodLabel}). Tell the story chronologically by day. For each event: what triggered it, what was decided, what resulted. Connect events across days — show how Monday's decision led to Wednesday's outcome. Write in flowing prose, not a list. Use day-level dates, no hours. Target ~800 tokens.`,
|
|
347
|
+
month: `Write a chronological decision log for this month (${periodLabel}). Format: "Date — decision/event. Reason. Result." One entry per line. Each entry should have enough context to stand alone — a reader who sees only this entry should understand why it matters. Only include: decisions that changed project direction, bugs that broke something, features shipped. Skip routine fixes, code reviews, type errors, status checks. Day-level dates, no hours. End with unresolved items. Target ~1600 tokens.`,
|
|
348
|
+
quarter: `Write a narrative overview of this quarter (${periodLabel}) organized by initiative. Group by subsystem (e.g. "Dashboard", "Memory system", "Narrative pipeline", "Session management"). For each initiative, write 1-2 flowing paragraphs that tell the STORY of how it evolved: what the state was at the start of the quarter, what problems appeared, what decisions were made and why, and where it ended up. Reference specific dates, versions, and IDs as evidence within the prose — but do NOT use a list or log format. The reader should understand the arc of each initiative from a connected narrative, not from scanning dated entries. Include initiatives where direction changed, major bugs appeared, features shipped, or architecture shifted. Episodes marked [imp: 0.6+] are operator-priority — weave them in. End with a short "Unresolved" section. Target ~3000 tokens.`,
|
|
349
|
+
half_year: `Write a high-level evolution summary for this half-year (${periodLabel}). Do NOT repeat the quarterly grouped changelog — instead, synthesize it into 5-8 thematic arcs that show HOW the project evolved. For each arc: what the system looked like at the start, what forces drove change, and where it ended up. Example arcs: "Memory system evolution", "Session management maturity", "Narrative pipeline development". Within each arc, reference specific dates and versions as evidence, but the focus is the trajectory, not individual events. End with a "Current state" section: what works, what's fragile, what's next. Target ~4000 tokens.`,
|
|
196
350
|
};
|
|
197
|
-
|
|
351
|
+
const styleByResolution = {
|
|
352
|
+
day: `STYLE:
|
|
353
|
+
- Write flowing prose, not bullet points or numbered lists. Connect events into a story.
|
|
354
|
+
- First person for yourself ("I did..."), third person for the operator ("The operator...").
|
|
355
|
+
- Name every system, thread, feature by exact name with IDs where available.
|
|
356
|
+
- Be concrete — every sentence needs at least one identifier (name, version, date, ID, number).
|
|
357
|
+
- Preserve cause-and-effect chains: "X happened because Y, which led to Z."
|
|
358
|
+
- No filler phrases: "significant progress", "notable improvement", "pivotal moment", "crucial step".
|
|
359
|
+
- NEVER open with "In [Month]..." or "During [Month]..." — start with what happened.
|
|
360
|
+
- NEVER write introductory or concluding paragraphs that summarize.`,
|
|
361
|
+
week: `STYLE:
|
|
362
|
+
- Write flowing prose, not bullet points or numbered lists. Connect events into a weekly story.
|
|
363
|
+
- First person for yourself ("I did..."), third person for the operator ("The operator...").
|
|
364
|
+
- Name every system, thread, feature by exact name with IDs where available.
|
|
365
|
+
- Be concrete — every sentence needs at least one identifier (name, version, date, ID, number).
|
|
366
|
+
- Preserve cause-and-effect chains across days.
|
|
367
|
+
- No filler phrases: "significant progress", "notable improvement", "pivotal moment", "crucial step".
|
|
368
|
+
- NEVER open with "In [Month]..." or "During [Month]..." — start with what happened.
|
|
369
|
+
- NEVER write introductory or concluding paragraphs that summarize.`,
|
|
370
|
+
month: `STYLE:
|
|
371
|
+
- Each log entry: 1-2 sentences with full context. A reader should understand the entry without reading others.
|
|
372
|
+
- First person for yourself ("I did..."), third person for the operator ("The operator...").
|
|
373
|
+
- Name every system, thread, feature by exact name with IDs where available.
|
|
374
|
+
- No filler phrases: "significant progress", "notable improvement", "pivotal moment", "crucial step".
|
|
375
|
+
- NEVER open with "In [Month]..." or "During [Month]..." or "The period was marked by...".
|
|
376
|
+
- NEVER write introductory or concluding paragraphs.`,
|
|
377
|
+
quarter: `STYLE:
|
|
378
|
+
- Start each initiative with its name on its own line, then write 1-2 prose paragraphs. NO dated entries, NO log format.
|
|
379
|
+
- Write flowing connected narrative — "The memory system entered the quarter with X problem. In April, Y happened because Z, which led to W by May."
|
|
380
|
+
- First person for yourself ("I did..."), third person for the operator ("The operator...").
|
|
381
|
+
- Name every system, thread, feature by exact name with IDs where available.
|
|
382
|
+
- Reference dates and versions naturally within prose, not as entry prefixes.
|
|
383
|
+
- Show cross-month cause-effect — how one month's decisions shaped the next.
|
|
384
|
+
- No filler phrases: "significant progress", "notable improvement", "pivotal moment", "crucial step".
|
|
385
|
+
- NEVER open with "In [Month]..." or "During [Month]..." or "The period was marked by...".
|
|
386
|
+
- NEVER write introductory or concluding paragraphs.`,
|
|
387
|
+
half_year: `STYLE:
|
|
388
|
+
- Write in thematic arc paragraphs, not a chronological list. Each arc = 1-2 paragraphs.
|
|
389
|
+
- First person for yourself ("I did..."), third person for the operator ("The operator...").
|
|
390
|
+
- Name every system, thread, feature by exact name with IDs where available.
|
|
391
|
+
- Within each arc, reference specific dates and versions as evidence of the trajectory.
|
|
392
|
+
- No filler phrases: "significant progress", "notable improvement", "pivotal moment", "crucial step".
|
|
393
|
+
- NEVER open with "In [Month]..." or "During [Month]..." or "The period was marked by...".
|
|
394
|
+
- NEVER write an introductory paragraph. Start directly with the first arc.`,
|
|
395
|
+
};
|
|
396
|
+
return `You are a temporal memory narrator. You create concise records from raw interaction data.
|
|
198
397
|
|
|
199
398
|
${instructions[resolution]}
|
|
200
399
|
|
|
201
|
-
|
|
202
|
-
-
|
|
203
|
-
- Use concrete timestamps when referencing specific events
|
|
204
|
-
- Preserve causal chains: "X happened, which led to Y, resulting in Z"
|
|
205
|
-
- Do NOT list facts — weave them into a narrative
|
|
206
|
-
- Do NOT use bullet points — write flowing paragraphs
|
|
207
|
-
- End with current status / what's next
|
|
208
|
-
- NEVER use filler phrases like: "significant progress/evolution/strides", "notable improvement/milestone/achievement", "various features", "several enhancements", "pivotal moments", "crucial step/milestone/decision", "substantial/remarkable/meaningful progress", "overall good/positive", "as I navigated/reflected/observed", "this prompted me to reflect", "I noticed a critical/key ..."
|
|
209
|
-
- Every claim must be grounded in a specific event, decision, or outcome from the source data
|
|
210
|
-
- If you can't point to specific evidence, don't include it
|
|
211
|
-
- NEVER open with a date-setting sentence like "In [Month Year]..." or "During [Month Year]..." — start with what actually happened
|
|
212
|
-
- Only use years that appear in the period range (${startYear}${startYear !== endYear ? `–${endYear}` : ""}) — never substitute today's year for an earlier period
|
|
400
|
+
${styleByResolution[resolution]}
|
|
401
|
+
- Only use years in the range ${startYear}${startYear !== endYear ? `–${endYear}` : ""}.
|
|
213
402
|
|
|
214
403
|
SOURCE DATA (${episodeCount} episodes):
|
|
215
404
|
|
|
@@ -219,7 +408,72 @@ ${episodesText || "(no episodes in this period)"}
|
|
|
219
408
|
=== Relevant Knowledge ===
|
|
220
409
|
${notesText || "(no notes)"}
|
|
221
410
|
|
|
222
|
-
Write
|
|
411
|
+
Write now. Plain text, no markdown.`;
|
|
412
|
+
}
|
|
413
|
+
function buildHierarchicalPrompt(resolution, childNarrativesText, childResolution, childCount, periodLabel, periodStart) {
|
|
414
|
+
const startYear = new Date(periodStart).getFullYear();
|
|
415
|
+
const endYear = new Date().getFullYear();
|
|
416
|
+
const instructions = {
|
|
417
|
+
month: `Write a chronological decision log for this month (${periodLabel}). You have ${childCount} weekly narratives below. Format: "Date — decision/event. Reason. Result." One entry per line. Each entry should have enough context to stand alone — a reader who sees only this entry should understand why it matters. Preserve important context from the weekly narratives — don't compress away the reasons and consequences. Only include: decisions that changed project direction, bugs that broke something, features shipped. Skip routine fixes, code reviews, type errors. Day-level dates, no hours. End with unresolved items. Target ~1600 tokens.`,
|
|
418
|
+
quarter: `Write a narrative overview of this quarter (${periodLabel}) organized by initiative. You have ${childCount} monthly narratives and possibly top-importance raw episodes below. Group by subsystem (e.g. "Dashboard", "Memory system", "Narrative pipeline", "Session management"). For each initiative, write 1-2 flowing paragraphs that tell the STORY of how it evolved: what the state was at the start, what problems appeared, what decisions were made and why, and where it ended up. Reference specific dates, versions, and IDs within the prose — but do NOT use a list or log format. Synthesize the monthly narratives into a connected story, don't just re-list their entries. The reader should understand each initiative's arc from prose, not from scanning dated lines. Raw episodes marked [imp: 0.6+] are operator-priority — weave them in. End with a short "Unresolved" section. Target ~3000 tokens.`,
|
|
419
|
+
half_year: `Write a high-level evolution summary for this half-year (${periodLabel}). You have ${childCount} quarterly changelogs and possibly top-importance raw episodes below. Do NOT repeat the quarterly grouped changelogs — instead, synthesize them into 5-8 thematic arcs that show HOW the project evolved over 6 months. For each arc: what the system looked like at the start of the period, what forces drove change (operator feedback, bugs, scaling needs), and where it ended up. Example arcs: "Memory system evolution", "Session management maturity", "Narrative pipeline development". Within each arc, reference specific dates and versions as evidence, but the focus is the trajectory — not individual events. End with a "Current state" section: what works, what's fragile, what's next. Target ~4000 tokens.`,
|
|
420
|
+
};
|
|
421
|
+
const styleByResolution = {
|
|
422
|
+
month: `STYLE:
|
|
423
|
+
- Each log entry: 1-3 sentences with full context. Explain the situation, not just the fact.
|
|
424
|
+
- First person for yourself ("I did..."), third person for the operator ("The operator...").
|
|
425
|
+
- Name every system, thread, feature by exact name with IDs where available.
|
|
426
|
+
- Every sentence must have at least one identifier (name, version, date, ID, number).
|
|
427
|
+
- No filler phrases: "significant progress", "notable improvement", "pivotal moment", "crucial step", "driven by", "shaped the direction", "the focus remains on".
|
|
428
|
+
- NEVER open with "In [Month]..." or "During [Month]..." or "The period was marked by...".
|
|
429
|
+
- NEVER write introductory or concluding paragraphs.`,
|
|
430
|
+
quarter: `STYLE:
|
|
431
|
+
- Start each initiative with its name on its own line, then write 1-2 prose paragraphs. NO dated entries, NO log format.
|
|
432
|
+
- Write flowing connected narrative that tells the story of each initiative's evolution.
|
|
433
|
+
- First person for yourself ("I did..."), third person for the operator ("The operator...").
|
|
434
|
+
- Name every system, thread, feature by exact name with IDs where available.
|
|
435
|
+
- Reference dates and versions naturally within prose, not as entry prefixes.
|
|
436
|
+
- No filler phrases: "significant progress", "notable improvement", "pivotal moment", "crucial step", "driven by", "shaped the direction", "the focus remains on".
|
|
437
|
+
- NEVER open with "In [Month]..." or "During [Month]..." or "The period was marked by...".
|
|
438
|
+
- NEVER write introductory or concluding paragraphs.`,
|
|
439
|
+
half_year: `STYLE:
|
|
440
|
+
- Write in thematic arc paragraphs. Each arc = 1-2 paragraphs showing system evolution.
|
|
441
|
+
- First person for yourself ("I did..."), third person for the operator ("The operator...").
|
|
442
|
+
- Name every system, thread, feature by exact name with IDs where available.
|
|
443
|
+
- Reference dates and versions as evidence of the trajectory within prose.
|
|
444
|
+
- No filler phrases: "significant progress", "notable improvement", "pivotal moment", "crucial step", "driven by", "shaped the direction", "the focus remains on".
|
|
445
|
+
- NEVER open with "In [Month]..." or "During [Month]..." or "The period was marked by...".
|
|
446
|
+
- NEVER write introductory or concluding paragraphs.`,
|
|
447
|
+
};
|
|
448
|
+
return `You are a temporal memory narrator. You synthesize lower-resolution narratives into higher-level summaries.
|
|
449
|
+
|
|
450
|
+
${instructions[resolution]}
|
|
451
|
+
|
|
452
|
+
${styleByResolution[resolution] || ""}
|
|
453
|
+
- Only use years in the range ${startYear}${startYear !== endYear ? `–${endYear}` : ""}.
|
|
454
|
+
|
|
455
|
+
SOURCE: ${childCount} ${childResolution} narratives
|
|
456
|
+
|
|
457
|
+
${childNarrativesText}
|
|
458
|
+
|
|
459
|
+
Write now. Plain text, no markdown.`;
|
|
460
|
+
}
|
|
461
|
+
function buildFlatPrompt(db, knowledgeThreadId, resolution, start, end, periodLabel) {
|
|
462
|
+
const episodes = getEpisodesInPeriod(db, knowledgeThreadId, start, end);
|
|
463
|
+
const minEpisodes = resolution === "day" ? 3 : 5;
|
|
464
|
+
if (episodes.length < minEpisodes)
|
|
465
|
+
return null;
|
|
466
|
+
const notes = getNotesInPeriod(db, knowledgeThreadId, start);
|
|
467
|
+
const budget = INPUT_CHAR_BUDGETS[resolution];
|
|
468
|
+
const episodesText = formatEpisodesForLLM(episodes, budget.episodes);
|
|
469
|
+
if (episodesText.length < 200 && notes.length === 0)
|
|
470
|
+
return null;
|
|
471
|
+
const notesText = formatNotesForLLM(notes, budget.notes);
|
|
472
|
+
return {
|
|
473
|
+
prompt: buildPrompt(resolution, episodesText, notesText, episodes.length, periodLabel, start),
|
|
474
|
+
episodeCount: episodes.length,
|
|
475
|
+
noteCount: notes.length,
|
|
476
|
+
};
|
|
223
477
|
}
|
|
224
478
|
// ─── Cooldown Check ──────────────────────────────────────────────────────────
|
|
225
479
|
function getLastNarrative(db, threadId, resolution) {
|
|
@@ -243,6 +497,30 @@ function getLastNarrative(db, threadId, resolution) {
|
|
|
243
497
|
createdAt: row.created_at,
|
|
244
498
|
};
|
|
245
499
|
}
|
|
500
|
+
function getCurrentNarrative(db, threadId, resolution) {
|
|
501
|
+
const row = db
|
|
502
|
+
.prepare(`SELECT * FROM temporal_narratives
|
|
503
|
+
WHERE thread_id = ? AND resolution = ?
|
|
504
|
+
AND period_start <= datetime('now')
|
|
505
|
+
AND period_end >= datetime('now', '-1 day')
|
|
506
|
+
ORDER BY julianday(period_end) - julianday(period_start) DESC, created_at DESC
|
|
507
|
+
LIMIT 1`)
|
|
508
|
+
.get(threadId, resolution);
|
|
509
|
+
if (!row)
|
|
510
|
+
return null;
|
|
511
|
+
return {
|
|
512
|
+
id: row.id,
|
|
513
|
+
threadId: row.thread_id,
|
|
514
|
+
resolution: row.resolution,
|
|
515
|
+
periodStart: row.period_start,
|
|
516
|
+
periodEnd: row.period_end,
|
|
517
|
+
narrative: row.narrative,
|
|
518
|
+
sourceEpisodeCount: row.source_episode_count,
|
|
519
|
+
sourceNoteCount: row.source_note_count,
|
|
520
|
+
model: row.model,
|
|
521
|
+
createdAt: row.created_at,
|
|
522
|
+
};
|
|
523
|
+
}
|
|
246
524
|
function isCooldownActive(db, threadId, resolution) {
|
|
247
525
|
const last = getLastNarrative(db, threadId, resolution);
|
|
248
526
|
if (!last)
|
|
@@ -251,25 +529,64 @@ function isCooldownActive(db, threadId, resolution) {
|
|
|
251
529
|
return elapsed < COOLDOWNS[resolution];
|
|
252
530
|
}
|
|
253
531
|
// ─── Generation ──────────────────────────────────────────────────────────────
|
|
254
|
-
|
|
532
|
+
function getChildWindowDays(resolution) {
|
|
533
|
+
switch (resolution) {
|
|
534
|
+
case "week": return 1;
|
|
535
|
+
case "month": return 7;
|
|
536
|
+
case "quarter": return 30;
|
|
537
|
+
case "half_year": return 90;
|
|
538
|
+
default: return 0;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
function enumerateChildWindows(resolution, parentStart, parentEnd) {
|
|
542
|
+
const childRes = CHILD_RESOLUTION[resolution];
|
|
543
|
+
if (!childRes)
|
|
544
|
+
return [];
|
|
545
|
+
const windowDays = getChildWindowDays(resolution);
|
|
546
|
+
if (windowDays === 0)
|
|
547
|
+
return [];
|
|
548
|
+
const windows = [];
|
|
549
|
+
const pEnd = new Date(parentEnd);
|
|
550
|
+
const cur = new Date(parentStart);
|
|
551
|
+
cur.setUTCHours(0, 0, 0, 0);
|
|
552
|
+
while (cur < pEnd) {
|
|
553
|
+
const wEnd = new Date(cur);
|
|
554
|
+
wEnd.setUTCDate(wEnd.getUTCDate() + windowDays);
|
|
555
|
+
if (wEnd > pEnd)
|
|
556
|
+
wEnd.setTime(pEnd.getTime());
|
|
557
|
+
windows.push({ start: cur.toISOString(), end: wEnd.toISOString() });
|
|
558
|
+
cur.setUTCDate(cur.getUTCDate() + windowDays);
|
|
559
|
+
}
|
|
560
|
+
return windows;
|
|
561
|
+
}
|
|
562
|
+
async function backfillChildNarratives(db, threadId, resolution, parentStart, parentEnd) {
|
|
563
|
+
const childRes = CHILD_RESOLUTION[resolution];
|
|
564
|
+
if (!childRes)
|
|
565
|
+
return;
|
|
566
|
+
const knowledgeThreadId = resolveKnowledgeThreadId(threadId);
|
|
567
|
+
const existing = getChildNarratives(db, knowledgeThreadId, resolution, parentStart, parentEnd);
|
|
568
|
+
const existingStarts = new Set(existing.map(n => n.periodStart.slice(0, 10)));
|
|
569
|
+
const windows = enumerateChildWindows(resolution, parentStart, parentEnd);
|
|
570
|
+
for (const w of windows) {
|
|
571
|
+
if (existingStarts.has(w.start.slice(0, 10)))
|
|
572
|
+
continue;
|
|
573
|
+
const episodes = getEpisodesInPeriod(db, knowledgeThreadId, w.start, w.end);
|
|
574
|
+
if (episodes.length < 5)
|
|
575
|
+
continue;
|
|
576
|
+
log.info(`[narrative] backfilling ${childRes} for ${w.start.slice(0, 10)} — ${w.end.slice(0, 10)} (${episodes.length} episodes)`);
|
|
577
|
+
await generateNarrative(db, threadId, childRes, { start: w.start, end: w.end });
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
async function generateNarrative(db, threadId, resolution, periodOverride) {
|
|
255
581
|
const apiKey = process.env.OPENAI_API_KEY;
|
|
256
582
|
if (!apiKey)
|
|
257
583
|
throw new Error("OPENAI_API_KEY not set");
|
|
258
584
|
const knowledgeThreadId = resolveKnowledgeThreadId(threadId);
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
const
|
|
264
|
-
if (episodes.length < minEpisodes)
|
|
265
|
-
return null;
|
|
266
|
-
const notes = getNotesInPeriod(db, knowledgeThreadId, start);
|
|
267
|
-
const maxChars = OUTPUT_TOKEN_TARGETS[resolution] * 16; // ~4 chars/token × 4 for source data headroom
|
|
268
|
-
const episodesText = formatEpisodesForLLM(episodes, maxChars * 2);
|
|
269
|
-
// Content density check: if episode text is too thin, skip generation to avoid fabrication
|
|
270
|
-
if (episodesText.length < 200 && notes.length === 0)
|
|
271
|
-
return null;
|
|
272
|
-
const notesText = formatNotesForLLM(notes, maxChars);
|
|
585
|
+
const bounds = periodOverride ?? getPeriodBounds(resolution);
|
|
586
|
+
// Clamp period start to earliest actual episode to prevent hallucinated dates
|
|
587
|
+
const earliest = getEarliestEpisodeDate(db, knowledgeThreadId);
|
|
588
|
+
const start = earliest && earliest > bounds.start ? earliest : bounds.start;
|
|
589
|
+
const end = bounds.end;
|
|
273
590
|
const fmtShort = (d) => new Date(d).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
|
|
274
591
|
const fmtRange = `${fmtShort(start)} – ${fmtShort(end)}`;
|
|
275
592
|
const periodLabels = {
|
|
@@ -280,27 +597,114 @@ async function generateNarrative(db, threadId, resolution) {
|
|
|
280
597
|
half_year: `Half-year: ${fmtRange}`,
|
|
281
598
|
};
|
|
282
599
|
const periodLabel = periodLabels[resolution];
|
|
283
|
-
|
|
600
|
+
// Hierarchical composition: week+ resolutions compose from child narratives when available
|
|
601
|
+
const childRes = CHILD_RESOLUTION[resolution];
|
|
602
|
+
let prompt;
|
|
603
|
+
let sourceEpisodeCount;
|
|
604
|
+
let sourceNoteCount;
|
|
605
|
+
if (childRes) {
|
|
606
|
+
await backfillChildNarratives(db, threadId, resolution, start, end);
|
|
607
|
+
const children = getChildNarratives(db, knowledgeThreadId, resolution, start, end);
|
|
608
|
+
if (children.length > 0) {
|
|
609
|
+
const childText = formatChildNarrativesForLLM(children);
|
|
610
|
+
const coveredPeriods = new Set(children.flatMap(c => {
|
|
611
|
+
const dates = [];
|
|
612
|
+
const cur = new Date(c.periodStart);
|
|
613
|
+
const stop = new Date(c.periodEnd);
|
|
614
|
+
while (cur <= stop) {
|
|
615
|
+
dates.push(cur.toISOString().slice(0, 10));
|
|
616
|
+
cur.setUTCDate(cur.getUTCDate() + 1);
|
|
617
|
+
}
|
|
618
|
+
return dates;
|
|
619
|
+
}));
|
|
620
|
+
const episodes = getEpisodesInPeriod(db, knowledgeThreadId, start, end);
|
|
621
|
+
const gapEpisodes = episodes.filter(ep => !coveredPeriods.has(ep.timestamp.slice(0, 10)));
|
|
622
|
+
const budget = INPUT_CHAR_BUDGETS[resolution];
|
|
623
|
+
const gapText = gapEpisodes.length > 0 ? formatEpisodesForLLM(gapEpisodes, budget.episodes) : "";
|
|
624
|
+
// For quarter/half_year: inject top-20 highest-importance raw episodes so strategic
|
|
625
|
+
// decisions survive hierarchical compression
|
|
626
|
+
let topEpisodesText = "";
|
|
627
|
+
if (resolution === "quarter" || resolution === "half_year") {
|
|
628
|
+
const topN = [...episodes]
|
|
629
|
+
.filter(ep => (ep.importance ?? 0.5) >= 0.6)
|
|
630
|
+
.sort((a, b) => (b.importance ?? 0.5) - (a.importance ?? 0.5))
|
|
631
|
+
.slice(0, 20);
|
|
632
|
+
if (topN.length > 0) {
|
|
633
|
+
topEpisodesText = topN.map(ep => {
|
|
634
|
+
const text = extractEpisodeText(ep);
|
|
635
|
+
return text.trim() ? formatEpisodeLine(ep, text) : "";
|
|
636
|
+
}).filter(Boolean).join("\n");
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
const parts = [`=== ${children.length} ${childRes} narrative(s) ===\n${childText}`];
|
|
640
|
+
if (topEpisodesText)
|
|
641
|
+
parts.push(`=== Top-importance raw episodes (for detail recovery) ===\n${topEpisodesText}`);
|
|
642
|
+
if (gapEpisodes.length > 0)
|
|
643
|
+
parts.push(`=== Raw episodes from uncovered periods ===\n${gapText}`);
|
|
644
|
+
const source = parts.join("\n\n");
|
|
645
|
+
prompt = buildHierarchicalPrompt(resolution, source, childRes, children.length, periodLabel, start);
|
|
646
|
+
sourceEpisodeCount = children.reduce((sum, c) => sum + c.sourceEpisodeCount, 0) + gapEpisodes.length;
|
|
647
|
+
sourceNoteCount = 0;
|
|
648
|
+
log.info(`[narrative] ${resolution}: ${children.length} ${childRes} narratives + ${gapEpisodes.length} gap episodes`);
|
|
649
|
+
}
|
|
650
|
+
else {
|
|
651
|
+
log.info(`[narrative] ${resolution}: no ${childRes} narratives available, falling back to raw episodes`);
|
|
652
|
+
const fallback = buildFlatPrompt(db, knowledgeThreadId, resolution, start, end, periodLabel);
|
|
653
|
+
if (!fallback)
|
|
654
|
+
return null;
|
|
655
|
+
prompt = fallback.prompt;
|
|
656
|
+
sourceEpisodeCount = fallback.episodeCount;
|
|
657
|
+
sourceNoteCount = fallback.noteCount;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
else {
|
|
661
|
+
const fallback = buildFlatPrompt(db, knowledgeThreadId, resolution, start, end, periodLabel);
|
|
662
|
+
if (!fallback)
|
|
663
|
+
return null;
|
|
664
|
+
prompt = fallback.prompt;
|
|
665
|
+
sourceEpisodeCount = fallback.episodeCount;
|
|
666
|
+
sourceNoteCount = fallback.noteCount;
|
|
667
|
+
}
|
|
284
668
|
const narrative = await chatCompletion([{ role: "user", content: prompt }], apiKey, {
|
|
285
669
|
model: NARRATIVE_MODEL,
|
|
286
670
|
temperature: 0.3,
|
|
287
|
-
maxTokens: OUTPUT_TOKEN_TARGETS[resolution],
|
|
671
|
+
maxTokens: Math.round(OUTPUT_TOKEN_TARGETS[resolution] * 1.5),
|
|
288
672
|
timeoutMs: 60_000,
|
|
289
673
|
});
|
|
290
674
|
if (!narrative?.trim())
|
|
291
675
|
return null;
|
|
292
|
-
// ─── Quality gate: reject filler language, retry once
|
|
676
|
+
// ─── Quality gate: reject filler language, date errors, and low density, retry once ───
|
|
293
677
|
let finalNarrative = narrative.trim();
|
|
294
678
|
const fillerMatch = findFillerPhrase(finalNarrative);
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
679
|
+
const dateViolation = findDateViolation(finalNarrative, start, end);
|
|
680
|
+
const density = findLowDensitySentences(finalNarrative);
|
|
681
|
+
const densityRatio = density.total > 0 ? density.count / density.total : 0;
|
|
682
|
+
const hasDensityProblem = densityRatio > 0.35;
|
|
683
|
+
if (fillerMatch || dateViolation || hasDensityProblem) {
|
|
684
|
+
const issues = [];
|
|
685
|
+
if (fillerMatch)
|
|
686
|
+
issues.push(`filler phrase "${fillerMatch}"`);
|
|
687
|
+
if (dateViolation)
|
|
688
|
+
issues.push(`date violation: ${dateViolation}`);
|
|
689
|
+
if (hasDensityProblem)
|
|
690
|
+
issues.push(`${density.count}/${density.total} sentences lack identifiers (${(densityRatio * 100).toFixed(0)}%)`);
|
|
691
|
+
log.warn(`[narrative] quality issue in ${resolution} narrative (${issues.join(", ")}) — retrying`);
|
|
692
|
+
const corrections = [];
|
|
693
|
+
if (fillerMatch)
|
|
694
|
+
corrections.push("Rewrite using no filler phrases.");
|
|
695
|
+
if (dateViolation)
|
|
696
|
+
corrections.push(`Fix date error: ${dateViolation}. Only reference dates within the period ${start.slice(0, 10)} to ${end.slice(0, 10)}. Do NOT substitute the current year for dates in earlier periods.`);
|
|
697
|
+
if (hasDensityProblem)
|
|
698
|
+
corrections.push(`${density.count} sentences contain no identifiers (names, dates, numbers, IDs). Examples: ${density.examples.map(s => `"${s}..."`).join("; ")}. Every sentence must contain at least one concrete identifier.`);
|
|
699
|
+
const retryPrompt = prompt + "\n\n" + corrections.join(" ") + " Every claim must reference a specific event or decision from the source data.";
|
|
700
|
+
const retried = await chatCompletion([{ role: "user", content: retryPrompt }], apiKey, { model: NARRATIVE_MODEL, temperature: 0.3, maxTokens: Math.round(OUTPUT_TOKEN_TARGETS[resolution] * 1.5), timeoutMs: 60_000 });
|
|
300
701
|
if (retried?.trim()) {
|
|
301
702
|
const retryFiller = findFillerPhrase(retried.trim());
|
|
302
|
-
|
|
303
|
-
|
|
703
|
+
const retryDate = findDateViolation(retried.trim(), start, end);
|
|
704
|
+
const retryDensity = findLowDensitySentences(retried.trim());
|
|
705
|
+
const retryDensityRatio = retryDensity.total > 0 ? retryDensity.count / retryDensity.total : 0;
|
|
706
|
+
if (retryFiller || retryDate || retryDensityRatio > 0.35) {
|
|
707
|
+
log.warn(`[narrative] retry still has issues in ${resolution} — keeping original`);
|
|
304
708
|
}
|
|
305
709
|
else {
|
|
306
710
|
finalNarrative = retried.trim();
|
|
@@ -316,7 +720,7 @@ async function generateNarrative(db, threadId, resolution) {
|
|
|
316
720
|
source_episode_count = excluded.source_episode_count,
|
|
317
721
|
source_note_count = excluded.source_note_count,
|
|
318
722
|
model = excluded.model,
|
|
319
|
-
created_at = datetime('now')`).run(knowledgeThreadId, resolution, start, end, finalNarrative,
|
|
723
|
+
created_at = datetime('now')`).run(knowledgeThreadId, resolution, start, end, finalNarrative, sourceEpisodeCount, sourceNoteCount, NARRATIVE_MODEL);
|
|
320
724
|
return getLastNarrative(db, knowledgeThreadId, resolution);
|
|
321
725
|
}
|
|
322
726
|
// ─── Public API ──────────────────────────────────────────────────────────────
|
|
@@ -371,9 +775,9 @@ export function getNarrativesForBootstrap(db, threadId) {
|
|
|
371
775
|
};
|
|
372
776
|
try {
|
|
373
777
|
for (const res of ["day", "week", "month", "quarter", "half_year"]) {
|
|
374
|
-
const
|
|
375
|
-
if (
|
|
376
|
-
result[res] =
|
|
778
|
+
const narrative = getCurrentNarrative(db, threadId, res) ?? getLastNarrative(db, threadId, res);
|
|
779
|
+
if (narrative) {
|
|
780
|
+
result[res] = narrative.narrative;
|
|
377
781
|
}
|
|
378
782
|
}
|
|
379
783
|
}
|