wyrm-mcp 7.2.0 → 7.2.2

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 (156) hide show
  1. package/LICENSE +26 -667
  2. package/NOTICE +14 -33
  3. package/dist/activation.d.ts.map +1 -1
  4. package/dist/activation.js +1 -44
  5. package/dist/activation.js.map +1 -1
  6. package/dist/agent-daemon.js +4 -281
  7. package/dist/agent-loop.js +7 -332
  8. package/dist/analytics.js +13 -236
  9. package/dist/attribution.js +1 -49
  10. package/dist/audit.js +2 -457
  11. package/dist/auto-capture.js +3 -138
  12. package/dist/auto-orchestrator.js +1 -325
  13. package/dist/autoconfig.js +39 -840
  14. package/dist/buddy-runner.js +1 -109
  15. package/dist/buddy.js +14 -564
  16. package/dist/build-flags.js +1 -17
  17. package/dist/capabilities.js +3 -183
  18. package/dist/capture.js +1 -56
  19. package/dist/causality.js +6 -107
  20. package/dist/cli.js +20 -281
  21. package/dist/cloud/cli.js +5 -541
  22. package/dist/cloud/client.js +1 -221
  23. package/dist/cloud/crypto.js +1 -85
  24. package/dist/cloud/machine-id.js +2 -113
  25. package/dist/cloud/recovery.js +1 -60
  26. package/dist/cloud/sync-engine.js +7 -543
  27. package/dist/cloud-backup.js +5 -579
  28. package/dist/cloud-profile.js +1 -138
  29. package/dist/cloud-sync-entrypoint.js +1 -47
  30. package/dist/cloud-sync.js +2 -309
  31. package/dist/constellation.js +12 -168
  32. package/dist/context-build-budgeted.js +4 -144
  33. package/dist/context-ranking.js +1 -69
  34. package/dist/crypto.js +1 -179
  35. package/dist/daemon-write-endpoint.js +1 -290
  36. package/dist/daemon-writer.js +2 -406
  37. package/dist/database.js +43 -1110
  38. package/dist/deprecations.js +2 -162
  39. package/dist/design.js +13 -141
  40. package/dist/event-replication.js +1 -112
  41. package/dist/events-sse.js +7 -43
  42. package/dist/events.js +6 -238
  43. package/dist/failure-patterns.js +42 -659
  44. package/dist/federation.js +12 -236
  45. package/dist/goals.js +13 -101
  46. package/dist/golden.js +3 -355
  47. package/dist/handlers/agent.js +4 -165
  48. package/dist/handlers/alias-adapters.js +1 -129
  49. package/dist/handlers/aliases.js +1 -171
  50. package/dist/handlers/audit.js +1 -87
  51. package/dist/handlers/boundary.js +1 -221
  52. package/dist/handlers/capture.js +73 -1109
  53. package/dist/handlers/causality.js +7 -114
  54. package/dist/handlers/cloud.js +85 -382
  55. package/dist/handlers/companion.js +28 -459
  56. package/dist/handlers/datalake.js +7 -187
  57. package/dist/handlers/dispatch-context.js +0 -22
  58. package/dist/handlers/entity.js +25 -256
  59. package/dist/handlers/events.js +16 -335
  60. package/dist/handlers/failure.js +13 -340
  61. package/dist/handlers/goals.js +4 -296
  62. package/dist/handlers/intelligence.js +126 -674
  63. package/dist/handlers/invoicing.js +1 -70
  64. package/dist/handlers/mcpclient.js +6 -137
  65. package/dist/handlers/orchestration.js +40 -125
  66. package/dist/handlers/output-schemas.js +1 -24
  67. package/dist/handlers/presence.js +3 -99
  68. package/dist/handlers/project.js +28 -182
  69. package/dist/handlers/prompts.js +6 -157
  70. package/dist/handlers/quest.js +4 -224
  71. package/dist/handlers/recall.js +11 -218
  72. package/dist/handlers/registry.js +1 -167
  73. package/dist/handlers/resources.js +1 -288
  74. package/dist/handlers/review.js +11 -74
  75. package/dist/handlers/run.js +17 -487
  76. package/dist/handlers/search.js +15 -326
  77. package/dist/handlers/session.js +28 -615
  78. package/dist/handlers/share.js +8 -184
  79. package/dist/handlers/shims.js +1 -464
  80. package/dist/handlers/skill.js +67 -449
  81. package/dist/handlers/survivors.js +1 -120
  82. package/dist/handlers/symbols.js +8 -109
  83. package/dist/handlers/syncops.js +4 -302
  84. package/dist/handlers/types.js +1 -27
  85. package/dist/harvest.js +5 -191
  86. package/dist/hours.js +7 -156
  87. package/dist/http-auth.js +3 -321
  88. package/dist/http-fast.js +21 -1137
  89. package/dist/icons.js +1 -47
  90. package/dist/index.js +2 -924
  91. package/dist/indexer.js +4 -145
  92. package/dist/intelligence.js +31 -261
  93. package/dist/internal-dispatch.js +3 -212
  94. package/dist/keyset.js +1 -110
  95. package/dist/knowledge-graph.js +12 -176
  96. package/dist/license.d.ts +11 -0
  97. package/dist/license.d.ts.map +1 -1
  98. package/dist/license.js +2 -414
  99. package/dist/license.js.map +1 -1
  100. package/dist/logger.js +2 -199
  101. package/dist/maintenance.js +2 -148
  102. package/dist/mcp-client.js +6 -262
  103. package/dist/memory-artifacts.js +30 -449
  104. package/dist/migrate-prompt.js +2 -124
  105. package/dist/migrations.js +40 -655
  106. package/dist/performance.js +1 -228
  107. package/dist/presence.js +11 -140
  108. package/dist/priority-embed.js +5 -164
  109. package/dist/providers/embedding-provider.js +1 -196
  110. package/dist/readonly-gate.js +1 -29
  111. package/dist/rehydration.js +9 -157
  112. package/dist/reindex.js +1 -88
  113. package/dist/render-target.js +21 -514
  114. package/dist/render.js +4 -280
  115. package/dist/repl-guard.js +1 -173
  116. package/dist/replication-daemon-entrypoint.js +1 -31
  117. package/dist/replication-daemon.js +2 -262
  118. package/dist/resilience.js +1 -591
  119. package/dist/reverse-bridge.js +5 -360
  120. package/dist/security.js +1 -244
  121. package/dist/session-seen.js +3 -51
  122. package/dist/setup.js +1 -260
  123. package/dist/skill-author.js +5 -168
  124. package/dist/spec-kit.js +1 -191
  125. package/dist/sqlite-busy.js +1 -154
  126. package/dist/statusline.js +11 -315
  127. package/dist/sub-agent.js +13 -262
  128. package/dist/summarizer.js +13 -139
  129. package/dist/symbols.js +7 -283
  130. package/dist/sync.js +5 -359
  131. package/dist/tasks-dispatch.js +1 -84
  132. package/dist/tasks.js +1 -282
  133. package/dist/token-budget.js +1 -143
  134. package/dist/tool-analytics.js +7 -129
  135. package/dist/tool-annotations.js +1 -365
  136. package/dist/tool-manifest-v2.json +1 -1
  137. package/dist/tool-manifest.json +1 -1
  138. package/dist/tool-profiles.js +1 -75
  139. package/dist/trace-harvest.js +6 -244
  140. package/dist/types.js +1 -30
  141. package/dist/ui-dashboard.js +41 -50
  142. package/dist/ulid.js +1 -81
  143. package/dist/validate.js +1 -129
  144. package/dist/vault.js +1 -534
  145. package/dist/vectors.js +3 -184
  146. package/dist/version-check.js +4 -136
  147. package/dist/visibility.js +19 -155
  148. package/dist/wyrm-cli.js +98 -2451
  149. package/dist/wyrm-cli.js.map +1 -1
  150. package/dist/wyrm-guard.js +14 -424
  151. package/dist/wyrm-loop.js +3 -150
  152. package/dist/wyrm-manifest.json +1 -1
  153. package/dist/wyrm-statusline-daemon.js +1 -11
  154. package/dist/wyrm-statusline.js +4 -56
  155. package/dist/wyrm-ui.js +9 -77
  156. package/package.json +4 -2
@@ -1,67 +1,9 @@
1
- /**
2
- * Wyrm Memory Artifacts — Distilled knowledge for AI intelligence amplification.
3
- *
4
- * Stores problem-solution records, lessons, patterns, and anti-patterns that
5
- * help AI models recall what worked in the past and apply it to new tasks.
6
- *
7
- * @copyright 2026 Ghost Protocol (Pvt) Ltd.
8
- * @license AGPL-3.0-or-later — dual-licensed; commercial terms: ghosts.lk@proton.me. See LICENSE.
9
- */
10
- import { emitEvent } from './events.js';
11
- import { sanitizeFtsQuery, buildFtsMatchQuery } from './security.js';
12
- import { getActor } from './handlers/boundary.js';
13
- import { buildPage, DEFAULT_PAGE_SIZE } from './keyset.js';
14
- /** Maximum artifacts returned from a recall query (before reranking) */
15
- const MAX_CANDIDATES = 40;
16
- /** Maximum artifacts included in a context brief */
17
- const MAX_BRIEF_ITEMS = 10;
18
- /** Minimum confidence to include in auto-generated briefs */
19
- const MIN_BRIEF_CONFIDENCE = 0.3;
20
- export class MemoryArtifacts {
21
- db;
22
- /** Optional dense-vector index — when set, `recallHybrid` fuses FTS + vectors (RRF). */
23
- vectorStore;
24
- setVectorStore(vs) { this.vectorStore = vs; }
25
- constructor(db) {
26
- this.db = db;
27
- }
28
- /** Best-effort dense-vector index of an ACTIVE artifact. Fire-and-forget: embedding
29
- * is async + may be unavailable (Ollama down) — it must never block or break add(). */
30
- indexArtifact(a) {
31
- if (!this.vectorStore)
32
- return;
33
- const text = `${a.problem}${a.validated_fix ? ' ' + a.validated_fix : ''}`.slice(0, 2000);
34
- void this.vectorStore.addVector(text, 'artifact', a.id, a.project_id).catch(() => { });
35
- }
36
- // ==================== CRUD ====================
37
- add(projectId, opts) {
38
- const tags = opts.tags?.length ? opts.tags.join(',') : null;
39
- // v7 F2 (T009): stamp the writing actor (NULL outside a fleet context).
40
- // `created_by` keeps its 6.x display semantics untouched.
41
- const ambient = getActor();
42
- const result = this.db.prepare(`
1
+ import{emitEvent as y}from"./events.js";import{sanitizeFtsQuery as v,buildFtsMatchQuery as k}from"./security.js";import{getActor as w}from"./handlers/boundary.js";import{buildPage as b,DEFAULT_PAGE_SIZE as T}from"./keyset.js";const D=40,M=10,I=.3;class U{db;vectorStore;setVectorStore(t){this.vectorStore=t}constructor(t){this.db=t}indexArtifact(t){if(!this.vectorStore)return;const e=`${t.problem}${t.validated_fix?" "+t.validated_fix:""}`.slice(0,2e3);this.vectorStore.addVector(e,"artifact",t.id,t.project_id).catch(()=>{})}add(t,e){const n=e.tags?.length?e.tags.join(","):null,i=w(),a=this.db.prepare(`
43
2
  INSERT INTO memory_artifacts
44
3
  (project_id, kind, problem, constraints, validated_fix, why_it_worked,
45
4
  outcome, source_session_id, tags, confidence, needs_review, created_by, agent_id, run_id)
46
5
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
47
- `).run(projectId, opts.kind, opts.problem.trim(), opts.constraints?.trim() ?? null, opts.validatedFix?.trim() ?? null, opts.whyItWorked?.trim() ?? null, opts.outcome ?? 'neutral', opts.sourceSessionId ?? null, tags, opts.confidence ?? 1.0, opts.needsReview ?? 0, opts.createdBy ?? 'local', ambient.agent_id, ambient.run_id);
48
- const artifact = this.get(result.lastInsertRowid);
49
- // Live Memory: emit ONLY for active memories. Review-queue candidates
50
- // (harvest dumps 1000s with needsReview=1) must not flood the event log.
51
- if (!opts.needsReview) {
52
- emitEvent(this.db, {
53
- projectId, kind: 'capture', refTable: 'memory_artifacts', refId: artifact.id,
54
- isShared: !!artifact.is_shared,
55
- });
56
- this.indexArtifact(artifact); // best-effort dense index for hybrid recall
57
- }
58
- return artifact;
59
- }
60
- get(id) {
61
- return this.db.prepare('SELECT * FROM memory_artifacts WHERE id = ?').get(id) ?? null;
62
- }
63
- update(id, updates) {
64
- this.db.prepare(`
6
+ `).run(t,e.kind,e.problem.trim(),e.constraints?.trim()??null,e.validatedFix?.trim()??null,e.whyItWorked?.trim()??null,e.outcome??"neutral",e.sourceSessionId??null,n,e.confidence??1,e.needsReview??0,e.createdBy??"local",i.agent_id,i.run_id),r=this.get(a.lastInsertRowid);return e.needsReview||(y(this.db,{projectId:t,kind:"capture",refTable:"memory_artifacts",refId:r.id,isShared:!!r.is_shared}),this.indexArtifact(r)),r}get(t){return this.db.prepare("SELECT * FROM memory_artifacts WHERE id = ?").get(t)??null}update(t,e){return this.db.prepare(`
65
7
  UPDATE memory_artifacts SET
66
8
  confidence = COALESCE(?, confidence),
67
9
  validated_fix = COALESCE(?, validated_fix),
@@ -70,182 +12,17 @@ export class MemoryArtifacts {
70
12
  needs_review = COALESCE(?, needs_review),
71
13
  updated_at = datetime('now')
72
14
  WHERE id = ?
73
- `).run(updates.confidence ?? null, updates.validated_fix ?? null, updates.why_it_worked ?? null, updates.last_validated_at ?? null, updates.needs_review ?? null, id);
74
- return this.get(id);
75
- }
76
- /** Mark an artifact as superseded by a newer/better one. */
77
- markSuperseded(oldId, newId) {
78
- this.db.prepare('UPDATE memory_artifacts SET supersedes_id = ?, updated_at = datetime(\'now\') WHERE id = ?').run(newId, oldId);
79
- }
80
- /** Record that an artifact was used and whether it helped. */
81
- recordFeedback(id, success) {
82
- const col = success ? 'reuse_success_count' : 'reuse_failure_count';
83
- this.db.prepare(`
15
+ `).run(e.confidence??null,e.validated_fix??null,e.why_it_worked??null,e.last_validated_at??null,e.needs_review??null,t),this.get(t)}markSuperseded(t,e){this.db.prepare("UPDATE memory_artifacts SET supersedes_id = ?, updated_at = datetime('now') WHERE id = ?").run(e,t)}recordFeedback(t,e){const n=e?"reuse_success_count":"reuse_failure_count";this.db.prepare(`
84
16
  UPDATE memory_artifacts SET
85
17
  reuse_count = reuse_count + 1,
86
- ${col} = ${col} + 1,
18
+ ${n} = ${n} + 1,
87
19
  confidence = CASE
88
20
  WHEN ? THEN MIN(1.0, confidence + 0.05)
89
21
  ELSE MAX(0.0, confidence - 0.1)
90
22
  END,
91
23
  updated_at = datetime('now')
92
24
  WHERE id = ?
93
- `).run(success ? 1 : 0, id);
94
- }
95
- // ==================== RETRIEVAL ====================
96
- /**
97
- * 2-stage retrieval: FTS candidates → sort by weighted relevance.
98
- * Excludes superseded artifacts and those below minConfidence.
99
- */
100
- recall(projectId, query, opts = {}) {
101
- const limit = opts.limit ?? 10;
102
- const minConf = opts.minConfidence ?? 0.0;
103
- // Stage 1: FTS candidates
104
- const ftsCandidates = this.searchByFts(projectId, query, MAX_CANDIDATES, opts.kind, minConf);
105
- // Stage 2: Tag candidates (exact tag matches not covered by FTS)
106
- const tagCandidates = this.searchByTags(projectId, query, MAX_CANDIDATES, opts.kind, minConf);
107
- // Merge and dedupe by id
108
- const seen = new Set();
109
- const merged = [];
110
- // ftsCandidates arrive in bm25 order (best first) — keep the rank so the score
111
- // below preserves it instead of flattening every FTS hit to one value.
112
- ftsCandidates.forEach((a, i) => {
113
- seen.add(a.id);
114
- merged.push({ artifact: a, inFts: true, inTag: false, ftsRank: i });
115
- });
116
- for (const a of tagCandidates) {
117
- if (!seen.has(a.id)) {
118
- seen.add(a.id);
119
- merged.push({ artifact: a, inFts: false, inTag: true });
120
- }
121
- else {
122
- const existing = merged.find(m => m.artifact.id === a.id);
123
- if (existing)
124
- existing.inTag = true;
125
- }
126
- }
127
- // Score and rerank — also enforce minConf as a safety net
128
- const results = merged
129
- .filter(({ artifact }) => artifact.confidence >= minConf)
130
- .map(({ artifact, inFts, inTag, ftsRank }) => {
131
- // bm25 rank is the PRIMARY signal: earlier FTS candidates (lower rank) score
132
- // higher, so the query's relevance ordering survives the merge/re-sort.
133
- let relevance;
134
- if (inFts) {
135
- const rankScore = 1 - (ftsRank ?? 0) / (ftsCandidates.length + 1); // ~1.0 best → ~0 worst
136
- relevance = (inTag ? 0.7 : 0.55) + 0.45 * rankScore; // 0.55..1.0 for FTS hits
137
- }
138
- else {
139
- relevance = 0.4; // tag-only match
140
- }
141
- // Boost by confidence
142
- relevance *= artifact.confidence;
143
- // Boost positive outcomes slightly
144
- if (artifact.outcome === 'positive')
145
- relevance *= 1.1;
146
- // Freshness: decay slowly over 180 days
147
- const daysSinceValidated = (Date.now() - new Date(artifact.last_validated_at).getTime()) / (1000 * 60 * 60 * 24);
148
- const freshnessScore = Math.max(0.5, 1.0 - daysSinceValidated / 180);
149
- relevance *= freshnessScore;
150
- return {
151
- artifact,
152
- relevance_score: Math.min(1.0, relevance),
153
- match_type: (inFts && inTag ? 'both' : inFts ? 'fts' : 'tag'),
154
- };
155
- });
156
- return results.sort((a, b) => b.relevance_score - a.relevance_score).slice(0, limit);
157
- }
158
- /**
159
- * Hybrid recall: FTS (bm25) ⊕ dense vectors (cosine), fused by convex score-blend
160
- * (default, α=0.7) or RRF (WYRM_RERANK_FUSION=rrf). Gracefully degrades to lexical
161
- * `recall()` when no vector store is wired or the embedding provider is unavailable.
162
- * Async (embedding the query is async). Measured on real LoCoMo (nomic-embed-text,
163
- * local): FTS recall@10 59.9% → hybrid+RRF 72.2% → hybrid+convex recall@5 62.1% / @10 72.7%.
164
- */
165
- async recallHybrid(projectId, query, opts = {}) {
166
- const limit = opts.limit ?? 10;
167
- const minConf = opts.minConfidence ?? 0.0;
168
- if (!this.vectorStore)
169
- return this.recall(projectId, query, opts); // no vectors → lexical
170
- // Candidate depth 50 (LoCoMo-grid-tuned): convex fusion tolerates depth where
171
- // RRF dilutes — raising RRF's depth actually regressed recall@10 in the sweep.
172
- const CAND = Math.max(50, limit * 5);
173
- const ftsCands = this.searchByFts(projectId, query, CAND, opts.kind, minConf);
174
- let vecScored = [];
175
- try {
176
- const hits = await this.vectorStore.search(query, CAND, projectId, ['artifact']);
177
- vecScored = hits.map((h) => ({ id: h.content_id, s: h.similarity })); // keep cosine for convex fusion
178
- }
179
- catch { /* provider down → fall through to FTS-only */ }
180
- if (vecScored.length === 0)
181
- return this.recall(projectId, query, opts); // nothing indexed yet
182
- // Fusion. DEFAULT = convex score-blend: α·(min-max-normalized cosine) +
183
- // (1-α)·(rank-normalized FTS). Won the LoCoMo grid (recall@5 60.3%→62.1%,
184
- // recall@10 →72.7% at α=0.7, cand=50) vs RRF. WYRM_RERANK_FUSION=rrf falls back.
185
- const RRF_K = 60;
186
- const rawAlpha = Number(process.env.WYRM_RERANK_ALPHA); // dense-vector weight
187
- const ALPHA = Number.isFinite(rawAlpha) ? Math.max(0, Math.min(1, rawAlpha)) : 0.7; // NaN / out-of-range / "0.7x" → 0.7
188
- const score = new Map();
189
- if (process.env.WYRM_RERANK_FUSION === 'rrf') {
190
- ftsCands.forEach((a, r) => score.set(a.id, (score.get(a.id) ?? 0) + 1 / (RRF_K + r + 1)));
191
- vecScored.forEach((x, r) => score.set(x.id, (score.get(x.id) ?? 0) + 1 / (RRF_K + r + 1)));
192
- }
193
- else {
194
- const fn = ftsCands.length + 1; // FTS has no bm25 score exposed here → rank-normalize
195
- ftsCands.forEach((a, r) => score.set(a.id, (score.get(a.id) ?? 0) + (1 - ALPHA) * (1 - r / fn)));
196
- const ss = vecScored.map((x) => x.s), lo = Math.min(...ss), hi = Math.max(...ss), span = hi - lo;
197
- vecScored.forEach((x) => score.set(x.id, (score.get(x.id) ?? 0) + ALPHA * (span > 1e-9 ? (x.s - lo) / span : 1)));
198
- }
199
- const ftsIds = new Set(ftsCands.map((a) => a.id));
200
- const vecIds = new Set(vecScored.map((x) => x.id));
201
- const byId = new Map(ftsCands.map((a) => [a.id, a]));
202
- const ranked = [...score.entries()].sort((a, b) => b[1] - a[1]);
203
- const top = ranked[0]?.[1] ?? 1;
204
- const out = [];
205
- for (const [id, s] of ranked) {
206
- if (out.length >= limit)
207
- break;
208
- const art = byId.get(id) ?? this.get(id) ?? undefined;
209
- if (!art)
210
- continue;
211
- // Vector-only hits bypass the FTS WHERE-clause, so enforce the same filters here.
212
- if (art.confidence < minConf || art.needs_review === 1 || art.supersedes_id != null)
213
- continue;
214
- if (opts.kind && art.kind !== opts.kind)
215
- continue;
216
- const match_type = ftsIds.has(id) && vecIds.has(id) ? 'hybrid' : vecIds.has(id) ? 'vector' : 'fts';
217
- out.push({ artifact: art, relevance_score: Math.min(1, s / top), match_type });
218
- }
219
- return out;
220
- }
221
- searchByFts(projectId, query, limit, kind, minConf = 0.0) {
222
- // Build a SAFE FTS5 MATCH query (OR-joined quoted terms). The old code passed
223
- // the raw natural-language query — punctuation like "?" made FTS5 throw, so EVERY
224
- // recall silently fell back to listRecent() (recency, not relevance). Use the same
225
- // helpers the rest of the search surface uses; ORDER BY rank gives bm25 ranking.
226
- // sanitizeFtsQuery THROWS on an empty-after-sanitization query (e.g. an
227
- // operator-/punctuation-only input). That must degrade to recency, never
228
- // throw out of recall()/context_build() — guard it.
229
- let match = '';
230
- try {
231
- const sanitized = sanitizeFtsQuery(query);
232
- match = sanitized ? buildFtsMatchQuery(sanitized) : '';
233
- }
234
- catch {
235
- return this.listRecent(projectId, limit, kind);
236
- }
237
- if (!match)
238
- return this.listRecent(projectId, limit, kind);
239
- const kindClause = kind ? 'AND a.kind = ?' : '';
240
- const params = [match, projectId, minConf];
241
- if (kind)
242
- params.push(kind);
243
- params.push(limit);
244
- try {
245
- // NB: reference the FTS table by its FULL NAME in MATCH, not an alias —
246
- // `<alias> MATCH ?` throws "no such column" in SQLite FTS5, which is what sent
247
- // every recall into the listRecent() fallback (recency instead of relevance).
248
- return this.db.prepare(`
25
+ `).run(e?1:0,t)}recall(t,e,n={}){const i=n.limit??10,a=n.minConfidence??0,r=this.searchByFts(t,e,D,n.kind,a),o=this.searchByTags(t,e,D,n.kind,a),l=new Set,_=[];r.forEach((d,c)=>{l.add(d.id),_.push({artifact:d,inFts:!0,inTag:!1,ftsRank:c})});for(const d of o)if(!l.has(d.id))l.add(d.id),_.push({artifact:d,inFts:!1,inTag:!0});else{const c=_.find(p=>p.artifact.id===d.id);c&&(c.inTag=!0)}return _.filter(({artifact:d})=>d.confidence>=a).map(({artifact:d,inFts:c,inTag:p,ftsRank:E})=>{let f;if(c){const R=1-(E??0)/(r.length+1);f=(p?.7:.55)+.45*R}else f=.4;f*=d.confidence,d.outcome==="positive"&&(f*=1.1);const h=(Date.now()-new Date(d.last_validated_at).getTime())/(1e3*60*60*24),A=Math.max(.5,1-h/180);return f*=A,{artifact:d,relevance_score:Math.min(1,f),match_type:c&&p?"both":c?"fts":"tag"}}).sort((d,c)=>c.relevance_score-d.relevance_score).slice(0,i)}async recallHybrid(t,e,n={}){const i=n.limit??10,a=n.minConfidence??0;if(!this.vectorStore)return this.recall(t,e,n);const r=Math.max(50,i*5),o=this.searchByFts(t,e,r,n.kind,a);let l=[];try{l=(await this.vectorStore.search(e,r,t,["artifact"])).map(u=>({id:u.content_id,s:u.similarity}))}catch{}if(l.length===0)return this.recall(t,e,n);const _=60,m=Number(process.env.WYRM_RERANK_ALPHA),d=Number.isFinite(m)?Math.max(0,Math.min(1,m)):.7,c=new Map;if(process.env.WYRM_RERANK_FUSION==="rrf")o.forEach((s,u)=>c.set(s.id,(c.get(s.id)??0)+1/(_+u+1))),l.forEach((s,u)=>c.set(s.id,(c.get(s.id)??0)+1/(_+u+1)));else{const s=o.length+1;o.forEach((C,L)=>c.set(C.id,(c.get(C.id)??0)+(1-d)*(1-L/s)));const u=l.map(C=>C.s),S=Math.min(...u),N=Math.max(...u),g=N-S;l.forEach(C=>c.set(C.id,(c.get(C.id)??0)+d*(g>1e-9?(C.s-S)/g:1)))}const p=new Set(o.map(s=>s.id)),E=new Set(l.map(s=>s.id)),f=new Map(o.map(s=>[s.id,s])),h=[...c.entries()].sort((s,u)=>u[1]-s[1]),A=h[0]?.[1]??1,R=[];for(const[s,u]of h){if(R.length>=i)break;const S=f.get(s)??this.get(s)??void 0;if(!S||S.confidence<a||S.needs_review===1||S.supersedes_id!=null||n.kind&&S.kind!==n.kind)continue;const N=p.has(s)&&E.has(s)?"hybrid":E.has(s)?"vector":"fts";R.push({artifact:S,relevance_score:Math.min(1,u/A),match_type:N})}return R}searchByFts(t,e,n,i,a=0){let r="";try{const _=v(e);r=_?k(_):""}catch{return this.listRecent(t,n,i)}if(!r)return this.listRecent(t,n,i);const o=i?"AND a.kind = ?":"",l=[r,t,a];i&&l.push(i),l.push(n);try{return this.db.prepare(`
249
26
  SELECT a.* FROM memory_artifacts a
250
27
  JOIN memory_artifacts_fts ON a.id = memory_artifacts_fts.rowid
251
28
  WHERE memory_artifacts_fts MATCH ?
@@ -253,249 +30,53 @@ export class MemoryArtifacts {
253
30
  AND a.confidence >= ?
254
31
  AND a.supersedes_id IS NULL
255
32
  AND a.needs_review = 0
256
- ${kindClause}
33
+ ${o}
257
34
  ORDER BY rank, a.confidence DESC
258
35
  LIMIT ?
259
- `).all(...params);
260
- }
261
- catch {
262
- return this.listRecent(projectId, limit, kind);
263
- }
264
- }
265
- searchByTags(projectId, query, limit, kind, minConf = 0.0) {
266
- // Match any word in the query against tags
267
- const words = query.toLowerCase().split(/\s+/).filter(w => w.length > 2);
268
- if (!words.length)
269
- return [];
270
- // Escape LIKE wildcards in user input to prevent injection
271
- const escapeLike = (s) => s.replace(/[%_\\]/g, '\\$&');
272
- const likeClause = words.map(() => "LOWER(tags) LIKE ? ESCAPE '\\'").join(' OR ');
273
- const likeParams = words.map(w => `%${escapeLike(w)}%`);
274
- const kindClause = kind ? 'AND kind = ?' : '';
275
- const params = [projectId, minConf, ...likeParams];
276
- if (kind)
277
- params.push(kind);
278
- params.push(limit);
279
- return this.db.prepare(`
36
+ `).all(...l)}catch{return this.listRecent(t,n,i)}}searchByTags(t,e,n,i,a=0){const r=e.toLowerCase().split(/\s+/).filter(c=>c.length>2);if(!r.length)return[];const o=c=>c.replace(/[%_\\]/g,"\\$&"),l=r.map(()=>"LOWER(tags) LIKE ? ESCAPE '\\'").join(" OR "),_=r.map(c=>`%${o(c)}%`),m=i?"AND kind = ?":"",d=[t,a,..._];return i&&d.push(i),d.push(n),this.db.prepare(`
280
37
  SELECT * FROM memory_artifacts
281
38
  WHERE project_id = ? AND confidence >= ? AND supersedes_id IS NULL
282
39
  AND needs_review = 0
283
- AND (${likeClause})
284
- ${kindClause}
40
+ AND (${l})
41
+ ${m}
285
42
  ORDER BY confidence DESC, reuse_success_count DESC
286
43
  LIMIT ?
287
- `).all(...params);
288
- }
289
- listRecent(projectId, limit = 20, kind) {
290
- const kindClause = kind ? 'AND kind = ?' : '';
291
- const params = [projectId];
292
- if (kind)
293
- params.push(kind);
294
- params.push(limit);
295
- return this.db.prepare(`
44
+ `).all(...d)}listRecent(t,e=20,n){const i=n?"AND kind = ?":"",a=[t];return n&&a.push(n),a.push(e),this.db.prepare(`
296
45
  SELECT * FROM memory_artifacts
297
46
  WHERE project_id = ? AND supersedes_id IS NULL AND needs_review = 0
298
- ${kindClause}
47
+ ${i}
299
48
  ORDER BY confidence DESC, created_at DESC
300
49
  LIMIT ?
301
- `).all(...params);
302
- }
303
- // ==================== CONTEXT BRIEF ====================
304
- /**
305
- * Assemble an optimized memory brief for injection into an AI model's context.
306
- * Excludes credentials/sensitive content. Groups by kind. Deduplicates.
307
- * Returns sections with headings + items, plus raw text for direct injection.
308
- */
309
- buildContextBrief(projectId, task, opts = {}) {
310
- const kinds = opts.kinds ?? ['pattern', 'heuristic', 'reasoning_trace', 'lesson', 'anti_pattern'];
311
- const maxItems = opts.maxItems ?? MAX_BRIEF_ITEMS;
312
- const minConf = opts.minConfidence ?? MIN_BRIEF_CONFIDENCE;
313
- // Recall relevant artifacts
314
- const recalled = this.recall(projectId, task, { limit: maxItems * 2, minConfidence: minConf });
315
- // Group by kind, pick top items per kind
316
- const byKind = new Map();
317
- for (const r of recalled) {
318
- if (!kinds.includes(r.artifact.kind))
319
- continue;
320
- const existing = byKind.get(r.artifact.kind) ?? [];
321
- existing.push(r);
322
- byKind.set(r.artifact.kind, existing);
323
- }
324
- const KIND_LABELS = {
325
- pattern: '✅ Proven Patterns',
326
- heuristic: '💡 Heuristics',
327
- reasoning_trace: '🧠 Past Reasoning',
328
- lesson: '📚 Lessons Learned',
329
- anti_pattern: '⚠️ Anti-Patterns to Avoid',
330
- };
331
- // Prioritize: patterns > heuristics > reasoning_traces > lessons > anti_patterns
332
- const kindOrder = ['pattern', 'heuristic', 'reasoning_trace', 'lesson', 'anti_pattern'];
333
- const sections = [];
334
- const sourceIds = [];
335
- let totalItems = 0;
336
- for (const kind of kindOrder) {
337
- if (!kinds.includes(kind))
338
- continue;
339
- const items = byKind.get(kind) ?? [];
340
- if (!items.length)
341
- continue;
342
- const sectionItems = [];
343
- for (const r of items) {
344
- if (totalItems >= maxItems)
345
- break;
346
- const a = r.artifact;
347
- let line = `**Problem:** ${a.problem}`;
348
- if (a.constraints)
349
- line += `\n _Constraints:_ ${a.constraints}`;
350
- if (a.validated_fix)
351
- line += `\n _Solution:_ ${a.validated_fix}`;
352
- if (a.why_it_worked)
353
- line += `\n _Why it worked:_ ${a.why_it_worked}`;
354
- if (a.outcome === 'negative')
355
- line += `\n _Note: This approach failed — avoid it_`;
356
- sectionItems.push(line);
357
- sourceIds.push(a.id);
358
- totalItems++;
359
- }
360
- if (sectionItems.length) {
361
- sections.push({ heading: KIND_LABELS[kind], items: sectionItems, source: kind });
362
- }
363
- }
364
- // Build formatted text
365
- let text = '';
366
- if (sections.length > 0) {
367
- text += '---\n## 󱅝 Memory Brief\n_Relevant past knowledge from Wyrm:_\n\n';
368
- for (const section of sections) {
369
- text += `### ${section.heading}\n`;
370
- for (const item of section.items) {
371
- text += `- ${item}\n`;
372
- }
373
- text += '\n';
374
- }
375
- text += '---\n';
376
- }
377
- return { sections, text, sourceIds };
378
- }
379
- // ==================== STATS ====================
380
- getStats(projectId) {
381
- const total = this.db.prepare('SELECT COUNT(*) as n FROM memory_artifacts WHERE project_id = ? AND supersedes_id IS NULL').get(projectId).n;
382
- const supersededCount = this.db.prepare('SELECT COUNT(*) as n FROM memory_artifacts WHERE project_id = ? AND supersedes_id IS NOT NULL').get(projectId).n;
383
- const avgConfidence = this.db.prepare('SELECT AVG(confidence) as v FROM memory_artifacts WHERE project_id = ? AND supersedes_id IS NULL').get(projectId).v ?? 0;
384
- const kindRows = this.db.prepare('SELECT kind, COUNT(*) as cnt FROM memory_artifacts WHERE project_id = ? AND supersedes_id IS NULL GROUP BY kind').all(projectId);
385
- const byKind = {};
386
- for (const r of kindRows)
387
- byKind[r.kind] = r.cnt;
388
- return { total, byKind, avgConfidence: Math.round(avgConfidence * 100) / 100, supersededCount };
389
- }
390
- listAll(projectId, opts = {}) {
391
- const kindClause = opts.kind ? 'AND kind = ?' : '';
392
- const supersededClause = opts.includeSuperseded ? '' : 'AND supersedes_id IS NULL';
393
- const params = [projectId];
394
- if (opts.kind)
395
- params.push(opts.kind);
396
- params.push(opts.limit ?? 50);
397
- return this.db.prepare(`
50
+ `).all(...a)}buildContextBrief(t,e,n={}){const i=n.kinds??["pattern","heuristic","reasoning_trace","lesson","anti_pattern"],a=n.maxItems??M,r=n.minConfidence??I,o=this.recall(t,e,{limit:a*2,minConfidence:r}),l=new Map;for(const f of o){if(!i.includes(f.artifact.kind))continue;const h=l.get(f.artifact.kind)??[];h.push(f),l.set(f.artifact.kind,h)}const _={pattern:"\u2705 Proven Patterns",heuristic:"\u{1F4A1} Heuristics",reasoning_trace:"\u{1F9E0} Past Reasoning",lesson:"\u{1F4DA} Lessons Learned",anti_pattern:"\u26A0\uFE0F Anti-Patterns to Avoid"},m=["pattern","heuristic","reasoning_trace","lesson","anti_pattern"],d=[],c=[];let p=0;for(const f of m){if(!i.includes(f))continue;const h=l.get(f)??[];if(!h.length)continue;const A=[];for(const R of h){if(p>=a)break;const s=R.artifact;let u=`**Problem:** ${s.problem}`;s.constraints&&(u+=`
51
+ _Constraints:_ ${s.constraints}`),s.validated_fix&&(u+=`
52
+ _Solution:_ ${s.validated_fix}`),s.why_it_worked&&(u+=`
53
+ _Why it worked:_ ${s.why_it_worked}`),s.outcome==="negative"&&(u+=`
54
+ _Note: This approach failed \u2014 avoid it_`),A.push(u),c.push(s.id),p++}A.length&&d.push({heading:_[f],items:A,source:f})}let E="";if(d.length>0){E+=`---
55
+ ## \u{F115D} Memory Brief
56
+ _Relevant past knowledge from Wyrm:_
57
+
58
+ `;for(const f of d){E+=`### ${f.heading}
59
+ `;for(const h of f.items)E+=`- ${h}
60
+ `;E+=`
61
+ `}E+=`---
62
+ `}return{sections:d,text:E,sourceIds:c}}getStats(t){const e=this.db.prepare("SELECT COUNT(*) as n FROM memory_artifacts WHERE project_id = ? AND supersedes_id IS NULL").get(t).n,n=this.db.prepare("SELECT COUNT(*) as n FROM memory_artifacts WHERE project_id = ? AND supersedes_id IS NOT NULL").get(t).n,i=this.db.prepare("SELECT AVG(confidence) as v FROM memory_artifacts WHERE project_id = ? AND supersedes_id IS NULL").get(t).v??0,a=this.db.prepare("SELECT kind, COUNT(*) as cnt FROM memory_artifacts WHERE project_id = ? AND supersedes_id IS NULL GROUP BY kind").all(t),r={};for(const o of a)r[o.kind]=o.cnt;return{total:e,byKind:r,avgConfidence:Math.round(i*100)/100,supersededCount:n}}listAll(t,e={}){const n=e.kind?"AND kind = ?":"",i=e.includeSuperseded?"":"AND supersedes_id IS NULL",a=[t];return e.kind&&a.push(e.kind),a.push(e.limit??50),this.db.prepare(`
398
63
  SELECT * FROM memory_artifacts
399
- WHERE project_id = ? ${kindClause} ${supersededClause}
64
+ WHERE project_id = ? ${n} ${i}
400
65
  ORDER BY confidence DESC, created_at DESC
401
66
  LIMIT ?
402
- `).all(...params);
403
- }
404
- /**
405
- * Keyset-paginated sibling of `listAll` (v7 F4 T035). Orders by
406
- * `(confidence DESC, id DESC)` — a TOTAL order (id is the unique tiebreak),
407
- * so a page boundary that falls inside a run of tied-confidence artifacts
408
- * drops nothing and repeats nothing (the bare-sortKey bug the cloud-sync
409
- * keyset cursor taught us). The composite cursor `(confidence, id)` is opaque
410
- * on the wire; a malformed cursor degrades to "first page" upstream.
411
- *
412
- * Over-fetches by one (`pageSize + 1`) so the caller can tell "is there more"
413
- * without a COUNT. `pageSize` is already clamped to `[1, MAX_PAGE_SIZE]` by
414
- * the keyset helper before it reaches here.
415
- */
416
- listPage(projectId, opts = { pageSize: DEFAULT_PAGE_SIZE }) {
417
- const kindClause = opts.kind ? 'AND kind = ?' : '';
418
- const supersededClause = opts.includeSuperseded ? '' : 'AND supersedes_id IS NULL';
419
- // The wyrm://project/{id}/memory resource is advertised as a project's
420
- // "validated memory artifacts" (handlers/resources.ts), and every other
421
- // validated read path here enforces needs_review = 0 (searchByTags,
422
- // listRecent, the FTS recall). Match that contract so the resource never
423
- // surfaces unreviewed/quarantined rows (security pass #2, finding #2).
424
- const reviewedClause = 'AND needs_review = 0';
425
- const params = [projectId];
426
- if (opts.kind)
427
- params.push(opts.kind);
428
- // Keyset predicate for a (confidence DESC, id DESC) total order: resume
429
- // strictly AFTER the anchor row. `confidence` is bound as a number, the id
430
- // as the unique tiebreak. Confidence is stored as a REAL so the cursor's
431
- // string sortKey is parsed back to a float here (the encode round-trips it).
432
- let keysetClause = '';
433
- if (opts.after) {
434
- const afterConf = Number(opts.after.sortKey);
435
- if (Number.isFinite(afterConf)) {
436
- keysetClause = 'AND (confidence < ? OR (confidence = ? AND id < ?))';
437
- params.push(afterConf, afterConf, opts.after.id);
438
- }
439
- }
440
- const fetch = opts.pageSize + 1;
441
- params.push(fetch);
442
- const rows = this.db.prepare(`
67
+ `).all(...a)}listPage(t,e={pageSize:T}){const n=e.kind?"AND kind = ?":"",i=e.includeSuperseded?"":"AND supersedes_id IS NULL",a="AND needs_review = 0",r=[t];e.kind&&r.push(e.kind);let o="";if(e.after){const m=Number(e.after.sortKey);Number.isFinite(m)&&(o="AND (confidence < ? OR (confidence = ? AND id < ?))",r.push(m,m,e.after.id))}const l=e.pageSize+1;r.push(l);const _=this.db.prepare(`
443
68
  SELECT * FROM memory_artifacts
444
- WHERE project_id = ? ${kindClause} ${supersededClause} ${reviewedClause} ${keysetClause}
69
+ WHERE project_id = ? ${n} ${i} ${a} ${o}
445
70
  ORDER BY confidence DESC, id DESC
446
71
  LIMIT ?
447
- `).all(...params);
448
- return buildPage(rows, opts.pageSize, (r) => ({ sortKey: String(r.confidence), id: r.id }));
449
- }
450
- /**
451
- * Find (and optionally delete) stale, low-confidence artifacts safe to prune.
452
- *
453
- * A candidate must be: strictly below `minConfidence`, untouched for at least
454
- * `olderThanDays` (or never accessed), already reviewed (needs_review = 0),
455
- * and not superseding another artifact. When `projectId` is supplied the
456
- * sweep is scoped to that project via an exact equality bind, so a prune can
457
- * never reach into another project's rows; omit it for a global prune. The
458
- * candidate set is capped at 500 per call. When `dryRun` is false the matched
459
- * rows are deleted and the delete re-asserts needs_review = 0 as a final
460
- * guard. Returns the candidates plus the number actually deleted.
461
- */
462
- pruneStale(opts = {}) {
463
- const minConfidence = opts.minConfidence ?? 0.3;
464
- const olderThanDays = opts.olderThanDays ?? 90;
465
- const dryRun = opts.dryRun ?? true;
466
- const scoped = opts.projectId != null;
467
- const params = [minConfidence, `-${olderThanDays} days`];
468
- if (scoped)
469
- params.push(opts.projectId);
470
- const candidates = this.db.prepare(`
72
+ `).all(...r);return b(_,e.pageSize,m=>({sortKey:String(m.confidence),id:m.id}))}pruneStale(t={}){const e=t.minConfidence??.3,n=t.olderThanDays??90,i=t.dryRun??!0,a=t.projectId!=null,r=[e,`-${n} days`];a&&r.push(t.projectId);const o=this.db.prepare(`
471
73
  SELECT id, kind, problem, confidence, last_accessed_at
472
74
  FROM memory_artifacts
473
75
  WHERE confidence < ?
474
76
  AND (last_accessed_at IS NULL OR last_accessed_at < datetime('now', ?))
475
77
  AND needs_review = 0
476
78
  AND supersedes_id IS NULL
477
- ${scoped ? 'AND project_id = ?' : ''}
79
+ ${a?"AND project_id = ?":""}
478
80
  ORDER BY confidence ASC
479
81
  LIMIT 500
480
- `).all(...params);
481
- if (dryRun || candidates.length === 0) {
482
- return { candidates, deleted: 0, dryRun };
483
- }
484
- return { candidates, deleted: this.deleteArtifacts(candidates.map(c => c.id)), dryRun };
485
- }
486
- /**
487
- * Delete artifacts by id, refusing any still flagged needs_review (a final
488
- * guard so a stale id list can never remove a row that has since been queued
489
- * for review). Returns the number of rows actually deleted. The CLI uses this
490
- * to delete exactly the candidate set it showed the operator, so the confirmed
491
- * set and the deleted set are always identical (no re-select in between).
492
- */
493
- deleteArtifacts(ids) {
494
- if (ids.length === 0)
495
- return 0;
496
- const placeholders = ids.map(() => '?').join(',');
497
- const result = this.db.prepare(`DELETE FROM memory_artifacts WHERE id IN (${placeholders}) AND needs_review = 0`).run(...ids);
498
- return result.changes;
499
- }
500
- }
501
- //# sourceMappingURL=memory-artifacts.js.map
82
+ `).all(...r);return i||o.length===0?{candidates:o,deleted:0,dryRun:i}:{candidates:o,deleted:this.deleteArtifacts(o.map(l=>l.id)),dryRun:i}}deleteArtifacts(t){if(t.length===0)return 0;const e=t.map(()=>"?").join(",");return this.db.prepare(`DELETE FROM memory_artifacts WHERE id IN (${e}) AND needs_review = 0`).run(...t).changes}}export{U as MemoryArtifacts};