e2e 0.16.0 → 0.18.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 (256) hide show
  1. package/README.md +10 -0
  2. package/dist/.build.json +1 -1
  3. package/dist/agent/act.js +7 -4
  4. package/dist/agent/action-dispatcher.js +4 -5
  5. package/dist/agent/actions.d.ts +32 -23
  6. package/dist/agent/actions.js +38 -24
  7. package/dist/agent/ai-sdk.js +5 -4
  8. package/dist/agent/error.js +3 -3
  9. package/dist/agent/invocation.js +3 -4
  10. package/dist/agent/model/sdk.js +2 -0
  11. package/dist/agent/model/tool-choice.d.ts +9 -0
  12. package/dist/agent/model/tool-choice.js +14 -0
  13. package/dist/agent/observation-feed.d.ts +5 -6
  14. package/dist/agent/observation-feed.js +8 -11
  15. package/dist/agent/observation.d.ts +40 -11
  16. package/dist/agent/observation.js +109 -59
  17. package/dist/agent/phases.d.ts +7 -1
  18. package/dist/agent/phases.js +12 -3
  19. package/dist/agent/point-tap.d.ts +8 -8
  20. package/dist/agent/point-tap.js +1 -1
  21. package/dist/agent/prompts.js +3 -0
  22. package/dist/agent/replay.d.ts +19 -8
  23. package/dist/agent/replay.js +29 -29
  24. package/dist/agent/scroll-target.d.ts +2 -3
  25. package/dist/agent/step-cache.d.ts +43 -15
  26. package/dist/agent/step-cache.js +156 -67
  27. package/dist/agent/tool-loop.js +94 -5
  28. package/dist/cache/anchors.d.ts +72 -23
  29. package/dist/cache/anchors.js +233 -60
  30. package/dist/cache/context.d.ts +25 -14
  31. package/dist/cache/context.js +50 -8
  32. package/dist/cache/decide.d.ts +2 -2
  33. package/dist/cache/decide.js +4 -6
  34. package/dist/cache/identity.d.ts +18 -4
  35. package/dist/cache/identity.js +9 -4
  36. package/dist/cache/recorder.d.ts +15 -1
  37. package/dist/cache/recorder.js +36 -23
  38. package/dist/cache/rekeyed.d.ts +46 -0
  39. package/dist/cache/rekeyed.js +89 -0
  40. package/dist/cache/relocate.d.ts +14 -14
  41. package/dist/cache/relocate.js +33 -25
  42. package/dist/cache/route.d.ts +32 -33
  43. package/dist/cache/route.js +94 -50
  44. package/dist/cache/store.d.ts +6 -0
  45. package/dist/cache/store.js +21 -1
  46. package/dist/cache/trace.d.ts +53 -14
  47. package/dist/cache/trace.js +76 -30
  48. package/dist/cli/bin.js +2 -2
  49. package/dist/cli/index.js +43 -32
  50. package/dist/cli/init/engines.d.ts +1 -3
  51. package/dist/cli/init/engines.js +2 -2
  52. package/dist/cli/init/gateways.d.ts +1 -1
  53. package/dist/cli/init/gateways.js +11 -1
  54. package/dist/cli/init/mcp-config.d.ts +2 -1
  55. package/dist/cli/init/mcp-config.js +8 -3
  56. package/dist/cli/init/sibling-versions.json +1 -1
  57. package/dist/cli/init/versions.d.ts +0 -8
  58. package/dist/cli/init/versions.js +1 -10
  59. package/dist/cli/mcp.d.ts +3 -2
  60. package/dist/cli/mcp.js +52 -16
  61. package/dist/cli/run-output.d.ts +17 -0
  62. package/dist/cli/run-output.js +45 -0
  63. package/dist/collect/collect.js +8 -4
  64. package/dist/collect/registry.js +27 -16
  65. package/dist/config/compiled-files.d.ts +36 -0
  66. package/dist/config/compiled-files.js +50 -0
  67. package/dist/config/diagnose.js +21 -19
  68. package/dist/config/esm-hooks.d.ts +39 -44
  69. package/dist/config/esm-hooks.js +238 -63
  70. package/dist/config/load.d.ts +1 -7
  71. package/dist/config/load.js +23 -66
  72. package/dist/config/register.d.ts +8 -0
  73. package/dist/config/register.js +9 -0
  74. package/dist/config/resolve.d.ts +1 -0
  75. package/dist/config/resolve.js +33 -1
  76. package/dist/config/specifiers.d.ts +19 -0
  77. package/dist/config/specifiers.js +33 -0
  78. package/dist/config/tsconfig.d.ts +56 -0
  79. package/dist/config/tsconfig.js +115 -0
  80. package/dist/config/typescript.d.ts +19 -0
  81. package/dist/config/typescript.js +253 -0
  82. package/dist/engine/contract.d.ts +1 -0
  83. package/dist/engine/index.d.ts +4 -2
  84. package/dist/engine/session.js +2 -2
  85. package/dist/expect/async.js +4 -2
  86. package/dist/expect/poll-scope.d.ts +52 -0
  87. package/dist/expect/poll-scope.js +113 -0
  88. package/dist/expect/poll.js +33 -6
  89. package/dist/expect/values.js +11 -4
  90. package/dist/internal/abandoned.d.ts +11 -0
  91. package/dist/internal/abandoned.js +23 -0
  92. package/dist/internal/brands.d.ts +2 -0
  93. package/dist/internal/brands.js +2 -0
  94. package/dist/internal/client-identity.d.ts +9 -0
  95. package/dist/internal/client-identity.js +14 -0
  96. package/dist/internal/errors.d.ts +10 -3
  97. package/dist/internal/errors.js +23 -8
  98. package/dist/internal/node-version.d.ts +16 -7
  99. package/dist/internal/node-version.js +50 -15
  100. package/dist/internal/package-manager.js +1 -1
  101. package/dist/internal/realm-slot.d.ts +5 -4
  102. package/dist/internal/redact.d.ts +9 -4
  103. package/dist/internal/redact.js +120 -23
  104. package/dist/internal/time.d.ts +26 -4
  105. package/dist/internal/time.js +58 -14
  106. package/dist/internal/urls.d.ts +9 -5
  107. package/dist/internal/urls.js +19 -10
  108. package/dist/locator/engine.d.ts +12 -2
  109. package/dist/locator/engine.js +55 -14
  110. package/dist/locator/screen.js +59 -9
  111. package/dist/mcp/catalog.d.ts +3 -2
  112. package/dist/mcp/catalog.js +6 -6
  113. package/dist/mcp/output.d.ts +59 -0
  114. package/dist/mcp/output.js +129 -0
  115. package/dist/mcp/server.d.ts +4 -0
  116. package/dist/mcp/server.js +9 -12
  117. package/dist/mcp/session.d.ts +8 -1
  118. package/dist/mcp/session.js +56 -15
  119. package/dist/mcp/tools.js +7 -5
  120. package/dist/oauth/chatgpt.js +3 -23
  121. package/dist/oauth/cli.js +1 -0
  122. package/dist/oauth/copilot.d.ts +5 -4
  123. package/dist/oauth/copilot.js +80 -7
  124. package/dist/oauth/grok.js +2 -1
  125. package/dist/oauth/models.js +2 -1
  126. package/dist/oauth/opencode-console.d.ts +10 -0
  127. package/dist/oauth/opencode-console.js +133 -0
  128. package/dist/oauth/providers/github-copilot.d.ts +12 -0
  129. package/dist/oauth/providers/github-copilot.js +77 -23
  130. package/dist/oauth/providers/opencode-console.d.ts +31 -0
  131. package/dist/oauth/providers/opencode-console.js +148 -0
  132. package/dist/oauth/providers.d.ts +2 -6
  133. package/dist/oauth/providers.js +3 -7
  134. package/dist/oauth/responses.d.ts +9 -0
  135. package/dist/oauth/responses.js +23 -0
  136. package/dist/report/build.d.ts +26 -0
  137. package/dist/report/build.js +16 -8
  138. package/dist/report/failure-text.d.ts +6 -1
  139. package/dist/report/failure-text.js +14 -4
  140. package/dist/report/format.d.ts +10 -5
  141. package/dist/report/format.js +12 -5
  142. package/dist/report/junit.js +11 -12
  143. package/dist/report/list.d.ts +12 -0
  144. package/dist/report/list.js +61 -24
  145. package/dist/report/markdown.d.ts +1 -1
  146. package/dist/report/markdown.js +37 -14
  147. package/dist/report/repeats.d.ts +6 -1
  148. package/dist/report/repeats.js +12 -3
  149. package/dist/run/artifacts.d.ts +22 -4
  150. package/dist/run/artifacts.js +71 -3
  151. package/dist/run/execute.d.ts +34 -5
  152. package/dist/run/execute.js +93 -44
  153. package/dist/run/extended-fixtures.js +6 -2
  154. package/dist/run/failure-evidence.js +10 -4
  155. package/dist/run/fixtures.js +6 -8
  156. package/dist/run/in-process.d.ts +2 -0
  157. package/dist/run/in-process.js +52 -10
  158. package/dist/run/last-run.d.ts +42 -8
  159. package/dist/run/last-run.js +121 -14
  160. package/dist/run/process-output.d.ts +73 -0
  161. package/dist/run/process-output.js +130 -0
  162. package/dist/run/provision.d.ts +2 -0
  163. package/dist/run/provision.js +1 -0
  164. package/dist/run/realm.d.ts +4 -1
  165. package/dist/run/realm.js +20 -6
  166. package/dist/run/records.d.ts +20 -4
  167. package/dist/run/records.js +26 -0
  168. package/dist/run/retry.d.ts +10 -1
  169. package/dist/run/retry.js +30 -12
  170. package/dist/run/runner.d.ts +9 -1
  171. package/dist/run/runner.js +101 -39
  172. package/dist/run/scheduler.js +255 -19
  173. package/dist/run/secrecy.d.ts +22 -19
  174. package/dist/run/secrecy.js +22 -18
  175. package/dist/run/serial.d.ts +75 -2
  176. package/dist/run/serial.js +243 -59
  177. package/dist/run/standalone.js +1 -1
  178. package/dist/run/steps.d.ts +0 -6
  179. package/dist/run/steps.js +3 -20
  180. package/dist/run/trace-redaction.d.ts +5 -6
  181. package/dist/run/trace-redaction.js +5 -6
  182. package/dist/run/worker/entry.js +40 -18
  183. package/dist/run/worker/handle.js +10 -5
  184. package/dist/run/worker/protocol.d.ts +85 -3
  185. package/dist/run/worker/session.d.ts +2 -0
  186. package/dist/run/worker/session.js +29 -0
  187. package/dist/telemetry/environment.d.ts +2 -1
  188. package/dist/telemetry/environment.js +12 -2
  189. package/dist/telemetry/events.js +1 -0
  190. package/dist/telemetry/project.d.ts +5 -6
  191. package/dist/telemetry/project.js +6 -11
  192. package/dist/telemetry/store.d.ts +5 -5
  193. package/dist/telemetry/store.js +5 -7
  194. package/dist/telemetry/telemetry.d.ts +2 -3
  195. package/dist/telemetry/telemetry.js +2 -4
  196. package/dist/types.d.ts +15 -6
  197. package/docs/agent-steps.mdx +1 -0
  198. package/docs/agents.mdx +7 -3
  199. package/docs/api-testing.mdx +2 -1
  200. package/docs/authentication.mdx +6 -3
  201. package/docs/browser.mdx +49 -12
  202. package/docs/bug-bash.mdx +2 -1
  203. package/docs/cache.mdx +66 -19
  204. package/docs/ci.mdx +14 -9
  205. package/docs/coding-agents.mdx +2 -1
  206. package/docs/core-concepts.mdx +208 -0
  207. package/docs/debugging.mdx +25 -12
  208. package/docs/decision-models.mdx +142 -0
  209. package/docs/example-projects.mdx +32 -0
  210. package/docs/executors.mdx +7 -2
  211. package/docs/explore.mdx +1 -0
  212. package/docs/github.mdx +14 -5
  213. package/docs/index.mdx +9 -5
  214. package/docs/integrations/eas.mdx +1 -0
  215. package/docs/integrations/index.mdx +1 -0
  216. package/docs/integrations/kernel.mdx +3 -2
  217. package/docs/migrate/cypress.mdx +26 -3
  218. package/docs/migrate/detox.mdx +24 -1
  219. package/docs/migrate/maestro.mdx +28 -4
  220. package/docs/migrate/playwright.mdx +44 -18
  221. package/docs/migrate/selenium.mdx +27 -3
  222. package/docs/mobile-ci.mdx +6 -5
  223. package/docs/mobile.mdx +96 -8
  224. package/docs/models.mdx +60 -9
  225. package/docs/quickstart.mdx +28 -16
  226. package/docs/reference/agent.mdx +13 -3
  227. package/docs/reference/app.mdx +2 -1
  228. package/docs/reference/cli.mdx +41 -18
  229. package/docs/reference/config.mdx +67 -13
  230. package/docs/reference/engine.mdx +25 -13
  231. package/docs/reference/environment.mdx +10 -2
  232. package/docs/reference/errors.mdx +13 -10
  233. package/docs/reference/expect.mdx +36 -13
  234. package/docs/reference/mcp.mdx +21 -8
  235. package/docs/reference/mobile.mdx +24 -13
  236. package/docs/reference/reporters.mdx +9 -2
  237. package/docs/reference/screen.mdx +49 -16
  238. package/docs/reference/test.mdx +32 -5
  239. package/docs/reference/web.mdx +133 -23
  240. package/docs/security.mdx +58 -42
  241. package/docs/subscriptions.mdx +92 -12
  242. package/docs/telemetry.mdx +9 -6
  243. package/docs/tools.mdx +1 -0
  244. package/docs/{starting-your-app.mdx → web.mdx} +157 -11
  245. package/docs/writing-an-engine.mdx +2 -1
  246. package/package.json +25 -8
  247. package/schema/report-v1.schema.json +14 -1
  248. package/skills/e2e/SKILL.md +5 -3
  249. package/skills/e2e/references/agent.md +33 -11
  250. package/skills/e2e/references/debugging.md +4 -4
  251. package/skills/e2e/references/mcp.md +8 -3
  252. package/skills/e2e/references/running.md +15 -9
  253. package/skills/e2e/references/setup.md +45 -16
  254. package/skills/e2e/references/writing-tests.md +35 -18
  255. package/docs/viewports.mdx +0 -146
  256. package/docs/writing-tests.mdx +0 -256
@@ -38,6 +38,18 @@ function flowOf(trace) {
38
38
  }),
39
39
  });
40
40
  }
41
+ /**
42
+ * Writes the step's full provenance into a kept entry recorded before the
43
+ * occurrence fields were, leaving every other entry untouched. Its replay
44
+ * proved which step it belongs to, and `cache.strict` matches only entries
45
+ * that say so exactly (`rekeyed.ts`).
46
+ */
47
+ async function completeProvenance(store, keyHash, recordedFor) {
48
+ const existing = await store.read(keyHash);
49
+ if (existing.status !== 'hit' || existing.entry.payload.recordedFor?.callIndex !== undefined)
50
+ return;
51
+ await store.write(keyHash, { ...existing.entry.payload, recordedFor });
52
+ }
41
53
  /**
42
54
  * Settles the attempt's staged trace writes. A staged trace is confirmed only
43
55
  * when a verification step — a deterministic assertion or an agent judgment
@@ -54,7 +66,7 @@ function flowOf(trace) {
54
66
  * for an interrupted attempt: interruption implicates nothing, so it writes
55
67
  * nothing and evicts nothing. An entry a step replayed whole is staged too,
56
68
  * so the same rule evicts it when nothing confirmed it; when something did,
57
- * it is left exactly as it was found.
69
+ * it is left exactly as it was found, but for provenance it lacked.
58
70
  */
59
71
  export async function flushStagedTraces(context, settlement) {
60
72
  const staged = context.staged.splice(0);
@@ -68,7 +80,11 @@ export async function flushStagedTraces(context, settlement) {
68
80
  await context.store.delete?.(entry.keyHash);
69
81
  continue;
70
82
  }
71
- if (entry.kind === 'keep' || (await holdsSameFlow(context.store, entry.keyHash, entry.trace)))
83
+ if (entry.kind === 'keep') {
84
+ await completeProvenance(context.store, entry.keyHash, entry.recordedFor);
85
+ continue;
86
+ }
87
+ if (await holdsSameFlow(context.store, entry.keyHash, entry.trace))
72
88
  continue;
73
89
  await context.store.write(entry.keyHash, entry.trace);
74
90
  }
@@ -95,23 +111,49 @@ export function createAgentCacheContext(options) {
95
111
  writable: mode === 'read-write',
96
112
  });
97
113
  const project = projectIdentity(options.projectId);
98
- const nextCallIndex = createCallIndexer();
114
+ // One occurrence count per agent: the agent is part of the key, so another
115
+ // agent's call of the same instruction must not renumber this one's.
116
+ const indexers = new Map();
117
+ const nextCallIndex = (agent, signature) => {
118
+ // Folded like the key folds it (`buildTraceCacheKey`): no context is the empty one.
119
+ const agentKey = canonicalJson([agent.name, agent.context ?? '']);
120
+ const indexer = indexers.get(agentKey) ?? createCallIndexer();
121
+ indexers.set(agentKey, indexer);
122
+ return indexer(signature);
123
+ };
99
124
  return {
100
125
  mode,
101
126
  store,
102
- identity: { testId: options.testId, targetId: options.target.targetId },
103
127
  replayEligible: options.attemptIndex === 0,
104
- strict: options.cache.strict === false ? false : { advice: staleAdvice(options.cache, options.cache.strict, options.projectRoot) },
105
- claimKeyHash: (kind, instruction, params) => {
128
+ strict: options.cache.strict === false
129
+ ? false
130
+ : {
131
+ advice: staleAdvice(options.cache, options.cache.strict, options.projectRoot),
132
+ ...(options.recordings === undefined ? {} : { recordings: options.recordings }),
133
+ },
134
+ claimKey: (kind, instruction, params, agent) => {
106
135
  const signature = traceCallSignature(kind, instruction, params);
107
- return traceCacheKeyHash(buildTraceCacheKey({
136
+ const callIndex = nextCallIndex(agent, signature);
137
+ const keyHash = traceCacheKeyHash(buildTraceCacheKey({
108
138
  project,
109
139
  testId: options.testId,
110
140
  target: options.target,
111
141
  signature,
112
- callIndex: nextCallIndex(signature),
142
+ callIndex,
143
+ agent,
113
144
  policyVersion: REPLAY_POLICY_VERSION,
114
145
  }));
146
+ return {
147
+ keyHash,
148
+ step: {
149
+ testId: options.testId,
150
+ targetId: options.target.targetId,
151
+ instructionDigest: signature.instructionDigest,
152
+ paramsDigest: signature.paramsDigest,
153
+ callIndex,
154
+ agent: agent.name,
155
+ },
156
+ };
115
157
  },
116
158
  staged: [],
117
159
  };
@@ -31,7 +31,7 @@ export type TraceReplayDecision = {
31
31
  * not open with a navigate carries a start precondition: the app must be on
32
32
  * the screen where the recording began, or the recorded actions would run
33
33
  * against a different screen than they were proven on. The screen is its
34
- * route (`routeOf`), not its URL; a route the path leaves undecided is a
35
- * miss, since nothing recorded at the start is specific enough to settle it.
34
+ * route (`cache/route.ts`), not its URL: the same origin, path, and query,
35
+ * whatever the ids in them.
36
36
  */
37
37
  export declare function decideTraceReplay(entry: TraceEntry, currentPath: string | undefined): TraceReplayDecision;
@@ -7,7 +7,7 @@
7
7
  * decision takes an already-trusted entry and answers only the questions the
8
8
  * entry alone cannot: is it whole, and is the app where the recording began?
9
9
  */
10
- import { compareRoutes, routeOf } from './route.js';
10
+ import { sameRoute } from './route.js';
11
11
  /**
12
12
  * Whether a trace establishes its own starting point by navigating first.
13
13
  * Such a trace needs no start-path precondition — and a trace with neither
@@ -21,8 +21,8 @@ export function opensWithNavigate(trace) {
21
21
  * not open with a navigate carries a start precondition: the app must be on
22
22
  * the screen where the recording began, or the recorded actions would run
23
23
  * against a different screen than they were proven on. The screen is its
24
- * route (`routeOf`), not its URL; a route the path leaves undecided is a
25
- * miss, since nothing recorded at the start is specific enough to settle it.
24
+ * route (`cache/route.ts`), not its URL: the same origin, path, and query,
25
+ * whatever the ids in them.
26
26
  */
27
27
  export function decideTraceReplay(entry, currentPath) {
28
28
  const trace = entry.payload;
@@ -32,7 +32,5 @@ export function decideTraceReplay(entry, currentPath) {
32
32
  return { action: 'replay' };
33
33
  if (trace.startPath === undefined || currentPath === undefined)
34
34
  return { action: 'miss', reason: 'wrong-context' };
35
- return compareRoutes(routeOf(trace.startPath), routeOf(currentPath)) === 'same'
36
- ? { action: 'replay' }
37
- : { action: 'miss', reason: 'wrong-context' };
35
+ return sameRoute(trace.startPath, currentPath) ? { action: 'replay' } : { action: 'miss', reason: 'wrong-context' };
38
36
  }
@@ -7,10 +7,13 @@
7
7
  * contributes only its stable name and purpose, exactly as the executor sees
8
8
  * it in the projected params.
9
9
  *
10
- * Executor identity is deliberately absent (OQ10's conservative default):
11
- * entries are shared across roster members and carry their producer as
12
- * provenance on the entry, so a future ReplayPolicy can reject foreign traces
13
- * without a re-key.
10
+ * The agent a step runs with is part of the key: its configured name, since
11
+ * two agents of one test (`agent: ['buyer', 'admin']`) act as different
12
+ * people, and a digest of the context it reads, since a test's
13
+ * `agentContext` decides what the step does. The context is digested after
14
+ * redaction, so a registered secret in it contributes its name only. The
15
+ * executor that produced a trace stays provenance on the entry: the agent's
16
+ * name already keeps two configured agents apart.
14
17
  */
15
18
  import type { JsonValue } from '../types.ts';
16
19
  import { TRACE_SCHEMA_VERSION } from './trace.ts';
@@ -37,8 +40,18 @@ export interface TraceCacheKey {
37
40
  readonly instructionDigest: string;
38
41
  readonly paramsDigest: string;
39
42
  readonly appIdentity: string;
43
+ /** Name of the configured agent the step ran with. */
44
+ readonly agent: string;
45
+ /** SHA-256 of the agent's redacted context (its `context` then the test's `agentContext`), or of the empty string. */
46
+ readonly agentContextDigest: string;
40
47
  readonly policyVersion: string;
41
48
  }
49
+ /** The agent a step runs with, as the key sees it. */
50
+ export interface CacheAgentIdentity {
51
+ readonly name: string;
52
+ /** The context the agent reads, already redacted. */
53
+ readonly context: string | undefined;
54
+ }
42
55
  /** Identity of the target/engine/app a trace was recorded against. */
43
56
  export interface CacheTargetIdentity {
44
57
  readonly targetId: string;
@@ -106,6 +119,7 @@ export declare function buildTraceCacheKey(parts: {
106
119
  readonly target: CacheTargetIdentity;
107
120
  readonly signature: TraceCallSignature;
108
121
  readonly callIndex: number;
122
+ readonly agent: CacheAgentIdentity;
109
123
  readonly policyVersion: string;
110
124
  }): TraceCacheKey;
111
125
  /** SHA-256/JCS of one key, used as both the entry digest and its file name. */
@@ -7,10 +7,13 @@
7
7
  * contributes only its stable name and purpose, exactly as the executor sees
8
8
  * it in the projected params.
9
9
  *
10
- * Executor identity is deliberately absent (OQ10's conservative default):
11
- * entries are shared across roster members and carry their producer as
12
- * provenance on the entry, so a future ReplayPolicy can reject foreign traces
13
- * without a re-key.
10
+ * The agent a step runs with is part of the key: its configured name, since
11
+ * two agents of one test (`agent: ['buyer', 'admin']`) act as different
12
+ * people, and a digest of the context it reads, since a test's
13
+ * `agentContext` decides what the step does. The context is digested after
14
+ * redaction, so a registered secret in it contributes its name only. The
15
+ * executor that produced a trace stays provenance on the entry: the agent's
16
+ * name already keeps two configured agents apart.
14
17
  */
15
18
  import { canonicalDigest, sha256Hex } from '../internal/ids.js';
16
19
  import { TRACE_SCHEMA_VERSION } from './trace.js';
@@ -99,6 +102,8 @@ export function buildTraceCacheKey(parts) {
99
102
  instructionDigest: parts.signature.instructionDigest,
100
103
  paramsDigest: parts.signature.paramsDigest,
101
104
  appIdentity: parts.target.appIdentity,
105
+ agent: parts.agent.name,
106
+ agentContextDigest: sha256Hex(parts.agent.context ?? ''),
102
107
  policyVersion: parts.policyVersion,
103
108
  };
104
109
  }
@@ -20,12 +20,15 @@ import { type ActionTrace, type DerivedReason, type TraceProvenance, type TraceT
20
20
  export interface TraceRecorderOptions {
21
21
  /** The run's secret redactor; applied to every recorded string. */
22
22
  readonly redact: (text: string) => string;
23
+ /** `redact` for a field cut at its observed limit (`SecretLedger.redactCut`). */
24
+ readonly redactCut: (text: string) => string;
23
25
  readonly maxActions?: number;
24
26
  }
25
27
  export declare class TraceRecorder {
26
28
  private readonly actions;
27
29
  private truncated;
28
30
  private readonly redact;
31
+ private readonly redactCut;
29
32
  private readonly maxActions;
30
33
  constructor(options: TraceRecorderOptions);
31
34
  private lastActionAt;
@@ -61,8 +64,10 @@ export declare class TraceRecorder {
61
64
  readonly summary: string;
62
65
  readonly startPath?: string;
63
66
  readonly endPath?: string;
64
- /** Already projected and capped by `describeAnchors`; recorded as given. */
67
+ /** Already projected and capped by `describeDelta`; recorded as given. */
65
68
  readonly endAnchors?: readonly TraceTargetDescriptor[];
69
+ /** The other side of the same delta: what was on screen at the start and gone at the end. */
70
+ readonly goneAnchors?: readonly TraceTargetDescriptor[];
66
71
  /** How long the recorded run took to reach its end state, plus margin. */
67
72
  readonly endWaitMs?: number;
68
73
  }): ActionTrace | undefined;
@@ -84,3 +89,12 @@ export declare class TraceRecorder {
84
89
  private verbatim;
85
90
  private push;
86
91
  }
92
+ /**
93
+ * Provenance as an entry stores it. Harness identity rather than screen
94
+ * content, so the redactor here is belt and braces: a title or agent name
95
+ * that happened to carry a registered secret is masked like any other
96
+ * recorded string, and provenance is not replay input, so masking it poisons
97
+ * nothing. A step compares its own provenance in this form to the entries
98
+ * the store holds.
99
+ */
100
+ export declare function recordedProvenance(provenance: TraceProvenance, redact: (text: string) => string): TraceProvenance;
@@ -24,9 +24,11 @@ export class TraceRecorder {
24
24
  actions = [];
25
25
  truncated = false;
26
26
  redact;
27
+ redactCut;
27
28
  maxActions;
28
29
  constructor(options) {
29
30
  this.redact = options.redact;
31
+ this.redactCut = options.redactCut;
30
32
  this.maxActions = Math.min(options.maxActions ?? MAX_TRACE_ACTIONS, MAX_TRACE_ACTIONS);
31
33
  }
32
34
  lastActionAt;
@@ -40,7 +42,7 @@ export class TraceRecorder {
40
42
  }
41
43
  /** Records one committed grammar action. */
42
44
  record(action) {
43
- this.push(this.toRecorded(action, describeAction(action, this.redact)));
45
+ this.push(this.toRecorded(action, describeAction(action, { redact: this.redact, redactCut: this.redactCut })));
44
46
  this.lastActionAt = Date.now();
45
47
  }
46
48
  /**
@@ -69,9 +71,10 @@ export class TraceRecorder {
69
71
  if (this.actions.length === 0)
70
72
  return undefined;
71
73
  const summary = bound(this.redact(conclusion.summary), MAX_TRACE_SUMMARY_CHARS);
72
- // Anchors are replay preconditions, so they follow the input rule: a path
73
- // the redactor alters carried a secret (a token in a query string) and
74
- // the trace is marked non-replayable rather than storing it verbatim.
74
+ // Locations are replay preconditions, so they follow the input rule: a
75
+ // path the redactor alters carried a secret (a token in a query string),
76
+ // and one too long to keep whole cannot be compared, so either marks the
77
+ // trace non-replayable rather than storing it bent.
75
78
  const startPath = this.anchorPath(conclusion.startPath);
76
79
  const endPath = this.anchorPath(conclusion.endPath);
77
80
  return {
@@ -80,21 +83,16 @@ export class TraceRecorder {
80
83
  name: conclusion.executor.name,
81
84
  ...(conclusion.executor.version === undefined ? {} : { version: conclusion.executor.version }),
82
85
  },
83
- recordedFor: {
84
- // Harness identity rather than screen content, so the redactor here is
85
- // belt and braces: a title that happened to carry a registered secret
86
- // is masked like any other recorded string, and provenance is not
87
- // replay input, so masking it poisons nothing.
88
- testId: bound(this.redact(conclusion.recordedFor.testId), MAX_TRACE_DESCRIPTOR_CHARS),
89
- targetId: bound(this.redact(conclusion.recordedFor.targetId), MAX_TRACE_DESCRIPTOR_CHARS),
90
- instructionDigest: conclusion.recordedFor.instructionDigest,
91
- },
86
+ recordedFor: recordedProvenance(conclusion.recordedFor, this.redact),
92
87
  summary: summary.trim() === '' ? 'step passed' : summary,
93
88
  ...(startPath === undefined ? {} : { startPath }),
94
89
  ...(endPath === undefined ? {} : { endPath }),
95
90
  ...(conclusion.endAnchors === undefined || conclusion.endAnchors.length === 0
96
91
  ? {}
97
92
  : { endAnchors: conclusion.endAnchors }),
93
+ ...(conclusion.goneAnchors === undefined || conclusion.goneAnchors.length === 0
94
+ ? {}
95
+ : { goneAnchors: conclusion.goneAnchors }),
98
96
  ...(conclusion.endWaitMs === undefined
99
97
  ? {}
100
98
  : { endWaitMs: Math.min(MAX_TRACE_END_WAIT_MS, Math.max(0, Math.round(conclusion.endWaitMs))) }),
@@ -102,12 +100,7 @@ export class TraceRecorder {
102
100
  };
103
101
  }
104
102
  anchorPath(value) {
105
- if (value === undefined || value === '')
106
- return undefined;
107
- const redacted = this.redact(value);
108
- if (redacted !== value)
109
- this.truncated = true;
110
- return bound(redacted, MAX_TRACE_DESCRIPTOR_CHARS);
103
+ return value === undefined || value === '' ? undefined : this.verbatim(value);
111
104
  }
112
105
  /**
113
106
  * Builds the stored variant for one action. The descriptor and the prose
@@ -117,13 +110,15 @@ export class TraceRecorder {
117
110
  * kept whole.
118
111
  */
119
112
  toRecorded(action, { target, summary, destination }) {
120
- // A targeted commit whose node yields no durable descriptor cannot be
121
- // re-found; the trace stays honest by poisoning instead of guessing.
113
+ // A targeted commit whose node yields no descriptor a replay could
114
+ // re-find it by (none at all, or an unnamed twin with no named row to
115
+ // tell it apart) cannot replay; the trace stays honest by poisoning
116
+ // instead of guessing.
122
117
  const require = (descriptor) => {
123
- if (descriptor !== undefined)
118
+ if (descriptor !== undefined && isRelocatableDescriptor(descriptor))
124
119
  return descriptor;
125
120
  this.truncated = true;
126
- return { role: 'unknown' };
121
+ return descriptor ?? { role: 'unknown' };
127
122
  };
128
123
  const requireTarget = () => require(target);
129
124
  if (isNodeAction(action))
@@ -240,3 +235,21 @@ function sameTarget(a, b) {
240
235
  a.position?.index === b.position?.index &&
241
236
  a.position?.of === b.position?.of);
242
237
  }
238
+ /**
239
+ * Provenance as an entry stores it. Harness identity rather than screen
240
+ * content, so the redactor here is belt and braces: a title or agent name
241
+ * that happened to carry a registered secret is masked like any other
242
+ * recorded string, and provenance is not replay input, so masking it poisons
243
+ * nothing. A step compares its own provenance in this form to the entries
244
+ * the store holds.
245
+ */
246
+ export function recordedProvenance(provenance, redact) {
247
+ return {
248
+ testId: bound(redact(provenance.testId), MAX_TRACE_DESCRIPTOR_CHARS),
249
+ targetId: bound(redact(provenance.targetId), MAX_TRACE_DESCRIPTOR_CHARS),
250
+ instructionDigest: provenance.instructionDigest,
251
+ ...(provenance.paramsDigest === undefined ? {} : { paramsDigest: provenance.paramsDigest }),
252
+ ...(provenance.callIndex === undefined ? {} : { callIndex: provenance.callIndex }),
253
+ ...(provenance.agent === undefined ? {} : { agent: bound(redact(provenance.agent), MAX_TRACE_DESCRIPTOR_CHARS) }),
254
+ };
255
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Recordings stored under a key the runner no longer derives.
3
+ *
4
+ * A key names everything that can change replay (`identity.ts`), so a new
5
+ * replay policy, an engine minor, or an agent's context changing turns every
6
+ * entry recorded before into a lookup miss, the same `no-entry` as a step
7
+ * that was never recorded. `cache.strict` lets a never-recorded step run
8
+ * live, so without this it would let a whole committed cache go quietly
9
+ * unused. The entries say which step they were recorded for
10
+ * (`TraceProvenance`), and a step whose own key misses while the store holds
11
+ * a recording made for exactly that step is stale, not new.
12
+ *
13
+ * Only the file store can be listed. The listing is read once per worker
14
+ * and target, on the first strict miss that asks, and kept: every entry
15
+ * written since has a key the run derives, so it is a hit rather than a
16
+ * candidate here.
17
+ */
18
+ import type { ResolvedCacheConfig } from '../config/resolve.ts';
19
+ import { FileCacheStore } from './store.ts';
20
+ import type { TraceProvenance } from './trace.ts';
21
+ export declare class StoredRecordings {
22
+ private readonly store;
23
+ /** Key hashes by `stepKey` of the step each entry was recorded for. */
24
+ private listing;
25
+ constructor(store: FileCacheStore);
26
+ /**
27
+ * The key hash of an entry recorded for `step` under a key other than
28
+ * `keyHash`, or undefined when there is none or the store cannot be
29
+ * listed. `step` is in the form an entry stores it (`recordedProvenance`).
30
+ * Only an entry that records the whole step counts: one written before the
31
+ * params, occurrence, and agent were recorded could belong to another call
32
+ * of the same instruction, and a read-write run that replays it completes
33
+ * it (`flushStagedTraces`). A truncated entry never replays under any key,
34
+ * so it is never the reason a step is stale.
35
+ */
36
+ underAnotherKey(keyHash: string, step: TraceProvenance): Promise<string | undefined>;
37
+ /** Every replayable entry that records its whole step, by `stepKey`. */
38
+ private list;
39
+ }
40
+ /**
41
+ * The listing `cache.strict` checks a run's misses against, for the file
42
+ * store only. Undefined when strict is off, the cache is off, or a custom
43
+ * `cache.store` replaced the file store, which `CacheStore` gives no way to
44
+ * list.
45
+ */
46
+ export declare function storedRecordingsFor(cache: ResolvedCacheConfig): StoredRecordings | undefined;
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Recordings stored under a key the runner no longer derives.
3
+ *
4
+ * A key names everything that can change replay (`identity.ts`), so a new
5
+ * replay policy, an engine minor, or an agent's context changing turns every
6
+ * entry recorded before into a lookup miss, the same `no-entry` as a step
7
+ * that was never recorded. `cache.strict` lets a never-recorded step run
8
+ * live, so without this it would let a whole committed cache go quietly
9
+ * unused. The entries say which step they were recorded for
10
+ * (`TraceProvenance`), and a step whose own key misses while the store holds
11
+ * a recording made for exactly that step is stale, not new.
12
+ *
13
+ * Only the file store can be listed. The listing is read once per worker
14
+ * and target, on the first strict miss that asks, and kept: every entry
15
+ * written since has a key the run derives, so it is a hit rather than a
16
+ * candidate here.
17
+ */
18
+ import { FileCacheStore, MAX_CACHE_WIRE_BYTES } from './store.js';
19
+ export class StoredRecordings {
20
+ store;
21
+ /** Key hashes by `stepKey` of the step each entry was recorded for. */
22
+ listing;
23
+ constructor(store) {
24
+ this.store = store;
25
+ }
26
+ /**
27
+ * The key hash of an entry recorded for `step` under a key other than
28
+ * `keyHash`, or undefined when there is none or the store cannot be
29
+ * listed. `step` is in the form an entry stores it (`recordedProvenance`).
30
+ * Only an entry that records the whole step counts: one written before the
31
+ * params, occurrence, and agent were recorded could belong to another call
32
+ * of the same instruction, and a read-write run that replays it completes
33
+ * it (`flushStagedTraces`). A truncated entry never replays under any key,
34
+ * so it is never the reason a step is stale.
35
+ */
36
+ async underAnotherKey(keyHash, step) {
37
+ const key = stepKey(step);
38
+ if (key === undefined)
39
+ return undefined;
40
+ let listing;
41
+ try {
42
+ this.listing ??= this.list();
43
+ listing = await this.listing;
44
+ }
45
+ catch {
46
+ return undefined;
47
+ }
48
+ for (const candidate of listing.get(key) ?? []) {
49
+ if (candidate === keyHash)
50
+ continue;
51
+ // Listed once: an entry evicted since is no evidence.
52
+ const read = await this.store.read(candidate).catch(() => undefined);
53
+ if (read?.status === 'hit')
54
+ return candidate;
55
+ }
56
+ return undefined;
57
+ }
58
+ /** Every replayable entry that records its whole step, by `stepKey`. */
59
+ async list() {
60
+ const steps = new Map();
61
+ for (const keyHash of await this.store.keyHashes()) {
62
+ const read = await this.store.read(keyHash);
63
+ if (read.status !== 'hit' || read.entry.payload.truncated === true)
64
+ continue;
65
+ const key = read.entry.payload.recordedFor === undefined ? undefined : stepKey(read.entry.payload.recordedFor);
66
+ if (key !== undefined)
67
+ steps.set(key, [...(steps.get(key) ?? []), keyHash]);
68
+ }
69
+ return steps;
70
+ }
71
+ }
72
+ /**
73
+ * The listing `cache.strict` checks a run's misses against, for the file
74
+ * store only. Undefined when strict is off, the cache is off, or a custom
75
+ * `cache.store` replaced the file store, which `CacheStore` gives no way to
76
+ * list.
77
+ */
78
+ export function storedRecordingsFor(cache) {
79
+ if (cache.strict === false || cache.mode === 'off' || cache.store !== undefined)
80
+ return undefined;
81
+ return new StoredRecordings(new FileCacheStore({ directory: cache.dir, maxBytes: MAX_CACHE_WIRE_BYTES, writable: false }));
82
+ }
83
+ /** The whole step as one map key, or undefined when the provenance does not record all of it. */
84
+ function stepKey(step) {
85
+ const { testId, targetId, instructionDigest, paramsDigest, callIndex, agent } = step;
86
+ if (paramsDigest === undefined || callIndex === undefined || agent === undefined)
87
+ return undefined;
88
+ return JSON.stringify([testId, targetId, instructionDigest, paramsDigest, callIndex, agent]);
89
+ }
@@ -14,7 +14,7 @@
14
14
  * means — is exported so end anchors (`anchors.ts`) are checked by the same
15
15
  * rules and can never drift from relocation.
16
16
  */
17
- import type { SemanticNode } from '../engine/surface.ts';
17
+ import type { RedactedNode } from '../agent/observation.ts';
18
18
  import type { TracePosition, TraceTargetDescriptor } from './trace.ts';
19
19
  /**
20
20
  * Version of the replay/relocation policy, part of every cache key. Bumping
@@ -22,7 +22,7 @@ import type { TracePosition, TraceTargetDescriptor } from './trace.ts';
22
22
  * change, because an entry recorded under different rules could relocate to a
23
23
  * different node.
24
24
  */
25
- export declare const REPLAY_POLICY_VERSION = "conservative/5";
25
+ export declare const REPLAY_POLICY_VERSION = "conservative/6";
26
26
  /**
27
27
  * The share of the viewport a scrolled node must have covered when it was
28
28
  * addressed (`ScrollAction.spans`) to scroll as the viewport does once it
@@ -45,14 +45,16 @@ export type RelocationResult = {
45
45
  readonly candidates: readonly string[];
46
46
  };
47
47
  export type DescriptorField = keyof TraceTargetDescriptor;
48
- export interface DescriptorMatchOptions {
49
- readonly redact: (text: string) => string;
50
- }
51
- /** A node's descriptor projection alongside its per-observation id. */
52
- export interface DescribedNode {
53
- readonly id: string;
54
- readonly descriptor: TraceTargetDescriptor;
55
- }
48
+ /**
49
+ * Whether a descriptor can identify a node at all. Role or selector alone
50
+ * cannot: a wrong match acts on the wrong control, so such a descriptor is
51
+ * not relocatable and, as an anchor, would prove nothing. An anonymous one
52
+ * is relocatable by its recorded place only when that place means something
53
+ * on the next screen: it was the one unnamed control of its role, or it sits
54
+ * in a named container (`within`) that tells it from its twins. A place
55
+ * counted among twins with no container is an order, and rows that reorder
56
+ * would hand the action to the wrong one, so such a target hands off.
57
+ */
56
58
  export declare function isRelocatableDescriptor(descriptor: TraceTargetDescriptor): boolean;
57
59
  /**
58
60
  * The tiers a recorded descriptor is matched in, strictest first:
@@ -70,8 +72,6 @@ export declare function isRelocatableDescriptor(descriptor: TraceTargetDescripto
70
72
  export declare function descriptorTiers(descriptor: TraceTargetDescriptor): readonly TraceTargetDescriptor[];
71
73
  /** Every listed field the recording captured must be present and equal on the candidate. */
72
74
  export declare function fieldsEqual(recorded: TraceTargetDescriptor, candidate: TraceTargetDescriptor, fields: readonly DescriptorField[]): boolean;
73
- /** Projects every node of an observation the way the recorder described its targets. */
74
- export declare function describeNodes(nodes: ReadonlyMap<string, SemanticNode>, options: DescriptorMatchOptions): readonly DescribedNode[];
75
75
  /**
76
76
  * Relocates one descriptor against the nodes of a fresh observation, tier by
77
77
  * tier (`descriptorTiers`), each exactly-one-or-diverge. Ambiguity at any
@@ -81,11 +81,11 @@ export declare function describeNodes(nodes: ReadonlyMap<string, SemanticNode>,
81
81
  * same twins and noted which one it acted on, so the same count of twins
82
82
  * resolves to the same one; any other count diverges as before.
83
83
  */
84
- export declare function relocateDescriptor(descriptor: TraceTargetDescriptor, nodes: ReadonlyMap<string, SemanticNode>, options: DescriptorMatchOptions): RelocationResult;
84
+ export declare function relocateDescriptor(descriptor: TraceTargetDescriptor, nodes: ReadonlyMap<string, RedactedNode>): RelocationResult;
85
85
  /**
86
86
  * Where `node` stands among the controls its own description matches on the
87
87
  * screen it was acted on, or undefined when the description already names it
88
88
  * alone. Recorded with the target so a replay can tell one "Set up" button per
89
89
  * card apart without an app change; a distinct label makes it unnecessary.
90
90
  */
91
- export declare function describePosition(node: SemanticNode, within: string | undefined, nodes: ReadonlyMap<string, SemanticNode>, options: DescriptorMatchOptions): TracePosition | undefined;
91
+ export declare function describePosition(node: RedactedNode, within: string | undefined, nodes: ReadonlyMap<string, RedactedNode>): TracePosition | undefined;