dsh-session-manager 0.7.5 → 0.7.6
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 +11 -0
- package/README.md +6 -8
- package/README.zh.md +6 -8
- package/lib/client.js +44 -12
- package/lib/compat/projection-cache-delete.js +110 -0
- package/lib/compat/zstd-frames.js +78 -1
- package/lib/index.js +38 -4
- package/lib/session-files.js +6 -9
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## 0.7.6 — 2026-11-13
|
|
6
|
+
|
|
7
|
+
- Fix conditional React hooks in the session header and move dialog (#49).
|
|
8
|
+
- Open sessions from their titles in the management panel and remove the redundant Open button (#54).
|
|
9
|
+
- Display Migrate preset as an icon button with a hover description, accessible label, and active state, matching the other row actions.
|
|
10
|
+
- Use the shared DSH tooltip for session row priority badges instead of the browser's native title tooltip.
|
|
11
|
+
- Stream session headers from first Zstd frames larger than 1 MiB, whether compressed or decompressed, without buffering the entire frame output. Preserve the header-line size limit and validate the complete first frame and its checksum before accepting the header (#53).
|
|
12
|
+
- Delete projection checkpoints through the host storage queue for single, batch, and cascaded subagent deletion. Prevent delayed writes from recreating deleted lifecycle records, and remove matching entries from the retained legacy cache with locking and atomic replacement. Report cleanup failures without hiding successful session deletion (#50, #55).
|
|
13
|
+
- Add regression tests and an optional integration test using the exact DSH `0.2.2-rc.1` cache and storage packages.
|
|
14
|
+
- Manually verified with the official DSH Desktop `0.2.2-rc.1`.
|
|
15
|
+
|
|
5
16
|
## 0.7.5 — 2026-10-22
|
|
6
17
|
|
|
7
18
|
- **fix(security)**: Enforce centralized API authentication on all `/session-manager/api/*` routes using DSH official `ctx.connection.admit(req)`. Enforce strict Owner-only authorization (`peer.principal.kind === "owner"`), reject untrusted, unauthenticated, empty, or non-owner requests before body reading and database queries, and fail closed with HTTP 503 (`auth-unavailable`) if connection service is missing or unready, preventing unauthorized access across public tunnels, reverse proxies, and local networks (#43).
|
package/README.md
CHANGED
|
@@ -12,13 +12,14 @@ English | [中文](README.zh.md)
|
|
|
12
12
|
|
|
13
13
|
DeepSeek Harness session and workspace manager —— Workspaces: archive and visibility. Sessions: move, migrate presets, delete, favorite, review later, prioritize, tag, and annotate. Batch operations.
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
The Web UI and Desktop app share the same plugin Host/client functionality; environment-specific installation notes are documented below.
|
|
16
16
|
|
|
17
17
|
## 1 Features
|
|
18
18
|
|
|
19
19
|
### 1.1 Session lifecycle management
|
|
20
20
|
|
|
21
21
|
- **Delete** is irreversible and always asks for confirmation. Subagent sessions and transient blank placeholders (no persisted artifact) cannot be deleted.
|
|
22
|
+
- **Projection cache cleanup (DSH 0.2.2-rc.1)**: session deletion also removes its checkpoint and retained legacy cache entry, including cascaded subagents and batch deletion. Delayed writes from the deleted lifecycle are blocked. Cleanup failures are reported as warnings; they do not undo session deletion.
|
|
22
23
|
- **Archive / Unarchive** moves a session in and out of the active list without touching its disk content.
|
|
23
24
|
- **Move to workspace** keeps history, title, archive state and derived-session relationships intact, rewrites the session's `cwd` to the target workspace, synchronizes the runtime `workingDirectory` projection for active agents, and updates the live writer's header in place so any pending tool calls keep landing on the new path. When the session's `cwd` already equals the target workspace path but its membership was never recorded, only the missing membership is repaired: no `cwd` rewrite, no artifact rewrite or move, and no session restart.
|
|
24
25
|
- **Migrate Agent preset** rewrites the latest `agent-preset/selected` event (or the session header if no such event exists) so a session whose preset was renamed or removed can resume. Message history is never altered.
|
|
@@ -70,7 +71,7 @@ The right side of the title area exposes actions for the **current session**: **
|
|
|
70
71
|
|
|
71
72
|
### 2.2 Sessions and workspaces management entry and interface
|
|
72
73
|
|
|
73
|
-
Open **Sessions & Workspaces** from the bottom of DSH's sidebar to browse all sessions and workspaces
|
|
74
|
+
Open **Sessions & Workspaces** from the bottom of DSH's sidebar to browse all sessions and workspaces. Click a session title to open it; there is no separate Open button. Row actions, including **Migrate preset**, use icons with hover descriptions and accessible labels. The session tab supports switching workspaces, searching by title / ID / notes / tags, filtering, and sorting; the workspace tab supports searching, sorting, bulk actions, and visibility toggling.
|
|
74
75
|
|
|
75
76
|
### 2.3 Bulk management
|
|
76
77
|
|
|
@@ -142,12 +143,11 @@ After changing plugin dependencies, `package.json`, bundle configuration, or oth
|
|
|
142
143
|
|
|
143
144
|
## 5 Compatibility
|
|
144
145
|
|
|
145
|
-
The table below lists
|
|
146
|
+
The table below lists compatible version combinations between DeepSeek Harness (DSH) and `dsh-session-manager`.
|
|
146
147
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
| DeepSeek Harness (DSH) | dsh-session-manager | Verified Environment |
|
|
148
|
+
| DeepSeek Harness (DSH) | dsh-session-manager | Environment |
|
|
150
149
|
| :--- | :--- | :--- |
|
|
150
|
+
| **v0.2.2-rc.1** | `0.7.6` | Desktop |
|
|
151
151
|
| **v0.2.1-rc.2** | `0.7.5`, `0.7.4`, `0.7.3`, `0.7.2` | Desktop |
|
|
152
152
|
| **v0.2.0-rc.2** | `0.7.1`, `0.7.0`, `0.6.4`, `0.6.3`, `0.6.2`, `0.5.4` | Desktop |
|
|
153
153
|
| **v0.1.7-rc.2** | `0.5.3` | Desktop / Web UI |
|
|
@@ -159,8 +159,6 @@ The table below lists verified version combinations between DeepSeek Harness (DS
|
|
|
159
159
|
| **v0.1.2-rc.1** | `0.4.0` | Web UI |
|
|
160
160
|
| **v0.1.0-rc.7** | `0.1.2`, `0.1.1`, `0.1.0` | Web UI |
|
|
161
161
|
|
|
162
|
-
The version combinations above have been tested with either the official Web UI or Desktop app. Other version combinations may also work but have not been individually verified.
|
|
163
|
-
|
|
164
162
|
When using a standalone DSH CLI/runtime, Node.js 22.15+ (22.x) or 24+ is required for built-in Zstd support. The official Desktop app ships and manages its matching runtime separately.
|
|
165
163
|
|
|
166
164
|
This is a Cordis plugin and declares `cordis: ">=4.0.0-rc <5"` as its peer dependency.
|
package/README.zh.md
CHANGED
|
@@ -12,13 +12,14 @@
|
|
|
12
12
|
|
|
13
13
|
DSH 会话与工作区管理——工作区:归档与显示。会话:跨工作区移动、预设迁移、删除;收藏、待回看、优先级、标签、备注。批量操作。
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
Web UI 与 Desktop 共用本插件的 Host 与客户端功能,具体安装方式见下文。
|
|
16
16
|
|
|
17
17
|
## 1 功能
|
|
18
18
|
|
|
19
19
|
### 1.1 会话生命周期管理
|
|
20
20
|
|
|
21
21
|
- **删除**:不可逆操作,UI 始终要求二次确认。subagent 会话和尚未落盘的空白会话占位不能删除。
|
|
22
|
+
- **投影缓存清理(DSH 0.2.2-rc.1)**:删除会话时同时清理其 checkpoint 与旧版聚合缓存条目,覆盖批量删除和级联子智能体删除,并阻止已删生命周期的延迟写回。清理失败会返回警告,不会撤销已完成的会话删除。
|
|
22
23
|
- **归档 / 移出归档**:把会话移出或移回主列表,不删除磁盘内容。
|
|
23
24
|
- **移动至工作区**:跨工作区移动时保留历史、标题、归档状态和派生会话关系,同时把 `cwd` 重写为目标工作区,实时同步活动 Agent 的运行时 `workingDirectory` 投影,并就地更新 live writer 的 header,使挂起的工具调用继续落到新路径。若会话的 `cwd` 本来就等于目标工作区、只是归属未登记,则只补全归属记录,不改写 `cwd`、不重写或移动工件、不重启会话。
|
|
24
25
|
- **迁移 Agent 预设**:原预设被改名或删除导致会话无法恢复时,可修复该会话。迁移会就地重写最后一条 `agent-preset/selected` 事件(若从未记录则修改会话 header),不改动历史消息。
|
|
@@ -68,7 +69,7 @@ DSH 会话与工作区管理——工作区:归档与显示。会话:跨工
|
|
|
68
69
|
|
|
69
70
|
### 2.2 会话与工作区管理入口与界面
|
|
70
71
|
|
|
71
|
-
从 DSH 侧边栏底部进入
|
|
72
|
+
从 DSH 侧边栏底部进入 **会话与工作区**,可浏览全部会话与工作区。点击会话名称即可打开会话,不再提供独立的「打开」按钮。行操作(含 **迁移预设**)统一使用图标,并提供悬停提示与无障碍名称。会话管理页支持切换工作区、按标题 / ID / 备注 / 标签搜索、筛选与排序;工作区管理页支持搜索、排序、批量处理与显示开关切换。
|
|
72
73
|
|
|
73
74
|
### 2.3 批量管理入口
|
|
74
75
|
|
|
@@ -140,12 +141,11 @@ dsh plugin --profile web add <本仓库路径>
|
|
|
140
141
|
|
|
141
142
|
## 5 兼容性
|
|
142
143
|
|
|
143
|
-
下表列出 DeepSeek Harness(DSH)与 `dsh-session-manager`
|
|
144
|
+
下表列出 DeepSeek Harness(DSH)与 `dsh-session-manager` 的兼容版本组合。
|
|
144
145
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
| DeepSeek Harness (DSH) | dsh-session-manager | 验证环境 |
|
|
146
|
+
| DeepSeek Harness (DSH) | dsh-session-manager | 环境 |
|
|
148
147
|
| :--- | :--- | :--- |
|
|
148
|
+
| **v0.2.2-rc.1** | `0.7.6` | Desktop |
|
|
149
149
|
| **v0.2.1-rc.2** | `0.7.5`, `0.7.4`, `0.7.3`, `0.7.2` | Desktop |
|
|
150
150
|
| **v0.2.0-rc.2** | `0.7.1`, `0.7.0`, `0.6.4`, `0.6.3`, `0.6.2`, `0.5.4` | Desktop |
|
|
151
151
|
| **v0.1.7-rc.2** | `0.5.3` | Desktop / Web UI |
|
|
@@ -157,8 +157,6 @@ dsh plugin --profile web add <本仓库路径>
|
|
|
157
157
|
| **v0.1.2-rc.1** | `0.4.0` | Web UI |
|
|
158
158
|
| **v0.1.0-rc.7** | `0.1.2`, `0.1.1`, `0.1.0` | Web UI |
|
|
159
159
|
|
|
160
|
-
以上版本组合已在官方 Web UI 或 Desktop 客户端中完成测试。其他版本组合可能同样兼容,但未逐一验证。
|
|
161
|
-
|
|
162
160
|
使用独立 DSH CLI / runtime 时,需要 Node.js 22.15+(22.x)或 24+,以提供内置 Zstd 支持。官方 Desktop 客户端单独携带并管理与其版本匹配的 runtime。
|
|
163
161
|
|
|
164
162
|
本插件是 Cordis 插件,peer dependency 为 `cordis: ">=4.0.0-rc <5"`。
|
package/lib/client.js
CHANGED
|
@@ -728,7 +728,7 @@ function isTopmostDialog(z) {
|
|
|
728
728
|
".sm-rowCurrent{border-color:var(--dsw-alias-state-business-primary,var(--dsw-alias-label-secondary))}",
|
|
729
729
|
".sm-rowMain{min-width:0;display:flex;flex-direction:column;gap:2px;flex:1 1 280px}",
|
|
730
730
|
".sm-rowTitle{color:var(--dsw-alias-label-primary);font-size:13px;line-height:20px;display:flex;align-items:center;gap:6px;min-width:0}",
|
|
731
|
-
".sm-rowTitle>span:first-child{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}",
|
|
731
|
+
".sm-rowTitle>span:first-child{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.sm-rowTitleButton{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;border:0;padding:0;background:transparent;color:inherit;font:inherit;text-align:left;cursor:pointer}.sm-rowTitleButton:hover{text-decoration:underline}",
|
|
732
732
|
".sm-rowMeta{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;white-space:nowrap;text-overflow:ellipsis;overflow:hidden}",
|
|
733
733
|
".sm-rowActions{display:flex;gap:4px;flex:none;flex-wrap:wrap;justify-content:flex-end}",
|
|
734
734
|
".sm-badge{flex:none;border-radius:999px;padding:0 6px;font-size:10px;line-height:16px}",
|
|
@@ -2585,6 +2585,20 @@ function isTopmostDialog(z) {
|
|
|
2585
2585
|
);
|
|
2586
2586
|
}
|
|
2587
2587
|
|
|
2588
|
+
function PresetIcon() {
|
|
2589
|
+
return h("svg", {
|
|
2590
|
+
viewBox: "0 0 24 24", width: 14, height: 14,
|
|
2591
|
+
fill: "none", stroke: "currentColor", strokeWidth: "1.5",
|
|
2592
|
+
strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true",
|
|
2593
|
+
style: { display: "block" }
|
|
2594
|
+
},
|
|
2595
|
+
h("path", { d: "M3 6h3m4 0h11M3 12h11m4 0h3M3 18h5m4 0h9" }),
|
|
2596
|
+
h("circle", { cx: 8, cy: 6, r: 2 }),
|
|
2597
|
+
h("circle", { cx: 16, cy: 12, r: 2 }),
|
|
2598
|
+
h("circle", { cx: 10, cy: 18, r: 2 })
|
|
2599
|
+
);
|
|
2600
|
+
}
|
|
2601
|
+
|
|
2588
2602
|
function HeaderEllipsisIcon(props = {}) {
|
|
2589
2603
|
const size = props.size || 14;
|
|
2590
2604
|
return h("svg", {
|
|
@@ -3019,7 +3033,9 @@ function isTopmostDialog(z) {
|
|
|
3019
3033
|
h("div", { className: "sm-bulkResultList" },
|
|
3020
3034
|
group.map((item, i) => h("div", { key: (item.sessionId || i) + "-" + i, className: "sm-bulkResultItem", title: item.sessionId || "" },
|
|
3021
3035
|
h("div", { className: "sm-bulkResultItemTitle" }, item.displayTitle || item.sessionId || "—"),
|
|
3022
|
-
reasonClass && (item.reason || item.error) ? h("div", { className: reasonClass }, item.reason || item.error) : null
|
|
3036
|
+
reasonClass && (item.reason || item.error) ? h("div", { className: reasonClass }, item.reason || item.error) : null,
|
|
3037
|
+
Array.isArray(item.warnings) && item.warnings.length > 0
|
|
3038
|
+
? h("div", { className: "sm-warn", role: "status" }, item.warnings.filter(warning => typeof warning === "string").join("\n")) : null
|
|
3023
3039
|
))
|
|
3024
3040
|
)
|
|
3025
3041
|
);
|
|
@@ -3661,6 +3677,7 @@ function isTopmostDialog(z) {
|
|
|
3661
3677
|
window.removeEventListener("click", clickHandler, true);
|
|
3662
3678
|
};
|
|
3663
3679
|
}, [open, zIndex]);
|
|
3680
|
+
const floatingCoords = useFloatingPosition(props.anchor, open, { placement: "bottom-end", dialogWidth: 360, dialogHeight: 320 });
|
|
3664
3681
|
if (!open) return null;
|
|
3665
3682
|
|
|
3666
3683
|
const isSame = Boolean(selectedWorkspaceId && currentWorkspaceId && selectedWorkspaceId === currentWorkspaceId);
|
|
@@ -3694,7 +3711,6 @@ function isTopmostDialog(z) {
|
|
|
3694
3711
|
onConfirm(selectedWorkspaceId);
|
|
3695
3712
|
}
|
|
3696
3713
|
};
|
|
3697
|
-
const floatingCoords = useFloatingPosition(props.anchor, open, { placement: "bottom-end", dialogWidth: 360, dialogHeight: 320 });
|
|
3698
3714
|
const topPx = floatingCoords ? floatingCoords.top : (props.anchor && typeof props.anchor.top === "number" ? props.anchor.top : null);
|
|
3699
3715
|
const leftPx = floatingCoords ? floatingCoords.left : (props.anchor && typeof props.anchor.left === "number" ? props.anchor.left : null);
|
|
3700
3716
|
const isCentered = Boolean(floatingCoords && floatingCoords.centered);
|
|
@@ -3927,7 +3943,7 @@ function isTopmostDialog(z) {
|
|
|
3927
3943
|
));
|
|
3928
3944
|
}
|
|
3929
3945
|
|
|
3930
|
-
const CURRENT_VERSION = "0.7.
|
|
3946
|
+
const CURRENT_VERSION = "0.7.6";
|
|
3931
3947
|
|
|
3932
3948
|
function compareSemver(v1, v2) {
|
|
3933
3949
|
if (typeof v1 !== "string" || typeof v2 !== "string") return 0;
|
|
@@ -5125,6 +5141,15 @@ function isTopmostDialog(z) {
|
|
|
5125
5141
|
const mark = annotationOf(annotations, s.id);
|
|
5126
5142
|
const isSelected = selectedIds.has(s.id);
|
|
5127
5143
|
const isWsArchived = isSessionWorkspaceArchived(s);
|
|
5144
|
+
const openRow = () => {
|
|
5145
|
+
if (isArchived) {
|
|
5146
|
+
if (isWsArchived) {
|
|
5147
|
+
if (typeof window !== "undefined" && typeof window.alert === "function") window.alert(t("error.workspaceArchived"));
|
|
5148
|
+
return;
|
|
5149
|
+
}
|
|
5150
|
+
runRow(s.id, async () => { await onUnarchive(s.id); await onOpen(s.id); });
|
|
5151
|
+
} else onOpen(s.id);
|
|
5152
|
+
};
|
|
5128
5153
|
const checkbox = (selectedIds.size === 0 && !selectModeEnabled)
|
|
5129
5154
|
? null
|
|
5130
5155
|
: h("label", { className: "sm-bulkRowCheckbox", onClick: (e) => e.stopPropagation() },
|
|
@@ -5144,7 +5169,7 @@ function isTopmostDialog(z) {
|
|
|
5144
5169
|
h("div", { className: "sm-rowCheckboxSlot" + ((selectedIds.size > 0 || selectModeEnabled) ? " sm-rowCheckboxSlot-visible" : "") }, checkbox),
|
|
5145
5170
|
h("div", { className: "sm-rowMain" },
|
|
5146
5171
|
h("div", { className: "sm-rowTitle" },
|
|
5147
|
-
h("
|
|
5172
|
+
h("button", { type: "button", className: "sm-rowTitleButton", title: t("row.open"), "aria-label": t("row.open") + ": " + (s.blank ? t("panel.session.new") : (s.displayTitle || s.id)), disabled: busy || bulkBusy, onClick: openRow }, s.blank ? t("panel.session.new") : (s.displayTitle || s.id)),
|
|
5148
5173
|
isWsArchived ? h("span", { className: "sm-badge sm-badgeWorkspaceArchived" }, t("panel.workspaceArchived")) : null,
|
|
5149
5174
|
isArchived ? h("span", { className: "sm-badge sm-badgeArchived" }, t("panel.archived")) : null,
|
|
5150
5175
|
s.running ? h("span", { className: "sm-badge sm-badgeRunning" }, t("panel.running")) : null,
|
|
@@ -5152,14 +5177,13 @@ function isTopmostDialog(z) {
|
|
|
5152
5177
|
),
|
|
5153
5178
|
h("div", { className: "sm-rowSecondary" },
|
|
5154
5179
|
h("div", { className: "sm-rowBadges" },
|
|
5155
|
-
h("span", { className: "sm-priorityBadge sm-priority-" + (mark.priority ?? 3),
|
|
5180
|
+
renderTip(t("marks.priority." + (mark.priority ?? 3)), h("span", { className: "sm-priorityBadge sm-priority-" + (mark.priority ?? 3), "aria-label": t("marks.priority." + (mark.priority ?? 3)) }, "P" + (mark.priority ?? 3))),
|
|
5156
5181
|
mark.tags.slice(0, 2).map(tag => h("button", { key: tag, type: "button", className: "sm-tagChip", title: tag, onClick: () => setTagFilter("tag:" + tag.toLowerCase()) }, tag)),
|
|
5157
5182
|
mark.tags.length > 2 ? h("span", { title: mark.tags.join(", ") }, "+" + (mark.tags.length - 2)) : null,
|
|
5158
5183
|
mark.note ? h("button", { type: "button", className: "sm-noteBadge", title: t("marks.note.exists"), onClick: () => { setAnnotationAnchor(null); setAnnotationFor(current => current?.id === s.id ? null : s); } }, "✎") : null
|
|
5159
5184
|
),
|
|
5160
5185
|
h("div", { className: "sm-rowMeta" }, shortPath(s.cwd) + (s.cwd ? " · " : "") + formatRelative(s.updatedAt, now)),
|
|
5161
5186
|
h("div", { className: "sm-rowActions" },
|
|
5162
|
-
h("button", { type: "button", className: "sm-rowBtn", disabled: busy, onClick: () => { if (isArchived) { if (isWsArchived) { if (typeof window !== "undefined" && typeof window.alert === "function") { window.alert(t("error.workspaceArchived")); } return; } /* DSH refuses to surface an archived session as the current view: clicking "Open" on an archived row flashes an empty chat panel and bounces back to "no session selected". Unarchive first, then open. (issue #17.4) */ runRow(s.id, async () => { await onUnarchive(s.id); await onOpen(s.id); }); } else { onOpen(s.id); } } }, t("row.open")),
|
|
5163
5187
|
renderTip(t(isArchived ? "row.unarchive" : "row.archive"), h("button", {
|
|
5164
5188
|
type: "button",
|
|
5165
5189
|
className: "sm-rowBtn sm-rowBtnIcon" + (isArchived ? " sm-rowBtnActive" : ""),
|
|
@@ -5192,7 +5216,16 @@ function isTopmostDialog(z) {
|
|
|
5192
5216
|
h("span", { className: "sm-headerBtnIcon" }, HeaderMoveIcon()),
|
|
5193
5217
|
h("span", { className: "sm-rowBtnLabel" }, t("row.move"))
|
|
5194
5218
|
)),
|
|
5195
|
-
|
|
5219
|
+
renderTip(t("row.migrate"), h("button", {
|
|
5220
|
+
type: "button",
|
|
5221
|
+
className: "sm-rowBtn sm-rowBtnIcon" + (migrateFor && migrateFor.id === s.id ? " sm-rowBtnActive" : ""),
|
|
5222
|
+
disabled: busy,
|
|
5223
|
+
"aria-label": t("row.migrate") + ": " + (s.displayTitle || s.id),
|
|
5224
|
+
onClick: () => { if (migrateFor && migrateFor.id === s.id) setMigrateFor(null); else setMigrateFor(s); }
|
|
5225
|
+
},
|
|
5226
|
+
h("span", { className: "sm-headerBtnIcon" }, PresetIcon()),
|
|
5227
|
+
h("span", { className: "sm-rowBtnLabel" }, t("row.migrate"))
|
|
5228
|
+
)),
|
|
5196
5229
|
renderTip(t("row.delete"), h("button", {
|
|
5197
5230
|
type: "button",
|
|
5198
5231
|
className: "sm-rowBtn sm-rowBtnDanger sm-rowBtnIcon" + (confirmFor && confirmFor.id === s.id ? " sm-rowBtnActive" : ""),
|
|
@@ -6089,16 +6122,15 @@ function isTopmostDialog(z) {
|
|
|
6089
6122
|
// <div data-slot-error="…" />, taking every title-bar button down
|
|
6090
6123
|
// and leaving 移动 / 删除 with no click target. Compute the value
|
|
6091
6124
|
// here once per render, then reuse the captured const in handlers.
|
|
6125
|
+
const sessionsSnapshot = useSessions ? useSessions((s) => s) : null;
|
|
6092
6126
|
const displayTitle = (() => {
|
|
6093
6127
|
const fallback = props.displayTitle;
|
|
6094
6128
|
if (fallback && fallback !== sessionId) return fallback;
|
|
6095
|
-
const
|
|
6096
|
-
const found = list ? list.byId[sessionId] : null;
|
|
6129
|
+
const found = sessionsSnapshot ? sessionsSnapshot.byId[sessionId] : null;
|
|
6097
6130
|
return (found && found.displayTitle) || "";
|
|
6098
6131
|
})();
|
|
6099
6132
|
const currentWorkspaceId = (() => {
|
|
6100
|
-
const
|
|
6101
|
-
const found = list ? list.byId[sessionId] : null;
|
|
6133
|
+
const found = sessionsSnapshot ? sessionsSnapshot.byId[sessionId] : null;
|
|
6102
6134
|
return (found && found.workspaceId) || props.workspaceId || "";
|
|
6103
6135
|
})();
|
|
6104
6136
|
const annotations = useAnnotations();
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { randomBytes } from "node:crypto";
|
|
4
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
5
|
+
import { assertSessionId, replaceSessionFile } from "../session-files.js";
|
|
6
|
+
|
|
7
|
+
// rc.1 cache.write() may await sessions.flush() before reaching table.put().
|
|
8
|
+
// A queue barrier alone cannot drain such a delayed writer. Keep a lifecycle
|
|
9
|
+
// tombstone at the table entry point: queued puts precede delete(), later puts
|
|
10
|
+
// of the deleted lifecycle are ignored. A recreated id with a new identity is
|
|
11
|
+
// independent. The guard lives with the table (including across plugin reloads).
|
|
12
|
+
const guards = new WeakMap();
|
|
13
|
+
const lifecycleKey = identity => Number.isSafeInteger(identity?.createdAt)
|
|
14
|
+
? JSON.stringify([identity.createdAt, identity.cwd ?? null, identity.isSeeded ?? false])
|
|
15
|
+
: undefined;
|
|
16
|
+
export function beginProjectionCacheDeletion(cache, id, header) {
|
|
17
|
+
assertSessionId(id);
|
|
18
|
+
const table = typeof cache?.requireTable === "function" ? cache.requireTable() : cache?.table;
|
|
19
|
+
if (!table || !["get", "put", "delete"].every(key => typeof table[key] === "function")) return undefined;
|
|
20
|
+
let guard = guards.get(table);
|
|
21
|
+
if (!guard) {
|
|
22
|
+
const removed = new Map();
|
|
23
|
+
const put = table.put;
|
|
24
|
+
table.put = function(key, record) {
|
|
25
|
+
if (removed.get(key)?.has(lifecycleKey(record?.identity))) return Promise.resolve();
|
|
26
|
+
return put.call(this, key, record);
|
|
27
|
+
};
|
|
28
|
+
guard = { removed };
|
|
29
|
+
guards.set(table, guard);
|
|
30
|
+
}
|
|
31
|
+
const identity = table.get(id)?.identity;
|
|
32
|
+
const stamps = [header, identity].map(lifecycleKey).filter(key => key !== undefined);
|
|
33
|
+
if (!stamps.length && (header || identity)) throw new Error("无法确认投影缓存的会话生命周期");
|
|
34
|
+
const previous = guard.removed.get(id);
|
|
35
|
+
const blocked = new Set([...(previous ?? []), ...stamps]);
|
|
36
|
+
guard.removed.set(id, blocked);
|
|
37
|
+
return {
|
|
38
|
+
remove: () => table.delete(id),
|
|
39
|
+
legacyRetained: table.host?.unit?.descriptor?.layout === "per-record",
|
|
40
|
+
legacyManaged: Boolean(table.host?.unit?.descriptor) && table.host.unit.descriptor.layout !== "per-record",
|
|
41
|
+
rollback() {
|
|
42
|
+
if (guard.removed.get(id) !== blocked) return;
|
|
43
|
+
if (previous) guard.removed.set(id, previous); else guard.removed.delete(id);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function regular(path, directory = false) {
|
|
49
|
+
const info = await fs.lstat(path);
|
|
50
|
+
if (info.isSymbolicLink() || !(directory ? info.isDirectory() : info.isFile())) {
|
|
51
|
+
throw new Error("投影缓存路径不是安全的普通" + (directory ? "目录" : "文件") + ": " + path);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function assertProjectionCachePath(home, id) {
|
|
56
|
+
assertSessionId(id);
|
|
57
|
+
const paths = [home, join(home, "storages"), join(home, "storages", "session_projcache"),
|
|
58
|
+
join(home, "storages", "session_projcache", "sessions"), join(home, "storages", "session_projcache", "sessions", `${id}.json`)];
|
|
59
|
+
for (let i = 0; i < paths.length; i++) {
|
|
60
|
+
try { await regular(paths[i], i < paths.length - 1); }
|
|
61
|
+
catch (error) { if (error.code === "ENOENT") return; throw error; }
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** rc.1 bootstraps but never writes its retained legacy unit. Serialize our
|
|
66
|
+
* read/modify/publish across deletes and processes; preserve its stamp/schema
|
|
67
|
+
* and every unrelated table/record. Unknown shapes fail without publication.
|
|
68
|
+
*/
|
|
69
|
+
export async function removeLegacyProjectionCache(home, id) {
|
|
70
|
+
assertSessionId(id);
|
|
71
|
+
const root = join(home, "storages");
|
|
72
|
+
const path = join(root, "session_projcache.json");
|
|
73
|
+
try {
|
|
74
|
+
await regular(home, true);
|
|
75
|
+
await regular(root, true);
|
|
76
|
+
await regular(path);
|
|
77
|
+
} catch (error) { if (error.code === "ENOENT") return; throw error; }
|
|
78
|
+
const lockPath = join(root, "session_projcache.session-manager.lock");
|
|
79
|
+
const token = randomBytes(16).toString("hex");
|
|
80
|
+
const deadline = Date.now() + 3000;
|
|
81
|
+
let lock;
|
|
82
|
+
while (!lock) {
|
|
83
|
+
try { lock = await fs.open(lockPath, "wx", 0o600); }
|
|
84
|
+
catch (error) {
|
|
85
|
+
if (error.code !== "EEXIST") throw error;
|
|
86
|
+
if (Date.now() >= deadline) throw new Error("投影缓存清理锁正被占用: " + lockPath);
|
|
87
|
+
await delay(25);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
let stamped = false;
|
|
91
|
+
try {
|
|
92
|
+
await lock.writeFile(token);
|
|
93
|
+
stamped = true;
|
|
94
|
+
await regular(root, true);
|
|
95
|
+
await regular(path);
|
|
96
|
+
const document = JSON.parse(await fs.readFile(path, "utf8"));
|
|
97
|
+
const records = document?.tables?.sessions;
|
|
98
|
+
if (document?.unit?.name !== "session_projcache" || ![3, 4, 5, 6, 7].includes(document.unit.version)
|
|
99
|
+
|| !records || typeof records !== "object" || Array.isArray(records)) {
|
|
100
|
+
throw new Error("未知的旧版投影缓存格式,未修改文件");
|
|
101
|
+
}
|
|
102
|
+
if (!Object.hasOwn(records, id)) return;
|
|
103
|
+
delete records[id];
|
|
104
|
+
const result = await replaceSessionFile(path, JSON.stringify(document) + "\n");
|
|
105
|
+
if (result.backupPath) throw new Error("旧版投影缓存已更新,但备份清理失败: " + result.backupPath);
|
|
106
|
+
} finally {
|
|
107
|
+
await lock.close();
|
|
108
|
+
if (!stamped || await fs.readFile(lockPath, "utf8").catch(() => undefined) === token) await fs.rm(lockPath, { force: true });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -12,8 +12,10 @@
|
|
|
12
12
|
*
|
|
13
13
|
* @module dsh-session-manager/compat/zstd-frames
|
|
14
14
|
*/
|
|
15
|
-
import { zstdDecompress } from "node:zlib";
|
|
15
|
+
import { zstdDecompress, createZstdDecompress } from "node:zlib";
|
|
16
16
|
import { promisify } from "node:util";
|
|
17
|
+
import { Readable } from "node:stream";
|
|
18
|
+
import { pipeline } from "node:stream/promises";
|
|
17
19
|
|
|
18
20
|
const zstdDecompressAsync = promisify(zstdDecompress);
|
|
19
21
|
const ZSTD_MAGIC = 0xfd2fb528;
|
|
@@ -76,6 +78,81 @@ export async function decompressZstdFrame(frame, options) {
|
|
|
76
78
|
return zstdDecompressAsync(frame, options);
|
|
77
79
|
}
|
|
78
80
|
|
|
81
|
+
export function decompressZstdFirstLine(frame, limit) {
|
|
82
|
+
return new Promise((resolve, reject) => {
|
|
83
|
+
const stream = createZstdDecompress();
|
|
84
|
+
const chunks = [];
|
|
85
|
+
let size = 0;
|
|
86
|
+
let settled = false;
|
|
87
|
+
let lineFound = false;
|
|
88
|
+
const finish = (error, value) => {
|
|
89
|
+
if (settled) return;
|
|
90
|
+
settled = true;
|
|
91
|
+
stream.destroy();
|
|
92
|
+
if (error) reject(error); else resolve(value);
|
|
93
|
+
};
|
|
94
|
+
stream.on("error", error => finish(error));
|
|
95
|
+
stream.on("end", () => lineFound
|
|
96
|
+
? finish(null, Buffer.concat(chunks, size))
|
|
97
|
+
: finish(new Error("会话头行不完整或超过大小限制")));
|
|
98
|
+
stream.on("data", chunk => {
|
|
99
|
+
if (lineFound) return;
|
|
100
|
+
const newline = chunk.indexOf(10);
|
|
101
|
+
const part = newline < 0 ? chunk : chunk.subarray(0, newline + 1);
|
|
102
|
+
size += part.length;
|
|
103
|
+
if (size > limit) return finish(new Error("会话头行不完整或超过大小限制"));
|
|
104
|
+
chunks.push(part);
|
|
105
|
+
if (newline >= 0) lineFound = true;
|
|
106
|
+
});
|
|
107
|
+
if (Buffer.isBuffer(frame)) stream.end(frame);
|
|
108
|
+
else pipeline(Readable.from(frame), stream).catch(error => finish(error));
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Feed exactly the first complete frame, with bounded reads even for huge batches.
|
|
113
|
+
* Keep consuming after the first line to validate the frame/checksum before discovery.
|
|
114
|
+
*/
|
|
115
|
+
export async function* firstZstdFrameChunks(handle) {
|
|
116
|
+
let position = 0;
|
|
117
|
+
const exact = async size => {
|
|
118
|
+
const bytes = Buffer.alloc(size);
|
|
119
|
+
let offset = 0;
|
|
120
|
+
while (offset < size) {
|
|
121
|
+
const { bytesRead } = await handle.read(bytes, offset, size - offset, position);
|
|
122
|
+
if (!bytesRead) throw new Error("会话日志存在截断的 Zstd 帧");
|
|
123
|
+
offset += bytesRead;
|
|
124
|
+
position += bytesRead;
|
|
125
|
+
}
|
|
126
|
+
return bytes;
|
|
127
|
+
};
|
|
128
|
+
const prefix = await exact(5);
|
|
129
|
+
if (prefix.readUInt32LE(0) !== ZSTD_MAGIC) throw new Error("corrupt Zstandard session log: invalid frame magic");
|
|
130
|
+
const descriptor = prefix[4];
|
|
131
|
+
if (descriptor & 0x18) throw new Error("corrupt Zstandard session log: reserved frame-header bit");
|
|
132
|
+
yield prefix;
|
|
133
|
+
const sizeFlag = descriptor >>> 6;
|
|
134
|
+
const singleSegment = Boolean(descriptor & 0x20);
|
|
135
|
+
const dictionaryFlag = descriptor & 3;
|
|
136
|
+
const headerSize = (singleSegment ? 0 : 1) + (dictionaryFlag === 3 ? 4 : dictionaryFlag)
|
|
137
|
+
+ (sizeFlag === 0 ? (singleSegment ? 1 : 0) : 1 << sizeFlag);
|
|
138
|
+
if (headerSize) yield await exact(headerSize);
|
|
139
|
+
for (;;) {
|
|
140
|
+
const header = await exact(3);
|
|
141
|
+
const block = header.readUIntLE(0, 3);
|
|
142
|
+
const type = (block >>> 1) & 3;
|
|
143
|
+
if (type === 3) throw new Error("corrupt Zstandard session log: reserved block type");
|
|
144
|
+
yield header;
|
|
145
|
+
let remaining = type === 1 ? 1 : block >>> 3;
|
|
146
|
+
while (remaining) {
|
|
147
|
+
const count = Math.min(remaining, 64 * 1024);
|
|
148
|
+
yield await exact(count);
|
|
149
|
+
remaining -= count;
|
|
150
|
+
}
|
|
151
|
+
if (block & 1) break;
|
|
152
|
+
}
|
|
153
|
+
if (descriptor & 4) yield await exact(4);
|
|
154
|
+
}
|
|
155
|
+
|
|
79
156
|
export async function decompressAllZstdFrames(buffer) {
|
|
80
157
|
const scan = scanZstdFrames(buffer);
|
|
81
158
|
if (scan.frames.length === 0) {
|
package/lib/index.js
CHANGED
|
@@ -29,6 +29,7 @@ import { randomBytes } from "node:crypto";
|
|
|
29
29
|
import { createAnnotationStore } from "./annotation-store.js";
|
|
30
30
|
import { createWorkspaceArchiveStore, assertWorkspaceId } from "./workspace-archive-store.js";
|
|
31
31
|
import { createDshAdapter, getService, normalizeSessionHeader, compareSemver, detectPluginSource, isHmrActive, resolveProfileDir } from "./compat/dsh-adapter.js";
|
|
32
|
+
import { assertProjectionCachePath, beginProjectionCacheDeletion, removeLegacyProjectionCache } from "./compat/projection-cache-delete.js";
|
|
32
33
|
import { ARTIFACT_NAMES, assertSessionId, assertSessionDirectory, candidateSessionDirs, encodeFsSessionSegment, decodeFsSessionSegment, encodeProjectSegment, encodeSessionSegment, parseSessionGenerationFromFilename, readSessionHeader, readSessionFile, writeTempFile, replaceSessionFile } from "./session-files.js";
|
|
33
34
|
|
|
34
35
|
export const name = "dsh-session-manager";
|
|
@@ -386,7 +387,7 @@ function verifyInstalledPackage(profileDir, expectedVersion) {
|
|
|
386
387
|
|
|
387
388
|
export function apply(ctx) {
|
|
388
389
|
try {
|
|
389
|
-
const buildId = "dsh-session-manager v0.7.
|
|
390
|
+
const buildId = "dsh-session-manager v0.7.6 (workspace-archive)";
|
|
390
391
|
if (typeof ctx?.logger?.info === "function") ctx.logger.info(buildId);
|
|
391
392
|
else if (typeof console !== "undefined" && console?.info) console.info(buildId);
|
|
392
393
|
} catch (_) { /* ignore logging failures */ }
|
|
@@ -2058,11 +2059,20 @@ ${rest}`, "utf8");
|
|
|
2058
2059
|
await assertAndStopActiveSession(sessionId, true);
|
|
2059
2060
|
}
|
|
2060
2061
|
|
|
2062
|
+
let projectionDeletion;
|
|
2063
|
+
let artifactsDeleted = false;
|
|
2061
2064
|
try {
|
|
2062
2065
|
// Resolve/validate before cancelling the agent or changing registry state.
|
|
2063
2066
|
let dir = await sessionDirOf(sessionId);
|
|
2064
2067
|
const session = ctx.sessions?.get?.(sessionId);
|
|
2065
2068
|
let agent = safeGetAgent(sessionId);
|
|
2069
|
+
try {
|
|
2070
|
+
await assertProjectionCachePath(dirname(adapter.homePath("storages")), sessionId);
|
|
2071
|
+
const artifact = await readSessionArtifact(sessionId, undefined, { headerOnly: true });
|
|
2072
|
+
projectionDeletion = beginProjectionCacheDeletion(adapter.projectionCache, sessionId, session?.header ?? artifact?.meta);
|
|
2073
|
+
} catch (error) {
|
|
2074
|
+
cleanupWarnings.push("投影缓存回写防护不可用: " + safeErrorMessage(error));
|
|
2075
|
+
}
|
|
2066
2076
|
|
|
2067
2077
|
if (agent !== void 0) {
|
|
2068
2078
|
// Stop any running turn (disposed-kind suppresses re-wake).
|
|
@@ -2123,6 +2133,29 @@ ${rest}`, "utf8");
|
|
|
2123
2133
|
await assertSessionDirectory(adapter.homePath("sessions"), dir, sessionId);
|
|
2124
2134
|
await rm(dir, { recursive: true, force: true });
|
|
2125
2135
|
}
|
|
2136
|
+
artifactsDeleted = true;
|
|
2137
|
+
|
|
2138
|
+
// Use the host queue to remove both its in-memory row and durable record.
|
|
2139
|
+
// Tombstones installed before disposal also stop writes still awaiting flush.
|
|
2140
|
+
try {
|
|
2141
|
+
await assertProjectionCachePath(dirname(adapter.homePath("storages")), sessionId);
|
|
2142
|
+
if (projectionDeletion) await projectionDeletion.remove();
|
|
2143
|
+
else {
|
|
2144
|
+
const file = adapter.homePath("storages", "session_projcache", "sessions", `${sessionId}.json`);
|
|
2145
|
+
if (existsSync(file)) throw new Error("宿主投影缓存删除接口不可用,缓存尚未清理");
|
|
2146
|
+
}
|
|
2147
|
+
} catch (error) {
|
|
2148
|
+
cleanupWarnings.push("会话已删除,但投影缓存清理失败: " + safeErrorMessage(error));
|
|
2149
|
+
}
|
|
2150
|
+
try {
|
|
2151
|
+
if (projectionDeletion?.legacyRetained) await removeLegacyProjectionCache(dirname(adapter.homePath("storages")), sessionId);
|
|
2152
|
+
else if (!projectionDeletion?.legacyManaged && existsSync(adapter.homePath("storages", "session_projcache.json"))) {
|
|
2153
|
+
throw new Error("无法确认旧版缓存已停止写入,未修改文件");
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
catch (error) {
|
|
2157
|
+
cleanupWarnings.push("会话已删除,但旧版投影缓存清理失败: " + safeErrorMessage(error));
|
|
2158
|
+
}
|
|
2126
2159
|
|
|
2127
2160
|
try {
|
|
2128
2161
|
await annotations().remove(sessionId);
|
|
@@ -2152,6 +2185,7 @@ ${rest}`, "utf8");
|
|
|
2152
2185
|
subagentCount: deletedSubagentIds.length
|
|
2153
2186
|
};
|
|
2154
2187
|
} catch (parentErr) {
|
|
2188
|
+
if (!artifactsDeleted) projectionDeletion?.rollback();
|
|
2155
2189
|
const err = deletedSubagentIds.length > 0
|
|
2156
2190
|
? new Error(`删除主会话 '${sessionId}' 失败: ${safeErrorMessage(parentErr)}`)
|
|
2157
2191
|
: parentErr;
|
|
@@ -2743,7 +2777,7 @@ ${rest}`, "utf8");
|
|
|
2743
2777
|
return send(res, 200, { ok: true, result });
|
|
2744
2778
|
}
|
|
2745
2779
|
if (req.method === "GET" && path === "/update/check") {
|
|
2746
|
-
const currentVersion = adapter.pluginVersion() || "0.7.
|
|
2780
|
+
const currentVersion = adapter.pluginVersion() || "0.7.6";
|
|
2747
2781
|
const runningVersion = currentVersion;
|
|
2748
2782
|
let installedVersion = runningVersion;
|
|
2749
2783
|
const profileDir = resolveProfileDir(adapter, ctx);
|
|
@@ -2927,7 +2961,7 @@ ${rest}`, "utf8");
|
|
|
2927
2961
|
};
|
|
2928
2962
|
setActiveTask(taskRecord);
|
|
2929
2963
|
|
|
2930
|
-
const currentVersion = adapter.pluginVersion() || "0.7.
|
|
2964
|
+
const currentVersion = adapter.pluginVersion() || "0.7.6";
|
|
2931
2965
|
let installedVersion = currentVersion;
|
|
2932
2966
|
if (profileDir) {
|
|
2933
2967
|
const pkgFile = join(profileDir, "node_modules", "dsh-session-manager", "package.json");
|
|
@@ -3457,7 +3491,7 @@ ${rest}`, "utf8");
|
|
|
3457
3491
|
}
|
|
3458
3492
|
if (req.method === "GET" && path === "/update/status") {
|
|
3459
3493
|
const profileDir = resolveProfileDir(adapter, ctx);
|
|
3460
|
-
const currentVersion = adapter.pluginVersion() || "0.7.
|
|
3494
|
+
const currentVersion = adapter.pluginVersion() || "0.7.6";
|
|
3461
3495
|
let installedVersion = currentVersion;
|
|
3462
3496
|
if (profileDir) {
|
|
3463
3497
|
const diskCheck = verifyInstalledPackage(profileDir);
|
package/lib/session-files.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
2
|
import * as fs from "node:fs/promises";
|
|
3
3
|
import { basename, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
|
-
import { decompressAllZstdFrames,
|
|
4
|
+
import { decompressAllZstdFrames, decompressZstdFirstLine, firstZstdFrameChunks } from "./compat/zstd-frames.js";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Filenames tried in this order to locate a session's durable artifact.
|
|
@@ -190,20 +190,17 @@ export async function readSessionHeader(path) {
|
|
|
190
190
|
await assertRegularFile(path);
|
|
191
191
|
const handle = await fs.open(path, "r");
|
|
192
192
|
try {
|
|
193
|
+
if (path.endsWith(".zstd")) {
|
|
194
|
+
const decoded = await decompressZstdFirstLine(firstZstdFrameChunks(handle), MAX_HEADER_BYTES);
|
|
195
|
+
return parseHeader(decoded.toString("utf8"));
|
|
196
|
+
}
|
|
193
197
|
let buffer = Buffer.alloc(0);
|
|
194
198
|
while (buffer.length < MAX_HEADER_BYTES) {
|
|
195
199
|
const chunk = Buffer.alloc(Math.min(64 * 1024, MAX_HEADER_BYTES - buffer.length));
|
|
196
200
|
const { bytesRead } = await handle.read(chunk, 0, chunk.length, buffer.length);
|
|
197
201
|
if (bytesRead === 0) break;
|
|
198
202
|
buffer = Buffer.concat([buffer, chunk.subarray(0, bytesRead)]);
|
|
199
|
-
if (
|
|
200
|
-
const { frames } = scanZstdFrames(buffer, 1);
|
|
201
|
-
if (frames.length) {
|
|
202
|
-
const frame = frames[0];
|
|
203
|
-
const decoded = await decompressZstdFrame(buffer.subarray(frame.start, frame.end), { maxOutputLength: MAX_HEADER_BYTES });
|
|
204
|
-
return parseHeader(decoded.toString("utf8"));
|
|
205
|
-
}
|
|
206
|
-
} else if (buffer.includes(10)) {
|
|
203
|
+
if (buffer.includes(10)) {
|
|
207
204
|
return parseHeader(buffer.toString("utf8"));
|
|
208
205
|
}
|
|
209
206
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-session-manager",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.6",
|
|
4
4
|
"description": "DeepSeek Harness session and workspace manager —— Workspaces: archive and visibility. Sessions: move, migrate presets, delete, favorite, review later, prioritize, tag, and annotate. Batch operations. | DSH 会话与工作区管理——工作区:归档与显示。会话:跨工作区移动、预设迁移、删除;收藏、待回看、优先级、标签、备注。批量操作。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|