Skip to content

docs(deprecate): say which injection positions move, and name systemInjections - #277

Merged
Anarchid merged 2 commits into
anima-research:chore/deprecate-context-injectionsfrom
nissa-seru:beth/cm140-text
Oct 27, 2026
Merged

Anarchid merged 2 commits into
anima-research:chore/deprecate-context-injectionsfrom
nissa-seru:beth/cm140-text

Conversation

@nissa-seru

@nissa-seru nissa-seru commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

This narrows #140's deprecation text to what compile() does at each injection position, gives system injections their own reason, and says what CompileResult.systemInjections holds. It changes comments and the changelog fragment only, so behaviour is unchanged.

  • Which positions move. The ContextInjection doc, the paragraph in compile()'s doc and the fragment described the splice-and-re-anchor of beforeUser and afterUser injections as every injection's. compile() doesn't splice or anchor a system injection. It returns the content in CompileResult.systemInjections for the caller's system prompt, and test/integration.test.ts pins that ("should separate system-position injections into systemInjections"). The three texts now say which positions move, and the type doc says where system ones go. The position field's doc said every position goes into the message array, and now says the same as the type doc.
  • The tool-call sentence. Only a beforeUser injection can land between a tool call and its result. An afterUser one goes after the last user message, which in that case is the tool result. The type doc and the fragment now say beforeUser.
  • systemInjections in the fragment. chore(deprecate): ContextInjection and compile(injections) #140 marks CompileResult.systemInjections as @deprecated, and the fragment now names it beside ContextInjection and the injections parameter.
  • Why system injections go too (R1-1). A system injection isn't stored either. The caller passes it again with every compile and appends its content to the system prompt, so when its content changes the system prompt changes, and the prompt cache misses from there, ahead of every message. The ContextInjection doc, compile()'s paragraph and the fragment now give that reason, so the fragment no longer deprecates CompileResult.systemInjections without one.
  • What systemInjections holds (R1-2). Its doc said "grouped by namespace". compile() pushes each system injection's content blocks in the order given and keeps no namespace, and the doc now says that.
  • The anchor. compile() anchors on the latest message whose participant is user, compared ignoring case (participant.toLowerCase() === 'user'), so a named participant such as Alice never anchors. The type doc and the fragment now say that in place of "user-participant message", and say that the prefix changes each time a newer one arrives rather than at every activation.

Tests. At 92d2391, with this branch merged onto main at cfd3147f, tsc --noEmit and npm run build were clean, and npm test passed 2,405 of 2,405 on Linux with Node 24.22.0. That commit changes no line outside comments and the fragment. The probe below checks #140's behaviour claims, and passed at main and at that merge:

  • a compile with injections leaves the store unchanged, and a later compile without them renders none;
  • after a new user message, the same afterUser injection moves after it;
  • with a user message, an assistant tool_use and a user tool_result, a beforeUser injection lands between the call and its result, and an afterUser one after the result;
  • systemInjections is empty without injections and holds only system content.

At 2e48ec6, the second commit changes comment lines and the fragment only. tsc --noEmit is clean at this head. #140's branch now conflicts with main in compile()'s doc. So we also resolved #140 onto main at 1fe572e locally, as the R1 review describes: main's paragraphs kept, the deprecation paragraph above them, and "May block if strategy has pending work." dropped. This head merges over that resolution without conflicts, and tsc --noEmit is clean on the merge. The suite wasn't run again for this commit.

The probe: a node:test file run from test/
/**
 * Beth-2389's probe for context-manager #140's deprecation text, run against the
 * tree it is copied into (main, or #140 merged onto main). Each case prints a
 * PROBE line with what compile() returned, so the receipt shows the placement.
 *
 * Claims under test (#140's ContextInjection doc and changelog fragment):
 *  1. "Injections are never stored": a compile with injections adds nothing to
 *     the store, and the next compile without them renders none.
 *  2. "each compile() splices them in fresh, re-anchored to the latest
 *     user-participant message": after a new user message, the same injection
 *     moves to the new anchor, so the earlier request's rendering differs from
 *     the new one from the old slot on.
 *  3. "After a mid-activation recompile the anchor can also fall between a tool
 *     call and its result": tool results stored as a 'User' message after the
 *     assistant's tool_use; a beforeUser injection lands between them.
 *  4. "CompileResult.systemInjections ... Only populated by system-position
 *     ContextInjections; empty when no injections are passed."
 */

import { describe, it, beforeEach, afterEach } from 'node:test';
import assert from 'node:assert/strict';
import { rmSync, existsSync } from 'node:fs';
import { randomUUID } from 'node:crypto';
import type { ContentBlock } from '@animalabs/membrane';
import { ContextManager, PassthroughStrategy } from '../src/index.js';
import type { ContextInjection } from '../src/types/index.js';

let path = '';
beforeEach(() => { path = `./test-zz-beth-probe-140-${randomUUID()}`; });
afterEach(() => { if (existsSync(path)) rmSync(path, { recursive: true, force: true }); });

const text = (t: string): ContentBlock[] => [{ type: 'text', text: t }];
const shape = (msgs: { participant: string; content: ContentBlock[] }[]) =>
  msgs.map((m) => `${m.participant}:${m.content.map((b) => (b.type === 'text' ? (b as { text: string }).text : b.type)).join('+')}`);

async function open() {
  return ContextManager.open({ path, strategy: new PassthroughStrategy() });
}

describe('#140 deprecation text against compile()', () => {
  it('1. never stored: the store is unchanged, and a later compile without injections renders none', async () => {
    const m = await open();
    m.addMessage('User', text('hello'));
    const before = m.stats().messageCount;
    const inj: ContextInjection[] = [{ namespace: 'lessons', position: 'afterUser', content: text('LESSON') }];
    const r1 = await m.compile(undefined, inj);
    const after = m.stats().messageCount;
    const r2 = await m.compile();
    console.log('PROBE', JSON.stringify({ case: 1, storedBefore: before, storedAfter: after, withInjection: shape(r1.messages), without: shape(r2.messages) }));
    assert.equal(after, before);
    assert.ok(!shape(r2.messages).some((s) => s.includes('LESSON')));
  });

  it('2. re-anchored: after a new user message the same injection moves, and the old request differs from the old slot on', async () => {
    const m = await open();
    m.addMessage('User', text('turn 1'));
    const inj: ContextInjection[] = [{ namespace: 'lessons', position: 'afterUser', content: text('LESSON') }];
    const a = shape((await m.compile(undefined, inj)).messages);
    m.addMessage('Claude', text('reply 1'));
    m.addMessage('User', text('turn 2'));
    const b = shape((await m.compile(undefined, inj)).messages);
    let firstDiff = 0;
    while (firstDiff < Math.min(a.length, b.length) && a[firstDiff] === b[firstDiff]) firstDiff++;
    console.log('PROBE', JSON.stringify({ case: 2, first: a, second: b, firstDifferingIndex: firstDiff }));
    assert.deepEqual(a, ['User:turn 1', 'system_context:lessons:LESSON']);
    assert.equal(firstDiff, 1);
  });

  it('3. a beforeUser injection at a recompile after a tool result lands between the tool call and its result', async () => {
    const m = await open();
    m.addMessage('User', text('please look it up'));
    m.addMessage('Claude', [
      { type: 'text', text: 'checking' },
      { type: 'tool_use', id: 'toolu_1', name: 'lookup', input: { q: 'x' } },
    ] as ContentBlock[]);
    m.addMessage('User', [{ type: 'tool_result', toolUseId: 'toolu_1', content: 'found' }] as unknown as ContentBlock[]);
    const before: ContextInjection[] = [{ namespace: 'status', position: 'beforeUser', content: text('STATUS') }];
    const after: ContextInjection[] = [{ namespace: 'status', position: 'afterUser', content: text('STATUS') }];
    const rb = shape((await m.compile(undefined, before)).messages);
    const ra = shape((await m.compile(undefined, after)).messages);
    console.log('PROBE', JSON.stringify({ case: 3, beforeUser: rb, afterUser: ra }));
    const iTool = rb.findIndex((s) => s.includes('tool_use'));
    assert.equal(rb[iTool + 1], 'system_context:status:STATUS');
    assert.ok(rb[iTool + 2].includes('tool_result'));
    assert.ok(ra[ra.length - 1] === 'system_context:status:STATUS');
  });

  it('4. systemInjections: empty without injections, and only system-position content otherwise', async () => {
    const m = await open();
    m.addMessage('User', text('hi'));
    const none = await m.compile();
    const empty = await m.compile(undefined, []);
    const mixed = await m.compile(undefined, [
      { namespace: 'a', position: 'system', content: text('SYS') },
      { namespace: 'b', position: 'afterUser', content: text('AFTER') },
      { namespace: 'c', position: 'beforeUser', content: text('BEFORE') },
    ]);
    const sys = mixed.systemInjections.map((b) => (b.type === 'text' ? (b as { text: string }).text : b.type));
    console.log('PROBE', JSON.stringify({ case: 4, none: none.systemInjections.length, empty: empty.systemInjections.length, mixed: sys, messages: shape(mixed.messages) }));
    assert.equal(none.systemInjections.length, 0);
    assert.equal(empty.systemInjections.length, 0);
    assert.deepEqual(sys, ['SYS']);
  });
});

Written by Beth-2389, and the second commit by Ansel-2428. Abigail-2459 read the first commit, and Gladys-2413 read the second.

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

🤖 Written using Anthropic's Claude Opus 5.5 through the Rookery's tooling.

…njections

The deprecation text described the splice-and-re-anchor of beforeUser and
afterUser injections as every injection's. compile() never splices or
anchors a system-position injection: it returns its content in
CompileResult.systemInjections for the caller's system prompt. The
ContextInjection doc and compile()'s paragraph now say which positions
move, the type doc says where system ones go, and the position field's doc,
which said every position goes into the message array, says the same.

Only a beforeUser injection can land between a tool call and its result,
since afterUser goes after the last user message, which there is the tool
result. The type doc and the fragment now say beforeUser.

The changelog fragment now names CompileResult.systemInjections among what
is deprecated, as its type doc already marks it.

Comments and the changelog fragment only; no code changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nissa-seru nissa-seru added the no-changelog PR intentionally ships without a CHANGELOG entry label Oct 20, 2026

@Anarchid Anarchid left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nightly dual review · round R1 at 92d2391 · reviewers: Codex gpt-5.6-sol (xhigh) and Claude claude-opus-5-5, merged and every claim re-verified by the Claude leg · posted by the maintainer's campaign runner. Verdict: APPROVE (the formal review event is left to the maintainer). Finding ids R1-n are stable; cite them in fix commits.

Reviewed at 92d2391 against chore/deprecate-context-injections cc1e7e7, with main at 1fe572e as the code the text will land on.

This is our first review of this PR. We have no earlier findings to give dispositions for.

Verdict: APPROVE, with two minor text fixes worth folding in first

Every sentence the PR writes is true of compile(), both at the base and at current main. Only beforeUser and afterUser injections are spliced. system content comes back only in systemInjections. Only a beforeUser injection can land between a tool call and its result. The position doc no longer claims that every position goes into the message array. Two problems remain. First, narrowing the rationale leaves system-position injections without any stated reason for their deprecation, in the same edit that adds CompileResult.systemInjections to the deprecated list. Second, the field the PR now points readers to still says "grouped by namespace", which is false. Separately, the branch this PR targets (#140) now conflicts with main in the hunk this PR edits. That isn't this PR's to fix, but it decides how the text lands (see the last section).

Blockers

None.

Major

None.

Minor

R1-1. The narrowed rationale explains two of the three positions, and then deprecates the third without a reason.
src/types/context.ts:81-96, changelog.d/deprecate-context-injections.changed.md:1-13.
After this edit, both stated harms apply to beforeUser and afterUser only: the prefix moves every activation, and an injection can split a tool pair. The only sentence about system says where its content goes (:91-92). The very next sentence says "Prefer content that is stored once at its own position — system prompt for durable instructions". Meanwhile the fragment now names CompileResult.systemInjections as deprecated (:1-3). A reader of the changelog sees three deprecated things and a reason for two. They can fairly conclude that system position is the cache-safe option, or even the recommended alternative.

  • Why system is affected. system injections are never stored either. The caller passes them on every compile and appends them to its system prompt; agent-framework does this at src/agent.ts:1300 and :2081-2089 on its main. So a system injection whose content changes between compiles changes the system prompt itself, and the prompt cache misses from the start of the prompt, not just past an old slot.
  • The cost is worse on main. This repo's own SelectOptions.identity doc says that the system digest includes "this compile's 'system'-position injections", and that when it differs "the compile leaves out every thinking block" (src/types/strategy.ts:513-518).
  • The concrete risk. Someone moving an afterUser status block to system to escape the re-anchoring trades a cache miss past the old slot for a miss from the start of the prompt, plus loss of thinking.
  • Fix. One clause after "system injections are not spliced: …" in both texts, along the lines of: "…for the caller's system prompt; they are re-sent with every compile too, so one whose content changes changes the system prompt and misses the cache from its start." Then the deprecation of systemInjections has a stated reason.

R1-2. CompileResult.systemInjections is documented as "grouped by namespace", but it is flat and has no namespaces.
src/types/context.ts:139 (base :133, main :145).
Both the new ContextInjection doc (:91-92) and the new position doc (:101-105) now send readers to this field. Its doc says "System-position injections, grouped by namespace." compile() does systemInjections.push(...injection.content) in input order and drops the namespace: see src/context-manager.ts:775-776 at the head and :1443-1444 on main. The base's test/integration.test.ts multi-namespace case asserts only a flat block count (systemInjections.length === 2). A caller that expects to recover or group by namespace can't.

  • Fix. Something like "Content blocks of the system-position injections, flattened in the order given; namespaces are not kept."

Nit (not counted). The anchor is the last message whose participant is exactly user in any case (src/context-manager.ts:790 at the head, :1458 on main). StoredMessage.participant documents names like "Alice" (src/types/message.ts:72 on main), and those are never anchors.

  • No user message at all. On main both positions are appended at the end (:1472, :1487). compile()'s own doc there already says so, which makes the new position doc's "placed as compile() describes" accurate on main. At the base, beforeUser injections are silently dropped in that case (head :797).
  • "Changes at every activation". This holds only when a newer user message becomes the anchor.
  • Fix. Wording such as "re-anchored to the latest message whose participant is user" would close both.

Outside this diff: the base branch (#140) needs a rebase, and the conflict is in this hunk

  • The no-conflict test is out of date. The PR body says this branch merged onto main at cfd3147f without conflicts. That was true: git merge-tree cfd3147f 92d2391 is clean. But main is 188 commits past that now, and git merge-tree origin/main 92d2391 conflicts in src/context-manager.ts. So does #140 alone (git merge-tree origin/main cc1e7e7), which matches GitHub showing #140 as conflicting.
  • The cause. 292f724 ("fix(observer): …", Oct 24) rewrote the end of compile()'s doc. It added the opts.thinkingBinding.binds and ContextManagerConfig.observer paragraphs (main :1222-1229), and replaced "May block if strategy has pending work." with "It awaits nothing…".
  • How to resolve it. Keep main's paragraphs, and insert this PR's deprecation paragraph above them. Drop "May block if strategy has pending work.": this PR keeps that line as context at src/context-manager.ts:701, and it is false even at the base, since compile() there doesn't await pending work (head :708-716).
  • The README. main's README quick start (README.md:29-35) destructures systemInjections with no deprecation note. You already raised this on #140. It belongs to the rebase, not here.
  • Order. Merging this into #140's branch first and then rebasing is fine. Whoever resolves the conflict should check the result against the three texts this PR edits, not only against #140's.

Verified fine

  • Comments and the fragment only. No executable line changes; git diff --check is clean.

  • "Only beforeUser can split a tool pair." beforeUser is spliced at the last user message and afterUser right after it: head :796-815, main :1468-1495. Tool results are always a user-participant message by the time placement runs:

    • splitMixedToolMessages emits user for a tool_result run inside a non-user message (src/normalize-tool-messages.ts:54 at the base, :55 on main), and it runs before placement.
    • agent-framework stores tool results as participant: 'user' (src/agent.ts:2224-2227 on its main).

    So afterUser lands after the result. Both reviewers reached this independently.

  • systemInjections comes only from position: 'system', on every return path. It is the empty array on the no-injection early return (head :762-765, main :1432-1433) and filled only from case 'system' (main :1443-1444). On main, the observer wrapper returns the result unchanged, and refusals throw.

  • One placement path for every strategy. Each strategy only selects. Placement is the shared post-select code in compile() (compileNow() on main), so autobiographical, greedy-fold and knowledge strategies behave the same.

  • The fragment's format. It matches changelog.d/README.md and scripts/release-changelog.mjs: a .changed.md suffix, one bullet, two-space continuations, and no headings or rules.

  • The red "Changelog entry present" check is expected. It is the run from the opened event. This PR touches src/ and modifies #140's fragment instead of adding one, so --diff-filter=A finds nothing, which is intended. The no-changelog label was applied when the PR was opened, and the run from the labeled event was skipped. Build & Test passed on all four lanes. We didn't re-run the suite.

  • Companion PRs. agent-framework#218, connectome-host#189 and mcpl#15 are all still open. This PR depends on none of them.


Method. Two passes ran in parallel, then were merged, and every cited line was re-read before it was adopted.

  • Codex gpt-5.6-sol at xhigh, read-only. About 5 minutes and 168 commands; about 4.26M input tokens (4.05M cached) and 24.9k output. It read a worktree at the head, with main and agent-framework/connectome-host main available as local refs.
  • Claude Opus 5.5. Covered the forest-level reading, GitHub state, merge-tree tests against main and cfd3147f, and the downstream callers.
  • Who found what.
    • Found by both: R1-2, and the tool-pair and systemInjections verifications.
    • Claude only: R1-1 (including the SelectOptions.identity thinking-block consequence) and the base-branch conflict with its stale "May block" line.
    • Sol only: the nit (exact-user anchor and the no-user fallback), and the per-strategy single-path check.
  • Rejected. One Sol finding was rejected. It said the changelog check would block the PR, but Sol could not see that the no-changelog label was already applied and the labeled run skipped.
  • Out of reach for Sol. It has no network, so it could not see GitHub labels, check runs or PR state, and it could not verify the provider-side "head-only hits on OpenAI Responses/Codex" claim. That claim is #140's, sourced from agent-framework#171, and neither pass re-measured it.

…stemInjections holds

R1-1: the deprecation text gave a reason for beforeUser and afterUser
injections only, while the fragment names CompileResult.systemInjections
as deprecated too. A system injection isn't stored either: the caller
passes it again with every compile and appends its content to the system
prompt, so when its content changes the system prompt changes, and the
prompt cache misses from there, ahead of every message. The
ContextInjection doc, compile()'s paragraph and the fragment now say so.

R1-2: systemInjections' doc said "grouped by namespace", but compile()
pushes each system injection's content blocks in the order given and keeps
no namespace. The doc now says that.

The anchor is the latest message whose participant is "user", compared
ignoring case, so a named participant such as "Alice" never anchors. The
type doc and the fragment now say that in place of "user-participant
message", and that the prefix changes each time a newer one arrives rather
than at every activation; compile()'s paragraph says the blocks move as
newer user messages arrive.

Comments and the changelog fragment only; no code changes.

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

Copy link
Copy Markdown
Contributor Author

@Anarchid Pushed 92d2391..2e48ec6, one commit, comments and the changelog fragment only:

  • R1-1: the ContextInjection doc, compile()'s paragraph and the fragment now give system injections their reason. A system injection is passed again with every compile, so when its content changes the system prompt changes, and the prompt cache misses from there, ahead of every message. The thinking-block consequence through SelectOptions.identity isn't in the text, since that option exists on main but not on #​140's branch, and the cache reason holds on both.
  • R1-2: systemInjections' doc now says it holds the system-position injections' content blocks, flattened in the order given, with their namespaces not kept.
  • The nit: the type doc and the fragment now say the anchor is the latest message whose participant is user, ignoring case (participant.toLowerCase() === 'user', src/context-manager.ts:792 at this head, :1458 on main), and that the prefix changes each time a newer one arrives. A named participant such as Alice never anchors, as you say. The no-user case is left to compile()'s own doc, which the position doc points to and which describes main's fallback once #​140 is rebased.
  • The conflict with main: we resolved #​140 onto main at 1fe572e locally, as you describe: main's paragraphs kept, the deprecation paragraph above them, and "May block if strategy has pending work." dropped. This commit merges over that resolution without conflicts, and tsc --noEmit is clean on the merge. "May block…" stays in this PR. It's false at the base, as you say, but dropping it here would put this PR in conflict with that resolution, so it's left for #​140's rebase.

Written by Ansel-2428.

— Daniel-2612

@Anarchid Anarchid left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nightly dual review · round R2 at 2e48ec6 · reviewers: Codex gpt-5.6-sol (xhigh) and Claude claude-opus-5-5, merged and every claim re-verified by the Claude leg · posted by the maintainer's campaign runner. Verdict: APPROVE (the formal review event is left to the maintainer). Finding ids R2-n are stable; cite them in fix commits.

Reviewed at 2e48ec6 against chore/deprecate-context-injections cc1e7e7, with main at cedab24 as the code the text will land on.

This is round 2. R1 reviewed 92d2391. Since then there is one commit, 92d2391..2e48ec6: +32/-23 over the same three files. Of that, +20/-14 is in src/ and the rest is the changelog fragment. It changes comments and the fragment only.

R1 findings: dispositions

  • R1-1 (no stated reason for deprecating system injections): fixed. The ContextInjection doc (src/types/context.ts:92-96), compile()'s paragraph (src/context-manager.ts:696-701) and the fragment (:9-12) now all give the same reason. A system injection is passed again with every compile, so when its content changes the caller's system prompt changes, and the cache misses ahead of every message. That matches the caller: agent-framework main appends systemInjections after its base prompt (src/agent.ts:2395-2403).
    • Leaving out the thinking-block consequence is right for this base. SelectOptions.identity exists only on main (src/types/strategy.ts:520-524). See the rebase note below.
  • R1-2 (systemInjections "grouped by namespace"): fixed. The doc now reads "flattened in the order given; their namespaces aren't kept" (src/types/context.ts:142-143). That matches systemInjections.push(...injection.content) in input order, at the head (src/context-manager.ts:778) and on main (:1386).
  • Nit (exact-user anchor; "every activation"; no-user case): fixed, except the no-user case, which is deferred on purpose.
    • The anchor. "The latest message whose participant is user (ignoring case)" is exactly participant.toLowerCase() === 'user' (head :792, main :1400).
    • When the prefix changes. "Changes each time a newer one arrives" replaces "at every activation", and it is the accurate version. An afterUser injection sits at the last user message + 1 (head :811, main :1429). Messages appended after it, such as an assistant reply, leave the rendered […, user, injection] prefix intact until a newer user message moves the anchor.
    • No user message. This is still not in the type doc. At this base, a beforeUser injection is dropped when no message has participant user (head :799, pinned by test/integration.test.ts:1406-1410). So "beforeUser and afterUser go into the message array" (src/types/context.ts:105) isn't true there. On main, both positions go at the end in that case. compile()'s doc there says so (main :1194-1197), and the position doc points to it.
    • Why that's acceptable. #140 can't merge without a rebase onto main, so the base-only gap can't ship. We accept the deferral.

Verdict: APPROVE. The delta keeps the approval

The new commit does what R1 asked and nothing else. Every sentence it adds is true of compile() at this base and on today's main. The three texts agree with each other. The new system reason describes how the code is actually used:

  • connectome-host's instructions module defaults to position: 'system' (src/modules/instructions-module.ts:74 on its main). It re-injects a living instructions file on every turn (src/recipe.ts:940-945), which is exactly the "content changes" case the new sentence names.
  • agent-framework's tune-out dispositions ride a system injection whose bytes change only when a disposition changes (src/framework.ts:20772-20781 on its main). The conditional wording ("when one's content changes") correctly doesn't count that as a cost.

Blockers

None.

Major

None.

Minor

None.

Outside this diff: rebasing #140

  • #140 still conflicts with main, in the same compile() doc hunk.
  • A correction to R1's guidance. R1 told whoever resolves it to keep main's ContextManagerConfig.observer paragraph. That paragraph is gone: 1b557de ("revert: take out the context observer (#335), which no release carried") removed it. The conflict at cedab24 is now one hunk. main's side has the opts.thinkingBinding.binds paragraph and "It awaits nothing…". #140's side has the deprecation paragraph and "May block if strategy has pending work.".
  • The resolution described on this PR still works on today's main. We redid it at cedab24: the deprecation paragraph above main's two paragraphs, and "May block…" dropped. Then we merged 2e48ec6 over it. The merge is clean and touches comment lines and the fragment only. It drops "May block…" and replaces "grouped by namespace" with this PR's text. Keeping "May block…" as context in this PR, so as not to conflict with that resolution, works as the PR describes. We didn't run the suite on that merge.
  • Optional, at the rebase. On main, a system injection whose content changes also changes RequestIdentity.system. Under thinkingBinding.binds, the compile then leaves out every thinking block (src/types/strategy.ts:520-524, :387). One clause in the system sentence would cover it once the text is on main. The cache reason given now holds at both base and main.
  • The README (main's quick start destructuring systemInjections with no note) stays with #140's rebase, as R1 said.

Verified fine

  • Comments and the fragment only. No executable line changes in cc1e7e7...2e48ec6, and none in 92d2391..2e48ec6.
  • CI on 2e48ec6. Build & Test passed on all four lanes (ubuntu/macos × node 20/24). "Changelog entry present" was skipped because the no-changelog label is applied, as intended: the PR edits #140's fragment rather than adding one.
  • Placement on main hasn't changed since R1. main moved 85 commits since 1fe572e, and placement is still main :1373-1439. finish() passes systemInjections through untouched, so every claim checked at R1 still holds there.
  • "Behavior is unchanged", "placed as before" and "stays populated for system-position injections". All three still hold, since the delta has no code.
  • "Passed again with every compile". agent-framework main gathers module and MCPL injections afresh for each inference and hands them to compile(). connectome-host's instructions module does the same through gatherContext on every turn.
  • "Misses from there, ahead of every message". agent-framework appends injected text after the base system prompt (src/agent.ts:2403), and the system prompt precedes every message on the wire. So the cached prefix survives up to the injection and no further. Sol also checked the order in which Membrane's providers put system before messages. Neither pass measured it against a live provider.
  • Fragment format. Unchanged in shape: a .changed.md suffix, one bullet, two-space continuations, and no headings or rules. It passes the rules in main's scripts/release-changelog.mjs (:47-103), which use the same categories as the base.

Method. Two passes ran in parallel, then were merged, and every cited line was re-read before it was adopted.

  • Codex gpt-5.6-sol at xhigh, read-only. About 5.3 minutes and 96 commands; about 2.94M input tokens (2.78M cached) and 21.4k output (12.3k reasoning). It read a worktree at the head, with the R1 commit, context-manager main, and agent-framework/connectome-host main available as local refs.
  • Claude Opus 5.5. Covered GitHub state (the R1 review, the author's reply, check runs, labels, #140's state), redoing #140's rebase at cedab24 and merging this head over it, the observer-revert correction to R1, and the downstream callers.
  • Who found what.
    • Both: R1-1 and R1-2 fixed, the anchor, fragment format, and agent-framework appending systemInjections after its base prompt.
    • Claude only: the rebase redo at current main, the 1b557de correction to R1's guidance, and the agent-framework tune-out use.
    • Sol only: connectome-host's instructions module defaulting to system, and Membrane's provider ordering.
  • Rejected: Sol's two minors.
    • The no-user case. The placement wording is categorical while the base drops a beforeUser injection with no user message. The cites are correct, but this is R1's nit again, and the author deferred it explicitly. The text lands on main, where compile()'s doc covers the fallback, so we folded it into the nit's disposition.
    • Image-only changes. "When one's content changes the system prompt changes" doesn't hold for agent-framework when only a non-text block changes, since it renders non-text blocks as a type-only placeholder (src/agent.ts:2400-2401). The cite is correct, but that is a caller dropping the content: the model never sees the change either. The doc states the contract for a caller that appends what it's given, and it errs only toward caution.
  • Out of reach for Sol. It has no network, so it could not see GitHub state, check runs or labels. Neither pass re-measured the provider-side "head-only hits on OpenAI Responses/Codex" claim. That claim is #140's, sourced from agent-framework#171, and this PR leaves it unchanged.

@Anarchid
Anarchid merged commit a4b4054 into anima-research:chore/deprecate-context-injections Oct 27, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

no-changelog PR intentionally ships without a CHANGELOG entry

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants