Design Notes¶
Why hypeman-social is shaped the way it is. Most of these decisions were paid for in production before they became rules.
Origin: three copies, drifting¶
Boon-Tube-Daemon, stream-daemon, and Star-Daemon each carried their own copies of the same social-posting and LLM code — roughly 55–65% identical line-for-line, with the LLM layer sharing 352 identical lines across files that had already diverged. A bug fixed in one daemon never reached the others. This library exists so a fix lands once and protects everyone.
The library deliberately knows nothing about what is being announced. Videos, streams, stars, jokes — that's the caller's business. What's shared is the shouting: where to post, how to survive failure, and how to keep a small model honest.
The availability contract¶
Never branch on .enabled. Always call is_available().
The outage that motivated the extraction: an auto-reconnect fix for a local
Ollama server was written for one daemon and never reached the other — and in
the daemon that had it, the fix was unreachable, because every call site
gated on an enabled flag that the failure path set to False and nothing
ever set back. Taking the AI box offline meant no AI until a human noticed
and restarted the process.
The lesson generalizes: a gate that can only ever go from working to broken is not a health check, it's a latch. So in hypeman:
is_available()is allowed to heal: if the provider is down but configuration is intact, it attempts a cooldown-guarded reconnect and reports the result. The daemon's own poll loop asking "can I use AI?" is the thing that fixes the connection.- A failed
authenticate()is "not right now", never "never". Config is retained so recovery has a target. heartbeat()runs the same probe on a timer, so recovery is noticed between announcements — the first post after an outage uses AI, not a template.- Recovery is paced (
LLM_RECONNECT_INTERVAL, default 60s) and by default never gives up (LLM_MAX_RECONNECT_ATTEMPTS=0).
Degraded is not dead¶
An LLM outage must cost you message quality, not messages. Every layer enforces this:
LLMManager.generate()returning None is a normal, expected state; every daemon keeps a template path.HealthState.is_healthy()ignores provider-supplied status: a daemon posting template messages because the AI box is offline reports healthy. A health check that cries wolf gets ignored, and then it misses the real outage.SocialPlatform.safe_post()never raises: one network having a bad day doesn't take down the daemon or block the others.- Optional SDKs fail closed at
authenticate()with an install hint — never at import. A daemon that only posts to Discord must not need atproto on disk. (CI proves this with a bare-install job.)
Failover is opt-in, and it self-heals backwards¶
Ollama and Gemini have complementary failure modes — the local box dies to
floods, power cuts, and GPU driver updates; the cloud dies to rate limits
and outages. They rarely die together. So the manager supports a fallback
chain (LLM_FALLBACK_PROVIDER), with two deliberate properties:
- Strictly opt-in. If you chose Ollama specifically so your titles stay on your network, silently shipping prompts to Google on a hiccup would be a betrayal, not a feature. No fallback happens unless configured.
- The primary wins back automatically. The manager keeps probing a downed primary while the fallback covers, and switches back the moment it recovers — coming home to find you're still paying Google two weeks after the local box came back would be its own kind of annoying.
Deduplication state lives on the manager, not a provider, so failing over doesn't wipe the memory of what you just posted.
Guardrails: fact-check the robot¶
Small local models (the 4B–12B class this library targets) reliably fail in the same ways: they invent details, pad with filler, and wrap answers in meta-chatter. The guardrails are tuned to those failure modes:
- Hallucination patterns are per-domain (
ContentProfile). A stream announcement inventing "VOD coming soon" is a different lie than a video announcement inventing "live now", and a starred-repo post inventing "50k stars" is a uniquely embarrassing way to be wrong in public. Rather than fork the guardrails per daemon, they're parameterized — and one hallucination match rejects the message outright, because posting "giveaway tonight at 8pm" when there is no giveaway is worse than posting nothing. - Hard checks vs. soft scores. Fabrications, wrong hashtag counts, and
inline URLs are hard failures. Style issues (generic phrases, weak
title relevance) only lower a score, and scoring is off by default —
strictness there is the caller's choice. Stream-daemon, for instance,
deliberately ships a message with minor style issues rather than staying
silent, and uses the individual
guardrails.*functions to build its own lenient retry flow. - URLs are stripped from content and appended after validation — links belong in facets/embeds, and a model-invented URL should never survive.
Config: two chains, different priorities¶
Plain settings and secrets deliberately resolve differently:
- Settings prefer the local environment (simple key, then sectioned) so a developer's shell wins during debugging.
- Secrets prefer the manager (Doppler → AWS → Vault → env) so a
production credential rotation genuinely overrides whatever stale value a
.envfile still carries. This ordering fixed a real bug that stream-daemon's own test suite had been flagging.
Both chains treat empty strings and YOUR_* template placeholders as unset,
and a secrets manager being unreachable degrades to env lookups instead of
crashing.
Explicit event kinds¶
Discord embeds used to infer upload-vs-live from the platform name, and
"youtube" is ambiguous: Boon-Tube means a new upload, stream-daemon means a
live broadcast. Guessing gave one of them the wrong embed. Now the caller
states event_kind in the payload, and a platform default
(DiscordPlatform(default_event_kind=...)) only applies when the payload
doesn't say. The general rule: when a value is ambiguous at the callee,
make the caller say it.
Versioning and compatibility¶
- Public API is everything documented in API.md;
_-prefixed names may change without notice. - Semantic versioning; while on
0.x, minor bumps may break, so daemons pin>=0.1,<0.2style ranges. - Python 3.9+ — CI runs the matrix through 3.13, plus a bare-install job
and a build/
twine checkgate, with all actions pinned by SHA.