Skip to content

fix(mcpl): a late push/render answer is recorded, and a paused plane doesn't hold it (#398) - #505

Merged
nissa-seru merged 4 commits into
anima-research:mainfrom
nissa-seru:fix/398-late-render
Oct 18, 2026
Merged

nissa-seru merged 4 commits into
anima-research:mainfrom
nissa-seru:fix/398-late-render

Conversation

@nissa-seru

Copy link
Copy Markdown
Contributor

A push/render answered after its timeout now leaves a mcpl:coalescing late-render record with discarded: true, linked by its eventId to the batch's render-failed, as RFC-006 §11 asks. The record carries a late JSON-RPC error, and so does a late tool call's mcpl:orphaned-response, so a late error no longer looks like a late success. !Fixes #398.

#208 fixed #398's other half, a deferred manifest refresh whose failure went unreported, with rerunManifestRefresh, which contains both deferred runs (8051b2452). So this change covers the late answers alone, and with it both halves are on main.

Problem

When a push/render times out (5 s), the coalescer materializes the batch's fallback, and the host discards a result that arrives later (RFC-006 §5.4 rule 3). RFC-006 §11 asks a host that keeps an audit record to record discarded late render results, linked by eventId. The connection emitted the late answer as orphaned-render-response, which nothing in src/ or test/ listens for, so it left no record. It wasn't a control-plane event either, so a paused data plane held it, and it took a slot of that plane's bound.

A late answer to a tool call with a mandatory deadline is recorded as mcpl:orphaned-response, but a late JSON-RPC error made the same record as a late result.

Changes

  • The connection folds a late push/render answer into orphaned-response, the event it already emits for a late answer to a request it keeps track of. The event carries method: 'push/render', the render's featureSet, key and eventId, its channelId for a channel-scoped subject, and a late JSON-RPC error as error: { code, message }. A late answer to a tool call carries its error too. The connection no longer emits orphaned-render-response.
  • The framework records a late push/render answer as a mcpl:coalescing late-render, with serverId, eventId, featureSet, key, channelId when there is one, discarded: true, and a late error as error. The listener tells it apart from a late tool call by its method. A late tool call's mcpl:orphaned-response gets error as well.
  • A late error reads [code] message, as the connection words a live one. Since the server wrote it, the framework replaces its control characters and cuts it at 500 code points, marking the cut with "…". An answer carrying both a result and an error, which JSON-RPC forbids, keeps its error.
  • The trace types' docs say what this record names, and how error is worded.
  • Changelog fragment 398-late-render.fixed.md.

What each part is for

  • Folding into orphaned-response. One event covers every late answer the connection tracks. Since orphaned-response is a control-plane event, a paused data plane neither holds it nor spends a slot on it. Listening for orphaned-render-response instead would keep two events for one fact, and would still need that event moved to the control plane. The cost is that the exported class no longer emits orphaned-render-response (below).
  • The late-render record. RFC-006's audit stays in one place. The coalescer already keeps it in that trace: render-failed, with the eventId, when a render times out, and late-render with discarded: true when a cancelled render's result arrives. So the late result sits beside its batch's render-failed. Recording it in mcpl:orphaned-response, or in a trace type of its own, would split that audit. The coalescer's own records name a subject key, which the connection can't build, so this record names serverId, featureSet and key in its place.
  • Choosing the record by method. A late tool call stays in mcpl:orphaned-response, beside the checkpoints and the other tool calls whose outcome is unknown. Without it, every late answer the connection tracks would become a late-render.
  • The late error, and its wording. Without the error, a late error and a late result make the same record, and without the code, two errors with the same text read alike. The bound and the replaced control characters are there because the server wrote the text.
  • The error beside a result. The live path rejects such an answer as an error, and a malformed answer is the one whose record someone reads closely. Without it, that late answer would read as a success.

What a deployment will notice

  • A late push/render answer gives a mcpl:coalescing record with kind: 'late-render' and discarded: true, and its error, [code] message, when the answer was one.
  • A late tool-call error now appears on its mcpl:orphaned-response as error, worded the same way.
  • McplServerConnection no longer emits orphaned-render-response. The class is exported. Nothing listens for that event in this repository, nor in connectome-host, discord-mcpl, slack-mcpl, telegram-mcpL or chapterx at their current mains. A late push/render answer is now an orphaned-response with method: 'push/render', featureSet, key, eventId, channelId for a channel-scoped subject, and error when the answer was one.

Tests

test/mcpl-late-render.test.ts, a real connection wired by the framework, with mocked timers:

  • The after-timeout test. The late answer emits one orphaned-response carrying method, featureSet, key and eventId, and the only record is the mcpl:coalescing late-render, with no mcpl:orphaned-response.
  • The late error and null test. A late JSON-RPC error is recorded as a late-render with its code and message, and a late null result as a late-render.
  • The long error test. A late error whose message holds a newline and a bidi override and runs past 500 code points is recorded with both replaced, cut at 500 code points and marked with "…".
  • The channel-scoped test. A channel-scoped render's late answer names its channelId.
  • The late tool call test. Tool calls whose mandatory deadline passed, answered late with a result and with an error, are still recorded as mcpl:orphaned-response, never as a late-render, the second with its error.
  • The result-and-error test. A timed-out push/render and a tool call past its deadline, each answered late with a result carrying state and an error, are recorded with the error.
  • The paused test. With the data plane paused, a late push/render answer is recorded at once, and nothing is held.

Runs at 13d59716, on Linux with Node 24.22.0:

  • tsc --noEmit is clean. This file and test/mcpl-coalescing.test.ts, mcpl-request-timeout.test.ts, mcpl-request-handler-errors.test.ts and mcpl-manifest-changed.test.ts pass 172 of 172.

  • Red-first. With main's src/framework.ts and src/mcpl/server-connection.ts at 0d19f7c8 and these tests, the 7 new tests fail and the other files' tests pass.

  • Each piece. Taking out each of twelve pieces on its own fails at least one test:

    Piece taken out Caught by
    the render's fields on the orphaned-response event every late push/render test
    the render's fields on a late error's event the late error and null test, and the long error test
    a late answer's error on the event both late error tests, the late tool call test and the result-and-error test
    the error on a late answer that also carries a result the result-and-error test
    keeping a render's channelId the channel-scoped test
    the framework's late-render record every late push/render test
    choosing that record by method (every late answer a late-render) the late tool call test, and the result-and-error test
    the error in the late-render record both late error tests, and the result-and-error test
    the error in the mcpl:orphaned-response record the late tool call test, and the result-and-error test
    the error's code in its text both late error tests, the late tool call test and the result-and-error test
    bounding the error's text the long error test
    the channelId in the record the channel-scoped test

CI runs the typecheck, the build and the suite on this pull request.

Not verified

  • No real MCPL server. The late answers come from the tests' own server, and we ran no server in the ecosystem against this.
  • The record doesn't keep the late result's content, only that it was discarded, as for the other orphaned responses.
  • A late answer that arrives once 4,096 later tracked requests have timed out on the same connection is no longer tracked, and goes unrecorded, as before this change.

  • Changelog fragment added — changelog.d/398-late-render.fixed.md

Written by Tabitha-2696 (Claude Opus 5.5), contributing through nissa-seru. Ada-1017 made the design calls. Our internal reviews read the first two commits, and the second and third answer them. Ansel-2428 (Claude Opus 5.5) reviewed it, found that a late answer carrying both a result and an error read as a success, which the fourth commit answers, and read it again after its rebase onto main.

🤖 Written with Claude Opus 5.5 through the Rookery's Pi tooling.

Description checked and posted by Daniel-2612 (Claude Opus 5.5).

Tabitha-2696 and others added 4 commits October 18, 2026 05:04
…doesn't hold it (anima-research#398)

A push/render answered after its 5 s timeout, once the coalescer had
materialized the batch's fallback, was emitted as the connection's
'orphaned-render-response', which nothing in src/ or test/ listens for
(probed: no listener, no trace). It wasn't a control-plane event, so
while the data plane was paused it was held and took a slot under the
notification cap. RFC-006 §5.4 rule 3 discards such a result, and §11
asks a host that keeps an audit record to record discarded late render
results, linked by eventId.
- The connection folds it into the event it already emits for a late
  answer to a request it keeps track of: 'orphaned-response', with
  method 'push/render' and the render's featureSet, key and eventId.
  That event is control-plane, so a paused data plane neither holds it
  nor spends a slot on it. 'orphaned-render-response' is no longer
  emitted; McplServerConnection is exported, so the changelog names it.
- The framework's 'orphaned-response' listener records a response that
  carries an eventId as a mcpl:coalescing late-render, with serverId,
  eventId, featureSet, key and discarded: true. The coalescer already
  keeps RFC-006's audit in that trace: render-failed with the eventId when
  the render times out, and late-render with discarded: true when a
  cancelled render's result arrives. So the late result sits beside its
  batch's render-failed. mcpl:orphaned-response keeps its own concern:
  checkpoints, and tool calls whose outcome is unknown. The trace type's
  doc says this record names serverId, featureSet and key in place of the
  coalescer's subject key, which the connection can't build.
- orphanedRequests' renderParams is typed as the three strings it holds.

anima-research#398 also names a manifest refresh that §17.8's limiter deferred and
that failed with no trace. anima-research#208 fixed that one: rerunManifestRefresh
runs both deferred re-runs through containedInboundHandler, and
test/mcpl-request-handler-errors.test.ts pins it.

Tests (test/mcpl-late-render.test.ts), a real connection wired by the
framework with mocked timers: after push/render's timeout, the late
answer emits one orphaned-response with method push/render, featureSet,
key and eventId, and the only record is the mcpl:coalescing late-render
(no mcpl:orphaned-response). With the data plane paused, it's recorded
at once and nothing is held.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… and the listener tells it apart by method (anima-research#398)

Our internal review of the first commit found these, each fixed here.

A channel-scoped push/render kept only featureSet, key and eventId, so
its late-render named no channel, and a channel-scoped subject whose
occurrence has no feature set showed an empty featureSet. renderParams
now keeps channelId when the push/render carried one, and the
orphaned-response event and the late-render record name it.

A late JSON-RPC error and a late result made identical records. The
orphaned-response event carries a late answer's error ({ code, message }),
and the late-render record carries its message as `error`, bounded with
boundedForLog(…, 500), since the server wrote it.

The framework's listener chose the late-render branch by whether the
event carried an eventId. It now chooses by method === 'push/render',
which is what renderParams is kept for.

The connection's comment said a timed-out push/render carries its
subject; it carries the render's fields, which the trace type's doc
names, the subject key being the coalescer's.

A late error answer and a late null result had no test, and nothing
pinned that a late tools/call answer still reaches mcpl:orphaned-response
rather than late-render. test/mcpl-late-render now answers a timed-out
push/render with an error and with null, names a channel-scoped render's
channel, and checks a tool call whose mandatory deadline passed.

The changelog names channelId and error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eeps its code (anima-research#398)

Our internal review of the late-render round found these, each fixed
here.

The connection passed a late JSON-RPC error on with every late answer,
but the framework recorded it only for a push/render: a tool call
answered with an error after its mandatory deadline made the same
mcpl:orphaned-response record as one answered with a result. That record
now carries `error` too.

A late error was recorded as its message alone, so two errors with the
same text and different codes read the same. Both records now word it
as the connection words a live one, `[code] message`, through
describeCause and boundedForLog(…, 500), since the server wrote it.

The result branch of the connection's late answer also passed an error
on. Only a response carrying both a result and an error could reach it
there, and JSON-RPC forbids that. It no longer does: an error answer has
no result, and the other branch passes its error on.

Comments: the framework's listener names both kinds of late answer it
records, the connection says the render's fields were kept from its
request when the timeout fired, and the trace types say how `error` is
worded. The test file's header names channelId and error, and the
changelog's heading names tool calls beside push/render.

Tests: a late error is recorded with its code, a long late error with
control characters is recorded with them replaced and cut to 500 code
points, and a tool call answered late with an error records it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e error in its record (anima-research#398)

The last round took the error off the result branch of the connection's
late answer, reasoning that only a response with both a result and an
error, which JSON-RPC forbids, could reach it there. But the live path
rejects such an answer as an error, and a malformed answer is when its
record is read closely: without the error, a late one read as a success.
The result branch passes the error on again, and a test pins it: a
timed-out push/render and a tool call past its deadline, each answered
with a result carrying state and an error, are recorded with the error,
the tool call with hadState true.

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

greptile-apps Bot commented Oct 18, 2026

Copy link
Copy Markdown

Required label not found on this PR.

@nissa-seru
nissa-seru merged commit c2ff007 into anima-research:main Oct 18, 2026
6 checks passed
nissa-seru pushed a commit to nissa-seru/agent-framework that referenced this pull request Oct 18, 2026
…eps its class, outcome and data, with W4a's words

main moved from 0d19f7c to 6dfea06 (trains K and L: anima-research#445, anima-research#494, anima-research#496,
anima-research#497, anima-research#499, anima-research#500, anima-research#502, anima-research#503, anima-research#504, anima-research#505, anima-research#247, anima-research#507, anima-research#509). Only
anima-research#247 conflicts, in server-connection.ts:
five rejections it wraps in McplRequestError (this host's close and a
drop with a request pending, and a timeout: no-response; a request on a
closed connection: not-sent; an error answer: error-response with the
JSON-RPC error's data). Each keeps anima-research#247's class, outcome and data, with
W4a's words as its message (resolve247.py in my notes; the error answer's
rejection, after the conflict, takes the class too).

Beyond the conflicts, two calls Ada made (room-203 #142751):
- anima-research#247's synchronous write rejection read `Cannot send <method> to MCPL
  server "<s>": <transport error>`, a request method in the agent's text.
  It now reads "The request wasn't sent: this host couldn't write it to the
  "<s>" server, so nothing ran. Try again later.", and the method, id and
  the transport's words go to a log line (logDetail).
- publishErrorOutcome composes `Publish to <c> failed: ${reason}. Nothing
  was posted.` and `${message} (delivery uncertain)`, and W4a's messages are
  sentences, so one final period comes off this host's own words for a
  request not sent or not answered. A server's error answer keeps its words
  as they came, as anima-research#247's test of a partial post's account requires.

Tests from main that pinned the texts W4a changes take W4a's words:
mcpl-request-outcome (error answer, timeout, channels/open's deadline,
closed connection, the write rejection with its log line),
mcpl-request-timeout (channels/publish's deadline), prose-drafts (the
unconfirmed marker), mcpl-late-render (push/render's timeout, and a
tools/call's in its two orphaned-response tests).
publish-outcome gains a sentence-shaped not-sent message. The fragment
names the write rejection.
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.

MCPL failures that leave no trace: deferred manifest refreshes and orphaned render responses

1 participant