dsh-session-recall 0.4.1 → 0.5.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/README.md +4 -2
- package/README.zh.md +4 -2
- package/lib/index.d.ts +67 -1
- package/lib/index.js +120 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,8 +27,6 @@ If you need agent memory orchestration, use a memory framework; if you need boun
|
|
|
27
27
|
|
|
28
28
|
## Roadmap
|
|
29
29
|
|
|
30
|
-
- **P1: ranking controls** — configurable recency decay and session pinning on top of FTS relevance.
|
|
31
|
-
- **P1: query diagnostics** — expose match reason (fts/cjk-fallback/filters) and scan budget in result metadata.
|
|
32
30
|
- **P2: evidence handoff** — one-click bridge to session export for matched sessions.
|
|
33
31
|
|
|
34
32
|
## Why
|
|
@@ -120,10 +118,14 @@ Deployment-level controls for what the model may read back:
|
|
|
120
118
|
| `redactionMode` | `off` / `mask` / `hash` | `off` | Redact secret-looking text (bearer headers, prefixed API keys, private-key blocks, emails) in snippets and titles. `hash` keeps secrets comparable (`#xxxxxxxx`, same secret → same marker) without being readable. Results carry a `redacted` count. |
|
|
121
119
|
| `cwdAllowlist` | list of paths | (none) | Only sessions started in these directories are searchable; the calling cwd itself must be listed. |
|
|
122
120
|
| `cwdDenylist` | list of paths | (none) | These directories are never searchable. Deny wins over allow. |
|
|
121
|
+
| `recencyHalfLifeDays` | days (e.g. `30`) | (off) | Re-rank cross-session hits: backend rank × exponential recency decay over the match time. Unset or `<= 0` keeps backend order. Per result page. |
|
|
122
|
+
| `pinnedCwds` | list of paths | (none) | Sessions from these project directories rank first, as a group. |
|
|
123
123
|
| `allProjectsPolicy` | `allow` / `deny` / `confirm` | `allow` | `deny` ignores `all_projects` with a model-facing hint; `confirm` asks the user through the official `@deepseek-ai/dsh-user-approval` seam — fail-closed when no answerer is composed. |
|
|
124
124
|
|
|
125
125
|
All three gates apply uniformly to cross-session hits, the CJK fallback scan, and `session_id` reads — no bypass route.
|
|
126
126
|
|
|
127
|
+
Every result also carries a `diagnostics` object (v0.5): which engine produced the matches (`fts` / `cjk-fallback` / `session-scan`), how many sessions a fallback scan visited against its budget, and whether re-ranking was applied — so callers can tell *why* they got what they got.
|
|
128
|
+
|
|
127
129
|
## Known limitations
|
|
128
130
|
|
|
129
131
|
- First search after startup walks the durable logs to build the index (the tool description warns the model); subsequent searches are incremental.
|
package/README.zh.md
CHANGED
|
@@ -27,8 +27,6 @@ DeepSeek Harness 的**确定性跨会话全文检索**插件:注册模型可
|
|
|
27
27
|
|
|
28
28
|
## 路线图
|
|
29
29
|
|
|
30
|
-
- **P1:排序策略可配** —— 在 FTS 相关度之上增加时间衰减、会话 pin 权重。
|
|
31
|
-
- **P1:查询诊断元数据** —— 返回命中来源(fts/cjk-fallback/filters)与扫描预算。
|
|
32
30
|
- **P2:证据联动导出** —— 命中后可一键触发对应会话导出。
|
|
33
31
|
|
|
34
32
|
## 为什么做这个
|
|
@@ -120,10 +118,14 @@ dsh plugin --profile web add github:kittimzhe/dsh-session-recall
|
|
|
120
118
|
| `redactionMode` | `off` / `mask` / `hash` | `off` | 对标题与摘录中疑似密钥的文本(Bearer 头、前缀式 API key、私钥块、邮箱)脱敏。`hash` 用确定性摘要 `#xxxxxxxx`(同一密钥同一标记)保持可比性。结果带 `redacted` 计数。 |
|
|
121
119
|
| `cwdAllowlist` | 路径列表 | (无) | 只检索这些目录下启动的会话;当前项目目录本身也必须在列表内。 |
|
|
122
120
|
| `cwdDenylist` | 路径列表 | (无) | 这些目录永不检索。deny 优先于 allow。 |
|
|
121
|
+
| `recencyHalfLifeDays` | 天数(如 `30`) | (关闭) | 跨会话命中重排:后端名次 × 命中时间上的指数衰减。不设或 `<= 0` 保持后端顺序。按结果页生效。 |
|
|
122
|
+
| `pinnedCwds` | 路径列表 | (无) | 这些项目目录的会话作为一组排在最前。 |
|
|
123
123
|
| `allProjectsPolicy` | `allow` / `deny` / `confirm` | `allow` | `deny` 忽略 `all_projects` 并向模型说明;`confirm` 走官方 `@deepseek-ai/dsh-user-approval` 接缝向用户请求批准——无应答者时 fail-closed。 |
|
|
124
124
|
|
|
125
125
|
三道闸门统一作用于跨会话命中、CJK 回退扫描和 `session_id` 直读——没有绕行路径。
|
|
126
126
|
|
|
127
|
+
每个结果还带 `diagnostics` 对象(v0.5):命中由哪个引擎产生(`fts` / `cjk-fallback` / `session-scan`)、回退扫描访问了几个会话(对照预算)、是否做了重排——让调用方知道「为什么是这些结果」。
|
|
128
|
+
|
|
127
129
|
## 已知限制
|
|
128
130
|
|
|
129
131
|
- 启动后第一次搜索会扫全量日志建索引(工具描述里已警告模型);之后增量更新。
|
package/lib/index.d.ts
CHANGED
|
@@ -46,6 +46,10 @@ interface RecallConfig {
|
|
|
46
46
|
* approve through the `@deepseek-ai/dsh-user-approval` seam; fail-closed).
|
|
47
47
|
*/
|
|
48
48
|
allProjectsPolicy?: AllProjectsPolicy;
|
|
49
|
+
/** Re-rank cross-session hits with exponential recency decay (half-life in days). Default: off. */
|
|
50
|
+
recencyHalfLifeDays?: number;
|
|
51
|
+
/** Sessions started in these project directories rank first. Default: none. */
|
|
52
|
+
pinnedCwds?: readonly string[];
|
|
49
53
|
}
|
|
50
54
|
/** Validated, fully defaulted configuration. */
|
|
51
55
|
interface NormalizedRecallConfig {
|
|
@@ -59,6 +63,8 @@ interface NormalizedRecallConfig {
|
|
|
59
63
|
readonly cwdAllowlist: readonly string[];
|
|
60
64
|
readonly cwdDenylist: readonly string[];
|
|
61
65
|
readonly allProjectsPolicy: AllProjectsPolicy;
|
|
66
|
+
readonly recencyHalfLifeDays: number | undefined;
|
|
67
|
+
readonly pinnedCwds: readonly string[];
|
|
62
68
|
}
|
|
63
69
|
/** Default, clamp, and cross-check every optional field. */
|
|
64
70
|
declare function normalizeRecallConfig(config?: RecallConfig): NormalizedRecallConfig;
|
|
@@ -91,6 +97,17 @@ interface RecallScope {
|
|
|
91
97
|
allProjects: boolean;
|
|
92
98
|
sessionId: string | null;
|
|
93
99
|
}
|
|
100
|
+
/** How the matches in a result were produced (v0.5 diagnostics). */
|
|
101
|
+
interface RecallDiagnostics {
|
|
102
|
+
/** Which engine produced the matches. */
|
|
103
|
+
readonly source: 'fts' | 'cjk-fallback' | 'session-scan';
|
|
104
|
+
/** Sessions visited by the fallback scan, when it ran. */
|
|
105
|
+
readonly scanned?: number;
|
|
106
|
+
/** The fallback scan budget (`cjkFallbackScanMax`), when a scan ran. */
|
|
107
|
+
readonly scanBudget?: number;
|
|
108
|
+
/** True when re-ranking (recency decay / pinning) was applied to this page. */
|
|
109
|
+
readonly ranked: boolean;
|
|
110
|
+
}
|
|
94
111
|
/** The `recall` tool's canonical JSON value, validated against the output schema. */
|
|
95
112
|
interface RecallResult {
|
|
96
113
|
query: string;
|
|
@@ -102,6 +119,8 @@ interface RecallResult {
|
|
|
102
119
|
hint: string | null;
|
|
103
120
|
/** How many secret-looking fields were redacted in this result (0 when redaction is off). */
|
|
104
121
|
redacted: number;
|
|
122
|
+
/** How this result was produced; null when the call failed before searching. */
|
|
123
|
+
diagnostics: RecallDiagnostics | null;
|
|
105
124
|
}
|
|
106
125
|
/** The typed model-facing arguments after schema validation. */
|
|
107
126
|
interface RecallArgs {
|
|
@@ -112,6 +131,53 @@ interface RecallArgs {
|
|
|
112
131
|
cursor?: string;
|
|
113
132
|
}
|
|
114
133
|
//#endregion
|
|
134
|
+
//#region src/rank.d.ts
|
|
135
|
+
/**
|
|
136
|
+
* Result re-ranking for cross-session recall hits.
|
|
137
|
+
*
|
|
138
|
+
* The FTS backend returns hits in its own relevance order but exposes no
|
|
139
|
+
* numeric score. Two optional, composable controls re-rank that order:
|
|
140
|
+
*
|
|
141
|
+
* - **Recency decay** (`recencyHalfLifeDays`): each hit's weight is its
|
|
142
|
+
* backend rank position (1/rank, a monotone relevance proxy) multiplied by
|
|
143
|
+
* an exponential decay `0.5^(ageDays / halfLife)` over the match time. A hit
|
|
144
|
+
* one half-life old needs roughly twice the relevance rank to keep its
|
|
145
|
+
* place — "the bug we fixed last week" beats an older, equally-ranked match.
|
|
146
|
+
*
|
|
147
|
+
* - **Session pinning** (`pinnedCwds`): hits from pinned project directories
|
|
148
|
+
* float to the top as a group, preserving their internal order.
|
|
149
|
+
*
|
|
150
|
+
* Sorting is stable (ties keep backend order) and per result page: the
|
|
151
|
+
* backend pages lazily, so ranking is applied to the page in hand — a
|
|
152
|
+
* documented limitation, not a global rerank.
|
|
153
|
+
*
|
|
154
|
+
* @module dsh-session-recall/rank
|
|
155
|
+
*/
|
|
156
|
+
/** The slice of a recall item ranking needs (structural, for testability). */
|
|
157
|
+
interface RankableItem {
|
|
158
|
+
readonly cwd: string | null;
|
|
159
|
+
readonly createdAt: number;
|
|
160
|
+
readonly bestMatch: {
|
|
161
|
+
readonly time: number;
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
interface RankingOptions {
|
|
165
|
+
/** Exponential recency half-life in days; unset disables decay re-ranking. */
|
|
166
|
+
readonly recencyHalfLifeDays?: number;
|
|
167
|
+
/** Project directories whose hits float to the top. */
|
|
168
|
+
readonly pinnedCwds?: readonly string[];
|
|
169
|
+
/** Clock override for tests; defaults to Date.now(). */
|
|
170
|
+
readonly now?: number;
|
|
171
|
+
}
|
|
172
|
+
/** True when any ranking control is active. */
|
|
173
|
+
declare function rankingActive(options: RankingOptions): boolean;
|
|
174
|
+
/**
|
|
175
|
+
* Re-rank one page of hits. Returns the same item references in a new order;
|
|
176
|
+
* the input is never mutated. With no controls active the input order is
|
|
177
|
+
* returned unchanged.
|
|
178
|
+
*/
|
|
179
|
+
declare function rankItems<T extends RankableItem>(items: readonly T[], options?: RankingOptions): T[];
|
|
180
|
+
//#endregion
|
|
115
181
|
//#region src/tool.d.ts
|
|
116
182
|
/** The ctx.sessionQuery surface this tool consumes (structural, for testability). */
|
|
117
183
|
interface RecallQueryEngine {
|
|
@@ -185,4 +251,4 @@ declare const inject: string[];
|
|
|
185
251
|
/** Plugin entry: mount the `recall` tool on the global tool registry. */
|
|
186
252
|
declare function apply(ctx: Context, config?: RecallConfig): void;
|
|
187
253
|
//#endregion
|
|
188
|
-
export { ALL_PROJECTS_POLICIES, type AllProjectsPolicy, type NormalizedRecallConfig, RECALL_TOOL_DESCRIPTION, REDACTION_MODES, type RecallApprovalVerdict, type RecallApprover, type RecallArgs, type RecallBestMatch, type RecallConfig, type RecallItem, type RecallQueryEngine, type RecallResult, type RecallScope, type RedactionMode, apply, cjkFallbackHint, cjkZeroHitHint, clamp, createRecallTool, cwdAllowed, firstLineClipped, formatDate, hasCJK, id8, inject, name, normalizeQuery, normalizeRecallConfig, normalizeRedactionMode, recallContentBlocks, recallPresentationMeta, redactText, renderRecallText, snippetAround };
|
|
254
|
+
export { ALL_PROJECTS_POLICIES, type AllProjectsPolicy, type NormalizedRecallConfig, RECALL_TOOL_DESCRIPTION, REDACTION_MODES, type RankableItem, type RankingOptions, type RecallApprovalVerdict, type RecallApprover, type RecallArgs, type RecallBestMatch, type RecallConfig, type RecallDiagnostics, type RecallItem, type RecallQueryEngine, type RecallResult, type RecallScope, type RedactionMode, apply, cjkFallbackHint, cjkZeroHitHint, clamp, createRecallTool, cwdAllowed, firstLineClipped, formatDate, hasCJK, id8, inject, name, normalizeQuery, normalizeRecallConfig, normalizeRedactionMode, rankItems, rankingActive, recallContentBlocks, recallPresentationMeta, redactText, renderRecallText, snippetAround };
|
package/lib/index.js
CHANGED
|
@@ -95,7 +95,9 @@ function normalizeRecallConfig(config) {
|
|
|
95
95
|
redactionMode: normalizeRedactionMode(config?.redactionMode),
|
|
96
96
|
cwdAllowlist: stringList(config?.cwdAllowlist),
|
|
97
97
|
cwdDenylist: stringList(config?.cwdDenylist),
|
|
98
|
-
allProjectsPolicy: normalizePolicy(config?.allProjectsPolicy)
|
|
98
|
+
allProjectsPolicy: normalizePolicy(config?.allProjectsPolicy),
|
|
99
|
+
recencyHalfLifeDays: typeof config?.recencyHalfLifeDays === "number" && Number.isFinite(config.recencyHalfLifeDays) && config.recencyHalfLifeDays > 0 ? Math.min(3650, Math.trunc(config.recencyHalfLifeDays)) : void 0,
|
|
100
|
+
pinnedCwds: stringList(config?.pinnedCwds)
|
|
99
101
|
};
|
|
100
102
|
}
|
|
101
103
|
/** Whether a session cwd is searchable under the allowlist/denylist policy. */
|
|
@@ -106,6 +108,43 @@ function cwdAllowed(cwd, cfg) {
|
|
|
106
108
|
return true;
|
|
107
109
|
}
|
|
108
110
|
//#endregion
|
|
111
|
+
//#region src/rank.ts
|
|
112
|
+
/** True when any ranking control is active. */
|
|
113
|
+
function rankingActive(options) {
|
|
114
|
+
return (options.recencyHalfLifeDays ?? 0) > 0 || (options.pinnedCwds?.length ?? 0) > 0;
|
|
115
|
+
}
|
|
116
|
+
/** One item's decayed weight; rank positions are 1-based. */
|
|
117
|
+
function weight(item, rank, halfLifeDays, now) {
|
|
118
|
+
const relevance = 1 / rank;
|
|
119
|
+
if (halfLifeDays === void 0 || halfLifeDays <= 0) return relevance;
|
|
120
|
+
const matchTime = item.bestMatch.time > 0 ? item.bestMatch.time : item.createdAt;
|
|
121
|
+
const ageDays = Math.max(0, (now - matchTime) / 864e5);
|
|
122
|
+
return relevance * Math.pow(.5, ageDays / halfLifeDays);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Re-rank one page of hits. Returns the same item references in a new order;
|
|
126
|
+
* the input is never mutated. With no controls active the input order is
|
|
127
|
+
* returned unchanged.
|
|
128
|
+
*/
|
|
129
|
+
function rankItems(items, options) {
|
|
130
|
+
if (options === void 0 || !rankingActive(options)) return [...items];
|
|
131
|
+
const now = options.now ?? Date.now();
|
|
132
|
+
const halfLife = options.recencyHalfLifeDays;
|
|
133
|
+
const pinned = new Set(options.pinnedCwds ?? []);
|
|
134
|
+
const decorated = items.map((item, index) => ({
|
|
135
|
+
item,
|
|
136
|
+
index,
|
|
137
|
+
pinned: item.cwd !== null && pinned.has(item.cwd),
|
|
138
|
+
score: weight(item, index + 1, halfLife, now)
|
|
139
|
+
}));
|
|
140
|
+
decorated.sort((a, b) => {
|
|
141
|
+
if (a.pinned !== b.pinned) return a.pinned ? -1 : 1;
|
|
142
|
+
if (b.score !== a.score) return b.score - a.score;
|
|
143
|
+
return a.index - b.index;
|
|
144
|
+
});
|
|
145
|
+
return decorated.map((entry) => entry.item);
|
|
146
|
+
}
|
|
147
|
+
//#endregion
|
|
109
148
|
//#region src/util.ts
|
|
110
149
|
/** Small pure helpers shared by the recall tool and its renderers. */
|
|
111
150
|
/** Clamp `n` into the inclusive `[lo, hi]` range. */
|
|
@@ -185,6 +224,8 @@ function renderRecallText(result) {
|
|
|
185
224
|
const lines = [];
|
|
186
225
|
const noun = result.scope.sessionId != null ? "events" : "sessions";
|
|
187
226
|
lines.push(`Recall "${result.query}" — ${result.count} ${noun}${scopeParts.length > 0 ? ` (${scopeParts.join(", ")})` : ""}`);
|
|
227
|
+
const diag = diagnosticsLine(result.diagnostics);
|
|
228
|
+
if (diag !== null) lines.push(diag);
|
|
188
229
|
if (result.items.length === 0) lines.push("No matches.");
|
|
189
230
|
else result.items.forEach((item, i) => {
|
|
190
231
|
lines.push(`${i + 1}. ${sessionLabel(result, i)}`);
|
|
@@ -194,6 +235,17 @@ function renderRecallText(result) {
|
|
|
194
235
|
if (result.hint != null) lines.push(`Hint: ${result.hint}`);
|
|
195
236
|
return lines.join("\n");
|
|
196
237
|
}
|
|
238
|
+
/** One compact diagnostics line, only when it carries information. */
|
|
239
|
+
function diagnosticsLine(d) {
|
|
240
|
+
if (d == null) return null;
|
|
241
|
+
const parts = [];
|
|
242
|
+
if (d.source !== "fts") {
|
|
243
|
+
const scan = d.scanned !== void 0 ? ` (scanned ${d.scanned}${d.scanBudget !== void 0 ? ` of ${d.scanBudget}` : ""})` : "";
|
|
244
|
+
parts.push(`matched via ${d.source}${scan}`);
|
|
245
|
+
}
|
|
246
|
+
if (d.ranked) parts.push("re-ranked: recency decay + pinned projects");
|
|
247
|
+
return parts.length > 0 ? `Diagnostics: ${parts.join("; ")}` : null;
|
|
248
|
+
}
|
|
197
249
|
/** Content-block projection required by the tool's output declaration. */
|
|
198
250
|
function recallContentBlocks(result) {
|
|
199
251
|
return [{
|
|
@@ -249,6 +301,7 @@ const RECALL_TOOL_DESCRIPTION = [
|
|
|
249
301
|
"Use it when the user refers to earlier work (\"that bug we fixed last week\", \"the font we chose for my resume\") or when prior context was compacted away.",
|
|
250
302
|
"Matches whole words/phrases for English and code identifiers; a zero-hit Chinese (CJK) query automatically falls back to a substring scan in which every whitespace-separated term must match. Returns the best-matching event snippet per session plus the session id.",
|
|
251
303
|
"Then use the read tool on files, or ask the user, to go deeper — this tool only points at history, it does not resume sessions.",
|
|
304
|
+
"To save a full evidence report of a hit session, call the transcript_export tool (from dsh-session-export) with its sessionId when available.",
|
|
252
305
|
"Scoping: by default only sessions started in the current project directory; pass all_projects=true to search everywhere (the deployment may ignore it or require user approval).",
|
|
253
306
|
"When the deployment enables redaction, secret-looking text in snippets appears as [REDACTED] or a #hash marker — treat it as removed; do not try to reconstruct or echo it.",
|
|
254
307
|
"The first search after startup may be slow while the index builds."
|
|
@@ -282,7 +335,8 @@ function recallError(query, error) {
|
|
|
282
335
|
items: [],
|
|
283
336
|
nextCursor: null,
|
|
284
337
|
hint: friendlyError(error),
|
|
285
|
-
redacted: 0
|
|
338
|
+
redacted: 0,
|
|
339
|
+
diagnostics: null
|
|
286
340
|
};
|
|
287
341
|
}
|
|
288
342
|
function brand(value) {
|
|
@@ -362,9 +416,9 @@ function cjkTextFilters(query) {
|
|
|
362
416
|
*/
|
|
363
417
|
async function cjkScanSessions(engine, query, agentCwd, wantAll, cfg, scanMax, limit, signal) {
|
|
364
418
|
const all = await engine.listSessions(signal);
|
|
365
|
-
const
|
|
419
|
+
const page = (!wantAll && agentCwd != null ? all.filter((record) => record.header.cwd === agentCwd) : all).filter((record) => cwdAllowed(record.header.cwd, cfg)).slice(0, scanMax);
|
|
366
420
|
const items = [];
|
|
367
|
-
for (const record of
|
|
421
|
+
for (const record of page) {
|
|
368
422
|
if (items.length >= limit) break;
|
|
369
423
|
const { filters, highlight } = cjkTextFilters(query);
|
|
370
424
|
const docs = await engine.filterEvents(record.header.id, filters);
|
|
@@ -387,7 +441,11 @@ async function cjkScanSessions(engine, query, agentCwd, wantAll, cfg, scanMax, l
|
|
|
387
441
|
}
|
|
388
442
|
});
|
|
389
443
|
}
|
|
390
|
-
return
|
|
444
|
+
return {
|
|
445
|
+
items,
|
|
446
|
+
scanned: page.length,
|
|
447
|
+
budget: scanMax
|
|
448
|
+
};
|
|
391
449
|
}
|
|
392
450
|
const nullableString = { oneOf: [{ type: "string" }, { type: "null" }] };
|
|
393
451
|
const recallOutputSchema = {
|
|
@@ -434,7 +492,24 @@ const recallOutputSchema = {
|
|
|
434
492
|
},
|
|
435
493
|
nextCursor: nullableString,
|
|
436
494
|
hint: nullableString,
|
|
437
|
-
redacted: { type: "integer" }
|
|
495
|
+
redacted: { type: "integer" },
|
|
496
|
+
diagnostics: { oneOf: [{ type: "null" }, {
|
|
497
|
+
type: "object",
|
|
498
|
+
additionalProperties: false,
|
|
499
|
+
properties: {
|
|
500
|
+
source: {
|
|
501
|
+
type: "string",
|
|
502
|
+
enum: [
|
|
503
|
+
"fts",
|
|
504
|
+
"cjk-fallback",
|
|
505
|
+
"session-scan"
|
|
506
|
+
]
|
|
507
|
+
},
|
|
508
|
+
scanned: { type: "integer" },
|
|
509
|
+
scanBudget: { type: "integer" },
|
|
510
|
+
ranked: { type: "boolean" }
|
|
511
|
+
}
|
|
512
|
+
}] }
|
|
438
513
|
}
|
|
439
514
|
};
|
|
440
515
|
/**
|
|
@@ -546,7 +621,8 @@ function createRecallTool(config, engine, approver) {
|
|
|
546
621
|
items: [],
|
|
547
622
|
nextCursor: null,
|
|
548
623
|
hint: "the current project directory is excluded by the recall scope policy (cwd allowlist/denylist). Ask the user to adjust the plugin configuration if this is unexpected.",
|
|
549
|
-
redacted: 0
|
|
624
|
+
redacted: 0,
|
|
625
|
+
diagnostics: null
|
|
550
626
|
};
|
|
551
627
|
try {
|
|
552
628
|
if (args.session_id != null && args.session_id !== "") {
|
|
@@ -565,13 +641,24 @@ function createRecallTool(config, engine, approver) {
|
|
|
565
641
|
items: [],
|
|
566
642
|
nextCursor: null,
|
|
567
643
|
hint: "that session belongs to a project directory excluded by the recall scope policy (cwd allowlist/denylist).",
|
|
568
|
-
redacted: 0
|
|
644
|
+
redacted: 0,
|
|
645
|
+
diagnostics: null
|
|
569
646
|
};
|
|
570
647
|
let items = eventItems(page, sessionId);
|
|
571
648
|
let hint = null;
|
|
649
|
+
let diagnostics = {
|
|
650
|
+
source: "fts",
|
|
651
|
+
ranked: false
|
|
652
|
+
};
|
|
572
653
|
if (items.length === 0 && hasCJK(query) && cfg.cjkFallback) {
|
|
573
654
|
const { filters, highlight } = cjkTextFilters(query);
|
|
574
655
|
const docs = await engine.filterEvents(sessionId, filters);
|
|
656
|
+
diagnostics = {
|
|
657
|
+
source: "session-scan",
|
|
658
|
+
scanned: 1,
|
|
659
|
+
scanBudget: cfg.cjkFallbackScanMax,
|
|
660
|
+
ranked: false
|
|
661
|
+
};
|
|
575
662
|
if (docs.length > 0) items = docs.slice(0, limit).map((doc) => ({
|
|
576
663
|
sessionId,
|
|
577
664
|
id8: id8(sessionId),
|
|
@@ -598,7 +685,8 @@ function createRecallTool(config, engine, approver) {
|
|
|
598
685
|
items: red.items,
|
|
599
686
|
nextCursor: page.nextCursor ?? null,
|
|
600
687
|
hint: joinHints(hint, red.hint),
|
|
601
|
-
redacted: red.redacted
|
|
688
|
+
redacted: red.redacted,
|
|
689
|
+
diagnostics
|
|
602
690
|
};
|
|
603
691
|
}
|
|
604
692
|
const request = {
|
|
@@ -612,17 +700,32 @@ function createRecallTool(config, engine, approver) {
|
|
|
612
700
|
if (args.cursor != null && args.cursor !== "") request.cursor = brand(args.cursor);
|
|
613
701
|
const page = await engine.searchSessions(request, { signal: exec.signal });
|
|
614
702
|
const titles = await titlesFor(engine, page.items.map((hit) => hit.header.id), exec.signal);
|
|
615
|
-
|
|
703
|
+
const rankingOptions = {
|
|
704
|
+
recencyHalfLifeDays: cfg.recencyHalfLifeDays,
|
|
705
|
+
pinnedCwds: cfg.pinnedCwds
|
|
706
|
+
};
|
|
707
|
+
const ranked = rankingActive(rankingOptions);
|
|
708
|
+
let items = rankItems(toItems(page.items, titles).filter((item) => cwdAllowed(item.cwd, cfg)), rankingOptions);
|
|
616
709
|
let hint = null;
|
|
617
710
|
let fallbackRan = false;
|
|
711
|
+
let diagnostics = {
|
|
712
|
+
source: "fts",
|
|
713
|
+
ranked
|
|
714
|
+
};
|
|
618
715
|
if (items.length === 0 && hasCJK(query) && cfg.cjkFallback) {
|
|
619
716
|
fallbackRan = true;
|
|
620
|
-
const
|
|
621
|
-
const scanTitles = await titlesFor(engine,
|
|
622
|
-
items =
|
|
717
|
+
const scan = await cjkScanSessions(engine, query, agentCwd, wantAll, cfg, cfg.cjkFallbackScanMax, limit, exec.signal);
|
|
718
|
+
const scanTitles = await titlesFor(engine, scan.items.map((item) => item.sessionId), exec.signal);
|
|
719
|
+
items = rankItems(scan.items.map((item) => ({
|
|
623
720
|
...item,
|
|
624
721
|
title: scanTitles.get(item.sessionId) ?? null
|
|
625
|
-
}));
|
|
722
|
+
})), rankingOptions);
|
|
723
|
+
diagnostics = {
|
|
724
|
+
source: "cjk-fallback",
|
|
725
|
+
scanned: scan.scanned,
|
|
726
|
+
scanBudget: scan.budget,
|
|
727
|
+
ranked
|
|
728
|
+
};
|
|
626
729
|
hint = items.length > 0 ? cjkFallbackHint(items.length, cfg.cjkHint) : cjkZeroHitHint(query, true, cfg.cjkHint);
|
|
627
730
|
} else if (items.length === 0) hint = cjkZeroHitHint(query, true, cfg.cjkHint);
|
|
628
731
|
const red = applyRedaction(items);
|
|
@@ -634,7 +737,8 @@ function createRecallTool(config, engine, approver) {
|
|
|
634
737
|
items: red.items,
|
|
635
738
|
nextCursor: !fallbackRan ? page.nextCursor ?? null : null,
|
|
636
739
|
hint: joinHints(hint, ...scopeHints, red.hint),
|
|
637
|
-
redacted: red.redacted
|
|
740
|
+
redacted: red.redacted,
|
|
741
|
+
diagnostics
|
|
638
742
|
};
|
|
639
743
|
} catch (error) {
|
|
640
744
|
return recallError(query, error);
|
|
@@ -693,4 +797,4 @@ function apply(ctx, config) {
|
|
|
693
797
|
}, "session-recall lifecycle");
|
|
694
798
|
}
|
|
695
799
|
//#endregion
|
|
696
|
-
export { ALL_PROJECTS_POLICIES, RECALL_TOOL_DESCRIPTION, REDACTION_MODES, apply, cjkFallbackHint, cjkZeroHitHint, clamp, createRecallTool, cwdAllowed, firstLineClipped, formatDate, hasCJK, id8, inject, name, normalizeQuery, normalizeRecallConfig, normalizeRedactionMode, recallContentBlocks, recallPresentationMeta, redactText, renderRecallText, snippetAround };
|
|
800
|
+
export { ALL_PROJECTS_POLICIES, RECALL_TOOL_DESCRIPTION, REDACTION_MODES, apply, cjkFallbackHint, cjkZeroHitHint, clamp, createRecallTool, cwdAllowed, firstLineClipped, formatDate, hasCJK, id8, inject, name, normalizeQuery, normalizeRecallConfig, normalizeRedactionMode, rankItems, rankingActive, recallContentBlocks, recallPresentationMeta, redactText, renderRecallText, snippetAround };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-session-recall",
|
|
3
3
|
"description": "Deterministic cross-session transcript retrieval for DeepSeek Harness: the model-facing `recall` tool searches past session logs with explicit scope control",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.5.1",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|