This repository was archived by the owner on Jul 16, 2026. It is now read-only.
Repository navigation
feat(bridges): single-instance flock guard for telegram/discord/slack (Phase 5.5) - #75
Merged
Merged
Conversation
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>
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 subscribe to this conversation on GitHub.
Already have an account?
Sign in.
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.
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.shracing 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
fcntl.LOCK_EX | LOCK_NBon a workspace-local lock file at<workspace>/state/locks/<name>.lock.Adoption (one line per bridge)
telegram-bridge.pymain()slack-bridge.pymain()discord-bridge.pyclient.run(...)branch — NOT thesendREST-shortcut branch, sodm-result.py-style one-shot sends still work alongside a running bridgeThe OSS
single_instance.pyis already wired toworkspace_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/:result_channel_key.py/result-channel-key.tstask_archive.pyclaim_taskmechanism stando doesn't have.voice-key.tsvoice-agent.tsalready does theGEMINI_VOICE_API_KEY → GEMINI_API_KEYchain inline; payoff requires refactoring discord-voice + phone-conversation skill scripts (separate effort).voice-config.ts/voice-config-switch.tssend_allowlist.pydiscord-bridge.py);dm-result.pydoesn'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.pyaccess.json → discord-config.jsonmigration path. Standalone feature, dedicated phase.morning-briefing.pymorning-briefingskill; standalone cron script may not be needed. User decision.obsidian-mirror.pySUTANDO_OBSIDIAN_MIRRORenv var. User decision.overlay-manager-ui.tsTest plan
tests/single-instance.test.py— 4 cases:telegram-bridge-tofu,telegram-bridge-allowlist,slack-bridge-access,discord-bridge-allowlist)pnpm run typecheckclean (no TS surface touched)Made with Cursor