@@ -34,6 +34,8 @@ const {
3434} = require ( './agenticChatHelpers' ) ;
3535const { LLMEngine } = require ( './llmEngine' ) ;
3636const { RollingSummary } = require ( './rollingSummary' ) ;
37+ const { SessionStore } = require ( './sessionStore' ) ;
38+ const { LongTermMemory } = require ( './longTermMemory' ) ;
3739const { 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 - z 0 - 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 ( / < t h i n k (?: i n g ) ? > \s * [ \s \S ] * ?< \/ t h i n k (?: i n g ) ? > / gi, '' ) ;
0 commit comments