dsh-session-recall 0.2.0 → 0.3.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 +2 -2
- package/README.zh.md +2 -2
- package/lib/index.js +28 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -33,7 +33,7 @@ recall({ query, session_id }) → search the events of one session
|
|
|
33
33
|
recall({ query, limit, cursor }) → page through results
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
Each hit carries the session id, title (best-effort), date, and a match snippet; the result renders as a native search card in the Web UI (`SearchMatchesResultView`). Because the FTS `unicode61` tokenizer indexes an uninterrupted CJK run as a single token, a short Chinese phrase inside a longer sentence would otherwise never match the index — so a zero-hit CJK query automatically falls back to
|
|
36
|
+
Each hit carries the session id, title (best-effort), date, and a match snippet; the result renders as a native search card in the Web UI (`SearchMatchesResultView`). Because the FTS `unicode61` tokenizer indexes an uninterrupted CJK run as a single token, a short Chinese phrase inside a longer sentence would otherwise never match the index — so a zero-hit CJK query automatically falls back to a substring scan over session text (the `sessionQuery.filterEvents` literal text clause). Every whitespace-separated term must match, so `简历 模板` still recovers `简历模板`; the hint reports when that path matched.
|
|
37
37
|
|
|
38
38
|
## Scoping (the authorization gap)
|
|
39
39
|
|
|
@@ -89,7 +89,7 @@ Every failure returns a friendly `hint` instead of a raw exception: a disabled i
|
|
|
89
89
|
## Known limitations
|
|
90
90
|
|
|
91
91
|
- First search after startup walks the durable logs to build the index (the tool description warns the model); subsequent searches are incremental.
|
|
92
|
-
- `unicode61` matches whole tokens/phrases, not substrings — `AI` does not match `BRAID`. CJK queries that get zero full-text hits fall back to
|
|
92
|
+
- `unicode61` matches whole tokens/phrases, not substrings — `AI` does not match `BRAID`. CJK queries that get zero full-text hits fall back to a substring scan (`filterEvents`) whose whitespace-separated terms are ANDed, so `简历 模板` also recovers `简历模板`; the hint reports when that path matched.
|
|
93
93
|
- One process must own the index file (single-writer SQLite, per the official backend).
|
|
94
94
|
- Matches return transcript text verbatim — there is no credential or local-path redaction. A token or sensitive path pasted into an earlier session can be surfaced by a matching search. Default cwd scoping and `allowAllProjects: false` are the only containment; fingerprinting or redaction is future work.
|
|
95
95
|
|
package/README.zh.md
CHANGED
|
@@ -33,7 +33,7 @@ recall({ query, session_id }) → 只搜指定会话内的事件
|
|
|
33
33
|
recall({ query, limit, cursor }) → 翻页
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
每条命中带会话 id、标题(尽力补全)、日期、命中摘录;结果在 Web UI 里渲染成原生搜索卡片(`SearchMatchesResultView`)。因为 FTS 的 `unicode61` 分词器会把连续中文当成一个 token,短中文短语一旦嵌在长句里就匹配不到索引——所以 CJK
|
|
36
|
+
每条命中带会话 id、标题(尽力补全)、日期、命中摘录;结果在 Web UI 里渲染成原生搜索卡片(`SearchMatchesResultView`)。因为 FTS 的 `unicode61` 分词器会把连续中文当成一个 token,短中文短语一旦嵌在长句里就匹配不到索引——所以 CJK 查询零命中时会自动回退到对会话文本的子串扫描(走 `sessionQuery.filterEvents` 的字面文本子句),空格拆出的每个词都必须命中,因此 `简历 模板` 也能找回 `简历模板`;hint 会说明这条回退路径是否命中。
|
|
37
37
|
|
|
38
38
|
## 授权边界(官方明确留给工具层的责任)
|
|
39
39
|
|
|
@@ -89,7 +89,7 @@ dsh plugin --profile web add github:kittimzhe/dsh-session-recall
|
|
|
89
89
|
## 已知限制
|
|
90
90
|
|
|
91
91
|
- 启动后第一次搜索会扫全量日志建索引(工具描述里已警告模型);之后增量更新。
|
|
92
|
-
- `unicode61` 按完整 token/短语匹配,不支持子串——`AI` 匹配不到 `BRAID`。CJK
|
|
92
|
+
- `unicode61` 按完整 token/短语匹配,不支持子串——`AI` 匹配不到 `BRAID`。CJK 查询零命中时会回退到子串扫描(`filterEvents`),空格分隔的各词按 AND 语义都必须命中,因此 `简历 模板` 也能找回 `简历模板`;hint 会说明是否命中。
|
|
93
93
|
- 索引文件单进程独占(官方后端的单写者 SQLite 约束)。
|
|
94
94
|
- 命中结果按原文照摘,**没有任何凭据或本地路径脱敏**——更早的会话里粘贴过的 token 或敏感路径可能被检索出来。目前只有默认 cwd 收窄与 `allowAllProjects: false` 两道闸;指纹识别/脱敏是后续增强。
|
|
95
95
|
|
package/lib/index.js
CHANGED
|
@@ -47,6 +47,10 @@ function hasCJK(text) {
|
|
|
47
47
|
function normalizeQuery(text) {
|
|
48
48
|
return text.trim().replaceAll(/\s+/g, " ");
|
|
49
49
|
}
|
|
50
|
+
/** Split into whitespace-separated terms, dropping empty pieces. */
|
|
51
|
+
function splitTerms(text) {
|
|
52
|
+
return text.split(/\s+/u).filter((term) => term.length > 0);
|
|
53
|
+
}
|
|
50
54
|
/** First line of `text` with control characters stripped, clipped to `limit` code points. */
|
|
51
55
|
function firstLineClipped(text, limit) {
|
|
52
56
|
const line = text.split("\n", 1)[0] ?? "";
|
|
@@ -155,7 +159,7 @@ function cjkZeroHitHint(query, zeroHits, enabled) {
|
|
|
155
159
|
const RECALL_TOOL_DESCRIPTION = [
|
|
156
160
|
"Search the FULL TEXT of past and current session transcripts on this machine (your own conversation history with this user).",
|
|
157
161
|
"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.",
|
|
158
|
-
"Matches whole words/phrases for English and code identifiers; a zero-hit Chinese (CJK) query automatically falls back to
|
|
162
|
+
"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.",
|
|
159
163
|
"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.",
|
|
160
164
|
"Scoping: by default only sessions started in the current project directory; pass all_projects=true to search everywhere.",
|
|
161
165
|
"The first search after startup may be slow while the index builds."
|
|
@@ -241,6 +245,23 @@ function eventItems(page, sessionId) {
|
|
|
241
245
|
/** Snippet window kept consistent with the tool's text projection. */
|
|
242
246
|
const CJK_SNIPPET_CHARS = 120;
|
|
243
247
|
/**
|
|
248
|
+
* Decompose a query into ANDed literal-text clauses. A space-separated CJK
|
|
249
|
+
* query like "简历 模板" must match each term as a substring (so it recovers
|
|
250
|
+
* "简历模板"), not a whitespace-joined literal (which would require the space
|
|
251
|
+
* to be present verbatim). `highlight` is the first term, guaranteed present
|
|
252
|
+
* whenever the ANDed scan matches.
|
|
253
|
+
*/
|
|
254
|
+
function cjkTextFilters(query) {
|
|
255
|
+
const terms = splitTerms(query);
|
|
256
|
+
return {
|
|
257
|
+
filters: terms.map((term) => ({
|
|
258
|
+
kind: "text",
|
|
259
|
+
text: term
|
|
260
|
+
})),
|
|
261
|
+
highlight: terms[0] ?? query
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
244
265
|
* CJK substring-scan fallback for zero-hit full-text searches. SQLite FTS5's
|
|
245
266
|
* `unicode61` tokenizer treats an uninterrupted CJK run as one token, so a
|
|
246
267
|
* short Chinese phrase inside a longer sentence never matches the index. The
|
|
@@ -255,10 +276,8 @@ async function cjkScanSessions(engine, query, agentCwd, wantAll, scanMax, limit,
|
|
|
255
276
|
const items = [];
|
|
256
277
|
for (const record of candidates.slice(0, scanMax)) {
|
|
257
278
|
if (items.length >= limit) break;
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
text: query
|
|
261
|
-
}]);
|
|
279
|
+
const { filters, highlight } = cjkTextFilters(query);
|
|
280
|
+
const docs = await engine.filterEvents(record.header.id, filters);
|
|
262
281
|
if (docs.length === 0) continue;
|
|
263
282
|
const doc = docs[0];
|
|
264
283
|
if (doc === void 0) continue;
|
|
@@ -274,7 +293,7 @@ async function cjkScanSessions(engine, query, agentCwd, wantAll, scanMax, limit,
|
|
|
274
293
|
seq: doc.seq,
|
|
275
294
|
type: doc.type,
|
|
276
295
|
time: doc.time,
|
|
277
|
-
snippet: snippetAround(doc.text,
|
|
296
|
+
snippet: snippetAround(doc.text, highlight, CJK_SNIPPET_CHARS)
|
|
278
297
|
}
|
|
279
298
|
});
|
|
280
299
|
}
|
|
@@ -390,10 +409,8 @@ function createRecallTool(config, engine) {
|
|
|
390
409
|
let items = eventItems(page, sessionId);
|
|
391
410
|
let hint = null;
|
|
392
411
|
if (items.length === 0 && hasCJK(query) && cfg.cjkFallback) {
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
text: query
|
|
396
|
-
}]);
|
|
412
|
+
const { filters, highlight } = cjkTextFilters(query);
|
|
413
|
+
const docs = await engine.filterEvents(sessionId, filters);
|
|
397
414
|
if (docs.length > 0) items = docs.slice(0, limit).map((doc) => ({
|
|
398
415
|
sessionId,
|
|
399
416
|
id8: id8(sessionId),
|
|
@@ -406,7 +423,7 @@ function createRecallTool(config, engine) {
|
|
|
406
423
|
seq: doc.seq,
|
|
407
424
|
type: doc.type,
|
|
408
425
|
time: doc.time,
|
|
409
|
-
snippet: snippetAround(doc.text,
|
|
426
|
+
snippet: snippetAround(doc.text, highlight, CJK_SNIPPET_CHARS)
|
|
410
427
|
}
|
|
411
428
|
}));
|
|
412
429
|
hint = items.length > 0 ? cjkFallbackHint(items.length, cfg.cjkHint) : cjkZeroHitHint(query, true, cfg.cjkHint);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-session-recall",
|
|
3
3
|
"description": "Cross-session full-text recall for DeepSeek Harness: the model-facing `recall` tool searches past session transcripts through ctx.sessionQuery",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.3.0",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|