@molecule/api-ai-tools 1.6.0 → 1.6.4
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 +52 -7
- package/dist/tools.d.ts +18 -0
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +108 -34
- package/dist/utilities.d.ts +25 -6
- package/dist/utilities.d.ts.map +1 -1
- package/dist/utilities.js +181 -42
- package/package.json +2 -2
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-
|
|
6
|
+
Generated: 2026-10-16T16:49:27.760Z
|
|
7
7
|
-->
|
|
8
8
|
|
|
9
9
|
# @molecule/api-ai-tools
|
|
@@ -644,6 +644,27 @@ function buildAgentPrompt(ctx: PromptContext): string
|
|
|
644
644
|
|
|
645
645
|
**Returns:** Fully assembled system prompt text for the coding agent.
|
|
646
646
|
|
|
647
|
+
#### `taskStopCommand(backend, taskId)`
|
|
648
|
+
|
|
649
|
+
The command that stops a running background task, read from the sandbox's own pid files.
|
|
650
|
+
|
|
651
|
+
THE one builder for it, so every note that hands the executor a way to stop a run hands over the
|
|
652
|
+
same pid-only form: a `pkill -f <something>` matches the shell running it (its argv IS the
|
|
653
|
+
command), and `molecule-dev/api/src/ai/pattern-kill-guard.ts` holds exactly that — X1
|
|
654
|
+
xr-molecule-flash-m87's stuck note suggested `kill $(cat /tmp/<id>.pid) 2>/dev/null; pkill -f
|
|
655
|
+
<id>.sh`, which the platform's own guard then refused, and the same shape cost a refused call at
|
|
656
|
+
+21:57. The pid tree is the whole of what the pattern was for: the detached shell and every
|
|
657
|
+
process under it.
|
|
658
|
+
|
|
659
|
+
```typescript
|
|
660
|
+
function taskStopCommand(backend: ExecutionBackend, taskId: string): Promise<string>
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
- `backend` — The execution backend.
|
|
664
|
+
- `taskId` — The task's `mol-bg-…` handle.
|
|
665
|
+
|
|
666
|
+
**Returns:** The command to hand the executor — `kill <pids>` — falling back to the task's own pid file when the walk comes back empty (a sandbox without `pgrep`, or a pid file already gone, in which case the pid-file form is still the one the platform's guard names).
|
|
667
|
+
|
|
647
668
|
#### `moleculeDistReadNote(path)`
|
|
648
669
|
|
|
649
670
|
The steer attached to a SUCCESSFUL read of a package's built output. The ENOENT
|
|
@@ -1040,6 +1061,26 @@ function pathArgError(path: unknown, tool: string): string | null
|
|
|
1040
1061
|
|
|
1041
1062
|
**Returns:** An actionable error string, or null when `path` is a non-empty string.
|
|
1042
1063
|
|
|
1064
|
+
#### `emptyCallError(input, tool)`
|
|
1065
|
+
|
|
1066
|
+
The error for a file tool called with NO arguments at all — `edit_file {}`, the
|
|
1067
|
+
shape a truncated provider stream leaves behind.
|
|
1068
|
+
|
|
1069
|
+
The path message is the wrong reading of it: "requires a non-empty path
|
|
1070
|
+
argument" says the call was understood and the path was left out, so the model
|
|
1071
|
+
retries the call it just made instead of sending the whole call again (x0
|
|
1072
|
+
flash-m83 +29:32: `edit_file {}`, one round lost). An empty object and a
|
|
1073
|
+
missing `path` are different failures and deserve different sentences.
|
|
1074
|
+
|
|
1075
|
+
```typescript
|
|
1076
|
+
function emptyCallError(input: unknown, tool: string): string | null
|
|
1077
|
+
```
|
|
1078
|
+
|
|
1079
|
+
- `input` — The raw tool input.
|
|
1080
|
+
- `tool` — The tool name, for the message.
|
|
1081
|
+
|
|
1082
|
+
**Returns:** The truncation message, or null when the call carried any argument.
|
|
1083
|
+
|
|
1043
1084
|
#### `directoryReadHint(message, path)`
|
|
1044
1085
|
|
|
1045
1086
|
Detect a "read/edit targeted a directory, not a file" failure from a backend
|
|
@@ -1233,9 +1274,11 @@ The cause repeating among a running suite's failures, or null.
|
|
|
1233
1274
|
Playwright's `list` reporter prints one line per test as it finishes — its mark, its title
|
|
1234
1275
|
path and how long it ran — and defers each failure's error to the summary at the end, so
|
|
1235
1276
|
mid-run the only signature those lines carry is the duration. Where the log carries the
|
|
1236
|
-
failure bodies (a `line` reporter's output, or a finished run's summary) the
|
|
1237
|
-
|
|
1238
|
-
failure should take), so
|
|
1277
|
+
failure bodies (a `line` reporter's output, or a finished run's summary) the signature is the
|
|
1278
|
+
error itself — {@link failureMessage}: the error class plus the locator or the values that
|
|
1279
|
+
differ — which is stronger (it needs no assumption about how long a failure should take), so
|
|
1280
|
+
the message is looked for first and the duration is then left with the tests whose bodies the
|
|
1281
|
+
log does NOT carry.
|
|
1239
1282
|
|
|
1240
1283
|
Either way the point is the same: a suite whose failures repeat one cause spends its whole
|
|
1241
1284
|
runtime proving that one cause, and the executor watching it can be told so at the third
|
|
@@ -1290,9 +1333,11 @@ after another with no statement of which of them share a cause, so a run of 19 f
|
|
|
1290
1333
|
bodies to read; X1 flash-m83 (2026-10-16) spent three deploys (6.7 minutes) each fixing a
|
|
1291
1334
|
single cause, two of them found by runs stopped before they walked the rest.
|
|
1292
1335
|
|
|
1293
|
-
The signature is the
|
|
1294
|
-
|
|
1295
|
-
|
|
1336
|
+
The signature is the cause itself where the log carries the failures' bodies (a finished run,
|
|
1337
|
+
or a `line` reporter) — {@link failureMessage}: the error class plus the locator or the values
|
|
1338
|
+
that differ — and how long they took where it does not (a still-running `list` log, which is
|
|
1339
|
+
why a duration group is labelled as a duration and not as a proven cause). A test the log
|
|
1340
|
+
printed twice, or failed twice, is one test.
|
|
1296
1341
|
|
|
1297
1342
|
```typescript
|
|
1298
1343
|
function failuresByCause(log: string): FailureCause[]
|
package/dist/tools.d.ts
CHANGED
|
@@ -7,6 +7,24 @@
|
|
|
7
7
|
* @module
|
|
8
8
|
*/
|
|
9
9
|
import type { AITool } from '@molecule/api-ai';
|
|
10
|
+
/**
|
|
11
|
+
* The command that stops a running background task, read from the sandbox's own pid files.
|
|
12
|
+
*
|
|
13
|
+
* THE one builder for it, so every note that hands the executor a way to stop a run hands over the
|
|
14
|
+
* same pid-only form: a `pkill -f <something>` matches the shell running it (its argv IS the
|
|
15
|
+
* command), and `molecule-dev/api/src/ai/pattern-kill-guard.ts` holds exactly that — X1
|
|
16
|
+
* xr-molecule-flash-m87's stuck note suggested `kill $(cat /tmp/<id>.pid) 2>/dev/null; pkill -f
|
|
17
|
+
* <id>.sh`, which the platform's own guard then refused, and the same shape cost a refused call at
|
|
18
|
+
* +21:57. The pid tree is the whole of what the pattern was for: the detached shell and every
|
|
19
|
+
* process under it.
|
|
20
|
+
*
|
|
21
|
+
* @param backend - The execution backend.
|
|
22
|
+
* @param taskId - The task's `mol-bg-…` handle.
|
|
23
|
+
* @returns The command to hand the executor — `kill <pids>` — falling back to the task's own pid
|
|
24
|
+
* file when the walk comes back empty (a sandbox without `pgrep`, or a pid file already gone,
|
|
25
|
+
* in which case the pid-file form is still the one the platform's guard names).
|
|
26
|
+
*/
|
|
27
|
+
export declare function taskStopCommand(backend: ExecutionBackend, taskId: string): Promise<string>;
|
|
10
28
|
import type { ExecutionBackend, ToolBuildConfig } from './types.js';
|
|
11
29
|
/**
|
|
12
30
|
* The steer attached to a SUCCESSFUL read of a package's built output. The ENOENT
|
package/dist/tools.d.ts.map
CHANGED
|
@@ -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;
|
|
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;AAsE9C;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,eAAe,CAAC,OAAO,EAAE,gBAAgB,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAahG;AAwHD,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAoEnE;;;;;;;;;;;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,CA65DxF"}
|
package/dist/tools.js
CHANGED
|
@@ -63,13 +63,21 @@ const taskPidsCommand = (taskId) => `R=$(cat /tmp/${taskId}.pid 2>/dev/null); `
|
|
|
63
63
|
/**
|
|
64
64
|
* The command that stops a running background task, read from the sandbox's own pid files.
|
|
65
65
|
*
|
|
66
|
+
* THE one builder for it, so every note that hands the executor a way to stop a run hands over the
|
|
67
|
+
* same pid-only form: a `pkill -f <something>` matches the shell running it (its argv IS the
|
|
68
|
+
* command), and `molecule-dev/api/src/ai/pattern-kill-guard.ts` holds exactly that — X1
|
|
69
|
+
* xr-molecule-flash-m87's stuck note suggested `kill $(cat /tmp/<id>.pid) 2>/dev/null; pkill -f
|
|
70
|
+
* <id>.sh`, which the platform's own guard then refused, and the same shape cost a refused call at
|
|
71
|
+
* +21:57. The pid tree is the whole of what the pattern was for: the detached shell and every
|
|
72
|
+
* process under it.
|
|
73
|
+
*
|
|
66
74
|
* @param backend - The execution backend.
|
|
67
75
|
* @param taskId - The task's `mol-bg-…` handle.
|
|
68
76
|
* @returns The command to hand the executor — `kill <pids>` — falling back to the task's own pid
|
|
69
77
|
* file when the walk comes back empty (a sandbox without `pgrep`, or a pid file already gone,
|
|
70
78
|
* in which case the pid-file form is still the one the platform's guard names).
|
|
71
79
|
*/
|
|
72
|
-
async function taskStopCommand(backend, taskId) {
|
|
80
|
+
export async function taskStopCommand(backend, taskId) {
|
|
73
81
|
const fallback = `kill $(cat /tmp/${taskId}.pid)`;
|
|
74
82
|
try {
|
|
75
83
|
const r = await backend.run(taskPidsCommand(taskId), { timeout: 10_000 });
|
|
@@ -122,7 +130,8 @@ function logProgress(log) {
|
|
|
122
130
|
/**
|
|
123
131
|
* What a wait knows about the command BESIDES "it has not exited": how long it
|
|
124
132
|
* has been running, whether its output is still growing, how far its own log
|
|
125
|
-
* says it has got, and how much longer that implies
|
|
133
|
+
* says it has got, and how much longer that implies — one fact per clause, built
|
|
134
|
+
* here so the note carrying them cannot state one of them twice.
|
|
126
135
|
*
|
|
127
136
|
* X1 flash-m62 (2026-10-16) spent three 290 s waits on one 8.3-minute suite and
|
|
128
137
|
* every answer read only "Still running after 280s": the run could not tell a
|
|
@@ -131,42 +140,78 @@ function logProgress(log) {
|
|
|
131
140
|
* waiting. The elapsed time comes from the task id, the progress from the log
|
|
132
141
|
* this wait already read, and the time left is the rate so far, said as such.
|
|
133
142
|
*
|
|
143
|
+
* X1 flash-m87 §+47:20 (2026-10-16): that suite's answer read "Still running
|
|
144
|
+
* after 342s of waiting, and its output is still growing (71839 bytes) — slow,
|
|
145
|
+
* not stuck." and then this sentence, which opens with the same growth clause
|
|
146
|
+
* and gives the same elapsed time in its other format — one answer, two facts,
|
|
147
|
+
* each said twice. The clauses ARE the facts, and {@link runningNote} is the
|
|
148
|
+
* only place they become a sentence, so a lead the caller writes itself cannot
|
|
149
|
+
* repeat one (the way `deployOwedNote` returns one sentence rather than a join).
|
|
150
|
+
*
|
|
134
151
|
* @param o - The wait's facts: `taskId`, the `slice` of the log it read,
|
|
135
152
|
* `stillWriting` (the log grew during this wait), `bytes` (the log's size, null
|
|
136
|
-
* when unreadable),
|
|
137
|
-
*
|
|
153
|
+
* when unreadable), `ceilingMs` (this call's ceiling, for "one more wait"), and
|
|
154
|
+
* `waitedMs` (this call's own wait, the elapsed figure when the id carries no
|
|
155
|
+
* start stamp).
|
|
156
|
+
* @returns The clauses in reading order. The first states the elapsed time, so a
|
|
157
|
+
* sentence built from them always says something.
|
|
138
158
|
*/
|
|
139
|
-
function
|
|
159
|
+
function waitProgressClauses(o) {
|
|
140
160
|
const startedAt = taskStartedAt(o.taskId);
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
161
|
+
const sinceStart = startedAt === null ? null : Math.max(0, Date.now() - startedAt);
|
|
162
|
+
// A task id this process minted always carries its start stamp; a synthetic one
|
|
163
|
+
// does not, and the time this call spent waiting is then the floor for how long
|
|
164
|
+
// the command has been running — it ran throughout that wait.
|
|
165
|
+
const clauses = [
|
|
166
|
+
sinceStart === null
|
|
167
|
+
? `it has been running at least ${durationText(o.waitedMs)}`
|
|
168
|
+
: `it has been running ${durationText(sinceStart)}`,
|
|
169
|
+
];
|
|
145
170
|
if (o.stillWriting)
|
|
146
|
-
|
|
171
|
+
clauses.push(`its output is still growing (${o.bytes} bytes) — slow, not stuck`);
|
|
147
172
|
else if (o.bytes !== null)
|
|
148
|
-
|
|
173
|
+
clauses.push(`its log has not grown during this wait (${o.bytes} bytes)`);
|
|
149
174
|
const counter = logProgress(o.slice);
|
|
150
175
|
if (counter) {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
176
|
+
clauses.push(`its log is at ${counter.done} of ${counter.total}`);
|
|
177
|
+
// The rate is the task's own elapsed time, never this call's wait: with no
|
|
178
|
+
// start stamp, "34 tests in the 290 s I just waited" would be reported as the
|
|
179
|
+
// speed of a suite that has been walking for ten minutes.
|
|
180
|
+
if (sinceStart !== null && counter.done > 0 && counter.done < counter.total) {
|
|
181
|
+
const leftMs = Math.round((sinceStart * (counter.total - counter.done)) / counter.done);
|
|
182
|
+
clauses.push(leftMs <= o.ceilingMs
|
|
155
183
|
? `about ${durationText(leftMs)} left at that rate — one more wait covers it`
|
|
156
184
|
: `about ${durationText(leftMs)} left at that rate (${Math.ceil(leftMs / o.ceilingMs)} more waits)`);
|
|
157
185
|
}
|
|
158
186
|
}
|
|
159
|
-
|
|
187
|
+
return clauses;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* One sentence from clauses: joined by "; ", capitalised and punctuated once.
|
|
191
|
+
*
|
|
192
|
+
* @param clauses - The facts to state, each in its own clause.
|
|
193
|
+
* @returns The sentence, or '' when there is nothing to say.
|
|
194
|
+
*/
|
|
195
|
+
function runningNote(clauses) {
|
|
196
|
+
if (!clauses.length)
|
|
160
197
|
return '';
|
|
161
|
-
const
|
|
162
|
-
return
|
|
198
|
+
const joined = clauses.join('; ');
|
|
199
|
+
return `${joined[0].toUpperCase()}${joined.slice(1)}.`;
|
|
163
200
|
}
|
|
164
|
-
import { checkBlockedCommand, declaredBudgetSeconds, DEFAULT_SEARCH_EXCLUDED_DIRS, directoryReadHint, failureCausesNote, findPatternArgs, fusedLine, htmlOutline, isEnvFilePath, isOutsideRoot, isPlaywrightRunCommand, isValidGlob, linesWithinSerialized, MAX_BATCH_READ_FILES, MAX_FIND_RESULTS, MAX_HTML_INLINE_CHARS, MAX_OUTPUT_SIZE, MAX_READ_CALL_CHARS, MAX_READ_RETURN_CHARS, MAX_READ_SIZE, MAX_SEARCH_CONTEXT_LINES, MAX_SEARCH_RESULTS, MAX_WRITE_SIZE, outputIsWithheldUntilExit, outputRedirectTarget, parseCheckCommand, parseCheckProblem, pathArgError, printsOnlySource, READ_RESULT_ENVELOPE_CHARS, redactSecrets, redactSecretsInCode, repeatingFailure, repeatingFailureNote, resolvePath, RUNNING_FAILURE_LINES, runningCauseCommand, runningOutputSliceCommand, serializedChars, shellQuote, sliceLines, SMALL_FILE_WHOLE_CHARS, streamingBackgroundCommand, stripControlChars, suiteRunPriorityLaunch, truncateMiddle, universalCause, whitespaceTolerantReplace, } from './utilities.js';
|
|
201
|
+
import { checkBlockedCommand, declaredBudgetSeconds, DEFAULT_SEARCH_EXCLUDED_DIRS, directoryReadHint, emptyCallError, failureCausesNote, findPatternArgs, fusedLine, htmlOutline, isEnvFilePath, isOutsideRoot, isPlaywrightRunCommand, isValidGlob, linesWithinSerialized, MAX_BATCH_READ_FILES, MAX_FIND_RESULTS, MAX_HTML_INLINE_CHARS, MAX_OUTPUT_SIZE, MAX_READ_CALL_CHARS, MAX_READ_RETURN_CHARS, MAX_READ_SIZE, MAX_SEARCH_CONTEXT_LINES, MAX_SEARCH_RESULTS, MAX_WRITE_SIZE, outputIsWithheldUntilExit, outputRedirectTarget, parseCheckCommand, parseCheckProblem, pathArgError, printsOnlySource, READ_RESULT_ENVELOPE_CHARS, redactSecrets, redactSecretsInCode, repeatingFailure, repeatingFailureNote, resolvePath, RUNNING_FAILURE_LINES, runningCauseCommand, runningOutputSliceCommand, serializedChars, shellQuote, sliceLines, SMALL_FILE_WHOLE_CHARS, streamingBackgroundCommand, stripControlChars, suiteRunPriorityLaunch, truncateMiddle, universalCause, whitespaceTolerantReplace, } from './utilities.js';
|
|
165
202
|
/**
|
|
166
203
|
* How long exec_command waits for a command it backgrounded only because the
|
|
167
204
|
* command asked for more than the tool's ceiling, before handing back a task id.
|
|
168
205
|
*/
|
|
169
206
|
const OVER_CEILING_GRACE_MS = 45_000;
|
|
207
|
+
/**
|
|
208
|
+
* The share of its budget a command must use before the "you asked for more than
|
|
209
|
+
* this tool's ceiling" note is worth attaching. Below it the clamp changed nothing
|
|
210
|
+
* about the run, and the line read as a warning about work that had already
|
|
211
|
+
* finished (xr-molecule-flash-m83 +53:11: a Hurl run of 25 s against a 290 s
|
|
212
|
+
* ceiling carried the note).
|
|
213
|
+
*/
|
|
214
|
+
const CEILING_NOTE_MIN_USED = 0.5;
|
|
170
215
|
/**
|
|
171
216
|
* The steer attached to a SUCCESSFUL read of a package's built output. The ENOENT
|
|
172
217
|
* redirect below fires only when the read FAILS, so a model that knows the hoisted
|
|
@@ -733,6 +778,9 @@ export function buildTools(backend, config) {
|
|
|
733
778
|
}
|
|
734
779
|
},
|
|
735
780
|
async read_file(input) {
|
|
781
|
+
const emptyErr = emptyCallError(input, 'read_file');
|
|
782
|
+
if (emptyErr)
|
|
783
|
+
return { error: emptyErr };
|
|
736
784
|
// Several paths in ONE call. The executor issues one tool call per model
|
|
737
785
|
// round-trip 89% of the time (X0: 464 assistant messages, 515 tool calls,
|
|
738
786
|
// a ~10s mean gap), so a turn's wall clock IS its round-trip count — and
|
|
@@ -833,6 +881,9 @@ export function buildTools(backend, config) {
|
|
|
833
881
|
return readOneFile(input.path, input);
|
|
834
882
|
},
|
|
835
883
|
async write_file(input) {
|
|
884
|
+
const emptyErr = emptyCallError(input, 'write_file');
|
|
885
|
+
if (emptyErr)
|
|
886
|
+
return { error: emptyErr };
|
|
836
887
|
const argErr = pathArgError(input.path, 'write_file');
|
|
837
888
|
if (argErr)
|
|
838
889
|
return { error: argErr };
|
|
@@ -879,6 +930,9 @@ export function buildTools(backend, config) {
|
|
|
879
930
|
}
|
|
880
931
|
},
|
|
881
932
|
async edit_file(input) {
|
|
933
|
+
const emptyErr = emptyCallError(input, 'edit_file');
|
|
934
|
+
if (emptyErr)
|
|
935
|
+
return { error: emptyErr };
|
|
882
936
|
const argErr = pathArgError(input.path, 'edit_file');
|
|
883
937
|
if (argErr)
|
|
884
938
|
return { error: argErr };
|
|
@@ -1544,16 +1598,28 @@ export function buildTools(backend, config) {
|
|
|
1544
1598
|
const effectiveBudgetMs = Number.isFinite(requestedMs) && requestedMs > 0 && commandBudgetMs
|
|
1545
1599
|
? Math.min(requestedMs, commandBudgetMs)
|
|
1546
1600
|
: commandBudgetMs;
|
|
1547
|
-
const
|
|
1548
|
-
? `You asked for ${Math.round(requestedMs / 1000)}s; this tool's ceiling is ${budgetSeconds}s. ` +
|
|
1549
|
-
'Split the work into smaller commands (one test file, one build step) rather than ' +
|
|
1550
|
-
're-running the same long one.'
|
|
1551
|
-
: null;
|
|
1601
|
+
const startedAt = Date.now();
|
|
1552
1602
|
const result = await backend.run(command, {
|
|
1553
1603
|
cwd,
|
|
1554
1604
|
timeout: execTimeoutMs,
|
|
1555
1605
|
...(effectiveBudgetMs ? { budgetMs: effectiveBudgetMs } : {}),
|
|
1556
1606
|
});
|
|
1607
|
+
const elapsedMs = Date.now() - startedAt;
|
|
1608
|
+
// The clamped ask is only worth a line when the clamp was felt: a command
|
|
1609
|
+
// that asked for 600 s and finished in 25 s says nothing about splitting
|
|
1610
|
+
// work, and the note read as a warning about a run that had already
|
|
1611
|
+
// finished cleanly (xr-molecule-flash-m83 +53:11 — a Hurl run, 25 s).
|
|
1612
|
+
// Half the budget is the line; below it no smaller unit would have been
|
|
1613
|
+
// chosen any differently. A run that actually hit the ceiling is answered
|
|
1614
|
+
// by the exit-124 message below, not by this note.
|
|
1615
|
+
const ceilingNote = wantsMore &&
|
|
1616
|
+
budgetSeconds &&
|
|
1617
|
+
effectiveBudgetMs &&
|
|
1618
|
+
elapsedMs >= effectiveBudgetMs * CEILING_NOTE_MIN_USED
|
|
1619
|
+
? `You asked for ${Math.round(requestedMs / 1000)}s; this tool's ceiling is ${budgetSeconds}s. ` +
|
|
1620
|
+
'Split the work into smaller commands (one test file, one build step) rather than ' +
|
|
1621
|
+
're-running the same long one.'
|
|
1622
|
+
: null;
|
|
1557
1623
|
// truncateMiddle (not truncate): a failing build/test/migration puts its
|
|
1558
1624
|
// error at the TAIL, so keep the head AND the tail — head-only truncation
|
|
1559
1625
|
// strands the executor with passing progress and no failure reason.
|
|
@@ -1710,8 +1776,8 @@ export function buildTools(backend, config) {
|
|
|
1710
1776
|
status: 'stuck',
|
|
1711
1777
|
waitedMs: waitedBefore,
|
|
1712
1778
|
note: `You have already waited ${Math.round(waitedBefore / 1000)}s on this command and it ` +
|
|
1713
|
-
`has not exited — it is stuck, not slow. Kill it (exec_command:
|
|
1714
|
-
|
|
1779
|
+
`has not exited — it is stuck, not slow. Kill it (\`exec_command: ` +
|
|
1780
|
+
`${await taskStopCommand(backend, taskId)}\`) and move on; do not wait on it again.`,
|
|
1715
1781
|
};
|
|
1716
1782
|
}
|
|
1717
1783
|
// The id names files under /tmp; only an id this tool minted may be used
|
|
@@ -1824,13 +1890,19 @@ export function buildTools(backend, config) {
|
|
|
1824
1890
|
// No slice: the notes still say how to peek, and the task id still
|
|
1825
1891
|
// carries how long the command has been running.
|
|
1826
1892
|
}
|
|
1827
|
-
|
|
1893
|
+
// ONE sentence of facts, each said once, then what to do next: the lead
|
|
1894
|
+
// this branch used to write ("Still running after 342s of waiting, and its
|
|
1895
|
+
// output is still growing (71839 bytes) — slow, not stuck") restated the
|
|
1896
|
+
// elapsed time and the growth clause these clauses already hold — the same
|
|
1897
|
+
// sentence, twice (X1 flash-m87 §+47:20).
|
|
1898
|
+
const progress = runningNote(waitProgressClauses({
|
|
1828
1899
|
taskId,
|
|
1829
1900
|
slice: soFar,
|
|
1830
1901
|
stillWriting,
|
|
1831
1902
|
bytes: grownTo,
|
|
1832
1903
|
ceilingMs: ceiling,
|
|
1833
|
-
|
|
1904
|
+
waitedMs: waitedBefore + waitedMs,
|
|
1905
|
+
}));
|
|
1834
1906
|
const sliceField = soFar.trim() ? { outputSoFar: sanitizeOutput(soFar) } : {};
|
|
1835
1907
|
if (stillWriting && waitedBefore + waitedMs >= stuckAfterMs()) {
|
|
1836
1908
|
waitedByTask.set(taskId, 0);
|
|
@@ -1839,9 +1911,8 @@ export function buildTools(backend, config) {
|
|
|
1839
1911
|
status: 'running',
|
|
1840
1912
|
waitedMs,
|
|
1841
1913
|
...sliceField,
|
|
1842
|
-
note:
|
|
1843
|
-
`
|
|
1844
|
-
`again to keep waiting; peek with exec_command: tail ${log}.`,
|
|
1914
|
+
note: `${progress} Call wait_for_task again to keep waiting; peek with exec_command: ` +
|
|
1915
|
+
`tail ${log}.`,
|
|
1845
1916
|
};
|
|
1846
1917
|
}
|
|
1847
1918
|
if (waitedBefore + waitedMs >= stuckAfterMs()) {
|
|
@@ -1851,8 +1922,8 @@ export function buildTools(backend, config) {
|
|
|
1851
1922
|
waitedMs: waitedBefore + waitedMs,
|
|
1852
1923
|
note: `${Math.round((waitedBefore + waitedMs) / 1000)}s of waiting and it has not exited — it is ` +
|
|
1853
1924
|
`stuck, not slow. See what it printed with exec_command (tail /tmp/${taskId}.log — ` +
|
|
1854
|
-
`read_file cannot reach /tmp), kill it (exec_command: ` +
|
|
1855
|
-
|
|
1925
|
+
`read_file cannot reach /tmp), kill it (\`exec_command: ` +
|
|
1926
|
+
`${await taskStopCommand(backend, taskId)}\`) and move on; do not wait on it again.`,
|
|
1856
1927
|
};
|
|
1857
1928
|
}
|
|
1858
1929
|
return {
|
|
@@ -1860,7 +1931,10 @@ export function buildTools(backend, config) {
|
|
|
1860
1931
|
status: 'running',
|
|
1861
1932
|
waitedMs,
|
|
1862
1933
|
...sliceField,
|
|
1863
|
-
|
|
1934
|
+
// No "Still running after Ns" lead here either: the clauses already say
|
|
1935
|
+
// how long it has been running, and the lead said it a second time in
|
|
1936
|
+
// the other format (X1 flash-m87 §+47:20).
|
|
1937
|
+
note: `${progress} Call wait_for_task again to ` +
|
|
1864
1938
|
`keep waiting — its result carries the log output. ` +
|
|
1865
1939
|
(soFar.trim()
|
|
1866
1940
|
? `outputSoFar is the failure lines so far (up to ${RUNNING_FAILURE_LINES}) and then the end of ` +
|
package/dist/utilities.d.ts
CHANGED
|
@@ -286,6 +286,21 @@ export declare function findPatternArgs(pattern: string): string;
|
|
|
286
286
|
* @returns An actionable error string, or null when `path` is a non-empty string.
|
|
287
287
|
*/
|
|
288
288
|
export declare function pathArgError(path: unknown, tool: string): string | null;
|
|
289
|
+
/**
|
|
290
|
+
* The error for a file tool called with NO arguments at all — `edit_file {}`, the
|
|
291
|
+
* shape a truncated provider stream leaves behind.
|
|
292
|
+
*
|
|
293
|
+
* The path message is the wrong reading of it: "requires a non-empty path
|
|
294
|
+
* argument" says the call was understood and the path was left out, so the model
|
|
295
|
+
* retries the call it just made instead of sending the whole call again (x0
|
|
296
|
+
* flash-m83 +29:32: `edit_file {}`, one round lost). An empty object and a
|
|
297
|
+
* missing `path` are different failures and deserve different sentences.
|
|
298
|
+
*
|
|
299
|
+
* @param input - The raw tool input.
|
|
300
|
+
* @param tool - The tool name, for the message.
|
|
301
|
+
* @returns The truncation message, or null when the call carried any argument.
|
|
302
|
+
*/
|
|
303
|
+
export declare function emptyCallError(input: unknown, tool: string): string | null;
|
|
289
304
|
/**
|
|
290
305
|
* Detect a "read/edit targeted a directory, not a file" failure from a backend
|
|
291
306
|
* error message (local fs `EISDIR` or the sandbox's `cat: X: Is a directory`),
|
|
@@ -573,9 +588,11 @@ export interface RepeatingFailure {
|
|
|
573
588
|
* Playwright's `list` reporter prints one line per test as it finishes — its mark, its title
|
|
574
589
|
* path and how long it ran — and defers each failure's error to the summary at the end, so
|
|
575
590
|
* mid-run the only signature those lines carry is the duration. Where the log carries the
|
|
576
|
-
* failure bodies (a `line` reporter's output, or a finished run's summary) the
|
|
577
|
-
*
|
|
578
|
-
* failure should take), so
|
|
591
|
+
* failure bodies (a `line` reporter's output, or a finished run's summary) the signature is the
|
|
592
|
+
* error itself — {@link failureMessage}: the error class plus the locator or the values that
|
|
593
|
+
* differ — which is stronger (it needs no assumption about how long a failure should take), so
|
|
594
|
+
* the message is looked for first and the duration is then left with the tests whose bodies the
|
|
595
|
+
* log does NOT carry.
|
|
579
596
|
*
|
|
580
597
|
* Either way the point is the same: a suite whose failures repeat one cause spends its whole
|
|
581
598
|
* runtime proving that one cause, and the executor watching it can be told so at the third
|
|
@@ -652,9 +669,11 @@ export interface FailureCause {
|
|
|
652
669
|
* bodies to read; X1 flash-m83 (2026-10-16) spent three deploys (6.7 minutes) each fixing a
|
|
653
670
|
* single cause, two of them found by runs stopped before they walked the rest.
|
|
654
671
|
*
|
|
655
|
-
* The signature is the
|
|
656
|
-
*
|
|
657
|
-
*
|
|
672
|
+
* The signature is the cause itself where the log carries the failures' bodies (a finished run,
|
|
673
|
+
* or a `line` reporter) — {@link failureMessage}: the error class plus the locator or the values
|
|
674
|
+
* that differ — and how long they took where it does not (a still-running `list` log, which is
|
|
675
|
+
* why a duration group is labelled as a duration and not as a proven cause). A test the log
|
|
676
|
+
* printed twice, or failed twice, is one test.
|
|
658
677
|
*
|
|
659
678
|
* @param log - The log of a finished or running run.
|
|
660
679
|
* @returns The causes, the one with the most tests first.
|
package/dist/utilities.d.ts.map
CHANGED
|
@@ -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
|
@@ -730,6 +730,30 @@ export function pathArgError(path, tool) {
|
|
|
730
730
|
return `${tool} requires a non-empty "path" argument (a file path relative to the project root, e.g. "api/src/handlers/index.ts").`;
|
|
731
731
|
return null;
|
|
732
732
|
}
|
|
733
|
+
/**
|
|
734
|
+
* The error for a file tool called with NO arguments at all — `edit_file {}`, the
|
|
735
|
+
* shape a truncated provider stream leaves behind.
|
|
736
|
+
*
|
|
737
|
+
* The path message is the wrong reading of it: "requires a non-empty path
|
|
738
|
+
* argument" says the call was understood and the path was left out, so the model
|
|
739
|
+
* retries the call it just made instead of sending the whole call again (x0
|
|
740
|
+
* flash-m83 +29:32: `edit_file {}`, one round lost). An empty object and a
|
|
741
|
+
* missing `path` are different failures and deserve different sentences.
|
|
742
|
+
*
|
|
743
|
+
* @param input - The raw tool input.
|
|
744
|
+
* @param tool - The tool name, for the message.
|
|
745
|
+
* @returns The truncation message, or null when the call carried any argument.
|
|
746
|
+
*/
|
|
747
|
+
export function emptyCallError(input, tool) {
|
|
748
|
+
const empty = input === undefined ||
|
|
749
|
+
input === null ||
|
|
750
|
+
(typeof input === 'object' &&
|
|
751
|
+
!Array.isArray(input) &&
|
|
752
|
+
Object.keys(input).length === 0);
|
|
753
|
+
return empty
|
|
754
|
+
? `${tool} arrived with no arguments at all — the call was cut short, usually a truncated response. Send the whole call again.`
|
|
755
|
+
: null;
|
|
756
|
+
}
|
|
733
757
|
/**
|
|
734
758
|
* Detect a "read/edit targeted a directory, not a file" failure from a backend
|
|
735
759
|
* error message (local fs `EISDIR` or the sandbox's `cat: X: Is a directory`),
|
|
@@ -1259,23 +1283,106 @@ function failedTests(log) {
|
|
|
1259
1283
|
* while running "beforeEach" hook.`), which changes with the hook; the thrown error below it is
|
|
1260
1284
|
* what repeats verbatim, so it is preferred.
|
|
1261
1285
|
*
|
|
1262
|
-
* @param
|
|
1286
|
+
* @param body - The block's lines, from {@link failureBody}.
|
|
1263
1287
|
* @returns The error line, or null when the block has no body.
|
|
1264
1288
|
*/
|
|
1265
|
-
function
|
|
1266
|
-
const
|
|
1289
|
+
function failureErrorLine(body) {
|
|
1290
|
+
const thrown = body.find((l) => /^[A-Za-z_$][\w.$]*(?:Error|Exception)\b/.test(l));
|
|
1291
|
+
return thrown ?? body[0] ?? null;
|
|
1292
|
+
}
|
|
1293
|
+
/**
|
|
1294
|
+
* A failure block's own words, one per line: its body without the runner's progress lines (the
|
|
1295
|
+
* `list` reporter interleaves them with the `line` reporter's blocks), without a code frame or an
|
|
1296
|
+
* `at …` line (where the error happened, not what it said), and without the report's file list.
|
|
1297
|
+
*
|
|
1298
|
+
* @param block - One block from {@link failureBlocks}.
|
|
1299
|
+
* @returns The body's lines, ANSI stripped and trimmed.
|
|
1300
|
+
*/
|
|
1301
|
+
function failureBody(block) {
|
|
1302
|
+
return block
|
|
1267
1303
|
.split('\n')
|
|
1268
1304
|
.slice(1)
|
|
1269
1305
|
.map((l) => l.replace(ANSI_SGR, '').trim())
|
|
1270
|
-
// Progress lines the `list` reporter interleaves with the `line` reporter's blocks are not
|
|
1271
|
-
// the block's own words, and a code frame or an `at …` line is where the error happened, not
|
|
1272
|
-
// what it said.
|
|
1273
1306
|
.filter((l) => l &&
|
|
1274
1307
|
!LIST_RESULT_LINE.test(l) &&
|
|
1275
1308
|
!/^(?:at\s|\d+ \||>\s*\d+ \||\|)/.test(l) &&
|
|
1276
1309
|
!/^Error Context:/.test(l));
|
|
1277
|
-
|
|
1278
|
-
|
|
1310
|
+
}
|
|
1311
|
+
/**
|
|
1312
|
+
* What a failure block says the failure is ABOUT, when its error line does not already say it:
|
|
1313
|
+
* the locator it waited for, the navigation it waited on, or the locator and the expected/received
|
|
1314
|
+
* pair of a failed assertion.
|
|
1315
|
+
*
|
|
1316
|
+
* Without this, every `TimeoutError: page.press: Timeout 5000ms exceeded.` in a log is one
|
|
1317
|
+
* message, because that line names the ACTION (`page.press`) and never its subject. X1 flash-m84
|
|
1318
|
+
* (2026-10-16) had four such failures in one run — one waiting on
|
|
1319
|
+
* `button:has-text("Publish Article")`, one on `.article-preview h1`, one on
|
|
1320
|
+
* `.article-preview').first().locator('.author')`, one on a generated `h1:has-text("Test
|
|
1321
|
+
* Article …")` — and the run's summary called them two causes while its mid-run note called them
|
|
1322
|
+
* one. The subject is what separates causes; it is in the block, and nothing else in the block
|
|
1323
|
+
* carries it.
|
|
1324
|
+
*
|
|
1325
|
+
* @param body - The block's lines, from {@link failureBody}.
|
|
1326
|
+
* @returns The detail line, or null when the block names none.
|
|
1327
|
+
*/
|
|
1328
|
+
function failureDetail(body) {
|
|
1329
|
+
const waited = body.find((l) => /^-?\s*waiting for (?:locator\(|navigation|element|selector)/.test(l));
|
|
1330
|
+
if (waited !== undefined)
|
|
1331
|
+
return waited.replace(/^-\s*/, '');
|
|
1332
|
+
// A failed assertion names its locator and the two values that differ.
|
|
1333
|
+
const locator = body.find((l) => /^Locator:\s/.test(l));
|
|
1334
|
+
if (locator === undefined)
|
|
1335
|
+
return null;
|
|
1336
|
+
const parts = [
|
|
1337
|
+
locator,
|
|
1338
|
+
body.find((l) => /^Expected\b/.test(l)),
|
|
1339
|
+
body.find((l) => /^Received\b/.test(l)),
|
|
1340
|
+
];
|
|
1341
|
+
return parts.filter((p) => p !== undefined).join(' · ');
|
|
1342
|
+
}
|
|
1343
|
+
/**
|
|
1344
|
+
* A cause signature with the values that differ between two tests of ONE cause replaced by `<n>`:
|
|
1345
|
+
* a duration, a generated id or timestamp, a host's port.
|
|
1346
|
+
*
|
|
1347
|
+
* The duration is the one that matters. A suite that sets `actionTimeout: 5_000` — the X1
|
|
1348
|
+
* realworld suite does — spends `5 s` plus teardown on every "the locator never appeared"
|
|
1349
|
+
* failure, so `~7 s` is a property of the CONFIG, not of the cause, and four different missing
|
|
1350
|
+
* elements print the same number (X1 flash-m84, 2026-10-16). Two tests waiting on the same
|
|
1351
|
+
* locator for 5 s and for 10 s wait for the same thing; two waiting on different locators do not
|
|
1352
|
+
* become one cause by timing out together.
|
|
1353
|
+
*
|
|
1354
|
+
* @param s - An error line, with its detail.
|
|
1355
|
+
* @returns The signature.
|
|
1356
|
+
*/
|
|
1357
|
+
function normalizedSignature(s) {
|
|
1358
|
+
return (s
|
|
1359
|
+
// A duration: the suite's own timeout, in whatever unit the runner printed.
|
|
1360
|
+
.replace(/\b\d+(?:\.\d+)?\s*(?:ms|s|m)\b/g, '<n>')
|
|
1361
|
+
// A host's port. The shape is what makes it a port: a bare `:32` is a line number.
|
|
1362
|
+
.replace(/\b(\d{1,3}(?:\.\d{1,3}){3}|localhost|0\.0\.0\.0):\d+/g, '$1:<n>')
|
|
1363
|
+
// A generated id, a timestamp, a serial: five digits or more is no part of a selector.
|
|
1364
|
+
.replace(/\b\d{5,}\b/g, '<n>')
|
|
1365
|
+
.replace(/\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/gi, '<id>'));
|
|
1366
|
+
}
|
|
1367
|
+
/**
|
|
1368
|
+
* What separates one cause from another in a `playwright test` log: the error CLASS plus the
|
|
1369
|
+
* normalized message — the error line, the thing it waited for or asserted on, and the values
|
|
1370
|
+
* with the test's own data normalized out.
|
|
1371
|
+
*
|
|
1372
|
+
* One test printed twice, or two tests failing the same way, share a signature; two tests whose
|
|
1373
|
+
* errors differ in their locator do not, however alike the rest of the line is.
|
|
1374
|
+
*
|
|
1375
|
+
* @param block - One block from {@link failureBlocks}.
|
|
1376
|
+
* @returns The signature, or null when the block has no body.
|
|
1377
|
+
*/
|
|
1378
|
+
function failureMessage(block) {
|
|
1379
|
+
const body = failureBody(block);
|
|
1380
|
+
const line = failureErrorLine(body);
|
|
1381
|
+
if (line === null)
|
|
1382
|
+
return null;
|
|
1383
|
+
const detail = failureDetail(body);
|
|
1384
|
+
const signature = detail !== null && !line.includes(detail) ? `${line} — ${detail}` : line;
|
|
1385
|
+
return normalizedSignature(signature);
|
|
1279
1386
|
}
|
|
1280
1387
|
/**
|
|
1281
1388
|
* The largest group of items sharing a key.
|
|
@@ -1325,9 +1432,11 @@ function distinctTests(tests) {
|
|
|
1325
1432
|
* Playwright's `list` reporter prints one line per test as it finishes — its mark, its title
|
|
1326
1433
|
* path and how long it ran — and defers each failure's error to the summary at the end, so
|
|
1327
1434
|
* mid-run the only signature those lines carry is the duration. Where the log carries the
|
|
1328
|
-
* failure bodies (a `line` reporter's output, or a finished run's summary) the
|
|
1329
|
-
*
|
|
1330
|
-
* failure should take), so
|
|
1435
|
+
* failure bodies (a `line` reporter's output, or a finished run's summary) the signature is the
|
|
1436
|
+
* error itself — {@link failureMessage}: the error class plus the locator or the values that
|
|
1437
|
+
* differ — which is stronger (it needs no assumption about how long a failure should take), so
|
|
1438
|
+
* the message is looked for first and the duration is then left with the tests whose bodies the
|
|
1439
|
+
* log does NOT carry.
|
|
1331
1440
|
*
|
|
1332
1441
|
* Either way the point is the same: a suite whose failures repeat one cause spends its whole
|
|
1333
1442
|
* runtime proving that one cause, and the executor watching it can be told so at the third
|
|
@@ -1349,12 +1458,14 @@ export function repeatingFailure(log) {
|
|
|
1349
1458
|
// DISTINCT test, so the largest group is the cause the most tests share — not the one whose
|
|
1350
1459
|
// failures happen to have been printed the most times.
|
|
1351
1460
|
const byMessage = new Map();
|
|
1461
|
+
const withBody = new Set();
|
|
1352
1462
|
for (const block of failureBlocks(text)) {
|
|
1463
|
+
const m = FAILURE_BLOCK_HEADER.exec(block.split('\n')[0] ?? '');
|
|
1464
|
+
const { where, title } = splitTitlePath(m?.[1] ?? '');
|
|
1353
1465
|
const message = failureMessage(block);
|
|
1354
1466
|
if (message === null)
|
|
1355
1467
|
continue;
|
|
1356
|
-
|
|
1357
|
-
const { where, title } = splitTitlePath(m?.[1] ?? '');
|
|
1468
|
+
withBody.add(testKey({ where, title, seconds: null }));
|
|
1358
1469
|
const group = byMessage.get(message);
|
|
1359
1470
|
if (group)
|
|
1360
1471
|
group.push({ where, title, seconds: null });
|
|
@@ -1372,10 +1483,16 @@ export function repeatingFailure(log) {
|
|
|
1372
1483
|
seconds: null,
|
|
1373
1484
|
tests: messageGroup.members,
|
|
1374
1485
|
};
|
|
1375
|
-
// Otherwise the durations
|
|
1376
|
-
//
|
|
1377
|
-
//
|
|
1378
|
-
|
|
1486
|
+
// Otherwise the durations — of the tests the log says nothing ELSE about. A test whose failure
|
|
1487
|
+
// body is in the log has a signature, and how long it took must not overrule it: a suite whose
|
|
1488
|
+
// config sets a timeout makes every failure that waits for anything land on the same number, so
|
|
1489
|
+
// the duration would merge causes the bodies just separated (X1 flash-m84, 2026-10-16 — four
|
|
1490
|
+
// failures at 6.8-7.4 s, four locators, `actionTimeout: 5_000`). What is left is the shape this
|
|
1491
|
+
// was written for: a `list`-only log, or a run whose bodies have not been printed yet, where
|
|
1492
|
+
// the duration is the ONLY signature the log carries. A test that runs to its timeout prints
|
|
1493
|
+
// slightly different durations on each attempt (15.6 s, 15.1 s, 15.0 s), so the band is 0.5 s
|
|
1494
|
+
// or 5% of the duration, whichever is larger.
|
|
1495
|
+
const tests = distinctTests(failedTests(text)).filter((t) => !withBody.has(testKey(t)));
|
|
1379
1496
|
const withSeconds = tests.filter((t) => t.seconds !== null);
|
|
1380
1497
|
let best = [];
|
|
1381
1498
|
for (const t of withSeconds) {
|
|
@@ -1395,21 +1512,16 @@ export const REPEATING_FAILURE_LISTED = 5;
|
|
|
1395
1512
|
export const FAILURE_CAUSES_LISTED = 5;
|
|
1396
1513
|
/** How many of one cause's tests that summary names before it says "and N more". */
|
|
1397
1514
|
export const FAILURE_CAUSE_TESTS_LISTED = 6;
|
|
1398
|
-
/**
|
|
1399
|
-
* The shortest unambiguous tail of a logged path (`…/specs/e2e/articles.spec.ts:32` →
|
|
1400
|
-
* `e2e/articles.spec.ts:32`).
|
|
1401
|
-
*
|
|
1402
|
-
* @param where - The location the log wrote.
|
|
1403
|
-
* @returns The last two path segments, with the line number.
|
|
1404
|
-
*/
|
|
1405
|
-
function shortWhere(where) {
|
|
1406
|
-
const parts = where.split('/');
|
|
1407
|
-
return parts.slice(-2).join('/');
|
|
1408
|
-
}
|
|
1409
1515
|
/**
|
|
1410
1516
|
* The shortest unambiguous tail of a logged path's FILE (`…/specs/e2e/articles.spec.ts` →
|
|
1411
1517
|
* `e2e/articles.spec.ts`).
|
|
1412
1518
|
*
|
|
1519
|
+
* Used only where a COUNT of files is what the sentence needs; every note that names a
|
|
1520
|
+
* test to the executor prints the path the log wrote, because that is the path its own
|
|
1521
|
+
* re-run hint names and the one the executor can paste back into a command (flash-m82
|
|
1522
|
+
* read a shortened `e2e/auth.spec.ts:19` as a file the app owned, and rewrote its config
|
|
1523
|
+
* over a suite path that was already right).
|
|
1524
|
+
*
|
|
1413
1525
|
* @param file - A path as {@link specFile} returns it.
|
|
1414
1526
|
* @returns The last two path segments.
|
|
1415
1527
|
*/
|
|
@@ -1512,9 +1624,11 @@ export function universalCause(log, found) {
|
|
|
1512
1624
|
* bodies to read; X1 flash-m83 (2026-10-16) spent three deploys (6.7 minutes) each fixing a
|
|
1513
1625
|
* single cause, two of them found by runs stopped before they walked the rest.
|
|
1514
1626
|
*
|
|
1515
|
-
* The signature is the
|
|
1516
|
-
*
|
|
1517
|
-
*
|
|
1627
|
+
* The signature is the cause itself where the log carries the failures' bodies (a finished run,
|
|
1628
|
+
* or a `line` reporter) — {@link failureMessage}: the error class plus the locator or the values
|
|
1629
|
+
* that differ — and how long they took where it does not (a still-running `list` log, which is
|
|
1630
|
+
* why a duration group is labelled as a duration and not as a proven cause). A test the log
|
|
1631
|
+
* printed twice, or failed twice, is one test.
|
|
1518
1632
|
*
|
|
1519
1633
|
* @param log - The log of a finished or running run.
|
|
1520
1634
|
* @returns The causes, the one with the most tests first.
|
|
@@ -1576,11 +1690,15 @@ export function failureCausesNote(log) {
|
|
|
1576
1690
|
return null;
|
|
1577
1691
|
const listed = causes.slice(0, FAILURE_CAUSES_LISTED);
|
|
1578
1692
|
const lines = listed.map((c, i) => {
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1693
|
+
// The path the log itself wrote, never a shortened one: it is relative to the command's own
|
|
1694
|
+
// cwd, so it is the form the executor can paste back (X1 flash-m82 read a shortened
|
|
1695
|
+
// `e2e/auth.spec.ts:19` as the suite's own stale starter spec, rewrote its Playwright config
|
|
1696
|
+
// over it and deleted the app's tests — 4 minutes of a 60-minute run on a working config).
|
|
1697
|
+
const tests = c.tests.slice(0, FAILURE_CAUSE_TESTS_LISTED).map((t) => t.where || t.title);
|
|
1582
1698
|
const more = c.tests.length > tests.length ? `, … and ${c.tests.length - tests.length} more` : '';
|
|
1583
|
-
const signature = c.message !== null
|
|
1699
|
+
const signature = c.message !== null
|
|
1700
|
+
? `\`${c.message}\``
|
|
1701
|
+
: `each after ~${c.seconds}s (a shared duration, not a proven cause)`;
|
|
1584
1702
|
const files = new Set(c.tests.map((t) => specFile(t.where)).filter(Boolean)).size;
|
|
1585
1703
|
return (` ${i + 1}. ${signature} — ${c.tests.length} test${c.tests.length === 1 ? '' : 's'} ` +
|
|
1586
1704
|
`in ${files} file${files === 1 ? '' : 's'}: ${tests.join(', ')}${more}`);
|
|
@@ -1611,12 +1729,26 @@ export function failureCausesNote(log) {
|
|
|
1611
1729
|
*/
|
|
1612
1730
|
export function repeatingFailureNote(found, o) {
|
|
1613
1731
|
const listed = found.tests.slice(0, REPEATING_FAILURE_LISTED);
|
|
1732
|
+
// Every row names its test by the SAME path the re-run hint below uses (`shortWhere`
|
|
1733
|
+
// would print this one file under two names: X1 flash-m82 read `✘ e2e/auth.spec.ts:19`
|
|
1734
|
+
// here against `Re-run fixtures/realworld/specs/e2e/auth.spec.ts:19 alone` three lines
|
|
1735
|
+
// down, took the suite-relative form for "the project's own stale starter spec" and
|
|
1736
|
+
// rewrote its Playwright config over it, deleted the app's tests and wrote its own —
|
|
1737
|
+
// 4 minutes of a 60-minute run, on a run whose config was right). The path the log
|
|
1738
|
+
// wrote is relative to the command's own cwd, so it is the one the executor can paste
|
|
1739
|
+
// back into a command.
|
|
1614
1740
|
const rows = listed
|
|
1615
|
-
.map((t) => ` ✘ ${[t.where
|
|
1741
|
+
.map((t) => ` ✘ ${[t.where, t.title].filter(Boolean).join(' › ')}` +
|
|
1616
1742
|
(t.seconds === null ? '' : ` (${t.seconds}s)`))
|
|
1617
1743
|
.join('\n');
|
|
1618
1744
|
const more = found.count > listed.length ? `\n … and ${found.count - listed.length} more` : '';
|
|
1619
|
-
|
|
1745
|
+
// Two signatures, and only ONE of them is a cause. An error repeating verbatim is the cause
|
|
1746
|
+
// itself; a duration repeating is what a suite's own `actionTimeout` does to every failure that
|
|
1747
|
+
// waits for anything, so the note must not call it one cause (X1 flash-m84, 2026-10-16: four
|
|
1748
|
+
// failures at 6.8-7.4 s, four different missing elements, and this sentence told the executor
|
|
1749
|
+
// "one cause is repeating").
|
|
1750
|
+
const isCause = found.message !== null;
|
|
1751
|
+
const signature = isCause
|
|
1620
1752
|
? `every one with \`${found.message}\``
|
|
1621
1753
|
: `each after ~${found.seconds}s — the same duration to within half a second`;
|
|
1622
1754
|
const first = found.tests[0];
|
|
@@ -1637,14 +1769,21 @@ export function repeatingFailureNote(found, o) {
|
|
|
1637
1769
|
`repeats this one cause. Stop it and fix that cause first: \`exec_command: ${o.stop}\`, ` +
|
|
1638
1770
|
`then run the whole suite again. It is still going (${where}).${reproduce}`);
|
|
1639
1771
|
}
|
|
1640
|
-
|
|
1641
|
-
`so one cause is repeating:\n${rows}${more}\n` +
|
|
1642
|
-
`Start fixing it now, while the run keeps walking.${reproduce} ` +
|
|
1643
|
-
`Let the rest of the run finish — it is the only thing that will name the causes that are NOT ` +
|
|
1772
|
+
const keepGoing = `Let the rest of the run finish — it is the only thing that will name the causes that are NOT ` +
|
|
1644
1773
|
`this one, and a run stopped at its first repeating cause costs a second whole run to find ` +
|
|
1645
1774
|
`them. Stop it only if every test it walks fails this same way (the app unreachable, a 5xx on ` +
|
|
1646
1775
|
`every request, the same selector timing out on every test of every file so far). ` +
|
|
1647
|
-
`It is still going (${where})
|
|
1776
|
+
`It is still going (${where}).`;
|
|
1777
|
+
if (isCause)
|
|
1778
|
+
return (`${found.count} different tests in this run have already failed the same way — ${signature} — ` +
|
|
1779
|
+
`so one cause is repeating:\n${rows}${more}\n` +
|
|
1780
|
+
`Start fixing it now, while the run keeps walking.${reproduce} ${keepGoing}`);
|
|
1781
|
+
return (`${found.count} different tests in this run each failed after ~${found.seconds}s — the same ` +
|
|
1782
|
+
`duration to within half a second. A shared duration is a lead, not a cause: a suite whose ` +
|
|
1783
|
+
`config sets a timeout spends that timeout on every failure that waits for anything, so ` +
|
|
1784
|
+
`different missing elements land on the same number. Read these failures before fixing one ` +
|
|
1785
|
+
`of them:\n${rows}${more}\n` +
|
|
1786
|
+
`Start on them now, while the run keeps walking.${reproduce} ${keepGoing}`);
|
|
1648
1787
|
}
|
|
1649
1788
|
/**
|
|
1650
1789
|
* Attempt a whitespace-tolerant replacement when an exact `old_string` match
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@molecule/api-ai-tools",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.4",
|
|
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.
|
|
34
|
+
"@molecule/api-ai": "1.3.3",
|
|
35
35
|
"@types/node": "26.1.2",
|
|
36
36
|
"esbuild": "0.28.2",
|
|
37
37
|
"typescript": "6.0.3",
|