@lota-sdk/core 0.1.5

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 (153) hide show
  1. package/infrastructure/schema/00_workstream.surql +55 -0
  2. package/infrastructure/schema/01_memory.surql +47 -0
  3. package/infrastructure/schema/02_execution_plan.surql +62 -0
  4. package/infrastructure/schema/03_learned_skill.surql +32 -0
  5. package/infrastructure/schema/04_runtime_bootstrap.surql +8 -0
  6. package/package.json +128 -0
  7. package/src/ai/definitions.ts +308 -0
  8. package/src/bifrost/bifrost.ts +256 -0
  9. package/src/config/agent-defaults.ts +99 -0
  10. package/src/config/constants.ts +33 -0
  11. package/src/config/env-shapes.ts +122 -0
  12. package/src/config/logger.ts +29 -0
  13. package/src/config/model-constants.ts +31 -0
  14. package/src/config/search.ts +17 -0
  15. package/src/config/workstream-defaults.ts +68 -0
  16. package/src/db/base.service.ts +55 -0
  17. package/src/db/cursor-pagination.ts +73 -0
  18. package/src/db/memory-query-builder.ts +207 -0
  19. package/src/db/memory-store.helpers.ts +118 -0
  20. package/src/db/memory-store.rows.ts +29 -0
  21. package/src/db/memory-store.ts +974 -0
  22. package/src/db/memory-types.ts +193 -0
  23. package/src/db/memory.ts +505 -0
  24. package/src/db/record-id.ts +78 -0
  25. package/src/db/service.ts +932 -0
  26. package/src/db/startup.ts +152 -0
  27. package/src/db/tables.ts +20 -0
  28. package/src/document/org-document-chunking.ts +224 -0
  29. package/src/document/parsing.ts +40 -0
  30. package/src/embeddings/provider.ts +76 -0
  31. package/src/index.ts +302 -0
  32. package/src/queues/context-compaction.queue.ts +82 -0
  33. package/src/queues/document-processor.queue.ts +118 -0
  34. package/src/queues/memory-consolidation.queue.ts +65 -0
  35. package/src/queues/post-chat-memory.queue.ts +128 -0
  36. package/src/queues/recent-activity-title-refinement.queue.ts +69 -0
  37. package/src/queues/regular-chat-memory-digest.config.ts +12 -0
  38. package/src/queues/regular-chat-memory-digest.queue.ts +73 -0
  39. package/src/queues/skill-extraction.config.ts +9 -0
  40. package/src/queues/skill-extraction.queue.ts +62 -0
  41. package/src/redis/connection.ts +176 -0
  42. package/src/redis/index.ts +30 -0
  43. package/src/redis/org-memory-lock.ts +43 -0
  44. package/src/redis/redis-lease-lock.ts +158 -0
  45. package/src/runtime/agent-contract.ts +1 -0
  46. package/src/runtime/agent-prompt-context.ts +119 -0
  47. package/src/runtime/agent-runtime-policy.ts +192 -0
  48. package/src/runtime/agent-stream-helpers.ts +117 -0
  49. package/src/runtime/agent-types.ts +22 -0
  50. package/src/runtime/approval-continuation.ts +16 -0
  51. package/src/runtime/chat-attachments.ts +46 -0
  52. package/src/runtime/chat-message.ts +10 -0
  53. package/src/runtime/chat-request-routing.ts +21 -0
  54. package/src/runtime/chat-run-orchestration.ts +25 -0
  55. package/src/runtime/chat-run-registry.ts +20 -0
  56. package/src/runtime/chat-types.ts +18 -0
  57. package/src/runtime/context-compaction-constants.ts +11 -0
  58. package/src/runtime/context-compaction-runtime.ts +86 -0
  59. package/src/runtime/context-compaction.ts +909 -0
  60. package/src/runtime/execution-plan.ts +59 -0
  61. package/src/runtime/helper-model.ts +405 -0
  62. package/src/runtime/indexed-repositories-policy.ts +28 -0
  63. package/src/runtime/instruction-sections.ts +8 -0
  64. package/src/runtime/llm-content.ts +71 -0
  65. package/src/runtime/memory-block.ts +264 -0
  66. package/src/runtime/memory-digest-policy.ts +14 -0
  67. package/src/runtime/memory-format.ts +8 -0
  68. package/src/runtime/memory-pipeline.ts +570 -0
  69. package/src/runtime/memory-prompts-fact.ts +47 -0
  70. package/src/runtime/memory-prompts-parse.ts +3 -0
  71. package/src/runtime/memory-prompts-update.ts +37 -0
  72. package/src/runtime/memory-scope.ts +43 -0
  73. package/src/runtime/plugin-types.ts +10 -0
  74. package/src/runtime/retrieval-adapters.ts +25 -0
  75. package/src/runtime/retrieval-pipeline.ts +3 -0
  76. package/src/runtime/runtime-extensions.ts +154 -0
  77. package/src/runtime/skill-extraction-policy.ts +3 -0
  78. package/src/runtime/team-consultation-orchestrator.ts +245 -0
  79. package/src/runtime/team-consultation-prompts.ts +32 -0
  80. package/src/runtime/title-helpers.ts +12 -0
  81. package/src/runtime/turn-lifecycle.ts +28 -0
  82. package/src/runtime/workstream-chat-helpers.ts +187 -0
  83. package/src/runtime/workstream-routing-policy.ts +301 -0
  84. package/src/runtime/workstream-state.ts +261 -0
  85. package/src/services/attachment.service.ts +159 -0
  86. package/src/services/chat-attachments.service.ts +17 -0
  87. package/src/services/chat-run-registry.service.ts +3 -0
  88. package/src/services/context-compaction-runtime.ts +13 -0
  89. package/src/services/context-compaction.service.ts +115 -0
  90. package/src/services/document-chunk.service.ts +141 -0
  91. package/src/services/execution-plan.service.ts +890 -0
  92. package/src/services/learned-skill.service.ts +328 -0
  93. package/src/services/memory-assessment.service.ts +43 -0
  94. package/src/services/memory.service.ts +807 -0
  95. package/src/services/memory.utils.ts +84 -0
  96. package/src/services/mutating-approval.service.ts +110 -0
  97. package/src/services/recent-activity-title.service.ts +74 -0
  98. package/src/services/recent-activity.service.ts +397 -0
  99. package/src/services/workstream-change-tracker.service.ts +313 -0
  100. package/src/services/workstream-message.service.ts +283 -0
  101. package/src/services/workstream-title.service.ts +58 -0
  102. package/src/services/workstream-turn-preparation.ts +1340 -0
  103. package/src/services/workstream-turn.ts +37 -0
  104. package/src/services/workstream.service.ts +854 -0
  105. package/src/services/workstream.types.ts +118 -0
  106. package/src/storage/attachment-parser.ts +101 -0
  107. package/src/storage/attachment-storage.service.ts +391 -0
  108. package/src/storage/attachments.types.ts +11 -0
  109. package/src/storage/attachments.utils.ts +58 -0
  110. package/src/storage/generated-document-storage.service.ts +55 -0
  111. package/src/system-agents/agent-result.ts +27 -0
  112. package/src/system-agents/context-compacter.agent.ts +46 -0
  113. package/src/system-agents/delegated-agent-factory.ts +177 -0
  114. package/src/system-agents/helper-agent-options.ts +20 -0
  115. package/src/system-agents/memory-reranker.agent.ts +38 -0
  116. package/src/system-agents/memory.agent.ts +58 -0
  117. package/src/system-agents/recent-activity-title-refiner.agent.ts +53 -0
  118. package/src/system-agents/regular-chat-memory-digest.agent.ts +75 -0
  119. package/src/system-agents/researcher.agent.ts +34 -0
  120. package/src/system-agents/skill-extractor.agent.ts +88 -0
  121. package/src/system-agents/skill-manager.agent.ts +80 -0
  122. package/src/system-agents/title-generator.agent.ts +42 -0
  123. package/src/system-agents/workstream-tracker.agent.ts +58 -0
  124. package/src/tools/execution-plan.tool.ts +163 -0
  125. package/src/tools/fetch-webpage.tool.ts +132 -0
  126. package/src/tools/firecrawl-client.ts +12 -0
  127. package/src/tools/memory-block.tool.ts +55 -0
  128. package/src/tools/read-file-parts.tool.ts +80 -0
  129. package/src/tools/remember-memory.tool.ts +85 -0
  130. package/src/tools/research-topic.tool.ts +15 -0
  131. package/src/tools/search-tools.ts +55 -0
  132. package/src/tools/search-web.tool.ts +175 -0
  133. package/src/tools/team-think.tool.ts +125 -0
  134. package/src/tools/tool-contract.ts +21 -0
  135. package/src/tools/user-questions.tool.ts +18 -0
  136. package/src/utils/async.ts +50 -0
  137. package/src/utils/date-time.ts +34 -0
  138. package/src/utils/error.ts +10 -0
  139. package/src/utils/errors.ts +28 -0
  140. package/src/utils/hono-error-handler.ts +71 -0
  141. package/src/utils/string.ts +51 -0
  142. package/src/workers/bootstrap.ts +44 -0
  143. package/src/workers/memory-consolidation.worker.ts +318 -0
  144. package/src/workers/regular-chat-memory-digest.helpers.ts +100 -0
  145. package/src/workers/regular-chat-memory-digest.runner.ts +363 -0
  146. package/src/workers/regular-chat-memory-digest.worker.ts +22 -0
  147. package/src/workers/skill-extraction.runner.ts +331 -0
  148. package/src/workers/skill-extraction.worker.ts +22 -0
  149. package/src/workers/utils/repo-indexer-chunker.ts +331 -0
  150. package/src/workers/utils/repo-structure-extractor.ts +645 -0
  151. package/src/workers/utils/repomix-process-concurrency.ts +65 -0
  152. package/src/workers/utils/sandbox-error.ts +5 -0
  153. package/src/workers/worker-utils.ts +182 -0
@@ -0,0 +1,71 @@
1
+ import type { ErrorHandler } from 'hono'
2
+ import { HTTPException } from 'hono/http-exception'
3
+ import { ZodError } from 'zod'
4
+
5
+ import { getErrorMessage } from './error'
6
+ import { AppError } from './errors'
7
+
8
+ type AppErrorLike = Pick<AppError, 'code' | 'message' | 'statusCode' | 'toResponse'> & { name?: string }
9
+
10
+ type HonoErrorLogger = Pick<Console, 'warn' | 'error'>
11
+
12
+ function isAppErrorLike(error: unknown): error is AppErrorLike {
13
+ if (!error || typeof error !== 'object') {
14
+ return false
15
+ }
16
+
17
+ const candidate = error as Partial<AppErrorLike>
18
+ return (
19
+ typeof candidate.code === 'string' &&
20
+ typeof candidate.message === 'string' &&
21
+ typeof candidate.statusCode === 'number' &&
22
+ typeof candidate.toResponse === 'function'
23
+ )
24
+ }
25
+
26
+ function createValidationErrorResponse(issues: Array<{ path: string; message: string }>) {
27
+ return { error: { code: 'VALIDATION_ERROR', message: 'Validation failed', issues } }
28
+ }
29
+
30
+ function createHttpErrorResponse(message: string) {
31
+ return { error: { code: 'HTTP_ERROR', message } }
32
+ }
33
+
34
+ function createServerErrorResponse(message: string) {
35
+ return { error: { code: 'INTERNAL_SERVER_ERROR', message } }
36
+ }
37
+
38
+ export function createHonoErrorHandler(logger: HonoErrorLogger): ErrorHandler {
39
+ return (error, c) => {
40
+ const appError = error instanceof AppError || isAppErrorLike(error) ? error : null
41
+
42
+ if (appError) {
43
+ const log = appError.statusCode >= 500 ? logger.error : logger.warn
44
+ const errorName = typeof appError.name === 'string' && appError.name.length > 0 ? appError.name : 'AppError'
45
+ log(`Request failed: ${errorName} (${appError.code}) ${appError.message}`)
46
+ const { status, body } = appError.toResponse()
47
+ const typedStatus = status as Parameters<typeof c.json>[1]
48
+ return c.json(body, typedStatus)
49
+ }
50
+
51
+ if (error instanceof HTTPException) {
52
+ const log = error.status >= 500 ? logger.error : logger.warn
53
+ log(`Request failed: HTTPException ${error.status} ${error.message}`)
54
+ return error.res ?? c.json(createHttpErrorResponse(error.message), error.status)
55
+ }
56
+
57
+ if (error instanceof ZodError) {
58
+ logger.warn(`Request failed: ZodError ${error.message}`)
59
+ const issues = error.issues.map((issue) => ({ path: issue.path.join('.'), message: issue.message }))
60
+ return c.json(createValidationErrorResponse(issues), 400)
61
+ }
62
+
63
+ if (error instanceof Error) {
64
+ logger.error(`Server error: ${error.name} ${error.message}\n${error.stack ?? ''}`)
65
+ return c.json(createServerErrorResponse('Internal Server Error'), 500)
66
+ }
67
+
68
+ logger.error(`Server error: ${getErrorMessage(error)}`)
69
+ return c.json(createServerErrorResponse('Internal Server Error'), 500)
70
+ }
71
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Returns true if the value is a non-null, non-array object (i.e. a plain record).
3
+ */
4
+ export function isRecord(value: unknown): value is Record<string, unknown> {
5
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
6
+ }
7
+
8
+ /**
9
+ * Reads a trimmed, non-empty string from an unknown value.
10
+ * Returns null for non-strings and empty/whitespace-only strings.
11
+ */
12
+ export function readString(value: unknown): string | null {
13
+ if (typeof value !== 'string') return null
14
+ const trimmed = value.trim()
15
+ return trimmed.length > 0 ? trimmed : null
16
+ }
17
+
18
+ /**
19
+ * Reads a trimmed, non-empty string from a record field by key.
20
+ * Returns undefined when the field is missing or not a non-empty string.
21
+ */
22
+ export function readStringField(record: Record<string, unknown>, key: string): string | undefined {
23
+ const value = record[key]
24
+ return typeof value === 'string' ? value : undefined
25
+ }
26
+
27
+ /**
28
+ * Truncates a string to the given character limit, appending "..." when truncated.
29
+ * Returns the original string when it fits within maxChars.
30
+ */
31
+ export function truncateText(value: string, maxChars: number): string {
32
+ return value.length <= maxChars ? value : `${value.slice(0, maxChars).trimEnd()}...`
33
+ }
34
+
35
+ /**
36
+ * Truncates an optional string value with trimming. Returns undefined for
37
+ * falsy or whitespace-only input.
38
+ */
39
+ export function truncateOptionalText(value: string | undefined, maxChars: number): string | undefined {
40
+ if (!value) return undefined
41
+ const trimmed = value.trim()
42
+ if (!trimmed) return undefined
43
+ return trimmed.length <= maxChars ? trimmed : `${trimmed.slice(0, maxChars)}...`
44
+ }
45
+
46
+ /**
47
+ * Collapses all runs of whitespace into a single space and trims.
48
+ */
49
+ export function compactWhitespace(value: string): string {
50
+ return value.trim().replace(/\s+/g, ' ')
51
+ }
@@ -0,0 +1,44 @@
1
+ import { configureLogger, serverLogger } from '../config/logger'
2
+ import { databaseService } from '../db/service'
3
+ import { connectWithStartupRetry, waitForDatabaseBootstrap } from '../db/startup'
4
+ import { getConfiguredPluginDatabaseConnector } from '../runtime/runtime-extensions'
5
+
6
+ let sandboxedWorkerRuntimePromise: Promise<void> | null = null
7
+
8
+ export async function initializeSandboxedWorkerRuntime(): Promise<void> {
9
+ if (sandboxedWorkerRuntimePromise) {
10
+ await sandboxedWorkerRuntimePromise
11
+ return
12
+ }
13
+
14
+ sandboxedWorkerRuntimePromise = (async () => {
15
+ await configureLogger()
16
+
17
+ await connectWithStartupRetry({
18
+ connect: () => databaseService.connect(),
19
+ label: 'sandboxed worker AI database runtime',
20
+ logger: serverLogger,
21
+ })
22
+
23
+ await connectWithStartupRetry({
24
+ connect: async () => {
25
+ const connectPluginRuntimeDatabases = getConfiguredPluginDatabaseConnector()
26
+ if (connectPluginRuntimeDatabases) {
27
+ await connectPluginRuntimeDatabases()
28
+ }
29
+ },
30
+ label: 'sandboxed worker plugin database runtime',
31
+ logger: serverLogger,
32
+ })
33
+
34
+ await waitForDatabaseBootstrap({
35
+ databaseService,
36
+ expectedFingerprint: process.env.DB_SCHEMA_FINGERPRINT,
37
+ label: 'sandboxed worker runtime',
38
+ logger: serverLogger,
39
+ connect: () => databaseService.connect(),
40
+ })
41
+ })()
42
+
43
+ await sandboxedWorkerRuntimePromise
44
+ }
@@ -0,0 +1,318 @@
1
+ import type { SandboxedJob } from 'bullmq'
2
+ import { BoundQuery, eq, inside } from 'surrealdb'
3
+
4
+ import { serverLogger } from '../config/logger'
5
+ import { ensureRecordId, recordIdToString } from '../db/record-id'
6
+ import type { RecordIdInput } from '../db/record-id'
7
+ import { databaseService } from '../db/service'
8
+ import { TABLES } from '../db/tables'
9
+ import type { MemoryConsolidationJob } from '../queues/memory-consolidation.queue'
10
+ import { initializeSandboxedWorkerRuntime } from './bootstrap'
11
+ import { toSandboxedWorkerError } from './utils/sandbox-error'
12
+ import { createTracedWorkerProcessor } from './worker-utils'
13
+
14
+ await initializeSandboxedWorkerRuntime()
15
+
16
+ const MEMORY_TABLE = TABLES.MEMORY
17
+ const MEMORY_RELATION_TABLE = TABLES.MEMORY_RELATION
18
+ const MEMORY_HISTORY_TABLE = TABLES.MEMORY_HISTORY
19
+ const HARD_SIMILARITY_THRESHOLD = 0.95
20
+ const SOFT_SIMILARITY_THRESHOLD = 0.9
21
+ const MAX_MEMORIES_PER_SCOPE = 500
22
+
23
+ function toMemoryId(value: RecordIdInput): string {
24
+ return recordIdToString(value, TABLES.MEMORY)
25
+ }
26
+
27
+ function isContentSubsumed(shorter: string, longer: string): boolean {
28
+ const a = shorter.toLowerCase().trim()
29
+ const b = longer.toLowerCase().trim()
30
+ if (b.includes(a)) return true
31
+ const aWords = new Set(a.split(/\s+/))
32
+ const bWords = new Set(b.split(/\s+/))
33
+ let overlap = 0
34
+ for (const word of aWords) {
35
+ if (bWords.has(word)) overlap++
36
+ }
37
+ return aWords.size > 0 && overlap / aWords.size >= 0.8
38
+ }
39
+
40
+ async function deduplicateScope(scopeId: string): Promise<number> {
41
+ const memoryRows = await databaseService.query<{
42
+ id: RecordIdInput
43
+ content: string
44
+ importance: number
45
+ embedding: number[]
46
+ updatedAt?: string | Date | number | null
47
+ createdAt: string | Date | number
48
+ }>(
49
+ new BoundQuery(
50
+ `SELECT id, content, importance, embedding, updatedAt, createdAt
51
+ FROM ${MEMORY_TABLE}
52
+ WHERE scopeId = $scopeId AND archivedAt IS NONE
53
+ ORDER BY createdAt DESC
54
+ LIMIT $limit`,
55
+ { scopeId, limit: MAX_MEMORIES_PER_SCOPE },
56
+ ),
57
+ )
58
+
59
+ const memories = memoryRows.map((row) => ({ ...row, id: toMemoryId(row.id) }))
60
+ if (memories.length < 2) return 0
61
+
62
+ const archived = new Set<string>()
63
+ let mergeCount = 0
64
+
65
+ for (const memory of memories) {
66
+ if (archived.has(memory.id)) continue
67
+
68
+ const candidateLimit = 20
69
+ const neighborStatements = await databaseService.queryAll<{
70
+ id: RecordIdInput
71
+ content: string
72
+ importance: number
73
+ similarity: number
74
+ updatedAt?: string | Date | number | null
75
+ createdAt: string | Date | number
76
+ }>(
77
+ new BoundQuery(
78
+ `LET $candidateRows = (
79
+ SELECT
80
+ id,
81
+ vector::distance::knn() AS distance
82
+ FROM ${MEMORY_TABLE}
83
+ WHERE scopeId = $scopeId
84
+ AND archivedAt IS NONE
85
+ AND id != $memoryId
86
+ AND embedding <|${candidateLimit}|> $embedding
87
+ ORDER BY distance ASC
88
+ LIMIT ${candidateLimit}
89
+ );
90
+
91
+ SELECT
92
+ id,
93
+ content,
94
+ importance,
95
+ updatedAt,
96
+ createdAt,
97
+ vector::similarity::cosine(embedding, $embedding) AS similarity
98
+ FROM ${MEMORY_TABLE}
99
+ WHERE id IN $candidateRows.id
100
+ ORDER BY similarity DESC
101
+ LIMIT ${candidateLimit}`,
102
+ { scopeId, memoryId: ensureRecordId(memory.id, TABLES.MEMORY), embedding: memory.embedding },
103
+ ),
104
+ )
105
+
106
+ const neighbors = (neighborStatements.at(-1) ?? []).map((row) => ({ ...row, id: toMemoryId(row.id) }))
107
+
108
+ for (const neighbor of neighbors) {
109
+ if (neighbor.similarity < SOFT_SIMILARITY_THRESHOLD) break
110
+ if (archived.has(neighbor.id)) continue
111
+
112
+ const isHardMatch = neighbor.similarity >= HARD_SIMILARITY_THRESHOLD
113
+ if (!isHardMatch) {
114
+ const [shorter, longer] =
115
+ memory.content.length <= neighbor.content.length
116
+ ? [memory.content, neighbor.content]
117
+ : [neighbor.content, memory.content]
118
+ if (!isContentSubsumed(shorter, longer)) continue
119
+ }
120
+
121
+ const memoryCreatedAt = new Date(memory.createdAt).getTime()
122
+ const neighborCreatedAt = new Date(neighbor.createdAt).getTime()
123
+ const keepMemory =
124
+ memory.importance > neighbor.importance ||
125
+ (memory.importance === neighbor.importance && memoryCreatedAt >= neighborCreatedAt)
126
+ const winner = keepMemory ? memory : neighbor
127
+ const loser = keepMemory ? neighbor : memory
128
+
129
+ await databaseService.relate(
130
+ ensureRecordId(winner.id, TABLES.MEMORY),
131
+ MEMORY_RELATION_TABLE,
132
+ ensureRecordId(loser.id, TABLES.MEMORY),
133
+ { relationType: 'supersedes', confidence: 1.0 },
134
+ )
135
+
136
+ await databaseService.query(
137
+ new BoundQuery(
138
+ `UPDATE ${MEMORY_TABLE}
139
+ SET archivedAt = time::now(), validUntil = time::now()
140
+ WHERE id = $loserId AND archivedAt IS NONE`,
141
+ { loserId: ensureRecordId(loser.id, TABLES.MEMORY) },
142
+ ),
143
+ )
144
+
145
+ await databaseService.insert<Record<string, unknown>>(MEMORY_HISTORY_TABLE, {
146
+ memoryId: ensureRecordId(loser.id, TABLES.MEMORY),
147
+ prevValue: loser.content,
148
+ event: 'DELETE',
149
+ })
150
+
151
+ archived.add(loser.id)
152
+ mergeCount++
153
+
154
+ if (!keepMemory) break
155
+ }
156
+ }
157
+
158
+ return mergeCount
159
+ }
160
+
161
+ async function pruneStaleMemories(): Promise<number> {
162
+ const stale = await databaseService.query<{ id: RecordIdInput }>(
163
+ new BoundQuery(
164
+ `SELECT id FROM ${MEMORY_TABLE}
165
+ WHERE accessCount = 0
166
+ AND createdAt < time::now() - 90d
167
+ AND archivedAt IS NONE
168
+ AND importance < 0.5
169
+ LIMIT 200`,
170
+ ),
171
+ )
172
+
173
+ if (stale.length === 0) return 0
174
+
175
+ const staleIds = stale.map((row) => ensureRecordId(row.id, TABLES.MEMORY))
176
+ await databaseService.updateWhere(MEMORY_TABLE, inside('id', staleIds), { archivedAt: new Date() })
177
+
178
+ return stale.length
179
+ }
180
+
181
+ async function collapseSupersedeCh(): Promise<number> {
182
+ const middleNodes = await databaseService.query<{
183
+ middleId: RecordIdInput
184
+ predecessors: RecordIdInput[]
185
+ successors: RecordIdInput[]
186
+ }>(
187
+ new BoundQuery(
188
+ `SELECT
189
+ id AS middleId,
190
+ <-${MEMORY_RELATION_TABLE}[WHERE relationType = 'supersedes']<-${MEMORY_TABLE}.id AS predecessors,
191
+ ->${MEMORY_RELATION_TABLE}[WHERE relationType = 'supersedes']->${MEMORY_TABLE}.id AS successors
192
+ FROM ${MEMORY_TABLE}
193
+ WHERE archivedAt IS NONE
194
+ AND count(->${MEMORY_RELATION_TABLE}[WHERE relationType = 'supersedes']) > 0
195
+ AND count(<-${MEMORY_RELATION_TABLE}[WHERE relationType = 'supersedes']) > 0
196
+ LIMIT 100`,
197
+ ),
198
+ )
199
+
200
+ let collapsed = 0
201
+
202
+ for (const node of middleNodes) {
203
+ const predecessors = node.predecessors
204
+ const successors = node.successors
205
+
206
+ for (const predId of predecessors) {
207
+ for (const succId of successors) {
208
+ const predRef = ensureRecordId(predId, TABLES.MEMORY)
209
+ const succRef = ensureRecordId(succId, TABLES.MEMORY)
210
+ const existing = await databaseService.query<{ id: RecordIdInput }>(
211
+ new BoundQuery(
212
+ `SELECT id FROM ${MEMORY_RELATION_TABLE}
213
+ WHERE in = $predId AND out = $succId AND relationType = 'supersedes'
214
+ LIMIT 1`,
215
+ { predId: predRef, succId: succRef },
216
+ ),
217
+ )
218
+
219
+ if (existing.length === 0) {
220
+ await databaseService.relate(predRef, MEMORY_RELATION_TABLE, succRef, {
221
+ relationType: 'supersedes',
222
+ confidence: 1.0,
223
+ })
224
+ }
225
+ }
226
+ }
227
+
228
+ await databaseService.updateWhere(MEMORY_TABLE, eq('id', ensureRecordId(node.middleId, TABLES.MEMORY)), {
229
+ archivedAt: new Date(),
230
+ })
231
+
232
+ collapsed++
233
+ }
234
+
235
+ return collapsed
236
+ }
237
+
238
+ async function decayImportance(): Promise<number> {
239
+ const standardResult = await databaseService.query<{ count: number }>(
240
+ new BoundQuery(
241
+ `UPDATE ${MEMORY_TABLE}
242
+ SET importance = math::max([0.1, importance * 0.95])
243
+ WHERE lastAccessedAt IS NOT NONE
244
+ AND lastAccessedAt < time::now() - 30d
245
+ AND archivedAt IS NONE
246
+ AND importance > 0.1
247
+ AND (durability = 'standard' OR durability IS NONE)
248
+ RETURN count() AS count`,
249
+ ),
250
+ )
251
+
252
+ const ephemeralResult = await databaseService.query<{ count: number }>(
253
+ new BoundQuery(
254
+ `UPDATE ${MEMORY_TABLE}
255
+ SET importance = math::max([0.1, importance * 0.85])
256
+ WHERE lastAccessedAt IS NOT NONE
257
+ AND lastAccessedAt < time::now() - 30d
258
+ AND archivedAt IS NONE
259
+ AND importance > 0.1
260
+ AND durability = 'ephemeral'
261
+ RETURN count() AS count`,
262
+ ),
263
+ )
264
+
265
+ return (standardResult[0]?.count ?? 0) + (ephemeralResult[0]?.count ?? 0)
266
+ }
267
+
268
+ async function cleanupOrphanedRelations(): Promise<number> {
269
+ const result = await databaseService.query<{ count: number }>(
270
+ new BoundQuery(
271
+ `DELETE ${MEMORY_RELATION_TABLE}
272
+ WHERE in.archivedAt IS NOT NONE OR out.archivedAt IS NOT NONE
273
+ RETURN count() AS count`,
274
+ ),
275
+ )
276
+ return result[0]?.count ?? 0
277
+ }
278
+
279
+ async function pruneOldOrgActions(): Promise<number> {
280
+ const result = await databaseService.query<{ count: number }>(
281
+ new BoundQuery(`DELETE ${TABLES.ORG_ACTION} WHERE createdAt < time::now() - 90d RETURN count() AS count`),
282
+ )
283
+ return result[0]?.count ?? 0
284
+ }
285
+
286
+ const handler = async (job: SandboxedJob<MemoryConsolidationJob>) => {
287
+ const targetScope = job.data.scopeId
288
+
289
+ try {
290
+ let totalMerged = 0
291
+ if (targetScope) {
292
+ totalMerged = await deduplicateScope(targetScope)
293
+ } else {
294
+ const scopeIds = await databaseService.query<string>(
295
+ new BoundQuery(`SELECT VALUE scopeId FROM ${MEMORY_TABLE} WHERE archivedAt IS NONE GROUP BY scopeId`),
296
+ )
297
+
298
+ for (const scopeId of scopeIds) {
299
+ const merged = await deduplicateScope(scopeId)
300
+ totalMerged += merged
301
+ }
302
+ }
303
+
304
+ const pruned = await pruneStaleMemories()
305
+ const collapsed = await collapseSupersedeCh()
306
+ const decayed = await decayImportance()
307
+ const orphaned = await cleanupOrphanedRelations()
308
+ const prunedActions = await pruneOldOrgActions()
309
+
310
+ serverLogger.info`Memory consolidation complete (merged: ${totalMerged}, pruned: ${pruned}, collapsed: ${collapsed}, decayed: ${decayed}, orphaned relations: ${orphaned}, pruned actions: ${prunedActions})`
311
+ } catch (error) {
312
+ const serialized = toSandboxedWorkerError(error, 'Memory consolidation job failed')
313
+ serverLogger.error`${serialized.message}`
314
+ throw serialized
315
+ }
316
+ }
317
+
318
+ export default createTracedWorkerProcessor('memory-consolidation', handler)
@@ -0,0 +1,100 @@
1
+ import { isAgentName } from '../config/agent-defaults'
2
+
3
+ interface DigestMessageForTranscript {
4
+ source: 'workstream'
5
+ sourceId: string
6
+ role: 'system' | 'user' | 'assistant'
7
+ parts: Array<Record<string, unknown>>
8
+ metadata?: Record<string, unknown>
9
+ }
10
+
11
+ function normalizeWhitespace(value: string): string {
12
+ return value.replace(/\s+/g, ' ').trim()
13
+ }
14
+
15
+ function normalizeFilePartMetadata(part: Record<string, unknown>): string | null {
16
+ if (part.type !== 'file') return null
17
+
18
+ const filename = typeof part.filename === 'string' && part.filename.trim() ? part.filename.trim() : 'attachment'
19
+ const mediaType = typeof part.mediaType === 'string' && part.mediaType.trim() ? part.mediaType.trim() : 'unknown'
20
+ const storageKey = typeof part.storageKey === 'string' && part.storageKey.trim() ? part.storageKey.trim() : 'unknown'
21
+ const sizeBytes =
22
+ typeof part.sizeBytes === 'number' && Number.isFinite(part.sizeBytes) && part.sizeBytes >= 0
23
+ ? Math.trunc(part.sizeBytes)
24
+ : null
25
+
26
+ const sizeSegment = sizeBytes === null ? '' : `, sizeBytes=${sizeBytes}`
27
+ return `${filename} (${mediaType}${sizeSegment}, storageKey=${storageKey})`
28
+ }
29
+
30
+ function extractAssistantLabel(message: DigestMessageForTranscript): string {
31
+ const metadataAgentId =
32
+ message.metadata && typeof message.metadata.agentId === 'string'
33
+ ? message.metadata.agentId.trim().toLowerCase()
34
+ : ''
35
+ if (metadataAgentId && isAgentName(metadataAgentId)) {
36
+ return metadataAgentId
37
+ }
38
+
39
+ const metadataAgentName =
40
+ message.metadata && typeof message.metadata.agentName === 'string' ? message.metadata.agentName.trim() : ''
41
+ if (metadataAgentName) {
42
+ return metadataAgentName
43
+ }
44
+
45
+ return 'assistant'
46
+ }
47
+
48
+ export function buildDigestTranscript(params: { messages: DigestMessageForTranscript[] }): {
49
+ transcript: string
50
+ involvedAgentNames: string[]
51
+ } {
52
+ const lines: string[] = []
53
+ const involvedAgentNames = new Set<string>()
54
+
55
+ for (const message of params.messages) {
56
+ if (message.role !== 'user' && message.role !== 'assistant') continue
57
+
58
+ const sourcePrefix = `[${message.source}:${message.sourceId}]`
59
+ const textParts = message.parts
60
+ .flatMap((part) =>
61
+ part.type === 'text' && typeof part.text === 'string' ? [normalizeWhitespace(part.text)] : [],
62
+ )
63
+ .filter((value) => value.length > 0)
64
+ const fileParts = message.parts
65
+ .map((part) => normalizeFilePartMetadata(part))
66
+ .filter((value): value is string => typeof value === 'string' && value.length > 0)
67
+
68
+ if (message.role === 'user') {
69
+ for (const textPart of textParts) {
70
+ lines.push(`${sourcePrefix} User: ${textPart}`)
71
+ }
72
+ if (fileParts.length > 0) {
73
+ lines.push(`${sourcePrefix} User files: ${fileParts.join('; ')}`)
74
+ }
75
+ continue
76
+ }
77
+
78
+ const assistantLabel = extractAssistantLabel(message)
79
+ if (isAgentName(assistantLabel)) {
80
+ involvedAgentNames.add(assistantLabel)
81
+ }
82
+
83
+ for (const textPart of textParts) {
84
+ lines.push(`${sourcePrefix} [${assistantLabel}] ${textPart}`)
85
+ }
86
+ if (fileParts.length > 0) {
87
+ lines.push(`${sourcePrefix} [${assistantLabel}] files: ${fileParts.join('; ')}`)
88
+ }
89
+ }
90
+
91
+ return { transcript: lines.join('\n'), involvedAgentNames: [...involvedAgentNames] }
92
+ }
93
+
94
+ export function resolveWorkspaceBootstrapCutoff(params: {
95
+ hasExistingCursor: boolean
96
+ bootstrapCompletedAt?: Date | null
97
+ }): Date | null {
98
+ if (params.hasExistingCursor) return null
99
+ return params.bootstrapCompletedAt instanceof Date ? params.bootstrapCompletedAt : null
100
+ }