@sema-agent/core 1.294.0 → 1.296.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/dist/agents/subagent.d.ts.map +1 -1
  2. package/dist/agents/subagent.js +24 -3
  3. package/dist/agents/subagent.js.map +1 -1
  4. package/dist/bin/sema-tb.d.ts +20 -0
  5. package/dist/bin/sema-tb.d.ts.map +1 -1
  6. package/dist/bin/sema-tb.js +71 -23
  7. package/dist/bin/sema-tb.js.map +1 -1
  8. package/dist/brain/anthropic.d.ts.map +1 -1
  9. package/dist/brain/anthropic.js +17 -3
  10. package/dist/brain/anthropic.js.map +1 -1
  11. package/dist/brain/openai.d.ts +1 -0
  12. package/dist/brain/openai.d.ts.map +1 -1
  13. package/dist/brain/openai.js +328 -298
  14. package/dist/brain/openai.js.map +1 -1
  15. package/dist/brain/stream-engine.d.ts +7 -0
  16. package/dist/brain/stream-engine.d.ts.map +1 -1
  17. package/dist/brain/stream-engine.js +44 -11
  18. package/dist/brain/stream-engine.js.map +1 -1
  19. package/dist/brain/timeout.d.ts +2 -1
  20. package/dist/brain/timeout.d.ts.map +1 -1
  21. package/dist/brain/timeout.js +14 -1
  22. package/dist/brain/timeout.js.map +1 -1
  23. package/dist/core/ask-question.d.ts.map +1 -1
  24. package/dist/core/ask-question.js.map +1 -1
  25. package/dist/core/hooks.d.ts +3 -3
  26. package/dist/core/hooks.d.ts.map +1 -1
  27. package/dist/core/hooks.js +11 -4
  28. package/dist/core/hooks.js.map +1 -1
  29. package/dist/core/present-plan-tool.d.ts.map +1 -1
  30. package/dist/core/present-plan-tool.js +12 -13
  31. package/dist/core/present-plan-tool.js.map +1 -1
  32. package/dist/core/runner/assemble-result.d.ts +1 -0
  33. package/dist/core/runner/assemble-result.d.ts.map +1 -1
  34. package/dist/core/runner/assemble-result.js.map +1 -1
  35. package/dist/core/runner/call-cap.d.ts +4 -1
  36. package/dist/core/runner/call-cap.d.ts.map +1 -1
  37. package/dist/core/runner/call-cap.js +4 -1
  38. package/dist/core/runner/call-cap.js.map +1 -1
  39. package/dist/core/runner/cut-kill.d.ts +11 -0
  40. package/dist/core/runner/cut-kill.d.ts.map +1 -0
  41. package/dist/core/runner/cut-kill.js +38 -0
  42. package/dist/core/runner/cut-kill.js.map +1 -0
  43. package/dist/core/runner/prepare-task.d.ts +5 -0
  44. package/dist/core/runner/prepare-task.d.ts.map +1 -1
  45. package/dist/core/runner/prepare-task.js +70 -23
  46. package/dist/core/runner/prepare-task.js.map +1 -1
  47. package/dist/core/runner/runtask.d.ts.map +1 -1
  48. package/dist/core/runner/runtask.js +22 -6
  49. package/dist/core/runner/runtask.js.map +1 -1
  50. package/dist/core/sensitive-path-policy.d.ts +2 -1
  51. package/dist/core/sensitive-path-policy.d.ts.map +1 -1
  52. package/dist/core/sensitive-path-policy.js +22 -0
  53. package/dist/core/sensitive-path-policy.js.map +1 -1
  54. package/dist/core/task-registry.d.ts +4 -0
  55. package/dist/core/task-registry.d.ts.map +1 -1
  56. package/dist/core/task-registry.js +83 -15
  57. package/dist/core/task-registry.js.map +1 -1
  58. package/dist/core/tool-policy.d.ts +5 -2
  59. package/dist/core/tool-policy.d.ts.map +1 -1
  60. package/dist/core/tool-policy.js +9 -0
  61. package/dist/core/tool-policy.js.map +1 -1
  62. package/dist/core/trace.d.ts +3 -0
  63. package/dist/core/trace.d.ts.map +1 -1
  64. package/dist/core/trace.js.map +1 -1
  65. package/dist/core/types.d.ts +6 -0
  66. package/dist/core/types.d.ts.map +1 -1
  67. package/dist/engine/compaction/utils.d.ts.map +1 -1
  68. package/dist/engine/compaction/utils.js +3 -1
  69. package/dist/engine/compaction/utils.js.map +1 -1
  70. package/dist/engine/execution-env/kill-tree.d.ts +1 -0
  71. package/dist/engine/execution-env/kill-tree.d.ts.map +1 -1
  72. package/dist/engine/execution-env/kill-tree.js +18 -2
  73. package/dist/engine/execution-env/kill-tree.js.map +1 -1
  74. package/dist/engine/execution-env/node-execution-env.d.ts +5 -0
  75. package/dist/engine/execution-env/node-execution-env.d.ts.map +1 -1
  76. package/dist/engine/execution-env/node-execution-env.js +19 -0
  77. package/dist/engine/execution-env/node-execution-env.js.map +1 -1
  78. package/dist/engine/harness/agent-harness.d.ts +1 -0
  79. package/dist/engine/harness/agent-harness.d.ts.map +1 -1
  80. package/dist/engine/harness/agent-harness.js +4 -0
  81. package/dist/engine/harness/agent-harness.js.map +1 -1
  82. package/dist/engine/harness/messages.d.ts +2 -0
  83. package/dist/engine/harness/messages.d.ts.map +1 -1
  84. package/dist/engine/harness/messages.js +52 -3
  85. package/dist/engine/harness/messages.js.map +1 -1
  86. package/dist/engine/harness/types.d.ts +7 -0
  87. package/dist/engine/harness/types.d.ts.map +1 -1
  88. package/dist/engine/harness/types.js.map +1 -1
  89. package/dist/engine/llm/types.d.ts +6 -0
  90. package/dist/engine/llm/types.d.ts.map +1 -1
  91. package/dist/engine/loop/agent-loop.js +3 -0
  92. package/dist/engine/loop/agent-loop.js.map +1 -1
  93. package/dist/engine/loop/types.d.ts +2 -0
  94. package/dist/engine/loop/types.d.ts.map +1 -1
  95. package/dist/engine/session/import-validate.d.ts.map +1 -1
  96. package/dist/engine/session/import-validate.js +64 -0
  97. package/dist/engine/session/import-validate.js.map +1 -1
  98. package/dist/index.d.ts +2 -2
  99. package/dist/index.d.ts.map +1 -1
  100. package/dist/index.js +1 -1
  101. package/dist/index.js.map +1 -1
  102. package/dist/internal/llm.d.ts +1 -1
  103. package/dist/internal/llm.d.ts.map +1 -1
  104. package/dist/scenarios/full-body.d.ts.map +1 -1
  105. package/dist/scenarios/full-body.js +3 -2
  106. package/dist/scenarios/full-body.js.map +1 -1
  107. package/dist/tools/fs/index.d.ts +8 -1
  108. package/dist/tools/fs/index.d.ts.map +1 -1
  109. package/dist/tools/fs/index.js +92 -30
  110. package/dist/tools/fs/index.js.map +1 -1
  111. package/dist/tools/fs/safety.d.ts +1 -1
  112. package/dist/tools/fs/safety.d.ts.map +1 -1
  113. package/dist/tools/fs/safety.js +3 -1
  114. package/dist/tools/fs/safety.js.map +1 -1
  115. package/dist/tools/fs/search.d.ts +7 -2
  116. package/dist/tools/fs/search.d.ts.map +1 -1
  117. package/dist/tools/fs/search.js +465 -57
  118. package/dist/tools/fs/search.js.map +1 -1
  119. package/dist/tools/monitor.d.ts.map +1 -1
  120. package/dist/tools/monitor.js +12 -1
  121. package/dist/tools/monitor.js.map +1 -1
  122. package/dist/tools/task-list.d.ts.map +1 -1
  123. package/dist/tools/task-list.js.map +1 -1
  124. package/dist/tools/worktree.d.ts.map +1 -1
  125. package/dist/tools/worktree.js +15 -2
  126. package/dist/tools/worktree.js.map +1 -1
  127. package/package.json +1 -1
@@ -161,6 +161,7 @@ export function walkIsPartial(w) {
161
161
  export async function walk(env, root, start, ignore, signal) {
162
162
  const out = [];
163
163
  const nameOnly = [];
164
+ const mtimes = new Map();
164
165
  let incomplete = false;
165
166
  let skippedLarge = 0;
166
167
  let skippedUnreadable = 0;
@@ -169,7 +170,10 @@ export async function walk(env, root, start, ignore, signal) {
169
170
  const rel = (abs) => (abs.startsWith(rootPrefix) ? abs.slice(rootPrefix.length) : abs);
170
171
  const stack = [{ dir: start, depth: 0 }];
171
172
  const visited = new Set();
173
+ let steps = 0;
172
174
  while (stack.length > 0 && out.length < WALK_MAX_FILES) {
175
+ if (++steps % SCAN_YIELD_EVERY === 0)
176
+ await yieldEventLoop();
173
177
  if (signal?.aborted) {
174
178
  aborted = true;
175
179
  break;
@@ -199,6 +203,8 @@ export async function walk(env, root, start, ignore, signal) {
199
203
  else if (info.kind === "file") {
200
204
  if (ignore(rel(info.path), false))
201
205
  continue;
206
+ if (typeof info.mtimeMs === "number")
207
+ mtimes.set(info.path, info.mtimeMs);
202
208
  if (hasBinaryExtension(info.path)) {
203
209
  if (nameOnly.length < WALK_MAX_FILES)
204
210
  nameOnly.push(info.path);
@@ -224,7 +230,7 @@ export async function walk(env, root, start, ignore, signal) {
224
230
  }
225
231
  if (stack.length > 0 && out.length >= WALK_MAX_FILES)
226
232
  incomplete = true;
227
- return { files: out, nameOnly, incomplete, skippedLarge, skippedUnreadable, aborted };
233
+ return { files: out, nameOnly, incomplete, skippedLarge, skippedUnreadable, aborted, mtimes };
228
234
  }
229
235
  function walkCaveat(w, forContentSearch) {
230
236
  const parts = [];
@@ -240,42 +246,404 @@ function walkCaveat(w, forContentSearch) {
240
246
  }
241
247
  return parts.length > 0 ? `\n…[results may be incomplete: ${parts.join("; ")}]` : "";
242
248
  }
243
- function compilePattern(p) {
244
- if (p.regex) {
245
- const re = new RegExp(p.pattern, p.ignore_case ? "i" : "");
246
- return (l) => re.test(l);
249
+ const SCAN_YIELD_EVERY = 32;
250
+ const yieldEventLoop = () => new Promise((resolve) => (typeof setImmediate === "function" ? setImmediate(resolve) : setTimeout(resolve, 0)));
251
+ const REDOS_BOUNDED_CEILING = 100;
252
+ const REDOS_AMBIGUOUS_REPEAT_CEILING = 10;
253
+ function analyzeRedos(pattern, ignoreCase = false) {
254
+ const quantAt = (i) => {
255
+ const c = pattern[i];
256
+ if (c === "*" || c === "+")
257
+ return { len: 1, unbounded: true, max: Infinity, range: true, brace: false };
258
+ if (c === "?")
259
+ return { len: 1, unbounded: false, max: 1, range: true, brace: false };
260
+ if (c === "{") {
261
+ const m = /^\{(\d+)(?:,(\d*))?\}/.exec(pattern.slice(i));
262
+ if (!m)
263
+ return undefined;
264
+ const lo = Number(m[1]);
265
+ const hi = m[2] === "" || m[2] === undefined ? (m[2] === "" ? Infinity : lo) : Number(m[2]);
266
+ return { len: m[0].length, unbounded: hi > REDOS_BOUNDED_CEILING, max: hi, range: lo < hi, brace: true };
267
+ }
268
+ return undefined;
269
+ };
270
+ let hasNamedGroup = false;
271
+ const escTokenLen = (s, i) => {
272
+ const c = s[i + 1];
273
+ if (c === "x" && /^[0-9a-fA-F]{2}$/.test(s.slice(i + 2, i + 4)))
274
+ return 4;
275
+ if (c === "u") {
276
+ if (/^[0-9a-fA-F]{4}$/.test(s.slice(i + 2, i + 6)))
277
+ return 6;
278
+ return 2;
279
+ }
280
+ if (c === "c" && /^[a-zA-Z]$/.test(s[i + 2] ?? ""))
281
+ return 3;
282
+ if (c === "k" && hasNamedGroup && s[i + 2] === "<") {
283
+ const close = s.indexOf(">", i + 3);
284
+ if (close > i + 3)
285
+ return close - i + 1;
286
+ }
287
+ if (c === "0") {
288
+ let j = i + 2;
289
+ while (j < s.length && j < i + 4 && s[j] >= "0" && s[j] <= "7")
290
+ j++;
291
+ return j - i;
292
+ }
293
+ if (c !== undefined && c >= "1" && c <= "9") {
294
+ let j = i + 2;
295
+ while (j < s.length && j < i + 10 && s[j] >= "0" && s[j] <= "9")
296
+ j++;
297
+ if (Number(s.slice(i + 1, j)) <= captureGroups)
298
+ return j - i;
299
+ if (c >= "8")
300
+ return 2;
301
+ const maxLen = c <= "3" ? 3 : 2;
302
+ let k = i + 1;
303
+ while (k < j && k - i - 1 < maxLen && s[k] >= "0" && s[k] <= "7")
304
+ k++;
305
+ return k - i;
306
+ }
307
+ return 2;
308
+ };
309
+ let captureGroups = 0;
310
+ for (let i = 0; i < pattern.length; i++) {
311
+ const ch = pattern[i];
312
+ if (ch === "\\") {
313
+ i++;
314
+ continue;
315
+ }
316
+ if (ch === "[") {
317
+ i++;
318
+ if (pattern[i] === "^")
319
+ i++;
320
+ while (i < pattern.length && pattern[i] !== "]") {
321
+ if (pattern[i] === "\\")
322
+ i++;
323
+ i++;
324
+ }
325
+ continue;
326
+ }
327
+ if (ch === "(") {
328
+ if (pattern[i + 1] !== "?")
329
+ captureGroups++;
330
+ else if (pattern[i + 2] === "<" && pattern[i + 3] !== "=" && pattern[i + 3] !== "!") {
331
+ captureGroups++;
332
+ hasNamedGroup = true;
333
+ }
334
+ }
247
335
  }
248
- if (p.ignore_case) {
249
- const needle = p.pattern.toLowerCase();
250
- return (l) => l.toLowerCase().includes(needle);
336
+ const readClassAtom = (s, pos) => {
337
+ const ch = s[pos];
338
+ if (ch === undefined)
339
+ return undefined;
340
+ if (ch !== "\\")
341
+ return { cp: ch, len: 1 };
342
+ const c = s[pos + 1];
343
+ if (c === undefined)
344
+ return undefined;
345
+ if (c === "x" && /^[0-9a-fA-F]{2}$/.test(s.slice(pos + 2, pos + 4)))
346
+ return { cp: String.fromCharCode(parseInt(s.slice(pos + 2, pos + 4), 16)), len: 4 };
347
+ if (c === "u" && /^[0-9a-fA-F]{4}$/.test(s.slice(pos + 2, pos + 6)))
348
+ return { cp: String.fromCharCode(parseInt(s.slice(pos + 2, pos + 6), 16)), len: 6 };
349
+ if (c === "c") {
350
+ if (/^[a-zA-Z0-9_]$/.test(s[pos + 2] ?? ""))
351
+ return { cp: String.fromCharCode(s[pos + 2].toUpperCase().charCodeAt(0) % 32), len: 3 };
352
+ return { cp: undefined, len: 2 };
353
+ }
354
+ if (c >= "0" && c <= "9") {
355
+ if (c === "8" || c === "9")
356
+ return { cp: c, len: 2 };
357
+ const maxLen = c <= "3" ? 3 : 2;
358
+ let j = pos + 1;
359
+ while (j < s.length && j - pos - 1 < maxLen && s[j] >= "0" && s[j] <= "7")
360
+ j++;
361
+ return { cp: String.fromCharCode(parseInt(s.slice(pos + 1, j), 8)), len: j - pos };
362
+ }
363
+ if (c === "b")
364
+ return { cp: "\b", len: 2 };
365
+ if (/[dDwWsS]/.test(c))
366
+ return { cp: undefined, len: 2 };
367
+ if (c === "t")
368
+ return { cp: "\t", len: 2 };
369
+ if (c === "n")
370
+ return { cp: "\n", len: 2 };
371
+ if (c === "r")
372
+ return { cp: "\r", len: 2 };
373
+ if (c === "f")
374
+ return { cp: "\f", len: 2 };
375
+ if (c === "v")
376
+ return { cp: "\v", len: 2 };
377
+ return { cp: c, len: 2 };
378
+ };
379
+ const atomKey = (src) => {
380
+ const canon = (ch) => {
381
+ if (!ignoreCase)
382
+ return ch;
383
+ const u = ch.toUpperCase();
384
+ if (u.length !== 1)
385
+ return ch;
386
+ if (ch.charCodeAt(0) >= 128 && u.charCodeAt(0) < 128)
387
+ return ch;
388
+ return u;
389
+ };
390
+ const lit = (ch) => `L${canon(ch)}`;
391
+ if (src.length === 0)
392
+ return src;
393
+ if (src[0] !== "\\") {
394
+ if (src.length === 1)
395
+ return src === "." ? src : lit(src);
396
+ if (src[0] === "[" && src.endsWith("]") && src[1] !== "^") {
397
+ const body = src.slice(1, -1);
398
+ const a1 = readClassAtom(body, 0);
399
+ if (a1?.cp !== undefined) {
400
+ if (a1.len === body.length)
401
+ return lit(a1.cp);
402
+ if (body[a1.len] === "-") {
403
+ const a2 = readClassAtom(body, a1.len + 1);
404
+ if (a2?.cp !== undefined && a1.len + 1 + a2.len === body.length && a1.cp === a2.cp) {
405
+ return lit(a1.cp);
406
+ }
407
+ }
408
+ }
409
+ return src;
410
+ }
411
+ if (src.startsWith("(?:") && src.endsWith(")")) {
412
+ const body = src.slice(3, -1);
413
+ if (body === ".")
414
+ return ".";
415
+ if (body.length === 1 && !"\\^$.|?*+()[]{}".includes(body))
416
+ return lit(body);
417
+ if (body[0] === "\\" && escTokenLen(body, 0) === body.length) {
418
+ const k = atomKey(body);
419
+ if (k.startsWith("L"))
420
+ return k;
421
+ }
422
+ if (body[0] === "[") {
423
+ const k = atomKey(body);
424
+ if (k.startsWith("L"))
425
+ return k;
426
+ }
427
+ return src;
428
+ }
429
+ return src;
430
+ }
431
+ const c = src[1];
432
+ if (c !== undefined && c >= "0" && c <= "9") {
433
+ const digits = src.slice(1);
434
+ if (c !== "0" && Number(digits) <= captureGroups)
435
+ return src;
436
+ if (c === "8" || c === "9")
437
+ return digits.length === 1 ? lit(c) : src;
438
+ if (/^[0-7]{1,3}$/.test(digits) && (digits.length < 3 || digits[0] <= "3")) {
439
+ return lit(String.fromCharCode(parseInt(digits, 8)));
440
+ }
441
+ return src;
442
+ }
443
+ if (c === "x" && src.length === 4)
444
+ return lit(String.fromCharCode(parseInt(src.slice(2), 16)));
445
+ if (c === "u" && src.length === 6)
446
+ return lit(String.fromCharCode(parseInt(src.slice(2), 16)));
447
+ if (c === "c" && src.length === 3)
448
+ return `L${String.fromCharCode(src[2].toUpperCase().charCodeAt(0) % 32)}`;
449
+ if (src.length === 2) {
450
+ if (!/[a-zA-Z]/.test(c))
451
+ return lit(c);
452
+ if (/[dDwWsSbB]/.test(c))
453
+ return src;
454
+ if (c === "t")
455
+ return lit("\t");
456
+ if (c === "n")
457
+ return lit("\n");
458
+ if (c === "r")
459
+ return lit("\r");
460
+ if (c === "f")
461
+ return lit("\f");
462
+ if (c === "v")
463
+ return lit("\v");
464
+ if (c === "c")
465
+ return src;
466
+ if (c === "k")
467
+ return hasNamedGroup ? src : lit("k");
468
+ return lit(c);
469
+ }
470
+ return src;
471
+ };
472
+ const stack = [{ start: 0, hasUnbounded: false, hasRange: false, hasAlt: false, maxCeil: 1 }];
473
+ const cur = () => stack[stack.length - 1];
474
+ const atomCtx = [{}];
475
+ const atom = () => atomCtx[atomCtx.length - 1];
476
+ let lastClosedGroup;
477
+ let quantifiedGroup = false;
478
+ let unboundedCount = 0;
479
+ for (let i = 0; i < pattern.length; i++) {
480
+ const c = pattern[i];
481
+ const q = quantAt(i);
482
+ if (q) {
483
+ const lazy = pattern[i + q.len] === "?" ? 1 : 0;
484
+ if (q.unbounded)
485
+ unboundedCount++;
486
+ const g = lastClosedGroup;
487
+ const lvl = cur();
488
+ const ctx = atom();
489
+ if (g) {
490
+ quantifiedGroup = true;
491
+ const snippet = JSON.stringify(pattern.slice(g.start, i + q.len));
492
+ if (q.unbounded) {
493
+ if (g.hasUnbounded)
494
+ return { risk: `nested unbounded quantifiers in ${snippet} (an unbounded quantifier applied to a group that itself contains one)`, quantifiedGroup, unboundedCount };
495
+ if (g.hasAlt)
496
+ return { risk: `quantified alternation in ${snippet} (an unbounded quantifier applied to a group containing "|" — the branches can split the same text many ways)`, quantifiedGroup, unboundedCount };
497
+ if (g.hasRange)
498
+ return { risk: `ambiguous nested repetition in ${snippet} (an unbounded quantifier applied to a group whose own repetition count can vary)`, quantifiedGroup, unboundedCount };
499
+ }
500
+ else if (q.brace && q.max >= 2) {
501
+ if (g.hasUnbounded)
502
+ return { risk: `nested quantifiers in ${snippet} (an unbounded quantifier inside a group repeated up to ${q.max} times)`, quantifiedGroup, unboundedCount };
503
+ if ((g.hasAlt || g.hasRange) && q.max > REDOS_AMBIGUOUS_REPEAT_CEILING)
504
+ return { risk: `ambiguous repetition in ${snippet} (a group with ${g.hasAlt ? "alternation" : "variable-count content"} repeated up to ${q.max} times — candidate paths grow exponentially with the repeat count)`, quantifiedGroup, unboundedCount };
505
+ if (g.maxCeil >= 2 && g.maxCeil * q.max > REDOS_BOUNDED_CEILING)
506
+ return { risk: `compounded bounded repetition in ${snippet} (inner ceiling ${g.maxCeil} × outer ${q.max} exceeds the ${REDOS_BOUNDED_CEILING} safety ceiling)`, quantifiedGroup, unboundedCount };
507
+ }
508
+ }
509
+ const sideUnbounded = q.unbounded && q.range;
510
+ const atomStart = g ? g.start : ctx.lastAtomStart;
511
+ if (atomStart !== undefined) {
512
+ const atomSrc = pattern.slice(atomStart, i);
513
+ const key = atomKey(atomSrc);
514
+ const prev = ctx.prevQuant;
515
+ if (prev && prev.key === key && prev.unbounded && sideUnbounded) {
516
+ const snippet = JSON.stringify(pattern.slice(prev.start, i + q.len + lazy));
517
+ return {
518
+ risk: `adjacent equivalent quantified atoms in ${snippet} (two consecutive unbounded quantifiers apply to the identical atom ${JSON.stringify(atomSrc)} — every split of a matching run between them is a distinct backtracking path, quadratic in line length on non-matching input; merge them into a single quantifier)`,
519
+ quantifiedGroup,
520
+ unboundedCount,
521
+ };
522
+ }
523
+ ctx.prevQuant = { key, start: atomStart, unbounded: sideUnbounded };
524
+ }
525
+ else
526
+ ctx.prevQuant = undefined;
527
+ if (g) {
528
+ if (q.unbounded)
529
+ lvl.hasUnbounded = true;
530
+ else {
531
+ if (q.range)
532
+ lvl.hasRange = true;
533
+ lvl.maxCeil = Math.max(lvl.maxCeil, Math.max(g.maxCeil, 1) * q.max);
534
+ }
535
+ }
536
+ else if (q.unbounded)
537
+ lvl.hasUnbounded = true;
538
+ else {
539
+ if (q.range)
540
+ lvl.hasRange = true;
541
+ lvl.maxCeil = Math.max(lvl.maxCeil, q.max);
542
+ }
543
+ i += q.len + lazy - 1;
544
+ ctx.lastAtomStart = undefined;
545
+ lastClosedGroup = undefined;
546
+ continue;
547
+ }
548
+ if (lastClosedGroup !== undefined || atom().lastAtomStart !== undefined)
549
+ atom().prevQuant = undefined;
550
+ lastClosedGroup = undefined;
551
+ if (c === "\\") {
552
+ atom().lastAtomStart = i;
553
+ i += escTokenLen(pattern, i) - 1;
554
+ continue;
555
+ }
556
+ if (c === "[") {
557
+ atom().lastAtomStart = i;
558
+ i++;
559
+ if (pattern[i] === "^")
560
+ i++;
561
+ while (i < pattern.length && pattern[i] !== "]") {
562
+ if (pattern[i] === "\\")
563
+ i++;
564
+ i++;
565
+ }
566
+ continue;
567
+ }
568
+ if (c === "|") {
569
+ cur().hasAlt = true;
570
+ const ctx = atom();
571
+ ctx.lastAtomStart = undefined;
572
+ ctx.prevQuant = undefined;
573
+ continue;
574
+ }
575
+ if (c === "(") {
576
+ stack.push({ start: i, hasUnbounded: false, hasRange: false, hasAlt: false, maxCeil: 1 });
577
+ atomCtx.push({});
578
+ if (pattern[i + 1] === "?") {
579
+ const c2 = pattern[i + 2];
580
+ if (c2 === ":" || c2 === "=" || c2 === "!")
581
+ i += 2;
582
+ else if (c2 === "<") {
583
+ const c3 = pattern[i + 3];
584
+ if (c3 === "=" || c3 === "!")
585
+ i += 3;
586
+ else {
587
+ const close = pattern.indexOf(">", i + 3);
588
+ if (close > 0)
589
+ i = close;
590
+ }
591
+ }
592
+ }
593
+ continue;
594
+ }
595
+ if (c === ")") {
596
+ if (stack.length === 1)
597
+ continue;
598
+ const frame = stack.pop();
599
+ atomCtx.pop();
600
+ const lvl = cur();
601
+ lvl.hasUnbounded ||= frame.hasUnbounded;
602
+ lvl.hasRange ||= frame.hasRange;
603
+ lvl.hasAlt ||= frame.hasAlt;
604
+ lvl.maxCeil = Math.max(lvl.maxCeil, frame.maxCeil);
605
+ lastClosedGroup = frame;
606
+ atom().lastAtomStart = undefined;
607
+ continue;
608
+ }
609
+ atom().lastAtomStart = i;
251
610
  }
252
- return (l) => l.includes(p.pattern);
611
+ return { quantifiedGroup, unboundedCount };
612
+ }
613
+ export function findRedosRisk(pattern, ignoreCase = false) {
614
+ return analyzeRedos(pattern, ignoreCase).risk;
615
+ }
616
+ function redosRejection(risk) {
617
+ return (`Error (grep): pattern rejected by the fallback scanner: ${risk}. ` +
618
+ `ripgrep is unavailable here, so this pattern would run on a backtracking regex engine where that shape ` +
619
+ `can hang the scan (catastrophic backtracking). Rewrite it so a repeated group cannot match the same text ` +
620
+ `in more than one way — remove the nested/variable quantifier or the alternation from the repeated group, ` +
621
+ `or lower the repetition counts (product ceiling: ${REDOS_BOUNDED_CEILING}).`);
622
+ }
623
+ const GREP_FILE_BUDGET_MS = 2_000;
624
+ const GREP_GRAY_LINE_MAX = 8_192;
625
+ const GREP_LINE_CHECK_EVERY = 256;
626
+ const monotonicNow = () => (typeof performance !== "undefined" ? performance.now() : Date.now());
627
+ class GrepBudgetExceeded extends Error {
628
+ }
629
+ function compilePattern(p) {
630
+ const re = new RegExp(p.pattern, p.ignore_case ? "i" : "");
631
+ return (l) => re.test(l);
253
632
  }
254
633
  function matchedParts(line, p) {
255
634
  const parts = [];
256
- if (p.regex) {
257
- const re = new RegExp(p.pattern, `g${p.ignore_case ? "i" : ""}`);
258
- for (let m = re.exec(line); m !== null; m = re.exec(line)) {
259
- if (m[0].length > 0)
260
- parts.push(m[0]);
261
- else
262
- re.lastIndex++;
263
- if (parts.length > 1000)
264
- break;
265
- }
266
- }
267
- else if (p.pattern.length > 0) {
268
- const hay = p.ignore_case ? line.toLowerCase() : line;
269
- const needle = p.ignore_case ? p.pattern.toLowerCase() : p.pattern;
270
- for (let idx = hay.indexOf(needle); idx !== -1; idx = hay.indexOf(needle, idx + needle.length)) {
271
- parts.push(line.slice(idx, idx + needle.length));
272
- if (parts.length > 1000)
273
- break;
274
- }
635
+ const re = new RegExp(p.pattern, `g${p.ignore_case ? "i" : ""}`);
636
+ for (let m = re.exec(line); m !== null; m = re.exec(line)) {
637
+ if (m[0].length > 0)
638
+ parts.push(m[0]);
639
+ else
640
+ re.lastIndex++;
641
+ if (parts.length > 1000)
642
+ break;
275
643
  }
276
644
  return parts;
277
645
  }
278
- function multilineSpans(content, p, captureText = false) {
646
+ function multilineSpans(content, p, captureText = false, deadline = Infinity) {
279
647
  const spans = [];
280
648
  const starts = [0];
281
649
  for (let i = 0; i < content.length; i++)
@@ -293,30 +661,33 @@ function multilineSpans(content, p, captureText = false) {
293
661
  return lo + 1;
294
662
  };
295
663
  const MATCH_TEXT_MAX = 2_000;
296
- if (p.regex) {
297
- const re = new RegExp(p.pattern, `gs${p.ignore_case ? "i" : ""}`);
298
- for (let m = re.exec(content); m !== null; m = re.exec(content)) {
299
- spans.push([lineAt(m.index), lineAt(m.index + Math.max(0, m[0].length - 1)), captureText ? m[0].slice(0, MATCH_TEXT_MAX) : undefined]);
300
- if (m[0].length === 0)
301
- re.lastIndex++;
302
- if (spans.length > 10_000)
303
- break;
304
- }
305
- }
306
- else {
307
- const hay = p.ignore_case ? content.toLowerCase() : content;
308
- const needle = p.ignore_case ? p.pattern.toLowerCase() : p.pattern;
309
- if (needle.length > 0) {
310
- for (let idx = hay.indexOf(needle); idx !== -1; idx = hay.indexOf(needle, idx + needle.length)) {
311
- spans.push([lineAt(idx), lineAt(idx + needle.length - 1), captureText ? content.slice(idx, idx + Math.min(needle.length, MATCH_TEXT_MAX)) : undefined]);
312
- if (spans.length > 10_000)
313
- break;
314
- }
315
- }
664
+ const re = new RegExp(p.pattern, `gs${p.ignore_case ? "i" : ""}`);
665
+ for (;;) {
666
+ if (monotonicNow() >= deadline)
667
+ throw new GrepBudgetExceeded();
668
+ const m = re.exec(content);
669
+ if (m === null)
670
+ break;
671
+ spans.push([lineAt(m.index), lineAt(m.index + Math.max(0, m[0].length - 1)), captureText ? m[0].slice(0, MATCH_TEXT_MAX) : undefined]);
672
+ if (m[0].length === 0)
673
+ re.lastIndex++;
674
+ if (spans.length > 10_000)
675
+ break;
316
676
  }
317
677
  return spans;
318
678
  }
319
- export async function jsGrep(env, root, p, signal) {
679
+ export async function jsGrep(env, root, p, signal, guards) {
680
+ const analysis = analyzeRedos(p.pattern, p.ignore_case === true);
681
+ if (analysis.risk)
682
+ return redosRejection(analysis.risk);
683
+ const budgetMs = guards?.budgetMs ?? GREP_FILE_BUDGET_MS;
684
+ const longLineLimit = guards?.longLineLimit ?? GREP_GRAY_LINE_MAX;
685
+ const grayPattern = analysis.quantifiedGroup || analysis.unboundedCount >= 2;
686
+ const budgetError = (file, elapsedMs) => `Error (grep): scan stopped after ${Math.round(elapsedMs)}ms inside ${file} — the pattern ` +
687
+ `${JSON.stringify(p.pattern)} is backtracking heavily on this content (fallback-engine budget: ` +
688
+ `${budgetMs}ms per file). Rewrite the pattern to be more specific, or rerun where ripgrep is available.`;
689
+ let skippedLongLines = 0;
690
+ let multilineSkippedFiles = 0;
320
691
  let test;
321
692
  try {
322
693
  test = compilePattern(p);
@@ -332,7 +703,7 @@ export async function jsGrep(env, root, p, signal) {
332
703
  let files = walked.files;
333
704
  files.sort();
334
705
  const cap = p.head_limit === 0 ? Infinity : Math.max(1, Math.floor(p.head_limit ?? GREP_DEFAULT_CAP));
335
- const mode = p.output_mode ?? "content";
706
+ const mode = p.output_mode ?? "files_with_matches";
336
707
  const rel = (abs) => (abs.startsWith(rootPrefix) ? abs.slice(rootPrefix.length) : abs);
337
708
  const relOut = (abs) => {
338
709
  const r = rel(abs);
@@ -366,7 +737,10 @@ export async function jsGrep(env, root, p, signal) {
366
737
  let totalFiles = 0;
367
738
  let skippedUnreadableFiles = 0;
368
739
  let scanAborted = false;
740
+ let scanned = 0;
369
741
  for (const f of files) {
742
+ if (++scanned % SCAN_YIELD_EVERY === 0)
743
+ await yieldEventLoop();
370
744
  if (signal?.aborted) {
371
745
  scanAborted = true;
372
746
  break;
@@ -384,12 +758,19 @@ export async function jsGrep(env, root, p, signal) {
384
758
  if (lines.length > 1 && lines.at(-1) === "")
385
759
  lines.pop();
386
760
  let fileCount = 0;
761
+ const fileMatchStart = monotonicNow();
387
762
  if (p.multiline) {
763
+ if (grayPattern && read.value.length > longLineLimit) {
764
+ multilineSkippedFiles++;
765
+ continue;
766
+ }
388
767
  let spans;
389
768
  try {
390
- spans = multilineSpans(read.value, p, mode === "content" && p.only_matching === true);
769
+ spans = multilineSpans(read.value, p, mode === "content" && p.only_matching === true, fileMatchStart + budgetMs);
391
770
  }
392
771
  catch (e) {
772
+ if (e instanceof GrepBudgetExceeded)
773
+ return budgetError(relOut(f), monotonicNow() - fileMatchStart);
393
774
  return `Error (grep): invalid regex: ${e instanceof Error ? e.message : String(e)}`;
394
775
  }
395
776
  fileCount = spans.length;
@@ -412,6 +793,22 @@ export async function jsGrep(env, root, p, signal) {
412
793
  }
413
794
  else {
414
795
  for (let i = 0; i < lines.length; i++) {
796
+ if (grayPattern || (i > 0 && i % GREP_LINE_CHECK_EVERY === 0)) {
797
+ const elapsed = monotonicNow() - fileMatchStart;
798
+ if (elapsed >= budgetMs)
799
+ return budgetError(relOut(f), elapsed);
800
+ }
801
+ if (i > 0 && i % GREP_LINE_CHECK_EVERY === 0) {
802
+ await yieldEventLoop();
803
+ if (signal?.aborted) {
804
+ scanAborted = true;
805
+ break;
806
+ }
807
+ }
808
+ if (grayPattern && lines[i].length > longLineLimit) {
809
+ skippedLongLines++;
810
+ continue;
811
+ }
415
812
  if (!test(lines[i]))
416
813
  continue;
417
814
  fileCount++;
@@ -434,6 +831,8 @@ export async function jsGrep(env, root, p, signal) {
434
831
  }
435
832
  }
436
833
  }
834
+ if (scanAborted)
835
+ break;
437
836
  if (mode === "content")
438
837
  totalContent += fileCount;
439
838
  if (fileCount > 0) {
@@ -444,12 +843,16 @@ export async function jsGrep(env, root, p, signal) {
444
843
  counts.push(`${relOut(f)}:${fileCount}`);
445
844
  }
446
845
  }
447
- if (scanAborted || skippedUnreadableFiles > 0) {
846
+ if (scanAborted || skippedUnreadableFiles > 0 || skippedLongLines > 0 || multilineSkippedFiles > 0) {
448
847
  const extra = [];
449
848
  if (scanAborted && !walked.aborted)
450
849
  extra.push("the search was interrupted before finishing");
451
850
  if (skippedUnreadableFiles > 0)
452
851
  extra.push(`${skippedUnreadableFiles} file(s) could not be read and were skipped`);
852
+ if (skippedLongLines > 0)
853
+ extra.push(`${skippedLongLines} line(s) over ${longLineLimit} chars were not matched (the pattern's worst-case matching cost grows superlinearly with line length on the fallback engine)`);
854
+ if (multilineSkippedFiles > 0)
855
+ extra.push(`${multilineSkippedFiles} file(s) skipped by multiline matching (content over ${longLineLimit} chars; the pattern's worst-case matching cost grows superlinearly with input length on the fallback engine)`);
453
856
  if (extra.length > 0) {
454
857
  caveat = caveat.length > 0
455
858
  ? caveat.replace(/\]$/, `; ${extra.join("; ")}]`)
@@ -514,11 +917,9 @@ async function jsGrepFallback(env, root, p, signal, reason) {
514
917
  };
515
918
  }
516
919
  export async function rgGrepDetailed(env, root, p, signal) {
517
- const mode = p.output_mode ?? "content";
920
+ const mode = p.output_mode ?? "files_with_matches";
518
921
  const flags = ["--no-messages", "--no-require-git"];
519
922
  flags.push("--max-columns", "500", "--max-columns-preview");
520
- if (!p.regex)
521
- flags.push("-F");
522
923
  if (p.ignore_case)
523
924
  flags.push("-i");
524
925
  if (p.multiline)
@@ -601,7 +1002,14 @@ export async function runGlobDetailed(env, root, pattern, opts = {}, signal) {
601
1002
  const re = globTokenToRegExp(pattern, false, true);
602
1003
  const rel = (abs) => (abs.startsWith(rootPrefix) ? abs.slice(rootPrefix.length) : abs);
603
1004
  const cap = Math.max(1, Math.floor(opts.max ?? 500));
604
- const matchedAll = files.filter((f) => re.test(rel(f)) || re.test(basename(f))).map(rel).sort();
1005
+ const matchedAbs = files.filter((f) => re.test(rel(f)) || re.test(basename(f)));
1006
+ const haveMtimes = matchedAbs.length > 0 && matchedAbs.every((f) => walked.mtimes.has(f));
1007
+ const matchedAll = haveMtimes
1008
+ ? matchedAbs
1009
+ .map((f) => ({ r: rel(f), m: walked.mtimes.get(f) }))
1010
+ .sort((a, b) => b.m - a.m || (a.r < b.r ? -1 : a.r > b.r ? 1 : 0))
1011
+ .map((x) => x.r)
1012
+ : matchedAbs.map(rel).sort();
605
1013
  const matched = matchedAll.slice(0, cap);
606
1014
  const totalMatches = matchedAll.length;
607
1015
  const countIsComplete = !walkIsPartial(walked);