@michengai/dsh-archive-manager 0.1.10 → 0.1.12
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 +3 -21
- package/README.zh-CN.md +3 -21
- package/assets/branding/banner.txt +2 -11
- package/assets/branding/dsh-banner.png +0 -0
- package/lib/client.js +207 -229
- package/lib/projcache.js +74 -56
- package/lib/tombstone.js +32 -0
- package/lib/workspace.js +141 -113
- package/package.json +17 -1
package/README.md
CHANGED
|
@@ -1,24 +1,6 @@
|
|
|
1
|
-
<
|
|
2
|
-
<
|
|
3
|
-
|
|
4
|
-
<td valign="middle" align="center">
|
|
5
|
-
<img src="assets/icon.png" alt="DSH Archive Manager logo" width="168">
|
|
6
|
-
</td>
|
|
7
|
-
<td valign="middle">
|
|
8
|
-
<pre>
|
|
9
|
-
____ ____ _ _
|
|
10
|
-
| _ \/ ___|| | | |
|
|
11
|
-
| | | \___ \| |_| |
|
|
12
|
-
| |_| |___) | _ |
|
|
13
|
-
|____/|____/|_| |_|
|
|
14
|
-
ARCHIVE MANAGER
|
|
15
|
-
Safe archived session control
|
|
16
|
-
DeepSeek Harness plugin
|
|
17
|
-
</pre>
|
|
18
|
-
</td>
|
|
19
|
-
</tr>
|
|
20
|
-
</table>
|
|
21
|
-
</div>
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="assets/branding/dsh-banner.png" alt="DSH Archive Manager" width="100%">
|
|
3
|
+
</p>
|
|
22
4
|
|
|
23
5
|
<div align="center">
|
|
24
6
|
|
package/README.zh-CN.md
CHANGED
|
@@ -1,24 +1,6 @@
|
|
|
1
|
-
<
|
|
2
|
-
<
|
|
3
|
-
|
|
4
|
-
<td valign="middle" align="center">
|
|
5
|
-
<img src="assets/icon.png" alt="DSH Archive Manager 标识" width="168">
|
|
6
|
-
</td>
|
|
7
|
-
<td valign="middle">
|
|
8
|
-
<pre>
|
|
9
|
-
____ ____ _ _
|
|
10
|
-
| _ \/ ___|| | | |
|
|
11
|
-
| | | \___ \| |_| |
|
|
12
|
-
| |_| |___) | _ |
|
|
13
|
-
|____/|____/|_| |_|
|
|
14
|
-
ARCHIVE MANAGER
|
|
15
|
-
Safe archived session control
|
|
16
|
-
DeepSeek Harness plugin
|
|
17
|
-
</pre>
|
|
18
|
-
</td>
|
|
19
|
-
</tr>
|
|
20
|
-
</table>
|
|
21
|
-
</div>
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="assets/branding/dsh-banner.png" alt="DSH Archive Manager" width="100%">
|
|
3
|
+
</p>
|
|
22
4
|
|
|
23
5
|
<div align="center">
|
|
24
6
|
|
|
@@ -1,11 +1,2 @@
|
|
|
1
|
-
README
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
____ ____ _ _
|
|
5
|
-
| _ \/ ___|| | | |
|
|
6
|
-
| | | \___ \| |_| |
|
|
7
|
-
| |_| |___) | _ |
|
|
8
|
-
|____/|____/|_| |_|
|
|
9
|
-
ARCHIVE MANAGER
|
|
10
|
-
Safe archived session control
|
|
11
|
-
DeepSeek Harness plugin
|
|
1
|
+
README 顶部使用 assets/branding/dsh-banner.png 全宽展示。
|
|
2
|
+
仓库标识仍保留 assets/icon.png,供插件图标和其它入口复用。
|
|
Binary file
|
package/lib/client.js
CHANGED
|
@@ -199,66 +199,6 @@ window.__ModuleLoader__.load({
|
|
|
199
199
|
function sessionVisible(session, current, archived, showArchived) {
|
|
200
200
|
return session.origin !== "subagent" && (!archived.has(session.id) || showArchived === true) && (!session.blank || session.id === current);
|
|
201
201
|
}
|
|
202
|
-
/**
|
|
203
|
-
* 单个删除按钮的目标:先子代理(含多层),再本会话。fork 分支不是 subagent,不会被收进来。
|
|
204
|
-
*/
|
|
205
|
-
function collectSessionAndDescendantIds(sessionId, byId) {
|
|
206
|
-
if (typeof sessionId !== "string" || sessionId.length === 0) return [];
|
|
207
|
-
const catalog = byId ?? {};
|
|
208
|
-
const childrenByParent = /* @__PURE__ */ new Map();
|
|
209
|
-
for (const session of Object.values(catalog)) {
|
|
210
|
-
if (session === void 0 || session.origin !== "subagent" || typeof session.id !== "string" || session.id.length === 0) continue;
|
|
211
|
-
const parentId = session.parentId;
|
|
212
|
-
if (typeof parentId !== "string") continue;
|
|
213
|
-
const list = childrenByParent.get(parentId) ?? [];
|
|
214
|
-
list.push(session.id);
|
|
215
|
-
childrenByParent.set(parentId, list);
|
|
216
|
-
}
|
|
217
|
-
const ordered = [];
|
|
218
|
-
const seen = /* @__PURE__ */ new Set();
|
|
219
|
-
const visit = (id) => {
|
|
220
|
-
for (const childId of childrenByParent.get(id) ?? []) {
|
|
221
|
-
if (seen.has(childId)) continue;
|
|
222
|
-
seen.add(childId);
|
|
223
|
-
visit(childId);
|
|
224
|
-
ordered.push(childId);
|
|
225
|
-
}
|
|
226
|
-
};
|
|
227
|
-
visit(sessionId);
|
|
228
|
-
if (!seen.has(sessionId)) ordered.push(sessionId);
|
|
229
|
-
return ordered;
|
|
230
|
-
}
|
|
231
|
-
/**
|
|
232
|
-
* 仅供 __test 使用:展开已归档 id 及其仍在目录中的 subagent 子会话,先子后父。运行时删除只提交根会话或归档集合。
|
|
233
|
-
*/
|
|
234
|
-
function collectArchivedDeleteAllIds(archivedSessionIds, byId) {
|
|
235
|
-
const catalog = byId ?? {};
|
|
236
|
-
const ids = [];
|
|
237
|
-
const seenArchived = /* @__PURE__ */ new Set();
|
|
238
|
-
for (const id of archivedSessionIds ?? []) {
|
|
239
|
-
if (typeof id !== "string" || id.length === 0 || seenArchived.has(id)) continue;
|
|
240
|
-
seenArchived.add(id);
|
|
241
|
-
ids.push(id);
|
|
242
|
-
}
|
|
243
|
-
const result = [];
|
|
244
|
-
const seen = /* @__PURE__ */ new Set();
|
|
245
|
-
const addTree = (id) => {
|
|
246
|
-
for (const item of collectSessionAndDescendantIds(id, catalog)) {
|
|
247
|
-
if (seen.has(item)) continue;
|
|
248
|
-
seen.add(item);
|
|
249
|
-
result.push(item);
|
|
250
|
-
}
|
|
251
|
-
};
|
|
252
|
-
for (const id of ids) {
|
|
253
|
-
if (catalog[id]?.origin === "subagent") continue;
|
|
254
|
-
addTree(id);
|
|
255
|
-
}
|
|
256
|
-
for (const id of ids) {
|
|
257
|
-
if (seen.has(id)) continue;
|
|
258
|
-
addTree(id);
|
|
259
|
-
}
|
|
260
|
-
return result;
|
|
261
|
-
}
|
|
262
202
|
/** 识别删除路径上“会话已不存在”的稳定标记;保留旧文案作兜底。 */
|
|
263
203
|
function isUnknownSessionError(reason) {
|
|
264
204
|
const message = reason instanceof Error ? reason.message : String(reason);
|
|
@@ -1741,7 +1681,7 @@ window.__ModuleLoader__.load({
|
|
|
1741
1681
|
(0, react.useEffect)(() => {
|
|
1742
1682
|
if (list.phase !== "ready") return;
|
|
1743
1683
|
const previousOrder = sessionOrderByAccount[FLAT_SESSION_ORDER_KEY];
|
|
1744
|
-
const previousUpdatedAt = sessionUpdatedAtByAccount[
|
|
1684
|
+
const previousUpdatedAt = sessionUpdatedAtByAccount[FLAT_SESSION_ORDER_KEY] ?? {};
|
|
1745
1685
|
const switchedToUpdated = previousOrderBy.current !== "updated" && orderBy === "updated";
|
|
1746
1686
|
previousOrderBy.current = orderBy;
|
|
1747
1687
|
const next = nextSessionOrderAccount({
|
|
@@ -2564,138 +2504,187 @@ window.__ModuleLoader__.load({
|
|
|
2564
2504
|
tag.textContent = ARCHIVE_SETTINGS_LAYOUT_OVERRIDE;
|
|
2565
2505
|
document.head.appendChild(tag);
|
|
2566
2506
|
}
|
|
2567
|
-
/** 设置页项目筛选:自定义菜单,避免原生 select 弹出系统浅色下拉。 */
|
|
2568
|
-
function ArchiveProjectSelect({ id, value, options, onChange, "aria-label": ariaLabel }) {
|
|
2569
|
-
const [open, setOpen] = (0, react.useState)(false);
|
|
2570
|
-
const selectedIndex = Math.max(0, options.findIndex((option) => option.value === value));
|
|
2571
|
-
const [active, setActive] = (0, react.useState)(selectedIndex);
|
|
2572
|
-
const rootRef = (0, react.useRef)(null);
|
|
2573
|
-
const triggerRef = (0, react.useRef)(null);
|
|
2574
|
-
const listRef = (0, react.useRef)(null);
|
|
2575
|
-
const wasOpen = (0, react.useRef)(false);
|
|
2576
|
-
const selected = options[selectedIndex];
|
|
2577
|
-
(0, react.useEffect)(() => {
|
|
2578
|
-
if (!open) return;
|
|
2579
|
-
setActive(selectedIndex);
|
|
2580
|
-
const onPointerDown = (event) => {
|
|
2581
|
-
const target = event.target;
|
|
2582
|
-
if (target instanceof Node && rootRef.current?.contains(target) === true) return;
|
|
2583
|
-
setOpen(false);
|
|
2584
|
-
};
|
|
2585
|
-
document.addEventListener("pointerdown", onPointerDown);
|
|
2586
|
-
return () => document.removeEventListener("pointerdown", onPointerDown);
|
|
2587
|
-
}, [open, selectedIndex]);
|
|
2588
|
-
(0, react.useEffect)(() => {
|
|
2589
|
-
if (open) {
|
|
2590
|
-
listRef.current?.focus();
|
|
2591
|
-
wasOpen.current = true;
|
|
2592
|
-
return;
|
|
2593
|
-
}
|
|
2594
|
-
if (wasOpen.current) {
|
|
2595
|
-
triggerRef.current?.focus();
|
|
2596
|
-
wasOpen.current = false;
|
|
2597
|
-
}
|
|
2598
|
-
}, [open]);
|
|
2599
|
-
(0, react.useEffect)(() => {
|
|
2600
|
-
if (!open) return;
|
|
2601
|
-
document.getElementById(id + "-opt-" + String(active))?.scrollIntoView({ block: "nearest" });
|
|
2602
|
-
}, [active, open, id]);
|
|
2603
|
-
const choose = (next) => {
|
|
2604
|
-
onChange(next);
|
|
2605
|
-
setOpen(false);
|
|
2606
|
-
};
|
|
2607
|
-
const move = (next) => {
|
|
2608
|
-
if (options.length === 0) return;
|
|
2609
|
-
setActive(Math.min(options.length - 1, Math.max(0, next)));
|
|
2610
|
-
};
|
|
2611
|
-
return (0, react_jsx_runtime.jsxs)("div", {
|
|
2612
|
-
className: "dsham_settingsFilter",
|
|
2613
|
-
ref: rootRef,
|
|
2614
|
-
children: [(0, react_jsx_runtime.jsxs)("button", {
|
|
2615
|
-
id,
|
|
2616
|
-
ref: triggerRef,
|
|
2617
|
-
type: "button",
|
|
2618
|
-
className: "dsham_selectTrigger",
|
|
2619
|
-
"aria-label": ariaLabel,
|
|
2620
|
-
"aria-haspopup": "listbox",
|
|
2621
|
-
"aria-expanded": open,
|
|
2622
|
-
"aria-controls": id + "-list",
|
|
2623
|
-
onClick: () => setOpen((current) => !current),
|
|
2624
|
-
onKeyDown: (event) => {
|
|
2625
|
-
if (event.key === "ArrowDown" || event.key === "ArrowUp" || event.key === "Enter" || event.key === " ") {
|
|
2626
|
-
event.preventDefault();
|
|
2627
|
-
setOpen(true);
|
|
2628
|
-
}
|
|
2629
|
-
},
|
|
2630
|
-
children: [(0, react_jsx_runtime.jsx)("span", { className: "dsham_selectValue", children: selected === void 0 ? "" : selected.label }), (0, react_jsx_runtime.jsx)("svg", {
|
|
2631
|
-
className: "dsham_selectCaret",
|
|
2632
|
-
viewBox: "0 0 12 12",
|
|
2633
|
-
"aria-hidden": true,
|
|
2634
|
-
focusable: false,
|
|
2635
|
-
children: (0, react_jsx_runtime.jsx)("path", {
|
|
2636
|
-
d: "M2.5 4.5L6 8l3.5-3.5",
|
|
2637
|
-
fill: "none",
|
|
2638
|
-
stroke: "currentColor",
|
|
2639
|
-
strokeWidth: "1.5",
|
|
2640
|
-
strokeLinecap: "round",
|
|
2641
|
-
strokeLinejoin: "round"
|
|
2642
|
-
})
|
|
2643
|
-
})]
|
|
2644
|
-
}), open ? (0, react_jsx_runtime.jsx)("div", {
|
|
2645
|
-
id: id + "-list",
|
|
2646
|
-
ref: listRef,
|
|
2647
|
-
className: "dsham_selectMenu",
|
|
2648
|
-
role: "listbox",
|
|
2649
|
-
tabIndex: 0,
|
|
2650
|
-
"aria-activedescendant": id + "-opt-" + String(active),
|
|
2651
|
-
onKeyDown: (event) => {
|
|
2652
|
-
if (event.key === "ArrowDown") {
|
|
2653
|
-
event.preventDefault();
|
|
2654
|
-
move(active + 1);
|
|
2655
|
-
return;
|
|
2656
|
-
}
|
|
2657
|
-
if (event.key === "ArrowUp") {
|
|
2658
|
-
event.preventDefault();
|
|
2659
|
-
move(active - 1);
|
|
2660
|
-
return;
|
|
2661
|
-
}
|
|
2662
|
-
if (event.key === "Home") {
|
|
2663
|
-
event.preventDefault();
|
|
2664
|
-
move(0);
|
|
2665
|
-
return;
|
|
2666
|
-
}
|
|
2667
|
-
if (event.key === "End") {
|
|
2668
|
-
event.preventDefault();
|
|
2669
|
-
move(options.length - 1);
|
|
2670
|
-
return;
|
|
2671
|
-
}
|
|
2672
|
-
if (event.key === "Enter" || event.key === " ") {
|
|
2673
|
-
event.preventDefault();
|
|
2674
|
-
const option = options[active];
|
|
2675
|
-
if (option !== void 0) choose(option.value);
|
|
2676
|
-
return;
|
|
2677
|
-
}
|
|
2678
|
-
if (event.key === "Escape" || event.key === "Tab") {
|
|
2679
|
-
event.preventDefault();
|
|
2680
|
-
event.stopPropagation();
|
|
2681
|
-
if (typeof event.stopImmediatePropagation === "function") event.stopImmediatePropagation();
|
|
2682
|
-
setOpen(false);
|
|
2683
|
-
}
|
|
2684
|
-
},
|
|
2685
|
-
children: options.map((option, index) => (0, react_jsx_runtime.jsx)("button", {
|
|
2686
|
-
id: id + "-opt-" + String(index),
|
|
2687
|
-
type: "button",
|
|
2688
|
-
role: "option",
|
|
2689
|
-
className: "dsham_selectOption",
|
|
2690
|
-
"aria-selected": option.value === value,
|
|
2691
|
-
"data-active": index === active,
|
|
2692
|
-
onMouseEnter: () => setActive(index),
|
|
2693
|
-
onClick: () => choose(option.value),
|
|
2694
|
-
children: option.label
|
|
2695
|
-
}, option.value === "all" ? "all" : option.value))
|
|
2696
|
-
}) : null]
|
|
2697
|
-
});
|
|
2698
|
-
}
|
|
2507
|
+
/** 设置页项目筛选:自定义菜单,避免原生 select 弹出系统浅色下拉。 */
|
|
2508
|
+
function ArchiveProjectSelect({ id, value, options, onChange, "aria-label": ariaLabel }) {
|
|
2509
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
2510
|
+
const selectedIndex = Math.max(0, options.findIndex((option) => option.value === value));
|
|
2511
|
+
const [active, setActive] = (0, react.useState)(selectedIndex);
|
|
2512
|
+
const rootRef = (0, react.useRef)(null);
|
|
2513
|
+
const triggerRef = (0, react.useRef)(null);
|
|
2514
|
+
const listRef = (0, react.useRef)(null);
|
|
2515
|
+
const wasOpen = (0, react.useRef)(false);
|
|
2516
|
+
const selected = options[selectedIndex];
|
|
2517
|
+
(0, react.useEffect)(() => {
|
|
2518
|
+
if (!open) return;
|
|
2519
|
+
setActive(selectedIndex);
|
|
2520
|
+
const onPointerDown = (event) => {
|
|
2521
|
+
const target = event.target;
|
|
2522
|
+
if (target instanceof Node && rootRef.current?.contains(target) === true) return;
|
|
2523
|
+
setOpen(false);
|
|
2524
|
+
};
|
|
2525
|
+
document.addEventListener("pointerdown", onPointerDown);
|
|
2526
|
+
return () => document.removeEventListener("pointerdown", onPointerDown);
|
|
2527
|
+
}, [open, selectedIndex]);
|
|
2528
|
+
(0, react.useEffect)(() => {
|
|
2529
|
+
if (open) {
|
|
2530
|
+
listRef.current?.focus();
|
|
2531
|
+
wasOpen.current = true;
|
|
2532
|
+
return;
|
|
2533
|
+
}
|
|
2534
|
+
if (wasOpen.current) {
|
|
2535
|
+
triggerRef.current?.focus();
|
|
2536
|
+
wasOpen.current = false;
|
|
2537
|
+
}
|
|
2538
|
+
}, [open]);
|
|
2539
|
+
(0, react.useEffect)(() => {
|
|
2540
|
+
if (!open) return;
|
|
2541
|
+
document.getElementById(id + "-opt-" + String(active))?.scrollIntoView({ block: "nearest" });
|
|
2542
|
+
}, [active, open, id]);
|
|
2543
|
+
const choose = (next) => {
|
|
2544
|
+
onChange(next);
|
|
2545
|
+
setOpen(false);
|
|
2546
|
+
};
|
|
2547
|
+
const move = (next) => {
|
|
2548
|
+
if (options.length === 0) return;
|
|
2549
|
+
setActive(Math.min(options.length - 1, Math.max(0, next)));
|
|
2550
|
+
};
|
|
2551
|
+
return (0, react_jsx_runtime.jsxs)("div", {
|
|
2552
|
+
className: "dsham_settingsFilter",
|
|
2553
|
+
ref: rootRef,
|
|
2554
|
+
children: [(0, react_jsx_runtime.jsxs)("button", {
|
|
2555
|
+
id,
|
|
2556
|
+
ref: triggerRef,
|
|
2557
|
+
type: "button",
|
|
2558
|
+
className: "dsham_selectTrigger",
|
|
2559
|
+
"aria-label": ariaLabel,
|
|
2560
|
+
"aria-haspopup": "listbox",
|
|
2561
|
+
"aria-expanded": open,
|
|
2562
|
+
"aria-controls": id + "-list",
|
|
2563
|
+
onClick: () => setOpen((current) => !current),
|
|
2564
|
+
onKeyDown: (event) => {
|
|
2565
|
+
if (event.key === "ArrowDown" || event.key === "ArrowUp" || event.key === "Enter" || event.key === " ") {
|
|
2566
|
+
event.preventDefault();
|
|
2567
|
+
setOpen(true);
|
|
2568
|
+
}
|
|
2569
|
+
},
|
|
2570
|
+
children: [(0, react_jsx_runtime.jsx)("span", { className: "dsham_selectValue", children: selected === void 0 ? "" : selected.label }), (0, react_jsx_runtime.jsx)("svg", {
|
|
2571
|
+
className: "dsham_selectCaret",
|
|
2572
|
+
viewBox: "0 0 12 12",
|
|
2573
|
+
"aria-hidden": true,
|
|
2574
|
+
focusable: false,
|
|
2575
|
+
children: (0, react_jsx_runtime.jsx)("path", {
|
|
2576
|
+
d: "M2.5 4.5L6 8l3.5-3.5",
|
|
2577
|
+
fill: "none",
|
|
2578
|
+
stroke: "currentColor",
|
|
2579
|
+
strokeWidth: "1.5",
|
|
2580
|
+
strokeLinecap: "round",
|
|
2581
|
+
strokeLinejoin: "round"
|
|
2582
|
+
})
|
|
2583
|
+
})]
|
|
2584
|
+
}), open ? (0, react_jsx_runtime.jsx)("div", {
|
|
2585
|
+
id: id + "-list",
|
|
2586
|
+
ref: listRef,
|
|
2587
|
+
className: "dsham_selectMenu",
|
|
2588
|
+
role: "listbox",
|
|
2589
|
+
tabIndex: 0,
|
|
2590
|
+
"aria-activedescendant": id + "-opt-" + String(active),
|
|
2591
|
+
onKeyDown: (event) => {
|
|
2592
|
+
if (event.key === "ArrowDown") {
|
|
2593
|
+
event.preventDefault();
|
|
2594
|
+
move(active + 1);
|
|
2595
|
+
return;
|
|
2596
|
+
}
|
|
2597
|
+
if (event.key === "ArrowUp") {
|
|
2598
|
+
event.preventDefault();
|
|
2599
|
+
move(active - 1);
|
|
2600
|
+
return;
|
|
2601
|
+
}
|
|
2602
|
+
if (event.key === "Home") {
|
|
2603
|
+
event.preventDefault();
|
|
2604
|
+
move(0);
|
|
2605
|
+
return;
|
|
2606
|
+
}
|
|
2607
|
+
if (event.key === "End") {
|
|
2608
|
+
event.preventDefault();
|
|
2609
|
+
move(options.length - 1);
|
|
2610
|
+
return;
|
|
2611
|
+
}
|
|
2612
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
2613
|
+
event.preventDefault();
|
|
2614
|
+
const option = options[active];
|
|
2615
|
+
if (option !== void 0) choose(option.value);
|
|
2616
|
+
return;
|
|
2617
|
+
}
|
|
2618
|
+
if (event.key === "Escape" || event.key === "Tab") {
|
|
2619
|
+
event.preventDefault();
|
|
2620
|
+
event.stopPropagation();
|
|
2621
|
+
if (typeof event.stopImmediatePropagation === "function") event.stopImmediatePropagation();
|
|
2622
|
+
setOpen(false);
|
|
2623
|
+
}
|
|
2624
|
+
},
|
|
2625
|
+
children: options.map((option, index) => (0, react_jsx_runtime.jsx)("button", {
|
|
2626
|
+
id: id + "-opt-" + String(index),
|
|
2627
|
+
type: "button",
|
|
2628
|
+
role: "option",
|
|
2629
|
+
className: "dsham_selectOption",
|
|
2630
|
+
"aria-selected": option.value === value,
|
|
2631
|
+
"data-active": index === active,
|
|
2632
|
+
onMouseEnter: () => setActive(index),
|
|
2633
|
+
onClick: () => choose(option.value),
|
|
2634
|
+
children: option.label
|
|
2635
|
+
}, option.value === "all" ? "all" : option.value))
|
|
2636
|
+
}) : null]
|
|
2637
|
+
});
|
|
2638
|
+
}
|
|
2639
|
+
/** 设置页“未分组”桶的稳定 key(workspaceId 均为非空 uuid,不会冲突)。 */
|
|
2640
|
+
const ARCHIVE_UNGROUPED_KEY = "__ungrouped__";
|
|
2641
|
+
/**
|
|
2642
|
+
* 设置页归档分组派生:按工作区分组,组 key 用 workspaceId(上游允许
|
|
2643
|
+
* 不同路径的工作区同名,title 不能作 React key / 筛选 value),未归入
|
|
2644
|
+
* 任何工作区的归档会话进“未分组”桶;隐藏 subagent(其删除由服务端级联)。
|
|
2645
|
+
* @param byId - 会话摘要表(缺失摘要的归档会话不进列表)。
|
|
2646
|
+
* @param items - 工作区列表(Host 顺序,含唯一 workspaceId)。
|
|
2647
|
+
* @param archivedSessionIds - 注册表全局归档集合。
|
|
2648
|
+
* @param ungroupedLabel - “未分组”显示文案。
|
|
2649
|
+
* @returns 分组数组(仅含有会话的组),每项含 key / title / sessions。
|
|
2650
|
+
*/
|
|
2651
|
+
function deriveArchivedGroups(byId, items, archivedSessionIds, ungroupedLabel) {
|
|
2652
|
+
const byWorkspace = items.map((workspace) => ({
|
|
2653
|
+
key: workspace.workspaceId,
|
|
2654
|
+
title: workspace.title,
|
|
2655
|
+
ids: new Set(workspace.sessionIds),
|
|
2656
|
+
sessions: []
|
|
2657
|
+
}));
|
|
2658
|
+
const ungrouped = [];
|
|
2659
|
+
for (const id of archivedSessionIds) {
|
|
2660
|
+
const session = byId[id];
|
|
2661
|
+
if (session === void 0 || session.origin === "subagent") continue;
|
|
2662
|
+
const group = byWorkspace.find((workspace) => workspace.ids.has(id));
|
|
2663
|
+
(group === void 0 ? ungrouped : group.sessions).push(session);
|
|
2664
|
+
}
|
|
2665
|
+
const result = byWorkspace.filter((group) => group.sessions.length > 0);
|
|
2666
|
+
if (ungrouped.length > 0) result.push({ key: ARCHIVE_UNGROUPED_KEY, title: ungroupedLabel, sessions: ungrouped });
|
|
2667
|
+
return result;
|
|
2668
|
+
}
|
|
2669
|
+
/**
|
|
2670
|
+
* “全部删除”的提交集合:直接取归档全集,仅排除摘要已知为 subagent 的
|
|
2671
|
+
* id(其删除由服务端级联)。不依赖摘要是否已加载,投影未就绪的会话
|
|
2672
|
+
* 不会被静默漏删。
|
|
2673
|
+
* @param archivedSessionIds - 注册表全局归档集合。
|
|
2674
|
+
* @param byId - 会话摘要表(可能不完整)。
|
|
2675
|
+
* @returns 去重后的待删除 id 列表。
|
|
2676
|
+
*/
|
|
2677
|
+
function deriveArchivedDeleteAllIds(archivedSessionIds, byId) {
|
|
2678
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2679
|
+
const ids = [];
|
|
2680
|
+
for (const id of archivedSessionIds ?? []) {
|
|
2681
|
+
if (typeof id !== "string" || id.length === 0 || seen.has(id)) continue;
|
|
2682
|
+
seen.add(id);
|
|
2683
|
+
if (byId?.[id]?.origin === "subagent") continue;
|
|
2684
|
+
ids.push(id);
|
|
2685
|
+
}
|
|
2686
|
+
return ids;
|
|
2687
|
+
}
|
|
2699
2688
|
/** 管理设置页中的归档会话,数据直接订阅 DSH 的会话与工作区投影。 */
|
|
2700
2689
|
function ArchivedSessionsSection({ sessionStore, workspaceStore, unarchiveSession, deleteSession, t }) {
|
|
2701
2690
|
const sessions = (0, react.useSyncExternalStore)(sessionStore.subscribe, sessionStore.getSnapshot);
|
|
@@ -2705,33 +2694,22 @@ window.__ModuleLoader__.load({
|
|
|
2705
2694
|
const [error, setError] = (0, react.useState)(null);
|
|
2706
2695
|
const [query, setQuery] = (0, react.useState)("");
|
|
2707
2696
|
const [project, setProject] = (0, react.useState)("all");
|
|
2708
|
-
const groups = (0, react.useMemo)(() =>
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
const ungrouped = [];
|
|
2717
|
-
for (const id of workspaceState.archivedSessionIds) {
|
|
2718
|
-
const session = sessions.byId[id];
|
|
2719
|
-
if (session === void 0 || session.origin === "subagent" || !archived.has(id)) continue;
|
|
2720
|
-
const group = byWorkspace.find((workspace) => workspace.ids.has(id));
|
|
2721
|
-
(group === void 0 ? ungrouped : group.sessions).push(session);
|
|
2722
|
-
}
|
|
2723
|
-
const result = byWorkspace.filter((group) => group.sessions.length > 0);
|
|
2724
|
-
if (ungrouped.length > 0) result.push({ title: t("group.ungrouped"), sessions: ungrouped });
|
|
2725
|
-
return result;
|
|
2726
|
-
}, [sessions, workspaceState, t]);
|
|
2697
|
+
const groups = (0, react.useMemo)(() => deriveArchivedGroups(sessions.byId, workspaceState.items, workspaceState.archivedSessionIds, t("group.ungrouped")), [sessions.byId, workspaceState, t]);
|
|
2698
|
+
(0, react.useEffect)(() => {
|
|
2699
|
+
// 选中的分组消失(如最后一个归档会话被取消归档)时回退到
|
|
2700
|
+
// “所有项目”,避免筛选值停留在失效 key 上把列表过滤为空。
|
|
2701
|
+
if (project === "all") return;
|
|
2702
|
+
if (groups.some((group) => group.key === project)) return;
|
|
2703
|
+
setProject("all");
|
|
2704
|
+
}, [groups, project]);
|
|
2727
2705
|
const filteredGroups = (0, react.useMemo)(() => {
|
|
2728
2706
|
const normalizedQuery = query.trim().toLocaleLowerCase();
|
|
2729
|
-
return groups.filter((group) => project === "all" || project === group.
|
|
2707
|
+
return groups.filter((group) => project === "all" || project === group.key).map((group) => ({
|
|
2730
2708
|
...group,
|
|
2731
2709
|
sessions: group.sessions.filter((session) => normalizedQuery === "" || displayTitle(session, t).toLocaleLowerCase().includes(normalizedQuery))
|
|
2732
2710
|
})).filter((group) => group.sessions.length > 0);
|
|
2733
2711
|
}, [groups, project, query, t]);
|
|
2734
|
-
const deleteAllSessionIds = (0, react.useMemo)(() =>
|
|
2712
|
+
const deleteAllSessionIds = (0, react.useMemo)(() => deriveArchivedDeleteAllIds(workspaceState.archivedSessionIds, sessions.byId), [workspaceState.archivedSessionIds, sessions.byId]);
|
|
2735
2713
|
const onUnarchive = (sessionId) => {
|
|
2736
2714
|
setError(null);
|
|
2737
2715
|
unarchiveSession(sessionId).catch((reason) => {
|
|
@@ -2776,7 +2754,7 @@ window.__ModuleLoader__.load({
|
|
|
2776
2754
|
return (0, react_jsx_runtime.jsxs)("section", {
|
|
2777
2755
|
className: "dsham_settings",
|
|
2778
2756
|
"aria-label": t("archives.title"),
|
|
2779
|
-
children: [(0, react_jsx_runtime.jsx)("style", { children: ARCHIVE_SETTINGS_CSS }), (0, react_jsx_runtime.jsxs)("header", { className: "dsham_settingsHeader", children: [(0, react_jsx_runtime.jsxs)("div", { children: [(0, react_jsx_runtime.jsx)("h2", { children: t("archives.title") }), (0, react_jsx_runtime.jsx)("p", { className: "dsham_settingsIntro", children: t("archives.description") })] }), (0, react_jsx_runtime.jsxs)("button", { type: "button", className: "dsham_settingsDanger", disabled: busy || deleteAllSessionIds.length === 0, onClick: () => setDeleteTarget({ all: true }), children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconTrashOutline16, {}), t("archives.deleteAll")] })] }), (0, react_jsx_runtime.jsxs)("div", { className: "dsham_settingsToolbar", children: [(0, react_jsx_runtime.jsxs)("label", { className: "dsham_settingsSearch", children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconSearchOutline16, {}), (0, react_jsx_runtime.jsx)("input", { type: "search", value: query, onChange: (event) => setQuery(event.target.value), placeholder: t("archives.searchPlaceholder"), "aria-label": t("archives.searchPlaceholder") })] }), (0, react_jsx_runtime.jsx)(ArchiveProjectSelect, { id: "dsham-project-filter", value: project, options: [{ value: "all", label: t("archives.allProjects") }, ...groups.map((group) => ({ value: group.
|
|
2757
|
+
children: [(0, react_jsx_runtime.jsx)("style", { children: ARCHIVE_SETTINGS_CSS }), (0, react_jsx_runtime.jsxs)("header", { className: "dsham_settingsHeader", children: [(0, react_jsx_runtime.jsxs)("div", { children: [(0, react_jsx_runtime.jsx)("h2", { children: t("archives.title") }), (0, react_jsx_runtime.jsx)("p", { className: "dsham_settingsIntro", children: t("archives.description") })] }), (0, react_jsx_runtime.jsxs)("button", { type: "button", className: "dsham_settingsDanger", disabled: busy || deleteAllSessionIds.length === 0, onClick: () => setDeleteTarget({ all: true }), children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconTrashOutline16, {}), t("archives.deleteAll")] })] }), (0, react_jsx_runtime.jsxs)("div", { className: "dsham_settingsToolbar", children: [(0, react_jsx_runtime.jsxs)("label", { className: "dsham_settingsSearch", children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconSearchOutline16, {}), (0, react_jsx_runtime.jsx)("input", { type: "search", value: query, onChange: (event) => setQuery(event.target.value), placeholder: t("archives.searchPlaceholder"), "aria-label": t("archives.searchPlaceholder") })] }), (0, react_jsx_runtime.jsx)(ArchiveProjectSelect, { id: "dsham-project-filter", value: project, options: [{ value: "all", label: t("archives.allProjects") }, ...groups.map((group) => ({ value: group.key, label: group.title }))], onChange: setProject, "aria-label": t("archives.projectFilter") })] }), groups.length === 0 ? (0, react_jsx_runtime.jsx)("div", { className: "dsham_settingsEmpty", children: t("archives.empty") }) : filteredGroups.length === 0 ? (0, react_jsx_runtime.jsx)("div", { className: "dsham_settingsEmpty", children: t("archives.emptyFiltered") }) : filteredGroups.map((group) => (0, react_jsx_runtime.jsxs)("section", {
|
|
2780
2758
|
className: "dsham_settingsGroup",
|
|
2781
2759
|
children: [(0, react_jsx_runtime.jsxs)("div", { className: "dsham_settingsGroupHeading", children: [(0, react_jsx_runtime.jsxs)("h3", { className: "dsham_settingsGroupTitle", children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFolderOpenOutline16, {}), group.title] }), (0, react_jsx_runtime.jsx)("span", { className: "dsham_settingsCount", children: t("archives.sessionCount", { n: group.sessions.length }) })] }), (0, react_jsx_runtime.jsx)("div", {
|
|
2782
2760
|
className: "dsham_settingsList",
|
|
@@ -2788,7 +2766,7 @@ window.__ModuleLoader__.load({
|
|
|
2788
2766
|
})]
|
|
2789
2767
|
}, session.id))
|
|
2790
2768
|
})]
|
|
2791
|
-
}, group.
|
|
2769
|
+
}, group.key)), error !== null && (0, react_jsx_runtime.jsx)("div", { className: "dsham_settingsError", role: "alert", children: error }), (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
|
|
2792
2770
|
open: deleteTarget !== null,
|
|
2793
2771
|
onClose: closeDelete,
|
|
2794
2772
|
closeLabel: t("close"),
|
|
@@ -2849,16 +2827,16 @@ window.__ModuleLoader__.load({
|
|
|
2849
2827
|
"archives.deleteAll": "全部删除",
|
|
2850
2828
|
"archives.deleteAllTitle": "删除全部已归档聊天",
|
|
2851
2829
|
"archives.deleteAllDesc": "将永久删除全部 {n} 个已归档聊天及其子代理(含正在运行的)和记录,此操作不可恢复。",
|
|
2852
|
-
"archives.deleteAllPending": "正在删除已归档聊天…",
|
|
2853
|
-
"archives.unarchiveUnknown": "会话已不存在,无法取消归档。",
|
|
2854
|
-
"archives.unarchiveFailed": "取消归档失败:{detail}",
|
|
2830
|
+
"archives.deleteAllPending": "正在删除已归档聊天…",
|
|
2831
|
+
"archives.unarchiveUnknown": "会话已不存在,无法取消归档。",
|
|
2832
|
+
"archives.unarchiveFailed": "取消归档失败:{detail}",
|
|
2855
2833
|
"archives.archiveUnknown": "会话已不存在,无法归档。",
|
|
2856
2834
|
"archives.archiveFailed": "归档失败:{detail}",
|
|
2857
2835
|
"archives.forkFailed": "分叉会话失败:{detail}",
|
|
2858
2836
|
"deleteSession.title": "删除会话",
|
|
2859
2837
|
"deleteSession.desc": "将永久删除会话“{name}”及其子代理(含正在运行的)和全部记录(对话内容、统计、缓存),此操作不可恢复。",
|
|
2860
|
-
"deleteSession.pending": "正在删除会话…",
|
|
2861
|
-
"deleteSession.unknown": "会话已不存在或已被删除。",
|
|
2838
|
+
"deleteSession.pending": "正在删除会话…",
|
|
2839
|
+
"deleteSession.unknown": "会话已不存在或已被删除。",
|
|
2862
2840
|
"deleteSession.failed": "删除会话失败:{detail}",
|
|
2863
2841
|
"picker.loading": "正在加载工作区…",
|
|
2864
2842
|
"conflict.named": "已存在名为“{name}”的工作区。",
|
|
@@ -2906,7 +2884,7 @@ window.__ModuleLoader__.load({
|
|
|
2906
2884
|
"section.sessions": "Sessions",
|
|
2907
2885
|
"viewOptions.label": "View options",
|
|
2908
2886
|
"groupBy.label": "Group by",
|
|
2909
|
-
"groupBy.workspace": "
|
|
2887
|
+
"groupBy.workspace": "Workspace",
|
|
2910
2888
|
"groupBy.flat": "In one list",
|
|
2911
2889
|
"orderBy.label": "Order by",
|
|
2912
2890
|
"orderBy.manual": "Manual",
|
|
@@ -2941,16 +2919,16 @@ window.__ModuleLoader__.load({
|
|
|
2941
2919
|
"archives.deleteAll": "Delete all",
|
|
2942
2920
|
"archives.deleteAllTitle": "Delete all archived chats",
|
|
2943
2921
|
"archives.deleteAllDesc": "This permanently deletes all {n} archived chats, their child agents (including any that are still running), and their records. This cannot be undone.",
|
|
2944
|
-
"archives.deleteAllPending": "Deleting archived chats…",
|
|
2945
|
-
"archives.unarchiveUnknown": "This session no longer exists, so it cannot be unarchived.",
|
|
2946
|
-
"archives.unarchiveFailed": "Could not unarchive the session: {detail}",
|
|
2922
|
+
"archives.deleteAllPending": "Deleting archived chats…",
|
|
2923
|
+
"archives.unarchiveUnknown": "This session no longer exists, so it cannot be unarchived.",
|
|
2924
|
+
"archives.unarchiveFailed": "Could not unarchive the session: {detail}",
|
|
2947
2925
|
"archives.archiveUnknown": "This session no longer exists, so it cannot be archived.",
|
|
2948
2926
|
"archives.archiveFailed": "Could not archive the session: {detail}",
|
|
2949
2927
|
"archives.forkFailed": "Could not fork the session: {detail}",
|
|
2950
2928
|
"deleteSession.title": "Delete session",
|
|
2951
2929
|
"deleteSession.desc": "This permanently deletes session “{name}”, its child agents (including any that are still running), and all of its records (conversation, stats, cache). This cannot be undone.",
|
|
2952
|
-
"deleteSession.pending": "Deleting session…",
|
|
2953
|
-
"deleteSession.unknown": "This session no longer exists or was already deleted.",
|
|
2930
|
+
"deleteSession.pending": "Deleting session…",
|
|
2931
|
+
"deleteSession.unknown": "This session no longer exists or was already deleted.",
|
|
2954
2932
|
"deleteSession.failed": "Could not delete the session: {detail}",
|
|
2955
2933
|
"picker.loading": "Loading workspaces…",
|
|
2956
2934
|
"conflict.named": "A workspace named “{name}” already exists.",
|
|
@@ -3147,12 +3125,12 @@ window.__ModuleLoader__.load({
|
|
|
3147
3125
|
exports.__test = {
|
|
3148
3126
|
displayTitle,
|
|
3149
3127
|
sessionVisible,
|
|
3150
|
-
collectArchivedDeleteAllIds,
|
|
3151
|
-
collectSessionAndDescendantIds,
|
|
3152
3128
|
isUnknownSessionError,
|
|
3153
3129
|
deriveGroups,
|
|
3154
3130
|
deriveFlat,
|
|
3155
3131
|
deriveSearchResults,
|
|
3132
|
+
deriveArchivedGroups,
|
|
3133
|
+
deriveArchivedDeleteAllIds,
|
|
3156
3134
|
createWorkspaceViewStore,
|
|
3157
3135
|
groupByWorkspace,
|
|
3158
3136
|
byRecency,
|
package/lib/projcache.js
CHANGED
|
@@ -1,74 +1,92 @@
|
|
|
1
|
-
import { SessionProjectionCache } from "@deepseek-ai/dsh-session-projection-cache";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* `deleteSession
|
|
10
|
-
*
|
|
11
|
-
* - `delete(id)`
|
|
12
|
-
*
|
|
13
|
-
* - `whenIdle()`
|
|
14
|
-
* -
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* `@deepseek-ai/dsh-session-projection-cache`
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*/
|
|
1
|
+
import { SessionProjectionCache } from "@deepseek-ai/dsh-session-projection-cache";
|
|
2
|
+
import { trackTombstone } from "./tombstone.js";
|
|
3
|
+
//#region lib/types/index.js
|
|
4
|
+
/**
|
|
5
|
+
* dsh-archive-manager projcache 半边。
|
|
6
|
+
*
|
|
7
|
+
* `ArchiveProjectionCache` 继承上游 `SessionProjectionCache`
|
|
8
|
+
* (服务名 `sessionProjectionCache`,同域、同 fail-soft 写路径),
|
|
9
|
+
* 为归档管理的 `deleteSession` 增加三处守护:
|
|
10
|
+
*
|
|
11
|
+
* - `delete(id)` - 永久移除一个会话的缓存投影行
|
|
12
|
+
* (`session_projcache` 域上的 `table.delete`),写入前先登记墓碑。
|
|
13
|
+
* - `whenIdle()` - 全部在途公开写入落定后 resolve。
|
|
14
|
+
* - 墓碑挡住已删除会话的写入:迟到的写入要么被直接拦截,要么在
|
|
15
|
+
* 落定后补删自己的残留行,不会复活已删除的缓存条目。守护覆盖
|
|
16
|
+
* `write()`(dispose 写后路径)与 `putSoft()`(`coldSnapshot` 的
|
|
17
|
+
* 冷读写回路径),两条落盘路径都纳入 `whenIdle` 跟踪。
|
|
18
|
+
*
|
|
19
|
+
* 默认导出是 Service 子类(与上游 `@deepseek-ai/dsh-session-projection-cache`
|
|
20
|
+
* 包同形),profile 补丁可直接替换 `session-projection-cache` 服务行,
|
|
21
|
+
* 无需其他接线改动。
|
|
22
|
+
*/
|
|
23
23
|
var ArchiveProjectionCache = class extends SessionProjectionCache {
|
|
24
|
-
/**
|
|
24
|
+
/** 已永久删除的会话:其投影缓存行不再允许写入。 */
|
|
25
25
|
deletedSessionIds = /* @__PURE__ */ new Set();
|
|
26
|
+
/** 墓碑插入顺序,用于在上限处淘汰最旧项。 */
|
|
26
27
|
deletedSessionOrder = [];
|
|
27
28
|
deletedSessionTombstoneLimit = 4096;
|
|
28
|
-
/**
|
|
29
|
+
/** 在途写入的队尾(只含已落定的 promise)。 */
|
|
29
30
|
writeTail = Promise.resolve();
|
|
30
|
-
constructor(ctx, config) {
|
|
31
|
-
super(ctx, config);
|
|
32
|
-
}
|
|
31
|
+
constructor(ctx, config) {
|
|
32
|
+
super(ctx, config);
|
|
33
|
+
}
|
|
33
34
|
/** 跟踪公开写入路径,避免依赖上游私有 `flushSoft` 的实现细节。 */
|
|
34
35
|
write(session) {
|
|
35
36
|
const task = this.writeCore(session);
|
|
36
37
|
this.writeTail = Promise.allSettled([this.writeTail, task]).then(() => void 0);
|
|
37
38
|
return task;
|
|
38
39
|
}
|
|
39
|
-
/**
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
async writeCore(session) {
|
|
40
|
+
/**
|
|
41
|
+
* 墓碑正确性依赖:super.write(session) 一次整体写入,返回后不再有后续异步落盘。
|
|
42
|
+
* 若上游改成多阶段异步,(C) 补删会漏掉后续写入,deletedSessionIds 挡不住复活。
|
|
43
|
+
*/
|
|
44
|
+
async writeCore(session) {
|
|
44
45
|
if (this.deletedSessionIds.has(session.id)) return;
|
|
45
46
|
await super.write(session);
|
|
46
47
|
if (this.deletedSessionIds.has(session.id)) await this.requireTable().delete(session.id);
|
|
47
48
|
}
|
|
48
|
-
/**
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
this.
|
|
65
|
-
}
|
|
66
|
-
while (this.deletedSessionOrder.length > this.deletedSessionTombstoneLimit) {
|
|
67
|
-
const oldest = this.deletedSessionOrder.shift();
|
|
68
|
-
if (oldest !== void 0 && oldest !== id) this.deletedSessionIds.delete(oldest);
|
|
49
|
+
/**
|
|
50
|
+
* 冷读写回路径(上游 `coldSnapshot` 经 `putSoft` 落盘)与 `write` 同守:
|
|
51
|
+
* 已删除直接放弃;删除落在写回进行中则在其落定后补删残留行。
|
|
52
|
+
* 同样纳入 `whenIdle` 跟踪。
|
|
53
|
+
*/
|
|
54
|
+
async putSoft(id, identity, rows, what) {
|
|
55
|
+
if (this.deletedSessionIds.has(id)) return;
|
|
56
|
+
const task = this.putSoftCore(id, identity, rows, what);
|
|
57
|
+
this.writeTail = Promise.allSettled([this.writeTail, task]).then(() => void 0);
|
|
58
|
+
return task;
|
|
59
|
+
}
|
|
60
|
+
async putSoftCore(id, identity, rows, what) {
|
|
61
|
+
await super.putSoft(id, identity, rows, what);
|
|
62
|
+
try {
|
|
63
|
+
if (this.deletedSessionIds.has(id)) await this.requireTable().delete(id);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
this.ctx.logger.warn(`archive-manager projcache: cold-read write-back cleanup for "${id}" failed: ${String(error)}`);
|
|
69
66
|
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* 全部被跟踪的在途写入落定后 resolve(含失败)。空闲缓存上调用立即返回。
|
|
70
|
+
* @returns 跟踪写入落定后的 resolution。
|
|
71
|
+
*/
|
|
72
|
+
whenIdle() {
|
|
73
|
+
return this.writeTail;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* 永久移除一个会话的缓存投影行。
|
|
77
|
+
* @param id - 要删除缓存行的会话。
|
|
78
|
+
* @returns 行删除完成后的 resolution。
|
|
79
|
+
*/
|
|
80
|
+
async delete(id) {
|
|
81
|
+
trackTombstone(this.deletedSessionIds, this.deletedSessionOrder, id, this.deletedSessionTombstoneLimit);
|
|
70
82
|
await this.requireTable().delete(id);
|
|
71
83
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
84
|
+
/** 撤销墓碑(供测试与同 id 新生命周期复用路径使用)。 */
|
|
85
|
+
clearTombstone(id) {
|
|
86
|
+
this.deletedSessionIds.delete(id);
|
|
87
|
+
const idx = this.deletedSessionOrder.indexOf(id);
|
|
88
|
+
if (idx !== -1) this.deletedSessionOrder.splice(idx, 1);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
//#endregion
|
|
92
|
+
export { ArchiveProjectionCache, ArchiveProjectionCache as default };
|
package/lib/tombstone.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
//#region lib/types/tombstone.js
|
|
2
|
+
/**
|
|
3
|
+
* 通用墓碑簿记:登记一个已删除 id,并按 FIFO 在上限处淘汰最旧项。
|
|
4
|
+
* 工作区注册表(lib/workspace.js)与投影缓存(lib/projcache.js)共用
|
|
5
|
+
* 同一份语义,避免两处拷贝的淘汰策略分叉。
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* 登记已删除 id 并执行上限淘汰。
|
|
9
|
+
* @param {Set<string>} ids - 墓碑集合。
|
|
10
|
+
* @param {string[]} order - 插入顺序队列(FIFO 淘汰依据)。
|
|
11
|
+
* @param {string} id - 本次登记的 id。
|
|
12
|
+
* @param {number} limit - 墓碑上限。
|
|
13
|
+
* @returns {string[]} 被淘汰出集合的旧 id 列表,调用方据此清理附属簿记。
|
|
14
|
+
*/
|
|
15
|
+
function trackTombstone(ids, order, id, limit) {
|
|
16
|
+
if (!ids.has(id)) {
|
|
17
|
+
ids.add(id);
|
|
18
|
+
order.push(id);
|
|
19
|
+
}
|
|
20
|
+
const evicted = [];
|
|
21
|
+
while (order.length > limit) {
|
|
22
|
+
const oldest = order.shift();
|
|
23
|
+
// id 自身被挤出队列时保留在集合中:宁多挡不错放。
|
|
24
|
+
if (oldest !== void 0 && oldest !== id) {
|
|
25
|
+
ids.delete(oldest);
|
|
26
|
+
evicted.push(oldest);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return evicted;
|
|
30
|
+
}
|
|
31
|
+
//#endregion
|
|
32
|
+
export { trackTombstone };
|
package/lib/workspace.js
CHANGED
|
@@ -5,27 +5,30 @@ import {
|
|
|
5
5
|
} from "@deepseek-ai/dsh-workspace";
|
|
6
6
|
import { bindTypertRemote, Remote } from "@deepseek-ai/dsh-typert-protocol";
|
|
7
7
|
import { sessionDir } from "@deepseek-ai/dsh-spill-local";
|
|
8
|
+
import { trackTombstone } from "./tombstone.js";
|
|
8
9
|
//#region lib/types/index.js
|
|
9
|
-
/**
|
|
10
|
-
* dsh-archive-manager 宿主侧归档会话管理。
|
|
11
|
-
*
|
|
12
|
-
* `deleteSession(sessionId)` 的顺序即语义:校验已知会话;实时会话先
|
|
13
|
-
* flush 再 detach;等待投影缓存写入完成;移除归档标记和工作区记账;
|
|
14
|
-
* 删除投影缓存,级联删除 SUBAGENT 子会话并清理 spill;最后才删除转录
|
|
15
|
-
* 目录。fork 分支虽有 `parentSession`,但属于独立用户会话,不参与级联。
|
|
16
|
-
*
|
|
17
|
-
* 所有可失败的持久化清理都先于不可逆的物理删除。此前任一步失败时,
|
|
18
|
-
* 转录仍保留,可重试删除;物理删除失败时记录残留路径供人工排查。
|
|
19
|
-
* 物理删除成功后必须遗忘父类内存索引(headers / sessionPaths /
|
|
20
|
-
* invalidSessionPaths)并打上删除墓碑:父类 sessionKnown 以 headers.has
|
|
21
|
-
* 短路,indexHeaders 只增不减,stale list() 否则会把已删 id 救活,
|
|
22
|
-
* 进而被 archiveSession 重新写回 archivedSessionIds。
|
|
23
|
-
*
|
|
24
|
-
|
|
10
|
+
/**
|
|
11
|
+
* dsh-archive-manager 宿主侧归档会话管理。
|
|
12
|
+
*
|
|
13
|
+
* `deleteSession(sessionId)` 的顺序即语义:校验已知会话;实时会话先
|
|
14
|
+
* flush 再 detach;等待投影缓存写入完成;移除归档标记和工作区记账;
|
|
15
|
+
* 删除投影缓存,级联删除 SUBAGENT 子会话并清理 spill;最后才删除转录
|
|
16
|
+
* 目录。fork 分支虽有 `parentSession`,但属于独立用户会话,不参与级联。
|
|
17
|
+
*
|
|
18
|
+
* 所有可失败的持久化清理都先于不可逆的物理删除。此前任一步失败时,
|
|
19
|
+
* 转录仍保留,可重试删除;物理删除失败时记录残留路径供人工排查。
|
|
20
|
+
* 物理删除成功后必须遗忘父类内存索引(headers / sessionPaths /
|
|
21
|
+
* invalidSessionPaths)并打上删除墓碑:父类 sessionKnown 以 headers.has
|
|
22
|
+
* 短路,indexHeaders 只增不减,stale list() 否则会把已删 id 救活,
|
|
23
|
+
* 进而被 archiveSession 重新写回 archivedSessionIds。
|
|
24
|
+
* 墓碑同时记录被删生命周期的日志身份(createdAt/cwd):其他进程以同 id
|
|
25
|
+
* 重建并落盘新会话时(冷复用),sessionKnown 的探针以身份差异区分新旧
|
|
26
|
+
* 生命周期并撤掉墓碑,避免已重建的会话在本进程永久 UNKNOWN_SESSION。
|
|
27
|
+
* `unarchiveSession` 与 `deleteSession` 通过 Typert Remote 暴露给浏览器,并注册到 typert.local,避免生产环境只靠 SRC 扫描时 404。
|
|
28
|
+
*/
|
|
25
29
|
function markRemoteMethod(instance, method) {
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
// context whose addInitializer runs immediately with `this` = instance.
|
|
30
|
+
// 模拟 TS 装饰器管线 `@Remote(method)`:`Remote` 返回标准方法装饰器,
|
|
31
|
+
// 这里构造一个 addInitializer 立即以 `this` = instance 执行的装饰器上下文。
|
|
29
32
|
const context = {
|
|
30
33
|
private: false,
|
|
31
34
|
static: false,
|
|
@@ -36,17 +39,24 @@ function markRemoteMethod(instance, method) {
|
|
|
36
39
|
};
|
|
37
40
|
Remote(method)(void 0, context);
|
|
38
41
|
}
|
|
39
|
-
function unknownSessionMessage(sessionId) {
|
|
40
|
-
return `unknown session "${sessionId}" (UNKNOWN_SESSION)`;
|
|
41
|
-
}
|
|
42
|
-
var ArchiveUnknownSessionError = class extends Error {
|
|
43
|
-
sessionId;
|
|
44
|
-
constructor(sessionId) {
|
|
45
|
-
super(unknownSessionMessage(sessionId));
|
|
46
|
-
this.sessionId = sessionId;
|
|
47
|
-
this.name = "ArchiveUnknownSessionError";
|
|
48
|
-
}
|
|
49
|
-
}
|
|
42
|
+
function unknownSessionMessage(sessionId) {
|
|
43
|
+
return `unknown session "${sessionId}" (UNKNOWN_SESSION)`;
|
|
44
|
+
}
|
|
45
|
+
var ArchiveUnknownSessionError = class extends Error {
|
|
46
|
+
sessionId;
|
|
47
|
+
constructor(sessionId) {
|
|
48
|
+
super(unknownSessionMessage(sessionId));
|
|
49
|
+
this.sessionId = sessionId;
|
|
50
|
+
this.name = "ArchiveUnknownSessionError";
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** 头部投影到“日志身份”字段(与投影缓存的 identity 语义一致)。cwd 缺失统一归一为 null,避免一侧带键一侧不带键时的比较歧义。 */
|
|
54
|
+
function headerIdentity(header) {
|
|
55
|
+
return {
|
|
56
|
+
createdAt: header.createdAt,
|
|
57
|
+
cwd: header.cwd ?? null
|
|
58
|
+
};
|
|
59
|
+
}
|
|
50
60
|
const sessionIdSchema = {
|
|
51
61
|
parse(value) {
|
|
52
62
|
if (typeof value !== "string" || value.length === 0) throw new TypeError(`sessionId must be a non-empty string, got ${String(value)}`);
|
|
@@ -141,6 +151,8 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
|
|
|
141
151
|
deletedSessionOrder = [];
|
|
142
152
|
/** 墓碑上限:足够挡住 stale list(),又避免长驻进程无限增长。 */
|
|
143
153
|
deletedSessionTombstoneLimit = 4096;
|
|
154
|
+
/** 被删生命周期的日志身份(createdAt/cwd):冷复用探针区分“同 id 新会话”与 stale list() 的依据。 */
|
|
155
|
+
deletedIdentities = /* @__PURE__ */ new Map();
|
|
144
156
|
constructor(ctx) {
|
|
145
157
|
super(ctx);
|
|
146
158
|
this.typertRemote = bindTypertRemote(this, this.name);
|
|
@@ -149,13 +161,11 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
|
|
|
149
161
|
registerHostRemote(this.ctx);
|
|
150
162
|
}
|
|
151
163
|
/**
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
* @param sessionId - the session to unarchive.
|
|
158
|
-
* @returns the full updated archive set.
|
|
164
|
+
* 把一个会话移出注册表全局归档集合,恢复其正常可见性(其记账位从未
|
|
165
|
+
* 移动,会话在原工作区位置重新出现)。幂等:未归档的已知会话直接返回
|
|
166
|
+
* 当前集合不写入;未知会话与 `archiveSession` 一样抛错。
|
|
167
|
+
* @param sessionId - 要取消归档的会话。
|
|
168
|
+
* @returns 更新后的完整归档集合。
|
|
159
169
|
*/
|
|
160
170
|
async unarchiveSession(sessionId) {
|
|
161
171
|
return this.enqueueOperation(async () => {
|
|
@@ -171,51 +181,52 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
|
|
|
171
181
|
});
|
|
172
182
|
}
|
|
173
183
|
/**
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
* @param sessionId -
|
|
177
|
-
* @returns `{ deleted: true }
|
|
178
|
-
* @throws {@link ArchiveUnknownSessionError}
|
|
184
|
+
* 永久删除一个会话及其全部痕迹(转录目录、工作区记账、归档标记、
|
|
185
|
+
* 投影缓存行)。
|
|
186
|
+
* @param sessionId - 要删除的会话。
|
|
187
|
+
* @returns 持久化完成后的 `{ deleted: true }`。
|
|
188
|
+
* @throws {@link ArchiveUnknownSessionError} 会话未知时抛出。
|
|
179
189
|
*/
|
|
180
190
|
async deleteSession(sessionId) {
|
|
181
191
|
return this.enqueueOperation(() => this.deleteSessionCore(sessionId));
|
|
182
192
|
}
|
|
183
|
-
/**
|
|
184
|
-
* already holds the operation chain — it must never re-enqueue). */
|
|
193
|
+
/** 串行化后的删除主体(级联路径复用:它已持有操作链,绝不能再入队)。 */
|
|
185
194
|
async deleteSessionCore(sessionId) {
|
|
186
195
|
if (!await this.sessionKnown(sessionId)) throw new ArchiveUnknownSessionError(sessionId);
|
|
187
196
|
const sessions = this.ctx.get("sessions");
|
|
188
197
|
const live = sessions?.get(sessionId);
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
198
|
+
// 先记录被删生命周期的日志身份:目录删除后头部不可再读,
|
|
199
|
+
// 冷复用探针(sessionKnown 墓碑分支)靠它区分同 id 的新生命周期。
|
|
200
|
+
const deletedHeader = this.headers.get(sessionId) ?? live?.header;
|
|
201
|
+
if (live !== void 0) {
|
|
202
|
+
// 持久化屏障先行:不能有未落盘的转录写入与目录删除竞争
|
|
203
|
+
//(持久化后端按批关闭句柄,flush 过的会话不再持有打开的文件)。
|
|
193
204
|
await sessions.flush(live);
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
} else if (sessions !== void 0) await this.publishColdSessionRemoval(sessionId, sessions);
|
|
205
|
+
// 从存储分离;`session/disposed` 同步触发,驱动浏览器端的
|
|
206
|
+
// `host/session-removed` 帧并启动投影缓存的最终写后落盘。
|
|
207
|
+
const entry = sessions.liveEntryFor(live);
|
|
208
|
+
sessions.detachEntered(entry);
|
|
209
|
+
} else if (sessions !== void 0) await this.publishColdSessionRemoval(sessionId, sessions);
|
|
200
210
|
const projCache = this.ctx.get("sessionProjectionCache");
|
|
201
|
-
//
|
|
202
|
-
//
|
|
203
|
-
await projCache?.whenIdle?.();
|
|
204
|
-
// 物理删除不可逆:先完成所有可失败的持久化清理,失败时仍可重试。
|
|
205
|
-
const state = this.requireState();
|
|
211
|
+
// dispose 的写后落盘必须先于缓存行删除完成,
|
|
212
|
+
// 否则该行会在删除之后被写回(复活)。
|
|
213
|
+
await projCache?.whenIdle?.();
|
|
214
|
+
// 物理删除不可逆:先完成所有可失败的持久化清理,失败时仍可重试。
|
|
215
|
+
const state = this.requireState();
|
|
206
216
|
if (state.archivedSessionIds.includes(sessionId)) {
|
|
207
217
|
await this.setState({
|
|
208
218
|
...state,
|
|
209
219
|
archivedSessionIds: state.archivedSessionIds.filter((id) => id !== sessionId)
|
|
210
220
|
});
|
|
211
|
-
}
|
|
212
|
-
await this.removeFromWorkspaceAccounts(sessionId);
|
|
213
|
-
if (projCache !== void 0) await projCache.delete(sessionId);
|
|
214
|
-
await this.deleteDescendants(sessionId);
|
|
221
|
+
}
|
|
222
|
+
await this.removeFromWorkspaceAccounts(sessionId);
|
|
223
|
+
if (projCache !== void 0) await projCache.delete(sessionId);
|
|
224
|
+
await this.deleteDescendants(sessionId);
|
|
215
225
|
await this.cleanSpill(sessionId);
|
|
216
226
|
await this.removeTranscriptDirectory(sessionId);
|
|
217
227
|
// 物理删除已成功:此时再清父类索引。失败时保留索引,便于重试。
|
|
218
228
|
this.forgetIndexedSession(sessionId);
|
|
229
|
+
if (deletedHeader !== void 0) this.deletedIdentities.set(sessionId, headerIdentity(deletedHeader));
|
|
219
230
|
return { deleted: true };
|
|
220
231
|
}
|
|
221
232
|
/**
|
|
@@ -224,18 +235,12 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
|
|
|
224
235
|
*/
|
|
225
236
|
clearTombstone(sessionId) {
|
|
226
237
|
this.deletedSessionIds.delete(sessionId);
|
|
238
|
+
this.deletedIdentities.delete(sessionId);
|
|
227
239
|
const idx = this.deletedSessionOrder.indexOf(sessionId);
|
|
228
240
|
if (idx !== -1) this.deletedSessionOrder.splice(idx, 1);
|
|
229
241
|
}
|
|
230
242
|
forgetIndexedSession(sessionId) {
|
|
231
|
-
|
|
232
|
-
this.deletedSessionIds.add(sessionId);
|
|
233
|
-
this.deletedSessionOrder.push(sessionId);
|
|
234
|
-
}
|
|
235
|
-
while (this.deletedSessionOrder.length > this.deletedSessionTombstoneLimit) {
|
|
236
|
-
const oldest = this.deletedSessionOrder.shift();
|
|
237
|
-
if (oldest !== void 0 && oldest !== sessionId) this.deletedSessionIds.delete(oldest);
|
|
238
|
-
}
|
|
243
|
+
for (const evicted of trackTombstone(this.deletedSessionIds, this.deletedSessionOrder, sessionId, this.deletedSessionTombstoneLimit)) this.deletedIdentities.delete(evicted);
|
|
239
244
|
this.headers.delete(sessionId);
|
|
240
245
|
this.sessionPaths.delete(sessionId);
|
|
241
246
|
this.invalidSessionPaths.delete(sessionId);
|
|
@@ -249,56 +254,80 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
|
|
|
249
254
|
this.clearTombstone(id);
|
|
250
255
|
return true;
|
|
251
256
|
}
|
|
252
|
-
if (this.deletedSessionIds.has(id)) return
|
|
257
|
+
if (this.deletedSessionIds.has(id)) return this.coldReuseKnown(id);
|
|
253
258
|
return super.sessionKnown(id);
|
|
254
259
|
}
|
|
255
260
|
/**
|
|
261
|
+
* 墓碑分支的冷复用探针:其他进程以同 id 重建并落盘的新会话(日志身份
|
|
262
|
+
* 不同)撤墓碑放行并重新编入索引;stale list() 里同生命周期的旧头部
|
|
263
|
+
* 仍视为未知。身份不可考(删除时未取到头部)时保守维持未知。
|
|
264
|
+
*/
|
|
265
|
+
async coldReuseKnown(id) {
|
|
266
|
+
const deletedIdentity = this.deletedIdentities.get(id);
|
|
267
|
+
if (deletedIdentity === void 0) return false;
|
|
268
|
+
const persistence = this.ctx.get("sessionPersistence");
|
|
269
|
+
if (persistence === void 0 || typeof persistence.list !== "function") return false;
|
|
270
|
+
let header;
|
|
271
|
+
try {
|
|
272
|
+
header = (await persistence.list()).find((item) => item.id === id);
|
|
273
|
+
} catch (error) {
|
|
274
|
+
this.ctx.logger.warn(`archive-manager: cold-reuse probe for "${id}" failed: ${String(error)}`);
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
if (header === void 0) return false;
|
|
278
|
+
const listed = headerIdentity(header);
|
|
279
|
+
if (listed.createdAt === deletedIdentity.createdAt && listed.cwd === deletedIdentity.cwd) return false;
|
|
280
|
+
this.clearTombstone(id);
|
|
281
|
+
await this.indexHeader(header);
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
256
285
|
* 父类 indexHeaders 只增不减;跳过墓碑 id,避免 stale persistence.list()
|
|
257
286
|
* 把已删除会话重新编入 headers。
|
|
258
287
|
*/
|
|
259
288
|
async indexHeader(header) {
|
|
260
289
|
if (this.deletedSessionIds.has(header.id)) return;
|
|
261
290
|
return super.indexHeader(header);
|
|
262
|
-
}
|
|
263
|
-
/** 为未处于实时状态的持久化会话发布相同的移除事件。 */
|
|
264
|
-
async publishColdSessionRemoval(sessionId, sessions) {
|
|
265
|
-
const persistence = this.ctx.get("sessionPersistence");
|
|
266
|
-
if (persistence === void 0 || typeof persistence.prepare !== "function") return;
|
|
267
|
-
try {
|
|
268
|
-
const preparation = await persistence.prepare(sessionId);
|
|
269
|
-
const detach = sessions.enter(preparation.session);
|
|
270
|
-
try {
|
|
271
|
-
sessions.announce(preparation.session);
|
|
272
|
-
} finally {
|
|
273
|
-
detach();
|
|
274
|
-
preparation[Symbol.dispose]();
|
|
275
|
-
}
|
|
276
|
-
} catch (error) {
|
|
277
|
-
this.ctx.logger.warn(`archive-manager: could not publish removal for stored session "${sessionId}": ${String(error)}`);
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
/** 删除会话的转录目录;仅在所有记账清理完成后调用。 */
|
|
281
|
-
async removeTranscriptDirectory(sessionId) {
|
|
291
|
+
}
|
|
292
|
+
/** 为未处于实时状态的持久化会话发布相同的移除事件。 */
|
|
293
|
+
async publishColdSessionRemoval(sessionId, sessions) {
|
|
294
|
+
const persistence = this.ctx.get("sessionPersistence");
|
|
295
|
+
if (persistence === void 0 || typeof persistence.prepare !== "function") return;
|
|
296
|
+
try {
|
|
297
|
+
const preparation = await persistence.prepare(sessionId);
|
|
298
|
+
const detach = sessions.enter(preparation.session);
|
|
299
|
+
try {
|
|
300
|
+
sessions.announce(preparation.session);
|
|
301
|
+
} finally {
|
|
302
|
+
detach();
|
|
303
|
+
preparation[Symbol.dispose]();
|
|
304
|
+
}
|
|
305
|
+
} catch (error) {
|
|
306
|
+
this.ctx.logger.warn(`archive-manager: could not publish removal for stored session "${sessionId}": ${String(error)}`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
/** 删除会话的转录目录;仅在所有记账清理完成后调用。 */
|
|
310
|
+
async removeTranscriptDirectory(sessionId) {
|
|
282
311
|
const persistence = this.ctx.get("sessionPersistence");
|
|
283
312
|
if (persistence === void 0 || typeof persistence.locate !== "function") {
|
|
284
313
|
throw new Error(`cannot delete session "${sessionId}": the session persistence backend does not expose locate() to resolve its transcript directory`);
|
|
285
314
|
}
|
|
286
|
-
const header = await this.readSessionHeader(sessionId);
|
|
287
|
-
const location = persistence.locate(header);
|
|
288
|
-
if (location === void 0 || typeof location.path !== "string") {
|
|
289
|
-
throw new Error(`cannot delete session "${sessionId}": the session persistence backend could not resolve its transcript directory`);
|
|
290
|
-
}
|
|
291
|
-
const transcriptDir = dirname(location.path);
|
|
292
|
-
try {
|
|
293
|
-
await rm(transcriptDir, { recursive: true, force: true });
|
|
294
|
-
} catch (error) {
|
|
295
|
-
const message = `cannot delete session "${sessionId}": transcript directory "${transcriptDir}" remains after bookkeeping cleanup`;
|
|
296
|
-
const detail = `${message}: ${String(error)}`;
|
|
297
|
-
this.ctx.logger.warn(`archive-manager: ${detail}`);
|
|
298
|
-
throw new Error(detail, { cause: error });
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
/**
|
|
315
|
+
const header = await this.readSessionHeader(sessionId);
|
|
316
|
+
const location = persistence.locate(header);
|
|
317
|
+
if (location === void 0 || typeof location.path !== "string") {
|
|
318
|
+
throw new Error(`cannot delete session "${sessionId}": the session persistence backend could not resolve its transcript directory`);
|
|
319
|
+
}
|
|
320
|
+
const transcriptDir = dirname(location.path);
|
|
321
|
+
try {
|
|
322
|
+
await rm(transcriptDir, { recursive: true, force: true });
|
|
323
|
+
} catch (error) {
|
|
324
|
+
const message = `cannot delete session "${sessionId}": transcript directory "${transcriptDir}" remains after bookkeeping cleanup`;
|
|
325
|
+
const detail = `${message}: ${String(error)}`;
|
|
326
|
+
this.ctx.logger.warn(`archive-manager: ${detail}`);
|
|
327
|
+
throw new Error(detail, { cause: error });
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
/** 把 id 从每个工作区记录中移除,并刷新实体快照。 */
|
|
302
331
|
async removeFromWorkspaceAccounts(sessionId) {
|
|
303
332
|
const table = this.requireTable();
|
|
304
333
|
const state = this.requireState();
|
|
@@ -314,10 +343,9 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
|
|
|
314
343
|
if (entity !== void 0) entity.record = next;
|
|
315
344
|
}
|
|
316
345
|
}
|
|
317
|
-
/**
|
|
318
|
-
*
|
|
319
|
-
|
|
320
|
-
* independent user session that must never be cascade-deleted. */
|
|
346
|
+
/** 尽力而为的级联删除:删除 `sessionId` 的 SUBAGENT 子会话。
|
|
347
|
+
* 仅头部标记 `origin: "subagent"` 的会话参与:单凭 `parentSession` 有歧义
|
|
348
|
+
*(fork 分支也携带它),而 fork 分支是独立的用户会话,绝不能被级联删除。 */
|
|
321
349
|
async deleteDescendants(sessionId) {
|
|
322
350
|
try {
|
|
323
351
|
const descendants = [];
|
|
@@ -341,7 +369,7 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
|
|
|
341
369
|
this.ctx.logger.warn(`archive-manager: descendant enumeration for deleted session "${sessionId}" failed: ${String(error)}`);
|
|
342
370
|
}
|
|
343
371
|
}
|
|
344
|
-
/**
|
|
372
|
+
/** 尽力而为的 spill 清理:移除该会话作用域的 spill 目录。 */
|
|
345
373
|
async cleanSpill(sessionId) {
|
|
346
374
|
try {
|
|
347
375
|
const spill = this.ctx.get("spillStore");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@michengai/dsh-archive-manager",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.12",
|
|
4
4
|
"description": "NPM-installable DSH Web plugin for managing archived sessions.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@11.21.0",
|
|
@@ -29,6 +29,22 @@
|
|
|
29
29
|
"pack:check": "pnpm pack --dry-run",
|
|
30
30
|
"prepublishOnly": "pnpm build && pnpm test"
|
|
31
31
|
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
34
|
+
"@deepseek-ai/dsh-client-connection": ">=0.1.0-rc.5 <0.2.0",
|
|
35
|
+
"@deepseek-ai/dsh-client-locale": ">=0.1.0-rc.5 <0.2.0",
|
|
36
|
+
"@deepseek-ai/dsh-client-runtime": ">=0.1.0-rc.5 <0.2.0",
|
|
37
|
+
"@deepseek-ai/dsh-client-ui-conversation": ">=0.1.0-rc.5 <0.2.0",
|
|
38
|
+
"@deepseek-ai/dsh-client-ui-primitives": ">=0.1.0-rc.5 <0.2.0",
|
|
39
|
+
"@deepseek-ai/dsh-client-ui-sidebar": ">=0.1.0-rc.5 <0.2.0",
|
|
40
|
+
"@deepseek-ai/dsh-client-ui-workspace": ">=0.1.0-rc.5 <0.2.0",
|
|
41
|
+
"@deepseek-ai/dsh-session": ">=0.1.0-rc.5 <0.2.0",
|
|
42
|
+
"@deepseek-ai/dsh-session-projection-cache": ">=0.1.0-rc.5 <0.2.0",
|
|
43
|
+
"@deepseek-ai/dsh-spill-local": ">=0.1.0-rc.5 <0.2.0",
|
|
44
|
+
"@deepseek-ai/dsh-typert-protocol": ">=0.1.0-rc.5 <0.2.0",
|
|
45
|
+
"@deepseek-ai/dsh-typert-registry": ">=0.1.0-rc.5 <0.2.0",
|
|
46
|
+
"@deepseek-ai/dsh-workspace": ">=0.1.0-rc.5 <0.2.0"
|
|
47
|
+
},
|
|
32
48
|
"devDependencies": {
|
|
33
49
|
"@deepseek-ai/cordis": "4.0.1",
|
|
34
50
|
"@deepseek-ai/dsh-api-gateway": "0.1.0-rc.6",
|