dsh-session-recall 0.2.0 → 0.4.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 +41 -3
- package/README.zh.md +41 -3
- package/lib/index.d.ts +49 -4
- package/lib/index.js +233 -29
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -4,7 +4,32 @@ English | [中文](README.zh.md)
|
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/dsh-session-recall) [](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
|
+
|
|
9
|
+
## Positioning
|
|
10
|
+
|
|
11
|
+
`dsh-session-recall` is a **transcript retrieval layer** focused on correctness and control.
|
|
12
|
+
|
|
13
|
+
- It returns evidence from original session logs, not synthesized summaries.
|
|
14
|
+
- It enforces explicit retrieval scope (cwd by default, opt-in widening).
|
|
15
|
+
- It favors deterministic behavior over "smart" but lossy memory extraction.
|
|
16
|
+
|
|
17
|
+
If you need agent memory orchestration, use a memory framework; if you need bounded, auditable lookup over historical transcripts, use this plugin.
|
|
18
|
+
|
|
19
|
+
## Competitive context
|
|
20
|
+
|
|
21
|
+
| Capability focus | Memory frameworks | Generic transcript search | `dsh-session-recall` |
|
|
22
|
+
|---|---|---|---|
|
|
23
|
+
| Retrieval target | Derived memory objects | Varies by implementation | **Original session transcript events** |
|
|
24
|
+
| Scope control | Framework-specific | Often coarse | **cwd-scoped default + explicit `all_projects` gate** |
|
|
25
|
+
| CJK behavior | Framework-specific | Often tokenizer-limited | **FTS + CJK zero-hit substring fallback** |
|
|
26
|
+
| Output contract | Usually framework-native | Varies | **Typed `recall` result with stable fields/hints** |
|
|
27
|
+
|
|
28
|
+
## Roadmap
|
|
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
|
+
- **P2: evidence handoff** — one-click bridge to session export for matched sessions.
|
|
8
33
|
|
|
9
34
|
## Why
|
|
10
35
|
|
|
@@ -33,7 +58,7 @@ recall({ query, session_id }) → search the events of one session
|
|
|
33
58
|
recall({ query, limit, cursor }) → page through results
|
|
34
59
|
```
|
|
35
60
|
|
|
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
|
|
61
|
+
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
62
|
|
|
38
63
|
## Scoping (the authorization gap)
|
|
39
64
|
|
|
@@ -86,10 +111,23 @@ Plugin row config (all optional):
|
|
|
86
111
|
|
|
87
112
|
Every failure returns a friendly `hint` instead of a raw exception: a disabled index explains the two config keys needed, a stale cursor tells the model to restart without one, an unknown `session_id` suggests discovering sessions first. Title enrichment is best-effort — a failed title batch degrades to untitled rows, never a failed search.
|
|
88
113
|
|
|
114
|
+
## Scope policy & redaction (v0.4)
|
|
115
|
+
|
|
116
|
+
Deployment-level controls for what the model may read back:
|
|
117
|
+
|
|
118
|
+
| Option | Values | Default | Effect |
|
|
119
|
+
|---|---|---|---|
|
|
120
|
+
| `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
|
+
| `cwdAllowlist` | list of paths | (none) | Only sessions started in these directories are searchable; the calling cwd itself must be listed. |
|
|
122
|
+
| `cwdDenylist` | list of paths | (none) | These directories are never searchable. Deny wins over allow. |
|
|
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
|
+
|
|
125
|
+
All three gates apply uniformly to cross-session hits, the CJK fallback scan, and `session_id` reads — no bypass route.
|
|
126
|
+
|
|
89
127
|
## Known limitations
|
|
90
128
|
|
|
91
129
|
- 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
|
|
130
|
+
- `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
131
|
- One process must own the index file (single-writer SQLite, per the official backend).
|
|
94
132
|
- 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
133
|
|
package/README.zh.md
CHANGED
|
@@ -4,7 +4,32 @@
|
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/dsh-session-recall) [](LICENSE)
|
|
6
6
|
|
|
7
|
-
DeepSeek Harness
|
|
7
|
+
DeepSeek Harness 的**确定性跨会话全文检索**插件:注册模型可调用的 `recall` 工具,让 agent 能**检索自己过往的会话原文**——"上周修的那个 bug"、"简历选的什么字体"——全部通过可信的 `ctx.sessionQuery` 缝完成。
|
|
8
|
+
|
|
9
|
+
## 项目定位
|
|
10
|
+
|
|
11
|
+
`dsh-session-recall` 是一个强调正确性与边界控制的**会话检索层**。
|
|
12
|
+
|
|
13
|
+
- 检索对象是原始会话日志,不是二次总结内容。
|
|
14
|
+
- 默认按 cwd 收敛权限范围,放宽范围必须显式声明。
|
|
15
|
+
- 追求可解释、可复现的检索行为,而不是"看起来更聪明"但有损的记忆抽取。
|
|
16
|
+
|
|
17
|
+
如果你要做长期记忆编排,请用记忆框架;如果你要做可审计、有权限边界的历史检索,请用本插件。
|
|
18
|
+
|
|
19
|
+
## 竞品视角
|
|
20
|
+
|
|
21
|
+
| 能力重心 | 记忆框架类插件 | 通用检索类插件 | `dsh-session-recall` |
|
|
22
|
+
|---|---|---|---|
|
|
23
|
+
| 检索对象 | 推导后的记忆结构 | 视实现而定 | **原始会话事件文本** |
|
|
24
|
+
| 范围控制 | 框架内约束 | 常较粗粒度 | **默认 cwd + 显式 `all_projects` 闸门** |
|
|
25
|
+
| CJK 体验 | 视实现而定 | 常受分词限制 | **FTS + CJK 零命中子串回退** |
|
|
26
|
+
| 输出契约 | 框架内部格式 | 不统一 | **类型化 `recall` 结果 + 稳定 hint** |
|
|
27
|
+
|
|
28
|
+
## 路线图
|
|
29
|
+
|
|
30
|
+
- **P1:排序策略可配** —— 在 FTS 相关度之上增加时间衰减、会话 pin 权重。
|
|
31
|
+
- **P1:查询诊断元数据** —— 返回命中来源(fts/cjk-fallback/filters)与扫描预算。
|
|
32
|
+
- **P2:证据联动导出** —— 命中后可一键触发对应会话导出。
|
|
8
33
|
|
|
9
34
|
## 为什么做这个
|
|
10
35
|
|
|
@@ -33,7 +58,7 @@ recall({ query, session_id }) → 只搜指定会话内的事件
|
|
|
33
58
|
recall({ query, limit, cursor }) → 翻页
|
|
34
59
|
```
|
|
35
60
|
|
|
36
|
-
每条命中带会话 id、标题(尽力补全)、日期、命中摘录;结果在 Web UI 里渲染成原生搜索卡片(`SearchMatchesResultView`)。因为 FTS 的 `unicode61` 分词器会把连续中文当成一个 token,短中文短语一旦嵌在长句里就匹配不到索引——所以 CJK
|
|
61
|
+
每条命中带会话 id、标题(尽力补全)、日期、命中摘录;结果在 Web UI 里渲染成原生搜索卡片(`SearchMatchesResultView`)。因为 FTS 的 `unicode61` 分词器会把连续中文当成一个 token,短中文短语一旦嵌在长句里就匹配不到索引——所以 CJK 查询零命中时会自动回退到对会话文本的子串扫描(走 `sessionQuery.filterEvents` 的字面文本子句),空格拆出的每个词都必须命中,因此 `简历 模板` 也能找回 `简历模板`;hint 会说明这条回退路径是否命中。
|
|
37
62
|
|
|
38
63
|
## 授权边界(官方明确留给工具层的责任)
|
|
39
64
|
|
|
@@ -86,10 +111,23 @@ dsh plugin --profile web add github:kittimzhe/dsh-session-recall
|
|
|
86
111
|
|
|
87
112
|
所有失败都返回友好的 `hint` 而不是裸异常:索引未开启会说明需要哪两个配置键;游标失效会告诉模型不带游标重开一次;`session_id` 不存在会建议先做跨会话搜索。标题补全是尽力而为——标题批量读取失败只降级为"无标题"行,绝不让搜索失败。
|
|
88
113
|
|
|
114
|
+
## 范围策略与脱敏(v0.4)
|
|
115
|
+
|
|
116
|
+
部署级权限控制——模型能读回什么,由配置说了算:
|
|
117
|
+
|
|
118
|
+
| 配置 | 取值 | 默认 | 效果 |
|
|
119
|
+
|---|---|---|---|
|
|
120
|
+
| `redactionMode` | `off` / `mask` / `hash` | `off` | 对标题与摘录中疑似密钥的文本(Bearer 头、前缀式 API key、私钥块、邮箱)脱敏。`hash` 用确定性摘要 `#xxxxxxxx`(同一密钥同一标记)保持可比性。结果带 `redacted` 计数。 |
|
|
121
|
+
| `cwdAllowlist` | 路径列表 | (无) | 只检索这些目录下启动的会话;当前项目目录本身也必须在列表内。 |
|
|
122
|
+
| `cwdDenylist` | 路径列表 | (无) | 这些目录永不检索。deny 优先于 allow。 |
|
|
123
|
+
| `allProjectsPolicy` | `allow` / `deny` / `confirm` | `allow` | `deny` 忽略 `all_projects` 并向模型说明;`confirm` 走官方 `@deepseek-ai/dsh-user-approval` 接缝向用户请求批准——无应答者时 fail-closed。 |
|
|
124
|
+
|
|
125
|
+
三道闸门统一作用于跨会话命中、CJK 回退扫描和 `session_id` 直读——没有绕行路径。
|
|
126
|
+
|
|
89
127
|
## 已知限制
|
|
90
128
|
|
|
91
129
|
- 启动后第一次搜索会扫全量日志建索引(工具描述里已警告模型);之后增量更新。
|
|
92
|
-
- `unicode61` 按完整 token/短语匹配,不支持子串——`AI` 匹配不到 `BRAID`。CJK
|
|
130
|
+
- `unicode61` 按完整 token/短语匹配,不支持子串——`AI` 匹配不到 `BRAID`。CJK 查询零命中时会回退到子串扫描(`filterEvents`),空格分隔的各词按 AND 语义都必须命中,因此 `简历 模板` 也能找回 `简历模板`;hint 会说明是否命中。
|
|
93
131
|
- 索引文件单进程独占(官方后端的单写者 SQLite 约束)。
|
|
94
132
|
- 命中结果按原文照摘,**没有任何凭据或本地路径脱敏**——更早的会话里粘贴过的 token 或敏感路径可能被检索出来。目前只有默认 cwd 收窄与 `allowAllProjects: false` 两道闸;指纹识别/脱敏是后续增强。
|
|
95
133
|
|
package/lib/index.d.ts
CHANGED
|
@@ -1,10 +1,26 @@
|
|
|
1
|
-
import { ToolDefinition } from "@deepseek-ai/dsh-tools";
|
|
1
|
+
import { ToolDefinition, ToolRunContext } from "@deepseek-ai/dsh-tools";
|
|
2
2
|
import { SessionEventResultFilter, SessionEventSearchDocument, SessionEventSearchPage, SessionEventSearchRequest, SessionRecord, SessionSearchExecContext, SessionSearchHit, SessionSearchPage, SessionSearchRequest, SessionTitleObservationResult } from "@deepseek-ai/dsh-session-query";
|
|
3
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
|
+
//#region src/redact.d.ts
|
|
7
|
+
type RedactionMode = 'off' | 'mask' | 'hash';
|
|
8
|
+
declare const REDACTION_MODES: readonly RedactionMode[];
|
|
9
|
+
/**
|
|
10
|
+
* Redact one piece of text. Returns the new text plus how many replacements
|
|
11
|
+
* happened (0 when mode is `off`).
|
|
12
|
+
*/
|
|
13
|
+
declare function redactText(text: string, mode: RedactionMode): {
|
|
14
|
+
text: string;
|
|
15
|
+
count: number;
|
|
16
|
+
};
|
|
17
|
+
/** Normalize an untrusted config value into a valid mode (default `off`). */
|
|
18
|
+
declare function normalizeRedactionMode(value: unknown): RedactionMode;
|
|
19
|
+
//#endregion
|
|
6
20
|
//#region src/config.d.ts
|
|
7
|
-
/**
|
|
21
|
+
/** What happens when the model passes `all_projects=true`. */
|
|
22
|
+
type AllProjectsPolicy = 'allow' | 'deny' | 'confirm';
|
|
23
|
+
declare const ALL_PROJECTS_POLICIES: readonly AllProjectsPolicy[];
|
|
8
24
|
interface RecallConfig {
|
|
9
25
|
/** Honor the tool's `all_projects` argument. Default `true`. */
|
|
10
26
|
allowAllProjects?: boolean;
|
|
@@ -18,6 +34,18 @@ interface RecallConfig {
|
|
|
18
34
|
cjkFallback?: boolean;
|
|
19
35
|
/** Max sessions to scan on a cross-session CJK fallback. Default `50`. */
|
|
20
36
|
cjkFallbackScanMax?: number;
|
|
37
|
+
/** Redact secret-looking text in snippets and titles. Default `'off'`. */
|
|
38
|
+
redactionMode?: RedactionMode;
|
|
39
|
+
/** When non-empty, only sessions started in these project directories are searchable. Default: no restriction. */
|
|
40
|
+
cwdAllowlist?: readonly string[];
|
|
41
|
+
/** Sessions started in these project directories are never searchable. Default: none. */
|
|
42
|
+
cwdDenylist?: readonly string[];
|
|
43
|
+
/**
|
|
44
|
+
* `all_projects` gate: `'allow'` (default, previous behavior), `'deny'`
|
|
45
|
+
* (ignored, with a model-facing hint), or `'confirm'` (the user must
|
|
46
|
+
* approve through the `@deepseek-ai/dsh-user-approval` seam; fail-closed).
|
|
47
|
+
*/
|
|
48
|
+
allProjectsPolicy?: AllProjectsPolicy;
|
|
21
49
|
}
|
|
22
50
|
/** Validated, fully defaulted configuration. */
|
|
23
51
|
interface NormalizedRecallConfig {
|
|
@@ -27,9 +55,15 @@ interface NormalizedRecallConfig {
|
|
|
27
55
|
readonly cjkHint: boolean;
|
|
28
56
|
readonly cjkFallback: boolean;
|
|
29
57
|
readonly cjkFallbackScanMax: number;
|
|
58
|
+
readonly redactionMode: RedactionMode;
|
|
59
|
+
readonly cwdAllowlist: readonly string[];
|
|
60
|
+
readonly cwdDenylist: readonly string[];
|
|
61
|
+
readonly allProjectsPolicy: AllProjectsPolicy;
|
|
30
62
|
}
|
|
31
63
|
/** Default, clamp, and cross-check every optional field. */
|
|
32
64
|
declare function normalizeRecallConfig(config?: RecallConfig): NormalizedRecallConfig;
|
|
65
|
+
/** Whether a session cwd is searchable under the allowlist/denylist policy. */
|
|
66
|
+
declare function cwdAllowed(cwd: string | null | undefined, cfg: NormalizedRecallConfig): boolean;
|
|
33
67
|
//#endregion
|
|
34
68
|
//#region src/types.d.ts
|
|
35
69
|
/** Canonical value types for the `recall` tool's declared output. */
|
|
@@ -66,6 +100,8 @@ interface RecallResult {
|
|
|
66
100
|
items: RecallItem[];
|
|
67
101
|
nextCursor: string | null;
|
|
68
102
|
hint: string | null;
|
|
103
|
+
/** How many secret-looking fields were redacted in this result (0 when redaction is off). */
|
|
104
|
+
redacted: number;
|
|
69
105
|
}
|
|
70
106
|
/** The typed model-facing arguments after schema validation. */
|
|
71
107
|
interface RecallArgs {
|
|
@@ -86,11 +122,20 @@ interface RecallQueryEngine {
|
|
|
86
122
|
filterEvents(sessionId: SessionId, filters: readonly SessionEventResultFilter[]): Promise<SessionEventSearchDocument[]>;
|
|
87
123
|
}
|
|
88
124
|
declare const RECALL_TOOL_DESCRIPTION: string;
|
|
125
|
+
/** The approval verdict vocabulary mirrored from `@deepseek-ai/dsh-user-approval`. */
|
|
126
|
+
type RecallApprovalVerdict = 'allowed-once' | 'rejected' | 'cancelled' | 'unavailable';
|
|
127
|
+
/**
|
|
128
|
+
* Optional user-approval seam for the `all_projects` gate (`allProjectsPolicy:
|
|
129
|
+
* 'confirm'`). Receives the tool run context (for the agent) and a
|
|
130
|
+
* human-readable reason; `'allowed-once'` is the only grant. Wired from
|
|
131
|
+
* `ctx.approval` in the plugin entry; fail-closed when absent.
|
|
132
|
+
*/
|
|
133
|
+
type RecallApprover = (exec: ToolRunContext, reason: string) => Promise<RecallApprovalVerdict>;
|
|
89
134
|
/**
|
|
90
135
|
* Build the `recall` ToolDefinition around a concrete sessionQuery engine.
|
|
91
136
|
* Pure construction — no registration happens here.
|
|
92
137
|
*/
|
|
93
|
-
declare function createRecallTool(config?: RecallConfig, engine?: RecallQueryEngine): ToolDefinition;
|
|
138
|
+
declare function createRecallTool(config?: RecallConfig, engine?: RecallQueryEngine, approver?: RecallApprover): ToolDefinition;
|
|
94
139
|
//#endregion
|
|
95
140
|
//#region src/render.d.ts
|
|
96
141
|
/**
|
|
@@ -140,4 +185,4 @@ declare const inject: string[];
|
|
|
140
185
|
/** Plugin entry: mount the `recall` tool on the global tool registry. */
|
|
141
186
|
declare function apply(ctx: Context, config?: RecallConfig): void;
|
|
142
187
|
//#endregion
|
|
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 };
|
|
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 };
|
package/lib/index.js
CHANGED
|
@@ -1,10 +1,87 @@
|
|
|
1
1
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
2
2
|
import { SessionSearchCursor } from "@deepseek-ai/dsh-session-query";
|
|
3
3
|
import { SessionId } from "@deepseek-ai/dsh-session";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
//#region src/redact.ts
|
|
6
|
+
/**
|
|
7
|
+
* Redaction for recall output text (snippets and titles).
|
|
8
|
+
*
|
|
9
|
+
* Three modes: `off` (default, unchanged), `mask` (placeholder), `hash`
|
|
10
|
+
* (deterministic 8-hex digest — the same secret always hashes to the same
|
|
11
|
+
* marker, so equal markers prove equality without revealing content).
|
|
12
|
+
*
|
|
13
|
+
* Pattern set covers the credential shapes that actually show up in agent
|
|
14
|
+
* transcripts: bearer headers, prefixed API keys, private-key blocks, and
|
|
15
|
+
* email addresses. It is best-effort, not a secrecy guarantee.
|
|
16
|
+
*/
|
|
17
|
+
const REDACTION_MODES = [
|
|
18
|
+
"off",
|
|
19
|
+
"mask",
|
|
20
|
+
"hash"
|
|
21
|
+
];
|
|
22
|
+
const RULES = [
|
|
23
|
+
{
|
|
24
|
+
pattern: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,
|
|
25
|
+
mask: "[PRIVATE KEY REDACTED]"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
pattern: /Bearer\s+[A-Za-z0-9\-._~+/]+=*/g,
|
|
29
|
+
mask: "Bearer [REDACTED]"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
pattern: /\b(?:sk-[A-Za-z0-9]{20,}|ghp_[A-Za-z0-9]{30,}|gho_[A-Za-z0-9]{30,}|github_pat_[A-Za-z0-9_]{30,}|xox[baprs]-[A-Za-z0-9-]{10,}|AKIA[A-Z0-9]{16})\b/g,
|
|
33
|
+
mask: "[REDACTED]"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
pattern: /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g,
|
|
37
|
+
mask: "[EMAIL]"
|
|
38
|
+
}
|
|
39
|
+
];
|
|
40
|
+
function hashMarker(match) {
|
|
41
|
+
return `#${createHash("sha256").update(match).digest("hex").slice(0, 8)}`;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Redact one piece of text. Returns the new text plus how many replacements
|
|
45
|
+
* happened (0 when mode is `off`).
|
|
46
|
+
*/
|
|
47
|
+
function redactText(text, mode) {
|
|
48
|
+
if (mode === "off" || text.length === 0) return {
|
|
49
|
+
text,
|
|
50
|
+
count: 0
|
|
51
|
+
};
|
|
52
|
+
let count = 0;
|
|
53
|
+
let out = text;
|
|
54
|
+
for (const rule of RULES) out = out.replace(rule.pattern, (match) => {
|
|
55
|
+
count += 1;
|
|
56
|
+
return mode === "mask" ? rule.mask : hashMarker(match);
|
|
57
|
+
});
|
|
58
|
+
return {
|
|
59
|
+
text: out,
|
|
60
|
+
count
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/** Normalize an untrusted config value into a valid mode (default `off`). */
|
|
64
|
+
function normalizeRedactionMode(value) {
|
|
65
|
+
return typeof value === "string" && REDACTION_MODES.includes(value) ? value : "off";
|
|
66
|
+
}
|
|
67
|
+
//#endregion
|
|
68
|
+
//#region src/config.ts
|
|
69
|
+
/** Plugin-row configuration for dsh-session-recall. */
|
|
70
|
+
const ALL_PROJECTS_POLICIES = [
|
|
71
|
+
"allow",
|
|
72
|
+
"deny",
|
|
73
|
+
"confirm"
|
|
74
|
+
];
|
|
4
75
|
function intIn(value, fallback, lo, hi) {
|
|
5
76
|
if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
|
|
6
77
|
return Math.min(hi, Math.max(lo, Math.trunc(value)));
|
|
7
78
|
}
|
|
79
|
+
function stringList(value) {
|
|
80
|
+
return Array.isArray(value) ? value.filter((entry) => typeof entry === "string" && entry.length > 0) : [];
|
|
81
|
+
}
|
|
82
|
+
function normalizePolicy(value) {
|
|
83
|
+
return typeof value === "string" && ALL_PROJECTS_POLICIES.includes(value) ? value : "allow";
|
|
84
|
+
}
|
|
8
85
|
/** Default, clamp, and cross-check every optional field. */
|
|
9
86
|
function normalizeRecallConfig(config) {
|
|
10
87
|
const defaultLimit = intIn(config?.defaultLimit, 5, 1, 10);
|
|
@@ -14,9 +91,20 @@ function normalizeRecallConfig(config) {
|
|
|
14
91
|
maxLimit: Math.max(defaultLimit, intIn(config?.maxLimit, 10, 1, 25)),
|
|
15
92
|
cjkHint: config?.cjkHint !== false,
|
|
16
93
|
cjkFallback: config?.cjkFallback !== false,
|
|
17
|
-
cjkFallbackScanMax: intIn(config?.cjkFallbackScanMax, 50, 1, 500)
|
|
94
|
+
cjkFallbackScanMax: intIn(config?.cjkFallbackScanMax, 50, 1, 500),
|
|
95
|
+
redactionMode: normalizeRedactionMode(config?.redactionMode),
|
|
96
|
+
cwdAllowlist: stringList(config?.cwdAllowlist),
|
|
97
|
+
cwdDenylist: stringList(config?.cwdDenylist),
|
|
98
|
+
allProjectsPolicy: normalizePolicy(config?.allProjectsPolicy)
|
|
18
99
|
};
|
|
19
100
|
}
|
|
101
|
+
/** Whether a session cwd is searchable under the allowlist/denylist policy. */
|
|
102
|
+
function cwdAllowed(cwd, cfg) {
|
|
103
|
+
if (cwd == null || cwd === "") return cfg.cwdAllowlist.length === 0;
|
|
104
|
+
if (cfg.cwdDenylist.includes(cwd)) return false;
|
|
105
|
+
if (cfg.cwdAllowlist.length > 0 && !cfg.cwdAllowlist.includes(cwd)) return false;
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
20
108
|
//#endregion
|
|
21
109
|
//#region src/util.ts
|
|
22
110
|
/** Small pure helpers shared by the recall tool and its renderers. */
|
|
@@ -47,6 +135,10 @@ function hasCJK(text) {
|
|
|
47
135
|
function normalizeQuery(text) {
|
|
48
136
|
return text.trim().replaceAll(/\s+/g, " ");
|
|
49
137
|
}
|
|
138
|
+
/** Split into whitespace-separated terms, dropping empty pieces. */
|
|
139
|
+
function splitTerms(text) {
|
|
140
|
+
return text.split(/\s+/u).filter((term) => term.length > 0);
|
|
141
|
+
}
|
|
50
142
|
/** First line of `text` with control characters stripped, clipped to `limit` code points. */
|
|
51
143
|
function firstLineClipped(text, limit) {
|
|
52
144
|
const line = text.split("\n", 1)[0] ?? "";
|
|
@@ -155,9 +247,10 @@ function cjkZeroHitHint(query, zeroHits, enabled) {
|
|
|
155
247
|
const RECALL_TOOL_DESCRIPTION = [
|
|
156
248
|
"Search the FULL TEXT of past and current session transcripts on this machine (your own conversation history with this user).",
|
|
157
249
|
"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
|
|
250
|
+
"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
251
|
"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
|
-
"Scoping: by default only sessions started in the current project directory; pass all_projects=true to search everywhere.",
|
|
252
|
+
"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
|
+
"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.",
|
|
161
254
|
"The first search after startup may be slow while the index builds."
|
|
162
255
|
].join(" ");
|
|
163
256
|
function errCode(error) {
|
|
@@ -188,7 +281,8 @@ function recallError(query, error) {
|
|
|
188
281
|
hasMore: false,
|
|
189
282
|
items: [],
|
|
190
283
|
nextCursor: null,
|
|
191
|
-
hint: friendlyError(error)
|
|
284
|
+
hint: friendlyError(error),
|
|
285
|
+
redacted: 0
|
|
192
286
|
};
|
|
193
287
|
}
|
|
194
288
|
function brand(value) {
|
|
@@ -241,6 +335,23 @@ function eventItems(page, sessionId) {
|
|
|
241
335
|
/** Snippet window kept consistent with the tool's text projection. */
|
|
242
336
|
const CJK_SNIPPET_CHARS = 120;
|
|
243
337
|
/**
|
|
338
|
+
* Decompose a query into ANDed literal-text clauses. A space-separated CJK
|
|
339
|
+
* query like "简历 模板" must match each term as a substring (so it recovers
|
|
340
|
+
* "简历模板"), not a whitespace-joined literal (which would require the space
|
|
341
|
+
* to be present verbatim). `highlight` is the first term, guaranteed present
|
|
342
|
+
* whenever the ANDed scan matches.
|
|
343
|
+
*/
|
|
344
|
+
function cjkTextFilters(query) {
|
|
345
|
+
const terms = splitTerms(query);
|
|
346
|
+
return {
|
|
347
|
+
filters: terms.map((term) => ({
|
|
348
|
+
kind: "text",
|
|
349
|
+
text: term
|
|
350
|
+
})),
|
|
351
|
+
highlight: terms[0] ?? query
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
244
355
|
* CJK substring-scan fallback for zero-hit full-text searches. SQLite FTS5's
|
|
245
356
|
* `unicode61` tokenizer treats an uninterrupted CJK run as one token, so a
|
|
246
357
|
* short Chinese phrase inside a longer sentence never matches the index. The
|
|
@@ -249,16 +360,14 @@ const CJK_SNIPPET_CHARS = 120;
|
|
|
249
360
|
* providers, so scanning each scoped session with it recovers the exact
|
|
250
361
|
* substring matches the full-text index cannot see.
|
|
251
362
|
*/
|
|
252
|
-
async function cjkScanSessions(engine, query, agentCwd, wantAll, scanMax, limit, signal) {
|
|
363
|
+
async function cjkScanSessions(engine, query, agentCwd, wantAll, cfg, scanMax, limit, signal) {
|
|
253
364
|
const all = await engine.listSessions(signal);
|
|
254
|
-
const candidates = !wantAll && agentCwd != null ? all.filter((record) => record.header.cwd === agentCwd) : all;
|
|
365
|
+
const candidates = (!wantAll && agentCwd != null ? all.filter((record) => record.header.cwd === agentCwd) : all).filter((record) => cwdAllowed(record.header.cwd, cfg));
|
|
255
366
|
const items = [];
|
|
256
367
|
for (const record of candidates.slice(0, scanMax)) {
|
|
257
368
|
if (items.length >= limit) break;
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
text: query
|
|
261
|
-
}]);
|
|
369
|
+
const { filters, highlight } = cjkTextFilters(query);
|
|
370
|
+
const docs = await engine.filterEvents(record.header.id, filters);
|
|
262
371
|
if (docs.length === 0) continue;
|
|
263
372
|
const doc = docs[0];
|
|
264
373
|
if (doc === void 0) continue;
|
|
@@ -274,7 +383,7 @@ async function cjkScanSessions(engine, query, agentCwd, wantAll, scanMax, limit,
|
|
|
274
383
|
seq: doc.seq,
|
|
275
384
|
type: doc.type,
|
|
276
385
|
time: doc.time,
|
|
277
|
-
snippet: snippetAround(doc.text,
|
|
386
|
+
snippet: snippetAround(doc.text, highlight, CJK_SNIPPET_CHARS)
|
|
278
387
|
}
|
|
279
388
|
});
|
|
280
389
|
}
|
|
@@ -324,15 +433,57 @@ const recallOutputSchema = {
|
|
|
324
433
|
}
|
|
325
434
|
},
|
|
326
435
|
nextCursor: nullableString,
|
|
327
|
-
hint: nullableString
|
|
436
|
+
hint: nullableString,
|
|
437
|
+
redacted: { type: "integer" }
|
|
328
438
|
}
|
|
329
439
|
};
|
|
330
440
|
/**
|
|
331
441
|
* Build the `recall` ToolDefinition around a concrete sessionQuery engine.
|
|
332
442
|
* Pure construction — no registration happens here.
|
|
333
443
|
*/
|
|
334
|
-
function createRecallTool(config, engine) {
|
|
444
|
+
function createRecallTool(config, engine, approver) {
|
|
335
445
|
const cfg = normalizeRecallConfig(config);
|
|
446
|
+
/** Apply configured redaction to titles and snippets; report a model-facing note. */
|
|
447
|
+
function applyRedaction(items) {
|
|
448
|
+
if (cfg.redactionMode === "off") return {
|
|
449
|
+
items,
|
|
450
|
+
redacted: 0,
|
|
451
|
+
hint: null
|
|
452
|
+
};
|
|
453
|
+
let redacted = 0;
|
|
454
|
+
const out = items.map((item) => {
|
|
455
|
+
const title = item.title == null ? null : redactText(item.title, cfg.redactionMode);
|
|
456
|
+
const snippet = redactText(item.bestMatch.snippet, cfg.redactionMode);
|
|
457
|
+
redacted += (title?.count ?? 0) + snippet.count;
|
|
458
|
+
return {
|
|
459
|
+
...item,
|
|
460
|
+
title: title == null ? null : title.text,
|
|
461
|
+
bestMatch: {
|
|
462
|
+
...item.bestMatch,
|
|
463
|
+
snippet: snippet.text
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
});
|
|
467
|
+
const hint = redacted > 0 ? `${redacted} secret-looking field(s) were redacted from this result (mode: ${cfg.redactionMode}).` : null;
|
|
468
|
+
return {
|
|
469
|
+
items: out,
|
|
470
|
+
redacted,
|
|
471
|
+
hint
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
function joinHints(...parts) {
|
|
475
|
+
const kept = parts.filter((part) => part != null && part !== "");
|
|
476
|
+
return kept.length > 0 ? kept.join(" ") : null;
|
|
477
|
+
}
|
|
478
|
+
/** Ask the approval seam whether this all_projects call may proceed. */
|
|
479
|
+
async function decideAllProjects(exec, query) {
|
|
480
|
+
if (approver == null) return "unavailable";
|
|
481
|
+
try {
|
|
482
|
+
return await approver(exec, `recall: search sessions from ALL project directories (query: "${query}")`);
|
|
483
|
+
} catch {
|
|
484
|
+
return "unavailable";
|
|
485
|
+
}
|
|
486
|
+
}
|
|
336
487
|
return defineTool({
|
|
337
488
|
name: "recall",
|
|
338
489
|
description: RECALL_TOOL_DESCRIPTION,
|
|
@@ -372,12 +523,31 @@ function createRecallTool(config, engine) {
|
|
|
372
523
|
if (query === "") return recallError(query, Object.assign(/* @__PURE__ */ new Error("empty query"), { code: "SESSION_QUERY_INVALID_QUERY" }));
|
|
373
524
|
const limit = clamp(Math.trunc(args.limit ?? cfg.defaultLimit), 1, cfg.maxLimit);
|
|
374
525
|
const agentCwd = exec.agent?.session.header?.cwd ?? null;
|
|
375
|
-
const
|
|
526
|
+
const scopeHints = [];
|
|
527
|
+
let wantAll = false;
|
|
528
|
+
if (args.session_id == null && args.all_projects === true) {
|
|
529
|
+
if (!cfg.allowAllProjects || cfg.allProjectsPolicy === "deny") scopeHints.push("all_projects was ignored: cross-project search is disabled by this deployment. Searched the current project only.");
|
|
530
|
+
else if (cfg.allProjectsPolicy === "confirm") {
|
|
531
|
+
const verdict = await decideAllProjects(exec, query);
|
|
532
|
+
if (verdict === "allowed-once") wantAll = true;
|
|
533
|
+
else scopeHints.push(`all_projects was not approved (${verdict}); searched the current project only.`);
|
|
534
|
+
} else wantAll = true;
|
|
535
|
+
}
|
|
376
536
|
const scope = {
|
|
377
537
|
cwd: agentCwd,
|
|
378
538
|
allProjects: wantAll,
|
|
379
539
|
sessionId: args.session_id ?? null
|
|
380
540
|
};
|
|
541
|
+
if (args.session_id == null && !wantAll && agentCwd != null && !cwdAllowed(agentCwd, cfg)) return {
|
|
542
|
+
query,
|
|
543
|
+
scope,
|
|
544
|
+
count: 0,
|
|
545
|
+
hasMore: false,
|
|
546
|
+
items: [],
|
|
547
|
+
nextCursor: null,
|
|
548
|
+
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
|
|
550
|
+
};
|
|
381
551
|
try {
|
|
382
552
|
if (args.session_id != null && args.session_id !== "") {
|
|
383
553
|
const sessionId = SessionId(args.session_id);
|
|
@@ -387,13 +557,21 @@ function createRecallTool(config, engine) {
|
|
|
387
557
|
limit,
|
|
388
558
|
cursor: brand(args.cursor)
|
|
389
559
|
}, { signal: exec.signal });
|
|
560
|
+
if (!cwdAllowed(page.session.cwd ?? null, cfg)) return {
|
|
561
|
+
query,
|
|
562
|
+
scope,
|
|
563
|
+
count: 0,
|
|
564
|
+
hasMore: false,
|
|
565
|
+
items: [],
|
|
566
|
+
nextCursor: null,
|
|
567
|
+
hint: "that session belongs to a project directory excluded by the recall scope policy (cwd allowlist/denylist).",
|
|
568
|
+
redacted: 0
|
|
569
|
+
};
|
|
390
570
|
let items = eventItems(page, sessionId);
|
|
391
571
|
let hint = null;
|
|
392
572
|
if (items.length === 0 && hasCJK(query) && cfg.cjkFallback) {
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
text: query
|
|
396
|
-
}]);
|
|
573
|
+
const { filters, highlight } = cjkTextFilters(query);
|
|
574
|
+
const docs = await engine.filterEvents(sessionId, filters);
|
|
397
575
|
if (docs.length > 0) items = docs.slice(0, limit).map((doc) => ({
|
|
398
576
|
sessionId,
|
|
399
577
|
id8: id8(sessionId),
|
|
@@ -406,19 +584,21 @@ function createRecallTool(config, engine) {
|
|
|
406
584
|
seq: doc.seq,
|
|
407
585
|
type: doc.type,
|
|
408
586
|
time: doc.time,
|
|
409
|
-
snippet: snippetAround(doc.text,
|
|
587
|
+
snippet: snippetAround(doc.text, highlight, CJK_SNIPPET_CHARS)
|
|
410
588
|
}
|
|
411
589
|
}));
|
|
412
590
|
hint = items.length > 0 ? cjkFallbackHint(items.length, cfg.cjkHint) : cjkZeroHitHint(query, true, cfg.cjkHint);
|
|
413
591
|
} else if (items.length === 0) hint = cjkZeroHitHint(query, true, cfg.cjkHint);
|
|
592
|
+
const red = applyRedaction(items);
|
|
414
593
|
return {
|
|
415
594
|
query,
|
|
416
595
|
scope,
|
|
417
|
-
count: items.length,
|
|
596
|
+
count: red.items.length,
|
|
418
597
|
hasMore: page.nextCursor != null,
|
|
419
|
-
items,
|
|
598
|
+
items: red.items,
|
|
420
599
|
nextCursor: page.nextCursor ?? null,
|
|
421
|
-
hint
|
|
600
|
+
hint: joinHints(hint, red.hint),
|
|
601
|
+
redacted: red.redacted
|
|
422
602
|
};
|
|
423
603
|
}
|
|
424
604
|
const request = {
|
|
@@ -432,12 +612,12 @@ function createRecallTool(config, engine) {
|
|
|
432
612
|
if (args.cursor != null && args.cursor !== "") request.cursor = brand(args.cursor);
|
|
433
613
|
const page = await engine.searchSessions(request, { signal: exec.signal });
|
|
434
614
|
const titles = await titlesFor(engine, page.items.map((hit) => hit.header.id), exec.signal);
|
|
435
|
-
let items = toItems(page.items, titles);
|
|
615
|
+
let items = toItems(page.items, titles).filter((item) => cwdAllowed(item.cwd, cfg));
|
|
436
616
|
let hint = null;
|
|
437
617
|
let fallbackRan = false;
|
|
438
618
|
if (items.length === 0 && hasCJK(query) && cfg.cjkFallback) {
|
|
439
619
|
fallbackRan = true;
|
|
440
|
-
const scanned = await cjkScanSessions(engine, query, agentCwd, wantAll, cfg.cjkFallbackScanMax, limit, exec.signal);
|
|
620
|
+
const scanned = await cjkScanSessions(engine, query, agentCwd, wantAll, cfg, cfg.cjkFallbackScanMax, limit, exec.signal);
|
|
441
621
|
const scanTitles = await titlesFor(engine, scanned.map((item) => item.sessionId), exec.signal);
|
|
442
622
|
items = scanned.map((item) => ({
|
|
443
623
|
...item,
|
|
@@ -445,14 +625,16 @@ function createRecallTool(config, engine) {
|
|
|
445
625
|
}));
|
|
446
626
|
hint = items.length > 0 ? cjkFallbackHint(items.length, cfg.cjkHint) : cjkZeroHitHint(query, true, cfg.cjkHint);
|
|
447
627
|
} else if (items.length === 0) hint = cjkZeroHitHint(query, true, cfg.cjkHint);
|
|
628
|
+
const red = applyRedaction(items);
|
|
448
629
|
return {
|
|
449
630
|
query,
|
|
450
631
|
scope,
|
|
451
|
-
count: items.length,
|
|
632
|
+
count: red.items.length,
|
|
452
633
|
hasMore: !fallbackRan && page.nextCursor != null,
|
|
453
|
-
items,
|
|
634
|
+
items: red.items,
|
|
454
635
|
nextCursor: !fallbackRan ? page.nextCursor ?? null : null,
|
|
455
|
-
hint
|
|
636
|
+
hint: joinHints(hint, ...scopeHints, red.hint),
|
|
637
|
+
redacted: red.redacted
|
|
456
638
|
};
|
|
457
639
|
} catch (error) {
|
|
458
640
|
return recallError(query, error);
|
|
@@ -482,11 +664,33 @@ function createRecallTool(config, engine) {
|
|
|
482
664
|
//#region src/index.ts
|
|
483
665
|
const name = "session-recall";
|
|
484
666
|
const inject = ["tools", "sessionQuery"];
|
|
667
|
+
/**
|
|
668
|
+
* Build the optional approval seam for `allProjectsPolicy: 'confirm'`: ask
|
|
669
|
+
* `ctx.approval` when the service is composed and the call carries an agent;
|
|
670
|
+
* fail closed (`'unavailable'`) otherwise. Never throws.
|
|
671
|
+
*/
|
|
672
|
+
function makeApprover(ctx) {
|
|
673
|
+
const approval = ctx.approval;
|
|
674
|
+
if (approval == null || typeof approval.request !== "function") return void 0;
|
|
675
|
+
return async (exec, reason) => {
|
|
676
|
+
const agent = exec.agent;
|
|
677
|
+
if (agent == null) return "unavailable";
|
|
678
|
+
try {
|
|
679
|
+
return await approval.request({
|
|
680
|
+
agent,
|
|
681
|
+
toolName: "recall",
|
|
682
|
+
reason
|
|
683
|
+
});
|
|
684
|
+
} catch {
|
|
685
|
+
return "unavailable";
|
|
686
|
+
}
|
|
687
|
+
};
|
|
688
|
+
}
|
|
485
689
|
/** Plugin entry: mount the `recall` tool on the global tool registry. */
|
|
486
690
|
function apply(ctx, config) {
|
|
487
691
|
ctx.effect(function* () {
|
|
488
|
-
yield ctx.tools.register(createRecallTool(config, ctx.sessionQuery));
|
|
692
|
+
yield ctx.tools.register(createRecallTool(config, ctx.sessionQuery, makeApprover(ctx)));
|
|
489
693
|
}, "session-recall lifecycle");
|
|
490
694
|
}
|
|
491
695
|
//#endregion
|
|
492
|
-
export { RECALL_TOOL_DESCRIPTION, apply, cjkFallbackHint, cjkZeroHitHint, clamp, createRecallTool, firstLineClipped, formatDate, hasCJK, id8, inject, name, normalizeQuery, normalizeRecallConfig, recallContentBlocks, recallPresentationMeta, renderRecallText, snippetAround };
|
|
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 };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-session-recall",
|
|
3
|
-
"description": "
|
|
4
|
-
"version": "0.
|
|
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.0",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|