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
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Storage layer for auto-learned project memories. Follows context-store.ts patterns.
|
|
4
4
|
// All functions degrade gracefully: return empty results when DB unavailable, never throw.
|
|
5
|
-
import { isDbAvailable, _getAdapter, transaction, insertMemoryRow, rewriteMemoryId, updateMemoryContentRow, incrementMemoryHitCount, supersedeMemoryRow, markMemoryUnitProcessed, decayMemoriesBefore, supersedeLowestRankedMemories, } from './gsd-db.js';
|
|
5
|
+
import { isDbAvailable, _getAdapter, transaction, insertMemoryRow, rewriteMemoryId, updateMemoryContentRow, incrementMemoryHitCount, supersedeMemoryRow, markMemoryUnitProcessed, decayMemoriesBefore, supersedeLowestRankedMemories, deleteMemoryEmbedding, deleteMemoryRelationsFor, } from './gsd-db.js';
|
|
6
|
+
import { createMemoryRelation, isValidRelation } from './memory-relations.js';
|
|
6
7
|
// ─── Category Display Order ─────────────────────────────────────────────────
|
|
7
8
|
const CATEGORY_PRIORITY = {
|
|
8
9
|
gotcha: 0,
|
|
@@ -26,8 +27,21 @@ function rowToMemory(row) {
|
|
|
26
27
|
updated_at: row['updated_at'],
|
|
27
28
|
superseded_by: row['superseded_by'] ?? null,
|
|
28
29
|
hit_count: row['hit_count'],
|
|
30
|
+
scope: row['scope'] ?? 'project',
|
|
31
|
+
tags: parseTags(row['tags']),
|
|
29
32
|
};
|
|
30
33
|
}
|
|
34
|
+
function parseTags(raw) {
|
|
35
|
+
if (typeof raw !== 'string' || raw.length === 0)
|
|
36
|
+
return [];
|
|
37
|
+
try {
|
|
38
|
+
const parsed = JSON.parse(raw);
|
|
39
|
+
return Array.isArray(parsed) ? parsed.filter((t) => typeof t === 'string') : [];
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
31
45
|
// ─── Query Functions ────────────────────────────────────────────────────────
|
|
32
46
|
/**
|
|
33
47
|
* Get all memories where superseded_by IS NULL.
|
|
@@ -68,6 +82,245 @@ export function getActiveMemoriesRanked(limit = 30) {
|
|
|
68
82
|
return [];
|
|
69
83
|
}
|
|
70
84
|
}
|
|
85
|
+
export function queryMemoriesRanked(opts) {
|
|
86
|
+
if (!isDbAvailable())
|
|
87
|
+
return [];
|
|
88
|
+
const adapter = _getAdapter();
|
|
89
|
+
if (!adapter)
|
|
90
|
+
return [];
|
|
91
|
+
const k = clampLimit(opts.k, 10);
|
|
92
|
+
const rrfK = opts.rrfK ?? 60;
|
|
93
|
+
const activeClause = opts.include_superseded === true ? '' : 'WHERE superseded_by IS NULL';
|
|
94
|
+
const trimmedQuery = (opts.query ?? '').trim();
|
|
95
|
+
// 1) Keyword hits — try FTS5 first, fall back to LIKE when unavailable.
|
|
96
|
+
const keywordHits = trimmedQuery ? keywordSearch(adapter, trimmedQuery, activeClause, 50) : [];
|
|
97
|
+
// 2) Semantic hits — cosine over memory_embeddings. Requires opts.queryVector.
|
|
98
|
+
const semanticHits = opts.queryVector
|
|
99
|
+
? semanticSearch(adapter, opts.queryVector, activeClause, 50)
|
|
100
|
+
: [];
|
|
101
|
+
if (keywordHits.length === 0 && semanticHits.length === 0 && !trimmedQuery) {
|
|
102
|
+
// No query at all — fall back to the existing ranked-by-score listing.
|
|
103
|
+
return getActiveMemoriesRanked(k).map((memory) => ({
|
|
104
|
+
memory,
|
|
105
|
+
score: memory.confidence * (1 + memory.hit_count * 0.1),
|
|
106
|
+
keywordRank: null,
|
|
107
|
+
semanticRank: null,
|
|
108
|
+
confidenceBoost: memory.confidence * (1 + memory.hit_count * 0.1),
|
|
109
|
+
reason: 'ranked',
|
|
110
|
+
})).filter((hit) => passesFilters(hit.memory, opts));
|
|
111
|
+
}
|
|
112
|
+
// 3) Reciprocal rank fusion — each hit contributes 1/(rrfK + rank).
|
|
113
|
+
const fused = new Map();
|
|
114
|
+
for (let i = 0; i < keywordHits.length; i++) {
|
|
115
|
+
const hit = keywordHits[i];
|
|
116
|
+
const existing = fused.get(hit.id);
|
|
117
|
+
const rrf = 1 / (rrfK + i + 1);
|
|
118
|
+
if (existing) {
|
|
119
|
+
existing.kwRank = i + 1;
|
|
120
|
+
existing.score += rrf;
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
fused.set(hit.id, { memory: hit, kwRank: i + 1, semRank: null, score: rrf });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
for (let i = 0; i < semanticHits.length; i++) {
|
|
127
|
+
const hit = semanticHits[i];
|
|
128
|
+
const existing = fused.get(hit.id);
|
|
129
|
+
const rrf = 1 / (rrfK + i + 1);
|
|
130
|
+
if (existing) {
|
|
131
|
+
existing.semRank = i + 1;
|
|
132
|
+
existing.score += rrf;
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
fused.set(hit.id, { memory: hit, kwRank: null, semRank: i + 1, score: rrf });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// 4) Apply filters + confidence boost, then sort.
|
|
139
|
+
const ranked = [];
|
|
140
|
+
for (const entry of fused.values()) {
|
|
141
|
+
if (!passesFilters(entry.memory, opts))
|
|
142
|
+
continue;
|
|
143
|
+
const boost = entry.memory.confidence * (1 + entry.memory.hit_count * 0.1);
|
|
144
|
+
const reason = entry.kwRank != null && entry.semRank != null
|
|
145
|
+
? 'both'
|
|
146
|
+
: entry.kwRank != null
|
|
147
|
+
? 'keyword'
|
|
148
|
+
: 'semantic';
|
|
149
|
+
ranked.push({
|
|
150
|
+
memory: entry.memory,
|
|
151
|
+
score: entry.score * boost,
|
|
152
|
+
keywordRank: entry.kwRank,
|
|
153
|
+
semanticRank: entry.semRank,
|
|
154
|
+
confidenceBoost: boost,
|
|
155
|
+
reason,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
ranked.sort((a, b) => b.score - a.score);
|
|
159
|
+
return ranked.slice(0, k);
|
|
160
|
+
}
|
|
161
|
+
function clampLimit(value, fallback) {
|
|
162
|
+
if (typeof value !== 'number' || !Number.isFinite(value))
|
|
163
|
+
return fallback;
|
|
164
|
+
if (value < 1)
|
|
165
|
+
return 1;
|
|
166
|
+
if (value > 100)
|
|
167
|
+
return 100;
|
|
168
|
+
return Math.floor(value);
|
|
169
|
+
}
|
|
170
|
+
function passesFilters(memory, filters) {
|
|
171
|
+
if (filters.category && memory.category.toLowerCase() !== filters.category.toLowerCase())
|
|
172
|
+
return false;
|
|
173
|
+
if (filters.scope && memory.scope !== filters.scope)
|
|
174
|
+
return false;
|
|
175
|
+
if (filters.tag) {
|
|
176
|
+
const needle = filters.tag.toLowerCase();
|
|
177
|
+
if (!memory.tags.map((t) => t.toLowerCase()).includes(needle))
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
function keywordSearch(adapter, rawQuery, activeClause, limit) {
|
|
183
|
+
const ftsAvailable = isFtsAvailable(adapter);
|
|
184
|
+
if (ftsAvailable) {
|
|
185
|
+
try {
|
|
186
|
+
const matchExpr = toFtsMatchExpr(rawQuery);
|
|
187
|
+
if (!matchExpr)
|
|
188
|
+
return [];
|
|
189
|
+
const activePart = activeClause ? `AND m.${activeClause.replace(/^WHERE\s+/i, '')}` : '';
|
|
190
|
+
const rows = adapter.prepare(`SELECT m.*
|
|
191
|
+
FROM memories_fts f
|
|
192
|
+
JOIN memories m ON m.seq = f.rowid
|
|
193
|
+
WHERE memories_fts MATCH :match
|
|
194
|
+
${activePart}
|
|
195
|
+
ORDER BY bm25(memories_fts)
|
|
196
|
+
LIMIT :limit`).all({ ':match': matchExpr, ':limit': limit });
|
|
197
|
+
return rows.map(rowToMemory);
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
// fall through to LIKE
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
// LIKE fallback — scans the candidate pool.
|
|
204
|
+
const terms = rawQuery
|
|
205
|
+
.toLowerCase()
|
|
206
|
+
.split(/[^a-z0-9_]+/)
|
|
207
|
+
.filter((t) => t.length >= 2);
|
|
208
|
+
if (terms.length === 0)
|
|
209
|
+
return [];
|
|
210
|
+
const rows = adapter.prepare(`SELECT * FROM memories ${activeClause}`).all();
|
|
211
|
+
const scored = [];
|
|
212
|
+
for (const row of rows) {
|
|
213
|
+
const memory = rowToMemory(row);
|
|
214
|
+
const lower = memory.content.toLowerCase();
|
|
215
|
+
let score = 0;
|
|
216
|
+
for (const term of terms) {
|
|
217
|
+
const idx = lower.indexOf(term);
|
|
218
|
+
if (idx === -1)
|
|
219
|
+
continue;
|
|
220
|
+
score += 1 + (term.length >= 5 ? 0.5 : 0);
|
|
221
|
+
}
|
|
222
|
+
if (score > 0)
|
|
223
|
+
scored.push({ memory, score });
|
|
224
|
+
}
|
|
225
|
+
scored.sort((a, b) => b.score - a.score);
|
|
226
|
+
return scored.slice(0, limit).map((s) => s.memory);
|
|
227
|
+
}
|
|
228
|
+
function isFtsAvailable(adapter) {
|
|
229
|
+
try {
|
|
230
|
+
const row = adapter
|
|
231
|
+
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='memories_fts'")
|
|
232
|
+
.get();
|
|
233
|
+
return !!row;
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
function toFtsMatchExpr(query) {
|
|
240
|
+
// Build a tolerant AND expression: quote each bare term with a trailing *.
|
|
241
|
+
const tokens = query
|
|
242
|
+
.toLowerCase()
|
|
243
|
+
.split(/[^a-z0-9_]+/)
|
|
244
|
+
.filter((t) => t.length >= 2)
|
|
245
|
+
.slice(0, 8);
|
|
246
|
+
if (tokens.length === 0)
|
|
247
|
+
return null;
|
|
248
|
+
return tokens.map((t) => `"${t.replace(/"/g, '""')}"*`).join(' OR ');
|
|
249
|
+
}
|
|
250
|
+
function semanticSearch(adapter, queryVector, activeClause, limit) {
|
|
251
|
+
try {
|
|
252
|
+
const rows = adapter
|
|
253
|
+
.prepare(`SELECT m.*, e.vector as embedding_vector, e.dim as embedding_dim
|
|
254
|
+
FROM memories m
|
|
255
|
+
JOIN memory_embeddings e ON e.memory_id = m.id
|
|
256
|
+
${activeClause}`)
|
|
257
|
+
.all();
|
|
258
|
+
const scored = [];
|
|
259
|
+
for (const row of rows) {
|
|
260
|
+
const dim = row['embedding_dim'];
|
|
261
|
+
if (dim !== queryVector.length)
|
|
262
|
+
continue;
|
|
263
|
+
const vector = unpackVector(row['embedding_vector'], dim);
|
|
264
|
+
if (!vector)
|
|
265
|
+
continue;
|
|
266
|
+
const sim = cosine(queryVector, vector);
|
|
267
|
+
if (sim <= 0)
|
|
268
|
+
continue;
|
|
269
|
+
scored.push({ memory: rowToMemory(row), sim });
|
|
270
|
+
}
|
|
271
|
+
scored.sort((a, b) => b.sim - a.sim);
|
|
272
|
+
return scored.slice(0, limit).map((s) => s.memory);
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
return [];
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
function unpackVector(blob, dim) {
|
|
279
|
+
if (!blob)
|
|
280
|
+
return null;
|
|
281
|
+
try {
|
|
282
|
+
let view = null;
|
|
283
|
+
if (blob instanceof Float32Array)
|
|
284
|
+
return blob;
|
|
285
|
+
if (blob instanceof Uint8Array)
|
|
286
|
+
view = blob;
|
|
287
|
+
else if (blob instanceof ArrayBuffer)
|
|
288
|
+
view = new Uint8Array(blob);
|
|
289
|
+
else if (blob.buffer && blob.byteLength != null) {
|
|
290
|
+
const buf = blob;
|
|
291
|
+
view = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
292
|
+
}
|
|
293
|
+
else if (Array.isArray(blob)) {
|
|
294
|
+
return new Float32Array(blob);
|
|
295
|
+
}
|
|
296
|
+
if (!view || view.byteLength % 4 !== 0)
|
|
297
|
+
return null;
|
|
298
|
+
const aligned = new ArrayBuffer(view.byteLength);
|
|
299
|
+
new Uint8Array(aligned).set(view);
|
|
300
|
+
const f32 = new Float32Array(aligned);
|
|
301
|
+
return f32.length === dim ? f32 : null;
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
function cosine(a, b) {
|
|
308
|
+
if (a.length === 0 || a.length !== b.length)
|
|
309
|
+
return 0;
|
|
310
|
+
let dot = 0;
|
|
311
|
+
let na = 0;
|
|
312
|
+
let nb = 0;
|
|
313
|
+
for (let i = 0; i < a.length; i++) {
|
|
314
|
+
const x = a[i];
|
|
315
|
+
const y = b[i];
|
|
316
|
+
dot += x * y;
|
|
317
|
+
na += x * x;
|
|
318
|
+
nb += y * y;
|
|
319
|
+
}
|
|
320
|
+
if (na === 0 || nb === 0)
|
|
321
|
+
return 0;
|
|
322
|
+
return dot / (Math.sqrt(na) * Math.sqrt(nb));
|
|
323
|
+
}
|
|
71
324
|
/**
|
|
72
325
|
* Generate the next memory ID: MEM + zero-padded 3-digit from MAX(seq).
|
|
73
326
|
* Returns MEM001 if no memories exist.
|
|
@@ -121,6 +374,8 @@ export function createMemory(fields) {
|
|
|
121
374
|
sourceUnitId: fields.source_unit_id ?? null,
|
|
122
375
|
createdAt: now,
|
|
123
376
|
updatedAt: now,
|
|
377
|
+
scope: fields.scope ?? 'project',
|
|
378
|
+
tags: fields.tags ?? [],
|
|
124
379
|
});
|
|
125
380
|
// Derive the real ID from the assigned seq (SELECT is still fine via adapter)
|
|
126
381
|
const row = adapter.prepare('SELECT seq FROM memories WHERE id = :id').get({ ':id': placeholder });
|
|
@@ -213,29 +468,34 @@ export function markUnitProcessed(unitKey, activityFile) {
|
|
|
213
468
|
/**
|
|
214
469
|
* Reduce confidence for memories not updated within the last N processed units.
|
|
215
470
|
* "Stale" = updated_at is older than the Nth most recent processed_at.
|
|
471
|
+
* Returns the number of decayed memory IDs for observability.
|
|
216
472
|
*/
|
|
217
473
|
export function decayStaleMemories(thresholdUnits = 20) {
|
|
218
474
|
if (!isDbAvailable())
|
|
219
|
-
return;
|
|
475
|
+
return [];
|
|
220
476
|
const adapter = _getAdapter();
|
|
221
477
|
if (!adapter)
|
|
222
|
-
return;
|
|
478
|
+
return [];
|
|
223
479
|
try {
|
|
224
480
|
// Find the timestamp of the Nth most recent processed unit (read-only SELECT)
|
|
225
481
|
const row = adapter.prepare(`SELECT processed_at FROM memory_processed_units
|
|
226
482
|
ORDER BY processed_at DESC
|
|
227
483
|
LIMIT 1 OFFSET :offset`).get({ ':offset': thresholdUnits - 1 });
|
|
228
484
|
if (!row)
|
|
229
|
-
return; // not enough processed units yet
|
|
485
|
+
return []; // not enough processed units yet
|
|
230
486
|
const cutoff = row['processed_at'];
|
|
487
|
+
const affected = adapter.prepare(`SELECT id FROM memories
|
|
488
|
+
WHERE superseded_by IS NULL AND updated_at < :cutoff AND confidence > 0.1`).all({ ':cutoff': cutoff }).map((r) => r['id']);
|
|
231
489
|
decayMemoriesBefore(cutoff, new Date().toISOString());
|
|
490
|
+
return affected;
|
|
232
491
|
}
|
|
233
492
|
catch {
|
|
234
|
-
|
|
493
|
+
return [];
|
|
235
494
|
}
|
|
236
495
|
}
|
|
237
496
|
/**
|
|
238
|
-
* Supersede lowest-ranked memories when count exceeds cap.
|
|
497
|
+
* Supersede lowest-ranked memories when count exceeds cap. Cascades to the
|
|
498
|
+
* embedding and relation rows so those tables don't grow unboundedly.
|
|
239
499
|
*/
|
|
240
500
|
export function enforceMemoryCap(max = 50) {
|
|
241
501
|
if (!isDbAvailable())
|
|
@@ -249,7 +509,24 @@ export function enforceMemoryCap(max = 50) {
|
|
|
249
509
|
if (count <= max)
|
|
250
510
|
return;
|
|
251
511
|
const excess = count - max;
|
|
512
|
+
// Capture the about-to-be-superseded IDs first so we can cascade cleanup.
|
|
513
|
+
const victims = adapter.prepare(`SELECT id FROM memories
|
|
514
|
+
WHERE superseded_by IS NULL
|
|
515
|
+
ORDER BY (confidence * (1.0 + hit_count * 0.1)) ASC
|
|
516
|
+
LIMIT :limit`).all({ ':limit': excess }).map((row) => row['id']);
|
|
252
517
|
supersedeLowestRankedMemories(excess, new Date().toISOString());
|
|
518
|
+
if (victims.length === 0)
|
|
519
|
+
return;
|
|
520
|
+
for (const id of victims) {
|
|
521
|
+
try {
|
|
522
|
+
deleteMemoryEmbedding(id);
|
|
523
|
+
}
|
|
524
|
+
catch { /* non-fatal */ }
|
|
525
|
+
try {
|
|
526
|
+
deleteMemoryRelationsFor(id);
|
|
527
|
+
}
|
|
528
|
+
catch { /* non-fatal */ }
|
|
529
|
+
}
|
|
253
530
|
}
|
|
254
531
|
catch {
|
|
255
532
|
// non-fatal
|
|
@@ -274,6 +551,8 @@ export function applyMemoryActions(actions, unitType, unitId) {
|
|
|
274
551
|
confidence: action.confidence,
|
|
275
552
|
source_unit_type: unitType,
|
|
276
553
|
source_unit_id: unitId,
|
|
554
|
+
scope: action.scope,
|
|
555
|
+
tags: action.tags,
|
|
277
556
|
});
|
|
278
557
|
break;
|
|
279
558
|
case 'UPDATE':
|
|
@@ -285,6 +564,9 @@ export function applyMemoryActions(actions, unitType, unitId) {
|
|
|
285
564
|
case 'SUPERSEDE':
|
|
286
565
|
supersedeMemory(action.id, action.superseded_by);
|
|
287
566
|
break;
|
|
567
|
+
case 'LINK':
|
|
568
|
+
applyLinkAction(action);
|
|
569
|
+
break;
|
|
288
570
|
}
|
|
289
571
|
}
|
|
290
572
|
enforceMemoryCap();
|
|
@@ -294,6 +576,17 @@ export function applyMemoryActions(actions, unitType, unitId) {
|
|
|
294
576
|
// non-fatal — transaction will have rolled back
|
|
295
577
|
}
|
|
296
578
|
}
|
|
579
|
+
// ─── LINK action ────────────────────────────────────────────────────────────
|
|
580
|
+
function applyLinkAction(action) {
|
|
581
|
+
try {
|
|
582
|
+
if (!isValidRelation(action.rel))
|
|
583
|
+
return;
|
|
584
|
+
createMemoryRelation(action.from, action.to, action.rel, action.confidence);
|
|
585
|
+
}
|
|
586
|
+
catch {
|
|
587
|
+
// Link failures should never break memory extraction.
|
|
588
|
+
}
|
|
589
|
+
}
|
|
297
590
|
// ─── Prompt Formatting ──────────────────────────────────────────────────────
|
|
298
591
|
/**
|
|
299
592
|
* Format memories as categorized markdown for system prompt injection.
|
|
@@ -189,8 +189,9 @@ export function computeTaskRequirements(unitType, metadata) {
|
|
|
189
189
|
*/
|
|
190
190
|
export function scoreEligibleModels(eligibleModelIds, requirements, capabilityOverrides) {
|
|
191
191
|
const scored = eligibleModelIds.map(modelId => {
|
|
192
|
-
const
|
|
193
|
-
const
|
|
192
|
+
const bareId = bareModelId(modelId);
|
|
193
|
+
const builtin = MODEL_CAPABILITY_PROFILES[bareId];
|
|
194
|
+
const override = capabilityOverrides?.[modelId] ?? capabilityOverrides?.[bareId];
|
|
194
195
|
const profile = builtin
|
|
195
196
|
? override ? { ...builtin, ...override } : builtin
|
|
196
197
|
: { coding: 50, debugging: 50, research: 50, reasoning: 50, speed: 50, longContext: 50, instruction: 50 };
|
|
@@ -400,7 +401,7 @@ export function defaultRoutingConfig() {
|
|
|
400
401
|
// ─── Internal ────────────────────────────────────────────────────────────────
|
|
401
402
|
function getModelTier(modelId) {
|
|
402
403
|
// Strip provider prefix if present
|
|
403
|
-
const bareId =
|
|
404
|
+
const bareId = bareModelId(modelId);
|
|
404
405
|
// Check exact match first
|
|
405
406
|
if (MODEL_CAPABILITY_TIER[bareId])
|
|
406
407
|
return MODEL_CAPABILITY_TIER[bareId];
|
|
@@ -414,7 +415,7 @@ function getModelTier(modelId) {
|
|
|
414
415
|
}
|
|
415
416
|
/** Check if a model ID has a known capability tier mapping. (#2192) */
|
|
416
417
|
function isKnownModel(modelId) {
|
|
417
|
-
const bareId =
|
|
418
|
+
const bareId = bareModelId(modelId);
|
|
418
419
|
if (MODEL_CAPABILITY_TIER[bareId])
|
|
419
420
|
return true;
|
|
420
421
|
for (const knownId of Object.keys(MODEL_CAPABILITY_TIER)) {
|
|
@@ -424,7 +425,7 @@ function isKnownModel(modelId) {
|
|
|
424
425
|
return false;
|
|
425
426
|
}
|
|
426
427
|
function getModelCost(modelId) {
|
|
427
|
-
const bareId =
|
|
428
|
+
const bareId = bareModelId(modelId);
|
|
428
429
|
if (MODEL_COST_PER_1K_INPUT[bareId] !== undefined) {
|
|
429
430
|
return MODEL_COST_PER_1K_INPUT[bareId];
|
|
430
431
|
}
|
|
@@ -436,6 +437,9 @@ function getModelCost(modelId) {
|
|
|
436
437
|
// Unknown cost — assume expensive to avoid routing to unknown cheap models
|
|
437
438
|
return 999;
|
|
438
439
|
}
|
|
440
|
+
function bareModelId(modelId) {
|
|
441
|
+
return modelId.includes("/") ? modelId.split("/").pop() : modelId;
|
|
442
|
+
}
|
|
439
443
|
// ─── Tool Compatibility Filter (ADR-005 Phase 3) ───────────────────────────
|
|
440
444
|
/**
|
|
441
445
|
* Check if a tool is compatible with a provider's capabilities.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// GSD Extension — Notification History Overlay
|
|
2
2
|
// Scrollable panel showing all persisted notifications with severity filtering.
|
|
3
3
|
// Toggled with Ctrl+Alt+N (⌃⌥N on macOS), Ctrl+Shift+N fallback, or /gsd notifications.
|
|
4
|
-
import { truncateToWidth, visibleWidth, matchesKey, Key } from "@gsd/pi-tui";
|
|
4
|
+
import { truncateToWidth, visibleWidth, wrapTextWithAnsi, matchesKey, Key } from "@gsd/pi-tui";
|
|
5
5
|
import { readNotifications, markAllRead, clearNotifications, onNotificationStoreChange, } from "./notification-store.js";
|
|
6
6
|
import { formattedShortcutPair } from "./shortcut-defs.js";
|
|
7
7
|
import { padRight, joinColumns } from "../shared/mod.js";
|
|
@@ -15,29 +15,14 @@ function severityIcon(severity) {
|
|
|
15
15
|
default: return "●";
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
|
-
/**
|
|
18
|
+
/** Column-aware word wrap using pi-tui's native wrapper (handles unicode/ANSI). */
|
|
19
19
|
function wrapText(text, maxWidth) {
|
|
20
|
-
if (
|
|
20
|
+
if (maxWidth <= 0)
|
|
21
21
|
return [text];
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
if (current.length === 0) {
|
|
27
|
-
current = word;
|
|
28
|
-
}
|
|
29
|
-
else if (current.length + 1 + word.length <= maxWidth) {
|
|
30
|
-
current += " " + word;
|
|
31
|
-
}
|
|
32
|
-
else {
|
|
33
|
-
lines.push(current);
|
|
34
|
-
current = word;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
if (current.length > 0)
|
|
38
|
-
lines.push(current);
|
|
39
|
-
// If a single word exceeds maxWidth, truncate it
|
|
40
|
-
return lines.map((l) => l.length > maxWidth ? l.slice(0, maxWidth - 1) + "…" : l);
|
|
22
|
+
const lines = wrapTextWithAnsi(text, maxWidth);
|
|
23
|
+
// Safety clamp: if any line still exceeds maxWidth (e.g. unbreakable long token),
|
|
24
|
+
// truncate it with an ellipsis so it cannot bleed past the box border.
|
|
25
|
+
return lines.map((l) => visibleWidth(l) > maxWidth ? truncateToWidth(l, maxWidth, "…") : l);
|
|
41
26
|
}
|
|
42
27
|
function formatTimestamp(ts) {
|
|
43
28
|
try {
|