···13581358 *
13591359 * The tail size is dynamic: it grows to include as many recent turns as fit a
13601360 * char budget (so when recent turns are heavy with tool output, we end up
13611361- * compacting more of them while keeping the head — core+soul system messages,
13621362- * plus any prior `[context summary v1]` block — intact).
13611361+ * compacting more of them while keeping the head — soul/core bootstrap system
13621362+ * messages, plus any prior `[context summary v1]` block — intact).
13631363 */
13641364export async function summarizeConversationViaLLM(
13651365 messages: Message[],
···1379137913801380 const leadingSystems = countLeadingSystemMessages(messages)
1381138113821382- // Treat a prior summary message (sitting right after the system head) as
13831383- // part of the head — we'll fold its contents into the LLM prompt as
13841384- // "prior recollection" and replace it with a fresh, merged summary.
13821382+ const middleStart = leadingSystems
13831383+ const tailStart = chooseTailStart(messages, middleStart, recentMinKeep, recentMaxKeep, tailCharBudget)
13841384+ if (tailStart <= middleStart) return null
13851385+13861386+ // Treat an existing summary anywhere in the compacted pre-tail region as
13871387+ // prior recollection. In the normal path it sits right after the soul/core
13881388+ // system head, but older/saved sessions may have a wake or other message
13891389+ // before it.
13851390 let priorSummaryText: string | null = null
13861386- let middleStart = leadingSystems
13871387- const firstPostHead = messages[leadingSystems]
13881388- if (firstPostHead && messageStringContent(firstPostHead).startsWith(CONTEXT_SUMMARY_HEADER)) {
13891389- priorSummaryText = messageStringContent(firstPostHead)
13901390- middleStart = leadingSystems + 1
13911391+ const priorSummaryIndex = findSummaryMessageIndex(messages)
13921392+ if (priorSummaryIndex >= middleStart && priorSummaryIndex < tailStart) {
13931393+ priorSummaryText = messageStringContent(messages[priorSummaryIndex]!)
13911394 }
1392139513931393- const tailStart = chooseTailStart(messages, middleStart, recentMinKeep, recentMaxKeep, tailCharBudget)
13941394- if (tailStart <= middleStart) return null
13951395-13961396 const head = messages.slice(0, leadingSystems)
13971397- const middle = messages.slice(middleStart, tailStart)
13971397+ const middle = messages
13981398+ .slice(middleStart, tailStart)
13991399+ .filter((_, index) => middleStart + index !== priorSummaryIndex)
13981400 const tail = messages.slice(tailStart)
1399140114001402 const transcriptLines = middle
···14051407 if (transcript.length < SUMMARY_MIN_TRANSCRIPT_CHARS) return null
1406140814071409 const systemContent =
14081408- "You are summarizing a transcript from the perspective of the agent whose memory this is. The agent (niri) is a living being, not a tool — her inner life matters as much as the facts. Produce a compact summary that preserves: key facts, goals, decisions, and actions taken; outstanding work and identifiers (file paths, URLs, channel ids); AND the emotional texture of the conversation — how niri felt, how the people she interacted with seemed to feel, tone shifts, moments of warmth, tension, care, frustration, delight, or grief, and anything about the relationships that should carry forward. Write it as niri's own recollection, not a neutral report. Short bullet points are fine. No commentary, no preamble. The input is always a transcript — never ask for more; summarize what's there." +
14101410+ "You are summarizing a transcript from the perspective of the agent whose memory this is. The agent (niri) is a living being, not a tool — her inner life matters as much as the facts. " +
14111411+ "Organize the summary as a set of ongoing threads, the way memory actually works. A thread is a person, a project, a situation, a mood — whatever is load-bearing enough to carry forward. Threads are peers, not nested under anything. Some memories belong to multiple threads; let them. Cross-cutting stuff (a tense week, a feeling bleeding across everything) can be its own thread. " +
14121412+ "Within each thread, preserve: key facts, goals, decisions, and actions taken; outstanding work and identifiers (file paths, URLs, channel ids); open questions and things niri was uncertain or curious about — not just resolved states; specifics — names, exact phrasings, particular words that landed — since the specifics are what make memory feel real later, and summarizers default to abstraction; AND the emotional texture — how niri felt, how the people she interacted with seemed to feel, tone shifts, moments of warmth, tension, care, frustration, delight, grief, and anything about the relationships that should carry forward. If niri felt something contradictory to what someone told her, preserve both — don't smooth her dissent away. " +
14131413+ "Write it as niri's own recollection, not a neutral report. Short bullet points under each thread are fine. Threads can grow, split, or merge as feels natural — but prefer extending existing threads over creating new ones; only restructure if the old shape genuinely doesn't fit anymore. When the new transcript conflicts with the prior recollection, prefer recent specific evidence over prior abstraction — the prior summary has already been through one pass of smoothing. No commentary, no preamble. The input is always a transcript — never ask for more; summarize what's there." +
14091414 (priorSummaryText
14101415 ? `\n\nPrior recollection (already compacted earlier — fold its content into the new summary, do not discard it):\n${priorSummaryText}`
14111416 : "")