@lota-sdk/core 0.1.14 → 0.1.16

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 (174) 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 +9 -8
  9. package/src/ai/definitions.ts +80 -2
  10. package/src/ai/embedding-cache.ts +7 -6
  11. package/src/ai/index.ts +0 -1
  12. package/src/bifrost/bifrost.ts +14 -14
  13. package/src/config/agent-defaults.ts +32 -22
  14. package/src/config/agent-types.ts +11 -0
  15. package/src/config/constants.ts +2 -14
  16. package/src/config/debug-logger.ts +5 -1
  17. package/src/config/index.ts +3 -0
  18. package/src/config/logger.ts +7 -9
  19. package/src/config/model-constants.ts +16 -34
  20. package/src/config/search.ts +1 -15
  21. package/src/create-runtime.ts +453 -0
  22. package/src/db/cursor-pagination.ts +3 -6
  23. package/src/db/index.ts +2 -0
  24. package/src/db/memory-store.rows.ts +7 -7
  25. package/src/db/memory-store.ts +24 -24
  26. package/src/db/memory.ts +18 -16
  27. package/src/db/schema-fingerprint.ts +1 -0
  28. package/src/db/service.ts +193 -122
  29. package/src/db/startup.ts +9 -13
  30. package/src/db/surreal-mutation.ts +43 -0
  31. package/src/db/tables.ts +7 -0
  32. package/src/db/workstream-message-row.ts +15 -0
  33. package/src/embeddings/provider.ts +1 -1
  34. package/src/index.ts +1 -1
  35. package/src/queues/context-compaction.queue.ts +17 -52
  36. package/src/queues/delayed-node-promotion.queue.ts +41 -0
  37. package/src/queues/document-processor.queue.ts +7 -7
  38. package/src/queues/index.ts +3 -0
  39. package/src/queues/memory-consolidation.queue.ts +18 -54
  40. package/src/queues/plan-scheduler.queue.ts +97 -0
  41. package/src/queues/post-chat-memory.queue.ts +15 -60
  42. package/src/queues/queue-factory.ts +100 -0
  43. package/src/queues/recent-activity-title-refinement.queue.ts +15 -54
  44. package/src/queues/regular-chat-memory-digest.queue.ts +16 -55
  45. package/src/queues/skill-extraction.queue.ts +15 -50
  46. package/src/queues/workstream-title-generation.queue.ts +15 -51
  47. package/src/redis/connection.ts +12 -3
  48. package/src/redis/index.ts +2 -1
  49. package/src/redis/org-memory-lock.ts +1 -1
  50. package/src/redis/redis-lease-lock.ts +41 -8
  51. package/src/redis/stream-context.ts +11 -0
  52. package/src/runtime/agent-runtime-policy.ts +106 -21
  53. package/src/runtime/agent-stream-helpers.ts +2 -1
  54. package/src/runtime/approval-continuation.ts +12 -6
  55. package/src/runtime/context-compaction-constants.ts +1 -1
  56. package/src/runtime/context-compaction-runtime.ts +7 -5
  57. package/src/runtime/context-compaction.ts +40 -97
  58. package/src/runtime/execution-plan.ts +23 -19
  59. package/src/runtime/graph-designer.ts +15 -0
  60. package/src/runtime/helper-model.ts +10 -196
  61. package/src/runtime/index.ts +14 -1
  62. package/src/runtime/llm-content.ts +1 -1
  63. package/src/runtime/memory-block.ts +11 -12
  64. package/src/runtime/memory-pipeline.ts +26 -10
  65. package/src/runtime/plugin-resolution.ts +35 -0
  66. package/src/runtime/plugin-types.ts +73 -1
  67. package/src/runtime/retrieval-adapters.ts +1 -1
  68. package/src/runtime/runtime-config.ts +25 -12
  69. package/src/runtime/runtime-extensions.ts +91 -15
  70. package/src/runtime/runtime-worker-registry.ts +6 -0
  71. package/src/runtime/team-consultation-orchestrator.ts +45 -28
  72. package/src/runtime/team-consultation-prompts.ts +11 -2
  73. package/src/runtime/title-helpers.ts +11 -4
  74. package/src/runtime/workstream-chat-helpers.ts +6 -7
  75. package/src/runtime/workstream-routing-policy.ts +0 -30
  76. package/src/runtime/workstream-state.ts +17 -7
  77. package/src/services/adaptive-playbook.service.ts +152 -0
  78. package/src/services/agent-executor.service.ts +293 -0
  79. package/src/services/artifact-provenance.service.ts +172 -0
  80. package/src/services/attachment.service.ts +7 -12
  81. package/src/services/context-compaction.service.ts +75 -58
  82. package/src/services/context-enrichment.service.ts +33 -0
  83. package/src/services/coordination-registry.service.ts +117 -0
  84. package/src/services/document-chunk.service.ts +38 -33
  85. package/src/services/domain-agent-executor.service.ts +71 -0
  86. package/src/services/execution-plan.service.ts +271 -50
  87. package/src/services/feedback-loop.service.ts +96 -0
  88. package/src/services/global-orchestrator.service.ts +148 -0
  89. package/src/services/index.ts +26 -0
  90. package/src/services/institutional-memory.service.ts +145 -0
  91. package/src/services/learned-skill.service.ts +30 -15
  92. package/src/services/memory-assessment.service.ts +3 -2
  93. package/src/services/{memory.utils.ts → memory-utils.ts} +4 -13
  94. package/src/services/memory.service.ts +55 -69
  95. package/src/services/monitoring-window.service.ts +86 -0
  96. package/src/services/mutating-approval.service.ts +1 -1
  97. package/src/services/node-workspace.service.ts +155 -0
  98. package/src/services/notification.service.ts +39 -0
  99. package/src/services/organization-member.service.ts +12 -5
  100. package/src/services/organization.service.ts +5 -5
  101. package/src/services/ownership-dispatcher.service.ts +403 -0
  102. package/src/services/plan-approval.service.ts +1 -1
  103. package/src/services/plan-artifact.service.ts +1 -0
  104. package/src/services/plan-builder.service.ts +1 -0
  105. package/src/services/plan-checkpoint.service.ts +30 -2
  106. package/src/services/plan-compiler.service.ts +5 -0
  107. package/src/services/plan-coordination.service.ts +152 -0
  108. package/src/services/plan-cycle.service.ts +284 -0
  109. package/src/services/plan-deadline.service.ts +287 -0
  110. package/src/services/plan-executor.service.ts +386 -58
  111. package/src/services/plan-helpers.ts +15 -0
  112. package/src/services/plan-run.service.ts +41 -7
  113. package/src/services/plan-scheduler.service.ts +240 -0
  114. package/src/services/plan-template.service.ts +117 -0
  115. package/src/services/plan-validator.service.ts +87 -20
  116. package/src/services/plan-workspace.service.ts +83 -0
  117. package/src/services/playbook-registry.service.ts +67 -0
  118. package/src/services/plugin-executor.service.ts +103 -0
  119. package/src/services/quality-metrics.service.ts +132 -0
  120. package/src/services/recent-activity-title.service.ts +3 -10
  121. package/src/services/recent-activity.service.ts +33 -43
  122. package/src/services/skill-resolver.service.ts +19 -0
  123. package/src/services/system-executor.service.ts +105 -0
  124. package/src/services/workstream-message.service.ts +29 -41
  125. package/src/services/workstream-plan-registry.service.ts +22 -0
  126. package/src/services/workstream-title.service.ts +3 -9
  127. package/src/services/{workstream-turn-preparation.ts → workstream-turn-preparation.service.ts} +428 -373
  128. package/src/services/workstream-turn.ts +2 -2
  129. package/src/services/workstream.service.ts +55 -65
  130. package/src/services/workstream.types.ts +10 -19
  131. package/src/services/write-intent-validator.service.ts +81 -0
  132. package/src/storage/attachment-parser.ts +1 -1
  133. package/src/storage/attachment-storage.service.ts +4 -4
  134. package/src/storage/{attachments.utils.ts → attachment-utils.ts} +2 -5
  135. package/src/storage/generated-document-storage.service.ts +3 -2
  136. package/src/storage/index.ts +2 -2
  137. package/src/system-agents/{context-compacter.agent.ts → context-compaction.agent.ts} +4 -4
  138. package/src/system-agents/delegated-agent-factory.ts +5 -2
  139. package/src/system-agents/index.ts +8 -0
  140. package/src/system-agents/memory-reranker.agent.ts +1 -1
  141. package/src/system-agents/memory.agent.ts +1 -1
  142. package/src/system-agents/recent-activity-title-refiner.agent.ts +1 -1
  143. package/src/tools/execution-plan.tool.ts +17 -19
  144. package/src/tools/fetch-webpage.tool.ts +20 -18
  145. package/src/tools/index.ts +2 -3
  146. package/src/tools/read-file-parts.tool.ts +1 -1
  147. package/src/tools/search-web.tool.ts +18 -15
  148. package/src/tools/{search-tools.ts → search.tool.ts} +1 -1
  149. package/src/tools/team-think.tool.ts +14 -8
  150. package/src/tools/{tool-contract.ts → tool-contracts.ts} +9 -2
  151. package/src/utils/async.ts +3 -2
  152. package/src/utils/date-time.ts +4 -32
  153. package/src/utils/env.ts +8 -0
  154. package/src/utils/errors.ts +47 -0
  155. package/src/utils/hono-error-handler.ts +1 -2
  156. package/src/utils/index.ts +19 -2
  157. package/src/utils/string.ts +128 -1
  158. package/src/workers/bootstrap.ts +2 -2
  159. package/src/workers/index.ts +1 -0
  160. package/src/workers/memory-consolidation.worker.ts +12 -12
  161. package/src/workers/regular-chat-memory-digest.helpers.ts +2 -7
  162. package/src/workers/regular-chat-memory-digest.runner.ts +11 -105
  163. package/src/workers/skill-extraction.runner.ts +8 -102
  164. package/src/workers/utils/file-section-chunker.ts +6 -3
  165. package/src/workers/utils/repomix-file-sections.ts +2 -2
  166. package/src/workers/utils/sandbox-error.ts +11 -2
  167. package/src/workers/utils/workstream-message-query.ts +97 -0
  168. package/src/workers/worker-utils.ts +6 -2
  169. package/src/runtime/retrieval-pipeline.ts +0 -3
  170. package/src/runtime.ts +0 -387
  171. package/src/tools/log-hello-world.tool.ts +0 -17
  172. package/src/utils/error.ts +0 -10
  173. /package/src/services/{context-compaction-runtime.ts → context-compaction-runtime.singleton.ts} +0 -0
  174. /package/src/storage/{attachments.types.ts → attachment-types.ts} +0 -0
@@ -24,10 +24,11 @@ import {
24
24
  scopedRetrievalToMap,
25
25
  } from '../runtime/retrieval-adapters'
26
26
  import { getRuntimeConfig } from '../runtime/runtime-config'
27
- import { createMemoryRerankerAgent, memoryRerankerPrompt } from '../system-agents/memory-reranker.agent'
28
- import { createOrgMemoryAgent, orgMemoryPrompt } from '../system-agents/memory.agent'
27
+ import { createMemoryRerankerAgent, MEMORY_RERANKER_PROMPT } from '../system-agents/memory-reranker.agent'
28
+ import { createOrgMemoryAgent, ORG_MEMORY_PROMPT } from '../system-agents/memory.agent'
29
+ import { compactWhitespace, truncateText } from '../utils/string'
29
30
  import { assessMemoryImportance, clampMemoryImportance } from './memory-assessment.service'
30
- import { formatMemoryResults, formatRerankedResults, getCandidateLimit } from './memory.utils'
31
+ import { formatMemoryResults, formatRerankedResults, getCandidateLimit } from './memory-utils'
31
32
 
32
33
  const ORG_MEMORY_TYPE = 'fact'
33
34
  const RERANK_CANDIDATE_MAX_CHARS = 500
@@ -42,6 +43,7 @@ const MAX_CONVERSATION_MEMORY_BLOCK_CHARS = 2_000
42
43
  const MAX_CONVERSATION_ATTACHMENT_CONTEXT_CHARS = 6_000
43
44
  const MAX_CONVERSATION_ASSESSMENT_CHARS = 7_000
44
45
  const ONBOARDING_MEMORY_MAX_FACTS = 16
46
+ const MAX_ORG_MEMORY_CLIENTS = 128
45
47
  const ONBOARDING_MEMORY_EXTRACTION_PROMPT =
46
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.'
47
49
 
@@ -63,16 +65,28 @@ const MemoryRerankOutputSchema = z.object({
63
65
 
64
66
  export type MemoryRerankOutput = z.infer<typeof MemoryRerankOutputSchema>
65
67
 
66
- const isRoutableAgentName = (value?: string): boolean => Boolean(value && agentRoster.includes(value))
68
+ const isRoutableAgentName = (value?: string): value is string => Boolean(value && agentRoster.includes(value))
67
69
 
68
70
  class MemoryService {
69
71
  private orgMemoryCache = new Map<string, Memory>()
70
72
 
71
73
  private getOrCreateMemory(cacheKey: string, cache: Map<string, Memory>): Memory {
72
74
  const cached = cache.get(cacheKey)
73
- if (cached) return cached
75
+ if (cached) {
76
+ cache.delete(cacheKey)
77
+ cache.set(cacheKey, cached)
78
+ return cached
79
+ }
80
+
81
+ const memory = new Memory({ createAgent: createOrgMemoryAgent }, { customPrompt: ORG_MEMORY_PROMPT })
74
82
 
75
- const memory = new Memory({ createAgent: createOrgMemoryAgent }, { customPrompt: orgMemoryPrompt })
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
+ }
76
90
 
77
91
  cache.set(cacheKey, memory)
78
92
  aiLogger.debug`Memory client created and cached for ${cacheKey}`
@@ -85,9 +99,7 @@ class MemoryService {
85
99
  }
86
100
 
87
101
  private truncateCandidateText(value: string): string {
88
- const normalized = value.replace(/\s+/g, ' ').trim()
89
- if (normalized.length <= RERANK_CANDIDATE_MAX_CHARS) return normalized
90
- return `${normalized.slice(0, RERANK_CANDIDATE_MAX_CHARS)}...`
102
+ return truncateText(compactWhitespace(value), RERANK_CANDIDATE_MAX_CHARS)
91
103
  }
92
104
 
93
105
  private readNumericMetadata(metadata: Record<string, unknown>, key: string): number {
@@ -125,10 +137,9 @@ class MemoryService {
125
137
  }
126
138
 
127
139
  private normalizeConversationText(value: string, maxChars: number): string {
128
- const normalized = value.replace(/\s+/g, ' ').trim()
140
+ const normalized = compactWhitespace(value)
129
141
  if (!normalized) return ''
130
- if (normalized.length <= maxChars) return normalized
131
- return `${normalized.slice(0, maxChars - 3)}...`
142
+ return truncateText(normalized, maxChars)
132
143
  }
133
144
 
134
145
  private resolveAgentScopeNames(agentName?: string, agentNames: string[] = []): string[] {
@@ -146,9 +157,7 @@ class MemoryService {
146
157
  }
147
158
 
148
159
  private normalizePreSeededMemoryText(value: string): string {
149
- const normalized = value.replace(/\s+/g, ' ').trim()
150
- if (normalized.length <= PRESEEDED_MEMORY_MAX_CHARS) return normalized
151
- return `${normalized.slice(0, PRESEEDED_MEMORY_MAX_CHARS - 3)}...`
160
+ return truncateText(compactWhitespace(value), PRESEEDED_MEMORY_MAX_CHARS)
152
161
  }
153
162
 
154
163
  private formatPreSeededMemoriesSection(memories: MemoryRecord[]): string | undefined {
@@ -232,7 +241,7 @@ class MemoryService {
232
241
  return await helperModelRuntime.generateHelperStructured({
233
242
  tag: 'memory-reranker',
234
243
  createAgent: createMemoryRerankerAgent,
235
- defaultSystemPrompt: memoryRerankerPrompt,
244
+ defaultSystemPrompt: MEMORY_RERANKER_PROMPT,
236
245
  messages: [
237
246
  {
238
247
  role: 'user',
@@ -269,7 +278,7 @@ class MemoryService {
269
278
  return await helperModelRuntime.generateHelperStructured({
270
279
  tag: 'memory-reranker-multi-scope',
271
280
  createAgent: createMemoryRerankerAgent,
272
- defaultSystemPrompt: memoryRerankerPrompt,
281
+ defaultSystemPrompt: MEMORY_RERANKER_PROMPT,
273
282
  messages: [
274
283
  {
275
284
  role: 'user',
@@ -329,15 +338,9 @@ class MemoryService {
329
338
  aiLogger.debug`Organization memory search requested (orgId: ${orgId}, scopeId: ${orgScopeId}, queryLength: ${query.length})`
330
339
  const memory = this.getOrgMemory(orgId)
331
340
 
332
- try {
333
- const results = await this.searchMemories({ query, memory, scopeId: orgScopeId, memoryType: ORG_MEMORY_TYPE })
334
- aiLogger.debug`Organization memory search completed (resultLength: ${results.length}, preview: ${results.slice(0, 100)})`
335
- return results
336
- } catch (error: unknown) {
337
- const normalizedError = error instanceof Error ? error : new Error(String(error))
338
- aiLogger.error`Organization memory search failed: ${normalizedError}`
339
- throw normalizedError
340
- }
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
341
344
  }
342
345
 
343
346
  async getStaleMemories(orgId: string): Promise<string> {
@@ -348,7 +351,8 @@ class MemoryService {
348
351
  if (stale.length === 0) return ''
349
352
  const items = stale.map((m) => `- [NEEDS REVIEW] ${m.content}`).join('\n')
350
353
  return `Memories flagged for review (parent fact was superseded):\n${items}`
351
- } catch {
354
+ } catch (error) {
355
+ aiLogger.warn`Failed to get stale memories: ${error}`
352
356
  return ''
353
357
  }
354
358
  }
@@ -359,27 +363,21 @@ class MemoryService {
359
363
  options?: { fastMode?: boolean; limit?: number },
360
364
  ): Promise<string> {
361
365
  const orgScopeId = scopeId(ORG_SCOPE_PREFIX, orgId)
362
- aiLogger.info`[MEMORY_DEBUG] searchOrganizationMemoriesRaw - orgId: "${orgId}", scopeId: "${orgScopeId}"`
366
+ aiLogger.debug`searchOrganizationMemoriesRaw - orgId: "${orgId}", scopeId: "${orgScopeId}"`
363
367
  const memory = this.getOrgMemory(orgId)
364
368
  const fastMode = options?.fastMode ?? true
365
369
  const searchK = getRuntimeConfig().memory.searchK
366
370
  const limit = options?.limit ?? (fastMode ? Math.min(searchK, 4) : searchK)
367
371
 
368
- try {
369
- const candidates = await memory.searchCandidates(query, {
370
- scopeId: orgScopeId,
371
- limit,
372
- memoryType: ORG_MEMORY_TYPE,
373
- fastMode,
374
- includeNeighborContext: !fastMode,
375
- })
376
- aiLogger.debug`Organization memory search (raw) completed (candidates: ${candidates.length})`
377
- return formatMemoryResults(candidates)
378
- } catch (error: unknown) {
379
- const normalizedError = error instanceof Error ? error : new Error(String(error))
380
- aiLogger.error`Organization memory search (raw) failed: ${normalizedError}`
381
- throw normalizedError
382
- }
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)
383
381
  }
384
382
 
385
383
  async searchAgentMemories(orgId: string, agentName: string, query: string): Promise<string> {
@@ -392,20 +390,14 @@ class MemoryService {
392
390
  aiLogger.debug`Agent memory search requested (orgId: ${orgId}, agentName: ${agentName}, scopeId: ${scoped}, queryLength: ${query.length})`
393
391
  const memory = this.getOrgMemory(orgId)
394
392
 
395
- try {
396
- const results = await this.searchMemories({ query, memory, scopeId: scoped, memoryType: ORG_MEMORY_TYPE })
397
- aiLogger.debug`Agent memory search completed (agentName: ${agentName}, resultLength: ${results.length}, preview: ${results.slice(0, 100)})`
398
- return results
399
- } catch (error: unknown) {
400
- const normalizedError = error instanceof Error ? error : new Error(String(error))
401
- aiLogger.error`Agent memory search failed: ${normalizedError}`
402
- throw normalizedError
403
- }
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
404
396
  }
405
397
 
406
398
  async searchOrgMemoriesForAgent(orgId: string, agentName: string, query: string): Promise<string> {
407
399
  if (!isRoutableAgentName(agentName)) {
408
- return await this.searchOrganizationMemories(orgId, query)
400
+ return this.searchOrganizationMemories(orgId, query)
409
401
  }
410
402
 
411
403
  const [agentResult, orgResult] = await Promise.all([
@@ -426,7 +418,7 @@ class MemoryService {
426
418
  }): Promise<MemoryRecord[]> {
427
419
  const { orgId, ...listOptions } = params
428
420
  const orgMemory = this.getOrgMemory(orgId)
429
- return await orgMemory.list({ scopeId: scopeId(ORG_SCOPE_PREFIX, orgId), ...listOptions })
421
+ return orgMemory.list({ scopeId: scopeId(ORG_SCOPE_PREFIX, orgId), ...listOptions })
430
422
  }
431
423
 
432
424
  async getTopMemories(params: { orgId: string; agentName?: string; limit?: number }): Promise<string | undefined> {
@@ -467,7 +459,7 @@ class MemoryService {
467
459
  const deduped: MemoryRecord[] = []
468
460
  const seen = new Set<string>()
469
461
  for (const memory of combined) {
470
- const normalizedKey = memory.content.replace(/\s+/g, ' ').trim().toLowerCase()
462
+ const normalizedKey = compactWhitespace(memory.content).toLowerCase()
471
463
  if (!normalizedKey || seen.has(normalizedKey)) continue
472
464
  seen.add(normalizedKey)
473
465
  deduped.push(memory)
@@ -510,7 +502,7 @@ class MemoryService {
510
502
  ]
511
503
 
512
504
  if (isRoutableAgentName(agentName)) {
513
- const agentScoped = agentScopeId(orgId, agentName as string)
505
+ const agentScoped = agentScopeId(orgId, agentName)
514
506
  retrievalTasks.push({
515
507
  scopeTag: `agent:${agentName}`,
516
508
  retrieve: async () =>
@@ -594,7 +586,7 @@ class MemoryService {
594
586
  durability?: MemoryRecord['durability']
595
587
  }): Promise<string> {
596
588
  const orgScopeId = scopeId(ORG_SCOPE_PREFIX, orgId)
597
- aiLogger.info`[MEMORY_DEBUG] createOrganizationMemory - orgId: "${orgId}", scopeId: "${orgScopeId}", content preview: "${content.slice(0, 50)}"`
589
+ aiLogger.debug`createOrganizationMemory - orgId: "${orgId}", scopeId: "${orgScopeId}", content preview: "${content.slice(0, 50)}"`
598
590
  const memory = this.getOrgMemory(orgId)
599
591
  try {
600
592
  return await memory.insert(content, {
@@ -646,7 +638,7 @@ class MemoryService {
646
638
  importance?: number
647
639
  }): Promise<string> {
648
640
  if (!isRoutableAgentName(agentName)) {
649
- throw new Error(`Invalid agentName for agent memory: ${agentName}`)
641
+ throw new Error(`Invalid agentName for agent memory: ${agentName as string}`)
650
642
  }
651
643
 
652
644
  const memory = this.getOrgMemory(orgId)
@@ -762,7 +754,7 @@ class MemoryService {
762
754
  }
763
755
 
764
756
  const orgScopeId = scopeId(ORG_SCOPE_PREFIX, orgId)
765
- aiLogger.info`[MEMORY_DEBUG] addConversationMemories - orgId: "${orgId}", scopeId: "${orgScopeId}", sourceId: ${sourceId ?? 'none'}`
757
+ aiLogger.debug`addConversationMemories - orgId: "${orgId}", scopeId: "${orgScopeId}", sourceId: ${sourceId ?? 'none'}`
766
758
 
767
759
  const orgMemory = this.getOrgMemory(orgId)
768
760
  let assessedImportance: number | undefined
@@ -827,16 +819,10 @@ class MemoryService {
827
819
  const preparedUpdates = await orgMemory.prepareFactsToScopes(extractedFacts, scopes)
828
820
  if (preparedUpdates.length === 0) return
829
821
 
830
- try {
831
- await withOrgMemoryLock(orgId, async () => {
832
- await orgMemory.applyPreparedScopeUpdates(preparedUpdates)
833
- })
834
- aiLogger.debug`Conversation memories added to ${scopes.length} scope(s) from ${messages.length} message(s)`
835
- } catch (error: unknown) {
836
- const normalizedError = error instanceof Error ? error : new Error(String(error))
837
- aiLogger.error`Memory write failed: ${normalizedError}`
838
- throw normalizedError
839
- }
822
+ await withOrgMemoryLock(orgId, async () => {
823
+ await orgMemory.applyPreparedScopeUpdates(preparedUpdates)
824
+ })
825
+ aiLogger.debug`Conversation memories added to ${scopes.length} scope(s) from ${messages.length} message(s)`
840
826
  }
841
827
  }
842
828
 
@@ -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
  })
@@ -23,7 +23,7 @@ const sdkOrganizationMemberSchema = z.object({
23
23
  createdAt: z.iso.datetime(),
24
24
  })
25
25
 
26
- export type SdkOrganizationMemberRecord = z.infer<typeof organizationMemberRecordSchema>
26
+ type SdkOrganizationMemberRecord = z.infer<typeof organizationMemberRecordSchema>
27
27
  export type SdkOrganizationMember = z.infer<typeof sdkOrganizationMemberSchema>
28
28
 
29
29
  class OrganizationMemberService extends BaseService<typeof organizationMemberRecordSchema> {
@@ -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> {