dsh-session-conductor 1.3.8 → 1.3.10

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.
@@ -13,13 +13,10 @@
13
13
  // 【落盘】<DSH_HOME>/storages/dsh-session-conductor/value-index.json,原子写(tmp + rename),
14
14
  // 节流 2 秒合并多次写入;任何异常都退化为「没有索引」,绝不影响筛选本身。
15
15
 
16
- import { readFileSync, writeFileSync, renameSync, mkdirSync } from "node:fs";
17
- import path from "node:path";
18
-
19
- import { cfg, resolveDshHome } from "../core/config.js";
16
+ import { cfg } from "../core/config.js";
17
+ import { getSection, setSection, saveStoreNow } from "../core/index-store.js";
20
18
 
21
19
  const VALUE_INDEX_VERSION = 1;
22
- const VALUE_INDEX_SUBDIR = ["storages", "dsh-session-conductor"];
23
20
  const DEFAULT_TTL_HOURS = 24;
24
21
  const SAVE_DEBOUNCE_MS = 2000;
25
22
 
@@ -29,27 +26,18 @@ let indexPath = null;
29
26
  let indexDirty = false;
30
27
  let indexTimer = null;
31
28
 
32
- /** 索引文件绝对路径(首次解析后缓存)。 */
33
- export function valueIndexFilePath(ctx) {
34
- if (indexPath !== null) return indexPath;
35
- const home = resolveDshHome(ctx, cfg);
36
- indexPath = home ? path.join(home, ...VALUE_INDEX_SUBDIR, "value-index.json") : "";
37
- return indexPath;
38
- }
39
-
40
- /** 读索引(进程内只读一次;文件不存在/损坏都按空索引处理,不向上抛)。 */
29
+ /** 读索引(进程内只读一次;索引不可用都按空处理,不向上抛)。
30
+ * 落盘统一走 <DSH_HOME>/storages/dsh-session-conductor/index.json 的 value section。 */
41
31
  export function loadValueIndex(ctx) {
42
32
  if (indexCache !== null) return indexCache;
43
33
  indexCache = { version: VALUE_INDEX_VERSION, entries: {} };
44
- const file = valueIndexFilePath(ctx);
45
- if (!file) return indexCache;
46
34
  try {
47
- const parsed = JSON.parse(readFileSync(file, "utf8"));
48
- if (parsed?.version === VALUE_INDEX_VERSION && parsed.entries && typeof parsed.entries === "object") {
49
- indexCache = { version: VALUE_INDEX_VERSION, entries: parsed.entries };
35
+ const section = getSection(ctx, "value");
36
+ if (section?.entries && typeof section.entries === "object") {
37
+ indexCache = { version: VALUE_INDEX_VERSION, entries: section.entries };
50
38
  }
51
39
  } catch {
52
- // 首次运行 / 文件损坏:按空索引继续,本次算完会覆盖写入
40
+ // 索引不可用:按空索引继续
53
41
  }
54
42
  return indexCache;
55
43
  }
@@ -112,13 +100,10 @@ export function forgetValueVerdict(ctx, sessionId) {
112
100
 
113
101
  /** 立即落盘(原子写:tmp + rename)。失败只丢缓存,不抛。 */
114
102
  export function saveValueIndexNow(ctx) {
115
- const file = valueIndexFilePath(ctx);
116
- if (!file || indexCache === null) return;
103
+ if (indexCache === null) return;
117
104
  try {
118
- mkdirSync(path.dirname(file), { recursive: true });
119
- const tmpPath = `${file}.tmp-${process.pid}`;
120
- writeFileSync(tmpPath, JSON.stringify(indexCache), "utf8");
121
- renameSync(tmpPath, file);
105
+ setSection(ctx, "value", { entries: indexCache.entries });
106
+ saveStoreNow(ctx);
122
107
  indexDirty = false;
123
108
  } catch {
124
109
  // 写盘失败(只读卷/磁盘满):本次不落盘,下次继续尝试
package/lib/i18n/en.json CHANGED
@@ -13,6 +13,21 @@
13
13
  "tab.archived": "Archived",
14
14
  "tab.active": "Active",
15
15
  "tab.help": "Help",
16
+ "tab.trash": "Trash",
17
+ "trash.retention.label": "Keep",
18
+ "trash.retention.unit": "days",
19
+ "trash.retention.save": "Save",
20
+ "trash.undo": "Undo last delete",
21
+ "trash.undo.title": "Restore the latest batch ({n})",
22
+ "trash.restoreSelected": "Restore selected ({n})",
23
+ "trash.selectAll": "Select all",
24
+ "trash.empty": "Empty trash",
25
+ "trash.emptyConfirm": "Emptying the trash permanently deletes {n} sessions (irreversible). Continue?",
26
+ "trash.restore": "Restore",
27
+ "trash.emptyList": "Trash is empty",
28
+ "trash.hint": "Deleting only marks the session; files stay until the retention period ends or you empty the trash",
29
+ "trash.remaining": "{n} day(s) left",
30
+ "trash.nothingToUndo": "Nothing to undo",
16
31
  "list.empty": "No sessions yet",
17
32
  "list.archivedEmpty": "Nothing archived",
18
33
  "list.activeEmpty": "No active sessions (live/running)",
@@ -62,8 +77,8 @@
62
77
  "action.analyze": "Analyze",
63
78
  "action.busy": "Working…",
64
79
  "action.retry": "Retry",
65
- "confirm.delete.title": "Permanently delete session \"{title}\"?",
66
- "confirm.delete.body": "This cannot be undone (the session log will be removed too).",
80
+ "confirm.delete.title": "Delete session \"{title}\"?",
81
+ "confirm.delete.body": "The session moves to the Trash and can be restored from the Trash tab at any time. Files are only removed when the retention period (default 7 days) expires or you empty the trash.",
67
82
  "error.action": "Action failed: {message}",
68
83
  "meta.time.now": "just now",
69
84
  "meta.time.minutes": "{n}m ago",
@@ -71,8 +86,8 @@
71
86
  "meta.time.days": "{n}d ago",
72
87
  "meta.time.months": "{n}mo ago",
73
88
  "meta.time.years": "{n}y ago",
74
- "help.delete.title": "Delete session (added by this plugin)",
75
- "help.delete.desc": "The built-in UI has no way to delete a session. This plugin adds it: refuses to delete a running session, double-checks before deleting, removes the log for good.",
89
+ "help.delete.title": "Delete sessions (added by this plugin)",
90
+ "help.delete.desc": "The built-in UI has no way to delete a session; this plugin adds it. Deleting only marks the session - files stay untouched: it goes to the Trash tab, where you can restore one, restore several, or undo the latest batch. Running sessions are refused. Files are really removed only when the retention period expires (configurable, default 7 days) or you empty the trash.",
76
91
  "help.autoRename.title": "Auto-rename (added by this plugin)",
77
92
  "help.autoRename.desc": "Turn on \"Auto-rename\" for a session and after each finished turn the plugin asks an AI to compare recent session content against the current title; when the topic has clearly drifted it generates a new title and renames automatically. Titles carry a live state suffix: \"(运行中)\" = turn in progress, \"(已中断)\" = crashed/errored and waiting to resume; updated at turn boundaries. Rate-limited (5 min interval, needs a few new messages) to keep token costs low.",
78
93
  "help.autoContinue.title": "Auto-resume (added by this plugin)",
@@ -99,10 +114,10 @@
99
114
  "search.hits": "{n} sessions matched",
100
115
  "search.close": "Collapse results",
101
116
  "deleteBatch.action": "Delete selected ({n})",
102
- "deleteBatch.confirm.title": "Permanently delete the {n} selected sessions?",
103
- "deleteBatch.confirm.body": "This cannot be undone (logs are removed too); running sessions are skipped automatically.",
104
- "deleteBatch.done": "Deleted {deleted}{skippedText}",
105
- "deleteBatch.skipped": ", skipped {n} (running/undeletable)",
117
+ "deleteBatch.confirm.title": "Delete the {n} selected sessions?",
118
+ "deleteBatch.confirm.body": "They move to the Trash and can be restored anytime. Running sessions are skipped automatically.",
119
+ "deleteBatch.done": "Moved {deleted} to the Trash{skippedText}",
120
+ "deleteBatch.skipped": ", skipped {n} (running / not deletable)",
106
121
  "deleteByRule.action": "Filter",
107
122
  "deleteByRule.label.archivedOnly": "Archived only",
108
123
  "deleteByRule.label.inactiveDays": "Inactive for more than {n} days (0 = any)",
@@ -202,4 +217,4 @@
202
217
  "armCurNone": "Current: not selected",
203
218
  "renderFail": "Settings render failed: "
204
219
  }
205
- }
220
+ }
package/lib/i18n/zh.json CHANGED
@@ -13,6 +13,21 @@
13
13
  "tab.archived": "已归档",
14
14
  "tab.active": "活跃",
15
15
  "tab.help": "说明",
16
+ "tab.trash": "回收站",
17
+ "trash.retention.label": "保留",
18
+ "trash.retention.unit": "天",
19
+ "trash.retention.save": "保存",
20
+ "trash.undo": "撤销上次删除",
21
+ "trash.undo.title": "还原最近一批({n} 个)",
22
+ "trash.restoreSelected": "还原选中 ({n})",
23
+ "trash.selectAll": "全选",
24
+ "trash.empty": "一键清空",
25
+ "trash.emptyConfirm": "清空回收站会立即真删 {n} 个会话(不可恢复),确定吗?",
26
+ "trash.restore": "还原",
27
+ "trash.emptyList": "回收站是空的",
28
+ "trash.hint": "删除只是打标记、文件还在;到期(保留天数)或一键清空才真正删盘",
29
+ "trash.remaining": "剩 {n} 天",
30
+ "trash.nothingToUndo": "没有可撤销的删除",
16
31
  "list.empty": "还没有会话",
17
32
  "list.archivedEmpty": "没有已归档的会话",
18
33
  "list.activeEmpty": "当前没有活跃会话(live/running)",
@@ -55,8 +70,8 @@
55
70
  "action.analyze": "分析",
56
71
  "action.busy": "处理中…",
57
72
  "action.retry": "重试",
58
- "confirm.delete.title": "确定要永久删除会话「{title}」吗?",
59
- "confirm.delete.body": "删除后无法恢复(会话日志与记录将一并删除)。",
73
+ "confirm.delete.title": "确定要删除会话「{title}」吗?",
74
+ "confirm.delete.body": "会话会移入回收站,可在「回收站」页随时还原;只有到期(保留天数,默认 7 天)或一键清空才真正删盘。",
60
75
  "action.undo": "撤回消息",
61
76
  "undo.preview.empty": "(无文本消息)",
62
77
  "confirm.undo.title": "撤回最后一条消息?",
@@ -72,7 +87,7 @@
72
87
  "meta.time.months": "{n}个月前",
73
88
  "meta.time.years": "{n}年前",
74
89
  "help.delete.title": "删除会话(本插件新增)",
75
- "help.delete.desc": "内置界面没有任何删除会话的入口。本插件补上了:拒绝删除运行中的会话,删除前二次确认,删除后日志一并清除、不可恢复。",
90
+ "help.delete.desc": "内置界面没有任何删除会话的入口,本插件补上了。删除**只打标记、不动会话文件**:会话会进「回收站」标签页,可单条还原、多选还原,或按批次撤销最近一次删除;运行中的会话拒绝删除。真正删盘只发生在回收站到期(保留天数可设,默认 7 天)或「一键清空」。",
76
91
  "help.autoRename.title": "自动重命名(本插件新增)",
77
92
  "help.autoRename.desc": "给会话勾选「自动重命名」后,每个回合结束,插件会用 AI 分析最近会话内容;发现主题/方向已明显偏离当前标题时,自动生成新标题并改掉。标题会带上实时状态后缀:「(运行中)」= 正在执行回合、「(已中断)」= 崩溃/错误中断待续跑;回合结束自动更新。分析带 5 分钟最小间隔和新增消息数门槛,避免频繁调用浪费额度。",
78
93
  "help.autoContinue.title": "自动续跑(本插件新增)",
@@ -99,9 +114,9 @@
99
114
  "search.hits": "命中 {n} 个会话",
100
115
  "search.close": "收起结果",
101
116
  "deleteBatch.action": "删除选中 ({n})",
102
- "deleteBatch.confirm.title": "确定要永久删除选中的 {n} 个会话吗?",
103
- "deleteBatch.confirm.body": "删除后无法恢复(日志一并清除);运行中的会话会被自动跳过。",
104
- "deleteBatch.done": "已删除 {deleted} 个{skippedText}",
117
+ "deleteBatch.confirm.title": "确定要删除选中的 {n} 个会话吗?",
118
+ "deleteBatch.confirm.body": "会移入回收站,可随时还原;运行中的会话会被自动跳过。",
119
+ "deleteBatch.done": "已移入回收站 {deleted} 个{skippedText}",
105
120
  "deleteBatch.skipped": ",跳过 {n} 个(运行中/不可删)",
106
121
  "deleteByRule.action": "筛选",
107
122
  "deleteByRule.label.archivedOnly": "仅已归档会话",
@@ -202,4 +217,4 @@
202
217
  "armCurNone": "当前:未选择",
203
218
  "renderFail": "设置页渲染失败:"
204
219
  }
205
- }
220
+ }
@@ -7,45 +7,32 @@
7
7
  // ③ 列表 TTL 缓存与失效(写操作后 invalidateSessionListCache 立即失效)。
8
8
  // 列表的构建流程(buildSessionList)在 list.js。
9
9
 
10
- import { readFileSync, writeFileSync, renameSync, mkdirSync } from "node:fs";
11
- import path from "node:path";
10
+ import { getSection, setSection, saveStoreNow } from "../core/index-store.js";
12
11
  import { log } from "../shared/log.js";
13
- import { cfg, resolveDshHome, autoRenameEnabled, effectiveAutoContinue } from "../core/config.js";
12
+ import { cfg, autoRenameEnabled, effectiveAutoContinue } from "../core/config.js";
14
13
  import { continueJobs } from "../core/state.js";
15
14
  import { stripArchiveWsPrefix } from "../features/rename/title.js";
16
15
 
17
16
  /** 缓存文件版本(不符视为空缓存)。 */
18
17
  const LIST_CACHE_VERSION = 1;
19
- /** 缓存文件所在子目录。 */
20
- const LIST_CACHE_SUBDIR = ["storages", "dsh-session-conductor"];
21
18
 
22
- /** 落盘缓存内容:{ version, entries: { [id]: { rev, title, updatedAt, interruption, inspectError, cwd, createdAt, parentSession } } } */
19
+ /** 内存缓存:{ version, entries: { [id]: { rev, title, updatedAt, interruption, inspectError, cwd, createdAt, parentSession } } }
20
+ * 落盘统一走 <DSH_HOME>/storages/dsh-session-conductor/index.json 的 list section。 */
23
21
  let listDiskCache = null;
24
- let listDiskCachePath = null;
25
22
  let listDiskCacheDirty = false;
26
23
  let listDiskCacheTimer = null;
27
24
 
28
- /** 缓存文件绝对路径(首次调用时解析并缓存)。 */
29
- export function listCacheFilePath(ctx) {
30
- if (listDiskCachePath !== null) return listDiskCachePath;
31
- const home = resolveDshHome(ctx, cfg);
32
- listDiskCachePath = home ? path.join(home, ...LIST_CACHE_SUBDIR, "list-cache.json") : "";
33
- return listDiskCachePath;
34
- }
35
-
36
- /** 读落盘缓存(进程内只读一次)。任何异常都退化成空缓存,不向上抛。 */
25
+ /** 读缓存(进程内只读一次)。任何异常都退化成空缓存,不向上抛。 */
37
26
  export function loadListDiskCache(ctx) {
38
27
  if (listDiskCache !== null) return listDiskCache;
39
28
  listDiskCache = { version: LIST_CACHE_VERSION, entries: {} };
40
- const file = listCacheFilePath(ctx);
41
- if (!file) return listDiskCache;
42
29
  try {
43
- const parsed = JSON.parse(readFileSync(file, "utf8"));
44
- if (parsed?.version === LIST_CACHE_VERSION && parsed.entries !== null && typeof parsed.entries === "object") {
45
- listDiskCache = { version: LIST_CACHE_VERSION, entries: parsed.entries };
30
+ const section = getSection(ctx, "list");
31
+ if (section?.entries && typeof section.entries === "object") {
32
+ listDiskCache = { version: LIST_CACHE_VERSION, entries: section.entries };
46
33
  }
47
34
  } catch {
48
- // 首次运行(文件不存在)/ 文件损坏 / JSON 非法:按空缓存继续,本次解析完会覆盖写入
35
+ // 索引不可用:按空缓存继续,本次解析完会覆盖写入
49
36
  }
50
37
  return listDiskCache;
51
38
  }
@@ -67,14 +54,10 @@ export function scheduleSaveListDiskCache(ctx) {
67
54
  /** 立即原子落盘(写临时文件再 rename,避免下次启动读到半个文件)。 */
68
55
  export function saveListDiskCacheNow(ctx) {
69
56
  if (!listDiskCacheDirty || listDiskCache === null) return;
70
- const file = listCacheFilePath(ctx);
71
- if (!file) return;
72
57
  listDiskCacheDirty = false;
73
58
  try {
74
- mkdirSync(path.dirname(file), { recursive: true });
75
- const tmpPath = `${file}.tmp`;
76
- writeFileSync(tmpPath, JSON.stringify(listDiskCache), "utf8");
77
- renameSync(tmpPath, file);
59
+ setSection(ctx, "list", { entries: listDiskCache.entries });
60
+ saveStoreNow(ctx);
78
61
  } catch (error) {
79
62
  // 落盘失败只是让下次重新解析,不影响列表功能;记录下来便于诊断
80
63
  log(ctx, `会话列表缓存落盘失败(不影响功能): ${String(error?.message ?? error)}`);
@@ -195,7 +178,6 @@ export function settleListBuild(promise, items) {
195
178
  /** 测试钩子:清空列表缓存状态(单测在场景之间调用)。 */
196
179
  export function resetListCacheForTest() {
197
180
  listDiskCache = null;
198
- listDiskCachePath = null;
199
181
  listDiskCacheDirty = false;
200
182
  if (listDiskCacheTimer !== null) clearTimeout(listDiskCacheTimer);
201
183
  listDiskCacheTimer = null;
@@ -221,6 +221,17 @@ export async function buildSessionList(ctx, opts = {}) { // dsh-skip-func-length
221
221
  }
222
222
  }
223
223
 
224
+ // 回收站里的会话不进列表:删除只是打标记,文件仍在磁盘上,只是对外隐藏。
225
+ try {
226
+ const { trashedIds } = await import("../features/trash.js");
227
+ const hidden = trashedIds(ctx);
228
+ if (hidden.size > 0) {
229
+ for (let i = items.length - 1; i >= 0; i -= 1) {
230
+ if (hidden.has(items[i].id)) items.splice(i, 1);
231
+ }
232
+ }
233
+ } catch { /* 回收站不可用不影响列表 */ }
234
+
224
235
  items.sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0));
225
236
  return items;
226
237
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-session-conductor",
3
- "version": "1.3.8",
3
+ "version": "1.3.10",
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",
@@ -66,4 +66,4 @@
66
66
  "./skills/dsh-session-official-doc.md"
67
67
  ]
68
68
  }
69
- }
69
+ }