Skip to content

fix(tasks): bound sedLines with a search deadline and restore the node export surface - #849

Merged
sroussey merged 1 commit into
mainfrom
claude/zealous-allen-ata6g5-sed-deadline-exports
Aug 20, 2026
Merged

fix(tasks): bound sedLines with a search deadline and restore the node export surface#849
sroussey merged 1 commit into
mainfrom
claude/zealous-allen-ata6g5-sed-deadline-exports

Conversation

@sroussey

Copy link
Copy Markdown
Collaborator

Two independent defects in the grep/sed pair in @workglow/tasks.

1 — sedLines had no overall search deadline

grepLines computes deadline = Date.now() + DEFAULT_LIMITS.grepMaxSearchMs and breaks when it passes. sedLines had no equivalent: its only bounds were the per-batch substituter timeout and the output caps. Under onlyChangedLines: true the cap checks are skipped for unchanged lines — the continue fires before the maxOutputLines / maxOutputChars tests — so a run that changes nothing had no stopping condition but EOF. FileSedTask.server.ts feeds sedLines a linesFromStream, so this was a real unbounded local-file scan:

fileSed({ url: "/var/log/huge.log", pattern: "NEVERMATCHES", replacement: "x", onlyChangedLines: true })

On a 100 GB file that streams the whole file, holds a task slot for hours, and then reports truncated: false.

The fix reuses DEFAULT_LIMITS.grepMaxSearchMs rather than adding a limits key — sedLines already reuses grepMaxLineChars, grepMaxOutputLines and grepMaxOutputChars, and a new key would drag a @workglow/util change into a @workglow/tasks fix.

Why the check sits where it does. It is placed after the existing abort check and before the budget computation — that is, after the batch has been filled from the iterator and before it is substituted, identical to grep. Because the batch is non-empty and entirely unprocessed at that point, something was definitively left unread, so truncated = true is unconditionally correct.

Why sed needs no sawMoreInput reconciliation. Grep carries that extra step because its stop reasons include the output caps, which reject the line they stopped on — grep therefore has to look ahead to decide whether the early stop actually dropped anything. The deadline is not such a reason: it stops on a whole unprocessed batch, so the answer is already known and no lookahead is needed.

2 — node/electron dropped most of the grep/sed public surface

browser.ts does export * from "./task/FileGrepTask" and "./task/FileSedTask". The server entrypoints only export * from the two .server files (which re-export just the Config/Input/Output types, the class and the helper fn) plus a hand-picked { grepLines, linesFromText } — and nothing at all from sed.

Missing from both node.ts and electron.ts: GrepOptions, GrepLineMatcher, createMatcher, SedOptions, SedBatchResult, SedLineSubstituter, createSedRegex, createSedExpander, createSubstituter, expandReplacement, sedLines.

So import type { SedOptions } from "@workglow/tasks" type-checked and ran under the browser condition and failed to resolve under node — a build that works in the web example and breaks in the CLI and Electron shells.

The export lists must stay explicit: export * from both grep modules would collide on FileGrepTask / fileGrep, and both sed modules on FileSedTask / fileSed. The remaining names were checked against common.ts and the .server files and collide with nothing.

A structurally better fix — lifting the platform-neutral helpers into grepLines.ts / sedLines.ts that both entrypoints export * — removes the drift permanently but touches every importer, so it was deliberately deferred. The new guard test stands in for it: it fails to type-check and at runtime the moment a name is dropped from an entrypoint again.

Tests

packages/test/src/test/task/FileSedTask.test.ts

  • stops at the search deadline under onlyChangedLines — drives sedLines directly with a finite counting generator against a non-matching pattern. vi.useFakeTimers() advances past DEFAULT_LIMITS.grepMaxSearchMs while the first batch is being filled, so the deadline trips at a batch boundary. Asserts truncated === true and that the generator was abandoned at SECURITY_LIMITS.regexMatchBatchLines lines rather than drained. The fixture is finite on purpose, so a regression fails on the assertions instead of wedging CI.
  • does not report truncation for a short unchanged run — a 10-line non-matching onlyChangedLines run still returns truncated: false, text: "", guarding the new check from firing where nothing was dropped.

New packages/test/src/test/task/TasksNodeExports.test.ts — static named imports of all eleven symbols from @workglow/tasks, plus type-only uses (const grepOptions: GrepOptions = {}, const sedOptions: SedOptions = {}). This fails both to type-check and at runtime on main.

Verification

Both new tests were confirmed to fail without the corresponding fix:

  • with the deadline removed (exports fix in place): stops at the search deadline under onlyChangedLines fails expected false to be true in 38 ms — bounded, not hung — while the short-run test still passes;
  • with the entrypoints reverted: TasksNodeExports.test.ts fails with createMatcher is not a function / typeof createSedExpander === "undefined".

With both fixes:

$ bun scripts/test.ts task vitest
Running all tests in sections [task] — 75 file(s)
 Test Files  75 passed (75)
      Tests  1229 passed | 24 skipped (1253)
   Duration  194.81s

$ bun run typecheck:tests
typecheck packages/ai/tsconfig.test.json
typecheck packages/job-queue/tsconfig.test.json
typecheck packages/knowledge-base/tsconfig.test.json
typecheck packages/mcp/tsconfig.test.json
typecheck packages/storage/tsconfig.test.json
typecheck packages/task-graph/tsconfig.test.json
typecheck packages/util/tsconfig.test.json

packages/test has no tsconfig.test.json, so that script does not cover the new test files; they are type-checked by packages/test's own tsconfig.json (include: ["src/**/*"]), run clean via tsgo and as part of bun run build:packages. bun run format reports All matched files use Prettier code style!.


🤖 Generated with Claude Code

https://claude.ai/code/session_01LowBJQsCghLDiHwPN6FgUT


Generated by Claude Code

…e export surface

`sedLines` had no overall search deadline. Its only bounds were the
per-batch substituter timeout and the output caps — and under
`onlyChangedLines` the cap checks are skipped for unchanged lines, so a
run that changes nothing had no stopping condition but EOF.

The deadline is checked after the batch is filled and before it is
substituted, identical to grep. Because the batch is non-empty and
unprocessed at that point, `truncated = true` is unconditionally correct,
so sed needs no equivalent of grep's `sawMoreInput` reconciliation.

The node and electron entrypoints also dropped eleven grep/sed names that
`browser.ts` exports, so an import that resolved under the browser
condition failed to resolve under node.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LowBJQsCghLDiHwPN6FgUT
@github-actions

Copy link
Copy Markdown

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 58.65% 33251 / 56687
🔵 Statements 58.34% 34769 / 59594
🔵 Functions 59.87% 6446 / 10766
🔵 Branches 47.22% 16861 / 35703
File CoverageNo changed files found.
Generated in workflow #3250 for commit 7378f12 by the Vitest Coverage Report Action

@sroussey
sroussey merged commit cd94d42 into main Aug 20, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants