@xopcai/xopc 0.0.92 → 0.0.94

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 (194) hide show
  1. package/dist/browser-ext/manifest.json +1 -1
  2. package/dist/extensions/telegram/xopc.extension.json +1 -1
  3. package/dist/gateway/static/root/assets/agents-OqhbJkMf.js +222 -0
  4. package/dist/gateway/static/root/assets/apps-page-OHXW9XP8.js +1 -0
  5. package/dist/gateway/static/root/assets/channels-settings-4N2R-jof.js +1 -0
  6. package/dist/gateway/static/root/assets/{channels-status-swr-XzddfJW2.js → channels-status-swr-Bv6f9kDq.js} +1 -1
  7. package/dist/gateway/static/root/assets/{cron-api--I8LJ44S.js → cron-api-BtaQaHJq.js} +1 -1
  8. package/dist/gateway/static/root/assets/cron-page-Dah32HJK.js +1 -0
  9. package/dist/gateway/static/root/assets/{dist-CYgHMQO0.js → dist-BJfD9Qvs.js} +1 -1
  10. package/dist/gateway/static/root/assets/{extension-debug-page-6cRP0nA9.js → extension-debug-page-DnYuMzmH.js} +1 -1
  11. package/dist/gateway/static/root/assets/{extension-page-DpwIkspI.js → extension-page-CJfc-6XV.js} +1 -1
  12. package/dist/gateway/static/root/assets/{extension-settings-page-DYbnQUxH.js → extension-settings-page-BxdfYQMG.js} +1 -1
  13. package/dist/gateway/static/root/assets/{fetch-DTN0w7rV.js → fetch-B0aeeY0q.js} +1 -1
  14. package/dist/gateway/static/root/assets/{field-primitives-CslW6HwD.js → field-primitives-DOLHwowi.js} +1 -1
  15. package/dist/gateway/static/root/assets/{heartbeat-config-api-2UiKevxG.js → heartbeat-config-api-Bj2INAf5.js} +1 -1
  16. package/dist/gateway/static/root/assets/index-Bj_l8QDp.css +1 -0
  17. package/dist/gateway/static/root/assets/{index-DnevRVa6.js → index-DuQ1XPoA.js} +99 -98
  18. package/dist/gateway/static/root/assets/logs-page-AsOgLNJE.js +2 -0
  19. package/dist/gateway/static/root/assets/{note-detail-page-DvW2qg4i.js → note-detail-page-24J4mVP-.js} +53 -53
  20. package/dist/gateway/static/root/assets/{note-time-BEiibLJv.js → note-time-JBszYV3s.js} +1 -1
  21. package/dist/gateway/static/root/assets/notes-page-BApAirFB.js +1 -0
  22. package/dist/gateway/static/root/assets/sessions-page-DX9huWsA.js +1 -0
  23. package/dist/gateway/static/root/assets/{settings-advanced-gate-BctKqHcf.js → settings-advanced-gate-DWvhsTuz.js} +1 -1
  24. package/dist/gateway/static/root/assets/{settings-form-section-QJh5ruel.js → settings-form-section-CxMjaMiy.js} +1 -1
  25. package/dist/gateway/static/root/assets/settings-page-4VmUTzQs.js +3 -0
  26. package/dist/gateway/static/root/assets/{share-preview-page-DBsvvbmD.js → share-preview-page-IX0TJvRd.js} +1 -1
  27. package/dist/gateway/static/root/assets/skills-page-CGKGKfwe.js +2 -0
  28. package/dist/gateway/static/root/assets/{theme-store-ht5iswWS.js → theme-store-Cg_SuBw0.js} +1 -1
  29. package/dist/gateway/static/root/assets/url-BHHmdJYc.js +3 -0
  30. package/dist/gateway/static/root/assets/{utils-DhPv9xoB.js → utils-BmlcxR2j.js} +1 -1
  31. package/dist/gateway/static/root/assets/voice-api-key-field-DaGm2N4J.js +1 -0
  32. package/dist/gateway/static/root/assets/{workflow-page.utils-CJqnPWkW.js → workflow-page.utils-D0vsIGHD.js} +1 -1
  33. package/dist/gateway/static/root/assets/workflows-page-BFCrD3nw.js +27 -0
  34. package/dist/gateway/static/root/index.html +5 -5
  35. package/dist/package.js +1 -1
  36. package/dist/src/agent/inbound/turn-dispatcher.d.ts +1 -0
  37. package/dist/src/agent/inbound/turn-dispatcher.js +3 -0
  38. package/dist/src/agent/inbound/turn-dispatcher.js.map +1 -1
  39. package/dist/src/agent/lifecycle/handlers/compaction.js +1 -1
  40. package/dist/src/agent/lifecycle/handlers/compaction.js.map +1 -1
  41. package/dist/src/agent/mcp/bundle-mcp-materialize.js +1 -1
  42. package/dist/src/agent/mcp/bundle-mcp-materialize.js.map +1 -1
  43. package/dist/src/agent/mcp/bundle-mcp-runtime.js +17 -4
  44. package/dist/src/agent/mcp/bundle-mcp-runtime.js.map +1 -1
  45. package/dist/src/agent/mcp/mcp-transport-config.js +10 -3
  46. package/dist/src/agent/mcp/mcp-transport-config.js.map +1 -1
  47. package/dist/src/agent/mcp/mcp-transport.js +1 -1
  48. package/dist/src/agent/mcp/mcp-transport.js.map +1 -1
  49. package/dist/src/agent/service/process-direct-streaming.d.ts +1 -0
  50. package/dist/src/agent/service/process-direct-streaming.js +15 -12
  51. package/dist/src/agent/service/process-direct-streaming.js.map +1 -1
  52. package/dist/src/agent/service.d.ts +4 -2
  53. package/dist/src/agent/service.js +20 -4
  54. package/dist/src/agent/service.js.map +1 -1
  55. package/dist/src/agent/service.types.d.ts +3 -1
  56. package/dist/src/agent/tools/browser/tool/browser-use-tool.js +1 -1
  57. package/dist/src/agent/tools/browser/tool/browser-use-tool.js.map +1 -1
  58. package/dist/src/agent/tools/search/registry.js +1 -1
  59. package/dist/src/agent/tools/search/registry.js.map +1 -1
  60. package/dist/src/agent/tools/session-search-tool.js +1 -1
  61. package/dist/src/agent/tools/session-search-tool.js.map +1 -1
  62. package/dist/src/agent/tools/workflow-tool.js +1 -1
  63. package/dist/src/agent/tools/workflow-tool.js.map +1 -1
  64. package/dist/src/agent/workflow/progress-broker.js +1 -1
  65. package/dist/src/agent/workflow/progress-broker.js.map +1 -1
  66. package/dist/src/agent/workflow/subagent-runner.js +1 -1
  67. package/dist/src/agent/workflow/subagent-runner.js.map +1 -1
  68. package/dist/src/channels/pipeline.js +3 -2
  69. package/dist/src/channels/pipeline.js.map +1 -1
  70. package/dist/src/cli/cli-log-level-preset.d.ts +1 -1
  71. package/dist/src/cli/cli-log-level-preset.js +2 -2
  72. package/dist/src/cli/cli-log-level-preset.js.map +1 -1
  73. package/dist/src/cli/commands/logs.js +3 -3
  74. package/dist/src/cli/commands/logs.js.map +1 -1
  75. package/dist/src/cron/executor.js +7 -4
  76. package/dist/src/cron/executor.js.map +1 -1
  77. package/dist/src/gateway/hono/app.js +4 -1
  78. package/dist/src/gateway/hono/app.js.map +1 -1
  79. package/dist/src/gateway/hono/lib/route-logger.d.ts +6 -0
  80. package/dist/src/gateway/hono/lib/route-logger.js +31 -0
  81. package/dist/src/gateway/hono/lib/route-logger.js.map +1 -0
  82. package/dist/src/gateway/hono/middleware/auth.js +16 -3
  83. package/dist/src/gateway/hono/middleware/auth.js.map +1 -1
  84. package/dist/src/gateway/hono/middleware/logger.js +1 -1
  85. package/dist/src/gateway/hono/middleware/logger.js.map +1 -1
  86. package/dist/src/gateway/hono/middleware/route-errors.d.ts +5 -0
  87. package/dist/src/gateway/hono/middleware/route-errors.js +27 -0
  88. package/dist/src/gateway/hono/middleware/route-errors.js.map +1 -0
  89. package/dist/src/gateway/hono/routes/agent-stream.js +6 -0
  90. package/dist/src/gateway/hono/routes/agent-stream.js.map +1 -1
  91. package/dist/src/gateway/hono/routes/browser-install.js +2 -4
  92. package/dist/src/gateway/hono/routes/browser-install.js.map +1 -1
  93. package/dist/src/gateway/hono/routes/config.js +25 -11
  94. package/dist/src/gateway/hono/routes/config.js.map +1 -1
  95. package/dist/src/gateway/hono/routes/cron.js +5 -0
  96. package/dist/src/gateway/hono/routes/cron.js.map +1 -1
  97. package/dist/src/gateway/hono/routes/host-fs.js +2 -4
  98. package/dist/src/gateway/hono/routes/host-fs.js.map +1 -1
  99. package/dist/src/gateway/hono/routes/lazy-bundles.js +14 -1
  100. package/dist/src/gateway/hono/routes/lazy-bundles.js.map +1 -1
  101. package/dist/src/gateway/hono/routes/lazy-fallback.js +3 -0
  102. package/dist/src/gateway/hono/routes/lazy-fallback.js.map +1 -1
  103. package/dist/src/gateway/hono/routes/logs.js +39 -7
  104. package/dist/src/gateway/hono/routes/logs.js.map +1 -1
  105. package/dist/src/gateway/hono/routes/mcp.d.ts +3 -0
  106. package/dist/src/gateway/hono/routes/mcp.js +107 -0
  107. package/dist/src/gateway/hono/routes/mcp.js.map +1 -0
  108. package/dist/src/gateway/hono/routes/notes.js +105 -1
  109. package/dist/src/gateway/hono/routes/notes.js.map +1 -1
  110. package/dist/src/gateway/hono/routes/sessions.js +6 -0
  111. package/dist/src/gateway/hono/routes/sessions.js.map +1 -1
  112. package/dist/src/gateway/hono/routes/update.js +2 -4
  113. package/dist/src/gateway/hono/routes/update.js.map +1 -1
  114. package/dist/src/gateway/hono/routes/voice.js +2 -4
  115. package/dist/src/gateway/hono/routes/voice.js.map +1 -1
  116. package/dist/src/gateway/hono/routes/workspace.js +2 -4
  117. package/dist/src/gateway/hono/routes/workspace.js.map +1 -1
  118. package/dist/src/gateway/hono/sse.js +9 -2
  119. package/dist/src/gateway/hono/sse.js.map +1 -1
  120. package/dist/src/gateway/host.d.ts +2 -0
  121. package/dist/src/gateway/host.js +6 -3
  122. package/dist/src/gateway/host.js.map +1 -1
  123. package/dist/src/gateway/service/agent-runner.js +1 -1
  124. package/dist/src/gateway/service/agent-runner.js.map +1 -1
  125. package/dist/src/gateway/service/config-coordinator.js +14 -6
  126. package/dist/src/gateway/service/config-coordinator.js.map +1 -1
  127. package/dist/src/gateway/service/marketplace-service.js +1 -1
  128. package/dist/src/gateway/service/marketplace-service.js.map +1 -1
  129. package/dist/src/gateway/service/run-gateway-agent.js +22 -5
  130. package/dist/src/gateway/service/run-gateway-agent.js.map +1 -1
  131. package/dist/src/gateway/service/sse-hub.js +1 -1
  132. package/dist/src/gateway/service/sse-hub.js.map +1 -1
  133. package/dist/src/gateway/service.js +12 -5
  134. package/dist/src/gateway/service.js.map +1 -1
  135. package/dist/src/mcp/channel-bridge.js +26 -2
  136. package/dist/src/mcp/channel-bridge.js.map +1 -1
  137. package/dist/src/mcp/gateway-http-client.js +24 -2
  138. package/dist/src/mcp/gateway-http-client.js.map +1 -1
  139. package/dist/src/notes/service.d.ts +13 -1
  140. package/dist/src/notes/service.js +237 -0
  141. package/dist/src/notes/service.js.map +1 -1
  142. package/dist/src/notes/store.d.ts +3 -0
  143. package/dist/src/notes/store.js +6 -2
  144. package/dist/src/notes/store.js.map +1 -1
  145. package/dist/src/notes/types.d.ts +31 -0
  146. package/dist/src/session/config-store.js +10 -4
  147. package/dist/src/session/config-store.js.map +1 -1
  148. package/dist/src/session/index.d.ts +1 -1
  149. package/dist/src/session/index.js +2 -2
  150. package/dist/src/session/manager.js +8 -1
  151. package/dist/src/session/manager.js.map +1 -1
  152. package/dist/src/session/session-title.d.ts +19 -3
  153. package/dist/src/session/session-title.js +82 -7
  154. package/dist/src/session/session-title.js.map +1 -1
  155. package/dist/src/utils/index.js +4 -4
  156. package/dist/src/utils/logger/config.js +2 -6
  157. package/dist/src/utils/logger/config.js.map +1 -1
  158. package/dist/src/utils/logger/context.d.ts +3 -22
  159. package/dist/src/utils/logger/context.js +4 -32
  160. package/dist/src/utils/logger/context.js.map +1 -1
  161. package/dist/src/utils/logger/index.d.ts +4 -7
  162. package/dist/src/utils/logger/index.js +9 -28
  163. package/dist/src/utils/logger/index.js.map +1 -1
  164. package/dist/src/utils/logger/log-store.d.ts +14 -32
  165. package/dist/src/utils/logger/log-store.js +67 -118
  166. package/dist/src/utils/logger/log-store.js.map +1 -1
  167. package/dist/src/utils/logger/log-stream.d.ts +5 -70
  168. package/dist/src/utils/logger/log-stream.js +67 -178
  169. package/dist/src/utils/logger/log-stream.js.map +1 -1
  170. package/dist/src/utils/logger/pino-record.d.ts +8 -0
  171. package/dist/src/utils/logger/pino-record.js +83 -0
  172. package/dist/src/utils/logger/pino-record.js.map +1 -0
  173. package/dist/src/utils/logger/stats.d.ts +1 -1
  174. package/dist/src/utils/logger/stats.js +2 -2
  175. package/dist/src/utils/logger/stats.js.map +1 -1
  176. package/dist/src/utils/logger/streams.js +18 -0
  177. package/dist/src/utils/logger/streams.js.map +1 -1
  178. package/dist/src/utils/logger/types.d.ts +0 -9
  179. package/dist/src/utils/logger/types.js.map +1 -1
  180. package/dist/src/utils/logger.js +4 -4
  181. package/package.json +6 -1
  182. package/dist/gateway/static/root/assets/agents-uwPn7ZW9.js +0 -222
  183. package/dist/gateway/static/root/assets/apps-page-CWKdhSPU.js +0 -1
  184. package/dist/gateway/static/root/assets/channels-settings-hEhW7Mbk.js +0 -1
  185. package/dist/gateway/static/root/assets/cron-page-B0kvgZGR.js +0 -1
  186. package/dist/gateway/static/root/assets/index-BUKUv7QW.css +0 -1
  187. package/dist/gateway/static/root/assets/logs-page-sOP4TXJ4.js +0 -1
  188. package/dist/gateway/static/root/assets/notes-page-BFQaquHU.js +0 -1
  189. package/dist/gateway/static/root/assets/sessions-page-CptjDKAX.js +0 -1
  190. package/dist/gateway/static/root/assets/settings-page-V3p-hISB.js +0 -2
  191. package/dist/gateway/static/root/assets/skills-page-q2zPUJAR.js +0 -2
  192. package/dist/gateway/static/root/assets/url-CWWpfkq1.js +0 -3
  193. package/dist/gateway/static/root/assets/voice-api-key-field-DLSKUipa.js +0 -1
  194. package/dist/gateway/static/root/assets/workflows-page-DRRQ1A0l.js +0 -27
@@ -1 +1 @@
1
- {"version":3,"file":"store.js","names":[],"sources":["../../../src/notes/store.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\nimport { readFile, access, mkdir, writeFile, rm, readdir } from 'node:fs/promises';\nimport { join } from 'node:path';\n\nimport { writeTextAtomic } from '../infra/write-file-atomic.js';\nimport { createLogger } from '../utils/logger.js';\nimport { buildNoteIndexMeta, notePlainText } from './note-index-meta.js';\nimport { resolveNotesDir, resolveNotesIndexPath, resolveNoteItemPath, resolveNoteMediaDir, resolveNoteHistoryDir } from './paths.js';\nimport type {\n Note,\n NoteIndexEntry,\n NoteSnapshot,\n NoteSnapshotEntry,\n NotesIndexFile,\n NotesListQuery,\n SnapshotTrigger,\n} from './types.js';\n\nconst log = createLogger('NotesStore');\n\nconst DEFAULT_INDEX: NotesIndexFile = { version: 3, notes: [] };\nconst INDEX_VERSION = 3;\nconst DEBOUNCE_MS = 500;\n\nfunction noteToIndexEntry(note: Note): NoteIndexEntry {\n const { snippet, coverAttachmentId, voiceAttachmentId, voiceDurationSec, attachmentNames } = buildNoteIndexMeta(note);\n return {\n id: note.id,\n title: note.title || undefined,\n kind: note.kind,\n status: note.status,\n createdAt: note.createdAt,\n updatedAt: note.updatedAt,\n pinned: note.pinned || undefined,\n tags: note.tags?.length ? note.tags : undefined,\n snippet,\n coverAttachmentId,\n voiceAttachmentId,\n voiceDurationSec,\n attachmentNames,\n groupId: note.groupId || undefined,\n lastOpenedAt: note.lastOpenedAt || undefined,\n taskDone: note.taskMeta?.done,\n taskDueAt: note.taskMeta?.dueAt,\n };\n}\n\nexport class NotesStore {\n private indexCache: NotesIndexFile | null = null;\n private dirty = false;\n private saveTimeout: ReturnType<typeof setTimeout> | null = null;\n private initialized = false;\n\n async initialize(): Promise<void> {\n if (this.initialized) return;\n const indexPath = resolveNotesIndexPath();\n try {\n await access(indexPath);\n await this.loadIndex();\n if ((this.indexCache?.version ?? 0) < INDEX_VERSION) {\n await this.rebuildIndexFromItems();\n }\n } catch {\n await this.writeIndex(DEFAULT_INDEX);\n this.indexCache = DEFAULT_INDEX;\n }\n this.initialized = true;\n log.debug('NotesStore initialized');\n }\n\n async addNote(note: Note): Promise<void> {\n const index = await this.loadIndex();\n await this.writeNoteItem(note);\n index.notes.push(noteToIndexEntry(note));\n index.version++;\n this.scheduleIndexSave(index);\n }\n\n async getNote(id: string): Promise<Note | null> {\n const itemPath = resolveNoteItemPath(id);\n try {\n const content = await readFile(itemPath, 'utf-8');\n return JSON.parse(content) as Note;\n } catch (err) {\n const code = err && typeof err === 'object' && 'code' in err\n ? (err as NodeJS.ErrnoException).code : '';\n if (code !== 'ENOENT') {\n log.debug({ err, id }, 'Failed to read note item');\n }\n return null;\n }\n }\n\n async updateNote(id: string, patch: Partial<Note>): Promise<Note | null> {\n const existing = await this.getNote(id);\n if (!existing) return null;\n\n const updated: Note = {\n ...existing,\n ...patch,\n id: existing.id,\n createdAt: existing.createdAt,\n updatedAt: Date.now(),\n };\n\n await this.writeNoteItem(updated);\n\n const index = await this.loadIndex();\n const idx = index.notes.findIndex((n) => n.id === id);\n if (idx !== -1) {\n index.notes[idx] = noteToIndexEntry(updated);\n }\n index.version++;\n this.scheduleIndexSave(index);\n\n return updated;\n }\n\n async deleteNote(id: string): Promise<boolean> {\n const existing = await this.getNote(id);\n if (!existing) return false;\n\n const itemPath = resolveNoteItemPath(id);\n await rm(itemPath, { force: true }).catch((err) => {\n log.warn({ err, id }, 'Failed to remove note item file');\n });\n\n const mediaDir = resolveNoteMediaDir(id);\n await rm(mediaDir, { recursive: true, force: true }).catch(() => undefined);\n\n const index = await this.loadIndex();\n const before = index.notes.length;\n index.notes = index.notes.filter((n) => n.id !== id);\n if (index.notes.length === before) {\n log.debug({ id }, 'Deleted note file but index entry was missing');\n }\n index.version++;\n this.scheduleIndexSave(index);\n\n return true;\n }\n\n async listNotes(query: NotesListQuery = {}): Promise<{ items: NoteIndexEntry[]; total: number }> {\n const index = await this.loadIndex();\n let results = index.notes;\n\n if (query.status) {\n results = results.filter((n) => n.status === query.status);\n } else {\n results = results.filter((n) => n.status !== 'trashed');\n }\n if (query.kind) {\n results = results.filter((n) => n.kind === query.kind);\n }\n if (query.tag) {\n results = results.filter((n) => n.tags?.includes(query.tag!));\n }\n if (query.pinned !== undefined) {\n results = results.filter((n) => Boolean(n.pinned) === query.pinned);\n }\n if (query.groupId !== undefined) {\n if (query.groupId === 'ungrouped') {\n results = results.filter((n) => !n.groupId);\n } else {\n results = results.filter((n) => n.groupId === query.groupId);\n }\n }\n if (query.pendingTasksOnly) {\n results = results.filter((n) => n.kind === 'task' && !n.taskDone);\n }\n if (query.search) {\n const term = query.search.toLowerCase();\n const indexMatches = results.filter((n) => this.noteIndexEntryMatchesSearch(n, term));\n const indexMatchedIds = new Set(indexMatches.map((n) => n.id));\n const contentMatches: NoteIndexEntry[] = [];\n const candidates = results.filter((n) => !indexMatchedIds.has(n.id));\n for (const candidate of candidates) {\n const note = await this.getNote(candidate.id);\n if (!note) continue;\n const content = [note.title, notePlainText(note), note.attachments?.map((a) => a.transcript).join(' ')]\n .filter(Boolean)\n .join(' ')\n .toLowerCase();\n if (content.includes(term)) {\n contentMatches.push(candidate);\n }\n }\n results = [...indexMatches, ...contentMatches];\n }\n\n const sortField = query.sortBy || 'createdAt';\n const sortDir = query.sortOrder === 'asc' ? 1 : -1;\n results = [...results].sort((a, b) => {\n const aVal = a[sortField] ?? 0;\n const bVal = b[sortField] ?? 0;\n return (aVal - bVal) * sortDir;\n });\n\n const total = results.length;\n const offset = query.offset || 0;\n const limit = Math.min(query.limit || 50, 200);\n const items = results.slice(offset, offset + limit);\n\n return { items, total };\n }\n\n private noteIndexEntryMatchesSearch(entry: NoteIndexEntry, term: string): boolean {\n return Boolean(\n entry.title?.toLowerCase().includes(term) ||\n entry.snippet?.toLowerCase().includes(term) ||\n entry.tags?.some((tag) => tag.toLowerCase().includes(term)) ||\n entry.attachmentNames?.some((name) => name.toLowerCase().includes(term)),\n );\n }\n\n async saveAttachment(\n noteId: string,\n fileName: string,\n buffer: Buffer,\n ): Promise<{ relativePath: string; size: number }> {\n const mediaDir = resolveNoteMediaDir(noteId);\n await mkdir(mediaDir, { recursive: true });\n const safeName = `${randomUUID().slice(0, 8)}_${fileName.replace(/[^a-zA-Z0-9._-]/g, '_')}`;\n const filePath = join(mediaDir, safeName);\n await writeFile(filePath, buffer);\n return { relativePath: safeName, size: buffer.length };\n }\n\n resolveAttachmentPath(noteId: string, relativePath: string): string {\n return join(resolveNoteMediaDir(noteId), relativePath);\n }\n\n async deleteAttachmentFile(noteId: string, relativePath: string): Promise<void> {\n const filePath = this.resolveAttachmentPath(noteId, relativePath);\n await rm(filePath, { force: true }).catch((err) => {\n log.warn({ err, noteId, relativePath }, 'Failed to remove note attachment file');\n });\n }\n\n async saveSnapshot(note: Note, trigger: SnapshotTrigger): Promise<void> {\n const historyDir = resolveNoteHistoryDir(note.id);\n await mkdir(historyDir, { recursive: true });\n const snapshot: NoteSnapshot = {\n noteId: note.id,\n timestamp: Date.now(),\n trigger,\n title: note.title,\n text: note.text,\n blocks: note.blocks,\n tags: note.tags,\n kind: note.kind,\n status: note.status,\n };\n const filePath = join(historyDir, `${snapshot.timestamp}.json`);\n await writeTextAtomic(filePath, JSON.stringify(snapshot, null, 2));\n log.debug({ noteId: note.id, trigger, timestamp: snapshot.timestamp }, 'Snapshot saved');\n }\n\n async listSnapshots(noteId: string): Promise<NoteSnapshotEntry[]> {\n const historyDir = resolveNoteHistoryDir(noteId);\n let files: string[];\n try {\n files = await readdir(historyDir);\n } catch {\n return [];\n }\n const entries: NoteSnapshotEntry[] = [];\n for (const file of files) {\n if (!file.endsWith('.json')) continue;\n const timestamp = parseInt(file.slice(0, -'.json'.length), 10);\n if (!Number.isFinite(timestamp)) continue;\n try {\n const content = await readFile(join(historyDir, file), 'utf-8');\n const snapshot = JSON.parse(content) as NoteSnapshot;\n const rawText = snapshot.text ?? '';\n entries.push({\n timestamp: snapshot.timestamp,\n trigger: snapshot.trigger,\n snippet: rawText.slice(0, 80) || undefined,\n });\n } catch {\n log.debug({ noteId, file }, 'Skipped unreadable snapshot');\n }\n }\n entries.sort((a, b) => b.timestamp - a.timestamp);\n return entries;\n }\n\n async getSnapshot(noteId: string, timestamp: number): Promise<NoteSnapshot | null> {\n const filePath = join(resolveNoteHistoryDir(noteId), `${timestamp}.json`);\n try {\n const content = await readFile(filePath, 'utf-8');\n return JSON.parse(content) as NoteSnapshot;\n } catch {\n return null;\n }\n }\n\n async pruneSnapshots(noteId: string, maxCount: number): Promise<void> {\n const historyDir = resolveNoteHistoryDir(noteId);\n let files: string[];\n try {\n files = await readdir(historyDir);\n } catch {\n return;\n }\n const jsonFiles = files\n .filter((f) => f.endsWith('.json'))\n .sort();\n if (jsonFiles.length <= maxCount) return;\n const toDelete = jsonFiles.slice(0, jsonFiles.length - maxCount);\n for (const file of toDelete) {\n await rm(join(historyDir, file), { force: true }).catch(() => undefined);\n }\n log.debug({ noteId, deleted: toDelete.length }, 'Pruned old snapshots');\n }\n\n async deleteAllSnapshots(noteId: string): Promise<void> {\n const historyDir = resolveNoteHistoryDir(noteId);\n await rm(historyDir, { recursive: true, force: true }).catch(() => undefined);\n }\n\n async flush(): Promise<void> {\n if (!this.dirty || !this.indexCache) return;\n if (this.saveTimeout) {\n clearTimeout(this.saveTimeout);\n this.saveTimeout = null;\n }\n await this.writeIndex(this.indexCache);\n this.dirty = false;\n }\n\n private async loadIndex(): Promise<NotesIndexFile> {\n if (this.indexCache) return this.indexCache;\n const indexPath = resolveNotesIndexPath();\n try {\n const content = await readFile(indexPath, 'utf-8');\n const data = JSON.parse(content) as NotesIndexFile;\n if (!data.notes || !Array.isArray(data.notes)) {\n log.warn('Notes index invalid, resetting');\n this.indexCache = DEFAULT_INDEX;\n return this.indexCache;\n }\n this.indexCache = data;\n return data;\n } catch {\n this.indexCache = DEFAULT_INDEX;\n return this.indexCache;\n }\n }\n\n private async writeIndex(data: NotesIndexFile): Promise<void> {\n const indexPath = resolveNotesIndexPath();\n await writeTextAtomic(indexPath, JSON.stringify(data, null, 2));\n log.debug({ count: data.notes.length }, 'Notes index saved');\n }\n\n private async writeNoteItem(note: Note): Promise<void> {\n const itemPath = resolveNoteItemPath(note.id);\n await writeTextAtomic(itemPath, JSON.stringify(note, null, 2));\n }\n\n private scheduleIndexSave(data: NotesIndexFile): void {\n this.indexCache = data;\n this.dirty = true;\n if (this.saveTimeout) {\n clearTimeout(this.saveTimeout);\n }\n this.saveTimeout = setTimeout(() => {\n this.flush().catch((err) => {\n log.error({ err }, 'Failed to flush notes index');\n });\n }, DEBOUNCE_MS);\n }\n\n private async rebuildIndexFromItems(): Promise<void> {\n const itemsDir = join(resolveNotesDir(), 'items');\n let files: string[];\n try {\n files = await readdir(itemsDir);\n } catch {\n this.indexCache = DEFAULT_INDEX;\n await this.writeIndex(DEFAULT_INDEX);\n return;\n }\n\n const entries: NoteIndexEntry[] = [];\n for (const file of files) {\n if (!file.endsWith('.json')) continue;\n const noteId = file.slice(0, -'.json'.length);\n const note = await this.getNote(noteId);\n if (note) {\n entries.push(noteToIndexEntry(note));\n }\n }\n\n entries.sort((a, b) => b.createdAt - a.createdAt);\n const index: NotesIndexFile = { version: INDEX_VERSION, notes: entries };\n this.indexCache = index;\n await this.writeIndex(index);\n log.debug({ count: entries.length }, 'Notes index rebuilt');\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"store.js","names":[],"sources":["../../../src/notes/store.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\nimport { readFile, access, mkdir, writeFile, rm, readdir } from 'node:fs/promises';\nimport { join } from 'node:path';\n\nimport { writeTextAtomic } from '../infra/write-file-atomic.js';\nimport { createLogger } from '../utils/logger.js';\nimport { buildNoteIndexMeta, notePlainText } from './note-index-meta.js';\nimport { resolveNotesDir, resolveNotesIndexPath, resolveNoteItemPath, resolveNoteMediaDir, resolveNoteHistoryDir } from './paths.js';\nimport type {\n Note,\n NoteIndexEntry,\n NoteSnapshot,\n NoteSnapshotEntry,\n NotesIndexFile,\n NotesListQuery,\n SnapshotTrigger,\n} from './types.js';\n\nconst log = createLogger('NotesStore');\n\nconst DEFAULT_INDEX: NotesIndexFile = { version: 3, notes: [] };\nconst INDEX_VERSION = 3;\nconst DEBOUNCE_MS = 500;\n\nfunction noteToIndexEntry(note: Note): NoteIndexEntry {\n const { snippet, coverAttachmentId, voiceAttachmentId, voiceDurationSec, attachmentNames } = buildNoteIndexMeta(note);\n return {\n id: note.id,\n title: note.title || undefined,\n kind: note.kind,\n status: note.status,\n createdAt: note.createdAt,\n updatedAt: note.updatedAt,\n pinned: note.pinned || undefined,\n tags: note.tags?.length ? note.tags : undefined,\n snippet,\n coverAttachmentId,\n voiceAttachmentId,\n voiceDurationSec,\n attachmentNames,\n groupId: note.groupId || undefined,\n lastOpenedAt: note.lastOpenedAt || undefined,\n taskDone: note.taskMeta?.done,\n taskDueAt: note.taskMeta?.dueAt,\n };\n}\n\nexport class NotesStore {\n private indexCache: NotesIndexFile | null = null;\n private dirty = false;\n private saveTimeout: ReturnType<typeof setTimeout> | null = null;\n private initialized = false;\n\n async initialize(): Promise<void> {\n if (this.initialized) return;\n const indexPath = resolveNotesIndexPath();\n try {\n await access(indexPath);\n await this.loadIndex();\n if ((this.indexCache?.version ?? 0) < INDEX_VERSION) {\n await this.rebuildIndexFromItems();\n }\n } catch {\n await this.writeIndex(DEFAULT_INDEX);\n this.indexCache = DEFAULT_INDEX;\n }\n this.initialized = true;\n log.debug('NotesStore initialized');\n }\n\n async addNote(note: Note): Promise<void> {\n const index = await this.loadIndex();\n await this.writeNoteItem(note);\n index.notes.push(noteToIndexEntry(note));\n index.version++;\n this.scheduleIndexSave(index);\n }\n\n async getNote(id: string): Promise<Note | null> {\n const itemPath = resolveNoteItemPath(id);\n try {\n const content = await readFile(itemPath, 'utf-8');\n return JSON.parse(content) as Note;\n } catch (err) {\n const code = err && typeof err === 'object' && 'code' in err\n ? (err as NodeJS.ErrnoException).code : '';\n if (code !== 'ENOENT') {\n log.debug({ err, id }, 'Failed to read note item');\n }\n return null;\n }\n }\n\n async updateNote(id: string, patch: Partial<Note>): Promise<Note | null> {\n const existing = await this.getNote(id);\n if (!existing) return null;\n\n const updated: Note = {\n ...existing,\n ...patch,\n id: existing.id,\n createdAt: existing.createdAt,\n updatedAt: Date.now(),\n };\n\n await this.writeNoteItem(updated);\n\n const index = await this.loadIndex();\n const idx = index.notes.findIndex((n) => n.id === id);\n if (idx !== -1) {\n index.notes[idx] = noteToIndexEntry(updated);\n }\n index.version++;\n this.scheduleIndexSave(index);\n\n return updated;\n }\n\n async deleteNote(id: string): Promise<boolean> {\n const existing = await this.getNote(id);\n if (!existing) return false;\n\n const itemPath = resolveNoteItemPath(id);\n await rm(itemPath, { force: true }).catch((err) => {\n log.warn({ err, id }, 'Failed to remove note item file');\n });\n\n const mediaDir = resolveNoteMediaDir(id);\n await rm(mediaDir, { recursive: true, force: true }).catch(() => undefined);\n\n const index = await this.loadIndex();\n const before = index.notes.length;\n index.notes = index.notes.filter((n) => n.id !== id);\n if (index.notes.length === before) {\n log.debug({ id }, 'Deleted note file but index entry was missing');\n }\n index.version++;\n this.scheduleIndexSave(index);\n\n return true;\n }\n\n async listNotes(query: NotesListQuery = {}): Promise<{ items: NoteIndexEntry[]; total: number; limit: number; offset: number; hasMore: boolean }> {\n const index = await this.loadIndex();\n let results = index.notes;\n\n if (query.status) {\n results = results.filter((n) => n.status === query.status);\n } else {\n results = results.filter((n) => n.status !== 'trashed');\n }\n if (query.kind) {\n results = results.filter((n) => n.kind === query.kind);\n }\n if (query.tag) {\n results = results.filter((n) => n.tags?.includes(query.tag!));\n }\n if (query.pinned !== undefined) {\n results = results.filter((n) => Boolean(n.pinned) === query.pinned);\n }\n if (query.groupId !== undefined) {\n if (query.groupId === 'ungrouped') {\n results = results.filter((n) => !n.groupId);\n } else {\n results = results.filter((n) => n.groupId === query.groupId);\n }\n }\n if (query.pendingTasksOnly) {\n results = results.filter((n) => n.kind === 'task' && !n.taskDone);\n }\n if (query.search) {\n const term = query.search.toLowerCase();\n const indexMatches = results.filter((n) => this.noteIndexEntryMatchesSearch(n, term));\n const indexMatchedIds = new Set(indexMatches.map((n) => n.id));\n const contentMatches: NoteIndexEntry[] = [];\n const candidates = results.filter((n) => !indexMatchedIds.has(n.id));\n for (const candidate of candidates) {\n const note = await this.getNote(candidate.id);\n if (!note) continue;\n const content = [note.title, notePlainText(note), note.attachments?.map((a) => a.transcript).join(' ')]\n .filter(Boolean)\n .join(' ')\n .toLowerCase();\n if (content.includes(term)) {\n contentMatches.push(candidate);\n }\n }\n results = [...indexMatches, ...contentMatches];\n }\n\n const sortField = query.sortBy || 'createdAt';\n const sortDir = query.sortOrder === 'asc' ? 1 : -1;\n results = [...results].sort((a, b) => {\n const aVal = a[sortField] ?? 0;\n const bVal = b[sortField] ?? 0;\n return (aVal - bVal) * sortDir;\n });\n\n const total = results.length;\n const offset = query.offset || 0;\n const limit = Math.min(query.limit || 50, 200);\n const items = results.slice(offset, offset + limit);\n const hasMore = offset + items.length < total;\n\n return { items, total, limit, offset, hasMore };\n }\n\n private noteIndexEntryMatchesSearch(entry: NoteIndexEntry, term: string): boolean {\n return Boolean(\n entry.title?.toLowerCase().includes(term) ||\n entry.snippet?.toLowerCase().includes(term) ||\n entry.tags?.some((tag) => tag.toLowerCase().includes(term)) ||\n entry.attachmentNames?.some((name) => name.toLowerCase().includes(term)),\n );\n }\n\n async saveAttachment(\n noteId: string,\n fileName: string,\n buffer: Buffer,\n ): Promise<{ relativePath: string; size: number }> {\n const mediaDir = resolveNoteMediaDir(noteId);\n await mkdir(mediaDir, { recursive: true });\n const safeName = `${randomUUID().slice(0, 8)}_${fileName.replace(/[^a-zA-Z0-9._-]/g, '_')}`;\n const filePath = join(mediaDir, safeName);\n await writeFile(filePath, buffer);\n return { relativePath: safeName, size: buffer.length };\n }\n\n resolveAttachmentPath(noteId: string, relativePath: string): string {\n return join(resolveNoteMediaDir(noteId), relativePath);\n }\n\n async deleteAttachmentFile(noteId: string, relativePath: string): Promise<void> {\n const filePath = this.resolveAttachmentPath(noteId, relativePath);\n await rm(filePath, { force: true }).catch((err) => {\n log.warn({ err, noteId, relativePath }, 'Failed to remove note attachment file');\n });\n }\n\n async saveSnapshot(note: Note, trigger: SnapshotTrigger): Promise<void> {\n const historyDir = resolveNoteHistoryDir(note.id);\n await mkdir(historyDir, { recursive: true });\n const snapshot: NoteSnapshot = {\n noteId: note.id,\n timestamp: Date.now(),\n trigger,\n title: note.title,\n text: note.text,\n blocks: note.blocks,\n tags: note.tags,\n kind: note.kind,\n status: note.status,\n };\n const filePath = join(historyDir, `${snapshot.timestamp}.json`);\n await writeTextAtomic(filePath, JSON.stringify(snapshot, null, 2));\n log.debug({ noteId: note.id, trigger, timestamp: snapshot.timestamp }, 'Snapshot saved');\n }\n\n async listSnapshots(noteId: string): Promise<NoteSnapshotEntry[]> {\n const historyDir = resolveNoteHistoryDir(noteId);\n let files: string[];\n try {\n files = await readdir(historyDir);\n } catch {\n return [];\n }\n const entries: NoteSnapshotEntry[] = [];\n for (const file of files) {\n if (!file.endsWith('.json')) continue;\n const timestamp = parseInt(file.slice(0, -'.json'.length), 10);\n if (!Number.isFinite(timestamp)) continue;\n try {\n const content = await readFile(join(historyDir, file), 'utf-8');\n const snapshot = JSON.parse(content) as NoteSnapshot;\n const rawText = snapshot.text ?? '';\n entries.push({\n timestamp: snapshot.timestamp,\n trigger: snapshot.trigger,\n snippet: rawText.slice(0, 80) || undefined,\n });\n } catch {\n log.debug({ noteId, file }, 'Skipped unreadable snapshot');\n }\n }\n entries.sort((a, b) => b.timestamp - a.timestamp);\n return entries;\n }\n\n async getSnapshot(noteId: string, timestamp: number): Promise<NoteSnapshot | null> {\n const filePath = join(resolveNoteHistoryDir(noteId), `${timestamp}.json`);\n try {\n const content = await readFile(filePath, 'utf-8');\n return JSON.parse(content) as NoteSnapshot;\n } catch {\n return null;\n }\n }\n\n async pruneSnapshots(noteId: string, maxCount: number): Promise<void> {\n const historyDir = resolveNoteHistoryDir(noteId);\n let files: string[];\n try {\n files = await readdir(historyDir);\n } catch {\n return;\n }\n const jsonFiles = files\n .filter((f) => f.endsWith('.json'))\n .sort();\n if (jsonFiles.length <= maxCount) return;\n const toDelete = jsonFiles.slice(0, jsonFiles.length - maxCount);\n for (const file of toDelete) {\n await rm(join(historyDir, file), { force: true }).catch(() => undefined);\n }\n log.debug({ noteId, deleted: toDelete.length }, 'Pruned old snapshots');\n }\n\n async deleteAllSnapshots(noteId: string): Promise<void> {\n const historyDir = resolveNoteHistoryDir(noteId);\n await rm(historyDir, { recursive: true, force: true }).catch(() => undefined);\n }\n\n async flush(): Promise<void> {\n if (!this.dirty || !this.indexCache) return;\n if (this.saveTimeout) {\n clearTimeout(this.saveTimeout);\n this.saveTimeout = null;\n }\n await this.writeIndex(this.indexCache);\n this.dirty = false;\n }\n\n private async loadIndex(): Promise<NotesIndexFile> {\n if (this.indexCache) return this.indexCache;\n const indexPath = resolveNotesIndexPath();\n try {\n const content = await readFile(indexPath, 'utf-8');\n const data = JSON.parse(content) as NotesIndexFile;\n if (!data.notes || !Array.isArray(data.notes)) {\n log.warn('Notes index invalid, resetting');\n this.indexCache = DEFAULT_INDEX;\n return this.indexCache;\n }\n this.indexCache = data;\n return data;\n } catch {\n this.indexCache = DEFAULT_INDEX;\n return this.indexCache;\n }\n }\n\n private async writeIndex(data: NotesIndexFile): Promise<void> {\n const indexPath = resolveNotesIndexPath();\n await writeTextAtomic(indexPath, JSON.stringify(data, null, 2));\n log.debug({ count: data.notes.length }, 'Notes index saved');\n }\n\n private async writeNoteItem(note: Note): Promise<void> {\n const itemPath = resolveNoteItemPath(note.id);\n await writeTextAtomic(itemPath, JSON.stringify(note, null, 2));\n }\n\n private scheduleIndexSave(data: NotesIndexFile): void {\n this.indexCache = data;\n this.dirty = true;\n if (this.saveTimeout) {\n clearTimeout(this.saveTimeout);\n }\n this.saveTimeout = setTimeout(() => {\n this.flush().catch((err) => {\n log.error({ err }, 'Failed to flush notes index');\n });\n }, DEBOUNCE_MS);\n }\n\n private async rebuildIndexFromItems(): Promise<void> {\n const itemsDir = join(resolveNotesDir(), 'items');\n let files: string[];\n try {\n files = await readdir(itemsDir);\n } catch {\n this.indexCache = DEFAULT_INDEX;\n await this.writeIndex(DEFAULT_INDEX);\n return;\n }\n\n const entries: NoteIndexEntry[] = [];\n for (const file of files) {\n if (!file.endsWith('.json')) continue;\n const noteId = file.slice(0, -'.json'.length);\n const note = await this.getNote(noteId);\n if (note) {\n entries.push(noteToIndexEntry(note));\n }\n }\n\n entries.sort((a, b) => b.createdAt - a.createdAt);\n const index: NotesIndexFile = { version: INDEX_VERSION, notes: entries };\n this.indexCache = index;\n await this.writeIndex(index);\n log.debug({ count: entries.length }, 'Notes index rebuilt');\n }\n}\n"],"mappings":";;;;;;;;;wBAIgE;aACd;AAalD,MAAM,MAAM,aAAa,aAAa;AAEtC,MAAM,gBAAgC;CAAE,SAAS;CAAG,OAAO,EAAE;CAAE;AAC/D,MAAM,gBAAgB;AACtB,MAAM,cAAc;AAEpB,SAAS,iBAAiB,MAA4B;CACpD,MAAM,EAAE,SAAS,mBAAmB,mBAAmB,kBAAkB,oBAAoB,mBAAmB,KAAK;AACrH,QAAO;EACL,IAAI,KAAK;EACT,OAAO,KAAK,SAAS,KAAA;EACrB,MAAM,KAAK;EACX,QAAQ,KAAK;EACb,WAAW,KAAK;EAChB,WAAW,KAAK;EAChB,QAAQ,KAAK,UAAU,KAAA;EACvB,MAAM,KAAK,MAAM,SAAS,KAAK,OAAO,KAAA;EACtC;EACA;EACA;EACA;EACA;EACA,SAAS,KAAK,WAAW,KAAA;EACzB,cAAc,KAAK,gBAAgB,KAAA;EACnC,UAAU,KAAK,UAAU;EACzB,WAAW,KAAK,UAAU;EAC3B;;AAGH,IAAa,aAAb,MAAwB;CACtB,aAA4C;CAC5C,QAAgB;CAChB,cAA4D;CAC5D,cAAsB;CAEtB,MAAM,aAA4B;AAChC,MAAI,KAAK,YAAa;EACtB,MAAM,YAAY,uBAAuB;AACzC,MAAI;AACF,SAAM,OAAO,UAAU;AACvB,SAAM,KAAK,WAAW;AACtB,QAAK,KAAK,YAAY,WAAW,KAAK,cACpC,OAAM,KAAK,uBAAuB;UAE9B;AACN,SAAM,KAAK,WAAW,cAAc;AACpC,QAAK,aAAa;;AAEpB,OAAK,cAAc;AACnB,MAAI,MAAM,yBAAyB;;CAGrC,MAAM,QAAQ,MAA2B;EACvC,MAAM,QAAQ,MAAM,KAAK,WAAW;AACpC,QAAM,KAAK,cAAc,KAAK;AAC9B,QAAM,MAAM,KAAK,iBAAiB,KAAK,CAAC;AACxC,QAAM;AACN,OAAK,kBAAkB,MAAM;;CAG/B,MAAM,QAAQ,IAAkC;EAC9C,MAAM,WAAW,oBAAoB,GAAG;AACxC,MAAI;GACF,MAAM,UAAU,MAAM,SAAS,UAAU,QAAQ;AACjD,UAAO,KAAK,MAAM,QAAQ;WACnB,KAAK;AAGZ,QAFa,OAAO,OAAO,QAAQ,YAAY,UAAU,MACpD,IAA8B,OAAO,QAC7B,SACX,KAAI,MAAM;IAAE;IAAK;IAAI,EAAE,2BAA2B;AAEpD,UAAO;;;CAIX,MAAM,WAAW,IAAY,OAA4C;EACvE,MAAM,WAAW,MAAM,KAAK,QAAQ,GAAG;AACvC,MAAI,CAAC,SAAU,QAAO;EAEtB,MAAM,UAAgB;GACpB,GAAG;GACH,GAAG;GACH,IAAI,SAAS;GACb,WAAW,SAAS;GACpB,WAAW,KAAK,KAAK;GACtB;AAED,QAAM,KAAK,cAAc,QAAQ;EAEjC,MAAM,QAAQ,MAAM,KAAK,WAAW;EACpC,MAAM,MAAM,MAAM,MAAM,WAAW,MAAM,EAAE,OAAO,GAAG;AACrD,MAAI,QAAQ,GACV,OAAM,MAAM,OAAO,iBAAiB,QAAQ;AAE9C,QAAM;AACN,OAAK,kBAAkB,MAAM;AAE7B,SAAO;;CAGT,MAAM,WAAW,IAA8B;AAE7C,MAAI,CAAC,MADkB,KAAK,QAAQ,GAAG,CACxB,QAAO;AAGtB,QAAM,GADW,oBAAoB,GACpB,EAAE,EAAE,OAAO,MAAM,CAAC,CAAC,OAAO,QAAQ;AACjD,OAAI,KAAK;IAAE;IAAK;IAAI,EAAE,kCAAkC;IACxD;AAGF,QAAM,GADW,oBAAoB,GACpB,EAAE;GAAE,WAAW;GAAM,OAAO;GAAM,CAAC,CAAC,YAAY,KAAA,EAAU;EAE3E,MAAM,QAAQ,MAAM,KAAK,WAAW;EACpC,MAAM,SAAS,MAAM,MAAM;AAC3B,QAAM,QAAQ,MAAM,MAAM,QAAQ,MAAM,EAAE,OAAO,GAAG;AACpD,MAAI,MAAM,MAAM,WAAW,OACzB,KAAI,MAAM,EAAE,IAAI,EAAE,gDAAgD;AAEpE,QAAM;AACN,OAAK,kBAAkB,MAAM;AAE7B,SAAO;;CAGT,MAAM,UAAU,QAAwB,EAAE,EAAwG;EAEhJ,IAAI,WAAU,MADM,KAAK,WAAW,EAChB;AAEpB,MAAI,MAAM,OACR,WAAU,QAAQ,QAAQ,MAAM,EAAE,WAAW,MAAM,OAAO;MAE1D,WAAU,QAAQ,QAAQ,MAAM,EAAE,WAAW,UAAU;AAEzD,MAAI,MAAM,KACR,WAAU,QAAQ,QAAQ,MAAM,EAAE,SAAS,MAAM,KAAK;AAExD,MAAI,MAAM,IACR,WAAU,QAAQ,QAAQ,MAAM,EAAE,MAAM,SAAS,MAAM,IAAK,CAAC;AAE/D,MAAI,MAAM,WAAW,KAAA,EACnB,WAAU,QAAQ,QAAQ,MAAM,QAAQ,EAAE,OAAO,KAAK,MAAM,OAAO;AAErE,MAAI,MAAM,YAAY,KAAA,EACpB,KAAI,MAAM,YAAY,YACpB,WAAU,QAAQ,QAAQ,MAAM,CAAC,EAAE,QAAQ;MAE3C,WAAU,QAAQ,QAAQ,MAAM,EAAE,YAAY,MAAM,QAAQ;AAGhE,MAAI,MAAM,iBACR,WAAU,QAAQ,QAAQ,MAAM,EAAE,SAAS,UAAU,CAAC,EAAE,SAAS;AAEnE,MAAI,MAAM,QAAQ;GAChB,MAAM,OAAO,MAAM,OAAO,aAAa;GACvC,MAAM,eAAe,QAAQ,QAAQ,MAAM,KAAK,4BAA4B,GAAG,KAAK,CAAC;GACrF,MAAM,kBAAkB,IAAI,IAAI,aAAa,KAAK,MAAM,EAAE,GAAG,CAAC;GAC9D,MAAM,iBAAmC,EAAE;GAC3C,MAAM,aAAa,QAAQ,QAAQ,MAAM,CAAC,gBAAgB,IAAI,EAAE,GAAG,CAAC;AACpE,QAAK,MAAM,aAAa,YAAY;IAClC,MAAM,OAAO,MAAM,KAAK,QAAQ,UAAU,GAAG;AAC7C,QAAI,CAAC,KAAM;AAKX,QAJgB;KAAC,KAAK;KAAO,cAAc,KAAK;KAAE,KAAK,aAAa,KAAK,MAAM,EAAE,WAAW,CAAC,KAAK,IAAI;KAAC,CACpG,OAAO,QAAQ,CACf,KAAK,IAAI,CACT,aACQ,CAAC,SAAS,KAAK,CACxB,gBAAe,KAAK,UAAU;;AAGlC,aAAU,CAAC,GAAG,cAAc,GAAG,eAAe;;EAGhD,MAAM,YAAY,MAAM,UAAU;EAClC,MAAM,UAAU,MAAM,cAAc,QAAQ,IAAI;AAChD,YAAU,CAAC,GAAG,QAAQ,CAAC,MAAM,GAAG,MAAM;AAGpC,YAFa,EAAE,cAAc,MAChB,EAAE,cAAc,MACN;IACvB;EAEF,MAAM,QAAQ,QAAQ;EACtB,MAAM,SAAS,MAAM,UAAU;EAC/B,MAAM,QAAQ,KAAK,IAAI,MAAM,SAAS,IAAI,IAAI;EAC9C,MAAM,QAAQ,QAAQ,MAAM,QAAQ,SAAS,MAAM;AAGnD,SAAO;GAAE;GAAO;GAAO;GAAO;GAAQ,SAFtB,SAAS,MAAM,SAAS;GAEO;;CAGjD,4BAAoC,OAAuB,MAAuB;AAChF,SAAO,QACL,MAAM,OAAO,aAAa,CAAC,SAAS,KAAK,IACzC,MAAM,SAAS,aAAa,CAAC,SAAS,KAAK,IAC3C,MAAM,MAAM,MAAM,QAAQ,IAAI,aAAa,CAAC,SAAS,KAAK,CAAC,IAC3D,MAAM,iBAAiB,MAAM,SAAS,KAAK,aAAa,CAAC,SAAS,KAAK,CAAC,CACzE;;CAGH,MAAM,eACJ,QACA,UACA,QACiD;EACjD,MAAM,WAAW,oBAAoB,OAAO;AAC5C,QAAM,MAAM,UAAU,EAAE,WAAW,MAAM,CAAC;EAC1C,MAAM,WAAW,GAAG,YAAY,CAAC,MAAM,GAAG,EAAE,CAAC,GAAG,SAAS,QAAQ,oBAAoB,IAAI;AAEzF,QAAM,UADW,KAAK,UAAU,SACR,EAAE,OAAO;AACjC,SAAO;GAAE,cAAc;GAAU,MAAM,OAAO;GAAQ;;CAGxD,sBAAsB,QAAgB,cAA8B;AAClE,SAAO,KAAK,oBAAoB,OAAO,EAAE,aAAa;;CAGxD,MAAM,qBAAqB,QAAgB,cAAqC;AAE9E,QAAM,GADW,KAAK,sBAAsB,QAAQ,aACnC,EAAE,EAAE,OAAO,MAAM,CAAC,CAAC,OAAO,QAAQ;AACjD,OAAI,KAAK;IAAE;IAAK;IAAQ;IAAc,EAAE,wCAAwC;IAChF;;CAGJ,MAAM,aAAa,MAAY,SAAyC;EACtE,MAAM,aAAa,sBAAsB,KAAK,GAAG;AACjD,QAAM,MAAM,YAAY,EAAE,WAAW,MAAM,CAAC;EAC5C,MAAM,WAAyB;GAC7B,QAAQ,KAAK;GACb,WAAW,KAAK,KAAK;GACrB;GACA,OAAO,KAAK;GACZ,MAAM,KAAK;GACX,QAAQ,KAAK;GACb,MAAM,KAAK;GACX,MAAM,KAAK;GACX,QAAQ,KAAK;GACd;AAED,QAAM,gBADW,KAAK,YAAY,GAAG,SAAS,UAAU,OAC1B,EAAE,KAAK,UAAU,UAAU,MAAM,EAAE,CAAC;AAClE,MAAI,MAAM;GAAE,QAAQ,KAAK;GAAI;GAAS,WAAW,SAAS;GAAW,EAAE,iBAAiB;;CAG1F,MAAM,cAAc,QAA8C;EAChE,MAAM,aAAa,sBAAsB,OAAO;EAChD,IAAI;AACJ,MAAI;AACF,WAAQ,MAAM,QAAQ,WAAW;UAC3B;AACN,UAAO,EAAE;;EAEX,MAAM,UAA+B,EAAE;AACvC,OAAK,MAAM,QAAQ,OAAO;AACxB,OAAI,CAAC,KAAK,SAAS,QAAQ,CAAE;GAC7B,MAAM,YAAY,SAAS,KAAK,MAAM,GAAG,GAAgB,EAAE,GAAG;AAC9D,OAAI,CAAC,OAAO,SAAS,UAAU,CAAE;AACjC,OAAI;IACF,MAAM,UAAU,MAAM,SAAS,KAAK,YAAY,KAAK,EAAE,QAAQ;IAC/D,MAAM,WAAW,KAAK,MAAM,QAAQ;IACpC,MAAM,UAAU,SAAS,QAAQ;AACjC,YAAQ,KAAK;KACX,WAAW,SAAS;KACpB,SAAS,SAAS;KAClB,SAAS,QAAQ,MAAM,GAAG,GAAG,IAAI,KAAA;KAClC,CAAC;WACI;AACN,QAAI,MAAM;KAAE;KAAQ;KAAM,EAAE,8BAA8B;;;AAG9D,UAAQ,MAAM,GAAG,MAAM,EAAE,YAAY,EAAE,UAAU;AACjD,SAAO;;CAGT,MAAM,YAAY,QAAgB,WAAiD;EACjF,MAAM,WAAW,KAAK,sBAAsB,OAAO,EAAE,GAAG,UAAU,OAAO;AACzE,MAAI;GACF,MAAM,UAAU,MAAM,SAAS,UAAU,QAAQ;AACjD,UAAO,KAAK,MAAM,QAAQ;UACpB;AACN,UAAO;;;CAIX,MAAM,eAAe,QAAgB,UAAiC;EACpE,MAAM,aAAa,sBAAsB,OAAO;EAChD,IAAI;AACJ,MAAI;AACF,WAAQ,MAAM,QAAQ,WAAW;UAC3B;AACN;;EAEF,MAAM,YAAY,MACf,QAAQ,MAAM,EAAE,SAAS,QAAQ,CAAC,CAClC,MAAM;AACT,MAAI,UAAU,UAAU,SAAU;EAClC,MAAM,WAAW,UAAU,MAAM,GAAG,UAAU,SAAS,SAAS;AAChE,OAAK,MAAM,QAAQ,SACjB,OAAM,GAAG,KAAK,YAAY,KAAK,EAAE,EAAE,OAAO,MAAM,CAAC,CAAC,YAAY,KAAA,EAAU;AAE1E,MAAI,MAAM;GAAE;GAAQ,SAAS,SAAS;GAAQ,EAAE,uBAAuB;;CAGzE,MAAM,mBAAmB,QAA+B;AAEtD,QAAM,GADa,sBAAsB,OACtB,EAAE;GAAE,WAAW;GAAM,OAAO;GAAM,CAAC,CAAC,YAAY,KAAA,EAAU;;CAG/E,MAAM,QAAuB;AAC3B,MAAI,CAAC,KAAK,SAAS,CAAC,KAAK,WAAY;AACrC,MAAI,KAAK,aAAa;AACpB,gBAAa,KAAK,YAAY;AAC9B,QAAK,cAAc;;AAErB,QAAM,KAAK,WAAW,KAAK,WAAW;AACtC,OAAK,QAAQ;;CAGf,MAAc,YAAqC;AACjD,MAAI,KAAK,WAAY,QAAO,KAAK;EACjC,MAAM,YAAY,uBAAuB;AACzC,MAAI;GACF,MAAM,UAAU,MAAM,SAAS,WAAW,QAAQ;GAClD,MAAM,OAAO,KAAK,MAAM,QAAQ;AAChC,OAAI,CAAC,KAAK,SAAS,CAAC,MAAM,QAAQ,KAAK,MAAM,EAAE;AAC7C,QAAI,KAAK,iCAAiC;AAC1C,SAAK,aAAa;AAClB,WAAO,KAAK;;AAEd,QAAK,aAAa;AAClB,UAAO;UACD;AACN,QAAK,aAAa;AAClB,UAAO,KAAK;;;CAIhB,MAAc,WAAW,MAAqC;AAE5D,QAAM,gBADY,uBACa,EAAE,KAAK,UAAU,MAAM,MAAM,EAAE,CAAC;AAC/D,MAAI,MAAM,EAAE,OAAO,KAAK,MAAM,QAAQ,EAAE,oBAAoB;;CAG9D,MAAc,cAAc,MAA2B;AAErD,QAAM,gBADW,oBAAoB,KAAK,GACZ,EAAE,KAAK,UAAU,MAAM,MAAM,EAAE,CAAC;;CAGhE,kBAA0B,MAA4B;AACpD,OAAK,aAAa;AAClB,OAAK,QAAQ;AACb,MAAI,KAAK,YACP,cAAa,KAAK,YAAY;AAEhC,OAAK,cAAc,iBAAiB;AAClC,QAAK,OAAO,CAAC,OAAO,QAAQ;AAC1B,QAAI,MAAM,EAAE,KAAK,EAAE,8BAA8B;KACjD;KACD,YAAY;;CAGjB,MAAc,wBAAuC;EACnD,MAAM,WAAW,KAAK,iBAAiB,EAAE,QAAQ;EACjD,IAAI;AACJ,MAAI;AACF,WAAQ,MAAM,QAAQ,SAAS;UACzB;AACN,QAAK,aAAa;AAClB,SAAM,KAAK,WAAW,cAAc;AACpC;;EAGF,MAAM,UAA4B,EAAE;AACpC,OAAK,MAAM,QAAQ,OAAO;AACxB,OAAI,CAAC,KAAK,SAAS,QAAQ,CAAE;GAC7B,MAAM,SAAS,KAAK,MAAM,GAAG,GAAgB;GAC7C,MAAM,OAAO,MAAM,KAAK,QAAQ,OAAO;AACvC,OAAI,KACF,SAAQ,KAAK,iBAAiB,KAAK,CAAC;;AAIxC,UAAQ,MAAM,GAAG,MAAM,EAAE,YAAY,EAAE,UAAU;EACjD,MAAM,QAAwB;GAAE,SAAS;GAAe,OAAO;GAAS;AACxE,OAAK,aAAa;AAClB,QAAM,KAAK,WAAW,MAAM;AAC5B,MAAI,MAAM,EAAE,OAAO,QAAQ,QAAQ,EAAE,sBAAsB"}
@@ -71,12 +71,43 @@ export interface NoteAiMeta {
71
71
  }>;
72
72
  suggestedTags?: string[];
73
73
  }
74
+ export interface NoteCatalysisAction {
75
+ text: string;
76
+ kind: 'task' | 'workflow' | 'research' | 'share' | 'chat';
77
+ }
78
+ export interface NoteCatalysisReport {
79
+ originalNoteId: string;
80
+ generatedAt: number;
81
+ title: string;
82
+ valueHypothesis: string;
83
+ targetUsers: string[];
84
+ keyQuestions: string[];
85
+ mvpPath: string[];
86
+ risks: string[];
87
+ nextActions: NoteCatalysisAction[];
88
+ confidence: number;
89
+ }
90
+ export interface NoteCatalysisMeta {
91
+ status: 'none' | 'queued' | 'catalyzed' | 'snoozed' | 'dismissed';
92
+ stage?: 'seed' | 'incubating' | 'developing' | 'validating' | 'shipped';
93
+ lastCatalyzedAt?: number;
94
+ nextCatalyzeAt?: number;
95
+ feedback?: 'helpful' | 'not_helpful' | 'neutral';
96
+ confidence?: number;
97
+ report?: NoteCatalysisReport;
98
+ reportNoteId?: string;
99
+ sourceSessionKey?: string;
100
+ linkedSessionKeys?: string[];
101
+ linkedWorkflowRunIds?: string[];
102
+ linkedShareIds?: string[];
103
+ }
74
104
  export interface NoteAiDeepMeta {
75
105
  processedAt: number;
76
106
  priority?: 'high' | 'medium' | 'low';
77
107
  relatedNoteIds?: string[];
78
108
  relatedGoalId?: string;
79
109
  insights?: string;
110
+ catalysis?: NoteCatalysisMeta;
80
111
  }
81
112
  export interface CaptureSource {
82
113
  channel: CaptureChannel;
@@ -49,10 +49,13 @@ var SessionConfigStore = class {
49
49
  const content = await readFile(configPath, "utf-8");
50
50
  return JSON.parse(content);
51
51
  } catch (error) {
52
+ const em = error instanceof Error ? error.message : String(error);
52
53
  log.error({
54
+ err: error,
55
+ errorMessage: em,
53
56
  sessionKey,
54
- error
55
- }, "Failed to read session config");
57
+ phase: "session.config"
58
+ }, `Failed to read session config: ${em}`);
56
59
  return null;
57
60
  }
58
61
  }
@@ -69,10 +72,13 @@ var SessionConfigStore = class {
69
72
  await writeTextAtomic(configPath, JSON.stringify(configWithTimestamp, null, 2));
70
73
  log.debug({ sessionKey }, "Session config saved");
71
74
  } catch (error) {
75
+ const em = error instanceof Error ? error.message : String(error);
72
76
  log.error({
77
+ err: error,
78
+ errorMessage: em,
73
79
  sessionKey,
74
- error
75
- }, "Failed to save session config");
80
+ phase: "session.config"
81
+ }, `Failed to save session config: ${em}`);
76
82
  throw error;
77
83
  }
78
84
  }
@@ -1 +1 @@
1
- {"version":3,"file":"config-store.js","names":[],"sources":["../../../src/session/config-store.ts"],"sourcesContent":["/**\n * Session Config Store\n * \n * Manages session-level configuration persistence.\n * Stores thinking level, reasoning visibility, verbose mode, and other\n * session-specific settings that can be overridden via commands.\n */\n\nimport { readFile, mkdir } from 'fs/promises';\nimport { writeTextAtomic } from '../infra/write-file-atomic.js';\nimport { join } from 'path';\nimport { existsSync } from 'fs';\nimport { createLogger } from '../utils/logger.js';\nimport type { ThinkLevel, ReasoningLevel, VerboseLevel, ElevatedMode } from '../agent/transcript/thinking-types.js';\n\nconst log = createLogger('SessionConfigStore');\n\n/**\n * Session-level agent configuration.\n * These settings override agent defaults for a specific session.\n */\nexport interface SessionAgentConfig {\n /** Thinking level for this session */\n thinkingLevel?: ThinkLevel;\n /** Reasoning visibility for this session */\n reasoningLevel?: ReasoningLevel;\n /** Verbose level for this session */\n verboseLevel?: VerboseLevel;\n /** Elevated mode for this session */\n elevatedMode?: ElevatedMode;\n /** Model override for this session */\n modelOverride?: string;\n /** Provider override for this session */\n providerOverride?: string;\n /** Absolute markdown workspace root for this session (set once; immutable after save). */\n workingDirectoryOverride?: string;\n /** Last updated timestamp */\n updatedAt?: number;\n}\n\n/**\n * Session config store manager.\n * Each session can have its own configuration that overrides agent defaults.\n */\nexport class SessionConfigStore {\n private configDir: string;\n\n /** @param agentHomeDir — `resolveAgentHomeDir(…)` (parent of `sessions/` transcript store) */\n constructor(agentHomeDir: string) {\n this.configDir = join(agentHomeDir, 'sessions', 'config');\n }\n\n /**\n * Initialize the config store\n */\n async initialize(): Promise<void> {\n await mkdir(this.configDir, { recursive: true });\n log.debug('Session config store initialized');\n }\n\n /**\n * Get the config file path for a session\n */\n private getConfigPath(sessionKey: string): string {\n // Sanitize session key to be a valid filename\n const safeKey = sessionKey.replace(/[^a-zA-Z0-9_-]/g, '_');\n return join(this.configDir, `${safeKey}.json`);\n }\n\n /**\n * Get config for a session\n */\n async get(sessionKey: string): Promise<SessionAgentConfig | null> {\n const configPath = this.getConfigPath(sessionKey);\n \n if (!existsSync(configPath)) {\n return null;\n }\n\n try {\n const content = await readFile(configPath, 'utf-8');\n const config = JSON.parse(content) as SessionAgentConfig;\n return config;\n } catch (error) {\n log.error({ sessionKey, error }, 'Failed to read session config');\n return null;\n }\n }\n\n /**\n * Set config for a session (full replacement)\n */\n async set(sessionKey: string, config: SessionAgentConfig): Promise<void> {\n const configPath = this.getConfigPath(sessionKey);\n const configWithTimestamp = {\n ...config,\n updatedAt: Date.now(),\n };\n\n try {\n await writeTextAtomic(configPath, JSON.stringify(configWithTimestamp, null, 2));\n log.debug({ sessionKey }, 'Session config saved');\n } catch (error) {\n log.error({ sessionKey, error }, 'Failed to save session config');\n throw error;\n }\n }\n\n /**\n * Update config for a session (partial update)\n */\n async update(sessionKey: string, partial: Partial<SessionAgentConfig>): Promise<SessionAgentConfig> {\n const existing = await this.get(sessionKey);\n const updated = {\n ...existing,\n ...partial,\n updatedAt: Date.now(),\n };\n \n await this.set(sessionKey, updated);\n return updated;\n }\n\n /**\n * Delete config for a session\n */\n async delete(sessionKey: string): Promise<void> {\n const configPath = this.getConfigPath(sessionKey);\n \n if (existsSync(configPath)) {\n try {\n const { unlink } = await import('fs/promises');\n await unlink(configPath);\n log.debug({ sessionKey }, 'Session config deleted');\n } catch (error) {\n log.error({ sessionKey, error }, 'Failed to delete session config');\n throw error;\n }\n }\n }\n\n /**\n * Check if config exists for a session\n */\n async has(sessionKey: string): Promise<boolean> {\n const configPath = this.getConfigPath(sessionKey);\n return existsSync(configPath);\n }\n\n /**\n * Get all session configs\n */\n async getAll(): Promise<Map<string, SessionAgentConfig>> {\n const { readdir } = await import('fs/promises');\n const configs = new Map<string, SessionAgentConfig>();\n\n try {\n const files = await readdir(this.configDir);\n \n for (const file of files) {\n if (!file.endsWith('.json')) continue;\n \n const sessionKey = file.replace('.json', '').replace(/_/g, '-');\n const config = await this.get(sessionKey);\n \n if (config) {\n configs.set(sessionKey, config);\n }\n }\n } catch (error) {\n log.error({ error }, 'Failed to list session configs');\n }\n\n return configs;\n }\n\n /**\n * Clear all session configs\n */\n async clear(): Promise<void> {\n const { readdir, rm } = await import('fs/promises');\n \n try {\n const files = await readdir(this.configDir);\n \n for (const file of files) {\n if (file.endsWith('.json')) {\n await rm(join(this.configDir, file), { force: true });\n }\n }\n \n log.debug('All session configs cleared');\n } catch (error) {\n log.error({ error }, 'Failed to clear session configs');\n throw error;\n }\n }\n}\n\n// ========== Helper Functions ==========\n\n/**\n * Resolve thinking level for a session.\n * Returns session config if set, otherwise falls back to agent defaults.\n */\nexport async function resolveThinkingLevel(\n sessionConfigStore: SessionConfigStore,\n sessionKey: string,\n agentDefault?: ThinkLevel\n): Promise<ThinkLevel | undefined> {\n const config = await sessionConfigStore.get(sessionKey);\n \n if (config?.thinkingLevel) {\n return config.thinkingLevel;\n }\n \n return agentDefault;\n}\n\n/**\n * Resolve reasoning level for a session.\n */\nexport async function resolveReasoningLevel(\n sessionConfigStore: SessionConfigStore,\n sessionKey: string,\n agentDefault?: ReasoningLevel\n): Promise<ReasoningLevel | undefined> {\n const config = await sessionConfigStore.get(sessionKey);\n \n if (config?.reasoningLevel) {\n return config.reasoningLevel;\n }\n \n return agentDefault;\n}\n\n/**\n * Resolve verbose level for a session.\n */\nexport async function resolveVerboseLevel(\n sessionConfigStore: SessionConfigStore,\n sessionKey: string,\n agentDefault?: VerboseLevel\n): Promise<VerboseLevel | undefined> {\n const config = await sessionConfigStore.get(sessionKey);\n \n if (config?.verboseLevel) {\n return config.verboseLevel;\n }\n \n return agentDefault;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"config-store.js","names":[],"sources":["../../../src/session/config-store.ts"],"sourcesContent":["/**\n * Session Config Store\n * \n * Manages session-level configuration persistence.\n * Stores thinking level, reasoning visibility, verbose mode, and other\n * session-specific settings that can be overridden via commands.\n */\n\nimport { readFile, mkdir } from 'fs/promises';\nimport { writeTextAtomic } from '../infra/write-file-atomic.js';\nimport { join } from 'path';\nimport { existsSync } from 'fs';\nimport { createLogger } from '../utils/logger.js';\nimport type { ThinkLevel, ReasoningLevel, VerboseLevel, ElevatedMode } from '../agent/transcript/thinking-types.js';\n\nconst log = createLogger('SessionConfigStore');\n\n/**\n * Session-level agent configuration.\n * These settings override agent defaults for a specific session.\n */\nexport interface SessionAgentConfig {\n /** Thinking level for this session */\n thinkingLevel?: ThinkLevel;\n /** Reasoning visibility for this session */\n reasoningLevel?: ReasoningLevel;\n /** Verbose level for this session */\n verboseLevel?: VerboseLevel;\n /** Elevated mode for this session */\n elevatedMode?: ElevatedMode;\n /** Model override for this session */\n modelOverride?: string;\n /** Provider override for this session */\n providerOverride?: string;\n /** Absolute markdown workspace root for this session (set once; immutable after save). */\n workingDirectoryOverride?: string;\n /** Last updated timestamp */\n updatedAt?: number;\n}\n\n/**\n * Session config store manager.\n * Each session can have its own configuration that overrides agent defaults.\n */\nexport class SessionConfigStore {\n private configDir: string;\n\n /** @param agentHomeDir — `resolveAgentHomeDir(…)` (parent of `sessions/` transcript store) */\n constructor(agentHomeDir: string) {\n this.configDir = join(agentHomeDir, 'sessions', 'config');\n }\n\n /**\n * Initialize the config store\n */\n async initialize(): Promise<void> {\n await mkdir(this.configDir, { recursive: true });\n log.debug('Session config store initialized');\n }\n\n /**\n * Get the config file path for a session\n */\n private getConfigPath(sessionKey: string): string {\n // Sanitize session key to be a valid filename\n const safeKey = sessionKey.replace(/[^a-zA-Z0-9_-]/g, '_');\n return join(this.configDir, `${safeKey}.json`);\n }\n\n /**\n * Get config for a session\n */\n async get(sessionKey: string): Promise<SessionAgentConfig | null> {\n const configPath = this.getConfigPath(sessionKey);\n \n if (!existsSync(configPath)) {\n return null;\n }\n\n try {\n const content = await readFile(configPath, 'utf-8');\n const config = JSON.parse(content) as SessionAgentConfig;\n return config;\n } catch (error) {\n const em = error instanceof Error ? error.message : String(error);\n log.error({ err: error, errorMessage: em, sessionKey, phase: 'session.config' }, `Failed to read session config: ${em}`);\n return null;\n }\n }\n\n /**\n * Set config for a session (full replacement)\n */\n async set(sessionKey: string, config: SessionAgentConfig): Promise<void> {\n const configPath = this.getConfigPath(sessionKey);\n const configWithTimestamp = {\n ...config,\n updatedAt: Date.now(),\n };\n\n try {\n await writeTextAtomic(configPath, JSON.stringify(configWithTimestamp, null, 2));\n log.debug({ sessionKey }, 'Session config saved');\n } catch (error) {\n const em = error instanceof Error ? error.message : String(error);\n log.error({ err: error, errorMessage: em, sessionKey, phase: 'session.config' }, `Failed to save session config: ${em}`);\n throw error;\n }\n }\n\n /**\n * Update config for a session (partial update)\n */\n async update(sessionKey: string, partial: Partial<SessionAgentConfig>): Promise<SessionAgentConfig> {\n const existing = await this.get(sessionKey);\n const updated = {\n ...existing,\n ...partial,\n updatedAt: Date.now(),\n };\n \n await this.set(sessionKey, updated);\n return updated;\n }\n\n /**\n * Delete config for a session\n */\n async delete(sessionKey: string): Promise<void> {\n const configPath = this.getConfigPath(sessionKey);\n \n if (existsSync(configPath)) {\n try {\n const { unlink } = await import('fs/promises');\n await unlink(configPath);\n log.debug({ sessionKey }, 'Session config deleted');\n } catch (error) {\n log.error({ sessionKey, error }, 'Failed to delete session config');\n throw error;\n }\n }\n }\n\n /**\n * Check if config exists for a session\n */\n async has(sessionKey: string): Promise<boolean> {\n const configPath = this.getConfigPath(sessionKey);\n return existsSync(configPath);\n }\n\n /**\n * Get all session configs\n */\n async getAll(): Promise<Map<string, SessionAgentConfig>> {\n const { readdir } = await import('fs/promises');\n const configs = new Map<string, SessionAgentConfig>();\n\n try {\n const files = await readdir(this.configDir);\n \n for (const file of files) {\n if (!file.endsWith('.json')) continue;\n \n const sessionKey = file.replace('.json', '').replace(/_/g, '-');\n const config = await this.get(sessionKey);\n \n if (config) {\n configs.set(sessionKey, config);\n }\n }\n } catch (error) {\n log.error({ error }, 'Failed to list session configs');\n }\n\n return configs;\n }\n\n /**\n * Clear all session configs\n */\n async clear(): Promise<void> {\n const { readdir, rm } = await import('fs/promises');\n \n try {\n const files = await readdir(this.configDir);\n \n for (const file of files) {\n if (file.endsWith('.json')) {\n await rm(join(this.configDir, file), { force: true });\n }\n }\n \n log.debug('All session configs cleared');\n } catch (error) {\n log.error({ error }, 'Failed to clear session configs');\n throw error;\n }\n }\n}\n\n// ========== Helper Functions ==========\n\n/**\n * Resolve thinking level for a session.\n * Returns session config if set, otherwise falls back to agent defaults.\n */\nexport async function resolveThinkingLevel(\n sessionConfigStore: SessionConfigStore,\n sessionKey: string,\n agentDefault?: ThinkLevel\n): Promise<ThinkLevel | undefined> {\n const config = await sessionConfigStore.get(sessionKey);\n \n if (config?.thinkingLevel) {\n return config.thinkingLevel;\n }\n \n return agentDefault;\n}\n\n/**\n * Resolve reasoning level for a session.\n */\nexport async function resolveReasoningLevel(\n sessionConfigStore: SessionConfigStore,\n sessionKey: string,\n agentDefault?: ReasoningLevel\n): Promise<ReasoningLevel | undefined> {\n const config = await sessionConfigStore.get(sessionKey);\n \n if (config?.reasoningLevel) {\n return config.reasoningLevel;\n }\n \n return agentDefault;\n}\n\n/**\n * Resolve verbose level for a session.\n */\nexport async function resolveVerboseLevel(\n sessionConfigStore: SessionConfigStore,\n sessionKey: string,\n agentDefault?: VerboseLevel\n): Promise<VerboseLevel | undefined> {\n const config = await sessionConfigStore.get(sessionKey);\n \n if (config?.verboseLevel) {\n return config.verboseLevel;\n }\n \n return agentDefault;\n}\n"],"mappings":"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"}
@@ -19,7 +19,7 @@ export { initSessionTurn, type InitSessionTurnOptions, type InitSessionTurnResul
19
19
  export { DEFAULT_RESET_TRIGGERS, RESET_OVERLAP_COMMANDS, bareResetAckMessage, matchResetTriggers, resolveResetTriggers, shouldSkipResetOverlapCommand, stripLeadingEnvelopeTimestamp, type ResetTriggerMatch, } from './reset-triggers.js';
20
20
  export type { CompactionConfig, CompactionResult } from '../agent/memory/compaction.js';
21
21
  export type { WindowConfig } from '../agent/memory/window.js';
22
- export { maybeAutoTitleSessionStore, generateSessionTitleFromMessages, sanitizeSessionTitle, fallbackTitleFromMessages, isWebchatSessionKey, shouldAutoTitleSessionKey, } from './session-title.js';
22
+ export { maybeSetProvisionalSessionTitle, maybeRefineSessionTitleWithLlm, generateSessionTitleFromMessages, sanitizeSessionTitle, provisionalTitleFromUserText, fallbackTitleFromMessages, getSessionTitleSource, shouldRefineSessionTitleWithLlm, isWebchatSessionKey, shouldAutoTitleSessionKey, type SessionTitleSource, } from './session-title.js';
23
23
  export { messagesToClientHistory, flattenMessageContent, type ClientHistoryMessage } from './client-history.js';
24
24
  export { XOPC_SESSION_TRANSCRIPT_TYPE, CURRENT_SESSION_TRANSCRIPT_VERSION, } from './transcript-format.js';
25
25
  export { buildSessionContextForLlm, isTranscriptContextEntry, mergeLlmMessagesPreservingContextRows, transcriptRowsFromJsonArray, type TranscriptStoredRow, type XopcTranscriptContextEntry, } from './session-context-for-llm.js';
@@ -2,7 +2,7 @@ import { emitSessionTranscriptUpdate, onSessionTranscriptUpdate } from "./transc
2
2
  import { buildSessionContextForLlm, isTranscriptContextEntry, mergeLlmMessagesPreservingContextRows, transcriptRowsFromJsonArray } from "./session-context-for-llm.js";
3
3
  import { SessionSearchIndex } from "./search-index.js";
4
4
  import { getOrLoadSessionSearchIndex, invalidateSessionSearchIndexCache } from "./search-index-cache.js";
5
- import { fallbackTitleFromMessages, generateSessionTitleFromMessages, isWebchatSessionKey, maybeAutoTitleSessionStore, sanitizeSessionTitle, shouldAutoTitleSessionKey } from "./session-title.js";
5
+ import { fallbackTitleFromMessages, generateSessionTitleFromMessages, getSessionTitleSource, isWebchatSessionKey, maybeRefineSessionTitleWithLlm, maybeSetProvisionalSessionTitle, provisionalTitleFromUserText, sanitizeSessionTitle, shouldAutoTitleSessionKey, shouldRefineSessionTitleWithLlm } from "./session-title.js";
6
6
  import { normalizeCompactionCheckpointId } from "./compaction-checkpoints.js";
7
7
  import { SessionStatus } from "./types.js";
8
8
  import { SessionStore } from "./store.js";
@@ -20,4 +20,4 @@ import { DEFAULT_RESET_TRIGGERS, RESET_OVERLAP_COMMANDS, bareResetAckMessage, ma
20
20
  import { initSessionTurn } from "./init-session-turn.js";
21
21
  import { flattenMessageContent, messagesToClientHistory } from "./client-history.js";
22
22
  import { CURRENT_SESSION_TRANSCRIPT_VERSION, XOPC_SESSION_TRANSCRIPT_TYPE } from "./transcript-format.js";
23
- export { CURRENT_SESSION_TRANSCRIPT_VERSION, DEFAULT_IDLE_MINUTES, DEFAULT_RESET_AT_HOUR, DEFAULT_RESET_MODE, DEFAULT_RESET_TRIGGERS, RESET_OVERLAP_COMMANDS, SessionConfigStore, SessionIndex, SessionSearchIndex, SessionStatus, SessionStore, XOPC_SESSION_TRANSCRIPT_TYPE, applySessionPatchToMetadata, bareResetAckMessage, buildSessionContextForLlm, effectiveWorkspacePathForSession, emitSessionTranscriptUpdate, evaluateSessionFreshness, fallbackTitleFromMessages, flattenMessageContent, generateSessionTitleFromMessages, getOrLoadSessionSearchIndex, initSessionTurn, invalidateSessionSearchIndexCache, isThreadSessionKey, isTranscriptContextEntry, isWebchatSessionKey, matchResetTriggers, maybeAutoTitleSessionStore, mergeLlmMessagesPreservingContextRows, messagesToClientHistory, normalizeCompactionCheckpointId, normalizeWorkingDirectoryInput, onSessionTranscriptUpdate, resolveChannelResetConfig, resolveDailyResetAtMs, resolveEffectiveReasoningLevel, resolveEffectiveThinkingLevel, resolveReasoningLevel, resolveResetTriggers, resolveSession, resolveSessionKeyForRequest, resolveSessionLifecycleTimestamps, resolveSessionResetPolicy, resolveSessionResetType, resolveThinkingLevel, resolveVerboseLevel, sanitizeSessionTitle, shouldAutoTitleSessionKey, shouldSkipResetOverlapCommand, shouldSkipWebchatInboundByAbortCutoff, stripLeadingEnvelopeTimestamp, transcriptRowsFromJsonArray };
23
+ export { CURRENT_SESSION_TRANSCRIPT_VERSION, DEFAULT_IDLE_MINUTES, DEFAULT_RESET_AT_HOUR, DEFAULT_RESET_MODE, DEFAULT_RESET_TRIGGERS, RESET_OVERLAP_COMMANDS, SessionConfigStore, SessionIndex, SessionSearchIndex, SessionStatus, SessionStore, XOPC_SESSION_TRANSCRIPT_TYPE, applySessionPatchToMetadata, bareResetAckMessage, buildSessionContextForLlm, effectiveWorkspacePathForSession, emitSessionTranscriptUpdate, evaluateSessionFreshness, fallbackTitleFromMessages, flattenMessageContent, generateSessionTitleFromMessages, getOrLoadSessionSearchIndex, getSessionTitleSource, initSessionTurn, invalidateSessionSearchIndexCache, isThreadSessionKey, isTranscriptContextEntry, isWebchatSessionKey, matchResetTriggers, maybeRefineSessionTitleWithLlm, maybeSetProvisionalSessionTitle, mergeLlmMessagesPreservingContextRows, messagesToClientHistory, normalizeCompactionCheckpointId, normalizeWorkingDirectoryInput, onSessionTranscriptUpdate, provisionalTitleFromUserText, resolveChannelResetConfig, resolveDailyResetAtMs, resolveEffectiveReasoningLevel, resolveEffectiveThinkingLevel, resolveReasoningLevel, resolveResetTriggers, resolveSession, resolveSessionKeyForRequest, resolveSessionLifecycleTimestamps, resolveSessionResetPolicy, resolveSessionResetType, resolveThinkingLevel, resolveVerboseLevel, sanitizeSessionTitle, shouldAutoTitleSessionKey, shouldRefineSessionTitleWithLlm, shouldSkipResetOverlapCommand, shouldSkipWebchatInboundByAbortCutoff, stripLeadingEnvelopeTimestamp, transcriptRowsFromJsonArray };
@@ -84,7 +84,14 @@ var SessionIndex = class extends EventEmitter$1 {
84
84
  return result;
85
85
  }
86
86
  async renameSession(key, name) {
87
- await this.store.updateMetadata(key, { name });
87
+ const existing = await this.store.getMetadata(key);
88
+ await this.store.updateMetadata(key, {
89
+ name,
90
+ customData: {
91
+ ...existing?.customData ?? {},
92
+ titleSource: "user"
93
+ }
94
+ });
88
95
  this.emit("sessionUpdated", {
89
96
  key,
90
97
  name
@@ -1 +1 @@
1
- {"version":3,"file":"manager.js","names":["EventEmitter"],"sources":["../../../src/session/manager.ts"],"sourcesContent":["// Session manager - high-level session management service\n\nimport EventEmitter from 'events';\nimport { createLogger } from '../utils/logger.js';\nimport { SessionStore } from './store.js';\nimport type {\n SessionMetadata,\n SessionDetail,\n SessionListQuery,\n PaginatedResult,\n GlobalSessionStats,\n ExportFormat,\n SessionStatus,\n} from './types.js';\nimport type { Message } from './types.js';\nimport type { CompactionConfig, CompactionResult } from '../agent/memory/compaction.js';\nimport type { XopcSessionTranscriptV1 } from './transcript-format.js';\nimport type { XopcTranscriptContextEntry } from './session-context-for-llm.js';\nimport { applySessionPatchToMetadata, type SessionPatchBody } from './patch-metadata.js';\nimport type { WindowConfig } from '../agent/memory/window.js';\nimport type { Config } from '../config/schema.js';\n\nconst log = createLogger('SessionIndex');\n\nexport interface SessionIndexConfig {\n config: Config;\n agentId?: string;\n sessionsDir?: string;\n windowConfig?: Partial<WindowConfig>;\n compactionConfig?: Partial<CompactionConfig>;\n}\n\nexport class SessionIndex extends EventEmitter {\n private store: SessionStore;\n\n constructor(config: SessionIndexConfig) {\n super();\n this.store = new SessionStore(\n {\n config: config.config,\n agentId: config.agentId,\n sessionsDir: config.sessionsDir,\n },\n config.windowConfig,\n config.compactionConfig\n );\n }\n\n async initialize(): Promise<void> {\n await this.store.initialize();\n this.emit('ready');\n }\n\n /** Low-level store (e.g. cron resolving weixin delivery from session index). */\n getStore(): SessionStore {\n return this.store;\n }\n\n // ========== CRUD Operations ==========\n\n async listSessions(query?: SessionListQuery): Promise<PaginatedResult<SessionMetadata>> {\n return this.store.list(query);\n }\n\n /**\n * List all subagent sessions.\n * Subagent sessions have keys starting with 'subagent:'.\n */\n async listSubagents(query: SessionListQuery = {}): Promise<PaginatedResult<SessionMetadata>> {\n // Filter for subagent sessions only\n const subagentQuery: SessionListQuery = {\n ...query,\n search: query.search ? `subagent:${query.search}` : 'subagent:',\n };\n \n const result = await this.store.list(subagentQuery);\n \n // Additional filtering to ensure only subagent sessions\n const subagentSessions = result.items.filter((s) => s.key.startsWith('subagent:'));\n \n return {\n ...result,\n items: subagentSessions,\n total: subagentSessions.length,\n hasMore: false, // Simplified for now\n };\n }\n\n async getSession(\n key: string,\n options?: { includeTranscriptSummary?: boolean; includeTranscriptRows?: boolean },\n ): Promise<SessionDetail | null> {\n const session = await this.store.get(key, options);\n if (session) {\n this.emit('sessionAccessed', { key });\n }\n return session;\n }\n\n async getSessionMessagePage(\n key: string,\n options?: {\n offset?: number;\n limit?: number;\n before?: string;\n includeTranscriptSummary?: boolean;\n includeTranscriptRows?: boolean;\n },\n ): Promise<{\n session: SessionDetail;\n pagination: {\n total: number;\n limit: number;\n offset: number;\n hasMore: boolean;\n before?: string;\n nextBeforeCursor?: string;\n };\n } | null> {\n const result = await this.store.getMessagePage(key, options);\n if (result) {\n this.emit('sessionAccessed', { key });\n }\n return result;\n }\n\n /**\n * OpenClaw-style `sessions.patch`: partial metadata (name, tags, customData shallow merge).\n */\n async patchSession(\n key: string,\n patch: SessionPatchBody,\n ): Promise<{ ok: true } | { ok: false; error: string }> {\n const meta = await this.store.getMetadata(key);\n if (!meta) {\n return { ok: false, error: 'Session not found' };\n }\n const updates = applySessionPatchToMetadata(meta, patch);\n if (Object.keys(updates).length === 0) {\n return { ok: true };\n }\n await this.store.updateMetadata(key, updates);\n this.emit('sessionUpdated', { key });\n return { ok: true };\n }\n\n async getSessionMetadata(key: string): Promise<SessionMetadata | null> {\n return this.store.getMetadata(key);\n }\n\n async deleteSession(key: string): Promise<boolean> {\n const result = await this.store.delete(key);\n if (result) {\n this.emit('sessionDeleted', { key });\n }\n return result;\n }\n\n async deleteSessions(keys: string[]): Promise<{ success: string[]; failed: string[] }> {\n const result = await this.store.deleteMany(keys);\n for (const key of result.success) {\n this.emit('sessionDeleted', { key });\n }\n return result;\n }\n\n // ========== Metadata Updates ==========\n\n async renameSession(key: string, name: string): Promise<void> {\n await this.store.updateMetadata(key, { name });\n this.emit('sessionUpdated', { key, name });\n }\n\n /** Partial metadata update (caller merges nested fields like `customData` when needed). */\n async updateSessionMetadata(key: string, updates: Partial<SessionMetadata>): Promise<void> {\n await this.store.updateMetadata(key, updates);\n this.emit('sessionUpdated', { key });\n }\n\n async tagSession(key: string, tags: string[]): Promise<void> {\n const existing = await this.store.getMetadata(key);\n if (!existing) {\n throw new Error(`Session not found: ${key}`);\n }\n\n // Merge tags, remove duplicates\n const mergedTags = [...new Set([...existing.tags, ...tags])];\n await this.store.updateMetadata(key, { tags: mergedTags });\n this.emit('sessionUpdated', { key, tags: mergedTags });\n }\n\n async untagSession(key: string, tags: string[]): Promise<void> {\n const existing = await this.store.getMetadata(key);\n if (!existing) {\n throw new Error(`Session not found: ${key}`);\n }\n\n const filteredTags = existing.tags.filter((t) => !tags.includes(t));\n await this.store.updateMetadata(key, { tags: filteredTags });\n this.emit('sessionUpdated', { key, tags: filteredTags });\n }\n\n async setSessionTags(key: string, tags: string[]): Promise<void> {\n await this.store.updateMetadata(key, { tags: [...new Set(tags)] });\n this.emit('sessionUpdated', { key, tags });\n }\n\n // ========== Status Management ==========\n\n async archiveSession(key: string): Promise<void> {\n await this.store.archive(key);\n this.emit('sessionArchived', { key });\n }\n\n async unarchiveSession(key: string): Promise<void> {\n await this.store.unarchive(key);\n this.emit('sessionRestored', { key });\n }\n\n async pinSession(key: string): Promise<void> {\n await this.store.pin(key);\n this.emit('sessionPinned', { key });\n }\n\n async unpinSession(key: string): Promise<void> {\n await this.store.unpin(key);\n this.emit('sessionUnpinned', { key });\n }\n\n async setSessionStatus(key: string, status: SessionStatus): Promise<void> {\n await this.store.setStatus(key, status);\n this.emit('sessionStatusChanged', { key, status });\n }\n\n // ========== Search ==========\n\n async searchSessions(query: string): Promise<SessionMetadata[]> {\n const result = await this.store.list({ search: query, limit: 100 });\n return result.items;\n }\n\n async searchInSession(key: string, keyword: string): Promise<Message[]> {\n return this.store.searchInSession(key, keyword);\n }\n\n // ========== Export/Import ==========\n\n async exportSession(key: string, format: ExportFormat): Promise<string> {\n return this.store.exportSession(key, format);\n }\n\n // ========== Statistics ==========\n\n async getStats(): Promise<GlobalSessionStats> {\n return this.store.getStats();\n }\n\n // ========== Maintenance ==========\n\n async archiveOldSessions(olderThanDays: number): Promise<number> {\n const count = await this.store.archiveOld(olderThanDays);\n log.info({ count, olderThanDays }, 'Archived old sessions');\n return count;\n }\n\n // ========== Event Helpers ==========\n\n onSessionCreated(callback: (metadata: SessionMetadata) => void): void {\n this.on('sessionCreated', callback);\n }\n\n onSessionUpdated(callback: (data: { key: string; name?: string; tags?: string[] }) => void): void {\n this.on('sessionUpdated', callback);\n }\n\n onSessionDeleted(callback: (data: { key: string }) => void): void {\n this.on('sessionDeleted', callback);\n }\n\n onSessionArchived(callback: (data: { key: string }) => void): void {\n this.on('sessionArchived', callback);\n }\n\n onSessionRestored(callback: (data: { key: string }) => void): void {\n this.on('sessionRestored', callback);\n }\n\n onSessionPinned(callback: (data: { key: string }) => void): void {\n this.on('sessionPinned', callback);\n }\n\n onSessionUnpinned(callback: (data: { key: string }) => void): void {\n this.on('sessionUnpinned', callback);\n }\n\n onSessionStatusChanged(callback: (data: { key: string; status: SessionStatus }) => void): void {\n this.on('sessionStatusChanged', callback);\n }\n\n onSessionAccessed(callback: (data: { key: string }) => void): void {\n this.on('sessionAccessed', callback);\n }\n\n // ========== Store delegation (messages, compaction) ==========\n\n /** Load messages for a session key */\n async loadMessages(key: string) {\n return this.store.loadMessages(key);\n }\n\n /** Wrapped transcript document (stable id, compaction history); null if missing or not a valid envelope. */\n async loadTranscriptDocument(key: string): Promise<XopcSessionTranscriptV1 | null> {\n return this.store.loadTranscriptDocument(key);\n }\n\n /**\n * Runtime turns must use PiTranscriptManager.appendMessage; this entry point\n * is reserved for compaction, tests, and admin tools.\n */\n async saveMessages(key: string, messages: any[]) {\n return this.store.saveMessages(key, messages);\n }\n\n /**\n * Append `kind: 'context'` transcript row (persisted, excluded from {@link loadMessages} / LLM).\n */\n async appendTranscriptContextEntry(\n key: string,\n entry: Omit<XopcTranscriptContextEntry, 'kind'> & Partial<Pick<XopcTranscriptContextEntry, 'kind'>>,\n ): Promise<void> {\n await this.store.appendTranscriptContextEntry(key, entry);\n this.emit('sessionUpdated', { key });\n }\n\n /** Delete session data */\n async delete(key: string): Promise<void> {\n await this.store.delete(key);\n }\n\n /** Archive transcript and start a new session id for the same key. */\n async resetSession(\n key: string,\n ): Promise<{ sessionId: string; previousSessionId: string } | null> {\n const result = await this.store.reset(key);\n if (result) {\n this.emit('sessionUpdated', { key });\n }\n return result;\n }\n\n /** Token/window stats for a message list */\n getWindowStats(messages: any[]) {\n return this.store.getWindowStats(messages);\n }\n\n /** Prepare compaction run */\n prepareCompaction(key: string, messages: any[], contextWindow: number) {\n return this.store.prepareCompaction(key, messages, contextWindow);\n }\n\n /** Compact session messages */\n compact(\n key: string,\n messages: any[],\n contextWindow: number,\n instructions?: string,\n force?: boolean,\n ): Promise<CompactionResult> {\n return this.store.compact(key, messages, contextWindow, instructions, force);\n }\n\n /** Compaction stats for a session */\n async getCompactionStats(key: string) {\n return this.store.getCompactionStats(key);\n }\n\n /** List pre-compaction transcript snapshots (newest first). */\n listCompactionCheckpoints(key: string) {\n return this.store.listCompactionCheckpoints(key);\n }\n\n getCompactionCheckpointDetail(key: string, checkpointId: string) {\n return this.store.getCompactionCheckpointDetail(key, checkpointId);\n }\n\n restoreCompactionCheckpoint(key: string, checkpointId: string) {\n return this.store.restoreCompactionCheckpoint(key, checkpointId);\n }\n\n /** Estimate token usage for messages */\n async estimateTokenUsage(key: string, messages: any[]): Promise<number> {\n return this.store.estimateTokens(messages);\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"manager.js","names":["EventEmitter"],"sources":["../../../src/session/manager.ts"],"sourcesContent":["// Session manager - high-level session management service\n\nimport EventEmitter from 'events';\nimport { createLogger } from '../utils/logger.js';\nimport { SessionStore } from './store.js';\nimport type {\n SessionMetadata,\n SessionDetail,\n SessionListQuery,\n PaginatedResult,\n GlobalSessionStats,\n ExportFormat,\n SessionStatus,\n} from './types.js';\nimport type { Message } from './types.js';\nimport type { CompactionConfig, CompactionResult } from '../agent/memory/compaction.js';\nimport type { XopcSessionTranscriptV1 } from './transcript-format.js';\nimport type { XopcTranscriptContextEntry } from './session-context-for-llm.js';\nimport { applySessionPatchToMetadata, type SessionPatchBody } from './patch-metadata.js';\nimport type { WindowConfig } from '../agent/memory/window.js';\nimport type { Config } from '../config/schema.js';\n\nconst log = createLogger('SessionIndex');\n\nexport interface SessionIndexConfig {\n config: Config;\n agentId?: string;\n sessionsDir?: string;\n windowConfig?: Partial<WindowConfig>;\n compactionConfig?: Partial<CompactionConfig>;\n}\n\nexport class SessionIndex extends EventEmitter {\n private store: SessionStore;\n\n constructor(config: SessionIndexConfig) {\n super();\n this.store = new SessionStore(\n {\n config: config.config,\n agentId: config.agentId,\n sessionsDir: config.sessionsDir,\n },\n config.windowConfig,\n config.compactionConfig\n );\n }\n\n async initialize(): Promise<void> {\n await this.store.initialize();\n this.emit('ready');\n }\n\n /** Low-level store (e.g. cron resolving weixin delivery from session index). */\n getStore(): SessionStore {\n return this.store;\n }\n\n // ========== CRUD Operations ==========\n\n async listSessions(query?: SessionListQuery): Promise<PaginatedResult<SessionMetadata>> {\n return this.store.list(query);\n }\n\n /**\n * List all subagent sessions.\n * Subagent sessions have keys starting with 'subagent:'.\n */\n async listSubagents(query: SessionListQuery = {}): Promise<PaginatedResult<SessionMetadata>> {\n // Filter for subagent sessions only\n const subagentQuery: SessionListQuery = {\n ...query,\n search: query.search ? `subagent:${query.search}` : 'subagent:',\n };\n \n const result = await this.store.list(subagentQuery);\n \n // Additional filtering to ensure only subagent sessions\n const subagentSessions = result.items.filter((s) => s.key.startsWith('subagent:'));\n \n return {\n ...result,\n items: subagentSessions,\n total: subagentSessions.length,\n hasMore: false, // Simplified for now\n };\n }\n\n async getSession(\n key: string,\n options?: { includeTranscriptSummary?: boolean; includeTranscriptRows?: boolean },\n ): Promise<SessionDetail | null> {\n const session = await this.store.get(key, options);\n if (session) {\n this.emit('sessionAccessed', { key });\n }\n return session;\n }\n\n async getSessionMessagePage(\n key: string,\n options?: {\n offset?: number;\n limit?: number;\n before?: string;\n includeTranscriptSummary?: boolean;\n includeTranscriptRows?: boolean;\n },\n ): Promise<{\n session: SessionDetail;\n pagination: {\n total: number;\n limit: number;\n offset: number;\n hasMore: boolean;\n before?: string;\n nextBeforeCursor?: string;\n };\n } | null> {\n const result = await this.store.getMessagePage(key, options);\n if (result) {\n this.emit('sessionAccessed', { key });\n }\n return result;\n }\n\n /**\n * OpenClaw-style `sessions.patch`: partial metadata (name, tags, customData shallow merge).\n */\n async patchSession(\n key: string,\n patch: SessionPatchBody,\n ): Promise<{ ok: true } | { ok: false; error: string }> {\n const meta = await this.store.getMetadata(key);\n if (!meta) {\n return { ok: false, error: 'Session not found' };\n }\n const updates = applySessionPatchToMetadata(meta, patch);\n if (Object.keys(updates).length === 0) {\n return { ok: true };\n }\n await this.store.updateMetadata(key, updates);\n this.emit('sessionUpdated', { key });\n return { ok: true };\n }\n\n async getSessionMetadata(key: string): Promise<SessionMetadata | null> {\n return this.store.getMetadata(key);\n }\n\n async deleteSession(key: string): Promise<boolean> {\n const result = await this.store.delete(key);\n if (result) {\n this.emit('sessionDeleted', { key });\n }\n return result;\n }\n\n async deleteSessions(keys: string[]): Promise<{ success: string[]; failed: string[] }> {\n const result = await this.store.deleteMany(keys);\n for (const key of result.success) {\n this.emit('sessionDeleted', { key });\n }\n return result;\n }\n\n // ========== Metadata Updates ==========\n\n async renameSession(key: string, name: string): Promise<void> {\n const existing = await this.store.getMetadata(key);\n await this.store.updateMetadata(key, {\n name,\n customData: { ...(existing?.customData ?? {}), titleSource: 'user' },\n });\n this.emit('sessionUpdated', { key, name });\n }\n\n /** Partial metadata update (caller merges nested fields like `customData` when needed). */\n async updateSessionMetadata(key: string, updates: Partial<SessionMetadata>): Promise<void> {\n await this.store.updateMetadata(key, updates);\n this.emit('sessionUpdated', { key });\n }\n\n async tagSession(key: string, tags: string[]): Promise<void> {\n const existing = await this.store.getMetadata(key);\n if (!existing) {\n throw new Error(`Session not found: ${key}`);\n }\n\n // Merge tags, remove duplicates\n const mergedTags = [...new Set([...existing.tags, ...tags])];\n await this.store.updateMetadata(key, { tags: mergedTags });\n this.emit('sessionUpdated', { key, tags: mergedTags });\n }\n\n async untagSession(key: string, tags: string[]): Promise<void> {\n const existing = await this.store.getMetadata(key);\n if (!existing) {\n throw new Error(`Session not found: ${key}`);\n }\n\n const filteredTags = existing.tags.filter((t) => !tags.includes(t));\n await this.store.updateMetadata(key, { tags: filteredTags });\n this.emit('sessionUpdated', { key, tags: filteredTags });\n }\n\n async setSessionTags(key: string, tags: string[]): Promise<void> {\n await this.store.updateMetadata(key, { tags: [...new Set(tags)] });\n this.emit('sessionUpdated', { key, tags });\n }\n\n // ========== Status Management ==========\n\n async archiveSession(key: string): Promise<void> {\n await this.store.archive(key);\n this.emit('sessionArchived', { key });\n }\n\n async unarchiveSession(key: string): Promise<void> {\n await this.store.unarchive(key);\n this.emit('sessionRestored', { key });\n }\n\n async pinSession(key: string): Promise<void> {\n await this.store.pin(key);\n this.emit('sessionPinned', { key });\n }\n\n async unpinSession(key: string): Promise<void> {\n await this.store.unpin(key);\n this.emit('sessionUnpinned', { key });\n }\n\n async setSessionStatus(key: string, status: SessionStatus): Promise<void> {\n await this.store.setStatus(key, status);\n this.emit('sessionStatusChanged', { key, status });\n }\n\n // ========== Search ==========\n\n async searchSessions(query: string): Promise<SessionMetadata[]> {\n const result = await this.store.list({ search: query, limit: 100 });\n return result.items;\n }\n\n async searchInSession(key: string, keyword: string): Promise<Message[]> {\n return this.store.searchInSession(key, keyword);\n }\n\n // ========== Export/Import ==========\n\n async exportSession(key: string, format: ExportFormat): Promise<string> {\n return this.store.exportSession(key, format);\n }\n\n // ========== Statistics ==========\n\n async getStats(): Promise<GlobalSessionStats> {\n return this.store.getStats();\n }\n\n // ========== Maintenance ==========\n\n async archiveOldSessions(olderThanDays: number): Promise<number> {\n const count = await this.store.archiveOld(olderThanDays);\n log.info({ count, olderThanDays }, 'Archived old sessions');\n return count;\n }\n\n // ========== Event Helpers ==========\n\n onSessionCreated(callback: (metadata: SessionMetadata) => void): void {\n this.on('sessionCreated', callback);\n }\n\n onSessionUpdated(callback: (data: { key: string; name?: string; tags?: string[] }) => void): void {\n this.on('sessionUpdated', callback);\n }\n\n onSessionDeleted(callback: (data: { key: string }) => void): void {\n this.on('sessionDeleted', callback);\n }\n\n onSessionArchived(callback: (data: { key: string }) => void): void {\n this.on('sessionArchived', callback);\n }\n\n onSessionRestored(callback: (data: { key: string }) => void): void {\n this.on('sessionRestored', callback);\n }\n\n onSessionPinned(callback: (data: { key: string }) => void): void {\n this.on('sessionPinned', callback);\n }\n\n onSessionUnpinned(callback: (data: { key: string }) => void): void {\n this.on('sessionUnpinned', callback);\n }\n\n onSessionStatusChanged(callback: (data: { key: string; status: SessionStatus }) => void): void {\n this.on('sessionStatusChanged', callback);\n }\n\n onSessionAccessed(callback: (data: { key: string }) => void): void {\n this.on('sessionAccessed', callback);\n }\n\n // ========== Store delegation (messages, compaction) ==========\n\n /** Load messages for a session key */\n async loadMessages(key: string) {\n return this.store.loadMessages(key);\n }\n\n /** Wrapped transcript document (stable id, compaction history); null if missing or not a valid envelope. */\n async loadTranscriptDocument(key: string): Promise<XopcSessionTranscriptV1 | null> {\n return this.store.loadTranscriptDocument(key);\n }\n\n /**\n * Runtime turns must use PiTranscriptManager.appendMessage; this entry point\n * is reserved for compaction, tests, and admin tools.\n */\n async saveMessages(key: string, messages: any[]) {\n return this.store.saveMessages(key, messages);\n }\n\n /**\n * Append `kind: 'context'` transcript row (persisted, excluded from {@link loadMessages} / LLM).\n */\n async appendTranscriptContextEntry(\n key: string,\n entry: Omit<XopcTranscriptContextEntry, 'kind'> & Partial<Pick<XopcTranscriptContextEntry, 'kind'>>,\n ): Promise<void> {\n await this.store.appendTranscriptContextEntry(key, entry);\n this.emit('sessionUpdated', { key });\n }\n\n /** Delete session data */\n async delete(key: string): Promise<void> {\n await this.store.delete(key);\n }\n\n /** Archive transcript and start a new session id for the same key. */\n async resetSession(\n key: string,\n ): Promise<{ sessionId: string; previousSessionId: string } | null> {\n const result = await this.store.reset(key);\n if (result) {\n this.emit('sessionUpdated', { key });\n }\n return result;\n }\n\n /** Token/window stats for a message list */\n getWindowStats(messages: any[]) {\n return this.store.getWindowStats(messages);\n }\n\n /** Prepare compaction run */\n prepareCompaction(key: string, messages: any[], contextWindow: number) {\n return this.store.prepareCompaction(key, messages, contextWindow);\n }\n\n /** Compact session messages */\n compact(\n key: string,\n messages: any[],\n contextWindow: number,\n instructions?: string,\n force?: boolean,\n ): Promise<CompactionResult> {\n return this.store.compact(key, messages, contextWindow, instructions, force);\n }\n\n /** Compaction stats for a session */\n async getCompactionStats(key: string) {\n return this.store.getCompactionStats(key);\n }\n\n /** List pre-compaction transcript snapshots (newest first). */\n listCompactionCheckpoints(key: string) {\n return this.store.listCompactionCheckpoints(key);\n }\n\n getCompactionCheckpointDetail(key: string, checkpointId: string) {\n return this.store.getCompactionCheckpointDetail(key, checkpointId);\n }\n\n restoreCompactionCheckpoint(key: string, checkpointId: string) {\n return this.store.restoreCompactionCheckpoint(key, checkpointId);\n }\n\n /** Estimate token usage for messages */\n async estimateTokenUsage(key: string, messages: any[]): Promise<number> {\n return this.store.estimateTokens(messages);\n }\n}\n"],"mappings":";;;;;;aAGkD;AAmBlD,MAAM,MAAM,aAAa,eAAe;AAUxC,IAAa,eAAb,cAAkCA,eAAa;CAC7C;CAEA,YAAY,QAA4B;AACtC,SAAO;AACP,OAAK,QAAQ,IAAI,aACf;GACE,QAAQ,OAAO;GACf,SAAS,OAAO;GAChB,aAAa,OAAO;GACrB,EACD,OAAO,cACP,OAAO,iBACR;;CAGH,MAAM,aAA4B;AAChC,QAAM,KAAK,MAAM,YAAY;AAC7B,OAAK,KAAK,QAAQ;;;CAIpB,WAAyB;AACvB,SAAO,KAAK;;CAKd,MAAM,aAAa,OAAqE;AACtF,SAAO,KAAK,MAAM,KAAK,MAAM;;;;;;CAO/B,MAAM,cAAc,QAA0B,EAAE,EAA6C;EAE3F,MAAM,gBAAkC;GACtC,GAAG;GACH,QAAQ,MAAM,SAAS,YAAY,MAAM,WAAW;GACrD;EAED,MAAM,SAAS,MAAM,KAAK,MAAM,KAAK,cAAc;EAGnD,MAAM,mBAAmB,OAAO,MAAM,QAAQ,MAAM,EAAE,IAAI,WAAW,YAAY,CAAC;AAElF,SAAO;GACL,GAAG;GACH,OAAO;GACP,OAAO,iBAAiB;GACxB,SAAS;GACV;;CAGH,MAAM,WACJ,KACA,SAC+B;EAC/B,MAAM,UAAU,MAAM,KAAK,MAAM,IAAI,KAAK,QAAQ;AAClD,MAAI,QACF,MAAK,KAAK,mBAAmB,EAAE,KAAK,CAAC;AAEvC,SAAO;;CAGT,MAAM,sBACJ,KACA,SAiBQ;EACR,MAAM,SAAS,MAAM,KAAK,MAAM,eAAe,KAAK,QAAQ;AAC5D,MAAI,OACF,MAAK,KAAK,mBAAmB,EAAE,KAAK,CAAC;AAEvC,SAAO;;;;;CAMT,MAAM,aACJ,KACA,OACsD;EACtD,MAAM,OAAO,MAAM,KAAK,MAAM,YAAY,IAAI;AAC9C,MAAI,CAAC,KACH,QAAO;GAAE,IAAI;GAAO,OAAO;GAAqB;EAElD,MAAM,UAAU,4BAA4B,MAAM,MAAM;AACxD,MAAI,OAAO,KAAK,QAAQ,CAAC,WAAW,EAClC,QAAO,EAAE,IAAI,MAAM;AAErB,QAAM,KAAK,MAAM,eAAe,KAAK,QAAQ;AAC7C,OAAK,KAAK,kBAAkB,EAAE,KAAK,CAAC;AACpC,SAAO,EAAE,IAAI,MAAM;;CAGrB,MAAM,mBAAmB,KAA8C;AACrE,SAAO,KAAK,MAAM,YAAY,IAAI;;CAGpC,MAAM,cAAc,KAA+B;EACjD,MAAM,SAAS,MAAM,KAAK,MAAM,OAAO,IAAI;AAC3C,MAAI,OACF,MAAK,KAAK,kBAAkB,EAAE,KAAK,CAAC;AAEtC,SAAO;;CAGT,MAAM,eAAe,MAAkE;EACrF,MAAM,SAAS,MAAM,KAAK,MAAM,WAAW,KAAK;AAChD,OAAK,MAAM,OAAO,OAAO,QACvB,MAAK,KAAK,kBAAkB,EAAE,KAAK,CAAC;AAEtC,SAAO;;CAKT,MAAM,cAAc,KAAa,MAA6B;EAC5D,MAAM,WAAW,MAAM,KAAK,MAAM,YAAY,IAAI;AAClD,QAAM,KAAK,MAAM,eAAe,KAAK;GACnC;GACA,YAAY;IAAE,GAAI,UAAU,cAAc,EAAE;IAAG,aAAa;IAAQ;GACrE,CAAC;AACF,OAAK,KAAK,kBAAkB;GAAE;GAAK;GAAM,CAAC;;;CAI5C,MAAM,sBAAsB,KAAa,SAAkD;AACzF,QAAM,KAAK,MAAM,eAAe,KAAK,QAAQ;AAC7C,OAAK,KAAK,kBAAkB,EAAE,KAAK,CAAC;;CAGtC,MAAM,WAAW,KAAa,MAA+B;EAC3D,MAAM,WAAW,MAAM,KAAK,MAAM,YAAY,IAAI;AAClD,MAAI,CAAC,SACH,OAAM,IAAI,MAAM,sBAAsB,MAAM;EAI9C,MAAM,aAAa,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,SAAS,MAAM,GAAG,KAAK,CAAC,CAAC;AAC5D,QAAM,KAAK,MAAM,eAAe,KAAK,EAAE,MAAM,YAAY,CAAC;AAC1D,OAAK,KAAK,kBAAkB;GAAE;GAAK,MAAM;GAAY,CAAC;;CAGxD,MAAM,aAAa,KAAa,MAA+B;EAC7D,MAAM,WAAW,MAAM,KAAK,MAAM,YAAY,IAAI;AAClD,MAAI,CAAC,SACH,OAAM,IAAI,MAAM,sBAAsB,MAAM;EAG9C,MAAM,eAAe,SAAS,KAAK,QAAQ,MAAM,CAAC,KAAK,SAAS,EAAE,CAAC;AACnE,QAAM,KAAK,MAAM,eAAe,KAAK,EAAE,MAAM,cAAc,CAAC;AAC5D,OAAK,KAAK,kBAAkB;GAAE;GAAK,MAAM;GAAc,CAAC;;CAG1D,MAAM,eAAe,KAAa,MAA+B;AAC/D,QAAM,KAAK,MAAM,eAAe,KAAK,EAAE,MAAM,CAAC,GAAG,IAAI,IAAI,KAAK,CAAC,EAAE,CAAC;AAClE,OAAK,KAAK,kBAAkB;GAAE;GAAK;GAAM,CAAC;;CAK5C,MAAM,eAAe,KAA4B;AAC/C,QAAM,KAAK,MAAM,QAAQ,IAAI;AAC7B,OAAK,KAAK,mBAAmB,EAAE,KAAK,CAAC;;CAGvC,MAAM,iBAAiB,KAA4B;AACjD,QAAM,KAAK,MAAM,UAAU,IAAI;AAC/B,OAAK,KAAK,mBAAmB,EAAE,KAAK,CAAC;;CAGvC,MAAM,WAAW,KAA4B;AAC3C,QAAM,KAAK,MAAM,IAAI,IAAI;AACzB,OAAK,KAAK,iBAAiB,EAAE,KAAK,CAAC;;CAGrC,MAAM,aAAa,KAA4B;AAC7C,QAAM,KAAK,MAAM,MAAM,IAAI;AAC3B,OAAK,KAAK,mBAAmB,EAAE,KAAK,CAAC;;CAGvC,MAAM,iBAAiB,KAAa,QAAsC;AACxE,QAAM,KAAK,MAAM,UAAU,KAAK,OAAO;AACvC,OAAK,KAAK,wBAAwB;GAAE;GAAK;GAAQ,CAAC;;CAKpD,MAAM,eAAe,OAA2C;AAE9D,UAAO,MADc,KAAK,MAAM,KAAK;GAAE,QAAQ;GAAO,OAAO;GAAK,CAAC,EACrD;;CAGhB,MAAM,gBAAgB,KAAa,SAAqC;AACtE,SAAO,KAAK,MAAM,gBAAgB,KAAK,QAAQ;;CAKjD,MAAM,cAAc,KAAa,QAAuC;AACtE,SAAO,KAAK,MAAM,cAAc,KAAK,OAAO;;CAK9C,MAAM,WAAwC;AAC5C,SAAO,KAAK,MAAM,UAAU;;CAK9B,MAAM,mBAAmB,eAAwC;EAC/D,MAAM,QAAQ,MAAM,KAAK,MAAM,WAAW,cAAc;AACxD,MAAI,KAAK;GAAE;GAAO;GAAe,EAAE,wBAAwB;AAC3D,SAAO;;CAKT,iBAAiB,UAAqD;AACpE,OAAK,GAAG,kBAAkB,SAAS;;CAGrC,iBAAiB,UAAiF;AAChG,OAAK,GAAG,kBAAkB,SAAS;;CAGrC,iBAAiB,UAAiD;AAChE,OAAK,GAAG,kBAAkB,SAAS;;CAGrC,kBAAkB,UAAiD;AACjE,OAAK,GAAG,mBAAmB,SAAS;;CAGtC,kBAAkB,UAAiD;AACjE,OAAK,GAAG,mBAAmB,SAAS;;CAGtC,gBAAgB,UAAiD;AAC/D,OAAK,GAAG,iBAAiB,SAAS;;CAGpC,kBAAkB,UAAiD;AACjE,OAAK,GAAG,mBAAmB,SAAS;;CAGtC,uBAAuB,UAAwE;AAC7F,OAAK,GAAG,wBAAwB,SAAS;;CAG3C,kBAAkB,UAAiD;AACjE,OAAK,GAAG,mBAAmB,SAAS;;;CAMtC,MAAM,aAAa,KAAa;AAC9B,SAAO,KAAK,MAAM,aAAa,IAAI;;;CAIrC,MAAM,uBAAuB,KAAsD;AACjF,SAAO,KAAK,MAAM,uBAAuB,IAAI;;;;;;CAO/C,MAAM,aAAa,KAAa,UAAiB;AAC/C,SAAO,KAAK,MAAM,aAAa,KAAK,SAAS;;;;;CAM/C,MAAM,6BACJ,KACA,OACe;AACf,QAAM,KAAK,MAAM,6BAA6B,KAAK,MAAM;AACzD,OAAK,KAAK,kBAAkB,EAAE,KAAK,CAAC;;;CAItC,MAAM,OAAO,KAA4B;AACvC,QAAM,KAAK,MAAM,OAAO,IAAI;;;CAI9B,MAAM,aACJ,KACkE;EAClE,MAAM,SAAS,MAAM,KAAK,MAAM,MAAM,IAAI;AAC1C,MAAI,OACF,MAAK,KAAK,kBAAkB,EAAE,KAAK,CAAC;AAEtC,SAAO;;;CAIT,eAAe,UAAiB;AAC9B,SAAO,KAAK,MAAM,eAAe,SAAS;;;CAI5C,kBAAkB,KAAa,UAAiB,eAAuB;AACrE,SAAO,KAAK,MAAM,kBAAkB,KAAK,UAAU,cAAc;;;CAInE,QACE,KACA,UACA,eACA,cACA,OAC2B;AAC3B,SAAO,KAAK,MAAM,QAAQ,KAAK,UAAU,eAAe,cAAc,MAAM;;;CAI9E,MAAM,mBAAmB,KAAa;AACpC,SAAO,KAAK,MAAM,mBAAmB,IAAI;;;CAI3C,0BAA0B,KAAa;AACrC,SAAO,KAAK,MAAM,0BAA0B,IAAI;;CAGlD,8BAA8B,KAAa,cAAsB;AAC/D,SAAO,KAAK,MAAM,8BAA8B,KAAK,aAAa;;CAGpE,4BAA4B,KAAa,cAAsB;AAC7D,SAAO,KAAK,MAAM,4BAA4B,KAAK,aAAa;;;CAIlE,MAAM,mBAAmB,KAAa,UAAkC;AACtE,SAAO,KAAK,MAAM,eAAe,SAAS"}
@@ -7,14 +7,30 @@ export declare function isWebchatSessionKey(sessionKey: string): boolean;
7
7
  /** Whether to run LLM/fallback session naming for this key (excludes cron, heartbeat). */
8
8
  export declare function shouldAutoTitleSessionKey(sessionKey: string): boolean;
9
9
  export declare function sanitizeSessionTitle(raw: string): string;
10
+ export type SessionTitleSource = 'provisional' | 'llm' | 'user';
11
+ export declare function getSessionTitleSource(meta: {
12
+ customData?: Record<string, unknown>;
13
+ } | null | undefined): SessionTitleSource | null;
14
+ /** Title from a single user message (first line), for immediate sidebar labels. */
15
+ export declare function provisionalTitleFromUserText(raw: string): string | null;
10
16
  /** Non-LLM title: first line of first user text, else first assistant line. */
11
17
  export declare function fallbackTitleFromMessages(messages: AgentMessage[]): string | null;
12
18
  /**
13
19
  * Returns a title string, or null if generation should be skipped or failed.
14
20
  */
15
21
  export declare function generateSessionTitleFromMessages(modelRef: string, messages: AgentMessage[], signal?: AbortSignal): Promise<string | null>;
22
+ export type SessionTitleUpdatedHook = (sessionKey: string, name: string) => void | Promise<void>;
23
+ /** Whether post-turn LLM refine may run (provisional or still unnamed; not user-locked). */
24
+ export declare function shouldRefineSessionTitleWithLlm(meta: {
25
+ name?: string;
26
+ customData?: Record<string, unknown>;
27
+ } | null | undefined): boolean;
16
28
  /**
17
- * If the session is still unnamed, set `name` (LLM when possible, else first-line fallback).
18
- * Skips cron/heartbeat keys. Ensures index row exists by re-saving when metadata is missing (fixes index lag).
29
+ * Set provisional title from first user text when session is still unnamed.
30
+ * Skips cron/heartbeat keys and user-locked titles.
19
31
  */
20
- export declare function maybeAutoTitleSessionStore(sessionStore: SessionStore, sessionKey: string, modelRef: string | undefined): Promise<void>;
32
+ export declare function maybeSetProvisionalSessionTitle(sessionStore: SessionStore, sessionKey: string, userText?: string, onUpdated?: SessionTitleUpdatedHook): Promise<void>;
33
+ /**
34
+ * LLM refine when title is empty or still provisional (not user-locked).
35
+ */
36
+ export declare function maybeRefineSessionTitleWithLlm(sessionStore: SessionStore, sessionKey: string, modelRef: string | undefined, onUpdated?: SessionTitleUpdatedHook): Promise<void>;
@@ -63,6 +63,19 @@ function sanitizeSessionTitle(raw) {
63
63
  if (s.length > MAX_TITLE_LEN) s = s.slice(0, MAX_TITLE_LEN - 1).trimEnd() + "…";
64
64
  return s;
65
65
  }
66
+ function getSessionTitleSource(meta) {
67
+ const raw = meta?.customData?.titleSource;
68
+ if (raw === "provisional" || raw === "llm" || raw === "user") return raw;
69
+ return null;
70
+ }
71
+ /** Title from a single user message (first line), for immediate sidebar labels. */
72
+ function provisionalTitleFromUserText(raw) {
73
+ const text = stripInboundFileMetadataFromText(stripEnvelopeTimestampPrefix(stripSessionStartupContextFromUserText((raw ?? "").trim())));
74
+ if (!text) return null;
75
+ const line = text.split(/\n/)[0]?.trim();
76
+ if (!line) return null;
77
+ return sanitizeSessionTitle(line);
78
+ }
66
79
  /** Non-LLM title: first line of first user text, else first assistant line. */
67
80
  function fallbackTitleFromMessages(messages) {
68
81
  const u = firstUserText(messages);
@@ -130,11 +143,56 @@ Title:`,
130
143
  return null;
131
144
  }
132
145
  }
146
+ /** Whether post-turn LLM refine may run (provisional or still unnamed; not user-locked). */
147
+ function shouldRefineSessionTitleWithLlm(meta) {
148
+ if (!meta) return false;
149
+ const source = getSessionTitleSource(meta);
150
+ if (source === "user") return false;
151
+ if (meta.name?.trim()) return source === "provisional";
152
+ return true;
153
+ }
154
+ function canAutoWriteTitle(meta) {
155
+ if (!meta) return false;
156
+ if (getSessionTitleSource(meta) === "user") return false;
157
+ return !meta.name?.trim();
158
+ }
159
+ /**
160
+ * Set provisional title from first user text when session is still unnamed.
161
+ * Skips cron/heartbeat keys and user-locked titles.
162
+ */
163
+ async function maybeSetProvisionalSessionTitle(sessionStore, sessionKey, userText, onUpdated) {
164
+ if (!shouldAutoTitleSessionKey(sessionKey)) return;
165
+ let meta = await sessionStore.getMetadata(sessionKey);
166
+ if (!meta) return;
167
+ if (!canAutoWriteTitle(meta)) return;
168
+ let title = null;
169
+ if (userText?.trim()) title = provisionalTitleFromUserText(userText);
170
+ if (!title) {
171
+ const messages = await sessionStore.load(sessionKey);
172
+ if (!messages.length) return;
173
+ title = fallbackTitleFromMessages(messages);
174
+ }
175
+ if (!title) return;
176
+ try {
177
+ await sessionStore.updateMetadata(sessionKey, {
178
+ name: title,
179
+ customData: {
180
+ ...meta.customData ?? {},
181
+ titleSource: "provisional"
182
+ }
183
+ });
184
+ await onUpdated?.(sessionKey, title);
185
+ } catch (err) {
186
+ log.warn({
187
+ err,
188
+ sessionKey
189
+ }, "Session title: provisional updateMetadata failed");
190
+ }
191
+ }
133
192
  /**
134
- * If the session is still unnamed, set `name` (LLM when possible, else first-line fallback).
135
- * Skips cron/heartbeat keys. Ensures index row exists by re-saving when metadata is missing (fixes index lag).
193
+ * LLM refine when title is empty or still provisional (not user-locked).
136
194
  */
137
- async function maybeAutoTitleSessionStore(sessionStore, sessionKey, modelRef) {
195
+ async function maybeRefineSessionTitleWithLlm(sessionStore, sessionKey, modelRef, onUpdated) {
138
196
  if (!shouldAutoTitleSessionKey(sessionKey)) return;
139
197
  let messages = await sessionStore.load(sessionKey);
140
198
  if (!messages.length) return;
@@ -147,7 +205,8 @@ async function maybeAutoTitleSessionStore(sessionStore, sessionKey, modelRef) {
147
205
  log.warn({ sessionKey }, "Session title: metadata missing after save");
148
206
  return;
149
207
  }
150
- if (meta.name && meta.name.trim().length > 0) return;
208
+ if (!shouldRefineSessionTitleWithLlm(meta)) return;
209
+ const source = getSessionTitleSource(meta);
151
210
  let title = null;
152
211
  const ref = modelRef?.trim();
153
212
  if (ref) {
@@ -161,16 +220,32 @@ async function maybeAutoTitleSessionStore(sessionStore, sessionKey, modelRef) {
161
220
  }
162
221
  if (!title) title = fallbackTitleFromMessages(messages);
163
222
  if (!title) return;
223
+ if (meta.name?.trim() === title) {
224
+ if (source !== "llm") try {
225
+ await sessionStore.updateMetadata(sessionKey, { customData: {
226
+ ...meta.customData ?? {},
227
+ titleSource: "llm"
228
+ } });
229
+ } catch {}
230
+ return;
231
+ }
164
232
  try {
165
- await sessionStore.updateMetadata(sessionKey, { name: title });
233
+ await sessionStore.updateMetadata(sessionKey, {
234
+ name: title,
235
+ customData: {
236
+ ...meta.customData ?? {},
237
+ titleSource: "llm"
238
+ }
239
+ });
240
+ await onUpdated?.(sessionKey, title);
166
241
  } catch (err) {
167
242
  log.warn({
168
243
  err,
169
244
  sessionKey
170
- }, "Session title: updateMetadata failed");
245
+ }, "Session title: refine updateMetadata failed");
171
246
  }
172
247
  }
173
248
  //#endregion
174
- export { fallbackTitleFromMessages, generateSessionTitleFromMessages, isWebchatSessionKey, maybeAutoTitleSessionStore, sanitizeSessionTitle, shouldAutoTitleSessionKey };
249
+ export { fallbackTitleFromMessages, generateSessionTitleFromMessages, getSessionTitleSource, isWebchatSessionKey, maybeRefineSessionTitleWithLlm, maybeSetProvisionalSessionTitle, provisionalTitleFromUserText, sanitizeSessionTitle, shouldAutoTitleSessionKey, shouldRefineSessionTitleWithLlm };
175
250
 
176
251
  //# sourceMappingURL=session-title.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"session-title.js","names":[],"sources":["../../../src/session/session-title.ts"],"sourcesContent":["/**\n * LLM-generated session titles (webchat and any path using SessionStore).\n */\n\nimport type { AgentMessage } from '@earendil-works/pi-agent-core';\nimport { complete, type UserMessage } from '@earendil-works/pi-ai';\n\nimport { stripSessionStartupContextFromUserText } from '../agent/reply/startup-context.js';\nimport { stripInboundFileMetadataFromText } from '../channels/attachments/inbound-persist.js';\nimport { stripEnvelopeTimestampPrefix } from '../channels/envelope-timestamp.js';\nimport { isCronSessionKey, parseSessionKey } from '../routing/session-key.js';\nimport { resolveModel } from '../providers/index.js';\nimport { createLogger } from '../utils/logger.js';\nimport { readAgentMessageContent } from '../agent/memory/agent-message-access.js';\nimport type { SessionStore } from './store.js';\n\nconst log = createLogger('SessionAutoTitle');\n\nconst MAX_TITLE_LEN = 80;\n\n/** Collect visible text from any content block that exposes `text` (pi-ai / OpenAI / Anthropic shapes). */\nfunction extractTextFromMessage(m: AgentMessage): string {\n const raw = readAgentMessageContent(m);\n if (typeof raw === 'string') return raw.trim();\n if (Array.isArray(raw)) {\n const parts: string[] = [];\n for (const c of raw) {\n if (c && typeof c === 'object') {\n const o = c as unknown as Record<string, unknown>;\n const type = typeof o.type === 'string' ? o.type : '';\n if (type === 'toolCall' || type === 'tool_use' || type === 'tool_result') continue;\n if (typeof o.text === 'string' && o.text.trim()) {\n parts.push(o.text.trim());\n }\n }\n }\n return parts.join(' ').trim();\n }\n return '';\n}\n\nfunction firstUserText(messages: AgentMessage[]): string {\n const u = messages.find((m) => m.role === 'user');\n if (!u) return '';\n const raw = extractTextFromMessage(u);\n // User turns include `formatInboundFileTextBlock` text blocks; do not feed [File:…] into title LLM / fallback.\n // Inbound pipeline / webchat prepends `[YYYY-MM-DD HH:MM TZ]`; strip so titles are not timestamp-led.\n return stripInboundFileMetadataFromText(\n stripEnvelopeTimestampPrefix(stripSessionStartupContextFromUserText(raw)),\n );\n}\n\n/** First assistant message that has visible text (skips tool-only assistant rows). */\nfunction firstAssistantText(messages: AgentMessage[]): string {\n for (const m of messages) {\n if (m.role === 'assistant') {\n const t = extractTextFromMessage(m);\n if (t.length > 0) return t;\n }\n }\n return '';\n}\n\nexport function isWebchatSessionKey(sessionKey: string): boolean {\n const p = parseSessionKey(sessionKey);\n if (p?.source === 'webchat') return true;\n return sessionKey.includes(':webchat:');\n}\n\n/** Whether to run LLM/fallback session naming for this key (excludes cron, heartbeat). */\nexport function shouldAutoTitleSessionKey(sessionKey: string): boolean {\n const raw = (sessionKey ?? '').trim();\n if (!raw) return false;\n if (isCronSessionKey(raw)) return false;\n if (raw.toLowerCase().startsWith('heartbeat:')) return false;\n return true;\n}\n\nexport function sanitizeSessionTitle(raw: string): string {\n let s = raw.trim();\n if ((s.startsWith('\"') && s.endsWith('\"')) || (s.startsWith(\"'\") && s.endsWith(\"'\"))) {\n s = s.slice(1, -1).trim();\n }\n const lineBreak = s.indexOf('\\n');\n if (lineBreak !== -1) s = s.slice(0, lineBreak).trim();\n if (s.length > MAX_TITLE_LEN) s = s.slice(0, MAX_TITLE_LEN - 1).trimEnd() + '…';\n return s;\n}\n\n/** Non-LLM title: first line of first user text, else first assistant line. */\nexport function fallbackTitleFromMessages(messages: AgentMessage[]): string | null {\n const u = firstUserText(messages);\n if (u) {\n const line = u.split(/\\n/)[0]?.trim();\n if (line) return sanitizeSessionTitle(line);\n }\n const a = firstAssistantText(messages);\n if (a) {\n const line = a.split(/\\n/)[0]?.trim();\n if (line) return sanitizeSessionTitle(line);\n }\n return null;\n}\n\n/**\n * Returns a title string, or null if generation should be skipped or failed.\n */\nexport async function generateSessionTitleFromMessages(\n modelRef: string,\n messages: AgentMessage[],\n signal?: AbortSignal,\n): Promise<string | null> {\n const userText = firstUserText(messages);\n const assistantText = firstAssistantText(messages);\n if (!userText && !assistantText) return null;\n\n let model: ReturnType<typeof resolveModel>;\n try {\n model = resolveModel(modelRef);\n } catch (err) {\n log.warn({ err, modelRef }, 'Cannot resolve model for session title');\n return null;\n }\n\n const prompt =\n userText && assistantText\n ? `You label chat sessions. Given the first user message and the start of the assistant reply, output ONE short title (max 8 words). No quotes. No punctuation at the end. Use the same language as the user when possible.\n\nUser: ${userText.slice(0, 2000)}\n\nAssistant: ${assistantText.slice(0, 2000)}\n\nTitle:`\n : userText\n ? `The assistant reply only used tools (no visible text yet). Output ONE short title (max 8 words) based only on the user's first message. No quotes. No punctuation at the end. Use the same language as the user.\n\nUser: ${userText.slice(0, 2000)}\n\nTitle:`\n : `Output ONE short title (max 8 words) for this assistant reply. No quotes. No punctuation at the end.\n\nAssistant: ${assistantText!.slice(0, 2000)}\n\nTitle:`;\n\n const userMsg: UserMessage = { role: 'user', content: prompt, timestamp: Date.now() };\n\n try {\n const result = await complete(\n model,\n { messages: [userMsg] },\n {\n maxTokens: 64,\n temperature: 0.35,\n signal: signal as AbortSignal,\n },\n );\n\n let text = '';\n if (Array.isArray(result.content)) {\n for (const c of result.content) {\n if (c && typeof c === 'object' && (c as { type?: string }).type === 'text') {\n text += String((c as { text?: string }).text || '');\n }\n }\n }\n\n const cleaned = sanitizeSessionTitle(text);\n return cleaned.length > 0 ? cleaned : null;\n } catch (err) {\n log.warn({ err }, 'Session title LLM call failed');\n return null;\n }\n}\n\n/**\n * If the session is still unnamed, set `name` (LLM when possible, else first-line fallback).\n * Skips cron/heartbeat keys. Ensures index row exists by re-saving when metadata is missing (fixes index lag).\n */\nexport async function maybeAutoTitleSessionStore(\n sessionStore: SessionStore,\n sessionKey: string,\n modelRef: string | undefined,\n): Promise<void> {\n if (!shouldAutoTitleSessionKey(sessionKey)) return;\n\n let messages = await sessionStore.load(sessionKey);\n if (!messages.length) return;\n\n let meta = await sessionStore.getMetadata(sessionKey);\n if (!meta) {\n await sessionStore.saveMessages(sessionKey, messages);\n meta = await sessionStore.getMetadata(sessionKey);\n }\n if (!meta) {\n log.warn({ sessionKey }, 'Session title: metadata missing after save');\n return;\n }\n if (meta.name && meta.name.trim().length > 0) return;\n\n let title: string | null = null;\n const ref = modelRef?.trim();\n if (ref) {\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), 25_000);\n try {\n title = await generateSessionTitleFromMessages(ref, messages, controller.signal);\n } finally {\n clearTimeout(timeout);\n }\n }\n if (!title) {\n title = fallbackTitleFromMessages(messages);\n }\n if (!title) return;\n\n try {\n await sessionStore.updateMetadata(sessionKey, { name: title });\n } catch (err) {\n log.warn({ err, sessionKey }, 'Session title: updateMetadata failed');\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"session-title.js","names":[],"sources":["../../../src/session/session-title.ts"],"sourcesContent":["/**\n * LLM-generated session titles (webchat and any path using SessionStore).\n */\n\nimport type { AgentMessage } from '@earendil-works/pi-agent-core';\nimport { complete, type UserMessage } from '@earendil-works/pi-ai';\n\nimport { stripSessionStartupContextFromUserText } from '../agent/reply/startup-context.js';\nimport { stripInboundFileMetadataFromText } from '../channels/attachments/inbound-persist.js';\nimport { stripEnvelopeTimestampPrefix } from '../channels/envelope-timestamp.js';\nimport { isCronSessionKey, parseSessionKey } from '../routing/session-key.js';\nimport { resolveModel } from '../providers/index.js';\nimport { createLogger } from '../utils/logger.js';\nimport { readAgentMessageContent } from '../agent/memory/agent-message-access.js';\nimport type { SessionStore } from './store.js';\n\nconst log = createLogger('SessionAutoTitle');\n\nconst MAX_TITLE_LEN = 80;\n\n/** Collect visible text from any content block that exposes `text` (pi-ai / OpenAI / Anthropic shapes). */\nfunction extractTextFromMessage(m: AgentMessage): string {\n const raw = readAgentMessageContent(m);\n if (typeof raw === 'string') return raw.trim();\n if (Array.isArray(raw)) {\n const parts: string[] = [];\n for (const c of raw) {\n if (c && typeof c === 'object') {\n const o = c as unknown as Record<string, unknown>;\n const type = typeof o.type === 'string' ? o.type : '';\n if (type === 'toolCall' || type === 'tool_use' || type === 'tool_result') continue;\n if (typeof o.text === 'string' && o.text.trim()) {\n parts.push(o.text.trim());\n }\n }\n }\n return parts.join(' ').trim();\n }\n return '';\n}\n\nfunction firstUserText(messages: AgentMessage[]): string {\n const u = messages.find((m) => m.role === 'user');\n if (!u) return '';\n const raw = extractTextFromMessage(u);\n // User turns include `formatInboundFileTextBlock` text blocks; do not feed [File:…] into title LLM / fallback.\n // Inbound pipeline / webchat prepends `[YYYY-MM-DD HH:MM TZ]`; strip so titles are not timestamp-led.\n return stripInboundFileMetadataFromText(\n stripEnvelopeTimestampPrefix(stripSessionStartupContextFromUserText(raw)),\n );\n}\n\n/** First assistant message that has visible text (skips tool-only assistant rows). */\nfunction firstAssistantText(messages: AgentMessage[]): string {\n for (const m of messages) {\n if (m.role === 'assistant') {\n const t = extractTextFromMessage(m);\n if (t.length > 0) return t;\n }\n }\n return '';\n}\n\nexport function isWebchatSessionKey(sessionKey: string): boolean {\n const p = parseSessionKey(sessionKey);\n if (p?.source === 'webchat') return true;\n return sessionKey.includes(':webchat:');\n}\n\n/** Whether to run LLM/fallback session naming for this key (excludes cron, heartbeat). */\nexport function shouldAutoTitleSessionKey(sessionKey: string): boolean {\n const raw = (sessionKey ?? '').trim();\n if (!raw) return false;\n if (isCronSessionKey(raw)) return false;\n if (raw.toLowerCase().startsWith('heartbeat:')) return false;\n return true;\n}\n\nexport function sanitizeSessionTitle(raw: string): string {\n let s = raw.trim();\n if ((s.startsWith('\"') && s.endsWith('\"')) || (s.startsWith(\"'\") && s.endsWith(\"'\"))) {\n s = s.slice(1, -1).trim();\n }\n const lineBreak = s.indexOf('\\n');\n if (lineBreak !== -1) s = s.slice(0, lineBreak).trim();\n if (s.length > MAX_TITLE_LEN) s = s.slice(0, MAX_TITLE_LEN - 1).trimEnd() + '…';\n return s;\n}\n\nexport type SessionTitleSource = 'provisional' | 'llm' | 'user';\n\nexport function getSessionTitleSource(\n meta: { customData?: Record<string, unknown> } | null | undefined,\n): SessionTitleSource | null {\n const raw = meta?.customData?.titleSource;\n if (raw === 'provisional' || raw === 'llm' || raw === 'user') return raw;\n return null;\n}\n\n/** Title from a single user message (first line), for immediate sidebar labels. */\nexport function provisionalTitleFromUserText(raw: string): string | null {\n const text = stripInboundFileMetadataFromText(\n stripEnvelopeTimestampPrefix(stripSessionStartupContextFromUserText((raw ?? '').trim())),\n );\n if (!text) return null;\n const line = text.split(/\\n/)[0]?.trim();\n if (!line) return null;\n return sanitizeSessionTitle(line);\n}\n\n/** Non-LLM title: first line of first user text, else first assistant line. */\nexport function fallbackTitleFromMessages(messages: AgentMessage[]): string | null {\n const u = firstUserText(messages);\n if (u) {\n const line = u.split(/\\n/)[0]?.trim();\n if (line) return sanitizeSessionTitle(line);\n }\n const a = firstAssistantText(messages);\n if (a) {\n const line = a.split(/\\n/)[0]?.trim();\n if (line) return sanitizeSessionTitle(line);\n }\n return null;\n}\n\n/**\n * Returns a title string, or null if generation should be skipped or failed.\n */\nexport async function generateSessionTitleFromMessages(\n modelRef: string,\n messages: AgentMessage[],\n signal?: AbortSignal,\n): Promise<string | null> {\n const userText = firstUserText(messages);\n const assistantText = firstAssistantText(messages);\n if (!userText && !assistantText) return null;\n\n let model: ReturnType<typeof resolveModel>;\n try {\n model = resolveModel(modelRef);\n } catch (err) {\n log.warn({ err, modelRef }, 'Cannot resolve model for session title');\n return null;\n }\n\n const prompt =\n userText && assistantText\n ? `You label chat sessions. Given the first user message and the start of the assistant reply, output ONE short title (max 8 words). No quotes. No punctuation at the end. Use the same language as the user when possible.\n\nUser: ${userText.slice(0, 2000)}\n\nAssistant: ${assistantText.slice(0, 2000)}\n\nTitle:`\n : userText\n ? `The assistant reply only used tools (no visible text yet). Output ONE short title (max 8 words) based only on the user's first message. No quotes. No punctuation at the end. Use the same language as the user.\n\nUser: ${userText.slice(0, 2000)}\n\nTitle:`\n : `Output ONE short title (max 8 words) for this assistant reply. No quotes. No punctuation at the end.\n\nAssistant: ${assistantText!.slice(0, 2000)}\n\nTitle:`;\n\n const userMsg: UserMessage = { role: 'user', content: prompt, timestamp: Date.now() };\n\n try {\n const result = await complete(\n model,\n { messages: [userMsg] },\n {\n maxTokens: 64,\n temperature: 0.35,\n signal: signal as AbortSignal,\n },\n );\n\n let text = '';\n if (Array.isArray(result.content)) {\n for (const c of result.content) {\n if (c && typeof c === 'object' && (c as { type?: string }).type === 'text') {\n text += String((c as { text?: string }).text || '');\n }\n }\n }\n\n const cleaned = sanitizeSessionTitle(text);\n return cleaned.length > 0 ? cleaned : null;\n } catch (err) {\n log.warn({ err }, 'Session title LLM call failed');\n return null;\n }\n}\n\nexport type SessionTitleUpdatedHook = (sessionKey: string, name: string) => void | Promise<void>;\n\n/** Whether post-turn LLM refine may run (provisional or still unnamed; not user-locked). */\nexport function shouldRefineSessionTitleWithLlm(\n meta: { name?: string; customData?: Record<string, unknown> } | null | undefined,\n): boolean {\n if (!meta) return false;\n const source = getSessionTitleSource(meta);\n if (source === 'user') return false;\n if (meta.name?.trim()) return source === 'provisional';\n return true;\n}\n\nfunction canAutoWriteTitle(meta: { name?: string; customData?: Record<string, unknown> } | null): boolean {\n if (!meta) return false;\n const source = getSessionTitleSource(meta);\n if (source === 'user') return false;\n return !meta.name?.trim();\n}\n\n/**\n * Set provisional title from first user text when session is still unnamed.\n * Skips cron/heartbeat keys and user-locked titles.\n */\nexport async function maybeSetProvisionalSessionTitle(\n sessionStore: SessionStore,\n sessionKey: string,\n userText?: string,\n onUpdated?: SessionTitleUpdatedHook,\n): Promise<void> {\n if (!shouldAutoTitleSessionKey(sessionKey)) return;\n\n let meta = await sessionStore.getMetadata(sessionKey);\n if (!meta) return;\n if (!canAutoWriteTitle(meta)) return;\n\n let title: string | null = null;\n if (userText?.trim()) {\n title = provisionalTitleFromUserText(userText);\n }\n if (!title) {\n const messages = await sessionStore.load(sessionKey);\n if (!messages.length) return;\n title = fallbackTitleFromMessages(messages);\n }\n if (!title) return;\n\n try {\n await sessionStore.updateMetadata(sessionKey, {\n name: title,\n customData: { ...(meta.customData ?? {}), titleSource: 'provisional' },\n });\n await onUpdated?.(sessionKey, title);\n } catch (err) {\n log.warn({ err, sessionKey }, 'Session title: provisional updateMetadata failed');\n }\n}\n\n/**\n * LLM refine when title is empty or still provisional (not user-locked).\n */\nexport async function maybeRefineSessionTitleWithLlm(\n sessionStore: SessionStore,\n sessionKey: string,\n modelRef: string | undefined,\n onUpdated?: SessionTitleUpdatedHook,\n): Promise<void> {\n if (!shouldAutoTitleSessionKey(sessionKey)) return;\n\n let messages = await sessionStore.load(sessionKey);\n if (!messages.length) return;\n\n let meta = await sessionStore.getMetadata(sessionKey);\n if (!meta) {\n await sessionStore.saveMessages(sessionKey, messages);\n meta = await sessionStore.getMetadata(sessionKey);\n }\n if (!meta) {\n log.warn({ sessionKey }, 'Session title: metadata missing after save');\n return;\n }\n if (!shouldRefineSessionTitleWithLlm(meta)) return;\n\n const source = getSessionTitleSource(meta);\n let title: string | null = null;\n const ref = modelRef?.trim();\n if (ref) {\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), 25_000);\n try {\n title = await generateSessionTitleFromMessages(ref, messages, controller.signal);\n } finally {\n clearTimeout(timeout);\n }\n }\n if (!title) {\n title = fallbackTitleFromMessages(messages);\n }\n if (!title) return;\n\n const existing = meta.name?.trim();\n if (existing === title) {\n if (source !== 'llm') {\n try {\n await sessionStore.updateMetadata(sessionKey, {\n customData: { ...(meta.customData ?? {}), titleSource: 'llm' },\n });\n } catch {\n /* ignore */\n }\n }\n return;\n }\n\n try {\n await sessionStore.updateMetadata(sessionKey, {\n name: title,\n customData: { ...(meta.customData ?? {}), titleSource: 'llm' },\n });\n await onUpdated?.(sessionKey, title);\n } catch (err) {\n log.warn({ err, sessionKey }, 'Session title: refine updateMetadata failed');\n }\n}\n"],"mappings":"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"}