Skip to content

FE-1218: Fix Petrinaut optimization streaming backpressure and cancellation latency#9057

Merged
kube merged 1 commit into
mainfrom
cf/fe-1218-petrinaut-optimization-streaming-cancellation
Jul 19, 2026
Merged

FE-1218: Fix Petrinaut optimization streaming backpressure and cancellation latency#9057
kube merged 1 commit into
mainfrom
cf/fe-1218-petrinaut-optimization-streaming-cancellation

Conversation

@kube

@kube kube commented Jul 19, 2026

Copy link
Copy Markdown
Collaborator

🌟 What is the purpose of this PR?

Hardens the Petrinaut optimization streaming and cancellation path from #9040 so a slow or disconnecting client cannot leak NodeAPI listeners, promises, or optimizer capacity, and so cancelling a busy study terminates its CLI promptly. Also restores and regression-tests propagation of the optimizer's 429 / Retry-After through NodeAPI.

🔗 Related links

🔍 What does this change?

  • NodeAPI (create-petrinaut-optimization-handler.ts): races the backpressure wait against drain, close, and the request abort signal, removing the losing listeners via an internal AbortController — no accumulated listeners, no orphaned rejected promise, no slot held past the timeouts.
  • Marks a terminal event as committed at write hand-off, so a timeout during the final backpressured write cannot append a second, contradictory terminal event; the failure-path terminal write is best-effort and never blocks teardown; heartbeats skip while the buffer needs draining.
  • Optimizer (petrinaut_client.py): close(graceful=False) signals the CLI process group immediately instead of waiting up to 5s for stdin EOF a mid-trial CLI never observes; the graceful EOF wait is reserved for studies that completed normally. An idempotent per-run cleanup shared between the stream wrapper and a StreamingResponse background task covers a client that aborts before the body is pulled.
  • Optimizer emits Retry-After on the 429 study-limit response (declared in the OpenAPI spec); the client preserves status + Retry-After, and NodeAPI passes them through.

Pre-Merge Checklist 🚀

🚢 Has this modified a publishable library?

This PR:

  • does not modify any publishable blocks or libraries, or modifications do not need publishing

📜 Does this require a change to the docs?

The changes in this PR:

  • are internal and do not require a docs change

🕸️ Does this require a change to the Turbo Graph?

The changes in this PR:

  • do not affect the execution graph

⚠️ Known issues

🛡 What tests cover this?

New/updated unit tests in apps/hash-api, apps/petrinaut-opt, and @local/petrinaut-optimizer-client covering: drain-first, close-first, client abort, idle timeout during backpressure, single-terminal-event, heartbeat suppression under backpressure, prompt process-group termination (fake and real subprocess), slot released exactly once (including never-started stream generators and double cancellation), and 429 / Retry-After propagation end to end.

❓ How to test this?

  1. cd apps/petrinaut-opt && uv run pytest
  2. turbo run test:unit --filter '@apps/hash-api' --filter '@local/petrinaut-optimizer-client'
  3. Confirm the streaming/cancellation and 429 suites pass.

…lation latency

NodeAPI (apps/hash-api):
- Race the backpressure wait against drain, close, and the request
  lifecycle abort signal, removing the losing listeners via an internal
  AbortController so stalled writes cannot leak listeners, strand a
  rejected promise, or hold an optimization slot past the timeouts.
- Mark terminal events as committed at write hand-off so an abort during
  the final backpressured write cannot append a second, contradictory
  terminal event.
- Write the failure-path terminal event without awaiting backpressure so
  teardown never blocks on a stalled client.
- Skip transport heartbeats while the response buffer needs draining.

Petrinaut Optimizer (apps/petrinaut-opt):
- PetrinautModel.close(graceful=False) signals the CLI process group
  immediately instead of waiting up to five seconds for stdin EOF; the
  graceful EOF wait is now reserved for studies that completed normally.
  All failure, timeout, and cancellation paths terminate promptly.
- Share one idempotent per-run cleanup between the stream wrapper and a
  StreamingResponse background task, so a client that aborts before the
  body is ever pulled cannot leak the admission slot or a live CLI.
- Close an abandoned CLI via an initializer done-callback when a second
  cancellation races the init-cancel recovery.
- Emit Retry-After on the 429 study-limit response and declare it in the
  OpenAPI spec.

Tests cover drain-first, close-first, client abort, idle timeout during
backpressure, single-terminal-event, heartbeat suppression, slot release,
prompt process-group termination, and 429/Retry-After propagation across
NodeAPI, the optimizer client, and the Python service.
@kube kube self-assigned this Jul 19, 2026
@vercel

vercel Bot commented Jul 19, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hash Ready Ready Preview, Comment Jul 19, 2026 1:58am
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
hashdotdesign-tokens Ignored Ignored Jul 19, 2026 1:58am
petrinaut Skipped Skipped Jul 19, 2026 1:58am

@github-actions github-actions Bot added area/apps > hash* Affects HASH (a `hash-*` app) area/apps > hash-api Affects the HASH API (app) area/libs Relates to first-party libraries/crates/packages (area) type/eng > backend Owned by the @backend team area/tests New or updated tests area/apps labels Jul 19, 2026
Comment thread apps/hash-api/src/petrinaut-optimizer/create-petrinaut-optimization-handler.ts Dismissed
@codecov

codecov Bot commented Jul 19, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 68.42105% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 59.23%. Comparing base (b00c6bd) to head (7e9f6ef).

Files with missing lines Patch % Lines
...optimizer/create-petrinaut-optimization-handler.ts 68.42% 3 Missing and 3 partials ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main    #9057   +/-   ##
=======================================
  Coverage   59.22%   59.23%           
=======================================
  Files        1391     1391           
  Lines      135140   135153   +13     
  Branches     6223     6226    +3     
=======================================
+ Hits        80032    80053   +21     
+ Misses      54152    54142   -10     
- Partials      956      958    +2     
Flag Coverage Δ
apps.hash-api 9.75% <68.42%> (+0.32%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@cursor

cursor Bot commented Jul 19, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Touches concurrent streaming, admission limits, and process lifecycle for the optimization path; changes are well-tested but affect capacity and client-visible stream semantics under failure.

Overview
Hardens Petrinaut optimization streaming so slow or disconnecting clients cannot leak listeners, hold user slots, or emit duplicate terminal events.

hash-api NDJSON proxy now waits on backpressure with a race among drain, client close, and the request abort signal, cleaning listeners via an internal AbortController. Terminal complete/error events are marked as sent when the write is handed off (so idle timeout cannot add a second error), failure-path terminal writes are best-effort without blocking on drain, and transport heartbeats are skipped while writableNeedDrain is set.

petrinaut-opt adds PetrinautModel.close(graceful=False) to signal the CLI process group immediately on cancel/error (graceful EOF only after a normal study finish). Shared idempotent per-run cleanup runs from the stream finally and a StreamingResponse background task so aborted responses that never pull the body still release admission slots and close the CLI. Init cancellation gets a done-callback for abandoned CLIs; study-limit 429 responses include Retry-After: 30 (OpenAPI + client types updated).

Tests cover backpressure teardown, slot release, timeouts, single terminal events, prompt CLI shutdown, and 429 header propagation.

Reviewed by Cursor Bugbot for commit 7e9f6ef. Bugbot is set up for automated code reviews on this repo. Configure here.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 7e9f6ef. Configure here.

Comment thread apps/petrinaut-opt/src/optimization_api.py
Comment thread apps/petrinaut-opt/src/optimization_api.py
@kube
kube added this pull request to the merge queue Jul 19, 2026
Merged via the queue into main with commit 5c7a2d9 Jul 19, 2026
64 of 66 checks passed
@kube
kube deleted the cf/fe-1218-petrinaut-optimization-streaming-cancellation branch July 19, 2026 21:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/apps > hash* Affects HASH (a `hash-*` app) area/apps > hash-api Affects the HASH API (app) area/apps area/libs Relates to first-party libraries/crates/packages (area) area/tests New or updated tests type/eng > backend Owned by the @backend team

Development

Successfully merging this pull request may close these issues.

3 participants