feat(commands): semantic answer_decision action + meta.decision in the pending payload#6
Conversation
|
Heads-up on the red CI: it wasn't this branch. Pushed a fix on this branch: the benchmark now derives the expected limit from the paired Herdres checkout ( You'll probably want that one commit on |
plotarmordev
left a comment
There was a problem hiding this comment.
I reviewed this together with the explicitly paired Herdres PR
luminexord/herdres#152. The focused Tendwire suites pass, and the complete
paired Herdres suite passes against this branch, but the following behavioral
gaps remain merge blockers because the current tests do not exercise them.
Required changes
1. Do not map a live competing claim to decision_not_pending
claim_backend_pending_decision() correctly distinguishes
already_claimed, but _validate_pending_decision() and
_claim_pending_decision() collapse every unmapped status to
decision_not_pending.
I reproduced this sequence:
claim request A: claimed
submit request B for the exact same decision: decision_not_pending / no_receipt
same decision_ref still present in pending.list: true
pane calls: 0
That response is factually incorrect. The paired Herdres handler interprets
decision_not_pending as authoritative stale state and retracts the Telegram
keyboard. If request A then fails or becomes uncertain, the user has lost the
active control even though the decision remains pending. The same collapse can
also interact badly with abandoned-reservation recovery near the independent
receipt/claim lease boundaries.
Required correction:
- Preserve
already_claimedas a distinct non-stale outcome. - Return a retryable/in-progress semantic that does not tell connectors the
decision disappeared and does not cause a second pane mutation. - Do not terminalize an abandoned reserved request merely because another
still-live claim exists. - Update the producer disposition matrix and paired Herdres validator together
if a new public status is introduced.
Required regressions:
- Two different request IDs race for one decision; exactly one claim can send.
- The second request never returns
decision_not_pendingwhile the exact
decision remains public. - Cover first claimant success, safe pre-send failure, send uncertainty,
process loss beforesend_started, and lease expiry/takeover. - Prove the paired Telegram keyboard is not retracted for the competing-claim
outcome.
2. Make the advertised multi contract real or remove it from this release
The paired production adapter currently emits multi-select and multi-question
AskUserQuestion prompts as pending_interaction, not pending_decision.
Running that real adapter output through this branch produced:
single AskUserQuestion -> open_prompt / decision_kind=single
multi AskUserQuestion -> read_succeeded_invalid_prompt / no decision
The new multi tests construct a synthetic top-level pending_decision shape
that the paired producer does not emit. Consequently, the relative-cursor
Down/Enter calibration is not exercised by the real cross-repository path.
Choose one bounded correction:
- Scope this release to the production-proven
singleandplankinds and
rejectmultifail-closed throughout Tendwire and Herdres; or - Change the paired adapter/parser contract so a production-shaped multi
decision reaches Tendwire, then prove its exact key calibration with a real
paired fixture. The test must begin with the adapter's actual tool input, not
a hand-built Tendwire-only object.
Do not retain an unreachable public multi promise for future use.
3. Reject option truncation before deriving key ordinals
_pending_observation_from_turn() silently keeps only the first 12 choices,
then the write-in calibration derives option_count + 1 from that shortened
list. A reproduced input containing 14 ordinary options plus a custom row at
ordinal 15 was accepted as a 12-option decision and generated key ordinal 13.
That can select the wrong row.
The currently paired adapter happens to cap its generated list, but this
boundary is private/untrusted input and must fail closed rather than silently
changing terminal semantics.
Required correction and proof:
- Reject a decision whose source option count exceeds the supported bound, or
retain and validate the exact source ordinal separately from displayed
options. - Add boundary tests at the maximum, maximum + 1, and with the custom row just
beyond the retained boundary. - Prove no pane operation occurs for an unsupported/truncated shape.
4. Complete the paired public contract gate
The linked Herdres response validator currently accepts malformed bare
responses such as {"ok": true, "status": "accepted"}. That is tracked on
luminexord/herdres#152, but this PR must not be merged independently because
the feature is unsafe until the consumer requires the exact schema-v2 envelope,
correlation fields, disposition, and decision result.
Before re-review, please also document the new meta.decision payload and
answer_decision request/response contract, including supported kinds,
fail-closed behavior, dry-run default, retry dispositions, and the paired
version requirement.
Re-review gate
- Focused decision, pending, command-replay, pre-send, CLI/daemon, and public
safety suites. - Deterministic concurrency and response-loss tests described above.
- Production-shaped adapter-to-Tendwire-to-Herdres fixtures for every supported
kind. - Complete Tendwire and paired Herdres suites, compilation, and diff checks.
- No merge or deployment of either half until both reviews are clean.
Surface a blocked Claude pane's AskUserQuestion / ExitPlanMode prompt into the
connector's topic as a native reply keyboard, and answer it remotely — WITHOUT
the connector ever touching Herdr, and WITHOUT it encoding any key sequence.
The connector renders the prompt Tendwire carries in its pending payload
(meta.decision), maps a tapped/typed answer to a SEMANTIC selection, and submits
a neutral `answer_decision` command back to Tendwire:
{action: "answer_decision", target: {worker_id},
params: {decision_ref, selection: {option_refs:[...]} | {text}}}
Tendwire owns all Herdr access: it resolves worker_id -> pane, confirms the
prompt is still displayed (freshness), owns the per-Claude-Code TUI calibration,
and replays it. This matches the tendwire answer_decision contract in
plotarmordev/tendwire#6. Both neutrality guard tests pass unchanged, and the
connector no longer contains any calibration.
Rebased onto the 0.7.0rc train and reworked per review (luminexord#150):
- Contract altitude (was raw send_keys steps): decisions.py is now a pure state
machine that emits a semantic selection; all TUI calibration moved to Tendwire.
- Request identity: the answer reuses the inbound message's HMAC-derived
request_id and the durable ingress-command path (tendwire_client +
ingress_requests both gain a validated `answer_decision` action), so it is
idempotent and can no longer be rejected by validate_request_id.
- False success: a rejected submit (incl. Tendwire's typed decision_not_pending /
invalid_selection / unknown_worker / unsupported_decision statuses) keeps the
decision active + keyboard and replies with an explicit failure; only a
terminal_accepted disposition confirms.
- Freshness: the answer path re-polls pending and refuses to submit for a stale
decision_ref (already answered / superseded); Tendwire fails closed too.
- Multi-question AskUserQuestion (question_count > 1) fails closed to the
read-only attention notice.
- Duplicate option labels are disambiguated so a reply-keyboard tap resolves to
exactly one option ref; rc-train drops Telegram callbacks, so reply keyboards.
- /send keeps its queue-for-turn-boundary semantics and falls through instead of
being hijacked into the decision wizard.
- Sync round-trip race: the delivery loop reloads the decisions bucket under the
held lock before its save, and exposes a public active_items() iterator.
- HERDRES_REMOTE_DECISIONS moved to config.py with the standard flag idiom (empty
value keeps the default) and documented in .env.example.
- tests/test_remote_decisions.py rewritten for the semantic contract: selection
shapes, gateway submit success/failure semantics, freshness/staleness,
multi-question fail-closed, /send fall-through, duplicate-label disambiguation,
and rejection-status recognition.
Pairs with plotarmordev/tendwire#6 (semantic answer_decision + meta.decision).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ey maps Probed key-by-key on a real Claude Code 2.1.211 pane: - single/plan: the digit alone selects AND submits — the previous trailing Enter would leak into whatever UI appears next (composer or a modal). - write-in: the Type-something row ignores digits; it is reached with Down x N and the focused row is itself the text input. - multi-select: digits toggle rows ABSOLUTELY (cursor-independent), Right reaches the Submit tab, Enter submits — replacing the fragile relative Down/Up + Space navigation entirely. - option bound tightened 11 -> 9: every driven ordinal must stay a single keystroke; larger decisions fail closed to the read-only interaction. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
8453ea5 to
bd6082c
Compare
|
Round 2 pushed — all four blockers addressed. Ready for re-review. 1. Competing claims: 2. Multi is now real — and the calibration is live-verified, not assumed. The paired herdres adapter (companion PR luminexord/herdres#153) emits production-shaped single-question multi-select decisions; the end-to-end fixture here starts from the actual AskUserQuestion
3. Truncation: 4. Contract: the herdres validator now requires the exact schema-v2 envelope (correlation echoes, dispositions, per-action result shape — see #153); Paired verification: tendwire 2752 passed (+ sidecar benchmark 7/7 against the herdres branch); herdres 673 passed against this branch. CI should be green — the branch also still carries the paired-benchmark page-size fix. |
plotarmordev
left a comment
There was a problem hiding this comment.
Accepted after paired correction. The branch now preserves retryable competing claims, supports production multi-select, binds decision handles to the exact private source, accepts nine selectable single-choice rows plus the trailing write-in row without exposing a two-digit ordinal, fails closed above that bound, and documents the implemented limit. Verified locally against current main: 2,763 passed, 1 skipped; paired Herdres suite: 677 passed.
Implements the validated alternative to #5's raw
send_keys, per the contract discussion there: Tendwire — the only party that can observe the pane — owns both freshness validation and key calibration, and connectors speak in intent, not keystrokes.What
meta.decisionon pending interactions:{decision_ref, kind: single|multi|plan, prompt, options: [{ref, label}], multi_select, question_count}, strictly normalized from the ingested backend-pending records;decision_refrotates whenever the prompt changes.command.submitactionanswer_decision:{decision_ref, selection: {option_refs} | {text}}, plugged into the full existing mutation machinery — canonical mutations, selector proofs, request-id dedup/replay, dry-run, and the claim/reservation/uncertainty pipeline.decision_refis the worker's current pending decision, typed failures (decision_not_pending,unknown_worker,invalid_selection,unsupported_decision), multi-question prompts refused, write-ins only forsingle.backends/herdr_decision.pywith every TUI assumption documented; no public rawsend_keysaction is exposed.dry_runkeeps the fail-safe default (omitted ⇒ dry-run) for this action like every other mutation; connectors send an explicitdry_run: false.Verification
TENDWIRE_BENCHMARK_HERDRES_ROOT; with it, one environment-dependent case fails identically onmainin our clean checkout, so it is not introduced here).answer_decisionand fail closed on the send path; 656 passed on the herdres side.Authored by a gpt-5.6-sol implementation agent from a coordinator spec; coordinator-reviewed. Happy to adjust the contract shape — the herdres half tracks this PR.
🤖 Generated with Claude Code