Skip to content

fix(web): 合成会话键原样存取,使标记在会话落盘后依然生效 - #354

Closed
onychen wants to merge 1 commit into
openpi-dev:mainfrom
onychen:fix/web-synthetic-session-keys
Closed

fix(web): 合成会话键原样存取,使标记在会话落盘后依然生效#354
onychen wants to merge 1 commit into
openpi-dev:mainfrom
onychen:fix/web-synthetic-session-keys

Conversation

@onychen

@onychen onychen commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Problem

Fixes #351.

当前工作区在活动会话尚未落盘(还没有会话文件)时被移除,removeWorkspace 会把合成标记 current:<sessionId> 先经 resolve() 再写入 ungroupedSessions,实际持久化的是 <cwd>/current:<uuid> 这样的污染键。该键只有在同一进程内写入与查询两侧 resolve 结果恰好一致时才能匹配投影;一旦会话获得真实文件(发出第一条消息),标记永久丢失:会话从所有侧栏分组中消失,/api/sessions/selectrequireSession 以 "Workspace is not available" 拒绝,切走后无法切回。archiveSession 存在同样模式,归档标记同样会丢失。

Value

  • 「删除工作区后其对话会移入 Ungrouped」的确认承诺,对尚未落盘的活动对话现在真正成立——此前恰恰在发出第一条消息时就破裂。
  • 归档未落盘的会话后,归档标记在落盘后依然保留。
  • 不再向 workspace-state.json / archived-sessions.json 累积假的 <cwd>\current:<uuid> 条目。

Approach

遵循 #351 中建议的最小修复:

  • 合成键 current:<sessionId> 现在原样往返:共享的 sessionKey() 辅助(配合 isSyntheticSessionKey)统一用于写入(removeWorkspacearchiveSession)与加载(ensureWorkspaceStateLoadedensureArchivesLoaded);真实路径仍照常 resolve()
  • 投影查询经新增的 hasSessionMark() 辅助,按「resolve 后的真实文件路径」(落盘后记录的标记)或「合成 current:<id> 键」(落盘前记录的标记)双查,标记在会话获得真实文件后依然生效,无需任何迁移。
  • 旧污染条目(<cwd>\current:<uuid>)resolve 幂等、不再匹配任何键——按 bug(web): removing the current workspace hides its unpersisted active session instead of moving it to Ungrouped #351 的决策保留为惰性孤儿,不做迁移。

Validation

  • npm run format:checknpm run lintnpm run typecheck — 通过。
  • node --test --experimental-strip-types tests/web/pi-adapter.test.ts — 15/15 通过,含 2 条新回归测试,完整复现 ungrouped 与 archive 两条流程的「未落盘 → 落盘」转换。
  • 反向验证:stash 修复还原旧代码后,上述 4 条测试全部失败,证明新测试确实锁定该 bug。
  • tests/web 纯逻辑套件(protocol、http-dispatcher、observer-registry、terminal-status、web-host-lease):38 通过 / 0 失败 / 2 平台跳过;app-render.test.ts:21/21。
  • Windows 上全量 node scripts/run-tests.mjs 会命中 test: eliminate Windows background-terminal process-test flakes in the full suite #304 已跟踪的背景终端 kill 测试 flake/挂起(进程管理域,与本改动无关),本机无法完整跑完,如实说明。

Impact

…stence

removeWorkspace and archiveSession resolved the synthetic current:<sessionId> key against the process cwd before persisting it. The polluted key matched the projection only while both sides resolved identically in the same process; once the session gained a real file the mark was lost forever, making the active session disappear from the sidebar after deleting its workspace and unrecoverable via /api/sessions/select.

Keep synthetic keys verbatim on write, load, and lookup via a shared sessionKey() helper, and match persisted marks by either the resolved file path or the synthetic current:<id> key so marks recorded before persistence keep applying after it. Legacy polluted entries stay inert.

Update the two assertions that locked the resolved form and add regression tests covering both the ungrouped and archive flows across the unpersisted-to-persisted transition.

Fixes openpi-dev#351
@onychen

onychen commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

关闭本 PR,原因:经进一步真实验证,修复所针对的问题分支在生产路径上不可达,按维护者与提交者商定的处理方式,连同 #351 一并关闭。

验证过程

搭建了真实 PiWebRuntime + WebHost HTTP 服务的端到端环境,完整重放 #351 的场景(在活动会话未落盘的窗口内删除当前工作区 → 归档该会话 → 首条 assistant 消息触发落盘 → 切走再切回),并对上游旧代码与本修复做了对照实验,两者行为完全一致。

根因核实

  • pi-coding-agentSessionManager.create 在会话创建时即分配 sessionFile 路径,文件内容延迟到首条 assistant 消息才写入磁盘(纯 user 消息不落盘);
  • 因此 web 场景下 getSessionFile() 恒非空,?? \current:${...}`` 合成键回退分支不会执行;
  • 唯一 getSessionFile() 为空的纯内存会话仅存在于 bootstrap 模式,而该模式下 removeWorkspace / archiveSession 无法针对其当前工作区执行,前端也不会发送 current: 路径。

结论

current: 合成键的 resolve() 处理确属潜伏的代码隐患(该分支一旦执行会持久化错误的键),但在当前依赖版本下用户无法触达,#351 描述的用户可见影响(会话消失、无法切回)不会发生。本 PR 的修复逻辑正确且对可达路径零行为差异,但定位应为防御性加固而非 bug 修复,为避免误导先关闭。感谢审阅!

@onychen onychen closed this Sep 2, 2026
@onychen
onychen deleted the fix/web-synthetic-session-keys branch September 2, 2026 06:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(web): removing the current workspace hides its unpersisted active session instead of moving it to Ungrouped

1 participant