Skip to content

feat(turns)!: trace a turn's retry wait, retry a 529 about as long as 0.26.0 did, keep a host's own retry ceilings, and keep a turn's first terminal event final (#734) - #791

Merged
nissa-seru merged 12 commits into
anima-research:mainfrom
nissa-seru:hilda/f345-main-rebuild
Oct 27, 2026
Merged

nissa-seru merged 12 commits into
anima-research:mainfrom
nissa-seru:hilda/f345-main-rebuild

Conversation

@nissa-seru

@nissa-seru nissa-seru commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

@antra-tess, this changes how turns retry since #728, which you merged and which shipped in 0.27.0. It follows up #734 and answers its three items. It keeps #728's design, in which the framework is the one retry layer for an agent's turns and decides each retry on membrane's own retryDecision, for each agent separately. It gives an overloaded error thirteen tries in the default policy's own settings, takes a host's own settings as the host gave them, and ends the silence while a turn waits to retry. For a host whose own settings set a stated-wait ceiling below ten minutes, that's a breaking change, since an ephemeral run's turn now ends where 0.27.0 retried it, and the run rejects with a plain Error (Change 3). When a stream's iterator throws a retryable failure, the turn is now retried as it is when the same failure comes as an error event. And once a stream's terminal event has decided its turn, nothing the stream sends or throws after it changes how the turn ended. Compression's retries stay as in 0.27.0 (see "What each part is for, and what's not here").

Problem

Each problem below was shown by a probe we ran at 0.27.0 (v0.27.0, 0a4a2c69), on Linux with Node 24.22.1, membrane 0.7.2 and context-manager 0.22.1, unless it says otherwise. The Tests section says which of this pull request's tests fail at 0.27.0.

  1. A turn waiting out the policy's delay is silent. Nothing is traced between a turn's inference:failed and its next inference:started. The default policy's waits before a 529's fifth and later retries fall between 80 s and 5 minutes. In one draw, its six waits were 7.3, 11.0, 33.7, 74.4, 137.3 and 168.8 s. connectome-host's subagent reaper reads that silence as a stuck subagent. We ran connectome-host's own SubagentModule at b3f371af, its main then, which has depended on agent-framework ^0.27.0 since its own 0.14.0, with Bun 1.4.4, and fed it 0.27.0's trace for a wait by hand. Its reaper scans every 60 s (REAPER_INTERVAL_MS, src/modules/subagent-module.ts:1324). We tried four timings of the scan against each wait. A subagent waiting out 200 s or 300 s was reclaimed, as cancelled, at every timing, 135 to 180 s into the wait. A 170 s wait was reclaimed at three timings of four, a 150 s wait at one, and a 125 s wait at none.

  2. A turn keeps retrying a 529 for about 5 to 10 minutes, where 0.26.0 kept retrying for about 30. The default policy gives a 529 membrane's 7 tries, then the turn ends (inference:exhausted). Its 6 waits come to 610 s nominal, and came to 432 s in our draw. On 0.26.0, membrane's 7 sends ran inside each of the old policy's 4 tries. That made membrane's 24 waits, 2,440 s nominal, and the old policy's own 3 waits of 1, 2 and 4 s between its tries, 2,447 s in all. 0.26.0: a turn whose model call is answered 529 sends 28 requests over about 30 minutes #721 measured 28 sends over 30 min 26 s against a model answering only 529s.

  3. Fast-follow for #728 (framework owns turn retries): stated-wait ceiling override, iterator throws, start-path tests #734 item 1: the default policy raises to 10 minutes a stated-wait ceiling that a host set lower. With a host ceiling of 30 s, the policy answers a 429 stating 90 s with a retry after 90 s ({ retry: true, delayMs: 90000 }). In a probe with that ceiling and that 429, a resident's turn was held by the framework's provider hold instead, with one send and no inference:exhausted within 2 s. An ephemeral run's turn, as a host's subagent is, was still waiting out the policy's 90 s retry after 2 s.

  4. Fast-follow for #728 (framework owns turn retries): stated-wait ceiling override, iterator throws, start-path tests #734 item 2: when a stream's iterator throws a retryable 529 that states no wait, the turn makes 1 send, then traces inference:failed and inference:exhausted, with no retry. The same 529 sent as an error event is retried.

  5. When a stream throws after its answer, the answered turn ends a second time, as a failure. It's related to Fast-follow for #728 (framework owns turn retries): stated-wait ceiling override, iterator throws, start-path tests #734 item 2 but separate from it, and this pull request's second internal review round found it. In our probe, a stream answered with complete, then its iterator threw.

    • With a retryable 529, the trace went inference:completed, inference:failed, inference:exhausted. The inference log got a failed row beside the answer's, and the resident's context got an [inference-failed] Your previous turn did not complete: … line after its own answer.
    • With an acceleration 429, the trace ended at inference:failed, and the log got a failed row. That installs a provider hold that keeps the turn's request as its retry. In a probe where the 429 stated a 1 s wait, the answered turn was sent again 4.5 s later, for 2 sends in all.

    The cause is that driveStream's for await keeps pulling after complete, error and aborted, and its catch doesn't read streamTerminal. An event sent after the answer is acted on too. This pull request's tests of a later error, aborted, complete or tokens event each fail at 0.27.0. In our probe, membrane's own stream, YieldingStreamImpl at 0.7.1 and at 0.7.2, was made to emit tokens, an error and a second complete after its complete, then throw from its inference loop. Its consumer saw only the complete, and its iterator didn't throw. We didn't run it emitting after an error or an aborted event.

Changes

This head, 3337fba5, merges 14fe38a8, main when it was made, into c141565e, which changes only two lines of one doc comment after 21fa6279. Where a probe below names 21fa6279, it ran there, on Linux with Node 24.22.1 and membrane 0.7.1. Where it names 0.27.0, it ran on Linux with Node 24.22.1 and membrane 0.7.2.

  1. A new trace event, inference:waiting (src/types/trace.ts). A turn waiting out its error policy's delay says so as the wait begins, with until, the time it's due to end. It says so again every 60 s while the wait lasts, and never after the wait ends, whether by its time, a stop or an abort. It comes from waitForRetry, so both of the policy's retry sites, a start's failure and a stream's, emit it. In the connectome-host run that Problem 1 describes, with inference:waiting fed every 60 s in place of the silence, no wait was reclaimed, at any length or timing. It's said only while the agent is the one registered under its name. A listener that calls abortInference() or stop() ends the wait. A stop through Agent.cancelStream() waits the wait out, as in 0.27.0, and the turn then ends as aborted. In our probe at 0.27.0 and at 21fa6279, cancelStream() was called about 300 ms into a 2 s wait (302 ms at 0.27.0, 300 ms at 21fa6279), and the next event was inference:aborted, at the wait's end, with no retry sent. A wait whose end would fall past the latest time a JavaScript Date can hold has no until. The wait still counts down by its timers, as in 0.27.0 (remaining -= step), now in steps of at most 60 s. So a stalled event loop makes the wait end later than its until. In six probe runs at 21fa6279, none started its retry before until. The retry started 0 ms after it with nothing stalled, and 1,000 to 1,008 ms after it with the loop stalled for 1 s at the wait's start. Its cadence is counted by the same timer steps, so a wall clock stepped back can't stretch it, and Date.now() is read only for until.
  2. Thirteen tries for an overloaded error, in the default policy's own settings (OVERLOADED_TURN_TRIES, TURN_RETRY_SETTINGS). Its 12 waits on membrane's overloaded schedule come to 2,410 s nominal, against 0.26.0's 2,447 s. Membrane's waits are jittered into [d/2, d) in both, while 0.26.0's own waits of 1, 2 and 4 s aren't. So a turn keeps retrying through a run of 529s about as long as on 0.26.0, about 20 to 40 minutes. A host that passes its own settings keeps its own count.
  3. Fast-follow for #728 (framework owns turn retries): stated-wait ceiling override, iterator throws, start-path tests #734 item 1: the default policy raises stated-wait ceilings to the framework's 10-minute cap only in its own settings. A host's settings are taken as they are, so with that 30 s ceiling the policy answers no retry. In our probe at 21fa6279, an ephemeral run's turn then ended at once, with inference:exhausted 13 ms after inference:failed, and runEphemeralToCompletion rejected with a plain Error, not ProviderWaitNotSleptError. A resident's turn was held by the provider hold, as in 0.27.0.
    • ProviderWaitNotSleptError is still the rejection for a stated wait past the framework's ten-minute cap, or one it can't represent, as test/provider-waits.test.ts's tests of those two ends check.
    • host-retry-ceilings.breaking.md now says that when a host's ceiling ends an ephemeral run's turn, runEphemeralToCompletion rejects with a plain Error. runEphemeralToCompletion's doc comment now counts a stated wait its error policy declines among the endings that reject with a plain Error. nonOwningProviderWait's doc comment now says the error policy sleeps and retries such a wait unless it declines, as a host's own ceiling can.
  4. Fast-follow for #728 (framework owns turn retries): stated-wait ceiling override, iterator throws, start-path tests #734 item 2: a retryable MembraneError that a turn stream's iterator throws is passed on as the error event that membrane's own streams send for one. So it takes that event's path, to a provider hold, a stated wait or the policy's retry.
    • Before it's passed on, the stream that threw is cancelled, as far as its cancel() goes. A rejected next() ends driveStream's loop without closing the stream, so a producer that outlives it would otherwise run on beside the retry's stream. A cancel() that throws gets a console line of its own, and the retry goes ahead.
    • Its inference-log row is the error event's row, with the provider's words, where the throw's path prefixed them with Provider acceleration cooldown: or Provider wait:.
    • One console line names the throw, with its message and frames. The error event's path doesn't log a failure that a stop or the framework's cancel then ends, where 0.27.0's throw path logged it.
    • Any other throw, a cancellation's included, takes the throw's path as before, which doesn't ask the policy.
  5. Once a terminal event (complete, error or aborted) has decided the turn, nothing after it decides it again. The first event after it ends the stream's events, and so does a throw after it. Each gets one console line naming it, and neither acts on the turn.
    • A throw after it also cancels the stream that threw, as far as its cancel() goes, since a rejected next() closes nothing. A cancel() that throws gets a console line of its own. The cancel for a converted throw in Change 4 is the same helper, cancelThrown.
    • Ending the events there, rather than pulling on past them, closes the stream through its iterator's return(), as driveStream's own early returns do. So a stream that sends a later event and then holds its iterator for something that will never come, such as the results of a later tool-calls event, can't keep the turn from finishing.
    • One pull past the terminal event is still made, to see what follows. So a stream that holds its iterator right there, sending nothing, keeps the turn waiting, as it does in 0.27.0. In our probe at 0.27.0 and at 21fa6279, a stream that answered and then held its next pull left the framework busy after 3 s, and the framework went idle once the pull was released.
    • After the events end, driveStream goes on as it does when a stream simply finishes after that event. Its post-loop block does nothing once streamTerminal is set, and its finally runs as always.
    • Changes 4 and 5 are made in one place, turnStreamEvents, through which driveStream reads the stream, since it's the one place that sees both the terminal events and the iterator's throws.

Changes 2, 3 and 4 have turn-529-window.changed.md, host-retry-ceilings.breaking.md and thrown-retryable-failure.fixed.md, each written against 0.27.0's text, since #728's fragment shipped in 0.27.0. Changes 1 and 5 have retry-wait-trace.added.md and turn-decided-once.fixed.md.

What each part is for, and what's not here:

  • Each change above answers one of the five problems, and each has a test that fails without it (see Tests).

  • What the changes need besides:

    • waitingTraceEveryMs, a field holding the 60 s cadence, so tests can shorten it;
    • waitForRetry keeping one waker for its whole wait, in place of one per step, so a stop or an abort made by a listener of the trace ends it;
    • the test file's stream and membrane stand-ins (ThrowingStream, ThrowOnceMembrane, EventOnceMembrane, SendThenThrowStream, SendThenThrowMembrane, AnswerThenToolCallsStream, ThrowAfterCancelStream, ThrowThenRecordMembrane, CompleteOptionsMembrane, RejectAfterAnswerStream, RejectAfterAnswerMembrane) and its helpers (scaled, makeFramework, traceOf, recordingPolicy, afterTurn, laterEventLines, answer, accelerationLimit).

    SendThenThrowStream with no error to throw simply ends, which gives each after-the-event test its control, the same event with nothing after it. laterEventLines collects the console lines this change writes, so a test can check them, and keeps them out of the test output. AnswerThenToolCallsStream holds its iterator for tool results after a later tool-calls event, as a membrane stream does before its terminal event, which is the case that ending the events is for.

  • Not here: a hold across agents on an overloaded model, and any notice to the agent of how long its turn waited. Neither is needed for these five problems, and the first would be a design change of its own.

  • Compression still retries in two layers, as in 0.27.0. Membrane retries each of a context manager's complete() calls, and context-manager's compression lane pauses after a failed call and sends it again. This pull request's earlier heads (a5e169ab, 5d9563f7) passed retry: false on those calls. This head leaves compression as 0.27.0 has it. Which of the two layers should retry compression, so that only one does, is for context-manager's side to decide. Not verified names the code in context-manager that bears on it.

  • Other calls are unchanged. In our probe at 0.27.0 and at 21fa6279, the inference router passed membrane no options for complete() or streamYielding(), so membrane's default retries apply to them. A direct caller of Agent.startStream keeps membrane's retries, as in 0.27.0.

Tests

The new tests are in test/default-error-policy.test.ts. One existing test in test/provider-waits.test.ts passes its agent to the wait and turns the cadence off, so it still steps by whole timers. Two existing tests in test/end-turn-usage.test.ts, whose streams throw a retryable 429 after a finished round, now expect the error event's log row.

The new tests check these:

  • The 529: thirteen tries and their jittered waits (2,410 s nominal), checked on the policy itself, and that a host's settings keep membrane's seven tries and the host's own ceiling. No turn-level test runs a run of 529s to exhaustion. The turn-level tests run one failure and its retry, on membrane's settings with the delays scaled down.
  • The waiting trace:
    • it's said as the wait begins, with until, and at each cadence point and no more often, with nothing after, and still at each cadence point after the wall clock steps back two minutes;
    • a listener's stop() ends the wait, and so does its abortInference(), which ends the turn with no retry;
    • nothing is said while another agent has the name;
    • an ephemeral run, as a host's subagent is, says it's waiting, with its end;
    • a wait whose end would fall past the latest time a JavaScript Date can hold has no until.
  • Throws:
    • a retryable 529 thrown from an iterator is retried, with the same sends, the same policy decisions and the same trace as the same 529 sent as an error event, and one console line names it;
    • a non-retryable one, and a plain Error, end the turn as a throw did, without the policy being asked;
    • one thrown after a stop cancelled the stream ends the turn as the stop, and its console line names it;
    • the stream that threw is cancelled before the retry's stream is asked for, and a cancel() that throws still leaves the retry to run, with its own console line.
  • After a terminal event:
    • a retryable 529 or a plain Error thrown after the answer leaves no failure, no [inference-failed] line, no failed log row and no retry, and one console line names it;
    • a stream whose next() rejects after its answer is cancelled, the answer stands and nothing is retried, and a cancel() that throws there gets its own console line;
    • a retryable error event, an aborted event, a second complete or a tokens event, sent twice after the answer, leaves the answer's trace, sends, log rows and [inference-failed] lines as they were, and one console line names the first, by its type;
    • a tool-calls event after the answer, from a stream that holds its iterator for the calls' results, runs nothing and doesn't keep the turn from finishing;
    • an acceleration 429 thrown after the answer installs no hold;
    • a throw after a retryable error event gives that error's one retry, not a second. This test passes on 0.27.0 too, since there the retry's stream supersedes the first and 0.27.0's catch then returns, and none of the breaks below fails it. It pins that outcome, not the rule;
    • after a non-retryable error event, or an aborted event no stop asked for, a throw or a later complete leaves the same trace, log rows and [inference-failed] lines as that event alone.
  • The start path: a retryable failure there is retried by the default policy, and its wait is traced (Fast-follow for #728 (framework owns turn retries): stated-wait ceiling override, iterator throws, start-path tests #734 item 3).
  • Compression: a resident's, a conversation agent's and an ephemeral run's context managers pass each complete() call's retry option to membrane as the caller gave it, so compression keeps membrane's own retries, as in 0.27.0. The test checks retry alone because, since fix(context): a released context manager makes no provider call, and managers close before an owned store #587, an ephemeral run's calls carry a signal of their own.

The breaks and the whole suite below ran at this head, 3337fba5.

Against 0.27.0: we ran this pull request's three test files against v0.27.0's source on Linux with Node 24.22.1, membrane 0.7.2 and context-manager 0.22.1. default-error-policy.test.ts and end-turn-usage.test.ts were as at f89ce002, this pull request's previous head. provider-waits.test.ts was as this pull request changes 0.27.0's, without the test that main has added to it since. tsc there reports six errors, all of one kind. 0.27.0's event type doesn't include the new 'inference:waiting', so the tests' comparisons with it can't be true. The tests ran from its emitted build.

  • default-error-policy.test.ts: 25 of its 38 tests fail, all of them tests of the five changes. Among them, the start path's test passes its first check, that the start was compiled twice, the failed start and then the retry. It fails its next check, of the waiting trace, so its last check, that the retry answered, never ran. The throw-after-a-stop test passes its checks that nothing was retried and that the turn ended as the stop, and fails its check of the console line.
  • The 13 that pass pin what 0.27.0 already does. A non-retryable failure ends at once. They check the base, rate-limit and stated-wait schedules, a stream deadline, and a failure that isn't a MembraneError. A turn asks membrane for no retries, the framework retries it, and a direct caller of Agent.startStream keeps membrane's. A non-retryable or plain throw ends the turn as a throw did. Compression keeps membrane's retries. A throw after a retryable error event gets that error's one retry.
  • end-turn-usage.test.ts: its two changed tests fail, on their log rows.
  • provider-waits.test.ts: all 92 pass.

The two tests added since, of a stream whose next() rejects after its answer, weren't run against 0.27.0. Run against f89ce002's code, which passes over such a throw without cancelling the stream, on Linux with Node 24.22.1, membrane 0.7.2 and context-manager 0.22.1, both fail on the check that the stream that threw was cancelled, and the file's other 38 tests pass.

Breaking each piece: we broke each piece in turn in this head's code, built each of the 21 breaks, and ran the tests, and each break's test failed. The run's tree was 3337fba5 plus the script that ran it, which isn't in this pull request, on Linux with Node 24.22.1 and membrane 0.7.2. The breaks were these:

  • the waiting trace never said, said only once, said after the wait, or said for another agent under the name; the wait not checking, after the trace's listeners have run, whether one of them ended it; until kept for a wait with no end;
  • seven tries in place of thirteen, and a host's ceilings raised again;
  • compression's calls given retry: false again;
  • for throws, the iterator unwrapped, every MembraneError passed on, every throw passed on, the converted throw's console line dropped, and the stream that threw left uncancelled;
  • after a terminal event, a throw passed on as an error event, a throw thrown on to driveStream's catch as in 0.27.0, every later event passed on, only later terminal events stopped, later events passed over while the stream is pulled on, the throw's console line dropped, and the stream that threw after it left uncancelled.

The clock test also fails at this pull request's first commit's code (a5e169ab), whose cadence read the wall clock.

The whole suite: on a tree of 3337fba5 plus the two scripts that ran it, which aren't in this pull request, on Linux with Node 24.22.1, context-manager 0.22.1 and membrane 0.7.1. tsc and the build are clean, and its 334 test files, each run on its own with npm test's --import and --test-force-exit, give 8,381 tests, of which 8,319 pass, 0 fail and 62 are skipped.

Not verified

  • No live provider: the failures are scripted 529s, 429s and plain Errors, against a mock membrane or, in Problem 5's probe of membrane's own stream, the installed membrane's YieldingStreamImpl. At this head no run of 529s was retried to exhaustion through a turn. At 0.27.0 one was, with the delays scaled down (Problem 2). The schedule's real waits, of minutes, weren't run. The turn-level tests run one failure and its retry with the delays scaled down.
  • A whole connectome-host run, against 0.27.0 or against this branch. Our runs of its reaper fed its SubagentModule the traces by hand.
  • What taking membrane's retries out of compression would do in context-manager. We didn't run whether it would change when context-manager 0.22.0's persistent-failure rule fires (COMPRESSION_PERSISTENT_FAILURES, src/strategies/autobiographical.ts:1992–2006), or when a compression 429 arms the resident's provider hold through the framework's auxiliary admission (src/framework.ts:4891–4900 at 5d9563f7, an earlier head of this pull request).
  • What a turn's longer window means for connectome-host's subagents. We didn't run how this pull request's thirteen tries combine with connectome-host's retries of a whole subagent run, or with its task timeouts, at its b98d9765 (maxRetries at src/modules/subagent-module.ts:941, isTransientError at :1644–1659, the retry loops at :2526 and :2696, and timeoutMs at :2189 and :2508). turn-529-window.changed.md gives the sends and minutes for connectome-host's case from the two schedules.

  • Changelog fragment added or a pending one edited: changelog.d/turn-529-window.changed.md, changelog.d/host-retry-ceilings.breaking.md, changelog.d/thrown-retryable-failure.fixed.md, changelog.d/retry-wait-trace.added.md and changelog.d/turn-decided-once.fixed.md added.

Written by Hilda-2681 (Claude Opus 5.5). Ada-1017 (Claude Opus 5.5) asked for a list of what main lacked and chose these from it. Gladys-2413 (Claude Opus 5.5), who built none of it, read it, and weighed its parts, at each round of review, its merges of main included, the last at 3337fba5. Description written 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-seru account.

…ain, retry compression in one layer, and decide a turn once (anima-research#734)

Follow-up to anima-research#728 and anima-research#734: what main still lacked, each shown by a run
at 0a55c05:

- inference:waiting: waitForRetry, the error policy's wait before a
  retry, traces it as the wait begins (with `until`) and every 60 s while
  it lasts, while its agent holds its name, never after. connectome-host
  0.13's subagent reaper reclaims a subagent silent for 120 s with no
  request in flight, and inference:failed ends its request in flight; the
  default policy's waits before a 529's fifth and later retries run from
  80 s to 5 min.
- The default policy's own settings (TURN_RETRY_SETTINGS) give an
  overloaded error 13 tries where membrane's give 7: 12 waits, 2,410 s
  nominal, against 0.26.0's nesting of membrane's 7 sends inside 4 tries,
  2,440 s; both jittered into [d/2, d).
- They alone raise stated-wait ceilings to the framework's 10-minute cap; a
  host's settings are taken as they are (anima-research#734 item 1).
- A resident's, a conversation agent's and an ephemeral run's context
  managers call complete() with retry: false over any options given:
  context-manager's compression lane pauses and resends on a transient
  failure, so membrane's schedules ran inside each of its sends. Every
  ephemeral run takes one proxy made once (ephemeralRunMembrane), since
  context-manager keys a compression's failure evidence by the membrane
  instance and the runs shared the framework's own on main. The
  subconscious keeps the plain membrane: its strategy makes no calls. A
  transition summary is now sent once.
- A retryable MembraneError a turn stream's iterator throws before its
  terminal event becomes the error event membrane's streams send for one,
  so it meets that path: holds, stated waits, the policy's retry, its log
  row (anima-research#734 item 2). Other throws take the throw's path as before.
- Once a terminal event (complete, error, aborted) has decided the turn,
  nothing after it decides it again: the first later event, or a throw,
  ends the events with a console line naming it, closing the stream so a
  stream holding its iterator can't keep the turn open (turnStreamEvents). On main, driveStream's
  catch settled an answered turn as failed, with an [inference-failed]
  line after its answer, or held it to send again (a probe at 0a55c05).

Tests in default-error-policy (anima-research#734 item 3, the start path included, and
throws and events after each terminal event); provider-waits' stepping
test passes the agent the wait now takes and turns the cadence off; two
end-turn-usage tests, whose streams throw a retryable 429, now expect the
error event's log row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@github-actions github-actions 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.

Automated review

Recommendation: merge and fast-follow. 1 medium severity finding can be fixed after the merge.

Reviewed commit a5e169a 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. This fits. It is a follow-up to the framework's own turn-retry design (#728/#734) and keeps the framework as the one retry layer.

5 reasons with sources

It adds a trace event the README's stop-and-wait model already implies, and every change stays in the components that already own each job.

  • It extends the turn-retry design the project already has instead of adding a second retry mechanism. The pending breaking fragment says the framework is the one retry layer for turns and decides with membrane's retryDecision. This PR keeps that. It moves the settings adjustment into a TURN_RETRY_SETTINGS constant, gives the overloaded class 13 tries, and leaves a host's own settings as they are, which answers #734 item 1. (changelog.d/af-owns-turn-retries.breaking.md (base); src/framework.ts DefaultErrorPolicy)
  • inference:waiting is a new trace event in the existing TraceEvent union, emitted from the existing waitForRetry. The README already describes turns waiting to retry and how abortInference() ends that wait. Making the wait visible with an event, next to inference:failed and inference:exhausted, follows the existing tracing pattern rather than adding a new concept. The PR ties it to how connectome-host, a declared consumer in CONTRIBUTING.md, detects agents that have gone quiet. A separate review found a medium defect in the event's timing that can leave a wait silent; that is for the author to fix and does not affect whether the event belongs. (src/types/trace.ts; README.md (stop/abort of a turn waiting to retry, ~line 456))
  • Compression retries are turned off through the framework's existing context-manager membrane proxy, so retrying stays with one layer. retry: false is added in membraneAdmitting, the proxy the framework already uses for context managers' calls. Ephemeral runs share one proxy, so context-manager's per-instance evidence about compression failures stays shared. No new dependency or new public interface is added. (src/framework.ts membraneAdmitting)
  • The fix for events after a stream's terminal event is a bug fix inside driveStream, and it shares the wrapper that implements #734 item 2. Both behaviours live in the one turnStreamEvents wrapper, and the bug turned up while working on #734. The PR has a .fixed.md fragment, tests that compare the turn against controls, and Problem/Changes/Tests sections, as CONTRIBUTING asks. (src/framework.ts turnStreamEvents; CONTRIBUTING.md (What a PR should contain))
  • The longer default for 529s (13 tries) is a policy choice that changes behaviour, and the PR rightly asks the maintainer who merged #728 to decide it. It brings back roughly the 0.26.0 tolerance for overload storms (about 30 minutes) and is recorded in the pending breaking fragment. Hosts that pass their own settings are unaffected. (changelog.d/af-owns-turn-retries.breaking.md)

Findings

1 finding of medium or high severity, see the inline comment:

3 more findings, less severe
  • Low severity, test coverage, test/default-error-policy.test.ts:423-444. The conversation-agent case calls the private factory directly, not a conversation agent's context manager. The test checks the resident and the ephemeral run through their actual context managers (membraneOf(...getContextManager())). For conversation agents, though, it calls conversationAuxiliaryMembraneFor('resident').complete(...) directly. That only shows the factory's proxy adds retry: false; it does not show that a conversation agent's context manager is built on that proxy. If the wiring were changed back to the plain membrane, this test would still pass. Create a conversation agent and check the membrane its context manager actually holds.
  • Low severity, maintainability, test/default-error-policy.test.ts:230-241. frameworkWith repeats makeFramework in the same test file. The new frameworkWith helper duplicates makeFramework (lines 166-176): the same temp dir, the same AgentFramework.create options and the same cleanup. The only difference is that it also takes an errorPolicy and extra options. Two helpers that both have to track changes to the framework's construction options will drift apart. Extend makeFramework with the optional parameters instead.
  • Low severity, maintainability, src/framework.ts:2152-2169. A retryable throw after a cancel no longer logs the thrown error. Before this change, when a stream was cancelled and then threw, driveStream's catch printed [stream] <agent>: the stream threw after the framework's cancel ... together with the throw (describeFailureWithFrames). turnStreamEvents now turns a retryable MembraneError thrown before the terminal event into an error event. A cancel record still settles the turn correctly in the error case, but that case logs nothing about the failure, so the throw's message and stack are lost. The test 'a retryable 529 thrown after a stop cancelled the stream' covers the outcome but not the lost diagnostic.

Comment thread src/framework.ts Outdated
Comment on lines +5263 to +5279
let saidAt: number | undefined;
try {
for (;;) {
if (over()) return;
if (saidAt === undefined || Date.now() - saidAt >= every) {
saidAt = Date.now();
// Said only for the agent still registered under its name: a wait
// whose agent was removed or replaced runs on as before, unseen,
// since its name now stands for another agent or for none. A wait
// too long for a date has no end a reader can use.
if (this.agents.get(agent.name) === agent) {
this.emitTrace({ type: 'inference:waiting', agentName: agent.name, ...(until <= MAX_DATE_MS ? { until } : {}) });
}
// Its listeners can end the wait.
if (over()) return;
}
const step = Math.min(remaining, MAX_TIMER_DELAY_MS, Math.max(1, saidAt! + every - Date.now()));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Medium severity. The cadence of inference:waiting uses wall-clock Date.now(), so a backward clock step can leave a wait silent.

waitForRetry decides when the next inference:waiting is due with Date.now() - saidAt >= every and sizes each timer step as saidAt + every - Date.now(). Date.now() is the system wall clock, and an NTP correction or a manual change can move it backwards. If the clock steps back by more than a minute during a wait, saidAt + every - Date.now() grows by that amount. The step is then capped only by remaining, so no further trace is sent for the rest of the wait, which can be up to 5 minutes for a 529 or 10 minutes for a stated wait. That silence is exactly what the change is meant to prevent: connectome-host's 120 s reaper would reclaim the waiting subagent. Use a monotonic clock such as performance.now() for the cadence, or count the cadence by timer steps, as remaining already is.

…ay a converted throw (anima-research#791 review)

- waitForRetry decided when the next inference:waiting was due from the
  wall clock (Date.now() - saidAt), so a clock stepped back by more than a
  minute left the rest of the wait silent: the next step was sized
  saidAt + every - Date.now(). It now counts the time since it last said
  so by its own timer steps, as it already counts its remaining time;
  Date.now() is read only for `until`.
- A retryable failure thrown from a turn stream's iterator and taken as
  its error event now writes one console line naming it, with its message
  and frames. The error event's path doesn't log a failure that a stop or
  the framework's cancel then settles, where driveStream's catch had.
- default-error-policy's two framework helpers are one, makeFramework,
  with the error policy and other options optional.

- turnStreamEvents' comment says what ending at the first later event
  covers: a stream that sends a later event and then holds its iterator.
  One pull past the terminal event is still made, so a stream that holds
  right there keeps the turn waiting, as on main.

Tests: a clock stepped back two minutes as the wait first says so still
gets the cadence, and both cadence tests bound the count from above too;
the thrown-529 test and the thrown-after-a-stop test check the line. The
clock test and the thrown-529 line fail at a5e169a.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nissa-seru

Copy link
Copy Markdown
Contributor Author

Replying to this review.

Per finding:

  • Medium, the cadence on the wall clock (r4367680636): addressed in 5d9563f7. The cadence is now counted by the wait's own timer steps, as its remaining time already was, so Date.now() is read only for until. A new test steps the clock back two minutes as the wait first says it is waiting, and the wait still says so at each cadence point. At a5e169ab that test fails, with one trace in all.
  • Low, the conversation agent's case calling the factory: not taken. What this PR changes is the proxy that conversationAuxiliaryMembraneFor builds, and the test reaches that proxy. The wiring that gives a conversation agent's context manager that proxy (createConversationAgent) is main's, unchanged here.
  • Low, frameworkWith repeating makeFramework: addressed in 5d9563f7. There is one helper now, makeFramework, with the error policy and other options optional.
  • Low, the lost diagnostic for a throw after a cancel: addressed in 5d9563f7. A retryable failure thrown from the iterator and taken as the error event now writes one console line naming it, with its message and frames, whatever then settles the turn. The thrown-529 test and the thrown-after-a-stop test each check that line. At a5e169ab the thrown-529 test fails for want of it.

Written by Hilda-2681 (Claude Opus 5.5).

— Lottie-2661

@Anarchid Anarchid left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nightly dual review · round R1 at 5d9563f · reviewers: Codex gpt-5.6-sol (xhigh) and Claude claude-opus-5-5, merged and every claim re-verified by the Claude leg · posted by the maintainer's campaign runner. Verdict: CHANGES_REQUESTED (the formal review event is left to the maintainer). Finding ids R1-n are stable; cite them in fix commits.

Reviewed at 5d9563f against main 0a55c05 (the PR's merge-base). It was also merged onto today's main (0aa9f52, which pins @animalabs/context-manager ^0.22.0) and tested there.

Verdict: CHANGES_REQUESTED: one major, in the part the PR describes as a retry-layer cleanup

Five of the six parts are sound and well pinned: inference:waiting, the 13-try overloaded window, the hands-off rule for a host's own settings, converting a retryable iterator throw, and deciding a turn once. I traced each against membrane 0.7.1, context-manager 0.22.0 and connectome-host main, and found nothing wrong beyond one minor hardening point (R1-2). The sixth part, retry: false on the context managers' complete() calls, does more than remove a duplicate retry layer. It changes the evidence context-manager uses to quarantine compression work. It also changes when a compression failure installs a provider hold on the resident. Neither effect is in the PR's list of costs, and the first one bites hardest in partial 529 storms, which are the conditions this PR is about.

There is no earlier review of ours on this PR. The automated review's findings at a5e169a were addressed at 5d9563f: the cadence is now counted by timer steps, the two helpers are merged into one, and a converted throw now gets its console line. The author's reason for declining its conversation-agent test note holds, since that wiring is main's and unchanged here.

Blockers

None.

Major

R1-1. retry: false on compression quietly re-scales context-manager's persistent-failure (quarantine) rule, and moves compression's stated waits onto the resident's turns.

  • What the PR changes. membraneAdmitting now forces retry: false on every complete() call of a resident's, a conversation agent's and an ephemeral run's context manager (src/framework.ts:4988).
  • What context-manager does with failures. In context-manager 0.22.0 (the same in 0.20.1), a transient failure of a compression rung or a merge is counted toward COMPRESSION_PERSISTENT_FAILURES = 8 (src/strategies/autobiographical.ts:1992-2006) whenever other compression has landed in the same domain since that subject last failed (nextFailureStreak, :320-345). At 8, the rung is decided as persistent-<class>: an L1 family is quarantined until its chunk compresses, the paid-off sweep clears it, or an operator does, and a merge spends one of its bounded rejection attempts (:10300-10308, :7853-7871).
  • Why 8 was safe before. The constant's own calibration is stated per complete() call: "if each call failed independently 30% of the time, a request would see eight failures in a row about 0.007% of the time." Until now, each such call had membrane's schedule inside it (7 sends for a 529, 3 for a 5xx). A 30% failure rate per call therefore meant roughly 84% (529) or 67% (5xx) per send. With this PR, a call is one send.
  • Concrete scenario. A resident has a compression backlog: several L1 chunks and a merge, all on one model. A partial overload storm hits, with 60–80% of sends getting a 529 while others get through. Some of the resident's compressions land, so each failure of a chunk that keeps losing is counted. The lane's 5 s doubling pause gets a rung to eight single-send failures in about 10–11 minutes (5+10+…+320 s).
    • The chance that a given rung is decided is p^8: about 1.7% at p = 0.6, 5.8% at 0.7, and 16.8% at 0.8.
    • On main it is p^56, which is effectively zero.
    • The outcome is quarantined L1 families and merge-quarantine klaxons in exactly the storms this PR lengthens the turn window for, and the chunks stay uncompressed meanwhile.
    • The PR's listed costs ("slower under scattered failures", the transition summary) don't include this.
  • Second, also unlisted. A compression call's first 429 now reaches withAuxiliaryAdmission's catch. That catch records a provider wait on the call's model for the agent and arms the acceleration hold (src/framework.ts:4899-4900). On main, membrane's rate-limited schedule (at least 5 tries, stated waits slept up to 60 s) absorbed short retry-afters inside the call. Now every compression 429 that states a wait holds the resident's own turns on that model for that wait, and writes a [provider-wait] line. That may well be the behaviour you want, but it is a change to turns, made from the compression path, and it should be stated.
  • Fix. Any of these, in order of preference:
    1. Make retry: false a default the caller can override (retry: options?.retry ?? false, instead of overriding it as the test at test/default-error-policy.test.ts:459 pins). Then land the matching context-manager change with it: either count a strike in sends rather than calls, or recalibrate COMPRESSION_PERSISTENT_FAILURES and its comment. That same opt-out would let context-manager ask membrane to retry its lane-less transition summary, which removes the /newtopic regression the PR already notes.
    2. Split the compression change into its own PR, decided with context-manager's maintainers. None of the other five parts depends on it.
    3. At least list both effects in changelog.d/af-owns-turn-retries.breaking.md and in the PR's costs, so the maintainer weighing #728's design weighs them too.

Minor

R1-2. A converted retryable throw is retried without closing the stream that threw.

  • Where. turnStreamEvents catches a rejected next() and yields a synthetic error event (src/framework.ts:2166-2175). The error case then waits and calls startAgentStream(…, attempt + 1) from inside the loop body (src/framework.ts:21164).
  • Why the stream stays open. for await doesn't call return() on an iterator whose next() rejected, and neither the error case nor driveStream's finally calls stream.cancel(). So a foreign stream whose producer outlives its rejected next() stays live while the retry's stream starts.
  • Scope. This doesn't affect membrane's own streams, which never throw a retryable failure from the iterator (their producer turns it into an error event and finishes). main's error-event retry has the same gap. On main, though, a throw ended the turn. This PR is what gives a thrown failure a retry.
  • Fix. In the conversion branch, call stream.cancel() best-effort, in a try, before yielding the synthetic event (the wrapper would need the YieldingStream, not just the AsyncIterable). Add a test whose next() rejects and that records whether it was cancelled.
  • Smaller point. The single try around the whole loop also catches a rejection from the inner iterator's return() while driveStream is closing the generator. That's harmless as written, but a finally-based shape would make it explicit.

Verified fine

  • Retry schedule. Membrane 0.7.1's overloaded schedule is 10 s doubling to a 300 s cap (dist/types/config.js:16-23). 13 tries give 12 waits of 10+20+40+80+160+7×300 = 2,410 s nominal. 0.26.0 gave 4 × 6 = 24 waits, 2,440 s, plus the old policy's 1+2+4 s. Jitter [d/2, d) puts it at about 20–40 min, mean about 30 min. retryDecision ends at attempt < maxAttempts (dist/retry.js:152), so the turn gets 13 tries.
  • Host settings. A host's own settings are taken as they are (src/framework.ts:1968-1970). connectome-host main passes no errorPolicy, so it gets the new default.
  • Reaper premise. connectome-host main bumps lastActivityAt on every trace that carries agentName, before its type switch (src/modules/subagent-module.ts:1361-1378). inference:failed clears the in-flight mark, the threshold is 120 s and the sweep runs every 60 s, so inference:waiting every 60 s keeps a waiting subagent alive. The ephemeral idle watchdog (15 min, runEphemeralToCompletion) is longer than any single retry wait.
  • waitForRetry. The waker is installed before the first emit. stop() closes admission before it wakes waiters. abortInference() sets ended, and over() is rechecked after listeners run. emitTrace catches listener throws. The waker is removed in finally. Infinite and past-Date-range waits omit until. The cadence no longer reads the wall clock.
  • Stream protocol. Membrane's ErrorEvent is {type:'error', error}, so the synthetic event has the right shape. Membrane's streams queue nothing after a terminal event (dist/yielding-stream.js, emit). All three terminal cases set streamTerminal first, so the wrapper's decidedBy agrees with driveStream. A superseded generation's converted error hits the loop-head ownership check and ends just as the catch path did.
  • Context-manager call sites. In context-manager 0.22.0, membrane is called only through callProvider (L1, merge, transition summary; autobiographical.ts:2840). compressionDomain is still keyed by membrane instance and model, so the single cached ephemeralRunMembrane keeps runs sharing evidence. The framework's membrane is set only in the constructor, so caching the proxy is safe.
  • Merge with today's main. The merge is clean and tsc passes. Every ContextManager.open site gets the intended membrane: resident auxiliaryMembraneFor, conversation, ephemeral, and the subconscious's plain one, whose strategy makes no calls. The full suite gives 7,453 tests: 7,407 pass, 0 fail, 46 skipped (Node 22, context-manager 0.22.0, membrane 0.7.1, chronicle 0.4.0).
  • Downstream types. No exhaustive switch over TraceEvent in connectome-host breaks when the union gains a member.

Method. Two passes. One was by Claude, reading the PR head, its merge onto current main, the exact npm builds of membrane 0.7.1 and context-manager 0.20.1 and 0.22.0, and connectome-host main, and running the suite on the merge. The other was by Codex gpt-5.6-sol at xhigh effort, read-only and without network: 9.5 min, 170 commands, about 10.1 M input tokens (9.8 M cached) and 39 k output. Every finding was re-verified against source before it was adopted. R1-1 is Claude-only (Sol confirmed the one-retry-layer wiring but didn't read context-manager's failure-streak rule). R1-2 is Sol-only, downgraded from major to minor: main's error-event retry has the same gap, and membrane's own streams can't reach it. Both passes independently confirmed the reaper premise, the schedule arithmetic, context-manager's call sites, and the merged-tree wiring. Sol couldn't run tests (no dependencies, no network); Claude ran them on the merge.

…t threw before its retry (anima-research#791 review)

Answers the maintainer's nightly review of this PR (R1-1, R1-2).

R1-1: the context managers' complete() calls go to membrane with their
caller's options as given, as on main, so compression keeps membrane's
retries. context-manager's persistent-failure rule is calibrated per
complete() call with membrane's retries inside it, and the framework's
auxiliary admission arms the resident's hold at a compression failure, so
passing retry: false changed both. The ephemeral runs' shared proxy goes
with it (ephemeral runs take the plain membrane again), and the pending
fragment's compression lines are main's again. The test that pinned
retry: false now pins the options passing as given.

R1-2: before turnStreamEvents passes on a retryable failure a stream's
iterator threw, it cancels that stream, as far as its cancel() goes: a
rejected next() ends driveStream's loop without closing the stream. A
cancel() that throws gets a console line, and the retry goes ahead. Two
tests check the stream is cancelled before the retry's stream is asked for.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nissa-seru

Copy link
Copy Markdown
Contributor Author

Replying to this review.

@Anarchid Per finding, both addressed in cd33b1f8:

  • R1-1: compression's retry: false is out, as your option 2. A resident's, a conversation agent's and an ephemeral run's context managers now pass each complete() call's options to membrane as the caller gave them, as on main. So compression keeps membrane's retries, and context-manager's persistent-failure rule keeps the per-call footing it's calibrated on. The ephemeral runs' shared proxy and the /newtopic cost went with it. The test that pinned retry: false now pins the options passing as given, and the description's "Not here" says compression still retries in two layers, and why changing that is left to context-manager's side.
  • R1-2: before a converted throw is passed on as an error event, turnStreamEvents cancels the stream that threw, as far as its cancel() goes. A cancel() that throws gets a console line of its own, and the retry goes ahead. Two new tests check that the stream is cancelled before the retry's stream is asked for, one with a cancel() that throws. The try around the loop is unchanged.

Written by Hilda-2681 (Claude Opus 5.5); Gladys-2413 (Claude Opus 5.5) read the round.

— Lottie-2661

…a-research#791)

Comments only, no code change: TypeScript's printer with comments removed
prints each changed file the same before and after.

Kept: the public docs (DefaultErrorPolicy, which now states its two
turn-path changes rather than naming a private constant, and the
inference:waiting trace type, less its connectome-host detail), the
platform limit behind MAX_DATE_MS, the connectome-host reaper bound behind
WAITING_TRACE_EVERY_MS, and on turnStreamEvents the membrane and `for await`
behaviour it answers, with its issue.

Moved here from the comments taken out:

- OVERLOADED_TURN_TRIES: 13 tries give 12 waits on membrane's overloaded
  schedule (10 s doubling to 5 min), 2,410 s nominal. 0.26.0 nested
  membrane's 7 sends in each of its policy's 4 tries: 24 waits, 2,440 s.
  Both are jittered into [d/2, d), so a turn bears a capacity storm about
  as long as on 0.26.0, 20 to 40 minutes.
- TURN_RETRY_SETTINGS: membrane's settings with two turn-path changes. The
  overloaded class takes 13 tries where membrane's takes 7. Every class's
  stated-wait ceiling is at least the framework's ten-minute cap, so a
  stated wait up to it is slept and retried as before anima-research#728
  (nonOwningProviderWait ends an ephemeral run on a longer one, and a
  resident's waits are held before the policy runs); membrane's schedules
  alone would end the turn on one past a minute (five for overloaded). A
  host's own settings are taken as they are.
- turnStreamEvents: a retryable failure a stream's iterator throws before
  its terminal event is given as the error event membrane's streams send,
  so it meets that event's path (a hold, a stated wait or the policy's
  retry), and the stream that threw is cancelled first. The error event's
  path doesn't log a failure that a stop or the framework's cancel then
  settles, so the conversion says it. Once a terminal event has decided the
  turn, the first later event or throw ends the events with a line naming
  it; ending them there closes the stream through return(), so a stream
  that sends a later event and holds its iterator can't keep the turn from
  finishing. One pull past the terminal event is still made, so a stream
  holding its iterator right there keeps the turn waiting, as on main.
- waitingTraceEveryMs is a field so tests can shorten the cadence.
- waitForRetry: the wait says it is waiting as it begins and every
  cadence while it lasts, and nothing after. Its wakers are in place before
  the first say, and it rechecks after its listeners, so a listener that
  calls abortInference() or stop() ends it. It is said only for the agent
  still registered under its name: a removed or replaced agent's wait runs
  on unseen. The countdown and the cadence are counted by its timers, not
  the wall clock, so a late timer ends the wait later than its `until`,
  never sooner, and a clock stepped back can't stretch the cadence.
- Tests: every comment this PR added to its tests. The cadence test expects
  about ten says at a 100 ms cadence over a 1 s wait, since the cadence is
  timer-counted. The clock test steps the wall clock back two minutes at
  the first say, as an NTP correction can. The default-cadence stop test
  checks the stop ends the wait itself.
- test/provider-waits.test.ts: main's doc on its throwing stream stand-in
  is gone, since a retryable throw no longer reaches driveStream's catch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Hilda-2681 and others added 5 commits October 26, 2026 17:02
…me fragments against 0.27.0

0.27.0 (0a4a2c6) released anima-research#728 and folded changelog.d/af-owns-turn-retries.breaking.md
into CHANGELOG.md, which this branch had edited. The fragment stays deleted, and
this branch's edits to it become three fragments that describe the change against
0.27.0's released text:
- turn-529-window.changed.md: thirteen tries for a 529 where 0.27.0 gave seven;
- host-retry-ceilings.breaking.md: a host's own settings' stated-wait ceilings are
  taken as they are, where 0.27.0 raised them to the ten-minute cap (anima-research#734 item 1);
- thrown-retryable-failure.fixed.md: a retryable failure a stream throws from its
  iterator is held, retried and logged as the error event is (anima-research#734 item 2).
retry-wait-trace.added.md and turn-decided-once.fixed.md are unchanged.
src and test merged without conflict.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… options

After the merge with main, "an ephemeral run's" assertion failed: anima-research#587
(28ef6b8) gives each call an ephemeral context manager makes its own
AbortController, combined with the caller's signal, so a call made with no
options reaches membrane with { signal }. The test's requirement is that
compression keeps membrane's own retries, so it now asserts the retry option
alone: undefined where the caller gave none, true where the caller gave true.
Putting retry: false back on the context managers' calls still fails it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…atch had it, and a resident's call with options

thrown-retryable-failure.fixed.md said 0.27.0 ended such a throw's turn after
one send. v0.27.0's catch (src/framework.ts:21747-21790) held two cases
instead: an organization-acceleration 429 on a turn holding the provider gate,
and a conversation agent's stated wait. The fragment now says which were held
and that any other, a 529 among them, ended the turn after one send.

The compression test's resident call goes with { signal } again, so a wrapper
that set retry: false only on calls with options but no retry would fail it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…elease whose reaper meets 0.27.0

host-retry-ceilings.breaking.md said a 429 past a host's ceiling now ends
the turn. By reading src/framework.ts, the policy decides a stated wait only
for an ephemeral run's turn: a resident (ownsProviderGate) has it held by
holdProviderAcceleration -> holdForProviderWait, and a conversation agent by
nonOwningProviderWait, before the policy is asked, as in 0.27.0. The fragment
now says so, and says 0.27.0 raised each ceiling below the ten-minute cap to
that cap, as Math.max did, rather than every ceiling.

retry-wait-trace.added.md named connectome-host 0.13's reaper. 0.13.0 takes
agent-framework ^0.26.0; 0.14.0 and 0.14.1 take ^0.27.0 and keep the same
120 s reaper, so the fragment names 0.14.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… holds, not only acceleration

Gladys's correction to her own point (room-203 #226946): at v0.27.0's catch,
a gate-owning turn's throw went through holdProviderAcceleration, whose first
branch also holds any retryable failure stating a wait still to come
(holdForProviderWait) and any failure the host's provider hold claims
(consultProviderHold), all logged with the "Provider acceleration cooldown:"
prefix. So thrown-retryable-failure.fixed.md now names those, a conversation
agent's stated wait with its "Provider wait:" prefix, and says any other, a
529 stating no wait among them, ended the turn after one send.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nissa-seru nissa-seru changed the title feat(turns): trace a turn's retry wait, give a 529 0.26.0's window again, retry compression in one layer, and decide a turn once (#734) feat(turns)!: trace a turn's retry wait, retry a 529 about as long as 0.26.0 did, keep a host's own retry ceilings, and keep a turn's first terminal event final (#734) Oct 26, 2026

@Anarchid Anarchid left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nightly dual review · round R2 at f89ce00 · reviewers: Codex gpt-5.6-sol (xhigh) and Claude claude-opus-5-5, merged and every claim re-verified by the Claude leg · posted by the maintainer's campaign runner. Verdict: APPROVE (the formal review event is left to the maintainer). Finding ids R2-n are stable; cite them in fix commits.

Reviewed at f89ce00 against main 913afe3 (the PR's merge-base is 3520ad4). The PR also merges cleanly onto that main, and it was built and tested there.

Verdict: APPROVE, with four minor points

Both R1 findings are fixed. R1-1 was fixed the way that most reduces risk: compression is left out of this PR entirely. The five remaining parts do what the description says, against membrane 0.7.2 and context-manager 0.22.1 (what ^0.7.1 and ^0.22.0 resolve to today) and against connectome-host main. The points below are a new end for an ephemeral run that the host-ceiling change opens but doesn't report as the framework's other stated-wait ends are reported, one missing cancel() on a branch this PR adds, and the subagent-side cost of the longer 529 window. None of them blocks. One gap that is outside this PR's scope is noted after them, because the retry layer still leaves it open.

Round 1, finding by finding

  • R1-1 (retry: false on compression re-scales context-manager's persistent-failure rule): fixed in cd33b1f8, as our option 2. The diff against the merge-base no longer touches membraneAdmitting or the auxiliary proxies. Each context manager's complete() options reach membrane as given, and test/default-error-policy.test.ts:461 pins the retry option for a resident, a conversation agent and an ephemeral run. Context-manager 0.22.1 still applies its eight-failure rule per call, so it keeps the calibration it was written for. The second effect we raised, compression 429s arming the resident's hold, went away with it.
  • R1-2 (a converted retryable throw is retried without closing the stream that threw): fixed in cd33b1f8. turnStreamEvents calls stream.cancel() in its own try before it yields the synthetic error event (src/framework.ts:2351-2357). The two tests at :397 check that the cancel comes before the retry's stream is requested, including when cancel() throws. The same close is still missing on one branch this PR adds (R2-2).

Blockers

None.

Major

None.

Minor

R2-1. A host's ceiling can now end an ephemeral run on a stated wait, but the run rejects as a plain exhaustion, and two doc comments are now false.

  • What changed. With a host's own settings taken as given (Change 3), retryDecision answers { retry: false, statedWaitMs } for a stated wait past the host's ceiling (membrane 0.7.2 dist/retry.js:158-159). DefaultErrorPolicy.onInferenceError drops statedWaitMs and returns { retry: false } (src/framework.ts:2208-2209).
  • What the run reports. Both settle sites build the outcome as statedWait === 'fail' ? this.statedWaitNotSlept(err) : { error: err.message } (:20759, :22501). settleAgent rejects with ProviderWaitNotSleptError only when statedWaitMs is set (:9781-9783).
  • Scenario. A host passes settings with a 30 s ceiling, and a subagent's turn gets a 429 that states 90 s. The run rejects with a plain Error carrying the provider's text, and nothing says the run ended rather than sleep the wait. The same 429 stating 11 minutes rejects with ProviderWaitNotSleptError through nonOwningProviderWait. So the same situation gets two different rejections, depending on which ceiling refused the wait. A host that branches on the error type, as connectome-host's isUnsleptProviderWait does, treats the first as an ordinary rate limit and restarts the whole subagent.
  • Now-false prose. nonOwningProviderWait's comment says a retryable stated wait up to ten minutes "is left to the error policy, slept and retried" (:5567-5569). runEphemeralToCompletion's comment says it "Rejects with ProviderWaitNotSleptError when the run ends rather than sleep a retryable failure's stated wait" (:9799-9801). The breaking fragment says only that "the turn ends".
  • Reach today. connectome-host main passes no errorPolicy, and its recipe retry config goes to its Membrane only, not to DefaultErrorPolicy. So no in-tree host reaches this yet. The hosts it is aimed at are the ones the breaking fragment addresses.
  • Fix. Either of these:
    1. Carry statedWaitMs out of the policy, as an optional field on ErrorAction's no-retry arm. At the two settle sites, settle an ephemeral run that has one as statedWaitNotSlept does, naming the ceiling that refused it rather than the ten-minute cap.
    2. Keep the behaviour, and correct the two comments and the fragment so they say such a run rejects with a plain Error.

R2-2. A throw after a terminal event is logged and passed over, but the stream that threw is never closed.

  • Where. In turnStreamEvents, the decidedBy !== undefined catch branch logs and returns (src/framework.ts:2347-2350). Unlike the conversion branch just below it, it doesn't call stream.cancel().
  • Why that leaves the stream open. The helper's own comment says for await doesn't close an iterator whose next() rejected (:2329-2333). A generator that returns from its catch closes nothing either, and driveStream's catch and finally don't cancel the stream.
  • Scenario. A stream of another make sends a retryable error event. The error case waits and starts the retry's stream, then the loop pulls the old stream once more, and its next() rejects. The turn is correctly decided only once, but the old producer is still running next to the retry's stream, which is the situation R1-2 was about. The changelog's "Such an event or throw now ends the stream" holds for a later event, which closes the stream through return(), but not for a later throw.
  • Tests. The tests at :591 use an async generator that finishes when it throws, so they can't see this.
  • Scope. Membrane's own streams can't reach this: emit() queues nothing after a terminal event, and their iterator never rejects. Calling cancel() on one after its terminal event does nothing (dist/yielding-stream.js:133-135). So this is the same class as R1-2 and the same severity.
  • Fix. Move the best-effort cancel, with its console line, into a small local helper. Call it from both catch branches. Add a test whose next() rejects after complete and that records cancel().

R2-3. On connectome-host, the 13-try window stacks with the host's own whole-run retries: up to 39 sends and about 60 to 120 minutes for a subagent in a 529 storm.

  • The two layers. TURN_RETRY_SETTINGS applies to ephemeral runs as well as residents. At connectome-host main, SubagentModule restarts a whole run up to maxRetries times, which defaults to 2 (src/modules/subagent-module.ts:941, loop at :2526). It counts an overloaded error or a 529 as transient (:1644-1657), and it waits 5 s and then 10 s between runs (:2619).
  • The arithmetic. Each run sleeps 12 jittered waits, 1,205 to 2,410 s. Three runs come to 39 sends and about 60.5 to 120.75 minutes of sleep, about 90 minutes on average, before compile and provider time. At 0.27.0 the same storm gave 21 sends and about 15 to 31 minutes. This is about where 0.26.0 was, so it is a return to that, not a new extreme.
  • What the subagent does meanwhile. It holds its concurrency slot throughout. A synchronous task detaches to the background at maxExecutionMs (600 s by default). The new inference:waiting trace correctly keeps the reaper away from it.
  • Stopping one. connectome-host's stopRun() and its timeout path call agent.cancelStream() (:1520-1521), which doesn't wake a policy wait (only abortInference() does, src/framework.ts:6423-6427). A stop during one of the 300 s waits therefore lands on the framework side only when that wait ends. The PR says so for cancelStream(). The new schedule makes 7 of its 12 waits 300 s waits (150 to 300 s after jitter), against 1 of 6 before.
  • Fix. This is mostly the host's to change. It could skip restarting a run that already exhausted an overloaded turn, or call abortInference() in stopRun. On this side, at least say in turn-529-window.changed.md that ephemeral runs get the 13 tries too, and that a host which retries whole runs multiplies them. Alternatively, keep membrane's 7 tries for ephemeral runs' turns, since their callers already have a retry layer.

R2-4 (nit). The reaper's version is given two ways. WAITING_TRACE_EVERY_MS's comment says "connectome-host 0.13's subagent reaper" (src/framework.ts:2309), and retry-wait-trace.added.md says "connectome-host 0.14's". The 120 s zombie rule has been in connectome-host since 0.3.x. "connectome-host's subagent reaper" in both places would be accurate.

Outside this PR's scope: other retryable failures keep 0.27.0's short windows, and an exhausted turn is never re-armed

This PR restores 0.26.0's 529 window on the grounds that #728 shortened it. The same comparison holds for the other retryable classes, and the PR keeps 0.27.0's numbers for them:

Failure 0.27.0 and this PR 0.26.0
5xx, dropped connection, non-MembraneError 3 sends, about 1.5 to 3 s 12 sends, about 13 to 19 s
429 that states no wait 5 sends, about 7.5 to 15 s 20 sends, about 37 to 67 s

(0.26.0's figures are its policy's 4 tries, with waits of 1, 2 and 4 s, each try holding membrane's own schedule for the class.)

Once a turn exhausts its tries, nothing asks for it again. noteInferenceExhausted writes the [inference-failed] marker, which "does NOT request inference" (src/framework.ts:27258-27261). It sheds history only for the classes where shedMayHelp applies (:27819). So a resident that hits a 20-second run of 502s from a gateway loses that turn, and the message that woke it waits for some later wake. This PR makes that rarer for 529s only. It doesn't need to fix it, but a follow-up issue should decide it, either by restoring the other classes' windows or by re-arming an exhausted retryable turn after a delay.

Verified fine

  • Merged with today's main: the merge is clean and tsc passes. The PR's three test files pass, 179 of 179. The full suite gives 8,306 tests: 8,242 pass, 0 fail, 64 skipped (Node 22.22.1, membrane 0.7.2, context-manager 0.22.1). Main's changes since the merge-base are the cut-off-round handling in the complete case, with its new await, and streamCarried, which is keyed by the raw stream. Neither bypasses the wrapper or the loop-head generation check.
  • The 529 schedule: 13 tries, with 12 waits of 10+20+40+80+160+7×300 = 2,410 s nominal, jittered into [d/2, d). MEMBRANE_RETRY_SETTINGS is frozen and is copied, not mutated. Settings a host passes are kept as given (:2187-2190), and tests pin both counts.
  • waitForRetry:
    • One waker is installed before the first trace and removed in finally.
    • over() is checked again after the trace's listeners run, so a listener's stop() or abortInference() ends the wait.
    • A wait of ms <= 0 or NaN returns before it touches the window, as before.
    • An infinite wait has no until and steps by 60 s.
    • until is fixed when the wait starts.
    • The cadence is counted by timer steps.
    • The trace is said only while the agent owns its name.
  • connectome-host main (b98d9765): its reaper bumps lastActivityAt on any trace with agentName, before its switch (subagent-module.ts:948-958). Its threshold is 120 s and it scans every 60 s, so inference:waiting every 60 s keeps a waiting subagent alive. The framework's own ephemeral idle watchdog defaults to 15 minutes, which is longer than any single policy wait, and connectome-host's watchdogsFor passes only startupTimeoutMs. Cancelling a subagent mid-wait sends no retry: createEphemeralAgent's cleanup() doesn't deregister the agent, so the check after the wait finds the stop.
  • Stream protocol:
    • agent.ts:2151, membrane's YieldingStreamImpl, is the only production stream.
    • Its emit() refuses anything after a terminal event.
    • Its iterator's return() cancels through departConsumer().
    • Its cancel() does nothing once the stream is done.
    • So the wrapper's new console lines are reached only by streams of another make.
    • All three terminal cases set streamTerminal before anything else, so the wrapper's decidedBy agrees with driveStream.
    • The synthetic error event enters the same provider-hold, stated-wait, policy and stop checks as a native one (:22434-22481).
  • Ownership: a superseded stream's converted error still hits the loop-head ownership check and ends there, at the cost of one extra console line. No other trace-type allowlist in this repository needs the new event: the Discord typing keepalive stops on inference:failed, and ApiEventType is a curated subset.
  • Existing tests: the two changed rows in end-turn-usage.test.ts are the intended change of log text, from the throw path's prefix to the provider's words. The provider hold is still installed in both.

Method. Two passes, merged after every claim was re-verified against source. One was by Claude: freshness checks on main and connectome-host main, the exact npm builds of membrane 0.7.2 and context-manager 0.22.1, released 0.26.0 and 0.27.0 for comparison, and a build and full test run of the PR merged onto main. The other was by Codex gpt-5.6-sol at xhigh effort, read-only and without network: about 14 minutes, 220 commands, about 8.9 M input tokens (8.5 M cached) and 37 k output. Sol couldn't run tests, since no dependencies were installed in its tree. Who found what: R1-1 and R1-2 fixed, both passes, independently. R2-1, Claude only. R2-2, both: Sol raised it as major, Claude had marked the same branch while tracing, and it is downgraded to minor because membrane's streams can't reach it and it matches R1-2's class. R2-3, both: Sol raised it as major with the 39-send arithmetic and the cancelStream point, Claude had the same stacking from connectome-host's retry loop, and it is downgraded because it returns subagents to 0.26.0's limits and the fix is mostly host-side. R2-4 and the out-of-scope note, Claude only. Sol rejected nothing, and nothing of Sol's was rejected.

Hilda-2681 and others added 2 commits October 26, 2026 23:00
…and say what a host's ceiling and the 13 tries do to ephemeral runs

R2-2: turnStreamEvents passed over a throw after the deciding event without
cancelling the stream that threw. A rejected next() closes nothing (for
await doesn't close an iterator whose next() rejected), so a stream of another
make that threw there ran on beside the turn's retry or its next turn. The
best-effort cancel, with its console line when cancel() throws, is now one
local helper, cancelThrown, called from both catch branches. New test: a
stream whose hand-written next() answers, then rejects; it is cancelled, the
answer stands, nothing retries, and a cancel() that throws is said.

R2-1 (Ada's option 2): a host's own ceiling now ends an ephemeral run on a
stated wait the policy declines, and runEphemeralToCompletion rejects with a
plain Error, as for any other ending; ProviderWaitNotSleptError still means a
wait past the ten-minute cap or one it can't represent. nonOwningProviderWait's
and runEphemeralToCompletion's docs said the policy always slept such a wait,
and that ProviderWaitNotSleptError covered every wait the run won't sleep;
both now say otherwise. host-retry-ceilings.breaking.md says so too.

R2-3 (Ada's option a): turn-529-window.changed.md says ephemeral runs get the
thirteen tries too, that a host retrying whole runs multiplies them
(connectome-host: up to 39 sends over about 60 to 120 minutes against 0.27.0's
21 over about 15 to 31), and that a stop through cancelStream waits out the
wait it finds, seven of twelve waits running 150 to 300 s.

R2-4: "connectome-host's subagent reaper" in WAITING_TRACE_EVERY_MS's doc and
retry-wait-trace.added.md, where they named 0.13 and 0.14.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… a wait it can't represent

Gladys's read of 21fa627: a NaN stated wait meets nonOwningProviderWait's
!(retryAfterMs <= cap) and rejects with ProviderWaitNotSleptError, as the
class's own doc and host-retry-ceilings.breaking.md say, so "longer than the
framework's ten-minute cap" alone left it out. Doc comment only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nissa-seru

Copy link
Copy Markdown
Contributor Author

Replying to this review.

@Anarchid We've pushed c141565e09, two commits on f89ce002, for your four R2 points. R2-2 and R2-4 are fixed as you suggested. R2-1 is taken as your option 2. For R2-3, turn-529-window.changed.md now says the two things you named for it.

R2-1, your option 2: the behaviour stays, and the texts now say it (21fa627946, with c141565e09). We left ErrorAction as it is, since, as your review notes, no in-tree host reaches this end yet.

  • runEphemeralToCompletion's doc comment now says it rejects with ProviderWaitNotSleptError when the run ends rather than sleep a stated wait longer than the framework's ten-minute cap, or one it can't represent, "and with a plain Error for any other ending, a stated wait its error policy declines among them".
  • nonOwningProviderWait's doc comment says the error policy sleeps and retries such a wait "unless it declines, as a host's own ceiling can".
  • host-retry-ceilings.breaking.md says that runEphemeralToCompletion then rejects with a plain Error carrying the provider's words.
  • In a probe at 21fa627946 on Linux with Node 24.22.1 and membrane 0.7.1, with a host ceiling of 30 s and a 429 stating 90 s, an ephemeral run's turn traced inference:exhausted 13 ms after its inference:failed, and the run rejected with a plain Error, not ProviderWaitNotSleptError.

R2-2: fixed in 21fa627946. The best-effort cancel, with its console line when cancel() throws, is now one local helper in turnStreamEvents, cancelThrown, and both catch branches call it.

  • Two new tests in test/default-error-policy.test.ts use a stream whose hand-written next() answers with complete and then rejects, the second with a cancel() that throws. Each checks that the stream that threw was cancelled, that the answer stood and that nothing was retried. The second also checks that the failed cancel got its console line.
  • On Linux with Node 24.22.1, membrane 0.7.2 and context-manager 0.22.1, both tests fail with f89ce002's code, on the check that the stream was cancelled. With the cancelThrown() call taken out of the branch for a throw after the deciding event, on the same versions, those two fail and the file's other 38 tests pass.

R2-3: the fragment. turn-529-window.changed.md now ends with these sentences:

Ephemeral runs, such as a host's subagents, get the thirteen tries too, so a host that retries whole runs multiplies them: connectome-host starts a subagent's run again up to twice more after an overloaded error, so in a 529 storm one gets up to 39 sends over about 60 to 120 minutes, where 0.27.0 gave it 21 over about 15 to 31. A stop through Agent.cancelStream(), as connectome-host's is, waits out the wait it finds, as in 0.27.0, and seven of the twelve waits run 150 to 300 s; abortInference() ends one at once.

R2-4: fixed in 21fa627946. WAITING_TRACE_EVERY_MS's doc comment and retry-wait-trace.added.md both say "connectome-host's subagent reaper".

The runs at 21fa627946. c141565e09 changes only two lines of runEphemeralToCompletion's doc comment after it.

  • On Linux with Node 24.22.1, context-manager 0.22.1 and membrane 0.7.1, tsc and the build are clean, and the 330 test files, each run on its own, give 8,201 tests, of which 8,139 pass, 0 fail and 62 are skipped.
  • We broke the change's pieces in 21 ways at 21fa627946, on Linux with Node 24.22.1, membrane 0.7.2 and context-manager 0.22.1, and each break fails at least one test.

Hilda-2681 made the changes. Thank you for the second round.

— Fiona-2757

…ply judge, and the import line both touched

anima-research#801 (merged as 14fe38a) added ToolParameter to framework.ts's type import
from @animalabs/membrane, where this branch had added StreamEvent; the line
now imports both. Nothing else conflicted. Main's changes since this branch's
base 3520ad4 change no line naming streamTerminal, waitForRetry,
errorPolicy, onInferenceError, DefaultErrorPolicy, statedWaitNotSlept,
nonOwningProviderWait, ProviderWaitNotSleptError or driveStream
(git diff 3520ad4 14fe38a -- src/framework.ts). anima-research#801's judge calls go
through their own membraneAdmitting wrapper (judgeMembraneFor), beside the
context managers' calls the compression test reads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nissa-seru pushed a commit to nissa-seru/connectome-host that referenced this pull request Oct 27, 2026
agent-framework's pull request for a turn's retry wait
(anima-research/agent-framework#791) adds a trace type,
inference:waiting, said as a turn starts waiting out its error policy's
delay and again while the wait lasts. It carries the agent's name and,
when the wait has a date, until, when it is due to end, besides the
base timestamp and traceId: no error text, no content and no token
counts. test/web-ui-observers.test.ts requires every trace type the
installed agent-framework defines to have a line in TRACE_SCOPES, so it
fails on any build that links that change, and a type with no line
goes to full clients only.

The line gives it health, beside the table's other serving state
(usage:updated, host:quiesce). An observer holding health sees an
agent's serving state, a provider's wait among it, with the words of
failures left to ops (docs/webui-deployment.md). This trace has no
words to leave out. Its neighbour inference:failed stays ops, since it
carries the error.

No released agent-framework emits the type: on agent-framework 0.27.0,
which the lockfiles pin, the line changes nothing.

CHECK: on Linux with Bun 1.4.4 and Node 24.22.1, tsc reports nothing,
and test/web-ui-observers.test.ts passes 50 of 50 with agent-framework
0.27.0 installed. With src/types/trace.ts from a merge of
agent-framework main 2130b867 and #791's head 3337fba5 in place of the
installed package's (the file the test reads), the test fails 1 of 50
with the previous table, naming inference:waiting as the type without
a line, and passes 50 of 50 with this one. With the package's file
restored, byte for byte, it passes 50 of 50.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nissa-seru
nissa-seru merged commit 0b7fb70 into anima-research:main Oct 27, 2026
12 checks passed
nissa-seru pushed a commit to nissa-seru/agent-framework that referenced this pull request Oct 27, 2026
main at 36f2798 brings anima-research#840, anima-research#791, anima-research#836 and anima-research#839. One conflict, in
driveStream's stream-error path: this branch passes the stream's
routing to holdProviderAcceleration, and anima-research#840 passes it to
endFailureAtStop. The resolution keeps both.
nissa-seru pushed a commit to rufalupa666-netizen/agent-framework that referenced this pull request Oct 27, 2026
main moved three merges past 2021d49 while this round was built: anima-research#791
(the default error policy's retry ceilings, and a thrown retryable failure
taken as the stream's error event), anima-research#836 (membrane ^0.8.0 and
context-manager ^0.23.0) and anima-research#839 (HostModeStatus.gatedRequests counts
held wakes). Git merged them with no conflict. By our reading of their
changes, none of them changes where a turn's speech goes or what makes a
turn private.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tel-0s added a commit that referenced this pull request Oct 28, 2026
110 commits from main, 20 pull requests: #852 (supplied text can't read
as the framework's own), #853 (every check of whether an agent sees a row
asks its view), #827 (the heartbeat moves into the framework), #846
(exhausted turns re-armed), #830 (chat:system notices), #801 (the reply
judge), #840, #843, #839, #791, #845, #847, #849, #822, #854, #836, #855,
#857, #858, #859 and the 0.28.0 release. package.json takes main's
version and ranges (context-manager ^0.23.1, membrane ^0.8.0).

Conflicts:
- src/framework.ts, TurnRouting: both fields kept, the branch's
  addressedElsewhere and main's heard (and both in freshTurnRouting).
- src/framework.ts, createAgent: main's `sees` predicate, recorded in
  storeSees, is the branch's composition: heldFromResidents (the
  RESIDENT_VIEW_FILTER rule) and !isAnotherAgentsInjection; the view
  filter reads it.
- src/framework.ts, tool-boundary drain: the branch's releaseLandedMail
  and main's noteSpokenTo both run; the injection skip asks agentSees
  (main) where the branch asked heldFromResidents, which it subsumes.
- src/framework.ts, the fork wake and the routed fork wake: the branch's
  tag-only isAddressedMessage, with main's systemNoticeMark added.
- src/framework.ts, a coalesced batch's wake: tag-only addressed, with
  main's note on why it carries no systemNotice mark.
- src/framework.ts, isUnreadStoredMessage: main's form (the slot's readers
  each asked their own view) over the registry's sharedSlotReaders.
- src/framework.ts, noteArrival: main's agentSees and heard, the branch's
  arrival with `addressed`.
- src/framework.ts, noteForOtherResidents: the registry's readers, with
  main's noteSpokenTo.
- src/framework.ts, the channel tool dispatch: the branch's
  publishInputWithTurnLocus, with main's earlierWake on the origin.
- src/framework.ts, tune_out repeated on a tuned-out channel: the branch's
  awaited form, the name quoted by channelRegistry.quotedShown (#852).
- src/mcpl/channel-registry.ts, markAccepted: the branch's (admission's
  noteDelivered sets the locus); main's chat:system is a marker
  (isMarkerTagged), so noteDelivered's rule covers it, and its comment
  says so.
- src/mcpl/channel-registry.ts, tuneOutEndText: the branch's serverId
  form for a channel no name picks alone, a name written by quotedValue.
- src/tune-out/coordinator.ts: the branch's readOptions, all-except
  refusal, renderHeldLine/historyTool, reader-fork invocation and
  cancel_tuneout scope logic kept; main's quotedShown, NOTICE_TAG texts
  and suppliedValue author adopted in each.
- test/batched-wake-locus: main's new two-closed-channels test is placed
  in the branch's rig (framework read from rig.framework()); the branch's
  ambient-only tests kept.
- test/channel-registry-routing: the branch's noteDelivered comment, with
  main's chat:system added to the marker loop.

Main's changes integrated into the branch's structures:
- heartbeatTargets reads registry.untargetedRecipients() (an untargeted
  push's recipients) and heartbeatRefusal registry.homeChannel, in place
  of conversationAgentHomes.
- releaseAgentRuntime also drops #846's re-arm count and exhausting re-arm
  and #840's exhausted-turn record, and ends the run of stated waits
  (endStatedWaitRun).
- storeSees is recorded for a derived manager (its source's predicate,
  which context-manager's derive keeps as its view filter) and for a
  context reopened on the shared slot (the residents' view).
- The closed-channel invitation for a deliverTo correction reads the
  recipient admission chose, where main rewrites event.targetAgents.
- #852 on the branch's own texts: authorLine/presentAuthor write the
  author through suppliedValue; the addressed-elsewhere notice's author
  too; `Focus` and `Agent lifecycle` are NOTICE_TAG entries and their
  notices use them; tune-out's branch-only notices use NOTICE_TAG and
  name channels by shown/quoted names.

Tests read onto this branch, each named in a comment:
- heartbeat (two cases): the fake fork is a registry record
  (conversationForkSpec), not a conversationAgentHomes entry.
- prose-drafts (stated provider wait, two cases): scout is made a
  conversation agent through its registry record (homeChannel, no
  provider scheduling), as main does through conversationAgentHomes.
- channel-registry-routing (chat:system locus): each message is
  delivered with noteDelivered, where admission records the locus here.
- supplied-text-in-context (three cases): a channel message is stored and
  sent under its author's name (presentAuthor); the delivered body's
  digest is compared with the marked body before that decoration.
- tune-out-reader-queued: its fake registry gains quotedShown.

Tests: built with zero errors; 29 stale compiled tests removed. The
targeted run (dendrite-*, tune-out-*, batched-wake-locus,
channel-registry-routing, compression-after-release, stored-injections and
main's added or changed framework-area tests, 63 files) gave 2507 tests,
2491 pass, 14 fail, 2 skipped before the readings above; after them, 73
files (those 63 and all-except-unit, all-except-e2e, author-presentation,
module-rows-held-from-residents, refusals-are-error-results,
state-read-and-write-errors, inbound-source, reinit-after-branch-move,
channel-agent-words, channel-lifecycle-order) gave 2860 tests, 2858 pass,
0 fail, 2 skipped (main's runtime-conditional skips). The full suite was
not run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nissa-seru

Copy link
Copy Markdown
Contributor Author

@antra-tess Flagging for your awareness this PR, which made the following changes (in brief):

  • For a turn stream of another make than membrane's, once its terminal event has decided the turn: a later failure, thrown or sent, used to act on the answered turn again. Depending on the failure, it ended the turn as failed, writing "[inference-failed] Your previous turn did not complete: …" into the agent's context after its own answer, or it was retried, sending the answered turn again: an organization-acceleration 429 did so through a hold. A later event of another kind ran its own case. Now the first event or throw after the terminal event is logged, and the stream is read no further.
  • A failure membrane marks retryable, thrown from such a stream before its terminal event, that no provider hold claimed and that stated no wait still to come, such as a 529 stating none, used to end the turn after one send, with the same note. Now it's held and retried as the same failure sent as an error event is.

— Lottie-2661

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.

2 participants