claude-mem-lite 5.2.0 → 5.3.1
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +8 -4
- package/deep-search.mjs +60 -0
- package/hook.mjs +9 -2
- package/lib/maintain-core.mjs +15 -0
- package/mem-cli.mjs +20 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/scripts/hook-launcher.mjs +17 -6
- package/server.mjs +19 -1
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"plugins": [
|
|
11
11
|
{
|
|
12
12
|
"name": "claude-mem-lite",
|
|
13
|
-
"version": "5.
|
|
13
|
+
"version": "5.3.1",
|
|
14
14
|
"source": "./",
|
|
15
15
|
"description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. Hybrid FTS5 + TF-IDF search with episode batching. Single SQLite DB, no external services. A lighter, lower-cost alternative to claude-mem (episode batching + a smaller model; cost savings are an internal estimate, not a measured benchmark)."
|
|
16
16
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.3.1",
|
|
4
4
|
"description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. Hybrid FTS5 + TF-IDF search with episode batching. Single SQLite DB, no external services. A lighter, lower-cost alternative to claude-mem (episode batching + a smaller model; cost savings are an internal estimate, not a measured benchmark).",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "sdsrss"
|
package/README.md
CHANGED
|
@@ -120,7 +120,7 @@ How claude-mem-lite differs from the major neighbors in the LLM-memory space (ve
|
|
|
120
120
|
- **Schema auto-migration** -- Idempotent `ALTER TABLE` migrations run on every startup, safely adding new columns and indexes without data loss
|
|
121
121
|
- **LLM concurrency control** -- File-based semaphore limits background workers to 2 concurrent LLM calls, preventing resource contention
|
|
122
122
|
- **stdin overflow protection** -- Hook input truncated at 256KB with regex-based action salvage for oversized tool outputs
|
|
123
|
-
- **Cross-session handoff** -- Captures session state (request, completed work, next steps, key files) on `/
|
|
123
|
+
- **Cross-session handoff** -- Captures session state (request, completed work, next steps, key files) on `/exit`, then injects context when the next session detects continuation intent via explicit keywords or FTS5 term overlap. **The `/clear` and `/compact` arm does not currently fire** (tracked as R10-P1-1). The measurement, first: on the maintainer's own install `session_handoffs` holds 4 `exit` rows and **0** `clear` rows — the `clear` snapshot has never once been written. The mechanism, as a hypothesis: SessionStart treats a session file left on disk as the marker of a previous session that ended without `Stop` (`hook.mjs:2393-2405`, whose own comment reads "Normal `/exit` deletes the file, so this only triggers for `/clear`, `/compact`, or crash recovery"), and that file is exactly what `Stop` deletes (`hook.mjs:1542`) — which would make the branch unreachable if `Stop` runs at the end of every assistant turn rather than once per session. **That last clause is NOT verified**, and it is the whole question: the fix differs depending on whether Claude Code rotates its session id across `/clear`, so a real `/clear` stdin capture comes before any code change
|
|
124
124
|
- **Git-SHA continuation anchor** (v2.31.0) -- Handoff rows include `git_sha_at_handoff`; any handoff matching the current `HEAD` counts as continuation regardless of TTL. Code state is a stronger continuation signal than wall-clock time
|
|
125
125
|
- **Startup dashboard** (v2.31.0) -- SessionStart hook aggregates `git status` + `~/.claude/tasks/*.json` + `~/.claude/plans/*.md` + most-recent exit handoff + recent event count into a single structured block injected via `hookSpecificOutput.additionalContext`
|
|
126
126
|
- **Activity namespace** (v2.31.0) -- Dedicated `events` table + FTS5 for non-memdir types (`bugfix`, `lesson`, `bug`, `discovery`, `refactor`, `feature`, `observation`, `decision`) that don't compete with `WHAT_NOT_TO_SAVE` semantics on the observations table. CLI: `claude-mem-lite activity save|search|recent|show`. `hook-llm` routes non-memdir summary types through `persistHaikuSummary` so upgrades from observations→events are atomic. (v3.39: the `/lesson` and `/bug` slash commands were redirected from this events table to searchable **observations** — `mem_search` never read the events table, so explicit saves were unfindable; the events table remains the auto-capture activity log.)
|
|
@@ -431,7 +431,9 @@ FTS5 indexes: `observations_fts` (title, subtitle, narrative, text, facts, conce
|
|
|
431
431
|
|
|
432
432
|
```
|
|
433
433
|
SessionStart
|
|
434
|
-
-> Generate session ID
|
|
434
|
+
-> Generate session ID
|
|
435
|
+
(the /clear|/compact handoff branch here is currently unreachable — R10-P1-1,
|
|
436
|
+
see Cross-session handoff above)
|
|
435
437
|
-> Mark stale sessions (>24h active) as abandoned
|
|
436
438
|
-> Clean orphaned/stale lock files
|
|
437
439
|
-> Query recent observations (24h)
|
|
@@ -458,8 +460,9 @@ UserPromptSubmit (two parallel paths)
|
|
|
458
460
|
|
|
459
461
|
Stop
|
|
460
462
|
-> Flush final episode buffer
|
|
461
|
-
-> Save handoff snapshot (
|
|
463
|
+
-> Save handoff snapshot (type 'exit')
|
|
462
464
|
-> Mark session completed
|
|
465
|
+
-> Delete the session file <- what makes the SessionStart /clear branch unreachable
|
|
463
466
|
-> Spawn LLM summary worker (poll-based wait)
|
|
464
467
|
```
|
|
465
468
|
|
|
@@ -796,6 +799,7 @@ benchmark and A/B harness are calibrated against — changing them invalidates t
|
|
|
796
799
|
| `MEM_OR_FALLBACK_MAX_TOKENS` | Max query tokens allowed into the OR fallback (∈ [0,50]). | `8` |
|
|
797
800
|
| `CLAUDE_MEM_CJK_PREC_MIN` | Precision floor for CJK segmentation candidates. | `0.2` |
|
|
798
801
|
| `CLAUDE_MEM_AUTO_DEEP` | `0` disables automatic deep-search escalation (one Haiku call rewriting a weak query into keyword/concept/HyDE variants). Explicit `deep: true` still works. | _(auto)_ |
|
|
802
|
+
| `CLAUDE_MEM_DEEP_DISCLOSURE` | `off` suppresses the one-line caveat appended to a multi-variant deep result. The caveat exists because deep search fills the page even when the corpus cannot answer — measured at 10 of 10 slots on queries whose answers had been removed (`benchmark/deep-search-holdout.mjs`) — and `deep` is AUTO by default on the MCP surface, i.e. it escalates precisely when the honest answer is "nothing". It does not change retrieval, ranking, or which rows are returned. | _(on)_ |
|
|
799
803
|
| `CLAUDE_MEM_AUTO_DEEP_CLI` | `0` disables the same auto-escalation on the CLI path only. | _(auto)_ |
|
|
800
804
|
| `CLAUDE_MEM_VECTORS` | `1` re-enables the persisted TF-IDF vector arm (off by default; also needs a vector rebuild via `maintain`). | _(off)_ |
|
|
801
805
|
| `CLAUDE_MEM_SCOPE_FILTER` | `1` stops environment-scoped observations from firing on file-triggered recall. They stay reachable via search. **Leave it off**: on the face it gates, `environment` is not the low-relevance class its premise assumes — it cites at least as well as `project` (47.5% vs 44.3%, intervals overlapping), and an earlier measurement left 173 recall groups empty with it on. | _(off)_ |
|
|
@@ -823,7 +827,7 @@ what is already stored — only whether new work runs.
|
|
|
823
827
|
|
|
824
828
|
| Variable | Description | Default |
|
|
825
829
|
|----------|-------------|---------|
|
|
826
|
-
| `CLAUDE_MEM_SKIP_SUMMARY` | Skip the LLM session summary at Stop. | _(runs)_ |
|
|
830
|
+
| `CLAUDE_MEM_SKIP_SUMMARY` | Skip the background LLM session summary at **both** of its spawn sites — `Stop`, and the SessionStart `/clear`-handoff path. Until v5.3.0 only the `Stop` one honoured it. | _(runs)_ |
|
|
827
831
|
| `CLAUDE_MEM_SKIP_EPISODE_LLM` | Skip LLM extraction on episode flush — observations are still batched, just not summarized. | _(runs)_ |
|
|
828
832
|
| `CLAUDE_MEM_SKIP_SAVE_ENRICH` | Skip the background Haiku call that backfills `lesson_learned` / search aliases after a save. | _(runs)_ |
|
|
829
833
|
| `CLAUDE_MEM_SKIP_COMPRESS` | Skip auto-compression of old observations. | _(runs)_ |
|
package/deep-search.mjs
CHANGED
|
@@ -185,6 +185,66 @@ export function resolveDeepMode(explicitDeep, { surface, env = process.env } = {
|
|
|
185
185
|
return surface === 'mcp' ? 'auto' : 'normal';
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
+
/**
|
|
189
|
+
* One-line disclosure for a deep result set, written to the channel the CALLER reads.
|
|
190
|
+
*
|
|
191
|
+
* D#3. benchmark/deep-search-holdout.mjs asks the suite's own queries of a corpus with
|
|
192
|
+
* their relevant_ids deleted, so the correct answer is zero rows and every returned row is
|
|
193
|
+
* a false positive by construction. It reads mean FP@10 = 10.00 across 12/12 queries: deep
|
|
194
|
+
* fills every slot, every time. The single-query baseline returns 1-2 rows on the same
|
|
195
|
+
* negatives — the flood is the UNION across paraphrase variants, which is also where deep's
|
|
196
|
+
* recall win comes from, so this is not a bug to be thresholded away. Three gates were
|
|
197
|
+
* tested against both arms and rejected; suppressing OR-fallback on rewrites takes deep
|
|
198
|
+
* R@10 from 0.7383 to 0.3962, because the vocab-mismatch win IS that fallback. rrfFuseN
|
|
199
|
+
* fuses by RANK, so no magnitude signal survives the merge for a downstream floor to read.
|
|
200
|
+
*
|
|
201
|
+
* The discrimination is not available at this layer, so the honest move is to hand the
|
|
202
|
+
* caller what the caller cannot otherwise see. Two things were missing:
|
|
203
|
+
* 1. `escalated` — that the widening happened BECAUSE the plain search was weak — was
|
|
204
|
+
* announced on stderr only. On the MCP surface stderr never reaches the model, which
|
|
205
|
+
* reads tool results; auto is the default there (resolveDeepMode, surface 'mcp'), so
|
|
206
|
+
* the one caller who most needs the caveat was the one who could not see it.
|
|
207
|
+
* 2. Nothing said a full page can be entirely adjacent rows.
|
|
208
|
+
*
|
|
209
|
+
* Silent in two cases, and both silences are load-bearing:
|
|
210
|
+
* - `variantCount <= 1`: with no usable rewrite, deep IS the baseline, and the existing
|
|
211
|
+
* "== baseline" note already says so. Crying flood there would train callers to ignore
|
|
212
|
+
* the line on the runs where it matters.
|
|
213
|
+
* - `rowCount <= 0`: "rows above may be adjacent" is nonsense with no rows above, and
|
|
214
|
+
* both zero-result faces ALREADY say the rewrite ran and found nothing — so the note
|
|
215
|
+
* would restate the page's own conclusion in more words. Caught in pre-ship review,
|
|
216
|
+
* which is also why `rowCount` is a parameter rather than a check in each face: two
|
|
217
|
+
* surfaces deciding this separately is how they drift.
|
|
218
|
+
*
|
|
219
|
+
* @param {object} [opts]
|
|
220
|
+
* @param {boolean} [opts.escalated] the result came from auto-escalation, not an explicit ask
|
|
221
|
+
* @param {number} [opts.escalatedObsCount] hits the plain search returned before widening
|
|
222
|
+
* @param {number} [opts.variantCount] query variants fused (1 = rewrite produced nothing)
|
|
223
|
+
* @param {number} [opts.rowCount] rows actually shown to the caller
|
|
224
|
+
* @param {object} [opts.env=process.env] opt-out: CLAUDE_MEM_DEEP_DISCLOSURE=off
|
|
225
|
+
* @returns {string} the note, or '' when it should not be shown
|
|
226
|
+
*/
|
|
227
|
+
export function deepDisclosureNote({
|
|
228
|
+
escalated = false,
|
|
229
|
+
escalatedObsCount = 0,
|
|
230
|
+
variantCount = 0,
|
|
231
|
+
rowCount = 0,
|
|
232
|
+
env = process.env,
|
|
233
|
+
} = {}) {
|
|
234
|
+
if (String(env.CLAUDE_MEM_DEEP_DISCLOSURE || '').toLowerCase() === 'off') return '';
|
|
235
|
+
if (!(variantCount > 1)) return '';
|
|
236
|
+
if (!(rowCount > 0)) return '';
|
|
237
|
+
const why = escalated
|
|
238
|
+
? `auto-escalated after the plain search returned ${escalatedObsCount} hit(s)`
|
|
239
|
+
: 'explicitly requested';
|
|
240
|
+
return (
|
|
241
|
+
`[deep search: ${why}. Rows above may be ADJACENT to the query rather than answers to it — ` +
|
|
242
|
+
`on a corpus that cannot answer, the widened query still fills the page (measured: 10 of 10 ` +
|
|
243
|
+
`slots on queries whose answers had been removed). Judge each row by its own text; ` +
|
|
244
|
+
`"nothing here actually answers this" is a valid conclusion.]`
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
188
248
|
// Echoes hook-llm.mjs MEMORY_INPUT_GUARD (kept inline rather than imported so
|
|
189
249
|
// this module — and the tests that import it — never pull in hook-llm's
|
|
190
250
|
// native-heavy chain; see #8729). Same security intent: the query is untrusted.
|
package/hook.mjs
CHANGED
|
@@ -2038,8 +2038,15 @@ function saveHandoffAndFastSummary(
|
|
|
2038
2038
|
.get(prevProject || project, 'clear', handoffScopeId);
|
|
2039
2039
|
} catch {}
|
|
2040
2040
|
|
|
2041
|
-
// Generate session summary for previous session (background Haiku — richer version)
|
|
2042
|
-
|
|
2041
|
+
// Generate session summary for previous session (background Haiku — richer version).
|
|
2042
|
+
// Honours CLAUDE_MEM_SKIP_SUMMARY like the handleStop site does. This is the SAME
|
|
2043
|
+
// worker, and the flag's whole purpose (see the comment at its other call site) is
|
|
2044
|
+
// that llm-summary recreates a test's sandbox tree behind its cleanup — timed at
|
|
2045
|
+
// 432ms there. Gating one of two call sites left the flag unable to do the one job
|
|
2046
|
+
// it exists for whenever this branch is reached.
|
|
2047
|
+
if (!process.env.CLAUDE_MEM_SKIP_SUMMARY) {
|
|
2048
|
+
spawnBackground('llm-summary', prevSessionId, prevProject || project);
|
|
2049
|
+
}
|
|
2043
2050
|
|
|
2044
2051
|
// Build fast synchronous summary for immediate context availability.
|
|
2045
2052
|
// Background llm-summary will produce a richer Haiku version later;
|
package/lib/maintain-core.mjs
CHANGED
|
@@ -394,6 +394,21 @@ export function cleanupBroken(db, { projectFilter, baseParams, opCap = OP_CAP })
|
|
|
394
394
|
-- lesson). Parity with the "lessons never auto-GC" guards in
|
|
395
395
|
-- decayAndMarkIdle / selectCompressionCandidates / findSmartCompressCandidates.
|
|
396
396
|
AND (lesson_learned IS NULL OR lesson_learned = '' OR lesson_learned = 'none')
|
|
397
|
+
-- D#4. The one HARD DELETE in this family, and until now the only site whose
|
|
398
|
+
-- exemption from liveObsFilterSql was a LIKELIHOOD argument rather than an
|
|
399
|
+
-- inertness proof: these rows have no title, narrative or lesson, so they are
|
|
400
|
+
-- absent from every injection surface, so an id that was never injected is not one
|
|
401
|
+
-- a #NN cites. Narrow, but reachable -- a hand-typed #NN, or a numeric
|
|
402
|
+
-- "save --supersedes" chain later blanked by a degenerate cluster-merge -- and what
|
|
403
|
+
-- the delete takes with it is the superseded_by that
|
|
404
|
+
-- citation-tracker.redirectSupersededIds (:1363) follows to credit a corrected
|
|
405
|
+
-- memory's #NN to its successor.
|
|
406
|
+
--
|
|
407
|
+
-- Deliberately NOT the full liveObsFilterSql. A retired row whose superseded_by is
|
|
408
|
+
-- null hands that redirect nothing: it falls through to out.add(id) (:1379-1381),
|
|
409
|
+
-- the same answer a missing row produces. Filtering on superseded_at instead would
|
|
410
|
+
-- strand every empty retired row here forever for no gain.
|
|
411
|
+
AND superseded_by IS NULL
|
|
397
412
|
${projectFilter} LIMIT ${opCap}
|
|
398
413
|
`,
|
|
399
414
|
)
|
package/mem-cli.mjs
CHANGED
|
@@ -35,7 +35,13 @@ import {
|
|
|
35
35
|
BROWSE_TIERS,
|
|
36
36
|
BROWSE_TIER_LABELS,
|
|
37
37
|
} from './lib/browse-core.mjs';
|
|
38
|
-
import {
|
|
38
|
+
import {
|
|
39
|
+
deepSearch,
|
|
40
|
+
resolveDeepMode,
|
|
41
|
+
shouldEscalateToDeep,
|
|
42
|
+
autoDeepLlmReady,
|
|
43
|
+
deepDisclosureNote,
|
|
44
|
+
} from './deep-search.mjs';
|
|
39
45
|
import { selectCompressionCandidates, groupByProjectWeek, compressGroup } from './lib/compress-core.mjs';
|
|
40
46
|
import {
|
|
41
47
|
runMaintainOps,
|
|
@@ -433,6 +439,19 @@ async function cmdSearch(db, args, { llm } = {}) {
|
|
|
433
439
|
: '[mem] Deep search: rerank produced no usable order; kept fused order\n',
|
|
434
440
|
);
|
|
435
441
|
}
|
|
442
|
+
// D#3. Same disclosure the MCP surface puts in its payload, on the channel this face
|
|
443
|
+
// already uses for deep notes — a Bash caller reads stderr alongside stdout, so unlike
|
|
444
|
+
// MCP there is no split here. One home for the text (deep-search.mjs) because two faces
|
|
445
|
+
// writing their own wording is this repo's most-paid-for defect class.
|
|
446
|
+
if (isDeep) {
|
|
447
|
+
const disclosure = deepDisclosureNote({
|
|
448
|
+
escalated: res.escalated,
|
|
449
|
+
escalatedObsCount: res.escalatedObsCount,
|
|
450
|
+
variantCount: deepVariants?.length ?? 0,
|
|
451
|
+
rowCount: paged?.length ?? 0,
|
|
452
|
+
});
|
|
453
|
+
if (disclosure) process.stderr.write(`${disclosure}\n`);
|
|
454
|
+
}
|
|
436
455
|
|
|
437
456
|
// "nothing matched" (no offset) vs "this page is empty" (with offset) — the two
|
|
438
457
|
// CLI messages. preFinalizeCount is the pre-pagination population (post-tier).
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.3.1",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "claude-mem-lite",
|
|
9
|
-
"version": "5.
|
|
9
|
+
"version": "5.3.1",
|
|
10
10
|
"os": [
|
|
11
11
|
"darwin",
|
|
12
12
|
"linux"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.3.1",
|
|
4
4
|
"description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. Hybrid FTS5 + TF-IDF search with episode batching. Single SQLite DB, no external services. A lighter, lower-cost alternative to claude-mem (episode batching + a smaller model; cost savings are an internal estimate, not a measured benchmark).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "npm@10.9.2",
|
|
@@ -124,12 +124,23 @@ if (!entryArg) {
|
|
|
124
124
|
|
|
125
125
|
const entryAbs = entryArg.startsWith('/') ? entryArg : join(INSTALL_DIR, entryArg);
|
|
126
126
|
|
|
127
|
-
// Swap barrier. An auto-update
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
//
|
|
127
|
+
// Swap barrier. An auto-update renames files into the install dir one at a time —
|
|
128
|
+
// atomic per file, not per file SET — so a hook process that starts mid-swap can
|
|
129
|
+
// resolve its entry from the old version and an import from the new one.
|
|
130
|
+
// hook-update.mjs marks that window; skip the fire instead of importing a mixed
|
|
131
|
+
// module graph. Hooks are best-effort and the swap lasts ~a second, so the next
|
|
132
|
+
// fire runs against a settled install.
|
|
133
|
+
//
|
|
134
|
+
// This covers the AUTO-UPDATE path only, and the distinction is not pedantic:
|
|
135
|
+
// `hook-update.mjs:719` is the sole writer of this marker in the whole tree (name set,
|
|
136
|
+
// 2026-09-07), so the barrier is never armed for `install.mjs install` — which copies the
|
|
137
|
+
// same file set in place with copyFileSync (`install.mjs:341-350`) and is what
|
|
138
|
+
// `install.mjs repair` ends up executing (`:2388`, after verifying the release). A repair
|
|
139
|
+
// spawned in the background at SessionStart therefore overwrites this tree while hooks
|
|
140
|
+
// keep firing into it. That is R10 P2-12, still open: the mechanism is settled, the runtime
|
|
141
|
+
// symptom is not reproduced, and R10 §8 asks for a repro in tests/sandbox/phaseB-npm.mjs
|
|
142
|
+
// before install()'s main path is touched. An earlier draft of this comment said "auto-update
|
|
143
|
+
// / repair", which reads as though the repair path were already covered — it is not.
|
|
133
144
|
//
|
|
134
145
|
// Stale-guarded on BOTH pid and ts: an updater killed mid-swap leaves the marker
|
|
135
146
|
// behind, and a marker that outlives its writer must never mute hooks permanently.
|
package/server.mjs
CHANGED
|
@@ -10,7 +10,13 @@ import { resolveProject as _resolveProjectShared } from './project-utils.mjs';
|
|
|
10
10
|
import { ensureDbWithWalRecovery, DB_PATH, DB_DIR } from './schema.mjs';
|
|
11
11
|
import { reRankWithContext, runIdleCleanup, buildServerInstructions } from './search-scoring.mjs';
|
|
12
12
|
import { searchObservationsHybrid } from './search-engine.mjs';
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
deepSearch,
|
|
15
|
+
resolveDeepMode,
|
|
16
|
+
shouldEscalateToDeep,
|
|
17
|
+
autoDeepLlmReady,
|
|
18
|
+
deepDisclosureNote,
|
|
19
|
+
} from './deep-search.mjs';
|
|
14
20
|
import { selectCompressionCandidates, groupByProjectWeek, compressGroup } from './lib/compress-core.mjs';
|
|
15
21
|
import {
|
|
16
22
|
resolveAnchorToken,
|
|
@@ -577,6 +583,18 @@ async function runSearchPipeline(db, args, { llm, rerankLlm } = {}) {
|
|
|
577
583
|
if (r.reranked && output.content?.[0]?.type === 'text') {
|
|
578
584
|
output.content[0].text += '\n\n[deep search: LLM-reranked the top candidates by relevance]';
|
|
579
585
|
}
|
|
586
|
+
// D#3. The stderr line above is for humans and logs; an MCP client reads the tool RESULT,
|
|
587
|
+
// so the escalation fact and the adjacency caveat have to land in the payload or the one
|
|
588
|
+
// caller running deep=auto by default never sees either.
|
|
589
|
+
if (r.isDeep && output.content?.[0]?.type === 'text') {
|
|
590
|
+
const disclosure = deepDisclosureNote({
|
|
591
|
+
escalated: r.escalated,
|
|
592
|
+
escalatedObsCount: r.escalatedObsCount,
|
|
593
|
+
variantCount: r.variants?.length ?? 0,
|
|
594
|
+
rowCount: r.page?.length ?? 0,
|
|
595
|
+
});
|
|
596
|
+
if (disclosure) output.content[0].text += `\n\n${disclosure}`;
|
|
597
|
+
}
|
|
580
598
|
appendDeferredTrailer(output);
|
|
581
599
|
|
|
582
600
|
// Expose structured fields for tests + the MCP content blob.
|