@michengai/dsh-archive-manager 0.1.14 → 0.1.16

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 CHANGED
@@ -2,7 +2,22 @@
2
2
 
3
3
  [简体中文](CHANGELOG.zh-CN.md)
4
4
 
5
- The five most recent published versions are listed below.
5
+ Published release notes are retained below; new versions are added without removing earlier entries.
6
+
7
+ ## 0.1.16 — 2026-08-27
8
+
9
+ - Added a restore icon to archived-group bulk action menus for consistent action affordances.
10
+ - Shortened the ungrouped bulk action labels to **Restore all** and **Delete all**, avoiding repeated context and reducing menu width.
11
+
12
+ Published package: [`@michengai/dsh-archive-manager@0.1.16`](https://www.npmjs.com/package/@michengai/dsh-archive-manager/v/0.1.16).
13
+
14
+ ## 0.1.15 — 2026-08-26
15
+
16
+ - Hardened permanent deletion so missing transcripts clear stale archive, workspace, spill, and projection-cache data while failed physical deletion remains retryable.
17
+ - Cleared workspace and projection-cache tombstones when a session ID is reused, preventing valid replacement sessions from being blocked.
18
+ - Corrected batch deletion feedback and duplicate restore submission handling, and aligned client batch counts with the host's authoritative archive set.
19
+
20
+ Published package: [`@michengai/dsh-archive-manager@0.1.15`](https://www.npmjs.com/package/@michengai/dsh-archive-manager/v/0.1.15).
6
21
 
7
22
  ## 0.1.14 — 2026-08-24
8
23
 
@@ -40,3 +55,10 @@ Release tag: [`v0.1.11`](https://github.com/MichengAI/dsh-archive-manager/tree/v
40
55
  - Counted only visible conversations in archive totals.
41
56
 
42
57
  Release tag: [`v0.1.10`](https://github.com/MichengAI/dsh-archive-manager/tree/v0.1.10).
58
+
59
+ ## 0.1.9 — 2026-08-17
60
+
61
+ - Removed client-side cascade deletes and surfaced sidebar operation errors.
62
+ - Restyled the project filter and refreshed plugin documentation.
63
+
64
+ Release tag: [`v0.1.9`](https://github.com/MichengAI/dsh-archive-manager/tree/v0.1.9).
@@ -2,7 +2,22 @@
2
2
 
3
3
  [English](CHANGELOG.md)
4
4
 
5
- 以下记录最近发布的五个版本。
5
+ 以下发布说明会持续保留;新增版本时不再删除较早记录。
6
+
7
+ ## 0.1.16 — 2026-08-27
8
+
9
+ - 为归档分组的批量操作菜单补充恢复图标,使操作入口的视觉提示保持一致。
10
+ - 将未分组批量操作文案缩短为「全部恢复」和「全部删除」,避免重复上下文并减小菜单宽度。
11
+
12
+ 发布包:[`@michengai/dsh-archive-manager@0.1.16`](https://www.npmjs.com/package/@michengai/dsh-archive-manager/v/0.1.16)。
13
+
14
+ ## 0.1.15 — 2026-08-26
15
+
16
+ - 加固永久删除流程:转录已缺失时清理陈旧归档标记、工作区记账、spill 和投影缓存,物理删除失败时仍可重试。
17
+ - 会话 ID 被复用时同步撤销工作区与投影缓存墓碑,避免合法的新会话生命周期被阻断。
18
+ - 修正批量删除反馈和单条恢复重复提交,并使客户端批量计数与宿主权威归档集合保持一致。
19
+
20
+ 发布包:[`@michengai/dsh-archive-manager@0.1.15`](https://www.npmjs.com/package/@michengai/dsh-archive-manager/v/0.1.15)。
6
21
 
7
22
  ## 0.1.14 — 2026-08-24
8
23
 
@@ -40,3 +55,10 @@
40
55
  - 归档计数仅统计当前可见会话。
41
56
 
42
57
  发布标签:[`v0.1.10`](https://github.com/MichengAI/dsh-archive-manager/tree/v0.1.10)。
58
+
59
+ ## 0.1.9 — 2026-08-17
60
+
61
+ - 移除客户端级联删除,并显示侧栏操作错误。
62
+ - 重做项目筛选样式并刷新插件文档。
63
+
64
+ 发布标签:[`v0.1.9`](https://github.com/MichengAI/dsh-archive-manager/tree/v0.1.9)。
package/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  **Safely manage archived sessions in DeepSeek Harness**
10
10
 
11
- [简体中文](README.zh-CN.md) · [Apache-2.0](LICENSE)
11
+ [简体中文](README.zh-CN.md) · [Changelog](CHANGELOG.md) · [Apache-2.0](LICENSE)
12
12
 
13
13
  [![License: Apache-2.0](https://img.shields.io/badge/License-Apache--2.0-blue.svg)](LICENSE)
14
14
  [![npm package](https://img.shields.io/npm/v/%40michengai%2Fdsh-archive-manager.svg?label=npm%20package)](https://www.npmjs.com/package/@michengai/dsh-archive-manager)
@@ -105,7 +105,7 @@ dsh plugin --profile web add @michengai/dsh-archive-manager@latest --registry=ht
105
105
  dsh --profile web --dump-config
106
106
  ```
107
107
 
108
- To pin a release, replace `@latest` with a version such as `@0.1.8`.
108
+ To pin a release, replace `@latest` with a specific version such as `@x.y.z`.
109
109
 
110
110
  The configuration output should contain `workspace-archive-manager` and `ui-workspace-archive-manager`. Restart DSH Web and hard-refresh the browser. Do not copy client files manually: the Settings page and archive menu need the mounted plugin.
111
111
 
@@ -186,4 +186,4 @@ Licensed under [Apache License 2.0](LICENSE).
186
186
 
187
187
  ## Changelog
188
188
 
189
- See [CHANGELOG.md](CHANGELOG.md) for the five most recent releases.
189
+ See [CHANGELOG.md](CHANGELOG.md) for retained release notes.
package/README.zh-CN.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  **在 DeepSeek Harness 中安全管理已归档会话**
10
10
 
11
- [English](README.md) · [Apache-2.0](LICENSE)
11
+ [English](README.md) · [更新日志](CHANGELOG.zh-CN.md) · [Apache-2.0](LICENSE)
12
12
 
13
13
  [![许可证:Apache-2.0](https://img.shields.io/badge/许可证-Apache--2.0-blue.svg)](LICENSE)
14
14
  [![npm package](https://img.shields.io/npm/v/%40michengai%2Fdsh-archive-manager.svg?label=npm%20package)](https://www.npmjs.com/package/@michengai/dsh-archive-manager)
@@ -105,7 +105,7 @@ dsh plugin --profile web add @michengai/dsh-archive-manager@latest --registry=ht
105
105
  dsh --profile web --dump-config
106
106
  ```
107
107
 
108
- 需要钉死某一版时,把 `@latest` 换成具体版本,例如 `@0.1.8`。
108
+ 需要钉死某一版时,把 `@latest` 换成具体版本,例如 `@x.y.z`。
109
109
 
110
110
  配置输出中应包含 `workspace-archive-manager` 与 `ui-workspace-archive-manager`。安装后重启 DSH Web 并在浏览器硬刷新;请勿手工复制客户端文件,否则设置页和归档菜单不会被挂载。
111
111
 
@@ -186,4 +186,4 @@ pnpm pack:check
186
186
 
187
187
  ## 更新日志
188
188
 
189
- 最近五个发布版本见 [CHANGELOG.zh-CN.md](CHANGELOG.zh-CN.md)。
189
+ 完整保留的发布说明见 [CHANGELOG.zh-CN.md](CHANGELOG.zh-CN.md)。
package/lib/client.js CHANGED
@@ -2192,9 +2192,7 @@ window.__ModuleLoader__.load({
2192
2192
  setDeletingSession(true);
2193
2193
  setDeleteSessionError(null);
2194
2194
  const rootId = deleteSessionTarget.sessionId;
2195
- (async () => {
2196
- await deleteSession(rootId);
2197
- })().then(() => {
2195
+ deleteSession(rootId).then(() => {
2198
2196
  setDeletingSession(false);
2199
2197
  setDeleteSessionTarget(null);
2200
2198
  setDeleteSessionCommittedId(rootId);
@@ -2728,7 +2726,8 @@ window.__ModuleLoader__.load({
2728
2726
  const ungrouped = group.key === ARCHIVE_UNGROUPED_KEY;
2729
2727
  const items = [{
2730
2728
  id: "restore",
2731
- label: t(ungrouped ? "archives.restoreUngrouped" : "archives.restoreProject")
2729
+ label: t(ungrouped ? "archives.restoreUngrouped" : "archives.restoreProject"),
2730
+ icon: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconRefreshOutline16, {})
2732
2731
  }, {
2733
2732
  id: "delete",
2734
2733
  label: t(ungrouped ? "archives.deleteUngrouped" : "archives.deleteProject"),
@@ -2762,7 +2761,7 @@ window.__ModuleLoader__.load({
2762
2761
  }
2763
2762
  /** 客户端只用此派生显示权威批次计数;宿主会用自己的持久状态重新解析。 */
2764
2763
  function deriveArchivedBatchIds(archivedSessionIds, items, target) {
2765
- const ids = [...new Set((archivedSessionIds ?? []).filter((id) => typeof id === "string" && id.length > 0))];
2764
+ const ids = [...new Set(archivedSessionIds ?? [])];
2766
2765
  if (target.scope === "all") return ids;
2767
2766
  if (target.scope === "workspace") {
2768
2767
  const accounted = new Set(items.find((workspace) => workspace.workspaceId === target.workspaceId)?.sessionIds ?? []);
@@ -2771,6 +2770,24 @@ window.__ModuleLoader__.load({
2771
2770
  const accounted = new Set(items.flatMap((workspace) => workspace.sessionIds));
2772
2771
  return ids.filter((id) => !accounted.has(id));
2773
2772
  }
2773
+ /** 把批量删除结果转换为准确区分“已删除”与“陈旧标记已清理”的反馈。 */
2774
+ function archivedDeleteFeedback(result, t) {
2775
+ const deleted = result.deletedSessionIds.length;
2776
+ const skipped = result.skippedSessionIds.length;
2777
+ if (result.failures.length > 0) return {
2778
+ kind: "error",
2779
+ message: t("archives.deletePartial", { deleted, skipped, failed: result.failures.length, detail: result.failures[0].message })
2780
+ };
2781
+ if (skipped > 0 && deleted > 0) return {
2782
+ kind: "notice",
2783
+ message: t("archives.deleteSuccessWithSkipped", { deleted, skipped })
2784
+ };
2785
+ if (skipped > 0) return {
2786
+ kind: "notice",
2787
+ message: t("archives.deleteSkipped", { n: skipped })
2788
+ };
2789
+ return { kind: "notice", message: t("archives.deleteSuccess", { n: deleted }) };
2790
+ }
2774
2791
  /**
2775
2792
  * 设置页归档分组派生:按工作区分组,组 key 用 workspaceId(上游允许
2776
2793
  * 不同路径的工作区同名,title 不能作 React key / 筛选 value),未归入
@@ -2837,6 +2854,8 @@ window.__ModuleLoader__.load({
2837
2854
  const [project, setProject] = (0, react.useState)("all");
2838
2855
  const [sortBy, setSortBy] = (0, react.useState)("updated");
2839
2856
  const [createdAtById, setCreatedAtById] = (0, react.useState)({});
2857
+ const [unarchivingSessionIds, setUnarchivingSessionIds] = (0, react.useState)(() => /* @__PURE__ */ new Set());
2858
+ const unarchivingSessionIdsRef = (0, react.useRef)(/* @__PURE__ */ new Set());
2840
2859
  const groups = (0, react.useMemo)(() => deriveArchivedGroups(sessions.byId, workspaceState.items, workspaceState.archivedSessionIds, t("group.ungrouped")), [sessions.byId, workspaceState, t]);
2841
2860
  const sortedGroups = (0, react.useMemo)(() => sortArchivedGroups(groups, sortBy, createdAtById, t), [groups, sortBy, createdAtById, t]);
2842
2861
  (0, react.useEffect)(() => {
@@ -2868,10 +2887,16 @@ window.__ModuleLoader__.load({
2868
2887
  const allBatchTarget = { scope: "all" };
2869
2888
  const allBatchSessionIds = (0, react.useMemo)(() => deriveArchivedBatchIds(workspaceState.archivedSessionIds, workspaceState.items, allBatchTarget), [workspaceState.archivedSessionIds, workspaceState.items]);
2870
2889
  const onUnarchive = (sessionId) => {
2890
+ if (busy || unarchivingSessionIdsRef.current.has(sessionId)) return;
2891
+ unarchivingSessionIdsRef.current.add(sessionId);
2892
+ setUnarchivingSessionIds(new Set(unarchivingSessionIdsRef.current));
2871
2893
  setError(null);
2872
2894
  setNotice(null);
2873
2895
  unarchiveSession(sessionId).catch((reason) => {
2874
2896
  setError(formatUnarchiveError(reason, t));
2897
+ }).finally(() => {
2898
+ unarchivingSessionIdsRef.current.delete(sessionId);
2899
+ setUnarchivingSessionIds(new Set(unarchivingSessionIdsRef.current));
2875
2900
  });
2876
2901
  };
2877
2902
  const onBatchUnarchive = async (target) => {
@@ -2911,12 +2936,9 @@ window.__ModuleLoader__.load({
2911
2936
  try {
2912
2937
  if (deleteTarget.kind === "batch") {
2913
2938
  const result = await deleteArchivedSessions(deleteTarget.target);
2914
- const completed = result.deletedSessionIds.length + result.skippedSessionIds.length;
2915
- if (result.failures.length > 0) {
2916
- setError(t("archives.deletePartial", { done: completed, failed: result.failures.length, detail: result.failures[0].message }));
2917
- } else {
2918
- setNotice(t("archives.deleteSuccess", { n: completed }));
2919
- }
2939
+ const feedback = archivedDeleteFeedback(result, t);
2940
+ if (feedback.kind === "error") setError(feedback.message);
2941
+ else setNotice(feedback.message);
2920
2942
  } else {
2921
2943
  await deleteSession(deleteTarget.session.id);
2922
2944
  }
@@ -2945,7 +2967,7 @@ window.__ModuleLoader__.load({
2945
2967
  className: "dsham_settingsRow",
2946
2968
  children: [(0, react_jsx_runtime.jsxs)("div", { className: "dsham_settingsContent", children: [(0, react_jsx_runtime.jsx)("div", { className: "dsham_settingsTitle", children: displayTitle(session, t) }), (0, react_jsx_runtime.jsx)("div", { className: "dsham_settingsMeta", children: archiveTimeLabel(session.updatedAt, t) })] }), (0, react_jsx_runtime.jsxs)("div", {
2947
2969
  className: "dsham_settingsActions",
2948
- children: [(0, react_jsx_runtime.jsx)("button", { type: "button", className: "dsham_settingsAction", disabled: busy, onClick: () => onUnarchive(session.id), children: t("menu.unarchive") }), (0, react_jsx_runtime.jsx)("button", { type: "button", className: "dsham_settingsDelete", disabled: busy, "aria-label": t("menu.deleteSession"), onClick: () => setDeleteTarget({ kind: "session", session }), children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconTrashOutline16, {}) })]
2970
+ children: [(0, react_jsx_runtime.jsx)("button", { type: "button", className: "dsham_settingsAction", disabled: busy || unarchivingSessionIds.has(session.id), onClick: () => onUnarchive(session.id), children: t("menu.unarchive") }), (0, react_jsx_runtime.jsx)("button", { type: "button", className: "dsham_settingsDelete", disabled: busy || unarchivingSessionIds.has(session.id), "aria-label": t("menu.deleteSession"), onClick: () => setDeleteTarget({ kind: "session", session }), children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconTrashOutline16, {}) })]
2949
2971
  })]
2950
2972
  }, session.id))
2951
2973
  })]
@@ -3014,9 +3036,9 @@ window.__ModuleLoader__.load({
3014
3036
  "archives.timestamp": "{date},{time}",
3015
3037
  "archives.restoreAll": "全部恢复",
3016
3038
  "archives.restoreProject": "恢复该项目的全部聊天",
3017
- "archives.restoreUngrouped": "恢复未分组的全部聊天",
3039
+ "archives.restoreUngrouped": "全部恢复",
3018
3040
  "archives.deleteProject": "删除该项目的全部聊天",
3019
- "archives.deleteUngrouped": "删除未分组的全部聊天",
3041
+ "archives.deleteUngrouped": "全部删除",
3020
3042
  "archives.projectActions": "项目“{name}”的归档操作",
3021
3043
  "archives.ungroupedActions": "未分组聊天的归档操作",
3022
3044
  "archives.restoreSuccess": "已恢复 {n} 个已归档聊天。",
@@ -3033,7 +3055,9 @@ window.__ModuleLoader__.load({
3033
3055
  "archives.deleteUngroupedConfirm": "删除未分组的全部聊天",
3034
3056
  "archives.deleteBatchPending": "正在删除已归档聊天…",
3035
3057
  "archives.deleteSuccess": "已删除 {n} 个已归档聊天。",
3036
- "archives.deletePartial": "已处理 {done} 个聊天,{failed} 个删除失败:{detail}",
3058
+ "archives.deleteSuccessWithSkipped": "已删除 {deleted} 个已归档聊天;另清理 {skipped} 个已不存在会话的归档记录。",
3059
+ "archives.deleteSkipped": "已清理 {n} 个已不存在会话的归档记录。",
3060
+ "archives.deletePartial": "已删除 {deleted} 个聊天,清理 {skipped} 个已不存在会话的归档记录;{failed} 个删除失败:{detail}",
3037
3061
  "archives.unarchiveUnknown": "会话已不存在,无法取消归档。",
3038
3062
  "archives.unarchiveFailed": "取消归档失败:{detail}",
3039
3063
  "archives.archiveUnknown": "会话已不存在,无法归档。",
@@ -3128,9 +3152,9 @@ window.__ModuleLoader__.load({
3128
3152
  "archives.timestamp": "{date}, {time}",
3129
3153
  "archives.restoreAll": "Restore all",
3130
3154
  "archives.restoreProject": "Restore all chats in this project",
3131
- "archives.restoreUngrouped": "Restore all ungrouped chats",
3155
+ "archives.restoreUngrouped": "Restore all",
3132
3156
  "archives.deleteProject": "Delete all chats in this project",
3133
- "archives.deleteUngrouped": "Delete all ungrouped chats",
3157
+ "archives.deleteUngrouped": "Delete all",
3134
3158
  "archives.projectActions": "Archive actions for project {name}",
3135
3159
  "archives.ungroupedActions": "Archive actions for ungrouped chats",
3136
3160
  "archives.restoreSuccess": "Restored {n} archived chats.",
@@ -3147,7 +3171,9 @@ window.__ModuleLoader__.load({
3147
3171
  "archives.deleteUngroupedConfirm": "Delete all ungrouped chats",
3148
3172
  "archives.deleteBatchPending": "Deleting archived chats…",
3149
3173
  "archives.deleteSuccess": "Deleted {n} archived chats.",
3150
- "archives.deletePartial": "Processed {done} chats; {failed} could not be deleted: {detail}",
3174
+ "archives.deleteSuccessWithSkipped": "Deleted {deleted} archived chats and cleared {skipped} stale archive entries for sessions that no longer exist.",
3175
+ "archives.deleteSkipped": "Cleared {n} stale archive entries for sessions that no longer exist.",
3176
+ "archives.deletePartial": "Deleted {deleted} chats and cleared {skipped} stale archive entries; {failed} could not be deleted: {detail}",
3151
3177
  "archives.unarchiveUnknown": "This session no longer exists, so it cannot be unarchived.",
3152
3178
  "archives.unarchiveFailed": "Could not unarchive the session: {detail}",
3153
3179
  "archives.archiveUnknown": "This session no longer exists, so it cannot be archived.",
@@ -3374,7 +3400,7 @@ window.__ModuleLoader__.load({
3374
3400
  }
3375
3401
  //#endregion
3376
3402
  /** Pure derivation surface for dsh-archive-manager self-tests (no-op for the runtime). */
3377
- exports.__test = {
3403
+ exports.__test = {
3378
3404
  displayTitle,
3379
3405
  sessionVisible,
3380
3406
  isUnknownSessionError,
@@ -3384,6 +3410,7 @@ window.__ModuleLoader__.load({
3384
3410
  deriveArchivedGroups,
3385
3411
  sortArchivedGroups,
3386
3412
  deriveArchivedBatchIds,
3413
+ archivedDeleteFeedback,
3387
3414
  createWorkspaceViewStore,
3388
3415
  groupByWorkspace,
3389
3416
  byRecency,
package/lib/workspace.js CHANGED
@@ -15,8 +15,9 @@ import { trackTombstone } from "./tombstone.js";
15
15
  * 删除投影缓存,级联删除 SUBAGENT 子会话并清理 spill;最后才删除转录
16
16
  * 目录。fork 分支虽有 `parentSession`,但属于独立用户会话,不参与级联。
17
17
  *
18
- * 所有可失败的持久化清理都先于不可逆的物理删除。此前任一步失败时,
19
- * 转录仍保留,可重试删除;物理删除失败时记录残留路径供人工排查。
18
+ * 父会话的可失败记账清理先于其转录目录删除;父目录删除失败时保留头部
19
+ * 索引,允许再次调用同一入口完成删除。SUBAGENT 级联跨多个转录目录,无法
20
+ * 组成事务:子会话可能已先删除,重试会跳过它们并继续处理仍保留的父会话。
20
21
  * 物理删除成功后必须遗忘父类内存索引(headers / sessionPaths /
21
22
  * invalidSessionPaths)并打上删除墓碑:父类 sessionKnown 以 headers.has
22
23
  * 短路,indexHeaders 只增不减,stale list() 否则会把已删 id 救活,
@@ -322,7 +323,14 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
322
323
  deletedSessionIds.push(sessionId);
323
324
  } catch (error) {
324
325
  if (error instanceof ArchiveUnknownSessionError) {
325
- skippedSessionIds.push(sessionId);
326
+ // 转录已消失时仍需清完可达的持久痕迹;只有全部完成才算
327
+ // skipped,否则保留归档标记并作为 failure 暴露,允许重试。
328
+ try {
329
+ await this.cleanupUnknownArchivedSession(sessionId);
330
+ skippedSessionIds.push(sessionId);
331
+ } catch (cleanupError) {
332
+ failures.push({ sessionId, message: String(cleanupError) });
333
+ }
326
334
  continue;
327
335
  }
328
336
  failures.push({ sessionId, message: String(error) });
@@ -331,6 +339,31 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
331
339
  return { requestedSessionIds, deletedSessionIds, skippedSessionIds, failures };
332
340
  });
333
341
  }
342
+ /**
343
+ * 清理已无转录的陈旧归档项。缓存墓碑只在清除在途写入期间短暂持有:
344
+ * workspace 没有可记录的旧 header 身份,永久保留它会挡住未来的冷复用。
345
+ * 归档标记最后清除,前序可失败步骤出错时批量入口仍能再次命中。
346
+ */
347
+ async cleanupUnknownArchivedSession(sessionId) {
348
+ const projCache = this.ctx.get("sessionProjectionCache");
349
+ await projCache?.whenIdle?.();
350
+ if (projCache !== void 0) {
351
+ await projCache.delete(sessionId);
352
+ // 等待删除期间可能已进入的写回观察到墓碑并完成补删,再允许
353
+ // 同 id 的未来新生命周期写入缓存。
354
+ await projCache.whenIdle?.();
355
+ projCache.clearTombstone?.(sessionId);
356
+ }
357
+ await this.cleanSpill(sessionId);
358
+ await this.removeFromWorkspaceAccounts(sessionId);
359
+ const state = this.requireState();
360
+ if (state.archivedSessionIds.includes(sessionId)) {
361
+ await this.setState({
362
+ ...state,
363
+ archivedSessionIds: state.archivedSessionIds.filter((id) => id !== sessionId)
364
+ });
365
+ }
366
+ }
334
367
  /** 以归档集合顺序解析批量目标,避免依赖浏览器尚未加载完整的摘要投影。 */
335
368
  archivedSessionIdsForTarget(target) {
336
369
  target = archivedBatchTargetSchema.parse(target);
@@ -381,7 +414,8 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
381
414
  // dispose 的写后落盘必须先于缓存行删除完成,
382
415
  // 否则该行会在删除之后被写回(复活)。
383
416
  await projCache?.whenIdle?.();
384
- // 物理删除不可逆:先完成所有可失败的持久化清理,失败时仍可重试。
417
+ // 先完成父会话的可失败记账清理;SUBAGENT 级联跨目录且非事务,可能
418
+ // 部分完成,但父转录失败时仍保留索引供重试。
385
419
  const state = this.requireState();
386
420
  if (state.archivedSessionIds.includes(sessionId)) {
387
421
  await this.setState({
@@ -403,12 +437,15 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
403
437
  * 从父类内存索引中遗忘已删除会话,并阻止后续 indexHeaders 把它加回。
404
438
  * 实时会话以同 id 重新出现时(自定义 id 复用)会撤掉墓碑。
405
439
  */
406
- clearTombstone(sessionId) {
407
- this.deletedSessionIds.delete(sessionId);
408
- this.deletedIdentities.delete(sessionId);
409
- const idx = this.deletedSessionOrder.indexOf(sessionId);
410
- if (idx !== -1) this.deletedSessionOrder.splice(idx, 1);
411
- }
440
+ clearTombstone(sessionId) {
441
+ this.deletedSessionIds.delete(sessionId);
442
+ this.deletedIdentities.delete(sessionId);
443
+ const idx = this.deletedSessionOrder.indexOf(sessionId);
444
+ if (idx !== -1) this.deletedSessionOrder.splice(idx, 1);
445
+ // workspace 与 projection-cache 共同描述同一删除生命周期;新生命周期
446
+ // 被接纳时必须同步撤销两处墓碑,否则缓存写入仍会永久被拦截。
447
+ this.ctx.get("sessionProjectionCache")?.clearTombstone?.(sessionId);
448
+ }
412
449
  forgetIndexedSession(sessionId) {
413
450
  for (const evicted of trackTombstone(this.deletedSessionIds, this.deletedSessionOrder, sessionId, this.deletedSessionTombstoneLimit)) this.deletedIdentities.delete(evicted);
414
451
  this.headers.delete(sessionId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@michengai/dsh-archive-manager",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
4
4
  "description": "NPM-installable DSH Web plugin for managing archived sessions.",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@11.21.0",