Skip to content

fix(bin): reconcile and escalate unread remote secondmate requests - #7369

Open
Moshik21 wants to merge 17 commits into
kunchenguid:mainfrom
Moshik21:fm/fm-upstream-pr-transport
Open

Moshik21 wants to merge 17 commits into
kunchenguid:mainfrom
Moshik21:fm/fm-upstream-pr-transport

Conversation

@Moshik21

@Moshik21 Moshik21 commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Intent

Reconcile and escalate unread remote secondmate requests: the parent's reply tracker reads the remote inbox and escalates an unread request once, a parked secondmate is woken on unread parent instructions, failed inbox reads are bounded, unconfirmed resolve-key answers stay on the printed resend, and a bare Herdr paste placeholder is accepted as the doorbell only after a recent typed ring. The branch was brought up to date by merging current main; the earlier tenth-shard CI commit is not part of the net diff because change 7838 already adds that shard.

What Changed

  • The parent's pending-reply tracker (bin/fm-pending-reply-lib.sh) now reads a remote secondmate's inbox through a new read-only inbox-status leg in bin/fm-remote-secondmate-control.sh. A request still unhandled after FM_PENDING_REPLY_UNREAD_SECS escalates once as pending-reply-unread (added to bin/fm-classify-lib.sh), and an idle mate with an unread record no longer counts as having taken a turn. Delivery-unknown remote records are reconciled against the inbox: landed records are marked delivered, and recent absent ones get one same-correlation resend using the request text that bin/fm-send.sh now stores. --resolve-key answers store no text and keep to the printed resend. Inbox reads are bounded by a read budget and a short retry spacing after a failed read. The new tunables are documented in docs/configuration.md and docs/remote-secondmates.md.
  • A secondmate's own watcher (parent_inbox_tick in bin/fm-watch.sh) queues one check: parent instruction waiting wake per oldest record once a parent-route inbox record is older than FM_TASK_INBOX_GRACE_SECS. A parked mate therefore reads parent instructions without depending on a typed doorbell.
  • bin/fm-task-inbox-lib.sh appends every ring decision to a bounded per-inbox .ring-log. It also accepts a bare Herdr [Pasted text #N] composer placeholder as the mate's own doorbell, but only when the .ring-log shows a recent earlier typed ring of that record. Tests are added or extended in tests/fm-pending-reply.test.sh, tests/fm-send-remote-delivery.test.sh and tests/fm-task-inbox.test.sh, and the serial-shard weights in bin/fm-test-run.sh are updated for them.
  • The branch has current main merged in. It carries no CI workflow change and no tenth serial shard, because main already has that shard from change 7838.

Risk Assessment

⚠️ Medium: I found no defects, and the merged tree meets the stated intent: no CI workflow change, and the serial hint numbers are the only changes in bin/fm-test-run.sh. The change is still large and touches shared tracker, ring and watcher state machines, so it carries moderate risk. I traced these paths by reading the code and did not run the tests.

Testing

I ran three test suites that execute the real scripts against fixtures. tests/fm-pending-reply.test.sh passed. tests/fm-task-inbox.test.sh passed 52/52 once GNU touch was on PATH, because a branch test uses touch -d. Both runs used fixtures, not a live Herdr or remote host, so they are not recorded as live. tests/fm-send-remote-delivery.test.sh failed at its first test, test_remote_large_steer_uses_stdin, which is main's test and not this branch's. The same failure happens at base commit 05196e3, so it predates this change. The cause is that fm_run_external_timeout in bin/fm-timeout-lib.sh backgrounds the command when timeout is on PATH, so the steer's stdin becomes /dev/null and send-stdin rejects the message as empty. This matches upstream issue 7968. With the perl timeout fallback the same test timed out at 30s, and bash-timeout mode failed as well. Docker is not running, so I couldn't try Linux. The branch's roughly 20 new remote reconcile tests did not run here. I confirmed live that the inbox-status usage line is present with the header lines 2 to 25 intact, that the merged tree has no .github change, and that the only change to bin/fm-test-run.sh is the three serial-test hint numbers. Temp copies and shims are removed and the worktree is clean.

  • Live validation: ⚠️ inconclusive - 1 of 4 scenarios driven live against the product
Scenario Result Live Evidence
Pending-reply tracker: an unread remote request is escalated once, a handled one counts as a turn, and the inbox-status schema and states are correct ⏸️ untested no The prior payload recorded this as live=false. The tests ran the real library against fixtures, not a real remote host, so no live pass was established. Driving it live needs a real Herdr session and…
Task inbox: a secondmate's own watcher wakes its session once per oldest unhandled record, the ring log records decisions, and a bare Herdr paste placeholder is submitted only after a recent typed rin… ⏸️ untested no The prior payload recorded this as live=false. The tests ran the real scripts against fixtures (the plain macOS run failed only on GNU-only touch -d, so GNU touch was shimmed). No live pass was esta…
Remote-delivery suite: unlanded-record resend once, byte-exact resend, spacing/text/age limits, unconfirmed resolve-key left to the printed resend, bounded failed inbox reads ⏸️ untested no The suite cannot start on this macOS host. A remote steer's stdin is dropped when timeout is on PATH (root cause: fm_run_external_timeout in bin/fm-timeout-lib.sh backgrounds the command), so the…
Merge hygiene: the merged tree has no CI workflow change and the remote-control usage text is the header lines 2 to 25, including inbox-status ✅ pass live remote-control-usage.txt; git diff 05196e30 HEAD -- .github is empty; bin/fm-test-run.sh differs only in the three serial-test hint numbers
Evidence: pending-reply test log

Source: pending-reply test log

ok - normal correlated reply resolves once (idempotent)
ok - completed turn with no report triggers exactly one recovery
ok - recovery never pokes a mate waiting on its own decision, and runs once it closes
ok - recovery sends during an open decision when config/wait-no-turns is absent
ok - recovery grace is measured from the request turn's completion, not delivery
ok - one fresh status read immediately before firing catches a just-landed reply, any verb
ok - a resolve that fails after committing resolved still blocks repost and escalation
ok - missed-report escalation grace is measured from the recovery turn's completion
ok - recovery attempts reconcile without reinjection
ok - recovery reply resolves the original expectation
ok - second missed turn escalates once and remains durable
ok - escalations and replies wake; the home's own escalation close stays quiet
ok - failed escalation publication remains retryable and publishes once
ok - legacy escalation closes under the shared default key
ok - legacy escalation cannot close an unrelated default-key decision
ok - foreign correlated blocker cannot impersonate a pending-reply escalation
ok - concurrent resolution closes one keyed escalation exactly once
ok - concurrent escalation yields to a late correlated reply
ok - transport success cannot masquerade as reply success
ok - undelivered records remain immutable across scan paths
ok - delivery confirmation fallback reconciles durably
ok - delivery confirmation serializes with reconciliation
ok - unrelated events and stale correlation ids cannot resolve
ok - another mate's echoed token cannot resolve a request
ok - restart preserves expectation and exact parent destination
ok - wrong-home reports are detected but do not silently acknowledge
ok - direct unmarked captain input creates no expectation
ok - fm-send marked secondmate path creates pending and embeds corr
ok - status-pointed document resolves the expectation
ok - optional helper report resolves without being required for correctness
ok - backend busy/idle observation covers Pi/Claude paths without conversation scrape
ok - tmux and zellij unknown states use bounded capture fallback
ok - pending replies scope Kimi capture fallback by recorded harness
ok - tick skips terminal records and reuses target observations
ok - the tick leaves settled records alone and still does the selected records' work
ok - correlations are reused only for matching open task records
ok - tick end-to-end: miss -> one recovery -> escalate -> durable
ok - failed transport discards undelivered expectation only
ok - a remote repost waits for the reply channel and still fires on a real miss
ok - a mirrored correlated remote reply resolves without any repost
ok - same-basename self-home corr= is restated onto the parent channel and resolves
ok - same-basename reply resolves at the recovery failure boundary
ok - a child-file mate-home sighting is not copied and still escalates
ok - mechanical helper writes the parent channel from verb, corr, and note
ok - remote parent-replies.status is not classified as wrong-home
ok - local parent-replies.status remains wrong-home evidence
ok - an escalated correlation stays retryable only while undelivered
ok - a same-kind escalation after an operator close opens the decision again
ok - the summary keeps accented letters, drops control characters, and caps by character
ok - inbox status: schema, unhandled count and age, and the unhandled/handled/absent correlation states
ok - a local mate's unread request raises no tracker escalation and a handled one counts as a turn
ok - all pending-reply tests passed
bash tests/$t.test.sh  41.58s user 95.46s system 58% cpu 3:55.17 total
Evidence: task-inbox test log with GNU touch (52 ok, no failures)

Source: task-inbox test log with GNU touch (52 ok, no failures)

ok - inbox: a steer is written durably and round-trips byte-exact with a self-describing doorbell
ok - inbox: a hostile-name doorbell executes as a no-op in bare shells
ok - inbox: terminal-control inbox names are rejected without typing
ok - inbox: the ring skips dead or missing endpoints and still rings live or unclassifiable endpoints
ok - inbox: the ring submits its own stuck doorbell, skips other pending text, and retries a lost Enter once on both paths
ok - inbox: the idempotent enqueue dedups an exact re-run onto the same record, handled or not
ok - inbox: idempotent enqueue follows a record concurrently moved to handled
ok - inbox: the handled mv is the idempotent ack and sequences are never reissued
ok - inbox: concurrent writers serialize on the sequence lock and lose nothing
ok - inbox: a writer retries when a competing lock vanishes after its failed claim
ok - inbox: ladder bookkeeping ignores a concurrently removed inbox
ok - inbox: fire-and-forget records stay durable and outside the ladder
ok - inbox: a fire-and-forget record whose ring did not land is owed exactly one retry
ok - inbox: without config/wait-no-turns a fire-and-forget retry mark stays quiet
ok - inbox: the re-ring ladder paces by grace, escalates once, and resets on ack
ok - watcher: an unhandled aged message on an idle pane re-rings without waking firstmate, and the ack silences it
ok - watcher: busy deferrals survive restart, escalate once at the bound, and never type
ok - watcher: delivery and acknowledgement reset the durable busy budget
ok - watcher: unwritable busy bookkeeping surfaces without typing
ok - watcher: idle successor surfaces once despite unremovable predecessor bookkeeping
ok - watcher: busy successor surfaces once despite unremovable predecessor bookkeeping
ok - watcher: persistent idle busy-reset failure escalates once and later instructions still deliver
ok - watcher: persistent protected busy-reset failure escalates once and later instructions still deliver
ok - watcher: busy limit '999999999999999999999999999999' escalates exactly once at 2
ok - watcher: busy limit '1000000000' escalates exactly once at 2
ok - watcher: busy limit '0' escalates exactly once at 2
ok - watcher: busy limit '000' escalates exactly once at 2
ok - watcher: busy limit '' escalates exactly once at 2
ok - watcher: busy limit 'invalid' escalates exactly once at 2
ok - watcher: busy limit '1' escalates exactly once at 1
ok - watcher: busy limit '3' escalates exactly once at 3
ok - watcher: unremovable obsolete busy state does not block or escalate a fire-and-forget retry
ok - watcher: busy successor escalates once despite stale predecessor ring history
ok - watcher: dead successor escalates once despite stale predecessor ring history
ok - watcher: missing successor escalates once despite stale predecessor ring history
ok - watcher: unwritable successor escalates once despite stale predecessor ring history
ok - watcher: a non-busy protected delivery breaks the busy streak and preserves the attempt ladder
ok - watcher: a non-busy failed delivery breaks the busy streak and preserves the attempt ladder
ok - watcher: a healthy or empty inbox stays completely silent
ok - watcher: acknowledgement silences an unwritable ladder without a stale wake
ok - watcher: unwritable ladder bookkeeping surfaces a stale wake after the doorbell
ok - watcher: a fire-and-forget record's owed retry rings exactly once and never escalates
ok - watcher: a fire-and-forget retry waits out the worker's own decision, then rings once
ok - watcher: spending a retry keeps a newer record's mark written during its ring
ok - watcher: a spent ring budget emits exactly one ordinary stale wake for recovery
ok - watcher: a positively dead pane is never typed into and surfaces exactly one stale wake
ok - watcher: dead-pane recovery overrides stale busy state
ok - inbox: the ring log records each ring decision and trims itself past 64 KiB
ok - inbox: the ring log records a dead agent's refusal
ok - watcher: a secondmate's own watcher wakes its session once per oldest unhandled parent record past the grace
ok - watcher: a home without a secondmate identity ignores a parent-route inbox
ok - inbox: a Herdr bare paste placeholder is submitted only after a recent ring of the record typed the doorbell
Evidence: task-inbox plain macOS log (fails on touch -d)

Source: task-inbox plain macOS log (fails on touch -d)

ok - inbox: a steer is written durably and round-trips byte-exact with a self-describing doorbell
ok - inbox: a hostile-name doorbell executes as a no-op in bare shells
ok - inbox: terminal-control inbox names are rejected without typing
ok - inbox: the ring skips dead or missing endpoints and still rings live or unclassifiable endpoints
ok - inbox: the ring submits its own stuck doorbell, skips other pending text, and retries a lost Enter once on both paths
ok - inbox: the idempotent enqueue dedups an exact re-run onto the same record, handled or not
ok - inbox: idempotent enqueue follows a record concurrently moved to handled
ok - inbox: the handled mv is the idempotent ack and sequences are never reissued
ok - inbox: concurrent writers serialize on the sequence lock and lose nothing
ok - inbox: a writer retries when a competing lock vanishes after its failed claim
ok - inbox: ladder bookkeeping ignores a concurrently removed inbox
ok - inbox: fire-and-forget records stay durable and outside the ladder
ok - inbox: a fire-and-forget record whose ring did not land is owed exactly one retry
ok - inbox: without config/wait-no-turns a fire-and-forget retry mark stays quiet
ok - inbox: the re-ring ladder paces by grace, escalates once, and resets on ack
ok - watcher: an unhandled aged message on an idle pane re-rings without waking firstmate, and the ack silences it
ok - watcher: busy deferrals survive restart, escalate once at the bound, and never type
ok - watcher: delivery and acknowledgement reset the durable busy budget
ok - watcher: unwritable busy bookkeeping surfaces without typing
ok - watcher: idle successor surfaces once despite unremovable predecessor bookkeeping
ok - watcher: busy successor surfaces once despite unremovable predecessor bookkeeping
ok - watcher: persistent idle busy-reset failure escalates once and later instructions still deliver
ok - watcher: persistent protected busy-reset failure escalates once and later instructions still deliver
ok - watcher: busy limit '999999999999999999999999999999' escalates exactly once at 2
ok - watcher: busy limit '1000000000' escalates exactly once at 2
ok - watcher: busy limit '0' escalates exactly once at 2
ok - watcher: busy limit '000' escalates exactly once at 2
ok - watcher: busy limit '' escalates exactly once at 2
ok - watcher: busy limit 'invalid' escalates exactly once at 2
ok - watcher: busy limit '1' escalates exactly once at 1
ok - watcher: busy limit '3' escalates exactly once at 3
ok - watcher: unremovable obsolete busy state does not block or escalate a fire-and-forget retry
ok - watcher: busy successor escalates once despite stale predecessor ring history
ok - watcher: dead successor escalates once despite stale predecessor ring history
ok - watcher: missing successor escalates once despite stale predecessor ring history
ok - watcher: unwritable successor escalates once despite stale predecessor ring history
ok - watcher: a non-busy protected delivery breaks the busy streak and preserves the attempt ladder
ok - watcher: a non-busy failed delivery breaks the busy streak and preserves the attempt ladder
ok - watcher: a healthy or empty inbox stays completely silent
ok - watcher: acknowledgement silences an unwritable ladder without a stale wake
ok - watcher: unwritable ladder bookkeeping surfaces a stale wake after the doorbell
ok - watcher: a fire-and-forget record's owed retry rings exactly once and never escalates
ok - watcher: a fire-and-forget retry waits out the worker's own decision, then rings once
ok - watcher: spending a retry keeps a newer record's mark written during its ring
ok - watcher: a spent ring budget emits exactly one ordinary stale wake for recovery
ok - watcher: a positively dead pane is never typed into and surfaces exactly one stale wake
ok - watcher: dead-pane recovery overrides stale busy state
ok - inbox: the ring log records each ring decision and trims itself past 64 KiB
ok - inbox: the ring log records a dead agent's refusal
touch: out of range or illegal time specification: YYYY-MM-DDThh:mm:SS[.frac][tz]
not ok - an aged record must wake the mate once: 
bash tests/$t.test.sh  46.06s user 71.76s system 41% cpu 4:46.30 total
Evidence: remote-delivery log on HEAD (fails at the first test)

Source: remote-delivery log on HEAD (fails at the first test)

not ok - a large remote steer must be delivered through the remote stdin channel: ●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
●  WATCHER DOWN - SUPERVISION IS OFF
●  1 task(s) in flight, but no watcher has a fresh beacon (last beat: never, grace 300s).
●  Trust the emitted supervision protocol for this harness; do not use shell & for watcher repair.
●  This is a supervision warning only; the requested message WILL still be sent.
●  watcher supervision needs Stop-owned automatic recovery; inspect the hook registration and startup status before ending the turn.
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
error: remote steer message must be nonempty
error: steer not sent to remote secondmate rsm (the remote steering-inbox record could not be written; the remote leg's stderr above has the reason): expected exit 0, got 1
bash tests/fm-send-remote-delivery.test.sh  6.39s user 1.63s system 79% cpu 10.056 total
Evidence: remote-delivery log at base 05196e3 (identical failure)

Source: remote-delivery log at base 05196e30 (identical failure)

not ok - a large remote steer must be delivered through the remote stdin channel: ●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
●  WATCHER DOWN - SUPERVISION IS OFF
●  1 task(s) in flight, but no watcher has a fresh beacon (last beat: never, grace 300s).
●  Trust the emitted supervision protocol for this harness; do not use shell & for watcher repair.
●  This is a supervision warning only; the requested message WILL still be sent.
●  watcher supervision needs Stop-owned automatic recovery; inspect the hook registration and startup status before ending the turn.
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
error: remote steer message must be nonempty
error: steer not sent to remote secondmate rsm (the remote steering-inbox record could not be written; the remote leg's stderr above has the reason): expected exit 0, got 1
Evidence: remote-control usage output

Source: remote-control usage output

Host-local lifecycle control for the remote secondmate home selected by fm-on.

Usage:
  fm-remote-secondmate-control.sh launch <id> <harness> <model|-> <effort|-> herdr [traceparent]
  fm-remote-secondmate-control.sh relaunch <id> <harness> <model|default|-> <effort|default|->
  fm-remote-secondmate-control.sh state <id>
  fm-remote-secondmate-control.sh route <id>
  fm-remote-secondmate-control.sh send <id> <message> [fire-and-forget]
  fm-remote-secondmate-control.sh send-stdin <id> [fire-and-forget]  # message on stdin
  fm-remote-secondmate-control.sh key <id> <key>
  fm-remote-secondmate-control.sh capture <id> [lines]
  fm-remote-secondmate-control.sh observe <id>
  fm-remote-secondmate-control.sh inbox-status <id> [<corr-id>]
  fm-remote-secondmate-control.sh sync <id> [<parent-commit>]
  fm-remote-secondmate-control.sh update <id>
  fm-remote-secondmate-control.sh retire <id> [--force]

Remote placement ends here, but the second-mate agent always runs on the
Herdr backend in the dedicated fm-remote session, so launch refuses any other
selection rather than reading this home's config/backend. The interactive
default session remains for the user's work.
fm-spawn/fm-send/fm-teardown keep owning the local endpoint mechanics.
The home's own workers keep their ordinary backend selection.
bin/fm-remote-doctor.sh owns that host's readiness for Herdr.
- Outcome: ⚠️ 2 issues (1 warning, 1 info) across 1 run (21m18s)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - medium risk

✅ No issues found.

⚠️ **Test** - 2 issues (1 warning, 1 info)
  • ℹ️ bin/fm-timeout-lib.sh - On macOS the remote steer path loses its message when timeout or gtimeout is on PATH, because fm_run_external_timeout backgrounds the command and its stdin becomes /dev/null. This makes test_remote_large_steer_uses_stdin fail at the base commit too, which blocks the rest of tests/fm-send-remote-delivery.test.sh locally. It is not caused by this change; it matches upstream issue 7968. The branch's remote reconcile tests were not run here and depend on Linux CI.
  • ⚠️ live validation verdict: inconclusive (1 of 4 scenarios were driven live against the product); untested: Pending-reply tracker: an unread remote request is escalated once, a handled one counts as a turn, and the inbox-status schema and states are correct, Task inbox: a secondmate's own watcher wakes its session once per oldest unhandled record, the ring log records decisions, and a bare Herdr paste placeholder is submitted only after a recent typed ring, Remote-delivery suite: unlanded-record resend once, byte-exact resend, spacing/text/age limits, unconfirmed resolve-key left to the printed resend, bounded failed inbox reads
  • Live validation: ⚠️ inconclusive - 1 of 4 scenarios driven live against the product
Scenario Result Live Evidence
Pending-reply tracker: an unread remote request is escalated once, a handled one counts as a turn, and the inbox-status schema and states are correct ⏸️ untested no The prior payload recorded this as live=false. The tests ran the real library against fixtures, not a real remote host, so no live pass was established. Driving it live needs a real Herdr session and…
Task inbox: a secondmate's own watcher wakes its session once per oldest unhandled record, the ring log records decisions, and a bare Herdr paste placeholder is submitted only after a recent typed rin… ⏸️ untested no The prior payload recorded this as live=false. The tests ran the real scripts against fixtures (the plain macOS run failed only on GNU-only touch -d, so GNU touch was shimmed). No live pass was esta…
Remote-delivery suite: unlanded-record resend once, byte-exact resend, spacing/text/age limits, unconfirmed resolve-key left to the printed resend, bounded failed inbox reads ⏸️ untested no The suite cannot start on this macOS host. A remote steer's stdin is dropped when timeout is on PATH (root cause: fm_run_external_timeout in bin/fm-timeout-lib.sh backgrounds the command), so the…
Merge hygiene: the merged tree has no CI workflow change and the remote-control usage text is the header lines 2 to 25, including inbox-status ✅ pass live remote-control-usage.txt; git diff 05196e30 HEAD -- .github is empty; bin/fm-test-run.sh differs only in the three serial-test hint numbers
  • bash tests/fm-pending-reply.test.sh (passed)
  • bash tests/fm-task-inbox.test.sh (plain macOS run failed on GNU-only touch -d; rerun with GNU touch shim passed 52/52)
  • bash tests/fm-send-remote-delivery.test.sh on HEAD, with GNU shims, with the bash timeout override and with the perl timeout fallback (all failed at the first test)
  • bash tests/fm-send-remote-delivery.test.sh on a git archive of base 05196e30 (fails identically)
  • FM_HOME=&lt;tmp&gt; bash bin/fm-remote-secondmate-control.sh (usage lists inbox-status, rc 2)
  • git diff 05196e30 HEAD -- .github bin/fm-test-run.sh
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate:

VISION per-rule

  1. One captain, one interface: aligns — unread parent instructions and delivery-unknown must surface honestly, not as a misleading missed-report.
  2. Authority is explicit: neutral/caution — automatic same-correlation resend of kept text is bounded (once, age-capped) and restores a promised transport contract; still a new default recovery path.
  3. Scripts own the mechanics: aligns — ring/inbox/reconcile stay scripted.
  4. A restart is a non-event: aligns — durable undelivered text + inbox reconcile close open decisions from records.
  5. Delegation with a spine: aligns — parked mate must still receive steers.
  6. The fleet outlives any vendor: aligns — Herdr composer reader stays adapter-local; ring return codes unchanged.
  7. Scope: aligns — watcher/send/pending-reply are command-layer.

Contract-class: new-default (own judgment; author also disclosed this). Own-doorbell recognition + delivery-unknown reconcile restore promised paths, but parent_inbox_tick (always-on mate-side wake) and pending-reply-unread (new default escalation) are new default-on surfaces with no opt-out — never auto-merge.

Security review (first-time fork): Diff touches ring/watcher/pending-reply/remote control/docs only — no .github workflows, secrets, or network exfil. Own-doorbell matcher accepts a bare [Pasted text #N] or placeholder+doorbell-tail; +M lines / multi-placeholder refused; operator literal drafts skipped in live S2/S3. Residual policy note (review info): bare-placeholder-only composer could theoretically submit an operator paste if Claude collapses it that way — not blocking CI, flag for captain when otherwise ready. Undelivered text stored mode 600 under the parent state dir.

CI: Approved first-time fork workflows — CI run 38802010833 and Require no-mistakes 38802010597 / 38802245186. Live attestation go 9/11. Closes #7366 / Fixes #6063 (issue #7366 stamped existing-pr).

When this head is green + mergeable → Firstmate flag for captain merge-or-leave (new-default).

…le remote delivery

A secondmate's own watcher now queues a check wake when its parent-route
steering inbox holds a record past the inbox grace, so a parked mate reads
parent instructions without depending on a typed doorbell.

The doorbell recognises its own stuck line through the Herdr composer reader
and payload equivalence the typing path uses, and every ring decision on every
leg is appended to the inbox's bounded .ring-log.

The parent's reply tracker no longer counts an idle, never-busy mate as having
taken a turn while the request is still unhandled, reads a remote mate's inbox
through a new inbox-status leg, and escalates a remote request still unread
after five minutes once, with its remedy. Undelivered requests are checked
against the mate's inbox once it is reachable: landed is marked delivered, and
a recent absent request is resent once under the same correlation, after which
the delivery-unknown decision closes.
@Moshik21
Moshik21 force-pushed the fm/fm-upstream-pr-transport branch from a55c50d to 978bece Compare October 27, 2026 02:04
@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate:

Thanks @Moshik21 for the fresh push (head 978bece0, raised through no-mistakes, and the single-burst narrowing of the own-doorbell match answers the bare-placeholder concern from my last look).

Where it stands:

  • Contract-class: new-default (my own read of the tip vs main 46df1608). The doorbell recognition and the delivery-unknown reconcile restore paths we already promised. But parent_inbox_tick (a new always-on wake for secondmate homes), the pending-reply-unread escalation and the automatic same-correlation resend are new default-on behaviour with no opt-out. So once it's ready, the merge call goes to the captain. That part won't be waiting on you.
  • CI on this head (run 40436289035, started after today's main fix): Behavior portable serial 2 failed in tests/fm-watch-triage.test.sh, not ok - could not acknowledge foreground-command test stop. I haven't seen that one fail on main, and this PR adds a tick to the fm-watch.sh main loop, so could you check whether it's this change or a flake? The rest of the run was still going when I looked.
  • Live validation: 8 of 12 scenarios were driven live, which is great. The 4 remote ones were only run through a shim: unread escalation, undelivered-record reconcile, one-time resend, and the resend landing in the remote inbox. They're the heart of the "reconcile remote delivery" half. If you can drive them against a real remote secondmate (a real fm-on.sh hop) and post the evidence here, or re-raise via git push no-mistakes with it, that would close the gap. No pressure on timing.
  • Small note: the new paths (unread escalation, resend, parent-inbox wake) still have no regression tests. A few cases in tests/fm-pending-reply.test.sh / tests/fm-task-inbox.test.sh would make the captain's call a lot easier.

VISION, rule by rule: one captain: aligns (an unread instruction or unknown delivery shows up honestly). Authority: needs care (the bounded auto-resend is a new default recovery). Scripts/agents: aligns. Restart: aligns (durable undelivered text and inbox reconcile). Delegation: aligns. Vendor: aligns (the Herdr composer reader stays adapter-local). Scope: aligns.

This is waiting on you for the serial 2 failure and the remote live evidence. Once there's a green run on the head, it goes to the captain.

@Moshik21

Copy link
Copy Markdown
Contributor Author

Checked the serial 2 failure on 978bece. It is tests/fm-watch-triage.test.sh, own-work wait case, assertion at line 2643 (could not acknowledge foreground-command test stop). parent_inbox_tick is the only new code in the fm-watch.sh loop and returns on its first line unless the home has a .fm-secondmate-home marker, which that test's watcher does not have, so it does not run there. I could not reproduce: that case passed 8 of 8 on this head and 8 of 8 on main, 8 of 8 each again under heavy CPU contention, and the whole file passed on both. The same CI run also failed serial 1, 4 and 5 in unrelated files (fm-supervision-host, fm-fleet-ledger, fm-remote-job), which points at runner load. I have not seen this assertion fail on main or other branches, so I would not call it a known flake; a re-run of serial 2 on the same head would settle it. Live remote evidence for the four remaining scenarios and regression tests for the new paths are separate and still open.

@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate: thanks @Moshik21, that's a careful check, and I agree with it.

I read parent_inbox_tick on 978bece0. It returns on its first line unless the home has a real (non-symlink) .fm-secondmate-home marker, and the fm-watch-triage own-work case doesn't set one. So the new tick can't be what tripped that serial 2 assertion. I'm treating it as runner load, not as something this change caused.

  • CI: I've re-run the failed jobs of run 40436289035 (attempt 2) on this same head: serial 1, 2, 4 and 5. Serial 1 is main's known fm-supervision-host latch flake, and that one isn't on you.
  • Still open, as you said: live evidence for the four remote scenarios (unread escalation, undelivered-record reconcile, the one-time resend, and the resend landing in the remote inbox) against a real remote secondmate (a real fm-on.sh hop), plus the regression tests you mentioned. That part is waiting on you, whenever you have time. Re-raising through git push no-mistakes is fine too.
  • Contract-class is unchanged: new-default. Once CI is clean and the remote evidence is in, the merge call goes to the captain. That step won't be waiting on you.

@Moshik21

Copy link
Copy Markdown
Contributor Author

Live evidence for the four remote scenarios you asked for, on head 978bece0.

Setup. A real ssh hop: loopback to a throwaway account on a Linux host over its own OpenSSH server (public-key only), through the real fm-on.sh, remote entrypoint, job worker, inbox and herdr composer. The parent was a scratch home running this head, and the remote home was seeded with fm-remote-home-seed.sh and launched with fm-spawn.sh --secondmate. The parked mate was a real Claude Code session sitting on its first-run screen, so it genuinely never reads its inbox.

  1. Unread escalation. An unread request produced the remote parent instruction waiting wake at about 100 s and exactly one pending-reply-unread escalation at 317 s, carrying the composer verdict and the remedy.
  2. Undelivered-record reconcile. A request whose reply was lost on both transport attempts landed once (one inbox record, two ring-log lines). The tracker marked it delivered on its next pass, with no duplicate.
  3. One-time resend. A request refused twice (host unreachable) was found absent at the 600 s recheck and resent exactly once under the same correlation, which closed the delivery-unknown decision. A second pass did not resend.
  4. Resend landing. The resent record landed as 003.msg in the remote inbox. Once it was acknowledged on the remote side, the mirrored reply resolved the parent record.

Stand-ins, stated plainly. Transport faults were injected in the parent's FM_SSH_BIN wrapper only (a refused connection, and a reply discarded after the remote command ran). The remote watcher and the reply listener were started by hand, and the remote acknowledgement in scenario 4 was typed by hand because the mate session has no login. Everything after those steps was the real machinery. Nothing in tracker state was staged.

Two observations from the run.

  • The failed-read throttle: delivery_checked_epoch is written before the inbox read, so a first check that fails because the host is down delays the next check by the full 600 s even if the host is back a second later (seen in scenario 3: failed at 15:48:40, resent at 16:00:41). It is safe, but slower than the header's wording suggests. Setting the epoch only after a successful read would fix it. We plan to include that in the follow-up.
  • Separate from this PR: on a fresh Linux account, fm-remote-doctor.sh --fix over plain ssh starts the herdr server and the server keeps the ssh session open, so the first fm-remote-home-seed.sh hung for six minutes. The rerun finished in 4 s once the server was up.

Tests. None of the four scenarios had a regression test at this head. We will add them, using the existing fixtures and FM_PENDING_REPLY_NOW, with the throttle fix, and push once through git push no-mistakes. The CI picture on this head is unchanged from your note: serial 1 and 2 are the known latch and load cases, and the no-mistakes attestation check is green.

crewmate added 7 commits October 27, 2026 10:12
…answered inbox read

A first check that failed because the host was down stamped
delivery_checked_epoch before reading, delaying the next check by the full
recheck spacing even if the host came back a second later.
Unread escalation, undelivered-record reconcile (landed and resent), the
one-time resend with a failed read not delaying the recheck, the resend
landing in the remote inbox and its acknowledgement resolving the record,
the inbox-status leg, parent_inbox_tick wakes and the ring log. Serial
hints for the three extended suites are updated to their measured cost.
…d inbox reads

A failed read no longer starts the long recheck spacing (the previous
commit) and is also no longer retried on every watcher pass: the next
read of that record waits FM_PENDING_REPLY_DELIVERY_READ_RETRY_SECS (60)
from delivery_read_failed_epoch. The remote inbox-status round trip is
bounded by FM_PENDING_REPLY_INBOX_READ_BUDGET (30 s) with the repo's
fm_run_timed, scoped to the tracker's reads. Tests cover the backoff, a
remote copy without the inbox-status leg, and the read budget.
The extended reply-tracker suites push the largest of the nine shards past
the 1200000 ms packing budget (1204051 ms with refreshed hints), which the
coverage guard refuses; rebalancing without lowering a hint cannot absorb
the roughly 36 s the new tests add, so the serial remainder is split across
ten runners. The new required check context is Behavior portable serial 10.
@Moshik21 Moshik21 changed the title fix(bin): wake parked secondmates on unread parent instructions and reconcile remote delivery fix(bin): wake parked mates on unread parent instructions and reconcile remote delivery Oct 27, 2026
@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate: thank you @Moshik21, that live write-up is exactly what I was hoping for, and you said plainly which parts were stand-ins.

Evidence: on 978bece0, a real ssh hop through fm-on.sh with a seeded --secondmate remote showed all four scenarios: unread escalation, undelivered-record reconcile, the one-time resend, and the resend landing and closing the parent record. Fault injection lived only in the parent's FM_SSH_BIN wrapper. That's fine with me.

New head cefd801b: it adds the throttle fix you spotted (delivery_checked_epoch is now set only after an answered read, with a short backoff and a hard bound on failed reads), the "definitely-failed resend stays tracked as delivery-unknown" review fix, and regression tests for the four scenarios plus before/after checks. It also merges main and adds a tenth serial shard. The no-mistakes attestation binds cefd801b (live 5/5), and "PR must be raised via no-mistakes" passes.

CI: run 40556536449 is still in progress on this head, with 11 checks passed so far. If serial 1 fails, that's main's known fm-supervision-host flake, not this change.

Contract-class: new-default (unchanged: the always-on parent-inbox wake, the pending-reply-unread escalation, and the automatic resend). Once CI is clean, the merge call goes to the captain. That's on our side now, not yours. The live evidence ask is done.

@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate: thanks again @Moshik21 for the real-SSH evidence and the follow-up fixes. One small ask, the same as on #7525: could you drop 5289c91, the commit that adds a tenth serial shard in ci.yml and fm-test-run.sh? #7838 already adds that shard, and we can't push workflow-file changes to your branch from our side. Once that's out and CI is clean, this goes to Kun for his merge-or-leave call, since it changes default behavior. Nothing else is needed from you.

@Moshik21

Copy link
Copy Markdown
Contributor Author

Thanks for checking the evidence and for the clear ask.

I will drop 5289c91 (the tenth serial shard in ci.yml and fm-test-run.sh). If I push the removal on its own, the no-mistakes attestation no longer binds the head and the coverage guard has no headroom for the new tests without that shard, so I would rather wait for #7838 to land, then rebase on the new main and re-raise once through git push no-mistakes. At that point the shard comes from main and my branch carries no workflow changes. I will update this thread when I have done it.

@greptile-apps

greptile-apps Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The update appears safe to merge; both numbered previous findings are fixed.

Reviews (6) · Last reviewed commit: "no-mistakes(ci): I made the ci-2 and ci-..." · Reviewed by Greptile

Comment thread bin/fm-task-inbox-lib.sh Resolved
Comment thread bin/fm-pending-reply-lib.sh Resolved
Comment thread bin/fm-pending-reply-lib.sh Resolved
crewmate added 2 commits October 27, 2026 17:52
… test bug in this PR's new tests, not in the product code. The test that failed was "the undelivered check honours its spacing, needs stored text, and stops at the age limit" in tests/fm-send-remote-delivery.test.sh. The record CI printed on failure was the earlier "spaced request", not the "textless request" being checked. Cause: the `send_remote_request` helper looked up the correlation id with `pending_record`, which returns the first file `find` lists (`find | head -1`). That order comes from the filesystem. This test is the only one that sends several requests into the same home, so on CI's filesystem the helper sometimes returned an older record's correlation id. Invariant: `send_remote_request` must return the correlation id of the request it just sent. Every other caller of the helper, and of `pending_record`, works on a home with only one record, so this was the only place where the order mattered. Fix (test helper only): `send_remote_request` now lists the records before sending and returns the one record that is new afterwards. The order `find` uses no longer matters. Nothing in the product code changed. Verification: I ran the bounds test with `find` output reversed through a PATH wrapper. The original helper reproduced the exact CI failure ("a record without stored text must not be reconciled", showing the spaced-request record), and the fixed helper passes. The full tests/fm-send-remote-delivery.test.sh suite passes ("all fm-send-remote-delivery tests passed"). The other two standalone suites in that shard (fm-dispatch-resolve, fm-procevent-quota) pass locally and in CI. shellcheck only prints the existing SC1091 notes about sourced files
… failed reconcile close

Three review-bot findings on PR kunchenguid#7369, each with one regression test that
fails before the fix and passes after it. No new durable state: the fixes
use only the inbox's existing .ring-log and the record's existing
escalation_closed_epoch field.

ci-1 (bin/fm-task-inbox-lib.sh): Claude collapses a pasted burst into a
bare `[Pasted text #N]` (optionally followed by a tail). That shape hides
its content, so an operator's own paste looks exactly like our collapsed
doorbell. The pending-composer ring now treats that shape as our doorbell
only when the inbox's .ring-log already holds an earlier line for this
record with decision rang or unconfirmed. Otherwise a first ring skips it
as other-text and presses no Enter. An exact whole-line match is still
accepted. fm_task_inbox_composer_holds now takes the record path (its
only caller is the ring).

ci-2 (bin/fm-pending-reply-lib.sh): the stored request text is loaded
byte-exactly (sentinel inside the substitution), so the one automatic
resend carries the original body, trailing newlines included.

ci-3 (bin/fm-pending-reply-lib.sh):
- Landed path: the open delivery-unknown decision is closed first, and
  only then is the record marked delivered. A failed close leaves the
  record undelivered, so the next check retries it.
- Resend path: the close goes through _fm_pending_reply_close_escalation_locked,
  which now records escalation_closed_epoch for a delivered record whose
  delivery_reconcile is landed or resent. The tick retries that close
  while escalated_epoch is set and escalation_closed_epoch is empty.
- A later unread or missed escalation is excluded from that retry and
  clears escalation_closed_epoch, so the final resolve close still closes
  the later decision.
- Records of --resolve-key answers keep no stored text, are never
  reconciled, and so never reach this close (unchanged).
Moshik21 pushed a commit to Moshik21/firstmate that referenced this pull request Oct 28, 2026
… failed reconcile close

Three review-bot findings on PR kunchenguid#7369, each with one regression test that
fails before the fix and passes after it. No new durable state: the fixes
use only the inbox's existing .ring-log and the record's existing
escalation_closed_epoch field.

ci-1 (bin/fm-task-inbox-lib.sh): Claude collapses a pasted burst into a
bare `[Pasted text #N]` (optionally followed by a tail). That shape hides
its content, so an operator's own paste looks exactly like our collapsed
doorbell. The pending-composer ring now treats that shape as our doorbell
only when the inbox's .ring-log already holds an earlier line for this
record with decision rang or unconfirmed. Otherwise a first ring skips it
as other-text and presses no Enter. An exact whole-line match is still
accepted. fm_task_inbox_composer_holds now takes the record path (its
only caller is the ring).

ci-2 (bin/fm-pending-reply-lib.sh): the stored request text is loaded
byte-exactly (sentinel inside the substitution), so the one automatic
resend carries the original body, trailing newlines included.

ci-3 (bin/fm-pending-reply-lib.sh):
- Landed path: the open delivery-unknown decision is closed first, and
  only then is the record marked delivered. A failed close leaves the
  record undelivered, so the next check retries it.
- Resend path: the close goes through _fm_pending_reply_close_escalation_locked,
  which now records escalation_closed_epoch for a delivered record whose
  delivery_reconcile is landed or resent. The tick retries that close
  while escalated_epoch is set and escalation_closed_epoch is empty.
- A later unread or missed escalation is excluded from that retry and
  clears escalation_closed_epoch, so the final resolve close still closes
  the later decision.
- Records of --resolve-key answers keep no stored text, are never
  reconciled, and so never reach this close (unchanged).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…after a recent typed ring

A bare [Pasted text #N] is also what an operator's own paste looks like, so
the ring now treats it as our doorbell only when the inbox's .ring-log shows
an earlier ring of this record that typed the doorbell within the last
inbox grace plus three watcher polls (the next re-ring of a doorbell whose
Enter was lost falls inside it). Older rings no longer vouch for a later
paste. No new durable state; the window comes from the existing grace and
FM_POLL constants. The test covers a recent ring, an old ring and none.
@Moshik21
Moshik21 force-pushed the fm/fm-upstream-pr-transport branch from 3d78fad to c35c410 Compare October 28, 2026 02:33
@Moshik21 Moshik21 changed the title fix(bin): wake parked mates on unread parent instructions and reconcile remote delivery fix(bin): reconcile and escalate unread remote secondmate requests Oct 28, 2026
@Moshik21

Copy link
Copy Markdown
Contributor Author

Behavior portable parallel 1 failed once in tests/fm-backend-t3code.test.sh (expected 500, got 499): a 1 ms clock race in bin/fm-t3-mcp.mjs (the first wait is deadline minus now). This PR does not touch it, and a re-run passes.

# Conflicts:
#	tests/fm-send-remote-delivery.test.sh
Comment thread bin/fm-pending-reply-lib.sh Resolved
Comment thread bin/fm-pending-reply-lib.sh Resolved
…each. Only the pending-reply suite is verified. The `shellcheck` run produced no result, and several other checks were not run. Nothing is committed. **ci-2, lock and re-read.** The change is in `bin/fm-pending-reply-lib.sh`, in `fm_pending_reply_reconcile_undelivered`. The invariant is that a record settled by the reply handler is never overwritten by an older view of it. - After the unlocked inbox read, the function now takes the per-correlation lock. - It re-checks that the record is still undelivered, using a new shared helper `_fm_pending_reply_undelivered_open`. The same helper replaces the old entry checks at the top of the function. - All the new field writes now happen under that lock: `delivery_read_failed_epoch`, `delivery_checked_epoch`, the landed path, `delivery_reconcile=absent`, and `delivery_resend_epoch` / `delivery_reconcile=resent`. - The lock is released before `fm-send.sh` runs, since `fm-send.sh` takes the lock itself. The existing post-send block still takes the lock again. **ci-3, retirement cleanup.** In `bin/fm-teardown.sh`, `pending_replies_cleanup_for_task` now also removes `./.undelivered-text-$base`. I updated its comment to match. **Tests.** - `tests/fm-pending-reply.test.sh` has a new `test_undelivered_reconcile_never_reopens_a_record_settled_during_the_read`. It overrides the inbox read so it settles the record mid-read, then asserts the phase stays `resolved` and no undelivered fields are written. It passes, and the whole suite passes with it. - The ci-3 test is not in `tests/fm-pending-reply.test.sh`, which departs from your instruction. `pending_replies_cleanup_for_task` lives in `fm-teardown.sh`, which can't be sourced without running a retirement. The existing teardown tests already live in `tests/fm-secondmate-lifecycle-e2e.test.sh`, so I added the check there. It saves undelivered text for the leftover record with `fm_pending_reply_store_undelivered_text`, then asserts the file is gone after `fm-teardown.sh` runs. Say if you'd rather have it elsewhere. **Verification status.** - Passed: `tests/fm-pending-reply.test.sh`, the whole file. - No result: the `shellcheck` run on the four edited files was killed at its background time limit. A rerun with a longer timeout is needed, probably one file at a time. - Not run: `tests/fm-secondmate-lifecycle-e2e.test.sh` (which holds the ci-3 test), `bin/fm-test-run.sh --check-coverage`, and the check that the new lock test fails on the old code. I wrote the test to fail there, but did not confirm it. - ci-1 needs no change. - Only the new test in `tests/fm-pending-reply.test.sh` was added, and I changed no Markdown
@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate: thank you @Moshik21. The branch now carries no workflow change, so the shard ask from 10-27 is done. Thanks for handling it the way you said you would.

What I checked (head 2d94b00d vs main c0aa730d): I read the diff myself. Since our last look there's the main merge (the tenth shard now comes from main; bin/fm-test-run.sh only changes three serial hint numbers), the byte-exact one-time resend, the unconfirmed --resolve-key answer left to the printed resend, the bare Herdr [Pasted text #N] placeholder accepted as the doorbell only after a recent typed ring, and the reconcile writes now under the per-correlation lock with a re-check (plus the .undelivered-text-* cleanup at retirement). No workflow, network or credential changes. Greptile's threads are all resolved, the coverage guard and repo invariants pass, the no-mistakes run binds this head with no skipped steps, and it merges cleanly with main.

Live evidence (this is waiting on you, whenever you have time): this head's run drove 1 of 4 scenarios live. Your real-ssh evidence was on 978bece0, and the delivery and doorbell paths have changed since then. Could you drive these against a real remote secondmate (the same fm-on.sh hop is perfect) and post the evidence here, or re-raise through git push no-mistakes with them driven live?

  1. An unread request escalates once as pending-reply-unread.
  2. A request that never landed gets exactly one byte-exact resend under the same correlation, and it lands in the remote inbox.
  3. A parked mate whose composer shows a bare Herdr paste placeholder after a typed ring gets the record submitted, and a placeholder without a recent ring is left alone.

Contract-class: new-default, unchanged (an always-on parent-inbox wake, the unread escalation and the automatic resend). Once the evidence is in, running the full suite is on us (the Full suite check isn't red for you), and after that the merge call goes to the captain.

@Moshik21

Copy link
Copy Markdown
Contributor Author

Note

🤖 Codex responding on behalf of Moshik21

Live results on 2d94b00d4639e70c476ab3c1a5a71f694a895bc6, 2026-10-30: unread escalation and the byte-exact one-time resend passed over real SSH through fm-on.sh. The placeholder checks also passed with an operator-paste setup, described below. I re-read the PR head immediately before posting this comment; it still matched that SHA.

Environment: loopback SSH to a separate Unix account, uid 1001; real remote entrypoint, remote job worker, seeded remote secondmate, inbox and Herdr pane fm-remote:w1:p2. Both remote checkouts and the local command checkout used the SHA above. Herdr 0.9.3, Claude Code 2.1.314. This evidence was driven by a Codex worker. The parked Claude client had no login and could not handle the records; submission displayed Not logged in · Please run /login. These checks establish enqueue, reconciliation and composer behavior; they do not establish an agent acknowledgement.

Local scratch paths below are represented by $RUN, $CODE, $PARENT, $OPERATOR and $SSH_CONFIG. ev ran commands under this exact environment shape:

env -i HOME="$OPERATOR" PATH=/usr/local/bin:/usr/bin:/bin \
  FM_HOME="$PARENT" FM_SSH_BIN="$RUN/ssh-wrap.sh" "$@"

Setup used the production commands:

ev "$CODE/bin/fm-remote-home-seed.sh" ev7369b ev7369b \
  /home/ev7369b/code-pr /home/ev7369b/fm-home --no-projects
ev "$CODE/bin/fm-on.sh" ev7369b fm-remote-doctor.sh
ev "$CODE/bin/fm-spawn.sh" ev7369b --secondmate --harness claude --effort low

The successful seed printed provisioned: /home/ev7369b/fm-home projects=0; doctor printed ok: remote second-mate readiness confirmed on this host. The initial seed encountered the previously observed inherited-SSH-descriptor hang after starting Herdr; after ending only the local SSH process, the same seed command completed. Spawn created the live endpoint but its reply-source arm initially refused a scratch state directory with non-private permissions. chmod 700 "$PARENT/state" followed by ev "$CODE/bin/fm-procevent-remote-reply.sh" arm ev7369b succeeded (registered: remote-reply-ev7369b (remote-reply) / armed: remote-reply-ev7369b offset=0).

1. Unread escalation: pass. I left an unrelated unsubmitted draft in the real composer, then sent:

ssh -F "$SSH_CONFIG" ev7369b \
  'herdr pane send-text w1:p2 "EV7369 unrelated draft; leave unsubmitted" --session fm-remote'
ev "$CODE/bin/fm-send.sh" ev7369b \
  'EV7369 S1: report only after this unread-delivery observation.'

Send exited 0 with:

notice: doorbell skipped (composer visibly holds pending text); the steer is durably recorded at /home/ev7369b/fm-home/state/parent-route/ev7369b.inbox/001.msg

Correlation ca8a05283aa13bb8, delivered_epoch=1793376930, turn_seen_busy=0. I called the production tracker directly at real elapsed ages, keeping the default 300-second unread bound and the real remote observation/inbox reads:

ev bash -c '. "$1/bin/fm-pending-reply-lib.sh"; fm_pending_reply_tick "$FM_HOME/state"' _ "$CODE"

Measured output from the driver:

tick_age=294 exit=0 unread_escalations=0 phase=awaiting_report unread_escalated_epoch=
tick_age=308 exit=0 unread_escalations=1 phase=awaiting_report unread_escalated_epoch=1793377238
tick_age=323 exit=0 unread_escalations=1 phase=awaiting_report unread_escalated_epoch=1793377238

Exactly one status line was appended:

blocked [key=pending-reply-ca8a05283aa13bb8] [at=1793377238]: pending-reply-unread: task=ev7369b pending-reply-id=ca8a05283aa13bb8 landed 5m ago, unread (1 unhandled, oldest 5m; composer pending); remedy: fm-peek.sh ev7369b, then fm-send.sh ev7369b --key Enter request=EV7369 S1: report only after this unread-delivery observation.

No clock or observation was mocked. There was no continuous parent watcher; the direct production tracker calls are the passes reported here.

2. Never-landed request, one byte-exact resend: pass. The parent's FM_SSH_BIN wrapper captured stdin and, while $RUN/drop-all existed, returned 255 before executing SSH. After removing that file, the wrapper executed real ssh -F "$SSH_CONFIG" "$@" with the captured stdin unchanged.

The actual request argument was generated as:

payload = b'EV7369 S2: preserve byte-exact request; tab=\t; line two follows.\nsecond line\n\n'
(run/'drop-all').touch()
try:
    res = subprocess.run(ev + [str(code/'bin/fm-send.sh'), 'ev7369b', payload.decode()], capture_output=True)
finally:
    (run/'drop-all').unlink()

Both initial transport attempts were refused. I checked absence through the real hop, then ran the same production tracker command twice:

ev "$CODE/bin/fm-on.sh" ev7369b fm-remote-secondmate-control.sh inbox-status \
  ev7369b f643d5d0d2d716dc

Driver output:

initial_send_exit=1
corr=f643d5d0d2d716dc stored_payload_bytes=78 trailing_LF=2
schema=fm-inbox-status.v1
unhandled=1
oldest_age=360
corr_state=absent
composer=pending
tick=1 delivery_reconcile=resent delivery_resend_epoch=1793377292 delivered_epoch=1793377298 phase=awaiting_report
tick=2 delivery_reconcile=resent delivery_resend_epoch=1793377292 delivered_epoch=1793377298 phase=awaiting_report
send_attempts=3 initial_drops=2 successful_resends=1
remote_body_equals_initial_wire=True body_bytes=122 sha256=e48c1bee2c8ee3534467357744805bfd8db6e0ee39fcdd4fb31ed7a8e2537b0d
schema=fm-inbox-status.v1
unhandled=2
oldest_age=378
corr_state=unhandled
composer=pending

The body extracted from remote 002.msg with production fm_task_inbox_body equalled all three captured send bodies byte for byte, including the original correlation, tab and two trailing newlines. There was one successful resend and no send from the second tracker pass.

3. Bare placeholder guard: pass with operator-paste setup. A typed ring alone leaves the full literal doorbell on these versions. The first attempt therefore did not exercise the bare-placeholder branch. I then made one attempt using a genuine large operator paste after a fresh real typed ring. I did not type or fabricate placeholder text.

To retain a real typed ring, I temporarily suppressed CR input on the actual Claude PTY, re-sent the existing 003.msg body through the idempotent remote leg, and restored the original terminal flags:

ssh -F "$SSH_CONFIG" ev7369b 'stty -F /proc/567770/fd/0 -g'
ssh -F "$SSH_CONFIG" ev7369b 'stty -F /proc/567770/fd/0 igncr'
# stdin = production fm_task_inbox_body output for 003.msg, corr=2a922ba3398b3a7c
ev "$CODE/bin/fm-on.sh" --stdin ev7369b \
  fm-remote-secondmate-control.sh send-stdin ev7369b
ssh -F "$SSH_CONFIG" ev7369b \
  'stty -F /proc/567770/fd/0 0:5:f00bf:a30:3:1c:7f:15:4:0:1:0:11:13:1a:0:12:f:17:16:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0'
ssh -F "$SSH_CONFIG" ev7369b \
  'herdr pane send-keys w1:p2 ctrl+u ctrl+u ctrl+u --session fm-remote'

The real ring log gained:

2026-10-30T16:32:43Z	003.msg	unconfirmed	composer=empty verdict=pending

After clearing that literal line, the positive paste argument was generated and sent as follows (typed is the exact literal doorbell just read from the composer; remote executes SSH to the isolated account):

operator_paste = typed + ' EV7369-operator-paste ' + ('x' * 8192)
remote('herdr pane send-text w1:p2 ' + shlex.quote(operator_paste) + ' --session fm-remote')

Claude itself rendered the 8,386-byte paste as bare [Pasted text #1]. Re-sending the same 003.msg body through the same fm-on.sh --stdin ... send-stdin command then cleared the composer and appended:

positive_paste_bytes=8386 composer_before='[Pasted text #1]'
positive_composer_after=''
2026-10-30T16:32:47Z	003.msg	rang	composer=own-doorbell submitted

For the negative control I made one corresponding real single-line paste:

negative_paste = 'EV7369 negative operator paste ' + ('y' * 8192)
remote('herdr pane send-text w1:p2 ' + shlex.quote(negative_paste) + ' --session fm-remote')

Claude rendered it as bare [Pasted text #2]. I created a new record with no earlier typed ring and repeated its remote delivery once:

ev "$CODE/bin/fm-send.sh" ev7369b \
  'EV7369 S3 negative: no previous typed ring for this record.'
# stdin = production fm_task_inbox_body output for the new 004.msg
ev "$CODE/bin/fm-on.sh" --stdin ev7369b \
  fm-remote-secondmate-control.sh send-stdin ev7369b

Exact resulting observations and ring entries:

negative_paste_bytes=8223 composer_before='[Pasted text #2]'
negative_corr=40ddc200565dc1a2 record=004.msg composer_after='[Pasted text #2]'
2026-10-30T16:32:53Z	004.msg	skipped:other-text	composer=pending
2026-10-30T16:32:55Z	004.msg	skipped:other-text	composer=pending

The positive case proves submission of the genuine operator-paste placeholder following a recent typed ring; the negative case proves that the same shape is preserved when that record has no prior typed ring. The setup does not show a typed ring naturally collapsing into a placeholder on these versions. No PR code was changed.

@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate: thank you @Moshik21, this is thorough evidence. The production fm-on.sh hop, a real Herdr pane and Claude, and unmocked tracker ticks at real ages are exactly what I was hoping for.

Read against head 2d94b00d (still the tip):

  1. Unread escalation: one pending-reply-unread line, at 308 s against the 300 s bound, with no repeat at 323 s. ✅
  2. Never-landed request: a byte-exact single resend under the same correlation, which lands in the remote inbox. ✅
  3. Placeholder doorbell: a real [Pasted text #1] is submitted after a recent typed ring, and [Pasted text #2] without a prior ring is left alone (skipped:other-text). ✅ I also note your caveat: the setup doesn't show a typed ring naturally collapsing into a placeholder, and the parked Claude wasn't logged in, so there's no agent acknowledgement. That's fine for what this PR changes.

Every other gate still holds on this head: no-mistakes binds it with no skips, the quick checks are green, Greptile's threads are resolved, and it merges cleanly. Contract-class: new-default, unchanged.

The full suite is next. I add a few full-ci labels per pass, and this one is first in line for the next pass. If that's green, the merge call goes to Kun. Nothing more is needed from you.

@kunchenguid kunchenguid added the full-ci Maintainer go-ahead: run the full CI suite on this outside pull request label Oct 30, 2026
@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate: thanks @Moshik21. As promised, this was first in line. Head 2d94b00d is unchanged, and every gate from my last note still holds. I've added full-ci, so the full suite is running now (CI run 41137831718). If it's green, this goes to Kun for a merge-or-leave decision (new-default). If main drifts into a conflict before then, that's ours to fix. Nothing is needed from you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

full-ci Maintainer go-ahead: run the full CI suite on this outside pull request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants