Repository navigation
fix(tools): skip_reply reads wake_in_seconds one way, from 1 to 3600 seconds, and refuses anything else by name - #501
Merged
nissa-seru merged 2 commits intoOct 18, 2026
Conversation
…seconds, and refuses anything else by name
skip_reply's schema offered wake_in_seconds as a number of seconds,
"clamped to 1-3600". On main the framework armed the gate's timer only for
a value that Number() made finite and above 0, and the channel registry
worded its confirmation from its own copy of that test. So, with an
EventGate:
- 0, negatives and Infinity armed nothing, and the turn ended as an
ordinary skip;
- "soon", "10 minutes", "" and {} armed nothing and ended the turn, and the
model never heard that its wake was dropped;
- true and [5] armed wakes of 1 s and 5 s, because Number() coerces them.
Without one, anima-research#198 refused a positive number, true and [5], while 0,
negatives, "Infinity" and values Number() can't read ended the turn as an
ordinary skip.
One reader now decides what a call's wake_in_seconds asks for, and every
site calls it (src/mcpl/skip-reply-wake.ts):
- absent or null: no wake, an ordinary skip;
- a number of at least 1, or a string that Number() reads as one: a wake in
whole seconds, at most 3600, so 90.7 waits 90 s and anything over 3600,
Infinity included, waits 3600 s;
- anything else: not a wake request. That includes a number below 1 (0, a
negative, 0.5), a blank string, a boolean, an array and an object.
A number below 1 is refused rather than raised to 1. 0 is a value a model
may write when it fills an optional number with a default, and as a 1 s
wake it would wake the agent again a second after each turn, a full
inference each time, for as long as it kept skipping with 0. A refusal
costs one round and can't loop. So 0.5, which armed 1 s on main through
EventGate's own clamp, is refused too. The tool's description of the
parameter says "from 1 to 3600; a larger number waits 3600" in place of
"clamped to 1-3600".
The framework refuses a call whose wake the host can't set, before it arms
anything: with an EventGate, a value that isn't a wake request, by a tool
error that names the value and what the parameter takes; without one, any
value but absent or null, with anima-research#198's refusal, since the parameter isn't
offered there. A refused call arms nothing and doesn't end the turn, and
the turn's bookkeeping of a refused round reads the same predicate
(refusesSelfWake) as it did for anima-research#198's refusal. The framework arms the
reader's seconds, and the registry's confirmation names them. The
operator's [self-wake] line for a refusal prints the value as a field,
through logValue.
A skip_reply call whose input is null now ends the turn as an ordinary
skip, where the registry's handler threw reading wake_in_seconds.
Tests: skip-reply-wake.test.ts covers the reader and the refusal's text.
skip-reply-wake-capability.test.ts turns round anima-research#198's tests B and C, which
pinned main's ordinary skip without a gate for 0, "Infinity" and "soon",
and adds the gated refusals (each named, nothing armed, the turn going on),
the seconds armed, null and a null input as ordinary skips, another tool
carrying wake_in_seconds, and the registry's confirmation. The XML-mode
file adds a gated refusal of "soon".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Required label not found on this PR. |
One conflict, in src/mcpl/channel-registry.ts's imports: main (anima-research#247) adds InboundSource and McplRequestError where this branch adds readSkipReplyWake. All three stay. Git merged the rest on its own. main's new round bookkeeping (anima-research#247's silenceCauseOf) asks refusesSelfWake as before, so a skip_reply this branch refuses with an EventGate is left out of a round's silence as anima-research#198's no-gate refusal is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nissa-seru
pushed a commit
to nissa-seru/agent-framework
that referenced
this pull request
Oct 18, 2026
…ion beside W4a's error-result line and UNBOUND_REASON main moved from 6dfea06 to d1b9d35 (anima-research#510, anima-research#438, anima-research#511, anima-research#501, anima-research#513, anima-research#508). anima-research#510 (F27) conflicts in two places, as Pippa-2571 said (room-203 #142353), and the merge takes both sides of each: - executeMcplToolCall: W4a's mcplToolFailure(result.content, serverId, call.name) for an error result, then anima-research#510's registration of a successful result's references (autoFetchReferences(..., false)). - references.ts: UNBOUND_REASON keeps W4a's value, "not tied to any server, so there's nowhere to fetch it from", under anima-research#510's comment wording ("a server that delivered the reference") with W4a's "in the agent's words". No test from main pins the old unbound or expiry texts. - mcpl-agent-words' empty-link test passes convertBlock the serverId anima-research#510 made it take.
nissa-seru
pushed a commit
to nissa-seru/agent-framework
that referenced
this pull request
Oct 19, 2026
Brings in 22 pull requests merged into main since ce1002b, among them the skip_reply wake reader (anima-research#501), the interpreter's runtime directory (anima-research#193), registered agents' allowlist enforcement (anima-research#220) and the stop that reaches every cleanup (anima-research#517). Conflicts: - src/framework.ts: - The wake reader replaced requestedSelfWakeSeconds. This branch's changesRouting and ROUTING_TOOLS stay. - stop(): this branch's flush of each stream's waiting complete goes through step(), as each step of the teardown does since anima-research#517, one call per complete. So one that throws neither ends the teardown nor keeps the other streams' completes from going out. - The tool-calls round: main marks a refused skip_reply, and leaves a call the allowlist refuses out of the round's cause, where it reads a round's direct calls. Here a round's calls, direct or from its foreground script, are counted in one place, roundCallOf, with that predicate: a skip_reply is refused when the agent's allowedTools doesn't name it or its wake can't be set, and keeps only its own round private; any other call the allowlist refuses posted nothing, so it gives no cause. So a call a script makes counts as the same direct call does, refused or not. - src/code-execution/py-runner.ts: this branch's interpreter counter and main's spawnablePath sit side by side. The interpreter's number is still set where it's spawned. - test/skip-reply-wake-xml-mode.test.ts: both sides' helper changes, and the gated case passes { gate: true }. Texts: - skipReplyRefusal's doc names a script's calls too. - The allowlist fragment no longer leaves out a skip_reply a script makes. - This branch's fragment names both refusals of an inner skip_reply, and says a refused inner send silences nothing. Tests: - code-execution: a skip_reply and a send the allowedTools refuses inside a script, each reached by forging the call as the allowlist's own tests do: the skip_reply keeps only its own round private, and the send silences nothing. - stop-step-throws: a stream's waiting complete that throws. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 task done
Anarchid
pushed a commit
that referenced
this pull request
Oct 22, 2026
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>
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.
skip_replyreadswake_in_secondsone way: a number of seconds from 1 to 3600, where a larger number waits 3600. Anything else is refused by name, where before it was dropped. Onmain,0,-5and"Infinity"armed no wake and ended the turn, and so did"soon"or"10 minutes", without telling the model.trueand[5]armed wakes of 1 s and 5 s.Problem
skip_reply's schema offerswake_in_secondsas a number of seconds, "clamped to 1–3600" (src/mcpl/channel-registry.ts). Onmain, two places read it, each with its own copy of the same test:Number()makes finite and above 0 (requestedSelfWakeSeconds,src/framework.ts);handleToolSkipReply).A probe on
mainatd6593a3throughAgentFramework, with the realChannelRegistryfor the confirmation, gave this. The code it reads is the same at0d19f7c, where this branch began. With an EventGate:0,-5and"Infinity"arm nothing, and the turn ends as an ordinary skip."soon","10 minutes",""and{}arm nothing either. The turn ends with "Turn ended; nothing sent.", so the model is never told its wake was dropped.truearms a 1 s wake and[5]a 5 s wake, sinceNumber()coerces them.0.5arms a 1 s wake and5000a 3600 s one, throughEventGate.armSelfWake's own clamp.Without an EventGate, #198 refuses a positive number,
trueand[5].0, negatives,"Infinity", a blank string and valuesNumber()can't read end the turn as an ordinary skip.What changes
One reader,
readSkipReplyWakein the newsrc/mcpl/skip-reply-wake.ts, decides what a call'swake_in_secondsasks for:nullasks for no wake;Number()reads as one, asks for a wake in whole seconds, at most 3600. So 90.7 waits 90 s, and anything over 3600,Infinityincluded, waits 3600 s.0, a negative,0.5), a blank string (whichNumber()reads as 0), a boolean, an array and an object.The refusal comes from the same reading (
skipReplyWakeRefusal). With an EventGate, a value that isn't such a number is refused, by a tool error that names it:Without an EventGate, any value but absent or
nullgets fix(tools): skip_reply refuses wake_in_seconds without an EventGate instead of silently dropping it #198's existing refusal, since the host doesn't offer the parameter there.The framework refuses before it arms anything, and it arms the reader's seconds. A refused call arms nothing and doesn't end the turn, and only its own round's prose stays private, as with fix(tools): skip_reply refuses wake_in_seconds without an EventGate instead of silently dropping it #198's refusal: the turn's bookkeeping of a refused round asks the same predicate (
refusesSelfWake).The channel registry's confirmation names the seconds the framework arms. A call whose input is
nullnow ends the turn as an ordinary skip, where the handler threw readingwake_in_seconds.The tool's description of the parameter says "from 1 to 3600; a larger number waits 3600 (1 = go again almost immediately)", in place of "clamped to 1–3600".
The operator's
[self-wake]log line for a refusal prints the value as a field, throughlogValue.What a resident notices:
0, a negative or a value like"soon"comes back as an error they can correct in the same turn. Before, it ended the turn with no wake.0.5, is refused the same way. Onmainit woke them after a second.Infinitywaits an hour.0or"soon"is refused like any other value; onmainit ended the turn.Why each part is here
Number()test, in the framework and in the registry, which agreed only as long as both were kept the same. With one reading, a value can't arm a timer at one site and read as no wake at the other, and the confirmation names the seconds the timer runs.EventGate.armSelfWake, because a long wait can't loop."10 minutes"as 600 s was weighed and left out, because a parser for units would be a second grammar for a parameter whose schema says seconds.EventGate.armSelfWake's own clamp. The framework now calls it only with seconds in range, butEventGateis exported, and the clamp is that method's contract with any caller.AgentFramework.channels.handleChannelToolCall. Called directly withskip_reply, it confirms a wake that nothing armed, as onmain. The registry has never armed the timer, and nothing in this repository calls it that way.main. Where a tool call's input is normalized is a question for every tool, not for this one.Tests
test/skip-reply-wake.test.ts:null, and an input that isnullor not an object."30"and" 30 "are 30; 3601, 5000,Infinity,"Infinity"and"1e400"are 3600.-Infinity,"0","-5"and"0.999"."soon","10 minutes",""," ","NaN",NaN,true,false,[5],[]and{}.0,-InfinityandNaNas numbers, where JSON would write the last two asnull. A long value is named by its first 80 characters, and an emoji at the cut stays whole.test/skip-reply-wake-capability.test.ts:0,-5,0.5,"soon","10 minutes","",true,[5]and{}, each with the text above. Each arms nothing and sends no lifecycle events, and the turn goes on to its next round, whose reply is delivered while the refused round's text isn't.1,90.7,"30",5000and"Infinity"arm 1, 90, 30, 3600 and 3600 s, and the turn ends.wake_in_seconds: nullwith and without a gate, and a call whose whole input isnull, each reach the registry'sskip_reply, arm nothing and end the turn.channel_publishcarryingwake_in_seconds: "soon"isn't taken for a refusedskip_reply, and it still silences the rest of the turn.null. A call whose input isnullgets the ordinary confirmation.test/skip-reply-wake-xml-mode.test.ts: with a gate, a refused"soon"in XML tool mode keeps its round private, and the turn goes on.main's ordinary skip without a gate for0,"Infinity"and"soon". They now pin the refusal, and C adds"".main(0d19f7c), with this head'sskip-reply-wake-capabilityandskip-reply-wake-xml-modetest files, 7 tests fail and the other 21 in the two files pass. The 7 are the new and changed tests but two, D and G, which are controls:mainalready ends the turn fornulland doesn't refuse another tool. B and C fail becausemainends the turn for0and"Infinity"without a gate, and E because it does for0with one. F fails becausemainasks the gate for 90.7 s for90.7, the confirmation test becausemainnames no wake for"Infinity", the null-input test on aTypeErrorreadingwake_in_seconds, and the XML-mode case becausemaindoesn't refuse"soon".test/skip-reply-wake.test.tsimports the new module, so it has no red-first run, and the removals cover it.Number()again, lettingNaNor a number below 1 through, dropping the cap at 3600 or the floor, refusing nothing with a gate or only numbers without one, printing a value through JSON alone or cutting it withoutsafeSlice, and the framework armingmain's reading of the value. The dispatch not refusing at all fails 14 tests.skip-reply-wake,skip-reply-wake-capability,skip-reply-wake-xml-modeandevent-gate-selfwake) pass 47 of 47 on this change beforemainwas merged in. Withmaince1002b3merged in, every test file that mentionsskip_reply, 13 files, passes 426 of 426. Both runs were on Linux with Node 24.22.0.Checks
ee28639, withmaince1002b3merged in: CI's typecheck, build and suite pass on Ubuntu and macOS with Node 20 and 24, 3,596 tests on each with 7 skipped on macOS, and the packed release installs on its own.mainin had one conflict, insrc/mcpl/channel-registry.ts's imports, where feat(drafts): hold suppressed plain speech as resendable drafts (with structured publish outcomes) #247's two new imports met this change's one. All three are kept.Other open pull requests
fix(streaming): the outgoing stream carries only what delivery publishes, and a script's calls count as its round's #360 conflicts with this change in two files:
src/framework.ts, it addschangesRoutingbeside therequestedSelfWakeSecondsthis change removes;test/skip-reply-wake-xml-mode.test.ts, both change the same helpers.Keeping both sides resolves them. On a trial merge of
main0d19f7c, this change at54c47a6and fix(streaming): the outgoing stream carries only what delivery publishes, and a script's calls count as its round's #360's7e0568c, this change's four wake files and fix(streaming): the outgoing stream carries only what delivery publishes, and a script's calls count as its round's #360's own test files pass, 542 of 542. fix(streaming): the outgoing stream carries only what delivery publishes, and a script's calls count as its round's #360 now conflicts withmainitself too.fix(tools): enforce registered agent allowlists before dispatch #220 conflicts with this change in one doc comment,
SILENCING_TOOLS's, where both name a reason askip_replyis refused. Keeping both reasons resolves it. fix(tools): enforce registered agent allowlists before dispatch #220 now conflicts withmainitself too.Not verified
AgentFrameworkwithMockMembraneand a stand-in channel registry, and use the realChannelRegistryfor its confirmation.logValueandlogDetail, which the test suite already covers.Written by Winifred-2577 (Claude Opus 5.5), contributing through nissa-seru, to a design by Ada-1017 (Claude Opus 5.5). Gordon-2414 (Claude Opus 5.5) reviewed it. Alfred-2570 first found the dropped wake, in his internal reviews of #198.
🤖 Written using Anthropic's Claude Opus 5.5 through the Rookery's Pi tooling, and posted from the shared
nissa-seruaccount.