Skip to content

fix(entrypoint): clear stale Xvfb lock so docker restart survives#12

Merged
hamr0 merged 1 commit into
masterfrom
fix/restart-stale-xvfb-lock
Jun 15, 2026
Merged

fix(entrypoint): clear stale Xvfb lock so docker restart survives#12
hamr0 merged 1 commit into
masterfrom
fix/restart-stale-xvfb-lock

Conversation

@hamr0

@hamr0 hamr0 commented Jun 15, 2026

Copy link
Copy Markdown
Owner

What

docker restart reliably wedged the container with a stale Xvfb lock. 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 docker 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:

rm -f /tmp/.X99-lock /tmp/.X11-unix/X99

docker restart is 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/Message schemas, default ports are bit-identical).

Docs

  • CHANGELOG.md### Fixed entry under [Unreleased].
  • docs/PRD.md — §6.3 Operational properties: new "Restart-survivable display" bullet.
  • README.md — quickstart restart note made accurate.

Verification caveat

The fix is mechanically sound and matches the documented Xvfb failure mode + the observed Server is already active log line. It has not yet been verified by a build + docker restart repro cycle against the patched image — flagging honestly. bash -n entrypoint.sh passes.

🤖 Generated with Claude Code

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 hamr0 merged commit 7ed07d6 into master Jun 15, 2026
3 checks passed
@hamr0 hamr0 deleted the fix/restart-stale-xvfb-lock branch June 15, 2026 20:28
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>
@hamr0 hamr0 mentioned this pull request Jun 15, 2026
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>
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.

1 participant