Skip to main content

slackwater-tminus

tests version python

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

slackwater_tminus-0.1.0.tar.gz (23.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

slackwater_tminus-0.1.0-py3-none-any.whl (18.5 kB view details)

Uploaded Python 3

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

Hashes for slackwater_tminus-0.1.0.tar.gz
Algorithm Hash digest
SHA256 475089ab7430c3048d01c79e9be258bcde1e9b7e645b33c55d0c8fd4e5481703
MD5 dd75e1abcfb5e787ce5e3baca08222b4
BLAKE2b-256 4699e0ead490ab18e7138fcac1fd22144206876acaeb2c63ff7c1420594473cd

See more details on using hashes here.

File details

Details for the file slackwater_tminus-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for slackwater_tminus-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 503f298ef59b1683645211fd18aac4b5efb4bceb096fc844ef315043771c61da
MD5 7857cf718cab06019ea35c8006e97078
BLAKE2b-256 7a06f355ab18c35fea8d819b06e46b95e1241bd9cd6127d94b32de8d17ec055e

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page