@orquentra/codebuddy 2.4.1 → 2.6.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.
Files changed (61) hide show
  1. package/README.md +25 -2
  2. package/dist/{chunk-ITGRANYW.js → chunk-6MKEIDX5.js} +2 -2
  3. package/dist/{chunk-K63EOA4B.js → chunk-776EQXDV.js} +25 -6
  4. package/dist/chunk-776EQXDV.js.map +1 -0
  5. package/dist/chunk-JFDRBPYF.js +99 -0
  6. package/dist/chunk-JFDRBPYF.js.map +1 -0
  7. package/dist/chunk-JPHJBCZB.js +99 -0
  8. package/dist/chunk-JPHJBCZB.js.map +1 -0
  9. package/dist/{chunk-IBMJYIEP.js → chunk-L5AYZC3F.js} +7 -7
  10. package/dist/{chunk-DSN5CEHO.js → chunk-N34XHWBA.js} +3 -3
  11. package/dist/{chunk-3ZELDCR3.js → chunk-PCE4STAY.js} +7 -7
  12. package/dist/{chunk-TI5TQD6D.js → chunk-QDBDYCM2.js} +2 -2
  13. package/dist/{chunk-5OWZVZSO.js → chunk-SYWJG5NA.js} +8 -165
  14. package/dist/chunk-SYWJG5NA.js.map +1 -0
  15. package/dist/chunk-U4HAJLIB.js +91 -0
  16. package/dist/chunk-U4HAJLIB.js.map +1 -0
  17. package/dist/{chunk-IE2OUO54.js → chunk-UFZD3JAA.js} +3 -3
  18. package/dist/chunk-UN57FKQX.js +4033 -0
  19. package/dist/chunk-UN57FKQX.js.map +1 -0
  20. package/dist/chunk-VAKBJV26.js +5112 -0
  21. package/dist/chunk-VAKBJV26.js.map +1 -0
  22. package/dist/chunk-XBJQKCLU.js +103 -0
  23. package/dist/chunk-XBJQKCLU.js.map +1 -0
  24. package/dist/cli/index.js +2 -2
  25. package/dist/cloud-7WM3YLI4.js +17 -0
  26. package/dist/{codebuddy-D2xJ6hFk.d.ts → codebuddy-DM-i0Fu5.d.ts} +49 -18
  27. package/dist/{config-S2TULOMK.js → config-KFEHLV6W.js} +3 -3
  28. package/dist/index.d.ts +23 -3
  29. package/dist/index.js +34 -25
  30. package/dist/langgraph/index.d.ts +1 -1
  31. package/dist/lean-command-C5YPFKHP.js +15 -0
  32. package/dist/local-OTOY56B4.js +13 -0
  33. package/dist/{main-BFDVQHKO.js → main-HFHLGID4.js} +1410 -245
  34. package/dist/main-HFHLGID4.js.map +1 -0
  35. package/dist/{memory-NIBFAMY4.js → memory-4RGVLPKG.js} +6 -6
  36. package/dist/operations-LOBAQRLO.js +24 -0
  37. package/dist/operations-LOBAQRLO.js.map +1 -0
  38. package/dist/{rerank-Q7MIFNKA.js → rerank-3P3C2UHN.js} +4 -4
  39. package/dist/{summaries-TSE44KUQ.js → summaries-ZBV5IN7Y.js} +6 -6
  40. package/dist/summaries-ZBV5IN7Y.js.map +1 -0
  41. package/dist/{zvec-F27AR357.js → zvec-MAPSJQSR.js} +3 -3
  42. package/dist/zvec-MAPSJQSR.js.map +1 -0
  43. package/package.json +6 -2
  44. package/dist/chunk-5OWZVZSO.js.map +0 -1
  45. package/dist/chunk-K63EOA4B.js.map +0 -1
  46. package/dist/chunk-OLFCOR4U.js +0 -8580
  47. package/dist/chunk-OLFCOR4U.js.map +0 -1
  48. package/dist/lean-command-D6TZJEFX.js +0 -13
  49. package/dist/main-BFDVQHKO.js.map +0 -1
  50. /package/dist/{chunk-ITGRANYW.js.map → chunk-6MKEIDX5.js.map} +0 -0
  51. /package/dist/{chunk-IBMJYIEP.js.map → chunk-L5AYZC3F.js.map} +0 -0
  52. /package/dist/{chunk-DSN5CEHO.js.map → chunk-N34XHWBA.js.map} +0 -0
  53. /package/dist/{chunk-3ZELDCR3.js.map → chunk-PCE4STAY.js.map} +0 -0
  54. /package/dist/{chunk-TI5TQD6D.js.map → chunk-QDBDYCM2.js.map} +0 -0
  55. /package/dist/{chunk-IE2OUO54.js.map → chunk-UFZD3JAA.js.map} +0 -0
  56. /package/dist/{config-S2TULOMK.js.map → cloud-7WM3YLI4.js.map} +0 -0
  57. /package/dist/{lean-command-D6TZJEFX.js.map → config-KFEHLV6W.js.map} +0 -0
  58. /package/dist/{summaries-TSE44KUQ.js.map → lean-command-C5YPFKHP.js.map} +0 -0
  59. /package/dist/{zvec-F27AR357.js.map → local-OTOY56B4.js.map} +0 -0
  60. /package/dist/{memory-NIBFAMY4.js.map → memory-4RGVLPKG.js.map} +0 -0
  61. /package/dist/{rerank-Q7MIFNKA.js.map → rerank-3P3C2UHN.js.map} +0 -0
@@ -0,0 +1,4033 @@
1
+ import {
2
+ REDACTED,
3
+ redactPersisted
4
+ } from "./chunk-GDJH7XJC.js";
5
+ import {
6
+ clampBudgetForCallerModel,
7
+ computeContentHash,
8
+ computeFactHash,
9
+ countTokens,
10
+ generateEntityId,
11
+ generateSessionId
12
+ } from "./chunk-XBJQKCLU.js";
13
+ import {
14
+ OpenRouterEmbeddingProvider
15
+ } from "./chunk-JFDRBPYF.js";
16
+ import {
17
+ LocalModelProvider
18
+ } from "./chunk-U4HAJLIB.js";
19
+ import {
20
+ DEFAULT_CLOUD_EMBEDDING_MODELS,
21
+ DEFAULT_EMBEDDING_MODELS,
22
+ DEFAULT_LLM_MODELS,
23
+ EMBEDDING_DIMENSIONS,
24
+ assertCompatibleEmbeddingModels,
25
+ getModelDefinition
26
+ } from "./chunk-JPHJBCZB.js";
27
+ import {
28
+ MISSING_DATABASE_URL,
29
+ assertNoSymlinkBetween,
30
+ checkConfigPermissions,
31
+ checkProjectScaffold,
32
+ composeMarkdown,
33
+ defaultNamespaceForCwd,
34
+ loadRuntimeConfig,
35
+ parseFrontMatter,
36
+ readConfigFile,
37
+ redactSecrets,
38
+ writeAtomic
39
+ } from "./chunk-776EQXDV.js";
40
+ import {
41
+ __export
42
+ } from "./chunk-PZ5AY32C.js";
43
+
44
+ // src/db/schema.ts
45
+ var schema_exports = {};
46
+ __export(schema_exports, {
47
+ HNSW_EF_CONSTRUCTION: () => HNSW_EF_CONSTRUCTION,
48
+ HNSW_M: () => HNSW_M,
49
+ VECTOR_DIMENSIONS: () => VECTOR_DIMENSIONS,
50
+ auditActionEnum: () => auditActionEnum,
51
+ auditLog: () => auditLog,
52
+ checkpoints: () => checkpoints,
53
+ embeddingOwnerTypeEnum: () => embeddingOwnerTypeEnum,
54
+ embeddingStatusEnum: () => embeddingStatusEnum,
55
+ embeddings: () => embeddings,
56
+ facts: () => facts,
57
+ interactionRoleEnum: () => interactionRoleEnum,
58
+ interactions: () => interactions,
59
+ memoryTypeEnum: () => memoryTypeEnum,
60
+ modelCallStatusEnum: () => modelCallStatusEnum,
61
+ modelCallTypeEnum: () => modelCallTypeEnum,
62
+ modelCalls: () => modelCalls,
63
+ namespaces: () => namespaces,
64
+ sessionSummaries: () => sessionSummaries,
65
+ shareModeEnum: () => shareModeEnum,
66
+ shares: () => shares
67
+ });
68
+ import { sql } from "drizzle-orm";
69
+ import {
70
+ boolean,
71
+ customType,
72
+ index,
73
+ integer,
74
+ jsonb,
75
+ numeric,
76
+ pgEnum,
77
+ pgTable,
78
+ text,
79
+ timestamp,
80
+ uniqueIndex
81
+ } from "drizzle-orm/pg-core";
82
+ var VECTOR_DIMENSIONS = 384;
83
+ var HNSW_M = 16;
84
+ var HNSW_EF_CONSTRUCTION = 64;
85
+ var vector = customType({
86
+ dataType() {
87
+ return `vector(${VECTOR_DIMENSIONS})`;
88
+ }
89
+ });
90
+ var memoryTypeEnum = pgEnum("memory_type", ["interaction", "fact", "summary"]);
91
+ var interactionRoleEnum = pgEnum("interaction_role", ["user", "assistant", "system"]);
92
+ var embeddingOwnerTypeEnum = pgEnum("embedding_owner_type", [
93
+ "interaction",
94
+ "fact",
95
+ "summary"
96
+ ]);
97
+ var embeddingStatusEnum = pgEnum("embedding_status", [
98
+ "pending",
99
+ "processing",
100
+ "ready",
101
+ "failed"
102
+ ]);
103
+ var shareModeEnum = pgEnum("share_mode", ["reference", "snapshot"]);
104
+ var modelCallTypeEnum = pgEnum("model_call_type", ["embedding", "llm"]);
105
+ var modelCallStatusEnum = pgEnum("model_call_status", [
106
+ "success",
107
+ "failed",
108
+ "timeout",
109
+ "rate_limited",
110
+ "gated",
111
+ "fallback"
112
+ ]);
113
+ var auditActionEnum = pgEnum("audit_action", [
114
+ "remember",
115
+ "remember_batch",
116
+ "share",
117
+ "forget",
118
+ "config"
119
+ ]);
120
+ var createdAt = timestamp("created_at", { withTimezone: true }).notNull().defaultNow();
121
+ var updatedAt = timestamp("updated_at", { withTimezone: true }).notNull().defaultNow();
122
+ var namespaces = pgTable(
123
+ "namespaces",
124
+ {
125
+ id: text("id").primaryKey(),
126
+ name: text("name").notNull(),
127
+ createdAt,
128
+ updatedAt
129
+ },
130
+ (table) => ({
131
+ nameUnique: uniqueIndex("namespaces_name_unique").on(table.name)
132
+ })
133
+ );
134
+ var interactions = pgTable(
135
+ "interactions",
136
+ {
137
+ id: text("id").primaryKey(),
138
+ namespaceId: text("namespace_id").notNull().references(() => namespaces.id, { onDelete: "cascade" }),
139
+ sessionId: text("session_id").notNull(),
140
+ role: interactionRoleEnum("role").notNull().default("user"),
141
+ content: text("content").notNull(),
142
+ contentHash: text("content_hash").notNull(),
143
+ tokenCount: integer("token_count").notNull().default(0),
144
+ createdByAgent: text("created_by_agent"),
145
+ createdAt
146
+ },
147
+ (table) => ({
148
+ namespaceContentHashUnique: uniqueIndex("interactions_namespace_content_hash_unique").on(
149
+ table.namespaceId,
150
+ table.contentHash
151
+ ),
152
+ namespaceSessionCreatedIdx: index("interactions_namespace_session_created_idx").on(
153
+ table.namespaceId,
154
+ table.sessionId,
155
+ table.createdAt
156
+ )
157
+ })
158
+ );
159
+ var sessionSummaries = pgTable(
160
+ "session_summaries",
161
+ {
162
+ id: text("id").primaryKey(),
163
+ namespaceId: text("namespace_id").notNull().references(() => namespaces.id, { onDelete: "cascade" }),
164
+ sessionId: text("session_id").notNull(),
165
+ content: text("content").notNull(),
166
+ version: integer("version").notNull().default(1),
167
+ tokenCount: integer("token_count").notNull().default(0),
168
+ createdByAgent: text("created_by_agent"),
169
+ /** See facts.fileBacked. */
170
+ fileBacked: boolean("file_backed").notNull().default(false),
171
+ createdAt
172
+ },
173
+ (table) => ({
174
+ namespaceSessionVersionIdx: index("session_summaries_namespace_session_version_idx").on(
175
+ table.namespaceId,
176
+ table.sessionId,
177
+ table.version
178
+ )
179
+ })
180
+ );
181
+ var facts = pgTable(
182
+ "facts",
183
+ {
184
+ id: text("id").primaryKey(),
185
+ namespaceId: text("namespace_id").notNull().references(() => namespaces.id, { onDelete: "cascade" }),
186
+ subject: text("subject").notNull(),
187
+ predicate: text("predicate").notNull(),
188
+ object: text("object").notNull(),
189
+ propositionKey: text("proposition_key"),
190
+ content: text("content").notNull(),
191
+ contentHash: text("content_hash").notNull(),
192
+ confidence: numeric("confidence", { precision: 5, scale: 4 }).notNull().default("1"),
193
+ /**
194
+ * Historical provenance only — deliberately not a foreign key. Interactions
195
+ * are ephemeral DB rows; Markdown facts must re-import after a rebuild that
196
+ * lost them, and the ID must survive so `sourceDeleted` can be tracked.
197
+ */
198
+ sourceInteractionId: text("source_interaction_id"),
199
+ sourceDeleted: boolean("source_deleted").notNull().default(false),
200
+ supersededBy: text("superseded_by"),
201
+ /**
202
+ * True once a reindex imported this row from its Markdown file. Only such
203
+ * rows are deleted when the file disappears; DB-only legacy rows are kept.
204
+ */
205
+ fileBacked: boolean("file_backed").notNull().default(false),
206
+ embeddingModel: text("embedding_model"),
207
+ createdByAgent: text("created_by_agent"),
208
+ createdAt,
209
+ updatedAt
210
+ },
211
+ (table) => ({
212
+ namespaceContentHashUnique: uniqueIndex("facts_namespace_content_hash_unique").on(
213
+ table.namespaceId,
214
+ table.contentHash
215
+ ),
216
+ namespaceSubjectIdx: index("facts_namespace_subject_idx").on(table.namespaceId, table.subject),
217
+ sourceInteractionIdx: index("facts_source_interaction_idx").on(table.sourceInteractionId)
218
+ })
219
+ );
220
+ var embeddings = pgTable(
221
+ "embeddings",
222
+ {
223
+ id: text("id").primaryKey(),
224
+ namespaceId: text("namespace_id").notNull().references(() => namespaces.id, { onDelete: "cascade" }),
225
+ ownerType: embeddingOwnerTypeEnum("owner_type").notNull(),
226
+ ownerId: text("owner_id").notNull(),
227
+ vector: vector("vector").notNull(),
228
+ embeddingModel: text("embedding_model").notNull(),
229
+ embeddingSpace: text("embedding_space"),
230
+ status: embeddingStatusEnum("status").notNull().default("pending"),
231
+ attempts: integer("attempts").notNull().default(0),
232
+ lastError: text("last_error"),
233
+ /** Lease deadline while status='processing'; null otherwise. */
234
+ leaseUntil: timestamp("lease_until", { withTimezone: true }),
235
+ leaseToken: text("lease_token"),
236
+ createdAt,
237
+ updatedAt
238
+ },
239
+ (table) => ({
240
+ ownerUnique: uniqueIndex("embeddings_owner_unique").on(table.ownerType, table.ownerId),
241
+ namespaceModelStatusIdx: index("embeddings_namespace_model_status_idx").on(
242
+ table.namespaceId,
243
+ table.embeddingModel,
244
+ table.status
245
+ ),
246
+ namespaceSpaceStatusIdx: index("embeddings_namespace_space_status_idx").on(
247
+ table.namespaceId,
248
+ table.embeddingSpace,
249
+ table.status
250
+ ),
251
+ vectorHnswIdx: index("embeddings_vector_hnsw_idx").using(
252
+ "hnsw",
253
+ sql`${table.vector} vector_cosine_ops`
254
+ )
255
+ })
256
+ );
257
+ var shares = pgTable(
258
+ "shares",
259
+ {
260
+ id: text("id").primaryKey(),
261
+ fromNamespaceId: text("from_namespace_id").notNull().references(() => namespaces.id, { onDelete: "cascade" }),
262
+ toNamespaceId: text("to_namespace_id").notNull().references(() => namespaces.id, { onDelete: "cascade" }),
263
+ sourceFactId: text("source_fact_id").notNull().references(() => facts.id, { onDelete: "cascade" }),
264
+ snapshotFactId: text("snapshot_fact_id").references(() => facts.id, { onDelete: "set null" }),
265
+ mode: shareModeEnum("mode").notNull().default("reference"),
266
+ tombstoned: boolean("tombstoned").notNull().default(false),
267
+ createdByAgent: text("created_by_agent"),
268
+ createdAt
269
+ },
270
+ (table) => ({
271
+ shareUnique: uniqueIndex("shares_target_source_unique").on(
272
+ table.toNamespaceId,
273
+ table.sourceFactId,
274
+ table.mode
275
+ ),
276
+ fromNamespaceIdx: index("shares_from_namespace_idx").on(table.fromNamespaceId)
277
+ })
278
+ );
279
+ var checkpoints = pgTable(
280
+ "checkpoints",
281
+ {
282
+ id: text("id").primaryKey(),
283
+ namespaceId: text("namespace_id").notNull().references(() => namespaces.id, { onDelete: "cascade" }),
284
+ threadId: text("thread_id").notNull(),
285
+ checkpointId: text("checkpoint_id").notNull(),
286
+ parentId: text("parent_id"),
287
+ channelValues: jsonb("channel_values").notNull().default(sql`'{}'::jsonb`),
288
+ metadata: jsonb("metadata").notNull().default(sql`'{}'::jsonb`),
289
+ createdAt
290
+ },
291
+ (table) => ({
292
+ checkpointUnique: uniqueIndex("checkpoints_thread_checkpoint_unique").on(
293
+ table.namespaceId,
294
+ table.threadId,
295
+ table.checkpointId
296
+ )
297
+ })
298
+ );
299
+ var modelCalls = pgTable(
300
+ "model_calls",
301
+ {
302
+ id: text("id").primaryKey(),
303
+ namespaceId: text("namespace_id").references(() => namespaces.id, { onDelete: "set null" }),
304
+ model: text("model").notNull(),
305
+ type: modelCallTypeEnum("type").notNull(),
306
+ latencyMs: integer("latency_ms"),
307
+ tokens: integer("tokens"),
308
+ status: modelCallStatusEnum("status").notNull(),
309
+ error: text("error"),
310
+ metadata: jsonb("metadata").notNull().default(sql`'{}'::jsonb`),
311
+ createdAt
312
+ },
313
+ (table) => ({
314
+ modelCreatedIdx: index("model_calls_model_created_idx").on(table.model, table.createdAt),
315
+ namespaceCreatedIdx: index("model_calls_namespace_created_idx").on(
316
+ table.namespaceId,
317
+ table.createdAt
318
+ )
319
+ })
320
+ );
321
+ var auditLog = pgTable(
322
+ "audit_log",
323
+ {
324
+ id: text("id").primaryKey(),
325
+ namespaceId: text("namespace_id").references(() => namespaces.id, { onDelete: "set null" }),
326
+ action: auditActionEnum("action").notNull(),
327
+ entityType: text("entity_type").notNull(),
328
+ entityId: text("entity_id"),
329
+ createdByAgent: text("created_by_agent"),
330
+ metadata: jsonb("metadata").notNull().default(sql`'{}'::jsonb`),
331
+ createdAt
332
+ },
333
+ (table) => ({
334
+ namespaceActionCreatedIdx: index("audit_log_namespace_action_created_idx").on(
335
+ table.namespaceId,
336
+ table.action,
337
+ table.createdAt
338
+ )
339
+ })
340
+ );
341
+
342
+ // src/db/bootstrap.ts
343
+ async function enablePgvector(sql3) {
344
+ await sql3`create extension if not exists vector`;
345
+ }
346
+ async function checkPgvector(sql3) {
347
+ const result = await sql3`
348
+ select extversion from pg_extension where extname = 'vector'
349
+ `;
350
+ return {
351
+ available: result.length > 0,
352
+ installedVersion: result[0]?.extversion ?? null
353
+ };
354
+ }
355
+ async function bootstrapDatabase(sql3) {
356
+ await enablePgvector(sql3);
357
+ return checkPgvector(sql3);
358
+ }
359
+
360
+ // src/db/client.ts
361
+ import { drizzle } from "drizzle-orm/postgres-js";
362
+ import postgres from "postgres";
363
+ function createDatabaseClient(config) {
364
+ const sql3 = postgres(config.postgresUrl, {
365
+ max: config.maxConnections ?? 10,
366
+ ...config.connectTimeoutSeconds !== void 0 ? { connect_timeout: config.connectTimeoutSeconds } : {},
367
+ // postgres.js logs NOTICE messages (e.g. "extension already exists,
368
+ // skipping") to stdout by default. Over the MCP stdio transport stdout
369
+ // carries JSON-RPC, so any notice corrupts the stream and the client
370
+ // reports "Unexpected token … is not valid JSON". Route notices to
371
+ // stderr instead, where logs belong.
372
+ onnotice: (notice) => {
373
+ process.stderr.write(`${JSON.stringify(notice)}
374
+ `);
375
+ }
376
+ });
377
+ return {
378
+ sql: sql3,
379
+ db: drizzle(sql3, { schema: schema_exports }),
380
+ async close() {
381
+ await sql3.end();
382
+ }
383
+ };
384
+ }
385
+ async function pingDatabase(client) {
386
+ const result = await client.sql`select 1 as ok`;
387
+ return result[0]?.ok === 1;
388
+ }
389
+
390
+ // src/db/migrator.ts
391
+ import { existsSync } from "fs";
392
+ import { dirname, resolve } from "path";
393
+ import { fileURLToPath } from "url";
394
+ import { migrate } from "drizzle-orm/postgres-js/migrator";
395
+ function resolveMigrationsFolder() {
396
+ const here = dirname(fileURLToPath(import.meta.url));
397
+ const candidates = [
398
+ // Co-located (src/db/ -> src/db/migrations)
399
+ resolve(here, "migrations"),
400
+ // Built dist (dist/<chunk>.js -> ../src/db/migrations relative paths)
401
+ resolve(here, "..", "src", "db", "migrations"),
402
+ resolve(here, "..", "..", "src", "db", "migrations"),
403
+ resolve(here, "..", "..", "..", "src", "db", "migrations"),
404
+ resolve(here, "..", "..", "..", "..", "src", "db", "migrations"),
405
+ // CWD fallback (developer running from repo root)
406
+ resolve(process.cwd(), "src", "db", "migrations")
407
+ ];
408
+ for (const candidate of candidates) {
409
+ if (existsSync(candidate)) return candidate;
410
+ }
411
+ throw new Error(
412
+ `Could not locate migrations folder. Tried:
413
+ ${candidates.join("\n ")}
414
+ If this is an npm-installed package, make sure src/db/migrations was included in the tarball.`
415
+ );
416
+ }
417
+ async function runMigrations(client) {
418
+ const migrationsFolder = resolveMigrationsFolder();
419
+ await migrate(client.db, { migrationsFolder });
420
+ }
421
+
422
+ // src/db/repository.ts
423
+ import { AsyncLocalStorage } from "async_hooks";
424
+ import { createHash } from "crypto";
425
+ import { and, desc, eq, inArray, isNull, lt, ne, or, sql as sql2 } from "drizzle-orm";
426
+
427
+ // src/core/memory-search.ts
428
+ function candidateLimit(limit) {
429
+ if (!Number.isFinite(limit)) return 256;
430
+ return Math.max(1, Math.min(1024, Math.trunc(limit)));
431
+ }
432
+ function validSearchVector(vector2) {
433
+ return vector2.length === VECTOR_DIMENSIONS && vector2.every((value) => Number.isFinite(Math.fround(value))) && vector2.some((value) => value !== 0);
434
+ }
435
+ var QUERY_STOPWORDS = /* @__PURE__ */ new Set([
436
+ "a",
437
+ "about",
438
+ "after",
439
+ "again",
440
+ "all",
441
+ "also",
442
+ "always",
443
+ "an",
444
+ "and",
445
+ "any",
446
+ "are",
447
+ "as",
448
+ "at",
449
+ "back",
450
+ "be",
451
+ "been",
452
+ "before",
453
+ "being",
454
+ "but",
455
+ "by",
456
+ "can",
457
+ "could",
458
+ "did",
459
+ "do",
460
+ "does",
461
+ "done",
462
+ "each",
463
+ "even",
464
+ "ever",
465
+ "for",
466
+ "from",
467
+ "get",
468
+ "gets",
469
+ "got",
470
+ "had",
471
+ "has",
472
+ "have",
473
+ "here",
474
+ "how",
475
+ "i",
476
+ "if",
477
+ "in",
478
+ "into",
479
+ "is",
480
+ "it",
481
+ "its",
482
+ "just",
483
+ "like",
484
+ "make",
485
+ "me",
486
+ "more",
487
+ "most",
488
+ "my",
489
+ "never",
490
+ "no",
491
+ "not",
492
+ "now",
493
+ "of",
494
+ "on",
495
+ "only",
496
+ "or",
497
+ "other",
498
+ "our",
499
+ "out",
500
+ "over",
501
+ "please",
502
+ "really",
503
+ "same",
504
+ "should",
505
+ "so",
506
+ "some",
507
+ "sometimes",
508
+ "still",
509
+ "such",
510
+ "than",
511
+ "that",
512
+ "the",
513
+ "their",
514
+ "them",
515
+ "then",
516
+ "there",
517
+ "these",
518
+ "they",
519
+ "this",
520
+ "those",
521
+ "through",
522
+ "to",
523
+ "too",
524
+ "under",
525
+ "up",
526
+ "us",
527
+ "very",
528
+ "was",
529
+ "we",
530
+ "were",
531
+ "what",
532
+ "when",
533
+ "where",
534
+ "which",
535
+ "while",
536
+ "who",
537
+ "why",
538
+ "will",
539
+ "with",
540
+ "would",
541
+ "yet",
542
+ "you",
543
+ "your"
544
+ ]);
545
+ function lexicalTerms(query) {
546
+ return [
547
+ ...new Set(
548
+ query.slice(0, 4096).toLowerCase().match(/[\p{L}\p{N}_]+/gu) ?? []
549
+ )
550
+ ].filter((term) => !QUERY_STOPWORDS.has(term)).slice(0, 32).map((term) => term.slice(0, 64));
551
+ }
552
+ var CLAIM_BOILERPLATE = /* @__PURE__ */ new Set([
553
+ "decided",
554
+ "decide",
555
+ "decision",
556
+ "made",
557
+ "use",
558
+ "uses",
559
+ "used",
560
+ "using",
561
+ "now",
562
+ "going",
563
+ "will"
564
+ ]);
565
+ function termOverlap(a, b) {
566
+ const terms = (text2) => new Set(
567
+ (text2.toLowerCase().match(/[\p{L}\p{N}_]+/gu) ?? []).filter(
568
+ (term) => !QUERY_STOPWORDS.has(term) && !CLAIM_BOILERPLATE.has(term)
569
+ )
570
+ );
571
+ const left = terms(a);
572
+ const right = terms(b);
573
+ if (left.size === 0 || right.size === 0) return 0;
574
+ let shared = 0;
575
+ for (const term of left) if (right.has(term)) shared += 1;
576
+ return shared / (left.size + right.size - shared);
577
+ }
578
+
579
+ // src/providers/embedding-space.ts
580
+ function embeddingSpace(response) {
581
+ const space = {
582
+ model: response.model,
583
+ revision: response.revision ?? "unversioned",
584
+ schema: response.schema ?? "provider-v1",
585
+ dimensions: response.dimensions
586
+ };
587
+ if (!space.model || !space.revision || !space.schema || !Number.isSafeInteger(space.dimensions) || space.dimensions <= 0) {
588
+ throw new Error("Invalid embedding space identity.");
589
+ }
590
+ return space;
591
+ }
592
+ function embeddingSpaceKey(space) {
593
+ return JSON.stringify([space.model, space.revision, space.schema, space.dimensions]);
594
+ }
595
+ function validEmbeddingVector(vector2, dimensions) {
596
+ return dimensions > 0 && vector2.length === dimensions && vector2.every(Number.isFinite);
597
+ }
598
+ function validateEmbeddingResponse(response, count) {
599
+ embeddingSpace(response);
600
+ if (response.vectors.length !== count || response.vectors.some((vector2) => !validEmbeddingVector(vector2, response.dimensions))) {
601
+ throw new Error("Provider returned invalid embedding dimensions, values, or batch size.");
602
+ }
603
+ }
604
+
605
+ // src/db/repository.ts
606
+ var PostgresMemoryRepository = class {
607
+ client;
608
+ sessionTransaction = new AsyncLocalStorage();
609
+ constructor(client) {
610
+ this.client = client;
611
+ }
612
+ get db() {
613
+ return this.sessionTransaction.getStore() ?? this.client.db;
614
+ }
615
+ async ensureNamespace(name) {
616
+ const existing = await this.getNamespaceByName(name);
617
+ if (existing) return existing;
618
+ const id = generateEntityId("ns");
619
+ await this.db.insert(namespaces).values({ id, name }).onConflictDoNothing({ target: namespaces.name });
620
+ const row = await this.getNamespaceByName(name);
621
+ if (!row) throw new Error(`Failed to ensure namespace ${name}.`);
622
+ return row;
623
+ }
624
+ async getNamespaceByName(name) {
625
+ const rows = await this.db.select({ id: namespaces.id, name: namespaces.name }).from(namespaces).where(eq(namespaces.name, name)).limit(1);
626
+ return rows[0] ?? null;
627
+ }
628
+ async withSessionLock(namespaceId, sessionId, fn) {
629
+ const [k1, k2] = advisoryLockKeys(namespaceId, sessionId);
630
+ return this.client.db.transaction(async (tx) => {
631
+ await tx.execute(sql2`select pg_advisory_xact_lock(${k1}, ${k2})`);
632
+ return this.sessionTransaction.run(tx, fn);
633
+ });
634
+ }
635
+ async writeInteraction(input) {
636
+ return this.db.transaction(async (tx) => {
637
+ const existing = await tx.select({ id: interactions.id, sessionId: interactions.sessionId }).from(interactions).where(
638
+ and(
639
+ eq(interactions.namespaceId, input.interaction.namespaceId),
640
+ eq(interactions.contentHash, input.interaction.contentHash)
641
+ )
642
+ ).limit(1);
643
+ const existingRow = existing[0];
644
+ if (existingRow) {
645
+ return {
646
+ id: existingRow.id,
647
+ embeddingId: "",
648
+ sessionId: existingRow.sessionId,
649
+ deduplicated: true
650
+ };
651
+ }
652
+ await tx.insert(interactions).values({
653
+ id: input.interaction.id,
654
+ namespaceId: input.interaction.namespaceId,
655
+ sessionId: input.interaction.sessionId,
656
+ content: input.interaction.content,
657
+ contentHash: input.interaction.contentHash,
658
+ tokenCount: input.interaction.tokenCount,
659
+ createdByAgent: input.interaction.createdByAgent ?? null
660
+ });
661
+ await tx.insert(embeddings).values({
662
+ id: input.embedding.id,
663
+ namespaceId: input.embedding.namespaceId,
664
+ ownerType: input.embedding.ownerType,
665
+ ownerId: input.embedding.ownerId,
666
+ vector: zeroVectorLiteral(),
667
+ embeddingModel: input.embedding.embeddingModel,
668
+ status: "pending"
669
+ });
670
+ await tx.insert(auditLog).values(toAuditRow(input.audit));
671
+ return {
672
+ id: input.interaction.id,
673
+ embeddingId: input.embedding.id,
674
+ sessionId: input.interaction.sessionId,
675
+ deduplicated: false
676
+ };
677
+ });
678
+ }
679
+ async writeFact(input) {
680
+ return this.db.transaction(async (tx) => {
681
+ const existing = await tx.select({ id: facts.id }).from(facts).where(
682
+ and(
683
+ eq(facts.namespaceId, input.fact.namespaceId),
684
+ eq(facts.contentHash, input.fact.contentHash)
685
+ )
686
+ ).limit(1);
687
+ const existingRow = existing[0];
688
+ if (existingRow) {
689
+ return {
690
+ id: existingRow.id,
691
+ embeddingId: "",
692
+ sessionId: "",
693
+ deduplicated: true
694
+ };
695
+ }
696
+ const sameId = await tx.select({ id: facts.id }).from(facts).where(eq(facts.id, input.fact.id)).limit(1);
697
+ if (sameId[0]) {
698
+ return { id: sameId[0].id, embeddingId: "", sessionId: "", deduplicated: true };
699
+ }
700
+ await tx.insert(facts).values({
701
+ id: input.fact.id,
702
+ namespaceId: input.fact.namespaceId,
703
+ subject: input.fact.subject,
704
+ predicate: input.fact.predicate,
705
+ object: input.fact.object,
706
+ content: input.fact.content,
707
+ contentHash: input.fact.contentHash,
708
+ propositionKey: input.fact.propositionKey ?? null,
709
+ confidence: String(input.fact.confidence ?? 1),
710
+ sourceDeleted: input.fact.sourceDeleted ?? false,
711
+ supersededBy: input.fact.supersededBy ?? null,
712
+ ...input.fact.createdAt ? { createdAt: input.fact.createdAt } : {},
713
+ sourceInteractionId: input.fact.sourceInteractionId ?? null,
714
+ createdByAgent: input.fact.createdByAgent ?? null
715
+ });
716
+ await tx.insert(embeddings).values({
717
+ id: input.embedding.id,
718
+ namespaceId: input.embedding.namespaceId,
719
+ ownerType: input.embedding.ownerType,
720
+ ownerId: input.embedding.ownerId,
721
+ vector: zeroVectorLiteral(),
722
+ embeddingModel: input.embedding.embeddingModel,
723
+ status: "pending"
724
+ });
725
+ await tx.insert(auditLog).values(toAuditRow(input.audit));
726
+ return {
727
+ id: input.fact.id,
728
+ embeddingId: input.embedding.id,
729
+ sessionId: "",
730
+ deduplicated: false
731
+ };
732
+ });
733
+ }
734
+ async writeSummary(input) {
735
+ return this.db.transaction(async (tx) => {
736
+ const existing = await tx.select({ id: sessionSummaries.id, sessionId: sessionSummaries.sessionId }).from(sessionSummaries).where(eq(sessionSummaries.id, input.summary.id)).limit(1);
737
+ const existingRow = existing[0];
738
+ if (existingRow) {
739
+ return {
740
+ id: existingRow.id,
741
+ embeddingId: "",
742
+ sessionId: existingRow.sessionId,
743
+ deduplicated: true
744
+ };
745
+ }
746
+ await tx.insert(sessionSummaries).values({
747
+ id: input.summary.id,
748
+ namespaceId: input.summary.namespaceId,
749
+ sessionId: input.summary.sessionId,
750
+ content: input.summary.content,
751
+ version: input.summary.version ?? 1,
752
+ tokenCount: input.summary.tokenCount,
753
+ createdByAgent: input.summary.createdByAgent ?? null
754
+ });
755
+ await tx.insert(embeddings).values({
756
+ id: input.embedding.id,
757
+ namespaceId: input.embedding.namespaceId,
758
+ ownerType: input.embedding.ownerType,
759
+ ownerId: input.embedding.ownerId,
760
+ vector: zeroVectorLiteral(),
761
+ embeddingModel: input.embedding.embeddingModel,
762
+ status: "pending"
763
+ });
764
+ await tx.insert(auditLog).values(toAuditRow(input.audit));
765
+ return {
766
+ id: input.summary.id,
767
+ embeddingId: input.embedding.id,
768
+ sessionId: input.summary.sessionId,
769
+ deduplicated: false
770
+ };
771
+ });
772
+ }
773
+ async upsertFactFromFile(input) {
774
+ const fact = input.fact;
775
+ return this.db.transaction(async (tx) => {
776
+ const [existing] = await tx.select().from(facts).where(eq(facts.id, fact.id)).for("update").limit(1);
777
+ if (existing && existing.namespaceId !== fact.namespaceId) {
778
+ throw new Error(`fact ${fact.id} already belongs to another namespace`);
779
+ }
780
+ const [duplicate] = await tx.select({ id: facts.id }).from(facts).where(
781
+ and(
782
+ eq(facts.namespaceId, fact.namespaceId),
783
+ eq(facts.contentHash, fact.contentHash),
784
+ ne(facts.id, fact.id)
785
+ )
786
+ ).limit(1);
787
+ if (duplicate) throw new Error(`content duplicates fact ${duplicate.id}`);
788
+ const values = {
789
+ subject: fact.subject,
790
+ predicate: fact.predicate,
791
+ object: fact.object,
792
+ content: fact.content,
793
+ contentHash: fact.contentHash,
794
+ confidence: String(fact.confidence),
795
+ sourceInteractionId: fact.sourceInteractionId ?? null,
796
+ sourceDeleted: fact.sourceDeleted,
797
+ supersededBy: fact.supersededBy,
798
+ propositionKey: fact.propositionKey ?? null,
799
+ createdByAgent: fact.createdByAgent ?? null,
800
+ createdAt: fact.createdAt,
801
+ fileBacked: true
802
+ };
803
+ if (!existing) {
804
+ await tx.insert(facts).values({ id: fact.id, namespaceId: fact.namespaceId, ...values });
805
+ await upsertPendingEmbedding(tx, input.embedding, true);
806
+ await tx.insert(auditLog).values(toAuditRow(input.audit));
807
+ return { status: "inserted" };
808
+ }
809
+ const contentChanged = existing.content !== fact.content;
810
+ const metadataChanged = existing.subject !== values.subject || existing.predicate !== values.predicate || existing.object !== values.object || Number(existing.confidence) !== Number(fact.confidence.toFixed(4)) || existing.sourceInteractionId !== values.sourceInteractionId || existing.sourceDeleted !== values.sourceDeleted || existing.supersededBy !== values.supersededBy || existing.propositionKey !== values.propositionKey || existing.createdByAgent !== values.createdByAgent || existing.createdAt.getTime() !== values.createdAt.getTime();
811
+ if (contentChanged || metadataChanged || !existing.fileBacked) {
812
+ await tx.update(facts).set({ ...values, updatedAt: sql2`now()` }).where(and(eq(facts.namespaceId, fact.namespaceId), eq(facts.id, fact.id)));
813
+ }
814
+ await upsertPendingEmbedding(tx, input.embedding, contentChanged);
815
+ if (!contentChanged && !metadataChanged) return { status: "unchanged" };
816
+ await tx.insert(auditLog).values(toAuditRow(input.audit));
817
+ return { status: "updated" };
818
+ });
819
+ }
820
+ async upsertSummaryFromFile(input) {
821
+ const summary = input.summary;
822
+ return this.db.transaction(async (tx) => {
823
+ const [existing] = await tx.select().from(sessionSummaries).where(eq(sessionSummaries.id, summary.id)).for("update").limit(1);
824
+ if (existing && existing.namespaceId !== summary.namespaceId) {
825
+ throw new Error(`summary ${summary.id} already belongs to another namespace`);
826
+ }
827
+ const values = {
828
+ sessionId: summary.sessionId,
829
+ content: summary.content,
830
+ version: summary.version ?? 1,
831
+ tokenCount: summary.tokenCount,
832
+ createdByAgent: summary.createdByAgent ?? null,
833
+ createdAt: summary.createdAt,
834
+ fileBacked: true
835
+ };
836
+ if (!existing) {
837
+ await tx.insert(sessionSummaries).values({
838
+ id: summary.id,
839
+ namespaceId: summary.namespaceId,
840
+ ...values
841
+ });
842
+ await upsertPendingEmbedding(tx, input.embedding, true);
843
+ await tx.insert(auditLog).values(toAuditRow(input.audit));
844
+ return { status: "inserted" };
845
+ }
846
+ const contentChanged = existing.content !== summary.content;
847
+ const metadataChanged = existing.sessionId !== values.sessionId || existing.version !== values.version || existing.tokenCount !== values.tokenCount || existing.createdByAgent !== values.createdByAgent || existing.createdAt.getTime() !== values.createdAt.getTime();
848
+ if (contentChanged || metadataChanged || !existing.fileBacked) {
849
+ await tx.update(sessionSummaries).set(values).where(
850
+ and(
851
+ eq(sessionSummaries.namespaceId, summary.namespaceId),
852
+ eq(sessionSummaries.id, summary.id)
853
+ )
854
+ );
855
+ }
856
+ await upsertPendingEmbedding(tx, input.embedding, contentChanged);
857
+ if (!contentChanged && !metadataChanged) return { status: "unchanged" };
858
+ await tx.insert(auditLog).values(toAuditRow(input.audit));
859
+ return { status: "updated" };
860
+ });
861
+ }
862
+ async listFileBackedIds(namespaceId) {
863
+ const [factRows, summaryRows] = await Promise.all([
864
+ this.db.select({ id: facts.id }).from(facts).where(and(eq(facts.namespaceId, namespaceId), eq(facts.fileBacked, true))),
865
+ this.db.select({ id: sessionSummaries.id }).from(sessionSummaries).where(
866
+ and(eq(sessionSummaries.namespaceId, namespaceId), eq(sessionSummaries.fileBacked, true))
867
+ )
868
+ ]);
869
+ return { facts: factRows.map((row) => row.id), summaries: summaryRows.map((row) => row.id) };
870
+ }
871
+ /**
872
+ * Claim up to `limit` embeddable jobs.
873
+ *
874
+ * A claim is a LEASE, not just a row lock: claimed rows move to
875
+ * status='processing' with a `lease_until` deadline, so protection survives
876
+ * the committing transaction. Concurrent workers (e.g. Claude Desktop and
877
+ * Codex both running `codebuddy serve`) therefore never process the same
878
+ * item twice. Expired leases are re-claimable, so a crashed worker's work is
879
+ * picked up again instead of being stuck forever.
880
+ */
881
+ async claimPendingEmbeddings(limit) {
882
+ const rows = await this.client.sql`
883
+ with claimed as (
884
+ select id from embeddings
885
+ where status = 'pending'
886
+ or (status = 'processing' and (lease_until is null or lease_until < now()))
887
+ order by created_at asc
888
+ for update skip locked
889
+ limit ${limit}
890
+ )
891
+ update embeddings e
892
+ set status = 'processing',
893
+ lease_until = now() + interval '120 seconds',
894
+ lease_token = ${generateEntityId("lease")},
895
+ updated_at = now()
896
+ from claimed
897
+ where e.id = claimed.id
898
+ returning
899
+ e.id,
900
+ e.lease_token,
901
+ e.namespace_id,
902
+ e.owner_type,
903
+ e.owner_id,
904
+ e.embedding_model,
905
+ e.attempts,
906
+ coalesce(
907
+ (select content from interactions where id = e.owner_id),
908
+ (select content from facts where id = e.owner_id),
909
+ (select content from session_summaries where id = e.owner_id)
910
+ ) as content
911
+ `;
912
+ return rows.map((row) => ({
913
+ id: row.id,
914
+ leaseToken: row.lease_token,
915
+ leaseMs: 12e4,
916
+ namespaceId: row.namespace_id,
917
+ ownerType: row.owner_type,
918
+ ownerId: row.owner_id,
919
+ embeddingModel: row.embedding_model,
920
+ attempts: row.attempts,
921
+ content: row.content ?? ""
922
+ }));
923
+ }
924
+ async renewEmbeddingLease(id, leaseToken) {
925
+ const rows = await this.db.update(embeddings).set({ leaseUntil: sql2`now() + interval '120 seconds'` }).where(
926
+ and(
927
+ eq(embeddings.id, id),
928
+ eq(embeddings.leaseToken, leaseToken),
929
+ eq(embeddings.status, "processing"),
930
+ sql2`${embeddings.leaseUntil} > now()`
931
+ )
932
+ ).returning({ id: embeddings.id });
933
+ return rows.length > 0;
934
+ }
935
+ async markEmbeddingReady(id, leaseToken, vector2, model, space) {
936
+ const literal = `[${vector2.join(",")}]`;
937
+ const rows = await this.db.update(embeddings).set({
938
+ status: "ready",
939
+ vector: literal,
940
+ embeddingModel: model,
941
+ embeddingSpace: space ?? embeddingSpaceKey(embeddingSpace({ model, dimensions: vector2.length })),
942
+ lastError: null,
943
+ leaseUntil: null,
944
+ leaseToken: null,
945
+ updatedAt: sql2`now()`
946
+ }).where(
947
+ and(
948
+ eq(embeddings.id, id),
949
+ eq(embeddings.leaseToken, leaseToken),
950
+ eq(embeddings.status, "processing"),
951
+ sql2`${embeddings.leaseUntil} > now()`
952
+ )
953
+ ).returning({ id: embeddings.id });
954
+ return rows.length > 0;
955
+ }
956
+ /**
957
+ * Record a failure. Below `maxAttempts`, the row goes back to
958
+ * 'processing' with an exponentially growing lease (2s · 2^(attempts-1),
959
+ * capped at 60s) that acts as a retry backoff timer — a rate-limited
960
+ * endpoint is no longer hammered every poll tick. At `maxAttempts` the job
961
+ * is parked as 'failed'.
962
+ */
963
+ async markEmbeddingFailed(id, leaseToken, error, attempts, options) {
964
+ const maxAttempts = options?.maxAttempts ?? 5;
965
+ const backoffSeconds = Math.min(60, 2 ** Math.max(0, attempts - 1));
966
+ const rows = await this.db.update(embeddings).set({
967
+ status: sql2`case when ${attempts} >= ${maxAttempts} then 'failed'::embedding_status else 'processing'::embedding_status end`,
968
+ leaseUntil: sql2`case when ${attempts} >= ${maxAttempts} then null else now() + (${backoffSeconds} || ' seconds')::interval end`,
969
+ leaseToken: null,
970
+ attempts,
971
+ lastError: error,
972
+ updatedAt: sql2`now()`
973
+ }).where(
974
+ and(
975
+ eq(embeddings.id, id),
976
+ eq(embeddings.leaseToken, leaseToken),
977
+ eq(embeddings.status, "processing"),
978
+ sql2`${embeddings.leaseUntil} > now()`
979
+ )
980
+ ).returning({ id: embeddings.id });
981
+ return rows.length > 0;
982
+ }
983
+ async getEmbeddingStatus(id) {
984
+ const rows = await this.db.select({
985
+ id: embeddings.id,
986
+ status: embeddings.status,
987
+ attempts: embeddings.attempts,
988
+ lastError: embeddings.lastError
989
+ }).from(embeddings).where(eq(embeddings.id, id)).limit(1);
990
+ const row = rows[0];
991
+ if (!row) return null;
992
+ return {
993
+ id: row.id,
994
+ status: row.status,
995
+ attempts: row.attempts,
996
+ lastError: row.lastError
997
+ };
998
+ }
999
+ async shareReference(input) {
1000
+ await this.db.insert(shares).values({
1001
+ id: input.id,
1002
+ fromNamespaceId: input.fromNamespaceId,
1003
+ toNamespaceId: input.toNamespaceId,
1004
+ sourceFactId: input.sourceFactId,
1005
+ mode: "reference",
1006
+ createdByAgent: input.createdByAgent ?? null
1007
+ }).onConflictDoUpdate({
1008
+ target: [shares.toNamespaceId, shares.sourceFactId, shares.mode],
1009
+ set: { tombstoned: false }
1010
+ });
1011
+ }
1012
+ async revokeShares(fromNamespaceId, toNamespaceId, factIds) {
1013
+ if (factIds.length === 0) return 0;
1014
+ const revoked = await this.db.update(shares).set({ tombstoned: true }).where(
1015
+ and(
1016
+ eq(shares.fromNamespaceId, fromNamespaceId),
1017
+ eq(shares.toNamespaceId, toNamespaceId),
1018
+ eq(shares.mode, "reference"),
1019
+ eq(shares.tombstoned, false),
1020
+ inArray(shares.sourceFactId, factIds)
1021
+ )
1022
+ ).returning({ id: shares.id });
1023
+ return revoked.length;
1024
+ }
1025
+ async shareSnapshot(input) {
1026
+ return this.db.transaction(async (tx) => {
1027
+ const prior = await tx.select({ snapshotFactId: shares.snapshotFactId }).from(shares).where(
1028
+ and(
1029
+ eq(shares.toNamespaceId, input.share.toNamespaceId),
1030
+ eq(shares.sourceFactId, input.share.sourceFactId),
1031
+ eq(shares.mode, "snapshot")
1032
+ )
1033
+ ).limit(1);
1034
+ const priorId = prior[0]?.snapshotFactId;
1035
+ if (priorId) return { snapshotFactId: priorId };
1036
+ const copy = await tx.select({ id: facts.id }).from(facts).where(
1037
+ and(
1038
+ eq(facts.namespaceId, input.snapshotFact.namespaceId),
1039
+ eq(facts.contentHash, input.snapshotFact.contentHash)
1040
+ )
1041
+ ).limit(1);
1042
+ const snapshotFactId = copy[0]?.id ?? input.snapshotFact.id;
1043
+ if (!copy[0]) {
1044
+ await tx.insert(facts).values({
1045
+ id: input.snapshotFact.id,
1046
+ namespaceId: input.snapshotFact.namespaceId,
1047
+ subject: input.snapshotFact.subject,
1048
+ predicate: input.snapshotFact.predicate,
1049
+ object: input.snapshotFact.object,
1050
+ content: input.snapshotFact.content,
1051
+ contentHash: input.snapshotFact.contentHash,
1052
+ sourceInteractionId: input.snapshotFact.sourceInteractionId ?? null,
1053
+ createdByAgent: input.snapshotFact.createdByAgent ?? null
1054
+ });
1055
+ await tx.insert(embeddings).values({
1056
+ id: input.embedding.id,
1057
+ namespaceId: input.embedding.namespaceId,
1058
+ ownerType: input.embedding.ownerType,
1059
+ ownerId: input.embedding.ownerId,
1060
+ vector: zeroVectorLiteral(),
1061
+ embeddingModel: input.embedding.embeddingModel,
1062
+ status: "pending"
1063
+ });
1064
+ }
1065
+ await tx.insert(shares).values({
1066
+ id: input.share.id,
1067
+ fromNamespaceId: input.share.fromNamespaceId,
1068
+ toNamespaceId: input.share.toNamespaceId,
1069
+ sourceFactId: input.share.sourceFactId,
1070
+ snapshotFactId,
1071
+ mode: "snapshot",
1072
+ createdByAgent: input.share.createdByAgent ?? null
1073
+ }).onConflictDoUpdate({
1074
+ target: [shares.toNamespaceId, shares.sourceFactId, shares.mode],
1075
+ set: { snapshotFactId }
1076
+ });
1077
+ return { snapshotFactId };
1078
+ });
1079
+ }
1080
+ async forgetInteraction(namespaceId, id, audit) {
1081
+ return this.db.transaction(async (tx) => {
1082
+ const existing = await tx.select({ id: interactions.id }).from(interactions).where(and(eq(interactions.namespaceId, namespaceId), eq(interactions.id, id))).for("update");
1083
+ if (existing.length === 0) return { found: false, sourceDeletedFactIds: [] };
1084
+ const marked = await tx.update(facts).set({ sourceDeleted: true }).where(and(eq(facts.namespaceId, namespaceId), eq(facts.sourceInteractionId, id))).returning({ id: facts.id });
1085
+ await tx.delete(interactions).where(and(eq(interactions.namespaceId, namespaceId), eq(interactions.id, id)));
1086
+ await tx.delete(embeddings).where(
1087
+ and(
1088
+ eq(embeddings.namespaceId, namespaceId),
1089
+ eq(embeddings.ownerType, "interaction"),
1090
+ eq(embeddings.ownerId, id)
1091
+ )
1092
+ );
1093
+ await tx.insert(auditLog).values(toAuditRow(audit));
1094
+ return { found: true, sourceDeletedFactIds: marked.map((row) => row.id) };
1095
+ });
1096
+ }
1097
+ async forgetFact(namespaceId, id, audit) {
1098
+ return this.db.transaction(async (tx) => {
1099
+ const deleted = await tx.delete(facts).where(and(eq(facts.namespaceId, namespaceId), eq(facts.id, id))).returning({ id: facts.id });
1100
+ if (deleted.length === 0) return { found: false };
1101
+ await tx.delete(embeddings).where(
1102
+ and(
1103
+ eq(embeddings.namespaceId, namespaceId),
1104
+ eq(embeddings.ownerType, "fact"),
1105
+ eq(embeddings.ownerId, id)
1106
+ )
1107
+ );
1108
+ await tx.update(shares).set({ tombstoned: true }).where(
1109
+ and(
1110
+ eq(shares.fromNamespaceId, namespaceId),
1111
+ eq(shares.sourceFactId, id),
1112
+ eq(shares.mode, "reference")
1113
+ )
1114
+ );
1115
+ await tx.insert(auditLog).values(toAuditRow(audit));
1116
+ return { found: true };
1117
+ });
1118
+ }
1119
+ async forgetSummary(namespaceId, id, audit) {
1120
+ return this.db.transaction(async (tx) => {
1121
+ const deleted = await tx.delete(sessionSummaries).where(and(eq(sessionSummaries.namespaceId, namespaceId), eq(sessionSummaries.id, id))).returning({ id: sessionSummaries.id });
1122
+ if (deleted.length === 0) return { found: false };
1123
+ await tx.delete(embeddings).where(
1124
+ and(
1125
+ eq(embeddings.namespaceId, namespaceId),
1126
+ eq(embeddings.ownerType, "summary"),
1127
+ eq(embeddings.ownerId, id)
1128
+ )
1129
+ );
1130
+ await tx.insert(auditLog).values(toAuditRow(audit));
1131
+ return { found: true };
1132
+ });
1133
+ }
1134
+ async recordModelCall(entry) {
1135
+ await this.db.insert(modelCalls).values({
1136
+ id: entry.id,
1137
+ namespaceId: entry.namespaceId ?? null,
1138
+ model: entry.metadata.model,
1139
+ type: entry.metadata.type,
1140
+ latencyMs: entry.metadata.latencyMs,
1141
+ tokens: entry.metadata.tokens ?? null,
1142
+ status: entry.metadata.status,
1143
+ error: entry.metadata.error ?? null,
1144
+ metadata: {
1145
+ retries: entry.metadata.retries,
1146
+ fallbackTriggered: entry.metadata.fallbackTriggered,
1147
+ coldStartWaitMs: entry.metadata.coldStartWaitMs
1148
+ }
1149
+ });
1150
+ }
1151
+ async recordAudit(entry) {
1152
+ await this.db.insert(auditLog).values(toAuditRow(entry));
1153
+ }
1154
+ async getReadyEmbeddings(namespaceId, limit, space) {
1155
+ const rows = await this.client.sql`
1156
+ select e.id, e.owner_type, e.owner_id, e.embedding_model, e.embedding_space,
1157
+ e.vector::text as vector
1158
+ from embeddings e
1159
+ where e.status = 'ready'
1160
+ and (${space ?? null}::text is null or e.embedding_space = ${space ?? null})
1161
+ and (
1162
+ e.namespace_id = ${namespaceId}
1163
+ or (
1164
+ e.owner_type = 'fact'
1165
+ and exists (
1166
+ select 1 from shares s
1167
+ where s.to_namespace_id = ${namespaceId}
1168
+ and s.from_namespace_id = e.namespace_id
1169
+ and s.source_fact_id = e.owner_id
1170
+ and s.mode = 'reference'
1171
+ and not s.tombstoned
1172
+ )
1173
+ )
1174
+ )
1175
+ order by e.updated_at desc
1176
+ limit ${limit}
1177
+ `;
1178
+ return rows.map((row) => ({
1179
+ embeddingId: row.id,
1180
+ ownerType: row.owner_type,
1181
+ ownerId: row.owner_id,
1182
+ embeddingModel: row.embedding_model,
1183
+ embeddingSpace: row.embedding_space,
1184
+ vector: parseVectorLiteral(row.vector)
1185
+ }));
1186
+ }
1187
+ async requeueEmbeddingsOutsideSpace(namespaceId, space) {
1188
+ const rows = await this.db.update(embeddings).set({
1189
+ status: "pending",
1190
+ vector: zeroVectorLiteral(),
1191
+ attempts: 0,
1192
+ lastError: null,
1193
+ leaseUntil: null,
1194
+ leaseToken: null,
1195
+ updatedAt: sql2`now()`
1196
+ }).where(
1197
+ and(
1198
+ eq(embeddings.namespaceId, namespaceId),
1199
+ inArray(embeddings.status, ["ready", "failed"]),
1200
+ sql2`${embeddings.embeddingSpace} is distinct from ${space}`
1201
+ )
1202
+ ).returning({ id: embeddings.id });
1203
+ return rows.length;
1204
+ }
1205
+ async searchSimilarEmbeddings(input) {
1206
+ if (!validSearchVector(input.vector))
1207
+ return { method: "exact_pgvector", candidates: [], compatibleReady: 0 };
1208
+ const rows = await this.db.execute(sql2`
1209
+ with compatible as materialized (
1210
+ select id, owner_type, owner_id, embedding_model, embedding_space, vector
1211
+ from embeddings
1212
+ where status = 'ready'
1213
+ and (
1214
+ namespace_id = ${input.namespaceId}
1215
+ or (
1216
+ owner_type = 'fact'
1217
+ and ${liveReference(input.namespaceId, sql2`embeddings.namespace_id`, sql2`embeddings.owner_id`)}
1218
+ )
1219
+ )
1220
+ and embedding_space = ${input.embeddingSpace}
1221
+ and embedding_model = ${input.embeddingModel}
1222
+ and vector_norm(vector) > 0
1223
+ and not exists (
1224
+ select 1 from facts f
1225
+ where owner_type = 'fact' and f.namespace_id = embeddings.namespace_id
1226
+ and f.id = embeddings.owner_id
1227
+ and (f.source_deleted or f.superseded_by is not null)
1228
+ )
1229
+ )
1230
+ select id, owner_type, owner_id, embedding_model, embedding_space,
1231
+ 1 - (vector <=> ${JSON.stringify(input.vector)}::vector) as score,
1232
+ count(*) over () as total
1233
+ from compatible
1234
+ order by vector <=> ${JSON.stringify(input.vector)}::vector, id collate "C"
1235
+ limit ${candidateLimit(input.limit)}
1236
+ `);
1237
+ return {
1238
+ method: "exact_pgvector",
1239
+ compatibleReady: Number(rows[0]?.total ?? 0),
1240
+ candidates: rows.map((row) => ({
1241
+ embeddingId: row.id,
1242
+ ownerType: row.owner_type,
1243
+ ownerId: row.owner_id,
1244
+ embeddingModel: row.embedding_model,
1245
+ embeddingSpace: row.embedding_space,
1246
+ score: Number(row.score)
1247
+ }))
1248
+ };
1249
+ }
1250
+ async searchFactsLexically(namespaceId, query, limit) {
1251
+ const terms = lexicalTerms(query);
1252
+ if (!terms.length) return { candidates: [], matchedFacts: 0 };
1253
+ const rows = await this.db.execute(sql2`
1254
+ with scored as materialized (
1255
+ select id, content, subject, predicate, object, proposition_key, confidence,
1256
+ created_by_agent, created_at,
1257
+ (select coalesce(sum(
1258
+ (case when strpos(' ' || regexp_replace(lower(subject || ' ' || object), '[^[:alnum:]_]+', ' ', 'g') || ' ', ' ' || term || ' ') > 0 then 2 else 0 end) +
1259
+ (case when strpos(' ' || regexp_replace(lower(content), '[^[:alnum:]_]+', ' ', 'g') || ' ', ' ' || term || ' ') > 0 then 1 else 0 end)
1260
+ ), 0)::float / ${3 * terms.length} from
1261
+ jsonb_array_elements_text(${JSON.stringify(terms)}::jsonb) term
1262
+ ) as score
1263
+ from facts
1264
+ where not source_deleted and superseded_by is null
1265
+ and (
1266
+ namespace_id = ${namespaceId}
1267
+ or ${liveReference(namespaceId, sql2`facts.namespace_id`, sql2`facts.id`)}
1268
+ )
1269
+ )
1270
+ select *, count(*) over () as total from scored where score > 0
1271
+ order by score desc, created_at desc, id collate "C"
1272
+ limit ${candidateLimit(limit)}
1273
+ `);
1274
+ return {
1275
+ matchedFacts: Number(rows[0]?.total ?? 0),
1276
+ candidates: rows.map((row) => ({
1277
+ id: row.id,
1278
+ content: row.content,
1279
+ subject: row.subject,
1280
+ predicate: row.predicate,
1281
+ object: row.object,
1282
+ propositionKey: row.proposition_key,
1283
+ confidence: Number(row.confidence),
1284
+ sourceDeleted: false,
1285
+ supersededBy: null,
1286
+ createdByAgent: row.created_by_agent,
1287
+ createdAt: new Date(row.created_at),
1288
+ score: Number(row.score)
1289
+ }))
1290
+ };
1291
+ }
1292
+ async getRecentInteractions(namespaceId, sessionId, limit) {
1293
+ const rows = await this.db.select({
1294
+ id: interactions.id,
1295
+ sessionId: interactions.sessionId,
1296
+ content: interactions.content,
1297
+ tokenCount: interactions.tokenCount,
1298
+ createdAt: interactions.createdAt
1299
+ }).from(interactions).where(and(eq(interactions.namespaceId, namespaceId), eq(interactions.sessionId, sessionId))).orderBy(desc(interactions.createdAt)).limit(limit);
1300
+ return rows.map((row) => ({ ...row, createdAt: row.createdAt }));
1301
+ }
1302
+ async getRecentFacts(namespaceId, limit) {
1303
+ const rows = await this.db.select(recentFactColumns).from(facts).where(
1304
+ and(
1305
+ eq(facts.namespaceId, namespaceId),
1306
+ eq(facts.sourceDeleted, false),
1307
+ isNull(facts.supersededBy)
1308
+ )
1309
+ ).orderBy(desc(facts.createdAt)).limit(limit);
1310
+ return rows.map((row) => ({ ...row, confidence: Number(row.confidence ?? 1) }));
1311
+ }
1312
+ async getLatestSummary(namespaceId, sessionId) {
1313
+ const rows = await this.db.select({
1314
+ id: sessionSummaries.id,
1315
+ sessionId: sessionSummaries.sessionId,
1316
+ content: sessionSummaries.content,
1317
+ tokenCount: sessionSummaries.tokenCount,
1318
+ createdAt: sessionSummaries.createdAt
1319
+ }).from(sessionSummaries).where(
1320
+ and(
1321
+ eq(sessionSummaries.namespaceId, namespaceId),
1322
+ eq(sessionSummaries.sessionId, sessionId)
1323
+ )
1324
+ ).orderBy(desc(sessionSummaries.version)).limit(1);
1325
+ return rows[0] ?? null;
1326
+ }
1327
+ async getInteractionsByIds(namespaceId, ids) {
1328
+ if (ids.length === 0) return [];
1329
+ const rows = await this.db.select({
1330
+ id: interactions.id,
1331
+ sessionId: interactions.sessionId,
1332
+ content: interactions.content,
1333
+ tokenCount: interactions.tokenCount,
1334
+ createdAt: interactions.createdAt
1335
+ }).from(interactions).where(and(eq(interactions.namespaceId, namespaceId), inArray(interactions.id, ids)));
1336
+ return rows;
1337
+ }
1338
+ async getFactsByIds(namespaceId, ids) {
1339
+ if (ids.length === 0) return [];
1340
+ const rows = await this.db.select(recentFactColumns).from(facts).where(
1341
+ and(
1342
+ inArray(facts.id, ids),
1343
+ isNull(facts.supersededBy),
1344
+ or(
1345
+ eq(facts.namespaceId, namespaceId),
1346
+ liveReference(namespaceId, sql2`${facts.namespaceId}`, sql2`${facts.id}`)
1347
+ )
1348
+ )
1349
+ );
1350
+ return rows.map((row) => ({ ...row, confidence: Number(row.confidence ?? 1) }));
1351
+ }
1352
+ async getSummariesByIds(namespaceId, ids) {
1353
+ if (ids.length === 0) return [];
1354
+ const rows = await this.db.select({
1355
+ id: sessionSummaries.id,
1356
+ sessionId: sessionSummaries.sessionId,
1357
+ content: sessionSummaries.content,
1358
+ tokenCount: sessionSummaries.tokenCount,
1359
+ createdAt: sessionSummaries.createdAt
1360
+ }).from(sessionSummaries).where(and(eq(sessionSummaries.namespaceId, namespaceId), inArray(sessionSummaries.id, ids)));
1361
+ return rows;
1362
+ }
1363
+ async getFactById(namespaceId, id) {
1364
+ const rows = await this.db.select({
1365
+ id: facts.id,
1366
+ subject: facts.subject,
1367
+ predicate: facts.predicate,
1368
+ object: facts.object,
1369
+ content: facts.content,
1370
+ confidence: facts.confidence,
1371
+ sourceInteractionId: facts.sourceInteractionId,
1372
+ sourceDeleted: facts.sourceDeleted,
1373
+ supersededBy: facts.supersededBy,
1374
+ createdByAgent: facts.createdByAgent,
1375
+ createdAt: facts.createdAt
1376
+ }).from(facts).where(and(eq(facts.namespaceId, namespaceId), eq(facts.id, id))).limit(1);
1377
+ const row = rows[0];
1378
+ if (!row) return null;
1379
+ return { ...row, confidence: Number(row.confidence ?? 1) };
1380
+ }
1381
+ async countEmbeddingStatuses(namespaceId) {
1382
+ const rows = await this.client.sql`
1383
+ select status::text as status, count(*)::text as count
1384
+ from embeddings
1385
+ where namespace_id = ${namespaceId}
1386
+ group by status
1387
+ `;
1388
+ let pending = 0;
1389
+ let processing = 0;
1390
+ let failed = 0;
1391
+ let ready = 0;
1392
+ for (const row of rows) {
1393
+ const value = Number(row.count);
1394
+ if (row.status === "pending") pending = value;
1395
+ else if (row.status === "processing") processing = value;
1396
+ else if (row.status === "failed") failed = value;
1397
+ else if (row.status === "ready") ready = value;
1398
+ }
1399
+ return { pending, processing, failed, ready };
1400
+ }
1401
+ async listFacts(input) {
1402
+ const where = [
1403
+ eq(facts.namespaceId, input.namespaceId),
1404
+ eq(facts.sourceDeleted, false),
1405
+ isNull(facts.supersededBy),
1406
+ input.subject ? eq(facts.subject, input.subject) : void 0,
1407
+ input.cursor ? or(
1408
+ lt(facts.createdAt, input.cursor.createdAt),
1409
+ and(eq(facts.createdAt, input.cursor.createdAt), lt(facts.id, input.cursor.id))
1410
+ ) : void 0
1411
+ ].filter((clause) => Boolean(clause));
1412
+ const rows = await this.db.select({
1413
+ id: facts.id,
1414
+ subject: facts.subject,
1415
+ predicate: facts.predicate,
1416
+ object: facts.object,
1417
+ content: facts.content,
1418
+ confidence: facts.confidence,
1419
+ sourceInteractionId: facts.sourceInteractionId,
1420
+ sourceDeleted: facts.sourceDeleted,
1421
+ supersededBy: facts.supersededBy,
1422
+ createdByAgent: facts.createdByAgent,
1423
+ createdAt: facts.createdAt
1424
+ }).from(facts).where(and(...where)).orderBy(desc(facts.createdAt), desc(facts.id)).limit(input.limit);
1425
+ return rows.map((row) => ({ ...row, confidence: Number(row.confidence ?? 1) }));
1426
+ }
1427
+ async listSummaries(input) {
1428
+ return this.db.select({
1429
+ id: sessionSummaries.id,
1430
+ sessionId: sessionSummaries.sessionId,
1431
+ content: sessionSummaries.content,
1432
+ version: sessionSummaries.version,
1433
+ tokenCount: sessionSummaries.tokenCount,
1434
+ createdByAgent: sessionSummaries.createdByAgent,
1435
+ createdAt: sessionSummaries.createdAt
1436
+ }).from(sessionSummaries).where(eq(sessionSummaries.namespaceId, input.namespaceId)).orderBy(desc(sessionSummaries.createdAt)).limit(input.limit);
1437
+ }
1438
+ async listNamespaces() {
1439
+ const rows = await this.client.sql`
1440
+ select
1441
+ n.name,
1442
+ count(f.id)::text as fact_count,
1443
+ greatest(
1444
+ max(f.created_at),
1445
+ max(i.created_at),
1446
+ max(s.created_at),
1447
+ n.updated_at
1448
+ ) as last_activity
1449
+ from namespaces n
1450
+ left join facts f on f.namespace_id = n.id and f.source_deleted = false
1451
+ left join interactions i on i.namespace_id = n.id
1452
+ left join session_summaries s on s.namespace_id = n.id
1453
+ group by n.id, n.name, n.updated_at
1454
+ order by last_activity desc nulls last, n.name asc
1455
+ `;
1456
+ return rows.map((row) => ({
1457
+ name: row.name,
1458
+ factCount: Number(row.fact_count),
1459
+ lastActivity: row.last_activity
1460
+ }));
1461
+ }
1462
+ async getAgentWriteBreakdown(namespaceId) {
1463
+ const rows = namespaceId ? await this.client.sql`
1464
+ select agent_id, sum(writes)::text as writes
1465
+ from (
1466
+ select created_by_agent as agent_id, count(*) as writes from interactions where namespace_id = ${namespaceId} group by created_by_agent
1467
+ union all
1468
+ select created_by_agent as agent_id, count(*) as writes from facts where namespace_id = ${namespaceId} group by created_by_agent
1469
+ union all
1470
+ select created_by_agent as agent_id, count(*) as writes from session_summaries where namespace_id = ${namespaceId} group by created_by_agent
1471
+ ) t
1472
+ group by agent_id
1473
+ order by writes desc
1474
+ ` : await this.client.sql`
1475
+ select agent_id, sum(writes)::text as writes
1476
+ from (
1477
+ select created_by_agent as agent_id, count(*) as writes from interactions group by created_by_agent
1478
+ union all
1479
+ select created_by_agent as agent_id, count(*) as writes from facts group by created_by_agent
1480
+ union all
1481
+ select created_by_agent as agent_id, count(*) as writes from session_summaries group by created_by_agent
1482
+ ) t
1483
+ group by agent_id
1484
+ order by writes desc
1485
+ `;
1486
+ return rows.map((row) => ({ agentId: row.agent_id, writes: Number(row.writes) }));
1487
+ }
1488
+ async getStats(namespaceId) {
1489
+ const namespaceRows = await this.client.sql`select count(*)::text as count from namespaces`;
1490
+ const interactionRows = namespaceId ? await this.client.sql`select count(*)::text as count from interactions where namespace_id = ${namespaceId}` : await this.client.sql`select count(*)::text as count from interactions`;
1491
+ const factRows = namespaceId ? await this.client.sql`select count(*)::text as count from facts where namespace_id = ${namespaceId}` : await this.client.sql`select count(*)::text as count from facts`;
1492
+ const summaryRows = namespaceId ? await this.client.sql`select count(*)::text as count from session_summaries where namespace_id = ${namespaceId}` : await this.client.sql`select count(*)::text as count from session_summaries`;
1493
+ const embeddingRows = namespaceId ? await this.client.sql`
1494
+ select status::text as status, count(*)::text as count from embeddings where namespace_id = ${namespaceId} group by status
1495
+ ` : await this.client.sql`
1496
+ select status::text as status, count(*)::text as count from embeddings group by status
1497
+ `;
1498
+ const callRows = namespaceId ? await this.client.sql`
1499
+ select
1500
+ count(*) filter (where created_at >= now() - interval '60 seconds')::text as last_60s,
1501
+ count(*) filter (where created_at >= now() - interval '1 hour')::text as last_hour,
1502
+ count(*) filter (where created_at >= now() - interval '24 hours')::text as last_24h,
1503
+ count(*) filter (where status = 'gated' and created_at >= now() - interval '24 hours')::text as gated_failures
1504
+ from model_calls
1505
+ where namespace_id = ${namespaceId}
1506
+ ` : await this.client.sql`
1507
+ select
1508
+ count(*) filter (where created_at >= now() - interval '60 seconds')::text as last_60s,
1509
+ count(*) filter (where created_at >= now() - interval '1 hour')::text as last_hour,
1510
+ count(*) filter (where created_at >= now() - interval '24 hours')::text as last_24h,
1511
+ count(*) filter (where status = 'gated' and created_at >= now() - interval '24 hours')::text as gated_failures
1512
+ from model_calls
1513
+ `;
1514
+ const embeddingsCount = { pending: 0, processing: 0, failed: 0, ready: 0 };
1515
+ for (const row of embeddingRows) {
1516
+ if (row.status === "pending") embeddingsCount.pending = Number(row.count);
1517
+ if (row.status === "processing") embeddingsCount.processing = Number(row.count);
1518
+ if (row.status === "failed") embeddingsCount.failed = Number(row.count);
1519
+ if (row.status === "ready") embeddingsCount.ready = Number(row.count);
1520
+ }
1521
+ const calls = callRows[0] ?? {
1522
+ last_60s: "0",
1523
+ last_hour: "0",
1524
+ last_24h: "0",
1525
+ gated_failures: "0"
1526
+ };
1527
+ return {
1528
+ namespaces: Number(namespaceRows[0]?.count ?? 0),
1529
+ interactions: Number(interactionRows[0]?.count ?? 0),
1530
+ facts: Number(factRows[0]?.count ?? 0),
1531
+ summaries: Number(summaryRows[0]?.count ?? 0),
1532
+ embeddings: embeddingsCount,
1533
+ modelCalls: {
1534
+ last60s: Number(calls.last_60s),
1535
+ lastHour: Number(calls.last_hour),
1536
+ last24h: Number(calls.last_24h),
1537
+ gatedFailures: Number(calls.gated_failures)
1538
+ }
1539
+ };
1540
+ }
1541
+ async pruneBefore(cutoff) {
1542
+ return this.db.transaction(async (tx) => {
1543
+ const deletedInteractions = await tx.delete(interactions).where(lt(interactions.createdAt, cutoff)).returning({ id: interactions.id });
1544
+ if (deletedInteractions.length > 0) {
1545
+ await tx.delete(embeddings).where(
1546
+ and(
1547
+ eq(embeddings.ownerType, "interaction"),
1548
+ inArray(
1549
+ embeddings.ownerId,
1550
+ deletedInteractions.map((row) => row.id)
1551
+ )
1552
+ )
1553
+ );
1554
+ }
1555
+ const deletedFacts = await tx.delete(facts).where(lt(facts.createdAt, cutoff)).returning({ id: facts.id });
1556
+ if (deletedFacts.length > 0) {
1557
+ await tx.delete(embeddings).where(
1558
+ and(
1559
+ eq(embeddings.ownerType, "fact"),
1560
+ inArray(
1561
+ embeddings.ownerId,
1562
+ deletedFacts.map((row) => row.id)
1563
+ )
1564
+ )
1565
+ );
1566
+ await tx.update(shares).set({ tombstoned: true }).where(
1567
+ and(
1568
+ eq(shares.mode, "reference"),
1569
+ inArray(
1570
+ shares.sourceFactId,
1571
+ deletedFacts.map((row) => row.id)
1572
+ )
1573
+ )
1574
+ );
1575
+ }
1576
+ const deletedSummaries = await tx.delete(sessionSummaries).where(lt(sessionSummaries.createdAt, cutoff)).returning({ id: sessionSummaries.id });
1577
+ if (deletedSummaries.length > 0) {
1578
+ await tx.delete(embeddings).where(
1579
+ and(
1580
+ eq(embeddings.ownerType, "summary"),
1581
+ inArray(
1582
+ embeddings.ownerId,
1583
+ deletedSummaries.map((row) => row.id)
1584
+ )
1585
+ )
1586
+ );
1587
+ }
1588
+ return {
1589
+ interactions: deletedInteractions.length,
1590
+ facts: deletedFacts.length,
1591
+ summaries: deletedSummaries.length
1592
+ };
1593
+ });
1594
+ }
1595
+ async getVectorIndexHealth(namespaceId) {
1596
+ const countRows = namespaceId ? await this.client.sql`select count(*)::text as count from embeddings where namespace_id = ${namespaceId}` : await this.client.sql`select count(*)::text as count from embeddings`;
1597
+ const indexRows = await this.client.sql`
1598
+ select indexname, indexdef from pg_indexes
1599
+ where schemaname = current_schema()
1600
+ and tablename = 'embeddings'
1601
+ and indexname = 'embeddings_vector_hnsw_idx'
1602
+ limit 1
1603
+ `;
1604
+ const vectorCount = Number(countRows[0]?.count ?? 0);
1605
+ const index2 = indexRows[0];
1606
+ return {
1607
+ vectorCount,
1608
+ indexName: index2?.indexname ?? null,
1609
+ indexDefinition: index2?.indexdef ?? null,
1610
+ needsTuning: vectorCount >= 1e5
1611
+ };
1612
+ }
1613
+ };
1614
+ function createPostgresRepository(client) {
1615
+ return new PostgresMemoryRepository(client);
1616
+ }
1617
+ function toAuditRow(entry) {
1618
+ return {
1619
+ id: entry.id,
1620
+ namespaceId: entry.namespaceId,
1621
+ action: entry.action,
1622
+ entityType: entry.entityType,
1623
+ entityId: entry.entityId ?? null,
1624
+ createdByAgent: entry.createdByAgent ?? null,
1625
+ metadata: entry.metadata ?? {}
1626
+ };
1627
+ }
1628
+ async function upsertPendingEmbedding(tx, embedding, reset) {
1629
+ const insert = tx.insert(embeddings).values({
1630
+ id: embedding.id,
1631
+ namespaceId: embedding.namespaceId,
1632
+ ownerType: embedding.ownerType,
1633
+ ownerId: embedding.ownerId,
1634
+ vector: zeroVectorLiteral(),
1635
+ embeddingModel: embedding.embeddingModel,
1636
+ status: "pending"
1637
+ });
1638
+ const target = [embeddings.ownerType, embeddings.ownerId];
1639
+ if (!reset) {
1640
+ await insert.onConflictDoNothing({ target });
1641
+ return;
1642
+ }
1643
+ await insert.onConflictDoUpdate({
1644
+ target,
1645
+ set: {
1646
+ vector: zeroVectorLiteral(),
1647
+ embeddingModel: embedding.embeddingModel,
1648
+ status: "pending",
1649
+ attempts: 0,
1650
+ lastError: null,
1651
+ leaseUntil: null,
1652
+ leaseToken: null,
1653
+ updatedAt: sql2`now()`
1654
+ }
1655
+ });
1656
+ }
1657
+ function zeroVectorLiteral() {
1658
+ return `[${Array.from({ length: 384 }, () => 0).join(",")}]`;
1659
+ }
1660
+ function parseVectorLiteral(literal) {
1661
+ const trimmed = literal.startsWith("[") ? literal.slice(1, -1) : literal;
1662
+ if (!trimmed) return [];
1663
+ return trimmed.split(",").map((value) => Number(value));
1664
+ }
1665
+ function advisoryLockKeys(namespaceId, sessionId) {
1666
+ const digest = createHash("sha256").update(`${namespaceId}${sessionId}`).digest();
1667
+ const k1 = digest.readInt32BE(0);
1668
+ const k2 = digest.readInt32BE(4);
1669
+ return [k1, k2];
1670
+ }
1671
+ var recentFactColumns = {
1672
+ id: facts.id,
1673
+ content: facts.content,
1674
+ subject: facts.subject,
1675
+ predicate: facts.predicate,
1676
+ object: facts.object,
1677
+ propositionKey: facts.propositionKey,
1678
+ confidence: facts.confidence,
1679
+ sourceDeleted: facts.sourceDeleted,
1680
+ supersededBy: facts.supersededBy,
1681
+ createdByAgent: facts.createdByAgent,
1682
+ createdAt: facts.createdAt
1683
+ };
1684
+ function liveReference(namespaceId, ownerNamespace, factId) {
1685
+ return sql2`exists (
1686
+ select 1 from shares s
1687
+ where s.to_namespace_id = ${namespaceId}
1688
+ and s.from_namespace_id = ${ownerNamespace}
1689
+ and s.source_fact_id = ${factId}
1690
+ and s.mode = 'reference'
1691
+ and not s.tombstoned
1692
+ )`;
1693
+ }
1694
+
1695
+ // src/core/codebuddy.ts
1696
+ import pTimeout2 from "p-timeout";
1697
+
1698
+ // src/core/memory-lifecycle.ts
1699
+ function isEligibleMemory(fact, options = {}) {
1700
+ if (options.namespace && fact.namespace !== options.namespace) return false;
1701
+ if (fact.sourceDeleted || fact.supersededBy) return false;
1702
+ if (fact.category === "incident" && fact.resolvedBy && !options.includeResolved) return false;
1703
+ return true;
1704
+ }
1705
+
1706
+ // src/planner/default-policy.ts
1707
+ var DefaultPolicy = class {
1708
+ recencyFloorSize;
1709
+ embeddingCandidateLimit;
1710
+ minVectorScore;
1711
+ constructor(options = {}) {
1712
+ this.recencyFloorSize = options.recencyFloorSize ?? 3;
1713
+ this.embeddingCandidateLimit = candidateLimit(options.embeddingCandidateLimit ?? 256);
1714
+ this.minVectorScore = options.minVectorScore ?? 0.1;
1715
+ }
1716
+ async plan(input) {
1717
+ const repo = input.repository;
1718
+ const counts = await repo.countEmbeddingStatuses(input.namespaceId);
1719
+ const query = input.queryEmbedding;
1720
+ const space = query ? embeddingSpaceKey(query.space) : null;
1721
+ let fallbackReason;
1722
+ if (!query) fallbackReason = "query_embedding_unavailable";
1723
+ else if (!validSearchVector(query.vector) || !validEmbeddingVector(query.vector, query.space.dimensions))
1724
+ fallbackReason = "invalid_query_vector";
1725
+ const recencyInteractions = await repo.getRecentInteractions(
1726
+ input.namespaceId,
1727
+ input.sessionId,
1728
+ this.recencyFloorSize
1729
+ );
1730
+ const coverage = {
1731
+ scope: "full_namespace",
1732
+ method: "lexical",
1733
+ compatibleReady: 0,
1734
+ candidateLimit: this.embeddingCandidateLimit,
1735
+ vectorCandidates: 0,
1736
+ lexicalCandidates: 0,
1737
+ lexicalMatchedFacts: 0,
1738
+ hydratedCandidates: 0,
1739
+ spaceFilter: space
1740
+ };
1741
+ let candidates = [];
1742
+ let fallback = "none";
1743
+ if (!fallbackReason && query && space) {
1744
+ const search = await repo.searchSimilarEmbeddings({
1745
+ namespaceId: input.namespaceId,
1746
+ vector: query.vector,
1747
+ embeddingSpace: space,
1748
+ embeddingModel: query.space.model,
1749
+ limit: this.embeddingCandidateLimit
1750
+ });
1751
+ coverage.compatibleReady = search.compatibleReady;
1752
+ coverage.vectorCandidates = search.candidates.length;
1753
+ coverage.method = search.method;
1754
+ const scored = search.candidates.map((row) => ({ row, score: row.score }));
1755
+ const ownerIdsByType = groupOwnerIdsByType(scored);
1756
+ const [factRows, interactionRows, summaryRows] = await Promise.all([
1757
+ repo.getFactsByIds(input.namespaceId, ownerIdsByType.fact),
1758
+ repo.getInteractionsByIds(input.namespaceId, ownerIdsByType.interaction),
1759
+ repo.getSummariesByIds(input.namespaceId, ownerIdsByType.summary)
1760
+ ]);
1761
+ candidates = this.hydrateScored(scored, factRows, interactionRows, summaryRows);
1762
+ coverage.hydratedCandidates = candidates.length;
1763
+ if (search.compatibleReady === 0)
1764
+ fallbackReason = counts.ready === 0 ? "no_ready_embeddings" : "no_compatible_embeddings";
1765
+ else if (candidates.length === 0) fallbackReason = "no_hydratable_candidates";
1766
+ }
1767
+ const useFallback = fallbackReason !== void 0;
1768
+ const lexical = await repo.searchFactsLexically(
1769
+ input.namespaceId,
1770
+ input.query,
1771
+ useFallback ? this.embeddingCandidateLimit : Math.min(this.embeddingCandidateLimit, 32)
1772
+ );
1773
+ coverage.lexicalCandidates = lexical.candidates.length;
1774
+ coverage.lexicalMatchedFacts = lexical.matchedFacts;
1775
+ if (useFallback) {
1776
+ const [summary, fallbackInteractions] = await Promise.all([
1777
+ repo.getLatestSummary(input.namespaceId, input.sessionId),
1778
+ repo.getRecentInteractions(
1779
+ input.namespaceId,
1780
+ input.sessionId,
1781
+ Math.max(this.recencyFloorSize, 10)
1782
+ )
1783
+ ]);
1784
+ candidates = this.buildFallbackCandidates(fallbackInteractions, summary);
1785
+ if (summary && fallbackInteractions.length > 0) fallback = "recency_summary";
1786
+ else if (summary) fallback = "summary";
1787
+ else fallback = "recency";
1788
+ }
1789
+ const byKey = new Map(candidates.map((candidate) => [candidate.key, candidate]));
1790
+ for (const fact of lexical.candidates) {
1791
+ if (!isEligibleMemory(fact)) continue;
1792
+ const candidate = factCandidate(fact, useFallback ? fact.score : 0.35 * fact.score);
1793
+ const existing = byKey.get(candidate.key);
1794
+ if (existing) existing.score = Math.max(existing.score, candidate.score);
1795
+ else byKey.set(candidate.key, candidate);
1796
+ }
1797
+ candidates = [...byKey.values()];
1798
+ candidates = this.applyRecencyFloor(candidates, recencyInteractions);
1799
+ candidates = this.applyConflictMode(candidates, input.conflictMode);
1800
+ candidates.sort((a, b) => {
1801
+ if (a.isRecencyFloor !== b.isRecencyFloor) return a.isRecencyFloor ? -1 : 1;
1802
+ return b.score - a.score || b.createdAt - a.createdAt || a.key.localeCompare(b.key);
1803
+ });
1804
+ const skipReasons = {};
1805
+ const included = [];
1806
+ let system = buildSystemPrompt(input.namespace, fallback, useFallback);
1807
+ let tokensUsed = countTokens(system);
1808
+ let itemsSkipped = 0;
1809
+ for (const candidate of candidates) {
1810
+ if (tokensUsed + candidate.tokens > input.budget) {
1811
+ bump(skipReasons, "budget_exhausted");
1812
+ itemsSkipped += 1;
1813
+ continue;
1814
+ }
1815
+ if (!candidate.isRecencyFloor && candidate.score < this.minVectorScore && !useFallback) {
1816
+ bump(skipReasons, "low_score");
1817
+ itemsSkipped += 1;
1818
+ continue;
1819
+ }
1820
+ included.push(candidate);
1821
+ tokensUsed += candidate.tokens;
1822
+ }
1823
+ const unresolvedKeys = [
1824
+ ...new Set(
1825
+ included.flatMap(
1826
+ (c) => c.fact?.conflict?.unresolved ? [c.fact.propositionKey ?? ""] : []
1827
+ )
1828
+ )
1829
+ ].sort();
1830
+ if (unresolvedKeys.length > 0) {
1831
+ const noted = `${system} Unresolved fact conflicts (compare fact provenance): ${unresolvedKeys.join(", ")}.`;
1832
+ const withNote = tokensUsed - countTokens(system) + countTokens(noted);
1833
+ if (withNote <= input.budget) {
1834
+ system = noted;
1835
+ tokensUsed = withNote;
1836
+ }
1837
+ }
1838
+ const messages = included.map((candidate) => ({
1839
+ role: candidate.role,
1840
+ content: candidate.content,
1841
+ ...candidate.fact ? { fact: candidate.fact } : {}
1842
+ }));
1843
+ const stats = {
1844
+ tokensUsed,
1845
+ itemsIncluded: included.length,
1846
+ itemsSkipped,
1847
+ budgetHeadroom: Math.max(0, input.budget - tokensUsed),
1848
+ fallback,
1849
+ skipReasons,
1850
+ embeddingCoverage: counts,
1851
+ candidateCoverage: coverage,
1852
+ ...fallbackReason ? { fallbackReason } : {},
1853
+ explain: [
1854
+ `Candidate search covers the full namespace; limit ${this.embeddingCandidateLimit} applies after ranking.`,
1855
+ `Embedding space filter: ${coverage.spaceFilter ?? "unavailable"}; ${coverage.compatibleReady} compatible ready vectors; ${coverage.vectorCandidates} vector candidates; ${coverage.hydratedCandidates} hydrated.`,
1856
+ `Durable lexical facts: ${coverage.lexicalCandidates} candidates of ${coverage.lexicalMatchedFacts} matches.`,
1857
+ ...useFallback ? [
1858
+ `Fallback: ${fallbackReason}; ${coverage.lexicalCandidates} durable lexical candidates of ${coverage.lexicalMatchedFacts} matches.`
1859
+ ] : []
1860
+ ],
1861
+ budgetClamped: false,
1862
+ budgetStatus: tokensUsed > input.budget ? "overflow" : "within_budget",
1863
+ ...tokensUsed > input.budget ? { budgetWarning: "Required memory system prompt exceeds budget." } : {}
1864
+ };
1865
+ return { system, messages, stats };
1866
+ }
1867
+ hydrateScored(scored, factRows, interactionRows, summaryRows) {
1868
+ const factById = new Map(factRows.map((row) => [row.id, row]));
1869
+ const interactionById = new Map(interactionRows.map((row) => [row.id, row]));
1870
+ const summaryById = new Map(summaryRows.map((row) => [row.id, row]));
1871
+ const out = [];
1872
+ for (const { row, score } of scored) {
1873
+ if (row.ownerType === "fact") {
1874
+ const fact = factById.get(row.ownerId);
1875
+ if (!fact || !isEligibleMemory(fact)) continue;
1876
+ out.push(factCandidate(fact, score));
1877
+ } else if (row.ownerType === "interaction") {
1878
+ const interaction = interactionById.get(row.ownerId);
1879
+ if (!interaction) continue;
1880
+ out.push({
1881
+ key: `interaction:${interaction.id}`,
1882
+ role: "user",
1883
+ content: interaction.content,
1884
+ tokens: countTokens(interaction.content),
1885
+ score,
1886
+ source: "interaction",
1887
+ ownerId: interaction.id,
1888
+ createdAt: interaction.createdAt.getTime(),
1889
+ confidence: 1,
1890
+ isRecencyFloor: false
1891
+ });
1892
+ } else {
1893
+ const summary = summaryById.get(row.ownerId);
1894
+ if (!summary) continue;
1895
+ out.push({
1896
+ key: `summary:${summary.id}`,
1897
+ role: "system",
1898
+ content: summary.content,
1899
+ tokens: countTokens(summary.content),
1900
+ score,
1901
+ source: "summary",
1902
+ ownerId: summary.id,
1903
+ createdAt: summary.createdAt.getTime(),
1904
+ confidence: 1,
1905
+ isRecencyFloor: false
1906
+ });
1907
+ }
1908
+ }
1909
+ return out;
1910
+ }
1911
+ applyRecencyFloor(candidates, floor) {
1912
+ const byKey = /* @__PURE__ */ new Map();
1913
+ for (const candidate of candidates) byKey.set(candidate.key, candidate);
1914
+ for (const row of floor) {
1915
+ const key = `interaction:${row.id}`;
1916
+ const existing = byKey.get(key);
1917
+ if (existing) {
1918
+ existing.isRecencyFloor = true;
1919
+ continue;
1920
+ }
1921
+ byKey.set(key, {
1922
+ key,
1923
+ role: "user",
1924
+ content: row.content,
1925
+ tokens: countTokens(row.content),
1926
+ score: 1,
1927
+ source: "interaction",
1928
+ ownerId: row.id,
1929
+ createdAt: row.createdAt.getTime(),
1930
+ confidence: 1,
1931
+ isRecencyFloor: true
1932
+ });
1933
+ }
1934
+ return Array.from(byKey.values());
1935
+ }
1936
+ buildFallbackCandidates(interactions2, summary) {
1937
+ const out = [];
1938
+ if (summary) {
1939
+ out.push({
1940
+ key: `summary:${summary.id}`,
1941
+ role: "system",
1942
+ content: summary.content,
1943
+ tokens: countTokens(summary.content),
1944
+ score: 1,
1945
+ source: "summary",
1946
+ ownerId: summary.id,
1947
+ createdAt: summary.createdAt.getTime(),
1948
+ confidence: 1,
1949
+ isRecencyFloor: true
1950
+ });
1951
+ }
1952
+ for (const row of interactions2) {
1953
+ out.push({
1954
+ key: `interaction:${row.id}`,
1955
+ role: "user",
1956
+ content: row.content,
1957
+ tokens: countTokens(row.content),
1958
+ score: 1,
1959
+ source: "interaction",
1960
+ ownerId: row.id,
1961
+ createdAt: row.createdAt.getTime(),
1962
+ confidence: 1,
1963
+ isRecencyFloor: true
1964
+ });
1965
+ }
1966
+ return out;
1967
+ }
1968
+ /**
1969
+ * Resolve rival assertions of the same explicit proposition key. Facts without
1970
+ * a key are treated as multi-valued and never grouped, so identical or
1971
+ * different surface text alone never merges or splits claims.
1972
+ */
1973
+ applyConflictMode(candidates, mode) {
1974
+ const groups = /* @__PURE__ */ new Map();
1975
+ for (const candidate of candidates) {
1976
+ const key = candidate.fact?.propositionKey;
1977
+ if (!key) continue;
1978
+ groups.set(key, [...groups.get(key) ?? [], candidate]);
1979
+ }
1980
+ const dropped = /* @__PURE__ */ new Set();
1981
+ for (const bucket of groups.values()) {
1982
+ if (bucket.length < 2) continue;
1983
+ bucket.sort((a, b) => compareRivals(a, b, mode));
1984
+ const winner = bucket[0];
1985
+ const contenders = bucket.filter(
1986
+ (c) => mode === "all" || c.createdAt === winner.createdAt && c.confidence === winner.confidence
1987
+ );
1988
+ const objects = new Set(contenders.map((c) => c.fact?.object.trim().toLowerCase()));
1989
+ const conflict = {
1990
+ candidateIds: bucket.map((c) => c.ownerId).sort(),
1991
+ unresolved: objects.size > 1
1992
+ };
1993
+ for (const candidate of bucket) {
1994
+ if (mode !== "all" && candidate !== winner) dropped.add(candidate.key);
1995
+ else if (candidate.fact) candidate.fact = { ...candidate.fact, conflict };
1996
+ }
1997
+ }
1998
+ return candidates.filter((candidate) => !dropped.has(candidate.key));
1999
+ }
2000
+ };
2001
+ function compareRivals(a, b, mode) {
2002
+ const byTime = b.createdAt - a.createdAt;
2003
+ const byConfidence = b.confidence - a.confidence;
2004
+ const ordered = mode === "highest_confidence" ? byConfidence || byTime : byTime || byConfidence;
2005
+ return ordered || a.ownerId.localeCompare(b.ownerId);
2006
+ }
2007
+ function groupOwnerIdsByType(scored) {
2008
+ const out = { fact: [], interaction: [], summary: [] };
2009
+ for (const { row } of scored) {
2010
+ out[row.ownerType].push(row.ownerId);
2011
+ }
2012
+ return out;
2013
+ }
2014
+ function bump(map, key) {
2015
+ map[key] = (map[key] ?? 0) + 1;
2016
+ }
2017
+ function buildSystemPrompt(namespace, fallback, isFallback) {
2018
+ const base = `Memory context for namespace "${namespace}".`;
2019
+ if (isFallback) {
2020
+ return `${base} Fallback active (${fallback}); vector search unavailable or empty.`;
2021
+ }
2022
+ return base;
2023
+ }
2024
+ function factCandidate(fact, score) {
2025
+ return {
2026
+ key: `fact:${fact.id}`,
2027
+ role: "system",
2028
+ content: fact.content,
2029
+ tokens: countTokens(fact.content),
2030
+ score,
2031
+ source: "fact",
2032
+ ownerId: fact.id,
2033
+ createdAt: fact.createdAt.getTime(),
2034
+ confidence: fact.confidence,
2035
+ isRecencyFloor: false,
2036
+ fact: {
2037
+ id: fact.id,
2038
+ subject: fact.subject,
2039
+ predicate: fact.predicate,
2040
+ object: fact.object,
2041
+ propositionKey: fact.propositionKey,
2042
+ confidence: fact.confidence,
2043
+ createdAt: fact.createdAt.toISOString(),
2044
+ createdByAgent: fact.createdByAgent
2045
+ }
2046
+ };
2047
+ }
2048
+
2049
+ // src/providers/huggingface.ts
2050
+ import { HfInference, setLogger } from "@huggingface/inference";
2051
+
2052
+ // src/providers/resilience.ts
2053
+ import PQueue from "p-queue";
2054
+ import pRetry, { AbortError } from "p-retry";
2055
+ import pTimeout from "p-timeout";
2056
+ var DEFAULT_RESILIENCE_OPTIONS = {
2057
+ retries: 5,
2058
+ concurrency: 2,
2059
+ rateLimitPerMinute: 10,
2060
+ timeoutMs: 9e4,
2061
+ warmCacheTtlMs: 5 * 6e4,
2062
+ coldStartBufferMs: 2e3,
2063
+ jitterMs: 250,
2064
+ minRetryTimeoutMs: 1e3,
2065
+ maxRetryTimeoutMs: 3e4
2066
+ };
2067
+ var GatedModelError = class extends Error {
2068
+ model;
2069
+ constructor(model) {
2070
+ super(
2071
+ `Model ${model} is gated. Accept the license at https://huggingface.co/${model} and retry.`
2072
+ );
2073
+ this.name = "GatedModelError";
2074
+ this.model = model;
2075
+ }
2076
+ };
2077
+ var UnauthorizedError = class extends Error {
2078
+ constructor() {
2079
+ super("Hugging Face rejected the API token (401). Set a valid HF token and retry.");
2080
+ this.name = "UnauthorizedError";
2081
+ }
2082
+ };
2083
+ var ModelUnavailableError = class extends Error {
2084
+ model;
2085
+ constructor(model, cause) {
2086
+ super(`Model ${model} is unavailable.`);
2087
+ this.name = "ModelUnavailableError";
2088
+ this.model = model;
2089
+ this.cause = cause;
2090
+ }
2091
+ };
2092
+ var HuggingFaceResilience = class {
2093
+ options;
2094
+ queues = /* @__PURE__ */ new Map();
2095
+ warmUntil = /* @__PURE__ */ new Map();
2096
+ constructor(options = {}) {
2097
+ this.options = {
2098
+ ...DEFAULT_RESILIENCE_OPTIONS,
2099
+ ...options
2100
+ };
2101
+ }
2102
+ isWarm(model, now = Date.now()) {
2103
+ return (this.warmUntil.get(model) ?? 0) > now;
2104
+ }
2105
+ markWarm(model, now = Date.now()) {
2106
+ this.warmUntil.set(model, now + this.options.warmCacheTtlMs);
2107
+ }
2108
+ async execute(input) {
2109
+ const models = [input.model, ...input.fallbackModels ?? []];
2110
+ let lastError;
2111
+ for (const [index2, model] of models.entries()) {
2112
+ const fallbackTriggered = index2 > 0;
2113
+ try {
2114
+ return await this.executeForModel(model, input, fallbackTriggered);
2115
+ } catch (error) {
2116
+ lastError = error;
2117
+ if (error instanceof GatedModelError || error instanceof UnauthorizedError) {
2118
+ throw error;
2119
+ }
2120
+ }
2121
+ }
2122
+ throw lastError instanceof Error ? lastError : new Error("All provider models failed.");
2123
+ }
2124
+ async executeForModel(model, input, fallbackTriggered) {
2125
+ const queue = this.getQueue(model);
2126
+ const startedAt = Date.now();
2127
+ let retries = 0;
2128
+ let coldStartWaitMs = 0;
2129
+ let status = fallbackTriggered ? "fallback" : "success";
2130
+ try {
2131
+ const result = await queue.add(
2132
+ async () => pRetry(
2133
+ async () => {
2134
+ try {
2135
+ return await pTimeout(input.call(model), {
2136
+ milliseconds: this.options.timeoutMs
2137
+ });
2138
+ } catch (error) {
2139
+ const classified = classifyHuggingFaceError(error, model);
2140
+ if (classified.kind === "gated") {
2141
+ status = "gated";
2142
+ throw new AbortError(new GatedModelError(model));
2143
+ }
2144
+ if (classified.kind === "unauthorized") {
2145
+ throw new AbortError(new UnauthorizedError());
2146
+ }
2147
+ if (classified.kind === "cold_start") {
2148
+ const waitMs = this.calculateColdStartWaitMs(classified.estimatedTimeSeconds);
2149
+ coldStartWaitMs += waitMs;
2150
+ await (input.sleep ?? sleep)(waitMs);
2151
+ throw error;
2152
+ }
2153
+ if (classified.kind === "rate_limited") {
2154
+ status = "rate_limited";
2155
+ if (classified.retryAfterMs) {
2156
+ await (input.sleep ?? sleep)(classified.retryAfterMs);
2157
+ }
2158
+ throw error;
2159
+ }
2160
+ if (classified.kind === "unavailable") {
2161
+ throw new ModelUnavailableError(model, error);
2162
+ }
2163
+ if (classified.kind === "timeout") {
2164
+ status = "timeout";
2165
+ }
2166
+ throw error;
2167
+ }
2168
+ },
2169
+ {
2170
+ retries: this.options.retries,
2171
+ minTimeout: this.options.minRetryTimeoutMs,
2172
+ maxTimeout: this.options.maxRetryTimeoutMs,
2173
+ factor: 2,
2174
+ randomize: true,
2175
+ onFailedAttempt: (error) => {
2176
+ retries = error.attemptNumber;
2177
+ }
2178
+ }
2179
+ ),
2180
+ { throwOnTimeout: true }
2181
+ );
2182
+ this.markWarm(model);
2183
+ status = fallbackTriggered ? "fallback" : "success";
2184
+ await input.onDiagnostic?.({
2185
+ model,
2186
+ type: input.type,
2187
+ status,
2188
+ latencyMs: Date.now() - startedAt,
2189
+ retries,
2190
+ fallbackTriggered,
2191
+ coldStartWaitMs
2192
+ });
2193
+ return result;
2194
+ } catch (error) {
2195
+ const finalError = error instanceof AbortError && error.originalError ? error.originalError : error;
2196
+ const classified = classifyHuggingFaceError(finalError, model);
2197
+ const finalStatus = statusForClassifiedError(classified.kind, status);
2198
+ await input.onDiagnostic?.({
2199
+ model,
2200
+ type: input.type,
2201
+ status: finalStatus,
2202
+ latencyMs: Date.now() - startedAt,
2203
+ error: finalError instanceof Error ? finalError.message : String(finalError),
2204
+ retries,
2205
+ fallbackTriggered,
2206
+ coldStartWaitMs
2207
+ });
2208
+ throw finalError;
2209
+ }
2210
+ }
2211
+ getQueue(model) {
2212
+ const existing = this.queues.get(model);
2213
+ if (existing) {
2214
+ return existing;
2215
+ }
2216
+ const queue = new PQueue({
2217
+ concurrency: this.options.concurrency,
2218
+ intervalCap: this.options.rateLimitPerMinute,
2219
+ interval: 6e4
2220
+ });
2221
+ this.queues.set(model, queue);
2222
+ return queue;
2223
+ }
2224
+ calculateColdStartWaitMs(estimatedTimeSeconds) {
2225
+ return Math.ceil(
2226
+ estimatedTimeSeconds * 1e3 + this.options.coldStartBufferMs + this.randomJitter()
2227
+ );
2228
+ }
2229
+ randomJitter() {
2230
+ return Math.floor(Math.random() * this.options.jitterMs);
2231
+ }
2232
+ };
2233
+ function classifyHuggingFaceError(error, model) {
2234
+ if (error instanceof GatedModelError) {
2235
+ return { kind: "gated" };
2236
+ }
2237
+ if (error instanceof ModelUnavailableError) {
2238
+ return { kind: "unavailable" };
2239
+ }
2240
+ if (error instanceof UnauthorizedError) {
2241
+ return { kind: "unauthorized" };
2242
+ }
2243
+ if (error instanceof Error && error.name === "TimeoutError") {
2244
+ return { kind: "timeout" };
2245
+ }
2246
+ const hfError = error;
2247
+ const status = hfError.status ?? hfError.statusCode ?? hfError.response?.status ?? // @huggingface/inference 4.x errors carry the code here.
2248
+ hfError.httpResponse?.status;
2249
+ if (status === 401) {
2250
+ return { kind: "unauthorized" };
2251
+ }
2252
+ if (status === 403) {
2253
+ return { kind: "gated" };
2254
+ }
2255
+ if (status === 404) {
2256
+ return { kind: "unavailable" };
2257
+ }
2258
+ if (status === 429) {
2259
+ const retryAfterMs = readRetryAfterMs(hfError);
2260
+ return retryAfterMs === void 0 ? { kind: "rate_limited" } : { kind: "rate_limited", retryAfterMs };
2261
+ }
2262
+ if (status === 503) {
2263
+ const estimatedTimeSeconds = readEstimatedTimeSeconds(hfError);
2264
+ if (estimatedTimeSeconds !== void 0) {
2265
+ return { kind: "cold_start", estimatedTimeSeconds };
2266
+ }
2267
+ }
2268
+ if (error instanceof Error && error.message.includes("timed out")) {
2269
+ return { kind: "timeout" };
2270
+ }
2271
+ if (error instanceof Error && error.message.includes(model) && error.message.includes("gated")) {
2272
+ return { kind: "gated" };
2273
+ }
2274
+ return { kind: "unknown" };
2275
+ }
2276
+ function statusForClassifiedError(kind, currentStatus) {
2277
+ if (kind === "gated") {
2278
+ return "gated";
2279
+ }
2280
+ if (kind === "rate_limited") {
2281
+ return "rate_limited";
2282
+ }
2283
+ if (kind === "timeout") {
2284
+ return "timeout";
2285
+ }
2286
+ if (currentStatus === "fallback") {
2287
+ return "fallback";
2288
+ }
2289
+ return "failed";
2290
+ }
2291
+ function readEstimatedTimeSeconds(error) {
2292
+ if (typeof error.estimated_time === "number") {
2293
+ return error.estimated_time;
2294
+ }
2295
+ if (typeof error.estimatedTime === "number") {
2296
+ return error.estimatedTime;
2297
+ }
2298
+ if (typeof error.cause === "object" && error.cause !== null && "estimated_time" in error.cause) {
2299
+ const estimatedTime = error.cause.estimated_time;
2300
+ return typeof estimatedTime === "number" ? estimatedTime : void 0;
2301
+ }
2302
+ return void 0;
2303
+ }
2304
+ function readRetryAfterMs(error) {
2305
+ const retryAfter = error.response?.headers?.get?.("retry-after");
2306
+ if (!retryAfter) {
2307
+ return void 0;
2308
+ }
2309
+ const retryAfterSeconds = Number(retryAfter);
2310
+ return Number.isFinite(retryAfterSeconds) ? retryAfterSeconds * 1e3 : void 0;
2311
+ }
2312
+ function sleep(ms) {
2313
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
2314
+ }
2315
+
2316
+ // src/providers/huggingface.ts
2317
+ var toStderr = (...args) => console.error(...args);
2318
+ setLogger({ debug: toStderr, info: toStderr, log: toStderr, warn: toStderr, error: toStderr });
2319
+ var HuggingFaceProvider = class {
2320
+ client;
2321
+ embeddingModels;
2322
+ llmModels;
2323
+ resilience;
2324
+ onDiagnostic;
2325
+ /** The primary model's space: `reindex` re-embeds rows from any other (incl. fallback) space. */
2326
+ space;
2327
+ constructor(options = {}) {
2328
+ this.client = options.client ?? new HfInference(options.apiKey);
2329
+ this.embeddingModels = options.embeddingModels ?? DEFAULT_EMBEDDING_MODELS;
2330
+ this.llmModels = options.llmModels ?? DEFAULT_LLM_MODELS;
2331
+ this.resilience = new HuggingFaceResilience({
2332
+ ...DEFAULT_RESILIENCE_OPTIONS,
2333
+ ...options.resilience
2334
+ });
2335
+ this.onDiagnostic = options.onDiagnostic;
2336
+ assertCompatibleEmbeddingModels(this.embeddingModels);
2337
+ const primary = this.embeddingModels[0];
2338
+ if (primary) {
2339
+ this.space = {
2340
+ model: primary.id,
2341
+ revision: primary.revision ?? "unversioned",
2342
+ schema: primary.schema ?? "hf-feature-extraction-v1",
2343
+ dimensions: primary.dimensions ?? EMBEDDING_DIMENSIONS
2344
+ };
2345
+ }
2346
+ }
2347
+ async embed(request) {
2348
+ const primaryModel = request.model ?? this.embeddingModels[0]?.id;
2349
+ if (!primaryModel) {
2350
+ throw new Error("No embedding model configured.");
2351
+ }
2352
+ const fallbackModels = this.embeddingModels.map((model) => model.id).filter((model) => model !== primaryModel);
2353
+ return this.resilience.execute({
2354
+ model: primaryModel,
2355
+ fallbackModels,
2356
+ type: "embedding",
2357
+ ...this.onDiagnostic ? { onDiagnostic: this.onDiagnostic } : {},
2358
+ call: async (model) => {
2359
+ const response = await this.client.featureExtraction({
2360
+ model,
2361
+ inputs: request.input
2362
+ });
2363
+ const definition = this.embeddingModels.find((entry) => entry.id === model);
2364
+ const vectors = normalizeEmbeddingResponse(response);
2365
+ const dimensions = vectors[0]?.length ?? 0;
2366
+ const result = {
2367
+ model,
2368
+ revision: definition?.revision ?? "unversioned",
2369
+ schema: definition?.schema ?? "hf-feature-extraction-v1",
2370
+ dimensions,
2371
+ vectors
2372
+ };
2373
+ validateEmbeddingResponse(
2374
+ result,
2375
+ typeof request.input === "string" ? 1 : request.input.length
2376
+ );
2377
+ if (dimensions !== (definition?.dimensions ?? EMBEDDING_DIMENSIONS)) {
2378
+ throw new Error(
2379
+ `Embedding model ${model} returned unexpected dimensions: ${dimensions}.`
2380
+ );
2381
+ }
2382
+ return result;
2383
+ }
2384
+ });
2385
+ }
2386
+ async generateText(request) {
2387
+ const primaryModel = request.model ?? this.llmModels[0]?.id;
2388
+ if (!primaryModel) {
2389
+ throw new Error("No LLM model configured.");
2390
+ }
2391
+ const fallbackModels = this.llmModels.map((model) => model.id).filter((model) => model !== primaryModel);
2392
+ return this.resilience.execute({
2393
+ model: primaryModel,
2394
+ fallbackModels,
2395
+ type: "llm",
2396
+ ...this.onDiagnostic ? { onDiagnostic: this.onDiagnostic } : {},
2397
+ call: async (model) => {
2398
+ const response = await this.client.textGeneration({
2399
+ model,
2400
+ inputs: request.prompt
2401
+ });
2402
+ return {
2403
+ model,
2404
+ text: response.generated_text
2405
+ };
2406
+ }
2407
+ });
2408
+ }
2409
+ };
2410
+ function normalizeEmbeddingResponse(response) {
2411
+ if (!Array.isArray(response)) {
2412
+ throw new Error("Unexpected Hugging Face embedding response.");
2413
+ }
2414
+ if (response.every((value) => typeof value === "number")) {
2415
+ return [response];
2416
+ }
2417
+ if (response.every(
2418
+ (vector2) => Array.isArray(vector2) && vector2.every((value) => typeof value === "number")
2419
+ )) {
2420
+ return response;
2421
+ }
2422
+ throw new Error("Unexpected Hugging Face embedding vector shape.");
2423
+ }
2424
+
2425
+ // src/tracing/langsmith.ts
2426
+ function isLangSmithTracingEnabled(env = process.env) {
2427
+ return env.LANGCHAIN_TRACING_V2 === "true" || env.LANGSMITH_TRACING === "true";
2428
+ }
2429
+ function createTracer(env = process.env) {
2430
+ const enabled = isLangSmithTracingEnabled(env);
2431
+ if (!enabled) {
2432
+ return {
2433
+ enabled: false,
2434
+ startSpan: () => noopSpan,
2435
+ withSpan: async (_name, _input, fn) => fn()
2436
+ };
2437
+ }
2438
+ return {
2439
+ enabled: true,
2440
+ startSpan(name, input) {
2441
+ const started = Date.now();
2442
+ log("span:start", { name, input });
2443
+ return {
2444
+ end(metadata) {
2445
+ log("span:end", { name, durationMs: Date.now() - started, metadata });
2446
+ },
2447
+ fail(error, metadata) {
2448
+ log("span:fail", {
2449
+ name,
2450
+ durationMs: Date.now() - started,
2451
+ error: error instanceof Error ? error.message : String(error),
2452
+ metadata
2453
+ });
2454
+ }
2455
+ };
2456
+ },
2457
+ async withSpan(name, input, fn) {
2458
+ const span = this.startSpan(name, input);
2459
+ try {
2460
+ const result = await fn();
2461
+ span.end();
2462
+ return result;
2463
+ } catch (error) {
2464
+ span.fail(error);
2465
+ throw error;
2466
+ }
2467
+ }
2468
+ };
2469
+ }
2470
+ var noopSpan = {
2471
+ end: () => void 0,
2472
+ fail: () => void 0
2473
+ };
2474
+ function log(event, payload) {
2475
+ if (process.env.NODE_ENV === "test") return;
2476
+ process.stderr.write(`${JSON.stringify({ at: "tracer", event, ...payload })}
2477
+ `);
2478
+ }
2479
+
2480
+ // src/core/config.ts
2481
+ import { z } from "zod";
2482
+ var providerSchema = z.object({
2483
+ type: z.enum(["local", "huggingface", "openrouter"]),
2484
+ apiKey: z.string().min(1).optional(),
2485
+ model: z.string().min(1).optional(),
2486
+ relevanceFloor: z.number().min(-1).max(1).optional()
2487
+ });
2488
+ var codeBuddyConfigSchema = z.object({
2489
+ postgresUrl: z.string().min(1, "postgresUrl is required"),
2490
+ provider: providerSchema,
2491
+ namespace: z.string().min(1, "namespace is required").max(128, "namespace must be 128 chars or fewer").regex(/^[a-zA-Z0-9_.:-]+$/, "namespace must match [a-zA-Z0-9_.:-]+"),
2492
+ projectRoot: z.string().min(1).optional(),
2493
+ tokenBudget: z.number().int().positive().max(1e6).optional(),
2494
+ graphify: z.object({
2495
+ enabled: z.boolean(),
2496
+ graphPath: z.string().min(1).optional()
2497
+ }).optional(),
2498
+ worker: z.object({
2499
+ pollIntervalMs: z.number().int().min(50).max(6e4).optional(),
2500
+ batchSize: z.number().int().min(1).max(64).optional(),
2501
+ maxAttempts: z.number().int().min(1).max(20).optional()
2502
+ }).optional()
2503
+ });
2504
+ function validateConfig(config) {
2505
+ return codeBuddyConfigSchema.parse(config);
2506
+ }
2507
+
2508
+ // src/core/memory-file-store.ts
2509
+ import { mkdir, readdir, readFile, rm } from "fs/promises";
2510
+ import { dirname as dirname2, join, relative, resolve as resolve2, sep } from "path";
2511
+ import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
2512
+ import { z as z2 } from "zod";
2513
+ var FACT_CATEGORIES = ["general", "incident"];
2514
+ var INCIDENT_SEVERITIES = ["low", "medium", "high", "critical"];
2515
+ var factFrontMatterSchema = z2.object({
2516
+ schemaVersion: z2.literal(1),
2517
+ id: z2.string().regex(/^fact_[a-z0-9]+$/),
2518
+ namespace: z2.string().min(1),
2519
+ type: z2.literal("fact"),
2520
+ subject: z2.string(),
2521
+ predicate: z2.string(),
2522
+ object: z2.string(),
2523
+ /** Explicit single-valued claim identity; facts sharing it are rival assertions. */
2524
+ propositionKey: z2.string().min(1).nullable().default(null),
2525
+ confidence: z2.number().min(0).max(1),
2526
+ createdAt: z2.preprocess(
2527
+ (value) => value instanceof Date ? value.toISOString() : value,
2528
+ z2.string().datetime()
2529
+ ),
2530
+ createdByAgent: z2.string().nullable(),
2531
+ sourceInteractionId: z2.string().nullable(),
2532
+ sourceDeleted: z2.boolean(),
2533
+ category: z2.enum(FACT_CATEGORIES).default("general"),
2534
+ paths: z2.array(z2.string().min(1)).default([]),
2535
+ severity: z2.enum(INCIDENT_SEVERITIES).nullable().default(null),
2536
+ introducedBy: z2.string().nullable().default(null),
2537
+ resolvedBy: z2.string().nullable().default(null),
2538
+ fingerprint: z2.string().regex(/^[a-f0-9]{64}$/).nullable().default(null),
2539
+ sourcePlanId: z2.string().nullable().default(null),
2540
+ sourceSessionId: z2.string().nullable().default(null),
2541
+ verification: z2.array(z2.string()).default([]),
2542
+ supersededBy: z2.string().nullable().default(null),
2543
+ /** Namespaces granted a live reference to this fact (durable form of reference shares). */
2544
+ sharedWith: z2.array(z2.string().min(1)).default([])
2545
+ });
2546
+ var summaryFrontMatterSchema = z2.object({
2547
+ schemaVersion: z2.literal(1),
2548
+ id: z2.string().regex(/^sum_[a-z0-9]+$/),
2549
+ namespace: z2.string().min(1),
2550
+ type: z2.literal("summary"),
2551
+ sessionId: z2.string().min(1),
2552
+ version: z2.number().int().positive(),
2553
+ tokenCount: z2.number().int().nonnegative(),
2554
+ createdAt: z2.preprocess(
2555
+ (value) => value instanceof Date ? value.toISOString() : value,
2556
+ z2.string().datetime()
2557
+ ),
2558
+ createdByAgent: z2.string().nullable()
2559
+ });
2560
+ var MemoryFileStore = class {
2561
+ repositoryRoot;
2562
+ factsDirectory;
2563
+ summariesDirectory;
2564
+ constructor(repositoryRoot = process.cwd()) {
2565
+ this.repositoryRoot = resolve2(repositoryRoot);
2566
+ this.factsDirectory = join(this.repositoryRoot, ".codebuddy", "memory", "facts");
2567
+ this.summariesDirectory = join(this.repositoryRoot, ".codebuddy", "memory", "summaries");
2568
+ }
2569
+ async writeFact(raw) {
2570
+ const input = redactPersisted(raw);
2571
+ const path = this.factPath(input.id);
2572
+ await this.prepareDirectory(this.factsDirectory);
2573
+ await assertNoSymlinkBetween(this.repositoryRoot, dirname2(path));
2574
+ const metadata = {
2575
+ schemaVersion: 1,
2576
+ id: input.id,
2577
+ namespace: input.namespace,
2578
+ type: "fact",
2579
+ subject: input.subject,
2580
+ predicate: input.predicate,
2581
+ object: input.object,
2582
+ propositionKey: input.propositionKey,
2583
+ confidence: input.confidence,
2584
+ createdAt: input.createdAt,
2585
+ createdByAgent: input.createdByAgent,
2586
+ sourceInteractionId: input.sourceInteractionId,
2587
+ sourceDeleted: input.sourceDeleted,
2588
+ category: input.category,
2589
+ paths: input.paths,
2590
+ severity: input.severity,
2591
+ introducedBy: input.introducedBy,
2592
+ resolvedBy: input.resolvedBy,
2593
+ fingerprint: input.fingerprint,
2594
+ sourcePlanId: input.sourcePlanId,
2595
+ sourceSessionId: input.sourceSessionId,
2596
+ verification: input.verification,
2597
+ supersededBy: input.supersededBy,
2598
+ sharedWith: input.sharedWith
2599
+ };
2600
+ const frontMatter = stringifyYaml(stripUndefinedAndDefaultIncidentFields(metadata));
2601
+ await writeAtomic(path, composeMarkdown(frontMatter, input.content));
2602
+ return path;
2603
+ }
2604
+ async writeSummary(input) {
2605
+ const path = this.summaryPath(input.id);
2606
+ await this.prepareDirectory(this.summariesDirectory);
2607
+ await assertNoSymlinkBetween(this.repositoryRoot, dirname2(path));
2608
+ const metadata = stringifyYaml({
2609
+ schemaVersion: 1,
2610
+ id: input.id,
2611
+ namespace: input.namespace,
2612
+ type: "summary",
2613
+ sessionId: input.sessionId,
2614
+ version: input.version,
2615
+ tokenCount: input.tokenCount,
2616
+ createdAt: input.createdAt,
2617
+ createdByAgent: input.createdByAgent
2618
+ });
2619
+ await writeAtomic(path, composeMarkdown(metadata, input.content));
2620
+ return path;
2621
+ }
2622
+ async readFact(pathOrId) {
2623
+ const path = pathOrId.endsWith(".md") ? this.safePath(pathOrId) : this.factPath(pathOrId);
2624
+ await assertNoSymlinkBetween(this.repositoryRoot, path);
2625
+ const parsed = parseFrontMatter(await readFile(path, "utf8"));
2626
+ const metadata = factFrontMatterSchema.parse(parseYaml(parsed.frontMatter));
2627
+ return {
2628
+ ...metadata,
2629
+ content: parsed.content.trim(),
2630
+ path
2631
+ };
2632
+ }
2633
+ async listFacts() {
2634
+ return (await this.scanFacts()).facts;
2635
+ }
2636
+ /**
2637
+ * Parse every managed fact file. Hand-written markdown is ignored, and a
2638
+ * schema-invalid/corrupt fact is reported in `invalid` (not thrown) so one
2639
+ * bad file can't break recall, capture or reindexing for the whole project.
2640
+ * Invalid files stay on disk for manual repair.
2641
+ */
2642
+ async scanFacts() {
2643
+ const scan = await this.scan(
2644
+ this.factsDirectory,
2645
+ /^fact_[a-z0-9]+\.md$/,
2646
+ (path) => this.readFact(path)
2647
+ );
2648
+ return { facts: scan.items, invalid: scan.invalid };
2649
+ }
2650
+ /**
2651
+ * List incident facts for the namespace, ranked by severity then recency.
2652
+ * Unlike {@link findIncidentFactsForPaths} this does not filter by path, so
2653
+ * callers (e.g. the context engine's project bootstrap) can surface the
2654
+ * repository's incident hotspots before any specific edit target is known.
2655
+ */
2656
+ async listIncidentFacts(input = {}) {
2657
+ const incidents = (await this.listFacts()).filter(
2658
+ (fact) => fact.category === "incident" && Boolean(fact.severity) && isEligibleMemory(fact, input)
2659
+ );
2660
+ return incidents.sort((left, right) => {
2661
+ const severity = severityRank(right.severity) - severityRank(left.severity);
2662
+ if (severity !== 0) return severity;
2663
+ return right.createdAt.localeCompare(left.createdAt);
2664
+ });
2665
+ }
2666
+ async findIncidentFactsForPaths(input) {
2667
+ const wanted = new Set(input.paths.map(normalizeMemoryPath));
2668
+ if (wanted.size === 0) return [];
2669
+ return (await this.listIncidentFacts({
2670
+ ...input.namespace !== void 0 ? { namespace: input.namespace } : {},
2671
+ ...input.includeResolved !== void 0 ? { includeResolved: input.includeResolved } : {}
2672
+ })).filter((fact) => fact.paths.map(normalizeMemoryPath).some((path) => wanted.has(path)));
2673
+ }
2674
+ async readSummary(pathOrId) {
2675
+ const path = pathOrId.endsWith(".md") ? this.safePath(pathOrId) : this.summaryPath(pathOrId);
2676
+ await assertNoSymlinkBetween(this.repositoryRoot, path);
2677
+ const parsed = parseFrontMatter(await readFile(path, "utf8"));
2678
+ return {
2679
+ ...summaryFrontMatterSchema.parse(parseYaml(parsed.frontMatter)),
2680
+ content: parsed.content.trim(),
2681
+ path
2682
+ };
2683
+ }
2684
+ async listSummaries() {
2685
+ return (await this.scanSummaries()).summaries;
2686
+ }
2687
+ async scanSummaries() {
2688
+ const scan = await this.scan(
2689
+ this.summariesDirectory,
2690
+ /^sum_[a-z0-9]+\.md$/,
2691
+ (path) => this.readSummary(path)
2692
+ );
2693
+ return { summaries: scan.items, invalid: scan.invalid };
2694
+ }
2695
+ async scan(directory, managedName, read) {
2696
+ try {
2697
+ await this.prepareDirectory(directory);
2698
+ const entries = await readdir(directory, { withFileTypes: true });
2699
+ const result = { items: [], invalid: [] };
2700
+ for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
2701
+ if (!entry.isFile() || !managedName.test(entry.name)) continue;
2702
+ const path = join(directory, entry.name);
2703
+ try {
2704
+ const item = await read(path);
2705
+ const id = entry.name.slice(0, -".md".length);
2706
+ if (item.id !== id)
2707
+ throw new Error(`file ID ${id} differs from front matter ID ${item.id}`);
2708
+ result.items.push(item);
2709
+ } catch (error) {
2710
+ result.invalid.push({
2711
+ id: entry.name.slice(0, -".md".length),
2712
+ path,
2713
+ error: error.message.split("\n")[0]?.slice(0, 300) ?? "unreadable"
2714
+ });
2715
+ }
2716
+ }
2717
+ return result;
2718
+ } catch (error) {
2719
+ if (error.code === "ENOENT") return { items: [], invalid: [] };
2720
+ throw error;
2721
+ }
2722
+ }
2723
+ async deleteFact(id) {
2724
+ await rm(this.factPath(id), { force: true });
2725
+ }
2726
+ async markIncidentResolved(id, resolvedBy) {
2727
+ const fact = await this.readFact(id);
2728
+ await this.writeFact(copyFact(fact, { resolvedBy }));
2729
+ }
2730
+ async markFactSuperseded(id, supersededBy) {
2731
+ const fact = await this.readFact(id);
2732
+ await this.writeFact(copyFact(fact, { supersededBy }));
2733
+ }
2734
+ async setFactSharedWith(id, sharedWith) {
2735
+ const fact = await this.readFact(id);
2736
+ await this.writeFact(copyFact(fact, { sharedWith: [...new Set(sharedWith)].sort() }));
2737
+ }
2738
+ /** Persist interaction deletion in Markdown before the derived DB is updated. */
2739
+ async markSourceInteractionDeleted(namespace, sourceInteractionId) {
2740
+ const { facts: facts2 } = await this.scanFacts();
2741
+ for (const fact of facts2) {
2742
+ if (fact.namespace !== namespace || fact.sourceInteractionId !== sourceInteractionId)
2743
+ continue;
2744
+ if (fact.sourceDeleted) continue;
2745
+ await this.writeFact(copyFact(fact, { sourceDeleted: true }));
2746
+ }
2747
+ }
2748
+ /** Persist that a fact's source interaction was forgotten, keeping its provenance id. */
2749
+ async markFactSourceDeleted(id) {
2750
+ const fact = await this.readFact(id);
2751
+ await this.writeFact(copyFact(fact, { sourceDeleted: true }));
2752
+ }
2753
+ factPath(id) {
2754
+ if (!/^fact_[a-z0-9]+$/.test(id)) {
2755
+ throw new Error(`Invalid fact id: ${id}`);
2756
+ }
2757
+ return this.safePath(join(this.factsDirectory, `${id}.md`));
2758
+ }
2759
+ summaryPath(id) {
2760
+ if (!/^sum_[a-z0-9]+$/.test(id)) throw new Error(`Invalid summary id: ${id}`);
2761
+ return this.safePath(join(this.summariesDirectory, `${id}.md`));
2762
+ }
2763
+ safePath(path) {
2764
+ const absolute = resolve2(path);
2765
+ const relativePath = relative(this.repositoryRoot, absolute);
2766
+ if (relativePath === ".." || relativePath.startsWith(`..${sep}`) || relativePath === "") {
2767
+ throw new Error(`Memory path escapes repository root: ${path}`);
2768
+ }
2769
+ return absolute;
2770
+ }
2771
+ async prepareDirectory(directory) {
2772
+ await assertNoSymlinkBetween(this.repositoryRoot, dirname2(directory));
2773
+ await mkdir(directory, { recursive: true, mode: 448 });
2774
+ }
2775
+ };
2776
+ function copyFact(fact, patch) {
2777
+ const { path: _path, schemaVersion: _schemaVersion, type: _type, ...write } = fact;
2778
+ return { ...write, ...patch };
2779
+ }
2780
+ function stripUndefinedAndDefaultIncidentFields(metadata) {
2781
+ return Object.fromEntries(
2782
+ Object.entries(metadata).filter(([key, value]) => {
2783
+ if (value === void 0) return false;
2784
+ if (key === "category" && value === "general") return false;
2785
+ if ((key === "paths" || key === "sharedWith") && Array.isArray(value) && value.length === 0)
2786
+ return false;
2787
+ if (["severity", "introducedBy", "resolvedBy", "propositionKey"].includes(key) && (value === null || value === void 0)) {
2788
+ return false;
2789
+ }
2790
+ return true;
2791
+ })
2792
+ );
2793
+ }
2794
+ function normalizeMemoryPath(path) {
2795
+ return path.replace(/\\/g, "/").replace(/^\.\//, "");
2796
+ }
2797
+ function severityRank(severity) {
2798
+ switch (severity) {
2799
+ case "critical":
2800
+ return 4;
2801
+ case "high":
2802
+ return 3;
2803
+ case "medium":
2804
+ return 2;
2805
+ case "low":
2806
+ return 1;
2807
+ }
2808
+ }
2809
+
2810
+ // src/core/worker.ts
2811
+ var DEFAULT_POLL_MS = 250;
2812
+ var DEFAULT_BATCH_SIZE = 8;
2813
+ var DEFAULT_MAX_ATTEMPTS = 5;
2814
+ var ERROR_BACKOFF_MS = 2e3;
2815
+ var EmbeddingWorker = class {
2816
+ repo;
2817
+ provider;
2818
+ pollIntervalMs;
2819
+ batchSize;
2820
+ maxAttempts;
2821
+ onDiagnostic;
2822
+ state = "idle";
2823
+ loopPromise = null;
2824
+ wake = null;
2825
+ idleResolvers = [];
2826
+ inFlight = 0;
2827
+ constructor(options) {
2828
+ this.repo = options.repository;
2829
+ this.provider = options.provider;
2830
+ this.pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_MS;
2831
+ this.batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE;
2832
+ this.maxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
2833
+ this.onDiagnostic = options.onDiagnostic;
2834
+ }
2835
+ getState() {
2836
+ return this.state;
2837
+ }
2838
+ start() {
2839
+ if (this.loopPromise !== null) return;
2840
+ this.state = "running";
2841
+ this.loopPromise = this.loop().finally(() => {
2842
+ this.loopPromise = null;
2843
+ if (this.state === "running") this.state = "idle";
2844
+ });
2845
+ this.loopPromise.catch(() => {
2846
+ });
2847
+ }
2848
+ notify() {
2849
+ if (this.state === "idle" && this.loopPromise === null) this.start();
2850
+ if (this.wake) {
2851
+ const fn = this.wake;
2852
+ this.wake = null;
2853
+ fn();
2854
+ }
2855
+ }
2856
+ /**
2857
+ * Resolve once the worker reaches an empty poll (no in-flight jobs and
2858
+ * nothing left to claim). Retained for API compatibility: with
2859
+ * repository-side leases and backoff there is no unbounded claim window to
2860
+ * wait out anymore.
2861
+ */
2862
+ async drain() {
2863
+ await new Promise((resolve3) => {
2864
+ this.idleResolvers.push(resolve3);
2865
+ this.notify();
2866
+ });
2867
+ }
2868
+ async stop() {
2869
+ if (this.state === "stopped" || this.state === "idle") {
2870
+ this.state = "stopped";
2871
+ return;
2872
+ }
2873
+ this.state = "draining";
2874
+ this.notify();
2875
+ if (this.loopPromise) {
2876
+ await this.loopPromise;
2877
+ }
2878
+ this.state = "stopped";
2879
+ }
2880
+ async shutdown() {
2881
+ await this.stop();
2882
+ }
2883
+ getInFlight() {
2884
+ return this.inFlight;
2885
+ }
2886
+ async loop() {
2887
+ while (this.state === "running") {
2888
+ let jobs;
2889
+ try {
2890
+ jobs = await this.repo.claimPendingEmbeddings(this.batchSize);
2891
+ } catch (error) {
2892
+ console.error(
2893
+ `[worker] claimPendingEmbeddings failed: ${error instanceof Error ? error.message : String(error)}; retrying`
2894
+ );
2895
+ if (this.state !== "running") break;
2896
+ await new Promise((resolve3) => setTimeout(resolve3, ERROR_BACKOFF_MS));
2897
+ continue;
2898
+ }
2899
+ if (jobs.length === 0) {
2900
+ this.flushIdleResolvers();
2901
+ if (this.state !== "running") break;
2902
+ await this.sleep();
2903
+ continue;
2904
+ }
2905
+ this.inFlight += jobs.length;
2906
+ try {
2907
+ const outcomes = await Promise.allSettled(jobs.map((job) => this.processJob(job)));
2908
+ for (const outcome of outcomes) {
2909
+ if (outcome.status === "rejected") {
2910
+ console.error(`[worker] embedding job failed unexpectedly: ${String(outcome.reason)}`);
2911
+ }
2912
+ }
2913
+ } finally {
2914
+ this.inFlight -= jobs.length;
2915
+ }
2916
+ }
2917
+ this.flushIdleResolvers();
2918
+ }
2919
+ flushIdleResolvers() {
2920
+ if (this.inFlight > 0) return;
2921
+ const resolvers = this.idleResolvers;
2922
+ this.idleResolvers = [];
2923
+ for (const resolve3 of resolvers) resolve3();
2924
+ }
2925
+ async processJob(job) {
2926
+ let leaseLost = false;
2927
+ const renewal = setInterval(
2928
+ () => {
2929
+ void this.repo.renewEmbeddingLease(job.id, job.leaseToken).then((renewed) => {
2930
+ if (!renewed) leaseLost = true;
2931
+ }).catch(() => {
2932
+ leaseLost = true;
2933
+ });
2934
+ },
2935
+ Math.max(250, Math.floor(job.leaseMs / 3))
2936
+ );
2937
+ try {
2938
+ const response = await this.provider.embed({
2939
+ input: job.content,
2940
+ model: job.embeddingModel
2941
+ });
2942
+ validateEmbeddingResponse(response, 1);
2943
+ const vector2 = response.vectors[0];
2944
+ if (!vector2 || vector2.length === 0) {
2945
+ throw new Error("Provider returned no embedding vector.");
2946
+ }
2947
+ if (leaseLost || !await this.repo.markEmbeddingReady(
2948
+ job.id,
2949
+ job.leaseToken,
2950
+ vector2,
2951
+ response.model,
2952
+ embeddingSpaceKey(embeddingSpace(response))
2953
+ ))
2954
+ return;
2955
+ await this.emitDiagnostic({
2956
+ id: generateEntityId("mc"),
2957
+ namespaceId: job.namespaceId,
2958
+ metadata: {
2959
+ model: response.model,
2960
+ type: "embedding",
2961
+ status: "success",
2962
+ latencyMs: 0,
2963
+ retries: 0,
2964
+ fallbackTriggered: false,
2965
+ coldStartWaitMs: 0
2966
+ }
2967
+ });
2968
+ } catch (error) {
2969
+ const attempts = job.attempts + 1;
2970
+ const message = error instanceof Error ? error.message : String(error);
2971
+ if (leaseLost) return;
2972
+ try {
2973
+ if (!await this.repo.markEmbeddingFailed(job.id, job.leaseToken, message, attempts, {
2974
+ maxAttempts: this.maxAttempts
2975
+ }))
2976
+ return;
2977
+ } catch (persistenceError) {
2978
+ console.error(
2979
+ `[worker] could not persist embedding failure: ${persistenceError instanceof Error ? persistenceError.message : String(persistenceError)}`
2980
+ );
2981
+ return;
2982
+ }
2983
+ await this.emitDiagnostic({
2984
+ id: generateEntityId("mc"),
2985
+ namespaceId: job.namespaceId,
2986
+ metadata: {
2987
+ model: job.embeddingModel,
2988
+ type: "embedding",
2989
+ status: attempts >= this.maxAttempts ? "failed" : "fallback",
2990
+ latencyMs: 0,
2991
+ error: message,
2992
+ retries: attempts,
2993
+ fallbackTriggered: false,
2994
+ coldStartWaitMs: 0
2995
+ }
2996
+ });
2997
+ } finally {
2998
+ clearInterval(renewal);
2999
+ }
3000
+ }
3001
+ emitDiagnostic(entry) {
3002
+ const sink = this.onDiagnostic;
3003
+ if (!sink) return Promise.resolve();
3004
+ return Promise.resolve().then(() => sink(entry)).catch(() => {
3005
+ });
3006
+ }
3007
+ sleep(ms) {
3008
+ return new Promise((resolve3) => {
3009
+ const timer = setTimeout(() => {
3010
+ this.wake = null;
3011
+ resolve3();
3012
+ }, ms ?? this.pollIntervalMs);
3013
+ this.wake = () => {
3014
+ clearTimeout(timer);
3015
+ resolve3();
3016
+ };
3017
+ });
3018
+ }
3019
+ };
3020
+
3021
+ // src/core/codebuddy.ts
3022
+ var RECALL_EMBED_TIMEOUT_MS = 1e4;
3023
+ var CodeBuddy = class {
3024
+ config;
3025
+ repo;
3026
+ provider;
3027
+ worker;
3028
+ policy;
3029
+ tracer;
3030
+ memoryFileStore;
3031
+ namespaceId = null;
3032
+ initialized = false;
3033
+ constructor(config, deps) {
3034
+ this.config = validateConfig(config);
3035
+ if (!deps?.repository) {
3036
+ throw new Error(
3037
+ "CodeBuddy requires a MemoryRepository. Pass deps.repository (use createPostgresRepository for production)."
3038
+ );
3039
+ }
3040
+ this.repo = deps.repository;
3041
+ this.memoryFileStore = deps.memoryFileStore;
3042
+ this.provider = deps.provider ?? buildProviderFromConfig(this.config.provider);
3043
+ this.worker = deps.worker ?? new EmbeddingWorker({
3044
+ repository: this.repo,
3045
+ provider: this.provider,
3046
+ ...this.config.worker?.pollIntervalMs !== void 0 ? { pollIntervalMs: this.config.worker.pollIntervalMs } : {},
3047
+ ...this.config.worker?.batchSize !== void 0 ? { batchSize: this.config.worker.batchSize } : {},
3048
+ ...this.config.worker?.maxAttempts !== void 0 ? { maxAttempts: this.config.worker.maxAttempts } : {}
3049
+ });
3050
+ this.policy = deps.policy ?? new DefaultPolicy();
3051
+ this.tracer = deps.tracer ?? createTracer();
3052
+ }
3053
+ async init(options = {}) {
3054
+ const namespace = await this.repo.ensureNamespace(this.config.namespace);
3055
+ this.namespaceId = namespace.id;
3056
+ this.initialized = true;
3057
+ if (options.startWorker !== false) this.worker.start();
3058
+ return {
3059
+ ready: true,
3060
+ phase: "runtime",
3061
+ message: `CodeBuddy ready in namespace ${namespace.name}.`,
3062
+ namespaceId: namespace.id
3063
+ };
3064
+ }
3065
+ async shutdown() {
3066
+ await this.worker.shutdown();
3067
+ }
3068
+ getWorker() {
3069
+ return this.worker;
3070
+ }
3071
+ async remember(input) {
3072
+ const namespaceId = this.requireNamespaceId();
3073
+ const type = input.type ?? "interaction";
3074
+ const sessionId = input.sessionId ?? generateSessionId();
3075
+ const content = input.content;
3076
+ if (!content || content.length === 0) {
3077
+ throw new Error("remember: content must not be empty.");
3078
+ }
3079
+ const result = await this.repo.withSessionLock(
3080
+ namespaceId,
3081
+ sessionId,
3082
+ () => this.writeOne({
3083
+ namespaceId,
3084
+ sessionId,
3085
+ type,
3086
+ content,
3087
+ idempotencyKey: input.idempotencyKey,
3088
+ ...input.agentId !== void 0 ? { agentId: input.agentId } : {}
3089
+ })
3090
+ );
3091
+ this.worker.notify();
3092
+ return {
3093
+ id: result.id,
3094
+ sessionId: result.sessionId,
3095
+ deduplicated: result.deduplicated
3096
+ };
3097
+ }
3098
+ async rememberBatch(items) {
3099
+ if (!Array.isArray(items) || items.length === 0) return [];
3100
+ const results = [];
3101
+ for (const item of items) {
3102
+ results.push(await this.remember(item));
3103
+ }
3104
+ return results;
3105
+ }
3106
+ async recall(input) {
3107
+ const namespaceId = this.requireNamespaceId();
3108
+ if (!input.query || input.query.length === 0) {
3109
+ throw new Error("recall: query must not be empty.");
3110
+ }
3111
+ const requested = input.budget ?? this.config.tokenBudget ?? 4e3;
3112
+ const clamp = clampBudgetForCallerModel(requested, input.callerModel);
3113
+ const queryEmbedding = await this.embedQueryWithFallback(input.query);
3114
+ const planInput = {
3115
+ namespace: this.config.namespace,
3116
+ namespaceId,
3117
+ sessionId: input.sessionId,
3118
+ query: input.query,
3119
+ budget: clamp.budget,
3120
+ conflictMode: input.conflictMode ?? "all",
3121
+ queryEmbedding,
3122
+ repository: this.repo,
3123
+ ...input.callerModel !== void 0 ? { callerModel: input.callerModel } : {}
3124
+ };
3125
+ const planned = await this.tracer.withSpan(
3126
+ "codebuddy.recall",
3127
+ {
3128
+ namespace: this.config.namespace,
3129
+ sessionId: input.sessionId,
3130
+ budget: clamp.budget,
3131
+ conflictMode: planInput.conflictMode,
3132
+ callerModel: input.callerModel ?? null
3133
+ },
3134
+ () => this.policy.plan(planInput)
3135
+ );
3136
+ planned.stats.budgetClamped = clamp.clamped;
3137
+ if (clamp.clamped && clamp.reason) {
3138
+ planned.stats.budgetClampReason = clamp.reason;
3139
+ }
3140
+ return planned;
3141
+ }
3142
+ /**
3143
+ * Dense evidence for file facts supplied by the context engine: the cosine of
3144
+ * each fact whose stored vector clears the embedding model's calibrated
3145
+ * `relevanceFloor` against the task (exact pgvector search over the
3146
+ * namespace). Facts below the floor, of unknown models, or on any failure or
3147
+ * timeout are simply absent, so callers fall back to lexical evidence.
3148
+ */
3149
+ async rankContextFacts(task, factIds, options = {}) {
3150
+ if (!task.trim() || factIds.length === 0) return {};
3151
+ const allowed = new Set(factIds.slice(0, 256));
3152
+ try {
3153
+ return await pTimeout2(
3154
+ (async () => {
3155
+ const query = await this.embedQueryWithFallback(task.slice(0, 4096));
3156
+ const floor = query ? this.config.provider.relevanceFloor ?? getModelDefinition(query.space.model)?.relevanceFloor : void 0;
3157
+ if (!query || floor === void 0) return {};
3158
+ const result = await this.repo.searchSimilarEmbeddings({
3159
+ namespaceId: this.requireNamespaceId(),
3160
+ vector: query.vector,
3161
+ embeddingSpace: embeddingSpaceKey(query.space),
3162
+ embeddingModel: query.space.model,
3163
+ limit: 256
3164
+ });
3165
+ const scores = {};
3166
+ for (const row of result.candidates) {
3167
+ if (row.ownerType === "fact" && allowed.has(row.ownerId) && Number.isFinite(row.score) && row.score >= floor) {
3168
+ scores[row.ownerId] = Math.max(scores[row.ownerId] ?? 0, Math.min(1, row.score));
3169
+ }
3170
+ }
3171
+ return scores;
3172
+ })(),
3173
+ { milliseconds: options.timeoutMs ?? 1500 }
3174
+ );
3175
+ } catch {
3176
+ return {};
3177
+ }
3178
+ }
3179
+ async embedQueryWithFallback(query) {
3180
+ try {
3181
+ const response = await this.tracer.withSpan(
3182
+ "codebuddy.recall.embed_query",
3183
+ { length: query.length },
3184
+ () => pTimeout2(this.provider.embed({ input: query }), {
3185
+ milliseconds: RECALL_EMBED_TIMEOUT_MS
3186
+ })
3187
+ );
3188
+ validateEmbeddingResponse(response, 1);
3189
+ const vector2 = response.vectors[0];
3190
+ return vector2 ? { vector: vector2, space: embeddingSpace(response) } : null;
3191
+ } catch {
3192
+ return null;
3193
+ }
3194
+ }
3195
+ /**
3196
+ * Share facts owned by the configured namespace. A reference is a live grant
3197
+ * recorded durably in the source fact's Markdown (`sharedWith`); a snapshot is
3198
+ * a copy owned by the target and written to the target project's own store.
3199
+ */
3200
+ async share(input) {
3201
+ if (!input.from || !input.to) {
3202
+ throw new Error("share: both 'from' and 'to' namespaces are required.");
3203
+ }
3204
+ if (input.from === input.to) {
3205
+ throw new Error("share: 'from' and 'to' must differ.");
3206
+ }
3207
+ const fromNamespaceId = this.requireNamespaceId();
3208
+ if (input.from !== this.config.namespace) {
3209
+ throw new Error(
3210
+ `share: only facts of the configured namespace "${this.config.namespace}" can be shared.`
3211
+ );
3212
+ }
3213
+ if (!Array.isArray(input.factIds) || input.factIds.length === 0) {
3214
+ return { shared: 0 };
3215
+ }
3216
+ const mode = input.mode ?? "reference";
3217
+ let targetStore;
3218
+ if (mode === "snapshot") {
3219
+ if (input.targetProjectRoot) {
3220
+ targetStore = await resolveTargetStore(input.targetProjectRoot, input.to);
3221
+ } else if (this.memoryFileStore) {
3222
+ throw new Error(
3223
+ "share: snapshot mode requires targetProjectRoot so the copy is durable in the target project's Markdown store."
3224
+ );
3225
+ }
3226
+ }
3227
+ const toNs = await this.repo.ensureNamespace(input.to);
3228
+ let shared = 0;
3229
+ const embeddingModel = DEFAULT_EMBEDDING_MODELS[0]?.id ?? "unknown";
3230
+ for (const factId of input.factIds) {
3231
+ const auditId = generateEntityId("aud");
3232
+ const shareId = generateEntityId("shr");
3233
+ const source = await this.repo.getFactById(fromNamespaceId, factId);
3234
+ if (!source) {
3235
+ throw new Error(`share: fact ${factId} not found in namespace "${input.from}".`);
3236
+ }
3237
+ if (mode === "reference") {
3238
+ const write = {
3239
+ id: shareId,
3240
+ fromNamespaceId,
3241
+ toNamespaceId: toNs.id,
3242
+ sourceFactId: factId,
3243
+ mode: "reference",
3244
+ ...input.agentId !== void 0 ? { createdByAgent: input.agentId } : {}
3245
+ };
3246
+ await this.repo.shareReference(write);
3247
+ await this.updateSharedWith(factId, (current) => [...current, toNs.name]);
3248
+ } else {
3249
+ const snapshotContent = [
3250
+ `[shared from ${input.from}] ${source.subject} ${source.predicate} ${source.object}`,
3251
+ source.content
3252
+ ].join("\n");
3253
+ const snapshotFactId = generateEntityId("fact");
3254
+ const snapshotEmbeddingId = generateEntityId("emb");
3255
+ const snapshotFact = {
3256
+ id: snapshotFactId,
3257
+ namespaceId: toNs.id,
3258
+ content: snapshotContent,
3259
+ contentHash: computeFactHash(toNs.name, snapshotContent),
3260
+ subject: source.subject,
3261
+ predicate: "snapshot_of",
3262
+ object: `${input.from}:${factId}`,
3263
+ ...input.agentId !== void 0 ? { createdByAgent: input.agentId } : {}
3264
+ };
3265
+ const embedding = {
3266
+ id: snapshotEmbeddingId,
3267
+ namespaceId: toNs.id,
3268
+ ownerType: "fact",
3269
+ ownerId: snapshotFactId,
3270
+ embeddingModel
3271
+ };
3272
+ const write = {
3273
+ id: shareId,
3274
+ fromNamespaceId,
3275
+ toNamespaceId: toNs.id,
3276
+ sourceFactId: factId,
3277
+ mode: "snapshot",
3278
+ snapshotFactId,
3279
+ ...input.agentId !== void 0 ? { createdByAgent: input.agentId } : {}
3280
+ };
3281
+ const result = await this.repo.shareSnapshot({ share: write, snapshotFact, embedding });
3282
+ if (targetStore)
3283
+ await writeSnapshotFile(this.repo, targetStore, toNs, result.snapshotFactId);
3284
+ }
3285
+ const audit = {
3286
+ id: auditId,
3287
+ namespaceId: fromNamespaceId,
3288
+ action: "share",
3289
+ entityType: "fact",
3290
+ entityId: factId,
3291
+ ...input.agentId !== void 0 ? { createdByAgent: input.agentId } : {},
3292
+ metadata: { mode, toNamespace: toNs.name, shareId }
3293
+ };
3294
+ await this.repo.recordAudit(audit);
3295
+ shared += 1;
3296
+ }
3297
+ this.worker.notify();
3298
+ return { shared };
3299
+ }
3300
+ /** Revoke live reference shares of configured-namespace facts to `to`. */
3301
+ async unshare(input) {
3302
+ const fromNamespaceId = this.requireNamespaceId();
3303
+ const toNs = await this.repo.getNamespaceByName(input.to);
3304
+ if (!toNs || input.factIds.length === 0) return { revoked: 0 };
3305
+ const revoked = await this.repo.revokeShares(fromNamespaceId, toNs.id, input.factIds);
3306
+ for (const factId of input.factIds) {
3307
+ await this.updateSharedWith(factId, (current) => current.filter((ns) => ns !== toNs.name));
3308
+ }
3309
+ if (revoked > 0) {
3310
+ await this.repo.recordAudit({
3311
+ id: generateEntityId("aud"),
3312
+ namespaceId: fromNamespaceId,
3313
+ action: "share",
3314
+ entityType: "fact",
3315
+ ...input.agentId !== void 0 ? { createdByAgent: input.agentId } : {},
3316
+ metadata: { mode: "reference", toNamespace: toNs.name, revoked: input.factIds }
3317
+ });
3318
+ }
3319
+ return { revoked };
3320
+ }
3321
+ /** Keep the source fact's durable `sharedWith` list in step with the shares table. */
3322
+ async updateSharedWith(factId, next) {
3323
+ if (!this.memoryFileStore) return;
3324
+ let file;
3325
+ try {
3326
+ file = await this.memoryFileStore.readFact(factId);
3327
+ } catch (error) {
3328
+ if (error.code === "ENOENT") return;
3329
+ throw error;
3330
+ }
3331
+ if (file.namespace !== this.config.namespace) return;
3332
+ await this.memoryFileStore.setFactSharedWith(factId, next(file.sharedWith));
3333
+ }
3334
+ async forget(id) {
3335
+ if (!id) throw new Error("forget: id is required.");
3336
+ const namespaceId = this.requireNamespaceId();
3337
+ const interactionAudit = {
3338
+ id: generateEntityId("aud"),
3339
+ namespaceId,
3340
+ action: "forget",
3341
+ entityType: "interaction",
3342
+ entityId: id
3343
+ };
3344
+ const interactionResult = await this.repo.withSessionLock(
3345
+ namespaceId,
3346
+ REINDEX_LOCK_KEY,
3347
+ async () => {
3348
+ const existing = await this.repo.getInteractionsByIds(namespaceId, [id]);
3349
+ if (existing.length === 0) return { found: false, sourceDeletedFactIds: [] };
3350
+ await this.memoryFileStore?.markSourceInteractionDeleted(this.config.namespace, id);
3351
+ return this.repo.forgetInteraction(namespaceId, id, interactionAudit);
3352
+ }
3353
+ );
3354
+ if (interactionResult.found) {
3355
+ for (const factId of interactionResult.sourceDeletedFactIds) {
3356
+ const file = await this.memoryFileStore?.readFact(factId).catch(() => null);
3357
+ if (file?.namespace === this.config.namespace) {
3358
+ await this.memoryFileStore?.markFactSourceDeleted(factId);
3359
+ }
3360
+ }
3361
+ return { ok: true, entityType: "interaction" };
3362
+ }
3363
+ const factAudit = {
3364
+ id: generateEntityId("aud"),
3365
+ namespaceId,
3366
+ action: "forget",
3367
+ entityType: "fact",
3368
+ entityId: id
3369
+ };
3370
+ const factResult = await this.repo.withSessionLock(namespaceId, REINDEX_LOCK_KEY, async () => {
3371
+ const result = await this.repo.forgetFact(namespaceId, id, factAudit);
3372
+ if (result.found) {
3373
+ const file = await this.memoryFileStore?.readFact(id).catch(() => null);
3374
+ if (file?.namespace === this.config.namespace) await this.memoryFileStore?.deleteFact(id);
3375
+ }
3376
+ return result;
3377
+ });
3378
+ if (factResult.found) return { ok: true, entityType: "fact" };
3379
+ return { ok: false, entityType: "unknown" };
3380
+ }
3381
+ /**
3382
+ * Reconcile the derived database with the Markdown memory files (the source
3383
+ * of truth) for this namespace: upsert by stable ID, preserving the file's
3384
+ * lifecycle metadata; re-embed only changed content; delete rows whose file
3385
+ * is gone. Only rows a reindex previously imported from Markdown
3386
+ * (`file_backed`) are ever deleted, so DB-only legacy memory survives. Each
3387
+ * file is applied independently: malformed or conflicting files are reported
3388
+ * in `failures` and never abort healthy imports (nor count as deletions).
3389
+ */
3390
+ async reindexMemory() {
3391
+ const namespaceId = this.requireNamespaceId();
3392
+ const store = this.memoryFileStore;
3393
+ if (!store) {
3394
+ throw new Error("Fact reindexing requires a repository-rooted MemoryFileStore.");
3395
+ }
3396
+ const namespace = this.config.namespace;
3397
+ const result = await this.repo.withSessionLock(namespaceId, REINDEX_LOCK_KEY, async () => {
3398
+ const [factScan, summaryScan] = await Promise.all([store.scanFacts(), store.scanSummaries()]);
3399
+ const facts2 = factScan.facts.filter((file) => isEligibleMemory(file, { namespace }));
3400
+ const summaries = summaryScan.summaries.filter((file) => file.namespace === namespace);
3401
+ const failures = [
3402
+ ...factScan.invalid.map((file) => ({ kind: "fact", ...file })),
3403
+ ...summaryScan.invalid.map((file) => ({ kind: "summary", ...file }))
3404
+ ];
3405
+ const factCounts = emptyCounts(
3406
+ facts2.length + factScan.invalid.length,
3407
+ factScan.invalid.length
3408
+ );
3409
+ const summaryCounts = emptyCounts(
3410
+ summaries.length + summaryScan.invalid.length,
3411
+ summaryScan.invalid.length
3412
+ );
3413
+ const apply = async (kind, file, counts, write) => {
3414
+ try {
3415
+ const { status } = await write();
3416
+ counts[status === "inserted" ? "imported" : status] += 1;
3417
+ return true;
3418
+ } catch (error) {
3419
+ counts.failed += 1;
3420
+ failures.push({ kind, id: file.id, path: file.path, error: describeError(error) });
3421
+ return false;
3422
+ }
3423
+ };
3424
+ const present = /* @__PURE__ */ new Set([
3425
+ ...facts2.map((file) => file.id),
3426
+ ...summaries.map((file) => file.id),
3427
+ ...factScan.invalid.map((file) => file.id),
3428
+ ...summaryScan.invalid.map((file) => file.id)
3429
+ ]);
3430
+ const owned = await this.repo.listFileBackedIds(namespaceId);
3431
+ for (const id of owned.facts.filter((id2) => !present.has(id2))) {
3432
+ const removed = await this.repo.forgetFact(
3433
+ namespaceId,
3434
+ id,
3435
+ reindexAudit(namespaceId, "fact", id)
3436
+ );
3437
+ if (removed.found) factCounts.deleted += 1;
3438
+ }
3439
+ for (const id of owned.summaries.filter((id2) => !present.has(id2))) {
3440
+ const removed = await this.repo.forgetSummary(
3441
+ namespaceId,
3442
+ id,
3443
+ reindexAudit(namespaceId, "summary", id)
3444
+ );
3445
+ if (removed.found) summaryCounts.deleted += 1;
3446
+ }
3447
+ for (const file of facts2) {
3448
+ const imported = await apply(
3449
+ "fact",
3450
+ file,
3451
+ factCounts,
3452
+ () => this.importFactFile(namespaceId, file)
3453
+ );
3454
+ if (imported) await this.restoreShares(namespaceId, file);
3455
+ }
3456
+ for (const file of summaries) {
3457
+ await apply(
3458
+ "summary",
3459
+ file,
3460
+ summaryCounts,
3461
+ () => this.importSummaryFile(namespaceId, file)
3462
+ );
3463
+ }
3464
+ return {
3465
+ facts: factCounts,
3466
+ summaries: summaryCounts,
3467
+ failures: failures.slice(0, MAX_REPORTED_FAILURES)
3468
+ };
3469
+ });
3470
+ const space = this.provider.space;
3471
+ const canEmbed = space !== void 0 && await this.provider.embed({ input: "codebuddy" }).then(
3472
+ () => true,
3473
+ () => false
3474
+ );
3475
+ const reembedding = space && canEmbed ? await this.repo.requeueEmbeddingsOutsideSpace(namespaceId, embeddingSpaceKey(space)) : 0;
3476
+ this.worker.notify();
3477
+ return { ...result, reembedding };
3478
+ }
3479
+ async reindexFacts() {
3480
+ return (await this.reindexMemory()).facts;
3481
+ }
3482
+ requireNamespaceId() {
3483
+ if (!this.initialized || !this.namespaceId) {
3484
+ throw new Error("CodeBuddy.init() must complete before this method is called.");
3485
+ }
3486
+ return this.namespaceId;
3487
+ }
3488
+ async writeOne(input) {
3489
+ const embeddingModel = DEFAULT_EMBEDDING_MODELS[0]?.id ?? "unknown";
3490
+ const ownerId = generateEntityId(prefixForType(input.type));
3491
+ const claim = redactPersisted({
3492
+ subject: "__inline__",
3493
+ predicate: "asserts",
3494
+ object: input.content.slice(0, 200),
3495
+ content: input.content
3496
+ });
3497
+ const content = input.type === "fact" ? claim.content : input.content;
3498
+ const contentHash = input.idempotencyKey ?? (input.type === "fact" ? factContentHash(this.config.namespace, ownerId, content) : computeContentHash(this.config.namespace, input.sessionId, input.content));
3499
+ const tokenCount = estimateTokenCount(content);
3500
+ const embeddingId = generateEntityId("emb");
3501
+ const embedding = {
3502
+ id: embeddingId,
3503
+ namespaceId: input.namespaceId,
3504
+ ownerType: input.type,
3505
+ ownerId,
3506
+ embeddingModel
3507
+ };
3508
+ const audit = {
3509
+ id: generateEntityId("aud"),
3510
+ namespaceId: input.namespaceId,
3511
+ action: "remember",
3512
+ entityType: input.type,
3513
+ entityId: ownerId,
3514
+ ...input.agentId !== void 0 ? { createdByAgent: input.agentId } : {},
3515
+ metadata: { sessionId: input.sessionId, type: input.type }
3516
+ };
3517
+ if (input.type === "interaction") {
3518
+ const interaction = {
3519
+ id: ownerId,
3520
+ namespaceId: input.namespaceId,
3521
+ sessionId: input.sessionId,
3522
+ content: input.content,
3523
+ contentHash,
3524
+ tokenCount,
3525
+ ...input.agentId !== void 0 ? { createdByAgent: input.agentId } : {}
3526
+ };
3527
+ return this.repo.writeInteraction({ interaction, embedding, audit });
3528
+ }
3529
+ if (input.type === "fact") {
3530
+ const fact = {
3531
+ id: ownerId,
3532
+ namespaceId: input.namespaceId,
3533
+ content,
3534
+ contentHash,
3535
+ subject: claim.subject,
3536
+ predicate: claim.predicate,
3537
+ object: claim.object,
3538
+ ...input.agentId !== void 0 ? { createdByAgent: input.agentId } : {}
3539
+ };
3540
+ const result2 = await this.repo.writeFact({ fact, embedding, audit });
3541
+ if (this.memoryFileStore) {
3542
+ if (result2.deduplicated) {
3543
+ try {
3544
+ await this.memoryFileStore.readFact(result2.id);
3545
+ return result2;
3546
+ } catch (error) {
3547
+ if (error.code !== "ENOENT") throw error;
3548
+ }
3549
+ }
3550
+ await this.memoryFileStore.writeFact({
3551
+ id: result2.id,
3552
+ namespace: this.config.namespace,
3553
+ subject: fact.subject,
3554
+ predicate: fact.predicate,
3555
+ object: fact.object,
3556
+ confidence: 1,
3557
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3558
+ createdByAgent: input.agentId ?? null,
3559
+ sourceInteractionId: fact.sourceInteractionId ?? null,
3560
+ sourceDeleted: false,
3561
+ content: fact.content
3562
+ });
3563
+ }
3564
+ return result2;
3565
+ }
3566
+ const summary = {
3567
+ id: ownerId,
3568
+ namespaceId: input.namespaceId,
3569
+ sessionId: input.sessionId,
3570
+ content: input.content,
3571
+ tokenCount,
3572
+ ...input.agentId !== void 0 ? { createdByAgent: input.agentId } : {}
3573
+ };
3574
+ const result = await this.repo.writeSummary({ summary, embedding, audit });
3575
+ if (this.memoryFileStore) {
3576
+ if (result.deduplicated) {
3577
+ try {
3578
+ await this.memoryFileStore.readSummary(result.id);
3579
+ return result;
3580
+ } catch (error) {
3581
+ if (error.code !== "ENOENT") throw error;
3582
+ }
3583
+ }
3584
+ await this.memoryFileStore.writeSummary({
3585
+ id: result.id,
3586
+ namespace: this.config.namespace,
3587
+ sessionId: result.sessionId,
3588
+ version: summary.version ?? 1,
3589
+ tokenCount: summary.tokenCount,
3590
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3591
+ createdByAgent: input.agentId ?? null,
3592
+ content: summary.content
3593
+ });
3594
+ }
3595
+ return result;
3596
+ }
3597
+ async restoreShares(namespaceId, file) {
3598
+ for (const to of file.sharedWith) {
3599
+ if (to === this.config.namespace) continue;
3600
+ const toNs = await this.repo.ensureNamespace(to);
3601
+ await this.repo.shareReference({
3602
+ id: generateEntityId("shr"),
3603
+ fromNamespaceId: namespaceId,
3604
+ toNamespaceId: toNs.id,
3605
+ sourceFactId: file.id,
3606
+ mode: "reference"
3607
+ });
3608
+ }
3609
+ }
3610
+ importFactFile(namespaceId, file) {
3611
+ return this.repo.upsertFactFromFile({
3612
+ fact: {
3613
+ id: file.id,
3614
+ namespaceId,
3615
+ content: file.content,
3616
+ contentHash: factContentHash(this.config.namespace, file.id, file.content),
3617
+ subject: file.subject,
3618
+ predicate: file.predicate,
3619
+ object: file.object,
3620
+ confidence: file.confidence,
3621
+ sourceInteractionId: file.sourceInteractionId ?? void 0,
3622
+ sourceDeleted: file.sourceDeleted,
3623
+ supersededBy: file.supersededBy,
3624
+ propositionKey: file.propositionKey ?? null,
3625
+ createdByAgent: file.createdByAgent ?? void 0,
3626
+ createdAt: new Date(file.createdAt)
3627
+ },
3628
+ embedding: pendingEmbedding(namespaceId, "fact", file.id),
3629
+ audit: {
3630
+ id: generateEntityId("aud"),
3631
+ namespaceId,
3632
+ action: "remember",
3633
+ entityType: "fact",
3634
+ entityId: file.id,
3635
+ metadata: { source: "reindex", path: file.path }
3636
+ }
3637
+ });
3638
+ }
3639
+ importSummaryFile(namespaceId, file) {
3640
+ return this.repo.upsertSummaryFromFile({
3641
+ summary: {
3642
+ id: file.id,
3643
+ namespaceId,
3644
+ sessionId: file.sessionId,
3645
+ content: file.content,
3646
+ version: file.version,
3647
+ tokenCount: file.tokenCount,
3648
+ createdByAgent: file.createdByAgent ?? void 0,
3649
+ createdAt: new Date(file.createdAt)
3650
+ },
3651
+ embedding: pendingEmbedding(namespaceId, "summary", file.id),
3652
+ audit: {
3653
+ id: generateEntityId("aud"),
3654
+ namespaceId,
3655
+ action: "remember",
3656
+ entityType: "summary",
3657
+ entityId: file.id,
3658
+ metadata: { source: "reindex", path: file.path }
3659
+ }
3660
+ });
3661
+ }
3662
+ };
3663
+ async function resolveTargetStore(root, namespace) {
3664
+ const resolved = (await readConfigFile(root)).namespace ?? defaultNamespaceForCwd(root);
3665
+ if (resolved !== namespace) {
3666
+ throw new Error(
3667
+ `share: targetProjectRoot ${root} resolves to namespace "${resolved}", not "${namespace}".`
3668
+ );
3669
+ }
3670
+ return new MemoryFileStore(root);
3671
+ }
3672
+ async function writeSnapshotFile(repo, store, namespace, factId) {
3673
+ try {
3674
+ await store.readFact(factId);
3675
+ return;
3676
+ } catch (error) {
3677
+ if (error.code !== "ENOENT") throw error;
3678
+ }
3679
+ const fact = await repo.getFactById(namespace.id, factId);
3680
+ if (!fact)
3681
+ throw new Error(`share: snapshot ${factId} missing from namespace "${namespace.name}".`);
3682
+ await store.writeFact({
3683
+ id: fact.id,
3684
+ namespace: namespace.name,
3685
+ subject: fact.subject,
3686
+ predicate: fact.predicate,
3687
+ object: fact.object,
3688
+ confidence: fact.confidence,
3689
+ createdAt: fact.createdAt.toISOString(),
3690
+ createdByAgent: fact.createdByAgent,
3691
+ sourceInteractionId: null,
3692
+ sourceDeleted: false,
3693
+ content: fact.content
3694
+ });
3695
+ }
3696
+ var REINDEX_LOCK_KEY = "__codebuddy_markdown_reindex__";
3697
+ var MAX_REPORTED_FAILURES = 50;
3698
+ function emptyCounts(scanned, failed) {
3699
+ return { scanned, imported: 0, updated: 0, unchanged: 0, deleted: 0, failed };
3700
+ }
3701
+ function pendingEmbedding(namespaceId, ownerType, ownerId) {
3702
+ return {
3703
+ id: generateEntityId("emb"),
3704
+ namespaceId,
3705
+ ownerType,
3706
+ ownerId,
3707
+ embeddingModel: DEFAULT_EMBEDDING_MODELS[0]?.id ?? "unknown"
3708
+ };
3709
+ }
3710
+ function reindexAudit(namespaceId, entityType, entityId) {
3711
+ return {
3712
+ id: generateEntityId("aud"),
3713
+ namespaceId,
3714
+ action: "forget",
3715
+ entityType,
3716
+ entityId,
3717
+ metadata: { source: "reindex", reason: "no eligible markdown file" }
3718
+ };
3719
+ }
3720
+ function describeError(error) {
3721
+ const cause = error.cause;
3722
+ const message = cause instanceof Error ? cause.message : error.message;
3723
+ return String(message).split("\n")[0]?.slice(0, 300) ?? "unknown error";
3724
+ }
3725
+ function factContentHash(namespace, id, content) {
3726
+ return computeFactHash(namespace, content === REDACTED ? `${REDACTED}${id}` : content);
3727
+ }
3728
+ function prefixForType(type) {
3729
+ if (type === "interaction") return "int";
3730
+ if (type === "fact") return "fact";
3731
+ return "sum";
3732
+ }
3733
+ function estimateTokenCount(content) {
3734
+ return countTokens(content);
3735
+ }
3736
+ function buildProviderFromConfig(provider) {
3737
+ if (provider.type === "local") return new LocalModelProvider();
3738
+ if (provider.type === "openrouter") {
3739
+ const model = provider.model ?? DEFAULT_CLOUD_EMBEDDING_MODELS.openrouter;
3740
+ const apiKey = process.env.OPENROUTER_API_KEY;
3741
+ if (!apiKey) return missingKeyProvider("OPENROUTER_API_KEY", model);
3742
+ return new OpenRouterEmbeddingProvider({ apiKey, model });
3743
+ }
3744
+ return new HuggingFaceProvider({
3745
+ ...provider.apiKey ? { apiKey: provider.apiKey } : {},
3746
+ ...provider.model ? {
3747
+ embeddingModels: [
3748
+ {
3749
+ id: provider.model,
3750
+ role: "primary",
3751
+ kind: "embedding",
3752
+ dimensions: EMBEDDING_DIMENSIONS
3753
+ }
3754
+ ]
3755
+ } : {}
3756
+ });
3757
+ }
3758
+ function missingKeyProvider(variable, model) {
3759
+ const fail = async () => {
3760
+ throw new Error(
3761
+ `${variable} is not set; ${model} cannot embed. Export it or run \`codebuddy models use local\`.`
3762
+ );
3763
+ };
3764
+ return { embed: fail, generateText: fail };
3765
+ }
3766
+
3767
+ // src/core/operations.ts
3768
+ async function createRuntime(config, options = {}) {
3769
+ const runtimeConfig = config ?? await loadRuntimeConfig();
3770
+ if (!runtimeConfig.postgresUrl) throw new Error(MISSING_DATABASE_URL);
3771
+ const client = createDatabaseClient({
3772
+ postgresUrl: runtimeConfig.postgresUrl,
3773
+ ...options.connectTimeoutSeconds !== void 0 ? { connectTimeoutSeconds: options.connectTimeoutSeconds } : {}
3774
+ });
3775
+ try {
3776
+ if (options.autoBootstrap) {
3777
+ await bootstrapDatabase(client.sql);
3778
+ await runMigrations(client);
3779
+ }
3780
+ const repository = createPostgresRepository(client);
3781
+ const memory = new CodeBuddy(runtimeConfig, {
3782
+ repository,
3783
+ memoryFileStore: new MemoryFileStore(
3784
+ options.repositoryRoot ?? runtimeConfig.projectRoot ?? process.cwd()
3785
+ )
3786
+ });
3787
+ await memory.init(options.startWorker === false ? { startWorker: false } : {});
3788
+ return {
3789
+ memory,
3790
+ repository,
3791
+ async close() {
3792
+ await memory.shutdown();
3793
+ await client.close();
3794
+ }
3795
+ };
3796
+ } catch (error) {
3797
+ await client.close().catch(() => void 0);
3798
+ throw error;
3799
+ }
3800
+ }
3801
+ function encodeFactCursor(fact) {
3802
+ return Buffer.from(JSON.stringify([fact.createdAt.toISOString(), fact.id]), "utf8").toString(
3803
+ "base64url"
3804
+ );
3805
+ }
3806
+ function decodeFactCursor(cursor) {
3807
+ let parsed;
3808
+ try {
3809
+ parsed = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
3810
+ } catch {
3811
+ throw new Error("Invalid cursor: expected a base64url-encoded [createdAt, id] pair.");
3812
+ }
3813
+ if (!Array.isArray(parsed) || parsed.length !== 2 || typeof parsed[0] !== "string" || typeof parsed[1] !== "string" || Number.isNaN(Date.parse(parsed[0]))) {
3814
+ throw new Error("Invalid cursor: expected a base64url-encoded [createdAt, id] pair.");
3815
+ }
3816
+ const [createdAt2, id] = parsed;
3817
+ return { createdAt: new Date(createdAt2), id };
3818
+ }
3819
+ async function listFactsPage(memory, repository, input) {
3820
+ const namespace = await repository.ensureNamespace(memory.config.namespace);
3821
+ const requestedLimit = input.limit ?? 50;
3822
+ const limit = Math.max(1, Math.min(100, requestedLimit));
3823
+ const facts2 = await repository.listFacts({
3824
+ namespaceId: namespace.id,
3825
+ ...input.subject ? { subject: input.subject } : {},
3826
+ limit: limit + 1,
3827
+ ...input.cursor ? { cursor: decodeFactCursor(input.cursor) } : {}
3828
+ });
3829
+ const page = facts2.slice(0, limit);
3830
+ const next = facts2.length > limit ? page.at(-1) : void 0;
3831
+ return {
3832
+ facts: page,
3833
+ ...next ? { nextCursor: encodeFactCursor(next) } : {}
3834
+ };
3835
+ }
3836
+ async function inspectNamespace(repository, namespace) {
3837
+ if (!namespace) {
3838
+ return {
3839
+ namespaces: await repository.listNamespaces(),
3840
+ agents: await repository.getAgentWriteBreakdown()
3841
+ };
3842
+ }
3843
+ const row = await repository.getNamespaceByName(namespace);
3844
+ if (!row) return { namespace, exists: false, facts: [], agents: [] };
3845
+ return {
3846
+ namespace,
3847
+ exists: true,
3848
+ facts: await repository.listFacts({ namespaceId: row.id, limit: 50 }),
3849
+ agents: await repository.getAgentWriteBreakdown(row.id)
3850
+ };
3851
+ }
3852
+ async function runDoctor(options = {}) {
3853
+ const fileConfig = await readConfigFile();
3854
+ const scaffold = await checkProjectScaffold();
3855
+ const configPermissions = await checkConfigPermissions();
3856
+ const expectedDefaultNamespace = defaultNamespaceForCwd();
3857
+ const runtimeNamespace = process.env.CODEBUDDY_NAMESPACE ?? fileConfig.namespace ?? options.config?.namespace ?? null;
3858
+ const namespaceReport = {
3859
+ expectedDefault: expectedDefaultNamespace,
3860
+ configured: fileConfig.namespace ?? null,
3861
+ runtime: runtimeNamespace,
3862
+ ok: !fileConfig.namespace || fileConfig.namespace === expectedDefaultNamespace || process.env.CODEBUDDY_NAMESPACE !== void 0,
3863
+ ...!fileConfig.namespace || fileConfig.namespace === expectedDefaultNamespace || process.env.CODEBUDDY_NAMESPACE !== void 0 ? {} : {
3864
+ warning: `Configured namespace "${fileConfig.namespace}" differs from this folder's default "${expectedDefaultNamespace}". Confirm your MCP entries point at the intended namespace.`
3865
+ }
3866
+ };
3867
+ let config = options.config;
3868
+ if (!config) {
3869
+ try {
3870
+ config = await loadRuntimeConfig();
3871
+ } catch (error) {
3872
+ const message = String(redactSecrets(error.message));
3873
+ return {
3874
+ db: {
3875
+ ok: false,
3876
+ error: message,
3877
+ canConnect: false,
3878
+ postgresUrlSource: "missing"
3879
+ },
3880
+ pgvector: { ok: false, error: "Postgres is not configured yet." },
3881
+ vectors: { count: 0, index: null, needsTuning: false },
3882
+ models: await checkModels(options.provider, true),
3883
+ usage: {
3884
+ last60s: 0,
3885
+ lastHour: 0,
3886
+ last24h: 0,
3887
+ estimatedDailyCapRemaining: 1e4,
3888
+ warning80Percent: false
3889
+ },
3890
+ scaffold,
3891
+ config: configPermissions,
3892
+ namespace: namespaceReport
3893
+ };
3894
+ }
3895
+ }
3896
+ let repository = options.repository;
3897
+ let close;
3898
+ let db = { ok: true };
3899
+ let pgvector = { ok: true };
3900
+ if (!repository) {
3901
+ const postgresUrlSource = options.config?.postgresUrl !== void 0 ? "explicit" : process.env.DATABASE_URL ? "environment" : fileConfig.postgresUrl ? "config" : "missing";
3902
+ const client = createDatabaseClient({ postgresUrl: config.postgresUrl });
3903
+ repository = createPostgresRepository(client);
3904
+ close = () => client.close();
3905
+ try {
3906
+ const canConnect = await pingDatabase(client);
3907
+ db = { ok: canConnect, canConnect, postgresUrlSource };
3908
+ if (!canConnect) db.error = "Postgres did not respond to a simple ping.";
3909
+ } catch (error) {
3910
+ db = {
3911
+ ok: false,
3912
+ canConnect: false,
3913
+ postgresUrlSource,
3914
+ error: String(redactSecrets(error.message))
3915
+ };
3916
+ }
3917
+ try {
3918
+ const health = await checkPgvector(client.sql);
3919
+ pgvector = {
3920
+ ok: health.available,
3921
+ ...health.available ? {} : { error: "pgvector extension is not installed." }
3922
+ };
3923
+ } catch (error) {
3924
+ pgvector = { ok: false, error: String(redactSecrets(error.message)) };
3925
+ }
3926
+ }
3927
+ try {
3928
+ if (!db.ok) {
3929
+ return {
3930
+ db,
3931
+ pgvector,
3932
+ vectors: { count: 0, index: null, needsTuning: false },
3933
+ models: await checkModels(options.provider, true),
3934
+ usage: {
3935
+ last60s: 0,
3936
+ lastHour: 0,
3937
+ last24h: 0,
3938
+ estimatedDailyCapRemaining: 1e4,
3939
+ warning80Percent: false
3940
+ },
3941
+ scaffold,
3942
+ config: configPermissions,
3943
+ namespace: namespaceReport
3944
+ };
3945
+ }
3946
+ const namespace = await repository.ensureNamespace(config.namespace);
3947
+ const [stats, vectorHealth] = await Promise.all([
3948
+ repository.getStats(namespace.id),
3949
+ repository.getVectorIndexHealth(namespace.id)
3950
+ ]);
3951
+ const models = await checkModels(options.provider, options.skipModelCheck);
3952
+ const dailyCap = 1e4;
3953
+ const used = stats.modelCalls.last24h;
3954
+ return {
3955
+ db,
3956
+ pgvector,
3957
+ vectors: {
3958
+ count: vectorHealth.vectorCount,
3959
+ index: vectorHealth.indexName,
3960
+ needsTuning: vectorHealth.needsTuning
3961
+ },
3962
+ models,
3963
+ usage: {
3964
+ last60s: stats.modelCalls.last60s,
3965
+ lastHour: stats.modelCalls.lastHour,
3966
+ last24h: used,
3967
+ estimatedDailyCapRemaining: Math.max(0, dailyCap - used),
3968
+ warning80Percent: used >= dailyCap * 0.8
3969
+ },
3970
+ scaffold,
3971
+ config: configPermissions,
3972
+ namespace: namespaceReport
3973
+ };
3974
+ } finally {
3975
+ await close?.();
3976
+ }
3977
+ }
3978
+ async function checkModels(provider, skip = false) {
3979
+ const ids = [...DEFAULT_EMBEDDING_MODELS, ...DEFAULT_LLM_MODELS].map((model) => model.id);
3980
+ if (!provider || skip) {
3981
+ return ids.map((id) => ({ id, reachable: false, status: "unknown" }));
3982
+ }
3983
+ const results = [];
3984
+ for (const id of ids) {
3985
+ try {
3986
+ if (DEFAULT_EMBEDDING_MODELS.some((model) => model.id === id)) {
3987
+ await provider.embed({ model: id, input: "doctor" });
3988
+ } else {
3989
+ await provider.generateText({ model: id, prompt: "doctor" });
3990
+ }
3991
+ results.push({ id, reachable: true, status: "warm" });
3992
+ } catch (error) {
3993
+ const message = String(redactSecrets(error.message));
3994
+ results.push({
3995
+ id,
3996
+ reachable: false,
3997
+ status: /gated|403/i.test(message) ? "gated" : /cold|503/i.test(message) ? "cold" : "failed",
3998
+ error: message
3999
+ });
4000
+ }
4001
+ }
4002
+ return results;
4003
+ }
4004
+
4005
+ export {
4006
+ isEligibleMemory,
4007
+ lexicalTerms,
4008
+ termOverlap,
4009
+ embeddingSpaceKey,
4010
+ DefaultPolicy,
4011
+ HuggingFaceProvider,
4012
+ isLangSmithTracingEnabled,
4013
+ createTracer,
4014
+ codeBuddyConfigSchema,
4015
+ FACT_CATEGORIES,
4016
+ INCIDENT_SEVERITIES,
4017
+ MemoryFileStore,
4018
+ EmbeddingWorker,
4019
+ CodeBuddy,
4020
+ buildProviderFromConfig,
4021
+ bootstrapDatabase,
4022
+ createDatabaseClient,
4023
+ pingDatabase,
4024
+ runMigrations,
4025
+ createPostgresRepository,
4026
+ createRuntime,
4027
+ encodeFactCursor,
4028
+ decodeFactCursor,
4029
+ listFactsPage,
4030
+ inspectNamespace,
4031
+ runDoctor
4032
+ };
4033
+ //# sourceMappingURL=chunk-UN57FKQX.js.map