diff --git a/CHANGELOG.md b/CHANGELOG.md index f0ab408c19b..ebd4a32381d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai ### Changes +- Chat commands: add `/think default` and `/fast default` to clear session overrides and inherit configured/provider defaults. (#79385) Thanks @VACInc. - Dependencies: refresh workspace dependency pins and lockfile, including `@openai/codex` `0.130.0`, `acpx` `0.7.0`, AWS SDK `3.1044.0`, OpenTelemetry `0.217.0`, `typebox` `1.1.38`, `vite` `8.0.11`, `oxfmt` `0.48.0`, and `oxlint` `1.63.0`, and update the Codex harness model snapshot for the new bundled app-server catalog. - Plugins/install: add guarded plugin install overrides so onboarding and repair tests can route specific plugins to registry specs or local `npm pack` artifacts via environment variables. - Tests/Docker: add Codex on-demand install and live plugin-tool dependency E2E lanes for packaged onboarding and npm-pack plugin proof. diff --git a/docs/tools/slash-commands.md b/docs/tools/slash-commands.md index f0a32073f1e..a4a88c3ca30 100644 --- a/docs/tools/slash-commands.md +++ b/docs/tools/slash-commands.md @@ -135,10 +135,10 @@ Current source-of-truth: - - `/think ` sets the thinking level. Options come from the active model's provider profile; common levels are `off`, `minimal`, `low`, `medium`, and `high`, with custom levels such as `xhigh`, `adaptive`, `max`, or binary `on` only where supported. Aliases: `/thinking`, `/t`. + - `/think ` sets the thinking level or clears the session override. Options come from the active model's provider profile; common levels are `off`, `minimal`, `low`, `medium`, and `high`, with custom levels such as `xhigh`, `adaptive`, `max`, or binary `on` only where supported. Aliases: `/thinking`, `/t`. - `/verbose on|off|full` toggles verbose output. Alias: `/v`. - `/trace on|off` toggles plugin trace output for the current session. - - `/fast [status|on|off]` shows or sets fast mode. + - `/fast [status|on|off|default]` shows, sets, or clears fast mode. - `/reasoning [on|off|stream]` toggles reasoning visibility. Alias: `/reason`. - `/elevated [on|off|ask|full]` toggles elevated mode. Alias: `/elev`. - `/exec host= security= ask= node=` shows or sets exec defaults. diff --git a/docs/tools/thinking.md b/docs/tools/thinking.md index 0968bf25ea4..d6d37e28115 100644 --- a/docs/tools/thinking.md +++ b/docs/tools/thinking.md @@ -48,7 +48,8 @@ title: "Thinking levels" ## Setting a session default - Send a message that is **only** the directive (whitespace allowed), e.g. `/think:medium` or `/t high`. -- That sticks for the current session (per-sender by default); cleared by `/think:off` or session idle reset. +- That sticks for the current session (per-sender by default). Use `/think default` to clear the session override and inherit the configured/provider default; aliases include `inherit`, `clear`, `reset`, and `unpin`. +- `/think off` stores an explicit off override. It disables thinking until you change or clear the session override. - Confirmation reply is sent (`Thinking level set to high.` / `Thinking disabled.`). If the level is invalid (e.g. `/thinking big`), the command is rejected with a hint and the session state is left unchanged. - Send `/think` (or `/think:`) with no argument to see the current thinking level. @@ -59,11 +60,11 @@ title: "Thinking levels" ## Fast mode (/fast) -- Levels: `on|off`. -- Directive-only message toggles a session fast-mode override and replies `Fast mode enabled.` / `Fast mode disabled.`. +- Levels: `on|off|default`. +- Directive-only message toggles a session fast-mode override and replies `Fast mode enabled.` / `Fast mode disabled.`. Use `/fast default` to clear the session override and inherit the configured default; aliases include `inherit`, `clear`, `reset`, and `unpin`. - Send `/fast` (or `/fast status`) with no mode to see the current effective fast-mode state. - OpenClaw resolves fast mode in this order: - 1. Inline/directive-only `/fast on|off` + 1. Inline/directive-only `/fast on|off` override (`/fast default` clears this layer) 2. Session override 3. Per-agent default (`agents.list[].fastModeDefault`) 4. Per-model config: `agents.defaults.models["/"].params.fastMode` diff --git a/src/auto-reply/command-status-builders.ts b/src/auto-reply/command-status-builders.ts index 30310f7551d..c16c8f11d4d 100644 --- a/src/auto-reply/command-status-builders.ts +++ b/src/auto-reply/command-status-builders.ts @@ -59,9 +59,9 @@ export function buildHelpMessage(cfg?: OpenClawConfig): string { lines.push(""); const optionParts = [ - "/think ", + "/think ", "/model ", - "/fast status|on|off", + "/fast status|on|off|default", "/verbose on|off|full", "/trace on|off|raw", ]; diff --git a/src/auto-reply/commands-registry.shared.ts b/src/auto-reply/commands-registry.shared.ts index 79810656727..05fea14000f 100644 --- a/src/auto-reply/commands-registry.shared.ts +++ b/src/auto-reply/commands-registry.shared.ts @@ -13,7 +13,7 @@ type ListThinkingLevels = ( provider?: string | null, model?: string | null, catalog?: CommandArgChoiceContext["catalog"], -) => ThinkLevel[]; +) => string[]; const BROWSER_SAFE_THINKING_LEVELS: ThinkLevel[] = [ ...BASE_THINKING_LEVELS, @@ -147,8 +147,12 @@ export function assertCommandRegistry(commands: ChatCommandDefinition[]): void { export function buildBuiltinChatCommands( params: { listThinkingLevels?: ListThinkingLevels } = {}, ): ChatCommandDefinition[] { - const listThinkingLevelChoices = + const configuredThinkingLevels = params.listThinkingLevels ?? (() => BROWSER_SAFE_THINKING_LEVELS); + const listThinkingLevelChoices: ListThinkingLevels = (provider, model, catalog) => { + const levels = configuredThinkingLevels(provider, model, catalog); + return ["default", ...levels.filter((level) => level !== "default")]; + }; const commands: ChatCommandDefinition[] = [ defineChatCommand({ key: "help", @@ -799,9 +803,9 @@ export function buildBuiltinChatCommands( args: [ { name: "mode", - description: "status, on, or off", + description: "status, on, off, or default", type: "string", - choices: ["status", "on", "off"], + choices: ["status", "on", "off", "default"], }, ], argsMenu: "auto", diff --git a/src/auto-reply/commands-registry.test.ts b/src/auto-reply/commands-registry.test.ts index 48aaae362e1..eef251b1e78 100644 --- a/src/auto-reply/commands-registry.test.ts +++ b/src/auto-reply/commands-registry.test.ts @@ -377,7 +377,7 @@ describe("commands registry", () => { category: "options", }); const modeArg = requireCommandArg(fast, "mode"); - expect(modeArg.choices).toEqual(["status", "on", "off"]); + expect(modeArg.choices).toEqual(["status", "on", "off", "default"]); }); it("detects known text commands", () => { @@ -633,6 +633,7 @@ describe("commands registry args", () => { expect(menu.arg.name).toBe("level"); expect(menu.choices.map((choice) => choice.value)).toEqual([ + "default", "off", "low", "medium", @@ -640,7 +641,7 @@ describe("commands registry args", () => { "max", ]); expect(formatCommandArgMenuTitle({ command, menu })).toBe( - "Choose level for /think.\nOptions: off, low, medium, high, max.", + "Choose level for /think.\nOptions: default, off, low, medium, high, max.", ); }); diff --git a/src/auto-reply/reply.directive.parse.test.ts b/src/auto-reply/reply.directive.parse.test.ts index e76ebebb02e..21b72b71b67 100644 --- a/src/auto-reply/reply.directive.parse.test.ts +++ b/src/auto-reply/reply.directive.parse.test.ts @@ -69,6 +69,21 @@ describe("directive parsing", () => { expect(res.fastMode).toBe(true); }); + it("parses default thinking and fast directives as override clears", () => { + expect(parseInlineDirectives("/think default")).toMatchObject({ + hasThinkDirective: true, + thinkLevel: undefined, + rawThinkLevel: "default", + clearThinkLevel: true, + }); + expect(parseInlineDirectives("/fast inherit")).toMatchObject({ + hasFastDirective: true, + fastMode: undefined, + rawFastMode: "inherit", + clearFastMode: true, + }); + }); + it("matches elevated with leading space", () => { const res = extractElevatedDirective(" please /elevated on now"); expect(res.hasDirective).toBe(true); diff --git a/src/auto-reply/reply/commands-session-usage.test.ts b/src/auto-reply/reply/commands-session-usage.test.ts index 47289332317..1fcfd709c34 100644 --- a/src/auto-reply/reply/commands-session-usage.test.ts +++ b/src/auto-reply/reply/commands-session-usage.test.ts @@ -241,4 +241,45 @@ describe("handleFastCommand", () => { }), ); }); + + it("clears fast mode for /fast default", async () => { + const params = buildUsageParams(); + params.command.commandBodyNormalized = "/fast default"; + params.sessionEntry = { + sessionId: "target-session", + updatedAt: Date.now(), + fastMode: true, + }; + params.sessionStore = { [params.sessionKey]: params.sessionEntry }; + + const result = await handleFastCommand(params, true); + + expect(result?.shouldContinue).toBe(false); + expect(result?.reply?.text).toBe("⚙️ Fast mode reset to default."); + expect(params.sessionEntry.fastMode).toBeUndefined(); + expect(params.sessionStore[params.sessionKey]?.fastMode).toBeUndefined(); + }); + + it("clears fast mode on the target store entry for /fast default", async () => { + const params = buildUsageParams(); + params.command.commandBodyNormalized = "/fast default"; + params.sessionEntry = { + sessionId: "wrapper-session", + updatedAt: Date.now(), + fastMode: false, + }; + params.sessionStore = { + [params.sessionKey]: { + sessionId: "target-session", + updatedAt: Date.now(), + fastMode: true, + }, + }; + + const result = await handleFastCommand(params, true); + + expect(result?.reply?.text).toBe("⚙️ Fast mode reset to default."); + expect(params.sessionEntry.fastMode).toBe(false); + expect(params.sessionStore[params.sessionKey]?.fastMode).toBeUndefined(); + }); }); diff --git a/src/auto-reply/reply/commands-session.ts b/src/auto-reply/reply/commands-session.ts index 0aadaa330a7..5e91c107a6a 100644 --- a/src/auto-reply/reply/commands-session.ts +++ b/src/auto-reply/reply/commands-session.ts @@ -29,7 +29,12 @@ import { import { formatTokenCount, formatUsd } from "../../utils/usage-format.js"; import { parseActivationCommand } from "../group-activation.js"; import { parseSendPolicyCommand } from "../send-policy.js"; -import { normalizeFastMode, normalizeUsageDisplay, resolveResponseUsageMode } from "../thinking.js"; +import { + isSessionDefaultDirectiveValue, + normalizeFastMode, + normalizeUsageDisplay, + resolveResponseUsageMode, +} from "../thinking.js"; import { resolveCommandSurfaceChannel } from "./channel-context.js"; import { rejectNonOwnerCommand, rejectUnauthorizedCommand } from "./command-gates.js"; import { handleAbortTrigger, handleStopCommand } from "./commands-session-abort.js"; @@ -412,17 +417,29 @@ export const handleFastCommand: CommandHandler = async (params, allowTextCommand }; } - const nextMode = normalizeFastMode(rawMode); + const targetSessionEntry = params.sessionStore?.[params.sessionKey] ?? params.sessionEntry; + const resetsToDefault = isSessionDefaultDirectiveValue(rawMode); + const nextMode = resetsToDefault ? undefined : normalizeFastMode(rawMode); if (nextMode === undefined) { + if (resetsToDefault) { + if (targetSessionEntry && params.sessionStore && params.sessionKey) { + delete targetSessionEntry.fastMode; + await persistSessionEntry({ ...params, sessionEntry: targetSessionEntry }); + } + return { + shouldContinue: false, + reply: { text: "⚙️ Fast mode reset to default." }, + }; + } return { shouldContinue: false, - reply: { text: "⚙️ Usage: /fast status|on|off" }, + reply: { text: "⚙️ Usage: /fast status|on|off|default" }, }; } - if (params.sessionEntry && params.sessionStore && params.sessionKey) { - params.sessionEntry.fastMode = nextMode; - await persistSessionEntry(params); + if (targetSessionEntry && params.sessionStore && params.sessionKey) { + targetSessionEntry.fastMode = nextMode; + await persistSessionEntry({ ...params, sessionEntry: targetSessionEntry }); } return { diff --git a/src/auto-reply/reply/commands-subagents.test-helpers.ts b/src/auto-reply/reply/commands-subagents.test-helpers.ts index 91beb33f2e5..b0545127304 100644 --- a/src/auto-reply/reply/commands-subagents.test-helpers.ts +++ b/src/auto-reply/reply/commands-subagents.test-helpers.ts @@ -48,8 +48,10 @@ export function createEmptyInlineDirectives(): InlineDirectives { return { cleaned: "", hasThinkDirective: false, + clearThinkLevel: false, hasVerboseDirective: false, hasFastDirective: false, + clearFastMode: false, hasReasoningDirective: false, hasTraceDirective: false, hasElevatedDirective: false, diff --git a/src/auto-reply/reply/directive-handling.impl.ts b/src/auto-reply/reply/directive-handling.impl.ts index 69b5d0a37d9..b0a1287cd65 100644 --- a/src/auto-reply/reply/directive-handling.impl.ts +++ b/src/auto-reply/reply/directive-handling.impl.ts @@ -142,25 +142,28 @@ export async function handleDirectiveOnly( provider: resolvedProvider, model: resolvedModel, agentId: activeAgentId, - sessionEntry, + sessionEntry: directives.clearFastMode ? undefined : sessionEntry, }); - const effectiveFastMode = directives.fastMode ?? currentFastMode ?? fastModeState.enabled; + const effectiveFastMode = + directives.fastMode ?? + (directives.clearFastMode ? fastModeState.enabled : currentFastMode) ?? + fastModeState.enabled; const effectiveFastModeSource = directives.fastMode !== undefined ? "session" : fastModeState.source; - if (directives.hasThinkDirective && !directives.thinkLevel) { + if (directives.hasThinkDirective && !directives.thinkLevel && !directives.clearThinkLevel) { // If no argument was provided, show the current level if (!directives.rawThinkLevel) { const level = currentThinkLevel ?? "off"; return { text: withOptions( `Current thinking level: ${level}.`, - formatThinkingLevels(resolvedProvider, resolvedModel, ", ", thinkingCatalog), + `default, ${formatThinkingLevels(resolvedProvider, resolvedModel, ", ", thinkingCatalog)}`, ), }; } return { - text: `Unrecognized thinking level "${directives.rawThinkLevel}". Valid levels: ${formatThinkingLevels(resolvedProvider, resolvedModel, ", ", thinkingCatalog)}.`, + text: `Unrecognized thinking level "${directives.rawThinkLevel}". Valid levels: default, ${formatThinkingLevels(resolvedProvider, resolvedModel, ", ", thinkingCatalog)}.`, }; } if (directives.hasVerboseDirective && !directives.verboseLevel) { @@ -185,7 +188,11 @@ export async function handleDirectiveOnly( text: `Unrecognized trace level "${directives.rawTraceLevel}". Valid levels: off, on, raw.`, }; } - if (directives.hasFastDirective && directives.fastMode === undefined) { + if ( + directives.hasFastDirective && + directives.fastMode === undefined && + !directives.clearFastMode + ) { if ( !directives.rawFastMode || normalizeLowercaseStringOrEmpty(directives.rawFastMode) === "status" @@ -199,12 +206,12 @@ export async function handleDirectiveOnly( return { text: withOptions( `Current fast mode: ${effectiveFastMode ? "on" : "off"}${sourceSuffix}.`, - "status, on, off", + "status, on, off, default", ), }; } return { - text: `Unrecognized fast mode "${directives.rawFastMode}". Valid levels: status, on, off.`, + text: `Unrecognized fast mode "${directives.rawFastMode}". Valid levels: status, on, off, default.`, }; } if (directives.hasReasoningDirective && !directives.reasoningLevel) { @@ -351,8 +358,10 @@ export async function handleDirectiveOnly( elevatedAllowed; let modelSelectionUpdated = false; const shouldPersistSessionEntry = - (directives.hasThinkDirective && Boolean(directives.thinkLevel)) || - (directives.hasFastDirective && directives.fastMode !== undefined) || + (directives.hasThinkDirective && + (Boolean(directives.thinkLevel) || directives.clearThinkLevel)) || + (directives.hasFastDirective && + (directives.fastMode !== undefined || directives.clearFastMode)) || (directives.hasVerboseDirective && Boolean(directives.verboseLevel) && allowInternalVerbosePersistence) || @@ -364,16 +373,25 @@ export async function handleDirectiveOnly( directives.hasQueueDirective || shouldRemapUnsupportedThinkLevel; const fastModeChanged = - directives.hasFastDirective && - directives.fastMode !== undefined && - directives.fastMode !== currentFastMode; + (directives.hasFastDirective && + directives.fastMode !== undefined && + directives.fastMode !== currentFastMode) || + (directives.clearFastMode && currentFastMode !== fastModeState.enabled); let reasoningChanged = directives.hasReasoningDirective && directives.reasoningLevel !== undefined; if (shouldPersistSessionEntry) { - if (directives.hasThinkDirective && directives.thinkLevel && resolvedDirectiveThinkLevel) { + if (directives.clearThinkLevel) { + delete sessionEntry.thinkingLevel; + } else if ( + directives.hasThinkDirective && + directives.thinkLevel && + resolvedDirectiveThinkLevel + ) { sessionEntry.thinkingLevel = resolvedDirectiveThinkLevel; } - if (directives.hasFastDirective && directives.fastMode !== undefined) { + if (directives.clearFastMode) { + delete sessionEntry.fastMode; + } else if (directives.hasFastDirective && directives.fastMode !== undefined) { sessionEntry.fastMode = directives.fastMode; } if (shouldRemapUnsupportedThinkLevel && remappedUnsupportedThinkLevel) { @@ -488,7 +506,9 @@ export async function handleDirectiveOnly( }); const parts: string[] = []; - if (directives.hasThinkDirective && directives.thinkLevel) { + if (directives.clearThinkLevel) { + parts.push("Thinking level reset to default."); + } else if (directives.hasThinkDirective && directives.thinkLevel) { const displayedThinkLevel = resolvedDirectiveThinkLevel ?? directives.thinkLevel; parts.push( displayedThinkLevel === "off" @@ -501,7 +521,9 @@ export async function handleDirectiveOnly( ); } } - if (directives.hasFastDirective && directives.fastMode !== undefined) { + if (directives.clearFastMode) { + parts.push(formatDirectiveAck("Fast mode reset to default.")); + } else if (directives.hasFastDirective && directives.fastMode !== undefined) { parts.push( directives.fastMode ? formatDirectiveAck("Fast mode enabled.") @@ -617,9 +639,10 @@ export async function handleDirectiveOnly( parts.push(formatDirectiveAck(`Queue drop set to ${directives.dropPolicy}.`)); } if (fastModeChanged) { - enqueueSystemEvent(`Fast mode ${sessionEntry.fastMode ? "enabled" : "disabled"}.`, { + const nextFastMode = directives.clearFastMode ? fastModeState.enabled : sessionEntry.fastMode; + enqueueSystemEvent(`Fast mode ${nextFastMode ? "enabled" : "disabled"}.`, { sessionKey, - contextKey: `fast:${sessionEntry.fastMode ? "on" : "off"}`, + contextKey: `fast:${nextFastMode ? "on" : "off"}`, }); } const ack = parts.join(" ").trim(); diff --git a/src/auto-reply/reply/directive-handling.model.test.ts b/src/auto-reply/reply/directive-handling.model.test.ts index 5cffd2e9ef6..5982ab3aff9 100644 --- a/src/auto-reply/reply/directive-handling.model.test.ts +++ b/src/auto-reply/reply/directive-handling.model.test.ts @@ -1331,6 +1331,22 @@ describe("handleDirectiveOnly model persist behavior (fixes #1435)", () => { expect(sessionStore["agent:main:dm:1"]?.thinkingLevel).toBe("off"); }); + it("clears thinking override for default directives", async () => { + const sessionEntry = createSessionEntry({ thinkingLevel: "high" }); + const sessionStore = { [sessionKey]: sessionEntry }; + const result = await handleDirectiveOnly( + createHandleParams({ + directives: parseInlineDirectives("/think default"), + sessionEntry, + sessionStore, + }), + ); + + expect(result?.text).toContain("Thinking level reset to default."); + expect(sessionEntry.thinkingLevel).toBeUndefined(); + expect(sessionStore["agent:main:dm:1"]?.thinkingLevel).toBeUndefined(); + }); + it("reports current thinking status", async () => { setDirectiveTestProviders([ { @@ -1358,7 +1374,7 @@ describe("handleDirectiveOnly model persist behavior (fixes #1435)", () => { ); expect(result?.text).toContain("Current thinking level: low"); - expect(result?.text).toContain("Options: off, minimal, low, medium, adaptive, high."); + expect(result?.text).toContain("Options: default, off, minimal, low, medium, adaptive, high."); }); it("uses catalog reasoning metadata for provider-owned thinking levels", async () => { @@ -1468,6 +1484,17 @@ describe("handleDirectiveOnly model persist behavior (fixes #1435)", () => { ); expect(offReply?.text).toContain("Fast mode disabled"); expect(sessionEntry.fastMode).toBe(false); + + const defaultReply = await handleDirectiveOnly( + createHandleParams({ + directives: parseInlineDirectives("/fast default"), + sessionEntry, + sessionStore, + currentFastMode: sessionEntry.fastMode, + }), + ); + expect(defaultReply?.text).toContain("Fast mode reset to default"); + expect(sessionEntry.fastMode).toBeUndefined(); }); it("persists and reports elevated-mode directives when allowed", async () => { diff --git a/src/auto-reply/reply/directive-handling.parse.ts b/src/auto-reply/reply/directive-handling.parse.ts index 85087aef3bd..6346b5b0e9a 100644 --- a/src/auto-reply/reply/directive-handling.parse.ts +++ b/src/auto-reply/reply/directive-handling.parse.ts @@ -1,5 +1,6 @@ import type { ExecAsk, ExecSecurity, ExecTarget } from "../../infra/exec-approvals.js"; import { extractModelDirective } from "../model.js"; +import { isSessionDefaultDirectiveValue } from "../thinking.js"; import type { ElevatedLevel, ReasoningLevel, @@ -25,6 +26,7 @@ export type InlineDirectives = { hasThinkDirective: boolean; thinkLevel?: ThinkLevel; rawThinkLevel?: string; + clearThinkLevel: boolean; hasVerboseDirective: boolean; verboseLevel?: VerboseLevel; rawVerboseLevel?: string; @@ -34,6 +36,7 @@ export type InlineDirectives = { hasFastDirective: boolean; fastMode?: boolean; rawFastMode?: string; + clearFastMode: boolean; hasReasoningDirective: boolean; reasoningLevel?: ReasoningLevel; rawReasoningLevel?: string; @@ -173,6 +176,7 @@ export function parseInlineDirectives( hasThinkDirective, thinkLevel, rawThinkLevel, + clearThinkLevel: hasThinkDirective && isSessionDefaultDirectiveValue(rawThinkLevel), hasVerboseDirective, verboseLevel, rawVerboseLevel, @@ -182,6 +186,7 @@ export function parseInlineDirectives( hasFastDirective, fastMode, rawFastMode, + clearFastMode: hasFastDirective && isSessionDefaultDirectiveValue(rawFastMode), hasReasoningDirective, reasoningLevel, rawReasoningLevel, diff --git a/src/auto-reply/reply/directive-handling.persist.ts b/src/auto-reply/reply/directive-handling.persist.ts index eb095740b9b..34cf3f97e71 100644 --- a/src/auto-reply/reply/directive-handling.persist.ts +++ b/src/auto-reply/reply/directive-handling.persist.ts @@ -166,10 +166,21 @@ export async function persistInlineDirectives(params: { directives.hasReasoningDirective && directives.reasoningLevel !== undefined; let updated = false; - if (directives.hasThinkDirective && directives.thinkLevel) { + if (directives.clearThinkLevel) { + if (sessionEntry.thinkingLevel) { + delete sessionEntry.thinkingLevel; + updated = true; + } + } else if (directives.hasThinkDirective && directives.thinkLevel) { sessionEntry.thinkingLevel = directives.thinkLevel; updated = true; } + if (directives.clearFastMode) { + if (sessionEntry.fastMode !== undefined) { + delete sessionEntry.fastMode; + updated = true; + } + } if ( directives.hasVerboseDirective && directives.verboseLevel && diff --git a/src/auto-reply/reply/get-reply-directives-utils.ts b/src/auto-reply/reply/get-reply-directives-utils.ts index bf3375b6a7f..f588bf31d9a 100644 --- a/src/auto-reply/reply/get-reply-directives-utils.ts +++ b/src/auto-reply/reply/get-reply-directives-utils.ts @@ -23,6 +23,7 @@ export function clearInlineDirectives(cleaned: string): InlineDirectives { hasThinkDirective: false, thinkLevel: undefined, rawThinkLevel: undefined, + clearThinkLevel: false, hasVerboseDirective: false, verboseLevel: undefined, rawVerboseLevel: undefined, @@ -32,6 +33,7 @@ export function clearInlineDirectives(cleaned: string): InlineDirectives { hasFastDirective: false, fastMode: undefined, rawFastMode: undefined, + clearFastMode: false, hasReasoningDirective: false, reasoningLevel: undefined, rawReasoningLevel: undefined, diff --git a/src/auto-reply/reply/get-reply-directives.ts b/src/auto-reply/reply/get-reply-directives.ts index 0336a6dc384..e604179bdb8 100644 --- a/src/auto-reply/reply/get-reply-directives.ts +++ b/src/auto-reply/reply/get-reply-directives.ts @@ -334,8 +334,10 @@ export async function resolveReplyDirectives(params: { : { ...parsedDirectives, hasThinkDirective: false, + clearThinkLevel: false, hasVerboseDirective: false, hasFastDirective: false, + clearFastMode: false, hasReasoningDirective: false, reasoningLevel: undefined, rawReasoningLevel: undefined, @@ -422,10 +424,11 @@ export async function resolveReplyDirectives(params: { groupResolution, }); const defaultActivation = defaultGroupActivation(requireMention); + const sessionThinkLevel = directives.clearThinkLevel + ? undefined + : (targetSessionEntry?.thinkingLevel as ThinkLevel | undefined); const resolvedThinkLevel = - normalizeThinkLevel(opts?.thinkingLevelOverride) ?? - directives.thinkLevel ?? - (targetSessionEntry?.thinkingLevel as ThinkLevel | undefined); + normalizeThinkLevel(opts?.thinkingLevelOverride) ?? directives.thinkLevel ?? sessionThinkLevel; const resolvedFastMode = opts?.fastModeOverride ?? directives.fastMode ?? @@ -434,7 +437,7 @@ export async function resolveReplyDirectives(params: { provider, model, agentId, - sessionEntry: targetSessionEntry, + sessionEntry: directives.clearFastMode ? undefined : targetSessionEntry, }).enabled; const resolvedVerboseLevel = @@ -537,7 +540,7 @@ export async function resolveReplyDirectives(params: { const thinkingExplicitlySet = directives.thinkLevel !== undefined || - targetSessionEntry?.thinkingLevel !== undefined || + sessionThinkLevel !== undefined || agentCfg?.thinkingDefault !== undefined; // When neither directive nor session nor agent set reasoning, default to model capability diff --git a/src/auto-reply/status.test.ts b/src/auto-reply/status.test.ts index d7993ae01b8..74843f83dbe 100644 --- a/src/auto-reply/status.test.ts +++ b/src/auto-reply/status.test.ts @@ -2103,7 +2103,7 @@ describe("buildHelpMessage", () => { }); it("includes /fast in help output", () => { - expect(buildHelpMessage()).toContain("/fast status|on|off"); + expect(buildHelpMessage()).toContain("/fast status|on|off|default"); }); it("includes raw trace mode in help output", () => { diff --git a/src/auto-reply/thinking.shared.ts b/src/auto-reply/thinking.shared.ts index 989057de0ef..0d1a8b4e707 100644 --- a/src/auto-reply/thinking.shared.ts +++ b/src/auto-reply/thinking.shared.ts @@ -83,6 +83,14 @@ export function normalizeThinkLevel(raw?: string | null): ThinkLevel | undefined return undefined; } +export function isSessionDefaultDirectiveValue(raw?: string | null): boolean { + const key = normalizeOptionalLowercaseString(raw); + if (!key) { + return false; + } + return ["default", "inherit", "inherited", "clear", "reset", "unpin"].includes(key); +} + export function formatXHighModelHint(): string { return "provider models that advertise xhigh reasoning"; } diff --git a/src/auto-reply/thinking.ts b/src/auto-reply/thinking.ts index 9226d5a6cd9..5a139e2a609 100644 --- a/src/auto-reply/thinking.ts +++ b/src/auto-reply/thinking.ts @@ -8,6 +8,7 @@ import { import type { ThinkLevel, ThinkingCatalogEntry } from "./thinking.shared.js"; export { formatXHighModelHint, + isSessionDefaultDirectiveValue, normalizeElevatedLevel, normalizeFastMode, normalizeNoticeLevel,