slackwater-tminus
Predict-and-confirm timing that replaces polling. Declare future events with predicted completion times in beats. Subscribers confirm readiness. When quorum is met and the countdown reaches zero, precompiled scripts fire — zero latency, one notification, no polling. Countdowns are measured in beats, not seconds, integrating directly with slackwater-tempo's BeatClock.
Installation
pip install slackwater-tminus
Core Concept
Polling: "Is it done yet? Is it done yet? Is it done yet?" — N messages.
T-Minus: "It will be done at beat 16." + "Confirmed." — 2 messages. The fire is just the trigger pull.
For a 60-second job polled at 0.5s intervals, T-Minus saves 118 messages (60× reduction).
API Reference
CountdownEvent
from slackwater_tminus import CountdownEvent, CountdownState
CountdownEvent(
name: str,
predicted_beat: float,
id: str = <auto>,
quorum: int = 1,
script: Callable | None = None,
metadata: dict = {},
)
The core primitive. A future event with a predicted completion beat.
State machine:
PENDING → PREDICTED → CONFIRMED → FIRED
↘ ↘
MISSED MISSED
| State | Meaning |
|---|---|
PENDING |
Event declared, prediction not committed |
PREDICTED |
Prediction committed, awaiting confirmations |
CONFIRMED |
Quorum reached, will fire at predicted beat |
FIRED |
Beat arrived, scripts executed, subscribers notified |
MISSED |
Prediction wrong or quorum not reached in time |
Lifecycle methods:
event.commit_prediction() -> None
event.subscribe(subscriber_id: str) -> None # auto-commits if PENDING
event.confirm(subscriber_id: str) -> bool # True if quorum reached
event.defer(subscriber_id: str, reason: str = "") -> None
event.miss(subscriber_id: str = "", reason: str = "") -> None
event.fire(actual_beat: float | None = None) -> Any # executes script
event.force_miss(reason: str = "forced") -> None
Properties:
event.state -> CountdownState
event.quorum_met -> bool
event.is_terminal -> bool # FIRED or MISSED
event.accuracy -> float | None # 1.0 = perfect, None if not fired
event.subscribers -> frozenset[str]
event.confirmations -> frozenset[str]
event.deferrals -> dict[str, str]
event.missed_by -> frozenset[str]
event.result -> Any # script return value
Accuracy formula:
accuracy = max(0.0, 1.0 − |predicted_beat − actual_beat| / max(predicted_beat, 1.0))
TMinusPredictor
from slackwater_tminus import TMinusPredictor, PredictionResult
TMinusPredictor(
bpm: float = 60.0,
start_beat: float = 0.0,
)
Orchestrates countdown events. Maintains a beat timeline, advances through it, and fires events when their predicted beats arrive.
Prediction:
event = predictor.predict(
name: str,
beats_ahead: float,
*,
quorum: int = 1,
script: Callable | None = None,
confidence: float = 0.8,
metadata: dict | None = None,
) -> CountdownEvent
Creates an event at current_beat + beats_ahead and immediately commits to PREDICTED.
Advancing time:
fired: list[CountdownEvent] = predictor.advance(beats: float)
fired: list[CountdownEvent] = predictor.tick() # shortcut for advance(1)
Returns events that FIRED at this tick. Events whose predicted beat arrived with quorum met → FIRED. Without quorum → MISSED.
Queries:
predictor.predict_next() -> CountdownEvent | None # nearest pending
predictor.countdown_beats() -> float | None # beats to next event
predictor.countdown_seconds() -> float | None # wall-clock estimate
predictor.get(event_id) -> CountdownEvent | None
predictor.get_by_name(name: str) -> list[CountdownEvent]
predictor.pending_events -> list[CountdownEvent]
predictor.fired_events -> list[CountdownEvent]
predictor.missed_events -> list[CountdownEvent]
predictor.avg_accuracy -> float
Calibration:
predictor.calibrate() -> dict[str, float]
Returns avg_accuracy, avg_lead_time_beats, fire_rate, total_predictions, fired, missed.
predictor.message_savings(polling_interval_s: float = 0.5) -> dict[str, int]
Compares T-Minus message count vs polling. Returns polling_messages, tminus_messages, savings_ratio.
PrecompiledScript
from slackwater_tminus import PrecompiledScript
from slackwater_tminus.precompiled import compile
PrecompiledScript(
action: Callable[..., Any],
label: str = "",
args: tuple = (),
kwargs: dict = {},
)
# Convenience function
script = compile("tower_build", build_function, height=10, material="stone)
Actions attached to predictions, ready for zero-latency execution. The work of figuring out WHAT to do is done during the countdown. The fire is the trigger pull.
script.execute() -> Any # idempotent — returns cached result
script.is_executed -> bool
script.result -> Any
script.latency_ms -> float | None # lead time: compile→execute
Subscriber
from slackwater_tminus import Subscriber, SubscriberState
Subscriber(id: str)
A participant in the predict-and-confirm cycle. Tracks state across multiple events independently.
Subscriber state machine (per event):
UNINFORMED → PENDING → CONFIRMED → NOTIFIED
↘ ↘
DEFERRED MISSED
sub.subscribe(event_id: str) -> None
sub.confirm(event_id: str) -> SubscriberState # → CONFIRMED
sub.defer(event_id: str, reason: str = "") -> SubscriberState # → DEFERRED
sub.miss(event_id: str, reason: str = "") -> SubscriberState # → MISSED (terminal)
sub.notify(event_id: str) -> SubscriberState # → NOTIFIED
Bulk operations:
sub.confirm_all(event_ids: list[str]) -> None
sub.stats() -> dict[str, int] # counts by state name
Properties:
sub.event_ids -> frozenset[str]
sub.confirmed_events -> frozenset[str]
sub.deferred_events -> frozenset[str]
sub.missed_events -> frozenset[str]
sub.state_for(event_id) -> SubscriberState
BeatClock
from slackwater_tminus import BeatClock
BeatClock(bpm: float = 60.0, current_beat: float = 0.0)
A clock that counts beats instead of seconds. Can auto-sync to wall-clock time or be advanced manually.
clock.sync() -> float # sync with wall-clock time
clock.advance(beats: float) -> float # manual advance
clock.beats_to_seconds(beats: float) -> float
clock.seconds_to_beats(seconds: float) -> float
clock.set_bpm(bpm: float, *, resync: bool = True) -> None
clock.beat_duration -> float # 60.0 / bpm
BeatCountdown
from slackwater_tminus import BeatCountdown
BeatCountdown(clock: BeatClock | None = None, bpm: float = 60.0)
Thin wrapper around TMinusPredictor using a BeatClock for timing. All durations are in beats. Clock and events are coupled — advancing the clock fires events.
event = bc.schedule("build_complete", beats=16, quorum=1) -> CountdownEvent
bc.subscribe(event_id, subscriber_id) -> CountdownEvent
bc.confirm(event_id, subscriber_id) -> bool # True if quorum reached
bc.defer(event_id, subscriber_id, reason) -> None
bc.advance(beats) -> list[CountdownEvent] # advances clock + predictor
bc.tick() -> list[CountdownEvent] # advance one beat
bc.sync() -> list[CountdownEvent] # wall-clock sync
bc.remaining_beats(event_id) -> float | None
bc.remaining_seconds(event_id) -> float | None
Examples
Full predict → confirm → fire cycle
from slackwater_tminus import TMinusPredictor, PrecompiledScript
predictor = TMinusPredictor(bpm=60)
script = PrecompiledScript(
action=lambda: print("Castle complete!"),
label="castle_build",
)
event = predictor.predict("castle_complete", beats_ahead=16, script=script.execute)
event.subscribe("player_1")
event.confirm("player_1") # quorum=1, so this confirms
assert event.state.name == "CONFIRMED"
fired = predictor.advance(16)
assert event in fired
assert script.is_executed # script ran with zero planning latency
Quorum with multiple subscribers
predictor = TMinusPredictor(bpm=60)
event = predictor.predict("consensus_vote", beats_ahead=32, quorum=3)
for agent in ["alice", "bob", "carol"]:
event.subscribe(agent)
event.confirm(agent)
# After all 3 confirm → CONFIRMED. Advance to fire.
fired = predictor.advance(32)
Beat-space scheduling with tempo change
from slackwater_tminus import BeatCountdown
bc = BeatCountdown(bpm=60)
event = bc.schedule("build", beats=16)
bc.confirm(event.id, "a")
bc.advance(8) # halfway
print(bc.remaining_beats(event.id)) # 8.0
bc.clock.set_bpm(120, resync=False) # tempo doubles
print(bc.remaining_seconds(event.id)) # 4.0 (halved)
print(bc.remaining_beats(event.id)) # 8.0 (unchanged — beats are tempo-relative)
fired = bc.advance(8) # fire
Message savings vs polling
predictor = TMinusPredictor(bpm=60)
event = predictor.predict("long_job", beats_ahead=60) # 60-second job
event.subscribe("a")
event.confirm("a")
predictor.advance(60)
savings = predictor.message_savings(polling_interval_s=0.5)
print(savings)
# {'polling_messages': 120, 'tminus_messages': 2, 'savings_ratio': 60.0}
License
MIT
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file slackwater_tminus-0.1.0.tar.gz.
File metadata
- Download URL: slackwater_tminus-0.1.0.tar.gz
- Upload date:
- Size: 23.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.14.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
475089ab7430c3048d01c79e9be258bcde1e9b7e645b33c55d0c8fd4e5481703
|
|
| MD5 |
dd75e1abcfb5e787ce5e3baca08222b4
|
|
| BLAKE2b-256 |
4699e0ead490ab18e7138fcac1fd22144206876acaeb2c63ff7c1420594473cd
|
File details
Details for the file slackwater_tminus-0.1.0-py3-none-any.whl.
File metadata
- Download URL: slackwater_tminus-0.1.0-py3-none-any.whl
- Upload date:
- Size: 18.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.14.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
503f298ef59b1683645211fd18aac4b5efb4bceb096fc844ef315043771c61da
|
|
| MD5 |
7857cf718cab06019ea35c8006e97078
|
|
| BLAKE2b-256 |
7a06f355ab18c35fea8d819b06e46b95e1241bd9cd6127d94b32de8d17ec055e
|