Skip to content

refactor(replay): move the daemon-side replay port into packages/replay-port #2663

Closed

Description

@thymikee

Purpose

src/daemon/replay/** is the daemon-side replay port: the adapter that implements @agent-device/ad-replay's AdReplayStepRuntime, the Maestro replay runtime, and the replay test command over @agent-device/replay-test. The engine, the codec and the scheduler already live in packages (ad-replay 2,253 LOC, ad-script 2,376, replay-test 2,064); the adapter is the last replay code inside the daemon, and #2652 established the destination shape for exactly this kind of port (packages/maestro/src/daemon-port/).

Measured on main 91652a8fc5 (probe over scripts/layering/model.ts, production sources):

value
unit: src/daemon/replay/** + session-replay-coordinator.ts + replay-script-source.ts 37 files, 6,300 LOC
tests that move with it (replay/**/*.test.ts, replay/**/__tests__/) 61 files, 17,450 LOC
inbound from outside the unit 2 files (handlers/session-replay-command.ts, handlers/session-replay-video-owner.ts), both through replay/index.ts
outbound into src/** 13 targets; 2 type-only hubs, 11 value targets touched by 1–6 files each
package deps already used kernel 31, contracts 17, host-kit 13, device-selection 3, command-registry 3, capture-kit 3, session-journal 2, selectors 1
churn, last 300 commits ~80 touches across the unit

The unit is already dependency-inverted on the session spine: handlers pass ReplaySession (bound ReplaySessionStore / ReplaySessionMutationStore / ReplaySessionObservationStore from the coordinator) and an invoke function. The unit reads req.internal in three places (publicNetworkOnly ×2, resolvedSessionScope ×1); both become explicit fields on the port's command input, as #2652 did. What is left is the residue below.

What stays in the daemon

  • session-replay-coordinator.ts (277 LOC): the daemon's binding of repair state over session-replay-transaction.ts and session-script-publication-state.ts. Stays; only its ReplaySessionStore/ReplaySessionMutationStore/ReplaySessionObservationStore/ReplayCoordinator types move to the package.
  • session-replay-transaction.ts (133 LOC): repair-transaction state on SessionState, imported by 7 daemon core files (session-store, session-action-recorder, session-script-writer, daemon-idle-reap, session-lifecycle/*, handlers/session-script-publication). Daemon-policy-essential; not part of the move.
  • replay-device-selection.ts (127 LOC, the unit's churniest file at 10 touches): request-binding glue imported by request-binding.ts, depends on open-device-selection.ts. Stays. The two builders the unit takes from it (buildMaestroReplayTargetDeviceResolutionOptions, buildReplayScriptPlatformFlags) arrive through the command input, or move down if they turn out to have package-only deps.

Cut set and disposition (measured; symbols named)

target edges symbols disposition
daemon-request.ts 22, type-only DaemonRequest, DaemonInvokeFn, DaemonResponse, DaemonResponseData, DaemonOpenLifecycle DaemonResponse/DaemonResponseData from @agent-device/kernel/contracts (what #2652 did). The port types its request on the wire shape (command, flags: CommandFlags, meta) and declares its own ReplayInvoke = (request) => Promise<DaemonResponse>, mirroring MaestroDaemonOperationInvoke. DaemonOpenLifecycle (1 use): inspect; inject if it is daemon-private.
session-state.ts 8, type-only SessionState, SessionRuntimeHints Fields the unit actually reads: device, actions, snapshotStart, pendingRecordAndHeal, appBundleId (plus coordinator-owned repairBoundary). Declare a structural ReplaySessionState slice in the package; the daemon passes its real SessionState. Same pattern as #2651's { perfCapture, audioProbe, screenRecording }. Confirm the field set with a grep before declaring it.
response.ts 6 errorResponse, noActiveSessionError 31-LOC failure constructors over DaemonResponse. Either move them to @agent-device/kernel/contracts beside the type (preferred if they carry no daemon semantics) or the port builds failures through AppError as #2652 does. One implementation; no package-local copy.
internal-observation.ts 3 bindInternalObservationAuthority Ref-publication authority over ref-frame/session-snapshot: daemon policy. The unit already receives observationStore; extend that capability with the bound authority and delete the import.
session-target-evidence.ts 3 (+7 daemon importers: selector-runtime, session-action-recorder, selector-recording, interaction/*) computeScrollRegionKey, scrollRegionKeysEqual, boundedLocalIdentity, computeSiblingOrdinal, orderByViewportPosition 300 LOC with package-only deps (kernel, capture-kit, ad-script, contracts). Moves to a package both replay and interaction sit above. Measure the destination against the layering ranks first: ad-script is imported by both consumers today but would gain a capture-kit dep; check that capture-kit does not reach ad-script before choosing.
session-script-publication-state.ts 2 repair helpers (armRepair, abortRepair, repairBoundary, …) Spine state. session-replay-runtime-session.ts's repair preflight is daemon policy: either it stays in the daemon as part of the handler, or ReplayCoordinator (which already wraps armStep/demoteForRerunIfArmed/markCompleteIfArmed) grows the missing operation. No direct import from the package.
session-routing.resolveSessionScope, session-selector.assertSessionSelectorMatches, session-runtime.resolveEffectiveOpenRuntimeHints, snapshot-capture.captureSnapshot, device-claim-conflict.isDeviceClaimConflictReason, open-device-selection.* 1 each Daemon policy or mechanics. Injected through a ReplayDaemonDependencies bag on ReplayCommand/ReplayTestCommand, the shape DaemonMaestroRuntimeDependencies already has.
action-utils.inferFillText 1 21 LOC, contracts-only. Moves down.

Required behavior

  1. Two PRs, same order as refactor(daemon): move the durable-capture admission family to capture-kit #2651. PR A narrows the cut set inside the daemon with zero file moves: kernel response types, the ReplaySessionState slice, the dependencies bag, the observation capability. After PR A, src/daemon/replay/** value-imports nothing under src/daemon/ except replay/index.ts's own internals, and the probe proves it. PR B is the pure move.
  2. Destination: a new packages/replay-port (decided 2026-09-18, see comment). The logical-module policy keeps ad-replay, maestro and replay-test mutually independent, and the port composes all three, so no engine package can host it. replay-port gets its own architecture-ownership.ts entry forbidding src/daemon/, src/providers/, src/compat/; subpaths mirror maestro's daemon-port layout: one for the native+Maestro replay runtime, one for replay test.
  3. Handlers import the owning specifier. No compatibility re-export at src/daemon/replay/index.ts (no-internal-package-reexports).
  4. Tests move with the code, mirroring source topology. Package tests may not import src/__tests__/test-utils; any fixture the moved tests need becomes a package-local fixture or a replay-test export.
  5. Eager closures: new subpath entries fall under NEW_ENTRY_CEILINGS (domain-facade 20, mechanics-surface 71). The unit's kernel/contracts/host-kit reach will exceed 20 as one entry; prefer granular subpaths (refactor(daemon): move the durable-capture admission family to capture-kit #2651 landed with no APPROVED_OVER_CEILING row) over an approval row. Do not work around gates: let the eager-closure budget tolerate a module split without admitting new eager work #2469.
  6. Characterize before moving: the replay unit suites, replay/replay test e2e lanes, and one live replay of a recorded .ad script and one Maestro flow on the shipped commit, per the reported path.

Completion conditions

  • src/daemon/replay/** no longer exists; src/daemon drops by the moved totals (report files and LOC).
  • packages/replay-port imports nothing under src/** (R11); ad-replay, maestro, replay-test gain no new inter-engine edge.
  • The only replay-named files left in src/daemon are session-replay-transaction.ts and replay-device-selection.ts.
  • pnpm depgraph: R4 0, R5 0, R6 not above 3. Eager-closure budgets green.
  • Same observable replay/heal/divergence behavior: characterization suites pass unchanged after the move.

Out of scope

  • Any change to .ad codec, replay semantics, heal, or divergence classification.
  • session-replay-transaction.ts and the repair lifecycle. If the repair preflight cannot be expressed through ReplayCoordinator, keep it in the handler and report it, do not widen the package.

Dependencies

None. Independent of #2658 (different package). Successor to #2544, same shape.

Activity

  1. thymikee commented on Sep 18, 2026

    @thymikee
    MemberAuthor

    Decisions after #2668: destination, req.internal, and the remaining cut set

    Two corrections to the body, both from #2668's findings.

    1. req.internal is read in the unit. My probe excluded paths containing internal/, which is where every unit file lives. The reads are internal?.publicNetworkOnly (twice, Maestro runtime) and internal?.resolvedSessionScope (once). Both become explicit fields on the port's command input, the way refactor(maestro): move the daemon-side runtime port out of src/daemon #2652 put publicNetworkOnly on CreateDaemonMaestroRuntimeOperationsOptions. The port never receives DaemonRequestInternal.
    2. Destination is a new package, not ad-replay. The logical-module policy (scripts/layering/architecture-ownership.ts) keeps the engines mutually independent: ad-replay forbids packages/maestro/, maestro forbids packages/ad-replay/, replay-test forbids both. The port composes all three (six files import @agent-device/maestro; session-replay-maestro-failure.ts also imports @agent-device/ad-replay/divergence; test-command.ts schedules over replay-test). Splitting per engine does not work for the same reason. Decision: packages/replay-port, with a policy entry whose forbiddenTargetRoots are src/daemon/, src/providers/, src/compat/, and whose allowed engine imports are ad-replay, maestro, replay-test, ad-script. It is the package form of what src/daemon/replay/ is today; the engines stay independent below it. Subpaths: one for the native+Maestro replay runtime, one for replay test, mirroring maestro's daemon-port layout; eager-closure ceilings per the body.

    Remaining cut set at 94a0b6115a (12 targets; unit = src/daemon/replay/** + session-replay-coordinator.ts + replay-script-source.ts):

    target edges disposition
    daemon-request.ts (type) 14: DaemonRequest, DaemonInvokeFn, DaemonResponse, DaemonOpenLifecycle wire-shaped request type + package-local ReplayInvoke; DaemonResponse from kernel/contracts (4 leftover sites); DaemonOpenLifecycle injected or dropped after inspection
    session-state.ts (type) 7: SessionState, SessionRuntimeHints structural ReplaySessionState slice; measured reads: device, actions, snapshotStart, pendingRecordAndHeal, appBundleId
    session-target-evidence.ts 3 move to a package below both replay and src/daemon/interaction/ (7 daemon importers); check capture-kit ↛ ad-script before choosing ad-script
    replay-device-selection.ts 2 the two builders arrive through the command input, or move down if package-only
    session-replay-transaction.ts 1 (5 symbols, session-replay-coordinator.ts) the coordinator is the daemon's binding of repair state and stays in the daemon; only its ReplaySessionStore/MutationStore/ObservationStore/ReplayCoordinator types move to the package. Unit = src/daemon/replay/** + replay-script-source.ts from here on
    session-script-publication-state.ts 1 same: expressed through ReplayCoordinator, or the preflight stays in the handler
    session-routing, session-selector, session-runtime, snapshot-capture, device-claim-conflict 1 each ReplayDaemonDependencies bag on the command input
    action-utils.inferFillText 1 moves down (contracts-only)

    Second half of PR A: everything in the table with zero file moves, then the probe shows src/daemon/replay/** value-imports nothing under src/daemon/ outside itself. PR B: the move into packages/replay-port.

  2. changed the title [-]refactor(replay): move the daemon-side replay port into packages/ad-replay[/-] [+]refactor(replay): move the daemon-side replay port into packages/replay-port[/+] on Sep 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions