fix: add contextInjection never mode (#65006) (thanks @xDarkicex)

This commit is contained in:
xDarkicex
2026-04-11 12:32:07 -07:00
committed by Ayaan Zaidi
parent 18ffa81564
commit cc0992564b
9 changed files with 65 additions and 28 deletions

View File

@@ -69,6 +69,7 @@ Docs: https://docs.openclaw.ai
- Providers/DeepSeek: add DeepSeek V4 Flash and V4 Pro to the bundled catalog and make V4 Flash the onboarding default. Thanks @lsdsjy.
- CLI/Gateway: make `gateway status` start faster by skipping plugin loading on the read-only status path. (#71364) Thanks @andyylin.
- Plugins/compatibility: add a central plugin compatibility registry and docs for SDK/config/setup/runtime deprecation records, including dated migration metadata for legacy harness naming and other plugin-facing aliases. Thanks @vincentkoc.
- Agents/bootstrap: add `agents.defaults.contextInjection: "never"` to disable workspace bootstrap file injection for agents that fully own their prompt lifecycle. (#65006) Thanks @xDarkicex.
### Fixes

View File

@@ -339,6 +339,7 @@ export async function loadCompactHooksHarness(): Promise<{
vi.doMock("../bootstrap-files.js", () => ({
makeBootstrapWarn: vi.fn(() => () => {}),
resolveContextInjectionMode: vi.fn(() => "always"),
resolveBootstrapContextForRun: vi.fn(async () => ({ contextFiles: [] })),
}));

View File

@@ -37,7 +37,11 @@ import { normalizeMessageChannel } from "../../utils/message-channel.js";
import { isReasoningTagProvider } from "../../utils/provider-utils.js";
import { resolveOpenClawAgentDir } from "../agent-paths.js";
import { resolveSessionAgentIds } from "../agent-scope.js";
import { makeBootstrapWarn, resolveBootstrapContextForRun } from "../bootstrap-files.js";
import {
makeBootstrapWarn,
resolveBootstrapContextForRun,
resolveContextInjectionMode,
} from "../bootstrap-files.js";
import {
listChannelSupportedActions,
resolveChannelMessageToolCapabilities,
@@ -471,17 +475,19 @@ export async function compactEmbeddedPiSessionDirect(
const sessionLabel = params.sessionKey ?? params.sessionId;
const resolvedMessageProvider = params.messageChannel ?? params.messageProvider;
const { contextFiles } = await resolveBootstrapContextForRun({
workspaceDir: effectiveWorkspace,
config: params.config,
sessionKey: params.sessionKey,
sessionId: params.sessionId,
warn: makeBootstrapWarn({
sessionLabel,
workspaceDir: effectiveWorkspace,
warn: (message) => log.warn(message),
}),
});
const contextInjectionMode = resolveContextInjectionMode(params.config);
const { contextFiles } = contextInjectionMode === "never"
? { contextFiles: [] }
: await resolveBootstrapContextForRun({
workspaceDir: effectiveWorkspace,
config: params.config,
sessionKey: params.sessionKey,
sessionId: params.sessionId,
warn: makeBootstrapWarn({
sessionLabel,
warn: (message) => log.warn(message),
}),
});
// Apply contextTokens cap to model so pi-coding-agent's auto-compaction
// threshold uses the effective limit, not the native context window.
const runtimeModelWithContext = runtimeModel as ProviderRuntimeModel;

View File

@@ -13,23 +13,23 @@ export {
export type AttemptContextEngine = ContextEngine;
export type AttemptBootstrapContext = {
bootstrapFiles: unknown[];
contextFiles: unknown[];
export type AttemptBootstrapContext<TBootstrapFile = unknown, TContextFile = unknown> = {
bootstrapFiles: TBootstrapFile[];
contextFiles: TContextFile[];
};
export async function resolveAttemptBootstrapContext<
TContext extends AttemptBootstrapContext,
>(params: {
contextInjectionMode: "always" | "continuation-skip";
export async function resolveAttemptBootstrapContext<TBootstrapFile, TContextFile>(params: {
contextInjectionMode: "always" | "continuation-skip" | "never";
bootstrapContextMode?: string;
bootstrapContextRunKind?: string;
bootstrapMode?: BootstrapMode;
sessionFile: string;
hasCompletedBootstrapTurn: (sessionFile: string) => Promise<boolean>;
resolveBootstrapContextForRun: () => Promise<TContext>;
resolveBootstrapContextForRun: () => Promise<
AttemptBootstrapContext<TBootstrapFile, TContextFile>
>;
}): Promise<
TContext & {
AttemptBootstrapContext<TBootstrapFile, TContextFile> & {
isContinuationTurn: boolean;
shouldRecordCompletedBootstrapTurn: boolean;
}
@@ -39,14 +39,16 @@ export async function resolveAttemptBootstrapContext<
params.contextInjectionMode === "continuation-skip" &&
params.bootstrapContextRunKind !== "heartbeat" &&
(await params.hasCompletedBootstrapTurn(params.sessionFile));
const shouldSkipBootstrapInjection =
params.contextInjectionMode === "never" || isContinuationTurn;
const shouldRecordCompletedBootstrapTurn =
!isContinuationTurn &&
!shouldSkipBootstrapInjection &&
params.bootstrapContextMode !== "lightweight" &&
params.bootstrapContextRunKind !== "heartbeat" &&
params.bootstrapMode === "full";
const context = isContinuationTurn
? ({ bootstrapFiles: [], contextFiles: [] } as unknown as TContext)
const context = shouldSkipBootstrapInjection
? { bootstrapFiles: [], contextFiles: [] }
: await params.resolveBootstrapContextForRun();
return {

View File

@@ -12,7 +12,7 @@ import {
import { resetEmbeddedAttemptHarness } from "./attempt.spawn-workspace.test-support.js";
async function resolveBootstrapContext(params: {
contextInjectionMode?: "always" | "continuation-skip";
contextInjectionMode?: "always" | "continuation-skip" | "never";
bootstrapContextMode?: string;
bootstrapContextRunKind?: string;
bootstrapMode?: "full" | "limited" | "none";
@@ -77,6 +77,22 @@ describe("embedded attempt context injection", () => {
expect(resolver).toHaveBeenCalledTimes(1);
});
it("disables bootstrap injection without marking the turn as a continuation", async () => {
const { result, hasCompletedBootstrapTurn, resolveBootstrapContextForRun } =
await resolveBootstrapContext({
contextInjectionMode: "never",
bootstrapMode: "full",
completed: true,
});
expect(result.isContinuationTurn).toBe(false);
expect(result.shouldRecordCompletedBootstrapTurn).toBe(false);
expect(result.bootstrapFiles).toEqual([]);
expect(result.contextFiles).toEqual([]);
expect(hasCompletedBootstrapTurn).not.toHaveBeenCalled();
expect(resolveBootstrapContextForRun).not.toHaveBeenCalled();
});
it("does not let a stale completed marker suppress pending workspace bootstrap", async () => {
const resolver = vi.fn(async () => ({
bootstrapFiles: [{ name: "BOOTSTRAP.md" }],

View File

@@ -3447,6 +3447,10 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
type: "string",
const: "continuation-skip",
},
{
type: "string",
const: "never",
},
],
title: "Context Injection",
description:

View File

@@ -15,7 +15,7 @@ import type {
} from "./types.base.js";
import type { MemorySearchConfig } from "./types.tools.js";
export type AgentContextInjection = "always" | "continuation-skip";
export type AgentContextInjection = "always" | "continuation-skip" | "never";
export type EmbeddedPiExecutionContract = "default" | "strict-agentic";
export type Gpt5PromptOverlayConfig = {

View File

@@ -52,8 +52,13 @@ describe("agent defaults schema", () => {
expect(result.contextInjection).toBe("continuation-skip");
});
it("accepts contextInjection: never", () => {
const result = AgentDefaultsSchema.parse({ contextInjection: "never" })!;
expect(result.contextInjection).toBe("never");
});
it("rejects invalid contextInjection values", () => {
expect(() => AgentDefaultsSchema.parse({ contextInjection: "never" })).toThrow();
expect(() => AgentDefaultsSchema.parse({ contextInjection: "unknown" })).toThrow();
});
it("accepts embeddedPi.executionContract", () => {

View File

@@ -83,7 +83,9 @@ export const AgentDefaultsSchema = z
.strict()
.optional(),
skipBootstrap: z.boolean().optional(),
contextInjection: z.union([z.literal("always"), z.literal("continuation-skip")]).optional(),
contextInjection: z
.union([z.literal("always"), z.literal("continuation-skip"), z.literal("never")])
.optional(),
bootstrapMaxChars: z.number().int().positive().optional(),
bootstrapTotalMaxChars: z.number().int().positive().optional(),
experimental: z