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.
- package/LICENSE +26 -667
- package/NOTICE +14 -33
- package/dist/activation.d.ts.map +1 -1
- package/dist/activation.js +1 -44
- package/dist/activation.js.map +1 -1
- package/dist/agent-daemon.js +4 -281
- package/dist/agent-loop.js +7 -332
- package/dist/analytics.js +13 -236
- package/dist/attribution.js +1 -49
- package/dist/audit.js +2 -457
- package/dist/auto-capture.js +3 -138
- package/dist/auto-orchestrator.js +1 -325
- package/dist/autoconfig.js +39 -840
- package/dist/buddy-runner.js +1 -109
- package/dist/buddy.js +14 -564
- package/dist/build-flags.js +1 -17
- package/dist/capabilities.js +3 -183
- package/dist/capture.js +1 -56
- package/dist/causality.js +6 -107
- package/dist/cli.js +20 -281
- package/dist/cloud/cli.js +5 -541
- package/dist/cloud/client.js +1 -221
- package/dist/cloud/crypto.js +1 -85
- package/dist/cloud/machine-id.js +2 -113
- package/dist/cloud/recovery.js +1 -60
- package/dist/cloud/sync-engine.js +7 -543
- package/dist/cloud-backup.js +5 -579
- package/dist/cloud-profile.js +1 -138
- package/dist/cloud-sync-entrypoint.js +1 -47
- package/dist/cloud-sync.js +2 -309
- package/dist/constellation.js +12 -168
- package/dist/context-build-budgeted.js +4 -144
- package/dist/context-ranking.js +1 -69
- package/dist/crypto.js +1 -179
- package/dist/daemon-write-endpoint.js +1 -290
- package/dist/daemon-writer.js +2 -406
- package/dist/database.js +43 -1110
- package/dist/deprecations.js +2 -162
- package/dist/design.js +13 -141
- package/dist/event-replication.js +1 -112
- package/dist/events-sse.js +7 -43
- package/dist/events.js +6 -238
- package/dist/failure-patterns.js +42 -659
- package/dist/federation.js +12 -236
- package/dist/goals.js +13 -101
- package/dist/golden.js +3 -355
- package/dist/handlers/agent.js +4 -165
- package/dist/handlers/alias-adapters.js +1 -129
- package/dist/handlers/aliases.js +1 -171
- package/dist/handlers/audit.js +1 -87
- package/dist/handlers/boundary.js +1 -221
- package/dist/handlers/capture.js +73 -1109
- package/dist/handlers/causality.js +7 -114
- package/dist/handlers/cloud.js +85 -382
- package/dist/handlers/companion.js +28 -459
- package/dist/handlers/datalake.js +7 -187
- package/dist/handlers/dispatch-context.js +0 -22
- package/dist/handlers/entity.js +25 -256
- package/dist/handlers/events.js +16 -335
- package/dist/handlers/failure.js +13 -340
- package/dist/handlers/goals.js +4 -296
- package/dist/handlers/intelligence.js +126 -674
- package/dist/handlers/invoicing.js +1 -70
- package/dist/handlers/mcpclient.js +6 -137
- package/dist/handlers/orchestration.js +40 -125
- package/dist/handlers/output-schemas.js +1 -24
- package/dist/handlers/presence.js +3 -99
- package/dist/handlers/project.js +28 -182
- package/dist/handlers/prompts.js +6 -157
- package/dist/handlers/quest.js +4 -224
- package/dist/handlers/recall.js +11 -218
- package/dist/handlers/registry.js +1 -167
- package/dist/handlers/resources.js +1 -288
- package/dist/handlers/review.js +11 -74
- package/dist/handlers/run.js +17 -487
- package/dist/handlers/search.js +15 -326
- package/dist/handlers/session.js +28 -615
- package/dist/handlers/share.js +8 -184
- package/dist/handlers/shims.js +1 -464
- package/dist/handlers/skill.js +67 -449
- package/dist/handlers/survivors.js +1 -120
- package/dist/handlers/symbols.js +8 -109
- package/dist/handlers/syncops.js +4 -302
- package/dist/handlers/types.js +1 -27
- package/dist/harvest.js +5 -191
- package/dist/hours.js +7 -156
- package/dist/http-auth.js +3 -321
- package/dist/http-fast.js +21 -1137
- package/dist/icons.js +1 -47
- package/dist/index.js +2 -924
- package/dist/indexer.js +4 -145
- package/dist/intelligence.js +31 -261
- package/dist/internal-dispatch.js +3 -212
- package/dist/keyset.js +1 -110
- package/dist/knowledge-graph.js +12 -176
- package/dist/license.d.ts +11 -0
- package/dist/license.d.ts.map +1 -1
- package/dist/license.js +2 -414
- package/dist/license.js.map +1 -1
- package/dist/logger.js +2 -199
- package/dist/maintenance.js +2 -148
- package/dist/mcp-client.js +6 -262
- package/dist/memory-artifacts.js +30 -449
- package/dist/migrate-prompt.js +2 -124
- package/dist/migrations.js +40 -655
- package/dist/performance.js +1 -228
- package/dist/presence.js +11 -140
- package/dist/priority-embed.js +5 -164
- package/dist/providers/embedding-provider.js +1 -196
- package/dist/readonly-gate.js +1 -29
- package/dist/rehydration.js +9 -157
- package/dist/reindex.js +1 -88
- package/dist/render-target.js +21 -514
- package/dist/render.js +4 -280
- package/dist/repl-guard.js +1 -173
- package/dist/replication-daemon-entrypoint.js +1 -31
- package/dist/replication-daemon.js +2 -262
- package/dist/resilience.js +1 -591
- package/dist/reverse-bridge.js +5 -360
- package/dist/security.js +1 -244
- package/dist/session-seen.js +3 -51
- package/dist/setup.js +1 -260
- package/dist/skill-author.js +5 -168
- package/dist/spec-kit.js +1 -191
- package/dist/sqlite-busy.js +1 -154
- package/dist/statusline.js +11 -315
- package/dist/sub-agent.js +13 -262
- package/dist/summarizer.js +13 -139
- package/dist/symbols.js +7 -283
- package/dist/sync.js +5 -359
- package/dist/tasks-dispatch.js +1 -84
- package/dist/tasks.js +1 -282
- package/dist/token-budget.js +1 -143
- package/dist/tool-analytics.js +7 -129
- package/dist/tool-annotations.js +1 -365
- package/dist/tool-manifest-v2.json +1 -1
- package/dist/tool-manifest.json +1 -1
- package/dist/tool-profiles.js +1 -75
- package/dist/trace-harvest.js +6 -244
- package/dist/types.js +1 -30
- package/dist/ui-dashboard.js +41 -50
- package/dist/ulid.js +1 -81
- package/dist/validate.js +1 -129
- package/dist/vault.js +1 -534
- package/dist/vectors.js +3 -184
- package/dist/version-check.js +4 -136
- package/dist/visibility.js +19 -155
- package/dist/wyrm-cli.js +98 -2451
- package/dist/wyrm-cli.js.map +1 -1
- package/dist/wyrm-guard.js +14 -424
- package/dist/wyrm-loop.js +3 -150
- package/dist/wyrm-manifest.json +1 -1
- package/dist/wyrm-statusline-daemon.js +1 -11
- package/dist/wyrm-statusline.js +4 -56
- package/dist/wyrm-ui.js +9 -77
- package/package.json +4 -2
package/dist/memory-artifacts.js
CHANGED
|
@@ -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(
|
|
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(
|
|
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
|
-
${
|
|
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(
|
|
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
|
-
${
|
|
33
|
+
${o}
|
|
257
34
|
ORDER BY rank, a.confidence DESC
|
|
258
35
|
LIMIT ?
|
|
259
|
-
`).all(...
|
|
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 (${
|
|
284
|
-
${
|
|
40
|
+
AND (${l})
|
|
41
|
+
${m}
|
|
285
42
|
ORDER BY confidence DESC, reuse_success_count DESC
|
|
286
43
|
LIMIT ?
|
|
287
|
-
`).all(...
|
|
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
|
-
${
|
|
47
|
+
${i}
|
|
299
48
|
ORDER BY confidence DESC, created_at DESC
|
|
300
49
|
LIMIT ?
|
|
301
|
-
`).all(...
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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 = ? ${
|
|
64
|
+
WHERE project_id = ? ${n} ${i}
|
|
400
65
|
ORDER BY confidence DESC, created_at DESC
|
|
401
66
|
LIMIT ?
|
|
402
|
-
`).all(...
|
|
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 = ? ${
|
|
69
|
+
WHERE project_id = ? ${n} ${i} ${a} ${o}
|
|
445
70
|
ORDER BY confidence DESC, id DESC
|
|
446
71
|
LIMIT ?
|
|
447
|
-
`).all(...
|
|
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
|
-
${
|
|
79
|
+
${a?"AND project_id = ?":""}
|
|
478
80
|
ORDER BY confidence ASC
|
|
479
81
|
LIMIT 500
|
|
480
|
-
`).all(...
|
|
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};
|