daemon/cadence.bend

cswap-bend-cli@0.1.0.0 · 89 lines · 15 definitions

1import Base
2
3# How long the loop sleeps after a tick (autoswitch.py _next_delay and
4# _respect_poll_plan). Bend has no random source, so the +-10 % jitter of the
5# normal cadence is not applied: the delay is the interval itself.
6
7# What a tick came to; the `--once` exit code is `exit_code`.
8
9type Outcome is Data:
10  Switched{}
11  NoAction{}
12  Blocked{}
13  Failed{}
14
15# What the tick tells the sleeper: when the soonest reset is (all exhausted),
16# whether waiting a long time is right (no candidates), whether the active
17# token is expired on an idle Claude Code, and the active row's next poll.
18type Info is Data:
19  Info{outcome: Outcome, sleep_until: Maybe<&2, U32>, blocked_long: Bool, idle_slow: Bool, next_poll_at: Maybe<&2, U32>}
20
21def exit_code(o: Outcome) -> U32:
22  match o:
23    case Switched{}:
24      0
25    case Failed{}:
26      1
27    case NoAction{}:
28      2
29    case Blocked{}:
30      3
31
32def no_reset_fallback() -> U32:
33  300
34
35def max_sleep() -> U32:
36  600
37
38def urgent() -> U32:
39  60
40
41def sel(+c: Bool, +x: U32, +y: U32) -> U32:
42  match c:
43    case True{}:
44      x
45    case False{}:
46      y
47
48def max_u(+a: U32, +b: U32) -> U32:
49  sel(U32.is_gt(a, b), a, b)
50
51def min_u(+a: U32, +b: U32) -> U32:
52  sel(U32.is_lt(a, b), a, b)
53
54# a - b, 0 when b is later.
55def since(+a: U32, +b: U32) -> U32:
56  sel(U32.is_gt(a, b), (a - b : U32), 0)
57
58# Shorten a normal sleep to the active row's own next poll, never under 60 s.
59def respect_plan(+next_poll: Maybe<&2, U32>, +now: U32, +delay: U32) -> U32:
60  match next_poll:
61    case None{}:
62      delay
63    case Some{t}:
64      min_u(delay, max_u(since(t, now), urgent()))
65
66def blocked_delay(+until: Maybe<&2, U32>, +long: Bool, +now: U32, +interval: U32, +normal: U32) -> U32:
67  match until:
68    case Some{t}:
69      min_u(max_u(since(t, now), interval), max_sleep())
70    case None{}:
71      sel(long, max_u(interval, no_reset_fallback()), normal)
72
73def next_delay2(o: Outcome, info: Info, +now: U32, +interval: U32) -> U32:
74  match o info:
75    case Blocked{} Info{oc, until, long, idle, np}:
76      blocked_delay(until, long, now, interval, respect_plan(np, now, interval))
77    case NoAction{} Info{oc, until, long, idle, np}:
78      sel(idle, max_u(interval, no_reset_fallback()), respect_plan(np, now, interval))
79    case _ Info{oc, until, long, idle, np}:
80      respect_plan(np, now, interval)
81
82def next_delay(info: Info, +now: U32, +interval: U32) -> U32:
83  match info:
84    case Info{+oc, until, long, idle, np}:
85      next_delay2(oc, info, now, interval)
86
87# A `sleep` event is emitted only when the delay is over 1.5 intervals.
88def announce(+delay: U32, +interval: U32) -> Bool:
89  U32.is_gt((delay * 2 : U32), (interval * 3 : U32))