claude-memory-layer 1.0.0
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/.claude-plugin/commands/memory-forget.md +42 -0
- package/.claude-plugin/commands/memory-history.md +34 -0
- package/.claude-plugin/commands/memory-import.md +56 -0
- package/.claude-plugin/commands/memory-list.md +37 -0
- package/.claude-plugin/commands/memory-search.md +36 -0
- package/.claude-plugin/commands/memory-stats.md +34 -0
- package/.claude-plugin/hooks.json +59 -0
- package/.claude-plugin/plugin.json +24 -0
- package/.history/package_20260201112328.json +45 -0
- package/.history/package_20260201113602.json +45 -0
- package/.history/package_20260201113713.json +45 -0
- package/.history/package_20260201114110.json +45 -0
- package/Memo.txt +558 -0
- package/README.md +520 -0
- package/context.md +636 -0
- package/dist/.claude-plugin/commands/memory-forget.md +42 -0
- package/dist/.claude-plugin/commands/memory-history.md +34 -0
- package/dist/.claude-plugin/commands/memory-import.md +56 -0
- package/dist/.claude-plugin/commands/memory-list.md +37 -0
- package/dist/.claude-plugin/commands/memory-search.md +36 -0
- package/dist/.claude-plugin/commands/memory-stats.md +34 -0
- package/dist/.claude-plugin/hooks.json +59 -0
- package/dist/.claude-plugin/plugin.json +24 -0
- package/dist/cli/index.js +3539 -0
- package/dist/cli/index.js.map +7 -0
- package/dist/core/index.js +4408 -0
- package/dist/core/index.js.map +7 -0
- package/dist/hooks/session-end.js +2971 -0
- package/dist/hooks/session-end.js.map +7 -0
- package/dist/hooks/session-start.js +2969 -0
- package/dist/hooks/session-start.js.map +7 -0
- package/dist/hooks/stop.js +3123 -0
- package/dist/hooks/stop.js.map +7 -0
- package/dist/hooks/user-prompt-submit.js +2960 -0
- package/dist/hooks/user-prompt-submit.js.map +7 -0
- package/dist/services/memory-service.js +2931 -0
- package/dist/services/memory-service.js.map +7 -0
- package/package.json +45 -0
- package/plan.md +1642 -0
- package/scripts/build.ts +102 -0
- package/spec.md +624 -0
- package/specs/citations-system/context.md +243 -0
- package/specs/citations-system/plan.md +495 -0
- package/specs/citations-system/spec.md +371 -0
- package/specs/endless-mode/context.md +305 -0
- package/specs/endless-mode/plan.md +620 -0
- package/specs/endless-mode/spec.md +455 -0
- package/specs/entity-edge-model/context.md +401 -0
- package/specs/entity-edge-model/plan.md +459 -0
- package/specs/entity-edge-model/spec.md +391 -0
- package/specs/evidence-aligner-v2/context.md +401 -0
- package/specs/evidence-aligner-v2/plan.md +303 -0
- package/specs/evidence-aligner-v2/spec.md +312 -0
- package/specs/mcp-desktop-integration/context.md +278 -0
- package/specs/mcp-desktop-integration/plan.md +550 -0
- package/specs/mcp-desktop-integration/spec.md +494 -0
- package/specs/post-tool-use-hook/context.md +319 -0
- package/specs/post-tool-use-hook/plan.md +469 -0
- package/specs/post-tool-use-hook/spec.md +364 -0
- package/specs/private-tags/context.md +288 -0
- package/specs/private-tags/plan.md +412 -0
- package/specs/private-tags/spec.md +345 -0
- package/specs/progressive-disclosure/context.md +346 -0
- package/specs/progressive-disclosure/plan.md +663 -0
- package/specs/progressive-disclosure/spec.md +415 -0
- package/specs/task-entity-system/context.md +297 -0
- package/specs/task-entity-system/plan.md +301 -0
- package/specs/task-entity-system/spec.md +314 -0
- package/specs/vector-outbox-v2/context.md +470 -0
- package/specs/vector-outbox-v2/plan.md +562 -0
- package/specs/vector-outbox-v2/spec.md +466 -0
- package/specs/web-viewer-ui/context.md +384 -0
- package/specs/web-viewer-ui/plan.md +797 -0
- package/specs/web-viewer-ui/spec.md +516 -0
- package/src/cli/index.ts +570 -0
- package/src/core/canonical-key.ts +186 -0
- package/src/core/citation-generator.ts +63 -0
- package/src/core/consolidated-store.ts +279 -0
- package/src/core/consolidation-worker.ts +384 -0
- package/src/core/context-formatter.ts +276 -0
- package/src/core/continuity-manager.ts +336 -0
- package/src/core/edge-repo.ts +324 -0
- package/src/core/embedder.ts +124 -0
- package/src/core/entity-repo.ts +342 -0
- package/src/core/event-store.ts +672 -0
- package/src/core/evidence-aligner.ts +635 -0
- package/src/core/graduation.ts +365 -0
- package/src/core/index.ts +32 -0
- package/src/core/matcher.ts +210 -0
- package/src/core/metadata-extractor.ts +203 -0
- package/src/core/privacy/filter.ts +179 -0
- package/src/core/privacy/index.ts +20 -0
- package/src/core/privacy/tag-parser.ts +145 -0
- package/src/core/progressive-retriever.ts +415 -0
- package/src/core/retriever.ts +235 -0
- package/src/core/task/blocker-resolver.ts +325 -0
- package/src/core/task/index.ts +9 -0
- package/src/core/task/task-matcher.ts +238 -0
- package/src/core/task/task-projector.ts +345 -0
- package/src/core/task/task-resolver.ts +414 -0
- package/src/core/types.ts +841 -0
- package/src/core/vector-outbox.ts +295 -0
- package/src/core/vector-store.ts +182 -0
- package/src/core/vector-worker.ts +488 -0
- package/src/core/working-set-store.ts +244 -0
- package/src/hooks/post-tool-use.ts +127 -0
- package/src/hooks/session-end.ts +78 -0
- package/src/hooks/session-start.ts +57 -0
- package/src/hooks/stop.ts +78 -0
- package/src/hooks/user-prompt-submit.ts +54 -0
- package/src/mcp/handlers.ts +212 -0
- package/src/mcp/index.ts +47 -0
- package/src/mcp/tools.ts +78 -0
- package/src/server/api/citations.ts +101 -0
- package/src/server/api/events.ts +101 -0
- package/src/server/api/index.ts +18 -0
- package/src/server/api/search.ts +98 -0
- package/src/server/api/sessions.ts +111 -0
- package/src/server/api/stats.ts +97 -0
- package/src/server/index.ts +91 -0
- package/src/services/memory-service.ts +626 -0
- package/src/services/session-history-importer.ts +367 -0
- package/tests/canonical-key.test.ts +101 -0
- package/tests/evidence-aligner.test.ts +152 -0
- package/tests/matcher.test.ts +112 -0
- package/tsconfig.json +24 -0
- package/vitest.config.ts +15 -0
|
@@ -0,0 +1,4408 @@
|
|
|
1
|
+
import { createRequire } from 'module';
|
|
2
|
+
import { fileURLToPath } from 'url';
|
|
3
|
+
import { dirname } from 'path';
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = dirname(__filename);
|
|
7
|
+
|
|
8
|
+
// src/core/types.ts
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
var EventTypeSchema = z.enum([
|
|
11
|
+
"user_prompt",
|
|
12
|
+
"agent_response",
|
|
13
|
+
"session_summary",
|
|
14
|
+
"tool_observation"
|
|
15
|
+
]);
|
|
16
|
+
var MemoryEventSchema = z.object({
|
|
17
|
+
id: z.string().uuid(),
|
|
18
|
+
eventType: EventTypeSchema,
|
|
19
|
+
sessionId: z.string(),
|
|
20
|
+
timestamp: z.date(),
|
|
21
|
+
content: z.string(),
|
|
22
|
+
canonicalKey: z.string(),
|
|
23
|
+
dedupeKey: z.string(),
|
|
24
|
+
metadata: z.record(z.unknown()).optional()
|
|
25
|
+
});
|
|
26
|
+
var MemoryEventInputSchema = MemoryEventSchema.omit({
|
|
27
|
+
id: true,
|
|
28
|
+
dedupeKey: true,
|
|
29
|
+
canonicalKey: true
|
|
30
|
+
});
|
|
31
|
+
var SessionSchema = z.object({
|
|
32
|
+
id: z.string(),
|
|
33
|
+
startedAt: z.date(),
|
|
34
|
+
endedAt: z.date().optional(),
|
|
35
|
+
projectPath: z.string().optional(),
|
|
36
|
+
summary: z.string().optional(),
|
|
37
|
+
tags: z.array(z.string()).optional()
|
|
38
|
+
});
|
|
39
|
+
var InsightTypeSchema = z.enum([
|
|
40
|
+
"preference",
|
|
41
|
+
"pattern",
|
|
42
|
+
"expertise"
|
|
43
|
+
]);
|
|
44
|
+
var InsightSchema = z.object({
|
|
45
|
+
id: z.string().uuid(),
|
|
46
|
+
insightType: InsightTypeSchema,
|
|
47
|
+
content: z.string(),
|
|
48
|
+
canonicalKey: z.string(),
|
|
49
|
+
confidence: z.number().min(0).max(1),
|
|
50
|
+
sourceEvents: z.array(z.string().uuid()),
|
|
51
|
+
createdAt: z.date(),
|
|
52
|
+
lastUpdated: z.date()
|
|
53
|
+
});
|
|
54
|
+
var MemoryMatchSchema = z.object({
|
|
55
|
+
event: MemoryEventSchema,
|
|
56
|
+
score: z.number().min(0).max(1),
|
|
57
|
+
relevanceReason: z.string().optional()
|
|
58
|
+
});
|
|
59
|
+
var MatchConfidenceSchema = z.enum(["high", "suggested", "none"]);
|
|
60
|
+
var MatchResultSchema = z.object({
|
|
61
|
+
match: MemoryMatchSchema.nullable(),
|
|
62
|
+
confidence: MatchConfidenceSchema,
|
|
63
|
+
gap: z.number().optional(),
|
|
64
|
+
alternatives: z.array(MemoryMatchSchema).optional()
|
|
65
|
+
});
|
|
66
|
+
var MATCH_THRESHOLDS = {
|
|
67
|
+
minCombinedScore: 0.92,
|
|
68
|
+
minGap: 0.03,
|
|
69
|
+
suggestionThreshold: 0.75
|
|
70
|
+
};
|
|
71
|
+
var MemoryLevelSchema = z.enum(["L0", "L1", "L2", "L3", "L4"]);
|
|
72
|
+
var GraduationResultSchema = z.object({
|
|
73
|
+
eventId: z.string().uuid(),
|
|
74
|
+
fromLevel: MemoryLevelSchema,
|
|
75
|
+
toLevel: MemoryLevelSchema,
|
|
76
|
+
success: z.boolean(),
|
|
77
|
+
reason: z.string().optional()
|
|
78
|
+
});
|
|
79
|
+
var EvidenceSpanSchema = z.object({
|
|
80
|
+
start: z.number().int().nonnegative(),
|
|
81
|
+
end: z.number().int().positive(),
|
|
82
|
+
confidence: z.number().min(0).max(1),
|
|
83
|
+
matchType: z.enum(["exact", "fuzzy", "none"]),
|
|
84
|
+
originalQuote: z.string(),
|
|
85
|
+
alignedText: z.string()
|
|
86
|
+
});
|
|
87
|
+
var ConfigSchema = z.object({
|
|
88
|
+
storage: z.object({
|
|
89
|
+
path: z.string().default("~/.claude-code/memory"),
|
|
90
|
+
maxSizeMB: z.number().default(500)
|
|
91
|
+
}).default({}),
|
|
92
|
+
embedding: z.object({
|
|
93
|
+
provider: z.enum(["local", "openai"]).default("local"),
|
|
94
|
+
model: z.string().default("Xenova/all-MiniLM-L6-v2"),
|
|
95
|
+
openaiModel: z.string().default("text-embedding-3-small"),
|
|
96
|
+
batchSize: z.number().default(32)
|
|
97
|
+
}).default({}),
|
|
98
|
+
retrieval: z.object({
|
|
99
|
+
topK: z.number().default(5),
|
|
100
|
+
minScore: z.number().default(0.7),
|
|
101
|
+
maxTokens: z.number().default(2e3)
|
|
102
|
+
}).default({}),
|
|
103
|
+
matching: z.object({
|
|
104
|
+
minCombinedScore: z.number().default(0.92),
|
|
105
|
+
minGap: z.number().default(0.03),
|
|
106
|
+
suggestionThreshold: z.number().default(0.75),
|
|
107
|
+
weights: z.object({
|
|
108
|
+
semanticSimilarity: z.number().default(0.4),
|
|
109
|
+
ftsScore: z.number().default(0.25),
|
|
110
|
+
recencyBonus: z.number().default(0.2),
|
|
111
|
+
statusWeight: z.number().default(0.15)
|
|
112
|
+
}).default({})
|
|
113
|
+
}).default({}),
|
|
114
|
+
privacy: z.object({
|
|
115
|
+
excludePatterns: z.array(z.string()).default(["password", "secret", "api_key", "token", "bearer"]),
|
|
116
|
+
anonymize: z.boolean().default(false),
|
|
117
|
+
privateTags: z.object({
|
|
118
|
+
enabled: z.boolean().default(true),
|
|
119
|
+
marker: z.enum(["[PRIVATE]", "[REDACTED]", ""]).default("[PRIVATE]"),
|
|
120
|
+
preserveLineCount: z.boolean().default(false),
|
|
121
|
+
supportedFormats: z.array(z.enum(["xml", "bracket", "comment"])).default(["xml"])
|
|
122
|
+
}).default({})
|
|
123
|
+
}).default({}),
|
|
124
|
+
toolObservation: z.object({
|
|
125
|
+
enabled: z.boolean().default(true),
|
|
126
|
+
excludedTools: z.array(z.string()).default(["TodoWrite", "TodoRead"]),
|
|
127
|
+
maxOutputLength: z.number().default(1e4),
|
|
128
|
+
maxOutputLines: z.number().default(100),
|
|
129
|
+
storeOnlyOnSuccess: z.boolean().default(false)
|
|
130
|
+
}).default({}),
|
|
131
|
+
features: z.object({
|
|
132
|
+
autoSave: z.boolean().default(true),
|
|
133
|
+
sessionSummary: z.boolean().default(true),
|
|
134
|
+
insightExtraction: z.boolean().default(true),
|
|
135
|
+
crossProjectLearning: z.boolean().default(false),
|
|
136
|
+
singleWriterMode: z.boolean().default(true)
|
|
137
|
+
}).default({}),
|
|
138
|
+
mode: z.enum(["session", "endless"]).default("session"),
|
|
139
|
+
endless: z.object({
|
|
140
|
+
enabled: z.boolean().default(false),
|
|
141
|
+
workingSet: z.object({
|
|
142
|
+
maxEvents: z.number().default(100),
|
|
143
|
+
timeWindowHours: z.number().default(24),
|
|
144
|
+
minRelevanceScore: z.number().default(0.5)
|
|
145
|
+
}).default({}),
|
|
146
|
+
consolidation: z.object({
|
|
147
|
+
triggerIntervalMs: z.number().default(36e5),
|
|
148
|
+
triggerEventCount: z.number().default(100),
|
|
149
|
+
triggerIdleMs: z.number().default(18e5),
|
|
150
|
+
useLLMSummarization: z.boolean().default(false)
|
|
151
|
+
}).default({}),
|
|
152
|
+
continuity: z.object({
|
|
153
|
+
minScoreForSeamless: z.number().default(0.7),
|
|
154
|
+
topicDecayHours: z.number().default(48)
|
|
155
|
+
}).default({})
|
|
156
|
+
}).optional()
|
|
157
|
+
});
|
|
158
|
+
var ToolMetadataSchema = z.object({
|
|
159
|
+
filePath: z.string().optional(),
|
|
160
|
+
fileType: z.string().optional(),
|
|
161
|
+
lineCount: z.number().optional(),
|
|
162
|
+
command: z.string().optional(),
|
|
163
|
+
exitCode: z.number().optional(),
|
|
164
|
+
pattern: z.string().optional(),
|
|
165
|
+
matchCount: z.number().optional(),
|
|
166
|
+
url: z.string().optional(),
|
|
167
|
+
statusCode: z.number().optional()
|
|
168
|
+
});
|
|
169
|
+
var ToolObservationPayloadSchema = z.object({
|
|
170
|
+
toolName: z.string(),
|
|
171
|
+
toolInput: z.record(z.unknown()),
|
|
172
|
+
toolOutput: z.string(),
|
|
173
|
+
durationMs: z.number(),
|
|
174
|
+
success: z.boolean(),
|
|
175
|
+
errorMessage: z.string().optional(),
|
|
176
|
+
metadata: ToolMetadataSchema.optional()
|
|
177
|
+
});
|
|
178
|
+
var EntityTypeSchema = z.enum(["task", "condition", "artifact"]);
|
|
179
|
+
var TaskStatusSchema = z.enum([
|
|
180
|
+
"pending",
|
|
181
|
+
"in_progress",
|
|
182
|
+
"blocked",
|
|
183
|
+
"done",
|
|
184
|
+
"cancelled"
|
|
185
|
+
]);
|
|
186
|
+
var TaskPrioritySchema = z.enum(["low", "medium", "high", "critical"]);
|
|
187
|
+
var EntityStageSchema = z.enum([
|
|
188
|
+
"raw",
|
|
189
|
+
"working",
|
|
190
|
+
"candidate",
|
|
191
|
+
"verified",
|
|
192
|
+
"certified"
|
|
193
|
+
]);
|
|
194
|
+
var EntityStatusSchema = z.enum([
|
|
195
|
+
"active",
|
|
196
|
+
"contested",
|
|
197
|
+
"deprecated",
|
|
198
|
+
"superseded"
|
|
199
|
+
]);
|
|
200
|
+
var EntitySchema = z.object({
|
|
201
|
+
entityId: z.string(),
|
|
202
|
+
entityType: EntityTypeSchema,
|
|
203
|
+
canonicalKey: z.string(),
|
|
204
|
+
title: z.string(),
|
|
205
|
+
stage: EntityStageSchema,
|
|
206
|
+
status: EntityStatusSchema,
|
|
207
|
+
currentJson: z.record(z.unknown()),
|
|
208
|
+
titleNorm: z.string().optional(),
|
|
209
|
+
searchText: z.string().optional(),
|
|
210
|
+
createdAt: z.date(),
|
|
211
|
+
updatedAt: z.date()
|
|
212
|
+
});
|
|
213
|
+
var TaskCurrentJsonSchema = z.object({
|
|
214
|
+
status: TaskStatusSchema,
|
|
215
|
+
priority: TaskPrioritySchema.optional(),
|
|
216
|
+
blockers: z.array(z.string()).optional(),
|
|
217
|
+
blockerSuggestions: z.array(z.string()).optional(),
|
|
218
|
+
description: z.string().optional(),
|
|
219
|
+
project: z.string().optional()
|
|
220
|
+
});
|
|
221
|
+
var EntityAliasSchema = z.object({
|
|
222
|
+
entityType: EntityTypeSchema,
|
|
223
|
+
canonicalKey: z.string(),
|
|
224
|
+
entityId: z.string(),
|
|
225
|
+
isPrimary: z.boolean()
|
|
226
|
+
});
|
|
227
|
+
var NodeTypeSchema = z.enum(["entry", "entity", "event"]);
|
|
228
|
+
var RelationTypeSchema = z.enum([
|
|
229
|
+
"evidence_of",
|
|
230
|
+
"blocked_by",
|
|
231
|
+
"blocked_by_suggested",
|
|
232
|
+
"resolves_to",
|
|
233
|
+
"derived_from",
|
|
234
|
+
"supersedes",
|
|
235
|
+
"source_of"
|
|
236
|
+
]);
|
|
237
|
+
var EdgeSchema = z.object({
|
|
238
|
+
edgeId: z.string(),
|
|
239
|
+
srcType: NodeTypeSchema,
|
|
240
|
+
srcId: z.string(),
|
|
241
|
+
relType: RelationTypeSchema,
|
|
242
|
+
dstType: NodeTypeSchema,
|
|
243
|
+
dstId: z.string(),
|
|
244
|
+
metaJson: z.record(z.unknown()).optional(),
|
|
245
|
+
createdAt: z.date()
|
|
246
|
+
});
|
|
247
|
+
var TaskEventTypeSchema = z.enum([
|
|
248
|
+
"task_created",
|
|
249
|
+
"task_status_changed",
|
|
250
|
+
"task_priority_changed",
|
|
251
|
+
"task_blockers_set",
|
|
252
|
+
"task_transition_rejected",
|
|
253
|
+
"condition_declared",
|
|
254
|
+
"artifact_declared",
|
|
255
|
+
"condition_resolved_to"
|
|
256
|
+
]);
|
|
257
|
+
var BlockerModeSchema = z.enum(["replace", "suggest"]);
|
|
258
|
+
var BlockerKindSchema = z.enum(["task", "condition", "artifact"]);
|
|
259
|
+
var BlockerRefSchema = z.object({
|
|
260
|
+
kind: BlockerKindSchema,
|
|
261
|
+
entityId: z.string(),
|
|
262
|
+
rawText: z.string().optional(),
|
|
263
|
+
confidence: z.number().min(0).max(1).optional(),
|
|
264
|
+
candidates: z.array(z.string()).optional()
|
|
265
|
+
});
|
|
266
|
+
var TaskCreatedPayloadSchema = z.object({
|
|
267
|
+
taskId: z.string(),
|
|
268
|
+
title: z.string(),
|
|
269
|
+
canonicalKey: z.string(),
|
|
270
|
+
initialStatus: TaskStatusSchema,
|
|
271
|
+
priority: TaskPrioritySchema.optional(),
|
|
272
|
+
description: z.string().optional(),
|
|
273
|
+
project: z.string().optional()
|
|
274
|
+
});
|
|
275
|
+
var TaskStatusChangedPayloadSchema = z.object({
|
|
276
|
+
taskId: z.string(),
|
|
277
|
+
fromStatus: TaskStatusSchema,
|
|
278
|
+
toStatus: TaskStatusSchema,
|
|
279
|
+
reason: z.string().optional()
|
|
280
|
+
});
|
|
281
|
+
var TaskBlockersSetPayloadSchema = z.object({
|
|
282
|
+
taskId: z.string(),
|
|
283
|
+
mode: BlockerModeSchema,
|
|
284
|
+
blockers: z.array(BlockerRefSchema),
|
|
285
|
+
sourceEntryId: z.string().optional()
|
|
286
|
+
});
|
|
287
|
+
var EntryTypeSchema = z.enum([
|
|
288
|
+
"fact",
|
|
289
|
+
"decision",
|
|
290
|
+
"insight",
|
|
291
|
+
"task_note",
|
|
292
|
+
"reference",
|
|
293
|
+
"preference",
|
|
294
|
+
"pattern"
|
|
295
|
+
]);
|
|
296
|
+
var EntrySchema = z.object({
|
|
297
|
+
entryId: z.string(),
|
|
298
|
+
createdTs: z.date(),
|
|
299
|
+
entryType: EntryTypeSchema,
|
|
300
|
+
title: z.string(),
|
|
301
|
+
contentJson: z.record(z.unknown()),
|
|
302
|
+
stage: EntityStageSchema,
|
|
303
|
+
status: EntityStatusSchema,
|
|
304
|
+
supersededBy: z.string().optional(),
|
|
305
|
+
buildId: z.string().optional(),
|
|
306
|
+
evidenceJson: z.record(z.unknown()).optional(),
|
|
307
|
+
canonicalKey: z.string()
|
|
308
|
+
});
|
|
309
|
+
var ExtractedEvidenceSchema = z.object({
|
|
310
|
+
messageIndex: z.number().int().nonnegative(),
|
|
311
|
+
quote: z.string()
|
|
312
|
+
});
|
|
313
|
+
var AlignedEvidenceSchema = z.object({
|
|
314
|
+
messageIndex: z.number().int().nonnegative(),
|
|
315
|
+
quote: z.string(),
|
|
316
|
+
spanStart: z.number().int().nonnegative(),
|
|
317
|
+
spanEnd: z.number().int().positive(),
|
|
318
|
+
quoteHash: z.string(),
|
|
319
|
+
confidence: z.number().min(0).max(1),
|
|
320
|
+
matchMethod: z.enum(["exact", "normalized", "fuzzy"])
|
|
321
|
+
});
|
|
322
|
+
var FailedEvidenceSchema = z.object({
|
|
323
|
+
messageIndex: z.number().int().nonnegative(),
|
|
324
|
+
quote: z.string(),
|
|
325
|
+
failureReason: z.enum(["not_found", "below_threshold", "ambiguous", "empty_quote", "invalid_index"])
|
|
326
|
+
});
|
|
327
|
+
var EvidenceAlignResultSchema = z.discriminatedUnion("aligned", [
|
|
328
|
+
z.object({ aligned: z.literal(true), evidence: AlignedEvidenceSchema }),
|
|
329
|
+
z.object({ aligned: z.literal(false), evidence: FailedEvidenceSchema })
|
|
330
|
+
]);
|
|
331
|
+
var OutboxStatusSchema = z.enum(["pending", "processing", "done", "failed"]);
|
|
332
|
+
var OutboxItemKindSchema = z.enum(["entry", "task_title", "event"]);
|
|
333
|
+
var OutboxJobSchema = z.object({
|
|
334
|
+
jobId: z.string(),
|
|
335
|
+
itemKind: OutboxItemKindSchema,
|
|
336
|
+
itemId: z.string(),
|
|
337
|
+
embeddingVersion: z.string(),
|
|
338
|
+
status: OutboxStatusSchema,
|
|
339
|
+
retryCount: z.number().int().nonnegative(),
|
|
340
|
+
error: z.string().optional(),
|
|
341
|
+
createdAt: z.date(),
|
|
342
|
+
updatedAt: z.date()
|
|
343
|
+
});
|
|
344
|
+
var VALID_OUTBOX_TRANSITIONS = [
|
|
345
|
+
{ from: "pending", to: "processing" },
|
|
346
|
+
{ from: "processing", to: "done" },
|
|
347
|
+
{ from: "processing", to: "failed" },
|
|
348
|
+
{ from: "failed", to: "pending" }
|
|
349
|
+
];
|
|
350
|
+
var BuildRunSchema = z.object({
|
|
351
|
+
buildId: z.string(),
|
|
352
|
+
startedAt: z.date(),
|
|
353
|
+
finishedAt: z.date().optional(),
|
|
354
|
+
extractorModel: z.string(),
|
|
355
|
+
extractorPromptHash: z.string(),
|
|
356
|
+
embedderModel: z.string(),
|
|
357
|
+
embeddingVersion: z.string(),
|
|
358
|
+
idrisVersion: z.string(),
|
|
359
|
+
schemaVersion: z.string(),
|
|
360
|
+
status: z.enum(["running", "success", "failed"]),
|
|
361
|
+
error: z.string().optional()
|
|
362
|
+
});
|
|
363
|
+
var PipelineMetricSchema = z.object({
|
|
364
|
+
id: z.string(),
|
|
365
|
+
ts: z.date(),
|
|
366
|
+
stage: z.string(),
|
|
367
|
+
latencyMs: z.number(),
|
|
368
|
+
success: z.boolean(),
|
|
369
|
+
error: z.string().optional(),
|
|
370
|
+
sessionId: z.string().optional()
|
|
371
|
+
});
|
|
372
|
+
var SearchIndexItemSchema = z.object({
|
|
373
|
+
id: z.string(),
|
|
374
|
+
summary: z.string().max(100),
|
|
375
|
+
score: z.number(),
|
|
376
|
+
type: z.enum(["user_prompt", "agent_response", "session_summary", "tool_observation"]),
|
|
377
|
+
timestamp: z.date(),
|
|
378
|
+
sessionId: z.string()
|
|
379
|
+
});
|
|
380
|
+
var TimelineItemSchema = z.object({
|
|
381
|
+
id: z.string(),
|
|
382
|
+
timestamp: z.date(),
|
|
383
|
+
type: z.enum(["user_prompt", "agent_response", "session_summary", "tool_observation"]),
|
|
384
|
+
preview: z.string().max(200),
|
|
385
|
+
isTarget: z.boolean()
|
|
386
|
+
});
|
|
387
|
+
var FullDetailSchema = z.object({
|
|
388
|
+
id: z.string(),
|
|
389
|
+
content: z.string(),
|
|
390
|
+
type: z.enum(["user_prompt", "agent_response", "session_summary", "tool_observation"]),
|
|
391
|
+
timestamp: z.date(),
|
|
392
|
+
sessionId: z.string(),
|
|
393
|
+
citationId: z.string().optional(),
|
|
394
|
+
metadata: z.object({
|
|
395
|
+
tokenCount: z.number(),
|
|
396
|
+
hasCode: z.boolean(),
|
|
397
|
+
files: z.array(z.string()).optional(),
|
|
398
|
+
tools: z.array(z.string()).optional()
|
|
399
|
+
})
|
|
400
|
+
});
|
|
401
|
+
var ProgressiveSearchResultSchema = z.object({
|
|
402
|
+
index: z.array(SearchIndexItemSchema),
|
|
403
|
+
timeline: z.array(TimelineItemSchema).optional(),
|
|
404
|
+
details: z.array(FullDetailSchema).optional(),
|
|
405
|
+
meta: z.object({
|
|
406
|
+
totalMatches: z.number(),
|
|
407
|
+
expandedCount: z.number(),
|
|
408
|
+
estimatedTokens: z.number(),
|
|
409
|
+
expansionReason: z.string().optional()
|
|
410
|
+
})
|
|
411
|
+
});
|
|
412
|
+
var ProgressiveDisclosureConfigSchema = z.object({
|
|
413
|
+
enabled: z.boolean().default(true),
|
|
414
|
+
layer1: z.object({
|
|
415
|
+
topK: z.number().default(10),
|
|
416
|
+
minScore: z.number().default(0.7)
|
|
417
|
+
}).default({}),
|
|
418
|
+
autoExpand: z.object({
|
|
419
|
+
enabled: z.boolean().default(true),
|
|
420
|
+
highConfidenceThreshold: z.number().default(0.92),
|
|
421
|
+
scoreGapThreshold: z.number().default(0.1),
|
|
422
|
+
maxAutoExpandCount: z.number().default(3)
|
|
423
|
+
}).default({}),
|
|
424
|
+
tokenBudget: z.object({
|
|
425
|
+
maxTotalTokens: z.number().default(2e3),
|
|
426
|
+
layer1PerItem: z.number().default(50),
|
|
427
|
+
layer2PerItem: z.number().default(40),
|
|
428
|
+
layer3PerItem: z.number().default(500)
|
|
429
|
+
}).default({})
|
|
430
|
+
});
|
|
431
|
+
var CitationSchema = z.object({
|
|
432
|
+
citationId: z.string().length(6),
|
|
433
|
+
eventId: z.string(),
|
|
434
|
+
createdAt: z.date()
|
|
435
|
+
});
|
|
436
|
+
var CitationUsageSchema = z.object({
|
|
437
|
+
usageId: z.string(),
|
|
438
|
+
citationId: z.string(),
|
|
439
|
+
sessionId: z.string(),
|
|
440
|
+
usedAt: z.date(),
|
|
441
|
+
context: z.string().optional()
|
|
442
|
+
});
|
|
443
|
+
var MemoryModeSchema = z.enum(["session", "endless"]);
|
|
444
|
+
var EndlessModeConfigSchema = z.object({
|
|
445
|
+
enabled: z.boolean().default(false),
|
|
446
|
+
workingSet: z.object({
|
|
447
|
+
maxEvents: z.number().default(100),
|
|
448
|
+
timeWindowHours: z.number().default(24),
|
|
449
|
+
minRelevanceScore: z.number().default(0.5)
|
|
450
|
+
}).default({}),
|
|
451
|
+
consolidation: z.object({
|
|
452
|
+
triggerIntervalMs: z.number().default(36e5),
|
|
453
|
+
// 1 hour
|
|
454
|
+
triggerEventCount: z.number().default(100),
|
|
455
|
+
triggerIdleMs: z.number().default(18e5),
|
|
456
|
+
// 30 minutes
|
|
457
|
+
useLLMSummarization: z.boolean().default(false)
|
|
458
|
+
}).default({}),
|
|
459
|
+
continuity: z.object({
|
|
460
|
+
minScoreForSeamless: z.number().default(0.7),
|
|
461
|
+
topicDecayHours: z.number().default(48)
|
|
462
|
+
}).default({})
|
|
463
|
+
});
|
|
464
|
+
var WorkingSetItemSchema = z.object({
|
|
465
|
+
id: z.string(),
|
|
466
|
+
eventId: z.string(),
|
|
467
|
+
addedAt: z.date(),
|
|
468
|
+
relevanceScore: z.number(),
|
|
469
|
+
topics: z.array(z.string()).optional(),
|
|
470
|
+
expiresAt: z.date()
|
|
471
|
+
});
|
|
472
|
+
var ConsolidatedMemorySchema = z.object({
|
|
473
|
+
memoryId: z.string(),
|
|
474
|
+
summary: z.string(),
|
|
475
|
+
topics: z.array(z.string()),
|
|
476
|
+
sourceEvents: z.array(z.string()),
|
|
477
|
+
confidence: z.number(),
|
|
478
|
+
createdAt: z.date(),
|
|
479
|
+
accessedAt: z.date().optional(),
|
|
480
|
+
accessCount: z.number().default(0)
|
|
481
|
+
});
|
|
482
|
+
var TransitionTypeSchema = z.enum(["seamless", "topic_shift", "break"]);
|
|
483
|
+
var ContinuityLogSchema = z.object({
|
|
484
|
+
logId: z.string(),
|
|
485
|
+
fromContextId: z.string().optional(),
|
|
486
|
+
toContextId: z.string().optional(),
|
|
487
|
+
continuityScore: z.number(),
|
|
488
|
+
transitionType: TransitionTypeSchema,
|
|
489
|
+
createdAt: z.date()
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
// src/core/canonical-key.ts
|
|
493
|
+
import { createHash } from "crypto";
|
|
494
|
+
var MAX_KEY_LENGTH = 200;
|
|
495
|
+
function makeCanonicalKey(title, context) {
|
|
496
|
+
let normalized = title.normalize("NFKC");
|
|
497
|
+
normalized = normalized.toLowerCase();
|
|
498
|
+
normalized = normalized.replace(/[^\p{L}\p{N}\s]/gu, "");
|
|
499
|
+
normalized = normalized.replace(/\s+/g, " ").trim();
|
|
500
|
+
let key = normalized;
|
|
501
|
+
if (context?.project) {
|
|
502
|
+
key = `${context.project}::${key}`;
|
|
503
|
+
}
|
|
504
|
+
if (key.length > MAX_KEY_LENGTH) {
|
|
505
|
+
const hashSuffix = createHash("md5").update(key).digest("hex").slice(0, 8);
|
|
506
|
+
key = key.slice(0, MAX_KEY_LENGTH - 9) + "_" + hashSuffix;
|
|
507
|
+
}
|
|
508
|
+
return key;
|
|
509
|
+
}
|
|
510
|
+
function isSameCanonicalKey(a, b) {
|
|
511
|
+
return makeCanonicalKey(a) === makeCanonicalKey(b);
|
|
512
|
+
}
|
|
513
|
+
function makeDedupeKey(content, sessionId) {
|
|
514
|
+
const contentHash = createHash("sha256").update(content).digest("hex");
|
|
515
|
+
return `${sessionId}:${contentHash}`;
|
|
516
|
+
}
|
|
517
|
+
function hashContent(content) {
|
|
518
|
+
return createHash("sha256").update(content).digest("hex");
|
|
519
|
+
}
|
|
520
|
+
function normalizeForKey(text) {
|
|
521
|
+
return text.normalize("NFKC").toLowerCase().replace(/[^\p{L}\p{N}\s]/gu, "").replace(/\s+/g, "_").trim();
|
|
522
|
+
}
|
|
523
|
+
function makeEntityCanonicalKey(entityType, identifier, context) {
|
|
524
|
+
const project = context?.project ?? "default";
|
|
525
|
+
switch (entityType) {
|
|
526
|
+
case "task":
|
|
527
|
+
return `task:${project}:${normalizeForKey(identifier)}`;
|
|
528
|
+
case "condition":
|
|
529
|
+
return `cond:${project}:${normalizeForKey(identifier)}`;
|
|
530
|
+
case "artifact":
|
|
531
|
+
return makeArtifactKey(identifier);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
function makeArtifactKey(identifier) {
|
|
535
|
+
if (/^https?:\/\//.test(identifier)) {
|
|
536
|
+
const hash2 = createHash("sha1").update(identifier).digest("hex").slice(0, 12);
|
|
537
|
+
return `art:url:${hash2}`;
|
|
538
|
+
}
|
|
539
|
+
const jiraMatch = identifier.match(/^([A-Z]+-\d+)$/);
|
|
540
|
+
if (jiraMatch) {
|
|
541
|
+
return `art:jira:${jiraMatch[1].toLowerCase()}`;
|
|
542
|
+
}
|
|
543
|
+
const ghMatch = identifier.match(/^([^\/]+\/[^#]+)#(\d+)$/);
|
|
544
|
+
if (ghMatch) {
|
|
545
|
+
return `art:gh_issue:${ghMatch[1]}:${ghMatch[2]}`;
|
|
546
|
+
}
|
|
547
|
+
const hash = createHash("sha1").update(identifier).digest("hex").slice(0, 12);
|
|
548
|
+
return `art:generic:${hash}`;
|
|
549
|
+
}
|
|
550
|
+
function makeTaskEventDedupeKey(eventType, taskId, sessionId, additionalContext) {
|
|
551
|
+
const parts = [eventType, taskId, sessionId];
|
|
552
|
+
if (additionalContext) {
|
|
553
|
+
parts.push(additionalContext);
|
|
554
|
+
}
|
|
555
|
+
const combined = parts.join(":");
|
|
556
|
+
return createHash("sha256").update(combined).digest("hex");
|
|
557
|
+
}
|
|
558
|
+
function parseEntityCanonicalKey(canonicalKey) {
|
|
559
|
+
const taskMatch = canonicalKey.match(/^task:([^:]+):(.+)$/);
|
|
560
|
+
if (taskMatch) {
|
|
561
|
+
return { entityType: "task", project: taskMatch[1], identifier: taskMatch[2] };
|
|
562
|
+
}
|
|
563
|
+
const condMatch = canonicalKey.match(/^cond:([^:]+):(.+)$/);
|
|
564
|
+
if (condMatch) {
|
|
565
|
+
return { entityType: "condition", project: condMatch[1], identifier: condMatch[2] };
|
|
566
|
+
}
|
|
567
|
+
const artMatch = canonicalKey.match(/^art:([^:]+):(.+)$/);
|
|
568
|
+
if (artMatch) {
|
|
569
|
+
return { entityType: "artifact", identifier: artMatch[2] };
|
|
570
|
+
}
|
|
571
|
+
return null;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// src/core/event-store.ts
|
|
575
|
+
import { Database } from "duckdb";
|
|
576
|
+
import { randomUUID } from "crypto";
|
|
577
|
+
var EventStore = class {
|
|
578
|
+
constructor(dbPath) {
|
|
579
|
+
this.dbPath = dbPath;
|
|
580
|
+
this.db = new Database(dbPath);
|
|
581
|
+
}
|
|
582
|
+
db;
|
|
583
|
+
initialized = false;
|
|
584
|
+
/**
|
|
585
|
+
* Initialize database schema
|
|
586
|
+
*/
|
|
587
|
+
async initialize() {
|
|
588
|
+
if (this.initialized)
|
|
589
|
+
return;
|
|
590
|
+
await this.db.run(`
|
|
591
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
592
|
+
id VARCHAR PRIMARY KEY,
|
|
593
|
+
event_type VARCHAR NOT NULL,
|
|
594
|
+
session_id VARCHAR NOT NULL,
|
|
595
|
+
timestamp TIMESTAMP NOT NULL,
|
|
596
|
+
content TEXT NOT NULL,
|
|
597
|
+
canonical_key VARCHAR NOT NULL,
|
|
598
|
+
dedupe_key VARCHAR UNIQUE,
|
|
599
|
+
metadata JSON
|
|
600
|
+
)
|
|
601
|
+
`);
|
|
602
|
+
await this.db.run(`
|
|
603
|
+
CREATE TABLE IF NOT EXISTS event_dedup (
|
|
604
|
+
dedupe_key VARCHAR PRIMARY KEY,
|
|
605
|
+
event_id VARCHAR NOT NULL,
|
|
606
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
607
|
+
)
|
|
608
|
+
`);
|
|
609
|
+
await this.db.run(`
|
|
610
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
611
|
+
id VARCHAR PRIMARY KEY,
|
|
612
|
+
started_at TIMESTAMP NOT NULL,
|
|
613
|
+
ended_at TIMESTAMP,
|
|
614
|
+
project_path VARCHAR,
|
|
615
|
+
summary TEXT,
|
|
616
|
+
tags JSON
|
|
617
|
+
)
|
|
618
|
+
`);
|
|
619
|
+
await this.db.run(`
|
|
620
|
+
CREATE TABLE IF NOT EXISTS insights (
|
|
621
|
+
id VARCHAR PRIMARY KEY,
|
|
622
|
+
insight_type VARCHAR NOT NULL,
|
|
623
|
+
content TEXT NOT NULL,
|
|
624
|
+
canonical_key VARCHAR NOT NULL,
|
|
625
|
+
confidence FLOAT,
|
|
626
|
+
source_events JSON,
|
|
627
|
+
created_at TIMESTAMP,
|
|
628
|
+
last_updated TIMESTAMP
|
|
629
|
+
)
|
|
630
|
+
`);
|
|
631
|
+
await this.db.run(`
|
|
632
|
+
CREATE TABLE IF NOT EXISTS embedding_outbox (
|
|
633
|
+
id VARCHAR PRIMARY KEY,
|
|
634
|
+
event_id VARCHAR NOT NULL,
|
|
635
|
+
content TEXT NOT NULL,
|
|
636
|
+
status VARCHAR DEFAULT 'pending',
|
|
637
|
+
retry_count INT DEFAULT 0,
|
|
638
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
639
|
+
processed_at TIMESTAMP,
|
|
640
|
+
error_message TEXT
|
|
641
|
+
)
|
|
642
|
+
`);
|
|
643
|
+
await this.db.run(`
|
|
644
|
+
CREATE TABLE IF NOT EXISTS projection_offsets (
|
|
645
|
+
projection_name VARCHAR PRIMARY KEY,
|
|
646
|
+
last_event_id VARCHAR,
|
|
647
|
+
last_timestamp TIMESTAMP,
|
|
648
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
649
|
+
)
|
|
650
|
+
`);
|
|
651
|
+
await this.db.run(`
|
|
652
|
+
CREATE TABLE IF NOT EXISTS memory_levels (
|
|
653
|
+
event_id VARCHAR PRIMARY KEY,
|
|
654
|
+
level VARCHAR NOT NULL DEFAULT 'L0',
|
|
655
|
+
promoted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
656
|
+
)
|
|
657
|
+
`);
|
|
658
|
+
await this.db.run(`
|
|
659
|
+
CREATE TABLE IF NOT EXISTS entries (
|
|
660
|
+
entry_id VARCHAR PRIMARY KEY,
|
|
661
|
+
created_ts TIMESTAMP NOT NULL,
|
|
662
|
+
entry_type VARCHAR NOT NULL,
|
|
663
|
+
title VARCHAR NOT NULL,
|
|
664
|
+
content_json JSON NOT NULL,
|
|
665
|
+
stage VARCHAR NOT NULL DEFAULT 'raw',
|
|
666
|
+
status VARCHAR DEFAULT 'active',
|
|
667
|
+
superseded_by VARCHAR,
|
|
668
|
+
build_id VARCHAR,
|
|
669
|
+
evidence_json JSON,
|
|
670
|
+
canonical_key VARCHAR,
|
|
671
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
672
|
+
)
|
|
673
|
+
`);
|
|
674
|
+
await this.db.run(`
|
|
675
|
+
CREATE TABLE IF NOT EXISTS entities (
|
|
676
|
+
entity_id VARCHAR PRIMARY KEY,
|
|
677
|
+
entity_type VARCHAR NOT NULL,
|
|
678
|
+
canonical_key VARCHAR NOT NULL,
|
|
679
|
+
title VARCHAR NOT NULL,
|
|
680
|
+
stage VARCHAR NOT NULL DEFAULT 'raw',
|
|
681
|
+
status VARCHAR NOT NULL DEFAULT 'active',
|
|
682
|
+
current_json JSON NOT NULL,
|
|
683
|
+
title_norm VARCHAR,
|
|
684
|
+
search_text VARCHAR,
|
|
685
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
686
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
687
|
+
)
|
|
688
|
+
`);
|
|
689
|
+
await this.db.run(`
|
|
690
|
+
CREATE TABLE IF NOT EXISTS entity_aliases (
|
|
691
|
+
entity_type VARCHAR NOT NULL,
|
|
692
|
+
canonical_key VARCHAR NOT NULL,
|
|
693
|
+
entity_id VARCHAR NOT NULL,
|
|
694
|
+
is_primary BOOLEAN DEFAULT FALSE,
|
|
695
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
696
|
+
PRIMARY KEY(entity_type, canonical_key)
|
|
697
|
+
)
|
|
698
|
+
`);
|
|
699
|
+
await this.db.run(`
|
|
700
|
+
CREATE TABLE IF NOT EXISTS edges (
|
|
701
|
+
edge_id VARCHAR PRIMARY KEY,
|
|
702
|
+
src_type VARCHAR NOT NULL,
|
|
703
|
+
src_id VARCHAR NOT NULL,
|
|
704
|
+
rel_type VARCHAR NOT NULL,
|
|
705
|
+
dst_type VARCHAR NOT NULL,
|
|
706
|
+
dst_id VARCHAR NOT NULL,
|
|
707
|
+
meta_json JSON,
|
|
708
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
709
|
+
)
|
|
710
|
+
`);
|
|
711
|
+
await this.db.run(`
|
|
712
|
+
CREATE TABLE IF NOT EXISTS vector_outbox (
|
|
713
|
+
job_id VARCHAR PRIMARY KEY,
|
|
714
|
+
item_kind VARCHAR NOT NULL,
|
|
715
|
+
item_id VARCHAR NOT NULL,
|
|
716
|
+
embedding_version VARCHAR NOT NULL,
|
|
717
|
+
status VARCHAR NOT NULL DEFAULT 'pending',
|
|
718
|
+
retry_count INT DEFAULT 0,
|
|
719
|
+
error VARCHAR,
|
|
720
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
721
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
722
|
+
UNIQUE(item_kind, item_id, embedding_version)
|
|
723
|
+
)
|
|
724
|
+
`);
|
|
725
|
+
await this.db.run(`
|
|
726
|
+
CREATE TABLE IF NOT EXISTS build_runs (
|
|
727
|
+
build_id VARCHAR PRIMARY KEY,
|
|
728
|
+
started_at TIMESTAMP NOT NULL,
|
|
729
|
+
finished_at TIMESTAMP,
|
|
730
|
+
extractor_model VARCHAR NOT NULL,
|
|
731
|
+
extractor_prompt_hash VARCHAR NOT NULL,
|
|
732
|
+
embedder_model VARCHAR NOT NULL,
|
|
733
|
+
embedding_version VARCHAR NOT NULL,
|
|
734
|
+
idris_version VARCHAR NOT NULL,
|
|
735
|
+
schema_version VARCHAR NOT NULL,
|
|
736
|
+
status VARCHAR NOT NULL DEFAULT 'running',
|
|
737
|
+
error VARCHAR
|
|
738
|
+
)
|
|
739
|
+
`);
|
|
740
|
+
await this.db.run(`
|
|
741
|
+
CREATE TABLE IF NOT EXISTS pipeline_metrics (
|
|
742
|
+
id VARCHAR PRIMARY KEY,
|
|
743
|
+
ts TIMESTAMP NOT NULL,
|
|
744
|
+
stage VARCHAR NOT NULL,
|
|
745
|
+
latency_ms DOUBLE NOT NULL,
|
|
746
|
+
success BOOLEAN NOT NULL,
|
|
747
|
+
error VARCHAR,
|
|
748
|
+
session_id VARCHAR
|
|
749
|
+
)
|
|
750
|
+
`);
|
|
751
|
+
await this.db.run(`
|
|
752
|
+
CREATE TABLE IF NOT EXISTS working_set (
|
|
753
|
+
id VARCHAR PRIMARY KEY,
|
|
754
|
+
event_id VARCHAR NOT NULL,
|
|
755
|
+
added_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
756
|
+
relevance_score FLOAT DEFAULT 1.0,
|
|
757
|
+
topics JSON,
|
|
758
|
+
expires_at TIMESTAMP
|
|
759
|
+
)
|
|
760
|
+
`);
|
|
761
|
+
await this.db.run(`
|
|
762
|
+
CREATE TABLE IF NOT EXISTS consolidated_memories (
|
|
763
|
+
memory_id VARCHAR PRIMARY KEY,
|
|
764
|
+
summary TEXT NOT NULL,
|
|
765
|
+
topics JSON,
|
|
766
|
+
source_events JSON,
|
|
767
|
+
confidence FLOAT DEFAULT 0.5,
|
|
768
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
769
|
+
accessed_at TIMESTAMP,
|
|
770
|
+
access_count INTEGER DEFAULT 0
|
|
771
|
+
)
|
|
772
|
+
`);
|
|
773
|
+
await this.db.run(`
|
|
774
|
+
CREATE TABLE IF NOT EXISTS continuity_log (
|
|
775
|
+
log_id VARCHAR PRIMARY KEY,
|
|
776
|
+
from_context_id VARCHAR,
|
|
777
|
+
to_context_id VARCHAR,
|
|
778
|
+
continuity_score FLOAT,
|
|
779
|
+
transition_type VARCHAR,
|
|
780
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
781
|
+
)
|
|
782
|
+
`);
|
|
783
|
+
await this.db.run(`
|
|
784
|
+
CREATE TABLE IF NOT EXISTS endless_config (
|
|
785
|
+
key VARCHAR PRIMARY KEY,
|
|
786
|
+
value JSON,
|
|
787
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
788
|
+
)
|
|
789
|
+
`);
|
|
790
|
+
await this.db.run(`CREATE INDEX IF NOT EXISTS idx_entries_type ON entries(entry_type)`);
|
|
791
|
+
await this.db.run(`CREATE INDEX IF NOT EXISTS idx_entries_stage ON entries(stage)`);
|
|
792
|
+
await this.db.run(`CREATE INDEX IF NOT EXISTS idx_entries_canonical ON entries(canonical_key)`);
|
|
793
|
+
await this.db.run(`CREATE INDEX IF NOT EXISTS idx_entities_type_key ON entities(entity_type, canonical_key)`);
|
|
794
|
+
await this.db.run(`CREATE INDEX IF NOT EXISTS idx_entities_status ON entities(status)`);
|
|
795
|
+
await this.db.run(`CREATE INDEX IF NOT EXISTS idx_edges_src ON edges(src_id, rel_type)`);
|
|
796
|
+
await this.db.run(`CREATE INDEX IF NOT EXISTS idx_edges_dst ON edges(dst_id, rel_type)`);
|
|
797
|
+
await this.db.run(`CREATE INDEX IF NOT EXISTS idx_edges_rel ON edges(rel_type)`);
|
|
798
|
+
await this.db.run(`CREATE INDEX IF NOT EXISTS idx_outbox_status ON vector_outbox(status)`);
|
|
799
|
+
await this.db.run(`CREATE INDEX IF NOT EXISTS idx_working_set_expires ON working_set(expires_at)`);
|
|
800
|
+
await this.db.run(`CREATE INDEX IF NOT EXISTS idx_working_set_relevance ON working_set(relevance_score DESC)`);
|
|
801
|
+
await this.db.run(`CREATE INDEX IF NOT EXISTS idx_consolidated_confidence ON consolidated_memories(confidence DESC)`);
|
|
802
|
+
await this.db.run(`CREATE INDEX IF NOT EXISTS idx_continuity_created ON continuity_log(created_at)`);
|
|
803
|
+
this.initialized = true;
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* Append event to store (AXIOMMIND Principle 2: Append-only)
|
|
807
|
+
* Returns existing event ID if duplicate (Principle 3: Idempotency)
|
|
808
|
+
*/
|
|
809
|
+
async append(input) {
|
|
810
|
+
await this.initialize();
|
|
811
|
+
const canonicalKey = makeCanonicalKey(input.content);
|
|
812
|
+
const dedupeKey = makeDedupeKey(input.content, input.sessionId);
|
|
813
|
+
const existing = await this.db.all(
|
|
814
|
+
`SELECT event_id FROM event_dedup WHERE dedupe_key = ?`,
|
|
815
|
+
[dedupeKey]
|
|
816
|
+
);
|
|
817
|
+
if (existing.length > 0) {
|
|
818
|
+
return {
|
|
819
|
+
success: true,
|
|
820
|
+
eventId: existing[0].event_id,
|
|
821
|
+
isDuplicate: true
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
const id = randomUUID();
|
|
825
|
+
const timestamp = input.timestamp.toISOString();
|
|
826
|
+
try {
|
|
827
|
+
await this.db.run(
|
|
828
|
+
`INSERT INTO events (id, event_type, session_id, timestamp, content, canonical_key, dedupe_key, metadata)
|
|
829
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
830
|
+
[
|
|
831
|
+
id,
|
|
832
|
+
input.eventType,
|
|
833
|
+
input.sessionId,
|
|
834
|
+
timestamp,
|
|
835
|
+
input.content,
|
|
836
|
+
canonicalKey,
|
|
837
|
+
dedupeKey,
|
|
838
|
+
JSON.stringify(input.metadata || {})
|
|
839
|
+
]
|
|
840
|
+
);
|
|
841
|
+
await this.db.run(
|
|
842
|
+
`INSERT INTO event_dedup (dedupe_key, event_id) VALUES (?, ?)`,
|
|
843
|
+
[dedupeKey, id]
|
|
844
|
+
);
|
|
845
|
+
await this.db.run(
|
|
846
|
+
`INSERT INTO memory_levels (event_id, level) VALUES (?, 'L0')`,
|
|
847
|
+
[id]
|
|
848
|
+
);
|
|
849
|
+
return { success: true, eventId: id, isDuplicate: false };
|
|
850
|
+
} catch (error) {
|
|
851
|
+
return {
|
|
852
|
+
success: false,
|
|
853
|
+
error: error instanceof Error ? error.message : String(error)
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
/**
|
|
858
|
+
* Get events by session ID
|
|
859
|
+
*/
|
|
860
|
+
async getSessionEvents(sessionId) {
|
|
861
|
+
await this.initialize();
|
|
862
|
+
const rows = await this.db.all(
|
|
863
|
+
`SELECT * FROM events WHERE session_id = ? ORDER BY timestamp ASC`,
|
|
864
|
+
[sessionId]
|
|
865
|
+
);
|
|
866
|
+
return rows.map(this.rowToEvent);
|
|
867
|
+
}
|
|
868
|
+
/**
|
|
869
|
+
* Get recent events
|
|
870
|
+
*/
|
|
871
|
+
async getRecentEvents(limit = 100) {
|
|
872
|
+
await this.initialize();
|
|
873
|
+
const rows = await this.db.all(
|
|
874
|
+
`SELECT * FROM events ORDER BY timestamp DESC LIMIT ?`,
|
|
875
|
+
[limit]
|
|
876
|
+
);
|
|
877
|
+
return rows.map(this.rowToEvent);
|
|
878
|
+
}
|
|
879
|
+
/**
|
|
880
|
+
* Get event by ID
|
|
881
|
+
*/
|
|
882
|
+
async getEvent(id) {
|
|
883
|
+
await this.initialize();
|
|
884
|
+
const rows = await this.db.all(
|
|
885
|
+
`SELECT * FROM events WHERE id = ?`,
|
|
886
|
+
[id]
|
|
887
|
+
);
|
|
888
|
+
if (rows.length === 0)
|
|
889
|
+
return null;
|
|
890
|
+
return this.rowToEvent(rows[0]);
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* Create or update session
|
|
894
|
+
*/
|
|
895
|
+
async upsertSession(session) {
|
|
896
|
+
await this.initialize();
|
|
897
|
+
const existing = await this.db.all(
|
|
898
|
+
`SELECT id FROM sessions WHERE id = ?`,
|
|
899
|
+
[session.id]
|
|
900
|
+
);
|
|
901
|
+
if (existing.length === 0) {
|
|
902
|
+
await this.db.run(
|
|
903
|
+
`INSERT INTO sessions (id, started_at, project_path, tags)
|
|
904
|
+
VALUES (?, ?, ?, ?)`,
|
|
905
|
+
[
|
|
906
|
+
session.id,
|
|
907
|
+
(session.startedAt || /* @__PURE__ */ new Date()).toISOString(),
|
|
908
|
+
session.projectPath || null,
|
|
909
|
+
JSON.stringify(session.tags || [])
|
|
910
|
+
]
|
|
911
|
+
);
|
|
912
|
+
} else {
|
|
913
|
+
const updates = [];
|
|
914
|
+
const values = [];
|
|
915
|
+
if (session.endedAt) {
|
|
916
|
+
updates.push("ended_at = ?");
|
|
917
|
+
values.push(session.endedAt.toISOString());
|
|
918
|
+
}
|
|
919
|
+
if (session.summary) {
|
|
920
|
+
updates.push("summary = ?");
|
|
921
|
+
values.push(session.summary);
|
|
922
|
+
}
|
|
923
|
+
if (session.tags) {
|
|
924
|
+
updates.push("tags = ?");
|
|
925
|
+
values.push(JSON.stringify(session.tags));
|
|
926
|
+
}
|
|
927
|
+
if (updates.length > 0) {
|
|
928
|
+
values.push(session.id);
|
|
929
|
+
await this.db.run(
|
|
930
|
+
`UPDATE sessions SET ${updates.join(", ")} WHERE id = ?`,
|
|
931
|
+
values
|
|
932
|
+
);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
/**
|
|
937
|
+
* Get session by ID
|
|
938
|
+
*/
|
|
939
|
+
async getSession(id) {
|
|
940
|
+
await this.initialize();
|
|
941
|
+
const rows = await this.db.all(
|
|
942
|
+
`SELECT * FROM sessions WHERE id = ?`,
|
|
943
|
+
[id]
|
|
944
|
+
);
|
|
945
|
+
if (rows.length === 0)
|
|
946
|
+
return null;
|
|
947
|
+
const row = rows[0];
|
|
948
|
+
return {
|
|
949
|
+
id: row.id,
|
|
950
|
+
startedAt: new Date(row.started_at),
|
|
951
|
+
endedAt: row.ended_at ? new Date(row.ended_at) : void 0,
|
|
952
|
+
projectPath: row.project_path,
|
|
953
|
+
summary: row.summary,
|
|
954
|
+
tags: row.tags ? JSON.parse(row.tags) : void 0
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
/**
|
|
958
|
+
* Add to embedding outbox (Single-Writer Pattern)
|
|
959
|
+
*/
|
|
960
|
+
async enqueueForEmbedding(eventId, content) {
|
|
961
|
+
await this.initialize();
|
|
962
|
+
const id = randomUUID();
|
|
963
|
+
await this.db.run(
|
|
964
|
+
`INSERT INTO embedding_outbox (id, event_id, content, status, retry_count)
|
|
965
|
+
VALUES (?, ?, ?, 'pending', 0)`,
|
|
966
|
+
[id, eventId, content]
|
|
967
|
+
);
|
|
968
|
+
return id;
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* Get pending outbox items
|
|
972
|
+
*/
|
|
973
|
+
async getPendingOutboxItems(limit = 32) {
|
|
974
|
+
await this.initialize();
|
|
975
|
+
const rows = await this.db.all(
|
|
976
|
+
`UPDATE embedding_outbox
|
|
977
|
+
SET status = 'processing'
|
|
978
|
+
WHERE id IN (
|
|
979
|
+
SELECT id FROM embedding_outbox
|
|
980
|
+
WHERE status = 'pending'
|
|
981
|
+
ORDER BY created_at
|
|
982
|
+
LIMIT ?
|
|
983
|
+
)
|
|
984
|
+
RETURNING *`,
|
|
985
|
+
[limit]
|
|
986
|
+
);
|
|
987
|
+
return rows.map((row) => ({
|
|
988
|
+
id: row.id,
|
|
989
|
+
eventId: row.event_id,
|
|
990
|
+
content: row.content,
|
|
991
|
+
status: row.status,
|
|
992
|
+
retryCount: row.retry_count,
|
|
993
|
+
createdAt: new Date(row.created_at),
|
|
994
|
+
errorMessage: row.error_message
|
|
995
|
+
}));
|
|
996
|
+
}
|
|
997
|
+
/**
|
|
998
|
+
* Mark outbox items as done
|
|
999
|
+
*/
|
|
1000
|
+
async completeOutboxItems(ids) {
|
|
1001
|
+
if (ids.length === 0)
|
|
1002
|
+
return;
|
|
1003
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
1004
|
+
await this.db.run(
|
|
1005
|
+
`DELETE FROM embedding_outbox WHERE id IN (${placeholders})`,
|
|
1006
|
+
ids
|
|
1007
|
+
);
|
|
1008
|
+
}
|
|
1009
|
+
/**
|
|
1010
|
+
* Mark outbox items as failed
|
|
1011
|
+
*/
|
|
1012
|
+
async failOutboxItems(ids, error) {
|
|
1013
|
+
if (ids.length === 0)
|
|
1014
|
+
return;
|
|
1015
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
1016
|
+
await this.db.run(
|
|
1017
|
+
`UPDATE embedding_outbox
|
|
1018
|
+
SET status = CASE WHEN retry_count >= 3 THEN 'failed' ELSE 'pending' END,
|
|
1019
|
+
retry_count = retry_count + 1,
|
|
1020
|
+
error_message = ?
|
|
1021
|
+
WHERE id IN (${placeholders})`,
|
|
1022
|
+
[error, ...ids]
|
|
1023
|
+
);
|
|
1024
|
+
}
|
|
1025
|
+
/**
|
|
1026
|
+
* Update memory level
|
|
1027
|
+
*/
|
|
1028
|
+
async updateMemoryLevel(eventId, level) {
|
|
1029
|
+
await this.initialize();
|
|
1030
|
+
await this.db.run(
|
|
1031
|
+
`UPDATE memory_levels SET level = ?, promoted_at = CURRENT_TIMESTAMP WHERE event_id = ?`,
|
|
1032
|
+
[level, eventId]
|
|
1033
|
+
);
|
|
1034
|
+
}
|
|
1035
|
+
/**
|
|
1036
|
+
* Get memory level statistics
|
|
1037
|
+
*/
|
|
1038
|
+
async getLevelStats() {
|
|
1039
|
+
await this.initialize();
|
|
1040
|
+
const rows = await this.db.all(
|
|
1041
|
+
`SELECT level, COUNT(*) as count FROM memory_levels GROUP BY level`
|
|
1042
|
+
);
|
|
1043
|
+
return rows;
|
|
1044
|
+
}
|
|
1045
|
+
// ============================================================
|
|
1046
|
+
// Endless Mode Helper Methods
|
|
1047
|
+
// ============================================================
|
|
1048
|
+
/**
|
|
1049
|
+
* Get database instance for Endless Mode stores
|
|
1050
|
+
*/
|
|
1051
|
+
getDatabase() {
|
|
1052
|
+
return this.db;
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* Get config value for endless mode
|
|
1056
|
+
*/
|
|
1057
|
+
async getEndlessConfig(key) {
|
|
1058
|
+
await this.initialize();
|
|
1059
|
+
const rows = await this.db.all(
|
|
1060
|
+
`SELECT value FROM endless_config WHERE key = ?`,
|
|
1061
|
+
[key]
|
|
1062
|
+
);
|
|
1063
|
+
if (rows.length === 0)
|
|
1064
|
+
return null;
|
|
1065
|
+
return JSON.parse(rows[0].value);
|
|
1066
|
+
}
|
|
1067
|
+
/**
|
|
1068
|
+
* Set config value for endless mode
|
|
1069
|
+
*/
|
|
1070
|
+
async setEndlessConfig(key, value) {
|
|
1071
|
+
await this.initialize();
|
|
1072
|
+
await this.db.run(
|
|
1073
|
+
`INSERT OR REPLACE INTO endless_config (key, value, updated_at)
|
|
1074
|
+
VALUES (?, ?, CURRENT_TIMESTAMP)`,
|
|
1075
|
+
[key, JSON.stringify(value)]
|
|
1076
|
+
);
|
|
1077
|
+
}
|
|
1078
|
+
/**
|
|
1079
|
+
* Get all sessions
|
|
1080
|
+
*/
|
|
1081
|
+
async getAllSessions() {
|
|
1082
|
+
await this.initialize();
|
|
1083
|
+
const rows = await this.db.all(
|
|
1084
|
+
`SELECT * FROM sessions ORDER BY started_at DESC`
|
|
1085
|
+
);
|
|
1086
|
+
return rows.map((row) => ({
|
|
1087
|
+
id: row.id,
|
|
1088
|
+
startedAt: new Date(row.started_at),
|
|
1089
|
+
endedAt: row.ended_at ? new Date(row.ended_at) : void 0,
|
|
1090
|
+
projectPath: row.project_path,
|
|
1091
|
+
summary: row.summary,
|
|
1092
|
+
tags: row.tags ? JSON.parse(row.tags) : void 0
|
|
1093
|
+
}));
|
|
1094
|
+
}
|
|
1095
|
+
/**
|
|
1096
|
+
* Close database connection
|
|
1097
|
+
*/
|
|
1098
|
+
async close() {
|
|
1099
|
+
await this.db.close();
|
|
1100
|
+
}
|
|
1101
|
+
/**
|
|
1102
|
+
* Convert database row to MemoryEvent
|
|
1103
|
+
*/
|
|
1104
|
+
rowToEvent(row) {
|
|
1105
|
+
return {
|
|
1106
|
+
id: row.id,
|
|
1107
|
+
eventType: row.event_type,
|
|
1108
|
+
sessionId: row.session_id,
|
|
1109
|
+
timestamp: new Date(row.timestamp),
|
|
1110
|
+
content: row.content,
|
|
1111
|
+
canonicalKey: row.canonical_key,
|
|
1112
|
+
dedupeKey: row.dedupe_key,
|
|
1113
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : void 0
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
};
|
|
1117
|
+
|
|
1118
|
+
// src/core/entity-repo.ts
|
|
1119
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
1120
|
+
var EntityRepo = class {
|
|
1121
|
+
constructor(db) {
|
|
1122
|
+
this.db = db;
|
|
1123
|
+
}
|
|
1124
|
+
/**
|
|
1125
|
+
* Create a new entity
|
|
1126
|
+
*/
|
|
1127
|
+
async create(input) {
|
|
1128
|
+
const entityId = randomUUID2();
|
|
1129
|
+
const canonicalKey = makeEntityCanonicalKey(input.entityType, input.title, {
|
|
1130
|
+
project: input.project
|
|
1131
|
+
});
|
|
1132
|
+
const titleNorm = input.title.toLowerCase().trim();
|
|
1133
|
+
const searchText = `${input.title} ${JSON.stringify(input.currentJson)}`;
|
|
1134
|
+
const now = /* @__PURE__ */ new Date();
|
|
1135
|
+
await this.db.run(
|
|
1136
|
+
`INSERT INTO entities (
|
|
1137
|
+
entity_id, entity_type, canonical_key, title, stage, status,
|
|
1138
|
+
current_json, title_norm, search_text, created_at, updated_at
|
|
1139
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1140
|
+
[
|
|
1141
|
+
entityId,
|
|
1142
|
+
input.entityType,
|
|
1143
|
+
canonicalKey,
|
|
1144
|
+
input.title,
|
|
1145
|
+
input.stage ?? "raw",
|
|
1146
|
+
input.status ?? "active",
|
|
1147
|
+
JSON.stringify(input.currentJson),
|
|
1148
|
+
titleNorm,
|
|
1149
|
+
searchText,
|
|
1150
|
+
now.toISOString(),
|
|
1151
|
+
now.toISOString()
|
|
1152
|
+
]
|
|
1153
|
+
);
|
|
1154
|
+
await this.db.run(
|
|
1155
|
+
`INSERT INTO entity_aliases (entity_type, canonical_key, entity_id, is_primary)
|
|
1156
|
+
VALUES (?, ?, ?, TRUE)
|
|
1157
|
+
ON CONFLICT (entity_type, canonical_key) DO NOTHING`,
|
|
1158
|
+
[input.entityType, canonicalKey, entityId]
|
|
1159
|
+
);
|
|
1160
|
+
return {
|
|
1161
|
+
entityId,
|
|
1162
|
+
entityType: input.entityType,
|
|
1163
|
+
canonicalKey,
|
|
1164
|
+
title: input.title,
|
|
1165
|
+
stage: input.stage ?? "raw",
|
|
1166
|
+
status: input.status ?? "active",
|
|
1167
|
+
currentJson: input.currentJson,
|
|
1168
|
+
titleNorm,
|
|
1169
|
+
searchText,
|
|
1170
|
+
createdAt: now,
|
|
1171
|
+
updatedAt: now
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
/**
|
|
1175
|
+
* Find entity by ID
|
|
1176
|
+
*/
|
|
1177
|
+
async findById(entityId) {
|
|
1178
|
+
const rows = await this.db.all(
|
|
1179
|
+
`SELECT * FROM entities WHERE entity_id = ?`,
|
|
1180
|
+
[entityId]
|
|
1181
|
+
);
|
|
1182
|
+
if (rows.length === 0)
|
|
1183
|
+
return null;
|
|
1184
|
+
return this.rowToEntity(rows[0]);
|
|
1185
|
+
}
|
|
1186
|
+
/**
|
|
1187
|
+
* Find entity by canonical key
|
|
1188
|
+
*/
|
|
1189
|
+
async findByCanonicalKey(entityType, canonicalKey) {
|
|
1190
|
+
const rows = await this.db.all(
|
|
1191
|
+
`SELECT * FROM entities
|
|
1192
|
+
WHERE entity_type = ? AND canonical_key = ?`,
|
|
1193
|
+
[entityType, canonicalKey]
|
|
1194
|
+
);
|
|
1195
|
+
if (rows.length === 0)
|
|
1196
|
+
return null;
|
|
1197
|
+
return this.rowToEntity(rows[0]);
|
|
1198
|
+
}
|
|
1199
|
+
/**
|
|
1200
|
+
* Find or create entity by title (idempotent)
|
|
1201
|
+
*/
|
|
1202
|
+
async findOrCreate(input) {
|
|
1203
|
+
const canonicalKey = makeEntityCanonicalKey(input.entityType, input.title, {
|
|
1204
|
+
project: input.project
|
|
1205
|
+
});
|
|
1206
|
+
const existing = await this.findByCanonicalKey(input.entityType, canonicalKey);
|
|
1207
|
+
if (existing) {
|
|
1208
|
+
return { entity: existing, created: false };
|
|
1209
|
+
}
|
|
1210
|
+
const entity = await this.create(input);
|
|
1211
|
+
return { entity, created: true };
|
|
1212
|
+
}
|
|
1213
|
+
/**
|
|
1214
|
+
* Update entity
|
|
1215
|
+
*/
|
|
1216
|
+
async update(entityId, input) {
|
|
1217
|
+
const existing = await this.findById(entityId);
|
|
1218
|
+
if (!existing)
|
|
1219
|
+
return null;
|
|
1220
|
+
const updates = [];
|
|
1221
|
+
const values = [];
|
|
1222
|
+
if (input.currentJson !== void 0) {
|
|
1223
|
+
updates.push("current_json = ?");
|
|
1224
|
+
values.push(JSON.stringify(input.currentJson));
|
|
1225
|
+
}
|
|
1226
|
+
if (input.stage !== void 0) {
|
|
1227
|
+
updates.push("stage = ?");
|
|
1228
|
+
values.push(input.stage);
|
|
1229
|
+
}
|
|
1230
|
+
if (input.status !== void 0) {
|
|
1231
|
+
updates.push("status = ?");
|
|
1232
|
+
values.push(input.status);
|
|
1233
|
+
}
|
|
1234
|
+
if (input.searchText !== void 0) {
|
|
1235
|
+
updates.push("search_text = ?");
|
|
1236
|
+
values.push(input.searchText);
|
|
1237
|
+
}
|
|
1238
|
+
updates.push("updated_at = ?");
|
|
1239
|
+
values.push((/* @__PURE__ */ new Date()).toISOString());
|
|
1240
|
+
values.push(entityId);
|
|
1241
|
+
await this.db.run(
|
|
1242
|
+
`UPDATE entities SET ${updates.join(", ")} WHERE entity_id = ?`,
|
|
1243
|
+
values
|
|
1244
|
+
);
|
|
1245
|
+
return this.findById(entityId);
|
|
1246
|
+
}
|
|
1247
|
+
/**
|
|
1248
|
+
* List entities by type
|
|
1249
|
+
*/
|
|
1250
|
+
async listByType(entityType, options) {
|
|
1251
|
+
let query = `SELECT * FROM entities WHERE entity_type = ?`;
|
|
1252
|
+
const params = [entityType];
|
|
1253
|
+
if (options?.status) {
|
|
1254
|
+
query += ` AND status = ?`;
|
|
1255
|
+
params.push(options.status);
|
|
1256
|
+
}
|
|
1257
|
+
query += ` ORDER BY updated_at DESC`;
|
|
1258
|
+
if (options?.limit) {
|
|
1259
|
+
query += ` LIMIT ?`;
|
|
1260
|
+
params.push(options.limit);
|
|
1261
|
+
}
|
|
1262
|
+
if (options?.offset) {
|
|
1263
|
+
query += ` OFFSET ?`;
|
|
1264
|
+
params.push(options.offset);
|
|
1265
|
+
}
|
|
1266
|
+
const rows = await this.db.all(query, params);
|
|
1267
|
+
return rows.map((row) => this.rowToEntity(row));
|
|
1268
|
+
}
|
|
1269
|
+
/**
|
|
1270
|
+
* Search entities by text
|
|
1271
|
+
*/
|
|
1272
|
+
async search(query, options) {
|
|
1273
|
+
const searchPattern = `%${query.toLowerCase()}%`;
|
|
1274
|
+
let sql = `SELECT * FROM entities WHERE (title_norm LIKE ? OR search_text LIKE ?)`;
|
|
1275
|
+
const params = [searchPattern, searchPattern];
|
|
1276
|
+
if (options?.entityType) {
|
|
1277
|
+
sql += ` AND entity_type = ?`;
|
|
1278
|
+
params.push(options.entityType);
|
|
1279
|
+
}
|
|
1280
|
+
sql += ` AND status = 'active' ORDER BY updated_at DESC`;
|
|
1281
|
+
if (options?.limit) {
|
|
1282
|
+
sql += ` LIMIT ?`;
|
|
1283
|
+
params.push(options.limit);
|
|
1284
|
+
}
|
|
1285
|
+
const rows = await this.db.all(sql, params);
|
|
1286
|
+
return rows.map((row) => this.rowToEntity(row));
|
|
1287
|
+
}
|
|
1288
|
+
/**
|
|
1289
|
+
* Get tasks by status
|
|
1290
|
+
*/
|
|
1291
|
+
async getTasksByStatus(status) {
|
|
1292
|
+
const rows = await this.db.all(
|
|
1293
|
+
`SELECT * FROM entities
|
|
1294
|
+
WHERE entity_type = 'task'
|
|
1295
|
+
AND json_extract(current_json, '$.status') = ?
|
|
1296
|
+
AND status = 'active'
|
|
1297
|
+
ORDER BY updated_at DESC`,
|
|
1298
|
+
[status]
|
|
1299
|
+
);
|
|
1300
|
+
return rows.map((row) => this.rowToEntity(row));
|
|
1301
|
+
}
|
|
1302
|
+
/**
|
|
1303
|
+
* Get blocked tasks with their blockers
|
|
1304
|
+
*/
|
|
1305
|
+
async getBlockedTasksWithBlockers() {
|
|
1306
|
+
const tasks = await this.getTasksByStatus("blocked");
|
|
1307
|
+
const results = [];
|
|
1308
|
+
for (const task of tasks) {
|
|
1309
|
+
const blockerEdges = await this.db.all(
|
|
1310
|
+
`SELECT e.dst_id, ent.entity_type, ent.title
|
|
1311
|
+
FROM edges e
|
|
1312
|
+
JOIN entities ent ON ent.entity_id = e.dst_id
|
|
1313
|
+
WHERE e.src_id = ? AND e.rel_type = 'blocked_by'`,
|
|
1314
|
+
[task.entityId]
|
|
1315
|
+
);
|
|
1316
|
+
results.push({
|
|
1317
|
+
task,
|
|
1318
|
+
blockers: blockerEdges.map((row) => ({
|
|
1319
|
+
entityId: row.dst_id,
|
|
1320
|
+
entityType: row.entity_type,
|
|
1321
|
+
title: row.title
|
|
1322
|
+
}))
|
|
1323
|
+
});
|
|
1324
|
+
}
|
|
1325
|
+
return results;
|
|
1326
|
+
}
|
|
1327
|
+
/**
|
|
1328
|
+
* Add alias for entity
|
|
1329
|
+
*/
|
|
1330
|
+
async addAlias(entityType, canonicalKey, entityId) {
|
|
1331
|
+
await this.db.run(
|
|
1332
|
+
`INSERT INTO entity_aliases (entity_type, canonical_key, entity_id, is_primary)
|
|
1333
|
+
VALUES (?, ?, ?, FALSE)
|
|
1334
|
+
ON CONFLICT (entity_type, canonical_key) DO NOTHING`,
|
|
1335
|
+
[entityType, canonicalKey, entityId]
|
|
1336
|
+
);
|
|
1337
|
+
}
|
|
1338
|
+
/**
|
|
1339
|
+
* Find entity by alias
|
|
1340
|
+
*/
|
|
1341
|
+
async findByAlias(entityType, canonicalKey) {
|
|
1342
|
+
const rows = await this.db.all(
|
|
1343
|
+
`SELECT e.* FROM entities e
|
|
1344
|
+
JOIN entity_aliases a ON e.entity_id = a.entity_id
|
|
1345
|
+
WHERE a.entity_type = ? AND a.canonical_key = ?`,
|
|
1346
|
+
[entityType, canonicalKey]
|
|
1347
|
+
);
|
|
1348
|
+
if (rows.length === 0)
|
|
1349
|
+
return null;
|
|
1350
|
+
return this.rowToEntity(rows[0]);
|
|
1351
|
+
}
|
|
1352
|
+
/**
|
|
1353
|
+
* Convert database row to Entity
|
|
1354
|
+
*/
|
|
1355
|
+
rowToEntity(row) {
|
|
1356
|
+
return {
|
|
1357
|
+
entityId: row.entity_id,
|
|
1358
|
+
entityType: row.entity_type,
|
|
1359
|
+
canonicalKey: row.canonical_key,
|
|
1360
|
+
title: row.title,
|
|
1361
|
+
stage: row.stage,
|
|
1362
|
+
status: row.status,
|
|
1363
|
+
currentJson: typeof row.current_json === "string" ? JSON.parse(row.current_json) : row.current_json,
|
|
1364
|
+
titleNorm: row.title_norm,
|
|
1365
|
+
searchText: row.search_text,
|
|
1366
|
+
createdAt: new Date(row.created_at),
|
|
1367
|
+
updatedAt: new Date(row.updated_at)
|
|
1368
|
+
};
|
|
1369
|
+
}
|
|
1370
|
+
};
|
|
1371
|
+
|
|
1372
|
+
// src/core/edge-repo.ts
|
|
1373
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
1374
|
+
var EdgeRepo = class {
|
|
1375
|
+
constructor(db) {
|
|
1376
|
+
this.db = db;
|
|
1377
|
+
}
|
|
1378
|
+
/**
|
|
1379
|
+
* Create a new edge (idempotent - ignores duplicates)
|
|
1380
|
+
*/
|
|
1381
|
+
async create(input) {
|
|
1382
|
+
const edgeId = randomUUID3();
|
|
1383
|
+
const now = /* @__PURE__ */ new Date();
|
|
1384
|
+
await this.db.run(
|
|
1385
|
+
`INSERT INTO edges (edge_id, src_type, src_id, rel_type, dst_type, dst_id, meta_json, created_at)
|
|
1386
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1387
|
+
ON CONFLICT DO NOTHING`,
|
|
1388
|
+
[
|
|
1389
|
+
edgeId,
|
|
1390
|
+
input.srcType,
|
|
1391
|
+
input.srcId,
|
|
1392
|
+
input.relType,
|
|
1393
|
+
input.dstType,
|
|
1394
|
+
input.dstId,
|
|
1395
|
+
JSON.stringify(input.metaJson ?? {}),
|
|
1396
|
+
now.toISOString()
|
|
1397
|
+
]
|
|
1398
|
+
);
|
|
1399
|
+
return {
|
|
1400
|
+
edgeId,
|
|
1401
|
+
srcType: input.srcType,
|
|
1402
|
+
srcId: input.srcId,
|
|
1403
|
+
relType: input.relType,
|
|
1404
|
+
dstType: input.dstType,
|
|
1405
|
+
dstId: input.dstId,
|
|
1406
|
+
metaJson: input.metaJson,
|
|
1407
|
+
createdAt: now
|
|
1408
|
+
};
|
|
1409
|
+
}
|
|
1410
|
+
/**
|
|
1411
|
+
* Create or update edge
|
|
1412
|
+
*/
|
|
1413
|
+
async upsert(input) {
|
|
1414
|
+
const existing = await this.findByEndpoints(
|
|
1415
|
+
input.srcType,
|
|
1416
|
+
input.srcId,
|
|
1417
|
+
input.relType,
|
|
1418
|
+
input.dstType,
|
|
1419
|
+
input.dstId
|
|
1420
|
+
);
|
|
1421
|
+
if (existing) {
|
|
1422
|
+
await this.db.run(
|
|
1423
|
+
`UPDATE edges SET meta_json = ? WHERE edge_id = ?`,
|
|
1424
|
+
[JSON.stringify(input.metaJson ?? {}), existing.edgeId]
|
|
1425
|
+
);
|
|
1426
|
+
return { ...existing, metaJson: input.metaJson };
|
|
1427
|
+
}
|
|
1428
|
+
return this.create(input);
|
|
1429
|
+
}
|
|
1430
|
+
/**
|
|
1431
|
+
* Find edge by endpoints
|
|
1432
|
+
*/
|
|
1433
|
+
async findByEndpoints(srcType, srcId, relType, dstType, dstId) {
|
|
1434
|
+
const rows = await this.db.all(
|
|
1435
|
+
`SELECT * FROM edges
|
|
1436
|
+
WHERE src_type = ? AND src_id = ? AND rel_type = ?
|
|
1437
|
+
AND dst_type = ? AND dst_id = ?`,
|
|
1438
|
+
[srcType, srcId, relType, dstType, dstId]
|
|
1439
|
+
);
|
|
1440
|
+
if (rows.length === 0)
|
|
1441
|
+
return null;
|
|
1442
|
+
return this.rowToEdge(rows[0]);
|
|
1443
|
+
}
|
|
1444
|
+
/**
|
|
1445
|
+
* Find edges by source
|
|
1446
|
+
*/
|
|
1447
|
+
async findBySrc(srcId, relType) {
|
|
1448
|
+
let query = `SELECT * FROM edges WHERE src_id = ?`;
|
|
1449
|
+
const params = [srcId];
|
|
1450
|
+
if (relType) {
|
|
1451
|
+
query += ` AND rel_type = ?`;
|
|
1452
|
+
params.push(relType);
|
|
1453
|
+
}
|
|
1454
|
+
query += ` ORDER BY created_at DESC`;
|
|
1455
|
+
const rows = await this.db.all(query, params);
|
|
1456
|
+
return rows.map((row) => this.rowToEdge(row));
|
|
1457
|
+
}
|
|
1458
|
+
/**
|
|
1459
|
+
* Find edges by destination
|
|
1460
|
+
*/
|
|
1461
|
+
async findByDst(dstId, relType) {
|
|
1462
|
+
let query = `SELECT * FROM edges WHERE dst_id = ?`;
|
|
1463
|
+
const params = [dstId];
|
|
1464
|
+
if (relType) {
|
|
1465
|
+
query += ` AND rel_type = ?`;
|
|
1466
|
+
params.push(relType);
|
|
1467
|
+
}
|
|
1468
|
+
query += ` ORDER BY created_at DESC`;
|
|
1469
|
+
const rows = await this.db.all(query, params);
|
|
1470
|
+
return rows.map((row) => this.rowToEdge(row));
|
|
1471
|
+
}
|
|
1472
|
+
/**
|
|
1473
|
+
* Find all edges for a node (both directions)
|
|
1474
|
+
*/
|
|
1475
|
+
async findByNode(nodeId) {
|
|
1476
|
+
const outgoing = await this.findBySrc(nodeId);
|
|
1477
|
+
const incoming = await this.findByDst(nodeId);
|
|
1478
|
+
return { outgoing, incoming };
|
|
1479
|
+
}
|
|
1480
|
+
/**
|
|
1481
|
+
* Delete edge by ID
|
|
1482
|
+
*/
|
|
1483
|
+
async delete(edgeId) {
|
|
1484
|
+
const result = await this.db.run(
|
|
1485
|
+
`DELETE FROM edges WHERE edge_id = ?`,
|
|
1486
|
+
[edgeId]
|
|
1487
|
+
);
|
|
1488
|
+
return true;
|
|
1489
|
+
}
|
|
1490
|
+
/**
|
|
1491
|
+
* Delete edges by source and relation type
|
|
1492
|
+
*/
|
|
1493
|
+
async deleteBySrcAndRel(srcId, relType) {
|
|
1494
|
+
await this.db.run(
|
|
1495
|
+
`DELETE FROM edges WHERE src_id = ? AND rel_type = ?`,
|
|
1496
|
+
[srcId, relType]
|
|
1497
|
+
);
|
|
1498
|
+
return 0;
|
|
1499
|
+
}
|
|
1500
|
+
/**
|
|
1501
|
+
* Delete edges by destination and relation type
|
|
1502
|
+
*/
|
|
1503
|
+
async deleteByDstAndRel(dstId, relType) {
|
|
1504
|
+
await this.db.run(
|
|
1505
|
+
`DELETE FROM edges WHERE dst_id = ? AND rel_type = ?`,
|
|
1506
|
+
[dstId, relType]
|
|
1507
|
+
);
|
|
1508
|
+
return 0;
|
|
1509
|
+
}
|
|
1510
|
+
/**
|
|
1511
|
+
* Replace edges for a source and relation type
|
|
1512
|
+
* Used for mode=replace in task_blockers_set
|
|
1513
|
+
*/
|
|
1514
|
+
async replaceEdges(srcId, relType, newEdges) {
|
|
1515
|
+
await this.deleteBySrcAndRel(srcId, relType);
|
|
1516
|
+
const created = [];
|
|
1517
|
+
for (const edge of newEdges) {
|
|
1518
|
+
const newEdge = await this.create({
|
|
1519
|
+
srcType: edge.srcType,
|
|
1520
|
+
srcId,
|
|
1521
|
+
relType,
|
|
1522
|
+
dstType: edge.dstType,
|
|
1523
|
+
dstId: edge.dstId,
|
|
1524
|
+
metaJson: edge.metaJson
|
|
1525
|
+
});
|
|
1526
|
+
created.push(newEdge);
|
|
1527
|
+
}
|
|
1528
|
+
return created;
|
|
1529
|
+
}
|
|
1530
|
+
/**
|
|
1531
|
+
* Get effective blockers (resolving condition → task)
|
|
1532
|
+
* Returns resolved blocker if condition has resolves_to edge
|
|
1533
|
+
*/
|
|
1534
|
+
async getEffectiveBlockers(taskId) {
|
|
1535
|
+
const blockerEdges = await this.findBySrc(taskId, "blocked_by");
|
|
1536
|
+
const results = [];
|
|
1537
|
+
for (const edge of blockerEdges) {
|
|
1538
|
+
const resolvesTo = await this.db.all(
|
|
1539
|
+
`SELECT dst_id FROM edges
|
|
1540
|
+
WHERE src_id = ? AND rel_type = 'resolves_to'
|
|
1541
|
+
LIMIT 1`,
|
|
1542
|
+
[edge.dstId]
|
|
1543
|
+
);
|
|
1544
|
+
if (resolvesTo.length > 0) {
|
|
1545
|
+
results.push({
|
|
1546
|
+
originalId: edge.dstId,
|
|
1547
|
+
effectiveId: resolvesTo[0].dst_id,
|
|
1548
|
+
isResolved: true
|
|
1549
|
+
});
|
|
1550
|
+
} else {
|
|
1551
|
+
results.push({
|
|
1552
|
+
originalId: edge.dstId,
|
|
1553
|
+
effectiveId: edge.dstId,
|
|
1554
|
+
isResolved: false
|
|
1555
|
+
});
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
return results;
|
|
1559
|
+
}
|
|
1560
|
+
/**
|
|
1561
|
+
* Find 2-hop related entries (Entry → Entity → Entry)
|
|
1562
|
+
*/
|
|
1563
|
+
async findRelatedEntries(entryId) {
|
|
1564
|
+
const rows = await this.db.all(
|
|
1565
|
+
`WITH first_hop AS (
|
|
1566
|
+
SELECT e1.dst_id AS entity_id
|
|
1567
|
+
FROM edges e1
|
|
1568
|
+
WHERE e1.src_type = 'entry'
|
|
1569
|
+
AND e1.rel_type = 'evidence_of'
|
|
1570
|
+
AND e1.src_id = ?
|
|
1571
|
+
)
|
|
1572
|
+
SELECT
|
|
1573
|
+
e2.src_id AS entry_id,
|
|
1574
|
+
f.entity_id AS via_entity_id,
|
|
1575
|
+
'evidence_of\u2192evidence_of' AS relation_path
|
|
1576
|
+
FROM first_hop f
|
|
1577
|
+
JOIN edges e2 ON e2.dst_id = f.entity_id
|
|
1578
|
+
AND e2.rel_type = 'evidence_of'
|
|
1579
|
+
AND e2.src_type = 'entry'
|
|
1580
|
+
WHERE e2.src_id != ?`,
|
|
1581
|
+
[entryId, entryId]
|
|
1582
|
+
);
|
|
1583
|
+
return rows.map((row) => ({
|
|
1584
|
+
entryId: row.entry_id,
|
|
1585
|
+
viaEntityId: row.via_entity_id,
|
|
1586
|
+
relationPath: row.relation_path
|
|
1587
|
+
}));
|
|
1588
|
+
}
|
|
1589
|
+
/**
|
|
1590
|
+
* Count edges by relation type
|
|
1591
|
+
*/
|
|
1592
|
+
async countByRelType() {
|
|
1593
|
+
const rows = await this.db.all(
|
|
1594
|
+
`SELECT rel_type, COUNT(*) as count FROM edges GROUP BY rel_type`
|
|
1595
|
+
);
|
|
1596
|
+
return rows.map((row) => ({
|
|
1597
|
+
relType: row.rel_type,
|
|
1598
|
+
count: Number(row.count)
|
|
1599
|
+
}));
|
|
1600
|
+
}
|
|
1601
|
+
/**
|
|
1602
|
+
* Convert database row to Edge
|
|
1603
|
+
*/
|
|
1604
|
+
rowToEdge(row) {
|
|
1605
|
+
return {
|
|
1606
|
+
edgeId: row.edge_id,
|
|
1607
|
+
srcType: row.src_type,
|
|
1608
|
+
srcId: row.src_id,
|
|
1609
|
+
relType: row.rel_type,
|
|
1610
|
+
dstType: row.dst_type,
|
|
1611
|
+
dstId: row.dst_id,
|
|
1612
|
+
metaJson: typeof row.meta_json === "string" ? JSON.parse(row.meta_json) : row.meta_json,
|
|
1613
|
+
createdAt: new Date(row.created_at)
|
|
1614
|
+
};
|
|
1615
|
+
}
|
|
1616
|
+
};
|
|
1617
|
+
|
|
1618
|
+
// src/core/vector-store.ts
|
|
1619
|
+
import * as lancedb from "@lancedb/lancedb";
|
|
1620
|
+
var VectorStore = class {
|
|
1621
|
+
constructor(dbPath) {
|
|
1622
|
+
this.dbPath = dbPath;
|
|
1623
|
+
}
|
|
1624
|
+
db = null;
|
|
1625
|
+
table = null;
|
|
1626
|
+
tableName = "conversations";
|
|
1627
|
+
/**
|
|
1628
|
+
* Initialize LanceDB connection
|
|
1629
|
+
*/
|
|
1630
|
+
async initialize() {
|
|
1631
|
+
if (this.db)
|
|
1632
|
+
return;
|
|
1633
|
+
this.db = await lancedb.connect(this.dbPath);
|
|
1634
|
+
try {
|
|
1635
|
+
const tables = await this.db.tableNames();
|
|
1636
|
+
if (tables.includes(this.tableName)) {
|
|
1637
|
+
this.table = await this.db.openTable(this.tableName);
|
|
1638
|
+
}
|
|
1639
|
+
} catch {
|
|
1640
|
+
this.table = null;
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
/**
|
|
1644
|
+
* Add or update vector record
|
|
1645
|
+
*/
|
|
1646
|
+
async upsert(record) {
|
|
1647
|
+
await this.initialize();
|
|
1648
|
+
if (!this.db) {
|
|
1649
|
+
throw new Error("Database not initialized");
|
|
1650
|
+
}
|
|
1651
|
+
const data = {
|
|
1652
|
+
id: record.id,
|
|
1653
|
+
eventId: record.eventId,
|
|
1654
|
+
sessionId: record.sessionId,
|
|
1655
|
+
eventType: record.eventType,
|
|
1656
|
+
content: record.content,
|
|
1657
|
+
vector: record.vector,
|
|
1658
|
+
timestamp: record.timestamp,
|
|
1659
|
+
metadata: JSON.stringify(record.metadata || {})
|
|
1660
|
+
};
|
|
1661
|
+
if (!this.table) {
|
|
1662
|
+
this.table = await this.db.createTable(this.tableName, [data]);
|
|
1663
|
+
} else {
|
|
1664
|
+
await this.table.add([data]);
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
/**
|
|
1668
|
+
* Add multiple vector records in batch
|
|
1669
|
+
*/
|
|
1670
|
+
async upsertBatch(records) {
|
|
1671
|
+
if (records.length === 0)
|
|
1672
|
+
return;
|
|
1673
|
+
await this.initialize();
|
|
1674
|
+
if (!this.db) {
|
|
1675
|
+
throw new Error("Database not initialized");
|
|
1676
|
+
}
|
|
1677
|
+
const data = records.map((record) => ({
|
|
1678
|
+
id: record.id,
|
|
1679
|
+
eventId: record.eventId,
|
|
1680
|
+
sessionId: record.sessionId,
|
|
1681
|
+
eventType: record.eventType,
|
|
1682
|
+
content: record.content,
|
|
1683
|
+
vector: record.vector,
|
|
1684
|
+
timestamp: record.timestamp,
|
|
1685
|
+
metadata: JSON.stringify(record.metadata || {})
|
|
1686
|
+
}));
|
|
1687
|
+
if (!this.table) {
|
|
1688
|
+
this.table = await this.db.createTable(this.tableName, data);
|
|
1689
|
+
} else {
|
|
1690
|
+
await this.table.add(data);
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
/**
|
|
1694
|
+
* Search for similar vectors
|
|
1695
|
+
*/
|
|
1696
|
+
async search(queryVector, options = {}) {
|
|
1697
|
+
await this.initialize();
|
|
1698
|
+
if (!this.table) {
|
|
1699
|
+
return [];
|
|
1700
|
+
}
|
|
1701
|
+
const { limit = 5, minScore = 0.7, sessionId } = options;
|
|
1702
|
+
let query = this.table.search(queryVector).limit(limit * 2);
|
|
1703
|
+
if (sessionId) {
|
|
1704
|
+
query = query.where(`sessionId = '${sessionId}'`);
|
|
1705
|
+
}
|
|
1706
|
+
const results = await query.toArray();
|
|
1707
|
+
return results.filter((r) => {
|
|
1708
|
+
const score = 1 - (r._distance || 0);
|
|
1709
|
+
return score >= minScore;
|
|
1710
|
+
}).slice(0, limit).map((r) => ({
|
|
1711
|
+
id: r.id,
|
|
1712
|
+
eventId: r.eventId,
|
|
1713
|
+
content: r.content,
|
|
1714
|
+
score: 1 - (r._distance || 0),
|
|
1715
|
+
sessionId: r.sessionId,
|
|
1716
|
+
eventType: r.eventType,
|
|
1717
|
+
timestamp: r.timestamp
|
|
1718
|
+
}));
|
|
1719
|
+
}
|
|
1720
|
+
/**
|
|
1721
|
+
* Delete vector by event ID
|
|
1722
|
+
*/
|
|
1723
|
+
async delete(eventId) {
|
|
1724
|
+
if (!this.table)
|
|
1725
|
+
return;
|
|
1726
|
+
await this.table.delete(`eventId = '${eventId}'`);
|
|
1727
|
+
}
|
|
1728
|
+
/**
|
|
1729
|
+
* Get total count of vectors
|
|
1730
|
+
*/
|
|
1731
|
+
async count() {
|
|
1732
|
+
if (!this.table)
|
|
1733
|
+
return 0;
|
|
1734
|
+
const result = await this.table.countRows();
|
|
1735
|
+
return result;
|
|
1736
|
+
}
|
|
1737
|
+
/**
|
|
1738
|
+
* Check if vector exists for event
|
|
1739
|
+
*/
|
|
1740
|
+
async exists(eventId) {
|
|
1741
|
+
if (!this.table)
|
|
1742
|
+
return false;
|
|
1743
|
+
const results = await this.table.search([]).where(`eventId = '${eventId}'`).limit(1).toArray();
|
|
1744
|
+
return results.length > 0;
|
|
1745
|
+
}
|
|
1746
|
+
};
|
|
1747
|
+
|
|
1748
|
+
// src/core/embedder.ts
|
|
1749
|
+
import { pipeline } from "@xenova/transformers";
|
|
1750
|
+
var Embedder = class {
|
|
1751
|
+
pipeline = null;
|
|
1752
|
+
modelName;
|
|
1753
|
+
initialized = false;
|
|
1754
|
+
constructor(modelName = "Xenova/all-MiniLM-L6-v2") {
|
|
1755
|
+
this.modelName = modelName;
|
|
1756
|
+
}
|
|
1757
|
+
/**
|
|
1758
|
+
* Initialize the embedding pipeline
|
|
1759
|
+
*/
|
|
1760
|
+
async initialize() {
|
|
1761
|
+
if (this.initialized)
|
|
1762
|
+
return;
|
|
1763
|
+
this.pipeline = await pipeline("feature-extraction", this.modelName);
|
|
1764
|
+
this.initialized = true;
|
|
1765
|
+
}
|
|
1766
|
+
/**
|
|
1767
|
+
* Generate embedding for a single text
|
|
1768
|
+
*/
|
|
1769
|
+
async embed(text) {
|
|
1770
|
+
await this.initialize();
|
|
1771
|
+
if (!this.pipeline) {
|
|
1772
|
+
throw new Error("Embedding pipeline not initialized");
|
|
1773
|
+
}
|
|
1774
|
+
const output = await this.pipeline(text, {
|
|
1775
|
+
pooling: "mean",
|
|
1776
|
+
normalize: true
|
|
1777
|
+
});
|
|
1778
|
+
const vector = Array.from(output.data);
|
|
1779
|
+
return {
|
|
1780
|
+
vector,
|
|
1781
|
+
model: this.modelName,
|
|
1782
|
+
dimensions: vector.length
|
|
1783
|
+
};
|
|
1784
|
+
}
|
|
1785
|
+
/**
|
|
1786
|
+
* Generate embeddings for multiple texts in batch
|
|
1787
|
+
*/
|
|
1788
|
+
async embedBatch(texts) {
|
|
1789
|
+
await this.initialize();
|
|
1790
|
+
if (!this.pipeline) {
|
|
1791
|
+
throw new Error("Embedding pipeline not initialized");
|
|
1792
|
+
}
|
|
1793
|
+
const results = [];
|
|
1794
|
+
const batchSize = 32;
|
|
1795
|
+
for (let i = 0; i < texts.length; i += batchSize) {
|
|
1796
|
+
const batch = texts.slice(i, i + batchSize);
|
|
1797
|
+
for (const text of batch) {
|
|
1798
|
+
const output = await this.pipeline(text, {
|
|
1799
|
+
pooling: "mean",
|
|
1800
|
+
normalize: true
|
|
1801
|
+
});
|
|
1802
|
+
const vector = Array.from(output.data);
|
|
1803
|
+
results.push({
|
|
1804
|
+
vector,
|
|
1805
|
+
model: this.modelName,
|
|
1806
|
+
dimensions: vector.length
|
|
1807
|
+
});
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
return results;
|
|
1811
|
+
}
|
|
1812
|
+
/**
|
|
1813
|
+
* Get embedding dimensions for the current model
|
|
1814
|
+
*/
|
|
1815
|
+
async getDimensions() {
|
|
1816
|
+
const result = await this.embed("test");
|
|
1817
|
+
return result.dimensions;
|
|
1818
|
+
}
|
|
1819
|
+
/**
|
|
1820
|
+
* Check if embedder is ready
|
|
1821
|
+
*/
|
|
1822
|
+
isReady() {
|
|
1823
|
+
return this.initialized && this.pipeline !== null;
|
|
1824
|
+
}
|
|
1825
|
+
/**
|
|
1826
|
+
* Get model name
|
|
1827
|
+
*/
|
|
1828
|
+
getModelName() {
|
|
1829
|
+
return this.modelName;
|
|
1830
|
+
}
|
|
1831
|
+
};
|
|
1832
|
+
var defaultEmbedder = null;
|
|
1833
|
+
function getDefaultEmbedder() {
|
|
1834
|
+
if (!defaultEmbedder) {
|
|
1835
|
+
defaultEmbedder = new Embedder();
|
|
1836
|
+
}
|
|
1837
|
+
return defaultEmbedder;
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
// src/core/vector-outbox.ts
|
|
1841
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
1842
|
+
var DEFAULT_CONFIG = {
|
|
1843
|
+
embeddingVersion: "v1",
|
|
1844
|
+
maxRetries: 3,
|
|
1845
|
+
stuckThresholdMs: 5 * 60 * 1e3,
|
|
1846
|
+
// 5 minutes
|
|
1847
|
+
cleanupDays: 7
|
|
1848
|
+
};
|
|
1849
|
+
var VectorOutbox = class {
|
|
1850
|
+
constructor(db, config) {
|
|
1851
|
+
this.db = db;
|
|
1852
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
1853
|
+
}
|
|
1854
|
+
config;
|
|
1855
|
+
/**
|
|
1856
|
+
* Enqueue item for vectorization (idempotent)
|
|
1857
|
+
*/
|
|
1858
|
+
async enqueue(itemKind, itemId, embeddingVersion) {
|
|
1859
|
+
const version = embeddingVersion ?? this.config.embeddingVersion;
|
|
1860
|
+
const jobId = randomUUID4();
|
|
1861
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1862
|
+
await this.db.run(
|
|
1863
|
+
`INSERT INTO vector_outbox (
|
|
1864
|
+
job_id, item_kind, item_id, embedding_version, status, retry_count, created_at, updated_at
|
|
1865
|
+
) VALUES (?, ?, ?, ?, 'pending', 0, ?, ?)
|
|
1866
|
+
ON CONFLICT (item_kind, item_id, embedding_version) DO NOTHING`,
|
|
1867
|
+
[jobId, itemKind, itemId, version, now, now]
|
|
1868
|
+
);
|
|
1869
|
+
return jobId;
|
|
1870
|
+
}
|
|
1871
|
+
/**
|
|
1872
|
+
* Claim pending jobs for processing
|
|
1873
|
+
*/
|
|
1874
|
+
async claimJobs(limit = 32) {
|
|
1875
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1876
|
+
const rows = await this.db.all(
|
|
1877
|
+
`UPDATE vector_outbox
|
|
1878
|
+
SET status = 'processing', updated_at = ?
|
|
1879
|
+
WHERE job_id IN (
|
|
1880
|
+
SELECT job_id FROM vector_outbox
|
|
1881
|
+
WHERE status = 'pending'
|
|
1882
|
+
ORDER BY created_at ASC
|
|
1883
|
+
LIMIT ?
|
|
1884
|
+
)
|
|
1885
|
+
RETURNING *`,
|
|
1886
|
+
[now, limit]
|
|
1887
|
+
);
|
|
1888
|
+
return rows.map((row) => this.rowToJob(row));
|
|
1889
|
+
}
|
|
1890
|
+
/**
|
|
1891
|
+
* Mark job as done
|
|
1892
|
+
*/
|
|
1893
|
+
async markDone(jobId) {
|
|
1894
|
+
await this.db.run(
|
|
1895
|
+
`UPDATE vector_outbox
|
|
1896
|
+
SET status = 'done', updated_at = ?
|
|
1897
|
+
WHERE job_id = ?`,
|
|
1898
|
+
[(/* @__PURE__ */ new Date()).toISOString(), jobId]
|
|
1899
|
+
);
|
|
1900
|
+
}
|
|
1901
|
+
/**
|
|
1902
|
+
* Mark job as failed
|
|
1903
|
+
*/
|
|
1904
|
+
async markFailed(jobId, error) {
|
|
1905
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1906
|
+
const rows = await this.db.all(
|
|
1907
|
+
`SELECT retry_count FROM vector_outbox WHERE job_id = ?`,
|
|
1908
|
+
[jobId]
|
|
1909
|
+
);
|
|
1910
|
+
if (rows.length === 0)
|
|
1911
|
+
return;
|
|
1912
|
+
const retryCount = rows[0].retry_count;
|
|
1913
|
+
const newStatus = retryCount >= this.config.maxRetries - 1 ? "failed" : "pending";
|
|
1914
|
+
await this.db.run(
|
|
1915
|
+
`UPDATE vector_outbox
|
|
1916
|
+
SET status = ?, error = ?, retry_count = retry_count + 1, updated_at = ?
|
|
1917
|
+
WHERE job_id = ?`,
|
|
1918
|
+
[newStatus, error, now, jobId]
|
|
1919
|
+
);
|
|
1920
|
+
}
|
|
1921
|
+
/**
|
|
1922
|
+
* Get job by ID
|
|
1923
|
+
*/
|
|
1924
|
+
async getJob(jobId) {
|
|
1925
|
+
const rows = await this.db.all(
|
|
1926
|
+
`SELECT * FROM vector_outbox WHERE job_id = ?`,
|
|
1927
|
+
[jobId]
|
|
1928
|
+
);
|
|
1929
|
+
if (rows.length === 0)
|
|
1930
|
+
return null;
|
|
1931
|
+
return this.rowToJob(rows[0]);
|
|
1932
|
+
}
|
|
1933
|
+
/**
|
|
1934
|
+
* Get jobs by status
|
|
1935
|
+
*/
|
|
1936
|
+
async getJobsByStatus(status, limit = 100) {
|
|
1937
|
+
const rows = await this.db.all(
|
|
1938
|
+
`SELECT * FROM vector_outbox
|
|
1939
|
+
WHERE status = ?
|
|
1940
|
+
ORDER BY created_at ASC
|
|
1941
|
+
LIMIT ?`,
|
|
1942
|
+
[status, limit]
|
|
1943
|
+
);
|
|
1944
|
+
return rows.map((row) => this.rowToJob(row));
|
|
1945
|
+
}
|
|
1946
|
+
/**
|
|
1947
|
+
* Reconcile: recover stuck and retry failed jobs
|
|
1948
|
+
*/
|
|
1949
|
+
async reconcile() {
|
|
1950
|
+
const now = /* @__PURE__ */ new Date();
|
|
1951
|
+
const stuckThreshold = new Date(now.getTime() - this.config.stuckThresholdMs);
|
|
1952
|
+
const recoveredResult = await this.db.run(
|
|
1953
|
+
`UPDATE vector_outbox
|
|
1954
|
+
SET status = 'pending', updated_at = ?
|
|
1955
|
+
WHERE status = 'processing'
|
|
1956
|
+
AND updated_at < ?`,
|
|
1957
|
+
[now.toISOString(), stuckThreshold.toISOString()]
|
|
1958
|
+
);
|
|
1959
|
+
const retriedResult = await this.db.run(
|
|
1960
|
+
`UPDATE vector_outbox
|
|
1961
|
+
SET status = 'pending', updated_at = ?
|
|
1962
|
+
WHERE status = 'failed'
|
|
1963
|
+
AND retry_count < ?`,
|
|
1964
|
+
[now.toISOString(), this.config.maxRetries]
|
|
1965
|
+
);
|
|
1966
|
+
const recoveredRows = await this.db.all(
|
|
1967
|
+
`SELECT COUNT(*) as count FROM vector_outbox
|
|
1968
|
+
WHERE status = 'pending' AND updated_at = ?`,
|
|
1969
|
+
[now.toISOString()]
|
|
1970
|
+
);
|
|
1971
|
+
return {
|
|
1972
|
+
recovered: 0,
|
|
1973
|
+
// Approximate
|
|
1974
|
+
retried: 0
|
|
1975
|
+
// Approximate
|
|
1976
|
+
};
|
|
1977
|
+
}
|
|
1978
|
+
/**
|
|
1979
|
+
* Cleanup old done jobs
|
|
1980
|
+
*/
|
|
1981
|
+
async cleanup() {
|
|
1982
|
+
const threshold = /* @__PURE__ */ new Date();
|
|
1983
|
+
threshold.setDate(threshold.getDate() - this.config.cleanupDays);
|
|
1984
|
+
await this.db.run(
|
|
1985
|
+
`DELETE FROM vector_outbox
|
|
1986
|
+
WHERE status = 'done'
|
|
1987
|
+
AND updated_at < ?`,
|
|
1988
|
+
[threshold.toISOString()]
|
|
1989
|
+
);
|
|
1990
|
+
return 0;
|
|
1991
|
+
}
|
|
1992
|
+
/**
|
|
1993
|
+
* Get metrics
|
|
1994
|
+
*/
|
|
1995
|
+
async getMetrics() {
|
|
1996
|
+
const statusCounts = await this.db.all(
|
|
1997
|
+
`SELECT status, COUNT(*) as count
|
|
1998
|
+
FROM vector_outbox
|
|
1999
|
+
GROUP BY status`
|
|
2000
|
+
);
|
|
2001
|
+
const oldestPending = await this.db.all(
|
|
2002
|
+
`SELECT created_at FROM vector_outbox
|
|
2003
|
+
WHERE status = 'pending'
|
|
2004
|
+
ORDER BY created_at ASC
|
|
2005
|
+
LIMIT 1`
|
|
2006
|
+
);
|
|
2007
|
+
const metrics = {
|
|
2008
|
+
pendingCount: 0,
|
|
2009
|
+
processingCount: 0,
|
|
2010
|
+
doneCount: 0,
|
|
2011
|
+
failedCount: 0,
|
|
2012
|
+
oldestPendingAge: null
|
|
2013
|
+
};
|
|
2014
|
+
for (const row of statusCounts) {
|
|
2015
|
+
switch (row.status) {
|
|
2016
|
+
case "pending":
|
|
2017
|
+
metrics.pendingCount = Number(row.count);
|
|
2018
|
+
break;
|
|
2019
|
+
case "processing":
|
|
2020
|
+
metrics.processingCount = Number(row.count);
|
|
2021
|
+
break;
|
|
2022
|
+
case "done":
|
|
2023
|
+
metrics.doneCount = Number(row.count);
|
|
2024
|
+
break;
|
|
2025
|
+
case "failed":
|
|
2026
|
+
metrics.failedCount = Number(row.count);
|
|
2027
|
+
break;
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
if (oldestPending.length > 0) {
|
|
2031
|
+
const oldestDate = new Date(oldestPending[0].created_at);
|
|
2032
|
+
metrics.oldestPendingAge = Date.now() - oldestDate.getTime();
|
|
2033
|
+
}
|
|
2034
|
+
return metrics;
|
|
2035
|
+
}
|
|
2036
|
+
/**
|
|
2037
|
+
* Validate state transition
|
|
2038
|
+
*/
|
|
2039
|
+
isValidTransition(from, to) {
|
|
2040
|
+
const validTransitions = [
|
|
2041
|
+
{ from: "pending", to: "processing" },
|
|
2042
|
+
{ from: "processing", to: "done" },
|
|
2043
|
+
{ from: "processing", to: "failed" },
|
|
2044
|
+
{ from: "failed", to: "pending" }
|
|
2045
|
+
];
|
|
2046
|
+
return validTransitions.some((t) => t.from === from && t.to === to);
|
|
2047
|
+
}
|
|
2048
|
+
/**
|
|
2049
|
+
* Convert database row to OutboxJob
|
|
2050
|
+
*/
|
|
2051
|
+
rowToJob(row) {
|
|
2052
|
+
return {
|
|
2053
|
+
jobId: row.job_id,
|
|
2054
|
+
itemKind: row.item_kind,
|
|
2055
|
+
itemId: row.item_id,
|
|
2056
|
+
embeddingVersion: row.embedding_version,
|
|
2057
|
+
status: row.status,
|
|
2058
|
+
retryCount: row.retry_count,
|
|
2059
|
+
error: row.error,
|
|
2060
|
+
createdAt: new Date(row.created_at),
|
|
2061
|
+
updatedAt: new Date(row.updated_at)
|
|
2062
|
+
};
|
|
2063
|
+
}
|
|
2064
|
+
};
|
|
2065
|
+
|
|
2066
|
+
// src/core/vector-worker.ts
|
|
2067
|
+
var DEFAULT_CONFIG2 = {
|
|
2068
|
+
batchSize: 32,
|
|
2069
|
+
pollIntervalMs: 1e3,
|
|
2070
|
+
maxRetries: 3
|
|
2071
|
+
};
|
|
2072
|
+
var VectorWorker = class {
|
|
2073
|
+
eventStore;
|
|
2074
|
+
vectorStore;
|
|
2075
|
+
embedder;
|
|
2076
|
+
config;
|
|
2077
|
+
running = false;
|
|
2078
|
+
pollTimeout = null;
|
|
2079
|
+
constructor(eventStore, vectorStore, embedder, config = {}) {
|
|
2080
|
+
this.eventStore = eventStore;
|
|
2081
|
+
this.vectorStore = vectorStore;
|
|
2082
|
+
this.embedder = embedder;
|
|
2083
|
+
this.config = { ...DEFAULT_CONFIG2, ...config };
|
|
2084
|
+
}
|
|
2085
|
+
/**
|
|
2086
|
+
* Start the worker polling loop
|
|
2087
|
+
*/
|
|
2088
|
+
start() {
|
|
2089
|
+
if (this.running)
|
|
2090
|
+
return;
|
|
2091
|
+
this.running = true;
|
|
2092
|
+
this.poll();
|
|
2093
|
+
}
|
|
2094
|
+
/**
|
|
2095
|
+
* Stop the worker
|
|
2096
|
+
*/
|
|
2097
|
+
stop() {
|
|
2098
|
+
this.running = false;
|
|
2099
|
+
if (this.pollTimeout) {
|
|
2100
|
+
clearTimeout(this.pollTimeout);
|
|
2101
|
+
this.pollTimeout = null;
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
/**
|
|
2105
|
+
* Process a single batch of outbox items
|
|
2106
|
+
*/
|
|
2107
|
+
async processBatch() {
|
|
2108
|
+
const items = await this.eventStore.getPendingOutboxItems(this.config.batchSize);
|
|
2109
|
+
if (items.length === 0) {
|
|
2110
|
+
return 0;
|
|
2111
|
+
}
|
|
2112
|
+
const successful = [];
|
|
2113
|
+
const failed = [];
|
|
2114
|
+
try {
|
|
2115
|
+
const embeddings = await this.embedder.embedBatch(items.map((i) => i.content));
|
|
2116
|
+
const records = [];
|
|
2117
|
+
for (let i = 0; i < items.length; i++) {
|
|
2118
|
+
const item = items[i];
|
|
2119
|
+
const embedding = embeddings[i];
|
|
2120
|
+
const event = await this.eventStore.getEvent(item.eventId);
|
|
2121
|
+
if (!event) {
|
|
2122
|
+
failed.push(item.id);
|
|
2123
|
+
continue;
|
|
2124
|
+
}
|
|
2125
|
+
records.push({
|
|
2126
|
+
id: `vec_${item.id}`,
|
|
2127
|
+
eventId: item.eventId,
|
|
2128
|
+
sessionId: event.sessionId,
|
|
2129
|
+
eventType: event.eventType,
|
|
2130
|
+
content: item.content,
|
|
2131
|
+
vector: embedding.vector,
|
|
2132
|
+
timestamp: event.timestamp.toISOString(),
|
|
2133
|
+
metadata: event.metadata
|
|
2134
|
+
});
|
|
2135
|
+
successful.push(item.id);
|
|
2136
|
+
}
|
|
2137
|
+
if (records.length > 0) {
|
|
2138
|
+
await this.vectorStore.upsertBatch(records);
|
|
2139
|
+
}
|
|
2140
|
+
if (successful.length > 0) {
|
|
2141
|
+
await this.eventStore.completeOutboxItems(successful);
|
|
2142
|
+
}
|
|
2143
|
+
if (failed.length > 0) {
|
|
2144
|
+
await this.eventStore.failOutboxItems(failed, "Event not found");
|
|
2145
|
+
}
|
|
2146
|
+
return successful.length;
|
|
2147
|
+
} catch (error) {
|
|
2148
|
+
const allIds = items.map((i) => i.id);
|
|
2149
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2150
|
+
await this.eventStore.failOutboxItems(allIds, errorMessage);
|
|
2151
|
+
throw error;
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
/**
|
|
2155
|
+
* Poll for new items
|
|
2156
|
+
*/
|
|
2157
|
+
async poll() {
|
|
2158
|
+
if (!this.running)
|
|
2159
|
+
return;
|
|
2160
|
+
try {
|
|
2161
|
+
await this.processBatch();
|
|
2162
|
+
} catch (error) {
|
|
2163
|
+
console.error("Vector worker error:", error);
|
|
2164
|
+
}
|
|
2165
|
+
this.pollTimeout = setTimeout(() => this.poll(), this.config.pollIntervalMs);
|
|
2166
|
+
}
|
|
2167
|
+
/**
|
|
2168
|
+
* Process all pending items (blocking)
|
|
2169
|
+
*/
|
|
2170
|
+
async processAll() {
|
|
2171
|
+
let totalProcessed = 0;
|
|
2172
|
+
let processed;
|
|
2173
|
+
do {
|
|
2174
|
+
processed = await this.processBatch();
|
|
2175
|
+
totalProcessed += processed;
|
|
2176
|
+
} while (processed > 0);
|
|
2177
|
+
return totalProcessed;
|
|
2178
|
+
}
|
|
2179
|
+
/**
|
|
2180
|
+
* Check if worker is running
|
|
2181
|
+
*/
|
|
2182
|
+
isRunning() {
|
|
2183
|
+
return this.running;
|
|
2184
|
+
}
|
|
2185
|
+
};
|
|
2186
|
+
function createVectorWorker(eventStore, vectorStore, embedder, config) {
|
|
2187
|
+
const worker = new VectorWorker(eventStore, vectorStore, embedder, config);
|
|
2188
|
+
return worker;
|
|
2189
|
+
}
|
|
2190
|
+
var DEFAULT_CONFIG_V2 = {
|
|
2191
|
+
batchSize: 32,
|
|
2192
|
+
pollIntervalMs: 1e3,
|
|
2193
|
+
maxRetries: 3,
|
|
2194
|
+
embeddingVersion: "v1"
|
|
2195
|
+
};
|
|
2196
|
+
var DefaultContentProvider = class {
|
|
2197
|
+
constructor(db) {
|
|
2198
|
+
this.db = db;
|
|
2199
|
+
}
|
|
2200
|
+
async getContent(itemKind, itemId) {
|
|
2201
|
+
switch (itemKind) {
|
|
2202
|
+
case "entry":
|
|
2203
|
+
return this.getEntryContent(itemId);
|
|
2204
|
+
case "task_title":
|
|
2205
|
+
return this.getTaskTitleContent(itemId);
|
|
2206
|
+
case "event":
|
|
2207
|
+
return this.getEventContent(itemId);
|
|
2208
|
+
default:
|
|
2209
|
+
return null;
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
async getEntryContent(entryId) {
|
|
2213
|
+
const rows = await this.db.all(
|
|
2214
|
+
`SELECT title, content_json, entry_type FROM entries WHERE entry_id = ?`,
|
|
2215
|
+
[entryId]
|
|
2216
|
+
);
|
|
2217
|
+
if (rows.length === 0)
|
|
2218
|
+
return null;
|
|
2219
|
+
const row = rows[0];
|
|
2220
|
+
const contentJson = typeof row.content_json === "string" ? JSON.parse(row.content_json) : row.content_json;
|
|
2221
|
+
return {
|
|
2222
|
+
content: `${row.title}
|
|
2223
|
+
${JSON.stringify(contentJson)}`,
|
|
2224
|
+
metadata: {
|
|
2225
|
+
itemKind: "entry",
|
|
2226
|
+
entryType: row.entry_type
|
|
2227
|
+
}
|
|
2228
|
+
};
|
|
2229
|
+
}
|
|
2230
|
+
async getTaskTitleContent(taskId) {
|
|
2231
|
+
const rows = await this.db.all(
|
|
2232
|
+
`SELECT title, search_text, current_json FROM entities
|
|
2233
|
+
WHERE entity_id = ? AND entity_type = 'task'`,
|
|
2234
|
+
[taskId]
|
|
2235
|
+
);
|
|
2236
|
+
if (rows.length === 0)
|
|
2237
|
+
return null;
|
|
2238
|
+
const row = rows[0];
|
|
2239
|
+
return {
|
|
2240
|
+
content: row.search_text || row.title,
|
|
2241
|
+
metadata: {
|
|
2242
|
+
itemKind: "task_title",
|
|
2243
|
+
entityType: "task"
|
|
2244
|
+
}
|
|
2245
|
+
};
|
|
2246
|
+
}
|
|
2247
|
+
async getEventContent(eventId) {
|
|
2248
|
+
const rows = await this.db.all(
|
|
2249
|
+
`SELECT content, event_type, session_id FROM events WHERE id = ?`,
|
|
2250
|
+
[eventId]
|
|
2251
|
+
);
|
|
2252
|
+
if (rows.length === 0)
|
|
2253
|
+
return null;
|
|
2254
|
+
const row = rows[0];
|
|
2255
|
+
return {
|
|
2256
|
+
content: row.content,
|
|
2257
|
+
metadata: {
|
|
2258
|
+
itemKind: "event",
|
|
2259
|
+
eventType: row.event_type,
|
|
2260
|
+
sessionId: row.session_id
|
|
2261
|
+
}
|
|
2262
|
+
};
|
|
2263
|
+
}
|
|
2264
|
+
};
|
|
2265
|
+
var VectorWorkerV2 = class {
|
|
2266
|
+
outbox;
|
|
2267
|
+
vectorStore;
|
|
2268
|
+
embedder;
|
|
2269
|
+
contentProvider;
|
|
2270
|
+
config;
|
|
2271
|
+
running = false;
|
|
2272
|
+
pollTimeout = null;
|
|
2273
|
+
constructor(db, vectorStore, embedder, config = {}, contentProvider) {
|
|
2274
|
+
this.outbox = new VectorOutbox(db, {
|
|
2275
|
+
embeddingVersion: config.embeddingVersion ?? DEFAULT_CONFIG_V2.embeddingVersion,
|
|
2276
|
+
maxRetries: config.maxRetries ?? DEFAULT_CONFIG_V2.maxRetries
|
|
2277
|
+
});
|
|
2278
|
+
this.vectorStore = vectorStore;
|
|
2279
|
+
this.embedder = embedder;
|
|
2280
|
+
this.config = { ...DEFAULT_CONFIG_V2, ...config };
|
|
2281
|
+
this.contentProvider = contentProvider ?? new DefaultContentProvider(db);
|
|
2282
|
+
}
|
|
2283
|
+
/**
|
|
2284
|
+
* Start the worker polling loop
|
|
2285
|
+
*/
|
|
2286
|
+
start() {
|
|
2287
|
+
if (this.running)
|
|
2288
|
+
return;
|
|
2289
|
+
this.running = true;
|
|
2290
|
+
this.poll();
|
|
2291
|
+
}
|
|
2292
|
+
/**
|
|
2293
|
+
* Stop the worker
|
|
2294
|
+
*/
|
|
2295
|
+
stop() {
|
|
2296
|
+
this.running = false;
|
|
2297
|
+
if (this.pollTimeout) {
|
|
2298
|
+
clearTimeout(this.pollTimeout);
|
|
2299
|
+
this.pollTimeout = null;
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
/**
|
|
2303
|
+
* Process a single batch of outbox jobs
|
|
2304
|
+
*/
|
|
2305
|
+
async processBatch() {
|
|
2306
|
+
const jobs = await this.outbox.claimJobs(this.config.batchSize);
|
|
2307
|
+
if (jobs.length === 0) {
|
|
2308
|
+
return 0;
|
|
2309
|
+
}
|
|
2310
|
+
let successCount = 0;
|
|
2311
|
+
for (const job of jobs) {
|
|
2312
|
+
try {
|
|
2313
|
+
await this.processJob(job);
|
|
2314
|
+
await this.outbox.markDone(job.jobId);
|
|
2315
|
+
successCount++;
|
|
2316
|
+
} catch (error) {
|
|
2317
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2318
|
+
await this.outbox.markFailed(job.jobId, errorMessage);
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
return successCount;
|
|
2322
|
+
}
|
|
2323
|
+
/**
|
|
2324
|
+
* Process a single job
|
|
2325
|
+
*/
|
|
2326
|
+
async processJob(job) {
|
|
2327
|
+
const contentData = await this.contentProvider.getContent(job.itemKind, job.itemId);
|
|
2328
|
+
if (!contentData) {
|
|
2329
|
+
return;
|
|
2330
|
+
}
|
|
2331
|
+
const embedding = await this.embedder.embed(contentData.content);
|
|
2332
|
+
const record = {
|
|
2333
|
+
id: `${job.itemKind}_${job.itemId}_${job.embeddingVersion}`,
|
|
2334
|
+
eventId: job.itemKind === "event" ? job.itemId : "",
|
|
2335
|
+
sessionId: contentData.metadata.sessionId ?? "",
|
|
2336
|
+
eventType: contentData.metadata.eventType ?? job.itemKind,
|
|
2337
|
+
content: contentData.content,
|
|
2338
|
+
vector: embedding.vector,
|
|
2339
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2340
|
+
metadata: {
|
|
2341
|
+
...contentData.metadata,
|
|
2342
|
+
embeddingVersion: job.embeddingVersion
|
|
2343
|
+
}
|
|
2344
|
+
};
|
|
2345
|
+
await this.vectorStore.upsertBatch([record]);
|
|
2346
|
+
}
|
|
2347
|
+
/**
|
|
2348
|
+
* Poll for new jobs
|
|
2349
|
+
*/
|
|
2350
|
+
async poll() {
|
|
2351
|
+
if (!this.running)
|
|
2352
|
+
return;
|
|
2353
|
+
try {
|
|
2354
|
+
await this.processBatch();
|
|
2355
|
+
} catch (error) {
|
|
2356
|
+
console.error("Vector worker V2 error:", error);
|
|
2357
|
+
}
|
|
2358
|
+
this.pollTimeout = setTimeout(() => this.poll(), this.config.pollIntervalMs);
|
|
2359
|
+
}
|
|
2360
|
+
/**
|
|
2361
|
+
* Process all pending jobs (blocking)
|
|
2362
|
+
*/
|
|
2363
|
+
async processAll() {
|
|
2364
|
+
let totalProcessed = 0;
|
|
2365
|
+
let processed;
|
|
2366
|
+
do {
|
|
2367
|
+
processed = await this.processBatch();
|
|
2368
|
+
totalProcessed += processed;
|
|
2369
|
+
} while (processed > 0);
|
|
2370
|
+
return totalProcessed;
|
|
2371
|
+
}
|
|
2372
|
+
/**
|
|
2373
|
+
* Run reconciliation
|
|
2374
|
+
*/
|
|
2375
|
+
async reconcile() {
|
|
2376
|
+
return this.outbox.reconcile();
|
|
2377
|
+
}
|
|
2378
|
+
/**
|
|
2379
|
+
* Get metrics
|
|
2380
|
+
*/
|
|
2381
|
+
async getMetrics() {
|
|
2382
|
+
return this.outbox.getMetrics();
|
|
2383
|
+
}
|
|
2384
|
+
/**
|
|
2385
|
+
* Check if worker is running
|
|
2386
|
+
*/
|
|
2387
|
+
isRunning() {
|
|
2388
|
+
return this.running;
|
|
2389
|
+
}
|
|
2390
|
+
/**
|
|
2391
|
+
* Get the outbox instance for direct access
|
|
2392
|
+
*/
|
|
2393
|
+
getOutbox() {
|
|
2394
|
+
return this.outbox;
|
|
2395
|
+
}
|
|
2396
|
+
};
|
|
2397
|
+
function createVectorWorkerV2(db, vectorStore, embedder, config) {
|
|
2398
|
+
return new VectorWorkerV2(db, vectorStore, embedder, config);
|
|
2399
|
+
}
|
|
2400
|
+
|
|
2401
|
+
// src/core/matcher.ts
|
|
2402
|
+
var DEFAULT_CONFIG3 = {
|
|
2403
|
+
weights: {
|
|
2404
|
+
semanticSimilarity: 0.4,
|
|
2405
|
+
ftsScore: 0.25,
|
|
2406
|
+
recencyBonus: 0.2,
|
|
2407
|
+
statusWeight: 0.15
|
|
2408
|
+
},
|
|
2409
|
+
minCombinedScore: 0.92,
|
|
2410
|
+
minGap: 0.03,
|
|
2411
|
+
suggestionThreshold: 0.75
|
|
2412
|
+
};
|
|
2413
|
+
var Matcher = class {
|
|
2414
|
+
config;
|
|
2415
|
+
constructor(config = {}) {
|
|
2416
|
+
this.config = {
|
|
2417
|
+
...DEFAULT_CONFIG3,
|
|
2418
|
+
...config,
|
|
2419
|
+
weights: { ...DEFAULT_CONFIG3.weights, ...config.weights }
|
|
2420
|
+
};
|
|
2421
|
+
}
|
|
2422
|
+
/**
|
|
2423
|
+
* Calculate combined score using AXIOMMIND weighted formula
|
|
2424
|
+
*/
|
|
2425
|
+
calculateCombinedScore(semanticScore, ftsScore = 0, recencyDays = 0, isActive = true) {
|
|
2426
|
+
const { weights } = this.config;
|
|
2427
|
+
const recencyBonus = Math.max(0, 1 - recencyDays / 30);
|
|
2428
|
+
const statusMultiplier = isActive ? 1 : 0.7;
|
|
2429
|
+
const combinedScore = weights.semanticSimilarity * semanticScore + weights.ftsScore * ftsScore + weights.recencyBonus * recencyBonus + weights.statusWeight * statusMultiplier;
|
|
2430
|
+
return Math.min(1, combinedScore);
|
|
2431
|
+
}
|
|
2432
|
+
/**
|
|
2433
|
+
* Classify match confidence based on AXIOMMIND thresholds
|
|
2434
|
+
*/
|
|
2435
|
+
classifyConfidence(topScore, secondScore) {
|
|
2436
|
+
const { minCombinedScore, minGap, suggestionThreshold } = this.config;
|
|
2437
|
+
const gap = secondScore !== null ? topScore - secondScore : Infinity;
|
|
2438
|
+
if (topScore >= minCombinedScore && gap >= minGap) {
|
|
2439
|
+
return "high";
|
|
2440
|
+
}
|
|
2441
|
+
if (topScore >= suggestionThreshold) {
|
|
2442
|
+
return "suggested";
|
|
2443
|
+
}
|
|
2444
|
+
return "none";
|
|
2445
|
+
}
|
|
2446
|
+
/**
|
|
2447
|
+
* Match search results to find best memory
|
|
2448
|
+
*/
|
|
2449
|
+
matchSearchResults(results, getEventAge) {
|
|
2450
|
+
if (results.length === 0) {
|
|
2451
|
+
return {
|
|
2452
|
+
match: null,
|
|
2453
|
+
confidence: "none"
|
|
2454
|
+
};
|
|
2455
|
+
}
|
|
2456
|
+
const scoredResults = results.map((result) => {
|
|
2457
|
+
const ageDays = getEventAge(result.eventId);
|
|
2458
|
+
const combinedScore = this.calculateCombinedScore(
|
|
2459
|
+
result.score,
|
|
2460
|
+
0,
|
|
2461
|
+
// FTS score - would need to be passed in
|
|
2462
|
+
ageDays,
|
|
2463
|
+
true
|
|
2464
|
+
// Assume active
|
|
2465
|
+
);
|
|
2466
|
+
return {
|
|
2467
|
+
result,
|
|
2468
|
+
combinedScore
|
|
2469
|
+
};
|
|
2470
|
+
});
|
|
2471
|
+
scoredResults.sort((a, b) => b.combinedScore - a.combinedScore);
|
|
2472
|
+
const topResult = scoredResults[0];
|
|
2473
|
+
const secondScore = scoredResults.length > 1 ? scoredResults[1].combinedScore : null;
|
|
2474
|
+
const confidence = this.classifyConfidence(topResult.combinedScore, secondScore);
|
|
2475
|
+
const match = {
|
|
2476
|
+
event: {
|
|
2477
|
+
id: topResult.result.eventId,
|
|
2478
|
+
eventType: topResult.result.eventType,
|
|
2479
|
+
sessionId: topResult.result.sessionId,
|
|
2480
|
+
timestamp: new Date(topResult.result.timestamp),
|
|
2481
|
+
content: topResult.result.content,
|
|
2482
|
+
canonicalKey: "",
|
|
2483
|
+
// Would need to be fetched
|
|
2484
|
+
dedupeKey: ""
|
|
2485
|
+
// Would need to be fetched
|
|
2486
|
+
},
|
|
2487
|
+
score: topResult.combinedScore
|
|
2488
|
+
};
|
|
2489
|
+
const gap = secondScore !== null ? topResult.combinedScore - secondScore : void 0;
|
|
2490
|
+
const alternatives = confidence === "suggested" ? scoredResults.slice(1, 4).map((sr) => ({
|
|
2491
|
+
event: {
|
|
2492
|
+
id: sr.result.eventId,
|
|
2493
|
+
eventType: sr.result.eventType,
|
|
2494
|
+
sessionId: sr.result.sessionId,
|
|
2495
|
+
timestamp: new Date(sr.result.timestamp),
|
|
2496
|
+
content: sr.result.content,
|
|
2497
|
+
canonicalKey: "",
|
|
2498
|
+
dedupeKey: ""
|
|
2499
|
+
},
|
|
2500
|
+
score: sr.combinedScore
|
|
2501
|
+
})) : void 0;
|
|
2502
|
+
return {
|
|
2503
|
+
match: confidence !== "none" ? match : null,
|
|
2504
|
+
confidence,
|
|
2505
|
+
gap,
|
|
2506
|
+
alternatives
|
|
2507
|
+
};
|
|
2508
|
+
}
|
|
2509
|
+
/**
|
|
2510
|
+
* Calculate days between two dates
|
|
2511
|
+
*/
|
|
2512
|
+
static calculateAgeDays(timestamp) {
|
|
2513
|
+
const now = /* @__PURE__ */ new Date();
|
|
2514
|
+
const diffMs = now.getTime() - timestamp.getTime();
|
|
2515
|
+
return diffMs / (1e3 * 60 * 60 * 24);
|
|
2516
|
+
}
|
|
2517
|
+
/**
|
|
2518
|
+
* Get current configuration
|
|
2519
|
+
*/
|
|
2520
|
+
getConfig() {
|
|
2521
|
+
return { ...this.config };
|
|
2522
|
+
}
|
|
2523
|
+
};
|
|
2524
|
+
var defaultMatcher = null;
|
|
2525
|
+
function getDefaultMatcher() {
|
|
2526
|
+
if (!defaultMatcher) {
|
|
2527
|
+
defaultMatcher = new Matcher();
|
|
2528
|
+
}
|
|
2529
|
+
return defaultMatcher;
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
// src/core/evidence-aligner.ts
|
|
2533
|
+
import { createHash as createHash2 } from "crypto";
|
|
2534
|
+
var DEFAULT_OPTIONS = {
|
|
2535
|
+
minMatchLength: 10,
|
|
2536
|
+
fuzzyThreshold: 0.8,
|
|
2537
|
+
maxMissingClaims: 2
|
|
2538
|
+
};
|
|
2539
|
+
var DEFAULT_OPTIONS_V2 = {
|
|
2540
|
+
minMatchLength: 5,
|
|
2541
|
+
exactMatchBonus: 1,
|
|
2542
|
+
normalizedThreshold: 0.95,
|
|
2543
|
+
fuzzyThreshold: 0.85,
|
|
2544
|
+
maxMissingRatio: 0.2
|
|
2545
|
+
};
|
|
2546
|
+
var EvidenceAligner = class {
|
|
2547
|
+
options;
|
|
2548
|
+
constructor(options = {}) {
|
|
2549
|
+
this.options = { ...DEFAULT_OPTIONS, ...options };
|
|
2550
|
+
}
|
|
2551
|
+
/**
|
|
2552
|
+
* Align claims against source content
|
|
2553
|
+
* Returns evidence spans showing where claims are supported
|
|
2554
|
+
*/
|
|
2555
|
+
align(claims, sourceContent) {
|
|
2556
|
+
const spans = [];
|
|
2557
|
+
const missingClaims = [];
|
|
2558
|
+
const normalizedSource = this.normalize(sourceContent);
|
|
2559
|
+
for (const claim of claims) {
|
|
2560
|
+
const normalizedClaim = this.normalize(claim);
|
|
2561
|
+
if (normalizedClaim.length < this.options.minMatchLength) {
|
|
2562
|
+
continue;
|
|
2563
|
+
}
|
|
2564
|
+
const exactSpan = this.findExactMatch(normalizedClaim, normalizedSource, sourceContent);
|
|
2565
|
+
if (exactSpan) {
|
|
2566
|
+
spans.push(exactSpan);
|
|
2567
|
+
continue;
|
|
2568
|
+
}
|
|
2569
|
+
const fuzzySpan = this.findFuzzyMatch(normalizedClaim, normalizedSource, sourceContent);
|
|
2570
|
+
if (fuzzySpan && fuzzySpan.confidence >= this.options.fuzzyThreshold) {
|
|
2571
|
+
spans.push(fuzzySpan);
|
|
2572
|
+
continue;
|
|
2573
|
+
}
|
|
2574
|
+
missingClaims.push(claim);
|
|
2575
|
+
}
|
|
2576
|
+
const totalClaims = claims.length;
|
|
2577
|
+
const alignedClaims = spans.length;
|
|
2578
|
+
const confidence = totalClaims > 0 ? alignedClaims / totalClaims : 1;
|
|
2579
|
+
const isAligned = missingClaims.length <= this.options.maxMissingClaims;
|
|
2580
|
+
return {
|
|
2581
|
+
isAligned,
|
|
2582
|
+
confidence,
|
|
2583
|
+
spans,
|
|
2584
|
+
missingClaims
|
|
2585
|
+
};
|
|
2586
|
+
}
|
|
2587
|
+
/**
|
|
2588
|
+
* Find exact substring match
|
|
2589
|
+
*/
|
|
2590
|
+
findExactMatch(normalizedClaim, normalizedSource, originalSource) {
|
|
2591
|
+
const index = normalizedSource.indexOf(normalizedClaim);
|
|
2592
|
+
if (index === -1) {
|
|
2593
|
+
return null;
|
|
2594
|
+
}
|
|
2595
|
+
return {
|
|
2596
|
+
start: index,
|
|
2597
|
+
end: index + normalizedClaim.length,
|
|
2598
|
+
confidence: 1,
|
|
2599
|
+
matchType: "exact",
|
|
2600
|
+
originalQuote: originalSource.slice(index, index + normalizedClaim.length),
|
|
2601
|
+
alignedText: normalizedClaim
|
|
2602
|
+
};
|
|
2603
|
+
}
|
|
2604
|
+
/**
|
|
2605
|
+
* Find fuzzy match using sliding window
|
|
2606
|
+
*/
|
|
2607
|
+
findFuzzyMatch(normalizedClaim, normalizedSource, originalSource) {
|
|
2608
|
+
const windowSize = normalizedClaim.length;
|
|
2609
|
+
let bestMatch = null;
|
|
2610
|
+
for (let i = 0; i <= normalizedSource.length - windowSize; i++) {
|
|
2611
|
+
const window = normalizedSource.slice(i, i + windowSize);
|
|
2612
|
+
const similarity = this.calculateSimilarity(normalizedClaim, window);
|
|
2613
|
+
if (!bestMatch || similarity > bestMatch.similarity) {
|
|
2614
|
+
bestMatch = { index: i, similarity };
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
if (!bestMatch || bestMatch.similarity < this.options.fuzzyThreshold) {
|
|
2618
|
+
return null;
|
|
2619
|
+
}
|
|
2620
|
+
return {
|
|
2621
|
+
start: bestMatch.index,
|
|
2622
|
+
end: bestMatch.index + windowSize,
|
|
2623
|
+
confidence: bestMatch.similarity,
|
|
2624
|
+
matchType: "fuzzy",
|
|
2625
|
+
originalQuote: originalSource.slice(bestMatch.index, bestMatch.index + windowSize),
|
|
2626
|
+
alignedText: normalizedClaim
|
|
2627
|
+
};
|
|
2628
|
+
}
|
|
2629
|
+
/**
|
|
2630
|
+
* Calculate similarity between two strings using Jaccard coefficient
|
|
2631
|
+
*/
|
|
2632
|
+
calculateSimilarity(a, b) {
|
|
2633
|
+
const setA = new Set(this.tokenize(a));
|
|
2634
|
+
const setB = new Set(this.tokenize(b));
|
|
2635
|
+
const intersection = new Set([...setA].filter((x) => setB.has(x)));
|
|
2636
|
+
const union = /* @__PURE__ */ new Set([...setA, ...setB]);
|
|
2637
|
+
return intersection.size / union.size;
|
|
2638
|
+
}
|
|
2639
|
+
/**
|
|
2640
|
+
* Tokenize text into words
|
|
2641
|
+
*/
|
|
2642
|
+
tokenize(text) {
|
|
2643
|
+
return text.toLowerCase().split(/\s+/).filter((t) => t.length > 0);
|
|
2644
|
+
}
|
|
2645
|
+
/**
|
|
2646
|
+
* Normalize text for comparison
|
|
2647
|
+
*/
|
|
2648
|
+
normalize(text) {
|
|
2649
|
+
return text.normalize("NFKC").toLowerCase().replace(/[^\p{L}\p{N}\s]/gu, "").replace(/\s+/g, " ").trim();
|
|
2650
|
+
}
|
|
2651
|
+
/**
|
|
2652
|
+
* Extract claims from a response text
|
|
2653
|
+
* Splits on sentence boundaries and filters short sentences
|
|
2654
|
+
*/
|
|
2655
|
+
extractClaims(text) {
|
|
2656
|
+
const sentences = text.split(/[.!?]+/).map((s) => s.trim()).filter((s) => s.length > 0);
|
|
2657
|
+
return sentences.filter((s) => {
|
|
2658
|
+
return s.length >= this.options.minMatchLength && !s.endsWith("?");
|
|
2659
|
+
});
|
|
2660
|
+
}
|
|
2661
|
+
/**
|
|
2662
|
+
* Verify that a response is grounded in the provided context
|
|
2663
|
+
*/
|
|
2664
|
+
verifyGrounding(response, context) {
|
|
2665
|
+
const claims = this.extractClaims(response);
|
|
2666
|
+
const combinedContext = context.join(" ");
|
|
2667
|
+
return this.align(claims, combinedContext);
|
|
2668
|
+
}
|
|
2669
|
+
// ============================================================
|
|
2670
|
+
// V2 Methods: Quote-only alignment
|
|
2671
|
+
// ============================================================
|
|
2672
|
+
optionsV2 = DEFAULT_OPTIONS_V2;
|
|
2673
|
+
/**
|
|
2674
|
+
* Configure V2 options
|
|
2675
|
+
*/
|
|
2676
|
+
configureV2(options) {
|
|
2677
|
+
this.optionsV2 = { ...DEFAULT_OPTIONS_V2, ...options };
|
|
2678
|
+
}
|
|
2679
|
+
/**
|
|
2680
|
+
* Align V2: Process extracted evidence with messageIndex and quote
|
|
2681
|
+
* @param sessionMessages - Array of session messages (original text)
|
|
2682
|
+
* @param evidence - Array of extracted evidence (messageIndex + quote)
|
|
2683
|
+
*/
|
|
2684
|
+
alignV2(sessionMessages, evidence) {
|
|
2685
|
+
const results = [];
|
|
2686
|
+
let alignedCount = 0;
|
|
2687
|
+
let totalConfidence = 0;
|
|
2688
|
+
for (const ev of evidence) {
|
|
2689
|
+
const result = this.alignSingleEvidence(sessionMessages, ev);
|
|
2690
|
+
results.push(result);
|
|
2691
|
+
if (result.aligned) {
|
|
2692
|
+
alignedCount++;
|
|
2693
|
+
totalConfidence += result.evidence.confidence;
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
const failedCount = evidence.length - alignedCount;
|
|
2697
|
+
const maxMissing = Math.floor(evidence.length * this.optionsV2.maxMissingRatio);
|
|
2698
|
+
const evidenceAligned = failedCount <= maxMissing;
|
|
2699
|
+
const overallConfidence = evidence.length > 0 ? totalConfidence / evidence.length : 1;
|
|
2700
|
+
return {
|
|
2701
|
+
evidenceAligned,
|
|
2702
|
+
alignedCount,
|
|
2703
|
+
failedCount,
|
|
2704
|
+
results,
|
|
2705
|
+
overallConfidence
|
|
2706
|
+
};
|
|
2707
|
+
}
|
|
2708
|
+
/**
|
|
2709
|
+
* Align a single evidence item
|
|
2710
|
+
*/
|
|
2711
|
+
alignSingleEvidence(sessionMessages, evidence) {
|
|
2712
|
+
const { messageIndex, quote } = evidence;
|
|
2713
|
+
if (messageIndex < 0 || messageIndex >= sessionMessages.length) {
|
|
2714
|
+
return {
|
|
2715
|
+
aligned: false,
|
|
2716
|
+
evidence: {
|
|
2717
|
+
messageIndex,
|
|
2718
|
+
quote,
|
|
2719
|
+
failureReason: "invalid_index"
|
|
2720
|
+
}
|
|
2721
|
+
};
|
|
2722
|
+
}
|
|
2723
|
+
if (!quote || quote.trim().length === 0) {
|
|
2724
|
+
return {
|
|
2725
|
+
aligned: false,
|
|
2726
|
+
evidence: {
|
|
2727
|
+
messageIndex,
|
|
2728
|
+
quote,
|
|
2729
|
+
failureReason: "empty_quote"
|
|
2730
|
+
}
|
|
2731
|
+
};
|
|
2732
|
+
}
|
|
2733
|
+
const sourceMessage = sessionMessages[messageIndex];
|
|
2734
|
+
const exactResult = this.tryExactMatchV2(quote, sourceMessage, messageIndex);
|
|
2735
|
+
if (exactResult) {
|
|
2736
|
+
return exactResult;
|
|
2737
|
+
}
|
|
2738
|
+
const normalizedResult = this.tryNormalizedMatchV2(quote, sourceMessage, messageIndex);
|
|
2739
|
+
if (normalizedResult) {
|
|
2740
|
+
return normalizedResult;
|
|
2741
|
+
}
|
|
2742
|
+
const fuzzyResult = this.tryFuzzyMatchV2(quote, sourceMessage, messageIndex);
|
|
2743
|
+
if (fuzzyResult) {
|
|
2744
|
+
return fuzzyResult;
|
|
2745
|
+
}
|
|
2746
|
+
return {
|
|
2747
|
+
aligned: false,
|
|
2748
|
+
evidence: {
|
|
2749
|
+
messageIndex,
|
|
2750
|
+
quote,
|
|
2751
|
+
failureReason: "not_found"
|
|
2752
|
+
}
|
|
2753
|
+
};
|
|
2754
|
+
}
|
|
2755
|
+
/**
|
|
2756
|
+
* Try exact substring match
|
|
2757
|
+
*/
|
|
2758
|
+
tryExactMatchV2(quote, source, messageIndex) {
|
|
2759
|
+
const index = source.indexOf(quote);
|
|
2760
|
+
if (index === -1) {
|
|
2761
|
+
return null;
|
|
2762
|
+
}
|
|
2763
|
+
return {
|
|
2764
|
+
aligned: true,
|
|
2765
|
+
evidence: {
|
|
2766
|
+
messageIndex,
|
|
2767
|
+
quote,
|
|
2768
|
+
spanStart: index,
|
|
2769
|
+
spanEnd: index + quote.length,
|
|
2770
|
+
quoteHash: this.hashQuote(quote),
|
|
2771
|
+
confidence: 1,
|
|
2772
|
+
matchMethod: "exact"
|
|
2773
|
+
}
|
|
2774
|
+
};
|
|
2775
|
+
}
|
|
2776
|
+
/**
|
|
2777
|
+
* Try normalized match (whitespace collapsed)
|
|
2778
|
+
*/
|
|
2779
|
+
tryNormalizedMatchV2(quote, source, messageIndex) {
|
|
2780
|
+
const normalizedQuote = this.normalizeWhitespace(quote);
|
|
2781
|
+
const normalizedSource = this.normalizeWhitespace(source);
|
|
2782
|
+
const normalizedIndex = normalizedSource.indexOf(normalizedQuote);
|
|
2783
|
+
if (normalizedIndex === -1) {
|
|
2784
|
+
return null;
|
|
2785
|
+
}
|
|
2786
|
+
const originalSpan = this.mapToOriginalPositions(
|
|
2787
|
+
source,
|
|
2788
|
+
normalizedSource,
|
|
2789
|
+
normalizedIndex,
|
|
2790
|
+
normalizedIndex + normalizedQuote.length
|
|
2791
|
+
);
|
|
2792
|
+
if (!originalSpan) {
|
|
2793
|
+
return null;
|
|
2794
|
+
}
|
|
2795
|
+
return {
|
|
2796
|
+
aligned: true,
|
|
2797
|
+
evidence: {
|
|
2798
|
+
messageIndex,
|
|
2799
|
+
quote,
|
|
2800
|
+
spanStart: originalSpan.start,
|
|
2801
|
+
spanEnd: originalSpan.end,
|
|
2802
|
+
quoteHash: this.hashQuote(quote),
|
|
2803
|
+
confidence: 0.95,
|
|
2804
|
+
matchMethod: "normalized"
|
|
2805
|
+
}
|
|
2806
|
+
};
|
|
2807
|
+
}
|
|
2808
|
+
/**
|
|
2809
|
+
* Try fuzzy match using sliding window
|
|
2810
|
+
*/
|
|
2811
|
+
tryFuzzyMatchV2(quote, source, messageIndex) {
|
|
2812
|
+
const normalizedQuote = this.normalize(quote);
|
|
2813
|
+
const normalizedSource = this.normalize(source);
|
|
2814
|
+
if (normalizedQuote.length < this.optionsV2.minMatchLength) {
|
|
2815
|
+
return null;
|
|
2816
|
+
}
|
|
2817
|
+
const windowSizes = [
|
|
2818
|
+
normalizedQuote.length,
|
|
2819
|
+
Math.floor(normalizedQuote.length * 1.1),
|
|
2820
|
+
Math.floor(normalizedQuote.length * 1.2)
|
|
2821
|
+
];
|
|
2822
|
+
let bestMatch = null;
|
|
2823
|
+
for (const windowSize of windowSizes) {
|
|
2824
|
+
for (let i = 0; i <= normalizedSource.length - windowSize; i++) {
|
|
2825
|
+
const window = normalizedSource.slice(i, i + windowSize);
|
|
2826
|
+
const similarity = this.calculateLevenshteinSimilarity(normalizedQuote, window);
|
|
2827
|
+
if (similarity >= this.optionsV2.fuzzyThreshold) {
|
|
2828
|
+
if (!bestMatch || similarity > bestMatch.similarity) {
|
|
2829
|
+
bestMatch = { index: i, windowSize, similarity };
|
|
2830
|
+
}
|
|
2831
|
+
}
|
|
2832
|
+
}
|
|
2833
|
+
}
|
|
2834
|
+
if (!bestMatch) {
|
|
2835
|
+
return null;
|
|
2836
|
+
}
|
|
2837
|
+
const originalSpan = this.mapToOriginalPositions(
|
|
2838
|
+
source,
|
|
2839
|
+
normalizedSource,
|
|
2840
|
+
bestMatch.index,
|
|
2841
|
+
bestMatch.index + bestMatch.windowSize
|
|
2842
|
+
);
|
|
2843
|
+
if (!originalSpan) {
|
|
2844
|
+
return null;
|
|
2845
|
+
}
|
|
2846
|
+
return {
|
|
2847
|
+
aligned: true,
|
|
2848
|
+
evidence: {
|
|
2849
|
+
messageIndex,
|
|
2850
|
+
quote,
|
|
2851
|
+
spanStart: originalSpan.start,
|
|
2852
|
+
spanEnd: originalSpan.end,
|
|
2853
|
+
quoteHash: this.hashQuote(quote),
|
|
2854
|
+
confidence: bestMatch.similarity,
|
|
2855
|
+
matchMethod: "fuzzy"
|
|
2856
|
+
}
|
|
2857
|
+
};
|
|
2858
|
+
}
|
|
2859
|
+
/**
|
|
2860
|
+
* Normalize whitespace only (preserve other characters)
|
|
2861
|
+
*/
|
|
2862
|
+
normalizeWhitespace(text) {
|
|
2863
|
+
return text.replace(/[\t\r]/g, " ").replace(/\n+/g, " ").replace(/ +/g, " ").trim();
|
|
2864
|
+
}
|
|
2865
|
+
/**
|
|
2866
|
+
* Map normalized positions back to original
|
|
2867
|
+
*/
|
|
2868
|
+
mapToOriginalPositions(original, normalized, normalizedStart, normalizedEnd) {
|
|
2869
|
+
const normalizedToOriginal = /* @__PURE__ */ new Map();
|
|
2870
|
+
let normalizedPos = 0;
|
|
2871
|
+
for (let origPos = 0; origPos < original.length; origPos++) {
|
|
2872
|
+
const char = original[origPos];
|
|
2873
|
+
if (/\s/.test(char)) {
|
|
2874
|
+
if (normalizedPos < normalized.length && /\s/.test(normalized[normalizedPos])) {
|
|
2875
|
+
normalizedToOriginal.set(normalizedPos, origPos);
|
|
2876
|
+
normalizedPos++;
|
|
2877
|
+
while (origPos + 1 < original.length && /\s/.test(original[origPos + 1])) {
|
|
2878
|
+
origPos++;
|
|
2879
|
+
}
|
|
2880
|
+
}
|
|
2881
|
+
} else {
|
|
2882
|
+
normalizedToOriginal.set(normalizedPos, origPos);
|
|
2883
|
+
normalizedPos++;
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
const startOrig = normalizedToOriginal.get(normalizedStart);
|
|
2887
|
+
let endOrig = normalizedToOriginal.get(normalizedEnd - 1);
|
|
2888
|
+
if (startOrig === void 0) {
|
|
2889
|
+
return null;
|
|
2890
|
+
}
|
|
2891
|
+
if (endOrig === void 0) {
|
|
2892
|
+
endOrig = original.length - 1;
|
|
2893
|
+
}
|
|
2894
|
+
return {
|
|
2895
|
+
start: startOrig,
|
|
2896
|
+
end: endOrig + 1
|
|
2897
|
+
};
|
|
2898
|
+
}
|
|
2899
|
+
/**
|
|
2900
|
+
* Calculate Levenshtein distance similarity
|
|
2901
|
+
*/
|
|
2902
|
+
calculateLevenshteinSimilarity(a, b) {
|
|
2903
|
+
const m = a.length;
|
|
2904
|
+
const n = b.length;
|
|
2905
|
+
if (m === 0)
|
|
2906
|
+
return n === 0 ? 1 : 0;
|
|
2907
|
+
if (n === 0)
|
|
2908
|
+
return 0;
|
|
2909
|
+
const dp = Array(m + 1).fill(null).map(() => Array(n + 1).fill(0));
|
|
2910
|
+
for (let i = 0; i <= m; i++)
|
|
2911
|
+
dp[i][0] = i;
|
|
2912
|
+
for (let j = 0; j <= n; j++)
|
|
2913
|
+
dp[0][j] = j;
|
|
2914
|
+
for (let i = 1; i <= m; i++) {
|
|
2915
|
+
for (let j = 1; j <= n; j++) {
|
|
2916
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
2917
|
+
dp[i][j] = Math.min(
|
|
2918
|
+
dp[i - 1][j] + 1,
|
|
2919
|
+
// deletion
|
|
2920
|
+
dp[i][j - 1] + 1,
|
|
2921
|
+
// insertion
|
|
2922
|
+
dp[i - 1][j - 1] + cost
|
|
2923
|
+
// substitution
|
|
2924
|
+
);
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
2927
|
+
const distance = dp[m][n];
|
|
2928
|
+
const maxLen = Math.max(m, n);
|
|
2929
|
+
return 1 - distance / maxLen;
|
|
2930
|
+
}
|
|
2931
|
+
/**
|
|
2932
|
+
* Hash quote for deduplication
|
|
2933
|
+
*/
|
|
2934
|
+
hashQuote(quote) {
|
|
2935
|
+
return createHash2("sha256").update(quote).digest("hex").slice(0, 16);
|
|
2936
|
+
}
|
|
2937
|
+
/**
|
|
2938
|
+
* Convert V2 result to V1 format for backwards compatibility
|
|
2939
|
+
*/
|
|
2940
|
+
convertToV1Result(v2Result) {
|
|
2941
|
+
const spans = [];
|
|
2942
|
+
const missingClaims = [];
|
|
2943
|
+
for (const result of v2Result.results) {
|
|
2944
|
+
if (result.aligned) {
|
|
2945
|
+
const ev = result.evidence;
|
|
2946
|
+
spans.push({
|
|
2947
|
+
start: ev.spanStart,
|
|
2948
|
+
end: ev.spanEnd,
|
|
2949
|
+
confidence: ev.confidence,
|
|
2950
|
+
matchType: ev.matchMethod === "exact" ? "exact" : "fuzzy",
|
|
2951
|
+
originalQuote: ev.quote,
|
|
2952
|
+
alignedText: ev.quote
|
|
2953
|
+
});
|
|
2954
|
+
} else {
|
|
2955
|
+
const ev = result.evidence;
|
|
2956
|
+
missingClaims.push(ev.quote);
|
|
2957
|
+
}
|
|
2958
|
+
}
|
|
2959
|
+
return {
|
|
2960
|
+
isAligned: v2Result.evidenceAligned,
|
|
2961
|
+
confidence: v2Result.overallConfidence,
|
|
2962
|
+
spans,
|
|
2963
|
+
missingClaims
|
|
2964
|
+
};
|
|
2965
|
+
}
|
|
2966
|
+
};
|
|
2967
|
+
var defaultAligner = null;
|
|
2968
|
+
function getDefaultAligner() {
|
|
2969
|
+
if (!defaultAligner) {
|
|
2970
|
+
defaultAligner = new EvidenceAligner();
|
|
2971
|
+
}
|
|
2972
|
+
return defaultAligner;
|
|
2973
|
+
}
|
|
2974
|
+
|
|
2975
|
+
// src/core/retriever.ts
|
|
2976
|
+
var DEFAULT_OPTIONS2 = {
|
|
2977
|
+
topK: 5,
|
|
2978
|
+
minScore: 0.7,
|
|
2979
|
+
maxTokens: 2e3,
|
|
2980
|
+
includeSessionContext: true
|
|
2981
|
+
};
|
|
2982
|
+
var Retriever = class {
|
|
2983
|
+
eventStore;
|
|
2984
|
+
vectorStore;
|
|
2985
|
+
embedder;
|
|
2986
|
+
matcher;
|
|
2987
|
+
constructor(eventStore, vectorStore, embedder, matcher) {
|
|
2988
|
+
this.eventStore = eventStore;
|
|
2989
|
+
this.vectorStore = vectorStore;
|
|
2990
|
+
this.embedder = embedder;
|
|
2991
|
+
this.matcher = matcher;
|
|
2992
|
+
}
|
|
2993
|
+
/**
|
|
2994
|
+
* Retrieve relevant memories for a query
|
|
2995
|
+
*/
|
|
2996
|
+
async retrieve(query, options = {}) {
|
|
2997
|
+
const opts = { ...DEFAULT_OPTIONS2, ...options };
|
|
2998
|
+
const queryEmbedding = await this.embedder.embed(query);
|
|
2999
|
+
const searchResults = await this.vectorStore.search(queryEmbedding.vector, {
|
|
3000
|
+
limit: opts.topK * 2,
|
|
3001
|
+
// Get extra for filtering
|
|
3002
|
+
minScore: opts.minScore,
|
|
3003
|
+
sessionId: opts.sessionId
|
|
3004
|
+
});
|
|
3005
|
+
const matchResult = this.matcher.matchSearchResults(
|
|
3006
|
+
searchResults,
|
|
3007
|
+
(eventId) => this.getEventAgeDays(eventId)
|
|
3008
|
+
);
|
|
3009
|
+
const memories = await this.enrichResults(searchResults.slice(0, opts.topK), opts);
|
|
3010
|
+
const context = this.buildContext(memories, opts.maxTokens);
|
|
3011
|
+
return {
|
|
3012
|
+
memories,
|
|
3013
|
+
matchResult,
|
|
3014
|
+
totalTokens: this.estimateTokens(context),
|
|
3015
|
+
context
|
|
3016
|
+
};
|
|
3017
|
+
}
|
|
3018
|
+
/**
|
|
3019
|
+
* Retrieve memories from a specific session
|
|
3020
|
+
*/
|
|
3021
|
+
async retrieveFromSession(sessionId) {
|
|
3022
|
+
return this.eventStore.getSessionEvents(sessionId);
|
|
3023
|
+
}
|
|
3024
|
+
/**
|
|
3025
|
+
* Get recent memories across all sessions
|
|
3026
|
+
*/
|
|
3027
|
+
async retrieveRecent(limit = 100) {
|
|
3028
|
+
return this.eventStore.getRecentEvents(limit);
|
|
3029
|
+
}
|
|
3030
|
+
/**
|
|
3031
|
+
* Enrich search results with full event data
|
|
3032
|
+
*/
|
|
3033
|
+
async enrichResults(results, options) {
|
|
3034
|
+
const memories = [];
|
|
3035
|
+
for (const result of results) {
|
|
3036
|
+
const event = await this.eventStore.getEvent(result.eventId);
|
|
3037
|
+
if (!event)
|
|
3038
|
+
continue;
|
|
3039
|
+
let sessionContext;
|
|
3040
|
+
if (options.includeSessionContext) {
|
|
3041
|
+
sessionContext = await this.getSessionContext(event.sessionId, event.id);
|
|
3042
|
+
}
|
|
3043
|
+
memories.push({
|
|
3044
|
+
event,
|
|
3045
|
+
score: result.score,
|
|
3046
|
+
sessionContext
|
|
3047
|
+
});
|
|
3048
|
+
}
|
|
3049
|
+
return memories;
|
|
3050
|
+
}
|
|
3051
|
+
/**
|
|
3052
|
+
* Get surrounding context from the same session
|
|
3053
|
+
*/
|
|
3054
|
+
async getSessionContext(sessionId, eventId) {
|
|
3055
|
+
const sessionEvents = await this.eventStore.getSessionEvents(sessionId);
|
|
3056
|
+
const eventIndex = sessionEvents.findIndex((e) => e.id === eventId);
|
|
3057
|
+
if (eventIndex === -1)
|
|
3058
|
+
return void 0;
|
|
3059
|
+
const start = Math.max(0, eventIndex - 1);
|
|
3060
|
+
const end = Math.min(sessionEvents.length, eventIndex + 2);
|
|
3061
|
+
const contextEvents = sessionEvents.slice(start, end);
|
|
3062
|
+
if (contextEvents.length <= 1)
|
|
3063
|
+
return void 0;
|
|
3064
|
+
return contextEvents.filter((e) => e.id !== eventId).map((e) => `[${e.eventType}]: ${e.content.slice(0, 200)}...`).join("\n");
|
|
3065
|
+
}
|
|
3066
|
+
/**
|
|
3067
|
+
* Build context string from memories (respecting token limit)
|
|
3068
|
+
*/
|
|
3069
|
+
buildContext(memories, maxTokens) {
|
|
3070
|
+
const parts = [];
|
|
3071
|
+
let currentTokens = 0;
|
|
3072
|
+
for (const memory of memories) {
|
|
3073
|
+
const memoryText = this.formatMemory(memory);
|
|
3074
|
+
const memoryTokens = this.estimateTokens(memoryText);
|
|
3075
|
+
if (currentTokens + memoryTokens > maxTokens) {
|
|
3076
|
+
break;
|
|
3077
|
+
}
|
|
3078
|
+
parts.push(memoryText);
|
|
3079
|
+
currentTokens += memoryTokens;
|
|
3080
|
+
}
|
|
3081
|
+
if (parts.length === 0) {
|
|
3082
|
+
return "";
|
|
3083
|
+
}
|
|
3084
|
+
return `## Relevant Memories
|
|
3085
|
+
|
|
3086
|
+
${parts.join("\n\n---\n\n")}`;
|
|
3087
|
+
}
|
|
3088
|
+
/**
|
|
3089
|
+
* Format a single memory for context
|
|
3090
|
+
*/
|
|
3091
|
+
formatMemory(memory) {
|
|
3092
|
+
const { event, score, sessionContext } = memory;
|
|
3093
|
+
const date = event.timestamp.toISOString().split("T")[0];
|
|
3094
|
+
let text = `**${event.eventType}** (${date}, score: ${score.toFixed(2)})
|
|
3095
|
+
${event.content}`;
|
|
3096
|
+
if (sessionContext) {
|
|
3097
|
+
text += `
|
|
3098
|
+
|
|
3099
|
+
_Context:_ ${sessionContext}`;
|
|
3100
|
+
}
|
|
3101
|
+
return text;
|
|
3102
|
+
}
|
|
3103
|
+
/**
|
|
3104
|
+
* Estimate token count (rough approximation)
|
|
3105
|
+
*/
|
|
3106
|
+
estimateTokens(text) {
|
|
3107
|
+
return Math.ceil(text.length / 4);
|
|
3108
|
+
}
|
|
3109
|
+
/**
|
|
3110
|
+
* Get event age in days (for recency scoring)
|
|
3111
|
+
*/
|
|
3112
|
+
getEventAgeDays(eventId) {
|
|
3113
|
+
return 0;
|
|
3114
|
+
}
|
|
3115
|
+
};
|
|
3116
|
+
function createRetriever(eventStore, vectorStore, embedder, matcher) {
|
|
3117
|
+
return new Retriever(eventStore, vectorStore, embedder, matcher);
|
|
3118
|
+
}
|
|
3119
|
+
|
|
3120
|
+
// src/core/graduation.ts
|
|
3121
|
+
var DEFAULT_CRITERIA = {
|
|
3122
|
+
L0toL1: {
|
|
3123
|
+
minAccessCount: 1,
|
|
3124
|
+
minConfidence: 0.5,
|
|
3125
|
+
minCrossSessionRefs: 0,
|
|
3126
|
+
maxAgeDays: 30
|
|
3127
|
+
},
|
|
3128
|
+
L1toL2: {
|
|
3129
|
+
minAccessCount: 3,
|
|
3130
|
+
minConfidence: 0.7,
|
|
3131
|
+
minCrossSessionRefs: 1,
|
|
3132
|
+
maxAgeDays: 60
|
|
3133
|
+
},
|
|
3134
|
+
L2toL3: {
|
|
3135
|
+
minAccessCount: 5,
|
|
3136
|
+
minConfidence: 0.85,
|
|
3137
|
+
minCrossSessionRefs: 2,
|
|
3138
|
+
maxAgeDays: 90
|
|
3139
|
+
},
|
|
3140
|
+
L3toL4: {
|
|
3141
|
+
minAccessCount: 10,
|
|
3142
|
+
minConfidence: 0.92,
|
|
3143
|
+
minCrossSessionRefs: 3,
|
|
3144
|
+
maxAgeDays: 180
|
|
3145
|
+
}
|
|
3146
|
+
};
|
|
3147
|
+
var GraduationPipeline = class {
|
|
3148
|
+
eventStore;
|
|
3149
|
+
criteria;
|
|
3150
|
+
metrics = /* @__PURE__ */ new Map();
|
|
3151
|
+
constructor(eventStore, criteria = {}) {
|
|
3152
|
+
this.eventStore = eventStore;
|
|
3153
|
+
this.criteria = {
|
|
3154
|
+
L0toL1: { ...DEFAULT_CRITERIA.L0toL1, ...criteria.L0toL1 },
|
|
3155
|
+
L1toL2: { ...DEFAULT_CRITERIA.L1toL2, ...criteria.L1toL2 },
|
|
3156
|
+
L2toL3: { ...DEFAULT_CRITERIA.L2toL3, ...criteria.L2toL3 },
|
|
3157
|
+
L3toL4: { ...DEFAULT_CRITERIA.L3toL4, ...criteria.L3toL4 }
|
|
3158
|
+
};
|
|
3159
|
+
}
|
|
3160
|
+
/**
|
|
3161
|
+
* Record an access to an event (used for graduation scoring)
|
|
3162
|
+
*/
|
|
3163
|
+
recordAccess(eventId, fromSessionId, confidence = 1) {
|
|
3164
|
+
const existing = this.metrics.get(eventId);
|
|
3165
|
+
if (existing) {
|
|
3166
|
+
existing.accessCount++;
|
|
3167
|
+
existing.lastAccessed = /* @__PURE__ */ new Date();
|
|
3168
|
+
existing.confidence = Math.max(existing.confidence, confidence);
|
|
3169
|
+
} else {
|
|
3170
|
+
this.metrics.set(eventId, {
|
|
3171
|
+
eventId,
|
|
3172
|
+
accessCount: 1,
|
|
3173
|
+
lastAccessed: /* @__PURE__ */ new Date(),
|
|
3174
|
+
crossSessionRefs: 0,
|
|
3175
|
+
confidence
|
|
3176
|
+
});
|
|
3177
|
+
}
|
|
3178
|
+
}
|
|
3179
|
+
/**
|
|
3180
|
+
* Evaluate if an event should graduate to the next level
|
|
3181
|
+
*/
|
|
3182
|
+
async evaluateGraduation(eventId, currentLevel) {
|
|
3183
|
+
const metrics = this.metrics.get(eventId);
|
|
3184
|
+
if (!metrics) {
|
|
3185
|
+
return {
|
|
3186
|
+
eventId,
|
|
3187
|
+
fromLevel: currentLevel,
|
|
3188
|
+
toLevel: currentLevel,
|
|
3189
|
+
success: false,
|
|
3190
|
+
reason: "No metrics available for event"
|
|
3191
|
+
};
|
|
3192
|
+
}
|
|
3193
|
+
const nextLevel = this.getNextLevel(currentLevel);
|
|
3194
|
+
if (!nextLevel) {
|
|
3195
|
+
return {
|
|
3196
|
+
eventId,
|
|
3197
|
+
fromLevel: currentLevel,
|
|
3198
|
+
toLevel: currentLevel,
|
|
3199
|
+
success: false,
|
|
3200
|
+
reason: "Already at maximum level"
|
|
3201
|
+
};
|
|
3202
|
+
}
|
|
3203
|
+
const criteria = this.getCriteria(currentLevel, nextLevel);
|
|
3204
|
+
const evaluation = this.checkCriteria(metrics, criteria);
|
|
3205
|
+
if (evaluation.passed) {
|
|
3206
|
+
await this.eventStore.updateMemoryLevel(eventId, nextLevel);
|
|
3207
|
+
return {
|
|
3208
|
+
eventId,
|
|
3209
|
+
fromLevel: currentLevel,
|
|
3210
|
+
toLevel: nextLevel,
|
|
3211
|
+
success: true
|
|
3212
|
+
};
|
|
3213
|
+
}
|
|
3214
|
+
return {
|
|
3215
|
+
eventId,
|
|
3216
|
+
fromLevel: currentLevel,
|
|
3217
|
+
toLevel: currentLevel,
|
|
3218
|
+
success: false,
|
|
3219
|
+
reason: evaluation.reason
|
|
3220
|
+
};
|
|
3221
|
+
}
|
|
3222
|
+
/**
|
|
3223
|
+
* Run graduation evaluation for all events at a given level
|
|
3224
|
+
*/
|
|
3225
|
+
async graduateBatch(level) {
|
|
3226
|
+
const results = [];
|
|
3227
|
+
for (const [eventId, metrics] of this.metrics) {
|
|
3228
|
+
const result = await this.evaluateGraduation(eventId, level);
|
|
3229
|
+
results.push(result);
|
|
3230
|
+
}
|
|
3231
|
+
return results;
|
|
3232
|
+
}
|
|
3233
|
+
/**
|
|
3234
|
+
* Extract insights from graduated events (L1+)
|
|
3235
|
+
*/
|
|
3236
|
+
extractInsights(events) {
|
|
3237
|
+
const insights = [];
|
|
3238
|
+
const patterns = this.detectPatterns(events);
|
|
3239
|
+
for (const pattern of patterns) {
|
|
3240
|
+
insights.push({
|
|
3241
|
+
id: crypto.randomUUID(),
|
|
3242
|
+
insightType: "pattern",
|
|
3243
|
+
content: pattern.description,
|
|
3244
|
+
canonicalKey: pattern.key,
|
|
3245
|
+
confidence: pattern.confidence,
|
|
3246
|
+
sourceEvents: pattern.eventIds,
|
|
3247
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
3248
|
+
lastUpdated: /* @__PURE__ */ new Date()
|
|
3249
|
+
});
|
|
3250
|
+
}
|
|
3251
|
+
const preferences = this.detectPreferences(events);
|
|
3252
|
+
for (const pref of preferences) {
|
|
3253
|
+
insights.push({
|
|
3254
|
+
id: crypto.randomUUID(),
|
|
3255
|
+
insightType: "preference",
|
|
3256
|
+
content: pref.description,
|
|
3257
|
+
canonicalKey: pref.key,
|
|
3258
|
+
confidence: pref.confidence,
|
|
3259
|
+
sourceEvents: pref.eventIds,
|
|
3260
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
3261
|
+
lastUpdated: /* @__PURE__ */ new Date()
|
|
3262
|
+
});
|
|
3263
|
+
}
|
|
3264
|
+
return insights;
|
|
3265
|
+
}
|
|
3266
|
+
/**
|
|
3267
|
+
* Get the next level in the graduation pipeline
|
|
3268
|
+
*/
|
|
3269
|
+
getNextLevel(current) {
|
|
3270
|
+
const levels = ["L0", "L1", "L2", "L3", "L4"];
|
|
3271
|
+
const currentIndex = levels.indexOf(current);
|
|
3272
|
+
if (currentIndex === -1 || currentIndex >= levels.length - 1) {
|
|
3273
|
+
return null;
|
|
3274
|
+
}
|
|
3275
|
+
return levels[currentIndex + 1];
|
|
3276
|
+
}
|
|
3277
|
+
/**
|
|
3278
|
+
* Get criteria for level transition
|
|
3279
|
+
*/
|
|
3280
|
+
getCriteria(from, to) {
|
|
3281
|
+
const key = `${from}to${to}`;
|
|
3282
|
+
return this.criteria[key] || DEFAULT_CRITERIA.L0toL1;
|
|
3283
|
+
}
|
|
3284
|
+
/**
|
|
3285
|
+
* Check if metrics meet criteria
|
|
3286
|
+
*/
|
|
3287
|
+
checkCriteria(metrics, criteria) {
|
|
3288
|
+
if (metrics.accessCount < criteria.minAccessCount) {
|
|
3289
|
+
return {
|
|
3290
|
+
passed: false,
|
|
3291
|
+
reason: `Access count ${metrics.accessCount} < ${criteria.minAccessCount}`
|
|
3292
|
+
};
|
|
3293
|
+
}
|
|
3294
|
+
if (metrics.confidence < criteria.minConfidence) {
|
|
3295
|
+
return {
|
|
3296
|
+
passed: false,
|
|
3297
|
+
reason: `Confidence ${metrics.confidence} < ${criteria.minConfidence}`
|
|
3298
|
+
};
|
|
3299
|
+
}
|
|
3300
|
+
if (metrics.crossSessionRefs < criteria.minCrossSessionRefs) {
|
|
3301
|
+
return {
|
|
3302
|
+
passed: false,
|
|
3303
|
+
reason: `Cross-session refs ${metrics.crossSessionRefs} < ${criteria.minCrossSessionRefs}`
|
|
3304
|
+
};
|
|
3305
|
+
}
|
|
3306
|
+
const ageDays = (Date.now() - metrics.lastAccessed.getTime()) / (1e3 * 60 * 60 * 24);
|
|
3307
|
+
if (ageDays > criteria.maxAgeDays) {
|
|
3308
|
+
return {
|
|
3309
|
+
passed: false,
|
|
3310
|
+
reason: `Event too old: ${ageDays.toFixed(1)} days > ${criteria.maxAgeDays}`
|
|
3311
|
+
};
|
|
3312
|
+
}
|
|
3313
|
+
return { passed: true };
|
|
3314
|
+
}
|
|
3315
|
+
/**
|
|
3316
|
+
* Detect patterns in events
|
|
3317
|
+
*/
|
|
3318
|
+
detectPatterns(events) {
|
|
3319
|
+
const keyGroups = /* @__PURE__ */ new Map();
|
|
3320
|
+
for (const event of events) {
|
|
3321
|
+
const existing = keyGroups.get(event.canonicalKey) || [];
|
|
3322
|
+
existing.push(event);
|
|
3323
|
+
keyGroups.set(event.canonicalKey, existing);
|
|
3324
|
+
}
|
|
3325
|
+
const patterns = [];
|
|
3326
|
+
for (const [key, groupEvents] of keyGroups) {
|
|
3327
|
+
if (groupEvents.length >= 2) {
|
|
3328
|
+
patterns.push({
|
|
3329
|
+
key,
|
|
3330
|
+
description: `Repeated topic: ${key.slice(0, 50)}`,
|
|
3331
|
+
confidence: Math.min(1, groupEvents.length / 5),
|
|
3332
|
+
eventIds: groupEvents.map((e) => e.id)
|
|
3333
|
+
});
|
|
3334
|
+
}
|
|
3335
|
+
}
|
|
3336
|
+
return patterns;
|
|
3337
|
+
}
|
|
3338
|
+
/**
|
|
3339
|
+
* Detect user preferences from events
|
|
3340
|
+
*/
|
|
3341
|
+
detectPreferences(events) {
|
|
3342
|
+
const preferenceKeywords = ["prefer", "like", "want", "always", "never", "favorite"];
|
|
3343
|
+
const preferences = [];
|
|
3344
|
+
for (const event of events) {
|
|
3345
|
+
if (event.eventType !== "user_prompt")
|
|
3346
|
+
continue;
|
|
3347
|
+
const lowerContent = event.content.toLowerCase();
|
|
3348
|
+
for (const keyword of preferenceKeywords) {
|
|
3349
|
+
if (lowerContent.includes(keyword)) {
|
|
3350
|
+
preferences.push({
|
|
3351
|
+
key: `preference_${keyword}_${event.id.slice(0, 8)}`,
|
|
3352
|
+
description: `User preference: ${event.content.slice(0, 100)}`,
|
|
3353
|
+
confidence: 0.7,
|
|
3354
|
+
eventIds: [event.id]
|
|
3355
|
+
});
|
|
3356
|
+
break;
|
|
3357
|
+
}
|
|
3358
|
+
}
|
|
3359
|
+
}
|
|
3360
|
+
return preferences;
|
|
3361
|
+
}
|
|
3362
|
+
/**
|
|
3363
|
+
* Get graduation statistics
|
|
3364
|
+
*/
|
|
3365
|
+
async getStats() {
|
|
3366
|
+
return this.eventStore.getLevelStats();
|
|
3367
|
+
}
|
|
3368
|
+
};
|
|
3369
|
+
function createGraduationPipeline(eventStore) {
|
|
3370
|
+
return new GraduationPipeline(eventStore);
|
|
3371
|
+
}
|
|
3372
|
+
|
|
3373
|
+
// src/core/task/task-matcher.ts
|
|
3374
|
+
var DEFAULT_CONFIG4 = {
|
|
3375
|
+
minCombinedScore: MATCH_THRESHOLDS.minCombinedScore,
|
|
3376
|
+
minGap: MATCH_THRESHOLDS.minGap,
|
|
3377
|
+
suggestionThreshold: MATCH_THRESHOLDS.suggestionThreshold,
|
|
3378
|
+
maxCandidates: 5
|
|
3379
|
+
};
|
|
3380
|
+
var TaskMatcher = class {
|
|
3381
|
+
constructor(db, config) {
|
|
3382
|
+
this.db = db;
|
|
3383
|
+
this.config = { ...DEFAULT_CONFIG4, ...config };
|
|
3384
|
+
}
|
|
3385
|
+
config;
|
|
3386
|
+
/**
|
|
3387
|
+
* Find task by exact canonical key match
|
|
3388
|
+
*/
|
|
3389
|
+
async findExact(title, project) {
|
|
3390
|
+
const canonicalKey = makeEntityCanonicalKey("task", title, { project });
|
|
3391
|
+
const rows = await this.db.all(
|
|
3392
|
+
`SELECT * FROM entities
|
|
3393
|
+
WHERE entity_type = 'task' AND canonical_key = ?
|
|
3394
|
+
AND status = 'active'`,
|
|
3395
|
+
[canonicalKey]
|
|
3396
|
+
);
|
|
3397
|
+
if (rows.length === 0)
|
|
3398
|
+
return null;
|
|
3399
|
+
return this.rowToEntity(rows[0]);
|
|
3400
|
+
}
|
|
3401
|
+
/**
|
|
3402
|
+
* Find task by alias
|
|
3403
|
+
*/
|
|
3404
|
+
async findByAlias(title, project) {
|
|
3405
|
+
const canonicalKey = makeEntityCanonicalKey("task", title, { project });
|
|
3406
|
+
const rows = await this.db.all(
|
|
3407
|
+
`SELECT e.* FROM entities e
|
|
3408
|
+
JOIN entity_aliases a ON e.entity_id = a.entity_id
|
|
3409
|
+
WHERE a.entity_type = 'task' AND a.canonical_key = ?
|
|
3410
|
+
AND e.status = 'active'`,
|
|
3411
|
+
[canonicalKey]
|
|
3412
|
+
);
|
|
3413
|
+
if (rows.length === 0)
|
|
3414
|
+
return null;
|
|
3415
|
+
return this.rowToEntity(rows[0]);
|
|
3416
|
+
}
|
|
3417
|
+
/**
|
|
3418
|
+
* Search tasks by text (FTS-like)
|
|
3419
|
+
*/
|
|
3420
|
+
async searchByText(query, project) {
|
|
3421
|
+
const searchPattern = `%${query.toLowerCase()}%`;
|
|
3422
|
+
let sql = `
|
|
3423
|
+
SELECT *,
|
|
3424
|
+
CASE
|
|
3425
|
+
WHEN title_norm = ? THEN 1.0
|
|
3426
|
+
WHEN title_norm LIKE ? THEN 0.9
|
|
3427
|
+
ELSE 0.7
|
|
3428
|
+
END as match_score
|
|
3429
|
+
FROM entities
|
|
3430
|
+
WHERE entity_type = 'task'
|
|
3431
|
+
AND status = 'active'
|
|
3432
|
+
AND (title_norm LIKE ? OR search_text LIKE ?)
|
|
3433
|
+
`;
|
|
3434
|
+
const normalizedQuery = query.toLowerCase().trim();
|
|
3435
|
+
const params = [normalizedQuery, `%${normalizedQuery}%`, searchPattern, searchPattern];
|
|
3436
|
+
if (project) {
|
|
3437
|
+
sql += ` AND json_extract(current_json, '$.project') = ?`;
|
|
3438
|
+
params.push(project);
|
|
3439
|
+
}
|
|
3440
|
+
sql += ` ORDER BY match_score DESC, updated_at DESC LIMIT ?`;
|
|
3441
|
+
params.push(this.config.maxCandidates);
|
|
3442
|
+
const rows = await this.db.all(sql, params);
|
|
3443
|
+
return rows.map((row) => ({
|
|
3444
|
+
entity: this.rowToEntity(row),
|
|
3445
|
+
score: row.match_score
|
|
3446
|
+
}));
|
|
3447
|
+
}
|
|
3448
|
+
/**
|
|
3449
|
+
* Match task with confidence classification
|
|
3450
|
+
* Returns high confidence only if score ≥ 0.92 AND gap ≥ 0.03
|
|
3451
|
+
*/
|
|
3452
|
+
async match(title, project) {
|
|
3453
|
+
const exactMatch = await this.findExact(title, project);
|
|
3454
|
+
if (exactMatch) {
|
|
3455
|
+
return {
|
|
3456
|
+
match: exactMatch,
|
|
3457
|
+
confidence: "high",
|
|
3458
|
+
score: 1
|
|
3459
|
+
};
|
|
3460
|
+
}
|
|
3461
|
+
const aliasMatch = await this.findByAlias(title, project);
|
|
3462
|
+
if (aliasMatch) {
|
|
3463
|
+
return {
|
|
3464
|
+
match: aliasMatch,
|
|
3465
|
+
confidence: "high",
|
|
3466
|
+
score: 0.98
|
|
3467
|
+
};
|
|
3468
|
+
}
|
|
3469
|
+
const searchResults = await this.searchByText(title, project);
|
|
3470
|
+
if (searchResults.length === 0) {
|
|
3471
|
+
return {
|
|
3472
|
+
match: null,
|
|
3473
|
+
confidence: "none",
|
|
3474
|
+
score: 0
|
|
3475
|
+
};
|
|
3476
|
+
}
|
|
3477
|
+
const topResult = searchResults[0];
|
|
3478
|
+
const secondScore = searchResults.length > 1 ? searchResults[1].score : null;
|
|
3479
|
+
const gap = secondScore !== null ? topResult.score - secondScore : Infinity;
|
|
3480
|
+
const confidence = this.classifyConfidence(topResult.score, gap);
|
|
3481
|
+
if (confidence === "high") {
|
|
3482
|
+
return {
|
|
3483
|
+
match: topResult.entity,
|
|
3484
|
+
confidence: "high",
|
|
3485
|
+
score: topResult.score,
|
|
3486
|
+
gap
|
|
3487
|
+
};
|
|
3488
|
+
}
|
|
3489
|
+
if (confidence === "suggested") {
|
|
3490
|
+
return {
|
|
3491
|
+
match: null,
|
|
3492
|
+
confidence: "suggested",
|
|
3493
|
+
score: topResult.score,
|
|
3494
|
+
gap,
|
|
3495
|
+
candidates: searchResults.slice(0, this.config.maxCandidates).map((r) => r.entity)
|
|
3496
|
+
};
|
|
3497
|
+
}
|
|
3498
|
+
return {
|
|
3499
|
+
match: null,
|
|
3500
|
+
confidence: "none",
|
|
3501
|
+
score: topResult.score
|
|
3502
|
+
};
|
|
3503
|
+
}
|
|
3504
|
+
/**
|
|
3505
|
+
* Classify confidence based on AXIOMMIND thresholds
|
|
3506
|
+
*/
|
|
3507
|
+
classifyConfidence(score, gap) {
|
|
3508
|
+
const { minCombinedScore, minGap, suggestionThreshold } = this.config;
|
|
3509
|
+
if (score >= minCombinedScore && gap >= minGap) {
|
|
3510
|
+
return "high";
|
|
3511
|
+
}
|
|
3512
|
+
if (score >= suggestionThreshold) {
|
|
3513
|
+
return "suggested";
|
|
3514
|
+
}
|
|
3515
|
+
return "none";
|
|
3516
|
+
}
|
|
3517
|
+
/**
|
|
3518
|
+
* Get suggestion candidates (for condition fallback)
|
|
3519
|
+
*/
|
|
3520
|
+
async getSuggestionCandidates(title, project) {
|
|
3521
|
+
const searchResults = await this.searchByText(title, project);
|
|
3522
|
+
return searchResults.filter((r) => r.score >= this.config.suggestionThreshold).slice(0, this.config.maxCandidates).map((r) => r.entity);
|
|
3523
|
+
}
|
|
3524
|
+
/**
|
|
3525
|
+
* Convert database row to Entity
|
|
3526
|
+
*/
|
|
3527
|
+
rowToEntity(row) {
|
|
3528
|
+
return {
|
|
3529
|
+
entityId: row.entity_id,
|
|
3530
|
+
entityType: row.entity_type,
|
|
3531
|
+
canonicalKey: row.canonical_key,
|
|
3532
|
+
title: row.title,
|
|
3533
|
+
stage: row.stage,
|
|
3534
|
+
status: row.status,
|
|
3535
|
+
currentJson: typeof row.current_json === "string" ? JSON.parse(row.current_json) : row.current_json,
|
|
3536
|
+
titleNorm: row.title_norm,
|
|
3537
|
+
searchText: row.search_text,
|
|
3538
|
+
createdAt: new Date(row.created_at),
|
|
3539
|
+
updatedAt: new Date(row.updated_at)
|
|
3540
|
+
};
|
|
3541
|
+
}
|
|
3542
|
+
};
|
|
3543
|
+
|
|
3544
|
+
// src/core/task/blocker-resolver.ts
|
|
3545
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
3546
|
+
var URL_PATTERN = /^https?:\/\/.+/;
|
|
3547
|
+
var JIRA_PATTERN = /^[A-Z]+-\d+$/;
|
|
3548
|
+
var GITHUB_ISSUE_PATTERN = /^[^\/]+\/[^#]+#\d+$/;
|
|
3549
|
+
var TASK_ID_PATTERN = /^task:[^:]+:[^:]+$/;
|
|
3550
|
+
var BlockerResolver = class {
|
|
3551
|
+
constructor(db, config = {}) {
|
|
3552
|
+
this.db = db;
|
|
3553
|
+
this.config = config;
|
|
3554
|
+
this.taskMatcher = new TaskMatcher(db);
|
|
3555
|
+
}
|
|
3556
|
+
taskMatcher;
|
|
3557
|
+
/**
|
|
3558
|
+
* Resolve a single blocker text to entity reference
|
|
3559
|
+
* Rules:
|
|
3560
|
+
* 1. Strong ID/URL/key pattern → artifact
|
|
3561
|
+
* 2. Explicit task_id → task
|
|
3562
|
+
* 3. Task title match (strict only) → task
|
|
3563
|
+
* 4. Fallback → condition (no stub task creation)
|
|
3564
|
+
*/
|
|
3565
|
+
async resolveBlocker(text, sourceEntryId) {
|
|
3566
|
+
const trimmedText = text.trim();
|
|
3567
|
+
const artifactRef = await this.tryResolveAsArtifact(trimmedText);
|
|
3568
|
+
if (artifactRef) {
|
|
3569
|
+
return artifactRef;
|
|
3570
|
+
}
|
|
3571
|
+
if (TASK_ID_PATTERN.test(trimmedText)) {
|
|
3572
|
+
const taskRef = await this.tryResolveAsTaskId(trimmedText);
|
|
3573
|
+
if (taskRef) {
|
|
3574
|
+
return taskRef;
|
|
3575
|
+
}
|
|
3576
|
+
}
|
|
3577
|
+
const taskMatch = await this.taskMatcher.match(trimmedText, this.config.project);
|
|
3578
|
+
if (taskMatch.confidence === "high" && taskMatch.match) {
|
|
3579
|
+
return {
|
|
3580
|
+
kind: "task",
|
|
3581
|
+
entityId: taskMatch.match.entityId,
|
|
3582
|
+
rawText: trimmedText,
|
|
3583
|
+
confidence: taskMatch.score
|
|
3584
|
+
};
|
|
3585
|
+
}
|
|
3586
|
+
const conditionRef = await this.createConditionBlocker(
|
|
3587
|
+
trimmedText,
|
|
3588
|
+
taskMatch.candidates
|
|
3589
|
+
);
|
|
3590
|
+
return conditionRef;
|
|
3591
|
+
}
|
|
3592
|
+
/**
|
|
3593
|
+
* Resolve multiple blocker texts
|
|
3594
|
+
*/
|
|
3595
|
+
async resolveBlockers(texts, sourceEntryId) {
|
|
3596
|
+
const results = [];
|
|
3597
|
+
for (const text of texts) {
|
|
3598
|
+
const ref = await this.resolveBlocker(text, sourceEntryId);
|
|
3599
|
+
results.push(ref);
|
|
3600
|
+
}
|
|
3601
|
+
return results;
|
|
3602
|
+
}
|
|
3603
|
+
/**
|
|
3604
|
+
* Try to resolve as artifact (URL, JIRA, GitHub)
|
|
3605
|
+
*/
|
|
3606
|
+
async tryResolveAsArtifact(text) {
|
|
3607
|
+
if (!URL_PATTERN.test(text) && !JIRA_PATTERN.test(text) && !GITHUB_ISSUE_PATTERN.test(text)) {
|
|
3608
|
+
return null;
|
|
3609
|
+
}
|
|
3610
|
+
const canonicalKey = makeArtifactKey(text);
|
|
3611
|
+
const existing = await this.db.all(
|
|
3612
|
+
`SELECT entity_id FROM entities
|
|
3613
|
+
WHERE entity_type = 'artifact' AND canonical_key = ?`,
|
|
3614
|
+
[canonicalKey]
|
|
3615
|
+
);
|
|
3616
|
+
let entityId;
|
|
3617
|
+
if (existing.length > 0) {
|
|
3618
|
+
entityId = existing[0].entity_id;
|
|
3619
|
+
} else {
|
|
3620
|
+
entityId = await this.declareArtifact(text, canonicalKey);
|
|
3621
|
+
}
|
|
3622
|
+
return {
|
|
3623
|
+
kind: "artifact",
|
|
3624
|
+
entityId,
|
|
3625
|
+
rawText: text,
|
|
3626
|
+
confidence: 1
|
|
3627
|
+
};
|
|
3628
|
+
}
|
|
3629
|
+
/**
|
|
3630
|
+
* Try to resolve as explicit task ID
|
|
3631
|
+
*/
|
|
3632
|
+
async tryResolveAsTaskId(taskId) {
|
|
3633
|
+
const rows = await this.db.all(
|
|
3634
|
+
`SELECT entity_id FROM entities
|
|
3635
|
+
WHERE entity_type = 'task' AND canonical_key = ?
|
|
3636
|
+
AND status = 'active'`,
|
|
3637
|
+
[taskId]
|
|
3638
|
+
);
|
|
3639
|
+
if (rows.length === 0) {
|
|
3640
|
+
return null;
|
|
3641
|
+
}
|
|
3642
|
+
return {
|
|
3643
|
+
kind: "task",
|
|
3644
|
+
entityId: rows[0].entity_id,
|
|
3645
|
+
rawText: taskId,
|
|
3646
|
+
confidence: 1
|
|
3647
|
+
};
|
|
3648
|
+
}
|
|
3649
|
+
/**
|
|
3650
|
+
* Create condition blocker (get-or-create)
|
|
3651
|
+
*/
|
|
3652
|
+
async createConditionBlocker(text, candidates) {
|
|
3653
|
+
const canonicalKey = makeEntityCanonicalKey("condition", text, {
|
|
3654
|
+
project: this.config.project
|
|
3655
|
+
});
|
|
3656
|
+
const existing = await this.db.all(
|
|
3657
|
+
`SELECT entity_id FROM entities
|
|
3658
|
+
WHERE entity_type = 'condition' AND canonical_key = ?`,
|
|
3659
|
+
[canonicalKey]
|
|
3660
|
+
);
|
|
3661
|
+
let entityId;
|
|
3662
|
+
if (existing.length > 0) {
|
|
3663
|
+
entityId = existing[0].entity_id;
|
|
3664
|
+
} else {
|
|
3665
|
+
entityId = await this.declareCondition(text, canonicalKey, candidates);
|
|
3666
|
+
}
|
|
3667
|
+
return {
|
|
3668
|
+
kind: "condition",
|
|
3669
|
+
entityId,
|
|
3670
|
+
rawText: text,
|
|
3671
|
+
confidence: 0.5,
|
|
3672
|
+
candidates: candidates?.map((c) => c.entityId)
|
|
3673
|
+
};
|
|
3674
|
+
}
|
|
3675
|
+
/**
|
|
3676
|
+
* Declare a new condition entity
|
|
3677
|
+
*/
|
|
3678
|
+
async declareCondition(text, canonicalKey, candidates) {
|
|
3679
|
+
const entityId = randomUUID5();
|
|
3680
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3681
|
+
const currentJson = {
|
|
3682
|
+
text,
|
|
3683
|
+
resolved: false,
|
|
3684
|
+
candidates: candidates?.map((c) => ({
|
|
3685
|
+
entityId: c.entityId,
|
|
3686
|
+
title: c.title
|
|
3687
|
+
}))
|
|
3688
|
+
};
|
|
3689
|
+
await this.db.run(
|
|
3690
|
+
`INSERT INTO entities (
|
|
3691
|
+
entity_id, entity_type, canonical_key, title, stage, status,
|
|
3692
|
+
current_json, title_norm, search_text, created_at, updated_at
|
|
3693
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
3694
|
+
[
|
|
3695
|
+
entityId,
|
|
3696
|
+
"condition",
|
|
3697
|
+
canonicalKey,
|
|
3698
|
+
text,
|
|
3699
|
+
"raw",
|
|
3700
|
+
"active",
|
|
3701
|
+
JSON.stringify(currentJson),
|
|
3702
|
+
text.toLowerCase().trim(),
|
|
3703
|
+
text,
|
|
3704
|
+
now,
|
|
3705
|
+
now
|
|
3706
|
+
]
|
|
3707
|
+
);
|
|
3708
|
+
await this.db.run(
|
|
3709
|
+
`INSERT INTO entity_aliases (entity_type, canonical_key, entity_id, is_primary)
|
|
3710
|
+
VALUES (?, ?, ?, TRUE)
|
|
3711
|
+
ON CONFLICT (entity_type, canonical_key) DO NOTHING`,
|
|
3712
|
+
["condition", canonicalKey, entityId]
|
|
3713
|
+
);
|
|
3714
|
+
return entityId;
|
|
3715
|
+
}
|
|
3716
|
+
/**
|
|
3717
|
+
* Declare a new artifact entity
|
|
3718
|
+
*/
|
|
3719
|
+
async declareArtifact(identifier, canonicalKey) {
|
|
3720
|
+
const entityId = randomUUID5();
|
|
3721
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3722
|
+
let artifactType = "generic";
|
|
3723
|
+
if (URL_PATTERN.test(identifier)) {
|
|
3724
|
+
artifactType = "url";
|
|
3725
|
+
} else if (JIRA_PATTERN.test(identifier)) {
|
|
3726
|
+
artifactType = "jira";
|
|
3727
|
+
} else if (GITHUB_ISSUE_PATTERN.test(identifier)) {
|
|
3728
|
+
artifactType = "github_issue";
|
|
3729
|
+
}
|
|
3730
|
+
const currentJson = {
|
|
3731
|
+
identifier,
|
|
3732
|
+
artifactType
|
|
3733
|
+
};
|
|
3734
|
+
await this.db.run(
|
|
3735
|
+
`INSERT INTO entities (
|
|
3736
|
+
entity_id, entity_type, canonical_key, title, stage, status,
|
|
3737
|
+
current_json, title_norm, search_text, created_at, updated_at
|
|
3738
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
3739
|
+
[
|
|
3740
|
+
entityId,
|
|
3741
|
+
"artifact",
|
|
3742
|
+
canonicalKey,
|
|
3743
|
+
identifier,
|
|
3744
|
+
"raw",
|
|
3745
|
+
"active",
|
|
3746
|
+
JSON.stringify(currentJson),
|
|
3747
|
+
identifier.toLowerCase(),
|
|
3748
|
+
identifier,
|
|
3749
|
+
now,
|
|
3750
|
+
now
|
|
3751
|
+
]
|
|
3752
|
+
);
|
|
3753
|
+
await this.db.run(
|
|
3754
|
+
`INSERT INTO entity_aliases (entity_type, canonical_key, entity_id, is_primary)
|
|
3755
|
+
VALUES (?, ?, ?, TRUE)
|
|
3756
|
+
ON CONFLICT (entity_type, canonical_key) DO NOTHING`,
|
|
3757
|
+
["artifact", canonicalKey, entityId]
|
|
3758
|
+
);
|
|
3759
|
+
return entityId;
|
|
3760
|
+
}
|
|
3761
|
+
/**
|
|
3762
|
+
* Create unknown placeholder condition
|
|
3763
|
+
* Used when task is blocked but no blocker text provided
|
|
3764
|
+
*/
|
|
3765
|
+
async createUnknownPlaceholder(taskTitle) {
|
|
3766
|
+
const text = `Unknown blocker for: ${taskTitle}`;
|
|
3767
|
+
const ref = await this.createConditionBlocker(text);
|
|
3768
|
+
await this.db.run(
|
|
3769
|
+
`UPDATE entities
|
|
3770
|
+
SET current_json = json_set(current_json, '$.auto_placeholder', true)
|
|
3771
|
+
WHERE entity_id = ?`,
|
|
3772
|
+
[ref.entityId]
|
|
3773
|
+
);
|
|
3774
|
+
return {
|
|
3775
|
+
...ref,
|
|
3776
|
+
confidence: 0
|
|
3777
|
+
};
|
|
3778
|
+
}
|
|
3779
|
+
};
|
|
3780
|
+
|
|
3781
|
+
// src/core/task/task-resolver.ts
|
|
3782
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
3783
|
+
var VALID_TRANSITIONS = {
|
|
3784
|
+
pending: ["in_progress", "cancelled"],
|
|
3785
|
+
in_progress: ["blocked", "done", "cancelled"],
|
|
3786
|
+
blocked: ["in_progress", "done", "cancelled"],
|
|
3787
|
+
done: [],
|
|
3788
|
+
// Terminal state
|
|
3789
|
+
cancelled: []
|
|
3790
|
+
// Terminal state
|
|
3791
|
+
};
|
|
3792
|
+
var TaskResolver = class {
|
|
3793
|
+
constructor(db, config) {
|
|
3794
|
+
this.db = db;
|
|
3795
|
+
this.config = config;
|
|
3796
|
+
this.taskMatcher = new TaskMatcher(db);
|
|
3797
|
+
this.blockerResolver = new BlockerResolver(db, { project: config.project });
|
|
3798
|
+
}
|
|
3799
|
+
taskMatcher;
|
|
3800
|
+
blockerResolver;
|
|
3801
|
+
/**
|
|
3802
|
+
* Process extracted task entry
|
|
3803
|
+
* 1. Find or create task entity
|
|
3804
|
+
* 2. Emit status/priority change events if needed
|
|
3805
|
+
* 3. Process blockers
|
|
3806
|
+
*/
|
|
3807
|
+
async processTask(extracted, sourceEntryId) {
|
|
3808
|
+
const events = [];
|
|
3809
|
+
const { task, isNew, eventId: createEventId } = await this.findOrCreateTask(extracted);
|
|
3810
|
+
if (isNew && createEventId) {
|
|
3811
|
+
events.push(createEventId);
|
|
3812
|
+
}
|
|
3813
|
+
if (extracted.status) {
|
|
3814
|
+
const statusEvent = await this.handleStatusChange(task, extracted.status);
|
|
3815
|
+
if (statusEvent) {
|
|
3816
|
+
events.push(statusEvent);
|
|
3817
|
+
}
|
|
3818
|
+
}
|
|
3819
|
+
if (extracted.priority) {
|
|
3820
|
+
const priorityEvent = await this.handlePriorityChange(task, extracted.priority);
|
|
3821
|
+
if (priorityEvent) {
|
|
3822
|
+
events.push(priorityEvent);
|
|
3823
|
+
}
|
|
3824
|
+
}
|
|
3825
|
+
if (extracted.blockedBy && extracted.blockedBy.length > 0) {
|
|
3826
|
+
const blockerEvent = await this.handleBlockers(
|
|
3827
|
+
task,
|
|
3828
|
+
extracted.blockedBy,
|
|
3829
|
+
sourceEntryId
|
|
3830
|
+
);
|
|
3831
|
+
if (blockerEvent) {
|
|
3832
|
+
events.push(blockerEvent);
|
|
3833
|
+
}
|
|
3834
|
+
} else if (extracted.status === "blocked") {
|
|
3835
|
+
const blockerEvent = await this.handleUnknownBlocker(task);
|
|
3836
|
+
if (blockerEvent) {
|
|
3837
|
+
events.push(blockerEvent);
|
|
3838
|
+
}
|
|
3839
|
+
}
|
|
3840
|
+
return {
|
|
3841
|
+
taskId: task.entityId,
|
|
3842
|
+
isNew,
|
|
3843
|
+
events
|
|
3844
|
+
};
|
|
3845
|
+
}
|
|
3846
|
+
/**
|
|
3847
|
+
* Find existing task or create new one
|
|
3848
|
+
*/
|
|
3849
|
+
async findOrCreateTask(extracted) {
|
|
3850
|
+
const matchResult = await this.taskMatcher.match(extracted.title, extracted.project);
|
|
3851
|
+
if (matchResult.confidence === "high" && matchResult.match) {
|
|
3852
|
+
return {
|
|
3853
|
+
task: matchResult.match,
|
|
3854
|
+
isNew: false
|
|
3855
|
+
};
|
|
3856
|
+
}
|
|
3857
|
+
const taskId = randomUUID6();
|
|
3858
|
+
const canonicalKey = makeEntityCanonicalKey("task", extracted.title, {
|
|
3859
|
+
project: extracted.project
|
|
3860
|
+
});
|
|
3861
|
+
let initialStatus = extracted.status ?? "pending";
|
|
3862
|
+
if (initialStatus === "done") {
|
|
3863
|
+
initialStatus = "in_progress";
|
|
3864
|
+
}
|
|
3865
|
+
const now = /* @__PURE__ */ new Date();
|
|
3866
|
+
const currentJson = {
|
|
3867
|
+
status: initialStatus,
|
|
3868
|
+
priority: extracted.priority ?? "medium",
|
|
3869
|
+
description: extracted.description,
|
|
3870
|
+
project: extracted.project ?? this.config.project
|
|
3871
|
+
};
|
|
3872
|
+
await this.db.run(
|
|
3873
|
+
`INSERT INTO entities (
|
|
3874
|
+
entity_id, entity_type, canonical_key, title, stage, status,
|
|
3875
|
+
current_json, title_norm, search_text, created_at, updated_at
|
|
3876
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
3877
|
+
[
|
|
3878
|
+
taskId,
|
|
3879
|
+
"task",
|
|
3880
|
+
canonicalKey,
|
|
3881
|
+
extracted.title,
|
|
3882
|
+
"raw",
|
|
3883
|
+
"active",
|
|
3884
|
+
JSON.stringify(currentJson),
|
|
3885
|
+
extracted.title.toLowerCase().trim(),
|
|
3886
|
+
`${extracted.title} ${extracted.description ?? ""}`,
|
|
3887
|
+
now.toISOString(),
|
|
3888
|
+
now.toISOString()
|
|
3889
|
+
]
|
|
3890
|
+
);
|
|
3891
|
+
await this.db.run(
|
|
3892
|
+
`INSERT INTO entity_aliases (entity_type, canonical_key, entity_id, is_primary)
|
|
3893
|
+
VALUES (?, ?, ?, TRUE)
|
|
3894
|
+
ON CONFLICT (entity_type, canonical_key) DO NOTHING`,
|
|
3895
|
+
["task", canonicalKey, taskId]
|
|
3896
|
+
);
|
|
3897
|
+
const eventId = await this.emitTaskEvent("task_created", {
|
|
3898
|
+
taskId,
|
|
3899
|
+
title: extracted.title,
|
|
3900
|
+
canonicalKey,
|
|
3901
|
+
initialStatus,
|
|
3902
|
+
priority: extracted.priority ?? "medium",
|
|
3903
|
+
description: extracted.description,
|
|
3904
|
+
project: extracted.project ?? this.config.project
|
|
3905
|
+
});
|
|
3906
|
+
const task = {
|
|
3907
|
+
entityId: taskId,
|
|
3908
|
+
entityType: "task",
|
|
3909
|
+
canonicalKey,
|
|
3910
|
+
title: extracted.title,
|
|
3911
|
+
stage: "raw",
|
|
3912
|
+
status: "active",
|
|
3913
|
+
currentJson,
|
|
3914
|
+
titleNorm: extracted.title.toLowerCase().trim(),
|
|
3915
|
+
searchText: `${extracted.title} ${extracted.description ?? ""}`,
|
|
3916
|
+
createdAt: now,
|
|
3917
|
+
updatedAt: now
|
|
3918
|
+
};
|
|
3919
|
+
return { task, isNew: true, eventId };
|
|
3920
|
+
}
|
|
3921
|
+
/**
|
|
3922
|
+
* Handle task status change
|
|
3923
|
+
*/
|
|
3924
|
+
async handleStatusChange(task, newStatus) {
|
|
3925
|
+
const currentJson = task.currentJson;
|
|
3926
|
+
const currentStatus = currentJson.status;
|
|
3927
|
+
if (currentStatus === newStatus) {
|
|
3928
|
+
return null;
|
|
3929
|
+
}
|
|
3930
|
+
const validNextStates = VALID_TRANSITIONS[currentStatus] ?? [];
|
|
3931
|
+
if (!validNextStates.includes(newStatus)) {
|
|
3932
|
+
return this.emitTaskEvent("task_transition_rejected", {
|
|
3933
|
+
taskId: task.entityId,
|
|
3934
|
+
fromStatus: currentStatus,
|
|
3935
|
+
toStatus: newStatus,
|
|
3936
|
+
reason: `Invalid transition from ${currentStatus} to ${newStatus}`
|
|
3937
|
+
});
|
|
3938
|
+
}
|
|
3939
|
+
const eventId = await this.emitTaskEvent("task_status_changed", {
|
|
3940
|
+
taskId: task.entityId,
|
|
3941
|
+
fromStatus: currentStatus,
|
|
3942
|
+
toStatus: newStatus
|
|
3943
|
+
});
|
|
3944
|
+
await this.db.run(
|
|
3945
|
+
`UPDATE entities
|
|
3946
|
+
SET current_json = json_set(current_json, '$.status', ?),
|
|
3947
|
+
updated_at = ?
|
|
3948
|
+
WHERE entity_id = ?`,
|
|
3949
|
+
[newStatus, (/* @__PURE__ */ new Date()).toISOString(), task.entityId]
|
|
3950
|
+
);
|
|
3951
|
+
return eventId;
|
|
3952
|
+
}
|
|
3953
|
+
/**
|
|
3954
|
+
* Handle task priority change
|
|
3955
|
+
*/
|
|
3956
|
+
async handlePriorityChange(task, newPriority) {
|
|
3957
|
+
const currentJson = task.currentJson;
|
|
3958
|
+
const currentPriority = currentJson.priority ?? "medium";
|
|
3959
|
+
if (currentPriority === newPriority) {
|
|
3960
|
+
return null;
|
|
3961
|
+
}
|
|
3962
|
+
const eventId = await this.emitTaskEvent("task_priority_changed", {
|
|
3963
|
+
taskId: task.entityId,
|
|
3964
|
+
fromPriority: currentPriority,
|
|
3965
|
+
toPriority: newPriority
|
|
3966
|
+
});
|
|
3967
|
+
await this.db.run(
|
|
3968
|
+
`UPDATE entities
|
|
3969
|
+
SET current_json = json_set(current_json, '$.priority', ?),
|
|
3970
|
+
updated_at = ?
|
|
3971
|
+
WHERE entity_id = ?`,
|
|
3972
|
+
[newPriority, (/* @__PURE__ */ new Date()).toISOString(), task.entityId]
|
|
3973
|
+
);
|
|
3974
|
+
return eventId;
|
|
3975
|
+
}
|
|
3976
|
+
/**
|
|
3977
|
+
* Handle blockers
|
|
3978
|
+
*/
|
|
3979
|
+
async handleBlockers(task, blockedByTexts, sourceEntryId) {
|
|
3980
|
+
const blockerRefs = await this.blockerResolver.resolveBlockers(blockedByTexts);
|
|
3981
|
+
const mode = this.config.evidenceAligned ? "replace" : "suggest";
|
|
3982
|
+
const eventId = await this.emitTaskEvent("task_blockers_set", {
|
|
3983
|
+
taskId: task.entityId,
|
|
3984
|
+
mode,
|
|
3985
|
+
blockers: blockerRefs,
|
|
3986
|
+
sourceEntryId
|
|
3987
|
+
});
|
|
3988
|
+
return eventId;
|
|
3989
|
+
}
|
|
3990
|
+
/**
|
|
3991
|
+
* Handle unknown blocker (status=blocked but no blockedBy)
|
|
3992
|
+
*/
|
|
3993
|
+
async handleUnknownBlocker(task) {
|
|
3994
|
+
const placeholderRef = await this.blockerResolver.createUnknownPlaceholder(task.title);
|
|
3995
|
+
const eventId = await this.emitTaskEvent("task_blockers_set", {
|
|
3996
|
+
taskId: task.entityId,
|
|
3997
|
+
mode: "suggest",
|
|
3998
|
+
blockers: [placeholderRef]
|
|
3999
|
+
});
|
|
4000
|
+
return eventId;
|
|
4001
|
+
}
|
|
4002
|
+
/**
|
|
4003
|
+
* Emit task event to events table
|
|
4004
|
+
*/
|
|
4005
|
+
async emitTaskEvent(eventType, payload) {
|
|
4006
|
+
const eventId = randomUUID6();
|
|
4007
|
+
const now = /* @__PURE__ */ new Date();
|
|
4008
|
+
const dedupeKey = makeTaskEventDedupeKey(
|
|
4009
|
+
eventType,
|
|
4010
|
+
payload.taskId,
|
|
4011
|
+
this.config.sessionId,
|
|
4012
|
+
JSON.stringify(payload)
|
|
4013
|
+
);
|
|
4014
|
+
const existing = await this.db.all(
|
|
4015
|
+
`SELECT event_id FROM event_dedup WHERE dedupe_key = ?`,
|
|
4016
|
+
[dedupeKey]
|
|
4017
|
+
);
|
|
4018
|
+
if (existing.length > 0) {
|
|
4019
|
+
return existing[0].event_id;
|
|
4020
|
+
}
|
|
4021
|
+
await this.db.run(
|
|
4022
|
+
`INSERT INTO events (
|
|
4023
|
+
id, event_type, session_id, timestamp, content,
|
|
4024
|
+
canonical_key, dedupe_key, metadata
|
|
4025
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
4026
|
+
[
|
|
4027
|
+
eventId,
|
|
4028
|
+
eventType,
|
|
4029
|
+
this.config.sessionId,
|
|
4030
|
+
now.toISOString(),
|
|
4031
|
+
JSON.stringify(payload),
|
|
4032
|
+
`task_event:${eventType}:${payload.taskId}`,
|
|
4033
|
+
dedupeKey,
|
|
4034
|
+
JSON.stringify({ source: "task_resolver" })
|
|
4035
|
+
]
|
|
4036
|
+
);
|
|
4037
|
+
await this.db.run(
|
|
4038
|
+
`INSERT INTO event_dedup (dedupe_key, event_id)
|
|
4039
|
+
VALUES (?, ?)
|
|
4040
|
+
ON CONFLICT DO NOTHING`,
|
|
4041
|
+
[dedupeKey, eventId]
|
|
4042
|
+
);
|
|
4043
|
+
return eventId;
|
|
4044
|
+
}
|
|
4045
|
+
/**
|
|
4046
|
+
* Resolve condition to task (when condition is identified as existing task)
|
|
4047
|
+
*/
|
|
4048
|
+
async resolveConditionToTask(conditionId, taskId) {
|
|
4049
|
+
const eventId = await this.emitTaskEvent("condition_resolved_to", {
|
|
4050
|
+
conditionId,
|
|
4051
|
+
resolvedTo: {
|
|
4052
|
+
kind: "task",
|
|
4053
|
+
entityId: taskId
|
|
4054
|
+
}
|
|
4055
|
+
});
|
|
4056
|
+
await this.db.run(
|
|
4057
|
+
`INSERT INTO edges (edge_id, src_type, src_id, rel_type, dst_type, dst_id, meta_json)
|
|
4058
|
+
VALUES (?, 'entity', ?, 'resolves_to', 'entity', ?, ?)
|
|
4059
|
+
ON CONFLICT DO NOTHING`,
|
|
4060
|
+
[randomUUID6(), conditionId, taskId, JSON.stringify({ resolved_at: (/* @__PURE__ */ new Date()).toISOString() })]
|
|
4061
|
+
);
|
|
4062
|
+
return eventId;
|
|
4063
|
+
}
|
|
4064
|
+
};
|
|
4065
|
+
|
|
4066
|
+
// src/core/task/task-projector.ts
|
|
4067
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
4068
|
+
var PROJECTOR_NAME = "task_projector";
|
|
4069
|
+
var TASK_EVENT_TYPES = [
|
|
4070
|
+
"task_created",
|
|
4071
|
+
"task_status_changed",
|
|
4072
|
+
"task_priority_changed",
|
|
4073
|
+
"task_blockers_set",
|
|
4074
|
+
"task_transition_rejected",
|
|
4075
|
+
"condition_resolved_to"
|
|
4076
|
+
];
|
|
4077
|
+
var TaskProjector = class {
|
|
4078
|
+
constructor(db) {
|
|
4079
|
+
this.db = db;
|
|
4080
|
+
}
|
|
4081
|
+
/**
|
|
4082
|
+
* Get current projection offset
|
|
4083
|
+
*/
|
|
4084
|
+
async getOffset() {
|
|
4085
|
+
const rows = await this.db.all(
|
|
4086
|
+
`SELECT last_event_id, last_timestamp
|
|
4087
|
+
FROM projection_offsets
|
|
4088
|
+
WHERE projection_name = ?`,
|
|
4089
|
+
[PROJECTOR_NAME]
|
|
4090
|
+
);
|
|
4091
|
+
if (rows.length === 0) {
|
|
4092
|
+
return { lastEventId: null, lastTimestamp: null };
|
|
4093
|
+
}
|
|
4094
|
+
return {
|
|
4095
|
+
lastEventId: rows[0].last_event_id,
|
|
4096
|
+
lastTimestamp: rows[0].last_timestamp ? new Date(rows[0].last_timestamp) : null
|
|
4097
|
+
};
|
|
4098
|
+
}
|
|
4099
|
+
/**
|
|
4100
|
+
* Update projection offset
|
|
4101
|
+
*/
|
|
4102
|
+
async updateOffset(eventId, timestamp) {
|
|
4103
|
+
await this.db.run(
|
|
4104
|
+
`INSERT INTO projection_offsets (projection_name, last_event_id, last_timestamp, updated_at)
|
|
4105
|
+
VALUES (?, ?, ?, CURRENT_TIMESTAMP)
|
|
4106
|
+
ON CONFLICT (projection_name) DO UPDATE SET
|
|
4107
|
+
last_event_id = excluded.last_event_id,
|
|
4108
|
+
last_timestamp = excluded.last_timestamp,
|
|
4109
|
+
updated_at = CURRENT_TIMESTAMP`,
|
|
4110
|
+
[PROJECTOR_NAME, eventId, timestamp.toISOString()]
|
|
4111
|
+
);
|
|
4112
|
+
}
|
|
4113
|
+
/**
|
|
4114
|
+
* Fetch events since last offset
|
|
4115
|
+
*/
|
|
4116
|
+
async fetchEventsSince(offset, limit = 100) {
|
|
4117
|
+
let query = `
|
|
4118
|
+
SELECT id, event_type, session_id, timestamp, content
|
|
4119
|
+
FROM events
|
|
4120
|
+
WHERE event_type IN (${TASK_EVENT_TYPES.map(() => "?").join(", ")})
|
|
4121
|
+
`;
|
|
4122
|
+
const params = [...TASK_EVENT_TYPES];
|
|
4123
|
+
if (offset.lastTimestamp && offset.lastEventId) {
|
|
4124
|
+
query += ` AND (timestamp > ? OR (timestamp = ? AND id > ?))`;
|
|
4125
|
+
params.push(
|
|
4126
|
+
offset.lastTimestamp.toISOString(),
|
|
4127
|
+
offset.lastTimestamp.toISOString(),
|
|
4128
|
+
offset.lastEventId
|
|
4129
|
+
);
|
|
4130
|
+
}
|
|
4131
|
+
query += ` ORDER BY timestamp ASC, id ASC LIMIT ?`;
|
|
4132
|
+
params.push(limit);
|
|
4133
|
+
const rows = await this.db.all(query, params);
|
|
4134
|
+
return rows.map((row) => ({
|
|
4135
|
+
id: row.id,
|
|
4136
|
+
eventType: row.event_type,
|
|
4137
|
+
sessionId: row.session_id,
|
|
4138
|
+
timestamp: new Date(row.timestamp),
|
|
4139
|
+
content: typeof row.content === "string" ? JSON.parse(row.content) : row.content
|
|
4140
|
+
}));
|
|
4141
|
+
}
|
|
4142
|
+
/**
|
|
4143
|
+
* Process a batch of events
|
|
4144
|
+
*/
|
|
4145
|
+
async processBatch(batchSize = 100) {
|
|
4146
|
+
const offset = await this.getOffset();
|
|
4147
|
+
const events = await this.fetchEventsSince(offset, batchSize);
|
|
4148
|
+
if (events.length === 0) {
|
|
4149
|
+
return 0;
|
|
4150
|
+
}
|
|
4151
|
+
for (const event of events) {
|
|
4152
|
+
await this.processEvent(event);
|
|
4153
|
+
await this.updateOffset(event.id, event.timestamp);
|
|
4154
|
+
}
|
|
4155
|
+
return events.length;
|
|
4156
|
+
}
|
|
4157
|
+
/**
|
|
4158
|
+
* Process all pending events
|
|
4159
|
+
*/
|
|
4160
|
+
async processAll() {
|
|
4161
|
+
let totalProcessed = 0;
|
|
4162
|
+
let processed;
|
|
4163
|
+
do {
|
|
4164
|
+
processed = await this.processBatch();
|
|
4165
|
+
totalProcessed += processed;
|
|
4166
|
+
} while (processed > 0);
|
|
4167
|
+
return totalProcessed;
|
|
4168
|
+
}
|
|
4169
|
+
/**
|
|
4170
|
+
* Process a single event
|
|
4171
|
+
*/
|
|
4172
|
+
async processEvent(event) {
|
|
4173
|
+
switch (event.eventType) {
|
|
4174
|
+
case "task_created":
|
|
4175
|
+
await this.enqueueForVectorization(event.content.taskId, "task_title");
|
|
4176
|
+
break;
|
|
4177
|
+
case "task_status_changed":
|
|
4178
|
+
await this.handleStatusChanged(event);
|
|
4179
|
+
break;
|
|
4180
|
+
case "task_priority_changed":
|
|
4181
|
+
break;
|
|
4182
|
+
case "task_blockers_set":
|
|
4183
|
+
await this.handleBlockersSet(event);
|
|
4184
|
+
break;
|
|
4185
|
+
case "condition_resolved_to":
|
|
4186
|
+
await this.handleConditionResolved(event);
|
|
4187
|
+
break;
|
|
4188
|
+
case "task_transition_rejected":
|
|
4189
|
+
break;
|
|
4190
|
+
}
|
|
4191
|
+
}
|
|
4192
|
+
/**
|
|
4193
|
+
* Handle task_status_changed event
|
|
4194
|
+
*/
|
|
4195
|
+
async handleStatusChanged(event) {
|
|
4196
|
+
const { taskId, toStatus } = event.content;
|
|
4197
|
+
if (toStatus === "done") {
|
|
4198
|
+
await this.db.run(
|
|
4199
|
+
`DELETE FROM edges
|
|
4200
|
+
WHERE src_id = ? AND rel_type IN ('blocked_by', 'blocked_by_suggested')`,
|
|
4201
|
+
[taskId]
|
|
4202
|
+
);
|
|
4203
|
+
await this.db.run(
|
|
4204
|
+
`UPDATE entities
|
|
4205
|
+
SET current_json = json_remove(json_remove(current_json, '$.blockers'), '$.blockerSuggestions'),
|
|
4206
|
+
updated_at = CURRENT_TIMESTAMP
|
|
4207
|
+
WHERE entity_id = ?`,
|
|
4208
|
+
[taskId]
|
|
4209
|
+
);
|
|
4210
|
+
}
|
|
4211
|
+
}
|
|
4212
|
+
/**
|
|
4213
|
+
* Handle task_blockers_set event
|
|
4214
|
+
*/
|
|
4215
|
+
async handleBlockersSet(event) {
|
|
4216
|
+
const { taskId, mode, blockers } = event.content;
|
|
4217
|
+
if (mode === "replace") {
|
|
4218
|
+
await this.db.run(
|
|
4219
|
+
`DELETE FROM edges WHERE src_id = ? AND rel_type = 'blocked_by'`,
|
|
4220
|
+
[taskId]
|
|
4221
|
+
);
|
|
4222
|
+
for (const blocker of blockers) {
|
|
4223
|
+
await this.createBlockerEdge(taskId, blocker, "blocked_by");
|
|
4224
|
+
}
|
|
4225
|
+
const blockerIds = blockers.map((b) => b.entityId);
|
|
4226
|
+
await this.db.run(
|
|
4227
|
+
`UPDATE entities
|
|
4228
|
+
SET current_json = json_set(current_json, '$.blockers', ?),
|
|
4229
|
+
updated_at = CURRENT_TIMESTAMP
|
|
4230
|
+
WHERE entity_id = ?`,
|
|
4231
|
+
[JSON.stringify(blockerIds), taskId]
|
|
4232
|
+
);
|
|
4233
|
+
} else {
|
|
4234
|
+
await this.db.run(
|
|
4235
|
+
`DELETE FROM edges WHERE src_id = ? AND rel_type = 'blocked_by_suggested'`,
|
|
4236
|
+
[taskId]
|
|
4237
|
+
);
|
|
4238
|
+
for (const blocker of blockers) {
|
|
4239
|
+
await this.createBlockerEdge(taskId, blocker, "blocked_by_suggested");
|
|
4240
|
+
}
|
|
4241
|
+
const suggestionIds = blockers.map((b) => b.entityId);
|
|
4242
|
+
await this.db.run(
|
|
4243
|
+
`UPDATE entities
|
|
4244
|
+
SET current_json = json_set(current_json, '$.blockerSuggestions', ?),
|
|
4245
|
+
updated_at = CURRENT_TIMESTAMP
|
|
4246
|
+
WHERE entity_id = ?`,
|
|
4247
|
+
[JSON.stringify(suggestionIds), taskId]
|
|
4248
|
+
);
|
|
4249
|
+
}
|
|
4250
|
+
}
|
|
4251
|
+
/**
|
|
4252
|
+
* Create blocker edge
|
|
4253
|
+
*/
|
|
4254
|
+
async createBlockerEdge(taskId, blocker, relType) {
|
|
4255
|
+
const edgeId = randomUUID7();
|
|
4256
|
+
await this.db.run(
|
|
4257
|
+
`INSERT INTO edges (edge_id, src_type, src_id, rel_type, dst_type, dst_id, meta_json, created_at)
|
|
4258
|
+
VALUES (?, 'entity', ?, ?, 'entity', ?, ?, CURRENT_TIMESTAMP)
|
|
4259
|
+
ON CONFLICT DO NOTHING`,
|
|
4260
|
+
[
|
|
4261
|
+
edgeId,
|
|
4262
|
+
taskId,
|
|
4263
|
+
relType,
|
|
4264
|
+
blocker.entityId,
|
|
4265
|
+
JSON.stringify({
|
|
4266
|
+
kind: blocker.kind,
|
|
4267
|
+
rawText: blocker.rawText,
|
|
4268
|
+
confidence: blocker.confidence,
|
|
4269
|
+
candidates: blocker.candidates
|
|
4270
|
+
})
|
|
4271
|
+
]
|
|
4272
|
+
);
|
|
4273
|
+
}
|
|
4274
|
+
/**
|
|
4275
|
+
* Handle condition_resolved_to event
|
|
4276
|
+
*/
|
|
4277
|
+
async handleConditionResolved(event) {
|
|
4278
|
+
const { conditionId, resolvedTo } = event.content;
|
|
4279
|
+
await this.db.run(
|
|
4280
|
+
`UPDATE entities
|
|
4281
|
+
SET current_json = json_set(json_set(current_json, '$.resolved', true), '$.resolvedTo', ?),
|
|
4282
|
+
updated_at = CURRENT_TIMESTAMP
|
|
4283
|
+
WHERE entity_id = ?`,
|
|
4284
|
+
[JSON.stringify(resolvedTo), conditionId]
|
|
4285
|
+
);
|
|
4286
|
+
}
|
|
4287
|
+
/**
|
|
4288
|
+
* Enqueue entity for vectorization
|
|
4289
|
+
*/
|
|
4290
|
+
async enqueueForVectorization(itemId, itemKind) {
|
|
4291
|
+
const jobId = randomUUID7();
|
|
4292
|
+
const embeddingVersion = "v1";
|
|
4293
|
+
await this.db.run(
|
|
4294
|
+
`INSERT INTO vector_outbox (job_id, item_kind, item_id, embedding_version, status, retry_count)
|
|
4295
|
+
VALUES (?, ?, ?, ?, 'pending', 0)
|
|
4296
|
+
ON CONFLICT (item_kind, item_id, embedding_version) DO NOTHING`,
|
|
4297
|
+
[jobId, itemKind, itemId, embeddingVersion]
|
|
4298
|
+
);
|
|
4299
|
+
}
|
|
4300
|
+
/**
|
|
4301
|
+
* Rebuild all projections from scratch
|
|
4302
|
+
* WARNING: This clears all edges and rebuilds from events
|
|
4303
|
+
*/
|
|
4304
|
+
async rebuild() {
|
|
4305
|
+
await this.db.run(
|
|
4306
|
+
`DELETE FROM edges WHERE rel_type IN ('blocked_by', 'blocked_by_suggested', 'resolves_to')`
|
|
4307
|
+
);
|
|
4308
|
+
await this.db.run(
|
|
4309
|
+
`DELETE FROM projection_offsets WHERE projection_name = ?`,
|
|
4310
|
+
[PROJECTOR_NAME]
|
|
4311
|
+
);
|
|
4312
|
+
return this.processAll();
|
|
4313
|
+
}
|
|
4314
|
+
};
|
|
4315
|
+
export {
|
|
4316
|
+
AlignedEvidenceSchema,
|
|
4317
|
+
BlockerKindSchema,
|
|
4318
|
+
BlockerModeSchema,
|
|
4319
|
+
BlockerRefSchema,
|
|
4320
|
+
BlockerResolver,
|
|
4321
|
+
BuildRunSchema,
|
|
4322
|
+
CitationSchema,
|
|
4323
|
+
CitationUsageSchema,
|
|
4324
|
+
ConfigSchema,
|
|
4325
|
+
ConsolidatedMemorySchema,
|
|
4326
|
+
ContinuityLogSchema,
|
|
4327
|
+
DefaultContentProvider,
|
|
4328
|
+
EdgeRepo,
|
|
4329
|
+
EdgeSchema,
|
|
4330
|
+
Embedder,
|
|
4331
|
+
EndlessModeConfigSchema,
|
|
4332
|
+
EntityAliasSchema,
|
|
4333
|
+
EntityRepo,
|
|
4334
|
+
EntitySchema,
|
|
4335
|
+
EntityStageSchema,
|
|
4336
|
+
EntityStatusSchema,
|
|
4337
|
+
EntityTypeSchema,
|
|
4338
|
+
EntrySchema,
|
|
4339
|
+
EntryTypeSchema,
|
|
4340
|
+
EventStore,
|
|
4341
|
+
EventTypeSchema,
|
|
4342
|
+
EvidenceAlignResultSchema,
|
|
4343
|
+
EvidenceAligner,
|
|
4344
|
+
EvidenceSpanSchema,
|
|
4345
|
+
ExtractedEvidenceSchema,
|
|
4346
|
+
FailedEvidenceSchema,
|
|
4347
|
+
FullDetailSchema,
|
|
4348
|
+
GraduationPipeline,
|
|
4349
|
+
GraduationResultSchema,
|
|
4350
|
+
InsightSchema,
|
|
4351
|
+
InsightTypeSchema,
|
|
4352
|
+
MATCH_THRESHOLDS,
|
|
4353
|
+
MatchConfidenceSchema,
|
|
4354
|
+
MatchResultSchema,
|
|
4355
|
+
Matcher,
|
|
4356
|
+
MemoryEventInputSchema,
|
|
4357
|
+
MemoryEventSchema,
|
|
4358
|
+
MemoryLevelSchema,
|
|
4359
|
+
MemoryMatchSchema,
|
|
4360
|
+
MemoryModeSchema,
|
|
4361
|
+
NodeTypeSchema,
|
|
4362
|
+
OutboxItemKindSchema,
|
|
4363
|
+
OutboxJobSchema,
|
|
4364
|
+
OutboxStatusSchema,
|
|
4365
|
+
PipelineMetricSchema,
|
|
4366
|
+
ProgressiveDisclosureConfigSchema,
|
|
4367
|
+
ProgressiveSearchResultSchema,
|
|
4368
|
+
RelationTypeSchema,
|
|
4369
|
+
Retriever,
|
|
4370
|
+
SearchIndexItemSchema,
|
|
4371
|
+
SessionSchema,
|
|
4372
|
+
TaskBlockersSetPayloadSchema,
|
|
4373
|
+
TaskCreatedPayloadSchema,
|
|
4374
|
+
TaskCurrentJsonSchema,
|
|
4375
|
+
TaskEventTypeSchema,
|
|
4376
|
+
TaskMatcher,
|
|
4377
|
+
TaskPrioritySchema,
|
|
4378
|
+
TaskProjector,
|
|
4379
|
+
TaskResolver,
|
|
4380
|
+
TaskStatusChangedPayloadSchema,
|
|
4381
|
+
TaskStatusSchema,
|
|
4382
|
+
TimelineItemSchema,
|
|
4383
|
+
ToolMetadataSchema,
|
|
4384
|
+
ToolObservationPayloadSchema,
|
|
4385
|
+
TransitionTypeSchema,
|
|
4386
|
+
VALID_OUTBOX_TRANSITIONS,
|
|
4387
|
+
VectorOutbox,
|
|
4388
|
+
VectorStore,
|
|
4389
|
+
VectorWorker,
|
|
4390
|
+
VectorWorkerV2,
|
|
4391
|
+
WorkingSetItemSchema,
|
|
4392
|
+
createGraduationPipeline,
|
|
4393
|
+
createRetriever,
|
|
4394
|
+
createVectorWorker,
|
|
4395
|
+
createVectorWorkerV2,
|
|
4396
|
+
getDefaultAligner,
|
|
4397
|
+
getDefaultEmbedder,
|
|
4398
|
+
getDefaultMatcher,
|
|
4399
|
+
hashContent,
|
|
4400
|
+
isSameCanonicalKey,
|
|
4401
|
+
makeArtifactKey,
|
|
4402
|
+
makeCanonicalKey,
|
|
4403
|
+
makeDedupeKey,
|
|
4404
|
+
makeEntityCanonicalKey,
|
|
4405
|
+
makeTaskEventDedupeKey,
|
|
4406
|
+
parseEntityCanonicalKey
|
|
4407
|
+
};
|
|
4408
|
+
//# sourceMappingURL=index.js.map
|