diff --git a/docs/concepts/session.md b/docs/concepts/session.md index 8493a9c20..c88e2ddfe 100644 --- a/docs/concepts/session.md +++ b/docs/concepts/session.md @@ -60,7 +60,7 @@ the workspace is writable. See [Memory](/concepts/memory) and - Idle reset (optional): `idleMinutes` adds a sliding idle window. When both daily and idle resets are configured, **whichever expires first** forces a new session. - Legacy idle-only: if you set `session.idleMinutes` without any `session.reset`/`resetByType` config, Clawdbot stays in idle-only mode for backward compatibility. - Per-type overrides (optional): `resetByType` lets you override the policy for `dm`, `group`, and `thread` sessions (thread = Slack/Discord threads, Telegram topics, Matrix threads when provided by the connector). -- Reset triggers: exact `/new` or `/reset` (plus any extras in `resetTriggers`) start a fresh session id and pass the remainder of the message through. If `/new` or `/reset` is sent alone, Clawdbot runs a short “hello” greeting turn to confirm the reset. +- Reset triggers: exact `/new` or `/reset` (plus any extras in `resetTriggers`) start a fresh session id and pass the remainder of the message through. `/new ` accepts a model alias, `provider/model`, or provider name (fuzzy match) to set the new session model. If `/new` or `/reset` is sent alone, Clawdbot runs a short “hello” greeting turn to confirm the reset. - Manual reset: delete specific keys from the store or remove the JSONL transcript; the next message recreates them. - Isolated cron jobs always mint a fresh `sessionId` per run (no idle reuse). diff --git a/docs/tools/slash-commands.md b/docs/tools/slash-commands.md index 89886e926..77a17fb61 100644 --- a/docs/tools/slash-commands.md +++ b/docs/tools/slash-commands.md @@ -73,7 +73,7 @@ Text + native (when enabled): - `/dock-slack` (alias: `/dock_slack`) (switch replies to Slack) - `/activation mention|always` (groups only) - `/send on|off|inherit` (owner-only) -- `/reset` or `/new` +- `/reset` or `/new [model]` (optional model hint; remainder is passed through) - `/think ` (dynamic choices by model/provider; aliases: `/thinking`, `/t`) - `/verbose on|full|off` (alias: `/v`) - `/reasoning on|off|stream` (alias: `/reason`; when on, sends a separate message prefixed `Reasoning:`; `stream` = Telegram draft only) @@ -91,6 +91,7 @@ Text-only: Notes: - Commands accept an optional `:` between the command and args (e.g. `/think: high`, `/send: on`, `/help:`). +- `/new ` accepts a model alias, `provider/model`, or a provider name (fuzzy match); if no match, the text is treated as the message body. - For full provider usage breakdown, use `clawdbot status --usage`. - `/usage` controls the per-response usage footer; `/usage cost` prints a local cost summary from Clawdbot session logs. - `/restart` is disabled by default; set `commands.restart: true` to enable it. diff --git a/src/agents/cli-runner/helpers.ts b/src/agents/cli-runner/helpers.ts index e079044b3..c1d96ea71 100644 --- a/src/agents/cli-runner/helpers.ts +++ b/src/agents/cli-runner/helpers.ts @@ -11,6 +11,7 @@ import type { CliBackendConfig } from "../../config/types.js"; import { runExec } from "../../process/exec.js"; import type { EmbeddedContextFile } from "../pi-embedded-helpers.js"; import { buildSystemPromptParams } from "../system-prompt-params.js"; +import { resolveDefaultModelForAgent } from "../model-selection.js"; import { buildAgentSystemPrompt } from "../system-prompt.js"; const CLI_RUN_QUEUE = new Map>(); @@ -174,6 +175,11 @@ export function buildSystemPrompt(params: { modelDisplay: string; agentId?: string; }) { + const defaultModelRef = resolveDefaultModelForAgent({ + cfg: params.config ?? {}, + agentId: params.agentId, + }); + const defaultModelLabel = `${defaultModelRef.provider}/${defaultModelRef.model}`; const { runtimeInfo, userTimezone, userTime, userTimeFormat } = buildSystemPromptParams({ config: params.config, agentId: params.agentId, @@ -183,6 +189,7 @@ export function buildSystemPrompt(params: { arch: os.arch(), node: process.version, model: params.modelDisplay, + defaultModel: defaultModelLabel, }, }); return buildAgentSystemPrompt({ diff --git a/src/agents/model-selection.ts b/src/agents/model-selection.ts index e8cca18af..a330423b3 100644 --- a/src/agents/model-selection.ts +++ b/src/agents/model-selection.ts @@ -1,6 +1,8 @@ import type { ClawdbotConfig } from "../config/config.js"; import type { ModelCatalogEntry } from "./model-catalog.js"; import { normalizeGoogleModelId } from "./models-config.providers.js"; +import { resolveAgentModelPrimary } from "./agent-scope.js"; +import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "./defaults.js"; export type ModelRef = { provider: string; @@ -141,6 +143,38 @@ export function resolveConfiguredModelRef(params: { return { provider: params.defaultProvider, model: params.defaultModel }; } +export function resolveDefaultModelForAgent(params: { + cfg: ClawdbotConfig; + agentId?: string; +}): ModelRef { + const agentModelOverride = params.agentId + ? resolveAgentModelPrimary(params.cfg, params.agentId) + : undefined; + const cfg = + agentModelOverride && agentModelOverride.length > 0 + ? { + ...params.cfg, + agents: { + ...params.cfg.agents, + defaults: { + ...params.cfg.agents?.defaults, + model: { + ...(typeof params.cfg.agents?.defaults?.model === "object" + ? params.cfg.agents.defaults.model + : undefined), + primary: agentModelOverride, + }, + }, + }, + } + : params.cfg; + return resolveConfiguredModelRef({ + cfg, + defaultProvider: DEFAULT_PROVIDER, + defaultModel: DEFAULT_MODEL, + }); +} + export function buildAllowedModelSet(params: { cfg: ClawdbotConfig; catalog: ModelCatalogEntry[]; diff --git a/src/agents/pi-embedded-runner/run/attempt.ts b/src/agents/pi-embedded-runner/run/attempt.ts index ba8a945a0..e75047082 100644 --- a/src/agents/pi-embedded-runner/run/attempt.ts +++ b/src/agents/pi-embedded-runner/run/attempt.ts @@ -43,6 +43,7 @@ import { resolveSkillsPromptForRun, } from "../../skills.js"; import { buildSystemPromptReport } from "../../system-prompt-report.js"; +import { resolveDefaultModelForAgent } from "../../model-selection.js"; import { isAbortError } from "../abort.js"; import { buildEmbeddedExtensionPaths } from "../extensions.js"; @@ -212,6 +213,11 @@ export async function runEmbeddedAttempt( }) : undefined; + const defaultModelRef = resolveDefaultModelForAgent({ + cfg: params.config ?? {}, + agentId: sessionAgentId, + }); + const defaultModelLabel = `${defaultModelRef.provider}/${defaultModelRef.model}`; const { runtimeInfo, userTimezone, userTime, userTimeFormat } = buildSystemPromptParams({ config: params.config, agentId: sessionAgentId, @@ -221,6 +227,7 @@ export async function runEmbeddedAttempt( arch: os.arch(), node: process.version, model: `${params.provider}/${params.modelId}`, + defaultModel: defaultModelLabel, channel: runtimeChannel, capabilities: runtimeCapabilities, channelActions, diff --git a/src/agents/system-prompt-params.ts b/src/agents/system-prompt-params.ts index 42e7247c1..21a97831a 100644 --- a/src/agents/system-prompt-params.ts +++ b/src/agents/system-prompt-params.ts @@ -13,6 +13,7 @@ export type RuntimeInfoInput = { arch: string; node: string; model: string; + defaultModel?: string; channel?: string; capabilities?: string[]; /** Supported message actions for the current channel (e.g., react, edit, unsend) */ diff --git a/src/agents/system-prompt.test.ts b/src/agents/system-prompt.test.ts index 17303d0e9..c9b225830 100644 --- a/src/agents/system-prompt.test.ts +++ b/src/agents/system-prompt.test.ts @@ -288,6 +288,7 @@ describe("buildAgentSystemPrompt", () => { arch: "arm64", node: "v20", model: "anthropic/claude", + defaultModel: "anthropic/claude-opus-4-5", }, "telegram", ["inlineButtons"], @@ -299,6 +300,7 @@ describe("buildAgentSystemPrompt", () => { expect(line).toContain("os=macOS (arm64)"); expect(line).toContain("node=v20"); expect(line).toContain("model=anthropic/claude"); + expect(line).toContain("default_model=anthropic/claude-opus-4-5"); expect(line).toContain("channel=telegram"); expect(line).toContain("capabilities=inlineButtons"); expect(line).toContain("thinking=low"); diff --git a/src/agents/system-prompt.ts b/src/agents/system-prompt.ts index 91dc06405..1d441aa4a 100644 --- a/src/agents/system-prompt.ts +++ b/src/agents/system-prompt.ts @@ -582,6 +582,7 @@ export function buildRuntimeLine( arch?: string; node?: string; model?: string; + defaultModel?: string; }, runtimeChannel?: string, runtimeCapabilities: string[] = [], @@ -597,6 +598,7 @@ export function buildRuntimeLine( : "", runtimeInfo?.node ? `node=${runtimeInfo.node}` : "", runtimeInfo?.model ? `model=${runtimeInfo.model}` : "", + runtimeInfo?.defaultModel ? `default_model=${runtimeInfo.defaultModel}` : "", runtimeChannel ? `channel=${runtimeChannel}` : "", runtimeChannel ? `capabilities=${runtimeCapabilities.length > 0 ? runtimeCapabilities.join(",") : "none"}` diff --git a/src/auto-reply/commands-registry.data.ts b/src/auto-reply/commands-registry.data.ts index 3a124b9dc..5e912d1ec 100644 --- a/src/auto-reply/commands-registry.data.ts +++ b/src/auto-reply/commands-registry.data.ts @@ -312,12 +312,14 @@ function buildChatCommands(): ChatCommandDefinition[] { nativeName: "reset", description: "Reset the current session.", textAlias: "/reset", + acceptsArgs: true, }), defineChatCommand({ key: "new", nativeName: "new", description: "Start a new session.", textAlias: "/new", + acceptsArgs: true, }), defineChatCommand({ key: "compact", diff --git a/src/auto-reply/reply/commands-context-report.ts b/src/auto-reply/reply/commands-context-report.ts index 5e5c20be2..5ba3aedc9 100644 --- a/src/auto-reply/reply/commands-context-report.ts +++ b/src/auto-reply/reply/commands-context-report.ts @@ -7,6 +7,7 @@ import { getSkillsSnapshotVersion } from "../../agents/skills/refresh.js"; import { buildAgentSystemPrompt } from "../../agents/system-prompt.js"; import { buildSystemPromptReport } from "../../agents/system-prompt-report.js"; import { buildSystemPromptParams } from "../../agents/system-prompt-params.js"; +import { resolveDefaultModelForAgent } from "../../agents/model-selection.js"; import { buildToolSummaryMap } from "../../agents/tool-summaries.js"; import { resolveBootstrapContextForRun } from "../../agents/bootstrap-files.js"; import type { SessionSystemPromptReport } from "../../config/sessions/types.js"; @@ -93,6 +94,11 @@ async function resolveContextReport( sessionKey: params.sessionKey, config: params.cfg, }); + const defaultModelRef = resolveDefaultModelForAgent({ + cfg: params.cfg, + agentId: sessionAgentId, + }); + const defaultModelLabel = `${defaultModelRef.provider}/${defaultModelRef.model}`; const { runtimeInfo, userTimezone, userTime, userTimeFormat } = buildSystemPromptParams({ config: params.cfg, agentId: sessionAgentId, @@ -102,6 +108,7 @@ async function resolveContextReport( arch: "unknown", node: process.version, model: `${params.provider}/${params.model}`, + defaultModel: defaultModelLabel, }, }); const sandboxInfo = sandboxRuntime.sandboxed diff --git a/src/auto-reply/reply/directive-handling.persist.ts b/src/auto-reply/reply/directive-handling.persist.ts index 3cb4606a3..e8dab44d2 100644 --- a/src/auto-reply/reply/directive-handling.persist.ts +++ b/src/auto-reply/reply/directive-handling.persist.ts @@ -1,16 +1,15 @@ import { resolveAgentDir, - resolveAgentModelPrimary, resolveDefaultAgentId, resolveSessionAgentId, } from "../../agents/agent-scope.js"; import { lookupContextTokens } from "../../agents/context.js"; -import { DEFAULT_CONTEXT_TOKENS, DEFAULT_MODEL, DEFAULT_PROVIDER } from "../../agents/defaults.js"; +import { DEFAULT_CONTEXT_TOKENS } from "../../agents/defaults.js"; import { buildModelAliasIndex, type ModelAliasIndex, modelKey, - resolveConfiguredModelRef, + resolveDefaultModelForAgent, resolveModelRefFromString, } from "../../agents/model-selection.js"; import type { ClawdbotConfig } from "../../config/config.js"; @@ -239,36 +238,14 @@ export function resolveDefaultModel(params: { cfg: ClawdbotConfig; agentId?: str defaultModel: string; aliasIndex: ModelAliasIndex; } { - const agentModelOverride = params.agentId - ? resolveAgentModelPrimary(params.cfg, params.agentId) - : undefined; - const cfg = - agentModelOverride && agentModelOverride.length > 0 - ? { - ...params.cfg, - agents: { - ...params.cfg.agents, - defaults: { - ...params.cfg.agents?.defaults, - model: { - ...(typeof params.cfg.agents?.defaults?.model === "object" - ? params.cfg.agents.defaults.model - : undefined), - primary: agentModelOverride, - }, - }, - }, - } - : params.cfg; - const mainModel = resolveConfiguredModelRef({ - cfg, - defaultProvider: DEFAULT_PROVIDER, - defaultModel: DEFAULT_MODEL, + const mainModel = resolveDefaultModelForAgent({ + cfg: params.cfg, + agentId: params.agentId, }); const defaultProvider = mainModel.provider; const defaultModel = mainModel.model; const aliasIndex = buildModelAliasIndex({ - cfg, + cfg: params.cfg, defaultProvider, }); return { defaultProvider, defaultModel, aliasIndex }; diff --git a/src/auto-reply/reply/get-reply-run.ts b/src/auto-reply/reply/get-reply-run.ts index a50600398..5c5b67c1d 100644 --- a/src/auto-reply/reply/get-reply-run.ts +++ b/src/auto-reply/reply/get-reply-run.ts @@ -38,6 +38,7 @@ import { SILENT_REPLY_TOKEN } from "../tokens.js"; import type { GetReplyOptions, ReplyPayload } from "../types.js"; import { runReplyAgent } from "./agent-runner.js"; import { applySessionHints } from "./body.js"; +import { routeReply } from "./route-reply.js"; import type { buildCommandContext } from "./commands.js"; import type { InlineDirectives } from "./directive-handling.js"; import { buildGroupIntro } from "./groups.js"; @@ -51,7 +52,7 @@ type AgentDefaults = NonNullable["defaults"]; type ExecOverrides = Pick; const BARE_SESSION_RESET_PROMPT = - "A new session was started via /new or /reset. Say hi briefly (1-2 sentences) and ask what the user wants to do next. Do not mention internal steps, files, tools, or reasoning."; + "A new session was started via /new or /reset. Say hi briefly (1-2 sentences) and ask what the user wants to do next. If the runtime model differs from default_model in the system prompt, mention the default model in the greeting. Do not mention internal steps, files, tools, or reasoning."; type RunPreparedReplyParams = { ctx: MsgContext; @@ -92,9 +93,11 @@ type RunPreparedReplyParams = { }; typing: TypingController; opts?: GetReplyOptions; + defaultProvider: string; defaultModel: string; timeoutMs: number; isNewSession: boolean; + resetTriggered: boolean; systemSent: boolean; sessionEntry?: SessionEntry; sessionStore?: Record; @@ -220,9 +223,11 @@ export async function runPreparedReply( perMessageQueueOptions, typing, opts, + defaultProvider, defaultModel, timeoutMs, isNewSession, + resetTriggered, systemSent, sessionKey, sessionId, @@ -369,6 +374,27 @@ export async function runPreparedReply( } } } + if (resetTriggered && command.isAuthorizedSender) { + const channel = ctx.OriginatingChannel || (command.channel as any); + const to = ctx.OriginatingTo || command.from || command.to; + if (channel && to) { + const modelLabel = `${provider}/${model}`; + const defaultLabel = `${defaultProvider}/${defaultModel}`; + const text = + modelLabel === defaultLabel + ? `✅ New session started · model: ${modelLabel}` + : `✅ New session started · model: ${modelLabel} (default: ${defaultLabel})`; + await routeReply({ + payload: { text }, + channel, + to, + sessionKey, + accountId: ctx.AccountId, + threadId: ctx.MessageThreadId, + cfg, + }); + } + } const sessionIdFinal = sessionId ?? crypto.randomUUID(); const sessionFile = resolveSessionFilePath(sessionIdFinal, sessionEntry); const queueBodyBase = [threadStarterNote, baseBodyFinal].filter(Boolean).join("\n\n"); diff --git a/src/auto-reply/reply/get-reply.ts b/src/auto-reply/reply/get-reply.ts index 7ab8928d5..27dc19d9a 100644 --- a/src/auto-reply/reply/get-reply.ts +++ b/src/auto-reply/reply/get-reply.ts @@ -19,6 +19,7 @@ import { handleInlineActions } from "./get-reply-inline-actions.js"; import { runPreparedReply } from "./get-reply-run.js"; import { finalizeInboundContext } from "./inbound-context.js"; import { initSessionState } from "./session.js"; +import { applyResetModelOverride } from "./session-reset-model.js"; import { stageSandboxMedia } from "./stage-sandbox-media.js"; import { createTypingController } from "./typing.js"; @@ -103,6 +104,7 @@ export async function getReplyFromConfig( sessionKey, sessionId, isNewSession, + resetTriggered, systemSent, abortedLastRun, storePath, @@ -110,8 +112,24 @@ export async function getReplyFromConfig( groupResolution, isGroup, triggerBodyNormalized, + bodyStripped, } = sessionState; + await applyResetModelOverride({ + cfg, + resetTriggered, + bodyStripped, + sessionCtx, + ctx: finalized, + sessionEntry, + sessionStore, + sessionKey, + storePath, + defaultProvider, + defaultModel, + aliasIndex, + }); + const directiveResult = await resolveReplyDirectives({ ctx: finalized, cfg, @@ -256,9 +274,11 @@ export async function getReplyFromConfig( perMessageQueueOptions, typing, opts, + defaultProvider, defaultModel, timeoutMs, isNewSession, + resetTriggered, systemSent, sessionEntry, sessionStore, diff --git a/src/auto-reply/reply/session-reset-model.test.ts b/src/auto-reply/reply/session-reset-model.test.ts new file mode 100644 index 000000000..07aeaf2f9 --- /dev/null +++ b/src/auto-reply/reply/session-reset-model.test.ts @@ -0,0 +1,74 @@ +import { describe, expect, it, vi } from "vitest"; + +import type { ClawdbotConfig } from "../../config/config.js"; +import { buildModelAliasIndex } from "../../agents/model-selection.js"; +import { applyResetModelOverride } from "./session-reset-model.js"; + +vi.mock("../../agents/model-catalog.js", () => ({ + loadModelCatalog: vi.fn(async () => [ + { provider: "minimax", id: "m2.1", name: "M2.1" }, + { provider: "openai", id: "gpt-4o-mini", name: "GPT-4o mini" }, + ]), +})); + +describe("applyResetModelOverride", () => { + it("selects a model hint and strips it from the body", async () => { + const cfg = {} as ClawdbotConfig; + const aliasIndex = buildModelAliasIndex({ cfg, defaultProvider: "openai" }); + const sessionEntry = { + sessionId: "s1", + updatedAt: Date.now(), + }; + const sessionStore = { "agent:main:dm:1": sessionEntry }; + const sessionCtx = { BodyStripped: "minimax summarize" }; + const ctx = { ChatType: "direct" }; + + await applyResetModelOverride({ + cfg, + resetTriggered: true, + bodyStripped: "minimax summarize", + sessionCtx, + ctx, + sessionEntry, + sessionStore, + sessionKey: "agent:main:dm:1", + defaultProvider: "openai", + defaultModel: "gpt-4o-mini", + aliasIndex, + }); + + expect(sessionEntry.providerOverride).toBe("minimax"); + expect(sessionEntry.modelOverride).toBe("m2.1"); + expect(sessionCtx.BodyStripped).toBe("summarize"); + }); + + it("skips when resetTriggered is false", async () => { + const cfg = {} as ClawdbotConfig; + const aliasIndex = buildModelAliasIndex({ cfg, defaultProvider: "openai" }); + const sessionEntry = { + sessionId: "s1", + updatedAt: Date.now(), + }; + const sessionStore = { "agent:main:dm:1": sessionEntry }; + const sessionCtx = { BodyStripped: "minimax summarize" }; + const ctx = { ChatType: "direct" }; + + await applyResetModelOverride({ + cfg, + resetTriggered: false, + bodyStripped: "minimax summarize", + sessionCtx, + ctx, + sessionEntry, + sessionStore, + sessionKey: "agent:main:dm:1", + defaultProvider: "openai", + defaultModel: "gpt-4o-mini", + aliasIndex, + }); + + expect(sessionEntry.providerOverride).toBeUndefined(); + expect(sessionEntry.modelOverride).toBeUndefined(); + expect(sessionCtx.BodyStripped).toBe("minimax summarize"); + }); +}); diff --git a/src/auto-reply/reply/session-reset-model.ts b/src/auto-reply/reply/session-reset-model.ts new file mode 100644 index 000000000..d14982920 --- /dev/null +++ b/src/auto-reply/reply/session-reset-model.ts @@ -0,0 +1,191 @@ +import { loadModelCatalog } from "../../agents/model-catalog.js"; +import { + buildAllowedModelSet, + modelKey, + normalizeProviderId, + resolveModelRefFromString, + type ModelAliasIndex, +} from "../../agents/model-selection.js"; +import type { ClawdbotConfig } from "../../config/config.js"; +import type { SessionEntry } from "../../config/sessions.js"; +import { updateSessionStore } from "../../config/sessions.js"; +import type { MsgContext, TemplateContext } from "../templating.js"; +import { formatInboundBodyWithSenderMeta } from "./inbound-sender-meta.js"; +import { resolveModelDirectiveSelection, type ModelDirectiveSelection } from "./model-selection.js"; + +type ResetModelResult = { + selection?: ModelDirectiveSelection; + cleanedBody?: string; +}; + +function splitBody(body: string) { + const tokens = body.split(/\s+/).filter(Boolean); + return { + tokens, + first: tokens[0], + second: tokens[1], + rest: tokens.slice(2), + }; +} + +function buildSelectionFromExplicit(params: { + raw: string; + defaultProvider: string; + defaultModel: string; + aliasIndex: ModelAliasIndex; + allowedModelKeys: Set; +}): ModelDirectiveSelection | undefined { + const resolved = resolveModelRefFromString({ + raw: params.raw, + defaultProvider: params.defaultProvider, + aliasIndex: params.aliasIndex, + }); + if (!resolved) return undefined; + const key = modelKey(resolved.ref.provider, resolved.ref.model); + if (params.allowedModelKeys.size > 0 && !params.allowedModelKeys.has(key)) return undefined; + const isDefault = + resolved.ref.provider === params.defaultProvider && resolved.ref.model === params.defaultModel; + return { + provider: resolved.ref.provider, + model: resolved.ref.model, + isDefault, + ...(resolved.alias ? { alias: resolved.alias } : undefined), + }; +} + +function applySelectionToSession(params: { + selection: ModelDirectiveSelection; + sessionEntry?: SessionEntry; + sessionStore?: Record; + sessionKey?: string; + storePath?: string; +}) { + const { selection, sessionEntry, sessionStore, sessionKey, storePath } = params; + if (!sessionEntry || !sessionStore || !sessionKey) return; + let updated = false; + if (selection.isDefault) { + if (sessionEntry.providerOverride || sessionEntry.modelOverride) { + delete sessionEntry.providerOverride; + delete sessionEntry.modelOverride; + updated = true; + } + } else { + if (sessionEntry.providerOverride !== selection.provider) { + sessionEntry.providerOverride = selection.provider; + updated = true; + } + if (sessionEntry.modelOverride !== selection.model) { + sessionEntry.modelOverride = selection.model; + updated = true; + } + } + if (!updated) return; + sessionEntry.updatedAt = Date.now(); + sessionStore[sessionKey] = sessionEntry; + if (storePath) { + updateSessionStore(storePath, (store) => { + store[sessionKey] = sessionEntry; + }).catch(() => { + // Ignore persistence errors; session still proceeds. + }); + } +} + +export async function applyResetModelOverride(params: { + cfg: ClawdbotConfig; + resetTriggered: boolean; + bodyStripped?: string; + sessionCtx: TemplateContext; + ctx: MsgContext; + sessionEntry?: SessionEntry; + sessionStore?: Record; + sessionKey?: string; + storePath?: string; + defaultProvider: string; + defaultModel: string; + aliasIndex: ModelAliasIndex; +}): Promise { + if (!params.resetTriggered) return {}; + const rawBody = params.bodyStripped?.trim(); + if (!rawBody) return {}; + + const { tokens, first, second } = splitBody(rawBody); + if (!first) return {}; + + const catalog = await loadModelCatalog({ config: params.cfg }); + const allowed = buildAllowedModelSet({ + cfg: params.cfg, + catalog, + defaultProvider: params.defaultProvider, + defaultModel: params.defaultModel, + }); + const allowedModelKeys = allowed.allowedKeys; + if (allowedModelKeys.size === 0) return {}; + + const providers = new Set(); + for (const key of allowedModelKeys) { + const slash = key.indexOf("/"); + if (slash <= 0) continue; + providers.add(normalizeProviderId(key.slice(0, slash))); + } + + const resolveSelection = (raw: string) => + resolveModelDirectiveSelection({ + raw, + defaultProvider: params.defaultProvider, + defaultModel: params.defaultModel, + aliasIndex: params.aliasIndex, + allowedModelKeys, + }); + + let selection: ModelDirectiveSelection | undefined; + let consumed = 0; + + if (providers.has(normalizeProviderId(first)) && second) { + const composite = `${normalizeProviderId(first)}/${second}`; + const resolved = resolveSelection(composite); + if (resolved.selection) { + selection = resolved.selection; + consumed = 2; + } + } + + if (!selection) { + selection = buildSelectionFromExplicit({ + raw: first, + defaultProvider: params.defaultProvider, + defaultModel: params.defaultModel, + aliasIndex: params.aliasIndex, + allowedModelKeys, + }); + if (selection) consumed = 1; + } + + if (!selection) { + const resolved = resolveSelection(first); + const allowFuzzy = providers.has(normalizeProviderId(first)) || first.trim().length >= 6; + if (allowFuzzy) { + selection = resolved.selection; + if (selection) consumed = 1; + } + } + + if (!selection) return {}; + + const cleanedBody = tokens.slice(consumed).join(" ").trim(); + params.sessionCtx.BodyStripped = formatInboundBodyWithSenderMeta({ + ctx: params.ctx, + body: cleanedBody, + }); + params.sessionCtx.BodyForCommands = cleanedBody; + + applySelectionToSession({ + selection, + sessionEntry: params.sessionEntry, + sessionStore: params.sessionStore, + sessionKey: params.sessionKey, + storePath: params.storePath, + }); + + return { selection, cleanedBody }; +} diff --git a/src/auto-reply/reply/session.ts b/src/auto-reply/reply/session.ts index 412bbb285..960ee92b6 100644 --- a/src/auto-reply/reply/session.ts +++ b/src/auto-reply/reply/session.ts @@ -40,6 +40,7 @@ export type SessionInitResult = { sessionKey: string; sessionId: string; isNewSession: boolean; + resetTriggered: boolean; systemSent: boolean; abortedLastRun: boolean; storePath: string; @@ -327,6 +328,7 @@ export async function initSessionState(params: { sessionKey, sessionId: sessionId ?? crypto.randomUUID(), isNewSession, + resetTriggered, systemSent, abortedLastRun, storePath,