@lota-sdk/core 0.1.14 → 0.1.16

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 (174) hide show
  1. package/infrastructure/schema/00_identity.surql +0 -2
  2. package/infrastructure/schema/01_memory.surql +1 -1
  3. package/infrastructure/schema/02_execution_plan.surql +62 -1
  4. package/infrastructure/schema/03_learned_skill.surql +1 -1
  5. package/infrastructure/schema/06_playbook.surql +25 -0
  6. package/infrastructure/schema/07_institutional_memory.surql +13 -0
  7. package/infrastructure/schema/08_quality_metrics.surql +17 -0
  8. package/package.json +9 -8
  9. package/src/ai/definitions.ts +80 -2
  10. package/src/ai/embedding-cache.ts +7 -6
  11. package/src/ai/index.ts +0 -1
  12. package/src/bifrost/bifrost.ts +14 -14
  13. package/src/config/agent-defaults.ts +32 -22
  14. package/src/config/agent-types.ts +11 -0
  15. package/src/config/constants.ts +2 -14
  16. package/src/config/debug-logger.ts +5 -1
  17. package/src/config/index.ts +3 -0
  18. package/src/config/logger.ts +7 -9
  19. package/src/config/model-constants.ts +16 -34
  20. package/src/config/search.ts +1 -15
  21. package/src/create-runtime.ts +453 -0
  22. package/src/db/cursor-pagination.ts +3 -6
  23. package/src/db/index.ts +2 -0
  24. package/src/db/memory-store.rows.ts +7 -7
  25. package/src/db/memory-store.ts +24 -24
  26. package/src/db/memory.ts +18 -16
  27. package/src/db/schema-fingerprint.ts +1 -0
  28. package/src/db/service.ts +193 -122
  29. package/src/db/startup.ts +9 -13
  30. package/src/db/surreal-mutation.ts +43 -0
  31. package/src/db/tables.ts +7 -0
  32. package/src/db/workstream-message-row.ts +15 -0
  33. package/src/embeddings/provider.ts +1 -1
  34. package/src/index.ts +1 -1
  35. package/src/queues/context-compaction.queue.ts +17 -52
  36. package/src/queues/delayed-node-promotion.queue.ts +41 -0
  37. package/src/queues/document-processor.queue.ts +7 -7
  38. package/src/queues/index.ts +3 -0
  39. package/src/queues/memory-consolidation.queue.ts +18 -54
  40. package/src/queues/plan-scheduler.queue.ts +97 -0
  41. package/src/queues/post-chat-memory.queue.ts +15 -60
  42. package/src/queues/queue-factory.ts +100 -0
  43. package/src/queues/recent-activity-title-refinement.queue.ts +15 -54
  44. package/src/queues/regular-chat-memory-digest.queue.ts +16 -55
  45. package/src/queues/skill-extraction.queue.ts +15 -50
  46. package/src/queues/workstream-title-generation.queue.ts +15 -51
  47. package/src/redis/connection.ts +12 -3
  48. package/src/redis/index.ts +2 -1
  49. package/src/redis/org-memory-lock.ts +1 -1
  50. package/src/redis/redis-lease-lock.ts +41 -8
  51. package/src/redis/stream-context.ts +11 -0
  52. package/src/runtime/agent-runtime-policy.ts +106 -21
  53. package/src/runtime/agent-stream-helpers.ts +2 -1
  54. package/src/runtime/approval-continuation.ts +12 -6
  55. package/src/runtime/context-compaction-constants.ts +1 -1
  56. package/src/runtime/context-compaction-runtime.ts +7 -5
  57. package/src/runtime/context-compaction.ts +40 -97
  58. package/src/runtime/execution-plan.ts +23 -19
  59. package/src/runtime/graph-designer.ts +15 -0
  60. package/src/runtime/helper-model.ts +10 -196
  61. package/src/runtime/index.ts +14 -1
  62. package/src/runtime/llm-content.ts +1 -1
  63. package/src/runtime/memory-block.ts +11 -12
  64. package/src/runtime/memory-pipeline.ts +26 -10
  65. package/src/runtime/plugin-resolution.ts +35 -0
  66. package/src/runtime/plugin-types.ts +73 -1
  67. package/src/runtime/retrieval-adapters.ts +1 -1
  68. package/src/runtime/runtime-config.ts +25 -12
  69. package/src/runtime/runtime-extensions.ts +91 -15
  70. package/src/runtime/runtime-worker-registry.ts +6 -0
  71. package/src/runtime/team-consultation-orchestrator.ts +45 -28
  72. package/src/runtime/team-consultation-prompts.ts +11 -2
  73. package/src/runtime/title-helpers.ts +11 -4
  74. package/src/runtime/workstream-chat-helpers.ts +6 -7
  75. package/src/runtime/workstream-routing-policy.ts +0 -30
  76. package/src/runtime/workstream-state.ts +17 -7
  77. package/src/services/adaptive-playbook.service.ts +152 -0
  78. package/src/services/agent-executor.service.ts +293 -0
  79. package/src/services/artifact-provenance.service.ts +172 -0
  80. package/src/services/attachment.service.ts +7 -12
  81. package/src/services/context-compaction.service.ts +75 -58
  82. package/src/services/context-enrichment.service.ts +33 -0
  83. package/src/services/coordination-registry.service.ts +117 -0
  84. package/src/services/document-chunk.service.ts +38 -33
  85. package/src/services/domain-agent-executor.service.ts +71 -0
  86. package/src/services/execution-plan.service.ts +271 -50
  87. package/src/services/feedback-loop.service.ts +96 -0
  88. package/src/services/global-orchestrator.service.ts +148 -0
  89. package/src/services/index.ts +26 -0
  90. package/src/services/institutional-memory.service.ts +145 -0
  91. package/src/services/learned-skill.service.ts +30 -15
  92. package/src/services/memory-assessment.service.ts +3 -2
  93. package/src/services/{memory.utils.ts → memory-utils.ts} +4 -13
  94. package/src/services/memory.service.ts +55 -69
  95. package/src/services/monitoring-window.service.ts +86 -0
  96. package/src/services/mutating-approval.service.ts +1 -1
  97. package/src/services/node-workspace.service.ts +155 -0
  98. package/src/services/notification.service.ts +39 -0
  99. package/src/services/organization-member.service.ts +12 -5
  100. package/src/services/organization.service.ts +5 -5
  101. package/src/services/ownership-dispatcher.service.ts +403 -0
  102. package/src/services/plan-approval.service.ts +1 -1
  103. package/src/services/plan-artifact.service.ts +1 -0
  104. package/src/services/plan-builder.service.ts +1 -0
  105. package/src/services/plan-checkpoint.service.ts +30 -2
  106. package/src/services/plan-compiler.service.ts +5 -0
  107. package/src/services/plan-coordination.service.ts +152 -0
  108. package/src/services/plan-cycle.service.ts +284 -0
  109. package/src/services/plan-deadline.service.ts +287 -0
  110. package/src/services/plan-executor.service.ts +386 -58
  111. package/src/services/plan-helpers.ts +15 -0
  112. package/src/services/plan-run.service.ts +41 -7
  113. package/src/services/plan-scheduler.service.ts +240 -0
  114. package/src/services/plan-template.service.ts +117 -0
  115. package/src/services/plan-validator.service.ts +87 -20
  116. package/src/services/plan-workspace.service.ts +83 -0
  117. package/src/services/playbook-registry.service.ts +67 -0
  118. package/src/services/plugin-executor.service.ts +103 -0
  119. package/src/services/quality-metrics.service.ts +132 -0
  120. package/src/services/recent-activity-title.service.ts +3 -10
  121. package/src/services/recent-activity.service.ts +33 -43
  122. package/src/services/skill-resolver.service.ts +19 -0
  123. package/src/services/system-executor.service.ts +105 -0
  124. package/src/services/workstream-message.service.ts +29 -41
  125. package/src/services/workstream-plan-registry.service.ts +22 -0
  126. package/src/services/workstream-title.service.ts +3 -9
  127. package/src/services/{workstream-turn-preparation.ts → workstream-turn-preparation.service.ts} +428 -373
  128. package/src/services/workstream-turn.ts +2 -2
  129. package/src/services/workstream.service.ts +55 -65
  130. package/src/services/workstream.types.ts +10 -19
  131. package/src/services/write-intent-validator.service.ts +81 -0
  132. package/src/storage/attachment-parser.ts +1 -1
  133. package/src/storage/attachment-storage.service.ts +4 -4
  134. package/src/storage/{attachments.utils.ts → attachment-utils.ts} +2 -5
  135. package/src/storage/generated-document-storage.service.ts +3 -2
  136. package/src/storage/index.ts +2 -2
  137. package/src/system-agents/{context-compacter.agent.ts → context-compaction.agent.ts} +4 -4
  138. package/src/system-agents/delegated-agent-factory.ts +5 -2
  139. package/src/system-agents/index.ts +8 -0
  140. package/src/system-agents/memory-reranker.agent.ts +1 -1
  141. package/src/system-agents/memory.agent.ts +1 -1
  142. package/src/system-agents/recent-activity-title-refiner.agent.ts +1 -1
  143. package/src/tools/execution-plan.tool.ts +17 -19
  144. package/src/tools/fetch-webpage.tool.ts +20 -18
  145. package/src/tools/index.ts +2 -3
  146. package/src/tools/read-file-parts.tool.ts +1 -1
  147. package/src/tools/search-web.tool.ts +18 -15
  148. package/src/tools/{search-tools.ts → search.tool.ts} +1 -1
  149. package/src/tools/team-think.tool.ts +14 -8
  150. package/src/tools/{tool-contract.ts → tool-contracts.ts} +9 -2
  151. package/src/utils/async.ts +3 -2
  152. package/src/utils/date-time.ts +4 -32
  153. package/src/utils/env.ts +8 -0
  154. package/src/utils/errors.ts +47 -0
  155. package/src/utils/hono-error-handler.ts +1 -2
  156. package/src/utils/index.ts +19 -2
  157. package/src/utils/string.ts +128 -1
  158. package/src/workers/bootstrap.ts +2 -2
  159. package/src/workers/index.ts +1 -0
  160. package/src/workers/memory-consolidation.worker.ts +12 -12
  161. package/src/workers/regular-chat-memory-digest.helpers.ts +2 -7
  162. package/src/workers/regular-chat-memory-digest.runner.ts +11 -105
  163. package/src/workers/skill-extraction.runner.ts +8 -102
  164. package/src/workers/utils/file-section-chunker.ts +6 -3
  165. package/src/workers/utils/repomix-file-sections.ts +2 -2
  166. package/src/workers/utils/sandbox-error.ts +11 -2
  167. package/src/workers/utils/workstream-message-query.ts +97 -0
  168. package/src/workers/worker-utils.ts +6 -2
  169. package/src/runtime/retrieval-pipeline.ts +0 -3
  170. package/src/runtime.ts +0 -387
  171. package/src/tools/log-hello-world.tool.ts +0 -17
  172. package/src/utils/error.ts +0 -10
  173. /package/src/services/{context-compaction-runtime.ts → context-compaction-runtime.singleton.ts} +0 -0
  174. /package/src/storage/{attachments.types.ts → attachment-types.ts} +0 -0
@@ -1,4 +1,4 @@
1
- import { configureLogger, serverLogger } from '../config/logger'
1
+ import { configureLotaLogger, serverLogger } from '../config/logger'
2
2
  import { LOTA_SDK_DATABASE_NAME } from '../db/sdk-database'
3
3
  import { SurrealDBService, databaseService, setDatabaseService } from '../db/service'
4
4
  import { connectWithStartupRetry, waitForDatabaseBootstrap } from '../db/startup'
@@ -27,7 +27,7 @@ export async function initializeSandboxedWorkerRuntime(): Promise<void> {
27
27
 
28
28
  sandboxedWorkerRuntimePromise = (async () => {
29
29
  const env = parseWorkerBootstrapEnv(process.env)
30
- await configureLogger()
30
+ await configureLotaLogger()
31
31
 
32
32
  ensureDatabaseServiceConfigured()
33
33
 
@@ -5,3 +5,4 @@ export * from './utils/file-section-chunker'
5
5
  export * from './utils/repomix-file-sections'
6
6
  export * from './utils/repo-structure-extractor'
7
7
  export * from './utils/repomix-process-concurrency'
8
+ export * from './utils/sandbox-error'
@@ -1,6 +1,7 @@
1
1
  import type { SandboxedJob } from 'bullmq'
2
2
  import { BoundQuery, eq, inside } from 'surrealdb'
3
3
 
4
+ import { MEMORY } from '../config/constants'
4
5
  import { serverLogger } from '../config/logger'
5
6
  import { ensureRecordId, recordIdToString } from '../db/record-id'
6
7
  import type { RecordIdInput } from '../db/record-id'
@@ -16,6 +17,7 @@ await initializeSandboxedWorkerRuntime()
16
17
  const MEMORY_TABLE = TABLES.MEMORY
17
18
  const MEMORY_RELATION_TABLE = TABLES.MEMORY_RELATION
18
19
  const MEMORY_HISTORY_TABLE = TABLES.MEMORY_HISTORY
20
+ const RELATION_SUPERSEDES = 'supersedes' as const
19
21
  const HARD_SIMILARITY_THRESHOLD = 0.95
20
22
  const SOFT_SIMILARITY_THRESHOLD = 0.9
21
23
  const MAX_MEMORIES_PER_SCOPE = 500
@@ -130,7 +132,7 @@ async function deduplicateScope(scopeId: string): Promise<number> {
130
132
  ensureRecordId(winner.id, TABLES.MEMORY),
131
133
  MEMORY_RELATION_TABLE,
132
134
  ensureRecordId(loser.id, TABLES.MEMORY),
133
- { relationType: 'supersedes', confidence: 1.0 },
135
+ { relationType: RELATION_SUPERSEDES, confidence: 1.0 },
134
136
  )
135
137
 
136
138
  await databaseService.query(
@@ -187,13 +189,13 @@ async function collapseSupersedeCh(): Promise<number> {
187
189
  new BoundQuery(
188
190
  `SELECT
189
191
  id AS middleId,
190
- <-${MEMORY_RELATION_TABLE}[WHERE relationType = 'supersedes']<-${MEMORY_TABLE}.id AS predecessors,
191
- ->${MEMORY_RELATION_TABLE}[WHERE relationType = 'supersedes']->${MEMORY_TABLE}.id AS successors
192
+ <-${MEMORY_RELATION_TABLE}[WHERE relationType = '${RELATION_SUPERSEDES}']<-${MEMORY_TABLE}.id AS predecessors,
193
+ ->${MEMORY_RELATION_TABLE}[WHERE relationType = '${RELATION_SUPERSEDES}']->${MEMORY_TABLE}.id AS successors
192
194
  FROM ${MEMORY_TABLE}
193
195
  WHERE archivedAt IS NONE
194
- AND count(->${MEMORY_RELATION_TABLE}[WHERE relationType = 'supersedes']) > 0
195
- AND count(<-${MEMORY_RELATION_TABLE}[WHERE relationType = 'supersedes']) > 0
196
- LIMIT 100`,
196
+ AND count(->${MEMORY_RELATION_TABLE}[WHERE relationType = '${RELATION_SUPERSEDES}']) > 0
197
+ AND count(<-${MEMORY_RELATION_TABLE}[WHERE relationType = '${RELATION_SUPERSEDES}']) > 0
198
+ LIMIT ${MEMORY.MAX_KNN_LIMIT}`,
197
199
  ),
198
200
  )
199
201
 
@@ -210,7 +212,7 @@ async function collapseSupersedeCh(): Promise<number> {
210
212
  const existing = await databaseService.query<{ id: RecordIdInput }>(
211
213
  new BoundQuery(
212
214
  `SELECT id FROM ${MEMORY_RELATION_TABLE}
213
- WHERE in = $predId AND out = $succId AND relationType = 'supersedes'
215
+ WHERE in = $predId AND out = $succId AND relationType = '${RELATION_SUPERSEDES}'
214
216
  LIMIT 1`,
215
217
  { predId: predRef, succId: succRef },
216
218
  ),
@@ -218,7 +220,7 @@ async function collapseSupersedeCh(): Promise<number> {
218
220
 
219
221
  if (existing.length === 0) {
220
222
  await databaseService.relate(predRef, MEMORY_RELATION_TABLE, succRef, {
221
- relationType: 'supersedes',
223
+ relationType: RELATION_SUPERSEDES,
222
224
  confidence: 1.0,
223
225
  })
224
226
  }
@@ -288,10 +290,8 @@ const handler = async (job: SandboxedJob<MemoryConsolidationJob>) => {
288
290
  new BoundQuery(`SELECT VALUE scopeId FROM ${MEMORY_TABLE} WHERE archivedAt IS NONE GROUP BY scopeId`),
289
291
  )
290
292
 
291
- for (const scopeId of scopeIds) {
292
- const merged = await deduplicateScope(scopeId)
293
- totalMerged += merged
294
- }
293
+ const results = await Promise.all(scopeIds.map(deduplicateScope))
294
+ totalMerged = results.reduce((a, b) => a + b, 0)
295
295
  }
296
296
 
297
297
  const pruned = await pruneStaleMemories()
@@ -1,4 +1,5 @@
1
1
  import { isAgentName } from '../config/agent-defaults'
2
+ import { compactWhitespace } from '../utils/string'
2
3
 
3
4
  interface DigestMessageForTranscript {
4
5
  source: 'workstream'
@@ -8,10 +9,6 @@ interface DigestMessageForTranscript {
8
9
  metadata?: Record<string, unknown>
9
10
  }
10
11
 
11
- function normalizeWhitespace(value: string): string {
12
- return value.replace(/\s+/g, ' ').trim()
13
- }
14
-
15
12
  function normalizeFilePartMetadata(part: Record<string, unknown>): string | null {
16
13
  if (part.type !== 'file') return null
17
14
 
@@ -57,9 +54,7 @@ export function buildDigestTranscript(params: { messages: DigestMessageForTransc
57
54
 
58
55
  const sourcePrefix = `[${message.source}:${message.sourceId}]`
59
56
  const textParts = message.parts
60
- .flatMap((part) =>
61
- part.type === 'text' && typeof part.text === 'string' ? [normalizeWhitespace(part.text)] : [],
62
- )
57
+ .flatMap((part) => (part.type === 'text' && typeof part.text === 'string' ? [compactWhitespace(part.text)] : []))
63
58
  .filter((value) => value.length > 0)
64
59
  const fileParts = message.parts
65
60
  .map((part) => normalizeFilePartMetadata(part))
@@ -1,4 +1,3 @@
1
- import { toTimestamp } from '@lota-sdk/shared'
2
1
  import { BoundQuery } from 'surrealdb'
3
2
  import { z } from 'zod'
4
3
 
@@ -16,26 +15,20 @@ import { createHelperModelRuntime } from '../runtime/helper-model'
16
15
  import { getRuntimeAdapters, withConfiguredWorkspaceMemoryLock } from '../runtime/runtime-extensions'
17
16
  import { memoryService } from '../services/memory.service'
18
17
  import { createRegularChatMemoryDigestAgent } from '../system-agents/regular-chat-memory-digest.agent'
18
+ import { compactWhitespace } from '../utils/string'
19
19
  import { buildDigestTranscript, resolveWorkspaceBootstrapCutoff } from './regular-chat-memory-digest.helpers'
20
+ import {
21
+ compareDigestMessageOrder,
22
+ listEligibleWorkstreamMessages,
23
+ listWorkstreamIdsForOrg,
24
+ normalizeBlock,
25
+ } from './utils/workstream-message-query'
26
+ import type { DigestCursor, DigestMessage } from './utils/workstream-message-query'
20
27
 
21
28
  const StructuredProfilePatchSchema = z.record(z.string(), z.unknown()).default({})
22
29
 
23
30
  const REGULAR_CHAT_MEMORY_DIGEST_TIMEOUT_MS = 10 * 60 * 1000
24
31
  const WorkspaceMemoryRowSchema = z.object({ content: z.string() })
25
- const EntityIdRowSchema = z.string().trim().min(1)
26
- const RecordTimestampSchema = z.union([z.date(), z.string(), z.number()])
27
- const MessageRoleSchema = z.enum(['system', 'user', 'assistant'])
28
- const MessagePartSchema = z.record(z.string(), z.unknown())
29
- const MessageMetadataSchema = z.record(z.string(), z.unknown()).nullish()
30
-
31
- const WorkstreamDigestMessageRowSchema = z.object({
32
- id: z.string(),
33
- workstreamId: z.string(),
34
- role: MessageRoleSchema,
35
- parts: z.array(MessagePartSchema).optional(),
36
- metadata: MessageMetadataSchema,
37
- createdAt: RecordTimestampSchema,
38
- })
39
32
 
40
33
  const ExtractedFactSchema = z.object({
41
34
  content: z.string().trim().min(1),
@@ -52,40 +45,18 @@ const RegularChatMemoryDigestOutputSchema = z.object({
52
45
 
53
46
  const helperModelRuntime = createHelperModelRuntime()
54
47
 
55
- interface DigestCursor {
56
- createdAt: Date
57
- id: string
58
- }
59
-
60
- interface DigestMessage {
61
- source: 'workstream'
62
- sourceId: string
63
- role: 'system' | 'user' | 'assistant'
64
- parts: Array<Record<string, unknown>>
65
- metadata?: Record<string, unknown>
66
- cursor: DigestCursor
67
- }
68
-
69
48
  interface RegularChatDigestRunResult {
70
49
  skipped: boolean
71
50
  processedWorkstreamMessages: number
72
51
  followUpScheduled: boolean
73
52
  }
74
53
 
75
- function normalizeWhitespace(value: string): string {
76
- return value.replace(/\s+/g, ' ').trim()
77
- }
78
-
79
- function normalizeBlock(value: string): string {
80
- return value.replaceAll(String.fromCharCode(0), '').replace(/\r/g, '').trim()
81
- }
82
-
83
54
  function buildMemoryContext(memories: Array<{ content: string }>): string {
84
55
  if (memories.length === 0) return 'No existing memories.'
85
56
 
86
57
  return memories
87
58
  .map((memory, index) => {
88
- const content = normalizeWhitespace(memory.content)
59
+ const content = compactWhitespace(memory.content)
89
60
  if (!content) return ''
90
61
  return `${index + 1}. ${content}`
91
62
  })
@@ -117,75 +88,10 @@ function buildPrompt(params: {
117
88
  ].join('\n')
118
89
  }
119
90
 
120
- function mapWorkstreamDigestRow(row: z.infer<typeof WorkstreamDigestMessageRowSchema>): DigestMessage {
121
- return {
122
- source: 'workstream',
123
- sourceId: row.workstreamId,
124
- role: row.role,
125
- parts: row.parts ?? [],
126
- metadata: row.metadata ?? undefined,
127
- cursor: { createdAt: new Date(toTimestamp(row.createdAt)), id: row.id },
128
- }
129
- }
130
-
131
- function compareDigestMessageOrder(left: DigestMessage, right: DigestMessage): number {
132
- const timeDiff = left.cursor.createdAt.getTime() - right.cursor.createdAt.getTime()
133
- if (timeDiff !== 0) return timeDiff
134
- return left.cursor.id.localeCompare(right.cursor.id)
135
- }
136
-
137
91
  function getLastCursor(messages: DigestMessage[]): DigestCursor | null {
138
92
  return messages.length > 0 ? messages[messages.length - 1].cursor : null
139
93
  }
140
94
 
141
- async function listWorkstreamIdsForOrg(orgRef: RecordIdRef): Promise<RecordIdRef[]> {
142
- const ids = await databaseService.query<unknown>(
143
- new BoundQuery(
144
- `SELECT VALUE type::string(id) FROM ${TABLES.WORKSTREAM}
145
- WHERE organizationId = $organizationId`,
146
- { organizationId: orgRef },
147
- ),
148
- )
149
-
150
- return ids.map((value) => ensureRecordId(EntityIdRowSchema.parse(value), TABLES.WORKSTREAM))
151
- }
152
-
153
- async function listEligibleWorkstreamMessages(params: {
154
- workstreamIds: RecordIdRef[]
155
- cursor: DigestCursor | null
156
- onboardingCutoff: Date | null
157
- }): Promise<DigestMessage[]> {
158
- if (params.workstreamIds.length === 0) return []
159
-
160
- let query: BoundQuery | null = null
161
- if (params.cursor) {
162
- const cursorRowId = ensureRecordId(params.cursor.id, TABLES.WORKSTREAM_MESSAGE)
163
- query = new BoundQuery(
164
- `SELECT type::string(id) AS id, type::string(workstreamId) AS workstreamId, role, parts, metadata, createdAt FROM ${TABLES.WORKSTREAM_MESSAGE}
165
- WHERE workstreamId IN $workstreamIds
166
- AND (
167
- createdAt > $cursorCreatedAt
168
- OR (createdAt = $cursorCreatedAt AND id > $cursorRowId)
169
- )
170
- ORDER BY createdAt ASC, id ASC`,
171
- { workstreamIds: params.workstreamIds, cursorCreatedAt: params.cursor.createdAt, cursorRowId },
172
- )
173
- } else if (params.onboardingCutoff) {
174
- query = new BoundQuery(
175
- `SELECT type::string(id) AS id, type::string(workstreamId) AS workstreamId, role, parts, metadata, createdAt FROM ${TABLES.WORKSTREAM_MESSAGE}
176
- WHERE workstreamId IN $workstreamIds
177
- AND createdAt > $onboardingCutoff
178
- ORDER BY createdAt ASC, id ASC`,
179
- { workstreamIds: params.workstreamIds, onboardingCutoff: params.onboardingCutoff },
180
- )
181
- }
182
-
183
- if (!query) return []
184
-
185
- const rows = await databaseService.query<unknown>(query)
186
- return rows.map((row) => mapWorkstreamDigestRow(WorkstreamDigestMessageRowSchema.parse(row)))
187
- }
188
-
189
95
  async function hasNewEligibleWorkstreamMessages(params: {
190
96
  workstreamIds: RecordIdRef[]
191
97
  cursor: DigestCursor | null
@@ -224,7 +130,7 @@ async function hasNewEligibleWorkstreamMessages(params: {
224
130
  }
225
131
 
226
132
  async function loadExistingOrganizationMemories(orgId: string): Promise<Array<{ content: string }>> {
227
- return await databaseService.queryMany(
133
+ return databaseService.queryMany(
228
134
  new BoundQuery(
229
135
  `SELECT content, createdAt, id FROM ${TABLES.MEMORY}
230
136
  WHERE metadata.orgId = $orgId
@@ -249,7 +155,7 @@ export async function runRegularChatMemoryDigest(
249
155
  return { skipped: true, processedWorkstreamMessages: 0, followUpScheduled: false }
250
156
  }
251
157
 
252
- return await withConfiguredWorkspaceMemoryLock(orgId, async () => {
158
+ return withConfiguredWorkspaceMemoryLock(orgId, async () => {
253
159
  if (
254
160
  !workspaceProvider.getBackgroundCursor ||
255
161
  !workspaceProvider.setBackgroundCursor ||
@@ -1,11 +1,5 @@
1
- import { toTimestamp } from '@lota-sdk/shared'
2
- import { BoundQuery } from 'surrealdb'
3
- import { z } from 'zod'
4
-
5
1
  import { serverLogger } from '../config/logger'
6
2
  import { ensureRecordId, recordIdToString } from '../db/record-id'
7
- import type { RecordIdRef } from '../db/record-id'
8
- import { databaseService } from '../db/service'
9
3
  import { TABLES } from '../db/tables'
10
4
  import { getDefaultEmbeddings } from '../embeddings/provider'
11
5
  import type { SkillExtractionJob } from '../queues/skill-extraction.queue'
@@ -16,38 +10,15 @@ import { createSkillExtractorAgent, SkillExtractionOutputSchema } from '../syste
16
10
  import type { SkillCandidate } from '../system-agents/skill-extractor.agent'
17
11
  import { createSkillManagerAgent, SkillManagerOutputSchema } from '../system-agents/skill-manager.agent'
18
12
  import { buildDigestTranscript, resolveWorkspaceBootstrapCutoff } from './regular-chat-memory-digest.helpers'
13
+ import {
14
+ compareDigestMessageOrder,
15
+ listEligibleWorkstreamMessages,
16
+ listWorkstreamIdsForOrg,
17
+ } from './utils/workstream-message-query'
19
18
 
20
19
  const SKILL_EXTRACTION_TIMEOUT_MS = 10 * 60 * 1000
21
20
  const MIN_MESSAGE_THRESHOLD = 10
22
21
 
23
- const RecordTimestampSchema = z.union([z.date(), z.string(), z.number()])
24
- const MessageRoleSchema = z.enum(['system', 'user', 'assistant'])
25
- const MessagePartSchema = z.record(z.string(), z.unknown())
26
- const MessageMetadataSchema = z.record(z.string(), z.unknown()).nullish()
27
-
28
- const WorkstreamMessageRowSchema = z.object({
29
- id: z.string(),
30
- workstreamId: z.string(),
31
- role: MessageRoleSchema,
32
- parts: z.array(MessagePartSchema).optional(),
33
- metadata: MessageMetadataSchema,
34
- createdAt: RecordTimestampSchema,
35
- })
36
-
37
- interface DigestCursor {
38
- createdAt: Date
39
- id: string
40
- }
41
-
42
- interface DigestMessage {
43
- source: 'workstream'
44
- sourceId: string
45
- role: 'system' | 'user' | 'assistant'
46
- parts: Array<Record<string, unknown>>
47
- metadata?: Record<string, unknown>
48
- cursor: DigestCursor
49
- }
50
-
51
22
  interface SkillExtractionRunResult {
52
23
  skipped: boolean
53
24
  processedMessages: number
@@ -58,71 +29,6 @@ const embeddings = getDefaultEmbeddings()
58
29
 
59
30
  const helperModelRuntime = createHelperModelRuntime()
60
31
 
61
- function mapWorkstreamRow(row: z.infer<typeof WorkstreamMessageRowSchema>): DigestMessage {
62
- return {
63
- source: 'workstream',
64
- sourceId: row.workstreamId,
65
- role: row.role,
66
- parts: row.parts ?? [],
67
- metadata: row.metadata ?? undefined,
68
- cursor: { createdAt: new Date(toTimestamp(row.createdAt)), id: row.id },
69
- }
70
- }
71
-
72
- function compareMessageOrder(left: DigestMessage, right: DigestMessage): number {
73
- const timeDiff = left.cursor.createdAt.getTime() - right.cursor.createdAt.getTime()
74
- if (timeDiff !== 0) return timeDiff
75
- return left.cursor.id.localeCompare(right.cursor.id)
76
- }
77
-
78
- async function listWorkstreamIdsForOrg(orgRef: RecordIdRef): Promise<RecordIdRef[]> {
79
- const EntityIdRowSchema = z.string().trim().min(1)
80
- const ids = await databaseService.query<unknown>(
81
- new BoundQuery(
82
- `SELECT VALUE type::string(id) FROM ${TABLES.WORKSTREAM}
83
- WHERE organizationId = $organizationId`,
84
- { organizationId: orgRef },
85
- ),
86
- )
87
- return ids.map((value) => ensureRecordId(EntityIdRowSchema.parse(value), TABLES.WORKSTREAM))
88
- }
89
-
90
- async function listEligibleMessages(params: {
91
- workstreamIds: RecordIdRef[]
92
- cursor: DigestCursor | null
93
- onboardingCutoff: Date | null
94
- }): Promise<DigestMessage[]> {
95
- if (params.workstreamIds.length === 0) return []
96
-
97
- let query: BoundQuery | null = null
98
- if (params.cursor) {
99
- const cursorRowId = ensureRecordId(params.cursor.id, TABLES.WORKSTREAM_MESSAGE)
100
- query = new BoundQuery(
101
- `SELECT type::string(id) AS id, type::string(workstreamId) AS workstreamId, role, parts, metadata, createdAt FROM ${TABLES.WORKSTREAM_MESSAGE}
102
- WHERE workstreamId IN $workstreamIds
103
- AND (
104
- createdAt > $cursorCreatedAt
105
- OR (createdAt = $cursorCreatedAt AND id > $cursorRowId)
106
- )
107
- ORDER BY createdAt ASC, id ASC`,
108
- { workstreamIds: params.workstreamIds, cursorCreatedAt: params.cursor.createdAt, cursorRowId },
109
- )
110
- } else if (params.onboardingCutoff) {
111
- query = new BoundQuery(
112
- `SELECT type::string(id) AS id, type::string(workstreamId) AS workstreamId, role, parts, metadata, createdAt FROM ${TABLES.WORKSTREAM_MESSAGE}
113
- WHERE workstreamId IN $workstreamIds
114
- AND createdAt > $onboardingCutoff
115
- ORDER BY createdAt ASC, id ASC`,
116
- { workstreamIds: params.workstreamIds, onboardingCutoff: params.onboardingCutoff },
117
- )
118
- }
119
-
120
- if (!query) return []
121
-
122
- const rows = await databaseService.query<unknown>(query)
123
- return rows.map((row) => mapWorkstreamRow(WorkstreamMessageRowSchema.parse(row)))
124
- }
125
-
126
32
  function buildExtractionPrompt(params: { workspaceName: string; transcript: string; existingSkills: string }): string {
127
33
  return [
128
34
  `Workspace name: ${params.workspaceName}`,
@@ -180,7 +86,7 @@ export async function runSkillExtraction(data: SkillExtractionJob): Promise<Skil
180
86
  return { skipped: true, processedMessages: 0, extractedSkills: 0 }
181
87
  }
182
88
 
183
- return await withConfiguredWorkspaceMemoryLock(orgId, async () => {
89
+ return withConfiguredWorkspaceMemoryLock(orgId, async () => {
184
90
  const workspace = await cursorAwareWorkspaceProvider.getWorkspace(orgRef)
185
91
  const lifecycleState = await cursorAwareWorkspaceProvider.getLifecycleState?.(workspace)
186
92
  if (lifecycleState?.bootstrapActive ?? false) {
@@ -196,14 +102,14 @@ export async function runSkillExtraction(data: SkillExtractionJob): Promise<Skil
196
102
  })
197
103
 
198
104
  const workstreamIds = await listWorkstreamIdsForOrg(orgRef)
199
- const messages = await listEligibleMessages({ workstreamIds, cursor: existingCursor, onboardingCutoff })
105
+ const messages = await listEligibleWorkstreamMessages({ workstreamIds, cursor: existingCursor, onboardingCutoff })
200
106
 
201
107
  if (messages.length < MIN_MESSAGE_THRESHOLD) {
202
108
  serverLogger.info`Skipping skill extraction for ${orgId}: only ${messages.length} messages (threshold: ${MIN_MESSAGE_THRESHOLD})`
203
109
  return { skipped: true, processedMessages: messages.length, extractedSkills: 0 }
204
110
  }
205
111
 
206
- const sortedMessages = [...messages].sort(compareMessageOrder)
112
+ const sortedMessages = [...messages].sort(compareDigestMessageOrder)
207
113
  const { transcript } = buildDigestTranscript({ messages: sortedMessages })
208
114
 
209
115
  const existingSkills = await learnedSkillService.listForOrg(orgId)
@@ -1,7 +1,10 @@
1
+ import { CHARS_PER_TOKEN_ESTIMATE } from '../../utils/string'
2
+
1
3
  export const DEFAULT_FILE_SECTION_CHUNK_MAX_CHARS = 250_000
2
- const MIN_FILE_SECTION_CHUNK_MAX_CHARS = 4_000
4
+ export const MIN_FILE_SECTION_CHUNK_MAX_CHARS = 4_000
3
5
  export const DEFAULT_FILE_SECTION_CHUNK_MIN_CHARS = 10_000
4
6
  const SECTION_SEPARATOR_LENGTH = 2
7
+ const MIN_CHUNK_CHARS_FLOOR = 512
5
8
 
6
9
  export interface FileSection {
7
10
  kind: 'preamble' | 'file'
@@ -29,7 +32,7 @@ export interface FileSectionChunkOptions {
29
32
 
30
33
  function estimateTokenCountFromChars(text: string): number {
31
34
  if (!text) return 0
32
- return Math.ceil(text.length / 3)
35
+ return Math.ceil(text.length / CHARS_PER_TOKEN_ESTIMATE)
33
36
  }
34
37
 
35
38
  function normalizeMaxChars(value?: number): number {
@@ -43,7 +46,7 @@ function normalizeMinChunkChars(value: number | undefined, maxChars: number): nu
43
46
  if (typeof value !== 'number' || !Number.isFinite(value)) {
44
47
  return Math.min(DEFAULT_FILE_SECTION_CHUNK_MIN_CHARS, Math.floor(maxChars * 0.35))
45
48
  }
46
- const normalized = Math.max(512, Math.floor(value))
49
+ const normalized = Math.max(MIN_CHUNK_CHARS_FLOOR, Math.floor(value))
47
50
  return Math.min(normalized, Math.floor(maxChars * 0.6))
48
51
  }
49
52
 
@@ -27,7 +27,7 @@ export function parseRepomixFileSections(repomixOutput: string): FileSection[] {
27
27
  const nextStart = matches[index + 1]?.index ?? source.length
28
28
  const content = source.slice(start, nextStart).trim()
29
29
  if (!content) continue
30
- const filePath = (match[1] ?? '').trim()
30
+ const filePath = match[1].trim()
31
31
  sections.push({ kind: 'file', content, filePath: filePath || undefined })
32
32
  }
33
33
 
@@ -38,5 +38,5 @@ export async function chunkRepomixFileSections(
38
38
  repomixOutput: string,
39
39
  options: FileSectionChunkOptions = {},
40
40
  ): Promise<FileSectionChunk[]> {
41
- return await chunkFileSections(parseRepomixFileSections(repomixOutput), options)
41
+ return chunkFileSections(parseRepomixFileSections(repomixOutput), options)
42
42
  }
@@ -1,5 +1,14 @@
1
- export function toSandboxedWorkerError(error: unknown, context?: string): Error {
2
- const base = error instanceof Error ? error : new Error(String(error))
1
+ export interface SandboxedWorkerError {
2
+ name: string
3
+ message: string
4
+ stack?: string
5
+ }
6
+
7
+ export function toSandboxedWorkerError(error: unknown, context?: string): SandboxedWorkerError {
8
+ const base =
9
+ error instanceof Error
10
+ ? { name: error.name || 'Error', message: error.message, stack: error.stack }
11
+ : { name: 'Error', message: String(error) }
3
12
  if (context) base.message = `${context}: ${base.message}`
4
13
  return base
5
14
  }
@@ -0,0 +1,97 @@
1
+ import { requireTimestamp } from '@lota-sdk/shared'
2
+ import { BoundQuery } from 'surrealdb'
3
+ import { z } from 'zod'
4
+
5
+ import { ensureRecordId, recordIdToString } from '../../db/record-id'
6
+ import type { RecordIdRef } from '../../db/record-id'
7
+ import { databaseService } from '../../db/service'
8
+ import { TABLES } from '../../db/tables'
9
+ import { WorkstreamMessageRowSchema } from '../../db/workstream-message-row'
10
+ import type { WorkstreamMessageRow } from '../../db/workstream-message-row'
11
+ import { normalizeTextBody } from '../../document/parsing'
12
+
13
+ export interface DigestCursor {
14
+ createdAt: Date
15
+ id: string
16
+ }
17
+
18
+ export interface DigestMessage {
19
+ source: 'workstream'
20
+ sourceId: string
21
+ role: 'system' | 'user' | 'assistant'
22
+ parts: Array<Record<string, unknown>>
23
+ metadata?: Record<string, unknown>
24
+ cursor: DigestCursor
25
+ }
26
+
27
+ function mapWorkstreamRow(row: WorkstreamMessageRow): DigestMessage {
28
+ return {
29
+ source: 'workstream',
30
+ sourceId: recordIdToString(row.workstreamId, TABLES.WORKSTREAM),
31
+ role: row.role,
32
+ parts: row.parts as Array<Record<string, unknown>>,
33
+ metadata: row.metadata ?? undefined,
34
+ cursor: {
35
+ createdAt: new Date(requireTimestamp(row.createdAt)),
36
+ id: recordIdToString(row.id, TABLES.WORKSTREAM_MESSAGE),
37
+ },
38
+ }
39
+ }
40
+
41
+ export function compareDigestMessageOrder(left: DigestMessage, right: DigestMessage): number {
42
+ const timeDiff = left.cursor.createdAt.getTime() - right.cursor.createdAt.getTime()
43
+ if (timeDiff !== 0) return timeDiff
44
+ return left.cursor.id.localeCompare(right.cursor.id)
45
+ }
46
+
47
+ export async function listWorkstreamIdsForOrg(orgRef: RecordIdRef): Promise<RecordIdRef[]> {
48
+ const EntityIdRowSchema = z.string().trim().min(1)
49
+ const ids = await databaseService.query<unknown>(
50
+ new BoundQuery(
51
+ `SELECT VALUE type::string(id) FROM ${TABLES.WORKSTREAM}
52
+ WHERE organizationId = $organizationId`,
53
+ { organizationId: orgRef },
54
+ ),
55
+ )
56
+ return ids.map((value) => ensureRecordId(EntityIdRowSchema.parse(value), TABLES.WORKSTREAM))
57
+ }
58
+
59
+ export async function listEligibleWorkstreamMessages(params: {
60
+ workstreamIds: RecordIdRef[]
61
+ cursor: DigestCursor | null
62
+ onboardingCutoff: Date | null
63
+ }): Promise<DigestMessage[]> {
64
+ if (params.workstreamIds.length === 0) return []
65
+
66
+ let query: BoundQuery | null = null
67
+ if (params.cursor) {
68
+ const cursorRowId = ensureRecordId(params.cursor.id, TABLES.WORKSTREAM_MESSAGE)
69
+ query = new BoundQuery(
70
+ `SELECT type::string(id) AS id, type::string(workstreamId) AS workstreamId, role, parts, metadata, createdAt FROM ${TABLES.WORKSTREAM_MESSAGE}
71
+ WHERE workstreamId IN $workstreamIds
72
+ AND (
73
+ createdAt > $cursorCreatedAt
74
+ OR (createdAt = $cursorCreatedAt AND id > $cursorRowId)
75
+ )
76
+ ORDER BY createdAt ASC, id ASC`,
77
+ { workstreamIds: params.workstreamIds, cursorCreatedAt: params.cursor.createdAt, cursorRowId },
78
+ )
79
+ } else if (params.onboardingCutoff) {
80
+ query = new BoundQuery(
81
+ `SELECT type::string(id) AS id, type::string(workstreamId) AS workstreamId, role, parts, metadata, createdAt FROM ${TABLES.WORKSTREAM_MESSAGE}
82
+ WHERE workstreamId IN $workstreamIds
83
+ AND createdAt > $onboardingCutoff
84
+ ORDER BY createdAt ASC, id ASC`,
85
+ { workstreamIds: params.workstreamIds, onboardingCutoff: params.onboardingCutoff },
86
+ )
87
+ }
88
+
89
+ if (!query) return []
90
+
91
+ const rows = await databaseService.query<unknown>(query)
92
+ return rows.map((row) => mapWorkstreamRow(WorkstreamMessageRowSchema.parse(row)))
93
+ }
94
+
95
+ export function normalizeBlock(value: string): string {
96
+ return normalizeTextBody(value)
97
+ }
@@ -4,6 +4,11 @@ import { fileURLToPath } from 'node:url'
4
4
  import type { Job, Worker } from 'bullmq'
5
5
 
6
6
  import { chatLogger } from '../config/logger'
7
+ import { truncateText } from '../utils/string'
8
+
9
+ export const DEFAULT_JOB_RETENTION = { removeOnComplete: 200, removeOnFail: 200 }
10
+ export const LOW_JOB_RETENTION = { removeOnComplete: 50, removeOnFail: 50 }
11
+ export const LONG_JOB_LOCK_DURATION_MS = 600_000
7
12
 
8
13
  const DEFAULT_SHUTDOWN_TIMEOUT_MS = 10_000
9
14
  const MAX_TRACE_STRING_CHARS = 2_000
@@ -28,8 +33,7 @@ interface TracedWorkerJobLike {
28
33
  }
29
34
 
30
35
  function truncateTraceString(value: string, maxChars = MAX_TRACE_STRING_CHARS): string {
31
- if (value.length <= maxChars) return value
32
- return `${value.slice(0, maxChars - 3)}...`
36
+ return truncateText(value, maxChars)
33
37
  }
34
38
 
35
39
  function normalizeTraceValue(value: unknown, depth = 0): unknown {
@@ -1,3 +0,0 @@
1
- export function resolveCandidateLimit(params: { limit: number; multiplier: number; minimum: number }): number {
2
- return Math.max(params.limit * params.multiplier, params.minimum)
3
- }