rea-agents 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +29 -10
  2. package/dist/application/AndroidApplicationService.js +17 -0
  3. package/dist/application/AppleApplicationService.js +17 -0
  4. package/dist/application/ClientRegistrationStatus.js +11 -4
  5. package/dist/application/InventoryProjectionEvidence.js +41 -0
  6. package/dist/application/InvestigationProviders.js +12 -0
  7. package/dist/application/ProcessPairedExperiment.js +72 -0
  8. package/dist/application/Setup.js +3 -3
  9. package/dist/application/SetupClientConfiguration.js +10 -6
  10. package/dist/application/SetupPlan.js +3 -2
  11. package/dist/application/SetupSkill.js +44 -40
  12. package/dist/application/Uninstall.js +7 -1
  13. package/dist/application/Upgrade.js +16 -1
  14. package/dist/cli/setupCommands.js +8 -3
  15. package/dist/cliSetup.js +29 -12
  16. package/dist/contracts/electronToolContracts.js +51 -3
  17. package/dist/contracts/managedToolContracts.js +7 -2
  18. package/dist/contracts/sessionStatusContract.js +26 -0
  19. package/dist/contracts/toolContractTypes.js +12 -1
  20. package/dist/contracts/toolContracts.js +3 -10
  21. package/dist/contracts/toolOutputSchemaGroups.js +51 -9
  22. package/dist/contracts/toolOutputSchemaPrimitives.js +33 -32
  23. package/dist/domain/androidApplication.js +206 -0
  24. package/dist/domain/appleApplication.js +221 -0
  25. package/dist/domain/boundedCartesianProjection.js +15 -0
  26. package/dist/domain/processComparison.js +19 -0
  27. package/dist/domain/processPairedExperiment.js +128 -0
  28. package/dist/domain/replayMachine.js +316 -0
  29. package/dist/domain/replayMachineRuntime.js +215 -0
  30. package/dist/domain/runtimeIdentification.js +210 -0
  31. package/dist/evaluation/CodexAgentEval.js +132 -0
  32. package/dist/generatedPackageMetadata.js +2 -2
  33. package/dist/identity.js +2 -1
  34. package/dist/server/createServer.js +5 -2
  35. package/dist/server/javascriptApplicationResult.js +85 -0
  36. package/dist/server/registerElectronTools.js +19 -5
  37. package/dist/server/registerEvidenceResources.js +2 -0
  38. package/dist/server/registerJavaScriptApplicationGraphResource.js +69 -0
  39. package/dist/server/registerManagedTools.js +18 -1
  40. package/dist/server/registerSessionStatusTool.js +95 -25
  41. package/dist/server/toolResult.js +10 -4
  42. package/package.json +2 -1
  43. package/skills/reverse-engineer-anything/SKILL.md +68 -227
  44. package/skills/reverse-engineer-anything/references/controlled-replay.md +12 -0
  45. package/skills/reverse-engineer-anything/references/evidence-workflows.md +33 -0
  46. package/skills/reverse-engineer-anything/references/javascript-applications.md +25 -0
  47. package/skills/reverse-engineer-anything/references/native-and-artifacts.md +36 -0
  48. package/skills/reverse-engineer-anything/references/runtime-observation.md +24 -0
@@ -0,0 +1,316 @@
1
+ import { MultiDirectedGraph } from "graphology";
2
+ import { singleSource } from "graphology-shortest-path";
3
+ import canonicalize from "canonicalize";
4
+ import { z } from "zod";
5
+ const replayIdentifierSchema = z
6
+ .string()
7
+ .regex(/^[A-Za-z][A-Za-z0-9_-]{0,63}$/u);
8
+ const replayPathSchema = z
9
+ .array(z.union([z.string().max(256), z.number().int().nonnegative()]))
10
+ .max(32);
11
+ const sensitiveHeaderNames = new Set([
12
+ "authorization",
13
+ "cookie",
14
+ "proxy-authorization",
15
+ "set-cookie",
16
+ ]);
17
+ const replayHeaderMatchersSchema = z
18
+ .record(z.string().min(1).max(256), z.string().max(8_192))
19
+ .superRefine((headers, context) => {
20
+ const names = Object.keys(headers).map((name) => name.toLowerCase());
21
+ if (new Set(names).size !== names.length)
22
+ context.addIssue({
23
+ code: "custom",
24
+ message: "replay header matchers must be unique ignoring case",
25
+ input: headers,
26
+ });
27
+ if (names.some((name) => sensitiveHeaderNames.has(name)))
28
+ context.addIssue({
29
+ code: "custom",
30
+ message: "replay header matchers cannot persist authorization or cookie values; capture and guard through a secret alias instead",
31
+ input: headers,
32
+ });
33
+ })
34
+ .transform((headers) => Object.fromEntries(Object.entries(headers).map(([name, value]) => [
35
+ name.toLowerCase(),
36
+ value,
37
+ ])));
38
+ const replayValueSourceSchema = z.discriminatedUnion("source", [
39
+ z.strictObject({
40
+ source: z.literal("request_header"),
41
+ name: z
42
+ .string()
43
+ .min(1)
44
+ .transform((name) => name.toLowerCase()),
45
+ }),
46
+ z.strictObject({ source: z.literal("request_json"), path: replayPathSchema }),
47
+ z.strictObject({
48
+ source: z.literal("websocket_json"),
49
+ path: replayPathSchema,
50
+ }),
51
+ z.strictObject({ source: z.literal("action_json"), path: replayPathSchema }),
52
+ ]);
53
+ const replayTriggerSchema = z.discriminatedUnion("protocol", [
54
+ z.strictObject({
55
+ protocol: z.literal("http"),
56
+ method: z
57
+ .string()
58
+ .min(1)
59
+ .max(16)
60
+ .transform((method) => method.toUpperCase()),
61
+ path: z.string().startsWith("/"),
62
+ headers: replayHeaderMatchersSchema.default({}),
63
+ body: z.string().max(1_000_000).nullable().default(null),
64
+ }),
65
+ z.strictObject({
66
+ protocol: z.literal("websocket_connect"),
67
+ path: z.string().startsWith("/"),
68
+ headers: replayHeaderMatchersSchema.default({}),
69
+ }),
70
+ z.strictObject({
71
+ protocol: z.literal("websocket_message"),
72
+ path: z.string().startsWith("/"),
73
+ body: z.string().max(1_000_000).nullable().default(null),
74
+ }),
75
+ ]);
76
+ const replayActionSchema = z.discriminatedUnion("type", [
77
+ z.strictObject({
78
+ type: z.literal("http_response"),
79
+ status: z.number().int().min(100).max(599),
80
+ headers: z.record(z.string(), z.string()).default({}),
81
+ body: z.string().max(1_000_000),
82
+ }),
83
+ z.strictObject({
84
+ type: z.literal("websocket_send"),
85
+ data: z.string().max(1_000_000),
86
+ }),
87
+ z.strictObject({ type: z.literal("disconnect") }),
88
+ z.strictObject({
89
+ type: z.literal("delay"),
90
+ duration_ms: z.number().int().nonnegative().max(30_000),
91
+ }),
92
+ ]);
93
+ const replayTransitionSchema = z.strictObject({
94
+ id: replayIdentifierSchema,
95
+ from: replayIdentifierSchema,
96
+ to: replayIdentifierSchema,
97
+ priority: z.number().int().min(0).max(1_000).default(100),
98
+ trigger: replayTriggerSchema,
99
+ guards: z
100
+ .array(z.strictObject({
101
+ variable: replayIdentifierSchema,
102
+ value: replayValueSourceSchema,
103
+ }))
104
+ .max(32)
105
+ .default([]),
106
+ captures: z
107
+ .array(z.strictObject({
108
+ variable: replayIdentifierSchema,
109
+ value: replayValueSourceSchema,
110
+ sensitive: z.boolean().default(false),
111
+ }))
112
+ .max(32)
113
+ .default([]),
114
+ actions: z.array(replayActionSchema).min(1).max(32),
115
+ max_uses: z.number().int().min(1).max(10_000),
116
+ });
117
+ const transitionSignature = (transition) => {
118
+ const signature = canonicalize({
119
+ from: transition.from,
120
+ priority: transition.priority,
121
+ trigger: transition.trigger,
122
+ });
123
+ if (signature === undefined)
124
+ throw new TypeError("Replay transition signature is not canonical JSON");
125
+ return signature;
126
+ };
127
+ const replayMachineBaseSchema = z.strictObject({
128
+ initial_state: replayIdentifierSchema,
129
+ states: z
130
+ .array(z.strictObject({
131
+ name: replayIdentifierSchema,
132
+ terminal: z.boolean().default(false),
133
+ max_visits: z.number().int().min(1).max(100_000).default(100_000),
134
+ }))
135
+ .min(1)
136
+ .max(256),
137
+ transitions: z.array(replayTransitionSchema).min(1).max(2_000),
138
+ max_transitions: z.number().int().min(1).max(100_000),
139
+ limits: z
140
+ .strictObject({
141
+ connections: z.number().int().min(1).max(1_000).default(100),
142
+ messages: z.number().int().min(1).max(100_000).default(10_000),
143
+ bytes: z.number().int().min(1).max(10_000_000).default(1_000_000),
144
+ duration_ms: z.number().int().min(1).max(300_000).default(30_000),
145
+ })
146
+ .default({
147
+ connections: 100,
148
+ messages: 10_000,
149
+ bytes: 1_000_000,
150
+ duration_ms: 30_000,
151
+ }),
152
+ });
153
+ const validateReplayIdentities = (machine, context) => {
154
+ const names = machine.states.map(({ name }) => name);
155
+ const nameSet = new Set(names);
156
+ if (nameSet.size !== names.length)
157
+ context.addIssue({
158
+ code: "custom",
159
+ message: "replay machine state names must be unique",
160
+ path: ["states"],
161
+ input: machine,
162
+ });
163
+ if (!nameSet.has(machine.initial_state))
164
+ context.addIssue({
165
+ code: "custom",
166
+ message: "replay machine initial state is not declared",
167
+ path: ["initial_state"],
168
+ input: machine,
169
+ });
170
+ const transitionIds = machine.transitions.map(({ id }) => id);
171
+ if (new Set(transitionIds).size !== transitionIds.length)
172
+ context.addIssue({
173
+ code: "custom",
174
+ message: "replay machine transition IDs must be unique",
175
+ path: ["transitions"],
176
+ input: machine,
177
+ });
178
+ const signatures = machine.transitions.map(transitionSignature);
179
+ if (new Set(signatures).size !== signatures.length)
180
+ context.addIssue({
181
+ code: "custom",
182
+ message: "replay machine transitions with the same state, trigger, and priority are ambiguous",
183
+ path: ["transitions"],
184
+ input: machine,
185
+ });
186
+ return nameSet;
187
+ };
188
+ const validateReplayVariables = (machine, context) => {
189
+ const capturedVariables = new Map();
190
+ for (const [transitionIndex, transition] of machine.transitions.entries()) {
191
+ for (const capture of transition.captures) {
192
+ const sensitivity = capturedVariables.get(capture.variable);
193
+ if (sensitivity !== undefined && sensitivity !== capture.sensitive)
194
+ context.addIssue({
195
+ code: "custom",
196
+ message: "a replay variable cannot change secret classification between captures",
197
+ path: ["transitions", transitionIndex, "captures"],
198
+ input: machine,
199
+ });
200
+ capturedVariables.set(capture.variable, capture.sensitive);
201
+ }
202
+ }
203
+ const availableByState = new Map([
204
+ [machine.initial_state, new Set()],
205
+ ]);
206
+ let changed = true;
207
+ while (changed) {
208
+ changed = false;
209
+ for (const transition of machine.transitions) {
210
+ const available = availableByState.get(transition.from);
211
+ if (available === undefined ||
212
+ transition.guards.some(({ variable }) => !available.has(variable)))
213
+ continue;
214
+ const nextAvailable = new Set(available);
215
+ for (const { variable } of transition.captures)
216
+ nextAvailable.add(variable);
217
+ const existing = availableByState.get(transition.to);
218
+ if (existing === undefined) {
219
+ availableByState.set(transition.to, nextAvailable);
220
+ changed = true;
221
+ continue;
222
+ }
223
+ for (const variable of nextAvailable)
224
+ if (!existing.has(variable)) {
225
+ existing.add(variable);
226
+ changed = true;
227
+ }
228
+ }
229
+ }
230
+ for (const [transitionIndex, transition] of machine.transitions.entries())
231
+ for (const [guardIndex, guard] of transition.guards.entries())
232
+ if (!availableByState.get(transition.from)?.has(guard.variable))
233
+ context.addIssue({
234
+ code: "custom",
235
+ message: "replay guard variable is not captured on an executable path before use",
236
+ path: ["transitions", transitionIndex, "guards", guardIndex],
237
+ input: machine,
238
+ });
239
+ };
240
+ const validateReplayActions = (machine, context) => {
241
+ for (const [index, transition] of machine.transitions.entries()) {
242
+ const httpResponses = transition.actions.filter(({ type }) => type === "http_response").length;
243
+ const websocketSends = transition.actions.some(({ type }) => type === "websocket_send");
244
+ const invalid = transition.trigger.protocol === "http"
245
+ ? httpResponses !== 1 || websocketSends
246
+ : httpResponses !== 0;
247
+ if (invalid)
248
+ context.addIssue({
249
+ code: "custom",
250
+ message: "replay actions must match the transition protocol and HTTP transitions require exactly one response",
251
+ path: ["transitions", index, "actions"],
252
+ input: machine,
253
+ });
254
+ }
255
+ };
256
+ const validateReplayGraph = (machine, names, context) => {
257
+ const graph = new MultiDirectedGraph();
258
+ for (const name of names)
259
+ graph.addNode(name);
260
+ for (const [index, transition] of machine.transitions.entries()) {
261
+ if (!names.has(transition.from) || !names.has(transition.to)) {
262
+ context.addIssue({
263
+ code: "custom",
264
+ message: "replay machine transition references an unknown state",
265
+ path: ["transitions", index],
266
+ input: machine,
267
+ });
268
+ continue;
269
+ }
270
+ graph.addDirectedEdgeWithKey(`${transition.id}:${String(index)}`, transition.from, transition.to);
271
+ }
272
+ if (!graph.hasNode(machine.initial_state))
273
+ return;
274
+ if (!machine.states.some(({ terminal }) => terminal))
275
+ context.addIssue({
276
+ code: "custom",
277
+ message: "replay machine must declare at least one terminal state",
278
+ path: ["states"],
279
+ input: machine,
280
+ });
281
+ const reachable = new Set(Object.keys(singleSource(graph, machine.initial_state)));
282
+ reachable.add(machine.initial_state);
283
+ for (const [index, state] of machine.states.entries()) {
284
+ if (!reachable.has(state.name))
285
+ context.addIssue({
286
+ code: "custom",
287
+ message: "replay machine state is unreachable from the initial state",
288
+ path: ["states", index],
289
+ input: machine,
290
+ });
291
+ if (state.terminal && graph.outDegree(state.name) > 0)
292
+ context.addIssue({
293
+ code: "custom",
294
+ message: "terminal replay machine state cannot have outgoing transitions",
295
+ path: ["states", index],
296
+ input: machine,
297
+ });
298
+ if (!state.terminal && graph.outDegree(state.name) === 0)
299
+ context.addIssue({
300
+ code: "custom",
301
+ message: "non-terminal replay machine state has no outgoing transition",
302
+ path: ["states", index],
303
+ input: machine,
304
+ });
305
+ }
306
+ };
307
+ /** Portable finite replay machine with no executable callbacks. */
308
+ export const replayMachineSchema = z
309
+ .object(replayMachineBaseSchema.shape)
310
+ .strict()
311
+ .superRefine((machine, context) => {
312
+ const names = validateReplayIdentities(machine, context);
313
+ validateReplayVariables(machine, context);
314
+ validateReplayActions(machine, context);
315
+ validateReplayGraph(machine, names, context);
316
+ });
@@ -0,0 +1,215 @@
1
+ import canonicalize from "canonicalize";
2
+ const parseJson = (value) => {
3
+ try {
4
+ return JSON.parse(value);
5
+ }
6
+ catch {
7
+ return undefined;
8
+ }
9
+ };
10
+ const atPath = (value, path) => {
11
+ let current = value;
12
+ for (const segment of path) {
13
+ if (typeof segment === "number") {
14
+ if (!Array.isArray(current))
15
+ return undefined;
16
+ current = current[segment];
17
+ continue;
18
+ }
19
+ if (typeof current !== "object" ||
20
+ current === null ||
21
+ !Object.hasOwn(current, segment))
22
+ return undefined;
23
+ current = current[segment];
24
+ }
25
+ return current;
26
+ };
27
+ const actionJsonBody = (transition) => {
28
+ for (const action of transition.actions)
29
+ if (action.type === "http_response")
30
+ return action.body;
31
+ else if (action.type === "websocket_send")
32
+ return action.data;
33
+ return undefined;
34
+ };
35
+ const readValue = (source, event, transition) => {
36
+ switch (source.source) {
37
+ case "request_header":
38
+ return event.headers[source.name.toLowerCase()];
39
+ case "request_json":
40
+ return atPath(parseJson(event.body), source.path);
41
+ case "websocket_json":
42
+ return atPath(parseJson(event.body), source.path);
43
+ case "action_json": {
44
+ const body = actionJsonBody(transition);
45
+ return body === undefined
46
+ ? undefined
47
+ : atPath(parseJson(body), source.path);
48
+ }
49
+ }
50
+ };
51
+ const triggerMatches = (transition, event) => {
52
+ const trigger = transition.trigger;
53
+ if (trigger.protocol !== event.protocol || trigger.path !== event.path)
54
+ return false;
55
+ if ("headers" in trigger &&
56
+ Object.entries(trigger.headers).some(([name, value]) => event.headers[name.toLowerCase()] !== value))
57
+ return false;
58
+ if (trigger.protocol === "http")
59
+ return (trigger.method === event.method &&
60
+ (trigger.body === null || trigger.body === event.body));
61
+ if (trigger.protocol === "websocket_message")
62
+ return trigger.body === null || trigger.body === event.body;
63
+ return true;
64
+ };
65
+ const isJsonCompatible = (value) => {
66
+ if (value === null || typeof value === "string" || typeof value === "boolean")
67
+ return true;
68
+ if (typeof value === "number")
69
+ return Number.isFinite(value);
70
+ if (Array.isArray(value))
71
+ return value.every(isJsonCompatible);
72
+ if (typeof value !== "object")
73
+ return false;
74
+ return Object.values(value).every(isJsonCompatible);
75
+ };
76
+ const replayValuesEqual = (left, right) => isJsonCompatible(left) &&
77
+ isJsonCompatible(right) &&
78
+ (Object.is(left, right) || canonicalize(left) === canonicalize(right));
79
+ /** Stateful evaluator for one validated replay-machine instance. */
80
+ export class ReplayMachineRuntime {
81
+ machine;
82
+ #variables = new Map();
83
+ #sensitiveValues = new Map();
84
+ #uses = new Map();
85
+ #stateVisits = new Map();
86
+ #timeline = [];
87
+ #state;
88
+ #connections = 0;
89
+ #messages = 0;
90
+ #bytes = 0;
91
+ #lastAtMs = 0;
92
+ constructor(machine) {
93
+ this.machine = machine;
94
+ this.#state = machine.initial_state;
95
+ this.#stateVisits.set(machine.initial_state, 1);
96
+ }
97
+ /** Current machine state after all accepted transitions. */
98
+ get state() {
99
+ return this.#state;
100
+ }
101
+ /** Ordered, secret-free transition records. */
102
+ get timeline() {
103
+ return this.#timeline;
104
+ }
105
+ /** Replace captured secret values with stable aliases before persistence. */
106
+ redact(value) {
107
+ let redacted = value;
108
+ const secrets = [...this.#sensitiveValues]
109
+ .flatMap(([variable, values]) => [...values].map((secret) => ({ variable, secret })))
110
+ .sort((left, right) => right.secret.length - left.secret.length ||
111
+ left.variable.localeCompare(right.variable));
112
+ for (const { variable, secret } of secrets)
113
+ redacted = redacted.replaceAll(secret, `<secret:${variable}>`);
114
+ return redacted;
115
+ }
116
+ /** Evaluate and, when admitted, commit one protocol event. */
117
+ dispatch(event) {
118
+ const normalizedEvent = {
119
+ ...event,
120
+ headers: Object.fromEntries(Object.entries(event.headers).map(([name, value]) => [
121
+ name.toLowerCase(),
122
+ value,
123
+ ])),
124
+ };
125
+ if (!this.#admitEvent(normalizedEvent))
126
+ return this.#refusal("limit_exhausted");
127
+ const matching = this.machine.transitions
128
+ .map((transition, declarationOrder) => ({
129
+ transition,
130
+ declarationOrder,
131
+ }))
132
+ .filter(({ transition }) => triggerMatches(transition, normalizedEvent));
133
+ const eligible = matching
134
+ .filter(({ transition }) => transition.from === this.#state)
135
+ .sort((left, right) => left.transition.priority - right.transition.priority ||
136
+ left.declarationOrder - right.declarationOrder);
137
+ if (eligible.length === 0)
138
+ return this.#refusal(matching.length === 0
139
+ ? "unmatched"
140
+ : normalizedEvent.protocol === "websocket_connect" &&
141
+ normalizedEvent.connection === "reconnect"
142
+ ? "unexpected_reconnect"
143
+ : "invalid_state");
144
+ const guarded = eligible.filter(({ transition }) => transition.guards.every((guard) => this.#variables.has(guard.variable) &&
145
+ replayValuesEqual(this.#variables.get(guard.variable), readValue(guard.value, normalizedEvent, transition))));
146
+ if (guarded.length === 0)
147
+ return this.#refusal("guard_failed");
148
+ if (this.#timeline.length >= this.machine.max_transitions)
149
+ return this.#refusal("transition_exhausted");
150
+ const usable = guarded.find(({ transition }) => (this.#uses.get(transition.id) ?? 0) < transition.max_uses);
151
+ if (usable === undefined)
152
+ return this.#refusal("transition_exhausted");
153
+ const transition = usable.transition;
154
+ const used = this.#uses.get(transition.id) ?? 0;
155
+ const nextState = this.machine.states.find(({ name }) => name === transition.to);
156
+ const nextVisits = (this.#stateVisits.get(transition.to) ?? 0) + 1;
157
+ if (nextState === undefined || nextVisits > nextState.max_visits)
158
+ return this.#refusal("limit_exhausted");
159
+ const captures = transition.captures.map((capture) => ({
160
+ ...capture,
161
+ captured: readValue(capture.value, normalizedEvent, transition),
162
+ }));
163
+ if (captures.some(({ captured, sensitive }) => !isJsonCompatible(captured) ||
164
+ (sensitive && typeof captured !== "string")))
165
+ return this.#refusal("invalid_capture");
166
+ for (const capture of captures) {
167
+ this.#variables.set(capture.variable, capture.captured);
168
+ if (capture.sensitive && typeof capture.captured === "string") {
169
+ const values = this.#sensitiveValues.get(capture.variable) ?? new Set();
170
+ if (capture.captured.length > 0)
171
+ values.add(capture.captured);
172
+ this.#sensitiveValues.set(capture.variable, values);
173
+ }
174
+ }
175
+ const record = {
176
+ sequence: this.#timeline.length,
177
+ transition_id: transition.id,
178
+ state_before: this.#state,
179
+ state_after: transition.to,
180
+ sensitive_aliases: captures
181
+ .filter(({ sensitive }) => sensitive)
182
+ .map(({ variable }) => variable)
183
+ .sort(),
184
+ };
185
+ this.#uses.set(transition.id, used + 1);
186
+ this.#stateVisits.set(transition.to, nextVisits);
187
+ this.#state = transition.to;
188
+ this.#timeline.push(record);
189
+ return {
190
+ outcome: "matched",
191
+ actions: transition.actions,
192
+ transition: record,
193
+ };
194
+ }
195
+ #admitEvent(event) {
196
+ const connections = this.#connections + (event.protocol === "websocket_connect" ? 1 : 0);
197
+ const messages = this.#messages + (event.protocol === "websocket_message" ? 1 : 0);
198
+ const bytes = this.#bytes + Buffer.byteLength(event.body);
199
+ if (!Number.isSafeInteger(event.at_ms) ||
200
+ event.at_ms < this.#lastAtMs ||
201
+ event.at_ms > this.machine.limits.duration_ms ||
202
+ connections > this.machine.limits.connections ||
203
+ messages > this.machine.limits.messages ||
204
+ bytes > this.machine.limits.bytes)
205
+ return false;
206
+ this.#connections = connections;
207
+ this.#messages = messages;
208
+ this.#bytes = bytes;
209
+ this.#lastAtMs = event.at_ms;
210
+ return true;
211
+ }
212
+ #refusal(outcome) {
213
+ return { outcome, actions: [], transition: null };
214
+ }
215
+ }