Repository navigation
fix(hitl): a decision card asked from the owner's DM is posted in, and answered from, that DM - #6027
Merged
Merged
Conversation
…d answered from, that DM ask-owner routed the text ask to the asking task's own conversation, but stored the decision card with an empty room, so the gateway posted it in its default owner DM (the core's). A worker's card then landed in the core's DM and the owner's click went to the core, never back to the worker that asked. - pending_questions_ask: the card's room is the task's own conversation when destination_from_task names one (owner tier + attested AG2 Space DM); any other origin, or no --task-file, keeps the empty room (the default owner DM). - hitl.cards.choice_request takes the room. - hitl.projector (and its vendored copy) posts a card, its edits and its notices to req.room when set, else the default room. One primary gateway still projects. - The existing reply gate (schema.validate_action) then rejects a click from any other room; an accepted click goes on as a hitl_click task whose channel_id is the card's room, which the pool router routes to the worker bound to that room. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
Coverage Gate✅ Diff coverage PASSES the 95% bar. Whole-tree (informational): 94%. Diff CoverageDiff: origin/main...HEAD, staged and unstaged changes
Summary
|
liususan091219
left a comment
Collaborator
There was a problem hiding this comment.
Not approving yet — the approval bar is not met. No code change requested.
Missing piece: there is no approval on this PR at all. It needs one expert approval (hitl / pending-questions area) before it can pass.
What I checked at 4847f35:
- The fix sits in the two policy owners:
card_room()inpending_questions_ask.py(room only for an owner-tier task withchannel_kind: dm), andreq.room or room_idinhitl/projector.py, with the vendored ag2-sparrow copy byte-identical. - Tests are behavioural:
CardRoomasks through the real_askand runs a click through the realHitlReplyHandler(rejected from another room, applied from the card's room); the projector tests check create, edit and follow-up notice all land in the card's room. The PR body shows them failing on main. - Shared room, no DM evidence and team tier all fall back to the default owner DM, so a card is never posted into a shared room.
- No hardcoded paths; CI green; license/cla success; no unresolved threads.
Also owed (named, not blocking): a live round trip — a worker asks from its owner DM, the card appears in that DM, and the click comes back to that worker.
— Sutando (susans-MacBook-Pro)
qingyun-wu
approved these changes
Oct 30, 2026
qingyun-wu
left a comment
Collaborator
There was a problem hiding this comment.
Approve at 4847f35. Ready to merge.
Verified:
- Bug is real on main:
build_cardnever passed a room, so every card hadroom: ""and the projector posted it in the gateway's default owner DM, whilevalidate_action(schema.py:263) only checks room when one is set. - The fix sits where the policy lives.
card_room()reusesdestination_from_task, which names a channel only for an owner-tier task with attested DM evidence. A shared room, no DM evidence, a non-owner tier and no task file all keep"", so a card never goes into a shared room. The projector usesreq.room or room_idfor create, edit and follow-up notices. The vendored ag2-sparrowprojector.pyis byte-identical tosrc/hitl/projector.py. - Tests exercise the failure mode through the real code:
_ask, thenHitlReplyHandler. A click from another room is rejected and one from the card's room is applied and turns. The projector tests show the card created and edited in its room, and its notice posted there. The PR body shows these tests failing on main. Diff coverage is 100% and CI is green. - Worst case for existing installs: cards with no room behave exactly as before. Only owner-DM tasks on AG2 Space change destination.
Non-blocking:
- If the projecting gateway's account is not a member of the asking DM, the send is rejected and retried on every pass. Because the card exists, no text fallback goes out, so the question stays invisible. This is fine while pool workers share the gateway's account. The live round trip the body lists as not done would confirm that.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Priority
Level: medium
Reason: a worker's decision card shows up in the wrong DM, and the owner's click never gets back to the worker that asked.
Closes
No issue.
Summary
A pool worker can call
ask-owner.py --task-file <its task> --default/--option ...from a task that came from the owner's DM with that worker. The text ask already goes to the task's own conversation (destination_from_task). The decision card did not: it was stored withroom: "", so the gateway's projector posted it in its default owner DM, which is the core's. The owner saw the card in the core's DM, and the click became a task for the core instead of the worker.The fix is in the two places that own the policy:
pending_questions_ask: newcard_room(dest). A card's room is the task's own conversation whendestination_from_tasknames one. It only names one for an owner-tier task whose attested headers say it is an AG2 Space DM (channel_kind: dm). Any other origin keeps the empty room, which means the default owner DM, as today. That covers a shared room, no DM evidence, a non-owner tier, or no--task-file. A decision card is never posted in a shared room.hitl.cards.choice_requesttakes theroom.hitl.projector(and its byte-identical vendored copy inag2_sparrow.hitl): each card is posted, edited and followed up inreq.roomwhen that is set, and in the defaultroom_idotherwise. Only the primary gateway projects (owns_unaddressed), so one loop posts each card and nothing is projected twice. Claude-local cards already hadreq.roomfilled with the owner DM, so where they post does not change.How the click gets back to the worker:
channel_idis the card's room.validate_actionalready rejects a reply whose room is not the card's (if req.room and reply.room != req.room).turn_on_actionkeeps going as the same task, now markedhitl_click: true(_handle_hitl_action→_write_task).targets_for(roster, task["channel_id"])sends it to the worker bound to that room.None of the click path needed a change.
Checklist
gh pr list --state open --search "hitl card room"; feat(pending-questions): ask-owner sends a decision card when --default or --option is given #5942 is the earlier draft that added the card and is a different change)main(see the before run below)review-checks.shclean)Before / after evidence
The new tests, run against the code on
main(only the production file swapped back):The same test files with this branch:
The full suite is left to CI.
🤖 Generated with Claude Code