Skip to content

fix(voyage): close response body stream when batch output JSONL parsing throws#98840

Merged
openclaw-clownfish[bot] merged 1 commit into
openclaw:mainfrom
solodmd:fix/voyage-stream-cleanup-embedding-batch
Jul 6, 2026
Merged

fix(voyage): close response body stream when batch output JSONL parsing throws#98840
openclaw-clownfish[bot] merged 1 commit into
openclaw:mainfrom
solodmd:fix/voyage-stream-cleanup-embedding-batch

Conversation

@solodmd

@solodmd solodmd commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

What Problem This Solves

The Voyage batch output file download path (extensions/voyage/embedding-batch.ts) reads the HTTP response body via readline.createInterface() over Readable.fromWeb(). If JSON.parse throws on a malformed JSONL line from the Voyage API, the for await loop exits via exception — but the readline interface and underlying Readable stream were never explicitly closed or destroyed, leaving the HTTP response body stream dangling.

Why This Change Was Made

reader.close() alone does not destroy the underlying input stream (Node.js Interface.close() only sets a flag and removes listeners; the Readable is not cleaned up). The fix wraps the iteration in try-finally so both reader.close() and inputStream.destroy() are always called.

The stream reading is extracted into a readBatchOutputContent helper function, exported via the test-only testing object, so the cleanup path has direct test coverage.

User Impact

Low probability (malformed JSONL from Voyage is rare), but a genuine resource leak on every occurrence. Users running large-scale Voyage embedding batches are most likely to encounter this.

Evidence

Real behavior proof — a standalone Node.js script that reproduces the exact leak pattern and confirms the fix:

$ node proof-stream-leak.mjs
OLD pattern — stream cancelled after JSON.parse throw? false
NEW pattern — stream cancelled after JSON.parse throw? true

The old code path (no try-finally) leaves the underlying ReadableStream dangling after JSON.parse throws. The new code path (reader.close() + inputStream.destroy() in a finally block) properly cancels the stream.

  • pnpm test extensions/voyage/embedding-batch.test.ts — 11 passed
  • Stream cancellation test gated to Linux — verifies the fix end-to-end through readBatchOutputContent with an HTTP Response containing malformed JSONL body

Side Effects

None. The refactored code paths are functionally identical — only the error-exit cleanup is added.

🤖 Generated with Claude Code

…ng throws

The batch output file download path creates a readline interface over a
Readable.fromWeb() response body stream. If JSON.parse throws on a malformed
JSONL line, the for-await loop exits via exception but the readline interface
and underlying Readable stream were never explicitly closed or destroyed,
leaving the HTTP response body stream dangling.

Extract the stream reading into , a testable helper
that wraps the iteration in a try-finally so both reader.close() and
inputStream.destroy() are always called, matching the pattern established in
openclaw#98493 for the same class of leak.
@clawsweeper

clawsweeper Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge. Reviewed July 1, 2026, 10:02 PM ET / 02:02 UTC.

Summary
The PR extracts Voyage batch output JSONL reading into a helper that closes the readline interface and destroys the response body stream in a finally block, with tests for null bodies, valid lines, and malformed-line cancellation.

PR surface: Source +24, Tests +93. Total +117 across 2 files.

Reproducibility: yes. at source/proof level: current main and v2026.6.11 parse Voyage batch output JSONL inside a readline loop without a cleanup finally, and the PR body includes terminal output showing old versus new stream cancellation behavior. I did not run the targeted test locally because this review was read-only.

Review metrics: none identified.

Stored data model
Persistent data-model change detected: serialized state: extensions/voyage/embedding-batch.test.ts, serialized state: extensions/voyage/embedding-batch.ts, unknown-data-model-change: extensions/voyage/embedding-batch.test.ts, unknown-data-model-change: extensions/voyage/embedding-batch.ts, vector/embedding metadata: extensions/voyage/embedding-batch.test.ts, vector/embedding metadata: extensions/voyage/embedding-batch.ts. Confirm migration or upgrade compatibility proof before merge.

Merge readiness
Overall: 🐚 platinum hermit
Proof: 🐚 platinum hermit
Patch quality: 🐚 platinum hermit
Result: ready for maintainer review.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Rank-up moves:

  • none.

Next step before merge

  • No ClawSweeper repair is needed; the PR is already the narrow fix and this review found no blocking defect.

Security
Cleared: The diff only changes Voyage response-stream cleanup and tests; it adds no dependencies, workflows, permissions, package scripts, credential handling, or supply-chain surface.

Review details

Best possible solution:

Land the focused Voyage plugin cleanup once exact-head required checks complete, keeping broader output-reader convergence as a separate follow-up if maintainers want it.

Do we have a high-confidence way to reproduce the issue?

Yes at source/proof level: current main and v2026.6.11 parse Voyage batch output JSONL inside a readline loop without a cleanup finally, and the PR body includes terminal output showing old versus new stream cancellation behavior. I did not run the targeted test locally because this review was read-only.

Is this the best way to solve the issue?

Yes. This is the narrow plugin-owned fix for the leak path; a shared parser refactor or OpenAI-style output-budget change would be broader than needed for this cleanup bug.

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against 313560d5b98e.

Label changes

Label justifications:

  • P2: This is a bounded Voyage provider resource-leak fix with limited blast radius and no evidence of an urgent live outage.
  • rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🐚 platinum hermit and patch quality is 🐚 platinum hermit.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (terminal): The PR body includes copied terminal output from an after-fix Node proof showing stream cancellation changes from false to true, plus targeted Voyage test output.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR body includes copied terminal output from an after-fix Node proof showing stream cancellation changes from false to true, plus targeted Voyage test output.
Evidence reviewed

PR surface:

Source +24, Tests +93. Total +117 across 2 files.

View PR surface stats
Area Files Added Removed Net
Source 1 38 14 +24
Tests 1 95 2 +93
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 2 133 16 +117

What I checked:

Likely related people:

  • steipete: git log -S runVoyageEmbeddingBatches shows the Voyage batch runner was introduced when memory embeddings moved into provider plugins. (role: feature/refactor origin; confidence: high; commits: 77e6e4cf87f7; files: extensions/voyage/embedding-batch.ts, extensions/voyage/memory-embedding-adapter.ts, extensions/openai/embedding-batch.ts)
  • Alix-007: Authored the merged Voyage bounded-response PR that touched the same Voyage batch module and established adjacent untrusted-response handling in this file. (role: recent sibling hardening contributor; confidence: medium; commits: d3620da3e031; files: extensions/voyage/embedding-batch.ts, extensions/voyage/embedding-batch.test.ts)
  • sunlit-deng: Authored the merged OpenAI embedding batch output streaming hardening, a sibling provider path with similar output-file cancellation concerns. (role: adjacent provider-output contributor; confidence: medium; commits: ce4a259485f2; files: extensions/openai/embedding-batch.ts, extensions/openai/embedding-batch.test.ts)
  • vincentkoc: The latest release commit carried the Voyage batch files that still show the pre-fix cleanup gap in shipped behavior. (role: recent area contributor; confidence: low; commits: e085fa1a3ffd; files: extensions/voyage/embedding-batch.ts, extensions/voyage/memory-embedding-adapter.ts)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

How this review workflow works
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. P2 Normal backlog priority with limited blast radius. labels Jul 2, 2026
@openclaw-clownfish
openclaw-clownfish Bot merged commit 1ec42c9 into openclaw:main Jul 6, 2026
144 of 151 checks passed
vincentkoc added a commit to zhangqueping/openclaw that referenced this pull request Jul 6, 2026
* origin/main: (1287 commits)
  fix(android): block loopback canvas navigation (openclaw#99874)
  fix(hooks): suppress unhandled stdout/stderr stream errors in gmail watcher (openclaw#100519)
  fix(memory-core): guard qmd mcporter JSON.parse against non-JSON stdout (openclaw#98381)
  fix(build): fall back to tsx for build TypeScript scripts (openclaw#91262)
  feat(skills): suggest saving detected reusable workflows by default (openclaw#95477) (openclaw#100692)
  docs(changelog): remove generated release-note entries
  feat(telegram): offer BotFather web app flow in setup help and docs (openclaw#100540)
  fix(ios): unify Talk and Settings row typography on one branded detail row (openclaw#100515)
  feat(gateway): add persisted crash-loop breaker and fatal-config exit contract
  refactor(macos): lock and unify PortGuardian tunnel record persistence so concurrent app instances cannot lose orphan records (openclaw#100601)
  fix: stop reconnecting on protocol mismatch (openclaw#98414)
  fix(maint): reuse recent hosted gates after rebase (openclaw#100663)
  fix(ui): reopen web terminals without stale content (openclaw#100665)
  fix(browser): diagnose empty WSL2 Chrome replies (openclaw#100590)
  fix(ios): chat snaps back to bottom when scrolling to top via status-bar tap (openclaw#100502)
  Treat already-compacted CLI compaction as no-op (openclaw#99136)
  docs(changelog): remove direct main fix entry
  fix(feishu): strip internal tool-trace banners from outbound text (openclaw#98705)
  fix(message): thread --limit through to CLI formatter and surface provider pagination hints (openclaw#99089)
  fix(voyage): close response body stream when batch output JSONL parsing throws (openclaw#98840)
  ...

# Conflicts:
#	extensions/memory-wiki/package.json
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request Jul 6, 2026
…ng throws (openclaw#98840)

The batch output file download path creates a readline interface over a
Readable.fromWeb() response body stream. If JSON.parse throws on a malformed
JSONL line, the for-await loop exits via exception but the readline interface
and underlying Readable stream were never explicitly closed or destroyed,
leaving the HTTP response body stream dangling.

Extract the stream reading into , a testable helper
that wraps the iteration in a try-finally so both reader.close() and
inputStream.destroy() are always called, matching the pattern established in
openclaw#98493 for the same class of leak.
giodl73-repo pushed a commit to giodl73-repo/openclaw that referenced this pull request Jul 8, 2026
…ng throws (openclaw#98840)

The batch output file download path creates a readline interface over a
Readable.fromWeb() response body stream. If JSON.parse throws on a malformed
JSONL line, the for-await loop exits via exception but the readline interface
and underlying Readable stream were never explicitly closed or destroyed,
leaving the HTTP response body stream dangling.

Extract the stream reading into , a testable helper
that wraps the iteration in a try-finally so both reader.close() and
inputStream.destroy() are always called, matching the pattern established in
openclaw#98493 for the same class of leak.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

P2 Normal backlog priority with limited blast radius. proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. size: S status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant