Files
SoulStream/handoff/tools/patch-adaptive.cjs
T
KeelrLeek 9f0601027c android: the handoff — the pending rounds, the pipeline, and what is unverified
Everything written this session that has not been through the device: rounds 43
to 48 (each patch adversarially verified, 43's transient-notice expectation
fixed), a partial round 49, the pipeline scripts, the Android Auto design and
the icon comparison mock. Also the two harness changes that were never A/B'd —
adaptive waits, which may only shorten a declared wait and never lengthen it,
and a ship that runs the mock walk and the deploy at the same time. The A/B and
its 255-step baseline are in handoff/tools; main stays at what the phone has
actually walked.
2026-09-14 07:57:41 -05:00

107 lines
5.3 KiB
JavaScript

// Adaptive waits for the walk. Every step used to sleep a fixed interval
// sized for its worst case — 414 seconds of them in the mock list and 223 in
// the phone's, over half of a round's twenty-three minutes. This polls a
// fingerprint of the page and the app's own state instead, and goes on once
// both have stopped moving.
//
// Applied on its own, after a chain clears, so it never rides inside a
// round's commit.
const fs = require('fs');
const root = process.env.SS_ROOT || 'F:/Coding/SoulStream/';
let p, s;
const load = (file) => { p = file; s = fs.readFileSync(root + p, 'utf8'); };
const save = () => fs.writeFileSync(root + p, s);
const once = (a) => {
const i = s.indexOf(a);
if (i < 0) throw new Error('missing in ' + p + ': ' + a.slice(0, 90));
if (s.indexOf(a, i + 1) >= 0) throw new Error('ambiguous in ' + p + ': ' + a.slice(0, 90));
return i;
};
const rep = (a, b) => { once(a); s = s.replace(a, () => b); };
load('tests/mobile/smoke.mjs');
if (s.includes('function settle(')) throw new Error('the walk already waits adaptively — nothing to do');
const A_LOOP = ` for (const s of steps) {`;
const A_SLEEP = ` await sleep(s.wait || 500);`;
once(A_LOOP);
once(A_SLEEP);
rep(A_LOOP, ` /* ── waiting only as long as it takes ───────────────────────────
Every step used to sleep its declared wait, sized for the worst
case it might ever meet. At 632 steps that was over half of a
round: 414 seconds declared in the mock list, 223 in the phone's.
So: poll something cheap until it stops changing, then go on. The
fingerprint reads the shape of the page AND the app's own moving
parts, because a step can be waiting on work that has not reached
the DOM yet — a resolve in flight, a transfer arriving, a command
that will only show itself in a toast.
Two properties keep this honest:
· It can only ever SHORTEN a wait, never lengthen one. The
declared wait is the ceiling, so a step that passes today still
gets every millisecond it got before.
· There is a floor. A page can be momentarily quiet in the middle
of work — a backend write with nothing visible behind it looks
settled the instant it is asked — so nothing is probed before a
third of its declared wait has passed.
\`hold: true\` on a step opts out and sleeps the whole wait, for
the handful that are waiting on something nothing here can see. */
const FINGERPRINT = \`(function () {
try {
var t = document.querySelector('.toast');
var ss = window.__ss || {};
var pl = ss.player || {}, rs = ss.resolver || {}, nt = ss.net || {}, lb = ss.lib || {};
return [
document.querySelectorAll('*').length,
document.images.length,
(document.querySelector('.screen') || {}).className || '',
(document.querySelector('.sheet') || {}).className || '',
(document.querySelector('.halfsheet') || {}).className || '',
t ? t.textContent.slice(0, 40) : '',
document.title,
pl.status, pl.phase, Math.round(pl.position || 0),
(pl.queue || []).length, pl.qi,
rs.active ? 1 : 0, nt.busy ? 1 : 0, lb.loading ? 1 : 0,
(ss.transfers && ss.transfers.items ? ss.transfers.items.length : 0)
].join('|');
} catch (e) { return 'unreadable'; }
})()\`;
/* two identical readings 90ms apart, and nothing has moved */
const STABLE_READINGS = 2;
const POLL_MS = 90;
const FLOOR = 0.34;
let waited = 0, saved = 0; /* what the walk actually spent, and what it did not */
async function settle(want, step) {
waited += want;
if (step && step.hold) { await sleep(want); return; }
const began = Date.now();
await sleep(Math.min(want, Math.max(200, Math.round(want * FLOOR))));
let last = null, stable = 0;
while (Date.now() - began < want) {
let fp;
try { fp = await cdp.evaluate(FINGERPRINT); } catch { break; }
if (fp === last) { if (++stable >= STABLE_READINGS) break; } else { stable = 0; last = fp; }
const left = want - (Date.now() - began);
if (left <= 0) break;
await sleep(Math.min(POLL_MS, left));
}
saved += Math.max(0, want - (Date.now() - began));
}
for (const s of steps) {`);
rep(A_SLEEP, ` await settle(s.wait || 500, s);`);
/* The tally, printed where `saved` and `waited` are provably in scope: the
comment below sits in the same block as the loop, at the same indent. */
rep(` /* ── the restart half of session restore, on the phone only ──────`,
` console.log("waits: " + Math.round(saved / 1000) + "s of " + Math.round(waited / 1000) + "s declared was not spent");
/* ── the restart half of session restore, on the phone only ──────`);
save();
console.log('the walk waits adaptively now');