@molecule/api-ai-tools 1.5.16 → 1.5.18

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.
package/README.md CHANGED
@@ -3,7 +3,7 @@ AUTO-GENERATED — DO NOT EDIT THIS FILE.
3
3
  Generated by `mlcl sync-docs` from the package's src/index.ts JSDoc + mlcl/registry.json.
4
4
  Edits here are overwritten on the next commit (molecule's pre-commit hook regenerates).
5
5
  To change this document, edit the module-level JSDoc in src/index.ts.
6
- Generated: 2026-10-16T07:29:43.225Z
6
+ Generated: 2026-10-16T09:30:45.054Z
7
7
  -->
8
8
 
9
9
  # @molecule/api-ai-tools
@@ -488,6 +488,24 @@ function buildAgentPrompt(ctx: PromptContext): string
488
488
 
489
489
  **Returns:** Fully assembled system prompt text for the coding agent.
490
490
 
491
+ #### `moleculeDistReadNote(path)`
492
+
493
+ The steer attached to a SUCCESSFUL read of a package's built output. The ENOENT
494
+ redirect below fires only when the read FAILS, so a model that knows the hoisted
495
+ layout reads `node_modules/@molecule/<pkg>/dist/*.js` happily and never learns the
496
+ documented API is one call away (X0 flash-m70: 24 such reads in 57 s, no note on
497
+ any of them). Carried as `distNote` rather than merged into `note` so a host that
498
+ has read_molecule_doc (molecule-dev's cadence note, which also names the functions)
499
+ can supersede it without string-matching.
500
+
501
+ ```typescript
502
+ function moleculeDistReadNote(path: string): string | null
503
+ ```
504
+
505
+ - `path` — The resolved path that was read.
506
+
507
+ **Returns:** The note, or null when the path is not inside a package's `dist`.
508
+
491
509
  #### `buildTools(backend, config)`
492
510
 
493
511
  Build a complete set of AI agent tools bound to an execution backend.
@@ -1027,7 +1045,9 @@ const MAX_READ_RETURN_CHARS: number
1027
1045
 
1028
1046
  #### `SMALL_FILE_WHOLE_CHARS`
1029
1047
 
1030
- A file this small is returned whole even when a window is asked for (paging it costs a call each).
1048
+ A file this small is returned whole on a second window of it (paging it costs a call each).
1049
+ The same bound as {@link MAX_READ_RETURN_CHARS}: the windowed-read note promises that a
1050
+ file under it "fits in ONE call", and a smaller bound here broke that promise (X1 flash-m67).
1031
1051
 
1032
1052
  ```typescript
1033
1053
  const SMALL_FILE_WHOLE_CHARS: number
package/dist/tools.d.ts CHANGED
@@ -8,6 +8,19 @@
8
8
  */
9
9
  import type { AITool } from '@molecule/api-ai';
10
10
  import type { ExecutionBackend, ToolBuildConfig } from './types.js';
11
+ /**
12
+ * The steer attached to a SUCCESSFUL read of a package's built output. The ENOENT
13
+ * redirect below fires only when the read FAILS, so a model that knows the hoisted
14
+ * layout reads `node_modules/@molecule/<pkg>/dist/*.js` happily and never learns the
15
+ * documented API is one call away (X0 flash-m70: 24 such reads in 57 s, no note on
16
+ * any of them). Carried as `distNote` rather than merged into `note` so a host that
17
+ * has read_molecule_doc (molecule-dev's cadence note, which also names the functions)
18
+ * can supersede it without string-matching.
19
+ *
20
+ * @param path - The resolved path that was read.
21
+ * @returns The note, or null when the path is not inside a package's `dist`.
22
+ */
23
+ export declare function moleculeDistReadNote(path: string): string | null;
11
24
  /**
12
25
  * Build a complete set of AI agent tools bound to an execution backend.
13
26
  *
@@ -1 +1 @@
1
- {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AA8B9C,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AA6CnE;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,gBAAgB,EAAE,MAAM,CAAC,EAAE,eAAe,GAAG,MAAM,EAAE,CAupDxF"}
1
+ {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AA0H9C,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AA6CnE;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAUhE;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,gBAAgB,EAAE,MAAM,CAAC,EAAE,eAAe,GAAG,MAAM,EAAE,CAutDxF"}
package/dist/tools.js CHANGED
@@ -25,12 +25,118 @@ const waitedByTask = new Map();
25
25
  const logBytesByTask = new Map();
26
26
  /** The file a background command writes its own output to, when it redirects it. */
27
27
  const outputFileByTask = new Map();
28
+ /**
29
+ * Task ids whose own-output file has already been looked for this process.
30
+ * {@link outputFileByTask} is empty for every task after a restart — it is
31
+ * process memory and the task is not — so without this the recovery below
32
+ * would re-read the task's script on every single wait.
33
+ */
34
+ const outputFileProbed = new Set();
35
+ /** A millisecond span the way a model reads it: seconds under 90, else minutes. */
36
+ function durationText(ms) {
37
+ const s = Math.max(0, Math.round(ms / 1000));
38
+ const mins = Math.floor(s / 60);
39
+ return s < 90 ? `${s} s` : s % 60 ? `${mins} min ${s % 60} s` : `${mins} min`;
40
+ }
41
+ /**
42
+ * When a background command started, read from its own id: the handle is minted
43
+ * as `mol-bg-${Date.now().toString(36)}-…`, so the start stamp is in the id and
44
+ * no new state has to be kept to have it.
45
+ *
46
+ * @param taskId - The handle a run_in_background command returned.
47
+ * @returns Epoch ms, or null when the id carries no usable stamp (a hand-written
48
+ * or synthetic id) — a wrong number in a note is worse than none.
49
+ */
50
+ function taskStartedAt(taskId) {
51
+ const stamp = Number.parseInt(taskId.split('-')[2] ?? '', 36);
52
+ const now = Date.now();
53
+ return Number.isFinite(stamp) && stamp > now - 24 * 3600_000 && stamp < now + 60_000
54
+ ? stamp
55
+ : null;
56
+ }
57
+ /**
58
+ * The progress a run reports in its own log: the last `[done/total]` counter a
59
+ * Playwright reporter writes (`[86/139] [chromium] › e2e/settings.spec.ts:…`).
60
+ *
61
+ * @param log - The log slice this wait read.
62
+ * @returns The counter, or null when the log carries none.
63
+ */
64
+ function logProgress(log) {
65
+ const last = [...log.matchAll(/\[(\d+)\/(\d+)\]/g)].at(-1);
66
+ const done = Number(last?.[1]);
67
+ const total = Number(last?.[2]);
68
+ return last && total > 0 && done >= 0 && done <= total ? { done, total } : null;
69
+ }
70
+ /**
71
+ * What a wait knows about the command BESIDES "it has not exited": how long it
72
+ * has been running, whether its output is still growing, how far its own log
73
+ * says it has got, and how much longer that implies.
74
+ *
75
+ * X1 flash-m62 (2026-10-16) spent three 290 s waits on one 8.3-minute suite and
76
+ * every answer read only "Still running after 280s": the run could not tell a
77
+ * suite with 30 s left from one with 6 minutes left, so the wait cadence — not
78
+ * the suite — set the pace, and the run learned nothing for its 14 minutes of
79
+ * waiting. The elapsed time comes from the task id, the progress from the log
80
+ * this wait already read, and the time left is the rate so far, said as such.
81
+ *
82
+ * @param o - The wait's facts: `taskId`, the `slice` of the log it read,
83
+ * `stillWriting` (the log grew during this wait), `bytes` (the log's size, null
84
+ * when unreadable), and `ceilingMs` (this call's ceiling, for "one more wait").
85
+ * @returns A sentence to append to a running note, or '' when nothing is known.
86
+ */
87
+ function waitProgressNote(o) {
88
+ const startedAt = taskStartedAt(o.taskId);
89
+ const elapsedMs = startedAt === null ? null : Math.max(0, Date.now() - startedAt);
90
+ const parts = [];
91
+ if (elapsedMs !== null)
92
+ parts.push(`it has been running ${durationText(elapsedMs)}`);
93
+ if (o.stillWriting)
94
+ parts.push(`its output is still growing (${o.bytes} bytes) — slow, not stuck`);
95
+ else if (o.bytes !== null)
96
+ parts.push(`its log has not grown during this wait (${o.bytes} bytes)`);
97
+ const counter = logProgress(o.slice);
98
+ if (counter) {
99
+ parts.push(`its log is at ${counter.done} of ${counter.total}`);
100
+ if (elapsedMs !== null && counter.done > 0 && counter.done < counter.total) {
101
+ const leftMs = Math.round((elapsedMs * (counter.total - counter.done)) / counter.done);
102
+ parts.push(leftMs <= o.ceilingMs
103
+ ? `about ${durationText(leftMs)} left at that rate — one more wait covers it`
104
+ : `about ${durationText(leftMs)} left at that rate (${Math.ceil(leftMs / o.ceilingMs)} more waits)`);
105
+ }
106
+ }
107
+ if (!parts.length)
108
+ return '';
109
+ const sentence = parts.join('; ');
110
+ return ` ${sentence[0].toUpperCase()}${sentence.slice(1)}.`;
111
+ }
28
112
  import { checkBlockedCommand, declaredBudgetSeconds, DEFAULT_SEARCH_EXCLUDED_DIRS, directoryReadHint, fusedLine, isEnvFilePath, isOutsideRoot, isValidGlob, MAX_BATCH_READ_BYTES, MAX_BATCH_READ_FILES, MAX_FIND_RESULTS, MAX_OUTPUT_SIZE, MAX_READ_RETURN_CHARS, MAX_READ_SIZE, MAX_SEARCH_CONTEXT_LINES, MAX_SEARCH_RESULTS, MAX_WRITE_SIZE, outputIsWithheldUntilExit, outputRedirectTarget, parseCheckCommand, parseCheckProblem, pathArgError, printsOnlySource, redactSecrets, redactSecretsInCode, resolvePath, RUNNING_FAILURE_LINES, runningOutputSliceCommand, shellQuote, sliceLines, SMALL_FILE_WHOLE_CHARS, streamingBackgroundCommand, stripControlChars, truncateMiddle, whitespaceTolerantReplace, } from './utilities.js';
29
113
  /**
30
114
  * How long exec_command waits for a command it backgrounded only because the
31
115
  * command asked for more than the tool's ceiling, before handing back a task id.
32
116
  */
33
117
  const OVER_CEILING_GRACE_MS = 45_000;
118
+ /**
119
+ * The steer attached to a SUCCESSFUL read of a package's built output. The ENOENT
120
+ * redirect below fires only when the read FAILS, so a model that knows the hoisted
121
+ * layout reads `node_modules/@molecule/<pkg>/dist/*.js` happily and never learns the
122
+ * documented API is one call away (X0 flash-m70: 24 such reads in 57 s, no note on
123
+ * any of them). Carried as `distNote` rather than merged into `note` so a host that
124
+ * has read_molecule_doc (molecule-dev's cadence note, which also names the functions)
125
+ * can supersede it without string-matching.
126
+ *
127
+ * @param path - The resolved path that was read.
128
+ * @returns The note, or null when the path is not inside a package's `dist`.
129
+ */
130
+ export function moleculeDistReadNote(path) {
131
+ const pkg = /node_modules\/@molecule\/([a-z0-9-]+)\/dist\//.exec(path)?.[1];
132
+ if (!pkg)
133
+ return null;
134
+ return (`This is @molecule/${pkg}'s compiled dist, not its API: read_molecule_doc("@molecule/${pkg}") ` +
135
+ 'returns its README.md — the functions, options and bond wiring — in one call.' +
136
+ (/\.d\.ts$/.test(path)
137
+ ? ' The declarations above are generated from it.'
138
+ : ' For one signature, read the matching .d.ts file.'));
139
+ }
34
140
  /**
35
141
  * Build a complete set of AI agent tools bound to an execution backend.
36
142
  *
@@ -135,6 +241,8 @@ export function buildTools(backend, config) {
135
241
  }
136
242
  /** Paths read with a window in this tool set, to tell paging from one targeted read. */
137
243
  const windowedReads = new Set();
244
+ /** The text each path was last handed back WHOLE as, so that correction fires once per version. */
245
+ const wholeReturned = new Map();
138
246
  /**
139
247
  * Verify that `path` does not symlink outside the project root when guards are enabled.
140
248
  *
@@ -365,6 +473,10 @@ export function buildTools(backend, config) {
365
473
  const symlinkErr = await checkSymlink(path);
366
474
  if (symlinkErr)
367
475
  return { error: symlinkErr };
476
+ // Carried on every successful read of a package's built output, so no dist read
477
+ // goes out unsteered (see moleculeDistReadNote).
478
+ const distNote = moleculeDistReadNote(path);
479
+ const noted = (r) => distNote ? { ...r, distNote } : r;
368
480
  try {
369
481
  const content = await backend.readFile(path);
370
482
  // A read that produced no usable text is verified against the file's real
@@ -376,12 +488,12 @@ export function buildTools(backend, config) {
376
488
  const verdict = await classifyEmptyRead(path, 'read_file');
377
489
  if ('error' in verdict)
378
490
  return verdict;
379
- return {
491
+ return noted({
380
492
  path,
381
493
  content: sanitizeFileContent(verdict.content, path),
382
494
  ...(verdict.content === '' ? { empty: true } : {}),
383
495
  ...(verdict.note ? { note: verdict.note } : {}),
384
- };
496
+ });
385
497
  }
386
498
  if (content.length > MAX_READ_SIZE)
387
499
  return {
@@ -395,21 +507,32 @@ export function buildTools(backend, config) {
395
507
  : window;
396
508
  // A SECOND window of the same small file: hand back all of it (one window is a targeted read). X1 flash-m50 paged files that fit in one
397
509
  // call 13 times in a row, each window answered with "read it again with no offset/limit".
510
+ // The bound is the one that note states (SMALL_FILE_WHOLE_CHARS = MAX_READ_RETURN_CHARS): 24 KB left
511
+ // RealWorld's 24,935-byte openapi.yml paged nine times under "fits in ONE call" (flash-m67).
398
512
  const small = sliceLines(content, window);
399
513
  const pagedBefore = small?.truncated ? windowedReads.has(path) : false;
400
514
  if (small?.truncated)
401
515
  windowedReads.add(path);
402
- if (small?.truncated && pagedBefore && content.length <= SMALL_FILE_WHOLE_CHARS) {
403
- return {
516
+ // Once per version of the file: after the whole text went back, a later window is a
517
+ // targeted read and gets its window (X1 flash-m67: 25 firings, 8 byte-identical repeats).
518
+ const sentWhole = wholeReturned.get(path) === content;
519
+ if (small?.truncated &&
520
+ pagedBefore &&
521
+ !sentWhole &&
522
+ content.length <= SMALL_FILE_WHOLE_CHARS) {
523
+ wholeReturned.set(path, content);
524
+ return noted({
404
525
  path,
405
526
  content: sanitizeFileContent(content, path),
527
+ offset: 1,
528
+ lines: small.totalLines,
406
529
  totalLines: small.totalLines,
407
530
  note: `The whole file is returned (${Math.round(content.length / 1024)} KB, ${small.totalLines} lines): a file this small fits in one call, so it is not paged.`,
408
- };
531
+ });
409
532
  }
410
533
  const windowed = sliceLines(content, effectiveWindow);
411
534
  if (windowed) {
412
- return {
535
+ return noted({
413
536
  path,
414
537
  content: sanitizeFileContent(windowed.text, path),
415
538
  offset: windowed.offset,
@@ -422,18 +545,21 @@ export function buildTools(backend, config) {
422
545
  // exact next window so the executor does not have to compute it.
423
546
  ...(windowed.truncated
424
547
  ? {
425
- note: content.length <= MAX_READ_RETURN_CHARS
426
- ? `Lines ${windowed.offset}-${windowed.offset + windowed.lines - 1} of ` +
427
- `${windowed.totalLines}. The whole file is ${Math.round(content.length / 1024)} KB ` +
428
- `and fits in ONE call — read it again with no offset/limit instead of paging.`
429
- : `Lines ${windowed.offset}-${windowed.offset + windowed.lines - 1} of ` +
430
- `${windowed.totalLines}. Next window: offset ${windowed.offset + windowed.lines}, ` +
431
- `limit ${windowed.lines}.`,
548
+ note: sentWhole
549
+ ? `Lines ${windowed.offset}-${windowed.offset + windowed.lines - 1} of ${windowed.totalLines}; ` +
550
+ 'the whole file, unchanged, was returned earlier in this conversation.'
551
+ : content.length <= MAX_READ_RETURN_CHARS
552
+ ? `Lines ${windowed.offset}-${windowed.offset + windowed.lines - 1} of ` +
553
+ `${windowed.totalLines}. The whole file is ${Math.round(content.length / 1024)} KB ` +
554
+ `and fits in ONE call — read it again with no offset/limit instead of paging.`
555
+ : `Lines ${windowed.offset}-${windowed.offset + windowed.lines - 1} of ` +
556
+ `${windowed.totalLines}. Next window: offset ${windowed.offset + windowed.lines}, ` +
557
+ `limit ${windowed.lines}.`,
432
558
  }
433
559
  : {}),
434
- };
560
+ });
435
561
  }
436
- return { path, content: sanitizeFileContent(content, path) };
562
+ return noted({ path, content: sanitizeFileContent(content, path) });
437
563
  }
438
564
  catch (e) {
439
565
  // Backends (e.g. the docker provider's `cat`) already prefix "Failed to read <path>: ";
@@ -1366,6 +1492,33 @@ export function buildTools(backend, config) {
1366
1492
  // server-side costs no round trip and no context.
1367
1493
  async wait_for_task(input) {
1368
1494
  const taskId = typeof input.taskId === 'string' ? input.taskId.trim() : '';
1495
+ // Where a task writes its own output is process memory; the task is not.
1496
+ // An API process swapped mid-run (a release, a patch, a crash) leaves the
1497
+ // command running in the sandbox with nothing left that remembers the file
1498
+ // it redirects to — and then `wait_for_task` returns the task's near-empty
1499
+ // log as the command's whole result, which is the X1 flash-m48 loss all
1500
+ // over again (X1 flash-m63's +43:20 restart: the public walk's handle
1501
+ // outlived the process, its redirect target did not). The script the task
1502
+ // was started from is still on disk and carries the command, so read the
1503
+ // target back from there. The id is format-checked before any /tmp path is
1504
+ // built from it, exactly as the reads below are.
1505
+ if (!outputFileByTask.has(taskId) &&
1506
+ !outputFileProbed.has(taskId) &&
1507
+ /^mol-bg-[a-z0-9]+-[a-z0-9]+$/.test(taskId)) {
1508
+ outputFileProbed.add(taskId);
1509
+ try {
1510
+ const target = outputRedirectTarget(await backend.readFile(`/tmp/${taskId}.sh`));
1511
+ // A relative target was written against the command's own cwd, which
1512
+ // the handle does not carry; the project root is where a background
1513
+ // command runs unless the caller passed one.
1514
+ if (target)
1515
+ outputFileByTask.set(taskId, target.startsWith('/') ? target : `${root}/${target}`);
1516
+ }
1517
+ catch (_error) {
1518
+ // No script under that id: this process did not start the task, so
1519
+ // there is nothing to recover. The log stays the whole result.
1520
+ }
1521
+ }
1369
1522
  // A command that has been waited on for stuckAfterMs() in total without
1370
1523
  // exiting is not going to: on one run the executor waited five times,
1371
1524
  // 120 s each, on an e2e suite that could never start (no browser page
@@ -1447,16 +1600,44 @@ export function buildTools(backend, config) {
1447
1600
  const lastSize = logBytesByTask.get(taskId);
1448
1601
  if (grownTo !== null)
1449
1602
  logBytesByTask.set(taskId, grownTo);
1603
+ // Evidence of liveness, read the same way by every answer below: a log
1604
+ // that grew during THIS wait means the command is working. On the first
1605
+ // wait any output at all counts (there is no earlier size to compare).
1450
1606
  const stillWriting = grownTo !== null && (lastSize === undefined ? grownTo > 0 : grownTo > lastSize);
1607
+ // The log's own slice, read ONCE for whichever answer follows: it is what
1608
+ // the progress sentence is built from, and a wait that showed nothing was
1609
+ // waited blind (X1 flash-m50: 4m42s across two waits on an 11-minute run).
1610
+ // The slice leads with the failure lines, not the last bytes — a suite's
1611
+ // tail is its progress lines (X1 flash-m61: the last 3 000 bytes of a
1612
+ // 139-test walk were the skip list).
1613
+ const own = outputFileByTask.get(taskId);
1614
+ let soFar = '';
1615
+ try {
1616
+ const r = await backend.run(runningOutputSliceCommand(log, own), { timeout: 5_000 });
1617
+ soFar = r.stdout ?? '';
1618
+ }
1619
+ catch (_error) {
1620
+ // No slice: the notes still say how to peek, and the task id still
1621
+ // carries how long the command has been running.
1622
+ }
1623
+ const progress = waitProgressNote({
1624
+ taskId,
1625
+ slice: soFar,
1626
+ stillWriting,
1627
+ bytes: grownTo,
1628
+ ceilingMs: ceiling,
1629
+ });
1630
+ const sliceField = soFar.trim() ? { outputSoFar: sanitizeOutput(soFar) } : {};
1451
1631
  if (stillWriting && waitedBefore + waitedMs >= stuckAfterMs()) {
1452
1632
  waitedByTask.set(taskId, 0);
1453
1633
  return {
1454
1634
  taskId,
1455
1635
  status: 'running',
1456
1636
  waitedMs,
1637
+ ...sliceField,
1457
1638
  note: `Still running after ${Math.round((waitedBefore + waitedMs) / 1000)}s of waiting, and its ` +
1458
- `output is still growing (${grownTo} bytes) — slow, not stuck. Call wait_for_task again to ` +
1459
- `keep waiting; peek with exec_command: tail ${log}.`,
1639
+ `output is still growing (${grownTo} bytes) — slow, not stuck.${progress} Call wait_for_task ` +
1640
+ `again to keep waiting; peek with exec_command: tail ${log}.`,
1460
1641
  };
1461
1642
  }
1462
1643
  if (waitedBefore + waitedMs >= stuckAfterMs()) {
@@ -1470,26 +1651,12 @@ export function buildTools(backend, config) {
1470
1651
  `kill $(cat /tmp/${taskId}.pid) 2>/dev/null; pkill -f ${taskId}.sh) and move on; do not wait on it again.`,
1471
1652
  };
1472
1653
  }
1473
- // The output so far rides the result: a wait that shows nothing was waited blind
1474
- // (X1 flash-m50: 4m42s across two waits on an 11-minute run). The slice leads with the
1475
- // failure lines, not the last bytes — a suite's tail is its progress lines
1476
- // (X1 flash-m61: the last 3 000 bytes of a 139-test walk were the skip list).
1477
- const own = outputFileByTask.get(taskId);
1478
- let soFar;
1479
- try {
1480
- const r = await backend.run(runningOutputSliceCommand(log, own), { timeout: 5_000 });
1481
- soFar = r.stdout ?? '';
1482
- }
1483
- catch (_error) {
1484
- // No slice: the note still says how to peek.
1485
- soFar = '';
1486
- }
1487
1654
  return {
1488
1655
  taskId,
1489
1656
  status: 'running',
1490
1657
  waitedMs,
1491
- ...(soFar.trim() ? { outputSoFar: sanitizeOutput(soFar) } : {}),
1492
- note: `Still running after ${Math.round(waitedMs / 1000)}s. Call wait_for_task again to ` +
1658
+ ...sliceField,
1659
+ note: `Still running after ${Math.round(waitedMs / 1000)}s.${progress} Call wait_for_task again to ` +
1493
1660
  `keep waiting — its result carries the log output. ` +
1494
1661
  (soFar.trim()
1495
1662
  ? `outputSoFar is the failure lines so far (up to ${RUNNING_FAILURE_LINES}) and then the end of ` +
@@ -267,7 +267,11 @@ export declare const MAX_BATCH_READ_BYTES: number;
267
267
  * needs.
268
268
  */
269
269
  export declare const MAX_READ_RETURN_CHARS: number;
270
- /** A file this small is returned whole even when a window is asked for (paging it costs a call each). */
270
+ /**
271
+ * A file this small is returned whole on a second window of it (paging it costs a call each).
272
+ * The same bound as {@link MAX_READ_RETURN_CHARS}: the windowed-read note promises that a
273
+ * file under it "fits in ONE call", and a smaller bound here broke that promise (X1 flash-m67).
274
+ */
271
275
  export declare const SMALL_FILE_WHOLE_CHARS: number;
272
276
  /** Max search results. */
273
277
  export declare const MAX_SEARCH_RESULTS = 50;
@@ -1 +1 @@
1
- {"version":3,"file":"utilities.d.ts","sourceRoot":"","sources":["../src/utilities.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"utilities.d.ts","sourceRoot":"","sources":["../src/utilities.ts"],"names":[],"mappings":"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"}
package/dist/utilities.js CHANGED
@@ -345,6 +345,15 @@ export function streamingBackgroundCommand(command) {
345
345
  changes.push('Added `--reporter=list` so each test result is written to the log as it finishes ' +
346
346
  '(the config reporter may print nothing until the end).');
347
347
  }
348
+ // A diagnosis or a re-run (capped, last-failed, grepped, or named files) walks each failure
349
+ // once: the suite's own `retries` doubled every failure of X1 flash-m71's first whole run,
350
+ // which had dropped the `--retries=0` it used one command earlier. The uncapped proof run
351
+ // keeps the suite's retries.
352
+ const iterating = /--max-failures\b|--last-failed\b|(?:^|\s)(?:-g|--grep)\b|\S+\.(?:spec|test)\.[cm]?[jt]sx?(?::\d+)?\b/.test(lastStep);
353
+ if (finalPlaywrightRun && iterating && !/--retries\b/.test(lastStep)) {
354
+ out = `${out.trimEnd()} --retries=0`;
355
+ changes.push('Added `--retries=0`: a diagnosis or re-run walks each failure once; keep the suite’s retries for the proof run.');
356
+ }
348
357
  return { command: out, changes };
349
358
  }
350
359
  // ── Command blocking ──────────────────────────────────────────────────────────
@@ -577,8 +586,12 @@ export const MAX_BATCH_READ_BYTES = 200 * 1024;
577
586
  * needs.
578
587
  */
579
588
  export const MAX_READ_RETURN_CHARS = 80 * 1024;
580
- /** A file this small is returned whole even when a window is asked for (paging it costs a call each). */
581
- export const SMALL_FILE_WHOLE_CHARS = 24 * 1024;
589
+ /**
590
+ * A file this small is returned whole on a second window of it (paging it costs a call each).
591
+ * The same bound as {@link MAX_READ_RETURN_CHARS}: the windowed-read note promises that a
592
+ * file under it "fits in ONE call", and a smaller bound here broke that promise (X1 flash-m67).
593
+ */
594
+ export const SMALL_FILE_WHOLE_CHARS = MAX_READ_RETURN_CHARS;
582
595
  /** Max search results. */
583
596
  export const MAX_SEARCH_RESULTS = 50;
584
597
  /** Max find results. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@molecule/api-ai-tools",
3
- "version": "1.5.16",
3
+ "version": "1.5.18",
4
4
  "description": "Shared AI agent tools with backend abstraction for sandbox and local execution",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -31,7 +31,7 @@
31
31
  "@molecule/api-ai": "^1.0.1"
32
32
  },
33
33
  "devDependencies": {
34
- "@molecule/api-ai": "1.3.0",
34
+ "@molecule/api-ai": "1.3.1",
35
35
  "@types/node": "26.1.2",
36
36
  "esbuild": "0.28.2",
37
37
  "typescript": "6.0.3",