Skip to content

feat: Hermes Communication Officer local Discord bridge (FM-3) - #116

Closed
dnth wants to merge 11 commits into
mainfrom
cursor/fm-ext-hermes-comms-mvp-e478
Closed

feat: Hermes Communication Officer local Discord bridge (FM-3)#116
dnth wants to merge 11 commits into
mainfrom
cursor/fm-ext-hermes-comms-mvp-e478

Conversation

@dnth

@dnth dnth commented Sep 6, 2026

Copy link
Copy Markdown
Owner

Summary

  • Sibling local-bridge for Discord → Hermes Gateway → Firstmate PRIMARY (fm-ext-* inbox/wake/outbox), not hosted relay / FMX / pending-reply.
  • Hermes Gateway /fm plugin template under contrib/hermes-gateway-firstmate-comms/ (dedicated gateway profile; crewmate TUI unchanged).
  • Discord reply split like X mode (FM_EXT_DISCORD_REPLY_MAX_CHARS), exclusive inflight claim, TTL/steal (dead pid + age) with steallock floor max(ttl,1).
  • Allowlist fail-closed; durable correlation; hermetic tests/fm-ext-bridge.test.sh.

Captain decisions applied

  • Split Discord replies like X mode
  • Exclusive resume claim before send
  • TTL/steal for stale .inflight (30s + dead pid)
  • Steallock floor for TTL=0
  • Accept residual aged-steallock reclaim TOCTOU for MVP

Test plan

  • tests/fm-ext-bridge.test.sh (26/26 at tip)
  • Hermes / X / pending-reply regression suites left green in ship runs
  • CI on this PR

Do not merge until captain word via Firstmate.

cursoragent and others added 11 commits September 5, 2026 12:52
Discord /fm requests enter this home through a dedicated Hermes Gateway
plugin and local inbox/outbox files. Opt-in is config/ext-bridge or
FM_EXT_BRIDGE=1 plus a mode-0600 secret, not the hosted X-mode relay.

Co-authored-by: Dickson Neoh <dickson.neoh@gmail.com>
Abort the outbox posting marker on a definite send failure before a
successful response so that generation can retry, and keep mid-delivery
only for an ambiguous crash after the post started. If intake cannot
append the wake after claiming the offer, drop the offer marker so a
later intake or poll can retry.

Co-authored-by: Dickson Neoh <dickson.neoh@gmail.com>
Keep the posting marker on ambiguous transport errors so a timeout or
URLError cannot double-post. Record a terminal failed marker for
permanent 4xx so pending stops retrying. Abort only for transient
definite failures (HTTP 429 and 5xx).

Co-authored-by: Dickson Neoh <dickson.neoh@gmail.com>
Reuse X mode's Discord numbered-thread split (default 1900, cap 25)
behind FM_EXT_DISCORD_REPLY_MAX_CHARS so the sibling bridge does not
need a pairing token or hosted relay. Post chunks in order with durable
per-chunk progress so a later failure cannot double-post earlier
messages, and so an oversized reply is split instead of becoming a
terminal HTTP 400.

Co-authored-by: Dickson Neoh <dickson.neoh@gmail.com>
Resume no longer treats JSON progress with a null inflight field as a
shared claim. begin CAS-claims a per-generation inflight marker before
any send, so two posters cannot both post the same remaining chunk.

Co-authored-by: Dickson Neoh <dickson.neoh@gmail.com>
The concurrent inflight-claim case added about three seconds of wall time.

Co-authored-by: Dickson Neoh <dickson.neoh@gmail.com>
Split failure after posted chunks and a stored/split chunk-count
mismatch now drop the send claim before returning, so resume is not
stuck. Abort releases inflight before posting so a mid-abort crash
cannot leave a stale claim that blocks the next begin.

Co-authored-by: Dickson Neoh <dickson.neoh@gmail.com>
Case 22 added about two seconds of wall time on this runner.

Co-authored-by: Dickson Neoh <dickson.neoh@gmail.com>
begin records owner pid and claim time. A leftover inflight is stolen
only when it is older than FM_EXT_INFLIGHT_TTL_SECS (default 30) and the
owner pid is dead. Live posters, including past TTL, still refuse so two
concurrent senders cannot both post the same chunk.

Co-authored-by: Dickson Neoh <dickson.neoh@gmail.com>
Case 23 added about two seconds of wall time on this runner.

Co-authored-by: Dickson Neoh <dickson.neoh@gmail.com>
Claim expiry may still be zero for tests, but steal serialization must
not treat a live lock as immediately reclaimable. Two concurrent
stealers therefore cannot both take the send right.

Co-authored-by: Dickson Neoh <dickson.neoh@gmail.com>
dnth added a commit that referenced this pull request Sep 7, 2026
* feat: Hermes Communication Officer local Discord bridge (FM-3)

Squashed rebase of PR #116 onto current main, unmodified, as the base
for the review fixes that follow. Original work by dnth on branch
cursor/fm-ext-hermes-comms-mvp-e478 (head 43140b8).

Claude-Session: https://claude.ai/code/session_01MmS2mAcduG9zy5HD3pUGuV

* fix(ext): recover wedged replies, bound poll cost, and tighten bridge auth

Addresses the scout review of PR #116. The core-infra edits it found clean
are unchanged in behavior; every fix below is in the ext-bridge seam.

Recoverable replies. An ambiguous mid-chunk send (URLError, TimeoutError,
OSError, HTTP 408, unparseable 200) deliberately leaves the chunk recorded
in-flight so it cannot double-post, but nothing cleared it: one network
timeout wedged that reply forever, and neither restart, wait, nor release
could recover it. fm_ext_outbox_begin now consults a bounded recovery first.
Past FM_EXT_MIDDELIVERY_RECOVERY_SECS (300, far above the 15s send timeout)
it reopens exactly that chunk for another attempt; after
FM_EXT_MIDDELIVERY_RECOVERY_MAX (3) attempts it records the terminal failure,
exits 5, and wakes firstmate. A timeout now costs a repeated chunk or a
surfaced failure, never a silent truncation.

Bounded poll and retention. A delivered payload is retired as soon as its
generation is terminal, so `pending` scans only pending work instead of every
reply ever sent, and marker names are matched without a basename process each.
Terminal markers stay in place so a duplicate emit is still idempotent, which
is why begin now answers a terminal outcome before requiring the payload.
ext-context records and retired outbox markers expire on the same seven-day
window X mode uses, swept from the poll the way fm-x-poll.sh sweeps its own.

Least-privilege auth. Admission and authority are now separate: only a
<guild>:<channel>:<author> rule grants standing authority to act on project
work, while a guild-only or channel-only rule admits a request that needs the
captain's confirmation before any project change. Malformed rules (an empty
component, a fourth component) are ignored rather than guessed at. The
plugin's parallel Python allowlist is deleted; bin/fm-ext-lib.sh is the single
owner and the plugin resolves through fm-ext-intake.sh, which exits 3 on
refusal. This settles the three rule shapes the two copies disagreed on, all
in the deny direction.

One opt-in authority. config/ext-bridge is the only way to activate the
bridge; FM_EXT_BRIDGE can now only disable one. The plugin no longer forces
that variable on, so installing it can no longer switch the intake half of a
home on while its bootstrap and watcher halves believe the bridge is off.

Durable wake before consumption. fm-ext-poll.sh appends its own wake record
per claimed offer and releases the claim when that append fails, mirroring
fm-ext-intake.sh. A watcher that dies between claim and append no longer
drops the request silently, and the watcher does not append a second record.

deliver_one treats a retired payload as an outcome rather than raising, so a
sibling poster retiring it mid-pass cannot abort a whole drain.

Tests: ten new cases (24-33) covering wedge recovery and its surfaced
exhaustion, pending flatness and retention, shell/plugin allowlist agreement
over ten rule shapes, the authority levels and every fail-closed allowlist,
the secret gate on mismatched/0644/symlinked/empty/missing secrets, opt-in
consistency, poll unclaim on wake failure, and inertness without opt-in. The
fixture now models the recommended author-scoped rule. 33/33 pass;
bin/fm-lint.sh and bin/fm-doc-audience-check.sh clean.

Claude-Session: https://claude.ai/code/session_01MmS2mAcduG9zy5HD3pUGuV

* no-mistakes(review): Replace source grep and add case-variant behavioral probes

* no-mistakes(review): Serialize recovery progress update before inflight release

* no-mistakes(review): Recover pre-send wedges without progress artifacts

* no-mistakes(review): Validate inbox artifacts before retaining bridge context

* no-mistakes(review): Honor state overrides and remove source-only spawn assertions

* no-mistakes(review): Recover only inactive in-flight deliveries using heartbeats

* no-mistakes(test): Fixed bridge test harness defects; targeted suite passes

* no-mistakes(document): Update heartbeat-based recovery documentation
@dnth

dnth commented Sep 7, 2026

Copy link
Copy Markdown
Owner Author

Superseded by #120 (a clean re-implementation validated through no-mistakes: both blockers fixed, least-privilege auth, and the concurrency race regression tests). #120 is merged to main.

@dnth dnth closed this Sep 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants