gsd-pi 2.75.0-dev.2203010a0 → 2.75.0-dev.96d4bb599

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/dist/onboarding.d.ts +5 -1
  2. package/dist/onboarding.js +5 -3
  3. package/dist/resources/extensions/gsd/auto-model-selection.js +1 -1
  4. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +23 -19
  5. package/dist/resources/extensions/gsd/bootstrap/memory-tools.js +128 -0
  6. package/dist/resources/extensions/gsd/bootstrap/register-extension.js +2 -0
  7. package/dist/resources/extensions/gsd/bootstrap/system-context.js +17 -4
  8. package/dist/resources/extensions/gsd/commands/handlers/onboarding.js +52 -68
  9. package/dist/resources/extensions/gsd/commands/handlers/ops.js +5 -0
  10. package/dist/resources/extensions/gsd/commands-memory.js +462 -0
  11. package/dist/resources/extensions/gsd/gsd-db.js +237 -4
  12. package/dist/resources/extensions/gsd/memory-embeddings.js +219 -0
  13. package/dist/resources/extensions/gsd/memory-extractor.js +78 -27
  14. package/dist/resources/extensions/gsd/memory-ingest.js +218 -0
  15. package/dist/resources/extensions/gsd/memory-relations.js +189 -0
  16. package/dist/resources/extensions/gsd/memory-source-store.js +113 -0
  17. package/dist/resources/extensions/gsd/memory-store.js +299 -6
  18. package/dist/resources/extensions/gsd/model-router.js +9 -5
  19. package/dist/resources/extensions/gsd/notification-overlay.js +7 -22
  20. package/dist/resources/extensions/gsd/tools/memory-tools.js +306 -0
  21. package/dist/resources/extensions/gsd/tools/skip-slice.js +78 -0
  22. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  23. package/dist/web/standalone/.next/BUILD_ID +1 -1
  24. package/dist/web/standalone/.next/app-path-routes-manifest.json +14 -14
  25. package/dist/web/standalone/.next/build-manifest.json +2 -2
  26. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  27. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  28. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  36. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/index.html +1 -1
  44. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app-paths-manifest.json +14 -14
  51. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  52. package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
  53. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  54. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  55. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  56. package/package.json +1 -1
  57. package/packages/mcp-server/dist/server.d.ts.map +1 -1
  58. package/packages/mcp-server/dist/server.js +12 -10
  59. package/packages/mcp-server/dist/server.js.map +1 -1
  60. package/packages/mcp-server/dist/session-manager.d.ts.map +1 -1
  61. package/packages/mcp-server/dist/session-manager.js +8 -1
  62. package/packages/mcp-server/dist/session-manager.js.map +1 -1
  63. package/packages/mcp-server/dist/workflow-tools.d.ts +1 -0
  64. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  65. package/packages/mcp-server/dist/workflow-tools.js +113 -14
  66. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  67. package/packages/mcp-server/src/mcp-server.test.ts +40 -4
  68. package/packages/mcp-server/src/server.ts +12 -10
  69. package/packages/mcp-server/src/session-manager.ts +10 -3
  70. package/packages/mcp-server/src/workflow-tools.test.ts +91 -1
  71. package/packages/mcp-server/src/workflow-tools.ts +128 -18
  72. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  73. package/src/resources/extensions/gsd/auto-model-selection.ts +1 -1
  74. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +24 -20
  75. package/src/resources/extensions/gsd/bootstrap/memory-tools.ts +158 -0
  76. package/src/resources/extensions/gsd/bootstrap/register-extension.ts +2 -0
  77. package/src/resources/extensions/gsd/bootstrap/system-context.ts +20 -4
  78. package/src/resources/extensions/gsd/commands/handlers/onboarding.ts +65 -98
  79. package/src/resources/extensions/gsd/commands/handlers/ops.ts +5 -0
  80. package/src/resources/extensions/gsd/commands-memory.ts +551 -0
  81. package/src/resources/extensions/gsd/gsd-db.ts +273 -4
  82. package/src/resources/extensions/gsd/memory-embeddings.ts +235 -0
  83. package/src/resources/extensions/gsd/memory-extractor.ts +100 -34
  84. package/src/resources/extensions/gsd/memory-ingest.ts +286 -0
  85. package/src/resources/extensions/gsd/memory-relations.ts +240 -0
  86. package/src/resources/extensions/gsd/memory-source-store.ts +138 -0
  87. package/src/resources/extensions/gsd/memory-store.ts +351 -7
  88. package/src/resources/extensions/gsd/model-router.ts +10 -5
  89. package/src/resources/extensions/gsd/notification-overlay.ts +9 -19
  90. package/src/resources/extensions/gsd/tests/auto-model-selection.test.ts +12 -0
  91. package/src/resources/extensions/gsd/tests/complete-slice.test.ts +2 -2
  92. package/src/resources/extensions/gsd/tests/complete-task.test.ts +2 -2
  93. package/src/resources/extensions/gsd/tests/escalation.test.ts +2 -2
  94. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +1 -1
  95. package/src/resources/extensions/gsd/tests/md-importer.test.ts +1 -1
  96. package/src/resources/extensions/gsd/tests/memory-embeddings.test.ts +213 -0
  97. package/src/resources/extensions/gsd/tests/memory-ingest.test.ts +153 -0
  98. package/src/resources/extensions/gsd/tests/memory-maintenance.test.ts +107 -0
  99. package/src/resources/extensions/gsd/tests/memory-relations.test.ts +175 -0
  100. package/src/resources/extensions/gsd/tests/memory-store.test.ts +2 -2
  101. package/src/resources/extensions/gsd/tests/memory-tools.test.ts +295 -0
  102. package/src/resources/extensions/gsd/tests/model-router.test.ts +50 -0
  103. package/src/resources/extensions/gsd/tests/notification-overlay.test.ts +56 -37
  104. package/src/resources/extensions/gsd/tests/skip-slice-cascades-tasks.test.ts +125 -0
  105. package/src/resources/extensions/gsd/tools/memory-tools.ts +380 -0
  106. package/src/resources/extensions/gsd/tools/skip-slice.ts +133 -0
  107. package/src/resources/extensions/gsd/workflow-logger.ts +3 -1
  108. /package/dist/web/standalone/.next/static/{8FZqxNe9FxQDmsbRzR8tA → o61X3klsB6C0UE0X1x3PA}/_buildManifest.js +0 -0
  109. /package/dist/web/standalone/.next/static/{8FZqxNe9FxQDmsbRzR8tA → o61X3klsB6C0UE0X1x3PA}/_ssgManifest.js +0 -0
@@ -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
- // non-fatal
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 builtin = MODEL_CAPABILITY_PROFILES[modelId];
193
- const override = capabilityOverrides?.[modelId];
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 = modelId.includes("/") ? modelId.split("/").pop() : modelId;
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 = modelId.includes("/") ? modelId.split("/").pop() : modelId;
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 = modelId.includes("/") ? modelId.split("/").pop() : modelId;
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
- /** Word-wrap plain text to fit within maxWidth columns. */
18
+ /** Column-aware word wrap using pi-tui's native wrapper (handles unicode/ANSI). */
19
19
  function wrapText(text, maxWidth) {
20
- if (text.length <= maxWidth)
20
+ if (maxWidth <= 0)
21
21
  return [text];
22
- const words = text.split(/\s+/);
23
- const lines = [];
24
- let current = "";
25
- for (const word of words) {
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 {