wyrm-mcp 7.2.1 → 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 (150) hide show
  1. package/LICENSE +26 -667
  2. package/NOTICE +14 -33
  3. package/dist/activation.js +1 -60
  4. package/dist/agent-daemon.js +4 -281
  5. package/dist/agent-loop.js +7 -332
  6. package/dist/analytics.js +13 -236
  7. package/dist/attribution.js +1 -49
  8. package/dist/audit.js +2 -457
  9. package/dist/auto-capture.js +3 -138
  10. package/dist/auto-orchestrator.js +1 -325
  11. package/dist/autoconfig.js +39 -840
  12. package/dist/buddy-runner.js +1 -109
  13. package/dist/buddy.js +14 -564
  14. package/dist/build-flags.js +1 -17
  15. package/dist/capabilities.js +3 -183
  16. package/dist/capture.js +1 -56
  17. package/dist/causality.js +6 -107
  18. package/dist/cli.js +20 -281
  19. package/dist/cloud/cli.js +5 -541
  20. package/dist/cloud/client.js +1 -221
  21. package/dist/cloud/crypto.js +1 -85
  22. package/dist/cloud/machine-id.js +2 -113
  23. package/dist/cloud/recovery.js +1 -60
  24. package/dist/cloud/sync-engine.js +7 -543
  25. package/dist/cloud-backup.js +5 -579
  26. package/dist/cloud-profile.js +1 -138
  27. package/dist/cloud-sync-entrypoint.js +1 -47
  28. package/dist/cloud-sync.js +2 -309
  29. package/dist/constellation.js +12 -168
  30. package/dist/context-build-budgeted.js +4 -144
  31. package/dist/context-ranking.js +1 -69
  32. package/dist/crypto.js +1 -179
  33. package/dist/daemon-write-endpoint.js +1 -290
  34. package/dist/daemon-writer.js +2 -406
  35. package/dist/database.js +43 -1110
  36. package/dist/deprecations.js +2 -162
  37. package/dist/design.js +13 -141
  38. package/dist/event-replication.js +1 -112
  39. package/dist/events-sse.js +7 -43
  40. package/dist/events.js +6 -238
  41. package/dist/failure-patterns.js +42 -659
  42. package/dist/federation.js +12 -236
  43. package/dist/goals.js +13 -101
  44. package/dist/golden.js +3 -355
  45. package/dist/handlers/agent.js +4 -165
  46. package/dist/handlers/alias-adapters.js +1 -129
  47. package/dist/handlers/aliases.js +1 -171
  48. package/dist/handlers/audit.js +1 -87
  49. package/dist/handlers/boundary.js +1 -221
  50. package/dist/handlers/capture.js +73 -1109
  51. package/dist/handlers/causality.js +7 -114
  52. package/dist/handlers/cloud.js +85 -382
  53. package/dist/handlers/companion.js +28 -459
  54. package/dist/handlers/datalake.js +7 -187
  55. package/dist/handlers/dispatch-context.js +0 -22
  56. package/dist/handlers/entity.js +25 -256
  57. package/dist/handlers/events.js +16 -335
  58. package/dist/handlers/failure.js +13 -340
  59. package/dist/handlers/goals.js +4 -296
  60. package/dist/handlers/intelligence.js +126 -674
  61. package/dist/handlers/invoicing.js +1 -70
  62. package/dist/handlers/mcpclient.js +6 -137
  63. package/dist/handlers/orchestration.js +40 -125
  64. package/dist/handlers/output-schemas.js +1 -24
  65. package/dist/handlers/presence.js +3 -99
  66. package/dist/handlers/project.js +28 -182
  67. package/dist/handlers/prompts.js +6 -157
  68. package/dist/handlers/quest.js +4 -224
  69. package/dist/handlers/recall.js +11 -218
  70. package/dist/handlers/registry.js +1 -167
  71. package/dist/handlers/resources.js +1 -288
  72. package/dist/handlers/review.js +11 -74
  73. package/dist/handlers/run.js +17 -487
  74. package/dist/handlers/search.js +15 -326
  75. package/dist/handlers/session.js +28 -615
  76. package/dist/handlers/share.js +8 -184
  77. package/dist/handlers/shims.js +1 -464
  78. package/dist/handlers/skill.js +67 -449
  79. package/dist/handlers/survivors.js +1 -120
  80. package/dist/handlers/symbols.js +8 -109
  81. package/dist/handlers/syncops.js +4 -302
  82. package/dist/handlers/types.js +1 -27
  83. package/dist/harvest.js +5 -191
  84. package/dist/hours.js +7 -156
  85. package/dist/http-auth.js +3 -321
  86. package/dist/http-fast.js +21 -1137
  87. package/dist/icons.js +1 -47
  88. package/dist/index.js +2 -924
  89. package/dist/indexer.js +4 -145
  90. package/dist/intelligence.js +31 -261
  91. package/dist/internal-dispatch.js +3 -212
  92. package/dist/keyset.js +1 -110
  93. package/dist/knowledge-graph.js +12 -176
  94. package/dist/license.js +2 -441
  95. package/dist/logger.js +2 -199
  96. package/dist/maintenance.js +2 -148
  97. package/dist/mcp-client.js +6 -262
  98. package/dist/memory-artifacts.js +30 -449
  99. package/dist/migrate-prompt.js +2 -124
  100. package/dist/migrations.js +40 -655
  101. package/dist/performance.js +1 -228
  102. package/dist/presence.js +11 -140
  103. package/dist/priority-embed.js +5 -164
  104. package/dist/providers/embedding-provider.js +1 -196
  105. package/dist/readonly-gate.js +1 -29
  106. package/dist/rehydration.js +9 -157
  107. package/dist/reindex.js +1 -88
  108. package/dist/render-target.js +21 -514
  109. package/dist/render.js +4 -280
  110. package/dist/repl-guard.js +1 -173
  111. package/dist/replication-daemon-entrypoint.js +1 -31
  112. package/dist/replication-daemon.js +2 -262
  113. package/dist/resilience.js +1 -591
  114. package/dist/reverse-bridge.js +5 -360
  115. package/dist/security.js +1 -244
  116. package/dist/session-seen.js +3 -51
  117. package/dist/setup.js +1 -260
  118. package/dist/skill-author.js +5 -168
  119. package/dist/spec-kit.js +1 -191
  120. package/dist/sqlite-busy.js +1 -154
  121. package/dist/statusline.js +11 -315
  122. package/dist/sub-agent.js +13 -262
  123. package/dist/summarizer.js +13 -139
  124. package/dist/symbols.js +7 -283
  125. package/dist/sync.js +5 -359
  126. package/dist/tasks-dispatch.js +1 -84
  127. package/dist/tasks.js +1 -282
  128. package/dist/token-budget.js +1 -143
  129. package/dist/tool-analytics.js +7 -129
  130. package/dist/tool-annotations.js +1 -365
  131. package/dist/tool-manifest-v2.json +1 -1
  132. package/dist/tool-manifest.json +1 -1
  133. package/dist/tool-profiles.js +1 -75
  134. package/dist/trace-harvest.js +6 -244
  135. package/dist/types.js +1 -30
  136. package/dist/ui-dashboard.js +41 -50
  137. package/dist/ulid.js +1 -81
  138. package/dist/validate.js +1 -129
  139. package/dist/vault.js +1 -534
  140. package/dist/vectors.js +3 -184
  141. package/dist/version-check.js +4 -136
  142. package/dist/visibility.js +19 -155
  143. package/dist/wyrm-cli.js +98 -2464
  144. package/dist/wyrm-guard.js +14 -424
  145. package/dist/wyrm-loop.js +3 -150
  146. package/dist/wyrm-manifest.json +1 -1
  147. package/dist/wyrm-statusline-daemon.js +1 -11
  148. package/dist/wyrm-statusline.js +4 -56
  149. package/dist/wyrm-ui.js +9 -77
  150. 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};