Skip to content

chore(deprecate): context injection (gatherContext + MCPL beforeInference) - #218

Open
antra-tess wants to merge 13 commits into
mainfrom
chore/deprecate-context-injection
Open

antra-tess wants to merge 13 commits into
mainfrom
chore/deprecate-context-injection

Conversation

@antra-tess

@antra-tess antra-tess commented Oct 3, 2026 •

Copy link
Copy Markdown
Collaborator

Why

Context injection, whether from module gatherContext() or MCPL context/beforeInference contextInjections, produces per-compile overlays. Injected blocks are never stored and are re-anchored to the latest user-participant message on every compile. This has two consequences (#171):

  • Prompt caching breaks across activations. At the next activation the previous block vanishes from its slot and reappears at the new tail, so the request diverges from every earlier one at that point. On OpenAI Responses/Codex, where the cache only hits boundaries an earlier request wrote, the first call of every activation falls back to a head-only cache hit. On Anthropic the loss is bounded but real.
  • Blocks can land between a tool call and its result. A mid-activation recompile, such as a stream retry, can anchor the block there.

The decision is to deprecate the mechanism and recommend against it.

What this does

Behavior is unchanged: injections still apply. Remaining uses become visible:

  • @deprecated annotations on Module.gatherContext, Module.contextTimeoutMs and BeforeInferenceResult.contextInjections, with the alternative. Durable content belongs in the system prompt. Changing state should be delivered as conversation content (an EventResponse that adds a message, a push event, a tool result), so it is stored once at its own position and stays cache-stable.
  • Deprecation warnings: ModuleRegistry.gatherContext and HookOrchestrator.beforeInference log one [deprecated] line per module or MCPL server the first time it actually injects, via the new src/context-injection-deprecation.ts.
    • Sources that return nothing stay silent.
    • Injections at positions outside a server's grant are still dropped exactly as before, and are not warned about.
  • README: the module-interface and context-management sections now say the mechanism is deprecated and point to Ephemeral context injections re-anchor on every activation and defeat prompt caching (head-only hits on OpenAI lanes) #171.
  • Changelog fragment: changelog.d/deprecate-context-injection.changed.md.

Removing the mechanism is left for a follow-up. The spec-level deprecation (and 0.6 removal schedule) is anima-research/mcpl#15 (RFC-010); the context-manager placement API is deprecated in anima-research/context-manager#140.

Tests

  • New: test/context-injection-deprecation.test.ts has 4 tests:

    • a module injection still applies and warns once across turns;
    • a module returning nothing is silent;
    • a granted MCPL injection still applies and warns once;
    • an ungranted position is still dropped and does not warn.

    The first three fail without the two call-site changes; the fourth guards the unchanged path.

  • Full suite: npm run typecheck and npm run build are clean. npm test has no failures attributable to this change. The two mcpl-awareness-barrier tests ("startup globally gates host/command…" / "…push/event…") fail intermittently under full-suite load on unmodified main too: I reproduced them on one of two clean main runs, and they pass 25/25 when the file is run alone. They are a pre-existing timing flake.

Companion PRs

anima-research/connectome-host#189 deprecates the host modules that are built on this hook (retrieval, subagents, instructions). It doesn't depend on this one, so they can merge in either order.

🤖 Generated with Claude Code

…ence)

Injected context blocks are per-compile overlays: never stored, and
re-anchored to the latest user-participant message on every compile. At
the next activation the old block vanishes from its slot and reappears at
the new tail, so the request diverges from every earlier one there — on
OpenAI Responses/Codex the first call of each activation falls back to a
head-only cache hit, and a mid-activation recompile can anchor the block
between a tool call and its result (#171).

Deprecate both sources without changing behavior:
- `Module.gatherContext`, `Module.contextTimeoutMs` and
  `BeforeInferenceResult.contextInjections` carry @deprecated with the
  recommended alternative (system prompt for durable content; conversation
  content — events, push events, tool results — for changing state).
- ModuleRegistry and HookOrchestrator log one `[deprecated]` line per
  module / MCPL server the first time it actually injects (sources that
  return nothing stay silent; ungranted positions are still dropped as
  before and not warned about).
- README notes + changelog fragment.

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

greptile-apps Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 4/5 Tier: apex

[Medium risk] Marks context injection as deprecated, still functional.

The PR appears safe to merge, with a non-blocking correction to the explanation of system injections.

Findings

  1. P2 `system` warnings overstate cache loss ▶
Fix with agent prompt
### Issue 1
src/context-injection-deprecation.ts:27-29
This notice also runs for `system` injections, but `buildSystemPrompt()` appends that content to the system prompt. Unchanged content can stay cache-stable and cannot split a tool call from its result. Saying it moves beside the latest user message can send users toward the wrong cache fix.

Keep the deprecation, but limit the movement and tool-pairing claims to `beforeUser` and `afterUser` here and in the matching docs.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

Deprecates Module.gatherContext, Module.contextTimeoutMs, and BeforeInferenceResult.contextInjections without removing existing injection behavior.

  • Adds one-time source warnings and four focused tests.
  • Updates migration advice in README.md and the changelog.
  • The warning and docs should distinguish fixed system injections from injections placed beside user messages.
  • antra-tess defers mechanism removal and MCPL spec changes to follow-ups. antra-tess also acknowledges the two mcpl-awareness-barrier failures as pre-existing timing flakes reproduced on unchanged main.

Reviews (1) · Last reviewed commit: "chore(deprecate): context injection (gat..."

Comment thread src/context-injection-deprecation.ts Outdated
Comment on lines +27 to +29
'Context injection is deprecated: injected blocks are per-compile overlays that are never ' +
'stored and are re-anchored on every activation, which breaks prompt-cache prefixes ' +
'(head-only hits on OpenAI lanes) and can land between a tool call and its result. ' +

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 system warnings overstate cache loss

This notice also runs for system injections, but buildSystemPrompt() appends that content to the system prompt. Unchanged content can stay cache-stable and cannot split a tool call from its result. Saying it moves beside the latest user message can send users toward the wrong cache fix.

Keep the deprecation, but limit the movement and tool-pairing claims to beforeUser and afterUser here and in the matching docs.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/context-injection-deprecation.ts
Line: 27-29

Comment:
**`system` warnings overstate cache loss**

This notice also runs for `system` injections, but `buildSystemPrompt()` appends that content to the system prompt. Unchanged content can stay cache-stable and cannot split a tool call from its result. Saying it moves beside the latest user message can send users toward the wrong cache fix.

Keep the deprecation, but limit the movement and tool-pairing claims to `beforeUser` and `afterUser` here and in the matching docs.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

The header comment, the notice, both @deprecated docs, the README note
and the changelog fragment described every injection as re-anchored.
All but the MCPL doc also said any injection could land between a tool
call and its result.

Only a beforeUser block can split a call from its result. A tool result
travels in a user-participant message, which the compile anchors to, so
an afterUser block lands after the result. A system-position injection
isn't re-anchored at all. buildSystemPrompt appends it to the system
prompt, and the compile returns it apart from the messages. When its
text changes, the request changes from there on, ahead of every message.
What holds for every position is that the block is never stored in the
conversation. Each text now gives each position its own cost.

The [deprecated] line is read by the host's operator, who often doesn't
maintain the module or server it names. It now says that the injections
still apply. It says that durable text belongs in the agent's configured
system prompt, which whoever configures the host sets, and that moving
changing state into conversation content is for whoever maintains that
source. It also says that turning the source off removes what it
injects. The line now says the source "returns context injections"
where it said "injected context", since it is logged where the source's
result is read, not where a request is built.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nissa-seru

Copy link
Copy Markdown
Contributor

@antra-tess We've opened #610 into this branch. It corrects what the texts say each injection position costs. A system injection is appended to the system prompt and never re-anchored, as Greptile noted above, and only a beforeUser block can land between a tool call and its result. It also makes the [deprecated] line say that the injections still apply. Nothing is needed from you for it.

We read #​218 against main at ea4297ab, 1,983 commits past this branch's base. It conflicts at its two call sites, both of which main has rewritten:

  • ModuleRegistry.gatherContext's .then now also notes a system block the system prompt can't carry (noteBlocksNotShown).
  • HookOrchestrator.beforeInference now converts each injection inside a try, which drops one it can't read.

In our trial merge the server warning comes after convertMcplInjection succeeds, so a server isn't named for an injection main drops as unreadable. A probe on that merge, with the text fix on top, showed it: an injection with content: null is dropped with "it couldn't be read", and no [deprecated] line names its server. With the warning before the conversion, as #​218 orders it, the same injection's server is named.

On that trial merge (main at ea4297ab merged with 6552677f, call sites resolved as above), on Linux with Node 24.22.0:

  • tsc and the build are clean.
  • The suite passes 5,716 of 5,760 tests, with 44 skipped and no failures. main's CI at ea4297ab (Ubuntu, Node 24) passes 5,711 of 5,756 with 45 skipped. The difference is the four new tests, plus one test CI skips because bun isn't on its PATH.
  • The four new tests pass. With both warnings removed, the three that pin them fail, and with either one removed, its own tests fail.
  • test/mcpl-awareness-barrier.test.ts, which the description's Tests section mentions, was removed from main in 2cb501a.

In connectome-host's main (f26b03dc), three modules implement gatherContext, so their operators would see the line:

  • instructions (src/modules/instructions-module.ts), at system unless configured otherwise;
  • retrieval (src/modules/retrieval-module.ts), at afterUser, which four of its recipes turn on;
  • the subagents module's Fleet Status HUD (src/modules/subagent-module.ts), at afterUser, which returns nothing until the agent turns it on with the module's hud tool.

connectome-host writes stderr to a log file in headless and TUI modes (src/headless.ts:154, src/tui.ts:441). An operator there would see one line per such module in each run of the host, the first time it injects.

We'd also suggest, outside the pull request, an instance set on ModuleRegistry and HookOrchestrator in place of the process-wide set. It would drop the test-only reset export, and main already keeps such a set for its line about system blocks, printed once per module and namespace.

Kestrel-2409 (Claude Opus 5.5), from an internal review.

— Daniel-2612

Kestrel-2409 and others added 2 commits October 21, 2026 13:44
… lands

A module's gatherContext() raced its own .then() against the timeout, so
an answer that came after the timeout was still pushed into the list
already returned for the turn, and its [deprecated] line was logged for
injections the turn had stopped waiting for. The race now settles on the
module's own promise, and the push and the line follow it, so an answer
after the timeout reaches neither. A test runs a late module beside one
that answers in time. The module's call also runs inside a promise now, so
a gatherContext() that throws before it returns one fails alone, logged
and skipped like a rejection, where it made the registry's gatherContext()
throw and no module's injections were returned.

The texts said a beforeUser or afterUser block reappears "at the new
tail", and that a beforeUser block lands between a tool call and its
result only after a mid-activation recompile. The anchor is the latest
message whose participant is "user", which tool results are stored as,
wherever it is in the history. On a host that stores human turns under
their authors' names, that's the latest tool result, so a beforeUser
block lands between a call and its result on an ordinary compile. With no
such message, context-manager before 0.16 drops a beforeUser block.

A beforeUser or afterUser block breaks prompt-cache prefixes when it
moves, each time a newer user-participant message takes its place, not on
every compile. A system injection's text is appended to the system
prompt, which comes before every message, so with a prefix cache each new
text of it misses the cache for everything after it. Blocks that aren't
text can't be carried in the system prompt.

The texts that said injections "still apply" and behavior is unchanged
now say that the deprecation changes nothing about how injections are
applied.

The line no longer says the injections "still apply", which didn't hold
for a late module, nor for a beforeUser block with nothing to anchor to.
It says that the line changes nothing about how they're applied. It no
longer names a position: a server's line named its first injection's,
and a module's none.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…drop the registry change

The line told every source to move durable text to the configured system
prompt, which is set when the agent is made and can't take system text
that changes at runtime. The line and the docs now say that changing
state belongs in conversation content, durable system text in the
configured system prompt, and that system text which changes at runtime
has no replacement. The header names the framework's own system
injections, a silent heartbeat's ephemeralSystemPrompt and the
subconscious's standing dispositions, which are added after the module
registry and the MCPL hooks and aren't logged.

The line now says who returned injections, that context injection is
deprecated (#171), and what an operator can do. How each position is
placed and what it costs stays in the header and the @deprecated docs.

Placement, from runs: with no user-participant message, context-manager
0.16.0, 0.20.1 and 0.21.0 put a beforeUser and an afterUser block at the
end, beforeUser first, and 0.11.0, 0.14.1 and 0.15.0 drop the beforeUser
block. The fallback that puts it at the end first appears in 0.16.0's
compile and is in every release since.
A beforeUser block beside a tool result lands between the call and its
result in the compiled messages; membrane's native formatter (0.5.87 and
0.7.1) then moves the result ahead of it into one message, and its OpenAI
Responses formatter sends it between the function call and its output.

The registry's late-answer and synchronous-throw changes, their two
tests and their fragment come out of this pull request; the fix goes to
main on its own. The MCPL hook's comment now says when a server is named:
at its first injection that passes the feature-set and grant checks,
before it's converted. The docs and the test header say the same of the
checks, where they said only the grant. Both hooks' comments say the
mechanism is still collected as before, where they said still applied,
since a collected injection can still be dropped for a conversation
fork. The README gains the no-user placement sentence the header and
docs carry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Kestrel-2409 and others added 4 commits October 24, 2026 23:34
Each sentence this round changes was checked against #218's head
(agent-framework 0.19.0, context-manager 0.11.0, membrane 0.5.87) and
against main (0.26.0, context-manager 0.22.0, membrane 0.7.1), since #218
ships only by merging into main.

- R3-1: the framework's own system injections are the subconscious's
  standing dispositions and, before 0.23.0, a silent heartbeat's
  ephemeralSystemPrompt. ee43daf, first released in 0.23.0, removed the
  heartbeat's.
- R3-2: the line, header and README name the per-server switch,
  disabledCapabilities: ["contextHooks.beforeInference.inject"], and say a
  server's system injections are denied by default. On both trees the
  switch leaves every inject leaf ungranted and observe, pushEvents and
  tools granted. The hook then collects none of the server's injections
  and prints a drop line for each. The whole-line test's literal follows.
- R3-3: system text that changes at runtime has "no provider-independent
  replacement". The header and both @deprecated docs say what membrane 0.7's
  system_change block can do (add text from its place, through the native
  formatter and the Anthropic adapter only) and what it can't.
- R3-4: a moved block or new system text also means no later reply can keep
  its thinking on a model that binds it, as context-manager 0.22's thinking
  binding describes.
- R3-5: the MCPL criterion holds in either order of warning and
  conversion. Each module and server whose injections are collected is
  named once, and none whose injections the feature-set and grant checks
  all drop. At this head the warning comes before the conversion, so a
  server whose granted injection can't be converted is named while the
  hook throws and collects nothing. "At the first injection the host
  collects from it" would be false there. The two comments no longer
  depend on that order.
- R3-6: the tests' titles and messages say "collected", which is what they
  check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… compile's part

- The system_change note now says that even through the native formatter
  and the Anthropic adapter, membrane refuses a request with no place for
  the change after it, as after a final assistant message, and that the
  API refuses one to a model without mid-conversation system messages. The
  short notes in both @deprecated docs, the README and the changelog
  fragment say it goes to a model that takes them.
- The header's thinking paragraph puts "context-manager 0.22 and later" on
  the compile's leaving thinking out, which is 0.22's, and names
  thinkingBinding.binds. The provider's cost holds at any version.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, and say where system_change goes

- The per-server switch, disabledCapabilities:
  ["contextHooks.beforeInference.inject"], also disables any feature set
  of the server's whose uses names an inject capability: at the handshake,
  with a stderr line, along with whatever else that feature set does, such
  as its push events. Run on both trees with a server declaring one
  feature set that uses inject.beforeUser and pushEvents and one that uses
  pushEvents alone: the first is disabled, an answer under it is skipped
  with no line, and the second stays enabled, its injection dropped with a
  line. The line, header, README and changelog fragment now say so, the
  header says an injection under a disabled feature set gets no line, and
  the whole-line test literal follows.
- A system_change's text goes at the first point at or after its place
  that follows a user turn, as membrane 0.7.1's NativeFormatter places it
  (a change after a user turn stays in place; one after an assistant turn
  goes after the next user turn). The texts no longer say "at its place".
- The native formatter sends one through an adapter that declares it can,
  which among membrane's own adapters is the Anthropic adapter, not through
  the Anthropic adapter alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… turn

membrane 0.7.1's NativeFormatter puts a system_change's message just
before the first assistant turn after the change's place, or at the end
when a user turn ends the request. Run with four placements: a change
between a user message and an assistant turn stays there; one between two
user messages goes after the second, since membrane merges them into one
turn; one after an assistant turn and before a user turn, or after a tool
result at the end, goes at the end. The short notes said "after the first
user turn at or after its place", which put the common case, a change
stored after a user's message, one user turn too late; the header's rule
read wrongly for two user messages in a row. All five texts now use
membrane's own placement.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nissa-seru

Copy link
Copy Markdown
Contributor

@antra-tess On agent-framework 0.27.0, an afterUser injection is stored in the agent's window and stays there, and the framework's own toolsets module injects through gatherContext. So this PR's deprecation, as written, contradicts the release it would ship after. We'd narrow it to the system and beforeUser positions, which 0.27.0 still places at every compile, and leave afterUser out of it. What we need from you is the scope you want here, since what #​610's texts should say depends on it. @Anarchid's review of #​610 found this (R4).

What 0.27.0 does (main at 0a4a2c69):

  • Since #​782, a start appends an afterUser injection to the agent's window, where it stays until a fold. Unchanged content isn't appended again while the agent's current copy holds it. A module can opt out with placedEachCompile: true, and compile() then places its injection at each start, as before. An MCPL server's afterUser injection can't opt out. compile() still places system and beforeUser injections at each start. The tests in test/stored-injections.test.ts pin each of these (:142, :170, :181, :229).
  • Since #​799, the toolsets module, registered when a server is configured with toolHints: true, answers gatherContext with the RFC-014 §8.3 reminder, when one is due, as an afterUser injection (src/mcpl/toolsets-module.ts:235-239; test/mcpl-tool-hints.test.ts:357).
  • main's own doc for placedEachCompile (src/types/module.ts:28-48) recommends opting out, so that compile() places an afterUser injection at each start, for "context meant to stay near the end, such as instructions moved off system". It names that injection's cost: "it moves, and on a model that binds a reply's thinking to everything before it, every reply after its old place loses its thinking".

What this PR says (src/types/module.ts:112-120 at 6552677): injections are "per-compile overlays — never stored, re-anchored to the latest user message on every compile", and "Do not add new implementations". On 0.27.0 the first part is false for an afterUser injection that doesn't opt out, and the toolsets module is a new implementation. git merge-tree of this PR's head into main merges src/types/module.ts without a conflict, and the merged doc then says both that an afterUser injection is stored "where it then stays" (:32) and that injections are "never stored" (:197).

Why we'd narrow to system and beforeUser. compile() places both at every start, so they're still the per-compile overlays this PR describes. main documents both forms of afterUser as supported. The stored form is the one #​782 added and the toolsets reminder uses. The opted-out form is what the placedEachCompile doc recommends, with its move cost written down. Deprecating either would contradict that doc. Exempting only the toolsets module would leave every other stored afterUser injection under a deprecation whose stated costs it no longer has. Keeping the deprecation whole would need the module and the placedEachCompile doc changed in this PR as well.

— Kestrel-2409

Kestrel-2409 added 3 commits November 9, 2026 09:39
Catches the branch up with main (96504c5) before the deprecation is
narrowed to the beforeUser position. Three files conflicted:

- README.md: the Module listing takes main's ModuleContextInjection
  signature and keeps the branch's deprecation marker and note; the
  EventResponse sentence and the "Before inference" paragraph take main's
  text, which says an afterUser injection is now stored where it lands.
- src/mcpl/hook-orchestrator.ts: main's per-injection try, namespace read
  and conversion stay; the deprecation line now comes just before the push,
  once the injection has converted.
- src/module-registry.ts: main's gatherContext stays; the deprecation line
  moves to where main keeps a module's answer, so a module that stopped
  before its answer was used isn't named.
Since 0.27.0 an afterUser injection is stored where it lands, and the
framework's own toolsets module injects that way, so deprecating every
injection deprecated what main recommends. A system injection goes into
the system prompt, so it doesn't move or split a tool call from its
result, and the subconscious's standing dispositions use it. Only a
beforeUser injection is placed again before the newest user message at
every compile.

- The [deprecated] line names a module or MCPL server only when its
  collected injections include a beforeUser one.
- @deprecated comes off Module.gatherContext, Module.contextTimeoutMs and
  BeforeInferenceResult.contextInjections. ModuleContextInjection's doc
  and McplContextInjection.position say what beforeUser and system cost.
- The README, the changelog fragment and the module's doc say that scope,
  and what placedEachCompile costs, in main's words.
- The tests give the registry the moduleContexts map main's gatherContext
  reads, and cover each position from a module and from a server.

Red: the parent's five changed source files with these tests fail 6 of 7.
A system, afterUser or placedEachCompile module and a system or afterUser
server are each named, and the new line's text differs; the out-of-grant
test passes. Green, on Linux with Node v24.22.2: 7 of 7, and CI's
scripts/test-as-ci.mjs passes 10,440 tests with none failing.
…ne is once per process

The deprecation module isn't exported, so its long doc comment repeated
the README, the changelog fragment and the type docs for no reader. It
keeps one line and the #171 link. The two public type docs no longer
point at that internal file, since a package's reader can't follow the
pointer and each doc already says why. The README and the fragment say
the [deprecated] line comes once per process for each source, since the
set that remembers them lives as long as the process.
@nissa-seru

Copy link
Copy Markdown
Contributor

This pull request now deprecates placing a context injection at beforeUser, and nothing else. @antra-tess, your title and description stay as you wrote them. This comment is the current account, at c0475266.

On Oct 26 we asked here to narrow it to system and beforeUser. That no longer holds for system. A system injection goes into the system prompt, so it doesn't move or split a tool call from its result.

What changed, before and after:

  • Scope: before, every context injection was deprecated, from a module's gatherContext() and from an MCPL server's contextInjections. Now only the beforeUser position is. system and afterUser stay supported, and the docs say what each costs.
  • At run time: before, a [deprecated] line named each module or server that injected anything. Now it names only one that places a beforeUser injection. Injections still apply as before.
  • Types: before, @deprecated was on Module.gatherContext, Module.contextTimeoutMs and BeforeInferenceResult.contextInjections. Now none of them has it. ModuleContextInjection's doc says what beforeUser and system cost, and McplContextInjection.position's says what beforeUser costs.
  • The branch: fix(deprecate): say what context injection costs at each position #610 is merged in, and then main.

afterUser stays because, since 0.27.0, an afterUser injection is stored where it lands. The framework's own toolsets module injects that way.

Credits

Kestrel-2409 caught up the branch and narrowed it, and wrote this comment. Basil-1007 read it. Ada-1017 decided the wording, and nissa decided the scope.

Details for agents

Head c0475266ae48, base main 96504c579a.

  • Commits after 6552677fd5: fix(deprecate): say what context injection costs at each position #610's seven, merged as a fast-forward; c9d6c978, merging main, with conflicts resolved in README.md, src/mcpl/hook-orchestrator.ts and src/module-registry.ts; c60899a9, the narrowing; and c0475266, which trims the internal module's doc to one line and says the line comes once per process.
  • Where each position goes: main requires context-manager ^0.25.1. At v0.25.1, a system injection's content goes to the system injections (src/context-manager.ts:3357), and a beforeUser one is spliced in before the newest user message at each compile (:3390). Tool results are stored as user messages (src/framework.ts:22658 at main).
  • Why afterUser and system stay: an afterUser injection is stored unless a module sets placedEachCompile (isStoredKind, src/stored-injections.ts:101 at main), first released in v0.27.0. The toolsets module returns an afterUser reminder (src/mcpl/toolsets-module.ts:309–312). The subconscious's standing dispositions are a system injection (src/tune-out/coordinator.ts:5302).
  • What logs: src/module-registry.ts:630 and src/mcpl/hook-orchestrator.ts:263 at the head.
  • Runs, on Linux with Node v24.22.2, each from a clean checkout after npm install (the repository keeps no lockfile) and npm run build:
    • Red: c60899a9's tree e8e756d3, with c9d6c978's versions of the five changed source files copied in, and only the new test file under node --test. 6 of 7 fail. The out-of-grant test passes.
    • Green: c0475266's tree a16088ab. The test file passes 7 of 7 under node --test. node scripts/test-as-ci.mjs passes 10,440, with 0 failing, 62 skipped and 1 todo.
  • Not verified: no host or MCPL server ran with a beforeUser injection. The tests drive ModuleRegistry.gatherContext and HookOrchestrator.beforeInference directly.

— Kestrel-2409

@nissa-seru
nissa-seru requested a review from Anarchid November 9, 2026 15:22
@nissa-seru

Copy link
Copy Markdown
Contributor

Replying to this comment.

A correction to one sentence of our earlier comment, about afterUser. It said: "afterUser stays because, since 0.27.0, an afterUser injection is stored where it lands." That holds unless a module sets placedEachCompile on the injection. Then it isn't stored (isStoredKind, src/stored-injections.ts:101–104 at c0475266). compile() places it again at each start, near the end, so it moves as a beforeUser injection does, with the same cache cost (src/types/module.ts:43–48). On a model that binds a reply's thinking to everything before it, every reply after its old place loses its thinking. No [deprecated] line names it, since that line comes for beforeUser alone (src/module-registry.ts:630, src/mcpl/hook-orchestrator.ts:263). This pull request's changelog fragment says the same. The rest of the comment holds.

Credits

Kestrel-2409 wrote this correction. Thanks to Leonard-2330, whose read found it.

— Kestrel-2409

@nissa-seru

Copy link
Copy Markdown
Contributor

/review

— Kestrel-2409

@anima-review

anima-review Bot commented Nov 9, 2026

Copy link
Copy Markdown

Automatic review of c0475266ae48.

About these findings

The findings below came from reading this PR and relevant code; not a holistic understanding of the project's functional intent and its architecture choices. Finding precision is higher for flagging issues than identifying ideal changes/fixes. This review prioritizes recall - minor findings (in particular) may be nits.

Before actioning a finding, consider the relevant code from first principles given the functional+architectural intent. In particular, when considering adding machinery/logic, scrutinize whether the incremental value of such pays rent for the complexity it adds to the codebase - kudzu has a direct negative impact on our future ability to work with the codebase.

Consider explicitly mentioning which findings you accept/decline (by code-formatted numerical id) and why; this enables future rounds of automated review to take your notes into account.

Findings, most severe first:

@anima-review

anima-review Bot commented Nov 9, 2026

Copy link
Copy Markdown

Automatic context view of c0475266ae48 merged onto 9d57cc81386d.

What a context view runs and what its checks judge: CHECKS.md.

The records of the 65 runs it could compare don't differ between the base and this head.

Couldn't tell whether 3 changed source lines ran, since Hoff/W13: its profiled drive failed at step 531330.refuse: turn 531330.refuse spoke, where its script ends it with no speech, so what it would have run after that can't be told; and 9 more reasons. 31 changed source lines map to nothing the host runs: 16 of src/context-injection-deprecation.ts's, 2 of src/mcpl/hook-orchestrator.ts's, 4 of src/mcpl/types.ts's, 2 of src/module-registry.ts's and 7 of src/types/module.ts's. The whole list is in the fold below.

11 runs couldn't be compared; the reasons are below.

Usage: the figures of the agents' requests, subagents' included, in the 65 runs are the same in the base and this head: 217,619 tokens sent a request, 70.5% of them read from the cache, 145,596,575 written to it, 3,857,208 output, 24 thinking blocks dropped, USD 0.6144 at list price a priced request and 123 requests with no list price. The figures are the stand-in's model of each provider's published caching and token rules, not a measurement; the stand-in answers the same every time, so a difference between the arms comes from the change. A list price is what each arm's host prices those figures at with its own table (connectome-host's priceCall), where it gives one, so it rests on the same model, with the stand-in's token estimate in place of the provider's own count, cache hits as the script's pacing gives them, and scripted replies.

Runs that couldn't be compared (11)

This head couldn't be compared at H5c/W12: its self-check, which drives this head again with recording off, didn't find the two drives' requests the same, so a difference from the base can't be put down to the change alone (93 requests with recording on, 93 with it off; its messages differs).

This head couldn't be compared at Hoff/W13: the run didn't measure (on the head, the run failed; on the base, the run failed) (both arms' runs failed: turn 531330.refuse spoke, where its script ends it with no speech).

This head couldn't be compared at H1/W13: the run didn't measure (on the head, the run failed; on the base, the run failed) (both arms' runs failed: turn 877092.refuse spoke, where its script ends it with no speech).

This head couldn't be compared at Hcont/W13: the run didn't measure (on the head, the run failed; on the base, the run failed) (both arms' runs failed: turn 292517.refuse spoke, where its script ends it with no speech).

This head couldn't be compared at Hlatest/W13: the run didn't measure (on the head, the run failed; on the base, the run failed) (both arms' runs failed: turn 476258.refuse spoke, where its script ends it with no speech).

This head couldn't be compared at Hexpl/W13: the run didn't measure (on the head, the run failed; on the base, the run failed) (both arms' runs failed: turn 282696.refuse spoke, where its script ends it with no speech).

This head couldn't be compared at Hhyb/W13: the run didn't measure (on the head, the run failed; on the base, the run failed) (both arms' runs failed: turn 377404.refuse spoke, where its script ends it with no speech).

This head couldn't be compared at Hdis/W13: the run didn't measure (on the head, the run failed; on the base, the run failed) (both arms' runs failed: turn 830687.refuse spoke, where its script ends it with no speech).

This head couldn't be compared at Hlock/W13: the run didn't measure (on the head, the run failed; on the base, the run failed) (both arms' runs failed: turn 555886.refuse spoke, where its script ends it with no speech).

This head couldn't be compared at Hpriv/W13: the run didn't measure (on the head, the run failed; on the base, the run failed) (both arms' runs failed: turn 467880.refuse spoke, where its script ends it with no speech).

This head couldn't be compared at Hretry/W13: the run didn't measure (on the head, the run failed; on the base, the run failed) (both arms' runs failed: turn 3465.refuse spoke, where its script ends it with no speech).

Upgraded runs beside their fresh twins
On the base requests tokens sent a request read from the cache written to it output thinking dropped last request dropped calibration
H1b/continuation/from-0.9.0 15 159,813 52.6% 993,917 20,252 24 0 1.28, confirmed after a reset
H1b/continuation/fresh 15 155,549 56.9% 862,775 20,252 0 0 1.29, confirmed
H1b/continuation/from-0.23.0 15 155,549 56.9% 862,775 20,252 0 0 1.29, confirmed
On the head requests tokens sent a request read from the cache written to it output thinking dropped last request dropped calibration
H1b/continuation/from-0.9.0 15 159,813 52.6% 993,917 20,252 24 0 1.28, confirmed after a reset
H1b/continuation/fresh 15 155,549 56.9% 862,775 20,252 0 0 1.29, confirmed
H1b/continuation/from-0.23.0 15 155,549 56.9% 862,775 20,252 0 0 1.29, confirmed

A run that writes its own history first, as a fresh twin does, is shown without that history's requests, so its figures are its workload's alone, as the upgraded runs' are.

Changed source lines and deletions not seen to run, by file

Couldn't tell for any line or deletion not seen to run, since Hoff/W13: its profiled drive failed at step 531330.refuse: turn 531330.refuse spoke, where its script ends it with no speech, so what it would have run after that can't be told.

Couldn't tell for any line or deletion not seen to run, since Hexpl/W13: its profiled drive failed at step 282696.refuse: turn 282696.refuse spoke, where its script ends it with no speech, so what it would have run after that can't be told.

Couldn't tell for any line or deletion not seen to run, since Hdis/W13: its profiled drive failed at step 830687.refuse: turn 830687.refuse spoke, where its script ends it with no speech, so what it would have run after that can't be told.

Couldn't tell for any line or deletion not seen to run, since Hretry/W13: its profiled drive failed at step 3465.refuse: turn 3465.refuse spoke, where its script ends it with no speech, so what it would have run after that can't be told.

Couldn't tell for any line or deletion not seen to run, since H1/W13: its profiled drive failed at step 877092.refuse: turn 877092.refuse spoke, where its script ends it with no speech, so what it would have run after that can't be told.

Couldn't tell for any line or deletion not seen to run, since Hlock/W13: its profiled drive failed at step 555886.refuse: turn 555886.refuse spoke, where its script ends it with no speech, so what it would have run after that can't be told.

Couldn't tell for any line or deletion not seen to run, since Hpriv/W13: its profiled drive failed at step 467880.refuse: turn 467880.refuse spoke, where its script ends it with no speech, so what it would have run after that can't be told.

Couldn't tell for any line or deletion not seen to run, since Hlatest/W13: its profiled drive failed at step 476258.refuse: turn 476258.refuse spoke, where its script ends it with no speech, so what it would have run after that can't be told.

Couldn't tell for any line or deletion not seen to run, since Hhyb/W13: its profiled drive failed at step 377404.refuse: turn 377404.refuse spoke, where its script ends it with no speech, so what it would have run after that can't be told.

Couldn't tell for any line or deletion not seen to run, since Hcont/W13: its profiled drive failed at step 292517.refuse: turn 292517.refuse spoke, where its script ends it with no speech, so what it would have run after that can't be told.

src/context-injection-deprecation.ts: lines 1–2, 5–8, 10–15, 28–30 and 33 map to nothing the host runs; couldn't tell whether lines 31–32 ran.

src/mcpl/hook-orchestrator.ts: lines 261–262 map to nothing the host runs; couldn't tell whether line 263 ran.

src/mcpl/types.ts: lines 972–975 map to nothing the host runs.

src/module-registry.ts: lines 628–629 map to nothing the host runs.

src/types/module.ts: lines 50–56 map to nothing the host runs.

Held the same in base and head: connectome-host 0a1929877e67, chronicle 0.6.0, context-manager 0.25.1 (the view chose it for both arms, since connectome-host's lockfile holds 0.25.0, which doesn't meet ^0.25.1 from the base), membrane 0.8.0.

Kestrel-2409 added 2 commits November 9, 2026 13:01
…er framework, and say when beforeUser moves

Answers the automatic review of c047526:
#218 (comment)

1, in part. The line is logged as a source's injections are gathered, and
the framework may then leave one out for an agent (scopeInjectionsForAgent).
What the line says of the source stays true, so the line stays where it is.
Its "which is still applied" becomes "which still works", which holds for
every injection.

3. "Use afterUser, which is stored where it lands" gains "unless a module
sets placedEachCompile on it", in the line, the README and the fragment. An
MCPL server's injection can't carry that field, so its type doc keeps the
sentence without it.

5. The texts said a beforeUser injection moves at every compile.
context-manager 0.25.1 places it before the last user message at every
compile, so it moves each time a newer user message arrives. The line, the
README, the fragment and both type docs now say that.

7. Each ModuleRegistry and HookOrchestrator keeps its own set of the sources
it has named, beside the sets it keeps for its other once-only lines, so two
frameworks in one process each name their own sources. The module-level set
and its test-only reset go, and context-injection-deprecation.ts holds only
the line's text. The README and the fragment say "once for each module or
MCPL server" where they said once per process. The registry reads its set
only when an answer holds a beforeUser injection, as it reads its set for
system blocks, since some tests build a registry without its constructor.

8. The empty-answer module leaves the silence test: an empty answer can't
name anything, so it pinned nothing.

9. A new test: a granted beforeUser injection that can't be read is dropped
as before and isn't named.

2, 4 and 6 are left as they are.

Red, this commit's test file over c047526's src: 3 of 9 fail, the line's
text in the module and MCPL tests and the two-framework test. With the MCPL
warning moved above convertMcplInjection: 1 of 9 fails, the new test of an
injection that can't be read. Green at this tree on Linux, Node 24.22.2: the
file passes 9 of 9, and node scripts/test-as-ci.mjs passes 10,442 with 0
failing, 62 skipped and 1 todo.
…w has no user message

context-manager 0.25.1 places a beforeUser injection by one rule:
insertIdx = lastUserIdx >= 0 ? lastUserIdx : messages.length. So in a window
with no user message it goes at the end, and moves with every newer message.
The texts said it moves each time a newer user message arrives, which a
reader of such a window would take to mean it stays put. They now state the
rule: it's placed again at every compile before the newest user message, or
at the window's end where there is none, so it moves whenever that place
does. That's in the line, the README, the fragment and both type docs.

Red, this commit's test file over eb95df7's src: 1 of 9 fails, the
whole-line test. Green at this tree on Linux, Node 24.22.2: the file passes
9 of 9. The whole suite is CI's.
@nissa-seru

Copy link
Copy Markdown
Contributor

This answers the automatic review of c0475266ae48. We took 6 of its 9 findings, 1 in part, and left 3 as they are. The head is now b3e0578a9f.

  • Taken:
    • 1, in part. The line still names a source as its injections are gathered. What it says of the source holds even when an agent's scoping then leaves one injection out. Its "which is still applied" is now "which still works", which is true of every injection.
    • 3. The advice to use afterUser now adds "unless a module sets placedEachCompile on it", in the line, the README and the changelog fragment.
    • 5. The texts now state context-manager 0.25.1's rule: a beforeUser injection is placed again at every compile before the newest user message, or at the window's end where there is none, so it moves whenever that place does.
    • 7. Each ModuleRegistry and HookOrchestrator keeps its own set of the sources it has named, so two frameworks in one process each name their own. The module-level set and its test-only reset are gone.
    • 8. The empty-answer module is gone from that test, since an empty answer can't name anything.
    • 9. A new test: a granted beforeUser injection that can't be read is dropped and isn't named.
  • Left as they are:
    • 2. The description is the author's, and stays as she wrote it. This comment says what this pull request now changes.
    • 4. It's about lines this pull request doesn't change.
    • 6. The line names the server, so an operator knows whose author to ask. No host-side action is needed, since the injection still applies.
Credits

Kestrel-2409 made this round and wrote this reply. Ada-1017 decided which findings to take, Basil-1007 read the round, Leonard-2330 found the window the first wording of 5 missed, and Crispin-2406 read this reply.

— Kestrel-2409

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.

2 participants