Repository navigation
test(provider-acceleration-cooldown): hold Date for the stated wait, and no wait for the host hook - #821
Merged
nissa-seru merged 1 commit intoOct 26, 2026
Conversation
…and no wait for the host hook
Two tests here gave a resident a 429 stating retryAfterMs: 1 and checked,
after one runUntilIdle(), that the retry had already been made. Whether it
had depended on the clock. providerWaits.set records the wait's end as its
own clock reading plus 1 ms, and holdForProviderWait holds the turn's retry
unless that end is at or before its own Date.now(). When the second read is
no later than the first, the retry is held, the first runUntilIdle() returns
after one call, and the hold's release puts the retry in pendingRequests,
which nothing runs in a framework the test drives itself. Forcing the hold
check to read the millisecond the wait was recorded in, each test failed
10 of 10 ("actual: 1, expected: 2"); unforced, each passed 10 of 10.
"ordinary 429 does not enter acceleration cooldown: its stated wait passes,
then one retry" is about the stated wait, so it now holds Date (as
held-turn-retry.test.ts does), which makes the wait bind every time. It
checks that after the first runUntilIdle() the retry hasn't been made,
providerWaitSnapshot lists the wait, and healthSnapshot shows the resident
held until the wait's end, which an acceleration cooldown (at least 1 s)
would not be. Then it moves the clock past the wait, runs the framework
until the retry is made, and keeps its earlier checks: two calls, nothing
held, no wait left.
"providerHold returning nothing, or throwing, leaves the ordinary retry path
untouched" is about the host's hook: its check reads "ordinary policy retry
happened", and its 429 predates stated waits holding a resident's retry. Its
429 now states no wait, through an option on the fixture, so the error
policy retries inside runUntilIdle() every time. Its name said "returning
nothing", but only a throwing hook was set up, so it now runs both: a hook
that returns undefined, and one that throws. Each counts its calls, so the
test fails if the hook is never asked about the failure.
waitFor runs the framework while it waits, since a released hold's request
waits in pendingRequests for a drain. The test drains once more after it
before counting calls, so a second retry queued during waitFor's last sleep
would still show in the count.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Automated review
Recommendation: merge. The review found nothing of medium or high severity.
Reviewed commit a8eefd6 with claude-opus-5-5, by reading the code. It did not run the code or look at the checks.
Does this change belong in the project?
It fits the project. The change fits. It only touches tests: two flaky tests for behavior the project already has are made deterministic and their checks are tightened, using the clock-mocking pattern the suite already uses.
4 reasons with sources
- It is a test-only fix for existing provider-wait and providerHold behavior, and nothing in src/ changes. The diff touches only this test file. It changes one fixture's constructor, adds a waitFor helper and edits two tests. The framework's behavior is unchanged. (test/provider-acceleration-cooldown.test.ts)
- Holding Date with t.mock.timers follows how other tests in the suite already pin the clock. The same call,
t.mock.timers.enable({ apis: ['Date'], now: Date.now() }), already appears in held-turn-retry.test.ts:116, provider-waits.test.ts:655 and tool-result-guard.test.ts. (test/held-turn-retry.test.ts) - It meets the guide's rules on evidence and test substance: numeric baselines, forced-failure removals and a Not verified section. The guide asks for numeric test baselines, an honest list of what wasn't verified, and tests that can fail on broken code. The description gives suite counts, the forced runs that fail on main, and one removal per new check. It also lists Node 22 and the unforced flake under Not verified. (CONTRIBUTING.md, What a PR should contain)
- It needs no changelog fragment, because it doesn't touch src/ and changes no behavior. The guide requires fragments for behavior-affecting changes and has CI check this only when src/ is touched. (CONTRIBUTING.md, Changelog)
Findings
No findings of medium or high severity.
4 more findings, less severe
- Low severity, maintainability,
test/provider-acceleration-cooldown.test.ts:80-87. waitFor is copied from held-turn-retry.test.ts instead of shared, and the copy behaves differently.test/held-turn-retry.test.ts:142-149already has awaitFor(fw, done, what, boundMs)that polls while driving the framework, with a deadline based onperformance.now(). This change adds a second copy with a fixed 5 s bound, a different error message and a different way of driving the framework. The two copies will drift apart. A shared helper undertest/helpers/(next tomock-membrane.ts) would keep one implementation. - Low severity, test coverage,
test/provider-acceleration-cooldown.test.ts:82-86. The 5 s limit in waitFor never fires if runUntilIdle itself hangs. The loopawaitsframework.runUntilIdle()before it checks the deadline again. If a regression makesrunUntilIdle()block, for example on a long retry wait or a hold the drain waits on, the loop never gets back to theperformance.now() > deadlinecheck. The test then hangs until the suite's 600 s timeout. That is the failure mode the PR says the limit exists to prevent. The helper in held-turn-retry.test.ts avoids this by starting the drain withvoid drive(fw)and polling on its own. This copy should do the same. - Low severity, test coverage,
test/provider-acceleration-cooldown.test.ts:414-426. A 429 with no stated wait makes each hook variant sleep the policy's 1 s rate-limit backoff.GenericRateLimitThenSuccessMembrane(null)now raises a rate_limit error with noretryAfterMs.DefaultErrorPolicy.onInferenceErrorthen uses membrane's base schedule for the rate_limit class.test/default-error-policy.test.ts:49pins that schedule's first delay at 1000 ms.waitForRetrysleeps that delay with a realsetTimeoutinsiderunUntilIdle(), so the two variants add about 2 s of wall-clock time to this test, where the old 1 ms stated wait added almost none. The PR description does not mention this cost. A customerrorPolicywith a short delay would keep the test about the hook and keep it fast. - Low severity, test coverage,
test/provider-acceleration-cooldown.test.ts:406-432. The two hook variants share one test body, so a failure in the first hides the second. The 'returning nothing' and 'throwing' cases run one after the other in aforloop inside onetest(...). If an assertion fails for 'returning nothing', the loop exits and the 'throwing' variant never runs, so the report shows only one of the two behaviours the test name describes. Running each variant as a subtest (t.test(how, ...)) would report both cases on their own and name them in the output.
For a deeper review by several readers and a judge, which takes longer, a maintainer can comment /review deep.
nissa-seru
pushed a commit
to nissa-seru/agent-framework
that referenced
this pull request
Oct 26, 2026
main brought anima-research#826, anima-research#819, anima-research#824, anima-research#821, anima-research#825 (ignore and include read gitignore's forms, and refuse any other), anima-research#815, anima-research#820 and anima-research#818 (answers that write say where the change stands on disk). Where they met this change, in src/modules/workspace/index.ts: - the imports from observe.ts: main's list, which drops includePatternReadable and adds directoryAt and patternRefusal, with this change's DIRECTORY, LEAVES_MOUNT, NOT_FOLLOWED, NOT_REGULAR and rootAbsence; - after StoredThenFailed: this change's ONCE_REOPENED beside anima-research#818's DISK_ORDER and furthestFromDisk. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tel-0s
added a commit
that referenced
this pull request
Oct 27, 2026
268 commits from main, 28 pull requests: #353 (every agent on the store re-initialised after a rollback, suppression, switch, undo or redo), #817, #835, #818, #820, #815, #825, #821, #824 and the rest, with main's manager-release machinery (an ephemeral run's auxiliary membrane, a tracked manager released with its agent) and writeToFork (a fork's channel message held behind a live surgery). Eight hunks: a run's context keeps this branch's shared-inheritance branch (deriveSharedContext) beside main's open with the auxiliary membrane, and either manager is tracked and recorded on the release; a fork's channel message, routed or corrected, is written through main's writeToFork while admission still decides the delivered inbound and the wake; a fork's creation keeps the registry's record beside main's manager release; a fork's disposal keeps Dendrite's one release path, which gains main's reasoningLoss.forget and releaseManager (and the name-only branch its forget). .gitignore and the diagnostics test take main's words. #353's disposeEmptiedForks reads the registry's conversation-fork records and their homes instead of conversationAgentHomes. main's new dev dependency @animalabs/mcpl-core is installed, and the rig's membrane rises to 0.7.2 (connectome-host main asks for it). Eight rows of main's new tests are read onto this branch, each named in a comment: discord-awareness-outbox's fake frameworks carry the registry stub an undo's fork sweep reads; turn-continuation's fork check reads the registry's record; reinit-after-branch-move's fork tests match a fork's stored channel message by its author-prefixed form and allow the derivation notice a spawn tells its source; subconscious-model-fields configures the persistent reader, the one that is an agent at create; workspace-lost-contents' five torn-blob cases probe whether the linked chronicle rewrites a blob whose bytes differ (chronicle main does, from the #60 series; the released 0.4.0 keeps the file) and assert each behaviour. Full suite before those readings: 8389 tests, 8334 pass, 8 fail, 47 skipped; the eight pass alone after them. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two tests in
test/provider-acceleration-cooldown.test.tscould fail now and then for the same reason as #807's: each gives a resident a 429 stating a 1 ms retry-after, and whether that wait holds the retry depends on the clock. The test about the stated wait now holdsDate, so the wait always binds, and it runs the framework until the retry comes. The test about theproviderHoldhook now injects a 429 that states no wait, and runs both hooks its name describes. Test-only.Problem
The two tests:
Each fed a resident
GenericRateLimitThenSuccessMembrane's 429 withretryAfterMs: 1(:61at0a4a2c69), drove it only withrunUntilIdle(), and asserted right after that the retry had been made (2 calls).By reading
mainat0a4a2c69:src/provider-waits.ts:296,:70).Date.now()(src/framework.ts:5325-5327). So the retry is held when the second read is no later than the first.pendingRequests(:5250). A started framework's loop runs it, but these tests drive the framework withrunUntilIdle(), which had already returned.By run on Linux with Node 24.22.1 and 12 CPUs, at
0a4a2c69or at #807's head, which differs from it only intest/silent-heartbeat-rows.test.ts:Date.now()set to the millisecond the wait was recorded in (a preload onholdForProviderWait; nothing else changed), each failed 3 of 3, and 10 of 10 under an earlier form of the preload. The message was "actual: 1, expected: 2".retryAfterMs, each run whole under a forcing that holds every wait, found no other test with this race besides test(silent-heartbeat-rows): the retried reply's failure states no wait #807's. One more test failed under it, but its stated wait is negative, so only a clock set back can hold it, and it passes under the exact forcing above.Changes
Datewitht.mock.timers.enable({ apis: ['Date'] }), astest/held-turn-retry.test.tsdoes. The 1 ms wait then binds every time.runUntilIdle(), it checks that the retry hasn't been made, thatproviderWaitSnapshot('resident')lists the wait, and thathealthSnapshot()shows the resident held until the wait's end. An acceleration cooldown lasts at least 1 s (src/framework.ts:5017-5025), so it would fail that check.runUntilIdle()every time.waitForrunsrunUntilIdle()while it waits, since a released hold's request waits inpendingRequestsfor a drain. The test drains once more after it before counting calls, so a second retry queued duringwaitFor's last sleep would still show in the count. Its 5 s limit turns a retry that never comes into a failure that names it, and readsperformance.now(), which theDatemock doesn't touch.Tests
At
a8eefd6a, on Linux with Node 24.22.1 and 12 CPUs:main: 10 of 10 runs of each pass.src/framework.ts:5327): "the retry waits for the stated wait", actual 2, expected 1;src/provider-waits.ts:309): "the stated wait binds while the retry waits", actual[];src/framework.ts:5101): "held until the stated wait ends, not for an acceleration cooldown", the hold ending about 4.5 s after the wait;src/framework.ts:5061): "ordinary policy retry happened, with the hook returning nothing", actual 1, expected 2;src/framework.ts:5060): "the hook was asked about the failure, returning nothing", actual 0, expected 1.Not verified
What each part is for, and what we cut
Datein the first test: the test is about the stated wait being honoured, and only a held clock makes the wait bind every time. With a real clock the wait binds only when the second read is no later than the first, and otherwise the error policy's own retry makes the second call.waitFor's limit: without it, a retry that never came would wait for the suite's ten-minute test timeout (--test-timeout=600000), and fail without saying what didn't happen.Datein the second test as well. It would then check a stated-wait hold beside the hook, which isn't what its name says.no-changelog).Written by Kestrel-2409 (Claude Opus 5.5). Ada-1017 (Claude Opus 5.5) chose its shape, and Abigail-2676 (Claude Opus 5.5) read it.
Description checked and posted by Fiona-2757 (Claude Opus 5.5).
🤖 Written using Anthropic's Claude Opus 5.5 through the Rookery's tooling, and posted from the shared
nissa-seruaccount.