Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 34 additions & 24 deletions packages/runner/src/runtime/jev-checklist.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,14 @@ export async function settle(run, state, laterReadings) {
}

/**
* Whether the current checkpoint, which reads no step left on a page that
* settles it neither way, first gets one wait for the page to move. A page can
* hold still with nothing out and still be on its way to the next one:
* A target product's create had its answer, its dialog stayed unchanged, and Get started
* opened only after the walk had read the page and handed it back. The wait
* is spent
* once per checkpoint, never on a page already waited out as busy, and never
* Whether a round about to hand back the current checkpoint — on a page that
* settles it neither way, or after an action round chose none — first gets one
* wait for the page to move. A page can hold still with nothing out and still
* be on its way to the next one: a target product's create had its answer, its
* dialog stayed unchanged, and Get started opened only after the walk had read
* the page and handed it back; a CLI's "Returning to your terminal…" page moved
* on to its callback a second after the walk chose none on it. The wait is
* spent once per checkpoint, never on a page already waited out as busy, and never
* on the page the last such wait ended on: a page that keeps changing, such as
* a clock or a live list, is judged once more and handed back, and so is the
* next checkpoint on the page the one before settled on after its wait.
Expand All @@ -97,30 +98,39 @@ async function unsettled(run, state, judgements, busyPage) {
return busyPage ? "stayed_busy" : "no_step_left";
}

/**
* The one wait for the page to move a round gets before it hands back, when
* it gets one (waitFor), recorded as `reason`: a checkpoint the page settles
* neither way (judge), and an action round that chose none (the executor's
* act). True when the page moved: the walk reads it again in the next round.
* Unmoved, the round hands back on what it read before the wait, without
* asking Jev again. `waitForMove` waits for the page to move and returns
* where it ended.
*/
export async function movedBeforeHandBack(run, state, reason, waitForMove) {
const here = readingKey(run.reading);
if (!waitFor(run, state, here)) return false;
run.unsettledWaits.add(state.currentCheckpoint.id);
await record(run, "wait", { choice: reason });
const there = readingKey(await waitForMove());
if (there === here) return false;
run.waitedPage = there;
return true;
}

/**
* Once no step remains before the current checkpoint, the page is judged and
* never acted on: its verdict, or a hand-back when it settles none, after one
* wait for the page to move when it gets one (waitFor). Moved, the page is
* judged again in the next round; unmoved, it is handed back on the readings
* taken before the wait, without asking Jev again: a second reading of the
* same page could read a step left and act where the first read none, or
* settle a verdict on that one reading alone. `busyPage` is whether the page
* reads busy; `waitForMove` waits for it to move and returns where it ended.
* wait for the page to move when it gets one (movedBeforeHandBack). Unmoved,
* it is handed back on the readings taken before the wait: a second reading
* of the same page could read a step left and act where the first read none,
* or settle a verdict on that one reading alone. `busyPage` is whether the
* page reads busy.
*/
export async function judge(run, state, judgements, { busyPage, waitForMove }) {
const verdict = verdictOf(judgements);
if (verdict) return conclude(run, state, state.currentCheckpoint, verdict, judgements);
const here = readingKey(run.reading);
if (waitFor(run, state, here)) {
run.unsettledWaits.add(state.currentCheckpoint.id);
await record(run, "wait", { choice: "unsettled" });
const reached = await waitForMove();
const there = readingKey(reached);
if (there !== here) {
run.waitedPage = there;
return null;
}
}
if (await movedBeforeHandBack(run, state, "unsettled", waitForMove)) return null;
return unsettled(run, state, judgements, busyPage);
}

Expand Down
25 changes: 22 additions & 3 deletions packages/runner/src/runtime/jev-executor.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import { renderJourneySteps } from "@shipbash/contracts/runtime/journey-steps.mjs";
import { splitWaiting } from "./action-requests.mjs";
import { chooseJevAction, closingOptions, pageOptions } from "./jev-actions.mjs";
import { judge, record, settle, stepRemains } from "./jev-checklist.mjs";
import { judge, movedBeforeHandBack, record, settle, stepRemains } from "./jev-checklist.mjs";
import { readSteps, whichStep } from "./jev-step-questions.mjs";
import { attribute, refuse, stepLedger, walkInputs } from "./jev-steps.mjs";
import { checkInterruption, movedFrom, readPage, waitOut } from "./jev-page-reading.mjs";
Expand All @@ -29,6 +29,21 @@ const BUSY_WAITS = 1;
const BUSY_THRESHOLD = 0.5;
// The guidance every walk starts with and returns to once a choice runs.
const BEGIN = "Begin the authorized goal.";
// The hosts of this machine's own servers, where a program in the walk's
// terminal sends the browser: a CLI's sign-in callback (localhost:<port>/callback).
const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "[::1]"]);

/**
* Whether the reading is a page a program in this walk's terminal led the
* browser to, and so no change of product: a local file, or a server on this
* machine, once the walk has a terminal of its own. Any other origin than the
* walk's first still ends it (origin_changed).
*/
function terminalLocalPage(reading) {
if (!reading.terminal) return false;
const { protocol, hostname } = new URL(reading.url);
return protocol === "file:" || LOOPBACK_HOSTS.has(hostname);
}

/**
* @param {{ url: string, snapshot: string, terminal: { sessionId: string, screen: string, altScreen: boolean, running: boolean, exitCode?: number } | null }} reading
Expand Down Expand Up @@ -179,7 +194,11 @@ async function stillOnPage(run, state, action) {
* @param {boolean} fromPage whether the action was read off the page
*/
async function act(run, state, action, fromPage) {
if (!action || action.ref === "none") return outcome("none_chosen");
if (!action || action.ref === "none") {
// The page may still be on its way to the next one: it gets the one wait a page that settles nothing gets.
const moved = await movedBeforeHandBack(run, state, "none_chosen", () => movedFrom(run));
return outcome(moved ? null : "none_chosen");
}
if (action.ref === "give_up") return outcome("stayed_busy");
const [chosen, step] = await Promise.all([!fromPage || stillOnPage(run, state, action), stepFor(run, action)]);
checkInterruption(run);
Expand Down Expand Up @@ -214,7 +233,7 @@ async function walk(run) {
const reading = await readPage(run);
const { url } = reading;
origin ??= new URL(url).origin;
if (new URL(url).origin !== origin) return "origin_changed";
if (new URL(url).origin !== origin && !terminalLocalPage(reading)) return "origin_changed";
notePageMove(run, url, seen);
if (reading.snapshot.length > 24_000) return "page_too_large";
const current = JSON.stringify([readingKey(reading), run.checklist.find((item) => item.status !== "passed")?.id]);
Expand Down
Loading