Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
d6dd2c6
deps: membrane ^0.6.0
Oct 18, 2026
67d63dd
test: a tool call whose arguments are JSON null arrives as {}
Oct 18, 2026
f62ac15
test(deferred-window-only): the landed-shard tests pin context-manage…
Oct 18, 2026
9c21933
fix(mcpl,deferral): refuse content context-manager won't store at the…
Oct 18, 2026
b94da58
Merge main (15aadc96) into the floors branch
Oct 19, 2026
cbda885
deps: context-manager ^0.16.0
Oct 19, 2026
44334f0
fix(maintenance): a withdrawn maintenance call is context-manager's C…
Oct 19, 2026
d9b3d16
build: Node 22.12 or later, as membrane 0.6.0 and context-manager 0.1…
Oct 19, 2026
f8c57ca
docs(changelog): what hosts get with membrane ^0.6.0 and context-mana…
Oct 19, 2026
c16727d
test: the null-arguments test's framework half is a control since #501
Oct 19, 2026
b703ffd
fix(deferral): refuse content context-manager won't store where it's …
Oct 19, 2026
dd378fe
ci: run the packed-install check on Node 22.12.0 and 24
Oct 19, 2026
76bc135
docs: what hosts read with the floors, and comments that named older …
Oct 19, 2026
9aa67cb
fix(modules): refuse a malformed process response before any response…
Oct 19, 2026
8697f4d
fix(modules): the trace and process log list only responses that applied
Oct 19, 2026
3107edc
fix: a refused process response gets a trace; the API refuses content…
Oct 19, 2026
fc64381
Merge main (e01a2337) into the floors branch
Oct 19, 2026
ad3d535
docs(changelog): what message.send did with content that isn't text
Oct 19, 2026
f0a7c43
test: pin the refusal trace's places, one line per refused response, …
Oct 19, 2026
73477a8
Merge main (815bfe35) into the floors branch
Oct 19, 2026
e096285
Merge dependency-floor work into main; align CM 0.17.1 and preserve d…
Oct 21, 2026
aa2d19b
Address alignment review with release notes and real-store recovery r…
Oct 21, 2026
72134b8
Honor CM pre-write malformed refusal while preserving ordinary-error …
Oct 21, 2026
2e7526a
Address R1 migration notes and exercise the intended count-failure bo…
Oct 21, 2026
35eae12
Merge main (4e3fea38) into the alignment branch
Anarchid Oct 21, 2026
26e66c0
deps: context-manager ^0.18.0
Anarchid Oct 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 21 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
runs-on: ${{ matrix.os }}
# Without a limit, a step that hangs leaves the check pending until
# GitHub's six-hour limit. Passing runs take under seven minutes. A
# test file stuck under Node 20, or a test stuck under Node 24, fails
# test file stuck under Node 22.12, or a test stuck under Node 24, fails
# sooner and by name, through npm test's --test-timeout.
timeout-minutes: 20
strategy:
Expand All @@ -24,11 +24,17 @@ jobs:
# node_modules tree records only that machine's native binaries, so a
# lock made on macOS breaks Linux installs and vice versa. Each runner
# catches the lock the other's platform produced. Tests run on ubuntu.
# Node: the engines floor and current LTS — stdio flushing semantics
# around process exit differ between versions and platforms.
# Node: the engines floor, pinned exactly; the newest Node the range
# admits; and, between them, every maintained major line (24), as
# membrane and context-manager test theirs. stdio flushing semantics
# around process exit differ between versions and platforms. macOS runs
# the two ends only.
matrix:
os: [ubuntu-latest, macos-latest]
node: [20, 24]
node: ['22.12.0', 24, latest]
exclude:
- os: macos-latest
node: 24
Comment on lines +34 to +37

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium severity, maintainability. Renamed CI check names may leave branch-protection required checks unsatisfiable.

The matrix moves from node [20, 24] to ['22.12.0', 24, latest] and excludes macOS/24. The pack-check job is renamed to 'Packed release installs on its own (node ${{ matrix.node }})'. As a result, checks such as 'Build & Test (ubuntu-latest, node 20)', 'Build & Test (macos-latest, node 24)' and 'Packed release installs on its own' no longer report. If any of them is a required status check on main, every PR will wait on a check that never runs until an admin updates the branch protection settings. Neither the PR nor CONTRIBUTING mentions that update.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Authored by Ivo (DevOps agent), published by Nestor under k-res-dev — addressed in aa2d19b.

Added a maintainer-prerequisite section to CONTRIBUTING listing the retired and replacement check names. An administrator must inspect/migrate the actual required-status contexts before merge; the PR does not alter branch protection, bypass it or claim those settings were inspected. Keep required coverage rather than silently dropping checks. This operational confirmation remains with maintainers.


steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
Expand Down Expand Up @@ -61,7 +67,7 @@ jobs:
# leaves running, such as a late arrival after stop() ("Queue is
# closed"). These files run unforced so that class fails here, not in a
# later PR's qualification. They get npm test's --test-timeout too:
# under Node 20 it fails a file whose leftover work keeps it alive, by
# under Node 22.12 it fails a file whose leftover work keeps it alive, by
# name, and under Node 24 the job's limit ends such a file.
- name: Test unforced (work in flight at stop)
run: node --test --test-timeout=600000 dist/test/stop-with-work-in-flight.test.js dist/test/stop-with-host-operations.test.js dist/test/conversation-routing.test.js
Expand All @@ -78,9 +84,17 @@ jobs:
# hours: above the check's own limits (10 minutes each for npm pack and npm
# install, a minute for each load), so the check names what hung.
pack-check:
name: Packed release installs on its own
name: Packed release installs on its own (node ${{ matrix.node }})
runs-on: ubuntu-latest
timeout-minutes: 40
# The engines floor and current LTS: the check's load hook goes in through
# register below Node 22.15 and through registerHooks from there, so each
# branch a host takes runs before a merge, as publish.yml runs both before
# a release.
strategy:
fail-fast: false
matrix:
node: ['22.12.0', 24]
Comment on lines +87 to +97

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium severity, maintainability. Renamed and re-matrixed CI jobs change the required status-check contexts.

The pack-check job is renamed from 'Packed release installs on its own' to '… (node ${{ matrix.node }})'. The Build & Test matrix drops node 20, which removes the 'Build & Test (…, node 20)' contexts and adds new ones. If branch protection requires the old names, every PR waits forever on checks that never report. The new 22.12.0/latest contexts are also not enforced until a maintainer edits the protection settings. The PR only adds a CONTRIBUTING note and leaves the mismatch for after merge. Either keep the old job names, for example a stable name for one matrix leg, or coordinate the protection change with the merge.


steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
Expand All @@ -90,7 +104,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
with:
node-version: 24
node-version: ${{ matrix.node }}

# As publish installs, so a build that came to need a dependency's
# install script fails here, before a tag, rather than in publish.
Expand Down
13 changes: 10 additions & 3 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
with:
node-version: 20
node-version: '22.12.0'

# The same check `npm version` runs first (see CONTRIBUTING.md): an
# open issue labelled release-hold stops the release. Both jobs that
Expand Down Expand Up @@ -64,11 +64,18 @@ jobs:
# 6 hours: above the check's own limits (10 minutes each for npm pack and
# npm install, a minute for each load), so the check names what hung.
pack-check:
name: Packed release installs on its own
name: Packed release installs on its own (node ${{ matrix.node }})
runs-on: ubuntu-latest
timeout-minutes: 40
permissions:
contents: read
# The engines floor and current LTS: the check's load hook goes in through
# register below Node 22.15 and through registerHooks from there, so each
# branch a host takes runs before a release.
strategy:
fail-fast: false
matrix:
node: ['22.12.0', 24]

steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
Expand All @@ -78,7 +85,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
with:
node-version: 24
node-version: ${{ matrix.node }}

# As publish installs, so a build that came to need a dependency's
# install script fails here rather than in publish.
Expand Down
14 changes: 10 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,10 +215,11 @@ npx tsc --noEmit # typecheck
code you no longer have — always build before testing, and after switching
branches. The `pretest` hook copies `test/fixtures/*.mjs` into `dist/`.

`--test-timeout=600000` bounds each test file as a whole under Node 20,
and each test under Node 24, at ten minutes; the slowest file,
`mcpl-runtime-lifecycle`, takes about three. Each job in `ci.yml` also has
a `timeout-minutes` limit, which ends a hang the test timeout doesn't reach.
`--test-timeout=600000` bounds each test file as a whole under Node
22.12, as it did under Node 20, and each test under Node 24, at ten
minutes; the slowest file, `mcpl-runtime-lifecycle`, takes about three.
Each job in `ci.yml` also has a `timeout-minutes` limit, which ends a hang
the test timeout doesn't reach.

Push-time CI (`ci.yml`) builds, typechecks and tests every push and PR on
ubuntu and macos. Lockfiles are deliberately gitignored in this repo, so CI
Expand All @@ -231,3 +232,8 @@ supply, fails the PR.
When working against local sibling checkouts rather than published versions,
see the ecosystem workspace setup — `bun install` at the `rust-connectome`
root links the four packages together.


CI check contexts follow the workflow matrices. When a matrix changes, a
maintainer should check that the repository's required-status list matches it;
workflow edits do not change branch-protection settings themselves.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -687,4 +687,4 @@ npm test # Run tests
npm run typecheck # Type-check without emitting
```

Requires Node.js >= 20. Chronicle requires a Rust toolchain for native module compilation (`npm run build` in the chronicle directory).
Requires Node.js >= 22.12. Chronicle requires a Rust toolchain for native module compilation (`npm run build` in the chronicle directory).
9 changes: 9 additions & 0 deletions changelog.d/549-maintenance-withdrawn.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
- A maintenance call agent-framework withdraws, under a surgery's hold or
once `stop()` has begun, no longer counts against a chunk's compression
(#549). `MaintenanceWithdrawnError` now extends context-manager's
`CompressionWithdrawn`, so context-manager rethrows it unchanged from every
rung of its compression fallback ladder and its merge path, recording no
outcome, attempt or quarantine event. Before, a call withdrawn after a
chunk's canonical call was refused was recorded as that rung's provider
error, and once every rung had failed, the chunk's request family was
quarantined, with its alert.
77 changes: 77 additions & 0 deletions changelog.d/floors-membrane-060-cm-017.breaking.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
- **Hosts:** agent-framework now depends on membrane `^0.6.0` and
context-manager `^0.18.0`, where it depended on `^0.5.87` and `^0.14.1`.
context-manager 0.18.0 requires membrane `^0.6.0` itself, so the two
install one membrane between them. What reaches a host through
agent-framework, from those releases' notes:
- **Your own membrane:** a host that builds its own `Membrane` and hands it
to agent-framework raises its own `@animalabs/membrane` range to
`^0.6.0`. context-manager counts a compression attempt's prompt through
that instance, and with an older membrane it counts Gemini's cached input
twice (below 0.5.81, OpenAI's too, and OpenRouter's on routes that report
OpenAI's `cached_tokens`), so a compression fallback that fits can be
rejected at admission and its chunk quarantined (context-manager 0.16.0).
- **Your own context-manager:** a host that constructs strategies from its
own `@animalabs/context-manager` must also raise that range to `^0.18.0`,
matching agent-framework's dependency, and verify that installation has
one compatible copy. Leaving the host on 0.14.x installs two copies and
hands an older strategy to a newer manager; strategy interfaces and
compression-pause/quarantine behavior differ between those versions.
- **The kv-unified hysteresis certificate is on by default** for a
strategy built from a recipe: a `kvUnified` block without
`hysteresisCertificate` takes the certified exit, and
`hysteresisCertificate: false` restores the full solver on every compile
(context-manager 0.15.0).
- **Content context-manager won't store** is refused where agent-framework
writes it, as the entry on malformed content says.
- **Stored history:** content an older context-manager stored that doesn't
match `ContentBlock` is now read mended, or left out, with one line per
message naming each place, where some such content made every compile
throw. A `tool_use` stored with `input: null`, as membrane 0.5.87's
OpenAI-family adapters gave it for the arguments `null`, reads as `{}`
with `null` in `unparseableInput`. 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).
- **Tools given `null` arguments:** an OpenAI-family tool call whose
arguments are the text `null` reaches the tool as `{}`, where it reached
it as `null`. membrane keeps the arguments' text on the `tool_use` block
as `unparseableInput` (membrane 0.6.0).
- **Anthropic streams:** after a stream's first event, SSE `ping`
keepalives now count as activity for its idle deadline, so a stream that
keeps sending only pings no longer ends at it (membrane 0.6.0).
- **Media notes:** two of the notes membrane writes in place of media it
doesn't send now say the model isn't seeing it. The first request after
upgrading carries the new text wherever it renders such a note, so a
provider's prompt cache misses once, from the first such note on
(membrane 0.6.0).
- **Caller-supplied blob references:** context-manager 0.16.0 and later refuse
`blob_ref` writes with `MalformedContentError`; these references are
store-internal. Read with blobs resolved (the default) before copying a
body into a new write. An unresolved read is not a write-ready payload.
- **Compression quarantine status:** callers that inspect or compare
`getCompressionQuarantineStatus()` must account for its `spans` field,
including an empty array when no spans are quarantined.
- **kv-unified policy:** context-manager 0.17 adds age bands, joint latent
demand and the subtree engine. Explicit recipe overrides remain important:
`policy.ageBandSteps: 0` requests exact ages, `latentDemandMode: 'individual'`
chooses independent ranking, and `engine: 'dag'` selects the DAG engine.
- **Merges across an unowned message:** the floor is 0.18.0, not 0.17.x,
because before it a merge could group the summaries on either side of a
message no chunk owns (a raw pin, a message an unpin freed, or a head
window `resetHeadWindow` left mid-stream), and under kv-unified without
`treeifyNonContiguousSummaries` or `preserveGapBearingSummaries` every
compile after it threw `CanonicalForestError`. A store already holding
such a summary loads as before (context-manager 0.18.0, #287).
- **`close()` is final:** after a context manager's `close()`, every
operation that writes or calls into its strategy throws, with an
app-owned store too; open a new manager on the store to go on with it
(context-manager 0.18.0).
- **Compression debt:** `getCompressionDebt()` answers `unknown` when it
can't read what decides its state, with `null` for each count it couldn't
compute and an operator-facing `error`. A host that shows the report
treats `unknown` as needing attention and never shows a `null` count as
0 (context-manager 0.18.0).
- **Strategy subclasses:** `AutobiographicalStrategy`'s protected
`pruneToolEntries` is removed; override `toolPruning` or `prunedForSend`
instead (context-manager 0.18.0).
- The rest is in membrane 0.6.0's notes and context-manager 0.15.0's,
0.16.0's, 0.17.x's and 0.18.0's.
12 changes: 12 additions & 0 deletions changelog.d/malformed-content-refused.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
- Content context-manager won't store is refused where it's written, and a deferred write that context-manager refuses no longer holds its agent's later writes. From context-manager 0.16.0, a write whose content doesn't match `ContentBlock` is refused with a `MalformedContentError` (context-manager #184). Before it, such content was stored as given, and a compile that read it could throw.
- **Writes and edits through the framework:** a module's write, and its edit of a write still held for a turn's end, throw `MalformedContentError`, naming each place, as the store's own write and edit do. They throw whether the write would be stored at once or deferred while a turn runs, where a deferred one used to be refused only at a later flush. Content the store mends, a `tool_use` whose input isn't an object, is written as before. The API's `message.send` refuses content that isn't text with an error, where it answered `pending` and stored the message as given.
- **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, and it doesn't count as accepting console or API input. A `[process-response-refused]` line and a `process:response-refused` trace name the module, the other modules' responses apply, and the `process:completed` trace and the process log list only the responses that applied.
- **The push and channel lanes** check content with context-manager's `malformedContentPlaces` before anything else. Content from an MCPL server is converted at the boundary into blocks the store accepts, so this catches events that never crossed it. When it names any place, the message stores no row and queues no wake. A stderr line, `[push-event-refused]` or `[channel-incoming-refused]`, names the server, the channel where there is one, the event or message, and the places. A new trace, `mcpl:malformed-content-refused`, carries the same, with the places as `places`. Content that's absent is still the empty-content check's.
- **A deferred write that context-manager refuses as malformed is dropped at the drain** and logged, rather than queued again: it would be refused at every flush, and so would hold every later deferred write for its agent for good. Since writes through the framework's `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. Any other failure keeps the entry queued, as before, unless a read-back shows it landed. A `tool_use` dropped this way lets a `tool_result` queued behind it be stored alone.

The store's own validation checks all content and shards before append, so
an unavailable readback does not make such a refusal retryable. Other errors
whose landing is unknown remain queued and are reconciled by ID before retry.
A synchronous custom strategy hook throwing this same error after append is
tracked separately in context-manager #288; this is not a general guarantee
for arbitrary host callbacks when readback is also unavailable.
9 changes: 9 additions & 0 deletions changelog.d/node-22-12.breaking.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
- **Anyone running agent-framework on a Node before 22.12:** agent-framework
now declares `engines.node` `>=22.12.0`, where it declared `>=20.0.0`, as
membrane 0.6.0 and context-manager 0.18.0, which it now depends on, both
do. CI tests at Node 22.12.0 exactly, at 24 and at the newest Node, where it
tested 20 and 24, and the publish workflow builds and tests at 22.12.0.
npm warns when it installs agent-framework on an older Node, and refuses
only with `engine-strict` set. Nothing in agent-framework's code changes
with this; older Node versions are simply no longer tested.
- **Migration:** run Node 22.12 or later.
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@
"license": "MIT",
"dependencies": {
"@animalabs/chronicle": "^0.4.0",
"@animalabs/context-manager": "^0.14.1",
"@animalabs/membrane": "^0.5.87",
"@animalabs/context-manager": "^0.18.0",
"@animalabs/membrane": "^0.6.0",
"@modelcontextprotocol/client": "^2.3.1",
"chokidar": "^4.0.3",
"discord.js": "^14.25.1",
Expand All @@ -65,6 +65,6 @@
"typescript": "^7.0.2"
},
"engines": {
"node": ">=20.0.0"
"node": ">=22.12.0"
}
}
6 changes: 6 additions & 0 deletions src/api/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,12 @@ export class ApiServer {
if (!params.participant || !params.content) {
throw new Error('participant and content are required');
}
// ApiModule stores the message as one text block, which context-manager
// refuses unless its text is a string, so content that isn't text is
// refused here, where the caller hears it, rather than at dispatch.
if (typeof params.content !== 'string') {
throw new Error('content must be text');
}

// Push api:message event for the ApiModule to handle
this.framework.pushEvent({
Expand Down
Loading
Loading