fix(agents): declare visible-session constraints upfront in sessions_spawn (#111502)

This commit is contained in:
Peter Steinberger
2026-07-19 11:42:05 -07:00
committed by GitHub
parent 3e154b6761
commit 163a6c6e16
11 changed files with 146 additions and 60 deletions
+2 -1
View File
@@ -221,6 +221,7 @@ Per-agent override: `agents.list[].subagents.delegationMode`.
<ParamField path="mode" type='"run" | "session"' default="run">
If `thread: true` and `mode` is omitted, default becomes `session`. `mode: "session"` requires `thread: true`.
If thread binding is unavailable for the requester channel, use `mode: "run"` instead.
With `visible: true`, omit `mode`; visible sessions are persistent and do not support `mode: "run"`.
</ParamField>
<ParamField path="cleanup" type='"delete" | "keep"' default="keep">
`"delete"` archives the session immediately after announce (still keeps the transcript via rename).
@@ -251,7 +252,7 @@ their latest assistant turn back to the requester; external delivery stays with
the parent/requester agent.
</Warning>
With `visible: true`, `model`, `cwd`, and a same-agent `context: "fork"` are supported. A sandboxed target restricts `cwd` to that agent's workspace. Thread binding, `mode`, thinking overrides, light bootstrap context, and attachment staging are unavailable on this path because visible sessions are persistent dashboard sessions created through `sessions.create`. Visible spawning is also rejected when inherited tool restrictions cannot be carried into the dashboard session. See [Managed worktrees](/concepts/managed-worktrees) for checkout naming, setup, cleanup, and restore behavior.
With `visible: true`, `model`, `cwd`, and a same-agent `context: "fork"` are supported. A sandboxed target restricts `cwd` to that agent's workspace. Thread binding, `mode`, thinking overrides, `lightContext`, `attachments`, and `attachAs` are unavailable on this path because visible sessions are persistent dashboard sessions created through `sessions.create`. Visible spawning is rejected when the requester was itself spawned with an inherited tool allowlist or denylist; that restriction is fixed at spawn time and has no config override. Session listing and addressing obey `tools.sessions.visibility`; the default `tree` scope covers the current session and its own spawn subtree. See [Managed worktrees](/concepts/managed-worktrees) for checkout naming, setup, cleanup, and restore behavior.
### Task names and targeting
+4 -2
View File
@@ -72,6 +72,8 @@ export function describeSessionsSpawnTool(options?: {
options?.threadAvailable
? '`mode="run"` one-shot; `mode="session"` persistent/thread-bound only on supporting requester channel.'
: '`mode="run"` one-shot background.',
'`visible=true`: persistent dashboard session; subagent only; omit `mode` (no `mode="run"`), `thread`, `thinking`, `lightContext`, `attachments`, `attachAs`; inherited tool allow/denylist blocks it at spawn with no config override.',
"Session listing/addressing obeys `tools.sessions.visibility` (`tree` default: current + own spawn subtree).",
"Inherits parent workspace. Native task arrives as first `[Subagent Task]`.",
'Native transcript needed: `context="fork"`; else omit/isolated.',
"Use fresh child for sidecar/parallel batch reads, multi-step search, data collection; avoid quick lookup/single read unless policy prefers.",
@@ -81,9 +83,9 @@ export function describeSessionsSpawnTool(options?: {
return baseDescription.join(" ");
}
return [
...baseDescription.slice(0, 3),
...baseDescription.slice(0, 5),
'`runtime="acp"` ids: codex, claude, gemini, opencode, or configured ACP.',
...baseDescription.slice(3),
...baseDescription.slice(5),
].join(" ");
}
+45 -10
View File
@@ -382,12 +382,26 @@ describe("sessions_spawn tool", () => {
it("advertises visible sessions with terse UI guidance", () => {
const tool = createSessionsSpawnTool();
const schema = tool.parameters as {
properties?: { visible?: { description?: string }; worktree?: unknown };
properties?: Record<
string,
{ anyOf?: unknown; description?: string; enum?: string[] } | undefined
>;
};
expect(schema.properties?.visible?.description).toBe(
"visible: user sees session in UI. Use when user asked or talks via web/app.",
"Persistent UI session; subagent only; omit mode/thread/thinking/lightContext/attachments/attachAs; unavailable with inherited tool allow/denylist.",
);
expect(tool.description).toContain("`visible=true`: persistent dashboard session");
expect(tool.description).toContain('no `mode="run"`');
expect(tool.description).toContain("inherited tool allow/denylist");
expect(tool.description).toContain("`tools.sessions.visibility`");
expect(schema.properties?.runtime?.description).toContain("visible=true");
expect(schema.properties?.mode?.description).toContain("Omit with visible=true");
expect(schema.properties?.lightContext?.description).toContain("unavailable with visible=true");
expect(schema.properties?.attachments?.description).toContain("unavailable with visible=true");
expect(schema.properties?.attachAs?.description).toContain("unavailable with visible=true");
expect(schema.properties?.mode?.enum).toEqual(["run"]);
expect(schema.properties?.mode?.anyOf).toBeUndefined();
expect(schema.properties?.worktree).toBeDefined();
});
@@ -477,7 +491,7 @@ describe("sessions_spawn tool", () => {
await expect(
tool.execute("hidden-worktree", { task: "inspect", worktree: true }),
).rejects.toThrow("worktree options require visible=true");
).rejects.toThrow("Parameters require visible=true: worktree");
expect(hoisted.spawnSubagentDirectMock).not.toHaveBeenCalled();
});
@@ -616,32 +630,32 @@ describe("sessions_spawn tool", () => {
[
"thinking",
{ thinking: "high" },
"thinking unavailable with visible=true: thinking overrides are not wired to the sessions.create path",
"Parameters unavailable with visible=true: thinking: thinking overrides are not wired to the sessions.create path",
],
[
"thread",
{ thread: true },
"thread unavailable with visible=true: visible sessions route to the dashboard, not a channel thread",
"Parameters unavailable with visible=true: thread: visible sessions route to the dashboard, not a channel thread",
],
[
"mode",
{ mode: "session" },
"mode unavailable with visible=true: visible sessions are persistent dashboard sessions",
"Parameters unavailable with visible=true: mode: visible sessions are persistent dashboard sessions",
],
[
"lightContext",
{ lightContext: true },
"lightContext unavailable with visible=true: bootstrap staging is not wired to the sessions.create path",
"Parameters unavailable with visible=true: lightContext: bootstrap staging is not wired to the sessions.create path",
],
[
"attachments",
{ attachments: [{ name: "note.txt", content: "hello" }] },
"attachments unavailable with visible=true: attachment staging is not wired to the sessions.create path",
"Parameters unavailable with visible=true: attachments: attachment staging is not wired to the sessions.create path",
],
[
"attachAs",
{ attachAs: { mountPath: "inputs" } },
"attachAs unavailable with visible=true: attachment staging is not wired to the sessions.create path",
"Parameters unavailable with visible=true: attachAs: attachment staging is not wired to the sessions.create path",
],
] as const)("rejects visible %s overrides with a reason", async (_name, override, message) => {
const tool = createSessionsSpawnTool({ agentSessionKey: "agent:main:main" });
@@ -651,6 +665,26 @@ describe("sessions_spawn tool", () => {
).rejects.toThrow(message);
});
it("reports every unsupported visible parameter in one error", async () => {
const tool = createSessionsSpawnTool({ agentSessionKey: "agent:main:main" });
await expect(
tool.execute("visible-unsupported-many", {
task: "inspect",
runtime: "acp",
thinking: "high",
thread: true,
mode: "run",
lightContext: true,
attachments: [{ name: "note.txt", content: "hello" }],
attachAs: { mountPath: "inputs" },
visible: true,
}),
).rejects.toThrow(
'Parameters unavailable with visible=true: runtime: supports runtime="subagent" only; thinking: thinking overrides are not wired to the sessions.create path; thread: visible sessions route to the dashboard, not a channel thread; mode: visible sessions are persistent dashboard sessions; lightContext: bootstrap staging is not wired to the sessions.create path; attachments: attachment staging is not wired to the sessions.create path; attachAs: attachment staging is not wired to the sessions.create path',
);
});
it("denies visible sessions when tool restrictions cannot carry forward", async () => {
const callGateway = vi.fn();
const tool = createSessionsSpawnTool({
@@ -667,7 +701,8 @@ describe("sessions_spawn tool", () => {
expect(result.details).toMatchObject({
status: "forbidden",
error: "Visible sessions unavailable with inherited tool restrictions.",
error:
"Visible sessions unavailable with inherited tool restrictions. This session was spawned with a tool allow/denylist; visible sessions require an unrestricted session.",
});
expect(callGateway).not.toHaveBeenCalled();
});
+26 -12
View File
@@ -143,29 +143,40 @@ function createSessionsSpawnToolSchema(params: {
label: Type.Optional(Type.String()),
runtime: optionalStringEnum(
params.acpAvailable ? SESSIONS_SPAWN_RUNTIMES : (["subagent"] as const),
{ description: 'Runtime; visible=true requires "subagent".' },
),
agentId: Type.Optional(Type.String()),
model: Type.Optional(Type.String()),
thinking: Type.Optional(Type.String()),
thinking: Type.Optional(
Type.String({ description: "Thinking override; unavailable with visible=true." }),
),
cwd: Type.Optional(Type.String()),
...(params.threadAvailable
? {
thread: Type.Optional(
Type.Boolean({
description: 'Bind new chat thread when supported; true defaults mode="session".',
description:
'Bind new chat thread when supported; true defaults mode="session"; unavailable with visible=true.',
}),
),
}
: {}),
mode: optionalStringEnum(spawnModes),
cleanup: optionalStringEnum(["delete", "keep"] as const),
mode: optionalStringEnum(spawnModes, {
description: params.threadAvailable
? '"run" one-shot; "session" persistent/thread-bound. Omit with visible=true.'
: '"run" one-shot. Omit with visible=true; visible sessions are persistent.',
}),
cleanup: optionalStringEnum(["delete", "keep"] as const, {
description: "Hidden session cleanup; visible=true always keeps the session.",
}),
sandbox: optionalStringEnum(SESSIONS_SPAWN_SANDBOX_MODES),
context: optionalStringEnum(SUBAGENT_SPAWN_CONTEXT_MODES, {
description: "Native: omit/isolated clean; fork only needing requester transcript.",
description:
"Native: omit/isolated clean; fork only needing requester transcript; visible fork requires same agent.",
}),
lightContext: Type.Optional(
Type.Boolean({
description: "Light bootstrap; subagent only.",
description: "Light bootstrap; subagent only; unavailable with visible=true.",
}),
),
...(params.swarmEnabled
@@ -187,15 +198,18 @@ function createSessionsSpawnToolSchema(params: {
encoding: Type.Optional(optionalStringEnum(["utf8", "base64"] as const)),
mimeType: Type.Optional(Type.String()),
}),
{ maxItems: 50 },
{ maxItems: 50, description: "Inline snapshots; unavailable with visible=true." },
),
),
attachAs: Type.Optional(
Type.Object({
// Where the spawned agent should look for attachments.
// Kept as a hint; implementation materializes into the child workspace.
mountPath: Type.Optional(Type.String()),
}),
Type.Object(
{
// Where the spawned agent should look for attachments.
// Kept as a hint; implementation materializes into the child workspace.
mountPath: Type.Optional(Type.String()),
},
{ description: "Attachment mount hint; unavailable with visible=true." },
),
),
...(params.acpAvailable
? {
+26 -10
View File
@@ -30,7 +30,8 @@ import { reserveVisibleChildSlot } from "./sessions-spawn-visible-admission.js";
export const VISIBLE_SESSIONS_SPAWN_SCHEMA = {
visible: Type.Optional(
Type.Boolean({
description: "visible: user sees session in UI. Use when user asked or talks via web/app.",
description:
"Persistent UI session; subagent only; omit mode/thread/thinking/lightContext/attachments/attachAs; unavailable with inherited tool allow/denylist.",
}),
),
worktree: Type.Optional(Type.Boolean({ description: "Visible session worktree" })),
@@ -94,18 +95,30 @@ export async function maybeSpawnVisibleSession(params: {
const worktreeName = readStringParam(params.raw, "worktreeName");
const worktreeBaseRef = readStringParam(params.raw, "worktreeBaseRef");
if (params.raw.visible !== true) {
if (worktree || worktreeName || worktreeBaseRef) {
throw new ToolInputError("worktree options require visible=true");
const visibleOnlyParams = [
["worktree", worktree],
["worktreeName", worktreeName],
["worktreeBaseRef", worktreeBaseRef],
] as const;
const providedVisibleOnlyParams = visibleOnlyParams
.filter(([, value]) => value !== undefined && value !== false)
.map(([name]) => name);
if (providedVisibleOnlyParams.length > 0) {
throw new ToolInputError(
`Parameters require visible=true: ${providedVisibleOnlyParams.join(", ")}`,
);
}
return undefined;
}
if (params.runtime !== "subagent") {
throw new ToolInputError('visible=true supports runtime="subagent" only');
}
const modelOverride = normalizeToolModelOverride(readStringParam(params.raw, "model"));
const requestedCwd = readStringParam(params.raw, "cwd");
const spawnedCwd = requestedCwd ? resolveUserPath(requestedCwd) : undefined;
const unsupported = [
[
"runtime",
params.runtime === "subagent" ? undefined : params.runtime,
'supports runtime="subagent" only',
],
[
"thinking",
readStringParam(params.raw, "thinking"),
@@ -133,10 +146,12 @@ export async function maybeSpawnVisibleSession(params: {
"attachment staging is not wired to the sessions.create path",
],
] as const;
const unsupportedEntry = unsupported.find(([, value]) => value !== undefined);
if (unsupportedEntry) {
const unsupportedEntries = unsupported.filter(([, value]) => value !== undefined);
if (unsupportedEntries.length > 0) {
throw new ToolInputError(
`${unsupportedEntry[0]} unavailable with visible=true: ${unsupportedEntry[2]}`,
`Parameters unavailable with visible=true: ${unsupportedEntries
.map(([name, , reason]) => `${name}: ${reason}`)
.join("; ")}`,
);
}
@@ -147,7 +162,8 @@ export async function maybeSpawnVisibleSession(params: {
) {
return {
status: "forbidden",
error: "Visible sessions unavailable with inherited tool restrictions.",
error:
"Visible sessions unavailable with inherited tool restrictions. This session was spawned with a tool allow/denylist; visible sessions require an unrestricted session.",
};
}
const ownership = resolveSubagentSpawnOwnership({
@@ -138,13 +138,14 @@
"type": "function"
},
{
"description": "Spawn clean child; default `runtime=\"subagent\"`. `mode=\"run\"` one-shot; `mode=\"session\"` persistent/thread-bound only on supporting requester channel. Inherits parent workspace. Native task arrives as first `[Subagent Task]`. Native transcript needed: `context=\"fork\"`; else omit/isolated. Use fresh child for sidecar/parallel batch reads, multi-step search, data collection; avoid quick lookup/single read unless policy prefers. After spawn, do non-overlap work. Run result returns; session output stays thread.",
"description": "Spawn clean child; default `runtime=\"subagent\"`. `mode=\"run\"` one-shot; `mode=\"session\"` persistent/thread-bound only on supporting requester channel. `visible=true`: persistent dashboard session; subagent only; omit `mode` (no `mode=\"run\"`), `thread`, `thinking`, `lightContext`, `attachments`, `attachAs`; inherited tool allow/denylist blocks it at spawn with no config override. Session listing/addressing obeys `tools.sessions.visibility` (`tree` default: current + own spawn subtree). Inherits parent workspace. Native task arrives as first `[Subagent Task]`. Native transcript needed: `context=\"fork\"`; else omit/isolated. Use fresh child for sidecar/parallel batch reads, multi-step search, data collection; avoid quick lookup/single read unless policy prefers. After spawn, do non-overlap work. Run result returns; session output stays thread.",
"inputSchema": {
"properties": {
"agentId": {
"type": "string"
},
"attachAs": {
"description": "Attachment mount hint; unavailable with visible=true.",
"properties": {
"mountPath": {
"type": "string"
@@ -153,6 +154,7 @@
"type": "object"
},
"attachments": {
"description": "Inline snapshots; unavailable with visible=true.",
"items": {
"properties": {
"content": {
@@ -176,11 +178,12 @@
"type": "array"
},
"cleanup": {
"description": "Hidden session cleanup; visible=true always keeps the session.",
"enum": ["delete", "keep"],
"type": "string"
},
"context": {
"description": "Native: omit/isolated clean; fork only needing requester transcript.",
"description": "Native: omit/isolated clean; fork only needing requester transcript; visible fork requires same agent.",
"enum": ["isolated", "fork"],
"type": "string"
},
@@ -191,10 +194,11 @@
"type": "string"
},
"lightContext": {
"description": "Light bootstrap; subagent only.",
"description": "Light bootstrap; subagent only; unavailable with visible=true.",
"type": "boolean"
},
"mode": {
"description": "\"run\" one-shot; \"session\" persistent/thread-bound. Omit with visible=true.",
"enum": ["run", "session"],
"type": "string"
},
@@ -202,6 +206,7 @@
"type": "string"
},
"runtime": {
"description": "Runtime; visible=true requires \"subagent\".",
"enum": ["subagent"],
"type": "string"
},
@@ -217,14 +222,15 @@
"type": "string"
},
"thinking": {
"description": "Thinking override; unavailable with visible=true.",
"type": "string"
},
"thread": {
"description": "Bind new chat thread when supported; true defaults mode=\"session\".",
"description": "Bind new chat thread when supported; true defaults mode=\"session\"; unavailable with visible=true.",
"type": "boolean"
},
"visible": {
"description": "visible: user sees session in UI. Use when user asked or talks via web/app.",
"description": "Persistent UI session; subagent only; omit mode/thread/thinking/lightContext/attachments/attachAs; unavailable with inherited tool allow/denylist.",
"type": "boolean"
},
"worktree": {
@@ -138,13 +138,14 @@
"type": "function"
},
{
"description": "Spawn clean child; default `runtime=\"subagent\"`. `mode=\"run\"` one-shot background. Inherits parent workspace. Native task arrives as first `[Subagent Task]`. Native transcript needed: `context=\"fork\"`; else omit/isolated. Use fresh child for sidecar/parallel batch reads, multi-step search, data collection; avoid quick lookup/single read unless policy prefers. After spawn, do non-overlap work while run result returns.",
"description": "Spawn clean child; default `runtime=\"subagent\"`. `mode=\"run\"` one-shot background. `visible=true`: persistent dashboard session; subagent only; omit `mode` (no `mode=\"run\"`), `thread`, `thinking`, `lightContext`, `attachments`, `attachAs`; inherited tool allow/denylist blocks it at spawn with no config override. Session listing/addressing obeys `tools.sessions.visibility` (`tree` default: current + own spawn subtree). Inherits parent workspace. Native task arrives as first `[Subagent Task]`. Native transcript needed: `context=\"fork\"`; else omit/isolated. Use fresh child for sidecar/parallel batch reads, multi-step search, data collection; avoid quick lookup/single read unless policy prefers. After spawn, do non-overlap work while run result returns.",
"inputSchema": {
"properties": {
"agentId": {
"type": "string"
},
"attachAs": {
"description": "Attachment mount hint; unavailable with visible=true.",
"properties": {
"mountPath": {
"type": "string"
@@ -153,6 +154,7 @@
"type": "object"
},
"attachments": {
"description": "Inline snapshots; unavailable with visible=true.",
"items": {
"properties": {
"content": {
@@ -176,11 +178,12 @@
"type": "array"
},
"cleanup": {
"description": "Hidden session cleanup; visible=true always keeps the session.",
"enum": ["delete", "keep"],
"type": "string"
},
"context": {
"description": "Native: omit/isolated clean; fork only needing requester transcript.",
"description": "Native: omit/isolated clean; fork only needing requester transcript; visible fork requires same agent.",
"enum": ["isolated", "fork"],
"type": "string"
},
@@ -191,10 +194,11 @@
"type": "string"
},
"lightContext": {
"description": "Light bootstrap; subagent only.",
"description": "Light bootstrap; subagent only; unavailable with visible=true.",
"type": "boolean"
},
"mode": {
"description": "\"run\" one-shot. Omit with visible=true; visible sessions are persistent.",
"enum": ["run"],
"type": "string"
},
@@ -202,6 +206,7 @@
"type": "string"
},
"runtime": {
"description": "Runtime; visible=true requires \"subagent\".",
"enum": ["subagent"],
"type": "string"
},
@@ -217,10 +222,11 @@
"type": "string"
},
"thinking": {
"description": "Thinking override; unavailable with visible=true.",
"type": "string"
},
"visible": {
"description": "visible: user sees session in UI. Use when user asked or talks via web/app.",
"description": "Persistent UI session; subagent only; omit mode/thread/thinking/lightContext/attachments/attachAs; unavailable with inherited tool allow/denylist.",
"type": "boolean"
},
"worktree": {
@@ -138,13 +138,14 @@
"type": "function"
},
{
"description": "Spawn clean child; default `runtime=\"subagent\"`. `mode=\"run\"` one-shot background. Inherits parent workspace. Native task arrives as first `[Subagent Task]`. Native transcript needed: `context=\"fork\"`; else omit/isolated. Use fresh child for sidecar/parallel batch reads, multi-step search, data collection; avoid quick lookup/single read unless policy prefers. After spawn, do non-overlap work while run result returns.",
"description": "Spawn clean child; default `runtime=\"subagent\"`. `mode=\"run\"` one-shot background. `visible=true`: persistent dashboard session; subagent only; omit `mode` (no `mode=\"run\"`), `thread`, `thinking`, `lightContext`, `attachments`, `attachAs`; inherited tool allow/denylist blocks it at spawn with no config override. Session listing/addressing obeys `tools.sessions.visibility` (`tree` default: current + own spawn subtree). Inherits parent workspace. Native task arrives as first `[Subagent Task]`. Native transcript needed: `context=\"fork\"`; else omit/isolated. Use fresh child for sidecar/parallel batch reads, multi-step search, data collection; avoid quick lookup/single read unless policy prefers. After spawn, do non-overlap work while run result returns.",
"inputSchema": {
"properties": {
"agentId": {
"type": "string"
},
"attachAs": {
"description": "Attachment mount hint; unavailable with visible=true.",
"properties": {
"mountPath": {
"type": "string"
@@ -153,6 +154,7 @@
"type": "object"
},
"attachments": {
"description": "Inline snapshots; unavailable with visible=true.",
"items": {
"properties": {
"content": {
@@ -176,11 +178,12 @@
"type": "array"
},
"cleanup": {
"description": "Hidden session cleanup; visible=true always keeps the session.",
"enum": ["delete", "keep"],
"type": "string"
},
"context": {
"description": "Native: omit/isolated clean; fork only needing requester transcript.",
"description": "Native: omit/isolated clean; fork only needing requester transcript; visible fork requires same agent.",
"enum": ["isolated", "fork"],
"type": "string"
},
@@ -191,10 +194,11 @@
"type": "string"
},
"lightContext": {
"description": "Light bootstrap; subagent only.",
"description": "Light bootstrap; subagent only; unavailable with visible=true.",
"type": "boolean"
},
"mode": {
"description": "\"run\" one-shot. Omit with visible=true; visible sessions are persistent.",
"enum": ["run"],
"type": "string"
},
@@ -202,6 +206,7 @@
"type": "string"
},
"runtime": {
"description": "Runtime; visible=true requires \"subagent\".",
"enum": ["subagent"],
"type": "string"
},
@@ -217,10 +222,11 @@
"type": "string"
},
"thinking": {
"description": "Thinking override; unavailable with visible=true.",
"type": "string"
},
"visible": {
"description": "visible: user sees session in UI. Use when user asked or talks via web/app.",
"description": "Persistent UI session; subagent only; omit mode/thread/thinking/lightContext/attachments/attachAs; unavailable with inherited tool allow/denylist.",
"type": "boolean"
},
"worktree": {
@@ -216,8 +216,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the
"roughTokens": 0
},
"dynamicToolsJson": {
"chars": 55806,
"roughTokens": 13952
"chars": 56822,
"roughTokens": 14206
},
"openClawDeveloperInstructions": {
"chars": 3559,
@@ -228,8 +228,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the
"roughTokens": 7021
},
"totalWithDynamicToolsJson": {
"chars": 83892,
"roughTokens": 20973
"chars": 84908,
"roughTokens": 21227
},
"userInputText": {
"chars": 1442,
@@ -216,8 +216,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the
"roughTokens": 0
},
"dynamicToolsJson": {
"chars": 55533,
"roughTokens": 13884
"chars": 56514,
"roughTokens": 14129
},
"openClawDeveloperInstructions": {
"chars": 2450,
@@ -228,8 +228,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the
"roughTokens": 6642
},
"totalWithDynamicToolsJson": {
"chars": 82101,
"roughTokens": 20526
"chars": 83082,
"roughTokens": 20771
},
"userInputText": {
"chars": 1033,
@@ -217,8 +217,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the
"roughTokens": 0
},
"dynamicToolsJson": {
"chars": 56823,
"roughTokens": 14206
"chars": 57804,
"roughTokens": 14451
},
"openClawDeveloperInstructions": {
"chars": 2469,
@@ -229,8 +229,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the
"roughTokens": 6777
},
"totalWithDynamicToolsJson": {
"chars": 83930,
"roughTokens": 20983
"chars": 84911,
"roughTokens": 21228
},
"userInputText": {
"chars": 1271,