Skip to content

Commit 1114835

Browse files
authored
Merge pull request #60 from kojibai/kai_cadence
kai cadence update with sigil explorer kai pulse
2 parents 4adeb60 + dc4bf4e commit 1114835

3 files changed

Lines changed: 287 additions & 17 deletions

File tree

src/components/SigilExplorer.tsx

Lines changed: 84 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,35 @@ const hasWindow = typeof window !== "undefined";
180180
const canStorage = hasWindow && typeof window.localStorage !== "undefined";
181181

182182
/** KKS-1.0 φ-breath pulse cadence (ms). Used ONLY for cadence, never ordering. */
183-
const PULSE_POLL_MS = 5236;
183+
/** KKS-1.0 φ-exact breath (seconds). */
184+
const KAI_BREATH_SEC = 3 + Math.sqrt(5);
185+
/** Breath duration in ms (≈ 5236.0679ms). Used ONLY to schedule, never to order. */
186+
const KAI_BREATH_MS = KAI_BREATH_SEC * 1000;
187+
188+
/**
189+
* Genesis epoch (bridge only) used to phase-lock “pulse ticks” to breath boundaries.
190+
* Ordering still uses payload pulse/beat/stepIndex; this is only the wake cadence.
191+
*/
192+
const KAI_GENESIS_EPOCH_MS = 1715323541888;
193+
194+
/** Timer guards (avoid 0ms storms / long sleeps). */
195+
const KAI_TIMER_MIN_MS = 25;
196+
const KAI_TIMER_MAX_MS = 30_000;
197+
198+
function msUntilNextKaiBreath(now = nowMs()): number {
199+
const dt = now - KAI_GENESIS_EPOCH_MS;
200+
if (!Number.isFinite(dt)) return 5236;
201+
202+
const breaths = dt / KAI_BREATH_MS;
203+
const nextBreathIndex = Math.floor(breaths) + 1;
204+
const nextAt = KAI_GENESIS_EPOCH_MS + nextBreathIndex * KAI_BREATH_MS;
205+
206+
const ms = nextAt - now;
207+
const safe = Number.isFinite(ms) ? ms : 5236;
208+
209+
return Math.min(KAI_TIMER_MAX_MS, Math.max(KAI_TIMER_MIN_MS, Math.round(safe)));
210+
}
211+
184212

185213
/** Remote pull limits. */
186214
const URLS_PAGE_LIMIT = 5000;
@@ -2863,17 +2891,61 @@ const SigilExplorer: React.FC = () => {
28632891
seedInhaleFromRegistry();
28642892
void syncOnce("open");
28652893

2866-
const intervalId = window.setInterval(() => void syncOnce("pulse"), PULSE_POLL_MS);
2894+
let breathTimer: number | null = null;
28672895

2868-
const onVis = () => {
2869-
if (document.visibilityState === "visible") void syncOnce("visible");
2870-
};
2871-
document.addEventListener("visibilitychange", onVis);
2896+
const scheduleNextBreath = (): void => {
2897+
if (!hasWindow) return;
2898+
if (unmounted.current) return;
2899+
2900+
if (breathTimer != null) window.clearTimeout(breathTimer);
2901+
2902+
const delay = msUntilNextKaiBreath();
2903+
breathTimer = window.setTimeout(() => {
2904+
breathTimer = null;
2905+
2906+
// Stay phase-locked, but don’t do network work while hidden/offline.
2907+
if (document.visibilityState !== "visible") {
2908+
scheduleNextBreath();
2909+
return;
2910+
}
2911+
if (!isOnline()) {
2912+
scheduleNextBreath();
2913+
return;
2914+
}
2915+
2916+
void syncOnce("pulse");
2917+
scheduleNextBreath(); // re-locks every tick → no drift
2918+
}, delay);
2919+
};
2920+
2921+
const resnapBreath = (): void => {
2922+
// Re-phase immediately off “now”
2923+
scheduleNextBreath();
2924+
};
2925+
2926+
// Start breath scheduler (phase-locked) after open sync is kicked
2927+
scheduleNextBreath();
2928+
2929+
const onVis = () => {
2930+
if (document.visibilityState === "visible") {
2931+
resnapBreath();
2932+
void syncOnce("visible");
2933+
}
2934+
};
2935+
document.addEventListener("visibilitychange", onVis);
2936+
2937+
const onFocus = () => {
2938+
resnapBreath();
2939+
void syncOnce("focus");
2940+
};
2941+
2942+
const onOnline = () => {
2943+
resnapBreath();
2944+
void syncOnce("online");
2945+
};
28722946

2873-
const onFocus = () => void syncOnce("focus");
2874-
const onOnline = () => void syncOnce("online");
2875-
window.addEventListener("focus", onFocus);
2876-
window.addEventListener("online", onOnline);
2947+
window.addEventListener("focus", onFocus);
2948+
window.addEventListener("online", onOnline);
28772949

28782950
return () => {
28792951
if (window.__SIGIL__) window.__SIGIL__.registerSigilUrl = prev;
@@ -2888,7 +2960,8 @@ const SigilExplorer: React.FC = () => {
28882960
if (typeof unsubClaims === "function") unsubClaims();
28892961
if (flushTimerRef.current != null) window.clearTimeout(flushTimerRef.current);
28902962
flushTimerRef.current = null;
2891-
window.clearInterval(intervalId);
2963+
if (breathTimer != null) window.clearTimeout(breathTimer);
2964+
breathTimer = null;
28922965
ac.abort();
28932966
unmounted.current = true;
28942967
};

src/main.tsx

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ import "./App.css";
88
import AppRouter from "./router/AppRouter";
99
import { APP_VERSION, SW_VERSION_EVENT } from "./version";
1010

11+
// ✅ REPLACE scheduler impl with your utils cadence file
12+
import { startKaiCadence } from "./utils/kai_cadence";
13+
1114
const isProduction = import.meta.env.MODE === "production";
1215

1316
declare global {
@@ -43,7 +46,6 @@ if (isProduction) {
4346
window.addEventListener("DOMContentLoaded", rewriteLegacyHash, { once: true });
4447
}
4548

46-
4749
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
4850
<React.StrictMode>
4951
<AppRouter />
@@ -93,11 +95,14 @@ if ("serviceWorker" in navigator && isProduction) {
9395
}
9496
});
9597

96-
// Periodically ask the browser to re-check the SW script to reduce staleness on mobile
97-
const ONE_HOUR = 60 * 60 * 1000;
98-
window.setInterval(() => {
99-
reg.update().catch(() => {});
100-
}, ONE_HOUR);
98+
// ✅ REPLACES the hour interval: Kai beat cadence via utils
99+
startKaiCadence({
100+
unit: "beat",
101+
every: 1, // "do a beat"
102+
onTick: async () => {
103+
await reg.update();
104+
},
105+
});
101106

102107
console.log("Kairos Service Worker registered:", reg);
103108
} catch (err) {

src/utils/kai_cadence.ts

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
// src/utils/kai_cadence.ts
2+
// Kai-native cadence scheduler: beat/pulse intervals with Fibonacci backoff.
3+
// - NO Date.now
4+
// - NO setInterval backlog (uses recursive setTimeout)
5+
// - Mobile reality: resync on foreground, optional pause when hidden
6+
// - Deterministic cadence in Kai units (as constants), not hours
7+
8+
import { PULSE_MS, PULSES_BEAT } from "./kai_pulse";
9+
10+
export type KaiCadenceUnit = "pulse" | "beat";
11+
12+
export type KaiCadenceOptions = {
13+
unit: KaiCadenceUnit;
14+
// Every N units. For SW updates you’ll likely use unit="beat", every=1.
15+
every: number;
16+
17+
// If true, no timers run while hidden; resumes cleanly on visible.
18+
pauseWhenHidden?: boolean;
19+
20+
// Called each tick. Throwing/rejecting can be used by wrappers to backoff.
21+
onTick: () => void | Promise<void>;
22+
};
23+
24+
export type KaiCadenceHandle = {
25+
stop: () => void;
26+
};
27+
28+
const clampInt = (n: number, min: number, max: number): number => {
29+
const x = Math.floor(n);
30+
if (x < min) return min;
31+
if (x > max) return max;
32+
return x;
33+
};
34+
35+
const MIN_DELAY_MS = 250;
36+
const MAX_DELAY_MS = 2 ** 31 - 1; // browser clamp for setTimeout
37+
38+
function unitMs(unit: KaiCadenceUnit): number {
39+
if (unit === "pulse") return PULSE_MS;
40+
// beat = pulses/beat * pulse_ms
41+
return PULSE_MS * PULSES_BEAT;
42+
}
43+
44+
export function startKaiCadence(opts: KaiCadenceOptions): KaiCadenceHandle {
45+
const pauseWhenHidden = opts.pauseWhenHidden !== false; // default true
46+
const every = clampInt(opts.every, 1, 1_000_000);
47+
48+
const baseMs = unitMs(opts.unit);
49+
const steadyDelay = clampInt(every * baseMs, MIN_DELAY_MS, MAX_DELAY_MS);
50+
51+
let disposed = false;
52+
let timer: number | null = null;
53+
54+
const clear = (): void => {
55+
if (timer !== null) window.clearTimeout(timer);
56+
timer = null;
57+
};
58+
59+
const schedule = (delayMs: number): void => {
60+
if (disposed) return;
61+
const d = clampInt(delayMs, MIN_DELAY_MS, MAX_DELAY_MS);
62+
timer = window.setTimeout(() => void tick(), d);
63+
};
64+
65+
const tick = async (): Promise<void> => {
66+
if (disposed) return;
67+
68+
if (pauseWhenHidden && document.visibilityState !== "visible") {
69+
// Don’t spin in background; re-armed on visibilitychange.
70+
schedule(steadyDelay);
71+
return;
72+
}
73+
74+
try {
75+
await opts.onTick();
76+
} finally {
77+
// Always continue cadence; caller can implement backoff externally.
78+
schedule(steadyDelay);
79+
}
80+
};
81+
82+
// Start cadence immediately (first tick after steadyDelay)
83+
schedule(steadyDelay);
84+
85+
const onVis = (): void => {
86+
if (document.visibilityState !== "visible") return;
87+
clear();
88+
// Foreground: tick soon (but not immediately) then resume steady cadence
89+
schedule(Math.min(steadyDelay, 1000));
90+
};
91+
92+
document.addEventListener("visibilitychange", onVis, { passive: true });
93+
94+
return {
95+
stop: () => {
96+
disposed = true;
97+
clear();
98+
document.removeEventListener("visibilitychange", onVis);
99+
},
100+
};
101+
}
102+
103+
// Fibonacci helper for backoff schedules (1,2,3,5,8,13,...)
104+
export const KAI_FIB: readonly number[] = [
105+
1, 2, 3, 5, 8, 13, 21, 34,
106+
] as const;
107+
108+
export type KaiFibBackoffOptions = {
109+
unit: KaiCadenceUnit;
110+
// Fibonacci list index cap (default last index)
111+
maxIndex?: number;
112+
pauseWhenHidden?: boolean;
113+
114+
// On success: reset to fib[0]
115+
onSuccess?: () => void;
116+
// On failure: step to next fib index
117+
onFailure?: () => void;
118+
119+
// Actual work (e.g., reg.update)
120+
work: () => Promise<void>;
121+
};
122+
123+
export function startKaiFibBackoff(opts: KaiFibBackoffOptions): KaiCadenceHandle {
124+
const pauseWhenHidden = opts.pauseWhenHidden !== false; // default true
125+
const maxIdx = clampInt(
126+
typeof opts.maxIndex === "number" ? opts.maxIndex : KAI_FIB.length - 1,
127+
0,
128+
KAI_FIB.length - 1
129+
);
130+
131+
const baseMs = unitMs(opts.unit);
132+
133+
let disposed = false;
134+
let timer: number | null = null;
135+
let idx = 0;
136+
137+
const clear = (): void => {
138+
if (timer !== null) window.clearTimeout(timer);
139+
timer = null;
140+
};
141+
142+
const scheduleNext = (): void => {
143+
if (disposed) return;
144+
145+
const beats = KAI_FIB[idx] ?? 1;
146+
const delay = clampInt(beats * baseMs, MIN_DELAY_MS, MAX_DELAY_MS);
147+
148+
timer = window.setTimeout(() => {
149+
void tick();
150+
}, delay);
151+
};
152+
153+
const tick = async (): Promise<void> => {
154+
if (disposed) return;
155+
156+
if (pauseWhenHidden && document.visibilityState !== "visible") {
157+
scheduleNext();
158+
return;
159+
}
160+
161+
try {
162+
await opts.work();
163+
idx = 0;
164+
opts.onSuccess?.();
165+
} catch {
166+
idx = Math.min(idx + 1, maxIdx);
167+
opts.onFailure?.();
168+
}
169+
170+
scheduleNext();
171+
};
172+
173+
scheduleNext();
174+
175+
const onVis = (): void => {
176+
if (document.visibilityState !== "visible") return;
177+
clear();
178+
// Foreground: poke once, then resume schedule
179+
void opts.work().catch(() => {});
180+
scheduleNext();
181+
};
182+
183+
document.addEventListener("visibilitychange", onVis, { passive: true });
184+
185+
return {
186+
stop: () => {
187+
disposed = true;
188+
clear();
189+
document.removeEventListener("visibilitychange", onVis);
190+
},
191+
};
192+
}

0 commit comments

Comments
 (0)