AI

审查机点了 Request changes,开这个 PR 的代理会话已经关了:谁来改下一刀?(中英切换)

早上九点十二分,4 号机在 GitHub 上点了 Request changes。三条线程写得很清楚:整数分运算、未知 tier 要拒、补奇数分测试。PR 还是昨晚 2 号机代理开的那条——agent/billing-discount,会话 id 还写在 commit message 里:sess-m2-8841。

问题是:sess-m2-8841 凌晨两点机器重启时已经没了。2 号机现在在跑另一张票的新会话。1 号机空着。3 号机满载。没有人开着 GitHub 通知面板盯着。审查意见落在线程里,下一刀代码该谁推?

三人、七台机:1–3 开发(代理高频开 PR),4–5 审查,6 Windows 验证,7 合并/运维。这篇文章不谈「修完了没有」(那是另一篇核对评论是否落地的事),也不谈任务做到一半怎么跨机交接 WIP。它只问:CHANGES_REQUESTED 撞上已死的开 PR 会话时,怎么把活着的 PR 绑到一台还醒着的开发机上,且不丢掉审查线程。

下面是我在本机搭的玩具仓库:真实 git 分支与修复提交;审查状态、标签、租约写在 JSON 里——不是 live GitHub API;Actions YAML 是可抄示意,没有对 live 仓库开火;没有在 Windows 上跑过。数字都来自这一次实验。

场景:审查意见在,作者会话不在

Request changes 之后会话已死

项 值
PR #512 billing: coupon tiers
开 PR 机器 machine-2 · 会话 sess-m2-8841
会话状态 dead(过夜重启)
审查 machine-4 · CHANGES_REQUESTED · 3 条未解决线程
空闲开发机 machine-1(machine-3 忙)

线程不是「感觉上要改改」,是可定位的:

  1. discount.py 浮点截断 → 改成 price * (100-pct) // 100
  2. table[tier] 遇错 tier 直接 KeyError → 显式 ValueError
  3. 测试缺 999@15% 与未知 tier

这些文字只活在 GitHub 线程里。死会话的 transcript 里没有它们——昨晚代理交卷时审查还没发生。

四种唤醒,同一种缺口

我让同一条 PR 在四种策略下各过 5 个合成早市,量四件事:有没有机器真的开始改(wake)、到第一次「修复向」推送的分钟数、漏掉几条线程、有没有两台机同时认领、有没有叫醒了不该叫的忙机。

四种策略对比

策略 唤醒率 平均首修(分) 漏线程率 双认领 错机 整天没人改
A 人收件箱 / 静音疲劳 60% 89.7 53.3% 0% 20% 2/5
B @mention 机器人账号 0% — 100% 0% 0% 5/5
C 标签 + 租约 pickup.py 100% 23.2 26.7%* 0% 0% 0/5
C + ack-threads 门闩 100% 26.4 0% 0% 0% 0/5
D Actions 交接评论 → 轮询认领 100% 29.2 0% 0% 0% 0/5

*C 的 26.7% 全部来自「2 号机(原作者死会话那台)抢到租约后凭记忆续写、没 ack 新线程」——加门闩后降到 0%,首修只多约 3 分钟。

A:人盯收件箱——默认会漏

审查员或 Tech Lead 靠邮件/App 通知。代理时代通知密度高,静音是理性选择。模拟里 5 天有 2 天完全没人注意到 CHANGES_REQUESTED;注意到的平均也要约 90 分钟才切到空闲机开改,而且口头转述线程时大约一半的早市会漏至少一条。错机率 20%:习惯性去敲还在忙的 3 号机。

这不是「人不够勤」。是通道与职责错位:审查状态在 GitHub,唤醒却依赖某个会疲劳的人的推送面。

B:@mention 为什么是 0%

很多人直觉是:让 Actions 或审查员 @coderobot,机器人就会续上。实验里 wake = 0,五天全灭。

原因很硬,和产品营销无关:

  • GitHub @mention 更新的是机器人账号的通知收件箱。
  • 本机 Claude Code / Codex 是交互式 PTY 会话,不是通知 webhook 的常驻监听器。
  • 会话已关 = 没有进程在听。除非你另外养一个 24h daemon 去轮询 mention 或 pull_request_review 事件,否则 @ 叫不醒「开这个 PR 的那一次本地代理」。

把 B 写进 runbook,等于把责任推给一个根本不会响的门铃。

C:标签状态机 + 租约

租约状态机

规则:

  1. 出现 CHANGES_REQUESTED → 标签 state:changes-requested(可过滤,不是群里一句「回头改」)。
  2. 空闲开发机跑 pickup.py claim --machine machine-N:写租约文件(持有者 + 过期时间,force-with-lease 风格),并投影为唯一的 pickup:machine-N。
  3. 第二台再 claim → exit 2 lease_held。本机实测:naive 双贴 pickup:* 标签会双写;有租约后双认领率 0。
  4. 改之前必须 ack-threads,把未解决线程 id 全部确认进状态。
  5. 推完 → 释放租约 → state:awaiting-review。

没有 ack 时,C 仍会 100% 唤醒,但 2 号机「续写」会漏线程。租约解决的是互斥,不是上下文新鲜度。两个问题要两道门。

D:Actions 在 pull_request_review 上写交接

示意工作流(lab 里没有对 live GitHub 开火):changes_requested 时打标签、清掉陈旧 pickup:*、留一条结构化交接评论(head SHA、原会话已死、claim 命令、ack 要求)。空闲机上的例行轮询(或人看到交接评论)再 claim。平均首修约 29 分钟——含轮询间隔——漏线程 0,双认领 0。

D 并不替代 C:Actions 负责广播缺口,租约负责互斥认领。只有评论没有 claim,仍可能两台机同时动手。

本机真实撞上的失败模式

不是散文里的「可能」,是脚本跑出来的:

  1. 双认领(无租约):两台机都往 PR 上贴 pickup:machine-*。GitHub 标签不是锁,两个代理都会推。
  2. 双认领(有租约):第一台 ok,第二台 exit 2 lease_held。
  3. 死会话续写:2 号机工作区还在昨天空分支上,agent「记得」意图,但三条新线程不在 transcript 里;未 ack 时漏 2/3。
  4. 不读线程就修:策略上等于把审查当气氛组;门闩是 ack-threads 覆盖全部 unresolved id,否则禁止 commit。
  5. TTL 前 --steal:持有者还在写,另一台强抢 → 双写窗口。政策:只允许过期后,或 4/5 号机打了 pickup:abandoned。
  6. 过期后重认领:过期租约自动清掉,下一台 claim 成功——这条路径是好的。

机制:三层生命周期根本不齐

PR / 机器 / 会话 寿命

  • PR 与审查线程:天级。CHANGES_REQUESTED 会一直挂着。
  • 开发机器:班次级。谁空谁能改;原作者那台可能已经在别的票上。
  • 代理会话:小时级。关机、换票、上下文满,进程就没了。transcript 不是工单。

缺口出在「会话已死 × PR 还活 × 人没在盯」。你要的不是让死会话复活,而是把活 PR 绑到一台活机器上,并把审查线程变成这台机器开工前的硬输入。

开 PR 的那次会话,从来就不该被默认成唯一修复者。代理时代更是如此:开 PR 便宜,会话更短命,审查更异步。

钢人反对:这不就是多造一层工单系统吗?

反对意见值得认真听:你们已经有 GitHub assignee、Project 板、Slack。再搞 pickup:* 和租约文件,是不是仪式过度?小团队三个人,喊一声不就行了?

在这些前提下,反对成立:

  • 每天 CHANGES_REQUESTED 少于 1 次,而且审查员和下一位开发者总在同一时区同一群里——人收件箱(A)的漏报可以接受。
  • 你有一台真的在听 webhook 的常驻修复 bot(不是本地 IDE 会话),那 B 才可能不是 0%。多数「我们 @ 了机器人」并不是这种架构。
  • 单人仓库,开 PR 和修 PR 永远是同一会话——没有七机,没有班次切换。

一旦出现「隔夜开、早上审、中午原会话已死」的节奏,assignee 字段解决不了互斥:两人都能假装自己是 assignee。Slack 喊一声没有线程清单,漏改会在复审时变成新一轮 CHANGES_REQUESTED。租约贵在小,不在大——一个 JSON 文件 + 一个 exit code,换的是双推和漏线程。

另一个钢人:「让原机器必须负责改完。」听起来有归属感,实测会堵死队列:2 号机可能正在跑长任务,或人根本不在。归属感应落在租约持有者上,而不是落在历史作者上。作者信息仍留在 commit 与 PR 里,方便问上下文;修复权要可转移。

什么时候这套不划算

  • 没有代理、PR 都是人开人改、审查当时就能对着作者喊——别上租约。
  • 修复 bot 是集群里的常驻 worker(真 webhook)——把精力放在队列服务,而不是 IDE 会话认领。
  • 审查只留 LGTM 不留线程——先修审查质量,租约救不了空洞意见。
  • 想用模型「摘要一下评论」代替 ack——摘要仍可能漏硬约束;门闩要的是线程 id 清单,不是又一段散文。

租约文件和 GitHub 标签各管什么

容易混的一点:有人以为贴上 pickup:machine-1 就完成了互斥。本机对照很清楚——naive 路径里两台机都能成功执行 gh pr edit --add-label pickup:…(在模拟里就是往 JSON 标签数组各 append 一次),PR 上同时出现两个 pickup:*。标签是对外可见的投影,不是比较并交换。

互斥发生在 claim 脚本读租约文件的那一跳:已有未过期且 holder 不同 → exit 2。标签更新是成功 claim 之后的副作用,方便 4/5/7 号机用 is:pr label:state:changes-requested -label:pickup:machine-1 … 这类过滤看见「还没人拿」。若你只有标签、没有租约(或没有等价的协调服务),过滤能告诉你「看起来有人拿了」,不能阻止第二个人也「看起来拿了」。

把租约放在哪里有取舍:仓库内 .pickup/pr-512.lease.json 会进版本库噪声;本机 lab 放在仓库外的 leases/。接到真系统时,更干净的是:GitHub 上只留标签投影,租约存在小协调服务、Issue 评论里的机器可读块、或带 If-Match 的外部 store。关键不变量只有一句——同一时刻至多一个未过期 holder。

交接评论要长什么样,才不是又一段散文

D 策略里 Actions 写的不是「请帮忙看看」。本机生成的 handoff 固定包含:

  • 原作者机器与会话 id,并标明 dead 与关闭原因;
  • head 分支与 SHA(防止认领时 checkout 到过期 tip 还自以为对齐审查);
  • 未解决线程的 id / 路径 / 行号 / 原文清单;
  • 四条硬规则:fetch+checkout、禁止复用死会话 transcript、ack 后才推、推完改标签。

没有清单的交接,本质上退回 A:靠人的短期记忆。有清单但没有 claim,两台空闲机仍可能各改各的。清单解决「改什么」,租约解决「谁改」。

2 号机「续写」为什么特别危险

直觉上原作者机器最懂上下文,应该优先。实验里把 2 号机放进抢租约的日子,它往往更快出手(首修 7–14 分钟),同时漏线程率爆炸。快,是因为工作区还在,代理不用冷启动;漏,是因为审查发生在会话死亡之后,transcript 里没有那三条意见,模型只会「把折扣功能再打磨一遍」——于是浮点问题也许碰巧改了,未知 tier 与测试线程却没看见。

所以「原作者优先」若变成自动路由,等于系统性地选出上下文最旧的那台机器。更稳的默认:任意空闲开发机可 claim;原作者机器若要 claim,同样走 ack 门闩,不享受豁免。归属感用租约 holder 表达,不用 git author 表达。

和「审查预算」的衔接(但不重复 triage 文)

若早会 triage 已经把一条 PR 标成 triage:merge,下午才出 CHANGES_REQUESTED,修复认领会吃掉 1–3 号机的白天槽。这和隔夜节流是同一类稀缺:审查意见不是免费的,响应意见也不是。pickup 租约的 TTL(本机默认 45 分钟)有两层意思——防止持有者失踪后永久锁死,也逼认领者在一个班次窗口内推或释放。TTL 不是性能调参,是班次对齐:短于半天、长于一次正经修复。

若 CHANGES_REQUESTED 堆了很多却无人 claim,问题回到审查节奏或开发产能,而不是把 TTL 调到 24 小时装看不见。

复审时怎么一眼看出「有没有人接球」

4/5 号机复审不必翻聊天记录。过滤建议:

  • is:pr is:open label:state:changes-requested 且没有任何 pickup:* → 缺口,需要叫醒一台开发机或打 pickup:abandoned 后让下一位偷租约。
  • 有 pickup:machine-N 且未过期 → 等,或看 handoff 评论里的 ack 是否已出现。
  • state:awaiting-review → 进入复审队列,核线程是否 resolve。

7 号机合并闸可以软拒绝:仍带 state:changes-requested 的不允许合。本次实验没有改 live 合并闸;写在这里是为了标明状态机下游也能用。

和相邻文章的边界

  • 核对评论是否真的修了:那是落地核验,发生在已经有人改完之后。本文发生在更早:还没有人认领下一刀。
  • 任务做到一半跨机交接 WIP:那是活会话迁移。本文是死会话之后的 pickup,输入是审查线程,不是未完成的 agent plan。
  • 早间隔夜 PR triage:那是决定先杀谁。本文假设这条 PR 已经进了审查,并且拿到了 CHANGES_REQUESTED。

明天就能抄的最小步骤

  1. 建标签:state:changes-requested、state:awaiting-review、pickup:machine-1|2|3、pickup:abandoned(kit 里有 label_commands.sh)。
  2. AGENTS.md:写死「CHANGES_REQUESTED 后原会话不自动负责;先 claim;先 ack-threads;禁止凭记忆续写」。
  3. 放 pickup.py:claim / release / status / ack-threads;--live 只打印 gh 命令,便于你接到真仓库。
  4. 可选 Actions:pull_request_review + changes_requested → 打标签 + 结构化交接评论(YAML 在 kit)。
  5. 4/5 号机复审过滤:label:state:awaiting-review;仍带 state:changes-requested 且无 pickup:* 的,就是没人认领的缺口。

落地顺序建议:先标签 + 人工 claim 两天,再挂 Actions。若两天内都做不到「CHANGES_REQUESTED 后一小时内出现唯一 pickup」,自动化只是把无人认领广播得更响,解决不了有人执行。

实际跑了什么

真跑:玩具 git 仓库分支 agent/billing-discount;三条模拟审查线程;pickup.py 租约互斥与过期;策略 A/B/C/D 各 5 个合成早市;失败模式脚本;在 claim+ack 后由 machine-1 真正提交修复(整数分 + 未知 tier),测试 tests_ok,SHA f307e0f1。

没跑:非 live GitHub/Actions;标签未打到远程;无 Windows 执行;未接七机联调;未部署常驻 webhook daemon。

Kit:changes-requested-pickup-kit.tar.gz — pickup.py、simulate_strategies.py、changes-requested-handoff.yml、AGENTS.md.snippet、label_commands.sh、results.json。

今天最小的一步:下一次 4 号机点 Request changes 时,不要问「原代理还在吗」。问「哪台空闲开发机持有 pickup: 租约,线程 id 是否都 ack 了」。若发现自己又在 Slack 里喊「2 号机你看一下」而 2 号机会话早死——你就再次看见了会话边界和 PR 边界错位的税。

At 09:12, machine 4 clicks Request changes. Three threads are concrete: integer cents, reject unknown tier, add odd-cent tests. The PR is the one an agent opened last night on machine 2 — agent/billing-discount, session id still in the commit message: sess-m2-8841.

The problem: sess-m2-8841 died at 02:00 when the machine rebooted. Machine 2 is now on a new session for another ticket. Machine 1 is free. Machine 3 is loaded. Nobody is staring at the GitHub notification panel. The review lives in threads. Who pushes the next edit?

Three people, seven machines: 1–3 develop (agents open PRs fast), 4–5 review, 6 Windows validation, 7 merge/ops. This post is not about verifying that fixes landed, and not about mid-task WIP handoff between live sessions. It asks one question: when CHANGES_REQUESTED hits a dead authoring session, how do you bind a live PR to a still-awake dev machine without dropping the review thread?

I built a toy repo on this machine: real git branch and fix commit; review state, labels, and leases in JSON — not the live GitHub API; the Actions YAML is copy-paste illustrative and was not fired at a live repo; nothing ran on Windows. Every number below is from this run.

Setup: review present, author session gone

After Request changes the session is dead

Item Value
PR #512 billing: coupon tiers
Author machine machine-2 · session sess-m2-8841
Session dead (overnight reboot)
Review machine-4 · CHANGES_REQUESTED · 3 unresolved threads
Free dev machine-1 (machine-3 busy)

The threads are locatable, not vibes:

  1. float truncation in discount.py → price * (100-pct) // 100
  2. table[tier] KeyError → explicit ValueError
  3. missing tests for 999@15% and unknown tier

That text lives only in GitHub threads. The dead session transcript does not contain it — review had not happened when the agent filed the PR.

Four wake strategies, one gap

Same PR, four strategies, five synthetic mornings each. Metrics: did a machine actually start (wake), minutes to first fix-oriented push, missed threads, double-claim, wrong-machine wake.

Four strategies compared

Strategy wake avg TTF fix (min) missed-thread double-claim wrong-machine days with no fix
A human inbox / mute fatigue 60% 89.7 53.3% 0% 20% 2/5
B @mention bot login 0% — 100% 0% 0% 5/5
C label + lease pickup.py 100% 23.2 26.7%* 0% 0% 0/5
C + ack-threads gate 100% 26.4 0% 0% 0% 0/5
D Actions handoff → poll claim 100% 29.2 0% 0% 0% 0/5

*C's 26.7% is entirely machine-2 (dead author machine) winning the lease and continuing from memory without ack — the gate drops it to 0% for ~3 extra minutes of TTF.

A: human inbox — misses by default

Reviewer or lead relies on mail/app notifications. Agent-era density makes muting rational. In the sim, 2 of 5 days nobody notices CHANGES_REQUESTED; when noticed, ~90 minutes to a free machine, and about half of those mornings drop at least one thread in the oral handoff. Wrong-machine 20%: habit-pinging busy machine 3.

This is not laziness. The channel and the duty are misaligned: review state is on GitHub; wake depends on a fatiguing human surface.

B: why @mention is 0%

The intuition: Actions or reviewer @coderobot and it continues. In the lab, wake = 0 for all five days.

The reason is mechanical:

  • A GitHub @mention updates a bot account notification inbox.
  • Local Claude Code / Codex is an interactive PTY session, not a notification webhook listener.
  • Closed session = no process listening. Without a separate always-on daemon polling mentions or pull_request_review, @ cannot resume "the local agent that opened this PR".

Putting B in the runbook installs a doorbell that never rings.

C: label state machine + lease

Lease state machine

Rules:

  1. On CHANGES_REQUESTED → label state:changes-requested (filterable, not a Slack "we'll get it").
  2. Free dev runs pickup.py claim --machine machine-N: writes a lease file (holder + expiry, force-with-lease style) and projects a single pickup:machine-N.
  3. Second claim → exit 2 lease_held. On this box: naive dual pickup:* labels dual-write; with lease, double-claim rate 0.
  4. Before editing: ack-threads must cover every unresolved thread id.
  5. After push → release → state:awaiting-review.

Without ack, C still wakes 100% but machine-2 "continues" miss threads. Lease solves exclusion, not fresh context. Two doors.

D: Actions on pull_request_review

Illustrative workflow (not fired at live GitHub in this lab): on changes_requested, set labels, clear stale pickup:*, leave a structured handoff comment (head SHA, author session likely dead, claim command, ack requirement). A free-machine cron (or a human reading the comment) then claims. Avg TTF ~29 minutes including poll interval; missed threads 0; double-claim 0.

D does not replace C: Actions broadcasts the gap; the lease serializes the claim. A comment alone still allows two machines to start.

Failure modes actually hit here

Not hypothetical:

  1. Double claim (no lease): two machines both apply pickup:machine-*. Labels are not locks; both agents push.
  2. Double claim (with lease): first ok, second exit 2 lease_held.
  3. Dead session continue: machine-2 worktree still on yesterday's branch; agent "remembers" intent; three new threads absent from transcript; without ack, miss 2/3.
  4. Fix without reading threads: treat review as ambience; gate is ack-threads covering all unresolved ids or refuse commit.
  5. --steal before TTL: holder still writing → dual-write window. Policy: only after expiry, or after machine 4/5 applies pickup:abandoned.
  6. Expire then reclaim: expired lease auto-clears; next claim succeeds — the good path.

Mechanism: three lifetimes do not line up

PR / machine / session lifetimes

  • PR / review threads: days. CHANGES_REQUESTED stays.
  • Dev machines: shifts. Whoever is free can fix; the author machine may already be on another ticket.
  • Agent session: hours. Reboot, next ticket, context full — process gone. A transcript is not a ticket.

The gap is dead session × live PR × no babysitter. You do not need the dead session resurrected. You need the live PR bound to a live machine, with review threads as hard input before work starts.

The session that opened the PR should never be the default sole fixer. In the agent era opening is cheap, sessions are shorter, review is more async.

Steelman: is this just another ticket system?

Fair objection: you already have assignee, Projects, Slack. Are pickup:* and a lease file ceremony? Three people — just shout?

The objection holds when:

  • CHANGES_REQUESTED is <1/day and reviewer + next dev share timezone and chat — A's miss rate is tolerable.
  • You have a real always-on worker listening to webhooks (not a local IDE session) — then B can be non-zero. Most "@ the bot" setups are not that architecture.
  • Solo repo, same human session always opens and fixes — no seven machines, no shift change.

Once the rhythm is "open overnight, review morning, author session dead by noon", assignee does not give exclusion: two people can both pretend. A Slack ping carries no thread checklist; misses become another CHANGES_REQUESTED round. The lease is cheap: one JSON file + one exit code against dual pushes and dropped threads.

Another steelman: "the author machine must finish." Sounds like ownership; in practice it stalls the queue — machine 2 may be in a long job, or the human away. Ownership should sit on the lease holder, not the historical author. Author metadata stays on commits/PR for context questions; fix rights must transfer.

When this is not worth it

  • No agents; humans open and fix; reviewer can shout at the author in the moment — skip leases.
  • Repair bot is a cluster worker with real webhooks — invest in a queue service, not IDE-session claiming.
  • Reviews are LGTM without threads — fix review quality first; leases cannot save empty comments.
  • Replacing ack with a model "summary of comments" — summaries still drop hard constraints; the gate wants thread ids, not another paragraph.

What the lease file owns vs what labels own

A common mix-up: applying pickup:machine-1 is treated as exclusion. The lab contrast is sharp — on the naive path both machines can successfully add pickup:… labels (in simulation: append twice to the JSON label list). Two pickup:* labels coexist. Labels are a visible projection, not compare-and-swap.

Exclusion happens in the claim script's lease read: non-expired holder different from me → exit 2. Label updates are a side effect after a successful claim so machines 4/5/7 can filter "nobody holds this yet." Labels without a lease (or equivalent coordinator) can tell you someone appears to hold it; they cannot stop a second someone from also appearing to hold it.

Where to store the lease is a trade-off: in-repo .pickup/ is noisy; this lab keeps leases/ outside the toy git root. In production, keep labels on GitHub and the lease in a tiny coordinator, a machine-readable comment block, or an external store with If-Match. The invariant is one sentence — at most one non-expired holder at a time.

What a handoff comment must contain

Strategy D's Actions comment is not "please take a look." The generated handoff always includes:

  • author machine + session id marked dead with close reason;
  • head ref + SHA (so checkout cannot silently drift from the reviewed tip);
  • unresolved thread id / path / line / body checklist;
  • four hard rules: fetch+checkout, do not reuse dead transcript, ack before push, relabel after push.

A handoff without a checklist collapses to A: human short-term memory. A checklist without a claim still allows two free machines to edit in parallel. Checklist answers what; lease answers who.

Why machine-2 "continue" is specially dangerous

Intuition says the author machine knows the context best. In trials where machine-2 raced for the lease, it often pushed faster (7–14 minutes TTF) and missed threads. Fast because the worktree is warm; blind because review landed after session death — the transcript has none of the three threads, so the model "polishes the discount feature" again. Float math might get lucky; unknown-tier and tests do not.

Auto-routing to the author therefore systematically picks the stalest context. Safer default: any free dev may claim; if the author machine claims, it still faces the ack gate — no exemption. Ownership is the lease holder, not git author.

Re-review filters without chat archaeology

  • state:changes-requested with no pickup:* → gap; wake a free dev or mark pickup:abandoned then steal.
  • pickup:machine-N unexpired → wait, or check whether ack appeared in the handoff thread.
  • state:awaiting-review → re-review queue; confirm resolves.

Machine 7 can soft-block merges while state:changes-requested remains. This lab did not change a live merge gate.

Boundaries with sibling posts

  • Verifying comments were actually fixed: post-fix audit. This post is earlier: nobody has claimed the next edit yet.
  • Mid-task WIP handoff between machines: live session migration. This is pickup after death; input is review threads, not an unfinished agent plan.
  • Morning overnight PR triage: deciding what to kill first. This assumes the PR already entered review and received CHANGES_REQUESTED.

Copy-tomorrow minimum

  1. Labels: state:changes-requested, state:awaiting-review, pickup:machine-1|2|3, pickup:abandoned (label_commands.sh in the kit).
  2. AGENTS.md: hard rule — after CHANGES_REQUESTED the original session is not automatically responsible; claim first; ack-threads first; no memory-continue.
  3. Ship pickup.py: claim / release / status / ack-threads; --live prints gh commands for a real repo.
  4. Optional Actions: pull_request_review + changes_requested → labels + structured handoff (YAML in kit).
  5. Re-review filter on 4/5: label:state:awaiting-review; still state:changes-requested with no pickup:* is an unclaimed gap.

Rollout: labels + manual claim for two days, then Actions. If you cannot get a unique pickup: within an hour of CHANGES_REQUESTED, automation only broadcasts the vacancy louder.

What actually ran

Real: toy git branch agent/billing-discount; three simulated review threads; pickup.py lease exclusion and expiry; strategies A/B/C/D × 5 mornings; failure-mode scripts; after claim+ack, machine-1 truly committed the integer-cents + unknown-tier fix; tests tests_ok; SHA f307e0f1.

Not run: live GitHub/Actions; labels not applied remotely; no Windows; no seven-machine integration; no always-on webhook daemon.

Kit: changes-requested-pickup-kit.tar.gz — pickup.py, simulate_strategies.py, changes-requested-handoff.yml, AGENTS.md.snippet, label_commands.sh, results.json.

Smallest step today: next time machine 4 hits Request changes, do not ask "is the original agent still up?" Ask "which free dev holds the pickup: lease, and are thread ids all acked?" If you catch yourself Slack-pinging machine 2 while its session is already dead — you are paying the session/PR boundary tax again.