@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
@@ -26,8 +26,7 @@ import {
26
26
  import { getRuntimeConfig } from '../runtime/runtime-config'
27
27
  import { createMemoryRerankerAgent, MEMORY_RERANKER_PROMPT } from '../system-agents/memory-reranker.agent'
28
28
  import { createOrgMemoryAgent, ORG_MEMORY_PROMPT } from '../system-agents/memory.agent'
29
- import { toError } from '../utils/errors'
30
- import { compactWhitespace } from '../utils/string'
29
+ import { compactWhitespace, truncateText } from '../utils/string'
31
30
  import { assessMemoryImportance, clampMemoryImportance } from './memory-assessment.service'
32
31
  import { formatMemoryResults, formatRerankedResults, getCandidateLimit } from './memory-utils'
33
32
 
@@ -44,6 +43,7 @@ const MAX_CONVERSATION_MEMORY_BLOCK_CHARS = 2_000
44
43
  const MAX_CONVERSATION_ATTACHMENT_CONTEXT_CHARS = 6_000
45
44
  const MAX_CONVERSATION_ASSESSMENT_CHARS = 7_000
46
45
  const ONBOARDING_MEMORY_MAX_FACTS = 16
46
+ const MAX_ORG_MEMORY_CLIENTS = 128
47
47
  const ONBOARDING_MEMORY_EXTRACTION_PROMPT =
48
48
  'Onboarding mode is active. Extract multiple concrete startup facts from user-provided context: company mission, product capabilities, customer segments, pricing, traction, go-to-market plans, roadmap, team composition, technical stack, risks, and referenced URLs. Prefer one fact per concrete claim.'
49
49
 
@@ -65,17 +65,29 @@ const MemoryRerankOutputSchema = z.object({
65
65
 
66
66
  export type MemoryRerankOutput = z.infer<typeof MemoryRerankOutputSchema>
67
67
 
68
- const isRoutableAgentName = (value?: string): boolean => Boolean(value && agentRoster.includes(value))
68
+ const isRoutableAgentName = (value?: string): value is string => Boolean(value && agentRoster.includes(value))
69
69
 
70
70
  class MemoryService {
71
71
  private orgMemoryCache = new Map<string, Memory>()
72
72
 
73
73
  private getOrCreateMemory(cacheKey: string, cache: Map<string, Memory>): Memory {
74
74
  const cached = cache.get(cacheKey)
75
- if (cached) return cached
75
+ if (cached) {
76
+ cache.delete(cacheKey)
77
+ cache.set(cacheKey, cached)
78
+ return cached
79
+ }
76
80
 
77
81
  const memory = new Memory({ createAgent: createOrgMemoryAgent }, { customPrompt: ORG_MEMORY_PROMPT })
78
82
 
83
+ if (cache.size >= MAX_ORG_MEMORY_CLIENTS) {
84
+ const oldestKey = cache.keys().next().value
85
+ if (typeof oldestKey === 'string') {
86
+ cache.delete(oldestKey)
87
+ aiLogger.debug`Evicted cached org memory client for ${oldestKey}`
88
+ }
89
+ }
90
+
79
91
  cache.set(cacheKey, memory)
80
92
  aiLogger.debug`Memory client created and cached for ${cacheKey}`
81
93
  return memory
@@ -87,9 +99,7 @@ class MemoryService {
87
99
  }
88
100
 
89
101
  private truncateCandidateText(value: string): string {
90
- const normalized = compactWhitespace(value)
91
- if (normalized.length <= RERANK_CANDIDATE_MAX_CHARS) return normalized
92
- return `${normalized.slice(0, RERANK_CANDIDATE_MAX_CHARS)}...`
102
+ return truncateText(compactWhitespace(value), RERANK_CANDIDATE_MAX_CHARS)
93
103
  }
94
104
 
95
105
  private readNumericMetadata(metadata: Record<string, unknown>, key: string): number {
@@ -129,8 +139,7 @@ class MemoryService {
129
139
  private normalizeConversationText(value: string, maxChars: number): string {
130
140
  const normalized = compactWhitespace(value)
131
141
  if (!normalized) return ''
132
- if (normalized.length <= maxChars) return normalized
133
- return `${normalized.slice(0, maxChars - 3)}...`
142
+ return truncateText(normalized, maxChars)
134
143
  }
135
144
 
136
145
  private resolveAgentScopeNames(agentName?: string, agentNames: string[] = []): string[] {
@@ -148,9 +157,7 @@ class MemoryService {
148
157
  }
149
158
 
150
159
  private normalizePreSeededMemoryText(value: string): string {
151
- const normalized = compactWhitespace(value)
152
- if (normalized.length <= PRESEEDED_MEMORY_MAX_CHARS) return normalized
153
- return `${normalized.slice(0, PRESEEDED_MEMORY_MAX_CHARS - 3)}...`
160
+ return truncateText(compactWhitespace(value), PRESEEDED_MEMORY_MAX_CHARS)
154
161
  }
155
162
 
156
163
  private formatPreSeededMemoriesSection(memories: MemoryRecord[]): string | undefined {
@@ -331,15 +338,9 @@ class MemoryService {
331
338
  aiLogger.debug`Organization memory search requested (orgId: ${orgId}, scopeId: ${orgScopeId}, queryLength: ${query.length})`
332
339
  const memory = this.getOrgMemory(orgId)
333
340
 
334
- try {
335
- const results = await this.searchMemories({ query, memory, scopeId: orgScopeId, memoryType: ORG_MEMORY_TYPE })
336
- aiLogger.debug`Organization memory search completed (resultLength: ${results.length}, preview: ${results.slice(0, 100)})`
337
- return results
338
- } catch (error: unknown) {
339
- const normalizedError = toError(error)
340
- aiLogger.error`Organization memory search failed: ${normalizedError}`
341
- throw normalizedError
342
- }
341
+ const results = await this.searchMemories({ query, memory, scopeId: orgScopeId, memoryType: ORG_MEMORY_TYPE })
342
+ aiLogger.debug`Organization memory search completed (resultLength: ${results.length}, preview: ${results.slice(0, 100)})`
343
+ return results
343
344
  }
344
345
 
345
346
  async getStaleMemories(orgId: string): Promise<string> {
@@ -368,21 +369,15 @@ class MemoryService {
368
369
  const searchK = getRuntimeConfig().memory.searchK
369
370
  const limit = options?.limit ?? (fastMode ? Math.min(searchK, 4) : searchK)
370
371
 
371
- try {
372
- const candidates = await memory.searchCandidates(query, {
373
- scopeId: orgScopeId,
374
- limit,
375
- memoryType: ORG_MEMORY_TYPE,
376
- fastMode,
377
- includeNeighborContext: !fastMode,
378
- })
379
- aiLogger.debug`Organization memory search (raw) completed (candidates: ${candidates.length})`
380
- return formatMemoryResults(candidates)
381
- } catch (error: unknown) {
382
- const normalizedError = toError(error)
383
- aiLogger.error`Organization memory search (raw) failed: ${normalizedError}`
384
- throw normalizedError
385
- }
372
+ const candidates = await memory.searchCandidates(query, {
373
+ scopeId: orgScopeId,
374
+ limit,
375
+ memoryType: ORG_MEMORY_TYPE,
376
+ fastMode,
377
+ includeNeighborContext: !fastMode,
378
+ })
379
+ aiLogger.debug`Organization memory search (raw) completed (candidates: ${candidates.length})`
380
+ return formatMemoryResults(candidates)
386
381
  }
387
382
 
388
383
  async searchAgentMemories(orgId: string, agentName: string, query: string): Promise<string> {
@@ -395,20 +390,14 @@ class MemoryService {
395
390
  aiLogger.debug`Agent memory search requested (orgId: ${orgId}, agentName: ${agentName}, scopeId: ${scoped}, queryLength: ${query.length})`
396
391
  const memory = this.getOrgMemory(orgId)
397
392
 
398
- try {
399
- const results = await this.searchMemories({ query, memory, scopeId: scoped, memoryType: ORG_MEMORY_TYPE })
400
- aiLogger.debug`Agent memory search completed (agentName: ${agentName}, resultLength: ${results.length}, preview: ${results.slice(0, 100)})`
401
- return results
402
- } catch (error: unknown) {
403
- const normalizedError = toError(error)
404
- aiLogger.error`Agent memory search failed: ${normalizedError}`
405
- throw normalizedError
406
- }
393
+ const results = await this.searchMemories({ query, memory, scopeId: scoped, memoryType: ORG_MEMORY_TYPE })
394
+ aiLogger.debug`Agent memory search completed (agentName: ${agentName}, resultLength: ${results.length}, preview: ${results.slice(0, 100)})`
395
+ return results
407
396
  }
408
397
 
409
398
  async searchOrgMemoriesForAgent(orgId: string, agentName: string, query: string): Promise<string> {
410
399
  if (!isRoutableAgentName(agentName)) {
411
- return await this.searchOrganizationMemories(orgId, query)
400
+ return this.searchOrganizationMemories(orgId, query)
412
401
  }
413
402
 
414
403
  const [agentResult, orgResult] = await Promise.all([
@@ -429,7 +418,7 @@ class MemoryService {
429
418
  }): Promise<MemoryRecord[]> {
430
419
  const { orgId, ...listOptions } = params
431
420
  const orgMemory = this.getOrgMemory(orgId)
432
- return await orgMemory.list({ scopeId: scopeId(ORG_SCOPE_PREFIX, orgId), ...listOptions })
421
+ return orgMemory.list({ scopeId: scopeId(ORG_SCOPE_PREFIX, orgId), ...listOptions })
433
422
  }
434
423
 
435
424
  async getTopMemories(params: { orgId: string; agentName?: string; limit?: number }): Promise<string | undefined> {
@@ -513,7 +502,7 @@ class MemoryService {
513
502
  ]
514
503
 
515
504
  if (isRoutableAgentName(agentName)) {
516
- const agentScoped = agentScopeId(orgId, agentName as string)
505
+ const agentScoped = agentScopeId(orgId, agentName)
517
506
  retrievalTasks.push({
518
507
  scopeTag: `agent:${agentName}`,
519
508
  retrieve: async () =>
@@ -649,7 +638,7 @@ class MemoryService {
649
638
  importance?: number
650
639
  }): Promise<string> {
651
640
  if (!isRoutableAgentName(agentName)) {
652
- throw new Error(`Invalid agentName for agent memory: ${agentName}`)
641
+ throw new Error(`Invalid agentName for agent memory: ${agentName as string}`)
653
642
  }
654
643
 
655
644
  const memory = this.getOrgMemory(orgId)
@@ -733,6 +722,8 @@ class MemoryService {
733
722
  input,
734
723
  output,
735
724
  sourceId,
725
+ source = 'chat',
726
+ sourceMetadata,
736
727
  onboardStatus,
737
728
  agentName,
738
729
  historyMessages = [],
@@ -744,6 +735,8 @@ class MemoryService {
744
735
  input: string
745
736
  output: string
746
737
  sourceId?: string
738
+ source?: string
739
+ sourceMetadata?: Record<string, unknown>
747
740
  onboardStatus?: string
748
741
  agentName?: string
749
742
  historyMessages?: Array<{ role: 'user' | 'agent'; content: string; agentName?: string }>
@@ -800,7 +793,7 @@ class MemoryService {
800
793
  scopeId: orgScopeId,
801
794
  memoryType: ORG_MEMORY_TYPE,
802
795
  ...(typeof assessedImportance === 'number' ? { importance: assessedImportance } : {}),
803
- metadata: { orgId, source: 'chat', ...(sourceId ? { sourceId } : {}), ...assessmentMetadata },
796
+ metadata: { orgId, source, ...(sourceId ? { sourceId } : {}), ...sourceMetadata, ...assessmentMetadata },
804
797
  },
805
798
  ]
806
799
 
@@ -814,8 +807,9 @@ class MemoryService {
814
807
  orgId,
815
808
  agentName: scopedAgentName,
816
809
  memoryScope: 'agent',
817
- source: 'chat',
810
+ source,
818
811
  ...(sourceId ? { sourceId } : {}),
812
+ ...sourceMetadata,
819
813
  ...assessmentMetadata,
820
814
  },
821
815
  })
@@ -830,16 +824,10 @@ class MemoryService {
830
824
  const preparedUpdates = await orgMemory.prepareFactsToScopes(extractedFacts, scopes)
831
825
  if (preparedUpdates.length === 0) return
832
826
 
833
- try {
834
- await withOrgMemoryLock(orgId, async () => {
835
- await orgMemory.applyPreparedScopeUpdates(preparedUpdates)
836
- })
837
- aiLogger.debug`Conversation memories added to ${scopes.length} scope(s) from ${messages.length} message(s)`
838
- } catch (error: unknown) {
839
- const normalizedError = toError(error)
840
- aiLogger.error`Memory write failed: ${normalizedError}`
841
- throw normalizedError
842
- }
827
+ await withOrgMemoryLock(orgId, async () => {
828
+ await orgMemory.applyPreparedScopeUpdates(preparedUpdates)
829
+ })
830
+ aiLogger.debug`Conversation memories added to ${scopes.length} scope(s) from ${messages.length} message(s)`
843
831
  }
844
832
  }
845
833
 
@@ -0,0 +1,86 @@
1
+ import type { MonitoringWindowConfig } from '@lota-sdk/shared'
2
+
3
+ import { planSchedulerService } from './plan-scheduler.service'
4
+
5
+ class MonitoringWindowService {
6
+ computeCheckTimes(config: MonitoringWindowConfig): { offsetMs: number; intervalMs: number }[] {
7
+ const times: { offsetMs: number; intervalMs: number }[] = []
8
+ let currentInterval = config.initialIntervalMs
9
+ let elapsed = 0
10
+ while (elapsed < config.durationMs) {
11
+ times.push({ offsetMs: elapsed, intervalMs: currentInterval })
12
+ elapsed += currentInterval
13
+ currentInterval = Math.min(currentInterval / config.decayFactor, config.maxIntervalMs)
14
+ }
15
+ return times
16
+ }
17
+
18
+ async startMonitoringWindow(params: {
19
+ runId: string
20
+ nodeId: string
21
+ config: MonitoringWindowConfig
22
+ organizationId: string
23
+ workstreamId: string
24
+ }): Promise<void> {
25
+ await planSchedulerService.createSchedule({
26
+ organizationId: params.organizationId,
27
+ workstreamId: params.workstreamId,
28
+ runId: params.runId,
29
+ nodeId: params.nodeId,
30
+ scheduleSpec: {
31
+ type: 'monitoring',
32
+ intervalMs: params.config.initialIntervalMs,
33
+ maxFires: this.computeCheckTimes(params.config).length,
34
+ missedPolicy: 'skip',
35
+ },
36
+ })
37
+ }
38
+
39
+ evaluateTermination(params: { samples: unknown[]; config: MonitoringWindowConfig }): boolean {
40
+ if (!params.config.earlyTermination) return false
41
+ if (params.samples.length < params.config.earlyTermination.minSamples) return false
42
+
43
+ const { condition, minSamples } = params.config.earlyTermination
44
+ const numericSamples = params.samples
45
+ .map((s) =>
46
+ typeof s === 'number'
47
+ ? s
48
+ : typeof s === 'object' && s !== null && 'value' in s
49
+ ? Number((s as Record<string, unknown>).value)
50
+ : NaN,
51
+ )
52
+ .filter((n) => !isNaN(n))
53
+
54
+ // No numeric data to evaluate against — don't terminate
55
+ if (numericSamples.length === 0) return false
56
+
57
+ if (condition === 'stable') {
58
+ const recent = numericSamples.slice(-minSamples)
59
+ if (recent.length < 2) return false
60
+ const mean = recent.reduce((a, b) => a + b, 0) / recent.length
61
+ const variance = recent.reduce((a, b) => a + (b - mean) ** 2, 0) / recent.length
62
+ const cv = mean !== 0 ? Math.sqrt(variance) / Math.abs(mean) : 0
63
+ return cv < 0.1
64
+ }
65
+
66
+ const thresholdMatch = condition.match(/^threshold:(\d+(?:\.\d+)?)$/)
67
+ if (thresholdMatch?.[1]) {
68
+ const threshold = parseFloat(thresholdMatch[1])
69
+ return numericSamples.some((v) => v >= threshold)
70
+ }
71
+
72
+ if (condition === 'trend:declining') {
73
+ const recent = numericSamples.slice(-minSamples)
74
+ return recent.length >= 2 && recent.every((v, i) => i === 0 || v <= (recent[i - 1] ?? v))
75
+ }
76
+
77
+ if (condition === 'trend:increasing') {
78
+ const recent = numericSamples.slice(-minSamples)
79
+ return recent.length >= 2 && recent.every((v, i) => i === 0 || v >= (recent[i - 1] ?? v))
80
+ }
81
+
82
+ throw new Error(`Unknown monitoring termination condition: ${condition}`)
83
+ }
84
+ }
85
+
86
+ export const monitoringWindowService = new MonitoringWindowService()
@@ -106,5 +106,5 @@ async function verifyMutatingApprovalForWorkstream(
106
106
  }
107
107
 
108
108
  export const verifyMutatingApproval: VerifyMutatingApproval = async (params) => {
109
- return await verifyMutatingApprovalForWorkstream(ensureRecordId(params.workstreamId, TABLES.WORKSTREAM), params)
109
+ return verifyMutatingApprovalForWorkstream(ensureRecordId(params.workstreamId, TABLES.WORKSTREAM), params)
110
110
  }
@@ -0,0 +1,155 @@
1
+ import type {
2
+ NodeResultQuality,
3
+ PlanArtifactSubmission,
4
+ PlanNodeResult,
5
+ PlanNodeSpec,
6
+ PlanSchemaRegistry,
7
+ WriteIntent,
8
+ WriteIntentAction,
9
+ } from '@lota-sdk/shared'
10
+
11
+ import { serverLogger } from '../config/logger'
12
+
13
+ export type WorkspaceEntryValidation = 'validated' | 'unvalidated'
14
+
15
+ export interface WorkspaceEntry {
16
+ targetPath: string
17
+ action: WriteIntentAction
18
+ payload: unknown
19
+ validation: WorkspaceEntryValidation
20
+ stagedAt: Date
21
+ }
22
+
23
+ export interface NodeWorkspace {
24
+ nodeId: string
25
+ ctx: Readonly<{
26
+ resolvedInput: Record<string, unknown>
27
+ inputArtifacts: PlanArtifactSubmission[]
28
+ schemaRegistry: PlanSchemaRegistry
29
+ nodeSpec: PlanNodeSpec
30
+ }>
31
+ work: Map<string, unknown>
32
+ sys: {
33
+ startedAt: Date
34
+ correctionCounts: Map<string, number>
35
+ writeLog: Array<{ targetPath: string; action: WriteIntentAction; timestamp: Date; result: 'accepted' | 'rejected' }>
36
+ }
37
+ deliverables: Map<string, WorkspaceEntry>
38
+ }
39
+
40
+ class NodeWorkspaceService {
41
+ initialize(params: {
42
+ nodeSpec: PlanNodeSpec
43
+ resolvedInput: Record<string, unknown>
44
+ inputArtifacts: PlanArtifactSubmission[]
45
+ schemaRegistry: PlanSchemaRegistry
46
+ }): NodeWorkspace {
47
+ const ctx = Object.freeze({
48
+ resolvedInput: params.resolvedInput,
49
+ inputArtifacts: params.inputArtifacts,
50
+ schemaRegistry: params.schemaRegistry,
51
+ nodeSpec: params.nodeSpec,
52
+ })
53
+
54
+ return {
55
+ nodeId: params.nodeSpec.id,
56
+ ctx,
57
+ work: new Map(),
58
+ sys: { startedAt: new Date(), correctionCounts: new Map(), writeLog: [] },
59
+ deliverables: new Map(),
60
+ }
61
+ }
62
+
63
+ stageWrite(workspace: NodeWorkspace, intent: WriteIntent, validation: WorkspaceEntryValidation): void {
64
+ const existing = workspace.deliverables.get(intent.targetPath)
65
+
66
+ if (intent.action === 'append' && existing && Array.isArray(existing.payload) && Array.isArray(intent.payload)) {
67
+ existing.payload = [...(existing.payload as unknown[]), ...intent.payload]
68
+ existing.action = intent.action
69
+ existing.validation = validation
70
+ existing.stagedAt = new Date()
71
+ } else {
72
+ workspace.deliverables.set(intent.targetPath, {
73
+ targetPath: intent.targetPath,
74
+ action: intent.action,
75
+ payload: intent.payload,
76
+ validation,
77
+ stagedAt: new Date(),
78
+ })
79
+ }
80
+
81
+ workspace.sys.writeLog.push({
82
+ targetPath: intent.targetPath,
83
+ action: intent.action,
84
+ timestamp: new Date(),
85
+ result: 'accepted',
86
+ })
87
+ }
88
+
89
+ finalize(workspace: NodeWorkspace): PlanNodeResult & { isComplete: boolean; quality: NodeResultQuality } {
90
+ const artifacts: PlanArtifactSubmission[] = []
91
+ let structuredOutput: Record<string, unknown> | undefined
92
+ let allValidated = true
93
+ const requiredNames = new Set(workspace.ctx.nodeSpec.deliverables.filter((d) => d.required).map((d) => d.name))
94
+ const presentRequired = new Set<string>()
95
+
96
+ for (const [targetPath, entry] of workspace.deliverables) {
97
+ if (entry.validation === 'unvalidated') {
98
+ allValidated = false
99
+ }
100
+
101
+ if (targetPath.startsWith('structuredOutput')) {
102
+ if (!structuredOutput) structuredOutput = {}
103
+ const subPath = targetPath.replace(/^structuredOutput\.?/, '')
104
+ if (subPath) {
105
+ structuredOutput[subPath] = entry.payload
106
+ } else {
107
+ structuredOutput =
108
+ typeof entry.payload === 'object' && entry.payload !== null && !Array.isArray(entry.payload)
109
+ ? (entry.payload as Record<string, unknown>)
110
+ : structuredOutput
111
+ }
112
+ continue
113
+ }
114
+
115
+ const spec = workspace.ctx.nodeSpec.deliverables.find((d) => d.name === targetPath)
116
+ if (!spec) {
117
+ serverLogger.warn`Write to unknown deliverable path "${targetPath}" in node ${workspace.nodeId} — skipping`
118
+ continue
119
+ }
120
+
121
+ if (requiredNames.has(targetPath)) {
122
+ presentRequired.add(targetPath)
123
+ }
124
+
125
+ artifacts.push({
126
+ name: spec.name,
127
+ kind: spec.kind,
128
+ pointer: `workspace://${workspace.nodeId}/${targetPath}`,
129
+ schemaRef: spec.schemaRef,
130
+ description: spec.description,
131
+ payload:
132
+ typeof entry.payload === 'string'
133
+ ? { content: entry.payload }
134
+ : typeof entry.payload === 'object' && entry.payload !== null
135
+ ? (entry.payload as Record<string, unknown> | unknown[])
136
+ : undefined,
137
+ })
138
+ }
139
+
140
+ const allRequiredPresent = requiredNames.size === presentRequired.size
141
+ const quality: NodeResultQuality = allRequiredPresent && allValidated ? 'full' : 'partial'
142
+ const isComplete = quality === 'full'
143
+
144
+ return { structuredOutput, artifacts, quality, isComplete }
145
+ }
146
+
147
+ cleanup(workspace: NodeWorkspace): void {
148
+ workspace.work.clear()
149
+ workspace.deliverables.clear()
150
+ workspace.sys.writeLog.length = 0
151
+ workspace.sys.correctionCounts.clear()
152
+ }
153
+ }
154
+
155
+ export const nodeWorkspaceService = new NodeWorkspaceService()
@@ -0,0 +1,39 @@
1
+ import type { NotificationSeverity } from '@lota-sdk/shared'
2
+
3
+ export interface NotificationPayload {
4
+ organizationId: string
5
+ workstreamId: string
6
+ runId?: string
7
+ nodeId?: string
8
+ severity: NotificationSeverity
9
+ title: string
10
+ body: string
11
+ dedupeKey?: string
12
+ metadata?: Record<string, unknown>
13
+ }
14
+
15
+ export interface NotificationService {
16
+ notify(payload: NotificationPayload): Promise<void>
17
+ remind(payload: NotificationPayload): Promise<void>
18
+ escalate(payload: NotificationPayload): Promise<void>
19
+ }
20
+
21
+ export class NoOpNotificationService implements NotificationService {
22
+ async notify(_payload: NotificationPayload): Promise<void> {}
23
+ async remind(_payload: NotificationPayload): Promise<void> {}
24
+ async escalate(_payload: NotificationPayload): Promise<void> {}
25
+ }
26
+
27
+ let _notificationService: NotificationService | null = null
28
+
29
+ export function configureNotificationService(service: NotificationService | null): void {
30
+ _notificationService = service
31
+ }
32
+
33
+ export function getNotificationService(): NotificationService {
34
+ if (_notificationService === null) {
35
+ throw new Error('Notification service is not configured.')
36
+ }
37
+
38
+ return _notificationService
39
+ }
@@ -1,4 +1,4 @@
1
- import { recordIdStringSchema } from '@lota-sdk/shared'
1
+ import { recordIdSchema, recordIdStringSchema } from '@lota-sdk/shared'
2
2
  import { z } from 'zod'
3
3
 
4
4
  import { BaseService } from '../db/base.service'
@@ -8,9 +8,9 @@ import { TABLES } from '../db/tables'
8
8
  import { toIsoDateTimeString } from '../utils/date-time'
9
9
 
10
10
  const organizationMemberRecordSchema = z.object({
11
- id: z.unknown(),
12
- in: z.unknown(),
13
- out: z.unknown(),
11
+ id: recordIdSchema,
12
+ in: recordIdSchema,
13
+ out: recordIdSchema,
14
14
  role: z.string(),
15
15
  createdAt: z.coerce.date(),
16
16
  })
@@ -91,6 +91,13 @@ class OrganizationMemberService extends BaseService<typeof organizationMemberRec
91
91
  )
92
92
  }
93
93
 
94
+ async getMembership(userId: RecordIdInput, organizationId: RecordIdInput): Promise<SdkOrganizationMember | null> {
95
+ const userRef = ensureRecordId(userId, TABLES.USER)
96
+ const organizationRef = ensureRecordId(organizationId, TABLES.ORGANIZATION)
97
+ const record = (await this.findAll({ in: userRef, out: organizationRef }, { limit: 1 })).at(0)
98
+ return record ? this.toPublic(record) : null
99
+ }
100
+
94
101
  async isMember(userId: RecordIdInput, organizationId: RecordIdInput): Promise<boolean> {
95
102
  const userRef = ensureRecordId(userId, TABLES.USER)
96
103
  const organizationRef = ensureRecordId(organizationId, TABLES.ORGANIZATION)
@@ -1,4 +1,4 @@
1
- import { recordIdStringSchema } from '@lota-sdk/shared'
1
+ import { recordIdSchema, recordIdStringSchema } from '@lota-sdk/shared'
2
2
  import { z } from 'zod'
3
3
 
4
4
  import { BaseService } from '../db/base.service'
@@ -9,12 +9,12 @@ import { TABLES } from '../db/tables'
9
9
  import { toIsoDateTimeString, toOptionalIsoDateTimeString } from '../utils/date-time'
10
10
 
11
11
  const organizationRecordSchema = z.object({
12
- id: z.unknown(),
12
+ id: recordIdSchema,
13
13
  name: z.string(),
14
- regularChatDigestLastWorkstreamCursorCreatedAt: z.union([z.date(), z.string(), z.number()]).optional(),
14
+ regularChatDigestLastWorkstreamCursorCreatedAt: z.coerce.date().optional(),
15
15
  regularChatDigestLastWorkstreamCursorId: z.string().optional(),
16
16
  skillExtractionLastCursorId: z.string().optional(),
17
- skillExtractionLastCursorCreatedAt: z.union([z.date(), z.string(), z.number()]).optional(),
17
+ skillExtractionLastCursorCreatedAt: z.coerce.date().optional(),
18
18
  createdAt: z.coerce.date(),
19
19
  updatedAt: z.coerce.date(),
20
20
  })
@@ -86,7 +86,7 @@ class OrganizationService extends BaseService<typeof organizationRecordSchema> {
86
86
  }
87
87
 
88
88
  async getOrganizationRecord(organizationId: RecordIdRef): Promise<SdkOrganizationRecord> {
89
- return await this.getById(ensureRecordId(organizationId, TABLES.ORGANIZATION))
89
+ return this.getById(ensureRecordId(organizationId, TABLES.ORGANIZATION))
90
90
  }
91
91
 
92
92
  async updateOrganization(organizationId: RecordIdInput, params: { name: string }): Promise<SdkOrganization> {