@@ -289,3 +289,305 @@ export function compressConversation(
289289 savings : Math . max ( 0 , savings ) , // Ensure non-negative
290290 } ;
291291}
292+
293+ // ============================================
294+ // Memory Management (Phase 6.2)
295+ // ============================================
296+
297+ /**
298+ * Decision extracted from conversation
299+ */
300+ export interface Decision {
301+ decision : string ;
302+ context : string ;
303+ timestamp : number ;
304+ }
305+
306+ /**
307+ * Code reference from conversation
308+ */
309+ export interface CodeReference {
310+ file : string ;
311+ element ?: string ;
312+ action : "created" | "modified" | "discussed" | "deleted" ;
313+ }
314+
315+ /**
316+ * Conversation memory state
317+ */
318+ export interface ConversationMemory {
319+ /** Rolling summary of conversation */
320+ summary : string ;
321+ /** Key decisions made */
322+ decisions : Decision [ ] ;
323+ /** Code references mentioned */
324+ codeReferences : CodeReference [ ] ;
325+ /** Compressed message history */
326+ compressedHistory : ConversationMessage [ ] ;
327+ /** Timestamp of last update */
328+ lastUpdated : number ;
329+ }
330+
331+ /**
332+ * Options for memory restoration
333+ */
334+ export interface MemoryRestoreOptions {
335+ /** Include full summary */
336+ includeSummary ?: boolean ;
337+ /** Number of recent messages to include */
338+ recentMessages ?: number ;
339+ /** Include code references */
340+ includeCodeRefs ?: boolean ;
341+ /** Include decisions */
342+ includeDecisions ?: boolean ;
343+ }
344+
345+ /**
346+ * Result of conversation memory operation
347+ */
348+ export interface ConversationMemoryResult {
349+ /** Restored context string */
350+ context : string ;
351+ /** Memory state */
352+ memory : ConversationMemory ;
353+ /** Statistics */
354+ stats : {
355+ originalTokens : number ;
356+ compressedTokens : number ;
357+ decisionsExtracted : number ;
358+ codeRefsFound : number ;
359+ } ;
360+ }
361+
362+ /**
363+ * Extract decisions from messages with enhanced patterns
364+ */
365+ export function extractDecisions ( messages : ConversationMessage [ ] ) : Decision [ ] {
366+ const decisions : Decision [ ] = [ ] ;
367+ const now = Date . now ( ) ;
368+
369+ // Decision patterns
370+ const decisionPatterns = [
371+ // Direct decisions
372+ / (?: d e c i d e d | w i l l | g o i n g t o | l e t ' s | I ' l l | w e ' l l | w e s h o u l d ) \s + ( .{ 10 , 150 } ) / gi,
373+ // Plans and approaches
374+ / (?: t h e a p p r o a c h | t h e s o l u t i o n | t h e p l a n | s t r a t e g y ) \s + (?: i s | w i l l b e ) \s + ( .{ 10 , 150 } ) / gi,
375+ // Requirements
376+ / (?: w e n e e d t o | m u s t | s h o u l d | h a v e t o ) \s + ( .{ 10 , 150 } ) / gi,
377+ // Completed actions
378+ / (?: d o n e | c o m p l e t e d | f i n i s h e d | i m p l e m e n t e d | c r e a t e d | f i x e d | u p d a t e d ) : \s * ( .{ 10 , 150 } ) / gi,
379+ ] ;
380+
381+ for ( const msg of messages ) {
382+ if ( msg . role === "system" ) continue ;
383+
384+ const lines = msg . content . split ( "\n" ) ;
385+
386+ for ( const line of lines ) {
387+ const trimmed = line . trim ( ) ;
388+ if ( trimmed . length < 15 || trimmed . length > 300 ) continue ;
389+
390+ for ( const pattern of decisionPatterns ) {
391+ pattern . lastIndex = 0 ; // Reset regex state
392+ const match = pattern . exec ( trimmed ) ;
393+ if ( match && match [ 1 ] ) {
394+ const decision = match [ 1 ] . trim ( ) ;
395+ // Avoid duplicates
396+ if ( ! decisions . some ( ( d ) => d . decision === decision ) ) {
397+ decisions . push ( {
398+ decision,
399+ context : trimmed . slice ( 0 , 200 ) ,
400+ timestamp : now ,
401+ } ) ;
402+ }
403+ }
404+ }
405+
406+ // Also check for numbered list items that look like decisions
407+ if ( / ^ \d + \. \s + / . test ( trimmed ) ) {
408+ const item = trimmed . replace ( / ^ \d + \. \s + / , "" ) . trim ( ) ;
409+ if (
410+ item . length > 15 &&
411+ item . length < 200 &&
412+ ! decisions . some ( ( d ) => d . decision === item )
413+ ) {
414+ decisions . push ( {
415+ decision : item ,
416+ context : trimmed ,
417+ timestamp : now ,
418+ } ) ;
419+ }
420+ }
421+ }
422+ }
423+
424+ // Limit to 20 most recent decisions
425+ return decisions . slice ( - 20 ) ;
426+ }
427+
428+ /**
429+ * Extract code references from messages
430+ */
431+ export function extractCodeReferences (
432+ messages : ConversationMessage [ ]
433+ ) : CodeReference [ ] {
434+ const refs : CodeReference [ ] = [ ] ;
435+ const seenFiles = new Set < string > ( ) ;
436+
437+ // File path pattern
438+ const filePattern =
439+ / [ a - z A - Z 0 - 9 _ \- . / ] + \. ( t s | j s | t s x | j s x | p y | g o | r s | j s o n | y a m l | y m l | m d | c s s | s c s s | h t m l ) \b / g;
440+
441+ // Action patterns
442+ const actionPatterns : Array < { pattern : RegExp ; action : CodeReference [ "action" ] } > = [
443+ { pattern : / \b ( c r e a t e d ? | a d d (?: e d | i n g ) ? | w r i t (?: e | i n g | t e n ) ) \b / i, action : "created" } ,
444+ { pattern : / \b ( m o d i f (?: y | i e d | y i n g ) | u p d a t (?: e | e d | i n g ) | c h a n g (?: e | e d | i n g ) | e d i t (?: e d | i n g ) ? ) \b / i, action : "modified" } ,
445+ { pattern : / \b ( d e l e t (?: e | e d | i n g ) | r e m o v (?: e | e d | i n g ) ) \b / i, action : "deleted" } ,
446+ ] ;
447+
448+ for ( const msg of messages ) {
449+ const content = msg . content ;
450+ let match ;
451+
452+ while ( ( match = filePattern . exec ( content ) ) !== null ) {
453+ const file = match [ 0 ] ;
454+ if ( seenFiles . has ( file ) ) continue ;
455+ seenFiles . add ( file ) ;
456+
457+ // Find surrounding context to determine action
458+ const start = Math . max ( 0 , match . index - 50 ) ;
459+ const end = Math . min ( content . length , match . index + file . length + 50 ) ;
460+ const context = content . slice ( start , end ) . toLowerCase ( ) ;
461+
462+ let action : CodeReference [ "action" ] = "discussed" ;
463+ for ( const { pattern, action : act } of actionPatterns ) {
464+ if ( pattern . test ( context ) ) {
465+ action = act ;
466+ break ;
467+ }
468+ }
469+
470+ refs . push ( { file, action } ) ;
471+ }
472+ }
473+
474+ // Limit to 30 most relevant
475+ return refs . slice ( 0 , 30 ) ;
476+ }
477+
478+ /**
479+ * Create conversation memory from messages
480+ */
481+ export function createMemory (
482+ messages : ConversationMessage [ ] ,
483+ options : ConversationCompressOptions
484+ ) : ConversationMemory {
485+ const summary = createRollingSummary ( messages ) ;
486+ const decisions = extractDecisions ( messages ) ;
487+ const codeReferences = extractCodeReferences ( messages ) ;
488+
489+ // Compress messages using hybrid strategy
490+ const result = compressConversation ( messages , {
491+ ...options ,
492+ strategy : "hybrid" ,
493+ } ) ;
494+
495+ return {
496+ summary,
497+ decisions,
498+ codeReferences,
499+ compressedHistory : result . compressedMessages ,
500+ lastUpdated : Date . now ( ) ,
501+ } ;
502+ }
503+
504+ /**
505+ * Restore context from memory state
506+ */
507+ export function restoreContext (
508+ memory : ConversationMemory ,
509+ options : MemoryRestoreOptions = { }
510+ ) : string {
511+ const {
512+ includeSummary = true ,
513+ recentMessages = 3 ,
514+ includeCodeRefs = true ,
515+ includeDecisions = true ,
516+ } = options ;
517+
518+ const parts : string [ ] = [ ] ;
519+
520+ // Add summary
521+ if ( includeSummary && memory . summary ) {
522+ parts . push ( "[Previous Context]" ) ;
523+ parts . push ( memory . summary ) ;
524+ parts . push ( "" ) ;
525+ }
526+
527+ // Add decisions
528+ if ( includeDecisions && memory . decisions . length > 0 ) {
529+ parts . push ( "[Key Decisions]" ) ;
530+ for ( const decision of memory . decisions . slice ( - 10 ) ) {
531+ parts . push ( `- ${ decision . decision } ` ) ;
532+ }
533+ parts . push ( "" ) ;
534+ }
535+
536+ // Add code references
537+ if ( includeCodeRefs && memory . codeReferences . length > 0 ) {
538+ parts . push ( "[Code References]" ) ;
539+ const byAction = new Map < string , string [ ] > ( ) ;
540+
541+ for ( const ref of memory . codeReferences ) {
542+ if ( ! byAction . has ( ref . action ) ) {
543+ byAction . set ( ref . action , [ ] ) ;
544+ }
545+ byAction . get ( ref . action ) ! . push ( ref . file ) ;
546+ }
547+
548+ for ( const [ action , files ] of byAction ) {
549+ parts . push ( `${ action } : ${ files . slice ( 0 , 10 ) . join ( ", " ) } ` ) ;
550+ }
551+ parts . push ( "" ) ;
552+ }
553+
554+ // Add recent messages
555+ if ( recentMessages > 0 && memory . compressedHistory . length > 0 ) {
556+ parts . push ( "[Recent Messages]" ) ;
557+ const recent = memory . compressedHistory . slice ( - recentMessages ) ;
558+ for ( const msg of recent ) {
559+ const preview = msg . content . slice ( 0 , 200 ) ;
560+ parts . push ( `${ msg . role } : ${ preview } ${ msg . content . length > 200 ? "..." : "" } ` ) ;
561+ }
562+ }
563+
564+ return parts . join ( "\n" ) ;
565+ }
566+
567+ /**
568+ * Compress conversation and create memory result
569+ */
570+ export function compressConversationWithMemory (
571+ messages : ConversationMessage [ ] ,
572+ options : ConversationCompressOptions
573+ ) : ConversationMemoryResult {
574+ const originalTokens = messages . reduce (
575+ ( sum , m ) => sum + countTokens ( m . content ) ,
576+ 0
577+ ) ;
578+
579+ const memory = createMemory ( messages , options ) ;
580+ const context = restoreContext ( memory ) ;
581+ const compressedTokens = countTokens ( context ) ;
582+
583+ return {
584+ context,
585+ memory,
586+ stats : {
587+ originalTokens,
588+ compressedTokens,
589+ decisionsExtracted : memory . decisions . length ,
590+ codeRefsFound : memory . codeReferences . length ,
591+ } ,
592+ } ;
593+ }
0 commit comments