@lota-sdk/core 0.1.14 → 0.1.15
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.
- package/package.json +5 -5
- package/src/ai/embedding-cache.ts +7 -6
- package/src/ai/index.ts +1 -0
- package/src/bifrost/bifrost.ts +12 -7
- package/src/config/agent-defaults.ts +1 -1
- package/src/config/logger.ts +7 -9
- package/src/{runtime.ts → create-runtime.ts} +6 -6
- package/src/db/cursor-pagination.ts +1 -1
- package/src/db/memory-store.ts +10 -6
- package/src/db/memory.ts +6 -4
- package/src/db/schema-fingerprint.ts +1 -0
- package/src/db/service.ts +40 -43
- package/src/db/startup.ts +3 -3
- package/src/index.ts +1 -1
- package/src/queues/context-compaction.queue.ts +4 -8
- package/src/queues/document-processor.queue.ts +7 -7
- package/src/queues/memory-consolidation.queue.ts +7 -8
- package/src/queues/post-chat-memory.queue.ts +2 -6
- package/src/queues/recent-activity-title-refinement.queue.ts +2 -6
- package/src/queues/regular-chat-memory-digest.queue.ts +4 -7
- package/src/queues/skill-extraction.queue.ts +4 -7
- package/src/queues/workstream-title-generation.queue.ts +2 -6
- package/src/redis/connection.ts +6 -3
- package/src/redis/index.ts +1 -0
- package/src/redis/org-memory-lock.ts +1 -1
- package/src/redis/redis-lease-lock.ts +41 -8
- package/src/runtime/agent-stream-helpers.ts +2 -1
- package/src/runtime/context-compaction-constants.ts +1 -1
- package/src/runtime/context-compaction-runtime.ts +6 -4
- package/src/runtime/context-compaction.ts +19 -38
- package/src/runtime/execution-plan.ts +2 -2
- package/src/runtime/helper-model.ts +3 -1
- package/src/runtime/index.ts +12 -1
- package/src/runtime/memory-block.ts +3 -2
- package/src/runtime/memory-pipeline.ts +24 -5
- package/src/runtime/plugin-types.ts +1 -1
- package/src/runtime/runtime-extensions.ts +89 -13
- package/src/runtime/title-helpers.ts +11 -2
- package/src/runtime/workstream-chat-helpers.ts +5 -6
- package/src/runtime/workstream-routing-policy.ts +0 -30
- package/src/runtime/workstream-state.ts +17 -7
- package/src/services/attachment.service.ts +1 -1
- package/src/services/context-compaction.service.ts +3 -3
- package/src/services/document-chunk.service.ts +37 -32
- package/src/services/execution-plan.service.ts +2 -0
- package/src/services/learned-skill.service.ts +6 -10
- package/src/services/{memory.utils.ts → memory-utils.ts} +4 -8
- package/src/services/memory.service.ts +21 -18
- package/src/services/organization-member.service.ts +1 -1
- package/src/services/plan-artifact.service.ts +1 -0
- package/src/services/plan-executor.service.ts +2 -18
- package/src/services/plan-helpers.ts +15 -0
- package/src/services/plan-validator.service.ts +3 -18
- package/src/services/recent-activity-title.service.ts +3 -10
- package/src/services/recent-activity.service.ts +6 -12
- package/src/services/workstream-message.service.ts +26 -16
- package/src/services/workstream-title.service.ts +1 -9
- package/src/services/{workstream-turn-preparation.ts → workstream-turn-preparation.service.ts} +401 -314
- package/src/services/workstream-turn.ts +2 -2
- package/src/services/workstream.service.ts +22 -10
- package/src/services/workstream.types.ts +7 -16
- package/src/storage/attachment-storage.service.ts +4 -4
- package/src/storage/{attachments.utils.ts → attachment-utils.ts} +1 -4
- package/src/storage/index.ts +2 -2
- package/src/system-agents/{context-compacter.agent.ts → context-compaction.agent.ts} +4 -4
- package/src/system-agents/delegated-agent-factory.ts +3 -2
- package/src/system-agents/index.ts +8 -0
- package/src/system-agents/memory-reranker.agent.ts +1 -1
- package/src/system-agents/memory.agent.ts +1 -1
- package/src/system-agents/recent-activity-title-refiner.agent.ts +1 -1
- package/src/tools/execution-plan.tool.ts +6 -2
- package/src/tools/fetch-webpage.tool.ts +20 -18
- package/src/tools/index.ts +2 -2
- package/src/tools/read-file-parts.tool.ts +1 -1
- package/src/tools/search-web.tool.ts +18 -15
- package/src/tools/{search-tools.ts → search.tool.ts} +1 -1
- package/src/tools/team-think.tool.ts +9 -5
- package/src/tools/{tool-contract.ts → tool-contracts.ts} +9 -2
- package/src/utils/async.ts +1 -1
- package/src/utils/errors.ts +15 -0
- package/src/utils/hono-error-handler.ts +1 -2
- package/src/utils/index.ts +10 -2
- package/src/utils/string.ts +14 -0
- package/src/workers/bootstrap.ts +2 -2
- package/src/workers/memory-consolidation.worker.ts +12 -12
- package/src/workers/regular-chat-memory-digest.helpers.ts +2 -7
- package/src/workers/regular-chat-memory-digest.runner.ts +9 -103
- package/src/workers/skill-extraction.runner.ts +7 -101
- package/src/workers/utils/file-section-chunker.ts +5 -3
- package/src/workers/utils/workstream-message-query.ts +106 -0
- package/src/workers/worker-utils.ts +4 -0
- package/src/runtime/retrieval-pipeline.ts +0 -3
- package/src/utils/error.ts +0 -10
- /package/src/services/{context-compaction-runtime.ts → context-compaction-runtime.singleton.ts} +0 -0
- /package/src/storage/{attachments.types.ts → attachment-types.ts} +0 -0
|
@@ -24,10 +24,12 @@ import {
|
|
|
24
24
|
scopedRetrievalToMap,
|
|
25
25
|
} from '../runtime/retrieval-adapters'
|
|
26
26
|
import { getRuntimeConfig } from '../runtime/runtime-config'
|
|
27
|
-
import { createMemoryRerankerAgent,
|
|
28
|
-
import { createOrgMemoryAgent,
|
|
27
|
+
import { createMemoryRerankerAgent, MEMORY_RERANKER_PROMPT } from '../system-agents/memory-reranker.agent'
|
|
28
|
+
import { createOrgMemoryAgent, ORG_MEMORY_PROMPT } from '../system-agents/memory.agent'
|
|
29
|
+
import { toError } from '../utils/errors'
|
|
30
|
+
import { compactWhitespace } from '../utils/string'
|
|
29
31
|
import { assessMemoryImportance, clampMemoryImportance } from './memory-assessment.service'
|
|
30
|
-
import { formatMemoryResults, formatRerankedResults, getCandidateLimit } from './memory
|
|
32
|
+
import { formatMemoryResults, formatRerankedResults, getCandidateLimit } from './memory-utils'
|
|
31
33
|
|
|
32
34
|
const ORG_MEMORY_TYPE = 'fact'
|
|
33
35
|
const RERANK_CANDIDATE_MAX_CHARS = 500
|
|
@@ -72,7 +74,7 @@ class MemoryService {
|
|
|
72
74
|
const cached = cache.get(cacheKey)
|
|
73
75
|
if (cached) return cached
|
|
74
76
|
|
|
75
|
-
const memory = new Memory({ createAgent: createOrgMemoryAgent }, { customPrompt:
|
|
77
|
+
const memory = new Memory({ createAgent: createOrgMemoryAgent }, { customPrompt: ORG_MEMORY_PROMPT })
|
|
76
78
|
|
|
77
79
|
cache.set(cacheKey, memory)
|
|
78
80
|
aiLogger.debug`Memory client created and cached for ${cacheKey}`
|
|
@@ -85,7 +87,7 @@ class MemoryService {
|
|
|
85
87
|
}
|
|
86
88
|
|
|
87
89
|
private truncateCandidateText(value: string): string {
|
|
88
|
-
const normalized = value
|
|
90
|
+
const normalized = compactWhitespace(value)
|
|
89
91
|
if (normalized.length <= RERANK_CANDIDATE_MAX_CHARS) return normalized
|
|
90
92
|
return `${normalized.slice(0, RERANK_CANDIDATE_MAX_CHARS)}...`
|
|
91
93
|
}
|
|
@@ -125,7 +127,7 @@ class MemoryService {
|
|
|
125
127
|
}
|
|
126
128
|
|
|
127
129
|
private normalizeConversationText(value: string, maxChars: number): string {
|
|
128
|
-
const normalized = value
|
|
130
|
+
const normalized = compactWhitespace(value)
|
|
129
131
|
if (!normalized) return ''
|
|
130
132
|
if (normalized.length <= maxChars) return normalized
|
|
131
133
|
return `${normalized.slice(0, maxChars - 3)}...`
|
|
@@ -146,7 +148,7 @@ class MemoryService {
|
|
|
146
148
|
}
|
|
147
149
|
|
|
148
150
|
private normalizePreSeededMemoryText(value: string): string {
|
|
149
|
-
const normalized = value
|
|
151
|
+
const normalized = compactWhitespace(value)
|
|
150
152
|
if (normalized.length <= PRESEEDED_MEMORY_MAX_CHARS) return normalized
|
|
151
153
|
return `${normalized.slice(0, PRESEEDED_MEMORY_MAX_CHARS - 3)}...`
|
|
152
154
|
}
|
|
@@ -232,7 +234,7 @@ class MemoryService {
|
|
|
232
234
|
return await helperModelRuntime.generateHelperStructured({
|
|
233
235
|
tag: 'memory-reranker',
|
|
234
236
|
createAgent: createMemoryRerankerAgent,
|
|
235
|
-
defaultSystemPrompt:
|
|
237
|
+
defaultSystemPrompt: MEMORY_RERANKER_PROMPT,
|
|
236
238
|
messages: [
|
|
237
239
|
{
|
|
238
240
|
role: 'user',
|
|
@@ -269,7 +271,7 @@ class MemoryService {
|
|
|
269
271
|
return await helperModelRuntime.generateHelperStructured({
|
|
270
272
|
tag: 'memory-reranker-multi-scope',
|
|
271
273
|
createAgent: createMemoryRerankerAgent,
|
|
272
|
-
defaultSystemPrompt:
|
|
274
|
+
defaultSystemPrompt: MEMORY_RERANKER_PROMPT,
|
|
273
275
|
messages: [
|
|
274
276
|
{
|
|
275
277
|
role: 'user',
|
|
@@ -334,7 +336,7 @@ class MemoryService {
|
|
|
334
336
|
aiLogger.debug`Organization memory search completed (resultLength: ${results.length}, preview: ${results.slice(0, 100)})`
|
|
335
337
|
return results
|
|
336
338
|
} catch (error: unknown) {
|
|
337
|
-
const normalizedError =
|
|
339
|
+
const normalizedError = toError(error)
|
|
338
340
|
aiLogger.error`Organization memory search failed: ${normalizedError}`
|
|
339
341
|
throw normalizedError
|
|
340
342
|
}
|
|
@@ -348,7 +350,8 @@ class MemoryService {
|
|
|
348
350
|
if (stale.length === 0) return ''
|
|
349
351
|
const items = stale.map((m) => `- [NEEDS REVIEW] ${m.content}`).join('\n')
|
|
350
352
|
return `Memories flagged for review (parent fact was superseded):\n${items}`
|
|
351
|
-
} catch {
|
|
353
|
+
} catch (error) {
|
|
354
|
+
aiLogger.warn`Failed to get stale memories: ${error}`
|
|
352
355
|
return ''
|
|
353
356
|
}
|
|
354
357
|
}
|
|
@@ -359,7 +362,7 @@ class MemoryService {
|
|
|
359
362
|
options?: { fastMode?: boolean; limit?: number },
|
|
360
363
|
): Promise<string> {
|
|
361
364
|
const orgScopeId = scopeId(ORG_SCOPE_PREFIX, orgId)
|
|
362
|
-
aiLogger.
|
|
365
|
+
aiLogger.debug`searchOrganizationMemoriesRaw - orgId: "${orgId}", scopeId: "${orgScopeId}"`
|
|
363
366
|
const memory = this.getOrgMemory(orgId)
|
|
364
367
|
const fastMode = options?.fastMode ?? true
|
|
365
368
|
const searchK = getRuntimeConfig().memory.searchK
|
|
@@ -376,7 +379,7 @@ class MemoryService {
|
|
|
376
379
|
aiLogger.debug`Organization memory search (raw) completed (candidates: ${candidates.length})`
|
|
377
380
|
return formatMemoryResults(candidates)
|
|
378
381
|
} catch (error: unknown) {
|
|
379
|
-
const normalizedError =
|
|
382
|
+
const normalizedError = toError(error)
|
|
380
383
|
aiLogger.error`Organization memory search (raw) failed: ${normalizedError}`
|
|
381
384
|
throw normalizedError
|
|
382
385
|
}
|
|
@@ -397,7 +400,7 @@ class MemoryService {
|
|
|
397
400
|
aiLogger.debug`Agent memory search completed (agentName: ${agentName}, resultLength: ${results.length}, preview: ${results.slice(0, 100)})`
|
|
398
401
|
return results
|
|
399
402
|
} catch (error: unknown) {
|
|
400
|
-
const normalizedError =
|
|
403
|
+
const normalizedError = toError(error)
|
|
401
404
|
aiLogger.error`Agent memory search failed: ${normalizedError}`
|
|
402
405
|
throw normalizedError
|
|
403
406
|
}
|
|
@@ -467,7 +470,7 @@ class MemoryService {
|
|
|
467
470
|
const deduped: MemoryRecord[] = []
|
|
468
471
|
const seen = new Set<string>()
|
|
469
472
|
for (const memory of combined) {
|
|
470
|
-
const normalizedKey = memory.content
|
|
473
|
+
const normalizedKey = compactWhitespace(memory.content).toLowerCase()
|
|
471
474
|
if (!normalizedKey || seen.has(normalizedKey)) continue
|
|
472
475
|
seen.add(normalizedKey)
|
|
473
476
|
deduped.push(memory)
|
|
@@ -594,7 +597,7 @@ class MemoryService {
|
|
|
594
597
|
durability?: MemoryRecord['durability']
|
|
595
598
|
}): Promise<string> {
|
|
596
599
|
const orgScopeId = scopeId(ORG_SCOPE_PREFIX, orgId)
|
|
597
|
-
aiLogger.
|
|
600
|
+
aiLogger.debug`createOrganizationMemory - orgId: "${orgId}", scopeId: "${orgScopeId}", content preview: "${content.slice(0, 50)}"`
|
|
598
601
|
const memory = this.getOrgMemory(orgId)
|
|
599
602
|
try {
|
|
600
603
|
return await memory.insert(content, {
|
|
@@ -762,7 +765,7 @@ class MemoryService {
|
|
|
762
765
|
}
|
|
763
766
|
|
|
764
767
|
const orgScopeId = scopeId(ORG_SCOPE_PREFIX, orgId)
|
|
765
|
-
aiLogger.
|
|
768
|
+
aiLogger.debug`addConversationMemories - orgId: "${orgId}", scopeId: "${orgScopeId}", sourceId: ${sourceId ?? 'none'}`
|
|
766
769
|
|
|
767
770
|
const orgMemory = this.getOrgMemory(orgId)
|
|
768
771
|
let assessedImportance: number | undefined
|
|
@@ -833,7 +836,7 @@ class MemoryService {
|
|
|
833
836
|
})
|
|
834
837
|
aiLogger.debug`Conversation memories added to ${scopes.length} scope(s) from ${messages.length} message(s)`
|
|
835
838
|
} catch (error: unknown) {
|
|
836
|
-
const normalizedError =
|
|
839
|
+
const normalizedError = toError(error)
|
|
837
840
|
aiLogger.error`Memory write failed: ${normalizedError}`
|
|
838
841
|
throw normalizedError
|
|
839
842
|
}
|
|
@@ -23,7 +23,7 @@ const sdkOrganizationMemberSchema = z.object({
|
|
|
23
23
|
createdAt: z.iso.datetime(),
|
|
24
24
|
})
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
type SdkOrganizationMemberRecord = z.infer<typeof organizationMemberRecordSchema>
|
|
27
27
|
export type SdkOrganizationMember = z.infer<typeof sdkOrganizationMemberSchema>
|
|
28
28
|
|
|
29
29
|
class OrganizationMemberService extends BaseService<typeof organizationMemberRecordSchema> {
|
|
@@ -17,6 +17,7 @@ class PlanArtifactService {
|
|
|
17
17
|
}): Promise<PlanArtifactRecord[]> {
|
|
18
18
|
const records: PlanArtifactRecord[] = []
|
|
19
19
|
|
|
20
|
+
// Sequential: SurrealDB transactions require ordered operations
|
|
20
21
|
for (const artifact of params.artifacts) {
|
|
21
22
|
const artifactId = new RecordId(TABLES.PLAN_ARTIFACT, Bun.randomUUIDv7())
|
|
22
23
|
const created = await params.tx
|
|
@@ -26,9 +26,11 @@ import { ensureRecordId, recordIdToString } from '../db/record-id'
|
|
|
26
26
|
import { databaseService } from '../db/service'
|
|
27
27
|
import type { DatabaseTransaction } from '../db/service'
|
|
28
28
|
import { TABLES } from '../db/tables'
|
|
29
|
+
import { isRecord } from '../utils/string'
|
|
29
30
|
import { planApprovalService } from './plan-approval.service'
|
|
30
31
|
import { planArtifactService } from './plan-artifact.service'
|
|
31
32
|
import { planCheckpointService } from './plan-checkpoint.service'
|
|
33
|
+
import { readPathValue } from './plan-helpers'
|
|
32
34
|
import { planRunService } from './plan-run.service'
|
|
33
35
|
import type { PlanValidationIssueInput } from './plan-validator.service'
|
|
34
36
|
import { planValidatorService } from './plan-validator.service'
|
|
@@ -37,24 +39,6 @@ const SUCCESSFUL_TERMINAL_NODE_STATUSES = new Set(['completed', 'partial', 'skip
|
|
|
37
39
|
const HUMAN_NODE_TYPES = new Set(['human-input', 'human-approval', 'human-review-edit', 'human-decision'])
|
|
38
40
|
const STRUCTURAL_NODE_TYPES = new Set(['switch', 'join'])
|
|
39
41
|
|
|
40
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
41
|
-
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function readPathValue(source: unknown, path: string): unknown {
|
|
45
|
-
if (!path.trim()) return source
|
|
46
|
-
|
|
47
|
-
let current: unknown = source
|
|
48
|
-
for (const segment of path
|
|
49
|
-
.split('.')
|
|
50
|
-
.map((part) => part.trim())
|
|
51
|
-
.filter(Boolean)) {
|
|
52
|
-
if (!isRecord(current)) return undefined
|
|
53
|
-
current = current[segment]
|
|
54
|
-
}
|
|
55
|
-
return current
|
|
56
|
-
}
|
|
57
|
-
|
|
58
42
|
function setPathValue(target: Record<string, unknown>, path: string, value: unknown) {
|
|
59
43
|
const segments = path
|
|
60
44
|
.split('.')
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { isRecord } from '../utils/string'
|
|
2
|
+
|
|
3
|
+
export function readPathValue(source: unknown, path: string): unknown {
|
|
4
|
+
if (!path.trim()) return source
|
|
5
|
+
|
|
6
|
+
let current: unknown = source
|
|
7
|
+
for (const segment of path
|
|
8
|
+
.split('.')
|
|
9
|
+
.map((part) => part.trim())
|
|
10
|
+
.filter(Boolean)) {
|
|
11
|
+
if (!isRecord(current)) return undefined
|
|
12
|
+
current = current[segment]
|
|
13
|
+
}
|
|
14
|
+
return current
|
|
15
|
+
}
|
|
@@ -8,6 +8,9 @@ import type {
|
|
|
8
8
|
PlanValidationIssueSeverity,
|
|
9
9
|
} from '@lota-sdk/shared'
|
|
10
10
|
|
|
11
|
+
import { isRecord } from '../utils/string'
|
|
12
|
+
import { readPathValue } from './plan-helpers'
|
|
13
|
+
|
|
11
14
|
const STRUCTURAL_NODE_TYPES = new Set(['switch', 'join'])
|
|
12
15
|
const HUMAN_NODE_TYPES = new Set(['human-input', 'human-approval', 'human-review-edit', 'human-decision'])
|
|
13
16
|
|
|
@@ -46,24 +49,6 @@ function createIssue(params: {
|
|
|
46
49
|
}
|
|
47
50
|
}
|
|
48
51
|
|
|
49
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
50
|
-
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function readPathValue(source: unknown, path: string): unknown {
|
|
54
|
-
if (!path.trim()) return source
|
|
55
|
-
|
|
56
|
-
let current: unknown = source
|
|
57
|
-
for (const segment of path
|
|
58
|
-
.split('.')
|
|
59
|
-
.map((part) => part.trim())
|
|
60
|
-
.filter(Boolean)) {
|
|
61
|
-
if (!isRecord(current)) return undefined
|
|
62
|
-
current = current[segment]
|
|
63
|
-
}
|
|
64
|
-
return current
|
|
65
|
-
}
|
|
66
|
-
|
|
67
52
|
function hasAllFields(value: unknown, fields: string[]): boolean {
|
|
68
53
|
if (!isRecord(value)) return false
|
|
69
54
|
return fields.every((field) => readPathValue(value, field) !== undefined)
|
|
@@ -1,20 +1,13 @@
|
|
|
1
1
|
import { createHelperModelRuntime } from '../runtime/helper-model'
|
|
2
|
+
import { normalizeTitle } from '../runtime/title-helpers'
|
|
2
3
|
import {
|
|
3
4
|
createRecentActivityTitleRefinerAgent,
|
|
4
|
-
|
|
5
|
+
RECENT_ACTIVITY_TITLE_REFINER_PROMPT,
|
|
5
6
|
} from '../system-agents/recent-activity-title-refiner.agent'
|
|
6
|
-
import { compactWhitespace } from '../utils/string'
|
|
7
7
|
import { recentActivityService } from './recent-activity.service'
|
|
8
8
|
|
|
9
9
|
const RECENT_ACTIVITY_TITLE_TIMEOUT_MS = 60_000
|
|
10
10
|
|
|
11
|
-
function normalizeTitle(value: string): string {
|
|
12
|
-
const normalized = compactWhitespace(value)
|
|
13
|
-
.replace(/^["'`]+|["'`]+$/g, '')
|
|
14
|
-
.replace(/[.!?,;:]+$/g, '')
|
|
15
|
-
return normalized.length <= 80 ? normalized : normalized.slice(0, 80).trim()
|
|
16
|
-
}
|
|
17
|
-
|
|
18
11
|
function buildRefinementPromptInput(
|
|
19
12
|
candidate: Awaited<ReturnType<typeof recentActivityService.getRefinementCandidate>>,
|
|
20
13
|
) {
|
|
@@ -52,7 +45,7 @@ class RecentActivityTitleService {
|
|
|
52
45
|
await this.helperRuntime.generateHelperText({
|
|
53
46
|
tag: 'recent-activity-title-refinement',
|
|
54
47
|
createAgent: createRecentActivityTitleRefinerAgent,
|
|
55
|
-
defaultSystemPrompt:
|
|
48
|
+
defaultSystemPrompt: RECENT_ACTIVITY_TITLE_REFINER_PROMPT,
|
|
56
49
|
timeoutMs: RECENT_ACTIVITY_TITLE_TIMEOUT_MS,
|
|
57
50
|
messages: [{ role: 'user', content: promptInput }],
|
|
58
51
|
}),
|
|
@@ -166,18 +166,12 @@ class RecentActivityService {
|
|
|
166
166
|
}): Promise<RecentActivity[]> {
|
|
167
167
|
await databaseService.connect()
|
|
168
168
|
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
event: candidate,
|
|
176
|
-
})
|
|
177
|
-
items.push(recorded.item)
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
return items
|
|
169
|
+
const results = await Promise.all(
|
|
170
|
+
params.events.map((candidate) =>
|
|
171
|
+
this.recordEvent({ orgId: params.orgId, userId: params.userId, source: params.source, event: candidate }),
|
|
172
|
+
),
|
|
173
|
+
)
|
|
174
|
+
return results.map((r) => r.item)
|
|
181
175
|
}
|
|
182
176
|
|
|
183
177
|
async recordEvent(params: {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
|
|
1
3
|
import { parseRowMetadata, toTimestamp, withCreatedAtMetadata } from '@lota-sdk/shared'
|
|
2
4
|
import type { ChatMessage } from '@lota-sdk/shared'
|
|
3
5
|
import { RecordId, surql } from 'surrealdb'
|
|
@@ -33,14 +35,21 @@ function toMessageId(value: string | RecordIdRef): string {
|
|
|
33
35
|
return recordIdToString(value, TABLES.WORKSTREAM_MESSAGE)
|
|
34
36
|
}
|
|
35
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Builds a collision-free row id by hashing the workstream + message id pair.
|
|
40
|
+
* Previous implementation replaced non-alphanumeric chars with '_', which was
|
|
41
|
+
* lossy (e.g. "msg:foo" and "msg_foo" mapped to the same row id).
|
|
42
|
+
* Now uses a 32-char SHA-256 hex prefix -- short enough for ergonomic ids,
|
|
43
|
+
* long enough (128 bits) to make collisions negligible.
|
|
44
|
+
*/
|
|
36
45
|
function toWorkstreamMessageRowId(workstreamId: RecordIdRef, messageId: string): RecordId {
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
return new RecordId(TABLES.WORKSTREAM_MESSAGE,
|
|
46
|
+
const workstreamStr = recordIdToString(workstreamId, TABLES.WORKSTREAM)
|
|
47
|
+
const digest = createHash('sha256').update(`${workstreamStr}\0${messageId}`).digest('hex').slice(0, 32)
|
|
48
|
+
return new RecordId(TABLES.WORKSTREAM_MESSAGE, digest)
|
|
40
49
|
}
|
|
41
50
|
|
|
42
51
|
function toChatMessage(row: WorkstreamMessageRow): ChatMessage {
|
|
43
|
-
const rowCreatedAt = toTimestamp(row.createdAt)
|
|
52
|
+
const rowCreatedAt = toTimestamp(row.createdAt) ?? Date.now()
|
|
44
53
|
const metadata = withCreatedAtMetadata(parseRowMetadata(row.metadata), rowCreatedAt)
|
|
45
54
|
|
|
46
55
|
return { id: row.messageId, role: row.role, parts: (row.parts ?? []) as ChatMessage['parts'], metadata }
|
|
@@ -74,16 +83,16 @@ class WorkstreamMessageService {
|
|
|
74
83
|
async upsertMessages(params: { workstreamId: RecordIdRef; messages: ChatMessage[] }): Promise<void> {
|
|
75
84
|
const workstreamId = params.workstreamId
|
|
76
85
|
|
|
77
|
-
|
|
86
|
+
const upsertPromises = params.messages.map(async (message) => {
|
|
78
87
|
const messageId = message.id.trim()
|
|
79
|
-
if (!messageId)
|
|
88
|
+
if (!messageId) return
|
|
80
89
|
|
|
81
90
|
const role = message.role
|
|
82
91
|
const parts = Array.isArray(message.parts)
|
|
83
92
|
? message.parts.map((part) => structuredClone(part) as Record<string, unknown>)
|
|
84
93
|
: []
|
|
85
94
|
if (parts.length === 0) {
|
|
86
|
-
if (role === 'assistant')
|
|
95
|
+
if (role === 'assistant') return
|
|
87
96
|
throw new Error(`Refusing to persist workstream message "${messageId}" with empty parts`)
|
|
88
97
|
}
|
|
89
98
|
const rowId = toWorkstreamMessageRowId(workstreamId, messageId)
|
|
@@ -93,7 +102,9 @@ class WorkstreamMessageService {
|
|
|
93
102
|
WorkstreamMessageExistingRowSchema,
|
|
94
103
|
)
|
|
95
104
|
const persistedCreatedAt =
|
|
96
|
-
existingRow === null
|
|
105
|
+
existingRow === null
|
|
106
|
+
? (toTimestamp(message.metadata?.createdAt) ?? Date.now())
|
|
107
|
+
: (toTimestamp(existingRow.createdAt) ?? Date.now())
|
|
97
108
|
const metadata = withCreatedAtMetadata({ ...message.metadata, createdAt: persistedCreatedAt })
|
|
98
109
|
|
|
99
110
|
await databaseService.upsert(
|
|
@@ -105,12 +116,15 @@ class WorkstreamMessageService {
|
|
|
105
116
|
role,
|
|
106
117
|
parts,
|
|
107
118
|
metadata,
|
|
108
|
-
createdAt: existingRow
|
|
119
|
+
createdAt: existingRow
|
|
120
|
+
? new Date(toTimestamp(existingRow.createdAt) ?? Date.now())
|
|
121
|
+
: new Date(persistedCreatedAt),
|
|
109
122
|
},
|
|
110
123
|
WorkstreamMessageRowSchema,
|
|
111
124
|
{ mutation: 'content' },
|
|
112
125
|
)
|
|
113
|
-
}
|
|
126
|
+
})
|
|
127
|
+
await Promise.all(upsertPromises)
|
|
114
128
|
}
|
|
115
129
|
|
|
116
130
|
async listMessages(workstreamId: RecordIdRef): Promise<ChatMessage[]> {
|
|
@@ -151,7 +165,7 @@ class WorkstreamMessageService {
|
|
|
151
165
|
throw new Error(`Workstream cursor message not found: ${cursorMessageId}`)
|
|
152
166
|
}
|
|
153
167
|
|
|
154
|
-
const cursorCreatedAt = new Date(toTimestamp(cursorRow.createdAt))
|
|
168
|
+
const cursorCreatedAt = new Date(toTimestamp(cursorRow.createdAt) ?? Date.now())
|
|
155
169
|
const cursorId = toWorkstreamMessageRowId(workstreamId, cursorMessageId)
|
|
156
170
|
const rows = await databaseService.query<unknown>(surql`
|
|
157
171
|
SELECT * FROM workstreamMessage
|
|
@@ -195,7 +209,7 @@ class WorkstreamMessageService {
|
|
|
195
209
|
.map((message) => ({
|
|
196
210
|
id: message.id,
|
|
197
211
|
role: message.role as 'user' | 'assistant',
|
|
198
|
-
createdAt: new Date(toTimestamp(message.metadata?.createdAt)).toISOString(),
|
|
212
|
+
createdAt: new Date(toTimestamp(message.metadata?.createdAt) ?? Date.now()).toISOString(),
|
|
199
213
|
content: message.parts
|
|
200
214
|
.flatMap((part) => (part.type === 'text' && typeof part.text === 'string' ? [part.text] : []))
|
|
201
215
|
.join('\n')
|
|
@@ -273,10 +287,6 @@ class WorkstreamMessageService {
|
|
|
273
287
|
async listAllMessages(workstreamId: RecordIdRef): Promise<ChatMessage[]> {
|
|
274
288
|
return await this.listMessages(workstreamId)
|
|
275
289
|
}
|
|
276
|
-
|
|
277
|
-
async addAttachments(): Promise<void> {
|
|
278
|
-
// Attachments are no longer persisted via workstreamMessage service in AI SDK mode.
|
|
279
|
-
}
|
|
280
290
|
}
|
|
281
291
|
|
|
282
292
|
export const workstreamMessageService = new WorkstreamMessageService()
|
|
@@ -3,21 +3,13 @@ import { WORKSTREAM } from '@lota-sdk/shared'
|
|
|
3
3
|
import { chatLogger } from '../config/logger'
|
|
4
4
|
import type { RecordIdRef } from '../db/record-id'
|
|
5
5
|
import { createHelperModelRuntime } from '../runtime/helper-model'
|
|
6
|
-
import { deriveTitle, limitTitleWords } from '../runtime/title-helpers'
|
|
6
|
+
import { deriveTitle, limitTitleWords, normalizeTitle } from '../runtime/title-helpers'
|
|
7
7
|
import {
|
|
8
8
|
createWorkstreamTitleGeneratorAgent,
|
|
9
9
|
WORKSTREAM_TITLE_GENERATOR_PROMPT,
|
|
10
10
|
} from '../system-agents/title-generator.agent'
|
|
11
|
-
import { compactWhitespace } from '../utils/string'
|
|
12
11
|
import { workstreamService } from './workstream.service'
|
|
13
12
|
|
|
14
|
-
function normalizeTitle(value: string): string {
|
|
15
|
-
const normalized = compactWhitespace(value)
|
|
16
|
-
.replace(/^["'`]+|["'`]+$/g, '')
|
|
17
|
-
.replace(/[.!?,;:]+$/g, '')
|
|
18
|
-
return normalized.length <= 80 ? normalized : normalized.slice(0, 80).trim()
|
|
19
|
-
}
|
|
20
|
-
|
|
21
13
|
class WorkstreamTitleService {
|
|
22
14
|
helperRuntime = createHelperModelRuntime()
|
|
23
15
|
|