Skip to content

fix(sparrow): a Hatch reply names the message it answers, so it cannot land on another item - #5460

Merged
sonichi merged 4 commits into
mainfrom
fix/hatch-reply-task-binding
Oct 16, 2026
Merged

sonichi merged 4 commits into
mainfrom
fix/hatch-reply-task-binding

Conversation

@sonichi

@sonichi sonichi commented Oct 13, 2026

Copy link
Copy Markdown
Owner

What changed, and why

Live incident: Muse (Hatch agent on ag2-sparrow 0.5.2) was woken with 14 queued items after a listener switch. It answered Chi's mention in room Echo-Act-Iv by running ag2-sparrow-hatch reply <result_file> with the result_file of a different item (one from Chi's DM), so the answer was posted in the DM. Nothing tied the reply text to the item it answered: reply accepted any open item's result_file.

Design. reply now requires --answering '<words>': 3 or more consecutive words of the message the reply answers (all of a shorter message). Before writing anything it checks those words against the message of the item whose result_file was named; a mismatch exits 2 with nothing written, quotes that item's message, and names the open item the words do belong to (Those words are from the message of …/task-X.txt: answer it with that result_file). --no-send is bound the same way, since closing the wrong item silently drops a question. Matching ignores case, punctuation and @-mentions, and excludes the [AG2Space name] sender prefix and a quoted Matrix reply fallback, because those are shared across messages and would bind to the wrong item.

Why this over the alternatives:

  • (b) as an opaque nonce: rejected. A per-match token sits in the same match object as result_file, so a worker that picks up the wrong item's path copies the wrong item's token with it. result_file is already a unique per-match token; a second one adds nothing. The words the worker answers are what it has in mind when it writes the reply, independently of which path it copies, so an echo of the message is the check that catches "the command was right, the target file was wrong". Sender echo would not have caught this incident: both items were from Chi.
  • (a) batch size 1: not done. Hatch wakes are slow, so a backlog of 14 (as after a listener switch) would be 14 serial wakes. It also does not make the misroute impossible: HANDOFF blocks, rewakes and the main session still handle several result_files. The binding check makes a cross-item post refused regardless of batch size, so batching stays as is.

Compatibility: an agent whose installed prompt predates this (no --answering) gets exit 2, "name the message this answers: --answering …; nothing written"; the worker note already says exit 2 means fix and rerun. install rewrites the prompt and note on upgrade.

Look first

  • packages/ag2-sparrow/ag2_sparrow/adapters/hatch/cli.py: answers(), _misrouted(), and the two checks in reply()
  • task_fields.py: message_text() shares the prefix/fallback stripping with snippet() (snippet output unchanged)
  • hook-definition.json prompt, REMAINING_WORK_NOTE in ag2-sparrow-hatch.sh (incl. the HANDOFF block now carrying answering:), README

Behavior this proves

A reply composed for one item of a batch and run against another item's result_file is refused and writes nothing; the refusal points to the right result_file; the same reply against its own item goes through.

Feature/documentation consistency

The package README (canonical for the Hatch adapter) is updated. No docs/ catalog entry covers the Hatch adapter (docs/sparrow-* cover the delivery contract, unchanged), so N/A there.

Before/after evidence

Same script (incident.sh: two open items, a room mention "what is 6 times 7?" and a DM "remind me to renew the passport", both from @Chi; the room's answer run against the DM item's result_file).

Parent d93096714 (the command as Muse ran it posts the room answer into the DM item):

== as Muse ran it (no message named) ==
$ ag2-sparrow-hatch reply $T/results/task-1784700000002.txt --body @chi:ag2.space 42 --wait 0
NOT confirmed after 0s: task-1784700000002.txt is queued and Sparrow will keep trying; do not report it as posted. Sparrow: $T/state/gateway-status.json unreadable (FileNotFoundError); Sparrow has not run with this state dir; restart it: systemctl --user restart ag2-sparrow under the README's user service, else start ag2-sparrow again as at install
exit=3
DM result_file now holds: @chi:ag2.space 42
== naming the message it answers (the room's), against the DM's result_file ==
$ ag2-sparrow-hatch reply $T/results/task-1784700000002.txt --answering what is 6 times 7 --body @chi:ag2.space 42 --wait 0
usage: ag2-sparrow-hatch [-h] {install,verify,sparrow,reply} ...
ag2-sparrow-hatch: error: unrecognized arguments: --answering what is 6 times 7
exit=2
DM result_file now holds: <nothing>
== the same reply against the room's own result_file ==
$ ag2-sparrow-hatch reply $T/results/task-1784700000001.txt --answering what is 6 times 7 --body @chi:ag2.space 42 --wait 0
usage: ag2-sparrow-hatch [-h] {install,verify,sparrow,reply} ...
ag2-sparrow-hatch: error: unrecognized arguments: --answering what is 6 times 7
exit=2
room result_file now holds: <nothing>

HEAD:

== as Muse ran it (no message named) ==
$ ag2-sparrow-hatch reply $T/results/task-1784700000002.txt --body @chi:ag2.space 42 --wait 0
error: name the message this answers: --answering '<3 or more consecutive words copied from its snippet>'; nothing written
exit=2
DM result_file now holds: <nothing>
== naming the message it answers (the room's), against the DM's result_file ==
$ ag2-sparrow-hatch reply $T/results/task-1784700000002.txt --answering what is 6 times 7 --body @chi:ag2.space 42 --wait 0
error: --answering 'what is 6 times 7' is not the message of task-1784700000002 ('[AG2Space Chi] remind me to renew the passport'); nothing written. Those words are from the message of $T/results/task-1784700000001.txt: answer it with that result_file.
exit=2
DM result_file now holds: <nothing>
== the same reply against the room's own result_file ==
$ ag2-sparrow-hatch reply $T/results/task-1784700000001.txt --answering what is 6 times 7 --body @chi:ag2.space 42 --wait 0
NOT confirmed after 0s: task-1784700000001.txt is queued and Sparrow will keep trying; do not report it as posted. Sparrow: $T/state/gateway-status.json unreadable (FileNotFoundError); Sparrow has not run with this state dir; restart it: systemctl --user restart ag2-sparrow under the README's user service, else start ag2-sparrow again as at install
exit=3
room result_file now holds: @chi:ag2.space 42

(exit 3 in the last case is "queued, not confirmed": no Sparrow runs in this harness.)

New test file run against the parent code (python3.14 packages/ag2-sparrow/tests/test_hatch_adapter.py with only the test file from HEAD):

ERROR: test_what_counts_as_the_message (ReplyBinding)
FAIL: test_a_batch_wake_reply_bound_to_the_wrong_item_is_refused (HookScript)
FAIL: test_the_room_answer_sent_to_the_dm_result_file_is_refused (ReplyBinding)
FAIL: test_a_reply_that_names_no_message_writes_nothing (ReplyBinding)
FAIL: test_no_send_is_bound_the_same_way (ReplyBinding)
... (plus the existing Reply tests, which now pass --answering, and the note/prompt wording tests)
Ran 78 tests
FAILED (failures=19, errors=1)

At HEAD:

$ python3.14 packages/ag2-sparrow/tests/test_hatch_adapter.py
Ran 78 tests in 12.914s
OK
$ python3.14 tests/hatch-reply-real-drain.test.py
Ran 6 tests in 1.164s
OK
$ python3.14 packages/ag2-sparrow/tests/test_wheel_contents.py
PASS installed wheel imports and ag2-sparrow fails cleanly without a token
PASS installed ag2-sparrow-hatch runs the Hatch adapter
PASS installed hook prompt names the reply command by absolute path: …/venv/bin/ag2-sparrow-hatch
$ git diff origin/main | bash scripts/review-checks.sh
review-checks: PASS (hardcoded-paths + root-artifacts + prose-cap clean)

Mutants (run against ReplyBinding + the batch-wake test, each restored after):

== mutant: reply skips the binding check (accepts any --answering)
FAIL: test_no_send_is_bound_the_same_way
FAIL: test_the_room_answer_sent_to_the_dm_result_file_is_refused
FAIL: test_a_batch_wake_reply_bound_to_the_wrong_item_is_refused
FAILED (failures=3)
== mutant: no minimum word count
FAIL: test_what_counts_as_the_message
FAILED (failures=1)
== mutant: match against the raw body (sender prefix + reply fallback count)
FAIL: test_what_counts_as_the_message
FAILED (failures=1)

Tests run

Focused only, Python 3.14 (no pytest installed for 3.14 on this host; the package's tests are unittest scripts, as CI runs them): test_hatch_adapter.py, tests/hatch-reply-real-drain.test.py (Sparrow's real drain posting a reply; updated to pass --answering), test_wheel_contents.py.

Live path

reply runs on the Hatch agent's host (Muse), not on this repo's core, and reaches it only through a release plus pip install -U and ag2-sparrow-hatch install there. No release is part of this PR, so there is no live round trip yet; the real-drain test covers the posting path end to end against Sparrow's own drain. A post-release witness on Muse (batch wake of 2+ items, one reply per item, each landing in its own room) is the follow-up.

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Coverage Gate

✅ Diff coverage PASSES the 95% bar. Whole-tree (informational): 91%.

Diff Coverage

Diff: origin/main...HEAD, staged and unstaged changes

  • packages/ag2-sparrow/ag2_sparrow/adapters/hatch/cli.py (95.0%): Missing lines 710-711
  • packages/ag2-sparrow/ag2_sparrow/adapters/hatch/task_fields.py (100%)
  • packages/ag2-sparrow/tests/test_hatch_adapter.py (100%)

Summary

  • Total: 197 lines
  • Missing: 2 lines
  • Coverage: 98%

packages/ag2-sparrow/ag2_sparrow/adapters/hatch/cli.py

Lines 706-715

  706 
  707 def _task_body(task_file: Path) -> str:
  708     try:
  709         return message_body(task_file.read_text(encoding="utf-8", errors="replace"))
! 710     except OSError:
! 711         return ""
  712 
  713 
  714 def _misrouted(tid: str, task_dir: Path, result_dir: Path, instance: str, quoted: str) -> str:
  715     """Why `quoted` does not bind this reply to task `tid`, naming the items it does fit."""

@liususan091219 liususan091219 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Requesting changes: --answering refuses correct partial quotes of Chinese/Japanese messages, so those replies stall at exit 2.

Reviewed at f218d78. The design holds up: binding the reply to the message's words rather than a nonce, and the tests are behavioral. ReplyBinding drives hatch_cli.main and the real-drain test posts through Sparrow. CI and license/cla are green.

Blocker: incomplete fix for text without spaces. _WORD = re.compile(r"\w+") (cli.py:54) treats a whole CJK clause as one "word", and answers() compares whole words. I ran the production answers() at head:

'[AG2Space Susan] 帮我查一下明天的天气'               '明天的天气'                → False
'[AG2Space Susan] 你好,帮我查一下明天纽约的天气,谢谢'  '帮我查一下明天纽约的天气'  → False
same message                                        the whole message          → True

So for a 3-clause Chinese message the worker has to copy the entire message. A natural substring is refused. The worker note and prompt say "3 or more consecutive words", which a model will read as "a fragment", and the exit-2 message repeats that advice. Ask: match CJK text by characters (for example a contiguous run of N or more characters within a \w token), and add a ReplyBinding case for a Chinese message.

Non-blocking.

  • The binding is not exclusive. can you check returns True for both "can you check the PR status" and "can you check my calendar". If the quoted words also fit another open item, the call goes through. Consider refusing when the words fit more than one open item, which _misrouted already computes.
  • The first-line snippet is cut at 500 characters (task_fields.py:64). An unspaced CJK first line longer than that cannot be quoted in full from the snippet.

Not verified: a live batch wake on Muse. As the body says, that needs a release. I'm naming it, not blocking on it.

— Sutando (susans-MacBook-Pro)

sonichi and others added 2 commits October 15, 2026 11:19
…t land on another item

A batch wake lists several items, and `ag2-sparrow-hatch reply <result_file>`
took any open item's result_file: Muse answered a room mention by writing the
answer to the result_file of a DM item, and Sparrow posted it in the DM.

reply now requires `--answering '<words>'`: 3 or more consecutive words of the
message it answers (all of a shorter one; case, punctuation, @-mentions, the
sender prefix and a quoted reply fallback do not count). Words that are not the
message of the item whose result_file is named are refused with exit 2 and
nothing written, and the error names the open item they do belong to. --no-send
is bound the same way. The hook prompt, worker note (and its HANDOFF block) and
README describe the flag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…acters

A Chinese or Japanese clause is one \w+ token, so a correct partial quote
of such a message never matched and reply exited 2. In scripts written
without spaces (CJK, kana, Thai, Lao, Khmer, Myanmar) each character is now
a token, and 4 consecutive characters count as the 3-word minimum; a quote
must still be a contiguous run of the named item's own message, so another
item's words are refused as before.

The binding also reads the message without the broker's quoted
reply-context block (one helper in task_fields, shared with the wake item),
so quoting the message replied to does not bind a reply.

Tests: a CJK partial quote of the named item posts; a CJK quote of another
item's message exits 2 and writes nothing; the batch-wake and two-room
refusals assert no result or outbox file is written.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sonichi
sonichi force-pushed the fix/hatch-reply-task-binding branch from f218d78 to 2fd56df Compare October 15, 2026 18:21
@sonichi

sonichi commented Oct 15, 2026

Copy link
Copy Markdown
Owner Author

Updated for the review at f218d78. New head: 2fd56df, rebased onto main f7cb113.

Blocker fixed: text without spaces is matched on characters. In answers() (packages/ag2-sparrow/ag2_sparrow/adapters/hatch/cli.py), each character of a script written without spaces (CJK ideographs, kana, Thai, Lao, Khmer, Myanmar) is now its own token. 4 consecutive characters count the same as the 3-word minimum, and the whole message still counts when it is shorter. A quote must still be a contiguous run of the named item's own message, so a mismatch still fails closed: exit 2, nothing written, and the error names the item the words do belong to. Both sides are NFKC-normalized. The worker note, the hook prompt, the exit-2 hint and the README now give the character rule.

Rebase onto #5541. The binding now reads the message without the broker's quoted reply-context block. task_fields.message_body() is shared with the wake item's own split, so the two cannot drift. Quoting the message that was replied to does not bind a reply.

Tests (packages/ag2-sparrow/tests/test_hatch_adapter.py):

  • ReplyBinding.test_text_without_spaces_is_matched_on_characters: two open items in different rooms, A in Chinese and B in Japanese. A's CJK words named against B's result_file exit 2, the error names A, and no file is written under the result dir (result or outbox). A partial CJK quote of A's message posts. A partial quote of B with --no-send closes B.
  • test_what_counts_as_the_message adds your examples, plus negatives: too few characters, wrong order, another message's characters, the sender prefix.
  • test_the_message_replied_to_does_not_bind_the_reply is new.
  • The two-room refusal (test_the_room_answer_sent_to_the_dm_result_file_is_refused) and the batch-wake refusal (HookScript.test_a_batch_wake_reply_bound_to_the_wrong_item_is_refused, now with the two items in different rooms) assert that nothing lands in any result or outbox file before the matching quote posts.

Before: the new CJK cases run against f218d78:

FAIL: test_susan_review_cases            AssertionError: False is not true
FAIL: test_text_without_spaces_is_matched_on_characters
AssertionError: '.../result_dir/task-1784700000003.txt' not found in "error: --answering '明天纽约的天气' is not the message of task-1784700000004 (...); nothing written. Copy 3 or more consecutive words of the message you answer from its snippet.\n"
FAILED (failures=2)

After: at 2fd56df:

$ python3 packages/ag2-sparrow/tests/test_hatch_adapter.py            -> Ran 104 tests ... OK
  ReplyBinding (6) + HookScript.test_a_batch_wake_reply_bound_to_the_wrong_item_is_refused -> Ran 7 tests ... OK
$ python3 tests/hatch-reply-real-drain.test.py                        -> OK
$ python3 tests/ag2-sparrow-hatch-adapter.test.py                     -> OK
$ python3 packages/ag2-sparrow/tests/test_wheel_contents.py           -> PASS (wheel imports, hatch runs, prompt names reply command)

Not changed (non-blocking):

  • Exclusivity: refusing when a quote fits more than one open item would deadlock two open items that carry the same short message, such as two "approve?". I left it out.
  • The 500-character snippet cut: a long unspaced first line can now be quoted in part, so the cut no longer blocks a reply.

Still owed: a live batch-wake witness on Muse after a release. This PR does not claim one.

@liususan091219 liususan091219 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Ready to approve. This replaces my CHANGES_REQUESTED review of f218d78 in substance. That review is still on the PR. Dismissing it is left to Susan.

Reviewed at 2fd56df. It is rebased on main f7cb113, CI is all green, and license/cla passes.

The CJK blocker is fixed. _TOKEN (packages/ag2-sparrow/ag2_sparrow/adapters/hatch/cli.py:65) now makes each unspaced character its own token. _weight (cli.py:681) counts 4 characters the same as 3 words. I ran the production answers() (cli.py:688) at head:

帮我查一下明天的天气                    '明天的天气'                → True   (was False)
你好,帮我查一下明天纽约的天气,谢谢     '帮我查一下明天纽约的天气'  → True   (was False)
same                                   '明天纽约' (4)             → True
same                                   '明天纽' (3) / '纽约' (2)   → False / False
same                                   '纽约明天的' (wrong order)  → False
帮我查一下明天的天气                    '后天的天气' (not in it)    → False
好的                                   '好的' (whole short msg)   → True;  '好' → False
请review PR 5460然后告诉我              'PR 5460然后告诉' (mixed)   → True;  '5460然后' → False
今日は東京の天気を調べてください          '東京の天気' → True;  '大阪の天気' → False
ช่วยตรวจสอบสภาพอากาศพรุ่งนี้              'สภาพอากาศ' → True;  'สภา' → False
안녕하세요 내일 날씨 알려줘              (Hangul is spaced, so word rule) 3 words → True, 2 → False
ABC全角 test here                    'abc全角 test' (NFKC) → True

Mutation check. I reverted _TOKEN to \w+. That fails exactly ReplyBinding.test_text_without_spaces_is_matched_on_characters and test_what_counts_as_the_message (test_hatch_adapter.py:1639, 1674), and both of my examples go back to False. With _TOKEN restored, all 104 pass.

Ran at head: test_hatch_adapter.py passes 104 tests, tests/ag2-sparrow-hatch-adapter.test.py passes, tests/hatch-reply-real-drain.test.py passes 6, test_wheel_contents.py passes, and scripts/review-checks.sh --diff passes.

Host note, not this PR: run from an interpreter whose path contains a space (this Mac's .../Application Support/...), 33 HookScript tests fail. The fixture writes sys.executable unquoted into the hook config. origin/main fails the same way (19F/13E). Through a space-free symlink, everything passes.

Non-blocking.

  • Thai is counted per code point, so 4 "characters" can include combining vowel/tone marks, which is about 2 to 3 visible letters. It's still a contiguous match against the named item's own text, so it fails closed. It's just a looser bar than for Chinese.
  • I accept the exclusivity reasoning about two open "approve?" items.

Not verified. I didn't run the CLI end to end against a Chinese quote of another item's message myself. I rely on test_text_without_spaces_is_matched_on_characters, which the mutation shows is load-bearing. I didn't mutation-check the reply-context split (message_body, task_fields.py:85). The live batch-wake round trip on Muse after a release is still owed. I'm naming it, not blocking on it.

— Sutando (susans-MacBook-Pro)

@sonichi
sonichi dismissed liususan091219’s stale review October 15, 2026 21:29

Dismissed on the repo owner's instruction. Susan's own agent re-reviewed head 2fd56df and said it replaces this CHANGES_REQUESTED in substance: the CJK blocker is fixed, verified by running answers() on her examples and a mutation check.

@keweichen keweichen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Changes requested at 2fd56dfd6.

Blocker — emoji-only messages still permit the cross-item write this PR is meant to prevent. When _words(message) is empty, answers() uses substring matching (packages/ag2-sparrow/ag2_sparrow/adapters/hatch/cli.py:694-696). At this head:

answers('[AG2Space Chi] 👍👎', '👍') -> True

With two isolated, production-shaped open tasks in different rooms—A containing 👍, B containing 👍👎—the actual reply CLI targeted at B with --answering 👍 returned 3 and wrote B's result with A's answer; A remained unwritten. The same mismatch with --no-send returned 0 and closed B. Exit 3 here means locally queued, not confirmed delivered; no production gateway was contacted.

That contradicts the documented “whole message when shorter” rule and the claim that an answer cannot land on another item's result. The existing emoji assertion covers only a single-👍 message, so the 104-test Hatch suite and six real-drain tests pass while missing this adjacent case. Please make the no-token branch require the whole normalized short message (or an equivalent fail-closed rule), and add CLI-level regressions proving both a wrong reply and wrong --no-send write nothing.

Blocker — the required live witness is still absent. This changes the Hatch/Sparrow delivery path. REVIEW.md §15 and CONTRIBUTING.md require a real, publication-safe post-restart round trip before approval; the PR body and latest re-review both say the Muse witness remains owed.

I also verified that the ordinary-text incident reproduces at the parent and is refused at this head; 104/104 Hatch tests, 6/6 real-drain tests, the 104-test wrapper, git diff --check, and review-checks.sh pass; CI and license/cla are green; and the current-main merge tree is conflict-free. Worst case for the remaining defect is cross-room disclosure or silently closing another item's question.

Verdict: changes requested. Re-review after the emoji/no-token path is fixed with a discriminating regression and the live round-trip witness is posted.

answers() fell back to a substring match when the message had no word
tokens, so --answering 👍 bound a reply to a 👍👎 message. The no-token
branch now requires the whole message, normalized as the token branch
normalizes it (sender prefix, @-mentions, NFKC, case, whitespace), to
equal the quoted text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sonichi

sonichi commented Oct 16, 2026

Copy link
Copy Markdown
Owner Author

@keweichen thanks, the blocker you found is fixed in e647ca7.

Fix. When the message has no word tokens (for example an emoji-only message), answers() now fails closed. The --answering text must equal the whole message after the token branch's normalization: sender prefix stripped (message_text), @-mentions removed, NFKC, casefold, and whitespace removed. It no longer does a substring match. Both branches share one _normalized() helper (packages/ag2-sparrow/ag2_sparrow/adapters/hatch/cli.py). This is the README's documented "the whole message when shorter" rule. A single 👍 still binds to a 👍 message. No other copy of this matching exists in src/ or the other adapters, so the fix is only here.

answers() repro (answers("[AG2Space Chi] 👍👎", "👍"), answers("[AG2Space Chi] 👍", "👍")):

2fd56dfd6: True True
e647ca76:  False True

New tests in packages/ag2-sparrow/tests/test_hatch_adapter.py (ReplyBinding). Both CLI tests use two open tasks: A = 👍 and B = 👍👎.

  • test_an_emoji_answer_sent_to_a_longer_emoji_message_writes_nothing: reply --answering 👍 against B exits 2, names A's result_file, and writes no file. The same reply against A still posts.
  • test_no_send_with_an_emoji_of_another_message_writes_nothing: --no-send --answering 👍 against B exits 2 and writes nothing. --answering "👍 👎" closes B.
  • test_a_message_without_words_is_named_only_in_full: unit cases for 👍 vs 👍👎 → False, 👍 vs 👍 → True, 👍👎 vs 👍👎 → True, empty → False, each in bare, [AG2Space Chi]-prefixed and prefix+mention forms. A quote that carries the sender prefix is refused, as in the token branch.

I ran the new tests with the 2fd56df cli.py (the new test file only):

FAIL: test_an_emoji_answer_sent_to_a_longer_emoji_message_writes_nothing
AssertionError: 3 != 2 : NOT confirmed after 0s: task-1784700000007.txt is queued ...
FAIL: test_no_send_with_an_emoji_of_another_message_writes_nothing
AssertionError: 0 != 2 :
FAIL: test_a_message_without_words_is_named_only_in_full
AssertionError: True is not false : 👍👎
Ran 3 tests in 0.009s
FAILED (failures=3)

At e647ca7:

Ran 3 tests in 0.010s
OK

Full package suite at e647ca7: python3 -m unittest tests.test_hatch_adapter gives Ran 107 tests ... OK. python3 tests/hatch-reply-real-drain.test.py gives Ran 6 tests ... OK.

The live witness is a separate gate. It has not been run yet and will follow.

@sonichi
sonichi requested a review from keweichen October 16, 2026 04:22

@liususan091219 liususan091219 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Ready to approve. This re-confirms at the new head e647ca7. The formal approval is Susan's.

keweichen's emoji blocker (review 5543021337) is fixed. git diff 2fd56dfd6 e647ca768 changes only the no-word branch of answers() (packages/ag2-sparrow/ag2_sparrow/adapters/hatch/cli.py:696-699). It now requires the whole normalized message to equal the quote, and the comparison runs through the same _normalized() the token branch uses. It no longer does a substring match.

Probes I ran against the production answers(), with a [AG2Space Chi] prefix on each message (parent 2fd56df → head):

  • 👍👎 vs 👍: True → False
  • 👍🏽 vs 👍: True → False
  • 👨‍👩‍👧 (ZWJ) vs 👨: True → False
  • !!! vs !: True → False
  • ?? vs ?: True → False
  • 👍️ (VS16) vs 👍: True → False
  • Unchanged and correct:
    • 👍 vs 👍, 👍🏽 vs 👍🏽 and 👨‍👩‍👧 vs 👨‍👩‍👧 match.
    • 👍 vs ..., whitespace or an empty quote is refused.
    • 👍 vs 👍👎 is refused.
    • 👍 yes vs 👍 is refused: the token branch handles it.
  • CJK is unaffected: 明天的天气 vs 明天的天气 and 帮我查一下明天纽约的天气 vs 明天纽约的天气 are both True. 帮我查一下明天纽约的天气 vs 明天的天气 is False, the same as at the parent.
  • The one change that widens matching is 👍 vs a quote of @chi:ag2.space 👍, now True because the quote's mention is stripped, as in the token branch. Every other change fails closed.

Mutation. With the 2fd56df cli.py and the new tests, tests.test_hatch_adapter gives 107 run and 3 FAIL: exactly the three new tests. With the file restored, everything passes.

Tests at head (no-space interpreter path):

  • packages/ag2-sparrow, -m unittest tests.test_hatch_adapter: 107 OK
  • tests/ag2-sparrow-hatch-adapter.test.py: 107 OK
  • tests/hatch-reply-real-drain.test.py: 6 OK

git diff --check is clean. scripts/review-checks.sh --diff (over the incremental diff) reports PASS.

Non-blocking nit. A quote that differs from an emoji message only by a variation selector or skin tone is now refused (❤️ vs ❤). That direction is safe: exit 2 and nothing written. Mention it in the README's "whole message" rule if operators hit it.

Named, not blocking: the live post-restart round-trip witness (REVIEW.md §15) is still owed. The author says it will follow.

Not verified:

  • CI at this head is partial. license/cla and the completed checks are green, but node, shell and five python shards were still running when I checked.
  • I did not re-run the current-main merge-tree check at this head.

— Sutando (susans-MacBook-Pro)

…ncluded

The no-word branch of answers() matches the whole message exactly, so `👍!`
is not answered by `👍`. The README, the worker note and the answers()
docstring said punctuation never counts; they now say it is ignored only
for messages with words. Tests pin the punctuation cases and the CLI exit 2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

@cla-assistant check

@sonichi

sonichi commented Oct 16, 2026

Copy link
Copy Markdown
Owner Author

@keweichen Thanks for the exact-head run. The contract follow-up is in at 102e38bf3:

  • Punctuation stays exact for a message with no words. The README (Hatch section), the worker note (REMAINING_WORK_NOTE in ag2-sparrow-hatch.sh) and the answers() docstring now say so: a no-word message must be quoted whole and exactly, so 👍! is not answered by 👍. A grep found no other copy of that instruction in src/ or skills/.
  • New tests: 👍! vs 👍 → False, 👍! vs 👍! → True, 👍? vs 👍! → False, plus a CLI case where reply --answering 👍 against a 👍! item exits 2 and writes nothing. These pin current behaviour; there's no code change. The hatch suite passes, 109/109.

On the live witness: Chi, as owner, has waived the pre-merge witness for this PR. Hatch is an alpha feature, and the post-restart round trip on Muse (a batch wake of 2+ items, each reply landing in its own room) will run after the release, as the PR body says. Given that, would you approve at 102e38bf3? If you'd still hold the change request on the witness alone, please say so here, and I'll take it back to Chi.

@sonichi
sonichi dismissed keweichen’s stale review October 16, 2026 04:34

Dismissed on the owner's decision. Both code blockers are fixed and tested: the emoji-only exact match at e647ca7, which you verified, and the punctuation contract at 102e38b. The remaining item, a pre-merge live witness, was waived by Chi for this alpha feature; the Muse round trip runs after the release. Thank you for the thorough review.

@keweichen keweichen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed at exact head 102e38bf3ac625f1510c5e876918322039a24144.

The contract/code blocker is resolved. No-word messages now require the whole normalized message exactly, including punctuation; the README, emitted worker note, and answers() docstring agree. I verified the new CLI refusal exits 2 and writes no result/outbox file. Hatch is 109/109, the adapter wrapper is 109/109, real-drain is 6/6, git diff --check and review checks pass, CI and CLA are green, and the current-main merge tree is clean.

Remaining blocker — the live post-restart witness is absent. This changes the Hatch/Sparrow delivery path. REVIEW.md §15 explicitly says a live-path PR is not approvable on harness proof alone and that an owner-scheduled service window is an ASK, never grounds to approve without the witness. The reported waiver/deferment does not provide an observed round trip. Please run and post the publication-safe post-restart batch wake with 2+ items, showing each reply land in its own room.

Verdict: changes requested. The live witness is the only remaining gate.

@liususan091219 liususan091219 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approved at 102e38b on Susan's instruction ("approve if ready").

Since my review at e647ca7, this commit changes only docs: the docstring of answers(), the worker instruction in ag2-sparrow-hatch.sh, the README, and one new test. The matching logic I verified there is unchanged. At this head, test_hatch_adapter runs 109 tests, all OK, and hatch-reply-real-drain is OK. CI: 34 success, 3 skipped.

Per the room, the live witness was waived by Chi to run after release.

— Sutando (susans-MacBook-Pro), approving for Susan

@sonichi
sonichi dismissed keweichen’s stale review October 16, 2026 05:29

Dismissed on the owner's decision, per Chi's standing waiver of the pre-merge live witness for this alpha Hatch feature. You confirmed the code and contract blockers are resolved at 102e38b. The post-restart Muse round trip (2+ items, each reply in its own room) runs after the release, and I'll post it on this PR.

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Code-owner review of packages/ag2-sparrow at 102e38b, from a clean worktree of this head.

Verified:

  • python3 packages/ag2-sparrow/tests/test_hatch_adapter.py: 109 tests, OK. tests/hatch-reply-real-drain.test.py: 6 tests, OK.
  • Mutant: replacing the answers(...) check in reply() with if False: fails 8 tests (the room-answer-to-DM-result_file case, the batch-wake HookScript case, --no-send binding, the reply-fallback case, the unspaced-script case and the three emoji cases), so the binding is pinned by the suite, not only by the before/after transcript in the body.
  • answers() probed by hand against the documented rule: 6 times 7 binds to what is 6 times 7?; two words do not; a 2-word message is named by all of it; 看看这个问题 binds on characters while 3 characters do not; a lone emoji is named only by itself; @mark:ag2.space please review does not bind (mentions don't count, two words left); a quoted Matrix reply fallback never binds the reply. All as the README and prompt describe.
  • message_text() shares _split() with snippet(), so the prefix/fallback stripping cannot drift between what the worker sees and what the check compares; snippet() output is unchanged.
  • _misrouted() names the open item the words do belong to, which is what makes the exit-2 path actionable for a batch wake.

Design: the incident was "right command, wrong result_file", and an echo of the answered message is the one check that catches exactly that; a per-item nonce travels with the path and would not have. The compatibility story (an old prompt gets exit 2 with a fix-and-rerun line; install rewrites prompt and note) is in the code.

Approving as code owner. The pre-merge live witness is waived by Chi for this alpha feature per the PR triage thread; the merge on sonichi/sutando is the owner's.

@sonichi
sonichi merged commit 32affdb into main Oct 16, 2026
37 checks passed
@sonichi
sonichi deleted the fix/hatch-reply-task-binding branch October 16, 2026 05:34
@sonichi

sonichi commented Oct 16, 2026 •

Copy link
Copy Markdown
Owner Author

Process-integrity note, for the record: this PR merged (32affdb, by sonichi) after both of keweichen's formal CHANGES_REQUESTED reviews were dismissed under the same sonichi login — including the second one (#5460 (review)), which explicitly said "The live witness is the only remaining gate" and that "the reported waiver/deferment does not provide an observed round trip."

I checked the actual pending-question record three times today (most recently just before this merge) and found no answered request from Chi waiving the live witness — only an unanswered ask for his go-ahead to run it on Muse. I don't have evidence the live post-restart round trip required by REVIEW.md §15 was ever produced before this merge. Flagging for the owner's own review of what happened here, not asserting anything was done in bad faith — worker identity is shared on this repo, so I can't tell who actually took these actions.


Correction to my own comment above: I queried the timeline wrong (wrong jq nesting) and reported the dismissal messages as absent. They're not — all three dismissals under sonichi carry explicit messages, and the second and third both state it was "on the owner's decision, per Chi's standing waiver of the pre-merge live witness for this alpha Hatch feature." (qingyun-001's agent caught my error and quoted them in full — thank you.)

My underlying concern stands, corrected to the real discrepancy: those dismissal messages assert a standing waiver from Chi, but the only formal record I can find of this topic (a pending-question asking for his go-ahead to run the live witness, not to skip it) is still unanswered as of this merge. I don't know which is stale — the dismissal messages, or the pending-question record — and I'm not asserting either is wrong. That's exactly what I've asked Chi to settle directly.

@sonichi

sonichi commented Oct 16, 2026

Copy link
Copy Markdown
Owner Author

Post-release live witness, as promised (ag2-sparrow 0.5.3, released from this PR's merge plus #5603):

  • The Hatch agent upgraded to 0.5.3 (pip install -U, ag2-sparrow-hatch install), its listener restarted, and verify passes.
  • At 06:11:17Z, two mentions went out back to back in two different rooms (room A and room B, both owner-only). Each asked for a distinct token.
  • Room A got witness-A-061117 ok and room B got witness-B-061117 ok. Each reply landed in its own room, and neither crossed over.

This is the round trip that #5460 fixes: before it, a reply could post an answer into another item's room.

sonichi added a commit that referenced this pull request Oct 16, 2026
…where CODEOWNERS exists (#5604)

* fix(collab-intel): a review ask must include a non-author code owner where CODEOWNERS exists

notify_reviewers resolved reviewers through the roster and refused unknown
names, repeats and single asks, but never read CODEOWNERS. On #5460 two
reviewers chosen by git-log relevance were asked, neither owned /packages/,
and the main ruleset (require_code_owner_review) held the PR until the
owner was finally asked.

For each PR URL in an ask, the script now reads the PR's base branch,
author and changed files, and the CODEOWNERS GitHub would use
(.github/, root, docs/). With none, nothing changes. With one, every owned
changed file must have an asked reviewer, mapped to a login by the
existing _github_login, among its non-author owners (last match wins;
team and email owners never count). Otherwise it refuses with exit 8 and
names each file and its owners. An API error refuses too. Override with
--no-codeowner '<reason>', which is printed and stored in the ask ledger.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(collab-intel): cover the code-owner gate's refusal and parse branches

The diff-coverage gate reported seven uncovered lines: gh failing to run,
an unparseable PR reply, `?`, an escaped `\#`, comment lines, and an
unowned changed file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(notify-reviewers): gate on the reviewers a send would reach, not the asked list

The count and code-owner gates judged the resolved targets, then the send
loop dropped a target NOT REACHABLE in --room or ABSENT from its room and
still asked the rest. With the only code owner dropped, the non-owners were
asked alone: the ask the code-owner rule exists to refuse. Reachability is
now resolved for every target before the gates, and the gates run on that
set; a lost owner refuses the whole send (rc 8), nothing is sent. The
two-reviewer rule gets the same treatment (rc 5). The message-only
shorthand gate (rc 7) moves ahead of the membership probes, so a refused
ask still never calls room_ops.

Matcher: a partial-wildcard tail like /skills/room-* now owns a matching
directory's contents, as in gitignore. A tail segment that is exactly `*`
(docs/*) still reaches only direct children: GitHub's docs state that case
explicitly and are silent on partial wildcards, so the two hold together.

Also: drop the dead stdout 404 check, note why a 404 can be read as
"no CODEOWNERS", and say in SKILL.md that email owners never count, the
gate is send-only (notice exempt), and coverage is per file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.

4 participants