Skip to content

refactor(session): 开场激活的分散串行化 fence 收进按 session 的命令队列(Stage 3) - #1308

Merged
deepcoldy merged 4 commits into
masterfrom
feat/session-turn-queue
Sep 8, 2026
Merged

refactor(session): 开场激活的分散串行化 fence 收进按 session 的命令队列(Stage 3)#1308
deepcoldy merged 4 commits into
masterfrom
feat/session-turn-queue

Conversation

@LucasIcarus

@LucasIcarus LucasIcarus commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

改了什么

设计文档 docs/design/2026-08-12-session-restage-store-first.md §3 Stage 3:同一 sessionId 上的命令跨 await 后仍要串行。此前这个保证由开场激活窗口里一组互相配合的机制拼出来:到达时预留 FIFO 序号、queuedActivationTailAdmissionsOutstanding 计数、queuedActivationTailReleasePending 延迟交接、settle 时回放释放、100ms 重试定时器。它们存在的唯一原因是「到达 → 构造 prompt(await 发送者查询)→ 落 durable tail」中间可能被后到的消息或 worker 的开场 ACK 插队。

本 PR 用一条按 session 的命令队列取代这组机制:

  • 新增 src/core/session-turn-queue.tsrunSessionTurn(sessionId, command) 是按 sessionId 的 Promise 链,命令按入队顺序执行、跨自身 await 不交错,前一条失败不阻塞后一条,链空即回收;hasPendingSessionTurns(sessionId) 供入口判定。不引入 mailbox / actor 类型。
  • daemon 两处 follower 入口(initialStartPending 下的同 anchor 后到消息;worker 已死、带 retained journal 的 refork 前置 staging)统一走 admitFollowerBehindOpening:到达时同步取序号,构造 + 落盘作为一条命令入队。若开场的释放已先跑完(tail 空、路由已放开),同一条命令内联 promote,不再排队。pendingRepo 分支从到达到落盘没有 await,直接同步落盘。
  • 开场路由释放 releaseQueuedActivationReservation(worker queued_activation_submitted ACK、普通冷 fork 后的交接、失败重试)入队,因此必然在先到的 follower 落盘之后执行;worker-pool 的 onQueuedActivationSubmitted 回调相应变为异步(回调类型本来就允许 Promise)。
  • hasQueuedActivationAdmissionGate 改查队列:队列上有命令时,live worker 的普通 turn 也进 durable tail,不得插队;forkReservedInitialSession 与原始命令冷启动对计数的判定同样改为查队列。
  • 删除 queuedActivationTailAdmissionsOutstandingqueuedActivationTailReleasePendingreserveAsync… / settleAsync… 及其回放和测试钩子。保留的重试定时器只负责 promote 落盘 / IPC 失败后的重试,不再承担排序。
  • 删除从未被写入的 pendingQueuedActivationFollowUpsreparkQueuedActivationFollowUpTailfix(codex-app): make turn ownership and recovery durable #597 落地 durable tail 之后没有任何写入方,repark 恒返回 false,worker exit / error 里对它的调用也一并删除。

有意保留、没有入队的

  • initialStartClaimToken / initialStartPending:fork 边界的所有权,跨越整段资源准备(秒级)。期间到达的 follower 必须立刻落 durable tail 才扛得住 daemon 崩溃,把整段准备做成队列命令会让 follower 在内存里等待。它是状态,不是 await 间隙。
  • pendingRepo 等待期的 pendingFollowUps* 缓冲:等人点卡片,队列不能被人拿着。
  • admitQueuedActivationTail / promoteQueuedActivationTail 里「store 外备份再回滚」的写法:删除条件不变(daemon 侧出现按命令更新且不替换 ds.session 引用的 apply 入口);让 admit 走它需要新的 store 导出并改多个 stub 了 updateSession 的测试夹具,本轮未做。
  • worker generation / exit 路径上的 updateSession:没有复现证据,按文档「没有复现的路径不改」不动。

设计文档

  • Stage 3 标为已落地并写明范围与保留项。
  • Stage 4 降为低优先级、暂不立项:触发条件是单 bot 进程的事件循环或崩溃域不可接受,目前没有证据(08-23 的恢复风暴在 tmux server 层)。
  • Stage 0 / Stage 1 收尾按升级窗口已关闭排期,范围清单写入 §5(含 mojo-containment 的 revoke 安全闸仍直接扫 JSON、SQLite 后静默失效这一条),作为下一个 PR。

影响面

  • 只动 daemon 进程内的开场激活路径(daemon.tshandleThreadReplyAdmitted / handleNewTopic 两处 follower 入口、forkReservedInitialSessionforkReservedInitialRawSession、release 回调)与 worker-pool.ts 的 admission gate 和 worker exit / error 处理;不动 session-store、CLI、dashboard、沙盒。
  • 所有 CLI 共用这条路径:codex-app 的 clean-input 决策仍在到达时冻结(序号预留未变),steer 授权的 COPY 点未变;PTY / tmux 后端与话题 / 群 / adopt / restore 会话类型无差别。
  • 时序上的行为差异只有一处:以前 forkReservedInitialSession 立即释放且失败不重试、延迟释放才重试;现在两种情况统一走队列并在失败时重试。

验证

  • bun run build 绿,tsc --noEmit 绿。
  • 新增 test/session-turn-queue.test.ts(5 条:入队顺序、跨 session 不互斥、失败不阻塞、pending 判定、链回收)。
  • test/daemon-rename-route.test.ts 里三条原本对计数/延迟交接断言的用例改为对队列顺序断言(ACK 落在 follower 构造中、ACK 后 worker 退出、follower 落盘失败),新增两条:开场释放先于 follower 跑完时 follower 内联 promote;follower 构造期间 admission gate 保持关闭。
  • 受影响的 21 个测试文件(含 session-lifecycle-startworker-ready-display-modetrigger-session-*session-resumedashboard-create-session 等)1029 条全过。
  • 全量 vitest run --project unit:1266 个文件通过、7 个跳过;21902 条通过、108 条跳过,0 失败。

真机验证

  • bun run switch:here && bun run daemon:restart:四个 bot 由新进程持有 occupancy 租约,活跃会话正常恢复,日志无 ERROR。
  • 普通新话题连发两条:第二条在 worker fork 完成后到达,走原有的 worker 侧排队,顺序正确(本 PR 不改这条路径)。
  • 从 Dashboard 待办池把会话拖到「进行中」启动,在开场被 CLI 确认接收之前发出第二条:daemon 日志出现「buffered same-anchor turn … behind queued activation submission ACK」(第二条经队列命令落入 durable tail),开场被 CLI 接收并回 ACK 后,第二条被提升并送入 worker;两条回复顺序正确,会话行的激活 journal 与 tail 均已清空,序号预留计数为 1。

LucasIcarus and others added 3 commits September 8, 2026 13:17
同一 sessionId 上的命令跨 await 后仍需串行。此前这个保证由开场激活窗口里
一组互相配合的机制拼出来:到达时预留 FIFO 序号、AdmissionsOutstanding 计数、
ReleasePending 延迟交接、settle 时回放释放、100ms 重试定时器。它们存在的唯一
原因是「到达 → 构造 prompt(await 发送者查询)→ 落 durable tail」中间可能被
后到的消息或 worker 的开场 ACK 插队。

- 新增 core/session-turn-queue.ts:按 sessionId 的 Promise 链,命令按入队顺序
  执行、跨自身 await 不交错,前一条失败不阻塞后一条,链空即回收。不引入
  mailbox / actor 类型。
- daemon 两处 follower 入口(initialStartPending 下的同 anchor 后到消息、
  worker 已死带 retained journal 的 refork 前置 staging)统一走
  admitFollowerBehindOpening:到达时同步取序号,构造 + 落盘作为一条命令
  入队;若开场的释放已先跑完(tail 空、路由已放开),同一条命令内联 promote。
  pendingRepo 分支从到达到落盘没有 await,直接同步落盘。
- 开场路由释放 releaseQueuedActivationReservation(worker ACK、普通冷 fork 后
  的交接、失败重试)入队,因此必然在先到的 follower 落盘之后执行;
  worker-pool 的 onQueuedActivationSubmitted 回调相应变为异步。
- hasQueuedActivationAdmissionGate 改查队列:有命令在队列上时 live worker 的
  普通 turn 也进 durable tail,不得插队。
- 删除 queuedActivationTailAdmissionsOutstanding、
  queuedActivationTailReleasePending、reserveAsync/settleAsync 及其回放;
  保留的重试定时器只负责 promote 落盘 / IPC 失败后重试。
- 删除从未被写入的 pendingQueuedActivationFollowUps 与
  reparkQueuedActivationFollowUpTail(#597 durable tail 落地后 repark 恒返回
  false)。
- 设计文档:Stage 3 标为已落地并列出有意保留项(fork 边界的 claim、
  pendingRepo 等待期缓冲、admit/promote 的回滚写法、worker exit 路径);
  Stage 4 降为低优先级、暂不立项;Stage 0 / Stage 1 收尾按升级窗口已关闭
  排期,范围清单写入 §5。
重试定时器不能只看 initialStartPending:ACK 已清路由时,promote 落盘失败
仍要把 durable tail 送出去,不能依赖 promote 在抛错前顺手改回 pending。

Co-authored-by: Cursor <cursoragent@cursor.com>
Bun 的 ReadStream 没有 unref(),destroy()+unref 是空操作;afterAll 的
kill-server 超时只发 SIGTERM。两条都过了之后进程仍不退出,CI bun-test
就被 per-file wall SIGKILL。teardown 改走 close(),清理用 SIGKILL,
runner 在出结果后空闲一个 test timeout 即收掉残留 handle。

Co-authored-by: Cursor <cursoragent@cursor.com>
@LucasIcarus
LucasIcarus force-pushed the feat/session-turn-queue branch from 29d659f to 86b051a Compare September 8, 2026 05:19
@LucasIcarus
LucasIcarus marked this pull request as ready for review September 8, 2026 05:20
cfc425a 仍是当前 master 祖先,只是过期。改记 origin/master@61dadb04c。

Co-authored-by: Cursor <cursoragent@cursor.com>

@deepcoldy deepcoldy left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

双审通过(0 阻断)。

首审 + 复审两轮结论一致:

  • 队列本体逐行核过;反变异 7 枪 5 红,2 绿均插桩定性为非缺陷(惰性编辑 / 下游在抛错前置真 flag)。
  • 无回归:本地全量两次 vs 干净 master 同负载对照,「两次都失败」∩「master 不失败」= 空集。
  • 自等死锁风险已排查:生产侧 2 个入队点 / 3 个查队列点全部核过调用上下文,无同 session 嵌套路径。
  • 必需 check(build / test)全绿;bun-test 1106/1106 files green。
  • 非阻断的设计文档 baseline 已由作者自行修正(f83c1ca2b)。

说明:github-advanced-security 这条红与代码无关——日志根因是 You are not licensed to use Copilot(403 / errorType: authentication),且该 check 不在必需列表(ruleset 只要求 build + test)。

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.

2 participants