agents-pod 0.3.5 → 0.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/cadence.ts ADDED
@@ -0,0 +1,67 @@
1
+ import type { WakeUpEntry } from "./registry.ts";
2
+
3
+ /**
4
+ * Cadence: what the wake-up record says about *when* a Pod woke, read the same
5
+ * way by `status` (one Pod), `doctor` (the fleet's rate) and the daily ceiling
6
+ * (ADR 0032). One module because the three must agree on what counts, and
7
+ * because "41 wake-ups in 5 h" was the whole incident and nothing in the store
8
+ * could say it in one command (issue #35).
9
+ *
10
+ * A record written before ADR 0032 carries no `at`: those wake-ups are counted
11
+ * (they are history) but never timed, and every reader says so rather than
12
+ * inventing a timestamp.
13
+ */
14
+
15
+ export const HOUR_MS = 3_600_000;
16
+ export const DAY_MS = 24 * HOUR_MS;
17
+
18
+ /** A wake-up entry that knows when it happened — the only kind a rate counts. */
19
+ export interface StampedWakeUp extends WakeUpEntry {
20
+ at: number;
21
+ }
22
+
23
+ export function stamped(wakeUps: readonly WakeUpEntry[]): StampedWakeUp[] {
24
+ return wakeUps.filter(
25
+ (wake): wake is StampedWakeUp => typeof wake.at === "number" && Number.isFinite(wake.at),
26
+ );
27
+ }
28
+
29
+ /**
30
+ * The stamped wake-ups inside a window ending now (half-open: `(now - ms, now]`).
31
+ * Every outcome counts, `lost` included: a wake-up whose wrapper died still
32
+ * spent an attempt, and a flapping machine that is called busy is exactly the
33
+ * report this is meant to prevent.
34
+ */
35
+ export function inWindow(
36
+ wakeUps: readonly WakeUpEntry[],
37
+ now: number,
38
+ windowMs: number,
39
+ ): StampedWakeUp[] {
40
+ const from = now - windowMs;
41
+ return stamped(wakeUps).filter((wake) => wake.at > from && wake.at <= now);
42
+ }
43
+
44
+ /**
45
+ * The median interval between consecutive stamped wake-ups, in ms — the one
46
+ * number that separates "it works when asked" from "it is re-arming itself".
47
+ * `undefined` when fewer than two wake-ups are timed.
48
+ */
49
+ export function medianIntervalMs(wakeUps: readonly WakeUpEntry[]): number | undefined {
50
+ const times = stamped(wakeUps)
51
+ .map((wake) => wake.at)
52
+ .sort((left, right) => left - right);
53
+ if (times.length < 2) return undefined;
54
+ const gaps: number[] = [];
55
+ for (let index = 1; index < times.length; index += 1) gaps.push(times[index]! - times[index - 1]!);
56
+ gaps.sort((left, right) => left - right);
57
+ const middle = Math.floor(gaps.length / 2);
58
+ return gaps.length % 2 === 1 ? gaps[middle]! : Math.round((gaps[middle - 1]! + gaps[middle]!) / 2);
59
+ }
60
+
61
+ /** Ages and intervals are read at a glance, never as an epoch (ADR 0027's habit). */
62
+ export function duration(ms: number): string {
63
+ if (ms < 1_000) return `${Math.max(0, Math.round(ms))}ms`;
64
+ if (ms < 60_000) return `${Math.round(ms / 1000)}s`;
65
+ if (ms < HOUR_MS) return `${Math.round(ms / 60_000)}m`;
66
+ return `${Math.round((ms / HOUR_MS) * 10) / 10}h`;
67
+ }