@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
@@ -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'
@@ -16,6 +16,7 @@ const embeddings = getDefaultEmbeddings()
16
16
  const PROMOTION_MIN_USES = 5
17
17
  const PROMOTION_MIN_SUCCESS_RATE = 0.6
18
18
 
19
+ const ACTIVE_SKILL_FILTER = "AND status IN ['learned', 'verified', 'promoted'] AND archivedAt IS NONE"
19
20
  const SKILL_EXISTS_TTL_SECONDS = 120
20
21
  const SKILL_EXISTS_KEY_PREFIX = 'skill-exists'
21
22
 
@@ -123,7 +124,7 @@ class LearnedSkillService {
123
124
 
124
125
  async update(skillId: string, input: UpdateLearnedSkillInput): Promise<LearnedSkillRow> {
125
126
  const ref = ensureRecordId(skillId, TABLES.LEARNED_SKILL)
126
- const data: Record<string, unknown> = { updatedAt: new Date().toISOString() }
127
+ const data: Record<string, unknown> = {}
127
128
 
128
129
  if (input.name !== undefined) data.name = input.name
129
130
  if (input.description !== undefined) data.description = input.description
@@ -148,11 +149,11 @@ class LearnedSkillService {
148
149
  await databaseService.update(
149
150
  TABLES.LEARNED_SKILL,
150
151
  ref,
151
- { status: 'archived', archivedAt: new Date().toISOString(), updatedAt: new Date().toISOString() },
152
+ { status: 'archived', archivedAt: new Date() },
152
153
  LearnedSkillRowSchema,
153
154
  )
154
155
  if (skill) {
155
- await this.invalidateSkillExistsCache(String(skill.organizationId), skill.agentId ?? null)
156
+ await this.invalidateSkillExistsCache(recordIdToString(skill.organizationId), skill.agentId ?? null)
156
157
  }
157
158
  }
158
159
 
@@ -176,8 +177,7 @@ class LearnedSkillService {
176
177
  new BoundQuery(
177
178
  `SELECT id FROM ${TABLES.LEARNED_SKILL}
178
179
  WHERE organizationId = $orgRef
179
- AND status IN ['learned', 'verified', 'promoted']
180
- AND archivedAt IS NONE
180
+ ${ACTIVE_SKILL_FILTER}
181
181
  AND (agentId IS NONE OR agentId = $agentId)
182
182
  LIMIT 1`,
183
183
  { orgRef, agentId },
@@ -227,8 +227,7 @@ class LearnedSkillService {
227
227
  vector::similarity::cosine(embedding, $embedding) AS similarity
228
228
  FROM ${TABLES.LEARNED_SKILL}
229
229
  WHERE organizationId = $organizationId
230
- AND status IN ['learned', 'verified', 'promoted']
231
- AND archivedAt IS NONE
230
+ ${ACTIVE_SKILL_FILTER}
232
231
  AND confidence >= $minConfidence
233
232
  AND (agentId IS NONE OR agentId = $agentId)
234
233
  AND embedding <|${params.limit}|> $embedding
@@ -293,7 +292,7 @@ class LearnedSkillService {
293
292
  await databaseService.update(
294
293
  TABLES.LEARNED_SKILL,
295
294
  ref,
296
- { 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) },
297
296
  LearnedSkillRowSchema,
298
297
  )
299
298
  return true
@@ -311,8 +310,7 @@ class LearnedSkillService {
311
310
  vector::similarity::cosine(embedding, $embedding) AS similarity
312
311
  FROM ${TABLES.LEARNED_SKILL}
313
312
  WHERE organizationId = $organizationId
314
- AND status IN ['learned', 'verified', 'promoted']
315
- AND archivedAt IS NONE
313
+ ${ACTIVE_SKILL_FILTER}
316
314
  AND embedding <|3|> $embedding
317
315
  ORDER BY similarity DESC
318
316
  LIMIT 1
@@ -333,8 +331,7 @@ class LearnedSkillService {
333
331
  `SELECT *, type::string(id) AS id, type::string(organizationId) AS organizationId
334
332
  FROM ${TABLES.LEARNED_SKILL}
335
333
  WHERE organizationId = $organizationId
336
- AND status IN ['learned', 'verified', 'promoted']
337
- AND archivedAt IS NONE
334
+ ${ACTIVE_SKILL_FILTER}
338
335
  ORDER BY createdAt DESC`,
339
336
  { organizationId: orgRef },
340
337
  ),
@@ -349,6 +346,25 @@ class LearnedSkillService {
349
346
  return hasher.digest('hex')
350
347
  }
351
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
+
352
368
  async findByHash(orgId: string, hash: string): Promise<LearnedSkillRow | null> {
353
369
  const orgRef = ensureRecordId(orgId, TABLES.ORGANIZATION)
354
370
  const rows = await databaseService.queryMany(
@@ -357,8 +373,7 @@ class LearnedSkillService {
357
373
  FROM ${TABLES.LEARNED_SKILL}
358
374
  WHERE organizationId = $organizationId
359
375
  AND hash = $hash
360
- AND status IN ['learned', 'verified', 'promoted']
361
- AND archivedAt IS NONE
376
+ ${ACTIVE_SKILL_FILTER}
362
377
  LIMIT 1`,
363
378
  { organizationId: orgRef, hash },
364
379
  ),
@@ -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,25 +1,17 @@
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 { resolveCandidateLimit } from '../runtime/retrieval-pipeline'
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 {
8
- return resolveCandidateLimit({
9
- limit,
10
- multiplier: VECTOR_SEARCH_OVERFETCH_MULTIPLIER,
11
- minimum: MEMORY.DEFAULT_CANDIDATE_LIMIT,
12
- })
8
+ return Math.max(limit * VECTOR_SEARCH_OVERFETCH_MULTIPLIER, MEMORY.DEFAULT_CANDIDATE_LIMIT)
13
9
  }
14
10
 
15
11
  export function formatMemoryResults(results: MemorySearchResult[]): string {
16
12
  if (results.length === 0) return 'No stored memories.'
17
13
 
18
- const normalize = (value: string) => {
19
- const trimmed = value.replace(/\s+/g, ' ').trim()
20
- if (trimmed.length <= 400) return trimmed
21
- return `${trimmed.slice(0, 400)}...`
22
- }
14
+ const normalize = (value: string) => truncateText(compactWhitespace(value), 400)
23
15
 
24
16
  return results
25
17
  .map((item) => {
@@ -65,8 +57,7 @@ export function formatRerankedResults(
65
57
  used.add(candidate.id)
66
58
  total += 1
67
59
  const reason = item.relevance ? ` — ${item.relevance}` : ''
68
- const trimmed = candidate.content.replace(/\s+/g, ' ').trim()
69
- const content = trimmed.length <= 400 ? trimmed : `${trimmed.slice(0, 400)}...`
60
+ const content = truncateText(compactWhitespace(candidate.content), 400)
70
61
  lines.push(`- ${content}${reason}`)
71
62
  if (total >= limit) break
72
63
  }