@lota-sdk/core 0.1.15 → 0.1.17

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 (159) hide show
  1. package/infrastructure/schema/00_identity.surql +0 -2
  2. package/infrastructure/schema/01_memory.surql +1 -1
  3. package/infrastructure/schema/02_execution_plan.surql +62 -1
  4. package/infrastructure/schema/03_learned_skill.surql +1 -1
  5. package/infrastructure/schema/06_playbook.surql +25 -0
  6. package/infrastructure/schema/07_institutional_memory.surql +13 -0
  7. package/infrastructure/schema/08_quality_metrics.surql +17 -0
  8. package/package.json +12 -8
  9. package/src/ai/definitions.ts +81 -3
  10. package/src/ai/embedding-cache.ts +2 -4
  11. package/src/ai/index.ts +0 -2
  12. package/src/bifrost/bifrost.ts +2 -7
  13. package/src/bifrost/cache-headers.ts +8 -0
  14. package/src/bifrost/index.ts +1 -0
  15. package/src/config/agent-defaults.ts +31 -21
  16. package/src/config/agent-types.ts +11 -0
  17. package/src/config/constants.ts +2 -14
  18. package/src/config/debug-logger.ts +5 -1
  19. package/src/config/index.ts +3 -0
  20. package/src/config/model-constants.ts +16 -34
  21. package/src/config/search.ts +1 -15
  22. package/src/create-runtime.ts +269 -178
  23. package/src/db/cursor-pagination.ts +3 -6
  24. package/src/db/index.ts +2 -0
  25. package/src/db/memory-store.helpers.ts +1 -3
  26. package/src/db/memory-store.rows.ts +7 -7
  27. package/src/db/memory-store.ts +14 -18
  28. package/src/db/memory.ts +13 -13
  29. package/src/db/schema-fingerprint.ts +1 -3
  30. package/src/db/service.ts +153 -79
  31. package/src/db/startup.ts +6 -10
  32. package/src/db/surreal-mutation.ts +43 -0
  33. package/src/db/tables.ts +7 -0
  34. package/src/db/workstream-message-row.ts +15 -0
  35. package/src/embeddings/provider.ts +1 -1
  36. package/src/queues/context-compaction.queue.ts +15 -46
  37. package/src/queues/delayed-node-promotion.queue.ts +41 -0
  38. package/src/queues/document-processor.queue.ts +2 -4
  39. package/src/queues/index.ts +3 -0
  40. package/src/queues/memory-consolidation.queue.ts +16 -51
  41. package/src/queues/plan-scheduler.queue.ts +97 -0
  42. package/src/queues/post-chat-memory.queue.ts +20 -55
  43. package/src/queues/queue-factory.ts +100 -0
  44. package/src/queues/recent-activity-title-refinement.queue.ts +15 -50
  45. package/src/queues/regular-chat-memory-digest.queue.ts +16 -52
  46. package/src/queues/skill-extraction.queue.ts +15 -47
  47. package/src/queues/workstream-title-generation.queue.ts +15 -47
  48. package/src/redis/connection.ts +6 -0
  49. package/src/redis/index.ts +1 -1
  50. package/src/redis/redis-lease-lock.ts +1 -2
  51. package/src/redis/stream-context.ts +11 -0
  52. package/src/runtime/agent-runtime-policy.ts +109 -35
  53. package/src/runtime/approval-continuation.ts +12 -6
  54. package/src/runtime/context-compaction-runtime.ts +1 -1
  55. package/src/runtime/context-compaction.ts +24 -64
  56. package/src/runtime/execution-plan.ts +22 -18
  57. package/src/runtime/graph-designer.ts +15 -0
  58. package/src/runtime/helper-model.ts +9 -197
  59. package/src/runtime/index.ts +3 -1
  60. package/src/runtime/llm-content.ts +1 -1
  61. package/src/runtime/memory-block.ts +9 -11
  62. package/src/runtime/memory-pipeline.ts +6 -9
  63. package/src/runtime/plugin-resolution.ts +35 -0
  64. package/src/runtime/plugin-types.ts +72 -0
  65. package/src/runtime/retrieval-adapters.ts +1 -1
  66. package/src/runtime/runtime-config.ts +111 -14
  67. package/src/runtime/runtime-extensions.ts +2 -3
  68. package/src/runtime/runtime-worker-registry.ts +6 -0
  69. package/src/runtime/social-chat.ts +752 -0
  70. package/src/runtime/team-consultation-orchestrator.ts +45 -32
  71. package/src/runtime/team-consultation-prompts.ts +11 -2
  72. package/src/runtime/title-helpers.ts +2 -4
  73. package/src/runtime/workstream-chat-helpers.ts +1 -1
  74. package/src/services/adaptive-playbook.service.ts +152 -0
  75. package/src/services/agent-executor.service.ts +292 -0
  76. package/src/services/artifact-provenance.service.ts +172 -0
  77. package/src/services/attachment.service.ts +6 -11
  78. package/src/services/context-compaction.service.ts +72 -55
  79. package/src/services/context-enrichment.service.ts +33 -0
  80. package/src/services/coordination-registry.service.ts +117 -0
  81. package/src/services/document-chunk.service.ts +2 -4
  82. package/src/services/domain-agent-executor.service.ts +71 -0
  83. package/src/services/execution-plan.service.ts +269 -50
  84. package/src/services/feedback-loop.service.ts +96 -0
  85. package/src/services/global-orchestrator.service.ts +148 -0
  86. package/src/services/index.ts +27 -0
  87. package/src/services/institutional-memory.service.ts +145 -0
  88. package/src/services/learned-skill.service.ts +24 -5
  89. package/src/services/memory-assessment.service.ts +3 -2
  90. package/src/services/memory-utils.ts +3 -8
  91. package/src/services/memory.service.ts +49 -61
  92. package/src/services/monitoring-window.service.ts +86 -0
  93. package/src/services/mutating-approval.service.ts +1 -1
  94. package/src/services/node-workspace.service.ts +155 -0
  95. package/src/services/notification.service.ts +39 -0
  96. package/src/services/organization-member.service.ts +11 -4
  97. package/src/services/organization.service.ts +5 -5
  98. package/src/services/ownership-dispatcher.service.ts +403 -0
  99. package/src/services/plan-approval.service.ts +1 -1
  100. package/src/services/plan-builder.service.ts +1 -0
  101. package/src/services/plan-checkpoint.service.ts +30 -2
  102. package/src/services/plan-compiler.service.ts +5 -0
  103. package/src/services/plan-coordination.service.ts +152 -0
  104. package/src/services/plan-cycle.service.ts +284 -0
  105. package/src/services/plan-deadline.service.ts +287 -0
  106. package/src/services/plan-executor.service.ts +384 -40
  107. package/src/services/plan-run.service.ts +41 -7
  108. package/src/services/plan-scheduler.service.ts +240 -0
  109. package/src/services/plan-template.service.ts +117 -0
  110. package/src/services/plan-validator.service.ts +84 -2
  111. package/src/services/plan-workspace.service.ts +83 -0
  112. package/src/services/playbook-registry.service.ts +67 -0
  113. package/src/services/plugin-executor.service.ts +103 -0
  114. package/src/services/quality-metrics.service.ts +132 -0
  115. package/src/services/recent-activity.service.ts +28 -34
  116. package/src/services/skill-resolver.service.ts +19 -0
  117. package/src/services/social-chat-history.service.ts +197 -0
  118. package/src/services/system-executor.service.ts +105 -0
  119. package/src/services/workstream-message.service.ts +13 -37
  120. package/src/services/workstream-plan-registry.service.ts +22 -0
  121. package/src/services/workstream-title.service.ts +3 -1
  122. package/src/services/workstream-turn-preparation.service.ts +34 -89
  123. package/src/services/workstream.service.ts +33 -55
  124. package/src/services/workstream.types.ts +9 -9
  125. package/src/services/write-intent-validator.service.ts +81 -0
  126. package/src/storage/attachment-parser.ts +1 -1
  127. package/src/storage/attachment-utils.ts +1 -1
  128. package/src/storage/generated-document-storage.service.ts +3 -2
  129. package/src/system-agents/context-compaction.agent.ts +2 -0
  130. package/src/system-agents/delegated-agent-factory.ts +5 -0
  131. package/src/system-agents/memory-reranker.agent.ts +4 -2
  132. package/src/system-agents/memory.agent.ts +2 -0
  133. package/src/system-agents/recent-activity-title-refiner.agent.ts +2 -0
  134. package/src/system-agents/regular-chat-memory-digest.agent.ts +2 -0
  135. package/src/system-agents/skill-extractor.agent.ts +2 -0
  136. package/src/system-agents/skill-manager.agent.ts +2 -0
  137. package/src/system-agents/title-generator.agent.ts +2 -0
  138. package/src/tools/execution-plan.tool.ts +17 -23
  139. package/src/tools/index.ts +0 -1
  140. package/src/tools/research-topic.tool.ts +2 -0
  141. package/src/tools/team-think.tool.ts +5 -6
  142. package/src/utils/async.ts +2 -1
  143. package/src/utils/date-time.ts +4 -32
  144. package/src/utils/env.ts +8 -0
  145. package/src/utils/errors.ts +42 -10
  146. package/src/utils/index.ts +9 -0
  147. package/src/utils/string.ts +114 -1
  148. package/src/workers/index.ts +1 -0
  149. package/src/workers/regular-chat-memory-digest.helpers.ts +1 -1
  150. package/src/workers/regular-chat-memory-digest.runner.ts +45 -12
  151. package/src/workers/skill-extraction.runner.ts +26 -6
  152. package/src/workers/utils/file-section-chunker.ts +2 -1
  153. package/src/workers/utils/repo-structure-extractor.ts +2 -2
  154. package/src/workers/utils/repomix-file-sections.ts +2 -2
  155. package/src/workers/utils/sandbox-error.ts +11 -2
  156. package/src/workers/utils/workstream-message-query.ts +14 -25
  157. package/src/workers/worker-utils.ts +2 -2
  158. package/src/runtime/workstream-routing-policy.ts +0 -267
  159. package/src/tools/log-hello-world.tool.ts +0 -17
@@ -0,0 +1,67 @@
1
+ import type { PlanTemplateRecord } from '@lota-sdk/shared'
2
+
3
+ import type { RecordIdInput } from '../db/record-id'
4
+ import type { PlaybookContribution } from '../runtime/plugin-types'
5
+ import { getRuntimeConfig } from '../runtime/runtime-config'
6
+ import { planTemplateService } from './plan-template.service'
7
+
8
+ class PlaybookRegistryService {
9
+ collectPlaybooks(): PlaybookContribution[] {
10
+ const plugins = getRuntimeConfig().pluginRuntime ?? {}
11
+ const playbooks: PlaybookContribution[] = []
12
+ for (const plugin of Object.values(plugins)) {
13
+ if (plugin.playbookContributor?.playbooks) {
14
+ playbooks.push(...plugin.playbookContributor.playbooks)
15
+ }
16
+ }
17
+ return playbooks
18
+ }
19
+
20
+ async syncPlaybookTemplates(organizationId: RecordIdInput): Promise<PlanTemplateRecord[]> {
21
+ const playbooks = this.collectPlaybooks()
22
+ const templates: PlanTemplateRecord[] = []
23
+ const existing = await planTemplateService.listTemplates(organizationId, { source: 'playbook' })
24
+
25
+ for (const pb of playbooks) {
26
+ const match = existing.find((t) => t.sourceRef === pb.name)
27
+ if (match) {
28
+ const updated = await planTemplateService.updateTemplate(match.id, { draft: pb.draft, tags: pb.tags })
29
+ templates.push(updated)
30
+ } else {
31
+ const created = await planTemplateService.createTemplate({
32
+ organizationId,
33
+ name: pb.name,
34
+ description: pb.description,
35
+ draft: pb.draft,
36
+ tags: pb.tags,
37
+ source: 'playbook',
38
+ sourceRef: pb.name,
39
+ })
40
+ templates.push(created)
41
+ }
42
+ }
43
+
44
+ return templates
45
+ }
46
+
47
+ async instantiatePlaybook(params: {
48
+ name: string
49
+ organizationId: RecordIdInput
50
+ workstreamId: RecordIdInput
51
+ leadAgentId: string
52
+ }): Promise<unknown> {
53
+ const templates = await planTemplateService.listTemplates(params.organizationId, { source: 'playbook' })
54
+ const template = templates.find((t) => t.sourceRef === params.name)
55
+ if (!template) {
56
+ throw new Error(`Playbook "${params.name}" not found.`)
57
+ }
58
+ return planTemplateService.instantiate({
59
+ templateId: template.id,
60
+ organizationId: params.organizationId,
61
+ workstreamId: params.workstreamId,
62
+ leadAgentId: params.leadAgentId,
63
+ })
64
+ }
65
+ }
66
+
67
+ export const playbookRegistryService = new PlaybookRegistryService()
@@ -0,0 +1,103 @@
1
+ import type { OwnershipDispatchContext, PlanNodeResult, PlanNodeSpec, PluginPlanNodeOwner } from '@lota-sdk/shared'
2
+
3
+ import type { LotaPlugin, PluginNodeExecutionParams } from '../runtime/plugin-types'
4
+ import { getRuntimeConfig } from '../runtime/runtime-config'
5
+ import type { PlanValidationIssueInput } from './plan-validator.service'
6
+
7
+ function getPluginRuntime() {
8
+ return (getRuntimeConfig().pluginRuntime ?? {}) as Record<string, LotaPlugin | undefined>
9
+ }
10
+
11
+ function buildPluginExecutionParams(params: {
12
+ owner: PluginPlanNodeOwner
13
+ nodeSpec: PlanNodeSpec
14
+ resolvedInput: Record<string, unknown>
15
+ context: OwnershipDispatchContext
16
+ }): PluginNodeExecutionParams {
17
+ return {
18
+ operation: params.owner.operation,
19
+ nodeSpec: params.nodeSpec,
20
+ inputs: params.resolvedInput,
21
+ context: {
22
+ organizationId: params.context.organizationId,
23
+ workstreamId: params.context.workstreamId,
24
+ planId: params.context.planId,
25
+ nodeId: params.context.nodeId,
26
+ ...(params.context.userId ? { userId: params.context.userId } : {}),
27
+ ...(params.context.userName ? { userName: params.context.userName } : {}),
28
+ },
29
+ }
30
+ }
31
+
32
+ class PluginExecutorService {
33
+ validateOwner(owner: PluginPlanNodeOwner, nodeId: string): PlanValidationIssueInput[] {
34
+ const plugin = getPluginRuntime()[owner.ref]
35
+ if (!plugin) {
36
+ return [
37
+ {
38
+ severity: 'blocking',
39
+ code: 'plugin_executor_missing',
40
+ message: `Node "${nodeId}" references unknown plugin executor "${owner.ref}".`,
41
+ nodeId,
42
+ detail: { pluginRef: owner.ref },
43
+ },
44
+ ]
45
+ }
46
+
47
+ const nodeExecutor = plugin.nodeExecutor
48
+ if (!nodeExecutor) {
49
+ return [
50
+ {
51
+ severity: 'blocking',
52
+ code: 'plugin_node_executor_missing',
53
+ message: `Plugin "${owner.ref}" does not expose a node executor.`,
54
+ nodeId,
55
+ detail: { pluginRef: owner.ref },
56
+ },
57
+ ]
58
+ }
59
+
60
+ if (!nodeExecutor.supportedOperations.includes(owner.operation)) {
61
+ return [
62
+ {
63
+ severity: 'blocking',
64
+ code: 'plugin_operation_missing',
65
+ message: `Plugin "${owner.ref}" does not support operation "${owner.operation}".`,
66
+ nodeId,
67
+ detail: { pluginRef: owner.ref, operation: owner.operation },
68
+ },
69
+ ]
70
+ }
71
+
72
+ return []
73
+ }
74
+
75
+ async executeNode(params: {
76
+ nodeSpec: PlanNodeSpec
77
+ resolvedInput: Record<string, unknown>
78
+ context: OwnershipDispatchContext
79
+ }): Promise<PlanNodeResult> {
80
+ if (params.nodeSpec.owner.executorType !== 'plugin') {
81
+ throw new Error(`PluginExecutor cannot execute owner type "${params.nodeSpec.owner.executorType}".`)
82
+ }
83
+
84
+ const plugin = getPluginRuntime()[params.nodeSpec.owner.ref]
85
+ const nodeExecutor = plugin?.nodeExecutor
86
+ if (!plugin || !nodeExecutor || !nodeExecutor.supportedOperations.includes(params.nodeSpec.owner.operation)) {
87
+ throw new Error(
88
+ `Plugin executor ${params.nodeSpec.owner.ref}.${params.nodeSpec.owner.operation} is not registered.`,
89
+ )
90
+ }
91
+
92
+ return nodeExecutor.executeNode(
93
+ buildPluginExecutionParams({
94
+ owner: params.nodeSpec.owner,
95
+ nodeSpec: params.nodeSpec,
96
+ resolvedInput: params.resolvedInput,
97
+ context: params.context,
98
+ }),
99
+ )
100
+ }
101
+ }
102
+
103
+ export const pluginExecutorService = new PluginExecutorService()
@@ -0,0 +1,132 @@
1
+ import { NodeQualityMetricsSchema } from '@lota-sdk/shared'
2
+ import type { NodeQualityMetrics } from '@lota-sdk/shared'
3
+ import { z } from 'zod'
4
+
5
+ import { ensureRecordId } from '../db/record-id'
6
+ import { databaseService } from '../db/service'
7
+ import { TABLES } from '../db/tables'
8
+
9
+ const QualityMetricRowSchema = NodeQualityMetricsSchema.extend({
10
+ id: z.unknown(),
11
+ organizationId: z.unknown(),
12
+ runId: z.unknown(),
13
+ nodeId: z.string().optional(),
14
+ createdAt: z.coerce.date().optional(),
15
+ updatedAt: z.coerce.date().optional(),
16
+ })
17
+
18
+ class QualityMetricsService {
19
+ async recordNodeMetrics(params: {
20
+ organizationId: string
21
+ runId: string
22
+ nodeId: string
23
+ metrics: NodeQualityMetrics
24
+ }): Promise<void> {
25
+ await databaseService.create(
26
+ TABLES.QUALITY_METRIC,
27
+ {
28
+ organizationId: ensureRecordId(params.organizationId, TABLES.ORGANIZATION),
29
+ runId: ensureRecordId(params.runId, TABLES.PLAN_RUN),
30
+ nodeId: params.nodeId,
31
+ ownerRef: params.metrics.ownerRef,
32
+ ownerType: params.metrics.ownerType,
33
+ nodeType: params.metrics.nodeType,
34
+ executionTimeMs: params.metrics.executionTimeMs,
35
+ attemptCount: params.metrics.attemptCount,
36
+ artifactCount: params.metrics.artifactCount,
37
+ validationIssueCount: params.metrics.validationIssueCount,
38
+ },
39
+ QualityMetricRowSchema,
40
+ )
41
+ }
42
+
43
+ async recordCycleMetrics(params: { organizationId: string; runId: string }): Promise<void> {
44
+ const existing = await databaseService.findMany(
45
+ TABLES.QUALITY_METRIC,
46
+ { runId: ensureRecordId(params.runId, TABLES.PLAN_RUN) },
47
+ QualityMetricRowSchema,
48
+ { orderBy: 'createdAt', orderDir: 'ASC' },
49
+ )
50
+
51
+ if (!Array.isArray(existing) || existing.length === 0) return
52
+
53
+ const totalExecutionTime = existing.reduce(
54
+ (sum, m) => sum + (typeof m.executionTimeMs === 'number' ? m.executionTimeMs : 0),
55
+ 0,
56
+ )
57
+ const totalAttempts = existing.reduce(
58
+ (sum, m) => sum + (typeof m.attemptCount === 'number' ? m.attemptCount : 0),
59
+ 0,
60
+ )
61
+ const totalIssues = existing.reduce(
62
+ (sum, m) => sum + (typeof m.validationIssueCount === 'number' ? m.validationIssueCount : 0),
63
+ 0,
64
+ )
65
+
66
+ await databaseService.create(
67
+ TABLES.QUALITY_METRIC,
68
+ {
69
+ organizationId: ensureRecordId(params.organizationId, TABLES.ORGANIZATION),
70
+ runId: ensureRecordId(params.runId, TABLES.PLAN_RUN),
71
+ ownerRef: 'cycle-summary',
72
+ ownerType: 'system',
73
+ nodeType: 'action',
74
+ executionTimeMs: totalExecutionTime,
75
+ attemptCount: totalAttempts,
76
+ artifactCount: existing.length,
77
+ validationIssueCount: totalIssues,
78
+ },
79
+ QualityMetricRowSchema,
80
+ )
81
+ }
82
+
83
+ detectRegression(params: {
84
+ metrics: Array<{ executionTimeMs: number; validationIssueCount: number }>
85
+ window?: number
86
+ }): { detected: boolean; trend?: 'improving' | 'stable' | 'regressing' } {
87
+ const window = params.window ?? 5
88
+ const recent = params.metrics.slice(-window)
89
+ if (recent.length < 2) return { detected: false, trend: 'stable' }
90
+
91
+ const mid = Math.floor(recent.length / 2)
92
+ const firstHalf = recent.slice(0, mid)
93
+ const secondHalf = recent.slice(mid)
94
+
95
+ const avgFirst = firstHalf.reduce((sum, m) => sum + m.validationIssueCount, 0) / firstHalf.length
96
+ const avgSecond = secondHalf.reduce((sum, m) => sum + m.validationIssueCount, 0) / secondHalf.length
97
+
98
+ const timeFirst = firstHalf.reduce((sum, m) => sum + m.executionTimeMs, 0) / firstHalf.length
99
+ const timeSecond = secondHalf.reduce((sum, m) => sum + m.executionTimeMs, 0) / secondHalf.length
100
+
101
+ const issueRegression = avgFirst > 0 && avgSecond > avgFirst * 1.5
102
+ const timeRegression = timeFirst > 0 && timeSecond > timeFirst * 2
103
+
104
+ if (issueRegression || timeRegression) {
105
+ return { detected: true, trend: 'regressing' }
106
+ }
107
+
108
+ const issueImproving = avgFirst > 0 && avgSecond < avgFirst * 0.7
109
+ const timeImproving = timeFirst > 0 && timeSecond < timeFirst * 0.7
110
+
111
+ if (issueImproving || timeImproving) {
112
+ return { detected: false, trend: 'improving' }
113
+ }
114
+
115
+ return { detected: false, trend: 'stable' }
116
+ }
117
+
118
+ async getMetricsByOwner(params: {
119
+ organizationId: string
120
+ ownerRef: string
121
+ limit?: number
122
+ }): Promise<z.infer<typeof QualityMetricRowSchema>[]> {
123
+ return databaseService.findMany(
124
+ TABLES.QUALITY_METRIC,
125
+ { organizationId: ensureRecordId(params.organizationId, TABLES.ORGANIZATION), ownerRef: params.ownerRef },
126
+ QualityMetricRowSchema,
127
+ { orderBy: 'createdAt', orderDir: 'DESC', limit: params.limit ?? 20 },
128
+ )
129
+ }
130
+ }
131
+
132
+ export const qualityMetricsService = new QualityMetricsService()
@@ -1,5 +1,3 @@
1
- import { createHash } from 'node:crypto'
2
-
3
1
  import {
4
2
  RecentActivityDeepLinkSchema,
5
3
  RecentActivityEventInputSchema,
@@ -8,6 +6,7 @@ import {
8
6
  RecentActivitySchema,
9
7
  RecentActivitySourceSchema,
10
8
  RecentActivityTitleSourceSchema,
9
+ recordIdSchema,
11
10
  } from '@lota-sdk/shared'
12
11
  import type {
13
12
  RecentActivity,
@@ -23,12 +22,12 @@ import type { RecordIdInput, RecordIdRef } from '../db/record-id'
23
22
  import { databaseService } from '../db/service'
24
23
  import { TABLES } from '../db/tables'
25
24
  import { toIsoDateTimeString, toOptionalIsoDateTimeString } from '../utils/date-time'
26
- import { compactWhitespace } from '../utils/string'
25
+ import { compactRecord, compactWhitespace, truncateText } from '../utils/string'
27
26
 
28
27
  const RecentActivityEventRowSchema = z.object({
29
- id: z.unknown(),
30
- organizationId: z.unknown(),
31
- userId: z.unknown(),
28
+ id: recordIdSchema,
29
+ organizationId: recordIdSchema,
30
+ userId: recordIdSchema,
32
31
  sourceEventId: z.string(),
33
32
  source: RecentActivitySourceSchema,
34
33
  kind: RecentActivityEventInputSchema.shape.kind,
@@ -39,14 +38,14 @@ const RecentActivityEventRowSchema = z.object({
39
38
  sourceLabel: z.string(),
40
39
  deepLink: RecentActivityDeepLinkSchema,
41
40
  metadata: RecentActivityMetadataSchema.optional(),
42
- occurredAt: z.union([z.string(), z.date(), z.number()]),
43
- createdAt: z.union([z.string(), z.date(), z.number()]),
41
+ occurredAt: z.coerce.date(),
42
+ createdAt: z.coerce.date(),
44
43
  })
45
44
 
46
45
  const RecentActivityRowSchema = z.object({
47
- id: z.unknown(),
48
- organizationId: z.unknown(),
49
- userId: z.unknown(),
46
+ id: recordIdSchema,
47
+ organizationId: recordIdSchema,
48
+ userId: recordIdSchema,
50
49
  mergeKey: z.string(),
51
50
  kind: RecentActivityEventInputSchema.shape.kind,
52
51
  targetKind: RecentActivityEventInputSchema.shape.targetKind,
@@ -57,29 +56,19 @@ const RecentActivityRowSchema = z.object({
57
56
  sourceLabel: z.string(),
58
57
  deepLink: RecentActivityDeepLinkSchema,
59
58
  metadata: RecentActivityMetadataSchema.optional(),
60
- latestEventId: z.unknown().optional(),
59
+ latestEventId: recordIdSchema.optional(),
61
60
  latestSourceEventId: z.string().optional(),
62
- latestEventAt: z.union([z.string(), z.date(), z.number()]),
63
- titleRefinedAt: z.union([z.string(), z.date(), z.number()]).optional(),
64
- createdAt: z.union([z.string(), z.date(), z.number()]),
65
- updatedAt: z.union([z.string(), z.date(), z.number()]),
61
+ latestEventAt: z.coerce.date(),
62
+ titleRefinedAt: z.coerce.date().optional(),
63
+ createdAt: z.coerce.date(),
64
+ updatedAt: z.coerce.date(),
66
65
  })
67
66
 
68
67
  type RecentActivityEventRow = z.infer<typeof RecentActivityEventRowSchema>
69
68
  type RecentActivityRow = z.infer<typeof RecentActivityRowSchema>
70
69
 
71
- function compactRecord(value: Record<string, unknown>): Record<string, unknown> {
72
- return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== null && entry !== undefined))
73
- }
74
-
75
- function clampText(value: string, maxLength: number): string {
76
- const normalized = compactWhitespace(value)
77
- if (normalized.length <= maxLength) return normalized
78
- return normalized.slice(0, maxLength).trim()
79
- }
80
-
81
70
  function buildDeterministicRecordId(table: string, key: string): RecordId {
82
- const digest = createHash('sha256').update(key).digest('hex')
71
+ const digest = new Bun.CryptoHasher('sha256').update(key).digest('hex')
83
72
  return new RecordId(table, digest)
84
73
  }
85
74
 
@@ -90,6 +79,11 @@ function shouldKeepExistingAgentTitle(existing: RecentActivityRow | null): boole
90
79
  function buildRecentActivityAreaKey(
91
80
  row: Pick<RecentActivityRow, 'targetKind' | 'targetId' | 'kind' | 'mergeKey' | 'metadata'>,
92
81
  ): string {
82
+ const workstreamId = row.metadata?.workstreamId
83
+ if (workstreamId) {
84
+ return `workstream:${compactWhitespace(workstreamId)}`
85
+ }
86
+
93
87
  if (row.targetId) {
94
88
  return `${row.targetKind}:${row.targetId}`
95
89
  }
@@ -149,11 +143,11 @@ class RecentActivityService {
149
143
  private sanitizeEvent(input: RecentActivityEventInput): RecentActivityEventInput {
150
144
  return {
151
145
  ...input,
152
- sourceEventId: clampText(input.sourceEventId, 200),
153
- ...(input.targetId ? { targetId: clampText(input.targetId, 200) } : {}),
154
- mergeKey: clampText(input.mergeKey, 200),
155
- title: clampText(input.title, 140),
156
- sourceLabel: clampText(input.sourceLabel, 40),
146
+ sourceEventId: truncateText(input.sourceEventId, 200),
147
+ ...(input.targetId ? { targetId: truncateText(input.targetId, 200) } : {}),
148
+ mergeKey: truncateText(input.mergeKey, 200),
149
+ title: truncateText(input.title, 140),
150
+ sourceLabel: truncateText(input.sourceLabel, 40),
157
151
  ...(input.metadata ? { metadata: RecentActivityMetadataSchema.parse(input.metadata) } : {}),
158
152
  }
159
153
  }
@@ -309,7 +303,7 @@ class RecentActivityService {
309
303
  const existing = await databaseService.findOne(TABLES.RECENT_ACTIVITY, { id: activityRef }, RecentActivityRowSchema)
310
304
  if (!existing) return null
311
305
 
312
- const nextTitle = clampText(params.title, 80)
306
+ const nextTitle = truncateText(params.title, 80)
313
307
  if (!nextTitle) return this.toPublicItem(existing)
314
308
  if (compactWhitespace(nextTitle).toLowerCase() === compactWhitespace(existing.title).toLowerCase()) {
315
309
  return this.toPublicItem(existing)
@@ -347,7 +341,7 @@ class RecentActivityService {
347
341
  systemTitle: row.systemTitle,
348
342
  sourceLabel: row.sourceLabel,
349
343
  kind: row.kind,
350
- metadata: RecentActivityMetadataSchema.parse(row.metadata ?? {}),
344
+ metadata: RecentActivityMetadataSchema.parse(row.metadata),
351
345
  }
352
346
  }
353
347
 
@@ -0,0 +1,19 @@
1
+ class SkillResolverService {
2
+ async resolve(params: {
3
+ skillRef: string
4
+ organizationId: string
5
+ }): Promise<{ executorType: 'agent' | 'plugin'; ref: string; operation?: string } | null> {
6
+ // Dynamic import to avoid circular dependencies and enable testability
7
+ const { learnedSkillService } = await import('./learned-skill.service')
8
+ const skill = await learnedSkillService.findByNameOrTag(params.organizationId, params.skillRef)
9
+ if (!skill) return null
10
+
11
+ if (skill.agentId) {
12
+ return { executorType: 'agent', ref: skill.agentId }
13
+ }
14
+
15
+ return null
16
+ }
17
+ }
18
+
19
+ export const skillResolverService = new SkillResolverService()
@@ -0,0 +1,197 @@
1
+ import { z } from 'zod'
2
+
3
+ import { getRedisConnection } from '../redis'
4
+ import type { LotaRuntimeBackgroundCursor, LotaRuntimeBackgroundCursorKind } from '../runtime/runtime-extensions'
5
+
6
+ const DEFAULT_SOCIAL_CHAT_HISTORY_PREFIX = 'lota:social:history'
7
+
8
+ const SocialChatMessageRoleSchema = z.enum(['user', 'assistant'])
9
+ const SocialChatSourceSchema = z.literal('social')
10
+ const SocialChatPlatformSchema = z.literal('slack')
11
+
12
+ const SocialChatHistoryMessageSchema = z.object({
13
+ source: SocialChatSourceSchema,
14
+ sourceId: z.string().trim().min(1),
15
+ platform: SocialChatPlatformSchema,
16
+ workspaceId: z.string().trim().min(1),
17
+ channelId: z.string().trim().min(1),
18
+ threadId: z.string().trim().min(1),
19
+ messageId: z.string().trim().min(1),
20
+ role: SocialChatMessageRoleSchema,
21
+ parts: z.array(z.record(z.string(), z.unknown())),
22
+ metadata: z.record(z.string(), z.unknown()).optional(),
23
+ cursor: z.object({ createdAt: z.coerce.date(), id: z.string().trim().min(1) }),
24
+ })
25
+
26
+ export type SocialChatHistoryMessage = z.infer<typeof SocialChatHistoryMessageSchema>
27
+
28
+ let socialChatHistoryPrefix = DEFAULT_SOCIAL_CHAT_HISTORY_PREFIX
29
+
30
+ function trimConfiguredPrefix(value: string | undefined): string {
31
+ const normalized = value?.trim()
32
+ return normalized && normalized.length > 0 ? normalized : DEFAULT_SOCIAL_CHAT_HISTORY_PREFIX
33
+ }
34
+
35
+ function compareCursorOrder(left: LotaRuntimeBackgroundCursor, right: LotaRuntimeBackgroundCursor): number {
36
+ const timeDiff = left.createdAt.getTime() - right.createdAt.getTime()
37
+ if (timeDiff !== 0) return timeDiff
38
+ return left.id.localeCompare(right.id)
39
+ }
40
+
41
+ class SocialChatHistoryService {
42
+ configure(params?: { keyPrefix?: string }): void {
43
+ socialChatHistoryPrefix = trimConfiguredPrefix(params?.keyPrefix)
44
+ }
45
+
46
+ private messageStorageKey(message: {
47
+ platform: 'slack'
48
+ workspaceId: string
49
+ threadId: string
50
+ messageId: string
51
+ }): string {
52
+ return `${socialChatHistoryPrefix}:message:${message.platform}:${message.workspaceId}:${message.threadId}:${message.messageId}`
53
+ }
54
+
55
+ private threadIndexKey(workspaceId: string, threadId: string): string {
56
+ return `${socialChatHistoryPrefix}:thread:${workspaceId}:${threadId}`
57
+ }
58
+
59
+ private workspaceIndexKey(workspaceId: string): string {
60
+ return `${socialChatHistoryPrefix}:workspace:${workspaceId}`
61
+ }
62
+
63
+ private cursorKey(kind: LotaRuntimeBackgroundCursorKind, workspaceId: string): string {
64
+ return `${socialChatHistoryPrefix}:cursor:${kind}:${workspaceId}`
65
+ }
66
+
67
+ private serializeMessage(message: SocialChatHistoryMessage): string {
68
+ return JSON.stringify({
69
+ ...message,
70
+ cursor: { ...message.cursor, createdAt: message.cursor.createdAt.toISOString() },
71
+ })
72
+ }
73
+
74
+ private parseStoredMessage(value: string | null): SocialChatHistoryMessage | null {
75
+ if (!value) return null
76
+ try {
77
+ const parsed = SocialChatHistoryMessageSchema.safeParse(JSON.parse(value))
78
+ return parsed.success ? parsed.data : null
79
+ } catch {
80
+ return null
81
+ }
82
+ }
83
+
84
+ private serializeCursor(cursor: LotaRuntimeBackgroundCursor): string {
85
+ return JSON.stringify({ ...cursor, createdAt: cursor.createdAt.toISOString() })
86
+ }
87
+
88
+ private parseCursor(value: string | null): LotaRuntimeBackgroundCursor | null {
89
+ if (!value) return null
90
+ try {
91
+ const parsed = z.object({ createdAt: z.coerce.date(), id: z.string().trim().min(1) }).safeParse(JSON.parse(value))
92
+ return parsed.success ? parsed.data : null
93
+ } catch {
94
+ return null
95
+ }
96
+ }
97
+
98
+ async upsertMessages(messages: SocialChatHistoryMessage[]): Promise<SocialChatHistoryMessage[]> {
99
+ if (messages.length === 0) return []
100
+
101
+ const redis = getRedisConnection()
102
+ const normalizedMessages = messages.map((message) => SocialChatHistoryMessageSchema.parse(message))
103
+ const multi = redis.multi()
104
+
105
+ for (const message of normalizedMessages) {
106
+ const storageKey = this.messageStorageKey(message)
107
+ const score = message.cursor.createdAt.getTime()
108
+ multi.set(storageKey, this.serializeMessage(message))
109
+ multi.zadd(this.threadIndexKey(message.workspaceId, message.threadId), score, storageKey)
110
+ multi.zadd(this.workspaceIndexKey(message.workspaceId), score, storageKey)
111
+ }
112
+
113
+ await multi.exec()
114
+ return normalizedMessages
115
+ }
116
+
117
+ async listThreadMessages(params: { workspaceId: string; threadId: string }): Promise<SocialChatHistoryMessage[]> {
118
+ const redis = getRedisConnection()
119
+ const storageKeys = await redis.zrange(this.threadIndexKey(params.workspaceId, params.threadId), 0, -1)
120
+ if (storageKeys.length === 0) return []
121
+
122
+ const storedValues = await redis.mget(storageKeys)
123
+ return storedValues
124
+ .map((value) => this.parseStoredMessage(value))
125
+ .filter((message): message is SocialChatHistoryMessage => message !== null)
126
+ .sort((left, right) => compareCursorOrder(left.cursor, right.cursor))
127
+ }
128
+
129
+ async listWorkspaceMessages(params: {
130
+ workspaceId: string
131
+ cursor: LotaRuntimeBackgroundCursor | null
132
+ onboardingCutoff: Date | null
133
+ }): Promise<SocialChatHistoryMessage[]> {
134
+ const redis = getRedisConnection()
135
+ const indexKey = this.workspaceIndexKey(params.workspaceId)
136
+ const scoreStart =
137
+ params.cursor?.createdAt.getTime() ??
138
+ (params.onboardingCutoff ? params.onboardingCutoff.getTime() : Number.NEGATIVE_INFINITY)
139
+ const storageKeys =
140
+ params.cursor || params.onboardingCutoff
141
+ ? await redis.zrangebyscore(indexKey, scoreStart, '+inf')
142
+ : await redis.zrange(indexKey, 0, -1)
143
+
144
+ if (storageKeys.length === 0) return []
145
+
146
+ const storedValues = await redis.mget(storageKeys)
147
+ return storedValues
148
+ .map((value) => this.parseStoredMessage(value))
149
+ .filter((message): message is SocialChatHistoryMessage => message !== null)
150
+ .filter((message) => {
151
+ if (params.cursor) {
152
+ return compareCursorOrder(message.cursor, params.cursor) > 0
153
+ }
154
+ if (params.onboardingCutoff) {
155
+ return message.cursor.createdAt.getTime() > params.onboardingCutoff.getTime()
156
+ }
157
+ return true
158
+ })
159
+ .sort((left, right) => compareCursorOrder(left.cursor, right.cursor))
160
+ }
161
+
162
+ async hasWorkspaceMessages(params: {
163
+ workspaceId: string
164
+ cursor: LotaRuntimeBackgroundCursor | null
165
+ onboardingCutoff: Date | null
166
+ }): Promise<boolean> {
167
+ const messages = await this.listWorkspaceMessages({
168
+ workspaceId: params.workspaceId,
169
+ cursor: params.cursor,
170
+ onboardingCutoff: params.onboardingCutoff,
171
+ })
172
+ return messages.length > 0
173
+ }
174
+
175
+ async getBackgroundCursor(
176
+ kind: LotaRuntimeBackgroundCursorKind,
177
+ workspaceId: string,
178
+ ): Promise<LotaRuntimeBackgroundCursor | null> {
179
+ const redis = getRedisConnection()
180
+ return this.parseCursor(await redis.get(this.cursorKey(kind, workspaceId)))
181
+ }
182
+
183
+ async setBackgroundCursor(
184
+ kind: LotaRuntimeBackgroundCursorKind,
185
+ workspaceId: string,
186
+ cursor: LotaRuntimeBackgroundCursor,
187
+ ): Promise<void> {
188
+ const redis = getRedisConnection()
189
+ await redis.set(this.cursorKey(kind, workspaceId), this.serializeCursor(cursor))
190
+ }
191
+ }
192
+
193
+ export const socialChatHistoryService = new SocialChatHistoryService()
194
+
195
+ export function configureSocialChatHistory(params?: { keyPrefix?: string }): void {
196
+ socialChatHistoryService.configure(params)
197
+ }