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
@@ -135,7 +135,7 @@ function openRawDb(path) {
135
135
  const Database = providerModule;
136
136
  return new Database(path);
137
137
  }
138
- const SCHEMA_VERSION = 17;
138
+ const SCHEMA_VERSION = 20;
139
139
  function indexExists(db, name) {
140
140
  return !!db.prepare("SELECT 1 as present FROM sqlite_master WHERE type = 'index' AND name = ?").get(name);
141
141
  }
@@ -232,7 +232,9 @@ function initSchema(db, fileBacked) {
232
232
  created_at TEXT NOT NULL,
233
233
  updated_at TEXT NOT NULL,
234
234
  superseded_by TEXT DEFAULT NULL,
235
- hit_count INTEGER NOT NULL DEFAULT 0
235
+ hit_count INTEGER NOT NULL DEFAULT 0,
236
+ scope TEXT NOT NULL DEFAULT 'project',
237
+ tags TEXT NOT NULL DEFAULT '[]'
236
238
  )
237
239
  `);
238
240
  db.exec(`
@@ -243,6 +245,41 @@ function initSchema(db, fileBacked) {
243
245
  )
244
246
  `);
245
247
  db.exec(`
248
+ CREATE TABLE IF NOT EXISTS memory_sources (
249
+ id TEXT PRIMARY KEY,
250
+ kind TEXT NOT NULL,
251
+ uri TEXT,
252
+ title TEXT,
253
+ content TEXT NOT NULL,
254
+ content_hash TEXT NOT NULL UNIQUE,
255
+ imported_at TEXT NOT NULL,
256
+ scope TEXT NOT NULL DEFAULT 'project',
257
+ tags TEXT NOT NULL DEFAULT '[]'
258
+ )
259
+ `);
260
+ db.exec(`
261
+ CREATE TABLE IF NOT EXISTS memory_embeddings (
262
+ memory_id TEXT PRIMARY KEY,
263
+ model TEXT NOT NULL,
264
+ dim INTEGER NOT NULL,
265
+ vector BLOB NOT NULL,
266
+ updated_at TEXT NOT NULL
267
+ )
268
+ `);
269
+ db.exec(`
270
+ CREATE TABLE IF NOT EXISTS memory_relations (
271
+ from_id TEXT NOT NULL,
272
+ to_id TEXT NOT NULL,
273
+ rel TEXT NOT NULL,
274
+ confidence REAL NOT NULL DEFAULT 0.8,
275
+ created_at TEXT NOT NULL,
276
+ PRIMARY KEY (from_id, to_id, rel)
277
+ )
278
+ `);
279
+ // FTS5 virtual table mirroring memories.content for fast keyword search.
280
+ // Optional — if the SQLite build lacks FTS5, we fall back to LIKE scans.
281
+ tryCreateMemoriesFts(db);
282
+ db.exec(`
246
283
  CREATE TABLE IF NOT EXISTS milestones (
247
284
  id TEXT PRIMARY KEY,
248
285
  title TEXT NOT NULL DEFAULT '',
@@ -453,6 +490,11 @@ function initSchema(db, fileBacked) {
453
490
  )
454
491
  `);
455
492
  db.exec("CREATE INDEX IF NOT EXISTS idx_memories_active ON memories(superseded_by)");
493
+ db.exec("CREATE INDEX IF NOT EXISTS idx_memories_scope ON memories(scope)");
494
+ db.exec("CREATE INDEX IF NOT EXISTS idx_memory_sources_kind ON memory_sources(kind)");
495
+ db.exec("CREATE INDEX IF NOT EXISTS idx_memory_sources_scope ON memory_sources(scope)");
496
+ db.exec("CREATE INDEX IF NOT EXISTS idx_memory_relations_from ON memory_relations(from_id)");
497
+ db.exec("CREATE INDEX IF NOT EXISTS idx_memory_relations_to ON memory_relations(to_id)");
456
498
  db.exec("CREATE INDEX IF NOT EXISTS idx_replan_history_milestone ON replan_history(milestone_id, created_at)");
457
499
  // v13 indexes — hot-path dispatch queries
458
500
  db.exec("CREATE INDEX IF NOT EXISTS idx_tasks_active ON tasks(milestone_id, slice_id, status)");
@@ -493,6 +535,56 @@ function columnExists(db, table, column) {
493
535
  const rows = db.prepare(`PRAGMA table_info(${table})`).all();
494
536
  return rows.some((row) => row["name"] === column);
495
537
  }
538
+ /**
539
+ * Create the FTS5 virtual table for memories plus the triggers that keep it
540
+ * in sync with the base table. FTS5 may be unavailable on stripped-down
541
+ * SQLite builds — callers should treat failure as non-fatal and fall back
542
+ * to LIKE-based scans in `memory-store.queryMemoriesRanked`.
543
+ */
544
+ export function tryCreateMemoriesFts(db) {
545
+ try {
546
+ db.exec(`
547
+ CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts
548
+ USING fts5(content, content='memories', content_rowid='seq', tokenize='porter unicode61')
549
+ `);
550
+ // Triggers mirror inserts / updates / deletes on the base memories table.
551
+ db.exec(`
552
+ CREATE TRIGGER IF NOT EXISTS memories_ai
553
+ AFTER INSERT ON memories BEGIN
554
+ INSERT INTO memories_fts(rowid, content) VALUES (new.seq, new.content);
555
+ END
556
+ `);
557
+ db.exec(`
558
+ CREATE TRIGGER IF NOT EXISTS memories_ad
559
+ AFTER DELETE ON memories BEGIN
560
+ INSERT INTO memories_fts(memories_fts, rowid, content) VALUES ('delete', old.seq, old.content);
561
+ END
562
+ `);
563
+ db.exec(`
564
+ CREATE TRIGGER IF NOT EXISTS memories_au
565
+ AFTER UPDATE OF content ON memories BEGIN
566
+ INSERT INTO memories_fts(memories_fts, rowid, content) VALUES ('delete', old.seq, old.content);
567
+ INSERT INTO memories_fts(rowid, content) VALUES (new.seq, new.content);
568
+ END
569
+ `);
570
+ return true;
571
+ }
572
+ catch (err) {
573
+ logWarning("db", `FTS5 unavailable — memory queries will use LIKE fallback: ${err.message}`);
574
+ return false;
575
+ }
576
+ }
577
+ export function isMemoriesFtsAvailable(db) {
578
+ try {
579
+ const row = db
580
+ .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='memories_fts'")
581
+ .get();
582
+ return !!row;
583
+ }
584
+ catch {
585
+ return false;
586
+ }
587
+ }
496
588
  function ensureColumn(db, table, column, ddl) {
497
589
  if (!columnExists(db, table, column))
498
590
  db.exec(ddl);
@@ -896,6 +988,77 @@ function migrateSchema(db) {
896
988
  ":applied_at": new Date().toISOString(),
897
989
  });
898
990
  }
991
+ if (currentVersion < 18) {
992
+ // Memory system Phase 2: scope + tags on memories, plus memory_sources
993
+ // table for raw ingested content (notes, files, URLs, artifacts).
994
+ ensureColumn(db, "memories", "scope", `ALTER TABLE memories ADD COLUMN scope TEXT NOT NULL DEFAULT 'project'`);
995
+ ensureColumn(db, "memories", "tags", `ALTER TABLE memories ADD COLUMN tags TEXT NOT NULL DEFAULT '[]'`);
996
+ db.exec(`
997
+ CREATE TABLE IF NOT EXISTS memory_sources (
998
+ id TEXT PRIMARY KEY,
999
+ kind TEXT NOT NULL,
1000
+ uri TEXT,
1001
+ title TEXT,
1002
+ content TEXT NOT NULL,
1003
+ content_hash TEXT NOT NULL UNIQUE,
1004
+ imported_at TEXT NOT NULL,
1005
+ scope TEXT NOT NULL DEFAULT 'project',
1006
+ tags TEXT NOT NULL DEFAULT '[]'
1007
+ )
1008
+ `);
1009
+ db.exec("CREATE INDEX IF NOT EXISTS idx_memories_scope ON memories(scope)");
1010
+ db.exec("CREATE INDEX IF NOT EXISTS idx_memory_sources_kind ON memory_sources(kind)");
1011
+ db.exec("CREATE INDEX IF NOT EXISTS idx_memory_sources_scope ON memory_sources(scope)");
1012
+ db.prepare("INSERT INTO schema_version (version, applied_at) VALUES (:version, :applied_at)").run({
1013
+ ":version": 18,
1014
+ ":applied_at": new Date().toISOString(),
1015
+ });
1016
+ }
1017
+ if (currentVersion < 19) {
1018
+ // Memory system Phase 3: embeddings + FTS5 for hybrid retrieval.
1019
+ db.exec(`
1020
+ CREATE TABLE IF NOT EXISTS memory_embeddings (
1021
+ memory_id TEXT PRIMARY KEY,
1022
+ model TEXT NOT NULL,
1023
+ dim INTEGER NOT NULL,
1024
+ vector BLOB NOT NULL,
1025
+ updated_at TEXT NOT NULL
1026
+ )
1027
+ `);
1028
+ tryCreateMemoriesFts(db);
1029
+ // Backfill FTS5 with any existing memories (triggers only cover future writes).
1030
+ if (isMemoriesFtsAvailable(db)) {
1031
+ try {
1032
+ db.exec(`INSERT INTO memories_fts(rowid, content) SELECT seq, content FROM memories`);
1033
+ }
1034
+ catch (err) {
1035
+ logWarning("db", `FTS5 backfill failed: ${err.message}`);
1036
+ }
1037
+ }
1038
+ db.prepare("INSERT INTO schema_version (version, applied_at) VALUES (:version, :applied_at)").run({
1039
+ ":version": 19,
1040
+ ":applied_at": new Date().toISOString(),
1041
+ });
1042
+ }
1043
+ if (currentVersion < 20) {
1044
+ // Memory system Phase 4: knowledge-graph relations between memories.
1045
+ db.exec(`
1046
+ CREATE TABLE IF NOT EXISTS memory_relations (
1047
+ from_id TEXT NOT NULL,
1048
+ to_id TEXT NOT NULL,
1049
+ rel TEXT NOT NULL,
1050
+ confidence REAL NOT NULL DEFAULT 0.8,
1051
+ created_at TEXT NOT NULL,
1052
+ PRIMARY KEY (from_id, to_id, rel)
1053
+ )
1054
+ `);
1055
+ db.exec("CREATE INDEX IF NOT EXISTS idx_memory_relations_from ON memory_relations(from_id)");
1056
+ db.exec("CREATE INDEX IF NOT EXISTS idx_memory_relations_to ON memory_relations(to_id)");
1057
+ db.prepare("INSERT INTO schema_version (version, applied_at) VALUES (:version, :applied_at)").run({
1058
+ ":version": 20,
1059
+ ":applied_at": new Date().toISOString(),
1060
+ });
1061
+ }
899
1062
  db.exec("COMMIT");
900
1063
  }
901
1064
  catch (err) {
@@ -2785,8 +2948,8 @@ export function bulkInsertLegacyHierarchy(payload) {
2785
2948
  export function insertMemoryRow(args) {
2786
2949
  if (!currentDb)
2787
2950
  throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
2788
- currentDb.prepare(`INSERT INTO memories (id, category, content, confidence, source_unit_type, source_unit_id, created_at, updated_at)
2789
- VALUES (:id, :category, :content, :confidence, :source_unit_type, :source_unit_id, :created_at, :updated_at)`).run({
2951
+ currentDb.prepare(`INSERT INTO memories (id, category, content, confidence, source_unit_type, source_unit_id, created_at, updated_at, scope, tags)
2952
+ VALUES (:id, :category, :content, :confidence, :source_unit_type, :source_unit_id, :created_at, :updated_at, :scope, :tags)`).run({
2790
2953
  ":id": args.id,
2791
2954
  ":category": args.category,
2792
2955
  ":content": args.content,
@@ -2795,8 +2958,78 @@ export function insertMemoryRow(args) {
2795
2958
  ":source_unit_id": args.sourceUnitId,
2796
2959
  ":created_at": args.createdAt,
2797
2960
  ":updated_at": args.updatedAt,
2961
+ ":scope": args.scope ?? "project",
2962
+ ":tags": JSON.stringify(args.tags ?? []),
2963
+ });
2964
+ }
2965
+ export function insertMemorySourceRow(args) {
2966
+ if (!currentDb)
2967
+ throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
2968
+ currentDb.prepare(`INSERT OR IGNORE INTO memory_sources (id, kind, uri, title, content, content_hash, imported_at, scope, tags)
2969
+ VALUES (:id, :kind, :uri, :title, :content, :content_hash, :imported_at, :scope, :tags)`).run({
2970
+ ":id": args.id,
2971
+ ":kind": args.kind,
2972
+ ":uri": args.uri,
2973
+ ":title": args.title,
2974
+ ":content": args.content,
2975
+ ":content_hash": args.contentHash,
2976
+ ":imported_at": args.importedAt,
2977
+ ":scope": args.scope ?? "project",
2978
+ ":tags": JSON.stringify(args.tags ?? []),
2979
+ });
2980
+ }
2981
+ export function deleteMemorySourceRow(id) {
2982
+ if (!currentDb)
2983
+ throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
2984
+ const res = currentDb
2985
+ .prepare("DELETE FROM memory_sources WHERE id = :id")
2986
+ .run({ ":id": id });
2987
+ return (res?.changes ?? 0) > 0;
2988
+ }
2989
+ export function upsertMemoryEmbedding(args) {
2990
+ if (!currentDb)
2991
+ throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
2992
+ currentDb.prepare(`INSERT INTO memory_embeddings (memory_id, model, dim, vector, updated_at)
2993
+ VALUES (:memory_id, :model, :dim, :vector, :updated_at)
2994
+ ON CONFLICT(memory_id) DO UPDATE SET
2995
+ model = excluded.model,
2996
+ dim = excluded.dim,
2997
+ vector = excluded.vector,
2998
+ updated_at = excluded.updated_at`).run({
2999
+ ":memory_id": args.memoryId,
3000
+ ":model": args.model,
3001
+ ":dim": args.dim,
3002
+ ":vector": args.vector,
3003
+ ":updated_at": args.updatedAt,
2798
3004
  });
2799
3005
  }
3006
+ export function deleteMemoryEmbedding(memoryId) {
3007
+ if (!currentDb)
3008
+ throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
3009
+ const res = currentDb
3010
+ .prepare("DELETE FROM memory_embeddings WHERE memory_id = :id")
3011
+ .run({ ":id": memoryId });
3012
+ return (res?.changes ?? 0) > 0;
3013
+ }
3014
+ export function insertMemoryRelationRow(args) {
3015
+ if (!currentDb)
3016
+ throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
3017
+ currentDb.prepare(`INSERT OR REPLACE INTO memory_relations (from_id, to_id, rel, confidence, created_at)
3018
+ VALUES (:from_id, :to_id, :rel, :confidence, :created_at)`).run({
3019
+ ":from_id": args.fromId,
3020
+ ":to_id": args.toId,
3021
+ ":rel": args.rel,
3022
+ ":confidence": args.confidence,
3023
+ ":created_at": args.createdAt,
3024
+ });
3025
+ }
3026
+ export function deleteMemoryRelationsFor(memoryId) {
3027
+ if (!currentDb)
3028
+ throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
3029
+ currentDb
3030
+ .prepare("DELETE FROM memory_relations WHERE from_id = :id OR to_id = :id")
3031
+ .run({ ":id": memoryId });
3032
+ }
2800
3033
  export function rewriteMemoryId(placeholderId, realId) {
2801
3034
  if (!currentDb)
2802
3035
  throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
@@ -0,0 +1,219 @@
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
+ import { _getAdapter, isDbAvailable, upsertMemoryEmbedding, deleteMemoryEmbedding } from "./gsd-db.js";
10
+ import { logWarning } from "./workflow-logger.js";
11
+ // ─── Model selection ────────────────────────────────────────────────────────
12
+ const EMBEDDING_ID_HINTS = [
13
+ "embed",
14
+ "embedding",
15
+ "voyage",
16
+ "text-embedding",
17
+ "nomic",
18
+ "jina-embed",
19
+ "bge",
20
+ "mxbai-embed",
21
+ ];
22
+ /**
23
+ * Try to build an embedding function from the model registry. Returns null
24
+ * when no embedding-capable model is obvious from the registry metadata.
25
+ *
26
+ * NOTE: the Pi SDK doesn't yet expose a dedicated embeddings API for every
27
+ * provider. This implementation currently targets Anthropic / OpenAI-shaped
28
+ * SDKs: when the caller has direct API access via `ctx.modelRegistry`, they
29
+ * can wire this up by providing an `embedFn` override. We ship the hint-based
30
+ * detection here so future providers can plug in without touching callers.
31
+ */
32
+ export function buildEmbeddingFn(ctx) {
33
+ try {
34
+ const available = ctx.modelRegistry?.getAvailable?.();
35
+ if (!available || available.length === 0)
36
+ return null;
37
+ const candidate = available.find((model) => {
38
+ const id = typeof model?.id === "string" ? model.id.toLowerCase() : "";
39
+ return EMBEDDING_ID_HINTS.some((hint) => id.includes(hint));
40
+ });
41
+ if (!candidate)
42
+ return null;
43
+ // We don't currently have a provider-neutral embedding call in Pi; the
44
+ // detection surface is in place so wiring can happen once Pi offers it.
45
+ return null;
46
+ }
47
+ catch (err) {
48
+ logWarning("memory-embeddings", `model discovery failed: ${err.message}`);
49
+ return null;
50
+ }
51
+ }
52
+ // ─── Vector (de)serialization ───────────────────────────────────────────────
53
+ export function packFloat32(vec) {
54
+ return new Uint8Array(vec.buffer, vec.byteOffset, vec.byteLength);
55
+ }
56
+ export function unpackFloat32(blob) {
57
+ if (!blob)
58
+ return null;
59
+ try {
60
+ if (blob instanceof Float32Array)
61
+ return blob;
62
+ let view;
63
+ if (blob instanceof Uint8Array) {
64
+ view = blob;
65
+ }
66
+ else if (blob instanceof ArrayBuffer) {
67
+ view = new Uint8Array(blob);
68
+ }
69
+ else if (blob.buffer && blob.byteLength != null) {
70
+ const buf = blob;
71
+ view = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
72
+ }
73
+ else if (Array.isArray(blob)) {
74
+ return new Float32Array(blob);
75
+ }
76
+ else {
77
+ return null;
78
+ }
79
+ if (view.byteLength % 4 !== 0)
80
+ return null;
81
+ // Copy into an aligned buffer — BLOBs may arrive at odd byte offsets.
82
+ const aligned = new ArrayBuffer(view.byteLength);
83
+ new Uint8Array(aligned).set(view);
84
+ return new Float32Array(aligned);
85
+ }
86
+ catch {
87
+ return null;
88
+ }
89
+ }
90
+ // ─── Math ───────────────────────────────────────────────────────────────────
91
+ export function cosineSimilarity(a, b) {
92
+ if (a.length === 0 || a.length !== b.length)
93
+ return 0;
94
+ let dot = 0;
95
+ let normA = 0;
96
+ let normB = 0;
97
+ for (let i = 0; i < a.length; i++) {
98
+ const x = a[i];
99
+ const y = b[i];
100
+ dot += x * y;
101
+ normA += x * x;
102
+ normB += y * y;
103
+ }
104
+ if (normA === 0 || normB === 0)
105
+ return 0;
106
+ return dot / (Math.sqrt(normA) * Math.sqrt(normB));
107
+ }
108
+ // ─── Read helpers ───────────────────────────────────────────────────────────
109
+ export function getEmbeddingForMemory(memoryId) {
110
+ if (!isDbAvailable())
111
+ return null;
112
+ const adapter = _getAdapter();
113
+ if (!adapter)
114
+ return null;
115
+ try {
116
+ const row = adapter
117
+ .prepare("SELECT memory_id, model, dim, vector FROM memory_embeddings WHERE memory_id = :id")
118
+ .get({ ":id": memoryId });
119
+ if (!row)
120
+ return null;
121
+ const vector = unpackFloat32(row["vector"]);
122
+ if (!vector)
123
+ return null;
124
+ return {
125
+ memoryId: row["memory_id"],
126
+ model: row["model"],
127
+ dim: row["dim"],
128
+ vector,
129
+ };
130
+ }
131
+ catch {
132
+ return null;
133
+ }
134
+ }
135
+ export function loadAllEmbeddings() {
136
+ if (!isDbAvailable())
137
+ return [];
138
+ const adapter = _getAdapter();
139
+ if (!adapter)
140
+ return [];
141
+ try {
142
+ const rows = adapter
143
+ .prepare(`SELECT e.memory_id, e.model, e.dim, e.vector
144
+ FROM memory_embeddings e
145
+ JOIN memories m ON m.id = e.memory_id
146
+ WHERE m.superseded_by IS NULL`)
147
+ .all();
148
+ const out = [];
149
+ for (const row of rows) {
150
+ const vector = unpackFloat32(row["vector"]);
151
+ if (!vector)
152
+ continue;
153
+ out.push({
154
+ memoryId: row["memory_id"],
155
+ model: row["model"],
156
+ dim: row["dim"],
157
+ vector,
158
+ });
159
+ }
160
+ return out;
161
+ }
162
+ catch {
163
+ return [];
164
+ }
165
+ }
166
+ // ─── Write helpers ──────────────────────────────────────────────────────────
167
+ export function saveEmbedding(memoryId, vector, model) {
168
+ if (!isDbAvailable())
169
+ return false;
170
+ try {
171
+ upsertMemoryEmbedding({
172
+ memoryId,
173
+ model,
174
+ dim: vector.length,
175
+ vector: packFloat32(vector),
176
+ updatedAt: new Date().toISOString(),
177
+ });
178
+ return true;
179
+ }
180
+ catch {
181
+ return false;
182
+ }
183
+ }
184
+ export function removeEmbedding(memoryId) {
185
+ if (!isDbAvailable())
186
+ return false;
187
+ try {
188
+ return deleteMemoryEmbedding(memoryId);
189
+ }
190
+ catch {
191
+ return false;
192
+ }
193
+ }
194
+ // ─── Orchestration ──────────────────────────────────────────────────────────
195
+ /**
196
+ * Embed each memory's content via `embedFn` (if provided) and persist the
197
+ * resulting vectors. Returns the number of successfully embedded memories.
198
+ * Safe to call with embedFn=null — it becomes a no-op.
199
+ */
200
+ export async function embedMemories(memories, embedFn, model) {
201
+ if (!embedFn || memories.length === 0)
202
+ return 0;
203
+ try {
204
+ const vectors = await embedFn(memories.map((m) => m.content));
205
+ let count = 0;
206
+ for (let i = 0; i < memories.length && i < vectors.length; i++) {
207
+ const vector = vectors[i];
208
+ if (!vector || vector.length === 0)
209
+ continue;
210
+ if (saveEmbedding(memories[i].id, vector, model))
211
+ count++;
212
+ }
213
+ return count;
214
+ }
215
+ catch (err) {
216
+ logWarning("memory-embeddings", `embed failed: ${err.message}`);
217
+ return 0;
218
+ }
219
+ }
@@ -95,6 +95,14 @@ Actions (return JSON array):
95
95
  - UPDATE: {"action": "UPDATE", "id": "<MEM###>", "content": "<revised text>"}
96
96
  - REINFORCE: {"action": "REINFORCE", "id": "<MEM###>"}
97
97
  - SUPERSEDE: {"action": "SUPERSEDE", "id": "<MEM###>", "superseded_by": "<MEM###>"}
98
+ - LINK: {"action": "LINK", "from": "<MEM###>", "to": "<MEM###>", "rel": "<rel>", "confidence": <0.6-0.95>}
99
+
100
+ Link relation types:
101
+ - related_to — two memories cover the same area
102
+ - depends_on — "to" is a prerequisite for "from"
103
+ - contradicts — "from" conflicts with "to"
104
+ - elaborates — "from" expands on "to"
105
+ - supersedes — "from" replaces "to" (rarely needed; prefer SUPERSEDE)
98
106
 
99
107
  Rules:
100
108
  - Don't create memories for one-off bug fixes or temporary state
@@ -102,6 +110,7 @@ Rules:
102
110
  - Keep content to 1-3 sentences
103
111
  - Confidence: 0.6 tentative, 0.8 solid, 0.95 well-confirmed
104
112
  - Prefer fewer high-quality memories over many low-quality ones
113
+ - Only LINK memories that genuinely relate — don't fabricate edges
105
114
  - Return empty array [] if nothing worth remembering
106
115
  - NEVER include secrets, API keys, or passwords
107
116
 
@@ -219,6 +228,19 @@ export function parseMemoryResponse(raw) {
219
228
  });
220
229
  }
221
230
  break;
231
+ case 'LINK':
232
+ if (typeof item.from === 'string' &&
233
+ typeof item.to === 'string' &&
234
+ typeof item.rel === 'string') {
235
+ actions.push({
236
+ action: 'LINK',
237
+ from: item.from,
238
+ to: item.to,
239
+ rel: item.rel,
240
+ confidence: typeof item.confidence === 'number' ? item.confidence : undefined,
241
+ });
242
+ }
243
+ break;
222
244
  }
223
245
  }
224
246
  return actions;
@@ -227,27 +249,72 @@ export function parseMemoryResponse(raw) {
227
249
  return [];
228
250
  }
229
251
  }
230
- // ─── Main Extraction Function ───────────────────────────────────────────────
252
+ /**
253
+ * Core extractor — shared by unit post-processing and explicit ingest.
254
+ * Returns the applied actions for observability. Never throws.
255
+ */
256
+ export async function extractMemoriesFromTranscript(transcript, llmCallFn, opts) {
257
+ if (!opts.force) {
258
+ if (_extracting)
259
+ return [];
260
+ const now = Date.now();
261
+ if (now - _lastExtractionTime < MIN_EXTRACTION_INTERVAL_MS)
262
+ return [];
263
+ _lastExtractionTime = now;
264
+ }
265
+ const acquireMutex = !_extracting;
266
+ if (acquireMutex)
267
+ _extracting = true;
268
+ try {
269
+ const trimmed = transcript.trim();
270
+ if (!trimmed)
271
+ return [];
272
+ const safeTranscript = redactSecrets(trimmed);
273
+ const activeMemories = getActiveMemories().map((m) => ({
274
+ id: m.id,
275
+ category: m.category,
276
+ content: m.content,
277
+ }));
278
+ const userPrompt = buildExtractionUserPrompt(opts.sourceType, opts.sourceId, activeMemories, safeTranscript);
279
+ const response = await llmCallFn(EXTRACTION_SYSTEM, userPrompt);
280
+ const actions = parseMemoryResponse(response);
281
+ if (actions.length === 0)
282
+ return [];
283
+ const decorated = actions.map((action) => {
284
+ if (action.action !== 'CREATE')
285
+ return action;
286
+ return {
287
+ ...action,
288
+ scope: action.scope ?? opts.scope,
289
+ tags: action.tags ?? opts.tags,
290
+ };
291
+ });
292
+ applyMemoryActions(decorated, opts.sourceType, opts.sourceId);
293
+ return decorated;
294
+ }
295
+ catch {
296
+ return [];
297
+ }
298
+ finally {
299
+ if (acquireMutex)
300
+ _extracting = false;
301
+ }
302
+ }
231
303
  /**
232
304
  * Extract memories from a completed unit's activity log.
233
305
  * Fire-and-forget — never throws, mutex-guarded, respects rate limiting.
234
306
  */
235
307
  export async function extractMemoriesFromUnit(activityFile, unitType, unitId, llmCallFn) {
236
- // Mutex guard
237
308
  if (_extracting)
238
309
  return;
239
- // Rate limit
240
310
  const now = Date.now();
241
311
  if (now - _lastExtractionTime < MIN_EXTRACTION_INTERVAL_MS)
242
312
  return;
243
- // Skip certain unit types
244
313
  if (SKIP_TYPES.has(unitType))
245
314
  return;
246
315
  const unitKey = `${unitType}/${unitId}`;
247
- // Already processed
248
316
  if (isUnitProcessed(unitKey))
249
317
  return;
250
- // Check file size
251
318
  try {
252
319
  const stat = statSync(activityFile);
253
320
  if (stat.size < MIN_ACTIVITY_SIZE)
@@ -259,32 +326,16 @@ export async function extractMemoriesFromUnit(activityFile, unitType, unitId, ll
259
326
  _extracting = true;
260
327
  _lastExtractionTime = now;
261
328
  try {
262
- // Read and parse activity file
263
329
  const raw = readFileSync(activityFile, 'utf-8');
264
330
  const transcript = extractTranscriptFromActivity(raw);
265
331
  if (!transcript.trim())
266
332
  return;
267
- // Redact secrets
268
- const safeTranscript = redactSecrets(transcript);
269
- // Get current memories for context
270
- const activeMemories = getActiveMemories().map(m => ({
271
- id: m.id,
272
- category: m.category,
273
- content: m.content,
274
- }));
275
- // Build prompts
276
- const userPrompt = buildExtractionUserPrompt(unitType, unitId, activeMemories, safeTranscript);
277
- // Call LLM
278
- const response = await llmCallFn(EXTRACTION_SYSTEM, userPrompt);
279
- // Parse response
280
- const actions = parseMemoryResponse(response);
281
- // Apply actions
282
- if (actions.length > 0) {
283
- applyMemoryActions(actions, unitType, unitId);
284
- }
285
- // Decay stale memories periodically
333
+ await extractMemoriesFromTranscript(transcript, llmCallFn, {
334
+ sourceType: unitType,
335
+ sourceId: unitId,
336
+ force: true,
337
+ });
286
338
  decayStaleMemories(20);
287
- // Mark unit as processed
288
339
  markUnitProcessed(unitKey, activityFile);
289
340
  }
290
341
  catch {