DSH / Atlas
2026-07-21implementedfeaturearchived 2026-08-04

Product-level TUI session resume

产品级 TUI 会话恢复

The original `/resume` printed shell commands. It did not let a keyboard user inspect titles or outcomes, distinguish corruption from a missing adapter, or safely transfer the terminal. Leaving the TUI and manually launching a command also hid the required ordering: finish current work, flush it, release the UI and app, then restore the exact persisted identity without silently creating a replacement.

English

Problem

The original /resume printed shell commands. It did not let a keyboard user inspect titles or outcomes, distinguish corruption from a missing adapter, or safely transfer the terminal. Leaving the TUI and manually launching a command also hid the required ordering: finish current work, flush it, release the UI and app, then restore the exact persisted identity without silently creating a replacement.

Decision

/resume uses the TUI's existing interactive overlay seam as a full-viewport picker rather than a centered dialog. The flat page keeps the search field, workspace scope line, candidates, and shortcut footer in stable screen regions; only the active row uses the accent role. Its search editor starts immediately after the search glyph and emits pi-tui's cursor marker, so terminal IME composition remains anchored in the field. Escape clears a non-empty query before a second Escape closes the picker. It orders candidates by last logged activity and searches log-backed title or id. Each candidate displays current/live/persisted state, last turn outcome, recent provider/model, durable goal phase when present, and the id as secondary text. The current session and sessions already live in this runtime remain visible but disabled. The picker opens on the current workspace and reaches every other one through the scope toggle the cross-workspace resume note owns.

session-query.readSession() supplies a detached complete log validated by the same core replay boundary used by resume. The TUI folds title and goal state from that log. A candidate load failure is local to that row; selecting a candidate revalidates the log, workspace, route, current agent's idle status, and the exclusions for the current session and sessions already live in this runtime, so a stale listing cannot bypass preflight. A missing adapter reports an intact session with an unavailable route. This preflight does not lock the target or exclude another process.

After preflight, the TUI flushes the current session, confirms that its agent remains idle, then stops the terminal before calling TuiRuntime.handoffResume with the validated id and the target workspace. The shipped dsh host disposes the root app and uses process.execve with a normalized --resume argument, atomically replacing the process rather than starting a child. The resumed app publishes the same SessionId; ordinary replay restores transcript, title, todos, and durable goal state. Goal activation is intentionally disarmed, and the TUI asks for human confirmation or /goal resume.

The exit line is a launcher-owned context slot rather than a config template, and a host without in-place handoff reports that the session stays resumable instead of naming a command it cannot construct; the launcher-owned resume identity note owns that ownership move and supersedes the resumeCommand config key this note originally shipped. The TUI still never executes shell text.

Alternatives considered

Have the TUI spawn the resume command. Rejected: the text is display copy, not trusted argv, and the TUI does not own app teardown or process lifetime. The constrained host seam receives only a validated SessionId.

Construct the resumed agent inside the existing TUI. Rejected: replacing one config-created agent would cross Loader ownership, scoped plugin setup, persistence retirement, and terminal lifecycle in the presentation layer. Root disposal plus process replacement reuses the supported startup path.

Treat a missing adapter as a missing session. Rejected: storage validity and current route availability are independent facts. The selector keeps the row and names the unavailable provider/model.

Persist goal activation across resume. Rejected: durable intent is not authorization to continue after a human or process boundary. Goal phase survives; automatic continuation does not.

Consequences

  • Concurrent processes can select or resume the same persisted session because preflight does not serialize them.
  • /resume depends on session-query for discovery and complete-log reads, but persistence and host handoff remain optional; without a host, the command fallback stays usable.
  • Process replacement intentionally restarts Loader composition. Runtime-only state is rebuilt, while only logged or header-backed session state survives.

Testing

TUI package tests cover keyboard navigation, title/id search, search-clear/cancel behavior, running-agent refusal, refusal of the current session and sessions already live in this runtime, route absence, corrupt rows, preflight revalidation, the no-host warning, and stop-before-handoff ordering. Session-query tests pin detached full-log validation. Agent-loop resume tests pin exact identity and history; title, todo, and goal replay suites pin restored projections and disarmed goal activation. The package semantic snapshot owns the full-viewport selector and its IME cursor anchor; a deployment shipping the TUI owns its process and PTY handoff acceptance.

中文

Problem

原有 /resume 只会打印 shell 命令。使用键盘操作的用户无法查看标题或结果、区分日志损坏与适配器缺失,也无法安全移交终端。退出 TUI 后手动启动命令还掩盖了必要的操作顺序:等待当前工作结束并将其刷写,释放 UI 和应用,再恢复持久化的原有身份,绝不能静默创建替代会话。

Decision

/resume 使用 TUI 现有的交互式浮层接口,但以占满 viewport 的选择页呈现,而不是居中弹窗。这个扁平页面把搜索框、workspace 作用域行、候选项和快捷键页脚放在稳定的屏幕区域,只有当前行使用强调色。搜索编辑器紧跟搜索图标起始,并输出 pi-tui 的光标标记,因此终端输入法的组合文本会锚定在输入框中。查询非空时,第一次按 Escape 会清空查询,第二次才关闭选择页。页面按日志记录的最后活动时间排列候选项,并支持按日志内标题或 id 搜索。每个候选项都会显示是否为当前会话、是否活跃、是否已持久化,最近一个轮次的结果,最近使用的提供方/模型,以及可用时的持久化目标阶段;id 作为次要信息显示。当前会话和已在本运行时中处于活跃状态的会话仍会显示,但不可选择。选择页打开时位于当前 workspace,并通过跨 workspace 恢复记录所拥有的作用域切换到达其他每一个 workspace。

session-query.readSession() 提供一份脱离运行时的完整日志,并通过恢复流程所用的同一核心回放边界完成验证。TUI 从该日志中折叠出标题和目标状态。候选项加载失败时只影响该行;选择候选项后会复查日志、workspace、路由、当前 agent 的空闲状态,以及针对当前会话和已在本运行时中处于活跃状态的会话的排除规则,避免陈旧列表绕过预检。适配器缺失时会报告会话完整但路由不可用。该预检不会锁定目标,也不会排除其他进程。

预检通过后,TUI 会刷写当前会话,再次确认其 agent 仍处于空闲状态,然后停止终端并以经过验证的 id 和目标 workspace 调用 TuiRuntime.handoffResume。已交付的 dsh 宿主会释放根应用,并使用带有规范化 --resume 参数的 process.execve 原子替换当前进程,而不是启动子进程。恢复后的应用发布相同的 SessionId;常规回放会还原 transcript(文本记录)、标题、待办事项和持久化目标状态。系统会有意解除目标的激活状态,TUI 则要求用户确认继续或执行 /goal resume

退出时打印的行是启动器拥有的上下文插槽,而非配置模板;不支持原地交接的宿主会说明会话仍可恢复,而不再给出它无法构造的命令。由启动器持有的会话身份与退出行记录了这次所有权迁移,并取代本记录最初交付的 resumeCommand 配置键。TUI 仍然绝不执行 shell 文本。

Alternatives considered

让 TUI 创建恢复命令进程。 否决:该文本是展示用文案,不是可信的参数列表,且 TUI 不拥有应用拆卸或进程生命周期。受约束的宿主接口只接收经过验证的 SessionId

在现有 TUI 内构造恢复后的 agent。 否决:在表现层替换由配置创建的 agent,会跨越 Loader 所有权、作用域插件初始化、持久化资源释放和终端生命周期。释放根应用并替换进程可以复用受支持的启动路径。

把适配器缺失视为会话缺失。 否决:存储有效性和当前路由可用性是相互独立的事实。选择器会保留该行,并指出不可用的提供方/模型。

恢复会话时延续目标激活状态。 否决:持久意图并不代表跨越用户或进程边界后仍获授权继续执行。目标阶段会保留,但不会自动续跑。

Consequences

  • 预检不会串行化不同进程;多个进程可以并发选择或恢复同一个持久化会话。
  • /resume 依赖 session-query 发现会话并读取完整日志,但持久化和宿主交接仍是可选功能;没有宿主时,命令回退仍可使用。
  • 进程替换会有意重启 Loader 组合。系统会重建仅存在于运行时的状态,而只有日志或会话头部记录的会话状态能够保留。

Testing

TUI 包(package)测试覆盖键盘导航、标题/id 搜索、清空搜索后再取消、agent 运行期间拒绝恢复、拒绝恢复当前会话和已在本运行时中处于活跃状态的会话、路由缺失、损坏的候选行、预检复查、无宿主时的告警,以及停止终端先于宿主交接的顺序。session-query 测试固定脱离运行时的完整日志验证。agent-loop 恢复测试固定会话身份和历史完全一致;标题、待办事项和目标回放测试套件固定这些投影均可恢复,且目标激活状态已经解除。包级语义快照固定全屏选择页和输入法光标锚点;交付 TUI 的部署负责其进程与 PTY 交接验收。