dsh-recall-plugin 2.3.12 → 2.3.14
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 +16 -0
- package/lib/index.js +30 -37
- package/lib/snapshots.js +32 -7
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,8 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
本文件格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),版本号遵循语义化版本。
|
|
4
4
|
|
|
5
|
+
## [2.3.14] - 2026-09-11
|
|
6
|
+
|
|
7
|
+
### 修复
|
|
8
|
+
|
|
9
|
+
- **撤回 fork 出的子会话内,撤回预览对任何消息都误报「该消息是本会话中第一条用户消息」**:`resolveCutSeq` 冷读取走 `sessionQuery.readSession`,官方实现内部却用 `Session.create(events, header, inheritedEventCount)` 做回放校验——快照模式要求 seeded 头的 `inheritedEventCount` 恰等于 `log.length`(「seeded session constructor seed must equal its inherited prefix」),而读取面交给它的是全量逻辑日志(`snapshotEvents()` 无参=继承前缀+自身事件;冷读返回完整存储日志),两者必然不等,`readSession` 对任何 seeded 会话(本插件每次撤回 fork 出的子会话即此类)直接抛错;此前的 `catch` 把异常静默折成 null,与「真首条」不可分——于是子会话里点**任何**消息,面板都显示「该消息是本会话中第一条用户消息,无法回退对话;确认后仅回退项目文件」。修复:`resolveCutSeq` 增加降级链——`readSession` 抛错、或消息根本没出现在它给的事件里(读取面缺继承前缀的版本差异)时,改用 `sessionQuery.observeSession`(经 `Session.fromRestore` 恢复,restore 模式无该约束)读取全量逻辑日志;租约以 `Symbol.dispose` 显式释放(prepared 缓存项靠它减引用,漏释放会卡淘汰);只有「消息在、其前无 turn/end」才是可信的“首条”,两跳都失败才落到 null。`scanCutSeq` 拆出 `scanCutSeqDetail`(cut + found)供降级判断;契约补 `observeSession` 与 `SessionObservationLease`(可选——旧版 dsh 无此 API 时维持原行为)。**真机复验**(dsh web 0.1.5-rc.2 + POST `/api/recall/preview`):修前子会话 6 条消息 cutSeq 全 null、父会话正常;修后子会话 156/43/62/88/197、真首条(你好)仍 null、父会话不变。新增 `tests/unit/snapshots-cutseq.test.js`(6 例),compat-audit 新增 I33。插件功能、客户端与其余行为零变化。
|
|
10
|
+
|
|
11
|
+
## [2.3.13] - 2026-09-11
|
|
12
|
+
|
|
13
|
+
### 修复
|
|
14
|
+
|
|
15
|
+
- **DSH Desktop 上插件树加载失败(fiber 永久 pending)**:桌面端 composition(`desktop.cordis.patch.yml`)把 `webserver` row 置 `disabled: true`(Electron 用 file:// + IPC 替代 HTTP),插件 host 侧顶层 `inject` 含 `webServer`,fiber 因此停在 `pending (waiting for service: webServer)` → 「1 entry did not activate」整树加载失败。修复:① 顶层 inject 收敛为 `['shell', 'sessions', 'agents']`;② 删除 `ctx.webServer` 前缀路由注册,改走官方「载体无关」路由注册表 `ctx.connection.fetch.register`——12 个端点各注册一条 `POST /api/recall/<name>` 的 exact 路由(`requestBody: 'buffered'`、插件自留 1MB 请求体上限、`{ ok:false, code, message }` 错误包装与 200 语义不变);③ `connection` 经 `ctx.inject(['connection'], cb)` 可选注入(服务缺席不 pending,仅 Client API 降级不可用),并用 `cb.effect` 包裹 register 返回的**异步 disposer**——注册本体挂在 connection 插件 fiber 的 effect 上(`owner = this.ctx`),不包会在 HMR 重载时撞「exact Fetch route ... is already registered」。web 端由 client-connection 把 `/api` 挂到 webServer 之下、桌面端由 dsh-desktop-host 以 `createSharedFetchHandler('/api')` 直接分发,客户端 URL 与方法两端完全一致,**客户端 bundle 零改动**。契约类型按官方 `.d.ts` 换为 `ConnectionFetchRoute` / `HostConnectionFetch`(HttpRequest/HttpResponse/WebRoute/WebServer 删除);`verify:host` 门禁改 connection 桩并新增路由形状(methods/requestBody/fetch)、exact 分发 404、异步 disposer 清零断言;`compat-audit` 新增 I32、`dsh-contract.md` 与 AGENTS.md 同步。插件功能与 `lib/client.js` 零变化。
|
|
16
|
+
|
|
5
17
|
## [2.3.12] - 2026-09-11
|
|
6
18
|
|
|
19
|
+
### 变更
|
|
20
|
+
|
|
21
|
+
- **dsh 0.1.5-rc.2 兼容性声明与台账同步**:全局实装 `@deepseek-ai/dsh@0.1.5-rc.2`(tag `dsh-v0.1.5-rc.2`,commit `fb2c4b9`,npm dist-tag `next`)后跑三层门禁——`test:probe` 31/31、`verify:host` 装配断言通过、`npm test` 307/307;rc.1→rc.2 的 4 commits / 300 文件按插件消费面包与类型源过滤后唯一命中 ui-chat 的 `TurnTailNodeView.module.css`(+3 行纯 CSS 间距),`sessions.d.ts`/`slots.d.ts`/`slot-contract.d.ts` 等类型源零改动;reference/ 镜像按 rc.2 tag 重拉 13 源(12 份内容相同、11 号仅 CRLF 噪声)。`dsh.compatibility.dshReleases` 补 `0.1.5-rc.2`,README 双语兼容声明、镜像索引与契约文档版本字段同步。peer 范围沿用按 minor 线开窗(`>=0.1.5-alpha.1 <0.1.6`),npm semver 的 prerelease 门槛天然放行同 tuple 的 rc.2,无需改 peer 串。评估实证沉淀于 `docs/upgrade-assessments/dsh-0.1.5-rc.2.md`,本插件源码零功能变更。
|
|
22
|
+
|
|
7
23
|
### 修复
|
|
8
24
|
|
|
9
25
|
- **DSH Desktop(0.1.5-rc.2)安装插件被 `validateDesktopPluginGraph` 拒绝**:桌面端 profile 固定 `autoInstallPeers: false`,安装时逐个解析 `package.json` 的 peerDependencies 并要求实体包存在于 profile 的 node_modules。插件声明的 `@deepseek-ai/dsh-client-web-react` 是 dsh 0.1.0 时代的历史包(npm 最高 0.1.0-rc.7),官方 0.1.1+ 已不再发布、0.1.5-rc.2 的宿主共享包中不存在,故报 `requires missing @deepseek-ai/dsh-client-web-react`。该声明早已无运行时意义:`lib/client.js` 唯一的模块请求是 `require("react")`,React 由宿主平台基线模块表提供。修复三处:① 删除该历史 peer;② 保留 `react` peer 但补 `peerDependenciesMeta.react.optional`——桌面端 profile 顶层无 `react`,不标 optional 会在下一轮校验继续报 missing;③ 删除过时的 `dsh.client.inject`(新机制下是包名依赖边,指向已不存在的包会被宿主静默跳过;`dsh.client.platform: "web"` 保留,宿主据此识别 client 半侧的运行平台)。其余 8 个 peer 及其范围不变。`dsh.compatibility.dshReleases` 补 `0.1.5-rc.2`,README 双语安装兼容声明同步。插件功能与运行时零变化,`lib/` 产物结构与 2.3.11 相同。
|
package/lib/index.js
CHANGED
|
@@ -9,9 +9,8 @@ import { createRoutesManage } from "./routes-manage.js";
|
|
|
9
9
|
import * as dshSettings from "@deepseek-ai/dsh-settings";
|
|
10
10
|
import * as E from "./errors.js";
|
|
11
11
|
const name = "dsh-recall-plugin";
|
|
12
|
-
const inject = ["shell", "sessions", "
|
|
12
|
+
const inject = ["shell", "sessions", "agents"];
|
|
13
13
|
function apply(ctx, config) {
|
|
14
|
-
const webServer = ctx.webServer;
|
|
15
14
|
const cfg = createConfig(config);
|
|
16
15
|
const rt = createRuntime(ctx, cfg);
|
|
17
16
|
const snaps = createSnapshots(ctx, rt, cfg);
|
|
@@ -56,21 +55,11 @@ function apply(ctx, config) {
|
|
|
56
55
|
const excludeCache = { at: 0, payload: null };
|
|
57
56
|
const usageCache = { at: 0, payload: null };
|
|
58
57
|
const sessionInfo = createSessionInfo(ctx);
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (size > MAX_BODY_BYTES) throw new Error(E.RECALL_BODY_TOO_LARGE);
|
|
65
|
-
chunks.push(chunk);
|
|
66
|
-
}
|
|
67
|
-
const text = Buffer.concat(chunks).toString("utf8");
|
|
68
|
-
if (!text.trim()) return {};
|
|
69
|
-
return JSON.parse(text);
|
|
70
|
-
}
|
|
71
|
-
function sendJson(res, status, body) {
|
|
72
|
-
res.writeHead(status, { "content-type": "application/json; charset=utf-8" });
|
|
73
|
-
res.end(JSON.stringify(body));
|
|
58
|
+
function jsonResponse(body, status = 200) {
|
|
59
|
+
return new Response(JSON.stringify(body), {
|
|
60
|
+
status,
|
|
61
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
62
|
+
});
|
|
74
63
|
}
|
|
75
64
|
function errBody(error) {
|
|
76
65
|
const e = error;
|
|
@@ -251,8 +240,6 @@ function apply(ctx, config) {
|
|
|
251
240
|
enqueue,
|
|
252
241
|
agentBusy,
|
|
253
242
|
runLimited,
|
|
254
|
-
readJsonBody,
|
|
255
|
-
sendJson,
|
|
256
243
|
errBody,
|
|
257
244
|
listExcludeFiles,
|
|
258
245
|
dumpStores,
|
|
@@ -278,25 +265,31 @@ function apply(ctx, config) {
|
|
|
278
265
|
...createRoutesCore(deps),
|
|
279
266
|
...createRoutesManage(deps)
|
|
280
267
|
};
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
sendJson(res, 200, await endpoint(args));
|
|
295
|
-
} catch (error) {
|
|
296
|
-
sendJson(res, 200, errBody(error));
|
|
297
|
-
}
|
|
268
|
+
const handleRecall = async (request) => {
|
|
269
|
+
const name2 = new URL(request.url).pathname.replace(/^\/api\/recall\/?/, "").split("/")[0];
|
|
270
|
+
const endpoint = endpoints[name2];
|
|
271
|
+
if (!endpoint) {
|
|
272
|
+
return jsonResponse({ ok: false, code: E.RECALL_UNKNOWN_ENDPOINT, message: "unknown endpoint: " + name2 });
|
|
273
|
+
}
|
|
274
|
+
try {
|
|
275
|
+
const text = await request.text();
|
|
276
|
+
if (Buffer.byteLength(text, "utf8") > MAX_BODY_BYTES) throw new Error(E.RECALL_BODY_TOO_LARGE);
|
|
277
|
+
const args = text.trim() ? JSON.parse(text) : {};
|
|
278
|
+
return jsonResponse(await endpoint(args));
|
|
279
|
+
} catch (error) {
|
|
280
|
+
return jsonResponse(errBody(error));
|
|
298
281
|
}
|
|
299
|
-
}
|
|
282
|
+
};
|
|
283
|
+
ctx.inject(["connection"], (connectionCtx) => {
|
|
284
|
+
for (const name2 of Object.keys(endpoints)) {
|
|
285
|
+
connectionCtx.effect(() => connectionCtx.connection.fetch.register({
|
|
286
|
+
path: "/api/recall/" + name2,
|
|
287
|
+
methods: ["POST"],
|
|
288
|
+
requestBody: "buffered",
|
|
289
|
+
fetch: handleRecall
|
|
290
|
+
}));
|
|
291
|
+
}
|
|
292
|
+
});
|
|
300
293
|
if (!supported) return;
|
|
301
294
|
ctx.on("session/event", (session, event) => {
|
|
302
295
|
if (!event || event.type !== "user/message") return;
|
package/lib/snapshots.js
CHANGED
|
@@ -52,7 +52,7 @@ function parseDiffOutput(text, isWin, maxChanges) {
|
|
|
52
52
|
const changes = all.slice(0, maxChanges);
|
|
53
53
|
return { changes, total: finalTotal, truncated: finalTotal > changes.length, treeId };
|
|
54
54
|
}
|
|
55
|
-
function
|
|
55
|
+
function scanCutSeqDetail(events, messageId) {
|
|
56
56
|
let anchor = -1;
|
|
57
57
|
for (let i = 0; i < events.length; i++) {
|
|
58
58
|
const e = events[i];
|
|
@@ -61,12 +61,21 @@ function scanCutSeq(events, messageId) {
|
|
|
61
61
|
break;
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
|
-
if (anchor < 0) return null;
|
|
64
|
+
if (anchor < 0) return { cut: null, found: false };
|
|
65
65
|
for (let i = anchor - 1; i >= 0; i--) {
|
|
66
66
|
const e = events[i];
|
|
67
|
-
if (e && e.type === "turn/end" && typeof e.seq === "number") return e.seq;
|
|
67
|
+
if (e && e.type === "turn/end" && typeof e.seq === "number") return { cut: e.seq, found: true };
|
|
68
68
|
}
|
|
69
|
-
return null;
|
|
69
|
+
return { cut: null, found: true };
|
|
70
|
+
}
|
|
71
|
+
function scanCutSeq(events, messageId) {
|
|
72
|
+
return scanCutSeqDetail(events, messageId).cut;
|
|
73
|
+
}
|
|
74
|
+
function disposeLease(lease) {
|
|
75
|
+
const disposeSymbol = Symbol.dispose;
|
|
76
|
+
if (!disposeSymbol || lease === null || typeof lease !== "object") return;
|
|
77
|
+
const dispose = lease[disposeSymbol];
|
|
78
|
+
if (typeof dispose === "function") dispose.call(lease);
|
|
70
79
|
}
|
|
71
80
|
async function rescueRollback(deps, opts) {
|
|
72
81
|
const { root, store, safetyId, safetyOk, rollbackError } = opts;
|
|
@@ -374,11 +383,26 @@ function createSnapshots(ctx, rt, config) {
|
|
|
374
383
|
} else {
|
|
375
384
|
const query = ctx.get("sessionQuery");
|
|
376
385
|
if (query) {
|
|
386
|
+
let fallback = false;
|
|
377
387
|
try {
|
|
378
388
|
const log = await query.readSession(sessionId);
|
|
379
|
-
|
|
389
|
+
const detail = scanCutSeqDetail(Array.isArray(log && log.events) ? log.events : [], messageId);
|
|
390
|
+
result = detail.cut;
|
|
391
|
+
fallback = !detail.found;
|
|
380
392
|
} catch (error) {
|
|
381
|
-
|
|
393
|
+
fallback = true;
|
|
394
|
+
}
|
|
395
|
+
if (fallback && typeof query.observeSession === "function") {
|
|
396
|
+
try {
|
|
397
|
+
const lease = await query.observeSession(sessionId);
|
|
398
|
+
try {
|
|
399
|
+
result = scanCutSeq(lease && Array.isArray(lease.events) ? lease.events : [], messageId);
|
|
400
|
+
} finally {
|
|
401
|
+
disposeLease(lease);
|
|
402
|
+
}
|
|
403
|
+
} catch (error) {
|
|
404
|
+
result = null;
|
|
405
|
+
}
|
|
382
406
|
}
|
|
383
407
|
}
|
|
384
408
|
}
|
|
@@ -396,5 +420,6 @@ export {
|
|
|
396
420
|
parseTagsWithTime,
|
|
397
421
|
parseTreeId,
|
|
398
422
|
rescueRollback,
|
|
399
|
-
scanCutSeq
|
|
423
|
+
scanCutSeq,
|
|
424
|
+
scanCutSeqDetail
|
|
400
425
|
};
|