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
|
@@ -15,7 +15,10 @@ import {
|
|
|
15
15
|
markMemoryUnitProcessed,
|
|
16
16
|
decayMemoriesBefore,
|
|
17
17
|
supersedeLowestRankedMemories,
|
|
18
|
+
deleteMemoryEmbedding,
|
|
19
|
+
deleteMemoryRelationsFor,
|
|
18
20
|
} from './gsd-db.js';
|
|
21
|
+
import { createMemoryRelation, isValidRelation } from './memory-relations.js';
|
|
19
22
|
|
|
20
23
|
// ─── Types ──────────────────────────────────────────────────────────────────
|
|
21
24
|
|
|
@@ -31,6 +34,8 @@ export interface Memory {
|
|
|
31
34
|
updated_at: string;
|
|
32
35
|
superseded_by: string | null;
|
|
33
36
|
hit_count: number;
|
|
37
|
+
scope: string;
|
|
38
|
+
tags: string[];
|
|
34
39
|
}
|
|
35
40
|
|
|
36
41
|
export type MemoryActionCreate = {
|
|
@@ -38,6 +43,8 @@ export type MemoryActionCreate = {
|
|
|
38
43
|
category: string;
|
|
39
44
|
content: string;
|
|
40
45
|
confidence?: number;
|
|
46
|
+
scope?: string;
|
|
47
|
+
tags?: string[];
|
|
41
48
|
};
|
|
42
49
|
|
|
43
50
|
export type MemoryActionUpdate = {
|
|
@@ -58,11 +65,20 @@ export type MemoryActionSupersede = {
|
|
|
58
65
|
superseded_by: string;
|
|
59
66
|
};
|
|
60
67
|
|
|
68
|
+
export type MemoryActionLink = {
|
|
69
|
+
action: 'LINK';
|
|
70
|
+
from: string;
|
|
71
|
+
to: string;
|
|
72
|
+
rel: string;
|
|
73
|
+
confidence?: number;
|
|
74
|
+
};
|
|
75
|
+
|
|
61
76
|
export type MemoryAction =
|
|
62
77
|
| MemoryActionCreate
|
|
63
78
|
| MemoryActionUpdate
|
|
64
79
|
| MemoryActionReinforce
|
|
65
|
-
| MemoryActionSupersede
|
|
80
|
+
| MemoryActionSupersede
|
|
81
|
+
| MemoryActionLink;
|
|
66
82
|
|
|
67
83
|
// ─── Category Display Order ─────────────────────────────────────────────────
|
|
68
84
|
|
|
@@ -90,9 +106,21 @@ function rowToMemory(row: Record<string, unknown>): Memory {
|
|
|
90
106
|
updated_at: row['updated_at'] as string,
|
|
91
107
|
superseded_by: (row['superseded_by'] as string) ?? null,
|
|
92
108
|
hit_count: row['hit_count'] as number,
|
|
109
|
+
scope: (row['scope'] as string) ?? 'project',
|
|
110
|
+
tags: parseTags(row['tags']),
|
|
93
111
|
};
|
|
94
112
|
}
|
|
95
113
|
|
|
114
|
+
function parseTags(raw: unknown): string[] {
|
|
115
|
+
if (typeof raw !== 'string' || raw.length === 0) return [];
|
|
116
|
+
try {
|
|
117
|
+
const parsed = JSON.parse(raw);
|
|
118
|
+
return Array.isArray(parsed) ? parsed.filter((t): t is string => typeof t === 'string') : [];
|
|
119
|
+
} catch {
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
96
124
|
// ─── Query Functions ────────────────────────────────────────────────────────
|
|
97
125
|
|
|
98
126
|
/**
|
|
@@ -134,6 +162,280 @@ export function getActiveMemoriesRanked(limit = 30): Memory[] {
|
|
|
134
162
|
}
|
|
135
163
|
}
|
|
136
164
|
|
|
165
|
+
// ─── Hybrid query (keyword FTS + optional semantic) ─────────────────────────
|
|
166
|
+
|
|
167
|
+
export interface QueryMemoriesFilters {
|
|
168
|
+
category?: string;
|
|
169
|
+
scope?: string;
|
|
170
|
+
tag?: string;
|
|
171
|
+
include_superseded?: boolean;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export interface QueryMemoriesOptions extends QueryMemoriesFilters {
|
|
175
|
+
query: string;
|
|
176
|
+
k?: number;
|
|
177
|
+
/**
|
|
178
|
+
* Optional query-side embedding. When provided and embeddings exist in the
|
|
179
|
+
* DB, results are fused with cosine similarity via reciprocal-rank-fusion.
|
|
180
|
+
*/
|
|
181
|
+
queryVector?: Float32Array | null;
|
|
182
|
+
/** RRF fusion constant (default 60). */
|
|
183
|
+
rrfK?: number;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export interface RankedMemory {
|
|
187
|
+
memory: Memory;
|
|
188
|
+
score: number;
|
|
189
|
+
keywordRank: number | null;
|
|
190
|
+
semanticRank: number | null;
|
|
191
|
+
confidenceBoost: number;
|
|
192
|
+
reason: 'keyword' | 'semantic' | 'both' | 'ranked';
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function queryMemoriesRanked(opts: QueryMemoriesOptions): RankedMemory[] {
|
|
196
|
+
if (!isDbAvailable()) return [];
|
|
197
|
+
const adapter = _getAdapter();
|
|
198
|
+
if (!adapter) return [];
|
|
199
|
+
|
|
200
|
+
const k = clampLimit(opts.k, 10);
|
|
201
|
+
const rrfK = opts.rrfK ?? 60;
|
|
202
|
+
const activeClause = opts.include_superseded === true ? '' : 'WHERE superseded_by IS NULL';
|
|
203
|
+
const trimmedQuery = (opts.query ?? '').trim();
|
|
204
|
+
|
|
205
|
+
// 1) Keyword hits — try FTS5 first, fall back to LIKE when unavailable.
|
|
206
|
+
const keywordHits = trimmedQuery ? keywordSearch(adapter, trimmedQuery, activeClause, 50) : [];
|
|
207
|
+
|
|
208
|
+
// 2) Semantic hits — cosine over memory_embeddings. Requires opts.queryVector.
|
|
209
|
+
const semanticHits = opts.queryVector
|
|
210
|
+
? semanticSearch(adapter, opts.queryVector, activeClause, 50)
|
|
211
|
+
: [];
|
|
212
|
+
|
|
213
|
+
if (keywordHits.length === 0 && semanticHits.length === 0 && !trimmedQuery) {
|
|
214
|
+
// No query at all — fall back to the existing ranked-by-score listing.
|
|
215
|
+
return getActiveMemoriesRanked(k).map((memory) => ({
|
|
216
|
+
memory,
|
|
217
|
+
score: memory.confidence * (1 + memory.hit_count * 0.1),
|
|
218
|
+
keywordRank: null,
|
|
219
|
+
semanticRank: null,
|
|
220
|
+
confidenceBoost: memory.confidence * (1 + memory.hit_count * 0.1),
|
|
221
|
+
reason: 'ranked' as const,
|
|
222
|
+
})).filter((hit) => passesFilters(hit.memory, opts));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// 3) Reciprocal rank fusion — each hit contributes 1/(rrfK + rank).
|
|
226
|
+
const fused = new Map<string, { memory: Memory; kwRank: number | null; semRank: number | null; score: number }>();
|
|
227
|
+
|
|
228
|
+
for (let i = 0; i < keywordHits.length; i++) {
|
|
229
|
+
const hit = keywordHits[i];
|
|
230
|
+
const existing = fused.get(hit.id);
|
|
231
|
+
const rrf = 1 / (rrfK + i + 1);
|
|
232
|
+
if (existing) {
|
|
233
|
+
existing.kwRank = i + 1;
|
|
234
|
+
existing.score += rrf;
|
|
235
|
+
} else {
|
|
236
|
+
fused.set(hit.id, { memory: hit, kwRank: i + 1, semRank: null, score: rrf });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
for (let i = 0; i < semanticHits.length; i++) {
|
|
241
|
+
const hit = semanticHits[i];
|
|
242
|
+
const existing = fused.get(hit.id);
|
|
243
|
+
const rrf = 1 / (rrfK + i + 1);
|
|
244
|
+
if (existing) {
|
|
245
|
+
existing.semRank = i + 1;
|
|
246
|
+
existing.score += rrf;
|
|
247
|
+
} else {
|
|
248
|
+
fused.set(hit.id, { memory: hit, kwRank: null, semRank: i + 1, score: rrf });
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// 4) Apply filters + confidence boost, then sort.
|
|
253
|
+
const ranked: RankedMemory[] = [];
|
|
254
|
+
for (const entry of fused.values()) {
|
|
255
|
+
if (!passesFilters(entry.memory, opts)) continue;
|
|
256
|
+
const boost = entry.memory.confidence * (1 + entry.memory.hit_count * 0.1);
|
|
257
|
+
const reason: RankedMemory['reason'] =
|
|
258
|
+
entry.kwRank != null && entry.semRank != null
|
|
259
|
+
? 'both'
|
|
260
|
+
: entry.kwRank != null
|
|
261
|
+
? 'keyword'
|
|
262
|
+
: 'semantic';
|
|
263
|
+
ranked.push({
|
|
264
|
+
memory: entry.memory,
|
|
265
|
+
score: entry.score * boost,
|
|
266
|
+
keywordRank: entry.kwRank,
|
|
267
|
+
semanticRank: entry.semRank,
|
|
268
|
+
confidenceBoost: boost,
|
|
269
|
+
reason,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
ranked.sort((a, b) => b.score - a.score);
|
|
274
|
+
return ranked.slice(0, k);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function clampLimit(value: unknown, fallback: number): number {
|
|
278
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) return fallback;
|
|
279
|
+
if (value < 1) return 1;
|
|
280
|
+
if (value > 100) return 100;
|
|
281
|
+
return Math.floor(value);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function passesFilters(memory: Memory, filters: QueryMemoriesFilters): boolean {
|
|
285
|
+
if (filters.category && memory.category.toLowerCase() !== filters.category.toLowerCase()) return false;
|
|
286
|
+
if (filters.scope && memory.scope !== filters.scope) return false;
|
|
287
|
+
if (filters.tag) {
|
|
288
|
+
const needle = filters.tag.toLowerCase();
|
|
289
|
+
if (!memory.tags.map((t) => t.toLowerCase()).includes(needle)) return false;
|
|
290
|
+
}
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function keywordSearch(
|
|
295
|
+
adapter: NonNullable<ReturnType<typeof _getAdapter>>,
|
|
296
|
+
rawQuery: string,
|
|
297
|
+
activeClause: string,
|
|
298
|
+
limit: number,
|
|
299
|
+
): Memory[] {
|
|
300
|
+
const ftsAvailable = isFtsAvailable(adapter);
|
|
301
|
+
if (ftsAvailable) {
|
|
302
|
+
try {
|
|
303
|
+
const matchExpr = toFtsMatchExpr(rawQuery);
|
|
304
|
+
if (!matchExpr) return [];
|
|
305
|
+
const activePart = activeClause ? `AND m.${activeClause.replace(/^WHERE\s+/i, '')}` : '';
|
|
306
|
+
const rows = adapter.prepare(
|
|
307
|
+
`SELECT m.*
|
|
308
|
+
FROM memories_fts f
|
|
309
|
+
JOIN memories m ON m.seq = f.rowid
|
|
310
|
+
WHERE memories_fts MATCH :match
|
|
311
|
+
${activePart}
|
|
312
|
+
ORDER BY bm25(memories_fts)
|
|
313
|
+
LIMIT :limit`,
|
|
314
|
+
).all({ ':match': matchExpr, ':limit': limit });
|
|
315
|
+
return rows.map(rowToMemory);
|
|
316
|
+
} catch {
|
|
317
|
+
// fall through to LIKE
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// LIKE fallback — scans the candidate pool.
|
|
322
|
+
const terms = rawQuery
|
|
323
|
+
.toLowerCase()
|
|
324
|
+
.split(/[^a-z0-9_]+/)
|
|
325
|
+
.filter((t) => t.length >= 2);
|
|
326
|
+
if (terms.length === 0) return [];
|
|
327
|
+
|
|
328
|
+
const rows = adapter.prepare(`SELECT * FROM memories ${activeClause}`).all();
|
|
329
|
+
const scored: Array<{ memory: Memory; score: number }> = [];
|
|
330
|
+
for (const row of rows) {
|
|
331
|
+
const memory = rowToMemory(row);
|
|
332
|
+
const lower = memory.content.toLowerCase();
|
|
333
|
+
let score = 0;
|
|
334
|
+
for (const term of terms) {
|
|
335
|
+
const idx = lower.indexOf(term);
|
|
336
|
+
if (idx === -1) continue;
|
|
337
|
+
score += 1 + (term.length >= 5 ? 0.5 : 0);
|
|
338
|
+
}
|
|
339
|
+
if (score > 0) scored.push({ memory, score });
|
|
340
|
+
}
|
|
341
|
+
scored.sort((a, b) => b.score - a.score);
|
|
342
|
+
return scored.slice(0, limit).map((s) => s.memory);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function isFtsAvailable(adapter: NonNullable<ReturnType<typeof _getAdapter>>): boolean {
|
|
346
|
+
try {
|
|
347
|
+
const row = adapter
|
|
348
|
+
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='memories_fts'")
|
|
349
|
+
.get();
|
|
350
|
+
return !!row;
|
|
351
|
+
} catch {
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function toFtsMatchExpr(query: string): string | null {
|
|
357
|
+
// Build a tolerant AND expression: quote each bare term with a trailing *.
|
|
358
|
+
const tokens = query
|
|
359
|
+
.toLowerCase()
|
|
360
|
+
.split(/[^a-z0-9_]+/)
|
|
361
|
+
.filter((t) => t.length >= 2)
|
|
362
|
+
.slice(0, 8);
|
|
363
|
+
if (tokens.length === 0) return null;
|
|
364
|
+
return tokens.map((t) => `"${t.replace(/"/g, '""')}"*`).join(' OR ');
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function semanticSearch(
|
|
368
|
+
adapter: NonNullable<ReturnType<typeof _getAdapter>>,
|
|
369
|
+
queryVector: Float32Array,
|
|
370
|
+
activeClause: string,
|
|
371
|
+
limit: number,
|
|
372
|
+
): Memory[] {
|
|
373
|
+
try {
|
|
374
|
+
const rows = adapter
|
|
375
|
+
.prepare(
|
|
376
|
+
`SELECT m.*, e.vector as embedding_vector, e.dim as embedding_dim
|
|
377
|
+
FROM memories m
|
|
378
|
+
JOIN memory_embeddings e ON e.memory_id = m.id
|
|
379
|
+
${activeClause}`,
|
|
380
|
+
)
|
|
381
|
+
.all();
|
|
382
|
+
|
|
383
|
+
const scored: Array<{ memory: Memory; sim: number }> = [];
|
|
384
|
+
for (const row of rows) {
|
|
385
|
+
const dim = row['embedding_dim'] as number;
|
|
386
|
+
if (dim !== queryVector.length) continue;
|
|
387
|
+
const vector = unpackVector(row['embedding_vector'], dim);
|
|
388
|
+
if (!vector) continue;
|
|
389
|
+
const sim = cosine(queryVector, vector);
|
|
390
|
+
if (sim <= 0) continue;
|
|
391
|
+
scored.push({ memory: rowToMemory(row), sim });
|
|
392
|
+
}
|
|
393
|
+
scored.sort((a, b) => b.sim - a.sim);
|
|
394
|
+
return scored.slice(0, limit).map((s) => s.memory);
|
|
395
|
+
} catch {
|
|
396
|
+
return [];
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function unpackVector(blob: unknown, dim: number): Float32Array | null {
|
|
401
|
+
if (!blob) return null;
|
|
402
|
+
try {
|
|
403
|
+
let view: Uint8Array | null = null;
|
|
404
|
+
if (blob instanceof Float32Array) return blob;
|
|
405
|
+
if (blob instanceof Uint8Array) view = blob;
|
|
406
|
+
else if (blob instanceof ArrayBuffer) view = new Uint8Array(blob);
|
|
407
|
+
else if ((blob as Buffer).buffer && (blob as Buffer).byteLength != null) {
|
|
408
|
+
const buf = blob as Buffer;
|
|
409
|
+
view = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
410
|
+
} else if (Array.isArray(blob)) {
|
|
411
|
+
return new Float32Array(blob as number[]);
|
|
412
|
+
}
|
|
413
|
+
if (!view || view.byteLength % 4 !== 0) return null;
|
|
414
|
+
const aligned = new ArrayBuffer(view.byteLength);
|
|
415
|
+
new Uint8Array(aligned).set(view);
|
|
416
|
+
const f32 = new Float32Array(aligned);
|
|
417
|
+
return f32.length === dim ? f32 : null;
|
|
418
|
+
} catch {
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function cosine(a: Float32Array, b: Float32Array): number {
|
|
424
|
+
if (a.length === 0 || a.length !== b.length) return 0;
|
|
425
|
+
let dot = 0;
|
|
426
|
+
let na = 0;
|
|
427
|
+
let nb = 0;
|
|
428
|
+
for (let i = 0; i < a.length; i++) {
|
|
429
|
+
const x = a[i];
|
|
430
|
+
const y = b[i];
|
|
431
|
+
dot += x * y;
|
|
432
|
+
na += x * x;
|
|
433
|
+
nb += y * y;
|
|
434
|
+
}
|
|
435
|
+
if (na === 0 || nb === 0) return 0;
|
|
436
|
+
return dot / (Math.sqrt(na) * Math.sqrt(nb));
|
|
437
|
+
}
|
|
438
|
+
|
|
137
439
|
/**
|
|
138
440
|
* Generate the next memory ID: MEM + zero-padded 3-digit from MAX(seq).
|
|
139
441
|
* Returns MEM001 if no memories exist.
|
|
@@ -173,6 +475,8 @@ export function createMemory(fields: {
|
|
|
173
475
|
confidence?: number;
|
|
174
476
|
source_unit_type?: string;
|
|
175
477
|
source_unit_id?: string;
|
|
478
|
+
scope?: string;
|
|
479
|
+
tags?: string[];
|
|
176
480
|
}): string | null {
|
|
177
481
|
if (!isDbAvailable()) return null;
|
|
178
482
|
const adapter = _getAdapter();
|
|
@@ -191,6 +495,8 @@ export function createMemory(fields: {
|
|
|
191
495
|
sourceUnitId: fields.source_unit_id ?? null,
|
|
192
496
|
createdAt: now,
|
|
193
497
|
updatedAt: now,
|
|
498
|
+
scope: fields.scope ?? 'project',
|
|
499
|
+
tags: fields.tags ?? [],
|
|
194
500
|
});
|
|
195
501
|
// Derive the real ID from the assigned seq (SELECT is still fine via adapter)
|
|
196
502
|
const row = adapter.prepare('SELECT seq FROM memories WHERE id = :id').get({ ':id': placeholder });
|
|
@@ -285,11 +591,12 @@ export function markUnitProcessed(unitKey: string, activityFile: string): boolea
|
|
|
285
591
|
/**
|
|
286
592
|
* Reduce confidence for memories not updated within the last N processed units.
|
|
287
593
|
* "Stale" = updated_at is older than the Nth most recent processed_at.
|
|
594
|
+
* Returns the number of decayed memory IDs for observability.
|
|
288
595
|
*/
|
|
289
|
-
export function decayStaleMemories(thresholdUnits = 20):
|
|
290
|
-
if (!isDbAvailable()) return;
|
|
596
|
+
export function decayStaleMemories(thresholdUnits = 20): string[] {
|
|
597
|
+
if (!isDbAvailable()) return [];
|
|
291
598
|
const adapter = _getAdapter();
|
|
292
|
-
if (!adapter) return;
|
|
599
|
+
if (!adapter) return [];
|
|
293
600
|
|
|
294
601
|
try {
|
|
295
602
|
// Find the timestamp of the Nth most recent processed unit (read-only SELECT)
|
|
@@ -299,17 +606,24 @@ export function decayStaleMemories(thresholdUnits = 20): void {
|
|
|
299
606
|
LIMIT 1 OFFSET :offset`,
|
|
300
607
|
).get({ ':offset': thresholdUnits - 1 });
|
|
301
608
|
|
|
302
|
-
if (!row) return; // not enough processed units yet
|
|
609
|
+
if (!row) return []; // not enough processed units yet
|
|
303
610
|
|
|
304
611
|
const cutoff = row['processed_at'] as string;
|
|
612
|
+
const affected = adapter.prepare(
|
|
613
|
+
`SELECT id FROM memories
|
|
614
|
+
WHERE superseded_by IS NULL AND updated_at < :cutoff AND confidence > 0.1`,
|
|
615
|
+
).all({ ':cutoff': cutoff }).map((r) => r['id'] as string);
|
|
616
|
+
|
|
305
617
|
decayMemoriesBefore(cutoff, new Date().toISOString());
|
|
618
|
+
return affected;
|
|
306
619
|
} catch {
|
|
307
|
-
|
|
620
|
+
return [];
|
|
308
621
|
}
|
|
309
622
|
}
|
|
310
623
|
|
|
311
624
|
/**
|
|
312
|
-
* Supersede lowest-ranked memories when count exceeds cap.
|
|
625
|
+
* Supersede lowest-ranked memories when count exceeds cap. Cascades to the
|
|
626
|
+
* embedding and relation rows so those tables don't grow unboundedly.
|
|
313
627
|
*/
|
|
314
628
|
export function enforceMemoryCap(max = 50): void {
|
|
315
629
|
if (!isDbAvailable()) return;
|
|
@@ -324,7 +638,21 @@ export function enforceMemoryCap(max = 50): void {
|
|
|
324
638
|
if (count <= max) return;
|
|
325
639
|
|
|
326
640
|
const excess = count - max;
|
|
641
|
+
// Capture the about-to-be-superseded IDs first so we can cascade cleanup.
|
|
642
|
+
const victims = adapter.prepare(
|
|
643
|
+
`SELECT id FROM memories
|
|
644
|
+
WHERE superseded_by IS NULL
|
|
645
|
+
ORDER BY (confidence * (1.0 + hit_count * 0.1)) ASC
|
|
646
|
+
LIMIT :limit`,
|
|
647
|
+
).all({ ':limit': excess }).map((row) => row['id'] as string);
|
|
648
|
+
|
|
327
649
|
supersedeLowestRankedMemories(excess, new Date().toISOString());
|
|
650
|
+
|
|
651
|
+
if (victims.length === 0) return;
|
|
652
|
+
for (const id of victims) {
|
|
653
|
+
try { deleteMemoryEmbedding(id); } catch { /* non-fatal */ }
|
|
654
|
+
try { deleteMemoryRelationsFor(id); } catch { /* non-fatal */ }
|
|
655
|
+
}
|
|
328
656
|
} catch {
|
|
329
657
|
// non-fatal
|
|
330
658
|
}
|
|
@@ -354,6 +682,8 @@ export function applyMemoryActions(
|
|
|
354
682
|
confidence: action.confidence,
|
|
355
683
|
source_unit_type: unitType,
|
|
356
684
|
source_unit_id: unitId,
|
|
685
|
+
scope: action.scope,
|
|
686
|
+
tags: action.tags,
|
|
357
687
|
});
|
|
358
688
|
break;
|
|
359
689
|
case 'UPDATE':
|
|
@@ -365,6 +695,9 @@ export function applyMemoryActions(
|
|
|
365
695
|
case 'SUPERSEDE':
|
|
366
696
|
supersedeMemory(action.id, action.superseded_by);
|
|
367
697
|
break;
|
|
698
|
+
case 'LINK':
|
|
699
|
+
applyLinkAction(action);
|
|
700
|
+
break;
|
|
368
701
|
}
|
|
369
702
|
}
|
|
370
703
|
enforceMemoryCap();
|
|
@@ -374,6 +707,17 @@ export function applyMemoryActions(
|
|
|
374
707
|
}
|
|
375
708
|
}
|
|
376
709
|
|
|
710
|
+
// ─── LINK action ────────────────────────────────────────────────────────────
|
|
711
|
+
|
|
712
|
+
function applyLinkAction(action: MemoryActionLink): void {
|
|
713
|
+
try {
|
|
714
|
+
if (!isValidRelation(action.rel)) return;
|
|
715
|
+
createMemoryRelation(action.from, action.to, action.rel, action.confidence);
|
|
716
|
+
} catch {
|
|
717
|
+
// Link failures should never break memory extraction.
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
377
721
|
// ─── Prompt Formatting ──────────────────────────────────────────────────────
|
|
378
722
|
|
|
379
723
|
/**
|
|
@@ -279,8 +279,9 @@ export function scoreEligibleModels(
|
|
|
279
279
|
capabilityOverrides?: Record<string, Partial<ModelCapabilities>>,
|
|
280
280
|
): Array<{ modelId: string; score: number }> {
|
|
281
281
|
const scored = eligibleModelIds.map(modelId => {
|
|
282
|
-
const
|
|
283
|
-
const
|
|
282
|
+
const bareId = bareModelId(modelId);
|
|
283
|
+
const builtin = MODEL_CAPABILITY_PROFILES[bareId];
|
|
284
|
+
const override = capabilityOverrides?.[modelId] ?? capabilityOverrides?.[bareId];
|
|
284
285
|
const profile: ModelCapabilities = builtin
|
|
285
286
|
? override ? { ...builtin, ...override } : builtin
|
|
286
287
|
: { coding: 50, debugging: 50, research: 50, reasoning: 50, speed: 50, longContext: 50, instruction: 50 };
|
|
@@ -516,7 +517,7 @@ export function defaultRoutingConfig(): DynamicRoutingConfig {
|
|
|
516
517
|
|
|
517
518
|
function getModelTier(modelId: string): ComplexityTier {
|
|
518
519
|
// Strip provider prefix if present
|
|
519
|
-
const bareId =
|
|
520
|
+
const bareId = bareModelId(modelId);
|
|
520
521
|
|
|
521
522
|
// Check exact match first
|
|
522
523
|
if (MODEL_CAPABILITY_TIER[bareId]) return MODEL_CAPABILITY_TIER[bareId];
|
|
@@ -532,7 +533,7 @@ function getModelTier(modelId: string): ComplexityTier {
|
|
|
532
533
|
|
|
533
534
|
/** Check if a model ID has a known capability tier mapping. (#2192) */
|
|
534
535
|
function isKnownModel(modelId: string): boolean {
|
|
535
|
-
const bareId =
|
|
536
|
+
const bareId = bareModelId(modelId);
|
|
536
537
|
if (MODEL_CAPABILITY_TIER[bareId]) return true;
|
|
537
538
|
for (const knownId of Object.keys(MODEL_CAPABILITY_TIER)) {
|
|
538
539
|
if (bareId.includes(knownId) || knownId.includes(bareId)) return true;
|
|
@@ -541,7 +542,7 @@ function isKnownModel(modelId: string): boolean {
|
|
|
541
542
|
}
|
|
542
543
|
|
|
543
544
|
function getModelCost(modelId: string): number {
|
|
544
|
-
const bareId =
|
|
545
|
+
const bareId = bareModelId(modelId);
|
|
545
546
|
|
|
546
547
|
if (MODEL_COST_PER_1K_INPUT[bareId] !== undefined) {
|
|
547
548
|
return MODEL_COST_PER_1K_INPUT[bareId];
|
|
@@ -556,6 +557,10 @@ function getModelCost(modelId: string): number {
|
|
|
556
557
|
return 999;
|
|
557
558
|
}
|
|
558
559
|
|
|
560
|
+
function bareModelId(modelId: string): string {
|
|
561
|
+
return modelId.includes("/") ? modelId.split("/").pop()! : modelId;
|
|
562
|
+
}
|
|
563
|
+
|
|
559
564
|
// ─── Tool Compatibility Filter (ADR-005 Phase 3) ───────────────────────────
|
|
560
565
|
|
|
561
566
|
/**
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Toggled with Ctrl+Alt+N (⌃⌥N on macOS), Ctrl+Shift+N fallback, or /gsd notifications.
|
|
4
4
|
|
|
5
5
|
import type { Theme } from "@gsd/pi-coding-agent";
|
|
6
|
-
import { truncateToWidth, visibleWidth, matchesKey, Key } from "@gsd/pi-tui";
|
|
6
|
+
import { truncateToWidth, visibleWidth, wrapTextWithAnsi, matchesKey, Key } from "@gsd/pi-tui";
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
9
|
readNotifications,
|
|
@@ -29,25 +29,15 @@ function severityIcon(severity: NotifySeverity): string {
|
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
/**
|
|
32
|
+
/** Column-aware word wrap using pi-tui's native wrapper (handles unicode/ANSI). */
|
|
33
33
|
function wrapText(text: string, maxWidth: number): string[] {
|
|
34
|
-
if (
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
} else if (current.length + 1 + word.length <= maxWidth) {
|
|
42
|
-
current += " " + word;
|
|
43
|
-
} else {
|
|
44
|
-
lines.push(current);
|
|
45
|
-
current = word;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
if (current.length > 0) lines.push(current);
|
|
49
|
-
// If a single word exceeds maxWidth, truncate it
|
|
50
|
-
return lines.map((l) => l.length > maxWidth ? l.slice(0, maxWidth - 1) + "…" : l);
|
|
34
|
+
if (maxWidth <= 0) return [text];
|
|
35
|
+
const lines = wrapTextWithAnsi(text, maxWidth);
|
|
36
|
+
// Safety clamp: if any line still exceeds maxWidth (e.g. unbreakable long token),
|
|
37
|
+
// truncate it with an ellipsis so it cannot bleed past the box border.
|
|
38
|
+
return lines.map((l) =>
|
|
39
|
+
visibleWidth(l) > maxWidth ? truncateToWidth(l, maxWidth, "…") : l,
|
|
40
|
+
);
|
|
51
41
|
}
|
|
52
42
|
|
|
53
43
|
function formatTimestamp(ts: string): string {
|
|
@@ -247,6 +247,18 @@ test("model policy receives task metadata for requirement-vector decisions", ()
|
|
|
247
247
|
);
|
|
248
248
|
});
|
|
249
249
|
|
|
250
|
+
test("dynamic routing passes provider-qualified model keys to the router", () => {
|
|
251
|
+
const src = readFileSync(join(__dirname, "..", "auto-model-selection.ts"), "utf-8");
|
|
252
|
+
assert.ok(
|
|
253
|
+
src.includes("routingEligibleModels.map(m => `${m.provider}/${m.id}`)"),
|
|
254
|
+
"selectAndApplyModel should preserve provider prefixes for dynamic routing candidates",
|
|
255
|
+
);
|
|
256
|
+
assert.ok(
|
|
257
|
+
!src.includes("routingEligibleModels.map(m => m.id)"),
|
|
258
|
+
"selectAndApplyModel must not strip providers before resolving tier_models",
|
|
259
|
+
);
|
|
260
|
+
});
|
|
261
|
+
|
|
250
262
|
test("resolveModelId: anthropic wins over claude-code when session provider is not claude-code", () => {
|
|
251
263
|
const availableModels = [
|
|
252
264
|
{ id: "claude-sonnet-4-6", provider: "claude-code" },
|
|
@@ -125,9 +125,9 @@ console.log('\n=== complete-slice: schema v6 migration ===');
|
|
|
125
125
|
|
|
126
126
|
const adapter = _getAdapter()!;
|
|
127
127
|
|
|
128
|
-
// Verify schema version is current (
|
|
128
|
+
// Verify schema version is current (v20 — memory system Phase 4 relations)
|
|
129
129
|
const versionRow = adapter.prepare('SELECT MAX(version) as v FROM schema_version').get();
|
|
130
|
-
assertEq(versionRow?.['v'],
|
|
130
|
+
assertEq(versionRow?.['v'], 20, 'schema version should be 20');
|
|
131
131
|
|
|
132
132
|
// Verify slices table has full_summary_md and full_uat_md columns
|
|
133
133
|
const cols = adapter.prepare("PRAGMA table_info(slices)").all();
|
|
@@ -109,9 +109,9 @@ console.log('\n=== complete-task: schema v5 migration ===');
|
|
|
109
109
|
|
|
110
110
|
const adapter = _getAdapter()!;
|
|
111
111
|
|
|
112
|
-
// Verify schema version is current (
|
|
112
|
+
// Verify schema version is current (v20 — memory system Phase 4 relations)
|
|
113
113
|
const versionRow = adapter.prepare('SELECT MAX(version) as v FROM schema_version').get();
|
|
114
|
-
assertEq(versionRow?.['v'],
|
|
114
|
+
assertEq(versionRow?.['v'], 20, 'schema version should be 20');
|
|
115
115
|
|
|
116
116
|
// Verify all 4 new tables exist
|
|
117
117
|
const tables = adapter.prepare(
|
|
@@ -327,7 +327,7 @@ test("ADR-011 P2: listEscalationArtifacts filters to actionable by default", (t)
|
|
|
327
327
|
assert.equal(all.length, 2, "both surface with --all");
|
|
328
328
|
});
|
|
329
329
|
|
|
330
|
-
test("ADR-011 P2: schema
|
|
330
|
+
test("ADR-011 P2: schema v20 fresh DB has all escalation columns on tasks + source on decisions", (t) => {
|
|
331
331
|
const base = makeBase();
|
|
332
332
|
t.after(() => cleanup(base));
|
|
333
333
|
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
@@ -348,7 +348,7 @@ test("ADR-011 P2: schema v17 fresh DB has all escalation columns on tasks + sour
|
|
|
348
348
|
assert.ok(decCols.includes("source"), "decisions table must have source column");
|
|
349
349
|
|
|
350
350
|
const version = adapter.prepare("SELECT MAX(version) as v FROM schema_version").get();
|
|
351
|
-
assert.equal(version?.["v"],
|
|
351
|
+
assert.equal(version?.["v"], 20);
|
|
352
352
|
});
|
|
353
353
|
|
|
354
354
|
test("ADR-011 P2: findUnappliedEscalationOverride returns null when escalation_pending=1 (still pending)", (t) => {
|
|
@@ -81,7 +81,7 @@ describe('gsd-db', () => {
|
|
|
81
81
|
// Check schema_version table
|
|
82
82
|
const adapter = _getAdapter()!;
|
|
83
83
|
const version = adapter.prepare('SELECT MAX(version) as version FROM schema_version').get();
|
|
84
|
-
assert.deepStrictEqual(version?.['version'],
|
|
84
|
+
assert.deepStrictEqual(version?.['version'], 20, 'schema version should be 20');
|
|
85
85
|
|
|
86
86
|
// Check tables exist by querying them
|
|
87
87
|
const dRows = adapter.prepare('SELECT count(*) as cnt FROM decisions').get();
|
|
@@ -363,7 +363,7 @@ test('md-importer: schema v1→v2 migration', () => {
|
|
|
363
363
|
openDatabase(':memory:');
|
|
364
364
|
const adapter = _getAdapter();
|
|
365
365
|
const version = adapter?.prepare('SELECT MAX(version) as v FROM schema_version').get();
|
|
366
|
-
assert.deepStrictEqual(version?.v,
|
|
366
|
+
assert.deepStrictEqual(version?.v, 20, 'new DB should be at schema version 20');
|
|
367
367
|
|
|
368
368
|
// Artifacts table should exist
|
|
369
369
|
const tableCheck = adapter?.prepare("SELECT count(*) as c FROM sqlite_master WHERE type='table' AND name='artifacts'").get();
|