@lota-sdk/core 0.1.5

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 (153) hide show
  1. package/infrastructure/schema/00_workstream.surql +55 -0
  2. package/infrastructure/schema/01_memory.surql +47 -0
  3. package/infrastructure/schema/02_execution_plan.surql +62 -0
  4. package/infrastructure/schema/03_learned_skill.surql +32 -0
  5. package/infrastructure/schema/04_runtime_bootstrap.surql +8 -0
  6. package/package.json +128 -0
  7. package/src/ai/definitions.ts +308 -0
  8. package/src/bifrost/bifrost.ts +256 -0
  9. package/src/config/agent-defaults.ts +99 -0
  10. package/src/config/constants.ts +33 -0
  11. package/src/config/env-shapes.ts +122 -0
  12. package/src/config/logger.ts +29 -0
  13. package/src/config/model-constants.ts +31 -0
  14. package/src/config/search.ts +17 -0
  15. package/src/config/workstream-defaults.ts +68 -0
  16. package/src/db/base.service.ts +55 -0
  17. package/src/db/cursor-pagination.ts +73 -0
  18. package/src/db/memory-query-builder.ts +207 -0
  19. package/src/db/memory-store.helpers.ts +118 -0
  20. package/src/db/memory-store.rows.ts +29 -0
  21. package/src/db/memory-store.ts +974 -0
  22. package/src/db/memory-types.ts +193 -0
  23. package/src/db/memory.ts +505 -0
  24. package/src/db/record-id.ts +78 -0
  25. package/src/db/service.ts +932 -0
  26. package/src/db/startup.ts +152 -0
  27. package/src/db/tables.ts +20 -0
  28. package/src/document/org-document-chunking.ts +224 -0
  29. package/src/document/parsing.ts +40 -0
  30. package/src/embeddings/provider.ts +76 -0
  31. package/src/index.ts +302 -0
  32. package/src/queues/context-compaction.queue.ts +82 -0
  33. package/src/queues/document-processor.queue.ts +118 -0
  34. package/src/queues/memory-consolidation.queue.ts +65 -0
  35. package/src/queues/post-chat-memory.queue.ts +128 -0
  36. package/src/queues/recent-activity-title-refinement.queue.ts +69 -0
  37. package/src/queues/regular-chat-memory-digest.config.ts +12 -0
  38. package/src/queues/regular-chat-memory-digest.queue.ts +73 -0
  39. package/src/queues/skill-extraction.config.ts +9 -0
  40. package/src/queues/skill-extraction.queue.ts +62 -0
  41. package/src/redis/connection.ts +176 -0
  42. package/src/redis/index.ts +30 -0
  43. package/src/redis/org-memory-lock.ts +43 -0
  44. package/src/redis/redis-lease-lock.ts +158 -0
  45. package/src/runtime/agent-contract.ts +1 -0
  46. package/src/runtime/agent-prompt-context.ts +119 -0
  47. package/src/runtime/agent-runtime-policy.ts +192 -0
  48. package/src/runtime/agent-stream-helpers.ts +117 -0
  49. package/src/runtime/agent-types.ts +22 -0
  50. package/src/runtime/approval-continuation.ts +16 -0
  51. package/src/runtime/chat-attachments.ts +46 -0
  52. package/src/runtime/chat-message.ts +10 -0
  53. package/src/runtime/chat-request-routing.ts +21 -0
  54. package/src/runtime/chat-run-orchestration.ts +25 -0
  55. package/src/runtime/chat-run-registry.ts +20 -0
  56. package/src/runtime/chat-types.ts +18 -0
  57. package/src/runtime/context-compaction-constants.ts +11 -0
  58. package/src/runtime/context-compaction-runtime.ts +86 -0
  59. package/src/runtime/context-compaction.ts +909 -0
  60. package/src/runtime/execution-plan.ts +59 -0
  61. package/src/runtime/helper-model.ts +405 -0
  62. package/src/runtime/indexed-repositories-policy.ts +28 -0
  63. package/src/runtime/instruction-sections.ts +8 -0
  64. package/src/runtime/llm-content.ts +71 -0
  65. package/src/runtime/memory-block.ts +264 -0
  66. package/src/runtime/memory-digest-policy.ts +14 -0
  67. package/src/runtime/memory-format.ts +8 -0
  68. package/src/runtime/memory-pipeline.ts +570 -0
  69. package/src/runtime/memory-prompts-fact.ts +47 -0
  70. package/src/runtime/memory-prompts-parse.ts +3 -0
  71. package/src/runtime/memory-prompts-update.ts +37 -0
  72. package/src/runtime/memory-scope.ts +43 -0
  73. package/src/runtime/plugin-types.ts +10 -0
  74. package/src/runtime/retrieval-adapters.ts +25 -0
  75. package/src/runtime/retrieval-pipeline.ts +3 -0
  76. package/src/runtime/runtime-extensions.ts +154 -0
  77. package/src/runtime/skill-extraction-policy.ts +3 -0
  78. package/src/runtime/team-consultation-orchestrator.ts +245 -0
  79. package/src/runtime/team-consultation-prompts.ts +32 -0
  80. package/src/runtime/title-helpers.ts +12 -0
  81. package/src/runtime/turn-lifecycle.ts +28 -0
  82. package/src/runtime/workstream-chat-helpers.ts +187 -0
  83. package/src/runtime/workstream-routing-policy.ts +301 -0
  84. package/src/runtime/workstream-state.ts +261 -0
  85. package/src/services/attachment.service.ts +159 -0
  86. package/src/services/chat-attachments.service.ts +17 -0
  87. package/src/services/chat-run-registry.service.ts +3 -0
  88. package/src/services/context-compaction-runtime.ts +13 -0
  89. package/src/services/context-compaction.service.ts +115 -0
  90. package/src/services/document-chunk.service.ts +141 -0
  91. package/src/services/execution-plan.service.ts +890 -0
  92. package/src/services/learned-skill.service.ts +328 -0
  93. package/src/services/memory-assessment.service.ts +43 -0
  94. package/src/services/memory.service.ts +807 -0
  95. package/src/services/memory.utils.ts +84 -0
  96. package/src/services/mutating-approval.service.ts +110 -0
  97. package/src/services/recent-activity-title.service.ts +74 -0
  98. package/src/services/recent-activity.service.ts +397 -0
  99. package/src/services/workstream-change-tracker.service.ts +313 -0
  100. package/src/services/workstream-message.service.ts +283 -0
  101. package/src/services/workstream-title.service.ts +58 -0
  102. package/src/services/workstream-turn-preparation.ts +1340 -0
  103. package/src/services/workstream-turn.ts +37 -0
  104. package/src/services/workstream.service.ts +854 -0
  105. package/src/services/workstream.types.ts +118 -0
  106. package/src/storage/attachment-parser.ts +101 -0
  107. package/src/storage/attachment-storage.service.ts +391 -0
  108. package/src/storage/attachments.types.ts +11 -0
  109. package/src/storage/attachments.utils.ts +58 -0
  110. package/src/storage/generated-document-storage.service.ts +55 -0
  111. package/src/system-agents/agent-result.ts +27 -0
  112. package/src/system-agents/context-compacter.agent.ts +46 -0
  113. package/src/system-agents/delegated-agent-factory.ts +177 -0
  114. package/src/system-agents/helper-agent-options.ts +20 -0
  115. package/src/system-agents/memory-reranker.agent.ts +38 -0
  116. package/src/system-agents/memory.agent.ts +58 -0
  117. package/src/system-agents/recent-activity-title-refiner.agent.ts +53 -0
  118. package/src/system-agents/regular-chat-memory-digest.agent.ts +75 -0
  119. package/src/system-agents/researcher.agent.ts +34 -0
  120. package/src/system-agents/skill-extractor.agent.ts +88 -0
  121. package/src/system-agents/skill-manager.agent.ts +80 -0
  122. package/src/system-agents/title-generator.agent.ts +42 -0
  123. package/src/system-agents/workstream-tracker.agent.ts +58 -0
  124. package/src/tools/execution-plan.tool.ts +163 -0
  125. package/src/tools/fetch-webpage.tool.ts +132 -0
  126. package/src/tools/firecrawl-client.ts +12 -0
  127. package/src/tools/memory-block.tool.ts +55 -0
  128. package/src/tools/read-file-parts.tool.ts +80 -0
  129. package/src/tools/remember-memory.tool.ts +85 -0
  130. package/src/tools/research-topic.tool.ts +15 -0
  131. package/src/tools/search-tools.ts +55 -0
  132. package/src/tools/search-web.tool.ts +175 -0
  133. package/src/tools/team-think.tool.ts +125 -0
  134. package/src/tools/tool-contract.ts +21 -0
  135. package/src/tools/user-questions.tool.ts +18 -0
  136. package/src/utils/async.ts +50 -0
  137. package/src/utils/date-time.ts +34 -0
  138. package/src/utils/error.ts +10 -0
  139. package/src/utils/errors.ts +28 -0
  140. package/src/utils/hono-error-handler.ts +71 -0
  141. package/src/utils/string.ts +51 -0
  142. package/src/workers/bootstrap.ts +44 -0
  143. package/src/workers/memory-consolidation.worker.ts +318 -0
  144. package/src/workers/regular-chat-memory-digest.helpers.ts +100 -0
  145. package/src/workers/regular-chat-memory-digest.runner.ts +363 -0
  146. package/src/workers/regular-chat-memory-digest.worker.ts +22 -0
  147. package/src/workers/skill-extraction.runner.ts +331 -0
  148. package/src/workers/skill-extraction.worker.ts +22 -0
  149. package/src/workers/utils/repo-indexer-chunker.ts +331 -0
  150. package/src/workers/utils/repo-structure-extractor.ts +645 -0
  151. package/src/workers/utils/repomix-process-concurrency.ts +65 -0
  152. package/src/workers/utils/sandbox-error.ts +5 -0
  153. package/src/workers/worker-utils.ts +182 -0
@@ -0,0 +1,807 @@
1
+ import { z } from 'zod'
2
+
3
+ import { agentRoster } from '../config/agent-defaults'
4
+ import { env } from '../config/env-shapes'
5
+ import { aiLogger } from '../config/logger'
6
+ import { Memory } from '../db/memory'
7
+ import { isUniqueIndexConflict } from '../db/memory-store.helpers'
8
+ import type {
9
+ AddOptions,
10
+ ExtractedFact,
11
+ MemoryRecord,
12
+ MemorySearchResult,
13
+ MemoryType,
14
+ Message,
15
+ } from '../db/memory-types'
16
+ import { withOrgMemoryLock } from '../redis/org-memory-lock'
17
+ import { createHelperModelRuntime } from '../runtime/helper-model'
18
+ import { sanitizeAgentOutputForMemory } from '../runtime/llm-content'
19
+ import { ORG_SCOPE_PREFIX, agentScopeId, scopeId } from '../runtime/memory-scope'
20
+ import {
21
+ countScopedRetrievalCandidates,
22
+ executeScopedRetrieval,
23
+ scopedRetrievalToMap,
24
+ } from '../runtime/retrieval-adapters'
25
+ import { createMemoryRerankerAgent, memoryRerankerPrompt } from '../system-agents/memory-reranker.agent'
26
+ import { createOrgMemoryAgent, orgMemoryPrompt } from '../system-agents/memory.agent'
27
+ import { assessMemoryImportance, clampMemoryImportance } from './memory-assessment.service'
28
+ import { formatMemoryResults, formatRerankedResults, getCandidateLimit } from './memory.utils'
29
+
30
+ const ORG_MEMORY_TYPE = 'fact'
31
+ const RERANK_CANDIDATE_MAX_CHARS = 500
32
+ const MAX_MEMORY_RESULTS_PER_SCOPE = 10
33
+ const PRESEEDED_MEMORY_LIMIT = 5
34
+ const PRESEEDED_MEMORY_MAX_CHARS = 300
35
+ const PRESEEDED_MIN_IMPORTANCE = 0.7
36
+ const PRESEEDED_MEMORY_DURABILITY: MemoryRecord['durability'] = 'core'
37
+ const MAX_CONVERSATION_HISTORY_MESSAGES = 24
38
+ const MAX_CONVERSATION_MESSAGE_CHARS = 1_200
39
+ const MAX_CONVERSATION_MEMORY_BLOCK_CHARS = 2_000
40
+ const MAX_CONVERSATION_ATTACHMENT_CONTEXT_CHARS = 6_000
41
+ const MAX_CONVERSATION_ASSESSMENT_CHARS = 7_000
42
+ const ONBOARDING_MEMORY_MAX_FACTS = 16
43
+ const ONBOARDING_MEMORY_EXTRACTION_PROMPT =
44
+ 'Onboarding mode is active. Extract multiple concrete startup facts from user-provided context: company mission, product capabilities, customer segments, pricing, traction, go-to-market plans, roadmap, team composition, technical stack, risks, and referenced URLs. Prefer one fact per concrete claim.'
45
+
46
+ const helperModelRuntime = createHelperModelRuntime()
47
+
48
+ const MemoryRerankOutputSchema = z.object({
49
+ sections: z.array(
50
+ z.object({
51
+ title: z.string(),
52
+ items: z.array(
53
+ z.object({
54
+ id: z.string(),
55
+ relevance: z.string().describe('Short relevance reason. Use empty string when no reason is needed.'),
56
+ }),
57
+ ),
58
+ }),
59
+ ),
60
+ })
61
+
62
+ export type MemoryRerankOutput = z.infer<typeof MemoryRerankOutputSchema>
63
+
64
+ const isRoutableAgentName = (value?: string): boolean => Boolean(value && agentRoster.includes(value))
65
+
66
+ class MemoryService {
67
+ private orgMemoryCache = new Map<string, Memory>()
68
+
69
+ private getOrCreateMemory(cacheKey: string, cache: Map<string, Memory>): Memory {
70
+ const cached = cache.get(cacheKey)
71
+ if (cached) return cached
72
+
73
+ const memory = new Memory({ createAgent: createOrgMemoryAgent }, { customPrompt: orgMemoryPrompt })
74
+
75
+ cache.set(cacheKey, memory)
76
+ aiLogger.debug`Memory client created and cached for ${cacheKey}`
77
+ return memory
78
+ }
79
+
80
+ private getOrgMemory(orgId: string): Memory {
81
+ const cacheKey = scopeId(ORG_SCOPE_PREFIX, orgId)
82
+ return this.getOrCreateMemory(cacheKey, this.orgMemoryCache)
83
+ }
84
+
85
+ private truncateCandidateText(value: string): string {
86
+ const normalized = value.replace(/\s+/g, ' ').trim()
87
+ if (normalized.length <= RERANK_CANDIDATE_MAX_CHARS) return normalized
88
+ return `${normalized.slice(0, RERANK_CANDIDATE_MAX_CHARS)}...`
89
+ }
90
+
91
+ private readNumericMetadata(metadata: Record<string, unknown>, key: string): number {
92
+ const value = metadata[key]
93
+ if (typeof value === 'number' && Number.isFinite(value)) return value
94
+ return 0
95
+ }
96
+
97
+ private buildRerankerCandidateText(candidate: MemorySearchResult): string {
98
+ const metadata = candidate.metadata
99
+ const relationCount = this.readNumericMetadata(metadata, 'relationCount')
100
+ const supportCount = this.readNumericMetadata(metadata, 'supportCount')
101
+ const contradictCount = this.readNumericMetadata(metadata, 'contradictCount')
102
+
103
+ const relatedContextRaw = metadata.relatedContext
104
+ const relatedContext = Array.isArray(relatedContextRaw)
105
+ ? relatedContextRaw
106
+ .slice(0, 3)
107
+ .map((item) => String(item).trim())
108
+ .filter((item) => item.length > 0)
109
+ : []
110
+
111
+ const contextLines: string[] = []
112
+ if (relatedContext.length > 0) {
113
+ contextLines.push(`Related context: ${relatedContext.join(' | ')}`)
114
+ }
115
+ if (relationCount > 0 || supportCount > 0 || contradictCount > 0) {
116
+ contextLines.push(
117
+ `Graph signals: relations=${relationCount}; supports=${supportCount}; contradicts=${contradictCount}`,
118
+ )
119
+ }
120
+
121
+ if (contextLines.length === 0) return candidate.content
122
+ return `${candidate.content}\n\n${contextLines.join('\n')}`
123
+ }
124
+
125
+ private normalizeConversationText(value: string, maxChars: number): string {
126
+ const normalized = value.replace(/\s+/g, ' ').trim()
127
+ if (!normalized) return ''
128
+ if (normalized.length <= maxChars) return normalized
129
+ return `${normalized.slice(0, maxChars - 3)}...`
130
+ }
131
+
132
+ private resolveAgentScopeNames(agentName?: string, agentNames: string[] = []): string[] {
133
+ const unique = new Set<string>()
134
+ if (typeof agentName === 'string' && agentName.trim()) {
135
+ unique.add(agentName.trim())
136
+ }
137
+ for (const candidate of agentNames) {
138
+ if (typeof candidate !== 'string') continue
139
+ const normalized = candidate.trim()
140
+ if (!normalized) continue
141
+ unique.add(normalized)
142
+ }
143
+ return [...unique].filter((name) => isRoutableAgentName(name))
144
+ }
145
+
146
+ private normalizePreSeededMemoryText(value: string): string {
147
+ const normalized = value.replace(/\s+/g, ' ').trim()
148
+ if (normalized.length <= PRESEEDED_MEMORY_MAX_CHARS) return normalized
149
+ return `${normalized.slice(0, PRESEEDED_MEMORY_MAX_CHARS - 3)}...`
150
+ }
151
+
152
+ private formatPreSeededMemoriesSection(memories: MemoryRecord[]): string | undefined {
153
+ const lines = memories
154
+ .map((memory) => this.normalizePreSeededMemoryText(memory.content))
155
+ .filter((line) => line.length > 0)
156
+ .map((line) => `- ${line}`)
157
+ if (lines.length === 0) return undefined
158
+
159
+ return ['<pre-seeded-memories>', ...lines, '</pre-seeded-memories>'].join('\n')
160
+ }
161
+
162
+ private buildConversationMessages(params: {
163
+ input: string
164
+ output: string
165
+ historyMessages: Array<{ role: 'user' | 'agent'; content: string; agentName?: string }>
166
+ memoryBlock?: string
167
+ attachmentContext?: string
168
+ }): { messages: Message[]; normalizedInput: string; sanitizedOutput: string } {
169
+ const normalizedInput = this.normalizeConversationText(params.input, MAX_CONVERSATION_MESSAGE_CHARS)
170
+ const sanitizedOutput = this.normalizeConversationText(
171
+ sanitizeAgentOutputForMemory(params.output),
172
+ MAX_CONVERSATION_MESSAGE_CHARS,
173
+ )
174
+ if (!normalizedInput || !sanitizedOutput) {
175
+ return { messages: [], normalizedInput, sanitizedOutput }
176
+ }
177
+
178
+ const messages: Message[] = []
179
+
180
+ const normalizedHistory: Message[] = params.historyMessages
181
+ .map((message): Message | null => {
182
+ const role: Message['role'] = message.role === 'agent' ? 'agent' : 'user'
183
+ const normalized = this.normalizeConversationText(
184
+ role === 'agent' ? sanitizeAgentOutputForMemory(message.content) : message.content,
185
+ MAX_CONVERSATION_MESSAGE_CHARS,
186
+ )
187
+ if (!normalized) return null
188
+ return { role, content: normalized }
189
+ })
190
+ .filter((message): message is Message => message !== null)
191
+ .slice(-MAX_CONVERSATION_HISTORY_MESSAGES)
192
+
193
+ messages.push(...normalizedHistory)
194
+
195
+ const normalizedMemoryBlock = this.normalizeConversationText(
196
+ params.memoryBlock ?? '',
197
+ MAX_CONVERSATION_MEMORY_BLOCK_CHARS,
198
+ )
199
+ if (normalizedMemoryBlock) {
200
+ messages.push({ role: 'user', content: `Workstream memory block:\n${normalizedMemoryBlock}` })
201
+ }
202
+
203
+ const normalizedAttachmentContext = this.normalizeConversationText(
204
+ params.attachmentContext ?? '',
205
+ MAX_CONVERSATION_ATTACHMENT_CONTEXT_CHARS,
206
+ )
207
+ if (normalizedAttachmentContext) {
208
+ messages.push({ role: 'user', content: `Attachment context:\n${normalizedAttachmentContext}` })
209
+ }
210
+
211
+ messages.push({ role: 'user', content: normalizedInput })
212
+ messages.push({ role: 'agent', content: sanitizedOutput })
213
+
214
+ return { messages, normalizedInput, sanitizedOutput }
215
+ }
216
+
217
+ private buildConversationAssessmentInput(messages: Message[]): string {
218
+ const lines = messages.map((message, index) => `[${message.role.toUpperCase()} #${index + 1}] ${message.content}`)
219
+ return this.normalizeConversationText(lines.join('\n\n'), MAX_CONVERSATION_ASSESSMENT_CHARS)
220
+ }
221
+
222
+ private async rerankCandidates(
223
+ query: string,
224
+ candidates: MemorySearchResult[],
225
+ maxItems: number,
226
+ ): Promise<MemoryRerankOutput | null> {
227
+ if (candidates.length === 0) return null
228
+
229
+ try {
230
+ return await helperModelRuntime.generateHelperStructured({
231
+ tag: 'memory-reranker',
232
+ createAgent: createMemoryRerankerAgent,
233
+ defaultSystemPrompt: memoryRerankerPrompt,
234
+ messages: [
235
+ {
236
+ role: 'user',
237
+ content: JSON.stringify({
238
+ query,
239
+ maxItems,
240
+ candidates: candidates.map((candidate) => ({
241
+ id: candidate.id,
242
+ text: this.truncateCandidateText(this.buildRerankerCandidateText(candidate)),
243
+ score: candidate.score,
244
+ })),
245
+ }),
246
+ },
247
+ ],
248
+ schema: MemoryRerankOutputSchema,
249
+ })
250
+ } catch (error) {
251
+ aiLogger.warn`Memory reranker failed: ${error}`
252
+ return null
253
+ }
254
+ }
255
+
256
+ private async rerankCandidatesMultiScope(
257
+ query: string,
258
+ scopedCandidates: Array<{ scopeTag: string; candidates: MemorySearchResult[] }>,
259
+ maxItems: number,
260
+ ): Promise<MemoryRerankOutput | null> {
261
+ const flattened = scopedCandidates.flatMap(({ scopeTag, candidates }) =>
262
+ candidates.map((candidate) => ({ ...candidate, scopeTag })),
263
+ )
264
+ if (flattened.length === 0 || flattened.length <= maxItems) return null
265
+
266
+ try {
267
+ return await helperModelRuntime.generateHelperStructured({
268
+ tag: 'memory-reranker-multi-scope',
269
+ createAgent: createMemoryRerankerAgent,
270
+ defaultSystemPrompt: memoryRerankerPrompt,
271
+ messages: [
272
+ {
273
+ role: 'user',
274
+ content: JSON.stringify({
275
+ query,
276
+ maxItems,
277
+ candidates: flattened.map((candidate) => ({
278
+ id: candidate.id,
279
+ text: this.truncateCandidateText(this.buildRerankerCandidateText(candidate)),
280
+ score: candidate.score,
281
+ scope: candidate.scopeTag,
282
+ })),
283
+ }),
284
+ },
285
+ ],
286
+ schema: MemoryRerankOutputSchema,
287
+ })
288
+ } catch (error) {
289
+ aiLogger.warn`Multi-scope memory reranker failed: ${error}`
290
+ return null
291
+ }
292
+ }
293
+
294
+ private async searchMemories({
295
+ query,
296
+ memory,
297
+ scopeId,
298
+ memoryType,
299
+ }: {
300
+ query: string
301
+ memory: Memory
302
+ scopeId: string
303
+ memoryType: MemoryType
304
+ }): Promise<string> {
305
+ const limit = env.MEMORY_SEARCH_K
306
+ const candidateLimit = getCandidateLimit(limit)
307
+
308
+ const candidates = await memory.searchCandidates(query, { scopeId, limit: candidateLimit, memoryType })
309
+
310
+ aiLogger.debug`Memory search candidates (scopeId: ${scopeId}, candidates: ${candidates.length})`
311
+
312
+ if (candidates.length === 0) {
313
+ return 'No stored memories.'
314
+ }
315
+
316
+ if (candidates.length <= limit) {
317
+ aiLogger.debug`Skipping reranking (candidates: ${candidates.length} <= limit: ${limit})`
318
+ return formatMemoryResults(candidates)
319
+ }
320
+
321
+ const reranked = await this.rerankCandidates(query, candidates, limit)
322
+ return formatRerankedResults(reranked, candidates, limit)
323
+ }
324
+
325
+ async searchOrganizationMemories(orgId: string, query: string): Promise<string> {
326
+ const orgScopeId = scopeId(ORG_SCOPE_PREFIX, orgId)
327
+ aiLogger.debug`Organization memory search requested (orgId: ${orgId}, scopeId: ${orgScopeId}, queryLength: ${query.length})`
328
+ const memory = this.getOrgMemory(orgId)
329
+
330
+ try {
331
+ const results = await this.searchMemories({ query, memory, scopeId: orgScopeId, memoryType: ORG_MEMORY_TYPE })
332
+ aiLogger.debug`Organization memory search completed (resultLength: ${results.length}, preview: ${results.slice(0, 100)})`
333
+ return results
334
+ } catch (error: unknown) {
335
+ const normalizedError = error instanceof Error ? error : new Error(String(error))
336
+ aiLogger.error`Organization memory search failed: ${normalizedError}`
337
+ throw normalizedError
338
+ }
339
+ }
340
+
341
+ async getStaleMemories(orgId: string): Promise<string> {
342
+ const orgScopeId = scopeId(ORG_SCOPE_PREFIX, orgId)
343
+ const memory = this.getOrgMemory(orgId)
344
+ try {
345
+ const stale = await memory.getStaleMemories(orgScopeId)
346
+ if (stale.length === 0) return ''
347
+ const items = stale.map((m) => `- [NEEDS REVIEW] ${m.content}`).join('\n')
348
+ return `Memories flagged for review (parent fact was superseded):\n${items}`
349
+ } catch {
350
+ return ''
351
+ }
352
+ }
353
+
354
+ async searchOrganizationMemoriesRaw(
355
+ orgId: string,
356
+ query: string,
357
+ options?: { fastMode?: boolean; limit?: number },
358
+ ): Promise<string> {
359
+ const orgScopeId = scopeId(ORG_SCOPE_PREFIX, orgId)
360
+ aiLogger.info`[MEMORY_DEBUG] searchOrganizationMemoriesRaw - orgId: "${orgId}", scopeId: "${orgScopeId}"`
361
+ const memory = this.getOrgMemory(orgId)
362
+ const fastMode = options?.fastMode ?? true
363
+ const limit = options?.limit ?? (fastMode ? Math.min(env.MEMORY_SEARCH_K, 4) : env.MEMORY_SEARCH_K)
364
+
365
+ try {
366
+ const candidates = await memory.searchCandidates(query, {
367
+ scopeId: orgScopeId,
368
+ limit,
369
+ memoryType: ORG_MEMORY_TYPE,
370
+ fastMode,
371
+ includeNeighborContext: !fastMode,
372
+ })
373
+ aiLogger.debug`Organization memory search (raw) completed (candidates: ${candidates.length})`
374
+ return formatMemoryResults(candidates)
375
+ } catch (error: unknown) {
376
+ const normalizedError = error instanceof Error ? error : new Error(String(error))
377
+ aiLogger.error`Organization memory search (raw) failed: ${normalizedError}`
378
+ throw normalizedError
379
+ }
380
+ }
381
+
382
+ async searchAgentMemories(orgId: string, agentName: string, query: string): Promise<string> {
383
+ if (!isRoutableAgentName(agentName)) {
384
+ aiLogger.debug`Agent memory search skipped - invalid agentName: ${agentName}`
385
+ return 'No stored memories.'
386
+ }
387
+
388
+ const scoped = agentScopeId(orgId, agentName)
389
+ aiLogger.debug`Agent memory search requested (orgId: ${orgId}, agentName: ${agentName}, scopeId: ${scoped}, queryLength: ${query.length})`
390
+ const memory = this.getOrgMemory(orgId)
391
+
392
+ try {
393
+ const results = await this.searchMemories({ query, memory, scopeId: scoped, memoryType: ORG_MEMORY_TYPE })
394
+ aiLogger.debug`Agent memory search completed (agentName: ${agentName}, resultLength: ${results.length}, preview: ${results.slice(0, 100)})`
395
+ return results
396
+ } catch (error: unknown) {
397
+ const normalizedError = error instanceof Error ? error : new Error(String(error))
398
+ aiLogger.error`Agent memory search failed: ${normalizedError}`
399
+ throw normalizedError
400
+ }
401
+ }
402
+
403
+ async searchOrgMemoriesForAgent(orgId: string, agentName: string, query: string): Promise<string> {
404
+ if (!isRoutableAgentName(agentName)) {
405
+ return await this.searchOrganizationMemories(orgId, query)
406
+ }
407
+
408
+ const [agentResult, orgResult] = await Promise.all([
409
+ this.searchAgentMemories(orgId, agentName, query),
410
+ this.searchOrganizationMemories(orgId, query),
411
+ ])
412
+
413
+ return `Agent memory (${agentName}):\n${agentResult}\n\nGlobal org memory:\n${orgResult}`
414
+ }
415
+
416
+ async getTopMemories(params: { orgId: string; agentName?: string; limit?: number }): Promise<string | undefined> {
417
+ const orgMemory = this.getOrgMemory(params.orgId)
418
+ const orgScopeId = scopeId(ORG_SCOPE_PREFIX, params.orgId)
419
+ const requestedLimit = params.limit ?? PRESEEDED_MEMORY_LIMIT
420
+ const limit = Math.max(1, Math.min(requestedLimit, PRESEEDED_MEMORY_LIMIT))
421
+
422
+ const orgRequest = orgMemory.listTopMemories({
423
+ scopeId: orgScopeId,
424
+ limit,
425
+ memoryType: ORG_MEMORY_TYPE,
426
+ durability: PRESEEDED_MEMORY_DURABILITY,
427
+ minImportance: PRESEEDED_MIN_IMPORTANCE,
428
+ })
429
+
430
+ const agentRequest =
431
+ params.agentName && isRoutableAgentName(params.agentName)
432
+ ? orgMemory.listTopMemories({
433
+ scopeId: agentScopeId(params.orgId, params.agentName),
434
+ limit,
435
+ memoryType: ORG_MEMORY_TYPE,
436
+ durability: PRESEEDED_MEMORY_DURABILITY,
437
+ minImportance: PRESEEDED_MIN_IMPORTANCE,
438
+ })
439
+ : Promise.resolve([] as MemoryRecord[])
440
+
441
+ const [orgTopMemories, agentTopMemories] = await Promise.all([orgRequest, agentRequest])
442
+ const combined = [...agentTopMemories, ...orgTopMemories].sort((left, right) => {
443
+ if (right.importance !== left.importance) return right.importance - left.importance
444
+ if (right.accessCount !== left.accessCount) return right.accessCount - left.accessCount
445
+ const rightLastAccess = right.lastAccessedAt?.getTime() ?? 0
446
+ const leftLastAccess = left.lastAccessedAt?.getTime() ?? 0
447
+ if (rightLastAccess !== leftLastAccess) return rightLastAccess - leftLastAccess
448
+ return right.createdAt.getTime() - left.createdAt.getTime()
449
+ })
450
+
451
+ const deduped: MemoryRecord[] = []
452
+ const seen = new Set<string>()
453
+ for (const memory of combined) {
454
+ const normalizedKey = memory.content.replace(/\s+/g, ' ').trim().toLowerCase()
455
+ if (!normalizedKey || seen.has(normalizedKey)) continue
456
+ seen.add(normalizedKey)
457
+ deduped.push(memory)
458
+ if (deduped.length >= limit) break
459
+ }
460
+
461
+ return this.formatPreSeededMemoriesSection(deduped)
462
+ }
463
+
464
+ async searchAllMemoriesBatched({
465
+ orgId,
466
+ agentName,
467
+ query,
468
+ fastMode = false,
469
+ allowMultiScopeRerank = true,
470
+ }: {
471
+ orgId: string
472
+ agentName?: string
473
+ query: string
474
+ fastMode?: boolean
475
+ allowMultiScopeRerank?: boolean
476
+ }): Promise<string> {
477
+ const limit = Math.min(env.MEMORY_SEARCH_K, MAX_MEMORY_RESULTS_PER_SCOPE)
478
+ const candidateLimit = fastMode ? limit : getCandidateLimit(limit)
479
+ const orgScopeId = scopeId(ORG_SCOPE_PREFIX, orgId)
480
+ const orgMemory = this.getOrgMemory(orgId)
481
+
482
+ const retrievalTasks = [
483
+ {
484
+ scopeTag: 'org',
485
+ retrieve: async () =>
486
+ await orgMemory.searchCandidates(query, {
487
+ scopeId: orgScopeId,
488
+ limit: candidateLimit,
489
+ memoryType: ORG_MEMORY_TYPE,
490
+ fastMode,
491
+ includeNeighborContext: !fastMode,
492
+ }),
493
+ },
494
+ ]
495
+
496
+ if (isRoutableAgentName(agentName)) {
497
+ const agentScoped = agentScopeId(orgId, agentName as string)
498
+ retrievalTasks.push({
499
+ scopeTag: `agent:${agentName}`,
500
+ retrieve: async () =>
501
+ await orgMemory.searchCandidates(query, {
502
+ scopeId: agentScoped,
503
+ limit: candidateLimit,
504
+ memoryType: ORG_MEMORY_TYPE,
505
+ fastMode,
506
+ includeNeighborContext: !fastMode,
507
+ }),
508
+ })
509
+ }
510
+
511
+ const results = await executeScopedRetrieval<MemorySearchResult>(retrievalTasks)
512
+
513
+ const totalCandidates = countScopedRetrievalCandidates(results)
514
+ aiLogger.debug`Batched memory search candidates (scopes: ${results.length}, total: ${totalCandidates})`
515
+
516
+ if (totalCandidates === 0) {
517
+ return 'No stored memories.'
518
+ }
519
+
520
+ if (fastMode || !allowMultiScopeRerank) {
521
+ const candidatesByScopeTag = scopedRetrievalToMap(results)
522
+ return this.formatBatchedResults(null, candidatesByScopeTag, limit, agentName)
523
+ }
524
+
525
+ const reranked = await this.rerankCandidatesMultiScope(query, results, limit)
526
+ const candidatesByScopeTag = scopedRetrievalToMap(results)
527
+
528
+ return this.formatBatchedResults(reranked, candidatesByScopeTag, limit, agentName)
529
+ }
530
+
531
+ private formatBatchedResults(
532
+ reranked: MemoryRerankOutput | null,
533
+ candidatesByScopeTag: Map<string, MemorySearchResult[]>,
534
+ limit: number,
535
+ agentName?: string,
536
+ ): string {
537
+ if (reranked && reranked.sections.length > 0) {
538
+ const allCandidates = Array.from(candidatesByScopeTag.values()).flat()
539
+ return formatRerankedResults(reranked, allCandidates, limit)
540
+ }
541
+
542
+ const sections: string[] = []
543
+
544
+ if (agentName) {
545
+ const agentCandidates = candidatesByScopeTag.get(`agent:${agentName}`) ?? []
546
+ if (agentCandidates.length > 0) {
547
+ sections.push(`Agent memory (${agentName}):\n${formatMemoryResults(agentCandidates.slice(0, limit))}`)
548
+ } else {
549
+ sections.push(`Agent memory (${agentName}):\nNo stored memories.`)
550
+ }
551
+ }
552
+
553
+ const orgCandidates = candidatesByScopeTag.get('org') ?? []
554
+ if (orgCandidates.length > 0) {
555
+ sections.push(
556
+ `${agentName ? 'Global org memory' : 'Organization memory'}:\n${formatMemoryResults(orgCandidates.slice(0, limit))}`,
557
+ )
558
+ } else {
559
+ sections.push(`${agentName ? 'Global org memory' : 'Organization memory'}:\nNo stored memories.`)
560
+ }
561
+
562
+ return sections.join('\n\n')
563
+ }
564
+
565
+ async createOrganizationMemory({
566
+ orgId,
567
+ content,
568
+ memoryType,
569
+ metadata,
570
+ importance,
571
+ }: {
572
+ orgId: string
573
+ content: string
574
+ memoryType: MemoryType
575
+ metadata?: Record<string, unknown>
576
+ importance?: number
577
+ }): Promise<string> {
578
+ const orgScopeId = scopeId(ORG_SCOPE_PREFIX, orgId)
579
+ aiLogger.info`[MEMORY_DEBUG] createOrganizationMemory - orgId: "${orgId}", scopeId: "${orgScopeId}", content preview: "${content.slice(0, 50)}"`
580
+ const memory = this.getOrgMemory(orgId)
581
+ try {
582
+ return await memory.insert(content, {
583
+ scopeId: orgScopeId,
584
+ memoryType,
585
+ importance: importance ?? 1,
586
+ metadata: { orgId, ...metadata },
587
+ })
588
+ } catch (error) {
589
+ if (isUniqueIndexConflict(error, 'memoryHashIdx')) {
590
+ aiLogger.debug`Organization memory already exists (hash conflict)`
591
+ return ''
592
+ }
593
+ throw error
594
+ }
595
+ }
596
+
597
+ async createAgentMemory({
598
+ orgId,
599
+ agentName,
600
+ content,
601
+ memoryType,
602
+ metadata,
603
+ importance,
604
+ }: {
605
+ orgId: string
606
+ agentName: string
607
+ content: string
608
+ memoryType: MemoryType
609
+ metadata?: Record<string, unknown>
610
+ importance?: number
611
+ }): Promise<string> {
612
+ if (!isRoutableAgentName(agentName)) {
613
+ throw new Error(`Invalid agentName for agent memory: ${agentName}`)
614
+ }
615
+
616
+ const memory = this.getOrgMemory(orgId)
617
+ const scoped = agentScopeId(orgId, agentName)
618
+ try {
619
+ return await memory.insert(content, {
620
+ scopeId: scoped,
621
+ memoryType,
622
+ importance: importance ?? 1,
623
+ metadata: { orgId, agentName, memoryScope: 'agent', ...metadata },
624
+ })
625
+ } catch (error) {
626
+ if (isUniqueIndexConflict(error, 'memoryHashIdx')) {
627
+ aiLogger.debug`Agent memory already exists (hash conflict)`
628
+ return ''
629
+ }
630
+ throw error
631
+ }
632
+ }
633
+
634
+ async updateOrganizationMemoryById({
635
+ orgId,
636
+ memoryId,
637
+ content,
638
+ }: {
639
+ orgId: string
640
+ memoryId: string
641
+ content: string
642
+ }): Promise<void> {
643
+ const memory = this.getOrgMemory(orgId)
644
+ await memory.updateMemory(memoryId, content)
645
+ }
646
+
647
+ async addExtractedFactsToScopes(params: {
648
+ orgId: string
649
+ facts: ExtractedFact[]
650
+ source: string
651
+ sourceMetadata?: Record<string, unknown>
652
+ agentNames?: string[]
653
+ acquireLock?: boolean
654
+ }): Promise<void> {
655
+ if (params.facts.length === 0) return
656
+
657
+ const orgMemory = this.getOrgMemory(params.orgId)
658
+ const scopes: AddOptions[] = [
659
+ {
660
+ scopeId: scopeId(ORG_SCOPE_PREFIX, params.orgId),
661
+ memoryType: ORG_MEMORY_TYPE,
662
+ metadata: { orgId: params.orgId, source: params.source, ...params.sourceMetadata },
663
+ },
664
+ ]
665
+
666
+ for (const scopedAgentName of this.resolveAgentScopeNames(undefined, params.agentNames ?? [])) {
667
+ scopes.push({
668
+ scopeId: agentScopeId(params.orgId, scopedAgentName),
669
+ memoryType: ORG_MEMORY_TYPE,
670
+ metadata: {
671
+ orgId: params.orgId,
672
+ source: params.source,
673
+ ...params.sourceMetadata,
674
+ agentName: scopedAgentName,
675
+ memoryScope: 'agent',
676
+ },
677
+ })
678
+ }
679
+
680
+ const preparedUpdates = await orgMemory.prepareFactsToScopes(params.facts, scopes)
681
+ if (preparedUpdates.length === 0) return
682
+
683
+ if (params.acquireLock === false) {
684
+ await orgMemory.applyPreparedScopeUpdates(preparedUpdates)
685
+ } else {
686
+ await withOrgMemoryLock(params.orgId, async () => {
687
+ await orgMemory.applyPreparedScopeUpdates(preparedUpdates)
688
+ })
689
+ }
690
+ }
691
+
692
+ async addConversationMemories({
693
+ orgId,
694
+ input,
695
+ output,
696
+ sourceId,
697
+ onboardStatus,
698
+ agentName,
699
+ historyMessages = [],
700
+ memoryBlock,
701
+ attachmentContext,
702
+ agentNames = [],
703
+ }: {
704
+ orgId: string
705
+ input: string
706
+ output: string
707
+ sourceId?: string
708
+ onboardStatus?: string
709
+ agentName?: string
710
+ historyMessages?: Array<{ role: 'user' | 'agent'; content: string; agentName?: string }>
711
+ memoryBlock?: string
712
+ attachmentContext?: string
713
+ agentNames?: string[]
714
+ }): Promise<void> {
715
+ const { messages, normalizedInput, sanitizedOutput } = this.buildConversationMessages({
716
+ input,
717
+ output,
718
+ historyMessages,
719
+ memoryBlock,
720
+ attachmentContext,
721
+ })
722
+
723
+ if (!normalizedInput || !sanitizedOutput || messages.length === 0) {
724
+ aiLogger.debug`Skipping memory add - empty input or output`
725
+ return
726
+ }
727
+
728
+ const orgScopeId = scopeId(ORG_SCOPE_PREFIX, orgId)
729
+ aiLogger.info`[MEMORY_DEBUG] addConversationMemories - orgId: "${orgId}", scopeId: "${orgScopeId}", sourceId: ${sourceId ?? 'none'}`
730
+
731
+ const orgMemory = this.getOrgMemory(orgId)
732
+ let assessedImportance: number | undefined
733
+ let assessmentMetadata: Record<string, unknown> | undefined
734
+
735
+ try {
736
+ const assessmentInput = this.buildConversationAssessmentInput(messages)
737
+ if (assessmentInput) {
738
+ const assessment = await assessMemoryImportance({
739
+ content: assessmentInput,
740
+ targetScope: 'global',
741
+ tag: 'memory-conversation-assessment',
742
+ })
743
+ if (assessment.classification === 'transient' && assessment.durability === 'ephemeral') {
744
+ aiLogger.debug`Skipping transient conversation memory`
745
+ return
746
+ }
747
+ assessedImportance = Math.round(clampMemoryImportance(assessment.importance) * 100) / 100
748
+ assessmentMetadata = {
749
+ importanceSource: 'model_assessed',
750
+ durability: assessment.durability,
751
+ classification: assessment.classification,
752
+ rationale: assessment.rationale,
753
+ }
754
+ }
755
+ } catch (error) {
756
+ aiLogger.warn`Conversation memory assessment failed; continuing with default scoring: ${error}`
757
+ }
758
+
759
+ const scopes: AddOptions[] = [
760
+ {
761
+ scopeId: orgScopeId,
762
+ memoryType: ORG_MEMORY_TYPE,
763
+ ...(typeof assessedImportance === 'number' ? { importance: assessedImportance } : {}),
764
+ metadata: { orgId, source: 'chat', ...(sourceId ? { sourceId } : {}), ...assessmentMetadata },
765
+ },
766
+ ]
767
+
768
+ for (const scopedAgentName of this.resolveAgentScopeNames(agentName, agentNames)) {
769
+ const agentId = agentScopeId(orgId, scopedAgentName)
770
+ scopes.push({
771
+ scopeId: agentId,
772
+ memoryType: ORG_MEMORY_TYPE,
773
+ ...(typeof assessedImportance === 'number' ? { importance: assessedImportance } : {}),
774
+ metadata: {
775
+ orgId,
776
+ agentName: scopedAgentName,
777
+ memoryScope: 'agent',
778
+ source: 'chat',
779
+ ...(sourceId ? { sourceId } : {}),
780
+ ...assessmentMetadata,
781
+ },
782
+ })
783
+ }
784
+
785
+ const onboardingActive = onboardStatus !== undefined && onboardStatus !== 'completed'
786
+ const extractionConfig = onboardingActive
787
+ ? { maxFacts: ONBOARDING_MEMORY_MAX_FACTS, customPrompt: ONBOARDING_MEMORY_EXTRACTION_PROMPT }
788
+ : undefined
789
+ const extractedFacts = await orgMemory.extractFactsFromMessages(messages, extractionConfig)
790
+ if (extractedFacts.length === 0) return
791
+ const preparedUpdates = await orgMemory.prepareFactsToScopes(extractedFacts, scopes)
792
+ if (preparedUpdates.length === 0) return
793
+
794
+ try {
795
+ await withOrgMemoryLock(orgId, async () => {
796
+ await orgMemory.applyPreparedScopeUpdates(preparedUpdates)
797
+ })
798
+ aiLogger.debug`Conversation memories added to ${scopes.length} scope(s) from ${messages.length} message(s)`
799
+ } catch (error: unknown) {
800
+ const normalizedError = error instanceof Error ? error : new Error(String(error))
801
+ aiLogger.error`Memory write failed: ${normalizedError}`
802
+ throw normalizedError
803
+ }
804
+ }
805
+ }
806
+
807
+ export const memoryService = new MemoryService()