@deepseek-ai/dsh-workspace 0.1.1-rc.2 → 0.1.2-alpha.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/README.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/workspace/workspace/README.md
5
- README.md: c1c4309efdc85f4bc7a6b0f128df6d9f4f810ae1
6
- README.zh.md: 4ba66cb5bb83a86cec00b33769f0ac85fef41190
5
+ README.md: 49ca861742fe9ea5f36edbaeb8d9700c475b1d50
6
+ README.zh.md: 707044970b716577d2f1f95e558c341e217985c4
package/README.md CHANGED
@@ -1,27 +1,139 @@
1
+ ---
2
+ description: "Workspace entity registry (ctx.workspaceRegistry) for hosts choosing, mounting, or debugging durable workspace records and header-validated session membership."
3
+ kind: "package-reference"
4
+ ---
5
+
1
6
  # @deepseek-ai/dsh-workspace
2
7
 
3
8
  English | [中文](README.zh.md)
4
9
 
5
- Workspace entity registry (`ctx.workspaceRegistry`) for the DeepSeek Harness: durable workspace records, stable workspace order, and a newest-first candidate session index stored through the domain data form. Consumers see the `Workspace` interface; the entity implementation stays package-private.
10
+ ## Summary
11
+
12
+ `dsh-workspace` gives a host a persistent set of workspaces: named user directories, each with the sessions that ran in it, kept in a stable order across restarts. With it, a UI can show a sidebar of projects, attach sessions to the right project, hide a session from the grouping without losing it, and remove a project — removal never deletes the folder or the session histories, which become ungrouped. Use it in GUI or host compositions that need durable project grouping; headless and minimal runs can omit it entirely. The package is host-side only: the model, tools, and agent loop never see it, so it adds no tokens, prompts, or request context. It needs a session store and a persistence backend mounted alongside it; setup is a few composition rows.
13
+
14
+ ## Table of Contents
15
+
16
+ - [Use this package](#use-this-package)
17
+ - [Understand the implementation](#understand-the-implementation)
18
+ - [Further Exploration](#further-exploration)
19
+ - [Model Experience](#model-experience)
20
+ - [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
21
+ - [Dev Note](#dev-note)
22
+
23
+ -----
24
+
25
+ <a id="use-this-package"></a>
26
+ ## Use this package
27
+
28
+ Use this package to give the product a project list: named directories the user works in, the sessions that ran in each, a stable order, and a way to hide sessions without losing them. The API contracts behind each action live in the implementation section.
29
+
30
+ ### When to use it
31
+
32
+ Use it when the product shows a persistent workspace surface — a sidebar, session grouping, or automation that names directories and orders them. It is invisible to the model, so it adds no token or request cost. Skip it when there is no grouping surface; nothing else in the harness needs it.
33
+
34
+ ### Setting up
35
+
36
+ The package takes no configuration of its own; it needs a session store, a session persistence backend, and the storage rows that keep its records. A minimal composition:
37
+
38
+ ```yaml
39
+ - name: '@deepseek-ai/dsh-session'
40
+ - name: '@deepseek-ai/dsh-session-persistence-jsonl'
41
+ - name: '@deepseek-ai/dsh-storage'
42
+ - name: '@deepseek-ai/dsh-storage-json'
43
+ - name: '@deepseek-ai/dsh-storage-domain'
44
+ config:
45
+ backend: json
46
+ - name: '@deepseek-ai/dsh-workspace'
47
+ ```
48
+
49
+ With these rows mounted, creating a project shows up in the list immediately and survives a restart; the first start also groups existing sessions by the directory they ran in. If a required peer is missing, the workspace feature stays unavailable until it is mounted.
50
+
51
+ ### Creating and ordering projects
52
+
53
+ Create a project from any directory that exists: give its path and an optional title, and the project appears in the list, newest first. A path that does not exist, or a file instead of a directory, is rejected and nothing changes; creating a project for a directory that already has one returns the existing project unchanged. Rename a project at any time, and move it to any position in the list:
54
+
55
+ ```text
56
+ // Host consumer code, after the composition above is loaded:
57
+ const project = await ctx.workspaceRegistry.create('/path/to/dir', 'My Project')
58
+ await project.setTitle('Renamed')
59
+ ctx.workspaceRegistry.list() // shows the project, newest first
60
+ ```
61
+
62
+ ### Grouping sessions under a project
63
+
64
+ A session joins the project of the directory it runs in: create a session in a project's directory and it appears under that project, newest first. A session can only belong to one project. A session whose directory cannot be validated — no recorded directory, or a moved or deleted folder — cannot join and stays ungrouped.
65
+
66
+ ### Hiding sessions and removing projects
67
+
68
+ Hide a session from the grouping when it should stop appearing there: it disappears from the visible list, while its session, history, and place in the project stay intact. Remove a project when it is no longer needed: it leaves the list, and its folder, files, and session histories are never touched — those sessions become ungrouped. Adding the same directory again afterwards starts a fresh project without the old sessions.
69
+
70
+ -----
71
+
72
+ <a id="understand-the-implementation"></a>
73
+ ## Understand the implementation
74
+
75
+ <details>
76
+ <summary>Implementation internals — click to expand</summary>
77
+
78
+ This section explains the design decisions behind the feature and points at the code that realizes them; the observable behavior is fully covered in [Use this package](#use-this-package).
6
79
 
7
- The entity/storage rationale lives in the [domain Agent Note](../../../.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.md); header-only bootstrap and GUI ordering live in the [Workspace UI product-flow Agent Note](../../../.agents/notes/implemented/feature/2026-07-25-workspace-ui-product-flow.md).
80
+ ### Design philosophy
8
81
 
9
- ## Shape
82
+ - **One record per canonical path.** `fs.realpath` is the single uniqueness canon: paths are stored canonicalized, so a symlink to an owned directory collides, and uniqueness is string equality of canonical paths.
83
+ - **Membership is ownership plus a live cwd fact.** The record's ordered `sessionIds` is the ownership truth; the startup header index validates it, and `sessionIds` filters on read while the next mutation prunes durably.
84
+ - **Header-only reads.** Bootstrap and attach validation read `SessionHeader` fields only; event bodies are never loaded.
85
+ - **Two-write mutations with an explicit marker.** Create and delete persist a `pendingMutation` marker before the record/order pair can diverge, so startup completes exactly the interrupted operation and unmarked divergence fails loud as corruption.
86
+ - **Serialized writes.** Registry operations run on one operation chain; entity mutations go through `table.update` on the domain write chain, stamping `updatedAt` and deciding membership at their chain slot.
10
87
 
11
- - `ctx.workspaceRegistry.create(path, title?)` — canonicalizes `path` via `fs.realpath`, rejects a nonexistent or non-directory path, creates at most one record per canonical path, and prepends a new record to durable workspace order. Repeated calls for that path return the existing workspace without changing its title; different paths may share a display title.
12
- - `ctx.workspaceRegistry.get(id)` / `list()` / `resolveByPath(path)` — cache-served lookups. `list()` is synchronous and follows durable registry order; `resolveByPath` is async because it applies the same `realpath` canon and rejects a missing path rather than creating it.
13
- - `ctx.workspaceRegistry.insertBefore(id, before?)` — moves a registered Workspace within durable registry order, DOM-insertBefore-like: before the anchor, or appended when the anchor is omitted. A source or anchor absent from the registry rejects without writing; a self-anchor or move to the current position resolves without writing. The returned id list is the complete committed order.
14
- - `ctx.workspaceRegistry.delete(id)` — removes only the Workspace registration, its durable order entry, and its session account. Unknown ids return `false`; a removed record returns `true`. The directory, user files, live Sessions, and persisted session logs are never touched, so those Sessions become Ungrouped. A table-write failure restores the prior order and published entity.
15
- - `Workspace.attachSession(id)` — validates a live or persisted session header cwd against the workspace path and prepends a new id. Unknown sessions, absent/unresolvable/non-directory cwd values, and mismatches reject without writing. `detachSession` removes only the candidate index entry.
16
- - `Workspace.insertSessionBefore(id, before?)` — moves an accounted session within the manual order, DOM-insertBefore-like: before the anchor, or appended when the anchor is omitted. A session or anchor absent from the account rejects without writing; a move to the current position resolves without writing. Registry Workspace order never changes.
17
- - `ctx.workspaceRegistry.archiveSession(id)` / `archivedSessionIds` — the registry-global archive set, layered over workspace accounting: an archived session disappears from grouping surfaces but keeps its session log and its `sessionIds` slot, so a future unarchive restores its position. Archiving accepts any live or persisted session (accounted or Ungrouped), resolves without writing for an already archived id, and rejects an unknown id. State written before the field existed parses with an empty set.
18
- - `Workspace.sessionIds` — synchronous id-plus-canonical-cwd membership projection in durable candidate order. Missing headers, invalid cwd values, and mismatches are filtered; the next workspace mutation prunes them. A medium indexing one session under two workspaces, claiming one path from two records, or diverging from durable workspace order rejects at startup.
19
- - `Workspace.status()` — uncached directory check, `'ok' | 'missing-dir'`; a missing directory never mutates the record.
88
+ ### API behavior
20
89
 
21
- `storageDomain` and `sessionPersistence` are required startup dependencies. An unavailable peer leaves the plugin pending and cannot commit an empty initialized marker. On the first successful start, the registry calls `SessionPersistence.list()` and uses only header `id`, `cwd`, and `createdAt` to group valid historical directories and persist initial order; it never reads event bodies. The initialized marker is written last, so partial bootstrap writes are reused safely after restart. Later cwd-only sessions remain Ungrouped.
90
+ The API is one small family with two owners: `WorkspaceRegistry` creates, orders, and deletes projects and manages their session accounting; the `Workspace` entity exposes the display title, directory status, and the session projection. Per-method contracts live in the code, not this README see [src/index.ts](src/index.ts) and [src/entity.ts](src/entity.ts).
22
91
 
23
- Create and delete persist an explicit pending-mutation marker before their record and order can diverge. Startup completes only the marked mutation, then clears the marker; an unmarked order/table mismatch remains unexplained corruption and fails loud. Deleting and re-registering the same path creates a fresh Workspace id and does not automatically re-adopt the retained Sessions.
92
+ ### Source map
24
93
 
94
+ | File | Role |
95
+ |---|---|
96
+ | [`src/index.ts`](src/index.ts) | Plugin entry: `WorkspaceRegistry` service, header index, bootstrap, operation serialization |
97
+ | [`src/entity.ts`](src/entity.ts) | Package-private `Workspace` implementation and its single `mutate` write path |
98
+ | [`src/spec.ts`](src/spec.ts) | Domain declaration: record schema, registry state, `defineDomain` spec |
99
+ | [`src/types.ts`](src/types.ts) | Public `Workspace` interface and `WorkspaceId` brand |
100
+ | [`src/paths.ts`](src/paths.ts) | The `realpath` uniqueness canon |
101
+ | [`src/invariant.ts`](src/invariant.ts) | Invariant companion: the entity cache mirrors the durable table |
102
+
103
+ ### Durable shape
104
+
105
+ The registry opens the `workspace` domain (version 2): a `workspaces` table keyed by `WorkspaceId` plus one global state holding `workspaceIds` (the authoritative display order), `archivedSessionIds`, and the optional `pendingMutation` marker. Records written before `archivedSessionIds` existed parse with an empty set through the schema default.
106
+
107
+ ### Lifecycle
108
+
109
+ On start, the registry opens the domain, completes a marked mutation if one is pending, validates stored state — duplicate paths, duplicate session accounts, and order drift all fail loud — and, when not yet initialized, bootstraps history from persisted headers before writing the initialized marker last, so an interrupted bootstrap resumes safely. A fresh empty registry is real once initialized; it never re-bootstraps.
110
+
111
+ ### Failure and recovery
112
+
113
+ A create or delete whose second write fails rolls the cache and the prior order back; when both the operation and its rollback fail, the durable marker still names the interrupted operation and the next startup completes or rolls it back. A committed delete whose marker cleanup fails still reports success, and the next startup clears the marker idempotently.
114
+
115
+ ### Invariant
116
+
117
+ The `workspace-invariant` companion registers the owned relationship: every durable `domain/changed` for the `workspaces` table must name a record the entity cache already holds — a delete is valid only after the registry removed the entity from its cache, so a bypassing write path fails the invariant.
118
+
119
+ </details>
120
+
121
+ -----
122
+
123
+ <a id="further-exploration"></a>
124
+ ## Further Exploration
125
+
126
+ Read these pages when this package's view is not enough: the subsystem reference is the authoritative feature contract, and the Agent Notes record why projects start from session history and why removal is non-destructive.
127
+
128
+ - [Workspace subsystem](../../../docs/subsystems/workspace.md) — the feature contract for projects and their sessions, and the generated API for the workspace service.
129
+ - [Workspace package map](../README.md) — the group's single package and its repository position.
130
+ - [domain KV storage Agent Note](../../../.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.md) — why project records use the domain data form.
131
+ - [Workspace UI product-flow Agent Note](../../../.agents/notes/implemented/feature/2026-07-25-workspace-ui-product-flow.md) — how the first start builds projects from session history and how the GUI orders them.
132
+ - [Workspace registration deletion decision](../../../.agents/notes/implemented/feature/2026-07-27-workspace-registration-deletion.md) — why removing a project never deletes its folder or sessions.
133
+
134
+ -----
135
+
136
+ <a id="model-experience"></a>
25
137
  ## Model Experience
26
138
 
27
139
  ### Workspace records and session accounts
@@ -40,5 +152,27 @@ Independent of live requests: the package never touches a request prefix, so it
40
152
 
41
153
  ## Known Limitations and Deferred Work
42
154
 
43
- - Session deletion and destructive folder removal are separate, absent capabilities; Workspace registration deletion never substitutes for either ([decision](../../../.agents/notes/implemented/feature/2026-07-27-workspace-registration-deletion.md)).
44
- - The header index refreshes at startup and when attach must resolve an uncached persisted id; deletion or cwd damage performed by another process is observed after the next refresh or restart.
155
+ <a id="known-limitations-and-deferred-work"></a>
156
+
157
+
158
+ These limits define when the project list is a poor fit or needs special operational care. They are current package constraints, not a task backlog.
159
+
160
+ - **Removal never deletes data** — removing a project leaves its folder, files, and session histories in place; those sessions become ungrouped, and session deletion or folder removal are separate, absent capabilities ([decision](../../../.agents/notes/implemented/feature/2026-07-27-workspace-registration-deletion.md)).
161
+ - **A session joins only with a recorded directory** — a session belongs to a project only when its record carries a directory that resolves to the project's path; sessions without one stay ungrouped, and a session from another directory cannot be moved in.
162
+ - **External changes are seen late** — if another process deletes or damages a directory, the project reflects it only at the next refresh or restart.
163
+ - **Archiving is one-way** — a hidden session keeps its history and its place, but no unarchive action exists yet; the archive set is a durable display filter.
164
+ - **Re-adding a directory starts fresh** — after removal, adding the same directory again creates a new project with an empty session list; the old sessions do not come back automatically.
165
+
166
+ <a id="dev-note"></a>
167
+ ### Dev Note
168
+
169
+ <details>
170
+ <summary>Working context for maintainers — click to expand</summary>
171
+
172
+ This Dev Note is working context for maintainers: open questions and directions that are not decided. It is explicitly non-authoritative — shipped behavior, limits, and accepted rationale live in the sections above, the package code, and the linked Agent Notes.
173
+
174
+ #### Open: the `create(path, title?)` title parameter
175
+
176
+ The `title` parameter has no production caller since the gateway's create-by-name branch was removed; a code TODO proposes dropping the parameter and its `@param` clause together ([note](../../../.agents/notes/implemented/simplification/2026-07-31-one-route-to-add-a-workspace.md)).
177
+
178
+ </details>
package/README.zh.md CHANGED
@@ -1,32 +1,144 @@
1
+ ---
2
+ description: "面向选择、挂载或排查持久 workspace 记录与会话头校验成员资格的宿主的 Workspace 实体注册表(ctx.workspaceRegistry)说明。"
3
+ kind: "package-reference"
4
+ ---
5
+
1
6
  # @deepseek-ai/dsh-workspace
2
7
 
3
8
  [English](README.md) | 中文
4
9
 
5
- DeepSeek Harness 的 Workspace 实体注册表(`ctx.workspaceRegistry`):通过领域数据形式存储持久 workspace 记录、稳定 workspace 顺序和按新到旧排列的候选会话索引。消费方看到 `Workspace` 接口;实体实现保持包私有。
10
+ ## 概述
11
+
12
+ `dsh-workspace` 为宿主提供一组持久 workspace:命名用户目录,每个目录带有在其中运行的会话,并在重启之间保持稳定顺序。借助它,UI 可以显示项目侧边栏、把会话附加到正确的项目、把会话从分组中隐藏而不丢失它,以及移除项目——移除绝不会删除文件夹或会话历史,它们变成 Ungrouped。在需要持久项目分组的 GUI 或宿主组合中使用它;headless 与最小运行可以完全省略它。此包只面向宿主侧:模型、工具与 agent loop 永远不会看到它,因此不会增加任何 token、提示词或请求上下文。它需要会话存储与持久化后端一并挂载;设置只需几行组合配置。
13
+
14
+ ## 目录
15
+
16
+ - [使用本包](#use-this-package)
17
+ - [理解实现](#understand-the-implementation)
18
+ - [进一步探索](#further-exploration)
19
+ - [模型体验](#model-experience)
20
+ - [已知限制与延期工作](#known-limitations-and-deferred-work)
21
+ - [开发备注](#dev-note)
22
+
23
+ -----
24
+
25
+ <a id="use-this-package"></a>
26
+ ## 使用本包
27
+
28
+ 使用此包为产品提供项目列表:用户工作的命名目录、每个目录中运行的会话、稳定顺序,以及在不丢失会话的前提下将其隐藏的能力。每项操作背后的 API 约定放在实现章节中。
29
+
30
+ ### 何时使用
31
+
32
+ 当产品展示持久 workspace 界面——侧边栏、会话分组或需要命名并排序目录的自动化——时使用它。它对模型不可见,因此不增加任何 token 或请求成本。没有分组界面时跳过它;harness 中没有其他包需要它。
33
+
34
+ ### 设置
35
+
36
+ 此包本身不声明任何配置;它需要会话存储、会话持久化后端,以及保存其记录的存储行。最小组合如下:
37
+
38
+ ```yaml
39
+ - name: '@deepseek-ai/dsh-session'
40
+ - name: '@deepseek-ai/dsh-session-persistence-jsonl'
41
+ - name: '@deepseek-ai/dsh-storage'
42
+ - name: '@deepseek-ai/dsh-storage-json'
43
+ - name: '@deepseek-ai/dsh-storage-domain'
44
+ config:
45
+ backend: json
46
+ - name: '@deepseek-ai/dsh-workspace'
47
+ ```
48
+
49
+ 挂载这些行之后,创建项目会立即出现在列表中并在重启后保留;首次启动还会按会话运行的目录对既有会话分组。如果缺少某个必需依赖,workspace 功能会一直不可用,直到它被挂载。
50
+
51
+ ### 创建与排序项目
52
+
53
+ 从任何存在的目录创建项目:给出路径和可选标题,项目即出现在列表中,新到旧排列。不存在的路径或文件而非目录会被拒绝,且不会有任何变化;为已有项目的目录再次创建会原样返回现有项目。你可以随时重命名项目,并把它移动到列表中的任意位置:
54
+
55
+ ```text
56
+ // Host consumer code, after the composition above is loaded:
57
+ const project = await ctx.workspaceRegistry.create('/path/to/dir', 'My Project')
58
+ await project.setTitle('Renamed')
59
+ ctx.workspaceRegistry.list() // shows the project, newest first
60
+ ```
61
+
62
+ ### 将会话归入项目
63
+
64
+ 会话加入它运行目录所在的项目:在项目目录中创建会话,它就会出现在该项目下,新到旧排列。一个会话只能属于一个项目。目录无法校验的会话——没有记录目录,或目录被移动、删除——无法加入,保持 Ungrouped。
65
+
66
+ ### 隐藏会话与移除项目
67
+
68
+ 当会话不应再出现在分组中时隐藏它:它会从可见列表中消失,但其会话、历史与在项目中的位置都保持不变。项目不再需要时移除它:它离开列表,而其文件夹、文件与会话历史绝不受影响——这些会话变成 Ungrouped。之后再次添加同一目录会从空项目开始,不会带回旧会话。
69
+
70
+ -----
71
+
72
+ <a id="understand-the-implementation"></a>
73
+ ## 理解实现
74
+
75
+ <details>
76
+ <summary>实现细节——点击展开</summary>
77
+
78
+ 本节解释此功能背后的设计决策,并指出实现它们的代码位置;可观察行为已在[使用本包](#use-this-package)中完整说明。
6
79
 
7
- 实体/存储理由见[领域 Agent Note](../../../.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.zh.md);仅使用头部的引导初始化和 GUI 排序见 [Workspace UI 产品流 Agent Note](../../../.agents/notes/implemented/feature/2026-07-25-workspace-ui-product-flow.zh.md)。
80
+ ### 设计理念
8
81
 
9
- ## 结构
82
+ - **每个规范路径一条记录。** `fs.realpath` 是唯一的一套唯一性规范:路径以规范化形式存储,因此指向已被拥有目录的符号链接会与之冲突,唯一性即规范路径的字符串相等。
83
+ - **成员资格是所有权加实时 cwd 事实。** 记录的 `sessionIds` 顺序是所有权真源;启动时的头部索引校验它,`sessionIds` 在读取时过滤,下一次变更持久剪除。
84
+ - **仅读取头部。** 引导与 attach 校验只读取 `SessionHeader` 字段;事件正文绝不加载。
85
+ - **两次写入的变更带显式标记。** 创建与删除在记录/顺序对可能分叉之前先持久化 `pendingMutation` 标记,因此启动只补全被中断的操作,未标记的分叉作为损坏明确报错。
86
+ - **串行化写入。** 注册表操作跑在同一条操作链上;实体变更通过领域写链上的 `table.update` 执行,盖上 `updatedAt` 并在其链槽决定成员资格。
10
87
 
11
- - `ctx.workspaceRegistry.create(path, title?)`:规范化 `path` 时使用 `fs.realpath`,拒绝不存在或非目录的路径,每个规范路径最多创建一条记录,并将新记录前置到持久 workspace 顺序。对同一路径重复调用会返回现有 workspace,且不改变其标题;不同路径可以共用显示标题。
12
- - `ctx.workspaceRegistry.get(id)`/`list()`/`resolveByPath(path)`:由缓存提供的查找。`list()` 为同步操作,并遵循持久注册表顺序;`resolveByPath` 为异步操作,因为它采用相同的 `realpath` 规范化方式,并会拒绝缺失路径,而不是创建路径。
13
- - `ctx.workspaceRegistry.insertBefore(id, before?)`:在持久注册表顺序内移动一个已注册 Workspace,语义类似 DOM 的 insertBefore:插到锚点之前,省略锚点则追加到末尾。来源或锚点不在注册表中时拒绝且不写入;以自身为锚点或移动到当前位置时直接完成且不写入。返回的 id 列表是完整的已提交顺序。
14
- - `ctx.workspaceRegistry.delete(id)`:只移除 Workspace 注册记录、对应的持久顺序条目及会话归属记录。未知 id 返回 `false`,成功移除记录则返回 `true`。目录、用户文件、活跃会话和持久化会话日志绝不受影响,因此相关会话会进入 Ungrouped。表写入失败时会恢复原顺序和此前发布的实体。
15
- - `Workspace.attachSession(id)`:对照 workspace 路径验证实时或已持久化的会话头 cwd,并将新 id 前置。未知会话、缺失/无法解析/非目录的 cwd 值和不匹配情况都会在不写入的前提下被拒绝。`detachSession` 只移除候选索引条目。
16
- - `Workspace.insertSessionBefore(id, before?)`:在手动顺序内移动一个已记账的会话,语义类似 DOM 的 insertBefore:插到锚点之前,省略锚点则追加到末尾。会话或锚点不在记账中时拒绝且不写入;移动到当前位置时直接完成且不写入。注册表中的 Workspace 顺序绝不改变。
17
- - `ctx.workspaceRegistry.archiveSession(id)`/`archivedSessionIds`:覆盖在 workspace 记账之上的注册表级全局归档集合:被归档的会话从各分组视图中消失,但其会话日志和 `sessionIds` 席位保持不变,未来取消归档时可恢复原位置。归档接受任何实时或已持久化的会话(无论已记账还是 Ungrouped),对已归档的 id 直接完成而不写入,并拒绝未知 id。在该字段出现之前写入的状态解析为一个空集合。
18
- - `Workspace.sessionIds`:按持久候选顺序提供同步 id 加规范 cwd 成员投影。缺失头部、无效 cwd 值和不匹配情况都被过滤;下一次 workspace 变更会剪除它们。如果同一存储介质将一个会话索引到两个 workspace 下、用两条记录声明同一路径,或偏离持久 workspace 顺序,启动会被拒绝。
19
- - `Workspace.status()`:未缓存的目录检查,返回 `'ok' | 'missing-dir'`;目录缺失绝不会改动记录。
88
+ ### API 行为
20
89
 
21
- `storageDomain` `sessionPersistence` 是启动必需依赖。任一依赖服务不可用时,插件保持待处理,且不能提交空的已初始化标记。首次成功启动时,注册表调用 `SessionPersistence.list()`,仅使用头部 `id`、`cwd` 和 `createdAt` 对有效历史目录分组并持久化初始顺序;它绝不读取事件正文。已初始化标记最后写入,因此重启后可安全复用引导初始化期间的部分写入。后续仅能通过 cwd 识别的会话仍属于 Ungrouped
90
+ API 是一个由两个所有者构成的小家族:`WorkspaceRegistry` 负责创建、排序与删除项目并管理其会话记账;`Workspace` 实体暴露显示标题、目录状态与会话投影。各方法的精确约定在代码中,而非本 README——参见 [src/index.ts](src/index.ts) [src/entity.ts](src/entity.ts)
22
91
 
23
- 创建和删除操作会在记录和顺序可能分叉之前,先持久化明确的待处理变更标记。启动时只补全该标记所指明的变更,随后清除标记;没有标记的顺序/表不一致仍属于来源不明的损坏,并会明确报错。删除后重新注册同一路径会生成新的 Workspace id,且不会自动重新接纳保留下来的会话。
92
+ ### 源码地图
24
93
 
94
+ | 文件 | 职责 |
95
+ |---|---|
96
+ | [`src/index.ts`](src/index.ts) | 插件入口:`WorkspaceRegistry` 服务、头部索引、引导、操作串行化 |
97
+ | [`src/entity.ts`](src/entity.ts) | 包私有 `Workspace` 实现及其唯一的 `mutate` 写入路径 |
98
+ | [`src/spec.ts`](src/spec.ts) | 领域声明:记录 schema、注册表状态、`defineDomain` 规范 |
99
+ | [`src/types.ts`](src/types.ts) | 公开 `Workspace` 接口与 `WorkspaceId` 品牌 |
100
+ | [`src/paths.ts`](src/paths.ts) | `realpath` 唯一性规范 |
101
+ | [`src/invariant.ts`](src/invariant.ts) | 不变式伴生插件:实体缓存镜像持久表 |
102
+
103
+ ### 持久形态
104
+
105
+ 注册表打开 `workspace` 领域(版本 2):一张以 `WorkspaceId` 为键的 `workspaces` 表,加上一个持有 `workspaceIds`(权威显示顺序)、`archivedSessionIds` 与可选 `pendingMutation` 标记的全局状态。在 `archivedSessionIds` 存在之前写入的记录会通过 schema 默认值解析为空集合。
106
+
107
+ ### 生命周期
108
+
109
+ 启动时,注册表打开领域、若存在标记则补全被标记的变更、校验已存状态——重复路径、重复会话账本与顺序漂移都会明确报错——并在尚未初始化时先凭持久化头部引导历史、最后写入已初始化标记,因此被中断的引导可以安全恢复。全新空注册表一旦初始化即为真,绝不会再次引导。
110
+
111
+ ### 失败与恢复
112
+
113
+ 创建或删除的第二次写入失败时,缓存与先前顺序会回滚;当操作与回滚都失败时,持久标记仍指明被中断的操作,下一次启动会补全或回滚它。已提交的删除即使标记清理失败仍报告成功,下一次启动会幂等地清除该标记。
114
+
115
+ ### 不变式
116
+
117
+ `workspace-invariant` 伴生插件注册归属关系:`workspaces` 表的每个持久 `domain/changed` 都必须指向实体缓存已持有的记录——只有在注册表从缓存移除实体之后删除才有效,因此绕过注册表的写入路径会触发不变式失败。
118
+
119
+ </details>
120
+
121
+ -----
122
+
123
+ <a id="further-exploration"></a>
124
+ ## 进一步探索
125
+
126
+ 当本包的视角不够用时阅读以下页面:子系统参考是权威的功能约定,Agent Note 记录了项目为何从会话历史起步、以及移除为何是非破坏性的。
127
+
128
+ - [Workspace 子系统](../../../docs/subsystems/workspace.zh.md)——项目及其会话的功能约定,以及 workspace 服务的生成 API。
129
+ - [Workspace 包映射](../README.zh.md)——本组唯一的包及其仓库位置。
130
+ - [领域 KV 存储 Agent Note](../../../.agents/notes/proposed/architecture/2026-07-24-domain-kv-storage-and-workspace.zh.md)——为什么项目记录使用领域数据形式。
131
+ - [Workspace UI 产品流 Agent Note](../../../.agents/notes/implemented/feature/2026-07-25-workspace-ui-product-flow.zh.md)——首次启动如何从会话历史构建项目,以及 GUI 如何排序。
132
+ - [删除 Workspace 注册记录决策](../../../.agents/notes/implemented/feature/2026-07-27-workspace-registration-deletion.zh.md)——为什么移除项目绝不会删除其文件夹或会话。
133
+
134
+ -----
135
+
136
+ <a id="model-experience"></a>
25
137
  ## 模型体验
26
138
 
27
- ### Workspace 记录与会话记账
139
+ ### Workspace 记录与会话账本
28
140
 
29
- #### 模型看到的内容
141
+ #### 模型看到什么
30
142
 
31
143
  没有。`ctx.workspaceRegistry` 只向宿主侧消费方提供 workspace 记录:此包不注册工具、不注入提示词、不写入会话事件,因此没有请求字段会携带此包数据。
32
144
 
@@ -38,7 +150,29 @@ DeepSeek Harness 的 Workspace 实体注册表(`ctx.workspaceRegistry`):
38
150
 
39
151
  与实时请求无关:此包绝不触及请求前缀,因此不会使提供方缓存复用失效。
40
152
 
41
- ## 已知限制与暂缓事项
153
+ ## 已知限制与延期工作
154
+
155
+ <a id="known-limitations-and-deferred-work"></a>
156
+
157
+
158
+ 这些限制说明项目列表何时不合适,或何时需要特别的运维注意。它们是当前包约束,不是任务积压。
159
+
160
+ - **移除绝不删除数据**——移除项目会保留其文件夹、文件与会话历史;这些会话变成 Ungrouped,而会话删除与文件夹移除是彼此独立且尚未提供的功能(参见[决策记录](../../../.agents/notes/implemented/feature/2026-07-27-workspace-registration-deletion.zh.md))。
161
+ - **只有带记录目录的会话才能加入**——只有记录中带有可解析为项目路径的目录的会话才属于项目;没有目录的会话保持 Ungrouped,来自其他目录的会话无法移入。
162
+ - **外部变更延迟可见**——如果另一进程删除或损坏目录,项目只能在下次刷新或重启后反映出来。
163
+ - **归档是单向的**——被隐藏的会话保留其历史与位置,但目前没有取消归档操作;归档集合是持久的显示过滤器。
164
+ - **重新添加目录从空开始**——移除后再次添加同一目录会创建空会话列表的新项目;旧会话不会自动回来。
165
+
166
+ <a id="dev-note"></a>
167
+ ### 开发备注
168
+
169
+ <details>
170
+ <summary>维护者的工作上下文——点击展开</summary>
171
+
172
+ 本开发备注是维护者的工作上下文:开放问题与尚未决定的探索方向。它明确不具权威性——已交付的行为、限制与既定理由以上文、包代码和相关 Agent Note 为准。
173
+
174
+ #### 开放:`create(path, title?)` 的 title 参数
175
+
176
+ 网关的按名称创建分支移除后,`title` 参数已无生产调用方;代码中的 TODO 提议把该参数与其 `@param` 子句一并移除(参见[笔记](../../../.agents/notes/implemented/simplification/2026-07-31-one-route-to-add-a-workspace.zh.md))。
42
177
 
43
- - 会话删除与破坏性的文件夹移除是彼此独立且尚未提供的功能;删除 Workspace 注册记录绝不能替代二者(参见[决策记录](../../../.agents/notes/implemented/feature/2026-07-27-workspace-registration-deletion.zh.md))。
44
- - 头部索引会在启动时刷新,也会在 attach 必须解析未缓存持久 id 时刷新;另一进程执行的删除或造成的 cwd 损坏会在下次刷新或重启后被发现。
178
+ </details>
package/lib/index.js CHANGED
@@ -3,7 +3,7 @@ import { realpath, stat } from "node:fs/promises";
3
3
  import { basename } from "node:path";
4
4
  import { Service } from "@deepseek-ai/cordis";
5
5
  import { z } from "zod";
6
- import { SessionId } from "@deepseek-ai/dsh-session";
6
+ import { brandString } from "@deepseek-ai/dsh-brand";
7
7
  import { defineDomain, domainTable } from "@deepseek-ai/dsh-storage-domain";
8
8
  //#region lib/types/paths.js
9
9
  /**
@@ -170,8 +170,8 @@ var WorkspaceEntity = class {
170
170
  //#region lib/types/spec.js
171
171
  /**
172
172
  * The workspace domain declaration: record schema and the `defineDomain` spec
173
- * the registry opens. The zod schema is the durable-boundary validator today
174
- * and the direct source of the RPC wire projection in a later phase.
173
+ * the registry opens. The zod schema validates the shipped format at the
174
+ * durability boundary and is the direct source of a future RPC wire projection.
175
175
  * @module @deepseek-ai/dsh-workspace/src/spec
176
176
  */
177
177
  /** Workspace id schema at the durable boundary; branding has no runtime representation. */
@@ -184,7 +184,7 @@ const workspaceId = z.string().transform((value) => value);
184
184
  const workspaceRecord = z.object({
185
185
  path: z.string(),
186
186
  title: z.string(),
187
- sessionIds: z.array(z.string().transform(SessionId)),
187
+ sessionIds: z.array(z.string().transform((value) => brandString(value))),
188
188
  createdAt: z.string(),
189
189
  updatedAt: z.string()
190
190
  });
@@ -212,7 +212,7 @@ const workspacePendingMutation = z.discriminatedUnion("operation", [z.object({
212
212
  const workspaceDomainState = z.object({
213
213
  initialized: z.boolean(),
214
214
  workspaceIds: z.array(workspaceId),
215
- archivedSessionIds: z.array(z.string().transform(SessionId)).default([]),
215
+ archivedSessionIds: z.array(z.string().transform((value) => brandString(value))).default([]),
216
216
  pendingMutation: workspacePendingMutation.optional()
217
217
  });
218
218
  /**
package/lib/invariant.js CHANGED
@@ -3,14 +3,14 @@ import "node:fs/promises";
3
3
  import "node:path";
4
4
  import { Service } from "@deepseek-ai/cordis";
5
5
  import { z } from "zod";
6
- import { SessionId } from "@deepseek-ai/dsh-session";
6
+ import { brandString } from "@deepseek-ai/dsh-brand";
7
7
  import { defineDomain, domainTable } from "@deepseek-ai/dsh-storage-domain";
8
8
  //#endregion
9
9
  //#region src/spec.ts
10
10
  /**
11
11
  * The workspace domain declaration: record schema and the `defineDomain` spec
12
- * the registry opens. The zod schema is the durable-boundary validator today
13
- * and the direct source of the RPC wire projection in a later phase.
12
+ * the registry opens. The zod schema validates the shipped format at the
13
+ * durability boundary and is the direct source of a future RPC wire projection.
14
14
  * @module @deepseek-ai/dsh-workspace/src/spec
15
15
  */
16
16
  /** Workspace id schema at the durable boundary; branding has no runtime representation. */
@@ -23,7 +23,7 @@ const workspaceId = z.string().transform((value) => value);
23
23
  const workspaceRecord = z.object({
24
24
  path: z.string(),
25
25
  title: z.string(),
26
- sessionIds: z.array(z.string().transform(SessionId)),
26
+ sessionIds: z.array(z.string().transform((value) => brandString(value))),
27
27
  createdAt: z.string(),
28
28
  updatedAt: z.string()
29
29
  });
@@ -46,7 +46,7 @@ defineDomain({
46
46
  schema: z.object({
47
47
  initialized: z.boolean(),
48
48
  workspaceIds: z.array(workspaceId),
49
- archivedSessionIds: z.array(z.string().transform(SessionId)).default([]),
49
+ archivedSessionIds: z.array(z.string().transform((value) => brandString(value))).default([]),
50
50
  pendingMutation: workspacePendingMutation.optional()
51
51
  }),
52
52
  initial: {
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * The workspace domain declaration: record schema and the `defineDomain` spec
3
- * the registry opens. The zod schema is the durable-boundary validator today
4
- * and the direct source of the RPC wire projection in a later phase.
3
+ * the registry opens. The zod schema validates the shipped format at the
4
+ * durability boundary and is the direct source of a future RPC wire projection.
5
5
  * @module @deepseek-ai/dsh-workspace/src/spec
6
6
  */
7
7
  import { z } from 'zod';
8
- import { SessionId } from '@deepseek-ai/dsh-session';
8
+ import type { SessionId } from '@deepseek-ai/dsh-session';
9
9
  import type { WorkspaceId } from './types.ts';
10
10
  /**
11
11
  * Durable shape of one workspace record. `path` is the `fs.realpath` canon
package/lib/types/spec.js CHANGED
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * The workspace domain declaration: record schema and the `defineDomain` spec
3
- * the registry opens. The zod schema is the durable-boundary validator today
4
- * and the direct source of the RPC wire projection in a later phase.
3
+ * the registry opens. The zod schema validates the shipped format at the
4
+ * durability boundary and is the direct source of a future RPC wire projection.
5
5
  * @module @deepseek-ai/dsh-workspace/src/spec
6
6
  */
7
7
  import { z } from 'zod';
8
- import { SessionId } from '@deepseek-ai/dsh-session';
8
+ import { brandString } from '@deepseek-ai/dsh-brand';
9
9
  import { defineDomain, domainTable } from '@deepseek-ai/dsh-storage-domain';
10
10
  /** Workspace id schema at the durable boundary; branding has no runtime representation. */
11
11
  const workspaceId = z.string().transform(value => value);
@@ -17,7 +17,7 @@ const workspaceId = z.string().transform(value => value);
17
17
  export const workspaceRecord = z.object({
18
18
  path: z.string(),
19
19
  title: z.string(),
20
- sessionIds: z.array(z.string().transform(SessionId)),
20
+ sessionIds: z.array(z.string().transform(value => brandString(value))),
21
21
  createdAt: z.string(),
22
22
  updatedAt: z.string(),
23
23
  });
@@ -42,7 +42,7 @@ const workspacePendingMutation = z.discriminatedUnion('operation', [
42
42
  export const workspaceDomainState = z.object({
43
43
  initialized: z.boolean(),
44
44
  workspaceIds: z.array(workspaceId),
45
- archivedSessionIds: z.array(z.string().transform(SessionId)).default([]),
45
+ archivedSessionIds: z.array(z.string().transform(value => brandString(value))).default([]),
46
46
  pendingMutation: workspacePendingMutation.optional(),
47
47
  });
48
48
  /**
@@ -5,12 +5,20 @@
5
5
  * @module @deepseek-ai/dsh-workspace/src/types
6
6
  */
7
7
  import type { Branded } from '@deepseek-ai/dsh-brand';
8
- import type { SessionId } from '@deepseek-ai/dsh-session';
8
+ import type { SessionId } from '@deepseek-ai/dsh-session/types';
9
9
  /**
10
10
  * Identifies one workspace record. A generated uuid, never the path: path
11
11
  * normalization rewrites paths, and a reference anchor must stay stable.
12
12
  */
13
13
  export type WorkspaceId = Branded<'WorkspaceId'>;
14
+ declare module '@deepseek-ai/dsh-typert-protocol' {
15
+ interface RemoteErrorDetailsMap {
16
+ /** No registration carries that Workspace identity. */
17
+ 'workspace/not-found': {
18
+ readonly workspaceId: WorkspaceId;
19
+ };
20
+ }
21
+ }
14
22
  /**
15
23
  * One workspace: a stable id over an existing directory, a display title, and
16
24
  * an ordered candidate account of sessions. Membership requires both an id in
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deepseek-ai/dsh-workspace",
3
3
  "description": "Workspace entity registry (ctx.workspaceRegistry): durable workspace records with validated session attachment over the domain data form for the DeepSeek Harness",
4
- "version": "0.1.1-rc.2",
4
+ "version": "0.1.2-alpha.2",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -37,24 +37,25 @@
37
37
  ],
38
38
  "license": "MIT",
39
39
  "peerDependencies": {
40
- "@deepseek-ai/dsh-brand": "^0.1.1-rc.2",
41
- "@deepseek-ai/dsh-storage-domain": "^0.1.1-rc.2",
42
- "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
43
- "@deepseek-ai/dsh-session": "^0.1.1-rc.2",
44
- "@deepseek-ai/dsh-session-persistence": "^0.1.1-rc.2",
45
- "@deepseek-ai/dsh-storage": "^0.1.1-rc.2",
46
- "@deepseek-ai/cordis": "^4.0.1"
40
+ "@deepseek-ai/cordis": "^4.0.2",
41
+ "@deepseek-ai/dsh-invariants": "^0.1.2-alpha.2",
42
+ "@deepseek-ai/dsh-session": "^0.1.2-alpha.2",
43
+ "@deepseek-ai/dsh-session-persistence": "^0.1.2-alpha.2",
44
+ "@deepseek-ai/dsh-storage": "^0.1.2-alpha.2",
45
+ "@deepseek-ai/dsh-storage-domain": "^0.1.2-alpha.2",
46
+ "@deepseek-ai/dsh-typert-protocol": "^0.1.2-alpha.2"
47
47
  },
48
48
  "dependencies": {
49
- "zod": "^4.4.3"
49
+ "zod": "^4.4.3",
50
+ "@deepseek-ai/dsh-brand": "^0.1.2-alpha.2"
50
51
  },
51
52
  "devDependencies": {
52
- "@deepseek-ai/dsh-brand": "^0.1.1-rc.2",
53
- "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
54
- "@deepseek-ai/dsh-storage-domain": "^0.1.1-rc.2",
55
- "@deepseek-ai/dsh-session": "^0.1.1-rc.2",
56
- "@deepseek-ai/dsh-session-persistence": "^0.1.1-rc.2",
57
- "@deepseek-ai/dsh-storage": "^0.1.1-rc.2",
58
- "@deepseek-ai/cordis": "^4.0.1"
53
+ "@deepseek-ai/cordis": "^4.0.2",
54
+ "@deepseek-ai/dsh-session": "^0.1.2-alpha.2",
55
+ "@deepseek-ai/dsh-session-persistence": "^0.1.2-alpha.2",
56
+ "@deepseek-ai/dsh-storage-domain": "^0.1.2-alpha.2",
57
+ "@deepseek-ai/dsh-typert-protocol": "^0.1.2-alpha.2",
58
+ "@deepseek-ai/dsh-invariants": "^0.1.2-alpha.2",
59
+ "@deepseek-ai/dsh-storage": "^0.1.2-alpha.2"
59
60
  }
60
61
  }