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
|
@@ -135,7 +135,7 @@ function openRawDb(path) {
|
|
|
135
135
|
const Database = providerModule;
|
|
136
136
|
return new Database(path);
|
|
137
137
|
}
|
|
138
|
-
const SCHEMA_VERSION =
|
|
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
|
-
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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 {
|