Skip to content

test(mcpl): the written-to test's host write fails before the child's SIGTERM, in both engines - #896

Merged
nissa-seru merged 1 commit into
anima-research:mainfrom
nissa-seru:amos/f578-race-order
Oct 29, 2026
Merged

nissa-seru merged 1 commit into
anima-research:mainfrom
nissa-seru:amos/f578-race-order

Conversation

@nissa-seru

Copy link
Copy Markdown
Contributor

This makes the test "a server that dies by SIGTERM while the host is writing to it raises no alert, though the host's write failed, in either engine" put its two events in order itself: the host's write to the child fails first, and only then does the test send the child SIGTERM. Its new title names that order. Before, each child killed itself as the host wrote to it, which left that order to the host's event loop. In CI at #884's head, which merges main at 0f11975e48, the test failed in its modern case. With this change, the test passed in each of 20 runs, and a red run shows it still fails when the transport alerts on such a close. The change is to test/mcpl-down-for-good.test.ts only.

Problem

The test came with #876. It needs the host to record its failed write before it handles the child's exit. At 0f11975e48, src/mcpl/transport.ts:413 gives the close's reason as `child process exited, and its pipe failed with ${this.pipeError.message}` only when this.pipeError is set, and 'child process exited' otherwise.

At 0f11975e48, the legacy child killed itself once its burst of pings was out, and the modern child as soon as its stdin brought data. In CI run 40826523575, at #884's head a3f568c96b, the Build & Test (ubuntu-latest, node 22.12.0) job failed this test. Its modern case read writeFailed: false, with signal: 'SIGTERM', alerts: 0 and posted: 0, and its legacy case read writeFailed: true. a3f568c96b doesn't change this test's file. In src/mcpl/transport.ts it has the pipe-failure wait take its grace from exitGrace(), which returns testExitGraces.get(this.child) ?? STDIO_EXIT_GRACE_MS, and this test file doesn't call setStdioExitGraceForTests, which sets testExitGraces. In a run at 0f11975e48 with the modern child's SIGTERM delayed 300 ms after its first stdin data, the test failed with the same reading.

Changes

  • Both engines' children now use the file's DEAF_ON_USR1, which the test "a connected server whose child the transport ends after a pipe failure is said to the operator once, though it dies by SIGTERM, in either engine" already uses. On SIGUSR1 that child runs closeSync(0) and openSync("/dev/null", "r"), writes deaf to stderr, and writes a ping request to stdout.
  • The test sends the child SIGUSR1 and waits for its deaf line, which the child writes after closeSync(0). In the modern case it then makes one tool call, for the reason the comment above DEAF_ON_USR1 gives: "A modern host drops the ping, so the test calls a tool instead." It waits until a new helper, pipeFailureOf(), reads the transport's recorded pipeFailure, and only then sends the child SIGTERM.
  • pipeFailureOf() reads pipeFailure from the legacy connection's transport, or from the modern connection's session's stdio.
  • The test's title is now "a server whose child dies by SIGTERM after the host's write to it failed raises no alert, in either engine".
  • The test's comment says that the close names the pipe only if the host has its write's failure before it handles the child's exit, and that the exit must then be handled within STDIO_EXIT_GRACE_MS of the failure, or the transport ends the child itself and the close raises an alert.
  • DIES_WHILE_WRITTEN_TO, the two children that killed themselves, is removed. The test's assertions are unchanged: in both engines, signal: 'SIGTERM', writeFailed: true, and no alert.

On October 29 at 06:20 UTC, no open pull request changes test/mcpl-down-for-good.test.ts.

Tests

At a clean checkout of eb88f49c69, on Linux with Node 24.22.1 and npm 11.19.1, after npm install with package-lock.json and node_modules removed, npm run build and npm run pretest, with a one-minute load average of 11.5 at the start and 14.7 after the 20 runs below. npm ls --depth=0 gives @animalabs/chronicle 0.5.0, @animalabs/context-manager 0.24.1, @animalabs/mcpl-core 0.4.0, @animalabs/membrane 0.8.0, @babel/parser 7.29.11, @modelcontextprotocol/client 2.4.0, @modelcontextprotocol/server 2.4.0, @types/node 22.20.5, @types/ws 8.18.2, chokidar 4.0.3, discord.js 14.27.0, tsx 4.23.15, typescript 7.0.2 and ws 8.22.0.

  • test/mcpl-down-for-good.test.ts: 20 of 20 pass.
  • This test alone, 20 times: it passed each time.
  • Red, with src/mcpl/transport.ts:414 reading ...(this.pipeError ? { pipeFailed: true } : {}) in place of ...(this.endedForPipe ? { pipeFailed: true } : {}): the test fails, reading writeFailed: true, alerts: 1 and posted: 1 in both engines.

On GitHub-hosted runners at Node 22.12.0, in a workflow outside this repository, with 3 CPU-burning processes beside the test on macOS and 2 on Ubuntu, this test alone passed 20 of 20 times on macOS and on Ubuntu with an earlier version of this change, whose test file differs from this head's only in comments and the test's title. At 0f11975e48 it also passed 20 of 20 on both. So these runs show that version holding under that load, and don't show 0f11975e48 failing.

Not verified

  • The time the test leaves to the transport. When the pipe error comes, src/mcpl/transport.ts:395 at 0f11975e48 calls this.exitWithin(STDIO_EXIT_GRACE_MS), and :565 sets that to 2,000 ms. The test sends its SIGTERM after it has polled for the recorded pipe failure. No run here measured how much of those 2 s the poll, the SIGTERM and the host's handling of the exit take, or slowed them past it. The red run shows that a close marked pipeFailed fails the test with alerts: 1.
  • 0f11975e48 failing under load alone. No run showed it fail without the delay above. The CI failure was at a3f568c96b, not at 0f11975e48.
  • This repository's CI hasn't run this change.

  • This change is test-only: it touches nothing in src/, and the no-changelog label applies.

Amos-2081 made the change. Alfred-2451 ran the delayed run at 0f11975e48, and Alfred-2451 and Amos-2081 found that the failing case was the modern one. Naomi-2422 read the change. Daniel-2612 reported the failure in #884's CI, ran this change's tests at a clean checkout and wrote this description.

… SIGTERM, in both engines

The test of a server that dies by SIGTERM after the host's write to it
failed needs the host to have seen its write fail before it handles the
child's exit: the close's reason names the pipe only if the pipe error
came first. Its children killed themselves as they were written to, the
legacy one once its burst of pings was out and the modern one as soon as
its stdin brought data, which left that order to the host's event loop.
Under load the loop could write everything before the child died, or take
the exit first, so that nothing it wrote had failed, and the modern case
read writeFailed false.

Both children now close their stdin on SIGUSR1, as the pipe-failure test's
child does (DEAF_ON_USR1), so the host's next write fails while the child
still runs. The test waits until the host has recorded that failure
(pipeFailureOf), and only then sends the SIGTERM. So the pipe error comes
first whatever the load. The child's exit then has to be handled within
the 2 s the transport gives a child after a pipe failure before ending it
itself; if it isn't, the transport ends the child and the test fails with
an alert.

The test's title said the child died while the host was writing to it,
the race this removes. It now says the SIGTERM comes after the host's
write failed. A SIGTERM with no failed write is the case of the test of a
child that ends as asked.
@nissa-seru nissa-seru added the no-changelog Exempts this PR from the changelog check (internal-only change) label Oct 29, 2026
@nissa-seru
nissa-seru merged commit 3968788 into anima-research:main Oct 29, 2026
14 checks passed
nissa-seru pushed a commit to nissa-seru/agent-framework that referenced this pull request Oct 29, 2026
main 7aab5e3 brings anima-research#896 (the written-to test's host write fails
before the child's SIGTERM, in both engines), anima-research#871, anima-research#897, anima-research#893, anima-research#870 and
anima-research#869 since 0f11975. No file conflicts.
tel-0s added a commit that referenced this pull request Oct 29, 2026
…'s context retired, MCPL inference aborted at stop, the re-armed deadline cut, failure accounting released with its agent

main at 612f6fd is eight merges past 4a45766, with no release among them,
so package.json stays 0.30.0 and this branch's floors stand (chronicle
^0.6.0, context-manager ^0.25.0, membrane ^0.8.0), which
test/dependency-ranges pins. It brings #871 (a re-armed turn's deadline cut
follows the deadline-cut rule; an ended agent takes its re-armed deadline
cut with it), #896 (the race order of the MCPL down alerts), #897 (an ended
agent's failure accounting goes with it: forgetFailureAccounting), #893
(the [inference-failed] marker's pins), #870 (strict credential mode for
opt-in WebSocket dials), #895 (stop() aborts each MCPL server's
inference/request still running, and answers it -32000), #898 (a module's
context is retired once its stop() completes: its pushes dropped with a
line, its writes, calls and subscriptions refused, its unapplied response
and unmerged gatherContext answer left out) and #884 (the suite's speed:
setAbandonGraceForTests and CI).

One path conflicted (3 regions):

- src/module-registry.ts:
  - imports: main's boundedErrorMessage; heldWriteId stays out, as on this
    branch, which has no held-write handles.
  - the registry's constructor: both, this branch's window lookups
    (windowStatusOf, observes, viewKeeps, agentNameOf) and main's
    dropRetiredPush.
  - findMessageByExternalId's comment: this branch's (a '' names a write
    kept in memory, a surgery's hold or the wait for its agent's
    recovery; no held rows) with main's clause that a stopped module's
    context keeps its mapping as it was. The code merged cleanly: main's
    retired guards around the mapping's updates, without the held-row
    answer this branch removed.

Read onto this branch past the conflict:
- releaseAgentRuntime takes main's freshCallsCut.delete,
  forgetFailureAccounting and rearmDeadlineCuts.delete, beside this
  branch's forgetPresentation; an ephemeral run's disposal and
  disposeConversationAgent's name-only arm take them as main put them.
- ModuleContext's doc (types/module.ts): a retired context's reads list
  this branch's window lookups (windowStatusOf, observes, viewKeeps,
  agentNameOf), which stay open as main's reads do; main's clause that a
  read meeting a held write may record its landing goes, as this branch
  has no held rows to meet.
- applyProcessResponse is synchronous on main now; this branch's body has
  no await, so it takes main's form.

storeSees: no new site; main opens and derives no context manager.
agentSees: no new view decision (noteSpokenTo's existing check gains only
the re-arm's reset). Notice tags: none new; main's new texts ([framework],
[process] and [unstick] lines, the -32000 answer) go to stderr or an MCPL
server, not to an agent. #898's retire and #884's suite changes touch no
presentation point.

Changelog: no release in main's range, so no fragment was consumed; main's
five new fragments come in as written. retire-stopped-module-context's
"reads answer, and fail, as before" covers this branch's window lookups.

Tests adapted:
- retired-module-context, the findMessageByExternalId case: its '' entry
  comes from a write a surgery's hold keeps in memory, released before the
  module's removal, where main made it with a quiesce. A write while
  quiesced is stored at once here (#291), so it mapped to its id.

Tests: built with zero errors; no stale compiled test; fixtures copied.
main's 35 changed test files: 1522 tests, 1516 pass, 1 fail (the
retired-module-context case, adapted above; its file then 22 of 22), 5
skipped (Linux-only workspace cases). presentation*, window-*, dendrite-*,
stored-injections, audience-stamp*, dependency-ranges, held-* and
tune-out-* (44 files): 606 of 606. Full suite (npm test): 9518 tests,
9469 pass, 0 fail, 49 skipped (runtime-conditional: Linux-only rules, and
temp directories that fold case or normalization).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

no-changelog Exempts this PR from the changelog check (internal-only change)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant