docs: catalogs and Agent Note for the session archive set
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-31-session-archive-global-set.md
|
||||
2026-07-31-session-archive-global-set.md: 0fe354ef0ed50ee60a69b0633522323124c80da7
|
||||
2026-07-31-session-archive-global-set.zh.md: 2b13f0eddc9aa62600a95ac5f412c51f3a38af7b
|
||||
@@ -0,0 +1,33 @@
|
||||
# Agent Note: Session archive (registry-global set)
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-07-31-session-archive-global-set.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The session row menu in the sidebar workspace browser carried a purely visual "Delete session" placeholder (no handler). The product decision is **archive**, not delete: the session log and its workspace accounting stay untouched; the session merely disappears from every grouping surface (workspace groups, Ungrouped, search, the flat list). The archive record needs a home: an Ungrouped session belongs to no workspace entity, so a per-workspace field cannot carry it.
|
||||
|
||||
## Decision
|
||||
|
||||
**The archive set is a new field on the workspace domain's global singleton (`workspaceDomainState.archivedSessionIds`), layered over workspace accounting; display filtering converges entirely in the client's `tree.ts` derivation layer; the wire surface uses the full-snapshot posture.**
|
||||
|
||||
- Storage: `archivedSessionIds: z.array(sessionId).default([])`, domain version stays 2 — a purely additive field; pre-field media parse to an empty set through the schema default, no migration code. An archived session keeps its `sessionIds` slot (a future unarchive restores its position), so the set never touches the one-owner accounting invariant.
|
||||
- Registry: `ctx.workspace.archiveSession(id)` rides `enqueueOperation`, serialized with create/delete; a session neither live nor persisted throws `WorkspaceUnknownSessionError`; an already archived id neither writes nor emits. The `archivedSessionIds` getter exposes the read-only set.
|
||||
- RPC: `workspace.archiveSession({sessionId}) → {archivedSessionIds}` (answers the full updated set); the `workspace.list` response carries the set as the reconnect baseline; a new host frame `host/archived-sessions-changed` pushes the full snapshot after every durable change (same posture as `host/workspace-changed`, emitted from the `domain/changed` global-put branch by set comparison). Unknown sessions reuse the `session-not-found` error code.
|
||||
- Client runtime: `WorkspaceListState.archivedSessionIds` (a `ReadonlySet`, reference replaced only on membership change); the list baseline, the unary echo, and the changed frame each install the complete set. `WorkspacesService.archiveSession` calls `sessions.clear()` when the archived session is the current selection, returning to the New Session view (user decision: archiving the open session sends the main view back to the hero).
|
||||
- UI: the `delete` menu row (visual-only) becomes `archive` (label "Archive session", non-danger styling, no confirmation dialog — a non-destructive action whose worst misfire is list hiding); filtering is one extra arm in `tree.ts`'s `sessionVisible` predicate, with `deriveGroups`/`deriveFlat` taking an `archived` set parameter so all four surfaces (group loop, stray bucket, search, flat) share one source.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Per-workspace archivedSessionIds (the original phrasing).** Rejected: Ungrouped sessions have no home; the user switched to global.
|
||||
|
||||
**An archived flag on SessionSummary (session.list layer).** Rejected: it joins a workspace-domain fact into the sessions-domain projection, summaries have no incremental frame so a separate notification would still be needed — cross-domain coupling outweighs the saving.
|
||||
|
||||
**Host-side filtering in `workspaceView`/the `sessionIds` getter.** Rejected: archiving ≠ changing accounting, and filtering the projection muddles the two concepts; a future restore surface also needs the client to see full accounting.
|
||||
|
||||
**Incremental frames (single archived/removed rows).** Rejected: the set is tiny and changes rarely; full snapshots spare the client merge logic and dedup state and match the existing workspace-changed posture.
|
||||
|
||||
## Consequences
|
||||
|
||||
Archived sessions have no viewing or unarchive surface yet (this iteration's scope; recorded as a README Known Limitation); data and accounting slots stay intact, so a future restore is one UI surface plus one inverse RPC. The `workspace.list` response shape change is a pre-release direct edit (no compatibility layer). The workspace-management e2e pins the full chain (archive → row disappears → still hidden after reload, log still present); domain tests pin idempotence, unknown-id rejection, restart recovery, and the pre-field media default upgrade.
|
||||
@@ -0,0 +1,33 @@
|
||||
# Agent Note: Session 归档(注册表级全局集合)
|
||||
|
||||
状态:implemented
|
||||
|
||||
[English](2026-07-31-session-archive-global-set.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
Sidebar workspace 浏览区的 session 行菜单里,「Delete session」一直是纯视觉占位(无 handler)。产品口径定为**归档**而非删除:session 日志与 workspace 记账都不动,只把该 session 从所有分组视图(workspace 分组、Ungrouped、搜索、平铺列表)里隐藏。归档记录需要一个落点:Ungrouped 的 session 不属于任何 workspace 实体,per-workspace 字段放不下它。
|
||||
|
||||
## 决策
|
||||
|
||||
**归档集合是 workspace domain 全局单例(`workspaceDomainState.archivedSessionIds`)上的一个新字段,覆盖在 workspace 记账之上;显示过滤全部收敛在 client 的 `tree.ts` 派生层;wire 面走全快照姿态。**
|
||||
|
||||
- 存储:`archivedSessionIds: z.array(sessionId).default([])`,domain version 保持 2——纯增量字段,旧介质经 schema default 解析为空集合,无迁移代码。被归档的 session 保留其 `sessionIds` 席位(未来取消归档恢复原位置),因此与「一个 session 只被一个 workspace 记账」不变式零纠缠。
|
||||
- Registry:`ctx.workspace.archiveSession(id)` 走 `enqueueOperation` 与 create/delete 串行;未知 session(实时与持久化都查不到)抛 `WorkspaceUnknownSessionError`;已归档 id 不写盘不发事件。`archivedSessionIds` getter 暴露只读集合。
|
||||
- RPC:`workspace.archiveSession({sessionId}) → {archivedSessionIds}`(应答完整更新后集合);`workspace.list` 响应携带集合作为重连基线;新 host 帧 `host/archived-sessions-changed` 在每次持久变更后推完整快照(与 `host/workspace-changed` 同姿态,从 `domain/changed` 的 global put 分支比对推帧)。未知 session 复用错误码 `session-not-found`。
|
||||
- client runtime:`WorkspaceListState.archivedSessionIds`(`ReadonlySet`,成员不变不换引用);list 基线、unary 回声、changed 帧三路都整体替换安装。`WorkspacesService.archiveSession` 在归档对象恰为当前 selection 时调 `sessions.clear()` 回 New Session 视图(用户拍板:归档当前打开的 session 主视图回 hero)。
|
||||
- UI:菜单项 `delete`(visual-only)改为 `archive`(label「Archive session」,非 danger 样式,无确认对话框——非破坏性操作,误触后果只是列表隐藏);过滤实现为 `tree.ts` 的 `sessionVisible` 判据加一档,`deriveGroups`/`deriveFlat` 增加 `archived` 集合入参,四个视图(分组循环、stray 兜底、搜索、平铺)同源生效。
|
||||
|
||||
## 已考虑的替代方案
|
||||
|
||||
**per-workspace archivedSessionIds(最初表述)。** 否决:Ungrouped session 无落点;用户改口全局。
|
||||
|
||||
**SessionSummary 打 archived 标(session.list 层)。** 否决:要把 workspace domain 事实 join 进 sessions domain 投影,summary 无增量帧还得另发通知,跨域耦合大于收益。
|
||||
|
||||
**host 侧在 `workspaceView`/`sessionIds` getter 过滤。** 否决:归档 ≠ 改记账,投影过滤会把两个概念搅浑;未来恢复入口也需要 client 拿到全量记账。
|
||||
|
||||
**增量帧(archived/removed 单条)。** 否决:集合极小、变更频率低,全快照免去 client 侧合并逻辑与去重状态,与 workspace-changed 现有姿态一致。
|
||||
|
||||
## 后果
|
||||
|
||||
归档后 UI 无查看/取消归档入口(本期口径,README Known Limitation 记账);数据与席位完好,后续加恢复面只是 UI + 一个逆向 RPC。`workspace.list` 响应形状变化是 pre-release 直改(无兼容层)。e2e(workspace-management)钉住了「归档→行消失→reload 后仍隐藏、日志仍在」的全链路;domain 层测试钉住幂等、未知 id 拒绝、跨重启恢复与旧介质默认升级。
|
||||
@@ -2544,6 +2544,15 @@ list(): Workspace[]
|
||||
*/
|
||||
delete(id: WorkspaceId): Promise<boolean>
|
||||
|
||||
/**
|
||||
* Archive one session durably. The session must exist (live or in session
|
||||
* persistence); its workspace accounting — or lack of one — is irrelevant.
|
||||
* An already archived id resolves without writing.
|
||||
* @param sessionId - The session to archive.
|
||||
* @returns resolution after durability.
|
||||
*/
|
||||
archiveSession(sessionId: SessionId): Promise<void>
|
||||
|
||||
/**
|
||||
* Resolve by canonical directory path without creating or mutating a
|
||||
* workspace. A missing path rejects during `realpath`; an existing unowned
|
||||
@@ -2554,7 +2563,9 @@ delete(id: WorkspaceId): Promise<boolean>
|
||||
async resolveByPath(path: string): Promise<Workspace | undefined>
|
||||
```
|
||||
|
||||
Source: [`packages/workspace/workspace/src/index.ts:78`](../../packages/workspace/workspace/src/index.ts)
|
||||
Types: [SessionId](../core-data-structures/core.md)
|
||||
|
||||
Source: [`packages/workspace/workspace/src/index.ts:90`](../../packages/workspace/workspace/src/index.ts)
|
||||
|
||||
## Inherited `ctx` members (cordis core + loader/hmr/timer)
|
||||
|
||||
|
||||
@@ -1162,6 +1162,10 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
|
||||
signature: 'delete(id: WorkspaceId): Promise<boolean>',
|
||||
jsDoc: '/**\n * Delete one workspace registration while retaining its directory and every\n * session log. The durable order is updated before the table deletion; a\n * failed table write restores the prior order and keeps the entity\n * published. Unknown ids are an idempotent no-op for domain callers.\n * @param id - Workspace registration to remove.\n * @returns `true` when a record was deleted, `false` when it was unknown.\n */',
|
||||
},
|
||||
{
|
||||
signature: 'archiveSession(sessionId: SessionId): Promise<void>',
|
||||
jsDoc: '/**\n * Archive one session durably. The session must exist (live or in session\n * persistence); its workspace accounting — or lack of one — is irrelevant.\n * An already archived id resolves without writing.\n * @param sessionId - The session to archive.\n * @returns resolution after durability.\n */',
|
||||
},
|
||||
{
|
||||
signature: 'async resolveByPath(path: string): Promise<Workspace | undefined>',
|
||||
jsDoc: '/**\n * Resolve by canonical directory path without creating or mutating a\n * workspace. A missing path rejects during `realpath`; an existing unowned\n * directory returns `undefined`.\n * @param path - Existing directory path in any spelling.\n * @returns the workspace owning the canonical path, when one exists.\n */',
|
||||
|
||||
Reference in New Issue
Block a user