Decisions and Delivery
A decision is the Director's answer to "how hard should I push right
now?". It is computed on demand - when you GET /decision or report
signals with respond: true - from three inputs: the decayed intensity
estimate, the target curve at the session's current cycle position, and
what budget remains.
The rules
With gap = target - current and band the policy's tolerance band:
trigger_climax- phase ispeak, the estimate is at or abovepeak_intensity - band, the peak cooldown (min_seconds_between_peaks) has elapsed, and event budget remains. Costs 1 event budget and stampslast_peak_at.spawn_wave- phase isbuild_up, the estimate is more than one band below the curve, and spawn budget remains.size(1-5) scales with how far below the curve the session is, jittered by the deterministic draw and capped by remaining budget; it costssizespawn budget.ease_off- phase isrelaxand the estimate is more than one band above the curve. Free; carriesease_off_min_secondsas its suggested cooldown.sawtooth_relaxandconstantcurves have no relax segment, so they never emit it - relief comes from the estimate decay instead.hold- everything else. Note the phase gate: a session running hot duringbuild_upholds (there is no ease-off outside the relax phase), and itsrationalestill reads "within tolerance band; holding".
A live spawn decision, captured while the estimate had decayed well below a rising curve:
{
"phase": "build_up",
"current_intensity": 0.27636334177129984,
"target_intensity": 0.8180766666666666,
"action": { "type": "spawn_wave", "size": 5, "cost": 5, "cooldown_seconds": 12.0 },
"rationale": "below target curve in build-up phase; spawn budget available",
"seed_draw": 0.5300190292209616,
"decision_index": 2
}Polling is not a pure read
Every decision read advances the session's decision_index and spends
budget when a costed action fires. Two back-to-back polls of the session
above both spawned: budgets went 12 -> 7 -> 3 (verified). Poll on your
game loop's cadence, not per frame - and treat the returned
cooldown_seconds as the earliest you should act again. Budgets
replenish when the session crosses into a new cycle: after this session's
180-second cycle rolled over, budgets_remaining.spawn was back to 12
(verified).
session get and curve are pure reads; only decision computation
mutates the session.
Determinism and replay
Each decision's seed_draw is a deterministic function of the session
seed and decision_index. Two sessions started with --seed 7 produced
the identical draw 0.9096310281224071 at index 0 (verified). With
deterministic.per_session_seed on (the default) an omitted seed is
derived from the session id, so re-running the same policy, seed, and
signal timeline reproduces the same decision sequence - and
GET /sessions/{id}/curve returns the full decision timeline to replay
against. Note that timing still matters: the curve target moves with
elapsed time, so replays are exact only when signals and reads land at
the same offsets.
Push delivery
Instead of only polling, provision push delivery of every computed decision (each session holds one subscription; subscribing again replaces it):
snug director subscribe --session-id docs4-sess-a \
--channel pubsub --target u.5JY5XUIVDLNWX5WV.director.decisionspubsub-targetis a channel name. Channel authorization applies: subscribe within your ownu.{namespace}.prefix (mint a capability token to learn it) or a listen on the channel is rejected with a403handshake - see PubSub. Verified end-to-end: asnug pubsub listenon the channel above received the exact decision JSON the moment a poll computed it.webhook-targetmust be anhttp(s)URL (anything else is400 invalid_target, verified); the Director registers a webhook for you and returns its id in the subscription. A replaced webhook subscription retires the old webhook.websocket-targetis an existing WebSocket connection id; decisions arrive as pub/sub-style frames on that connection (frame format in the API reference).
Delivery is best-effort by design: a failed push is logged and never
fails the decision itself, so the pacing loop keeps running when a
downstream sink is unavailable. The decision returned to the caller and
the one pushed are the same object - subscribers received
decision_index 4 exactly as the poller saw it (verified).