Workspace Registration Deletion
删除 Workspace 注册记录
A Workspace registers an existing code directory so the GUI can name it and order its Sessions. That record does not say that Harness created or owns the directory, and the Session log is an independent persistence object. Treating the row's Delete action as recursive source deletion or Session deletion would destroy data outside the record's ownership boundary. The existing visual-only menu row also left deletion se
English
Problem
A Workspace registers an existing code directory so the GUI can name it and order its Sessions. That record does not say that Harness created or owns the directory, and the Session log is an independent persistence object. Treating the row's Delete action as recursive source deletion or Session deletion would destroy data outside the record's ownership boundary.
The existing visual-only menu row also left deletion semantics undefined across durable order, the Workspace table, Host streams, concurrent browser tabs, reconnect baselines, and a list request racing the mutation.
Decision
ctx.workspaceRegistry.delete(id) deletes only the Workspace registration: its id leaves durable workspaceIds, its workspaces table row and entity-cache entry disappear, and its ordered sessionIds account disappears with that row. It never calls filesystem removal or SessionPersistence; the directory, every user file, every live Session, and every persisted Session log remain. Because sidebar grouping is the complement of all surviving Workspace accounts, those Sessions immediately appear under Ungrouped, including the current Session.
Unknown ids return false at the domain contract. workspace.delete({ workspaceId }) maps that distinction to workspace-not-found; success returns { deleted: true }. workspace.list remains the reconnect baseline.
Durable commit and publication
Registry operations serialize create and delete. Deletion first writes the Workspace order without the id, then removes the entity from the cache, then deletes the table row. The table deletion is the notification commit point: the package invariant accepts it only after the cache stopped publishing the entity, and the Host emits host/workspace-removed only from that committed deletion. A table-write failure restores the cache and prior durable order; no removal frame is published.
The Host stream keeps its committed-id set through the preceding global-order write and removes the id only on the table deletion. Create rollback therefore emits no false removal, while every connected tab receives exactly the id needed to delete its projection.
Create and delete write a durable pendingMutation before their record/order pair can diverge. Startup completes only the operation named by that marker and clears it; an orphan row alone does not identify which operation was interrupted. Unmarked order/table divergence therefore retains the registry's fail-loud corruption behavior. A deletion whose table write committed but marker cleanup failed still reports success—the requested state and removal frame are already committed—and the next startup clears that marker idempotently.
Client convergence
WorkspaceManager treats both host/workspace-changed and host/workspace-removed as ordered deltas replayed over an in-flight workspace.list response. A successful unary delete removes the row immediately instead of waiting for its own stream echo. Removal is idempotent, and a process-local tombstone rejects late changed frames or stale baseline rows for the never-reused Workspace id. A reconnect still refreshes from workspace.list; Session state is never pruned by a Workspace delta.
The delete confirmation remains pending until the React Workspace projection has committed the removed id, so the next Workspace gesture cannot observe or target one stale list frame.
Confirmation interaction
The existing Workspace row menu opens a shared Modal before deletion. The text states all three consequences: the Workspace leaves the list, the folder and session logs remain, and its Sessions appear under Ungrouped. While the request is pending, the confirm and Cancel controls are disabled, duplicate confirmation is ignored, and Escape or Close cannot dismiss the operation. Failure keeps the Modal open with the error; Cancel, Escape, and Close before submission never delete.
The menu, Modal, and buttons retain their existing structure and design tokens. Session deletion remains visual-only and outside this decision.
Alternatives considered
Cascade-delete Sessions. Rejected because Workspace registration does not own Session persistence and the product requirement is to preserve histories under Ungrouped. Session deletion needs its own lifecycle, running checks, descendant semantics, and explicit UI.
Move the folder to Trash. Rejected because the record cannot prove directory ownership. A future destructive filesystem action must be separately named, separately confirmed, and enforce explicit safety boundaries.
Delete the table row and repair order later. Rejected because a crash or write failure would leave an initialized registry whose order and table disagree. The registry updates both under one serialized operation and restores the prior order on table failure.
Delete every unreferenced row at startup. Rejected because the same shape can come from unexplained order corruption; silently discarding it could lose Workspace metadata and Session accounting. Recovery requires the explicit pending marker written by the owning mutation.
Refetch both lists after success. Rejected because the committed removal frame plus immediate unary echo is sufficient, preserves the current Session object, and avoids turning a local mutation into two list requests. Reconnect baselines remain the repair path.
Verification
Workspace package tests pin successful metadata-only deletion, same-path re-registration, unknown-id idempotence, table-failure rollback, explicit-marker restart recovery, unexplained-corruption rejection, and cache/table invariant behavior. Apiproxy and carrier tests pin the schema, handler, workspace-not-found, retained Session/folder, fresh-id re-registration, and committed host/workspace-removed frame. Client tests pin unary direct echo, duplicate removal, late changed frames, and deletion racing an in-flight baseline. Component tests pin confirmation, projection-settled closing, success-frame-before-unary ordering, failure, Cancel, Escape, and Close. The browser scenario observes every transient alert, slot error, console error, and page error while reusing a deleted title for a different directory.
The assembled keyless Web scenario registers an existing temporary project directory, accounts a persisted Session, makes that Session current, confirms deletion in Chromium, and verifies the Workspace group disappears while Ungrouped retains the current Session. It checks the user file and JSONL log before and after deletion and repeats the UI, directory, and log assertions after reload.
Consequences
Deleting a Workspace is intentionally reversible by registering the same directory again with a fresh id, although its prior manual Session order is gone; re-registration does not automatically re-adopt existing Sessions after bootstrap. The operation gives up a one-click cleanup of Session histories or source directories in exchange for a deletion boundary that matches what the record actually owns.
中文
问题
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 结束后自动重新收编现有会话。该操作放弃一键清理会话历史或源码目录,以换取与记录实际所有权一致的删除边界。