fix: honor lightContext in spawned subagents (#62264) (thanks @theSamPadilla)

* Add lightContext support for spawned subagents

* Clarify and guard lightContext usage in sessions_spawn

* test: guard sessions_spawn lightContext acp misuse

* fix: honor lightContext in spawned subagents (#62264) (thanks @theSamPadilla)

---------

Co-authored-by: Jaz <jaz@bycrux.com>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
This commit is contained in:
Sam Padilla
2026-04-07 00:35:45 -05:00
committed by GitHub
parent 7240830ca4
commit f1b7dd6c0a
10 changed files with 103 additions and 0 deletions

View File

@@ -54,6 +54,7 @@ Docs: https://docs.openclaw.ai
- UI/light mode: target both root and nested WebKit scrollbar thumbs in the light theme so page-level and container scrollbars stay visible on light backgrounds. (#61753) Thanks @chziyue.
- Matrix/onboarding: add an invite auto-join setup step with explicit off warnings and strict stable-target validation so new Matrix accounts stop silently ignoring invited rooms and fresh DM-style invites unless operators opt in. (#62168) Thanks @gumadeiras.
- Telegram/doctor: keep top-level access-control fallback in place during multi-account normalization while still promoting legacy default auth into `accounts.default`, so existing named bots keep inherited allowlists without dropping the legacy default bot. (#62263) Thanks @obviyus.
- Agents/subagents: honor `sessions_spawn(lightContext: true)` for spawned subagent runs by preserving lightweight bootstrap context through the gateway and embedded runner instead of silently falling back to full workspace bootstrap injection. (#62264) Thanks @theSamPadilla.
## 2026.4.5

View File

@@ -473,6 +473,8 @@ export function runAgentAttempt(params: {
lane: params.opts.lane,
abortSignal: params.opts.abortSignal,
extraSystemPrompt: params.opts.extraSystemPrompt,
bootstrapContextMode: params.opts.bootstrapContextMode,
bootstrapContextRunKind: params.opts.bootstrapContextRunKind,
internalEvents: params.opts.internalEvents,
inputProvenance: params.opts.inputProvenance,
streamParams: params.opts.streamParams,

View File

@@ -85,6 +85,10 @@ export type AgentCommandOpts = {
lane?: string;
runId?: string;
extraSystemPrompt?: string;
/** Bootstrap workspace context injection mode for this run. */
bootstrapContextMode?: "full" | "lightweight";
/** Run kind hint for bootstrap context behavior. */
bootstrapContextRunKind?: "default" | "heartbeat" | "cron";
internalEvents?: AgentInternalEvent[];
inputProvenance?: InputProvenance;
/** Per-call stream param overrides (best-effort). */

View File

@@ -666,6 +666,8 @@ export async function runEmbeddedPiAgent(
ownerNumbers: params.ownerNumbers,
enforceFinalTag: params.enforceFinalTag,
silentExpected: params.silentExpected,
bootstrapContextMode: params.bootstrapContextMode,
bootstrapContextRunKind: params.bootstrapContextRunKind,
bootstrapPromptWarningSignaturesSeen,
bootstrapPromptWarningSignature:
bootstrapPromptWarningSignaturesSeen[bootstrapPromptWarningSignaturesSeen.length - 1],

View File

@@ -44,6 +44,7 @@ import {
updateSessionStore,
isAdminOnlyMethod,
} from "./subagent-spawn.runtime.js";
import type { BootstrapContextMode } from "./bootstrap-files.js";
import { readStringParam } from "./tools/common.js";
export const SUBAGENT_SPAWN_MODES = ["run", "session"] as const;
@@ -80,6 +81,7 @@ export type SpawnSubagentParams = {
mode?: SpawnSubagentMode;
cleanup?: "delete" | "keep";
sandbox?: SpawnSubagentSandboxMode;
lightContext?: boolean;
expectsCompletionMessage?: boolean;
attachments?: Array<{
name: string;
@@ -672,6 +674,10 @@ export async function spawnSubagentDirect(
childSystemPrompt = `${childSystemPrompt}\n\n${materializedAttachments.systemPromptSuffix}`;
}
const bootstrapContextMode: BootstrapContextMode | undefined = params.lightContext
? "lightweight"
: undefined;
const childTaskMessage = [
`[Subagent Context] You are running as a subagent (depth ${childDepth}/${maxSpawnDepth}). Results auto-announce to your requester; do not busy-poll for status.`,
spawnMode === "session"
@@ -742,6 +748,8 @@ export async function spawnSubagentDirect(
thinking: thinkingOverride,
timeout: runTimeoutSeconds,
label: label || undefined,
bootstrapContextMode,
bootstrapContextRunKind: "default",
...publicSpawnedMetadata,
},
timeoutMs: 10_000,

View File

@@ -148,6 +148,31 @@ describe("spawnSubagentDirect workspace inheritance", () => {
});
});
it("passes lightweight bootstrap context flags for lightContext subagent spawns", async () => {
await spawnSubagentDirect(
{
task: "inspect workspace",
lightContext: true,
},
{
agentSessionKey: "agent:main:main",
agentChannel: "telegram",
agentAccountId: "123",
agentTo: "456",
workspaceDir: "/tmp/requester-workspace",
},
);
const agentCall = hoisted.callGatewayMock.mock.calls.find(
([request]) => (request as { method?: string }).method === "agent",
)?.[0] as { params?: Record<string, unknown> } | undefined;
expect(agentCall?.params).toMatchObject({
bootstrapContextMode: "lightweight",
bootstrapContextRunKind: "default",
});
});
it("deletes the provisional child session when a non-thread subagent start fails", async () => {
hoisted.callGatewayMock.mockImplementation(
async (request: {

View File

@@ -103,6 +103,42 @@ describe("sessions_spawn tool", () => {
);
});
it("passes lightContext through to subagent spawns", async () => {
const tool = createSessionsSpawnTool({
agentSessionKey: "agent:main:main",
});
await tool.execute("call-light", {
task: "summarize this",
lightContext: true,
});
expect(hoisted.spawnSubagentDirectMock).toHaveBeenCalledWith(
expect.objectContaining({
task: "summarize this",
lightContext: true,
}),
expect.any(Object),
);
});
it('rejects lightContext when runtime is not "subagent"', async () => {
const tool = createSessionsSpawnTool({
agentSessionKey: "agent:main:main",
});
await expect(
tool.execute("call-light-acp", {
runtime: "acp",
task: "summarize this",
lightContext: true,
}),
).rejects.toThrow("lightContext is only supported for runtime='subagent'.");
expect(hoisted.spawnSubagentDirectMock).not.toHaveBeenCalled();
expect(hoisted.spawnAcpDirectMock).not.toHaveBeenCalled();
});
it("routes to ACP runtime when runtime=acp", async () => {
const tool = createSessionsSpawnTool({
agentSessionKey: "agent:main:main",

View File

@@ -97,6 +97,12 @@ const SessionsSpawnToolSchema = Type.Object({
cleanup: optionalStringEnum(["delete", "keep"] as const),
sandbox: optionalStringEnum(SESSIONS_SPAWN_SANDBOX_MODES),
streamTo: optionalStringEnum(SESSIONS_SPAWN_ACP_STREAM_TARGETS),
lightContext: Type.Optional(
Type.Boolean({
description:
"When true, spawned subagent runs use lightweight bootstrap context. Only applies to runtime='subagent'.",
}),
),
// Inline attachments (snapshot-by-value).
// NOTE: Attachment contents are redacted from transcript persistence by sanitizeToolCallInputs.
@@ -161,6 +167,10 @@ export function createSessionsSpawnTool(
params.cleanup === "keep" || params.cleanup === "delete" ? params.cleanup : "keep";
const sandbox = params.sandbox === "require" ? "require" : "inherit";
const streamTo = params.streamTo === "parent" ? "parent" : undefined;
const lightContext = params.lightContext === true;
if (runtime === "acp" && lightContext) {
throw new Error("lightContext is only supported for runtime='subagent'.");
}
// Back-compat: older callers used timeoutSeconds for this tool.
const timeoutSecondsCandidate =
typeof params.runTimeoutSeconds === "number"
@@ -300,6 +310,7 @@ export function createSessionsSpawnTool(
mode,
cleanup,
sandbox,
lightContext,
expectsCompletionMessage: true,
attachments,
attachMountPath:

View File

@@ -102,6 +102,16 @@ export const AgentParamsSchema = Type.Object(
bestEffortDeliver: Type.Optional(Type.Boolean()),
lane: Type.Optional(Type.String()),
extraSystemPrompt: Type.Optional(Type.String()),
bootstrapContextMode: Type.Optional(
Type.Union([Type.Literal("full"), Type.Literal("lightweight")]),
),
bootstrapContextRunKind: Type.Optional(
Type.Union([
Type.Literal("default"),
Type.Literal("heartbeat"),
Type.Literal("cron"),
]),
),
internalEvents: Type.Optional(Type.Array(AgentInternalEventSchema)),
inputProvenance: Type.Optional(InputProvenanceSchema),
idempotencyKey: NonEmptyString,

View File

@@ -307,6 +307,8 @@ export const agentHandlers: GatewayRequestHandlers = {
groupSpace?: string;
lane?: string;
extraSystemPrompt?: string;
bootstrapContextMode?: "full" | "lightweight";
bootstrapContextRunKind?: "default" | "heartbeat" | "cron";
internalEvents?: AgentInternalEvent[];
idempotencyKey: string;
timeout?: number;
@@ -828,6 +830,8 @@ export const agentHandlers: GatewayRequestHandlers = {
runId,
lane: request.lane,
extraSystemPrompt: request.extraSystemPrompt,
bootstrapContextMode: request.bootstrapContextMode,
bootstrapContextRunKind: request.bootstrapContextRunKind,
internalEvents: request.internalEvents,
inputProvenance,
// Internal-only: allow workspace override for spawned subagent runs.