Skip to content

FE-1225: Proxy detached Petrinaut optimization runs through NodeAPI#9067

Open
kube wants to merge 2 commits into
cf/fe-1224-petrinaut-detached-runsfrom
cf/fe-1225-nodeapi-detached-run-proxy
Open

FE-1225: Proxy detached Petrinaut optimization runs through NodeAPI#9067
kube wants to merge 2 commits into
cf/fe-1224-petrinaut-detached-runsfrom
cf/fe-1225-nodeapi-detached-run-proxy

Conversation

@kube

@kube kube commented Jul 19, 2026

Copy link
Copy Markdown
Collaborator

🌟 What is the purpose of this PR?

Proxies the detached, reconnectable optimization runs (#9064) through NodeAPI so the browser can create a run, attach/re-attach to its NDJSON event stream with a cursor, and cancel it explicitly — with per-user ownership enforced at NodeAPI. This is the middle layer of the fix for the staging network error: a transport reset now costs one re-attach instead of the whole study.

🔗 Related links

  • FE-1225 — this issue
  • FE-1223 — parent issue with the full contract
  • SRE-831 — the transport-reset root cause

🚫 Blocked by

🔍 What does this change?

  • apps/hash-api: three new authenticated routes behind the existing rate limiter — POST /api/petrinaut-optimizer/optimize/runs201 {runId} (manifest validation and error mapping identical to legacy; client-disconnect windows handled, including cancelling a run created for a client that already left), GET .../optimize/runs/:runId/events?cursor=N → NDJSON attachment stream with the legacy backpressure/timeout machinery (each event carries seq), DELETE .../optimize/runs/:runId → idempotent cancel. The legacy streaming route is unchanged (deprecated in docs) and its 16 tests pass as-is.
  • Run ownership (runId → account) with lastSeenAt-based TTL; 404-not-403 for other users' runs; ownership released only when a terminal event is delivered (an abort during the terminal write keeps the entry so one re-attach can replay it); a stale owned run (e.g. reaped upstream after the browser lost its id) is liveness-probed against the optimizer's /status/{run_id} on the next create and released if terminal — so an orphan can never 429-lock an account.
  • Cross-family single-flight: one active optimization per account across the legacy stream, in-flight creates, and owned detached runs (shared occupancy module).
  • @local/petrinaut-optimizer-client: createPetrinautOptimizationRun, attachPetrinautOptimizationRunStream, cancelPetrinautOptimizationRun (204+404 both resolve), getPetrinautOptimizationRunStatus; the SSE decoder threads id:seq (strict ^\d{1,15}$), maps event: cancelled to a terminal optimization_cancelled error event, and supports attachment mode (no synthetic started; best-so-far aggregation skipped when direction is unknown — best: null, the browser computes its own).
  • @hashintel/petrinaut-core: optional seq on the canonical event schema (changeset included).

Pre-Merge Checklist 🚀

🚢 Has this modified a publishable library?

This PR:

  • modifies an npm-publishable library and I have added a changeset file(s)

📜 Does this require a change to the docs?

The changes in this PR:

  • require changes to docs which are made as part of this PR

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

The changes in this PR:

  • do not affect the execution graph

⚠️ Known issues

  • After a terminal event is delivered, the ownership entry is dropped, so re-attaching to a finished run 404s at NodeAPI (the browser already holds every event) — documented, and the frontend treats attach-404 as "finished or gone".
  • Attachment-mode complete events carry counts observed since the cursor (the frontend keeps its own accumulated totals); requestedTrials is the true total.
  • The seq schema addition also exists identically on the frontend branch — one-line dedupe at merge time.

🛡 What tests cover this?

  • apps/hash-api petrinaut-optimizer: 55 vitest — create happy path + both disconnect windows, cross-family single-flight both directions, ownership TTL touch/expiry, terminal-delivery-before-release (abort-during-terminal-write regression), cancelled-terminal release, stale-owner liveness probe (terminal / unknown / still-running / probe-unreachable), cursor validation, 404-not-403, route ordering.
  • @local/petrinaut-optimizer-client: 40 vitest incl. the codegen drift gate — seq threading, junk-id rejection, cancelled-event adaptation, attachment mode, run status.
  • @hashintel/petrinaut-core: 833; apps/petrinaut-opt: 129 pytest (consumed unchanged).

❓ How to test this?

  1. Run NodeAPI + the optimizer from this branch.
  2. POST /api/petrinaut-optimizer/optimize/runs with a valid manifest (authenticated) → note runId.
  3. Stream GET .../optimize/runs/{runId}/events, kill the connection mid-run, re-attach with ?cursor=<last seq> → resumes without duplicates; the run never stopped.
  4. DELETE the run → prompt optimization_cancelled terminal event on the next attach; account immediately free for a new create.

🤖 Generated with Claude Code

@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 9:20pm
petrinaut Ready Ready Preview Jul 19, 2026 9:20pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
hashdotdesign-tokens Ignored Ignored Preview Jul 19, 2026 9:20pm

@github-actions github-actions Bot added area/apps > hash* Affects HASH (a `hash-*` app) area/infra Relates to version control, CI, CD or IaC (area) area/apps > hash-api Affects the HASH API (app) area/libs Relates to first-party libraries/crates/packages (area) type/eng > frontend Owned by the @frontend team type/eng > backend Owned by the @backend team area/apps labels Jul 19, 2026
@kube
kube marked this pull request as ready for review July 19, 2026 20:29
@kube
kube requested a review from YannisZa July 19, 2026 20:29
@cursor

cursor Bot commented Jul 19, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
New authenticated API surface and in-memory ownership/TTL mean multi-instance deployments could diverge on run visibility; concurrency and disconnect/cancel edge cases are security- and availability-sensitive but heavily tested.

Overview
NodeAPI now exposes detached, reconnectable Petrinaut optimization alongside the legacy single-request stream: create returns a runId, attach/resume events with ?cursor=, and cancel via DELETE. The legacy POST …/optimize path is unchanged but shares one optimization per account with detached runs through shared occupancy and in-process run ownership (404 for non-owners, release only after a terminal event is delivered).

Shared streaming/validation logic is extracted into petrinaut-optimizer/shared; the optimizer client gains create/attach/cancel/status helpers and an SSE decoder that maps frame id → optional seq, handles cancelled, and supports attachment mode without synthetic started or server-side best. @hashintel/petrinaut-core adds optional seq on canonical optimization events.

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

@kube
kube force-pushed the cf/fe-1224-petrinaut-detached-runs branch from fec71b8 to 760a6f0 Compare July 19, 2026 21:02
@kube
kube force-pushed the cf/fe-1225-nodeapi-detached-run-proxy branch from 8763d1f to a39600b Compare July 19, 2026 21:02

@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 a39600b. Configure here.

error: "An optimization is already running for this account",
});
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Detached routes skip global cap

Medium Severity

The legacy POST /optimize handler enforces MAX_CONCURRENT_OPTIMIZATIONS (4) via a module-local counter, but the new detached create and event-attach handlers never participate in that limit. Many accounts can run detached optimizations concurrently while a fifth legacy request gets 502-style “optimizer busy,” overloading the optimizer beyond the cap the legacy path was meant to enforce.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit a39600b. Configure here.

// consumed run keeps its entry alive for the whole study.
owner.lastSeenAt = now();
}
return owner;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Ownership TTL blocks late re-attach

Medium Severity

Run ownership entries expire after OPTIMIZATION_RUN_OWNER_TTL_MS of inactivity based on lastSeenAt, which is only refreshed on get() at attach/cancel time—not while a client is disconnected mid-run. If the user waits longer than that before re-attaching, NodeAPI returns 404 even when the optimizer still has the run and event log, so reconnect/resume fails at the proxy layer.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit a39600b. Configure here.

@codecov

codecov Bot commented Jul 19, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 84.30380% with 62 lines in your changes missing coverage. Please review.
✅ Project coverage is 59.30%. Comparing base (760a6f0) to head (a39600b).

Files with missing lines Patch % Lines
...reate-petrinaut-optimization-run-events-handler.ts 74.66% 6 Missing and 13 partials ⚠️
...mizer/create-petrinaut-optimization-run-handler.ts 86.04% 3 Missing and 9 partials ⚠️
...reate-petrinaut-optimization-run-cancel-handler.ts 69.44% 4 Missing and 7 partials ⚠️
...-optimizer/shared/validate-optimization-request.ts 75.86% 4 Missing and 3 partials ⚠️
...ut-optimizer/shared/forward-optimization-events.ts 73.91% 3 Missing and 3 partials ⚠️
...-optimizer/shared/optimization-run-test-harness.ts 91.22% 3 Missing and 2 partials ⚠️
...optimizer/shared/optimization-request-lifecycle.ts 94.59% 2 Missing ⚠️
Additional details and impacted files
@@                          Coverage Diff                           @@
##           cf/fe-1224-petrinaut-detached-runs    #9067      +/-   ##
======================================================================
+ Coverage                               59.24%   59.30%   +0.05%     
======================================================================
  Files                                    1391     1400       +9     
  Lines                                  135208   135510     +302     
  Branches                                 6242     6315      +73     
======================================================================
+ Hits                                    80100    80358     +258     
- Misses                                  54143    54156      +13     
- Partials                                  965      996      +31     
Flag Coverage Δ
apps.hash-api 13.96% <84.30%> (+3.53%) ⬆️

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.

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/infra Relates to version control, CI, CD or IaC (area) area/libs Relates to first-party libraries/crates/packages (area) type/eng > backend Owned by the @backend team type/eng > frontend Owned by the @frontend team

Development

Successfully merging this pull request may close these issues.

2 participants