Skip to content

fix: honor configured deadlines for Agent streams - #145

Open
grantjayy wants to merge 1 commit into
perplexityai:mainfrom
grantjayy:fix/agent-stream-termination
Open

grantjayy wants to merge 1 commit into
perplexityai:mainfrom
grantjayy:fix/agent-stream-termination

Conversation

@grantjayy

Copy link
Copy Markdown

Problem

Agent requests have an absolute PERPLEXITY_TIMEOUT_MS deadline, but native fetch and default ProxyAgent inherit Undici's independent 300000ms headers/body-idle limits. A valid quiet stream can fail before a longer configured deadline.

Streaming errors also discard error.cause.code, making body timeouts and peer socket loss indistinguishable. Both reproduce TypeError: terminated. A historical 401-second interruption cannot be attributed conclusively because that evidence was discarded.

Solution

Give each Agent call a dispatcher whose headers/body limits match the application deadline. Destroy that dispatcher after timer/listener cleanup. Retain only allowlisted UND_ERR_* cause codes in streaming errors.

Scope

Only Agent transport configuration and regression tests change. Public tools, schemas, presets, proxy precedence, search/cancel behavior and connect timeout remain unchanged. No retry, background API migration, new dependency, global dispatcher change, or version bump.

Validation

  • npm test: 105/105 pass, five files, no skipped tests.
  • npm run build: pass.
  • npm pack --dry-run: no compiled tests in package.
  • New wire tests against unchanged base: eight expected defect failures; seven guards pass. After correction, all 15 pass.
  • Packaged stdio checks on Node24 and actual Node22: delayed headers, silent body, socket loss, and deadline cancellation pass. Each call submits one Agent POST; timeout submits one cancel.
  • Heavy independent review: two first-wave reviewers; one confirmed missing search network-error test repaired. Fresh incremental review passes with no unresolved medium+ findings.

Evidence and context

Related prior reports: #51 (transport timeout surviving application timeout), #110/#122 (MCP client progress), #83/#85 (old Sonar streaming). This correction addresses the current Agent transport path, not those older paths.

Base: c73c8561bbc2d9eb666334a53c311b50f4f4cf76. Reviewed head: e2ce829ba490f0fd190e8cdea78588f79018c2d6.

Local untracked plan: ~/.hermes/plans/modelcontextprotocol/2026-09-16-agent-stream-termination.md; canonical SHA256 5ad3982b8995c014ccd9ac5359dc6026bf809e369c9b01e2841aeb525e46f380 (declared digest token replaced with PENDING).

Risk

Genuine provider/socket interruptions remain errors; accepted requests are never blindly resubmitted. Per-call dispatchers sacrifice cross-call keep-alive reuse for explicit lifecycle and independently configured deadlines.

Notes / accepted debt

  • Invalid proxy settings can now return bare Invalid URL rather than the previous prefixed error; still fail closed.
  • Existing malformed-timeout warning remains unchanged.
  • Fixture assertions use real HTTP and CONNECT paths with shortened inherited defaults; some ancillary timing/count assertions are covered by separate packaged acceptance.
  • Existing search/cancel proxy lifecycle remains outside this Agent-only change.
  • Destroy-error masking was rejected as a false positive after inspection of Undici's resolving destroy implementation.
  • Cancel interference was rejected: cancel uses an independent dispatcher and the wire checks record the cancel.

Attribution and delivery

Standard implementation: GJC investigative/direct. Test-only repair: GJC procedural/direct. Heavy review: Claude Fable 5.1, high effort. One repair round. Human-facing PR text: Oren.

Open and unmerged. No upstream merge requested.

Agent calls used an absolute PERPLEXITY_TIMEOUT_MS deadline but inherited
Undici's independent 300-second headers and body-idle limits. Valid quiet
streams could therefore stop before a longer configured deadline.

Use a per-call dispatcher with aligned transport limits for Agent requests
only, and destroy the dispatcher after timer and abort-listener cleanup.
Preserve safe Undici cause codes to distinguish idle timeouts from socket
loss without exposing nested metadata. Do not retry accepted requests.

Validated:
- Eight new defect tests fail on the unchanged base; seven guards pass.
- Full suite: 105 tests pass. TypeScript build passes.
- Packaged stdio checks pass on Node 24 and actual remote Node 22:
  delayed headers, silent body, socket loss, and timeout cancellation.
- Search network-error regression added after independent review.

The historical interruption cannot be attributed conclusively because the
old error discarded its nested cause. Genuine network failures remain
errors, not automatically resubmitted requests.
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.

1 participant