dsh-session-conductor 1.2.0 → 1.2.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 CHANGED
@@ -36,6 +36,25 @@
36
36
 
37
37
  ![模板注入设置](assets/screenshots/settings.png)
38
38
 
39
+ ## 环境要求(支持的 DSH 版本)
40
+
41
+ | 项 | 要求 |
42
+ |---|---|
43
+ | **DSH 版本** | **0.2.1 及以上(推荐)**;0.2.x 早期版本功能可用,但**列表缓存不生效**(见下) |
44
+ | Node | 与宿主 DSH 自带的 Node 一致(无需额外安装) |
45
+ | 平台 | Linux(群晖 DSM / fnOS 等);纯 Node 实现,无原生依赖 |
46
+
47
+ **为什么 0.2.1 是推荐下限**:本插件的会话列表缓存依赖 `sessionPersistence.list()` 返回的
48
+ **`SessionPersistenceSnapshot`**(`{ header, revision, eventCount?, sizeBytes? }`)里的 `revision`
49
+ 作为「日志变过没有」的变更令牌。0.2.1 起该 API 稳定提供 `revision`;更早的版本里
50
+ `list()` 只给裸 header(没有 `revision`),插件会**优雅降级**:功能全部正常,
51
+ 只是每次列表构建都要重新解析会话日志(会话多、日志大时会明显变慢、内存占用高)。
52
+
53
+ > 早期代码曾探测 `persistence.listSnapshots()` 并期望它返回 `{ header, revision }` ——
54
+ > 该 session 级方法**在任何 DSH 版本里都不存在**(0.1.6 里同名的 `listSnapshots(profileDir)`
55
+ > 是 profile 快照,另一套东西),所以那个探测永远失败、缓存从未生效过。
56
+ > 1.2.0 起统一改用 `list()` 并按快照解析。
57
+
39
58
  ## 安装
40
59
 
41
60
  装到哪个 profile 由 `--profile` 指定。`dsh plugin` 会把后面的参数**转发给该 profile 目录下的 pnpm**,
@@ -72,8 +72,11 @@ import { buildSessionListCached } from "../sessions/list.js";
72
72
  * 逼近 --max-old-space-size=2048 上限,即 修的那个堆 OOM)。
73
73
  *
74
74
  * 做法(持久性保存 + 懒加载,内存优先于速度):
75
- * ① 用 persistence.listSnapshots() 拿每个会话的 header + revision(**只读 header 行 + 一次 stat,
76
- * 不解析日志**),revision 是「该会话日志内容是否变过」的变更令牌;
75
+ * ① 用 persistence.list() 拿每个会话的 **SessionPersistenceSnapshot**(`{ header, revision, ... }`;
76
+ * list() 只读 header 行 + 一次 stat,**不解析日志**),revision 是「该会话日志内容是否变过」的变更令牌;
77
+ * ⚠️ 不要探测 `listSnapshots()` —— 该 session 级方法在任何 DSH 版本里都不存在
78
+ * (0.1.6 里同名的 `listSnapshots(profileDir)` 是 profile 快照,另一套 API),
79
+ * 探测永远失败会让缓存静默失效(1.2.0 修掉的正是这个)。
77
80
  * ② 把上面三个字段连同 revision 落盘到 <DSH_HOME>/storages/dsh-session-conductor/list-cache.json;
78
81
  * ③ 下次列列表时 **revision 没变就直接用落盘结果,一次 inspect 都不做**(懒加载核心);
79
82
  * 只有新会话、或日志真的追加过(revision 变了)才重新解析那一条;
@@ -254,7 +257,7 @@ export async function undoLastMessage(ctx, sessionId, { dryRun = false } = {}) {
254
257
  if (targetLineIdx >= 0) break;
255
258
  }
256
259
  if (targetLineIdx < 0) {
257
- return { ok: false, error: { code: "no-user-message", message: "会话里没有可撤回的用户消息" } };
260
+ return { ok: false, error: { code: "no-user-message", message: "该会话没有可撤回的用户消息" } };
258
261
  }
259
262
 
260
263
  // 删除范围 [targetLineIdx, 行尾]:这条消息 + 它触发的整轮回复(文件尾部)
package/lib/group.js CHANGED
@@ -7,7 +7,7 @@
7
7
  * GET /api/session-conductor/group/status 状态(分组数/groupRoot/profile)
8
8
  * GET /api/session-conductor/group/list 全部分组(含 sessionIds / path / title)
9
9
  * POST /api/session-conductor/group/new-session 在分组下新建会话 {workspaceId?, prompt?, handoff?}
10
- * —— workspaceId 缺省 = **上次会话的工作区**(最近活跃会话 cwd 归属的分组);
10
+ * —— workspaceId 缺省 = **最近活跃会话的工作区**(该会话 cwd 归属的分组);
11
11
  * 找不到则兜底 .dsh 上一层 /workspace 默认工作区(不存在自动创建)。
12
12
  *
13
13
  * 面板「全部会话按工作区分组」渲染用 group/list(只读数据源);
@@ -111,7 +111,7 @@ function workspaceIdOfCwd(registry, cwd) {
111
111
  return ws?.id ?? '';
112
112
  }
113
113
 
114
- /** 「上次会话」的 cwd:最近活跃(updatedAt/createdAt 最大)会话的工作目录(live + 冷会话合并)。 */
114
+ /** 最近活跃会话的 cwd:updatedAt/createdAt 最大者的工作目录(live + 冷会话合并)。 */
115
115
  async function lastSessionCwd(ctx) {
116
116
  const sessions = ctx.get('sessions');
117
117
  const persistence = ctx.get('sessionPersistence');
@@ -122,17 +122,15 @@ async function lastSessionCwd(ctx) {
122
122
  if (updatedAtMs >= best.t && header?.cwd) best = { t: updatedAtMs, cwd: header.cwd };
123
123
  }
124
124
  try {
125
- if (typeof persistence?.listSnapshots === 'function') {
126
- for (const snap of await persistence.listSnapshots()) {
127
- const header = snap?.header ?? {};
128
- const updatedAtMs = Number(header?.updatedAt ?? header?.createdAt ?? 0);
129
- if (updatedAtMs >= best.t && header?.cwd) best = { t: updatedAtMs, cwd: header.cwd };
130
- }
131
- } else {
132
- for (const snapshot of await persistence?.list?.() ?? []) {
133
- const updatedAtMs = Number(snapshot?.updatedAt ?? snapshot?.createdAt ?? 0);
134
- if (updatedAtMs >= best.t && snapshot?.cwd) best = { t: updatedAtMs, cwd: snapshot.cwd };
135
- }
125
+ // 宿主 list() 返回的是 **SessionPersistenceSnapshot**(`{ header, revision, ... }`),不是裸 header。
126
+ // 旧写法分两支:探测 `listSnapshots()`(宿主从未有过该 session 级方法,探测永远失败),
127
+ // 失败支里直接读 `snapshot.cwd / snapshot.updatedAt` —— 两者都是 undefined,
128
+ // 于是「最近活跃会话的 cwd」在冷会话上**静默失效**(只剩 live 会话参与比较)。
129
+ // 现在统一解包:`entry.header ?? entry` 兼容新快照与旧裸 header 两种形态。
130
+ for (const entry of await persistence?.list?.() ?? []) {
131
+ const header = entry?.header ?? entry ?? {};
132
+ const updatedAtMs = Number(header?.updatedAt ?? header?.createdAt ?? 0);
133
+ if (updatedAtMs >= best.t && header?.cwd) best = { t: updatedAtMs, cwd: header.cwd };
136
134
  }
137
135
  } catch { /* 冷会话扫描失败不影响 live 结果 */ }
138
136
  return best.cwd;
@@ -198,7 +196,7 @@ export async function registerGroupRoutes(ctx, config = {}, hooks = {}) {
198
196
  const body = await readJsonBody(req);
199
197
  let workspaceId = String(body?.workspaceId ?? '').trim();
200
198
  if (!workspaceId) {
201
- // workspaceId 缺省 = 上次会话的工作区(最近活跃会话 cwd 归属的分组)
199
+ // workspaceId 缺省 = 最近活跃会话的工作区(该会话 cwd 归属的分组)
202
200
  const lastCwd = await lastSessionCwd(ctx);
203
201
  if (lastCwd) workspaceId = workspaceIdOfCwd(registry, lastCwd);
204
202
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-session-conductor",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "description": "DSH session management enhancement: archive/restore sessions, batch & conditional delete, AI auto-rename, interrupted-session auto-resume, release idle sessions, workspace grouping, undo last message, repair damaged session logs, value analysis, full-text search, per-member model switching (list_models / set_member_model), and plan/closing template injection (system-prompt + context).",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",