Repository navigation
docs(deprecate): say which injection positions move, and name systemInjections - #277
Conversation
…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>
Anarchid
left a comment
There was a problem hiding this comment.
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
systemis affected.systeminjections are never stored either. The caller passes them on every compile and appends them to its system prompt; agent-framework does this atsrc/agent.ts:1300and:2081-2089on itsmain. So asysteminjection 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 ownSelectOptions.identitydoc says that thesystemdigest 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
afterUserstatus block tosystemto 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 "
systeminjections 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 ofsystemInjectionshas 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
usermessage at all. Onmainboth positions are appended at the end (:1472,:1487).compile()'s own doc there already says so, which makes the newpositiondoc's "placed ascompile()describes" accurate onmain. At the base,beforeUserinjections are silently dropped in that case (head:797). - "Changes at every activation". This holds only when a newer
usermessage 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
mainatcfd3147fwithout conflicts. That was true:git merge-tree cfd3147f 92d2391is clean. Butmainis 188 commits past that now, andgit merge-tree origin/main 92d2391conflicts insrc/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 theopts.thinkingBinding.bindsandContextManagerConfig.observerparagraphs (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 atsrc/context-manager.ts:701, and it is false even at the base, sincecompile()there doesn't await pending work (head:708-716). - The README.
main's README quick start (README.md:29-35) destructuressystemInjectionswith 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 --checkis clean. -
"Only
beforeUsercan split a tool pair."beforeUseris spliced at the lastusermessage andafterUserright after it: head:796-815,main:1468-1495. Tool results are always auser-participant message by the time placement runs:splitMixedToolMessagesemitsuserfor atool_resultrun inside a non-user message (src/normalize-tool-messages.ts:54at the base,:55onmain), and it runs before placement.- agent-framework stores tool results as
participant: 'user'(src/agent.ts:2224-2227on itsmain).
So
afterUserlands after the result. Both reviewers reached this independently. -
systemInjectionscomes only fromposition: '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 fromcase 'system'(main:1443-1444). Onmain, 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-
selectcode incompile()(compileNow()onmain), so autobiographical, greedy-fold and knowledge strategies behave the same. -
The fragment's format. It matches
changelog.d/README.mdandscripts/release-changelog.mjs: a.changed.mdsuffix, one bullet, two-space continuations, and no headings or rules. -
The red "Changelog entry present" check is expected. It is the run from the
openedevent. This PR touchessrc/and modifies #140's fragment instead of adding one, so--diff-filter=Afinds nothing, which is intended. Theno-changeloglabel was applied when the PR was opened, and the run from thelabeledevent 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-solat 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, withmainand agent-framework/connectome-hostmainavailable as local refs. - Claude Opus 5.5. Covered the forest-level reading, GitHub state, merge-tree tests against
mainandcfd3147f, and the downstream callers. - Who found what.
- Found by both: R1-2, and the tool-pair and
systemInjectionsverifications. - Claude only: R1-1 (including the
SelectOptions.identitythinking-block consequence) and the base-branch conflict with its stale "May block" line. - Sol only: the nit (exact-
useranchor and the no-user fallback), and the per-strategy single-path check.
- Found by both: R1-2, and the tool-pair and
- Rejected. One Sol finding was rejected. It said the changelog check would block the PR, but Sol could not see that the
no-changeloglabel 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>
|
@Anarchid Pushed
Written by Ansel-2428. — Daniel-2612 |
Anarchid
left a comment
There was a problem hiding this comment.
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
systeminjections): fixed. TheContextInjectiondoc (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. Asysteminjection 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-frameworkmainappendssystemInjectionsafter its base prompt (src/agent.ts:2395-2403).- Leaving out the thinking-block consequence is right for this base.
SelectOptions.identityexists only onmain(src/types/strategy.ts:520-524). See the rebase note below.
- Leaving out the thinking-block consequence is right for this base.
- 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 matchessystemInjections.push(...injection.content)in input order, at the head (src/context-manager.ts:778) and onmain(:1386). - Nit (exact-
useranchor; "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 exactlyparticipant.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
afterUserinjection sits at the lastusermessage + 1 (head:811,main:1429). Messages appended after it, such as an assistant reply, leave the rendered[…, user, injection]prefix intact until a newerusermessage moves the anchor. - No
usermessage. This is still not in the type doc. At this base, abeforeUserinjection is dropped when no message has participantuser(head:799, pinned bytest/integration.test.ts:1406-1410). So "beforeUserandafterUsergo into the message array" (src/types/context.ts:105) isn't true there. Onmain, both positions go at the end in that case.compile()'s doc there says so (main:1194-1197), and thepositiondoc 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.
- The anchor. "The latest message whose participant is
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:74on itsmain). 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
systeminjection whose bytes change only when a disposition changes (src/framework.ts:20772-20781on itsmain). 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 samecompile()doc hunk. - A correction to R1's guidance. R1 told whoever resolves it to keep
main'sContextManagerConfig.observerparagraph. That paragraph is gone: 1b557de ("revert: take out the context observer (#335), which no release carried") removed it. The conflict atcedab24is now one hunk.main's side has theopts.thinkingBinding.bindsparagraph 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 atcedab24: the deprecation paragraph abovemain's two paragraphs, and "May block…" dropped. Then we merged2e48ec6over 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, asysteminjection whose content changes also changesRequestIdentity.system. UnderthinkingBinding.binds, the compile then leaves out every thinking block (src/types/strategy.ts:520-524,:387). One clause in thesystemsentence would cover it once the text is onmain. The cache reason given now holds at both base andmain. - The README (
main's quick start destructuringsystemInjectionswith 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 in92d2391..2e48ec6. - CI on
2e48ec6. Build & Test passed on all four lanes (ubuntu/macos × node 20/24). "Changelog entry present" was skipped because theno-changeloglabel is applied, as intended: the PR edits #140's fragment rather than adding one. - Placement on
mainhasn't changed since R1.mainmoved 85 commits since1fe572e, and placement is stillmain:1373-1439.finish()passessystemInjectionsthrough 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
maingathers module and MCPL injections afresh for each inference and hands them tocompile(). connectome-host's instructions module does the same throughgatherContexton 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.mdsuffix, one bullet, two-space continuations, and no headings or rules. It passes the rules inmain'sscripts/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-solat 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-managermain, and agent-framework/connectome-hostmainavailable 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
cedab24and 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
systemInjectionsafter 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.
- Both: R1-1 and R1-2 fixed, the anchor, fragment format, and agent-framework appending
- Rejected: Sol's two minors.
- The no-user case. The placement wording is categorical while the base drops a
beforeUserinjection with nousermessage. The cites are correct, but this is R1's nit again, and the author deferred it explicitly. The text lands onmain, wherecompile()'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.
- The no-user case. The placement wording is categorical while the base drops a
- 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.
a4b4054
into
anima-research:chore/deprecate-context-injections
This narrows #140's deprecation text to what
compile()does at each injection position, givessysteminjections their own reason, and says whatCompileResult.systemInjectionsholds. It changes comments and the changelog fragment only, so behaviour is unchanged.ContextInjectiondoc, the paragraph incompile()'s doc and the fragment described the splice-and-re-anchor ofbeforeUserandafterUserinjections as every injection's.compile()doesn't splice or anchor asysteminjection. It returns the content inCompileResult.systemInjectionsfor the caller's system prompt, andtest/integration.test.tspins that ("should separate system-position injections into systemInjections"). The three texts now say which positions move, and the type doc says wheresystemones go. Thepositionfield's doc said every position goes into the message array, and now says the same as the type doc.beforeUserinjection can land between a tool call and its result. AnafterUserone goes after the last user message, which in that case is the tool result. The type doc and the fragment now saybeforeUser.systemInjectionsin the fragment. chore(deprecate): ContextInjection and compile(injections) #140 marksCompileResult.systemInjectionsas@deprecated, and the fragment now names it besideContextInjectionand theinjectionsparameter.systeminjections go too (R1-1). Asysteminjection 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. TheContextInjectiondoc,compile()'s paragraph and the fragment now give that reason, so the fragment no longer deprecatesCompileResult.systemInjectionswithout one.systemInjectionsholds (R1-2). Its doc said "grouped by namespace".compile()pushes eachsysteminjection's content blocks in the order given and keeps no namespace, and the doc now says that.compile()anchors on the latest message whose participant isuser, compared ignoring case (participant.toLowerCase() === 'user'), so a named participant such asAlicenever 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 ontomainatcfd3147f,tsc --noEmitandnpm run buildwere clean, andnpm testpassed 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 atmainand at that merge:afterUserinjection moves after it;tool_useand a usertool_result, abeforeUserinjection lands between the call and its result, and anafterUserone after the result;systemInjectionsis empty without injections and holds onlysystemcontent.At
2e48ec6, the second commit changes comment lines and the fragment only.tsc --noEmitis clean at this head. #140's branch now conflicts withmainincompile()'s doc. So we also resolved #140 ontomainat1fe572elocally, 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, andtsc --noEmitis clean on the merge. The suite wasn't run again for this commit.The probe: a
node:testfile run fromtest/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.