@lota-sdk/core 0.1.14 → 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 +40 -43
  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
@@ -1,4 +1,5 @@
1
1
  import { agentDisplayNames, agentShortDisplayNames, resolveAgentNameAlias } from '../config/agent-defaults'
2
+ import { compactWhitespace } from '../utils/string'
2
3
 
3
4
  function escapeRegex(value: string): string {
4
5
  return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
@@ -173,8 +174,7 @@ export function createMemoryBlockRuntime(options: CreateMemoryBlockRuntimeOption
173
174
  )
174
175
  .filter(Boolean)
175
176
 
176
- const candidate = normalizedLines.join(' ').trim()
177
- const collapsed = candidate.replace(/\s+/g, ' ').trim()
177
+ const collapsed = compactWhitespace(normalizedLines.join(' '))
178
178
  if (!collapsed) return ''
179
179
  return collapsed
180
180
  }
@@ -186,6 +186,7 @@ export function createMemoryBlockRuntime(options: CreateMemoryBlockRuntimeOption
186
186
  try {
187
187
  const parsed: unknown = JSON.parse(trimmed)
188
188
  if (!Array.isArray(parsed)) return []
189
+ if (!parsed.every((item: unknown) => typeof item === 'object' && item !== null)) return []
189
190
  return parsed as MemoryBlockEntry[]
190
191
  } catch {
191
192
  return []
@@ -1,3 +1,11 @@
1
+ import { compactWhitespace } from '../utils/string'
2
+
3
+ const SCORE_WEIGHTS = {
4
+ durability: { core: 0.35, standard: 0.2, weak: 0.05 },
5
+ type: { decision: 0.25, preference: 0.18, default: 0.1 },
6
+ maxContentLength: 120,
7
+ } as const
8
+
1
9
  interface MemoryFactInput {
2
10
  content: string
3
11
  confidence: number
@@ -117,9 +125,20 @@ function scoreFact<T extends MemoryFactInput>(fact: T): number {
117
125
  const durability = fact.durability ?? 'standard'
118
126
  const type = fact.type ?? 'fact'
119
127
 
120
- const durabilityWeight = durability === 'core' ? 0.35 : durability === 'standard' ? 0.2 : 0.05
121
- const typeWeight = type === 'decision' ? 0.25 : type === 'fact' ? 0.18 : 0.1
122
- const lengthWeight = Math.min(fact.content.length, 120) / 120 / 10
128
+ const durabilityWeight =
129
+ durability === 'core'
130
+ ? SCORE_WEIGHTS.durability.core
131
+ : durability === 'standard'
132
+ ? SCORE_WEIGHTS.durability.standard
133
+ : SCORE_WEIGHTS.durability.weak
134
+ const typeWeight =
135
+ type === 'decision'
136
+ ? SCORE_WEIGHTS.type.decision
137
+ : type === 'fact'
138
+ ? SCORE_WEIGHTS.type.preference
139
+ : SCORE_WEIGHTS.type.default
140
+ const lengthWeight =
141
+ Math.min(fact.content.length, SCORE_WEIGHTS.maxContentLength) / SCORE_WEIGHTS.maxContentLength / 10
123
142
 
124
143
  return confidence + durabilityWeight + typeWeight + lengthWeight
125
144
  }
@@ -160,7 +179,7 @@ export function postProcessMemoryFacts<T extends MemoryFactInput>(
160
179
  const deduped = new Map<string, T>()
161
180
 
162
181
  for (const fact of rawFacts) {
163
- const content = typeof fact.content === 'string' ? fact.content.replace(/\s+/g, ' ').trim() : ''
182
+ const content = typeof fact.content === 'string' ? compactWhitespace(fact.content) : ''
164
183
  if (!content || content.length < minChars || content.length > maxChars) continue
165
184
  const normalizedFact = { ...fact, content }
166
185
  const key = normalizeFactForDedupe(content)
@@ -464,7 +483,7 @@ export function createMemoryActionPlan<TRelation extends string = string>(params
464
483
  }
465
484
 
466
485
  for (const [index, item] of params.updates.memory.entries()) {
467
- const text = typeof item.text === 'string' ? item.text.replace(/\s+/g, ' ').trim() : ''
486
+ const text = typeof item.text === 'string' ? compactWhitespace(item.text) : ''
468
487
  const itemId = typeof item.id === 'string' ? item.id.trim() : ''
469
488
 
470
489
  switch (item.event) {
@@ -3,7 +3,7 @@ export interface LotaPluginContributions {
3
3
  schemaFiles: readonly (string | URL)[]
4
4
  }
5
5
 
6
- export interface LotaPlugin<TServices = unknown, TTools = unknown> {
6
+ export interface LotaPlugin<TServices = Record<string, unknown>, TTools = Record<string, unknown>> {
7
7
  services: TServices
8
8
  tools?: TTools
9
9
  contributions: LotaPluginContributions
@@ -1,7 +1,7 @@
1
1
  import type { ToolSet } from 'ai'
2
2
 
3
3
  import type { RecordIdRef } from '../db/record-id'
4
- import type { ReadableUploadMetadata } from '../services/attachment.service'
4
+ import type { ReadableUploadMetadata } from '../storage/attachment-types'
5
5
  import type { LotaRuntimeWorkerExtensions } from './runtime-worker-registry'
6
6
 
7
7
  export interface LotaRuntimeBackgroundCursor {
@@ -34,7 +34,7 @@ export interface LotaRuntimeWorkspaceProvider {
34
34
  ): Promise<LotaRuntimeWorkspaceLifecycleState> | LotaRuntimeWorkspaceLifecycleState
35
35
  readProfileProjectionState?(
36
36
  workspace: Record<string, unknown>,
37
- ): Promise<LotaRuntimeWorkspaceProjectionState | void> | LotaRuntimeWorkspaceProjectionState | void
37
+ ): Promise<LotaRuntimeWorkspaceProjectionState | undefined> | LotaRuntimeWorkspaceProjectionState | undefined
38
38
  buildPromptSummary?(workspaceId: RecordIdRef): Promise<string | undefined>
39
39
  listRecentDomainEvents?(workspaceId: RecordIdRef, limit?: number): Promise<Array<Record<string, unknown>>>
40
40
  hasActiveKnowledgeSources?(workspaceId: string): Promise<boolean>
@@ -76,11 +76,87 @@ export interface LotaRuntimeTeamThinkToolsParams {
76
76
  toolProviders?: ToolSet
77
77
  }
78
78
 
79
+ export interface BuildContextParams {
80
+ workstream: unknown
81
+ workstreamRef: RecordIdRef
82
+ orgRef: RecordIdRef
83
+ userRef: RecordIdRef
84
+ userName?: string | null
85
+ workspace: Record<string, unknown>
86
+ onboardingActive: boolean
87
+ messageText: string
88
+ linearInstalled: boolean
89
+ githubInstalled: boolean
90
+ indexedRepoContext: unknown
91
+ promptContext: unknown
92
+ workspaceLifecycleState: unknown
93
+ workspaceProfileState: unknown
94
+ promptSummary: string | undefined
95
+ recentDomainEvents: Array<Record<string, unknown>>
96
+ retrievedKnowledgeSection: string | undefined
97
+ [key: string]: unknown
98
+ }
99
+
100
+ export interface AfterTurnParams {
101
+ workstream: unknown
102
+ workstreamRef: RecordIdRef
103
+ orgRef: RecordIdRef
104
+ userRef: RecordIdRef
105
+ userName?: string | null
106
+ onboardingActive: boolean
107
+ referenceUserMessage: unknown
108
+ assistantMessages: unknown[]
109
+ latestWorkstreamRecord: unknown
110
+ latestPersistedState: unknown
111
+ context: Record<string, unknown> | null
112
+ [key: string]: unknown
113
+ }
114
+
115
+ export interface ResolveAgentParams {
116
+ agentId: string
117
+ mode: string
118
+ workstream: unknown
119
+ workstreamRef: RecordIdRef
120
+ orgRef: RecordIdRef
121
+ userRef: RecordIdRef
122
+ userName?: string | null
123
+ onboardingActive: boolean
124
+ linearInstalled: boolean
125
+ githubInstalled: boolean
126
+ reasoningProfile: string
127
+ skills?: string[]
128
+ additionalInstructionSections?: string[]
129
+ context: Record<string, unknown> | null
130
+ [key: string]: unknown
131
+ }
132
+
133
+ export interface BuildExtraInstructionSectionsParams {
134
+ workstream: unknown
135
+ workstreamRef: RecordIdRef
136
+ orgRef: RecordIdRef
137
+ userRef: RecordIdRef
138
+ userName?: string | null
139
+ workspace: Record<string, unknown>
140
+ onboardingActive: boolean
141
+ messageText: string
142
+ linearInstalled: boolean
143
+ githubInstalled: boolean
144
+ indexedRepoContext: unknown
145
+ promptContext: unknown
146
+ workspaceLifecycleState: unknown
147
+ workspaceProfileState: unknown
148
+ promptSummary: string | undefined
149
+ recentDomainEvents: Array<Record<string, unknown>>
150
+ retrievedKnowledgeSection: string | undefined
151
+ context: Record<string, unknown> | null
152
+ [key: string]: unknown
153
+ }
154
+
79
155
  export interface LotaRuntimeTurnHooks {
80
- buildContext?: (params: Record<string, unknown>) => Promise<Record<string, unknown> | void>
81
- afterTurn?: (params: Record<string, unknown>) => Promise<void>
82
- resolveAgent?: (params: Record<string, unknown>) => Promise<Record<string, unknown> | void>
83
- buildExtraInstructionSections?: (params: Record<string, unknown>) => Promise<string[] | void>
156
+ buildContext?: (params: BuildContextParams) => Promise<Record<string, unknown> | void>
157
+ afterTurn?: (params: AfterTurnParams) => Promise<void>
158
+ resolveAgent?: (params: ResolveAgentParams) => Promise<Record<string, unknown> | void>
159
+ buildExtraInstructionSections?: (params: BuildExtraInstructionSectionsParams) => Promise<string[] | void>
84
160
  }
85
161
 
86
162
  export interface LotaRuntimeAdapters {
@@ -90,8 +166,8 @@ export interface LotaRuntimeAdapters {
90
166
  buildTeamThinkAgentTools?: (params: LotaRuntimeTeamThinkToolsParams) => Promise<{ tools: ToolSet }>
91
167
  }
92
168
  queues?: {
93
- enqueuePostChatOrgAction?: (job: Record<string, unknown>) => Promise<void> | void
94
- enqueueOnboardingRepoIndexFollowUp?: (job: Record<string, unknown>) => Promise<void> | void
169
+ enqueuePostChatOrgAction?: (job: Record<string, unknown>) => Promise<void>
170
+ enqueueOnboardingRepoIndexFollowUp?: (job: Record<string, unknown>) => Promise<void>
95
171
  }
96
172
  workers?: {
97
173
  connectPluginDatabases?: () => Promise<void>
@@ -115,17 +191,17 @@ let runtimeExtensionsState: RuntimeExtensionsState = {
115
191
  extraWorkers: {},
116
192
  }
117
193
 
118
- export function configureRuntimeExtensions(params?: {
194
+ export function configureRuntimeExtensions(params: {
119
195
  adapters?: LotaRuntimeAdapters
120
196
  turnHooks?: LotaRuntimeTurnHooks
121
197
  toolProviders?: ToolSet
122
198
  extraWorkers?: LotaRuntimeWorkerExtensions
123
199
  }): void {
124
200
  runtimeExtensionsState = {
125
- adapters: params?.adapters ?? {},
126
- turnHooks: params?.turnHooks ?? {},
127
- toolProviders: params?.toolProviders ?? EMPTY_TOOLS,
128
- extraWorkers: params?.extraWorkers ?? {},
201
+ adapters: params.adapters ?? {},
202
+ turnHooks: params.turnHooks ?? {},
203
+ toolProviders: params.toolProviders ?? EMPTY_TOOLS,
204
+ extraWorkers: params.extraWorkers ?? {},
129
205
  }
130
206
  }
131
207
 
@@ -1,12 +1,21 @@
1
+ import { compactWhitespace } from '../utils/string'
2
+
1
3
  const TITLE_WORD_LIMIT = 5
2
4
 
3
5
  export function limitTitleWords(text: string): string {
4
- const words = text.replace(/\s+/g, ' ').trim().split(' ').filter(Boolean)
6
+ const words = compactWhitespace(text).split(' ').filter(Boolean)
5
7
  return words.slice(0, TITLE_WORD_LIMIT).join(' ')
6
8
  }
7
9
 
8
10
  export function deriveTitle(text: string): string {
9
- const trimmed = text.replace(/\s+/g, ' ').trim()
11
+ const trimmed = compactWhitespace(text)
10
12
  if (trimmed.length <= 60) return trimmed
11
13
  return `${trimmed.slice(0, 57)}...`
12
14
  }
15
+
16
+ export function normalizeTitle(value: string): string {
17
+ const normalized = compactWhitespace(value)
18
+ .replace(/^["'`]+|["'`]+$/g, '')
19
+ .replace(/[.!?,;:]+$/g, '')
20
+ return normalized.length <= 80 ? normalized : normalized.slice(0, 80).trim()
21
+ }
@@ -114,14 +114,13 @@ export function collectToolOutputErrors(params: {
114
114
  if (typeof part !== 'object') continue
115
115
  if (part.type !== undefined && typeof part.type !== 'string') continue
116
116
  if (!part.type?.startsWith('tool-')) continue
117
- if ((part as Record<string, unknown>).state !== 'output-error') continue
117
+
118
+ const p = part as Record<string, unknown>
119
+ if (p.state !== 'output-error') continue
118
120
 
119
121
  const toolName = part.type.slice('tool-'.length) || 'unknown'
120
- const toolCallId =
121
- typeof (part as Record<string, unknown>).toolCallId === 'string' && (part as Record<string, unknown>).toolCallId
122
- ? ((part as Record<string, unknown>).toolCallId as string)
123
- : 'unknown'
124
- const errorTextRaw = (part as Record<string, unknown>).errorText
122
+ const toolCallId = typeof p.toolCallId === 'string' && p.toolCallId ? p.toolCallId : 'unknown'
123
+ const errorTextRaw = p.errorText
125
124
  const errorText =
126
125
  typeof errorTextRaw === 'string' && errorTextRaw.trim()
127
126
  ? errorTextRaw.trim()
@@ -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