Skip to content

build(deps)!: membrane ^0.6.0, context-manager ^0.16.0 and Node 22.12 or later - #582

Merged
Anarchid merged 20 commits into
anima-research:mainfrom
nissa-seru:nigel/floors-membrane-060
Oct 22, 2026
Merged

Anarchid merged 20 commits into
anima-research:mainfrom
nissa-seru:nigel/floors-membrane-060

Conversation

@nissa-seru

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

Copy link
Copy Markdown
Contributor

agent-framework now depends on membrane ^0.6.0 and context-manager ^0.16.0, and needs Node 22.12 or later, as both of them do. Three changes come with them:

  • content context-manager now refuses to store is refused where agent-framework writes it, whether the write is stored at once or deferred while a turn runs, and a queued write the store refuses is dropped at the drain rather than queued again for good;
  • a maintenance call the framework withdraws is context-manager's CompressionWithdrawn, so it no longer counts against a chunk's compression;
  • the deferred-write tests pin context-manager 0.15's sharded write, where they pinned 0.14's.

Fixes #549.

Problem

  • The ranges. agent-framework depends on membrane ^0.5.87 and 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.
  • Content context-manager now refuses. From 0.16.0, context-manager refuses a write whose content doesn't match ContentBlock, with a MalformedContentError, 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.
  • Throw context-manager's CompressionWithdrawn for maintenance withdrawals (CM #196's fix needs the host half) #549. The framework throws MaintenanceWithdrawnError for a maintenance call it withdraws, under a surgery's hold or once stop() has begun. It was a plain Error, 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 exports CompressionWithdrawn for this.
  • Node. membrane 0.6.0 and context-manager 0.16.0 each declare engines.node >=22.12.0, and test at Node 22.12.0, 24 and the newest. agent-framework declared >=20.0.0 and tested at 20 and 24.

Changes

What each part is for

  • The floors (package.json): membrane ^0.6.0 and context-manager ^0.16.0. With them, one membrane installs for both packages.
  • A refusal where content is written: addMessage, which a module's writes, the MCPL lanes' and the framework's own notices take, editMessage for a write still held for a turn's end, a module's process response, and the API's message.send. addMessage and editMessage run context-manager's malformedContentPlaces and throw its MalformedContentError, 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.
    • A module's write fails at its call either way, where a deferred one used to be refused at a flush the module never saw.
    • An edit of a held write to refused content leaves the write as it was.
    • A process response with such content, in a message it adds or an edit it makes, is refused whole before any response to the event is applied. None of it is applied, its module state update and inference request included, so no turn runs for content that was never stored. It doesn't count as accepting console or API input. A [process-response-refused] line and a process:response-refused trace name the module and the message or edit, and the other modules' responses to the event still apply. The process:completed trace 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.
    • The API's message.send refuses content that isn't text with an error. MessageSendParams declares it a string, and ApiModule stores it as one text block, so other content was answered pending. 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.
    • Content the store mends, a tool_use whose input isn't an object, passes as before.
  • The MCPL lanes' check (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.
    • What it does: each lane asks malformedContentPlaces before 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.
    • Why at the lane: the refusal can name where the content came from, and the lane's other effects don't run.
    • What it leaves: absent content is the empty-content check's, as before. A refused event still reaches the modules, as an empty one does.
  • A drop at the drain (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 through addMessage are refused before they're queued, this meets an entry a process before this release persisted, a pair puppetToolCall queues itself, or a write the strategy's chunking splits into a shard the store refuses, which the line names.
    • The drop: a queued write refused with MalformedContentError is 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.
    • A release: a fork's or an ephemeral run's release writes what's still queued for it into its own window through the store, and logs and drops a write that fails there, a refused one included, as before.
    • A tool pair: a tool_use dropped at the drain leaves a tool_result queued behind it to be stored alone. The only queued pair is puppetToolCall'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.
  • MaintenanceWithdrawnError extends CompressionWithdrawn (Throw context-manager's CompressionWithdrawn for 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, with Symbol.for, would work without the import. With the floor at 0.16.0, the import costs nothing.
  • Two docstrings and a comment (Throw context-manager's CompressionWithdrawn for 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 and releaseProviderWait's comment no longer name a context-manager change no release carries: 0.16.0's compression doesn't read the refusal's retryAfterMs, and has no releaseCompressionPause.
  • Node 22.12 (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.
    • Build & Test runs Node 22.12.0, 24 and the newest on Ubuntu, and the two ends on macOS.
    • The packed-install check runs on 22.12.0 and 24, in CI and before a release. Its load hook goes in through register below Node 22.15 and through registerHooks from there.
    • The text: npm test's --test-timeout bounds a whole file under Node 22.12, as it did under 20, and each test under 24. So CONTRIBUTING.md, ci.yml's comments and the timeout test's header name 22.12 where they named 20, and the README says 22.12.
    • The check names change with the matrices. A branch rule that requires a check by name would need the new names.
  • Comments on what the store keeps: the boot recovery's comment no longer says the store keeps a string or number block, which 0.16.0 refuses, and says a recovered write the store refuses is dropped at its flush.
  • Comments that named older floors: 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.ts copies context-manager's private matchesFilter, which it named at 0.14.1; it names 0.16.0 too, whose rule is the same, and test/module-held-writes.test.ts still 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.
  • Changelog fragments: two breaking, the floors and Node, and two fixed, the refusal and Throw context-manager's CompressionWithdrawn for maintenance withdrawals (CM #196's fix needs the host half) #549.

What callers notice

  • A host on a Node before 22.12 gets npm's engine warning when it installs agent-framework, and an install fails only with engine-strict set.
  • A host that builds its own Membrane raises its own @animalabs/membrane range 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.
  • A recipe-built kv-unified strategy takes the hysteresis certificate by default since context-manager 0.15.0, and hysteresisCertificate: false restores the full solver.
  • An Anthropic stream that sends only ping keepalives after its first event no longer ends at membrane's idle deadline, since membrane 0.6.0 counts them as activity.
  • Two notes membrane writes for media it doesn't send now say the model isn't seeing it, so a provider's prompt cache misses once, from the first such note.
  • A module that writes content context-manager won't store gets MalformedContentError at its call, whether or not a turn is running. A process response with such content is refused whole, with a line and a process:response-refused trace naming the module. Under context-manager 0.14 such content was stored.
  • An API client that sends message.send content that isn't text gets an error, where it got pending and the message was stored as given.
  • Tools given null arguments by an OpenAI-family model receive {}, where they received null. membrane keeps the arguments' text on the tool_use block as unparseableInput (membrane 0.6.0).
  • Stored history: malformed content an older context-manager stored is read mended, or left out, with a line naming each place. A tool_use stored with input: null reads 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.
    • The refusals: both lanes, each with three malformed shapes, handed the lanes directly as a module's event would be: a string block, a text block whose text isn't a string, and an image whose source is 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.
    • With conversation routing on, a message with visible text beside a malformed block, which would bind a fork, spawns none, where the same message well-formed spawns one.
    • Absent content, undefined or null, is the empty-content check's on both lanes.
    • A control: a tool_use whose 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.
    • A module's write: a malformed one throws MalformedContentError while a turn runs and is never queued, and the same write while the agent is idle throws the same refusal.
    • A held write's edit: an edit to malformed content, or to [null], throws and leaves the write as it was, which lands, and the turn goes on past its tool boundary.
    • A process response: one with a malformed message, and one with a malformed edit, are each refused whole, with no inference run, while another module's response to the same event applies. The trace and the stored process log list only that one, and a process:response-refused trace names each refused response.
    • The API's 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.
    • Console input whose only message is refused isn't counted as accepted, where the same input with a well-formed message is.
  • test/host-quiesce.test.ts.
    • A persisted malformed write: one queued between two writes in a queue an earlier process persisted is restored at boot, dropped at the first flush with its line, and the ack takes it out of the recovery file, while the writes before and behind it land in order.
    • An existing test that queued a bare-string block through addMessage now queues well-formed content for the same purpose.
  • test/deferred-window-only.test.ts.
    • The drop at a tool boundary: a malformed entry placed in the queue, as boot's restore would place it, ahead of an interjection. The interjection is carried and stored, the malformed entry is nowhere and not queued again, and the line says it was dropped.
    • The two landed-shard tests pinned 0.14's partial write and were skipped from 0.15. They now pin 0.15's. A body whose write throws on its first shard lands nothing: the drain cuts the agent at it, and at the turn's end it's stored whole, followed by what was queued behind it. When the strategy throws on a later shard and removing the shards throws too, the shards that stayed are carried as the next compile renders them. That pins the join for a partial body; main's whole-body test pins it for a body stored whole.
  • test/maintenance-withdrawal-ladder.test.ts (new), Throw context-manager's CompressionWithdrawn for 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 text null, through membrane's OpenAI-compatible adapter, arrives as {}, with the text kept in unparseableInput. skip_reply then runs as a call with no arguments. The input assertions fail on membrane 0.5.87, where the input is null. 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

Tests

  • The suite, on Linux, at this head (merged with main 815bfe35), from a clean detached worktree:
    • The install puts one membrane, 0.6.0, under both packages, and typecheck and build are clean.
    • npm test passes 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.
    • At main 815bfe35, CI's Ubuntu legs, Node 20 and 24, each ran 5270 tests (macOS ran 5272). This change adds nineteen tests: ten in malformed-content-refusal, five in malformed-content-writes, one each in maintenance-withdrawal-ladder, tool-input-null-arguments and host-quiesce, and one more in deferred-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, a JSON.rawJSON test.
  • CI (run 39290809330) passed on this head merged onto main a0b06cf4. 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).
  • Red first, on Node 24.22.0: the new tests with main 815bfe35's src/framework.ts, with this change's dependencies and its other files:
    • malformed-content-refusal, with main's: the six refusal tests and the routing test fail, and the two absent-content tests and the mended tool_use pass. On the lanes, a text block whose text is a number and an image whose source is 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, with main's: the four framework tests fail, and the API test, whose check is in src/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, with main's: the drop test fails, as what was queued behind the malformed entry isn't carried.
    • maintenance-withdrawal-ladder, with main's: fails, as the tick ends normally where it should end with the withdrawal.
    • tool-input-null-arguments passes 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 is null.
  • Removals, on Node 24.22.0, at this head: nineteen pieces, each removed or changed on its own, each failing at least one test that passes with nothing removed:
    • each lane's check;
    • the channel lane's trace;
    • the channel in the push lane's line;
    • absent content left to the empty-content check;
    • the drain's drop, and the drop's own wording in its line;
    • MaintenanceWithdrawnError extending Error again;
    • addMessage's check and editMessage's;
    • a process response's check, and its check of edits;
    • counting a refused response as accepting a surface's input;
    • listing a refused response in the trace, and in the process log;
    • the refusal's own trace, and that trace carrying other places;
    • the refusal's line logged twice;
    • the API's check of message.send's content.

Not verified

  • No live provider, MCPL server or Discord was used. The lanes' tests drive the framework with its own MCPL event objects, and the withdrawal test's provider is a stub whose refusal has the shape context-manager's own tests use.
  • The drop at the drain is tested at a tool boundary, and at the flush resume() runs after boot restored a queue. The turn start's and the turn end's flushes write through the same writeDrainedInOrder, by reading.
  • The withdrawal is tested at the source-only rung after a refused canonical call. The other rungs and the merge path rethrow it by context-manager's code and its own tests, not this change's.
  • macOS and the newest Node weren't run here. CI runs both.
  • connectome-host wasn't run with these floors.
  • What callers notice of membrane 0.6.0's and context-manager 0.15.0's and 0.16.0's own changes rests on their release notes, apart from the null arguments, which a test here runs, and npm's engine warning on its documentation. The rest didn't run here.

  • Changelog fragments added: 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-seru account.

Nigel-2405 and others added 20 commits October 18, 2026 05:20
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>
@greptile-apps

greptile-apps Bot commented Oct 19, 2026

Copy link
Copy Markdown

Required label not found on this PR.

@k-res-dev

Copy link
Copy Markdown

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>
@Anarchid
Anarchid merged commit e096285 into anima-research:main Oct 22, 2026
8 checks passed
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.

Throw context-manager's CompressionWithdrawn for maintenance withdrawals (CM #196's fix needs the host half)

3 participants