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.
- package/dist/onboarding.d.ts +5 -1
- package/dist/onboarding.js +5 -3
- package/dist/resources/extensions/gsd/auto-model-selection.js +1 -1
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +23 -19
- package/dist/resources/extensions/gsd/bootstrap/memory-tools.js +128 -0
- package/dist/resources/extensions/gsd/bootstrap/register-extension.js +2 -0
- package/dist/resources/extensions/gsd/bootstrap/system-context.js +17 -4
- package/dist/resources/extensions/gsd/commands/handlers/onboarding.js +52 -68
- package/dist/resources/extensions/gsd/commands/handlers/ops.js +5 -0
- package/dist/resources/extensions/gsd/commands-memory.js +462 -0
- package/dist/resources/extensions/gsd/gsd-db.js +237 -4
- package/dist/resources/extensions/gsd/memory-embeddings.js +219 -0
- package/dist/resources/extensions/gsd/memory-extractor.js +78 -27
- package/dist/resources/extensions/gsd/memory-ingest.js +218 -0
- package/dist/resources/extensions/gsd/memory-relations.js +189 -0
- package/dist/resources/extensions/gsd/memory-source-store.js +113 -0
- package/dist/resources/extensions/gsd/memory-store.js +299 -6
- package/dist/resources/extensions/gsd/model-router.js +9 -5
- package/dist/resources/extensions/gsd/notification-overlay.js +7 -22
- package/dist/resources/extensions/gsd/tools/memory-tools.js +306 -0
- package/dist/resources/extensions/gsd/tools/skip-slice.js +78 -0
- package/dist/tsconfig.extensions.tsbuildinfo +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +14 -14
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +14 -14
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/package.json +1 -1
- package/packages/mcp-server/dist/server.d.ts.map +1 -1
- package/packages/mcp-server/dist/server.js +12 -10
- package/packages/mcp-server/dist/server.js.map +1 -1
- package/packages/mcp-server/dist/session-manager.d.ts.map +1 -1
- package/packages/mcp-server/dist/session-manager.js +8 -1
- package/packages/mcp-server/dist/session-manager.js.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.d.ts +1 -0
- package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js +113 -14
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/src/mcp-server.test.ts +40 -4
- package/packages/mcp-server/src/server.ts +12 -10
- package/packages/mcp-server/src/session-manager.ts +10 -3
- package/packages/mcp-server/src/workflow-tools.test.ts +91 -1
- package/packages/mcp-server/src/workflow-tools.ts +128 -18
- package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
- package/src/resources/extensions/gsd/auto-model-selection.ts +1 -1
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +24 -20
- package/src/resources/extensions/gsd/bootstrap/memory-tools.ts +158 -0
- package/src/resources/extensions/gsd/bootstrap/register-extension.ts +2 -0
- package/src/resources/extensions/gsd/bootstrap/system-context.ts +20 -4
- package/src/resources/extensions/gsd/commands/handlers/onboarding.ts +65 -98
- package/src/resources/extensions/gsd/commands/handlers/ops.ts +5 -0
- package/src/resources/extensions/gsd/commands-memory.ts +551 -0
- package/src/resources/extensions/gsd/gsd-db.ts +273 -4
- package/src/resources/extensions/gsd/memory-embeddings.ts +235 -0
- package/src/resources/extensions/gsd/memory-extractor.ts +100 -34
- package/src/resources/extensions/gsd/memory-ingest.ts +286 -0
- package/src/resources/extensions/gsd/memory-relations.ts +240 -0
- package/src/resources/extensions/gsd/memory-source-store.ts +138 -0
- package/src/resources/extensions/gsd/memory-store.ts +351 -7
- package/src/resources/extensions/gsd/model-router.ts +10 -5
- package/src/resources/extensions/gsd/notification-overlay.ts +9 -19
- package/src/resources/extensions/gsd/tests/auto-model-selection.test.ts +12 -0
- package/src/resources/extensions/gsd/tests/complete-slice.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/complete-task.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/escalation.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/md-importer.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/memory-embeddings.test.ts +213 -0
- package/src/resources/extensions/gsd/tests/memory-ingest.test.ts +153 -0
- package/src/resources/extensions/gsd/tests/memory-maintenance.test.ts +107 -0
- package/src/resources/extensions/gsd/tests/memory-relations.test.ts +175 -0
- package/src/resources/extensions/gsd/tests/memory-store.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/memory-tools.test.ts +295 -0
- package/src/resources/extensions/gsd/tests/model-router.test.ts +50 -0
- package/src/resources/extensions/gsd/tests/notification-overlay.test.ts +56 -37
- package/src/resources/extensions/gsd/tests/skip-slice-cascades-tasks.test.ts +125 -0
- package/src/resources/extensions/gsd/tools/memory-tools.ts +380 -0
- package/src/resources/extensions/gsd/tools/skip-slice.ts +133 -0
- package/src/resources/extensions/gsd/workflow-logger.ts +3 -1
- /package/dist/web/standalone/.next/static/{8FZqxNe9FxQDmsbRzR8tA → o61X3klsB6C0UE0X1x3PA}/_buildManifest.js +0 -0
- /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 =
|
|
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
|
+
}
|