DSH / Atlas
2026-09-29implementedbug-fix

A reused blank Session runs the requested preset

被复用的空白会话运行请求的 preset

Web New Session, Workspace navigation, and startup restoration reused a blank Session by naming it in `session.create`. The Host resumed that Session under the preset its log recorded, so a new conversation inherited the preset the blank had been created or switched under. When that preset was no longer declared, such as a removed or renamed definition or a legacy `$DSH_HOME/.agent-presets/<id>/` directory that nothi

English

Problem

Web New Session, Workspace navigation, and startup restoration reused a blank Session by naming it in session.create. The Host resumed that Session under the preset its log recorded, so a new conversation inherited the preset the blank had been created or switched under. When that preset was no longer declared, such as a removed or renamed definition or a legacy $DSH_HOME/.agent-presets/<id>/ directory that nothing reads any more, every New Session in that Workspace and every startup restoration of that draft failed with agent-preset/not-found although the default preset was valid (issue #2566). A blank created under an earlier default silently kept that default. The client retried only session/writer-held, so the stale blank stayed first in line.

Decision

session.create returns a Session for a new conversation. A named sessionId is a reuse candidate that the Host verifies: it must not have started a turn, must belong to the requested directory, must be an unarchived member of the requested Workspace, and must not be written by another owner. For a persisted candidate the Host repeats the started check under the writer it acquires. Requests naming one identity run in arrival order, including their preset resolution, and the directory and ownership checks precede that resolution. The reused Session runs the requested agentPreset, or the configured default when the request names none; the Host does not resolve the recorded preset for reuse. When the target differs, a live Session switches through the preset registry's select, and a persisted Session mounts the target in its unpublished resume setup and appends agent-preset/selected there, so the log names the composition before the Agent is published. A candidate that has started, fails placement, or is written by another owner stays unchanged, and the Host creates a new Session; the response names the Session used. An explicit request for an undeclared preset still fails with agent-preset/not-found, and resuming a started Session still resolves its recorded identity.

The Web client names the first eligible blank when it connects a Workspace or prepares an explicit draft, and passes the preset a flow knows before it starts, such as the creator entry's cordis; a plain New Session creates a fresh Session instead. Startup restoration names the saved blank with the preset its log recorded, reloading the Session's projections first because listed values are cached hints that can lag the log after an unclean Host exit, so a draft keeps its selection; after agent-preset/not-found or agent-preset/invalid it asks again without a preset, and the draft continues under the default. When the Host declines a named blank, the client reloads that Session's projections, because stale cached list metadata can still mark a started conversation blank and hide it from the Session tree.

Alternatives considered

Fall back to the default only when the recorded preset is missing. This removes the failure but keeps an inherited preset whenever it still resolves, so a New Session after a default change runs the old default.

Retry agent-preset/not-found in the client. The stale blank stays the first reuse candidate, so later New Sessions fail once before creating another Session, and startup restoration and other callers still fail.

Skip blanks whose preset is missing from the roster. Navigation would need the roster, stale Sessions would never be repaired, and other callers would still fail.

Let the Host choose the blank. The Host would scan the catalog on every request, while the client already holds the ordered catalog; verification under the writer covers a stale client view.

Keep a restored draft's preset on the Host when it resolves. A request that names a blank without a preset would then mean "keep" for restoration and "default" for New Session. Passing the recorded preset gives each request one meaning.

Consequences

A reused blank runs the default or the preset its flow names, including after the default changes or a definition disappears. A preset picked on a draft survives a page reload but not a later Workspace connection or draft preparation that reuses the draft, and reusing a draft that another tab shows changes that tab's preset too. Writer contention no longer reaches the client: the Host creates the replacement Session. A blank whose first prompt was accepted but whose turn has not started still counts as blank, as it does for select. A declined stale blank is corrected only in the client that named it, until the Session is next activated. The preset registry's select leaves a blank Session that records the requested preset and runs its current revision unchanged, so a client that also selects that preset after the creation request adds no second record.

This note replaces the client-side contention fallback of Writer acquisition for blank Session reuse; writer acquisition and candidate order remain as decided there.

Testing

Session Controller tests cover live and persisted reuse, a recorded preset that is no longer declared, named presets, started and overtaken Sessions, writer contention, Workspace placement, storage failures, arrival order when an earlier preset resolves later, and directory checks before preset resolution. Workspace navigation tests cover restoration with and without a usable recorded preset, restoration when the listed preset lags the log, connection coalescing that a different preset request supersedes, and the projection reload for a declined blank; the preset plugin tests cover the creator entry, and the registry tests cover a repeated selection with and without a replaced definition. The shipped Web composition test reuses a blank recorded under an undeclared preset under the default when startup connects its Workspace and when startup restores it, keeps a restored draft's declared preset, and checks the durable agent-preset/selected record; the stale blank cache scenario checks that startup opens a new Session and still lists the declined conversation.

中文

Problem

Web 的新建会话、工作区导航与启动恢复过去通过在 session.create 中指明空白会话来复用它。Host 过去按日志记录的 preset 恢复该会话,因此新对话继承了空白会话创建时或切换后的 preset。当该 preset 已不再声明时,例如定义被删除或改名,或是如今已无人读取的旧版 $DSH_HOME/.agent-presets/<id>/ 目录,即使默认 preset 有效,该工作区中的每次新建会话以及该草稿的每次启动恢复都会以 agent-preset/not-found 失败(issue #2566)。在更早默认值下创建的空白会话则会静默保留那个旧默认值。客户端只对 session/writer-held 重试,因此过期的空白会话始终排在复用候选首位。

Decision

session.create 返回一个用于新对话的会话。请求指明的 sessionId 是由 Host 核实的复用候选:它必须尚未开始任何轮次、属于请求的目录、是请求的工作区中未归档的成员,且未被其他写者占用。对于持久化候选,Host 会在取得写锁后再次检查它是否已开始。指明同一标识的请求按到达顺序执行,preset 解析也包含在内,且目录与归属检查先于该解析。被复用的会话运行请求的 agentPreset,请求未指明时运行配置的默认 preset;Host 复用时不解析记录的 preset。目标与当前不同时,活动会话通过 preset 注册表的 select 切换;持久化会话在尚未发布的恢复 setup 中挂载目标 preset,并在同一处追加 agent-preset/selected,使日志在 Agent 发布前就写明其组成。已开始、不满足归属或被其他写者占用的候选保持不变,Host 改为新建会话;响应指明实际使用的会话。显式请求未声明的 preset 仍以 agent-preset/not-found 失败,恢复已开始的会话仍解析其记录的标识。

Web 客户端在连接工作区或显式准备草稿时指明第一个符合条件的空白会话,并传入流程在开始前已确定的 preset,例如创造模式入口的 cordis;普通新建会话则直接新建 Session。启动恢复指明已保存的空白会话及其日志记录的 preset,并先重新加载该会话的投影,因为列表中的值是缓存提示,Host 非正常退出后可能落后于日志,从而使草稿保留其选择;遇到 agent-preset/not-found 或 agent-preset/invalid 后改为不带 preset 再请求一次,草稿在默认 preset 下继续。Host 拒绝复用某个指明的空白会话时,客户端会重新读取该会话的投影,因为过期的列表缓存元数据可能仍把已开始的对话标为空白,并把它从会话树中隐藏。

Alternatives considered

只在记录的 preset 缺失时回退到默认值。 这能消除失败,但只要继承的 preset 仍可解析就会保留它,因此默认值变更后的新建会话仍运行旧默认值。

在客户端重试 agent-preset/not-found。 过期的空白会话仍是第一个复用候选,之后每次新建会话都会先失败一次再新建,启动恢复和其他调用方仍会失败。

跳过 preset 不在列表中的空白会话。 导航需要读取 preset 列表,过期会话永远得不到修复,其他调用方仍会失败。

由 Host 选择空白会话。 Host 每次请求都要扫描会话列表,而客户端已持有排好序的列表;在写锁下的核实足以覆盖客户端视图过期的情况。

恢复草稿时由 Host 在记录的 preset 可解析时保留它。 这样一个指明空白会话但未带 preset 的请求,在恢复时表示「保留」,在新建会话时表示「默认」。传入记录的 preset 让每个请求只有一种含义。

Consequences

被复用的空白会话运行默认 preset 或其流程指明的 preset,默认值变更或定义消失之后也是如此。在草稿上选择的 preset 在刷新页面后仍保留,但之后复用该草稿的工作区连接或草稿准备不会保留它;复用另一个标签页正在显示的草稿也会改变那个标签页的 preset。写锁争用不再到达客户端:由 Host 新建替代会话。首条提示词已被接受但轮次尚未开始的空白会话仍算空白,与 select 一致。被拒绝复用的过期空白会话只在指明它的客户端中得到纠正,直到该会话下次被激活。若空白会话已记录所请求的 preset 且正运行其当前版本,preset 注册表的 select 不做改动,因此客户端在创建请求之后再选择同一 preset 也不会追加第二条记录。

本注记取代 复用空白 Session 时获取写锁 中的客户端锁争用回退;写锁获取与候选顺序仍按该决策执行。

Testing

Session Controller 测试覆盖活动会话与持久化会话的复用、记录的 preset 已不再声明、指明的 preset、已开始与被抢先开始的会话、写锁争用、工作区归属、存储失败、较早请求的 preset 解析较慢时仍按到达顺序执行,以及目录检查先于 preset 解析。工作区导航测试覆盖记录的 preset 可用与不可用时的恢复、列表中的 preset 落后于日志时的恢复、被不同 preset 请求取代的连接不再合并,以及被拒绝的空白会话的投影重读;preset 插件测试覆盖创造模式入口,注册表测试覆盖定义未替换与已替换时的重复选择。实际 Web 组合测试在启动连接工作区与启动恢复时,把记录了未声明 preset 的空白会话复用到默认 preset 下,验证恢复的草稿保留已声明的 preset,并检查持久化的 agent-preset/selected 记录;过期空白缓存场景验证启动时打开新会话,并仍列出被拒绝复用的对话。