recallnest 3.0.0 → 4.0.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.
- package/CHANGELOG.md +304 -0
- package/README.md +251 -258
- package/README_CN.md +226 -247
- package/assets/ui/app.js +6 -1
- package/bin/recallnest.mjs +20 -37
- package/docs/api-reference.md +8 -0
- package/docs/architecture.md +18 -13
- package/eval/cases-canary.json +107 -9
- package/integrations/examples/claude-agent-sdk/memory-agent.ts +2 -0
- package/integrations/examples/langchain/memory-chain.py +4 -0
- package/integrations/examples/openai-agents-sdk/memory-agent.py +4 -0
- package/package.json +12 -3
- package/src/activity-counter.ts +1 -0
- package/src/adaptive-retrieval.ts +23 -0
- package/src/api-server.ts +13 -11
- package/src/asset-sync.ts +26 -2
- package/src/audit-log.ts +2 -0
- package/src/auto-gc.ts +4 -3
- package/src/belief-history.ts +137 -31
- package/src/capture-engine.ts +459 -71
- package/src/cascade-forget.ts +7 -3
- package/src/checkpoint-mirror.ts +417 -0
- package/src/cli.ts +574 -54
- package/src/conflict-engine.ts +43 -15
- package/src/consolidation-engine.ts +24 -3
- package/src/context-collapse-renderer.ts +8 -1
- package/src/context-composer-stable.ts +5 -1
- package/src/context-composer.ts +11 -2
- package/src/distill-lock.ts +246 -74
- package/src/dream-pipeline.ts +3 -1
- package/src/embedder.ts +24 -0
- package/src/env-config.ts +136 -0
- package/src/flock.ts +97 -0
- package/src/forget-engine.ts +342 -58
- package/src/gateway-server.ts +4 -0
- package/src/ingest-plan.ts +56 -0
- package/src/ingest.ts +294 -121
- package/src/kg-contradiction.ts +753 -0
- package/src/kg-extractor.ts +17 -0
- package/src/kg-store.ts +1 -1
- package/src/known-item-eval.ts +455 -0
- package/src/llm-client.ts +13 -26
- package/src/llm-consolidation.ts +2 -2
- package/src/mcp-tool-deps.ts +6 -0
- package/src/mcp-tools-advanced.ts +80 -45
- package/src/mcp-tools-core.ts +23 -17
- package/src/mcp-tools-governance.ts +13 -7
- package/src/memory-assets.ts +144 -8
- package/src/memory-boundaries.ts +19 -0
- package/src/memory-evolution.ts +9 -0
- package/src/memory-lint.ts +1 -1
- package/src/memory-output.ts +311 -14
- package/src/memory-reconcile.ts +1437 -0
- package/src/memory-schema.ts +11 -0
- package/src/memory-store-port.ts +1 -0
- package/src/minis-archive.ts +151 -0
- package/src/noise-filter.ts +15 -2
- package/src/pii-detector.ts +31 -1
- package/src/pivot-apply.ts +55 -5
- package/src/pivot-evidence.ts +128 -0
- package/src/ppr-traversal.ts +25 -1
- package/src/preference-matcher.ts +40 -28
- package/src/prospective-memory.ts +7 -4
- package/src/retriever.ts +297 -57
- package/src/retroactive-boost.ts +3 -2
- package/src/runtime-config.ts +15 -1
- package/src/scope-policy.ts +1 -0
- package/src/session-distiller.ts +6 -2
- package/src/session-engine.ts +11 -4
- package/src/session-output.ts +20 -1
- package/src/session-schema.ts +5 -2
- package/src/session-store.ts +59 -12
- package/src/skill-promotion.ts +67 -1
- package/src/store-receipts.ts +109 -0
- package/src/store-write-budget.ts +37 -0
- package/src/store.ts +298 -57
- package/src/text-fingerprint.ts +25 -0
- package/src/trigger-store.ts +443 -0
- package/src/ui-server.ts +22 -21
- package/src/version-manager.ts +19 -5
- package/src/write-verifier.ts +16 -11
- package/src/__tests__/access-tracker-novelty.test.ts +0 -324
- package/src/__tests__/activity-counter.test.ts +0 -125
- package/src/__tests__/adaptive-fidelity.test.ts +0 -102
- package/src/__tests__/admission-control.test.ts +0 -206
- package/src/__tests__/age-label.test.ts +0 -33
- package/src/__tests__/anchor-generator.test.ts +0 -67
- package/src/__tests__/api-server-security.test.ts +0 -102
- package/src/__tests__/auto-consolidation.test.ts +0 -168
- package/src/__tests__/auto-recall.test.ts +0 -131
- package/src/__tests__/babel-integration.test.ts +0 -144
- package/src/__tests__/batch-dedup.test.ts +0 -65
- package/src/__tests__/belief-history.test.ts +0 -220
- package/src/__tests__/canary-eval.test.ts +0 -114
- package/src/__tests__/capture-engine.test.ts +0 -1328
- package/src/__tests__/capture-heuristic.test.ts +0 -171
- package/src/__tests__/cascade-forget.test.ts +0 -210
- package/src/__tests__/category-min-scores.test.ts +0 -116
- package/src/__tests__/cc8-cc1-essential-context.test.ts +0 -298
- package/src/__tests__/circuit-breaker.test.ts +0 -99
- package/src/__tests__/cluster-consolidation.test.ts +0 -1236
- package/src/__tests__/confidence-metadata.test.ts +0 -125
- package/src/__tests__/confidence-tracker.test.ts +0 -261
- package/src/__tests__/conflict-advisor.test.ts +0 -206
- package/src/__tests__/conflict-escalation.test.ts +0 -149
- package/src/__tests__/conflict-lifecycle.test.ts +0 -83
- package/src/__tests__/conflict-output.test.ts +0 -85
- package/src/__tests__/conflict-store.test.ts +0 -418
- package/src/__tests__/connector-v1.test.ts +0 -181
- package/src/__tests__/consolidation-engine.test.ts +0 -520
- package/src/__tests__/constructive-retrieval.test.ts +0 -509
- package/src/__tests__/context-collapse-renderer.test.ts +0 -214
- package/src/__tests__/context-composer-pins.test.ts +0 -64
- package/src/__tests__/context-composer-task-selection.test.ts +0 -24
- package/src/__tests__/context-reconstructor.test.ts +0 -65
- package/src/__tests__/context-renderer.test.ts +0 -91
- package/src/__tests__/continuity-eval.test.ts +0 -275
- package/src/__tests__/continuity-schema.test.ts +0 -97
- package/src/__tests__/contracts.test.ts +0 -262
- package/src/__tests__/conversation-importer.test.ts +0 -692
- package/src/__tests__/core-recall-eval.test.ts +0 -235
- package/src/__tests__/core-summary.test.ts +0 -109
- package/src/__tests__/data-checkup.test.ts +0 -304
- package/src/__tests__/decay-exemption.test.ts +0 -129
- package/src/__tests__/dedup-actions.test.ts +0 -58
- package/src/__tests__/dedup-guard.test.ts +0 -39
- package/src/__tests__/deterministic-id.test.ts +0 -39
- package/src/__tests__/distill-lock.test.ts +0 -305
- package/src/__tests__/distill-session-argshape.test.ts +0 -79
- package/src/__tests__/doctor.test.ts +0 -319
- package/src/__tests__/dream-pipeline.test.ts +0 -820
- package/src/__tests__/dream-scope-isolation.test.ts +0 -386
- package/src/__tests__/dream-write-batching.test.ts +0 -211
- package/src/__tests__/drill-down.test.ts +0 -108
- package/src/__tests__/embedder.test.ts +0 -122
- package/src/__tests__/emotion-decay.test.ts +0 -46
- package/src/__tests__/emotion-detector.test.ts +0 -56
- package/src/__tests__/emotion-phase1-integration.test.ts +0 -153
- package/src/__tests__/emotion-retrieval.test.ts +0 -56
- package/src/__tests__/entity-resolver.test.ts +0 -58
- package/src/__tests__/env-config-invariant.test.ts +0 -80
- package/src/__tests__/env-config.test.ts +0 -211
- package/src/__tests__/error-signature-boost.test.ts +0 -87
- package/src/__tests__/error-signature.test.ts +0 -95
- package/src/__tests__/error-taxonomy.test.ts +0 -129
- package/src/__tests__/ethics-layer.test.ts +0 -476
- package/src/__tests__/event-segmenter.test.ts +0 -153
- package/src/__tests__/feature-flags-integration.test.ts +0 -54
- package/src/__tests__/frequency-tracker.test.ts +0 -113
- package/src/__tests__/freshness.test.ts +0 -186
- package/src/__tests__/graph-export.test.ts +0 -489
- package/src/__tests__/hotness-blend.test.ts +0 -283
- package/src/__tests__/ingest-chunking.test.ts +0 -44
- package/src/__tests__/ingest-scope-prefix.test.ts +0 -69
- package/src/__tests__/ingest.test.ts +0 -360
- package/src/__tests__/interference-detector.test.ts +0 -203
- package/src/__tests__/kg-extractor.test.ts +0 -192
- package/src/__tests__/kg-store.test.ts +0 -300
- package/src/__tests__/llm-client-pivot.test.ts +0 -628
- package/src/__tests__/llm-consolidation.test.ts +0 -226
- package/src/__tests__/maintenance-locks.test.ts +0 -197
- package/src/__tests__/mcp-efficiency.test.ts +0 -288
- package/src/__tests__/mcp-related-scopes.test.ts +0 -151
- package/src/__tests__/memory-boundaries.test.ts +0 -282
- package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +0 -362
- package/src/__tests__/memory-evolution.test.ts +0 -271
- package/src/__tests__/memory-health-rebalance.test.ts +0 -144
- package/src/__tests__/memory-lint.test.ts +0 -421
- package/src/__tests__/memory-output.test.ts +0 -224
- package/src/__tests__/memory-promotion.test.ts +0 -315
- package/src/__tests__/memory-utility.test.ts +0 -256
- package/src/__tests__/migrate-dedup-ids.test.ts +0 -65
- package/src/__tests__/multi-vector.test.ts +0 -242
- package/src/__tests__/narrative-architecture.test.ts +0 -456
- package/src/__tests__/noise-filter.test.ts +0 -205
- package/src/__tests__/noise-prototype-bank.test.ts +0 -280
- package/src/__tests__/obsidian-connector.test.ts +0 -363
- package/src/__tests__/omitted-reporting.test.ts +0 -98
- package/src/__tests__/openai-contract.test.ts +0 -513
- package/src/__tests__/pending-review.test.ts +0 -77
- package/src/__tests__/pii-redact.test.ts +0 -333
- package/src/__tests__/pivot-apply.test.ts +0 -373
- package/src/__tests__/pivot-distill-supervisor.test.ts +0 -457
- package/src/__tests__/pivot-distill.test.ts +0 -1178
- package/src/__tests__/post-retrieval-filter.test.ts +0 -178
- package/src/__tests__/ppr-traversal.test.ts +0 -256
- package/src/__tests__/prediction-engine.test.ts +0 -349
- package/src/__tests__/predictive-prospective-memory.test.ts +0 -416
- package/src/__tests__/preference-guard-smoke.test.ts +0 -211
- package/src/__tests__/preference-matcher.test.ts +0 -282
- package/src/__tests__/preference-slots.test.ts +0 -205
- package/src/__tests__/promotion-distinct-sources.test.ts +0 -162
- package/src/__tests__/prospective-memory.test.ts +0 -283
- package/src/__tests__/pull-from-macbook-sync-only.test.ts +0 -679
- package/src/__tests__/query-entity-detector.test.ts +0 -171
- package/src/__tests__/query-expander-alias.test.ts +0 -146
- package/src/__tests__/query-expander.test.ts +0 -34
- package/src/__tests__/recall-gate.test.ts +0 -266
- package/src/__tests__/recall-governor.test.ts +0 -227
- package/src/__tests__/recall-mode.test.ts +0 -64
- package/src/__tests__/resolve-db-path.test.ts +0 -130
- package/src/__tests__/resolve-tier.test.ts +0 -64
- package/src/__tests__/result-synthesizer.test.ts +0 -198
- package/src/__tests__/resume-context.test.ts +0 -4898
- package/src/__tests__/retrieval-eval.test.ts +0 -163
- package/src/__tests__/retrieval-trace.test.ts +0 -213
- package/src/__tests__/retrieve-audit-revision.test.ts +0 -255
- package/src/__tests__/retriever-boundary-weight.test.ts +0 -136
- package/src/__tests__/retriever-lazy-init-dispatch.test.ts +0 -123
- package/src/__tests__/retroactive-boost.test.ts +0 -223
- package/src/__tests__/salience-scoring.test.ts +0 -106
- package/src/__tests__/scope-inventory.test.ts +0 -200
- package/src/__tests__/scope-policy.test.ts +0 -69
- package/src/__tests__/scope-suggester.test.ts +0 -80
- package/src/__tests__/session-checkpoint.test.ts +0 -291
- package/src/__tests__/session-distiller.test.ts +0 -640
- package/src/__tests__/session-gc.test.ts +0 -215
- package/src/__tests__/skill-engine.test.ts +0 -552
- package/src/__tests__/skill-promotion.test.ts +0 -654
- package/src/__tests__/skill-verifier.test.ts +0 -122
- package/src/__tests__/source-heartbeat.test.ts +0 -254
- package/src/__tests__/stable-context-length-contract.test.ts +0 -62
- package/src/__tests__/store-patch-metadata.test.ts +0 -235
- package/src/__tests__/store-read-consistency.test.ts +0 -122
- package/src/__tests__/store-schema-migration.test.ts +0 -87
- package/src/__tests__/store-scope-audit.test.ts +0 -81
- package/src/__tests__/store-upsert.test.ts +0 -201
- package/src/__tests__/supersede-chain.test.ts +0 -154
- package/src/__tests__/synthesis-contract.test.ts +0 -252
- package/src/__tests__/synthesis-promotion.test.ts +0 -369
- package/src/__tests__/temporal-parser.test.ts +0 -241
- package/src/__tests__/temporal-validity.test.ts +0 -160
- package/src/__tests__/term-registry-terminal-roster.test.ts +0 -127
- package/src/__tests__/term-registry.test.ts +0 -121
- package/src/__tests__/tool-error-surfacing.test.ts +0 -98
- package/src/__tests__/tool-output-compressor.test.ts +0 -160
- package/src/__tests__/topic-tag.test.ts +0 -113
- package/src/__tests__/trusted-memory.test.ts +0 -867
- package/src/__tests__/ui-server-security.test.ts +0 -93
- package/src/__tests__/usage-tracker.test.ts +0 -287
- package/src/__tests__/utility-weight.test.ts +0 -57
- package/src/__tests__/verbatim-self-recall.test.ts +0 -279
- package/src/__tests__/version-manager.test.ts +0 -248
- package/src/__tests__/workflow-observation-skill-outcome.test.ts +0 -352
- package/src/__tests__/workflow-observation.test.ts +0 -433
- package/src/__tests__/write-verifier.test.ts +0 -122
package/src/memory-lint.ts
CHANGED
|
@@ -185,7 +185,7 @@ function groupEntries(entries: MemoryEntry[]): Map<string, MemoryEntry[]> {
|
|
|
185
185
|
// Check 1: Contradictions
|
|
186
186
|
// ---------------------------------------------------------------------------
|
|
187
187
|
|
|
188
|
-
function findContradictions(entries: MemoryEntry[]): LintFinding[] {
|
|
188
|
+
export function findContradictions(entries: MemoryEntry[]): LintFinding[] {
|
|
189
189
|
const findings: LintFinding[] = [];
|
|
190
190
|
|
|
191
191
|
// Only check merge-type categories where contradictions are meaningful
|
package/src/memory-output.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { parseNarrative } from "./narrative-schema.js";
|
|
|
8
8
|
import { getConfidence, getConfidenceMetadata } from "./confidence-tracker.js";
|
|
9
9
|
import { estimateTokens } from "./context-collapse-renderer.js";
|
|
10
10
|
import { evaluateEntryFreshness, createFreshnessCache } from "./freshness.js";
|
|
11
|
+
import { fullTextScoreThreshold, searchFirstScreen } from "./env-config.js";
|
|
11
12
|
|
|
12
13
|
interface MemoryMetadata {
|
|
13
14
|
source?: string;
|
|
@@ -84,6 +85,80 @@ function getTierLabel(result: RetrievalResult): string {
|
|
|
84
85
|
return String(meta.tier || "peripheral");
|
|
85
86
|
}
|
|
86
87
|
|
|
88
|
+
/**
|
|
89
|
+
* 所在 session 里有用户贴图时,多给一行提示。
|
|
90
|
+
*
|
|
91
|
+
* 图本体不在库里,正文里通常也没有对图的描述——这一行是唯一能让读到这条
|
|
92
|
+
* 记忆的人意识到「这附近还有图没看」的信号。判断值不值得看,回 Deja 按
|
|
93
|
+
* session 读原文。
|
|
94
|
+
*
|
|
95
|
+
* ⚠️ 这是 **session 级**标记,不是「本条记忆有图」:同一个 session 切出来
|
|
96
|
+
* 的每条记忆都带同一个数,图未必跟眼前这条有关。粒度换覆盖,明知而为——
|
|
97
|
+
* 图所在的那一轮常因正文太短根本没进库,只有盖到同 session 的其他轮次上
|
|
98
|
+
* 才可能被搜到。
|
|
99
|
+
*
|
|
100
|
+
* 无图时返回 null,调用方据此不输出这一行——不给无图记忆添噪音。
|
|
101
|
+
*/
|
|
102
|
+
function formatSessionImages(result: RetrievalResult): string | null {
|
|
103
|
+
const meta = parseMetadata(result.entry);
|
|
104
|
+
const own = typeof meta.sessionImages === "number" ? meta.sessionImages : 0;
|
|
105
|
+
const tool = typeof meta.sessionToolImages === "number" ? meta.sessionToolImages : 0;
|
|
106
|
+
if (own <= 0 && tool <= 0) return null;
|
|
107
|
+
|
|
108
|
+
// 两类分开报,因为它们答的是不同的问题:找「我发过的那张截图」看前者,
|
|
109
|
+
// 回溯「我当时生成的图 / 那个页面当时的样子」看后者。
|
|
110
|
+
// 措辞用英文,与同一块里的 prov / fresh 行保持一致(这个输出面是英文的)。
|
|
111
|
+
const parts: string[] = [];
|
|
112
|
+
if (own > 0) parts.push(`${own} user-pasted`);
|
|
113
|
+
if (tool > 0) parts.push(`${tool} agent-made`);
|
|
114
|
+
const sess = typeof meta.sessionId === "string" ? meta.sessionId.slice(0, 8) : "?";
|
|
115
|
+
|
|
116
|
+
return `${parts.join(", ")} in this session · read sess=${sess}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
interface DistilledSource {
|
|
120
|
+
/** 原会话日期(`date:` 标签),不是写入日——批量回填的行两者能差出半年。 */
|
|
121
|
+
date: string | null;
|
|
122
|
+
/** 写入时存在行上的原文 anchor;可能是当时助手说的,不一定是用户的话。 */
|
|
123
|
+
text: string;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 批量提炼行(pivot-apply 从历史会话回填,2026-08-04/05 共 1,516 条):表里那句是转述,不是原话。
|
|
128
|
+
*
|
|
129
|
+
* 这批提炼句没逐条核对过——2026-09-11 抽 20 条对照各自存的原文,8 条有实质走样(丢限定词、
|
|
130
|
+
* 一次性要求记成长期规则、把当时助手的话记成用户偏好……)。原文 anchor 本来就存在行上,
|
|
131
|
+
* 读取时亮出来,比事后清洗存量便宜,也能罩住以后同一条路写进来的行。
|
|
132
|
+
*
|
|
133
|
+
* 只认 pivot-apply 标签:当场记录(manual / agent)的 anchor 语义不同,不在这里出。
|
|
134
|
+
* 不是批量行时返回 null,调用方据此什么都不输出——不给别的记忆添噪音。
|
|
135
|
+
*/
|
|
136
|
+
function getDistilledSource(result: RetrievalResult): DistilledSource | null {
|
|
137
|
+
const meta = parseMetadata(result.entry);
|
|
138
|
+
const tags = Array.isArray(meta.tags) ? meta.tags.map(String) : [];
|
|
139
|
+
if (!tags.includes("pivot-apply")) return null;
|
|
140
|
+
const dateTag = tags.find((tag) => tag.startsWith("date:"));
|
|
141
|
+
return {
|
|
142
|
+
date: dateTag ? dateTag.slice("date:".length) : null,
|
|
143
|
+
text: typeof meta.anchor === "string" ? meta.anchor : "",
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const DISTILLED_ORIGIN_MAX_LEN = 160;
|
|
148
|
+
|
|
149
|
+
// 措辞用英文,与 prov / imgs / fresh 行保持一致(这个输出面是英文的)。
|
|
150
|
+
const DISTILLED_NOTE =
|
|
151
|
+
"rows with an orig line were batch-distilled from old sessions: the snippet is a paraphrase, " +
|
|
152
|
+
"orig is the source text (it may be the assistant's words, not the user's) — trust orig";
|
|
153
|
+
|
|
154
|
+
function formatDistilledOrigin(source: DistilledSource): string {
|
|
155
|
+
const when = source.date ? `(${source.date} session) ` : "";
|
|
156
|
+
const text = source.text
|
|
157
|
+
? cleanSnippet(source.text, DISTILLED_ORIGIN_MAX_LEN)
|
|
158
|
+
: "(source text missing)";
|
|
159
|
+
return `${when}${text}`;
|
|
160
|
+
}
|
|
161
|
+
|
|
87
162
|
function getProvenanceSummary(result: RetrievalResult): string {
|
|
88
163
|
const provenance = extractMemoryProvenance({
|
|
89
164
|
scope: result.entry.scope,
|
|
@@ -174,6 +249,7 @@ function getRetrievalPath(result: RetrievalResult): string {
|
|
|
174
249
|
if (result.sources.vector) parts.push("vector");
|
|
175
250
|
if (result.sources.bm25) parts.push("bm25");
|
|
176
251
|
if (result.sources.reranked) parts.push("reranked");
|
|
252
|
+
if (result.sources.trigger) parts.push("trigger");
|
|
177
253
|
if (result.sources.narrativeSibling) parts.push("narrative");
|
|
178
254
|
return parts.join("+") || "direct";
|
|
179
255
|
}
|
|
@@ -225,8 +301,12 @@ function pickBestSnippet(query: string, text: string): string {
|
|
|
225
301
|
* adaptive 专用 query-aware snippet:取匹配词周围窗口(而非整句/开头),保证匹配证据可见。
|
|
226
302
|
* 解决 pickBestSnippet 的两个退化(Codex 审点4 C 版 P2):① 短缩写 query(如 "CI")
|
|
227
303
|
* extractTerms 提取不到词 → 退回开头;② 匹配在超长句/log 行后段 → cleanSnippet 从头截断丢匹配。
|
|
228
|
-
*
|
|
304
|
+
* adaptive 档与默认第一屏(`formatFullTextResults` 里超预算降级的那条路)共用;旧表格(legacy)与 explain
|
|
305
|
+
* 走的是 pickBestSnippet,不经这里。
|
|
229
306
|
*/
|
|
307
|
+
const ADAPTIVE_SNIPPET_BEFORE = 60;
|
|
308
|
+
const ADAPTIVE_SNIPPET_AFTER = 180;
|
|
309
|
+
|
|
230
310
|
function adaptiveSnippet(query: string, text: string): string {
|
|
231
311
|
const clean = text.replace(/\s+/g, " ").trim();
|
|
232
312
|
const lower = clean.toLowerCase();
|
|
@@ -254,10 +334,8 @@ function adaptiveSnippet(query: string, text: string): string {
|
|
|
254
334
|
}
|
|
255
335
|
if (matchIdx < 0) return pickBestSnippet(query, text); // 完全无匹配 → 退回整句最佳
|
|
256
336
|
|
|
257
|
-
const
|
|
258
|
-
const
|
|
259
|
-
const start = Math.max(0, matchIdx - WINDOW_BEFORE);
|
|
260
|
-
const end = Math.min(clean.length, matchIdx + WINDOW_AFTER);
|
|
337
|
+
const start = Math.max(0, matchIdx - ADAPTIVE_SNIPPET_BEFORE);
|
|
338
|
+
const end = Math.min(clean.length, matchIdx + ADAPTIVE_SNIPPET_AFTER);
|
|
261
339
|
let snip = clean.slice(start, end).trim();
|
|
262
340
|
if (start > 0) snip = "…" + snip;
|
|
263
341
|
if (end < clean.length) snip = snip + "…";
|
|
@@ -305,6 +383,12 @@ function buildWhyMatched(query: string, result: RetrievalResult): string {
|
|
|
305
383
|
reasons.push("reranked");
|
|
306
384
|
}
|
|
307
385
|
|
|
386
|
+
if (result.sources.trigger) {
|
|
387
|
+
// T-Mem:说清是哪句预演问法把它捞出来的(只在 explain 露面,不进 context)
|
|
388
|
+
const t = result.sources.trigger;
|
|
389
|
+
reasons.push(`trigger「${t.text}」cos=${t.cosine.toFixed(2)}${t.admittedBy === "soft" ? ` overlap=${(t.overlap ?? 0).toFixed(2)}` : ""}`);
|
|
390
|
+
}
|
|
391
|
+
|
|
308
392
|
if (matchedTerms.length > 0) {
|
|
309
393
|
reasons.push(`terms:${matchedTerms.slice(0, 3).join(",")}`);
|
|
310
394
|
}
|
|
@@ -353,14 +437,25 @@ export function formatBriefResults(
|
|
|
353
437
|
): string {
|
|
354
438
|
if (results.length === 0) return "No results found.";
|
|
355
439
|
const freshnessCache = createFreshnessCache();
|
|
356
|
-
|
|
440
|
+
// brief 只标不附原文:它就是为省 token 选的档,原文会让它长一倍。
|
|
441
|
+
const distilled = results.map(getDistilledSource);
|
|
442
|
+
const lines = [
|
|
443
|
+
`Query: ${context.query}`,
|
|
444
|
+
`Hits: ${results.length}`,
|
|
445
|
+
...(distilled.some(Boolean)
|
|
446
|
+
? ["Note: [distilled] rows paraphrase old sessions — use detail_level=normal to see the source text"]
|
|
447
|
+
: []),
|
|
448
|
+
"",
|
|
449
|
+
];
|
|
357
450
|
for (let i = 0; i < results.length; i++) {
|
|
358
451
|
const r = results[i];
|
|
359
452
|
const excerpt = extractBriefExcerpt(r);
|
|
453
|
+
const orig = distilled[i] ?? null;
|
|
454
|
+
const distilledTag = orig ? ` [distilled${orig.date ? `, ${orig.date} session` : ""}]` : "";
|
|
360
455
|
// Freshness suffix: only for memories that declared dependsOn (opt-in, cheap check).
|
|
361
456
|
const fresh = evaluateEntryFreshness(r.entry.metadata, freshnessCache);
|
|
362
457
|
const freshTag = fresh ? ` [fresh:${fresh}]` : "";
|
|
363
|
-
lines.push(`#${i + 1} ${r.entry.id.slice(0, 8)} ${(r.score * 100).toFixed(1)}% — ${excerpt}${freshTag}`);
|
|
458
|
+
lines.push(`#${i + 1} ${r.entry.id.slice(0, 8)} ${(r.score * 100).toFixed(1)}% — ${excerpt}${distilledTag}${freshTag}`);
|
|
364
459
|
}
|
|
365
460
|
return lines.join("\n");
|
|
366
461
|
}
|
|
@@ -372,10 +467,12 @@ export function formatFullResults(
|
|
|
372
467
|
if (results.length === 0) return "No results found.";
|
|
373
468
|
|
|
374
469
|
const freshnessCache = createFreshnessCache();
|
|
470
|
+
const distilled = results.map(getDistilledSource);
|
|
375
471
|
const lines = [
|
|
376
472
|
`Query : ${context.query}`,
|
|
377
473
|
`Profile : ${context.profile}`,
|
|
378
474
|
`Hits : ${results.length}`,
|
|
475
|
+
...(distilled.some(Boolean) ? [`Note : ${DISTILLED_NOTE}`] : []),
|
|
379
476
|
"",
|
|
380
477
|
"# ID Score Category Tier Source Date Age Retrieval Path File / Snippet",
|
|
381
478
|
"-- -------- ----- ------------ ---------- ------- ---------- ----- -------------------- --------------",
|
|
@@ -385,6 +482,10 @@ export function formatFullResults(
|
|
|
385
482
|
const row = buildSearchRow(i, context.query, results[i]);
|
|
386
483
|
lines.push(`${row[0]} ${row[1]} ${row[2]} ${row[3]} ${row[4]} ${row[5]} ${row[6]} ${row[7]} ${row[8]} ${row[9]} | ${row[10]}`);
|
|
387
484
|
lines.push(` prov : ${getProvenanceSummary(results[i])}`);
|
|
485
|
+
const imgs = formatSessionImages(results[i]);
|
|
486
|
+
if (imgs) lines.push(` imgs : ${imgs}`);
|
|
487
|
+
const orig = distilled[i] ?? null;
|
|
488
|
+
if (orig) lines.push(` orig : ${formatDistilledOrigin(orig)}`);
|
|
388
489
|
// Full mode: append metadata details
|
|
389
490
|
const meta = parseMetadata(results[i].entry);
|
|
390
491
|
const evolution = typeof meta.evolutionStatus === "string" ? meta.evolutionStatus : "-";
|
|
@@ -415,17 +516,23 @@ export function formatFullResults(
|
|
|
415
516
|
return lines.join("\n");
|
|
416
517
|
}
|
|
417
518
|
|
|
418
|
-
|
|
519
|
+
/**
|
|
520
|
+
* 2026-10-05 之前的默认第一屏:表格 + 每条 120 字片段。函数体一个字没动,只是改了名。
|
|
521
|
+
* 现在只在 `RECALLNEST_SEARCH_FIRST_SCREEN=legacy` 时由 `formatSearchResults` 走到(退回开关)。
|
|
522
|
+
*/
|
|
523
|
+
export function formatLegacyTableResults(
|
|
419
524
|
results: RetrievalResult[],
|
|
420
525
|
context: RenderContext,
|
|
421
526
|
): string {
|
|
422
527
|
if (results.length === 0) return "No results found.";
|
|
423
528
|
|
|
424
529
|
const freshnessCache = createFreshnessCache();
|
|
530
|
+
const distilled = results.map(getDistilledSource);
|
|
425
531
|
const lines = [
|
|
426
532
|
`Query : ${context.query}`,
|
|
427
533
|
`Profile : ${context.profile}`,
|
|
428
534
|
`Hits : ${results.length}`,
|
|
535
|
+
...(distilled.some(Boolean) ? [`Note : ${DISTILLED_NOTE}`] : []),
|
|
429
536
|
"",
|
|
430
537
|
"# ID Score Category Tier Source Date Age Retrieval Path File / Snippet",
|
|
431
538
|
"-- -------- ----- ------------ ---------- ------- ---------- ----- -------------------- --------------",
|
|
@@ -435,6 +542,10 @@ export function formatSearchResults(
|
|
|
435
542
|
const row = buildSearchRow(i, context.query, results[i]);
|
|
436
543
|
lines.push(`${row[0]} ${row[1]} ${row[2]} ${row[3]} ${row[4]} ${row[5]} ${row[6]} ${row[7]} ${row[8]} ${row[9]} | ${row[10]}`);
|
|
437
544
|
lines.push(` prov : ${getProvenanceSummary(results[i])}`);
|
|
545
|
+
const imgs = formatSessionImages(results[i]);
|
|
546
|
+
if (imgs) lines.push(` imgs : ${imgs}`);
|
|
547
|
+
const orig = distilled[i] ?? null;
|
|
548
|
+
if (orig) lines.push(` orig : ${formatDistilledOrigin(orig)}`);
|
|
438
549
|
// Freshness: only shown for memories that declared dependsOn (opt-in, cheap check).
|
|
439
550
|
const fresh = evaluateEntryFreshness(results[i].entry.metadata, freshnessCache);
|
|
440
551
|
if (fresh) lines.push(` fresh: ${fresh}`);
|
|
@@ -444,7 +555,182 @@ export function formatSearchResults(
|
|
|
444
555
|
}
|
|
445
556
|
|
|
446
557
|
const ADAPTIVE_TOKEN_BUDGET = 8000;
|
|
447
|
-
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* 默认第一屏单条正文的封顶(按码点数)。
|
|
561
|
+
*
|
|
562
|
+
* 线是照库里的实际长度定的(2026-10-05 只读快照,138,501 条活跃记忆):手写与提炼条目最长 2,278、
|
|
563
|
+
* 会话切片最长 2,077、记忆文件切片最长 1,523,都在线下,永远给全文;会被截的只有项目文档的大切片
|
|
564
|
+
* (`project:` 7,797 条,中位 2,401、最长 3,990)。封顶防的是一条长文档排第一时把后面几条的预算吃光。
|
|
565
|
+
*/
|
|
566
|
+
const FIRST_SCREEN_ENTRY_CAP = 2400;
|
|
567
|
+
|
|
568
|
+
const DISTILLED_NOTE_FULLTEXT =
|
|
569
|
+
"entries with an orig line are batch-distilled paraphrases of old sessions; " +
|
|
570
|
+
"orig is the source text (it may be the assistant's words, not the user's) — trust orig";
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* 条目行末尾的出处:有会话编号就给 `sess=前 8 位`(imgs 行、Deja 回查用的都是它);文件名只在它不是
|
|
574
|
+
* 会话编号的重复时才给——Claude Code 的会话文件叫 `<会话编号>.jsonl`、Codex 的叫 `rollout-<时间>-<会话编号>.jsonl`,
|
|
575
|
+
* 再写一遍每条白占四五十个字。
|
|
576
|
+
*/
|
|
577
|
+
function getOriginLabel(result: RetrievalResult): string[] {
|
|
578
|
+
const meta = parseMetadata(result.entry);
|
|
579
|
+
const sessionId = typeof meta.sessionId === "string" ? meta.sessionId : "";
|
|
580
|
+
const file = getFileLabel(result);
|
|
581
|
+
const parts: string[] = [];
|
|
582
|
+
if (sessionId) parts.push(`sess=${sessionId.slice(0, 8)}`);
|
|
583
|
+
// 要整段会话编号对上才算重复:只对上前 8 位的可能是另有内容的文件名。
|
|
584
|
+
const repeatsSession = sessionId !== "" && file.endsWith(`${sessionId}.jsonl`);
|
|
585
|
+
if (file !== "-" && !repeatsSession) parts.push(file);
|
|
586
|
+
return parts;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g;
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* adaptiveSnippet / cleanSnippet 按 UTF-16 下标切,窗口边上可能留下半个代理对。
|
|
593
|
+
* 默认第一屏这条路上把落单的那半个去掉;那两个函数本身不动(adaptive、legacy、explain 的输出不变)。
|
|
594
|
+
*/
|
|
595
|
+
function dropLoneSurrogates(text: string): string {
|
|
596
|
+
return text.replace(LONE_SURROGATE, "");
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/** 片段里属于原文的字数:去掉片段函数在两头补的省略号。 */
|
|
600
|
+
function snippetSourceChars(snippet: string): number {
|
|
601
|
+
return Array.from(snippet.replace(/^…/, "").replace(/(…|\.\.\.)$/, "")).length;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/** 按码点取开头一段,不把代理对切成两半。`total` 是整条的码点数。 */
|
|
605
|
+
function headByCodePoints(text: string, max: number): { head: string; total: number; truncated: boolean } {
|
|
606
|
+
const points = Array.from(text);
|
|
607
|
+
if (points.length <= max) return { head: text, total: points.length, truncated: false };
|
|
608
|
+
return { head: points.slice(0, max).join(""), total: points.length, truncated: true };
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* 默认第一屏(2026-10-05 起):按名次给正文全文。
|
|
613
|
+
*
|
|
614
|
+
* 为什么不沿用 120 字片段:同一批 48 条真实查询、同一份命中,三位读者盲判「只看这一屏就够」,
|
|
615
|
+
* 表格 + 120 字是 1 条、全文是 18 条;旧默认屏里正文只占 23%,5 条全文合计的中位字数反而比它短。
|
|
616
|
+
* 为什么不按分数给(adaptive 的做法):分数是融合排序分,只表示这一批里的相对位置,排第一的中位
|
|
617
|
+
* 只有 0.64,43 条非空查询里 31 条一条全文都拿不到。所以这里只看名次,不看分数。
|
|
618
|
+
*
|
|
619
|
+
* 规则:
|
|
620
|
+
* - 按传入顺序(handler 已排好)逐条给正文;单条超过 FIRST_SCREEN_ENTRY_CAP 只给开头那一段。
|
|
621
|
+
* - 给全文的额度是 ADAPTIVE_TOKEN_BUDGET(与 adaptive 同一个数,正文与 orig 行一起算)。放不下的条目降成
|
|
622
|
+
* 匹配词周围的片段,后面更短的条目放得下仍给全文。
|
|
623
|
+
* - 每条命中都列出来,不因预算整条消失——这一点有意不照 adaptive(它超预算会停并报 omitted)。
|
|
624
|
+
* 所以 8000 是「给全文给到哪为止」的线,不是整屏的硬顶:片段、以及整条不比片段窗口(240 字)长的短条目
|
|
625
|
+
* 不受它拦,整屏最多超出「剩下每条一个片段」(limit ≤ 20;20 条都是 2400 个汉字时约 12,000)。
|
|
626
|
+
* 先给所有条目预留片段再分全文的做法试算过:尾部的片段会把排第一的长条目挤成片段,和「按名次给」相反。
|
|
627
|
+
* - search_memory 的 related-scope 附栏各调一次本函数,各有一份额度。
|
|
628
|
+
* - 缩短过的条目各带一行说明:共多少字、给了多少、用哪个编号展开。顶上 Text 行报整屏的全文 / 截断 / 片段条数。
|
|
629
|
+
* - 元数据每条一行。旧表格里的 Tier 与 Retrieval Path 两列不再出,会话文件名换成 `sess=前 8 位`;
|
|
630
|
+
* 这三样 detail_level=full 里还有。
|
|
631
|
+
* - imgs / orig / fresh 三种附加行照旧。
|
|
632
|
+
*/
|
|
633
|
+
export function formatFullTextResults(
|
|
634
|
+
results: RetrievalResult[],
|
|
635
|
+
context: RenderContext,
|
|
636
|
+
): string {
|
|
637
|
+
if (results.length === 0) return "No results found.";
|
|
638
|
+
|
|
639
|
+
const freshnessCache = createFreshnessCache();
|
|
640
|
+
const blocks: string[] = [];
|
|
641
|
+
let tokensUsed = 0;
|
|
642
|
+
let fullCount = 0;
|
|
643
|
+
let cappedCount = 0;
|
|
644
|
+
let snippetCount = 0;
|
|
645
|
+
let anyOrig = false;
|
|
646
|
+
|
|
647
|
+
for (let i = 0; i < results.length; i++) {
|
|
648
|
+
const r = results[i];
|
|
649
|
+
const id = r.entry.id.slice(0, 8);
|
|
650
|
+
// 展开提示给完整编号:8 位前缀在库里有撞的(2026-10-05 快照 138,502 行里 2 组),撞了 memory_drill_down 取不回来。
|
|
651
|
+
const drillHint = `memory_drill_down ${r.entry.id} for the full text`;
|
|
652
|
+
const capped = headByCodePoints(r.entry.text, FIRST_SCREEN_ENTRY_CAP);
|
|
653
|
+
const orig = getDistilledSource(r);
|
|
654
|
+
const origLine = orig ? ` orig : ${dropLoneSurrogates(formatDistilledOrigin(orig))}` : "";
|
|
655
|
+
const origTokens = origLine ? estimateTokens(origLine) : 0;
|
|
656
|
+
|
|
657
|
+
let text = capped.head;
|
|
658
|
+
let cutNote = capped.truncated
|
|
659
|
+
? ` … [truncated: showing ${FIRST_SCREEN_ENTRY_CAP} of ${capped.total} chars · ${drillHint}]`
|
|
660
|
+
: "";
|
|
661
|
+
let tokens = estimateTokens(text) + origTokens;
|
|
662
|
+
// 整条不比一个片段窗口长的条目不降级:降了也省不下字,只会多出一行「给了片段」的说明。
|
|
663
|
+
const worthSnipping = capped.total > ADAPTIVE_SNIPPET_BEFORE + ADAPTIVE_SNIPPET_AFTER;
|
|
664
|
+
if (worthSnipping && tokensUsed + tokens > ADAPTIVE_TOKEN_BUDGET) {
|
|
665
|
+
text = dropLoneSurrogates(adaptiveSnippet(context.query, r.entry.text));
|
|
666
|
+
cutNote = ` [snippet: token budget reached · showing ${snippetSourceChars(text)} of ${capped.total} chars · ${drillHint}]`;
|
|
667
|
+
tokens = estimateTokens(text) + origTokens;
|
|
668
|
+
snippetCount++;
|
|
669
|
+
} else if (capped.truncated) {
|
|
670
|
+
cappedCount++;
|
|
671
|
+
} else {
|
|
672
|
+
fullCount++;
|
|
673
|
+
}
|
|
674
|
+
tokensUsed += tokens;
|
|
675
|
+
|
|
676
|
+
const prov = getProvenanceSummary(r);
|
|
677
|
+
const header = [
|
|
678
|
+
id,
|
|
679
|
+
`${(r.score * 100).toFixed(1)}%`,
|
|
680
|
+
`${getDateLabel(r.entry.timestamp)} (${formatAgeLabel(r.entry.timestamp)})`,
|
|
681
|
+
getCategoryLabel(r),
|
|
682
|
+
getSourceLabel(r),
|
|
683
|
+
...(prov !== "-" ? [prov] : []),
|
|
684
|
+
...getOriginLabel(r),
|
|
685
|
+
].join(" · ");
|
|
686
|
+
blocks.push(`[${i + 1}] ${header}`);
|
|
687
|
+
// fresh 管的是「这条还能不能照着用」,放在正文前面;只有声明过 dependsOn 的记忆才有(opt-in)。
|
|
688
|
+
const fresh = evaluateEntryFreshness(r.entry.metadata, freshnessCache);
|
|
689
|
+
if (fresh) blocks.push(` fresh: ${fresh}`);
|
|
690
|
+
blocks.push(text);
|
|
691
|
+
if (cutNote) blocks.push(cutNote);
|
|
692
|
+
if (origLine) {
|
|
693
|
+
blocks.push(origLine);
|
|
694
|
+
anyOrig = true;
|
|
695
|
+
}
|
|
696
|
+
const imgs = formatSessionImages(r);
|
|
697
|
+
if (imgs) blocks.push(` imgs : ${imgs}`);
|
|
698
|
+
blocks.push("");
|
|
699
|
+
}
|
|
700
|
+
// 去掉自己加的最后一个块间空行;不对整份输出 trimEnd,否则最后一条正文自己的结尾空白也会被吃掉。
|
|
701
|
+
blocks.pop();
|
|
702
|
+
|
|
703
|
+
// 全部给全的时候这一行只报条数;有缩短的才把封顶与预算写出来。
|
|
704
|
+
const shortened = cappedCount + snippetCount > 0;
|
|
705
|
+
const breakdown = [
|
|
706
|
+
`${fullCount} in full`,
|
|
707
|
+
...(cappedCount > 0 ? [`${cappedCount} capped (${FIRST_SCREEN_ENTRY_CAP}-char cap)`] : []),
|
|
708
|
+
...(snippetCount > 0 ? [`${snippetCount} snippet (${ADAPTIVE_TOKEN_BUDGET}-token budget)`] : []),
|
|
709
|
+
].join(", ") + (shortened ? " — shortened entries say how much is shown" : "");
|
|
710
|
+
const lines = [
|
|
711
|
+
`Query : ${context.query}`,
|
|
712
|
+
`Profile : ${context.profile}`,
|
|
713
|
+
`Hits : ${results.length}`,
|
|
714
|
+
`Text : ${breakdown}`,
|
|
715
|
+
...(anyOrig ? [`Note : ${DISTILLED_NOTE_FULLTEXT}`] : []),
|
|
716
|
+
"",
|
|
717
|
+
...blocks,
|
|
718
|
+
];
|
|
719
|
+
return lines.join("\n");
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* search_memory 默认档(detail_level=normal)、命令行 `search`、本地 UI 共用的入口。
|
|
724
|
+
* 默认出全文那一版;`RECALLNEST_SEARCH_FIRST_SCREEN=legacy` 退回表格 + 120 字片段。
|
|
725
|
+
*/
|
|
726
|
+
export function formatSearchResults(
|
|
727
|
+
results: RetrievalResult[],
|
|
728
|
+
context: RenderContext,
|
|
729
|
+
): string {
|
|
730
|
+
return searchFirstScreen() === "legacy"
|
|
731
|
+
? formatLegacyTableResults(results, context)
|
|
732
|
+
: formatFullTextResults(results, context);
|
|
733
|
+
}
|
|
448
734
|
|
|
449
735
|
/**
|
|
450
736
|
* P-fidelity (点4): adaptive 保真度渲染 — 借鉴 RepoPrompt CE「保真度阶梯」的内核
|
|
@@ -454,7 +740,7 @@ const ADAPTIVE_FULL_SCORE = 0.85;
|
|
|
454
740
|
* collapse 的 l0 floor / 按 score 重排 / query-agnostic fallback 都是为 resume 场景定制,
|
|
455
741
|
* 搬到 search 连续撞三个 P2(隐藏过滤 / 覆盖 highlight 排序 / 丢失匹配证据)。search 的语义
|
|
456
742
|
* 不同——结果是"为什么这条命中",必须 query-aware;故第一版自实现两档:
|
|
457
|
-
* - 高相关(score ≥ 0.85)→ 全文
|
|
743
|
+
* - 高相关(score ≥ fullTextScoreThreshold():legacy 0.85,bounded 流行度下 0.80)→ 全文
|
|
458
744
|
* - 其余 → query-aware snippet(pickBestSnippet,显示匹配证据)
|
|
459
745
|
* 按 handler 传入顺序(normal=score / highlight=contextual)依次填入 token 预算,超预算时
|
|
460
746
|
* 全文降级 snippet、snippet 仍超则停止(剩余计入 omitted)。完整三档保真度阶梯(每条记忆预存
|
|
@@ -469,21 +755,27 @@ export function formatCollapsedResults(
|
|
|
469
755
|
const rendered: string[] = [];
|
|
470
756
|
let tokensUsed = 0;
|
|
471
757
|
let shown = 0;
|
|
758
|
+
let anyOrigShown = false;
|
|
759
|
+
const fullScore = fullTextScoreThreshold();
|
|
472
760
|
|
|
473
761
|
// 保留传入顺序(不重排):handler 已按 normal=score / highlight=contextual 排好。
|
|
474
762
|
for (const r of results) {
|
|
475
|
-
const isFull = r.score >=
|
|
763
|
+
const isFull = r.score >= fullScore;
|
|
476
764
|
let level = isFull ? "FULL" : "SNIP";
|
|
477
765
|
// query-aware:snippet 取匹配词周围窗口、显示匹配证据(search 是"为什么命中")。
|
|
478
766
|
let text = isFull ? r.entry.text : adaptiveSnippet(context.query, r.entry.text);
|
|
479
|
-
|
|
767
|
+
// 批量提炼行附原文。它同样占预算,和正文一起算,别让原文把预算悄悄撑破。
|
|
768
|
+
const orig = getDistilledSource(r);
|
|
769
|
+
const origLine = orig ? `orig: ${formatDistilledOrigin(orig)}` : "";
|
|
770
|
+
const origTokens = origLine ? estimateTokens(origLine) : 0;
|
|
771
|
+
let tokens = estimateTokens(text) + origTokens;
|
|
480
772
|
|
|
481
773
|
if (tokensUsed + tokens > ADAPTIVE_TOKEN_BUDGET) {
|
|
482
774
|
if (isFull) {
|
|
483
775
|
// 全文超预算 → 降级为 query-aware snippet
|
|
484
776
|
text = adaptiveSnippet(context.query, r.entry.text);
|
|
485
777
|
level = "SNIP";
|
|
486
|
-
tokens = estimateTokens(text);
|
|
778
|
+
tokens = estimateTokens(text) + origTokens;
|
|
487
779
|
}
|
|
488
780
|
if (tokensUsed + tokens > ADAPTIVE_TOKEN_BUDGET) break; // snippet 仍超 → 停止,剩余计入 omitted
|
|
489
781
|
}
|
|
@@ -495,6 +787,10 @@ export function formatCollapsedResults(
|
|
|
495
787
|
const scorePct = `${(r.score * 100).toFixed(0)}%`;
|
|
496
788
|
rendered.push(`[${level}] ${id} ${scorePct} ${getCategoryLabel(r)} ${getSourceLabel(r)}`);
|
|
497
789
|
rendered.push(text);
|
|
790
|
+
if (origLine) {
|
|
791
|
+
rendered.push(origLine);
|
|
792
|
+
anyOrigShown = true;
|
|
793
|
+
}
|
|
498
794
|
rendered.push("");
|
|
499
795
|
}
|
|
500
796
|
|
|
@@ -502,8 +798,9 @@ export function formatCollapsedResults(
|
|
|
502
798
|
const lines = [
|
|
503
799
|
`Query : ${context.query}`,
|
|
504
800
|
`Profile : ${context.profile}`,
|
|
505
|
-
`Mode : adaptive — full text for score ≥ ${
|
|
801
|
+
`Mode : adaptive — full text for score ≥ ${fullScore}, query-aware snippet otherwise, ${ADAPTIVE_TOKEN_BUDGET}-token budget`,
|
|
506
802
|
`Shown : ${shown} of ${results.length}${omitted > 0 ? ` (${omitted} omitted: token budget)` : ""}`,
|
|
803
|
+
...(anyOrigShown ? [`Note : ${DISTILLED_NOTE}`] : []),
|
|
507
804
|
"",
|
|
508
805
|
...rendered,
|
|
509
806
|
];
|