Skip to content

Commit cf3c7e5

Browse files
author
Brendan Gray
committed
Context management overhaul: Phases 2-4 (tiered assembly, session persistence, long-term memory)
Phase 2: Budget-aware tiered context assembly - assembleTieredContext() replaces unbounded prompt concatenation - 4-tier system: session summary, hot (current), warm (recent), cold (old) - recordToolResult() stores full output in 50-entry ring buffer - All rotation handlers enhanced with generateRotationSummary() Phase 3: Session persistence and crash recovery - New sessionStore.js: JSON file persistence to userData/sessions/ - 3-second debounced saves, immediate flush on rotation events - Crash recovery: finds recent session within 30min, merges state - Auto-cleanup of sessions older than 24 hours Phase 4: Long-term cross-session memory - New longTermMemory.js: unified index across .guide-memory/ and .ide-memory/ - Keyword-relevance retrieval within token budget for prompt injection - Automatic fact extraction from rollingSummary at conversation end - Live index update when MCP save_memory tool is used - Bridges previously disconnected MCP memory files with prompt injection
1 parent 8b084a6 commit cf3c7e5

4 files changed

Lines changed: 806 additions & 17 deletions

File tree

main/agenticChat.js

Lines changed: 98 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ const {
3434
} = require('./agenticChatHelpers');
3535
const { LLMEngine } = require('./llmEngine');
3636
const { RollingSummary } = require('./rollingSummary');
37+
const { SessionStore } = require('./sessionStore');
38+
const { LongTermMemory } = require('./longTermMemory');
3739
const { repairToolCalls: repairToolCallsFn } = require('./tools/toolParser');
3840

3941
/**
@@ -761,6 +763,13 @@ function register(ctx) {
761763
const memoryContext = memoryStore.getContextPrompt();
762764
if (memoryContext) appendIfBudget('\n' + memoryContext + '\n');
763765

766+
// Long-term memory — cross-session relevant memories
767+
if (longTermMemory) {
768+
const ltmBudget = Math.floor(tokenBudget * 0.08); // 8% of remaining budget
769+
const ltmBlock = longTermMemory.getRelevantMemories(message, ltmBudget);
770+
if (ltmBlock) appendIfBudget('\n' + ltmBlock + '\n');
771+
}
772+
764773
_staticPromptCache.set(cacheKey, prompt);
765774
return prompt;
766775
};
@@ -863,6 +872,40 @@ function register(ctx) {
863872
const rollingSummary = new RollingSummary();
864873
rollingSummary.setGoal(message);
865874

875+
// ── Long-Term Memory (Phase 4) — cross-session memory injection & extraction ──
876+
const longTermMemory = new LongTermMemory();
877+
try { longTermMemory.initialize(context?.projectPath); } catch (e) {
878+
console.warn('[AI Chat] Long-term memory init failed:', e.message);
879+
}
880+
881+
// ── Session Store (Phase 3) — persistent session state for crash recovery ──
882+
const sessionBasePath = path.join(ctx.userDataPath || require('electron').app.getPath('userData'), 'sessions');
883+
const sessionStore = new SessionStore(sessionBasePath);
884+
const sessionId = `${Date.now()}_${message.substring(0, 30).replace(/[^a-z0-9]/gi, '')}`;
885+
const recovered = sessionStore.initialize(sessionId);
886+
if (!recovered) {
887+
// Check for crash recovery from recent session
888+
const recoverable = SessionStore.findRecoverableSession(sessionBasePath);
889+
if (recoverable?.hasRollingSummary) {
890+
const recoveredSummary = sessionStore.initialize(recoverable.sessionId)
891+
? sessionStore.loadRollingSummary(RollingSummary)
892+
: null;
893+
if (recoveredSummary) {
894+
// Merge recovered state into current rolling summary
895+
rollingSummary._completedWork = recoveredSummary._completedWork;
896+
rollingSummary._fileState = recoveredSummary._fileState;
897+
rollingSummary._userCorrections = recoveredSummary._userCorrections;
898+
rollingSummary._keyDecisions = recoveredSummary._keyDecisions;
899+
rollingSummary._currentPlan = recoveredSummary._currentPlan;
900+
rollingSummary._rotationCount = recoveredSummary._rotationCount;
901+
rollingSummary._fullResults = recoveredSummary._fullResults || [];
902+
console.log(`[AI Chat] Recovered session state: ${recoveredSummary._completedWork.length} tool calls, ${recoveredSummary._rotationCount} rotations`);
903+
}
904+
}
905+
}
906+
// Clean up old sessions (async, non-blocking)
907+
try { sessionStore.cleanup(); } catch (_) {}
908+
866909
// Auto-create todos for large/incremental tasks (helps model track progress across rotations)
867910
const autoTodoResult = autoCreateLargeTaskTodos(message, mcpToolServer);
868911
if (autoTodoResult?.success) {
@@ -1158,9 +1201,13 @@ function register(ctx) {
11581201
systemContext: buildStaticPrompt(),
11591202
userMessage: buildDynamicContext(Math.floor(maxPromptTokens * 0.10)) +
11601203
(actionsSummary ? '\n\n' + actionsSummary : '') +
1204+
'\n' + rollingSummary.generateRotationSummary(mcpToolServer?._todos) +
11611205
partialHint +
11621206
'\n\n**Context rotated. Continue the task from where you left off.**\n' + message,
11631207
};
1208+
rollingSummary.markRotation();
1209+
sessionStore.saveRollingSummary(rollingSummary);
1210+
sessionStore.flush();
11641211
continue;
11651212
}
11661213

@@ -1240,10 +1287,14 @@ function register(ctx) {
12401287

12411288
currentPrompt = {
12421289
systemContext: buildStaticPrompt(),
1243-
userMessage: buildDynamicContext() + '\n' + convSummary + hint,
1290+
userMessage: buildDynamicContext() + '\n' + convSummary +
1291+
'\n' + rollingSummary.generateRotationSummary(mcpToolServer?._todos) + hint,
12441292
};
12451293
sessionJustRotated = true;
12461294
lastConvSummary = convSummary;
1295+
rollingSummary.markRotation();
1296+
sessionStore.saveRollingSummary(rollingSummary);
1297+
sessionStore.flush();
12471298
continue;
12481299
} catch (resetErr) {
12491300
console.error('[AI Chat] Context rotation failed:', resetErr.message);
@@ -1492,12 +1543,16 @@ function register(ctx) {
14921543
currentPrompt = {
14931544
systemContext: buildStaticPrompt(),
14941545
userMessage: buildDynamicContext() + '\n\n' + convSummary +
1546+
'\n' + rollingSummary.generateRotationSummary(mcpToolServer?._todos) +
14951547
`\n\n## CONTINUE FROM HERE\n---\n${partialOutput}\n---` +
14961548
incrementalHint + fileProgressHint +
14971549
`\n\n**CRITICAL: DO NOT REFUSE. DO NOT SAY "I cannot continue."**` +
14981550
`\nUse append_to_file to add more content. Call a tool NOW to make progress.`,
14991551
};
15001552
sessionJustRotated = true;
1553+
rollingSummary.markRotation();
1554+
sessionStore.saveRollingSummary(rollingSummary);
1555+
sessionStore.flush();
15011556
continue;
15021557
} catch (rotErr) {
15031558
console.error('[AI Chat] Budget-triggered rotation failed:', rotErr.message);
@@ -1645,12 +1700,16 @@ function register(ctx) {
16451700
currentPrompt = {
16461701
systemContext: buildStaticPrompt(),
16471702
userMessage: buildDynamicContext() + '\n\n' + convSummary +
1703+
'\n' + rollingSummary.generateRotationSummary(mcpToolServer?._todos) +
16481704
`\n\n## CONTINUE FROM HERE\n---\n${partialOutput}\n---` +
16491705
incrementalHint + fileProgressHint + explicitFileHint +
16501706
`\n\n**CRITICAL: DO NOT REFUSE. DO NOT SAY "I cannot continue."**` +
16511707
`\nUse append_to_file to add more content to the same file. Call a tool NOW to make progress.`,
16521708
};
16531709
sessionJustRotated = true;
1710+
rollingSummary.markRotation();
1711+
sessionStore.saveRollingSummary(rollingSummary);
1712+
sessionStore.flush();
16541713
continue;
16551714
} catch (rotErr) {
16561715
console.error('[AI Chat] Large-output rotation failed:', rotErr.message);
@@ -1903,8 +1962,16 @@ function register(ctx) {
19031962
summarizer.markPlanStepCompleted(tr.tool, tr.params);
19041963
executionState.update(tr.tool, tr.params, tr.result, iteration);
19051964
rollingSummary.recordToolCall(tr.tool, tr.params, tr.result, iteration);
1965+
rollingSummary.recordToolResult(tr.tool, tr.params, tr.result, iteration);
1966+
// Notify long-term memory when model saves a memory
1967+
if (tr.tool === 'save_memory' && tr.result?.success && tr.params?.key) {
1968+
longTermMemory.notifySaved(tr.params.key, tr.params.value);
1969+
}
19061970
}
19071971

1972+
// Persist rolling summary to disk (debounced)
1973+
sessionStore.saveRollingSummary(rollingSummary);
1974+
19081975
// UI events — send only non-deferred results to prevent duplicate bubbles
19091976
sendToolExecutionEvents(mainWindow, uiToolResults, playwrightBrowser, { checkSuccess: true });
19101977

@@ -1960,31 +2027,36 @@ function register(ctx) {
19602027
const iterContext = executionBlock + stepDirective + taskReminder;
19612028
const allFeedback = toolFeedback + snapFeedback;
19622029

1963-
// ── Rolling Summary Injection ──
1964-
// Generate context-proportional summary for the next prompt.
1965-
// This ensures the model always has task awareness, not just post-rotation.
1966-
let rollingSummaryBlock = '';
1967-
{
1968-
let _rsCtxUsed = 0;
1969-
try { if (llmEngine.sequence?.nextTokenIndex) _rsCtxUsed = llmEngine.sequence.nextTokenIndex; } catch (_) {}
1970-
if (!_rsCtxUsed) _rsCtxUsed = Math.ceil((fullResponseText.length + (iterContext + allFeedback).length) / 4);
1971-
const _rsCtxPct = _rsCtxUsed / totalCtx;
1972-
if (rollingSummary.shouldInjectSummary(iteration, _rsCtxPct)) {
1973-
const summaryBudget = rollingSummary.getSummaryBudget(totalCtx, _rsCtxPct);
1974-
rollingSummaryBlock = rollingSummary.generateSummary(summaryBudget);
1975-
}
1976-
}
2030+
// ── Budget-Aware Tiered Context Assembly (Phase 2) ──
2031+
// Instead of dumping raw feedback + rolling summary separately,
2032+
// assemble a single context block within calculated token budget.
2033+
// HOT tier: current iteration results (full)
2034+
// WARM tier: recent iterations (compressed)
2035+
// COLD tier: old iterations (bullets)
2036+
const dynamicCtx = buildDynamicContext();
2037+
const staticTokens = estimateTokens(basePrompt);
2038+
const dynamicTokens = estimateTokens(dynamicCtx);
2039+
const iterTokens = estimateTokens(iterContext);
2040+
const contTokens = estimateTokens(continueInstruction);
2041+
const availableBudget = Math.max(
2042+
maxPromptTokens - staticTokens - dynamicTokens - iterTokens - contTokens - 100,
2043+
200
2044+
);
19772045

19782046
if (sessionJustRotated) {
19792047
sessionJustRotated = false;
2048+
const rotSummaryTokens = estimateTokens(lastConvSummary);
2049+
const rotBudget = Math.max(availableBudget - rotSummaryTokens, 200);
2050+
const assembledContext = rollingSummary.assembleTieredContext(rotBudget, iteration, allFeedback);
19802051
currentPrompt = {
19812052
systemContext: buildStaticPrompt(),
1982-
userMessage: iterContext + buildDynamicContext() + '\n' + lastConvSummary + `\nLatest results:\n${allFeedback.substring(0, 6000)}${continueInstruction}`,
2053+
userMessage: iterContext + dynamicCtx + '\n' + lastConvSummary + '\n' + assembledContext + continueInstruction,
19832054
};
19842055
} else {
2056+
const assembledContext = rollingSummary.assembleTieredContext(availableBudget, iteration, allFeedback);
19852057
currentPrompt = {
19862058
systemContext: buildStaticPrompt(),
1987-
userMessage: iterContext + buildDynamicContext() + (rollingSummaryBlock ? '\n' + rollingSummaryBlock : '') + '\n' + allFeedback + continueInstruction,
2059+
userMessage: iterContext + dynamicCtx + '\n' + assembledContext + continueInstruction,
19882060
};
19892061
}
19902062
}
@@ -2022,6 +2094,15 @@ function register(ctx) {
20222094

20232095
memoryStore.addConversation('assistant', fullResponseText);
20242096

2097+
// Extract and save long-term memories from this conversation
2098+
try { longTermMemory.extractAndSave(rollingSummary, message); } catch (e) {
2099+
console.warn('[AI Chat] Long-term memory extraction failed:', e.message);
2100+
}
2101+
2102+
// Flush session store on conversation end
2103+
sessionStore.saveRollingSummary(rollingSummary);
2104+
sessionStore.flush();
2105+
20252106
// Clean display text
20262107
let cleanResponse = displayResponseText;
20272108
cleanResponse = cleanResponse.replace(/<think(?:ing)?>\s*[\s\S]*?<\/think(?:ing)?>/gi, '');

0 commit comments

Comments
 (0)