@lota-sdk/core 0.2.3 → 0.3.1

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 (106) hide show
  1. package/infrastructure/schema/00_identity.surql +2 -2
  2. package/infrastructure/schema/00_thread.surql +73 -0
  3. package/infrastructure/schema/02_execution_plan.surql +10 -11
  4. package/infrastructure/schema/04_runtime_bootstrap.surql +1 -0
  5. package/infrastructure/schema/10_autonomous_job.surql +3 -3
  6. package/package.json +2 -2
  7. package/src/ai/definitions.ts +1 -1
  8. package/src/config/agent-defaults.ts +5 -5
  9. package/src/config/index.ts +1 -1
  10. package/src/config/thread-defaults.ts +72 -0
  11. package/src/create-runtime.ts +90 -94
  12. package/src/db/record-id.ts +21 -21
  13. package/src/db/service.ts +44 -40
  14. package/src/db/tables.ts +3 -3
  15. package/src/db/{workstream-message-row.ts → thread-message-row.ts} +3 -3
  16. package/src/queues/context-compaction.queue.ts +6 -6
  17. package/src/queues/plan-agent-heartbeat.queue.ts +3 -3
  18. package/src/queues/post-chat-memory.queue.ts +1 -1
  19. package/src/queues/title-generation.queue.ts +10 -13
  20. package/src/redis/index.ts +1 -1
  21. package/src/redis/stream-context.ts +1 -1
  22. package/src/runtime/agent-identity-overrides.ts +1 -1
  23. package/src/runtime/agent-runtime-policy.ts +19 -21
  24. package/src/runtime/chat-request-routing.ts +1 -1
  25. package/src/runtime/context-compaction-constants.ts +1 -1
  26. package/src/runtime/context-compaction.ts +1 -1
  27. package/src/runtime/execution-plan.ts +1 -1
  28. package/src/runtime/index.ts +1 -1
  29. package/src/runtime/memory-digest-policy.ts +1 -1
  30. package/src/runtime/plugin-types.ts +1 -1
  31. package/src/runtime/post-turn-side-effects.ts +35 -35
  32. package/src/runtime/runtime-config.ts +24 -21
  33. package/src/runtime/runtime-extensions.ts +11 -11
  34. package/src/runtime/social-chat-agent-runner.ts +3 -3
  35. package/src/runtime/social-chat-history.ts +1 -1
  36. package/src/runtime/social-chat.ts +6 -6
  37. package/src/runtime/team-consultation-orchestrator.ts +1 -1
  38. package/src/runtime/{workstream-chat-helpers.ts → thread-chat-helpers.ts} +7 -7
  39. package/src/runtime/{workstream-plan-turn.ts → thread-plan-turn.ts} +11 -17
  40. package/src/runtime/{workstream-turn-context.ts → thread-turn-context.ts} +10 -10
  41. package/src/services/agent-activity.service.ts +39 -44
  42. package/src/services/agent-executor.service.ts +17 -19
  43. package/src/services/attachment.service.ts +4 -8
  44. package/src/services/autonomous-job.service.ts +29 -28
  45. package/src/services/context-compaction.service.ts +19 -29
  46. package/src/services/execution-plan.service.ts +58 -70
  47. package/src/services/global-orchestrator.service.ts +5 -5
  48. package/src/services/index.ts +6 -6
  49. package/src/services/memory.service.ts +1 -1
  50. package/src/services/monitoring-window.service.ts +2 -2
  51. package/src/services/mutating-approval.service.ts +7 -10
  52. package/src/services/node-workspace.service.ts +8 -7
  53. package/src/services/notification.service.ts +1 -1
  54. package/src/services/organization.service.ts +9 -9
  55. package/src/services/ownership-dispatcher.service.ts +13 -19
  56. package/src/services/plan-agent-heartbeat.service.ts +13 -13
  57. package/src/services/plan-agent-query.service.ts +7 -7
  58. package/src/services/plan-artifact.service.ts +1 -2
  59. package/src/services/plan-coordination.service.ts +4 -4
  60. package/src/services/plan-cycle.service.ts +7 -7
  61. package/src/services/plan-deadline.service.ts +4 -4
  62. package/src/services/plan-event-delivery.service.ts +8 -12
  63. package/src/services/plan-executor.service.ts +25 -39
  64. package/src/services/plan-run-data.ts +27 -8
  65. package/src/services/plan-run.service.ts +7 -9
  66. package/src/services/plan-scheduler.service.ts +4 -4
  67. package/src/services/plan-template.service.ts +2 -2
  68. package/src/services/plan-validator.service.ts +0 -11
  69. package/src/services/plugin-executor.service.ts +1 -1
  70. package/src/services/queue-job.service.ts +1 -1
  71. package/src/services/recent-activity-title.service.ts +1 -1
  72. package/src/services/recent-activity.service.ts +4 -4
  73. package/src/services/system-executor.service.ts +2 -2
  74. package/src/services/{workstream-message.service.ts → thread-message.service.ts} +72 -76
  75. package/src/services/thread-plan-registry.service.ts +22 -0
  76. package/src/services/thread-title.service.ts +39 -0
  77. package/src/services/{workstream-turn-preparation.service.ts → thread-turn-preparation.service.ts} +148 -171
  78. package/src/services/{workstream-turn.ts → thread-turn.ts} +27 -31
  79. package/src/services/thread.service.ts +853 -0
  80. package/src/services/thread.types.ts +17 -0
  81. package/src/storage/attachment-storage.service.ts +4 -4
  82. package/src/system-agents/index.ts +1 -1
  83. package/src/system-agents/memory.agent.ts +1 -1
  84. package/src/system-agents/recent-activity-title-refiner.agent.ts +2 -2
  85. package/src/system-agents/regular-chat-memory-digest.agent.ts +1 -1
  86. package/src/system-agents/researcher.agent.ts +3 -3
  87. package/src/system-agents/{workstream-router.agent.ts → thread-router.agent.ts} +68 -135
  88. package/src/system-agents/title-generator.agent.ts +8 -8
  89. package/src/tools/execution-plan.tool.ts +39 -40
  90. package/src/tools/memory-block.tool.ts +4 -4
  91. package/src/tools/research-topic.tool.ts +1 -0
  92. package/src/tools/search-web.tool.ts +1 -1
  93. package/src/tools/search.tool.ts +4 -4
  94. package/src/tools/team-think.tool.ts +9 -9
  95. package/src/utils/async.ts +6 -7
  96. package/src/workers/regular-chat-memory-digest.helpers.ts +1 -1
  97. package/src/workers/regular-chat-memory-digest.runner.ts +43 -43
  98. package/src/workers/skill-extraction.runner.ts +9 -13
  99. package/src/workers/utils/{workstream-message-query.ts → thread-message-query.ts} +21 -21
  100. package/infrastructure/schema/00_workstream.surql +0 -64
  101. package/src/config/workstream-defaults.ts +0 -72
  102. package/src/services/workstream-plan-registry.service.ts +0 -22
  103. package/src/services/workstream-title.service.ts +0 -42
  104. package/src/services/workstream.service.ts +0 -803
  105. package/src/services/workstream.types.ts +0 -17
  106. /package/src/services/{workstream-constants.ts → thread-constants.ts} +0 -0
@@ -55,7 +55,7 @@ class GlobalOrchestratorService {
55
55
  return 'skip'
56
56
  }
57
57
 
58
- async routeGraphFull(params: { workstreamId: string; runId: string }): Promise<void> {
58
+ async routeGraphFull(params: { threadId: string; runId: string }): Promise<void> {
59
59
  const MAX_ROUNDS = 32
60
60
  const STRUCTURAL_TYPES = new Set(['switch', 'join', 'deliberation-fork'])
61
61
 
@@ -105,7 +105,7 @@ class GlobalOrchestratorService {
105
105
  if (!ns || ns.owner.executorType !== 'agent') continue
106
106
  await enqueuePlanAgentHeartbeatWake({
107
107
  organizationId: recordIdToString(updatedRunForWake.organizationId, TABLES.ORGANIZATION),
108
- workstreamId: recordIdToString(updatedRunForWake.workstreamId, TABLES.WORKSTREAM),
108
+ threadId: recordIdToString(updatedRunForWake.threadId, TABLES.THREAD),
109
109
  runId: recordIdToString(updatedRunForWake.id, TABLES.PLAN_RUN),
110
110
  nodeId: nodeRun.nodeId,
111
111
  agentId: ns.owner.ref,
@@ -140,7 +140,7 @@ class GlobalOrchestratorService {
140
140
  }),
141
141
  )
142
142
 
143
- const workstreamId = recordIdToString(updatedRun.workstreamId, TABLES.WORKSTREAM)
143
+ const threadId = recordIdToString(updatedRun.threadId, TABLES.THREAD)
144
144
  const runId = recordIdToString(updatedRun.id, TABLES.PLAN_RUN)
145
145
 
146
146
  // Submit results sequentially (each triggers syncRunGraph internally)
@@ -151,7 +151,7 @@ class GlobalOrchestratorService {
151
151
 
152
152
  if (settled.status === 'fulfilled') {
153
153
  await planExecutorService.submitNodeResult({
154
- workstreamId,
154
+ threadId,
155
155
  runId,
156
156
  nodeId: settled.value.nodeId,
157
157
  emittedBy: settled.value.ownerRef,
@@ -160,7 +160,7 @@ class GlobalOrchestratorService {
160
160
  } else {
161
161
  serverLogger.warn`routeGraphFull: dispatch failed for node "${nodeRun.nodeId}": ${settled.reason}`
162
162
  await planExecutorService.blockNodeOnDispatchFailure({
163
- workstreamId,
163
+ threadId,
164
164
  runId,
165
165
  nodeId: nodeRun.nodeId,
166
166
  emittedBy: nodeSpecRecord?.owner.ref ?? 'unknown',
@@ -35,10 +35,10 @@ export * from './skill-resolver.service'
35
35
  export * from './social-chat-history.service'
36
36
  export * from './system-executor.service'
37
37
  export * from './user.service'
38
- export * from './workstream-message.service'
39
- export * from './workstream.types'
40
- export * from './workstream-title.service'
41
- export * from './workstream-turn'
42
- export * from './workstream-plan-registry.service'
43
- export * from './workstream.service'
38
+ export * from './thread-message.service'
39
+ export * from './thread.types'
40
+ export * from './thread-title.service'
41
+ export * from './thread-turn'
42
+ export * from './thread-plan-registry.service'
43
+ export * from './thread.service'
44
44
  export * from './write-intent-validator.service'
@@ -209,7 +209,7 @@ class MemoryService {
209
209
  MAX_CONVERSATION_MEMORY_BLOCK_CHARS,
210
210
  )
211
211
  if (normalizedMemoryBlock) {
212
- messages.push({ role: 'user', content: `Workstream memory block:\n${normalizedMemoryBlock}` })
212
+ messages.push({ role: 'user', content: `Thread memory block:\n${normalizedMemoryBlock}` })
213
213
  }
214
214
 
215
215
  const normalizedAttachmentContext = this.normalizeConversationText(
@@ -20,11 +20,11 @@ class MonitoringWindowService {
20
20
  nodeId: string
21
21
  config: MonitoringWindowConfig
22
22
  organizationId: string
23
- workstreamId: string
23
+ threadId: string
24
24
  }): Promise<void> {
25
25
  await planSchedulerService.createSchedule({
26
26
  organizationId: params.organizationId,
27
- workstreamId: params.workstreamId,
27
+ threadId: params.threadId,
28
28
  runId: params.runId,
29
29
  nodeId: params.nodeId,
30
30
  scheduleSpec: {
@@ -1,13 +1,13 @@
1
1
  import type { RecordIdRef } from '../db/record-id'
2
2
  import { ensureRecordId } from '../db/record-id'
3
3
  import { TABLES } from '../db/tables'
4
- import { extractMessageText } from '../runtime/workstream-chat-helpers'
5
- import { workstreamMessageService } from './workstream-message.service'
4
+ import { extractMessageText } from '../runtime/thread-chat-helpers'
5
+ import { threadMessageService } from './thread-message.service'
6
6
 
7
7
  const APPROVAL_VERIFICATION_MESSAGE_WINDOW = 20
8
8
 
9
9
  type VerifyMutatingApproval = (params: {
10
- workstreamId: string
10
+ threadId: string
11
11
  approvalReason: string
12
12
  approvalToken: string
13
13
  approvalMessageId?: string
@@ -58,8 +58,8 @@ function messageContainsExactApproval(messageText: string, approvalToken: string
58
58
  return extracted.token === approvalToken && extracted.reason === approvalReason
59
59
  }
60
60
 
61
- async function verifyMutatingApprovalForWorkstream(
62
- workstreamId: RecordIdRef,
61
+ async function verifyMutatingApprovalForThread(
62
+ threadId: RecordIdRef,
63
63
  params: { approvalReason: string; approvalToken: string; approvalMessageId?: string },
64
64
  ): Promise<{ ok: true } | { ok: false; reason: string }> {
65
65
  const token = extractQuotedValue(params.approvalToken)
@@ -72,10 +72,7 @@ async function verifyMutatingApprovalForWorkstream(
72
72
  return { ok: false, reason: 'approvalMessageId cannot be empty when provided.' }
73
73
  }
74
74
 
75
- const recentMessages = await workstreamMessageService.listRecentMessages(
76
- workstreamId,
77
- APPROVAL_VERIFICATION_MESSAGE_WINDOW,
78
- )
75
+ const recentMessages = await threadMessageService.listRecentMessages(threadId, APPROVAL_VERIFICATION_MESSAGE_WINDOW)
79
76
  const userMessages = recentMessages.filter((message) => message.role === 'user')
80
77
  if (userMessages.length === 0) {
81
78
  return { ok: false, reason: 'No user message history available to verify mutating approval.' }
@@ -106,5 +103,5 @@ async function verifyMutatingApprovalForWorkstream(
106
103
  }
107
104
 
108
105
  export const verifyMutatingApproval: VerifyMutatingApproval = async (params) => {
109
- return verifyMutatingApprovalForWorkstream(ensureRecordId(params.workstreamId, TABLES.WORKSTREAM), params)
106
+ return verifyMutatingApprovalForThread(ensureRecordId(params.threadId, TABLES.THREAD), params)
110
107
  }
@@ -1,5 +1,4 @@
1
1
  import type {
2
- NodeResultQuality,
3
2
  PlanArtifactSubmission,
4
3
  PlanNodeResult,
5
4
  PlanNodeSpec,
@@ -86,7 +85,7 @@ class NodeWorkspaceService {
86
85
  })
87
86
  }
88
87
 
89
- finalize(workspace: NodeWorkspace): PlanNodeResult & { isComplete: boolean; quality: NodeResultQuality } {
88
+ finalize(workspace: NodeWorkspace): PlanNodeResult & { isComplete: boolean } {
90
89
  const artifacts: PlanArtifactSubmission[] = []
91
90
  let structuredOutput: Record<string, unknown> | undefined
92
91
  let allValidated = true
@@ -125,8 +124,6 @@ class NodeWorkspaceService {
125
124
  artifacts.push({
126
125
  name: spec.name,
127
126
  kind: spec.kind,
128
- pointer: `workspace://${workspace.nodeId}/${targetPath}`,
129
- schemaRef: spec.schemaRef,
130
127
  description: spec.description,
131
128
  payload:
132
129
  typeof entry.payload === 'string'
@@ -138,10 +135,14 @@ class NodeWorkspaceService {
138
135
  }
139
136
 
140
137
  const allRequiredPresent = requiredNames.size === presentRequired.size
141
- const quality: NodeResultQuality = allRequiredPresent && allValidated ? 'full' : 'partial'
142
- const isComplete = quality === 'full'
138
+ const isComplete = allRequiredPresent && allValidated
143
139
 
144
- return { structuredOutput, artifacts, quality, isComplete }
140
+ return {
141
+ notes: isComplete ? 'All deliverables completed.' : 'Partial deliverables completed.',
142
+ structuredOutput,
143
+ artifacts,
144
+ isComplete,
145
+ }
145
146
  }
146
147
 
147
148
  cleanup(workspace: NodeWorkspace): void {
@@ -2,7 +2,7 @@ import type { NotificationSeverity } from '@lota-sdk/shared'
2
2
 
3
3
  export interface NotificationPayload {
4
4
  organizationId: string
5
- workstreamId: string
5
+ threadId: string
6
6
  runId?: string
7
7
  nodeId?: string
8
8
  severity: NotificationSeverity
@@ -11,8 +11,8 @@ import { toIsoDateTimeString, toOptionalIsoDateTimeString } from '../utils/date-
11
11
  const organizationRecordSchema = z.object({
12
12
  id: recordIdSchema,
13
13
  name: z.string(),
14
- regularChatDigestLastWorkstreamCursorCreatedAt: z.coerce.date().optional(),
15
- regularChatDigestLastWorkstreamCursorId: z.string().optional(),
14
+ regularChatDigestLastThreadCursorCreatedAt: z.coerce.date().optional(),
15
+ regularChatDigestLastThreadCursorId: z.string().optional(),
16
16
  skillExtractionLastCursorId: z.string().optional(),
17
17
  skillExtractionLastCursorCreatedAt: z.coerce.date().optional(),
18
18
  createdAt: z.coerce.date(),
@@ -22,8 +22,8 @@ const organizationRecordSchema = z.object({
22
22
  const sdkOrganizationSchema = z.object({
23
23
  id: recordIdStringSchema,
24
24
  name: z.string(),
25
- regularChatDigestLastWorkstreamCursorCreatedAt: z.iso.datetime().nullable().optional(),
26
- regularChatDigestLastWorkstreamCursorId: z.string().nullable().optional(),
25
+ regularChatDigestLastThreadCursorCreatedAt: z.iso.datetime().nullable().optional(),
26
+ regularChatDigestLastThreadCursorId: z.string().nullable().optional(),
27
27
  skillExtractionLastCursorId: z.string().nullable().optional(),
28
28
  skillExtractionLastCursorCreatedAt: z.iso.datetime().nullable().optional(),
29
29
  createdAt: z.iso.datetime(),
@@ -51,10 +51,10 @@ class OrganizationService extends BaseService<typeof organizationRecordSchema> {
51
51
  return sdkOrganizationSchema.parse({
52
52
  id: recordIdToString(ensureRecordId(record.id as RecordIdInput, TABLES.ORGANIZATION), TABLES.ORGANIZATION),
53
53
  name: record.name,
54
- regularChatDigestLastWorkstreamCursorCreatedAt: toOptionalCursorTimestamp(
55
- record.regularChatDigestLastWorkstreamCursorCreatedAt,
54
+ regularChatDigestLastThreadCursorCreatedAt: toOptionalCursorTimestamp(
55
+ record.regularChatDigestLastThreadCursorCreatedAt,
56
56
  ),
57
- regularChatDigestLastWorkstreamCursorId: record.regularChatDigestLastWorkstreamCursorId ?? null,
57
+ regularChatDigestLastThreadCursorId: record.regularChatDigestLastThreadCursorId ?? null,
58
58
  skillExtractionLastCursorId: record.skillExtractionLastCursorId ?? null,
59
59
  skillExtractionLastCursorCreatedAt: toOptionalCursorTimestamp(record.skillExtractionLastCursorCreatedAt),
60
60
  createdAt: toIsoDateTimeString(record.createdAt),
@@ -101,8 +101,8 @@ class OrganizationService extends BaseService<typeof organizationRecordSchema> {
101
101
 
102
102
  async updateRegularChatDigestCursor(organizationId: RecordIdRef, cursor: BackgroundCursor): Promise<void> {
103
103
  await this.update(organizationId, {
104
- regularChatDigestLastWorkstreamCursorCreatedAt: cursor.createdAt,
105
- regularChatDigestLastWorkstreamCursorId: cursor.id,
104
+ regularChatDigestLastThreadCursorCreatedAt: cursor.createdAt,
105
+ regularChatDigestLastThreadCursorId: cursor.id,
106
106
  })
107
107
  }
108
108
 
@@ -31,8 +31,8 @@ import type { PlanValidationIssueInput } from './plan-validator.service'
31
31
  import { pluginExecutorService } from './plugin-executor.service'
32
32
  import { skillResolverService } from './skill-resolver.service'
33
33
  import { systemExecutorService } from './system-executor.service'
34
+ import { ThreadSchema } from './thread.types'
34
35
  import { userService } from './user.service'
35
- import { WorkstreamSchema } from './workstream.types'
36
36
 
37
37
  const STABLE_RUN_STATUSES = new Set(['awaiting-human', 'blocked', 'failed', 'completed', 'aborted'])
38
38
  const MAX_DISPATCH_ITERATIONS = 64
@@ -73,8 +73,6 @@ function toArtifactSubmission(artifact: PlanArtifactRecord): PlanArtifactSubmiss
73
73
  return {
74
74
  name: artifact.name,
75
75
  kind: artifact.kind,
76
- pointer: artifact.pointer,
77
- ...(artifact.schemaRef ? { schemaRef: artifact.schemaRef } : {}),
78
76
  ...(artifact.description ? { description: artifact.description } : {}),
79
77
  ...(artifact.payload !== undefined ? { payload: artifact.payload } : {}),
80
78
  }
@@ -157,7 +155,7 @@ class OwnershipDispatcherService {
157
155
  if (spec.executionMode === 'graph-full') {
158
156
  const { globalOrchestratorService } = await import('./global-orchestrator.service')
159
157
  await globalOrchestratorService.routeGraphFull({
160
- workstreamId: recordIdToString(run.workstreamId, TABLES.WORKSTREAM),
158
+ threadId: recordIdToString(run.threadId, TABLES.THREAD),
161
159
  runId: recordIdToString(run.id, TABLES.PLAN_RUN),
162
160
  })
163
161
  return this.serializeRun(run.id)
@@ -200,7 +198,7 @@ class OwnershipDispatcherService {
200
198
  })
201
199
 
202
200
  await planExecutorService.submitNodeResult({
203
- workstreamId: run.workstreamId,
201
+ threadId: run.threadId,
204
202
  runId: recordIdToString(run.id, TABLES.PLAN_RUN),
205
203
  nodeId: planNode.id,
206
204
  emittedBy: planNode.owner.ref,
@@ -208,7 +206,7 @@ class OwnershipDispatcherService {
208
206
  })
209
207
  } catch (error) {
210
208
  await planExecutorService.blockNodeOnDispatchFailure({
211
- workstreamId: run.workstreamId,
209
+ threadId: run.threadId,
212
210
  runId: recordIdToString(run.id, TABLES.PLAN_RUN),
213
211
  nodeId: planNode.id,
214
212
  emittedBy: planNode.owner.ref,
@@ -283,18 +281,14 @@ class OwnershipDispatcherService {
283
281
  nodeSpecRecord: PlanNodeSpecRecord,
284
282
  ): Promise<Omit<OwnershipDispatchContext, 'nodeId'>> {
285
283
  const organizationId = recordIdToString(run.organizationId, TABLES.ORGANIZATION)
286
- const workstreamId = recordIdToString(run.workstreamId, TABLES.WORKSTREAM)
284
+ const threadId = recordIdToString(run.threadId, TABLES.THREAD)
287
285
  const planId = recordIdToString(run.id, TABLES.PLAN_RUN)
288
- const [workstream, nodeSpecs, nodeRuns] = await Promise.all([
289
- databaseService.findOne(
290
- TABLES.WORKSTREAM,
291
- { id: ensureRecordId(run.workstreamId, TABLES.WORKSTREAM) },
292
- WorkstreamSchema,
293
- ),
286
+ const [thread, nodeSpecs, nodeRuns] = await Promise.all([
287
+ databaseService.findOne(TABLES.THREAD, { id: ensureRecordId(run.threadId, TABLES.THREAD) }, ThreadSchema),
294
288
  planRunService.listNodeSpecs(spec.id),
295
289
  planRunService.listNodeRuns(run.id),
296
290
  ])
297
- const userId = workstream?.userId ? recordIdToString(workstream.userId, TABLES.USER) : undefined
291
+ const userId = thread?.userId ? recordIdToString(thread.userId, TABLES.USER) : undefined
298
292
  const userName = userId
299
293
  ? await userService
300
294
  .getUser(userId)
@@ -304,8 +298,7 @@ class OwnershipDispatcherService {
304
298
  const nodeSpecsById = new Map(nodeSpecs.map((candidate) => [candidate.nodeId, candidate]))
305
299
  const upstreamHandoffs: UpstreamHandoff[] = nodeRuns
306
300
  .filter(
307
- (candidate): candidate is typeof candidate & { handoffContext: NonNullable<typeof candidate.handoffContext> } =>
308
- nodeSpecRecord.upstreamNodeIds.includes(candidate.nodeId) && candidate.handoffContext !== undefined,
301
+ (candidate) => nodeSpecRecord.upstreamNodeIds.includes(candidate.nodeId) && candidate.latestNotes !== undefined,
309
302
  )
310
303
  .map((candidate) => {
311
304
  const upstreamNodeSpec = nodeSpecsById.get(candidate.nodeId)
@@ -314,13 +307,13 @@ class OwnershipDispatcherService {
314
307
  label: upstreamNodeSpec?.label ?? candidate.nodeId,
315
308
  ownerRef: upstreamNodeSpec?.owner.ref ?? 'unknown',
316
309
  ownerType: upstreamNodeSpec?.owner.executorType ?? 'system',
317
- handoffContext: candidate.handoffContext,
310
+ summary: candidate.latestNotes ?? undefined,
318
311
  }
319
312
  })
320
313
 
321
314
  return {
322
315
  organizationId,
323
- workstreamId,
316
+ threadId,
324
317
  planId,
325
318
  leadAgentId: run.leadAgentId,
326
319
  ...(userId ? { userId } : {}),
@@ -355,9 +348,10 @@ class OwnershipDispatcherService {
355
348
  nodeId: params.nodeSpec.id,
356
349
  config: params.nodeSpec.monitoringConfig,
357
350
  organizationId: params.context.organizationId,
358
- workstreamId: params.context.workstreamId,
351
+ threadId: params.context.threadId,
359
352
  })
360
353
  return {
354
+ notes: `Monitoring window started for node "${params.nodeSpec.label}".`,
361
355
  structuredOutput: { status: 'monitoring-started', config: params.nodeSpec.monitoringConfig },
362
356
  artifacts: [],
363
357
  }
@@ -6,7 +6,7 @@ import { resolvePlanNodeExecutionVisibility } from '../runtime/execution-plan-vi
6
6
  import { planAgentQueryService } from './plan-agent-query.service'
7
7
  import { planExecutorService } from './plan-executor.service'
8
8
  import { planRunService } from './plan-run.service'
9
- import { workstreamService } from './workstream.service'
9
+ import { threadService } from './thread.service'
10
10
 
11
11
  const PLAN_AGENT_HEARTBEAT_LOCK_TTL_MS = 300_000
12
12
  const PLAN_AGENT_HEARTBEAT_LOCK_REFRESH_MS = 20_000
@@ -17,26 +17,26 @@ function buildHeartbeatLockKey(runId: string, nodeId: string): string {
17
17
 
18
18
  function buildWakeDedupeKey(params: {
19
19
  organizationId: string
20
- workstreamId: string
20
+ threadId: string
21
21
  runId: string
22
22
  nodeId: string
23
23
  agentId: string
24
24
  }) {
25
- return `${params.organizationId}:${params.workstreamId}:${params.runId}:${params.nodeId}:${params.agentId}`
25
+ return `${params.organizationId}:${params.threadId}:${params.runId}:${params.nodeId}:${params.agentId}`
26
26
  }
27
27
 
28
28
  class PlanAgentHeartbeatService {
29
29
  async wakeNode(params: {
30
30
  organizationId: string
31
- workstreamId: string
31
+ threadId: string
32
32
  runId: string
33
33
  nodeId: string
34
34
  agentId: string
35
35
  reason: string
36
36
  }): Promise<boolean> {
37
- const workstreamRef = ensureRecordId(params.workstreamId, TABLES.WORKSTREAM)
38
- await workstreamService.clearStaleActiveRunIfMissingFromRegistry(workstreamRef)
39
- if (await workstreamService.getActiveRunId(workstreamRef)) {
37
+ const threadRef = ensureRecordId(params.threadId, TABLES.THREAD)
38
+ await threadService.clearStaleActiveRunIfMissingFromRegistry(threadRef)
39
+ if (await threadService.getActiveRunId(threadRef)) {
40
40
  return false
41
41
  }
42
42
 
@@ -51,11 +51,11 @@ class PlanAgentHeartbeatService {
51
51
  logger: serverLogger,
52
52
  },
53
53
  async () => {
54
- await workstreamService.clearStaleActiveRunIfMissingFromRegistry(workstreamRef)
55
- if (await workstreamService.hasActiveRunLease(workstreamRef)) {
54
+ await threadService.clearStaleActiveRunIfMissingFromRegistry(threadRef)
55
+ if (await threadService.hasActiveRunLease(threadRef)) {
56
56
  return false
57
57
  }
58
- if (await workstreamService.getActiveRunId(workstreamRef)) {
58
+ if (await threadService.getActiveRunId(threadRef)) {
59
59
  return false
60
60
  }
61
61
 
@@ -83,7 +83,7 @@ class PlanAgentHeartbeatService {
83
83
  await planExecutorService.transitionNodeToRunning({ runId: params.runId, nodeId: params.nodeId })
84
84
  }
85
85
 
86
- const { triggerPlanNodeTurn } = await import('./workstream-turn')
86
+ const { triggerPlanNodeTurn } = await import('./thread-turn')
87
87
 
88
88
  await triggerPlanNodeTurn({ runId: params.runId, nodeId: params.nodeId })
89
89
  return true
@@ -101,7 +101,7 @@ class PlanAgentHeartbeatService {
101
101
 
102
102
  const wakeTargets = new Map<
103
103
  string,
104
- { organizationId: string; workstreamId: string; runId: string; nodeId: string; agentId: string; reason: string }
104
+ { organizationId: string; threadId: string; runId: string; nodeId: string; agentId: string; reason: string }
105
105
  >()
106
106
 
107
107
  for (const node of actionable) {
@@ -118,7 +118,7 @@ class PlanAgentHeartbeatService {
118
118
  }
119
119
  const wakeTarget = {
120
120
  organizationId: node.organizationId,
121
- workstreamId: node.workstreamId,
121
+ threadId: node.threadId,
122
122
  runId: node.runId,
123
123
  nodeId: node.nodeId,
124
124
  agentId: node.agentId,
@@ -15,7 +15,7 @@ const DEADLINE_TRACKED_NODE_STATUSES = new Set(['ready', 'running', 'awaiting-hu
15
15
 
16
16
  export interface ActionablePlanAgentNode {
17
17
  organizationId: string
18
- workstreamId: string
18
+ threadId: string
19
19
  runId: string
20
20
  nodeId: string
21
21
  agentId: string
@@ -25,7 +25,7 @@ export interface ActionablePlanAgentNode {
25
25
 
26
26
  export interface ApproachingDeadlineNode {
27
27
  organizationId: string
28
- workstreamId: string
28
+ threadId: string
29
29
  runId: string
30
30
  nodeId: string
31
31
  agentId?: string
@@ -37,7 +37,7 @@ export interface ApproachingDeadlineNode {
37
37
 
38
38
  export interface RecentlyUnblockedNode {
39
39
  organizationId: string
40
- workstreamId: string
40
+ threadId: string
41
41
  runId: string
42
42
  nodeId: string
43
43
  agentId: string
@@ -89,7 +89,7 @@ class PlanAgentQueryService {
89
89
  if (params.agentId && params.agentId !== visibleTarget.agentId) continue
90
90
  actionable.push({
91
91
  organizationId: recordIdToString(run.organizationId, TABLES.ORGANIZATION),
92
- workstreamId: recordIdToString(run.workstreamId, TABLES.WORKSTREAM),
92
+ threadId: recordIdToString(run.threadId, TABLES.THREAD),
93
93
  runId: recordIdToString(run.id, TABLES.PLAN_RUN),
94
94
  nodeId: nodeSpec.nodeId,
95
95
  agentId: visibleTarget.agentId,
@@ -124,7 +124,7 @@ class PlanAgentQueryService {
124
124
 
125
125
  actionable.push({
126
126
  organizationId: recordIdToString(run.organizationId, TABLES.ORGANIZATION),
127
- workstreamId: recordIdToString(run.workstreamId, TABLES.WORKSTREAM),
127
+ threadId: recordIdToString(run.threadId, TABLES.THREAD),
128
128
  runId: recordIdToString(run.id, TABLES.PLAN_RUN),
129
129
  nodeId: nodeSpec.nodeId,
130
130
  agentId: visibleTarget.agentId,
@@ -176,7 +176,7 @@ class PlanAgentQueryService {
176
176
  const visibleTarget = isVisibleAgentNode({ nodeSpec, spec })
177
177
  matches.push({
178
178
  organizationId: recordIdToString(run.organizationId, TABLES.ORGANIZATION),
179
- workstreamId: recordIdToString(run.workstreamId, TABLES.WORKSTREAM),
179
+ threadId: recordIdToString(run.threadId, TABLES.THREAD),
180
180
  runId: recordIdToString(run.id, TABLES.PLAN_RUN),
181
181
  nodeId: nodeSpec.nodeId,
182
182
  ...(visibleTarget ? { agentId: visibleTarget.agentId, visibility: visibleTarget.visibility } : {}),
@@ -232,7 +232,7 @@ class PlanAgentQueryService {
232
232
 
233
233
  matches.push({
234
234
  organizationId: recordIdToString(run.organizationId, TABLES.ORGANIZATION),
235
- workstreamId: recordIdToString(run.workstreamId, TABLES.WORKSTREAM),
235
+ threadId: recordIdToString(run.threadId, TABLES.THREAD),
236
236
  runId: recordIdToString(run.id, TABLES.PLAN_RUN),
237
237
  nodeId: currentNodeId,
238
238
  agentId: visibleTarget.agentId,
@@ -28,8 +28,7 @@ class PlanArtifactService {
28
28
  nodeId: params.nodeId,
29
29
  name: artifact.name,
30
30
  kind: artifact.kind,
31
- pointer: artifact.pointer,
32
- ...(artifact.schemaRef ? { schemaRef: artifact.schemaRef } : {}),
31
+ pointer: `artifact://${params.nodeId}/${artifact.name}`,
33
32
  ...(artifact.description ? { description: artifact.description } : {}),
34
33
  ...(artifact.payload ? { payload: artifact.payload } : {}),
35
34
  })
@@ -13,7 +13,7 @@ class PlanCoordinationService {
13
13
  /**
14
14
  * Resolve cross-plan artifact dependencies.
15
15
  * For each dependency:
16
- * 1. Find the source plan by title in the workstream
16
+ * 1. Find the source plan by title in the thread
17
17
  * 2. Find the artifact by (nodeId, artifactName) in that plan's run
18
18
  * 3. Check staleness if maxStalenessMs set
19
19
  * 4. Based on triggerMode:
@@ -23,7 +23,7 @@ class PlanCoordinationService {
23
23
  */
24
24
  async resolveDependencies(params: {
25
25
  dependencies: PlanDependency[]
26
- workstreamId: string
26
+ threadId: string
27
27
  }): Promise<DependencyResolutionResult> {
28
28
  const { planRunService } = await import('./plan-run.service')
29
29
 
@@ -34,10 +34,10 @@ class PlanCoordinationService {
34
34
  for (const dep of params.dependencies) {
35
35
  const depKey = `${dep.sourcePlanTitle}:${dep.sourceNodeId}:${dep.artifactName}`
36
36
 
37
- const specs = await planRunService.listPlanSpecsByWorkstream(params.workstreamId)
37
+ const specs = await planRunService.listPlanSpecsByThread(params.threadId)
38
38
  const sourceSpec = specs.find((s) => s.title === dep.sourcePlanTitle)
39
39
  if (!sourceSpec) {
40
- const reason = `Source plan "${dep.sourcePlanTitle}" not found in workstream.`
40
+ const reason = `Source plan "${dep.sourcePlanTitle}" not found in thread.`
41
41
  if (dep.triggerMode === 'block') {
42
42
  unresolved.push(dep)
43
43
  } else if (dep.triggerMode === 'notify') {
@@ -64,7 +64,7 @@ class PlanCycleService {
64
64
 
65
65
  async createCycle(params: {
66
66
  organizationId: RecordIdInput
67
- workstreamId: RecordIdInput
67
+ threadId: RecordIdInput
68
68
  templateId: RecordIdInput
69
69
  name: string
70
70
  schedule: CycleSchedule
@@ -77,7 +77,7 @@ class PlanCycleService {
77
77
  TABLES.PLAN_CYCLE,
78
78
  {
79
79
  organizationId: ensureRecordId(params.organizationId, TABLES.ORGANIZATION),
80
- workstreamId: ensureRecordId(params.workstreamId, TABLES.WORKSTREAM),
80
+ threadId: ensureRecordId(params.threadId, TABLES.THREAD),
81
81
  templateId: ensureRecordId(params.templateId, TABLES.PLAN_TEMPLATE),
82
82
  name: params.name,
83
83
  schedule: params.schedule,
@@ -92,7 +92,7 @@ class PlanCycleService {
92
92
  const createResult = await planTemplateService.instantiate({
93
93
  templateId: params.templateId,
94
94
  organizationId: params.organizationId,
95
- workstreamId: params.workstreamId,
95
+ threadId: params.threadId,
96
96
  leadAgentId: params.leadAgentId,
97
97
  })
98
98
 
@@ -101,7 +101,7 @@ class PlanCycleService {
101
101
  const scheduleSpec = cycleScheduleToSpec(params.schedule)
102
102
  const scheduleRecord = await planSchedulerService.createSchedule({
103
103
  organizationId: params.organizationId,
104
- workstreamId: params.workstreamId,
104
+ threadId: params.threadId,
105
105
  ...(createdRunId ? { runId: ensureRecordId(createdRunId, TABLES.PLAN_RUN) } : {}),
106
106
  scheduleSpec,
107
107
  })
@@ -160,7 +160,7 @@ class PlanCycleService {
160
160
  const result = await planTemplateService.instantiate({
161
161
  templateId: cycle.templateId,
162
162
  organizationId: cycle.organizationId,
163
- workstreamId: cycle.workstreamId,
163
+ threadId: cycle.threadId,
164
164
  leadAgentId: 'system',
165
165
  overrides: draft,
166
166
  })
@@ -263,10 +263,10 @@ class PlanCycleService {
263
263
  )
264
264
  }
265
265
 
266
- async listCycles(workstreamId: RecordIdInput): Promise<PlanCycleRecord[]> {
266
+ async listCycles(threadId: RecordIdInput): Promise<PlanCycleRecord[]> {
267
267
  return databaseService.findMany(
268
268
  TABLES.PLAN_CYCLE,
269
- { workstreamId: ensureRecordId(workstreamId, TABLES.WORKSTREAM) },
269
+ { threadId: ensureRecordId(threadId, TABLES.THREAD) },
270
270
  PlanCycleRecordSchema,
271
271
  { orderBy: 'createdAt', orderDir: 'ASC' },
272
272
  )
@@ -126,7 +126,7 @@ class PlanDeadlineService {
126
126
  await this.applyDeadlineMissAction({
127
127
  runId: entry.nodeRun.runId,
128
128
  nodeId: entry.nodeRun.nodeId,
129
- workstreamId: recordIdToString(run.workstreamId, TABLES.WORKSTREAM),
129
+ threadId: recordIdToString(run.threadId, TABLES.THREAD),
130
130
  organizationId: recordIdToString(run.organizationId, TABLES.ORGANIZATION),
131
131
  action: deadline.missAction,
132
132
  emittedBy: 'plan-deadline-checker',
@@ -228,7 +228,7 @@ class PlanDeadlineService {
228
228
  async applyDeadlineMissAction(params: {
229
229
  runId: RecordIdInput
230
230
  nodeId: string
231
- workstreamId: string
231
+ threadId: string
232
232
  organizationId: string
233
233
  action: DeadlineAction
234
234
  emittedBy: string
@@ -275,7 +275,7 @@ class PlanDeadlineService {
275
275
  })
276
276
  const { planExecutorService } = await import('./plan-executor.service')
277
277
  await planExecutorService.blockNodeOnDispatchFailure({
278
- workstreamId: params.workstreamId,
278
+ threadId: params.threadId,
279
279
  runId: runIdStr,
280
280
  nodeId: params.nodeId,
281
281
  emittedBy: params.emittedBy,
@@ -296,7 +296,7 @@ class PlanDeadlineService {
296
296
  })
297
297
  const { planExecutorService } = await import('./plan-executor.service')
298
298
  await planExecutorService.blockNodeOnDispatchFailure({
299
- workstreamId: params.workstreamId,
299
+ threadId: params.threadId,
300
300
  runId: runIdStr,
301
301
  nodeId: params.nodeId,
302
302
  emittedBy: params.emittedBy,