@michengai/dsh-archive-manager 0.1.1 → 0.1.2

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/lib/client.js CHANGED
@@ -492,9 +492,9 @@ window.__ModuleLoader__.load({
492
492
  * except workspace Rename/Delete and session Rename/Fork/Archive; the session
493
493
  * and workspace hover cards are suppressed while a menu is open.
494
494
  */
495
- /** Row display title: blank rows show the localized New Session label. */
496
- function displayTitle(node, t) {
497
- return node.blank ? t("session.new") : node.title;
495
+ /** Row display title: blank rows show the localized New Session label. */
496
+ function displayTitle(node, t) {
497
+ return node.blank ? t("session.new") : node.title ?? node.displayTitle ?? "";
498
498
  }
499
499
  /** Localized compact relative time ("刚刚"/"5分钟" in zh, "now"/"5min" in en). */
500
500
  function timeLabel(updatedAt, now, t) {
@@ -2885,8 +2885,9 @@ window.__ModuleLoader__.load({
2885
2885
  }
2886
2886
  //#endregion
2887
2887
  /** Pure derivation surface for dsh-archive-manager self-tests (no-op for the runtime). */
2888
- exports.__test = {
2889
- sessionVisible,
2888
+ exports.__test = {
2889
+ displayTitle,
2890
+ sessionVisible,
2890
2891
  deriveGroups,
2891
2892
  deriveFlat,
2892
2893
  deriveSearchResults,
@@ -2899,6 +2900,4 @@ window.__ModuleLoader__.load({
2899
2900
  exports.inject = inject;
2900
2901
  return module.exports;
2901
2902
  }
2902
- });
2903
-
2904
- //# sourceMappingURL=client.js.map
2903
+ });
package/lib/workspace.js CHANGED
@@ -7,51 +7,18 @@ import {
7
7
  import { bindTypertRemote, Remote } from "@deepseek-ai/dsh-typert-protocol";
8
8
  import { sessionDir } from "@deepseek-ai/dsh-spill-local";
9
9
  //#region lib/types/index.js
10
- /**
11
- * dsh-archive-manager host half — archive session management.
12
- *
13
- * `ArchiveWorkspaceRegistry` extends the shipped `WorkspaceRegistry`
14
- * (service name `workspaceRegistry`, one domain open, same accounting
15
- * invariants) and adds two durable operations:
16
- *
17
- * - `unarchiveSession(sessionId)` — removes the id from the registry-global
18
- * `archivedSessionIds` set. Archiving never moves the accounting seat, so
19
- * unarchiving restores the session at its original workspace position.
20
- * Idempotent: a session that is not archived resolves without writing.
21
- *
22
- * - `deleteSession(sessionId)` — PERMANENT deletion, no trace left on disk:
23
- * 1. validate the session is known (live, header-indexed, or persisted);
24
- * 2. if live: flush buffered events to disk, then detach the session from
25
- * the sessions store (`session/disposed` is emitted, so the browser
26
- * receives `host/session-removed` and greys the composer);
27
- * 3. wait for the projection cache's dispose write-behind (`whenIdle`) so
28
- * the final cache row is durable BEFORE it is deleted;
29
- * 4. remove the transcript directory (the whole `session-<id>` dir under
30
- * the persistence root, located through `sessionPersistence.locate`);
31
- * 5. remove the id from `archivedSessionIds` (durable `setState`);
32
- * 6. remove the id from every workspace record's `sessionIds` and refresh
33
- * the in-memory entity snapshots (durable `table.update`, so the host
34
- * stream pushes `host/workspace-changed` to every browser);
35
- * 7. delete the projection-cache row (`sessionProjectionCache.delete`);
36
- * 8. best-effort (failures only warn): cascade-delete SUBAGENT child
37
- * sessions of the deleted session (`header.origin === "subagent"` ONLY —
38
- * a fork branch carries `parentSession` too but is an independent user
39
- * session and is never cascade-deleted) and clear its spill data.
40
- *
41
- * Core steps throw on failure (no half-delete states: every failing step is
42
- * idempotent and re-runnable, so a retry heals); cascade/spill are
43
- * best-effort by design.
44
- *
45
- * The two new methods are also exported as Typert Remote endpoints
46
- * (`workspaceRegistry/unarchiveSession`, `workspaceRegistry/deleteSession`)
47
- * through the service's `typertRemote` binding + `Remote` markers — the
48
- * browser reaches them through the standard typert gateway SRC path, keeping
49
- * the legacy `/api/workspace.*` gateway untouched.
50
- *
51
- * The default export is a Service subclass (same shape as the shipped
52
- * `@deepseek-ai/dsh-workspace` package), so the profile patch can substitute
53
- * this package for the `workspace` row with no other wiring change.
54
- */
10
+ /**
11
+ * dsh-archive-manager 宿主侧归档会话管理。
12
+ *
13
+ * `deleteSession(sessionId)` 的顺序即语义:校验已知会话;实时会话先
14
+ * flush 再 detach;等待投影缓存写入完成;移除归档标记和工作区记账;
15
+ * 删除投影缓存,级联删除 SUBAGENT 子会话并清理 spill;最后才删除转录
16
+ * 目录。fork 分支虽有 `parentSession`,但属于独立用户会话,不参与级联。
17
+ *
18
+ * 所有可失败的持久化清理都先于不可逆的物理删除。此前任一步失败时,
19
+ * 转录仍保留,可重试删除;物理删除失败时记录残留路径供人工排查。
20
+ * `unarchiveSession` 与 `deleteSession` 通过 Typert Remote 暴露给浏览器。
21
+ */
55
22
  function markRemoteMethod(instance, method) {
56
23
  // Simulate the TS decorator pipeline `@Remote(method)` for one method:
57
24
  // `Remote` returns a standard method decorator; we hand it a decorator
@@ -128,21 +95,22 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
128
95
  sessions.detachEntered(entry);
129
96
  } else if (sessions !== void 0) await this.publishColdSessionRemoval(sessionId, sessions);
130
97
  const projCache = this.ctx.get("sessionProjectionCache");
131
- // The dispose write-behind must land BEFORE the cache row is deleted,
132
- // otherwise the row is written back after deletion (resurrected).
133
- await projCache?.whenIdle?.();
134
- await this.removeTranscriptDirectory(sessionId);
135
- const state = this.requireState();
98
+ // The dispose write-behind must land BEFORE the cache row is deleted,
99
+ // otherwise the row is written back after deletion (resurrected).
100
+ await projCache?.whenIdle?.();
101
+ // 物理删除不可逆:先完成所有可失败的持久化清理,失败时仍可重试。
102
+ const state = this.requireState();
136
103
  if (state.archivedSessionIds.includes(sessionId)) {
137
104
  await this.setState({
138
105
  ...state,
139
106
  archivedSessionIds: state.archivedSessionIds.filter((id) => id !== sessionId)
140
107
  });
141
- }
142
- await this.removeFromWorkspaceAccounts(sessionId);
143
- if (projCache !== void 0) await projCache.delete(sessionId);
144
- await this.deleteDescendants(sessionId);
145
- await this.cleanSpill(sessionId);
108
+ }
109
+ await this.removeFromWorkspaceAccounts(sessionId);
110
+ if (projCache !== void 0) await projCache.delete(sessionId);
111
+ await this.deleteDescendants(sessionId);
112
+ await this.cleanSpill(sessionId);
113
+ await this.removeTranscriptDirectory(sessionId);
146
114
  return { deleted: true };
147
115
  }
148
116
  /** 为未处于实时状态的持久化会话发布相同的移除事件。 */
@@ -162,17 +130,26 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
162
130
  this.ctx.logger.warn(`archive-manager: could not publish removal for stored session "${sessionId}": ${String(error)}`);
163
131
  }
164
132
  }
165
- /** Remove the session's whole transcript directory (throw on failure —
166
- * the irreversible step runs before any accounting mutation). */
167
- async removeTranscriptDirectory(sessionId) {
133
+ /** 删除会话的转录目录;仅在所有记账清理完成后调用。 */
134
+ async removeTranscriptDirectory(sessionId) {
168
135
  const persistence = this.ctx.get("sessionPersistence");
169
136
  if (persistence === void 0 || typeof persistence.locate !== "function") {
170
137
  throw new Error(`cannot delete session "${sessionId}": the session persistence backend does not expose locate() to resolve its transcript directory`);
171
138
  }
172
- const header = await this.readSessionHeader(sessionId);
173
- const location = persistence.locate(header);
174
- await rm(dirname(location.path), { recursive: true, force: true });
175
- }
139
+ const header = await this.readSessionHeader(sessionId);
140
+ const location = persistence.locate(header);
141
+ if (location === void 0 || typeof location.path !== "string") {
142
+ throw new Error(`cannot delete session "${sessionId}": the session persistence backend could not resolve its transcript directory`);
143
+ }
144
+ const transcriptDir = dirname(location.path);
145
+ try {
146
+ await rm(transcriptDir, { recursive: true, force: true });
147
+ } catch (error) {
148
+ const message = `cannot delete session "${sessionId}": transcript directory "${transcriptDir}" remains after bookkeeping cleanup`;
149
+ this.ctx.logger.warn(`archive-manager: ${message}: ${String(error)}`);
150
+ throw new Error(`${message}: ${String(error)}`, { cause: error });
151
+ }
152
+ }
176
153
  /** Drop the id from every workspace record and refresh entity snapshots. */
177
154
  async removeFromWorkspaceAccounts(sessionId) {
178
155
  const table = this.requireTable();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@michengai/dsh-archive-manager",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "NPM-installable DSH Web plugin for managing archived sessions.",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@11.21.0",
@@ -28,7 +28,21 @@
28
28
  "test": "node --test test/*.test.mjs",
29
29
  "pack:check": "pnpm pack --dry-run",
30
30
  "prepublishOnly": "pnpm build && pnpm test"
31
- },
31
+ },
32
+ "devDependencies": {
33
+ "@deepseek-ai/cordis": "4.0.1",
34
+ "@deepseek-ai/dsh-api-gateway": "0.1.0-rc.6",
35
+ "@deepseek-ai/dsh-client-runtime": "0.1.0-rc.6",
36
+ "@deepseek-ai/dsh-client-ui-slots": "0.1.0-rc.6",
37
+ "@deepseek-ai/dsh-client-web-react": "0.1.0-rc.6",
38
+ "@deepseek-ai/dsh-session-projection-cache": "0.1.0-rc.6",
39
+ "@deepseek-ai/dsh-spill-local": "0.1.0-rc.6",
40
+ "@deepseek-ai/dsh-typert-protocol": "0.1.0-rc.6",
41
+ "@deepseek-ai/dsh-typert-registry": "0.1.0-rc.6",
42
+ "@deepseek-ai/dsh-workspace": "0.1.0-rc.6",
43
+ "react": "18.3.1",
44
+ "react-dom": "18.3.1"
45
+ },
32
46
  "license": "Apache-2.0",
33
47
  "dsh": {
34
48
  "bundle": {