LearnNewsExamplesServices
Frontmatter
id15135
titleTourRunner: expose a post-step settlement event for progress consumers
stateOpen
labels
enhancementaitesting
assignees[]
createdAt9:13 PM
updatedAt9:13 PM
githubUrlhttps://github.com/neomjs/neo/issues/15135
authorneo-gpt-emmy
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
milestonev13.2

TourRunner: expose a post-step settlement event for progress consumers

Open Backlog/active-chunk-5 enhancementaitesting
neo-gpt-emmy
neo-gpt-emmy commented on 9:13 PM

Context

The visual-repair pass on #15099 exposed a misleading progress contract: the Demo-C header could advance from the runner's beat event before the corresponding operation, assertion, pause, runtime cue, and projection had settled. The local draft repair serializes progress behind TourRunner.log.length, the hosting surface's cue promise, and its projection promise. That removes the visible jump, but polling the runner's mutable log is an app-local adapter around a missing lifecycle signal.

A live latest-20-open sweep plus all-state GitHub search was checked at 2026-07-13T19:11Z; no equivalent ticket exists. The current A2A all-state window contains no overlapping claim or intent.

The Problem

Neo.ai.client.TourRunner exposes:

  • beat before every step, intentionally serving captions and data-only runtime cues;
  • error on abort;
  • complete after the full script.

It does not expose the successful boundary between those two ends. A progress consumer therefore has only three bad choices: claim progress from the pre-step beat, poll log.length, or wait until the entire tour completes.

This is observable in DemoCWorkspace.waitForTourBeatSettlement(): a bounded polling loop waits for the log entry before it can safely paint the next pip. The code works as containment, but it leaks runner internals into the hosting surface and can drift as new step types land.

Architectural Reality

  • src/ai/client/TourRunner.mjs#run fires beat before dispatch, then appends one deterministic log entry only after each runner-owned step has completed.
  • For op, runner-owned settlement means the dock executor resolved, the log entry exists, and any post-operation expectations passed.
  • For topology-assert, it means the topology read and expectations passed.
  • For pause, it means the mode-specific pacing wait completed.
  • Hosting-surface cues and view projection remain hosting-surface authority. Closed ticket #15009 deliberately kept deferred view settlement in the workspace; this ticket does not pull DOM or projection ownership into the runner.

The Fix

Add one additive observable event, stepSettled, emitted after every successful runner-owned step and before the next beat.

The deterministic, timestamp-free payload contains:

  • sceneId, sceneIndex, stepIndex, and stepType;
  • one-based completedCount;
  • logLength matching the runner's completed log.

Then migrate Demo C's progress adapter to consume stepSettled and combine it with its existing cue/projection promise. Delete the log-length polling loop; runtime-only cue settlement stays local.

Contract Ledger

Target surface Source of authority Proposed behavior Fallback / failure Docs Evidence
pre-step narration TourRunner.beat remains pre-step and unchanged no compatibility change class event summary event-order unit spec
runner-owned step completion TourRunner#run fire exactly one stepSettled after a successful step rejected/failed step emits no settled event; existing error path wins class + event payload JSDoc op/assert/pause unit matrix
Demo-C progress DemoCWorkspace cue + projection promises advance one pip after stepSettled and local surface settlement cue/projection failure remains visible in the tour receipt method JSDoc focused whitebox E2E exact sequence

Decision Record impact

none — additive lifecycle precision inside the existing neo.tour.script.v1 runner; no script-schema or authority change.

Acceptance Criteria

  • beat remains a pre-step caption/cue event.
  • stepSettled fires exactly once after each successful op, topology-assert, and pause step.
  • The payload is timestamp-free, JSON-safe, and carries sceneId, sceneIndex, stepIndex, stepType, completedCount, and logLength.
  • A rejected operation or failed expectation emits error without a false stepSettled for that step.
  • Focused unit coverage pins beat → runner work → stepSettled → next beat order in spec, plus pause settlement in paced mode.
  • Demo C consumes the event and removes waitForTourBeatSettlement()'s log polling while retaining local cue/projection settlement.
  • Demo-C whitebox E2E observes the exact monotonic progress sequence with no fill-then-reset frame.
  • Class and consumer JSDoc name the runner-owned versus hosting-surface settlement boundary.

Out of Scope

Awaiting arbitrary Observable listener promises · moving runtime cue execution into TourRunner · DOM/view-projection ownership · changing pause semantics · a new script schema version.

Avoided Traps

  • Do not rename or move beat; it is correctly pre-step.
  • Do not make fire() globally await listener return values.
  • Do not include timestamps or mutable log-array aliases in the payload.
  • Do not treat stepSettled as proof that a hosting surface's asynchronous cue or projection has painted.

Related

Related: #15099 · #15009 · #13158

Origin Session ID: f95e01ff-ba36-409a-98af-573263fab247

Retrieval Hint: TourRunner stepSettled post-step progress beat pre-step log polling Demo C