@lota-sdk/core 0.2.2 → 0.3.0
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.
- package/infrastructure/schema/00_identity.surql +2 -2
- package/infrastructure/schema/00_thread.surql +75 -0
- package/infrastructure/schema/02_execution_plan.surql +10 -11
- package/infrastructure/schema/10_autonomous_job.surql +3 -3
- package/package.json +2 -2
- package/src/ai/definitions.ts +1 -1
- package/src/config/agent-defaults.ts +5 -5
- package/src/config/index.ts +1 -1
- package/src/config/thread-defaults.ts +72 -0
- package/src/create-runtime.ts +89 -93
- package/src/db/tables.ts +3 -3
- package/src/db/{workstream-message-row.ts → thread-message-row.ts} +3 -3
- package/src/queues/context-compaction.queue.ts +6 -6
- package/src/queues/plan-agent-heartbeat.queue.ts +3 -3
- package/src/queues/post-chat-memory.queue.ts +1 -1
- package/src/queues/title-generation.queue.ts +10 -13
- package/src/redis/index.ts +1 -1
- package/src/redis/stream-context.ts +1 -1
- package/src/runtime/agent-identity-overrides.ts +1 -1
- package/src/runtime/agent-runtime-policy.ts +19 -21
- package/src/runtime/chat-request-routing.ts +1 -1
- package/src/runtime/context-compaction-constants.ts +1 -1
- package/src/runtime/context-compaction.ts +1 -1
- package/src/runtime/execution-plan.ts +1 -1
- package/src/runtime/index.ts +1 -1
- package/src/runtime/memory-digest-policy.ts +1 -1
- package/src/runtime/plugin-types.ts +1 -1
- package/src/runtime/post-turn-side-effects.ts +35 -35
- package/src/runtime/runtime-config.ts +12 -12
- package/src/runtime/runtime-extensions.ts +11 -11
- package/src/runtime/social-chat-agent-runner.ts +3 -3
- package/src/runtime/social-chat-history.ts +1 -1
- package/src/runtime/social-chat.ts +6 -6
- package/src/runtime/team-consultation-orchestrator.ts +1 -1
- package/src/runtime/{workstream-chat-helpers.ts → thread-chat-helpers.ts} +7 -7
- package/src/runtime/{workstream-plan-turn.ts → thread-plan-turn.ts} +11 -17
- package/src/runtime/{workstream-turn-context.ts → thread-turn-context.ts} +10 -10
- package/src/services/agent-activity.service.ts +39 -44
- package/src/services/agent-executor.service.ts +17 -19
- package/src/services/attachment.service.ts +4 -8
- package/src/services/autonomous-job.service.ts +29 -28
- package/src/services/context-compaction.service.ts +19 -29
- package/src/services/execution-plan.service.ts +58 -70
- package/src/services/global-orchestrator.service.ts +5 -5
- package/src/services/index.ts +6 -6
- package/src/services/memory.service.ts +1 -1
- package/src/services/monitoring-window.service.ts +2 -2
- package/src/services/mutating-approval.service.ts +7 -10
- package/src/services/node-workspace.service.ts +8 -7
- package/src/services/notification.service.ts +1 -1
- package/src/services/organization.service.ts +9 -9
- package/src/services/ownership-dispatcher.service.ts +13 -19
- package/src/services/plan-agent-heartbeat.service.ts +13 -13
- package/src/services/plan-agent-query.service.ts +7 -7
- package/src/services/plan-artifact.service.ts +1 -2
- package/src/services/plan-coordination.service.ts +4 -4
- package/src/services/plan-cycle.service.ts +7 -7
- package/src/services/plan-deadline.service.ts +4 -4
- package/src/services/plan-event-delivery.service.ts +8 -12
- package/src/services/plan-executor.service.ts +16 -37
- package/src/services/plan-run-data.ts +27 -8
- package/src/services/plan-run.service.ts +7 -9
- package/src/services/plan-scheduler.service.ts +4 -4
- package/src/services/plan-template.service.ts +2 -2
- package/src/services/plan-validator.service.ts +0 -11
- package/src/services/plugin-executor.service.ts +1 -1
- package/src/services/queue-job.service.ts +1 -1
- package/src/services/recent-activity-title.service.ts +1 -1
- package/src/services/recent-activity.service.ts +4 -4
- package/src/services/system-executor.service.ts +2 -2
- package/src/services/{workstream-message.service.ts → thread-message.service.ts} +72 -76
- package/src/services/thread-plan-registry.service.ts +22 -0
- package/src/services/thread-title.service.ts +39 -0
- package/src/services/{workstream-turn-preparation.service.ts → thread-turn-preparation.service.ts} +131 -143
- package/src/services/{workstream-turn.ts → thread-turn.ts} +27 -31
- package/src/services/thread.service.ts +707 -0
- package/src/services/thread.types.ts +17 -0
- package/src/storage/attachment-storage.service.ts +4 -4
- package/src/system-agents/index.ts +1 -1
- package/src/system-agents/memory.agent.ts +1 -1
- package/src/system-agents/recent-activity-title-refiner.agent.ts +2 -2
- package/src/system-agents/regular-chat-memory-digest.agent.ts +1 -1
- package/src/system-agents/researcher.agent.ts +3 -3
- package/src/system-agents/{workstream-router.agent.ts → thread-router.agent.ts} +21 -21
- package/src/system-agents/title-generator.agent.ts +8 -8
- package/src/tools/execution-plan.tool.ts +39 -40
- package/src/tools/memory-block.tool.ts +4 -4
- package/src/tools/research-topic.tool.ts +1 -0
- package/src/tools/search-web.tool.ts +1 -1
- package/src/tools/search.tool.ts +4 -4
- package/src/tools/team-think.tool.ts +9 -9
- package/src/workers/regular-chat-memory-digest.helpers.ts +1 -1
- package/src/workers/regular-chat-memory-digest.runner.ts +43 -43
- package/src/workers/skill-extraction.runner.ts +9 -13
- package/src/workers/utils/{workstream-message-query.ts → thread-message-query.ts} +21 -21
- package/infrastructure/schema/00_workstream.surql +0 -64
- package/src/config/workstream-defaults.ts +0 -72
- package/src/services/workstream-plan-registry.service.ts +0 -22
- package/src/services/workstream-title.service.ts +0 -42
- package/src/services/workstream.service.ts +0 -803
- package/src/services/workstream.types.ts +0 -17
- /package/src/services/{workstream-constants.ts → thread-constants.ts} +0 -0
|
@@ -1,803 +0,0 @@
|
|
|
1
|
-
import { WORKSTREAM, sdkWorkstreamStatusSchema } from '@lota-sdk/shared'
|
|
2
|
-
import { BoundQuery, RecordId, StringRecordId, surql } from 'surrealdb'
|
|
3
|
-
|
|
4
|
-
import { agentDisplayNames, agentRoster, getCoreWorkstreamProfile, isAgentName } from '../config/agent-defaults'
|
|
5
|
-
import { serverLogger } from '../config/logger'
|
|
6
|
-
import { getWorkstreamBootstrapConfig } from '../config/workstream-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 './workstream-constants'
|
|
25
|
-
import { workstreamMessageService } from './workstream-message.service'
|
|
26
|
-
import { NormalizedWorkstreamSchema, PublicWorkstreamSchema, WorkstreamSchema } from './workstream.types'
|
|
27
|
-
import type { NormalizedWorkstream, PublicWorkstream, WorkstreamRecord } from './workstream.types'
|
|
28
|
-
|
|
29
|
-
// Uses SurrealQL directly to keep pagination/order logic close to queries.
|
|
30
|
-
|
|
31
|
-
const WORKSTREAM_ACTIVE_RUN_LOCK_TTL_MS = 90_000
|
|
32
|
-
const WORKSTREAM_ACTIVE_RUN_LOCK_MAX_WAIT_MS = 750
|
|
33
|
-
const WORKSTREAM_ACTIVE_RUN_LOCK_RETRY_DELAY_MS = 75
|
|
34
|
-
|
|
35
|
-
function toSafeDirectIdSegment(value: string): string {
|
|
36
|
-
return value.replace(/[^a-zA-Z0-9_-]/g, '_')
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function toRecordIdValueString(value: RecordIdRef, fallbackTable: string): string {
|
|
40
|
-
const canonical = recordIdToString(ensureRecordId(value, fallbackTable), fallbackTable)
|
|
41
|
-
const prefix = `${fallbackTable}:`
|
|
42
|
-
const withoutTable = canonical.startsWith(prefix) ? canonical.slice(prefix.length) : canonical
|
|
43
|
-
const wrappedMatch = withoutTable.match(/^⟨(.+)⟩$/)
|
|
44
|
-
return wrappedMatch ? wrappedMatch[1] : withoutTable
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function isRecordIdInput(value: unknown): value is RecordIdInput {
|
|
48
|
-
if (typeof value === 'string' || value instanceof RecordId || value instanceof StringRecordId) {
|
|
49
|
-
return true
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if (!value || typeof value !== 'object') {
|
|
53
|
-
return false
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const record = value as { tb?: unknown; id?: unknown }
|
|
57
|
-
return typeof record.tb === 'string' && record.id !== undefined
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function getAgentDisplayName(agentId: string): string {
|
|
61
|
-
return agentDisplayNames[agentId] ?? agentId
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function requireDirectAgentId(agentId: string | undefined): string {
|
|
65
|
-
if (!agentId) {
|
|
66
|
-
throw new Error('Direct workstreams require an agentId')
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return agentId
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function requireString(coreType: string | undefined): string {
|
|
73
|
-
if (!coreType) {
|
|
74
|
-
throw new Error('Core workstreams require a coreType')
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return coreType
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function buildActiveRunLockKey(workstreamId: RecordIdRef): string {
|
|
81
|
-
return `workstream-active-run:${recordIdToString(ensureRecordId(workstreamId, TABLES.WORKSTREAM), TABLES.WORKSTREAM)}`
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function buildListWorkstreamsQuery(options: { includeArchived: boolean; paginate: boolean }): string {
|
|
85
|
-
const clauses = [
|
|
86
|
-
`SELECT * FROM ${TABLES.WORKSTREAM}`,
|
|
87
|
-
'WHERE userId = $userId',
|
|
88
|
-
' AND organizationId = $orgId',
|
|
89
|
-
' AND mode = $mode',
|
|
90
|
-
' AND core = $core',
|
|
91
|
-
]
|
|
92
|
-
if (!options.includeArchived) {
|
|
93
|
-
clauses.push(' AND status = "regular"')
|
|
94
|
-
}
|
|
95
|
-
clauses.push('ORDER BY updatedAt DESC')
|
|
96
|
-
if (options.paginate) {
|
|
97
|
-
clauses.push('LIMIT $limit START $offset')
|
|
98
|
-
}
|
|
99
|
-
return clauses.join('\n')
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function normalizeActiveTurnValue(value: unknown): string | null {
|
|
103
|
-
if (typeof value !== 'string') {
|
|
104
|
-
return null
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const normalized = value.trim()
|
|
108
|
-
return normalized.length > 0 ? normalized : null
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
export class ActiveWorkstreamRunConflictError extends Error {
|
|
112
|
-
constructor() {
|
|
113
|
-
super('A chat run is already active.')
|
|
114
|
-
this.name = 'ActiveWorkstreamRunConflictError'
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function buildDirectWorkstreamId({
|
|
119
|
-
userId,
|
|
120
|
-
orgId,
|
|
121
|
-
agentId,
|
|
122
|
-
}: {
|
|
123
|
-
userId: RecordIdRef
|
|
124
|
-
orgId: RecordIdRef
|
|
125
|
-
agentId: string
|
|
126
|
-
}): RecordId {
|
|
127
|
-
const userValue = toSafeDirectIdSegment(toRecordIdValueString(userId, TABLES.USER))
|
|
128
|
-
const orgValue = toSafeDirectIdSegment(toRecordIdValueString(orgId, TABLES.ORGANIZATION))
|
|
129
|
-
return new RecordId(TABLES.WORKSTREAM, `direct_${agentId}_user_${userValue}_organization_${orgValue}`)
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function buildCoreWorkstreamId({
|
|
133
|
-
userId,
|
|
134
|
-
orgId,
|
|
135
|
-
coreType,
|
|
136
|
-
}: {
|
|
137
|
-
userId: RecordIdRef
|
|
138
|
-
orgId: RecordIdRef
|
|
139
|
-
coreType: string
|
|
140
|
-
}): RecordId {
|
|
141
|
-
const userValue = toSafeDirectIdSegment(toRecordIdValueString(userId, TABLES.USER))
|
|
142
|
-
const orgValue = toSafeDirectIdSegment(toRecordIdValueString(orgId, TABLES.ORGANIZATION))
|
|
143
|
-
const typeValue = toSafeDirectIdSegment(coreType)
|
|
144
|
-
return new RecordId(TABLES.WORKSTREAM, `core_${typeValue}_user_${userValue}_organization_${orgValue}`)
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
class WorkstreamService extends BaseService<typeof WorkstreamSchema> {
|
|
148
|
-
constructor() {
|
|
149
|
-
super(TABLES.WORKSTREAM, WorkstreamSchema)
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
async createWorkstream(
|
|
153
|
-
userId: RecordIdRef,
|
|
154
|
-
orgId: RecordIdRef,
|
|
155
|
-
options?: {
|
|
156
|
-
title?: string
|
|
157
|
-
mode?: string
|
|
158
|
-
agentId?: string
|
|
159
|
-
core?: boolean
|
|
160
|
-
coreType?: string
|
|
161
|
-
members?: string[]
|
|
162
|
-
},
|
|
163
|
-
): Promise<NormalizedWorkstream> {
|
|
164
|
-
const mode = options?.mode ?? 'group'
|
|
165
|
-
const directAgentId = options?.agentId
|
|
166
|
-
const core = options?.core === true
|
|
167
|
-
const coreType = options?.coreType
|
|
168
|
-
|
|
169
|
-
if (mode === 'direct' && !directAgentId) {
|
|
170
|
-
throw new Error('Direct workstreams require an agentId')
|
|
171
|
-
}
|
|
172
|
-
if (mode === 'group' && directAgentId) {
|
|
173
|
-
throw new Error('Group workstreams cannot set agentId')
|
|
174
|
-
}
|
|
175
|
-
if (mode === 'direct' && core) {
|
|
176
|
-
throw new Error('Direct workstreams cannot be core workstreams')
|
|
177
|
-
}
|
|
178
|
-
if (core && mode !== 'group') {
|
|
179
|
-
throw new Error('Core workstreams must use group mode')
|
|
180
|
-
}
|
|
181
|
-
if (core && !coreType) {
|
|
182
|
-
throw new Error('Core workstreams require a coreType')
|
|
183
|
-
}
|
|
184
|
-
if (!core && coreType) {
|
|
185
|
-
throw new Error('Only core workstreams can set a coreType')
|
|
186
|
-
}
|
|
187
|
-
const title = (() => {
|
|
188
|
-
if (options?.title) {
|
|
189
|
-
return options.title
|
|
190
|
-
}
|
|
191
|
-
if (core) {
|
|
192
|
-
return getCoreWorkstreamProfile(requireString(coreType)).config.title
|
|
193
|
-
}
|
|
194
|
-
if (mode === 'direct') {
|
|
195
|
-
return getAgentDisplayName(requireDirectAgentId(directAgentId))
|
|
196
|
-
}
|
|
197
|
-
return WORKSTREAM.DEFAULT_TITLE
|
|
198
|
-
})()
|
|
199
|
-
|
|
200
|
-
if (mode === 'direct') {
|
|
201
|
-
const agentId = requireDirectAgentId(directAgentId)
|
|
202
|
-
const directWorkstreamId = buildDirectWorkstreamId({ userId, orgId, agentId })
|
|
203
|
-
const workstream = await this.upsertDeterministicWorkstream(directWorkstreamId, {
|
|
204
|
-
userId,
|
|
205
|
-
organizationId: orgId,
|
|
206
|
-
mode,
|
|
207
|
-
core: false,
|
|
208
|
-
agentId,
|
|
209
|
-
members: [agentId],
|
|
210
|
-
title,
|
|
211
|
-
status: 'regular',
|
|
212
|
-
nameGenerated: true,
|
|
213
|
-
})
|
|
214
|
-
return await this.toNormalizedWorkstream(workstream)
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
if (core) {
|
|
218
|
-
const resolvedCoreType = requireString(coreType)
|
|
219
|
-
const coreProfile = getCoreWorkstreamProfile(resolvedCoreType)
|
|
220
|
-
const coreWorkstreamId = buildCoreWorkstreamId({ userId, orgId, coreType: resolvedCoreType })
|
|
221
|
-
const workstream = await this.upsertDeterministicWorkstream(coreWorkstreamId, {
|
|
222
|
-
userId,
|
|
223
|
-
organizationId: orgId,
|
|
224
|
-
mode,
|
|
225
|
-
core: true,
|
|
226
|
-
coreType: resolvedCoreType,
|
|
227
|
-
agentId: coreProfile.config.agentId,
|
|
228
|
-
members: [...coreProfile.members],
|
|
229
|
-
title,
|
|
230
|
-
status: 'regular',
|
|
231
|
-
nameGenerated: true,
|
|
232
|
-
})
|
|
233
|
-
return await this.toNormalizedWorkstream(workstream)
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
const groupWorkstream = await this.create({
|
|
237
|
-
userId,
|
|
238
|
-
organizationId: orgId,
|
|
239
|
-
mode,
|
|
240
|
-
core: false,
|
|
241
|
-
members: options?.members ?? [...agentRoster],
|
|
242
|
-
title,
|
|
243
|
-
status: 'regular',
|
|
244
|
-
nameGenerated: options?.title !== undefined && options.title !== WORKSTREAM.DEFAULT_TITLE,
|
|
245
|
-
})
|
|
246
|
-
|
|
247
|
-
return await this.toNormalizedWorkstream(groupWorkstream)
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
async ensureBootstrapWorkstreams(
|
|
251
|
-
userId: RecordIdRef,
|
|
252
|
-
orgId: RecordIdRef,
|
|
253
|
-
options?: { onboardStatus?: string; userName?: string | null },
|
|
254
|
-
): Promise<void> {
|
|
255
|
-
const onboardStatus = options?.onboardStatus ?? 'completed'
|
|
256
|
-
const onboardingCompleted = onboardStatus === 'completed'
|
|
257
|
-
const bootstrapConfig = getWorkstreamBootstrapConfig()
|
|
258
|
-
|
|
259
|
-
const existingWorkstreams = await databaseService.findMany(
|
|
260
|
-
TABLES.WORKSTREAM,
|
|
261
|
-
{ userId, organizationId: orgId },
|
|
262
|
-
WorkstreamSchema,
|
|
263
|
-
)
|
|
264
|
-
|
|
265
|
-
const hasStandardGroupWorkstream = existingWorkstreams.some(
|
|
266
|
-
(workstream) => workstream.mode === 'group' && !workstream.core,
|
|
267
|
-
)
|
|
268
|
-
const directWorkstreamsByAgent = new Map<string, WorkstreamRecord>()
|
|
269
|
-
const coreWorkstreamsByType = new Map<string, WorkstreamRecord>()
|
|
270
|
-
for (const workstream of existingWorkstreams) {
|
|
271
|
-
if (workstream.mode !== 'direct' || !workstream.agentId) continue
|
|
272
|
-
directWorkstreamsByAgent.set(workstream.agentId, workstream)
|
|
273
|
-
}
|
|
274
|
-
for (const workstream of existingWorkstreams) {
|
|
275
|
-
if (workstream.mode !== 'group' || !workstream.core) continue
|
|
276
|
-
if (typeof workstream.coreType !== 'string') continue
|
|
277
|
-
coreWorkstreamsByType.set(workstream.coreType, workstream)
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
const requiredDirectAgents = onboardingCompleted
|
|
281
|
-
? bootstrapConfig.completedDirectAgents
|
|
282
|
-
: bootstrapConfig.onboardingDirectAgents
|
|
283
|
-
const creations: Promise<NormalizedWorkstream>[] = []
|
|
284
|
-
for (const agentId of requiredDirectAgents) {
|
|
285
|
-
if (directWorkstreamsByAgent.has(agentId)) continue
|
|
286
|
-
creations.push(
|
|
287
|
-
this.createWorkstream(userId, orgId, { mode: 'direct', agentId, title: getAgentDisplayName(agentId) }),
|
|
288
|
-
)
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
if (onboardingCompleted && bootstrapConfig.ensureDefaultGroupOnCompleted && !hasStandardGroupWorkstream) {
|
|
292
|
-
creations.push(
|
|
293
|
-
this.createWorkstream(userId, orgId, { mode: 'group', core: false, title: WORKSTREAM.DEFAULT_TITLE }),
|
|
294
|
-
)
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
if (onboardingCompleted) {
|
|
298
|
-
for (const coreType of bootstrapConfig.coreTypesAfterOnboarding) {
|
|
299
|
-
if (coreWorkstreamsByType.has(coreType)) continue
|
|
300
|
-
creations.push(
|
|
301
|
-
this.createWorkstream(userId, orgId, {
|
|
302
|
-
mode: 'group',
|
|
303
|
-
core: true,
|
|
304
|
-
coreType,
|
|
305
|
-
title: getCoreWorkstreamProfile(coreType).config.title,
|
|
306
|
-
}),
|
|
307
|
-
)
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
let createdWorkstreams: NormalizedWorkstream[] = []
|
|
312
|
-
if (creations.length > 0) {
|
|
313
|
-
createdWorkstreams = await Promise.all(creations)
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
const onboardingWelcome = bootstrapConfig.onboardingWelcome
|
|
317
|
-
if (!onboardingCompleted && onboardingWelcome) {
|
|
318
|
-
const createdOnboardingOwnerWorkstream = createdWorkstreams.find(
|
|
319
|
-
(workstream) => workstream.mode === 'direct' && workstream.agentId === onboardingWelcome.directAgentId,
|
|
320
|
-
)
|
|
321
|
-
const existingOnboardingOwnerWorkstream = directWorkstreamsByAgent.get(onboardingWelcome.directAgentId)
|
|
322
|
-
|
|
323
|
-
const onboardingOwnerWorkstreamId =
|
|
324
|
-
createdOnboardingOwnerWorkstream?.id ??
|
|
325
|
-
(existingOnboardingOwnerWorkstream ? this.normalizeWorkstreamId(existingOnboardingOwnerWorkstream.id) : null)
|
|
326
|
-
|
|
327
|
-
if (onboardingOwnerWorkstreamId) {
|
|
328
|
-
const onboardingOwnerWorkstreamRef = ensureRecordId(onboardingOwnerWorkstreamId, TABLES.WORKSTREAM)
|
|
329
|
-
await workstreamMessageService.ensureBootstrapWelcomeMessage({
|
|
330
|
-
workstreamId: onboardingOwnerWorkstreamRef,
|
|
331
|
-
agentId: onboardingWelcome.directAgentId,
|
|
332
|
-
text: onboardingWelcome.buildMessageText({ userName: options?.userName }),
|
|
333
|
-
})
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
async listWorkstreams(
|
|
339
|
-
userId: RecordIdRef,
|
|
340
|
-
orgId: RecordIdRef,
|
|
341
|
-
options: { mode: string; core?: boolean; take?: number; page?: number; includeArchived?: boolean },
|
|
342
|
-
): Promise<{ workstreams: NormalizedWorkstream[]; hasMore: boolean }> {
|
|
343
|
-
const core = options.core === true
|
|
344
|
-
const includeArchived = options.includeArchived ?? false
|
|
345
|
-
if (options.mode === 'direct' && core) {
|
|
346
|
-
throw new Error('Direct workstreams cannot be queried as core workstreams')
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
if (options.mode === 'direct' || core) {
|
|
350
|
-
const workstreams = await databaseService.queryMany<typeof WorkstreamSchema>(
|
|
351
|
-
new BoundQuery(buildListWorkstreamsQuery({ includeArchived, paginate: false }), {
|
|
352
|
-
userId,
|
|
353
|
-
orgId,
|
|
354
|
-
mode: options.mode,
|
|
355
|
-
core,
|
|
356
|
-
}),
|
|
357
|
-
WorkstreamSchema,
|
|
358
|
-
)
|
|
359
|
-
|
|
360
|
-
return { workstreams: await this.toNormalizedWorkstreams(workstreams, { checkLease: false }), hasMore: false }
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
const take = options.take ?? WORKSTREAM.DEFAULT_PAGE_LIMIT
|
|
364
|
-
const page = options.page ?? 1
|
|
365
|
-
const workstreams = await databaseService.queryMany<typeof WorkstreamSchema>(
|
|
366
|
-
new BoundQuery(buildListWorkstreamsQuery({ includeArchived, paginate: true }), {
|
|
367
|
-
userId,
|
|
368
|
-
orgId,
|
|
369
|
-
mode: options.mode,
|
|
370
|
-
core: false,
|
|
371
|
-
limit: take + 1,
|
|
372
|
-
offset: (page - 1) * take,
|
|
373
|
-
}),
|
|
374
|
-
WorkstreamSchema,
|
|
375
|
-
)
|
|
376
|
-
|
|
377
|
-
const hasMore = workstreams.length > take
|
|
378
|
-
const sliced = hasMore ? workstreams.slice(0, take) : workstreams
|
|
379
|
-
|
|
380
|
-
return { workstreams: await this.toNormalizedWorkstreams(sliced, { checkLease: false }), hasMore }
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
async listOrganizationWorkstreams(params: {
|
|
384
|
-
orgId: RecordIdRef
|
|
385
|
-
mode?: 'direct' | 'group'
|
|
386
|
-
agentId?: string
|
|
387
|
-
core?: boolean
|
|
388
|
-
includeArchived?: boolean
|
|
389
|
-
}): Promise<NormalizedWorkstream[]> {
|
|
390
|
-
const whereClauses = ['organizationId = $orgId']
|
|
391
|
-
const variables: Record<string, unknown> = { orgId: params.orgId }
|
|
392
|
-
|
|
393
|
-
if (params.mode) {
|
|
394
|
-
whereClauses.push('mode = $mode')
|
|
395
|
-
variables.mode = params.mode
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
if (typeof params.core === 'boolean') {
|
|
399
|
-
whereClauses.push('core = $core')
|
|
400
|
-
variables.core = params.core
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
if (params.agentId) {
|
|
404
|
-
whereClauses.push('agentId = $agentId')
|
|
405
|
-
variables.agentId = params.agentId
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
if (params.includeArchived !== true) {
|
|
409
|
-
whereClauses.push('status = "regular"')
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
const workstreams = await databaseService.queryMany<typeof WorkstreamSchema>(
|
|
413
|
-
new BoundQuery(
|
|
414
|
-
`SELECT * FROM ${TABLES.WORKSTREAM}
|
|
415
|
-
WHERE ${whereClauses.join('\n AND ')}
|
|
416
|
-
ORDER BY createdAt ASC, id ASC`,
|
|
417
|
-
variables,
|
|
418
|
-
),
|
|
419
|
-
WorkstreamSchema,
|
|
420
|
-
)
|
|
421
|
-
|
|
422
|
-
return await this.toNormalizedWorkstreams(workstreams, { checkLease: false })
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
async getWorkstream(workstreamId: RecordIdRef): Promise<NormalizedWorkstream> {
|
|
426
|
-
const workstream = await this.getById(workstreamId)
|
|
427
|
-
return await this.toNormalizedWorkstream(workstream)
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
async updateTitle(workstreamId: RecordIdRef, title: string): Promise<NormalizedWorkstream> {
|
|
431
|
-
const existing = await this.getById(workstreamId)
|
|
432
|
-
this.assertMutableWorkstream(existing, 'rename')
|
|
433
|
-
const workstream = await this.update(workstreamId, { title, nameGenerated: true })
|
|
434
|
-
return await this.toNormalizedWorkstream(workstream)
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
async updateStatus(workstreamId: RecordIdRef, status: string): Promise<NormalizedWorkstream> {
|
|
438
|
-
const validStatus = sdkWorkstreamStatusSchema.parse(status)
|
|
439
|
-
const existing = await this.getById(workstreamId)
|
|
440
|
-
this.assertMutableWorkstream(existing, validStatus === 'archived' ? 'archive' : 'unarchive')
|
|
441
|
-
const workstream = await this.update(workstreamId, { status: validStatus })
|
|
442
|
-
return await this.toNormalizedWorkstream(workstream)
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
async setActiveTurn(workstreamId: RecordIdRef, runId: string, streamId?: string | null): Promise<void> {
|
|
446
|
-
const workstreamRef = ensureRecordId(workstreamId, TABLES.WORKSTREAM)
|
|
447
|
-
if (streamId === null || streamId === undefined) {
|
|
448
|
-
await databaseService.query<unknown>(surql`
|
|
449
|
-
UPDATE ONLY ${workstreamRef}
|
|
450
|
-
SET activeRunId = ${runId},
|
|
451
|
-
activeStreamId = NONE
|
|
452
|
-
`)
|
|
453
|
-
return
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
await databaseService.query<unknown>(surql`
|
|
457
|
-
UPDATE ONLY ${workstreamRef}
|
|
458
|
-
SET activeRunId = ${runId},
|
|
459
|
-
activeStreamId = ${streamId}
|
|
460
|
-
`)
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
async getActiveTurn(workstreamId: RecordIdRef): Promise<{ runId: string | null; streamId: string | null }> {
|
|
464
|
-
const workstream = await this.getById(workstreamId)
|
|
465
|
-
return {
|
|
466
|
-
runId: normalizeActiveTurnValue(workstream.activeRunId),
|
|
467
|
-
streamId: normalizeActiveTurnValue(workstream.activeStreamId),
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
async getActiveRunId(workstreamId: RecordIdRef): Promise<string | null> {
|
|
472
|
-
const { runId } = await this.getActiveTurn(workstreamId)
|
|
473
|
-
return runId
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
async hasActiveRunLease(workstreamId: RecordIdRef): Promise<boolean> {
|
|
477
|
-
const count = await getRedisConnection().exists(buildActiveRunLockKey(workstreamId))
|
|
478
|
-
return count > 0
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
async withActiveRunLease<T>(workstreamId: RecordIdRef, fn: (signal: AbortSignal) => Promise<T>): Promise<T> {
|
|
482
|
-
try {
|
|
483
|
-
return await withRedisLeaseLock(
|
|
484
|
-
{
|
|
485
|
-
redis: getRedisConnection(),
|
|
486
|
-
lockKey: buildActiveRunLockKey(workstreamId),
|
|
487
|
-
lockTtlMs: WORKSTREAM_ACTIVE_RUN_LOCK_TTL_MS,
|
|
488
|
-
retryDelayMs: WORKSTREAM_ACTIVE_RUN_LOCK_RETRY_DELAY_MS,
|
|
489
|
-
maxWaitMs: WORKSTREAM_ACTIVE_RUN_LOCK_MAX_WAIT_MS,
|
|
490
|
-
label: 'workstream active run',
|
|
491
|
-
logger: serverLogger,
|
|
492
|
-
},
|
|
493
|
-
fn,
|
|
494
|
-
)
|
|
495
|
-
} catch (error) {
|
|
496
|
-
if (error instanceof Error && error.message.startsWith('Timed out waiting for workstream active run')) {
|
|
497
|
-
throw new ActiveWorkstreamRunConflictError()
|
|
498
|
-
}
|
|
499
|
-
throw error
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
async getActiveStreamId(workstreamId: RecordIdRef): Promise<string | null> {
|
|
504
|
-
const { streamId } = await this.getActiveTurn(workstreamId)
|
|
505
|
-
return streamId
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
async clearActiveTurn(workstreamId: RecordIdRef, params: { runId: string; streamId?: string | null }): Promise<void> {
|
|
509
|
-
const workstreamRef = ensureRecordId(workstreamId, TABLES.WORKSTREAM)
|
|
510
|
-
const currentStreamId = params.streamId ?? null
|
|
511
|
-
if (currentStreamId === null) {
|
|
512
|
-
await databaseService.query(
|
|
513
|
-
surql`UPDATE ONLY ${workstreamRef} SET activeRunId = NONE, activeStreamId = NONE WHERE activeRunId = ${params.runId}`,
|
|
514
|
-
)
|
|
515
|
-
return
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
await databaseService.query(surql`
|
|
519
|
-
UPDATE ONLY ${workstreamRef}
|
|
520
|
-
SET activeRunId = NONE,
|
|
521
|
-
activeStreamId = NONE
|
|
522
|
-
WHERE activeRunId = ${params.runId} AND activeStreamId = ${currentStreamId}
|
|
523
|
-
`)
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
async clearStaleActiveRunIfMissingFromRegistry(workstreamId: RecordIdRef): Promise<boolean> {
|
|
527
|
-
const { runId: activeRunId, streamId: activeStreamId } = await this.getActiveTurn(workstreamId)
|
|
528
|
-
if (!activeRunId || (await this.hasActiveRunLease(workstreamId))) {
|
|
529
|
-
return false
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
await this.clearActiveTurn(workstreamId, { runId: activeRunId, streamId: activeStreamId })
|
|
533
|
-
|
|
534
|
-
serverLogger.warn`Cleared stale workstream run after lease expired: workstream=${recordIdToString(ensureRecordId(workstreamId, TABLES.WORKSTREAM), TABLES.WORKSTREAM)} run=${activeRunId}`
|
|
535
|
-
return true
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
async stopActiveRun(workstreamId: RecordIdRef): Promise<boolean> {
|
|
539
|
-
const { runId: activeRunId } = await this.getActiveTurn(workstreamId)
|
|
540
|
-
if (!activeRunId) return false
|
|
541
|
-
|
|
542
|
-
const stopped = chatRunRegistry.stop(activeRunId, new DOMException('Run stopped by user.', 'AbortError'))
|
|
543
|
-
if (stopped) {
|
|
544
|
-
return true
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
await this.clearStaleActiveRunIfMissingFromRegistry(workstreamId)
|
|
548
|
-
return false
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
async setCompacting(workstreamId: RecordIdRef, value: boolean): Promise<void> {
|
|
552
|
-
const workstreamRef = ensureRecordId(workstreamId, TABLES.WORKSTREAM)
|
|
553
|
-
await databaseService.query<unknown>(surql`
|
|
554
|
-
UPDATE ONLY ${workstreamRef}
|
|
555
|
-
SET isCompacting = ${value}
|
|
556
|
-
`)
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
async appendMemoryBlock(workstreamId: RecordIdRef, entry: string): Promise<string> {
|
|
560
|
-
const workstreamRef = ensureRecordId(workstreamId, TABLES.WORKSTREAM)
|
|
561
|
-
const workstream = await this.getById(workstreamRef)
|
|
562
|
-
const entries = parseMemoryBlock(workstream.memoryBlock)
|
|
563
|
-
|
|
564
|
-
const labelMatch = entry.match(/^(\w+):\s*/i)
|
|
565
|
-
const role = labelMatch ? labelMatch[1].toLowerCase() : 'system'
|
|
566
|
-
const content = labelMatch ? entry.slice(labelMatch[0].length).trim() : entry.trim()
|
|
567
|
-
|
|
568
|
-
const updatedEntries = appendToMemoryBlock(entries, role, content)
|
|
569
|
-
const serialized = serializeMemoryBlock(updatedEntries)
|
|
570
|
-
|
|
571
|
-
await this.update(workstreamRef, { memoryBlock: serialized })
|
|
572
|
-
|
|
573
|
-
if (updatedEntries.length >= MEMORY_BLOCK_COMPACTION_TRIGGER_ENTRIES) {
|
|
574
|
-
void this.compactMemoryBlock(workstreamRef).catch((err: unknown) => {
|
|
575
|
-
serverLogger.warn`Memory block compaction failed for ${workstreamRef}: ${err}`
|
|
576
|
-
})
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
return this.formatMemoryBlockForPrompt({
|
|
580
|
-
memoryBlock: serialized,
|
|
581
|
-
memoryBlockSummary: workstream.memoryBlockSummary,
|
|
582
|
-
})
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
async compactMemoryBlock(workstreamId: RecordIdRef): Promise<boolean> {
|
|
586
|
-
const workstreamRef = ensureRecordId(workstreamId, TABLES.WORKSTREAM)
|
|
587
|
-
const workstream = await this.getById(workstreamRef)
|
|
588
|
-
const result = await compactMemoryBlockEntries({
|
|
589
|
-
previousSummary: workstream.memoryBlockSummary,
|
|
590
|
-
entries: parseMemoryBlock(workstream.memoryBlock),
|
|
591
|
-
triggerEntries: MEMORY_BLOCK_COMPACTION_TRIGGER_ENTRIES,
|
|
592
|
-
chunkEntries: MEMORY_BLOCK_COMPACTION_CHUNK_ENTRIES,
|
|
593
|
-
compact: (params) => contextCompactionService.compactMemoryBlock(params),
|
|
594
|
-
})
|
|
595
|
-
|
|
596
|
-
if (!result.compacted) return false
|
|
597
|
-
|
|
598
|
-
await this.update(workstreamRef, {
|
|
599
|
-
memoryBlockSummary: result.summary || '',
|
|
600
|
-
memoryBlock: serializeMemoryBlock(result.entries),
|
|
601
|
-
})
|
|
602
|
-
|
|
603
|
-
return true
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
async deleteWorkstream(workstreamId: RecordIdRef): Promise<void> {
|
|
607
|
-
const existing = await this.getById(workstreamId)
|
|
608
|
-
this.assertMutableWorkstream(existing, 'delete')
|
|
609
|
-
await this.delete(workstreamId)
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
async listRecentWorkstreams({
|
|
613
|
-
userId,
|
|
614
|
-
orgId,
|
|
615
|
-
excludeWorkstreamId,
|
|
616
|
-
limit,
|
|
617
|
-
}: {
|
|
618
|
-
userId: RecordIdRef
|
|
619
|
-
orgId: RecordIdRef
|
|
620
|
-
excludeWorkstreamId?: RecordIdRef
|
|
621
|
-
limit: number
|
|
622
|
-
}): Promise<NormalizedWorkstream[]> {
|
|
623
|
-
let excludeCondition = ''
|
|
624
|
-
const vars: Record<string, unknown> = { userId, orgId, limit }
|
|
625
|
-
|
|
626
|
-
if (excludeWorkstreamId) {
|
|
627
|
-
excludeCondition = 'AND id != $excludeWorkstreamId'
|
|
628
|
-
vars.excludeWorkstreamId = excludeWorkstreamId
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
const workstreams = await databaseService.queryMany<typeof WorkstreamSchema>(
|
|
632
|
-
new BoundQuery(
|
|
633
|
-
`SELECT * FROM ${TABLES.WORKSTREAM}
|
|
634
|
-
WHERE userId = $userId
|
|
635
|
-
AND organizationId = $orgId
|
|
636
|
-
${excludeCondition}
|
|
637
|
-
AND status != "archived"
|
|
638
|
-
ORDER BY updatedAt DESC
|
|
639
|
-
LIMIT $limit`,
|
|
640
|
-
vars,
|
|
641
|
-
),
|
|
642
|
-
WorkstreamSchema,
|
|
643
|
-
)
|
|
644
|
-
|
|
645
|
-
return await this.toNormalizedWorkstreams(workstreams, { checkLease: false })
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
private normalizeWorkstreamId(id: unknown): string {
|
|
649
|
-
return this.normalizeRecordIdString(id, TABLES.WORKSTREAM)
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
private normalizeRecordIdString(id: unknown, table: DatabaseTable): string {
|
|
653
|
-
if (!isRecordIdInput(id)) {
|
|
654
|
-
throw new Error(`Invalid record id for table ${table}`)
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
return recordIdToString(id, table)
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
formatMemoryBlockForPrompt(workstream: Pick<WorkstreamRecord, 'memoryBlock' | 'memoryBlockSummary'>): string {
|
|
661
|
-
return formatPersistedMemoryBlockForPrompt({
|
|
662
|
-
summary: workstream.memoryBlockSummary,
|
|
663
|
-
entries: parseMemoryBlock(workstream.memoryBlock),
|
|
664
|
-
})
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
private getDefaultTitle(workstream: Pick<WorkstreamRecord, 'core' | 'coreType'>): string {
|
|
668
|
-
if (workstream.core && typeof workstream.coreType === 'string') {
|
|
669
|
-
return getCoreWorkstreamProfile(workstream.coreType).config.title
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
return WORKSTREAM.DEFAULT_TITLE
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
private async computeIsRunning(
|
|
676
|
-
workstream: Pick<WorkstreamRecord, 'id' | 'activeRunId'>,
|
|
677
|
-
options: { checkLease: boolean },
|
|
678
|
-
): Promise<boolean> {
|
|
679
|
-
const activeRunId =
|
|
680
|
-
typeof workstream.activeRunId === 'string' && workstream.activeRunId.trim().length > 0
|
|
681
|
-
? workstream.activeRunId
|
|
682
|
-
: null
|
|
683
|
-
|
|
684
|
-
if (activeRunId === null) {
|
|
685
|
-
return false
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
if (chatRunRegistry.has(activeRunId)) {
|
|
689
|
-
return true
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
if (!options.checkLease) {
|
|
693
|
-
return true
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
return await this.hasActiveRunLease(ensureRecordId(workstream.id, TABLES.WORKSTREAM))
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
private async toNormalizedWorkstream(
|
|
700
|
-
workstream: WorkstreamRecord,
|
|
701
|
-
options: { checkLease?: boolean } = {},
|
|
702
|
-
): Promise<NormalizedWorkstream> {
|
|
703
|
-
const isRunning = await this.computeIsRunning(workstream, { checkLease: options.checkLease ?? true })
|
|
704
|
-
const isCompacting = workstream.isCompacting === true
|
|
705
|
-
const mode = workstream.mode
|
|
706
|
-
const core = workstream.core
|
|
707
|
-
const coreType = core && typeof workstream.coreType === 'string' ? workstream.coreType : undefined
|
|
708
|
-
const status = workstream.status
|
|
709
|
-
return NormalizedWorkstreamSchema.parse({
|
|
710
|
-
id: this.normalizeWorkstreamId(workstream.id),
|
|
711
|
-
userId: this.normalizeRecordIdString(workstream.userId, TABLES.USER),
|
|
712
|
-
organizationId: this.normalizeRecordIdString(workstream.organizationId, TABLES.ORGANIZATION),
|
|
713
|
-
mode,
|
|
714
|
-
core,
|
|
715
|
-
...(coreType ? { coreType } : {}),
|
|
716
|
-
nameGenerated: workstream.nameGenerated,
|
|
717
|
-
isRunning,
|
|
718
|
-
isCompacting,
|
|
719
|
-
...(isAgentName(workstream.agentId) ? { agentId: workstream.agentId } : {}),
|
|
720
|
-
title: workstream.title ?? this.getDefaultTitle(workstream),
|
|
721
|
-
status,
|
|
722
|
-
memoryBlock: this.formatMemoryBlockForPrompt(workstream),
|
|
723
|
-
createdAt: toIsoDateTimeString(workstream.createdAt),
|
|
724
|
-
updatedAt: toIsoDateTimeString(workstream.updatedAt),
|
|
725
|
-
})
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
private async toNormalizedWorkstreams(
|
|
729
|
-
workstreams: WorkstreamRecord[],
|
|
730
|
-
options: { checkLease?: boolean } = {},
|
|
731
|
-
): Promise<NormalizedWorkstream[]> {
|
|
732
|
-
return await Promise.all(
|
|
733
|
-
workstreams.map(async (workstream) => await this.toNormalizedWorkstream(workstream, options)),
|
|
734
|
-
)
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
toPublicWorkstream(workstream: NormalizedWorkstream): PublicWorkstream {
|
|
738
|
-
const {
|
|
739
|
-
organizationId: _organizationId,
|
|
740
|
-
userId: _userId,
|
|
741
|
-
memoryBlock: _memoryBlock,
|
|
742
|
-
...publicWorkstream
|
|
743
|
-
} = workstream
|
|
744
|
-
return PublicWorkstreamSchema.parse(publicWorkstream)
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
async incrementTurnCount(workstreamId: RecordIdRef): Promise<number> {
|
|
748
|
-
const workstreamRef = ensureRecordId(workstreamId, TABLES.WORKSTREAM)
|
|
749
|
-
const result = await databaseService.query<{ turnCount: number }>(surql`
|
|
750
|
-
UPDATE ONLY ${workstreamRef}
|
|
751
|
-
SET turnCount += 1
|
|
752
|
-
RETURN turnCount
|
|
753
|
-
`)
|
|
754
|
-
return result[0].turnCount
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
private assertMutableWorkstream(
|
|
758
|
-
workstream: WorkstreamRecord,
|
|
759
|
-
action: 'rename' | 'archive' | 'unarchive' | 'delete',
|
|
760
|
-
): void {
|
|
761
|
-
if (workstream.mode === 'direct') {
|
|
762
|
-
throw new Error(`Direct workstreams cannot be ${action}d`)
|
|
763
|
-
}
|
|
764
|
-
if (workstream.core) {
|
|
765
|
-
throw new Error(`Core workstreams cannot be ${action}d`)
|
|
766
|
-
}
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
private async upsertDeterministicWorkstream(
|
|
770
|
-
workstreamId: RecordIdRef,
|
|
771
|
-
data: Record<string, unknown>,
|
|
772
|
-
): Promise<WorkstreamRecord> {
|
|
773
|
-
const workstreamRef = ensureRecordId(workstreamId, TABLES.WORKSTREAM)
|
|
774
|
-
const existing = await this.findById(workstreamRef)
|
|
775
|
-
if (existing) {
|
|
776
|
-
return existing
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
let createError: unknown = null
|
|
780
|
-
let workstream = await databaseService
|
|
781
|
-
.createWithId(TABLES.WORKSTREAM, workstreamRef, data, WorkstreamSchema)
|
|
782
|
-
.catch((error) => {
|
|
783
|
-
createError = error
|
|
784
|
-
return null
|
|
785
|
-
})
|
|
786
|
-
|
|
787
|
-
if (!workstream) {
|
|
788
|
-
workstream = await this.findById(workstreamRef)
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
if (workstream) {
|
|
792
|
-
return workstream
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
if (createError instanceof Error) {
|
|
796
|
-
throw createError
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
throw new Error('Failed to create or load deterministic workstream')
|
|
800
|
-
}
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
export const workstreamService = new WorkstreamService()
|