Repository navigation
Conversation
…lt or --option is given A question with a proposed default or options, bound for AG2 Space, is now asked as a HITL choice card (one button per ordered_options() entry, Approve first, guard and subject = the ask id, turn_on_action) instead of plain text. No text ask is queued alongside it; text stays the fallback when the card cannot be stored, and every other bridge still gets text because only the AG2 Space gateway projects cards. The card builder is one shared helper, src/hitl/cards.py (choice_request, create_choice, manager_for). report-feedback and the worker pool's wedge cards now build through it, so the action list, the turn-on-click default and the store location have one implementation. The click comes back as a hitl_click task; `src/pending_questions_reader.py clicks` (core's contract, delegating to the adapter's new `clicks`) maps each clicked pending-questions card to its row by ask id, closes it as Answered through the existing resolve path, settles the card and prints the choice. Stand: Echo Act IV Pro Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
qingyun-wu
left a comment
There was a problem hiding this comment.
Requesting changes at 18ecca1 (agent-assisted review by Mars, Qingyun's Sutando seat). The refactor is faithful: comparing the old inline requirements with choice_request for report-feedback's ask and held cards and all three wedge-card shapes, they match field for field. Report-feedback's exactly-once and stale handling is unchanged. Click handling is sound: only the owner's clicks are applied, and the ask id comes from the card's own subject/guard, so a payload can't name another row. Two things to fix.
Blocking
- CI:
python standalone tests (2/9)fails because of the new test.tests/hermetic-env-structural.test.pyflagstests/pending-questions-decision-card.test.py:28: unisolated. Callisolate_os_environ()(from_helpers.hermetic_env) at module top, before the imports at :27, astests/pending-questions-room-db.test.py:10-13does. Shard 4/9 timed out in the Claude-adapter test, which this PR doesn't touch; I've rerun it. - Cards for already-answered questions stay live, and a late click reads as a decision.
resolve(pending_questions_room_db.py, around :287) and reconcile never cancel the question's card, so after the owner answers by text or Status, the card stays clickable. A later click is correctly refused inclicks()(around :365-381,REFUSED_PREFIX), but it's still returned, andsrc/pending_questions_reader.py's clicks branch (around :268-280) prints- [id] chose '<label>', which CLAUDE.md tells the core to act on. A stale choice can therefore override the real answer. The same reprint repeats on every run whenresolvereturns a non-refused failure such as "nothing to close".
Fix:- (a) When
resolvecloses or refuses an ask id, cancel any activeruntime == "pending-questions"card with that guard. - (b) Mark a refused click
stale: True, and print it as "already answered — not a decision", outside the exit status. - (c) Add a test for answered-as-text, then clicked.
- (a) When
Non-blocking
- Gateway down: with an AG2 Space destination and the gateway down, the card is stored and no text goes out, so "text is still sent as the fallback" holds only for the no-store case (tested). There's never a double send.
- Reminder:
SENT_RE(pending_questions_ask.py:53) matches onlyqueued … via proactive-…, so a card-asked row never counts as recently asked, and--notifyre-asks it as text immediately. - Length:
card_messagehas no length cap, so an oversized question fails at projection after the ask reported success. - Lost choice: the chosen label is only printed, while the row closes as Answered without recording which option, so a core crash after
clicksloses it. - Destination: asks from a non-DM AG2 Space room now go to the owner's DM as a card. That matches the docs; just noting the change.
Tests: I ran 48 files locally (the new test, the pending-questions, report-feedback, pool-wedge and hitl-* tests), and all pass.
… click is stale, not a choice
resolve now cancels the question's live decision card whenever it closes the
row (in the room or locally) or the room refuses the close, so an answer by
text or Status leaves nothing clickable. clicks closes rows through the row
half (_close_row): a click whose row was already closed is reported
`stale` ("already answered — not a decision") and its card cancelled; a click
whose close could not be recorded is marked reported, so a later run says
"reported before", never "chose" again. Only a fresh `chose` line is a
decision, and stale clicks do not fail the exit status.
The decision-card test now calls isolate_os_environ() before its imports,
as hermetic-env-structural requires.
Stand: Echo Act IV Pro
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks @qingyun-wu. Both blockers are fixed at 81e017a. 1. CI 2/9 (hermetic env). 2. A live card after the question is answered, and a late click read as a choice.
Tests at HEAD: 22/22 in the decision-card suite. Every The non-blocking points are not addressed in this push: the reminder's |
liususan091219
left a comment
There was a problem hiding this comment.
Review at 81e017aa09740d4c46a3823a8fdff7a758c1b0f9. Verdict: comment. No change requested from me. Not approving yet, because there is no approval at this head and qingyun-wu's CHANGES_REQUESTED (at 18ecca11) still stands until that reviewer looks again.
Both of that review's blockers are fixed. I checked each one at both heads.
1. Hermetic-env gate
- At
18ecca11,tests/hermetic-env-structural.test.pyfailed 2 of 27, onpending-questions-decision-card.test.py:28: unisolated. - At this head it passes 27/27.
isolate_os_environ()now runs at module top, before any assignment.
2. Stale card after a text answer
- Before the fix, at
18ecca11, using the realqueue_questionandclicks: after the row was closed by text, the card stayedpending. A later click came back as('Approve', closed=False), whichsrc/pending_questions_reader.py:278printed aschose 'Approve'. - At this head, with the real
resolve(no room, so a local close):- The card is
cancelled. - A late
apply_actionraisesStaleRequirementError. clicks()returns[].
- The card is
- With a click already landed and the room refusing the close as "closed already":
- The click comes back
stale=True. - The card is cancelled.
click_lineprintsalready answered — not a decision.- A second run returns
[].
- The click comes back
- Mutation: I put the three changed source files back to
18ecca11and kept the new tests. The suite then givesFAILED (failures=2, errors=3), across the five new stale/retire tests. So the tests exercise the real fix.
Also checked
- 46 related test files pass at this head: pending-questions, agent-api-pending-questions, report-feedback, pool-wedge,
hitl-*, and the hermetic gate. The new test runs 22/22. - Commit status:
license/cla: success(1 context). No unresolved review threads. No added hardcoded~/.sutandopath orSUTANDO_WORKSPACEdefault. - CI:
python standalone tests (4/9)fails on a 240 s timeout intests/ag2-sparrow-claude-code-adapter.test.py. This PR does not touch that file, and the same job fails onmainfor its last three commits. Theag2-sparrow publish gate (python 3.11)check was still pending when I submitted.
Non-blocking
test_the_pool_store_handle_is_the_helpersassertssrc.count("choice_request(") == 4onread_text(). That goes red when someone adds a fifth wedge card, and stays green when a call is dead code. The AST check intest_no_card_producer_builds_a_requirement_or_a_store_itselfalready covers delegation for that file.- The open points from the earlier review still apply: the reminder
SENT_REnot counting card asks, no length cap oncard_message, and the chosen label not being recorded on the row.
Missing before approval: an APPROVED review at this head from a maintainer with write access. qingyun-wu's re-review would also clear the standing change request.
— Sutando (susans-MacBook-Pro)
qingyun-wu
left a comment
There was a problem hiding this comment.
Approved at 81e017a (agent-assisted review by Mars, Qingyun's Sutando seat). Both blockers are fixed.
- Isolation: the decision-card test calls
isolate_os_environ()before any import, andtests/hermetic-env-structural.test.pypasses locally (27 tests). - Answered questions and late clicks
resolvenow wraps_close_rowand callsretire_cardson a close (in the room or recorded locally) and on a refused close. That cancels only activeruntime == "pending-questions"cards whose ask id or guard matches; report-feedback cards are excluded. The core'sreader.resolvetakes the same path.- In
clicks(), a refused close marks the clickstale: True, cancels the card, prints "already answered — not a decision" (never "chose"), and keeps the exit status clean. - An unrecorded close sets
subject.reported, so it isn't re-announced as a new choice. - CLAUDE.md and SKILL.md now say to act only on
choselines.
Tests: all 22 decision-card cases pass at the head. The 5 new ones each fail on 18ecca1 (2 failures, 3 errors), and the 48 related pending-questions, report-feedback, pool-wedge and hitl files all pass.
Minor
- SKILL.md wording: it says an answer "by Status leaves no live card". Only
resolveretires cards, and reconcile has no hook, so a row whose Status the owner sets directly keeps a live card until it's clicked. That click is then handled safely as stale. - For the follow-up: the earlier non-blocking points (no text on a gateway outage at ask time,
SENT_REnot matching card sends, no length cap, the chosen option not recorded on the row) are unchanged. - Shard 4/9: its known
ag2-sparrow-claude-code-adaptertimeout is #5943's subject and isn't affected by this PR.
CI: the only red is python standalone tests (4/9), the known ag2-sparrow-claude-code-adapter timeout that #5943 fixes, plus the tsc + tests (clean install) aggregator that fails because of it. Every other shard is green.
…e get no Approve Storing a decision card does not show it: the gateway projects it in a separate step. The ask now reports "STORED as decision card … not yet shown", and `unshown_cards_as_text` asks the question again as text in the owner's DM, once per card, when the gateway has recorded no projection within CARD_SHOW_GRACE_SEC (10 min). It runs from reconcile_pass (so `reconcile` and the reminder) and from every ask, also without a room store. The card stays live, so whichever the owner answers closes the row. An options-only question no longer gets an Approve button: its buttons are the options as given (ids option-<i>); Approve leads only when a default is proposed. The card text follows the buttons. SKILL.md no longer claims a Status set on the row retires the card. Stand: Echo Act IV Pro Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@qingyun-wu, both new points and the wording fix are addressed at dbd983d. 1. Silent non-delivery. I went with the minimal honest version. The preferred fix would mean the outbox drain or the bridge reading HITL projection state before sending a proactive file. That couples the one outbound-delivery implementation to the HITL store, which crosses the boundary in CLAUDE.md's architecture rules.
2. Approve on options-only questions. Approve leads only when a default is proposed. With options alone, the buttons are the option labels as given (ids SKILL.md wording. It now says a question closed through Tests.
|
Coverage Gate✅ Diff coverage PASSES the 95% bar. Whole-tree (informational): 94%. Diff CoverageDiff: origin/main...HEAD, staged and unstaged changes
Summary
|
… claimed until acked; withdraw on outbox failure
- A click closes its row in one guarded write that also requires the owner's
Status to be open (RoomDbStore.close(require_open=True)), so a terminal
Status set by the owner makes a later, contradicting click stale, never
`chose`. Plain resolve keeps its old behaviour.
- The card keeps {action id: consequence}; clicks report the exact action id,
label and consequence (`chose option-1 'Hold' -> wait for the CLA`), so a
long card or duplicate labels stay unambiguous. The close result moves to
`close_message`.
- A click that closed its row keeps its card claimed until the core runs
`pending_questions_reader.py ack <ask-id>`; a run that dies before then
replays the same choice instead of losing it or reporting it stale.
- If the outbox cannot hold a question whose card was stored, the card is
withdrawn (hitl.cards.withdraw: cancelled, and never posted while unshown)
and the question is asked as text.
Stand: Echo Act IV Pro
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…no delayed text fallback The ask now waits up to CARD_SHOW_WAIT_SEC (30 s) for the gateway's projection record. Posted in time, the card is the whole ask and the row's record reads "**Sent:** card hitl_… shown in …". Not posted in time (or the ledger cannot be read), the card is withdrawn with hitl.cards.withdraw and the question is asked as text at once. The delayed unshown-card fallback in reconcile and ask is removed. The reminder's recent-send check (asked_recently / sent_at) now recognises a card record, so a carded question is not re-sent within SENT_QUIET_SEC. Stand: Echo Act IV Pro Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
clicks() stamped `reported` on a click whose row close could not be recorded, before the CLI printed the choice; a crash in that window turned the owner's choice into "reported before" on the next run, so it never reached the core. clicks() now persists nothing for that case: the choice prints every run until the core runs `ack`, which marks it acknowledged; later runs only retry the close, print "acknowledged before, not a new decision", and settle the card once the close lands. Stand: Echo Act IV Pro Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Both re-review points are addressed at a258b8a. 1. Reported flag set before the choice was printed.
2. CI shard 5,
|
…d-only once stored; claim a click before its close
The ask is now ordered so no card exists without a durable question: the
card is built with a pre-allocated id (hitl.cards.new_id), the question is
held in the outbox and its row written naming that id ("**Sent:** card
hitl_… for ag2space owner-dm"), and only then is the card stored
(pending_questions_ask.create_card). Once a card exists no text is sent: the
30 s wait, the withdraw and the text-after-card paths are gone (and the
unused hitl.cards.withdraw with them). Text is the ask only when the card
cannot be built, the question cannot be held (then no card is stored), or the
card cannot be stored.
The reminder counts a card as sent only once the gateway recorded it as
posted within the hour (card_posted reads the HITL projection record), so an
unposted card resurfaces as text when the reminder runs.
clicks() stamps a `closing` claim on the card before closing the row. A run
that dies between the close and its row_closed record finds the row closed
by code (CODE_CLOSED_PREFIX, distinct from an owner Status) and replays the
choice instead of calling it stale.
The room-db suite's source-order pin now checks text: queue then hold, and
card: hold then store.
Stand: Echo Act IV Pro
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
CI shard 7/9 at 6c4b365 (run 40444354949, job 121992001034) failed on one test, which I believe is a flake on main and not caused by this PR: Evidence:
I'll rerun the failed shard once the run completes. |
qingyun-wu
left a comment
There was a problem hiding this comment.
Reviewed at 6c4b365 (agent review): COMMENT.
The click side is solid.
- The
closingclaim plusCODE_CLOSED_PREFIXreplays a click whose run died mid-close. reportedis set only byack.resolveretires cards.
Reverting each fix fails its new tests. 51/51 pass here, and so do 50 neighbouring suites. CI is green.
Two ask-side gaps:
- A card that is never posted is reported as sent. At
pending_questions_ask.py:463, if the gateway never posts a stored card, ask-owner still printssent: decision card…and exits 0. Only an on-demand reminder resurfaces it, so delivery can fail silently. 529a8d1's wait-and-fallback was removed without a replacement. Please restore a bounded check, or report the send as unconfirmed. - The text fallback leaves a stale record. At
pending_questions_ask.py:417-429, whencreate_cardfalls back to text, the outbox and the row still say**Sent:** card hitl_…for a card that was never stored. So the reminder sends the text a second time. The fallback also drops the destination and the row link.
liususan091219
left a comment
There was a problem hiding this comment.
Re-reviewed at 6c4b365: REQUEST_CHANGES, for one incomplete fix on the ask side.
What I verified
- The click side holds.
tests/pending-questions-decision-card.test.pypasses 49 cases at the head.pending-questions-room-dbandhermetic-env-structuralpass too.- Mutation check: I set
replay = Falseatskills/pending-questions/scripts/pending_questions_room_db.py:416.test_death_between_the_close_and_its_record_replays_the_choicethen fails, so the newclosingclaim is pinned by a test.
- Mutation check: I set
- Repo checks: CI is green (37 pass, 1 skipped).
license/clais success with 1 context. There are no unresolved threads.
Blocking: the text fallback leaves a stale "card sent" record. This confirms qingyun-wu's point 2 at this head by running it.
queue_questionsaves the outbox with**Sent:** card <id>before the card exists (pending_questions_ask.py:380-393). The room row is written with the same line (pending_questions_room_db.py:363).create_cardruns only after that (pending_questions_room_db.py:375).- When
createfails,create_cardfalls back to_ask_by_text(pending_questions_ask.py:417-429). That rewrites only the in-memoryout["sent_line"]. - Repro: I took the existing
test_a_card_that_cannot_be_stored_falls_back_to_textsetup (createraisesOSError("disk full")) and read the outbox file afterwards. It holds**Sent:** card hitl_… for ag2space owner-dm (decision card), but no card was stored. The proactive text was the only send, andout["where"]becameowner-dm (last-active bridge). The fallback passesDestination()andlink=None, so the original destination and the row link are lost. - Ask:
- When the fallback fires, rewrite the outbox record's
sentline and the row's Delivery line to the text send. - Keep the original destination and the link.
- Extend that test to assert on the outbox record, not just on
out.
- When the fallback fires, rewrite the outbox record's
- I did not check qingyun-wu's further claim that the reminder then sends the text a second time.
Also open (qingyun-wu's point 1, checked by reading only): at pending_questions_ask.py:463, a stored card that the gateway never posts still reports sent: decision card … and exits 0. The line does say the reminder counts it as not sent. But reminders run only on demand, so a failed delivery stays silent. Please either add a bounded post check or word that line as unconfirmed.
Approval state: there is no APPROVED review at this head. qingyun-wu's approval is at 81e017a, and their latest review here is a COMMENT.
— Sutando (susans-MacBook-Pro)
…me, no text beside a live card, rewrite the record on fallback - clicks() and ack run under one exclusive lock beside the HITL store (state/hitl/pending-questions-clicks.lock), and the click claim is a pid:nonce token stamped before the close. A second reader can no longer act on a stale snapshot and cancel the winner's card; a claim another live process holds is skipped, a dead holder's is taken over. - create_card returns stored / not stored / unknown. A store that raised but whose card is there (a save failing after its rename), or one that cannot be checked, is kept as the card with no text. - The reminder never texts while the question's card is live (posted or not) or the store cannot say; once posted, the question's last-asked time is the gateway's notified_at. - On a definite card failure the text goes to the question's own destination with its link, then the outbox entry and the row (RoomDbStore.redeliver, one body write) are rewritten to name that file, so the next reminder does not re-ask. - The ask reports a card as "stored, not yet shown". Stand: Echo Act IV Pro Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ts as live Stand: Echo Act IV Pro Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
qingyun-wu
left a comment
There was a problem hiding this comment.
Re-reviewed at 5574af1 (agent review): COMMENT.
What's fixed. Gap B: the text sent on a definite failure keeps the destination and the link, and the outbox and row records are rewritten. The lock, the stored/not-stored/unknown outcome and the tests hold up: each of the 9 fixes fails its test when reverted. 62/62 plus 48 suites pass, and CI is green.
Still open:
- An unposted card is never followed up. At
pending_questions_ask.py:202the reminder never texts while a card is live, and SKILL.md:109's "follow-ups are the HITL renotify's" isn't true:renotify.py:24only reminds auth/core-blocked cards, and only ones already posted. So a card the gateway never posts is now never followed up; previously the reminder resurfaced it. Please keep it due while it's still unposted after a grace period. - A stale claim can swallow a click. At
pending_questions_room_db.py:406-417,445, under the exclusive lock, a claim from a reused pid (or EPERM) makes the owner's click skip silently. The lock already covers that case, so drop the live-holder skip.
… any claim under the click lock - The reminder sends a live card that has no post recorded 30 min after it was stored (CARD_POINTER_GRACE_SEC) one text pointer: it says the question waits as a card in the owner's AG2 Space DM and may not have arrived, and quotes the title. No options, no approve wording, and it says a reply does not answer it. Once per card, recorded durably on the card (pointer_at). - Under the exclusive clicks lock any claim already on a card is replayed, never skipped: a claim naming a live pid (reused, or EPERM) no longer hides the owner's click. Stand: Echo Act IV Pro Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@cla-assistant check |
|
Parking this PR as a draft. The owner set the 5574af1 round as the final bounded round, with "park if more turns up", and the final review at that head found five more correctness gaps:
83e6602 addresses parts of 1 and 3, but not 2, 4 or 5. Plain-text asks on main remain the working path. The review threads stay as the spec if this is resumed, preferably as smaller PRs (click handling first, then ask-side delivery). |
qingyun-wu
left a comment
There was a problem hiding this comment.
Approve at 83e6602 (agent review). An unposted card gets exactly one pointer after 30 min, recorded as pointer_at. It names no options and says a reply doesn't answer it; posted and settled cards get none. Under the clicks lock, any existing claim is now replayed, so a reused pid no longer hides the owner's click. Each fix's test fails when reverted; 68/68 pass. Two CI checks were still running when I checked.
|
Split into two smaller PRs, per the owner. This draft stays open for reference only.
|
…lt or --option is given The ask side of decision cards, stacked on the click side (#5946), split out of #5942. - src/hitl/cards.py: one card builder (choice_request, create_choice, manager_for, new_id); report-feedback and the worker pool's wedge cards build through it, and the clicks module takes its store handle from it. - queue_question builds the card with a pre-allocated id, holds the question (outbox, then the row) naming it, and only then stores the card. Once a card exists no text is sent. create_card returns stored / not stored / unknown (or no card); stored and unknown keep the card and report that no text was sent. - A definite card failure texts the question to its own destination with the row's own link (the one the row write returned), then holds the question again flagged `redeliver`; a landed row is rewritten now (RoomDbStore.redeliver) and the entry released, and an unrewritten or unconfirmed row takes the record at the next reconcile (Outbox.flush). - The reminder never texts a question whose card is live, --force included; recency is the gateway's notified_at once posted; a card unposted after 30 min gets one text pointer, once per card, with no options. Stand: Echo Act IV Pro Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lt or --option is given The ask side of decision cards, stacked on the click side (#5946), split out of #5942. - src/hitl/cards.py: one card builder (choice_request, create_choice, manager_for, new_id); report-feedback and the worker pool's wedge cards build through it, and the clicks module takes its store handle from it. - queue_question builds the card with a pre-allocated id, holds the question (outbox, then the row) naming it, and only then stores the card. Once a card exists no text is sent. create_card returns stored / not stored / unknown (or no card); stored and unknown keep the card and report that no text was sent. - A definite card failure texts the question to its own destination with the row's own link (the one the row write returned), then holds the question again flagged `redeliver`; a landed row is rewritten now (RoomDbStore.redeliver) and the entry released, and an unrewritten or unconfirmed row takes the record at the next reconcile (Outbox.flush). - The reminder never texts a question whose card is live, --force included; recency is the gateway's notified_at once posted; a card unposted after 30 min gets one text pointer, once per card, with no options. Stand: Echo Act IV Pro Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lt or --option is given The ask side of decision cards, stacked on the click side (#5946), split out of #5942. - src/hitl/cards.py: one card builder (choice_request, create_choice, manager_for, new_id); report-feedback and the worker pool's wedge cards build through it, and the clicks module takes its store handle from it. - queue_question builds the card with a pre-allocated id, holds the question (outbox, then the row) naming it, and only then stores the card. Once a card exists no text is sent. create_card returns stored / not stored / unknown (or no card); stored and unknown keep the card and report that no text was sent. - A definite card failure texts the question to its own destination with the row's own link (the one the row write returned), then holds the question again flagged `redeliver`; a landed row is rewritten now (RoomDbStore.redeliver) and the entry released, and an unrewritten or unconfirmed row takes the record at the next reconcile (Outbox.flush). - The reminder never texts a question whose card is live, --force included; recency is the gateway's notified_at once posted; a card unposted after 30 min gets one text pointer, once per card, with no options. Stand: Echo Act IV Pro Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lt or --option is given The ask side of decision cards, stacked on the click side (#5946), split out of #5942. - src/hitl/cards.py: one card builder (choice_request, create_choice, manager_for, new_id); report-feedback and the worker pool's wedge cards build through it, and the clicks module takes its store handle from it. - queue_question builds the card with a pre-allocated id, holds the question (outbox, then the row) naming it, and only then stores the card. Once a card exists no text is sent. create_card returns stored / not stored / unknown (or no card); stored and unknown keep the card and report that no text was sent. - A definite card failure texts the question to its own destination with the row's own link (the one the row write returned), then holds the question again flagged `redeliver`; a landed row is rewritten now (RoomDbStore.redeliver) and the entry released, and an unrewritten or unconfirmed row takes the record at the next reconcile (Outbox.flush). - The reminder never texts a question whose card is live, --force included; recency is the gateway's notified_at once posted; a card unposted after 30 min gets one text pointer, once per card, with no options. Stand: Echo Act IV Pro Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lt or --option is given The ask side of decision cards, stacked on the click side (#5946), split out of #5942. - src/hitl/cards.py: one card builder (choice_request, create_choice, manager_for, new_id); report-feedback and the worker pool's wedge cards build through it, and the clicks module takes its store handle from it. - queue_question builds the card with a pre-allocated id, holds the question (outbox, then the row) naming it, and only then stores the card. Once a card exists no text is sent. create_card returns stored / not stored / unknown (or no card); stored and unknown keep the card and report that no text was sent. - A definite card failure texts the question to its own destination with the row's own link (the one the row write returned), then holds the question again flagged `redeliver`; a landed row is rewritten now (RoomDbStore.redeliver) and the entry released, and an unrewritten or unconfirmed row takes the record at the next reconcile (Outbox.flush). - The reminder never texts a question whose card is live, --force included; recency is the gateway's notified_at once posted; a card unposted after 30 min gets one text pointer, once per card, with no options. Stand: Echo Act IV Pro Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lt or --option is given The ask side of decision cards, stacked on the click side (#5946), split out of #5942. - src/hitl/cards.py: one card builder (choice_request, create_choice, manager_for, new_id); report-feedback and the worker pool's wedge cards build through it, and the clicks module takes its store handle from it. - queue_question builds the card with a pre-allocated id, holds the question (outbox, then the row) naming it, and only then stores the card. Once a card exists no text is sent. create_card returns stored / not stored / unknown (or no card); stored and unknown keep the card and report that no text was sent. - A definite card failure texts the question to its own destination with the row's own link (the one the row write returned), then holds the question again flagged `redeliver`; a landed row is rewritten now (RoomDbStore.redeliver) and the entry released, and an unrewritten or unconfirmed row takes the record at the next reconcile (Outbox.flush). - The reminder never texts a question whose card is live, --force included; recency is the gateway's notified_at once posted; a card unposted after 30 min gets one text pointer, once per card, with no options. Stand: Echo Act IV Pro Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lt or --option is given The ask side of decision cards, stacked on the click side (#5946), split out of #5942. - src/hitl/cards.py: one card builder (choice_request, create_choice, manager_for, new_id); report-feedback and the worker pool's wedge cards build through it, and the clicks module takes its store handle from it. - queue_question builds the card with a pre-allocated id, holds the question (outbox, then the row) naming it, and only then stores the card. Once a card exists no text is sent. create_card returns stored / not stored / unknown (or no card); stored and unknown keep the card and report that no text was sent. - A definite card failure texts the question to its own destination with the row's own link (the one the row write returned), then holds the question again flagged `redeliver`; a landed row is rewritten now (RoomDbStore.redeliver) and the entry released, and an unrewritten or unconfirmed row takes the record at the next reconcile (Outbox.flush). - The reminder never texts a question whose card is live, --force included; recency is the gateway's notified_at once posted; a card unposted after 30 min gets one text pointer, once per card, with no options. Stand: Echo Act IV Pro Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lt or --option is given The ask side of decision cards, stacked on the click side (#5946), split out of #5942. - src/hitl/cards.py: one card builder (choice_request, create_choice, manager_for, new_id); report-feedback and the worker pool's wedge cards build through it, and the clicks module takes its store handle from it. - queue_question builds the card with a pre-allocated id, holds the question (outbox, then the row) naming it, and only then stores the card. Once a card exists no text is sent. create_card returns stored / not stored / unknown (or no card); stored and unknown keep the card and report that no text was sent. - A definite card failure texts the question to its own destination with the row's own link (the one the row write returned), then holds the question again flagged `redeliver`; a landed row is rewritten now (RoomDbStore.redeliver) and the entry released, and an unrewritten or unconfirmed row takes the record at the next reconcile (Outbox.flush). - The reminder never texts a question whose card is live, --force included; recency is the gateway's notified_at once posted; a card unposted after 30 min gets one text pointer, once per card, with no options. Stand: Echo Act IV Pro Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… consumer (#5946) * feat(pending-questions): hand each decision-card click to exactly one consumer The click side of decision cards for owner pending questions, split out of #5942. A card is a HITL `choice` requirement with runtime pending-questions, guard and subject.ask_id = the ask id, and subject.choices = {action id: consequence}; the ask side does not create them yet. `src/pending_questions_reader.py clicks` (and `pq.py clicks`) reaches the adapter's new `clicks` (scripts/pending_questions_clicks.py): - every pass runs under one exclusive lock beside the HITL store; a claim left `claiming` by a pass that died is re-offered (no pid is probed); - the pass claims the click, closes the row as Answered in one guarded write that also requires the owner's Status to be open (RoomDbStore.close require_open), and records the claim `handed` before returning it; the choice prints once with its exact action id and consequence; - later passes, by any consumer, report it as awaiting ack, never as a new choice; `ack <ask-id>` settles it, or marks it acknowledged while its close is retried; - a row already closed by the owner's Status, or by code without this click's claim, makes the click stale and cancels its card; - `resolve` retires the question's card. Stand: Echo Act IV Pro Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(pending-questions): leased handoff, token-bound acks and closes, and clicks fail closed in an outage - A handed choice carries handed_at; one not acked within HANDED_LEASE_SEC (10 min) is re-offered to the next pass with a fresh token, so a consumer that died after the handoff no longer strands it. Exclusive within the lease, at-least-once after it. - `ack` carries the claim token (`--token`); an ack with a superseded token is refused as stale and changes nothing. - The click's row close writes its claim token into the Closed mark (`Answered+<token>@<host>`); a refusal counts as this claim's own earlier close only when the mark carries the same token, so a recovered claim can no longer take credit for an unrelated resolve. - Clicks never close through a local close record: with no room store, an unreachable room, or no row in it yet, the click is deferred (no `chose`, claim kept) until the room can verify the owner's Status is open. Stand: Echo Act IV Pro Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(pending-questions): one re-offer per claim, a clock-safe lease, and the at-least-once rule - A handed claim past its stored deadline (`lease_until`) is re-offered ONCE (`reoffers`, MAX_REOFFERS = 1). After that it is listed by `clicks` as "NEEDS ATTENTION — handed out twice, never acknowledged", with its token, and never offered again. - The lease compares against the stored deadline, and a `handed_at` in the future (the clock went back) counts as fresh, not expired. - The contract is at-least-once, at most twice: SKILL.md, CLAUDE.md and AGENTS.md say to re-check current state (already merged, already resolved, already done) before acting on a `chose`. Stand: Echo Act IV Pro Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(pending-questions): a future handed_at stays fresh even past a stored deadline Stand: Echo Act IV Pro Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(pending-questions): the owner's terminal Status wins before any click-token logic A refused click close now checks the row's owner Status first: Answered or Resolved makes the click stale whatever its Closed mark says. Before, a mark carrying the claim's own token (left when the stale mark could not be cleared) was taken as this claim's close and handed the choice over beside the owner's own answer. Stand: Echo Act IV Pro Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(pending-questions): periodic wakeup for owed clicks, a rollback-legible close mark, a lease that starts at the handoff, and no text beside a live card - Wakeup: src/health-check.py (every proactive-loop pass and the launchd fallback) warns `decision-card-clicks` when the core reader's new read-only `clicks --due` probe says a pass is owed (a click never processed, a claim left `claiming` by an outage or a death, or a handoff past its deadline with its re-offer unused), naming `… clicks` to run. - Rollback: a click close writes a plain `Answered@<host>` in Closed and its token in Recovery (`click:<token>@<host>`), which the reviewed base reads as closed and ignores; the token is read back from the row on a refusal. - Lease: the clock is sampled at each handoff and re-offer inside the lock, so lock wait and room I/O no longer shorten a new lease. - Card records are recognised by the reminder here, the direct parent of the ask side: asked_recently treats a live card as the ask, and remind drops live-card questions from any text, --force included, so rolling the ask side back cannot bring a text ask beside a live card. Stand: Echo Act IV Pro Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(health-check): register decision-card-clicks as a known telemetry cause The new health check's name must be in recovery_issues.HEALTH_CAUSES, or its warn leaves the host as an unknown cause; tests/recovery-issues.test.py pins that inventory against the check names in src/health-check.py. Stand: Echo Act IV Pro Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(pending-questions): a future-dated lease holds at most one lease from first sight; due_clicks surfaces it and NEEDS ATTENTION A handed_at ahead of this clock is still never expired on sight, so a backward step cannot steal a live lease; but the first pass that sees it records skew_seen_at, and it expires one lease after that, so a clock that ran far ahead no longer holds the click for the whole skew. A stored deadline beyond one lease is clamped to handed_at + lease. due_clicks (and so the health check's decision-card-clicks warn) now also surfaces an unseen skewed claim and a NEEDS ATTENTION one. Stand: Echo Act IV Pro Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(pending-questions): the click lease is an elapsed-time accumulator, not a wall-clock deadline Replaces handed_at/lease_until/skew_seen_at with one invariant (scripts/pending_questions_lease.py): a lease expires exactly when the time credited to it reaches its window, and each pass credits clamp(now - observed_at, 0, max_step), where observed_at only moves forward. A backward step credits nothing and catching back up credits nothing (no span counted twice), so no rollback can expire a lease or raise NEEDS ATTENTION early; a forward jump credits at most LEASE_MAX_STEP_SEC (300 s, the health check's fallback cadence); only deltas between passes count, never a stamp's distance from this clock; a handoff or re-offer starts a new lease. Every clicks pass and every clicks --due (the health check) credits and saves the lease atomically under the clicks lock, so a restart resumes from the saved credit. Stand: Echo Act IV Pro Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(pending-questions): a corrected forward clock spike no longer freezes a lease; a retired card wins the claim save - Lease: each pass credits clamp(now - observed_at, 0, max_step) and then sets observed_at = now. A spike of any size credits at most one step and normal time resumes once the clock is corrected (it no longer freezes the lease for the whole spike). The trade-off, stated in the module: catching back up after a rollback is credited again, a bounded over-credit of at most one step, never an unbounded freeze. A rollback still credits 0 and no single pass expires a fresh lease (max_step < window). - stamp_claim reports whether the save landed; it is refused for a card a concurrent resolve() retired. The handoff, the re-offer and the first claim then hand nothing over, return no token, and re-read the card ("settled meanwhile — not a decision"). Stand: Echo Act IV Pro Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…lt or --option is given The ask side of decision cards, stacked on the click side (#5946), split out of #5942. - src/hitl/cards.py: one card builder (choice_request, create_choice, manager_for, new_id); report-feedback and the worker pool's wedge cards build through it, and the clicks module takes its store handle from it. - queue_question builds the card with a pre-allocated id, holds the question (outbox, then the row) naming it, and only then stores the card. Once a card exists no text is sent. create_card returns stored / not stored / unknown (or no card); stored and unknown keep the card and report that no text was sent. - A definite card failure texts the question to its own destination with the row's own link (the one the row write returned), then holds the question again flagged `redeliver`; a landed row is rewritten now (RoomDbStore.redeliver) and the entry released, and an unrewritten or unconfirmed row takes the record at the next reconcile (Outbox.flush). - The reminder never texts a question whose card is live, --force included; recency is the gateway's notified_at once posted; a card unposted after 30 min gets one text pointer, once per card, with no options. Stand: Echo Act IV Pro Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lt or --option is given (#5947) * feat(pending-questions): ask-owner asks by decision card when --default or --option is given The ask side of decision cards, stacked on the click side (#5946), split out of #5942. - src/hitl/cards.py: one card builder (choice_request, create_choice, manager_for, new_id); report-feedback and the worker pool's wedge cards build through it, and the clicks module takes its store handle from it. - queue_question builds the card with a pre-allocated id, holds the question (outbox, then the row) naming it, and only then stores the card. Once a card exists no text is sent. create_card returns stored / not stored / unknown (or no card); stored and unknown keep the card and report that no text was sent. - A definite card failure texts the question to its own destination with the row's own link (the one the row write returned), then holds the question again flagged `redeliver`; a landed row is rewritten now (RoomDbStore.redeliver) and the entry released, and an unrewritten or unconfirmed row takes the record at the next reconcile (Outbox.flush). - The reminder never texts a question whose card is live, --force included; recency is the gateway's notified_at once posted; a card unposted after 30 min gets one text pointer, once per card, with no options. Stand: Echo Act IV Pro Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(pending-questions): claim the card pointer before publishing it; commit history before releasing a fallback hold - send_card_pointers now claims each pointer first (claim_pointer: under the HITL store lock, re-check the card is live, unposted, past the grace and unclaimed, then stamp pointer_at with the store's fsynced save) and only the claimant publishes. Overlapping reminders queue one pointer; a death after the claim queues none; a card posted before the claim gets none. - The definite-failure fast path now commits store history, then rewrites the landed row, then releases the redeliver hold (the order Outbox.flush keeps). A history that cannot be written keeps the hold, so a later room outage lists the question instead of a measured zero. Stand: Echo Act IV Pro Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(pending-questions): a card pointer is claimed, published, then marked sent; a stale claim is retried The pointer state is split: claim_pointer stamps pointer_claim_at and a token under the HITL store lock, the claimant publishes, then mark_pointer_sent records pointer_sent_at. A claim with no sent mark older than POINTER_CLAIM_RETRY_SEC (5 min) — a claimant that died or failed to publish — is claimed again and retried. The pointer is now at-least-once (a death between the publish and the sent mark can deliver it twice), and SKILL.md says so. Stand: Echo Act IV Pro Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(pending-questions): a pointer is re-checked before it is written, never revives a settled card, and a back-stepped clock cannot steal its claim - send_card_pointers re-checks the card under the store lock just before writing the file (live, unposted, still this claim's) and writes it inside that lock, so a post recorded after the claim cancels the pointer. The window left is a projector send in flight (it records the post only after its send returns): bounded by one send, documented. - mark_pointer_sent refuses a terminal record, so a card settled or archived after the claim is never written back live. - A pointer claim stamped in the future (the clock went back) stays fresh until its window passes, so a second claimant cannot take it. - The card-record reader (card_sent / card_state / card_live) now lives in the parent (#5946); this layer keeps the pointer and sent_at logic. Stand: Echo Act IV Pro Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(pending-questions): a pointer claim stamped ahead of this clock holds one window from first sight Same bound as the click lease: a pointer_claim_at ahead of this clock is never taken on sight (a backward step cannot steal a fresh claim), but the first reminder that sees it records pointer_skew_seen_at and the claim can be retried one POINTER_CLAIM_RETRY_SEC after that, so a clock that ran far ahead no longer suppresses the pointer for the whole skew. Stand: Echo Act IV Pro Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(pending-questions): a pointer claim uses the same elapsed-time lease as a click Replaces pointer_claim_at / pointer_skew_seen_at with the shared lease (pending_questions_lease): a pointer claim is kept until its lease has been credited POINTER_CLAIM_RETRY_SEC across reminder passes, each pass crediting clamp(now - observed_at, 0, POINTER_MAX_STEP_SEC) with observed_at only moving forward; a new claim starts a new lease. No clock step replaces a live claim, and a near- or far-future stamp holds nothing beyond the observed window. Stand: Echo Act IV Pro Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(pending-questions): pin the pointer claim lease under rollback over-credit and a corrected year spike The parent's lease now sets observed_at = now each pass (a corrected forward spike resumes normal time instead of freezing); the pointer claim table pins one step, W-1, a 1 s rollback, the bounded catch-up over-credit, a renewal, a year's spike then correction, and recovery after it. Stand: Echo Act IV Pro Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(pending-questions): every reminder pass credits a pointer claim's lease; grace only gates the replacement claim_pointer returned before crediting an existing claim when the card was still inside its grace, so passes before grace never counted. It now credits and saves the existing lease on every pass first, returns without replacing while still before grace, and mints a replacement only when the lease has expired AND the card is grace-eligible. Also: `clicks --due` is described as writing lease credit (not read-only), and the trailing blank line at the end of src/hitl/cards.py is gone. Stand: Echo Act IV Pro Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(pending-questions): send the unposted-card pointer on every pass, flagless included The 30-minute text pointer for a decision card the gateway never posted ran only under --notify, which is on demand, so an unattended host never sent it. The pointer completes the question's one ask rather than reminding, so it now runs on every pass of src/check-pending-questions.py (the proactive loop's flagless pass included), outside presenter mode; text reminders still need --notify. Docs (CLAUDE.md/AGENTS.md, SKILL.md, proactive-loop, rationale) say so, and the crash-before-hold text is now attributed to the next --notify run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(proactive-loop): keep step 2 under the skill byte cap Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(pending-questions): replace source-text assertions with behaviour tests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
liususan091219
left a comment
There was a problem hiding this comment.
Comment at 83e6602. Both items from our change request at 6c4b365 are fixed and pinned by tests, and no change-request condition fires. This is not an approval yet, for two reasons: the PR is still a draft, and GitHub reports it as conflicting with main (mergeStateStatus DIRTY). Our earlier CHANGES_REQUESTED therefore still stands formally; an approval once it is rebased and out of draft will clear it.
Our items, verified at this head
- Text fallback left a stale "card sent" record: fixed.
card_to_textsends the text to the question's own destination with its link, then re-saves the outbox record.- Revert check: replacing the
Outbox(...).save(...)call incard_to_textwithpassfailstest_a_definite_failure_texts_the_same_destination_with_the_link_and_rewrites_the_recordandtest_an_outbox_that_cannot_be_rewritten_is_reported. - Revert check: sending the fallback to an empty
Destination()with no link failstest_a_definite_failure_texts_the_same_destination_with_the_link_and_rewrites_the_record.
- Revert check: replacing the
- An unposted card was never followed up: fixed. A live card with no post after
CARD_POINTER_GRACE_SEC(30 min) gets one text pointer, recorded aspointer_at.- Revert check: dropping the
pointer_atonce-only guard failstest_two_reminder_passes_send_a_single_pointer_recorded_on_the_card.
- Revert check: dropping the
tests/pending-questions-decision-card.test.py(68 tests) andtests/pending-questions-room-db.test.pypass at this head. CI is green (Windows job skipped);license/clasuccess with 1 context.
Approval state: qingyun-wu approved at this exact head after re-checking each fix by revert, which meets the expert route. The live round trip of a card through the gateway is still owed (the body's "Live path" section says none has run); that is named here, not blocking.
Missing: rebase onto main to clear the conflict, and take it out of draft. Then it is ready to approve.
— Sutando (susans-MacBook-Pro)
Stack
#5910 merged (20f3042). It fixed the HITL click tier check: an owner id at a non-owner tier must not apply a click. origin/main, including #5910, is merged into this branch.
What changed, and why
Before this PR,
scripts/ask-owner.py --default/--optionsent only plain text. The default, the reason and the options were stored on the row and nowhere else. Now a question that has a default or options, and is bound for AG2 Space, is asked as a decision card.One shared card builder,
src/hitl/cards.py.choice_request: one button per action; action ids must be unique;turn_on_actionis on by default; an optional pre-allocatedreq_id.create_choice: stores the card; the store dedupes by guard.manager_for(workspace): the workspace's HITL store.new_id: allocates a card id before the card is stored.report-feedback (
register_askandregister_held) and all four worker-pool wedge cards now build through this helper. Neither constructs aHumanRequirementor aHitlStoreitself any more.The ask is ordered so that no card exists without a durable question.
Buttons: with a default, Approve comes first and carries it, followed by the other options. With options only, the buttons are exactly the options as given, with no Approve. The card stores
{action id: consequence}.Order:
queue_questionbuilds the card with a pre-allocated id and holds the question in the outbox, naming that id. The adapter then writes the row (**Sent:** card hitl_<id> for ag2space owner-dm (decision card) at <time>). Only after that doescreate_cardstore the card for the AG2 Space gateway to post in the owner's DM.Report: the ask reports
stored, not yet shown: decision card hitl_<id>, because storing a card is not showing it.Card only: once a card exists, no text is sent for that question. There is no wait, no withdraw, and no text fallback after a card exists.
Three-way outcome:
create_cardreturnsstored,not storedorunknown.stored.unknown.When text is used: only when the card cannot be built; when the question cannot be held (then no card is stored at all); when the card is provably not stored; and for any other bridge, since only the AG2 Space gateway posts cards.
Definite card failure: the text goes to the question's own destination with its link. Then the outbox entry and the row are rewritten to name that file; the row through
RoomDbStore.redeliver, one body write. The next reminder therefore sees a drained text, not an absent card. If the process dies between the text and the rewrite, the record still names an absent card and the reminder reads the question as due: the owner may get the text twice, never not at all.Known limit: the owner may not be asked again by this skill if the card is never posted. Nothing here can see the DM. The reminder never sends text while the question's card is live (pending or clicked), whether posted or not, nor while the store cannot say. Follow-ups for the card itself belong to the HITL renotify. Once the gateway has posted the card, the question's last-asked time is the gateway's
notified_at.CARD_POINTER_GRACE_SEC) gets one text pointer when the reminder runs. It is sent once per card and recorded durably on the card aspointer_at, so a gateway that stays down is not repeated.Only a card that is gone (never stored, or settled without closing the row) leaves the question due; the reminder then texts it, when it runs. Nothing is scheduled. SKILL.md documents this.
Clicks come back to the row. The bridge owner-checks the click, records it, and forwards the relay task with
hitl_click: true. The core then runspython3 src/pending_questions_reader.py clicks(pq.py clicksdoes the same).closingclaim, apid:noncetoken, is stamped on the card before the row is closed. The row is then closed as Answered in one guarded write that also requires the owner's Status to be open.clicksandackrun under one exclusive lock beside the HITL store (state/hitl/pending-questions-clicks.lock), so two readers never act on the same snapshot. Under that lock, any claim already on a card is replayed, never skipped. A pid can be reused, or unsignalable (EPERM), so a claim naming a live pid must not hide the owner's click.chose <action-id> '<label>' -> <consequence>.ack <ask-id>. Until the ack, the card stays claimed and a re-run replays the choice. A run that dies between the close and its record finds the row closed by code, not by the owner, and replayschose.already answered — not a decisionand its card is cancelled.resolvecancels the question's card whenever it closes the row or the room refuses the close.This is documented in CLAUDE.md/AGENTS.md (Pending decisions) and in the skill's SKILL.md.
Where to look first
src/hitl/cards.pypending_questions_ask.py:queue_question,create_card,card_postedandasked_recentlypending_questions_room_db.py:ask_owner(thecreate_cardcall after the row, and the row rewrite),clickswith its lock and claim token,ack_click, and_close_row(CODE_CLOSED_PREFIX)RoomDbStore.close(require_open=)andRoomDbStore.redeliverUser behavior this proves
Before and after, from one probe script run against an origin/main checkout and against this head. The call is
queue_question("Merge #1?", default_action="merge it", options=[("Hold", "wait")]), with the owner last active on ag2space; at HEAD it is followed bycreate_card:Crash boundaries. The
CrashBoundariestests use a real process death: a subprocess callsos._exit(17)at the injected point. They run against the production stores: the outbox, the HITL store, and the room database through the adapter's own serve over the fake room capability's on-disk state.chose option-1 'Hold' -> wait, not as stale. With the claim check disabled, the same test fails with(False, True, False) != (True, False, True).clicks()processes: both take their snapshot before either closes (a file barrier after the snapshot), and the second one's close lands after the first's. The card staysin_progress, no reader reports the choice as stale, and the choice can still be acked. With the previouspending_questions_room_db.py(6c4b365) the same test fails with'cancelled' != 'in_progress'. Five consecutive runs of all the crash and concurrency tests passed.chose option-1 'Hold'. With the previouspending_questions_room_db.py(5574af1) the same test errors, because the click is skipped and nothing is returned.Reproduced at 2f46f93 before this round, each by a real run:
wait_shownexception queued text while the card stayed pending and projectable.Outbox.saveplus a failingwithdrawqueued text beside a live, posted card.os._exitinOutbox.saveleft a posted card with no outbox entry, no row and no text.os._exitafter the row close made the real click read as stale on restart.Items 1 and 2 no longer have a code path:
wait_shown,_require_shownand_card_to_textare gone, and a test asserts it. Item 3 is fixed by the ordering, item 4 by the claim.Feature/documentation consistency
docs/catalog.jsonhas no canonical document for pending questions. The behaviour is documented inskills/pending-questions/SKILL.md("Decision cards"), in CLAUDE.md/AGENTS.md (Pending decisions), and in thescripts/ask-owner.pydocstring.docs/src-map.mdwas regenerated.Tests run (all pass at HEAD 83e6602)
python3 tests/<file>for each of these:pending-questions-decision-card: 68 tests, OK. This includes:notified_at;pending-questions-room-db: 51 tests, OK. Its source-order pin now checks "text: queue, then hold" and "card: hold, then store the card".hermetic-env-structural: 27 tests, OK.pending-questions-*suite,tests/skills/pending-questions/pq.test.py, everyreport-feedback*,pool-wedge-*andhitl-*suite,agent-api-pending-questions,check-pending-questions-collapse,friction-pending-questions-collapse,src-map,ag2-sparrow-hitl-vendored, andsparrow-hitl-projection: all pass, after merging origin/main.Changed-line coverage was measured locally with coverage.py over those suites, against the merge base with main. Before the final pointer refinement it was 99.5% (380 of 382 changed executable lines). The once-per-card test added since then also runs the remaining two lines, the default-store branch of
send_card_pointers. The only file not measured isscripts/ask-owner.py, whose change is docstring-only.scripts/review-checks.shpasses, andagents-md-sync.sh --checkandgen-src-map.py --checkare up to date.Edge cases and limits
Live path
No live round trip has been run; a restart for it needs the owner's word. The bridge is untouched, and the card goes through the existing
_project_hitlandHitlReplyHandlerpath. A witness run would be:ask-owner.py --optionfrom an AG2 Space owner-DM task;src/pending_questions_reader.py clicks, thenack.🤖 Generated with Claude Code