dsh-engramory 0.2.7 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +242 -80
- package/README.zh-CN.md +124 -60
- package/client.js +652 -0
- package/icon.svg +8 -0
- package/index.js +1958 -50
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +15 -1
package/README.md
CHANGED
|
@@ -4,11 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
[](https://unstone.github.io/dsh-xray/registry.html#tinqiao-oss__engramory)
|
|
6
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
7
|
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,
|
|
11
|
-
|
|
8
|
+
plain markdown notes you can open with anything,
|
|
9
|
+
[one writer and many readers](../../reader/README.md) across hosts, and a pre-write
|
|
10
|
+
index guard for matched file tools. (If the store
|
|
12
11
|
lives inside a project's git repo it must be git-ignored — memories carry machine-local
|
|
13
12
|
detail; see SKILL.md §1. A *dedicated private* repo for the store itself is fine.)
|
|
14
13
|
|
|
@@ -16,22 +15,24 @@ Part of [Engramory](https://github.com/tinqiao-oss/engramory).
|
|
|
16
15
|
|
|
17
16
|
## Why a plugin, when an `AGENTS.md` block already works
|
|
18
17
|
|
|
19
|
-
The block
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
18
|
+
The block carries the discipline. Three things it cannot do:
|
|
19
|
+
|
|
20
|
+
**The index is already in context — recall needs no read.** On every step the plugin
|
|
21
|
+
puts the memory index into dsh's runtime context: the project store
|
|
22
|
+
(`<project>/.engramory-memory/MEMORY.md`, the nearest one between the session's
|
|
23
|
+
directory and its git root — the way dsh finds a project's `AGENTS.md`) and the global one
|
|
24
|
+
(`$DSH_HOME/.engramory-memory/MEMORY.md`, `~/.dsh` when `$DSH_HOME` is unset) —
|
|
25
|
+
where `tools/engramory_init.py dsh` puts them — or, when set, `indexPath` alone. It is
|
|
26
|
+
re-read whenever it changes, so a write shows on the next request, and cut at the cap:
|
|
27
|
+
lines past it are left out, and the model is told to compact. Turn it off with
|
|
28
|
+
`injectIndex: false`.
|
|
29
|
+
|
|
30
|
+
**The cap becomes deterministic for matched tools.** `ctx.tools.guard()` is a
|
|
31
|
+
synchronous, **monotonic** refusal: once a guard returns a reason, later listeners
|
|
32
|
+
cannot allow the call. This keeps the index within its configured growth limits
|
|
33
|
+
without relying on the agent to run a checker. Use **`dsh-engramory >= 0.2.7`**;
|
|
34
|
+
[PORTING.md](../../../PORTING.md) records which hosts have shipped guards, and
|
|
35
|
+
[CHANGELOG.md](../../../CHANGELOG.md) holds release history.
|
|
35
36
|
|
|
36
37
|
**The protocol arrives with the plugin.** The skill is registered at runtime through
|
|
37
38
|
dsh's skill registry — via a reactive `ctx.inject(['skills'], …)` child, so a profile
|
|
@@ -41,11 +42,18 @@ roots dsh scans, a path that is easy to get subtly wrong and fails silently when
|
|
|
41
42
|
|
|
42
43
|
## Install
|
|
43
44
|
|
|
45
|
+
Zero-dependency ESM with no build step; requires Node.js 18+ and `pnpm` on PATH
|
|
46
|
+
for dsh's plugin command.
|
|
47
|
+
|
|
44
48
|
```sh
|
|
45
49
|
dsh plugin --profile <name> add dsh-engramory
|
|
46
50
|
```
|
|
47
51
|
|
|
48
|
-
|
|
52
|
+
Check `dsh.compatibility.dshReleases` in [`package.json`](package.json) for the
|
|
53
|
+
exact compatible releases, backed by [`results.json`](../../../tests/dsh_e2e/results.json).
|
|
54
|
+
Use the [E2E guide](../../../tests/dsh_e2e/README.md) to verify another release.
|
|
55
|
+
|
|
56
|
+
The package ships a `dsh.bundle` manifest pointing at its own
|
|
49
57
|
`cordis.patch.yml`, so the row is inserted into the profile's plugin tree for you — no
|
|
50
58
|
hand-editing. To change a default, patch the EXISTING row by id in your profile's own
|
|
51
59
|
patch layer — do **not** use a second `- insert:` (insert always appends, so you would
|
|
@@ -61,20 +69,170 @@ end up with two engramory rows and the original caps still enforced):
|
|
|
61
69
|
|
|
62
70
|
A patch replaces the targeted row's whole `config`, so list every key you mean to keep.
|
|
63
71
|
|
|
64
|
-
|
|
65
|
-
(`
|
|
66
|
-
|
|
72
|
+
With the plugin alone, the memory is one global store, `$DSH_HOME/.engramory-memory/`
|
|
73
|
+
(`$DSH_HOME` defaults to `~/.dsh`): the skill tells the AI where it is, and the AI
|
|
74
|
+
creates it on its first save. A project can have a store of its own,
|
|
75
|
+
`.engramory-memory/` in its root, which only you create — the installer does it and
|
|
76
|
+
git-ignores it (`python tools/engramory_init.py dsh --project-root <project>`). The AI
|
|
77
|
+
never creates one; where one exists, what holds only in that project goes there, and
|
|
78
|
+
everything else stays in the global store. The installer also writes the always-on block
|
|
79
|
+
into `AGENTS.md` (`python tools/engramory_init.py dsh --install-skill`).
|
|
80
|
+
|
|
81
|
+
To uninstall:
|
|
82
|
+
|
|
83
|
+
```sh
|
|
84
|
+
dsh plugin --profile <name> remove dsh-engramory
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Companion mode (大肥鱼模式)
|
|
88
|
+
|
|
89
|
+
For a character the user chats with — the community's 大肥鱼, or any persona. Turn it on
|
|
90
|
+
with `companion: true`, or the 大肥鱼模式 switch on the settings page (below).
|
|
91
|
+
|
|
92
|
+
- **Three more types for the character's own life** ([SKILL.md §2.2](../../../SKILL.md)):
|
|
93
|
+
`moment` — something the character and the user shared, with its date; kept, and
|
|
94
|
+
folded into a monthly archive when the index fills. `upcoming` — something coming up
|
|
95
|
+
in the user's life; asked about once its date passes, and rewritten as a `moment`
|
|
96
|
+
once the user has said how it went. `self` —
|
|
97
|
+
a change in the character itself, such as a nickname; updated in place, never folded.
|
|
98
|
+
A `moment` or `self` note is deleted only when the user asks to forget. In a Chinese
|
|
99
|
+
store their headings are `## 经历`, `## 约定` and `## 我自己`. The other four types
|
|
100
|
+
keep their meaning.
|
|
101
|
+
- **Recall in character.** The agent is told that, when it plays a character for you,
|
|
102
|
+
any message, a greeting included, may call for recall, and to speak of what it
|
|
103
|
+
remembered — or failed to save — in character, without naming files or tools.
|
|
104
|
+
- **The persona preset decides what it can do with memory.** With companion mode or
|
|
105
|
+
social mode on, what a conversation is given — the memory and its rules, or social
|
|
106
|
+
mode's note — is a *required* runtime context, so it arrives even under a preset that
|
|
107
|
+
turns optional context off (dsh-persona's `includeRuntimeContext: false`, as dsh's
|
|
108
|
+
`minimal` preset does). Saving is another matter: the preset needs dsh's file tools and
|
|
109
|
+
the skill tool for the character to write notes the way the skill describes. A preset
|
|
110
|
+
with only a shell can read the memory it is given, and its writes go past the guard.
|
|
111
|
+
|
|
112
|
+
## Social mode
|
|
113
|
+
|
|
114
|
+
A choice inside companion mode, for a character other people can talk to as well — one
|
|
115
|
+
wired to QQ, WeChat, Douyin or Bilibili through a bridge plugin. With companion mode on,
|
|
116
|
+
choose *Social* (社交) under *Memory access* (记忆权限) on the settings page, or set
|
|
117
|
+
`audience: social`. The default, *Private* (专属), gives every conversation the memory:
|
|
118
|
+
if only you talk to this dsh, keep it. Without companion mode, `audience` changes
|
|
119
|
+
nothing.
|
|
120
|
+
|
|
121
|
+
- **Only your own conversations get the memory.** A conversation is yours when you have
|
|
122
|
+
written in it in dsh's own window — the desktop app or `dsh web` — and nobody else has.
|
|
123
|
+
dsh stamps every user message with where it came from, and its browser client sends
|
|
124
|
+
each prompt with a request id and the browser's time zone: that is how a message you
|
|
125
|
+
typed there is told apart. Everything else is someone else's:
|
|
126
|
+
- a message a bridge plugin submits, under a kind of its own (`plugin:<name>`, as
|
|
127
|
+
dsh-onebot writes, or any other kind dsh's own producers do not use) or dsh's plain
|
|
128
|
+
`user` kind, unless the bridge marks it as yours (the standard below);
|
|
129
|
+
- a program calling dsh web's API, `dsh headless`, an editor or terminal client;
|
|
130
|
+
- a conversation an automation or a plugin opened, until you write in it;
|
|
131
|
+
- a caller dsh itself names as someone other than the owner.
|
|
132
|
+
|
|
133
|
+
Once anyone else has written in a conversation it stays theirs, even after you write
|
|
134
|
+
in it too: what the memory put into it earlier stays in its history. Messages dsh
|
|
135
|
+
adds itself (runtime context, skills, compaction, agent-to-agent messages) decide
|
|
136
|
+
nothing.
|
|
137
|
+
- **Someone else's conversation gets no memory:** no index, a note saying so, no full path
|
|
138
|
+
to the store (the skill, which every conversation reads, gives none in social mode; your
|
|
139
|
+
own conversations are told it in their runtime context), and file tools that may name
|
|
140
|
+
files only. A directory is refused (so grep, glob and directory
|
|
141
|
+
views are — any tree may hold a store), and so is any path into a store, however it is
|
|
142
|
+
spelled or linked, including a store that does not exist yet, so none can be planted
|
|
143
|
+
for you. A subagent is yours while its live parent is — read afresh on every call —
|
|
144
|
+
and nobody else has written to it directly; a forked conversation is judged by its own
|
|
145
|
+
history, the part it inherited included. A call with no session, a
|
|
146
|
+
dsh without session projections, or a network path counts as someone else's.
|
|
147
|
+
|
|
148
|
+
This keeps the user's memory out of others' context by the routes dsh's own file tools
|
|
149
|
+
take. It is **not** a security boundary: a shell tool reads any file, and a different
|
|
150
|
+
profile on the same account can still read the store's files. Keeping others away from
|
|
151
|
+
the store needs filesystem permissions or a sandbox.
|
|
152
|
+
|
|
153
|
+
### For bridge authors: marking who wrote a message
|
|
154
|
+
|
|
155
|
+
With social mode on, every message a bridge submits counts as someone else's. For the
|
|
156
|
+
owner's own chat to get the memory — their private QQ chat with the character, say —
|
|
157
|
+
put a `speaker` on the message's source:
|
|
158
|
+
|
|
159
|
+
```js
|
|
160
|
+
agent.followup(createUserMessage({
|
|
161
|
+
content: [{ type: 'text', text }],
|
|
162
|
+
source: { kind: 'plugin:your-bridge', speaker: { role: 'owner' } },
|
|
163
|
+
}))
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
| `speaker.role` | Meaning |
|
|
167
|
+
|---|---|
|
|
168
|
+
| `owner` | The dsh owner wrote it: a message from the account the user set as theirs in your bridge. |
|
|
169
|
+
| `guest` | Someone else wrote it — the same as no `speaker` at all. |
|
|
170
|
+
| `none` | Nobody wrote it: your plugin's own report, reminder or notice. It does not turn the owner's conversation into someone else's. |
|
|
171
|
+
|
|
172
|
+
Mark a message `owner` only when it comes from the owner's own account. A group's
|
|
173
|
+
conversation stays someone else's once anyone else in it speaks. dsh keeps unknown
|
|
174
|
+
source fields in the session log, so the role survives a restart.
|
|
175
|
+
|
|
176
|
+
A program that talks to dsh only through dsh web's API cannot set a source, and its
|
|
177
|
+
messages count as someone else's — unless it sends `clientTimeZone` with a prompt: dsh
|
|
178
|
+
then records exactly what its own page sends, and the message counts as the owner's. A
|
|
179
|
+
program forwarding other people's messages must leave `clientTimeZone` out.
|
|
180
|
+
|
|
181
|
+
## Settings page
|
|
182
|
+
|
|
183
|
+
On dsh's **Plugins** page, open *Engramory memory*: its page sets companion mode (a
|
|
184
|
+
switch) and, while it is on, its memory access (private or social), and the index cap.
|
|
185
|
+
The memory's language follows dsh's own (see `language` below).
|
|
186
|
+
|
|
187
|
+
Saving goes through dsh's own profile editor: it validates the whole config, writes it
|
|
188
|
+
to your profile's `cordis.patch.yml`, and reloads the plugin with it — or leaves the
|
|
189
|
+
file as it was. A save made against settings that changed elsewhere in the meantime is
|
|
190
|
+
refused, and the page reads them again. Settings put back to their defaults remove
|
|
191
|
+
their override instead of pinning it. Other fields (`indexName`, `indexPath`, …) stay
|
|
192
|
+
in the file.
|
|
193
|
+
|
|
194
|
+
Only you reach the page's route — your signed-in browser, or a program holding your
|
|
195
|
+
login: dsh answers 401 to a request without it and 403 to a cross-site one, so an agent
|
|
196
|
+
cannot call it to switch social mode off. A dsh without the profile editor shows the page read-only.
|
|
197
|
+
Companion mode and its memory access are also at the top of the memory page (below).
|
|
198
|
+
|
|
199
|
+
## Memory page
|
|
200
|
+
|
|
201
|
+
dsh's sidebar gets a **Memory** entry. Its page lists the stores recall can use — the
|
|
202
|
+
global store under `$DSH_HOME` and each workspace's project store, found as recall finds
|
|
203
|
+
one, or, with `indexPath` pinned, that index's directory alone — with each index's size
|
|
204
|
+
against the cap, its notes' names by type, and its archive. It is read-only and shows
|
|
205
|
+
names only, never a note's text: a name opens the note in your computer's default app,
|
|
206
|
+
through dsh's own opener, on the machine dsh runs on. Where dsh says it cannot open there
|
|
207
|
+
(over SSH, say), the names are plain text and the page shows where the files are.
|
|
208
|
+
*Open the index* opens the index the same way; *Open the folder* shows the index in your
|
|
209
|
+
file manager (Explorer and Finder select it in the store's folder; on Linux the folder
|
|
210
|
+
opens), so a store with no index has no such button. At the top, companion mode and its
|
|
211
|
+
memory access save as they are switched; what they mean, and the index cap, are on the
|
|
212
|
+
settings page.
|
|
213
|
+
|
|
214
|
+
The listing is bounded — 2000 entries looked at per folder, 200 workspaces, 5000 names in
|
|
215
|
+
all — and never looks into a workspace on a network share path (`\\server\share\…`), or
|
|
216
|
+
one whose folder has since become a link to somewhere else. A note that is a link is not
|
|
217
|
+
listed; what is opened is a note or the index found again, by name, directly in its store,
|
|
218
|
+
and refused if it has become a link. A relative `indexPath` follows each conversation's
|
|
219
|
+
directory, so the page cannot list it. Only you reach the page's route, as the settings
|
|
220
|
+
page's.
|
|
67
221
|
|
|
68
222
|
## Configuration
|
|
69
223
|
|
|
70
224
|
| Field | Default | Meaning |
|
|
71
225
|
|---|---|---|
|
|
72
|
-
| `indexName` | `MEMORY.md` | Basename treated as the memory index (case-insensitive; both `file_path` and `str_replace_editor`'s `path` are checked) — the name a write is called by, or the name of the file it lands on, so a symlink under another name is caught too. An empty or non-string value falls back to the default. |
|
|
226
|
+
| `indexName` | `MEMORY.md` | Basename treated as the memory index (case-insensitive; both `file_path` and `str_replace_editor`'s `path` are checked) — the name a write is called by, or the name of the file it lands on, so a symlink under another name is caught too. An empty or non-string value falls back to the default, and so does one that is not a plain file name (a path separator, a `:`, `.` or `..`). |
|
|
73
227
|
| `maxLines` | `200` | Hard line cap. Non-positive or non-finite values fall back to the default. |
|
|
74
228
|
| `maxBytes` | `25600` | Hard UTF-8 byte cap (25 KB). |
|
|
75
|
-
| `indexPath` | unset |
|
|
229
|
+
| `indexPath` | unset | Path of the ONE index to guard: absolute, or relative to each session's working directory (resolved there by the guard as by recall; the memory page lists only an absolute pin). 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 — the file the write lands on, whatever it is called (symlinks and `..` resolved; case-folded on Windows only) — and a path that does not exist yet still has its first write guarded. |
|
|
76
230
|
| `registerSkill` | `true` | Set `false` to keep the cap but skip the runtime skill. |
|
|
77
|
-
| `
|
|
231
|
+
| `injectIndex` | `true` | Put the memory index into dsh's runtime context on every step (see above). Set `false` to recall by reading the file instead. |
|
|
232
|
+
| `language` | dsh's | Unset (or empty), the memory follows dsh's language — the one chosen in dsh's Settings → General, else the language of the system dsh runs on (dsh's page, with no choice made, follows the browser's, so on a remote `dsh web` the two can differ); a change applies once dsh restarts. Set it to pin one: `zh` writes the store in Chinese (SKILL.md §1.1): Chinese note names, `## 用户` / `## 反馈` / `## 项目` / `## 参考` headings, `**为什么:**` / `**怎么用:**` labels, and the skill and the injected index in Chinese. Frontmatter keys and `type:` values stay English, so every host still reads the store. The cap's refusals stay English. `zh` and its regional forms (`zh-CN`, `zh-TW`, …) select Chinese; any other string selects `en`. |
|
|
233
|
+
| `companion` | `false` | `true` turns companion mode on (above): three more types for the character's own life, and recall in character. Only the boolean `true` counts. |
|
|
234
|
+
| `audience` | `private` | With companion mode on, `social` turns social mode on (above): only your own conversations get the memory. Any other value is `private`, where every conversation does. Without companion mode it changes nothing. |
|
|
235
|
+
| `skill` | built-in | Replace the skill body with your own markdown — which must then say where the store is itself. |
|
|
78
236
|
|
|
79
237
|
## What the guard actually does
|
|
80
238
|
|
|
@@ -83,7 +241,7 @@ supplies the protocol; it does not create the store.
|
|
|
83
241
|
| `write` (or `str_replace_editor` `create`) ending within caps | allow |
|
|
84
242
|
| `write`/`create` that GROWS the index past a cap | **deny**, naming the numbers and what to compact |
|
|
85
243
|
| `write`/`create` that SHRINKS or keeps an over-cap index | allow — incremental compaction (210 → 205 → 198) must stay possible |
|
|
86
|
-
| Edit carrying `old_str`/`new_str`: result simulated | judged by the RESULT, same grow/shrink rule as a write — sized before it is built, so a `replace_all` whose result would be too long to build is
|
|
244
|
+
| Edit carrying `old_str`/`new_str`: result simulated | judged by the RESULT, same grow/shrink rule as a write — sized before it is built, so a `replace_all` whose result would be too long to build is judged on lower bounds of its bytes and lines |
|
|
87
245
|
| Unsimulable partial (e.g. `insert`) on an over-cap index | **deny**, telling the agent a shrinking whole-file write passes |
|
|
88
246
|
| Unsimulable partial on a healthy index | allow |
|
|
89
247
|
| `read`, `view`, or any unknown tool | allow — recall must never be blocked, even over cap |
|
|
@@ -91,63 +249,67 @@ supplies the protocol; it does not create the store.
|
|
|
91
249
|
| 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 |
|
|
92
250
|
| Missing/unreadable index on a partial edit | allow — a guard must never block work over a path it cannot read |
|
|
93
251
|
| A FIFO, device, or directory under the index's name | never read — counts as unreadable (a read there would hang or flood the host, which runs guards on its main thread) |
|
|
94
|
-
| An index past the read limit (1 MiB,
|
|
252
|
+
| An index past the read limit (`min(16 MiB, max(1 MiB, 4 × maxBytes))`) | not read whole: a whole-file write is judged on bytes against the filesystem's size (every shrink passes; its line count is unknown, so lines are not capped there), and an edit passes unmeasured, as on an unreadable index |
|
|
95
253
|
| Malformed execution (no arguments, non-string path/content) | allow — not recognisably a write |
|
|
96
254
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
255
|
+
For measured results, deny only a dimension that is over its cap AND grew past
|
|
256
|
+
the current file. The line count ignores a trailing newline, so an index exactly
|
|
257
|
+
at the cap stays writable. This decision table is pinned by `node --test` in CI.
|
|
258
|
+
|
|
259
|
+
Relative paths use the session's `workingDirectory` service when available,
|
|
260
|
+
falling back to the process cwd. Edits match LF-normalised text and preserve the
|
|
261
|
+
file's line endings when sizing the result, including CRLF bytes.
|
|
100
262
|
|
|
101
263
|
## Known limits
|
|
102
264
|
|
|
103
|
-
- **
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
- **
|
|
108
|
-
`
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
the
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
own
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
265
|
+
- **Unsimulable partial edits can cross the cap from below.** On a readable,
|
|
266
|
+
already-over-cap index they are denied; use a shrinking whole-file write to
|
|
267
|
+
compact. Missing/unreadable files and the read-limit exceptions are listed above.
|
|
268
|
+
Run `engramory_check.py` after index edits and `engramory_doctor.py` periodically.
|
|
269
|
+
- **Coverage is limited to known file tools:** `write`/`edit` with `file_path`,
|
|
270
|
+
`str_replace`/`insert`, and mutating `str_replace_editor` commands with `path`.
|
|
271
|
+
Unknown tools pass. **Shell tools, MCP file tools, and external editors bypass
|
|
272
|
+
the guard.**
|
|
273
|
+
- **Injection finds the stores where the installer puts them.** A store elsewhere
|
|
274
|
+
(`--memory-root`) needs `indexPath`. The project root is found by dsh's default `.git`
|
|
275
|
+
marker only (a profile's custom `projectRootMarkers` are not visible to a plugin), and
|
|
276
|
+
a project store created mid-session can take up to ten seconds to appear. A project
|
|
277
|
+
store whose real path leaves the project — a symlink out of the checkout — is not
|
|
278
|
+
read; point `indexPath` at it if that is deliberate. The global store's location comes
|
|
279
|
+
from `$DSH_HOME`, resolved the way dsh resolves it. What is injected is framed as notes,
|
|
280
|
+
not instructions, that may be stale; a host without runtime context
|
|
281
|
+
(no `ctx.systemPrompt.context`) goes without it.
|
|
282
|
+
- **Social mode knows your typing by what dsh's browser client sends.** A program that
|
|
283
|
+
sends the same request id and time zone as dsh's own page is taken for you, and a
|
|
284
|
+
browser whose time zone cannot be read leaves your messages unmarked. dsh's terminal,
|
|
285
|
+
editor (ACP) and SDK clients do not mark them either: with social mode on, only
|
|
286
|
+
dsh's own window counts.
|
|
287
|
+
- **Some of dsh's messages carry no author.** An answer to the agent's question, a
|
|
288
|
+
scheduled prompt and a message a slash command adds (`/plan <text>`, `/goal`'s
|
|
289
|
+
attachments) count as nobody's, and a queued message edited through dsh's API keeps the
|
|
290
|
+
author it had. On dsh 0.2.1 only the owner can answer, schedule or edit — in dsh's
|
|
291
|
+
window, or through a program with the owner's login, which can read the store's files
|
|
292
|
+
through dsh's own file API anyway — and a chat bridge that forwards a command runs it in
|
|
293
|
+
its own conversation, which its other messages already make someone else's.
|
|
294
|
+
- **A kind of message a later dsh adds counts as someone else's** until this plugin knows
|
|
295
|
+
it: a conversation of yours may lose the memory, never the other way round.
|
|
296
|
+
- **The memory page reads the disks as they answer.** On a filesystem whose directory
|
|
297
|
+
entries carry no type (some network mounts), Node stats each listed entry on dsh's main
|
|
298
|
+
thread, at most 2000 per folder; and a listing stuck on a drive that never answers keeps
|
|
299
|
+
reporting a timeout until it does, rather than starting another that would get stuck too.
|
|
300
|
+
- **What the memory page opens is checked by path.** dsh's opener takes a path, which it
|
|
301
|
+
resolves once more before the desktop opens it; a process changing a store's files at
|
|
302
|
+
that very moment could still redirect what the desktop opens — as with dsh's own opener
|
|
303
|
+
for any workspace file — though not to a link the page saw.
|
|
304
|
+
- dsh's plugin API can change. The plugin uses `ctx.tools.guard()` and reactive
|
|
305
|
+
`ctx.inject()` children for the skill registry, the session working directory, the
|
|
306
|
+
prompt registry, and — in social mode — dsh's session projections, where it folds
|
|
307
|
+
who has written in each session; a compatible-release declaration needs recorded host
|
|
308
|
+
evidence, not just unit tests. The recorded end-to-end runs cover the cap, the skill
|
|
309
|
+
and the injected index; companion mode, social mode, the settings page and the memory page were checked by hand on
|
|
310
|
+
a real dsh (0.2.1-rc.5) — messages typed in dsh's window, sent by `dsh headless` and by
|
|
311
|
+
a program through dsh web's API, a save through the page, and the memory page's listing,
|
|
312
|
+
a switch saved from it and a note opened from it — but are not part of that record yet.
|
|
151
313
|
|
|
152
314
|
## License
|
|
153
315
|
|