@lota-sdk/core 0.4.8 → 0.4.10

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 (272) hide show
  1. package/package.json +11 -12
  2. package/src/ai/embedding-cache.ts +96 -22
  3. package/src/ai-gateway/ai-gateway.ts +766 -223
  4. package/src/config/agent-defaults.ts +189 -75
  5. package/src/config/agent-types.ts +54 -4
  6. package/src/config/background-processing.ts +1 -1
  7. package/src/config/constants.ts +8 -2
  8. package/src/config/index.ts +0 -1
  9. package/src/config/logger.ts +299 -19
  10. package/src/config/thread-defaults.ts +40 -20
  11. package/src/create-runtime.ts +200 -449
  12. package/src/db/base.service.ts +52 -28
  13. package/src/db/cursor-pagination.ts +71 -30
  14. package/src/db/memory-query-builder.ts +2 -1
  15. package/src/db/memory-store.helpers.ts +4 -7
  16. package/src/db/memory-store.ts +868 -601
  17. package/src/db/memory.ts +396 -280
  18. package/src/db/record-id.ts +32 -10
  19. package/src/db/schema-fingerprint.ts +30 -12
  20. package/src/db/service-normalization.ts +288 -0
  21. package/src/db/service.ts +912 -779
  22. package/src/db/startup.ts +153 -68
  23. package/src/db/transaction-conflict.ts +15 -0
  24. package/src/effect/awaitable-effect.ts +96 -0
  25. package/src/effect/errors.ts +121 -0
  26. package/src/effect/helpers.ts +123 -0
  27. package/src/effect/index.ts +24 -0
  28. package/src/effect/layers.ts +238 -0
  29. package/src/effect/runtime-ref.ts +25 -0
  30. package/src/effect/runtime.ts +46 -0
  31. package/src/effect/services.ts +61 -0
  32. package/src/effect/zod.ts +43 -0
  33. package/src/embeddings/provider.ts +128 -83
  34. package/src/index.ts +48 -1
  35. package/src/openrouter/direct-provider.ts +11 -35
  36. package/src/queues/autonomous-job.queue.ts +117 -73
  37. package/src/queues/context-compaction.queue.ts +50 -17
  38. package/src/queues/delayed-node-promotion.queue.ts +46 -17
  39. package/src/queues/document-processor.queue.ts +52 -77
  40. package/src/queues/memory-consolidation.queue.ts +47 -32
  41. package/src/queues/organization-learning.queue.ts +26 -4
  42. package/src/queues/plan-agent-heartbeat.queue.ts +71 -24
  43. package/src/queues/plan-scheduler.queue.ts +97 -33
  44. package/src/queues/post-chat-memory.queue.ts +56 -26
  45. package/src/queues/queue-factory.ts +227 -59
  46. package/src/queues/standalone-worker.ts +39 -0
  47. package/src/queues/title-generation.queue.ts +45 -11
  48. package/src/redis/connection.ts +182 -113
  49. package/src/redis/index.ts +6 -8
  50. package/src/redis/org-memory-lock.ts +60 -27
  51. package/src/redis/redis-lease-lock.ts +200 -121
  52. package/src/redis/runtime-connection.ts +20 -0
  53. package/src/redis/stream-context.ts +92 -46
  54. package/src/runtime/agent-identity-overrides.ts +2 -2
  55. package/src/runtime/agent-runtime-policy.ts +5 -2
  56. package/src/runtime/agent-stream-helpers.ts +24 -9
  57. package/src/runtime/chat-run-orchestration.ts +102 -19
  58. package/src/runtime/chat-run-registry.ts +36 -2
  59. package/src/runtime/context-compaction/context-compaction-runtime.ts +107 -0
  60. package/src/runtime/{context-compaction.ts → context-compaction/context-compaction.ts} +161 -94
  61. package/src/runtime/domain-layer.ts +192 -0
  62. package/src/runtime/execution-plan-visibility.ts +2 -2
  63. package/src/runtime/execution-plan.ts +42 -15
  64. package/src/runtime/graph-designer.ts +16 -4
  65. package/src/runtime/helper-model.ts +139 -48
  66. package/src/runtime/index.ts +7 -8
  67. package/src/runtime/indexed-repositories-policy.ts +3 -3
  68. package/src/runtime/{memory-block.ts → memory/memory-block.ts} +50 -36
  69. package/src/runtime/{memory-digest-policy.ts → memory/memory-digest-policy.ts} +1 -1
  70. package/src/runtime/{memory-pipeline.ts → memory/memory-pipeline.ts} +54 -67
  71. package/src/runtime/{memory-prompts-fact.ts → memory/memory-prompts-fact.ts} +2 -2
  72. package/src/runtime/memory/memory-scope.ts +53 -0
  73. package/src/runtime/plugin-resolution.ts +124 -25
  74. package/src/runtime/plugin-types.ts +9 -1
  75. package/src/runtime/post-turn-side-effects.ts +177 -130
  76. package/src/runtime/retrieval-adapters.ts +40 -6
  77. package/src/runtime/runtime-accessors.ts +92 -0
  78. package/src/runtime/runtime-config.ts +150 -61
  79. package/src/runtime/runtime-extensions.ts +23 -25
  80. package/src/runtime/runtime-lifecycle.ts +124 -0
  81. package/src/runtime/runtime-services.ts +386 -0
  82. package/src/runtime/runtime-token.ts +47 -0
  83. package/src/runtime/social-chat/social-chat-agent-runner.ts +159 -0
  84. package/src/runtime/{social-chat-history.ts → social-chat/social-chat-history.ts} +51 -20
  85. package/src/runtime/social-chat/social-chat.ts +630 -0
  86. package/src/runtime/specialist-runner.ts +36 -10
  87. package/src/runtime/team-consultation/team-consultation-orchestrator.ts +433 -0
  88. package/src/runtime/{team-consultation-prompts.ts → team-consultation/team-consultation-prompts.ts} +6 -2
  89. package/src/runtime/thread-chat-helpers.ts +2 -2
  90. package/src/runtime/thread-plan-turn.ts +2 -1
  91. package/src/runtime/thread-turn-context.ts +183 -111
  92. package/src/runtime/turn-lifecycle.ts +93 -27
  93. package/src/services/agent-activity.service.ts +287 -203
  94. package/src/services/agent-executor.service.ts +253 -149
  95. package/src/services/artifact.service.ts +231 -149
  96. package/src/services/attachment.service.ts +171 -115
  97. package/src/services/autonomous-job.service.ts +890 -491
  98. package/src/services/background-work.service.ts +54 -0
  99. package/src/services/chat-run-registry.service.ts +13 -1
  100. package/src/services/context-compaction.service.ts +136 -86
  101. package/src/services/document-chunk.service.ts +151 -88
  102. package/src/services/execution-plan/execution-plan-approval.ts +26 -0
  103. package/src/services/execution-plan/execution-plan-context.ts +29 -0
  104. package/src/services/execution-plan/execution-plan-graph.ts +278 -0
  105. package/src/services/execution-plan/execution-plan-schedule.ts +84 -0
  106. package/src/services/execution-plan/execution-plan-spec.ts +75 -0
  107. package/src/services/execution-plan/execution-plan.service.ts +1041 -0
  108. package/src/services/feedback-loop.service.ts +132 -76
  109. package/src/services/global-orchestrator.service.ts +101 -168
  110. package/src/services/graph-full-routing.ts +193 -0
  111. package/src/services/index.ts +19 -21
  112. package/src/services/institutional-memory.service.ts +213 -125
  113. package/src/services/learned-skill.service.ts +368 -260
  114. package/src/services/memory/memory-conversation.ts +95 -0
  115. package/src/services/memory/memory-errors.ts +27 -0
  116. package/src/services/memory/memory-org-memory.ts +50 -0
  117. package/src/services/memory/memory-preseeded.ts +86 -0
  118. package/src/services/memory/memory-rerank.ts +297 -0
  119. package/src/services/{memory-utils.ts → memory/memory-utils.ts} +6 -5
  120. package/src/services/memory/memory.service.ts +674 -0
  121. package/src/services/memory/rerank.service.ts +201 -0
  122. package/src/services/monitoring-window.service.ts +92 -70
  123. package/src/services/mutating-approval.service.ts +62 -53
  124. package/src/services/node-workspace.service.ts +141 -98
  125. package/src/services/notification.service.ts +29 -16
  126. package/src/services/organization-member.service.ts +120 -66
  127. package/src/services/organization.service.ts +153 -77
  128. package/src/services/ownership-dispatcher.service.ts +456 -263
  129. package/src/services/plan/plan-agent-heartbeat.service.ts +234 -0
  130. package/src/services/plan/plan-agent-query.service.ts +322 -0
  131. package/src/services/{plan-approval.service.ts → plan/plan-approval.service.ts} +45 -22
  132. package/src/services/plan/plan-artifact.service.ts +60 -0
  133. package/src/services/plan/plan-builder.service.ts +76 -0
  134. package/src/services/plan/plan-checkpoint.service.ts +103 -0
  135. package/src/services/{plan-compiler.service.ts → plan/plan-compiler.service.ts} +26 -9
  136. package/src/services/plan/plan-completion-side-effects.ts +169 -0
  137. package/src/services/plan/plan-coordination.service.ts +181 -0
  138. package/src/services/plan/plan-cycle.service.ts +405 -0
  139. package/src/services/plan/plan-deadline.service.ts +533 -0
  140. package/src/services/plan/plan-event-delivery.service.ts +266 -0
  141. package/src/services/plan/plan-executor-context.ts +35 -0
  142. package/src/services/plan/plan-executor-graph.ts +522 -0
  143. package/src/services/plan/plan-executor-helpers.ts +307 -0
  144. package/src/services/plan/plan-executor-persistence.ts +209 -0
  145. package/src/services/plan/plan-executor.service.ts +1737 -0
  146. package/src/services/{plan-helpers.ts → plan/plan-helpers.ts} +1 -1
  147. package/src/services/{plan-run-data.ts → plan/plan-run-data.ts} +4 -4
  148. package/src/services/plan/plan-run-serialization.ts +15 -0
  149. package/src/services/plan/plan-run.service.ts +637 -0
  150. package/src/services/plan/plan-scheduler.service.ts +379 -0
  151. package/src/services/plan/plan-template.service.ts +224 -0
  152. package/src/services/plan/plan-transaction-events.ts +36 -0
  153. package/src/services/plan/plan-validator.service.ts +907 -0
  154. package/src/services/plan/plan-workspace.service.ts +131 -0
  155. package/src/services/plugin-executor.service.ts +102 -68
  156. package/src/services/quality-metrics.service.ts +112 -94
  157. package/src/services/queue-job.service.ts +288 -231
  158. package/src/services/recent-activity-title.service.ts +73 -36
  159. package/src/services/recent-activity.service.ts +274 -259
  160. package/src/services/skill-resolver.service.ts +38 -12
  161. package/src/services/social-chat-history.service.ts +190 -122
  162. package/src/services/system-executor.service.ts +96 -61
  163. package/src/services/thread/thread-active-run.ts +203 -0
  164. package/src/services/thread/thread-bootstrap.ts +385 -0
  165. package/src/services/thread/thread-listing.ts +199 -0
  166. package/src/services/thread/thread-memory-block.ts +130 -0
  167. package/src/services/thread/thread-message.service.ts +379 -0
  168. package/src/services/thread/thread-record-store.ts +155 -0
  169. package/src/services/thread/thread-title.service.ts +74 -0
  170. package/src/services/thread/thread-turn-execution.ts +280 -0
  171. package/src/services/thread/thread-turn-message-context.ts +73 -0
  172. package/src/services/thread/thread-turn-preparation.service.ts +1148 -0
  173. package/src/services/thread/thread-turn-streaming.ts +403 -0
  174. package/src/services/thread/thread-turn-tracing.ts +35 -0
  175. package/src/services/thread/thread-turn.ts +376 -0
  176. package/src/services/thread/thread.service.ts +344 -0
  177. package/src/services/user.service.ts +82 -32
  178. package/src/services/write-intent-validator.service.ts +63 -51
  179. package/src/storage/attachment-parser.ts +69 -27
  180. package/src/storage/attachment-storage.service.ts +334 -275
  181. package/src/storage/generated-document-storage.service.ts +66 -34
  182. package/src/system-agents/agent-result.ts +3 -1
  183. package/src/system-agents/context-compaction.agent.ts +3 -3
  184. package/src/system-agents/delegated-agent-factory.ts +159 -90
  185. package/src/system-agents/helper-agent-options.ts +1 -1
  186. package/src/system-agents/memory-reranker.agent.ts +3 -3
  187. package/src/system-agents/memory.agent.ts +3 -3
  188. package/src/system-agents/recent-activity-title-refiner.agent.ts +3 -3
  189. package/src/system-agents/regular-chat-memory-digest.agent.ts +3 -3
  190. package/src/system-agents/skill-extractor.agent.ts +3 -3
  191. package/src/system-agents/skill-manager.agent.ts +3 -3
  192. package/src/system-agents/thread-router.agent.ts +157 -113
  193. package/src/system-agents/title-generator.agent.ts +3 -3
  194. package/src/tools/execution-plan.tool.ts +241 -171
  195. package/src/tools/fetch-webpage.tool.ts +29 -18
  196. package/src/tools/firecrawl-client.ts +26 -6
  197. package/src/tools/index.ts +1 -0
  198. package/src/tools/memory-block.tool.ts +14 -6
  199. package/src/tools/plan-approval.tool.ts +57 -47
  200. package/src/tools/read-file-parts.tool.ts +44 -33
  201. package/src/tools/remember-memory.tool.ts +65 -45
  202. package/src/tools/search-web.tool.ts +33 -22
  203. package/src/tools/search.tool.ts +41 -29
  204. package/src/tools/team-think.tool.ts +125 -84
  205. package/src/tools/user-questions.tool.ts +4 -3
  206. package/src/tools/web-tool-shared.ts +6 -0
  207. package/src/utils/async.ts +25 -22
  208. package/src/utils/crypto.ts +21 -0
  209. package/src/utils/date-time.ts +40 -1
  210. package/src/utils/errors.ts +111 -20
  211. package/src/utils/hono-error-handler.ts +24 -39
  212. package/src/utils/index.ts +2 -1
  213. package/src/utils/null-proto-record.ts +41 -0
  214. package/src/utils/sse-keepalive.ts +124 -21
  215. package/src/workers/bootstrap.ts +164 -52
  216. package/src/workers/memory-consolidation.worker.ts +325 -237
  217. package/src/workers/organization-learning.worker.ts +50 -16
  218. package/src/workers/regular-chat-memory-digest.helpers.ts +28 -27
  219. package/src/workers/regular-chat-memory-digest.runner.ts +185 -114
  220. package/src/workers/skill-extraction.runner.ts +176 -93
  221. package/src/workers/utils/file-section-chunker.ts +8 -10
  222. package/src/workers/utils/repo-structure-extractor.ts +349 -260
  223. package/src/workers/utils/repomix-file-sections.ts +2 -2
  224. package/src/workers/utils/thread-message-query.ts +97 -38
  225. package/src/workers/worker-utils.ts +74 -31
  226. package/src/config/debug-logger.ts +0 -47
  227. package/src/config/search.ts +0 -3
  228. package/src/redis/connection-accessor.ts +0 -26
  229. package/src/runtime/agent-types.ts +0 -1
  230. package/src/runtime/context-compaction-runtime.ts +0 -87
  231. package/src/runtime/memory-scope.ts +0 -43
  232. package/src/runtime/social-chat-agent-runner.ts +0 -118
  233. package/src/runtime/social-chat.ts +0 -516
  234. package/src/runtime/team-consultation-orchestrator.ts +0 -272
  235. package/src/services/adaptive-playbook.service.ts +0 -152
  236. package/src/services/artifact-provenance.service.ts +0 -172
  237. package/src/services/chat-attachments.service.ts +0 -17
  238. package/src/services/context-compaction-runtime.singleton.ts +0 -13
  239. package/src/services/execution-plan.service.ts +0 -1118
  240. package/src/services/memory.service.ts +0 -914
  241. package/src/services/plan-agent-heartbeat.service.ts +0 -136
  242. package/src/services/plan-agent-query.service.ts +0 -267
  243. package/src/services/plan-artifact.service.ts +0 -50
  244. package/src/services/plan-builder.service.ts +0 -67
  245. package/src/services/plan-checkpoint.service.ts +0 -81
  246. package/src/services/plan-completion-side-effects.ts +0 -80
  247. package/src/services/plan-coordination.service.ts +0 -157
  248. package/src/services/plan-cycle.service.ts +0 -284
  249. package/src/services/plan-deadline.service.ts +0 -430
  250. package/src/services/plan-event-delivery.service.ts +0 -166
  251. package/src/services/plan-executor.service.ts +0 -1950
  252. package/src/services/plan-run.service.ts +0 -515
  253. package/src/services/plan-scheduler.service.ts +0 -240
  254. package/src/services/plan-template.service.ts +0 -177
  255. package/src/services/plan-validator.service.ts +0 -818
  256. package/src/services/plan-workspace.service.ts +0 -83
  257. package/src/services/rerank.service.ts +0 -156
  258. package/src/services/thread-message.service.ts +0 -275
  259. package/src/services/thread-plan-registry.service.ts +0 -22
  260. package/src/services/thread-title.service.ts +0 -39
  261. package/src/services/thread-turn-preparation.service.ts +0 -1147
  262. package/src/services/thread-turn.ts +0 -172
  263. package/src/services/thread.service.ts +0 -869
  264. package/src/utils/env.ts +0 -8
  265. /package/src/runtime/{context-compaction-constants.ts → context-compaction/context-compaction-constants.ts} +0 -0
  266. /package/src/runtime/{memory-format.ts → memory/memory-format.ts} +0 -0
  267. /package/src/runtime/{memory-prompts-parse.ts → memory/memory-prompts-parse.ts} +0 -0
  268. /package/src/runtime/{memory-prompts-update.ts → memory/memory-prompts-update.ts} +0 -0
  269. /package/src/runtime/{social-chat-prompts.ts → social-chat/social-chat-prompts.ts} +0 -0
  270. /package/src/services/{plan-node-spec.ts → plan/plan-node-spec.ts} +0 -0
  271. /package/src/services/{thread-constants.ts → thread/thread-constants.ts} +0 -0
  272. /package/src/services/{thread.types.ts → thread/thread.types.ts} +0 -0
@@ -1,869 +0,0 @@
1
- import { THREAD, sdkThreadStatusSchema } from '@lota-sdk/shared'
2
- import { BoundQuery, RecordId, StringRecordId, surql } from 'surrealdb'
3
-
4
- import { agentDisplayNames, agentRoster, getCoreThreadProfile, isAgentName } from '../config/agent-defaults'
5
- import { serverLogger } from '../config/logger'
6
- import { getThreadBootstrapConfig } from '../config/thread-defaults'
7
- import { BaseService } from '../db/base.service'
8
- import { ensureRecordId, recordIdToString } from '../db/record-id'
9
- import type { RecordIdInput, RecordIdRef } from '../db/record-id'
10
- import { databaseService } from '../db/service'
11
- import type { DatabaseTable } from '../db/tables'
12
- import { TABLES } from '../db/tables'
13
- import { getRedisConnection, withRedisLeaseLock } from '../redis'
14
- import {
15
- appendToMemoryBlock,
16
- compactMemoryBlockEntries,
17
- formatPersistedMemoryBlockForPrompt,
18
- parseMemoryBlock,
19
- serializeMemoryBlock,
20
- } from '../runtime/memory-block'
21
- import { toIsoDateTimeString } from '../utils/date-time'
22
- import { chatRunRegistry } from './chat-run-registry.service'
23
- import { contextCompactionService } from './context-compaction.service'
24
- import { MEMORY_BLOCK_COMPACTION_CHUNK_ENTRIES, MEMORY_BLOCK_COMPACTION_TRIGGER_ENTRIES } from './thread-constants'
25
- import { threadMessageService } from './thread-message.service'
26
- import { NormalizedThreadSchema, PublicThreadSchema, ThreadSchema } from './thread.types'
27
- import type { NormalizedThread, PublicThread, ThreadRecord } from './thread.types'
28
-
29
- const THREAD_ACTIVE_RUN_LOCK_TTL_MS = 90_000
30
- const THREAD_ACTIVE_RUN_LOCK_MAX_WAIT_MS = 750
31
- const THREAD_ACTIVE_RUN_LOCK_RETRY_DELAY_MS = 75
32
- const THREAD_BOOTSTRAP_LOCK_TTL_MS = 15_000
33
- const THREAD_BOOTSTRAP_LOCK_REFRESH_INTERVAL_MS = 5_000
34
- const THREAD_BOOTSTRAP_LOCK_MAX_WAIT_MS = 5_000
35
- const THREAD_BOOTSTRAP_LOCK_RETRY_DELAY_MS = 100
36
- const THREAD_UNIQUE_LOOKUP_MAX_ATTEMPTS = 20
37
- const THREAD_UNIQUE_LOOKUP_RETRY_DELAY_MS = 100
38
-
39
- function isRecordIdInput(value: unknown): value is RecordIdInput {
40
- if (typeof value === 'string' || value instanceof RecordId || value instanceof StringRecordId) {
41
- return true
42
- }
43
-
44
- if (!value || typeof value !== 'object') {
45
- return false
46
- }
47
-
48
- const record = value as { tb?: unknown; id?: unknown }
49
- return typeof record.tb === 'string' && record.id !== undefined
50
- }
51
-
52
- function getAgentDisplayName(agentId: string): string {
53
- return agentDisplayNames[agentId] ?? agentId
54
- }
55
-
56
- function buildActiveRunLockKey(threadId: RecordIdRef): string {
57
- return `thread-active-run:${recordIdToString(ensureRecordId(threadId, TABLES.THREAD), TABLES.THREAD)}`
58
- }
59
-
60
- function buildBootstrapThreadsLockKey(userId: RecordIdRef, orgId: RecordIdRef): string {
61
- return `thread-bootstrap:${recordIdToString(ensureRecordId(userId, TABLES.USER), TABLES.USER)}:${recordIdToString(ensureRecordId(orgId, TABLES.ORGANIZATION), TABLES.ORGANIZATION)}`
62
- }
63
-
64
- function buildListThreadsQuery(options: {
65
- includeArchived: boolean
66
- paginate: boolean
67
- type?: string
68
- types?: string[]
69
- }): string {
70
- const clauses = [`SELECT * FROM ${TABLES.THREAD}`, 'WHERE userId = $userId', ' AND organizationId = $orgId']
71
-
72
- if (options.types) {
73
- clauses.push(' AND type IN $types')
74
- } else if (options.type) {
75
- clauses.push(' AND type = $type')
76
- }
77
-
78
- if (!options.includeArchived) {
79
- clauses.push(' AND status = "active"')
80
- }
81
- clauses.push('ORDER BY updatedAt DESC')
82
- if (options.paginate) {
83
- clauses.push('LIMIT $limit START $offset')
84
- }
85
- return clauses.join('\n')
86
- }
87
-
88
- function normalizeActiveTurnValue(value: unknown): string | null {
89
- if (typeof value !== 'string') {
90
- return null
91
- }
92
-
93
- const normalized = value.trim()
94
- return normalized.length > 0 ? normalized : null
95
- }
96
-
97
- function assertMutableThread(thread: ThreadRecord): void {
98
- if (thread.type === 'default') throw new Error('Default threads cannot be modified')
99
- if (thread.type === 'thread') throw new Error('Thread threads cannot be modified')
100
- }
101
-
102
- function isUniqueConstraintError(error: unknown): boolean {
103
- if (!(error instanceof Error)) return false
104
- return error.message.includes('already contains')
105
- }
106
-
107
- function requireExistingThread(
108
- record: ThreadRecord | null,
109
- params: {
110
- type: 'default' | 'thread'
111
- organizationId: RecordIdRef
112
- userId: RecordIdRef
113
- agentId?: string
114
- threadType?: string
115
- },
116
- ): ThreadRecord {
117
- if (record) {
118
- return record
119
- }
120
-
121
- const scope = {
122
- organizationId: recordIdToString(params.organizationId, TABLES.ORGANIZATION),
123
- userId: recordIdToString(params.userId, TABLES.USER),
124
- ...(params.agentId ? { agentId: params.agentId } : {}),
125
- ...(params.threadType ? { threadType: params.threadType } : {}),
126
- }
127
- throw new Error(`Thread lookup failed after duplicate ${params.type} thread create: ${JSON.stringify(scope)}`)
128
- }
129
-
130
- type UniqueThreadLookupParams = Parameters<typeof requireExistingThread>[1]
131
-
132
- function haveSameMembers(left: string[], right: string[]): boolean {
133
- return left.length === right.length && left.every((value, index) => value === right[index])
134
- }
135
-
136
- export class ActiveThreadRunConflictError extends Error {
137
- constructor() {
138
- super('A chat run is already active.')
139
- this.name = 'ActiveThreadRunConflictError'
140
- }
141
- }
142
-
143
- class ThreadService extends BaseService<typeof ThreadSchema> {
144
- constructor() {
145
- super(TABLES.THREAD, ThreadSchema)
146
- }
147
-
148
- async createThread(input: {
149
- userId: RecordIdRef
150
- organizationId: RecordIdRef
151
- type: string
152
- agentId?: string
153
- threadType?: string
154
- members?: string[]
155
- title?: string
156
- }): Promise<NormalizedThread> {
157
- switch (input.type) {
158
- case 'default':
159
- if (!input.agentId) throw new Error('Default threads require agentId')
160
- if (input.threadType) throw new Error('Default threads cannot have threadType')
161
- break
162
- case 'topic':
163
- if (!input.agentId) throw new Error('Topic threads require agentId')
164
- if (input.threadType) throw new Error('Topic threads cannot have threadType')
165
- break
166
- case 'thread':
167
- if (!input.threadType) throw new Error('Thread threads require threadType')
168
- if (input.agentId) throw new Error('Thread threads cannot have agentId')
169
- break
170
- case 'group':
171
- if (input.agentId) throw new Error('Group threads cannot have agentId')
172
- if (input.threadType) throw new Error('Group threads cannot have threadType')
173
- break
174
- }
175
-
176
- const title = input.title ?? THREAD.DEFAULT_TITLE
177
- const nameGenerated = input.title !== undefined && input.title !== THREAD.DEFAULT_TITLE
178
-
179
- if (input.type === 'default') {
180
- const agentId = input.agentId
181
- if (!agentId) {
182
- throw new Error('Default threads require agentId')
183
- }
184
- const { record } = await this.getOrCreateDefault(input.organizationId, input.userId, agentId)
185
- return await this.toNormalizedThread(record)
186
- }
187
-
188
- if (input.type === 'thread') {
189
- const threadType = input.threadType
190
- if (!threadType) {
191
- throw new Error('Thread threads require threadType')
192
- }
193
- const { record } = await this.getOrCreateThread(input.organizationId, input.userId, threadType, {
194
- members: input.members ?? [...agentRoster],
195
- title,
196
- nameGenerated,
197
- })
198
- return await this.toNormalizedThread(record)
199
- }
200
-
201
- const thread = await this.create({
202
- userId: input.userId,
203
- organizationId: input.organizationId,
204
- type: input.type,
205
- agentId: input.agentId,
206
- threadType: input.threadType,
207
- members: input.members ?? [...agentRoster],
208
- title,
209
- status: 'active',
210
- nameGenerated,
211
- isCompacting: false,
212
- turnCount: 0,
213
- })
214
-
215
- return await this.toNormalizedThread(thread)
216
- }
217
-
218
- async getOrCreateDefault(
219
- orgId: RecordIdRef,
220
- userId: RecordIdRef,
221
- agentId: string,
222
- config?: { title?: string; nameGenerated?: boolean },
223
- ): Promise<{ record: ThreadRecord; created: boolean }> {
224
- const lookup = { type: 'default' as const, organizationId: orgId, userId, agentId }
225
- const existing = await this.findThreadByUniqueLookup(lookup)
226
- if (existing) {
227
- return { record: await this.syncThreadConfig(existing, config), created: false }
228
- }
229
-
230
- try {
231
- const record = await this.create({
232
- type: 'default',
233
- organizationId: orgId,
234
- userId,
235
- agentId,
236
- members: [agentId],
237
- title: config?.title ?? getAgentDisplayName(agentId),
238
- status: 'active',
239
- nameGenerated: config?.nameGenerated ?? false,
240
- isCompacting: false,
241
- turnCount: 0,
242
- })
243
- return { record, created: true }
244
- } catch (e) {
245
- if (isUniqueConstraintError(e)) {
246
- return { record: await this.syncThreadConfig(await this.waitForExistingThread(lookup), config), created: false }
247
- }
248
- throw e
249
- }
250
- }
251
-
252
- async getOrCreateThread(
253
- orgId: RecordIdRef,
254
- userId: RecordIdRef,
255
- threadType: string,
256
- config: { members: string[]; title: string; nameGenerated?: boolean },
257
- ): Promise<{ record: ThreadRecord; created: boolean }> {
258
- const lookup = { type: 'thread' as const, organizationId: orgId, userId, threadType }
259
- const existing = await this.findThreadByUniqueLookup(lookup)
260
- if (existing) {
261
- return { record: await this.syncThreadConfig(existing, config), created: false }
262
- }
263
-
264
- try {
265
- const record = await this.create({
266
- type: 'thread',
267
- organizationId: orgId,
268
- userId,
269
- threadType,
270
- members: config.members,
271
- title: config.title,
272
- status: 'active',
273
- nameGenerated: config.nameGenerated ?? false,
274
- isCompacting: false,
275
- turnCount: 0,
276
- })
277
- return { record, created: true }
278
- } catch (e) {
279
- if (isUniqueConstraintError(e)) {
280
- return { record: await this.syncThreadConfig(await this.waitForExistingThread(lookup), config), created: false }
281
- }
282
- throw e
283
- }
284
- }
285
-
286
- private async syncThreadConfig(
287
- record: ThreadRecord,
288
- config?: { title?: string; nameGenerated?: boolean; members?: string[] },
289
- ): Promise<ThreadRecord> {
290
- if (!config) {
291
- return record
292
- }
293
-
294
- const updates: Partial<ThreadRecord> = {}
295
-
296
- if (config.title !== undefined && record.title !== config.title) {
297
- updates.title = config.title
298
- }
299
- if (config.nameGenerated !== undefined && record.nameGenerated !== config.nameGenerated) {
300
- updates.nameGenerated = config.nameGenerated
301
- }
302
- if (config.members !== undefined && !haveSameMembers(record.members, config.members)) {
303
- updates.members = config.members
304
- }
305
-
306
- if (Object.keys(updates).length === 0) {
307
- return record
308
- }
309
-
310
- return await this.update(record.id, updates)
311
- }
312
-
313
- private async findThreadByUniqueLookup(params: UniqueThreadLookupParams): Promise<ThreadRecord | null> {
314
- return await this.databaseService.findOne(
315
- this.table,
316
- {
317
- type: params.type,
318
- organizationId: params.organizationId,
319
- userId: params.userId,
320
- ...(params.agentId ? { agentId: params.agentId } : {}),
321
- ...(params.threadType ? { threadType: params.threadType } : {}),
322
- },
323
- ThreadSchema,
324
- )
325
- }
326
-
327
- private async waitForExistingThread(params: UniqueThreadLookupParams): Promise<ThreadRecord> {
328
- for (let attempt = 0; attempt < THREAD_UNIQUE_LOOKUP_MAX_ATTEMPTS; attempt += 1) {
329
- const record = await this.findThreadByUniqueLookup(params)
330
- if (record) {
331
- return record
332
- }
333
- if (attempt < THREAD_UNIQUE_LOOKUP_MAX_ATTEMPTS - 1) {
334
- await Bun.sleep(THREAD_UNIQUE_LOOKUP_RETRY_DELAY_MS)
335
- }
336
- }
337
-
338
- return requireExistingThread(null, params)
339
- }
340
-
341
- async ensureBootstrapThreads(
342
- userId: RecordIdRef,
343
- orgId: RecordIdRef,
344
- options?: { onboardStatus?: string; userName?: string | null },
345
- ): Promise<void> {
346
- await withRedisLeaseLock(
347
- {
348
- redis: getRedisConnection(),
349
- lockKey: buildBootstrapThreadsLockKey(userId, orgId),
350
- lockTtlMs: THREAD_BOOTSTRAP_LOCK_TTL_MS,
351
- refreshIntervalMs: THREAD_BOOTSTRAP_LOCK_REFRESH_INTERVAL_MS,
352
- retryDelayMs: THREAD_BOOTSTRAP_LOCK_RETRY_DELAY_MS,
353
- maxWaitMs: THREAD_BOOTSTRAP_LOCK_MAX_WAIT_MS,
354
- label: 'thread bootstrap',
355
- logger: serverLogger,
356
- },
357
- async (signal) => {
358
- const throwIfAborted = () => {
359
- if (signal.aborted) {
360
- throw signal.reason instanceof Error ? signal.reason : new Error('Thread bootstrap lease was aborted.')
361
- }
362
- }
363
-
364
- throwIfAborted()
365
- const onboardStatus = options?.onboardStatus ?? 'completed'
366
- const onboardingCompleted = onboardStatus === 'completed'
367
- const bootstrapConfig = getThreadBootstrapConfig()
368
-
369
- const existingThreads = await databaseService.findMany(
370
- TABLES.THREAD,
371
- { userId, organizationId: orgId },
372
- ThreadSchema,
373
- )
374
- throwIfAborted()
375
-
376
- const hasGroupThread = existingThreads.some((t) => t.type === 'group')
377
- const defaultThreadsByAgent = new Map<string, ThreadRecord>()
378
- const threadThreadsByType = new Map<string, ThreadRecord>()
379
-
380
- for (const thread of existingThreads) {
381
- if (thread.type === 'default' && thread.agentId) {
382
- defaultThreadsByAgent.set(thread.agentId, thread)
383
- }
384
- if (thread.type === 'thread' && typeof thread.threadType === 'string') {
385
- threadThreadsByType.set(thread.threadType, thread)
386
- }
387
- }
388
-
389
- const requiredDefaultAgents = onboardingCompleted
390
- ? bootstrapConfig.completedDefaultAgents
391
- : bootstrapConfig.onboardingDefaultAgents
392
-
393
- const creationTasks: Array<() => Promise<{ record: ThreadRecord; created: boolean }>> = []
394
-
395
- for (const agentId of requiredDefaultAgents) {
396
- if (defaultThreadsByAgent.has(agentId)) continue
397
- creationTasks.push(async () => await this.getOrCreateDefault(orgId, userId, agentId))
398
- }
399
-
400
- if (onboardingCompleted && bootstrapConfig.ensureDefaultGroupOnCompleted && !hasGroupThread) {
401
- creationTasks.push(
402
- async () =>
403
- await this.createThread({
404
- userId,
405
- organizationId: orgId,
406
- type: 'group',
407
- title: THREAD.DEFAULT_TITLE,
408
- }).then(async (normalized) => ({
409
- record: await this.getById(ensureRecordId(normalized.id, TABLES.THREAD)),
410
- created: true,
411
- })),
412
- )
413
- }
414
-
415
- if (onboardingCompleted) {
416
- for (const wsType of bootstrapConfig.threadTypesAfterOnboarding) {
417
- if (threadThreadsByType.has(wsType)) continue
418
- const profile = getCoreThreadProfile(wsType)
419
- creationTasks.push(
420
- async () =>
421
- await this.getOrCreateThread(orgId, userId, wsType, {
422
- members: [...profile.members],
423
- title: profile.config.title,
424
- }),
425
- )
426
- }
427
- }
428
-
429
- let createdResults: { record: ThreadRecord; created: boolean }[] = []
430
- if (creationTasks.length > 0) {
431
- for (const runCreation of creationTasks) {
432
- throwIfAborted()
433
- createdResults.push(await runCreation())
434
- throwIfAborted()
435
- }
436
- }
437
-
438
- const onboardingWelcome = bootstrapConfig.onboardingWelcome
439
- if (!onboardingCompleted && onboardingWelcome) {
440
- const createdOwnerThread = createdResults.find(
441
- (r) => r.created && r.record.type === 'default' && r.record.agentId === onboardingWelcome.defaultAgentId,
442
- )
443
- const existingOwnerThread = defaultThreadsByAgent.get(onboardingWelcome.defaultAgentId)
444
-
445
- const ownerThreadId = createdOwnerThread?.record.id ?? existingOwnerThread?.id
446
-
447
- if (ownerThreadId) {
448
- throwIfAborted()
449
- const ownerThreadRef = ensureRecordId(ownerThreadId, TABLES.THREAD)
450
- await threadMessageService.ensureBootstrapWelcomeMessage({
451
- threadId: ownerThreadRef,
452
- agentId: onboardingWelcome.defaultAgentId,
453
- text: onboardingWelcome.buildMessageText({ userName: options?.userName }),
454
- })
455
- throwIfAborted()
456
- }
457
- }
458
- },
459
- )
460
- }
461
-
462
- async listThreads(
463
- userId: RecordIdRef,
464
- orgId: RecordIdRef,
465
- options: { type?: string; types?: string[]; take?: number; page?: number; includeArchived?: boolean },
466
- ): Promise<{ threads: NormalizedThread[]; hasMore: boolean }> {
467
- const includeArchived = options.includeArchived ?? false
468
- const type = options.type
469
- const types = options.types
470
-
471
- if (type === 'default' || type === 'thread') {
472
- const vars: Record<string, unknown> = { userId, orgId, type }
473
- const threads = await databaseService.queryMany<typeof ThreadSchema>(
474
- new BoundQuery(buildListThreadsQuery({ includeArchived, paginate: false, type }), vars),
475
- ThreadSchema,
476
- )
477
- return { threads: await this.toNormalizedThreads(threads, { checkLease: false }), hasMore: false }
478
- }
479
-
480
- const take = options.take ?? THREAD.DEFAULT_PAGE_LIMIT
481
- const page = options.page ?? 1
482
- const vars: Record<string, unknown> = { userId, orgId, limit: take + 1, offset: (page - 1) * take }
483
-
484
- if (types) {
485
- vars.types = types
486
- } else if (type) {
487
- vars.type = type
488
- }
489
-
490
- const threads = await databaseService.queryMany<typeof ThreadSchema>(
491
- new BoundQuery(buildListThreadsQuery({ includeArchived, paginate: true, type, types }), vars),
492
- ThreadSchema,
493
- )
494
-
495
- const hasMore = threads.length > take
496
- const sliced = hasMore ? threads.slice(0, take) : threads
497
-
498
- return { threads: await this.toNormalizedThreads(sliced, { checkLease: false }), hasMore }
499
- }
500
-
501
- async listOrganizationThreads(params: {
502
- orgId: RecordIdRef
503
- type?: string
504
- agentId?: string
505
- includeArchived?: boolean
506
- }): Promise<NormalizedThread[]> {
507
- const whereClauses = ['organizationId = $orgId']
508
- const variables: Record<string, unknown> = { orgId: params.orgId }
509
-
510
- if (params.type) {
511
- whereClauses.push('type = $type')
512
- variables.type = params.type
513
- }
514
-
515
- if (params.agentId) {
516
- whereClauses.push('agentId = $agentId')
517
- variables.agentId = params.agentId
518
- }
519
-
520
- if (params.includeArchived !== true) {
521
- whereClauses.push('status = "active"')
522
- }
523
-
524
- const threads = await databaseService.queryMany<typeof ThreadSchema>(
525
- new BoundQuery(
526
- `SELECT * FROM ${TABLES.THREAD}
527
- WHERE ${whereClauses.join('\n AND ')}
528
- ORDER BY createdAt ASC, id ASC`,
529
- variables,
530
- ),
531
- ThreadSchema,
532
- )
533
-
534
- return await this.toNormalizedThreads(threads, { checkLease: false })
535
- }
536
-
537
- async getThread(threadId: RecordIdRef): Promise<NormalizedThread> {
538
- const thread = await this.getById(threadId)
539
- return await this.toNormalizedThread(thread)
540
- }
541
-
542
- async updateTitle(threadId: RecordIdRef, title: string): Promise<NormalizedThread> {
543
- const existing = await this.getById(threadId)
544
- assertMutableThread(existing)
545
- const thread = await this.update(threadId, { title, nameGenerated: true })
546
- return await this.toNormalizedThread(thread)
547
- }
548
-
549
- async updateStatus(threadId: RecordIdRef, status: string): Promise<NormalizedThread> {
550
- const validStatus = sdkThreadStatusSchema.parse(status)
551
- const existing = await this.getById(threadId)
552
- assertMutableThread(existing)
553
- const thread = await this.update(threadId, { status: validStatus })
554
- return await this.toNormalizedThread(thread)
555
- }
556
-
557
- async setActiveTurn(threadId: RecordIdRef, runId: string, streamId?: string | null): Promise<void> {
558
- const threadRef = ensureRecordId(threadId, TABLES.THREAD)
559
- if (streamId === null || streamId === undefined) {
560
- await databaseService.query<unknown>(surql`
561
- UPDATE ONLY ${threadRef}
562
- SET activeRunId = ${runId},
563
- activeStreamId = NONE
564
- `)
565
- return
566
- }
567
-
568
- await databaseService.query<unknown>(surql`
569
- UPDATE ONLY ${threadRef}
570
- SET activeRunId = ${runId},
571
- activeStreamId = ${streamId}
572
- `)
573
- }
574
-
575
- async getActiveTurn(threadId: RecordIdRef): Promise<{ runId: string | null; streamId: string | null }> {
576
- const thread = await this.getById(threadId)
577
- return {
578
- runId: normalizeActiveTurnValue(thread.activeRunId),
579
- streamId: normalizeActiveTurnValue(thread.activeStreamId),
580
- }
581
- }
582
-
583
- async getActiveRunId(threadId: RecordIdRef): Promise<string | null> {
584
- const { runId } = await this.getActiveTurn(threadId)
585
- return runId
586
- }
587
-
588
- async hasActiveRunLease(threadId: RecordIdRef): Promise<boolean> {
589
- const count = await getRedisConnection().exists(buildActiveRunLockKey(threadId))
590
- return count > 0
591
- }
592
-
593
- async withActiveRunLease<T>(threadId: RecordIdRef, fn: (signal: AbortSignal) => Promise<T>): Promise<T> {
594
- try {
595
- return await withRedisLeaseLock(
596
- {
597
- redis: getRedisConnection(),
598
- lockKey: buildActiveRunLockKey(threadId),
599
- lockTtlMs: THREAD_ACTIVE_RUN_LOCK_TTL_MS,
600
- retryDelayMs: THREAD_ACTIVE_RUN_LOCK_RETRY_DELAY_MS,
601
- maxWaitMs: THREAD_ACTIVE_RUN_LOCK_MAX_WAIT_MS,
602
- label: 'thread active run',
603
- logger: serverLogger,
604
- },
605
- fn,
606
- )
607
- } catch (error) {
608
- if (error instanceof Error && error.message.startsWith('Timed out waiting for thread active run')) {
609
- throw new ActiveThreadRunConflictError()
610
- }
611
- throw error
612
- }
613
- }
614
-
615
- async getActiveStreamId(threadId: RecordIdRef): Promise<string | null> {
616
- const { streamId } = await this.getActiveTurn(threadId)
617
- return streamId
618
- }
619
-
620
- async clearActiveTurn(threadId: RecordIdRef, params: { runId: string; streamId?: string | null }): Promise<void> {
621
- const threadRef = ensureRecordId(threadId, TABLES.THREAD)
622
- const currentStreamId = params.streamId ?? null
623
- if (currentStreamId === null) {
624
- await databaseService.query(
625
- surql`UPDATE ONLY ${threadRef} SET activeRunId = NONE, activeStreamId = NONE WHERE activeRunId = ${params.runId}`,
626
- )
627
- return
628
- }
629
-
630
- await databaseService.query(surql`
631
- UPDATE ONLY ${threadRef}
632
- SET activeRunId = NONE,
633
- activeStreamId = NONE
634
- WHERE activeRunId = ${params.runId} AND activeStreamId = ${currentStreamId}
635
- `)
636
- }
637
-
638
- async clearStaleActiveRunIfMissingFromRegistry(threadId: RecordIdRef): Promise<boolean> {
639
- const { runId: activeRunId, streamId: activeStreamId } = await this.getActiveTurn(threadId)
640
- if (!activeRunId || (await this.hasActiveRunLease(threadId))) {
641
- return false
642
- }
643
-
644
- await this.clearActiveTurn(threadId, { runId: activeRunId, streamId: activeStreamId })
645
-
646
- serverLogger.warn`Cleared stale thread run after lease expired: thread=${recordIdToString(ensureRecordId(threadId, TABLES.THREAD), TABLES.THREAD)} run=${activeRunId}`
647
- return true
648
- }
649
-
650
- async stopActiveRun(threadId: RecordIdRef): Promise<boolean> {
651
- const { runId: activeRunId } = await this.getActiveTurn(threadId)
652
- if (!activeRunId) return false
653
-
654
- const stopped = chatRunRegistry.stop(activeRunId, new DOMException('Run stopped by user.', 'AbortError'))
655
- if (stopped) {
656
- return true
657
- }
658
-
659
- await this.clearStaleActiveRunIfMissingFromRegistry(threadId)
660
- return false
661
- }
662
-
663
- async setCompacting(threadId: RecordIdRef, value: boolean): Promise<void> {
664
- const threadRef = ensureRecordId(threadId, TABLES.THREAD)
665
- await databaseService.query<unknown>(surql`
666
- UPDATE ONLY ${threadRef}
667
- SET isCompacting = ${value}
668
- `)
669
- }
670
-
671
- async appendMemoryBlock(threadId: RecordIdRef, entry: string): Promise<string> {
672
- const threadRef = ensureRecordId(threadId, TABLES.THREAD)
673
- const thread = await this.getById(threadRef)
674
- const entries = parseMemoryBlock(thread.memoryBlock)
675
-
676
- const labelMatch = entry.match(/^(\w+):\s*/i)
677
- const role = labelMatch ? labelMatch[1].toLowerCase() : 'system'
678
- const content = labelMatch ? entry.slice(labelMatch[0].length).trim() : entry.trim()
679
-
680
- const updatedEntries = appendToMemoryBlock(entries, role, content)
681
- const serialized = serializeMemoryBlock(updatedEntries)
682
-
683
- await this.update(threadRef, { memoryBlock: serialized })
684
-
685
- if (updatedEntries.length >= MEMORY_BLOCK_COMPACTION_TRIGGER_ENTRIES) {
686
- void this.compactMemoryBlock(threadRef).catch((err: unknown) => {
687
- serverLogger.warn`Memory block compaction failed for ${threadRef}: ${err}`
688
- })
689
- }
690
-
691
- return this.formatMemoryBlockForPrompt({ memoryBlock: serialized, memoryBlockSummary: thread.memoryBlockSummary })
692
- }
693
-
694
- async compactMemoryBlock(threadId: RecordIdRef): Promise<boolean> {
695
- const threadRef = ensureRecordId(threadId, TABLES.THREAD)
696
- const thread = await this.getById(threadRef)
697
- const result = await compactMemoryBlockEntries({
698
- previousSummary: thread.memoryBlockSummary,
699
- entries: parseMemoryBlock(thread.memoryBlock),
700
- triggerEntries: MEMORY_BLOCK_COMPACTION_TRIGGER_ENTRIES,
701
- chunkEntries: MEMORY_BLOCK_COMPACTION_CHUNK_ENTRIES,
702
- compact: (params) => contextCompactionService.compactMemoryBlock(params),
703
- })
704
-
705
- if (!result.compacted) return false
706
-
707
- await this.update(threadRef, {
708
- memoryBlockSummary: result.summary || '',
709
- memoryBlock: serializeMemoryBlock(result.entries),
710
- })
711
-
712
- return true
713
- }
714
-
715
- async clearThread(threadId: RecordIdRef): Promise<void> {
716
- const existing = await this.getById(threadId)
717
- assertMutableThread(existing)
718
- const threadRef = ensureRecordId(threadId, TABLES.THREAD)
719
- await databaseService.deleteWhere(TABLES.THREAD_MESSAGE, { threadId: threadRef })
720
- await databaseService.query<unknown>(surql`
721
- UPDATE ONLY ${threadRef}
722
- SET turnCount = 0,
723
- compactionSummary = NONE,
724
- lastCompactedMessageId = NONE,
725
- activeRunId = NONE,
726
- activeStreamId = NONE,
727
- isCompacting = false
728
- `)
729
- }
730
-
731
- async deleteThread(threadId: RecordIdRef): Promise<void> {
732
- const existing = await this.getById(threadId)
733
- assertMutableThread(existing)
734
- await this.delete(threadId)
735
- }
736
-
737
- async listRecentThreads({
738
- userId,
739
- orgId,
740
- excludeThreadId,
741
- limit,
742
- }: {
743
- userId: RecordIdRef
744
- orgId: RecordIdRef
745
- excludeThreadId?: RecordIdRef
746
- limit: number
747
- }): Promise<NormalizedThread[]> {
748
- let excludeCondition = ''
749
- const vars: Record<string, unknown> = { userId, orgId, limit }
750
-
751
- if (excludeThreadId) {
752
- excludeCondition = 'AND id != $excludeThreadId'
753
- vars.excludeThreadId = excludeThreadId
754
- }
755
-
756
- const threads = await databaseService.queryMany<typeof ThreadSchema>(
757
- new BoundQuery(
758
- `SELECT * FROM ${TABLES.THREAD}
759
- WHERE userId = $userId
760
- AND organizationId = $orgId
761
- ${excludeCondition}
762
- AND status != "archived"
763
- ORDER BY updatedAt DESC
764
- LIMIT $limit`,
765
- vars,
766
- ),
767
- ThreadSchema,
768
- )
769
-
770
- return await this.toNormalizedThreads(threads, { checkLease: false })
771
- }
772
-
773
- private normalizeRecordIdString(id: unknown, table: DatabaseTable): string {
774
- if (!isRecordIdInput(id)) {
775
- throw new Error(`Invalid record id for table ${table}`)
776
- }
777
-
778
- return recordIdToString(id, table)
779
- }
780
-
781
- formatMemoryBlockForPrompt(thread: Pick<ThreadRecord, 'memoryBlock' | 'memoryBlockSummary'>): string {
782
- return formatPersistedMemoryBlockForPrompt({
783
- summary: thread.memoryBlockSummary,
784
- entries: parseMemoryBlock(thread.memoryBlock),
785
- })
786
- }
787
-
788
- private getDefaultTitle(thread: Pick<ThreadRecord, 'type' | 'threadType'>): string {
789
- if (thread.type === 'thread' && typeof thread.threadType === 'string') {
790
- return getCoreThreadProfile(thread.threadType).config.title
791
- }
792
-
793
- return THREAD.DEFAULT_TITLE
794
- }
795
-
796
- private async computeIsRunning(
797
- thread: Pick<ThreadRecord, 'id' | 'activeRunId'>,
798
- options: { checkLease: boolean },
799
- ): Promise<boolean> {
800
- const activeRunId =
801
- typeof thread.activeRunId === 'string' && thread.activeRunId.trim().length > 0 ? thread.activeRunId : null
802
-
803
- if (activeRunId === null) {
804
- return false
805
- }
806
-
807
- if (chatRunRegistry.has(activeRunId)) {
808
- return true
809
- }
810
-
811
- if (!options.checkLease) {
812
- return true
813
- }
814
-
815
- return await this.hasActiveRunLease(ensureRecordId(thread.id, TABLES.THREAD))
816
- }
817
-
818
- private async toNormalizedThread(
819
- thread: ThreadRecord,
820
- options: { checkLease?: boolean } = {},
821
- ): Promise<NormalizedThread> {
822
- const isRunning = await this.computeIsRunning(thread, { checkLease: options.checkLease ?? true })
823
- const isCompacting = thread.isCompacting === true
824
- const type = thread.type
825
- const threadType = type === 'thread' && typeof thread.threadType === 'string' ? thread.threadType : undefined
826
- const status = thread.status
827
- return NormalizedThreadSchema.parse({
828
- id: this.normalizeRecordIdString(thread.id, TABLES.THREAD),
829
- userId: this.normalizeRecordIdString(thread.userId, TABLES.USER),
830
- organizationId: this.normalizeRecordIdString(thread.organizationId, TABLES.ORGANIZATION),
831
- type,
832
- ...(threadType ? { threadType } : {}),
833
- nameGenerated: thread.nameGenerated,
834
- isRunning,
835
- isCompacting,
836
- ...(isAgentName(thread.agentId) ? { agentId: thread.agentId } : {}),
837
- title: thread.title ?? this.getDefaultTitle(thread),
838
- status,
839
- memoryBlock: this.formatMemoryBlockForPrompt(thread),
840
- members: thread.members,
841
- createdAt: toIsoDateTimeString(thread.createdAt),
842
- updatedAt: toIsoDateTimeString(thread.updatedAt),
843
- })
844
- }
845
-
846
- private async toNormalizedThreads(
847
- threads: ThreadRecord[],
848
- options: { checkLease?: boolean } = {},
849
- ): Promise<NormalizedThread[]> {
850
- return await Promise.all(threads.map(async (thread) => await this.toNormalizedThread(thread, options)))
851
- }
852
-
853
- toPublicThread(thread: NormalizedThread): PublicThread {
854
- const { organizationId: _organizationId, userId: _userId, memoryBlock: _memoryBlock, ...publicThread } = thread
855
- return PublicThreadSchema.parse(publicThread)
856
- }
857
-
858
- async incrementTurnCount(threadId: RecordIdRef): Promise<number> {
859
- const threadRef = ensureRecordId(threadId, TABLES.THREAD)
860
- const result = await databaseService.query<{ turnCount: number }>(surql`
861
- UPDATE ONLY ${threadRef}
862
- SET turnCount += 1
863
- RETURN turnCount
864
- `)
865
- return result[0].turnCount
866
- }
867
- }
868
-
869
- export const threadService = new ThreadService()