Skip to content
This repository was archived by the owner on Jul 16, 2026. It is now read-only.

feat(bridges): single-instance flock guard for telegram/discord/slack (Phase 5.5) - #75

Merged
jgluck96 merged 1 commit into
dev-clientfrom
josh/phase-5.5-oss-only-sweep
Jun 1, 2026
Merged

jgluck96 merged 1 commit into
dev-clientfrom
josh/phase-5.5-oss-only-sweep

Conversation

@jgluck96

@jgluck96 jgluck96 commented Jun 1, 2026

Copy link
Copy Markdown

Summary

Phase 5.5 of the OSS→private sync — the OSS-only files sweep. After triaging the 12 files that exist in OSS sutando's src/ but not in stando's, exactly one was a clean, high-value port: single_instance.py.

This PR ports it verbatim and adopts it in the three bridges (telegram / discord / slack) so a duplicate process — the canonical case being startup.sh racing Sutando.app's launchd agent at boot — exits cleanly instead of running side-by-side and double-delivering tasks / racing API tokens (observed in OSS: 409s on telegram, double DMs on discord).

Mechanism

  • OS-level fcntl.LOCK_EX | LOCK_NB on a workspace-local lock file at <workspace>/state/locks/<name>.lock.
  • Auto-released by the OS when the holder process dies — no manual cleanup.
  • Contended acquirers exit 0 (not 1) so launchd's KeepAlive doesn't restart-loop on the second instance.

Adoption (one line per bridge)

Bridge Where the guard is placed
telegram-bridge.py First line of main()
slack-bridge.py First line of main()
discord-bridge.py Top of the client.run(...) branch — NOT the send REST-shortcut branch, so dm-result.py-style one-shot sends still work alongside a running bridge

The OSS single_instance.py is already wired to workspace_default.resolve_workspace() — which stando's Python bridges already use — so the port required zero adaptation.

What was triaged and intentionally NOT ported

The other 11 OSS-only files in src/:

File Decision
result_channel_key.py / result-channel-key.ts Needs consumer-side adoption in discord-voice + phone-conversation skills — separate multi-file feature port.
task_archive.py Depends on the claim_task mechanism stando doesn't have.
voice-key.ts voice-agent.ts already does the GEMINI_VOICE_API_KEY → GEMINI_API_KEY chain inline; payoff requires refactoring discord-voice + phone-conversation skill scripts (separate effort).
voice-config.ts / voice-config-switch.ts stando uses env vars by design (per Phase 5.4 finding).
send_allowlist.py Pure extraction with only one current stando consumer (discord-bridge.py); dm-result.py doesn't upload files yet, so the dedup payoff is mostly future-proofing. Pair with the dm-result.py upload feature port in a dedicated phase.
discord_config.py Owner-resolution helper; needs consumer-side migration of discord-bridge + dm-result and an access.json → discord-config.json migration path. Standalone feature, dedicated phase.
morning-briefing.py stando has the morning-briefing skill; standalone cron script may not be needed. User decision.
obsidian-mirror.py Net-new feature, gated by SUTANDO_OBSIDIAN_MIRROR env var. User decision.
overlay-manager-ui.ts Net-new UI for the overlay-apps system; standalone feature, dedicated phase.

Test plan

  • tests/single-instance.test.py — 4 cases:
    • First acquire writes PID into lock file
    • Contended acquire exits 0 (not 1) with the expected stderr message
    • Lock is released after first holder dies (next acquire succeeds)
    • Distinct names do not contend
  • Existing bridge tests still pass (telegram-bridge-tofu, telegram-bridge-allowlist, slack-bridge-access, discord-bridge-allowlist)
  • pnpm run typecheck clean (no TS surface touched)
  • Smoke: kill an existing telegram bridge, start two in quick succession, confirm the second exits cleanly with the lock-contention message

Made with Cursor

Phase 5.5 of the OSS→private sync. Ports `src/single_instance.py`
verbatim from OSS sutando and adopts it in the three bridges so a
duplicate process (the canonical case: `startup.sh` racing
Sutando.app's launchd agent at boot) exits cleanly instead of running
side-by-side and double-delivering tasks / racing API tokens.

Mechanism: OS-level `fcntl.LOCK_EX | LOCK_NB` on a workspace-local
lock file (`<workspace>/state/locks/<name>.lock`). Auto-released by
the OS when the holder process dies (no manual cleanup). Contended
acquirers exit 0 (NOT 1) so launchd KeepAlive doesn't restart-loop
on the second instance.

Adoption is one line per bridge:
  - telegram-bridge.py / slack-bridge.py: first line of `main()`.
  - discord-bridge.py: at the top of the `client.run(...)` branch
    (NOT the `send` REST-shortcut branch — `dm-result.py`-style
    one-shot sends must keep working alongside a running bridge).

Verified:
  - tests/single-instance.test.py — 4 cases (lock-create, contention
    exits 0, lock-release on holder death, distinct names don't
    contend).
  - Existing bridge tests still pass (telegram-tofu, telegram-allowlist,
    slack-access, discord-allowlist).
  - `pnpm typecheck` clean (no TS surface touched).

This phase explicitly chose only single_instance.py from the 12
OSS-only files; the rest (result_channel_key, task_archive,
voice-key, voice-config*, send_allowlist, discord_config,
overlay-manager-ui, morning-briefing, obsidian-mirror) are either
covered inline in stando, conflict with stando's architecture, or
are net-new features that deserve their own dedicated phase.

Co-authored-by: Cursor <cursoragent@cursor.com>
@jgluck96
jgluck96 merged commit 9e5571a into dev-client Jun 1, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant