@lota-sdk/core 0.4.7 → 0.4.9

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 (259) hide show
  1. package/package.json +11 -12
  2. package/src/ai/embedding-cache.ts +94 -22
  3. package/src/ai-gateway/ai-gateway.ts +738 -223
  4. package/src/config/agent-defaults.ts +176 -75
  5. package/src/config/agent-types.ts +54 -4
  6. package/src/config/constants.ts +8 -2
  7. package/src/config/logger.ts +286 -19
  8. package/src/config/model-constants.ts +1 -0
  9. package/src/config/thread-defaults.ts +33 -21
  10. package/src/create-runtime.ts +725 -383
  11. package/src/db/base.service.ts +52 -28
  12. package/src/db/cursor-pagination.ts +71 -30
  13. package/src/db/memory-store.helpers.ts +4 -7
  14. package/src/db/memory-store.ts +856 -598
  15. package/src/db/memory.ts +398 -275
  16. package/src/db/record-id.ts +32 -10
  17. package/src/db/schema-fingerprint.ts +30 -12
  18. package/src/db/service-normalization.ts +255 -0
  19. package/src/db/service.ts +726 -761
  20. package/src/db/startup.ts +140 -66
  21. package/src/db/transaction-conflict.ts +15 -0
  22. package/src/effect/awaitable-effect.ts +87 -0
  23. package/src/effect/errors.ts +121 -0
  24. package/src/effect/helpers.ts +98 -0
  25. package/src/effect/index.ts +22 -0
  26. package/src/effect/layers.ts +228 -0
  27. package/src/effect/runtime-ref.ts +25 -0
  28. package/src/effect/runtime.ts +31 -0
  29. package/src/effect/services.ts +57 -0
  30. package/src/effect/zod.ts +43 -0
  31. package/src/embeddings/provider.ts +122 -71
  32. package/src/index.ts +46 -1
  33. package/src/openrouter/direct-provider.ts +29 -0
  34. package/src/queues/autonomous-job.queue.ts +130 -74
  35. package/src/queues/context-compaction.queue.ts +60 -15
  36. package/src/queues/delayed-node-promotion.queue.ts +52 -15
  37. package/src/queues/document-processor.queue.ts +52 -77
  38. package/src/queues/memory-consolidation.queue.ts +47 -32
  39. package/src/queues/organization-learning.queue.ts +13 -4
  40. package/src/queues/plan-agent-heartbeat.queue.ts +65 -21
  41. package/src/queues/plan-scheduler.queue.ts +107 -31
  42. package/src/queues/post-chat-memory.queue.ts +66 -24
  43. package/src/queues/queue-factory.ts +142 -52
  44. package/src/queues/standalone-worker.ts +39 -0
  45. package/src/queues/title-generation.queue.ts +54 -9
  46. package/src/redis/connection.ts +84 -32
  47. package/src/redis/index.ts +6 -8
  48. package/src/redis/org-memory-lock.ts +60 -27
  49. package/src/redis/redis-lease-lock.ts +200 -121
  50. package/src/redis/runtime-connection.ts +10 -0
  51. package/src/redis/stream-context.ts +84 -46
  52. package/src/runtime/agent-identity-overrides.ts +2 -2
  53. package/src/runtime/agent-runtime-policy.ts +4 -1
  54. package/src/runtime/agent-stream-helpers.ts +20 -9
  55. package/src/runtime/chat-run-orchestration.ts +102 -19
  56. package/src/runtime/chat-run-registry.ts +36 -2
  57. package/src/runtime/context-compaction/context-compaction-runtime.ts +107 -0
  58. package/src/runtime/{context-compaction.ts → context-compaction/context-compaction.ts} +114 -91
  59. package/src/runtime/execution-plan-visibility.ts +2 -2
  60. package/src/runtime/execution-plan.ts +42 -15
  61. package/src/runtime/graph-designer.ts +11 -7
  62. package/src/runtime/helper-model.ts +135 -48
  63. package/src/runtime/index.ts +7 -7
  64. package/src/runtime/indexed-repositories-policy.ts +3 -3
  65. package/src/runtime/{memory-block.ts → memory/memory-block.ts} +40 -36
  66. package/src/runtime/{memory-digest-policy.ts → memory/memory-digest-policy.ts} +1 -1
  67. package/src/runtime/{memory-pipeline.ts → memory/memory-pipeline.ts} +1 -1
  68. package/src/runtime/{memory-prompts-fact.ts → memory/memory-prompts-fact.ts} +2 -2
  69. package/src/runtime/{memory-scope.ts → memory/memory-scope.ts} +12 -6
  70. package/src/runtime/plugin-resolution.ts +144 -24
  71. package/src/runtime/plugin-types.ts +9 -1
  72. package/src/runtime/post-turn-side-effects.ts +197 -130
  73. package/src/runtime/retrieval-adapters.ts +38 -4
  74. package/src/runtime/runtime-config.ts +165 -61
  75. package/src/runtime/runtime-extensions.ts +21 -34
  76. package/src/runtime/social-chat/social-chat-agent-runner.ts +157 -0
  77. package/src/runtime/{social-chat-history.ts → social-chat/social-chat-history.ts} +42 -20
  78. package/src/runtime/social-chat/social-chat.ts +594 -0
  79. package/src/runtime/specialist-runner.ts +36 -10
  80. package/src/runtime/team-consultation/team-consultation-orchestrator.ts +427 -0
  81. package/src/runtime/{team-consultation-prompts.ts → team-consultation/team-consultation-prompts.ts} +6 -2
  82. package/src/runtime/thread-chat-helpers.ts +2 -2
  83. package/src/runtime/thread-plan-turn.ts +2 -1
  84. package/src/runtime/thread-turn-context.ts +172 -94
  85. package/src/runtime/turn-lifecycle.ts +93 -27
  86. package/src/services/agent-activity.service.ts +287 -203
  87. package/src/services/agent-executor.service.ts +329 -217
  88. package/src/services/artifact.service.ts +225 -148
  89. package/src/services/attachment.service.ts +137 -115
  90. package/src/services/autonomous-job.service.ts +888 -491
  91. package/src/services/chat-run-registry.service.ts +11 -1
  92. package/src/services/context-compaction.service.ts +136 -86
  93. package/src/services/document-chunk.service.ts +162 -90
  94. package/src/services/execution-plan/execution-plan-approval.ts +26 -0
  95. package/src/services/execution-plan/execution-plan-context.ts +29 -0
  96. package/src/services/execution-plan/execution-plan-graph.ts +256 -0
  97. package/src/services/execution-plan/execution-plan-schedule.ts +84 -0
  98. package/src/services/execution-plan/execution-plan-spec.ts +75 -0
  99. package/src/services/execution-plan/execution-plan.service.ts +1041 -0
  100. package/src/services/feedback-loop.service.ts +132 -76
  101. package/src/services/global-orchestrator.service.ts +80 -170
  102. package/src/services/graph-full-routing.ts +182 -0
  103. package/src/services/index.ts +18 -20
  104. package/src/services/institutional-memory.service.ts +220 -123
  105. package/src/services/learned-skill.service.ts +364 -259
  106. package/src/services/memory/memory-conversation.ts +95 -0
  107. package/src/services/memory/memory-org-memory.ts +39 -0
  108. package/src/services/memory/memory-preseeded.ts +80 -0
  109. package/src/services/memory/memory-rerank.ts +297 -0
  110. package/src/services/{memory-utils.ts → memory/memory-utils.ts} +5 -5
  111. package/src/services/memory/memory.service.ts +692 -0
  112. package/src/services/memory/rerank.service.ts +209 -0
  113. package/src/services/monitoring-window.service.ts +92 -70
  114. package/src/services/mutating-approval.service.ts +62 -53
  115. package/src/services/node-workspace.service.ts +141 -98
  116. package/src/services/notification.service.ts +17 -16
  117. package/src/services/organization-member.service.ts +120 -66
  118. package/src/services/organization.service.ts +144 -51
  119. package/src/services/ownership-dispatcher.service.ts +415 -264
  120. package/src/services/plan/plan-agent-heartbeat.service.ts +234 -0
  121. package/src/services/plan/plan-agent-query.service.ts +322 -0
  122. package/src/services/plan/plan-approval.service.ts +102 -0
  123. package/src/services/plan/plan-artifact.service.ts +60 -0
  124. package/src/services/plan/plan-builder.service.ts +76 -0
  125. package/src/services/plan/plan-checkpoint.service.ts +103 -0
  126. package/src/services/{plan-compiler.service.ts → plan/plan-compiler.service.ts} +26 -9
  127. package/src/services/plan/plan-completion-side-effects.ts +175 -0
  128. package/src/services/plan/plan-coordination.service.ts +181 -0
  129. package/src/services/plan/plan-cycle.service.ts +398 -0
  130. package/src/services/plan/plan-deadline.service.ts +547 -0
  131. package/src/services/plan/plan-event-delivery.service.ts +261 -0
  132. package/src/services/plan/plan-executor-context.ts +35 -0
  133. package/src/services/plan/plan-executor-graph.ts +475 -0
  134. package/src/services/plan/plan-executor-helpers.ts +322 -0
  135. package/src/services/plan/plan-executor-persistence.ts +209 -0
  136. package/src/services/plan/plan-executor.service.ts +1654 -0
  137. package/src/services/{plan-helpers.ts → plan/plan-helpers.ts} +1 -1
  138. package/src/services/{plan-run-data.ts → plan/plan-run-data.ts} +4 -4
  139. package/src/services/plan/plan-run-serialization.ts +15 -0
  140. package/src/services/plan/plan-run.service.ts +644 -0
  141. package/src/services/plan/plan-scheduler.service.ts +385 -0
  142. package/src/services/plan/plan-template.service.ts +224 -0
  143. package/src/services/plan/plan-transaction-events.ts +33 -0
  144. package/src/services/plan/plan-validator.service.ts +907 -0
  145. package/src/services/plan/plan-workspace.service.ts +125 -0
  146. package/src/services/plugin-executor.service.ts +97 -68
  147. package/src/services/quality-metrics.service.ts +112 -94
  148. package/src/services/queue-job.service.ts +296 -230
  149. package/src/services/recent-activity-title.service.ts +65 -36
  150. package/src/services/recent-activity.service.ts +274 -259
  151. package/src/services/skill-resolver.service.ts +38 -12
  152. package/src/services/social-chat-history.service.ts +176 -125
  153. package/src/services/system-executor.service.ts +91 -61
  154. package/src/services/thread/thread-active-run.ts +203 -0
  155. package/src/services/thread/thread-bootstrap.ts +369 -0
  156. package/src/services/thread/thread-listing.ts +198 -0
  157. package/src/services/thread/thread-memory-block.ts +117 -0
  158. package/src/services/thread/thread-message.service.ts +363 -0
  159. package/src/services/thread/thread-record-store.ts +155 -0
  160. package/src/services/thread/thread-title.service.ts +74 -0
  161. package/src/services/thread/thread-turn-execution.ts +280 -0
  162. package/src/services/thread/thread-turn-message-context.ts +73 -0
  163. package/src/services/thread/thread-turn-preparation.service.ts +1146 -0
  164. package/src/services/thread/thread-turn-streaming.ts +402 -0
  165. package/src/services/thread/thread-turn-tracing.ts +35 -0
  166. package/src/services/thread/thread-turn.ts +343 -0
  167. package/src/services/thread/thread.service.ts +335 -0
  168. package/src/services/user.service.ts +82 -32
  169. package/src/services/write-intent-validator.service.ts +63 -51
  170. package/src/storage/attachment-parser.ts +69 -27
  171. package/src/storage/attachment-storage.service.ts +331 -275
  172. package/src/storage/generated-document-storage.service.ts +66 -34
  173. package/src/system-agents/agent-result.ts +3 -1
  174. package/src/system-agents/context-compaction.agent.ts +2 -2
  175. package/src/system-agents/delegated-agent-factory.ts +159 -90
  176. package/src/system-agents/memory-reranker.agent.ts +2 -2
  177. package/src/system-agents/memory.agent.ts +2 -2
  178. package/src/system-agents/recent-activity-title-refiner.agent.ts +2 -2
  179. package/src/system-agents/regular-chat-memory-digest.agent.ts +2 -2
  180. package/src/system-agents/skill-extractor.agent.ts +2 -2
  181. package/src/system-agents/skill-manager.agent.ts +2 -2
  182. package/src/system-agents/thread-router.agent.ts +157 -113
  183. package/src/system-agents/title-generator.agent.ts +2 -2
  184. package/src/tools/execution-plan.tool.ts +220 -161
  185. package/src/tools/fetch-webpage.tool.ts +21 -17
  186. package/src/tools/firecrawl-client.ts +16 -6
  187. package/src/tools/index.ts +1 -0
  188. package/src/tools/memory-block.tool.ts +14 -6
  189. package/src/tools/plan-approval.tool.ts +49 -47
  190. package/src/tools/read-file-parts.tool.ts +44 -33
  191. package/src/tools/remember-memory.tool.ts +65 -45
  192. package/src/tools/search-web.tool.ts +26 -22
  193. package/src/tools/search.tool.ts +41 -29
  194. package/src/tools/team-think.tool.ts +124 -83
  195. package/src/tools/user-questions.tool.ts +4 -3
  196. package/src/tools/web-tool-shared.ts +6 -0
  197. package/src/utils/async.ts +17 -23
  198. package/src/utils/crypto.ts +21 -0
  199. package/src/utils/date-time.ts +40 -1
  200. package/src/utils/errors.ts +95 -16
  201. package/src/utils/hono-error-handler.ts +24 -39
  202. package/src/utils/index.ts +2 -1
  203. package/src/utils/null-proto-record.ts +41 -0
  204. package/src/utils/sse-keepalive.ts +124 -21
  205. package/src/workers/bootstrap.ts +186 -51
  206. package/src/workers/memory-consolidation.worker.ts +325 -237
  207. package/src/workers/organization-learning.worker.ts +50 -16
  208. package/src/workers/regular-chat-memory-digest.helpers.ts +28 -27
  209. package/src/workers/regular-chat-memory-digest.runner.ts +175 -114
  210. package/src/workers/skill-extraction.runner.ts +176 -93
  211. package/src/workers/utils/file-section-chunker.ts +8 -10
  212. package/src/workers/utils/repo-structure-extractor.ts +349 -260
  213. package/src/workers/utils/repomix-file-sections.ts +2 -2
  214. package/src/workers/utils/thread-message-query.ts +97 -38
  215. package/src/workers/worker-utils.ts +56 -31
  216. package/src/config/debug-logger.ts +0 -47
  217. package/src/redis/connection-accessor.ts +0 -26
  218. package/src/runtime/context-compaction-runtime.ts +0 -87
  219. package/src/runtime/social-chat-agent-runner.ts +0 -118
  220. package/src/runtime/social-chat.ts +0 -516
  221. package/src/runtime/team-consultation-orchestrator.ts +0 -272
  222. package/src/services/adaptive-playbook.service.ts +0 -152
  223. package/src/services/artifact-provenance.service.ts +0 -172
  224. package/src/services/chat-attachments.service.ts +0 -17
  225. package/src/services/context-compaction-runtime.singleton.ts +0 -13
  226. package/src/services/execution-plan.service.ts +0 -1118
  227. package/src/services/memory.service.ts +0 -844
  228. package/src/services/plan-agent-heartbeat.service.ts +0 -136
  229. package/src/services/plan-agent-query.service.ts +0 -267
  230. package/src/services/plan-approval.service.ts +0 -83
  231. package/src/services/plan-artifact.service.ts +0 -50
  232. package/src/services/plan-builder.service.ts +0 -67
  233. package/src/services/plan-checkpoint.service.ts +0 -81
  234. package/src/services/plan-completion-side-effects.ts +0 -80
  235. package/src/services/plan-coordination.service.ts +0 -157
  236. package/src/services/plan-cycle.service.ts +0 -284
  237. package/src/services/plan-deadline.service.ts +0 -430
  238. package/src/services/plan-event-delivery.service.ts +0 -166
  239. package/src/services/plan-executor.service.ts +0 -1950
  240. package/src/services/plan-run.service.ts +0 -515
  241. package/src/services/plan-scheduler.service.ts +0 -240
  242. package/src/services/plan-template.service.ts +0 -177
  243. package/src/services/plan-validator.service.ts +0 -818
  244. package/src/services/plan-workspace.service.ts +0 -83
  245. package/src/services/thread-message.service.ts +0 -275
  246. package/src/services/thread-plan-registry.service.ts +0 -22
  247. package/src/services/thread-title.service.ts +0 -39
  248. package/src/services/thread-turn-preparation.service.ts +0 -1147
  249. package/src/services/thread-turn.ts +0 -172
  250. package/src/services/thread.service.ts +0 -869
  251. package/src/utils/env.ts +0 -8
  252. /package/src/runtime/{context-compaction-constants.ts → context-compaction/context-compaction-constants.ts} +0 -0
  253. /package/src/runtime/{memory-format.ts → memory/memory-format.ts} +0 -0
  254. /package/src/runtime/{memory-prompts-parse.ts → memory/memory-prompts-parse.ts} +0 -0
  255. /package/src/runtime/{memory-prompts-update.ts → memory/memory-prompts-update.ts} +0 -0
  256. /package/src/runtime/{social-chat-prompts.ts → social-chat/social-chat-prompts.ts} +0 -0
  257. /package/src/services/{plan-node-spec.ts → plan/plan-node-spec.ts} +0 -0
  258. /package/src/services/{thread-constants.ts → thread/thread-constants.ts} +0 -0
  259. /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()