@deepseek-ai/dsh-workspace 0.2.1-alpha.2 → 0.2.1-alpha.4

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
@@ -9,8 +9,8 @@
9
9
  en: 7a12adba4e7766e3
10
10
  zh: f6d5c8374f4512c8
11
11
  /deepseek-ai-dsh-workspace/summary:
12
- en: 58d66b238882b849
13
- zh: a427930afd293c6b
12
+ en: 30aea62cabd8e8a8
13
+ zh: b22b30ef08fd4713
14
14
  /deepseek-ai-dsh-workspace/table-of-contents:
15
15
  en: d152484eb41ac6b4
16
16
  zh: 09388d293f9be9cb
@@ -30,8 +30,8 @@
30
30
  en: 217475057b6506ed
31
31
  zh: be2312a29499c203
32
32
  /deepseek-ai-dsh-workspace/use-this-package/grouping-sessions-under-a-project:
33
- en: 510be21d012de28c
34
- zh: fdcb05447d6ba93c
33
+ en: 5a68da22516a6a3c
34
+ zh: a229d57acdab882c
35
35
  /deepseek-ai-dsh-workspace/use-this-package/hiding-and-restoring-sessions-and-removing-projects:
36
36
  en: 7b298aa5c48501d4
37
37
  zh: 8ffeec728c1c13fe
@@ -39,8 +39,8 @@
39
39
  en: 447ced401b3b14a8
40
40
  zh: 3ce2105605223945
41
41
  /deepseek-ai-dsh-workspace/understand-the-implementation/design-philosophy:
42
- en: e8ff536e19566447
43
- zh: 53ba4c15f10c1549
42
+ en: e969314497b99389
43
+ zh: 82eafc832617a1eb
44
44
  /deepseek-ai-dsh-workspace/understand-the-implementation/api-behavior:
45
45
  en: 69327adb9ea52680
46
46
  zh: fa32eea5019c7abb
@@ -75,8 +75,8 @@
75
75
  en: 524afbded27fb06b
76
76
  zh: 9dcf8539495e9c68
77
77
  /deepseek-ai-dsh-workspace/known-limitations-and-deferred-work:
78
- en: 85694227357bbd7c
79
- zh: 16ad935226c51022
78
+ en: 1a9c71a3dbfc4506
79
+ zh: 87b57414053f4d00
80
80
  /deepseek-ai-dsh-workspace/known-limitations-and-deferred-work/dev-note:
81
81
  en: ac1d08360a93382b
82
82
  zh: 6c58749bae9a6e65
package/README.md CHANGED
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
9
9
 
10
10
  ## Summary
11
11
 
12
- Use this package to keep an ordered, persistent list of project directories and the sessions run in each directory. Hosts can build project sidebars, hide sessions from grouping without deleting their histories, and remove projects without deleting folders, files, or sessions. Re-adding a removed directory creates a fresh project, while sessions whose directories cannot be validated remain ungrouped. Choose it for GUI or host workflows that need durable project grouping; it is invisible to models and adds no prompt or request-context cost, but requires session persistence and storage backends.
12
+ Use this package to keep an ordered, persistent list of project directories and the sessions run in each directory. Hosts can build project sidebars, hide sessions from grouping without deleting their histories, and remove projects without deleting folders, files, or sessions. Re-adding a removed directory creates a fresh project; an existing project's sessions stay grouped when its directory disappears. Choose it for GUI or host workflows that need durable project grouping; it is invisible to models and adds no prompt or request-context cost, but requires session persistence and storage backends.
13
13
 
14
14
  ## Table of Contents
15
15
 
@@ -70,7 +70,7 @@ The first successful registration records its identity durably. Repeated calls r
70
70
 
71
71
  ### Grouping sessions under a project
72
72
 
73
- 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.
73
+ 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 new session must have a recorded directory that resolves to the project's directory. Already-accounted sessions keep their project and order when their fully qualified recorded path is missing, including a dangling symlink; renaming or reordering the project does not remove that ownership. Missing headers, invalid recorded paths, existing mismatched directories, and filesystem failures other than ENOENT remain excluded. Keeping ownership does not recreate the directory or relax new-session attachment, so a fork can still fail to attach while the directory is missing.
74
74
 
75
75
  ### Hiding and restoring sessions, and removing projects
76
76
 
@@ -91,7 +91,7 @@ This section explains the design decisions behind the feature and points at the
91
91
  ### Design philosophy
92
92
 
93
93
  - **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.
94
- - **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.
94
+ - **Membership starts from the recorded account.** The header index validates ordered `sessionIds`; missing-directory candidates retain their account, while other invalid candidates are filtered on read and pruned by the next mutation. The strict canonical-path index used for history bootstrap includes existing directories only.
95
95
  - **Header-only reads.** Bootstrap and attach validation read `SessionHeader` fields only; event bodies are never loaded.
96
96
  - **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.
97
97
  - **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.
@@ -166,7 +166,7 @@ Independent of live requests: the package never touches a request prefix, so it
166
166
  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.
167
167
 
168
168
  - **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.
169
- - **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.
169
+ - **Missing paths retain recorded ownership, not verified symlink history** — a removed symlink and an incorrect but missing absolute cwd cannot be distinguished from existing records; both preserve an already-accounted id. No unaccounted session is assigned by this exception, and an account already removed from storage is not reconstructed.
170
170
  - **External changes are seen late** — if another process deletes or damages a directory, the project reflects it only at the next refresh or restart.
171
171
  - **Archive and unarchive enforce different session checks** — a restore only drops an id from the archive set, so an entry whose session is gone still unarchives and leaves no unknown referent; a restore of an id that is not archived resolves without writing, while `archiveSession` rejects a session that is neither live nor persisted.
172
172
  - **The activity check and the archive write are not one atomic step** — a turn that starts between the providers' answer and the durable write is hidden while running, and every model step whose `agent/pre-step` precedes the write still runs with its tool calls; the API Session Controller's gate ends the first step proposed after the write as `blocked`, so the exposure is bounded by that write's latency, in practice one model step.
package/README.zh.md CHANGED
@@ -9,7 +9,7 @@ kind: "package-reference"
9
9
 
10
10
  ## 概述
11
11
 
12
- 使用此包可以维护一个有序、持久的项目目录列表,以及在每个目录中运行的会话。宿主可以构建项目侧边栏、在不删除历史的情况下把会话从分组中隐藏,并在不删除文件夹、文件或会话的情况下移除项目。重新添加已移除的目录会创建一个全新项目,而目录无法校验的会话会保持 Ungrouped。需要持久项目分组的 GUI 或宿主工作流适合使用它;它对模型不可见,不增加提示词或请求上下文成本,但需要会话持久化与存储后端。
12
+ 使用此包可以维护一个有序、持久的项目目录列表,以及在每个目录中运行的会话。宿主可以构建项目侧边栏、在不删除历史的情况下把会话从分组中隐藏,并在不删除文件夹、文件或会话的情况下移除项目。重新添加已移除的目录会创建一个全新项目;已有项目的目录消失时,其会话仍保持原分组。需要持久项目分组的 GUI 或宿主工作流适合使用它;它对模型不可见,不增加提示词或请求上下文成本,但需要会话持久化与存储后端。
13
13
 
14
14
  ## 目录
15
15
 
@@ -70,7 +70,7 @@ ctx.workspaceRegistry.list() // shows the project, newest first
70
70
 
71
71
  ### 将会话归入项目
72
72
 
73
- 会话加入它运行目录所在的项目:在项目目录中创建会话,它就会出现在该项目下,新到旧排列。一个会话只能属于一个项目。目录无法校验的会话——没有记录目录,或目录被移动、删除——无法加入,保持 Ungrouped。
73
+ 会话加入它运行目录所在的项目:在项目目录中创建会话,它就会出现在该项目下,新到旧排列。一个会话只能属于一个项目。新会话必须记录可解析为项目目录的工作目录。已记账会话的完整绝对路径缺失时(包括悬空符号链接),仍保留原项目与顺序;重命名或排序项目不会移除该归属。头部缺失、无效记录路径、现存但不匹配的目录,以及 ENOENT 以外的文件系统错误仍会被排除。保留归属不会重建目录,也不放宽新会话挂接要求,因此目录缺失时 Fork 仍可能挂接失败。
74
74
 
75
75
  ### 隐藏、恢复会话与移除项目
76
76
 
@@ -91,7 +91,7 @@ Workspace 注册不意味着拥有其目录。未来的破坏性文件系统操
91
91
  ### 设计理念
92
92
 
93
93
  - **每个规范路径一条记录。** `fs.realpath` 是唯一的一套唯一性规范:路径以规范化形式存储,因此指向已有记录目录的符号链接会与之冲突,唯一性即规范路径的字符串相等。
94
- - **成员资格是所有权加实时 cwd 事实。** 记录的 `sessionIds` 顺序是所有权真源;启动时的头部索引校验它,`sessionIds` 在读取时过滤,下一次变更会持久化剪除无效项。
94
+ - **成员资格以已记录的归属为起点。** 头部索引校验有序的 `sessionIds`;目录缺失的候选保留归属,其他无效候选在读取时过滤,并在下一次变更时剪除。历史引导使用的严格规范路径索引只包含现存目录。
95
95
  - **仅读取头部。** 引导与 attach 校验只读取 `SessionHeader` 字段;事件正文绝不加载。
96
96
  - **两次写入的变更带显式标记。** 创建与删除在记录/顺序对可能分叉之前先持久化 `pendingMutation` 标记,因此启动只补全被中断的操作,未标记的分叉作为损坏明确报错。
97
97
  - **串行化写入。** 注册表操作跑在同一条操作链上;实体变更通过领域写链上的 `table.update` 执行,写入 `updatedAt`,并在其所在的链位置决定成员资格。
@@ -167,7 +167,7 @@ Workspace 注册不意味着拥有其目录。未来的破坏性文件系统操
167
167
  这些限制说明项目列表何时不合适,或何时需要特别的运维注意。它们是当前包约束,不是任务积压。
168
168
 
169
169
  - **移除绝不删除数据**——移除项目会保留其文件夹、文件与会话历史;这些会话变成 Ungrouped,而会话删除与文件夹移除是彼此独立且尚未提供的功能。
170
- - **只有带记录目录的会话才能加入**——只有记录中带有可解析为项目路径的目录的会话才属于项目;没有目录的会话保持 Ungrouped,来自其他目录的会话无法移入。
170
+ - **路径缺失时保留记录归属,而非重新验证符号链接历史**——现有记录无法区分被移除的符号链接与错误但不存在的绝对 cwd;两者均保留已记账 id。此例外不吸收未记账会话,也不重建已经从存储中删除的归属。
171
171
  - **外部变更延迟可见**——如果另一进程删除或损坏目录,项目只能在下次刷新或重启后反映出来。
172
172
  - **归档与取消归档执行不同的会话校验**——恢复只是从归档集合中移除 id,因此会话已不存在的条目仍能取消归档,也不会留下未知引用;对未归档 id 执行恢复不写盘即完成,而 `archiveSession` 会拒绝既非实时也未持久化的会话。
173
173
  - **活动检查与归档写入不是一个原子步骤**——在提供方作答与持久化写入之间开始的回合会在隐藏状态下运行,`agent/pre-step` 先于该写入的每个模型步连同其工具调用照常执行;API Session Controller 的门禁把写入之后提出的第一步以 `blocked` 收口,因此暴露面以该写入的时延为界,实际上是一个模型步。
package/lib/index.js CHANGED
@@ -100,7 +100,7 @@ var WorkspaceEntity = class {
100
100
  return this.record.updatedAt;
101
101
  }
102
102
  get sessionIds() {
103
- return this.record.sessionIds.filter((id) => this.host.sessionPath(id) === this.record.path);
103
+ return this.record.sessionIds.filter((id) => this.host.candidateMatches(id, this.record.path));
104
104
  }
105
105
  async setTitle(title) {
106
106
  await this.mutate((record) => ({
@@ -161,7 +161,7 @@ var WorkspaceEntity = class {
161
161
  /**
162
162
  * The single write path: run `fn` on the domain write chain via
163
163
  * `table.update`, stamping `updatedAt` and pruning candidates that no
164
- * longer pass the id-plus-canonical-cwd membership check, then swap the
164
+ * longer pass the candidate membership check, then swap the
165
165
  * snapshot.
166
166
  *
167
167
  * `fn` sees the value current at its chain slot, so membership decisions
@@ -175,7 +175,7 @@ var WorkspaceEntity = class {
175
175
  try {
176
176
  next = await this.host.table().update(this.id, (current) => {
177
177
  const changed = fn(current);
178
- const sessionIds = changed.sessionIds.filter((id) => this.host.sessionPath(id) === changed.path);
178
+ const sessionIds = changed.sessionIds.filter((id) => this.host.candidateMatches(id, changed.path));
179
179
  if (changed === current && sessionIds.length === current.sessionIds.length) throw unchangedSentinel;
180
180
  return {
181
181
  ...changed,
@@ -359,14 +359,16 @@ var WorkspaceRegistry = class extends Service {
359
359
  entities = /* @__PURE__ */ new Map();
360
360
  headers = /* @__PURE__ */ new Map();
361
361
  sessionPaths = /* @__PURE__ */ new Map();
362
+ missingSessionPaths = /* @__PURE__ */ new Set();
362
363
  invalidSessionPaths = /* @__PURE__ */ new Map();
363
364
  operationTail = Promise.resolve();
364
365
  host = {
365
366
  table: () => this.requireTable(),
366
- sessionPath: (id) => this.sessionPaths.get(id),
367
+ candidateMatches: (id, path) => this.sessionPaths.get(id) === path || this.missingSessionPaths.has(id),
367
368
  readSessionHeader: (id) => this.readSessionHeader(id),
368
369
  rememberSessionPath: (id, path) => {
369
370
  this.sessionPaths.set(id, path);
371
+ this.missingSessionPaths.delete(id);
370
372
  this.invalidSessionPaths.delete(id);
371
373
  }
372
374
  };
@@ -863,6 +865,7 @@ var WorkspaceRegistry = class extends Service {
863
865
  async replaceHeaderIndex(headers) {
864
866
  this.headers.clear();
865
867
  this.sessionPaths.clear();
868
+ this.missingSessionPaths.clear();
866
869
  this.invalidSessionPaths.clear();
867
870
  await this.indexHeaders(headers);
868
871
  }
@@ -872,6 +875,7 @@ var WorkspaceRegistry = class extends Service {
872
875
  async indexHeader(header) {
873
876
  this.headers.set(header.id, header);
874
877
  this.sessionPaths.delete(header.id);
878
+ this.missingSessionPaths.delete(header.id);
875
879
  if (header.cwd === void 0) {
876
880
  this.invalidSessionPaths.set(header.id, "header has no cwd");
877
881
  return;
@@ -884,7 +888,8 @@ var WorkspaceRegistry = class extends Service {
884
888
  }
885
889
  this.sessionPaths.set(header.id, path);
886
890
  this.invalidSessionPaths.delete(header.id);
887
- } catch {
891
+ } catch (error) {
892
+ if (fullyQualifiedWorkspacePath(header.cwd) && error instanceof Error && "code" in error && error.code === "ENOENT") this.missingSessionPaths.add(header.id);
888
893
  this.invalidSessionPaths.set(header.id, `cwd '${header.cwd}' does not resolve`);
889
894
  }
890
895
  }
@@ -901,8 +906,8 @@ var WorkspaceRegistry = class extends Service {
901
906
  for (const entity of this.entities.values()) {
902
907
  const record = this.requireTable().get(entity.id);
903
908
  for (const sessionId of record.sessionIds) {
909
+ if (this.host.candidateMatches(sessionId, record.path)) continue;
904
910
  const path = this.sessionPaths.get(sessionId);
905
- if (path === record.path) continue;
906
911
  const reason = this.invalidSessionPaths.get(sessionId) ?? (this.headers.has(sessionId) ? `canonical cwd '${path}' differs from workspace path '${record.path}'` : "session header is missing");
907
912
  this.ctx.logger.warn(`workspace '${entity.id}' filtered session '${sessionId}' from membership: ${reason}`);
908
913
  }
@@ -30,12 +30,13 @@ export interface WorkspaceEntityHost {
30
30
  */
31
31
  table(): KvTable<WorkspaceId, WorkspaceRecord>;
32
32
  /**
33
- * Read a session's canonical directory from the registry's header index.
34
- * @param id - Session whose indexed path is requested.
35
- * @returns the canonical directory, or `undefined` when the header is
36
- * missing or its cwd cannot identify an existing directory.
33
+ * Validate an already-accounted session, preserving ownership when its fully
34
+ * qualified cwd cannot resolve because a path component is missing.
35
+ * @param id - Session already present in this workspace's candidate account.
36
+ * @param path - Workspace's stored canonical directory.
37
+ * @returns whether the candidate remains a member.
37
38
  */
38
- sessionPath(id: SessionId): string | undefined;
39
+ candidateMatches(id: SessionId, path: string): boolean;
39
40
  /**
40
41
  * Read one stored session header for attach validation.
41
42
  * @param id - The session whose header to read.
@@ -74,7 +75,7 @@ export declare class WorkspaceEntity implements Workspace {
74
75
  /**
75
76
  * The single write path: run `fn` on the domain write chain via
76
77
  * `table.update`, stamping `updatedAt` and pruning candidates that no
77
- * longer pass the id-plus-canonical-cwd membership check, then swap the
78
+ * longer pass the candidate membership check, then swap the
78
79
  * snapshot.
79
80
  *
80
81
  * `fn` sees the value current at its chain slot, so membership decisions
@@ -49,7 +49,7 @@ export class WorkspaceEntity {
49
49
  return this.record.updatedAt;
50
50
  }
51
51
  get sessionIds() {
52
- return this.record.sessionIds.filter(id => this.host.sessionPath(id) === this.record.path);
52
+ return this.record.sessionIds.filter(id => this.host.candidateMatches(id, this.record.path));
53
53
  }
54
54
  async setTitle(title) {
55
55
  await this.mutate(record => ({ ...record, title }));
@@ -124,7 +124,7 @@ export class WorkspaceEntity {
124
124
  /**
125
125
  * The single write path: run `fn` on the domain write chain via
126
126
  * `table.update`, stamping `updatedAt` and pruning candidates that no
127
- * longer pass the id-plus-canonical-cwd membership check, then swap the
127
+ * longer pass the candidate membership check, then swap the
128
128
  * snapshot.
129
129
  *
130
130
  * `fn` sees the value current at its chain slot, so membership decisions
@@ -138,7 +138,7 @@ export class WorkspaceEntity {
138
138
  try {
139
139
  next = await this.host.table().update(this.id, (current) => {
140
140
  const changed = fn(current);
141
- const sessionIds = changed.sessionIds.filter(id => this.host.sessionPath(id) === changed.path);
141
+ const sessionIds = changed.sessionIds.filter(id => this.host.candidateMatches(id, changed.path));
142
142
  if (changed === current && sessionIds.length === current.sessionIds.length) {
143
143
  throw unchangedSentinel;
144
144
  }
@@ -125,6 +125,7 @@ export declare class WorkspaceRegistry extends Service {
125
125
  private readonly entities;
126
126
  private readonly headers;
127
127
  private readonly sessionPaths;
128
+ private readonly missingSessionPaths;
128
129
  private readonly invalidSessionPaths;
129
130
  private operationTail;
130
131
  private readonly host;
@@ -98,14 +98,16 @@ export class WorkspaceRegistry extends Service {
98
98
  entities = new Map();
99
99
  headers = new Map();
100
100
  sessionPaths = new Map();
101
+ missingSessionPaths = new Set();
101
102
  invalidSessionPaths = new Map();
102
103
  operationTail = Promise.resolve();
103
104
  host = {
104
105
  table: () => this.requireTable(),
105
- sessionPath: id => this.sessionPaths.get(id),
106
+ candidateMatches: (id, path) => this.sessionPaths.get(id) === path || this.missingSessionPaths.has(id),
106
107
  readSessionHeader: id => this.readSessionHeader(id),
107
108
  rememberSessionPath: (id, path) => {
108
109
  this.sessionPaths.set(id, path);
110
+ this.missingSessionPaths.delete(id);
109
111
  this.invalidSessionPaths.delete(id);
110
112
  },
111
113
  };
@@ -693,6 +695,7 @@ export class WorkspaceRegistry extends Service {
693
695
  async replaceHeaderIndex(headers) {
694
696
  this.headers.clear();
695
697
  this.sessionPaths.clear();
698
+ this.missingSessionPaths.clear();
696
699
  this.invalidSessionPaths.clear();
697
700
  await this.indexHeaders(headers);
698
701
  }
@@ -703,6 +706,7 @@ export class WorkspaceRegistry extends Service {
703
706
  async indexHeader(header) {
704
707
  this.headers.set(header.id, header);
705
708
  this.sessionPaths.delete(header.id);
709
+ this.missingSessionPaths.delete(header.id);
706
710
  if (header.cwd === undefined) {
707
711
  this.invalidSessionPaths.set(header.id, 'header has no cwd');
708
712
  return;
@@ -716,7 +720,11 @@ export class WorkspaceRegistry extends Service {
716
720
  this.sessionPaths.set(header.id, path);
717
721
  this.invalidSessionPaths.delete(header.id);
718
722
  }
719
- catch {
723
+ catch (error) {
724
+ if (fullyQualifiedWorkspacePath(header.cwd)
725
+ && error instanceof Error && 'code' in error && error.code === 'ENOENT') {
726
+ this.missingSessionPaths.add(header.id);
727
+ }
720
728
  this.invalidSessionPaths.set(header.id, `cwd '${header.cwd}' does not resolve`);
721
729
  }
722
730
  }
@@ -735,9 +743,9 @@ export class WorkspaceRegistry extends Service {
735
743
  for (const entity of this.entities.values()) {
736
744
  const record = this.requireTable().get(entity.id);
737
745
  for (const sessionId of record.sessionIds) {
738
- const path = this.sessionPaths.get(sessionId);
739
- if (path === record.path)
746
+ if (this.host.candidateMatches(sessionId, record.path))
740
747
  continue;
748
+ const path = this.sessionPaths.get(sessionId);
741
749
  const reason = this.invalidSessionPaths.get(sessionId)
742
750
  ?? (this.headers.has(sessionId)
743
751
  ? `canonical cwd '${path}' differs from workspace path '${record.path}'`
@@ -6,12 +6,17 @@
6
6
  */
7
7
  import type { Branded } from '@deepseek-ai/dsh-brand';
8
8
  import type { SessionId } from '@deepseek-ai/dsh-session/types';
9
+ import type { TypertResource } from '@deepseek-ai/dsh-typert-protocol';
9
10
  /**
10
11
  * Identifies one workspace record. A generated uuid, never the path: path
11
12
  * normalization rewrites paths, and a reference anchor must stay stable.
12
13
  */
13
14
  export type WorkspaceId = Branded<'WorkspaceId'>;
14
15
  declare module '@deepseek-ai/dsh-typert-protocol' {
16
+ interface TypertResourceMap {
17
+ /** One Workspace; the Workspace Controller registers its default `workspaceId` wire field. */
18
+ workspace: TypertResource<WorkspaceId>;
19
+ }
15
20
  interface RemoteErrorDetailsMap {
16
21
  /** No registration carries that Workspace identity. */
17
22
  'workspace/not-found': {
@@ -50,9 +55,10 @@ export interface SessionActivity {
50
55
  }
51
56
  /**
52
57
  * One workspace: a stable id over an existing directory, a display title, and
53
- * an ordered candidate account of sessions. Membership requires both an id in
54
- * that account and a session header whose canonical cwd equals the workspace
55
- * path. Consumers only see this interface; the implementation stays private.
58
+ * an ordered candidate account of sessions. Membership validates header cwd
59
+ * against the workspace path, but preserves an existing account when its
60
+ * fully qualified cwd is missing. Consumers only see this interface; the
61
+ * implementation stays private.
56
62
  */
57
63
  export interface Workspace {
58
64
  /** Stable record id (generated uuid). */
@@ -74,8 +80,11 @@ export interface Workspace {
74
80
  * prepended at attach, explicit reordering goes through
75
81
  * `insertSessionBefore`, and activity never reorders. The durable candidate
76
82
  * account is filtered synchronously: missing headers, invalid cwd values,
77
- * and canonical cwd mismatches are never returned. A subsequent workspace
78
- * mutation prunes those filtered candidates durably.
83
+ * and canonical cwd mismatches are never returned. A fully qualified cwd
84
+ * whose realpath or directory stat fails with ENOENT preserves an existing
85
+ * candidate, including a dangling symlink; other filesystem failures do not.
86
+ * A subsequent mutation prunes filtered candidates, not missing-directory
87
+ * candidates. This does not admit new sessions or recreate directories.
79
88
  */
80
89
  readonly sessionIds: readonly SessionId[];
81
90
  /**
package/locale/en.json ADDED
@@ -0,0 +1,5 @@
1
+ {
2
+ "meta": {
3
+ "title": "workspace"
4
+ }
5
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "工作区管理",
4
+ "description": "保存工作区信息,并管理工作区与会话的关联。"
5
+ }
6
+ }
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.2.1-alpha.2",
4
+ "version": "0.2.1-alpha.4",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -23,32 +23,34 @@
23
23
  "default": "./lib/types/types.js"
24
24
  },
25
25
  "./src/*": "./src/*",
26
- "./package.json": "./package.json"
26
+ "./package.json": "./package.json",
27
+ "./locale/*.json": "./locale/*.json"
27
28
  },
28
29
  "files": [
30
+ "locale/*.json",
29
31
  "lib/index.js",
30
32
  "lib/types/**/*.js",
31
33
  "lib/types/**/*.d.ts"
32
34
  ],
33
35
  "license": "MIT",
34
36
  "peerDependencies": {
35
- "@deepseek-ai/cordis": "~4.0.5-alpha.1",
36
- "@deepseek-ai/dsh-session": "0.2.1-alpha.2",
37
- "@deepseek-ai/dsh-session-persistence": "0.2.1-alpha.2",
38
- "@deepseek-ai/dsh-storage": "0.2.1-alpha.2",
39
- "@deepseek-ai/dsh-storage-domain": "0.2.1-alpha.2",
40
- "@deepseek-ai/dsh-typert-protocol": "0.2.1-alpha.2"
37
+ "@deepseek-ai/cordis": "~4.0.5-alpha.2",
38
+ "@deepseek-ai/dsh-session": "0.2.1-alpha.4",
39
+ "@deepseek-ai/dsh-session-persistence": "0.2.1-alpha.4",
40
+ "@deepseek-ai/dsh-storage": "0.2.1-alpha.4",
41
+ "@deepseek-ai/dsh-storage-domain": "0.2.1-alpha.4",
42
+ "@deepseek-ai/dsh-typert-protocol": "0.2.1-alpha.4"
41
43
  },
42
44
  "dependencies": {
43
- "@deepseek-ai/dsh-brand": "0.2.1-alpha.2",
44
- "zod": "^4.4.3"
45
+ "@deepseek-ai/dsh-brand": "0.2.1-alpha.4",
46
+ "zod": "4.4.3"
45
47
  },
46
48
  "devDependencies": {
47
- "@deepseek-ai/cordis": "~4.0.5-alpha.1",
48
- "@deepseek-ai/dsh-session": "0.2.1-alpha.2",
49
- "@deepseek-ai/dsh-session-persistence": "0.2.1-alpha.2",
50
- "@deepseek-ai/dsh-storage": "0.2.1-alpha.2",
51
- "@deepseek-ai/dsh-storage-domain": "0.2.1-alpha.2",
52
- "@deepseek-ai/dsh-typert-protocol": "0.2.1-alpha.2"
49
+ "@deepseek-ai/cordis": "~4.0.5-alpha.2",
50
+ "@deepseek-ai/dsh-session": "0.2.1-alpha.4",
51
+ "@deepseek-ai/dsh-session-persistence": "0.2.1-alpha.4",
52
+ "@deepseek-ai/dsh-storage": "0.2.1-alpha.4",
53
+ "@deepseek-ai/dsh-storage-domain": "0.2.1-alpha.4",
54
+ "@deepseek-ai/dsh-typert-protocol": "0.2.1-alpha.4"
53
55
  }
54
56
  }