dsh-engram 0.5.0 → 0.5.1
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 +12 -4
- package/README.zh.md +11 -4
- package/docs/RELEASE-NOTES-v0.5.1.md +54 -0
- package/lib/boundary-prune.js +2 -1
- package/lib/client.js +241 -3
- package/lib/session-cjk.js +2 -2
- package/lib/usage.js +47 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -264,8 +264,8 @@ curl -s -X POST http://127.0.0.1:3080/api/dsh-engram/import \
|
|
|
264
264
|
<!-- dsh-market compatibility block — keep labels bilingual and in sync with
|
|
265
265
|
docs/RELEASE-NOTES.TEMPLATE.md (market README convention) -->
|
|
266
266
|
|
|
267
|
-
- **DSH 版本 / DSH versions**: `>=0.1.2-alpha.2 <0.3.0-0` (本版 `0.5.
|
|
268
|
-
- **最后验证日期 / Last verified**: 2026-10-
|
|
267
|
+
- **DSH 版本 / DSH versions**: `>=0.1.2-alpha.2 <0.3.0-0` (本版 `0.5.1`; 已发布的 `0.3.7`、`0.4.0` 仍声明旧的 `0.1.x` 范围 `>=0.1.2-alpha.2 <0.2.0-0` — 见下方矩阵)
|
|
268
|
+
- **最后验证日期 / Last verified**: 2026-10-19 — 会话扫描修复(`session.v<N>.jsonl.zstd`,issue #8)+ 数据导出/导入与替换/冲突可视化(issue #3),npm test 334/334 通过
|
|
269
269
|
- **安装方式已验证 / Verified install**: `dsh plugin --profile web add dsh-engram`
|
|
270
270
|
|
|
271
271
|
> **Short version.** Every **published** release (`0.2.0` … `0.3.6`) supports
|
|
@@ -279,7 +279,8 @@ curl -s -X POST http://127.0.0.1:3080/api/dsh-engram/import \
|
|
|
279
279
|
| --- | --- | --- | --- |
|
|
280
280
|
| `0.2.0` … `0.3.6` (npm, GitHub v0.3.3–v0.3.6) | published | **old DSH only** — `0.1.0-rc.7` / `0.1.0-rc.8` / `0.1.1-rc.1` / `0.1.1-rc.2` | peers `@deepseek-ai/dsh-*@^0.1.0-rc.7` |
|
|
281
281
|
| `0.3.7` / `0.4.0` (npm) | published | **new DSH, settings `installSection`** — `>=0.1.2-alpha.2 <0.2.0-0` | `engines.dsh` + peers `>=0.1.2-alpha.2 <0.2.0-0` |
|
|
282
|
-
| `0.5.0` (
|
|
282
|
+
| `0.5.0` (npm) | published | **DSH `0.1.2-alpha.2` … `<0.3.0-0`** — 设置卡迁移到 entry-id volatile Config(`0.1.7+`/`0.2.x`),`≤0.1.5` 自动回退 `installSection` | `engines.dsh` + peers `>=0.1.2-alpha.2 <0.3.0-0` |
|
|
283
|
+
| `0.5.1` (本期 / this release) | published | 同 `0.5.0` — 会话扫描修复(`session.v<N>.jsonl.zstd`,issue #8)、数据导出/导入 GUI、替换/冲突可视化(issue #3) | 同 `0.5.0` |
|
|
283
284
|
|
|
284
285
|
### Where the API family broke — `0.1.2-alpha.2`
|
|
285
286
|
|
|
@@ -329,7 +330,14 @@ drives mutations).
|
|
|
329
330
|
So: keep DSH on `0.1.1-rc.2` or older while running a release ≤ `0.3.6`, and
|
|
330
331
|
upgrade the plugin to `>=0.3.7` before moving DSH to `0.1.2-alpha.2`.
|
|
331
332
|
|
|
332
|
-
### Current release — `0.5.
|
|
333
|
+
### Current release — `0.5.1`
|
|
334
|
+
|
|
335
|
+
Patch release on top of `0.5.0`: session scanning resolves the **versioned** log
|
|
336
|
+
name (`session.v3/v4.jsonl.zstd`) instead of the hardcoded
|
|
337
|
+
`session.jsonl.zstd` — on current DSH that mismatch made `/stats`, `/toolstats`,
|
|
338
|
+
BoundaryPrune's repayment history and the CJK session search all return zero
|
|
339
|
+
silently (issue #8). The memory viewer also gains data export/import and
|
|
340
|
+
supersede/contradict visualization (issue #3).
|
|
333
341
|
|
|
334
342
|
The published `0.4.0` declared `>=0.1.2-alpha.2 <0.2.0-0` — which the semver
|
|
335
343
|
gate **rejects on every DSH `0.2.0-0` prerelease** (`<0.2.0-0` excludes
|
package/README.zh.md
CHANGED
|
@@ -166,8 +166,8 @@ curl -s -X POST http://127.0.0.1:3080/api/dsh-engram/import \
|
|
|
166
166
|
|
|
167
167
|
<!-- dsh-market 兼容信息块 —— 与 docs/RELEASE-NOTES.TEMPLATE.md 的市场 README 公约保持一致 -->
|
|
168
168
|
|
|
169
|
-
- **DSH 版本**:`>=0.1.2-alpha.2 <0.3.0-0`(本版 `0.5.
|
|
170
|
-
- **最后验证日期**:2026-10-
|
|
169
|
+
- **DSH 版本**:`>=0.1.2-alpha.2 <0.3.0-0`(本版 `0.5.1`;已发布的 `0.3.7`、`0.4.0` 仍声明旧的 `0.1.x` 范围 `>=0.1.2-alpha.2 <0.2.0-0` —— 见下文矩阵)
|
|
170
|
+
- **最后验证日期**:2026-10-19 —— 会话扫描修复(`session.v<N>.jsonl.zstd`,issue #8)+ 数据导出/导入与替换/冲突可视化(issue #3),npm test 334/334 通过
|
|
171
171
|
- **安装方式已验证**:`dsh plugin --profile web add dsh-engram`
|
|
172
172
|
|
|
173
173
|
> **一句话结论**:所有**已真正发布**的版本(`0.2.0` … `0.3.6`)**只支持旧版
|
|
@@ -179,7 +179,8 @@ curl -s -X POST http://127.0.0.1:3080/api/dsh-engram/import \
|
|
|
179
179
|
| --- | --- | --- | --- |
|
|
180
180
|
| `0.2.0` … `0.3.6`(npm 全量 + GitHub v0.3.3–v0.3.6) | 已发布 | **仅旧版** —— `0.1.0-rc.7` / `0.1.0-rc.8` / `0.1.1-rc.1` / `0.1.1-rc.2` | peers `@deepseek-ai/dsh-*@^0.1.0-rc.7` |
|
|
181
181
|
| `0.3.7` / `0.4.0`(npm) | 已发布 | **新版 DSH,settings `installSection`** —— `>=0.1.2-alpha.2 <0.2.0-0` | `engines.dsh` + peers 同范围 |
|
|
182
|
-
| `0.5.0
|
|
182
|
+
| `0.5.0`(npm) | 已发布 | **DSH `0.1.2-alpha.2` … `<0.3.0-0`** —— 设置卡迁移到 entry-id volatile Config(`0.1.7+`/`0.2.x`),`≤0.1.5` 自动回退 `installSection` | `engines.dsh` + peers `>=0.1.2-alpha.2 <0.3.0-0` |
|
|
183
|
+
| `0.5.1`(本期 / this release) | 已发布 | 同 `0.5.0` —— 会话扫描修复(`session.v<N>.jsonl.zstd`,issue #8)、数据导出/导入 GUI、替换/冲突可视化(issue #3) | 同 `0.5.0` |
|
|
183
184
|
|
|
184
185
|
### API 族断裂点:`0.1.2-alpha.2`
|
|
185
186
|
|
|
@@ -219,7 +220,13 @@ profile 条目 id(`engram`)为命名空间,不再需要任何“挂载”
|
|
|
219
220
|
所以:跑 ≤ `0.3.6` 的已发布版本时请把 DSH 保持在 `0.1.1-rc.2` 或更旧;升级
|
|
220
221
|
DSH 到 `0.1.2-alpha.2` 之前,先升级插件到 `>=0.3.7`。
|
|
221
222
|
|
|
222
|
-
### 当前发布版本 —— `0.5.
|
|
223
|
+
### 当前发布版本 —— `0.5.1`
|
|
224
|
+
|
|
225
|
+
在 `0.5.0` 之上的补丁版:会话扫描改为解析**版本化**日志名
|
|
226
|
+
(`session.v3/v4.jsonl.zstd`)而不硬编码 `session.jsonl.zstd`——在当前 DSH 上
|
|
227
|
+
这个错位让 `/stats`、`/toolstats`、BoundaryPrune 的偿还历史、CJK 会话搜索全部
|
|
228
|
+
**静默返回 0**(issue #8)。记忆查看器同时补上数据导出/导入与替换/冲突可视化
|
|
229
|
+
(issue #3)。
|
|
223
230
|
|
|
224
231
|
已发布的 `0.4.0` 声明 `>=0.1.2-alpha.2 <0.2.0-0`——这个范围被 semver 门槛在
|
|
225
232
|
**所有 DSH `0.2.0-0` 预发布**上拒绝(`<0.2.0-0` 排除 `0.2.0-rc.x`),且其宿主设置端
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
## dsh-engram v0.5.1
|
|
2
|
+
|
|
3
|
+
补丁版:修复会话日志扫描对**版本化文件名**的硬编码(issue #8,所有会话扫描
|
|
4
|
+
功能静默返回 0),并补齐记忆查看器的**数据导出/导入**与**替换/冲突可视化**
|
|
5
|
+
(issue #3 收尾)。
|
|
6
|
+
|
|
7
|
+
### 修复
|
|
8
|
+
|
|
9
|
+
- **会话扫描识别 `session.v<N>.jsonl.zstd`**(#8)—— `lib/usage.js`、
|
|
10
|
+
`lib/boundary-prune.js`、`lib/session-cjk.js` 曾硬编码 `session.jsonl.zstd`;
|
|
11
|
+
当前 DSH 把会话格式版本写进文件名(`0.1.x` → `session.v3.jsonl.zstd`,
|
|
12
|
+
`0.2.x` → `session.v4.jsonl.zstd`),`statSync` 抛 `ENOENT` 后被循环 `continue`
|
|
13
|
+
静默吞掉,导致:
|
|
14
|
+
|
|
15
|
+
- 设置页 → 遥测恒为「样本不足(0 次)」,`GET /stats`、`GET /toolstats` 全 0;
|
|
16
|
+
- BoundaryPrune 的 `workspaceHistory()`(L0 偿还输入)看不到任何会话;
|
|
17
|
+
- CJK 跨会话搜索恒返回 `[]`。
|
|
18
|
+
|
|
19
|
+
新增 `sessionLogFile(dir)`:正则匹配 `session[.v<N>].jsonl.zstd`,取目录内
|
|
20
|
+
**mtime 最新**的一个(迁移会话可能 v3/v4/legacy 并存,最新 = 宿主正在追加的
|
|
21
|
+
文件),无匹配时回退历史名、调用方保留原有 ENOENT 处理。四处调用点统一改用它。
|
|
22
|
+
本机实测(14 天窗口):`files 0 → 17`、`events 0 → 643`。
|
|
23
|
+
|
|
24
|
+
### 功能
|
|
25
|
+
|
|
26
|
+
- **数据备份面板**(#3)—— 设置页 → Engram 记忆 →「数据备份」:一键导出当前
|
|
27
|
+
工作区(或全部)为 `dsh-engram/export` v1 JSON 并下载;导入支持 **dry-run 预览**
|
|
28
|
+
→ `merge`(只补缺失,幂等)/ `replace`(需勾选确认,宿主要求
|
|
29
|
+
`confirm:"restore"`)。宿主端的 `GET /export`、`POST /import` 早已存在,本版只补
|
|
30
|
+
GUI 入口,GUI 与 API 走同一格式。
|
|
31
|
+
- **替换 / 冲突可视化**(#3)—— 记忆表给被替代的旧行挂「已替换」徽标、给新行挂
|
|
32
|
+
「替代 #id」跳转;记忆详情面板新增「替换 / 冲突」双向链(出边来自记录的
|
|
33
|
+
`supersedes`/`contradicts`,入边由已加载列表反查),元数据露出
|
|
34
|
+
`source`/`conditions`/`filePath`。
|
|
35
|
+
|
|
36
|
+
### 测试
|
|
37
|
+
|
|
38
|
+
- 334/334(新增 5 个 #8 用例:版本化/纯 legacy/v3+v4+legacy 混合/陈旧 legacy
|
|
39
|
+
不遮蔽 v4/无关文件名不被误选,以及 CJK 版本化读取与 `workspaceHistory` 计数)。
|
|
40
|
+
- `npm run typecheck`、`npm run prepublish-check` 通过;`lib/client.js` 重新构建。
|
|
41
|
+
|
|
42
|
+
## DSH 版本支持
|
|
43
|
+
|
|
44
|
+
- **支持的 DSH 范围**:>=0.1.2-alpha.2 <0.3.0-0
|
|
45
|
+
- **peer 声明**:
|
|
46
|
+
- `@deepseek-ai/dsh-compaction`: `>=0.1.2-alpha.2 <0.3.0-0`
|
|
47
|
+
- `@deepseek-ai/dsh-compaction-basic`: `>=0.1.2-alpha.2 <0.3.0-0`
|
|
48
|
+
- `@deepseek-ai/dsh-llm`: `>=0.1.2-alpha.2 <0.3.0-0`
|
|
49
|
+
- `@deepseek-ai/dsh-settings`: `>=0.1.2-alpha.2 <0.3.0-0`
|
|
50
|
+
- `@deepseek-ai/dsh-storage-domain`: `>=0.1.2-alpha.2 <0.3.0-0`
|
|
51
|
+
- **验证版本**:`dsh-v0.2.0-rc.2`(与 0.5.0 相同的宿主依赖集;本版只改会话日志
|
|
52
|
+
解析与纯客户端界面,未触碰设置/注入路径)
|
|
53
|
+
- **最后验证日期**:2026-10-19
|
|
54
|
+
- **安装方式已验证**:`dsh plugin --profile web add dsh-engram`
|
package/lib/boundary-prune.js
CHANGED
|
@@ -40,6 +40,7 @@ import fs from "node:fs";
|
|
|
40
40
|
import os from "node:os";
|
|
41
41
|
import path from "node:path";
|
|
42
42
|
|
|
43
|
+
import { sessionLogFile } from "./usage.js";
|
|
43
44
|
import { workspaceKey as wk } from "./util.js";
|
|
44
45
|
|
|
45
46
|
/** Fixed fallback for the repayment gate when no history exists yet. */
|
|
@@ -93,7 +94,7 @@ export function workspaceHistory(root = path.join(os.homedir(), ".dsh", "session
|
|
|
93
94
|
let sessions = 0;
|
|
94
95
|
const bdir = path.join(root, bucket);
|
|
95
96
|
for (const sid of fs.readdirSync(bdir)) {
|
|
96
|
-
const file = path.join(bdir, sid
|
|
97
|
+
const file = sessionLogFile(path.join(bdir, sid));
|
|
97
98
|
try {
|
|
98
99
|
const st = fs.statSync(file);
|
|
99
100
|
if (st.size > 0) sessions += 1;
|
package/lib/client.js
CHANGED
|
@@ -153,6 +153,28 @@ var EngramApi = class {
|
|
|
153
153
|
})
|
|
154
154
|
);
|
|
155
155
|
}
|
|
156
|
+
/**
|
|
157
|
+
* Data contract (issue #3) — dump the selected workspace, or every
|
|
158
|
+
* workspace when omitted, as a `dsh-engram/export` v1 payload. The same
|
|
159
|
+
* format `POST /import` restores, so the dump is a portable backup.
|
|
160
|
+
*/
|
|
161
|
+
async exportData(workspace) {
|
|
162
|
+
return readJson(await fetch(`${API_PREFIX}/export${query({ workspace })}`));
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Data contract (issue #3) — restore a dump. `merge` adds missing rows
|
|
166
|
+
* (idempotent), `replace` wipes the target workspace first (the host then
|
|
167
|
+
* requires `confirm: "restore"`). `dryRun` plans without writing.
|
|
168
|
+
*/
|
|
169
|
+
async importData(body) {
|
|
170
|
+
return readJson(
|
|
171
|
+
await fetch(`${API_PREFIX}/import`, {
|
|
172
|
+
method: "POST",
|
|
173
|
+
headers: { "content-type": "application/json" },
|
|
174
|
+
body: JSON.stringify(body)
|
|
175
|
+
})
|
|
176
|
+
);
|
|
177
|
+
}
|
|
156
178
|
/** Settings card transport — read the `dsh-engram` namespace through our
|
|
157
179
|
* own HTTP API instead of the host connection/remote RPC. */
|
|
158
180
|
async getSettings() {
|
|
@@ -2254,7 +2276,7 @@ function gapsOf(t) {
|
|
|
2254
2276
|
if (!t.memoryRefs || t.memoryRefs.length === 0) gaps.push("memory_ref");
|
|
2255
2277
|
return gaps;
|
|
2256
2278
|
}
|
|
2257
|
-
function EngramDetail({ target, api, tasks, nodes, links, onClose, onNavigateMemory, onChanged }) {
|
|
2279
|
+
function EngramDetail({ target, api, tasks, nodes, links, memories = [], onClose, onNavigateMemory, onChanged }) {
|
|
2258
2280
|
const { dark, vars } = useEngramTheme();
|
|
2259
2281
|
const [gArt, setGArt] = (0, import_react6.useState)("");
|
|
2260
2282
|
const [gEval, setGEval] = (0, import_react6.useState)("");
|
|
@@ -2274,6 +2296,15 @@ function EngramDetail({ target, api, tasks, nodes, links, onClose, onNavigateMem
|
|
|
2274
2296
|
if (target.kind !== "node" || !node) return [];
|
|
2275
2297
|
return links.filter((l) => l.source === node.id || l.target === node.id).map((l) => ({ l, outgoing: l.source === node.id }));
|
|
2276
2298
|
}, [links, node, target.kind]);
|
|
2299
|
+
const memoryLinks = (0, import_react6.useMemo)(() => {
|
|
2300
|
+
if (target.kind !== "memory") return null;
|
|
2301
|
+
const m = target.memory;
|
|
2302
|
+
return {
|
|
2303
|
+
m,
|
|
2304
|
+
replacedBy: memories.filter((x) => x.status === "active" && x.supersedes === m.id),
|
|
2305
|
+
contradictedBy: memories.filter((x) => x.status === "active" && x.contradicts === m.id)
|
|
2306
|
+
};
|
|
2307
|
+
}, [target, memories]);
|
|
2277
2308
|
const hb2 = {
|
|
2278
2309
|
root: {
|
|
2279
2310
|
...vars,
|
|
@@ -2304,6 +2335,8 @@ function EngramDetail({ target, api, tasks, nodes, links, onClose, onNavigateMem
|
|
|
2304
2335
|
badge: { fontSize: 10.5, fontWeight: 700, padding: "2px 8px", borderRadius: 999, color: "#fff" },
|
|
2305
2336
|
tag: { fontSize: 11, padding: "1px 8px", borderRadius: 999, background: "var(--dsh-color-hover-bg, #f3f4f6)", color: "var(--dsh-color-muted, #6b7280)", border: "1px solid var(--dsh-color-border, #e5e7eb)" },
|
|
2306
2337
|
chip: { fontSize: 11, padding: "2px 9px", borderRadius: 999, background: "rgba(37,99,235,.1)", color: "#2563eb", border: "1px solid rgba(37,99,235,.25)", cursor: "pointer" },
|
|
2338
|
+
chipSup: { fontSize: 11, padding: "2px 9px", borderRadius: 999, background: "rgba(124,58,237,.1)", color: "#7c3aed", border: "1px solid rgba(124,58,237,.25)", cursor: "pointer" },
|
|
2339
|
+
chipDanger: { fontSize: 11, padding: "2px 9px", borderRadius: 999, background: "rgba(220,38,38,.1)", color: "#dc2626", border: "1px solid rgba(220,38,38,.25)", cursor: "pointer" },
|
|
2307
2340
|
input: { fontSize: 12, padding: "5px 8px", border: "1px solid var(--dsh-color-border, #d1d5db)", borderRadius: 8, background: "var(--dsh-color-surface, #ffffff)", color: "var(--dsh-color-muted-strong, #374151)", width: "100%", boxSizing: "border-box" },
|
|
2308
2341
|
btn: { fontSize: 11.5, fontWeight: 600, padding: "5px 10px", borderRadius: 8, cursor: "pointer", border: "1px solid var(--dsh-color-border, #d1d5db)", background: "transparent", color: "var(--dsh-color-muted-strong, #374151)" },
|
|
2309
2342
|
btnPrimary: { fontSize: 11.5, fontWeight: 700, padding: "5px 12px", borderRadius: 8, cursor: "pointer", border: "none", background: "#2563eb", color: "#fff" },
|
|
@@ -2442,6 +2475,29 @@ function EngramDetail({ target, api, tasks, nodes, links, onClose, onNavigateMem
|
|
|
2442
2475
|
t
|
|
2443
2476
|
] }, t)) })
|
|
2444
2477
|
] }),
|
|
2478
|
+
memoryLinks && (memoryLinks.m.supersedes || memoryLinks.m.contradicts || memoryLinks.replacedBy.length > 0 || memoryLinks.contradictedBy.length > 0) && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: hb2.section, children: [
|
|
2479
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: hb2.k, children: "\u66FF\u6362 / \u51B2\u7A81" }),
|
|
2480
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: { display: "flex", gap: 5, flexWrap: "wrap", marginTop: 3 }, children: [
|
|
2481
|
+
memoryLinks.m.supersedes && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { style: hb2.chipSup, title: `\u672C\u6761\u66FF\u4EE3 ${memoryLinks.m.supersedes}\uFF08\u65E7\u884C\u4FDD\u7559\u3001\u53EC\u56DE\u964D\u7EA7\uFF09`, onClick: () => onNavigateMemory(memoryLinks.m.supersedes), children: [
|
|
2482
|
+
"\u66FF\u4EE3 #",
|
|
2483
|
+
memoryLinks.m.supersedes.slice(0, 8)
|
|
2484
|
+
] }),
|
|
2485
|
+
memoryLinks.m.contradicts && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { style: hb2.chipDanger, title: `\u672C\u6761\u4E0E ${memoryLinks.m.contradicts} \u51B2\u7A81\uFF08\u4E24\u6761\u90FD\u4FDD\u7559\uFF09`, onClick: () => onNavigateMemory(memoryLinks.m.contradicts), children: [
|
|
2486
|
+
"\u51B2\u7A81 #",
|
|
2487
|
+
memoryLinks.m.contradicts.slice(0, 8)
|
|
2488
|
+
] }),
|
|
2489
|
+
memoryLinks.replacedBy.map((x) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { style: hb2.chipSup, title: `\u5DF2\u88AB ${x.text.slice(0, 40)}\u2026 \u66FF\u4EE3`, onClick: () => onNavigateMemory(x.id), children: [
|
|
2490
|
+
"\u2190 \u88AB #",
|
|
2491
|
+
x.id.slice(0, 8),
|
|
2492
|
+
" \u66FF\u4EE3"
|
|
2493
|
+
] }, x.id)),
|
|
2494
|
+
memoryLinks.contradictedBy.map((x) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { style: hb2.chipDanger, title: `${x.text.slice(0, 40)}\u2026 \u4E0E\u672C\u6761\u51B2\u7A81`, onClick: () => onNavigateMemory(x.id), children: [
|
|
2495
|
+
"\u21C4 #",
|
|
2496
|
+
x.id.slice(0, 8),
|
|
2497
|
+
" \u51B2\u7A81"
|
|
2498
|
+
] }, x.id))
|
|
2499
|
+
] })
|
|
2500
|
+
] }),
|
|
2445
2501
|
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: hb2.section, children: [
|
|
2446
2502
|
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: hb2.k, children: "\u5143\u6570\u636E" }),
|
|
2447
2503
|
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: 2, marginTop: 3, fontSize: 11.5, fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", color: "var(--dsh-color-muted, #6b7280)" }, children: [
|
|
@@ -2469,6 +2525,18 @@ function EngramDetail({ target, api, tasks, nodes, links, onClose, onNavigateMem
|
|
|
2469
2525
|
" \xB7 updated ",
|
|
2470
2526
|
fmtDate(target.memory.updatedAt)
|
|
2471
2527
|
] }),
|
|
2528
|
+
target.memory.source && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { children: [
|
|
2529
|
+
"source ",
|
|
2530
|
+
target.memory.source
|
|
2531
|
+
] }),
|
|
2532
|
+
target.memory.conditions && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { children: [
|
|
2533
|
+
"conditions ",
|
|
2534
|
+
target.memory.conditions
|
|
2535
|
+
] }),
|
|
2536
|
+
target.memory.filePath && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { children: [
|
|
2537
|
+
"file ",
|
|
2538
|
+
target.memory.filePath
|
|
2539
|
+
] }),
|
|
2472
2540
|
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { children: [
|
|
2473
2541
|
"TTL ",
|
|
2474
2542
|
daysLeft(target.memory.expiresAt)
|
|
@@ -2791,6 +2859,14 @@ function EngramSection({ api, t }) {
|
|
|
2791
2859
|
const [closeEval, setCloseEval] = (0, import_react7.useState)("");
|
|
2792
2860
|
const [closeRefs, setCloseRefs] = (0, import_react7.useState)("");
|
|
2793
2861
|
const [closeBusy, setCloseBusy] = (0, import_react7.useState)(false);
|
|
2862
|
+
const [importDump, setImportDump] = (0, import_react7.useState)(null);
|
|
2863
|
+
const [importName, setImportName] = (0, import_react7.useState)("");
|
|
2864
|
+
const [importMode, setImportMode] = (0, import_react7.useState)("merge");
|
|
2865
|
+
const [importDryRun, setImportDryRun] = (0, import_react7.useState)(true);
|
|
2866
|
+
const [importReplaceOk, setImportReplaceOk] = (0, import_react7.useState)(false);
|
|
2867
|
+
const [importBusy, setImportBusy] = (0, import_react7.useState)(false);
|
|
2868
|
+
const [importReport, setImportReport] = (0, import_react7.useState)(null);
|
|
2869
|
+
const [dataError, setDataError] = (0, import_react7.useState)(null);
|
|
2794
2870
|
const refresh = (0, import_react7.useCallback)(async () => {
|
|
2795
2871
|
setBusy(true);
|
|
2796
2872
|
setError(null);
|
|
@@ -2847,6 +2923,24 @@ function EngramSection({ api, t }) {
|
|
|
2847
2923
|
for (const t2 of tasks) map.set(t2.id, t2.name);
|
|
2848
2924
|
return (id) => map.get(id) ?? id;
|
|
2849
2925
|
}, [nodes, tasks]);
|
|
2926
|
+
const supersedeIndex = (0, import_react7.useMemo)(() => {
|
|
2927
|
+
const replacedBy = /* @__PURE__ */ new Map();
|
|
2928
|
+
const contradictedBy = /* @__PURE__ */ new Map();
|
|
2929
|
+
for (const m of memories) {
|
|
2930
|
+
if (m.status !== "active") continue;
|
|
2931
|
+
if (m.supersedes) {
|
|
2932
|
+
const list = replacedBy.get(m.supersedes) ?? [];
|
|
2933
|
+
list.push(m);
|
|
2934
|
+
replacedBy.set(m.supersedes, list);
|
|
2935
|
+
}
|
|
2936
|
+
if (m.contradicts) {
|
|
2937
|
+
const list = contradictedBy.get(m.contradicts) ?? [];
|
|
2938
|
+
list.push(m);
|
|
2939
|
+
contradictedBy.set(m.contradicts, list);
|
|
2940
|
+
}
|
|
2941
|
+
}
|
|
2942
|
+
return { replacedBy, contradictedBy };
|
|
2943
|
+
}, [memories]);
|
|
2850
2944
|
(0, import_react7.useEffect)(() => {
|
|
2851
2945
|
if (!newTaskWs && workspaces.length > 0 && !workspace) setNewTaskWs(workspaces[0]);
|
|
2852
2946
|
}, [newTaskWs, workspaces, workspace]);
|
|
@@ -2880,6 +2974,60 @@ function EngramSection({ api, t }) {
|
|
|
2880
2974
|
setGcRunning(false);
|
|
2881
2975
|
}
|
|
2882
2976
|
};
|
|
2977
|
+
const downloadExport = async () => {
|
|
2978
|
+
setDataError(null);
|
|
2979
|
+
try {
|
|
2980
|
+
const dump = await api.exportData(workspace || void 0);
|
|
2981
|
+
const blob = new Blob([JSON.stringify(dump, null, 2)], { type: "application/json" });
|
|
2982
|
+
const url = URL.createObjectURL(blob);
|
|
2983
|
+
const a = document.createElement("a");
|
|
2984
|
+
const safe = (workspace || "all").replace(/[^a-zA-Z0-9._-]+/g, "_");
|
|
2985
|
+
a.href = url;
|
|
2986
|
+
a.download = `dsh-engram-export-${safe}-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.json`;
|
|
2987
|
+
a.click();
|
|
2988
|
+
URL.revokeObjectURL(url);
|
|
2989
|
+
} catch (e) {
|
|
2990
|
+
setDataError(e instanceof Error ? e.message : String(e));
|
|
2991
|
+
}
|
|
2992
|
+
};
|
|
2993
|
+
const pickImportFile = async (file) => {
|
|
2994
|
+
setDataError(null);
|
|
2995
|
+
setImportReport(null);
|
|
2996
|
+
setImportDump(null);
|
|
2997
|
+
setImportName("");
|
|
2998
|
+
if (!file) return;
|
|
2999
|
+
try {
|
|
3000
|
+
const text = await file.text();
|
|
3001
|
+
setImportDump(JSON.parse(text));
|
|
3002
|
+
setImportName(file.name);
|
|
3003
|
+
} catch (e) {
|
|
3004
|
+
setDataError(`\u65E0\u6CD5\u89E3\u6790 ${file.name}\uFF1A${e instanceof Error ? e.message : String(e)}`);
|
|
3005
|
+
}
|
|
3006
|
+
};
|
|
3007
|
+
const runImport = async () => {
|
|
3008
|
+
if (importDump === null) return;
|
|
3009
|
+
if (importMode === "replace" && !importReplaceOk) {
|
|
3010
|
+
setDataError("\u66FF\u6362\u6A21\u5F0F\u4F1A\u5148\u6E05\u7A7A\u76EE\u6807\u5DE5\u4F5C\u533A \u2014 \u8BF7\u5148\u52FE\u9009\u786E\u8BA4");
|
|
3011
|
+
return;
|
|
3012
|
+
}
|
|
3013
|
+
setImportBusy(true);
|
|
3014
|
+
setDataError(null);
|
|
3015
|
+
try {
|
|
3016
|
+
const report = await api.importData({
|
|
3017
|
+
payload: importDump,
|
|
3018
|
+
mode: importMode,
|
|
3019
|
+
dryRun: importDryRun,
|
|
3020
|
+
workspace: workspace || void 0,
|
|
3021
|
+
confirm: importMode === "replace" ? "restore" : void 0
|
|
3022
|
+
});
|
|
3023
|
+
setImportReport(report);
|
|
3024
|
+
if (!report.dryRun) await refresh();
|
|
3025
|
+
} catch (e) {
|
|
3026
|
+
setDataError(e instanceof Error ? e.message : String(e));
|
|
3027
|
+
} finally {
|
|
3028
|
+
setImportBusy(false);
|
|
3029
|
+
}
|
|
3030
|
+
};
|
|
2883
3031
|
const groupedRows = workspace === "" ? groupByWorkspace(memories) : [[workspace, memories]];
|
|
2884
3032
|
const taskGroups = workspace === "" ? groupByWorkspace(tasks) : [[workspace, tasks]];
|
|
2885
3033
|
const linkGroups = workspace === "" ? groupByWorkspace(links) : [[workspace, links]];
|
|
@@ -3069,6 +3217,90 @@ function EngramSection({ api, t }) {
|
|
|
3069
3217
|
] }, l.id))
|
|
3070
3218
|
] })
|
|
3071
3219
|
] }),
|
|
3220
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: s2.subPanel, children: [
|
|
3221
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap" }, children: [
|
|
3222
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { style: { fontWeight: 700, fontSize: 13 }, children: "\u6570\u636E\u5907\u4EFD\uFF08export / import\uFF09" }),
|
|
3223
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { style: s2.btn, title: "\u4E0B\u8F7D\u5F53\u524D\u5DE5\u4F5C\u533A\uFF08\u6216\u5168\u90E8\uFF09\u7684 dsh-engram/export v1 \u8F6C\u50A8", onClick: () => void downloadExport(), children: "\u5BFC\u51FA JSON" }),
|
|
3224
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("label", { style: { ...s2.btn, display: "inline-flex", alignItems: "center", gap: 6, cursor: "pointer" }, children: [
|
|
3225
|
+
"\u9009\u62E9\u5BFC\u5165\u6587\u4EF6\u2026",
|
|
3226
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
3227
|
+
"input",
|
|
3228
|
+
{
|
|
3229
|
+
type: "file",
|
|
3230
|
+
accept: "application/json,.json",
|
|
3231
|
+
style: { display: "none" },
|
|
3232
|
+
onChange: (e) => {
|
|
3233
|
+
void pickImportFile(e.target.files?.[0] ?? null);
|
|
3234
|
+
e.currentTarget.value = "";
|
|
3235
|
+
}
|
|
3236
|
+
}
|
|
3237
|
+
)
|
|
3238
|
+
] }),
|
|
3239
|
+
importName && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { style: s2.mono, children: importName })
|
|
3240
|
+
] }),
|
|
3241
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: { fontSize: 11.5, color: "var(--dsh-color-muted, #6b7280)", marginTop: 4 }, children: [
|
|
3242
|
+
"\u5BFC\u51FA\u5373\u53EF\u79FB\u690D\u5907\u4EFD\uFF08\u8BB0\u5FC6/\u4EFB\u52A1/\u5173\u7CFB/\u8282\u70B9\uFF0C\u542B\u5F52\u6863\uFF09\uFF1B",
|
|
3243
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("b", { children: "merge" }),
|
|
3244
|
+
" \u53EA\u8865\u7F3A\u5931\uFF08\u5E42\u7B49\uFF09\uFF0C",
|
|
3245
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("b", { children: "replace" }),
|
|
3246
|
+
" \u5148\u6E05\u7A7A\u76EE\u6807\u5DE5\u4F5C\u533A\u518D\u6062\u590D\u3002\u8BB0\u5FC6\u6B63\u6587\u4E0E\u6240\u6709\u5199\u5165\u8DEF\u5F84\u4E00\u6837\u8FC7\u8131\u654F\u3002"
|
|
3247
|
+
] }),
|
|
3248
|
+
importDump !== null && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: { marginTop: 8, display: "flex", flexDirection: "column", gap: 6 }, children: [
|
|
3249
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: { display: "flex", gap: 10, alignItems: "center", flexWrap: "wrap", fontSize: 12.5 }, children: [
|
|
3250
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("label", { style: { display: "inline-flex", gap: 4, alignItems: "center" }, children: [
|
|
3251
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("input", { type: "checkbox", checked: importDryRun, onChange: (e) => setImportDryRun(e.target.checked) }),
|
|
3252
|
+
"\u4EC5\u9884\u89C8\uFF08dry run\uFF09"
|
|
3253
|
+
] }),
|
|
3254
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
|
|
3255
|
+
"select",
|
|
3256
|
+
{
|
|
3257
|
+
style: s2.input,
|
|
3258
|
+
value: importMode,
|
|
3259
|
+
onChange: (e) => {
|
|
3260
|
+
const mode = e.target.value;
|
|
3261
|
+
setImportMode(mode);
|
|
3262
|
+
if (mode !== "replace") setImportReplaceOk(false);
|
|
3263
|
+
},
|
|
3264
|
+
children: [
|
|
3265
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("option", { value: "merge", children: "\u5408\u5E76\uFF08merge\uFF0C\u5E42\u7B49\uFF09" }),
|
|
3266
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("option", { value: "replace", children: "\u66FF\u6362\uFF08replace\uFF0C\u5148\u6E05\u7A7A\u5DE5\u4F5C\u533A\uFF09" })
|
|
3267
|
+
]
|
|
3268
|
+
}
|
|
3269
|
+
),
|
|
3270
|
+
importMode === "replace" && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("label", { style: { display: "inline-flex", gap: 4, alignItems: "center", color: "#b45309" }, children: [
|
|
3271
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("input", { type: "checkbox", checked: importReplaceOk, onChange: (e) => setImportReplaceOk(e.target.checked) }),
|
|
3272
|
+
"\u6211\u786E\u8BA4\u66FF\u6362",
|
|
3273
|
+
workspace ? `\uFF08${workspace}\uFF09` : "\uFF08\u9700\u5148\u9009\u5DE5\u4F5C\u533A\uFF09"
|
|
3274
|
+
] }),
|
|
3275
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
3276
|
+
"button",
|
|
3277
|
+
{
|
|
3278
|
+
style: s2.btnPrimary,
|
|
3279
|
+
disabled: importBusy || importMode === "replace" && (!importReplaceOk || !workspace),
|
|
3280
|
+
onClick: () => void runImport(),
|
|
3281
|
+
children: importBusy ? "\u2026" : importDryRun ? "\u9884\u89C8\u5BFC\u5165" : "\u6267\u884C\u5BFC\u5165"
|
|
3282
|
+
}
|
|
3283
|
+
)
|
|
3284
|
+
] }),
|
|
3285
|
+
importReport && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: { fontSize: 12.5 }, children: [
|
|
3286
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { children: [
|
|
3287
|
+
importReport.dryRun ? "dry-run \u9884\u89C8\uFF1A" : "\u5DF2\u5BFC\u5165\uFF1A",
|
|
3288
|
+
" ",
|
|
3289
|
+
Object.entries(importReport.written).map(([k, v]) => `${k} ${v}`).join(" \xB7 ") || "\u2014",
|
|
3290
|
+
importReport.skipped.length > 0 ? ` \xB7 \u8DF3\u8FC7 ${importReport.skipped.length}` : ""
|
|
3291
|
+
] }),
|
|
3292
|
+
importReport.skipped.slice(0, 5).map((x, i) => /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: s2.mono, children: [
|
|
3293
|
+
"- ",
|
|
3294
|
+
x.table,
|
|
3295
|
+
" ",
|
|
3296
|
+
x.id ? x.id.slice(0, 8) : "?",
|
|
3297
|
+
": ",
|
|
3298
|
+
x.reason
|
|
3299
|
+
] }, `${x.table}-${x.id ?? i}`))
|
|
3300
|
+
] }),
|
|
3301
|
+
dataError && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: { fontSize: 12, color: "#dc2626", fontWeight: 600 }, children: dataError })
|
|
3302
|
+
] })
|
|
3303
|
+
] }),
|
|
3072
3304
|
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: s2.row, children: [
|
|
3073
3305
|
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("select", { style: s2.input, value: kind, onChange: (e) => setKind(e.target.value), children: [
|
|
3074
3306
|
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("option", { value: "", children: "\u5168\u90E8\u7C7B\u578B" }),
|
|
@@ -3142,7 +3374,8 @@ function EngramSection({ api, t }) {
|
|
|
3142
3374
|
] }) }, `h-${r.ws}`) : /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("tr", { children: [
|
|
3143
3375
|
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("td", { style: { ...s2.td, whiteSpace: "nowrap" }, children: [
|
|
3144
3376
|
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { style: { ...s2.badge, background: KIND_COLORS2[r.m.kind] ?? "#6b7280" }, children: KIND_LABEL2[r.m.kind] ?? r.m.kind }),
|
|
3145
|
-
r.m.status === "archived" && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { style: { ...s2.tag, color: "#b45309", background: "#fef3c7" }, children: "archived" })
|
|
3377
|
+
r.m.status === "archived" && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { style: { ...s2.tag, color: "#b45309", background: "#fef3c7" }, children: "archived" }),
|
|
3378
|
+
supersedeIndex.replacedBy.has(r.m.id) && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { style: { ...s2.tag, color: "#b45309", background: "#fef3c7" }, title: `\u5DF2\u88AB ${(supersedeIndex.replacedBy.get(r.m.id) ?? []).map((x) => `#${x.id.slice(0, 8)}`).join(", ")} \u66FF\u4EE3\uFF08\u65E7\u884C\u4FDD\u7559\u3001\u53EC\u56DE\u964D\u7EA7\uFF09`, children: "\u5DF2\u66FF\u6362" })
|
|
3146
3379
|
] }),
|
|
3147
3380
|
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("td", { style: { ...s2.td, minWidth: 0 }, children: [
|
|
3148
3381
|
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { title: r.m.text, style: expandedRows.has(r.m.id) ? s2.expanded : s2.clamp3, children: r.m.text }),
|
|
@@ -3159,7 +3392,11 @@ function EngramSection({ api, t }) {
|
|
|
3159
3392
|
" \xB7 ",
|
|
3160
3393
|
daysLeft2(r.m.expiresAt)
|
|
3161
3394
|
] }),
|
|
3162
|
-
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { style: s2.linkBtn, onClick: () => toggleExpand(r.m.id), children: expandedRows.has(r.m.id) ? "\u6536\u8D77" : "\u5C55\u5F00\u5168\u6587" })
|
|
3395
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { style: s2.linkBtn, onClick: () => toggleExpand(r.m.id), children: expandedRows.has(r.m.id) ? "\u6536\u8D77" : "\u5C55\u5F00\u5168\u6587" }),
|
|
3396
|
+
r.m.supersedes && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("button", { style: s2.linkBtn, title: `\u6253\u5F00\u88AB\u66FF\u4EE3\u7684\u65E7\u8BB0\u5FC6 ${r.m.supersedes}`, onClick: () => openMemory(r.m.supersedes), children: [
|
|
3397
|
+
"\u66FF\u4EE3 #",
|
|
3398
|
+
r.m.supersedes.slice(0, 8)
|
|
3399
|
+
] })
|
|
3163
3400
|
] }),
|
|
3164
3401
|
r.m.tags.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: { marginTop: 3, display: "flex", flexWrap: "wrap", gap: 4 }, children: r.m.tags.map((tag) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { style: s2.tag, children: tag }, tag)) })
|
|
3165
3402
|
] }),
|
|
@@ -3359,6 +3596,7 @@ ${r.m.text.slice(0, 60)}`)) void act(() => api.remove(r.m.id, r.m.workspace));
|
|
|
3359
3596
|
tasks,
|
|
3360
3597
|
nodes,
|
|
3361
3598
|
links,
|
|
3599
|
+
memories,
|
|
3362
3600
|
onClose: () => setDetail(null),
|
|
3363
3601
|
onNavigateMemory: openMemory,
|
|
3364
3602
|
onChanged: () => void refresh()
|
package/lib/session-cjk.js
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
import fs from "node:fs";
|
|
23
23
|
import path from "node:path";
|
|
24
24
|
|
|
25
|
-
import { decodeZstd, sessionsRoot } from "./usage.js";
|
|
25
|
+
import { decodeZstd, sessionLogFile, sessionsRoot } from "./usage.js";
|
|
26
26
|
import { redactText } from "./redact.js";
|
|
27
27
|
|
|
28
28
|
/** CJK ideographs (incl. Ext-A and compatibility ideographs). */
|
|
@@ -135,7 +135,7 @@ export function searchSessionsCJK({
|
|
|
135
135
|
}
|
|
136
136
|
const files = [];
|
|
137
137
|
for (const sid of sids) {
|
|
138
|
-
const file = path.join(bucketDir, sid
|
|
138
|
+
const file = sessionLogFile(path.join(bucketDir, sid));
|
|
139
139
|
let st;
|
|
140
140
|
try {
|
|
141
141
|
st = fs.statSync(file);
|
package/lib/usage.js
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* on every engram/esr tool call, and a separate session-log scan that counted
|
|
8
8
|
* tool calls (to include the built-in `todo` tool). Both are now ONE module:
|
|
9
9
|
* both /stats and /toolstats answer from the canonical log stream
|
|
10
|
-
* `~/.dsh/sessions/<bucket>/<sid>/session.jsonl.zstd`, decompressed on
|
|
11
|
-
* with a short TTL cache. No tool call pays a write anymore (pure GUI-side
|
|
10
|
+
* `~/.dsh/sessions/<bucket>/<sid>/session[.v<N>].jsonl.zstd`, decompressed on
|
|
11
|
+
* demand with a short TTL cache. No tool call pays a write anymore (pure GUI-side
|
|
12
12
|
* observability), and the log stream is the single source of truth.
|
|
13
13
|
*
|
|
14
14
|
* Zero external dependency: Node >=22.19 provides the zstd codec in
|
|
@@ -44,6 +44,49 @@ export function sessionsRoot() {
|
|
|
44
44
|
return path.join(os.homedir(), ".dsh", "sessions");
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Session log file names: current DSH stamps the session format version into
|
|
49
|
+
* the name (`session.v3.jsonl.zstd` on 0.1.x, `session.v4.jsonl.zstd` on 0.2);
|
|
50
|
+
* older releases used the unversioned `session.jsonl.zstd` (issue #8).
|
|
51
|
+
*/
|
|
52
|
+
const SESSION_LOG_RE = /^session(?:\.v\d+)?\.jsonl\.zstd$/;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Resolve the log file inside one session directory.
|
|
56
|
+
*
|
|
57
|
+
* A directory normally holds exactly one log, but a session migrated across
|
|
58
|
+
* format versions can hold both a v3 and a v4 file (and a legacy unversioned
|
|
59
|
+
* one). The newest-mtime match wins — that is the file the host is currently
|
|
60
|
+
* appending to. When nothing matches, the historical unversioned name is
|
|
61
|
+
* returned so callers keep their existing ENOENT handling (`statSync` throws →
|
|
62
|
+
* session skipped) instead of silently scanning the wrong corpus.
|
|
63
|
+
*/
|
|
64
|
+
export function sessionLogFile(dir) {
|
|
65
|
+
let names;
|
|
66
|
+
try {
|
|
67
|
+
names = fs.readdirSync(dir);
|
|
68
|
+
} catch {
|
|
69
|
+
return path.join(dir, "session.jsonl.zstd");
|
|
70
|
+
}
|
|
71
|
+
let best = null;
|
|
72
|
+
let bestMtime = -1;
|
|
73
|
+
for (const name of names) {
|
|
74
|
+
if (!SESSION_LOG_RE.test(name)) continue;
|
|
75
|
+
let st;
|
|
76
|
+
try {
|
|
77
|
+
st = fs.statSync(path.join(dir, name));
|
|
78
|
+
} catch {
|
|
79
|
+
continue; // unreadable entry: ignore
|
|
80
|
+
}
|
|
81
|
+
if (!st.isFile()) continue;
|
|
82
|
+
if (st.mtimeMs > bestMtime) {
|
|
83
|
+
bestMtime = st.mtimeMs;
|
|
84
|
+
best = name;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return path.join(dir, best ?? "session.jsonl.zstd");
|
|
88
|
+
}
|
|
89
|
+
|
|
47
90
|
/** Byte range of one structurally complete zstd frame in a concatenated stream. */
|
|
48
91
|
function frameRanges(buffer) {
|
|
49
92
|
const frames = [];
|
|
@@ -244,7 +287,7 @@ export function collectToolCounts({ root = sessionsRoot(), days = 14, now = Date
|
|
|
244
287
|
for (const bucket of buckets) {
|
|
245
288
|
const bdir = path.join(root, bucket);
|
|
246
289
|
for (const sid of listDirOrEmpty(bdir)) {
|
|
247
|
-
const file = path.join(bdir, sid
|
|
290
|
+
const file = sessionLogFile(path.join(bdir, sid));
|
|
248
291
|
let st;
|
|
249
292
|
try {
|
|
250
293
|
st = fs.statSync(file);
|
|
@@ -330,7 +373,7 @@ export function collectUsageStats({ root = sessionsRoot(), days = 14, now = Date
|
|
|
330
373
|
const buckets = listDirOrEmpty(root).filter((b) => b.startsWith("--"));
|
|
331
374
|
for (const bucket of buckets) {
|
|
332
375
|
for (const sid of listDirOrEmpty(path.join(root, bucket))) {
|
|
333
|
-
const file = path.join(root, bucket, sid
|
|
376
|
+
const file = sessionLogFile(path.join(root, bucket, sid));
|
|
334
377
|
let st;
|
|
335
378
|
try {
|
|
336
379
|
st = fs.statSync(file);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-engram",
|
|
3
3
|
"description": "Minimalist long-term memory for DeepSeek Harness, distilling symbolic-index + pi-esr ideas: zero-LLM auto-capture, a symbolic [ENGRAM] index with progressive disclosure, and an ESR-lite evidence-closure protocol (esr_task / esr_node / esr_close / esr_link). Real agent-behaviour telemetry (usage rollup + /stats) and a deterministic offline recall benchmark (npm run eval). Hot path is model-free; storage on ctx.storageDomain; web memory viewer, observability panel and config card through DSH's native settings slots.",
|
|
4
|
-
"version": "0.5.
|
|
4
|
+
"version": "0.5.1",
|
|
5
5
|
"packageManager": "pnpm@11.22.0",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|