1- import type { DaemonRequest , DaemonResponse , SessionAction } from '../types.ts' ;
1+ import type { DaemonInvokeFn , DaemonRequest , DaemonResponse , SessionAction } from '../types.ts' ;
2+ import type { SessionStore } from '../session-store.ts' ;
3+ import { errorResponse } from './response.ts' ;
4+ import { readReplaySelectorDisplayValue } from '../replay-selector-port.ts' ;
5+ import type { ResponseLevel } from '@agent-device/kernel/contracts' ;
6+ import type { SnapshotTimingSample } from '@agent-device/contracts/capture' ;
7+ import { withReplayFailureDiagnostics } from './session-replay-runtime-failure.ts' ;
8+ import type { ReplayCoordinator } from '../session-replay-coordinator.ts' ;
29import { invokeReplayAction } from './session-replay-action-runtime.ts' ;
3- import type { AdReplayStepFailure , AdReplayStepRuntime } from '@agent-device/ad-replay' ;
10+ import type {
11+ AdReplayStepFailure ,
12+ AdReplayStepRuntime ,
13+ ReplaySelectorPort ,
14+ } from '@agent-device/ad-replay' ;
415import { collectReplayActionArtifactPaths } from './session-replay-runtime-artifacts.ts' ;
516import {
617 applyReplayDispatchGuard ,
@@ -19,14 +30,6 @@ import {
1930 resolveTargetVerificationEntry ,
2031 type TargetBindingDivergenceContext ,
2132} from './session-replay-target-verification.ts' ;
22- import {
23- asFailedReplayStepResponse ,
24- buildReplayActionFailure ,
25- describeReplayStepValue ,
26- readSessionSnapshotSampleCount ,
27- readSessionSnapshotSamplesSince ,
28- type ReplayStepContext ,
29- } from './session-replay-runtime-step-support.ts' ;
3033import type { ReplayTestAttemptStepSink } from '@agent-device/replay-test' ;
3134
3235/**
@@ -35,17 +38,16 @@ import type { ReplayTestAttemptStepSink } from '@agent-device/replay-test';
3538 * `createAdReplayStepRuntime`. See that file's `runReplayScriptFile` for the request-level
3639 * orchestration this adapter plugs into.
3740 *
38- * #1555 structural-quality review ("shrink the runtime adapter toward the
39- * plan's <300 LOC metric"): the wire-narrowing concern (guard threading,
40- * `details` bag -> typed evidence, dispatch-failure classification) moved to
41- * `session-replay-dispatch-narrowing.ts`; `ReplayStepContext` and the
42- * failure- wrapping/diagnostics support helpers moved to
43- * `session-replay-runtime- step-support.ts` (re-exporting `ReplayStepContext`
44- * by name so this file's own importers see no path change). This file is
45- * left with exactly the `createAdReplayStepRuntime` factory and the small
46- * closures only it needs .
41+ * The wire-narrowing concern (guard threading, `details` bag -> typed
42+ * evidence, dispatch-failure classification) lives in
43+ * `session-replay-dispatch-narrowing.ts` — a real seam with one nameable job.
44+ * Everything else the factory's capabilities delegate to ( the step context
45+ * shape, failure wrapping, progress display, diagnostics sampling) lives
46+ * below in this file: a briefly-extracted `- step-support` module was folded
47+ * back after review judged it a size-target fragment, not a concern boundary
48+ * — this adapter's honest size is ~430 lines, renegotiated from the plan's
49+ * <300 metric on #1478 .
4750 */
48- export type { ReplayStepContext } from './session-replay-runtime-step-support.ts' ;
4951
5052/**
5153 * #1478 P5 stage C2b (narrowed further by the #1555 review's neutral-outcomes
@@ -312,3 +314,131 @@ export function createAdReplayStepRuntime(params: {
312314 } ;
313315 return { runtime, readLastResponse : ( ) => lastResponse } ;
314316}
317+
318+ /**
319+ * Per-run invariants for a single replay step (ADR 0012 step 4 verify +
320+ * dispatch + guard). No `${VAR}` scope here (#1555 review P1, "move variable
321+ * semantics/planning behind the replay entrypoint") — the engine
322+ * (`runAdReplay`) builds and owns it; the adapter never resolves an action
323+ * or reads a scope value itself.
324+ */
325+ export type ReplayStepContext = {
326+ replayReq : DaemonRequest ;
327+ sessionName : string ;
328+ sessionStore : SessionStore ;
329+ logPath : string ;
330+ resolved : string ;
331+ actions : SessionAction [ ] ;
332+ actionLines : number [ ] ;
333+ actionSourcePaths : ( string | undefined ) [ ] | undefined ;
334+ planDigest : string ;
335+ actionTracePath : string | undefined ;
336+ responseLevel : ResponseLevel | undefined ;
337+ invoke : DaemonInvokeFn ;
338+ signal : AbortSignal | undefined ;
339+ /** #1478 P4b: the one locked gateway to this request's repair transaction. */
340+ coordinator : ReplayCoordinator ;
341+ /** #1478 P5 stage C: the one selector-port instance this request threads through the divergence-report chain. */
342+ port : ReplaySelectorPort ;
343+ } ;
344+
345+ /**
346+ * `runAdReplay` only ever calls `handleActionFailure` right after a step's
347+ * dispatch/build-failure capability reported `status: 'failed'`, and every
348+ * one of those capabilities records its response in the adapter's
349+ * `lastResponse` side-map before returning — so this narrowing cannot
350+ * actually fail in practice. The `COMMAND_FAILED` fallback exists only so
351+ * `buildReplayActionFailure` (which needs a real failed response to wrap)
352+ * stays total if that invariant is ever violated.
353+ */
354+ function asFailedReplayStepResponse (
355+ response : DaemonResponse | undefined ,
356+ ) : Extract < DaemonResponse , { ok : false } > {
357+ if ( response && ! response . ok ) return response ;
358+ return errorResponse (
359+ 'COMMAND_FAILED' ,
360+ 'replay step reported failure with no recorded response' ,
361+ ) as Extract < DaemonResponse , { ok : false } > ;
362+ }
363+
364+ async function buildReplayActionFailure (
365+ ctx : ReplayStepContext ,
366+ req : DaemonRequest ,
367+ action : SessionAction ,
368+ index : number ,
369+ response : Extract < DaemonResponse , { ok : false } > ,
370+ artifactPaths : string [ ] ,
371+ snapshotDiagnosticSamples : SnapshotTimingSample [ ] ,
372+ scrubVars : TargetBindingDivergenceContext [ 'scrubVars' ] ,
373+ ) : Promise < DaemonResponse > {
374+ const heldResponse = ( failure : DaemonResponse ) : DaemonResponse =>
375+ ctx . coordinator . markSessionHeldIfArmed ( failure ) ;
376+ if ( isCompleteTargetBindingDivergenceResponse ( response ) ) return heldResponse ( response ) ;
377+ return heldResponse (
378+ await withReplayFailureDiagnostics ( {
379+ response,
380+ action,
381+ index,
382+ replayPath : ctx . resolved ,
383+ sourcePath : ctx . actionSourcePaths ?. [ index ] ?? ctx . resolved ,
384+ sourceLine : ctx . actionLines [ index ] ?? 1 ,
385+ artifactPaths,
386+ snapshotDiagnosticSamples,
387+ scrubVars,
388+ req,
389+ sessionName : ctx . sessionName ,
390+ sessionStore : ctx . sessionStore ,
391+ resumeStamper : ctx . coordinator . resumeStamper ,
392+ logPath : ctx . logPath ,
393+ planActions : ctx . actions ,
394+ planDigest : ctx . planDigest ,
395+ port : ctx . port ,
396+ } ) ,
397+ ) ;
398+ }
399+
400+ /**
401+ * A replay-test progress step's display value: the recorded selector's
402+ * label/text/id term value when every alternative agrees on ONE value, else
403+ * `undefined`. Needs `readReplaySelectorDisplayValue`'s private selector AST
404+ * (`replay-selector-port.ts` deliberately keeps it daemon-only — see that
405+ * file's own comment), so this stays daemon-side and is handed to the engine
406+ * loop as the narrow `describeStepValue` capability.
407+ */
408+ function describeReplayStepValue ( action : SessionAction ) : string | undefined {
409+ const positionals = action . positionals ?? [ ] ;
410+ const selectorValue = readReplaySelectorDisplayValue ( positionals [ 0 ] ) ;
411+ if ( selectorValue ) return selectorValue ;
412+ if ( positionals . length === 0 ) return undefined ;
413+ return positionals . join ( ' ' ) ;
414+ }
415+
416+ // ADR 0012 step 4: a target-binding divergence is already a complete, final
417+ // REPLAY_DIVERGENCE built from its own pre-action capture — distinguished from
418+ // an action-failure divergence by its non-`action-failure` kind. Pinned
419+ // daemon-side: it re-inspects the already-projected `DaemonResponse` wire
420+ // shape to decide whether the wire-level diagnostics-augmentation step
421+ // applies, which is daemon/wire authority, not target-binding classification
422+ // itself (that already happened, in `session-replay-target-classification.ts`'s
423+ // `classifyReplayTarget`, called from `classifyPreDispatchTarget`).
424+ function isCompleteTargetBindingDivergenceResponse ( response : DaemonResponse ) : boolean {
425+ if ( response . ok || response . error . code !== 'REPLAY_DIVERGENCE' ) return false ;
426+ const divergence = response . error . details ?. divergence ;
427+ const kind =
428+ divergence && typeof divergence === 'object'
429+ ? ( divergence as Record < string , unknown > ) . kind
430+ : undefined ;
431+ return typeof kind === 'string' && kind !== 'action-failure' ;
432+ }
433+
434+ function readSessionSnapshotSampleCount ( sessionStore : SessionStore , sessionName : string ) : number {
435+ return sessionStore . get ( sessionName ) ?. snapshotDiagnostics ?. samples . length ?? 0 ;
436+ }
437+
438+ function readSessionSnapshotSamplesSince (
439+ sessionStore : SessionStore ,
440+ sessionName : string ,
441+ start : number ,
442+ ) : SnapshotTimingSample [ ] {
443+ return sessionStore . get ( sessionName ) ?. snapshotDiagnostics ?. samples . slice ( start ) ?? [ ] ;
444+ }
0 commit comments