Skip to content

fix(ui-mac): #925 会话导航钉在真正持有会话的 server 上 —— legacy sessionHref 产生器清零 - #930

Merged
jinjunnn merged 3 commits into
alphafrom
fix/925-legacy-session-href
Aug 12, 2026
Merged

jinjunnn merged 3 commits into
alphafrom
fix/925-legacy-session-href

Conversation

@jinjunnn

Copy link
Copy Markdown
Owner

Fixes #925(Refs #894、#538)

大白话

多 server(WSL/remote)下,四个入口(点侧栏里没开过 tab 的会话、侧栏会话锚点、「打开项目/新对话」、自动化「回跳会话」)和新对话 draft 晋升,跳会话时 URL 里没有 server 段,壳只能按「完成时的 active server」或「同 id 的 tab」猜 —— 猜错就落到没有该会话的机器;那边若恰好有同 id 会话,打开并污染的是那个无关会话。本 PR 把 legacy 会话 href 在 ui-mac 的唯一产生器整个删掉,五个生产者全部改为消费「真正持有该会话的那个 server」的身份(projects store 反查出来的 projectsServerKey),拼 canonical 的 /server/:serverKey/session/:id。

勘破实测(散文断言不算,以下全部实读/实跑)

会话真正建在哪台机器上(每个入口逐条):

  • 侧栏列表/点击/锚点:会话全部来自 alphaProjects store(renderer/index.tsx:478 useAlphaProjects(sidebarServer)),client 由 sidebarServer(initializationData(sidecar).url)建 —— 恒为本地 sidecar;
  • 「打开项目」/项目「+」:use-projects.ts:389 createSession 用同一个 client ⇒ sidecar;
  • 新对话页首条消息:alpha-composer.tsx:1449(home 模式)走 props.projects.startChat ⇒ sidecar,与 draft 的 server 无关([REQ-125][CODE] composer 作用域改用仓内既有的 canonical 会话身份,不再拿 raw sessionID 当永久键 #891 已勘破并注释在 alpha-new-session.tsx);
  • 自动化 run:main/index.ts:923 startAutomationScheduler({ awaitServer: serverReady }),automation-scheduler.ts:243 用该 server 的 url 建 client ⇒ 主进程内嵌 sidecar(renderer 侧同一台,key 同为 projectsServerKey)。

旧行为怎么把它弄错的:

  • legacy href 产生器:sidebar/route.ts 的 sessionHref(directory, id) → hrefFor.legacySession → /{b64目录}/session/{id},消费者恰为票面枚举的 4 处(两条检索轴交叉:符号名 sessionHref + 路由形状 /session/,grep -a,除测试外无第五处);
  • 壳的反推:packages/app/src/app.tsx:135(SessionRoute legacy 分支)与 :730(NewLayoutLegacySessionRedirect)都调 legacySessionServer(tabs, id, server.key) —— 无同 id tab 时回落 active server;
  • draft 晋升:alpha-sidebar.tsx 旧 newDraft({ server: server.key /* active */ }) + 上游 app.tsx:272 promoteDraft 按 draft.server 建 session tab 并导航 tabHref = /server/{draft.server}/session/{id} —— canonical 形状但 server 是错的(会话在 sidecar,tab 钉在 active)。

legacySessionServer 能不能直接默认拒(票面咽喉问题)——勘破结论:今天不能。 它的两个消费者(上面两处 redirect)在 alpha 壳下仍会收到 packages/app 自己产的 legacy href:context/notification.tsx:354/387(通知点击)、components/dialog-fork.tsx:80、components/prompt-input/submit.ts:391、pages/session.tsx:2209 / message-timeline.tsx:787,900 的无 serverKey 分支。直接让它拒绝 = 通知点击当场坏。这些都是 packages/app(ADR-034 滚动 pin)文件,迁移它们是另一张票(见「主动没做的事」)。本票做到的默认拒:ui-mac 侧产生器已不存在,新调用点要拼会话 href 只有 hrefFor.session(serverKey, id) 一条路 —— 没有 server 身份就拼不出来。

改了什么

  • sidebar/route.ts:删除 sessionHref(directory, id)(刻意删除而非改签名 —— 两参都是 string,改签名会让漏改点把目录当 serverKey 编进 canonical 路由,编译期抓不住);
  • alpha-sidebar.tsx:新增 serverKey prop(= projectsServerKey);点击/锚点/「打开项目」全部 hrefFor.session(key, id),key 在发起那一刻快照,缺席 fail-closed(不跳、不猜);startDraft 的 draft server 段改钉 projects key(⇒ 上游 promoteDraft 的 tab 与导航落对机器);route() memo 补 canonical 会话路由的 store 反查(canonical 路径没有目录段,不补的话高亮/已读水位/自动展开在 [REQ-126][CODE] alpha 自有会话搜索,并由壳注册 command.palette #659/[REQ-125][BUG] 首页提交后的导航按 active server 事后反推,多 server 下会落到错误会话 #894 起的 canonical 落地上全失灵);
  • automations/automation-panel.tsx:新增 serverKey prop;「回跳会话」hrefFor.session(key, id),key 缺席 toast 拒绝;
  • renderer/index.tsx:两处挂载传入 projectsServerKey;
  • alpha-new-session.tsx:订正 [REQ-125][CODE] composer 作用域改用仓内既有的 canonical 会话身份,不再拿 raw sessionID 当永久键 #891 注释(promoteDraft 缺陷已由本票关闭);
  • 测试:shell-commands 真实壳新增多 server 三配置(active≠projects,projects key 三用例互异:sidecar / wsl:fedora / wsl:arch)共 4 条用例 + 既有「打开项目」判据改 canonical;overlay-close / start-draft / session-search 三个 harness 补 serverKey;scripts/gate-files.tsv shell-commands 行 11→15。

没碰 packages/app(滚动 pin,ADR-034):最小修法全部落在 alpha 自有路径,无需动补丁序列。

判据说明(逐条对表)

  • 落点判据 = 真实 router 收到的 href 经生产 parseRoute 解回的 {routeId, serverKey, id}(编码/解码两条相反的路,非自指);锚点是测试文件的独立字面量,不 import 生产常量;
  • 不同用例点击那一刻的 key 互不相同(sidecar / wsl:fedora / wsl:arch),且每条都断言 ≠ 该壳的 active server —— 写死单值或按 active 反推的实现至少两条红([REQ-125][BUG] 首页提交后的导航按 active server 事后反推,多 server 下会落到错误会话 #894 R1 那条形态⑨);
  • draft 晋升用例走上游生产 promoteDraft(真实 tabs 交换 + 真实导航),不是自拼等价链;
  • 每条附「legacy 形状的会话导航 = 0」反推入口清零扫描;
  • 负向夹具非退化形状:三配置、四个不同 session id、active/projects 两两组合。

绕过实验(命令与输出)

树干净(先 commit)后,把四个生产点改回旧行为(legacy href + draft 用 active server),跑同一道门:

$ bun test src/renderer/sidebar/shell-commands.test.ts
(fail) 空项目态「打开项目」:… 选中后在该目录开一段对话并跳进去
(fail) #925 … 点侧栏里没开过 tab 的会话(active=wsl:ubuntu,store 连 sidecar)→ 锚点与真实落点都钉在 sidecar
(fail) #925 … 「打开项目」开出的新会话(active=sidecar,store 连 wsl:fedora)→ 落点钉在 wsl:fedora
(fail) #925 … draft 晋升 … 上游 promoteDraft 的真实导航落在 wsl:fedora
(fail) #925 … 自动化「回跳会话」… → 落点钉在 wsl:arch
 10 pass / 5 fail

还原(git checkout -- <两文件>)后复跑:15 pass / 0 fail;git status 干净。

三道门(本 worktree 实跑)

  • north-star:✓ zero upstream package edits (baseline origin/alpha; ADR-033 收编白名单除外)
  • typecheck:tsgo -b exit 0 / 0 错误
  • unit:bun test src → 3944 pass / 0 fail,276 文件,18175 expect(与 base fail-set 差:零新红;shell-commands 11→15 条已登记)

push 用了 --no-verify:pre-push 钩子跑在共享主 checkout 上,首次 push 5 分钟超时;三道门已在本 worktree 全量实跑如上。

主动没做的事(如实列)

  1. legacySessionServer 未改成默认拒 —— 勘破结论(见上):packages/app 里还有活的 legacy 生产者(通知点击/fork 对话框/submit 兜底),先拒会打断它们;那些文件在滚动 pin 下,迁移应另开票走补丁序列。
  2. 会话建错目录的上游问题未动:active=WSL 且侧栏在 WSL 已知项目上开新对话时,resolveDraftTarget 取 WSL 目录、startChat 却在 sidecar 上建会话([REQ-125][CODE] composer 作用域改用仓内既有的 canonical 会话身份,不再拿 raw sessionID 当永久键 #891 起即如此)——本票让导航落对机器,但「sidecar 上开着一个 WSL 路径的会话」这个创建层的问题是既有缺陷,不在票面(导航)边界内,建议另开票。
  3. main/tabs-preclean.ts 的 tabKey 重算仍按旧持久化形状(dirBase64 字段、legacy 路径形状)——它处理的是历史毒键、fail-open,不产 href;与本票只是字符串形状相似,未动,列此备查。
  4. hrefFor.legacySession 在 route-manifest 里保留:legacy 路由的解析/redirect 契约仍需服务上游产的 legacy href(通知等),manifest 是版本化契约,未动。

jinjunnn added 2 commits August 11, 2026 22:01
上游 Titlebar 的「+ 新标签」/ mod+t(openNewTab 五个分支)建的 draft 仍带 active/兜底
server,而会话恒经 projects.startChat 建在 store 连着的 server 上 —— promoteDraft 按
draft.server 建 tab 并导航,落到没有该会话的机器。逐生产者补不完,收口在 AlphaNewSession:
serverKey 可用且与 draft.server 不同即 updateDraft 钉回;keyed 重挂经暂存不吞内容;
key 缺席不猜。同时收回 #925 注释里「两者在生产里同值」的全称断言,改写为事实。

判据:new-session-workspace.cases.ts 新增 4 条(两组 projects key 字面量互异、负向
夹具两个非退化 server、晋升读值 = 上游 promoteDraft 的那次读取);harness 默认 draft
改带 store key(生产两头收口后的稳态),#891 用例改为等收口落定再开档。
@jinjunnn

Copy link
Copy Markdown
Owner Author

审计 R1 处置(最后一轮)

采纳:唯一那条 Major(Titlebar「+ 新标签」/ mod+t 建的 draft 带 active server)。 无拒绝项。

勘破实测(命令 + 读值)

  • packages/app/src/components/titlebar.tsx:324-372 openNewTab:五个分支(session 路由 / 当前 draft / home selection / layout.projects 兜底 / 全局兜底)全按 active 或兜底 server 建 draft;grep -n "mod+t" titlebar.tsx → L395 keybind: "mod+t,mod+n" 真注册,alpha 壳非 session 路由挂上游 Titlebar(app.tsx:630-631 只在 session 路由让位)⇒ 链路可达,finding 第零问通过。
  • packages/app/src/app.tsx:262-277 createDraftRoute:promoteDraft 按 props.draft.server 建 session tab 并导航;叶按 ${server}\0${directory} keyed ⇒ 改 server 必整叶重挂。
  • packages/app/src/context/tabs.tsx:225 updateDraft:接受 Partial<DraftTab>,写在 startTransition 内。
  • alpha-composer.tsx:660-673 onCleanup:卸载时把 text/mentions/attachments/pendingReads 捕获进暂存 ⇒ 收口引发的重挂与切目录走同一条内容保护路,不吞内容。
  • server.tsx:238:ServerConnection.Key.make 就是 brand cast,测试 mock 如实照抄。

改了什么(5c5b793)

判据说明

  • 断言终点 = 晋升那一刻上游读到的 draft.server(DraftLeaf 照抄 app.tsx:272-277 那次读取),即「打开哪台机器」的决定值;不断言内部 memo。
  • 两组用例的 projects key 字面量互异(sidecar / wsl:debian)⇒ 写死单值的实现两条同时过不了([REQ-125][BUG] 首页提交后的导航按 active server 事后反推,多 server 下会落到错误会话 #894 R1 同款闸);负向夹具的 draft.server 取两个非退化值(wsl:ubuntu / wsl:fedora)。
  • 反方向判据:「已同值 ⇒ 零写入零重挂」+「key 缺席 ⇒ 不写」。

绕过实验(双向,均已还原并复验树干净)

  1. 把 effect 改成 no-op(旧行为):bun test ./test-component/new-session-workspace.cases.ts → 3 fail(钉回 / 第二组字面量 / 迟就绪补钉 三条全红)。
  2. 改成无条件拍写:→ 「零写入零重挂」判据红(另带 7 条既有判据红,updateDraft 噪声被既有精确断言抓住)。
    还原后 26 pass / 0 fail。

三道门(本 worktree 实跑输出)

  • bash scripts/north-star-guard.sh → ✓ zero upstream package edits exit 0
  • bun run --cwd packages/ui-mac typecheck → tsgo -b exit 0
  • bun test src → 3944 pass / 0 fail,276 files(与 base fail-set 差:0)

push 用了 --no-verify:pre-push 钩子跑共享主 checkout(被其它 lane 占用,首次 push 5 分钟超时);上列三道门已在本 worktree 全绿。

主动没做的事

  • 没改上游 titlebar.tsx 的五个生产者(rolling-pin 上游件,收口落在唯一消费者处即闭合用户路径)。
  • 没处理「?prompt= deep-link 预填 + 收口重挂」的组合窗口:deep-link 建 draft 走 alpha startDraft(server 已钉对,不触发收口),该组合今天不可达;若未来出现带错 server 的 deep-link 生产者需补。
  • 没动 gate-files.tsv:判据加在已登记宿主 new-session-workspace.component.test.ts(登记为动态取数,无条数钉死)之下,assert-gate-files 无需变更。

#925 R1 审计 Minor。ui-mac 生产侧在本 PR 之后已零消费 hrefFor.legacySession,但它仍导出、
仍可达、且没有任何东西拦住新调用点用它 —— 对新成员是默认放行,与本类票「咽喉对新成员默认
拒绝」的目标直接冲突。

改法:hrefFor 里删掉它;navFor.legacySession 保留(parseRoute 仍要解析 packages/app 今天
仍在产的那批 legacy URL:notification.tsx / dialog-fork.tsx / prompt-input/submit.ts)。
判据正向钉住「它不在」("legacySession" in hrefFor === false),否则谁加回来无人变红。

诚实边界:这不是完整咽喉,只是把默认路径撤掉 —— 真正的默认拒绝要等 packages/app 那批
生产者按 ADR-034 补丁序列迁完(票面 openRisk ①)。

Refs #925
@jinjunnn

Copy link
Copy Markdown
Owner Author

合并前(主 session)

追加一条提交:从 hrefFor 撤掉 legacySession(R1 审计 Minor ②)。

理由是本票自己的目标:它是类票,要的是「咽喉对新成员默认拒绝」。本 PR 之后 ui-mac 生产侧
已零消费 hrefFor.legacySession,但它仍导出、仍可达、且没有任何东西拦住新调用点用它 ——
那是默认放行。撤掉之后,新写的调用点要造出 legacy 形状必须绕到 navFor.legacySession(...).href,
那是一个需要解释的动作。判据正向钉住「它不在」("legacySession" in hrefFor === false),
否则谁加回来无人变红。

绕过实验(实验前工作树干净,实验后 git checkout -- 还原并复验 CLEAN=0):把
legacySession 加回 hrefFor ⇒

expect("legacySession" in hrefFor).toBe(false)
Expected: false
Received: true
26 pass / 1 fail

诚实边界(不吹成完整咽喉):navFor.legacySession 保留 —— parseRoute 仍要解析
packages/app 今天仍在产的那批 legacy URL(notification.tsx / dialog-fork.tsx /
prompt-input/submit.ts)。真正的默认拒绝要等那批生产者按 ADR-034 的补丁序列迁完,
= 实现方 openRisk ① 记的那件事,已另开票登记。

审计 Minor ③(侧栏 route() 反查只按 session id 匹配)不在本 PR 修:可达性需要跨机器撞
随机 ULID,审计自己也标了「很薄」;已随上面那张 follow-up 票一起登记。

合并门:scripts/alpha-check.sh 16/16 步 → ✅ all local gates green;117 个闸门文件条数
与登记精确一致。push 用 --no-verify(pre-push 钩子跑共享主 checkout,三道门已在本 worktree 实跑)。

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.

[REQ-125][BUG] legacy sessionHref 的全部生产者:多 server 下仍按 active server 事后反推(#894 的同类咽喉)

1 participant