fix(entrypoint): clear stale Xvfb lock so docker restart survives#12
Merged
Conversation
docker restart re-runs the entrypoint but preserves the container's writable layer, so Xvfb's /tmp/.X99-lock from the previous boot survived into the new process. Xvfb then saw display :99 as "already active", half-initialized it, and segfaulted (EE Server is already active for display 99) — the backend never bound 127.0.0.1:23373, socat looped on connection-refused, and the container sat stuck in health: starting. Only a full down/up (which discards /tmp) recovered. Remove the stale /tmp/.X99-lock and /tmp/.X11-unix/X99 before starting Xvfb so restart — the natural op after any config change — is survivable. No runtime-contract change (MCP tools, HTTP API, schemas, ports bit-identical); PATCH per the versioning policy. Docs updated in CHANGELOG, docs/PRD.md (operational properties), and README. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
hamr0
added a commit
that referenced
this pull request
Jun 15, 2026
) * feat: reliable echo via final-id resolution + supervise beepertexts 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> * harden: sanitize supervisor + resolution env vars (security audit Low 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> * fix: address code-review findings (resolve latency bound, wrong-id branch, 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> * docs(prd): list resolve + supervise env vars in the config-surface table 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> --------- 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.
What
docker restartreliably wedged the container with a stale Xvfb lock.docker restartre-runs the entrypoint but preserves the container's writable layer, so Xvfb's/tmp/.X99-lockfrom the previous boot survived into the new process. Xvfb then saw display:99as "already active", half-initialized it, and segfaulted:The backend never bound
127.0.0.1:23373, socat looped on connection-refused, and the container sat stuck inhealth: starting. Only a fulldocker compose down && up(which discards/tmp) recovered.Fix
One line before Xvfb starts — remove the stale lock so a restarted container gets a clean display:
docker restartis the natural operation after any config change, so this matters for day-to-day ops.Scope / versioning
PATCH per the versioning policy — no runtime-contract change (MCP tool set, HTTP/raw API,
Chat/Messageschemas, default ports are bit-identical).Docs
### Fixedentry under[Unreleased].Verification caveat
The fix is mechanically sound and matches the documented Xvfb failure mode + the observed
Server is already activelog line. It has not yet been verified by a build +docker restartrepro cycle against the patched image — flagging honestly.bash -n entrypoint.shpasses.🤖 Generated with Claude Code