@lota-sdk/core 0.1.13 → 0.1.15

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 (95) hide show
  1. package/package.json +5 -5
  2. package/src/ai/embedding-cache.ts +7 -6
  3. package/src/ai/index.ts +1 -0
  4. package/src/bifrost/bifrost.ts +12 -7
  5. package/src/config/agent-defaults.ts +1 -1
  6. package/src/config/logger.ts +7 -9
  7. package/src/{runtime.ts → create-runtime.ts} +6 -6
  8. package/src/db/cursor-pagination.ts +1 -1
  9. package/src/db/memory-store.ts +10 -6
  10. package/src/db/memory.ts +6 -4
  11. package/src/db/schema-fingerprint.ts +1 -0
  12. package/src/db/service.ts +45 -51
  13. package/src/db/startup.ts +3 -3
  14. package/src/index.ts +1 -1
  15. package/src/queues/context-compaction.queue.ts +4 -8
  16. package/src/queues/document-processor.queue.ts +7 -7
  17. package/src/queues/memory-consolidation.queue.ts +7 -8
  18. package/src/queues/post-chat-memory.queue.ts +2 -6
  19. package/src/queues/recent-activity-title-refinement.queue.ts +2 -6
  20. package/src/queues/regular-chat-memory-digest.queue.ts +4 -7
  21. package/src/queues/skill-extraction.queue.ts +4 -7
  22. package/src/queues/workstream-title-generation.queue.ts +2 -6
  23. package/src/redis/connection.ts +6 -3
  24. package/src/redis/index.ts +1 -0
  25. package/src/redis/org-memory-lock.ts +1 -1
  26. package/src/redis/redis-lease-lock.ts +41 -8
  27. package/src/runtime/agent-stream-helpers.ts +2 -1
  28. package/src/runtime/context-compaction-constants.ts +1 -1
  29. package/src/runtime/context-compaction-runtime.ts +6 -4
  30. package/src/runtime/context-compaction.ts +19 -38
  31. package/src/runtime/execution-plan.ts +2 -2
  32. package/src/runtime/helper-model.ts +3 -1
  33. package/src/runtime/index.ts +12 -1
  34. package/src/runtime/memory-block.ts +3 -2
  35. package/src/runtime/memory-pipeline.ts +24 -5
  36. package/src/runtime/plugin-types.ts +1 -1
  37. package/src/runtime/runtime-extensions.ts +89 -13
  38. package/src/runtime/title-helpers.ts +11 -2
  39. package/src/runtime/workstream-chat-helpers.ts +5 -6
  40. package/src/runtime/workstream-routing-policy.ts +0 -30
  41. package/src/runtime/workstream-state.ts +17 -7
  42. package/src/services/attachment.service.ts +1 -1
  43. package/src/services/context-compaction.service.ts +3 -3
  44. package/src/services/document-chunk.service.ts +37 -32
  45. package/src/services/execution-plan.service.ts +2 -0
  46. package/src/services/learned-skill.service.ts +6 -10
  47. package/src/services/{memory.utils.ts → memory-utils.ts} +4 -8
  48. package/src/services/memory.service.ts +21 -18
  49. package/src/services/organization-member.service.ts +1 -1
  50. package/src/services/plan-artifact.service.ts +1 -0
  51. package/src/services/plan-executor.service.ts +2 -18
  52. package/src/services/plan-helpers.ts +15 -0
  53. package/src/services/plan-validator.service.ts +3 -18
  54. package/src/services/recent-activity-title.service.ts +3 -10
  55. package/src/services/recent-activity.service.ts +6 -12
  56. package/src/services/workstream-message.service.ts +26 -16
  57. package/src/services/workstream-title.service.ts +1 -9
  58. package/src/services/{workstream-turn-preparation.ts → workstream-turn-preparation.service.ts} +401 -314
  59. package/src/services/workstream-turn.ts +2 -2
  60. package/src/services/workstream.service.ts +22 -10
  61. package/src/services/workstream.types.ts +7 -16
  62. package/src/storage/attachment-storage.service.ts +4 -4
  63. package/src/storage/{attachments.utils.ts → attachment-utils.ts} +1 -4
  64. package/src/storage/index.ts +2 -2
  65. package/src/system-agents/{context-compacter.agent.ts → context-compaction.agent.ts} +4 -4
  66. package/src/system-agents/delegated-agent-factory.ts +3 -2
  67. package/src/system-agents/index.ts +8 -0
  68. package/src/system-agents/memory-reranker.agent.ts +1 -1
  69. package/src/system-agents/memory.agent.ts +1 -1
  70. package/src/system-agents/recent-activity-title-refiner.agent.ts +1 -1
  71. package/src/tools/execution-plan.tool.ts +6 -2
  72. package/src/tools/fetch-webpage.tool.ts +20 -18
  73. package/src/tools/index.ts +2 -2
  74. package/src/tools/read-file-parts.tool.ts +1 -1
  75. package/src/tools/search-web.tool.ts +18 -15
  76. package/src/tools/{search-tools.ts → search.tool.ts} +1 -1
  77. package/src/tools/team-think.tool.ts +9 -5
  78. package/src/tools/{tool-contract.ts → tool-contracts.ts} +9 -2
  79. package/src/utils/async.ts +1 -1
  80. package/src/utils/errors.ts +15 -0
  81. package/src/utils/hono-error-handler.ts +1 -2
  82. package/src/utils/index.ts +10 -2
  83. package/src/utils/string.ts +14 -0
  84. package/src/workers/bootstrap.ts +2 -2
  85. package/src/workers/memory-consolidation.worker.ts +12 -12
  86. package/src/workers/regular-chat-memory-digest.helpers.ts +2 -7
  87. package/src/workers/regular-chat-memory-digest.runner.ts +9 -103
  88. package/src/workers/skill-extraction.runner.ts +7 -101
  89. package/src/workers/utils/file-section-chunker.ts +5 -3
  90. package/src/workers/utils/workstream-message-query.ts +106 -0
  91. package/src/workers/worker-utils.ts +4 -0
  92. package/src/runtime/retrieval-pipeline.ts +0 -3
  93. package/src/utils/error.ts +0 -10
  94. /package/src/services/{context-compaction-runtime.ts → context-compaction-runtime.singleton.ts} +0 -0
  95. /package/src/storage/{attachments.types.ts → attachment-types.ts} +0 -0
@@ -36,36 +36,6 @@ export function uniqueMentionOrder(message: string): string[] {
36
36
  return ordered
37
37
  }
38
38
 
39
- const GTM_STRONG_INTENT_PATTERNS: RegExp[] = [
40
- /\bgo[-\s]?to[-\s]?market\b/i,
41
- /\bgtm\b/i,
42
- /\bcontent\s+marketing\b/i,
43
- /\bcontent\s+strategy\b/i,
44
- /\bdemand\s+generation\b/i,
45
- /\bdistribution\s+strategy\b/i,
46
- /\bpositioning\s+strategy\b/i,
47
- /\blaunch\s+strategy\b/i,
48
- /\blaunch\s+plan\b/i,
49
- ]
50
-
51
- const GTM_WEAK_INTENT_PATTERNS: RegExp[] = [/\blaunch\b/i, /\bcommunity\b/i, /\bdistribution\b/i, /\bpositioning\b/i]
52
-
53
- export function isGtmIntentMessage(message: string): boolean {
54
- const text = message.trim()
55
- if (!text) return false
56
-
57
- if (GTM_STRONG_INTENT_PATTERNS.some((pattern) => pattern.test(text))) {
58
- return true
59
- }
60
-
61
- let weakMatches = 0
62
- for (const pattern of GTM_WEAK_INTENT_PATTERNS) {
63
- if (pattern.test(text)) weakMatches += 1
64
- if (weakMatches >= 2) return true
65
- }
66
- return false
67
- }
68
-
69
39
  const HIGH_IMPACT_CLASS_PATTERNS: Array<{ className: HighImpactResponseClass; patterns: RegExp[] }> = [
70
40
  {
71
41
  className: 'architecture-recommendation',
@@ -238,13 +238,23 @@ export interface CompactionOutput {
238
238
  stateDelta: WorkstreamStateDelta
239
239
  }
240
240
 
241
- export const WORKSTREAM_STATE_MAX_KEY_DECISIONS = 8
242
- export const WORKSTREAM_STATE_MAX_ACTIVE_CONSTRAINTS = 6
243
- export const WORKSTREAM_STATE_MAX_TASKS = 10
244
- export const WORKSTREAM_STATE_MAX_OPEN_QUESTIONS = 5
245
- export const WORKSTREAM_STATE_MAX_RISKS = 5
246
- export const WORKSTREAM_STATE_MAX_ARTIFACTS = 10
247
- export const WORKSTREAM_STATE_MAX_AGENT_CONTRIBUTIONS = 6
241
+ const WORKSTREAM_STATE_MAX_KEY_DECISIONS = 8
242
+ const WORKSTREAM_STATE_MAX_ACTIVE_CONSTRAINTS = 6
243
+ const WORKSTREAM_STATE_MAX_TASKS = 10
244
+ const WORKSTREAM_STATE_MAX_OPEN_QUESTIONS = 5
245
+ const WORKSTREAM_STATE_MAX_RISKS = 5
246
+ const WORKSTREAM_STATE_MAX_ARTIFACTS = 10
247
+ const WORKSTREAM_STATE_MAX_AGENT_CONTRIBUTIONS = 6
248
+
249
+ export function applyWorkstreamStateCaps(state: WorkstreamState): void {
250
+ state.keyDecisions = state.keyDecisions.slice(-WORKSTREAM_STATE_MAX_KEY_DECISIONS)
251
+ state.activeConstraints = state.activeConstraints.slice(-WORKSTREAM_STATE_MAX_ACTIVE_CONSTRAINTS)
252
+ state.tasks = state.tasks.slice(-WORKSTREAM_STATE_MAX_TASKS)
253
+ state.openQuestions = state.openQuestions.slice(-WORKSTREAM_STATE_MAX_OPEN_QUESTIONS)
254
+ state.risks = state.risks.slice(-WORKSTREAM_STATE_MAX_RISKS)
255
+ state.artifacts = state.artifacts.slice(-WORKSTREAM_STATE_MAX_ARTIFACTS)
256
+ state.agentContributions = state.agentContributions.slice(-WORKSTREAM_STATE_MAX_AGENT_CONTRIBUTIONS)
257
+ }
248
258
 
249
259
  export function createEmptyWorkstreamState(now = Date.now()): WorkstreamState {
250
260
  return {
@@ -3,7 +3,7 @@ import { recordIdToString } from '../db/record-id'
3
3
  import { TABLES } from '../db/tables'
4
4
  import { attachmentStorageService } from '../storage/attachment-storage.service'
5
5
  import type { UploadedWorkstreamAttachment as SdkUploadedWorkstreamAttachment } from '../storage/attachment-storage.service'
6
- import type { MessagePartLike, ReadableUploadMetadata as SdkReadableUploadMetadata } from '../storage/attachments.types'
6
+ import type { MessagePartLike, ReadableUploadMetadata as SdkReadableUploadMetadata } from '../storage/attachment-types'
7
7
 
8
8
  export type ReadableUploadMetadata = SdkReadableUploadMetadata
9
9
 
@@ -6,9 +6,9 @@ import { recordIdToString } from '../db/record-id'
6
6
  import { databaseService } from '../db/service'
7
7
  import { TABLES } from '../db/tables'
8
8
  import { parseWorkstreamState, toStateFieldsUpdated } from '../runtime/context-compaction'
9
- import { CONTEXT_SIZE, WORKSTREAM_RAW_TAIL_MESSAGES } from '../runtime/context-compaction-constants'
9
+ import { CONTEXT_WINDOW_TOKENS, WORKSTREAM_RAW_TAIL_MESSAGES } from '../runtime/context-compaction-constants'
10
10
  import type { WorkstreamState } from '../runtime/workstream-state'
11
- import { compactMemoryBlockSummary, contextCompactionRuntime } from './context-compaction-runtime'
11
+ import { compactMemoryBlockSummary, contextCompactionRuntime } from './context-compaction-runtime.singleton'
12
12
  import { workstreamMessageService } from './workstream-message.service'
13
13
  import { WorkstreamSchema } from './workstream.types'
14
14
 
@@ -35,7 +35,7 @@ class ContextCompactionService {
35
35
  return contextCompactionRuntime.formatWorkstreamStateForPrompt(state)
36
36
  }
37
37
 
38
- estimateThreshold(contextSize = CONTEXT_SIZE): number {
38
+ estimateThreshold(contextSize = CONTEXT_WINDOW_TOKENS): number {
39
39
  return contextCompactionRuntime.estimateThreshold(contextSize)
40
40
  }
41
41
 
@@ -3,6 +3,7 @@ import { createHash } from 'node:crypto'
3
3
  import { chunkMarkdownDocument, chunkPagedDocument, chunkPlainTextDocument } from '../document/org-document-chunking'
4
4
  import type { ParsedDocumentChunk } from '../document/org-document-chunking'
5
5
  import { getDefaultEmbeddings } from '../embeddings/provider'
6
+ import { CHARS_PER_TOKEN_ESTIMATE } from '../utils/string'
6
7
 
7
8
  type DocumentChunkEmbeddings = {
8
9
  embedDocuments(documents: string[]): Promise<number[][]>
@@ -58,8 +59,10 @@ export class DocumentChunkService {
58
59
  return createHash('sha256').update(content).digest('hex')
59
60
  }
60
61
 
62
+ // Uses 4 chars/token (conservative estimate for document content which tends
63
+ // to have longer words than conversational text where 3 chars/token is used).
61
64
  estimateTokenCount(content: string): number {
62
- return Math.max(1, Math.ceil(content.length / 4))
65
+ return Math.max(1, Math.ceil(content.length / (CHARS_PER_TOKEN_ESTIMATE + 1)))
63
66
  }
64
67
 
65
68
  async embedQuery(query: string): Promise<number[]> {
@@ -95,37 +98,39 @@ export class DocumentChunkService {
95
98
 
96
99
  await params.archive(staleVersionRows)
97
100
 
98
- for (const [index, chunk] of params.chunks.entries()) {
99
- const contentHash = this.hashContent(chunk.content)
100
- const existingRow = existingByChunkKey.get(chunk.chunkKey)
101
- const payload = params.buildPayload({
102
- chunk,
103
- embedding: embeddings[index] ?? [],
104
- contentHash,
105
- tokenEstimate: this.estimateTokenCount(chunk.content),
106
- })
107
-
108
- seenChunkKeys.add(chunk.chunkKey)
109
-
110
- if (!existingRow) {
111
- await params.create(payload)
112
- continue
113
- }
114
-
115
- const current = params.selectShape(existingRow)
116
- const hasChanged =
117
- current.contentHash !== contentHash ||
118
- current.chunkIndex !== chunk.chunkIndex ||
119
- (current.sectionPath ?? null) !== (chunk.sectionPath ?? null) ||
120
- (current.pageStart ?? null) !== (chunk.pageStart ?? null) ||
121
- (current.pageEnd ?? null) !== (chunk.pageEnd ?? null)
122
-
123
- if (!hasChanged) {
124
- continue
125
- }
126
-
127
- await params.update(existingRow, payload)
128
- }
101
+ await Promise.all(
102
+ params.chunks.map(async (chunk, index) => {
103
+ const contentHash = this.hashContent(chunk.content)
104
+ const existingRow = existingByChunkKey.get(chunk.chunkKey)
105
+ const payload = params.buildPayload({
106
+ chunk,
107
+ embedding: embeddings[index] ?? [],
108
+ contentHash,
109
+ tokenEstimate: this.estimateTokenCount(chunk.content),
110
+ })
111
+
112
+ seenChunkKeys.add(chunk.chunkKey)
113
+
114
+ if (!existingRow) {
115
+ await params.create(payload)
116
+ return
117
+ }
118
+
119
+ const current = params.selectShape(existingRow)
120
+ const hasChanged =
121
+ current.contentHash !== contentHash ||
122
+ current.chunkIndex !== chunk.chunkIndex ||
123
+ (current.sectionPath ?? null) !== (chunk.sectionPath ?? null) ||
124
+ (current.pageStart ?? null) !== (chunk.pageStart ?? null) ||
125
+ (current.pageEnd ?? null) !== (chunk.pageEnd ?? null)
126
+
127
+ if (!hasChanged) {
128
+ return
129
+ }
130
+
131
+ await params.update(existingRow, payload)
132
+ }),
133
+ )
129
134
 
130
135
  const removedCurrentVersionRows = existingRows.filter((row) => {
131
136
  const current = params.selectShape(row)
@@ -612,6 +612,7 @@ class ExecutionPlanService {
612
612
  ): Promise<PlanNodeSpecRecord[]> {
613
613
  const createdRecords: PlanNodeSpecRecord[] = []
614
614
 
615
+ // Sequential: SurrealDB transactions require ordered operations
615
616
  for (const compiledNode of nodes) {
616
617
  const nodeSpecId = new RecordId(TABLES.PLAN_NODE_SPEC, Bun.randomUUIDv7())
617
618
  const created = await tx
@@ -657,6 +658,7 @@ class ExecutionPlanService {
657
658
  nodeSpecs: PlanNodeSpecRecord[],
658
659
  ): Promise<PlanNodeRunRecord[]> {
659
660
  const createdNodeRuns: PlanNodeRunRecord[] = []
661
+ // Sequential: SurrealDB transactions require ordered operations
660
662
  for (const nodeSpec of nodeSpecs) {
661
663
  const nodeRunId = new RecordId(TABLES.PLAN_NODE_RUN, Bun.randomUUIDv7())
662
664
  const created = await tx
@@ -16,6 +16,7 @@ const embeddings = getDefaultEmbeddings()
16
16
  const PROMOTION_MIN_USES = 5
17
17
  const PROMOTION_MIN_SUCCESS_RATE = 0.6
18
18
 
19
+ const ACTIVE_SKILL_FILTER = "AND status IN ['learned', 'verified', 'promoted'] AND archivedAt IS NONE"
19
20
  const SKILL_EXISTS_TTL_SECONDS = 120
20
21
  const SKILL_EXISTS_KEY_PREFIX = 'skill-exists'
21
22
 
@@ -176,8 +177,7 @@ class LearnedSkillService {
176
177
  new BoundQuery(
177
178
  `SELECT id FROM ${TABLES.LEARNED_SKILL}
178
179
  WHERE organizationId = $orgRef
179
- AND status IN ['learned', 'verified', 'promoted']
180
- AND archivedAt IS NONE
180
+ ${ACTIVE_SKILL_FILTER}
181
181
  AND (agentId IS NONE OR agentId = $agentId)
182
182
  LIMIT 1`,
183
183
  { orgRef, agentId },
@@ -227,8 +227,7 @@ class LearnedSkillService {
227
227
  vector::similarity::cosine(embedding, $embedding) AS similarity
228
228
  FROM ${TABLES.LEARNED_SKILL}
229
229
  WHERE organizationId = $organizationId
230
- AND status IN ['learned', 'verified', 'promoted']
231
- AND archivedAt IS NONE
230
+ ${ACTIVE_SKILL_FILTER}
232
231
  AND confidence >= $minConfidence
233
232
  AND (agentId IS NONE OR agentId = $agentId)
234
233
  AND embedding <|${params.limit}|> $embedding
@@ -311,8 +310,7 @@ class LearnedSkillService {
311
310
  vector::similarity::cosine(embedding, $embedding) AS similarity
312
311
  FROM ${TABLES.LEARNED_SKILL}
313
312
  WHERE organizationId = $organizationId
314
- AND status IN ['learned', 'verified', 'promoted']
315
- AND archivedAt IS NONE
313
+ ${ACTIVE_SKILL_FILTER}
316
314
  AND embedding <|3|> $embedding
317
315
  ORDER BY similarity DESC
318
316
  LIMIT 1
@@ -333,8 +331,7 @@ class LearnedSkillService {
333
331
  `SELECT *, type::string(id) AS id, type::string(organizationId) AS organizationId
334
332
  FROM ${TABLES.LEARNED_SKILL}
335
333
  WHERE organizationId = $organizationId
336
- AND status IN ['learned', 'verified', 'promoted']
337
- AND archivedAt IS NONE
334
+ ${ACTIVE_SKILL_FILTER}
338
335
  ORDER BY createdAt DESC`,
339
336
  { organizationId: orgRef },
340
337
  ),
@@ -357,8 +354,7 @@ class LearnedSkillService {
357
354
  FROM ${TABLES.LEARNED_SKILL}
358
355
  WHERE organizationId = $organizationId
359
356
  AND hash = $hash
360
- AND status IN ['learned', 'verified', 'promoted']
361
- AND archivedAt IS NONE
357
+ ${ACTIVE_SKILL_FILTER}
362
358
  LIMIT 1`,
363
359
  { organizationId: orgRef, hash },
364
360
  ),
@@ -1,22 +1,18 @@
1
1
  import { MEMORY } from '../config/constants'
2
2
  import { VECTOR_SEARCH_OVERFETCH_MULTIPLIER } from '../config/search'
3
3
  import type { MemorySearchResult } from '../db/memory-types'
4
- import { resolveCandidateLimit } from '../runtime/retrieval-pipeline'
4
+ import { compactWhitespace } from '../utils/string'
5
5
  import type { MemoryRerankOutput } from './memory.service'
6
6
 
7
7
  export function getCandidateLimit(limit: number): number {
8
- return resolveCandidateLimit({
9
- limit,
10
- multiplier: VECTOR_SEARCH_OVERFETCH_MULTIPLIER,
11
- minimum: MEMORY.DEFAULT_CANDIDATE_LIMIT,
12
- })
8
+ return Math.max(limit * VECTOR_SEARCH_OVERFETCH_MULTIPLIER, MEMORY.DEFAULT_CANDIDATE_LIMIT)
13
9
  }
14
10
 
15
11
  export function formatMemoryResults(results: MemorySearchResult[]): string {
16
12
  if (results.length === 0) return 'No stored memories.'
17
13
 
18
14
  const normalize = (value: string) => {
19
- const trimmed = value.replace(/\s+/g, ' ').trim()
15
+ const trimmed = compactWhitespace(value)
20
16
  if (trimmed.length <= 400) return trimmed
21
17
  return `${trimmed.slice(0, 400)}...`
22
18
  }
@@ -65,7 +61,7 @@ export function formatRerankedResults(
65
61
  used.add(candidate.id)
66
62
  total += 1
67
63
  const reason = item.relevance ? ` — ${item.relevance}` : ''
68
- const trimmed = candidate.content.replace(/\s+/g, ' ').trim()
64
+ const trimmed = compactWhitespace(candidate.content)
69
65
  const content = trimmed.length <= 400 ? trimmed : `${trimmed.slice(0, 400)}...`
70
66
  lines.push(`- ${content}${reason}`)
71
67
  if (total >= limit) break
@@ -24,10 +24,12 @@ import {
24
24
  scopedRetrievalToMap,
25
25
  } from '../runtime/retrieval-adapters'
26
26
  import { getRuntimeConfig } from '../runtime/runtime-config'
27
- import { createMemoryRerankerAgent, memoryRerankerPrompt } from '../system-agents/memory-reranker.agent'
28
- import { createOrgMemoryAgent, orgMemoryPrompt } from '../system-agents/memory.agent'
27
+ import { createMemoryRerankerAgent, MEMORY_RERANKER_PROMPT } from '../system-agents/memory-reranker.agent'
28
+ import { createOrgMemoryAgent, ORG_MEMORY_PROMPT } from '../system-agents/memory.agent'
29
+ import { toError } from '../utils/errors'
30
+ import { compactWhitespace } from '../utils/string'
29
31
  import { assessMemoryImportance, clampMemoryImportance } from './memory-assessment.service'
30
- import { formatMemoryResults, formatRerankedResults, getCandidateLimit } from './memory.utils'
32
+ import { formatMemoryResults, formatRerankedResults, getCandidateLimit } from './memory-utils'
31
33
 
32
34
  const ORG_MEMORY_TYPE = 'fact'
33
35
  const RERANK_CANDIDATE_MAX_CHARS = 500
@@ -72,7 +74,7 @@ class MemoryService {
72
74
  const cached = cache.get(cacheKey)
73
75
  if (cached) return cached
74
76
 
75
- const memory = new Memory({ createAgent: createOrgMemoryAgent }, { customPrompt: orgMemoryPrompt })
77
+ const memory = new Memory({ createAgent: createOrgMemoryAgent }, { customPrompt: ORG_MEMORY_PROMPT })
76
78
 
77
79
  cache.set(cacheKey, memory)
78
80
  aiLogger.debug`Memory client created and cached for ${cacheKey}`
@@ -85,7 +87,7 @@ class MemoryService {
85
87
  }
86
88
 
87
89
  private truncateCandidateText(value: string): string {
88
- const normalized = value.replace(/\s+/g, ' ').trim()
90
+ const normalized = compactWhitespace(value)
89
91
  if (normalized.length <= RERANK_CANDIDATE_MAX_CHARS) return normalized
90
92
  return `${normalized.slice(0, RERANK_CANDIDATE_MAX_CHARS)}...`
91
93
  }
@@ -125,7 +127,7 @@ class MemoryService {
125
127
  }
126
128
 
127
129
  private normalizeConversationText(value: string, maxChars: number): string {
128
- const normalized = value.replace(/\s+/g, ' ').trim()
130
+ const normalized = compactWhitespace(value)
129
131
  if (!normalized) return ''
130
132
  if (normalized.length <= maxChars) return normalized
131
133
  return `${normalized.slice(0, maxChars - 3)}...`
@@ -146,7 +148,7 @@ class MemoryService {
146
148
  }
147
149
 
148
150
  private normalizePreSeededMemoryText(value: string): string {
149
- const normalized = value.replace(/\s+/g, ' ').trim()
151
+ const normalized = compactWhitespace(value)
150
152
  if (normalized.length <= PRESEEDED_MEMORY_MAX_CHARS) return normalized
151
153
  return `${normalized.slice(0, PRESEEDED_MEMORY_MAX_CHARS - 3)}...`
152
154
  }
@@ -232,7 +234,7 @@ class MemoryService {
232
234
  return await helperModelRuntime.generateHelperStructured({
233
235
  tag: 'memory-reranker',
234
236
  createAgent: createMemoryRerankerAgent,
235
- defaultSystemPrompt: memoryRerankerPrompt,
237
+ defaultSystemPrompt: MEMORY_RERANKER_PROMPT,
236
238
  messages: [
237
239
  {
238
240
  role: 'user',
@@ -269,7 +271,7 @@ class MemoryService {
269
271
  return await helperModelRuntime.generateHelperStructured({
270
272
  tag: 'memory-reranker-multi-scope',
271
273
  createAgent: createMemoryRerankerAgent,
272
- defaultSystemPrompt: memoryRerankerPrompt,
274
+ defaultSystemPrompt: MEMORY_RERANKER_PROMPT,
273
275
  messages: [
274
276
  {
275
277
  role: 'user',
@@ -334,7 +336,7 @@ class MemoryService {
334
336
  aiLogger.debug`Organization memory search completed (resultLength: ${results.length}, preview: ${results.slice(0, 100)})`
335
337
  return results
336
338
  } catch (error: unknown) {
337
- const normalizedError = error instanceof Error ? error : new Error(String(error))
339
+ const normalizedError = toError(error)
338
340
  aiLogger.error`Organization memory search failed: ${normalizedError}`
339
341
  throw normalizedError
340
342
  }
@@ -348,7 +350,8 @@ class MemoryService {
348
350
  if (stale.length === 0) return ''
349
351
  const items = stale.map((m) => `- [NEEDS REVIEW] ${m.content}`).join('\n')
350
352
  return `Memories flagged for review (parent fact was superseded):\n${items}`
351
- } catch {
353
+ } catch (error) {
354
+ aiLogger.warn`Failed to get stale memories: ${error}`
352
355
  return ''
353
356
  }
354
357
  }
@@ -359,7 +362,7 @@ class MemoryService {
359
362
  options?: { fastMode?: boolean; limit?: number },
360
363
  ): Promise<string> {
361
364
  const orgScopeId = scopeId(ORG_SCOPE_PREFIX, orgId)
362
- aiLogger.info`[MEMORY_DEBUG] searchOrganizationMemoriesRaw - orgId: "${orgId}", scopeId: "${orgScopeId}"`
365
+ aiLogger.debug`searchOrganizationMemoriesRaw - orgId: "${orgId}", scopeId: "${orgScopeId}"`
363
366
  const memory = this.getOrgMemory(orgId)
364
367
  const fastMode = options?.fastMode ?? true
365
368
  const searchK = getRuntimeConfig().memory.searchK
@@ -376,7 +379,7 @@ class MemoryService {
376
379
  aiLogger.debug`Organization memory search (raw) completed (candidates: ${candidates.length})`
377
380
  return formatMemoryResults(candidates)
378
381
  } catch (error: unknown) {
379
- const normalizedError = error instanceof Error ? error : new Error(String(error))
382
+ const normalizedError = toError(error)
380
383
  aiLogger.error`Organization memory search (raw) failed: ${normalizedError}`
381
384
  throw normalizedError
382
385
  }
@@ -397,7 +400,7 @@ class MemoryService {
397
400
  aiLogger.debug`Agent memory search completed (agentName: ${agentName}, resultLength: ${results.length}, preview: ${results.slice(0, 100)})`
398
401
  return results
399
402
  } catch (error: unknown) {
400
- const normalizedError = error instanceof Error ? error : new Error(String(error))
403
+ const normalizedError = toError(error)
401
404
  aiLogger.error`Agent memory search failed: ${normalizedError}`
402
405
  throw normalizedError
403
406
  }
@@ -467,7 +470,7 @@ class MemoryService {
467
470
  const deduped: MemoryRecord[] = []
468
471
  const seen = new Set<string>()
469
472
  for (const memory of combined) {
470
- const normalizedKey = memory.content.replace(/\s+/g, ' ').trim().toLowerCase()
473
+ const normalizedKey = compactWhitespace(memory.content).toLowerCase()
471
474
  if (!normalizedKey || seen.has(normalizedKey)) continue
472
475
  seen.add(normalizedKey)
473
476
  deduped.push(memory)
@@ -594,7 +597,7 @@ class MemoryService {
594
597
  durability?: MemoryRecord['durability']
595
598
  }): Promise<string> {
596
599
  const orgScopeId = scopeId(ORG_SCOPE_PREFIX, orgId)
597
- aiLogger.info`[MEMORY_DEBUG] createOrganizationMemory - orgId: "${orgId}", scopeId: "${orgScopeId}", content preview: "${content.slice(0, 50)}"`
600
+ aiLogger.debug`createOrganizationMemory - orgId: "${orgId}", scopeId: "${orgScopeId}", content preview: "${content.slice(0, 50)}"`
598
601
  const memory = this.getOrgMemory(orgId)
599
602
  try {
600
603
  return await memory.insert(content, {
@@ -762,7 +765,7 @@ class MemoryService {
762
765
  }
763
766
 
764
767
  const orgScopeId = scopeId(ORG_SCOPE_PREFIX, orgId)
765
- aiLogger.info`[MEMORY_DEBUG] addConversationMemories - orgId: "${orgId}", scopeId: "${orgScopeId}", sourceId: ${sourceId ?? 'none'}`
768
+ aiLogger.debug`addConversationMemories - orgId: "${orgId}", scopeId: "${orgScopeId}", sourceId: ${sourceId ?? 'none'}`
766
769
 
767
770
  const orgMemory = this.getOrgMemory(orgId)
768
771
  let assessedImportance: number | undefined
@@ -833,7 +836,7 @@ class MemoryService {
833
836
  })
834
837
  aiLogger.debug`Conversation memories added to ${scopes.length} scope(s) from ${messages.length} message(s)`
835
838
  } catch (error: unknown) {
836
- const normalizedError = error instanceof Error ? error : new Error(String(error))
839
+ const normalizedError = toError(error)
837
840
  aiLogger.error`Memory write failed: ${normalizedError}`
838
841
  throw normalizedError
839
842
  }
@@ -23,7 +23,7 @@ const sdkOrganizationMemberSchema = z.object({
23
23
  createdAt: z.iso.datetime(),
24
24
  })
25
25
 
26
- export type SdkOrganizationMemberRecord = z.infer<typeof organizationMemberRecordSchema>
26
+ type SdkOrganizationMemberRecord = z.infer<typeof organizationMemberRecordSchema>
27
27
  export type SdkOrganizationMember = z.infer<typeof sdkOrganizationMemberSchema>
28
28
 
29
29
  class OrganizationMemberService extends BaseService<typeof organizationMemberRecordSchema> {
@@ -17,6 +17,7 @@ class PlanArtifactService {
17
17
  }): Promise<PlanArtifactRecord[]> {
18
18
  const records: PlanArtifactRecord[] = []
19
19
 
20
+ // Sequential: SurrealDB transactions require ordered operations
20
21
  for (const artifact of params.artifacts) {
21
22
  const artifactId = new RecordId(TABLES.PLAN_ARTIFACT, Bun.randomUUIDv7())
22
23
  const created = await params.tx
@@ -26,9 +26,11 @@ import { ensureRecordId, recordIdToString } from '../db/record-id'
26
26
  import { databaseService } from '../db/service'
27
27
  import type { DatabaseTransaction } from '../db/service'
28
28
  import { TABLES } from '../db/tables'
29
+ import { isRecord } from '../utils/string'
29
30
  import { planApprovalService } from './plan-approval.service'
30
31
  import { planArtifactService } from './plan-artifact.service'
31
32
  import { planCheckpointService } from './plan-checkpoint.service'
33
+ import { readPathValue } from './plan-helpers'
32
34
  import { planRunService } from './plan-run.service'
33
35
  import type { PlanValidationIssueInput } from './plan-validator.service'
34
36
  import { planValidatorService } from './plan-validator.service'
@@ -37,24 +39,6 @@ const SUCCESSFUL_TERMINAL_NODE_STATUSES = new Set(['completed', 'partial', 'skip
37
39
  const HUMAN_NODE_TYPES = new Set(['human-input', 'human-approval', 'human-review-edit', 'human-decision'])
38
40
  const STRUCTURAL_NODE_TYPES = new Set(['switch', 'join'])
39
41
 
40
- function isRecord(value: unknown): value is Record<string, unknown> {
41
- return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
42
- }
43
-
44
- function readPathValue(source: unknown, path: string): unknown {
45
- if (!path.trim()) return source
46
-
47
- let current: unknown = source
48
- for (const segment of path
49
- .split('.')
50
- .map((part) => part.trim())
51
- .filter(Boolean)) {
52
- if (!isRecord(current)) return undefined
53
- current = current[segment]
54
- }
55
- return current
56
- }
57
-
58
42
  function setPathValue(target: Record<string, unknown>, path: string, value: unknown) {
59
43
  const segments = path
60
44
  .split('.')
@@ -0,0 +1,15 @@
1
+ import { isRecord } from '../utils/string'
2
+
3
+ export function readPathValue(source: unknown, path: string): unknown {
4
+ if (!path.trim()) return source
5
+
6
+ let current: unknown = source
7
+ for (const segment of path
8
+ .split('.')
9
+ .map((part) => part.trim())
10
+ .filter(Boolean)) {
11
+ if (!isRecord(current)) return undefined
12
+ current = current[segment]
13
+ }
14
+ return current
15
+ }
@@ -8,6 +8,9 @@ import type {
8
8
  PlanValidationIssueSeverity,
9
9
  } from '@lota-sdk/shared'
10
10
 
11
+ import { isRecord } from '../utils/string'
12
+ import { readPathValue } from './plan-helpers'
13
+
11
14
  const STRUCTURAL_NODE_TYPES = new Set(['switch', 'join'])
12
15
  const HUMAN_NODE_TYPES = new Set(['human-input', 'human-approval', 'human-review-edit', 'human-decision'])
13
16
 
@@ -46,24 +49,6 @@ function createIssue(params: {
46
49
  }
47
50
  }
48
51
 
49
- function isRecord(value: unknown): value is Record<string, unknown> {
50
- return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
51
- }
52
-
53
- function readPathValue(source: unknown, path: string): unknown {
54
- if (!path.trim()) return source
55
-
56
- let current: unknown = source
57
- for (const segment of path
58
- .split('.')
59
- .map((part) => part.trim())
60
- .filter(Boolean)) {
61
- if (!isRecord(current)) return undefined
62
- current = current[segment]
63
- }
64
- return current
65
- }
66
-
67
52
  function hasAllFields(value: unknown, fields: string[]): boolean {
68
53
  if (!isRecord(value)) return false
69
54
  return fields.every((field) => readPathValue(value, field) !== undefined)
@@ -1,20 +1,13 @@
1
1
  import { createHelperModelRuntime } from '../runtime/helper-model'
2
+ import { normalizeTitle } from '../runtime/title-helpers'
2
3
  import {
3
4
  createRecentActivityTitleRefinerAgent,
4
- recentActivityTitleRefinerPrompt,
5
+ RECENT_ACTIVITY_TITLE_REFINER_PROMPT,
5
6
  } from '../system-agents/recent-activity-title-refiner.agent'
6
- import { compactWhitespace } from '../utils/string'
7
7
  import { recentActivityService } from './recent-activity.service'
8
8
 
9
9
  const RECENT_ACTIVITY_TITLE_TIMEOUT_MS = 60_000
10
10
 
11
- function normalizeTitle(value: string): string {
12
- const normalized = compactWhitespace(value)
13
- .replace(/^["'`]+|["'`]+$/g, '')
14
- .replace(/[.!?,;:]+$/g, '')
15
- return normalized.length <= 80 ? normalized : normalized.slice(0, 80).trim()
16
- }
17
-
18
11
  function buildRefinementPromptInput(
19
12
  candidate: Awaited<ReturnType<typeof recentActivityService.getRefinementCandidate>>,
20
13
  ) {
@@ -52,7 +45,7 @@ class RecentActivityTitleService {
52
45
  await this.helperRuntime.generateHelperText({
53
46
  tag: 'recent-activity-title-refinement',
54
47
  createAgent: createRecentActivityTitleRefinerAgent,
55
- defaultSystemPrompt: recentActivityTitleRefinerPrompt,
48
+ defaultSystemPrompt: RECENT_ACTIVITY_TITLE_REFINER_PROMPT,
56
49
  timeoutMs: RECENT_ACTIVITY_TITLE_TIMEOUT_MS,
57
50
  messages: [{ role: 'user', content: promptInput }],
58
51
  }),
@@ -166,18 +166,12 @@ class RecentActivityService {
166
166
  }): Promise<RecentActivity[]> {
167
167
  await databaseService.connect()
168
168
 
169
- const items: RecentActivity[] = []
170
- for (const candidate of params.events) {
171
- const recorded = await this.recordEvent({
172
- orgId: params.orgId,
173
- userId: params.userId,
174
- source: params.source,
175
- event: candidate,
176
- })
177
- items.push(recorded.item)
178
- }
179
-
180
- return items
169
+ const results = await Promise.all(
170
+ params.events.map((candidate) =>
171
+ this.recordEvent({ orgId: params.orgId, userId: params.userId, source: params.source, event: candidate }),
172
+ ),
173
+ )
174
+ return results.map((r) => r.item)
181
175
  }
182
176
 
183
177
  async recordEvent(params: {