@lota-sdk/core 0.1.15 → 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 (138) 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 +8 -7
  9. package/src/ai/definitions.ts +80 -2
  10. package/src/ai/index.ts +0 -2
  11. package/src/bifrost/bifrost.ts +2 -7
  12. package/src/config/agent-defaults.ts +31 -21
  13. package/src/config/agent-types.ts +11 -0
  14. package/src/config/constants.ts +2 -14
  15. package/src/config/debug-logger.ts +5 -1
  16. package/src/config/index.ts +3 -0
  17. package/src/config/model-constants.ts +16 -34
  18. package/src/config/search.ts +1 -15
  19. package/src/create-runtime.ts +244 -178
  20. package/src/db/cursor-pagination.ts +3 -6
  21. package/src/db/index.ts +2 -0
  22. package/src/db/memory-store.rows.ts +7 -7
  23. package/src/db/memory-store.ts +14 -18
  24. package/src/db/memory.ts +13 -13
  25. package/src/db/service.ts +153 -79
  26. package/src/db/startup.ts +6 -10
  27. package/src/db/surreal-mutation.ts +43 -0
  28. package/src/db/tables.ts +7 -0
  29. package/src/db/workstream-message-row.ts +15 -0
  30. package/src/embeddings/provider.ts +1 -1
  31. package/src/queues/context-compaction.queue.ts +15 -46
  32. package/src/queues/delayed-node-promotion.queue.ts +41 -0
  33. package/src/queues/index.ts +3 -0
  34. package/src/queues/memory-consolidation.queue.ts +16 -51
  35. package/src/queues/plan-scheduler.queue.ts +97 -0
  36. package/src/queues/post-chat-memory.queue.ts +15 -56
  37. package/src/queues/queue-factory.ts +100 -0
  38. package/src/queues/recent-activity-title-refinement.queue.ts +15 -50
  39. package/src/queues/regular-chat-memory-digest.queue.ts +16 -52
  40. package/src/queues/skill-extraction.queue.ts +15 -47
  41. package/src/queues/workstream-title-generation.queue.ts +15 -47
  42. package/src/redis/connection.ts +6 -0
  43. package/src/redis/index.ts +1 -1
  44. package/src/redis/stream-context.ts +11 -0
  45. package/src/runtime/agent-runtime-policy.ts +106 -21
  46. package/src/runtime/approval-continuation.ts +12 -6
  47. package/src/runtime/context-compaction-runtime.ts +1 -1
  48. package/src/runtime/context-compaction.ts +22 -60
  49. package/src/runtime/execution-plan.ts +22 -18
  50. package/src/runtime/graph-designer.ts +15 -0
  51. package/src/runtime/helper-model.ts +9 -197
  52. package/src/runtime/index.ts +2 -0
  53. package/src/runtime/llm-content.ts +1 -1
  54. package/src/runtime/memory-block.ts +9 -11
  55. package/src/runtime/memory-pipeline.ts +6 -9
  56. package/src/runtime/plugin-resolution.ts +35 -0
  57. package/src/runtime/plugin-types.ts +72 -0
  58. package/src/runtime/retrieval-adapters.ts +1 -1
  59. package/src/runtime/runtime-config.ts +25 -12
  60. package/src/runtime/runtime-extensions.ts +2 -2
  61. package/src/runtime/runtime-worker-registry.ts +6 -0
  62. package/src/runtime/team-consultation-orchestrator.ts +45 -28
  63. package/src/runtime/team-consultation-prompts.ts +11 -2
  64. package/src/runtime/title-helpers.ts +2 -4
  65. package/src/runtime/workstream-chat-helpers.ts +1 -1
  66. package/src/services/adaptive-playbook.service.ts +152 -0
  67. package/src/services/agent-executor.service.ts +293 -0
  68. package/src/services/artifact-provenance.service.ts +172 -0
  69. package/src/services/attachment.service.ts +6 -11
  70. package/src/services/context-compaction.service.ts +72 -55
  71. package/src/services/context-enrichment.service.ts +33 -0
  72. package/src/services/coordination-registry.service.ts +117 -0
  73. package/src/services/document-chunk.service.ts +1 -1
  74. package/src/services/domain-agent-executor.service.ts +71 -0
  75. package/src/services/execution-plan.service.ts +269 -50
  76. package/src/services/feedback-loop.service.ts +96 -0
  77. package/src/services/global-orchestrator.service.ts +148 -0
  78. package/src/services/index.ts +26 -0
  79. package/src/services/institutional-memory.service.ts +145 -0
  80. package/src/services/learned-skill.service.ts +24 -5
  81. package/src/services/memory-assessment.service.ts +3 -2
  82. package/src/services/memory-utils.ts +3 -8
  83. package/src/services/memory.service.ts +42 -59
  84. package/src/services/monitoring-window.service.ts +86 -0
  85. package/src/services/mutating-approval.service.ts +1 -1
  86. package/src/services/node-workspace.service.ts +155 -0
  87. package/src/services/notification.service.ts +39 -0
  88. package/src/services/organization-member.service.ts +11 -4
  89. package/src/services/organization.service.ts +5 -5
  90. package/src/services/ownership-dispatcher.service.ts +403 -0
  91. package/src/services/plan-approval.service.ts +1 -1
  92. package/src/services/plan-builder.service.ts +1 -0
  93. package/src/services/plan-checkpoint.service.ts +30 -2
  94. package/src/services/plan-compiler.service.ts +5 -0
  95. package/src/services/plan-coordination.service.ts +152 -0
  96. package/src/services/plan-cycle.service.ts +284 -0
  97. package/src/services/plan-deadline.service.ts +287 -0
  98. package/src/services/plan-executor.service.ts +384 -40
  99. package/src/services/plan-run.service.ts +41 -7
  100. package/src/services/plan-scheduler.service.ts +240 -0
  101. package/src/services/plan-template.service.ts +117 -0
  102. package/src/services/plan-validator.service.ts +84 -2
  103. package/src/services/plan-workspace.service.ts +83 -0
  104. package/src/services/playbook-registry.service.ts +67 -0
  105. package/src/services/plugin-executor.service.ts +103 -0
  106. package/src/services/quality-metrics.service.ts +132 -0
  107. package/src/services/recent-activity.service.ts +27 -31
  108. package/src/services/skill-resolver.service.ts +19 -0
  109. package/src/services/system-executor.service.ts +105 -0
  110. package/src/services/workstream-message.service.ts +12 -34
  111. package/src/services/workstream-plan-registry.service.ts +22 -0
  112. package/src/services/workstream-title.service.ts +3 -1
  113. package/src/services/workstream-turn-preparation.service.ts +34 -66
  114. package/src/services/workstream.service.ts +33 -55
  115. package/src/services/workstream.types.ts +9 -9
  116. package/src/services/write-intent-validator.service.ts +81 -0
  117. package/src/storage/attachment-parser.ts +1 -1
  118. package/src/storage/attachment-utils.ts +1 -1
  119. package/src/storage/generated-document-storage.service.ts +3 -2
  120. package/src/system-agents/delegated-agent-factory.ts +2 -0
  121. package/src/tools/execution-plan.tool.ts +17 -23
  122. package/src/tools/index.ts +0 -1
  123. package/src/tools/team-think.tool.ts +6 -4
  124. package/src/utils/async.ts +2 -1
  125. package/src/utils/date-time.ts +4 -32
  126. package/src/utils/env.ts +8 -0
  127. package/src/utils/errors.ts +42 -10
  128. package/src/utils/index.ts +9 -0
  129. package/src/utils/string.ts +114 -1
  130. package/src/workers/index.ts +1 -0
  131. package/src/workers/regular-chat-memory-digest.runner.ts +2 -2
  132. package/src/workers/skill-extraction.runner.ts +1 -1
  133. package/src/workers/utils/file-section-chunker.ts +2 -1
  134. package/src/workers/utils/repomix-file-sections.ts +2 -2
  135. package/src/workers/utils/sandbox-error.ts +11 -2
  136. package/src/workers/utils/workstream-message-query.ts +11 -20
  137. package/src/workers/worker-utils.ts +2 -2
  138. package/src/tools/log-hello-world.tool.ts +0 -17
@@ -0,0 +1,148 @@
1
+ import type { ConvergenceState, PlanFailureClass } from '@lota-sdk/shared'
2
+
3
+ import { serverLogger } from '../config/logger'
4
+ import { recordIdToString } from '../db/record-id'
5
+ import { TABLES } from '../db/tables'
6
+
7
+ function classifyDispatchFailure(ownerType: string, error: unknown): PlanFailureClass {
8
+ const errorMessage = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase()
9
+ if (errorMessage.includes('timeout')) return 'timeout_exceeded'
10
+ if (ownerType === 'plugin' || ownerType === 'system') return 'external_system_unavailable'
11
+ return 'non_recoverable_logic_error'
12
+ }
13
+
14
+ function formatDispatchError(error: unknown): string {
15
+ return error instanceof Error ? error.message : String(error)
16
+ }
17
+
18
+ const STABLE_RUN_STATUSES = new Set(['awaiting-human', 'blocked', 'failed', 'completed', 'aborted'])
19
+
20
+ class GlobalOrchestratorService {
21
+ detectConvergence(params: {
22
+ totalNodes: number
23
+ completedNodes: number
24
+ failedNodes: number
25
+ previousCompletedNodes?: number
26
+ previousFailedNodes?: number
27
+ }): ConvergenceState {
28
+ const completionRatio = params.totalNodes > 0 ? params.completedNodes / params.totalNodes : 0
29
+ const failureRatio = params.totalNodes > 0 ? params.failedNodes / params.totalNodes : 0
30
+
31
+ if (params.previousCompletedNodes !== undefined) {
32
+ const completionVelocity = params.completedNodes - params.previousCompletedNodes
33
+ const failureVelocity = params.failedNodes - (params.previousFailedNodes ?? 0)
34
+
35
+ if (completionVelocity > 0 && failureVelocity === 0) return 'converging'
36
+ if (completionVelocity === 0 && failureVelocity === 0) return 'stalled'
37
+ if (failureVelocity > 0) return 'diverging'
38
+ }
39
+
40
+ if (completionRatio > 0.5 && failureRatio === 0) return 'converging'
41
+ if (failureRatio > 0.3) return 'diverging'
42
+ return 'progressing'
43
+ }
44
+
45
+ decideRerouteAction(params: {
46
+ failedNodeId: string
47
+ retryCount: number
48
+ maxRetries: number
49
+ convergenceState: ConvergenceState
50
+ }): 'retry' | 'skip' | 'abort' {
51
+ if (params.retryCount < params.maxRetries) return 'retry'
52
+ if (params.convergenceState === 'converging') return 'skip'
53
+ if (params.convergenceState === 'diverging') return 'abort'
54
+ return 'skip'
55
+ }
56
+
57
+ async routeGraphFull(params: { workstreamId: string; runId: string }): Promise<void> {
58
+ const MAX_ROUNDS = 32
59
+ const STRUCTURAL_TYPES = new Set(['switch', 'join', 'deliberation-fork'])
60
+
61
+ // Dynamic imports to avoid circular dependencies
62
+ const { planRunService } = await import('./plan-run.service')
63
+ const { ownershipDispatcherService } = await import('./ownership-dispatcher.service')
64
+ const { planExecutorService } = await import('./plan-executor.service')
65
+
66
+ let round = 0
67
+ for (; round < MAX_ROUNDS; round++) {
68
+ const run = await planRunService.getRunById(params.runId)
69
+ if (STABLE_RUN_STATUSES.has(run.status)) break
70
+
71
+ const spec = await planRunService.getPlanSpecById(run.planSpecId)
72
+ const nodeSpecs = await planRunService.listNodeSpecs(spec.id)
73
+ const nodeRuns = await planRunService.listNodeRuns(run.id)
74
+
75
+ // Find ready action nodes (not structural, not human — those are handled by syncRunGraph)
76
+ const readyNodes = nodeRuns.filter((nr) => {
77
+ if (nr.status !== 'ready') return false
78
+ const ns = nodeSpecs.find((s) => s.nodeId === nr.nodeId)
79
+ return ns && ns.owner.executorType !== 'user' && !STRUCTURAL_TYPES.has(ns.type)
80
+ })
81
+ if (readyNodes.length === 0) break
82
+
83
+ // Transition all ready nodes to 'running' BEFORE dispatching
84
+ for (const nodeRun of readyNodes) {
85
+ await planExecutorService.transitionNodeToRunning({ runId: params.runId, nodeId: nodeRun.nodeId })
86
+ }
87
+
88
+ // Re-fetch run after transitions for accurate state in dispatch context
89
+ const updatedRun = await planRunService.getRunById(params.runId)
90
+
91
+ // Dispatch all in parallel with LINEAR mode override (prevents recursion)
92
+ const results = await Promise.allSettled(
93
+ readyNodes.map(async (nodeRun) => {
94
+ const nodeSpecRecord = nodeSpecs.find((ns) => ns.nodeId === nodeRun.nodeId)
95
+ if (!nodeSpecRecord) {
96
+ throw new Error(`Node spec not found for node "${nodeRun.nodeId}".`)
97
+ }
98
+ // Re-fetch the node run to get the updated 'running' state with resolvedInput
99
+ const updatedNodeRun = await planRunService.getNodeRunByNodeId(updatedRun.id, nodeRun.nodeId)
100
+ const result = await ownershipDispatcherService.dispatchReadyNode({
101
+ run: updatedRun,
102
+ nodeSpecRecord,
103
+ nodeRun: updatedNodeRun,
104
+ spec,
105
+ executionModeOverride: 'linear',
106
+ })
107
+ return { nodeId: nodeRun.nodeId, ownerRef: nodeSpecRecord.owner.ref, result }
108
+ }),
109
+ )
110
+
111
+ const workstreamId = recordIdToString(updatedRun.workstreamId, TABLES.WORKSTREAM)
112
+ const runId = recordIdToString(updatedRun.id, TABLES.PLAN_RUN)
113
+
114
+ // Submit results sequentially (each triggers syncRunGraph internally)
115
+ for (let i = 0; i < results.length; i++) {
116
+ const settled = results[i]
117
+ const nodeRun = readyNodes[i]
118
+ const nodeSpecRecord = nodeSpecs.find((ns) => ns.nodeId === nodeRun.nodeId)
119
+
120
+ if (settled.status === 'fulfilled') {
121
+ await planExecutorService.submitNodeResult({
122
+ workstreamId,
123
+ runId,
124
+ nodeId: settled.value.nodeId,
125
+ emittedBy: settled.value.ownerRef,
126
+ result: settled.value.result,
127
+ })
128
+ } else {
129
+ serverLogger.warn`routeGraphFull: dispatch failed for node "${nodeRun.nodeId}": ${settled.reason}`
130
+ await planExecutorService.blockNodeOnDispatchFailure({
131
+ workstreamId,
132
+ runId,
133
+ nodeId: nodeRun.nodeId,
134
+ emittedBy: nodeSpecRecord?.owner.ref ?? 'unknown',
135
+ message: formatDispatchError(settled.reason),
136
+ failureClass: classifyDispatchFailure(nodeSpecRecord?.owner.executorType ?? 'agent', settled.reason),
137
+ })
138
+ }
139
+ }
140
+ }
141
+
142
+ if (round === MAX_ROUNDS - 1) {
143
+ serverLogger.warn`graph-full execution reached max rounds (${MAX_ROUNDS}) for run ${params.runId} — possible non-converging graph`
144
+ }
145
+ }
146
+ }
147
+
148
+ export const globalOrchestratorService = new GlobalOrchestratorService()
@@ -1,14 +1,40 @@
1
+ export * from './adaptive-playbook.service'
2
+ export * from './agent-executor.service'
3
+ export * from './artifact-provenance.service'
1
4
  export * from './attachment.service'
5
+ export * from './context-enrichment.service'
6
+ export * from './coordination-registry.service'
2
7
  export * from './document-chunk.service'
8
+ export * from './domain-agent-executor.service'
3
9
  export * from './execution-plan.service'
10
+ export * from './institutional-memory.service'
11
+ export * from './feedback-loop.service'
12
+ export * from './global-orchestrator.service'
4
13
  export * from './memory.service'
14
+ export * from './node-workspace.service'
15
+ export * from './notification.service'
16
+ export * from './ownership-dispatcher.service'
5
17
  export * from './organization-member.service'
6
18
  export * from './organization.service'
19
+ export * from './plan-coordination.service'
20
+ export * from './plan-cycle.service'
21
+ export * from './plan-deadline.service'
22
+ export * from './plan-workspace.service'
7
23
  export * from './plan-run.service'
24
+ export * from './plan-scheduler.service'
25
+ export * from './plan-template.service'
26
+ export * from './playbook-registry.service'
27
+ export * from './quality-metrics.service'
28
+ export * from './monitoring-window.service'
29
+ export * from './plugin-executor.service'
8
30
  export * from './recent-activity-title.service'
9
31
  export * from './recent-activity.service'
32
+ export * from './skill-resolver.service'
33
+ export * from './system-executor.service'
10
34
  export * from './user.service'
11
35
  export * from './workstream-message.service'
12
36
  export * from './workstream-title.service'
13
37
  export * from './workstream-turn'
38
+ export * from './workstream-plan-registry.service'
14
39
  export * from './workstream.service'
40
+ export * from './write-intent-validator.service'
@@ -0,0 +1,145 @@
1
+ import type { InstitutionalMemory, InstitutionalMemoryType } from '@lota-sdk/shared'
2
+ import { InstitutionalMemorySchema } from '@lota-sdk/shared'
3
+ import { BoundQuery } from 'surrealdb'
4
+
5
+ import { ensureRecordId } from '../db/record-id'
6
+ import { databaseService } from '../db/service'
7
+ import { TABLES } from '../db/tables'
8
+
9
+ class InstitutionalMemoryService {
10
+ async extractPatterns(params: { organizationId: string; runId: string }): Promise<InstitutionalMemory[]> {
11
+ const { planRunService } = await import('./plan-run.service')
12
+
13
+ const run = await planRunService.getRunById(params.runId)
14
+ const nodeRuns = await planRunService.listNodeRuns(run.id)
15
+ const nodeSpecs = await planRunService.listNodeSpecs(run.planSpecId)
16
+ const nodeSpecsByNodeId = new Map(nodeSpecs.map((n) => [n.nodeId, n]))
17
+
18
+ const patterns: InstitutionalMemory[] = []
19
+ const orgRef = ensureRecordId(params.organizationId, TABLES.ORGANIZATION)
20
+ const ownerRef = (nodeId: string): string => nodeSpecsByNodeId.get(nodeId)?.owner.ref ?? 'unknown'
21
+
22
+ const completedNodes = nodeRuns.filter((nr) => nr.status === 'completed' && nr.startedAt && nr.completedAt)
23
+ if (completedNodes.length > 0) {
24
+ const timings = completedNodes.map((nr) => ({
25
+ nodeId: nr.nodeId,
26
+ owner: ownerRef(nr.nodeId),
27
+ durationMs: new Date(nr.completedAt ?? 0).getTime() - new Date(nr.startedAt ?? 0).getTime(),
28
+ }))
29
+
30
+ const record = await this.persistPattern({
31
+ organizationId: orgRef,
32
+ type: 'timing-pattern',
33
+ pattern: { runId: params.runId, timings },
34
+ confidence: 0.7,
35
+ sampleCount: completedNodes.length,
36
+ })
37
+ patterns.push(record)
38
+ }
39
+
40
+ const failedNodes = nodeRuns.filter((nr) => nr.status === 'failed')
41
+ if (failedNodes.length > 0) {
42
+ const failures = failedNodes.map((nr) => ({
43
+ nodeId: nr.nodeId,
44
+ owner: ownerRef(nr.nodeId),
45
+ attemptCount: nr.attemptCount,
46
+ }))
47
+
48
+ const record = await this.persistPattern({
49
+ organizationId: orgRef,
50
+ type: 'failure-pattern',
51
+ pattern: { runId: params.runId, failures },
52
+ confidence: 0.6,
53
+ sampleCount: failedNodes.length,
54
+ })
55
+ patterns.push(record)
56
+ }
57
+
58
+ const agentCompletions = new Map<string, number>()
59
+ for (const nr of completedNodes) {
60
+ const owner = ownerRef(nr.nodeId)
61
+ agentCompletions.set(owner, (agentCompletions.get(owner) ?? 0) + 1)
62
+ }
63
+ if (agentCompletions.size > 0) {
64
+ const record = await this.persistPattern({
65
+ organizationId: orgRef,
66
+ type: 'agent-affinity',
67
+ pattern: { runId: params.runId, completions: Object.fromEntries(agentCompletions) },
68
+ confidence: 0.5,
69
+ sampleCount: completedNodes.length,
70
+ })
71
+ patterns.push(record)
72
+ }
73
+
74
+ return patterns
75
+ }
76
+
77
+ async queryRelevant(params: {
78
+ organizationId: string
79
+ objective: string
80
+ limit?: number
81
+ }): Promise<InstitutionalMemory[]> {
82
+ const orgRef = ensureRecordId(params.organizationId, TABLES.ORGANIZATION)
83
+ const limit = params.limit ?? 10
84
+
85
+ // Fetch a broader set, then rank by relevance to the objective.
86
+ // SurrealDB does not support full-text scoring on FLEXIBLE object fields,
87
+ // so we over-fetch and rank in application code.
88
+ const fetchLimit = Math.max(limit * 3, 30)
89
+ const records = await databaseService.queryMany(
90
+ new BoundQuery(
91
+ `SELECT * FROM ${TABLES.INSTITUTIONAL_MEMORY}
92
+ WHERE organizationId = $orgId
93
+ ORDER BY confidence DESC, createdAt DESC
94
+ LIMIT $fetchLimit`,
95
+ { orgId: orgRef, fetchLimit },
96
+ ),
97
+ InstitutionalMemorySchema,
98
+ )
99
+
100
+ // Score each record by how many objective keywords appear in its pattern JSON
101
+ const objectiveTerms = params.objective
102
+ .toLowerCase()
103
+ .split(/\s+/)
104
+ .filter((t) => t.length > 2)
105
+
106
+ if (objectiveTerms.length === 0) return records.slice(0, limit)
107
+
108
+ const scored = records.map((record) => {
109
+ const patternStr = JSON.stringify(record.pattern).toLowerCase()
110
+ const matchCount = objectiveTerms.filter((term) => patternStr.includes(term)).length
111
+ return { record, relevance: matchCount / objectiveTerms.length }
112
+ })
113
+
114
+ scored.sort((a, b) => {
115
+ // Primary: relevance score, secondary: confidence, tertiary: recency
116
+ if (b.relevance !== a.relevance) return b.relevance - a.relevance
117
+ if (b.record.confidence !== a.record.confidence) return b.record.confidence - a.record.confidence
118
+ return 0
119
+ })
120
+
121
+ return scored.slice(0, limit).map((s) => s.record)
122
+ }
123
+
124
+ private async persistPattern(params: {
125
+ organizationId: ReturnType<typeof ensureRecordId>
126
+ type: InstitutionalMemoryType
127
+ pattern: Record<string, unknown>
128
+ confidence: number
129
+ sampleCount: number
130
+ }): Promise<InstitutionalMemory> {
131
+ return databaseService.create(
132
+ TABLES.INSTITUTIONAL_MEMORY,
133
+ {
134
+ organizationId: params.organizationId,
135
+ type: params.type,
136
+ pattern: params.pattern,
137
+ confidence: params.confidence,
138
+ sampleCount: params.sampleCount,
139
+ },
140
+ InstitutionalMemorySchema,
141
+ )
142
+ }
143
+ }
144
+
145
+ export const institutionalMemoryService = new InstitutionalMemoryService()
@@ -5,7 +5,7 @@ import { z } from 'zod'
5
5
  import { renderLearnedSkillInstructions } from '../ai/definitions'
6
6
  import { lotaDebugLogger } from '../config/debug-logger'
7
7
  import { serverLogger } from '../config/logger'
8
- import { ensureRecordId } from '../db/record-id'
8
+ import { ensureRecordId, recordIdToString } from '../db/record-id'
9
9
  import { databaseService } from '../db/service'
10
10
  import { TABLES } from '../db/tables'
11
11
  import { getDefaultEmbeddings } from '../embeddings/provider'
@@ -124,7 +124,7 @@ class LearnedSkillService {
124
124
 
125
125
  async update(skillId: string, input: UpdateLearnedSkillInput): Promise<LearnedSkillRow> {
126
126
  const ref = ensureRecordId(skillId, TABLES.LEARNED_SKILL)
127
- const data: Record<string, unknown> = { updatedAt: new Date().toISOString() }
127
+ const data: Record<string, unknown> = {}
128
128
 
129
129
  if (input.name !== undefined) data.name = input.name
130
130
  if (input.description !== undefined) data.description = input.description
@@ -149,11 +149,11 @@ class LearnedSkillService {
149
149
  await databaseService.update(
150
150
  TABLES.LEARNED_SKILL,
151
151
  ref,
152
- { status: 'archived', archivedAt: new Date().toISOString(), updatedAt: new Date().toISOString() },
152
+ { status: 'archived', archivedAt: new Date() },
153
153
  LearnedSkillRowSchema,
154
154
  )
155
155
  if (skill) {
156
- await this.invalidateSkillExistsCache(String(skill.organizationId), skill.agentId ?? null)
156
+ await this.invalidateSkillExistsCache(recordIdToString(skill.organizationId), skill.agentId ?? null)
157
157
  }
158
158
  }
159
159
 
@@ -292,7 +292,7 @@ class LearnedSkillService {
292
292
  await databaseService.update(
293
293
  TABLES.LEARNED_SKILL,
294
294
  ref,
295
- { status: 'verified', confidence: Math.min(skill.confidence + 0.1, 1.0), updatedAt: new Date().toISOString() },
295
+ { status: 'verified', confidence: Math.min(skill.confidence + 0.1, 1.0) },
296
296
  LearnedSkillRowSchema,
297
297
  )
298
298
  return true
@@ -346,6 +346,25 @@ class LearnedSkillService {
346
346
  return hasher.digest('hex')
347
347
  }
348
348
 
349
+ async findByNameOrTag(orgId: string, nameOrTag: string): Promise<LearnedSkillRow | null> {
350
+ const orgRef = ensureRecordId(orgId, TABLES.ORGANIZATION)
351
+ const normalizedRef = nameOrTag.trim().toLowerCase()
352
+ const rows = await databaseService.queryMany(
353
+ new BoundQuery(
354
+ `SELECT *, type::string(id) AS id, type::string(organizationId) AS organizationId
355
+ FROM ${TABLES.LEARNED_SKILL}
356
+ WHERE organizationId = $organizationId
357
+ ${ACTIVE_SKILL_FILTER}
358
+ AND (string::lowercase(name) = $nameRef OR $nameRef IN tags)
359
+ ORDER BY confidence DESC
360
+ LIMIT 1`,
361
+ { organizationId: orgRef, nameRef: normalizedRef },
362
+ ),
363
+ LearnedSkillRowSchema,
364
+ )
365
+ return rows[0] ?? null
366
+ }
367
+
349
368
  async findByHash(orgId: string, hash: string): Promise<LearnedSkillRow | null> {
350
369
  const orgRef = ensureRecordId(orgId, TABLES.ORGANIZATION)
351
370
  const rows = await databaseService.queryMany(
@@ -3,13 +3,14 @@ import type { z } from 'zod'
3
3
  import { MemoryImportanceAssessmentSchema } from '../db/memory-types'
4
4
  import { createHelperModelRuntime } from '../runtime/helper-model'
5
5
  import { createOrgMemoryAgent } from '../system-agents/memory.agent'
6
+ import { clampImportance } from '../utils/string'
6
7
 
7
8
  type MemoryImportanceAssessment = z.infer<typeof MemoryImportanceAssessmentSchema>
8
9
  const MEMORY_IMPORTANCE_ASSESSMENT_TIMEOUT_MS = 10 * 60 * 1000
9
10
  const helperModelRuntime = createHelperModelRuntime()
10
11
 
11
12
  export function clampMemoryImportance(value: number): number {
12
- return Math.max(0.2, Math.min(0.95, value))
13
+ return clampImportance(Math.max(0.2, Math.min(0.95, value)))
13
14
  }
14
15
 
15
16
  export async function assessMemoryImportance(params: {
@@ -32,7 +33,7 @@ export async function assessMemoryImportance(params: {
32
33
  'Return only schema fields.',
33
34
  ].join('\n')
34
35
 
35
- return await helperModelRuntime.generateHelperStructured({
36
+ return helperModelRuntime.generateHelperStructured({
36
37
  tag: params.tag ?? 'memory-importance-assessment',
37
38
  createAgent: createOrgMemoryAgent,
38
39
  systemPrompt: 'You are a strict long-term memory quality assessor for an AI agent.',
@@ -1,7 +1,7 @@
1
1
  import { MEMORY } from '../config/constants'
2
2
  import { VECTOR_SEARCH_OVERFETCH_MULTIPLIER } from '../config/search'
3
3
  import type { MemorySearchResult } from '../db/memory-types'
4
- import { compactWhitespace } from '../utils/string'
4
+ import { compactWhitespace, truncateText } from '../utils/string'
5
5
  import type { MemoryRerankOutput } from './memory.service'
6
6
 
7
7
  export function getCandidateLimit(limit: number): number {
@@ -11,11 +11,7 @@ export function getCandidateLimit(limit: number): number {
11
11
  export function formatMemoryResults(results: MemorySearchResult[]): string {
12
12
  if (results.length === 0) return 'No stored memories.'
13
13
 
14
- const normalize = (value: string) => {
15
- const trimmed = compactWhitespace(value)
16
- if (trimmed.length <= 400) return trimmed
17
- return `${trimmed.slice(0, 400)}...`
18
- }
14
+ const normalize = (value: string) => truncateText(compactWhitespace(value), 400)
19
15
 
20
16
  return results
21
17
  .map((item) => {
@@ -61,8 +57,7 @@ export function formatRerankedResults(
61
57
  used.add(candidate.id)
62
58
  total += 1
63
59
  const reason = item.relevance ? ` — ${item.relevance}` : ''
64
- const trimmed = compactWhitespace(candidate.content)
65
- const content = trimmed.length <= 400 ? trimmed : `${trimmed.slice(0, 400)}...`
60
+ const content = truncateText(compactWhitespace(candidate.content), 400)
66
61
  lines.push(`- ${content}${reason}`)
67
62
  if (total >= limit) break
68
63
  }
@@ -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)
@@ -830,16 +819,10 @@ class MemoryService {
830
819
  const preparedUpdates = await orgMemory.prepareFactsToScopes(extractedFacts, scopes)
831
820
  if (preparedUpdates.length === 0) return
832
821
 
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
- }
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)`
843
826
  }
844
827
  }
845
828