clawmem 0.1.2 → 0.1.4
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/AGENTS.md +2 -0
- package/CLAUDE.md +2 -0
- package/README.md +16 -1
- package/SKILL.md +2 -0
- package/package.json +1 -1
- package/src/config.ts +16 -3
- package/src/hooks/context-surfacing.ts +118 -11
- package/src/hooks/curator-nudge.ts +14 -1
package/AGENTS.md
CHANGED
|
@@ -257,6 +257,8 @@ ClawMem hooks handle ~90% of retrieval automatically. Agent-initiated MCP calls
|
|
|
257
257
|
|
|
258
258
|
**Hook blind spots (by design):** Hooks filter out `_clawmem/` system artifacts, enforce score thresholds, and cap token budget. Absence in `<vault-context>` does NOT mean absence in memory. If you expect a memory to exist but it wasn't surfaced, escalate to Tier 3.
|
|
259
259
|
|
|
260
|
+
**Adaptive thresholds:** Context-surfacing uses ratio-based scoring that adapts to vault characteristics (size, document quality, content age, embedding model). Results are kept within a percentage of the best result's composite score rather than a fixed absolute threshold. An activation floor prevents surfacing when all results are weak. Profiles control the ratio: `speed` (65%), `balanced` (55%), `deep` (45% + query expansion + reranking). `CLAWMEM_PROFILE=deep` is recommended for vaults with older content or lower-quality documents. MCP tools use fixed absolute thresholds, not adaptive.
|
|
261
|
+
|
|
260
262
|
### Tier 3 — Agent-Initiated (one targeted MCP call)
|
|
261
263
|
|
|
262
264
|
**Escalate ONLY when one of these three rules fires:**
|
package/CLAUDE.md
CHANGED
|
@@ -257,6 +257,8 @@ ClawMem hooks handle ~90% of retrieval automatically. Agent-initiated MCP calls
|
|
|
257
257
|
|
|
258
258
|
**Hook blind spots (by design):** Hooks filter out `_clawmem/` system artifacts, enforce score thresholds, and cap token budget. Absence in `<vault-context>` does NOT mean absence in memory. If you expect a memory to exist but it wasn't surfaced, escalate to Tier 3.
|
|
259
259
|
|
|
260
|
+
**Adaptive thresholds:** Context-surfacing uses ratio-based scoring that adapts to vault characteristics (size, document quality, content age, embedding model). Results are kept within a percentage of the best result's composite score rather than a fixed absolute threshold. An activation floor prevents surfacing when all results are weak. Profiles control the ratio: `speed` (65%), `balanced` (55%), `deep` (45% + query expansion + reranking). `CLAWMEM_PROFILE=deep` is recommended for vaults with older content or lower-quality documents. MCP tools use fixed absolute thresholds, not adaptive.
|
|
261
|
+
|
|
260
262
|
### Tier 3 — Agent-Initiated (one targeted MCP call)
|
|
261
263
|
|
|
262
264
|
**Escalate ONLY when one of these three rules fires:**
|
package/README.md
CHANGED
|
@@ -63,7 +63,7 @@ Runs fully local with no API keys and no cloud services. Integrates via Claude C
|
|
|
63
63
|
|
|
64
64
|
**Required:**
|
|
65
65
|
|
|
66
|
-
- [Bun](https://bun.sh) v1.0+ — runtime for ClawMem
|
|
66
|
+
- [Bun](https://bun.sh) v1.0+ — runtime for ClawMem. On Linux, install via `curl -fsSL https://bun.sh/install | bash` (not snap — snap Bun cannot read stdin, which breaks hooks).
|
|
67
67
|
- SQLite with FTS5 — included with Bun. On macOS, install `brew install sqlite` for extension loading support (ClawMem detects and uses Homebrew SQLite automatically).
|
|
68
68
|
|
|
69
69
|
**Optional (for better performance):**
|
|
@@ -1015,6 +1015,21 @@ Built on the shoulders of:
|
|
|
1015
1015
|
- [SAME](https://github.com/sgx-labs/statelessagent) — agent memory concepts (recency decay, confidence scoring, session tracking)
|
|
1016
1016
|
- [supermemory](https://github.com/supermemoryai/clawdbot-supermemory) — hook patterns and context surfacing ideas
|
|
1017
1017
|
|
|
1018
|
+
## Roadmap
|
|
1019
|
+
|
|
1020
|
+
| Status | Feature | Description |
|
|
1021
|
+
|--------|---------|-------------|
|
|
1022
|
+
| :white_check_mark: | Adaptive thresholds | Ratio-based filtering that adapts to vault characteristics (v0.1.3) |
|
|
1023
|
+
| :white_check_mark: | Deep escalation | Budget-aware query expansion + cross-encoder reranking for `deep` profile |
|
|
1024
|
+
| :white_check_mark: | Cloud embedding providers | Jina, OpenAI, Voyage, Cohere with batch embedding + TPM pacing |
|
|
1025
|
+
| :construction: | Calibration probes | One-time `clawmem calibrate` command that measures your vault's score distribution and tunes thresholds automatically |
|
|
1026
|
+
| :construction: | Rolling threshold learning | Learns optimal activation floor from actual usage patterns (which surfaced content gets referenced vs ignored) |
|
|
1027
|
+
| :memo: | Multi-vault namespacing | Isolated vaults with independent calibration and lifecycle policies |
|
|
1028
|
+
| :memo: | REST API authentication | Token-scoped access for multi-agent deployments |
|
|
1029
|
+
| :memo: | Streaming rerank | Cross-encoder reranking within the hook timeout budget for larger candidate sets |
|
|
1030
|
+
|
|
1031
|
+
:white_check_mark: Shipped  :construction: Planned  :memo: Exploring
|
|
1032
|
+
|
|
1018
1033
|
## License
|
|
1019
1034
|
|
|
1020
1035
|
MIT
|
package/SKILL.md
CHANGED
|
@@ -191,6 +191,8 @@ Hooks handle ~90% of retrieval. Zero agent effort.
|
|
|
191
191
|
|
|
192
192
|
**Hook blind spots (by design):** Hooks filter out `_clawmem/` system artifacts, enforce score thresholds, and cap token budget. Absence in `<vault-context>` does NOT mean absence in memory. Escalate to Tier 3 if expected memory wasn't surfaced.
|
|
193
193
|
|
|
194
|
+
**Adaptive thresholds:** Context-surfacing uses ratio-based scoring that adapts to vault characteristics. Results are kept within a percentage of the best result's composite score (speed: 65%, balanced: 55%, deep: 45%). An activation floor prevents surfacing when all results are weak. `CLAWMEM_PROFILE=deep` adds query expansion + reranking. MCP tools use fixed absolute thresholds, not adaptive.
|
|
195
|
+
|
|
194
196
|
---
|
|
195
197
|
|
|
196
198
|
## Tier 3 — Agent-Initiated Retrieval (MCP Tools)
|
package/package.json
CHANGED
package/src/config.ts
CHANGED
|
@@ -70,13 +70,26 @@ export interface ProfileConfig {
|
|
|
70
70
|
maxResults: number;
|
|
71
71
|
useVector: boolean;
|
|
72
72
|
vectorTimeout: number;
|
|
73
|
+
/** Legacy absolute threshold — used by MCP tools and as fallback when thresholdMode="absolute" */
|
|
73
74
|
minScore: number;
|
|
75
|
+
/** Adaptive: keep results within this ratio of best score (e.g., 0.55 = top 55%) */
|
|
76
|
+
minScoreRatio: number;
|
|
77
|
+
/** Adaptive: never surface below this regardless of ratio */
|
|
78
|
+
absoluteFloor: number;
|
|
79
|
+
/** Adaptive: if best result is below this, return empty (prevents all-weak surfacing) */
|
|
80
|
+
activationFloor: number;
|
|
81
|
+
/** "adaptive" uses ratio-based filtering; "absolute" uses legacy minScore */
|
|
82
|
+
thresholdMode: "adaptive" | "absolute";
|
|
83
|
+
/** Budget-aware escalation: if fast path finishes early, spend remaining time on expansion + reranking */
|
|
84
|
+
deepEscalation: boolean;
|
|
85
|
+
/** Max time (ms) allowed for the fast path before escalation is considered */
|
|
86
|
+
escalationBudgetMs: number;
|
|
74
87
|
}
|
|
75
88
|
|
|
76
89
|
export const PROFILES: Record<PerformanceProfile, ProfileConfig> = {
|
|
77
|
-
speed: { tokenBudget: 400, maxResults: 5, useVector: false, vectorTimeout: 0, minScore: 0.55 },
|
|
78
|
-
balanced: { tokenBudget: 800, maxResults: 10, useVector: true, vectorTimeout: 900, minScore: 0.45 },
|
|
79
|
-
deep: { tokenBudget: 1200, maxResults: 15, useVector: true, vectorTimeout: 2000, minScore: 0.
|
|
90
|
+
speed: { tokenBudget: 400, maxResults: 5, useVector: false, vectorTimeout: 0, minScore: 0.55, minScoreRatio: 0.65, absoluteFloor: 0.18, activationFloor: 0.24, thresholdMode: "adaptive", deepEscalation: false, escalationBudgetMs: 0 },
|
|
91
|
+
balanced: { tokenBudget: 800, maxResults: 10, useVector: true, vectorTimeout: 900, minScore: 0.45, minScoreRatio: 0.55, absoluteFloor: 0.15, activationFloor: 0.20, thresholdMode: "adaptive", deepEscalation: false, escalationBudgetMs: 0 },
|
|
92
|
+
deep: { tokenBudget: 1200, maxResults: 15, useVector: true, vectorTimeout: 2000, minScore: 0.25, minScoreRatio: 0.45, absoluteFloor: 0.12, activationFloor: 0.16, thresholdMode: "adaptive", deepEscalation: true, escalationBudgetMs: 4000 },
|
|
80
93
|
};
|
|
81
94
|
|
|
82
95
|
export function getActiveProfile(): ProfileConfig {
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { Store, SearchResult } from "../store.ts";
|
|
10
|
-
import { DEFAULT_EMBED_MODEL, extractSnippet } from "../store.ts";
|
|
10
|
+
import { DEFAULT_EMBED_MODEL, DEFAULT_QUERY_MODEL, DEFAULT_RERANK_MODEL, extractSnippet, resolveStore } from "../store.ts";
|
|
11
|
+
import { getVaultPath, getActiveProfile } from "../config.ts";
|
|
11
12
|
import type { HookInput, HookOutput } from "../hooks.ts";
|
|
12
13
|
import {
|
|
13
14
|
makeContextOutput,
|
|
@@ -29,13 +30,12 @@ import { enrichResults } from "../search-utils.ts";
|
|
|
29
30
|
import { sanitizeSnippet } from "../promptguard.ts";
|
|
30
31
|
import { shouldSkipRetrieval, isRetrievedNoise } from "../retrieval-gate.ts";
|
|
31
32
|
import { MAX_QUERY_LENGTH } from "../limits.ts";
|
|
32
|
-
import { getActiveProfile } from "../config.ts";
|
|
33
33
|
|
|
34
34
|
// =============================================================================
|
|
35
35
|
// Config
|
|
36
36
|
// =============================================================================
|
|
37
37
|
|
|
38
|
-
// Profile-driven defaults (overridden by CLAWMEM_PROFILE env var)
|
|
38
|
+
// Profile-driven defaults (overridden by CLAWMEM_PROFILE env var via E14)
|
|
39
39
|
const DEFAULT_TOKEN_BUDGET = 800;
|
|
40
40
|
const DEFAULT_MAX_RESULTS = 10;
|
|
41
41
|
const DEFAULT_MIN_SCORE = 0.45;
|
|
@@ -52,7 +52,7 @@ function getTierConfig(score: number): { snippetLen: number; showMeta: boolean;
|
|
|
52
52
|
// Directories to never surface
|
|
53
53
|
const FILTERED_PATHS = ["_PRIVATE/", "experiments/", "_clawmem/"];
|
|
54
54
|
|
|
55
|
-
// File path patterns to extract from prompts (E13: file-aware UserPromptSubmit)
|
|
55
|
+
// File path patterns to extract from prompts (E13 replacement: file-aware UserPromptSubmit)
|
|
56
56
|
const FILE_PATH_RE = /(?:^|\s)((?:\/[\w.@-]+)+(?:\.\w+)?|[\w.@-]+\.(?:ts|js|py|md|sh|yaml|yml|json|toml|rs|go|tsx|jsx|css|html))\b/g;
|
|
57
57
|
|
|
58
58
|
// =============================================================================
|
|
@@ -81,10 +81,11 @@ export async function contextSurfacing(
|
|
|
81
81
|
return makeEmptyOutput("context-surfacing");
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
// Load active performance profile
|
|
84
|
+
// Load active performance profile (E14)
|
|
85
85
|
const profile = getActiveProfile();
|
|
86
86
|
const maxResults = profile.maxResults;
|
|
87
87
|
const tokenBudget = profile.tokenBudget;
|
|
88
|
+
const startTime = Date.now();
|
|
88
89
|
|
|
89
90
|
const isRecency = hasRecencyIntent(prompt);
|
|
90
91
|
const minScore = isRecency ? MIN_COMPOSITE_SCORE_RECENCY : profile.minScore;
|
|
@@ -118,7 +119,22 @@ export async function contextSurfacing(
|
|
|
118
119
|
}
|
|
119
120
|
}
|
|
120
121
|
|
|
121
|
-
//
|
|
122
|
+
// Dual-query: also search skill vault if configured (secondary source)
|
|
123
|
+
if (getVaultPath("skill")) {
|
|
124
|
+
try {
|
|
125
|
+
const skillStore = resolveStore("skill");
|
|
126
|
+
const skillResults = skillStore.searchFTS(prompt, 5);
|
|
127
|
+
// Tag skill vault results for identification in output
|
|
128
|
+
for (const r of skillResults) {
|
|
129
|
+
(r as any)._fromVault = "skill";
|
|
130
|
+
}
|
|
131
|
+
results = [...results, ...skillResults];
|
|
132
|
+
} catch {
|
|
133
|
+
// Skill vault unavailable — continue with general results only
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// File-aware supplemental search (E13 replacement): extract file paths/names from prompt
|
|
122
138
|
// and run targeted FTS queries to surface file-specific vault context
|
|
123
139
|
const fileMatches = [...prompt.matchAll(FILE_PATH_RE)].map(m => m[1]!.trim()).filter(Boolean);
|
|
124
140
|
if (fileMatches.length > 0) {
|
|
@@ -138,6 +154,57 @@ export async function contextSurfacing(
|
|
|
138
154
|
|
|
139
155
|
if (results.length === 0) return makeEmptyOutput("context-surfacing");
|
|
140
156
|
|
|
157
|
+
// Budget-aware deep escalation (deep profile only):
|
|
158
|
+
// If the fast path finished quickly and found results, spend remaining time budget
|
|
159
|
+
// on query expansion (discovers new candidates) and cross-encoder reranking (reorders).
|
|
160
|
+
if (profile.deepEscalation && results.length >= 2) {
|
|
161
|
+
const elapsed = Date.now() - startTime;
|
|
162
|
+
if (elapsed < profile.escalationBudgetMs) {
|
|
163
|
+
try {
|
|
164
|
+
// Phase 1: Query expansion — discover candidates BM25+vector missed
|
|
165
|
+
const expanded = await store.expandQuery(prompt, DEFAULT_QUERY_MODEL);
|
|
166
|
+
if (expanded.length > 0) {
|
|
167
|
+
const seen = new Set(results.map(r => r.filepath));
|
|
168
|
+
for (const eq of expanded.slice(0, 3)) {
|
|
169
|
+
if (Date.now() - startTime > 6000) break; // hard stop at 6s
|
|
170
|
+
const ftsExp = store.searchFTS(eq, 5);
|
|
171
|
+
for (const r of ftsExp) {
|
|
172
|
+
if (!seen.has(r.filepath)) {
|
|
173
|
+
seen.add(r.filepath);
|
|
174
|
+
results.push(r);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Phase 2: Cross-encoder reranking — reorder with deeper relevance signal
|
|
181
|
+
// Sort by score first so reranking covers the best candidates, not just
|
|
182
|
+
// the first-inserted (expansion hits appended later would otherwise be missed)
|
|
183
|
+
if (Date.now() - startTime < 6000 && results.length >= 3) {
|
|
184
|
+
results.sort((a, b) => b.score - a.score);
|
|
185
|
+
const toRerank = results.slice(0, 15).map(r => ({
|
|
186
|
+
file: r.filepath,
|
|
187
|
+
text: (r.body || "").slice(0, 2000),
|
|
188
|
+
}));
|
|
189
|
+
const reranked = await store.rerank(prompt, toRerank, DEFAULT_RERANK_MODEL);
|
|
190
|
+
if (reranked.length > 0) {
|
|
191
|
+
const rerankedMap = new Map(reranked.map(r => [r.file, r.score]));
|
|
192
|
+
// Blend: 60% original score + 40% reranker score for stability
|
|
193
|
+
for (const r of results) {
|
|
194
|
+
const rerankScore = rerankedMap.get(r.filepath);
|
|
195
|
+
if (rerankScore !== undefined) {
|
|
196
|
+
r.score = 0.6 * r.score + 0.4 * rerankScore;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
results.sort((a, b) => b.score - a.score);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
} catch {
|
|
203
|
+
// Escalation failed (GPU down, timeout, etc.) — continue with fast-path results
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
141
208
|
// Filter out private/excluded paths
|
|
142
209
|
results = results.filter(r =>
|
|
143
210
|
!FILTERED_PATHS.some(p => r.displayPath.includes(p))
|
|
@@ -148,8 +215,12 @@ export async function contextSurfacing(
|
|
|
148
215
|
// Filter out snoozed documents
|
|
149
216
|
const now = new Date();
|
|
150
217
|
results = results.filter(r => {
|
|
218
|
+
// filepath is a virtual path (clawmem://collection/path) but findActiveDocument
|
|
219
|
+
// expects the collection-relative path, not the full virtual path
|
|
151
220
|
const parsed = r.filepath.startsWith('clawmem://') ? r.filepath.replace(/^clawmem:\/\/[^/]+\/?/, '') : r.filepath;
|
|
152
|
-
|
|
221
|
+
// Use the correct store for skill-vault results
|
|
222
|
+
const targetStore = (r as any)._fromVault === "skill" ? (() => { try { return resolveStore("skill"); } catch { return store; } })() : store;
|
|
223
|
+
const doc = targetStore.findActiveDocument(r.collectionName, parsed);
|
|
153
224
|
if (!doc) return true;
|
|
154
225
|
if (doc.snoozed_until && new Date(doc.snoozed_until) > now) return false;
|
|
155
226
|
return true;
|
|
@@ -170,12 +241,41 @@ export async function contextSurfacing(
|
|
|
170
241
|
// Filter out noise results (agent denials, too-short snippets) before enrichment
|
|
171
242
|
results = results.filter(r => !r.body || !isRetrievedNoise(r.body));
|
|
172
243
|
|
|
173
|
-
// Enrich with SAME metadata
|
|
174
|
-
const
|
|
244
|
+
// Enrich with SAME metadata — route skill-vault results through their own store
|
|
245
|
+
const generalResults = results.filter(r => !(r as any)._fromVault);
|
|
246
|
+
const skillResults = results.filter(r => (r as any)._fromVault === "skill");
|
|
247
|
+
let enriched = enrichResults(store, generalResults, prompt);
|
|
248
|
+
if (skillResults.length > 0) {
|
|
249
|
+
try {
|
|
250
|
+
const skillStore = resolveStore("skill");
|
|
251
|
+
enriched = [...enriched, ...enrichResults(skillStore, skillResults, prompt)];
|
|
252
|
+
} catch {
|
|
253
|
+
// Skill store unavailable — enrich with general store as fallback
|
|
254
|
+
enriched = [...enriched, ...enrichResults(store, skillResults, prompt)];
|
|
255
|
+
}
|
|
256
|
+
}
|
|
175
257
|
|
|
176
258
|
// Apply composite scoring
|
|
177
|
-
const
|
|
178
|
-
|
|
259
|
+
const allScored = applyCompositeScoring(enriched, prompt);
|
|
260
|
+
|
|
261
|
+
// Threshold filtering — adaptive (ratio-based) or absolute (legacy)
|
|
262
|
+
let scored: typeof allScored;
|
|
263
|
+
if (profile.thresholdMode === "adaptive") {
|
|
264
|
+
// Use max composite score across the set (not positional [0], which may be
|
|
265
|
+
// reordered by recency-intent sorting in applyCompositeScoring)
|
|
266
|
+
const bestScore = allScored.length > 0
|
|
267
|
+
? Math.max(...allScored.map(r => r.compositeScore))
|
|
268
|
+
: 0;
|
|
269
|
+
|
|
270
|
+
// Activation floor: if even the best result is too weak, bail entirely
|
|
271
|
+
if (bestScore < profile.activationFloor) return makeEmptyOutput("context-surfacing");
|
|
272
|
+
|
|
273
|
+
const adaptiveMin = Math.max(bestScore * profile.minScoreRatio, profile.absoluteFloor);
|
|
274
|
+
scored = allScored.filter(r => r.compositeScore >= adaptiveMin);
|
|
275
|
+
} else {
|
|
276
|
+
// Legacy absolute threshold (backward compat)
|
|
277
|
+
scored = allScored.filter(r => r.compositeScore >= minScore);
|
|
278
|
+
}
|
|
179
279
|
|
|
180
280
|
if (scored.length === 0) return makeEmptyOutput("context-surfacing");
|
|
181
281
|
|
|
@@ -191,6 +291,7 @@ export async function contextSurfacing(
|
|
|
191
291
|
for (const ca of coActs) {
|
|
192
292
|
const existing = scored.find(r => r.displayPath === ca.path);
|
|
193
293
|
if (existing && existing.compositeScore <= 0.8) {
|
|
294
|
+
// Boost by 0.1 per co-activation count, capped at +0.2
|
|
194
295
|
existing.compositeScore += Math.min(0.2, 0.1 * Math.min(ca.count, 2));
|
|
195
296
|
}
|
|
196
297
|
}
|
|
@@ -202,12 +303,14 @@ export async function contextSurfacing(
|
|
|
202
303
|
}
|
|
203
304
|
|
|
204
305
|
// Memory type diversification (E10): ensure procedural results aren't crowded out
|
|
306
|
+
// If top results are all semantic, promote the best procedural result
|
|
205
307
|
if (scored.length > 3) {
|
|
206
308
|
const top3Types = scored.slice(0, 3).map(r => inferMemoryType(r.displayPath, r.contentType, r.body));
|
|
207
309
|
const hasProc = top3Types.includes("procedural");
|
|
208
310
|
if (!hasProc) {
|
|
209
311
|
const procIdx = scored.findIndex(r => inferMemoryType(r.displayPath, r.contentType, r.body) === "procedural");
|
|
210
312
|
if (procIdx > 3) {
|
|
313
|
+
// Move the best procedural result to position 3
|
|
211
314
|
const [proc] = scored.splice(procIdx, 1);
|
|
212
315
|
scored.splice(3, 0, proc!);
|
|
213
316
|
}
|
|
@@ -225,6 +328,7 @@ export async function contextSurfacing(
|
|
|
225
328
|
}
|
|
226
329
|
|
|
227
330
|
// Routing hint: detect query intent signals and prepend a tool routing directive
|
|
331
|
+
// This makes routing instructions salient at the moment of tool selection (per research)
|
|
228
332
|
const routingHint = detectRoutingHint(prompt);
|
|
229
333
|
|
|
230
334
|
return makeContextOutput(
|
|
@@ -247,14 +351,17 @@ export async function contextSurfacing(
|
|
|
247
351
|
function detectRoutingHint(prompt: string): string | null {
|
|
248
352
|
const q = prompt.toLowerCase();
|
|
249
353
|
|
|
354
|
+
// Timeline/session signals
|
|
250
355
|
if (/\b(last session|yesterday|prior session|previous session|last time we|handoff|what happened last|what did we do|cross.session|earlier today|what we discussed|when we last)\b/i.test(q)) {
|
|
251
356
|
return "If searching memory for this: use session_log or memory_retrieve, NOT query.";
|
|
252
357
|
}
|
|
253
358
|
|
|
359
|
+
// Causal signals
|
|
254
360
|
if (/\b(why did|why was|why were|what caused|what led to|reason for|decided to|decision about|trade.?off|instead of|chose to)\b/i.test(q) || /^why\b/i.test(q)) {
|
|
255
361
|
return "If searching memory for this: use intent_search or memory_retrieve, NOT query.";
|
|
256
362
|
}
|
|
257
363
|
|
|
364
|
+
// Discovery signals
|
|
258
365
|
if (/\b(similar to|related to|what else|what other|reminds? me of|like this)\b/i.test(q)) {
|
|
259
366
|
return "If searching memory for this: use find_similar or memory_retrieve, NOT query.";
|
|
260
367
|
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { resolve as pathResolve } from "path";
|
|
10
10
|
import { existsSync, readFileSync } from "fs";
|
|
11
11
|
import type { Store } from "../store.ts";
|
|
12
|
+
import { getIndexHealth } from "../store.ts";
|
|
12
13
|
import type { HookInput, HookOutput } from "../hooks.ts";
|
|
13
14
|
import {
|
|
14
15
|
makeContextOutput,
|
|
@@ -64,11 +65,23 @@ export async function curatorNudge(
|
|
|
64
65
|
return makeEmptyOutput("curator-nudge");
|
|
65
66
|
}
|
|
66
67
|
|
|
68
|
+
// Override embedding backlog with live data (report value goes stale after embed timer runs)
|
|
69
|
+
let actions = [...report.actions];
|
|
70
|
+
try {
|
|
71
|
+
const health = getIndexHealth(store.db);
|
|
72
|
+
actions = actions.filter(a => !/documents? need embedding/i.test(a));
|
|
73
|
+
if (health.needsEmbedding > 0) {
|
|
74
|
+
actions.unshift(`${health.needsEmbedding} documents need embedding`);
|
|
75
|
+
}
|
|
76
|
+
} catch { /* fail-open: use report actions as-is */ }
|
|
77
|
+
|
|
78
|
+
if (actions.length === 0) return makeEmptyOutput("curator-nudge");
|
|
79
|
+
|
|
67
80
|
// Build compact action summary within budget
|
|
68
81
|
const lines = [`**Curator (${report.timestamp.slice(0, 10)}):**`];
|
|
69
82
|
let tokens = estimateTokens(lines[0]!);
|
|
70
83
|
|
|
71
|
-
for (const action of
|
|
84
|
+
for (const action of actions) {
|
|
72
85
|
const line = `- ${action}`;
|
|
73
86
|
const lineTokens = estimateTokens(line);
|
|
74
87
|
if (tokens + lineTokens > MAX_TOKEN_BUDGET && lines.length > 1) break;
|