Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
0d0030d
proto(reliability): observation engine, readiness poll on promotedTar…
thymikee Sep 27, 2026
0d85953
feat(interaction): operator-only readiness budget for press/click/lon…
thymikee Sep 28, 2026
49ca3eb
test(client): split the readinessTimeoutMs SDK test out of client.tes…
thymikee Sep 28, 2026
5fc0669
feat(interaction): end a readiness wait on a sparse capture; widen th…
thymikee Sep 30, 2026
63ebc04
docs(interaction): document the replay readiness wait and readinessTi…
thymikee Sep 30, 2026
5b23c92
refactor(interaction): keep readiness comments to their constraints
thymikee Sep 30, 2026
c5bd3fd
refactor(move): split interaction target resolution by domain question
thymikee Sep 30, 2026
87cd2b9
refactor(replay): read the readiness-budgeted commands from their dec…
thymikee Sep 30, 2026
78ba97c
refactor(interaction): keep the engine and the readiness wait; loop m…
thymikee Sep 30, 2026
5edfe95
feat(interaction): declare a per-capture deadline mode and report rid…
thymikee Sep 30, 2026
44f537b
fix(interaction): scope readinessTimeoutMs to budgeted commands and c…
thymikee Sep 30, 2026
9890d3b
fix(replay): wait for a late target at the pre-dispatch gate and repo…
thymikee Sep 30, 2026
661c3f8
docs(interaction): describe where readiness evidence appears
thymikee Sep 30, 2026
a1e3461
test(layering): record AdReplayTargetObservation on the ad-replay roo…
thymikee Sep 30, 2026
16f15cc
refactor(interaction): drop the readiness forceFresh plumbing; the pr…
thymikee Sep 30, 2026
09cbe1e
fix(replay): share one readiness budget between the target gate and t…
thymikee Sep 30, 2026
1481f36
fix(command-registry): pin the readiness cap without importing the se…
thymikee Sep 30, 2026
1f8f627
refactor(selectors): one owner for the readiness schedule
thymikee Sep 30, 2026
1b93af6
refactor(capture-kit): observeUntil defaults to no per-capture deadli…
thymikee Sep 30, 2026
b092125
fix(replay): keep a thrown error's typed details on the replay response
thymikee Sep 30, 2026
9c76e9a
test(replay): cancelling the readiness gate ends at the next poll
thymikee Sep 30, 2026
fa7f36a
docs(replay): state that the readiness clock is test injection like A…
thymikee Sep 30, 2026
d476097
fix(capture-kit): a poll never starts after the signal aborted
thymikee Sep 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/ad-replay/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
* and the classification/guard/binding-evidence/verification-routing shapes
* `verifyAndDispatchStep` exchanges with the daemon's `AdReplayStepRuntime`
* implementation (`AdReplayVerificationEntry`, `AdReplayTargetClassification`,
* `AdReplayTargetBindingEvidence`, `AdReplayDispatchGuard`,
* `AdReplayTargetObservation`, `AdReplayTargetBindingEvidence`, `AdReplayDispatchGuard`,
* `AdReplayDispatchOutcome`) ARE named here: the
* daemon builds/reads real values of these shapes directly now (routing in
* `session-replay-target-verification.ts`, wire-narrowing in
Expand Down Expand Up @@ -93,6 +93,7 @@ export type {
AdReplayStepRuntime,
AdReplayTargetBindingEvidence,
AdReplayTargetClassification,
AdReplayTargetObservation,
AdReplayVarSources,
AdReplayVerificationEntry,
} from './internal/runtime-port-types.ts';
Expand Down
16 changes: 4 additions & 12 deletions packages/ad-replay/src/internal/__tests__/step-loop.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,8 @@ function createFakeRuntime(params: { isRepairArmed?: () => boolean } = {}): {
let armCount = 0;
const runtime: AdReplayStepRuntime = {
beginTargetVerification: () => ({ kind: 'inactive' }),
captureObservation: async () => {
throw new Error('captureObservation: not used by this fixture (no targetEvidence)');
},
classifyTarget: () => {
throw new Error('classifyTarget: not used by this fixture (no targetEvidence)');
observeTarget: async () => {
throw new Error('observeTarget: not used by this fixture (no targetEvidence)');
},
async dispatchStep(dispatchedAction, _resolvedAction, _index, artifactPaths) {
dispatched.push(dispatchedAction.command);
Expand Down Expand Up @@ -175,13 +172,8 @@ test("a post-dispatch target-binding mismatch reports the pre-step artifact snap
// called for it — routed to the #1349 deferred-landmark path, which
// dispatches with a guard WITHOUT any capture/classify round trip.
beginTargetVerification: () => ({ kind: 'post-resolution', isSelectorWait: true }),
captureObservation: async () => {
throw new Error(
'captureObservation: not used — deferred-landmark skips straight to dispatch',
);
},
classifyTarget: () => {
throw new Error('classifyTarget: not used — deferred-landmark skips straight to dispatch');
observeTarget: async () => {
throw new Error('observeTarget: not used — deferred-landmark skips straight to dispatch');
},
async dispatchStep(dispatchedAction, _resolvedAction, _index, artifactPaths, _guard) {
if (dispatchedAction.command === 'open') {
Expand Down
36 changes: 15 additions & 21 deletions packages/ad-replay/src/internal/runtime-port-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,9 @@ export type AdReplayVerifiedTargetGuard = Readonly<{
matchCount: number;
}>;

/** `captureObservation`'s neutral result: nodes for classification, or why a capture was not available. */
export type AdReplayObservation = Readonly<
| { readonly state: 'available'; readonly nodes: readonly SnapshotNode[] }
/** `observeTarget`'s neutral result: the recorded target's classification, or why no capture was available. */
export type AdReplayTargetObservation = Readonly<
| { readonly state: 'classified'; readonly classification: AdReplayTargetClassification }
| { readonly state: 'unavailable'; readonly reason: string; readonly hint?: string }
>;

Expand All @@ -120,7 +120,7 @@ export type AdReplayVerificationEntry = Readonly<
}
>;

/** `classifyTarget`'s result: a verified guard, or the divergence evidence a target-binding failure reports. */
/** The recorded target's classification: a verified guard, or the divergence evidence a target-binding failure reports. */
export type AdReplayTargetClassification = Readonly<
| { readonly verified: true; readonly guard: AdReplayVerifiedTargetGuard }
| Readonly<{
Expand Down Expand Up @@ -220,26 +220,20 @@ export type AdReplayStepRuntime = Readonly<{
targetRole?: 'source' | 'destination',
): AdReplayVerificationEntry;
/**
* Captures a fresh snapshot for classification or for a divergence's
* `screen` — daemon authority (`SessionStore`, the capture pipeline, the
* #1385 launch-race retry).
* Captures a fresh snapshot (with the #1385 launch-race retry) and resolves
* the recorded target against it using the SAME lookup/matching a real
* dispatch would — daemon authority (`SessionStore`, the capture pipeline,
* tree helpers and the selectors package engine). A step whose dispatch
* would wait for its target under a readiness budget re-captures while the
* target does not match yet, under that same budget, so this gate never
* refuses a target the dispatch itself would have waited for. The last
* capture is the divergence's `screen`.
*/
captureObservation(
action: SessionAction,
index: number,
options: { retryLaunchRace: boolean },
): Promise<AdReplayObservation>;
/**
* Resolves the recorded target against `nodes` using the SAME
* lookup/matching a real dispatch would — daemon authority (tree helpers and
* the selectors package engine).
*/
classifyTarget(params: {
observeTarget(params: {
action: SessionAction;
index: number;
token: string;
nodes: readonly SnapshotNode[];
}): AdReplayTargetClassification;
}): Promise<AdReplayTargetObservation>;
/**
* Dispatches the action, optionally carrying a pre-action identity guard,
* and detects the guard-mismatch / wait-landmark-mismatch post-resolution
Expand Down Expand Up @@ -279,7 +273,7 @@ export type AdReplayStepRuntime = Readonly<{
): Promise<AdReplayStepFailure>;
/**
* Builds a target-binding divergence from `evidence`, reusing the LAST
* `captureObservation` result for its `screen` (the pre-dispatch capture
* `observeTarget` capture for its `screen` (the pre-dispatch capture
* and classification/capture-failure evidence share one capture) —
* daemon authority. `artifactPaths` is the pre-step snapshot, as above;
* `scrubVars` as above.
Expand Down
32 changes: 13 additions & 19 deletions packages/ad-replay/src/internal/verify-dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ import {
import type {
AdReplayDispatchGuard,
AdReplayDispatchOutcome,
AdReplayObservation,
AdReplayScrubValue,
AdReplayStepOutcome,
AdReplayStepRuntime,
AdReplayTargetObservation,
AdReplayVerifiedTargetGuard,
} from './runtime-port-types.ts';

Expand All @@ -25,8 +25,8 @@ import type {
* those four functions live here — engine-private, never re-exported by the
* façade — and the daemon side is the narrow `AdReplayStepRuntime`
* capabilities this function drives: routing (`beginTargetVerification`),
* capture (`captureObservation`), classification (`classifyTarget`),
* dispatch (`dispatchStep`), and wire-building the resulting divergence
* capture-and-classification (`observeTarget`), dispatch (`dispatchStep`),
* and wire-building the resulting divergence
* (`buildRecordedUnverifiableFailure`, `buildTargetBindingFailure`,
* `buildPostDispatchTargetBindingFailure`). `./step-loop.ts`'s `runAdReplay`
* is this module's one caller.
Expand Down Expand Up @@ -121,11 +121,8 @@ export async function verifyAndDispatchStep(
}
const token = preDispatchPlan.token;

// #1385: this is the pre-dispatch gate a step right after `open --relaunch`
// can race — the app may still be launching/mounting when this capture
// lands. Bounded retry rides out that transition (`retryLaunchRace`).
const observation = await runtime.captureObservation(action, index, { retryLaunchRace: true });
if (observation.state !== 'available') {
const observation = await runtime.observeTarget({ action, index, token });
if (observation.state !== 'classified') {
return {
status: 'failed',
failure: await runtime.buildTargetBindingFailure(
Expand All @@ -138,7 +135,7 @@ export async function verifyAndDispatchStep(
};
}

const classification = runtime.classifyTarget({ action, index, token, nodes: observation.nodes });
const { classification } = observation;
if (classification.verified) {
return dispatchWithGuard(runtime, scrubVars, action, resolvedAction, index, artifactPaths, {
kind: 'target',
Expand Down Expand Up @@ -213,10 +210,12 @@ async function verifyAndDispatchMultiTargetStep(
};
}

const observation = await runtime.captureObservation(endpointAction, index, {
retryLaunchRace: true,
const observation = await runtime.observeTarget({
action: endpointAction,
index,
token: plan.token,
});
if (observation.state !== 'available') {
if (observation.state !== 'classified') {
return {
status: 'failed',
failure: await runtime.buildTargetBindingFailure(
Expand All @@ -229,12 +228,7 @@ async function verifyAndDispatchMultiTargetStep(
};
}

const classification = runtime.classifyTarget({
action: endpointAction,
index,
token: plan.token,
nodes: observation.nodes,
});
const { classification } = observation;
if (!classification.verified) {
return {
status: 'failed',
Expand Down Expand Up @@ -268,7 +262,7 @@ async function verifyAndDispatchMultiTargetStep(
}

function captureUnavailableEvidence(
observation: Extract<AdReplayObservation, { state: 'unavailable' }>,
observation: Extract<AdReplayTargetObservation, { state: 'unavailable' }>,
) {
return {
kind: 'identity-unverifiable' as const,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ type IdentityTreeNode = Pick<RawSnapshotNode, 'type' | 'identifier' | 'label'>;
* (`@agent-device/selectors/target-evidence`), replay-time verification
* (`packages/replay-port/src/daemon-port/session-replay-target-verification.ts`), and the
* dispatch-side post-resolution guard
* (`src/commands/interaction/runtime/resolution.ts`), so all three compute
* (`src/commands/interaction/runtime/replay-target-guard.ts`), so all three compute
* a node's identity with byte-identical semantics.
*/
export function readNodeLocalIdentity(
Expand Down
4 changes: 4 additions & 0 deletions packages/capture-kit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@
"types": "./src/capture-admission/durable-capture-runtime-recovery.ts",
"default": "./src/capture-admission/durable-capture-runtime-recovery.ts"
},
"./observe-until": {
"types": "./src/observe-until.ts",
"default": "./src/observe-until.ts"
},
"./perf-capture-admission-ledger": {
"types": "./src/capture-admission/perf-capture-admission-ledger.ts",
"default": "./src/capture-admission/perf-capture-admission-ledger.ts"
Expand Down
Loading
Loading