@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.
Files changed (102) hide show
  1. package/infrastructure/schema/00_identity.surql +2 -2
  2. package/infrastructure/schema/00_thread.surql +75 -0
  3. package/infrastructure/schema/02_execution_plan.surql +10 -11
  4. package/infrastructure/schema/10_autonomous_job.surql +3 -3
  5. package/package.json +2 -2
  6. package/src/ai/definitions.ts +1 -1
  7. package/src/config/agent-defaults.ts +5 -5
  8. package/src/config/index.ts +1 -1
  9. package/src/config/thread-defaults.ts +72 -0
  10. package/src/create-runtime.ts +89 -93
  11. package/src/db/tables.ts +3 -3
  12. package/src/db/{workstream-message-row.ts → thread-message-row.ts} +3 -3
  13. package/src/queues/context-compaction.queue.ts +6 -6
  14. package/src/queues/plan-agent-heartbeat.queue.ts +3 -3
  15. package/src/queues/post-chat-memory.queue.ts +1 -1
  16. package/src/queues/title-generation.queue.ts +10 -13
  17. package/src/redis/index.ts +1 -1
  18. package/src/redis/stream-context.ts +1 -1
  19. package/src/runtime/agent-identity-overrides.ts +1 -1
  20. package/src/runtime/agent-runtime-policy.ts +19 -21
  21. package/src/runtime/chat-request-routing.ts +1 -1
  22. package/src/runtime/context-compaction-constants.ts +1 -1
  23. package/src/runtime/context-compaction.ts +1 -1
  24. package/src/runtime/execution-plan.ts +1 -1
  25. package/src/runtime/index.ts +1 -1
  26. package/src/runtime/memory-digest-policy.ts +1 -1
  27. package/src/runtime/plugin-types.ts +1 -1
  28. package/src/runtime/post-turn-side-effects.ts +35 -35
  29. package/src/runtime/runtime-config.ts +12 -12
  30. package/src/runtime/runtime-extensions.ts +11 -11
  31. package/src/runtime/social-chat-agent-runner.ts +3 -3
  32. package/src/runtime/social-chat-history.ts +1 -1
  33. package/src/runtime/social-chat.ts +6 -6
  34. package/src/runtime/team-consultation-orchestrator.ts +1 -1
  35. package/src/runtime/{workstream-chat-helpers.ts → thread-chat-helpers.ts} +7 -7
  36. package/src/runtime/{workstream-plan-turn.ts → thread-plan-turn.ts} +11 -17
  37. package/src/runtime/{workstream-turn-context.ts → thread-turn-context.ts} +10 -10
  38. package/src/services/agent-activity.service.ts +39 -44
  39. package/src/services/agent-executor.service.ts +17 -19
  40. package/src/services/attachment.service.ts +4 -8
  41. package/src/services/autonomous-job.service.ts +29 -28
  42. package/src/services/context-compaction.service.ts +19 -29
  43. package/src/services/execution-plan.service.ts +58 -70
  44. package/src/services/global-orchestrator.service.ts +5 -5
  45. package/src/services/index.ts +6 -6
  46. package/src/services/memory.service.ts +1 -1
  47. package/src/services/monitoring-window.service.ts +2 -2
  48. package/src/services/mutating-approval.service.ts +7 -10
  49. package/src/services/node-workspace.service.ts +8 -7
  50. package/src/services/notification.service.ts +1 -1
  51. package/src/services/organization.service.ts +9 -9
  52. package/src/services/ownership-dispatcher.service.ts +13 -19
  53. package/src/services/plan-agent-heartbeat.service.ts +13 -13
  54. package/src/services/plan-agent-query.service.ts +7 -7
  55. package/src/services/plan-artifact.service.ts +1 -2
  56. package/src/services/plan-coordination.service.ts +4 -4
  57. package/src/services/plan-cycle.service.ts +7 -7
  58. package/src/services/plan-deadline.service.ts +4 -4
  59. package/src/services/plan-event-delivery.service.ts +8 -12
  60. package/src/services/plan-executor.service.ts +16 -37
  61. package/src/services/plan-run-data.ts +27 -8
  62. package/src/services/plan-run.service.ts +7 -9
  63. package/src/services/plan-scheduler.service.ts +4 -4
  64. package/src/services/plan-template.service.ts +2 -2
  65. package/src/services/plan-validator.service.ts +0 -11
  66. package/src/services/plugin-executor.service.ts +1 -1
  67. package/src/services/queue-job.service.ts +1 -1
  68. package/src/services/recent-activity-title.service.ts +1 -1
  69. package/src/services/recent-activity.service.ts +4 -4
  70. package/src/services/system-executor.service.ts +2 -2
  71. package/src/services/{workstream-message.service.ts → thread-message.service.ts} +72 -76
  72. package/src/services/thread-plan-registry.service.ts +22 -0
  73. package/src/services/thread-title.service.ts +39 -0
  74. package/src/services/{workstream-turn-preparation.service.ts → thread-turn-preparation.service.ts} +131 -143
  75. package/src/services/{workstream-turn.ts → thread-turn.ts} +27 -31
  76. package/src/services/thread.service.ts +707 -0
  77. package/src/services/thread.types.ts +17 -0
  78. package/src/storage/attachment-storage.service.ts +4 -4
  79. package/src/system-agents/index.ts +1 -1
  80. package/src/system-agents/memory.agent.ts +1 -1
  81. package/src/system-agents/recent-activity-title-refiner.agent.ts +2 -2
  82. package/src/system-agents/regular-chat-memory-digest.agent.ts +1 -1
  83. package/src/system-agents/researcher.agent.ts +3 -3
  84. package/src/system-agents/{workstream-router.agent.ts → thread-router.agent.ts} +21 -21
  85. package/src/system-agents/title-generator.agent.ts +8 -8
  86. package/src/tools/execution-plan.tool.ts +39 -40
  87. package/src/tools/memory-block.tool.ts +4 -4
  88. package/src/tools/research-topic.tool.ts +1 -0
  89. package/src/tools/search-web.tool.ts +1 -1
  90. package/src/tools/search.tool.ts +4 -4
  91. package/src/tools/team-think.tool.ts +9 -9
  92. package/src/workers/regular-chat-memory-digest.helpers.ts +1 -1
  93. package/src/workers/regular-chat-memory-digest.runner.ts +43 -43
  94. package/src/workers/skill-extraction.runner.ts +9 -13
  95. package/src/workers/utils/{workstream-message-query.ts → thread-message-query.ts} +21 -21
  96. package/infrastructure/schema/00_workstream.surql +0 -64
  97. package/src/config/workstream-defaults.ts +0 -72
  98. package/src/services/workstream-plan-registry.service.ts +0 -22
  99. package/src/services/workstream-title.service.ts +0 -42
  100. package/src/services/workstream.service.ts +0 -803
  101. package/src/services/workstream.types.ts +0 -17
  102. /package/src/services/{workstream-constants.ts → thread-constants.ts} +0 -0
@@ -20,11 +20,11 @@ import { compactWhitespace } from '../utils/string'
20
20
  import { buildDigestTranscript, resolveWorkspaceBootstrapCutoff } from './regular-chat-memory-digest.helpers'
21
21
  import {
22
22
  compareDigestMessageOrder,
23
- listEligibleWorkstreamMessages,
24
- listWorkstreamIdsForOrg,
23
+ listEligibleThreadMessages,
24
+ listThreadIdsForOrg,
25
25
  normalizeBlock,
26
- } from './utils/workstream-message-query'
27
- import type { DigestCursor, DigestMessage } from './utils/workstream-message-query'
26
+ } from './utils/thread-message-query'
27
+ import type { DigestCursor, DigestMessage } from './utils/thread-message-query'
28
28
 
29
29
  // Onboarding extracts memory immediately inside the turn flow. This delayed
30
30
  // runner handles the regular-chat path after onboarding so longer transcripts
@@ -54,7 +54,7 @@ const helperModelRuntime = createHelperModelRuntime()
54
54
 
55
55
  interface RegularChatDigestRunResult {
56
56
  skipped: boolean
57
- processedWorkstreamMessages: number
57
+ processedThreadMessages: number
58
58
  processedSocialMessages: number
59
59
  followUpScheduled: boolean
60
60
  }
@@ -100,35 +100,35 @@ function getLastCursor(messages: DigestMessage[]): DigestCursor | null {
100
100
  return messages.length > 0 ? messages[messages.length - 1].cursor : null
101
101
  }
102
102
 
103
- async function hasNewEligibleWorkstreamMessages(params: {
104
- workstreamIds: RecordIdRef[]
103
+ async function hasNewEligibleThreadMessages(params: {
104
+ threadIds: RecordIdRef[]
105
105
  cursor: DigestCursor | null
106
106
  onboardingCutoff: Date | null
107
107
  }): Promise<boolean> {
108
- if (params.workstreamIds.length === 0) return false
108
+ if (params.threadIds.length === 0) return false
109
109
 
110
110
  let query: BoundQuery | null = null
111
111
  if (params.cursor) {
112
- const cursorRowId = ensureRecordId(params.cursor.id, TABLES.WORKSTREAM_MESSAGE)
112
+ const cursorRowId = ensureRecordId(params.cursor.id, TABLES.THREAD_MESSAGE)
113
113
  query = new BoundQuery(
114
- `SELECT id, createdAt FROM ${TABLES.WORKSTREAM_MESSAGE}
115
- WHERE workstreamId IN $workstreamIds
114
+ `SELECT id, createdAt FROM ${TABLES.THREAD_MESSAGE}
115
+ WHERE threadId IN $threadIds
116
116
  AND (
117
117
  createdAt > $cursorCreatedAt
118
118
  OR (createdAt = $cursorCreatedAt AND id > $cursorRowId)
119
119
  )
120
120
  ORDER BY createdAt ASC, id ASC
121
121
  LIMIT 1`,
122
- { workstreamIds: params.workstreamIds, cursorCreatedAt: params.cursor.createdAt, cursorRowId },
122
+ { threadIds: params.threadIds, cursorCreatedAt: params.cursor.createdAt, cursorRowId },
123
123
  )
124
124
  } else if (params.onboardingCutoff) {
125
125
  query = new BoundQuery(
126
- `SELECT id, createdAt FROM ${TABLES.WORKSTREAM_MESSAGE}
127
- WHERE workstreamId IN $workstreamIds
126
+ `SELECT id, createdAt FROM ${TABLES.THREAD_MESSAGE}
127
+ WHERE threadId IN $threadIds
128
128
  AND createdAt > $onboardingCutoff
129
129
  ORDER BY createdAt ASC, id ASC
130
130
  LIMIT 1`,
131
- { workstreamIds: params.workstreamIds, onboardingCutoff: params.onboardingCutoff },
131
+ { threadIds: params.threadIds, onboardingCutoff: params.onboardingCutoff },
132
132
  )
133
133
  }
134
134
 
@@ -160,7 +160,7 @@ export async function runRegularChatMemoryDigest(
160
160
  const workspaceProvider = getRuntimeAdapters().workspaceProvider
161
161
  if (!workspaceProvider) {
162
162
  serverLogger.info`Skipping regular chat memory digest for ${orgId}: workspaceProvider is not configured`
163
- return { skipped: true, processedWorkstreamMessages: 0, processedSocialMessages: 0, followUpScheduled: false }
163
+ return { skipped: true, processedThreadMessages: 0, processedSocialMessages: 0, followUpScheduled: false }
164
164
  }
165
165
 
166
166
  return withConfiguredWorkspaceMemoryLock(orgId, async () => {
@@ -170,28 +170,28 @@ export async function runRegularChatMemoryDigest(
170
170
  !workspaceProvider.applyProfileProjection
171
171
  ) {
172
172
  serverLogger.info`Skipping regular chat memory digest for ${orgId}: workspaceProvider background/profile methods are incomplete`
173
- return { skipped: true, processedWorkstreamMessages: 0, processedSocialMessages: 0, followUpScheduled: false }
173
+ return { skipped: true, processedThreadMessages: 0, processedSocialMessages: 0, followUpScheduled: false }
174
174
  }
175
175
 
176
176
  const workspace = await workspaceProvider.getWorkspace(orgRef)
177
177
  const lifecycleState = await workspaceProvider.getLifecycleState?.(workspace)
178
178
  if (lifecycleState?.bootstrapActive ?? false) {
179
179
  serverLogger.info`Skipping regular chat memory digest for ${orgId}: onboarding is not completed`
180
- return { skipped: true, processedWorkstreamMessages: 0, processedSocialMessages: 0, followUpScheduled: false }
180
+ return { skipped: true, processedThreadMessages: 0, processedSocialMessages: 0, followUpScheduled: false }
181
181
  }
182
182
  const projectionState = await workspaceProvider.readProfileProjectionState?.(workspace)
183
183
 
184
- const existingWorkstreamCursor = await workspaceProvider.getBackgroundCursor('regular-chat-digest', orgRef)
185
- const workstreamOnboardingCutoff = resolveWorkspaceBootstrapCutoff({
186
- hasExistingCursor: existingWorkstreamCursor !== null,
184
+ const existingThreadCursor = await workspaceProvider.getBackgroundCursor('regular-chat-digest', orgRef)
185
+ const threadOnboardingCutoff = resolveWorkspaceBootstrapCutoff({
186
+ hasExistingCursor: existingThreadCursor !== null,
187
187
  bootstrapCompletedAt: lifecycleState?.bootstrapCompletedAt,
188
188
  })
189
189
 
190
- const workstreamIds = await listWorkstreamIdsForOrg(orgRef)
191
- const workstreamMessages = await listEligibleWorkstreamMessages({
192
- workstreamIds,
193
- cursor: existingWorkstreamCursor,
194
- onboardingCutoff: workstreamOnboardingCutoff,
190
+ const threadIds = await listThreadIdsForOrg(orgRef)
191
+ const threadMessages = await listEligibleThreadMessages({
192
+ threadIds,
193
+ cursor: existingThreadCursor,
194
+ onboardingCutoff: threadOnboardingCutoff,
195
195
  })
196
196
  const existingSocialCursor = await socialChatHistoryService.getBackgroundCursor('regular-chat-digest', orgId)
197
197
  const socialOnboardingCutoff = resolveWorkspaceBootstrapCutoff({
@@ -204,12 +204,12 @@ export async function runRegularChatMemoryDigest(
204
204
  onboardingCutoff: socialOnboardingCutoff,
205
205
  })
206
206
 
207
- if (workstreamMessages.length === 0 && socialMessages.length === 0) {
207
+ if (threadMessages.length === 0 && socialMessages.length === 0) {
208
208
  serverLogger.info`Skipping regular chat memory digest for ${orgId}: no eligible messages`
209
- return { skipped: true, processedWorkstreamMessages: 0, processedSocialMessages: 0, followUpScheduled: false }
209
+ return { skipped: true, processedThreadMessages: 0, processedSocialMessages: 0, followUpScheduled: false }
210
210
  }
211
211
 
212
- const combinedMessages = [...workstreamMessages, ...socialMessages].sort(compareDigestMessageOrder)
212
+ const combinedMessages = [...threadMessages, ...socialMessages].sort(compareDigestMessageOrder)
213
213
  const { transcript, involvedAgentNames } = buildDigestTranscript({ messages: combinedMessages })
214
214
  const existingMemories = await loadExistingOrganizationMemories(orgId)
215
215
 
@@ -237,7 +237,7 @@ export async function runRegularChatMemoryDigest(
237
237
  throw new Error('Regular chat memory digest returned an empty summaryBlock')
238
238
  }
239
239
 
240
- const processedWorkstreamCursor = getLastCursor(workstreamMessages)
240
+ const processedThreadCursor = getLastCursor(threadMessages)
241
241
  const processedSocialCursor = getLastCursor(socialMessages)
242
242
  const digestRunAt = new Date().toISOString()
243
243
 
@@ -248,10 +248,10 @@ export async function runRegularChatMemoryDigest(
248
248
  source: 'regular_chat_digest',
249
249
  sourceMetadata: {
250
250
  digestRunAt,
251
- ...(processedWorkstreamCursor
251
+ ...(processedThreadCursor
252
252
  ? {
253
- digestWorkstreamCursorCreatedAt: processedWorkstreamCursor.createdAt.toISOString(),
254
- digestWorkstreamCursorId: processedWorkstreamCursor.id,
253
+ digestThreadCursorCreatedAt: processedThreadCursor.createdAt.toISOString(),
254
+ digestThreadCursorId: processedThreadCursor.id,
255
255
  }
256
256
  : {}),
257
257
  ...(processedSocialCursor
@@ -270,18 +270,18 @@ export async function runRegularChatMemoryDigest(
270
270
  summaryBlock,
271
271
  structuredPatch: synthesis.structuredProfilePatch,
272
272
  })
273
- if (processedWorkstreamCursor) {
274
- await workspaceProvider.setBackgroundCursor('regular-chat-digest', orgRef, processedWorkstreamCursor)
273
+ if (processedThreadCursor) {
274
+ await workspaceProvider.setBackgroundCursor('regular-chat-digest', orgRef, processedThreadCursor)
275
275
  }
276
276
  if (processedSocialCursor) {
277
277
  await socialChatHistoryService.setBackgroundCursor('regular-chat-digest', orgId, processedSocialCursor)
278
278
  }
279
279
 
280
- const workstreamBoundaryCursor = processedWorkstreamCursor ?? existingWorkstreamCursor
281
- const hasMoreWorkstreamMessages = await hasNewEligibleWorkstreamMessages({
282
- workstreamIds,
283
- cursor: workstreamBoundaryCursor,
284
- onboardingCutoff: workstreamBoundaryCursor ? null : workstreamOnboardingCutoff,
280
+ const threadBoundaryCursor = processedThreadCursor ?? existingThreadCursor
281
+ const hasMoreThreadMessages = await hasNewEligibleThreadMessages({
282
+ threadIds,
283
+ cursor: threadBoundaryCursor,
284
+ onboardingCutoff: threadBoundaryCursor ? null : threadOnboardingCutoff,
285
285
  })
286
286
  const socialBoundaryCursor = processedSocialCursor ?? existingSocialCursor
287
287
  const hasMoreSocialMessages = await socialChatHistoryService.hasWorkspaceMessages({
@@ -290,17 +290,17 @@ export async function runRegularChatMemoryDigest(
290
290
  onboardingCutoff: socialBoundaryCursor ? null : socialOnboardingCutoff,
291
291
  })
292
292
 
293
- const followUpScheduled = hasMoreWorkstreamMessages || hasMoreSocialMessages
293
+ const followUpScheduled = hasMoreThreadMessages || hasMoreSocialMessages
294
294
  if (followUpScheduled) {
295
295
  await clearRegularChatMemoryDigestDeduplicationKey(orgId)
296
296
  await enqueueRegularChatMemoryDigest({ orgId })
297
297
  }
298
298
 
299
- serverLogger.info`Regular chat memory digest completed for ${orgId}: workstreamMessages=${workstreamMessages.length}, socialMessages=${socialMessages.length}, facts=${synthesis.facts.length}, followUpScheduled=${followUpScheduled}`
299
+ serverLogger.info`Regular chat memory digest completed for ${orgId}: threadMessages=${threadMessages.length}, socialMessages=${socialMessages.length}, facts=${synthesis.facts.length}, followUpScheduled=${followUpScheduled}`
300
300
 
301
301
  return {
302
302
  skipped: false,
303
- processedWorkstreamMessages: workstreamMessages.length,
303
+ processedThreadMessages: threadMessages.length,
304
304
  processedSocialMessages: socialMessages.length,
305
305
  followUpScheduled,
306
306
  }
@@ -13,9 +13,9 @@ import { createSkillManagerAgent, SkillManagerOutputSchema } from '../system-age
13
13
  import { buildDigestTranscript, resolveWorkspaceBootstrapCutoff } from './regular-chat-memory-digest.helpers'
14
14
  import {
15
15
  compareDigestMessageOrder,
16
- listEligibleWorkstreamMessages,
17
- listWorkstreamIdsForOrg,
18
- } from './utils/workstream-message-query'
16
+ listEligibleThreadMessages,
17
+ listThreadIdsForOrg,
18
+ } from './utils/thread-message-query'
19
19
 
20
20
  const SKILL_EXTRACTION_TIMEOUT_MS = 10 * 60 * 1000
21
21
  const MIN_MESSAGE_THRESHOLD = 10
@@ -107,18 +107,14 @@ export async function runSkillExtraction(data: SkillExtractionJob): Promise<Skil
107
107
  bootstrapCompletedAt: lifecycleState?.bootstrapCompletedAt,
108
108
  })
109
109
 
110
- const workstreamIds = await listWorkstreamIdsForOrg(orgRef)
111
- const workstreamMessages = await listEligibleWorkstreamMessages({
112
- workstreamIds,
113
- cursor: existingCursor,
114
- onboardingCutoff,
115
- })
110
+ const threadIds = await listThreadIdsForOrg(orgRef)
111
+ const threadMessages = await listEligibleThreadMessages({ threadIds, cursor: existingCursor, onboardingCutoff })
116
112
  const socialMessages = await socialChatHistoryService.listWorkspaceMessages({
117
113
  workspaceId: orgId,
118
114
  cursor: existingSocialCursor,
119
115
  onboardingCutoff: socialOnboardingCutoff,
120
116
  })
121
- const messages = [...workstreamMessages, ...socialMessages]
117
+ const messages = [...threadMessages, ...socialMessages]
122
118
 
123
119
  if (messages.length < MIN_MESSAGE_THRESHOLD) {
124
120
  serverLogger.info`Skipping skill extraction for ${orgId}: only ${messages.length} messages (threshold: ${MIN_MESSAGE_THRESHOLD})`
@@ -241,10 +237,10 @@ export async function runSkillExtraction(data: SkillExtractionJob): Promise<Skil
241
237
  }
242
238
  }
243
239
 
244
- const lastWorkstreamMessage = workstreamMessages.at(-1)
240
+ const lastThreadMessage = threadMessages.at(-1)
245
241
  const lastSocialMessage = socialMessages.at(-1)
246
- if (lastWorkstreamMessage) {
247
- await cursorAwareWorkspaceProvider.setBackgroundCursor('skill-extraction', orgRef, lastWorkstreamMessage.cursor)
242
+ if (lastThreadMessage) {
243
+ await cursorAwareWorkspaceProvider.setBackgroundCursor('skill-extraction', orgRef, lastThreadMessage.cursor)
248
244
  }
249
245
  if (lastSocialMessage) {
250
246
  await socialChatHistoryService.setBackgroundCursor('skill-extraction', orgId, lastSocialMessage.cursor)
@@ -6,15 +6,15 @@ import { ensureRecordId, recordIdToString } from '../../db/record-id'
6
6
  import type { RecordIdRef } from '../../db/record-id'
7
7
  import { databaseService } from '../../db/service'
8
8
  import { TABLES } from '../../db/tables'
9
- import { WorkstreamMessageRowSchema } from '../../db/workstream-message-row'
10
- import type { WorkstreamMessageRow } from '../../db/workstream-message-row'
9
+ import { ThreadMessageRowSchema } from '../../db/thread-message-row'
10
+ import type { ThreadMessageRow } from '../../db/thread-message-row'
11
11
  import { normalizeTextBody } from '../../document/parsing'
12
12
  import type { LotaRuntimeBackgroundCursor } from '../../runtime/runtime-extensions'
13
13
 
14
14
  export type DigestCursor = LotaRuntimeBackgroundCursor
15
15
 
16
16
  export interface DigestMessage {
17
- source: 'workstream' | 'social'
17
+ source: 'thread' | 'social'
18
18
  sourceId: string
19
19
  role: 'system' | 'user' | 'assistant'
20
20
  parts: Array<Record<string, unknown>>
@@ -22,16 +22,16 @@ export interface DigestMessage {
22
22
  cursor: DigestCursor
23
23
  }
24
24
 
25
- function mapWorkstreamRow(row: WorkstreamMessageRow): DigestMessage {
25
+ function mapThreadRow(row: ThreadMessageRow): DigestMessage {
26
26
  return {
27
- source: 'workstream',
28
- sourceId: recordIdToString(row.workstreamId, TABLES.WORKSTREAM),
27
+ source: 'thread',
28
+ sourceId: recordIdToString(row.threadId, TABLES.THREAD),
29
29
  role: row.role,
30
30
  parts: row.parts as Array<Record<string, unknown>>,
31
31
  metadata: row.metadata ?? undefined,
32
32
  cursor: {
33
33
  createdAt: new Date(requireTimestamp(row.createdAt)),
34
- id: recordIdToString(row.id, TABLES.WORKSTREAM_MESSAGE),
34
+ id: recordIdToString(row.id, TABLES.THREAD_MESSAGE),
35
35
  },
36
36
  }
37
37
  }
@@ -42,52 +42,52 @@ export function compareDigestMessageOrder(left: DigestMessage, right: DigestMess
42
42
  return left.cursor.id.localeCompare(right.cursor.id)
43
43
  }
44
44
 
45
- export async function listWorkstreamIdsForOrg(orgRef: RecordIdRef): Promise<RecordIdRef[]> {
45
+ export async function listThreadIdsForOrg(orgRef: RecordIdRef): Promise<RecordIdRef[]> {
46
46
  const EntityIdRowSchema = z.string().trim().min(1)
47
47
  const ids = await databaseService.query<unknown>(
48
48
  new BoundQuery(
49
- `SELECT VALUE type::string(id) FROM ${TABLES.WORKSTREAM}
49
+ `SELECT VALUE type::string(id) FROM ${TABLES.THREAD}
50
50
  WHERE organizationId = $organizationId`,
51
51
  { organizationId: orgRef },
52
52
  ),
53
53
  )
54
- return ids.map((value) => ensureRecordId(EntityIdRowSchema.parse(value), TABLES.WORKSTREAM))
54
+ return ids.map((value) => ensureRecordId(EntityIdRowSchema.parse(value), TABLES.THREAD))
55
55
  }
56
56
 
57
- export async function listEligibleWorkstreamMessages(params: {
58
- workstreamIds: RecordIdRef[]
57
+ export async function listEligibleThreadMessages(params: {
58
+ threadIds: RecordIdRef[]
59
59
  cursor: DigestCursor | null
60
60
  onboardingCutoff: Date | null
61
61
  }): Promise<DigestMessage[]> {
62
- if (params.workstreamIds.length === 0) return []
62
+ if (params.threadIds.length === 0) return []
63
63
 
64
64
  let query: BoundQuery | null = null
65
65
  if (params.cursor) {
66
- const cursorRowId = ensureRecordId(params.cursor.id, TABLES.WORKSTREAM_MESSAGE)
66
+ const cursorRowId = ensureRecordId(params.cursor.id, TABLES.THREAD_MESSAGE)
67
67
  query = new BoundQuery(
68
- `SELECT type::string(id) AS id, type::string(workstreamId) AS workstreamId, role, parts, metadata, createdAt FROM ${TABLES.WORKSTREAM_MESSAGE}
69
- WHERE workstreamId IN $workstreamIds
68
+ `SELECT type::string(id) AS id, type::string(threadId) AS threadId, role, parts, metadata, createdAt FROM ${TABLES.THREAD_MESSAGE}
69
+ WHERE threadId IN $threadIds
70
70
  AND (
71
71
  createdAt > $cursorCreatedAt
72
72
  OR (createdAt = $cursorCreatedAt AND id > $cursorRowId)
73
73
  )
74
74
  ORDER BY createdAt ASC, id ASC`,
75
- { workstreamIds: params.workstreamIds, cursorCreatedAt: params.cursor.createdAt, cursorRowId },
75
+ { threadIds: params.threadIds, cursorCreatedAt: params.cursor.createdAt, cursorRowId },
76
76
  )
77
77
  } else if (params.onboardingCutoff) {
78
78
  query = new BoundQuery(
79
- `SELECT type::string(id) AS id, type::string(workstreamId) AS workstreamId, role, parts, metadata, createdAt FROM ${TABLES.WORKSTREAM_MESSAGE}
80
- WHERE workstreamId IN $workstreamIds
79
+ `SELECT type::string(id) AS id, type::string(threadId) AS threadId, role, parts, metadata, createdAt FROM ${TABLES.THREAD_MESSAGE}
80
+ WHERE threadId IN $threadIds
81
81
  AND createdAt > $onboardingCutoff
82
82
  ORDER BY createdAt ASC, id ASC`,
83
- { workstreamIds: params.workstreamIds, onboardingCutoff: params.onboardingCutoff },
83
+ { threadIds: params.threadIds, onboardingCutoff: params.onboardingCutoff },
84
84
  )
85
85
  }
86
86
 
87
87
  if (!query) return []
88
88
 
89
89
  const rows = await databaseService.query<unknown>(query)
90
- return rows.map((row) => mapWorkstreamRow(WorkstreamMessageRowSchema.parse(row)))
90
+ return rows.map((row) => mapThreadRow(ThreadMessageRowSchema.parse(row)))
91
91
  }
92
92
 
93
93
  export function normalizeBlock(value: string): string {
@@ -1,64 +0,0 @@
1
- # Workstream table.
2
- DEFINE TABLE IF NOT EXISTS workstream SCHEMAFULL;
3
- DEFINE FIELD IF NOT EXISTS organizationId ON TABLE workstream TYPE record<organization>;
4
- DEFINE FIELD IF NOT EXISTS userId ON TABLE workstream TYPE record<user>;
5
- DEFINE FIELD IF NOT EXISTS agentId ON TABLE workstream TYPE option<string>;
6
- DEFINE FIELD IF NOT EXISTS mode ON TABLE workstream TYPE string DEFAULT 'group';
7
- DEFINE FIELD IF NOT EXISTS core ON TABLE workstream TYPE bool DEFAULT false;
8
- DEFINE FIELD IF NOT EXISTS coreType ON TABLE workstream TYPE option<string>;
9
- DEFINE FIELD IF NOT EXISTS title ON TABLE workstream TYPE option<string>;
10
- DEFINE FIELD IF NOT EXISTS status ON TABLE workstream TYPE string DEFAULT 'regular';
11
- DEFINE FIELD IF NOT EXISTS createdAt ON TABLE workstream TYPE datetime DEFAULT time::now() READONLY;
12
- DEFINE FIELD IF NOT EXISTS updatedAt ON TABLE workstream TYPE datetime VALUE time::now();
13
- DEFINE FIELD IF NOT EXISTS memoryBlock ON TABLE workstream TYPE option<string>;
14
- DEFINE FIELD IF NOT EXISTS memoryBlockSummary ON TABLE workstream TYPE option<string>;
15
- DEFINE FIELD IF NOT EXISTS activeRunId ON TABLE workstream TYPE option<string>;
16
- DEFINE FIELD IF NOT EXISTS activeStreamId ON TABLE workstream TYPE option<string>;
17
- DEFINE FIELD IF NOT EXISTS compactionSummary ON TABLE workstream TYPE option<string>;
18
- DEFINE FIELD IF NOT EXISTS lastCompactedMessageId ON TABLE workstream TYPE option<string>;
19
- DEFINE FIELD IF NOT EXISTS nameGenerated ON TABLE workstream TYPE bool DEFAULT false;
20
- DEFINE FIELD IF NOT EXISTS isCompacting ON TABLE workstream TYPE bool DEFAULT false;
21
- DEFINE FIELD IF NOT EXISTS members ON TABLE workstream TYPE option<array<string>> DEFAULT [];
22
- DEFINE FIELD IF NOT EXISTS turnCount ON TABLE workstream TYPE int DEFAULT 0;
23
-
24
- DEFINE INDEX IF NOT EXISTS workstreamOrgIdx ON TABLE workstream COLUMNS organizationId;
25
- DEFINE INDEX IF NOT EXISTS workstreamUserIdx ON TABLE workstream COLUMNS userId;
26
- DEFINE INDEX IF NOT EXISTS workstreamUserOrgModeUpdatedIdx ON TABLE workstream COLUMNS userId, organizationId, mode, updatedAt;
27
- DEFINE INDEX IF NOT EXISTS workstreamUserOrgStatusModeUpdatedIdx ON TABLE workstream COLUMNS userId, organizationId, status, mode, updatedAt;
28
- DEFINE INDEX IF NOT EXISTS workstreamUserOrgCoreModeUpdatedIdx ON TABLE workstream COLUMNS userId, organizationId, core, mode, updatedAt;
29
- DEFINE INDEX IF NOT EXISTS workstreamUserOrgStatusCoreModeUpdatedIdx ON TABLE workstream COLUMNS userId, organizationId, status, core, mode, updatedAt;
30
-
31
- # Workstream Message table (AI SDK UIMessage persistence).
32
- # parts uses OVERWRITE on the wildcard to override the implicit non-FLEXIBLE
33
- # definition that array<object> creates — this is the only way to allow
34
- # arbitrary nested object shapes inside the array on SCHEMAFULL tables.
35
- DEFINE TABLE IF NOT EXISTS workstreamMessage SCHEMAFULL;
36
- DEFINE FIELD IF NOT EXISTS workstreamId ON TABLE workstreamMessage TYPE record<workstream> REFERENCE ON DELETE CASCADE;
37
- DEFINE FIELD IF NOT EXISTS messageId ON TABLE workstreamMessage TYPE string;
38
- DEFINE FIELD IF NOT EXISTS role ON TABLE workstreamMessage TYPE string;
39
- DEFINE FIELD IF NOT EXISTS parts ON TABLE workstreamMessage TYPE array<object> FLEXIBLE;
40
- DEFINE FIELD OVERWRITE parts.* ON TABLE workstreamMessage TYPE object FLEXIBLE;
41
- DEFINE FIELD IF NOT EXISTS metadata ON TABLE workstreamMessage TYPE option<object> FLEXIBLE;
42
- DEFINE FIELD IF NOT EXISTS createdAt ON TABLE workstreamMessage TYPE datetime DEFAULT time::now() READONLY;
43
- DEFINE FIELD IF NOT EXISTS updatedAt ON TABLE workstreamMessage TYPE datetime VALUE time::now();
44
-
45
- DEFINE INDEX IF NOT EXISTS workstreamMessageWorkstreamIdx ON TABLE workstreamMessage COLUMNS workstreamId;
46
- DEFINE INDEX IF NOT EXISTS workstreamMessageWorkstreamCreatedIdx ON TABLE workstreamMessage COLUMNS workstreamId, createdAt;
47
- DEFINE INDEX IF NOT EXISTS workstreamMessageWorkstreamMessageUniqueIdx ON TABLE workstreamMessage COLUMNS workstreamId, messageId UNIQUE;
48
-
49
- # Workstream attachments.
50
- DEFINE TABLE IF NOT EXISTS workstreamAttachment SCHEMAFULL;
51
- DEFINE FIELD IF NOT EXISTS workstreamId ON TABLE workstreamAttachment TYPE record<workstream> REFERENCE ON DELETE CASCADE;
52
- DEFINE FIELD IF NOT EXISTS messageId ON TABLE workstreamAttachment TYPE record<workstreamMessage> REFERENCE ON DELETE CASCADE;
53
- DEFINE FIELD IF NOT EXISTS attachmentType ON TABLE workstreamAttachment TYPE string;
54
- DEFINE FIELD IF NOT EXISTS name ON TABLE workstreamAttachment TYPE string;
55
- DEFINE FIELD IF NOT EXISTS contentType ON TABLE workstreamAttachment TYPE string;
56
- DEFINE FIELD IF NOT EXISTS storageKey ON TABLE workstreamAttachment TYPE option<string>;
57
- DEFINE FIELD IF NOT EXISTS sizeBytes ON TABLE workstreamAttachment TYPE option<int>;
58
- DEFINE FIELD IF NOT EXISTS url ON TABLE workstreamAttachment TYPE option<string>;
59
- DEFINE FIELD IF NOT EXISTS createdAt ON TABLE workstreamAttachment TYPE datetime DEFAULT time::now() READONLY;
60
- DEFINE FIELD IF NOT EXISTS updatedAt ON TABLE workstreamAttachment TYPE datetime VALUE time::now();
61
-
62
- DEFINE INDEX IF NOT EXISTS workstreamAttachmentWorkstreamIdx ON TABLE workstreamAttachment COLUMNS workstreamId;
63
- DEFINE INDEX IF NOT EXISTS workstreamAttachmentMessageIdx ON TABLE workstreamAttachment COLUMNS messageId;
64
- DEFINE INDEX IF NOT EXISTS workstreamAttachmentWorkstreamMessageIdx ON TABLE workstreamAttachment COLUMNS workstreamId, messageId;
@@ -1,72 +0,0 @@
1
- export interface WorkstreamBootstrapWelcomeConfig {
2
- directAgentId: string
3
- buildMessageText: (params: { userName?: string | null }) => string
4
- }
5
-
6
- export interface LotaWorkstreamBootstrapConfig {
7
- onboardingDirectAgents?: readonly string[]
8
- completedDirectAgents?: readonly string[]
9
- coreTypesAfterOnboarding?: readonly string[]
10
- ensureDefaultGroupOnCompleted?: boolean
11
- onboardingWelcome?: WorkstreamBootstrapWelcomeConfig
12
- }
13
-
14
- export interface LotaWorkstreamConfig {
15
- bootstrap?: LotaWorkstreamBootstrapConfig
16
- }
17
-
18
- interface ResolvedWorkstreamBootstrapConfig {
19
- onboardingDirectAgents: readonly string[]
20
- completedDirectAgents: readonly string[]
21
- coreTypesAfterOnboarding: readonly string[]
22
- ensureDefaultGroupOnCompleted: boolean
23
- onboardingWelcome?: WorkstreamBootstrapWelcomeConfig
24
- }
25
-
26
- const DEFAULT_WORKSTREAM_BOOTSTRAP_CONFIG: ResolvedWorkstreamBootstrapConfig = {
27
- onboardingDirectAgents: [],
28
- completedDirectAgents: [],
29
- coreTypesAfterOnboarding: [],
30
- ensureDefaultGroupOnCompleted: true,
31
- }
32
-
33
- let resolvedWorkstreamBootstrapConfig: ResolvedWorkstreamBootstrapConfig = DEFAULT_WORKSTREAM_BOOTSTRAP_CONFIG
34
-
35
- function withDedupedStrings(values: readonly string[]): string[] {
36
- const seen = new Set<string>()
37
- const deduped: string[] = []
38
-
39
- for (const value of values) {
40
- const normalized = value.trim()
41
- if (!normalized || seen.has(normalized)) continue
42
- seen.add(normalized)
43
- deduped.push(normalized)
44
- }
45
-
46
- return deduped
47
- }
48
-
49
- export function configureWorkstreams(params: { agentRoster: readonly string[]; config?: LotaWorkstreamConfig }): void {
50
- const bootstrap = params.config?.bootstrap
51
- const onboardingWelcome = bootstrap?.onboardingWelcome
52
- const onboardingDirectAgents = withDedupedStrings([
53
- ...(bootstrap?.onboardingDirectAgents ?? params.agentRoster),
54
- ...(onboardingWelcome ? [onboardingWelcome.directAgentId] : []),
55
- ])
56
-
57
- resolvedWorkstreamBootstrapConfig = {
58
- onboardingDirectAgents,
59
- completedDirectAgents: withDedupedStrings(bootstrap?.completedDirectAgents ?? params.agentRoster),
60
- coreTypesAfterOnboarding: withDedupedStrings(bootstrap?.coreTypesAfterOnboarding ?? []),
61
- ensureDefaultGroupOnCompleted: bootstrap?.ensureDefaultGroupOnCompleted ?? true,
62
- ...(onboardingWelcome ? { onboardingWelcome } : {}),
63
- }
64
- }
65
-
66
- export function getWorkstreamBootstrapConfig(): ResolvedWorkstreamBootstrapConfig {
67
- return resolvedWorkstreamBootstrapConfig
68
- }
69
-
70
- export function resolveOnboardingOwnerAgentId(defaultLeadAgentId: string): string {
71
- return resolvedWorkstreamBootstrapConfig.onboardingWelcome?.directAgentId ?? defaultLeadAgentId
72
- }
@@ -1,22 +0,0 @@
1
- import type { PlanRunRecord, SerializableExecutionPlan } from '@lota-sdk/shared'
2
-
3
- import type { RecordIdInput } from '../db/record-id'
4
- import { planRunService } from './plan-run.service'
5
-
6
- class WorkstreamPlanRegistryService {
7
- async listActiveRuns(workstreamId: RecordIdInput): Promise<PlanRunRecord[]> {
8
- return planRunService.getActiveRunRecords(workstreamId)
9
- }
10
-
11
- async countActiveRuns(workstreamId: RecordIdInput): Promise<number> {
12
- const runs = await this.listActiveRuns(workstreamId)
13
- return runs.length
14
- }
15
-
16
- async listActivePlans(workstreamId: RecordIdInput): Promise<SerializableExecutionPlan[]> {
17
- const runs = await this.listActiveRuns(workstreamId)
18
- return Promise.all(runs.map((run) => planRunService.toSerializablePlan(run)))
19
- }
20
- }
21
-
22
- export const workstreamPlanRegistryService = new WorkstreamPlanRegistryService()
@@ -1,42 +0,0 @@
1
- import { WORKSTREAM } from '@lota-sdk/shared'
2
-
3
- import { chatLogger } from '../config/logger'
4
- import type { RecordIdRef } from '../db/record-id'
5
- import { createHelperModelRuntime } from '../runtime/helper-model'
6
- import { deriveTitle, limitTitleWords, normalizeTitle } from '../runtime/title-helpers'
7
- import {
8
- createWorkstreamTitleGeneratorAgent,
9
- WORKSTREAM_TITLE_GENERATOR_PROMPT,
10
- } from '../system-agents/title-generator.agent'
11
- import { workstreamService } from './workstream.service'
12
-
13
- const WORKSTREAM_TITLE_TIMEOUT_MS = 30_000
14
-
15
- class WorkstreamTitleService {
16
- helperRuntime = createHelperModelRuntime()
17
-
18
- async generateAndPersistTitle(workstreamId: RecordIdRef, sourceText: string): Promise<void> {
19
- let title = ''
20
- try {
21
- title = normalizeTitle(
22
- await this.helperRuntime.generateHelperText({
23
- tag: 'workstream-title',
24
- createAgent: createWorkstreamTitleGeneratorAgent,
25
- defaultSystemPrompt: WORKSTREAM_TITLE_GENERATOR_PROMPT,
26
- timeoutMs: WORKSTREAM_TITLE_TIMEOUT_MS,
27
- messages: [{ role: 'user', content: sourceText }],
28
- }),
29
- )
30
- } catch (error) {
31
- chatLogger.warn`Failed to generate workstream title via LLM (non-fatal): ${error}`
32
- }
33
-
34
- if (!title) {
35
- title = limitTitleWords(deriveTitle(sourceText || WORKSTREAM.DEFAULT_TITLE))
36
- }
37
-
38
- await workstreamService.update(workstreamId, { title, nameGenerated: true })
39
- }
40
- }
41
-
42
- export const workstreamTitleService = new WorkstreamTitleService()