sensorium-mcp 3.0.54 → 3.0.55
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 +451 -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.map +1 -1
- package/dist/services/dispatcher/lock.js +7 -4
- package/dist/services/dispatcher/lock.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/package.json +1 -1
- package/templates/azure-devops-securevault.default.md +9 -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,59 @@ 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
|
|
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 chronological decision log for this quarter (${periodLabel}). Format: "Date — decision/event. Reason. Result." One entry per line. Each entry should have enough context to stand alone — explain what problem existed before, what decision was made, and what changed after. Only include: decisions that changed project direction, major bugs, features shipped, architectural changes. Skip routine fixes, minor code reviews, type errors. Episodes marked [imp: 0.6+] are operator-priority — include them. Day-level dates, strict chronological order. End with unresolved items. Target ~3000 tokens.`,
|
|
349
|
+
half_year: `Write a chronological decision log for this half-year (${periodLabel}). Format: "Date — decision/event. Reason. Result." One entry per line. Each entry should have enough context to stand alone — explain the situation before, what decision was made, and what changed as a result. Only include: decisions that changed project direction, major bugs, features shipped, architectural changes. Skip routine fixes, minor code reviews, type errors. Episodes marked [imp: 0.6+] are operator-priority — include them. Day-level dates, strict chronological order. End with current state. 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
|
+
- Each log entry: 1-3 sentences with full context. Explain the situation before the decision, not just the decision itself.
|
|
379
|
+
- First person for yourself ("I did..."), third person for the operator ("The operator...").
|
|
380
|
+
- Name every system, thread, feature by exact name with IDs where available.
|
|
381
|
+
- No filler phrases: "significant progress", "notable improvement", "pivotal moment", "crucial step".
|
|
382
|
+
- NEVER open with "In [Month]..." or "During [Month]..." or "The period was marked by...".
|
|
383
|
+
- NEVER write introductory or concluding paragraphs.`,
|
|
384
|
+
half_year: `STYLE:
|
|
385
|
+
- Each log entry: 1-3 sentences with full context. Explain the situation before the decision, not just the decision itself.
|
|
386
|
+
- First person for yourself ("I did..."), third person for the operator ("The operator...").
|
|
387
|
+
- Name every system, thread, feature by exact name with IDs where available.
|
|
388
|
+
- No filler phrases: "significant progress", "notable improvement", "pivotal moment", "crucial step".
|
|
389
|
+
- NEVER open with "In [Month]..." or "During [Month]..." or "The period was marked by...".
|
|
390
|
+
- NEVER write introductory or concluding paragraphs.`,
|
|
391
|
+
};
|
|
392
|
+
return `You are a temporal memory narrator. You create concise records from raw interaction data.
|
|
198
393
|
|
|
199
394
|
${instructions[resolution]}
|
|
200
395
|
|
|
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
|
|
396
|
+
${styleByResolution[resolution]}
|
|
397
|
+
- Only use years in the range ${startYear}${startYear !== endYear ? `–${endYear}` : ""}.
|
|
213
398
|
|
|
214
399
|
SOURCE DATA (${episodeCount} episodes):
|
|
215
400
|
|
|
@@ -219,7 +404,52 @@ ${episodesText || "(no episodes in this period)"}
|
|
|
219
404
|
=== Relevant Knowledge ===
|
|
220
405
|
${notesText || "(no notes)"}
|
|
221
406
|
|
|
222
|
-
Write
|
|
407
|
+
Write now. Plain text, no markdown.`;
|
|
408
|
+
}
|
|
409
|
+
function buildHierarchicalPrompt(resolution, childNarrativesText, childResolution, childCount, periodLabel, periodStart) {
|
|
410
|
+
const startYear = new Date(periodStart).getFullYear();
|
|
411
|
+
const endYear = new Date().getFullYear();
|
|
412
|
+
const instructions = {
|
|
413
|
+
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.`,
|
|
414
|
+
quarter: `Write a chronological decision log for this quarter (${periodLabel}). You have ${childCount} monthly narratives and possibly top-importance raw episodes below. Format: "Date — decision/event. Reason. Result." One entry per line. Each entry should have enough context to stand alone — explain what problem existed before, what decision was made, and what changed after. Don't just extract dates and facts from the monthly narratives — preserve the WHY and the consequences. Only include: decisions that changed project direction, major bugs, features shipped, architectural changes. Raw episodes marked [imp: 0.6+] are operator-priority — include them. Day-level dates, strict chronological order. End with unresolved items. Target ~3000 tokens.`,
|
|
415
|
+
half_year: `Write a chronological decision log for this half-year (${periodLabel}). You have ${childCount} quarterly narratives and possibly top-importance raw episodes below. Format: "Date — decision/event. Reason. Result." One entry per line. Each entry should have enough context to stand alone — explain the situation before, what decision was made, and what changed as a result. Don't just extract dates and facts — preserve the full story behind each entry from the source narratives. Only include: decisions that changed project direction, major bugs, features shipped, architectural changes. Raw episodes marked [imp: 0.6+] are operator-priority — include them. Day-level dates, strict chronological order. End with current state. Target ~4000 tokens.`,
|
|
416
|
+
};
|
|
417
|
+
return `You are a temporal memory narrator. You create concise decision logs by synthesizing lower-resolution narratives.
|
|
418
|
+
|
|
419
|
+
${instructions[resolution]}
|
|
420
|
+
|
|
421
|
+
STYLE:
|
|
422
|
+
- Each log entry: 1-3 sentences with full context. Explain the situation, not just the fact.
|
|
423
|
+
- First person for yourself ("I did..."), third person for the operator ("The operator...").
|
|
424
|
+
- Name every system, thread, feature by exact name with IDs where available.
|
|
425
|
+
- Every sentence must have at least one identifier (name, version, date, ID, number).
|
|
426
|
+
- No filler phrases: "significant progress", "notable improvement", "pivotal moment", "crucial step", "driven by", "shaped the direction", "the focus remains on".
|
|
427
|
+
- NEVER open with "In [Month]..." or "During [Month]..." or "The period was marked by...".
|
|
428
|
+
- NEVER write introductory or concluding paragraphs.
|
|
429
|
+
- Only use years in the range ${startYear}${startYear !== endYear ? `–${endYear}` : ""}.
|
|
430
|
+
|
|
431
|
+
SOURCE: ${childCount} ${childResolution} narratives
|
|
432
|
+
|
|
433
|
+
${childNarrativesText}
|
|
434
|
+
|
|
435
|
+
Write now. Plain text, no markdown.`;
|
|
436
|
+
}
|
|
437
|
+
function buildFlatPrompt(db, knowledgeThreadId, resolution, start, end, periodLabel) {
|
|
438
|
+
const episodes = getEpisodesInPeriod(db, knowledgeThreadId, start, end);
|
|
439
|
+
const minEpisodes = resolution === "day" ? 3 : 5;
|
|
440
|
+
if (episodes.length < minEpisodes)
|
|
441
|
+
return null;
|
|
442
|
+
const notes = getNotesInPeriod(db, knowledgeThreadId, start);
|
|
443
|
+
const budget = INPUT_CHAR_BUDGETS[resolution];
|
|
444
|
+
const episodesText = formatEpisodesForLLM(episodes, budget.episodes);
|
|
445
|
+
if (episodesText.length < 200 && notes.length === 0)
|
|
446
|
+
return null;
|
|
447
|
+
const notesText = formatNotesForLLM(notes, budget.notes);
|
|
448
|
+
return {
|
|
449
|
+
prompt: buildPrompt(resolution, episodesText, notesText, episodes.length, periodLabel, start),
|
|
450
|
+
episodeCount: episodes.length,
|
|
451
|
+
noteCount: notes.length,
|
|
452
|
+
};
|
|
223
453
|
}
|
|
224
454
|
// ─── Cooldown Check ──────────────────────────────────────────────────────────
|
|
225
455
|
function getLastNarrative(db, threadId, resolution) {
|
|
@@ -243,6 +473,30 @@ function getLastNarrative(db, threadId, resolution) {
|
|
|
243
473
|
createdAt: row.created_at,
|
|
244
474
|
};
|
|
245
475
|
}
|
|
476
|
+
function getCurrentNarrative(db, threadId, resolution) {
|
|
477
|
+
const row = db
|
|
478
|
+
.prepare(`SELECT * FROM temporal_narratives
|
|
479
|
+
WHERE thread_id = ? AND resolution = ?
|
|
480
|
+
AND period_start <= datetime('now')
|
|
481
|
+
AND period_end >= datetime('now', '-1 day')
|
|
482
|
+
ORDER BY julianday(period_end) - julianday(period_start) DESC, created_at DESC
|
|
483
|
+
LIMIT 1`)
|
|
484
|
+
.get(threadId, resolution);
|
|
485
|
+
if (!row)
|
|
486
|
+
return null;
|
|
487
|
+
return {
|
|
488
|
+
id: row.id,
|
|
489
|
+
threadId: row.thread_id,
|
|
490
|
+
resolution: row.resolution,
|
|
491
|
+
periodStart: row.period_start,
|
|
492
|
+
periodEnd: row.period_end,
|
|
493
|
+
narrative: row.narrative,
|
|
494
|
+
sourceEpisodeCount: row.source_episode_count,
|
|
495
|
+
sourceNoteCount: row.source_note_count,
|
|
496
|
+
model: row.model,
|
|
497
|
+
createdAt: row.created_at,
|
|
498
|
+
};
|
|
499
|
+
}
|
|
246
500
|
function isCooldownActive(db, threadId, resolution) {
|
|
247
501
|
const last = getLastNarrative(db, threadId, resolution);
|
|
248
502
|
if (!last)
|
|
@@ -251,25 +505,64 @@ function isCooldownActive(db, threadId, resolution) {
|
|
|
251
505
|
return elapsed < COOLDOWNS[resolution];
|
|
252
506
|
}
|
|
253
507
|
// ─── Generation ──────────────────────────────────────────────────────────────
|
|
254
|
-
|
|
508
|
+
function getChildWindowDays(resolution) {
|
|
509
|
+
switch (resolution) {
|
|
510
|
+
case "week": return 1;
|
|
511
|
+
case "month": return 7;
|
|
512
|
+
case "quarter": return 30;
|
|
513
|
+
case "half_year": return 90;
|
|
514
|
+
default: return 0;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
function enumerateChildWindows(resolution, parentStart, parentEnd) {
|
|
518
|
+
const childRes = CHILD_RESOLUTION[resolution];
|
|
519
|
+
if (!childRes)
|
|
520
|
+
return [];
|
|
521
|
+
const windowDays = getChildWindowDays(resolution);
|
|
522
|
+
if (windowDays === 0)
|
|
523
|
+
return [];
|
|
524
|
+
const windows = [];
|
|
525
|
+
const pEnd = new Date(parentEnd);
|
|
526
|
+
const cur = new Date(parentStart);
|
|
527
|
+
cur.setUTCHours(0, 0, 0, 0);
|
|
528
|
+
while (cur < pEnd) {
|
|
529
|
+
const wEnd = new Date(cur);
|
|
530
|
+
wEnd.setUTCDate(wEnd.getUTCDate() + windowDays);
|
|
531
|
+
if (wEnd > pEnd)
|
|
532
|
+
wEnd.setTime(pEnd.getTime());
|
|
533
|
+
windows.push({ start: cur.toISOString(), end: wEnd.toISOString() });
|
|
534
|
+
cur.setUTCDate(cur.getUTCDate() + windowDays);
|
|
535
|
+
}
|
|
536
|
+
return windows;
|
|
537
|
+
}
|
|
538
|
+
async function backfillChildNarratives(db, threadId, resolution, parentStart, parentEnd) {
|
|
539
|
+
const childRes = CHILD_RESOLUTION[resolution];
|
|
540
|
+
if (!childRes)
|
|
541
|
+
return;
|
|
542
|
+
const knowledgeThreadId = resolveKnowledgeThreadId(threadId);
|
|
543
|
+
const existing = getChildNarratives(db, knowledgeThreadId, resolution, parentStart, parentEnd);
|
|
544
|
+
const existingStarts = new Set(existing.map(n => n.periodStart.slice(0, 10)));
|
|
545
|
+
const windows = enumerateChildWindows(resolution, parentStart, parentEnd);
|
|
546
|
+
for (const w of windows) {
|
|
547
|
+
if (existingStarts.has(w.start.slice(0, 10)))
|
|
548
|
+
continue;
|
|
549
|
+
const episodes = getEpisodesInPeriod(db, knowledgeThreadId, w.start, w.end);
|
|
550
|
+
if (episodes.length < 5)
|
|
551
|
+
continue;
|
|
552
|
+
log.info(`[narrative] backfilling ${childRes} for ${w.start.slice(0, 10)} — ${w.end.slice(0, 10)} (${episodes.length} episodes)`);
|
|
553
|
+
await generateNarrative(db, threadId, childRes, { start: w.start, end: w.end });
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
async function generateNarrative(db, threadId, resolution, periodOverride) {
|
|
255
557
|
const apiKey = process.env.OPENAI_API_KEY;
|
|
256
558
|
if (!apiKey)
|
|
257
559
|
throw new Error("OPENAI_API_KEY not set");
|
|
258
560
|
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);
|
|
561
|
+
const bounds = periodOverride ?? getPeriodBounds(resolution);
|
|
562
|
+
// Clamp period start to earliest actual episode to prevent hallucinated dates
|
|
563
|
+
const earliest = getEarliestEpisodeDate(db, knowledgeThreadId);
|
|
564
|
+
const start = earliest && earliest > bounds.start ? earliest : bounds.start;
|
|
565
|
+
const end = bounds.end;
|
|
273
566
|
const fmtShort = (d) => new Date(d).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
|
|
274
567
|
const fmtRange = `${fmtShort(start)} – ${fmtShort(end)}`;
|
|
275
568
|
const periodLabels = {
|
|
@@ -280,27 +573,114 @@ async function generateNarrative(db, threadId, resolution) {
|
|
|
280
573
|
half_year: `Half-year: ${fmtRange}`,
|
|
281
574
|
};
|
|
282
575
|
const periodLabel = periodLabels[resolution];
|
|
283
|
-
|
|
576
|
+
// Hierarchical composition: week+ resolutions compose from child narratives when available
|
|
577
|
+
const childRes = CHILD_RESOLUTION[resolution];
|
|
578
|
+
let prompt;
|
|
579
|
+
let sourceEpisodeCount;
|
|
580
|
+
let sourceNoteCount;
|
|
581
|
+
if (childRes) {
|
|
582
|
+
await backfillChildNarratives(db, threadId, resolution, start, end);
|
|
583
|
+
const children = getChildNarratives(db, knowledgeThreadId, resolution, start, end);
|
|
584
|
+
if (children.length > 0) {
|
|
585
|
+
const childText = formatChildNarrativesForLLM(children);
|
|
586
|
+
const coveredPeriods = new Set(children.flatMap(c => {
|
|
587
|
+
const dates = [];
|
|
588
|
+
const cur = new Date(c.periodStart);
|
|
589
|
+
const stop = new Date(c.periodEnd);
|
|
590
|
+
while (cur <= stop) {
|
|
591
|
+
dates.push(cur.toISOString().slice(0, 10));
|
|
592
|
+
cur.setUTCDate(cur.getUTCDate() + 1);
|
|
593
|
+
}
|
|
594
|
+
return dates;
|
|
595
|
+
}));
|
|
596
|
+
const episodes = getEpisodesInPeriod(db, knowledgeThreadId, start, end);
|
|
597
|
+
const gapEpisodes = episodes.filter(ep => !coveredPeriods.has(ep.timestamp.slice(0, 10)));
|
|
598
|
+
const budget = INPUT_CHAR_BUDGETS[resolution];
|
|
599
|
+
const gapText = gapEpisodes.length > 0 ? formatEpisodesForLLM(gapEpisodes, budget.episodes) : "";
|
|
600
|
+
// For quarter/half_year: inject top-20 highest-importance raw episodes so strategic
|
|
601
|
+
// decisions survive hierarchical compression
|
|
602
|
+
let topEpisodesText = "";
|
|
603
|
+
if (resolution === "quarter" || resolution === "half_year") {
|
|
604
|
+
const topN = [...episodes]
|
|
605
|
+
.filter(ep => (ep.importance ?? 0.5) >= 0.6)
|
|
606
|
+
.sort((a, b) => (b.importance ?? 0.5) - (a.importance ?? 0.5))
|
|
607
|
+
.slice(0, 20);
|
|
608
|
+
if (topN.length > 0) {
|
|
609
|
+
topEpisodesText = topN.map(ep => {
|
|
610
|
+
const text = extractEpisodeText(ep);
|
|
611
|
+
return text.trim() ? formatEpisodeLine(ep, text) : "";
|
|
612
|
+
}).filter(Boolean).join("\n");
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
const parts = [`=== ${children.length} ${childRes} narrative(s) ===\n${childText}`];
|
|
616
|
+
if (topEpisodesText)
|
|
617
|
+
parts.push(`=== Top-importance raw episodes (for detail recovery) ===\n${topEpisodesText}`);
|
|
618
|
+
if (gapEpisodes.length > 0)
|
|
619
|
+
parts.push(`=== Raw episodes from uncovered periods ===\n${gapText}`);
|
|
620
|
+
const source = parts.join("\n\n");
|
|
621
|
+
prompt = buildHierarchicalPrompt(resolution, source, childRes, children.length, periodLabel, start);
|
|
622
|
+
sourceEpisodeCount = children.reduce((sum, c) => sum + c.sourceEpisodeCount, 0) + gapEpisodes.length;
|
|
623
|
+
sourceNoteCount = 0;
|
|
624
|
+
log.info(`[narrative] ${resolution}: ${children.length} ${childRes} narratives + ${gapEpisodes.length} gap episodes`);
|
|
625
|
+
}
|
|
626
|
+
else {
|
|
627
|
+
log.info(`[narrative] ${resolution}: no ${childRes} narratives available, falling back to raw episodes`);
|
|
628
|
+
const fallback = buildFlatPrompt(db, knowledgeThreadId, resolution, start, end, periodLabel);
|
|
629
|
+
if (!fallback)
|
|
630
|
+
return null;
|
|
631
|
+
prompt = fallback.prompt;
|
|
632
|
+
sourceEpisodeCount = fallback.episodeCount;
|
|
633
|
+
sourceNoteCount = fallback.noteCount;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
else {
|
|
637
|
+
const fallback = buildFlatPrompt(db, knowledgeThreadId, resolution, start, end, periodLabel);
|
|
638
|
+
if (!fallback)
|
|
639
|
+
return null;
|
|
640
|
+
prompt = fallback.prompt;
|
|
641
|
+
sourceEpisodeCount = fallback.episodeCount;
|
|
642
|
+
sourceNoteCount = fallback.noteCount;
|
|
643
|
+
}
|
|
284
644
|
const narrative = await chatCompletion([{ role: "user", content: prompt }], apiKey, {
|
|
285
645
|
model: NARRATIVE_MODEL,
|
|
286
646
|
temperature: 0.3,
|
|
287
|
-
maxTokens: OUTPUT_TOKEN_TARGETS[resolution],
|
|
647
|
+
maxTokens: Math.round(OUTPUT_TOKEN_TARGETS[resolution] * 1.5),
|
|
288
648
|
timeoutMs: 60_000,
|
|
289
649
|
});
|
|
290
650
|
if (!narrative?.trim())
|
|
291
651
|
return null;
|
|
292
|
-
// ─── Quality gate: reject filler language, retry once
|
|
652
|
+
// ─── Quality gate: reject filler language, date errors, and low density, retry once ───
|
|
293
653
|
let finalNarrative = narrative.trim();
|
|
294
654
|
const fillerMatch = findFillerPhrase(finalNarrative);
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
655
|
+
const dateViolation = findDateViolation(finalNarrative, start, end);
|
|
656
|
+
const density = findLowDensitySentences(finalNarrative);
|
|
657
|
+
const densityRatio = density.total > 0 ? density.count / density.total : 0;
|
|
658
|
+
const hasDensityProblem = densityRatio > 0.35;
|
|
659
|
+
if (fillerMatch || dateViolation || hasDensityProblem) {
|
|
660
|
+
const issues = [];
|
|
661
|
+
if (fillerMatch)
|
|
662
|
+
issues.push(`filler phrase "${fillerMatch}"`);
|
|
663
|
+
if (dateViolation)
|
|
664
|
+
issues.push(`date violation: ${dateViolation}`);
|
|
665
|
+
if (hasDensityProblem)
|
|
666
|
+
issues.push(`${density.count}/${density.total} sentences lack identifiers (${(densityRatio * 100).toFixed(0)}%)`);
|
|
667
|
+
log.warn(`[narrative] quality issue in ${resolution} narrative (${issues.join(", ")}) — retrying`);
|
|
668
|
+
const corrections = [];
|
|
669
|
+
if (fillerMatch)
|
|
670
|
+
corrections.push("Rewrite using no filler phrases.");
|
|
671
|
+
if (dateViolation)
|
|
672
|
+
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.`);
|
|
673
|
+
if (hasDensityProblem)
|
|
674
|
+
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.`);
|
|
675
|
+
const retryPrompt = prompt + "\n\n" + corrections.join(" ") + " Every claim must reference a specific event or decision from the source data.";
|
|
676
|
+
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
677
|
if (retried?.trim()) {
|
|
301
678
|
const retryFiller = findFillerPhrase(retried.trim());
|
|
302
|
-
|
|
303
|
-
|
|
679
|
+
const retryDate = findDateViolation(retried.trim(), start, end);
|
|
680
|
+
const retryDensity = findLowDensitySentences(retried.trim());
|
|
681
|
+
const retryDensityRatio = retryDensity.total > 0 ? retryDensity.count / retryDensity.total : 0;
|
|
682
|
+
if (retryFiller || retryDate || retryDensityRatio > 0.35) {
|
|
683
|
+
log.warn(`[narrative] retry still has issues in ${resolution} — keeping original`);
|
|
304
684
|
}
|
|
305
685
|
else {
|
|
306
686
|
finalNarrative = retried.trim();
|
|
@@ -316,7 +696,7 @@ async function generateNarrative(db, threadId, resolution) {
|
|
|
316
696
|
source_episode_count = excluded.source_episode_count,
|
|
317
697
|
source_note_count = excluded.source_note_count,
|
|
318
698
|
model = excluded.model,
|
|
319
|
-
created_at = datetime('now')`).run(knowledgeThreadId, resolution, start, end, finalNarrative,
|
|
699
|
+
created_at = datetime('now')`).run(knowledgeThreadId, resolution, start, end, finalNarrative, sourceEpisodeCount, sourceNoteCount, NARRATIVE_MODEL);
|
|
320
700
|
return getLastNarrative(db, knowledgeThreadId, resolution);
|
|
321
701
|
}
|
|
322
702
|
// ─── Public API ──────────────────────────────────────────────────────────────
|
|
@@ -371,9 +751,9 @@ export function getNarrativesForBootstrap(db, threadId) {
|
|
|
371
751
|
};
|
|
372
752
|
try {
|
|
373
753
|
for (const res of ["day", "week", "month", "quarter", "half_year"]) {
|
|
374
|
-
const
|
|
375
|
-
if (
|
|
376
|
-
result[res] =
|
|
754
|
+
const narrative = getCurrentNarrative(db, threadId, res) ?? getLastNarrative(db, threadId, res);
|
|
755
|
+
if (narrative) {
|
|
756
|
+
result[res] = narrative.narrative;
|
|
377
757
|
}
|
|
378
758
|
}
|
|
379
759
|
}
|