@relayflows/sdk 2.0.32 → 2.0.33

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.
@@ -27,7 +27,8 @@ export function helperNamespacesUsed(body, root) {
27
27
  if (program === null)
28
28
  return textFallback(body, root);
29
29
  const used = new Set();
30
- walk(program, (node) => {
30
+ const scope = { rootFunctionFound: false };
31
+ walkReferences(program, root, false, scope, (node) => {
31
32
  if (node.type !== 'MemberExpression')
32
33
  return;
33
34
  const object = node.object;
@@ -106,9 +107,46 @@ function textFallback(body, root) {
106
107
  }
107
108
  return used;
108
109
  }
109
- /** Depth-first over every child node, without pulling in a second package. */
110
- function walk(node, visit) {
111
- visit(node);
110
+ /**
111
+ * Depth-first over references that still resolve to the outer flow context.
112
+ *
113
+ * A nested callback/method parameter or lexical destructuring can reuse the
114
+ * context's spelling while binding an unrelated value. Counting its members
115
+ * would refuse the flow for a mount it never touches. Acorn gives us binding
116
+ * shapes, so scopes are tracked as syntax instead of guessed from text.
117
+ */
118
+ function walkReferences(node, root, shadowed, state, visit) {
119
+ if (isFunction(node)) {
120
+ const parameters = Array.isArray(node.params) ? node.params.filter(isNode) : [];
121
+ const bindsParameter = parameters.some(parameter => patternBinds(parameter, root));
122
+ let bodyHidden = shadowed;
123
+ const isRootFunction = bindsParameter && !state.rootFunctionFound;
124
+ if (isRootFunction)
125
+ state.rootFunctionFound = true;
126
+ else if (bindsParameter || (isNode(node.id) && patternBinds(node.id, root)) || functionVarBinds(node, root)) {
127
+ bodyHidden = true;
128
+ }
129
+ // Parameter initializers run before the function body and are outside a
130
+ // body-level `var` scope. Keep them visible unless the parameter list
131
+ // itself binds the root name (in which case all parameter references are
132
+ // local to that parameter environment).
133
+ const parameterHidden = shadowed || (bindsParameter && !isRootFunction);
134
+ for (const parameter of parameters)
135
+ walkReferences(parameter, root, parameterHidden, state, visit);
136
+ const body = isNode(node.body) ? node.body : undefined;
137
+ if (body !== undefined)
138
+ walkReferences(body, root, bodyHidden, state, visit);
139
+ return;
140
+ }
141
+ let hidden = shadowed;
142
+ if (node.type === 'BlockStatement' && blockBinds(node, root)) {
143
+ hidden = true;
144
+ }
145
+ else if (node.type === 'CatchClause' && isNode(node.param) && patternBinds(node.param, root)) {
146
+ hidden = true;
147
+ }
148
+ if (!hidden)
149
+ visit(node);
112
150
  for (const key of Object.keys(node)) {
113
151
  if (key === 'type' || key === 'start' || key === 'end' || key === 'loc')
114
152
  continue;
@@ -116,13 +154,103 @@ function walk(node, visit) {
116
154
  if (Array.isArray(child)) {
117
155
  for (const entry of child)
118
156
  if (isNode(entry))
119
- walk(entry, visit);
157
+ walkReferences(entry, root, hidden, state, visit);
120
158
  }
121
159
  else if (isNode(child)) {
122
- walk(child, visit);
160
+ walkReferences(child, root, hidden, state, visit);
123
161
  }
124
162
  }
125
163
  }
164
+ function isFunction(node) {
165
+ return node.type === 'ArrowFunctionExpression'
166
+ || node.type === 'FunctionExpression'
167
+ || node.type === 'FunctionDeclaration';
168
+ }
169
+ /** Lexical declarations bind across their complete block, including TDZ. */
170
+ function blockBinds(node, root) {
171
+ const statements = Array.isArray(node.body) ? node.body.filter(isNode) : [];
172
+ return statements.some((statement) => {
173
+ if (statement.type === 'VariableDeclaration' && statement.kind !== 'var') {
174
+ const declarations = Array.isArray(statement.declarations)
175
+ ? statement.declarations.filter(isNode) : [];
176
+ return declarations.some(declaration => isNode(declaration.id) && patternBinds(declaration.id, root));
177
+ }
178
+ return (statement.type === 'FunctionDeclaration' || statement.type === 'ClassDeclaration')
179
+ && isNode(statement.id) && patternBinds(statement.id, root);
180
+ });
181
+ }
182
+ /** Identifier occurrences that introduce bindings, including nested patterns. */
183
+ function patternBinds(pattern, root) {
184
+ switch (pattern.type) {
185
+ case 'Identifier':
186
+ return pattern.name === root;
187
+ case 'AssignmentPattern':
188
+ return isNode(pattern.left) && patternBinds(pattern.left, root);
189
+ case 'RestElement':
190
+ return isNode(pattern.argument) && patternBinds(pattern.argument, root);
191
+ case 'ArrayPattern': {
192
+ const elements = Array.isArray(pattern.elements) ? pattern.elements : [];
193
+ return elements.some(element => isNode(element) && patternBinds(element, root));
194
+ }
195
+ case 'ObjectPattern': {
196
+ const properties = Array.isArray(pattern.properties) ? pattern.properties : [];
197
+ return properties.some(property => {
198
+ if (!isNode(property))
199
+ return false;
200
+ if (property.type === 'RestElement') {
201
+ return isNode(property.argument) && patternBinds(property.argument, root);
202
+ }
203
+ // Object keys are labels, not bindings. Only the value side introduces
204
+ // the local (including a default-value AssignmentPattern's left side).
205
+ return property.type === 'Property'
206
+ && isNode(property.value)
207
+ && patternBinds(property.value, root);
208
+ });
209
+ }
210
+ default:
211
+ return false;
212
+ }
213
+ }
214
+ /** `var` is function-scoped, so a nested function's declaration hides the
215
+ * outer flow context for its entire body, including code before the
216
+ * declaration. Nested functions have their own var scopes and are skipped. */
217
+ function functionVarBinds(node, root) {
218
+ const body = isNode(node.body) ? node.body : undefined;
219
+ if (body === undefined)
220
+ return false;
221
+ let found = false;
222
+ const scan = (current) => {
223
+ if (found)
224
+ return;
225
+ if (current !== body && isFunction(current))
226
+ return;
227
+ if (current.type === 'VariableDeclaration' && current.kind === 'var') {
228
+ const declarations = Array.isArray(current.declarations)
229
+ ? current.declarations.filter(isNode) : [];
230
+ if (declarations.some(declaration => isNode(declaration.id) && patternBinds(declaration.id, root))) {
231
+ found = true;
232
+ return;
233
+ }
234
+ }
235
+ for (const key of Object.keys(current)) {
236
+ if (key === 'type' || key === 'start' || key === 'end' || key === 'loc')
237
+ continue;
238
+ const child = current[key];
239
+ if (Array.isArray(child)) {
240
+ for (const entry of child)
241
+ if (isNode(entry))
242
+ scan(entry);
243
+ }
244
+ else if (isNode(child)) {
245
+ scan(child);
246
+ }
247
+ if (found)
248
+ return;
249
+ }
250
+ };
251
+ scan(body);
252
+ return found;
253
+ }
126
254
  function isNode(value) {
127
255
  return typeof value === 'object' && value !== null && typeof value.type === 'string';
128
256
  }
@@ -1 +1 @@
1
- {"version":3,"file":"helper-reference.js","sourceRoot":"","sources":["../src/helper-reference.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"helper-reference.js","sourceRoot":"","sources":["../src/helper-reference.ts"],"names":[],"mappings":"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"}
@@ -69,7 +69,7 @@ const ADDRESS_SPACE_BYTES = 16 * 1024 * 1024 * 1024;
69
69
  const DATA_BYTES = 3 * 1024 * 1024 * 1024;
70
70
  const SURFACE_RUNTIME_SHA256 = OBJECT_FREEZE({
71
71
  'flow.js': '4aaeacc55de3074f4d121ce7253c3be50a93757e6540ba8159889a9450d1c05c',
72
- 'helpers/providers.js': '7bc62eccaa3a9e786ae0a689bf74160585149e91feef8208e17ef8eca51eed7f',
72
+ 'helpers/providers.js': '4eb06d0d85ca0a3434bb2dbba7407e3d95eef2dfbedaeb0e2de0c0c0ea812457',
73
73
  'provider-trigger.js': 'e2664c65397f93fb486eb6f1e756c7cec3f88b3851d79c23567cad986f80f1ff',
74
74
  'schedule.js': '8fe72f176a75ec0b5f26e12db7a597575c259a2e2cbb59690f9dc20a5e63940b',
75
75
  'triggers.js': '4a3515b571a318f6c7a5661f9310bc9af43e3faf20ea39903a9b363c51258e4c',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@relayflows/sdk",
3
- "version": "2.0.32",
3
+ "version": "2.0.33",
4
4
  "description": "TypeScript-first authoring SDK for Relayflows. Compiles specs; speaks the journal protocol.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -49,7 +49,7 @@
49
49
  "@modelcontextprotocol/sdk": "^1.30.0",
50
50
  "@relayfile/adapter-core": "0.6.2",
51
51
  "@relayfile/relay-helpers": "0.4.12",
52
- "@relayflows/surface": "2.0.32",
52
+ "@relayflows/surface": "2.0.33",
53
53
  "@types/js-yaml": "^4.0.9",
54
54
  "acorn": "^8.18.0",
55
55
  "ai-hist": "0.4.1",
@@ -114,6 +114,18 @@ export interface FinalStep {
114
114
  model?: string;
115
115
  tokensInput?: number;
116
116
  tokensOutput?: number;
117
+ /**
118
+ * True when tokens were counted but nobody priced them.
119
+ *
120
+ * The kernel already records this (`run-state.ts`'s `Spend`), and
121
+ * `flows status` already prints `(unmetered)` from it. Without it on the
122
+ * wire, Cloud cannot tell a step that genuinely cost nothing — a
123
+ * deterministic `echo` — from one that spent real money against an unpriced
124
+ * model: both arrive as an absent `costUsd` and render blank. Codex is the
125
+ * everyday case, because it selects its own model and never reports it, so
126
+ * `MODEL_PRICING` has nothing to match (`model-pricing.ts` says so outright).
127
+ */
128
+ costUnmetered?: true;
117
129
  costUsd?: number;
118
130
  error?: string;
119
131
  detail?: Record<string, unknown>;
@@ -314,6 +326,7 @@ interface AttemptRecord {
314
326
  tokensIn?: number;
315
327
  tokensOut?: number;
316
328
  costUsd?: number;
329
+ costUnmetered?: boolean;
317
330
  }
318
331
 
319
332
  /**
@@ -346,6 +359,7 @@ function attemptsByStep(events: readonly JournalEvent[]): Map<string, AttemptRec
346
359
  ...(typeof result?.['total_cost_usd'] === 'number' && Number.isFinite(result['total_cost_usd'])
347
360
  && result['total_cost_usd'] >= 0
348
361
  ? { costUsd: result['total_cost_usd'] as number } : {}),
362
+ ...(budget['dollars_unmetered'] === true ? { costUnmetered: true } : {}),
349
363
  });
350
364
  byStep.set(event.step_id, entries);
351
365
  }
@@ -374,6 +388,23 @@ function finalStep(
374
388
  // Cloud totals these rows for the run's spend — so a retried agent's earlier
375
389
  // charges simply vanished from the run.
376
390
  const spentUsd = attempts.reduce((total, entry) => total + (entry.costUsd ?? 0), 0);
391
+ const priced = attempts.some(entry => entry.costUsd !== undefined);
392
+ /**
393
+ * Any unpriced attempt makes the step's cost unknown, even when another
394
+ * attempt reported one.
395
+ *
396
+ * The first cut suppressed the flag whenever `spentUsd > 0`, which published
397
+ * a partial subtotal as though it were the whole cost: a step that spent
398
+ * $0.02 on attempt 1 and an unpriced amount on attempt 2 reported exactly
399
+ * $0.02. The two facts are not exclusive — `costUsd` is then a *lower bound*
400
+ * and the flag says so. `run-state.ts`'s `addSpend` keeps them together for
401
+ * the same reason.
402
+ *
403
+ * Presence, not a positive sum: an attempt reporting `total_cost_usd: 0` did
404
+ * produce a price, and testing `spentUsd === 0` would have called that step
405
+ * unpriced.
406
+ */
407
+ const unmetered = attempts.some(entry => entry.costUnmetered === true);
377
408
  const detail = stepDetail(attempts, env);
378
409
  const verification = step.last_attempt?.verification ?? null;
379
410
  // What the step said about itself. A gate's verdict detail is the nearest
@@ -407,7 +438,8 @@ function finalStep(
407
438
  ...(model === undefined ? {} : { model }),
408
439
  ...(tokensIn > 0 ? { tokensInput: Math.min(tokensIn, MAX_INT32) } : {}),
409
440
  ...(tokensOut > 0 ? { tokensOutput: Math.min(tokensOut, MAX_INT32) } : {}),
410
- ...(spentUsd > 0 ? { costUsd: spentUsd } : {}),
441
+ ...(priced ? { costUsd: spentUsd } : {}),
442
+ ...(unmetered ? { costUnmetered: true as const } : {}),
411
443
  ...(error === undefined ? {} : { error }),
412
444
  ...(detail.detail === undefined ? {} : { detail: detail.detail }),
413
445
  ...(detail.truncated === undefined ? {} : { detailTruncated: detail.truncated }),
@@ -32,12 +32,22 @@ export const CODEX_FRAME_TYPES: ReadonlySet<string> = new Set([
32
32
  /** Item types whose lifecycles are matched and whose fields are read. */
33
33
  const SUPPORTED_ITEMS: ReadonlySet<string> = new Set([
34
34
  'agent_message', 'reasoning', 'command_execution', 'file_change', 'mcp_tool_call', 'error',
35
+ // The rest of Codex's `ThreadItemDetails` union
36
+ // (codex-rs/exec/src/exec_events.rs). Without these three, a real agent turn
37
+ // reported them as bare `unknown` placeholders naming a type and a size —
38
+ // honest, but useless: `todo_list` in particular updates continuously
39
+ // through a turn, so it was the most frequent thing a reader could not read.
40
+ 'web_search', 'todo_list', 'collab_tool_call',
35
41
  ]);
36
42
 
37
43
  const ITEM_FRAMES: ReadonlySet<string> = new Set(['item.started', 'item.updated', 'item.completed']);
38
44
 
39
45
  /** Calls are numbered; an `apply_patch` is file activity, not a call. */
40
- const NUMBERED_ITEMS: ReadonlySet<string> = new Set(['command_execution', 'mcp_tool_call']);
46
+ const NUMBERED_ITEMS: ReadonlySet<string> = new Set([
47
+ 'command_execution', 'mcp_tool_call', 'web_search', 'collab_tool_call',
48
+ ]);
49
+ /** Todo entries listed per snapshot; a longer plan is counted, not printed. */
50
+ const TODO_MAX_ITEMS = 12;
41
51
 
42
52
  interface Lifecycle {
43
53
  snapshots: Array<Record<string, unknown>>;
@@ -246,6 +256,65 @@ function mcpEntry(item: Record<string, unknown>, complete: boolean, context: Con
246
256
  });
247
257
  }
248
258
 
259
+ /**
260
+ * `web_search`: a query and what was done with it (`WebSearchItem` — `query`,
261
+ * `action`, optional structured `results`). Rendered as a call rather than
262
+ * prose, so a reader sees the search in sequence beside the commands.
263
+ */
264
+ function webSearchEntry(item: Record<string, unknown>, complete: boolean, context: Context,
265
+ seq: number): TranscriptEntry | null {
266
+ const query = str(item['query']);
267
+ if (query === null) return null;
268
+ const action = isRecord(item['action']) ? str(item['action']['type']) : str(item['action']);
269
+ const results = item['results'];
270
+ const count = Array.isArray(results) ? results.length : null;
271
+ return tool('web_search', bounded(query, context.clean),
272
+ count === null ? null : JSON.stringify(results).length, false, {
273
+ seq, status: action === null ? null : bounded(action, context.clean, 40),
274
+ exit_code: null,
275
+ output_excerpt: count === null ? null : `${count} result${count === 1 ? '' : 's'}`,
276
+ output_truncated: false, complete, error: null,
277
+ });
278
+ }
279
+
280
+ /**
281
+ * `collab_tool_call`: Codex driving other Codex threads (`CollabToolCallItem` —
282
+ * `tool`, `sender_thread_id`, `receiver_thread_ids`, `agents_states`).
283
+ *
284
+ * The per-agent states are reduced to a count per status rather than listed:
285
+ * `agents_states` is a map keyed by agent id, and ids are not something a
286
+ * transcript reader can act on.
287
+ */
288
+ function collabEntry(item: Record<string, unknown>, complete: boolean, context: Context,
289
+ seq: number): TranscriptEntry | null {
290
+ const name = str(item['tool']);
291
+ if (name === null) return null;
292
+ const receivers = Array.isArray(item['receiver_thread_ids']) ? item['receiver_thread_ids'].length : 0;
293
+ const states = isRecord(item['agents_states']) ? item['agents_states'] : null;
294
+ const byStatus = new Map<string, number>();
295
+ if (states !== null) {
296
+ for (const state of Object.values(states)) {
297
+ const status = isRecord(state) ? str(state['status']) : null;
298
+ if (status !== null) byStatus.set(status, (byStatus.get(status) ?? 0) + 1);
299
+ }
300
+ }
301
+ // Redacted like every neighbouring field. A status is provider text, not a
302
+ // closed vocabulary this module controls, and it was the one string here that
303
+ // reached the page without passing the redactor.
304
+ const summary = bounded(
305
+ [...byStatus.entries()].sort().map(([status, n]) => `${n} ${status}`).join(', '),
306
+ context.clean,
307
+ );
308
+ const status = str(item['status']);
309
+ return tool('collab_tool_call',
310
+ bounded(`${name}${receivers > 0 ? ` → ${receivers} thread${receivers === 1 ? '' : 's'}` : ''}`, context.clean),
311
+ null, status === 'failed', {
312
+ seq, status: status === null ? null : bounded(status, context.clean, 40),
313
+ exit_code: null, output_excerpt: summary.length === 0 ? null : summary,
314
+ output_truncated: false, complete, error: null,
315
+ });
316
+ }
317
+
249
318
  function fileChangeEntry(item: Record<string, unknown>, complete: boolean, context: Context): TranscriptEntry | null {
250
319
  const raw = item['changes'];
251
320
  if (!Array.isArray(raw)) return null;
@@ -288,12 +357,35 @@ function itemEntries(item: Record<string, unknown>, complete: boolean, context:
288
357
  if (message === null) return [placeholder(context, itemType)];
289
358
  return [{ kind: 'error', source: 'item', message: bounded(message, context.clean, ERROR_MAX_CHARS) }];
290
359
  }
291
- if (itemType === 'command_execution' || itemType === 'mcp_tool_call') {
360
+ if (itemType === 'todo_list') {
361
+ // A plan, not a call: it carries no status and earns no sequence number.
362
+ // `items[]` is `{text, completed}` (TodoListItem/TodoItem).
363
+ const items = Array.isArray(item['items']) ? item['items'] : null;
364
+ if (items === null) return [placeholder(context, itemType)];
365
+ const listed = items.slice(0, TODO_MAX_ITEMS).flatMap((entry) => {
366
+ const todo = isRecord(entry) ? entry : undefined;
367
+ const text = str(todo?.['text']);
368
+ return text === null ? [] : [{ text: context.clean(text), done: todo!['completed'] === true }];
369
+ });
370
+ // Counted across the whole plan; only the *display* is capped. Counting the
371
+ // prefix rendered a 13-item plan whose last item was done as `0/13`, which
372
+ // is exactly backwards from "the rest are counted, not printed".
373
+ const done = items.filter(entry => isRecord(entry) && entry['completed'] === true).length;
374
+ return [{
375
+ kind: 'todo', total: items.length, done,
376
+ items: listed, ...(items.length > listed.length ? { omitted: items.length - listed.length } : {}),
377
+ ...(complete ? {} : { complete: false }),
378
+ }];
379
+ }
380
+ if (itemType === 'command_execution' || itemType === 'mcp_tool_call'
381
+ || itemType === 'web_search' || itemType === 'collab_tool_call') {
292
382
  // The number is only spent on a call this module could actually read, so a
293
383
  // malformed item leaves no gap in the sequence.
294
384
  const seq = context.state.seq + 1;
295
- const entry = itemType === 'command_execution'
296
- ? commandEntry(item, complete, context, seq) : mcpEntry(item, complete, context, seq);
385
+ const entry = itemType === 'command_execution' ? commandEntry(item, complete, context, seq)
386
+ : itemType === 'mcp_tool_call' ? mcpEntry(item, complete, context, seq)
387
+ : itemType === 'web_search' ? webSearchEntry(item, complete, context, seq)
388
+ : collabEntry(item, complete, context, seq);
297
389
  if (entry === null) return [placeholder(context, itemType)];
298
390
  context.state.seq = seq;
299
391
  return [entry];
@@ -138,6 +138,22 @@ export interface TranscriptResult {
138
138
  cache_creation: number | null;
139
139
  }
140
140
 
141
+ /**
142
+ * Codex's running to-do list (`TodoListItem`). A plan, not a call: it carries
143
+ * no status and takes no sequence number, and it updates repeatedly through a
144
+ * turn — which is why leaving it unparsed was the most visible gap.
145
+ */
146
+ export interface TranscriptTodo {
147
+ kind: 'todo';
148
+ total: number;
149
+ done: number;
150
+ items: Array<{ text: string; done: boolean }>;
151
+ /** Entries past the per-snapshot cap: counted, never printed. */
152
+ omitted?: number;
153
+ /** False when read off a snapshot that never completed. */
154
+ complete?: boolean;
155
+ }
156
+
141
157
  export interface TranscriptUnknown {
142
158
  kind: 'unknown';
143
159
  type: string;
@@ -152,7 +168,8 @@ export interface TranscriptUnparsed {
152
168
  export type TranscriptEntry =
153
169
  | TranscriptAttempt | TranscriptAttemptOmitted | TranscriptInit | TranscriptMessage
154
170
  | TranscriptThinking | TranscriptTool | TranscriptFileChange | TranscriptThread
155
- | TranscriptTurn | TranscriptError | TranscriptResult | TranscriptUnknown | TranscriptUnparsed;
171
+ | TranscriptTurn | TranscriptError | TranscriptResult | TranscriptTodo
172
+ | TranscriptUnknown | TranscriptUnparsed;
156
173
 
157
174
  export interface ParsedTranscript {
158
175
  entries: TranscriptEntry[];
@@ -34,7 +34,7 @@ import { redact } from './redact.js';
34
34
  export type {
35
35
  ParsedTranscript, TranscriptEntry, TranscriptAttempt, TranscriptAttemptOmitted, TranscriptError,
36
36
  TranscriptFileChange, TranscriptInit, TranscriptMessage, TranscriptResult, TranscriptThinking,
37
- TranscriptThread, TranscriptTool, TranscriptToolCodex, TranscriptTurn, TranscriptUnknown,
37
+ TranscriptThread, TranscriptTodo, TranscriptTool, TranscriptToolCodex, TranscriptTurn, TranscriptUnknown,
38
38
  TranscriptUnparsed,
39
39
  } from './cloud-transcript-types.js';
40
40
 
@@ -297,6 +297,14 @@ export function renderAgentTranscript(parsed: ParsedTranscript): string[] {
297
297
  case 'thinking':
298
298
  lines.push(` thinking ${thousands(entry.chars)} chars (not shown)`);
299
299
  break;
300
+ case 'todo': {
301
+ // The plan the agent is working to, as it stood at this snapshot.
302
+ const progress = `${entry.done}/${entry.total}`;
303
+ lines.push(` plan ${progress}${entry.complete === false ? ' (in progress)' : ''}`);
304
+ for (const todo of entry.items) lines.push(` ${todo.done ? '✓' : '·'} ${safe(todo.text)}`);
305
+ if (entry.omitted !== undefined) lines.push(` …${entry.omitted} more`);
306
+ break;
307
+ }
300
308
  case 'tool': {
301
309
  const size = entry.is_error ? 'ERROR'
302
310
  : entry.result_chars === null ? 'no result' : `${thousands(entry.result_chars)} chars`;
@@ -27,7 +27,8 @@ export function helperNamespacesUsed(body: string, root: string): ReadonlySet<st
27
27
  const program = parseFlowBody(body);
28
28
  if (program === null) return textFallback(body, root);
29
29
  const used = new Set<string>();
30
- walk(program, (node) => {
30
+ const scope = { rootFunctionFound: false };
31
+ walkReferences(program, root, false, scope, (node) => {
31
32
  if (node.type !== 'MemberExpression') return;
32
33
  const object = node.object as AstNode | undefined;
33
34
  if (object?.type !== 'Identifier' || object.name !== root) return;
@@ -111,20 +112,144 @@ type AstNode = {
111
112
  [key: string]: unknown;
112
113
  };
113
114
 
114
- /** Depth-first over every child node, without pulling in a second package. */
115
- function walk(node: AstNode, visit: (node: AstNode) => void): void {
116
- visit(node);
115
+ /**
116
+ * Depth-first over references that still resolve to the outer flow context.
117
+ *
118
+ * A nested callback/method parameter or lexical destructuring can reuse the
119
+ * context's spelling while binding an unrelated value. Counting its members
120
+ * would refuse the flow for a mount it never touches. Acorn gives us binding
121
+ * shapes, so scopes are tracked as syntax instead of guessed from text.
122
+ */
123
+ function walkReferences(
124
+ node: AstNode,
125
+ root: string,
126
+ shadowed: boolean,
127
+ state: { rootFunctionFound: boolean },
128
+ visit: (node: AstNode) => void,
129
+ ): void {
130
+ if (isFunction(node)) {
131
+ const parameters = Array.isArray(node.params) ? node.params.filter(isNode) : [];
132
+ const bindsParameter = parameters.some(parameter => patternBinds(parameter, root));
133
+ let bodyHidden = shadowed;
134
+ const isRootFunction = bindsParameter && !state.rootFunctionFound;
135
+ if (isRootFunction) state.rootFunctionFound = true;
136
+ else if (bindsParameter || (isNode(node.id) && patternBinds(node.id, root)) || functionVarBinds(node, root)) {
137
+ bodyHidden = true;
138
+ }
139
+ // Parameter initializers run before the function body and are outside a
140
+ // body-level `var` scope. Keep them visible unless the parameter list
141
+ // itself binds the root name (in which case all parameter references are
142
+ // local to that parameter environment).
143
+ const parameterHidden = shadowed || (bindsParameter && !isRootFunction);
144
+ for (const parameter of parameters) walkReferences(parameter, root, parameterHidden, state, visit);
145
+ const body = isNode(node.body) ? node.body : undefined;
146
+ if (body !== undefined) walkReferences(body, root, bodyHidden, state, visit);
147
+ return;
148
+ }
149
+ let hidden = shadowed;
150
+ if (node.type === 'BlockStatement' && blockBinds(node, root)) {
151
+ hidden = true;
152
+ } else if (node.type === 'CatchClause' && isNode(node.param) && patternBinds(node.param, root)) {
153
+ hidden = true;
154
+ }
155
+
156
+ if (!hidden) visit(node);
117
157
  for (const key of Object.keys(node)) {
118
158
  if (key === 'type' || key === 'start' || key === 'end' || key === 'loc') continue;
119
159
  const child = node[key];
120
160
  if (Array.isArray(child)) {
121
- for (const entry of child) if (isNode(entry)) walk(entry, visit);
161
+ for (const entry of child) if (isNode(entry)) walkReferences(entry, root, hidden, state, visit);
122
162
  } else if (isNode(child)) {
123
- walk(child, visit);
163
+ walkReferences(child, root, hidden, state, visit);
124
164
  }
125
165
  }
126
166
  }
127
167
 
168
+ function isFunction(node: AstNode): boolean {
169
+ return node.type === 'ArrowFunctionExpression'
170
+ || node.type === 'FunctionExpression'
171
+ || node.type === 'FunctionDeclaration';
172
+ }
173
+
174
+ /** Lexical declarations bind across their complete block, including TDZ. */
175
+ function blockBinds(node: AstNode, root: string): boolean {
176
+ const statements = Array.isArray(node.body) ? node.body.filter(isNode) : [];
177
+ return statements.some((statement) => {
178
+ if (statement.type === 'VariableDeclaration' && statement.kind !== 'var') {
179
+ const declarations = Array.isArray(statement.declarations)
180
+ ? statement.declarations.filter(isNode) : [];
181
+ return declarations.some(declaration => isNode(declaration.id) && patternBinds(declaration.id, root));
182
+ }
183
+ return (statement.type === 'FunctionDeclaration' || statement.type === 'ClassDeclaration')
184
+ && isNode(statement.id) && patternBinds(statement.id, root);
185
+ });
186
+ }
187
+
188
+ /** Identifier occurrences that introduce bindings, including nested patterns. */
189
+ function patternBinds(pattern: AstNode, root: string): boolean {
190
+ switch (pattern.type) {
191
+ case 'Identifier':
192
+ return pattern.name === root;
193
+ case 'AssignmentPattern':
194
+ return isNode(pattern.left) && patternBinds(pattern.left, root);
195
+ case 'RestElement':
196
+ return isNode(pattern.argument) && patternBinds(pattern.argument, root);
197
+ case 'ArrayPattern': {
198
+ const elements = Array.isArray(pattern.elements) ? pattern.elements : [];
199
+ return elements.some(element => isNode(element) && patternBinds(element, root));
200
+ }
201
+ case 'ObjectPattern': {
202
+ const properties = Array.isArray(pattern.properties) ? pattern.properties : [];
203
+ return properties.some(property => {
204
+ if (!isNode(property)) return false;
205
+ if (property.type === 'RestElement') {
206
+ return isNode(property.argument) && patternBinds(property.argument, root);
207
+ }
208
+ // Object keys are labels, not bindings. Only the value side introduces
209
+ // the local (including a default-value AssignmentPattern's left side).
210
+ return property.type === 'Property'
211
+ && isNode(property.value)
212
+ && patternBinds(property.value, root);
213
+ });
214
+ }
215
+ default:
216
+ return false;
217
+ }
218
+ }
219
+
220
+ /** `var` is function-scoped, so a nested function's declaration hides the
221
+ * outer flow context for its entire body, including code before the
222
+ * declaration. Nested functions have their own var scopes and are skipped. */
223
+ function functionVarBinds(node: AstNode, root: string): boolean {
224
+ const body = isNode(node.body) ? node.body : undefined;
225
+ if (body === undefined) return false;
226
+ let found = false;
227
+ const scan = (current: AstNode): void => {
228
+ if (found) return;
229
+ if (current !== body && isFunction(current)) return;
230
+ if (current.type === 'VariableDeclaration' && current.kind === 'var') {
231
+ const declarations = Array.isArray(current.declarations)
232
+ ? current.declarations.filter(isNode) : [];
233
+ if (declarations.some(declaration => isNode(declaration.id) && patternBinds(declaration.id, root))) {
234
+ found = true;
235
+ return;
236
+ }
237
+ }
238
+ for (const key of Object.keys(current)) {
239
+ if (key === 'type' || key === 'start' || key === 'end' || key === 'loc') continue;
240
+ const child = current[key];
241
+ if (Array.isArray(child)) {
242
+ for (const entry of child) if (isNode(entry)) scan(entry);
243
+ } else if (isNode(child)) {
244
+ scan(child);
245
+ }
246
+ if (found) return;
247
+ }
248
+ };
249
+ scan(body);
250
+ return found;
251
+ }
252
+
128
253
  function isNode(value: unknown): value is AstNode {
129
254
  return typeof value === 'object' && value !== null && typeof (value as { type?: unknown }).type === 'string';
130
255
  }
@@ -109,7 +109,7 @@ const ADDRESS_SPACE_BYTES = 16 * 1024 * 1024 * 1024;
109
109
  const DATA_BYTES = 3 * 1024 * 1024 * 1024;
110
110
  const SURFACE_RUNTIME_SHA256 = OBJECT_FREEZE({
111
111
  'flow.js': '4aaeacc55de3074f4d121ce7253c3be50a93757e6540ba8159889a9450d1c05c',
112
- 'helpers/providers.js': '7bc62eccaa3a9e786ae0a689bf74160585149e91feef8208e17ef8eca51eed7f',
112
+ 'helpers/providers.js': '4eb06d0d85ca0a3434bb2dbba7407e3d95eef2dfbedaeb0e2de0c0c0ea812457',
113
113
  'provider-trigger.js': 'e2664c65397f93fb486eb6f1e756c7cec3f88b3851d79c23567cad986f80f1ff',
114
114
  'schedule.js': '8fe72f176a75ec0b5f26e12db7a597575c259a2e2cbb59690f9dc20a5e63940b',
115
115
  'triggers.js': '4a3515b571a318f6c7a5661f9310bc9af43e3faf20ea39903a9b363c51258e4c',