feat: reliable echo via final-id resolution + supervise beepertexts - #13
Merged
Conversation
Two asks from multis (beeperbox's first consumer).
Ask A — reliable echo via id resolution (mcp/server.js):
A send returns a pendingMessageID, but Beeper swaps it for the real bridge
id on ack, so the echo-guard's exact-id match always missed and degraded to
fragile 15-min text matching. send_message / note_to_self now resolve the
pending id to the final bridge id (GET .../messages/{pendingMessageID} until
it swaps, bounded + best-effort) and record BOTH ids, so a read-back matches
by exact id whether or not the swap happened. The text fallback survives only
as a last-ditch net AND only for sends whose final id couldn't be resolved —
so a human re-typing identical text is no longer mis-tagged as the agent's
own and dropped. Additive return fields: pending_message_id, resolved.
Tunable via BEEPERBOX_RESOLVE_RETRIES / BEEPERBOX_RESOLVE_DELAY_MS.
Ask B(2) — supervise beepertexts (entrypoint.sh):
beepertexts was launched unattended; when its API process crashed while the
Electron launcher lingered, the container looked up and the MCP layer kept
answering but every tool call failed (half-dead, never self-heals). The
entrypoint now supervises: relaunch on process death, recycle if the API
(:23373) stays down after having been up. An API_WAS_UP gate protects
first-run login (API is down by design until enabled). Tunable/reversible
via BEEPERBOX_SUPERVISE*. (Ask B(1), the stale-Xvfb-lock restart fix, already
shipped in #12.)
Verification (AGENT_RULES: POC + test-must-fail):
- mcp/server.test.js: 26 tests (was 21). New: read-back tagged by resolved
id; the acceptance test that a human re-typing identical text after a
resolved send stays external; two identical-text sends matched by their own
ids; unresolved entry still caught by the text net; addResolvedId persist/
reload round-trip. Existing 21 pass unchanged (legacy sent_id back-compat).
- POC spikes proved the load-bearing claims AND that the tests can fail: the
prior matcher mis-tags the human re-type, and an ungated supervisor would
recycle pre-login.
Honest limits (unverified in CI — no live Beeper account): the pending->final
id resolution depends on Beeper's live ack behavior (proven against a real
account by multis, id 908; not exercised in CI). The supervisor's kill/
relaunch integration needs a live container; only its decision logic is
POC-covered.
Docs: CHANGELOG (Ask A + supervision under [Unreleased]), beeperbox.context.md
(send returns + echo-guard caveat), docs/GUIDE.md (send row + self-heal note),
docs/PRD.md (supervised-backend operational property).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… findings) Two robustness fixes from the /security pass on this branch, both validated empirically before fixing: - entrypoint.sh: a non-numeric BEEPERBOX_SUPERVISE_INTERVAL / _API_GRACE made `sleep`/`-ge` fail instantly, spinning the supervisor loop at 100% CPU (and hammering the API probe); 0 was a no-wait spin too. Confirmed `sleep abc` exits 1 instantly. Now sanitized to a positive integer, falling back to the documented default. - mcp/server.js: a typo like BEEPERBOX_RESOLVE_RETRIES=four parsed to NaN and silently passed the loop guards, DISABLING id resolution (echo-guard quietly degrades to text-only, no error). Confirmed NaN -> 0 loop iters -> null. Now clamped via envIntNonNeg(): malformed -> default, 0 stays an intentional disable. Not fixed: the send->backend GET amplification finding is the pre-existing MCP-layer rate-limit gap (mitigated by the loopback bind), out of scope for this diff. Unit suite still 26/26. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…anch, shutdown race)
From the /code-review pass on this branch. Validated each finding against the
code before fixing; non-issues confirmed and left alone.
mcp/server.js:
- I-1 (Important): resolveSentId's beeperFetch calls had no timeout, so a hung
Beeper API could stall every send unbounded. Added opt-in beeperFetch
timeoutMs (AbortController; existing 19 callers unchanged) and a per-attempt
RESOLVE_TIMEOUT_MS (default 3000, env-tunable). Worst-case added send latency
is now bounded at retries × (timeout + delay). Proven: a hung server aborts
in ~300ms instead of hanging.
- M-1 (Minor): dropped the speculative list-shape branch in resolveSentId. On a
by-id GET it took items[0] of an unexpected list response and could store the
WRONG final id, mis-tagging an unrelated read-back. Now trusts only the
single-object shape.
- I-2 (defensive): resolveSentId early-returns on empty chatID (in practice
chatID is always set — send_message validates chat_id, note_to_self uses the
resolved note chat — but the guard makes the function self-safe).
entrypoint.sh:
- M-4 (Minor): re-check SHUTTING_DOWN before relaunch in the process-death
branch, so a SIGTERM landing in that window doesn't spawn a fresh Beeper just
to immediately stop it during docker stop.
- M-5: comment noting the `|| true` / `if`-guard discipline is load-bearing
under `set -e`.
Confirmed NON-issues (no change): the entry mutation after LEDGER_MAX slicing
is never lost (push keeps the reference; eviction is from the front); the trap
targets the current PID after relaunch (single-quoted body expands at delivery);
I-3 — resolution does engage for note_to_self (chatID always valid).
Tests: +1 legacy {sent_id} back-compat test (id-match AND text-fallback). 27/27.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The RESOLVE_* and SUPERVISE_* knobs were documented in prose but missing from the §5.3 env var table. Add them so the configuration surface is complete. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Merged
hamr0
added a commit
that referenced
this pull request
Jun 15, 2026
) Cuts 0.6.0 (MINOR) so :latest carries Ask A/B before multis Phase 2 ships (Phase 2 hard-depends on the exact-id echo-guard — no text fallback left). Bundles everything under [Unreleased]: poll_messages watch primitive + source echo-guard (#11), docker-restart stale-lock fix (#12), reliable echo via final-id resolution + supervised backend (#13). - CHANGELOG: [Unreleased] -> [0.6.0] — 2026-06-15 with a release summary. - mcp/server.js: serverInfo.version 0.5.0 -> 0.6.0 (was stale). - docs/PRD.md: status stamp -> v0.6.0; release-history table corrected (the stale "(unreleased)" row is 0.5.1) + 0.6.0 row; example tag bumped. - beeperbox.context.md / README.md: version stamps + pin example -> 0.6.0. Tag v0.6.0 after merge triggers the gated publish (:0.6.0/:0.6/:0/:latest, old :latest -> :previous). Live id-resolution round-trip validated by multis; CI gate still runs the server standalone (no account). Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two asks from multis (beeperbox's first consumer). Ask B(1) — the stale-Xvfb-lock restart fix — already shipped in #12; this PR is Ask A + Ask B(2).
Ask A — reliable echo via id resolution (
mcp/server.js)Problem: a send returns a
pendingMessageID, but Beeper swaps it for the real bridge id on ack. The echo-guard recorded the pending id and matched read-backs by exact id → always missed → every match degraded to the fragile 15-minute text fallback.Fix (grounded in Beeper's docs):
send_message/note_to_selfnow resolve the pending id to the final bridge id —GET .../messages/{pendingMessageID}retried until the id swaps (bounded, best-effort, never throws) — and record both ids in the ledger. A read-back then matches by exact id whether or not the swap happened. The content fallback survives only as a last-ditch net and only for sends whose final id couldn't be resolved; once a send is resolved, its text fallback is retired.Acceptance criteria (all covered by unit tests):
external)_isLoopingbackstopAdditive return fields on both send tools:
pending_message_id,resolved.message_idis the final bridge id whenresolved: true, else the pending id. No field removed. Tunable viaBEEPERBOX_RESOLVE_RETRIES(default 4) /BEEPERBOX_RESOLVE_DELAY_MS(default 250); set retries0to disable.Ask B(2) — supervise beepertexts (
entrypoint.sh)Problem: beepertexts ran unattended. When its API process crashed while the Electron launcher lingered, the container looked "up" and the MCP layer kept answering, but every tool call returned
-32603 fetch failedbecause:23373was gone — half-dead, never self-heals.Fix: the entrypoint now supervises — relaunch if the process dies, recycle if the API stays down after having been up (watches both the PID and
:23373/v1/spec). AnAPI_WAS_UPgate protects first-run login: until the user enables the API it's down by design, so the loop never recycles before the API has come up once. Cleandocker stopshutdown preserved. Reversible/tunable:BEEPERBOX_SUPERVISE(default1,0= old behavior),BEEPERBOX_SUPERVISE_INTERVAL(10s),BEEPERBOX_SUPERVISE_API_GRACE(6 ≈ 60s).Verification — AGENT_RULES (POC + test-must-fail)
addResolvedIdpersist/reload round-trip. The existing 21 pass unchanged → legacysent_idback-compat holds.Honest limitations (unverified in CI — no live Beeper account)
pendingMessageID → final idresolution depends on Beeper's live ack behavior. multis proved it against a real account (id 908); it is not exercised in CI. Validate against your own account before relying on it for a high-stakes auto-responder.Docs
CHANGELOG (Ask A + supervision under
[Unreleased]),beeperbox.context.md(send returns + rewritten echo-guard caveat),docs/GUIDE.md(send-return row + self-heal troubleshooting note),docs/PRD.md(supervised-backend operational property).🤖 Generated with Claude Code