dsh-session-recall 0.4.0 → 0.5.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/README.md +7 -5
- package/README.zh.md +7 -5
- package/lib/index.d.ts +67 -1
- package/lib/index.js +119 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# dsh-session-recall
|
|
2
2
|
|
|
3
|
-
English | [中文](README.zh.md)
|
|
3
|
+
English | [中文](https://github.com/kittimzhe/dsh-session-recall/blob/main/README.zh.md)
|
|
4
4
|
|
|
5
|
-
[](https://www.npmjs.com/package/dsh-session-recall) [](LICENSE)
|
|
5
|
+
[](https://www.npmjs.com/package/dsh-session-recall) [](https://github.com/kittimzhe/dsh-session-recall/blob/main/LICENSE)
|
|
6
6
|
|
|
7
7
|
Deterministic cross-session full-text retrieval for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness): the model-facing `recall` tool lets the agent **search its own past session transcripts** — "that bug we fixed last week", "the font we chose for my resume" — through the trusted `ctx.sessionQuery` seam.
|
|
8
8
|
|
|
@@ -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.
|
|
@@ -162,4 +164,4 @@ npm run bundle # tsdown → lib/
|
|
|
162
164
|
|
|
163
165
|
## License
|
|
164
166
|
|
|
165
|
-
[MIT](LICENSE)
|
|
167
|
+
[MIT](https://github.com/kittimzhe/dsh-session-recall/blob/main/LICENSE)
|
package/README.zh.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# dsh-session-recall
|
|
2
2
|
|
|
3
|
-
[English](README.md) | 中文
|
|
3
|
+
[English](https://github.com/kittimzhe/dsh-session-recall/blob/main/README.md) | 中文
|
|
4
4
|
|
|
5
|
-
[](https://www.npmjs.com/package/dsh-session-recall) [](LICENSE)
|
|
5
|
+
[](https://www.npmjs.com/package/dsh-session-recall) [](https://github.com/kittimzhe/dsh-session-recall/blob/main/LICENSE)
|
|
6
6
|
|
|
7
7
|
DeepSeek Harness 的**确定性跨会话全文检索**插件:注册模型可调用的 `recall` 工具,让 agent 能**检索自己过往的会话原文**——"上周修的那个 bug"、"简历选的什么字体"——全部通过可信的 `ctx.sessionQuery` 缝完成。
|
|
8
8
|
|
|
@@ -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
|
- 启动后第一次搜索会扫全量日志建索引(工具描述里已警告模型);之后增量更新。
|
|
@@ -162,4 +164,4 @@ npm run bundle # tsdown → lib/
|
|
|
162
164
|
|
|
163
165
|
## 许可
|
|
164
166
|
|
|
165
|
-
[MIT](LICENSE)
|
|
167
|
+
[MIT](https://github.com/kittimzhe/dsh-session-recall/blob/main/LICENSE)
|
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 [{
|
|
@@ -282,7 +334,8 @@ function recallError(query, error) {
|
|
|
282
334
|
items: [],
|
|
283
335
|
nextCursor: null,
|
|
284
336
|
hint: friendlyError(error),
|
|
285
|
-
redacted: 0
|
|
337
|
+
redacted: 0,
|
|
338
|
+
diagnostics: null
|
|
286
339
|
};
|
|
287
340
|
}
|
|
288
341
|
function brand(value) {
|
|
@@ -362,9 +415,9 @@ function cjkTextFilters(query) {
|
|
|
362
415
|
*/
|
|
363
416
|
async function cjkScanSessions(engine, query, agentCwd, wantAll, cfg, scanMax, limit, signal) {
|
|
364
417
|
const all = await engine.listSessions(signal);
|
|
365
|
-
const
|
|
418
|
+
const page = (!wantAll && agentCwd != null ? all.filter((record) => record.header.cwd === agentCwd) : all).filter((record) => cwdAllowed(record.header.cwd, cfg)).slice(0, scanMax);
|
|
366
419
|
const items = [];
|
|
367
|
-
for (const record of
|
|
420
|
+
for (const record of page) {
|
|
368
421
|
if (items.length >= limit) break;
|
|
369
422
|
const { filters, highlight } = cjkTextFilters(query);
|
|
370
423
|
const docs = await engine.filterEvents(record.header.id, filters);
|
|
@@ -387,7 +440,11 @@ async function cjkScanSessions(engine, query, agentCwd, wantAll, cfg, scanMax, l
|
|
|
387
440
|
}
|
|
388
441
|
});
|
|
389
442
|
}
|
|
390
|
-
return
|
|
443
|
+
return {
|
|
444
|
+
items,
|
|
445
|
+
scanned: page.length,
|
|
446
|
+
budget: scanMax
|
|
447
|
+
};
|
|
391
448
|
}
|
|
392
449
|
const nullableString = { oneOf: [{ type: "string" }, { type: "null" }] };
|
|
393
450
|
const recallOutputSchema = {
|
|
@@ -434,7 +491,24 @@ const recallOutputSchema = {
|
|
|
434
491
|
},
|
|
435
492
|
nextCursor: nullableString,
|
|
436
493
|
hint: nullableString,
|
|
437
|
-
redacted: { type: "integer" }
|
|
494
|
+
redacted: { type: "integer" },
|
|
495
|
+
diagnostics: { oneOf: [{ type: "null" }, {
|
|
496
|
+
type: "object",
|
|
497
|
+
additionalProperties: false,
|
|
498
|
+
properties: {
|
|
499
|
+
source: {
|
|
500
|
+
type: "string",
|
|
501
|
+
enum: [
|
|
502
|
+
"fts",
|
|
503
|
+
"cjk-fallback",
|
|
504
|
+
"session-scan"
|
|
505
|
+
]
|
|
506
|
+
},
|
|
507
|
+
scanned: { type: "integer" },
|
|
508
|
+
scanBudget: { type: "integer" },
|
|
509
|
+
ranked: { type: "boolean" }
|
|
510
|
+
}
|
|
511
|
+
}] }
|
|
438
512
|
}
|
|
439
513
|
};
|
|
440
514
|
/**
|
|
@@ -546,7 +620,8 @@ function createRecallTool(config, engine, approver) {
|
|
|
546
620
|
items: [],
|
|
547
621
|
nextCursor: null,
|
|
548
622
|
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
|
|
623
|
+
redacted: 0,
|
|
624
|
+
diagnostics: null
|
|
550
625
|
};
|
|
551
626
|
try {
|
|
552
627
|
if (args.session_id != null && args.session_id !== "") {
|
|
@@ -565,13 +640,24 @@ function createRecallTool(config, engine, approver) {
|
|
|
565
640
|
items: [],
|
|
566
641
|
nextCursor: null,
|
|
567
642
|
hint: "that session belongs to a project directory excluded by the recall scope policy (cwd allowlist/denylist).",
|
|
568
|
-
redacted: 0
|
|
643
|
+
redacted: 0,
|
|
644
|
+
diagnostics: null
|
|
569
645
|
};
|
|
570
646
|
let items = eventItems(page, sessionId);
|
|
571
647
|
let hint = null;
|
|
648
|
+
let diagnostics = {
|
|
649
|
+
source: "fts",
|
|
650
|
+
ranked: false
|
|
651
|
+
};
|
|
572
652
|
if (items.length === 0 && hasCJK(query) && cfg.cjkFallback) {
|
|
573
653
|
const { filters, highlight } = cjkTextFilters(query);
|
|
574
654
|
const docs = await engine.filterEvents(sessionId, filters);
|
|
655
|
+
diagnostics = {
|
|
656
|
+
source: "session-scan",
|
|
657
|
+
scanned: 1,
|
|
658
|
+
scanBudget: cfg.cjkFallbackScanMax,
|
|
659
|
+
ranked: false
|
|
660
|
+
};
|
|
575
661
|
if (docs.length > 0) items = docs.slice(0, limit).map((doc) => ({
|
|
576
662
|
sessionId,
|
|
577
663
|
id8: id8(sessionId),
|
|
@@ -598,7 +684,8 @@ function createRecallTool(config, engine, approver) {
|
|
|
598
684
|
items: red.items,
|
|
599
685
|
nextCursor: page.nextCursor ?? null,
|
|
600
686
|
hint: joinHints(hint, red.hint),
|
|
601
|
-
redacted: red.redacted
|
|
687
|
+
redacted: red.redacted,
|
|
688
|
+
diagnostics
|
|
602
689
|
};
|
|
603
690
|
}
|
|
604
691
|
const request = {
|
|
@@ -612,17 +699,32 @@ function createRecallTool(config, engine, approver) {
|
|
|
612
699
|
if (args.cursor != null && args.cursor !== "") request.cursor = brand(args.cursor);
|
|
613
700
|
const page = await engine.searchSessions(request, { signal: exec.signal });
|
|
614
701
|
const titles = await titlesFor(engine, page.items.map((hit) => hit.header.id), exec.signal);
|
|
615
|
-
|
|
702
|
+
const rankingOptions = {
|
|
703
|
+
recencyHalfLifeDays: cfg.recencyHalfLifeDays,
|
|
704
|
+
pinnedCwds: cfg.pinnedCwds
|
|
705
|
+
};
|
|
706
|
+
const ranked = rankingActive(rankingOptions);
|
|
707
|
+
let items = rankItems(toItems(page.items, titles).filter((item) => cwdAllowed(item.cwd, cfg)), rankingOptions);
|
|
616
708
|
let hint = null;
|
|
617
709
|
let fallbackRan = false;
|
|
710
|
+
let diagnostics = {
|
|
711
|
+
source: "fts",
|
|
712
|
+
ranked
|
|
713
|
+
};
|
|
618
714
|
if (items.length === 0 && hasCJK(query) && cfg.cjkFallback) {
|
|
619
715
|
fallbackRan = true;
|
|
620
|
-
const
|
|
621
|
-
const scanTitles = await titlesFor(engine,
|
|
622
|
-
items =
|
|
716
|
+
const scan = await cjkScanSessions(engine, query, agentCwd, wantAll, cfg, cfg.cjkFallbackScanMax, limit, exec.signal);
|
|
717
|
+
const scanTitles = await titlesFor(engine, scan.items.map((item) => item.sessionId), exec.signal);
|
|
718
|
+
items = rankItems(scan.items.map((item) => ({
|
|
623
719
|
...item,
|
|
624
720
|
title: scanTitles.get(item.sessionId) ?? null
|
|
625
|
-
}));
|
|
721
|
+
})), rankingOptions);
|
|
722
|
+
diagnostics = {
|
|
723
|
+
source: "cjk-fallback",
|
|
724
|
+
scanned: scan.scanned,
|
|
725
|
+
scanBudget: scan.budget,
|
|
726
|
+
ranked
|
|
727
|
+
};
|
|
626
728
|
hint = items.length > 0 ? cjkFallbackHint(items.length, cfg.cjkHint) : cjkZeroHitHint(query, true, cfg.cjkHint);
|
|
627
729
|
} else if (items.length === 0) hint = cjkZeroHitHint(query, true, cfg.cjkHint);
|
|
628
730
|
const red = applyRedaction(items);
|
|
@@ -634,7 +736,8 @@ function createRecallTool(config, engine, approver) {
|
|
|
634
736
|
items: red.items,
|
|
635
737
|
nextCursor: !fallbackRan ? page.nextCursor ?? null : null,
|
|
636
738
|
hint: joinHints(hint, ...scopeHints, red.hint),
|
|
637
|
-
redacted: red.redacted
|
|
739
|
+
redacted: red.redacted,
|
|
740
|
+
diagnostics
|
|
638
741
|
};
|
|
639
742
|
} catch (error) {
|
|
640
743
|
return recallError(query, error);
|
|
@@ -693,4 +796,4 @@ function apply(ctx, config) {
|
|
|
693
796
|
}, "session-recall lifecycle");
|
|
694
797
|
}
|
|
695
798
|
//#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 };
|
|
799
|
+
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.0",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|