mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-02 07:51:35 +00:00
feat(agents): surface watched-session awareness to the model (#114835)
This commit is contained in:
@@ -4,7 +4,7 @@ cbf4e2c3088f8886a7c9ea91325a66e0f0846cea21f0b2891f36399b4811306c module/account
|
||||
8e985f345f21a1c9a2b0e94304aaaad6a326bec1c1ce3b26027d2862804a366e module/account-resolution
|
||||
e5e67ddf3cab38fcbf9220bc3160715897e2709d9a9ff6ff36f1ecc9453c2367 module/agent-config-primitives
|
||||
74daa746deb548379d3f0d6eac3c4d082df1034c4360cc03bf51fee0f10a2e4d module/agent-harness
|
||||
ccfc54a6dc389b38bb06c2dbff05b30f35ff4efd953887cac8066e1aae7db45d module/agent-harness-runtime
|
||||
9285d1fee755e391a85f5ba892144684ee5b2c09d42410781324b4ddf9b36852 module/agent-harness-runtime
|
||||
5168648cd946abad8a92822889f13ceacc87ed502314a66190d0b1eb8ebe76ea module/agent-media-payload
|
||||
2dcb4d62d90e5d71594f6b843c97534509a154784e378fb1c75bd86b5122b710 module/agent-runtime
|
||||
56b6d5fb6af3d95af1200065aca2e7d4f59e5fa59740505fe6ff433077ef6646 module/allow-from
|
||||
|
||||
@@ -37,7 +37,8 @@ world converges:
|
||||
Activity queues up as compact notices — coalesced per conversation, never
|
||||
one wake-up per message — and the agent sees them the next time it runs: on
|
||||
your next message or on a scheduled heartbeat. The agent can also read the
|
||||
sessions it watches, so "what did I miss in the family group?" works.
|
||||
sessions it watches — its system prompt names them — so "what did I miss in
|
||||
the family group?" works.
|
||||
- **Background work.** Sub-agents and spawned sessions announce their results
|
||||
back to the session that started them, so work the agent kicked off from
|
||||
Home reports back to Home.
|
||||
|
||||
@@ -149,7 +149,9 @@ Session tools are scoped to limit what the agent can see:
|
||||
|
||||
Default is `tree`. Sandboxed sessions are clamped to `tree` regardless of config.
|
||||
With the default `session.dmScope: "main"`, group activity makes watched
|
||||
same-agent group sessions readable from the main session.
|
||||
same-agent group sessions readable from the main session, and the main
|
||||
session's system prompt lists those watched sessions so the agent knows it can
|
||||
read them.
|
||||
|
||||
## Further reading
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ import {
|
||||
} from "openclaw/plugin-sdk/memory-host-core";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
buildCodexOpenClawPromptContext,
|
||||
buildCodexWatchedSessionsContext,
|
||||
buildCodexWorkspaceBootstrapContext,
|
||||
buildCodexSystemPromptReport,
|
||||
readContextEngineThreadBootstrapProjection,
|
||||
@@ -249,4 +251,47 @@ describe("Codex app-server attempt context", () => {
|
||||
reason: "dynamic-tools-mismatch",
|
||||
});
|
||||
});
|
||||
|
||||
it("stitches watched-session context into the per-turn OpenClaw prompt context", () => {
|
||||
const attempt = { config: {} } as EmbeddedRunAttemptParams;
|
||||
|
||||
expect(
|
||||
buildCodexOpenClawPromptContext({
|
||||
params: attempt,
|
||||
watchedSessionsContext: [
|
||||
"## Watched Sessions",
|
||||
"- agent:main:telegram:group:beta — Family group",
|
||||
].join("\n"),
|
||||
}),
|
||||
).toContain("## Watched Sessions");
|
||||
|
||||
// No ambient watches (and no state) must render nothing, not an empty section.
|
||||
expect(
|
||||
buildCodexWatchedSessionsContext({
|
||||
attempt,
|
||||
dynamicTools: [
|
||||
{
|
||||
type: "function",
|
||||
name: "sessions_history",
|
||||
description: "history",
|
||||
inputSchema: {},
|
||||
},
|
||||
],
|
||||
sessionKey: "agent:codex-test:main",
|
||||
}),
|
||||
).toBe(undefined);
|
||||
|
||||
// Lightweight cron turns keep the runtime context byte-for-byte untouched.
|
||||
expect(
|
||||
buildCodexWatchedSessionsContext({
|
||||
attempt: {
|
||||
config: {},
|
||||
bootstrapContextMode: "lightweight",
|
||||
bootstrapContextRunKind: "cron",
|
||||
} as EmbeddedRunAttemptParams,
|
||||
dynamicTools: [],
|
||||
sessionKey: "agent:codex-test:main",
|
||||
}),
|
||||
).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ import { createHash } from "node:crypto";
|
||||
import path from "node:path";
|
||||
import {
|
||||
buildBootstrapContextForFiles,
|
||||
buildWatchedSessionsHarnessContext,
|
||||
embeddedAgentLog,
|
||||
resolveBootstrapFilesForRun,
|
||||
type AgentMessage,
|
||||
@@ -503,6 +504,7 @@ function readNonEmptyString(value: unknown): string | undefined {
|
||||
export function buildCodexOpenClawPromptContext(params: {
|
||||
params: EmbeddedRunAttemptParams;
|
||||
workspacePromptContext?: string;
|
||||
watchedSessionsContext?: string;
|
||||
}): string | undefined {
|
||||
if (!shouldInjectCodexOpenClawPromptContext(params.params)) {
|
||||
return undefined;
|
||||
@@ -511,6 +513,7 @@ export function buildCodexOpenClawPromptContext(params: {
|
||||
params.workspacePromptContext?.trim()
|
||||
? ["## OpenClaw Workspace Context", "", params.workspacePromptContext.trim()].join("\n")
|
||||
: undefined,
|
||||
params.watchedSessionsContext?.trim() || undefined,
|
||||
].filter(isNonEmptyString);
|
||||
if (sections.length === 0) {
|
||||
return undefined;
|
||||
@@ -523,6 +526,31 @@ export function buildCodexOpenClawPromptContext(params: {
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the watched-sessions block for the Codex per-turn runtime context.
|
||||
* Codex builds its own instruction layers, so the embedded prompt's Watched
|
||||
* Sessions section must be re-surfaced here or Codex-backed main sessions
|
||||
* keep refusing cross-session questions (openclaw#114797).
|
||||
*/
|
||||
export function buildCodexWatchedSessionsContext(params: {
|
||||
attempt: EmbeddedRunAttemptParams;
|
||||
dynamicTools: readonly CodexDynamicToolSpec[];
|
||||
sessionKey?: string;
|
||||
sandboxed?: boolean;
|
||||
}): string | undefined {
|
||||
if (!shouldInjectCodexOpenClawPromptContext(params.attempt)) {
|
||||
return undefined;
|
||||
}
|
||||
return buildWatchedSessionsHarnessContext({
|
||||
config: params.attempt.config,
|
||||
sessionKey: params.sessionKey,
|
||||
sandboxed: params.sandboxed,
|
||||
toolNames: flattenCodexDynamicToolFunctions(params.dynamicTools).map((tool) =>
|
||||
normalizeCodexDynamicToolName(tool.name),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
function shouldInjectCodexOpenClawPromptContext(params: EmbeddedRunAttemptParams): boolean {
|
||||
// Lightweight cron runs are commonly exact commands. Keep the user input byte-for-byte
|
||||
// to avoid changing command intent while Codex keeps its native project-doc loader.
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from "openclaw/plugin-sdk/agent-harness-runtime";
|
||||
import {
|
||||
buildCodexOpenClawPromptContext,
|
||||
buildCodexWatchedSessionsContext,
|
||||
buildCodexWorkspaceBootstrapContext,
|
||||
getCodexWorkspaceMemoryToolNames,
|
||||
readMirroredSessionHistoryMessages,
|
||||
@@ -147,6 +148,12 @@ export async function prepareCodexAttemptContext(
|
||||
const openClawPromptContext = buildCodexOpenClawPromptContext({
|
||||
params: runtimeParams,
|
||||
workspacePromptContext: workspaceBootstrapContext.promptContext,
|
||||
watchedSessionsContext: buildCodexWatchedSessionsContext({
|
||||
attempt: runtimeParams,
|
||||
dynamicTools: toolBridge.availableSpecs,
|
||||
sessionKey: contextSessionKey,
|
||||
sandboxed: sandbox?.enabled === true,
|
||||
}),
|
||||
});
|
||||
const skillsCollaborationInstructions = renderCodexSkillsCollaborationInstructions({
|
||||
attempt: runtimeParams,
|
||||
|
||||
@@ -196,7 +196,8 @@ export function readPluginSdkSurfaceBudgets(env = process.env) {
|
||||
// +3: root-bounded walk iterator, options, and entry contract.
|
||||
// +5: pinned secret create/read functions and their options contract.
|
||||
// +1: canonical Gateway browser-origin acceptance for browser-facing plugin routes.
|
||||
4756,
|
||||
// +1: watched-sessions prompt block for plugin-owned harness runtimes.
|
||||
4757,
|
||||
env,
|
||||
),
|
||||
publicFunctionExports: readPluginSdkSurfaceBudgetEnv(
|
||||
@@ -230,7 +231,8 @@ export function readPluginSdkSurfaceBudgets(env = process.env) {
|
||||
// +1: root-bounded directory walk iterator.
|
||||
// +4: pinned secret create and synchronous/asynchronous reads.
|
||||
// +1: canonical Gateway browser-origin acceptance for browser-facing plugin routes.
|
||||
2877,
|
||||
// +1: watched-sessions prompt block for plugin-owned harness runtimes.
|
||||
2878,
|
||||
env,
|
||||
),
|
||||
publicDeprecatedExports: readPluginSdkSurfaceBudgetEnv(
|
||||
|
||||
@@ -60,6 +60,7 @@ import {
|
||||
filterRuntimeCompatibleTools,
|
||||
} from "../tool-schema-projection.js";
|
||||
import { logRuntimeToolSchemaQuarantine } from "../tool-schema-quarantine.js";
|
||||
import { prepareWatchedSessionsPrompt } from "../watched-sessions-prompt.js";
|
||||
import { resolveCompactionContextTokenBudget } from "./compaction-runtime-context.js";
|
||||
import type { DirectCompactionPreparation } from "./direct-compaction-preparation.js";
|
||||
import { applyFinalEffectiveToolPolicy } from "./effective-tool-policy.js";
|
||||
@@ -540,6 +541,18 @@ export async function buildPreparedCompactionRuntime(prepared: DirectCompactionP
|
||||
agentSessionKey: runtimeInfo.sessionKey,
|
||||
sandboxed: sandboxInfo?.enabled === true,
|
||||
});
|
||||
// Compaction must build byte-identical prompt sections to live turns, or
|
||||
// the compaction run misses the transcript's cached prompt prefix. The
|
||||
// allowlist doubles as the capability set so a session-read tool reachable
|
||||
// only through capability names gates the section the same way live turns do.
|
||||
const preparedWatchedSessions = prepareWatchedSessionsPrompt({
|
||||
enabled: promptMode === "full",
|
||||
config: params.config,
|
||||
sessionKey: params.sessionKey,
|
||||
sandboxed: sandboxInfo?.enabled === true,
|
||||
toolNames: effectiveTools.map((tool) => tool.name),
|
||||
capabilityToolNames: allowedToolNames,
|
||||
});
|
||||
const buildSystemPromptText = (defaultThinkLevel: ThinkLevel) => {
|
||||
const builtSystemPrompt = buildEmbeddedSystemPrompt({
|
||||
config: params.config,
|
||||
@@ -575,6 +588,7 @@ export async function buildPreparedCompactionRuntime(prepared: DirectCompactionP
|
||||
userTimeFormat,
|
||||
contextFiles,
|
||||
preparedMemoryPrompt,
|
||||
preparedWatchedSessions,
|
||||
promptContribution,
|
||||
nativeCommandGuidanceLines,
|
||||
});
|
||||
|
||||
@@ -34,6 +34,7 @@ import { buildSystemPromptParams } from "../../system-prompt-params.js";
|
||||
import { buildSystemPromptReport } from "../../system-prompt-report.js";
|
||||
import type { ToolSearchCatalogRef } from "../../tool-search.js";
|
||||
import { buildToolSchemaDirectoryPrompt } from "../../tool-search.js";
|
||||
import { prepareWatchedSessionsPrompt } from "../../watched-sessions-prompt.js";
|
||||
import { buildEmbeddedMessageActionDiscoveryInput } from "../message-action-discovery-input.js";
|
||||
import { buildEmbeddedSandboxInfo, resolveEmbeddedSandboxInfoExecPolicy } from "../sandbox-info.js";
|
||||
import { buildEmbeddedSystemPrompt } from "../system-prompt.js";
|
||||
@@ -248,6 +249,14 @@ export async function prepareEmbeddedAttemptSystemPrompt(params: {
|
||||
agentSessionKey: runtimeInfo.sessionKey,
|
||||
sandboxed: sandboxInfo?.enabled === true,
|
||||
});
|
||||
const preparedWatchedSessions = prepareWatchedSessionsPrompt({
|
||||
enabled: effectivePromptMode === "full",
|
||||
config: attempt.config,
|
||||
sessionKey: attempt.sessionKey,
|
||||
sandboxed: sandboxInfo?.enabled === true,
|
||||
toolNames: params.effectiveTools.map((tool) => tool.name),
|
||||
capabilityToolNames: params.capabilityToolNames,
|
||||
});
|
||||
|
||||
const attemptSystemPrompt = buildAttemptSystemPrompt({
|
||||
isRawModelRun: params.isRawModelRun,
|
||||
@@ -302,6 +311,7 @@ export async function prepareEmbeddedAttemptSystemPrompt(params: {
|
||||
),
|
||||
includeMemorySection,
|
||||
preparedMemoryPrompt,
|
||||
preparedWatchedSessions,
|
||||
promptContribution,
|
||||
},
|
||||
providerTransform: {
|
||||
|
||||
@@ -17,6 +17,7 @@ import type { AgentSession } from "../sessions/index.js";
|
||||
import { buildConfiguredAgentSystemPrompt } from "../system-prompt-config.js";
|
||||
import type { ProviderSystemPromptContribution } from "../system-prompt-contribution.js";
|
||||
import type { PromptMode, SilentReplyPromptMode } from "../system-prompt.types.js";
|
||||
import type { PreparedWatchedSessionsPrompt } from "../watched-sessions-prompt.js";
|
||||
import type { EmbeddedSandboxInfo } from "./types.js";
|
||||
import type { ReasoningLevel, ThinkLevel } from "./utils.js";
|
||||
|
||||
@@ -93,6 +94,7 @@ export function buildEmbeddedSystemPrompt(params: {
|
||||
includeMemorySection?: boolean;
|
||||
memoryCitationsMode?: MemoryCitationsMode;
|
||||
preparedMemoryPrompt?: PreparedMemoryPromptSection;
|
||||
preparedWatchedSessions?: PreparedWatchedSessionsPrompt;
|
||||
promptContribution?: ProviderSystemPromptContribution;
|
||||
}): string {
|
||||
return buildConfiguredAgentSystemPrompt({
|
||||
@@ -139,6 +141,7 @@ export function buildEmbeddedSystemPrompt(params: {
|
||||
includeMemorySection: params.includeMemorySection,
|
||||
memoryCitationsMode: params.memoryCitationsMode,
|
||||
preparedMemoryPrompt: params.preparedMemoryPrompt,
|
||||
preparedWatchedSessions: params.preparedWatchedSessions,
|
||||
promptContribution: params.promptContribution,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2231,7 +2231,7 @@ describe("session_status tool", () => {
|
||||
model: "default",
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
"Session status visibility is restricted to the current session tree (tools.sessions.visibility=tree).",
|
||||
"Session status visibility is restricted to the current session tree and any watched same-agent group sessions (tools.sessions.visibility=tree).",
|
||||
);
|
||||
|
||||
expect(loadSessionStoreMock).not.toHaveBeenCalled();
|
||||
@@ -2334,7 +2334,7 @@ describe("session_status tool", () => {
|
||||
model: "default",
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
"Session status visibility is restricted to the current session tree (tools.sessions.visibility=tree).",
|
||||
"Session status visibility is restricted to the current session tree and any watched same-agent group sessions (tools.sessions.visibility=tree).",
|
||||
);
|
||||
|
||||
expect(updateSessionStoreMock).not.toHaveBeenCalled();
|
||||
|
||||
@@ -1840,4 +1840,61 @@ describe("buildSubagentSystemPrompt", () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("watched sessions prompt surfaces", () => {
|
||||
it("renders prepared watched sessions with titles, overflow, and recall guidance", () => {
|
||||
const prompt = buildAgentSystemPrompt({
|
||||
workspaceDir: "/tmp/openclaw",
|
||||
toolNames: ["sessions_list", "sessions_history", "sessions_search"],
|
||||
preparedWatchedSessions: {
|
||||
sessions: [
|
||||
{ key: "agent:main:telegram:group:alpha", title: "Family group" },
|
||||
{ key: "agent:main:telegram:group:beta" },
|
||||
],
|
||||
hiddenCount: 1,
|
||||
readToolNames: ["sessions_history", "sessions_search"],
|
||||
listToolAvailable: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(prompt).toContain("## Watched Sessions");
|
||||
expect(prompt).toContain(
|
||||
"Readable now (read-only) via sessions_history/sessions_search; rows appear in sessions_list.",
|
||||
);
|
||||
expect(prompt).toContain("- agent:main:telegram:group:alpha — Family group");
|
||||
expect(prompt).toContain("- agent:main:telegram:group:beta");
|
||||
expect(prompt).toContain('(+1 more: sessions_list kinds=["group"].)');
|
||||
expect(prompt).toContain(
|
||||
"Asked about another chat/group/session not in context: check `sessions_list`/`sessions_search` before claiming no access.",
|
||||
);
|
||||
});
|
||||
|
||||
it("names only granted read tools and skips the sessions_list overflow hint without it", () => {
|
||||
const prompt = buildAgentSystemPrompt({
|
||||
workspaceDir: "/tmp/openclaw",
|
||||
toolNames: ["sessions_history"],
|
||||
preparedWatchedSessions: {
|
||||
sessions: [{ key: "agent:main:telegram:group:alpha" }],
|
||||
hiddenCount: 2,
|
||||
readToolNames: ["sessions_history"],
|
||||
listToolAvailable: false,
|
||||
},
|
||||
});
|
||||
|
||||
expect(prompt).toContain("Readable now (read-only) via sessions_history.");
|
||||
expect(prompt).not.toContain("rows appear in sessions_list");
|
||||
expect(prompt).toContain("(+2 more.)");
|
||||
expect(prompt).not.toContain('sessions_list kinds=["group"]');
|
||||
});
|
||||
|
||||
it("omits the watched section and recall line without prepared data or session tools", () => {
|
||||
const prompt = buildAgentSystemPrompt({
|
||||
workspaceDir: "/tmp/openclaw",
|
||||
toolNames: ["read", "exec"],
|
||||
});
|
||||
|
||||
expect(prompt).not.toContain("## Watched Sessions");
|
||||
expect(prompt).not.toContain("before claiming no access");
|
||||
});
|
||||
});
|
||||
/* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */
|
||||
|
||||
@@ -62,6 +62,10 @@ import type {
|
||||
ProviderSystemPromptSectionId,
|
||||
} from "./system-prompt-contribution.js";
|
||||
import type { PromptMode, SilentReplyPromptMode } from "./system-prompt.types.js";
|
||||
import {
|
||||
buildWatchedSessionsPromptLines,
|
||||
type PreparedWatchedSessionsPrompt,
|
||||
} from "./watched-sessions-prompt.js";
|
||||
|
||||
/**
|
||||
* Controls which hardcoded sections are included in the system prompt.
|
||||
@@ -812,6 +816,8 @@ export function buildAgentSystemPrompt(params: {
|
||||
memoryCitationsMode?: MemoryCitationsMode;
|
||||
/** Immutable memory state prepared before synchronous prompt assembly. */
|
||||
preparedMemoryPrompt?: PreparedMemoryPromptSection;
|
||||
/** Watched same-agent group sessions prepared before synchronous prompt assembly. */
|
||||
preparedWatchedSessions?: PreparedWatchedSessionsPrompt;
|
||||
promptContribution?: ProviderSystemPromptContribution;
|
||||
}) {
|
||||
const acpEnabled = params.acpEnabled === true;
|
||||
@@ -850,8 +856,8 @@ export function buildAgentSystemPrompt(params: {
|
||||
agents_list: acpSpawnRuntimeEnabled
|
||||
? "List allowed OpenClaw subagent ids; not ACP ids"
|
||||
: "List allowed subagent ids",
|
||||
sessions_list: "List other sessions/subagents; filters/last",
|
||||
sessions_history: "Read other session/subagent history",
|
||||
sessions_list: "List visible sessions; filters/last",
|
||||
sessions_history: "Read visible session/subagent history",
|
||||
sessions_search: "Search past sessions; use sessionKey with sessions_history",
|
||||
sessions_send: "Message other session/subagent",
|
||||
sessions_spawn: acpSpawnRuntimeEnabled
|
||||
@@ -1178,6 +1184,12 @@ export function buildAgentSystemPrompt(params: {
|
||||
: "Never loop-poll `subagents list`/`sessions_list`; status only on-demand/intervention/debug/request.",
|
||||
]
|
||||
: []),
|
||||
...(renderOpenClawToolWorkflowHints &&
|
||||
(availableTools.has("sessions_search") || availableTools.has("sessions_list"))
|
||||
? [
|
||||
"Asked about another chat/group/session not in context: check `sessions_list`/`sessions_search` before claiming no access.",
|
||||
]
|
||||
: []),
|
||||
"",
|
||||
...buildProactiveSubagentOrchestrationSection({
|
||||
enabled: proactiveSubagentOrchestration,
|
||||
@@ -1423,6 +1435,10 @@ export function buildAgentSystemPrompt(params: {
|
||||
lines.push(providerDynamicSuffix, "");
|
||||
}
|
||||
|
||||
// Watched sessions change rarely but per-session; keep them below the cache
|
||||
// boundary so the shared stable prefix stays byte-identical across sessions.
|
||||
lines.push(...buildWatchedSessionsPromptLines(params.preparedWatchedSessions));
|
||||
|
||||
lines.push(...buildHeartbeatSection({ isMinimal, heartbeatPrompt }));
|
||||
|
||||
lines.push(
|
||||
|
||||
@@ -16,6 +16,33 @@ export const ASK_USER_TOOL_DISPLAY_SUMMARY = "Ask the user and wait for an answe
|
||||
export const SPAWN_TASK_TOOL_DISPLAY_SUMMARY = "Suggest follow-up work for operator approval.";
|
||||
export const DISMISS_TASK_TOOL_DISPLAY_SUMMARY = "Withdraw a pending task suggestion.";
|
||||
|
||||
// Mirrors plugin-sdk SessionToolsVisibility; kept local because importing that
|
||||
// module here would close an agents<->plugin-sdk madge cycle. Call sites pass
|
||||
// the policy union, so a new mode fails compilation at every consumer.
|
||||
type SessionVisibilityScope = "self" | "tree" | "agent" | "all";
|
||||
|
||||
// Single source for model-facing session-visibility scope wording; every tool
|
||||
// description or warning that explains visibility renders through this so the
|
||||
// prose cannot drift from the session-visibility checker (openclaw#114797).
|
||||
const SESSION_VISIBILITY_SCOPE_COPY = {
|
||||
self: "current session only",
|
||||
tree: "current session + own spawn subtree; reads also cover any watched same-agent group sessions",
|
||||
agent: "all sessions of this agent",
|
||||
all: "all sessions, cross-agent per tools.agentToAgent",
|
||||
} satisfies Record<SessionVisibilityScope, string>;
|
||||
|
||||
export function describeSessionVisibilityScope(
|
||||
visibility: SessionVisibilityScope,
|
||||
options?: { spawnRestricted?: boolean },
|
||||
): string {
|
||||
// Sandboxed sessions under the "spawned" clamp list/read only spawned rows,
|
||||
// so the tree watched-read clause would promise reads that context denies.
|
||||
if (options?.spawnRestricted && visibility === "tree") {
|
||||
return "current session + own spawn subtree (sandbox: spawned sessions only)";
|
||||
}
|
||||
return SESSION_VISIBILITY_SCOPE_COPY[visibility];
|
||||
}
|
||||
|
||||
/** Describes the sessions_list tool for model-facing instructions. */
|
||||
export function describeSessionsListTool(): string {
|
||||
return [
|
||||
@@ -57,7 +84,14 @@ export function describeSessionsSpawnTool(options?: {
|
||||
acpAvailable?: boolean;
|
||||
threadAvailable?: boolean;
|
||||
swarmEnabled?: boolean;
|
||||
sessionToolsVisibility?: SessionVisibilityScope;
|
||||
spawnRestricted?: boolean;
|
||||
}): string {
|
||||
// Callers that resolve the effective visibility get it rendered as fact;
|
||||
// without it the copy must keep the "default" hedge instead of asserting tree.
|
||||
const visibilityLine = options?.sessionToolsVisibility
|
||||
? `Session listing/addressing obeys \`tools.sessions.visibility\` (${options.sessionToolsVisibility}: ${describeSessionVisibilityScope(options.sessionToolsVisibility, { spawnRestricted: options.spawnRestricted })}).`
|
||||
: `Session listing/addressing obeys \`tools.sessions.visibility\` (\`tree\` default: ${describeSessionVisibilityScope("tree")}).`;
|
||||
const runtimeDescription =
|
||||
options?.acpAvailable === false
|
||||
? 'Spawn clean child; default `runtime="subagent"`.'
|
||||
@@ -76,7 +110,7 @@ export function describeSessionsSpawnTool(options?: {
|
||||
: '`mode="run"` one-shot background.',
|
||||
"`agentId` targets a configured agent (see agents_list); `model` overrides its model; `cleanup` delete|keep hidden child session; `sandbox` inherit|require.",
|
||||
'`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; may check out a git worktree via `worktree`/`worktreeName`/`worktreeBaseRef`.',
|
||||
"Session listing/addressing obeys `tools.sessions.visibility` (`tree` default: current + own spawn subtree).",
|
||||
visibilityLine,
|
||||
...(options?.swarmEnabled
|
||||
? [
|
||||
"`collect=true` (swarm): parallel fan-out collector children; structured result per `outputSchema`; `groupId` groups a batch; await with agents_wait.",
|
||||
|
||||
@@ -744,7 +744,9 @@ export function createSessionStatusTool(opts?: {
|
||||
visibilitySessionKey: requestedKeyInput,
|
||||
});
|
||||
if (!visibleSession.ok) {
|
||||
throw new Error("Session status visibility is restricted to the current session tree.");
|
||||
// The resolver's copy already names the denying policy (including the
|
||||
// watched-group carve-out); a local string here would drift from it.
|
||||
throw new Error(visibleSession.error);
|
||||
}
|
||||
// If resolution points at another agent, enforce A2A policy before switching stores.
|
||||
ensureAgentAccess(
|
||||
|
||||
@@ -273,13 +273,13 @@ describe("createSessionVisibilityGuard", () => {
|
||||
allowed: false,
|
||||
status: "forbidden",
|
||||
error:
|
||||
"Session history visibility is restricted to the current session tree (tools.sessions.visibility=tree).",
|
||||
"Session history visibility is restricted to the current session tree and any watched same-agent group sessions (tools.sessions.visibility=tree).",
|
||||
});
|
||||
expect(guard.check({ key: explicitDirectSessionKey })).toEqual({
|
||||
allowed: false,
|
||||
status: "forbidden",
|
||||
error:
|
||||
"Session history visibility is restricted to the current session tree (tools.sessions.visibility=tree).",
|
||||
"Session history visibility is restricted to the current session tree and any watched same-agent group sessions (tools.sessions.visibility=tree).",
|
||||
});
|
||||
expect(guard.check({ key: crossAgentSessionKey })).toEqual({
|
||||
allowed: false,
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
} from "../schema/typebox.js";
|
||||
import {
|
||||
describeSessionsListTool,
|
||||
describeSessionVisibilityScope,
|
||||
SESSIONS_LIST_TOOL_DISPLAY_SUMMARY,
|
||||
} from "../tool-description-presets.js";
|
||||
import { stripToolMessages } from "./chat-history-text.js";
|
||||
@@ -462,7 +463,7 @@ export function createSessionsListTool(opts?: {
|
||||
: {
|
||||
mode: visibility,
|
||||
restricted: true,
|
||||
warning: `Session visibility is restricted (effective tools.sessions.visibility=${visibility}). Results may omit sessions outside the current scope. The count field reflects only sessions within the current scope.`,
|
||||
warning: `Session visibility is restricted (effective tools.sessions.visibility=${visibility}: ${describeSessionVisibilityScope(visibility, { spawnRestricted: restrictToSpawned })}). Sessions outside that scope are omitted from results and count.`,
|
||||
};
|
||||
|
||||
return jsonResult({
|
||||
|
||||
@@ -48,6 +48,10 @@ import {
|
||||
readStringParam,
|
||||
ToolInputError,
|
||||
} from "./common.js";
|
||||
import {
|
||||
resolveEffectiveSessionToolsVisibility,
|
||||
resolveSandboxedSessionToolContext,
|
||||
} from "./sessions-helpers.js";
|
||||
import {
|
||||
maybeSpawnVisibleSession,
|
||||
type VisibleSessionsSpawnDeps,
|
||||
@@ -290,6 +294,16 @@ export function createSessionsSpawnTool(
|
||||
const requesterAgentId =
|
||||
opts?.requesterAgentIdOverride ?? parseAgentSessionKey(opts?.agentSessionKey)?.agentId;
|
||||
const swarmConfig = resolveSwarmConfig(opts?.config, requesterAgentId);
|
||||
const visibilityCfg = opts?.config ?? getRuntimeConfig();
|
||||
const sessionToolsVisibility = resolveEffectiveSessionToolsVisibility({
|
||||
cfg: visibilityCfg,
|
||||
sandboxed: opts?.sandboxed === true,
|
||||
});
|
||||
const { restrictToSpawned } = resolveSandboxedSessionToolContext({
|
||||
cfg: visibilityCfg,
|
||||
agentSessionKey: opts?.agentSessionKey,
|
||||
sandboxed: opts?.sandboxed,
|
||||
});
|
||||
return {
|
||||
label: "Sessions",
|
||||
name: "sessions_spawn",
|
||||
@@ -300,6 +314,8 @@ export function createSessionsSpawnTool(
|
||||
acpAvailable,
|
||||
threadAvailable,
|
||||
swarmEnabled: swarmConfig.enabled,
|
||||
sessionToolsVisibility,
|
||||
spawnRestricted: restrictToSpawned,
|
||||
}),
|
||||
parameters: createSessionsSpawnToolSchema({
|
||||
acpAvailable,
|
||||
|
||||
@@ -655,7 +655,7 @@ describe("sessions_list gating", () => {
|
||||
mode: "tree",
|
||||
restricted: true,
|
||||
warning:
|
||||
"Session visibility is restricted (effective tools.sessions.visibility=tree). Results may omit sessions outside the current scope. The count field reflects only sessions within the current scope.",
|
||||
"Session visibility is restricted (effective tools.sessions.visibility=tree: current session + own spawn subtree; reads also cover any watched same-agent group sessions). Sessions outside that scope are omitted from results and count.",
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
153
src/agents/watched-sessions-prompt.test.ts
Normal file
153
src/agents/watched-sessions-prompt.test.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import { afterAll, afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { cleanupTempDirs, makeTempDir } from "../../test/helpers/temp-dir.js";
|
||||
import { upsertSessionEntry } from "../config/sessions/session-accessor.js";
|
||||
import { buildWatchedSessionsHarnessContext } from "../plugin-sdk/agent-harness-runtime.js";
|
||||
import { registerMainSessionGroupWatch } from "../sessions/session-state-events.js";
|
||||
import { closeOpenClawAgentDatabasesForTest } from "../state/openclaw-agent-db.js";
|
||||
import { closeOpenClawStateDatabaseForTest } from "../state/openclaw-state-db.js";
|
||||
import { prepareWatchedSessionsPrompt } from "./watched-sessions-prompt.js";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
const mainSessionKey = "agent:main:main";
|
||||
const sessionReadTools = ["sessions_history", "sessions_search", "sessions_list"];
|
||||
|
||||
function stubStateDir() {
|
||||
const stateDir = makeTempDir(tempDirs, "openclaw-watched-sessions-");
|
||||
vi.stubEnv("OPENCLAW_STATE_DIR", stateDir);
|
||||
}
|
||||
|
||||
function watchGroup(sessionKey: string) {
|
||||
expect(registerMainSessionGroupWatch({ sessionKey, agentId: "main", dmScope: "main" })).toBe(
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
closeOpenClawStateDatabaseForTest();
|
||||
closeOpenClawAgentDatabasesForTest();
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
cleanupTempDirs(tempDirs);
|
||||
});
|
||||
|
||||
describe("prepareWatchedSessionsPrompt", () => {
|
||||
it("returns key-sorted watched sessions with store-derived titles", async () => {
|
||||
stubStateDir();
|
||||
watchGroup("agent:main:telegram:group:beta");
|
||||
watchGroup("agent:main:telegram:group:alpha:topic:7");
|
||||
await upsertSessionEntry(
|
||||
{ sessionKey: "agent:main:telegram:group:beta" },
|
||||
{ sessionId: "session-beta", displayName: "Family group", updatedAt: 1 },
|
||||
);
|
||||
|
||||
const prepared = prepareWatchedSessionsPrompt({
|
||||
enabled: true,
|
||||
sessionKey: mainSessionKey,
|
||||
toolNames: sessionReadTools,
|
||||
});
|
||||
|
||||
expect(prepared).toEqual({
|
||||
sessions: [
|
||||
{ key: "agent:main:telegram:group:alpha:topic:7" },
|
||||
{ key: "agent:main:telegram:group:beta", title: "Family group" },
|
||||
],
|
||||
hiddenCount: 0,
|
||||
readToolNames: ["sessions_history", "sessions_search"],
|
||||
listToolAvailable: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("caps rendered rows and reports the overflow count", () => {
|
||||
stubStateDir();
|
||||
for (let index = 0; index < 23; index += 1) {
|
||||
watchGroup(`agent:main:telegram:group:room-${String(index).padStart(2, "0")}`);
|
||||
}
|
||||
|
||||
const prepared = prepareWatchedSessionsPrompt({
|
||||
enabled: true,
|
||||
sessionKey: mainSessionKey,
|
||||
toolNames: ["sessions_history"],
|
||||
});
|
||||
|
||||
expect(prepared?.sessions).toHaveLength(20);
|
||||
expect(prepared?.hiddenCount).toBe(3);
|
||||
expect(prepared?.sessions[0]?.key).toBe("agent:main:telegram:group:room-00");
|
||||
expect(prepared?.readToolNames).toEqual(["sessions_history"]);
|
||||
expect(prepared?.listToolAvailable).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts capability-provided read tools regardless of casing", () => {
|
||||
stubStateDir();
|
||||
watchGroup("agent:main:telegram:group:beta");
|
||||
|
||||
const prepared = prepareWatchedSessionsPrompt({
|
||||
enabled: true,
|
||||
sessionKey: mainSessionKey,
|
||||
toolNames: [],
|
||||
capabilityToolNames: [" Sessions_Search "],
|
||||
});
|
||||
|
||||
expect(prepared?.readToolNames).toEqual(["sessions_search"]);
|
||||
});
|
||||
|
||||
it("keeps the section for sandboxed sessions only when the clamp allows non-spawned reads", () => {
|
||||
stubStateDir();
|
||||
watchGroup("agent:main:telegram:group:beta");
|
||||
const base = {
|
||||
enabled: true,
|
||||
sessionKey: mainSessionKey,
|
||||
sandboxed: true,
|
||||
toolNames: sessionReadTools,
|
||||
};
|
||||
|
||||
expect(prepareWatchedSessionsPrompt({ ...base, config: {} })).toBe(undefined);
|
||||
expect(
|
||||
prepareWatchedSessionsPrompt({
|
||||
...base,
|
||||
config: { agents: { defaults: { sandbox: { sessionToolsVisibility: "all" } } } },
|
||||
})?.sessions,
|
||||
).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("returns undefined when disabled, keyless, non-main, toolless, or unwatched", () => {
|
||||
stubStateDir();
|
||||
watchGroup("agent:main:telegram:group:beta");
|
||||
const base = { enabled: true, sessionKey: mainSessionKey, toolNames: sessionReadTools };
|
||||
|
||||
expect(prepareWatchedSessionsPrompt({ ...base, enabled: false })).toBe(undefined);
|
||||
expect(prepareWatchedSessionsPrompt({ ...base, sessionKey: undefined })).toBe(undefined);
|
||||
expect(
|
||||
prepareWatchedSessionsPrompt({ ...base, sessionKey: "agent:main:subagent:worker" }),
|
||||
).toBe(undefined);
|
||||
expect(
|
||||
prepareWatchedSessionsPrompt({ ...base, sessionKey: "agent:main:telegram:group:beta" }),
|
||||
).toBe(undefined);
|
||||
expect(prepareWatchedSessionsPrompt({ ...base, toolNames: ["sessions_list"] })).toBe(undefined);
|
||||
expect(prepareWatchedSessionsPrompt({ ...base, sessionKey: "agent:other:main" })).toBe(
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it("renders the harness context block plugin-owned runtimes inject per turn", () => {
|
||||
stubStateDir();
|
||||
watchGroup("agent:main:telegram:group:beta");
|
||||
|
||||
const block = buildWatchedSessionsHarnessContext({
|
||||
sessionKey: mainSessionKey,
|
||||
toolNames: ["sessions_history"],
|
||||
});
|
||||
|
||||
expect(block).toBe(
|
||||
[
|
||||
"## Watched Sessions",
|
||||
"Group/topic sessions this session ambiently watches. Readable now (read-only) via sessions_history.",
|
||||
"- agent:main:telegram:group:beta",
|
||||
].join("\n"),
|
||||
);
|
||||
expect(buildWatchedSessionsHarnessContext({ sessionKey: mainSessionKey, toolNames: [] })).toBe(
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
118
src/agents/watched-sessions-prompt.ts
Normal file
118
src/agents/watched-sessions-prompt.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* Prepares the Watched Sessions system-prompt section (openclaw#114797).
|
||||
*
|
||||
* Ambient group watches make same-agent group sessions readable from the main
|
||||
* session, but the model only acts on that when the prompt names them. Prepare
|
||||
* runs before synchronous prompt assembly, mirroring prepareAgentMemoryPrompt.
|
||||
*/
|
||||
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
|
||||
import { loadExactSessionEntryReadOnly } from "../config/sessions/session-accessor.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { deriveSessionTitle } from "../gateway/session-utils.js";
|
||||
import { resolveSandboxSessionToolsVisibility } from "../plugin-sdk/session-visibility.js";
|
||||
import { buildAgentMainSessionKey, parseAgentSessionKey } from "../routing/session-key.js";
|
||||
import { listAmbientGroupWatchTargets } from "../sessions/session-state-events.js";
|
||||
import { sanitizeForPromptLiteral } from "./sanitize-for-prompt.js";
|
||||
|
||||
/** Watched-session facts resolved before synchronous prompt assembly. */
|
||||
export type PreparedWatchedSessionsPrompt = {
|
||||
/** Key-sorted capped rows; deterministic bytes keep the prompt cache stable. */
|
||||
sessions: Array<{ key: string; title?: string }>;
|
||||
/** Watch targets beyond the row cap; rendered as an overflow note. */
|
||||
hiddenCount: number;
|
||||
/** Granted read tools the section may name, in render order. */
|
||||
readToolNames: string[];
|
||||
listToolAvailable: boolean;
|
||||
};
|
||||
|
||||
// Caps prompt bytes for pathological watch counts; the render notes the overflow.
|
||||
const WATCHED_SESSIONS_PROMPT_LIMIT = 20;
|
||||
// Titles are external group names; clamp so one hostile rename cannot bloat the prompt.
|
||||
const WATCHED_SESSION_TITLE_MAX_CHARS = 80;
|
||||
|
||||
const WATCHED_SESSION_READ_TOOLS = ["sessions_history", "sessions_search"];
|
||||
|
||||
/** Resolve watched same-agent group sessions for the current session's prompt. */
|
||||
export function prepareWatchedSessionsPrompt(params: {
|
||||
enabled: boolean;
|
||||
config?: OpenClawConfig;
|
||||
sessionKey?: string;
|
||||
sandboxed?: boolean;
|
||||
toolNames: Iterable<string>;
|
||||
capabilityToolNames?: Iterable<string>;
|
||||
}): PreparedWatchedSessionsPrompt | undefined {
|
||||
const sessionKey = params.sessionKey?.trim();
|
||||
if (!params.enabled || !sessionKey) {
|
||||
return undefined;
|
||||
}
|
||||
// Ambient watch cursors are written only for buildAgentMainSessionKey watchers
|
||||
// (registerMainSessionGroupWatch), so any other session key can skip the probe.
|
||||
const parsedKey = parseAgentSessionKey(sessionKey);
|
||||
if (!parsedKey || buildAgentMainSessionKey({ agentId: parsedKey.agentId }) !== sessionKey) {
|
||||
return undefined;
|
||||
}
|
||||
// Sandboxed sessions with the default "spawned" clamp list/read only spawned
|
||||
// rows, so the section would advertise reads that context cannot make. The
|
||||
// "all" clamp lifts that restriction and keeps the section.
|
||||
if (params.sandboxed && resolveSandboxSessionToolsVisibility(params.config ?? {}) === "spawned") {
|
||||
return undefined;
|
||||
}
|
||||
const availableTools = new Set(
|
||||
[...params.toolNames, ...(params.capabilityToolNames ?? [])]
|
||||
.map((tool) => tool.trim().toLowerCase())
|
||||
.filter(Boolean),
|
||||
);
|
||||
const readToolNames = WATCHED_SESSION_READ_TOOLS.filter((tool) => availableTools.has(tool));
|
||||
if (readToolNames.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
// Sorted by key, not recency: recency-ordered rows would reshuffle prompt
|
||||
// bytes on every group message and defeat provider prompt caching.
|
||||
const targets = [...listAmbientGroupWatchTargets(sessionKey)].toSorted();
|
||||
if (targets.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
const sessions = targets.slice(0, WATCHED_SESSIONS_PROMPT_LIMIT).map((key) => {
|
||||
const row: { key: string; title?: string } = { key };
|
||||
// Exact persisted-key probe: watch cursors store canonical keys, so the
|
||||
// alias-resolving loader's full-snapshot scan is wasted work here.
|
||||
const entry = loadExactSessionEntryReadOnly({ sessionKey: key, clone: false })?.entry;
|
||||
const title = deriveSessionTitle(entry);
|
||||
if (title) {
|
||||
row.title = truncateUtf16Safe(title, WATCHED_SESSION_TITLE_MAX_CHARS);
|
||||
}
|
||||
return row;
|
||||
});
|
||||
return {
|
||||
sessions,
|
||||
hiddenCount: targets.length - sessions.length,
|
||||
readToolNames,
|
||||
listToolAvailable: availableTools.has("sessions_list"),
|
||||
};
|
||||
}
|
||||
|
||||
/** Renders the shared Watched Sessions block used by every prompt-assembly surface. */
|
||||
export function buildWatchedSessionsPromptLines(
|
||||
prepared?: PreparedWatchedSessionsPrompt,
|
||||
): string[] {
|
||||
if (!prepared || prepared.sessions.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const listHint = prepared.listToolAvailable ? "; rows appear in sessions_list" : "";
|
||||
return [
|
||||
"## Watched Sessions",
|
||||
`Group/topic sessions this session ambiently watches. Readable now (read-only) via ${prepared.readToolNames.join("/")}${listHint}.`,
|
||||
...prepared.sessions.map((session) => {
|
||||
const title = session.title ? ` — ${sanitizeForPromptLiteral(session.title)}` : "";
|
||||
return `- ${sanitizeForPromptLiteral(session.key)}${title}`;
|
||||
}),
|
||||
...(prepared.hiddenCount > 0
|
||||
? [
|
||||
prepared.listToolAvailable
|
||||
? `(+${prepared.hiddenCount} more: sessions_list kinds=["group"].)`
|
||||
: `(+${prepared.hiddenCount} more.)`,
|
||||
]
|
||||
: []),
|
||||
"",
|
||||
];
|
||||
}
|
||||
@@ -28,6 +28,11 @@ import {
|
||||
} from "../agents/embedded-agent-runner/runs.js";
|
||||
import type { SandboxFsBridge } from "../agents/sandbox/fs-bridge.js";
|
||||
import { formatToolDetail, resolveToolDisplay } from "../agents/tool-display.js";
|
||||
import {
|
||||
buildWatchedSessionsPromptLines,
|
||||
prepareWatchedSessionsPrompt,
|
||||
} from "../agents/watched-sessions-prompt.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import type { ImageContent } from "../llm/types.js";
|
||||
import { redactToolDetail } from "../logging/redact.js";
|
||||
import type { PromptImageOrderEntry } from "../media/prompt-image-order.js";
|
||||
@@ -36,6 +41,25 @@ import { truncateUtf16Safe } from "../utils.js";
|
||||
/** Default truncation limit for user-facing tool progress output. */
|
||||
export const TOOL_PROGRESS_OUTPUT_MAX_CHARS = 8_000;
|
||||
|
||||
/**
|
||||
* Renders the Watched Sessions prompt block for plugin-owned harness prompts.
|
||||
* Harness runtimes that assemble their own instruction layers (e.g. Codex)
|
||||
* must surface the same watched-session facts as the embedded prompt, or the
|
||||
* model keeps refusing cross-session questions on those runtimes (openclaw#114797).
|
||||
*/
|
||||
export function buildWatchedSessionsHarnessContext(params: {
|
||||
config?: OpenClawConfig;
|
||||
sessionKey?: string;
|
||||
sandboxed?: boolean;
|
||||
toolNames: Iterable<string>;
|
||||
capabilityToolNames?: Iterable<string>;
|
||||
}): string | undefined {
|
||||
const lines = buildWatchedSessionsPromptLines(
|
||||
prepareWatchedSessionsPrompt({ enabled: true, ...params }),
|
||||
);
|
||||
return lines.length > 0 ? lines.join("\n").trimEnd() : undefined;
|
||||
}
|
||||
|
||||
export { FAST_MODE_AUTO_PROGRESS_KIND } from "../auto-reply/reply-payload.js";
|
||||
export {
|
||||
isDeliveredMessageToolOnlySourceReplyResult,
|
||||
|
||||
@@ -302,7 +302,12 @@ function selfVisibilityMessage(action: SessionAccessAction): string {
|
||||
}
|
||||
|
||||
function treeVisibilityMessage(action: SessionAccessAction): string {
|
||||
return `${actionPrefix(action)} visibility is restricted to the current session tree (tools.sessions.visibility=tree).`;
|
||||
// Reads under tree also cover watched same-agent group sessions (isWatchedRead
|
||||
// below); the deny copy must say so or the model concludes group reads never work.
|
||||
if (action === "send") {
|
||||
return `${actionPrefix(action)} visibility is restricted to the current session tree (tools.sessions.visibility=tree).`;
|
||||
}
|
||||
return `${actionPrefix(action)} visibility is restricted to the current session tree and any watched same-agent group sessions (tools.sessions.visibility=tree).`;
|
||||
}
|
||||
|
||||
/** Create a direct session-key visibility checker for one requester/action pair. */
|
||||
|
||||
@@ -138,7 +138,7 @@
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"description": "Spawn clean child; default `runtime=\"subagent\"`. `mode=\"run\"` one-shot; `mode=\"session\"` persistent/thread-bound only on supporting requester channel. `agentId` targets a configured agent (see agents_list); `model` overrides its model; `cleanup` delete|keep hidden child session; `sandbox` inherit|require. `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; may check out a git worktree via `worktree`/`worktreeName`/`worktreeBaseRef`. 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.",
|
||||
"description": "Spawn clean child; default `runtime=\"subagent\"`. `mode=\"run\"` one-shot; `mode=\"session\"` persistent/thread-bound only on supporting requester channel. `agentId` targets a configured agent (see agents_list); `model` overrides its model; `cleanup` delete|keep hidden child session; `sandbox` inherit|require. `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; may check out a git worktree via `worktree`/`worktreeName`/`worktreeBaseRef`. Session listing/addressing obeys `tools.sessions.visibility` (tree: current session + own spawn subtree; reads also cover any watched same-agent group sessions). 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": {
|
||||
|
||||
@@ -138,7 +138,7 @@
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"description": "Spawn clean child; default `runtime=\"subagent\"`. `mode=\"run\"` one-shot background. `agentId` targets a configured agent (see agents_list); `model` overrides its model; `cleanup` delete|keep hidden child session; `sandbox` inherit|require. `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; may check out a git worktree via `worktree`/`worktreeName`/`worktreeBaseRef`. 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.",
|
||||
"description": "Spawn clean child; default `runtime=\"subagent\"`. `mode=\"run\"` one-shot background. `agentId` targets a configured agent (see agents_list); `model` overrides its model; `cleanup` delete|keep hidden child session; `sandbox` inherit|require. `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; may check out a git worktree via `worktree`/`worktreeName`/`worktreeBaseRef`. Session listing/addressing obeys `tools.sessions.visibility` (tree: current session + own spawn subtree; reads also cover any watched same-agent group sessions). 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": {
|
||||
|
||||
@@ -138,7 +138,7 @@
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"description": "Spawn clean child; default `runtime=\"subagent\"`. `mode=\"run\"` one-shot background. `agentId` targets a configured agent (see agents_list); `model` overrides its model; `cleanup` delete|keep hidden child session; `sandbox` inherit|require. `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; may check out a git worktree via `worktree`/`worktreeName`/`worktreeBaseRef`. 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.",
|
||||
"description": "Spawn clean child; default `runtime=\"subagent\"`. `mode=\"run\"` one-shot background. `agentId` targets a configured agent (see agents_list); `model` overrides its model; `cleanup` delete|keep hidden child session; `sandbox` inherit|require. `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; may check out a git worktree via `worktree`/`worktreeName`/`worktreeBaseRef`. Session listing/addressing obeys `tools.sessions.visibility` (tree: current session + own spawn subtree; reads also cover any watched same-agent group sessions). 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": {
|
||||
|
||||
@@ -221,8 +221,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the
|
||||
"roughTokens": 0
|
||||
},
|
||||
"dynamicToolsJson": {
|
||||
"chars": 61383,
|
||||
"roughTokens": 15346
|
||||
"chars": 61437,
|
||||
"roughTokens": 15360
|
||||
},
|
||||
"openClawDeveloperInstructions": {
|
||||
"chars": 3471,
|
||||
@@ -233,8 +233,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the
|
||||
"roughTokens": 6964
|
||||
},
|
||||
"totalWithDynamicToolsJson": {
|
||||
"chars": 89239,
|
||||
"roughTokens": 22310
|
||||
"chars": 89293,
|
||||
"roughTokens": 22324
|
||||
},
|
||||
"userInputText": {
|
||||
"chars": 1300,
|
||||
|
||||
@@ -221,8 +221,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the
|
||||
"roughTokens": 0
|
||||
},
|
||||
"dynamicToolsJson": {
|
||||
"chars": 61075,
|
||||
"roughTokens": 15269
|
||||
"chars": 61129,
|
||||
"roughTokens": 15283
|
||||
},
|
||||
"openClawDeveloperInstructions": {
|
||||
"chars": 2362,
|
||||
@@ -233,8 +233,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the
|
||||
"roughTokens": 6594
|
||||
},
|
||||
"totalWithDynamicToolsJson": {
|
||||
"chars": 87451,
|
||||
"roughTokens": 21863
|
||||
"chars": 87505,
|
||||
"roughTokens": 21877
|
||||
},
|
||||
"userInputText": {
|
||||
"chars": 929,
|
||||
|
||||
@@ -222,8 +222,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the
|
||||
"roughTokens": 0
|
||||
},
|
||||
"dynamicToolsJson": {
|
||||
"chars": 62607,
|
||||
"roughTokens": 15652
|
||||
"chars": 62661,
|
||||
"roughTokens": 15666
|
||||
},
|
||||
"openClawDeveloperInstructions": {
|
||||
"chars": 2362,
|
||||
@@ -234,8 +234,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the
|
||||
"roughTokens": 6701
|
||||
},
|
||||
"totalWithDynamicToolsJson": {
|
||||
"chars": 89412,
|
||||
"roughTokens": 22353
|
||||
"chars": 89466,
|
||||
"roughTokens": 22367
|
||||
},
|
||||
"userInputText": {
|
||||
"chars": 1284,
|
||||
|
||||
Reference in New Issue
Block a user