fix(quota): name the workspace move when a selection is deferred for it - #4601
huangruiteng wants to merge 4 commits into
Conversation
Selecting an action from the shared checkout returns
`state=quota_action_selection_deferred` with
`reason="explicit action selection was deferred by the current delivery
frontier: control_repair"` and `delivery_preemptions=[control_repair,
delivery_not_allowed]`. Read literally, that points at an unrelated
control-plane preemption.
The instruction the agent actually owes is in a different field of the same
payload:
execution_obligation.kind = agent_workspace_repair
minimum = one_workspace_move_then_guard_rerun
contract = "do not edit repository files from a shared
checkout; create or switch to an independent
worktree/branch, then rerun quota should-run
with the same agent id"
Rerunning the identical selection from a lane worktree returns
`decision=run, effective_action=normal_run` immediately. This lane read the
frontier wording as a blocker three times in one wake before inspecting the
obligation field, and each reading cost a turn of real work.
When the deferral is a workspace repair, the reason now names the move and the
recommendation names the worktree rerun with the same `--turn-instance-id` and
`--todo-id`. Every other deferral and rejection keeps its existing wording,
including the auxiliary-monitor and autonomous-replan messages; the only
behavior change is which sentence a workspace-repair deferral prints.
Validated: `pytest tests/control_plane/test_quota_settlement_cli.py` reports 65
passed. Two `test_prior_host_closeout_survives_hidden_todo_lifecycle[sqlite]`
failures come from `canonical_authority_fixture.py` failing to run Node.js and
reproduce unchanged on a worktree without this change.
Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
4442293 to
e377399
Compare
Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
huangruiteng
left a comment
There was a problem hiding this comment.
动机
从共享 checkout 选 Todo 时,配额守卫返回 state=quota_action_selection_deferred、reason=control_repair。这条文本读起来像是无关的控制面前置条件,而真正要做的动作是“把工作移出共享 checkout”,于是 agent 容易多花几轮反复读 portfolio 才找到 execution_obligation.kind=agent_workspace_repair。这是本车道今天亲身遇到的一类“提示没有指向可执行动作”的摩擦。
改动思路
不动选绑逻辑,只让这条 deferral 的文案与建议对齐它唯一可执行的动作:workspace_repair_allowed 且义务为 workspace repair 时,reason 直接指出必须先把工作区移出共享 checkout,并附上契约原文;recommended_action 指明带着同一 --turn-instance-id 与 --todo-id 在独立 worktree 重跑守卫。其余 deferral 保持可读但更具体的原因与建议。
具体改动
- loopx/cli_commands/quota.py:
_apply_requested_quota_action_selection_preflight增加 workspace-repair 分支,并为其它 deferral/不合格分支给出更具体的 reason 与 recommended_action(含 auxiliary monitor 的场景)。 - tests/control_plane/test_quota_settlement_cli.py:新增
_deferred_selection_payload构造器与命名测试,覆盖 workspace repair 与普通 frontier deferral 两条路径。
对主干的风险
只改错误摘要字段,不改 decision/state/error_code 或选绑判定:payload 的机器字段保持原样,附加的是人读文本,因此不会改变调用方的分支逻辑。测试用构造 payload 而不是真实运行,覆盖的是渲染规则本身;这类文案分支的回归风险由命名测试固定。
我的整体评价
把“某类 deferral 其实有明确下一步”显式化,方向和最小性都对,且用命名测试固定两条分支。建议在该 head 的必过检查转绿后合并。
Approval conclusion (author-owned PR; GitHub blocks formal self-approval)
Head: 842828f
English verdict: APPROVE
|
Closing as superseded by current main (e160b1b). The real quota CLI already preserves a selected Todo through agent_workspace_repair, permits creating an independent worktree, and resumes normal_run on the same Turn without rebinding settlement. Revalidated the existing workspace-repair and action-selection preemption tests: 3 passed. This PR changes rejection-path wording using an incomplete fixture, while the production path is already covered; keeping it would add a second explanation without repairing a remaining behavior gap. The other recovery PRs are being reconciled separately. |
The friction
Binding an action from the shared checkout produces a message that names the wrong thing:
That reads as an unrelated control-plane preemption, so an agent can reasonably conclude it is blocked and go look for other work. The instruction it actually owes is in a different field of the same payload:
Rerunning the identical selection from a lane worktree returns
decision=run, effective_action=normal_runimmediately. This lane read the frontier wording as a blocker three times in one wake before inspecting the obligation field; each misreading cost a turn of real work, and the deferral is the only one whose recovery is a single mechanical move rather than a frontier question.Change
When the deferral is a workspace repair, the reason names the move and carries the contract text, and the recommendation names the worktree rerun with the same
--turn-instance-idand--todo-id. The two branches are precomputed beforepayload.updateinstead of nested inside it, which is what the diff mostly is.Every other message is byte-identical, including autonomous-replan deferrals, the auxiliary-monitor rejection, and the generic rejection. The only behavior change is which sentence a workspace-repair deferral prints.
Validation
Two new tests pin both sides: a workspace-repair deferral must say "move the workspace first" / "independent worktree or branch" /
--turn-instance-idand must not say "delivery frontier"; an unrelatedautonomous_replandeferral must keep the existing frontier wording and the existing recommendation.Two
test_prior_host_closeout_survives_hidden_todo_lifecycle[sqlite]failures come fromcanonical_authority_fixture.pyfailing to run Node.js in this environment and reproduce unchanged on a worktree without this change.Boundary
Public-safe: no private state, credentials, local paths or raw evidence. No change to selection eligibility, settlement, spend, or which action the guard picks — only what the deferral message tells the agent to do next.