From 7c079f98859e853eea9103cb5ef99b3e7f94a3f0 Mon Sep 17 00:00:00 2001 From: falkoro <39274208+falkoro@users.noreply.github.com> Date: Wed, 7 Oct 2026 19:36:59 +0200 Subject: [PATCH 1/2] fix(bin): resolve a pending reply only from its own task's status line Remote reply ingestion handed every corr= token in a mate's payload to fm_pending_reply_try_resolve together with that mate's own status log, so one mate echoing another mate's token resolved the other request. Honor a status-file override only when it is the record's own parent_status, and match the corr= token as a whole word. Fixes #6538 --- bin/fm-pending-reply-lib.sh | 25 +++++++++++++++---------- tests/fm-pending-reply.test.sh | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 10 deletions(-) diff --git a/bin/fm-pending-reply-lib.sh b/bin/fm-pending-reply-lib.sh index dc5b96a96f7..ce189b4cb50 100755 --- a/bin/fm-pending-reply-lib.sh +++ b/bin/fm-pending-reply-lib.sh @@ -5,8 +5,9 @@ # to a secondmate, this library records a durable parent-owned pending-reply # expectation BEFORE delivery, embeds a privacy-safe correlation id in the # outbound message, and later resolves that expectation only from a correlated -# parent status line or status-pointed document - never from transport success, -# chat content, or unrelated status activity. +# line in the asked task's own parent status log, or a document it points to - +# never from transport success, chat content, unrelated status activity, or +# another task's line that echoes or quotes the token. # # Safety property (captain direction 2026-07-22): a secondmate agent may ignore # the marker and answer only in its visible conversation. The parent must notice @@ -194,14 +195,12 @@ fm_pending_reply_extract_corr() { # printf '%s' "$text" | grep -oE "$FM_PENDING_REPLY_CORR_RE" 2>/dev/null | head -1 | cut -d= -f2- | tr 'A-F' 'a-f' || true } -# 0 if carries the exact correlation token for . +# 0 if carries the exact correlation token for , as a whole +# word: xcorr= or corr=ff is a different token, not this one. fm_pending_reply_text_has_corr() { # - local text=$1 corr=$2 token - token=$(fm_pending_reply_corr_token "$corr") - case "$text" in - *"$token"*) return 0 ;; - esac - return 1 + local text=$1 corr=$2 re + re="(^|[^[:alnum:]_])$(fm_pending_reply_corr_token "$corr")([^[:alnum:]_]|\$)" + [[ $text =~ $re ]] } # Sanitize a short request summary: single line, bounded, no control chars. @@ -689,7 +688,13 @@ _fm_pending_reply_try_resolve_locked() { # [status-file-o case "$delivery_state" in attempted|confirmed) ;; *) return 1 ;; esac unconfirmed=1 fi - status_file=${status_override:-$(fm_pending_reply_get "$rec" parent_status)} + status_file=$(fm_pending_reply_get "$rec" parent_status) + # Only the asked task's own status log answers its request: another mate's + # line echoing or quoting this corr= token must leave the request open. + if [ -n "$status_override" ]; then + [ "$status_override" -ef "$status_file" ] || return 1 + status_file=$status_override + fi if [ -z "$status_override" ] && [ "$unconfirmed" = 0 ]; then signature=$(fm_pending_reply_file_signature "$status_file") previous=$(fm_pending_reply_get "$rec" parent_status_scan_signature) diff --git a/tests/fm-pending-reply.test.sh b/tests/fm-pending-reply.test.sh index 697ec00796f..7410b2d8432 100755 --- a/tests/fm-pending-reply.test.sh +++ b/tests/fm-pending-reply.test.sh @@ -13,6 +13,8 @@ # 4. Second missed turn escalates once and remains durable # 5. Transport success cannot masquerade as reply success # 6. Unrelated events and stale correlation ids cannot resolve a request +# - including another mate's line that echoes the request's token, or a +# token embedded in a longer word # 7. Restart/compaction preserves the expectation and exact parent destination # 8. Wrong-home reports are detected but do not silently acknowledge # 9. Direct unmarked captain input creates no expectation @@ -1067,6 +1069,37 @@ test_unrelated_and_stale_corr_cannot_resolve() { pass "unrelated events and stale correlation ids cannot resolve" } +test_another_mates_echo_cannot_resolve() { + local home state corr_a corr_b + home=$(setup_parent two-mates) + state="$home/state" + # shellcheck disable=SC2031 + export FM_PENDING_REPLY_NOW=6100 + corr_a=$(fm_pending_reply_create "$home" "$state" "alpha" "need alpha's answer") + corr_b=$(fm_pending_reply_create "$home" "$state" "beta" "need beta's answer") + fm_pending_reply_mark_delivered "$state" "$corr_a" + fm_pending_reply_mark_delivered "$state" "$corr_b" + # Beta's reply echoes alpha's token; remote reply ingest hands every corr= in + # beta's payload over together with beta's own status log. + printf 'done [corr=%s]: answered, and alpha still owes corr=%s\n' "$corr_b" "$corr_a" \ + > "$state/beta.status" + if fm_pending_reply_try_resolve "$state" "$corr_a" "$state/beta.status"; then + fail "another mate's line echoing the token must not resolve the request" + fi + [ "$(phase_of "$state" "$corr_a")" = awaiting_report ] || fail "alpha's request must stay open" + fm_pending_reply_try_resolve "$state" "$corr_b" "$state/beta.status" \ + || fail "beta's own correlated line should resolve beta's request" + printf 'done corr=%sff: a longer token is a different token\n' "$corr_a" > "$state/alpha.status" + printf 'done xcorr=%s: so is a prefixed one\n' "$corr_a" >> "$state/alpha.status" + if fm_pending_reply_try_resolve "$state" "$corr_a"; then + fail "a token embedded in a longer word must not resolve" + fi + printf 'done [corr=%s]: alpha answered\n' "$corr_a" >> "$state/alpha.status" + fm_pending_reply_try_resolve "$state" "$corr_a" "$state/alpha.status" \ + || fail "alpha's own correlated line should resolve alpha's request" + pass "another mate's echoed token cannot resolve a request" +} + test_restart_preserves_expectation_and_parent_destination() { local home state corr rec parent_status parent_home home=$(setup_parent restart) @@ -2093,6 +2126,7 @@ test_undelivered_records_are_scan_immutable test_delivery_confirmation_fallback_reconciles test_delivery_confirmation_serializes_with_reconciliation test_unrelated_and_stale_corr_cannot_resolve +test_another_mates_echo_cannot_resolve test_restart_preserves_expectation_and_parent_destination test_wrong_home_detected_not_acknowledged test_unmarked_captain_input_creates_no_expectation From 035f4832058b06e3ead39d4fc8702b0790e56810 Mon Sep 17 00:00:00 2001 From: falkoro <39274208+falkoro@users.noreply.github.com> Date: Wed, 7 Oct 2026 19:39:19 +0200 Subject: [PATCH 2/2] no-mistakes(document): docs: scope remote reply settlement to the asked mate --- docs/remote-secondmates.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/remote-secondmates.md b/docs/remote-secondmates.md index 980fbceaf8c..ee60a26903b 100644 --- a/docs/remote-secondmates.md +++ b/docs/remote-secondmates.md @@ -574,7 +574,9 @@ An SSH exit status of 255 while fetching a referenced document leaves the delta The process-event runner applies each captured delta through this adapter as soon as it is captured. So a mirrored reply reaches the primary status channel without depending on the wake handler running the adapter itself. -A mirrored line that carries a correlation token settles its pending-reply record and closes that request's own open escalation decision. +A mirrored line that carries a correlation token settles the pending-reply record only when the mate it came from is the task that request was sent to. +Another mate echoing the token leaves it open. +A settled record also closes that request's own open escalation decision. A remote reply reaches the primary only through this asynchronous mirror. Because of that, the primary treats a missing correlated report as a missed report only once the mirror has been read through the end of the remote log after that turn ended.