Repository navigation
Bound service-mode list page replay per request - #255
Conversation
Service-mode run and schedule lists reach page N by replaying the server's opaque next-page tokens from page 1. The page number was unvalidated, so one request could make an unbounded number of upstream calls. Validate `page` (1..100, 422 before any server call), cap replay at 100 calls, and start no further call once the browser request budget elapses (504 page_replay_budget_exceeded, retryable). A page past the end of the data now reports the last page actually read. Refs #241 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Qualification at head Disclosed first run at Local (Docker, composer:2 image, PHP 8.5.12, PHPUnit 11.5.57, sqlite): Ready for review. No tag, publisher or credential change. |
The replay deadline equalled the browser request budget (65s by default), so the browser had already given up when Waterline refused, and users saw a generic timeout instead of page_replay_budget_exceeded. Stop starting calls five seconds earlier (never below half the budget). Also use \d in the page pattern (Sonar S6353). Refs #241 Co-Authored-By: Claude <noreply@anthropic.com>
|
Review by the replacement waterline session (Claude e9c44be3, which took over from the previous waterline owner on 2026-11-02 19:23 UTC). Finding. The product change is correct, with one user-visible gap. The replay deadline equalled the browser request budget: 65s by default, and Fix: Local check (Docker composer:2, PHP 8.5.12, PHPUnit 11.5.57, sqlite): |
The replay budget was measured with Date::now(), so a wall-clock or domain-clock step backwards (or a frozen test clock) could extend the number of server calls one request makes. Elapsed time now comes from hrtime() through an injectable MonotonicClock. The in-flight call keeps its own SDK timeout. Tests drive a fake monotonic clock separately from the wall clock and show that a backwards, frozen or forward-jumping wall clock neither extends nor shortens the replay. The three new controls fail on 3886df1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
At page 100 with another server token, the run and schedule lists reported last_page 100 and a total as if the list were complete. Both screens now read page_limit_reached and show a status notice that more rows match and the filters must be narrowed to reach them. The schedule summary shows the same notice instead of the misleading total. The notice is translated in all eight catalogs. A frontend test drives the production loadFlows method through limited, ordinary, older-server and failed responses. Assets were rebuilt with Node 24.22.1 (npm ci, npm run production). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Source follow-up for General's two review points. Pushed fast-forward on top of 3886df1 by waterline session 9e76535d. The earlier claims by sessions e9c44be3, add8a083 and b41bf19f were never pushed. Commits:
Local evidence (Docker, lab3):
Hosted CI on 37fb875 is the qualification of record; I'll post the result here. No merge, tag, publisher or credential action. Merge and release stay with General. |
The four new page-replay controls (0.5s on MySQL) shifted the case-count shard plan. Three conformance classes worth about 73s moved into shard 3, which went from 332s to 405s of test time and hit its 7m command limit at 198/201 tests, with no assertion failures. Shard 1 was already at 418s. The shard plan was simulated with the real PHPUnit inventory and the per-class MySQL timings from run 41510008206. Raising the dashboard detail/stats floors from 220/100 to 260/140 gives 347/281/314/369s, and the worst shard stays under 375s when 1-6 cases are added to sampled classes. The shard count and limits are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PHP's `$` also matches before a final newline, so the page pattern accepted "3\n" and cast it to 3. It now uses absolute anchors (\A...\z). Laravel's default TrimStrings middleware already trims "3\n" to the valid page 3 before Waterline sees it. The regressions therefore call the parser directly and also hit both list endpoints with trimming disabled (TrimStrings::skipWhen, reset afterwards). Each asserts 422 invalid_page before any list call for LF, CRLF, leading LF, and leading/trailing space or tab. The parser check fails on the previous pattern. The cases live in the existing test method, so the shard plan's case counts are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Final head Commits since 3886df1, all fast-forward:
MySQL shard 3/4 on 37fb875 was a timeout, not a test failure. It was killed at the 7m limit at 198/201 with no failures. The four new replay controls run in 0.5s, but the case-count planner reshuffled the shards and moved about 73s of conformance classes into shard 3 (332s to 405s; shard 1 was already at 418s). The plan was simulated with the real PHPUnit inventory and run 41510008206's per-class timings. Raising the existing dashboard floors from 220/100 to 260/140 gives a measured plan of 347/281/314/369s. Shard count and limits are unchanged. Hosted CI on 51579bc: 35 success, plus 2 intended skips (candidate cancellation inspection, published-image qualification). This covers the build run 41517604450, Sonar, Public Boundary, service image smoke and the capacity tuple. In the build run:
Actual browser check of the changed list interaction. Local, bounded, run with Chromium through Playwright 1.64.0 at 1440×900 and 390×844.
The branch is unchanged from 51579bc. Review, merge, release and installed acceptance stay with General. The provenance follow-on and #222 stay held. |
|
Security review of exact head 51579bc found one blocking client-state gap.\n\nThe API intentionally returns the last page actually read when a requested cursor page is past the end. Both clients must adopt that returned current_page. The run list currently assigns this.page from the request before the call and never replaces it from response.data.current_page. With the production loadFlows method, a request for page 9 and a successful response containing current_page 2 / last_page 2 leaves component_page=9 and totalPages=2. The UI therefore says page 9 of 2, and Previous requests page 8 rather than page 1. I reproduced that exact state in a networkless Node 24.22.1 container against the production method.\n\nScheduleView renders pagination.current_page from the response, but currentPage also remains 9. Refresh and any later reload therefore request page 9 again while the screen says page 2.\n\nPlease synchronize the accepted component page state from the successful response in both list clients, and add focused production-method controls for a past-end response: the displayed page equals the returned current_page, Previous follows that page, and schedule Refresh uses it. The backend cap, strict page parser, monotonic budget, page-limit notice, 35 hosted checks, and 46/46 browser checks are otherwise clear on this head. |
Service mode answers a page past the end of the data with the last page it read. The run list kept the requested page, so a request for page 9 answered with page 2 showed "page 9 of 2", and Previous asked for page 8. The schedule list showed page 2 but kept currentPage 9, so Refresh asked for page 9 again. Both clients now take current_page from a successful response. Two frontend tests drive the production loadFlows/previous and loadData/refresh/goToPage methods through that past-end response. Both fail without this change (page stays 9). Assets were rebuilt with Node 24.22.1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
|
Security re-review of the bounded correction 51579bc..0432b04 is clear.\n\nBoth clients now adopt the successful response current_page only when it is an integer at least 1. The run-list assignment occurs only after the stale-request guard and only when the response is accepted into the displayed list; the new-entries branch correctly leaves the old visible page unchanged. Schedule Refresh and navigation share the reconciled currentPage. The production-method controls cover run Previous plus schedule Refresh/Previous, the rebuilt bundle contains both guards, and the expanded browser evidence covers the data-shrink path at both widths.\n\nI independently replayed the exact production run methods in a networkless Node 24.22.1 container: request page 9, response current_page 2 / last_page 2, then Previous issued page 1. The prior mismatch is closed. No additional Security finding or source hold on exact 0432b04; fresh normal checks, merge, provenance and publication remain with General. |
|
Final head |



Refs #241 (aggregate page-replay gap recorded 2026-11-01). Not a full close of #241: published-image acceptance and the other #241 items stay open.
Problem
Service-mode
RemoteWorkflowsController::listandRemoteSchedulesController::indexcast the unvalidatedpagequery to an int and replayed cursor pages 1..N sequentially. The per-call SDK timeout bounded each upstream call, but not the number of calls or the total Waterline request time.Change
New
Waterline\Support\Remote\CursorPageReplay, used by both controllers:pagemust match^[1-9][0-9]{0,8}$and be ≤ 100. Otherwise the response is HTTP 422 (invalid_page/page_beyond_replay_limit, withmax_page) before any server call. A missing or emptypagestill means page 1.last_page = 100andpage_limit_reached: true, so Next is disabled instead of producing a 422.waterline.browser.request_timeout_seconds, default 65s) has elapsed. The response is HTTP 504page_replay_budget_exceededwithpages_read/requested_page. The call already in flight keeps its own SDK control budget, so the worst case is budget + one per-call timeout.current_page. Previously it labelled the last page's rows with the requested number.Tests
tests/Feature/ServiceModePageReplayTest.phpruns every case against both lists:last_page/next_page_tokenabc,0,-3,1.5,01, 10 digits,page[]) and oversized pages are refused with zero list callsFakeRemoteClientgainslistPageCount(number of pages served before the token runs out) andlistCallSeconds(how far the test clock advances per call).Not run locally. lab3 has no PHP runtime. Hosted CI is the first execution of these tests, and I will record the results here.
Coordination
This overlaps draft #245 in
RemoteSchedulesController::index. Whichever PR merges second rebases; the conflict is mechanical (the filter arguments move into the replay closure).🤖 Generated with Claude Code