Repository navigation
Expand file tree
/
Copy pathgeneric-settle.ts
More file actions
141 lines (134 loc) · 6.15 KB
/
Copy pathgeneric-settle.ts
File metadata and controls
141 lines (134 loc) · 6.15 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
import type { CommandFlags } from '@agent-device/contracts/command';
import type { CreateDaemonCommandSurface } from '@agent-device/contracts/daemon-command-surface';
import type {
SettleObservation,
SettleParams,
SurfaceScopedNodes,
} from '@agent-device/contracts/interaction';
import { commandSupportsSettleObservation } from '@agent-device/command-registry/registry';
import {
captureSnapshotForSession,
createInteractionRuntime,
readSettleRequest,
settleFlagGuardResponse,
} from './interaction/index.ts';
import type { BoundContextFromFlags } from './context.ts';
import type { DaemonPlatformServices } from './platform-services.ts';
import { issueSettleRefs } from './session-snapshot.ts';
import type { SessionStore } from './session-store.ts';
import type { DaemonRequest, DaemonResponse } from './daemon-request.ts';
import type { SessionRef, SessionState } from './session-state.ts';
/**
* `--settle` on the generic daemon route (#1638): `scroll` and `back` change
* the screen without resolving an element, so they never reach the interaction
* handler that owns the touch commands' settle wiring — but scroll-then-observe
* and back-then-observe are exactly the pairs an agent wants collapsed into one
* call. This module is the generic route's half of that contract.
*
* It is reached through a LAZY SEAM (`await import`) from
* request-generic-dispatch, and hands back a CLOSURE rather than a plan object,
* so that dispatcher needs no static edge here — not even a type-only one. That
* is not incidental: observing a settled screen runs the interaction runtime, a
* subgraph the generic dispatcher otherwise never touches, and a static edge
* folds all ~18 of its files into the daemon-server type cycle (R10 caught
* exactly that). An ordinary scroll/back pays neither the import nor the
* comprehension cost.
*
* Two seams, and the order between them is load-bearing:
*
* 1. {@link planGenericSettleObservation} runs BEFORE the command dispatches and
* freezes the diff baseline. The baseline is the session's STORED pre-action
* tree, so the diff reads "settled tree vs the last tree you observed" — it
* may be several commands old, unlike press's freshly resolved pre-action
* capture. That is the honest #1101 contract for a route with no resolution
* step, and the settle loop's own captures overwrite `session.snapshot`, so
* reading it afterwards would compare the settled tree against itself.
* 2. The returned observer runs AFTER the command AND after
* `markDeferredInteractionOutcome`. Marking first is what lets settle's very
* first capture fold in the #1542 post-gesture stabilization instead of
* racing it — every capture on this path goes through the same deferred
* outcome resolution a plain snapshot would.
*/
/** Runs the settle loop and returns the observation to attach as `data.settle`. */
export type GenericSettleObserver = () => Promise<SettleObservation | undefined>;
export type GenericSettlePlan = { response: DaemonResponse } | { observe?: GenericSettleObserver };
type GenericSettleContext = {
createCommandSurface: CreateDaemonCommandSurface;
sessionRef: SessionRef | undefined;
req: DaemonRequest;
session: SessionState;
sessionName: string;
logPath: string;
sessionStore: SessionStore;
contextFromFlags: BoundContextFromFlags;
platformServices: DaemonPlatformServices;
};
/**
* Rejects an orphaned `--settle-quiet` up front (the shared flag grammar), and
* otherwise returns the observer this dispatch owes — with the pre-action
* baseline already captured.
*/
export function planGenericSettleObservation(
params: GenericSettleContext & { command: string; flags: CommandFlags | undefined },
): GenericSettlePlan {
if (!commandSupportsSettleObservation(params.command)) return {};
const invalidSettleFlags = settleFlagGuardResponse(params.command, params.flags);
if (invalidSettleFlags) return { response: invalidSettleFlags };
const settle = readSettleRequest(params.flags);
if (!settle) return {};
const snapshot = params.session.snapshot;
const baseline: SurfaceScopedNodes = {
nodes: snapshot?.nodes ?? [],
...(snapshot?.iosSystemSurfaceBundleId
? { surfaceBundleId: snapshot.iosSystemSurfaceBundleId }
: {}),
};
return { observe: async () => await observeSettled(params, settle, baseline) };
}
/**
* Best-effort like the settle engine itself: the action already succeeded, so a
* runtime that cannot even be constructed (session evicted mid-request)
* degrades to no observation rather than failing the response. `refsGeneration`
* is folded in when the settled diff actually published refs (ADR 0014 — see
* `settle-ref-issuance.ts`).
*/
async function observeSettled(
context: GenericSettleContext,
settle: SettleParams,
baseline: SurfaceScopedNodes,
): Promise<SettleObservation | undefined> {
const runtime = await createGenericSettleRuntime(context);
if (!runtime) return undefined;
// R2: the daemon reaches the settle engine through the runtime command
// surface, never by importing `commands/` — the same seam the touch handlers
// use for press/fill.
const observation = await runtime.interactions.settleObservation({
...settle,
baseline,
session: context.sessionName,
requestId: context.req.meta?.requestId,
});
if (context.sessionRef && !context.sessionStore.resolveCurrent(context.sessionRef))
return undefined;
const refsGeneration = issueSettleRefs(context.sessionRef, context.sessionStore, observation);
return refsGeneration === undefined ? observation : { ...observation, refsGeneration };
}
async function createGenericSettleRuntime(
context: GenericSettleContext,
): Promise<Awaited<ReturnType<typeof createInteractionRuntime>> | undefined> {
try {
return await createInteractionRuntime({
createCommandSurface: context.createCommandSurface,
req: context.req,
sessionName: context.sessionName,
sessionRef: context.sessionRef,
logPath: context.logPath,
sessionStore: context.sessionStore,
contextFromFlags: context.contextFromFlags,
platformServices: context.platformServices,
captureSnapshotForSession,
});
} catch {
return undefined;
}
}