/ services / SessionMemory / sessionMemoryUtils.ts
sessionMemoryUtils.ts
  1  /**
  2   * Session Memory utility functions that can be imported without circular dependencies.
  3   * These are separate from the main sessionMemory.ts to avoid importing runAgent.
  4   */
  5  
  6  import { isFsInaccessible } from '../../utils/errors.js'
  7  import { getFsImplementation } from '../../utils/fsOperations.js'
  8  import { getSessionMemoryPath } from '../../utils/permissions/filesystem.js'
  9  import { sleep } from '../../utils/sleep.js'
 10  import { logEvent } from '../analytics/index.js'
 11  
 12  const EXTRACTION_WAIT_TIMEOUT_MS = 15000
 13  const EXTRACTION_STALE_THRESHOLD_MS = 60000 // 1 minute
 14  
 15  /**
 16   * Configuration for session memory extraction thresholds
 17   */
 18  export type SessionMemoryConfig = {
 19    /** Minimum context window tokens before initializing session memory.
 20     * Uses the same token counting as autocompact (input + output + cache tokens)
 21     * to ensure consistent behavior between the two features. */
 22    minimumMessageTokensToInit: number
 23    /** Minimum context window growth (in tokens) between session memory updates.
 24     * Uses the same token counting as autocompact (tokenCountWithEstimation)
 25     * to measure actual context growth, not cumulative API usage. */
 26    minimumTokensBetweenUpdate: number
 27    /** Number of tool calls between session memory updates */
 28    toolCallsBetweenUpdates: number
 29  }
 30  
 31  // Default configuration values
 32  export const DEFAULT_SESSION_MEMORY_CONFIG: SessionMemoryConfig = {
 33    minimumMessageTokensToInit: 10000,
 34    minimumTokensBetweenUpdate: 5000,
 35    toolCallsBetweenUpdates: 3,
 36  }
 37  
 38  // Current session memory configuration
 39  let sessionMemoryConfig: SessionMemoryConfig = {
 40    ...DEFAULT_SESSION_MEMORY_CONFIG,
 41  }
 42  
 43  // Track the last summarized message ID (shared state)
 44  let lastSummarizedMessageId: string | undefined
 45  
 46  // Track extraction state with timestamp (set by sessionMemory.ts)
 47  let extractionStartedAt: number | undefined
 48  
 49  // Track context size at last memory extraction (for minimumTokensBetweenUpdate)
 50  let tokensAtLastExtraction = 0
 51  
 52  // Track whether session memory has been initialized (met minimumMessageTokensToInit)
 53  let sessionMemoryInitialized = false
 54  
 55  /**
 56   * Get the message ID up to which the session memory is current
 57   */
 58  export function getLastSummarizedMessageId(): string | undefined {
 59    return lastSummarizedMessageId
 60  }
 61  
 62  /**
 63   * Set the last summarized message ID (called from sessionMemory.ts)
 64   */
 65  export function setLastSummarizedMessageId(
 66    messageId: string | undefined,
 67  ): void {
 68    lastSummarizedMessageId = messageId
 69  }
 70  
 71  /**
 72   * Mark extraction as started (called from sessionMemory.ts)
 73   */
 74  export function markExtractionStarted(): void {
 75    extractionStartedAt = Date.now()
 76  }
 77  
 78  /**
 79   * Mark extraction as completed (called from sessionMemory.ts)
 80   */
 81  export function markExtractionCompleted(): void {
 82    extractionStartedAt = undefined
 83  }
 84  
 85  /**
 86   * Wait for any in-progress session memory extraction to complete (with 15s timeout)
 87   * Returns immediately if no extraction is in progress or if extraction is stale (>1min old).
 88   */
 89  export async function waitForSessionMemoryExtraction(): Promise<void> {
 90    const startTime = Date.now()
 91    while (extractionStartedAt) {
 92      const extractionAge = Date.now() - extractionStartedAt
 93      if (extractionAge > EXTRACTION_STALE_THRESHOLD_MS) {
 94        // Extraction is stale, don't wait
 95        return
 96      }
 97  
 98      if (Date.now() - startTime > EXTRACTION_WAIT_TIMEOUT_MS) {
 99        // Timeout - continue anyway
100        return
101      }
102  
103      await sleep(1000)
104    }
105  }
106  
107  /**
108   * Get the current session memory content
109   */
110  export async function getSessionMemoryContent(): Promise<string | null> {
111    const fs = getFsImplementation()
112    const memoryPath = getSessionMemoryPath()
113  
114    try {
115      const content = await fs.readFile(memoryPath, { encoding: 'utf-8' })
116  
117      logEvent('tengu_session_memory_loaded', {
118        content_length: content.length,
119      })
120  
121      return content
122    } catch (e: unknown) {
123      if (isFsInaccessible(e)) return null
124      throw e
125    }
126  }
127  
128  /**
129   * Set the session memory configuration
130   */
131  export function setSessionMemoryConfig(
132    config: Partial<SessionMemoryConfig>,
133  ): void {
134    sessionMemoryConfig = {
135      ...sessionMemoryConfig,
136      ...config,
137    }
138  }
139  
140  /**
141   * Get the current session memory configuration
142   */
143  export function getSessionMemoryConfig(): SessionMemoryConfig {
144    return { ...sessionMemoryConfig }
145  }
146  
147  /**
148   * Record the context size at the time of extraction.
149   * Used to measure context growth for minimumTokensBetweenUpdate threshold.
150   */
151  export function recordExtractionTokenCount(currentTokenCount: number): void {
152    tokensAtLastExtraction = currentTokenCount
153  }
154  
155  /**
156   * Check if session memory has been initialized (met minimumTokensToInit threshold)
157   */
158  export function isSessionMemoryInitialized(): boolean {
159    return sessionMemoryInitialized
160  }
161  
162  /**
163   * Mark session memory as initialized
164   */
165  export function markSessionMemoryInitialized(): void {
166    sessionMemoryInitialized = true
167  }
168  
169  /**
170   * Check if we've met the threshold to initialize session memory.
171   * Uses total context window tokens (same as autocompact) for consistent behavior.
172   */
173  export function hasMetInitializationThreshold(
174    currentTokenCount: number,
175  ): boolean {
176    return currentTokenCount >= sessionMemoryConfig.minimumMessageTokensToInit
177  }
178  
179  /**
180   * Check if we've met the threshold for the next update.
181   * Measures actual context window growth since last extraction
182   * (same metric as autocompact and initialization threshold).
183   */
184  export function hasMetUpdateThreshold(currentTokenCount: number): boolean {
185    const tokensSinceLastExtraction = currentTokenCount - tokensAtLastExtraction
186    return (
187      tokensSinceLastExtraction >= sessionMemoryConfig.minimumTokensBetweenUpdate
188    )
189  }
190  
191  /**
192   * Get the configured number of tool calls between updates
193   */
194  export function getToolCallsBetweenUpdates(): number {
195    return sessionMemoryConfig.toolCallsBetweenUpdates
196  }
197  
198  /**
199   * Reset session memory state (useful for testing)
200   */
201  export function resetSessionMemoryState(): void {
202    sessionMemoryConfig = { ...DEFAULT_SESSION_MEMORY_CONFIG }
203    tokensAtLastExtraction = 0
204    sessionMemoryInitialized = false
205    lastSummarizedMessageId = undefined
206    extractionStartedAt = undefined
207  }