Repository navigation
refactor(replay): move the daemon-side replay port into packages/replay-port #2663
Description
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 oversession-replay-transaction.tsandsession-script-publication-state.ts. Stays; only itsReplaySessionStore/ReplaySessionMutationStore/ReplaySessionObservationStore/ReplayCoordinatortypes move to the package.session-replay-transaction.ts(133 LOC): repair-transaction state onSessionState, 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 byrequest-binding.ts, depends onopen-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
- 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
ReplaySessionStateslice, the dependencies bag, the observation capability. After PR A,src/daemon/replay/**value-imports nothing undersrc/daemon/exceptreplay/index.ts's own internals, and the probe proves it. PR B is the pure move. - Destination: a new
packages/replay-port(decided 2026-09-18, see comment). The logical-module policy keepsad-replay,maestroandreplay-testmutually independent, and the port composes all three, so no engine package can host it.replay-portgets its ownarchitecture-ownership.tsentry forbiddingsrc/daemon/,src/providers/,src/compat/; subpaths mirrormaestro'sdaemon-portlayout: one for the native+Maestro replay runtime, one forreplay test. - Handlers import the owning specifier. No compatibility re-export at
src/daemon/replay/index.ts(no-internal-package-reexports). - 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 areplay-testexport. - Eager closures: new subpath entries fall under
NEW_ENTRY_CEILINGS(domain-facade20,mechanics-surface71). 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 noAPPROVED_OVER_CEILINGrow) over an approval row. Do not work around gates: let the eager-closure budget tolerate a module split without admitting new eager work #2469. - Characterize before moving: the replay unit suites,
replay/replay teste2e lanes, and one live replay of a recorded.adscript and one Maestro flow on the shipped commit, per the reported path.
Completion conditions
src/daemon/replay/**no longer exists;src/daemondrops by the moved totals (report files and LOC).packages/replay-portimports nothing undersrc/**(R11);ad-replay,maestro,replay-testgain no new inter-engine edge.- The only replay-named files left in
src/daemonaresession-replay-transaction.tsandreplay-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
.adcodec, replay semantics, heal, or divergence classification. session-replay-transaction.tsand the repair lifecycle. If the repair preflight cannot be expressed throughReplayCoordinator, 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
refactor(replay): repoint response constructors at kernel/contracts a…
refactor(replay): move the response and observation vocabulary out of…
thymikee commentedon Sep 18, 2026
Decisions after #2668: destination,
req.internal, and the remaining cut setTwo corrections to the body, both from #2668's findings.
req.internalis read in the unit. My probe excluded paths containinginternal/, which is where every unit file lives. The reads areinternal?.publicNetworkOnly(twice, Maestro runtime) andinternal?.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 putpublicNetworkOnlyonCreateDaemonMaestroRuntimeOperationsOptions. The port never receivesDaemonRequestInternal.ad-replay. The logical-module policy (scripts/layering/architecture-ownership.ts) keeps the engines mutually independent:ad-replayforbidspackages/maestro/,maestroforbidspackages/ad-replay/,replay-testforbids both. The port composes all three (six files import@agent-device/maestro;session-replay-maestro-failure.tsalso imports@agent-device/ad-replay/divergence;test-command.tsschedules overreplay-test). Splitting per engine does not work for the same reason. Decision:packages/replay-port, with a policy entry whoseforbiddenTargetRootsaresrc/daemon/,src/providers/,src/compat/, and whose allowed engine imports aread-replay,maestro,replay-test,ad-script. It is the package form of whatsrc/daemon/replay/is today; the engines stay independent below it. Subpaths: one for the native+Maestro replay runtime, one forreplay test, mirroringmaestro'sdaemon-portlayout; 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):daemon-request.ts(type)DaemonRequest,DaemonInvokeFn,DaemonResponse,DaemonOpenLifecycleReplayInvoke;DaemonResponsefrom kernel/contracts (4 leftover sites);DaemonOpenLifecycleinjected or dropped after inspectionsession-state.ts(type)SessionState,SessionRuntimeHintsReplaySessionStateslice; measured reads:device,actions,snapshotStart,pendingRecordAndHeal,appBundleIdsession-target-evidence.tssrc/daemon/interaction/(7 daemon importers); checkcapture-kit ↛ ad-scriptbefore choosingad-scriptreplay-device-selection.tssession-replay-transaction.tssession-replay-coordinator.ts)ReplaySessionStore/MutationStore/ObservationStore/ReplayCoordinatortypes move to the package. Unit =src/daemon/replay/**+replay-script-source.tsfrom here onsession-script-publication-state.tsReplayCoordinator, or the preflight stays in the handlersession-routing,session-selector,session-runtime,snapshot-capture,device-claim-conflictReplayDaemonDependenciesbag on the command inputaction-utils.inferFillTextSecond half of PR A: everything in the table with zero file moves, then the probe shows
src/daemon/replay/**value-imports nothing undersrc/daemon/outside itself. PR B: the move intopackages/replay-port.[-]refactor(replay): move the daemon-side replay port into packages/ad-replay[/-][+]refactor(replay): move the daemon-side replay port into packages/replay-port[/+]refactor(replay): pass the Maestro script-trust flag as a command inp…
refactor(replay): cut the daemon out of the replay unit's imports (#2663
refactor(replay): bind the session and dispatch envelope inside the u…
refactor(replay): bootstrap packages/replay-port with the builtin-var…
refactor(replay): move source and target decisions into replay-port (#…
refactor(replay): move target classification into replay-port (#2663) (…