dsh-engramory 0.2.3 → 0.2.6

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/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Beijing Tinqiao Technology Co., Ltd.
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Beijing Tinqiao Technology Co., Ltd.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,124 +1,150 @@
1
- [English](README.md) | [简体中文](README.zh-CN.md)
2
-
3
- # dsh-engramory
4
-
5
- [![dsh-xray](https://img.shields.io/endpoint?url=https%3A%2F%2Funstone.github.io%2Fdsh-xray%2Fbadge%2Ftinqiao-oss__engramory.json)](https://unstone.github.io/dsh-xray/registry.html#tinqiao-oss__engramory)
6
-
7
- <sub>`C2` is the level `manifest.bundle.patch` puts every mountable dsh plugin at (74.6% of the scanned ecosystem); it is the only flag on this card — no `exec`, `eval`, install script, outbound domain, or environment read.</sub>
8
-
9
- Curated, file-based long-term memory for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) —
10
- plain markdown notes you can open with anything, one store shared across every host you
11
- use, and an index cap that is a **real refusal** rather than a request. (If the store
12
- lives inside a project's git repo it must be git-ignored — memories carry machine-local
13
- detail; see SKILL.md §1. A *dedicated private* repo for the store itself is fine.)
14
-
15
- Part of [Engramory](https://github.com/tinqiao-oss/engramory).
16
-
17
- ## Why a plugin, when an `AGENTS.md` block already works
18
-
19
- The block does carry the discipline, and for recall that is enough. Two things it cannot
20
- do:
21
-
22
- **The cap becomes deterministic.** Engramory keeps its index under 200 lines / 25 KB
23
- because the index loads every session and everything past the cap silently stops being
24
- recalled. On most hosts that limit is rules plus a checker the agent has to remember to
25
- run. dsh exposes `ctx.tools.guard()` — a synchronous, **monotonic** refusal: once a guard
26
- returns a reason, no later listener can turn it back into an allow. So here the limit is
27
- enforced, not requested. Use **0.2.1 or later**: 0.2.0 shipped before profile installs
28
- worked upstream and carried an `inject` declaration this Cordis does not accept — it
29
- installed but never activated (issue #8). Outside Claude Code, dsh is the first host with
30
- the shim actually written (a few others expose equivalent pre-write seams — see
31
- PORTING.md — but have no shim yet).
32
-
33
- **The protocol arrives with the plugin.** The skill is registered at runtime through
34
- dsh's skill registry — via a reactive `ctx.inject(['skills'], …)` child, so a profile
35
- without a registry still gets the cap and one whose registry activates late still gets
36
- the skill — which means it does not depend on landing files in one of the five skill
37
- roots dsh scans, a path that is easy to get subtly wrong and fails silently when you do.
38
-
39
- ## Install
40
-
41
- ```sh
42
- dsh plugin --profile <name> add dsh-engramory
43
- ```
44
-
45
- That is the whole install. The package ships a `dsh.bundle` manifest pointing at its own
46
- `cordis.patch.yml`, so the row is inserted into the profile's plugin tree for you — no
47
- hand-editing. To change a default, patch the EXISTING row by id in your profile's own
48
- patch layer — do **not** use a second `- insert:` (insert always appends, so you would
49
- end up with two engramory rows and the original caps still enforced):
50
-
51
- ```yaml
52
- - id: engramory
53
- config:
54
- indexName: MEMORY.md
55
- maxLines: 200
56
- maxBytes: 25600
57
- ```
58
-
59
- A patch replaces the targeted row's whole `config`, so list every key you mean to keep.
60
-
61
- For the store itself and the always-on block, use the installer in the Engramory repo
62
- (`python tools/engramory_init.py dsh --install-skill`). This plugin enforces the cap and
63
- supplies the protocol; it does not create the store.
64
-
65
- ## Configuration
66
-
67
- | Field | Default | Meaning |
68
- |---|---|---|
69
- | `indexName` | `MEMORY.md` | Basename treated as the memory index (case-insensitive; both `file_path` and `str_replace_editor`'s `path` are checked). An empty or non-string value falls back to the default. Nothing else is inspected. |
70
- | `maxLines` | `200` | Hard line cap. Non-positive or non-finite values fall back to the default. |
71
- | `maxBytes` | `25600` | Hard UTF-8 byte cap (25 KB). |
72
- | `indexPath` | unset | Absolute path of the ONE index to guard. Without it the guard matches on basename alone, so an unrelated `MEMORY.md` in another project is gated too; set this and only that exact file is. Compared by identity (symlinks and `..` resolved; case-folded on Windows only), and a path that does not exist yet still has its first write guarded. |
73
- | `registerSkill` | `true` | Set `false` to keep the cap but skip the runtime skill. |
74
- | `skill` | built-in | Replace the skill body with your own markdown. |
75
-
76
- ## What the guard actually does
77
-
78
- | Call | Decision |
79
- |---|---|
80
- | `write` (or `str_replace_editor` `create`) ending within caps | allow |
81
- | `write`/`create` that GROWS the index past a cap | **deny**, naming the numbers and what to compact |
82
- | `write`/`create` that SHRINKS or keeps an over-cap index | allow — incremental compaction (210 → 205 → 198) must stay possible |
83
- | Edit carrying `old_str`/`new_str`: result simulated | judged by the RESULT, same grow/shrink rule as a write |
84
- | Unsimulable partial (e.g. `insert`) on an over-cap index | **deny**, telling the agent a shrinking whole-file write passes |
85
- | Unsimulable partial on a healthy index | allow |
86
- | `read`, `view`, or any unknown tool | allow — recall must never be blocked, even over cap |
87
- | Any write to any other file | allow — not the guard's business |
88
- | Missing/unreadable current index on a whole-file write | treated as EMPTY (mirrors the Python guard) — a within-caps write passes, an over-cap first write is refused |
89
- | Missing/unreadable index on a partial edit | allow — a guard must never block work over a path it cannot read |
90
- | Malformed execution (no arguments, non-string path/content) | allow — not recognisably a write |
91
-
92
- The deny rule mirrors `hooks/engramory_index_guard.py` exactly: refuse only a result
93
- that is over a cap AND grew past the current file. The line count ignores a trailing
94
- newline, so an index sitting exactly at the cap stays writable.
95
-
96
- ## Known limits
97
-
98
- - **An unsimulable partial that crosses the cap from under it is not caught.** `insert`
99
- and friends do not carry the resulting text. The breach is caught by the next
100
- whole-file write, or by `engramory_check.py`. What is guaranteed: an already-over
101
- index cannot be grown further, and a shrinking write always passes.
102
- - **The tool roster follows dsh's documented tool-fs contract** (`write`/`edit` with
103
- `file_path`, `str_replace_editor` with `path`) and is deliberately conservative:
104
- unknown tools pass.
105
- - **0.2.0 never activated; 0.2.1 is the first version that runs.** While the rc.6
106
- preview bug blocked all third-party profile installs upstream, the only coverage a
107
- plugin could have was mocks — and 0.2.0's mock hid two activation bugs: an
108
- older-Cordis `{ required, optional }` inject shape (read as waiting for services
109
- literally named `required`/`optional`, pending forever) and a bare `ctx.skills` read
110
- of an undeclared service (throws under Cordis' reflective context). The first field
111
- install caught both the moment installs became possible (issue #8). 0.2.1 fixes them,
112
- verified end to end on a live dsh 0.1.0-rc.7 web profile (0.2.0 reproduces the boot
113
- failure byte for byte; 0.2.1 boots and serves) and against the vendored
114
- `@deepseek-ai/cordis` resolver (activation with, without, and with a late-mounted
115
- skill registry). The test mock now mirrors the reflective-context access rules, and
116
- the guard's decision table stays covered by `node --test` (28 cases, run in
117
- Engramory's CI).
118
- - dsh is a developer preview and its plugin API can change. This plugin deliberately
119
- touches only `ctx.tools.guard()` and a reactive `ctx.inject(['skills'], …)` child
120
- that registers the skill, so it stays cheap to fix.
121
-
122
- ## License
123
-
124
- MIT — see the [Engramory repository](https://github.com/tinqiao-oss/engramory).
1
+ [English](README.md) | [简体中文](README.zh-CN.md)
2
+
3
+ # dsh-engramory
4
+
5
+ [![dsh-xray](https://img.shields.io/endpoint?url=https%3A%2F%2Funstone.github.io%2Fdsh-xray%2Fbadge%2Ftinqiao-oss__engramory.json)](https://unstone.github.io/dsh-xray/registry.html#tinqiao-oss__engramory)
6
+
7
+ <sub>`C2` is the level `manifest.bundle.patch` puts every mountable dsh plugin at (74.6% of the scanned ecosystem); it is the only flag on this card — no `exec`, `eval`, install script, outbound domain, or environment read.</sub>
8
+
9
+ Curated, file-based long-term memory for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) —
10
+ plain markdown notes you can open with anything, one store shared across every host you
11
+ use, and an index cap that is a **real refusal** rather than a request. (If the store
12
+ lives inside a project's git repo it must be git-ignored — memories carry machine-local
13
+ detail; see SKILL.md §1. A *dedicated private* repo for the store itself is fine.)
14
+
15
+ Part of [Engramory](https://github.com/tinqiao-oss/engramory).
16
+
17
+ ## Why a plugin, when an `AGENTS.md` block already works
18
+
19
+ The block does carry the discipline, and for recall that is enough. Two things it cannot
20
+ do:
21
+
22
+ **The cap becomes deterministic.** Engramory keeps its index under 200 lines / 25 KB
23
+ because the index loads every session and everything past the cap silently stops being
24
+ recalled. On most hosts that limit is rules plus a checker the agent has to remember to
25
+ run. dsh exposes `ctx.tools.guard()` — a synchronous, **monotonic** refusal: once a guard
26
+ returns a reason, no later listener can turn it back into an allow. So here the limit is
27
+ enforced, not requested. Use **0.2.6 or later**: 0.2.0 shipped before profile installs
28
+ worked upstream and carried an `inject` declaration this Cordis does not accept — it
29
+ installed but never activated (issue #8) — and up to 0.2.5 an edit could slip past the
30
+ cap on a CRLF index, or on dsh 0.2 through a relative path after a cd (see Known limits). Outside Claude Code, dsh is the first host with
31
+ the shim actually written (a few others expose equivalent pre-write seams — see
32
+ PORTING.md — but have no shim yet).
33
+
34
+ **The protocol arrives with the plugin.** The skill is registered at runtime through
35
+ dsh's skill registry — via a reactive `ctx.inject(['skills'], …)` child, so a profile
36
+ without a registry still gets the cap and one whose registry activates late still gets
37
+ the skill — which means it does not depend on landing files in one of the five skill
38
+ roots dsh scans, a path that is easy to get subtly wrong and fails silently when you do.
39
+
40
+ ## Install
41
+
42
+ ```sh
43
+ dsh plugin --profile <name> add dsh-engramory
44
+ ```
45
+
46
+ That is the whole install. The package ships a `dsh.bundle` manifest pointing at its own
47
+ `cordis.patch.yml`, so the row is inserted into the profile's plugin tree for you — no
48
+ hand-editing. To change a default, patch the EXISTING row by id in your profile's own
49
+ patch layer — do **not** use a second `- insert:` (insert always appends, so you would
50
+ end up with two engramory rows and the original caps still enforced):
51
+
52
+ ```yaml
53
+ - id: engramory
54
+ config:
55
+ indexName: MEMORY.md
56
+ maxLines: 200
57
+ maxBytes: 25600
58
+ ```
59
+
60
+ A patch replaces the targeted row's whole `config`, so list every key you mean to keep.
61
+
62
+ For the store itself and the always-on block, use the installer in the Engramory repo
63
+ (`python tools/engramory_init.py dsh --install-skill`). This plugin enforces the cap and
64
+ supplies the protocol; it does not create the store.
65
+
66
+ ## Configuration
67
+
68
+ | Field | Default | Meaning |
69
+ |---|---|---|
70
+ | `indexName` | `MEMORY.md` | Basename treated as the memory index (case-insensitive; both `file_path` and `str_replace_editor`'s `path` are checked). An empty or non-string value falls back to the default. Nothing else is inspected. |
71
+ | `maxLines` | `200` | Hard line cap. Non-positive or non-finite values fall back to the default. |
72
+ | `maxBytes` | `25600` | Hard UTF-8 byte cap (25 KB). |
73
+ | `indexPath` | unset | Absolute path of the ONE index to guard. Without it the guard matches on basename alone, so an unrelated `MEMORY.md` in another project is gated too; set this and only that exact file is. Compared by identity (symlinks and `..` resolved; case-folded on Windows only), and a path that does not exist yet still has its first write guarded. |
74
+ | `registerSkill` | `true` | Set `false` to keep the cap but skip the runtime skill. |
75
+ | `skill` | built-in | Replace the skill body with your own markdown. |
76
+
77
+ ## What the guard actually does
78
+
79
+ | Call | Decision |
80
+ |---|---|
81
+ | `write` (or `str_replace_editor` `create`) ending within caps | allow |
82
+ | `write`/`create` that GROWS the index past a cap | **deny**, naming the numbers and what to compact |
83
+ | `write`/`create` that SHRINKS or keeps an over-cap index | allow — incremental compaction (210 → 205 → 198) must stay possible |
84
+ | Edit carrying `old_str`/`new_str`: result simulated | judged by the RESULT, same grow/shrink rule as a write |
85
+ | Unsimulable partial (e.g. `insert`) on an over-cap index | **deny**, telling the agent a shrinking whole-file write passes |
86
+ | Unsimulable partial on a healthy index | allow |
87
+ | `read`, `view`, or any unknown tool | allow — recall must never be blocked, even over cap |
88
+ | Any write to any other file | allow — not the guard's business |
89
+ | Missing/unreadable current index on a whole-file write | treated as EMPTY (mirrors the Python guard) — a within-caps write passes, an over-cap first write is refused |
90
+ | Missing/unreadable index on a partial edit | allow — a guard must never block work over a path it cannot read |
91
+ | Malformed execution (no arguments, non-string path/content) | allow — not recognisably a write |
92
+
93
+ The deny rule mirrors `hooks/engramory_index_guard.py` exactly: refuse only a result
94
+ that is over a cap AND grew past the current file. The line count ignores a trailing
95
+ newline, so an index sitting exactly at the cap stays writable.
96
+
97
+ ## Known limits
98
+
99
+ - **An unsimulable partial that crosses the cap from under it is not caught.** `insert`
100
+ and friends do not carry the resulting text. The breach is caught by the next
101
+ whole-file write, or by `engramory_check.py`. What is guaranteed: an already-over
102
+ index cannot be grown further, and a shrinking write always passes.
103
+ - **The tool roster follows dsh's documented tool-fs contract** (`write`/`edit` with
104
+ `file_path`, `str_replace_editor` with `path`) and is deliberately conservative:
105
+ unknown tools pass.
106
+ - **0.2.0 never activated; 0.2.1 is the first version that runs.** While the rc.6
107
+ preview bug blocked all third-party profile installs upstream, the only coverage a
108
+ plugin could have was mocks — and 0.2.0's mock hid two activation bugs: an
109
+ older-Cordis `{ required, optional }` inject shape (read as waiting for services
110
+ literally named `required`/`optional`, pending forever) and a bare `ctx.skills` read
111
+ of an undeclared service (throws under Cordis' reflective context). The first field
112
+ install caught both the moment installs became possible (issue #8). 0.2.1 fixes them,
113
+ verified end to end on a live dsh 0.1.0-rc.7 web profile (0.2.0 reproduces the boot
114
+ failure byte for byte; 0.2.1 boots and serves) and against the vendored
115
+ `@deepseek-ai/cordis` resolver (activation with, without, and with a late-mounted
116
+ skill registry). The test mock now mirrors the reflective-context access rules, and
117
+ the guard's decision table stays covered by `node --test` (37 cases, run in
118
+ Engramory's CI).
119
+ - **A relative path is measured where dsh writes it.** dsh 0.2 gives every session its
120
+ own working directory (moved by its `working_directory` tool) and resolves a
121
+ relative `file_path` against that. Up to 0.2.5 the guard resolved against the
122
+ process's cwd instead, so after a cd an edit of a bare `MEMORY.md` read no current
123
+ file and went through unmeasured — on dsh 0.2.1-rc.2 it grew the index to 253
124
+ lines. 0.2.6 reads the session's directory from dsh's `workingDirectory` service
125
+ when the profile has one, and falls back to the process's cwd when it does not
126
+ (every 0.1.x release).
127
+ - **A CRLF index is edited the way dsh edits it.** dsh matches an edit on
128
+ LF-normalised text and writes the result back in the file's own line endings. Up
129
+ to 0.2.5 the guard matched the raw bytes, so on a CRLF `MEMORY.md` — the usual
130
+ shape on Windows — an LF `old_string` looked absent, the edit was predicted to
131
+ change nothing, and dsh applied it. 0.2.6 normalises the same way and sizes a CRLF
132
+ index in CRLF bytes.
133
+ - **Checked against the dsh you are likely running, not only the one it was written
134
+ on.** `tests/dsh_e2e/run.py` in the Engramory repo installs a dsh release into a
135
+ throwaway prefix, adds this plugin with `dsh plugin add`, and drives one headless
136
+ task against a scripted stand-in for the DeepSeek API: a 260-line write and an edit
137
+ past the cap must come back refused by this guard with nothing reaching the disk, a
138
+ small write must land byte for byte, the skill must be advertised, every tool dsh
139
+ offers must already be classified (a new one could write past the cap), and removal
140
+ must leave a profile that still boots. The releases it passed on are listed in
141
+ `package.json` under `dsh.compatibility.dshReleases`; a unit test rejects a
142
+ `compatible` there without a recorded run of the same plugin version and code.
143
+ - dsh is a developer preview and its plugin API can change. This plugin deliberately
144
+ touches only `ctx.tools.guard()` and two reactive `ctx.inject()` children — one
145
+ registers the skill, one reads each session's working directory — so it stays cheap
146
+ to fix.
147
+
148
+ ## License
149
+
150
+ MIT — see the [Engramory repository](https://github.com/tinqiao-oss/engramory).
package/README.zh-CN.md CHANGED
@@ -1,111 +1,129 @@
1
- [English](README.md) | **简体中文**
2
-
3
- # dsh-engramory
4
-
5
- [![dsh-xray](https://img.shields.io/endpoint?url=https%3A%2F%2Funstone.github.io%2Fdsh-xray%2Fbadge%2Ftinqiao-oss__engramory.json)](https://unstone.github.io/dsh-xray/registry.html#tinqiao-oss__engramory)
6
-
7
- <sub>`C2` 是 `manifest.bundle.patch` 给每个可挂载 dsh 插件定的档位(被扫描生态里 74.6% 都在这一档);它是这张卡片上**唯一**的标记 —— 没有 `exec`、没有 `eval`、没有安装脚本、没有外连域名、不读环境变量。</sub>
8
-
9
- 给 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 用的**策展式、文件式**长期记忆 ——
10
- 纯 markdown 笔记,随便什么工具都能打开;一个记忆库被你用的所有宿主共用;索引上限是一次
11
- **真正的拒绝**,而不是一句请求。(记忆库若位于某个项目的 git 仓库内,必须 git-ignore ——
12
- 记忆里带着本机独有的细节,见 SKILL.md §1。给记忆库单独开一个*私有仓*则完全可以。)
13
-
14
- 隶属于 [Engramory](https://github.com/tinqiao-oss/engramory)。
15
-
16
- ## 已经有 `AGENTS.md` 常驻块了,为什么还要插件
17
-
18
- 那个常驻块确实承载了纪律,单论召回也够用了。但有两件事它做不到:
19
-
20
- **上限从此是确定性的。** Engramory 把索引控制在 200 行 / 25 KB 以内,是因为索引每次会话
21
- 都要加载,超出的部分会**悄无声息地不再被召回**。在多数宿主上,这个限制只是"规则 + 一个
22
- 要靠 agent 自己记得去跑的校验器"。而 dsh 暴露了 `ctx.tools.guard()` —— 一个同步的、
23
- **单调**的否决:守卫一旦返回理由,后面任何监听器都无法把它翻回放行。所以在这里,上限是被
24
- **强制执行**的,不是被请求的。请用 **0.2.1 或更高版本**:0.2.0 发布时上游的 profile 安装还
25
- 没打通,而且它的 `inject` 声明是这版 Cordis 不接受的写法 —— 装得上,但**永远激活不了**
26
- (issue #8)。在 Claude Code 之外,dsh 是第一个真正把这层垫片写出来的宿主(另有几个宿主
27
- 同样暴露了等价的写前接缝,见 PORTING.md,但还没有垫片实现)。
28
-
29
- **协议随插件一起到达。** skill 是在运行时通过 dsh 的 skill 注册表注册的 —— 走一个响应式的
30
- `ctx.inject(['skills'], …)` 子上下文,所以没有注册表的 profile 照样拿到上限,而注册表晚一步
31
- 挂载的 profile 也照样拿得到 skill。这意味着它**不依赖**把文件放进 dsh 扫描的那五个 skill 根
32
- 目录之一 —— 那条路很容易在细微处放错,而一旦放错就是**静默失败**。
33
-
34
- ## 安装
35
-
36
- ```sh
37
- dsh plugin --profile <名字> add dsh-engramory
38
- ```
39
-
40
- 装到这里就完了。这个包自带 `dsh.bundle` manifest 指向它自己的 `cordis.patch.yml`,所以那一行
41
- 会被自动插进 profile 的插件树里 —— 不需要手改配置。要改默认值,请在你自己 profile 的 patch
42
- 层里**按 id 修改那一行已存在的配置**,**不要**再写一个 `- insert:`(insert 永远是追加,你会得到
43
- 两行 engramory,而原来那行的上限依旧在生效):
44
-
45
- ```yaml
46
- - id: engramory
47
- config:
48
- indexName: MEMORY.md
49
- maxLines: 200
50
- maxBytes: 25600
51
- ```
52
-
53
- 一次 patch 会**整体替换**目标行的 `config`,所以你想保留的键要逐个列全。
54
-
55
- 记忆库本身和常驻块,请用 Engramory 仓库里的安装器
56
- (`python tools/engramory_init.py dsh --install-skill`)。本插件负责强制上限、提供协议;
57
- **它不创建记忆库**。
58
-
59
- ## 配置
60
-
61
- | 字段 | 默认值 | 含义 |
62
- |---|---|---|
63
- | `indexName` | `MEMORY.md` | 被当作记忆索引的文件名(不区分大小写;`file_path` 与 `str_replace_editor` 的 `path` 都会检查)。空值或非字符串会回退到默认值。除此之外不检查任何东西。 |
64
- | `maxLines` | `200` | 硬性行数上限。非正数或非有限值回退到默认值。 |
65
- | `maxBytes` | `25600` | 硬性 UTF-8 字节上限(25 KB)。 |
66
- | `indexPath` | 未设置 | 要守护的**那一个**索引的绝对路径。不设它时,守卫只按文件名匹配,于是**别的项目里一个无关的 `MEMORY.md` 也会被拦**;设了它就只有这个确切的文件会被守。比较走的是身份而非拼写(symlink 与 `..` 都会解析;仅在 Windows 上折叠大小写),而且即使文件尚不存在,它的第一次写入照样受守护。 |
67
- | `registerSkill` | `true` | 设为 `false` 则保留上限、但跳过运行时 skill。 |
68
- | `skill` | 内置 | 用你自己的 markdown 替换 skill 正文。 |
69
-
70
- ## 守卫到底做了什么
71
-
72
- | 调用 | 判决 |
73
- |---|---|
74
- | `write`(或 `str_replace_editor` 的 `create`)且结果在上限内 | 放行 |
75
- | `write`/`create` 会让索引**增长**并越过上限 | **拒绝**,并报出具体数字和该压缩什么 |
76
- | `write`/`create` 让一个已超限的索引**缩小或持平** | 放行 —— 增量压缩(210 → 205 → 198)必须走得通 |
77
- | 带 `old_str`/`new_str` 的 edit:结果可模拟 | 按**结果**判定,增长/缩小规则与整文件写入相同 |
78
- | 无法模拟的部分写入(如 `insert`)落在已超限的索引上 | **拒绝**,并告诉 agent:缩小的整文件写入是放行的 |
79
- | 无法模拟的部分写入落在健康的索引上 | 放行 |
80
- | `read`、`view`,以及任何未知工具 | 放行 —— 哪怕已超限,召回也绝不能被挡住 |
81
- | 对任何其他文件的写入 | 放行 —— 不关守卫的事 |
82
- | 整文件写入时当前索引缺失/读不到 | 视为**空**(与 Python 版守卫一致)—— 上限内的写入放行,而首次就超限的写入被拒 |
83
- | 部分编辑时索引缺失/读不到 | 放行 —— 守卫绝不能因为一个自己读不了的路径而挡住工作 |
84
- | 畸形的执行(没有 arguments、路径/内容不是字符串) | 放行 —— 它压根不像一次写入 |
85
-
86
- 拒绝规则与 `hooks/engramory_index_guard.py` **完全一致**:只拒绝那种结果既超过上限、
87
- 又比当前文件更大的写入。行数统计忽略结尾换行,所以一个正好卡在上限的索引仍然可写。
88
-
89
- ## 已知限制
90
-
91
- - **从上限之下一跃越过上限的、无法模拟的部分写入,抓不住。** `insert` 这类调用并不携带
92
- 写入后的文本。这种越界会由下一次整文件写入、或 `engramory_check.py` 抓到。能保证的是:
93
- 一个**已经超限**的索引不会被继续撑大,而缩小的写入永远放行。
94
- - **工具名单遵循 dsh 文档化的 tool-fs 契约**(`write`/`edit` 用 `file_path`,
95
- `str_replace_editor` 用 `path`),并且刻意保守:未知工具一律放行。
96
- - **0.2.0 从未激活成功;0.2.1 才是第一个真正跑得起来的版本。** 在 rc.6 预览版那个 bug
97
- 堵住上游所有第三方 profile 安装期间,插件能有的覆盖只有 mock —— 而 0.2.0 的 mock 恰好
98
- 掩盖了两个激活期 bug:一个是旧版 Cordis 的 `{ required, optional }` inject 写法(被解读成
99
- 在等待名字就叫 `required`/`optional` 的服务,于是永远 pending),另一个是裸读未声明的
100
- `ctx.skills`(在 Cordis 的反射式上下文下会直接抛错)。安装一旦成为可能,**首个真实安装
101
- 当场把两个都暴露了**(issue #8)。0.2.1 修掉了它们,并在一个真跑着的 dsh 0.1.0-rc.7 web
102
- profile 上端到端验证过(0.2.0 逐字节复现启动失败;0.2.1 能启动并提供服务),也对着 dsh
103
- 内置的 `@deepseek-ai/cordis` 解析器验过(注册表存在、不存在、以及晚挂载三种情况下的激活)。
104
- 测试用的 mock 现在会照着反射式上下文的访问规则来,守卫的判决表则由 `node --test`
105
- 持续覆盖(28 个用例,在 Engramory 的 CI 里跑)。
106
- - dsh 目前是开发者预览版,插件 API 可能变化。本插件刻意只碰 `ctx.tools.guard()` 和一个
107
- 注册 skill 的响应式 `ctx.inject(['skills'], …)` 子上下文,所以要跟着改也很便宜。
108
-
109
- ## 许可
110
-
111
- MIT —— 见 [Engramory 仓库](https://github.com/tinqiao-oss/engramory)。
1
+ [English](README.md) | **简体中文**
2
+
3
+ # dsh-engramory
4
+
5
+ [![dsh-xray](https://img.shields.io/endpoint?url=https%3A%2F%2Funstone.github.io%2Fdsh-xray%2Fbadge%2Ftinqiao-oss__engramory.json)](https://unstone.github.io/dsh-xray/registry.html#tinqiao-oss__engramory)
6
+
7
+ <sub>`C2` 是 `manifest.bundle.patch` 给每个可挂载 dsh 插件定的档位(被扫描生态里 74.6% 都在这一档);它是这张卡片上**唯一**的标记 —— 没有 `exec`、没有 `eval`、没有安装脚本、没有外连域名、不读环境变量。</sub>
8
+
9
+ 给 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 用的**策展式、文件式**长期记忆 ——
10
+ 纯 markdown 笔记,随便什么工具都能打开;一个记忆库被你用的所有宿主共用;索引上限是一次
11
+ **真正的拒绝**,而不是一句请求。(记忆库若位于某个项目的 git 仓库内,必须 git-ignore ——
12
+ 记忆里带着本机独有的细节,见 SKILL.md §1。给记忆库单独开一个*私有仓*则完全可以。)
13
+
14
+ 隶属于 [Engramory](https://github.com/tinqiao-oss/engramory)。
15
+
16
+ ## 已经有 `AGENTS.md` 常驻块了,为什么还要插件
17
+
18
+ 那个常驻块确实承载了纪律,单论召回也够用了。但有两件事它做不到:
19
+
20
+ **上限从此是确定性的。** Engramory 把索引控制在 200 行 / 25 KB 以内,是因为索引每次会话
21
+ 都要加载,超出的部分会**悄无声息地不再被召回**。在多数宿主上,这个限制只是"规则 + 一个
22
+ 要靠 agent 自己记得去跑的校验器"。而 dsh 暴露了 `ctx.tools.guard()` —— 一个同步的、
23
+ **单调**的否决:守卫一旦返回理由,后面任何监听器都无法把它翻回放行。所以在这里,上限是被
24
+ **强制执行**的,不是被请求的。请用 **0.2.6 或更高版本**:0.2.0 发布时上游的 profile 安装还
25
+ 没打通,而且它的 `inject` 声明是这版 Cordis 不接受的写法 —— 装得上,但**永远激活不了**
26
+ (issue #8);0.2.5 及以前,在 CRLF 换行的索引上、或在 dsh 0.2 上 cd 之后用相对路径,edit
27
+ 能绕过上限(见下文「已知限制」)。在 Claude Code 之外,dsh 是第一个真正把这层垫片写出来的宿主(另有几个宿主
28
+ 同样暴露了等价的写前接缝,见 PORTING.md,但还没有垫片实现)。
29
+
30
+ **协议随插件一起到达。** skill 是在运行时通过 dsh 的 skill 注册表注册的 —— 走一个响应式的
31
+ `ctx.inject(['skills'], …)` 子上下文,所以没有注册表的 profile 照样拿到上限,而注册表晚一步
32
+ 挂载的 profile 也照样拿得到 skill。这意味着它**不依赖**把文件放进 dsh 扫描的那五个 skill 根
33
+ 目录之一 —— 那条路很容易在细微处放错,而一旦放错就是**静默失败**。
34
+
35
+ ## 安装
36
+
37
+ ```sh
38
+ dsh plugin --profile <名字> add dsh-engramory
39
+ ```
40
+
41
+ 装到这里就完了。这个包自带 `dsh.bundle` manifest 指向它自己的 `cordis.patch.yml`,所以那一行
42
+ 会被自动插进 profile 的插件树里 —— 不需要手改配置。要改默认值,请在你自己 profile 的 patch
43
+ 层里**按 id 修改那一行已存在的配置**,**不要**再写一个 `- insert:`(insert 永远是追加,你会得到
44
+ 两行 engramory,而原来那行的上限依旧在生效):
45
+
46
+ ```yaml
47
+ - id: engramory
48
+ config:
49
+ indexName: MEMORY.md
50
+ maxLines: 200
51
+ maxBytes: 25600
52
+ ```
53
+
54
+ 一次 patch 会**整体替换**目标行的 `config`,所以你想保留的键要逐个列全。
55
+
56
+ 记忆库本身和常驻块,请用 Engramory 仓库里的安装器
57
+ (`python tools/engramory_init.py dsh --install-skill`)。本插件负责强制上限、提供协议;
58
+ **它不创建记忆库**。
59
+
60
+ ## 配置
61
+
62
+ | 字段 | 默认值 | 含义 |
63
+ |---|---|---|
64
+ | `indexName` | `MEMORY.md` | 被当作记忆索引的文件名(不区分大小写;`file_path` 与 `str_replace_editor` 的 `path` 都会检查)。空值或非字符串会回退到默认值。除此之外不检查任何东西。 |
65
+ | `maxLines` | `200` | 硬性行数上限。非正数或非有限值回退到默认值。 |
66
+ | `maxBytes` | `25600` | 硬性 UTF-8 字节上限(25 KB)。 |
67
+ | `indexPath` | 未设置 | 要守护的**那一个**索引的绝对路径。不设它时,守卫只按文件名匹配,于是**别的项目里一个无关的 `MEMORY.md` 也会被拦**;设了它就只有这个确切的文件会被守。比较走的是身份而非拼写(symlink 与 `..` 都会解析;仅在 Windows 上折叠大小写),而且即使文件尚不存在,它的第一次写入照样受守护。 |
68
+ | `registerSkill` | `true` | 设为 `false` 则保留上限、但跳过运行时 skill。 |
69
+ | `skill` | 内置 | 用你自己的 markdown 替换 skill 正文。 |
70
+
71
+ ## 守卫到底做了什么
72
+
73
+ | 调用 | 判决 |
74
+ |---|---|
75
+ | `write`(或 `str_replace_editor` 的 `create`)且结果在上限内 | 放行 |
76
+ | `write`/`create` 会让索引**增长**并越过上限 | **拒绝**,并报出具体数字和该压缩什么 |
77
+ | `write`/`create` 让一个已超限的索引**缩小或持平** | 放行 —— 增量压缩(210 → 205 → 198)必须走得通 |
78
+ | 带 `old_str`/`new_str` 的 edit:结果可模拟 | 按**结果**判定,增长/缩小规则与整文件写入相同 |
79
+ | 无法模拟的部分写入(如 `insert`)落在已超限的索引上 | **拒绝**,并告诉 agent:缩小的整文件写入是放行的 |
80
+ | 无法模拟的部分写入落在健康的索引上 | 放行 |
81
+ | `read`、`view`,以及任何未知工具 | 放行 —— 哪怕已超限,召回也绝不能被挡住 |
82
+ | 对任何其他文件的写入 | 放行 —— 不关守卫的事 |
83
+ | 整文件写入时当前索引缺失/读不到 | 视为**空**(与 Python 版守卫一致)—— 上限内的写入放行,而首次就超限的写入被拒 |
84
+ | 部分编辑时索引缺失/读不到 | 放行 —— 守卫绝不能因为一个自己读不了的路径而挡住工作 |
85
+ | 畸形的执行(没有 arguments、路径/内容不是字符串) | 放行 —— 它压根不像一次写入 |
86
+
87
+ 拒绝规则与 `hooks/engramory_index_guard.py` **完全一致**:只拒绝那种结果既超过上限、
88
+ 又比当前文件更大的写入。行数统计忽略结尾换行,所以一个正好卡在上限的索引仍然可写。
89
+
90
+ ## 已知限制
91
+
92
+ - **从上限之下一跃越过上限的、无法模拟的部分写入,抓不住。** `insert` 这类调用并不携带
93
+ 写入后的文本。这种越界会由下一次整文件写入、或 `engramory_check.py` 抓到。能保证的是:
94
+ 一个**已经超限**的索引不会被继续撑大,而缩小的写入永远放行。
95
+ - **工具名单遵循 dsh 文档化的 tool-fs 契约**(`write`/`edit` 用 `file_path`,
96
+ `str_replace_editor` 用 `path`),并且刻意保守:未知工具一律放行。
97
+ - **0.2.0 从未激活成功;0.2.1 才是第一个真正跑得起来的版本。** 在 rc.6 预览版那个 bug
98
+ 堵住上游所有第三方 profile 安装期间,插件能有的覆盖只有 mock —— 而 0.2.0 的 mock 恰好
99
+ 掩盖了两个激活期 bug:一个是旧版 Cordis 的 `{ required, optional }` inject 写法(被解读成
100
+ 在等待名字就叫 `required`/`optional` 的服务,于是永远 pending),另一个是裸读未声明的
101
+ `ctx.skills`(在 Cordis 的反射式上下文下会直接抛错)。安装一旦成为可能,**首个真实安装
102
+ 当场把两个都暴露了**(issue #8)。0.2.1 修掉了它们,并在一个真跑着的 dsh 0.1.0-rc.7 web
103
+ profile 上端到端验证过(0.2.0 逐字节复现启动失败;0.2.1 能启动并提供服务),也对着 dsh
104
+ 内置的 `@deepseek-ai/cordis` 解析器验过(注册表存在、不存在、以及晚挂载三种情况下的激活)。
105
+ 测试用的 mock 现在会照着反射式上下文的访问规则来,守卫的判决表则由 `node --test`
106
+ 持续覆盖(37 个用例,在 Engramory 的 CI 里跑)。
107
+ - **相对路径按 dsh 实际写入的位置来量。** dsh 0.2 给每个会话一个自己的工作目录(由它的
108
+ `working_directory` 工具切换),并按这个目录解析相对的 `file_path`。0.2.5 及以前的守卫却按
109
+ 进程的 cwd 解析,于是 cd 之后对裸 `MEMORY.md` 的 edit 读不到当前文件、不经测量就放行 ——
110
+ 在 dsh 0.2.1-rc.2 上它把索引撑到了 253 行。0.2.6 在 profile 提供 `workingDirectory` 服务时
111
+ 读取会话目录,没有时(所有 0.1.x 版本)退回进程 cwd。
112
+ - **CRLF 索引按 dsh 的方式来模拟 edit。** dsh 先把文本统一成 LF 再匹配 edit,写回时恢复文件
113
+ 原来的换行。0.2.5 及以前的守卫按原始字节匹配,于是在 CRLF 的 `MEMORY.md` 上(Windows 上很
114
+ 常见),LF 的 `old_string` 看起来不存在,edit 被预测成"什么都不改"而放行,dsh 却照常改了。
115
+ 0.2.6 用同样的方式归一化,并按 CRLF 字节数计算 CRLF 索引的大小。
116
+ - **验的是你大概率正在用的 dsh,不只是写它时的那一版。** Engramory 仓库里的
117
+ `tests/dsh_e2e/run.py` 会把指定的 dsh 版本装进临时目录,用 `dsh plugin add` 装上本插件,
118
+ 再对着一个脚本化的 DeepSeek API 替身跑一次 headless 任务:260 行的整文件写入、把索引撑过
119
+ 上限的 edit,都必须被本守卫拒绝,而且磁盘上什么都没变;小写入必须逐字节落盘;skill 必须出现
120
+ 在发给模型的技能目录里;dsh 提供的每个工具都必须已经归过类(没归类的新工具可能绕过上限写文件);卸载后
121
+ profile 必须还能启动。通过的版本逐个写在 `package.json` 的 `dsh.compatibility.dshReleases`
122
+ 里;没有同一插件版本、同一份代码的通过记录,单元测试就不接受把它标成 `compatible`。
123
+ - dsh 目前是开发者预览版,插件 API 可能变化。本插件刻意只碰 `ctx.tools.guard()` 和两个
124
+ 响应式 `ctx.inject()` 子上下文 —— 一个注册 skill,一个读取各会话的工作目录 —— 所以要跟着改
125
+ 也很便宜。
126
+
127
+ ## 许可
128
+
129
+ MIT —— 见 [Engramory 仓库](https://github.com/tinqiao-oss/engramory)。
package/cordis.patch.yml CHANGED
@@ -1,16 +1,16 @@
1
- # The dsh-engramory bundle patch: one row, inserted over whatever layers came before
2
- # it. A patch replaces the targeted row's whole `config`, so a profile overriding this
3
- # row must restate every key it means to keep.
4
- #
5
- # The numbers below are the protocol's own caps, not this port's opinion: 200 lines /
6
- # 25 KB is what keeps MEMORY.md cheap enough to load at the start of every session, and
7
- # it is the same limit hooks/engramory_index_guard.py enforces on Claude Code. They live
8
- # here, beside the row, so a deployment can raise or lower them without touching code.
9
-
10
- - insert:
11
- - id: engramory
12
- name: dsh-engramory
13
- config:
14
- indexName: MEMORY.md
15
- maxLines: 200
16
- maxBytes: 25600
1
+ # The dsh-engramory bundle patch: one row, inserted over whatever layers came before
2
+ # it. A patch replaces the targeted row's whole `config`, so a profile overriding this
3
+ # row must restate every key it means to keep.
4
+ #
5
+ # The numbers below are the protocol's own caps, not this port's opinion: 200 lines /
6
+ # 25 KB is what keeps MEMORY.md cheap enough to load at the start of every session, and
7
+ # it is the same limit hooks/engramory_index_guard.py enforces on Claude Code. They live
8
+ # here, beside the row, so a deployment can raise or lower them without touching code.
9
+
10
+ - insert:
11
+ - id: engramory
12
+ name: dsh-engramory
13
+ config:
14
+ indexName: MEMORY.md
15
+ maxLines: 200
16
+ maxBytes: 25600
package/index.js CHANGED
@@ -24,7 +24,7 @@
24
24
  * Zero dependencies, no build step: plain ESM, node: builtins only.
25
25
  */
26
26
  import { readFileSync, realpathSync } from 'node:fs'
27
- import { basename, dirname, join, resolve } from 'node:path'
27
+ import { basename, dirname, isAbsolute, join, resolve, sep } from 'node:path'
28
28
 
29
29
  export const name = 'engramory'
30
30
 
@@ -79,7 +79,33 @@ export function apply(ctx, config = {}) {
79
79
  maxBytes: positive(config.maxBytes, DEFAULT_MAX_BYTES),
80
80
  }
81
81
 
82
- ctx.tools.guard((exec) => refuseOversizedIndex(exec, settings))
82
+ // dsh 0.2 resolves a relative file path against the calling SESSION's working
83
+ // directory, which its `working_directory` tool moves away from the process's own
84
+ // cwd. Measured against process.cwd(), a bare `MEMORY.md` after a cd is the wrong
85
+ // file or none: an edit of the real index passed unmeasured, and a shrinking rewrite
86
+ // of an over-cap index was judged against an empty file and refused. The service is
87
+ // optional (0.1.x has none) and may mount late, so — like `skills` below — it is
88
+ // picked up reactively and never makes the cap wait.
89
+ let workingDirectory
90
+ ctx.inject(['workingDirectory'], (inner) => {
91
+ inner.effect(() => {
92
+ const service = inner.workingDirectory
93
+ workingDirectory = service
94
+ return () => {
95
+ if (workingDirectory === service) workingDirectory = undefined
96
+ }
97
+ })
98
+ })
99
+ const sessionCwd = (exec) => {
100
+ try {
101
+ const dir = workingDirectory?.get(exec.agent.session)
102
+ return typeof dir === 'string' && isAbsolute(dir) ? dir : undefined
103
+ } catch {
104
+ return undefined // no agent, no session, or a service shape this port predates
105
+ }
106
+ }
107
+
108
+ ctx.tools.guard((exec) => refuseOversizedIndex(exec, settings, sessionCwd))
83
109
 
84
110
  // The skill registry is optional AND may activate after this plugin — fiber order
85
111
  // between unrelated providers is not guaranteed, so a one-shot probe here would
@@ -99,12 +125,18 @@ export function apply(ctx, config = {}) {
99
125
  'Curated file-based long-term memory: recall through MEMORY.md at the start of ' +
100
126
  'a task, save durable user/feedback/project/reference facts, run a curation ' +
101
127
  'checkpoint when a task finishes, and sync before compacting or opening a ' +
102
- 'fresh thread.',
128
+ 'fresh thread. A turn that starts no work — a greeting, an acknowledgement — ' +
129
+ 'is not a task and needs none of this, but never judge that by length: a ' +
130
+ 'one-word reply continuing work underway inherits that task.',
103
131
  whenToUse:
104
132
  'Starting or resuming work, learning something durable worth a future session, ' +
105
133
  'finishing a task — including one that leaves nothing worth saving, since the ' +
106
134
  'checkpoint is still a decision to make — or approaching a ' +
107
- 'compact/clear/new-thread boundary.',
135
+ 'compact/clear/new-thread boundary. Not for a bare greeting, ' +
136
+ 'acknowledgement, or reaction, which starts no task — but a terse reply ' +
137
+ 'that continues work underway does, and material already in hand (a ' +
138
+ 'pasted diff to review) does not make work a non-task. Unsure? Treat it ' +
139
+ 'as a task.',
108
140
  source: 'runtime',
109
141
  content: skill,
110
142
  }))
@@ -119,7 +151,7 @@ export function apply(ctx, config = {}) {
119
151
  * rejects the overwhelming majority of calls before anything is measured, and the
120
152
  * only I/O is a single read of the index on a gated write.
121
153
  */
122
- function refuseOversizedIndex(exec, settings) {
154
+ function refuseOversizedIndex(exec, settings, sessionCwd) {
123
155
  const args = exec?.arguments
124
156
  if (!args || typeof args !== 'object') return undefined
125
157
 
@@ -146,19 +178,26 @@ function refuseOversizedIndex(exec, settings) {
146
178
  && (args.command === 'create' || args.command === 'str_replace'
147
179
  || args.command === 'insert'))
148
180
  if (!gated) return undefined
181
+ // Every read and identity check below uses the file the host will actually touch.
182
+ let target
183
+ try {
184
+ target = hostPath(sessionCwd(exec), filePath)
185
+ } catch {
186
+ return undefined // a relative path and no cwd left to resolve it (deleted under us)
187
+ }
149
188
  // If the guard is pinned to one file, this is where an unrelated same-named file in
150
189
  // another project drops out.
151
- if (settings.indexKey !== undefined && pathKey(filePath) !== settings.indexKey) {
190
+ if (settings.indexKey !== undefined && pathKey(target) !== settings.indexKey) {
152
191
  return undefined
153
192
  }
154
193
 
155
194
  if (WHOLE_FILE_WRITES.has(tool)) {
156
195
  if (typeof args.content !== 'string') return undefined
157
- return verdict(args.content, readIfPossible(filePath), filePath, settings)
196
+ return verdict(args.content, readIfPossible(target), filePath, settings)
158
197
  }
159
198
  if (tool === 'str_replace_editor' && args.command === 'create') {
160
199
  if (typeof args.file_text !== 'string') return undefined
161
- return verdict(args.file_text, readIfPossible(filePath), filePath, settings)
200
+ return verdict(args.file_text, readIfPossible(target), filePath, settings)
162
201
  }
163
202
 
164
203
  const partial = PARTIAL_WRITES.has(tool)
@@ -166,7 +205,7 @@ function refuseOversizedIndex(exec, settings) {
166
205
  && (args.command === 'str_replace' || args.command === 'insert'))
167
206
  if (!partial) return undefined // read / view / list / unknown: never refused
168
207
 
169
- const currentBuf = readIfPossible(filePath)
208
+ const currentBuf = readIfPossible(target)
170
209
  if (currentBuf === undefined) return undefined
171
210
  const current = currentBuf.toString('utf8')
172
211
  const simulated = simulateEdit(current, args)
@@ -255,19 +294,46 @@ function notMyIndexHint(settings) {
255
294
  * of them under a replace-all flag; an absent or ambiguous old-string means the
256
295
  * real tool errors and changes nothing, so the current text is the honest
257
296
  * prediction. Returns undefined when the shape isn't recognised.
297
+ *
298
+ * Matching happens on LF-normalised text and the result goes back in the file's own
299
+ * line endings, as dsh's local backend does it (`applyLiteralEdit` /
300
+ * `restoreLineEndings` in @deepseek-ai/dsh-fs-local). Matching the raw text let a CRLF
301
+ * index — common on Windows — read every LF `old_string` as "not found", predict no
302
+ * change, and pass an edit dsh then applied.
258
303
  */
259
304
  function simulateEdit(current, args) {
260
- const oldStr = firstString(args.old_str, args.old_string)
261
- if (oldStr === undefined || oldStr === '') return undefined
262
- const newStr = firstString(args.new_str, args.new_string) ?? ''
305
+ const oldRaw = firstString(args.old_str, args.old_string)
306
+ if (oldRaw === undefined || oldRaw === '') return undefined
307
+ const oldStr = lf(oldRaw)
308
+ const newStr = lf(firstString(args.new_str, args.new_string) ?? '')
309
+ // dsh decodes with TextDecoder, which drops a leading BOM — from the text it
310
+ // matches, from the sample it detects line endings on, and from what it writes back.
311
+ const decoded = current.startsWith('') ? current.slice(1) : current
312
+ const text = lf(decoded)
313
+ const first = text.indexOf(oldStr)
314
+ if (first === -1) return current // the real tool errors: nothing changes
315
+ let edited
263
316
  if (args.replace_all === true || args.replaceAll === true) {
264
- return current.split(oldStr).join(newStr)
317
+ edited = text.split(oldStr).join(newStr)
318
+ } else {
319
+ const second = text.indexOf(oldStr, first + oldStr.length)
320
+ if (second !== -1) return current // ambiguous: the real tool errors, nothing changes
321
+ edited = text.slice(0, first) + newStr + text.slice(first + oldStr.length)
265
322
  }
266
- const first = current.indexOf(oldStr)
267
- if (first === -1) return current // the real tool errors: nothing changes
268
- const second = current.indexOf(oldStr, first + oldStr.length)
269
- if (second !== -1) return current // ambiguous: the real tool errors, nothing changes
270
- return current.slice(0, first) + newStr + current.slice(first + oldStr.length)
323
+ // Normalised once more before expanding, as dsh's restoreLineEndings does: a lone
324
+ // `\r` meeting an inserted `\n` at the seam is one CRLF, not `\r\r\n`.
325
+ return crlfDominates(decoded) ? lf(edited).split('\n').join('\r\n') : edited
326
+ }
327
+
328
+ function lf(text) {
329
+ return text.replaceAll('\r\n', '\n')
330
+ }
331
+
332
+ /** dsh's own call (`detectLineEndings`): CRLF wins on a majority in the first 4096 chars. */
333
+ function crlfDominates(raw) {
334
+ const sample = raw.slice(0, 4096)
335
+ const crlf = sample.split('\r\n').length - 1
336
+ return crlf > sample.split('\n').length - 1 - crlf
271
337
  }
272
338
 
273
339
  function firstString(...values) {
@@ -313,6 +379,22 @@ function positive(value, fallback) {
313
379
  return floored > 0 ? floored : fallback
314
380
  }
315
381
 
382
+ /**
383
+ * The path the host's own file tools would touch for `p`, mirroring dsh's local
384
+ * backend (`localDisplayPath` in @deepseek-ai/dsh-fs-local): resolve against `cwd`,
385
+ * except that on POSIX a spelling with a `..` segment is kept as written, so a `..`
386
+ * after a symlinked segment stays physical — collapsing it could measure a different
387
+ * file than the one the write lands on. Without a session directory (`cwd`
388
+ * undefined), the process's is used, and only when `p` needs one: process.cwd()
389
+ * throws once that directory is deleted, and an absolute path must not care.
390
+ */
391
+ function hostPath(cwd, p) {
392
+ const resolved = () => (cwd === undefined ? resolve(p) : resolve(cwd, p))
393
+ if (process.platform === 'win32') return resolved()
394
+ const raw = isAbsolute(p) ? p : `${cwd ?? process.cwd()}${sep}${p}`
395
+ return /(?:^|[\\/])\.\.(?:[\\/]|$)/u.test(raw) ? raw : resolved()
396
+ }
397
+
316
398
  /**
317
399
  * Normalise a path for identity comparison. `realpath` resolves symlinks and `..` so
318
400
  * two spellings of the same file compare equal, but it throws for a path with no file
@@ -374,6 +456,16 @@ function builtinSkillBody() {
374
456
  '',
375
457
  '## Recall',
376
458
  '',
459
+ 'A turn, a message, or a session is NOT BY ITSELF a task: a greeting, an',
460
+ 'acknowledgement, or a reaction starts none, so it gets no recall and no',
461
+ 'checkpoint. A task is user-directed work whose CORRECT handling could depend',
462
+ 'on what the store holds — read-only work (analysis, diagnosis, planning,',
463
+ 'review)',
464
+ 'counts, and changing a file is not required. HAVING THE MATERIAL IN HAND does',
465
+ 'not make work a non-task: a diff pasted for review is still a task. NEVER INFER',
466
+ 'THIS FROM LENGTH: a one-word reply that picks an option or carries on work',
467
+ 'underway inherits that task. Still unclear? Treat it as a task.',
468
+ '',
377
469
  'At the start of a task, read the index and open only the notes whose hooks look',
378
470
  'relevant. Treat what you recall as background that may be stale: re-verify any',
379
471
  'file, flag, or version against the repo before acting on it.',
package/package.json CHANGED
@@ -1,56 +1,64 @@
1
- {
2
- "name": "dsh-engramory",
3
- "version": "0.2.3",
4
- "description": "Curated file-based long-term memory for DeepSeek Harness — human-readable markdown notes, one plain-file store shared across hosts (git-ignored when it lives inside a project repo), and a deterministic index cap enforced by ctx.tools.guard()",
5
- "keywords": [
6
- "dsh-plugin",
7
- "deepseek-harness",
8
- "dsh",
9
- "memory",
10
- "agent-memory",
11
- "long-term-memory",
12
- "engramory",
13
- "markdown"
14
- ],
15
- "type": "module",
16
- "main": "index.js",
17
- "dsh": {
18
- "bundle": {
19
- "patch": "./cordis.patch.yml"
20
- }
21
- },
22
- "exports": {
23
- ".": "./index.js",
24
- "./package.json": "./package.json"
25
- },
26
- "files": [
27
- "index.js",
28
- "cordis.patch.yml",
29
- "README.md",
30
- "LICENSE"
31
- ],
32
- "license": "MIT",
33
- "repository": {
34
- "type": "git",
35
- "url": "git+https://github.com/tinqiao-oss/engramory.git",
36
- "directory": "adapters/dsh/plugin"
37
- },
38
- "homepage": "https://github.com/tinqiao-oss/engramory/tree/master/adapters/dsh#readme",
39
- "bugs": {
40
- "url": "https://github.com/tinqiao-oss/engramory/issues"
41
- },
42
- "engines": {
43
- "node": ">=18"
44
- },
45
- "peerDependencies": {
46
- "@deepseek-ai/dsh-tools": ">=0.0.1-rc.1"
47
- },
48
- "peerDependenciesMeta": {
49
- "@deepseek-ai/dsh-tools": {
50
- "optional": false
51
- }
52
- },
53
- "publishConfig": {
54
- "access": "public"
55
- }
56
- }
1
+ {
2
+ "name": "dsh-engramory",
3
+ "version": "0.2.6",
4
+ "description": "Curated file-based long-term memory for DeepSeek Harness — human-readable markdown notes, one plain-file store shared across hosts (git-ignored when it lives inside a project repo), and a deterministic index cap enforced by ctx.tools.guard()",
5
+ "keywords": [
6
+ "dsh-plugin",
7
+ "deepseek-harness",
8
+ "dsh",
9
+ "memory",
10
+ "agent-memory",
11
+ "long-term-memory",
12
+ "engramory",
13
+ "markdown"
14
+ ],
15
+ "type": "module",
16
+ "main": "index.js",
17
+ "dsh": {
18
+ "bundle": {
19
+ "patch": "./cordis.patch.yml"
20
+ },
21
+ "compatibility": {
22
+ "dsh": ">=0.1.0-rc.7 <0.3.0-0",
23
+ "dshReleases": {
24
+ "0.1.5-rc.3": "compatible",
25
+ "0.1.7-alpha.1": "compatible",
26
+ "0.1.7-alpha.2": "compatible",
27
+ "0.1.7-rc.1": "compatible",
28
+ "0.1.7-rc.2": "compatible",
29
+ "0.2.1-alpha.5": "compatible",
30
+ "0.2.1-rc.1": "compatible",
31
+ "0.2.1-rc.2": "compatible",
32
+ "0.2.1-rc.3": "compatible",
33
+ "0.2.1-rc.4": "compatible",
34
+ "0.2.2-alpha.2": "compatible"
35
+ }
36
+ }
37
+ },
38
+ "exports": {
39
+ ".": "./index.js",
40
+ "./package.json": "./package.json"
41
+ },
42
+ "files": [
43
+ "index.js",
44
+ "cordis.patch.yml",
45
+ "README.md",
46
+ "LICENSE"
47
+ ],
48
+ "license": "MIT",
49
+ "repository": {
50
+ "type": "git",
51
+ "url": "git+https://github.com/tinqiao-oss/engramory.git",
52
+ "directory": "adapters/dsh/plugin"
53
+ },
54
+ "homepage": "https://github.com/tinqiao-oss/engramory/tree/master/adapters/dsh#readme",
55
+ "bugs": {
56
+ "url": "https://github.com/tinqiao-oss/engramory/issues"
57
+ },
58
+ "engines": {
59
+ "node": ">=18"
60
+ },
61
+ "publishConfig": {
62
+ "access": "public"
63
+ }
64
+ }