@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
@@ -1,8 +1,9 @@
1
1
  import { S3Client } from 'bun'
2
2
 
3
3
  import { getRuntimeConfig } from '../runtime/runtime-config'
4
+ import { toSafeSegment } from './attachment-utils'
4
5
 
5
- function toSafeSegment(value: string): string {
6
+ function toSafePathSegment(value: string): string {
6
7
  return value
7
8
  .trim()
8
9
  .replace(/\\/g, '/')
@@ -18,7 +19,7 @@ function buildGeneratedDocumentStorageKey(params: {
18
19
  }): string {
19
20
  const safeOrganizationId = toSafeSegment(params.organizationId)
20
21
  const safeNamespace = toSafeSegment(params.namespace)
21
- const safeRelativePath = toSafeSegment(params.relativePath)
22
+ const safeRelativePath = toSafePathSegment(params.relativePath)
22
23
  return `${safeOrganizationId}/generated/${safeNamespace}/${safeRelativePath}`
23
24
  }
24
25
 
@@ -3,6 +3,7 @@ import type { ModelMessage, LanguageModel, ToolLoopAgentSettings, ToolSet } from
3
3
  import { z } from 'zod'
4
4
 
5
5
  import type { ToolDefinition } from '../ai/definitions'
6
+ import { aiLogger } from '../config/logger'
6
7
  import { isRecord } from '../utils/string'
7
8
  import { assertSubstantiveAgentResult } from './agent-result'
8
9
 
@@ -136,6 +137,7 @@ async function generateSubstantiveDelegatedAgentResult(params: {
136
137
  try {
137
138
  return assertSubstantiveAgentResult(result.text, params.label)
138
139
  } catch (error) {
140
+ aiLogger.error`Delegated agent returned non-substantive result (label=${params.label}, attempt=${attempt + 1}, textLength=${result.text.length}, textPreview=${result.text.slice(0, 200)})`
139
141
  lastError = error
140
142
  if (params.abortSignal?.aborted) {
141
143
  throw error
@@ -1,35 +1,17 @@
1
1
  import {
2
2
  CreateExecutionPlanArgsSchema,
3
3
  GetActiveExecutionPlanArgsSchema,
4
+ ListExecutionPlansArgsSchema,
4
5
  ResumeExecutionPlanRunArgsSchema,
5
6
  ReplaceExecutionPlanArgsSchema,
6
7
  SubmitExecutionNodeResultArgsSchema,
8
+ getLatestExecutionPlanResult,
7
9
  } from '@lota-sdk/shared'
8
- import type { ExecutionPlanToolResultData } from '@lota-sdk/shared'
9
10
  import { tool } from 'ai'
10
11
 
11
12
  import type { RecordIdRef } from '../db/record-id'
12
13
  import { executionPlanService } from '../services/execution-plan.service'
13
14
 
14
- function isExecutionPlanResult(value: unknown): value is ExecutionPlanToolResultData {
15
- return value !== null && value !== undefined && typeof value === 'object' && 'hasPlan' in value
16
- }
17
-
18
- function getLatestExecutionPlanToolResult(output: unknown): ExecutionPlanToolResultData | undefined {
19
- if (isExecutionPlanResult(output)) {
20
- return output
21
- }
22
-
23
- if (Array.isArray(output)) {
24
- for (let index = output.length - 1; index >= 0; index -= 1) {
25
- const candidate = getLatestExecutionPlanToolResult(output[index])
26
- if (candidate) return candidate
27
- }
28
- }
29
-
30
- return undefined
31
- }
32
-
33
15
  export function createCreateExecutionPlanTool(params: {
34
16
  orgId: RecordIdRef
35
17
  workstreamId: RecordIdRef
@@ -45,6 +27,7 @@ export function createCreateExecutionPlanTool(params: {
45
27
  organizationId: params.orgId,
46
28
  workstreamId: params.workstreamId,
47
29
  leadAgentId: params.agentId,
30
+ dispatchMode: 'deferred',
48
31
  input,
49
32
  })
50
33
  params.onPlanChanged?.()
@@ -68,6 +51,7 @@ export function createReplaceExecutionPlanTool(params: {
68
51
  organizationId: params.orgId,
69
52
  workstreamId: params.workstreamId,
70
53
  leadAgentId: params.agentId,
54
+ dispatchMode: 'deferred',
71
55
  input,
72
56
  })
73
57
  params.onPlanChanged?.()
@@ -95,21 +79,31 @@ export function createSubmitExecutionNodeResultTool(params: {
95
79
  return result
96
80
  },
97
81
  toModelOutput: ({ output }) => {
98
- const result = getLatestExecutionPlanToolResult(output)
82
+ const result = getLatestExecutionPlanResult(output)
99
83
  const summary = result?.message?.trim()
100
84
  return { type: 'text', value: summary && summary.length > 0 ? summary : 'Execution node result submitted.' }
101
85
  },
102
86
  })
103
87
  }
104
88
 
105
- export function createGetActiveExecutionPlanTool(params: { workstreamId: RecordIdRef }) {
89
+ export function createListExecutionPlansTool(params: { workstreamId: RecordIdRef }) {
90
+ return tool({
91
+ description:
92
+ 'List all active execution plans for this workstream with summary info (title, status, objective, node counts). Use getExecutionPlanDetails to inspect a specific plan.',
93
+ inputSchema: ListExecutionPlansArgsSchema,
94
+ execute: async () => await executionPlanService.listActivePlanSummaries(params.workstreamId),
95
+ })
96
+ }
97
+
98
+ export function createGetExecutionPlanDetailsTool(params: { workstreamId: RecordIdRef }) {
106
99
  return tool({
107
100
  description:
108
- 'Load the active execution run for this workstream, including graph state, node contracts, recent events, approvals, artifacts, and checkpoints when requested.',
101
+ 'Load a specific execution run by runId, or the most recent active run if runId is omitted. Returns full graph state, node contracts, events, approvals, artifacts, and checkpoints.',
109
102
  inputSchema: GetActiveExecutionPlanArgsSchema,
110
103
  execute: async (input) =>
111
104
  await executionPlanService.getActivePlanToolResult({
112
105
  workstreamId: params.workstreamId,
106
+ runId: input.runId,
113
107
  includeEvents: input.includeEvents,
114
108
  includeArtifacts: input.includeArtifacts,
115
109
  includeApprovals: input.includeApprovals,
@@ -1,6 +1,5 @@
1
1
  export * from './execution-plan.tool'
2
2
  export * from './fetch-webpage.tool'
3
- export * from './log-hello-world.tool'
4
3
  export * from './memory-block.tool'
5
4
  export * from './read-file-parts.tool'
6
5
  export * from './remember-memory.tool'
@@ -27,7 +27,8 @@ async function buildTeamThinkAgentTools(
27
27
  return { tools: result.tools as Record<string, unknown> }
28
28
  }
29
29
 
30
- const TEAM_THINK_AGENT_MAX_RETRIES = 4
30
+ const TEAM_THINK_AGENT_MAX_RETRIES = 1
31
+ const TEAM_THINK_AGENT_MAX_STEPS = 3
31
32
 
32
33
  export function createTeamThinkTool(params: {
33
34
  historyMessages: ChatMessage[]
@@ -58,7 +59,7 @@ export function createTeamThinkTool(params: {
58
59
  mode: 'fixedWorkstreamMode',
59
60
  onboardingActive: false,
60
61
  linearInstalled: false,
61
- reasoningProfile: runParams.reasoningProfile,
62
+ reasoningProfile: 'fast',
62
63
  systemWorkspaceDetails: runParams.systemWorkspaceDetails,
63
64
  preSeededMemoriesSection: runParams.preSeededMemoriesSection,
64
65
  retrievedKnowledgeSection: runParams.retrievedKnowledgeSection,
@@ -75,7 +76,7 @@ export function createTeamThinkTool(params: {
75
76
  workspaceIdString: recordIdToString(params.orgId, TABLES.ORGANIZATION),
76
77
  workstreamId: params.workstreamId,
77
78
  githubInstalled: params.githubInstalled,
78
- provideRepoTool: params.provideRepoTool,
79
+ provideRepoTool: agentId !== 'mentor' && params.provideRepoTool,
79
80
  availableUploads: params.availableUploads,
80
81
  defaultRepoSections: params.defaultRepoSectionsByAgent[agentId],
81
82
  context: params.context,
@@ -83,7 +84,8 @@ export function createTeamThinkTool(params: {
83
84
  })
84
85
  const agentConfig = config as Record<string, unknown>
85
86
  const agentId_ = typeof agentConfig.id === 'string' ? agentConfig.id : agentId
86
- const maxSteps = typeof agentConfig.maxSteps === 'number' ? agentConfig.maxSteps : 10
87
+ const configuredMaxSteps = typeof agentConfig.maxSteps === 'number' ? agentConfig.maxSteps : 10
88
+ const maxSteps = Math.min(configuredMaxSteps, TEAM_THINK_AGENT_MAX_STEPS)
87
89
  const agent = createAgent[agentId_]({
88
90
  mode: 'fixedWorkstreamMode',
89
91
  tools,
@@ -1,3 +1,4 @@
1
+ import { serverLogger } from '../config/logger'
1
2
  import { getErrorMessage } from './errors'
2
3
 
3
4
  class TimeoutError extends Error {
@@ -41,7 +42,7 @@ export function createSafeEnqueue(logger: { warn: (message: string) => void }) {
41
42
  }
42
43
  }
43
44
 
44
- const _defaultSafeEnqueue = createSafeEnqueue({ warn: console.warn })
45
+ const _defaultSafeEnqueue = createSafeEnqueue({ warn: (message: string) => serverLogger.warn`${message}` })
45
46
  export function safeEnqueue<T>(
46
47
  operation: () => T | Promise<T>,
47
48
  options: { operationName: string; onError?: (error: unknown) => void; logPrefix?: string },
@@ -1,34 +1,6 @@
1
- export function toIsoDateTimeString(value: unknown): string {
2
- if (value instanceof Date) {
3
- return value.toISOString()
4
- }
1
+ export { toIsoDateTimeString, toOptionalIsoDateTimeString } from '@lota-sdk/shared'
5
2
 
6
- // Assume API boundaries already use ISO strings.
7
- if (typeof value === 'string') {
8
- return value
9
- }
10
-
11
- // Support unix timestamps (seconds or milliseconds).
12
- if (typeof value === 'number') {
13
- const millis = value < 1_000_000_000_000 ? value * 1000 : value
14
- return new Date(millis).toISOString()
15
- }
16
-
17
- // Support objects that expose toISOString (e.g., SurrealDB temporal types).
18
- if (value && typeof value === 'object') {
19
- const maybeToIso = (value as { toISOString?: unknown }).toISOString
20
- if (typeof maybeToIso === 'function') {
21
- return (value as { toISOString: () => string }).toISOString()
22
- }
23
- }
24
-
25
- return String(value)
26
- }
27
-
28
- export function toOptionalIsoDateTimeString(value: unknown): string | undefined {
29
- if (value === null || value === undefined || value === '') {
30
- return undefined
31
- }
32
-
33
- return toIsoDateTimeString(value)
3
+ export function toDatabaseDateTime(value: string | Date | null | undefined): Date | undefined {
4
+ if (value === null || value === undefined) return undefined
5
+ return value instanceof Date ? value : new Date(value)
34
6
  }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Reads a required environment variable. Throws if the variable is missing or empty.
3
+ */
4
+ export function getRequiredEnv(key: string): string {
5
+ const value = process.env[key]
6
+ if (!value) throw new Error(`Missing required env var: ${key}`)
7
+ return value
8
+ }
@@ -1,13 +1,4 @@
1
- export function getErrorMessage(error: unknown): string {
2
- if (error instanceof Error) return error.message
3
- if (typeof error === 'string') return error
4
-
5
- try {
6
- return JSON.stringify(error)
7
- } catch {
8
- return String(error)
9
- }
10
- }
1
+ export { getErrorMessage } from '@lota-sdk/shared'
11
2
 
12
3
  export function toError(value: unknown): Error {
13
4
  return value instanceof Error ? value : new Error(String(value))
@@ -41,3 +32,44 @@ export class BadRequestError extends AppError {
41
32
  super(message, 'BAD_REQUEST', 400)
42
33
  }
43
34
  }
35
+
36
+ type ErrorCode =
37
+ | 'UNAUTHORIZED'
38
+ | 'FORBIDDEN'
39
+ | 'NOT_FOUND'
40
+ | 'CONFLICT'
41
+ | 'BAD_REQUEST'
42
+ | 'VALIDATION_ERROR'
43
+ | 'INTERNAL_SERVER_ERROR'
44
+ | 'HTTP_ERROR'
45
+ | 'TOO_MANY_REQUESTS'
46
+
47
+ interface ValidationIssue {
48
+ path: string
49
+ message: string
50
+ }
51
+
52
+ export interface ErrorBody {
53
+ error: { code: ErrorCode; message: string; issues?: ValidationIssue[] }
54
+ }
55
+
56
+ function createErrorResponse(code: ErrorCode, message: string): ErrorBody {
57
+ return { error: { code, message } }
58
+ }
59
+
60
+ function createValidationErrorResponse(message: string, issues: ValidationIssue[]): ErrorBody {
61
+ return { error: { code: 'VALIDATION_ERROR', message, issues } }
62
+ }
63
+
64
+ export const errorResponses = {
65
+ unauthorized: (message = 'Invalid credentials') => createErrorResponse('UNAUTHORIZED', message),
66
+ forbidden: (message = 'Access denied') => createErrorResponse('FORBIDDEN', message),
67
+ notFound: (message: string) => createErrorResponse('NOT_FOUND', message),
68
+ conflict: (message: string) => createErrorResponse('CONFLICT', message),
69
+ badRequest: (message: string) => createErrorResponse('BAD_REQUEST', message),
70
+ tooManyRequests: (message = 'Too many requests') => createErrorResponse('TOO_MANY_REQUESTS', message),
71
+ serverError: (message = 'Internal server error') => createErrorResponse('INTERNAL_SERVER_ERROR', message),
72
+ validationError: (issues: ValidationIssue[], message = 'Validation failed') =>
73
+ createValidationErrorResponse(message, issues),
74
+ httpError: (message: string) => createErrorResponse('HTTP_ERROR', message),
75
+ } as const
@@ -1,14 +1,23 @@
1
1
  export * from './async'
2
2
  export * from './date-time'
3
+ export * from './env'
3
4
  export * from './errors'
4
5
  export * from './hono-error-handler'
5
6
  export * from './sse-keepalive'
6
7
  export {
7
8
  CHARS_PER_TOKEN_ESTIMATE,
9
+ clampImportance,
10
+ compactRecord,
8
11
  compactWhitespace,
9
12
  isRecord,
13
+ parseLineList,
14
+ readRecordArray,
10
15
  readString,
16
+ readStringArray,
11
17
  readStringField,
18
+ slugify,
19
+ stringifyLineList,
20
+ stringifyUnknown,
12
21
  truncateOptionalText,
13
22
  truncateText,
14
23
  } from './string'
@@ -29,7 +29,7 @@ export function readStringField(record: Record<string, unknown>, key: string): s
29
29
  * Returns the original string when it fits within maxChars.
30
30
  */
31
31
  export function truncateText(value: string, maxChars: number): string {
32
- return value.length <= maxChars ? value : `${value.slice(0, maxChars).trimEnd()}...`
32
+ return value.length <= maxChars ? value : `${value.slice(0, maxChars - 3).trimEnd()}...`
33
33
  }
34
34
 
35
35
  /**
@@ -59,7 +59,120 @@ export function readRecord(value: unknown): Record<string, unknown> | null {
59
59
  return value as Record<string, unknown>
60
60
  }
61
61
 
62
+ /**
63
+ * Strips null and undefined values from a record, returning a new object with only defined entries.
64
+ */
65
+ export function compactRecord(value: Record<string, unknown>): Record<string, unknown> {
66
+ return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== null && entry !== undefined))
67
+ }
68
+
69
+ /**
70
+ * Converts a string into a URL-safe slug: lowercase, non-alphanumeric runs
71
+ * replaced with hyphens, leading/trailing hyphens stripped, capped at 80 chars.
72
+ */
73
+ export function slugify(value: string): string {
74
+ return value
75
+ .trim()
76
+ .toLowerCase()
77
+ .replace(/[^a-z0-9]+/g, '-')
78
+ .replace(/(^-|-$)/g, '')
79
+ .slice(0, 80)
80
+ }
81
+
62
82
  /**
63
83
  * Rough character-to-token estimate used for context budget calculations.
64
84
  */
65
85
  export const CHARS_PER_TOKEN_ESTIMATE = 3
86
+
87
+ /**
88
+ * Converts an unknown value to a trimmed, non-empty string representation.
89
+ * Returns null for null, undefined, empty, or whitespace-only results.
90
+ *
91
+ * When maxChars is provided, the result is compacted and truncated.
92
+ * Without maxChars, strings are trimmed and objects are JSON-serialized.
93
+ */
94
+ export function stringifyUnknown(value: unknown, maxChars?: number): string | null {
95
+ if (value === null || value === undefined) return null
96
+
97
+ const raw = (() => {
98
+ if (typeof value === 'string') return value
99
+ if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') {
100
+ return String(value)
101
+ }
102
+ if (typeof value === 'symbol') return value.description ? `Symbol(${value.description})` : 'Symbol()'
103
+ if (typeof value === 'function') return value.name ? `[function ${value.name}]` : '[function anonymous]'
104
+ if (Array.isArray(value)) {
105
+ try {
106
+ return JSON.stringify(value)
107
+ } catch {
108
+ return `[array(${value.length})]`
109
+ }
110
+ }
111
+
112
+ try {
113
+ return JSON.stringify(value)
114
+ } catch {
115
+ const maybeName =
116
+ isRecord(value) && typeof (value as { constructor?: { name?: unknown } }).constructor?.name === 'string'
117
+ ? (value as { constructor?: { name?: string } }).constructor?.name
118
+ : 'Object'
119
+ return `[object ${maybeName}]`
120
+ }
121
+ })()
122
+
123
+ if (maxChars !== undefined) {
124
+ const normalized = compactWhitespace(raw)
125
+ if (!normalized) return null
126
+ return truncateText(normalized, maxChars)
127
+ }
128
+
129
+ const trimmed = raw.trim()
130
+ return trimmed.length > 0 ? trimmed : null
131
+ }
132
+
133
+ /**
134
+ * Clamps a numeric importance/confidence value to [0, 1].
135
+ * Returns 0 for non-finite values.
136
+ */
137
+ export function clampImportance(value: number): number {
138
+ if (!Number.isFinite(value)) return 0
139
+ return Math.max(0, Math.min(1, value))
140
+ }
141
+
142
+ /**
143
+ * Splits a string on newlines, trims each line, and filters empties.
144
+ */
145
+ export function parseLineList(value: string): string[] {
146
+ return value
147
+ .split('\n')
148
+ .map((entry) => entry.trim())
149
+ .filter((entry) => entry.length > 0)
150
+ }
151
+
152
+ /**
153
+ * Joins an array of values into a newline-separated string.
154
+ * Returns an empty string for non-array inputs.
155
+ */
156
+ export function stringifyLineList(value: unknown): string {
157
+ return Array.isArray(value) ? value.map(String).join('\n') : ''
158
+ }
159
+
160
+ /**
161
+ * Reads an array of strings from an unknown value.
162
+ * Returns an empty array when the input is not an array.
163
+ */
164
+ export function readStringArray(value: unknown): string[] {
165
+ if (!Array.isArray(value)) return []
166
+ return value.filter((entry): entry is string => typeof entry === 'string')
167
+ }
168
+
169
+ /**
170
+ * Reads an array of plain records from an unknown value.
171
+ * Returns an empty array when the input is not an array.
172
+ */
173
+ export function readRecordArray(value: unknown): Array<Record<string, unknown>> {
174
+ if (!Array.isArray(value)) return []
175
+ return value.filter(
176
+ (entry): entry is Record<string, unknown> => typeof entry === 'object' && entry !== null && !Array.isArray(entry),
177
+ )
178
+ }
@@ -5,3 +5,4 @@ export * from './utils/file-section-chunker'
5
5
  export * from './utils/repomix-file-sections'
6
6
  export * from './utils/repo-structure-extractor'
7
7
  export * from './utils/repomix-process-concurrency'
8
+ export * from './utils/sandbox-error'
@@ -130,7 +130,7 @@ async function hasNewEligibleWorkstreamMessages(params: {
130
130
  }
131
131
 
132
132
  async function loadExistingOrganizationMemories(orgId: string): Promise<Array<{ content: string }>> {
133
- return await databaseService.queryMany(
133
+ return databaseService.queryMany(
134
134
  new BoundQuery(
135
135
  `SELECT content, createdAt, id FROM ${TABLES.MEMORY}
136
136
  WHERE metadata.orgId = $orgId
@@ -155,7 +155,7 @@ export async function runRegularChatMemoryDigest(
155
155
  return { skipped: true, processedWorkstreamMessages: 0, followUpScheduled: false }
156
156
  }
157
157
 
158
- return await withConfiguredWorkspaceMemoryLock(orgId, async () => {
158
+ return withConfiguredWorkspaceMemoryLock(orgId, async () => {
159
159
  if (
160
160
  !workspaceProvider.getBackgroundCursor ||
161
161
  !workspaceProvider.setBackgroundCursor ||
@@ -86,7 +86,7 @@ export async function runSkillExtraction(data: SkillExtractionJob): Promise<Skil
86
86
  return { skipped: true, processedMessages: 0, extractedSkills: 0 }
87
87
  }
88
88
 
89
- return await withConfiguredWorkspaceMemoryLock(orgId, async () => {
89
+ return withConfiguredWorkspaceMemoryLock(orgId, async () => {
90
90
  const workspace = await cursorAwareWorkspaceProvider.getWorkspace(orgRef)
91
91
  const lifecycleState = await cursorAwareWorkspaceProvider.getLifecycleState?.(workspace)
92
92
  if (lifecycleState?.bootstrapActive ?? false) {
@@ -1,8 +1,9 @@
1
+ import { CHARS_PER_TOKEN_ESTIMATE } from '../../utils/string'
2
+
1
3
  export const DEFAULT_FILE_SECTION_CHUNK_MAX_CHARS = 250_000
2
4
  export const MIN_FILE_SECTION_CHUNK_MAX_CHARS = 4_000
3
5
  export const DEFAULT_FILE_SECTION_CHUNK_MIN_CHARS = 10_000
4
6
  const SECTION_SEPARATOR_LENGTH = 2
5
- const CHARS_PER_TOKEN_ESTIMATE = 3
6
7
  const MIN_CHUNK_CHARS_FLOOR = 512
7
8
 
8
9
  export interface FileSection {
@@ -27,7 +27,7 @@ export function parseRepomixFileSections(repomixOutput: string): FileSection[] {
27
27
  const nextStart = matches[index + 1]?.index ?? source.length
28
28
  const content = source.slice(start, nextStart).trim()
29
29
  if (!content) continue
30
- const filePath = (match[1] ?? '').trim()
30
+ const filePath = match[1].trim()
31
31
  sections.push({ kind: 'file', content, filePath: filePath || undefined })
32
32
  }
33
33
 
@@ -38,5 +38,5 @@ export async function chunkRepomixFileSections(
38
38
  repomixOutput: string,
39
39
  options: FileSectionChunkOptions = {},
40
40
  ): Promise<FileSectionChunk[]> {
41
- return await chunkFileSections(parseRepomixFileSections(repomixOutput), options)
41
+ return chunkFileSections(parseRepomixFileSections(repomixOutput), options)
42
42
  }
@@ -1,5 +1,14 @@
1
- export function toSandboxedWorkerError(error: unknown, context?: string): Error {
2
- const base = error instanceof Error ? error : new Error(String(error))
1
+ export interface SandboxedWorkerError {
2
+ name: string
3
+ message: string
4
+ stack?: string
5
+ }
6
+
7
+ export function toSandboxedWorkerError(error: unknown, context?: string): SandboxedWorkerError {
8
+ const base =
9
+ error instanceof Error
10
+ ? { name: error.name || 'Error', message: error.message, stack: error.stack }
11
+ : { name: 'Error', message: String(error) }
3
12
  if (context) base.message = `${context}: ${base.message}`
4
13
  return base
5
14
  }
@@ -1,27 +1,15 @@
1
- import { toTimestamp } from '@lota-sdk/shared'
1
+ import { requireTimestamp } from '@lota-sdk/shared'
2
2
  import { BoundQuery } from 'surrealdb'
3
3
  import { z } from 'zod'
4
4
 
5
- import { ensureRecordId } from '../../db/record-id'
5
+ import { ensureRecordId, recordIdToString } from '../../db/record-id'
6
6
  import type { RecordIdRef } from '../../db/record-id'
7
7
  import { databaseService } from '../../db/service'
8
8
  import { TABLES } from '../../db/tables'
9
+ import { WorkstreamMessageRowSchema } from '../../db/workstream-message-row'
10
+ import type { WorkstreamMessageRow } from '../../db/workstream-message-row'
9
11
  import { normalizeTextBody } from '../../document/parsing'
10
12
 
11
- const RecordTimestampSchema = z.union([z.date(), z.string(), z.number()])
12
- const MessageRoleSchema = z.enum(['system', 'user', 'assistant'])
13
- const MessagePartSchema = z.record(z.string(), z.unknown())
14
- const MessageMetadataSchema = z.record(z.string(), z.unknown()).nullish()
15
-
16
- const WorkstreamMessageRowSchema = z.object({
17
- id: z.string(),
18
- workstreamId: z.string(),
19
- role: MessageRoleSchema,
20
- parts: z.array(MessagePartSchema).optional(),
21
- metadata: MessageMetadataSchema,
22
- createdAt: RecordTimestampSchema,
23
- })
24
-
25
13
  export interface DigestCursor {
26
14
  createdAt: Date
27
15
  id: string
@@ -36,14 +24,17 @@ export interface DigestMessage {
36
24
  cursor: DigestCursor
37
25
  }
38
26
 
39
- function mapWorkstreamRow(row: z.infer<typeof WorkstreamMessageRowSchema>): DigestMessage {
27
+ function mapWorkstreamRow(row: WorkstreamMessageRow): DigestMessage {
40
28
  return {
41
29
  source: 'workstream',
42
- sourceId: row.workstreamId,
30
+ sourceId: recordIdToString(row.workstreamId, TABLES.WORKSTREAM),
43
31
  role: row.role,
44
- parts: row.parts ?? [],
32
+ parts: row.parts as Array<Record<string, unknown>>,
45
33
  metadata: row.metadata ?? undefined,
46
- cursor: { createdAt: new Date(toTimestamp(row.createdAt) ?? Date.now()), id: row.id },
34
+ cursor: {
35
+ createdAt: new Date(requireTimestamp(row.createdAt)),
36
+ id: recordIdToString(row.id, TABLES.WORKSTREAM_MESSAGE),
37
+ },
47
38
  }
48
39
  }
49
40
 
@@ -4,6 +4,7 @@ import { fileURLToPath } from 'node:url'
4
4
  import type { Job, Worker } from 'bullmq'
5
5
 
6
6
  import { chatLogger } from '../config/logger'
7
+ import { truncateText } from '../utils/string'
7
8
 
8
9
  export const DEFAULT_JOB_RETENTION = { removeOnComplete: 200, removeOnFail: 200 }
9
10
  export const LOW_JOB_RETENTION = { removeOnComplete: 50, removeOnFail: 50 }
@@ -32,8 +33,7 @@ interface TracedWorkerJobLike {
32
33
  }
33
34
 
34
35
  function truncateTraceString(value: string, maxChars = MAX_TRACE_STRING_CHARS): string {
35
- if (value.length <= maxChars) return value
36
- return `${value.slice(0, maxChars - 3)}...`
36
+ return truncateText(value, maxChars)
37
37
  }
38
38
 
39
39
  function normalizeTraceValue(value: unknown, depth = 0): unknown {
@@ -1,17 +0,0 @@
1
- import { tool } from 'ai'
2
- import { z } from 'zod'
3
-
4
- export function createLogHelloWorldTool() {
5
- return tool({
6
- description: 'Logs "Hello World" to the server console. Requires user approval before running.',
7
- inputSchema: z.object({
8
- message: z.string().optional().describe('Optional custom message to log alongside Hello World'),
9
- }),
10
- needsApproval: true,
11
- execute: async ({ message }: { message?: string }) => {
12
- const output = message ? `Hello World: ${message}` : 'Hello World'
13
- console.log('[logHelloWorld]', output)
14
- return { logged: output, timestamp: new Date().toISOString() }
15
- },
16
- })
17
- }