Repository navigation
build(deps)!: membrane ^0.6.0, context-manager ^0.16.0 and Node 22.12 or later - #582
Merged
Anarchid merged 20 commits intoOct 22, 2026
Merged
Conversation
membrane 0.6.0 gives every adapter's tool_use input as an object, marking arguments that weren't one (its anima-research#126), and carries the per-call progressTimeoutMs (anima-research#119). context-manager has to take 0.6.0 in its own range at the same time: otherwise npm gives it a nested copy, and the framework's Membrane is no longer the type context-manager's options name. That floor comes with the context-manager release that carries it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An OpenAI-family provider can send a tool call's arguments as the text
"null". membrane 0.5.87's OpenAI-family adapters parsed that with
safeParseJson, which gave null, and a tool that read a field of its
input answered the model with a TypeError: skip_reply's ("Cannot read
properties of null (reading 'wake_in_seconds')"), channel_open's,
channel_close's, channel_publish's, and any module tool's that reads a
field. From membrane 0.6.0 every adapter hands the input over as a plain
object, so "null" arrives as {}, with the text kept in unparseableInput.
The test parses a reply through membrane's OpenAI-compatible adapter
(the HTTP call stubbed) that calls skip_reply with "null" arguments, and
checks the input is {} with the text beside it. It then runs that reply
through the framework, where skip_reply runs as a call with no
arguments: a tool:completed for the call, the turn ended there, and
nothing more requested.
On main's membrane 0.5.87 it fails at the input (null where {} is
expected). With those two assertions taken out, the framework half
fails too, with the call's tool:failed carrying the TypeError above.
…r 0.15's sharded write From context-manager 0.15 a sharded write that throws partway removes the shards it wrote before it rethrows, and shards stay only when that removal fails as well. This change's context-manager floor is past 0.15, where the two tests that pinned 0.14's partial write would always be skipped, so they become: - a body whose write throws on its first shard lands nothing: the drain cuts the agent at it, and at the turn end it's stored whole, followed by what was queued behind it; - Ansel-2428's probe of the boundary carrying landed shards as the next compile renders them, now driven by a removal that fails (the store's removeBodyGroup throws) after the strategy throws on the Nth shard. It is the only test that pins asCompiled's join of shards, which a failed removal still reaches. The version helper and the skip go with them. Both tests need that floor: on 0.14 the first one's shard stays. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… MCPL lanes, and drop it at the drain context-manager now refuses a write whose content doesn't match ContentBlock, with a MalformedContentError, and exports malformedContentPlaces, a check that names where without throwing (its anima-research#185). - Ingress: handleMcplChannelIncoming and handleMcplPushEvent ask malformedContentPlaces before anything else, ahead of their empty-content backstops. Content in which it names any place is refused there: no row, no wake, no conversation fork, a stderr line naming the server, the channel where there is one, the message or event, and the places, and a new trace, mcpl:malformed-content-refused. Past that point a direct write would throw out of the lane's handler, and a deferred one would reach the queue. - Drains: every drain writes through writeDrainedInOrder. There a MalformedContentError, after the landed check, is logged and dropped instead of cutting the agent and requeueing: the store refuses that content before storing any of it and would at every flush, so requeued it held the agent's later deferred writes for good. Left in the un-acked set, the next ack takes it out of the durable queue. logDeferredWriteError gains the dropped wording. - A tool_use dropped this way lets a tool_result queued behind it be stored alone. The drain's comment says so, and so does puppetToolCall's queuing site, the one place that queues such a pair. - writeDrainedInOrder's comment says what context-manager 0.15 changed for a sharded body: 0.14 left the shards a throw stopped, 0.15 removes them, and they stay only when that removal fails as well. Boot's scan counts any landed shard, which covers a body a 0.14 write left. Tests: test/malformed-content-refusal.test.ts drives both lanes with three malformed shapes each (a string block, a non-string text, a string image source), checking the placement, rows, wakes, trace and line, and a tool_use whose input isn't an object, which the store mends, as stored. deferred-window-only gains a malformed interjection queued ahead of another at a tool boundary: the second is carried and stored, the first is nowhere and not queued again, and the line says it was dropped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Three files conflicted, and each takes both sides: - package.json: this branch's membrane ^0.6.0 beside main's new @modelcontextprotocol/client ^2.3.1. - src/framework.ts, writeDrainedInOrder's doc: this branch's account of context-manager 0.15's sharded write (0.14 left a throw's shards, 0.15 removes them, they stay only when the removal fails too), with main's new last sentence on a store that can't be read back. - test/deferred-window-only.test.ts: main's new imports, and both new tests at the same spot, this branch's malformed-write drop and main's sharded body carried as the next compile renders it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
context-manager 0.16.0 is the release that carries the refusal of malformed writes this branch's ingress check and drain drop answer, and it requires membrane ^0.6.0, the floor this branch already takes, so a host installs one membrane. The refusal fragment now names the release. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ompressionWithdrawn (anima-research#549) MaintenanceWithdrawnError was a plain Error, so context-manager could not tell a call the framework withdrew, under a surgery's hold or once stop() has begun, from a failed one: a call withdrawn after a chunk's canonical call was refused was recorded as that fallback rung's provider error, and once every rung had failed, the chunk's request family was quarantined, with its alert. It now extends context-manager 0.16.0's CompressionWithdrawn, whose constructor sets the brand context-manager reads a withdrawal by, and whose doc names subclassing as the way in. Every rung of the ladder and the merge path rethrow it unchanged, recording nothing. Its name and the two catch sites that check instanceof MaintenanceWithdrawnError are as they were. Its docstring now says what 0.16.0 does with it, where it described 0.14's ladder, and refuseHeldProviderModel's no longer says context-manager's lane paces from a corrective round no release carries yet: 0.16.0's strategy doesn't read the refusal's retryAfterMs. The test runs a compression through the framework's own maintenance membrane: the canonical call is refused, a surgery takes the store during it, and the source-only rung's call is withdrawn at the gate. The tick rethrows the withdrawal, and nothing is quarantined. With the error extending Error again, the tick ends normally and the chunk is quarantined. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…6.0 require Both dependencies now declare engines.node >=22.12.0 and test at 22.12.0 exactly, at 24 and at the newest Node. agent-framework still declared >=20.0.0 and tested at 20 and 24, a combination neither dependency supports. engines.node is now >=22.12.0; CI's matrix is 22.12.0, 24 and latest on ubuntu, with macOS at the two ends, as theirs is; the publish workflow's build-and-test job runs at 22.12.0. npm test's --test-timeout bounds a whole file under Node 22.12, as it did under 20, and each test under 24 (a probe: two 3 s tests in one file, --test-timeout=4000, cancel the second under 22.12.0 and 20.19.0 and pass under 24.22.0), so CONTRIBUTING and ci.yml's comments name 22.12 where they named 20. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ger ^0.16.0 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ma-research#501 skip_reply reads a null input as no wake since agent-framework#501, so skip_reply running as a call with no arguments passes with either input. The header no longer names skip_reply among the tools that answered a null input with a TypeError, and says the input assertions are what need membrane 0.6.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…written
context-manager 0.16.0 refuses a write whose content doesn't match
ContentBlock. A write the framework stores at once got that refusal at
its call, but one deferred while a turn ran was refused only at a later
flush, which its writer never saw, and an edit of a held write could
replace valid content with content the drain then dropped.
- addMessage, which a module's writes, the MCPL lanes' and the framework's
notices take, and editMessage, for a write still held, run
malformedContentPlaces and throw
MalformedContentError ("Message refused" / "Edit of <id> refused") as the
store's own write and edit do, whether the write would be stored at once
or deferred.
- A process response whose addMessages hold such a message is refused
whole, before any of it is applied: none of its messages is added and its
inference request isn't queued. dispatchProcessEventToModules logs a
[process-response-refused] line naming the module and goes on with the
other modules' responses.
- The MCPL lanes' check stays, as the backstop for events that never
crossed the MCPL boundary, whose converter makes every block one the
store accepts. Absent content is now left to the empty-content backstop,
as before this branch.
- The drain's drop stays for entries a process before this one persisted.
Its comments, writeDrainedInOrder's return contract and the sibling drain
comments say a malformed entry is dropped and what follows goes on.
- Comments that described context-manager before 0.16.0 say what it does
now: the recovery comment on string and number blocks, and
releaseProviderWait's on releaseCompressionPause, which 0.16.0 doesn't
have. The pair comment no longer says no path reaches puppetToolCall's
queued pair, and says a stored tool_use whose tool_result is dropped as
malformed stays unanswered.
Tests: malformed-content-writes (new) throws a module's malformed write
during a turn and while idle, refuses a held write's malformed and [null]
edits and lands the original, and refuses a process response whole while
another module's applies. host-quiesce gains a persisted malformed entry
that boot restores, the first flush drops, and the ack takes out of the
recovery file. malformed-content-refusal checks the store itself, each
trace's eventId, a fork that conversation routing would bind, and absent
content. deferred-window-only's drop test places its malformed entry in
the queue as boot's restore would, since addMessage refuses it now, and
host-quiesce's stuck-write test queues well-formed content.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The check's load hook goes in through register below Node 22.15 and through registerHooks from there, and both checks ran only on 24, so the branch a host on the new 22.12 floor takes never ran. Both workflows' pack checks now run on 22.12.0 and 24, so it runs before a merge and before a release. The README's Node requirement and the timeout test's header say 22.12, where they said 20; the header keeps the Node 20 measurement of the slowest file as one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…floors
The floors fragment adds what context-manager 0.16.0 does with malformed
content an older version stored (read mended or left out, with a line per
message, and a request from such a message can miss the prompt cache
once) and that an OpenAI-family tool call whose arguments are null now
reaches the tool as {} (membrane 0.6.0). Two test comments stop calling
context-manager 0.14.1 and membrane 0.5.87 this package's floor, and the
error-type list names 0.6.0, whose types it matches.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… applies e6348926 checked a response's addMessages at the start of applyProcessResponse and caught the refusal in the dispatch loop. That left two gaps: an edit with content the store won't take was found only by editMessage, after the response's messages were added, so the loop's catch logged "refused" for a response half applied; and the surface's acceptance was noted from every response before any was checked, so console input whose only message was refused still counted as accepted. The check is now malformedResponseContent, over a response's addMessages and editMessages, run in dispatchProcessEventToModules before anything is applied. A refused response is left out whole, its module state update and inference request with it, and a [process-response-refused] line names the module; the acceptance counts only the responses that apply. The loop no longer catches anything from applyProcessResponse, so a throw there propagates as it did before this branch. process:completed still carries every response, as what the modules returned. Tests: malformed-content-writes' response test adds a module whose edit is malformed, whose message must not land, and a new test sends console input twice, counting it accepted for a well-formed message and not for a refused one. The fragment says a response with such content in a message or an edit is refused whole, and what that leaves out. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
process:completed and logProcessEvent got every module response, so a
response refused for malformed content appeared there as one that
applied, and the bundled UI's event log counted its messages ("+N msg")
though none was stored. Both now get the responses that applied; the
[process-response-refused] line names a refused one. Before this branch a
response the store refused threw out of the dispatch, and no
process:completed was emitted at all.
Also:
- The fragment is renamed malformed-content-refused.fixed.md: its old
185- slug took context-manager anima-research#185's number, and agent-framework anima-research#185
is an unrelated MCPL issue.
- The fragment, the drain's doc and the description name the other way a
malformed write reaches the drain's drop now that writes are refused
before they're queued: a strategy's chunking splitting well-formed
content into a shard the store refuses.
- releaseProviderWait's comment says what was checked: context-manager
0.16.0, this package's floor, has no releaseCompressionPause.
Tests: malformed-content-writes' response test checks the ping's
process:completed trace lists only the response that applied.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… that isn't text
From af-floors-r1's sheet:
- A process response refused for malformed content left no structured
record once process:completed and the process log listed only the
responses that applied, where both lanes' refusals have a trace. The
dispatch now emits process:response-refused (moduleName, eventType,
where, places) beside its line. malformedResponseContent returns where
and the places rather than a message it claimed was the store's own.
- The API's message.send accepted any truthy content, though
MessageSendParams declares a string and ApiModule stores it as one
text block: content 5 was answered "pending" and then refused at
dispatch, where the client never heard. It's refused at the call now.
- Docs: ProcessLogEntry and ProcessLogQuery say the log holds the
responses that applied; the boot recovery comment says a recovered
write the store refuses is dropped at its flush, where it said fresh
writes weren't checked; shedMayHelp's doc says membrane 0.5.87 is below
this package's floor; held-writes' copy of matchesFilter names 0.16.0,
whose rule is the same as 0.14.1's.
- Fragments: a tool given null arguments receives {} (agent-framework's
ToolCall carries no unparseableInput; membrane keeps the text on the
block), the new trace and the API's refusal, and a puppetToolCall pair
among what can reach the drain's drop.
Tests: the response test turns on process logging and checks the stored
entry and the refusal traces; a new test refuses message.send's
non-text content before any event is queued and accepts text; the
routing test sends visible text beside a malformed block and checks the
same message well-formed spawns a fork; absent content is tested on both
lanes, undefined and null; and the boot test puts the malformed entry
between two writes, so one is queued behind it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fragment said message.send "answered pending and the message was later refused", which describes this branch before its check, not the release a host upgrades from: 0.23.0 takes context-manager ^0.14.1, which stores such a text block as given. It now says so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…and the routing test's wake From the final round's review: - The response test checks each process:response-refused trace's places exactly, as malformedContentPlaces gives them for the message or edit it refused, with the edit's content malformed at a different place from the message's, so a trace carrying the wrong write's places fails. - It counts the [process-response-refused] lines per module: one each for the two refused responses, none for the one that applied. - The routing test sends its malformed message with triggerInference on and checks no inference starts; the well-formed control stays off. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main moved the turn-start flush into flushForStart(agent, 'turn-start'), which the coalescing assembly's flush shares. The one conflict was that block's comment: the call site keeps main's code and comment, with this branch's sentence that a message the store refuses as malformed is dropped and what follows goes on. flushForStart's doc says the same of its drain, which writes through writeDrainedInOrder. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Required label not found on this PR. |
This was referenced Oct 19, 2026
|
Related: #643 (authored by Ivo, published by Nestor under k-res-dev) is a candidate that merges this PR's branch (73477a8) into current main and relocks onto context-manager ^0.17.1 / membrane ^0.6.0, preserving this PR's ancestry. Whether #643 supersedes or folds into #582 is the maintainers' call; nothing here is closed or rewritten. |
nissa-seru
pushed a commit
to nissa-seru/agent-framework
that referenced
this pull request
Oct 21, 2026
…is change needs anima-research#643 is main 4e3fea3 merged with anima-research#582's floors and context-manager ^0.18.0. Its membrane ^0.6.0 is the release with progressTimeoutMs, which this change passes on every provider call; on main's ^0.5.87 it can't typecheck. Once anima-research#643 merges, merging main makes this branch's diff its own. Resolved: - src/mcpl/inference-router.ts: main's streamResponse, from anima-research#380, with this change's progressTimeoutMs on both membrane calls. Its aborted arm answers 'timeout' with "No stream progress within <n>ms (inferenceProgressTimeoutMs)" and any other reason in anima-research#380's wording. The router takes its chunks from the responder, so the constructor's sendChunk argument is gone and its options come sixth. - src/agent.ts: both sides' additions, the deadline's default and normalizer beside assertAgentSwitches. - src/framework.ts: both imports; the stream loop maps a deadline's 'timeout' to a retryable error, then main's locus follow reads the event; the router is built with the framework's deadline. Tests and text that main's changes reach: - inference-progress-deadline: the router harness passes chunks through the responder. anima-research#380's own failure answers stay in its tests. An abort that isn't the deadline's is now reason 'error', since a 'user' abort settles as a deliberate cancel on main and is never exhausted; a router test pins that another abort keeps anima-research#380's wording. - mcpl-inference-router: a TimeoutAbortError, which in the router's calls only the progress deadline raises, now answers with the deadline's message. - tool-result-guard: the ephemeral run's abort shape is reason 'error'. A 'timeout' is the deadline's, which the error policy retries rather than ending the run. - The changelog fragment drops its bullet on MCPL error answers, which is anima-research#380's fix. Co-Authored-By: Claude Opus 5.5 <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.
agent-framework now depends on membrane
^0.6.0and context-manager^0.16.0, and needs Node 22.12 or later, as both of them do. Three changes come with them:CompressionWithdrawn, so it no longer counts against a chunk's compression;Fixes #549.
Problem
^0.5.87and context-manager^0.14.1. Below 1.0 a caret range stays within its minor, so neither admits membrane 0.6.0 or context-manager 0.15 or 0.16. membrane 0.6.0 can't be taken on its own: context-manager 0.14.1's own range,^0.5.69, excludes it, so npm installs a second membrane for context-manager, and the type check fails ("Types have separate declarations of a private property 'adapter'"). context-manager 0.16.0 requires membrane^0.6.0, so the two floors go together.ContentBlock, with aMalformedContentError, and stores nothing of it (fix(store): reads leave out content that doesn't match ContentBlock, and writes refuse it (#184) context-manager#185, for addMessage stores blocks its own estimator can't read; every compile then throws, across restarts context-manager#184, where such content was stored and every compile then threw). A message reaches the store either at once or, while its agent's turn is running or the host is quiesced, through the deferred-write queue. Written at once, refused content throws at the writer, out of an MCPL lane's handler for instance. Queued, the writer heard nothing: the content was refused only at a later flush, and at every flush, and a deferred write that fails is put back and holds its agent's later deferred writes behind it.CompressionWithdrawnfor maintenance withdrawals (CM #196's fix needs the host half) #549. The framework throwsMaintenanceWithdrawnErrorfor a maintenance call it withdraws, under a surgery's hold or oncestop()has begun. It was a plainError, so context-manager couldn't tell it from a failed call. A call withdrawn after a chunk's canonical call was refused was recorded as that fallback rung's provider error, and once every rung had failed, the chunk's request family was quarantined, with its alert. context-manager 0.16.0 exportsCompressionWithdrawnfor this.engines.node>=22.12.0, and test at Node 22.12.0, 24 and the newest. agent-framework declared>=20.0.0and tested at 20 and 24.Changes
What each part is for
package.json): membrane^0.6.0and context-manager^0.16.0. With them, one membrane installs for both packages.addMessage, which a module's writes, the MCPL lanes' and the framework's own notices take,editMessagefor a write still held for a turn's end, a module's process response, and the API'smessage.send.addMessageandeditMessagerun context-manager'smalformedContentPlacesand throw itsMalformedContentError, naming each place, as the store's own write and edit do, whether the write would be stored at once or deferred. The writes that go to a store directly carry a model's own output, tool results serialized as text, and lane content the lanes have checked.[process-response-refused]line and aprocess:response-refusedtrace name the module and the message or edit, and the other modules' responses to the event still apply. Theprocess:completedtrace and the process log list only the responses that applied, so a view built on them, such as the bundled UI's event log, doesn't show a refused response's messages as the event's. A store error while a response applies, such as a shard the store refuses when a strategy's chunking makes one, propagates out of the dispatch as any store error does.message.sendrefuses content that isn't text with an error.MessageSendParamsdeclares it a string, andApiModulestores it as one text block, so other content was answeredpending. Under context-manager 0.14 it was then stored as given, and with these floors it would be refused at dispatch, where the client never hears.tool_usewhose input isn't an object, passes as before.handleMcplChannelIncoming,handleMcplPushEvent). Content from an MCPL server is converted at the boundary into blocks the store accepts, so this is the backstop for events that never crossed it, such as a module's, as the empty-content backstop beside it is.malformedContentPlacesbefore anything else. Content in which it names a place is refused there: no row, no wake, no conversation fork. A stderr line names the server, the channel where there is one, the message or event, and the places, and a trace,mcpl:malformed-content-refused, sits beside the empty-content backstop's.writeDrainedInOrder, which every flush of the queue writes through: at a tool boundary, a turn's start and end, a resume, a stop and the rest). Since writes throughaddMessageare refused before they're queued, this meets an entry a process before this release persisted, a pairpuppetToolCallqueues itself, or a write the strategy's chunking splits into a shard the store refuses, which the line names.MalformedContentErroris logged and dropped, not put back, and its agent's later queued writes go on. Put back, it would be refused at every flush and hold them for good. It stays in the un-acked set, so the next ack takes it out of the durable queue.tool_usedropped at the drain leaves atool_resultqueued behind it to be stored alone. The only queued pair ispuppetToolCall's, queued only when its turn's token is no longer its own once the pair is built. The drain's comment and that queuing site both say so.MaintenanceWithdrawnErrorextendsCompressionWithdrawn(Throw context-manager'sCompressionWithdrawnfor maintenance withdrawals (CM #196's fix needs the host half) #549). Its constructor sets the brand context-manager reads a withdrawal by, and context-manager's doc names a subclass as the way in. So agent-framework doesn't spell the brand itself, and two installed copies of context-manager still agree. Every rung of the ladder and the merge path rethrow it unchanged, recording no outcome, attempt, streak or quarantine event. The opt-in compression log marks the call's receipts withdrawn. Its name and the two places that catch it are unchanged. Setting the brand alone, withSymbol.for, would work without the import. With the floor at 0.16.0, the import costs nothing.CompressionWithdrawnfor maintenance withdrawals (CM #196's fix needs the host half) #549).MaintenanceWithdrawnError's docstring now says what 0.16.0 does with a withdrawal, where it described 0.14's ladder.refuseHeldProviderModel's docstring andreleaseProviderWait's comment no longer name a context-manager change no release carries: 0.16.0's compression doesn't read the refusal'sretryAfterMs, and has noreleaseCompressionPause.engines.node, CI's matrix, the publish workflow's build-and-test job, and both workflows' packed-install check): as membrane and context-manager have them.registerbelow Node 22.15 and throughregisterHooksfrom there.npm test's--test-timeoutbounds a whole file under Node 22.12, as it did under 20, and each test under 24. SoCONTRIBUTING.md,ci.yml's comments and the timeout test's header name 22.12 where they named 20, and the README says 22.12.shedMayHelp's doc and two test headers called membrane 0.5.87 or context-manager 0.14.1 this package's floor; they now say those versions are below it, and the error-type list names membrane 0.6.0's types.held-writes.tscopies context-manager's privatematchesFilter, which it named at 0.14.1; it names 0.16.0 too, whose rule is the same, andtest/module-held-writes.test.tsstill compares the copy with the store's own query.writeDrainedInOrder's comment on a sharded body. Under context-manager 0.14, a sharded write that threw partway left the shards it had written. From 0.15 it removes them, and they stay only when that removal fails as well. Boot's scan for landed writes counts any landed shard, which also covers a body a 0.14 write left.CompressionWithdrawnfor maintenance withdrawals (CM #196's fix needs the host half) #549.What callers notice
engine-strictset.Membraneraises its own@animalabs/membranerange to^0.6.0. context-manager 0.16.0 counts a compression attempt's prompt through the host's instance, and an older membrane's Gemini counts, and below 0.5.81 its OpenAI counts, include the cached input, which 0.16.0 then counts twice.hysteresisCertificate: falserestores the full solver.pingkeepalives after its first event no longer ends at membrane's idle deadline, since membrane 0.6.0 counts them as activity.MalformedContentErrorat its call, whether or not a turn is running. A process response with such content is refused whole, with a line and aprocess:response-refusedtrace naming the module. Under context-manager 0.14 such content was stored.message.sendcontent that isn't text gets an error, where it gotpendingand the message was stored as given.nullarguments by an OpenAI-family model receive{}, where they receivednull. membrane keeps the arguments' text on thetool_useblock asunparseableInput(membrane 0.6.0).tool_usestored withinput: nullreads as{}. A request compiled from such a message can change there, so a provider's prompt cache misses from that point, once (context-manager 0.16.0).The breaking fragment gives these with what each release's notes say.
Tests and what each protects
test/malformed-content-refusal.test.ts(new): the lanes' backstop.textisn't a string, and an image whosesourceis a string. Each is refused. The tests check its placement, its trace with the event's id, its line, that no wake runs, and that the agent's store holds nothing new.tool_usewhose input isn't an object is mended by the store, so it's stored, not refused.test/malformed-content-writes.test.ts(new): the refusal where content is written.MalformedContentErrorwhile a turn runs and is never queued, and the same write while the agent is idle throws the same refusal.[null], throws and leaves the write as it was, which lands, and the turn goes on past its tool boundary.process:response-refusedtrace names each refused response.message.send: content that isn't text, a number or an array of blocks, is refused at the call and queues no event, and text is accepted as before.test/host-quiesce.test.ts.addMessagenow queues well-formed content for the same purpose.test/deferred-window-only.test.ts.main's whole-body test pins it for a body stored whole.test/maintenance-withdrawal-ladder.test.ts(new), Throw context-manager'sCompressionWithdrawnfor maintenance withdrawals (CM #196's fix needs the host half) #549's suggested test. A compression runs through the framework's own maintenance membrane. Its canonical call is refused, and a surgery takes the store during that call, so the source-only rung's call is withdrawn at the gate. The tick rethrows the withdrawal unchanged, nothing is sent after it, and nothing is quarantined.test/tool-input-null-arguments.test.ts(new). A tool call whose arguments are the textnull, through membrane's OpenAI-compatible adapter, arrives as{}, with the text kept inunparseableInput.skip_replythen runs as a call with no arguments. The input assertions fail on membrane 0.5.87, where the input isnull. Since fix(tools): skip_reply reads wake_in_seconds one way, from 1 to 3600 seconds, and refuses anything else by name #501, the framework half passes with either input.Other open pull requests
progressTimeoutMs, which needs this membrane floor. membrane 0.6.0's Anthropic idle watchdog countspingkeepalives as activity after a stream's first event, so with this change alone, nothing in agent-framework or membrane ends a stream that sends only pings. The progress deadline, which pings don't renew, does. So releasingmainwith this change and without fix(inference): a progress deadline on resident inference, retried when it expires #378 would leave such a stream open.Tests
main815bfe35), from a clean detached worktree:npm testpasses 5262 of 5289 tests on Node 24.22.0, and the same tests on Node 22.12.0, with 27 skipped and none failing. The two runs took 301 s and 354 s, 405 and 444 CPU-seconds, on a shared host with 12 CPUs whose load average was 43 and 38 as each began.main815bfe35, CI's Ubuntu legs, Node 20 and 24, each ran 5270 tests (macOS ran 5272). This change adds nineteen tests: ten inmalformed-content-refusal, five inmalformed-content-writes, one each inmaintenance-withdrawal-ladder,tool-input-null-argumentsandhost-quiesce, and one more indeferred-window-only, where two others are renamed. Compared by name with that run, every other test is the same, on both Nodes here. CI skips 28 on Node 24 and 29 on Node 20: the 27 skipped here, a bun test (CI's build-and-test runner has no bun) and, on Node 20, aJSON.rawJSONtest.maina0b06cf4. The Build & Test legs on Ubuntu, with Node 22.12.0, 24 and latest (26.12.0), each ran 5289 tests, 5261 passing and 28 skipped; the macOS legs, with Node 22.12.0 and latest, each ran 5291, 5263 passing and 28 skipped. None failed. The packed release installed on its own on Node 22.12.0 and 24, and the changelog check passed (run 39290809596).main815bfe35'ssrc/framework.ts, with this change's dependencies and its other files:malformed-content-refusal, withmain's: the six refusal tests and the routing test fail, and the two absent-content tests and the mendedtool_usepass. On the lanes, a text block whosetextis a number and an image whosesourceis a string throw context-manager's refusal out of the handler ("Message refused: content[1].text is a number, not a string.").malformed-content-writes, withmain's: the four framework tests fail, and the API test, whose check is insrc/api/server.ts, passes. A malformed write during a turn isn't refused. The[null]edit makes the tool boundary throw "Cannot read properties of null (reading 'type')". In the process response test and the console input test, the malformed message meets the store's own refusal, "Message refused: content[0].text is a number, not a string.", in place of the whole response's.deferred-window-only, withmain's: the drop test fails, as what was queued behind the malformed entry isn't carried.maintenance-withdrawal-ladder, withmain's: fails, as the tick ends normally where it should end with the withdrawal.tool-input-null-argumentspasses there, since it pins membrane's behaviour, not the framework's. With membrane 0.5.87 put back under the same build, it fails at the input, which isnull.MaintenanceWithdrawnErrorextendingErroragain;addMessage's check andeditMessage's;message.send's content.Not verified
resume()runs after boot restored a queue. The turn start's and the turn end's flushes write through the samewriteDrainedInOrder, by reading.nullarguments, which a test here runs, and npm's engine warning on its documentation. The rest didn't run here.changelog.d/floors-membrane-060-cm-016.breaking.md,changelog.d/node-22-12.breaking.md,changelog.d/malformed-content-refused.fixed.md,changelog.d/549-maintenance-withdrawn.fixed.md.Written by Nigel-2405 (Claude Opus 5.5), on the design Ada-1017 (Claude Opus 5.5) approved. Esther-2329 (Claude Opus 5.5) wrote the null-arguments test. Byron-2452 (Claude Opus 5.5) and Cordelia-1930 (Claude Opus 5.5) read it.
Description checked and posted by Lottie-2661 (Claude Opus 5.5).
🤖 Written using Anthropic's Claude Opus 5.5 through the Rookery's tooling, and posted from the shared
nissa-seruaccount.