dsh-loci 2.10.1 → 3.0.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/CHANGELOG.md +98 -0
- package/README.md +2 -2
- package/client.js +138 -208
- package/docs/PRIOR-ART.md +2 -2
- package/package.json +1 -1
- package/src/host/index.js +1 -1
- package/src/host/scope.js +16 -6
- package/src/mcp/store-facade.js +1 -1
- package/locale/zh.json +0 -12
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,104 @@ All notable changes to this project are documented here. The format follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); the project follows
|
|
5
5
|
[Semantic Versioning](https://semver.org/spec/v2.0.0.html) from 1.0.0.
|
|
6
6
|
|
|
7
|
+
## [3.0.0] — 2026-11-15
|
|
8
|
+
|
|
9
|
+
The sidebar card was not a card, and the plugin had a second language it did not need.
|
|
10
|
+
|
|
11
|
+
### Changed — the card
|
|
12
|
+
|
|
13
|
+
The screenshot that prompted this showed three plugin cards in a row: two with white
|
|
14
|
+
rounded shells, a status dot, a version chip and numeric rows; one — this one — as bare
|
|
15
|
+
text on the sidebar background, two sentences of prose and nothing clickable.
|
|
16
|
+
|
|
17
|
+
The difference was measurable, not aesthetic. `dsh-ustc-quota`'s shell carries
|
|
18
|
+
`box-shadow: 0 3px 12px`, `flex: 0 0 100%`, `order: 1`, `grid-column: 1 / -1`,
|
|
19
|
+
`border-radius: 12px` and `font-variant-numeric: tabular-nums`. This card had none of them,
|
|
20
|
+
so it sat in the same visual plane as the sidebar and looked like a note pasted over it.
|
|
21
|
+
|
|
22
|
+
Now:
|
|
23
|
+
|
|
24
|
+
- **Shell** — the same box model the neighbouring plugins use, so three cards in one column
|
|
25
|
+
read as three cards.
|
|
26
|
+
- **Header** — title, a dim monospace version chip, a 5px status dot (brand / warn / error),
|
|
27
|
+
and a right-aligned `updated <time>` that truncates instead of wrapping.
|
|
28
|
+
- **Numbers** — `Verified / Pending / Disputed` in a three-column grid, tabular figures,
|
|
29
|
+
amber for pending and red for disputed.
|
|
30
|
+
- **Budget** — a thin bar of index bytes against `sectionMaxBytes`, turning amber past 85%.
|
|
31
|
+
- **Usage** — `Injected / Opened / Dead` (the last one only when non-zero), which is the row
|
|
32
|
+
that makes schema v2's counters visible where a person will actually see them.
|
|
33
|
+
- **Empty state** — one line and one instruction instead of two sentences of apology.
|
|
34
|
+
|
|
35
|
+
This reverses the earlier "a page of notes, not a dashboard" stance *for this surface*. The
|
|
36
|
+
audit had already called that stance out: an aesthetic refusal to interrupt, applied to the
|
|
37
|
+
one place where a person must act. The panel keeps the prose; the card gets the numbers.
|
|
38
|
+
|
|
39
|
+
### Changed — one language
|
|
40
|
+
|
|
41
|
+
Chinese was not in the client because anyone chose it; the client shipped a full `ZH`
|
|
42
|
+
dictionary, so a browser whose locale resolved to `zh` rendered the UI in Chinese. The
|
|
43
|
+
English dictionary was already complete. The second dictionary was the only reason the
|
|
44
|
+
product had a language at all.
|
|
45
|
+
|
|
46
|
+
- `client.js`: the 128-line `ZH` dictionary is gone, the locale registration is `{ en: EN }`,
|
|
47
|
+
and four Chinese comments are rewritten.
|
|
48
|
+
- `locale/zh.json`: deleted; `locale/en.json` is the only locale file.
|
|
49
|
+
- `package.json` / `manifests`: no longer advertise a second language. `README.md` loses the
|
|
50
|
+
`记忆宫殿` gloss and the `更新于` label description; the Chinese *examples* of stored
|
|
51
|
+
content stay, because they demonstrate CJK retrieval.
|
|
52
|
+
- `scripts/capture-panel.mjs`: its panel selector matched `记忆|Memory` and can only ever
|
|
53
|
+
match the latter now.
|
|
54
|
+
|
|
55
|
+
Kept, deliberately: the Chinese regexes in `capture.js` (they match Chinese input) and in
|
|
56
|
+
`sanitize.js` (they match Chinese prompt-injection phrasings). Deleting those would disable
|
|
57
|
+
the features for the language most of the user's memories are written in. They are pattern
|
|
58
|
+
data, not output.
|
|
59
|
+
|
|
60
|
+
### Added
|
|
61
|
+
|
|
62
|
+
- `test/unit/client-i18n.test.mjs` — no Chinese in `client.js`, no `ZH` dictionary, no
|
|
63
|
+
`locale/zh.json`, every `t()` key defined, and the card still has its shell/metric pieces.
|
|
64
|
+
- `test/unit/english-only.test.mjs` — no Chinese in emitted strings under `src/` outside a
|
|
65
|
+
two-file allowlist, and the allowlisted files must still contain their patterns (a guard
|
|
66
|
+
that silently allows a file would hide the deletion it exists to catch).
|
|
67
|
+
- `scripts/check-manifest.mjs` now **fails if `locale/zh.json` reappears**, so the language
|
|
68
|
+
cannot come back in a version that never touched `client.js`.
|
|
69
|
+
|
|
70
|
+
### Still Chinese, on purpose
|
|
71
|
+
|
|
72
|
+
`README.zh.md`, `docs/*.md`, and the `titleZh`/`descriptionZh` fields in
|
|
73
|
+
`marketplace/entry.json` are documentation and storefront metadata, not output the plugin
|
|
74
|
+
emits at runtime. They were left alone rather than deleted; if they should go too, that is a
|
|
75
|
+
separate decision about the deliverable, not about the product's language.
|
|
76
|
+
|
|
77
|
+
## [2.11.0] — 2026-11-15
|
|
78
|
+
|
|
79
|
+
One fix, made because it is the last thing that could put another project's facts into a
|
|
80
|
+
conversation.
|
|
81
|
+
|
|
82
|
+
### Fixed
|
|
83
|
+
|
|
84
|
+
- **A session with no `cwd` borrowed the Host process directory as its scope.**
|
|
85
|
+
`resolveScopes` fell back to `process.cwd()` whenever the host did not supply
|
|
86
|
+
`session.header.cwd`. Every such session and every panel request therefore shared one
|
|
87
|
+
workspace key — the directory the Host process was started in, which for a packaged
|
|
88
|
+
install is the install directory. Two consequences: memories written there belonged to no
|
|
89
|
+
project, and any two cwd-less sessions could read each other's entries.
|
|
90
|
+
|
|
91
|
+
A missing cwd now yields an explicit `workspace:unresolved` / `project:unresolved`. It
|
|
92
|
+
collides with no real workspace, appears in no other session's read set, and the panel
|
|
93
|
+
labels it "unresolved workspace" — visible rather than quiet.
|
|
94
|
+
|
|
95
|
+
The trade is deliberate: a cwd-less session loses access to workspace memories instead of
|
|
96
|
+
being handed someone else's. That is the direction the I4′ invariant points.
|
|
97
|
+
|
|
98
|
+
### Docs
|
|
99
|
+
|
|
100
|
+
- `PRIOR-ART.md` I4′ and its expiry table said the process-cwd fallback was "a known risk,
|
|
101
|
+
not a guarantee". It is now a stated behaviour with a test, and the remaining half of that
|
|
102
|
+
risk (project-scope collapse past 12 ancestor directories, no `doctor` check on scope
|
|
103
|
+
identity) is named as still open.
|
|
104
|
+
|
|
7
105
|
## [2.10.1] — 2026-11-15
|
|
8
106
|
|
|
9
107
|
Three more ways a scope mistake could have answered with the wrong data. Found by checking
|
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# dsh-loci · Loci
|
|
1
|
+
# dsh-loci · Loci
|
|
2
2
|
|
|
3
3
|
**A long-term memory for DeepSeek Harness where the prompt only ever sees the palace's index — every room is one lookup away.**
|
|
4
4
|
|
|
@@ -86,7 +86,7 @@ one of them.
|
|
|
86
86
|
## Where you see it
|
|
87
87
|
|
|
88
88
|
- **Sidebar note** — a small card beside Settings, like the quota and advisor
|
|
89
|
-
cards
|
|
89
|
+
cards: a shadowed card in the sidebar footer with a status dot, a `updated <time>` stamp and numeric rows (verified / pending / disputed, index bytes against the budget, injected / opened / dead weight)
|
|
90
90
|
over one short paragraph of what the agent currently understands in this
|
|
91
91
|
project (a consensus, a lesson, a fact), plus one quiet line if something is
|
|
92
92
|
waiting for your review. It answers "did it take that in?" — no counters.
|
package/client.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* - a **sidebar mirror** (`sidebar.footer.action`): a flat, dense card that
|
|
7
7
|
* shows what memory changed today and last week, with the last few changes
|
|
8
|
-
* as one-liners. It is the
|
|
8
|
+
* as one-liners. It is the sidebar mirror — glanceable, never modal;
|
|
9
9
|
* - a **panel** (`sidebar.panellist` + `main`): the full view, with a feed,
|
|
10
10
|
* the exact injected index, search, the review queue and statistics.
|
|
11
11
|
*
|
|
@@ -34,8 +34,16 @@ window.__ModuleLoader__.load({
|
|
|
34
34
|
'Layered long-term memory: the prompt holds a stable index, full text is one lookup away, and only verified facts are injected.',
|
|
35
35
|
|
|
36
36
|
'card.title': 'Memory',
|
|
37
|
-
'card.empty': 'Nothing
|
|
38
|
-
'card.
|
|
37
|
+
'card.empty': 'Nothing saved yet',
|
|
38
|
+
'card.metrics': 'Memory at a glance',
|
|
39
|
+
'card.emptyHint': 'Ask the agent to remember something, then confirm it here.',
|
|
40
|
+
'card.verified': 'Verified',
|
|
41
|
+
'card.pending': 'Pending',
|
|
42
|
+
'card.disputed': 'Disputed',
|
|
43
|
+
'card.injected': 'Injected',
|
|
44
|
+
'card.opened': 'Opened',
|
|
45
|
+
'card.dead': 'Dead',
|
|
46
|
+
'card.pendingHint': '{n} item(s) waiting for you in Review.',
|
|
39
47
|
'card.updated': 'updated',
|
|
40
48
|
'card.pending': '{n} still need your review',
|
|
41
49
|
'card.pinned': 'pinned',
|
|
@@ -157,151 +165,35 @@ window.__ModuleLoader__.load({
|
|
|
157
165
|
'panel.version': 'version',
|
|
158
166
|
};
|
|
159
167
|
|
|
160
|
-
const ZH = {
|
|
161
|
-
'meta.title': 'Loci 记忆宫殿',
|
|
162
|
-
'meta.description':
|
|
163
|
-
'分层长期记忆:提示词里只放稳定索引,全文按需一次取回;只有已核实的记忆才会被注入。',
|
|
164
|
-
|
|
165
|
-
'card.title': '记忆',
|
|
166
|
-
'card.empty': '还没有真正记下的东西',
|
|
167
|
-
'card.emptyHint': '你确认过的事实会出现在这里',
|
|
168
|
-
'card.updated': '更新于',
|
|
169
|
-
'card.pending': '另有 {n} 条我拿不准,等你看一眼',
|
|
170
|
-
'card.pinned': '置顶',
|
|
171
|
-
'card.open': '打开记忆',
|
|
172
|
-
'card.check': '检查更新',
|
|
173
|
-
'card.upToDate': '已是最新',
|
|
174
|
-
'card.updateTo': '有新版本',
|
|
175
|
-
'card.checkOff': '更新检查已关闭',
|
|
176
|
-
'card.checking': '检查中…',
|
|
177
|
-
'card.unknown': '无法检查版本',
|
|
178
|
-
'card.unpublished': '尚未发布',
|
|
179
|
-
|
|
180
|
-
'prose.agreed': '已定:',
|
|
181
|
-
'prose.lesson': '经验:',
|
|
182
|
-
|
|
183
|
-
'kind.rule': '共识',
|
|
184
|
-
'kind.procedural': '经验',
|
|
185
|
-
'kind.semantic': '事实',
|
|
186
|
-
'kind.episodic': '事件',
|
|
187
|
-
'kind.working': '草稿',
|
|
188
|
-
|
|
189
|
-
'since.now': '刚才',
|
|
190
|
-
'since.minutes': '{n} 分钟前',
|
|
191
|
-
'since.hours': '{n} 小时前',
|
|
192
|
-
'since.yesterday': '昨天',
|
|
193
|
-
'since.days': '{n} 天前',
|
|
194
|
-
|
|
195
|
-
'action.added': '新增',
|
|
196
|
-
'action.revised': '更正',
|
|
197
|
-
'action.verified': '核实',
|
|
198
|
-
'action.disputed': '存疑',
|
|
199
|
-
'action.pinned': '置顶',
|
|
200
|
-
'action.unpinned': '取消置顶',
|
|
201
|
-
'action.deleted': '删除',
|
|
202
|
-
'action.purged': '彻底删除',
|
|
203
|
-
'action.restored': '恢复',
|
|
204
|
-
'action.imported': '导入',
|
|
205
|
-
'action.maintained': '维护',
|
|
206
|
-
|
|
207
|
-
'settings.title': 'Loci',
|
|
208
|
-
'settings.intro': '给 agent 的长期记忆。标注「即时」的改动立即生效。',
|
|
209
|
-
'settings.unavailable': '当前缺少设置服务,以下显示实际生效的配置。',
|
|
210
|
-
'settings.saved': '已保存',
|
|
211
|
-
'settings.rejected': '宿主拒绝写入',
|
|
212
|
-
'settings.failed': '写入失败',
|
|
213
|
-
'settings.budget': '索引预算',
|
|
214
|
-
'settings.budgetHint': '整段注入的字节上限',
|
|
215
|
-
'settings.scope': '新事实的默认作用域',
|
|
216
|
-
'settings.capture': '会话捕获',
|
|
217
|
-
'settings.assist': '首轮辅助召回',
|
|
218
|
-
'settings.maintenance': '后台维护',
|
|
219
|
-
'settings.secret': '记忆中的密钥',
|
|
220
|
-
'settings.vector': '向量通道',
|
|
221
|
-
'settings.diagnostics': '诊断',
|
|
222
|
-
'settings.danger': '清空记忆',
|
|
223
|
-
'settings.dangerHint': '输入确认短语才会执行。条目为软删除:历史仍可读、可导出。',
|
|
224
|
-
'settings.dangerPhrase': 'forget everything',
|
|
225
|
-
'settings.dangerRun': '删除全部记忆',
|
|
226
|
-
'settings.dangerBusy': '删除中…',
|
|
227
|
-
'settings.dangerDone': '已删除 {n} 条。',
|
|
228
|
-
'off': '关闭',
|
|
229
|
-
'captureRules': '仅规则',
|
|
230
|
-
'secretRedact': '脱敏',
|
|
231
|
-
'secretReject': '拒绝写入',
|
|
232
|
-
'vectorOff': '关闭(仅词法)',
|
|
233
|
-
'vectorHttp': 'HTTP 端点',
|
|
234
|
-
|
|
235
|
-
'panel.title': '记忆',
|
|
236
|
-
'panel.feed': '动态',
|
|
237
|
-
'panel.index': '索引',
|
|
238
|
-
'panel.search': '检索',
|
|
239
|
-
'panel.review': '待审',
|
|
240
|
-
'panel.stats': '统计',
|
|
241
|
-
'panel.refresh': '刷新',
|
|
242
|
-
'panel.loading': '加载中…',
|
|
243
|
-
'panel.empty': '这里还没有内容。',
|
|
244
|
-
'panel.feedHint': '记住了什么、更正了什么、核实或删除了什么。最新的在前。',
|
|
245
|
-
'panel.indexHint': '与模型系统提示里完全一致的那一段,以及它的字节开销。',
|
|
246
|
-
'panel.bytes': '字节',
|
|
247
|
-
'panel.listed': '已列出',
|
|
248
|
-
'panel.hidden': '未列出',
|
|
249
|
-
'panel.searchPlaceholder': '检索标题与正文…',
|
|
250
|
-
'panel.searchEmpty': '没有匹配。',
|
|
251
|
-
'panel.more': '加载更多',
|
|
252
|
-
'panel.total': '总数',
|
|
253
|
-
'panel.verify': '核实',
|
|
254
|
-
'panel.dispute': '标记争议',
|
|
255
|
-
'panel.delete': '删除',
|
|
256
|
-
'panel.restore': '恢复',
|
|
257
|
-
'panel.pin': '置顶',
|
|
258
|
-
'panel.unpin': '取消置顶',
|
|
259
|
-
'panel.back': '返回',
|
|
260
|
-
'panel.history': '历史',
|
|
261
|
-
'panel.evidence': '证据',
|
|
262
|
-
'panel.source': '出处',
|
|
263
|
-
'panel.lesson': '教训',
|
|
264
|
-
'panel.lesson.pattern': '问题形态',
|
|
265
|
-
'panel.lesson.fix': '做法',
|
|
266
|
-
'panel.lesson.prevents': '不做会怎样',
|
|
267
|
-
'panel.lesson.when': '适用场景',
|
|
268
|
-
'panel.relations': '关联',
|
|
269
|
-
'panel.candidates': '候选',
|
|
270
|
-
'panel.disputed': '争议',
|
|
271
|
-
'panel.noCandidates': '没有待审条目。',
|
|
272
|
-
'panel.reviewHint': '这里只列可能被注入的条目;过去会话的摘要属于历史,不是主张。',
|
|
273
|
-
'panel.reject': '驳回',
|
|
274
|
-
'panel.digestOf': '同一批次提出 {n} 条',
|
|
275
|
-
'panel.verifyGroup': '核实这 {n} 条',
|
|
276
|
-
'panel.verifyAll': '全部核实({n})',
|
|
277
|
-
'panel.actionFailed': '操作失败',
|
|
278
|
-
'panel.injections': '最近注入',
|
|
279
|
-
'panel.deadWeight': '注入了但从未被打开',
|
|
280
|
-
'panel.injected': '次注入',
|
|
281
|
-
'panel.opened': '次打开',
|
|
282
|
-
'panel.store': '存储',
|
|
283
|
-
'panel.confirmDelete': '软删除这条记忆?',
|
|
284
|
-
'panel.error': '请求失败',
|
|
285
|
-
'panel.updated': '更新于',
|
|
286
|
-
'panel.version': '版本',
|
|
287
|
-
};
|
|
288
|
-
|
|
289
168
|
const CSS = `
|
|
290
169
|
.loci-root { display: flex; flex-direction: column; height: 100%; min-height: 0; color: var(--dsw-alias-label-primary); font-size: 13px; }
|
|
291
|
-
.loci-card-root { display: flex; flex-direction: column; gap:
|
|
170
|
+
.loci-card-root { display: flex; flex-direction: column; gap: 5px; flex: 0 0 100%; order: 1; grid-column: 1 / -1; width: 100%; min-width: 0; box-sizing: border-box; margin: 0 0 6px; padding: 10px 9px 8px; border: 1px solid var(--dsw-alias-border-l1); border-radius: 12px; background: var(--dsw-alias-bg-layer-1); box-shadow: 0 3px 12px rgba(0, 0, 0, .16); cursor: pointer; text-align: left; font: inherit; color: var(--dsw-alias-label-primary); font-size: 11px; line-height: 1.3; font-variant-numeric: tabular-nums; }
|
|
292
171
|
.loci-card-root:hover { background: var(--dsw-alias-bg-layer-2); }
|
|
293
|
-
.loci-card-root.is-rail { padding: 6px; align-items: center; justify-content: center; }
|
|
294
|
-
.loci-card-head { display: flex; align-items: center; gap: 6px; width: 100%; }
|
|
295
|
-
.loci-card-title { font-size:
|
|
296
|
-
.loci-card-
|
|
297
|
-
.loci-card-ver {
|
|
172
|
+
.loci-card-root.is-rail { flex: 0 0 auto; padding: 6px; align-items: center; justify-content: center; }
|
|
173
|
+
.loci-card-head { display: flex; align-items: center; gap: 6px; min-width: 0; width: 100%; }
|
|
174
|
+
.loci-card-title { font-size: 13px; font-weight: 700; letter-spacing: -.01em; white-space: nowrap; }
|
|
175
|
+
.loci-card-ver { flex: 0 0 auto; margin: 0; padding: 0; border: 0; background: transparent; color: var(--dsw-alias-label-secondary); font-size: 8.5px; line-height: 1.2; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; cursor: pointer; white-space: nowrap; }
|
|
176
|
+
.loci-card-ver:hover { color: var(--dsw-alias-label-primary); }
|
|
177
|
+
.loci-card-dot { width: 5px; height: 5px; border-radius: 50%; background: var(--dsw-alias-brand-primary); flex: 0 0 auto; }
|
|
178
|
+
.loci-card-dot.is-warn { background: var(--dsw-alias-state-warn-primary); }
|
|
179
|
+
.loci-card-dot.is-error { background: var(--dsw-alias-state-error-primary); }
|
|
180
|
+
.loci-card-meta { flex: 1 1 auto; min-width: 0; font-size: 8.5px; color: var(--dsw-alias-label-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; text-align: right; }
|
|
298
181
|
.loci-card-mark { display: flex; align-items: center; color: var(--dsw-alias-brand-primary); flex: 0 0 auto; }
|
|
299
|
-
.loci-card-
|
|
300
|
-
.loci-card-
|
|
301
|
-
.loci-card-
|
|
302
|
-
.loci-card-
|
|
303
|
-
.loci-card-
|
|
304
|
-
.loci-card-
|
|
182
|
+
.loci-card-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 3px 8px; width: 100%; }
|
|
183
|
+
.loci-card-metric { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
|
|
184
|
+
.loci-card-metric-label { font-size: 8px; color: var(--dsw-alias-label-secondary); text-transform: uppercase; letter-spacing: .05em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
185
|
+
.loci-card-metric-value { font-size: 12px; font-weight: 600; color: var(--dsw-alias-label-primary); font-variant-numeric: tabular-nums; }
|
|
186
|
+
.loci-card-metric-value.is-warn { color: var(--dsw-alias-state-warn-primary); }
|
|
187
|
+
.loci-card-metric-value.is-error { color: var(--dsw-alias-state-error-primary); }
|
|
188
|
+
.loci-card-barline { display: flex; align-items: center; gap: 6px; width: 100%; }
|
|
189
|
+
.loci-card-bar { flex: 1 1 auto; min-width: 20px; height: 4px; border-radius: 999px; background: var(--dsw-alias-bg-layer-2); overflow: hidden; }
|
|
190
|
+
.loci-card-bar-fill { height: 100%; border-radius: 999px; background: var(--dsw-alias-brand-primary); }
|
|
191
|
+
.loci-card-bar-fill.is-warn { background: var(--dsw-alias-state-warn-primary); }
|
|
192
|
+
.loci-card-bar-label { font-size: 8.5px; color: var(--dsw-alias-label-secondary); white-space: nowrap; }
|
|
193
|
+
.loci-card-foot { display: flex; align-items: center; gap: 6px; width: 100%; font-size: 8.5px; color: var(--dsw-alias-label-secondary); }
|
|
194
|
+
.loci-card-foot-grow { flex: 1 1 auto; }
|
|
195
|
+
.loci-card-empty { font-size: 9.5px; color: var(--dsw-alias-label-secondary); }
|
|
196
|
+
.loci-card-hint { font-size: 9px; color: var(--dsw-alias-label-secondary); }
|
|
305
197
|
.loci-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--dsw-alias-brand-primary); flex: 0 0 auto; }
|
|
306
198
|
|
|
307
199
|
.loci-bar { display: flex; align-items: center; gap: 12px; padding: 10px 16px; border-bottom: 1px solid var(--dsw-alias-border-l1); }
|
|
@@ -420,7 +312,7 @@ window.__ModuleLoader__.load({
|
|
|
420
312
|
*
|
|
421
313
|
* The card is a page of notes, so this reads as prose rather than a list: one
|
|
422
314
|
* clause per entry, joined with 。or , and cut when it stops fitting. Consensus
|
|
423
|
-
* items lead with
|
|
315
|
+
* items lead with the agreement verb and a fact leads with its own subject, because "we
|
|
424
316
|
* agreed" and "it happens to be true" are different kinds of knowing.
|
|
425
317
|
*/
|
|
426
318
|
function proseOf(notes, t) {
|
|
@@ -452,7 +344,7 @@ window.__ModuleLoader__.load({
|
|
|
452
344
|
/**
|
|
453
345
|
* How long ago something was understood, in words rather than numbers.
|
|
454
346
|
*
|
|
455
|
-
* "
|
|
347
|
+
* "yesterday" and "just now" are the whole point of the card: they are what makes a
|
|
456
348
|
* reader believe the memory is alive. A bare timestamp makes them do the
|
|
457
349
|
* arithmetic instead.
|
|
458
350
|
*/
|
|
@@ -503,41 +395,35 @@ window.__ModuleLoader__.load({
|
|
|
503
395
|
}
|
|
504
396
|
|
|
505
397
|
/** The small version line shared by the card and the panel footer. */
|
|
506
|
-
function
|
|
507
|
-
|
|
398
|
+
function VersionLabel({ t, update, onCheck, checking }) {
|
|
399
|
+
// A chip, not a sentence. The old label said "update check is off" inline, which
|
|
400
|
+
// is why the card header read like body copy; the state now lives in the tooltip.
|
|
401
|
+
const version = update?.version;
|
|
508
402
|
const hasUpdate = update?.updateAvailable === true;
|
|
509
|
-
const
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
const checkDisabled = update?.checkEnabled === false;
|
|
513
|
-
const text = hasUpdate
|
|
514
|
-
? `${t('card.updateTo')}${update.latestVersion ? ` · v${update.latestVersion}` : ''}`
|
|
515
|
-
: unknown
|
|
403
|
+
const note = checking
|
|
404
|
+
? t('card.checking')
|
|
405
|
+
: update === undefined || update === null
|
|
516
406
|
? t('card.unknown')
|
|
517
|
-
:
|
|
518
|
-
? t('card.
|
|
519
|
-
: update
|
|
520
|
-
? t('card.
|
|
521
|
-
: t('card.
|
|
407
|
+
: hasUpdate
|
|
408
|
+
? t('card.updateTo')
|
|
409
|
+
: update.status === 'disabled'
|
|
410
|
+
? t('card.checkOff')
|
|
411
|
+
: t('card.upToDate');
|
|
522
412
|
return h(
|
|
523
|
-
'
|
|
524
|
-
{
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
onClick: (event) => {
|
|
534
|
-
event.stopPropagation();
|
|
535
|
-
onCheck?.();
|
|
536
|
-
},
|
|
413
|
+
'button',
|
|
414
|
+
{
|
|
415
|
+
type: 'button',
|
|
416
|
+
className: 'loci-card-ver',
|
|
417
|
+
title: note,
|
|
418
|
+
'aria-label': note,
|
|
419
|
+
disabled: checking,
|
|
420
|
+
onClick: (event) => {
|
|
421
|
+
event.stopPropagation();
|
|
422
|
+
onCheck?.();
|
|
537
423
|
},
|
|
538
|
-
|
|
539
|
-
),
|
|
540
|
-
hasUpdate ? h('span', { className: 'loci-dot' }) : null,
|
|
424
|
+
},
|
|
425
|
+
version ? `v${version}` : t('card.unpublished'),
|
|
426
|
+
hasUpdate ? h('span', { className: 'loci-card-dot is-error', 'aria-hidden': true }) : null,
|
|
541
427
|
);
|
|
542
428
|
}
|
|
543
429
|
|
|
@@ -547,10 +433,12 @@ window.__ModuleLoader__.load({
|
|
|
547
433
|
const t = props?.t ?? ((key) => key);
|
|
548
434
|
const wide = props?.wide !== false;
|
|
549
435
|
const open = props?.openPanel;
|
|
550
|
-
//
|
|
551
|
-
//
|
|
552
|
-
//
|
|
553
|
-
|
|
436
|
+
// A compact readout, not a note: the counts are the reason a person looks at this
|
|
437
|
+
// strip at all. The earlier version showed one prose sentence and no numbers, which
|
|
438
|
+
// read as bare text inside the sidebar rather than as a card — and the audit's own
|
|
439
|
+
// "no dashboard" objection was about refusing to interrupt, not about hiding data.
|
|
440
|
+
const stats = useJson('/stats', { intervalMs: 30_000 });
|
|
441
|
+
const config = useJson('/config');
|
|
554
442
|
const update = useJson('/update');
|
|
555
443
|
const [checking, setChecking] = useState(false);
|
|
556
444
|
const [fresh, setFresh] = useState(undefined);
|
|
@@ -567,9 +455,19 @@ window.__ModuleLoader__.load({
|
|
|
567
455
|
}
|
|
568
456
|
}, []);
|
|
569
457
|
|
|
570
|
-
const data =
|
|
571
|
-
const
|
|
572
|
-
const
|
|
458
|
+
const data = stats.data;
|
|
459
|
+
const byStatus = data?.byStatus ?? {};
|
|
460
|
+
const verified = Number(byStatus.verified ?? 0);
|
|
461
|
+
const pending = Number(data?.pendingCandidates ?? byStatus.candidate ?? 0);
|
|
462
|
+
const disputed = Number(data?.disputed ?? byStatus.disputed ?? 0);
|
|
463
|
+
const entries = Number(data?.heads ?? 0);
|
|
464
|
+
const injection = data?.lastInjection ?? null;
|
|
465
|
+
const indexBytes = Number(injection?.bytes ?? 0);
|
|
466
|
+
const budget = Number(config.data?.config?.sectionMaxBytes ?? 0);
|
|
467
|
+
const usage = Array.isArray(data?.usage) ? data.usage : [];
|
|
468
|
+
const injected = usage.reduce((total, row) => total + Number(row.injections ?? 0), 0);
|
|
469
|
+
const opened = usage.reduce((total, row) => total + Number(row.reads ?? 0), 0);
|
|
470
|
+
const dead = usage.filter((row) => Number(row.injections ?? 0) > 0 && Number(row.reads ?? 0) === 0).length;
|
|
573
471
|
|
|
574
472
|
if (!wide) {
|
|
575
473
|
return h(
|
|
@@ -588,6 +486,16 @@ window.__ModuleLoader__.load({
|
|
|
588
486
|
);
|
|
589
487
|
}
|
|
590
488
|
|
|
489
|
+
const dotClass = stats.error !== undefined ? 'is-error' : pending > 0 || disputed > 0 ? 'is-warn' : '';
|
|
490
|
+
const stamp = data === undefined ? '' : stampOf(injection?.ts ?? data.generatedAt ?? Date.now());
|
|
491
|
+
const metric = (label, value, tone) =>
|
|
492
|
+
h(
|
|
493
|
+
'div',
|
|
494
|
+
{ className: 'loci-card-metric' },
|
|
495
|
+
h('span', { className: 'loci-card-metric-label' }, label),
|
|
496
|
+
h('span', { className: `loci-card-metric-value${tone ? ` ${tone}` : ''}` }, String(value)),
|
|
497
|
+
);
|
|
498
|
+
|
|
591
499
|
return h(
|
|
592
500
|
'div',
|
|
593
501
|
{
|
|
@@ -616,34 +524,56 @@ window.__ModuleLoader__.load({
|
|
|
616
524
|
),
|
|
617
525
|
),
|
|
618
526
|
h('span', { className: 'loci-card-title' }, t('card.title')),
|
|
619
|
-
h(
|
|
620
|
-
h(
|
|
527
|
+
h(VersionLabel, { t, update: fresh ?? update.data, onCheck: check, checking }),
|
|
528
|
+
h('span', { className: `loci-card-dot ${dotClass}`.trim(), 'aria-hidden': true }),
|
|
529
|
+
h('span', { className: 'loci-card-meta' }, stamp ? `${t('card.updated')} ${stamp}` : ''),
|
|
621
530
|
),
|
|
622
|
-
|
|
623
|
-
? h('div', { className: 'loci-card-empty' },
|
|
624
|
-
:
|
|
625
|
-
? h(
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
h('div', { className: 'loci-card-empty' }, t('card.empty')),
|
|
629
|
-
h('div', { className: 'loci-card-hint' }, t('card.emptyHint')),
|
|
630
|
-
)
|
|
631
|
-
: h(
|
|
632
|
-
'div',
|
|
633
|
-
{ className: 'loci-card-note' },
|
|
634
|
-
h(
|
|
531
|
+
stats.error !== undefined
|
|
532
|
+
? h('div', { className: 'loci-card-empty' }, stats.error)
|
|
533
|
+
: data === undefined
|
|
534
|
+
? h('div', { className: 'loci-card-empty' }, t('panel.loading'))
|
|
535
|
+
: entries === 0 && pending === 0 && disputed === 0
|
|
536
|
+
? h(
|
|
635
537
|
'div',
|
|
636
|
-
|
|
637
|
-
|
|
538
|
+
null,
|
|
539
|
+
h('div', { className: 'loci-card-empty' }, t('card.empty')),
|
|
540
|
+
h('div', { className: 'loci-card-hint' }, t('card.emptyHint')),
|
|
541
|
+
)
|
|
542
|
+
: h(
|
|
543
|
+
'div',
|
|
544
|
+
{ className: 'loci-card-grid' },
|
|
545
|
+
metric(t('card.verified'), verified),
|
|
546
|
+
metric(t('card.pending'), pending, pending > 0 ? 'is-warn' : ''),
|
|
547
|
+
metric(t('card.disputed'), disputed, disputed > 0 ? 'is-error' : ''),
|
|
638
548
|
),
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
549
|
+
data !== undefined && entries > 0 && budget > 0
|
|
550
|
+
? h(
|
|
551
|
+
'div',
|
|
552
|
+
{ className: 'loci-card-barline' },
|
|
553
|
+
h(
|
|
554
|
+
'div',
|
|
555
|
+
{ className: 'loci-card-bar', role: 'presentation' },
|
|
556
|
+
h('div', {
|
|
557
|
+
className: `loci-card-bar-fill${indexBytes / budget > 0.85 ? ' is-warn' : ''}`,
|
|
558
|
+
style: { width: `${Math.min(100, Math.round((indexBytes / budget) * 100))}%` },
|
|
559
|
+
}),
|
|
643
560
|
),
|
|
561
|
+
h('span', { className: 'loci-card-bar-label' }, `${indexBytes} / ${budget} B`),
|
|
562
|
+
)
|
|
563
|
+
: null,
|
|
564
|
+
data !== undefined && injected > 0
|
|
565
|
+
? h(
|
|
566
|
+
'div',
|
|
567
|
+
{ className: 'loci-card-foot' },
|
|
568
|
+
h('span', null, `${t('card.injected')} ${injected}`),
|
|
569
|
+
h('span', null, `${t('card.opened')} ${opened}`),
|
|
570
|
+
dead > 0 ? h('span', null, `${t('card.dead')} ${dead}`) : null,
|
|
571
|
+
h('span', { className: 'loci-card-foot-grow' }),
|
|
572
|
+
)
|
|
573
|
+
: null,
|
|
574
|
+
pending > 0 ? h('div', { className: 'loci-card-hint' }, t('card.pendingHint').replace('{n}', String(pending))) : null,
|
|
644
575
|
);
|
|
645
576
|
}
|
|
646
|
-
|
|
647
577
|
// ---------------------------------------------------------------- panel
|
|
648
578
|
|
|
649
579
|
function FeedTab({ t, onOpen }) {
|
|
@@ -1434,11 +1364,11 @@ window.__ModuleLoader__.load({
|
|
|
1434
1364
|
}
|
|
1435
1365
|
|
|
1436
1366
|
function apply(ctx) {
|
|
1437
|
-
ctx.effect(() => ctx.locale.register(NS, {
|
|
1367
|
+
ctx.effect(() => ctx.locale.register(NS, { en: EN }), 'dsh-loci: locale dictionaries');
|
|
1438
1368
|
const bind = ctx.locale.bind(NS);
|
|
1439
1369
|
const t = tOf(bind);
|
|
1440
1370
|
|
|
1441
|
-
// The
|
|
1371
|
+
// The sidebar mirror: a card beside Settings, always visible while the sidebar is.
|
|
1442
1372
|
ctx.slots.inject('sidebar.footer.action', () =>
|
|
1443
1373
|
ctx.slots.register(
|
|
1444
1374
|
{
|
package/docs/PRIOR-ART.md
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
| **I1** | 注入闸门 | 只有 `status='verified'` 且未过期、未删除的条目进入提示词。`status` 由 store 重新派生,模型无法通过工具断言它。verified 来自**面板/CLI 的人工操作**,或**用户自己消息里逐字存在且与主张有实质关联的引文**(§2.1 的两问闸门)。 |
|
|
19
19
|
| **I2′** | 前缀稳定 | **没有任何写入会重排或改写已有行**;重求值只发生在会话/日边界,绝不在步内。尾部(`as of` 日期、`+N more`、待审提示)允许随写入与 UTC 日期变化。 |
|
|
20
20
|
| **I3** | 默认零网络 | 默认配置下不发任何请求。三条可选出网路径:更新检查、HTTP 向量通道、会话结束的抽取(经宿主 `ctx.llm`,非本进程直连)。前两条默认关闭,第三条默认 `off`。 |
|
|
21
|
-
| **I4′** | 作用域显式 |
|
|
21
|
+
| **I4′** | 作用域显式 | **会话内的每一项读取都绑定显式键集**,不使用通配;包括两个按 id 寻址的工具(`loci_get`/`loci_forget`)和全部按 id 寻址的写操作。面板/导出枚举全部键,那是**明确的行政通道**(管理员视图),不是会话读取。作用域键由宿主 `session.header.cwd` 派生;**该字段缺失时返回显式的 `workspace:unresolved`,不再回落进程 cwd**。 |
|
|
22
22
|
|
|
23
23
|
> **旧版 I2 / I4 为什么被替换**:I2 原文"无写入时字节级不变"在 UTC 零点自破(尾部日期),且在 TTL 到期时于会话中途静默改变内容;I4 原文"读永不越界"与 `allScopeKeys()` 的通配枚举矛盾。把它们改宽不是妥协,是把判据改到能被代码验证的精度。
|
|
24
24
|
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
| I1 | 结构(数据层)/ 语义依赖 | 宿主持续发 `session/event` 让引文缓冲存在 | 缓冲缺失 → 闸门**更严**(fail-safe,全部变候选);引文门槛或 `autoVerifyCaptured` 默认值改动 → 闸门**更松** | **不会**。所有单测都桩掉验证器,真实函数只在手动 e2e 里跑过 |
|
|
32
32
|
| I2′ | 结构(排序)/ 经济性依赖 | provider 的 prefix cache 粒度 | provider 改为按段缓存或取消前缀缓存 → 经济理由消失(安全性理由仍在) | **不会**。仓库里没有任何 cache 命中量;`request_stat` 记的是字节数 |
|
|
33
33
|
| I3 | 结构(无直连)/ 计数依赖 | 三个默认布尔值 + 枚举完整性 | 新增经宿主服务的出网(如 `ctx.llm` 的另一处调用)→ grep 门看不见 | **部分**。直接 `fetch` 会挂 CI,宿主中介出网不会 |
|
|
34
|
-
| I4′ | 结构(查询)/ 派生依赖 | `session.header.cwd` 存在性 | 会话创建时不带 cwd →
|
|
34
|
+
| I4′ | 结构(查询)/ 派生依赖 | `session.header.cwd` 存在性 | 会话创建时不带 cwd → 2.11.0 起落到显式的 `workspace:unresolved`(不泄漏,但该会话看不到工作区记忆);深目录超过 12 层 → 项目作用域塌成工作区 | **部分**。无 cwd 已由测试固定并与面板标签可见;12 层上限与作用域身份仍无 doctor 检查 |
|
|
35
35
|
|
|
36
36
|
**这套机制本身是这次审计最有价值的产出**:判据要带失效条件,否则它只是当时当地的偏好,被后人当成真理。任何新增不变量必须同时写它的失效信号。
|
|
37
37
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-loci",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "Layered long-term memory for DeepSeek Harness: the prompt sees only a stable index, full text is one lookup away. Bi-temporal facts, strict scope isolation, verified-only injection.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/host/index.js
CHANGED
|
@@ -23,7 +23,7 @@ import { registerCommands } from './commands.js';
|
|
|
23
23
|
import { runDoctor } from './doctor.js';
|
|
24
24
|
|
|
25
25
|
/** Version stamped into the database and diagnostics. */
|
|
26
|
-
export const VERSION = '
|
|
26
|
+
export const VERSION = '3.0.0';
|
|
27
27
|
|
|
28
28
|
/** Optional collaborators; without them the plugin still serves tools. */
|
|
29
29
|
export const inject = ['systemPrompt', 'tools'];
|
package/src/host/scope.js
CHANGED
|
@@ -80,21 +80,31 @@ function pathKey(prefix, path) {
|
|
|
80
80
|
|
|
81
81
|
/**
|
|
82
82
|
* Build the scope identity of one session.
|
|
83
|
+
*
|
|
84
|
+
* When the host does not supply a cwd, the identity is **explicitly unresolved** rather
|
|
85
|
+
* than borrowed from `process.cwd()`. Borrowing it meant every cwd-less session and every
|
|
86
|
+
* panel request shared one scope keyed to wherever the Host process happened to be
|
|
87
|
+
* installed — a silent cross-project bucket, and the opposite of what the explicit-scope
|
|
88
|
+
* invariant promises. An unresolved key appears in no other session's read set, so the
|
|
89
|
+
* failure is visible (the panel labels it "unresolved workspace") instead of a quiet leak.
|
|
90
|
+
*
|
|
83
91
|
* @param options - `sessionId`, `cwd`.
|
|
84
92
|
* @returns `{session, workspace, project, global, labels, cwd, projectRoot}`.
|
|
85
93
|
*/
|
|
86
94
|
export function resolveScopes({ sessionId, cwd }) {
|
|
87
|
-
const
|
|
88
|
-
const
|
|
95
|
+
const requested = typeof cwd === 'string' && cwd.length > 0 ? cwd : undefined;
|
|
96
|
+
const dir = requested === undefined ? undefined : canonicalDir(requested);
|
|
97
|
+
const projectRoot = dir === undefined ? undefined : findProjectRoot(dir);
|
|
98
|
+
const unresolved = dir === undefined;
|
|
89
99
|
return {
|
|
90
100
|
session: `session:${sessionId ?? 'unknown'}`,
|
|
91
|
-
workspace: pathKey('workspace', dir),
|
|
92
|
-
project: pathKey('project', projectRoot),
|
|
101
|
+
workspace: unresolved ? 'workspace:unresolved' : pathKey('workspace', dir),
|
|
102
|
+
project: unresolved ? 'project:unresolved' : pathKey('project', projectRoot),
|
|
93
103
|
global: 'global:',
|
|
94
104
|
labels: {
|
|
95
105
|
session: sessionId ? `session ${sessionId}` : 'session',
|
|
96
|
-
workspace: basename(dir) || dir,
|
|
97
|
-
project: basename(projectRoot) || projectRoot,
|
|
106
|
+
workspace: unresolved ? 'unresolved workspace' : basename(dir) || dir,
|
|
107
|
+
project: unresolved ? 'unresolved project' : basename(projectRoot) || projectRoot,
|
|
98
108
|
global: 'global',
|
|
99
109
|
},
|
|
100
110
|
cwd: dir,
|
package/src/mcp/store-facade.js
CHANGED