perf(agents): add contextInjection option to skip workspace re-injection#55221
perf(agents): add contextInjection option to skip workspace re-injection#55221cgdusek wants to merge 4 commits into
Conversation
Greptile SummaryThis PR adds an opt-in Key aspects of the implementation:
Confidence Score: 5/5Safe to merge — opt-in with a no-op default, compaction path verified, and tests cover the key edge cases. The change is strictly additive and opt-in (default 'always' preserves existing behavior). The compaction-safety claim is verified: compact.ts calls resolveBootstrapContextForRun independently. Session-file detection logic is correct. The only findings are two minor style issues that don't affect correctness or reliability. No files require special attention.
|
| Filename | Overview |
|---|---|
| src/agents/bootstrap-files.ts | Adds resolveContextInjectionMode helper that reads the new config setting with a safe 'always' default. Minor: missing blank line after the new function. |
| src/agents/pi-embedded-runner/run/attempt.ts | Core change: adds isContinuationTurn guard that skips bootstrap context resolution when contextInjection is 'continuation-skip' and the session file already exists. Logic is correct — compaction safety is delegated to compact.ts which calls resolveBootstrapContextForRun independently. |
| src/config/types.agent-defaults.ts | Exports AgentContextInjection type and adds contextInjection field to AgentDefaultsConfig. Minor inconsistency: the field uses an inline union rather than the AgentContextInjection type alias defined in the same file. |
| src/config/zod-schema.agent-defaults.ts | Adds optional contextInjection Zod union validator that accepts 'always' or 'continuation-skip', consistent with the TypeScript type definition. |
| src/agents/bootstrap-files.test.ts | 4 new unit tests for resolveContextInjectionMode covering undefined config, missing field, 'continuation-skip', and explicit 'always'. |
| src/config/zod-schema.agent-defaults.test.ts | 4 new schema validation tests: accepts 'always', 'continuation-skip', omitted value; rejects invalid string. |
| src/config/schema.base.generated.ts | Generated schema updated to include contextInjection as a const-union anyOf and adds the corresponding label/help entry. |
| src/config/schema.help.ts | Adds help text for agents.defaults.contextInjection describing both allowed values and their behavior. |
| src/config/schema.labels.ts | Adds the 'Context Injection' display label for agents.defaults.contextInjection. |
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/config/types.agent-defaults.ts
Line: 150
Comment:
**Use `AgentContextInjection` instead of inline union**
`AgentContextInjection` is exported from the same file at line 11 specifically to represent this type. The `contextInjection` field in `AgentDefaultsConfig` should reference it rather than repeating the inline union, so future additions to the allowed values only need to be made in one place.
```suggestion
contextInjection?: AgentContextInjection;
```
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: src/agents/bootstrap-files.ts
Line: 22-23
Comment:
**Missing blank line after function declaration**
A blank line is missing between the closing brace of `resolveContextInjectionMode` and the `BootstrapContextRunKind` type declaration, breaking the visual separation that exists between all other top-level declarations in this file.
```suggestion
}
export type BootstrapContextRunKind = "default" | "heartbeat" | "cron";
```
How can I resolve this? If you propose a fix, please make it concise.Reviews (1): Last reviewed commit: "perf(agents): add contextInjection optio..." | Re-trigger Greptile
| * - continuation-skip: skip injection on continuation turns when a session | ||
| * file already exists, re-inject only on first message and after compaction | ||
| */ | ||
| contextInjection?: "always" | "continuation-skip"; |
There was a problem hiding this comment.
Use
AgentContextInjection instead of inline union
AgentContextInjection is exported from the same file at line 11 specifically to represent this type. The contextInjection field in AgentDefaultsConfig should reference it rather than repeating the inline union, so future additions to the allowed values only need to be made in one place.
| contextInjection?: "always" | "continuation-skip"; | |
| contextInjection?: AgentContextInjection; |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/config/types.agent-defaults.ts
Line: 150
Comment:
**Use `AgentContextInjection` instead of inline union**
`AgentContextInjection` is exported from the same file at line 11 specifically to represent this type. The `contextInjection` field in `AgentDefaultsConfig` should reference it rather than repeating the inline union, so future additions to the allowed values only need to be made in one place.
```suggestion
contextInjection?: AgentContextInjection;
```
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Addressed in bad2a92.
| } | ||
| export type BootstrapContextRunKind = "default" | "heartbeat" | "cron"; |
There was a problem hiding this comment.
Missing blank line after function declaration
A blank line is missing between the closing brace of resolveContextInjectionMode and the BootstrapContextRunKind type declaration, breaking the visual separation that exists between all other top-level declarations in this file.
| } | |
| export type BootstrapContextRunKind = "default" | "heartbeat" | "cron"; | |
| } | |
| export type BootstrapContextRunKind = "default" | "heartbeat" | "cron"; |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/bootstrap-files.ts
Line: 22-23
Comment:
**Missing blank line after function declaration**
A blank line is missing between the closing brace of `resolveContextInjectionMode` and the `BootstrapContextRunKind` type declaration, breaking the visual separation that exists between all other top-level declarations in this file.
```suggestion
}
export type BootstrapContextRunKind = "default" | "heartbeat" | "cron";
```
How can I resolve this? If you propose a fix, please make it concise.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!
There was a problem hiding this comment.
Addressed in bad2a92.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d52d036704
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| contextInjectionMode === "continuation-skip" && | ||
| (await fs | ||
| .stat(params.sessionFile) | ||
| .then(() => true) | ||
| .catch(() => false)); |
There was a problem hiding this comment.
Detect continuation from transcript, not file existence
contextInjection: "continuation-skip" currently treats any existing sessionFile as a continuation turn, but this file can exist before a real prior turn is established (for example after prewarmSessionFile(...) on a failed first attempt, or during run-loop retries/failover). In those cases, this branch skips bootstrap/context injection even though there may be no assistant history yet; prepareSessionManagerForRun explicitly handles this "file exists but no assistant" state in src/agents/pi-embedded-runner/session-manager-init.ts. The result is that first successful retries can run without AGENTS/bootstrap instructions, which is a behavior regression introduced by this check.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Addressed in bad2a92 — replaced fs.stat with hasCompletedBootstrapTurn() that checks for assistant messages in the session JSONL.
🔒 Aisle Security AnalysisWe found 2 potential security issue(s) in this PR:
Vulnerabilities1. 🟡 Potential memory/time DoS via unbounded transcript read in hasCompletedBootstrapTurn()
Description
Vulnerable code: content = await fs.readFile(sessionFile, "utf-8");
...
for (const line of content.split(/\r?\n/)) {
...
}RecommendationAvoid loading the full transcript into memory. Safer options (pick one):
import fs from "node:fs";
import readline from "node:readline";
export async function hasCompletedBootstrapTurn(sessionFile: string): Promise<boolean> {
try {
const stream = fs.createReadStream(sessionFile, { encoding: "utf8" });
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
let hasAssistant = false;
let compactedAfterLastAssistant = false;
for await (const line of rl) {
if (!line) continue;
// same parsing logic...
}
return hasAssistant && !compactedAfterLastAssistant;
} catch {
return false;
}
}
These changes prevent large transcripts from causing unbounded RAM usage per attempt. 2. 🟡 Optional skipping of workspace bootstrap context on continuation turns can drop safety constraints from system prompt
DescriptionIn This is a security regression risk because the skipped files (e.g.,
While the code comment describes the continuation as “safe”, the safety property is only that a previous assistant message exists and no compaction occurred after it. It does not guarantee that the model will still follow operator-defined constraints if they are no longer present in the current system prompt, nor does it prevent prompt-injection in later turns from overriding missing guidance. Vulnerable behavior: const { bootstrapFiles: hookAdjustedBootstrapFiles, contextFiles } = isContinuationTurn
? {
bootstrapFiles: [] as WorkspaceBootstrapFile[],
contextFiles: [] as EmbeddedContextFile[],
}
: await resolveBootstrapContextForRun({ ... });If an installation relies on these bootstrap files as part of its safety policy, enabling RecommendationTreat bootstrap/context injection as part of the security boundary, not only as a token-optimization. Options:
Example: inject a minimal subset on continuation turns: const minimalRun = await resolveBootstrapContextForRun({
...,
contextMode: "lightweight",
runKind: params.bootstrapContextRunKind,
});
// or explicitly filter to required filenames before buildBootstrapContextFilesAlso update documentation to warn that Analyzed PR: #55221 at commit Last updated on: 2026-03-27T04:04:21Z |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3892c4b68f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const { bootstrapFiles: hookAdjustedBootstrapFiles, contextFiles } = isContinuationTurn | ||
| ? { | ||
| bootstrapFiles: [] as WorkspaceBootstrapFile[], | ||
| contextFiles: [] as EmbeddedContextFile[], |
There was a problem hiding this comment.
Preserve HEARTBEAT.md injection on heartbeat continuation runs
When agents.defaults.contextInjection is set to "continuation-skip", this branch bypasses resolveBootstrapContextForRun(...) and forces empty bootstrap/context files whenever a session file exists. That also affects heartbeat turns, even though heartbeat code can request lightweight context (src/infra/heartbeat-runner.ts) where applyContextModeFilter is designed to keep HEARTBEAT.md (src/agents/bootstrap-files.ts). As a result, heartbeat continuations lose their checklist context after the first persisted turn, which can change heartbeat decisions/output in production automation flows.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Addressed in bad2a92 — heartbeat runs are now explicitly excluded from continuation-skip, preserving the HEARTBEAT.md lightweight filtering path.
|
Thanks for the analysis @aisle-research-bot. Addressing the security concern:
The tradeoff (token savings vs. reduced system prompt surface on continuation turns) is intentional and opt-in. The existing Addressed Greptile's style nits in 769d5a0. |
|
This is a useful optimization target, but I do not think it is merge-ready as written. Two behavioral regressions stand out:
I would want the continuation-skip gate tightened to real transcript state with at least one assistant turn, heartbeat lightweight semantics preserved, and run-level tests added for:
Also worth being explicit in the PR description: this can help workspace-bootstrap overhead, but it does not address the broader token-burn complaints from Mission Control heartbeats, repeated skill prompt injection, or long-session history growth. |
|
I think the underlying issue in #9157 is real and clear, but this specific fix is still using the wrong signal. The new guard in Two concrete cases break:
So an existing session file does not necessarily mean the session has already had a real assistant turn with bootstrap context available. With
The PR description says compaction is safe because After successful compaction, the main run loop retries by calling
That retried attempt rebuilds the system prompt from scratch, but So I don’t think I’d want regression coverage for at least:
|
769d5a0 to
bad2a92
Compare
|
Pushed @steipete's feedback (both comments)1. "continuation detected from sessionFile existence, not real prior-turn state" Fixed. Replaced 2. "heartbeat continuation under lightweight context loses HEARTBEAT.md" Fixed. The continuation-skip gate now explicitly excludes heartbeat runs ( 3. "post-compaction retry" Fixed. 4. Regression tests requested Added 8 tests for
Greptile style nitsBoth addressed in previous commit (
Codex inline commentsP1 "Detect continuation from transcript, not file existence" — same fix as steipete point 1 above. Aisle Security concernAddressed in previous comment — the feature is opt-in, has pre-existing precedent ( |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bad2a92f68
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const contextInjectionMode = resolveContextInjectionMode(params.config); | ||
| const isContinuationTurn = | ||
| contextInjectionMode === "continuation-skip" && | ||
| params.bootstrapContextRunKind !== "heartbeat" && | ||
| (await hasCompletedBootstrapTurn(params.sessionFile)); |
There was a problem hiding this comment.
Re-check continuation status after taking session lock
The continuation-skip decision is made from hasCompletedBootstrapTurn(params.sessionFile) before the session lock is acquired later in this function, so another process can compact or rewrite the same transcript in between. In that window, this attempt can still skip bootstrap/context on what is now the first post-compaction turn, which reintroduces missing-context behavior under concurrent runners. Re-evaluate continuation state after acquireSessionWriteLock(...) (or move this check under the lock) so the decision matches the transcript state actually used for the run.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Acknowledged. In practice, this race window is narrow and bounded:
-
Compaction is process-scoped — it happens within the same run loop (the
continuebranches inrun.ts), not cross-process. The session lock serializes writes within the current process. -
If a concurrent process does compact between the check and the lock, the worst case is skipping bootstrap on one turn — the next compaction event will re-inject full context via
compact.tsindependently. -
Moving the check under the lock would require restructuring the bootstrap resolution to happen after session manager init (line 704+), which is a larger refactor. The current position mirrors the existing
resolveBootstrapContextForRuncall site.
I think this is a valid concern for a follow-up hardening pass but not a blocking regression for an opt-in feature. The cross-process concurrent compaction scenario is an existing design limitation that affects other session state decisions too.
| content = await fs.readFile(sessionFile, "utf-8"); | ||
| } catch { |
There was a problem hiding this comment.
Stream session scan instead of reading full transcript
hasCompletedBootstrapTurn loads the entire session JSONL into memory with readFile before scanning, which adds large allocations on every continuation-skip turn. Since session files can grow substantially in long-lived conversations, this can cause avoidable latency and memory pressure in the exact workload this optimization targets. A streaming line reader (or precomputed session metadata) would keep this check O(1) memory.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Good call. For v1, the full-file read with fast-path string checks (line.includes('"assistant"')) before JSON parsing keeps the overhead reasonable — JSON.parse is only called for lines that match the fast check, which is a small subset.
For large sessions (100+ turns), a streaming line reader or precomputed session metadata would be better. I'll note this as a follow-up optimization. The fast-path checks mean the current implementation is O(n) time but mostly avoids allocation pressure from JSON parsing.
bad2a92 to
3ee8918
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3ee891815c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const contextInjectionMode = resolveContextInjectionMode(params.config); | ||
| const isContinuationTurn = | ||
| contextInjectionMode === "continuation-skip" && | ||
| params.bootstrapContextRunKind !== "heartbeat" && | ||
| (await hasCompletedBootstrapTurn(params.sessionFile)); |
There was a problem hiding this comment.
Re-evaluate continuation skip after acquiring session lock
isContinuationTurn is decided before acquireSessionWriteLock, so the decision can become stale while this attempt waits for the lock. Fresh evidence: same-session concurrency is expected because active runs can still take the run-now path (src/auto-reply/reply/queue-policy.ts:13-20, wired in src/auto-reply/reply/agent-runner.ts:213-256), which allows another attempt to append a compaction entry first. In that case this turn still skips bootstrap/context even though it is now the first post-compaction turn, reintroducing the missing-context behavior this feature was trying to avoid.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Same concern as the earlier thread — acknowledged there. The run-now path is a good additional data point.
To summarize: this is a known limitation of the current placement. The continuation check runs before the session lock, so a concurrent compaction between check and lock could cause one turn to skip bootstrap incorrectly. The fallback is that the next compaction event will re-inject full context, bounding the exposure to at most one turn.
Moving the check under the lock requires restructuring bootstrap resolution into the inner try block (after prepareSessionManagerForRun), which I think is the right follow-up but too invasive for this PR. Happy to do it as a follow-up if maintainers want it before merge.
…ion (openclaw#9157) Add an opt-in agents.defaults.contextInjection config setting that skips redundant workspace bootstrap file injection on continuation turns. When set to "continuation-skip", bootstrap files (AGENTS.md, SOUL.md, etc.) are only injected on the first message and after compaction, saving ~93% of workspace context tokens on long sessions. Default is "always" (current behavior) to avoid surprising changes on upgrade. Compaction path in compact.ts independently resolves full context, so workspace files are always re-injected after compaction. Closes openclaw#9157
…eat filtering Address review feedback from steipete, Codex, and Greptile: 1. Replace fs.stat(sessionFile) with hasCompletedBootstrapTurn() that reads the session JSONL and checks for a completed assistant turn. This prevents false positives from pre-created/repaired/empty session files that exist before the first real assistant turn. 2. Detect post-compaction retries: if the last significant entry after the final assistant message is a compaction entry, force re-injection since compact.ts rewrites the transcript and the retry must rebuild context. 3. Never skip bootstrap on heartbeat runs, preserving the lightweight context filtering path that keeps HEARTBEAT.md via applyContextModeFilter. 4. Add 8 regression tests for hasCompletedBootstrapTurn covering: - nonexistent session file - empty session file - header-only (pre-created) session - user-only messages (no assistant turn) - completed assistant turn (safe to skip) - post-compaction retry (must re-inject) - normal continuation after compaction+assistant - malformed JSON lines Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
3ee8918 to
9adc03a
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9adc03aac7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| for (const line of content.split(/\r?\n/)) { | ||
| if (!line) { | ||
| continue; | ||
| } | ||
| // Fast string checks before parsing JSON to keep large-session overhead low. | ||
| if (line.includes('"assistant"') && line.includes('"message"')) { |
There was a problem hiding this comment.
Compute continuation state from active branch only
hasCompletedBootstrapTurn scans every JSONL line in file order and infers continuation from the last seen assistant/compaction pair, but it never follows the session’s active leaf/branch. Session files can contain sibling branches (the truncation path explicitly preserves them in src/agents/pi-embedded-runner/session-truncation.ts), so an assistant message on another branch can make this return true and cause continuation-skip to drop bootstrap context on the current branch’s first post-compaction turn. Derive this check from the active branch (for example via SessionManager.getBranch() under the session lock) to avoid cross-branch false positives.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Valid point. The raw JSONL scan doesn't distinguish between branches — an assistant message on a sibling branch could cause a false positive.
In practice, this is a narrow edge case: sibling branches are created during truncation retry paths, and the linear scan's compaction-after-assistant check provides some protection (compaction entries in the active branch reset the state). But it's not airtight.
The proper fix (using `SessionManager.getBranch()` to scan only the active branch) requires the same restructuring as the lock-timing concern: moving the continuation check after `SessionManager.open()` at line 704. I've noted both as a follow-up refactor — same change addresses both issues.
For v1, the false-positive risk is bounded: if we incorrectly skip bootstrap on a branched session, the next compaction re-injects full context. And this only affects sessions with `contextInjection: "continuation-skip"` enabled.
|
Re: updated Aisle analysis (2 issues): 1. Unbounded transcript read (CWE-400) — Acknowledged. Same concern as Codex P2 (streaming). The current implementation uses fast-path string checks before JSON parsing to minimize overhead. A streaming reader is a valid follow-up optimization for very large sessions but not a blocking concern for an opt-in feature that only runs when 2. Bootstrap context dropping — Same concern from the first Aisle analysis, already addressed in my earlier comment. Opt-in, pre-existing precedent ( No code changes needed — both are acknowledged limitations documented in the PR and tracked for follow-up. |
|
closing this one out. this was a real perf direction, but it has already landed differently on if there is still a gap after that landed version, the right move is a focused follow-up against current |
Summary
Adds an opt-in
agents.defaults.contextInjectionconfig setting that skips redundant workspace bootstrap file injection on continuation message turns, addressing the ~93% token waste reported in #9157."always"(default): current behavior — workspace files injected every turn"continuation-skip": skip injection when the session file already exists (continuation turns); always re-inject on first message and after compactionWhy existing PRs are not acceptable
Five community PRs attempted this fix (#46813, #44220, #52916, #28072, #40372) — all were either closed or have blocking issues:
fs.statcallHow this fix differs
compact.tscallsresolveBootstrapContextForRun()independently without the continuation-skip guard, so workspace files are always re-injected after compactionresolveContextInjectionModehelper, 4 for Zod schema validationfs.statbefore bootstrap resolution, not duplicated with the existinghadSessionFile(which serves a different purpose)"always"to avoid surprising behavior changes on upgradeUsage
Closes #9157
Test plan
pnpm buildpasses (noINEFFECTIVE_DYNAMIC_IMPORTwarnings)pnpm checkpasses (0 errors, 0 warnings)pnpm test -- src/agents/bootstrap-files.test.ts src/config/zod-schema.agent-defaults.test.ts— 14/14 passpnpm config:docs:gen/config:docs:check— in synccontextInjection: continuation-skip, verify first message includes workspace files, second message does not, post-compaction message re-includes them🤖 Generated with Claude Code