gsd-pi 2.75.0-dev.2203010a0 → 2.75.0-dev.96d4bb599

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 (109) hide show
  1. package/dist/onboarding.d.ts +5 -1
  2. package/dist/onboarding.js +5 -3
  3. package/dist/resources/extensions/gsd/auto-model-selection.js +1 -1
  4. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +23 -19
  5. package/dist/resources/extensions/gsd/bootstrap/memory-tools.js +128 -0
  6. package/dist/resources/extensions/gsd/bootstrap/register-extension.js +2 -0
  7. package/dist/resources/extensions/gsd/bootstrap/system-context.js +17 -4
  8. package/dist/resources/extensions/gsd/commands/handlers/onboarding.js +52 -68
  9. package/dist/resources/extensions/gsd/commands/handlers/ops.js +5 -0
  10. package/dist/resources/extensions/gsd/commands-memory.js +462 -0
  11. package/dist/resources/extensions/gsd/gsd-db.js +237 -4
  12. package/dist/resources/extensions/gsd/memory-embeddings.js +219 -0
  13. package/dist/resources/extensions/gsd/memory-extractor.js +78 -27
  14. package/dist/resources/extensions/gsd/memory-ingest.js +218 -0
  15. package/dist/resources/extensions/gsd/memory-relations.js +189 -0
  16. package/dist/resources/extensions/gsd/memory-source-store.js +113 -0
  17. package/dist/resources/extensions/gsd/memory-store.js +299 -6
  18. package/dist/resources/extensions/gsd/model-router.js +9 -5
  19. package/dist/resources/extensions/gsd/notification-overlay.js +7 -22
  20. package/dist/resources/extensions/gsd/tools/memory-tools.js +306 -0
  21. package/dist/resources/extensions/gsd/tools/skip-slice.js +78 -0
  22. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  23. package/dist/web/standalone/.next/BUILD_ID +1 -1
  24. package/dist/web/standalone/.next/app-path-routes-manifest.json +14 -14
  25. package/dist/web/standalone/.next/build-manifest.json +2 -2
  26. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  27. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  28. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  36. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/index.html +1 -1
  44. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app-paths-manifest.json +14 -14
  51. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  52. package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
  53. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  54. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  55. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  56. package/package.json +1 -1
  57. package/packages/mcp-server/dist/server.d.ts.map +1 -1
  58. package/packages/mcp-server/dist/server.js +12 -10
  59. package/packages/mcp-server/dist/server.js.map +1 -1
  60. package/packages/mcp-server/dist/session-manager.d.ts.map +1 -1
  61. package/packages/mcp-server/dist/session-manager.js +8 -1
  62. package/packages/mcp-server/dist/session-manager.js.map +1 -1
  63. package/packages/mcp-server/dist/workflow-tools.d.ts +1 -0
  64. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  65. package/packages/mcp-server/dist/workflow-tools.js +113 -14
  66. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  67. package/packages/mcp-server/src/mcp-server.test.ts +40 -4
  68. package/packages/mcp-server/src/server.ts +12 -10
  69. package/packages/mcp-server/src/session-manager.ts +10 -3
  70. package/packages/mcp-server/src/workflow-tools.test.ts +91 -1
  71. package/packages/mcp-server/src/workflow-tools.ts +128 -18
  72. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  73. package/src/resources/extensions/gsd/auto-model-selection.ts +1 -1
  74. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +24 -20
  75. package/src/resources/extensions/gsd/bootstrap/memory-tools.ts +158 -0
  76. package/src/resources/extensions/gsd/bootstrap/register-extension.ts +2 -0
  77. package/src/resources/extensions/gsd/bootstrap/system-context.ts +20 -4
  78. package/src/resources/extensions/gsd/commands/handlers/onboarding.ts +65 -98
  79. package/src/resources/extensions/gsd/commands/handlers/ops.ts +5 -0
  80. package/src/resources/extensions/gsd/commands-memory.ts +551 -0
  81. package/src/resources/extensions/gsd/gsd-db.ts +273 -4
  82. package/src/resources/extensions/gsd/memory-embeddings.ts +235 -0
  83. package/src/resources/extensions/gsd/memory-extractor.ts +100 -34
  84. package/src/resources/extensions/gsd/memory-ingest.ts +286 -0
  85. package/src/resources/extensions/gsd/memory-relations.ts +240 -0
  86. package/src/resources/extensions/gsd/memory-source-store.ts +138 -0
  87. package/src/resources/extensions/gsd/memory-store.ts +351 -7
  88. package/src/resources/extensions/gsd/model-router.ts +10 -5
  89. package/src/resources/extensions/gsd/notification-overlay.ts +9 -19
  90. package/src/resources/extensions/gsd/tests/auto-model-selection.test.ts +12 -0
  91. package/src/resources/extensions/gsd/tests/complete-slice.test.ts +2 -2
  92. package/src/resources/extensions/gsd/tests/complete-task.test.ts +2 -2
  93. package/src/resources/extensions/gsd/tests/escalation.test.ts +2 -2
  94. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +1 -1
  95. package/src/resources/extensions/gsd/tests/md-importer.test.ts +1 -1
  96. package/src/resources/extensions/gsd/tests/memory-embeddings.test.ts +213 -0
  97. package/src/resources/extensions/gsd/tests/memory-ingest.test.ts +153 -0
  98. package/src/resources/extensions/gsd/tests/memory-maintenance.test.ts +107 -0
  99. package/src/resources/extensions/gsd/tests/memory-relations.test.ts +175 -0
  100. package/src/resources/extensions/gsd/tests/memory-store.test.ts +2 -2
  101. package/src/resources/extensions/gsd/tests/memory-tools.test.ts +295 -0
  102. package/src/resources/extensions/gsd/tests/model-router.test.ts +50 -0
  103. package/src/resources/extensions/gsd/tests/notification-overlay.test.ts +56 -37
  104. package/src/resources/extensions/gsd/tests/skip-slice-cascades-tasks.test.ts +125 -0
  105. package/src/resources/extensions/gsd/tools/memory-tools.ts +380 -0
  106. package/src/resources/extensions/gsd/tools/skip-slice.ts +133 -0
  107. package/src/resources/extensions/gsd/workflow-logger.ts +3 -1
  108. /package/dist/web/standalone/.next/static/{8FZqxNe9FxQDmsbRzR8tA → o61X3klsB6C0UE0X1x3PA}/_buildManifest.js +0 -0
  109. /package/dist/web/standalone/.next/static/{8FZqxNe9FxQDmsbRzR8tA → o61X3klsB6C0UE0X1x3PA}/_ssgManifest.js +0 -0
@@ -180,7 +180,7 @@ function openRawDb(path: string): unknown {
180
180
  return new Database(path);
181
181
  }
182
182
 
183
- const SCHEMA_VERSION = 17;
183
+ const SCHEMA_VERSION = 20;
184
184
 
185
185
  function indexExists(db: DbAdapter, name: string): boolean {
186
186
  return !!db.prepare(
@@ -281,7 +281,9 @@ function initSchema(db: DbAdapter, fileBacked: boolean): void {
281
281
  created_at TEXT NOT NULL,
282
282
  updated_at TEXT NOT NULL,
283
283
  superseded_by TEXT DEFAULT NULL,
284
- hit_count INTEGER NOT NULL DEFAULT 0
284
+ hit_count INTEGER NOT NULL DEFAULT 0,
285
+ scope TEXT NOT NULL DEFAULT 'project',
286
+ tags TEXT NOT NULL DEFAULT '[]'
285
287
  )
286
288
  `);
287
289
 
@@ -293,6 +295,45 @@ function initSchema(db: DbAdapter, fileBacked: boolean): void {
293
295
  )
294
296
  `);
295
297
 
298
+ db.exec(`
299
+ CREATE TABLE IF NOT EXISTS memory_sources (
300
+ id TEXT PRIMARY KEY,
301
+ kind TEXT NOT NULL,
302
+ uri TEXT,
303
+ title TEXT,
304
+ content TEXT NOT NULL,
305
+ content_hash TEXT NOT NULL UNIQUE,
306
+ imported_at TEXT NOT NULL,
307
+ scope TEXT NOT NULL DEFAULT 'project',
308
+ tags TEXT NOT NULL DEFAULT '[]'
309
+ )
310
+ `);
311
+
312
+ db.exec(`
313
+ CREATE TABLE IF NOT EXISTS memory_embeddings (
314
+ memory_id TEXT PRIMARY KEY,
315
+ model TEXT NOT NULL,
316
+ dim INTEGER NOT NULL,
317
+ vector BLOB NOT NULL,
318
+ updated_at TEXT NOT NULL
319
+ )
320
+ `);
321
+
322
+ db.exec(`
323
+ CREATE TABLE IF NOT EXISTS memory_relations (
324
+ from_id TEXT NOT NULL,
325
+ to_id TEXT NOT NULL,
326
+ rel TEXT NOT NULL,
327
+ confidence REAL NOT NULL DEFAULT 0.8,
328
+ created_at TEXT NOT NULL,
329
+ PRIMARY KEY (from_id, to_id, rel)
330
+ )
331
+ `);
332
+
333
+ // FTS5 virtual table mirroring memories.content for fast keyword search.
334
+ // Optional — if the SQLite build lacks FTS5, we fall back to LIKE scans.
335
+ tryCreateMemoriesFts(db);
336
+
296
337
  db.exec(`
297
338
  CREATE TABLE IF NOT EXISTS milestones (
298
339
  id TEXT PRIMARY KEY,
@@ -516,6 +557,11 @@ function initSchema(db: DbAdapter, fileBacked: boolean): void {
516
557
  `);
517
558
 
518
559
  db.exec("CREATE INDEX IF NOT EXISTS idx_memories_active ON memories(superseded_by)");
560
+ db.exec("CREATE INDEX IF NOT EXISTS idx_memories_scope ON memories(scope)");
561
+ db.exec("CREATE INDEX IF NOT EXISTS idx_memory_sources_kind ON memory_sources(kind)");
562
+ db.exec("CREATE INDEX IF NOT EXISTS idx_memory_sources_scope ON memory_sources(scope)");
563
+ db.exec("CREATE INDEX IF NOT EXISTS idx_memory_relations_from ON memory_relations(from_id)");
564
+ db.exec("CREATE INDEX IF NOT EXISTS idx_memory_relations_to ON memory_relations(to_id)");
519
565
  db.exec("CREATE INDEX IF NOT EXISTS idx_replan_history_milestone ON replan_history(milestone_id, created_at)");
520
566
 
521
567
  // v13 indexes — hot-path dispatch queries
@@ -565,6 +611,56 @@ function columnExists(db: DbAdapter, table: string, column: string): boolean {
565
611
  return rows.some((row) => row["name"] === column);
566
612
  }
567
613
 
614
+ /**
615
+ * Create the FTS5 virtual table for memories plus the triggers that keep it
616
+ * in sync with the base table. FTS5 may be unavailable on stripped-down
617
+ * SQLite builds — callers should treat failure as non-fatal and fall back
618
+ * to LIKE-based scans in `memory-store.queryMemoriesRanked`.
619
+ */
620
+ export function tryCreateMemoriesFts(db: DbAdapter): boolean {
621
+ try {
622
+ db.exec(`
623
+ CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts
624
+ USING fts5(content, content='memories', content_rowid='seq', tokenize='porter unicode61')
625
+ `);
626
+ // Triggers mirror inserts / updates / deletes on the base memories table.
627
+ db.exec(`
628
+ CREATE TRIGGER IF NOT EXISTS memories_ai
629
+ AFTER INSERT ON memories BEGIN
630
+ INSERT INTO memories_fts(rowid, content) VALUES (new.seq, new.content);
631
+ END
632
+ `);
633
+ db.exec(`
634
+ CREATE TRIGGER IF NOT EXISTS memories_ad
635
+ AFTER DELETE ON memories BEGIN
636
+ INSERT INTO memories_fts(memories_fts, rowid, content) VALUES ('delete', old.seq, old.content);
637
+ END
638
+ `);
639
+ db.exec(`
640
+ CREATE TRIGGER IF NOT EXISTS memories_au
641
+ AFTER UPDATE OF content ON memories BEGIN
642
+ INSERT INTO memories_fts(memories_fts, rowid, content) VALUES ('delete', old.seq, old.content);
643
+ INSERT INTO memories_fts(rowid, content) VALUES (new.seq, new.content);
644
+ END
645
+ `);
646
+ return true;
647
+ } catch (err) {
648
+ logWarning("db", `FTS5 unavailable — memory queries will use LIKE fallback: ${(err as Error).message}`);
649
+ return false;
650
+ }
651
+ }
652
+
653
+ export function isMemoriesFtsAvailable(db: DbAdapter): boolean {
654
+ try {
655
+ const row = db
656
+ .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='memories_fts'")
657
+ .get();
658
+ return !!row;
659
+ } catch {
660
+ return false;
661
+ }
662
+ }
663
+
568
664
  function ensureColumn(db: DbAdapter, table: string, column: string, ddl: string): void {
569
665
  if (!columnExists(db, table, column)) db.exec(ddl);
570
666
  }
@@ -988,6 +1084,79 @@ function migrateSchema(db: DbAdapter): void {
988
1084
  });
989
1085
  }
990
1086
 
1087
+ if (currentVersion < 18) {
1088
+ // Memory system Phase 2: scope + tags on memories, plus memory_sources
1089
+ // table for raw ingested content (notes, files, URLs, artifacts).
1090
+ ensureColumn(db, "memories", "scope", `ALTER TABLE memories ADD COLUMN scope TEXT NOT NULL DEFAULT 'project'`);
1091
+ ensureColumn(db, "memories", "tags", `ALTER TABLE memories ADD COLUMN tags TEXT NOT NULL DEFAULT '[]'`);
1092
+ db.exec(`
1093
+ CREATE TABLE IF NOT EXISTS memory_sources (
1094
+ id TEXT PRIMARY KEY,
1095
+ kind TEXT NOT NULL,
1096
+ uri TEXT,
1097
+ title TEXT,
1098
+ content TEXT NOT NULL,
1099
+ content_hash TEXT NOT NULL UNIQUE,
1100
+ imported_at TEXT NOT NULL,
1101
+ scope TEXT NOT NULL DEFAULT 'project',
1102
+ tags TEXT NOT NULL DEFAULT '[]'
1103
+ )
1104
+ `);
1105
+ db.exec("CREATE INDEX IF NOT EXISTS idx_memories_scope ON memories(scope)");
1106
+ db.exec("CREATE INDEX IF NOT EXISTS idx_memory_sources_kind ON memory_sources(kind)");
1107
+ db.exec("CREATE INDEX IF NOT EXISTS idx_memory_sources_scope ON memory_sources(scope)");
1108
+ db.prepare("INSERT INTO schema_version (version, applied_at) VALUES (:version, :applied_at)").run({
1109
+ ":version": 18,
1110
+ ":applied_at": new Date().toISOString(),
1111
+ });
1112
+ }
1113
+
1114
+ if (currentVersion < 19) {
1115
+ // Memory system Phase 3: embeddings + FTS5 for hybrid retrieval.
1116
+ db.exec(`
1117
+ CREATE TABLE IF NOT EXISTS memory_embeddings (
1118
+ memory_id TEXT PRIMARY KEY,
1119
+ model TEXT NOT NULL,
1120
+ dim INTEGER NOT NULL,
1121
+ vector BLOB NOT NULL,
1122
+ updated_at TEXT NOT NULL
1123
+ )
1124
+ `);
1125
+ tryCreateMemoriesFts(db);
1126
+ // Backfill FTS5 with any existing memories (triggers only cover future writes).
1127
+ if (isMemoriesFtsAvailable(db)) {
1128
+ try {
1129
+ db.exec(`INSERT INTO memories_fts(rowid, content) SELECT seq, content FROM memories`);
1130
+ } catch (err) {
1131
+ logWarning("db", `FTS5 backfill failed: ${(err as Error).message}`);
1132
+ }
1133
+ }
1134
+ db.prepare("INSERT INTO schema_version (version, applied_at) VALUES (:version, :applied_at)").run({
1135
+ ":version": 19,
1136
+ ":applied_at": new Date().toISOString(),
1137
+ });
1138
+ }
1139
+
1140
+ if (currentVersion < 20) {
1141
+ // Memory system Phase 4: knowledge-graph relations between memories.
1142
+ db.exec(`
1143
+ CREATE TABLE IF NOT EXISTS memory_relations (
1144
+ from_id TEXT NOT NULL,
1145
+ to_id TEXT NOT NULL,
1146
+ rel TEXT NOT NULL,
1147
+ confidence REAL NOT NULL DEFAULT 0.8,
1148
+ created_at TEXT NOT NULL,
1149
+ PRIMARY KEY (from_id, to_id, rel)
1150
+ )
1151
+ `);
1152
+ db.exec("CREATE INDEX IF NOT EXISTS idx_memory_relations_from ON memory_relations(from_id)");
1153
+ db.exec("CREATE INDEX IF NOT EXISTS idx_memory_relations_to ON memory_relations(to_id)");
1154
+ db.prepare("INSERT INTO schema_version (version, applied_at) VALUES (:version, :applied_at)").run({
1155
+ ":version": 20,
1156
+ ":applied_at": new Date().toISOString(),
1157
+ });
1158
+ }
1159
+
991
1160
  db.exec("COMMIT");
992
1161
  } catch (err) {
993
1162
  db.exec("ROLLBACK");
@@ -3423,11 +3592,13 @@ export function insertMemoryRow(args: {
3423
3592
  sourceUnitId: string | null;
3424
3593
  createdAt: string;
3425
3594
  updatedAt: string;
3595
+ scope?: string;
3596
+ tags?: string[];
3426
3597
  }): void {
3427
3598
  if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
3428
3599
  currentDb.prepare(
3429
- `INSERT INTO memories (id, category, content, confidence, source_unit_type, source_unit_id, created_at, updated_at)
3430
- VALUES (:id, :category, :content, :confidence, :source_unit_type, :source_unit_id, :created_at, :updated_at)`,
3600
+ `INSERT INTO memories (id, category, content, confidence, source_unit_type, source_unit_id, created_at, updated_at, scope, tags)
3601
+ VALUES (:id, :category, :content, :confidence, :source_unit_type, :source_unit_id, :created_at, :updated_at, :scope, :tags)`,
3431
3602
  ).run({
3432
3603
  ":id": args.id,
3433
3604
  ":category": args.category,
@@ -3437,9 +3608,107 @@ export function insertMemoryRow(args: {
3437
3608
  ":source_unit_id": args.sourceUnitId,
3438
3609
  ":created_at": args.createdAt,
3439
3610
  ":updated_at": args.updatedAt,
3611
+ ":scope": args.scope ?? "project",
3612
+ ":tags": JSON.stringify(args.tags ?? []),
3440
3613
  });
3441
3614
  }
3442
3615
 
3616
+ export function insertMemorySourceRow(args: {
3617
+ id: string;
3618
+ kind: string;
3619
+ uri: string | null;
3620
+ title: string | null;
3621
+ content: string;
3622
+ contentHash: string;
3623
+ importedAt: string;
3624
+ scope?: string;
3625
+ tags?: string[];
3626
+ }): void {
3627
+ if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
3628
+ currentDb.prepare(
3629
+ `INSERT OR IGNORE INTO memory_sources (id, kind, uri, title, content, content_hash, imported_at, scope, tags)
3630
+ VALUES (:id, :kind, :uri, :title, :content, :content_hash, :imported_at, :scope, :tags)`,
3631
+ ).run({
3632
+ ":id": args.id,
3633
+ ":kind": args.kind,
3634
+ ":uri": args.uri,
3635
+ ":title": args.title,
3636
+ ":content": args.content,
3637
+ ":content_hash": args.contentHash,
3638
+ ":imported_at": args.importedAt,
3639
+ ":scope": args.scope ?? "project",
3640
+ ":tags": JSON.stringify(args.tags ?? []),
3641
+ });
3642
+ }
3643
+
3644
+ export function deleteMemorySourceRow(id: string): boolean {
3645
+ if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
3646
+ const res = currentDb
3647
+ .prepare("DELETE FROM memory_sources WHERE id = :id")
3648
+ .run({ ":id": id }) as { changes?: number };
3649
+ return (res?.changes ?? 0) > 0;
3650
+ }
3651
+
3652
+ export function upsertMemoryEmbedding(args: {
3653
+ memoryId: string;
3654
+ model: string;
3655
+ dim: number;
3656
+ vector: Uint8Array;
3657
+ updatedAt: string;
3658
+ }): void {
3659
+ if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
3660
+ currentDb.prepare(
3661
+ `INSERT INTO memory_embeddings (memory_id, model, dim, vector, updated_at)
3662
+ VALUES (:memory_id, :model, :dim, :vector, :updated_at)
3663
+ ON CONFLICT(memory_id) DO UPDATE SET
3664
+ model = excluded.model,
3665
+ dim = excluded.dim,
3666
+ vector = excluded.vector,
3667
+ updated_at = excluded.updated_at`,
3668
+ ).run({
3669
+ ":memory_id": args.memoryId,
3670
+ ":model": args.model,
3671
+ ":dim": args.dim,
3672
+ ":vector": args.vector,
3673
+ ":updated_at": args.updatedAt,
3674
+ });
3675
+ }
3676
+
3677
+ export function deleteMemoryEmbedding(memoryId: string): boolean {
3678
+ if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
3679
+ const res = currentDb
3680
+ .prepare("DELETE FROM memory_embeddings WHERE memory_id = :id")
3681
+ .run({ ":id": memoryId }) as { changes?: number };
3682
+ return (res?.changes ?? 0) > 0;
3683
+ }
3684
+
3685
+ export function insertMemoryRelationRow(args: {
3686
+ fromId: string;
3687
+ toId: string;
3688
+ rel: string;
3689
+ confidence: number;
3690
+ createdAt: string;
3691
+ }): void {
3692
+ if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
3693
+ currentDb.prepare(
3694
+ `INSERT OR REPLACE INTO memory_relations (from_id, to_id, rel, confidence, created_at)
3695
+ VALUES (:from_id, :to_id, :rel, :confidence, :created_at)`,
3696
+ ).run({
3697
+ ":from_id": args.fromId,
3698
+ ":to_id": args.toId,
3699
+ ":rel": args.rel,
3700
+ ":confidence": args.confidence,
3701
+ ":created_at": args.createdAt,
3702
+ });
3703
+ }
3704
+
3705
+ export function deleteMemoryRelationsFor(memoryId: string): void {
3706
+ if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
3707
+ currentDb
3708
+ .prepare("DELETE FROM memory_relations WHERE from_id = :id OR to_id = :id")
3709
+ .run({ ":id": memoryId });
3710
+ }
3711
+
3443
3712
  export function rewriteMemoryId(placeholderId: string, realId: string): void {
3444
3713
  if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
3445
3714
  currentDb.prepare("UPDATE memories SET id = :real_id WHERE id = :placeholder").run({
@@ -0,0 +1,235 @@
1
+ // GSD Memory Embeddings — provider-agnostic embedding layer
2
+ //
3
+ // Same model-discovery pattern as buildMemoryLLMCall: prefers a dedicated
4
+ // embedding-capable model when available, and returns null when none is
5
+ // found (which is the common case — not every provider exposes embeddings).
6
+ //
7
+ // When embeddings are unavailable, all calls become no-ops and
8
+ // queryMemoriesRanked falls back to keyword-only scoring.
9
+
10
+ import type { ExtensionContext } from "@gsd/pi-coding-agent";
11
+
12
+ import { _getAdapter, isDbAvailable, upsertMemoryEmbedding, deleteMemoryEmbedding } from "./gsd-db.js";
13
+ import { logWarning } from "./workflow-logger.js";
14
+
15
+ // ─── Types ──────────────────────────────────────────────────────────────────
16
+
17
+ export type EmbedFn = (texts: string[]) => Promise<Float32Array[]>;
18
+
19
+ export interface EmbeddingModelInfo {
20
+ id: string;
21
+ }
22
+
23
+ export interface MemoryEmbeddingRow {
24
+ memoryId: string;
25
+ model: string;
26
+ dim: number;
27
+ vector: Float32Array;
28
+ }
29
+
30
+ // ─── Model selection ────────────────────────────────────────────────────────
31
+
32
+ const EMBEDDING_ID_HINTS = [
33
+ "embed",
34
+ "embedding",
35
+ "voyage",
36
+ "text-embedding",
37
+ "nomic",
38
+ "jina-embed",
39
+ "bge",
40
+ "mxbai-embed",
41
+ ];
42
+
43
+ /**
44
+ * Try to build an embedding function from the model registry. Returns null
45
+ * when no embedding-capable model is obvious from the registry metadata.
46
+ *
47
+ * NOTE: the Pi SDK doesn't yet expose a dedicated embeddings API for every
48
+ * provider. This implementation currently targets Anthropic / OpenAI-shaped
49
+ * SDKs: when the caller has direct API access via `ctx.modelRegistry`, they
50
+ * can wire this up by providing an `embedFn` override. We ship the hint-based
51
+ * detection here so future providers can plug in without touching callers.
52
+ */
53
+ export function buildEmbeddingFn(ctx: ExtensionContext): EmbedFn | null {
54
+ try {
55
+ const available = ctx.modelRegistry?.getAvailable?.();
56
+ if (!available || available.length === 0) return null;
57
+ const candidate = available.find((model) => {
58
+ const id = typeof model?.id === "string" ? model.id.toLowerCase() : "";
59
+ return EMBEDDING_ID_HINTS.some((hint) => id.includes(hint));
60
+ });
61
+ if (!candidate) return null;
62
+ // We don't currently have a provider-neutral embedding call in Pi; the
63
+ // detection surface is in place so wiring can happen once Pi offers it.
64
+ return null;
65
+ } catch (err) {
66
+ logWarning("memory-embeddings", `model discovery failed: ${(err as Error).message}`);
67
+ return null;
68
+ }
69
+ }
70
+
71
+ // ─── Vector (de)serialization ───────────────────────────────────────────────
72
+
73
+ export function packFloat32(vec: Float32Array): Uint8Array {
74
+ return new Uint8Array(vec.buffer, vec.byteOffset, vec.byteLength);
75
+ }
76
+
77
+ export function unpackFloat32(blob: unknown): Float32Array | null {
78
+ if (!blob) return null;
79
+ try {
80
+ if (blob instanceof Float32Array) return blob;
81
+ let view: Uint8Array;
82
+ if (blob instanceof Uint8Array) {
83
+ view = blob;
84
+ } else if (blob instanceof ArrayBuffer) {
85
+ view = new Uint8Array(blob);
86
+ } else if ((blob as Buffer).buffer && (blob as Buffer).byteLength != null) {
87
+ const buf = blob as Buffer;
88
+ view = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
89
+ } else if (Array.isArray(blob)) {
90
+ return new Float32Array(blob as number[]);
91
+ } else {
92
+ return null;
93
+ }
94
+ if (view.byteLength % 4 !== 0) return null;
95
+ // Copy into an aligned buffer — BLOBs may arrive at odd byte offsets.
96
+ const aligned = new ArrayBuffer(view.byteLength);
97
+ new Uint8Array(aligned).set(view);
98
+ return new Float32Array(aligned);
99
+ } catch {
100
+ return null;
101
+ }
102
+ }
103
+
104
+ // ─── Math ───────────────────────────────────────────────────────────────────
105
+
106
+ export function cosineSimilarity(a: Float32Array, b: Float32Array): number {
107
+ if (a.length === 0 || a.length !== b.length) return 0;
108
+ let dot = 0;
109
+ let normA = 0;
110
+ let normB = 0;
111
+ for (let i = 0; i < a.length; i++) {
112
+ const x = a[i];
113
+ const y = b[i];
114
+ dot += x * y;
115
+ normA += x * x;
116
+ normB += y * y;
117
+ }
118
+ if (normA === 0 || normB === 0) return 0;
119
+ return dot / (Math.sqrt(normA) * Math.sqrt(normB));
120
+ }
121
+
122
+ // ─── Read helpers ───────────────────────────────────────────────────────────
123
+
124
+ export function getEmbeddingForMemory(memoryId: string): MemoryEmbeddingRow | null {
125
+ if (!isDbAvailable()) return null;
126
+ const adapter = _getAdapter();
127
+ if (!adapter) return null;
128
+ try {
129
+ const row = adapter
130
+ .prepare(
131
+ "SELECT memory_id, model, dim, vector FROM memory_embeddings WHERE memory_id = :id",
132
+ )
133
+ .get({ ":id": memoryId });
134
+ if (!row) return null;
135
+ const vector = unpackFloat32(row["vector"]);
136
+ if (!vector) return null;
137
+ return {
138
+ memoryId: row["memory_id"] as string,
139
+ model: row["model"] as string,
140
+ dim: row["dim"] as number,
141
+ vector,
142
+ };
143
+ } catch {
144
+ return null;
145
+ }
146
+ }
147
+
148
+ export function loadAllEmbeddings(): MemoryEmbeddingRow[] {
149
+ if (!isDbAvailable()) return [];
150
+ const adapter = _getAdapter();
151
+ if (!adapter) return [];
152
+ try {
153
+ const rows = adapter
154
+ .prepare(
155
+ `SELECT e.memory_id, e.model, e.dim, e.vector
156
+ FROM memory_embeddings e
157
+ JOIN memories m ON m.id = e.memory_id
158
+ WHERE m.superseded_by IS NULL`,
159
+ )
160
+ .all();
161
+ const out: MemoryEmbeddingRow[] = [];
162
+ for (const row of rows) {
163
+ const vector = unpackFloat32(row["vector"]);
164
+ if (!vector) continue;
165
+ out.push({
166
+ memoryId: row["memory_id"] as string,
167
+ model: row["model"] as string,
168
+ dim: row["dim"] as number,
169
+ vector,
170
+ });
171
+ }
172
+ return out;
173
+ } catch {
174
+ return [];
175
+ }
176
+ }
177
+
178
+ // ─── Write helpers ──────────────────────────────────────────────────────────
179
+
180
+ export function saveEmbedding(
181
+ memoryId: string,
182
+ vector: Float32Array,
183
+ model: string,
184
+ ): boolean {
185
+ if (!isDbAvailable()) return false;
186
+ try {
187
+ upsertMemoryEmbedding({
188
+ memoryId,
189
+ model,
190
+ dim: vector.length,
191
+ vector: packFloat32(vector),
192
+ updatedAt: new Date().toISOString(),
193
+ });
194
+ return true;
195
+ } catch {
196
+ return false;
197
+ }
198
+ }
199
+
200
+ export function removeEmbedding(memoryId: string): boolean {
201
+ if (!isDbAvailable()) return false;
202
+ try {
203
+ return deleteMemoryEmbedding(memoryId);
204
+ } catch {
205
+ return false;
206
+ }
207
+ }
208
+
209
+ // ─── Orchestration ──────────────────────────────────────────────────────────
210
+
211
+ /**
212
+ * Embed each memory's content via `embedFn` (if provided) and persist the
213
+ * resulting vectors. Returns the number of successfully embedded memories.
214
+ * Safe to call with embedFn=null — it becomes a no-op.
215
+ */
216
+ export async function embedMemories(
217
+ memories: Array<{ id: string; content: string }>,
218
+ embedFn: EmbedFn | null,
219
+ model: string,
220
+ ): Promise<number> {
221
+ if (!embedFn || memories.length === 0) return 0;
222
+ try {
223
+ const vectors = await embedFn(memories.map((m) => m.content));
224
+ let count = 0;
225
+ for (let i = 0; i < memories.length && i < vectors.length; i++) {
226
+ const vector = vectors[i];
227
+ if (!vector || vector.length === 0) continue;
228
+ if (saveEmbedding(memories[i].id, vector, model)) count++;
229
+ }
230
+ return count;
231
+ } catch (err) {
232
+ logWarning("memory-embeddings", `embed failed: ${(err as Error).message}`);
233
+ return 0;
234
+ }
235
+ }