wyrm-mcp 5.3.0 → 5.4.0

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.
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Combined-score computation for context-build candidates (spec 014).
3
+ *
4
+ * Combines four signals into a single score in [0, 1]:
5
+ *
6
+ * confidence × wC + recency × wR + relevance × wU + usefulness × wU
7
+ *
8
+ * Weights load from WYRM_RANK_WEIGHTS env (JSON) with sane defaults.
9
+ *
10
+ * @copyright 2026 Ghost Protocol (Pvt) Ltd. All Rights Reserved.
11
+ * @license Proprietary
12
+ */
13
+ export interface RankWeights {
14
+ confidence: number;
15
+ recency: number;
16
+ relevance: number;
17
+ usefulness: number;
18
+ }
19
+ export declare const DEFAULT_WEIGHTS: Readonly<RankWeights>;
20
+ /**
21
+ * Load weights from WYRM_RANK_WEIGHTS env var. Falls back to defaults
22
+ * on any parse/validation error. Logs the active weights at startup.
23
+ */
24
+ export declare function loadWeightsFromEnv(env?: NodeJS.ProcessEnv): RankWeights;
25
+ export interface ScoreableItem {
26
+ /** [0, 1] — confidence the item is correct/true */
27
+ confidence?: number;
28
+ /** ISO timestamp — used for recency decay */
29
+ updatedAt?: string;
30
+ /** [0, 1] — pre-computed relevance (FTS5 rank / vector score) */
31
+ relevance?: number;
32
+ /** [0, 1] — usefulness inferred from wyrm_feedback signals */
33
+ usefulness?: number;
34
+ }
35
+ /**
36
+ * Compute the combined score in [0, 1].
37
+ *
38
+ * Recency uses exponential decay over 30 days from updatedAt to now.
39
+ * Missing signals default to neutral (0.5 confidence, 0.5 relevance, 0.5
40
+ * usefulness; recency to 0 if no updatedAt).
41
+ */
42
+ export declare function score(item: ScoreableItem, weights: RankWeights, now?: Date): number;
43
+ //# sourceMappingURL=context-ranking.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context-ranking.d.ts","sourceRoot":"","sources":["../src/context-ranking.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,MAAM,WAAW,WAAW;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,eAAO,MAAM,eAAe,EAAE,QAAQ,CAAC,WAAW,CAKjD,CAAC;AAEF;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,WAAW,CAgBpF;AAOD,MAAM,WAAW,aAAa;IAC5B,mDAAmD;IACnD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,6CAA6C;IAC7C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iEAAiE;IACjE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8DAA8D;IAC9D,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;GAMG;AACH,wBAAgB,KAAK,CACnB,IAAI,EAAE,aAAa,EACnB,OAAO,EAAE,WAAW,EACpB,GAAG,GAAE,IAAiB,GACrB,MAAM,CAgBR"}
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Combined-score computation for context-build candidates (spec 014).
3
+ *
4
+ * Combines four signals into a single score in [0, 1]:
5
+ *
6
+ * confidence × wC + recency × wR + relevance × wU + usefulness × wU
7
+ *
8
+ * Weights load from WYRM_RANK_WEIGHTS env (JSON) with sane defaults.
9
+ *
10
+ * @copyright 2026 Ghost Protocol (Pvt) Ltd. All Rights Reserved.
11
+ * @license Proprietary
12
+ */
13
+ import { logger } from './logger.js';
14
+ export const DEFAULT_WEIGHTS = {
15
+ confidence: 0.4,
16
+ recency: 0.2,
17
+ relevance: 0.3,
18
+ usefulness: 0.1,
19
+ };
20
+ /**
21
+ * Load weights from WYRM_RANK_WEIGHTS env var. Falls back to defaults
22
+ * on any parse/validation error. Logs the active weights at startup.
23
+ */
24
+ export function loadWeightsFromEnv(env = process.env) {
25
+ const raw = env.WYRM_RANK_WEIGHTS;
26
+ if (!raw)
27
+ return { ...DEFAULT_WEIGHTS };
28
+ try {
29
+ const parsed = JSON.parse(raw);
30
+ const w = {
31
+ confidence: clamp01(parsed.confidence ?? DEFAULT_WEIGHTS.confidence),
32
+ recency: clamp01(parsed.recency ?? DEFAULT_WEIGHTS.recency),
33
+ relevance: clamp01(parsed.relevance ?? DEFAULT_WEIGHTS.relevance),
34
+ usefulness: clamp01(parsed.usefulness ?? DEFAULT_WEIGHTS.usefulness),
35
+ };
36
+ return w;
37
+ }
38
+ catch (e) {
39
+ logger.warn(`Invalid WYRM_RANK_WEIGHTS, using defaults: ${e.message}`);
40
+ return { ...DEFAULT_WEIGHTS };
41
+ }
42
+ }
43
+ function clamp01(x) {
44
+ if (!Number.isFinite(x))
45
+ return 0;
46
+ return Math.max(0, Math.min(1, x));
47
+ }
48
+ /**
49
+ * Compute the combined score in [0, 1].
50
+ *
51
+ * Recency uses exponential decay over 30 days from updatedAt to now.
52
+ * Missing signals default to neutral (0.5 confidence, 0.5 relevance, 0.5
53
+ * usefulness; recency to 0 if no updatedAt).
54
+ */
55
+ export function score(item, weights, now = new Date()) {
56
+ const c = clamp01(item.confidence ?? 0.5);
57
+ const u = clamp01(item.usefulness ?? 0.5);
58
+ const r = clamp01(item.relevance ?? 0.5);
59
+ const ageDays = item.updatedAt
60
+ ? Math.max(0, (now.getTime() - new Date(item.updatedAt).getTime()) / (24 * 60 * 60 * 1000))
61
+ : Infinity;
62
+ const recency = Number.isFinite(ageDays) ? Math.exp(-ageDays / 30) : 0;
63
+ const total = c * weights.confidence +
64
+ recency * weights.recency +
65
+ r * weights.relevance +
66
+ u * weights.usefulness;
67
+ return clamp01(total);
68
+ }
69
+ //# sourceMappingURL=context-ranking.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context-ranking.js","sourceRoot":"","sources":["../src/context-ranking.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AASrC,MAAM,CAAC,MAAM,eAAe,GAA0B;IACpD,UAAU,EAAE,GAAG;IACf,OAAO,EAAE,GAAG;IACZ,SAAS,EAAE,GAAG;IACd,UAAU,EAAE,GAAG;CAChB,CAAC;AAEF;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAAC,MAAyB,OAAO,CAAC,GAAG;IACrE,MAAM,GAAG,GAAG,GAAG,CAAC,iBAAiB,CAAC;IAClC,IAAI,CAAC,GAAG;QAAE,OAAO,EAAE,GAAG,eAAe,EAAE,CAAC;IACxC,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAyB,CAAC;QACvD,MAAM,CAAC,GAAgB;YACrB,UAAU,EAAE,OAAO,CAAC,MAAM,CAAC,UAAU,IAAI,eAAe,CAAC,UAAU,CAAC;YACpE,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,OAAO,IAAI,eAAe,CAAC,OAAO,CAAC;YAC3D,SAAS,EAAE,OAAO,CAAC,MAAM,CAAC,SAAS,IAAI,eAAe,CAAC,SAAS,CAAC;YACjE,UAAU,EAAE,OAAO,CAAC,MAAM,CAAC,UAAU,IAAI,eAAe,CAAC,UAAU,CAAC;SACrE,CAAC;QACF,OAAO,CAAC,CAAC;IACX,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,MAAM,CAAC,IAAI,CAAC,8CAA+C,CAAW,CAAC,OAAO,EAAE,CAAC,CAAC;QAClF,OAAO,EAAE,GAAG,eAAe,EAAE,CAAC;IAChC,CAAC;AACH,CAAC;AAED,SAAS,OAAO,CAAC,CAAS;IACxB,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC;QAAE,OAAO,CAAC,CAAC;IAClC,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;AACrC,CAAC;AAaD;;;;;;GAMG;AACH,MAAM,UAAU,KAAK,CACnB,IAAmB,EACnB,OAAoB,EACpB,MAAY,IAAI,IAAI,EAAE;IAEtB,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,UAAU,IAAI,GAAG,CAAC,CAAC;IAC1C,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,UAAU,IAAI,GAAG,CAAC,CAAC;IAC1C,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,SAAS,IAAI,GAAG,CAAC,CAAC;IACzC,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS;QAC5B,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,CAAC,GAAG,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;QAC3F,CAAC,CAAC,QAAQ,CAAC;IACb,MAAM,OAAO,GAAG,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,OAAO,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAEvE,MAAM,KAAK,GACT,CAAC,GAAG,OAAO,CAAC,UAAU;QACtB,OAAO,GAAG,OAAO,CAAC,OAAO;QACzB,CAAC,GAAG,OAAO,CAAC,SAAS;QACrB,CAAC,GAAG,OAAO,CAAC,UAAU,CAAC;IAEzB,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC;AACxB,CAAC"}
package/dist/index.js CHANGED
@@ -61,6 +61,9 @@ import { AgentDaemon } from "./agent-daemon.js";
61
61
  import { getUpdateStatus, emitStartupVersionBanner } from "./version-check.js";
62
62
  import { getCapabilities, renderCapabilityBriefing } from "./capabilities.js";
63
63
  import { spawn as cpSpawn } from "child_process";
64
+ import { makeEstimator, applyBudget, resolveBudget } from "./token-budget.js";
65
+ import { loadWeightsFromEnv, score as rankScore } from "./context-ranking.js";
66
+ import { SessionSeen } from "./session-seen.js";
64
67
  import { fileURLToPath as _versionFileURLToPath } from "url";
65
68
  import { dirname as _versionDirname, join as _versionJoin } from "path";
66
69
  import { readFileSync as _versionReadFile, existsSync as _versionExists } from "fs";
@@ -68,6 +71,149 @@ import { readFileSync as _versionReadFile, existsSync as _versionExists } from "
68
71
  // The ListToolsRequestSchema handler is the single source of truth at runtime —
69
72
  // this constant is just for the self-description and the postinstall pitch.
70
73
  const WYRM_TOOL_COUNT = 114;
74
+ // Spec 014 — token budgeting subsystem. Lazy-init on first use.
75
+ const tokenEstimator = makeEstimator();
76
+ const rankWeights = loadWeightsFromEnv();
77
+ let _sessionSeen = null;
78
+ function sessionSeen() {
79
+ if (!_sessionSeen)
80
+ _sessionSeen = new SessionSeen(db.getDatabase());
81
+ return _sessionSeen;
82
+ }
83
+ /**
84
+ * Spec 014 budgeted wyrm_context_build path.
85
+ *
86
+ * Renders a stable preamble (ground truths) + dynamic body (memory
87
+ * artifacts, scaffold). Items below the token-budget cutoff elide to
88
+ * one-line stubs the AI can recall via wyrm_recall.
89
+ */
90
+ function runBudgetedContextBuild(opts) {
91
+ // Resolve effective budget per spec 014 D1 precedence: call > env > fallback.
92
+ const budget = resolveBudget({
93
+ callArg: opts.maxTokens,
94
+ envValue: process.env.WYRM_CONTEXT_TOKEN_BUDGET,
95
+ clientName: null, // clientInfo wiring is a follow-up; env override is the lever today
96
+ });
97
+ const seen = opts.sessionId ? sessionSeen().getSeen(opts.sessionId) : new Set();
98
+ // --- Stable preamble (ground truths) ---
99
+ const truthsText = groundTruths.formatForContext(opts.project.id);
100
+ const truthsTokens = tokenEstimator.count(truthsText ?? '');
101
+ // Strict budget mode (D3) elides truths if even the preamble overflows;
102
+ // default behavior is to warn-and-overflow per spec 014.
103
+ let preambleText = truthsText ?? '';
104
+ let preambleEmitted = truthsTokens;
105
+ if (truthsTokens > budget * 0.5 && !opts.strictBudget) {
106
+ process.stderr.write(`[wyrm_context_build] ground truths consume ${truthsTokens} of ${budget}-token budget — body will be heavily elided. Pass strict_budget:true to elide truths too.\n`);
107
+ }
108
+ if (opts.strictBudget && truthsTokens > budget * 0.5) {
109
+ // Strict mode: keep truths brief
110
+ preambleText = (truthsText ?? '').slice(0, budget * 2); // 2 chars/token rough cap on chars
111
+ preambleEmitted = tokenEstimator.count(preambleText);
112
+ }
113
+ // --- Build candidate body items: best scaffold + memory artifacts ---
114
+ const items = [];
115
+ // Scaffold (treated as a single candidate)
116
+ const scaffoldMatch = scaffoldLib.findBest(opts.task, opts.project.id);
117
+ if (scaffoldMatch) {
118
+ const sText = scaffoldLib.formatForContext(scaffoldMatch);
119
+ const sId = scaffoldMatch.scaffold.id;
120
+ items.push({
121
+ item: { kind: 'scaffold', id: sId, title: scaffoldMatch.scaffold.problem_type, body: sText },
122
+ key: `scaffold:${sId}`,
123
+ inlineCost: tokenEstimator.count(sText),
124
+ stubCost: 20,
125
+ // Scaffold is high-relevance by definition; assign generous score.
126
+ score: rankScore({ confidence: 0.9, relevance: 0.9, updatedAt: new Date().toISOString() }, rankWeights),
127
+ });
128
+ }
129
+ // Memory artifacts via recall (which returns id-level results with relevance scores)
130
+ const recalled = memory.recall(opts.project.id, opts.task, {
131
+ limit: 20,
132
+ minConfidence: opts.minConfidence ?? 0.2,
133
+ });
134
+ for (const r of recalled) {
135
+ const a = r.artifact;
136
+ // Skip kind filter mismatches if caller specified one
137
+ if (opts.kinds && opts.kinds.length > 0 && !opts.kinds.includes(a.kind))
138
+ continue;
139
+ const body = [
140
+ `**${a.kind.toUpperCase()}** · ${a.problem}`,
141
+ a.constraints ? `Constraints: ${a.constraints}` : '',
142
+ a.validated_fix ? `Fix: ${a.validated_fix}` : '',
143
+ a.why_it_worked ? `Why: ${a.why_it_worked}` : '',
144
+ ].filter(Boolean).join('\n');
145
+ items.push({
146
+ item: { kind: 'memory', id: a.id, title: a.problem.slice(0, 60), body },
147
+ key: `memory:${a.id}`,
148
+ inlineCost: tokenEstimator.count(body),
149
+ stubCost: 25,
150
+ score: rankScore({
151
+ confidence: a.confidence,
152
+ relevance: r.relevance_score,
153
+ updatedAt: a.updated_at,
154
+ }, rankWeights),
155
+ });
156
+ }
157
+ // --- Apply budget ---
158
+ const result = applyBudget(items, {
159
+ budget,
160
+ alreadySeen: seen,
161
+ reserved: opts.strictBudget ? 0 : preambleEmitted,
162
+ });
163
+ // --- Mark inlined items as seen for this session ---
164
+ if (opts.sessionId) {
165
+ sessionSeen().markBulk(opts.sessionId, result.inline.map((it) => ({ id: it.id, kind: it.kind, mode: 'inline' })));
166
+ sessionSeen().markBulk(opts.sessionId, result.elided.map((e) => ({ id: e.item.id, kind: e.item.kind, mode: 'stub' })));
167
+ }
168
+ // --- Render output ---
169
+ const lines = [];
170
+ lines.push(`🐉 **Context Brief** — "${opts.task}"`);
171
+ lines.push('');
172
+ lines.push('## Ground truths (stable preamble)');
173
+ lines.push('');
174
+ lines.push(preambleText || '_no ground truths set for this project yet_');
175
+ lines.push('');
176
+ lines.push('## Task-relevant memory');
177
+ lines.push('');
178
+ for (const it of result.inline) {
179
+ lines.push(`### [${it.kind}:${it.id}] ${it.title}`);
180
+ lines.push(it.body);
181
+ lines.push('');
182
+ }
183
+ if (result.elided.length > 0) {
184
+ lines.push('## Elided to stubs');
185
+ lines.push('');
186
+ lines.push('_Below the budget cutoff or already seen this session. Recall with `wyrm_recall(id)`:_');
187
+ lines.push('');
188
+ for (const e of result.elided) {
189
+ const tag = e.reason === 'seen' ? 'shown earlier in session' : `~${e.stubCost} tokens`;
190
+ lines.push(`- [${e.item.kind}:${e.item.id}] ${e.item.title} · ${tag}`);
191
+ }
192
+ lines.push('');
193
+ }
194
+ lines.push('---');
195
+ lines.push(`_Budget: ${result.tokensInline}/${budget} tokens inline · ${result.elided.length} stub${result.elided.length === 1 ? '' : 's'} · estimator: ${tokenEstimator.source}_`);
196
+ const text = lines.join('\n');
197
+ // Telemetry — fire-and-forget via the existing tool_call_log surface.
198
+ try {
199
+ toolAnalytics.log({
200
+ tool_name: 'wyrm_context_build',
201
+ project_id: opts.project.id,
202
+ args: {
203
+ budget,
204
+ items_total: items.length,
205
+ items_inline: result.inline.length,
206
+ items_elided: result.elided.length,
207
+ tokens_inline: result.tokensInline,
208
+ tokens_stubs: result.tokensStubs,
209
+ },
210
+ success: true,
211
+ latency_ms: 0,
212
+ });
213
+ }
214
+ catch { /* never fail context_build on telemetry */ }
215
+ return { content: [{ type: 'text', text }] };
216
+ }
71
217
  // Resolve our own package.json once so wyrm_check_update knows what version we are.
72
218
  const WYRM_PACKAGE_VERSION = (() => {
73
219
  try {
@@ -1119,19 +1265,22 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
1119
1265
  },
1120
1266
  {
1121
1267
  name: "wyrm_context_build",
1122
- description: "Assemble an optimized memory brief for the current task — a formatted block of relevant patterns, lessons, reasoning traces, and anti-patterns from past sessions. Inject this at the start of complex tasks to give AI models the right context without bloating the whole conversation.",
1268
+ description: "Assemble an optimized memory brief for the current task — a formatted block of relevant patterns, lessons, reasoning traces, and anti-patterns from past sessions. Inject this at the start of complex tasks to give AI models the right context without bloating the whole conversation. Pass max_tokens for budget-aware delivery (low-score items elide to recallable stubs); omit max_tokens for legacy unbounded behaviour.",
1123
1269
  inputSchema: {
1124
1270
  type: "object",
1125
1271
  properties: {
1126
1272
  projectPath: { type: "string", description: "Project to build context from" },
1127
1273
  task: { type: "string", description: "Describe the current task in detail — the more specific, the better the recall" },
1128
- maxItems: { type: "number", description: "Max knowledge items to include (default: 10, max: 20)" },
1274
+ maxItems: { type: "number", description: "Max knowledge items to include (default: 10, max: 20). Ignored when max_tokens is set." },
1129
1275
  kinds: {
1130
1276
  type: "array",
1131
1277
  items: { type: "string", enum: ["reasoning_trace", "lesson", "pattern", "anti_pattern", "heuristic"] },
1132
1278
  description: "Limit to specific artifact kinds (default: all)",
1133
1279
  },
1134
1280
  minConfidence: { type: "number", description: "Minimum confidence threshold (default: 0.3)" },
1281
+ max_tokens: { type: "number", description: "Spec 014: token budget. Default per model tier auto-detected (Opus 4096 / Sonnet 8192 / Haiku 12288), env WYRM_CONTEXT_TOKEN_BUDGET, fallback 4096. Items below budget cutoff elide to stubs the AI can deref with wyrm_recall." },
1282
+ session_id: { type: "number", description: "Spec 014: session id for already-seen dedup. Items already shown in this session render as one-line references." },
1283
+ strict_budget: { type: "boolean", description: "Spec 014: if true, elide ground truths too when preamble exceeds budget. Default false — ground truths are foundation and never elided unless explicitly requested." },
1135
1284
  },
1136
1285
  required: ["projectPath", "task"],
1137
1286
  },
@@ -2910,10 +3059,24 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2910
3059
  return { content: [{ type: "text", text: `🐉 Feedback recorded ${icon}\n\n- Artifact #${artifactId}: "${artifact.problem.slice(0, 60)}"\n- New confidence: ${(updated.confidence * 100).toFixed(0)}%\n- Total uses: ${updated.reuse_count} (✅ ${updated.reuse_success_count} / ❌ ${updated.reuse_failure_count})` }] };
2911
3060
  }
2912
3061
  case "wyrm_context_build": {
2913
- const { projectPath: cbPath, task: cbTask, maxItems: cbMax, kinds: cbKinds, minConfidence: cbMinConf } = args;
3062
+ const { projectPath: cbPath, task: cbTask, maxItems: cbMax, kinds: cbKinds, minConfidence: cbMinConf, max_tokens: cbMaxTokens, session_id: cbSessionId, strict_budget: cbStrictBudget, } = args;
2914
3063
  const cbProject = db.getProject(cbPath);
2915
3064
  if (!cbProject)
2916
3065
  return { content: [{ type: "text", text: `Project not found: ${cbPath}` }], isError: true };
3066
+ // Spec 014 budgeted path — only when max_tokens is supplied (backwards compat: criterion 7).
3067
+ // Kill switch: WYRM_DISABLE_TOKEN_BUDGET=1 routes everything to legacy.
3068
+ const wantsBudget = cbMaxTokens != null && !process.env.WYRM_DISABLE_TOKEN_BUDGET;
3069
+ if (wantsBudget) {
3070
+ return runBudgetedContextBuild({
3071
+ project: cbProject,
3072
+ task: cbTask,
3073
+ maxTokens: cbMaxTokens,
3074
+ sessionId: cbSessionId,
3075
+ strictBudget: cbStrictBudget === true,
3076
+ kinds: cbKinds,
3077
+ minConfidence: cbMinConf,
3078
+ });
3079
+ }
2917
3080
  const sections = [];
2918
3081
  // Section 1: Ground truths (~1200 char budget)
2919
3082
  const truthsText = groundTruths.formatForContext(cbProject.id);
@@ -3306,6 +3469,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3306
3469
  db.vacuum();
3307
3470
  text += `- Vacuumed database\n`;
3308
3471
  }
3472
+ // Spec 014: prune session_seen_artifacts older than WYRM_SEEN_TTL_DAYS (default 7).
3473
+ try {
3474
+ const ttlDays = parseInt(process.env.WYRM_SEEN_TTL_DAYS ?? '7', 10);
3475
+ const pruned = sessionSeen().prune(Number.isFinite(ttlDays) && ttlDays > 0 ? ttlDays : 7);
3476
+ if (pruned > 0)
3477
+ text += `- Pruned ${pruned} session_seen_artifacts rows older than ${ttlDays}d\n`;
3478
+ }
3479
+ catch (e) { /* never fail maintenance on this */ }
3309
3480
  db.checkpoint();
3310
3481
  text += `- Checkpointed WAL\n`;
3311
3482
  const stats = db.getStats();