@xopcai/xopc 0.0.30 → 0.0.31

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 (105) hide show
  1. package/dist/extensions/telegram/xopc.extension.json +1 -1
  2. package/dist/gateway/static/root/assets/agents-3u63Fw2Y.js +216 -0
  3. package/dist/gateway/static/root/assets/agents-3u63Fw2Y.js.map +1 -0
  4. package/dist/gateway/static/root/assets/{apps-page-CTChHQAu.js → apps-page-CWegY6Kp.js} +2 -2
  5. package/dist/gateway/static/root/assets/{apps-page-CTChHQAu.js.map → apps-page-CWegY6Kp.js.map} +1 -1
  6. package/dist/gateway/static/root/assets/channels-settings-CiyeXcTK.js +9 -0
  7. package/dist/gateway/static/root/assets/channels-settings-CiyeXcTK.js.map +1 -0
  8. package/dist/gateway/static/root/assets/cron-api-_j_79Zf5.js +3 -0
  9. package/dist/gateway/static/root/assets/cron-api-_j_79Zf5.js.map +1 -0
  10. package/dist/gateway/static/root/assets/cron-page-S86YNTtI.js +2 -0
  11. package/dist/gateway/static/root/assets/cron-page-S86YNTtI.js.map +1 -0
  12. package/dist/gateway/static/root/assets/dist-D0jxbvuz.js +2 -0
  13. package/dist/gateway/static/root/assets/{dist-UWGUW3x8.js.map → dist-D0jxbvuz.js.map} +1 -1
  14. package/dist/gateway/static/root/assets/{extension-debug-page-BwB4a4cK.js → extension-debug-page-DB630cW8.js} +2 -2
  15. package/dist/gateway/static/root/assets/{extension-debug-page-BwB4a4cK.js.map → extension-debug-page-DB630cW8.js.map} +1 -1
  16. package/dist/gateway/static/root/assets/{extension-page-CSWu2PHZ.js → extension-page-CnoPUBul.js} +2 -2
  17. package/dist/gateway/static/root/assets/{extension-page-CSWu2PHZ.js.map → extension-page-CnoPUBul.js.map} +1 -1
  18. package/dist/gateway/static/root/assets/{extension-settings-page-B12K2a13.js → extension-settings-page-BsiOkvBe.js} +2 -2
  19. package/dist/gateway/static/root/assets/{extension-settings-page-B12K2a13.js.map → extension-settings-page-BsiOkvBe.js.map} +1 -1
  20. package/dist/gateway/static/root/assets/{index-D0pFZ0OE.js → index-DHLmAIQl.js} +81 -81
  21. package/dist/gateway/static/root/assets/{index-D0pFZ0OE.js.map → index-DHLmAIQl.js.map} +1 -1
  22. package/dist/gateway/static/root/assets/index-DoPwy4aU.css +1 -0
  23. package/dist/gateway/static/root/assets/logs-page-Bndhenn2.js +2 -0
  24. package/dist/gateway/static/root/assets/logs-page-Bndhenn2.js.map +1 -0
  25. package/dist/gateway/static/root/assets/sessions-page-Q201-_lP.js +2 -0
  26. package/dist/gateway/static/root/assets/{sessions-page-DJkuWpOT.js.map → sessions-page-Q201-_lP.js.map} +1 -1
  27. package/dist/gateway/static/root/assets/settings-page-Cw75fpc6.js +2 -0
  28. package/dist/gateway/static/root/assets/settings-page-Cw75fpc6.js.map +1 -0
  29. package/dist/gateway/static/root/assets/skills-page-CVwEzD_J.js +3 -0
  30. package/dist/gateway/static/root/assets/skills-page-CVwEzD_J.js.map +1 -0
  31. package/dist/gateway/static/root/index.html +2 -2
  32. package/dist/package.js +1 -1
  33. package/dist/src/agent/orchestration/agent-orchestrator.js +1 -1
  34. package/dist/src/agent/service/process-direct-streaming.js +12 -1
  35. package/dist/src/agent/service/process-direct-streaming.js.map +1 -1
  36. package/dist/src/agent/service.d.ts +4 -0
  37. package/dist/src/agent/service.js +7 -1
  38. package/dist/src/agent/service.js.map +1 -1
  39. package/dist/src/agent/skills/marketplace/resolve-adapter.js +1 -1
  40. package/dist/src/agent/skills/marketplace/resolve-adapter.js.map +1 -1
  41. package/dist/src/config/schema.js +1 -0
  42. package/dist/src/config/schema.js.map +1 -1
  43. package/dist/src/cron/validation.js +1 -1
  44. package/dist/src/cron/validation.js.map +1 -1
  45. package/dist/src/gateway/hono/routes/sessions.js +124 -2
  46. package/dist/src/gateway/hono/routes/sessions.js.map +1 -1
  47. package/dist/src/gateway/hono/sse.js +9 -2
  48. package/dist/src/gateway/hono/sse.js.map +1 -1
  49. package/dist/src/gateway/service/run-gateway-agent.d.ts +1 -0
  50. package/dist/src/gateway/service/run-gateway-agent.js +18 -10
  51. package/dist/src/gateway/service/run-gateway-agent.js.map +1 -1
  52. package/dist/src/gateway/service.d.ts +23 -1
  53. package/dist/src/gateway/service.js +47 -3
  54. package/dist/src/gateway/service.js.map +1 -1
  55. package/dist/src/session/abort-cutoff.d.ts +6 -0
  56. package/dist/src/session/abort-cutoff.js +10 -0
  57. package/dist/src/session/abort-cutoff.js.map +1 -0
  58. package/dist/src/session/compaction-checkpoints.d.ts +8 -0
  59. package/dist/src/session/compaction-checkpoints.js +21 -0
  60. package/dist/src/session/compaction-checkpoints.js.map +1 -0
  61. package/dist/src/session/index.d.ts +8 -1
  62. package/dist/src/session/index.js +7 -1
  63. package/dist/src/session/manager.d.ts +26 -1
  64. package/dist/src/session/manager.js +39 -2
  65. package/dist/src/session/manager.js.map +1 -1
  66. package/dist/src/session/patch-metadata.d.ts +12 -0
  67. package/dist/src/session/patch-metadata.js +23 -0
  68. package/dist/src/session/patch-metadata.js.map +1 -0
  69. package/dist/src/session/search-index.d.ts +2 -0
  70. package/dist/src/session/search-index.js +30 -2
  71. package/dist/src/session/search-index.js.map +1 -1
  72. package/dist/src/session/session-context-for-llm.d.ts +32 -0
  73. package/dist/src/session/session-context-for-llm.js +60 -0
  74. package/dist/src/session/session-context-for-llm.js.map +1 -0
  75. package/dist/src/session/store.d.ts +36 -2
  76. package/dist/src/session/store.js +200 -28
  77. package/dist/src/session/store.js.map +1 -1
  78. package/dist/src/session/strip-webchat-early-save.d.ts +5 -0
  79. package/dist/src/session/strip-webchat-early-save.js +17 -0
  80. package/dist/src/session/strip-webchat-early-save.js.map +1 -0
  81. package/dist/src/session/transcript-format.d.ts +46 -0
  82. package/dist/src/session/transcript-format.js +88 -0
  83. package/dist/src/session/transcript-format.js.map +1 -0
  84. package/dist/src/session/types.d.ts +37 -0
  85. package/dist/src/session/types.js.map +1 -1
  86. package/dist/src/utils/logger/log-store.js +4 -3
  87. package/dist/src/utils/logger/log-store.js.map +1 -1
  88. package/package.json +1 -1
  89. package/dist/gateway/static/root/assets/agents-BfwtJOPK.js +0 -216
  90. package/dist/gateway/static/root/assets/agents-BfwtJOPK.js.map +0 -1
  91. package/dist/gateway/static/root/assets/channels-settings-BpwVOvvf.js +0 -9
  92. package/dist/gateway/static/root/assets/channels-settings-BpwVOvvf.js.map +0 -1
  93. package/dist/gateway/static/root/assets/cron-page-C_6AbVRf.js +0 -2
  94. package/dist/gateway/static/root/assets/cron-page-C_6AbVRf.js.map +0 -1
  95. package/dist/gateway/static/root/assets/cron-utils-DZ7pabh5.js +0 -3
  96. package/dist/gateway/static/root/assets/cron-utils-DZ7pabh5.js.map +0 -1
  97. package/dist/gateway/static/root/assets/dist-UWGUW3x8.js +0 -2
  98. package/dist/gateway/static/root/assets/index-C6itMrqR.css +0 -1
  99. package/dist/gateway/static/root/assets/logs-page-BXqha2gI.js +0 -2
  100. package/dist/gateway/static/root/assets/logs-page-BXqha2gI.js.map +0 -1
  101. package/dist/gateway/static/root/assets/sessions-page-DJkuWpOT.js +0 -2
  102. package/dist/gateway/static/root/assets/settings-page-CleZrGHy.js +0 -2
  103. package/dist/gateway/static/root/assets/settings-page-CleZrGHy.js.map +0 -1
  104. package/dist/gateway/static/root/assets/skills-page-D7NiIOzA.js +0 -3
  105. package/dist/gateway/static/root/assets/skills-page-D7NiIOzA.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"store.js","names":["parseRoutingSessionKey"],"sources":["../../../src/session/store.ts"],"sourcesContent":["// Session store - manages session persistence, indexing, compaction, and sliding window\n\nimport { randomUUID } from 'node:crypto';\nimport { readFile, mkdir, unlink, readdir, stat, copyFile } from 'fs/promises';\nimport { basename, join } from 'path';\nimport { existsSync } from 'fs';\nimport { writeTextAtomic } from '../infra/write-file-atomic.js';\nimport { resolveSessionsDir, FILENAMES } from '../config/paths.js';\nimport { resolveDefaultAgentId } from '../agent/agent-scope.js';\nimport type { Config } from '../config/schema.js';\nimport { resolveSessionShardRelativePath } from './shard-path.js';\nimport { parseSessionKey as parseRoutingSessionKey } from '../routing/session-key.js';\nimport type { AgentMessage } from '@mariozechner/pi-agent-core';\nimport { createLogger } from '../utils/logger.js';\nimport type {\n SessionMetadata,\n SessionDetail,\n SessionIndex,\n SessionListQuery,\n PaginatedResult,\n GlobalSessionStats,\n ExportFormat,\n SessionExport,\n} from './types.js';\nimport { SessionStatus } from './types.js';\nimport type { Message } from './types.js';\nimport { SessionCompactor, type CompactionConfig, type CompactionResult } from '../agent/memory/compaction.js';\nimport { SlidingWindow, type WindowConfig } from '../agent/memory/window.js';\nimport { cleanTrailingErrors, hasProblematicMessages } from '../agent/memory/message-sanitizer.js';\nimport { invalidateSessionSearchIndexCache } from './search-index-cache.js';\n\nconst log = createLogger('SessionStore');\n\nconst INDEX_VERSION = '1.0';\nconst DEFAULT_LIMIT = 50;\n/** Pre-compaction transcript snapshots per session file (same directory as `{safeKey}.json`). */\nconst MAX_COMPACTION_CHECKPOINTS = 15;\n\n/**\n * Session files live under `resolveSessionsDir(config, agentId)` (ADR-003), sharded by\n * `resolveSessionShardRelativePath(sessionKey)` (users/… vs system/heartbeat; web UI uses\n * compact `users/{agent}/web/{peerId}` for gateway/webchat direct sessions).\n */\nexport interface SessionStoreOptions {\n /** Loaded app config (required for session path resolution). */\n config: Config;\n /** Agent id for the session store root (default: configured default agent). */\n agentId?: string;\n /** Override storage root (tests); skips `resolveSessionsDir` */\n sessionsDir?: string;\n}\n\nexport class SessionStore {\n private sessionsDir: string;\n private archiveDir: string;\n private indexFile: string;\n private indexCache: SessionIndex | null = null;\n private indexCacheTime: number = 0;\n private indexDirty = false;\n private window: SlidingWindow;\n private compactor: SessionCompactor;\n /** Serialize index + transcript mutations (reentrant for nested store calls). */\n private storeMutationChain: Promise<void> = Promise.resolve();\n private storeMutationDepth = 0;\n\n constructor(\n options: SessionStoreOptions,\n windowConfig?: Partial<WindowConfig>,\n compactionConfig?: Partial<CompactionConfig>\n ) {\n const agentId = options.agentId ?? resolveDefaultAgentId(options.config);\n this.sessionsDir = options.sessionsDir ?? resolveSessionsDir(options.config, agentId);\n this.archiveDir = join(this.sessionsDir, 'archive');\n this.indexFile = join(this.sessionsDir, FILENAMES.SESSIONS_INDEX);\n this.window = new SlidingWindow(windowConfig);\n this.compactor = new SessionCompactor(compactionConfig);\n }\n\n /** Root directory of session JSON files (sharded). Used by `session_search` indexing. */\n getSessionsRoot(): string {\n return this.sessionsDir;\n }\n\n // ========== Initialization ==========\n\n async initialize(): Promise<void> {\n await mkdir(this.sessionsDir, { recursive: true });\n await mkdir(this.archiveDir, { recursive: true });\n\n if (!existsSync(this.indexFile)) {\n await this.rebuildIndex();\n } else {\n await this.loadIndex();\n }\n\n log.debug('Session store initialized');\n }\n\n private sessionPathsForKey(key: string): { dir: string; jsonPath: string; metaPath: string } {\n const safeKey = this.sanitizeKey(key);\n const shard = resolveSessionShardRelativePath(key);\n const dir = join(this.sessionsDir, shard);\n return {\n dir,\n jsonPath: join(dir, `${safeKey}.json`),\n metaPath: join(dir, `${safeKey}.meta.json`),\n };\n }\n\n private invalidateIndexCache(): void {\n this.indexCache = null;\n this.indexCacheTime = 0;\n }\n\n /**\n * Serialize mutations that touch the sessions index or transcript paths.\n * Reentrant: nested calls (e.g. applyCompaction → saveMessages) run inline without deadlocking.\n */\n private async runStoreMutation<T>(fn: () => Promise<T>): Promise<T> {\n if (this.storeMutationDepth > 0) {\n return fn();\n }\n const run = this.storeMutationChain.then(async () => {\n this.storeMutationDepth++;\n try {\n return await fn();\n } finally {\n this.storeMutationDepth--;\n }\n });\n this.storeMutationChain = run.then(() => undefined).catch(() => undefined);\n return run as Promise<T>;\n }\n\n private checkpointBasenamePrefix(safeKey: string): string {\n return `${safeKey}.compaction-backup.`;\n }\n\n private async pruneCompactionCheckpoints(dir: string, safeKey: string): Promise<void> {\n const prefix = this.checkpointBasenamePrefix(safeKey);\n let names: string[];\n try {\n names = await readdir(dir);\n } catch {\n return;\n }\n const candidates = names.filter((n) => n.startsWith(prefix) && n.endsWith('.json'));\n if (candidates.length <= MAX_COMPACTION_CHECKPOINTS) {\n return;\n }\n const stats = await Promise.all(\n candidates.map(async (name) => {\n const p = join(dir, name);\n try {\n const s = await stat(p);\n return { p, mtimeMs: s.mtimeMs };\n } catch {\n return { p, mtimeMs: 0 };\n }\n }),\n );\n stats.sort((a, b) => a.mtimeMs - b.mtimeMs);\n const removeCount = stats.length - MAX_COMPACTION_CHECKPOINTS;\n for (let i = 0; i < removeCount; i++) {\n try {\n await unlink(stats[i]!.p);\n } catch {\n /* ignore */\n }\n }\n }\n\n /** Best-effort copy of the current transcript before compaction replaces it. */\n private async captureCompactionCheckpointIfExists(key: string, jsonPath: string): Promise<void> {\n if (!existsSync(jsonPath)) {\n return;\n }\n const safeKey = this.sanitizeKey(key);\n const dir = join(this.sessionsDir, resolveSessionShardRelativePath(key));\n const backupPath = join(dir, `${this.checkpointBasenamePrefix(safeKey)}${randomUUID()}.json`);\n try {\n await copyFile(jsonPath, backupPath);\n await this.pruneCompactionCheckpoints(dir, safeKey);\n } catch (err) {\n log.warn({ err, key, jsonPath }, 'Compaction checkpoint copy failed (continuing)');\n }\n }\n\n // ========== Index Management ==========\n\n /**\n * Get sessions by agent ID\n */\n async getByAgent(agentId: string): Promise<SessionMetadata[]> {\n const index = await this.loadIndex();\n return (index.sessions || []).filter(\n (s) => s.routing?.agentId?.toLowerCase() === agentId.toLowerCase()\n );\n }\n\n /**\n * Get sessions by account ID\n */\n async getByAccount(accountId: string): Promise<SessionMetadata[]> {\n const index = await this.loadIndex();\n return (index.sessions || []).filter(\n (s) => s.routing?.accountId?.toLowerCase() === accountId.toLowerCase()\n );\n }\n\n /**\n * Get sessions by peer\n */\n async getByPeer(peerKind: string, peerId: string): Promise<SessionMetadata[]> {\n const index = await this.loadIndex();\n return (index.sessions || []).filter(\n (s) =>\n s.routing?.peerKind?.toLowerCase() === peerKind.toLowerCase() &&\n s.routing?.peerId?.toLowerCase() === peerId.toLowerCase()\n );\n }\n\n /**\n * Get main session for a DM conversation\n */\n async getMainSession(channel: string, accountId: string): Promise<SessionMetadata | null> {\n const index = await this.loadIndex();\n return (\n (index.sessions || []).find(\n (s) =>\n s.routing?.source?.toLowerCase() === channel.toLowerCase() &&\n s.routing?.accountId?.toLowerCase() === accountId.toLowerCase() &&\n s.routing?.peerKind?.toLowerCase() === 'dm' &&\n s.routing?.peerId === 'main'\n ) ?? null\n );\n }\n\n private async loadIndex(): Promise<SessionIndex> {\n try {\n // Check if index file has been modified\n const stats = await stat(this.indexFile);\n const mtime = stats.mtime.getTime();\n\n // If cache is valid and file hasn't changed, use cache\n if (this.indexCache && mtime <= this.indexCacheTime) {\n // Ensure sessions array exists\n if (!this.indexCache.sessions) {\n this.indexCache.sessions = [];\n }\n return this.indexCache;\n }\n\n // File has changed or cache is empty, reload\n const data = await readFile(this.indexFile, 'utf-8');\n const parsed = JSON.parse(data) as SessionIndex;\n // Ensure sessions array exists\n if (!parsed.sessions) {\n parsed.sessions = [];\n }\n this.indexCache = parsed;\n this.indexCacheTime = mtime;\n return this.indexCache;\n } catch {\n // Index corrupted or missing, rebuild\n return this.rebuildIndex();\n }\n }\n\n /**\n * Force refresh the index cache from disk\n */\n async refreshIndex(): Promise<void> {\n this.indexCache = null;\n this.indexCacheTime = 0;\n await this.loadIndex();\n }\n\n private async saveIndex(): Promise<void> {\n if (!this.indexCache) return;\n\n this.indexCache.lastUpdated = new Date().toISOString();\n await writeTextAtomic(this.indexFile, JSON.stringify(this.indexCache, null, 2));\n this.indexDirty = false;\n \n // Update cache time after saving\n try {\n const stats = await stat(this.indexFile);\n this.indexCacheTime = stats.mtime.getTime();\n } catch {\n this.indexCacheTime = Date.now();\n }\n }\n\n private async rebuildIndex(): Promise<SessionIndex> {\n return this.runStoreMutation(async () => {\n log.info('Rebuilding session index...');\n\n const sessions: SessionMetadata[] = [];\n\n // Scan sessions directory\n const files = await this.scanSessionFiles();\n\n for (const file of files) {\n if (file.endsWith('.json') && !file.endsWith('.meta.json')) {\n const stem = basename(file, '.json');\n if (stem.includes('.compaction-backup.')) {\n continue;\n }\n const key = this.fileNameToKey(stem);\n try {\n const metadata = await this.scanSessionFile(key);\n if (metadata) {\n sessions.push(metadata);\n }\n } catch (err) {\n log.warn({ key, err }, 'Failed to scan session file');\n }\n }\n }\n\n this.indexCache = {\n version: INDEX_VERSION,\n lastUpdated: new Date().toISOString(),\n sessions,\n };\n\n await this.saveIndex();\n\n // Update cache time after saving\n try {\n const stats = await stat(this.indexFile);\n this.indexCacheTime = stats.mtime.getTime();\n } catch {\n this.indexCacheTime = Date.now();\n }\n\n log.info({ count: sessions.length }, 'Session index rebuilt');\n\n return this.indexCache!;\n });\n }\n\n private async scanSessionFiles(): Promise<string[]> {\n const out: string[] = [];\n const walk = async (rel: string): Promise<void> => {\n const abs = join(this.sessionsDir, rel);\n let entries;\n try {\n entries = await readdir(abs, { withFileTypes: true });\n } catch {\n return;\n }\n for (const ent of entries) {\n const childRel = rel ? join(rel, ent.name) : ent.name;\n if (ent.isDirectory()) {\n if (ent.name === 'archive') continue;\n await walk(childRel);\n } else if (\n ent.name.endsWith('.json') &&\n ent.name !== FILENAMES.SESSIONS_INDEX &&\n !ent.name.endsWith('.meta.json') &&\n !ent.name.includes('.compaction-backup.')\n ) {\n out.push(childRel);\n }\n }\n };\n await walk('');\n return out;\n }\n\n private async scanSessionFile(key: string): Promise<SessionMetadata | null> {\n const messages = await this.loadMessages(key);\n if (messages.length === 0) return null;\n\n const { jsonPath } = this.sessionPathsForKey(key);\n const stats = await stat(jsonPath);\n\n const { channel, chatId } = this.parseSessionKey(key);\n const routing = this.extractRoutingFromKey(key, channel);\n const isCronSession = channel === 'cron';\n const isHeartbeatSession = channel === 'heartbeat';\n\n return {\n key,\n status: SessionStatus.ACTIVE,\n tags: [],\n createdAt: stats.birthtime.toISOString(),\n updatedAt: stats.mtime.toISOString(),\n lastAccessedAt: stats.mtime.toISOString(),\n messageCount: messages.length,\n estimatedTokens: this.estimateTokens(messages),\n compactedCount: 0,\n sourceChannel: channel,\n sourceChatId: chatId,\n routing,\n ...(isCronSession\n ? {\n sessionType: 'cron',\n customData: { cronJobId: chatId },\n }\n : {}),\n ...(isHeartbeatSession\n ? {\n sessionType: 'heartbeat',\n customData: { heartbeatTarget: chatId },\n }\n : {}),\n stats: {\n messageCount: messages.length,\n tokenCount: this.estimateTokens(messages),\n },\n };\n }\n\n /**\n * Extract routing metadata from session key\n */\n private extractRoutingFromKey(key: string, channel: string): SessionMetadata['routing'] {\n const parts = key.split(':');\n if (parts.length < 5) {\n return undefined;\n }\n\n const [agentId, source, accountId, peerKind, peerId, ...rest] = parts;\n \n let threadId: string | undefined;\n let scopeId: string | undefined;\n \n // Parse optional thread and scope\n for (let i = 0; i < rest.length; i++) {\n if (rest[i] === 'thread' && rest[i + 1]) {\n threadId = rest[i + 1];\n i++;\n } else if (rest[i] === 'scope' && rest[i + 1]) {\n scopeId = rest[i + 1];\n i++;\n }\n }\n\n return {\n agentId: agentId?.toLowerCase() || 'main',\n source: source?.toLowerCase() || channel,\n accountId: accountId?.toLowerCase() || 'default',\n peerKind: peerKind?.toLowerCase() || 'dm',\n peerId: peerId?.toLowerCase() || 'unknown',\n threadId,\n scopeId,\n };\n }\n\n // ========== CRUD Operations ==========\n\n async list(query: SessionListQuery = {}): Promise<PaginatedResult<SessionMetadata>> {\n const index = await this.loadIndex();\n let sessions = [...(index.sessions || [])];\n\n // Apply filters\n if (query.status) {\n const statuses = Array.isArray(query.status) ? query.status : [query.status];\n sessions = sessions.filter((s) => statuses.includes(s.status));\n }\n\n if (query.channel) {\n const channels = query.channel\n .split(',')\n .map((c) => c.trim())\n .filter(Boolean);\n if (channels.length === 0) {\n sessions = [];\n } else if (channels.length === 1) {\n sessions = sessions.filter((s) => s.sourceChannel === channels[0]);\n } else {\n sessions = sessions.filter((s) => channels.includes(s.sourceChannel));\n }\n }\n\n if (query.tags && query.tags.length > 0) {\n sessions = sessions.filter((s) => query.tags!.some((tag) => s.tags.includes(tag)));\n }\n\n if (query.search) {\n const searchLower = query.search.toLowerCase();\n sessions = sessions.filter(\n (s) =>\n s.key.toLowerCase().includes(searchLower) ||\n s.name?.toLowerCase().includes(searchLower) ||\n s.tags.some((t) => t.toLowerCase().includes(searchLower))\n );\n }\n\n // Apply sorting\n const sortBy = query.sortBy || 'updatedAt';\n const sortOrder = query.sortOrder || 'desc';\n\n sessions.sort((a, b) => {\n const aVal = a[sortBy];\n const bVal = b[sortBy];\n const comparison = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;\n return sortOrder === 'asc' ? comparison : -comparison;\n });\n\n // Apply pagination\n const total = sessions.length;\n const limit = query.limit || DEFAULT_LIMIT;\n const offset = query.offset || 0;\n const items = sessions.slice(offset, offset + limit);\n\n return {\n items,\n total,\n limit,\n offset,\n hasMore: offset + limit < total,\n };\n }\n\n async get(key: string): Promise<SessionDetail | null> {\n const metadata = await this.getMetadata(key);\n if (!metadata) return null;\n\n const messages = await this.loadMessages(key);\n\n return {\n ...metadata,\n messages: this.convertMessages(messages),\n };\n }\n\n async getMetadata(key: string): Promise<SessionMetadata | null> {\n const index = await this.loadIndex();\n const metadata = index.sessions.find((s) => s.key === key);\n\n if (!metadata) {\n // Try to load from file directly (orphaned session)\n const scanned = await this.scanSessionFile(key);\n if (!scanned) {\n return null;\n }\n await this.runStoreMutation(async () => {\n this.invalidateIndexCache();\n const fresh = await this.loadIndex();\n if (fresh.sessions.some((s) => s.key === key)) {\n return;\n }\n fresh.sessions.push(scanned);\n this.indexDirty = true;\n await this.saveIndex();\n invalidateSessionSearchIndexCache();\n });\n return scanned;\n }\n\n return metadata;\n }\n\n async updateMetadata(key: string, updates: Partial<SessionMetadata>): Promise<void> {\n return this.runStoreMutation(async () => {\n this.invalidateIndexCache();\n const index = await this.loadIndex();\n const idx = index.sessions.findIndex((s) => s.key === key);\n\n if (idx === -1) {\n throw new Error(`Session not found: ${key}`);\n }\n\n index.sessions[idx] = {\n ...index.sessions[idx],\n ...updates,\n updatedAt: new Date().toISOString(),\n };\n\n this.indexDirty = true;\n await this.saveIndex();\n\n log.debug({ key, updates }, 'Session metadata updated');\n });\n }\n\n async delete(key: string): Promise<boolean> {\n return this.runStoreMutation(async () => {\n this.invalidateIndexCache();\n const index = await this.loadIndex();\n const idx = index.sessions.findIndex((s) => s.key === key);\n\n const primary = this.sessionPathsForKey(key);\n\n for (const p of [primary.jsonPath]) {\n try {\n await unlink(p);\n } catch (err: any) {\n if (err.code !== 'ENOENT') throw err;\n }\n }\n for (const p of [primary.metaPath]) {\n try {\n await unlink(p);\n } catch (err: any) {\n if (err.code !== 'ENOENT') throw err;\n }\n }\n\n // Remove from index\n if (idx !== -1) {\n index.sessions.splice(idx, 1);\n this.indexDirty = true;\n await this.saveIndex();\n }\n\n log.info({ key }, 'Session deleted');\n return true;\n });\n }\n\n async deleteMany(keys: string[]): Promise<{ success: string[]; failed: string[] }> {\n const success: string[] = [];\n const failed: string[] = [];\n\n for (const key of keys) {\n try {\n await this.delete(key);\n success.push(key);\n } catch {\n failed.push(key);\n }\n }\n\n return { success, failed };\n }\n\n // ========== Status Operations ==========\n\n async setStatus(key: string, status: SessionStatus): Promise<void> {\n await this.updateMetadata(key, { status });\n\n if (status === SessionStatus.ARCHIVED) {\n await this.moveToArchive(key);\n } else {\n await this.moveFromArchive(key);\n }\n }\n\n async archive(key: string): Promise<void> {\n await this.setStatus(key, SessionStatus.ARCHIVED);\n }\n\n async unarchive(key: string): Promise<void> {\n await this.setStatus(key, SessionStatus.ACTIVE);\n }\n\n async pin(key: string): Promise<void> {\n await this.setStatus(key, SessionStatus.PINNED);\n }\n\n async unpin(key: string): Promise<void> {\n await this.setStatus(key, SessionStatus.ACTIVE);\n }\n\n // ========== Message Operations ==========\n\n async loadMessages(key: string, options?: { fromArchive?: boolean }): Promise<AgentMessage[]> {\n const primary = this.sessionPathsForKey(key);\n\n const readAndNormalize = async (path: string): Promise<AgentMessage[] | null> => {\n try {\n const data = await readFile(path, 'utf-8');\n const messages = JSON.parse(data) as AgentMessage[];\n if (hasProblematicMessages(messages)) {\n const cleaned = cleanTrailingErrors(messages);\n if (cleaned.length !== messages.length) {\n log.info(\n { key, original: messages.length, cleaned: cleaned.length },\n 'Cleaned problematic messages on load'\n );\n }\n return cleaned;\n }\n return messages;\n } catch {\n return null;\n }\n };\n\n const messages = await readAndNormalize(primary.jsonPath);\n\n if (messages !== null) {\n return messages;\n }\n\n if (options?.fromArchive) {\n const archivedFile = await this.findMostRecentArchive(key);\n if (!archivedFile) {\n return [];\n }\n const archived = await readAndNormalize(archivedFile);\n return archived ?? [];\n }\n return [];\n }\n\n /**\n * Find the most recent archived session file for a given key.\n * Archived files have format: {safeKey}.{timestamp}.json\n */\n private async findMostRecentArchive(sessionKey: string): Promise<string | null> {\n const safeKey = this.sanitizeKey(sessionKey);\n const shardDir = join(this.archiveDir, resolveSessionShardRelativePath(sessionKey));\n\n const scanDir = async (dir: string): Promise<string | null> => {\n try {\n const files = await readdir(dir);\n const matchingFiles = files\n .filter(\n (f) =>\n f.startsWith(`${safeKey}.`) &&\n f.endsWith('.json') &&\n !f.endsWith('.meta.json') &&\n !f.includes('.compaction-backup.'),\n )\n .sort()\n .reverse();\n if (matchingFiles.length === 0) return null;\n return join(dir, matchingFiles[0]);\n } catch {\n return null;\n }\n };\n\n const inShard = await scanDir(shardDir);\n if (inShard) return inShard;\n return await scanDir(this.archiveDir);\n }\n\n /**\n * Persist transcript JSON + merge session row into the index. Caller must hold {@link runStoreMutation} (or be nested under it).\n */\n private async writeSessionTranscriptAndUpdateIndex(\n key: string,\n messages: AgentMessage[],\n ): Promise<void> {\n const { dir, jsonPath } = this.sessionPathsForKey(key);\n\n await mkdir(dir, { recursive: true });\n await writeTextAtomic(jsonPath, JSON.stringify(messages, null, 2));\n\n const index = await this.loadIndex();\n const existingIdx = index.sessions.findIndex((s) => s.key === key);\n const now = new Date().toISOString();\n\n const { channel, chatId } = this.parseSessionKey(key);\n const routing = this.extractRoutingFromKey(key, channel);\n const isCronSession = channel === 'cron';\n const isHeartbeatSession = channel === 'heartbeat';\n\n if (existingIdx !== -1) {\n const prev = index.sessions[existingIdx];\n index.sessions[existingIdx] = {\n ...prev,\n sourceChannel: channel,\n sourceChatId: chatId,\n messageCount: messages.length,\n estimatedTokens: this.estimateTokens(messages),\n updatedAt: now,\n lastAccessedAt: now,\n routing: routing || prev.routing,\n ...(isCronSession\n ? {\n sessionType: 'cron',\n customData: {\n ...prev.customData,\n cronJobId: chatId,\n },\n }\n : {}),\n ...(isHeartbeatSession\n ? {\n sessionType: 'heartbeat',\n customData: {\n ...prev.customData,\n heartbeatTarget: chatId,\n },\n }\n : {}),\n stats: {\n ...prev.stats,\n messageCount: messages.length,\n tokenCount: this.estimateTokens(messages),\n lastTurnAt: Date.now(),\n },\n };\n } else {\n index.sessions.push({\n key,\n status: SessionStatus.ACTIVE,\n tags: [],\n createdAt: now,\n updatedAt: now,\n lastAccessedAt: now,\n messageCount: messages.length,\n estimatedTokens: this.estimateTokens(messages),\n compactedCount: 0,\n sourceChannel: channel,\n sourceChatId: chatId,\n routing,\n ...(isCronSession\n ? {\n sessionType: 'cron',\n customData: { cronJobId: chatId },\n }\n : {}),\n ...(isHeartbeatSession\n ? {\n sessionType: 'heartbeat',\n customData: { heartbeatTarget: chatId },\n }\n : {}),\n stats: {\n messageCount: messages.length,\n tokenCount: this.estimateTokens(messages),\n lastTurnAt: Date.now(),\n },\n });\n }\n\n this.indexDirty = true;\n await this.saveIndex();\n\n invalidateSessionSearchIndexCache();\n }\n\n async saveMessages(key: string, messages: AgentMessage[]): Promise<void> {\n return this.runStoreMutation(async () => {\n this.invalidateIndexCache();\n await this.writeSessionTranscriptAndUpdateIndex(key, messages);\n });\n }\n\n // ========== Sliding Window & Compaction ==========\n\n /**\n * Get window stats for messages\n */\n getWindowStats(messages: AgentMessage[]) {\n return this.window.getStats(messages);\n }\n\n /**\n * Check if session needs compaction\n */\n needsCompaction(key: string, messages: AgentMessage[], contextWindow: number) {\n return this.compactor.needsCompaction(messages, contextWindow);\n }\n\n /**\n * Prepare compaction (check if needed)\n */\n prepareCompaction(\n key: string,\n messages: AgentMessage[],\n contextWindow: number\n ): { needsCompaction: boolean; messages: AgentMessage[]; stats?: ReturnType<typeof this.compactor.needsCompaction> } {\n const result = this.compactor.needsCompaction(messages, contextWindow);\n return {\n needsCompaction: result.needed,\n messages,\n stats: result,\n };\n }\n\n /**\n * Apply compaction result to messages\n */\n async applyCompaction(\n key: string,\n messages: AgentMessage[],\n result: CompactionResult\n ): Promise<AgentMessage[]> {\n const compacted = this.compactor.applyCompaction(messages, result);\n\n return this.runStoreMutation(async () => {\n this.invalidateIndexCache();\n const { jsonPath } = this.sessionPathsForKey(key);\n await this.captureCompactionCheckpointIfExists(key, jsonPath);\n\n await this.writeSessionTranscriptAndUpdateIndex(key, compacted);\n\n const metadata = await this.getMetadata(key);\n if (metadata) {\n await this.updateMetadata(key, {\n compactedCount: metadata.compactedCount + 1,\n });\n }\n\n log.info(\n {\n key,\n tokensBefore: result.tokensBefore,\n tokensAfter: result.tokensAfter,\n keptMessages: compacted.length,\n },\n 'Session compacted',\n );\n\n return compacted;\n });\n }\n\n /**\n * Compact session with LLM summary\n */\n async compact(\n key: string,\n messages: AgentMessage[],\n contextWindow: number,\n instructions?: string,\n force?: boolean,\n ): Promise<CompactionResult> {\n const result = await this.compactor.compact(messages, instructions, force);\n \n if (result.compacted) {\n await this.applyCompaction(key, messages, result);\n }\n \n return result;\n }\n\n /**\n * Get compaction stats for a session\n */\n async getCompactionStats(key: string) {\n const metadata = await this.getMetadata(key);\n if (!metadata) return undefined;\n \n return {\n compactionCount: metadata.compactedCount,\n totalTokensBefore: 0,\n totalTokensAfter: 0,\n lastCompactionAt: undefined,\n };\n }\n\n // ========== MemoryStore API Aliases ==========\n\n /** Alias for delete */\n async deleteSession(key: string): Promise<boolean> {\n return this.delete(key);\n }\n\n /** Alias for loadMessages */\n async load(key: string, options?: { fromArchive?: boolean }): Promise<AgentMessage[]> {\n return this.loadMessages(key, options);\n }\n\n /** Alias for saveMessages */\n async save(key: string, messages: AgentMessage[]): Promise<void> {\n return this.saveMessages(key, messages);\n }\n\n /** Alias for estimateTokens */\n async estimateTokenUsage(key: string, messages: AgentMessage[]): Promise<number> {\n return this.estimateTokens(messages);\n }\n\n // ========== Search ==========\n\n async searchInSession(key: string, keyword: string): Promise<Message[]> {\n const messages = await this.loadMessages(key);\n const keywordLower = keyword.toLowerCase();\n\n return this.convertMessages(\n messages.filter((m) => {\n const content = this.extractTextContent(m.content);\n return content.toLowerCase().includes(keywordLower);\n })\n );\n }\n\n // ========== Export/Import ==========\n\n async exportSession(key: string, format: ExportFormat): Promise<string> {\n const detail = await this.get(key);\n if (!detail) {\n throw new Error(`Session not found: ${key}`);\n }\n\n if (format === 'json') {\n const exportData: SessionExport = {\n version: INDEX_VERSION,\n exportedAt: new Date().toISOString(),\n metadata: detail,\n messages: detail.messages,\n };\n return JSON.stringify(exportData, null, 2);\n } else {\n // Markdown format\n const lines = [\n `# ${detail.name || detail.key}`,\n '',\n `- **Channel:** ${detail.sourceChannel}`,\n `- **Created:** ${detail.createdAt}`,\n `- **Messages:** ${detail.messageCount}`,\n `- **Tags:** ${detail.tags.join(', ') || 'none'}`,\n '',\n '---',\n '',\n ];\n\n for (const msg of detail.messages) {\n const role = msg.role === 'assistant' ? 'Assistant' : msg.role === 'user' ? 'User' : msg.role;\n lines.push(`## ${role}`);\n lines.push('');\n const body =\n typeof msg.content === 'string'\n ? msg.content\n : JSON.stringify(msg.content, null, 2);\n lines.push(body);\n lines.push('');\n lines.push('---');\n lines.push('');\n }\n\n return lines.join('\\n');\n }\n }\n\n // ========== Statistics ==========\n\n async getStats(): Promise<GlobalSessionStats> {\n const index = await this.loadIndex();\n const sessions = index.sessions;\n\n const byChannel: Record<string, number> = {};\n for (const s of sessions) {\n byChannel[s.sourceChannel] = (byChannel[s.sourceChannel] || 0) + 1;\n }\n\n let oldestSession: string | undefined;\n let newestSession: string | undefined;\n\n if (sessions.length > 0) {\n const sorted = [...sessions].sort((a, b) => a.createdAt.localeCompare(b.createdAt));\n oldestSession = sorted[0].createdAt;\n newestSession = sorted[sorted.length - 1].createdAt;\n }\n\n return {\n totalSessions: sessions.length,\n activeSessions: sessions.filter((s) => s.status === SessionStatus.ACTIVE || s.status === SessionStatus.IDLE).length,\n archivedSessions: sessions.filter((s) => s.status === SessionStatus.ARCHIVED).length,\n pinnedSessions: sessions.filter((s) => s.status === SessionStatus.PINNED).length,\n totalMessages: sessions.reduce((sum, s) => sum + s.messageCount, 0),\n totalTokens: sessions.reduce((sum, s) => sum + s.estimatedTokens, 0),\n oldestSession,\n newestSession,\n byChannel,\n };\n }\n\n // ========== Cleanup ==========\n\n async archiveOld(olderThanDays: number): Promise<number> {\n const cutoff = new Date();\n cutoff.setDate(cutoff.getDate() - olderThanDays);\n\n const index = await this.loadIndex();\n let archived = 0;\n\n for (const session of index.sessions) {\n if (session.status !== SessionStatus.ARCHIVED && session.status !== SessionStatus.PINNED) {\n const lastAccess = new Date(session.lastAccessedAt);\n if (lastAccess < cutoff) {\n await this.archive(session.key);\n archived++;\n }\n }\n }\n\n return archived;\n }\n\n // ========== Helper Methods ==========\n\n private sanitizeKey(key: string): string {\n return key.replace(/[^a-zA-Z0-9_-]/g, '_');\n }\n\n private fileNameToKey(fileName: string): string {\n // Reverse of sanitizeKey - restore all colons from underscores\n // telegram_dm_123456 -> telegram:dm:123456\n // telegram_g_-100123456_t_789 -> telegram:g:-100123456:t:789\n return fileName.replace(/_/g, ':');\n }\n\n private parseSessionKey(key: string): { channel: string; chatId: string } {\n const parts = key.split(':');\n // Session key format: {agentId}:{source}:{accountId}:{peerKind}:{peerId}\n if (parts.length >= 5) {\n const parsed = parseRoutingSessionKey(key);\n if (parsed?.source === 'cron') {\n return { channel: 'cron', chatId: parsed.peerId };\n }\n return { channel: parts[1], chatId: parts.slice(2).join(':') };\n }\n // Gateway heartbeat: `heartbeat:main` / `heartbeat:isolated:<ts>`\n if (parts.length >= 2 && parts[0] === 'heartbeat') {\n return { channel: 'heartbeat', chatId: parts.slice(1).join(':') };\n }\n return { channel: 'unknown', chatId: key };\n }\n\n estimateTokens(messages: AgentMessage[]): number {\n // Rough estimate: 1 token ≈ 4 characters\n let total = 0;\n for (const msg of messages) {\n const text = this.extractTextContent(msg.content);\n total += Math.ceil(text.length / 4);\n }\n return total;\n }\n\n private extractTextContent(content: unknown): string {\n if (typeof content === 'string') return content;\n if (Array.isArray(content)) {\n const parts: string[] = [];\n for (const item of content) {\n if (typeof item !== 'object' || item === null || !('type' in item)) continue;\n const c = item as { type?: string; text?: string; name?: string };\n if (c.type === 'text' && typeof c.text === 'string') {\n parts.push(c.text);\n } else if (c.type === 'toolCall' || c.type === 'tool_use') {\n parts.push(c.name ? `[${c.name}]` : '');\n }\n }\n return parts.join('');\n }\n return '';\n }\n\n private convertMessages(messages: AgentMessage[]): Message[] {\n return messages.map((m: any) => {\n const c = m.content;\n const content: string | unknown[] =\n typeof c === 'string'\n ? c\n : Array.isArray(c)\n ? c\n : this.extractTextContent(c);\n\n const row: Message = {\n role: m.role as 'system' | 'user' | 'assistant' | 'tool' | 'toolResult',\n content,\n timestamp: m.timestamp ? new Date(m.timestamp).toISOString() : undefined,\n tool_call_id: m.tool_call_id || m.toolCallId,\n tool_calls: m.tool_calls,\n name: m.name,\n };\n if (Array.isArray(m.attachments) && m.attachments.length > 0) {\n row.attachments = m.attachments;\n }\n return row;\n });\n }\n\n private async moveToArchive(key: string): Promise<void> {\n return this.runStoreMutation(async () => {\n const safeKey = this.sanitizeKey(key);\n const primary = this.sessionPathsForKey(key);\n const sourcePath = existsSync(primary.jsonPath) ? primary.jsonPath : null;\n if (!sourcePath) {\n return;\n }\n\n const timestamp = new Date().toISOString().replace(/[:.]/g, '-');\n const archiveShard = join(this.archiveDir, resolveSessionShardRelativePath(key));\n await mkdir(archiveShard, { recursive: true });\n const targetPath = join(archiveShard, `${safeKey}.${timestamp}.json`);\n\n try {\n const data = await readFile(sourcePath, 'utf-8');\n await writeTextAtomic(targetPath, data);\n await unlink(sourcePath);\n\n const metaSource = primary.metaPath;\n const metaTarget = join(archiveShard, `${safeKey}.${timestamp}.meta.json`);\n try {\n const metaData = await readFile(metaSource, 'utf-8');\n await writeTextAtomic(metaTarget, metaData);\n await unlink(metaSource);\n } catch {\n // Meta file might not exist\n }\n } catch (err: any) {\n if (err.code !== 'ENOENT') throw err;\n }\n });\n }\n\n private async moveFromArchive(key: string): Promise<void> {\n return this.runStoreMutation(async () => {\n const sourcePath = await this.findMostRecentArchive(key);\n if (!sourcePath) {\n return;\n }\n\n const primary = this.sessionPathsForKey(key);\n await mkdir(primary.dir, { recursive: true });\n const targetPath = primary.jsonPath;\n\n try {\n const data = await readFile(sourcePath, 'utf-8');\n await writeTextAtomic(targetPath, data);\n await unlink(sourcePath);\n\n const metaSource = sourcePath.replace('.json', '.meta.json');\n const metaTarget = primary.metaPath;\n try {\n const metaData = await readFile(metaSource, 'utf-8');\n await writeTextAtomic(metaTarget, metaData);\n await unlink(metaSource);\n } catch {\n // Meta file might not exist\n }\n } catch (err: any) {\n if (err.code !== 'ENOENT') throw err;\n }\n });\n }\n\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"store.js","names":["parseRoutingSessionKey"],"sources":["../../../src/session/store.ts"],"sourcesContent":["// Session store - manages session persistence, indexing, compaction, and sliding window\n\nimport { randomUUID } from 'node:crypto';\nimport { readFile, mkdir, unlink, readdir, stat, copyFile } from 'fs/promises';\nimport { basename, join } from 'path';\nimport { existsSync } from 'fs';\nimport { writeTextAtomic } from '../infra/write-file-atomic.js';\nimport { resolveSessionsDir, FILENAMES } from '../config/paths.js';\nimport { resolveDefaultAgentId } from '../agent/agent-scope.js';\nimport type { Config } from '../config/schema.js';\nimport { resolveSessionShardRelativePath } from './shard-path.js';\nimport { parseSessionKey as parseRoutingSessionKey } from '../routing/session-key.js';\nimport type { AgentMessage } from '@mariozechner/pi-agent-core';\nimport { createLogger } from '../utils/logger.js';\nimport type {\n SessionMetadata,\n SessionDetail,\n SessionIndex,\n SessionListQuery,\n PaginatedResult,\n GlobalSessionStats,\n ExportFormat,\n SessionExport,\n SessionTranscriptSummary,\n CompactionCheckpointSummary,\n CompactionCheckpointDetail,\n} from './types.js';\nimport { SessionStatus } from './types.js';\nimport type { Message } from './types.js';\nimport { SessionCompactor, type CompactionConfig, type CompactionResult } from '../agent/memory/compaction.js';\nimport { SlidingWindow, type WindowConfig } from '../agent/memory/window.js';\nimport { invalidateSessionSearchIndexCache } from './search-index-cache.js';\nimport {\n buildTranscriptEnvelope,\n parseStoredTranscriptJson,\n type TranscriptCompactionRecord,\n type XopcSessionTranscriptV1,\n} from './transcript-format.js';\nimport {\n buildSessionContextForLlm,\n isTranscriptContextEntry,\n mergeLlmMessagesPreservingContextRows,\n type TranscriptStoredRow,\n type XopcTranscriptContextEntry,\n} from './session-context-for-llm.js';\nimport { normalizeCompactionCheckpointId } from './compaction-checkpoints.js';\n\nconst log = createLogger('SessionStore');\n\nconst INDEX_VERSION = '1.0';\nconst DEFAULT_LIMIT = 50;\n/** Pre-compaction transcript snapshots per session file (same directory as `{safeKey}.json`). */\nconst MAX_COMPACTION_CHECKPOINTS = 15;\n\n/**\n * Session files live under `resolveSessionsDir(config, agentId)` (ADR-003), sharded by\n * `resolveSessionShardRelativePath(sessionKey)` (users/… vs system/heartbeat; web UI uses\n * compact `users/{agent}/web/{peerId}` for gateway/webchat direct sessions).\n */\nexport interface SessionStoreOptions {\n /** Loaded app config (required for session path resolution). */\n config: Config;\n /** Agent id for the session store root (default: configured default agent). */\n agentId?: string;\n /** Override storage root (tests); skips `resolveSessionsDir` */\n sessionsDir?: string;\n}\n\nexport class SessionStore {\n private sessionsDir: string;\n private archiveDir: string;\n private indexFile: string;\n private indexCache: SessionIndex | null = null;\n private indexCacheTime: number = 0;\n private indexDirty = false;\n private window: SlidingWindow;\n private compactor: SessionCompactor;\n /** Serialize index + transcript mutations (reentrant for nested store calls). */\n private storeMutationChain: Promise<void> = Promise.resolve();\n private storeMutationDepth = 0;\n\n constructor(\n options: SessionStoreOptions,\n windowConfig?: Partial<WindowConfig>,\n compactionConfig?: Partial<CompactionConfig>\n ) {\n const agentId = options.agentId ?? resolveDefaultAgentId(options.config);\n this.sessionsDir = options.sessionsDir ?? resolveSessionsDir(options.config, agentId);\n this.archiveDir = join(this.sessionsDir, 'archive');\n this.indexFile = join(this.sessionsDir, FILENAMES.SESSIONS_INDEX);\n this.window = new SlidingWindow(windowConfig);\n this.compactor = new SessionCompactor(compactionConfig);\n }\n\n /** Root directory of session JSON files (sharded). Used by `session_search` indexing. */\n getSessionsRoot(): string {\n return this.sessionsDir;\n }\n\n // ========== Initialization ==========\n\n async initialize(): Promise<void> {\n await mkdir(this.sessionsDir, { recursive: true });\n await mkdir(this.archiveDir, { recursive: true });\n\n if (!existsSync(this.indexFile)) {\n await this.rebuildIndex();\n } else {\n await this.loadIndex();\n }\n\n log.debug('Session store initialized');\n }\n\n private sessionPathsForKey(key: string): { dir: string; jsonPath: string; metaPath: string } {\n const safeKey = this.sanitizeKey(key);\n const shard = resolveSessionShardRelativePath(key);\n const dir = join(this.sessionsDir, shard);\n return {\n dir,\n jsonPath: join(dir, `${safeKey}.json`),\n metaPath: join(dir, `${safeKey}.meta.json`),\n };\n }\n\n private invalidateIndexCache(): void {\n this.indexCache = null;\n this.indexCacheTime = 0;\n }\n\n /**\n * Serialize mutations that touch the sessions index or transcript paths.\n * Reentrant: nested calls (e.g. applyCompaction → saveMessages) run inline without deadlocking.\n */\n private async runStoreMutation<T>(fn: () => Promise<T>): Promise<T> {\n if (this.storeMutationDepth > 0) {\n return fn();\n }\n const run = this.storeMutationChain.then(async () => {\n this.storeMutationDepth++;\n try {\n return await fn();\n } finally {\n this.storeMutationDepth--;\n }\n });\n this.storeMutationChain = run.then(() => undefined).catch(() => undefined);\n return run as Promise<T>;\n }\n\n private checkpointBasenamePrefix(safeKey: string): string {\n return `${safeKey}.compaction-backup.`;\n }\n\n private async pruneCompactionCheckpoints(dir: string, safeKey: string): Promise<void> {\n const prefix = this.checkpointBasenamePrefix(safeKey);\n let names: string[];\n try {\n names = await readdir(dir);\n } catch {\n return;\n }\n const candidates = names.filter((n) => n.startsWith(prefix) && n.endsWith('.json'));\n if (candidates.length <= MAX_COMPACTION_CHECKPOINTS) {\n return;\n }\n const stats = await Promise.all(\n candidates.map(async (name) => {\n const p = join(dir, name);\n try {\n const s = await stat(p);\n return { p, mtimeMs: s.mtimeMs };\n } catch {\n return { p, mtimeMs: 0 };\n }\n }),\n );\n stats.sort((a, b) => a.mtimeMs - b.mtimeMs);\n const removeCount = stats.length - MAX_COMPACTION_CHECKPOINTS;\n for (let i = 0; i < removeCount; i++) {\n try {\n await unlink(stats[i]!.p);\n } catch {\n /* ignore */\n }\n }\n }\n\n /** Best-effort copy of the current transcript before compaction replaces it. */\n private async captureCompactionCheckpointIfExists(key: string, jsonPath: string): Promise<void> {\n if (!existsSync(jsonPath)) {\n return;\n }\n const safeKey = this.sanitizeKey(key);\n const dir = join(this.sessionsDir, resolveSessionShardRelativePath(key));\n const backupPath = join(dir, `${this.checkpointBasenamePrefix(safeKey)}${randomUUID()}.json`);\n try {\n await copyFile(jsonPath, backupPath);\n await this.pruneCompactionCheckpoints(dir, safeKey);\n } catch (err) {\n log.warn({ err, key, jsonPath }, 'Compaction checkpoint copy failed (continuing)');\n }\n }\n\n // ========== Index Management ==========\n\n /**\n * Get sessions by agent ID\n */\n async getByAgent(agentId: string): Promise<SessionMetadata[]> {\n const index = await this.loadIndex();\n return (index.sessions || []).filter(\n (s) => s.routing?.agentId?.toLowerCase() === agentId.toLowerCase()\n );\n }\n\n /**\n * Get sessions by account ID\n */\n async getByAccount(accountId: string): Promise<SessionMetadata[]> {\n const index = await this.loadIndex();\n return (index.sessions || []).filter(\n (s) => s.routing?.accountId?.toLowerCase() === accountId.toLowerCase()\n );\n }\n\n /**\n * Get sessions by peer\n */\n async getByPeer(peerKind: string, peerId: string): Promise<SessionMetadata[]> {\n const index = await this.loadIndex();\n return (index.sessions || []).filter(\n (s) =>\n s.routing?.peerKind?.toLowerCase() === peerKind.toLowerCase() &&\n s.routing?.peerId?.toLowerCase() === peerId.toLowerCase()\n );\n }\n\n /**\n * Get main session for a DM conversation\n */\n async getMainSession(channel: string, accountId: string): Promise<SessionMetadata | null> {\n const index = await this.loadIndex();\n return (\n (index.sessions || []).find(\n (s) =>\n s.routing?.source?.toLowerCase() === channel.toLowerCase() &&\n s.routing?.accountId?.toLowerCase() === accountId.toLowerCase() &&\n s.routing?.peerKind?.toLowerCase() === 'dm' &&\n s.routing?.peerId === 'main'\n ) ?? null\n );\n }\n\n private async loadIndex(): Promise<SessionIndex> {\n try {\n // Check if index file has been modified\n const stats = await stat(this.indexFile);\n const mtime = stats.mtime.getTime();\n\n // If cache is valid and file hasn't changed, use cache\n if (this.indexCache && mtime <= this.indexCacheTime) {\n // Ensure sessions array exists\n if (!this.indexCache.sessions) {\n this.indexCache.sessions = [];\n }\n return this.indexCache;\n }\n\n // File has changed or cache is empty, reload\n const data = await readFile(this.indexFile, 'utf-8');\n const parsed = JSON.parse(data) as SessionIndex;\n // Ensure sessions array exists\n if (!parsed.sessions) {\n parsed.sessions = [];\n }\n this.indexCache = parsed;\n this.indexCacheTime = mtime;\n return this.indexCache;\n } catch {\n // Index corrupted or missing, rebuild\n return this.rebuildIndex();\n }\n }\n\n /**\n * Force refresh the index cache from disk\n */\n async refreshIndex(): Promise<void> {\n this.indexCache = null;\n this.indexCacheTime = 0;\n await this.loadIndex();\n }\n\n private async saveIndex(): Promise<void> {\n if (!this.indexCache) return;\n\n this.indexCache.lastUpdated = new Date().toISOString();\n await writeTextAtomic(this.indexFile, JSON.stringify(this.indexCache, null, 2));\n this.indexDirty = false;\n \n // Update cache time after saving\n try {\n const stats = await stat(this.indexFile);\n this.indexCacheTime = stats.mtime.getTime();\n } catch {\n this.indexCacheTime = Date.now();\n }\n }\n\n private async rebuildIndex(): Promise<SessionIndex> {\n return this.runStoreMutation(async () => {\n log.info('Rebuilding session index...');\n\n const sessions: SessionMetadata[] = [];\n\n // Scan sessions directory\n const files = await this.scanSessionFiles();\n\n for (const file of files) {\n if (file.endsWith('.json') && !file.endsWith('.meta.json')) {\n const stem = basename(file, '.json');\n if (stem.includes('.compaction-backup.')) {\n continue;\n }\n const key = this.fileNameToKey(stem);\n try {\n const metadata = await this.scanSessionFile(key);\n if (metadata) {\n sessions.push(metadata);\n }\n } catch (err) {\n log.warn({ key, err }, 'Failed to scan session file');\n }\n }\n }\n\n this.indexCache = {\n version: INDEX_VERSION,\n lastUpdated: new Date().toISOString(),\n sessions,\n };\n\n await this.saveIndex();\n\n // Update cache time after saving\n try {\n const stats = await stat(this.indexFile);\n this.indexCacheTime = stats.mtime.getTime();\n } catch {\n this.indexCacheTime = Date.now();\n }\n\n log.info({ count: sessions.length }, 'Session index rebuilt');\n\n return this.indexCache!;\n });\n }\n\n private async scanSessionFiles(): Promise<string[]> {\n const out: string[] = [];\n const walk = async (rel: string): Promise<void> => {\n const abs = join(this.sessionsDir, rel);\n let entries;\n try {\n entries = await readdir(abs, { withFileTypes: true });\n } catch {\n return;\n }\n for (const ent of entries) {\n const childRel = rel ? join(rel, ent.name) : ent.name;\n if (ent.isDirectory()) {\n if (ent.name === 'archive') continue;\n await walk(childRel);\n } else if (\n ent.name.endsWith('.json') &&\n ent.name !== FILENAMES.SESSIONS_INDEX &&\n !ent.name.endsWith('.meta.json') &&\n !ent.name.includes('.compaction-backup.')\n ) {\n out.push(childRel);\n }\n }\n };\n await walk('');\n return out;\n }\n\n private async scanSessionFile(key: string): Promise<SessionMetadata | null> {\n const { jsonPath } = this.sessionPathsForKey(key);\n let raw: string;\n try {\n raw = await readFile(jsonPath, 'utf-8');\n } catch {\n return null;\n }\n const { messages, envelope } = parseStoredTranscriptJson(raw);\n if (messages.length === 0) return null;\n\n const stats = await stat(jsonPath);\n const sessionStartedAt = envelope?.createdAt ?? stats.birthtime.toISOString();\n const lastInteractionAt = envelope?.updatedAt ?? stats.mtime.toISOString();\n\n const { channel, chatId } = this.parseSessionKey(key);\n const routing = this.extractRoutingFromKey(key, channel);\n const isCronSession = channel === 'cron';\n const isHeartbeatSession = channel === 'heartbeat';\n\n return {\n key,\n status: SessionStatus.ACTIVE,\n tags: [],\n createdAt: stats.birthtime.toISOString(),\n updatedAt: stats.mtime.toISOString(),\n lastAccessedAt: stats.mtime.toISOString(),\n messageCount: messages.length,\n estimatedTokens: this.estimateTokens(messages),\n compactedCount: 0,\n sourceChannel: channel,\n sourceChatId: chatId,\n ...(envelope?.id ? { transcriptId: envelope.id } : {}),\n sessionStartedAt,\n lastInteractionAt,\n routing,\n ...(isCronSession\n ? {\n sessionType: 'cron',\n customData: { cronJobId: chatId },\n }\n : {}),\n ...(isHeartbeatSession\n ? {\n sessionType: 'heartbeat',\n customData: { heartbeatTarget: chatId },\n }\n : {}),\n stats: {\n messageCount: messages.length,\n tokenCount: this.estimateTokens(messages),\n },\n };\n }\n\n /**\n * Extract routing metadata from session key\n */\n private extractRoutingFromKey(key: string, channel: string): SessionMetadata['routing'] {\n const parts = key.split(':');\n if (parts.length < 5) {\n return undefined;\n }\n\n const [agentId, source, accountId, peerKind, peerId, ...rest] = parts;\n \n let threadId: string | undefined;\n let scopeId: string | undefined;\n \n // Parse optional thread and scope\n for (let i = 0; i < rest.length; i++) {\n if (rest[i] === 'thread' && rest[i + 1]) {\n threadId = rest[i + 1];\n i++;\n } else if (rest[i] === 'scope' && rest[i + 1]) {\n scopeId = rest[i + 1];\n i++;\n }\n }\n\n return {\n agentId: agentId?.toLowerCase() || 'main',\n source: source?.toLowerCase() || channel,\n accountId: accountId?.toLowerCase() || 'default',\n peerKind: peerKind?.toLowerCase() || 'dm',\n peerId: peerId?.toLowerCase() || 'unknown',\n threadId,\n scopeId,\n };\n }\n\n // ========== CRUD Operations ==========\n\n async list(query: SessionListQuery = {}): Promise<PaginatedResult<SessionMetadata>> {\n const index = await this.loadIndex();\n let sessions = [...(index.sessions || [])];\n\n // Apply filters\n if (query.status) {\n const statuses = Array.isArray(query.status) ? query.status : [query.status];\n sessions = sessions.filter((s) => statuses.includes(s.status));\n }\n\n if (query.channel) {\n const channels = query.channel\n .split(',')\n .map((c) => c.trim())\n .filter(Boolean);\n if (channels.length === 0) {\n sessions = [];\n } else if (channels.length === 1) {\n sessions = sessions.filter((s) => s.sourceChannel === channels[0]);\n } else {\n sessions = sessions.filter((s) => channels.includes(s.sourceChannel));\n }\n }\n\n if (query.tags && query.tags.length > 0) {\n sessions = sessions.filter((s) => query.tags!.some((tag) => s.tags.includes(tag)));\n }\n\n if (query.search) {\n const searchLower = query.search.toLowerCase();\n sessions = sessions.filter(\n (s) =>\n s.key.toLowerCase().includes(searchLower) ||\n s.name?.toLowerCase().includes(searchLower) ||\n s.tags.some((t) => t.toLowerCase().includes(searchLower))\n );\n }\n\n // Apply sorting\n const sortBy = query.sortBy || 'updatedAt';\n const sortOrder = query.sortOrder || 'desc';\n\n sessions.sort((a, b) => {\n const aVal = a[sortBy];\n const bVal = b[sortBy];\n const comparison = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;\n return sortOrder === 'asc' ? comparison : -comparison;\n });\n\n // Apply pagination\n const total = sessions.length;\n const limit = query.limit || DEFAULT_LIMIT;\n const offset = query.offset || 0;\n const items = sessions.slice(offset, offset + limit);\n\n return {\n items,\n total,\n limit,\n offset,\n hasMore: offset + limit < total,\n };\n }\n\n async get(\n key: string,\n options?: { includeTranscriptSummary?: boolean; includeTranscriptRows?: boolean },\n ): Promise<SessionDetail | null> {\n const metadata = await this.getMetadata(key);\n if (!metadata) return null;\n\n const messages = await this.loadMessages(key);\n\n let transcriptSummary: SessionTranscriptSummary | undefined;\n if (options?.includeTranscriptSummary) {\n const env = await this.loadTranscriptDocument(key);\n if (env) {\n transcriptSummary = {\n id: env.id,\n version: env.version,\n createdAt: env.createdAt,\n updatedAt: env.updatedAt,\n compactionCount: env.compactions?.length ?? 0,\n };\n }\n }\n\n let transcriptRows: TranscriptStoredRow[] | undefined;\n if (options?.includeTranscriptRows) {\n transcriptRows = await this.loadTranscriptRows(key);\n }\n\n return {\n ...metadata,\n messages: this.convertMessages(messages),\n ...(transcriptSummary ? { transcriptSummary } : {}),\n ...(transcriptRows !== undefined ? { transcriptRows } : {}),\n };\n }\n\n /** Full on-disk transcript rows (LLM messages + optional `kind: 'context'`). */\n async loadTranscriptRows(key: string): Promise<TranscriptStoredRow[]> {\n const { jsonPath } = this.sessionPathsForKey(key);\n try {\n const raw = await readFile(jsonPath, 'utf-8');\n return parseStoredTranscriptJson(raw).rows;\n } catch {\n return [];\n }\n }\n\n async getMetadata(key: string): Promise<SessionMetadata | null> {\n const index = await this.loadIndex();\n const metadata = index.sessions.find((s) => s.key === key);\n\n if (!metadata) {\n // Try to load from file directly (orphaned session)\n const scanned = await this.scanSessionFile(key);\n if (!scanned) {\n return null;\n }\n await this.runStoreMutation(async () => {\n this.invalidateIndexCache();\n const fresh = await this.loadIndex();\n if (fresh.sessions.some((s) => s.key === key)) {\n return;\n }\n fresh.sessions.push(scanned);\n this.indexDirty = true;\n await this.saveIndex();\n invalidateSessionSearchIndexCache();\n });\n return scanned;\n }\n\n return metadata;\n }\n\n async updateMetadata(key: string, updates: Partial<SessionMetadata>): Promise<void> {\n return this.runStoreMutation(async () => {\n this.invalidateIndexCache();\n const index = await this.loadIndex();\n const idx = index.sessions.findIndex((s) => s.key === key);\n\n if (idx === -1) {\n throw new Error(`Session not found: ${key}`);\n }\n\n index.sessions[idx] = {\n ...index.sessions[idx],\n ...updates,\n updatedAt: new Date().toISOString(),\n };\n\n this.indexDirty = true;\n await this.saveIndex();\n\n log.debug({ key, updates }, 'Session metadata updated');\n });\n }\n\n async delete(key: string): Promise<boolean> {\n return this.runStoreMutation(async () => {\n this.invalidateIndexCache();\n const index = await this.loadIndex();\n const idx = index.sessions.findIndex((s) => s.key === key);\n\n const primary = this.sessionPathsForKey(key);\n\n for (const p of [primary.jsonPath]) {\n try {\n await unlink(p);\n } catch (err: any) {\n if (err.code !== 'ENOENT') throw err;\n }\n }\n for (const p of [primary.metaPath]) {\n try {\n await unlink(p);\n } catch (err: any) {\n if (err.code !== 'ENOENT') throw err;\n }\n }\n\n // Remove from index\n if (idx !== -1) {\n index.sessions.splice(idx, 1);\n this.indexDirty = true;\n await this.saveIndex();\n }\n\n log.info({ key }, 'Session deleted');\n return true;\n });\n }\n\n async deleteMany(keys: string[]): Promise<{ success: string[]; failed: string[] }> {\n const success: string[] = [];\n const failed: string[] = [];\n\n for (const key of keys) {\n try {\n await this.delete(key);\n success.push(key);\n } catch {\n failed.push(key);\n }\n }\n\n return { success, failed };\n }\n\n // ========== Status Operations ==========\n\n async setStatus(key: string, status: SessionStatus): Promise<void> {\n await this.updateMetadata(key, { status });\n\n if (status === SessionStatus.ARCHIVED) {\n await this.moveToArchive(key);\n } else {\n await this.moveFromArchive(key);\n }\n }\n\n async archive(key: string): Promise<void> {\n await this.setStatus(key, SessionStatus.ARCHIVED);\n }\n\n async unarchive(key: string): Promise<void> {\n await this.setStatus(key, SessionStatus.ACTIVE);\n }\n\n async pin(key: string): Promise<void> {\n await this.setStatus(key, SessionStatus.PINNED);\n }\n\n async unpin(key: string): Promise<void> {\n await this.setStatus(key, SessionStatus.ACTIVE);\n }\n\n // ========== Message Operations ==========\n\n async loadMessages(key: string, options?: { fromArchive?: boolean }): Promise<AgentMessage[]> {\n const primary = this.sessionPathsForKey(key);\n\n const readAndNormalize = async (path: string): Promise<AgentMessage[] | null> => {\n try {\n const data = await readFile(path, 'utf-8');\n const { messages } = parseStoredTranscriptJson(data);\n return messages;\n } catch {\n return null;\n }\n };\n\n const messages = await readAndNormalize(primary.jsonPath);\n\n if (messages !== null) {\n return messages;\n }\n\n if (options?.fromArchive) {\n const archivedFile = await this.findMostRecentArchive(key);\n if (!archivedFile) {\n return [];\n }\n const archived = await readAndNormalize(archivedFile);\n return archived ?? [];\n }\n return [];\n }\n\n /**\n * Load the versioned transcript document (stable id, compaction history), or null if missing/invalid.\n */\n async loadTranscriptDocument(key: string): Promise<XopcSessionTranscriptV1 | null> {\n const { jsonPath } = this.sessionPathsForKey(key);\n try {\n const raw = await readFile(jsonPath, 'utf-8');\n return parseStoredTranscriptJson(raw).envelope;\n } catch {\n return null;\n }\n }\n\n /**\n * Find the most recent archived session file for a given key.\n * Archived files have format: {safeKey}.{timestamp}.json\n */\n private async findMostRecentArchive(sessionKey: string): Promise<string | null> {\n const safeKey = this.sanitizeKey(sessionKey);\n const shardDir = join(this.archiveDir, resolveSessionShardRelativePath(sessionKey));\n\n const scanDir = async (dir: string): Promise<string | null> => {\n try {\n const files = await readdir(dir);\n const matchingFiles = files\n .filter(\n (f) =>\n f.startsWith(`${safeKey}.`) &&\n f.endsWith('.json') &&\n !f.endsWith('.meta.json') &&\n !f.includes('.compaction-backup.'),\n )\n .sort()\n .reverse();\n if (matchingFiles.length === 0) return null;\n return join(dir, matchingFiles[0]);\n } catch {\n return null;\n }\n };\n\n const inShard = await scanDir(shardDir);\n if (inShard) return inShard;\n return await scanDir(this.archiveDir);\n }\n\n /**\n * Persist transcript JSON + merge session row into the index. Caller must hold {@link runStoreMutation} (or be nested under it).\n * Transcript is stored as a versioned document (pi-style header) with stable {@link XopcSessionTranscriptV1.id}.\n */\n private async writeSessionTranscriptFromStoredRows(\n key: string,\n storedRows: TranscriptStoredRow[],\n options?: { appendCompaction?: TranscriptCompactionRecord },\n ): Promise<void> {\n const { dir, jsonPath } = this.sessionPathsForKey(key);\n\n await mkdir(dir, { recursive: true });\n\n let previous: XopcSessionTranscriptV1 | null = null;\n try {\n const raw = await readFile(jsonPath, 'utf-8');\n previous = parseStoredTranscriptJson(raw).envelope;\n } catch {\n /* new session or unreadable */\n }\n\n const doc = buildTranscriptEnvelope({\n storedRows,\n previous,\n appendCompaction: options?.appendCompaction,\n });\n await writeTextAtomic(jsonPath, JSON.stringify(doc, null, 2));\n\n const llmMessages = buildSessionContextForLlm(storedRows);\n const index = await this.loadIndex();\n const existingIdx = index.sessions.findIndex((s) => s.key === key);\n const now = new Date().toISOString();\n\n const { channel, chatId } = this.parseSessionKey(key);\n const routing = this.extractRoutingFromKey(key, channel);\n const isCronSession = channel === 'cron';\n const isHeartbeatSession = channel === 'heartbeat';\n\n if (existingIdx !== -1) {\n const prev = index.sessions[existingIdx];\n index.sessions[existingIdx] = {\n ...prev,\n sourceChannel: channel,\n sourceChatId: chatId,\n messageCount: llmMessages.length,\n estimatedTokens: this.estimateTokens(llmMessages),\n updatedAt: now,\n lastAccessedAt: now,\n transcriptId: doc.id,\n sessionStartedAt: prev.sessionStartedAt ?? doc.createdAt,\n lastInteractionAt: now,\n routing: routing || prev.routing,\n ...(isCronSession\n ? {\n sessionType: 'cron',\n customData: {\n ...prev.customData,\n cronJobId: chatId,\n },\n }\n : {}),\n ...(isHeartbeatSession\n ? {\n sessionType: 'heartbeat',\n customData: {\n ...prev.customData,\n heartbeatTarget: chatId,\n },\n }\n : {}),\n stats: {\n ...prev.stats,\n messageCount: llmMessages.length,\n tokenCount: this.estimateTokens(llmMessages),\n lastTurnAt: Date.now(),\n },\n };\n } else {\n index.sessions.push({\n key,\n status: SessionStatus.ACTIVE,\n tags: [],\n createdAt: now,\n updatedAt: now,\n lastAccessedAt: now,\n transcriptId: doc.id,\n sessionStartedAt: doc.createdAt,\n lastInteractionAt: now,\n messageCount: llmMessages.length,\n estimatedTokens: this.estimateTokens(llmMessages),\n compactedCount: 0,\n sourceChannel: channel,\n sourceChatId: chatId,\n routing,\n ...(isCronSession\n ? {\n sessionType: 'cron',\n customData: { cronJobId: chatId },\n }\n : {}),\n ...(isHeartbeatSession\n ? {\n sessionType: 'heartbeat',\n customData: { heartbeatTarget: chatId },\n }\n : {}),\n stats: {\n messageCount: llmMessages.length,\n tokenCount: this.estimateTokens(llmMessages),\n lastTurnAt: Date.now(),\n },\n });\n }\n\n this.indexDirty = true;\n await this.saveIndex();\n\n invalidateSessionSearchIndexCache();\n }\n\n private async writeSessionTranscriptAndUpdateIndex(\n key: string,\n messages: AgentMessage[],\n options?: { appendCompaction?: TranscriptCompactionRecord },\n ): Promise<void> {\n const { jsonPath } = this.sessionPathsForKey(key);\n let storedRows: TranscriptStoredRow[] = messages;\n try {\n const raw = await readFile(jsonPath, 'utf-8');\n const parsed = parseStoredTranscriptJson(raw);\n if (parsed.rows.some((r) => isTranscriptContextEntry(r))) {\n storedRows = mergeLlmMessagesPreservingContextRows(parsed.rows, messages);\n }\n } catch {\n /* new session or unreadable */\n }\n await this.writeSessionTranscriptFromStoredRows(key, storedRows, options);\n }\n\n /**\n * Append a persisted-only transcript row (`kind: 'context'`), visible on disk and in session search\n * after stripping, but never returned from {@link loadMessages}.\n */\n async appendTranscriptContextEntry(\n key: string,\n entry: Omit<XopcTranscriptContextEntry, 'kind'> & Partial<Pick<XopcTranscriptContextEntry, 'kind'>>,\n ): Promise<void> {\n return this.runStoreMutation(async () => {\n this.invalidateIndexCache();\n const { jsonPath } = this.sessionPathsForKey(key);\n let rows: TranscriptStoredRow[] = [];\n try {\n const raw = await readFile(jsonPath, 'utf-8');\n rows = parseStoredTranscriptJson(raw).rows;\n } catch {\n /* new file */\n }\n const row: XopcTranscriptContextEntry = {\n kind: 'context',\n id: typeof entry.id === 'string' ? entry.id : undefined,\n text: typeof entry.text === 'string' ? entry.text : undefined,\n data: entry.data,\n createdAt: entry.createdAt ?? new Date().toISOString(),\n };\n await this.writeSessionTranscriptFromStoredRows(key, [...rows, row], {});\n });\n }\n\n async saveMessages(key: string, messages: AgentMessage[]): Promise<void> {\n return this.runStoreMutation(async () => {\n this.invalidateIndexCache();\n await this.writeSessionTranscriptAndUpdateIndex(key, messages);\n });\n }\n\n // ========== Sliding Window & Compaction ==========\n\n /**\n * Get window stats for messages\n */\n getWindowStats(messages: AgentMessage[]) {\n return this.window.getStats(messages);\n }\n\n /**\n * Check if session needs compaction\n */\n needsCompaction(key: string, messages: AgentMessage[], contextWindow: number) {\n return this.compactor.needsCompaction(messages, contextWindow);\n }\n\n /**\n * Prepare compaction (check if needed)\n */\n prepareCompaction(\n key: string,\n messages: AgentMessage[],\n contextWindow: number\n ): { needsCompaction: boolean; messages: AgentMessage[]; stats?: ReturnType<typeof this.compactor.needsCompaction> } {\n const result = this.compactor.needsCompaction(messages, contextWindow);\n return {\n needsCompaction: result.needed,\n messages,\n stats: result,\n };\n }\n\n /**\n * Apply compaction result to messages\n */\n async applyCompaction(\n key: string,\n messages: AgentMessage[],\n result: CompactionResult\n ): Promise<AgentMessage[]> {\n const compacted = this.compactor.applyCompaction(messages, result);\n\n return this.runStoreMutation(async () => {\n this.invalidateIndexCache();\n const { jsonPath } = this.sessionPathsForKey(key);\n await this.captureCompactionCheckpointIfExists(key, jsonPath);\n\n await this.writeSessionTranscriptAndUpdateIndex(key, compacted, {\n appendCompaction: {\n at: new Date().toISOString(),\n summary: result.summary,\n firstKeptIndex: result.firstKeptIndex,\n tokensBefore: result.tokensBefore,\n tokensAfter: result.tokensAfter,\n },\n });\n\n const metadata = await this.getMetadata(key);\n if (metadata) {\n await this.updateMetadata(key, {\n compactedCount: metadata.compactedCount + 1,\n });\n }\n\n log.info(\n {\n key,\n tokensBefore: result.tokensBefore,\n tokensAfter: result.tokensAfter,\n keptMessages: compacted.length,\n },\n 'Session compacted',\n );\n\n return compacted;\n });\n }\n\n /**\n * Compact session with LLM summary\n */\n async compact(\n key: string,\n messages: AgentMessage[],\n contextWindow: number,\n instructions?: string,\n force?: boolean,\n ): Promise<CompactionResult> {\n const result = await this.compactor.compact(messages, instructions, force);\n \n if (result.compacted) {\n await this.applyCompaction(key, messages, result);\n }\n \n return result;\n }\n\n /**\n * Get compaction stats for a session\n */\n async getCompactionStats(key: string) {\n const metadata = await this.getMetadata(key);\n if (!metadata) return undefined;\n \n return {\n compactionCount: metadata.compactedCount,\n totalTokensBefore: 0,\n totalTokensAfter: 0,\n lastCompactionAt: undefined,\n };\n }\n\n /**\n * List pre-compaction transcript snapshots for a session (newest first).\n */\n async listCompactionCheckpoints(key: string): Promise<CompactionCheckpointSummary[]> {\n const safeKey = this.sanitizeKey(key);\n const dir = join(this.sessionsDir, resolveSessionShardRelativePath(key));\n const prefix = this.checkpointBasenamePrefix(safeKey);\n let names: string[];\n try {\n names = await readdir(dir);\n } catch {\n return [];\n }\n const files = names.filter((n) => n.startsWith(prefix) && n.endsWith('.json'));\n const rows = await Promise.all(\n files.map(async (name) => {\n const p = join(dir, name);\n try {\n const s = await stat(p);\n const id = name.slice(prefix.length, -'.json'.length);\n if (!normalizeCompactionCheckpointId(id)) {\n return null;\n }\n return {\n id: normalizeCompactionCheckpointId(id)!,\n sizeBytes: s.size,\n modifiedAt: new Date(s.mtimeMs).toISOString(),\n } satisfies CompactionCheckpointSummary;\n } catch {\n return null;\n }\n }),\n );\n const valid = rows.filter((r): r is CompactionCheckpointSummary => r !== null);\n valid.sort((a, b) => b.modifiedAt.localeCompare(a.modifiedAt));\n return valid;\n }\n\n /**\n * Metadata for a single compaction checkpoint file.\n */\n async getCompactionCheckpointDetail(\n key: string,\n checkpointId: string,\n ): Promise<CompactionCheckpointDetail | null> {\n const id = normalizeCompactionCheckpointId(checkpointId);\n if (!id) {\n return null;\n }\n const safeKey = this.sanitizeKey(key);\n const dir = join(this.sessionsDir, resolveSessionShardRelativePath(key));\n const fname = `${this.checkpointBasenamePrefix(safeKey)}${id}.json`;\n const cpPath = join(dir, fname);\n if (!existsSync(cpPath)) {\n return null;\n }\n try {\n const raw = await readFile(cpPath, 'utf-8');\n const { messages } = parseStoredTranscriptJson(raw);\n const s = await stat(cpPath);\n return {\n id,\n sizeBytes: s.size,\n modifiedAt: new Date(s.mtimeMs).toISOString(),\n messageCount: messages.length,\n };\n } catch {\n return null;\n }\n }\n\n /**\n * Restore main transcript from a pre-compaction snapshot (then re-wrap + index sync).\n * Does not delete the checkpoint file.\n */\n async restoreCompactionCheckpoint(key: string, checkpointId: string): Promise<void> {\n const id = normalizeCompactionCheckpointId(checkpointId);\n if (!id) {\n throw new Error('Invalid checkpoint id');\n }\n return this.runStoreMutation(async () => {\n const safeKey = this.sanitizeKey(key);\n const dir = join(this.sessionsDir, resolveSessionShardRelativePath(key));\n const fname = `${this.checkpointBasenamePrefix(safeKey)}${id}.json`;\n const cpPath = join(dir, fname);\n if (!existsSync(cpPath)) {\n throw new Error(`Checkpoint not found: ${id}`);\n }\n const { jsonPath } = this.sessionPathsForKey(key);\n await copyFile(cpPath, jsonPath);\n const messages = await this.loadMessages(key);\n await this.writeSessionTranscriptAndUpdateIndex(key, messages);\n log.info({ key, checkpointId: id }, 'Session transcript restored from compaction checkpoint');\n });\n }\n\n // ========== MemoryStore API Aliases ==========\n\n /** Alias for delete */\n async deleteSession(key: string): Promise<boolean> {\n return this.delete(key);\n }\n\n /** Alias for loadMessages */\n async load(key: string, options?: { fromArchive?: boolean }): Promise<AgentMessage[]> {\n return this.loadMessages(key, options);\n }\n\n /** Alias for saveMessages */\n async save(key: string, messages: AgentMessage[]): Promise<void> {\n return this.saveMessages(key, messages);\n }\n\n /** Alias for estimateTokens */\n async estimateTokenUsage(key: string, messages: AgentMessage[]): Promise<number> {\n return this.estimateTokens(messages);\n }\n\n // ========== Search ==========\n\n async searchInSession(key: string, keyword: string): Promise<Message[]> {\n const messages = await this.loadMessages(key);\n const keywordLower = keyword.toLowerCase();\n\n return this.convertMessages(\n messages.filter((m) => {\n const content = this.extractTextContent(m.content);\n return content.toLowerCase().includes(keywordLower);\n })\n );\n }\n\n // ========== Export/Import ==========\n\n /**\n * JSON export includes API-shaped `messages` (LLM-only) plus `transcriptRows` (full on-disk order).\n */\n async exportSession(key: string, format: ExportFormat): Promise<string> {\n const detail = await this.get(key);\n if (!detail) {\n throw new Error(`Session not found: ${key}`);\n }\n\n if (format === 'json') {\n const transcriptRows = await this.loadTranscriptRows(key);\n const exportData: SessionExport = {\n version: INDEX_VERSION,\n exportedAt: new Date().toISOString(),\n metadata: detail,\n messages: detail.messages,\n transcriptRows,\n };\n return JSON.stringify(exportData, null, 2);\n } else {\n // Markdown format\n const lines = [\n `# ${detail.name || detail.key}`,\n '',\n `- **Channel:** ${detail.sourceChannel}`,\n `- **Created:** ${detail.createdAt}`,\n `- **Messages:** ${detail.messageCount}`,\n `- **Tags:** ${detail.tags.join(', ') || 'none'}`,\n '',\n '---',\n '',\n ];\n\n for (const msg of detail.messages) {\n const role = msg.role === 'assistant' ? 'Assistant' : msg.role === 'user' ? 'User' : msg.role;\n lines.push(`## ${role}`);\n lines.push('');\n const body =\n typeof msg.content === 'string'\n ? msg.content\n : JSON.stringify(msg.content, null, 2);\n lines.push(body);\n lines.push('');\n lines.push('---');\n lines.push('');\n }\n\n return lines.join('\\n');\n }\n }\n\n // ========== Statistics ==========\n\n async getStats(): Promise<GlobalSessionStats> {\n const index = await this.loadIndex();\n const sessions = index.sessions;\n\n const byChannel: Record<string, number> = {};\n for (const s of sessions) {\n byChannel[s.sourceChannel] = (byChannel[s.sourceChannel] || 0) + 1;\n }\n\n let oldestSession: string | undefined;\n let newestSession: string | undefined;\n\n if (sessions.length > 0) {\n const sorted = [...sessions].sort((a, b) => a.createdAt.localeCompare(b.createdAt));\n oldestSession = sorted[0].createdAt;\n newestSession = sorted[sorted.length - 1].createdAt;\n }\n\n return {\n totalSessions: sessions.length,\n activeSessions: sessions.filter((s) => s.status === SessionStatus.ACTIVE || s.status === SessionStatus.IDLE).length,\n archivedSessions: sessions.filter((s) => s.status === SessionStatus.ARCHIVED).length,\n pinnedSessions: sessions.filter((s) => s.status === SessionStatus.PINNED).length,\n totalMessages: sessions.reduce((sum, s) => sum + s.messageCount, 0),\n totalTokens: sessions.reduce((sum, s) => sum + s.estimatedTokens, 0),\n oldestSession,\n newestSession,\n byChannel,\n };\n }\n\n // ========== Cleanup ==========\n\n async archiveOld(olderThanDays: number): Promise<number> {\n const cutoff = new Date();\n cutoff.setDate(cutoff.getDate() - olderThanDays);\n\n const index = await this.loadIndex();\n let archived = 0;\n\n for (const session of index.sessions) {\n if (session.status !== SessionStatus.ARCHIVED && session.status !== SessionStatus.PINNED) {\n const lastAccess = new Date(session.lastAccessedAt);\n if (lastAccess < cutoff) {\n await this.archive(session.key);\n archived++;\n }\n }\n }\n\n return archived;\n }\n\n // ========== Helper Methods ==========\n\n private sanitizeKey(key: string): string {\n return key.replace(/[^a-zA-Z0-9_-]/g, '_');\n }\n\n private fileNameToKey(fileName: string): string {\n // Reverse of sanitizeKey - restore all colons from underscores\n // telegram_dm_123456 -> telegram:dm:123456\n // telegram_g_-100123456_t_789 -> telegram:g:-100123456:t:789\n return fileName.replace(/_/g, ':');\n }\n\n private parseSessionKey(key: string): { channel: string; chatId: string } {\n const parts = key.split(':');\n // Session key format: {agentId}:{source}:{accountId}:{peerKind}:{peerId}\n if (parts.length >= 5) {\n const parsed = parseRoutingSessionKey(key);\n if (parsed?.source === 'cron') {\n return { channel: 'cron', chatId: parsed.peerId };\n }\n return { channel: parts[1], chatId: parts.slice(2).join(':') };\n }\n // Gateway heartbeat: `heartbeat:main` / `heartbeat:isolated:<ts>`\n if (parts.length >= 2 && parts[0] === 'heartbeat') {\n return { channel: 'heartbeat', chatId: parts.slice(1).join(':') };\n }\n return { channel: 'unknown', chatId: key };\n }\n\n estimateTokens(messages: AgentMessage[]): number {\n // Rough estimate: 1 token ≈ 4 characters\n let total = 0;\n for (const msg of messages) {\n const text = this.extractTextContent(msg.content);\n total += Math.ceil(text.length / 4);\n }\n return total;\n }\n\n private extractTextContent(content: unknown): string {\n if (typeof content === 'string') return content;\n if (Array.isArray(content)) {\n const parts: string[] = [];\n for (const item of content) {\n if (typeof item !== 'object' || item === null || !('type' in item)) continue;\n const c = item as { type?: string; text?: string; name?: string };\n if (c.type === 'text' && typeof c.text === 'string') {\n parts.push(c.text);\n } else if (c.type === 'toolCall' || c.type === 'tool_use') {\n parts.push(c.name ? `[${c.name}]` : '');\n }\n }\n return parts.join('');\n }\n return '';\n }\n\n private convertMessages(messages: AgentMessage[]): Message[] {\n return messages.map((m: any) => {\n const c = m.content;\n const content: string | unknown[] =\n typeof c === 'string'\n ? c\n : Array.isArray(c)\n ? c\n : this.extractTextContent(c);\n\n const row: Message = {\n role: m.role as 'system' | 'user' | 'assistant' | 'tool' | 'toolResult',\n content,\n timestamp: m.timestamp ? new Date(m.timestamp).toISOString() : undefined,\n tool_call_id: m.tool_call_id || m.toolCallId,\n tool_calls: m.tool_calls,\n name: m.name,\n };\n if (Array.isArray(m.attachments) && m.attachments.length > 0) {\n row.attachments = m.attachments;\n }\n return row;\n });\n }\n\n private async moveToArchive(key: string): Promise<void> {\n return this.runStoreMutation(async () => {\n const safeKey = this.sanitizeKey(key);\n const primary = this.sessionPathsForKey(key);\n const sourcePath = existsSync(primary.jsonPath) ? primary.jsonPath : null;\n if (!sourcePath) {\n return;\n }\n\n const timestamp = new Date().toISOString().replace(/[:.]/g, '-');\n const archiveShard = join(this.archiveDir, resolveSessionShardRelativePath(key));\n await mkdir(archiveShard, { recursive: true });\n const targetPath = join(archiveShard, `${safeKey}.${timestamp}.json`);\n\n try {\n const data = await readFile(sourcePath, 'utf-8');\n await writeTextAtomic(targetPath, data);\n await unlink(sourcePath);\n\n const metaSource = primary.metaPath;\n const metaTarget = join(archiveShard, `${safeKey}.${timestamp}.meta.json`);\n try {\n const metaData = await readFile(metaSource, 'utf-8');\n await writeTextAtomic(metaTarget, metaData);\n await unlink(metaSource);\n } catch {\n // Meta file might not exist\n }\n } catch (err: any) {\n if (err.code !== 'ENOENT') throw err;\n }\n });\n }\n\n private async moveFromArchive(key: string): Promise<void> {\n return this.runStoreMutation(async () => {\n const sourcePath = await this.findMostRecentArchive(key);\n if (!sourcePath) {\n return;\n }\n\n const primary = this.sessionPathsForKey(key);\n await mkdir(primary.dir, { recursive: true });\n const targetPath = primary.jsonPath;\n\n try {\n const data = await readFile(sourcePath, 'utf-8');\n await writeTextAtomic(targetPath, data);\n await unlink(sourcePath);\n\n const metaSource = sourcePath.replace('.json', '.meta.json');\n const metaTarget = primary.metaPath;\n try {\n const metaData = await readFile(metaSource, 'utf-8');\n await writeTextAtomic(metaTarget, metaData);\n await unlink(metaSource);\n } catch {\n // Meta file might not exist\n }\n } catch (err: any) {\n if (err.code !== 'ENOENT') throw err;\n }\n });\n }\n\n}\n"],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ import type { SessionStore } from './store.js';
2
+ /**
3
+ * Remove the last row if it is a gateway early-saved webchat user message (dangling prompt after abort).
4
+ */
5
+ export declare function stripTrailingWebchatEarlySaveUserIfPresent(sessionStore: SessionStore, sessionKey: string): Promise<boolean>;
@@ -0,0 +1,17 @@
1
+ //#region src/session/strip-webchat-early-save.ts
2
+ /**
3
+ * Remove the last row if it is a gateway early-saved webchat user message (dangling prompt after abort).
4
+ */
5
+ async function stripTrailingWebchatEarlySaveUserIfPresent(sessionStore, sessionKey) {
6
+ const msgs = await sessionStore.load(sessionKey);
7
+ const last = msgs[msgs.length - 1];
8
+ if (last?.role === "user" && last.webchatEarlySave === true) {
9
+ await sessionStore.save(sessionKey, msgs.slice(0, -1));
10
+ return true;
11
+ }
12
+ return false;
13
+ }
14
+ //#endregion
15
+ export { stripTrailingWebchatEarlySaveUserIfPresent };
16
+
17
+ //# sourceMappingURL=strip-webchat-early-save.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"strip-webchat-early-save.js","names":[],"sources":["../../../src/session/strip-webchat-early-save.ts"],"sourcesContent":["import type { AgentMessage } from '@mariozechner/pi-agent-core';\n\nimport type { SessionStore } from './store.js';\n\n/**\n * Remove the last row if it is a gateway early-saved webchat user message (dangling prompt after abort).\n */\nexport async function stripTrailingWebchatEarlySaveUserIfPresent(\n sessionStore: SessionStore,\n sessionKey: string,\n): Promise<boolean> {\n const msgs = await sessionStore.load(sessionKey);\n const last = msgs[msgs.length - 1] as\n | (AgentMessage & { webchatEarlySave?: boolean })\n | undefined;\n if (last?.role === 'user' && last.webchatEarlySave === true) {\n await sessionStore.save(sessionKey, msgs.slice(0, -1));\n return true;\n }\n return false;\n}\n"],"mappings":";;;;AAOA,eAAsB,2CACpB,cACA,YACkB;CAClB,MAAM,OAAO,MAAM,aAAa,KAAK,WAAW;CAChD,MAAM,OAAO,KAAK,KAAK,SAAS;AAGhC,KAAI,MAAM,SAAS,UAAU,KAAK,qBAAqB,MAAM;AAC3D,QAAM,aAAa,KAAK,YAAY,KAAK,MAAM,GAAG,GAAG,CAAC;AACtD,SAAO;;AAET,QAAO"}
@@ -0,0 +1,46 @@
1
+ /**
2
+ * On-disk session transcript shape inspired by pi-mono coding-agent session files:
3
+ * versioned document + stable id + optional compaction audit trail (vs only mutating messages).
4
+ */
5
+ import type { AgentMessage } from '@mariozechner/pi-agent-core';
6
+ import { type TranscriptStoredRow } from './session-context-for-llm.js';
7
+ export declare const XOPC_SESSION_TRANSCRIPT_TYPE: "xopc_session_transcript";
8
+ /** Bump when the envelope schema changes (load path must tolerate older versions). */
9
+ export declare const CURRENT_SESSION_TRANSCRIPT_VERSION = 1;
10
+ /** Record appended when context compaction runs (pi `CompactionEntry`-style audit). */
11
+ export interface TranscriptCompactionRecord {
12
+ at: string;
13
+ summary: string;
14
+ firstKeptIndex: number;
15
+ tokensBefore: number;
16
+ tokensAfter: number;
17
+ }
18
+ export interface XopcSessionTranscriptV1 {
19
+ type: typeof XOPC_SESSION_TRANSCRIPT_TYPE;
20
+ version: number;
21
+ /** Stable id for this transcript file (survives rewrites; like pi session header id). */
22
+ id: string;
23
+ createdAt: string;
24
+ updatedAt: string;
25
+ /**
26
+ * Transcript rows: normal {@link AgentMessage} plus optional `kind: 'context'` entries
27
+ * (see {@link buildSessionContextForLlm}).
28
+ */
29
+ messages: TranscriptStoredRow[];
30
+ compactions?: TranscriptCompactionRecord[];
31
+ }
32
+ /**
33
+ * Parse stored transcript JSON: only wrapped {@link XopcSessionTranscriptV1} documents are accepted.
34
+ * `messages` is {@link buildSessionContextForLlm} of `rows` (LLM-only).
35
+ */
36
+ export declare function parseStoredTranscriptJson(raw: string): {
37
+ rows: TranscriptStoredRow[];
38
+ messages: AgentMessage[];
39
+ envelope: XopcSessionTranscriptV1 | null;
40
+ };
41
+ export declare function buildTranscriptEnvelope(params: {
42
+ /** Full on-disk rows (LLM messages + optional `kind: 'context'`). */
43
+ storedRows: TranscriptStoredRow[];
44
+ previous: XopcSessionTranscriptV1 | null;
45
+ appendCompaction?: TranscriptCompactionRecord;
46
+ }): XopcSessionTranscriptV1;
@@ -0,0 +1,88 @@
1
+ import { buildSessionContextForLlm, transcriptRowsFromJsonArray } from "./session-context-for-llm.js";
2
+ import { randomUUID } from "node:crypto";
3
+ //#region src/session/transcript-format.ts
4
+ /**
5
+ * On-disk session transcript shape inspired by pi-mono coding-agent session files:
6
+ * versioned document + stable id + optional compaction audit trail (vs only mutating messages).
7
+ */
8
+ const XOPC_SESSION_TRANSCRIPT_TYPE = "xopc_session_transcript";
9
+ /** Bump when the envelope schema changes (load path must tolerate older versions). */
10
+ const CURRENT_SESSION_TRANSCRIPT_VERSION = 1;
11
+ function normalizeTranscriptEnvelope(o) {
12
+ if (o.type !== "xopc_session_transcript" || typeof o.version !== "number" || typeof o.id !== "string" || typeof o.updatedAt !== "string" || !Array.isArray(o.messages)) return null;
13
+ const updatedAt = o.updatedAt;
14
+ const createdAt = typeof o.createdAt === "string" ? o.createdAt : updatedAt;
15
+ const rawCompactions = Array.isArray(o.compactions) ? o.compactions : [];
16
+ if (rawCompactions.length > 0 && rawCompactions.some((x) => !isCompactionRecord(x))) return null;
17
+ const compactions = rawCompactions;
18
+ const rawMsgs = o.messages;
19
+ const rows = transcriptRowsFromJsonArray(rawMsgs);
20
+ if (rows.length !== rawMsgs.length) return null;
21
+ return {
22
+ type: XOPC_SESSION_TRANSCRIPT_TYPE,
23
+ version: o.version,
24
+ id: o.id,
25
+ createdAt,
26
+ updatedAt,
27
+ messages: rows,
28
+ ...compactions.length > 0 ? { compactions } : {}
29
+ };
30
+ }
31
+ function isCompactionRecord(x) {
32
+ if (!x || typeof x !== "object") return false;
33
+ const r = x;
34
+ return typeof r.at === "string" && typeof r.summary === "string" && typeof r.firstKeptIndex === "number" && typeof r.tokensBefore === "number" && typeof r.tokensAfter === "number";
35
+ }
36
+ /**
37
+ * Parse stored transcript JSON: only wrapped {@link XopcSessionTranscriptV1} documents are accepted.
38
+ * `messages` is {@link buildSessionContextForLlm} of `rows` (LLM-only).
39
+ */
40
+ function parseStoredTranscriptJson(raw) {
41
+ let parsed;
42
+ try {
43
+ parsed = JSON.parse(raw);
44
+ } catch {
45
+ return {
46
+ rows: [],
47
+ messages: [],
48
+ envelope: null
49
+ };
50
+ }
51
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
52
+ const envelope = normalizeTranscriptEnvelope(parsed);
53
+ if (envelope) {
54
+ const messages = buildSessionContextForLlm(envelope.messages);
55
+ return {
56
+ rows: envelope.messages,
57
+ messages,
58
+ envelope
59
+ };
60
+ }
61
+ }
62
+ return {
63
+ rows: [],
64
+ messages: [],
65
+ envelope: null
66
+ };
67
+ }
68
+ function buildTranscriptEnvelope(params) {
69
+ const now = (/* @__PURE__ */ new Date()).toISOString();
70
+ const id = params.previous?.id ?? randomUUID();
71
+ const createdAt = params.previous?.createdAt ?? now;
72
+ const compactions = [...params.previous?.compactions ?? []];
73
+ if (params.appendCompaction) compactions.push(params.appendCompaction);
74
+ const doc = {
75
+ type: XOPC_SESSION_TRANSCRIPT_TYPE,
76
+ version: 1,
77
+ id,
78
+ createdAt,
79
+ updatedAt: now,
80
+ messages: params.storedRows
81
+ };
82
+ if (compactions.length > 0) doc.compactions = compactions;
83
+ return doc;
84
+ }
85
+ //#endregion
86
+ export { CURRENT_SESSION_TRANSCRIPT_VERSION, XOPC_SESSION_TRANSCRIPT_TYPE, buildTranscriptEnvelope, parseStoredTranscriptJson };
87
+
88
+ //# sourceMappingURL=transcript-format.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transcript-format.js","names":[],"sources":["../../../src/session/transcript-format.ts"],"sourcesContent":["/**\n * On-disk session transcript shape inspired by pi-mono coding-agent session files:\n * versioned document + stable id + optional compaction audit trail (vs only mutating messages).\n */\n\nimport { randomUUID } from 'node:crypto';\n\nimport type { AgentMessage } from '@mariozechner/pi-agent-core';\n\nimport {\n buildSessionContextForLlm,\n transcriptRowsFromJsonArray,\n type TranscriptStoredRow,\n} from './session-context-for-llm.js';\n\nexport const XOPC_SESSION_TRANSCRIPT_TYPE = 'xopc_session_transcript' as const;\n\n/** Bump when the envelope schema changes (load path must tolerate older versions). */\nexport const CURRENT_SESSION_TRANSCRIPT_VERSION = 1;\n\n/** Record appended when context compaction runs (pi `CompactionEntry`-style audit). */\nexport interface TranscriptCompactionRecord {\n at: string;\n summary: string;\n firstKeptIndex: number;\n tokensBefore: number;\n tokensAfter: number;\n}\n\nexport interface XopcSessionTranscriptV1 {\n type: typeof XOPC_SESSION_TRANSCRIPT_TYPE;\n version: number;\n /** Stable id for this transcript file (survives rewrites; like pi session header id). */\n id: string;\n createdAt: string;\n updatedAt: string;\n /**\n * Transcript rows: normal {@link AgentMessage} plus optional `kind: 'context'` entries\n * (see {@link buildSessionContextForLlm}).\n */\n messages: TranscriptStoredRow[];\n compactions?: TranscriptCompactionRecord[];\n}\n\nfunction normalizeTranscriptEnvelope(o: Record<string, unknown>): XopcSessionTranscriptV1 | null {\n if (\n o.type !== XOPC_SESSION_TRANSCRIPT_TYPE ||\n typeof o.version !== 'number' ||\n typeof o.id !== 'string' ||\n typeof o.updatedAt !== 'string' ||\n !Array.isArray(o.messages)\n ) {\n return null;\n }\n const updatedAt = o.updatedAt as string;\n const createdAt = typeof o.createdAt === 'string' ? o.createdAt : updatedAt;\n const rawCompactions = Array.isArray(o.compactions) ? o.compactions : [];\n if (rawCompactions.length > 0 && rawCompactions.some((x) => !isCompactionRecord(x))) {\n return null;\n }\n const compactions = rawCompactions as TranscriptCompactionRecord[];\n\n const rawMsgs = o.messages as unknown[];\n const rows = transcriptRowsFromJsonArray(rawMsgs);\n if (rows.length !== rawMsgs.length) {\n return null;\n }\n\n return {\n type: XOPC_SESSION_TRANSCRIPT_TYPE,\n version: o.version as number,\n id: o.id as string,\n createdAt,\n updatedAt,\n messages: rows,\n ...(compactions.length > 0 ? { compactions } : {}),\n };\n}\n\nfunction isCompactionRecord(x: unknown): x is TranscriptCompactionRecord {\n if (!x || typeof x !== 'object') return false;\n const r = x as Record<string, unknown>;\n return (\n typeof r.at === 'string' &&\n typeof r.summary === 'string' &&\n typeof r.firstKeptIndex === 'number' &&\n typeof r.tokensBefore === 'number' &&\n typeof r.tokensAfter === 'number'\n );\n}\n\n/**\n * Parse stored transcript JSON: only wrapped {@link XopcSessionTranscriptV1} documents are accepted.\n * `messages` is {@link buildSessionContextForLlm} of `rows` (LLM-only).\n */\nexport function parseStoredTranscriptJson(raw: string): {\n rows: TranscriptStoredRow[];\n messages: AgentMessage[];\n envelope: XopcSessionTranscriptV1 | null;\n} {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return { rows: [], messages: [], envelope: null };\n }\n\n if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {\n const envelope = normalizeTranscriptEnvelope(parsed as Record<string, unknown>);\n if (envelope) {\n const messages = buildSessionContextForLlm(envelope.messages);\n return { rows: envelope.messages, messages, envelope };\n }\n }\n\n return { rows: [], messages: [], envelope: null };\n}\n\nexport function buildTranscriptEnvelope(params: {\n /** Full on-disk rows (LLM messages + optional `kind: 'context'`). */\n storedRows: TranscriptStoredRow[];\n previous: XopcSessionTranscriptV1 | null;\n appendCompaction?: TranscriptCompactionRecord;\n}): XopcSessionTranscriptV1 {\n const now = new Date().toISOString();\n const id = params.previous?.id ?? randomUUID();\n const createdAt = params.previous?.createdAt ?? now;\n const compactions = [...(params.previous?.compactions ?? [])];\n if (params.appendCompaction) {\n compactions.push(params.appendCompaction);\n }\n\n const doc: XopcSessionTranscriptV1 = {\n type: XOPC_SESSION_TRANSCRIPT_TYPE,\n version: CURRENT_SESSION_TRANSCRIPT_VERSION,\n id,\n createdAt,\n updatedAt: now,\n messages: params.storedRows,\n };\n if (compactions.length > 0) {\n doc.compactions = compactions;\n }\n return doc;\n}\n"],"mappings":";;;;;;;AAeA,MAAa,+BAA+B;;AAG5C,MAAa,qCAAqC;AA0BlD,SAAS,4BAA4B,GAA4D;AAC/F,KACE,EAAE,SAAA,6BACF,OAAO,EAAE,YAAY,YACrB,OAAO,EAAE,OAAO,YAChB,OAAO,EAAE,cAAc,YACvB,CAAC,MAAM,QAAQ,EAAE,SAAS,CAE1B,QAAO;CAET,MAAM,YAAY,EAAE;CACpB,MAAM,YAAY,OAAO,EAAE,cAAc,WAAW,EAAE,YAAY;CAClE,MAAM,iBAAiB,MAAM,QAAQ,EAAE,YAAY,GAAG,EAAE,cAAc,EAAE;AACxE,KAAI,eAAe,SAAS,KAAK,eAAe,MAAM,MAAM,CAAC,mBAAmB,EAAE,CAAC,CACjF,QAAO;CAET,MAAM,cAAc;CAEpB,MAAM,UAAU,EAAE;CAClB,MAAM,OAAO,4BAA4B,QAAQ;AACjD,KAAI,KAAK,WAAW,QAAQ,OAC1B,QAAO;AAGT,QAAO;EACL,MAAM;EACN,SAAS,EAAE;EACX,IAAI,EAAE;EACN;EACA;EACA,UAAU;EACV,GAAI,YAAY,SAAS,IAAI,EAAE,aAAa,GAAG,EAAE;EAClD;;AAGH,SAAS,mBAAmB,GAA6C;AACvE,KAAI,CAAC,KAAK,OAAO,MAAM,SAAU,QAAO;CACxC,MAAM,IAAI;AACV,QACE,OAAO,EAAE,OAAO,YAChB,OAAO,EAAE,YAAY,YACrB,OAAO,EAAE,mBAAmB,YAC5B,OAAO,EAAE,iBAAiB,YAC1B,OAAO,EAAE,gBAAgB;;;;;;AAQ7B,SAAgB,0BAA0B,KAIxC;CACA,IAAI;AACJ,KAAI;AACF,WAAS,KAAK,MAAM,IAAI;SAClB;AACN,SAAO;GAAE,MAAM,EAAE;GAAE,UAAU,EAAE;GAAE,UAAU;GAAM;;AAGnD,KAAI,UAAU,OAAO,WAAW,YAAY,CAAC,MAAM,QAAQ,OAAO,EAAE;EAClE,MAAM,WAAW,4BAA4B,OAAkC;AAC/E,MAAI,UAAU;GACZ,MAAM,WAAW,0BAA0B,SAAS,SAAS;AAC7D,UAAO;IAAE,MAAM,SAAS;IAAU;IAAU;IAAU;;;AAI1D,QAAO;EAAE,MAAM,EAAE;EAAE,UAAU,EAAE;EAAE,UAAU;EAAM;;AAGnD,SAAgB,wBAAwB,QAKZ;CAC1B,MAAM,uBAAM,IAAI,MAAM,EAAC,aAAa;CACpC,MAAM,KAAK,OAAO,UAAU,MAAM,YAAY;CAC9C,MAAM,YAAY,OAAO,UAAU,aAAa;CAChD,MAAM,cAAc,CAAC,GAAI,OAAO,UAAU,eAAe,EAAE,CAAE;AAC7D,KAAI,OAAO,iBACT,aAAY,KAAK,OAAO,iBAAiB;CAG3C,MAAM,MAA+B;EACnC,MAAM;EACN,SAAA;EACA;EACA;EACA,WAAW;EACX,UAAU,OAAO;EAClB;AACD,KAAI,YAAY,SAAS,EACvB,KAAI,cAAc;AAEpB,QAAO"}
@@ -1,3 +1,4 @@
1
+ import type { TranscriptStoredRow } from './session-context-for-llm.js';
1
2
  export interface Message {
2
3
  role: 'system' | 'user' | 'assistant' | 'tool' | 'toolResult';
3
4
  /** Plain string or structured content blocks (tool calls, multimodal). */
@@ -87,10 +88,35 @@ export interface SessionMetadata {
87
88
  * Distinct from `sourceChannel` (routing namespace).
88
89
  */
89
90
  sessionType?: string;
91
+ /**
92
+ * Stable transcript document id (wrapped on-disk format), aligned with OpenClaw `sessionId`.
93
+ */
94
+ transcriptId?: string;
95
+ /** First activity time for this session row (ISO), from transcript header when available. */
96
+ sessionStartedAt?: string;
97
+ /** Last transcript write / interaction (ISO), updated on each persist. */
98
+ lastInteractionAt?: string;
99
+ /**
100
+ * Epoch ms when the last webchat run was aborted (`POST /api/agent/abort`).
101
+ * Used with `clientCreatedAtMs` on the next POST /api/agent to drop stale queued sends.
102
+ */
103
+ abortCutoffTimestamp?: number;
104
+ }
105
+ /** Summary of wrapped transcript (no duplicate message bodies). */
106
+ export interface SessionTranscriptSummary {
107
+ id: string;
108
+ version: number;
109
+ createdAt: string;
110
+ updatedAt: string;
111
+ compactionCount: number;
90
112
  }
91
113
  /** Session detail (metadata + messages) */
92
114
  export interface SessionDetail extends SessionMetadata {
93
115
  messages: Message[];
116
+ /** Present when loaded with `includeTranscriptSummary` (gateway `?include=transcript`). */
117
+ transcriptSummary?: SessionTranscriptSummary;
118
+ /** Present when loaded with `include=transcriptRows` (full on-disk rows, LLM + `kind: 'context'`). */
119
+ transcriptRows?: TranscriptStoredRow[];
94
120
  }
95
121
  /** Session index file structure */
96
122
  export interface SessionIndex {
@@ -126,4 +152,15 @@ export interface SessionExport {
126
152
  exportedAt: string;
127
153
  metadata: SessionMetadata;
128
154
  messages: Message[];
155
+ /** Full on-disk rows (LLM + `kind: 'context'`) for audit / round-trip. */
156
+ transcriptRows: TranscriptStoredRow[];
157
+ }
158
+ /** On-disk pre-compaction snapshot (OpenClaw-style checkpoint list). */
159
+ export interface CompactionCheckpointSummary {
160
+ id: string;
161
+ sizeBytes: number;
162
+ modifiedAt: string;
163
+ }
164
+ export interface CompactionCheckpointDetail extends CompactionCheckpointSummary {
165
+ messageCount: number;
129
166
  }
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","names":[],"sources":["../../../src/session/types.ts"],"sourcesContent":["// Session management types\n\nexport interface Message {\n role: 'system' | 'user' | 'assistant' | 'tool' | 'toolResult';\n /** Plain string or structured content blocks (tool calls, multimodal). */\n content: string | unknown[];\n timestamp?: string;\n tool_call_id?: string;\n tool_calls?: Array<{\n id: string;\n type: 'function';\n function: {\n name: string;\n arguments: string;\n };\n }>;\n tool_call_id$?: string;\n toolName?: string;\n isError?: boolean;\n name?: string;\n /** Webchat early-save / UI: inbound file metadata (no base64). Omitted when absent. */\n attachments?: Array<{\n type?: string;\n mimeType?: string;\n name?: string;\n size?: number;\n workspaceRelativePath?: string;\n }>;\n}\n\n/** Session status enum */\nexport enum SessionStatus {\n ACTIVE = 'active',\n IDLE = 'idle',\n ARCHIVED = 'archived',\n PINNED = 'pinned',\n}\n\n/** Session routing metadata */\nexport interface SessionRoutingMeta {\n agentId: string;\n source: string;\n accountId: string;\n peerKind: string;\n peerId: string;\n threadId?: string;\n scopeId?: string;\n mainSessionKey?: string;\n lastRoutePolicy?: 'main' | 'session';\n}\n\n/** Session-level statistics (per session) */\nexport interface SessionStats {\n messageCount: number;\n tokenCount: number;\n turnCount?: number;\n lastTurnAt?: number;\n}\n\n/** Global session statistics (aggregate) */\nexport interface GlobalSessionStats {\n totalSessions: number;\n activeSessions: number;\n archivedSessions: number;\n pinnedSessions: number;\n totalMessages: number;\n totalTokens: number;\n oldestSession?: string;\n newestSession?: string;\n byChannel: Record<string, number>;\n}\n\n/** Session metadata (stored in index) */\nexport interface SessionMetadata {\n key: string;\n name?: string;\n status: SessionStatus;\n tags: string[];\n createdAt: string;\n updatedAt: string;\n lastAccessedAt: string;\n messageCount: number;\n estimatedTokens: number;\n compactedCount: number;\n sourceChannel: string;\n sourceChatId: string;\n customData?: Record<string, unknown>;\n /** Routing metadata */\n routing?: SessionRoutingMeta;\n /** Session statistics */\n stats?: SessionStats;\n /**\n * High-level origin for filtering/UI (e.g. `cron`, `heartbeat`, `webchat`).\n * Distinct from `sourceChannel` (routing namespace).\n */\n sessionType?: string;\n}\n\n/** Session detail (metadata + messages) */\nexport interface SessionDetail extends SessionMetadata {\n messages: Message[];\n}\n\n/** Session index file structure */\nexport interface SessionIndex {\n version: string;\n lastUpdated: string;\n sessions: SessionMetadata[];\n}\n\n/** Session list query parameters */\nexport interface SessionListQuery {\n status?: SessionStatus | SessionStatus[];\n /** Single `sourceChannel`, or comma-separated list (e.g. `telegram,weixin`). */\n channel?: string;\n tags?: string[];\n search?: string;\n sortBy?: 'updatedAt' | 'createdAt' | 'messageCount' | 'lastAccessedAt';\n sortOrder?: 'asc' | 'desc';\n limit?: number;\n offset?: number;\n}\n\n/** Paginated result */\nexport interface PaginatedResult<T> {\n items: T[];\n total: number;\n limit: number;\n offset: number;\n hasMore: boolean;\n}\n\n/** Export format */\nexport type ExportFormat = 'json' | 'markdown';\n\n/** Session export data */\nexport interface SessionExport {\n version: string;\n exportedAt: string;\n metadata: SessionMetadata;\n messages: Message[];\n}\n"],"mappings":";;AA+BA,IAAY,gBAAL,yBAAA,eAAA;AACL,eAAA,YAAA;AACA,eAAA,UAAA;AACA,eAAA,cAAA;AACA,eAAA,YAAA;;KACD"}
1
+ {"version":3,"file":"types.js","names":[],"sources":["../../../src/session/types.ts"],"sourcesContent":["// Session management types\n\nimport type { TranscriptStoredRow } from './session-context-for-llm.js';\n\nexport interface Message {\n role: 'system' | 'user' | 'assistant' | 'tool' | 'toolResult';\n /** Plain string or structured content blocks (tool calls, multimodal). */\n content: string | unknown[];\n timestamp?: string;\n tool_call_id?: string;\n tool_calls?: Array<{\n id: string;\n type: 'function';\n function: {\n name: string;\n arguments: string;\n };\n }>;\n tool_call_id$?: string;\n toolName?: string;\n isError?: boolean;\n name?: string;\n /** Webchat early-save / UI: inbound file metadata (no base64). Omitted when absent. */\n attachments?: Array<{\n type?: string;\n mimeType?: string;\n name?: string;\n size?: number;\n workspaceRelativePath?: string;\n }>;\n}\n\n/** Session status enum */\nexport enum SessionStatus {\n ACTIVE = 'active',\n IDLE = 'idle',\n ARCHIVED = 'archived',\n PINNED = 'pinned',\n}\n\n/** Session routing metadata */\nexport interface SessionRoutingMeta {\n agentId: string;\n source: string;\n accountId: string;\n peerKind: string;\n peerId: string;\n threadId?: string;\n scopeId?: string;\n mainSessionKey?: string;\n lastRoutePolicy?: 'main' | 'session';\n}\n\n/** Session-level statistics (per session) */\nexport interface SessionStats {\n messageCount: number;\n tokenCount: number;\n turnCount?: number;\n lastTurnAt?: number;\n}\n\n/** Global session statistics (aggregate) */\nexport interface GlobalSessionStats {\n totalSessions: number;\n activeSessions: number;\n archivedSessions: number;\n pinnedSessions: number;\n totalMessages: number;\n totalTokens: number;\n oldestSession?: string;\n newestSession?: string;\n byChannel: Record<string, number>;\n}\n\n/** Session metadata (stored in index) */\nexport interface SessionMetadata {\n key: string;\n name?: string;\n status: SessionStatus;\n tags: string[];\n createdAt: string;\n updatedAt: string;\n lastAccessedAt: string;\n messageCount: number;\n estimatedTokens: number;\n compactedCount: number;\n sourceChannel: string;\n sourceChatId: string;\n customData?: Record<string, unknown>;\n /** Routing metadata */\n routing?: SessionRoutingMeta;\n /** Session statistics */\n stats?: SessionStats;\n /**\n * High-level origin for filtering/UI (e.g. `cron`, `heartbeat`, `webchat`).\n * Distinct from `sourceChannel` (routing namespace).\n */\n sessionType?: string;\n /**\n * Stable transcript document id (wrapped on-disk format), aligned with OpenClaw `sessionId`.\n */\n transcriptId?: string;\n /** First activity time for this session row (ISO), from transcript header when available. */\n sessionStartedAt?: string;\n /** Last transcript write / interaction (ISO), updated on each persist. */\n lastInteractionAt?: string;\n /**\n * Epoch ms when the last webchat run was aborted (`POST /api/agent/abort`).\n * Used with `clientCreatedAtMs` on the next POST /api/agent to drop stale queued sends.\n */\n abortCutoffTimestamp?: number;\n}\n\n/** Summary of wrapped transcript (no duplicate message bodies). */\nexport interface SessionTranscriptSummary {\n id: string;\n version: number;\n createdAt: string;\n updatedAt: string;\n compactionCount: number;\n}\n\n/** Session detail (metadata + messages) */\nexport interface SessionDetail extends SessionMetadata {\n messages: Message[];\n /** Present when loaded with `includeTranscriptSummary` (gateway `?include=transcript`). */\n transcriptSummary?: SessionTranscriptSummary;\n /** Present when loaded with `include=transcriptRows` (full on-disk rows, LLM + `kind: 'context'`). */\n transcriptRows?: TranscriptStoredRow[];\n}\n\n/** Session index file structure */\nexport interface SessionIndex {\n version: string;\n lastUpdated: string;\n sessions: SessionMetadata[];\n}\n\n/** Session list query parameters */\nexport interface SessionListQuery {\n status?: SessionStatus | SessionStatus[];\n /** Single `sourceChannel`, or comma-separated list (e.g. `telegram,weixin`). */\n channel?: string;\n tags?: string[];\n search?: string;\n sortBy?: 'updatedAt' | 'createdAt' | 'messageCount' | 'lastAccessedAt';\n sortOrder?: 'asc' | 'desc';\n limit?: number;\n offset?: number;\n}\n\n/** Paginated result */\nexport interface PaginatedResult<T> {\n items: T[];\n total: number;\n limit: number;\n offset: number;\n hasMore: boolean;\n}\n\n/** Export format */\nexport type ExportFormat = 'json' | 'markdown';\n\n/** Session export data */\nexport interface SessionExport {\n version: string;\n exportedAt: string;\n metadata: SessionMetadata;\n messages: Message[];\n /** Full on-disk rows (LLM + `kind: 'context'`) for audit / round-trip. */\n transcriptRows: TranscriptStoredRow[];\n}\n\n/** On-disk pre-compaction snapshot (OpenClaw-style checkpoint list). */\nexport interface CompactionCheckpointSummary {\n id: string;\n sizeBytes: number;\n modifiedAt: string;\n}\n\nexport interface CompactionCheckpointDetail extends CompactionCheckpointSummary {\n messageCount: number;\n}\n"],"mappings":";;AAiCA,IAAY,gBAAL,yBAAA,eAAA;AACL,eAAA,YAAA;AACA,eAAA,UAAA;AACA,eAAA,cAAA;AACA,eAAA,YAAA;;KACD"}
@@ -181,6 +181,8 @@ function matchesQuery(entry, query) {
181
181
  if (query.sessionId && entry.sessionId !== query.sessionId) return false;
182
182
  return true;
183
183
  }
184
+ /** Cap matched rows before sort/slice so query stays bounded on huge log dirs. */
185
+ const QUERY_LOGS_MAX_MATCHED = 25e3;
184
186
  /**
185
187
  * Query logs across multiple files
186
188
  */
@@ -190,12 +192,11 @@ async function queryLogs(query = {}) {
190
192
  let relevantFiles = getLogFiles();
191
193
  if (query.from || query.to) relevantFiles = getLogFilesForRange(query.from ? new Date(query.from) : /* @__PURE__ */ new Date(0), query.to ? new Date(query.to) : /* @__PURE__ */ new Date());
192
194
  for (const file of relevantFiles) {
195
+ if (results.length >= QUERY_LOGS_MAX_MATCHED) break;
193
196
  for await (const entry of streamLogFile(file.path, query)) {
194
197
  results.push(entry);
195
- const limit = query.limit || 100;
196
- if (results.length >= limit) break;
198
+ if (results.length >= QUERY_LOGS_MAX_MATCHED) break;
197
199
  }
198
- if (query.limit && results.length >= query.limit) break;
199
200
  }
200
201
  const order = query.order || "desc";
201
202
  results.sort((a, b) => {