DSH / Atlas
2026-06-19implementedsimplificationarchived 2026-09-04

Drop the mutable session summary

移除可变的会话摘要

The [session-persistence seam](../architecture/2026-06-14-session-persistence.md) split a session's out-of-log metadata into two types owned by `dsh-session`: an immutable `SessionHeader` (`version`, `id`, `createdAt`, `cwd?`, `parentSession?`) written once at creation, and a mutable `SessionSummary` (`updatedAt`, `title?`, `firstPrompt?`) "updateable without touching the append-only log". Their union was `SessionMet

English

Problem

The session-persistence seam split a session's out-of-log metadata into two types owned by dsh-session: an immutable SessionHeader (version, id, createdAt, cwd?, parentSession?) written once at creation, and a mutable SessionSummary (updatedAt, title?, firstPrompt?) "updateable without touching the append-only log". Their union was SessionMeta = SessionHeader & SessionSummary, and the abstract SessionPersistence service carried a seventh method — update(id, summary) — for rewriting the summary. Each backend implemented the mutable store its own way: JSONL wrote a separate atomic .summary.json sidecar beside the log (temp-write + rename, best-effort), SQLite kept updated_at/title/first_prompt columns bumped inside the append transaction.

The summary was designed for a future session picker (recency ordering via updatedAt, a title/firstPrompt preview). That picker was never built. An audit of the whole repo found the entire SessionSummary API is dead state:

  • SessionPersistence.update() has zero production callers (every .update( hit is createHash().update() or a test).
  • firstPrompt is never read anywhere in production.
  • Session titles come from durable session/title events, while tool-card titles come from tool presenters; neither reads mutable session metadata.
  • Persistence-list consumers use immutable header identity, creation, lineage, and cwd fields. Recency and previews derive from the log rather than an updatedAt summary.
  • Decisively: the live Session.header was already typed SessionHeader, not SessionMeta — the summary never existed on the live session object; it lived only in the persistence layer, written and read by nothing but its own contract test.

Decision

Delete the mutable session summary entirely. SessionSummary and the SessionMeta name are absent; the metadata a backend stores and returns is just SessionHeader. SessionPersistence.update() is absent from the abstract service. The shipped JSONL provider has no summary sidecar machinery (writeSidecar/readSidecar/touchSummary/removeSidecars/sidecarPath or load/list overlays), and an out-of-tree provider implements the same summary-free service contract.

Anything the summary was meant to provide is derivable from the append-only log when a consumer actually needs it (firstPrompt = first user/message; recency = the last event's time or the file mtime) or already lives in the immutable header (createdAt, cwd). The one thing not derivable — a user-edited title — had no implementation and is pure YAGNI; it can return as its own log event or header field if a real feature ever needs it.

The removal narrows the public service contract and JSONL on-disk format; the summary was a deliberate forward-looking design, not an accident; and SessionHeader stands where the original Agent Note described SessionMeta, which is why the summary vanished. It also simplified the then-current shared persistence write coordinator: with no mutable summary, that orchestration needed no updateSummary hook.

No migration

The shipped JSONL provider has no mutable summary format or migration path: it reads and writes only SessionHeader plus the append-only log. The repository has no first-party SQLite Session provider. The JSONL-only persistence decision owns the compatibility cut for databases written by the removed provider and directs operators to export them with an older build before upgrading.

Consequences

A future session picker now has to derive its preview/ordering from the log (or reintroduce a typed field) rather than reading a ready-made summary row. That is the correct cost: a cache for a feature that does not exist is dead weight that every backend pays to maintain and every contract test pays to assert. The principle — a passing test pins current behavior, not necessarily correct behavior; behavior can be an artifact of a past compromise — is now recorded as a standalone convention in root AGENTS.md, with this change as its worked example.

<!-- agent-note-format: alternatives-not-recorded (pre-format Agent Note) -->

中文

问题

会话持久化 seam 将会话的日志外元数据拆分为 dsh-session 拥有的两种类型:一个不可变的 SessionHeader(version、id、createdAt、cwd?、parentSession?),在创建时一次性写入;一个可变的 SessionSummary(updatedAt、title?、firstPrompt?),「可在不触碰仅追加日志的情况下更新」。二者合并为 SessionMeta = SessionHeader & SessionSummary,抽象的 SessionPersistence 服务为此多出第七个方法 update(id, summary),用于重写摘要。各后端各自实现可变存储:JSONL 在日志旁先写入临时文件再重命名,并以尽力而为的方式原子发布一个独立的 .summary.json 伴随文件;SQLite 则使用 updated_at/title/first_prompt 列,并在追加事务内更新其中的时间列。

摘要是为未来的会话选择器设计的(通过 updatedAt 排序近期会话,用 title/firstPrompt 做预览)。该选择器从未实现。对整个仓库的审计表明,SessionSummary 的整套相关接口都只是在维护无用状态:

  • SessionPersistence.update() 零个生产调用方(所有 .update( 匹配都是 createHash().update() 或测试代码)。
  • firstPrompt 在生产代码中从未被读取。
  • 会话标题来自持久的 session/title 事件,工具卡片标题来自工具 presenter;二者都不读取可变的会话元数据。
  • 持久化列表的消费方使用不可变 header 中的标识、创建时间、谱系和 cwd 字段。近期排序和预览派生自日志,而非某个 updatedAt 摘要。
  • 决定性的一点:活跃的 Session.header 类型本来就是 SessionHeader 而非 SessionMeta——摘要从未存在于活跃会话对象上;它只存在于持久化层,除了自身的约定测试外无人写入、无人读取。

决策

彻底删除可变的会话摘要。SessionSummary 与 SessionMeta 这个名称均不存在;后端存储和返回的元数据仅为 SessionHeader。抽象服务不包含 SessionPersistence.update()。交付的 JSONL provider 不包含摘要伴随文件机制(writeSidecar/readSidecar/touchSummary/removeSidecars/sidecarPath 或 load/list 覆盖逻辑),仓库外 provider 实现相同的无摘要服务约定。

摘要原本要提供的一切,在消费方真正需要时都可从仅追加日志中派生(firstPrompt = 第一条 user/message;近期度 = 最后一个事件的 time 或文件 mtime),或者已经存在于不可变 header 中(createdAt、cwd)。唯一不可派生的是用户手动编辑的标题,但它从未实现,纯属 YAGNI;如果未来真有功能需要,它可以作为独立的日志事件或 header 字段回归。

这次移除收窄公开服务约定与 JSONL 磁盘格式;摘要是有意为未来设计的结果,而非意外;原 Agent Note 描述 SessionMeta 之处由 SessionHeader 承担,这就是摘要消失的原因。它也简化了当时的共享持久化写入协调器:没有可变摘要后,那套编排不需要 updateSummary 钩子。

无需迁移

交付的 JSONL provider 不存在可变摘要格式或迁移路径:它只读写 SessionHeader 与仅追加日志。仓库不包含 first-party SQLite Session provider。JSONL-only 持久化决策负责删除 provider 写入的数据库的兼容性切断,并要求 operator 在升级前先用旧 build 导出数据。

后果

未来的会话选择器现在必须从日志派生预览/排序信息(或重新引入一个类型化字段),而不能直接读取现成的摘要行。这是正确的代价:为一个尚不存在的功能维护缓存,是每个后端都要承担维护成本、每个约定测试都要承担断言成本的无谓负担。这一原则——通过的测试固定的是当前行为,不一定是正确行为;行为可能是过去妥协的产物——现已作为独立约定记录在根 AGENTS.md 中,本次变更即为其实例。

<!-- agent-note-format: alternatives-not-recorded (pre-format Agent Note) -->