refactor: share parent fork policy

This commit is contained in:
Peter Steinberger
2026-05-02 05:29:21 +01:00
parent d049af642a
commit 4f31cbbf55
18 changed files with 233 additions and 68 deletions

View File

@@ -5808,7 +5808,7 @@ Docs: https://docs.openclaw.ai
- Slack/Threading: when `replyToMode="all"` auto-threads top-level Slack DMs, seed the thread session key from the message `ts` so the initial message and later replies share the same isolated `:thread:` session instead of falling back to base DM context. (#26849) Thanks @calder-sandy.
- Agents/Subagents delivery: refactor subagent completion announce dispatch into an explicit queue/direct/fallback state machine, recover outbound channel-plugin resolution in cold/stale plugin-registry states across announce/message/gateway send paths, finalize cleanup bookkeeping when announce flow rejects, and treat Telegram sends without `message_id` as delivery failures (instead of false-success `"unknown"` IDs). (#26867, #25961, #26803, #25069, #26741) Thanks @SmithLabsLLC and @docaohieu2808.
- Telegram/Webhook: pre-initialize webhook bots, switch webhook processing to callback-mode JSON handling, and preserve full near-limit payload reads under delayed handlers to prevent webhook request hangs and dropped updates. (#26156).
- Slack/Session threads: prevent oversized parent-session inheritance from silently bricking new thread sessions, surface embedded context-overflow empty-result failures to users, and add configurable `session.parentForkMaxTokens` (default `100000`, `0` disables). (#26912) Thanks @markshields-tl.
- Slack/Session threads: prevent oversized parent-session inheritance from silently bricking new thread sessions, surface embedded context-overflow empty-result failures to users, and share the PI parent-fork fallback between channel threads and subagents. The legacy `session.parentForkMaxTokens` compatibility guard remains available but is no longer the preferred tuning surface. (#26912) Thanks @markshields-tl.
- Cron/Message multi-account routing: honor explicit `delivery.accountId` for isolated cron delivery resolution, and when `message.send` omits `accountId`, fall back to the sending agent's bound channel account instead of defaulting to the global account. (#27015, #26975) Thanks @lbo728 and @stakeswky.
- Gateway/Message media roots: thread `agentId` through gateway `send` RPC and prefer explicit `agentId` over session/default resolution so non-default agent workspace media sends no longer fail with `LocalMediaAccessError`; added regression coverage for agent precedence and blank-agent fallback. (#23249) Thanks @Sid-Qin.
- Followups/Routing: when explicit origin routing fails, allow same-channel fallback dispatch (while still blocking cross-channel fallback) so followup replies do not get dropped on transient origin-adapter failures. (#26109) Thanks @Sid-Qin.

View File

@@ -1,4 +1,4 @@
d0b1fc318d2f737c91c21ffffae2fe12197b4ba6d49859c4786ecbc586cf5a82 config-baseline.json
3f9c52903905d82d4b4ca9dbda530cac2e059870b08c69965099ebcd09a270a3 config-baseline.core.json
f40d6dea3b81c42d5af9e340f091d34b9e39321d171fade6115da594ba90522f config-baseline.json
40b1f91714d6f17e2718ea5d34c135550f5a39fe47f781cddc153cf3a59fe2e7 config-baseline.core.json
f42329d45c095881bd226bdb192c235980658fd250606d0c0badc2b12f12f5d3 config-baseline.channel.json
af71b84b2411d8ccabcc6e09de0ee41f8212ff9869a6677698b6e7e3afdfaa47 config-baseline.plugin.json

View File

@@ -1174,7 +1174,6 @@ See [Multi-Agent Sandbox & Tools](/tools/multi-agent-sandbox-tools) for preceden
},
resetTriggers: ["/new", "/reset"],
store: "~/.openclaw/agents/{agentId}/sessions/sessions.json",
parentForkMaxTokens: 100000, // skip parent-thread fork above this token count (0 disables)
maintenance: {
mode: "warn", // warn | enforce
pruneAfter: "30d",
@@ -1211,9 +1210,10 @@ See [Multi-Agent Sandbox & Tools](/tools/multi-agent-sandbox-tools) for preceden
- **`identityLinks`**: map canonical ids to provider-prefixed peers for cross-channel session sharing. Dock commands such as `/dock_discord` use the same map to switch the active session's reply route to another linked channel peer; see [Channel docking](/concepts/channel-docking).
- **`reset`**: primary reset policy. `daily` resets at `atHour` local time; `idle` resets after `idleMinutes`. When both configured, whichever expires first wins. Daily reset freshness uses the session row's `sessionStartedAt`; idle reset freshness uses `lastInteractionAt`. Background/system-event writes such as heartbeat, cron wakeups, exec notifications, and gateway bookkeeping can update `updatedAt`, but they do not keep daily/idle sessions fresh.
- **`resetByType`**: per-type overrides (`direct`, `group`, `thread`). Legacy `dm` accepted as alias for `direct`.
- **`parentForkMaxTokens`**: max parent-session `totalTokens` allowed when creating a forked thread session (default `100000`).
- If parent `totalTokens` is above this value, OpenClaw starts a fresh thread session instead of inheriting parent transcript history.
- Set `0` to disable this guard and always allow parent forking.
- **`parentForkMaxTokens`**: deprecated compatibility guard for the historical parent fork ceiling (default `100000`).
- Channel thread sessions and subagent `context="fork"` now use the same parent fork decision path.
- When the active parent branch is too large, OpenClaw starts with isolated context instead of failing or inheriting unusable history.
- Set `0` only if you intentionally want to disable this guard.
- **`mainKey`**: legacy field. Runtime always uses `"main"` for the main direct-chat bucket.
- **`agentToAgent.maxPingPongTurns`**: maximum reply-back turns between agents during agent-to-agent exchanges (integer, range: `0``5`). `0` disables ping-pong chaining.
- **`sendPolicy`**: match by `channel`, `chatType` (`direct|group|channel`, with legacy `dm` alias), `keyPrefix`, or `rawKeyPrefix`. First deny wins.

View File

@@ -148,7 +148,7 @@ Rules of thumb:
- **Daily reset** (default 4:00 AM local time on the gateway host) creates a new `sessionId` on the next message after the reset boundary.
- **Idle expiry** (`session.reset.idleMinutes` or legacy `session.idleMinutes`) creates a new `sessionId` when a message arrives after the idle window. When daily + idle are both configured, whichever expires first wins.
- **System events** (heartbeat, cron wakeups, exec notifications, gateway bookkeeping) may mutate the session row but do not extend daily/idle reset freshness. Reset rollover discards queued system-event notices for the previous session before the fresh prompt is built.
- **Thread parent fork guard** (`session.parentForkMaxTokens`, default `100000`) skips parent transcript forking when the parent session is already too large; the new thread starts fresh. Set `0` to disable.
- **Parent fork policy** uses PI's active branch when creating a thread or subagent fork. If that branch is too large, OpenClaw starts the child with isolated context instead of failing or inheriting unusable history. The legacy `session.parentForkMaxTokens` key remains as a deprecated compatibility guard; set `0` only if you intentionally want to disable it.
Implementation detail: the decision happens in `initSessionState()` in `src/auto-reply/reply/session.ts`.

View File

@@ -194,7 +194,10 @@ export async function getSessionsSpawnTool(opts: CreateOpenClawToolsOpts) {
compact: async () => ({ ok: true, compacted: false }),
ingest: async () => ({ ingested: false }),
}),
resolveParentForkMaxTokens: () => 100_000,
resolveParentForkDecision: async () => ({
status: "fork",
maxTokens: 100_000,
}),
forkSessionFromParent: async () => ({
sessionId: "forked-session-id",
sessionFile: "/tmp/forked-session.jsonl",

View File

@@ -115,6 +115,37 @@ describe("sessions_spawn context modes", () => {
);
});
it("falls back to isolated context when requested fork is too large", async () => {
const store: SessionStore = {
main: {
sessionId: "parent-session-id",
sessionFile: "/tmp/parent-session.jsonl",
updatedAt: 1,
totalTokens: 170_000,
},
};
usePersistentStoreMock(store);
const prepareSubagentSpawn = vi.fn(async () => undefined);
resolveContextEngineMock.mockResolvedValue({ prepareSubagentSpawn });
const result = await spawnSubagentDirect(
{ task: "inspect the current thread", context: "fork" },
{ agentSessionKey: "main" },
);
expect(result).toMatchObject({ status: "accepted", runId: "run-1" });
expect(result.note).toContain("Parent context is too large to fork");
expect(forkSessionFromParentMock).not.toHaveBeenCalled();
expect(prepareSubagentSpawn).toHaveBeenCalledWith(
expect.objectContaining({
parentSessionKey: "main",
childSessionKey: result.childSessionKey,
contextMode: "isolated",
parentSessionId: "parent-session-id",
}),
);
});
it("forks by default for thread-bound subagent sessions", async () => {
const store: SessionStore = {
main: {

View File

@@ -6,7 +6,8 @@ export { getRuntimeConfig } from "../config/config.js";
export { mergeSessionEntry, updateSessionStore } from "../config/sessions.js";
export {
forkSessionFromParent,
resolveParentForkMaxTokens,
resolveParentForkDecision,
type ParentForkDecision,
} from "../auto-reply/reply/session-fork.js";
export { ensureContextEnginesInitialized } from "../context-engine/init.js";
export { resolveContextEngine } from "../context-engine/registry.js";

View File

@@ -121,7 +121,7 @@ export async function loadSubagentSpawnModuleForTest(params: {
updateSessionStoreMock?: MockFn;
forkSessionFromParentMock?: MockFn;
resolveContextEngineMock?: MockFn;
resolveParentForkMaxTokensMock?: MockFn;
resolveParentForkDecisionMock?: MockFn;
pruneLegacyStoreKeysMock?: MockFn;
registerSubagentRunMock?: MockFn;
emitSessionLifecycleEventMock?: MockFn;
@@ -182,7 +182,37 @@ export async function loadSubagentSpawnModuleForTest(params: {
ensureContextEnginesInitialized:
params.ensureContextEnginesInitializedMock ?? (() => undefined),
resolveContextEngine: params.resolveContextEngineMock ?? (async () => ({})),
resolveParentForkMaxTokens: params.resolveParentForkMaxTokensMock ?? (() => 100_000),
resolveParentForkDecision:
params.resolveParentForkDecisionMock ??
(async (forkParams: {
cfg?: { session?: { parentForkMaxTokens?: unknown } };
parentEntry?: { totalTokens?: unknown };
}) => {
const configured = forkParams.cfg?.session?.parentForkMaxTokens;
const maxTokens =
typeof configured === "number" && Number.isFinite(configured) && configured >= 0
? Math.floor(configured)
: 100_000;
const parentTokens =
typeof forkParams.parentEntry?.totalTokens === "number" &&
Number.isFinite(forkParams.parentEntry.totalTokens)
? Math.floor(forkParams.parentEntry.totalTokens)
: undefined;
if (maxTokens > 0 && typeof parentTokens === "number" && parentTokens > maxTokens) {
return {
status: "skip",
reason: "parent-too-large",
maxTokens,
parentTokens,
message: `Parent context is too large to fork (${parentTokens}/${maxTokens} tokens); starting with isolated context instead.`,
};
}
return {
status: "fork",
maxTokens,
...(typeof parentTokens === "number" ? { parentTokens } : {}),
};
}),
mergeSessionEntry: (
current: Record<string, unknown> | undefined,
next: Record<string, unknown>,

View File

@@ -56,13 +56,13 @@ import {
normalizeDeliveryContext,
pruneLegacyStoreKeys,
ensureContextEnginesInitialized,
resolveParentForkDecision,
resolveAgentConfig,
resolveContextEngine,
resolveDisplaySessionKey,
resolveGatewaySessionStoreTarget,
resolveInternalSessionKey,
resolveMainSessionAlias,
resolveParentForkMaxTokens,
resolveSandboxRuntimeStatus,
updateSessionStore,
isAdminOnlyMethod,
@@ -96,7 +96,7 @@ type SubagentSpawnDeps = {
getRuntimeConfig: typeof getRuntimeConfig;
ensureContextEnginesInitialized: typeof ensureContextEnginesInitialized;
resolveContextEngine: typeof resolveContextEngine;
resolveParentForkMaxTokens: typeof resolveParentForkMaxTokens;
resolveParentForkDecision: typeof resolveParentForkDecision;
updateSessionStore: typeof updateSessionStore;
};
@@ -107,7 +107,7 @@ const defaultSubagentSpawnDeps: SubagentSpawnDeps = {
getRuntimeConfig,
ensureContextEnginesInitialized,
resolveContextEngine,
resolveParentForkMaxTokens,
resolveParentForkDecision,
updateSessionStore,
};
@@ -306,13 +306,20 @@ function resolveStoreEntryByKeys(
}
type PreparedSpawnContext =
| { status: "ok"; mode: "isolated"; parentEntry?: SessionEntry; childEntry?: SessionEntry }
| {
status: "ok";
mode: "isolated";
parentEntry?: SessionEntry;
childEntry?: SessionEntry;
forkFallbackNote?: string;
}
| {
status: "ok";
mode: "fork";
parentEntry: SessionEntry;
childEntry?: SessionEntry;
forked: { sessionId: string; sessionFile: string };
forkFallbackNote?: never;
}
| { status: "error"; error: string };
@@ -338,7 +345,7 @@ async function prepareSubagentSessionContext(params: {
let parentEntry: SessionEntry | undefined;
let childEntry: SessionEntry | undefined;
const forkMaxTokens = subagentSpawnDeps.resolveParentForkMaxTokens(params.cfg);
let forkFallbackNote: string | undefined;
const sessionsDir = path.dirname(parentTarget.storePath);
try {
@@ -356,14 +363,14 @@ async function prepareSubagentSessionContext(params: {
'context="fork" requested but the requester session transcript is not available.',
);
}
const parentTokens =
typeof parentEntry.totalTokens === "number" && Number.isFinite(parentEntry.totalTokens)
? parentEntry.totalTokens
: 0;
if (forkMaxTokens > 0 && parentTokens > forkMaxTokens) {
throw new Error(
`context="fork" requested but requester context is too large to fork (${parentTokens}/${forkMaxTokens} tokens). Use context="isolated" or compact first.`,
);
const forkDecision = await subagentSpawnDeps.resolveParentForkDecision({
cfg: params.cfg,
parentEntry,
storePath: parentTarget.storePath,
});
if (forkDecision.status === "skip") {
forkFallbackNote = forkDecision.message;
return null;
}
const fork = await subagentSpawnDeps.forkSessionFromParent({
@@ -392,6 +399,15 @@ async function prepareSubagentSessionContext(params: {
if (params.contextMode === "fork") {
if (!parentEntry || !forked) {
if (forkFallbackNote) {
return {
status: "ok",
mode: "isolated",
parentEntry,
childEntry,
forkFallbackNote,
};
}
return {
status: "error",
error: 'context="fork" requested but OpenClaw could not prepare forked context.',
@@ -405,7 +421,13 @@ async function prepareSubagentSessionContext(params: {
forked,
};
}
return { status: "ok", mode: "isolated", parentEntry, childEntry };
return {
status: "ok",
mode: "isolated",
parentEntry,
childEntry,
...(forkFallbackNote ? { forkFallbackNote } : {}),
};
} catch (err) {
return { status: "error", error: summarizeError(err) };
}
@@ -1276,15 +1298,18 @@ export async function spawnSubagentDirect(
label: label || undefined,
});
const acceptedNote = resolveSubagentSpawnAcceptedNote({
spawnMode,
agentSessionKey: ctx.agentSessionKey,
});
return {
status: "accepted",
childSessionKey,
runId: childRunId,
mode: spawnMode,
note: resolveSubagentSpawnAcceptedNote({
spawnMode,
agentSessionKey: ctx.agentSessionKey,
}),
note: preparedSpawnContext.forkFallbackNote
? `${acceptedNote} ${preparedSpawnContext.forkFallbackNote}`
: acceptedNote,
modelApplied: resolvedModel ? modelApplied : undefined,
attachments: attachmentsReceipt,
};

View File

@@ -9,11 +9,30 @@ import type { OpenClawConfig } from "../../config/types.openclaw.js";
const DEFAULT_PARENT_FORK_MAX_TOKENS = 100_000;
let sessionForkRuntimePromise: Promise<typeof import("./session-fork.runtime.js")> | null = null;
export type ParentForkDecision =
| {
status: "fork";
maxTokens: number;
parentTokens?: number;
}
| {
status: "skip";
reason: "parent-too-large";
maxTokens: number;
parentTokens: number;
message: string;
};
function loadSessionForkRuntime(): Promise<typeof import("./session-fork.runtime.js")> {
sessionForkRuntimePromise ??= import("./session-fork.runtime.js");
return sessionForkRuntimePromise;
}
/**
* Deprecated compatibility guard for deployments that explicitly tuned the
* historical thread fork ceiling. New behavior should use the shared parent
* fork decision helper so channel threads and subagents degrade the same way.
*/
export function resolveParentForkMaxTokens(cfg: OpenClawConfig): number {
const configured = cfg.session?.parentForkMaxTokens;
if (typeof configured === "number" && Number.isFinite(configured) && configured >= 0) {
@@ -22,6 +41,45 @@ export function resolveParentForkMaxTokens(cfg: OpenClawConfig): number {
return DEFAULT_PARENT_FORK_MAX_TOKENS;
}
export function formatParentForkTooLargeMessage(params: {
parentTokens: number;
maxTokens: number;
}): string {
return (
`Parent context is too large to fork (${params.parentTokens}/${params.maxTokens} tokens); ` +
"starting with isolated context instead."
);
}
export async function resolveParentForkDecision(params: {
cfg: OpenClawConfig;
parentEntry: SessionEntry;
storePath: string;
}): Promise<ParentForkDecision> {
const maxTokens = resolveParentForkMaxTokens(params.cfg);
if (maxTokens <= 0) {
return { status: "fork", maxTokens };
}
const parentTokens = await resolveParentForkTokenCount({
parentEntry: params.parentEntry,
storePath: params.storePath,
});
if (typeof parentTokens === "number" && parentTokens > maxTokens) {
return {
status: "skip",
reason: "parent-too-large",
maxTokens,
parentTokens,
message: formatParentForkTooLargeMessage({ parentTokens, maxTokens }),
};
}
return {
status: "fork",
maxTokens,
...(typeof parentTokens === "number" ? { parentTokens } : {}),
};
}
export async function forkSessionFromParent(params: {
parentEntry: SessionEntry;
agentId: string;

View File

@@ -52,11 +52,37 @@ vi.mock("./session-fork.js", () => ({
sessionForkMocks.forkSessionFromParent(...args),
resolveParentForkTokenCount: (...args: [{ parentEntry: SessionEntry; storePath: string }]) =>
sessionForkMocks.resolveParentForkTokenCount(...args),
resolveParentForkMaxTokens: (cfg: { session?: { parentForkMaxTokens?: unknown } }) => {
const configured = cfg.session?.parentForkMaxTokens;
return typeof configured === "number" && Number.isFinite(configured) && configured >= 0
? Math.floor(configured)
: 100_000;
resolveParentForkDecision: async (params: {
cfg: { session?: { parentForkMaxTokens?: unknown } };
parentEntry: SessionEntry;
storePath: string;
}) => {
const configured = params.cfg.session?.parentForkMaxTokens;
const maxTokens =
typeof configured === "number" && Number.isFinite(configured) && configured >= 0
? Math.floor(configured)
: 100_000;
if (maxTokens <= 0) {
return { status: "fork", maxTokens };
}
const parentTokens = await sessionForkMocks.resolveParentForkTokenCount({
parentEntry: params.parentEntry,
storePath: params.storePath,
});
if (typeof parentTokens === "number" && parentTokens > maxTokens) {
return {
status: "skip",
reason: "parent-too-large",
maxTokens,
parentTokens,
message: `Parent context is too large to fork (${parentTokens}/${maxTokens} tokens); starting with isolated context instead.`,
};
}
return {
status: "fork",
maxTokens,
...(typeof parentTokens === "number" ? { parentTokens } : {}),
};
},
}));

View File

@@ -60,11 +60,7 @@ import {
resolveLastChannelRaw,
resolveLastToRaw,
} from "./session-delivery.js";
import {
forkSessionFromParent,
resolveParentForkMaxTokens,
resolveParentForkTokenCount,
} from "./session-fork.js";
import { forkSessionFromParent, resolveParentForkDecision } from "./session-fork.js";
import { buildSessionEndHookPayload, buildSessionStartHookPayload } from "./session-hooks.js";
import { clearSessionResetRuntimeState } from "./session-reset-cleanup.js";
@@ -274,7 +270,6 @@ export async function initSessionState(params: {
const resetTriggers = sessionCfg?.resetTriggers?.length
? sessionCfg.resetTriggers
: DEFAULT_RESET_TRIGGERS;
const parentForkMaxTokens = resolveParentForkMaxTokens(cfg);
const sessionScope = sessionCfg?.scope ?? "per-sender";
const storePath = resolveStorePath(sessionCfg?.store, { agentId });
const ingressTimingEnabled = process.env.OPENCLAW_DEBUG_INGRESS_TIMING === "1";
@@ -705,30 +700,23 @@ export async function initSessionState(params: {
!alreadyForked
) {
const parentEntry = sessionStore[parentSessionKey];
const parentTokens =
parentForkMaxTokens > 0
? await resolveParentForkTokenCount({
parentEntry,
storePath,
})
: undefined;
if (
parentForkMaxTokens > 0 &&
typeof parentTokens === "number" &&
parentTokens > parentForkMaxTokens
) {
// Parent context is too large — forking would create a thread session
// that immediately overflows the model's context window. Start fresh
// instead and mark as forked to prevent re-attempts. See #26905.
const forkDecision = await resolveParentForkDecision({
cfg,
parentEntry,
storePath,
});
if (forkDecision.status === "skip") {
// The parent branch is too large to inherit usefully. Start fresh and
// mark as handled so the thread does not retry this decision every turn.
log.warn(
`skipping parent fork (parent too large): parentKey=${parentSessionKey} → sessionKey=${sessionKey} ` +
`parentTokens=${parentTokens} maxTokens=${parentForkMaxTokens}`,
`parentTokens=${forkDecision.parentTokens} maxTokens=${forkDecision.maxTokens}`,
);
sessionEntry.forkedFromParent = true;
} else {
log.warn(
`forking from parent session: parentKey=${parentSessionKey} → sessionKey=${sessionKey} ` +
`parentTokens=${parentTokens ?? "unknown"}`,
`parentTokens=${forkDecision.parentTokens ?? "unknown"}`,
);
const forked = await forkSessionFromParent({
parentEntry,

View File

@@ -20696,9 +20696,9 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
type: "integer",
minimum: 0,
maximum: 9007199254740991,
title: "Session Parent Fork Max Tokens",
title: "Deprecated Session Parent Fork Max Tokens",
description:
"Maximum parent-session token count allowed for thread/session inheritance forking. If the parent exceeds this, OpenClaw starts a fresh thread session instead of forking; set 0 to disable this protection.",
"Deprecated compatibility guard for the historical parent-session fork ceiling. OpenClaw now shares the parent fork decision between channel threads and subagents and falls back to isolated context when the active parent branch is too large; set 0 only to disable that guard intentionally.",
},
mainKey: {
type: "string",
@@ -27842,8 +27842,8 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
tags: ["storage"],
},
"session.parentForkMaxTokens": {
label: "Session Parent Fork Max Tokens",
help: "Maximum parent-session token count allowed for thread/session inheritance forking. If the parent exceeds this, OpenClaw starts a fresh thread session instead of forking; set 0 to disable this protection.",
label: "Deprecated Session Parent Fork Max Tokens",
help: "Deprecated compatibility guard for the historical parent-session fork ceiling. OpenClaw now shares the parent fork decision between channel threads and subagents and falls back to isolated context when the active parent branch is too large; set 0 only to disable that guard intentionally.",
tags: ["security", "auth", "performance", "storage"],
},
"session.mainKey": {

View File

@@ -1428,7 +1428,7 @@ export const FIELD_HELP: Record<string, string> = {
"session.typingMode":
'Controls typing behavior timing: "never", "instant", "thinking", or "message" based emission points. Keep conservative modes in high-volume channels to avoid unnecessary typing noise.',
"session.parentForkMaxTokens":
"Maximum parent-session token count allowed for thread/session inheritance forking. If the parent exceeds this, OpenClaw starts a fresh thread session instead of forking; set 0 to disable this protection.",
"Deprecated compatibility guard for the historical parent-session fork ceiling. OpenClaw now shares the parent fork decision between channel threads and subagents and falls back to isolated context when the active parent branch is too large; set 0 only to disable that guard intentionally.",
"session.mainKey":
'Overrides the canonical main session key used for continuity when dmScope or routing logic points to "main". Use a stable value only if you intentionally need custom session anchoring.',
"session.sendPolicy":

View File

@@ -706,7 +706,7 @@ export const FIELD_LABELS: Record<string, string> = {
"session.store": "Session Store Path",
"session.typingIntervalSeconds": "Session Typing Interval (seconds)",
"session.typingMode": "Session Typing Mode",
"session.parentForkMaxTokens": "Session Parent Fork Max Tokens",
"session.parentForkMaxTokens": "Deprecated Session Parent Fork Max Tokens",
"session.mainKey": "Session Main Key",
"session.sendPolicy": "Session Send Policy",
"session.sendPolicy.default": "Session Send Policy Default Action",

View File

@@ -182,9 +182,11 @@ export type SessionConfig = {
typingIntervalSeconds?: number;
typingMode?: TypingMode;
/**
* Max parent transcript token count allowed for thread/session forking.
* If parent totalTokens is above this value, OpenClaw skips parent fork and
* starts a fresh thread session instead. Set to 0 to disable this guard.
* @deprecated Compatibility guard for the historical parent fork ceiling.
* OpenClaw now uses a shared parent fork decision path for channel threads
* and subagents, and falls back to isolated context when the active parent
* branch is too large. Set to 0 only if you intentionally want to disable
* that compatibility guard.
*/
parentForkMaxTokens?: number;
mainKey?: string;

View File

@@ -14,7 +14,7 @@ describe("SessionSchema maintenance extensions", () => {
).not.toThrow();
});
it("accepts parentForkMaxTokens including 0 to disable the guard", () => {
it("accepts deprecated parentForkMaxTokens including 0 to disable the guard", () => {
expect(() => SessionSchema.parse({ parentForkMaxTokens: 100_000 })).not.toThrow();
expect(() => SessionSchema.parse({ parentForkMaxTokens: 0 })).not.toThrow();
});

View File

@@ -53,6 +53,7 @@ export const SessionSchema = z
store: z.string().optional(),
typingIntervalSeconds: z.number().int().positive().optional(),
typingMode: TypingModeSchema.optional(),
/** @deprecated Compatibility guard for the historical parent fork ceiling. */
parentForkMaxTokens: z.number().int().nonnegative().optional(),
mainKey: z.string().optional(),
sendPolicy: SessionSendPolicySchema.optional(),