dsh-session-recall 0.1.0 → 0.2.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 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`). Zero-hit CJK queries get a tokenizer hint: the FTS `unicode61` tokenizer indexes uninterrupted CJK runs as single tokens, so the tool teaches the model to retry with short space-separated keywords.
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 an exact substring scan over session text (the `sessionQuery.filterEvents` literal text clause), and the hint reports when that path matched.
37
37
 
38
38
  ## Scoping (the authorization gap)
39
39
 
@@ -77,7 +77,9 @@ Plugin row config (all optional):
77
77
  allowAllProjects: true # honor the tool's all_projects argument
78
78
  defaultLimit: 5 # page size when the model omits limit (1..10)
79
79
  maxLimit: 10 # largest accepted page size (1..25)
80
- cjkHint: true # zero-hit CJK tokenizer workaround hint
80
+ cjkHint: true # explain CJK zero-hit results
81
+ cjkFallback: true # CJK zero-hit → exact substring scan over session text
82
+ cjkFallbackScanMax: 50 # max sessions scanned per cross-session fallback (1..500)
81
83
  ```
82
84
 
83
85
  ## Failure behavior
@@ -87,8 +89,29 @@ Every failure returns a friendly `hint` instead of a raw exception: a disabled i
87
89
  ## Known limitations
88
90
 
89
91
  - First search after startup walks the durable logs to build the index (the tool description warns the model); subsequent searches are incremental.
90
- - `unicode61` matches whole tokens/phrases, not substrings — `AI` does not match `BRAID`. The zero-hit CJK hint mitigates the worst case; a substring fallback via `filterEvents()` is a possible v2.
92
+ - `unicode61` matches whole tokens/phrases, not substrings — `AI` does not match `BRAID`. CJK queries that get zero full-text hits fall back to an exact substring scan (`filterEvents`), and the hint reports when that path matched; a multi-word CJK phrase still has to survive the tokenizer's whole-run indexing.
91
93
  - One process must own the index file (single-writer SQLite, per the official backend).
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
+
96
+ ## Benchmark
97
+
98
+ Measured on a real headless profile (Node 25, Apple Silicon, warm filesystem cache).
99
+
100
+ | Corpus | |
101
+ |---|---|
102
+ | Sessions | 31 |
103
+ | Events in the durable logs | 187,706 (~104 MB uncompressed, 49.7 MB zstd) |
104
+ | Indexed text events | 8,187 |
105
+ | FTS index on disk | 15 MB |
106
+
107
+ | Query | Hits | Warm latency (FTS5 `MATCH`) |
108
+ |---|---|---|
109
+ | EN `font` | 100 (capped) | 1.1 ms |
110
+ | EN `resume template` | 46 | 0.6 ms |
111
+ | CN `字体` | 29 | 0.3 ms |
112
+ | CN `简历 模板` | 10 | 0.1 ms |
113
+
114
+ Warm searches run sub-millisecond to ~1.5 ms against the on-disk index. Cold start: scanning the 49.7 MB of session logs takes ~4.7 s (decompress + line scan) and inserting the 8,187 text events into a fresh FTS5 table takes ~190 ms; the first `recall` in a fresh profile completes within the tool's 10 s timeout. After that, restarts reuse the persisted index with incremental reconciliation.
92
115
 
93
116
  ## Development
94
117
 
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`)。CJK 查询零命中时会给出分词提示:FTS 的 `unicode61` 分词器把连续中文当成一个 token,工具会教模型改用空格分隔的短关键词重试。
36
+ 每条命中带会话 id、标题(尽力补全)、日期、命中摘录;结果在 Web UI 里渲染成原生搜索卡片(`SearchMatchesResultView`)。因为 FTS 的 `unicode61` 分词器会把连续中文当成一个 token,短中文短语一旦嵌在长句里就匹配不到索引——所以 CJK 查询零命中时会自动回退到对会话文本的**精确子串扫描**(走 `sessionQuery.filterEvents` 的字面文本子句),hint 会说明这条回退路径是否命中。
37
37
 
38
38
  ## 授权边界(官方明确留给工具层的责任)
39
39
 
@@ -77,7 +77,9 @@ dsh plugin --profile web add github:kittimzhe/dsh-session-recall
77
77
  allowAllProjects: true # 是否允许工具的 all_projects 参数
78
78
  defaultLimit: 5 # 模型省略 limit 时的页大小(1..10)
79
79
  maxLimit: 10 # 最大页大小(1..25)
80
- cjkHint: true # CJK 零命中的分词提示开关
80
+ cjkHint: true # CJK 零命中的提示开关
81
+ cjkFallback: true # CJK 零命中 → 对会话文本做精确子串扫描
82
+ cjkFallbackScanMax: 50 # 跨会话回退时最多扫描的会话数(1..500)
81
83
  ```
82
84
 
83
85
  ## 失败行为
@@ -87,8 +89,29 @@ dsh plugin --profile web add github:kittimzhe/dsh-session-recall
87
89
  ## 已知限制
88
90
 
89
91
  - 启动后第一次搜索会扫全量日志建索引(工具描述里已警告模型);之后增量更新。
90
- - `unicode61` 按完整 token/短语匹配,不支持子串——`AI` 匹配不到 `BRAID`。CJK 零命中提示缓解了最坏情况;基于 `filterEvents()` 的子串兜底是 v2 方向。
92
+ - `unicode61` 按完整 token/短语匹配,不支持子串——`AI` 匹配不到 `BRAID`。CJK 查询零命中时会回退到精确子串扫描(`filterEvents`),hint 会说明是否命中;多词中文短语仍受"连续中文=一个 token"的约束。
91
93
  - 索引文件单进程独占(官方后端的单写者 SQLite 约束)。
94
+ - 命中结果按原文照摘,**没有任何凭据或本地路径脱敏**——更早的会话里粘贴过的 token 或敏感路径可能被检索出来。目前只有默认 cwd 收窄与 `allowAllProjects: false` 两道闸;指纹识别/脱敏是后续增强。
95
+
96
+ ## 基准
97
+
98
+ 真机 headless profile 实测(Node 25,Apple Silicon,暖文件缓存)。
99
+
100
+ | 语料 | |
101
+ |---|---|
102
+ | 会话数 | 31 |
103
+ | 日志总事件 | 187,706(解压后约 104 MB,zstd 压缩后 49.7 MB) |
104
+ | 已索引文本事件 | 8,187 |
105
+ | FTS 索引体积 | 15 MB |
106
+
107
+ | 查询 | 命中 | 暖查询耗时(FTS5 `MATCH`) |
108
+ |---|---|---|
109
+ | 英文 `font` | 100(上限) | 1.1 ms |
110
+ | 英文 `resume template` | 46 | 0.6 ms |
111
+ | 中文 `字体` | 29 | 0.3 ms |
112
+ | 中文 `简历 模板` | 10 | 0.1 ms |
113
+
114
+ 暖查询(索引已在盘上)耗时 0.1~1.5 ms。冷启动首建:扫描 49.7 MB 日志约 4.7 s(解压 + 逐行扫描),把 8,187 条文本事件写入全新 FTS5 表约 190 ms;全新 profile 的首次 `recall` 在工具 10 s 超时内完成。此后重启复用持久化索引、增量 reconcile。
92
115
 
93
116
  ## 开发
94
117
 
package/lib/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { ToolDefinition } from "@deepseek-ai/dsh-tools";
2
- import { SessionEventSearchPage, SessionEventSearchRequest, SessionSearchExecContext, SessionSearchHit, SessionSearchPage, SessionSearchRequest, SessionTitleObservationResult } from "@deepseek-ai/dsh-session-query";
3
- import { JsonValue } from "@deepseek-ai/dsh-session";
2
+ import { SessionEventResultFilter, SessionEventSearchDocument, SessionEventSearchPage, SessionEventSearchRequest, SessionRecord, SessionSearchExecContext, SessionSearchHit, SessionSearchPage, SessionSearchRequest, SessionTitleObservationResult } from "@deepseek-ai/dsh-session-query";
3
+ import { JsonValue, SessionId } from "@deepseek-ai/dsh-session";
4
4
  import { Context } from "@deepseek-ai/cordis";
5
5
  import { ContentBlock } from "@deepseek-ai/dsh-llm";
6
6
  //#region src/config.d.ts
@@ -12,8 +12,12 @@ interface RecallConfig {
12
12
  defaultLimit?: number;
13
13
  /** Largest accepted page size. Default `10`. */
14
14
  maxLimit?: number;
15
- /** Teach the model the CJK tokenizer workaround on zero hits. Default `true`. */
15
+ /** Explain CJK zero-hit results with a hint line. Default `true`. */
16
16
  cjkHint?: boolean;
17
+ /** Fall back to an exact substring scan when a CJK query gets no full-text hits. Default `true`. */
18
+ cjkFallback?: boolean;
19
+ /** Max sessions to scan on a cross-session CJK fallback. Default `50`. */
20
+ cjkFallbackScanMax?: number;
17
21
  }
18
22
  /** Validated, fully defaulted configuration. */
19
23
  interface NormalizedRecallConfig {
@@ -21,6 +25,8 @@ interface NormalizedRecallConfig {
21
25
  readonly defaultLimit: number;
22
26
  readonly maxLimit: number;
23
27
  readonly cjkHint: boolean;
28
+ readonly cjkFallback: boolean;
29
+ readonly cjkFallbackScanMax: number;
24
30
  }
25
31
  /** Default, clamp, and cross-check every optional field. */
26
32
  declare function normalizeRecallConfig(config?: RecallConfig): NormalizedRecallConfig;
@@ -76,6 +82,8 @@ interface RecallQueryEngine {
76
82
  searchSessions(request: SessionSearchRequest, exec?: SessionSearchExecContext): Promise<SessionSearchPage<SessionSearchHit>>;
77
83
  searchEvents(request: SessionEventSearchRequest, exec?: SessionSearchExecContext): Promise<SessionEventSearchPage>;
78
84
  readTitleSnapshots(sessionIds: readonly string[], signal?: AbortSignal): Promise<SessionTitleObservationResult[]>;
85
+ listSessions(signal?: AbortSignal): Promise<SessionRecord[]>;
86
+ filterEvents(sessionId: SessionId, filters: readonly SessionEventResultFilter[]): Promise<SessionEventSearchDocument[]>;
79
87
  }
80
88
  declare const RECALL_TOOL_DESCRIPTION: string;
81
89
  /**
@@ -94,7 +102,12 @@ declare function renderRecallText(result: RecallResult): string;
94
102
  declare function recallContentBlocks(result: RecallResult): ContentBlock[];
95
103
  /** Replayable presentation payload consumed by `presentResult` on the UI plane. */
96
104
  declare function recallPresentationMeta(result: RecallResult): JsonValue;
97
- /** The zero-hit CJK tokenizer hint, or `null` when not applicable. */
105
+ /**
106
+ * Hint shown when the CJK substring fallback succeeded — explains why the
107
+ * full-text index missed the query and reports how many sessions matched.
108
+ */
109
+ declare function cjkFallbackHint(matched: number, enabled: boolean): string | null;
110
+ /** The zero-hit hint, shown only when both the full-text and substring paths miss. */
98
111
  declare function cjkZeroHitHint(query: string, zeroHits: boolean, enabled: boolean): string | null;
99
112
  //#endregion
100
113
  //#region src/util.d.ts
@@ -114,6 +127,12 @@ declare function hasCJK(text: string): boolean;
114
127
  declare function normalizeQuery(text: string): string;
115
128
  /** First line of `text` with control characters stripped, clipped to `limit` code points. */
116
129
  declare function firstLineClipped(text: string, limit: number): string;
130
+ /**
131
+ * Single-line snippet clipped around the first case-insensitive occurrence of
132
+ * `query`, with ellipses at either end when text was cut. Falls back to a
133
+ * head clip when the query is empty or absent.
134
+ */
135
+ declare function snippetAround(text: string, query: string, limit: number): string;
117
136
  //#endregion
118
137
  //#region src/index.d.ts
119
138
  declare const name = "session-recall";
@@ -121,4 +140,4 @@ declare const inject: string[];
121
140
  /** Plugin entry: mount the `recall` tool on the global tool registry. */
122
141
  declare function apply(ctx: Context, config?: RecallConfig): void;
123
142
  //#endregion
124
- export { type NormalizedRecallConfig, RECALL_TOOL_DESCRIPTION, type RecallArgs, type RecallBestMatch, type RecallConfig, type RecallItem, type RecallQueryEngine, type RecallResult, type RecallScope, apply, cjkZeroHitHint, clamp, createRecallTool, firstLineClipped, formatDate, hasCJK, id8, inject, name, normalizeQuery, normalizeRecallConfig, recallContentBlocks, recallPresentationMeta, renderRecallText };
143
+ export { type NormalizedRecallConfig, RECALL_TOOL_DESCRIPTION, type RecallArgs, type RecallBestMatch, type RecallConfig, type RecallItem, type RecallQueryEngine, type RecallResult, type RecallScope, apply, cjkFallbackHint, cjkZeroHitHint, clamp, createRecallTool, firstLineClipped, formatDate, hasCJK, id8, inject, name, normalizeQuery, normalizeRecallConfig, recallContentBlocks, recallPresentationMeta, renderRecallText, snippetAround };
package/lib/index.js CHANGED
@@ -12,7 +12,9 @@ function normalizeRecallConfig(config) {
12
12
  allowAllProjects: config?.allowAllProjects !== false,
13
13
  defaultLimit,
14
14
  maxLimit: Math.max(defaultLimit, intIn(config?.maxLimit, 10, 1, 25)),
15
- cjkHint: config?.cjkHint !== false
15
+ cjkHint: config?.cjkHint !== false,
16
+ cjkFallback: config?.cjkFallback !== false,
17
+ cjkFallbackScanMax: intIn(config?.cjkFallbackScanMax, 50, 1, 500)
16
18
  };
17
19
  }
18
20
  //#endregion
@@ -56,6 +58,21 @@ function firstLineClipped(text, limit) {
56
58
  }
57
59
  return out;
58
60
  }
61
+ /**
62
+ * Single-line snippet clipped around the first case-insensitive occurrence of
63
+ * `query`, with ellipses at either end when text was cut. Falls back to a
64
+ * head clip when the query is empty or absent.
65
+ */
66
+ function snippetAround(text, query, limit) {
67
+ const flat = text.replaceAll(/\s+/g, " ");
68
+ const q = normalizeQuery(query).toLowerCase();
69
+ const idx = q === "" ? -1 : flat.toLowerCase().indexOf(q);
70
+ if (idx < 0) return flat.slice(0, limit);
71
+ const pad = Math.floor(limit / 3);
72
+ const start = Math.max(0, idx - pad);
73
+ const end = Math.min(flat.length, start + limit);
74
+ return `${start > 0 ? "…" : ""}${flat.slice(start, end)}${end < flat.length ? "…" : ""}`;
75
+ }
59
76
  //#endregion
60
77
  //#region src/render.ts
61
78
  const SNIPPET_CHARS = 120;
@@ -108,10 +125,18 @@ function recallPresentationMeta(result) {
108
125
  }))
109
126
  };
110
127
  }
111
- /** The zero-hit CJK tokenizer hint, or `null` when not applicable. */
128
+ /**
129
+ * Hint shown when the CJK substring fallback succeeded — explains why the
130
+ * full-text index missed the query and reports how many sessions matched.
131
+ */
132
+ function cjkFallbackHint(matched, enabled) {
133
+ if (!enabled || matched <= 0) return null;
134
+ return `full-text search returned no CJK hits (SQLite FTS5 tokenizer "unicode61" does not segment CJK), so an exact substring scan over session text was used instead and matched ${matched} session(s).`;
135
+ }
136
+ /** The zero-hit hint, shown only when both the full-text and substring paths miss. */
112
137
  function cjkZeroHitHint(query, zeroHits, enabled) {
113
138
  if (!enabled || !zeroHits || !hasCJK(query)) return null;
114
- return "no hits for a CJK query — the FTS unicode61 tokenizer indexes uninterrupted CJK runs as single tokens. Retry with short space-separated keywords (e.g. \"简历 模板\" instead of \"我的简历模板在这里\"), or an English term.";
139
+ return `no matches for this CJK query. Try a shorter phrase, or an English/code term.`;
115
140
  }
116
141
  //#endregion
117
142
  //#region src/tool.ts
@@ -130,7 +155,7 @@ function cjkZeroHitHint(query, zeroHits, enabled) {
130
155
  const RECALL_TOOL_DESCRIPTION = [
131
156
  "Search the FULL TEXT of past and current session transcripts on this machine (your own conversation history with this user).",
132
157
  "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.",
133
- "Matches whole words/phrases (English and code identifiers work best); returns the best-matching event snippet per session plus the session id.",
158
+ "Matches whole words/phrases for English and code identifiers; a zero-hit Chinese (CJK) query automatically falls back to an exact substring scan. Returns the best-matching event snippet per session plus the session id.",
134
159
  "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.",
135
160
  "Scoping: by default only sessions started in the current project directory; pass all_projects=true to search everywhere.",
136
161
  "The first search after startup may be slow while the index builds."
@@ -213,6 +238,48 @@ function eventItems(page, sessionId) {
213
238
  }
214
239
  }));
215
240
  }
241
+ /** Snippet window kept consistent with the tool's text projection. */
242
+ const CJK_SNIPPET_CHARS = 120;
243
+ /**
244
+ * CJK substring-scan fallback for zero-hit full-text searches. SQLite FTS5's
245
+ * `unicode61` tokenizer treats an uninterrupted CJK run as one token, so a
246
+ * short Chinese phrase inside a longer sentence never matches the index. The
247
+ * official sessionQuery service's `filterEvents` text clause is a literal
248
+ * Unicode/case-insensitive regex scan that is deliberately independent of FTS
249
+ * providers, so scanning each scoped session with it recovers the exact
250
+ * substring matches the full-text index cannot see.
251
+ */
252
+ async function cjkScanSessions(engine, query, agentCwd, wantAll, scanMax, limit, signal) {
253
+ const all = await engine.listSessions(signal);
254
+ const candidates = !wantAll && agentCwd != null ? all.filter((record) => record.header.cwd === agentCwd) : all;
255
+ const items = [];
256
+ for (const record of candidates.slice(0, scanMax)) {
257
+ if (items.length >= limit) break;
258
+ const docs = await engine.filterEvents(record.header.id, [{
259
+ kind: "text",
260
+ text: query
261
+ }]);
262
+ if (docs.length === 0) continue;
263
+ const doc = docs[0];
264
+ if (doc === void 0) continue;
265
+ items.push({
266
+ sessionId: record.header.id,
267
+ id8: id8(record.header.id),
268
+ title: null,
269
+ createdAt: record.header.createdAt,
270
+ cwd: record.header.cwd ?? null,
271
+ live: record.live,
272
+ persisted: record.persisted,
273
+ bestMatch: {
274
+ seq: doc.seq,
275
+ type: doc.type,
276
+ time: doc.time,
277
+ snippet: snippetAround(doc.text, query, CJK_SNIPPET_CHARS)
278
+ }
279
+ });
280
+ }
281
+ return items;
282
+ }
216
283
  const nullableString = { oneOf: [{ type: "string" }, { type: "null" }] };
217
284
  const recallOutputSchema = {
218
285
  type: "object",
@@ -320,7 +387,30 @@ function createRecallTool(config, engine) {
320
387
  limit,
321
388
  cursor: brand(args.cursor)
322
389
  }, { signal: exec.signal });
323
- const items = eventItems(page, sessionId);
390
+ let items = eventItems(page, sessionId);
391
+ let hint = null;
392
+ if (items.length === 0 && hasCJK(query) && cfg.cjkFallback) {
393
+ const docs = await engine.filterEvents(sessionId, [{
394
+ kind: "text",
395
+ text: query
396
+ }]);
397
+ if (docs.length > 0) items = docs.slice(0, limit).map((doc) => ({
398
+ sessionId,
399
+ id8: id8(sessionId),
400
+ title: null,
401
+ createdAt: page.session.createdAt,
402
+ cwd: page.session.cwd ?? null,
403
+ live: true,
404
+ persisted: false,
405
+ bestMatch: {
406
+ seq: doc.seq,
407
+ type: doc.type,
408
+ time: doc.time,
409
+ snippet: snippetAround(doc.text, query, CJK_SNIPPET_CHARS)
410
+ }
411
+ }));
412
+ hint = items.length > 0 ? cjkFallbackHint(items.length, cfg.cjkHint) : cjkZeroHitHint(query, true, cfg.cjkHint);
413
+ } else if (items.length === 0) hint = cjkZeroHitHint(query, true, cfg.cjkHint);
324
414
  return {
325
415
  query,
326
416
  scope,
@@ -328,7 +418,7 @@ function createRecallTool(config, engine) {
328
418
  hasMore: page.nextCursor != null,
329
419
  items,
330
420
  nextCursor: page.nextCursor ?? null,
331
- hint: cjkZeroHitHint(query, items.length === 0, cfg.cjkHint)
421
+ hint
332
422
  };
333
423
  }
334
424
  const request = {
@@ -342,15 +432,27 @@ function createRecallTool(config, engine) {
342
432
  if (args.cursor != null && args.cursor !== "") request.cursor = brand(args.cursor);
343
433
  const page = await engine.searchSessions(request, { signal: exec.signal });
344
434
  const titles = await titlesFor(engine, page.items.map((hit) => hit.header.id), exec.signal);
345
- const items = toItems(page.items, titles);
435
+ let items = toItems(page.items, titles);
436
+ let hint = null;
437
+ let fallbackRan = false;
438
+ if (items.length === 0 && hasCJK(query) && cfg.cjkFallback) {
439
+ fallbackRan = true;
440
+ const scanned = await cjkScanSessions(engine, query, agentCwd, wantAll, cfg.cjkFallbackScanMax, limit, exec.signal);
441
+ const scanTitles = await titlesFor(engine, scanned.map((item) => item.sessionId), exec.signal);
442
+ items = scanned.map((item) => ({
443
+ ...item,
444
+ title: scanTitles.get(item.sessionId) ?? null
445
+ }));
446
+ hint = items.length > 0 ? cjkFallbackHint(items.length, cfg.cjkHint) : cjkZeroHitHint(query, true, cfg.cjkHint);
447
+ } else if (items.length === 0) hint = cjkZeroHitHint(query, true, cfg.cjkHint);
346
448
  return {
347
449
  query,
348
450
  scope,
349
451
  count: items.length,
350
- hasMore: page.nextCursor != null,
452
+ hasMore: !fallbackRan && page.nextCursor != null,
351
453
  items,
352
- nextCursor: page.nextCursor ?? null,
353
- hint: cjkZeroHitHint(query, items.length === 0, cfg.cjkHint)
454
+ nextCursor: !fallbackRan ? page.nextCursor ?? null : null,
455
+ hint
354
456
  };
355
457
  } catch (error) {
356
458
  return recallError(query, error);
@@ -387,4 +489,4 @@ function apply(ctx, config) {
387
489
  }, "session-recall lifecycle");
388
490
  }
389
491
  //#endregion
390
- export { RECALL_TOOL_DESCRIPTION, apply, cjkZeroHitHint, clamp, createRecallTool, firstLineClipped, formatDate, hasCJK, id8, inject, name, normalizeQuery, normalizeRecallConfig, recallContentBlocks, recallPresentationMeta, renderRecallText };
492
+ export { RECALL_TOOL_DESCRIPTION, apply, cjkFallbackHint, cjkZeroHitHint, clamp, createRecallTool, firstLineClipped, formatDate, hasCJK, id8, inject, name, normalizeQuery, normalizeRecallConfig, recallContentBlocks, recallPresentationMeta, renderRecallText, snippetAround };
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.1.0",
4
+ "version": "0.2.0",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },