Skip to content

fix: 每张卡都能跳回它的会话;修掉「进行中」死锁 - #50

Merged
cloader merged 1 commit into
cloader:mainfrom
huandu:fix/review-feedback-loop
Oct 16, 2026
Merged

cloader merged 1 commit into
cloader:mainfrom
huandu:fix/review-feedback-loop

Conversation

@huandu

@huandu huandu commented Oct 16, 2026

Copy link
Copy Markdown

现象(真实环境复现)

在一次"检查某库在最新 Go 上能否运行、有问题就开子任务修"的验收流程里报了 4 个现象,最后定位到两个缺陷:

  1. 看板 7 张卡里只有 1 张能从详情页跳回它的会话(双击卡片正常,详情页不行);
  2. in_review 的卡点「Move to in progress」后不触发重跑,而且此后「▶ 立即执行」按钮消失、接口执行也被拒——必须先退回 todo 才能重跑;
  3. 卡上留言后没有任何响应;
  4. 想"在任务上下文里提意见并回到进行中"没有可用路径。

(3)(4) 是能力缺口,不在本 PR 范围,见文末。

根因一:同一份解析规则有两份实现,详情页那份少了一档

TaskCard 和 TaskDetail 各维护了一份"这张卡指向哪个会话"的实现:

  • TaskCard.resolveTaskSessionId():运行中的执行 → 最近一次带 sessionId 的执行 → claimedBy → 定时绑定 → createdBy(创建它的会话) → 最后编辑它的 agent;
  • TaskDetail 内部那份止步于 claimedBy,没有 createdBy 这一档。

而在会话里创建、也在同一会话里手工认领干完的卡不会产生 execution 记录(没有任何东西 spawn 过会话)。于是这类卡在详情页里没有任何跳转目标——报告的看板上 7 张里 6 张正是这一类。

同一根因也解释了那几张卡的 taskboard_execution_report 为何失败(no running execution and no settled execution of yours to report on):它们确实没有执行记录。

根因二:拿"状态标签"当了"有没有活"

两处都只看状态:

  • execution.run:target.status === 'in_progress' 直接拒绝;
  • 详情页:canRun = status !== 'in_progress' && …,同一条件把按钮藏起来。

于是"用户手动移回进行中"的卡进入一个没有任何东西在跑、却又不能跑的状态,只能退到 todo 绕回去。

旧测试 rejects a run on a running or unknown task 的夹具本身就是一张裸 in_progress 卡——它把这个混淆固化进了测试。本 PR 改的是夹具(补上真正 running 的执行),断言意图未变。

解法

1. 解析收敛成一个共享纯函数 taskSessionId()(src/shared/protocol.ts),两个调用点都改用它,优先级由测试锁死:

运行中的执行 → 最近一次带 sessionId 的执行 → 持有者 → 定时绑定 → 创建它的 agent 会话 → 最后编辑它的 agent 会话。

它刻意与 taskAssociatedSessionIds() 分开,并在两边都写了注释:后者回答"这张卡拥有哪些会话"(用于归档),绝不能把用户自己的会话卷进归档;前者回答"人在这张卡的哪个会话里说话"。两边各加了一条测试,防止再被合并或再分叉。

2. 两处判断改成"有没有活":

  • execution.run 只在存在 running 执行或有会话持有认领时拒绝;
  • 详情页 canRun 同理(done/archived 之外,只要没有 running 执行、没有持有者)。

重跑会重新认领这张卡,所以"没人持有的进行中"会自我修复,而不是一直挂着。

3. 刻意不动的地方:Move to in progress 仍是纯状态变更,不会自动起一次执行。"用户手动置为进行中"是否等于"重开并重跑"是个产品判断,本 PR 不替它决定。

4. README 的跳转说明同步到新行为(中英双语)。changelog 与「升级日志」按本仓库既有习惯留给 release 提交,需要的话我可以补。

验证

  • npx tsc --noEmit 通过;npx vitest run 478 通过。新增 5 条:解析优先级、导航集合与归档集合互不污染、搁浅卡可重跑、持有/运行中仍拒绝、详情页按钮可见性。
  • 用报告者真实台账(7 张卡)核对:修复前 1/7 能解析出会话,修复后 7/7;同时归档集合对这类卡仍是空集,确认没有把用户会话卷进归档。

未涉及

留言唤醒任务会话("在 comment 里直接对话")。报告者认为"留言没人理"像 bug,但实现它意味着一条评论就触发一次 agent 推理,涉及成本与连发行为的设计,不适合塞进这个修复。会另开 issue 讨论设计(默认关的开关,还是别的形式),本 PR 只修上面两个缺陷。

一个已知边界(未变,仅记录)

被用户置为 in_progress、又没有任何东西在跑的卡,agent 无法认领(认领只允许 todo → in_progress),只能从 GUI 重跑或退回 todo。这是既有状态机性质,本 PR 未改动;要让它可被认领是状态机层面的另一个决定。

Two defects behind "only one card can return to its session" and "moving a
card back to in progress does nothing".

Session link (three of the four reported symptoms followed from this):
TaskCard and TaskDetail each resolved a card's session with their own copy of
the same rules, and the detail pane's copy had lost the fallback to the
conversation that created the card. A card created in a conversation and
worked on in that conversation records NO execution (nothing spawned a
session), so on the reporter's board exactly 1 of 7 cards resolved; the other
six could not be navigated back. One shared pure helper (`taskSessionId`) now
serves both call sites, with a lock on the precedence order. It stays
deliberately separate from `taskAssociatedSessionIds`, which answers "which
sessions does this card OWN" for archiving and must never include the user's
originating conversation.

Unstuck in progress:
`execution.run` refused on `status === 'in_progress'` alone, and the detail
pane hid the run button on the same condition. Together they turned a card the
user moved back from review — in progress with nothing holding it — into a
dead end that had to be pushed back through todo. Both now test for work
actually in flight (a live execution, or a session holding the claim), and a
run re-claims the card so the orphan state repairs itself.

The existing "rejects a run on a running task" fixture was itself a bare
in_progress card, i.e. it encoded the conflation; it now carries a genuinely
running execution, so its intent is preserved rather than relaxed.
@huandu

huandu commented Oct 16, 2026

Copy link
Copy Markdown
Author

另外,我注意到 ./lib 目录里面都是一些 build 生成的文件,这些文件不应该提交到 git 吧,这是不是也是个问题?如果是一个问题,我可以在这个 PR 里面顺便的修复这个问题,将这个目录加入到 .gitignore 然后删除目录。

@cloader
cloader merged commit 879d695 into cloader:main Oct 16, 2026
1 check passed
@cloader

cloader commented Oct 16, 2026

Copy link
Copy Markdown
Owner

另外,我注意到 ./lib 目录里面都是一些 build 生成的文件,这些文件不应该提交到 git 吧,这是不是也是个问题?如果是一个问题,我可以在这个 PR 里面顺便的修复这个问题,将这个目录加入到 .gitignore 然后删除目录。

lib目录是方便通过github安装插件而不是npm安装插件使用的,所以需要提交。

@huandu

huandu commented Oct 20, 2026

Copy link
Copy Markdown
Author

如果通过 Github 按照,其实也可以考虑本地运行一下 build 来避免提交 lib 目录。毕竟启动 dsh 就需要 Node.js,本地开发环境肯定是有的,build 并不会有什么技术上的阻碍。

@cloader

cloader commented Oct 20, 2026

Copy link
Copy Markdown
Owner

如果通过 Github 按照,其实也可以考虑本地运行一下 build 来避免提交 lib 目录。毕竟启动 dsh 就需要 Node.js,本地开发环境肯定是有的,build 并不会有什么技术上的阻碍。

好的 那我这边改一下

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