# Agent Note: 删除 Workspace 注册记录

Status: implemented

[English](2026-07-27-workspace-registration-deletion.md) | 中文

## 问题

Workspace 注册已有代码目录，使 GUI 能够为目录命名，并对其会话排序。该记录没有说 Harness 创建或拥有该目录，会话日志也是独立的持久化对象。若将行内 Delete 操作视为递归删除源码或删除会话，就会破坏该记录所有权边界之外的数据。

现有菜单行只有视觉呈现，没有实际功能，因此持久化顺序、Workspace 表、Host 流、并发浏览器标签页、重连基线，以及列表请求与变更并发时的删除语义也没有定义。

## 决策

`ctx.workspaceRegistry.delete(id)` 只删除 Workspace 注册记录：其 id 会从持久化的 `workspaceIds` 中移除，`workspaces` 表行与实体缓存条目会消失，有序 `sessionIds` 账本也随该行一并消失。它绝不调用文件系统移除操作或 `SessionPersistence`；目录、所有用户文件、所有实时会话和所有已持久化的会话日志都会保留。侧边栏分组是所有存续 Workspace 账本的补集，因此这些会话（包括当前会话）会立即出现在 Ungrouped 下。

未知 id 在领域约定处返回 `false`。`workspace.delete({ workspaceId })` 将该结果映射为 `workspace-not-found`；成功时返回 `{ deleted: true }`。`workspace.list` 仍是重连基线。

## 持久化提交与发布

注册表操作会串行执行创建与删除。删除时先写入移除该 id 后的 Workspace 顺序，再从缓存中移除实体，最后删除表行。表删除是通知提交点：只有缓存停止发布该实体后，包不变量才接受该删除；Host 也只根据这次已提交的删除发出 `host/workspace-removed`。表写入失败时，系统会恢复缓存和此前的持久化顺序，且不会发布移除帧。

Host 流在前一笔全局顺序写入期间继续保留其已提交 id 集合，只在删除表行时移除该 id。因此，创建回滚不会发出错误的移除帧，而每个已连接标签页都能收到从自身投影中删除该记录所需的准确 id。

创建与删除会在记录／顺序对可能分叉之前写入持久化的 `pendingMutation`。启动时只补全该标记明确命名的操作，并清除标记；仅有一行孤立记录无法确定哪个操作被中断。因此，没有标记的顺序／表分叉仍会保持注册表原有的损坏直接失败语义。如果删除的表写入已经提交、但标记清理失败，操作仍会报告成功——所请求的状态和移除帧都已经提交——下一次启动会以幂等方式清除该标记。

## 客户端收敛

`WorkspaceManager` 将 `host/workspace-changed` 与 `host/workspace-removed` 都视为有序增量，并在进行中的 `workspace.list` 响应之上回放。成功的一元删除会立即移除行，无需等待本次操作自己的流回显。移除操作具有幂等性；由于 Workspace id 永不复用，进程本地墓碑标记会拒绝延迟到达的 changed 帧或陈旧基线行。重连仍从 `workspace.list` 刷新；Workspace 增量绝不会剪除会话状态。

删除确认框会保持待处理，直到 React Workspace 投影已经提交目标 id 的移除，因此下一次 Workspace 操作不会看到陈旧列表帧，也不会以其为操作目标。

## 确认交互

现有 Workspace 行菜单会在删除前打开共享 `Modal`。文案明确说明三项后果：Workspace 会从列表中移除，文件夹和会话日志会保留，相关会话会出现在 Ungrouped 下。请求待处理期间，确认与 Cancel 控件均被禁用，重复确认会被忽略，Escape 或 Close 也无法关闭此次操作。失败时 `Modal` 保持打开并显示错误；提交前使用 Cancel、Escape 或 Close 绝不会触发删除。

菜单、`Modal` 和按钮保留现有结构与设计 token。会话删除仍只有视觉呈现，没有实际功能，不在本决策范围内。

## 考虑过的替代方案

**级联删除会话。** 不予采纳，因为 Workspace 注册记录不拥有会话持久化，且产品需求是将历史记录保留在 Ungrouped 下。会话删除需要自己的生命周期、运行状态检查、后代对象的处理语义和明确 UI。

**将文件夹移到废纸篓。** 不予采纳，因为该记录无法证明目录所有权。未来的破坏性文件系统操作必须使用单独名称、单独确认，并实施明确的安全边界。

**先删除表行，之后再修复顺序。** 不予采纳，因为崩溃或写入失败会使已初始化注册表的顺序与表不一致。注册表会在同一串行操作内更新二者，并在表操作失败时恢复此前顺序。

**启动时删除所有未引用表行。** 不予采纳，因为来源不明的顺序损坏也会呈现相同形状；静默丢弃可能损失 Workspace 元数据和 Session 账本。恢复必须依赖拥有该变更的操作预先写入的明确待处理标记。

**成功后重新拉取两个列表。** 不予采纳，因为已提交的移除帧与即时一元回显已足够，既能保留当前会话对象，也避免将局部变更扩大为两次列表请求。重连基线仍是修复路径。

## 验证

Workspace 包测试固定了仅删除元数据的成功路径、同路径重新注册、未知 id 的幂等行为、表操作失败回滚、明确标记的重启恢复、来源不明损坏的拒绝，以及缓存／表不变量行为。Apiproxy 与载体测试固定了 schema、处理器、`workspace-not-found`、保留会话／文件夹、使用新 id 重新注册，以及已提交的 `host/workspace-removed` 帧。客户端测试固定了一元直接回显、重复移除、延迟到达的 changed 帧，以及删除与进行中基线并发的行为。组件测试固定了确认交互、投影稳定后关闭、成功帧先于一元响应、失败、Cancel、Escape 与 Close。浏览器场景会在为不同目录复用已删除名称时，观测每一次瞬时 alert、slot error、console error 与 page error。

组装后的无密钥 Web 场景会注册一个已有临时项目目录，将持久化会话计入账本，把该会话设为当前会话，在 Chromium 中确认删除，并验证 Workspace 分组消失，而 Ungrouped 保留当前会话。该场景在删除前后检查用户文件和 JSONL 日志，并在刷新后重复验证 UI、目录与日志。

## 后果

删除 Workspace 后仍可使用新 id 重新注册同一目录，因此该操作有意设计为可逆；但此前的手动会话顺序会丢失，重新注册后，系统也不会在 bootstrap 结束后自动重新收编现有会话。该操作放弃一键清理会话历史或源码目录，以换取与记录实际所有权一致的删除边界。
