refactor(daemon): extract native .ad replay to packages/ad-replay (#1478 P5) - #1555
Conversation
|
Size Report
Startup median (7 runs, lower is better):
Top changed chunks:
|
|
Review verdict: request changes / not ready at
The selector port direction, package dependency direction, coordinator ownership, and current CI are otherwise clean. The PR should remain draft: #1478 still requires both exact-head live replay suites plus target-v1 verification, divergence, resume, and save-script repair evidence; the iOS checkout leg is currently blocked by #1542. No |
…inspectAdReplay (#1555 review) P1 "do not smuggle daemon wire failures through a generic": drop the TResponse generic from AdReplayStepRuntime/runAdReplay. executeStep and handleActionFailure now return neutral tagged AdReplayStepOutcome/ AdReplayStepFailure values (kind/message/artifactPaths only); runAdReplay returns a neutral completed/failed AdReplayRunOutcome. The engine never holds or returns a DaemonResponse. The daemon adapter (createAdReplayStepRuntime, session-replay-runtime.ts) keeps its real wire response in a local side-map as it builds each neutral outcome, and runReplayScriptFile reads it back once runAdReplay reports which step failed, so the final response is byte-identical to before this split. P1 "parsing/planning/digest/resume must also occur behind runAdReplay": relocate computeReplayPlanDigest's call site and the --from/--plan-digest resume-point math (resolveReplayEntryIndex) behind inspectAdReplay's manifest as planDigest and a resolveEntryIndex closure. Neither is a new top-level export -- inspectAdReplay/runAdReplay stay the only two. Timing is preserved exactly (still called eagerly in prepareReplayPlan, before prepareReplaySession's coordinator-mutating side effects) since moving resume validation to run inside runAdReplay itself would let a rejected --from request mutate coordinator/session state first -- a real ordering hazard, not just a cosmetic one. computeReplayPlanDigest/ReplayPlanDigestMetadata/resolveReplayEntryIndex leave the ad-replay façade; request-router-repair-expired.test.ts and prepareReplayPlan read the digest/resume result off the manifest instead.
…replay façade (#1555 review) P1 "complete the binding façade instead of documenting deviations": classifyTargetBindingMatch never had a real consumer reachable through inspectAdReplay/runAdReplay -- both its callers (the daemon's record-time self-check in session-target-evidence.ts and its replay-time classification wrapper in session-replay-target-classification.ts) are daemon files that imported it directly. It interprets TargetAnnotationV1 evidence semantics shared beyond the engine, so it moves to packages/ad-script alongside target-annotation-identity.ts (new target-annotation-classification.ts + its test), and both daemon call sites now import it from there instead of @agent-device/ad-replay. One deviation remains and is reported rather than papered over per the review's own instruction: the four target-verification policy functions (planPreDispatchTargetVerification, planPostResolutionTargetVerification, deriveReplayTargetGuardMismatchEvidence, deriveWaitLandmarkMismatchEvidence) and the ReplaySelectorPort type family stay exported. Their sole caller, session-replay-target-verification.ts, interleaves these pure decisions with daemon-only async work (capture, SessionStore, coordinator/resume stamping, wire shaping) that must stay outside the engine by design; moving their call sites to live only behind runAdReplay would require restructuring that whole orchestration into new fine-grained AdReplayStepRuntime capabilities, which is out of scope for this pass. See packages/ad-replay/src/index.ts's header comment for the full reasoning. P1 "add the reviewer-required exact exported-symbol gate": adds readNamedExports (scripts/layering/package-boundaries.ts), a small parser over a façade's `export { .. } from`, `export type { .. } from`, and direct-declaration forms, and pins @agent-device/ad-replay's exact 21-symbol export list in package-boundaries.test.ts. Plant-verified: a stray `export const` addition failed the assertion; removed it and the gate went green again.
…#1555 review) Moves the verify-then-dispatch decision flow into packages/ad-replay's step loop so the four target-verification policy functions (plan{PostResolution,PreDispatch}TargetVerification, derive{ReplayTargetGuardMismatch,WaitLandmark}MismatchEvidence) become engine-private and leave the ad-replay façade. The daemon (session-replay-target-verification.ts) shrinks to the narrow AdReplayStepRuntime capabilities the engine drives: routing (beginTargetVerification), capture (captureObservation), classification (classifyTarget), dispatch (dispatchStep), and wire-building (buildRecordedUnverifiableFailure, buildTargetBindingFailure, buildPostDispatchTargetBindingFailure). Wire output and replay-compat stay byte-identical; the exact-symbol façade gate is updated to the shrunken export list.
|
Re-review verdict: still request changes at Resolved since Remaining blockers:
The prior live evidence is for |
|
All four findings addressed at head P1 invalid-backend rejection — restored at the authoritative point ( P1 complete the façade — done, no deviations remain. Shared vocabulary went to its proper owner ( P1 no wire smuggling — the P2 second adapter conformance — the in-memory adapter honors Live-readiness — round-2 evidence at Remaining ready-for-human blocker: #1542 (iOS checkout leg). The PR stays draft until that's fixed or the corpus is repaired without weakening coverage. Generated by Claude Code |
5a4f4c2 to
e6cbe6b
Compare
…inspectAdReplay (#1555 review) P1 "do not smuggle daemon wire failures through a generic": drop the TResponse generic from AdReplayStepRuntime/runAdReplay. executeStep and handleActionFailure now return neutral tagged AdReplayStepOutcome/ AdReplayStepFailure values (kind/message/artifactPaths only); runAdReplay returns a neutral completed/failed AdReplayRunOutcome. The engine never holds or returns a DaemonResponse. The daemon adapter (createAdReplayStepRuntime, session-replay-runtime.ts) keeps its real wire response in a local side-map as it builds each neutral outcome, and runReplayScriptFile reads it back once runAdReplay reports which step failed, so the final response is byte-identical to before this split. P1 "parsing/planning/digest/resume must also occur behind runAdReplay": relocate computeReplayPlanDigest's call site and the --from/--plan-digest resume-point math (resolveReplayEntryIndex) behind inspectAdReplay's manifest as planDigest and a resolveEntryIndex closure. Neither is a new top-level export -- inspectAdReplay/runAdReplay stay the only two. Timing is preserved exactly (still called eagerly in prepareReplayPlan, before prepareReplaySession's coordinator-mutating side effects) since moving resume validation to run inside runAdReplay itself would let a rejected --from request mutate coordinator/session state first -- a real ordering hazard, not just a cosmetic one. computeReplayPlanDigest/ReplayPlanDigestMetadata/resolveReplayEntryIndex leave the ad-replay façade; request-router-repair-expired.test.ts and prepareReplayPlan read the digest/resume result off the manifest instead.
…replay façade (#1555 review) P1 "complete the binding façade instead of documenting deviations": classifyTargetBindingMatch never had a real consumer reachable through inspectAdReplay/runAdReplay -- both its callers (the daemon's record-time self-check in session-target-evidence.ts and its replay-time classification wrapper in session-replay-target-classification.ts) are daemon files that imported it directly. It interprets TargetAnnotationV1 evidence semantics shared beyond the engine, so it moves to packages/ad-script alongside target-annotation-identity.ts (new target-annotation-classification.ts + its test), and both daemon call sites now import it from there instead of @agent-device/ad-replay. One deviation remains and is reported rather than papered over per the review's own instruction: the four target-verification policy functions (planPreDispatchTargetVerification, planPostResolutionTargetVerification, deriveReplayTargetGuardMismatchEvidence, deriveWaitLandmarkMismatchEvidence) and the ReplaySelectorPort type family stay exported. Their sole caller, session-replay-target-verification.ts, interleaves these pure decisions with daemon-only async work (capture, SessionStore, coordinator/resume stamping, wire shaping) that must stay outside the engine by design; moving their call sites to live only behind runAdReplay would require restructuring that whole orchestration into new fine-grained AdReplayStepRuntime capabilities, which is out of scope for this pass. See packages/ad-replay/src/index.ts's header comment for the full reasoning. P1 "add the reviewer-required exact exported-symbol gate": adds readNamedExports (scripts/layering/package-boundaries.ts), a small parser over a façade's `export { .. } from`, `export type { .. } from`, and direct-declaration forms, and pins @agent-device/ad-replay's exact 21-symbol export list in package-boundaries.test.ts. Plant-verified: a stray `export const` addition failed the assertion; removed it and the gate went green again.
…#1555 review) Moves the verify-then-dispatch decision flow into packages/ad-replay's step loop so the four target-verification policy functions (plan{PostResolution,PreDispatch}TargetVerification, derive{ReplayTargetGuardMismatch,WaitLandmark}MismatchEvidence) become engine-private and leave the ad-replay façade. The daemon (session-replay-target-verification.ts) shrinks to the narrow AdReplayStepRuntime capabilities the engine drives: routing (beginTargetVerification), capture (captureObservation), classification (classifyTarget), dispatch (dispatchStep), and wire-building (buildRecordedUnverifiableFailure, buildTargetBindingFailure, buildPostDispatchTargetBindingFailure). Wire output and replay-compat stay byte-identical; the exact-symbol façade gate is updated to the shrunken export list.
…les (#1555) Splits the ~1096-line replay runtime into cohesive pieces, keeping session-replay-runtime.ts as thin orchestration (~240 LOC): - session-replay-runtime-engine-adapter.ts: the AdReplayStepRuntime adapter (createAdReplayStepRuntime, the build*Failure capability implementations, and the lastResponse/lastObservation side-map mechanics), extracted verbatim. - session-replay-runtime-plan.ts: extended with the plan-side helpers (validateReplayBackendFlag, inspectReplayPlanManifest, resolveReplayPlanEntryIndex, prepareReplayPlan, routeMaestroReplay) alongside the buildReplayMetadataFlags helper already there — buildReplayMetadataFlags is now module-private since its one caller moved into the same file. Also introduces ReplayScriptFileParams, named here (instead of derived via Parameters<typeof runReplayScriptFile>) so routeMaestroReplay can reference the shape without importing back from session-replay-runtime.ts. - session-replay-runtime-session.ts (new): session preparation (prepareReplaySession and its coordinator arming/repair-preflight helpers), extracted verbatim. Coordinator ownership is unchanged: createReplayCoordinator is still constructed only in session-replay-runtime.ts, matching replay-coordinator-ownership.test.ts's allowlist as-is — every extracted module receives the already-constructed ReplayCoordinator as a parameter. Pure move; no behavior change.
…tion (#1555) Two invariants found during the P5 decomposition pass now have direct counterfactual-verified coverage: - packages/ad-replay/src/internal/__tests__/step-loop.test.ts: a post-dispatch target-binding mismatch (dispatchWithGuard) must report the accumulated PRE-step artifact snapshot it was called with, never the artifacts the failed dispatch itself produced. Verified red by swapping the buildPostDispatchTargetBindingFailure call to outcome.artifactPaths. - src/daemon/handlers/__tests__/session-replay-runtime-plan.test.ts: a rejected --from/--plan-digest resume must never reach prepareReplaySession's coordinator-mutating writes (the R2 ordering invariant) — a pre-armed repair transaction and corrective-resume watermark are asserted byte-for-byte unchanged after rejection. Verified red by calling prepareReplaySession before honoring the plan-validation rejection.
|
Resync complete at head Since the redraft: rebased onto main absorbing #1554 (its terminal-lifecycle policy folded INTO the engine step loop, unified with the repair terminal-close predicate as one suppression condition; the daemon module deleted; all six #1554 unit tests pass unchanged end-to-end, plus five new package-internal Round-3 live evidence at exact head Prior review findings all addressed in Generated by Claude Code |
|
Review findings at e6cbe6b:\n\n- [P1] Enforce the accepted two-entrypoint facade. The approved P5 amendment says packages/ad-replay exports only inspectAdReplay and runAdReplay; the current facade additionally exports formatReplaySuccessMessage, runtime/outcome types, and the full selector-port family, and the new exact-symbol gate blesses that widening. The gate also ignores export-star declarations, so it can miss future widening. Keep success formatting daemon-side, hide internal capability/selector types behind entrypoint signatures, and make the gate reject every export form.\n- [P1] Translate wire failures before the engine boundary. AdReplayDispatchOutcome carries details as a generic unknown-valued record, the daemon adapter assigns response.error.details verbatim, and engine policy parses that bag. This is still daemon wire projection crossing into the engine despite the PR's neutral-outcomes/no-generic claim. Narrow each mismatch in the adapter into explicit tagged evidence values.\n- [P1] Move variable semantics/planning behind the replay entrypoint. The daemon still assembles ReplayVarScope, interpolates actions in invokeReplayAction, and independently interpolates target verification. P5 assigns variables and planning to ad-replay; leaving these paths daemon-owned preserves duplicated orchestration/semantics.\n- [P1] Keep this draft pending exact-head live evidence. The PR body's live corpus is for 947b90c, not current head e6cbe6b, and the prescribed iOS checkout leg remains blocked by #1542. Re-run the full required corpus on the corrected exact head before readiness.\n\nThe #1554 terminal-close fold-in and current CI checks look sound, but the accepted P5 boundary/readiness gates are not yet met. |
… P1) packages/ad-replay/src/index.ts now exports exactly two value symbols, inspectAdReplay and runAdReplay, and zero types — formatReplaySuccessMessage (presentation) moves beside its one caller in session-replay-runtime.ts, and every type a root daemon file needs is derived structurally off the two entrypoints in the one new src/daemon/ad-replay-facade-types.ts module instead of being named off the façade. scripts/layering/package-boundaries.ts's readNamedExports is rewritten on oxc-parser's own static-export table instead of a regex, so it can no longer silently miss a widening export form: a bare `export *` re-export or an `export default` now throws (an un-enumerable, and therefore un-pinnable, export), while `export * as ns` and every other enumerable form is still counted. The pinned exact-symbol assertion in package-boundaries.test.ts is narrowed to ['inspectAdReplay', 'runAdReplay'].
…1555 review P1) AdReplayDispatchOutcome's guard-mismatch/landmark-mismatch variants carried a generic `details: Record<string, unknown> | undefined` bag straight off the wire response — a daemon wire projection crossing into the engine even though the outcome itself was already a neutral type. The daemon adapter (session-replay-runtime-engine-adapter.ts) now narrows that bag into the typed AdReplayGuardMismatchEvidence/AdReplayLandmarkMismatchEvidence shapes (observed identity, expected/observed structural denotation, ancestry entries, match count) before returning the outcome; the unknown-parsing readers move there with the wire-reading responsibility they always were. target-verification.ts's deriveReplayTargetGuardMismatchEvidence/ deriveWaitLandmarkMismatchEvidence now consume only the typed values — no `unknown`-valued record type remains on any engine-crossing signature.
…replay façade (#1555 review) P1 "complete the binding façade instead of documenting deviations": classifyTargetBindingMatch never had a real consumer reachable through inspectAdReplay/runAdReplay -- both its callers (the daemon's record-time self-check in session-target-evidence.ts and its replay-time classification wrapper in session-replay-target-classification.ts) are daemon files that imported it directly. It interprets TargetAnnotationV1 evidence semantics shared beyond the engine, so it moves to packages/ad-script alongside target-annotation-identity.ts (new target-annotation-classification.ts + its test), and both daemon call sites now import it from there instead of @agent-device/ad-replay. One deviation remains and is reported rather than papered over per the review's own instruction: the four target-verification policy functions (planPreDispatchTargetVerification, planPostResolutionTargetVerification, deriveReplayTargetGuardMismatchEvidence, deriveWaitLandmarkMismatchEvidence) and the ReplaySelectorPort type family stay exported. Their sole caller, session-replay-target-verification.ts, interleaves these pure decisions with daemon-only async work (capture, SessionStore, coordinator/resume stamping, wire shaping) that must stay outside the engine by design; moving their call sites to live only behind runAdReplay would require restructuring that whole orchestration into new fine-grained AdReplayStepRuntime capabilities, which is out of scope for this pass. See packages/ad-replay/src/index.ts's header comment for the full reasoning. P1 "add the reviewer-required exact exported-symbol gate": adds readNamedExports (scripts/layering/package-boundaries.ts), a small parser over a façade's `export { .. } from`, `export type { .. } from`, and direct-declaration forms, and pins @agent-device/ad-replay's exact 21-symbol export list in package-boundaries.test.ts. Plant-verified: a stray `export const` addition failed the assertion; removed it and the gate went green again.
…#1555 review) Moves the verify-then-dispatch decision flow into packages/ad-replay's step loop so the four target-verification policy functions (plan{PostResolution,PreDispatch}TargetVerification, derive{ReplayTargetGuardMismatch,WaitLandmark}MismatchEvidence) become engine-private and leave the ad-replay façade. The daemon (session-replay-target-verification.ts) shrinks to the narrow AdReplayStepRuntime capabilities the engine drives: routing (beginTargetVerification), capture (captureObservation), classification (classifyTarget), dispatch (dispatchStep), and wire-building (buildRecordedUnverifiableFailure, buildTargetBindingFailure, buildPostDispatchTargetBindingFailure). Wire output and replay-compat stay byte-identical; the exact-symbol façade gate is updated to the shrunken export list.
… into the ad-replay engine Rebasing p5/extract-ad-replay onto main pulled in #1554's --keep-session feature, which had grown its own daemon-side terminal-close-suppression predicate (session-replay-terminal-lifecycle.ts's resolveSuppressedTerminalCloseIndex/countExecutedReplayActions) independently of this branch's own engine-side one (step-loop.ts's isRepairArmedTerminalCloseAction). Both are the same decision family — replay --keep-session and an active --save-script repair now share ONE structural resolution (resolveSuppressedTerminalCloseIndex, generalized to "terminal among EXECUTABLE actions" rather than the old physical-last-index check) and one suppression check inside runAdReplay, gated on keepSession OR runtime.isRepairArmed(). AdReplayRunRequest grew a keepSession field; the neutral 'replayed' count in AdReplayRunOutcome is now computed inline in the loop instead of the daemon's old actions.length - entryIndex approximation. requireLiveSessionForKeepSession (the --keep-session live-session postcondition) stays daemon-side, inlined into session-replay-runtime.ts, since it inspects SessionStore state the engine never sees. The daemon-only session-replay-terminal-lifecycle.ts this arrived with is deleted entirely — its isExecutableReplayAction was a duplicate of the engine's own. runReplayScriptFile's Maestro-format routing (including the new --keep-session Maestro rejection) was extracted into routeMaestroReplay to keep the function under fallow's complexity threshold after re-threading keepSession through it. Added packages/ad-replay/src/internal/__tests__/step-loop.test.ts covering the unified suppression decision (both keepSession and repair-armed) directly against runAdReplay, including the terminal-among-executable-actions case with a trailing nested replay marker. The daemon-level integration tests (6 tests in session-replay-terminal-lifecycle.test.ts, exercising the same behavior through runReplayScriptFile) and the SDK provider-scenario test (active-session-script-publication.test.ts) needed no changes and pass unmodified.
…les (#1555) Splits the ~1096-line replay runtime into cohesive pieces, keeping session-replay-runtime.ts as thin orchestration (~240 LOC): - session-replay-runtime-engine-adapter.ts: the AdReplayStepRuntime adapter (createAdReplayStepRuntime, the build*Failure capability implementations, and the lastResponse/lastObservation side-map mechanics), extracted verbatim. - session-replay-runtime-plan.ts: extended with the plan-side helpers (validateReplayBackendFlag, inspectReplayPlanManifest, resolveReplayPlanEntryIndex, prepareReplayPlan, routeMaestroReplay) alongside the buildReplayMetadataFlags helper already there — buildReplayMetadataFlags is now module-private since its one caller moved into the same file. Also introduces ReplayScriptFileParams, named here (instead of derived via Parameters<typeof runReplayScriptFile>) so routeMaestroReplay can reference the shape without importing back from session-replay-runtime.ts. - session-replay-runtime-session.ts (new): session preparation (prepareReplaySession and its coordinator arming/repair-preflight helpers), extracted verbatim. Coordinator ownership is unchanged: createReplayCoordinator is still constructed only in session-replay-runtime.ts, matching replay-coordinator-ownership.test.ts's allowlist as-is — every extracted module receives the already-constructed ReplayCoordinator as a parameter. Pure move; no behavior change.
…tion (#1555) Two invariants found during the P5 decomposition pass now have direct counterfactual-verified coverage: - packages/ad-replay/src/internal/__tests__/step-loop.test.ts: a post-dispatch target-binding mismatch (dispatchWithGuard) must report the accumulated PRE-step artifact snapshot it was called with, never the artifacts the failed dispatch itself produced. Verified red by swapping the buildPostDispatchTargetBindingFailure call to outcome.artifactPaths. - src/daemon/handlers/__tests__/session-replay-runtime-plan.test.ts: a rejected --from/--plan-digest resume must never reach prepareReplaySession's coordinator-mutating writes (the R2 ordering invariant) — a pre-armed repair transaction and corrective-resume watermark are asserted byte-for-byte unchanged after rejection. Verified red by calling prepareReplaySession before honoring the plan-validation rejection.
… P1) packages/ad-replay/src/index.ts now exports exactly two value symbols, inspectAdReplay and runAdReplay, and zero types — formatReplaySuccessMessage (presentation) moves beside its one caller in session-replay-runtime.ts, and every type a root daemon file needs is derived structurally off the two entrypoints in the one new src/daemon/ad-replay-facade-types.ts module instead of being named off the façade. scripts/layering/package-boundaries.ts's readNamedExports is rewritten on oxc-parser's own static-export table instead of a regex, so it can no longer silently miss a widening export form: a bare `export *` re-export or an `export default` now throws (an un-enumerable, and therefore un-pinnable, export), while `export * as ns` and every other enumerable form is still counted. The pinned exact-symbol assertion in package-boundaries.test.ts is narrowed to ['inspectAdReplay', 'runAdReplay'].
…1555 review P1) AdReplayDispatchOutcome's guard-mismatch/landmark-mismatch variants carried a generic `details: Record<string, unknown> | undefined` bag straight off the wire response — a daemon wire projection crossing into the engine even though the outcome itself was already a neutral type. The daemon adapter (session-replay-runtime-engine-adapter.ts) now narrows that bag into the typed AdReplayGuardMismatchEvidence/AdReplayLandmarkMismatchEvidence shapes (observed identity, expected/observed structural denotation, ancestry entries, match count) before returning the outcome; the unknown-parsing readers move there with the wire-reading responsibility they always were. target-verification.ts's deriveReplayTargetGuardMismatchEvidence/ deriveWaitLandmarkMismatchEvidence now consume only the typed values — no `unknown`-valued record type remains on any engine-crossing signature.
…1555 review P1) The daemon assembled the `${VAR}` scope (buildPreparedReplayScope) and interpolated actions at two independent call sites: dispatch's own (invokeReplayAction) and target verification's separate one (resolveTargetVerificationEntry) — duplicated orchestration the P5 design assigns to the engine. runAdReplay's request now carries the raw scope INPUTS (varSources: plain builtins/file/shell/cli-env data, plus actionLines/actionSourcePaths/ resolvedPath for interpolation-error location) instead of a built scope; the engine builds the scope and resolves each action exactly once per step, handing the RESOLVED action to dispatchStep/beginTargetVerification while every other capability still receives the ORIGINAL recorded action (a target-binding divergence reports the recorded selector, never an expanded ${VAR}). This is the one resolution site now — session-replay-action-runtime.ts's invokeReplayAction and session-replay-target-verification.ts's resolveTargetVerificationEntry no longer hold a scope or call resolveReplayAction themselves. Scrub-value collection (collectReplayScrubbableVarValues, for divergence-report redaction) is kept single-sourced in the engine too: it's computed from the engine's own live scope and threaded to each build-failure/handleActionFailure capability as an explicit scrubVars argument, rather than the daemon recomputing it from a second scope object (which would have gone stale, since expandedBuiltinNames tracking now only happens engine-side). The Maestro replay path's own daemon-side vars usage is unrelated (a different engine) and is out of scope here.
CodeQL flagged the interpolation regex's fallback group as js/polynomial-redos
once vars.ts moved into packages/ (library-input classification): every
${NAME:- prefix of an unclosed input rescanned to end-of-string, quadratic
overall — 1,857 ms measured on 20k repetitions of '${A:-['. Replaced with a
single-pass scanner; failed fallback scans emit their span verbatim and resume
after it (escape-pair alignment is identical from every candidate start inside
the span, so no later candidate can terminate where the failed scan could not).
Equivalence: 200k-trial differential fuzz against the retired regex over the
adversarial alphabet, zero mismatches; both adversarial shapes now resolve in
1-2 ms.
…structural-quality review) Reverses the exact-two-value zero-type export shape #1555's second review pass established: it forced every root type derivation through one shim (src/daemon/ad-replay-facade-types.ts) and left four daemon-side twin types (TargetVerificationEntry, TargetClassificationOutcome, TargetBindingFailureEvidence, ReplayVerifiedTargetGuard) plus a toDaemonEvidence copy translator shadowing the engine's own shapes. packages/ad-replay/src/index.ts now exports inspectAdReplay/runAdReplay (unchanged, still the only two values) plus the neutral vocabulary their signatures are built from, by name — following packages/maestro's façade precedent. The exact-symbol gate in scripts/layering/package-boundaries.test.ts is widened to pin the full sorted list (values + types). The four daemon twins are deleted; session-replay-target-verification.ts and session-replay-runtime-engine-adapter.ts now use the engine's own AdReplayVerificationEntry/AdReplayTargetClassification/ AdReplayTargetBindingEvidence/AdReplayVerifiedTargetGuard directly. TargetBindingDivergenceBuilt's array fields are now readonly-compatible, so toDaemonEvidence's copy is gone — evidence flows through unchanged.
target-verification.ts's planPreDispatchTargetVerification used
resolveRecordedTarget (operation 2, resolve) over an empty node tree purely
to read its parse-invalid reason — a resolve call standing in for a parse
call, even though readSelectorExpression (operation 1, parse) exists to
answer exactly that question and was already unused inside the engine.
Replaced with port.readSelectorExpression('ordinary', [token]). The mapping
is not 'invalid' -> skip: production's 'ordinary'/'wait' grammars only ever
record a boundary once it has already parsed, so a single malformed token
can only come back 'not-applicable' there ('invalid' is unreachable from
this call site on the production adapter). Both non-'expression' outcomes
map to skip, matching the historical behavior (a single parse-invalid reason
covered both cases). platform dropped from the function's params — it was
only ever threaded to the resolve call this replaces.
Added a contract-suite cell pinning the exact (diverging) discriminant each
adapter reports for a selector-shaped-but-malformed bare token, and why the
divergence is harmless for the one real consumer.
…#1555 structural-quality review) step-loop.ts (810 LOC) splits three ways, following packages/maestro's own precedent: - internal/runtime-port-types.ts: the AdReplayStepRuntime boundary vocabulary (all the neutral types the engine/daemon exchange). - internal/verify-dispatch.ts: verifyAndDispatchStep + its dispatchNoGuard/ dispatchWithGuard helpers. - internal/step-loop.ts: runAdReplay itself plus the terminal-close/ executable-action structural logic (isExecutableReplayAction, resolveSuppressedTerminalCloseIndex). packages/ad-replay/src/index.ts's type exports now source from runtime-port-types.ts. step-loop.test.ts's AdReplayStepRuntime import moves to the new path (no assertion changes). src/daemon/handlers/session-replay-runtime-engine-adapter.ts (553 LOC after item 1's twin removal) shrinks to 294 via two further extractions: - session-replay-dispatch-narrowing.ts: the wire `details` bag -> typed evidence narrowing and dispatch-failure classification. - session-replay-runtime-step-support.ts: ReplayStepContext (moved here to avoid a cycle with the adapter, which re-exports it by name) plus the failure-wrapping/diagnostics-support helpers. Final LOC: adapter 294, dispatch-narrowing 148, step-support 153, step-loop 225, verify-dispatch 246, runtime-port-types 374.
…n.ts + terminal-lifecycle test rename resume.test.ts covers resolveReplayEntryIndex directly (previously only exercised transitively through the daemon's session-replay-runtime-plan tests): no --from/--plan-digest, the paired-flags requirement, in-range --from, out-of-range rejection, stale-digest rejection, the authorized empty-tail boundary (actionCount + 1) gated on a matching watermark, and the unperformed-record-and-heal growth check. Counterfactual run and restored: widening describeOutOfRangeResumeFrom's bound turns the out-of-range/ empty-tail-without-watermark assertions red (2 failures observed). target-verification.test.ts covers all four engine policy functions directly: the two plan* pre-capture gates and the two derive* post-dispatch evidence builders, including item 2's own new decision surface (a fake ReplaySelectorPort proving both non-'expression' readSelectorExpression outcomes map to skip). Counterfactual run and restored: narrowing the check to the literal `'invalid' -> skip` reading turns the 'not-applicable' case red (reports recorded-unverifiable instead of skip). session-replay-terminal-lifecycle.test.ts renamed to session-replay-runtime-keep-session.test.ts: its production module (session-replay-terminal-lifecycle.ts) was already deleted by the #1554 fold-in, and its six cases drive the full runReplayScriptFile round trip against a real SessionStore (including daemon-only postconditions the engine's step loop never reaches) rather than testing engine policy through the façade in isolation — the engine's own terminal-close-suppression decision already has direct, cheaper coverage in step-loop.test.ts. No assertion changes; both files' header comments cross-reference the split.
… to end collectReplayScrubbableVarValues(scope) was called fresh at 5 separate return points inside one verifyAndDispatchStep invocation plus once more in handleActionFailure — always the same result, since nothing between them mutates scope. step-loop.ts's runAdReplay now computes scrubVars ONCE per step, right after resolveReplayAction (the one call that can grow the scope's expanded-builtins set), and threads it as a plain readonly AdReplayScrubValue[] value; verify-dispatch.ts no longer imports ReplayVarScope or collectReplayScrubbableVarValues at all. "One name" end to end: the daemon's TargetBindingDivergenceContext.scrubVars and withReplayFailureDiagnostics's scrubVars param used a separately-derived ReturnType<typeof collectReplayScrubbableVarValues> (mutable array) instead of the engine's own AdReplayScrubValue, requiring a [...scrubVars] copy at every daemon call site to satisfy the mutable-array type. Both now use readonly AdReplayScrubValue[]/readonly ReplayVarScrubEntry[] (structurally identical, already readonly-safe downstream — scrubReplayVarValues and createReplayDivergenceSanitizer already accepted readonly arrays), so the four [...scrubVars] copies in session-replay-runtime-engine-adapter.ts are gone.
createAdReplayStepRuntime's lastObservation closure lives for the whole
replay run (one factory call covers every step), but was never reset
between steps. Every current buildTargetBindingFailure call site happens to
be preceded by this same step's own captureObservation, so the
`lastObservation ?? { reason: 'observation-missing' }` fallback could never
actually fire — but if it ever did (a future call path reaching
buildTargetBindingFailure without capturing first), it would silently
attach the PREVIOUS step's screen instead of reporting the missing-capture
condition the fallback message claims.
armStep runs exactly once per step, before any of that step's other
capabilities (verified against step-loop.ts's runAdReplay loop order) — the
natural per-step boundary. It now clears lastObservation first. No behavior
change on any reachable path today (full daemon + ad-replay suite: 1766/1766
green); an unrelated device-claim-prune contention flake was observed once
and did not reproduce on isolated or full-suite reruns.
… symbols Four comments named symbols/paths that no longer exist, left behind by earlier review passes describing PR history rather than the current constraint: - session-replay-runtime-step-support.ts / session-replay-runtime.ts (2 sites): referenced a function called executeStep, which was never reintroduced under that name after the P5 split — the actual mechanism is the runtime's dispatch/build-failure capabilities recording into the lastResponse side-map. - session-replay-runtime.ts: referenced an engine collectArtifactPaths capability that does not exist — artifactPaths is a daemon-side Set the adapter mutates via collectReplayActionArtifactPaths. - packages/ad-replay/src/internal/selector-port.ts: pointed at ./testing/in-memory-selector-port.ts, the in-memory adapter's pre-stage-D location — it has lived at src/__tests__/test-utils/in-memory-replay-selector-port.ts since. - session-replay-repair-hint.ts / session-replay-runtime-step-support.ts (2 sites): named target-identity.ts, which does not exist (the real file is target-identity-node.ts); the second site additionally mislabeled classifyReplayTarget as engine-side when it is daemon-side (session-replay-target-classification.ts). Comment-only; no behavior change.
…d owner packages/ad-replay/src/internal/inspect.ts's declaredScriptPlatform and src/daemon/replay-device-selection.ts's readScriptReplaySelection each kept their own copy of the same "platform declared before the first open" scan over runtime/open actions — .ad script semantics, not engine or daemon policy, needed independently by ad-replay's plan-digest precedence and the daemon's device-selection platform resolution. Verified this was a genuine duplicate (not the single-sourced state I initially reported): readScriptReplaySelection's platform-tracking loop computes the identical result via a differently-shaped traversal fused with its own app-target scan. resolveDeclaredScriptPlatform now lives in packages/ad-script (its natural owner: the one package both ad-replay and the daemon already depend on, avoiding the R11 issue that justified the original duplication). The daemon's app-target scan stays its own separate pass; fusing it back into the shared function would smuggle a daemon-only concern into ad-script for no measurable cost (the actions array is small, and the shared function already stops at the same point the app-target scan needs to look).
…çade Described "target-identity, variable substitution, plan-digest, and report primitives" — the wide pre-#1555-review façade shape. Vars/identity/report vocabulary moved to ad-script/daemon across the P5 and #1555 review passes; the package now exports exactly inspectAdReplay/runAdReplay plus the neutral AdReplayStepRuntime vocabulary. Description updated to match.
… adapter A simplicity audit judged session-replay-runtime-step-support.ts a size-target fragment, not a concern boundary: four unrelated concerns, one consumer, and a header comment admitting it existed to satisfy the <300 LOC metric. Folded back; the previously-exported helpers are module-private again; the adapter's honest size is renegotiated from the plan metric (dispatch-narrowing stays extracted — it has one nameable job).
49d4d70 to
6a69554
Compare
|
Re-reviewed exact head Readiness is still pending exact-head practical evidence: now that #1542 is fixed, run and attach the prescribed iOS checkout corpus plus the extended target-v1/divergence/resume/save-script evidence at this head. Then refresh the PR body to remove the stale #1542 blocker and future-tense evidence language. The PR remains draft and should not receive |
|
Post-merge evidence round (branch head Fully green: Android suite (checkout 20/20, gesture-lab 32/32, helper backend/version probe-verified), iOS gesture-lab 3/3, and all seven extended legs with device-state proofs — target-v1 annotation quoted; recorded replay green; wrong-screen iOS checkout-form: 0/3 this round, all failures attributed to known/new main-side issues, none to this PR's content (the pre-merge 2×2 matrix proved main fails/passes identically): twice the #1569 signature ( Net: the extraction's own behavior is fully evidenced; the checkout corpus's reliability now tracks #1569/#1571, both filed with mechanisms and owned separately. Generated by Claude Code |
refactor(daemon): extract native .ad replay to packages/ad-replay (#1478 P5)
Implements P5 per the approved design of record (proposal, binding amendment), built as staged, individually-gated commits on one branch, cutting over atomically in this PR. Step 2 of the approved sequence (behavior pinning) merged separately as #1552 and is this PR's regression net — none of those tests' assertions changed anywhere in this branch. All four findings from the first review are addressed in commits
281f33599..947b90ca3; the response comment maps them one-by-one.The façade — final shape (structural-quality round)
packages/ad-replayexports exactlyinspectAdReplayandrunAdReplayas values, plus the enumerated neutral runtime vocabulary (20 types: runtime bag, outcomes/failures, manifest, evidence, and theReplaySelectorPortfamily) — the maestro-shaped typed façade, pinned by the exact-symbol gate inscripts/layering/package-boundaries.test.ts(oxc static-export table; bareexport *and default exports throw; plant-verified). Packageexportsmap is exactly".".The façade design went through a deliberate reversal during review: an earlier round required hiding all types behind entrypoint signatures, which produced a root-side derivation shim, four daemon twin types, and a readonly→mutable copy translator; the structural-quality review reversed that rule, and this branch now exports the typed vocabulary directly — shim, twins, and translator deleted, daemon consumers use the engine's own types.
Engine internals follow the maestro precedent:
runtime-port-types.ts(374 LOC, boundary vocabulary),verify-dispatch.ts(245, verify-then-dispatch policy),step-loop.ts(237, the loop + terminal-close). The daemon engine-adapter is 314 LOC (target was <300; the overage is thelastObservationper-step fix's load-bearing comment, kept deliberately). The engine's parse gate honors the port's own contract (readSelectorExpression('ordinary', [token]), with a contract-suite cell pinning adapter parity on malformed bare tokens).inspectAdReplay's manifest carriesplanDigest/resolveEntryIndex; resume validation stays eager by design (a rejected--frommust not mutate coordinator state — pinned by test).AdReplayGuardMismatchEvidence/AdReplayLandmarkMismatchEvidence), never a wiredetailsbag.DaemonResponsein either direction; the daemon side-map preserves the exact wire object.${VAR}interpolation is a linear scanner in ad-script after a CodeQL polynomial-redos find — 200k-trial differential fuzz, zero mismatches); scrub values are computed once per step under one name.Shared vocabulary went to its owner
Per the review's "proper shared owner" alternative, measured per symbol: the identity vocabulary (
annotationLocalIdentity,matchesLocalIdentity,matchesAncestryPrefix,LocalIdentity,identityFieldMismatches,firstAncestryMismatch) andclassifyTargetBindingMatch(+vars.ts) moved topackages/ad-script— they interpret.ad/TargetAnnotationV1semantics and are consumed by recording-side root code (and, for vars, the Maestro path).session-replay-report-action.ts/session-replay-suggestion-ranking.tsmeasured as root-only consumers and moved back to the daemon (undoing an over-move). ad-script's façade additions are covered by its existing boundary row.The selector port
Three operations (
readSelectorExpression,resolveRecordedTarget— same-alternative winner+domain invariant implemented in the production adapter, lifted verbatim;buildSelectorCandidates), trafficking only in strings, kernel snapshot values, and tagged unions. Production adapter:src/daemon/replay-selector-port.ts. In-memory adapter:src/__tests__/test-utils/in-memory-replay-selector-port.ts— now honorsReplaySelectorCandidateOptions.nodeswith production's exact shared-ID drop semantics. Contract suite: 9 cells × 2 adapters (18 tests), including the shared-ID demotion cell (counterfactual: ignoringnodesfails the in-memory leg while production stays green). Two AST-needing helpers (resolveReplaySuggestionCandidate,readReplaySelectorDisplayValue) are daemon-side plain exports beside the adapter — provably inexpressible through the port without leaking the AST.Invalid-backend rejection restored
prepareReplayPlanrejects any non-maestroreplayBackendwith the byte-identicalINVALID_ARGSmessage from main, before any inspection or session work. Handler-level regression test proves zero step dispatch (counterfactual: without the guard, the script executed); a companion test pins thatmaestrostill routes.What moved / stayed (final)
${VAR}vars module (shared with the Maestro path).format.tsrouting (above both engines), report-action + suggestion-ranking,sanitizeIdentity/describeCandidate(pinned by snapshot-lines),target-identity-node.ts/target-evidence-tree.ts(shared with dispatch/recording).Rebase: #1554's keep-session absorbed into the engine (head
e6cbe6b76)After #1554 merged, this branch rebased onto main and folded its terminal-lifecycle policy into the P5 architecture rather than keeping a parallel decider:
resolveSuppressedTerminalCloseIndex/countExecutedReplayActionsunified with the engine's repair terminal-close predicate inside the step loop (one OR'd suppression condition,keepSession || runtime.isRepairArmed(), checked dynamically afterarmStepso a first-time--save-scriptarm is visible);AdReplayRunRequestgained one field (keepSession); the daemon'ssession-replay-terminal-lifecycle.tsmodule is deleted (no duplicateisExecutableReplayActionanywhere); theSessionStorepostcondition stays daemon-side. #1554's six unit tests pass unchanged end-to-end, plus five new package-internalrunAdReplaytests cover the unified policy directly. Thereplayedcount is now a per-dispatched-step counter (fixing the old approximation that over-counted nested-replay markers).Decomposition: the daemon adapter is now four cohesive modules
session-replay-runtime.tswent from ~1100 lines (post-rebase) to 242 — thin orchestration only. Extracted along its natural seams:session-replay-runtime-engine-adapter.ts(473: the runtime-bag capabilities,build*Failureimplementations, side-map mechanics),session-replay-runtime-plan.ts(261: backend validation, manifest inspection, resume-index resolution, Maestro routing),session-replay-runtime-session.ts(219: session preparation, repair preflight, save-script arming). Coordinator construction stays solely in the orchestrator — the ownership test passes untouched, no allowlist changes.Two load-bearing ordering invariants discovered during the extraction are now pinned by counterfactual-verified tests: post-dispatch mismatch divergences report the pre-step artifact snapshot (engine test; counterfactual red showed the failed dispatch's own artifacts leaking in), and a rejected
--from/--plan-digestnever reachesprepareReplaySession's coordinator-mutating writes (plan test; counterfactual red showedpendingRecordAndHealbeing cleared before the failure).Gates (re-run at every stage; latest full chain at head
e6cbe6b76)typecheck / lint / format:check / check:layering (53 tests incl. the new exact-symbol gate) / check:replay-compat (10 mined scripts, 6 tags, 12 digest-pinned entries) / fallow — green. Full vitest: 5351/5352 at the last full run with the only failures being the documented contention-timeout class (isolate-rerun green; the two pid-liveness assertion races are fixed separately in #1556). Fallow baseline: one surgical 8-line addition for the relocated in-memory adapter; full regen deliberately rejected (would silently drop 12 unrelated stale entries).
Live evidence (exact head
947b90ca3, round 2)Standard suites:
checkout-form-android.adgesture-lab-android.adgesture-lab.adcheckout-form.adExtended evidence (Android, constructed via the CLI's own record/replay loop; verbatim log in artifacts):
open --save-script; saved script carries# agent-device:target-v1 {"id":"refresh-metrics","role":"button",...,"verification":"verified"}. (Finding: annotations require arming atopen—close --save-scriptalone yields selector chains without target-v1 evidence, persession-open-surface.tsarming semantics.)REPLAY_DIVERGENCE, classificationselector-miss (matchCount 0) — recorded target evidence did not verify, with a record-and-heal repair suggestion including--from 3 --plan-digest 8ff7b932….replay --from 3 --plan-digest <hash>after correcting the screen —Replayed 2 steps in 0.8s, completed cleanly.@ref, resumed, explicitclose --save-scriptcommitted the repair; repaired script ends with# agent-device:heal-complete, carries fresh"verification":"verified"annotations for every step, and replays green (6 steps, 4.0s). Behavioral observation (informational, consistent with the repair-transaction commit boundary): an armed session's--fromresume does not auto-run a trailing scripted bareclose; the explicit close commits.Artifacts:
/private/tmp/ad-p5-live-artifacts-r2/(28 files incl. the verbatim command log, original/divergence/repaired scripts, per-attempt replay trees). Round-1 artifacts at the pre-review head remain in/private/tmp/ad-p5-live-artifacts/.Residual risks
Later rounds (each gated green)
ac5488628): typed façade + gate widening; parse-gate contract fix; step-loop three-way split; adapter 580→314; package-localresume/target-verificationtests with counterfactuals; keep-session test renamed to match topology; scrub single-naming;lastObservationper-step fix; decayed comments removed;declaredScriptPlatformsingle-sourced in ad-script (a real engine↔daemon duplicate was confirmed and removed).Evidence status (final): rounds 1–4 of live evidence across the branch's history proved both platform suites, the extended target-v1/divergence/resume/keep-session/save-script-rejection/
${VAR}legs, and the #1558/#1563 compose checks (verbatim details in the PR comments). At the final head, a controlled 2×2 attribution matrix (main vs. this branch, same simulator) confirmed the one remaining iOS checkout flake is a main-side contention-triggered gap in the #1563/#1566 interaction — filed as #1569 with full mechanism evidence (stale_acceptsaturates at the cap on every run, main included; the wall-clock retry budget starves under load) — explicitly not a finding against this branch (P5 touches no iOS capture code; main passed and failed identically). Merged on maintainer decision with #1569 as the documented residual, parallel fix in progress; a final quiet-host evidence round was in flight at merge time and its results follow up on this PR.Generated by Claude Code