mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
fix(session): prevent silent overflow on parent thread forks (#26912)
Lands #26912 from @markshields-tl with configurable session.parentForkMaxTokens and docs/tests/changelog updates. Co-authored-by: Mark Shields <239231357+markshields-tl@users.noreply.github.com>
This commit is contained in:
@@ -12,6 +12,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
|
|
||||||
|
- 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.
|
||||||
- Security/Signal: enforce DM/group authorization before reaction-only notification enqueue so unauthorized senders can no longer inject Signal reaction system events under `dmPolicy`/`groupPolicy`; reaction notifications now require channel access checks first. This ships in the next npm release (`2026.2.25`). Thanks @tdjackey for reporting.
|
- Security/Signal: enforce DM/group authorization before reaction-only notification enqueue so unauthorized senders can no longer inject Signal reaction system events under `dmPolicy`/`groupPolicy`; reaction notifications now require channel access checks first. This ships in the next npm release (`2026.2.25`). Thanks @tdjackey for reporting.
|
||||||
- Security/macOS beta onboarding: remove Anthropic OAuth sign-in and the legacy `oauth.json` onboarding path that exposed the PKCE verifier via OAuth `state`; this impacted the macOS beta onboarding path only. Anthropic subscription auth is now setup-token-only and will ship in the next npm release (`2026.2.25`). Thanks @zdi-disclosures for reporting.
|
- Security/macOS beta onboarding: remove Anthropic OAuth sign-in and the legacy `oauth.json` onboarding path that exposed the PKCE verifier via OAuth `state`; this impacted the macOS beta onboarding path only. Anthropic subscription auth is now setup-token-only and will ship in the next npm release (`2026.2.25`). Thanks @zdi-disclosures for reporting.
|
||||||
- Security/Nextcloud Talk: drop replayed signed webhook events with persistent per-account replay dedupe across restarts, and reject unexpected webhook backend origins when account base URL is configured. Thanks @aristorechina for reporting.
|
- Security/Nextcloud Talk: drop replayed signed webhook events with persistent per-account replay dedupe across restarts, and reject unexpected webhook backend origins when account base URL is configured. Thanks @aristorechina for reporting.
|
||||||
|
|||||||
@@ -1250,6 +1250,7 @@ See [Multi-Agent Sandbox & Tools](/tools/multi-agent-sandbox-tools) for preceden
|
|||||||
},
|
},
|
||||||
resetTriggers: ["/new", "/reset"],
|
resetTriggers: ["/new", "/reset"],
|
||||||
store: "~/.openclaw/agents/{agentId}/sessions/sessions.json",
|
store: "~/.openclaw/agents/{agentId}/sessions/sessions.json",
|
||||||
|
parentForkMaxTokens: 100000, // skip parent-thread fork above this token count (0 disables)
|
||||||
maintenance: {
|
maintenance: {
|
||||||
mode: "warn", // warn | enforce
|
mode: "warn", // warn | enforce
|
||||||
pruneAfter: "30d",
|
pruneAfter: "30d",
|
||||||
@@ -1283,6 +1284,9 @@ 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.
|
- **`identityLinks`**: map canonical ids to provider-prefixed peers for cross-channel session sharing.
|
||||||
- **`reset`**: primary reset policy. `daily` resets at `atHour` local time; `idle` resets after `idleMinutes`. When both configured, whichever expires first wins.
|
- **`reset`**: primary reset policy. `daily` resets at `atHour` local time; `idle` resets after `idleMinutes`. When both configured, whichever expires first wins.
|
||||||
- **`resetByType`**: per-type overrides (`direct`, `group`, `thread`). Legacy `dm` accepted as alias for `direct`.
|
- **`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.
|
||||||
- **`mainKey`**: legacy field. Runtime now always uses `"main"` for the main direct-chat bucket.
|
- **`mainKey`**: legacy field. Runtime now always uses `"main"` for the main direct-chat bucket.
|
||||||
- **`sendPolicy`**: match by `channel`, `chatType` (`direct|group|channel`, with legacy `dm` alias), `keyPrefix`, or `rawKeyPrefix`. First deny wins.
|
- **`sendPolicy`**: match by `channel`, `chatType` (`direct|group|channel`, with legacy `dm` alias), `keyPrefix`, or `rawKeyPrefix`. First deny wins.
|
||||||
- **`maintenance`**: session-store cleanup + retention controls.
|
- **`maintenance`**: session-store cleanup + retention controls.
|
||||||
|
|||||||
@@ -128,6 +128,7 @@ Rules of thumb:
|
|||||||
- **Reset** (`/new`, `/reset`) creates a new `sessionId` for that `sessionKey`.
|
- **Reset** (`/new`, `/reset`) creates a new `sessionId` for that `sessionKey`.
|
||||||
- **Daily reset** (default 4:00 AM local time on the gateway host) creates a new `sessionId` on the next message after the reset boundary.
|
- **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.
|
- **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.
|
||||||
|
- **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.
|
||||||
|
|
||||||
Implementation detail: the decision happens in `initSessionState()` in `src/auto-reply/reply/session.ts`.
|
Implementation detail: the decision happens in `initSessionState()` in `src/auto-reply/reply/session.ts`.
|
||||||
|
|
||||||
|
|||||||
@@ -572,6 +572,22 @@ export async function runAgentTurnWithFallback(params: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the run completed but with an embedded context overflow error that
|
||||||
|
// wasn't recovered from (e.g. compaction reset already attempted), surface
|
||||||
|
// the error to the user instead of silently returning an empty response.
|
||||||
|
// See #26905: Slack DM sessions silently swallowed messages when context
|
||||||
|
// overflow errors were returned as embedded error payloads.
|
||||||
|
const finalEmbeddedError = runResult?.meta?.error;
|
||||||
|
const hasPayloadText = runResult?.payloads?.some((p) => p.text?.trim());
|
||||||
|
if (finalEmbeddedError && isContextOverflowError(finalEmbeddedError.message) && !hasPayloadText) {
|
||||||
|
return {
|
||||||
|
kind: "final",
|
||||||
|
payload: {
|
||||||
|
text: "⚠️ Context overflow — this conversation is too large for the model. Use /new to start a fresh session.",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
kind: "success",
|
kind: "success",
|
||||||
runId,
|
runId,
|
||||||
|
|||||||
@@ -205,6 +205,130 @@ describe("initSessionState thread forking", () => {
|
|||||||
warn.mockRestore();
|
warn.mockRestore();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("skips fork and creates fresh session when parent tokens exceed threshold", async () => {
|
||||||
|
const root = await makeCaseDir("openclaw-thread-session-overflow-");
|
||||||
|
const sessionsDir = path.join(root, "sessions");
|
||||||
|
await fs.mkdir(sessionsDir);
|
||||||
|
|
||||||
|
const parentSessionId = "parent-overflow";
|
||||||
|
const parentSessionFile = path.join(sessionsDir, "parent.jsonl");
|
||||||
|
const header = {
|
||||||
|
type: "session",
|
||||||
|
version: 3,
|
||||||
|
id: parentSessionId,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
cwd: process.cwd(),
|
||||||
|
};
|
||||||
|
const message = {
|
||||||
|
type: "message",
|
||||||
|
id: "m1",
|
||||||
|
parentId: null,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
message: { role: "user", content: "Parent prompt" },
|
||||||
|
};
|
||||||
|
await fs.writeFile(
|
||||||
|
parentSessionFile,
|
||||||
|
`${JSON.stringify(header)}\n${JSON.stringify(message)}\n`,
|
||||||
|
"utf-8",
|
||||||
|
);
|
||||||
|
|
||||||
|
const storePath = path.join(root, "sessions.json");
|
||||||
|
const parentSessionKey = "agent:main:slack:channel:c1";
|
||||||
|
// Set totalTokens well above PARENT_FORK_MAX_TOKENS (100_000)
|
||||||
|
await saveSessionStore(storePath, {
|
||||||
|
[parentSessionKey]: {
|
||||||
|
sessionId: parentSessionId,
|
||||||
|
sessionFile: parentSessionFile,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
totalTokens: 170_000,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const cfg = {
|
||||||
|
session: { store: storePath },
|
||||||
|
} as OpenClawConfig;
|
||||||
|
|
||||||
|
const threadSessionKey = "agent:main:slack:channel:c1:thread:456";
|
||||||
|
const result = await initSessionState({
|
||||||
|
ctx: {
|
||||||
|
Body: "Thread reply",
|
||||||
|
SessionKey: threadSessionKey,
|
||||||
|
ParentSessionKey: parentSessionKey,
|
||||||
|
},
|
||||||
|
cfg,
|
||||||
|
commandAuthorized: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should be marked as forked (to prevent re-attempts) but NOT actually forked from parent
|
||||||
|
expect(result.sessionEntry.forkedFromParent).toBe(true);
|
||||||
|
// Session ID should NOT match the parent — it should be a fresh UUID
|
||||||
|
expect(result.sessionEntry.sessionId).not.toBe(parentSessionId);
|
||||||
|
// Session file should NOT be the parent's file (it was not forked)
|
||||||
|
expect(result.sessionEntry.sessionFile).not.toBe(parentSessionFile);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("respects session.parentForkMaxTokens override", async () => {
|
||||||
|
const root = await makeCaseDir("openclaw-thread-session-overflow-override-");
|
||||||
|
const sessionsDir = path.join(root, "sessions");
|
||||||
|
await fs.mkdir(sessionsDir);
|
||||||
|
|
||||||
|
const parentSessionId = "parent-override";
|
||||||
|
const parentSessionFile = path.join(sessionsDir, "parent.jsonl");
|
||||||
|
const header = {
|
||||||
|
type: "session",
|
||||||
|
version: 3,
|
||||||
|
id: parentSessionId,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
cwd: process.cwd(),
|
||||||
|
};
|
||||||
|
const message = {
|
||||||
|
type: "message",
|
||||||
|
id: "m1",
|
||||||
|
parentId: null,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
message: { role: "user", content: "Parent prompt" },
|
||||||
|
};
|
||||||
|
await fs.writeFile(
|
||||||
|
parentSessionFile,
|
||||||
|
`${JSON.stringify(header)}\n${JSON.stringify(message)}\n`,
|
||||||
|
"utf-8",
|
||||||
|
);
|
||||||
|
|
||||||
|
const storePath = path.join(root, "sessions.json");
|
||||||
|
const parentSessionKey = "agent:main:slack:channel:c1";
|
||||||
|
await saveSessionStore(storePath, {
|
||||||
|
[parentSessionKey]: {
|
||||||
|
sessionId: parentSessionId,
|
||||||
|
sessionFile: parentSessionFile,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
totalTokens: 170_000,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const cfg = {
|
||||||
|
session: {
|
||||||
|
store: storePath,
|
||||||
|
parentForkMaxTokens: 200_000,
|
||||||
|
},
|
||||||
|
} as OpenClawConfig;
|
||||||
|
|
||||||
|
const threadSessionKey = "agent:main:slack:channel:c1:thread:789";
|
||||||
|
const result = await initSessionState({
|
||||||
|
ctx: {
|
||||||
|
Body: "Thread reply",
|
||||||
|
SessionKey: threadSessionKey,
|
||||||
|
ParentSessionKey: parentSessionKey,
|
||||||
|
},
|
||||||
|
cfg,
|
||||||
|
commandAuthorized: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.sessionEntry.forkedFromParent).toBe(true);
|
||||||
|
expect(result.sessionEntry.sessionFile).toBeTruthy();
|
||||||
|
const forkedContent = await fs.readFile(result.sessionEntry.sessionFile ?? "", "utf-8");
|
||||||
|
expect(forkedContent).toContain(parentSessionFile);
|
||||||
|
});
|
||||||
|
|
||||||
it("records topic-specific session files when MessageThreadId is present", async () => {
|
it("records topic-specific session files when MessageThreadId is present", async () => {
|
||||||
const root = await makeCaseDir("openclaw-topic-session-");
|
const root = await makeCaseDir("openclaw-topic-session-");
|
||||||
const storePath = path.join(root, "sessions.json");
|
const storePath = path.join(root, "sessions.json");
|
||||||
|
|||||||
@@ -105,6 +105,21 @@ export type SessionInitResult = {
|
|||||||
triggerBodyNormalized: string;
|
triggerBodyNormalized: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default max parent token count beyond which thread/session parent forking is skipped.
|
||||||
|
* This prevents new thread sessions from inheriting near-full parent context.
|
||||||
|
* See #26905.
|
||||||
|
*/
|
||||||
|
const DEFAULT_PARENT_FORK_MAX_TOKENS = 100_000;
|
||||||
|
|
||||||
|
function resolveParentForkMaxTokens(cfg: OpenClawConfig): number {
|
||||||
|
const configured = cfg.session?.parentForkMaxTokens;
|
||||||
|
if (typeof configured === "number" && Number.isFinite(configured) && configured >= 0) {
|
||||||
|
return Math.floor(configured);
|
||||||
|
}
|
||||||
|
return DEFAULT_PARENT_FORK_MAX_TOKENS;
|
||||||
|
}
|
||||||
|
|
||||||
function forkSessionFromParent(params: {
|
function forkSessionFromParent(params: {
|
||||||
parentEntry: SessionEntry;
|
parentEntry: SessionEntry;
|
||||||
agentId: string;
|
agentId: string;
|
||||||
@@ -171,6 +186,7 @@ export async function initSessionState(params: {
|
|||||||
const resetTriggers = sessionCfg?.resetTriggers?.length
|
const resetTriggers = sessionCfg?.resetTriggers?.length
|
||||||
? sessionCfg.resetTriggers
|
? sessionCfg.resetTriggers
|
||||||
: DEFAULT_RESET_TRIGGERS;
|
: DEFAULT_RESET_TRIGGERS;
|
||||||
|
const parentForkMaxTokens = resolveParentForkMaxTokens(cfg);
|
||||||
const sessionScope = sessionCfg?.scope ?? "per-sender";
|
const sessionScope = sessionCfg?.scope ?? "per-sender";
|
||||||
const storePath = resolveStorePath(sessionCfg?.store, { agentId });
|
const storePath = resolveStorePath(sessionCfg?.store, { agentId });
|
||||||
|
|
||||||
@@ -399,21 +415,33 @@ export async function initSessionState(params: {
|
|||||||
sessionStore[parentSessionKey] &&
|
sessionStore[parentSessionKey] &&
|
||||||
!alreadyForked
|
!alreadyForked
|
||||||
) {
|
) {
|
||||||
log.warn(
|
const parentTokens = sessionStore[parentSessionKey].totalTokens ?? 0;
|
||||||
`forking from parent session: parentKey=${parentSessionKey} → sessionKey=${sessionKey} ` +
|
if (parentForkMaxTokens > 0 && parentTokens > parentForkMaxTokens) {
|
||||||
`parentTokens=${sessionStore[parentSessionKey].totalTokens ?? "?"}`,
|
// Parent context is too large — forking would create a thread session
|
||||||
);
|
// that immediately overflows the model's context window. Start fresh
|
||||||
const forked = forkSessionFromParent({
|
// instead and mark as forked to prevent re-attempts. See #26905.
|
||||||
parentEntry: sessionStore[parentSessionKey],
|
log.warn(
|
||||||
agentId,
|
`skipping parent fork (parent too large): parentKey=${parentSessionKey} → sessionKey=${sessionKey} ` +
|
||||||
sessionsDir: path.dirname(storePath),
|
`parentTokens=${parentTokens} maxTokens=${parentForkMaxTokens}`,
|
||||||
});
|
);
|
||||||
if (forked) {
|
|
||||||
sessionId = forked.sessionId;
|
|
||||||
sessionEntry.sessionId = forked.sessionId;
|
|
||||||
sessionEntry.sessionFile = forked.sessionFile;
|
|
||||||
sessionEntry.forkedFromParent = true;
|
sessionEntry.forkedFromParent = true;
|
||||||
log.warn(`forked session created: file=${forked.sessionFile}`);
|
} else {
|
||||||
|
log.warn(
|
||||||
|
`forking from parent session: parentKey=${parentSessionKey} → sessionKey=${sessionKey} ` +
|
||||||
|
`parentTokens=${parentTokens}`,
|
||||||
|
);
|
||||||
|
const forked = forkSessionFromParent({
|
||||||
|
parentEntry: sessionStore[parentSessionKey],
|
||||||
|
agentId,
|
||||||
|
sessionsDir: path.dirname(storePath),
|
||||||
|
});
|
||||||
|
if (forked) {
|
||||||
|
sessionId = forked.sessionId;
|
||||||
|
sessionEntry.sessionId = forked.sessionId;
|
||||||
|
sessionEntry.sessionFile = forked.sessionFile;
|
||||||
|
sessionEntry.forkedFromParent = true;
|
||||||
|
log.warn(`forked session created: file=${forked.sessionFile}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const fallbackSessionFile = !sessionEntry.sessionFile
|
const fallbackSessionFile = !sessionEntry.sessionFile
|
||||||
|
|||||||
@@ -973,6 +973,8 @@ export const FIELD_HELP: Record<string, string> = {
|
|||||||
"Controls interval for repeated typing indicators while replies are being prepared in typing-capable channels. Increase to reduce chatty updates or decrease for more active typing feedback.",
|
"Controls interval for repeated typing indicators while replies are being prepared in typing-capable channels. Increase to reduce chatty updates or decrease for more active typing feedback.",
|
||||||
"session.typingMode":
|
"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.',
|
'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.",
|
||||||
"session.mainKey":
|
"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.',
|
'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":
|
"session.sendPolicy":
|
||||||
|
|||||||
@@ -455,6 +455,7 @@ export const FIELD_LABELS: Record<string, string> = {
|
|||||||
"session.store": "Session Store Path",
|
"session.store": "Session Store Path",
|
||||||
"session.typingIntervalSeconds": "Session Typing Interval (seconds)",
|
"session.typingIntervalSeconds": "Session Typing Interval (seconds)",
|
||||||
"session.typingMode": "Session Typing Mode",
|
"session.typingMode": "Session Typing Mode",
|
||||||
|
"session.parentForkMaxTokens": "Session Parent Fork Max Tokens",
|
||||||
"session.mainKey": "Session Main Key",
|
"session.mainKey": "Session Main Key",
|
||||||
"session.sendPolicy": "Session Send Policy",
|
"session.sendPolicy": "Session Send Policy",
|
||||||
"session.sendPolicy.default": "Session Send Policy Default Action",
|
"session.sendPolicy.default": "Session Send Policy Default Action",
|
||||||
|
|||||||
@@ -112,6 +112,12 @@ export type SessionConfig = {
|
|||||||
store?: string;
|
store?: string;
|
||||||
typingIntervalSeconds?: number;
|
typingIntervalSeconds?: number;
|
||||||
typingMode?: TypingMode;
|
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.
|
||||||
|
*/
|
||||||
|
parentForkMaxTokens?: number;
|
||||||
mainKey?: string;
|
mainKey?: string;
|
||||||
sendPolicy?: SessionSendPolicyConfig;
|
sendPolicy?: SessionSendPolicyConfig;
|
||||||
agentToAgent?: {
|
agentToAgent?: {
|
||||||
|
|||||||
@@ -14,6 +14,19 @@ describe("SessionSchema maintenance extensions", () => {
|
|||||||
).not.toThrow();
|
).not.toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("accepts parentForkMaxTokens including 0 to disable the guard", () => {
|
||||||
|
expect(() => SessionSchema.parse({ parentForkMaxTokens: 100_000 })).not.toThrow();
|
||||||
|
expect(() => SessionSchema.parse({ parentForkMaxTokens: 0 })).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects negative parentForkMaxTokens", () => {
|
||||||
|
expect(() =>
|
||||||
|
SessionSchema.parse({
|
||||||
|
parentForkMaxTokens: -1,
|
||||||
|
}),
|
||||||
|
).toThrow(/parentForkMaxTokens/i);
|
||||||
|
});
|
||||||
|
|
||||||
it("accepts disabling reset archive cleanup", () => {
|
it("accepts disabling reset archive cleanup", () => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
SessionSchema.parse({
|
SessionSchema.parse({
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ export const SessionSchema = z
|
|||||||
store: z.string().optional(),
|
store: z.string().optional(),
|
||||||
typingIntervalSeconds: z.number().int().positive().optional(),
|
typingIntervalSeconds: z.number().int().positive().optional(),
|
||||||
typingMode: TypingModeSchema.optional(),
|
typingMode: TypingModeSchema.optional(),
|
||||||
|
parentForkMaxTokens: z.number().int().nonnegative().optional(),
|
||||||
mainKey: z.string().optional(),
|
mainKey: z.string().optional(),
|
||||||
sendPolicy: SessionSendPolicySchema.optional(),
|
sendPolicy: SessionSendPolicySchema.optional(),
|
||||||
agentToAgent: z
|
agentToAgent: z
|
||||||
|
|||||||
Reference in New Issue
Block a user