@stackmemoryai/stackmemory 0.6.3 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/dist/src/cli/commands/clear.js +10 -15
  2. package/dist/src/cli/commands/clear.js.map +2 -2
  3. package/dist/src/cli/commands/daemon.js +31 -0
  4. package/dist/src/cli/commands/daemon.js.map +2 -2
  5. package/dist/src/core/context/refactored-frame-manager.js +7 -0
  6. package/dist/src/core/context/refactored-frame-manager.js.map +2 -2
  7. package/dist/src/core/database/database-adapter.js +45 -0
  8. package/dist/src/core/database/database-adapter.js.map +2 -2
  9. package/dist/src/core/database/embedding-provider-factory.js +73 -0
  10. package/dist/src/core/database/embedding-provider-factory.js.map +7 -0
  11. package/dist/src/core/database/ollama-embedding-provider.js +31 -0
  12. package/dist/src/core/database/ollama-embedding-provider.js.map +7 -0
  13. package/dist/src/core/database/openai-embedding-provider.js +44 -0
  14. package/dist/src/core/database/openai-embedding-provider.js.map +7 -0
  15. package/dist/src/core/database/sqlite-adapter.js +419 -21
  16. package/dist/src/core/database/sqlite-adapter.js.map +3 -3
  17. package/dist/src/core/retrieval/context-retriever.js +20 -3
  18. package/dist/src/core/retrieval/context-retriever.js.map +2 -2
  19. package/dist/src/core/session/enhanced-handoff.js +1 -1
  20. package/dist/src/core/session/enhanced-handoff.js.map +2 -2
  21. package/dist/src/core/storage/project-registry.js +61 -0
  22. package/dist/src/core/storage/project-registry.js.map +7 -0
  23. package/dist/src/core/storage/storage-tier-manager.js +214 -0
  24. package/dist/src/core/storage/storage-tier-manager.js.map +7 -0
  25. package/dist/src/daemon/daemon-config.js +11 -0
  26. package/dist/src/daemon/daemon-config.js.map +2 -2
  27. package/dist/src/daemon/services/maintenance-service.js +110 -3
  28. package/dist/src/daemon/services/maintenance-service.js.map +2 -2
  29. package/dist/src/daemon/services/memory-service.js +190 -0
  30. package/dist/src/daemon/services/memory-service.js.map +7 -0
  31. package/dist/src/daemon/unified-daemon.js +39 -5
  32. package/dist/src/daemon/unified-daemon.js.map +2 -2
  33. package/package.json +1 -1
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/daemon/daemon-config.ts"],
4
- "sourcesContent": ["/**\n * Daemon Configuration Management\n * Handles loading, saving, and validating daemon configuration\n */\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';\nimport { join } from 'path';\nimport { homedir } from 'os';\n\nexport interface DaemonServiceConfig {\n enabled: boolean;\n interval: number; // minutes\n}\n\nexport interface ContextServiceConfig extends DaemonServiceConfig {\n checkpointMessage?: string;\n}\n\nexport interface LinearServiceConfig extends DaemonServiceConfig {\n quietHours?: {\n start: number; // hour 0-23\n end: number;\n };\n retryAttempts: number;\n retryDelay: number; // ms\n}\n\nexport interface MaintenanceServiceConfig extends DaemonServiceConfig {\n staleFrameThresholdDays: number;\n ftsRebuildInterval: number; // hours\n embeddingBatchSize: number;\n vacuumInterval: number; // hours\n embeddingModel?: string; // default: 'Xenova/all-MiniLM-L6-v2'\n}\n\nexport interface FileWatchConfig extends DaemonServiceConfig {\n paths: string[];\n extensions: string[];\n ignore: string[];\n debounceMs: number;\n}\n\nexport interface DaemonConfig {\n version: string;\n context: ContextServiceConfig;\n linear: LinearServiceConfig;\n maintenance: MaintenanceServiceConfig;\n fileWatch: FileWatchConfig;\n heartbeatInterval: number; // seconds\n inactivityTimeout: number; // minutes, 0 = disabled\n logLevel: 'debug' | 'info' | 'warn' | 'error';\n}\n\nexport const DEFAULT_DAEMON_CONFIG: DaemonConfig = {\n version: '1.0.0',\n context: {\n enabled: true,\n interval: 15, // 15 minutes\n checkpointMessage: 'Auto-checkpoint',\n },\n linear: {\n enabled: false, // Disabled by default, requires setup\n interval: 60, // 60 minutes\n quietHours: { start: 22, end: 7 },\n retryAttempts: 3,\n retryDelay: 30000,\n },\n maintenance: {\n enabled: true,\n interval: 360, // 6 hours\n staleFrameThresholdDays: 30,\n ftsRebuildInterval: 24, // hours\n embeddingBatchSize: 50,\n vacuumInterval: 168, // weekly\n },\n fileWatch: {\n enabled: false, // Disabled by default\n interval: 0, // Not interval-based\n paths: ['.'],\n extensions: ['.ts', '.js', '.tsx', '.jsx', '.py', '.go', '.rs'],\n ignore: ['node_modules', '.git', 'dist', 'build', '.stackmemory'],\n debounceMs: 2000,\n },\n heartbeatInterval: 60, // 1 minute\n inactivityTimeout: 0, // Disabled by default\n logLevel: 'info',\n};\n\nexport interface DaemonStatus {\n running: boolean;\n pid?: number;\n startTime?: number;\n uptime?: number;\n services: {\n context: { enabled: boolean; lastRun?: number; saveCount?: number };\n linear: { enabled: boolean; lastRun?: number; syncCount?: number };\n maintenance: {\n enabled: boolean;\n lastRun?: number;\n staleFramesCleaned?: number;\n ftsRebuilds?: number;\n embeddingsGenerated?: number;\n };\n fileWatch: { enabled: boolean; eventsProcessed?: number };\n };\n errors: string[];\n}\n\n/**\n * Get the daemon directory path\n */\nexport function getDaemonDir(): string {\n const dir = join(homedir(), '.stackmemory', 'daemon');\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n return dir;\n}\n\n/**\n * Get the logs directory path\n */\nexport function getLogsDir(): string {\n const dir = join(homedir(), '.stackmemory', 'logs');\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n return dir;\n}\n\n/**\n * Get daemon file paths\n */\nexport function getDaemonPaths() {\n const daemonDir = getDaemonDir();\n const logsDir = getLogsDir();\n return {\n pidFile: join(daemonDir, 'daemon.pid'),\n statusFile: join(daemonDir, 'daemon.status'),\n configFile: join(daemonDir, 'config.json'),\n logFile: join(logsDir, 'daemon.log'),\n };\n}\n\n/**\n * Load daemon configuration\n */\nexport function loadDaemonConfig(): DaemonConfig {\n const { configFile } = getDaemonPaths();\n\n if (!existsSync(configFile)) {\n return { ...DEFAULT_DAEMON_CONFIG };\n }\n\n try {\n const content = readFileSync(configFile, 'utf8');\n const config = JSON.parse(content) as Partial<DaemonConfig>;\n return {\n ...DEFAULT_DAEMON_CONFIG,\n ...config,\n context: { ...DEFAULT_DAEMON_CONFIG.context, ...config.context },\n linear: { ...DEFAULT_DAEMON_CONFIG.linear, ...config.linear },\n maintenance: {\n ...DEFAULT_DAEMON_CONFIG.maintenance,\n ...config.maintenance,\n },\n fileWatch: { ...DEFAULT_DAEMON_CONFIG.fileWatch, ...config.fileWatch },\n };\n } catch {\n return { ...DEFAULT_DAEMON_CONFIG };\n }\n}\n\n/**\n * Save daemon configuration\n */\nexport function saveDaemonConfig(config: Partial<DaemonConfig>): void {\n const { configFile } = getDaemonPaths();\n const currentConfig = loadDaemonConfig();\n const newConfig = {\n ...currentConfig,\n ...config,\n context: { ...currentConfig.context, ...config.context },\n linear: { ...currentConfig.linear, ...config.linear },\n maintenance: { ...currentConfig.maintenance, ...config.maintenance },\n fileWatch: { ...currentConfig.fileWatch, ...config.fileWatch },\n };\n writeFileSync(configFile, JSON.stringify(newConfig, null, 2));\n}\n\n/**\n * Read daemon status\n */\nexport function readDaemonStatus(): DaemonStatus {\n const { statusFile, pidFile } = getDaemonPaths();\n\n const defaultStatus: DaemonStatus = {\n running: false,\n services: {\n context: { enabled: false },\n linear: { enabled: false },\n maintenance: { enabled: false },\n fileWatch: { enabled: false },\n },\n errors: [],\n };\n\n // Check PID file first\n if (!existsSync(pidFile)) {\n return defaultStatus;\n }\n\n try {\n const pidContent = readFileSync(pidFile, 'utf8').trim();\n const pid = parseInt(pidContent, 10);\n\n // Check if process is running\n try {\n process.kill(pid, 0);\n } catch {\n // Process not running\n return defaultStatus;\n }\n\n // Read status file\n if (!existsSync(statusFile)) {\n return { ...defaultStatus, running: true, pid };\n }\n\n const content = readFileSync(statusFile, 'utf8');\n const status = JSON.parse(content) as DaemonStatus;\n return {\n ...status,\n running: true,\n pid,\n uptime: status.startTime ? Date.now() - status.startTime : undefined,\n };\n } catch {\n return defaultStatus;\n }\n}\n\n/**\n * Write daemon status\n */\nexport function writeDaemonStatus(status: Partial<DaemonStatus>): void {\n const { statusFile } = getDaemonPaths();\n const currentStatus = readDaemonStatus();\n const newStatus = { ...currentStatus, ...status };\n writeFileSync(statusFile, JSON.stringify(newStatus, null, 2));\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * Daemon Configuration Management\n * Handles loading, saving, and validating daemon configuration\n */\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';\nimport { join } from 'path';\nimport { homedir } from 'os';\n\nexport interface DaemonServiceConfig {\n enabled: boolean;\n interval: number; // minutes\n}\n\nexport interface ContextServiceConfig extends DaemonServiceConfig {\n checkpointMessage?: string;\n}\n\nexport interface LinearServiceConfig extends DaemonServiceConfig {\n quietHours?: {\n start: number; // hour 0-23\n end: number;\n };\n retryAttempts: number;\n retryDelay: number; // ms\n}\n\nexport interface MaintenanceServiceConfig extends DaemonServiceConfig {\n staleFrameThresholdDays: number;\n ftsRebuildInterval: number; // hours\n embeddingBatchSize: number;\n vacuumInterval: number; // hours\n embeddingProvider?: 'transformers' | 'ollama' | 'openai' | 'none';\n embeddingModel?: string; // default: 'Xenova/all-MiniLM-L6-v2'\n embeddingDimension?: number;\n embeddingApiKey?: string;\n embeddingBaseUrl?: string;\n embeddingFallbackProviders?: Array<'transformers' | 'ollama' | 'openai'>;\n gcEnabled?: boolean; // default: true\n gcRetentionDays?: number; // default: 90\n gcBatchSize?: number; // default: 100\n coldTierProvider?: 'none' | 's3' | 'gcs'; // default: 'none'\n coldTierBucket?: string;\n coldTierPrefix?: string; // default: 'stackmemory/frames/'\n coldTierMigrationAgeDays?: number; // default: 60\n coldTierRehydrateCacheMinutes?: number; // default: 30\n}\n\nexport interface MemoryServiceConfig extends DaemonServiceConfig {\n ramThreshold: number; // 0.9 = 90% system RAM\n heapThreshold: number; // 0.9 = 90% Node.js heap\n cooldownMinutes: number; // avoid repeated triggers\n}\n\nexport interface FileWatchConfig extends DaemonServiceConfig {\n paths: string[];\n extensions: string[];\n ignore: string[];\n debounceMs: number;\n}\n\nexport interface DaemonConfig {\n version: string;\n context: ContextServiceConfig;\n linear: LinearServiceConfig;\n maintenance: MaintenanceServiceConfig;\n memory: MemoryServiceConfig;\n fileWatch: FileWatchConfig;\n heartbeatInterval: number; // seconds\n inactivityTimeout: number; // minutes, 0 = disabled\n logLevel: 'debug' | 'info' | 'warn' | 'error';\n}\n\nexport const DEFAULT_DAEMON_CONFIG: DaemonConfig = {\n version: '1.0.0',\n context: {\n enabled: true,\n interval: 15, // 15 minutes\n checkpointMessage: 'Auto-checkpoint',\n },\n linear: {\n enabled: false, // Disabled by default, requires setup\n interval: 60, // 60 minutes\n quietHours: { start: 22, end: 7 },\n retryAttempts: 3,\n retryDelay: 30000,\n },\n maintenance: {\n enabled: true,\n interval: 360, // 6 hours\n staleFrameThresholdDays: 30,\n ftsRebuildInterval: 24, // hours\n embeddingBatchSize: 50,\n vacuumInterval: 168, // weekly\n },\n memory: {\n enabled: true,\n interval: 0.5, // 30 seconds\n ramThreshold: 0.9,\n heapThreshold: 0.9,\n cooldownMinutes: 10,\n },\n fileWatch: {\n enabled: false, // Disabled by default\n interval: 0, // Not interval-based\n paths: ['.'],\n extensions: ['.ts', '.js', '.tsx', '.jsx', '.py', '.go', '.rs'],\n ignore: ['node_modules', '.git', 'dist', 'build', '.stackmemory'],\n debounceMs: 2000,\n },\n heartbeatInterval: 60, // 1 minute\n inactivityTimeout: 0, // Disabled by default\n logLevel: 'info',\n};\n\nexport interface DaemonStatus {\n running: boolean;\n pid?: number;\n startTime?: number;\n uptime?: number;\n services: {\n context: { enabled: boolean; lastRun?: number; saveCount?: number };\n linear: { enabled: boolean; lastRun?: number; syncCount?: number };\n maintenance: {\n enabled: boolean;\n lastRun?: number;\n staleFramesCleaned?: number;\n ftsRebuilds?: number;\n embeddingsGenerated?: number;\n embeddingsTotal?: number;\n embeddingsRemaining?: number;\n framesGarbageCollected?: number;\n lastGcRun?: number;\n };\n memory: {\n enabled: boolean;\n lastTrigger?: number;\n triggerCount?: number;\n currentRamPercent?: number;\n };\n fileWatch: { enabled: boolean; eventsProcessed?: number };\n };\n errors: string[];\n}\n\n/**\n * Get the daemon directory path\n */\nexport function getDaemonDir(): string {\n const dir = join(homedir(), '.stackmemory', 'daemon');\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n return dir;\n}\n\n/**\n * Get the logs directory path\n */\nexport function getLogsDir(): string {\n const dir = join(homedir(), '.stackmemory', 'logs');\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n return dir;\n}\n\n/**\n * Get daemon file paths\n */\nexport function getDaemonPaths() {\n const daemonDir = getDaemonDir();\n const logsDir = getLogsDir();\n return {\n pidFile: join(daemonDir, 'daemon.pid'),\n statusFile: join(daemonDir, 'daemon.status'),\n configFile: join(daemonDir, 'config.json'),\n logFile: join(logsDir, 'daemon.log'),\n };\n}\n\n/**\n * Load daemon configuration\n */\nexport function loadDaemonConfig(): DaemonConfig {\n const { configFile } = getDaemonPaths();\n\n if (!existsSync(configFile)) {\n return { ...DEFAULT_DAEMON_CONFIG };\n }\n\n try {\n const content = readFileSync(configFile, 'utf8');\n const config = JSON.parse(content) as Partial<DaemonConfig>;\n return {\n ...DEFAULT_DAEMON_CONFIG,\n ...config,\n context: { ...DEFAULT_DAEMON_CONFIG.context, ...config.context },\n linear: { ...DEFAULT_DAEMON_CONFIG.linear, ...config.linear },\n maintenance: {\n ...DEFAULT_DAEMON_CONFIG.maintenance,\n ...config.maintenance,\n },\n memory: { ...DEFAULT_DAEMON_CONFIG.memory, ...config.memory },\n fileWatch: { ...DEFAULT_DAEMON_CONFIG.fileWatch, ...config.fileWatch },\n };\n } catch {\n return { ...DEFAULT_DAEMON_CONFIG };\n }\n}\n\n/**\n * Save daemon configuration\n */\nexport function saveDaemonConfig(config: Partial<DaemonConfig>): void {\n const { configFile } = getDaemonPaths();\n const currentConfig = loadDaemonConfig();\n const newConfig = {\n ...currentConfig,\n ...config,\n context: { ...currentConfig.context, ...config.context },\n linear: { ...currentConfig.linear, ...config.linear },\n maintenance: { ...currentConfig.maintenance, ...config.maintenance },\n memory: { ...currentConfig.memory, ...config.memory },\n fileWatch: { ...currentConfig.fileWatch, ...config.fileWatch },\n };\n writeFileSync(configFile, JSON.stringify(newConfig, null, 2));\n}\n\n/**\n * Read daemon status\n */\nexport function readDaemonStatus(): DaemonStatus {\n const { statusFile, pidFile } = getDaemonPaths();\n\n const defaultStatus: DaemonStatus = {\n running: false,\n services: {\n context: { enabled: false },\n linear: { enabled: false },\n maintenance: { enabled: false },\n memory: { enabled: false },\n fileWatch: { enabled: false },\n },\n errors: [],\n };\n\n // Check PID file first\n if (!existsSync(pidFile)) {\n return defaultStatus;\n }\n\n try {\n const pidContent = readFileSync(pidFile, 'utf8').trim();\n const pid = parseInt(pidContent, 10);\n\n // Check if process is running\n try {\n process.kill(pid, 0);\n } catch {\n // Process not running\n return defaultStatus;\n }\n\n // Read status file\n if (!existsSync(statusFile)) {\n return { ...defaultStatus, running: true, pid };\n }\n\n const content = readFileSync(statusFile, 'utf8');\n const status = JSON.parse(content) as DaemonStatus;\n return {\n ...status,\n running: true,\n pid,\n uptime: status.startTime ? Date.now() - status.startTime : undefined,\n };\n } catch {\n return defaultStatus;\n }\n}\n\n/**\n * Write daemon status\n */\nexport function writeDaemonStatus(status: Partial<DaemonStatus>): void {\n const { statusFile } = getDaemonPaths();\n const currentStatus = readDaemonStatus();\n const newStatus = { ...currentStatus, ...status };\n writeFileSync(statusFile, JSON.stringify(newStatus, null, 2));\n}\n"],
5
+ "mappings": ";;;;AAKA,SAAS,YAAY,WAAW,cAAc,qBAAqB;AACnE,SAAS,YAAY;AACrB,SAAS,eAAe;AAkEjB,MAAM,wBAAsC;AAAA,EACjD,SAAS;AAAA,EACT,SAAS;AAAA,IACP,SAAS;AAAA,IACT,UAAU;AAAA;AAAA,IACV,mBAAmB;AAAA,EACrB;AAAA,EACA,QAAQ;AAAA,IACN,SAAS;AAAA;AAAA,IACT,UAAU;AAAA;AAAA,IACV,YAAY,EAAE,OAAO,IAAI,KAAK,EAAE;AAAA,IAChC,eAAe;AAAA,IACf,YAAY;AAAA,EACd;AAAA,EACA,aAAa;AAAA,IACX,SAAS;AAAA,IACT,UAAU;AAAA;AAAA,IACV,yBAAyB;AAAA,IACzB,oBAAoB;AAAA;AAAA,IACpB,oBAAoB;AAAA,IACpB,gBAAgB;AAAA;AAAA,EAClB;AAAA,EACA,QAAQ;AAAA,IACN,SAAS;AAAA,IACT,UAAU;AAAA;AAAA,IACV,cAAc;AAAA,IACd,eAAe;AAAA,IACf,iBAAiB;AAAA,EACnB;AAAA,EACA,WAAW;AAAA,IACT,SAAS;AAAA;AAAA,IACT,UAAU;AAAA;AAAA,IACV,OAAO,CAAC,GAAG;AAAA,IACX,YAAY,CAAC,OAAO,OAAO,QAAQ,QAAQ,OAAO,OAAO,KAAK;AAAA,IAC9D,QAAQ,CAAC,gBAAgB,QAAQ,QAAQ,SAAS,cAAc;AAAA,IAChE,YAAY;AAAA,EACd;AAAA,EACA,mBAAmB;AAAA;AAAA,EACnB,mBAAmB;AAAA;AAAA,EACnB,UAAU;AACZ;AAmCO,SAAS,eAAuB;AACrC,QAAM,MAAM,KAAK,QAAQ,GAAG,gBAAgB,QAAQ;AACpD,MAAI,CAAC,WAAW,GAAG,GAAG;AACpB,cAAU,KAAK,EAAE,WAAW,KAAK,CAAC;AAAA,EACpC;AACA,SAAO;AACT;AAKO,SAAS,aAAqB;AACnC,QAAM,MAAM,KAAK,QAAQ,GAAG,gBAAgB,MAAM;AAClD,MAAI,CAAC,WAAW,GAAG,GAAG;AACpB,cAAU,KAAK,EAAE,WAAW,KAAK,CAAC;AAAA,EACpC;AACA,SAAO;AACT;AAKO,SAAS,iBAAiB;AAC/B,QAAM,YAAY,aAAa;AAC/B,QAAM,UAAU,WAAW;AAC3B,SAAO;AAAA,IACL,SAAS,KAAK,WAAW,YAAY;AAAA,IACrC,YAAY,KAAK,WAAW,eAAe;AAAA,IAC3C,YAAY,KAAK,WAAW,aAAa;AAAA,IACzC,SAAS,KAAK,SAAS,YAAY;AAAA,EACrC;AACF;AAKO,SAAS,mBAAiC;AAC/C,QAAM,EAAE,WAAW,IAAI,eAAe;AAEtC,MAAI,CAAC,WAAW,UAAU,GAAG;AAC3B,WAAO,EAAE,GAAG,sBAAsB;AAAA,EACpC;AAEA,MAAI;AACF,UAAM,UAAU,aAAa,YAAY,MAAM;AAC/C,UAAM,SAAS,KAAK,MAAM,OAAO;AACjC,WAAO;AAAA,MACL,GAAG;AAAA,MACH,GAAG;AAAA,MACH,SAAS,EAAE,GAAG,sBAAsB,SAAS,GAAG,OAAO,QAAQ;AAAA,MAC/D,QAAQ,EAAE,GAAG,sBAAsB,QAAQ,GAAG,OAAO,OAAO;AAAA,MAC5D,aAAa;AAAA,QACX,GAAG,sBAAsB;AAAA,QACzB,GAAG,OAAO;AAAA,MACZ;AAAA,MACA,QAAQ,EAAE,GAAG,sBAAsB,QAAQ,GAAG,OAAO,OAAO;AAAA,MAC5D,WAAW,EAAE,GAAG,sBAAsB,WAAW,GAAG,OAAO,UAAU;AAAA,IACvE;AAAA,EACF,QAAQ;AACN,WAAO,EAAE,GAAG,sBAAsB;AAAA,EACpC;AACF;AAKO,SAAS,iBAAiB,QAAqC;AACpE,QAAM,EAAE,WAAW,IAAI,eAAe;AACtC,QAAM,gBAAgB,iBAAiB;AACvC,QAAM,YAAY;AAAA,IAChB,GAAG;AAAA,IACH,GAAG;AAAA,IACH,SAAS,EAAE,GAAG,cAAc,SAAS,GAAG,OAAO,QAAQ;AAAA,IACvD,QAAQ,EAAE,GAAG,cAAc,QAAQ,GAAG,OAAO,OAAO;AAAA,IACpD,aAAa,EAAE,GAAG,cAAc,aAAa,GAAG,OAAO,YAAY;AAAA,IACnE,QAAQ,EAAE,GAAG,cAAc,QAAQ,GAAG,OAAO,OAAO;AAAA,IACpD,WAAW,EAAE,GAAG,cAAc,WAAW,GAAG,OAAO,UAAU;AAAA,EAC/D;AACA,gBAAc,YAAY,KAAK,UAAU,WAAW,MAAM,CAAC,CAAC;AAC9D;AAKO,SAAS,mBAAiC;AAC/C,QAAM,EAAE,YAAY,QAAQ,IAAI,eAAe;AAE/C,QAAM,gBAA8B;AAAA,IAClC,SAAS;AAAA,IACT,UAAU;AAAA,MACR,SAAS,EAAE,SAAS,MAAM;AAAA,MAC1B,QAAQ,EAAE,SAAS,MAAM;AAAA,MACzB,aAAa,EAAE,SAAS,MAAM;AAAA,MAC9B,QAAQ,EAAE,SAAS,MAAM;AAAA,MACzB,WAAW,EAAE,SAAS,MAAM;AAAA,IAC9B;AAAA,IACA,QAAQ,CAAC;AAAA,EACX;AAGA,MAAI,CAAC,WAAW,OAAO,GAAG;AACxB,WAAO;AAAA,EACT;AAEA,MAAI;AACF,UAAM,aAAa,aAAa,SAAS,MAAM,EAAE,KAAK;AACtD,UAAM,MAAM,SAAS,YAAY,EAAE;AAGnC,QAAI;AACF,cAAQ,KAAK,KAAK,CAAC;AAAA,IACrB,QAAQ;AAEN,aAAO;AAAA,IACT;AAGA,QAAI,CAAC,WAAW,UAAU,GAAG;AAC3B,aAAO,EAAE,GAAG,eAAe,SAAS,MAAM,IAAI;AAAA,IAChD;AAEA,UAAM,UAAU,aAAa,YAAY,MAAM;AAC/C,UAAM,SAAS,KAAK,MAAM,OAAO;AACjC,WAAO;AAAA,MACL,GAAG;AAAA,MACH,SAAS;AAAA,MACT;AAAA,MACA,QAAQ,OAAO,YAAY,KAAK,IAAI,IAAI,OAAO,YAAY;AAAA,IAC7D;AAAA,EACF,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAKO,SAAS,kBAAkB,QAAqC;AACrE,QAAM,EAAE,WAAW,IAAI,eAAe;AACtC,QAAM,gBAAgB,iBAAiB;AACvC,QAAM,YAAY,EAAE,GAAG,eAAe,GAAG,OAAO;AAChD,gBAAc,YAAY,KAAK,UAAU,WAAW,MAAM,CAAC,CAAC;AAC9D;",
6
6
  "names": []
7
7
  }
@@ -21,7 +21,11 @@ class DaemonMaintenanceService {
21
21
  lastVacuum: 0,
22
22
  staleFramesCleaned: 0,
23
23
  embeddingsGenerated: 0,
24
+ embeddingsTotal: 0,
25
+ embeddingsRemaining: 0,
24
26
  ftsRebuilds: 0,
27
+ framesGarbageCollected: 0,
28
+ lastGcRun: 0,
25
29
  errors: []
26
30
  };
27
31
  }
@@ -89,6 +93,7 @@ class DaemonMaintenanceService {
89
93
  await this.backfillEmbeddings(db);
90
94
  await this.maybeVacuum(db);
91
95
  await this.generateMissingDigests(db);
96
+ await this.runGC(db);
92
97
  await db.disconnect();
93
98
  this.state.lastRunTime = Date.now();
94
99
  this.onLog("INFO", "Maintenance cycle completed", {
@@ -143,12 +148,56 @@ class DaemonMaintenanceService {
143
148
  if (typeof db.getFramesMissingEmbeddings !== "function") return;
144
149
  if (!db.getFeatures?.().supportsVectorSearch) return;
145
150
  if (!this.embeddingProvider) return;
151
+ let sinceRowid;
152
+ if (typeof db.getMaintenanceState === "function") {
153
+ const lastId = await db.getMaintenanceState(
154
+ "embedding_backfill_last_id"
155
+ );
156
+ if (lastId != null) {
157
+ sinceRowid = parseInt(lastId, 10);
158
+ if (isNaN(sinceRowid)) sinceRowid = void 0;
159
+ }
160
+ }
146
161
  const frames = await db.getFramesMissingEmbeddings(
147
- this.config.embeddingBatchSize
162
+ this.config.embeddingBatchSize,
163
+ sinceRowid
148
164
  );
165
+ if (typeof db.getMaintenanceState === "function") {
166
+ const totalStr = await db.getMaintenanceState(
167
+ "embedding_backfill_total"
168
+ );
169
+ const completedStr = await db.getMaintenanceState(
170
+ "embedding_backfill_completed"
171
+ );
172
+ if (totalStr != null)
173
+ this.state.embeddingsTotal = parseInt(totalStr, 10) || 0;
174
+ if (completedStr != null) {
175
+ const completed = parseInt(completedStr, 10) || 0;
176
+ this.state.embeddingsRemaining = Math.max(
177
+ 0,
178
+ this.state.embeddingsTotal - completed
179
+ );
180
+ }
181
+ }
149
182
  if (frames.length === 0) return;
183
+ if (this.state.embeddingsTotal === 0 && typeof db.setMaintenanceState === "function") {
184
+ const rawDb = db.getRawDatabase?.();
185
+ if (rawDb) {
186
+ const countResult = rawDb.prepare(
187
+ `SELECT COUNT(*) as count FROM frames f
188
+ LEFT JOIN frame_embeddings ve ON f.frame_id = ve.frame_id
189
+ WHERE ve.frame_id IS NULL`
190
+ ).get();
191
+ this.state.embeddingsTotal = countResult.count;
192
+ await db.setMaintenanceState(
193
+ "embedding_backfill_total",
194
+ String(this.state.embeddingsTotal)
195
+ );
196
+ }
197
+ }
150
198
  this.onLog("INFO", `Generating embeddings for ${frames.length} frames`);
151
199
  let generated = 0;
200
+ let lastRowid;
152
201
  for (const frame of frames) {
153
202
  try {
154
203
  const text = [
@@ -160,12 +209,37 @@ class DaemonMaintenanceService {
160
209
  const embedding = await this.embeddingProvider.embed(text);
161
210
  await db.storeEmbedding(frame.frame_id, embedding);
162
211
  generated++;
212
+ const rawDb = db.getRawDatabase?.();
213
+ if (rawDb) {
214
+ const rowidRow = rawDb.prepare("SELECT rowid FROM frames WHERE frame_id = ?").get(frame.frame_id);
215
+ if (rowidRow) lastRowid = rowidRow.rowid;
216
+ }
163
217
  } catch (err) {
164
218
  this.addError(
165
219
  `Embedding frame ${frame.frame_id}: ${err instanceof Error ? err.message : String(err)}`
166
220
  );
167
221
  }
168
222
  }
223
+ if (typeof db.setMaintenanceState === "function") {
224
+ if (lastRowid != null) {
225
+ await db.setMaintenanceState(
226
+ "embedding_backfill_last_id",
227
+ String(lastRowid)
228
+ );
229
+ }
230
+ const prevCompleted = await db.getMaintenanceState(
231
+ "embedding_backfill_completed"
232
+ );
233
+ const totalCompleted = (parseInt(prevCompleted ?? "0", 10) || 0) + generated;
234
+ await db.setMaintenanceState(
235
+ "embedding_backfill_completed",
236
+ String(totalCompleted)
237
+ );
238
+ this.state.embeddingsRemaining = Math.max(
239
+ 0,
240
+ this.state.embeddingsTotal - totalCompleted
241
+ );
242
+ }
169
243
  this.state.embeddingsGenerated += generated;
170
244
  this.onLog("INFO", `Generated ${generated} embeddings`);
171
245
  } catch (err) {
@@ -206,17 +280,50 @@ class DaemonMaintenanceService {
206
280
  );
207
281
  }
208
282
  }
283
+ async runGC(db) {
284
+ try {
285
+ if (this.config.gcEnabled === false) return;
286
+ if (typeof db.runGC !== "function") return;
287
+ const result = await db.runGC({
288
+ retentionDays: this.config.gcRetentionDays ?? 90,
289
+ batchSize: this.config.gcBatchSize ?? 100,
290
+ dryRun: false
291
+ });
292
+ this.state.framesGarbageCollected += result.framesDeleted;
293
+ this.state.lastGcRun = Date.now();
294
+ if (result.framesDeleted > 0) {
295
+ this.onLog(
296
+ "INFO",
297
+ `GC deleted ${result.framesDeleted} expired frames`,
298
+ {
299
+ eventsDeleted: result.eventsDeleted,
300
+ anchorsDeleted: result.anchorsDeleted,
301
+ embeddingsDeleted: result.embeddingsDeleted
302
+ }
303
+ );
304
+ }
305
+ } catch (err) {
306
+ this.addError(`GC: ${err instanceof Error ? err.message : String(err)}`);
307
+ }
308
+ }
209
309
  async getDatabase() {
210
310
  try {
211
311
  const { SQLiteAdapter } = await import("../../core/database/sqlite-adapter.js");
212
- const { createTransformersProvider } = await import("../../core/database/transformers-embedding-provider.js");
312
+ const { EmbeddingProviderFactory } = await import("../../core/database/embedding-provider-factory.js");
213
313
  const dbPath = this.findDatabasePath();
214
314
  if (!dbPath) {
215
315
  this.onLog("WARN", "No database found for maintenance");
216
316
  return null;
217
317
  }
218
318
  if (!this.embeddingProvider) {
219
- this.embeddingProvider = await createTransformersProvider(this.config.embeddingModel) ?? null;
319
+ this.embeddingProvider = await EmbeddingProviderFactory.create({
320
+ provider: this.config.embeddingProvider ?? "transformers",
321
+ model: this.config.embeddingModel,
322
+ dimension: this.config.embeddingDimension,
323
+ apiKey: this.config.embeddingApiKey,
324
+ baseUrl: this.config.embeddingBaseUrl,
325
+ fallbackProviders: this.config.embeddingFallbackProviders
326
+ });
220
327
  }
221
328
  const adapter = new SQLiteAdapter("maintenance", {
222
329
  dbPath,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/daemon/services/maintenance-service.ts"],
4
- "sourcesContent": ["/**\n * Database Maintenance Service\n * Background housekeeping: stale frame cleanup, FTS rebuild,\n * embedding backfill, VACUUM, and digest generation\n */\n\nimport { existsSync } from 'fs';\nimport { join } from 'path';\nimport { homedir } from 'os';\nimport type { MaintenanceServiceConfig } from '../daemon-config.js';\nimport type { EmbeddingProvider } from '../../core/database/embedding-provider.js';\n\nexport interface MaintenanceServiceState {\n lastRunTime: number;\n lastFtsRebuild: number;\n lastVacuum: number;\n staleFramesCleaned: number;\n embeddingsGenerated: number;\n ftsRebuilds: number;\n errors: string[];\n}\n\nexport class DaemonMaintenanceService {\n private config: MaintenanceServiceConfig;\n private state: MaintenanceServiceState;\n private embeddingProvider: EmbeddingProvider | null = null;\n private intervalId?: NodeJS.Timeout;\n private isRunning = false;\n private onLog: (level: string, message: string, data?: unknown) => void;\n\n constructor(\n config: MaintenanceServiceConfig,\n onLog: (level: string, message: string, data?: unknown) => void\n ) {\n this.config = config;\n this.onLog = onLog;\n this.state = {\n lastRunTime: 0,\n lastFtsRebuild: 0,\n lastVacuum: 0,\n staleFramesCleaned: 0,\n embeddingsGenerated: 0,\n ftsRebuilds: 0,\n errors: [],\n };\n }\n\n start(): void {\n if (this.isRunning || !this.config.enabled) {\n return;\n }\n\n this.isRunning = true;\n const intervalMs = this.config.interval * 60 * 1000;\n\n this.onLog('INFO', 'Maintenance service started', {\n interval: this.config.interval,\n staleThresholdDays: this.config.staleFrameThresholdDays,\n ftsRebuildInterval: this.config.ftsRebuildInterval,\n vacuumInterval: this.config.vacuumInterval,\n });\n\n // Schedule periodic maintenance\n this.intervalId = setInterval(() => {\n this.runMaintenance().catch((err) => {\n this.addError(\n `Maintenance cycle failed: ${err instanceof Error ? err.message : String(err)}`\n );\n });\n }, intervalMs);\n }\n\n stop(): void {\n if (this.intervalId) {\n clearInterval(this.intervalId);\n this.intervalId = undefined;\n }\n this.isRunning = false;\n this.onLog('INFO', 'Maintenance service stopped');\n }\n\n getState(): MaintenanceServiceState {\n return { ...this.state };\n }\n\n updateConfig(config: Partial<MaintenanceServiceConfig>): void {\n const wasRunning = this.isRunning;\n if (wasRunning) {\n this.stop();\n }\n\n this.config = { ...this.config, ...config };\n\n if (wasRunning && this.config.enabled) {\n this.start();\n }\n }\n\n /**\n * Force-run all maintenance tasks immediately\n */\n async forceRun(): Promise<void> {\n await this.runMaintenance();\n }\n\n /**\n * Run all maintenance tasks in sequence\n */\n async runMaintenance(): Promise<void> {\n const startTime = Date.now();\n this.onLog('INFO', 'Starting maintenance cycle');\n\n try {\n const db = await this.getDatabase();\n if (!db) {\n this.onLog('WARN', 'No database available for maintenance');\n return;\n }\n\n // Task 1: Stale frame cleanup\n await this.cleanStaleFrames(db);\n\n // Task 2: FTS rebuild (if due)\n await this.maybeRebuildFts(db);\n\n // Task 3: Embedding backfill (if provider configured)\n await this.backfillEmbeddings(db);\n\n // Task 4: VACUUM (if due)\n await this.maybeVacuum(db);\n\n // Task 5: Digest generation for frames missing digest_text\n await this.generateMissingDigests(db);\n\n await db.disconnect();\n\n this.state.lastRunTime = Date.now();\n this.onLog('INFO', 'Maintenance cycle completed', {\n durationMs: Date.now() - startTime,\n staleFramesCleaned: this.state.staleFramesCleaned,\n ftsRebuilds: this.state.ftsRebuilds,\n embeddingsGenerated: this.state.embeddingsGenerated,\n });\n } catch (err) {\n const errorMsg = err instanceof Error ? err.message : String(err);\n this.addError(errorMsg);\n this.onLog('ERROR', 'Maintenance cycle failed', { error: errorMsg });\n }\n }\n\n private async cleanStaleFrames(db: any): Promise<void> {\n try {\n const thresholdSec =\n Math.floor(Date.now() / 1000) -\n this.config.staleFrameThresholdDays * 24 * 3600;\n\n const rawDb = db.getRawDatabase?.();\n if (!rawDb) return;\n\n const result = rawDb\n .prepare(\n \"UPDATE frames SET state = 'stale' WHERE state = 'active' AND created_at < ?\"\n )\n .run(thresholdSec);\n\n const cleaned = result.changes || 0;\n this.state.staleFramesCleaned += cleaned;\n\n if (cleaned > 0) {\n this.onLog('INFO', `Marked ${cleaned} stale frames`);\n }\n } catch (err) {\n this.addError(\n `Stale frame cleanup: ${err instanceof Error ? err.message : String(err)}`\n );\n }\n }\n\n private async maybeRebuildFts(db: any): Promise<void> {\n try {\n const hoursSinceLastRebuild =\n (Date.now() - this.state.lastFtsRebuild) / (1000 * 3600);\n\n if (hoursSinceLastRebuild < this.config.ftsRebuildInterval) return;\n\n if (typeof db.rebuildFtsIndex === 'function') {\n await db.rebuildFtsIndex();\n this.state.lastFtsRebuild = Date.now();\n this.state.ftsRebuilds++;\n this.onLog('INFO', 'FTS index rebuilt');\n }\n } catch (err) {\n this.addError(\n `FTS rebuild: ${err instanceof Error ? err.message : String(err)}`\n );\n }\n }\n\n private async backfillEmbeddings(db: any): Promise<void> {\n try {\n if (typeof db.getFramesMissingEmbeddings !== 'function') return;\n if (!db.getFeatures?.().supportsVectorSearch) return;\n if (!this.embeddingProvider) return;\n\n const frames = await db.getFramesMissingEmbeddings(\n this.config.embeddingBatchSize\n );\n\n if (frames.length === 0) return;\n\n this.onLog('INFO', `Generating embeddings for ${frames.length} frames`);\n\n let generated = 0;\n for (const frame of frames) {\n try {\n const text = [\n frame.name,\n frame.digest_text,\n JSON.stringify(frame.inputs),\n ]\n .filter(Boolean)\n .join(' ');\n if (!text.trim()) continue;\n\n const embedding = await this.embeddingProvider.embed(text);\n await db.storeEmbedding(frame.frame_id, embedding);\n generated++;\n } catch (err) {\n this.addError(\n `Embedding frame ${frame.frame_id}: ${err instanceof Error ? err.message : String(err)}`\n );\n }\n }\n\n this.state.embeddingsGenerated += generated;\n this.onLog('INFO', `Generated ${generated} embeddings`);\n } catch (err) {\n this.addError(\n `Embedding backfill: ${err instanceof Error ? err.message : String(err)}`\n );\n }\n }\n\n private async maybeVacuum(db: any): Promise<void> {\n try {\n const hoursSinceLastVacuum =\n (Date.now() - this.state.lastVacuum) / (1000 * 3600);\n\n if (hoursSinceLastVacuum < this.config.vacuumInterval) return;\n\n const rawDb = db.getRawDatabase?.();\n if (!rawDb) return;\n\n rawDb.pragma('optimize');\n rawDb.pragma('vacuum');\n\n this.state.lastVacuum = Date.now();\n this.onLog('INFO', 'Database optimized and vacuumed');\n } catch (err) {\n this.addError(\n `VACUUM: ${err instanceof Error ? err.message : String(err)}`\n );\n }\n }\n\n private async generateMissingDigests(db: any): Promise<void> {\n try {\n const rawDb = db.getRawDatabase?.();\n if (!rawDb) return;\n\n const count = (\n rawDb\n .prepare(\n \"SELECT COUNT(*) as count FROM frames WHERE digest_text IS NULL AND state = 'active'\"\n )\n .get() as { count: number }\n ).count;\n\n if (count > 0) {\n this.onLog('INFO', `Found ${count} frames missing digest_text`);\n }\n } catch (err) {\n this.addError(\n `Digest generation: ${err instanceof Error ? err.message : String(err)}`\n );\n }\n }\n\n private async getDatabase(): Promise<any> {\n try {\n const { SQLiteAdapter } =\n await import('../../core/database/sqlite-adapter.js');\n const { createTransformersProvider } =\n await import('../../core/database/transformers-embedding-provider.js');\n\n const dbPath = this.findDatabasePath();\n if (!dbPath) {\n this.onLog('WARN', 'No database found for maintenance');\n return null;\n }\n\n if (!this.embeddingProvider) {\n this.embeddingProvider =\n (await createTransformersProvider(this.config.embeddingModel)) ??\n null;\n }\n\n const adapter = new SQLiteAdapter('maintenance', {\n dbPath,\n embeddingProvider: this.embeddingProvider ?? undefined,\n });\n await adapter.connect();\n await adapter.initializeSchema();\n return adapter;\n } catch (err) {\n this.addError(\n `Database init: ${err instanceof Error ? err.message : String(err)}`\n );\n return null;\n }\n }\n\n private findDatabasePath(): string | null {\n const homeDir = homedir();\n const candidates = [\n join(process.cwd(), '.stackmemory', 'stackmemory.db'),\n join(homeDir, '.stackmemory', 'stackmemory.db'),\n ];\n\n for (const candidate of candidates) {\n if (existsSync(candidate)) {\n return candidate;\n }\n }\n\n return null;\n }\n\n private addError(msg: string): void {\n this.state.errors.push(msg);\n if (this.state.errors.length > 10) {\n this.state.errors = this.state.errors.slice(-10);\n }\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * Database Maintenance Service\n * Background housekeeping: stale frame cleanup, FTS rebuild,\n * embedding backfill, VACUUM, and digest generation\n */\n\nimport { existsSync } from 'fs';\nimport { join } from 'path';\nimport { homedir } from 'os';\nimport type { MaintenanceServiceConfig } from '../daemon-config.js';\nimport type { EmbeddingProvider } from '../../core/database/embedding-provider.js';\n\nexport interface MaintenanceServiceState {\n lastRunTime: number;\n lastFtsRebuild: number;\n lastVacuum: number;\n staleFramesCleaned: number;\n embeddingsGenerated: number;\n embeddingsTotal: number;\n embeddingsRemaining: number;\n ftsRebuilds: number;\n framesGarbageCollected: number;\n lastGcRun: number;\n errors: string[];\n}\n\nexport class DaemonMaintenanceService {\n private config: MaintenanceServiceConfig;\n private state: MaintenanceServiceState;\n private embeddingProvider: EmbeddingProvider | null = null;\n private intervalId?: NodeJS.Timeout;\n private isRunning = false;\n private onLog: (level: string, message: string, data?: unknown) => void;\n\n constructor(\n config: MaintenanceServiceConfig,\n onLog: (level: string, message: string, data?: unknown) => void\n ) {\n this.config = config;\n this.onLog = onLog;\n this.state = {\n lastRunTime: 0,\n lastFtsRebuild: 0,\n lastVacuum: 0,\n staleFramesCleaned: 0,\n embeddingsGenerated: 0,\n embeddingsTotal: 0,\n embeddingsRemaining: 0,\n ftsRebuilds: 0,\n framesGarbageCollected: 0,\n lastGcRun: 0,\n errors: [],\n };\n }\n\n start(): void {\n if (this.isRunning || !this.config.enabled) {\n return;\n }\n\n this.isRunning = true;\n const intervalMs = this.config.interval * 60 * 1000;\n\n this.onLog('INFO', 'Maintenance service started', {\n interval: this.config.interval,\n staleThresholdDays: this.config.staleFrameThresholdDays,\n ftsRebuildInterval: this.config.ftsRebuildInterval,\n vacuumInterval: this.config.vacuumInterval,\n });\n\n // Schedule periodic maintenance\n this.intervalId = setInterval(() => {\n this.runMaintenance().catch((err) => {\n this.addError(\n `Maintenance cycle failed: ${err instanceof Error ? err.message : String(err)}`\n );\n });\n }, intervalMs);\n }\n\n stop(): void {\n if (this.intervalId) {\n clearInterval(this.intervalId);\n this.intervalId = undefined;\n }\n this.isRunning = false;\n this.onLog('INFO', 'Maintenance service stopped');\n }\n\n getState(): MaintenanceServiceState {\n return { ...this.state };\n }\n\n updateConfig(config: Partial<MaintenanceServiceConfig>): void {\n const wasRunning = this.isRunning;\n if (wasRunning) {\n this.stop();\n }\n\n this.config = { ...this.config, ...config };\n\n if (wasRunning && this.config.enabled) {\n this.start();\n }\n }\n\n /**\n * Force-run all maintenance tasks immediately\n */\n async forceRun(): Promise<void> {\n await this.runMaintenance();\n }\n\n /**\n * Run all maintenance tasks in sequence\n */\n async runMaintenance(): Promise<void> {\n const startTime = Date.now();\n this.onLog('INFO', 'Starting maintenance cycle');\n\n try {\n const db = await this.getDatabase();\n if (!db) {\n this.onLog('WARN', 'No database available for maintenance');\n return;\n }\n\n // Task 1: Stale frame cleanup\n await this.cleanStaleFrames(db);\n\n // Task 2: FTS rebuild (if due)\n await this.maybeRebuildFts(db);\n\n // Task 3: Embedding backfill (if provider configured)\n await this.backfillEmbeddings(db);\n\n // Task 4: VACUUM (if due)\n await this.maybeVacuum(db);\n\n // Task 5: Digest generation for frames missing digest_text\n await this.generateMissingDigests(db);\n\n // Task 6: Incremental garbage collection\n await this.runGC(db);\n\n await db.disconnect();\n\n this.state.lastRunTime = Date.now();\n this.onLog('INFO', 'Maintenance cycle completed', {\n durationMs: Date.now() - startTime,\n staleFramesCleaned: this.state.staleFramesCleaned,\n ftsRebuilds: this.state.ftsRebuilds,\n embeddingsGenerated: this.state.embeddingsGenerated,\n });\n } catch (err) {\n const errorMsg = err instanceof Error ? err.message : String(err);\n this.addError(errorMsg);\n this.onLog('ERROR', 'Maintenance cycle failed', { error: errorMsg });\n }\n }\n\n private async cleanStaleFrames(db: any): Promise<void> {\n try {\n const thresholdSec =\n Math.floor(Date.now() / 1000) -\n this.config.staleFrameThresholdDays * 24 * 3600;\n\n const rawDb = db.getRawDatabase?.();\n if (!rawDb) return;\n\n const result = rawDb\n .prepare(\n \"UPDATE frames SET state = 'stale' WHERE state = 'active' AND created_at < ?\"\n )\n .run(thresholdSec);\n\n const cleaned = result.changes || 0;\n this.state.staleFramesCleaned += cleaned;\n\n if (cleaned > 0) {\n this.onLog('INFO', `Marked ${cleaned} stale frames`);\n }\n } catch (err) {\n this.addError(\n `Stale frame cleanup: ${err instanceof Error ? err.message : String(err)}`\n );\n }\n }\n\n private async maybeRebuildFts(db: any): Promise<void> {\n try {\n const hoursSinceLastRebuild =\n (Date.now() - this.state.lastFtsRebuild) / (1000 * 3600);\n\n if (hoursSinceLastRebuild < this.config.ftsRebuildInterval) return;\n\n if (typeof db.rebuildFtsIndex === 'function') {\n await db.rebuildFtsIndex();\n this.state.lastFtsRebuild = Date.now();\n this.state.ftsRebuilds++;\n this.onLog('INFO', 'FTS index rebuilt');\n }\n } catch (err) {\n this.addError(\n `FTS rebuild: ${err instanceof Error ? err.message : String(err)}`\n );\n }\n }\n\n private async backfillEmbeddings(db: any): Promise<void> {\n try {\n if (typeof db.getFramesMissingEmbeddings !== 'function') return;\n if (!db.getFeatures?.().supportsVectorSearch) return;\n if (!this.embeddingProvider) return;\n\n // Read checkpoint: last processed rowid\n let sinceRowid: number | undefined;\n if (typeof db.getMaintenanceState === 'function') {\n const lastId = await db.getMaintenanceState(\n 'embedding_backfill_last_id'\n );\n if (lastId != null) {\n sinceRowid = parseInt(lastId, 10);\n if (isNaN(sinceRowid)) sinceRowid = undefined;\n }\n }\n\n const frames = await db.getFramesMissingEmbeddings(\n this.config.embeddingBatchSize,\n sinceRowid\n );\n\n // Update total/remaining counts for progress reporting\n if (typeof db.getMaintenanceState === 'function') {\n const totalStr = await db.getMaintenanceState(\n 'embedding_backfill_total'\n );\n const completedStr = await db.getMaintenanceState(\n 'embedding_backfill_completed'\n );\n if (totalStr != null)\n this.state.embeddingsTotal = parseInt(totalStr, 10) || 0;\n if (completedStr != null) {\n const completed = parseInt(completedStr, 10) || 0;\n this.state.embeddingsRemaining = Math.max(\n 0,\n this.state.embeddingsTotal - completed\n );\n }\n }\n\n if (frames.length === 0) return;\n\n // If we have no total yet, count all frames missing embeddings\n if (\n this.state.embeddingsTotal === 0 &&\n typeof db.setMaintenanceState === 'function'\n ) {\n const rawDb = db.getRawDatabase?.();\n if (rawDb) {\n const countResult = rawDb\n .prepare(\n `SELECT COUNT(*) as count FROM frames f\n LEFT JOIN frame_embeddings ve ON f.frame_id = ve.frame_id\n WHERE ve.frame_id IS NULL`\n )\n .get() as { count: number };\n this.state.embeddingsTotal = countResult.count;\n await db.setMaintenanceState(\n 'embedding_backfill_total',\n String(this.state.embeddingsTotal)\n );\n }\n }\n\n this.onLog('INFO', `Generating embeddings for ${frames.length} frames`);\n\n let generated = 0;\n let lastRowid: number | undefined;\n for (const frame of frames) {\n try {\n const text = [\n frame.name,\n frame.digest_text,\n JSON.stringify(frame.inputs),\n ]\n .filter(Boolean)\n .join(' ');\n if (!text.trim()) continue;\n\n const embedding = await this.embeddingProvider.embed(text);\n await db.storeEmbedding(frame.frame_id, embedding);\n generated++;\n\n // Track the rowid of the last processed frame for checkpoint\n const rawDb = db.getRawDatabase?.();\n if (rawDb) {\n const rowidRow = rawDb\n .prepare('SELECT rowid FROM frames WHERE frame_id = ?')\n .get(frame.frame_id) as { rowid: number } | undefined;\n if (rowidRow) lastRowid = rowidRow.rowid;\n }\n } catch (err) {\n this.addError(\n `Embedding frame ${frame.frame_id}: ${err instanceof Error ? err.message : String(err)}`\n );\n }\n }\n\n // Save checkpoint after batch\n if (typeof db.setMaintenanceState === 'function') {\n if (lastRowid != null) {\n await db.setMaintenanceState(\n 'embedding_backfill_last_id',\n String(lastRowid)\n );\n }\n const prevCompleted = await db.getMaintenanceState(\n 'embedding_backfill_completed'\n );\n const totalCompleted =\n (parseInt(prevCompleted ?? '0', 10) || 0) + generated;\n await db.setMaintenanceState(\n 'embedding_backfill_completed',\n String(totalCompleted)\n );\n\n this.state.embeddingsRemaining = Math.max(\n 0,\n this.state.embeddingsTotal - totalCompleted\n );\n }\n\n this.state.embeddingsGenerated += generated;\n this.onLog('INFO', `Generated ${generated} embeddings`);\n } catch (err) {\n this.addError(\n `Embedding backfill: ${err instanceof Error ? err.message : String(err)}`\n );\n }\n }\n\n private async maybeVacuum(db: any): Promise<void> {\n try {\n const hoursSinceLastVacuum =\n (Date.now() - this.state.lastVacuum) / (1000 * 3600);\n\n if (hoursSinceLastVacuum < this.config.vacuumInterval) return;\n\n const rawDb = db.getRawDatabase?.();\n if (!rawDb) return;\n\n rawDb.pragma('optimize');\n rawDb.pragma('vacuum');\n\n this.state.lastVacuum = Date.now();\n this.onLog('INFO', 'Database optimized and vacuumed');\n } catch (err) {\n this.addError(\n `VACUUM: ${err instanceof Error ? err.message : String(err)}`\n );\n }\n }\n\n private async generateMissingDigests(db: any): Promise<void> {\n try {\n const rawDb = db.getRawDatabase?.();\n if (!rawDb) return;\n\n const count = (\n rawDb\n .prepare(\n \"SELECT COUNT(*) as count FROM frames WHERE digest_text IS NULL AND state = 'active'\"\n )\n .get() as { count: number }\n ).count;\n\n if (count > 0) {\n this.onLog('INFO', `Found ${count} frames missing digest_text`);\n }\n } catch (err) {\n this.addError(\n `Digest generation: ${err instanceof Error ? err.message : String(err)}`\n );\n }\n }\n\n private async runGC(db: any): Promise<void> {\n try {\n if (this.config.gcEnabled === false) return;\n if (typeof db.runGC !== 'function') return;\n\n const result = await db.runGC({\n retentionDays: this.config.gcRetentionDays ?? 90,\n batchSize: this.config.gcBatchSize ?? 100,\n dryRun: false,\n });\n\n this.state.framesGarbageCollected += result.framesDeleted;\n this.state.lastGcRun = Date.now();\n\n if (result.framesDeleted > 0) {\n this.onLog(\n 'INFO',\n `GC deleted ${result.framesDeleted} expired frames`,\n {\n eventsDeleted: result.eventsDeleted,\n anchorsDeleted: result.anchorsDeleted,\n embeddingsDeleted: result.embeddingsDeleted,\n }\n );\n }\n } catch (err) {\n this.addError(`GC: ${err instanceof Error ? err.message : String(err)}`);\n }\n }\n\n private async getDatabase(): Promise<any> {\n try {\n const { SQLiteAdapter } =\n await import('../../core/database/sqlite-adapter.js');\n const { EmbeddingProviderFactory } =\n await import('../../core/database/embedding-provider-factory.js');\n\n const dbPath = this.findDatabasePath();\n if (!dbPath) {\n this.onLog('WARN', 'No database found for maintenance');\n return null;\n }\n\n if (!this.embeddingProvider) {\n this.embeddingProvider = await EmbeddingProviderFactory.create({\n provider: this.config.embeddingProvider ?? 'transformers',\n model: this.config.embeddingModel,\n dimension: this.config.embeddingDimension,\n apiKey: this.config.embeddingApiKey,\n baseUrl: this.config.embeddingBaseUrl,\n fallbackProviders: this.config.embeddingFallbackProviders,\n });\n }\n\n const adapter = new SQLiteAdapter('maintenance', {\n dbPath,\n embeddingProvider: this.embeddingProvider ?? undefined,\n });\n await adapter.connect();\n await adapter.initializeSchema();\n return adapter;\n } catch (err) {\n this.addError(\n `Database init: ${err instanceof Error ? err.message : String(err)}`\n );\n return null;\n }\n }\n\n private findDatabasePath(): string | null {\n const homeDir = homedir();\n const candidates = [\n join(process.cwd(), '.stackmemory', 'stackmemory.db'),\n join(homeDir, '.stackmemory', 'stackmemory.db'),\n ];\n\n for (const candidate of candidates) {\n if (existsSync(candidate)) {\n return candidate;\n }\n }\n\n return null;\n }\n\n private addError(msg: string): void {\n this.state.errors.push(msg);\n if (this.state.errors.length > 10) {\n this.state.errors = this.state.errors.slice(-10);\n }\n }\n}\n"],
5
+ "mappings": ";;;;AAMA,SAAS,kBAAkB;AAC3B,SAAS,YAAY;AACrB,SAAS,eAAe;AAkBjB,MAAM,yBAAyB;AAAA,EAC5B;AAAA,EACA;AAAA,EACA,oBAA8C;AAAA,EAC9C;AAAA,EACA,YAAY;AAAA,EACZ;AAAA,EAER,YACE,QACA,OACA;AACA,SAAK,SAAS;AACd,SAAK,QAAQ;AACb,SAAK,QAAQ;AAAA,MACX,aAAa;AAAA,MACb,gBAAgB;AAAA,MAChB,YAAY;AAAA,MACZ,oBAAoB;AAAA,MACpB,qBAAqB;AAAA,MACrB,iBAAiB;AAAA,MACjB,qBAAqB;AAAA,MACrB,aAAa;AAAA,MACb,wBAAwB;AAAA,MACxB,WAAW;AAAA,MACX,QAAQ,CAAC;AAAA,IACX;AAAA,EACF;AAAA,EAEA,QAAc;AACZ,QAAI,KAAK,aAAa,CAAC,KAAK,OAAO,SAAS;AAC1C;AAAA,IACF;AAEA,SAAK,YAAY;AACjB,UAAM,aAAa,KAAK,OAAO,WAAW,KAAK;AAE/C,SAAK,MAAM,QAAQ,+BAA+B;AAAA,MAChD,UAAU,KAAK,OAAO;AAAA,MACtB,oBAAoB,KAAK,OAAO;AAAA,MAChC,oBAAoB,KAAK,OAAO;AAAA,MAChC,gBAAgB,KAAK,OAAO;AAAA,IAC9B,CAAC;AAGD,SAAK,aAAa,YAAY,MAAM;AAClC,WAAK,eAAe,EAAE,MAAM,CAAC,QAAQ;AACnC,aAAK;AAAA,UACH,6BAA6B,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA,QAC/E;AAAA,MACF,CAAC;AAAA,IACH,GAAG,UAAU;AAAA,EACf;AAAA,EAEA,OAAa;AACX,QAAI,KAAK,YAAY;AACnB,oBAAc,KAAK,UAAU;AAC7B,WAAK,aAAa;AAAA,IACpB;AACA,SAAK,YAAY;AACjB,SAAK,MAAM,QAAQ,6BAA6B;AAAA,EAClD;AAAA,EAEA,WAAoC;AAClC,WAAO,EAAE,GAAG,KAAK,MAAM;AAAA,EACzB;AAAA,EAEA,aAAa,QAAiD;AAC5D,UAAM,aAAa,KAAK;AACxB,QAAI,YAAY;AACd,WAAK,KAAK;AAAA,IACZ;AAEA,SAAK,SAAS,EAAE,GAAG,KAAK,QAAQ,GAAG,OAAO;AAE1C,QAAI,cAAc,KAAK,OAAO,SAAS;AACrC,WAAK,MAAM;AAAA,IACb;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,WAA0B;AAC9B,UAAM,KAAK,eAAe;AAAA,EAC5B;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,iBAAgC;AACpC,UAAM,YAAY,KAAK,IAAI;AAC3B,SAAK,MAAM,QAAQ,4BAA4B;AAE/C,QAAI;AACF,YAAM,KAAK,MAAM,KAAK,YAAY;AAClC,UAAI,CAAC,IAAI;AACP,aAAK,MAAM,QAAQ,uCAAuC;AAC1D;AAAA,MACF;AAGA,YAAM,KAAK,iBAAiB,EAAE;AAG9B,YAAM,KAAK,gBAAgB,EAAE;AAG7B,YAAM,KAAK,mBAAmB,EAAE;AAGhC,YAAM,KAAK,YAAY,EAAE;AAGzB,YAAM,KAAK,uBAAuB,EAAE;AAGpC,YAAM,KAAK,MAAM,EAAE;AAEnB,YAAM,GAAG,WAAW;AAEpB,WAAK,MAAM,cAAc,KAAK,IAAI;AAClC,WAAK,MAAM,QAAQ,+BAA+B;AAAA,QAChD,YAAY,KAAK,IAAI,IAAI;AAAA,QACzB,oBAAoB,KAAK,MAAM;AAAA,QAC/B,aAAa,KAAK,MAAM;AAAA,QACxB,qBAAqB,KAAK,MAAM;AAAA,MAClC,CAAC;AAAA,IACH,SAAS,KAAK;AACZ,YAAM,WAAW,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAChE,WAAK,SAAS,QAAQ;AACtB,WAAK,MAAM,SAAS,4BAA4B,EAAE,OAAO,SAAS,CAAC;AAAA,IACrE;AAAA,EACF;AAAA,EAEA,MAAc,iBAAiB,IAAwB;AACrD,QAAI;AACF,YAAM,eACJ,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI,IAC5B,KAAK,OAAO,0BAA0B,KAAK;AAE7C,YAAM,QAAQ,GAAG,iBAAiB;AAClC,UAAI,CAAC,MAAO;AAEZ,YAAM,SAAS,MACZ;AAAA,QACC;AAAA,MACF,EACC,IAAI,YAAY;AAEnB,YAAM,UAAU,OAAO,WAAW;AAClC,WAAK,MAAM,sBAAsB;AAEjC,UAAI,UAAU,GAAG;AACf,aAAK,MAAM,QAAQ,UAAU,OAAO,eAAe;AAAA,MACrD;AAAA,IACF,SAAS,KAAK;AACZ,WAAK;AAAA,QACH,wBAAwB,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA,MAC1E;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAc,gBAAgB,IAAwB;AACpD,QAAI;AACF,YAAM,yBACH,KAAK,IAAI,IAAI,KAAK,MAAM,mBAAmB,MAAO;AAErD,UAAI,wBAAwB,KAAK,OAAO,mBAAoB;AAE5D,UAAI,OAAO,GAAG,oBAAoB,YAAY;AAC5C,cAAM,GAAG,gBAAgB;AACzB,aAAK,MAAM,iBAAiB,KAAK,IAAI;AACrC,aAAK,MAAM;AACX,aAAK,MAAM,QAAQ,mBAAmB;AAAA,MACxC;AAAA,IACF,SAAS,KAAK;AACZ,WAAK;AAAA,QACH,gBAAgB,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA,MAClE;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAc,mBAAmB,IAAwB;AACvD,QAAI;AACF,UAAI,OAAO,GAAG,+BAA+B,WAAY;AACzD,UAAI,CAAC,GAAG,cAAc,EAAE,qBAAsB;AAC9C,UAAI,CAAC,KAAK,kBAAmB;AAG7B,UAAI;AACJ,UAAI,OAAO,GAAG,wBAAwB,YAAY;AAChD,cAAM,SAAS,MAAM,GAAG;AAAA,UACtB;AAAA,QACF;AACA,YAAI,UAAU,MAAM;AAClB,uBAAa,SAAS,QAAQ,EAAE;AAChC,cAAI,MAAM,UAAU,EAAG,cAAa;AAAA,QACtC;AAAA,MACF;AAEA,YAAM,SAAS,MAAM,GAAG;AAAA,QACtB,KAAK,OAAO;AAAA,QACZ;AAAA,MACF;AAGA,UAAI,OAAO,GAAG,wBAAwB,YAAY;AAChD,cAAM,WAAW,MAAM,GAAG;AAAA,UACxB;AAAA,QACF;AACA,cAAM,eAAe,MAAM,GAAG;AAAA,UAC5B;AAAA,QACF;AACA,YAAI,YAAY;AACd,eAAK,MAAM,kBAAkB,SAAS,UAAU,EAAE,KAAK;AACzD,YAAI,gBAAgB,MAAM;AACxB,gBAAM,YAAY,SAAS,cAAc,EAAE,KAAK;AAChD,eAAK,MAAM,sBAAsB,KAAK;AAAA,YACpC;AAAA,YACA,KAAK,MAAM,kBAAkB;AAAA,UAC/B;AAAA,QACF;AAAA,MACF;AAEA,UAAI,OAAO,WAAW,EAAG;AAGzB,UACE,KAAK,MAAM,oBAAoB,KAC/B,OAAO,GAAG,wBAAwB,YAClC;AACA,cAAM,QAAQ,GAAG,iBAAiB;AAClC,YAAI,OAAO;AACT,gBAAM,cAAc,MACjB;AAAA,YACC;AAAA;AAAA;AAAA,UAGF,EACC,IAAI;AACP,eAAK,MAAM,kBAAkB,YAAY;AACzC,gBAAM,GAAG;AAAA,YACP;AAAA,YACA,OAAO,KAAK,MAAM,eAAe;AAAA,UACnC;AAAA,QACF;AAAA,MACF;AAEA,WAAK,MAAM,QAAQ,6BAA6B,OAAO,MAAM,SAAS;AAEtE,UAAI,YAAY;AAChB,UAAI;AACJ,iBAAW,SAAS,QAAQ;AAC1B,YAAI;AACF,gBAAM,OAAO;AAAA,YACX,MAAM;AAAA,YACN,MAAM;AAAA,YACN,KAAK,UAAU,MAAM,MAAM;AAAA,UAC7B,EACG,OAAO,OAAO,EACd,KAAK,GAAG;AACX,cAAI,CAAC,KAAK,KAAK,EAAG;AAElB,gBAAM,YAAY,MAAM,KAAK,kBAAkB,MAAM,IAAI;AACzD,gBAAM,GAAG,eAAe,MAAM,UAAU,SAAS;AACjD;AAGA,gBAAM,QAAQ,GAAG,iBAAiB;AAClC,cAAI,OAAO;AACT,kBAAM,WAAW,MACd,QAAQ,6CAA6C,EACrD,IAAI,MAAM,QAAQ;AACrB,gBAAI,SAAU,aAAY,SAAS;AAAA,UACrC;AAAA,QACF,SAAS,KAAK;AACZ,eAAK;AAAA,YACH,mBAAmB,MAAM,QAAQ,KAAK,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA,UACxF;AAAA,QACF;AAAA,MACF;AAGA,UAAI,OAAO,GAAG,wBAAwB,YAAY;AAChD,YAAI,aAAa,MAAM;AACrB,gBAAM,GAAG;AAAA,YACP;AAAA,YACA,OAAO,SAAS;AAAA,UAClB;AAAA,QACF;AACA,cAAM,gBAAgB,MAAM,GAAG;AAAA,UAC7B;AAAA,QACF;AACA,cAAM,kBACH,SAAS,iBAAiB,KAAK,EAAE,KAAK,KAAK;AAC9C,cAAM,GAAG;AAAA,UACP;AAAA,UACA,OAAO,cAAc;AAAA,QACvB;AAEA,aAAK,MAAM,sBAAsB,KAAK;AAAA,UACpC;AAAA,UACA,KAAK,MAAM,kBAAkB;AAAA,QAC/B;AAAA,MACF;AAEA,WAAK,MAAM,uBAAuB;AAClC,WAAK,MAAM,QAAQ,aAAa,SAAS,aAAa;AAAA,IACxD,SAAS,KAAK;AACZ,WAAK;AAAA,QACH,uBAAuB,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA,MACzE;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAc,YAAY,IAAwB;AAChD,QAAI;AACF,YAAM,wBACH,KAAK,IAAI,IAAI,KAAK,MAAM,eAAe,MAAO;AAEjD,UAAI,uBAAuB,KAAK,OAAO,eAAgB;AAEvD,YAAM,QAAQ,GAAG,iBAAiB;AAClC,UAAI,CAAC,MAAO;AAEZ,YAAM,OAAO,UAAU;AACvB,YAAM,OAAO,QAAQ;AAErB,WAAK,MAAM,aAAa,KAAK,IAAI;AACjC,WAAK,MAAM,QAAQ,iCAAiC;AAAA,IACtD,SAAS,KAAK;AACZ,WAAK;AAAA,QACH,WAAW,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA,MAC7D;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAc,uBAAuB,IAAwB;AAC3D,QAAI;AACF,YAAM,QAAQ,GAAG,iBAAiB;AAClC,UAAI,CAAC,MAAO;AAEZ,YAAM,QACJ,MACG;AAAA,QACC;AAAA,MACF,EACC,IAAI,EACP;AAEF,UAAI,QAAQ,GAAG;AACb,aAAK,MAAM,QAAQ,SAAS,KAAK,6BAA6B;AAAA,MAChE;AAAA,IACF,SAAS,KAAK;AACZ,WAAK;AAAA,QACH,sBAAsB,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA,MACxE;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAc,MAAM,IAAwB;AAC1C,QAAI;AACF,UAAI,KAAK,OAAO,cAAc,MAAO;AACrC,UAAI,OAAO,GAAG,UAAU,WAAY;AAEpC,YAAM,SAAS,MAAM,GAAG,MAAM;AAAA,QAC5B,eAAe,KAAK,OAAO,mBAAmB;AAAA,QAC9C,WAAW,KAAK,OAAO,eAAe;AAAA,QACtC,QAAQ;AAAA,MACV,CAAC;AAED,WAAK,MAAM,0BAA0B,OAAO;AAC5C,WAAK,MAAM,YAAY,KAAK,IAAI;AAEhC,UAAI,OAAO,gBAAgB,GAAG;AAC5B,aAAK;AAAA,UACH;AAAA,UACA,cAAc,OAAO,aAAa;AAAA,UAClC;AAAA,YACE,eAAe,OAAO;AAAA,YACtB,gBAAgB,OAAO;AAAA,YACvB,mBAAmB,OAAO;AAAA,UAC5B;AAAA,QACF;AAAA,MACF;AAAA,IACF,SAAS,KAAK;AACZ,WAAK,SAAS,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC,EAAE;AAAA,IACzE;AAAA,EACF;AAAA,EAEA,MAAc,cAA4B;AACxC,QAAI;AACF,YAAM,EAAE,cAAc,IACpB,MAAM,OAAO,uCAAuC;AACtD,YAAM,EAAE,yBAAyB,IAC/B,MAAM,OAAO,mDAAmD;AAElE,YAAM,SAAS,KAAK,iBAAiB;AACrC,UAAI,CAAC,QAAQ;AACX,aAAK,MAAM,QAAQ,mCAAmC;AACtD,eAAO;AAAA,MACT;AAEA,UAAI,CAAC,KAAK,mBAAmB;AAC3B,aAAK,oBAAoB,MAAM,yBAAyB,OAAO;AAAA,UAC7D,UAAU,KAAK,OAAO,qBAAqB;AAAA,UAC3C,OAAO,KAAK,OAAO;AAAA,UACnB,WAAW,KAAK,OAAO;AAAA,UACvB,QAAQ,KAAK,OAAO;AAAA,UACpB,SAAS,KAAK,OAAO;AAAA,UACrB,mBAAmB,KAAK,OAAO;AAAA,QACjC,CAAC;AAAA,MACH;AAEA,YAAM,UAAU,IAAI,cAAc,eAAe;AAAA,QAC/C;AAAA,QACA,mBAAmB,KAAK,qBAAqB;AAAA,MAC/C,CAAC;AACD,YAAM,QAAQ,QAAQ;AACtB,YAAM,QAAQ,iBAAiB;AAC/B,aAAO;AAAA,IACT,SAAS,KAAK;AACZ,WAAK;AAAA,QACH,kBAAkB,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA,MACpE;AACA,aAAO;AAAA,IACT;AAAA,EACF;AAAA,EAEQ,mBAAkC;AACxC,UAAM,UAAU,QAAQ;AACxB,UAAM,aAAa;AAAA,MACjB,KAAK,QAAQ,IAAI,GAAG,gBAAgB,gBAAgB;AAAA,MACpD,KAAK,SAAS,gBAAgB,gBAAgB;AAAA,IAChD;AAEA,eAAW,aAAa,YAAY;AAClC,UAAI,WAAW,SAAS,GAAG;AACzB,eAAO;AAAA,MACT;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA,EAEQ,SAAS,KAAmB;AAClC,SAAK,MAAM,OAAO,KAAK,GAAG;AAC1B,QAAI,KAAK,MAAM,OAAO,SAAS,IAAI;AACjC,WAAK,MAAM,SAAS,KAAK,MAAM,OAAO,MAAM,GAAG;AAAA,IACjD;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,190 @@
1
+ import { fileURLToPath as __fileURLToPath } from 'url';
2
+ import { dirname as __pathDirname } from 'path';
3
+ const __filename = __fileURLToPath(import.meta.url);
4
+ const __dirname = __pathDirname(__filename);
5
+ import { existsSync, mkdirSync, writeFileSync } from "fs";
6
+ import { join } from "path";
7
+ import { execSync } from "child_process";
8
+ import { freemem, homedir, totalmem } from "os";
9
+ class DaemonMemoryService {
10
+ config;
11
+ state;
12
+ intervalId;
13
+ isRunning = false;
14
+ onLog;
15
+ constructor(config, onLog) {
16
+ this.config = config;
17
+ this.onLog = onLog;
18
+ this.state = {
19
+ lastCheckTime: 0,
20
+ lastTriggerTime: 0,
21
+ triggerCount: 0,
22
+ currentRamPercent: 0,
23
+ currentHeapPercent: 0,
24
+ errors: []
25
+ };
26
+ }
27
+ start() {
28
+ if (this.isRunning || !this.config.enabled) {
29
+ return;
30
+ }
31
+ this.isRunning = true;
32
+ const intervalMs = this.config.interval * 60 * 1e3;
33
+ this.onLog("INFO", "Memory service started", {
34
+ interval: this.config.interval,
35
+ ramThreshold: this.config.ramThreshold,
36
+ heapThreshold: this.config.heapThreshold,
37
+ cooldownMinutes: this.config.cooldownMinutes
38
+ });
39
+ this.checkMemory();
40
+ this.intervalId = setInterval(() => {
41
+ this.checkMemory();
42
+ }, intervalMs);
43
+ }
44
+ stop() {
45
+ if (this.intervalId) {
46
+ clearInterval(this.intervalId);
47
+ this.intervalId = void 0;
48
+ }
49
+ this.isRunning = false;
50
+ this.onLog("INFO", "Memory service stopped");
51
+ }
52
+ getState() {
53
+ return { ...this.state };
54
+ }
55
+ updateConfig(config) {
56
+ const wasRunning = this.isRunning;
57
+ if (wasRunning) {
58
+ this.stop();
59
+ }
60
+ this.config = { ...this.config, ...config };
61
+ if (wasRunning && this.config.enabled) {
62
+ this.start();
63
+ }
64
+ }
65
+ checkMemory() {
66
+ if (!this.isRunning) return;
67
+ try {
68
+ const total = totalmem();
69
+ const free = freemem();
70
+ const ramPercent = (total - free) / total;
71
+ const heap = process.memoryUsage();
72
+ const heapPercent = heap.heapUsed / heap.heapTotal;
73
+ this.state.currentRamPercent = ramPercent;
74
+ this.state.currentHeapPercent = heapPercent;
75
+ this.state.lastCheckTime = Date.now();
76
+ this.onLog("DEBUG", "Memory check", {
77
+ ramPercent: Math.round(ramPercent * 100),
78
+ heapPercent: Math.round(heapPercent * 100)
79
+ });
80
+ const ramExceeded = ramPercent > this.config.ramThreshold;
81
+ const heapExceeded = heapPercent > this.config.heapThreshold;
82
+ if (!ramExceeded && !heapExceeded) return;
83
+ const cooldownMs = this.config.cooldownMinutes * 60 * 1e3;
84
+ const elapsed = Date.now() - this.state.lastTriggerTime;
85
+ if (this.state.lastTriggerTime > 0 && elapsed < cooldownMs) {
86
+ this.onLog("DEBUG", "Memory threshold exceeded but in cooldown", {
87
+ remainingMs: cooldownMs - elapsed
88
+ });
89
+ return;
90
+ }
91
+ const reason = ramExceeded ? `RAM ${Math.round(ramPercent * 100)}% > ${Math.round(this.config.ramThreshold * 100)}%` : `Heap ${Math.round(heapPercent * 100)}% > ${Math.round(this.config.heapThreshold * 100)}%`;
92
+ this.onLog("WARN", `Memory threshold exceeded: ${reason}`);
93
+ this.triggerCaptureAndClear(reason, ramPercent, heapPercent);
94
+ } catch (err) {
95
+ const errorMsg = err instanceof Error ? err.message : String(err);
96
+ this.state.errors.push(errorMsg);
97
+ this.onLog("ERROR", "Memory check failed", { error: errorMsg });
98
+ if (this.state.errors.length > 10) {
99
+ this.state.errors = this.state.errors.slice(-10);
100
+ }
101
+ }
102
+ }
103
+ triggerCaptureAndClear(reason, ramPercent, heapPercent) {
104
+ try {
105
+ this.writeSignalFile(reason, ramPercent, heapPercent);
106
+ this.state.lastTriggerTime = Date.now();
107
+ this.state.triggerCount++;
108
+ const stackmemoryBin = this.getStackMemoryBin();
109
+ if (stackmemoryBin) {
110
+ try {
111
+ execSync(`"${stackmemoryBin}" capture --no-commit --basic`, {
112
+ timeout: 3e4,
113
+ encoding: "utf8",
114
+ stdio: "pipe"
115
+ });
116
+ this.onLog("INFO", "Context captured before memory clear");
117
+ } catch (err) {
118
+ const msg = err instanceof Error ? err.message : String(err);
119
+ this.onLog("WARN", "Capture failed", { error: msg });
120
+ }
121
+ try {
122
+ execSync(`"${stackmemoryBin}" clear --save`, {
123
+ timeout: 3e4,
124
+ encoding: "utf8",
125
+ stdio: "pipe"
126
+ });
127
+ this.onLog("INFO", "Context cleared");
128
+ } catch (err) {
129
+ const msg = err instanceof Error ? err.message : String(err);
130
+ this.onLog("WARN", "Clear failed", { error: msg });
131
+ }
132
+ }
133
+ this.onLog("INFO", "Memory trigger completed", {
134
+ triggerCount: this.state.triggerCount,
135
+ reason
136
+ });
137
+ } catch (err) {
138
+ const errorMsg = err instanceof Error ? err.message : String(err);
139
+ this.state.errors.push(errorMsg);
140
+ this.onLog("ERROR", "Memory trigger failed", { error: errorMsg });
141
+ if (this.state.errors.length > 10) {
142
+ this.state.errors = this.state.errors.slice(-10);
143
+ }
144
+ }
145
+ }
146
+ writeSignalFile(reason, ramPercent, heapPercent) {
147
+ const signalDir = join(process.cwd(), ".stackmemory");
148
+ if (!existsSync(signalDir)) {
149
+ mkdirSync(signalDir, { recursive: true });
150
+ }
151
+ const signalPath = join(signalDir, ".memory-clear-signal");
152
+ const signal = {
153
+ timestamp: Date.now(),
154
+ reason,
155
+ ramPercent: Math.round(ramPercent * 100),
156
+ heapPercent: Math.round(heapPercent * 100)
157
+ };
158
+ writeFileSync(signalPath, JSON.stringify(signal, null, 2));
159
+ this.onLog("INFO", "Signal file written", { path: signalPath });
160
+ }
161
+ getStackMemoryBin() {
162
+ const homeDir = homedir();
163
+ const locations = [
164
+ join(homeDir, ".stackmemory", "bin", "stackmemory"),
165
+ join(homeDir, ".local", "bin", "stackmemory"),
166
+ "/usr/local/bin/stackmemory",
167
+ "/opt/homebrew/bin/stackmemory"
168
+ ];
169
+ for (const loc of locations) {
170
+ if (existsSync(loc)) {
171
+ return loc;
172
+ }
173
+ }
174
+ try {
175
+ const result = execSync("which stackmemory", {
176
+ encoding: "utf8",
177
+ stdio: "pipe"
178
+ }).trim();
179
+ if (result && existsSync(result)) {
180
+ return result;
181
+ }
182
+ } catch {
183
+ }
184
+ return null;
185
+ }
186
+ }
187
+ export {
188
+ DaemonMemoryService
189
+ };
190
+ //# sourceMappingURL=memory-service.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/daemon/services/memory-service.ts"],
4
+ "sourcesContent": ["/**\n * Memory Monitor Service\n * Monitors system RAM and Node.js heap usage, triggering\n * capture/clear cycle when thresholds are exceeded.\n */\n\nimport { existsSync, mkdirSync, writeFileSync } from 'fs';\nimport { join } from 'path';\nimport { execSync } from 'child_process';\nimport { freemem, homedir, totalmem } from 'os';\nimport type { MemoryServiceConfig } from '../daemon-config.js';\n\nexport interface MemoryServiceState {\n lastCheckTime: number;\n lastTriggerTime: number;\n triggerCount: number;\n currentRamPercent: number;\n currentHeapPercent: number;\n errors: string[];\n}\n\nexport class DaemonMemoryService {\n private config: MemoryServiceConfig;\n private state: MemoryServiceState;\n private intervalId?: NodeJS.Timeout;\n private isRunning = false;\n private onLog: (level: string, message: string, data?: unknown) => void;\n\n constructor(\n config: MemoryServiceConfig,\n onLog: (level: string, message: string, data?: unknown) => void\n ) {\n this.config = config;\n this.onLog = onLog;\n this.state = {\n lastCheckTime: 0,\n lastTriggerTime: 0,\n triggerCount: 0,\n currentRamPercent: 0,\n currentHeapPercent: 0,\n errors: [],\n };\n }\n\n start(): void {\n if (this.isRunning || !this.config.enabled) {\n return;\n }\n\n this.isRunning = true;\n const intervalMs = this.config.interval * 60 * 1000;\n\n this.onLog('INFO', 'Memory service started', {\n interval: this.config.interval,\n ramThreshold: this.config.ramThreshold,\n heapThreshold: this.config.heapThreshold,\n cooldownMinutes: this.config.cooldownMinutes,\n });\n\n // Initial check\n this.checkMemory();\n\n // Schedule periodic checks\n this.intervalId = setInterval(() => {\n this.checkMemory();\n }, intervalMs);\n }\n\n stop(): void {\n if (this.intervalId) {\n clearInterval(this.intervalId);\n this.intervalId = undefined;\n }\n this.isRunning = false;\n this.onLog('INFO', 'Memory service stopped');\n }\n\n getState(): MemoryServiceState {\n return { ...this.state };\n }\n\n updateConfig(config: Partial<MemoryServiceConfig>): void {\n const wasRunning = this.isRunning;\n if (wasRunning) {\n this.stop();\n }\n\n this.config = { ...this.config, ...config };\n\n if (wasRunning && this.config.enabled) {\n this.start();\n }\n }\n\n private checkMemory(): void {\n if (!this.isRunning) return;\n\n try {\n const total = totalmem();\n const free = freemem();\n const ramPercent = (total - free) / total;\n\n const heap = process.memoryUsage();\n const heapPercent = heap.heapUsed / heap.heapTotal;\n\n this.state.currentRamPercent = ramPercent;\n this.state.currentHeapPercent = heapPercent;\n this.state.lastCheckTime = Date.now();\n\n this.onLog('DEBUG', 'Memory check', {\n ramPercent: Math.round(ramPercent * 100),\n heapPercent: Math.round(heapPercent * 100),\n });\n\n const ramExceeded = ramPercent > this.config.ramThreshold;\n const heapExceeded = heapPercent > this.config.heapThreshold;\n\n if (!ramExceeded && !heapExceeded) return;\n\n // Check cooldown\n const cooldownMs = this.config.cooldownMinutes * 60 * 1000;\n const elapsed = Date.now() - this.state.lastTriggerTime;\n if (this.state.lastTriggerTime > 0 && elapsed < cooldownMs) {\n this.onLog('DEBUG', 'Memory threshold exceeded but in cooldown', {\n remainingMs: cooldownMs - elapsed,\n });\n return;\n }\n\n const reason = ramExceeded\n ? `RAM ${Math.round(ramPercent * 100)}% > ${Math.round(this.config.ramThreshold * 100)}%`\n : `Heap ${Math.round(heapPercent * 100)}% > ${Math.round(this.config.heapThreshold * 100)}%`;\n\n this.onLog('WARN', `Memory threshold exceeded: ${reason}`);\n this.triggerCaptureAndClear(reason, ramPercent, heapPercent);\n } catch (err) {\n const errorMsg = err instanceof Error ? err.message : String(err);\n this.state.errors.push(errorMsg);\n this.onLog('ERROR', 'Memory check failed', { error: errorMsg });\n\n if (this.state.errors.length > 10) {\n this.state.errors = this.state.errors.slice(-10);\n }\n }\n }\n\n private triggerCaptureAndClear(\n reason: string,\n ramPercent: number,\n heapPercent: number\n ): void {\n try {\n // Write signal file FIRST so the hook can warn the user immediately\n this.writeSignalFile(reason, ramPercent, heapPercent);\n\n this.state.lastTriggerTime = Date.now();\n this.state.triggerCount++;\n\n const stackmemoryBin = this.getStackMemoryBin();\n\n if (stackmemoryBin) {\n // Capture current context\n try {\n execSync(`\"${stackmemoryBin}\" capture --no-commit --basic`, {\n timeout: 30000,\n encoding: 'utf8',\n stdio: 'pipe',\n });\n this.onLog('INFO', 'Context captured before memory clear');\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n this.onLog('WARN', 'Capture failed', { error: msg });\n }\n\n // Clear saved context\n try {\n execSync(`\"${stackmemoryBin}\" clear --save`, {\n timeout: 30000,\n encoding: 'utf8',\n stdio: 'pipe',\n });\n this.onLog('INFO', 'Context cleared');\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n this.onLog('WARN', 'Clear failed', { error: msg });\n }\n }\n\n this.onLog('INFO', 'Memory trigger completed', {\n triggerCount: this.state.triggerCount,\n reason,\n });\n } catch (err) {\n const errorMsg = err instanceof Error ? err.message : String(err);\n this.state.errors.push(errorMsg);\n this.onLog('ERROR', 'Memory trigger failed', { error: errorMsg });\n\n if (this.state.errors.length > 10) {\n this.state.errors = this.state.errors.slice(-10);\n }\n }\n }\n\n private writeSignalFile(\n reason: string,\n ramPercent: number,\n heapPercent: number\n ): void {\n const signalDir = join(process.cwd(), '.stackmemory');\n if (!existsSync(signalDir)) {\n mkdirSync(signalDir, { recursive: true });\n }\n\n const signalPath = join(signalDir, '.memory-clear-signal');\n const signal = {\n timestamp: Date.now(),\n reason,\n ramPercent: Math.round(ramPercent * 100),\n heapPercent: Math.round(heapPercent * 100),\n };\n\n writeFileSync(signalPath, JSON.stringify(signal, null, 2));\n this.onLog('INFO', 'Signal file written', { path: signalPath });\n }\n\n private getStackMemoryBin(): string | null {\n const homeDir = homedir();\n\n const locations = [\n join(homeDir, '.stackmemory', 'bin', 'stackmemory'),\n join(homeDir, '.local', 'bin', 'stackmemory'),\n '/usr/local/bin/stackmemory',\n '/opt/homebrew/bin/stackmemory',\n ];\n\n for (const loc of locations) {\n if (existsSync(loc)) {\n return loc;\n }\n }\n\n try {\n const result = execSync('which stackmemory', {\n encoding: 'utf8',\n stdio: 'pipe',\n }).trim();\n if (result && existsSync(result)) {\n return result;\n }\n } catch {\n // Not in PATH\n }\n\n return null;\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }