Skip to content

Commit 94a0b61

Browse files
authored
refactor(replay): move the response and observation vocabulary out of the daemon (#2668)
* refactor(replay): move the response and observation vocabulary out of the daemon First half of PR A for #2663: the replay unit stops importing two daemon modules, with no file moved and no behaviour changed. - `errorResponse`/`noActiveSessionError`/`DaemonFailureResponse` move to `@agent-device/kernel/contracts` beside the `DaemonResponse` shape they build (#2663's preferred disposition: they carry no daemon semantics). `src/daemon/ response.ts` keeps a daemon-local alias so the 58 non-replay handlers are untouched; the unit imports the owning specifier. `DaemonResponse`/ `DaemonResponseData` now come from kernel/contracts too, cutting all 22 `daemon-request.ts` response-type edges. - The ref-publication authority vocabulary (opaque evidence, capture, projection, result, authority) moves to `@agent-device/contracts/replay`, the cross-layer contract home both `internal-observation.ts` and the replay unit already sit above. `src/daemon/replay/**` no longer imports `src/daemon/internal-observation.ts`: `ReplaySession.observationStore` carries a `bindAuthority(signal)` binder the handler draws from the bound session store, so the unit gets the capability without naming the store pair or the session name. `sessionName` threaded through four capture helpers purely to re-bind that authority is now dead and removed. Two findings against the issue, both verified: - `req.internal` IS read in the unit (3 sites: `session-replay-action-runtime.ts:154`, `session-replay-maestro-runtime.ts:132,330`, `test-command.ts:237`), contrary to "No file in the unit reads `req.internal`". Those, plus the 10 remaining `DaemonRequest`/`DaemonInvokeFn` edges, need the neutral dispatch envelope #2652 established — the next step of PR A. - The stated destination `packages/ad-replay/src/daemon-port/` conflicts with `LOGICAL_MODULE_POLICIES`: `ad-replay` forbids `packages/maestro/`, and the six Maestro-side replay files import `@agent-device/maestro`, while `session-replay-maestro-failure.ts` also imports `@agent-device/ad-replay/ divergence`, so no existing engine package can host the port as written. Part of #2663 * refactor(replay): repoint response constructors at kernel/contracts and delete the alias Review finding on #2668: `src/daemon/response.ts` as a re-export gave `errorResponse`, `noActiveSessionError`, `NO_ACTIVE_SESSION_MESSAGE` and `DaemonFailureResponse` two import paths, which the no-internal-re-export rule refuses. Completing the migration instead of keeping the alias: all 58 remaining importers now name `@agent-device/kernel/contracts` and `src/daemon/response.ts` is gone. No gate table named the deleted path. Also picking up the review's non-blocking notes: - `sessionName`, left unread by `buildReplayFailureDivergence`, `TargetBindingDivergenceContext` and the two target-binding response builders once `sessionName` stopped re-binding the observation authority, is removed from those signatures and from every caller. `ctx.sessionName` stays: `dispatchStep` still passes it to `invokeReplayAction`. - Six `@agent-device/kernel/contracts` imports the first commit inserted between a function's JSDoc and the declaration are hoisted into the file's import block, so each doc comment attaches to what it documents again. Part of #2663
1 parent c576493 commit 94a0b61

88 files changed

Lines changed: 265 additions & 269 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎packages/contracts/src/facades/replay.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ export type { RefFrameEffect } from '../ref-frame-effect.ts';
22
export { REPLAY_TARGET_GUARD_MISMATCH_REASON, WAIT_LANDMARK_MISMATCH_REASON } from '../replay.ts';
33
export type {
44
ReplayCommandResult,
5+
ReplayObservationAuthority,
6+
ReplayObservationAuthorityBinder,
7+
ReplayObservationCapture,
8+
ReplayObservationEvidence,
59
ReplayScriptSourceBundle,
610
ReplaySuiteAttemptFailure,
711
ReplaySuiteResult,
@@ -10,6 +14,8 @@ export type {
1014
ReplaySuiteTestResult,
1115
ReplaySuiteTestSkipReason,
1216
ReplaySuiteTestSkipped,
17+
ReplayRefPublicationProjection,
18+
ReplayRefPublicationResult,
1319
ReplayTargetGuardDenotation,
1420
WaitLandmarkMismatchEvidence,
1521
} from '../replay.ts';

‎packages/contracts/src/replay.ts‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { DaemonError } from '@agent-device/kernel/errors';
2+
import type { SnapshotState } from '@agent-device/kernel/snapshot';
23
import type { SnapshotDiagnosticsSummary } from './snapshot-diagnostics.ts';
34
import type {
45
LocalIdentity,
@@ -174,3 +175,49 @@ export type ReplayScriptSourceBundle = Readonly<{
174175
entry: string;
175176
files: Readonly<Record<string, string>>;
176177
}>;
178+
179+
declare const REPLAY_OBSERVATION_EVIDENCE: unique symbol;
180+
181+
/**
182+
* Opaque capture lineage a replay engine may carry as data but cannot use to publish refs.
183+
* Only the daemon-owned finalizer behind {@link ReplayObservationAuthority} can resolve it.
184+
*/
185+
export type ReplayObservationEvidence = {
186+
readonly [REPLAY_OBSERVATION_EVIDENCE]: true;
187+
};
188+
189+
export type ReplayObservationCapture = Readonly<{
190+
evidence: ReplayObservationEvidence;
191+
refsGeneration: number;
192+
}>;
193+
194+
export type ReplayRefPublicationProjection = Readonly<{
195+
refsGeneration: number | undefined;
196+
refs: readonly string[];
197+
}>;
198+
199+
export type ReplayRefPublicationResult =
200+
| Readonly<{ published: true; refsGeneration: number; refCount: number }>
201+
| Readonly<{
202+
published: false;
203+
reason: 'empty' | 'cancelled' | 'stale-capture' | 'invalid-projection';
204+
}>;
205+
206+
/**
207+
* The daemon-owned authority a replay run uses to record an operational capture and, once its
208+
* own projection is exact, to activate exactly the refs that projection exposed. The capability is
209+
* already bound to one admitted session: a holder cannot name or reacquire another one.
210+
*/
211+
export type ReplayObservationAuthority = Readonly<{
212+
store(snapshot: SnapshotState): ReplayObservationCapture;
213+
finalize(
214+
evidence: ReplayObservationEvidence,
215+
projection: ReplayRefPublicationProjection,
216+
): ReplayRefPublicationResult;
217+
}>;
218+
219+
/**
220+
* Binds that authority for one request scope. A replay run receives the binder, never the
221+
* session-store pair the binding is drawn from.
222+
*/
223+
export type ReplayObservationAuthorityBinder = (signal?: AbortSignal) => ReplayObservationAuthority;

‎packages/kernel/src/contracts.ts‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,41 @@ export type DaemonResponse =
171171
error: DaemonError;
172172
};
173173

174+
export type DaemonFailureResponse = Extract<DaemonResponse, { ok: false }>;
175+
176+
/**
177+
* The one `DaemonResponse` failure constructor. Declared beside the response type so a
178+
* command-side port that returns — rather than throws — a dispatch failure builds the same
179+
* wire shape as a daemon handler without importing the daemon.
180+
*/
181+
export function errorResponse(
182+
code: string,
183+
message: string,
184+
details?: Record<string, unknown>,
185+
options?: { hint?: string; retriable?: boolean },
186+
): DaemonFailureResponse {
187+
return {
188+
ok: false,
189+
error: {
190+
code,
191+
message,
192+
...(options?.hint ? { hint: options.hint } : {}),
193+
...(options?.retriable === undefined ? {} : { retriable: options.retriable }),
194+
...(details ? { details } : {}),
195+
},
196+
};
197+
}
198+
199+
export const NO_ACTIVE_SESSION_MESSAGE = 'No active session. Run open first.';
200+
201+
/**
202+
* Shared "No active session. Run open first." failure used by handlers that require
203+
* an open session before dispatching.
204+
*/
205+
export function noActiveSessionError(): DaemonFailureResponse {
206+
return errorResponse('SESSION_NOT_FOUND', NO_ACTIVE_SESSION_MESSAGE);
207+
}
208+
174209
export type JsonRpcId = string | number | null;
175210

176211
export type JsonRpcRequestEnvelope<TParams = unknown> = {

‎src/daemon/__tests__/runtime-binding-conformance.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@ import type {
2323
InspectDeviceRuntimeFacts,
2424
RuntimeAdmissionBindings,
2525
} from '../request-runtime-binding.ts';
26-
import type { DaemonFailureResponse } from '../response.ts';
2726
import { dispatchSnapshotDiffViaRuntime } from '../snapshot-diff-runtime.ts';
2827
import { resolveBoundTvRemoteRuntime } from '../tv-remote-runtime.ts';
2928
import { resolveBoundTypeTextRuntime } from '../type-text-runtime.ts';
3029
import { resolveBoundViewportRuntime } from '../viewport-runtime.ts';
30+
import type { DaemonFailureResponse } from '@agent-device/kernel/contracts';
3131

3232
type RuntimeOperation = RuntimeOperationKey<PlatformRuntimeOperations>;
3333

‎src/daemon/app-event-runtime.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import { successText } from '@agent-device/kernel/success-text';
77
import type { DaemonCommandContext } from './context.ts';
88
import { admitRuntimeUse, type RuntimeAdmissionBindings } from './runtime-admission.ts';
99
import { runtimeExecutionFromContext } from './snapshot-runtime-capture-input.ts';
10-
import type { DaemonFailureResponse } from './response.ts';
1110
import type { DeviceReadyOptions } from './device-ready.ts';
11+
import type { DaemonFailureResponse } from '@agent-device/kernel/contracts';
1212

1313
/**
1414
* What the admit-then-bind step reports: either the refusal an unadmitted cell produced, or the

‎src/daemon/device-claim-conflict.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
type InspectedDeviceClaim,
1717
} from './device-claim-inspection.ts';
1818
import type { DaemonResponse } from './daemon-request.ts';
19-
import { errorResponse } from './response.ts';
19+
import { errorResponse } from '@agent-device/kernel/contracts';
2020

2121
export type DeviceClaimConflictReason =
2222
| 'DEVICE_CLAIM_LIVE_OWNER'

‎src/daemon/generic-runtime-execution.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import type { ResolvedGenericExecution } from './request-generic-dispatch.ts';
2-
import { errorResponse } from './response.ts';
32
import { resolveBoundFocusRuntime } from './focus-runtime.ts';
43
import { resolveScreenshotGenericExecution } from './screenshot-runtime.ts';
54
import { resolveBoundScrollRuntime } from './scroll-runtime.ts';
@@ -13,6 +12,7 @@ import { resolveBoundHomeRuntime } from './home-runtime.ts';
1312
import { resolveBoundAppSwitcherRuntime } from './app-switcher-runtime.ts';
1413
import { resolveBoundOrientationRuntime } from './orientation-runtime.ts';
1514
import { resolveBoundTvRemoteRuntime } from './tv-remote-runtime.ts';
15+
import { errorResponse } from '@agent-device/kernel/contracts';
1616

1717
/**
1818
* The generic route's runtime-owned leaves (ADR 0019). Each one admits its own exact owner facts

‎src/daemon/gesture-runtime.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,13 @@ import type { DeviceInfo } from '@agent-device/kernel/device';
1717
import { AppError, normalizeError } from '@agent-device/kernel/errors';
1818
import type { Rect } from '@agent-device/kernel/snapshot';
1919
import type { DaemonCommandContext } from './context.ts';
20-
import type { DaemonFailureResponse } from './response.ts';
2120
import {
2221
admitRuntimeOperations,
2322
admitRuntimeUse,
2423
type RuntimeAdmissionBindings,
2524
} from './runtime-admission.ts';
2625
import { runtimeExecutionFromContext } from './snapshot-runtime-capture-input.ts';
26+
import type { DaemonFailureResponse } from '@agent-device/kernel/contracts';
2727

2828
/**
2929
* One request's bound gesture authority. `gesture` and `swipe` both build their plans inside the

‎src/daemon/handlers/react-native.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@ import type { SnapshotQualityVerdict, SnapshotState } from '@agent-device/kernel
1616
import { isSparseSnapshotQualityVerdict } from '@agent-device/capture-kit/snapshot-quality-verdict';
1717
import type { DaemonResponse } from '../daemon-request.ts';
1818
import type { SessionState } from '../session-state.ts';
19-
import { errorResponse, noActiveSessionError } from '../response.ts';
2019
import {
2120
captureSnapshotForSession,
2221
finalizeTouchInteraction,
2322
type InteractionRouteInput,
2423
} from '../interaction/index.ts';
2524
import { expireRefFrame } from '../ref-frame.ts';
2625
import { readSnapshotNodesReferenceFrame } from '@agent-device/capture-kit/touch-reference-frame';
26+
import { errorResponse, noActiveSessionError } from '@agent-device/kernel/contracts';
2727

2828
export async function handleReactNativeCommands(
2929
params: InteractionRouteInput,

‎src/daemon/handlers/session-app-deployment.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import { resolvePayloadInput } from '../payload-input.ts';
1616
import { resolveDeployResultTarget } from '../../core/deploy-result-target.ts';
1717
import { withSuccessText } from '@agent-device/kernel/success-text';
1818
import { recordSessionAction } from '../session-action-recorder.ts';
19-
import { errorResponse } from '../response.ts';
2019
import {
2120
requireSessionOrExplicitSelector,
2221
resolveCommandDevice,
@@ -27,6 +26,7 @@ import {
2726
type RuntimeCommandHandlerParams,
2827
unavailableRuntimeOperationResponse,
2928
} from '../session-runtime-admission.ts';
29+
import { errorResponse } from '@agent-device/kernel/contracts';
3030

3131
type DeployCommand = 'install' | 'reinstall';
3232

0 commit comments

Comments
 (0)