security/correctness fixes from automated review (High)#506
Merged
Conversation
The Proxy-based context wrapper hid the wrapped object's true identity from callers that key on reference (e.g. WeakMap caches): repeated reads from the proxy returned new transparent views rather than a single stable wrapper. Switching to a plain shallow spread gives the strategy a single, identity-stable wrapper while still substituting `signal` with the local AbortController. Behaviour is otherwise unchanged: every non-`signal` field still aliases the original context by reference.
Previously, if a provider's strategy.execute() rejected after the stream had begun, the rejection bubbled out before result() ran, so callers saw the original provider error. If the provider resolved cleanly but the stream never produced a finish event, result() threw a generic Error with no way to distinguish that case from any other failure. This commit: - Wraps strategy.execute() and result() in separate try/catch blocks in AiTask.execute. The original provider error is preserved on the thrown TaskError's `cause` when materialisation also fails; if materialisation succeeds against a partial accumulation, the original provider error is rethrown verbatim so error-shape expectations stay stable. - Tags the materialise error with `code: "ACCUMULATOR_NO_FINISH"` and a diagnostic `lastEventType` field on StreamEventAccumulator. AiTask forwards both fields through to the wrapped TaskError so callers can branch on the shape.
Previously, the cap-eviction loops in `recordPendingAbort` and `scheduleCompletedRequestCleanup` iterated the live `Set` via `set.values()` and called `set.delete(entry.value)` from inside the loop. This is technically permitted by the Map/Set spec, but couples the iterator's internal cursor to mutation and is a perennial bug magnet — easy to misread, easy to break in a future refactor. Switch to snapshot-then-delete: `Array.from(set).slice(0, EVICT_BATCH)` captures the eviction list FIFO (Set preserves insertion order), then a plain `for ... of` loop deletes each. Extract `HARD_CAP` (1000) and `EVICT_BATCH` (500) as named module-level constants so the policy parameters are reviewable in one place.
`simpleRerank` constructed `new RegExp(word, "gi")` directly from user-supplied query tokens. Tokens containing regex metacharacters (`foo(`, `\\`, `*abc`, `[`, …) caused `new RegExp` to throw `SyntaxError: Invalid regular expression`, surfacing a 500-shaped failure in what is supposed to be a literal-keyword count. Add a local `escapeRegExp` helper (matches the canonical TC39 `RegExp.escape` pattern) and apply it before building the regex. Also defensively skip empty / whitespace-only tokens so the contract is robust to upstream changes.
@workglow/cli
@workglow/ai
@workglow/browser-control
@workglow/indexeddb
@workglow/javascript
@workglow/job-queue
@workglow/knowledge-base
@workglow/mcp
@workglow/storage
@workglow/task-graph
@workglow/tasks
@workglow/util
workglow
@workglow/anthropic
@workglow/bun-webview
@workglow/chrome-ai
@workglow/electron
@workglow/google-gemini
@workglow/huggingface-inference
@workglow/huggingface-transformers
@workglow/node-llama-cpp
@workglow/ollama
@workglow/openai
@workglow/playwright
@workglow/postgres
@workglow/sqlite
@workglow/supabase
@workglow/tf-mediapipe
commit: |
Coverage Report
File CoverageNo changed files found. |
Contributor
There was a problem hiding this comment.
Pull request overview
This PR applies focused security/correctness fixes across AI streaming/task execution, reranking, and worker request bookkeeping, with targeted Vitest coverage for the regressions.
Changes:
- Replaces proxy-based streaming context wrapping with a shallow clone for stable identity.
- Adds no-finish error tagging/classification for AI stream accumulation and provider failures.
- Escapes reranker query tokens before regex construction and snapshots worker eviction batches before deletion.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
packages/ai/src/task/base/runWithIterable.ts |
Uses a shallow-cloned execution context with a substituted local abort signal. |
packages/ai/src/task/base/AiTask.ts |
Separates provider execution errors from stream materialization errors and wraps no-finish failures. |
packages/ai/src/capability/StreamEventAccumulator.ts |
Adds ACCUMULATOR_NO_FINISH diagnostics and tracks the last observed event type. |
packages/ai/src/task/RerankerTask.ts |
Escapes query tokens before constructing keyword-match regexes. |
packages/util/src/worker/WorkerServerBase.ts |
Extracts eviction constants and snapshots Set entries before deleting capped bookkeeping entries. |
packages/test/src/test/ai/streaming-abort.test.ts |
Adds coverage for stable wrapped context identity and local abort propagation. |
packages/test/src/test/ai/StreamEventAccumulator.test.ts |
Adds coverage for no-finish error code and lastEventType. |
packages/test/src/test/ai/AiTask.providerError.test.ts |
Adds provider-error vs no-finish classification coverage. |
packages/test/src/test/ai/RerankerTask.simpleRerank.test.ts |
Adds regression tests for regex metacharacter queries. |
packages/test/src/test/util/WorkerServerBase.eviction.test.ts |
Adds FIFO eviction behavior coverage for pending abort markers. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Skip pure-whitespace / empty tokens (defensive: split(/\s+/) + | ||
| // .filter(w => w.length > 0) above should already exclude them, but | ||
| // make the contract local). | ||
| if (word.length === 0 || /^\s+$/.test(word)) continue; |
Collaborator
Author
|
@copilot apply changes based on the comments in this thread |
Agent-Logs-Url: https://github.com/workglow-dev/libs/sessions/4e18de91-3eec-49ac-b62f-ce4d286fe0a3 Co-authored-by: sroussey <127349+sroussey@users.noreply.github.com>
Contributor
Applied the review-thread change in 381bb46: |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Four High-severity security/correctness fixes from an automated review. Each fix is a single self-contained commit with a focused vitest suite.
Fixes applied
1.
fix(ai): replace runWithIterable Proxy with shallow clonepackages/ai/src/task/base/runWithIterable.tsProxy(context, { get })wrapper hid the wrapper's true identity from callers that key on reference (e.g.WeakMapcaches). Repeated reads through the proxy returned new transparent views rather than a single stable wrapper.const wrappedContext: IExecuteContext = { ...context, signal: localAbort.signal };. Behaviour is otherwise unchanged: every non-signalfield still aliases the original context by reference. Tests assertWeakMap-keyed identity stability and thatlocalAbortpropagates viawrappedContext.signal.2.
fix(ai): classify provider-error vs no-finish in AiTask.executepackages/ai/src/task/base/AiTask.ts,packages/ai/src/capability/StreamEventAccumulator.tsresult()now runs in its own try/catch. The original provider error is preserved on the thrownTaskError's.cause(and rethrown verbatim when materialise succeeded against a partial accumulation, so existing error-shape expectations stay stable).StreamEventAccumulator.materializetags no-finish failures withcode: "ACCUMULATOR_NO_FINISH"and a diagnosticlastEventTypefield.AiTask.executepropagates both fields onto the wrappedTaskError. ExportedACCUMULATOR_NO_FINISHconstant for consumers that branch on it.3.
fix(util): snapshot-then-delete eviction in WorkerServerBase Set capspackages/util/src/worker/WorkerServerBase.tsrecordPendingAbortandscheduleCompletedRequestCleanupiterated the liveSetviaset.values()and calledset.delete()from inside the loop. Permitted by spec but a perennial bug magnet.const evict = Array.from(set).slice(0, EVICT_BATCH); for (const item of evict) set.delete(item);. ExtractedHARD_CAP(1000) andEVICT_BATCH(500) as named module-level constants. Tests assert FIFO eviction order and that concurrent insert+evict workloads do not throw.4.
fix(ai): escape regex metacharacters in RerankerTask.simpleRerankpackages/ai/src/task/RerankerTask.tssimpleRerankconstructednew RegExp(word, "gi")directly from query tokens. Tokens containing regex metacharacters (foo(,\\,*abc,[, …) causednew RegExpto throwSyntaxError: Invalid regular expression.escapeRegExphelper (canonical TC39RegExp.escapepattern) and apply it before building the regex; defensively skip empty / whitespace-only tokens. Parametrised tests cover unbalanced-paren, lone-backslash, leading-quantifier, bracket-open, mixed, unicode, whitespace-only, and empty-after-split inputs.Deferred fixes
Each item below was checked against the current
mainhead and deferred because the target file/symbol does not exist there yet (and the plan instructed us to defer if the target only exists in an unmerged PR branch):WorkerServerBasehaspendingAbortsandconsumePendingAbort, but noPENDING_ABORT_TTL_MSconstant or time-based expiry path onmain. Apply when fix(util/worker, ai/task): TTL-based pendingAborts eviction; clarify runWithIterable bond #500 lands.monotonicNowMshelper) — depends on fix(util/worker, ai/task): TTL-based pendingAborts eviction; clarify runWithIterable bond #500 H1; nothing to scope down onmain.scoreThresholdschemaminimum: 0) —KbSearchTask.tsexists but its input schema has noscoreThresholdproperty. Apply when fix(ai): rerank candidate floor + KbSearchTask scoreThreshold forwarding #496 lands.createStandardKbStrategy.tsdoes not exist onmain.chunk_idfield-name mismatch intoInsertChunkEntities) —toInsertChunkEntitiesdoes not exist anywhere inpackages/onmain.OnSearchCallback(and friends) still exist onmain; no deprecation needed yet.firstStageTopK/vectorWeightthroughstrategy.search) — no strategy abstraction onmain(createStandardKbStrategy.tsmissing);ISearchOptionsalready exposes the fields directly onKnowledgeBase, but the strategy plumbing this fix targets is not yet present.IKbAiStrategy) —packages/knowledge-base/src/knowledge-base/IKbAiStrategy.tsdoes not exist onmain.Test plan
bun install --frozen-lockfilesucceeded.turbo run build-types --filter=@workglow/ai --filter=@workglow/util --filter=@workglow/knowledge-basepassed.bun run build:packages(full Turbo build) succeeded.vitest run:packages/test/src/test/ai/streaming-abort.test.ts(5 tests, including 2 newrunWithIterableidentity / abort tests)packages/test/src/test/ai/StreamEventAccumulator.test.ts(10 tests, including 2 newACCUMULATOR_NO_FINISHtag tests)packages/test/src/test/ai/AiTask.providerError.test.ts(NEW, 2 tests)packages/test/src/test/util/WorkerServerBase.race.test.ts(3 existing tests still pass)packages/test/src/test/util/WorkerServerBase.eviction.test.ts(NEW, 2 tests)packages/test/src/test/ai/RerankerTask.simpleRerank.test.ts(NEW, 9 parametrised tests)AiTaskPhases,AiTask.requires,AiTaskSchemas,AiChatTask,accumulatingEmit,collectStream,AiJob_runFn) all pass — 57/57 green.Notes on reviewer-visible deltas
StreamEventAccumulatornow tracks alastEventTypefield; this is purely additive and is included in the no-finish error message ("… (lastEventType=phase)."). Existing tests that match/finish/icontinue to pass.TaskErrorfromAiTask.executecarriescause,code, andlastEventTypeas own properties (the project'sBaseErrordoes not declare acauseslot in its constructor signature, so they are attached after construction); this is a documented pattern used elsewhere in the codebase for diagnostic enrichment.WorkerServerBaseintroduces two module-level constants (HARD_CAP = 1000,EVICT_BATCH = 500); the 1000/500 values match the previous inline magic numbers exactly, so behaviour is unchanged at the cap.https://claude.ai/code/session_01VMmVCK1hBYWtq6Up8cxiAh
Generated by Claude Code