feat(chrome): read the page's exceptions and navigations, and hold the picker state across them

a page navigation replaces the runtime, so the seeded picker restarted the seed's stream at its first draw on every reload and a trace could not tell a reload from a generator repeating itself. the driver now drains the main-frame navigations it saw, reports the page's buffered uncaught errors so state.exceptions is the page's list on the goja host too, and carries the picker's draw position out of v8 and back in around each decision.
This commit is contained in:
pj committed 2026-08-16 17:44:29 +05:30
1 parent 45dd52c5d9
commit 944d1e9ad9
8 files changed
+331 -3

No files matched your search

+13
View File
@@ -33,6 +33,19 @@ export class Pcg {
this.lo = lo & MASK64;
}
// state and restore carry the draw position out of and back into one engine.
// A web run's runtime is reinstalled by every page navigation, so the host
// holds this pair and puts it back, and the seed's stream is one stream
// whatever the page does.
state(): { hi: bigint; lo: bigint } {
return { hi: this.hi, lo: this.lo };
}
restore(hi: bigint, lo: bigint): void {
this.hi = hi & MASK64;
this.lo = lo & MASK64;
}
// next advances the 128-bit LCG state and returns the new (hi, lo) pair.
private next(): { hi: bigint; lo: bigint } {
// 128-bit multiply of state by the 128-bit multiplier, keeping 128 bits.
+14
View File
@@ -161,6 +161,20 @@ export function installRuntime(
// picker's rng scope. It brackets those calls with this so a multi-item
// sampler refuses rather than handing back its first item forever.
defineLockedGlobal("__sanderlingSetEnumeratingCandidates__", setEnumeratingCandidates);
// The picker's draw position, as "hi,lo" decimal (a bigint pair survives no
// JSON hop). A web run's runtime is reinstalled by every page navigation, so
// the host reads the position back after each decision and puts it in place
// before the next one; without that the seed's stream restarts at its first
// draw every time the page reloads.
defineLockedGlobal("__sanderlingPickerState__", () => {
const { hi, lo } = rng.state();
return `${hi},${lo}`;
});
defineLockedGlobal("__sanderlingRestorePickerState__", (state: string) => {
const [hi, lo] = state.split(",");
if (hi === undefined || lo === undefined) return;
rng.restore(BigInt(hi), BigInt(lo));
});
defineLockedGlobal("__sanderlingExtractors__", () => evaluateExtractors());
// __sanderlingSetupAction__ walks ONLY the setup generator once, for the LLM
// action generator (Go), which drives selection itself and must not run the
+6
View File
@@ -672,6 +672,12 @@ defineLockedGlobal("__sanderlingSetLastAction__", (value: unknown) => {
lastAction = value ?? null;
});
// The host reads the same buffer buildState puts behind state.exceptions, so
// the goja-side state.exceptions is the page's list rather than the empty one
// it held before, and the trace records an error surface an offline oracle can
// read back.
defineLockedGlobal("__sanderlingExceptions__", () => capturedExceptions.slice());
// writable:false stops a page script from shadowing the runtime via plain
// assignment (the realistic in-page threat). configurable:true is required so
// unit tests sharing one process can reinstall a fake via defineProperty; a
+14
View File
@@ -56,3 +56,17 @@ for (const c of cases) {
});
}
}
// A web run's runtime is reinstalled by every page navigation, so the host
// carries this pair across and the seed's stream stays one stream. A restore
// that lost a bit would silently re-explore ground the seed already covered.
test("a restored state continues the sequence it was taken from", () => {
const source = new Pcg(1n, 0n);
for (let draw = 0; draw < 5; draw++) source.uint64();
const carried = source.state();
const continuation = [source.uint64(), source.uint64(), source.uint64()];
const fresh = new Pcg(1n, 0n);
fresh.restore(carried.hi, carried.lo);
assert.deepEqual([fresh.uint64(), fresh.uint64(), fresh.uint64()], continuation);
});
+11
View File
@@ -0,0 +1,11 @@
// The seeded picker, over a page that gets navigated out from under it.
//
// internal/driver/chrome/picker_stream_test.go bundles this through the
// production web bundler and asks for one action at a time. `taps` over a page
// whose targets never move draws exactly once per call, so the actions it hands
// back ARE the seed's draw stream, and a stream that restarts is visible in
// them.
import { taps } from "../src/actions.ts";
export const actionsRoot = taps;