Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
383bdcd778 | ||
|
|
18cb665b66 | ||
|
|
22060137e0 | ||
|
|
390ba5f42a | ||
|
|
b8593fd4fb | ||
|
|
68a467dd66 | ||
|
|
d18319a57d | ||
|
|
15e5bb3459 | ||
|
|
41f6d06967 | ||
|
|
e3a99aa2ce | ||
|
|
c1d8456860 |
No files matched your search
@@ -66,3 +66,6 @@ apps/ios/*.mobileprovision
|
|||||||
IDENTITY.md
|
IDENTITY.md
|
||||||
USER.md
|
USER.md
|
||||||
.tgz
|
.tgz
|
||||||
|
|
||||||
|
# local tooling
|
||||||
|
.serena/
|
||||||
@@ -1 +0,0 @@
|
|||||||
/cache
|
|
||||||
BIN
Binary file not shown.
Binary file not shown.
@@ -1,87 +0,0 @@
|
|||||||
# list of languages for which language servers are started; choose from:
|
|
||||||
# al bash clojure cpp csharp csharp_omnisharp
|
|
||||||
# dart elixir elm erlang fortran fsharp
|
|
||||||
# go groovy haskell java julia kotlin
|
|
||||||
# lua markdown nix pascal perl php
|
|
||||||
# powershell python python_jedi r rego ruby
|
|
||||||
# ruby_solargraph rust scala swift terraform toml
|
|
||||||
# typescript typescript_vts yaml zig
|
|
||||||
# Note:
|
|
||||||
# - For C, use cpp
|
|
||||||
# - For JavaScript, use typescript
|
|
||||||
# - For Free Pascal / Lazarus, use pascal
|
|
||||||
# Special requirements:
|
|
||||||
# - csharp: Requires the presence of a .sln file in the project folder.
|
|
||||||
# - pascal: Requires Free Pascal Compiler (fpc) and optionally Lazarus.
|
|
||||||
# When using multiple languages, the first language server that supports a given file will be used for that file.
|
|
||||||
# The first language is the default language and the respective language server will be used as a fallback.
|
|
||||||
# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored.
|
|
||||||
languages:
|
|
||||||
- typescript
|
|
||||||
|
|
||||||
# the encoding used by text files in the project
|
|
||||||
# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
|
|
||||||
encoding: "utf-8"
|
|
||||||
|
|
||||||
# whether to use the project's gitignore file to ignore files
|
|
||||||
# Added on 2025-04-07
|
|
||||||
ignore_all_files_in_gitignore: true
|
|
||||||
|
|
||||||
# list of additional paths to ignore
|
|
||||||
# same syntax as gitignore, so you can use * and **
|
|
||||||
# Was previously called `ignored_dirs`, please update your config if you are using that.
|
|
||||||
# Added (renamed) on 2025-04-07
|
|
||||||
ignored_paths: []
|
|
||||||
|
|
||||||
# whether the project is in read-only mode
|
|
||||||
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
|
|
||||||
# Added on 2025-04-18
|
|
||||||
read_only: false
|
|
||||||
|
|
||||||
# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
|
|
||||||
# Below is the complete list of tools for convenience.
|
|
||||||
# To make sure you have the latest list of tools, and to view their descriptions,
|
|
||||||
# execute `uv run scripts/print_tool_overview.py`.
|
|
||||||
#
|
|
||||||
# * `activate_project`: Activates a project by name.
|
|
||||||
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
|
|
||||||
# * `create_text_file`: Creates/overwrites a file in the project directory.
|
|
||||||
# * `delete_lines`: Deletes a range of lines within a file.
|
|
||||||
# * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
|
|
||||||
# * `execute_shell_command`: Executes a shell command.
|
|
||||||
# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
|
|
||||||
# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
|
|
||||||
# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
|
|
||||||
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
|
|
||||||
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
|
|
||||||
# * `initial_instructions`: Gets the initial instructions for the current project.
|
|
||||||
# Should only be used in settings where the system prompt cannot be set,
|
|
||||||
# e.g. in clients you have no control over, like Claude Desktop.
|
|
||||||
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
|
|
||||||
# * `insert_at_line`: Inserts content at a given line in a file.
|
|
||||||
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
|
|
||||||
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
|
|
||||||
# * `list_memories`: Lists memories in Serena's project-specific memory store.
|
|
||||||
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
|
|
||||||
# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
|
|
||||||
# * `read_file`: Reads a file within the project directory.
|
|
||||||
# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
|
|
||||||
# * `remove_project`: Removes a project from the Serena configuration.
|
|
||||||
# * `replace_lines`: Replaces a range of lines within a file with new content.
|
|
||||||
# * `replace_symbol_body`: Replaces the full definition of a symbol.
|
|
||||||
# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
|
|
||||||
# * `search_for_pattern`: Performs a search for a pattern in the project.
|
|
||||||
# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
|
|
||||||
# * `switch_modes`: Activates modes by providing a list of their names
|
|
||||||
# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
|
|
||||||
# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
|
|
||||||
# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
|
|
||||||
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
|
|
||||||
excluded_tools: []
|
|
||||||
|
|
||||||
# initial prompt for the project. It will always be given to the LLM upon activating the project
|
|
||||||
# (contrary to the memories, which are loaded on demand).
|
|
||||||
initial_prompt: ""
|
|
||||||
|
|
||||||
project_name: "clawdbot"
|
|
||||||
included_optional_tools: []
|
|
||||||
@@ -16,6 +16,7 @@ Docs: https://docs.clawd.bot
|
|||||||
- Zalouser: add channel dock metadata, config schema, setup wiring, probe, and status issues. (#1219) — thanks @suminhthanh.
|
- Zalouser: add channel dock metadata, config schema, setup wiring, probe, and status issues. (#1219) — thanks @suminhthanh.
|
||||||
### Fixes
|
### Fixes
|
||||||
- Web search: infer Perplexity base URL from API key source (direct vs OpenRouter).
|
- Web search: infer Perplexity base URL from API key source (direct vs OpenRouter).
|
||||||
|
- Session: preserve command argument casing while matching reset triggers. (#1308) — thanks @dougvk.
|
||||||
- TUI: keep thinking blocks ordered before content during streaming and isolate per-run assembly. (#1202) — thanks @aaronveklabs.
|
- TUI: keep thinking blocks ordered before content during streaming and isolate per-run assembly. (#1202) — thanks @aaronveklabs.
|
||||||
- TUI: align custom editor initialization with the latest pi-tui API. (#1298) — thanks @sibbl.
|
- TUI: align custom editor initialization with the latest pi-tui API. (#1298) — thanks @sibbl.
|
||||||
- CLI: avoid duplicating --profile/--dev flags when formatting commands.
|
- CLI: avoid duplicating --profile/--dev flags when formatting commands.
|
||||||
|
|||||||
@@ -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.
|
- 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.
|
- 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).
|
- 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 <model>` 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.
|
- 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).
|
- Isolated cron jobs always mint a fresh `sessionId` per run (no idle reuse).
|
||||||
|
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ Text + native (when enabled):
|
|||||||
- `/dock-slack` (alias: `/dock_slack`) (switch replies to Slack)
|
- `/dock-slack` (alias: `/dock_slack`) (switch replies to Slack)
|
||||||
- `/activation mention|always` (groups only)
|
- `/activation mention|always` (groups only)
|
||||||
- `/send on|off|inherit` (owner-only)
|
- `/send on|off|inherit` (owner-only)
|
||||||
- `/reset` or `/new`
|
- `/reset` or `/new [model]` (optional model hint; remainder is passed through)
|
||||||
- `/think <off|minimal|low|medium|high|xhigh>` (dynamic choices by model/provider; aliases: `/thinking`, `/t`)
|
- `/think <off|minimal|low|medium|high|xhigh>` (dynamic choices by model/provider; aliases: `/thinking`, `/t`)
|
||||||
- `/verbose on|full|off` (alias: `/v`)
|
- `/verbose on|full|off` (alias: `/v`)
|
||||||
- `/reasoning on|off|stream` (alias: `/reason`; when on, sends a separate message prefixed `Reasoning:`; `stream` = Telegram draft only)
|
- `/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:
|
Notes:
|
||||||
- Commands accept an optional `:` between the command and args (e.g. `/think: high`, `/send: on`, `/help:`).
|
- Commands accept an optional `:` between the command and args (e.g. `/think: high`, `/send: on`, `/help:`).
|
||||||
|
- `/new <model>` 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`.
|
- 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.
|
- `/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.
|
- `/restart` is disabled by default; set `commands.restart: true` to enable it.
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import type { CliBackendConfig } from "../../config/types.js";
|
|||||||
import { runExec } from "../../process/exec.js";
|
import { runExec } from "../../process/exec.js";
|
||||||
import type { EmbeddedContextFile } from "../pi-embedded-helpers.js";
|
import type { EmbeddedContextFile } from "../pi-embedded-helpers.js";
|
||||||
import { buildSystemPromptParams } from "../system-prompt-params.js";
|
import { buildSystemPromptParams } from "../system-prompt-params.js";
|
||||||
|
import { resolveDefaultModelForAgent } from "../model-selection.js";
|
||||||
import { buildAgentSystemPrompt } from "../system-prompt.js";
|
import { buildAgentSystemPrompt } from "../system-prompt.js";
|
||||||
|
|
||||||
const CLI_RUN_QUEUE = new Map<string, Promise<unknown>>();
|
const CLI_RUN_QUEUE = new Map<string, Promise<unknown>>();
|
||||||
@@ -174,6 +175,11 @@ export function buildSystemPrompt(params: {
|
|||||||
modelDisplay: string;
|
modelDisplay: string;
|
||||||
agentId?: string;
|
agentId?: string;
|
||||||
}) {
|
}) {
|
||||||
|
const defaultModelRef = resolveDefaultModelForAgent({
|
||||||
|
cfg: params.config ?? {},
|
||||||
|
agentId: params.agentId,
|
||||||
|
});
|
||||||
|
const defaultModelLabel = `${defaultModelRef.provider}/${defaultModelRef.model}`;
|
||||||
const { runtimeInfo, userTimezone, userTime, userTimeFormat } = buildSystemPromptParams({
|
const { runtimeInfo, userTimezone, userTime, userTimeFormat } = buildSystemPromptParams({
|
||||||
config: params.config,
|
config: params.config,
|
||||||
agentId: params.agentId,
|
agentId: params.agentId,
|
||||||
@@ -183,6 +189,7 @@ export function buildSystemPrompt(params: {
|
|||||||
arch: os.arch(),
|
arch: os.arch(),
|
||||||
node: process.version,
|
node: process.version,
|
||||||
model: params.modelDisplay,
|
model: params.modelDisplay,
|
||||||
|
defaultModel: defaultModelLabel,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return buildAgentSystemPrompt({
|
return buildAgentSystemPrompt({
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
import type { Api, Model } from "@mariozechner/pi-ai";
|
import type { Api, Model } from "@mariozechner/pi-ai";
|
||||||
|
|
||||||
function isOpenAICompletionsModel(model: Model<Api>): model is Model<"openai-completions"> {
|
|
||||||
return model.api === "openai-completions";
|
|
||||||
}
|
|
||||||
|
|
||||||
export function normalizeModelCompat(model: Model<Api>): Model<Api> {
|
export function normalizeModelCompat(model: Model<Api>): Model<Api> {
|
||||||
const baseUrl = model.baseUrl ?? "";
|
const baseUrl = model.baseUrl ?? "";
|
||||||
const isZai = model.provider === "zai" || baseUrl.includes("api.z.ai");
|
const isZai = model.provider === "zai" || baseUrl.includes("api.z.ai");
|
||||||
if (!isZai || !isOpenAICompletionsModel(model)) return model;
|
if (!isZai || model.api !== "openai-completions") return model;
|
||||||
|
|
||||||
const compat = model.compat ?? {};
|
const compat = model.compat ?? {};
|
||||||
if (compat.supportsDeveloperRole === false) return model;
|
if (compat.supportsDeveloperRole === false) return model;
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import type { ClawdbotConfig } from "../config/config.js";
|
import type { ClawdbotConfig } from "../config/config.js";
|
||||||
import type { ModelCatalogEntry } from "./model-catalog.js";
|
import type { ModelCatalogEntry } from "./model-catalog.js";
|
||||||
import { normalizeGoogleModelId } from "./models-config.providers.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 = {
|
export type ModelRef = {
|
||||||
provider: string;
|
provider: string;
|
||||||
@@ -141,6 +143,38 @@ export function resolveConfiguredModelRef(params: {
|
|||||||
return { provider: params.defaultProvider, model: params.defaultModel };
|
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: {
|
export function buildAllowedModelSet(params: {
|
||||||
cfg: ClawdbotConfig;
|
cfg: ClawdbotConfig;
|
||||||
catalog: ModelCatalogEntry[];
|
catalog: ModelCatalogEntry[];
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import {
|
|||||||
resolveSkillsPromptForRun,
|
resolveSkillsPromptForRun,
|
||||||
} from "../../skills.js";
|
} from "../../skills.js";
|
||||||
import { buildSystemPromptReport } from "../../system-prompt-report.js";
|
import { buildSystemPromptReport } from "../../system-prompt-report.js";
|
||||||
|
import { resolveDefaultModelForAgent } from "../../model-selection.js";
|
||||||
|
|
||||||
import { isAbortError } from "../abort.js";
|
import { isAbortError } from "../abort.js";
|
||||||
import { buildEmbeddedExtensionPaths } from "../extensions.js";
|
import { buildEmbeddedExtensionPaths } from "../extensions.js";
|
||||||
@@ -212,6 +213,11 @@ export async function runEmbeddedAttempt(
|
|||||||
})
|
})
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
const defaultModelRef = resolveDefaultModelForAgent({
|
||||||
|
cfg: params.config ?? {},
|
||||||
|
agentId: sessionAgentId,
|
||||||
|
});
|
||||||
|
const defaultModelLabel = `${defaultModelRef.provider}/${defaultModelRef.model}`;
|
||||||
const { runtimeInfo, userTimezone, userTime, userTimeFormat } = buildSystemPromptParams({
|
const { runtimeInfo, userTimezone, userTime, userTimeFormat } = buildSystemPromptParams({
|
||||||
config: params.config,
|
config: params.config,
|
||||||
agentId: sessionAgentId,
|
agentId: sessionAgentId,
|
||||||
@@ -221,6 +227,7 @@ export async function runEmbeddedAttempt(
|
|||||||
arch: os.arch(),
|
arch: os.arch(),
|
||||||
node: process.version,
|
node: process.version,
|
||||||
model: `${params.provider}/${params.modelId}`,
|
model: `${params.provider}/${params.modelId}`,
|
||||||
|
defaultModel: defaultModelLabel,
|
||||||
channel: runtimeChannel,
|
channel: runtimeChannel,
|
||||||
capabilities: runtimeCapabilities,
|
capabilities: runtimeCapabilities,
|
||||||
channelActions,
|
channelActions,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export type RuntimeInfoInput = {
|
|||||||
arch: string;
|
arch: string;
|
||||||
node: string;
|
node: string;
|
||||||
model: string;
|
model: string;
|
||||||
|
defaultModel?: string;
|
||||||
channel?: string;
|
channel?: string;
|
||||||
capabilities?: string[];
|
capabilities?: string[];
|
||||||
/** Supported message actions for the current channel (e.g., react, edit, unsend) */
|
/** Supported message actions for the current channel (e.g., react, edit, unsend) */
|
||||||
|
|||||||
@@ -288,6 +288,7 @@ describe("buildAgentSystemPrompt", () => {
|
|||||||
arch: "arm64",
|
arch: "arm64",
|
||||||
node: "v20",
|
node: "v20",
|
||||||
model: "anthropic/claude",
|
model: "anthropic/claude",
|
||||||
|
defaultModel: "anthropic/claude-opus-4-5",
|
||||||
},
|
},
|
||||||
"telegram",
|
"telegram",
|
||||||
["inlineButtons"],
|
["inlineButtons"],
|
||||||
@@ -299,6 +300,7 @@ describe("buildAgentSystemPrompt", () => {
|
|||||||
expect(line).toContain("os=macOS (arm64)");
|
expect(line).toContain("os=macOS (arm64)");
|
||||||
expect(line).toContain("node=v20");
|
expect(line).toContain("node=v20");
|
||||||
expect(line).toContain("model=anthropic/claude");
|
expect(line).toContain("model=anthropic/claude");
|
||||||
|
expect(line).toContain("default_model=anthropic/claude-opus-4-5");
|
||||||
expect(line).toContain("channel=telegram");
|
expect(line).toContain("channel=telegram");
|
||||||
expect(line).toContain("capabilities=inlineButtons");
|
expect(line).toContain("capabilities=inlineButtons");
|
||||||
expect(line).toContain("thinking=low");
|
expect(line).toContain("thinking=low");
|
||||||
|
|||||||
@@ -582,6 +582,7 @@ export function buildRuntimeLine(
|
|||||||
arch?: string;
|
arch?: string;
|
||||||
node?: string;
|
node?: string;
|
||||||
model?: string;
|
model?: string;
|
||||||
|
defaultModel?: string;
|
||||||
},
|
},
|
||||||
runtimeChannel?: string,
|
runtimeChannel?: string,
|
||||||
runtimeCapabilities: string[] = [],
|
runtimeCapabilities: string[] = [],
|
||||||
@@ -597,6 +598,7 @@ export function buildRuntimeLine(
|
|||||||
: "",
|
: "",
|
||||||
runtimeInfo?.node ? `node=${runtimeInfo.node}` : "",
|
runtimeInfo?.node ? `node=${runtimeInfo.node}` : "",
|
||||||
runtimeInfo?.model ? `model=${runtimeInfo.model}` : "",
|
runtimeInfo?.model ? `model=${runtimeInfo.model}` : "",
|
||||||
|
runtimeInfo?.defaultModel ? `default_model=${runtimeInfo.defaultModel}` : "",
|
||||||
runtimeChannel ? `channel=${runtimeChannel}` : "",
|
runtimeChannel ? `channel=${runtimeChannel}` : "",
|
||||||
runtimeChannel
|
runtimeChannel
|
||||||
? `capabilities=${runtimeCapabilities.length > 0 ? runtimeCapabilities.join(",") : "none"}`
|
? `capabilities=${runtimeCapabilities.length > 0 ? runtimeCapabilities.join(",") : "none"}`
|
||||||
|
|||||||
@@ -312,12 +312,14 @@ function buildChatCommands(): ChatCommandDefinition[] {
|
|||||||
nativeName: "reset",
|
nativeName: "reset",
|
||||||
description: "Reset the current session.",
|
description: "Reset the current session.",
|
||||||
textAlias: "/reset",
|
textAlias: "/reset",
|
||||||
|
acceptsArgs: true,
|
||||||
}),
|
}),
|
||||||
defineChatCommand({
|
defineChatCommand({
|
||||||
key: "new",
|
key: "new",
|
||||||
nativeName: "new",
|
nativeName: "new",
|
||||||
description: "Start a new session.",
|
description: "Start a new session.",
|
||||||
textAlias: "/new",
|
textAlias: "/new",
|
||||||
|
acceptsArgs: true,
|
||||||
}),
|
}),
|
||||||
defineChatCommand({
|
defineChatCommand({
|
||||||
key: "compact",
|
key: "compact",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { getSkillsSnapshotVersion } from "../../agents/skills/refresh.js";
|
|||||||
import { buildAgentSystemPrompt } from "../../agents/system-prompt.js";
|
import { buildAgentSystemPrompt } from "../../agents/system-prompt.js";
|
||||||
import { buildSystemPromptReport } from "../../agents/system-prompt-report.js";
|
import { buildSystemPromptReport } from "../../agents/system-prompt-report.js";
|
||||||
import { buildSystemPromptParams } from "../../agents/system-prompt-params.js";
|
import { buildSystemPromptParams } from "../../agents/system-prompt-params.js";
|
||||||
|
import { resolveDefaultModelForAgent } from "../../agents/model-selection.js";
|
||||||
import { buildToolSummaryMap } from "../../agents/tool-summaries.js";
|
import { buildToolSummaryMap } from "../../agents/tool-summaries.js";
|
||||||
import { resolveBootstrapContextForRun } from "../../agents/bootstrap-files.js";
|
import { resolveBootstrapContextForRun } from "../../agents/bootstrap-files.js";
|
||||||
import type { SessionSystemPromptReport } from "../../config/sessions/types.js";
|
import type { SessionSystemPromptReport } from "../../config/sessions/types.js";
|
||||||
@@ -93,6 +94,11 @@ async function resolveContextReport(
|
|||||||
sessionKey: params.sessionKey,
|
sessionKey: params.sessionKey,
|
||||||
config: params.cfg,
|
config: params.cfg,
|
||||||
});
|
});
|
||||||
|
const defaultModelRef = resolveDefaultModelForAgent({
|
||||||
|
cfg: params.cfg,
|
||||||
|
agentId: sessionAgentId,
|
||||||
|
});
|
||||||
|
const defaultModelLabel = `${defaultModelRef.provider}/${defaultModelRef.model}`;
|
||||||
const { runtimeInfo, userTimezone, userTime, userTimeFormat } = buildSystemPromptParams({
|
const { runtimeInfo, userTimezone, userTime, userTimeFormat } = buildSystemPromptParams({
|
||||||
config: params.cfg,
|
config: params.cfg,
|
||||||
agentId: sessionAgentId,
|
agentId: sessionAgentId,
|
||||||
@@ -102,6 +108,7 @@ async function resolveContextReport(
|
|||||||
arch: "unknown",
|
arch: "unknown",
|
||||||
node: process.version,
|
node: process.version,
|
||||||
model: `${params.provider}/${params.model}`,
|
model: `${params.provider}/${params.model}`,
|
||||||
|
defaultModel: defaultModelLabel,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const sandboxInfo = sandboxRuntime.sandboxed
|
const sandboxInfo = sandboxRuntime.sandboxed
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
import {
|
import {
|
||||||
resolveAgentDir,
|
resolveAgentDir,
|
||||||
resolveAgentModelPrimary,
|
|
||||||
resolveDefaultAgentId,
|
resolveDefaultAgentId,
|
||||||
resolveSessionAgentId,
|
resolveSessionAgentId,
|
||||||
} from "../../agents/agent-scope.js";
|
} from "../../agents/agent-scope.js";
|
||||||
import { lookupContextTokens } from "../../agents/context.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 {
|
import {
|
||||||
buildModelAliasIndex,
|
buildModelAliasIndex,
|
||||||
type ModelAliasIndex,
|
type ModelAliasIndex,
|
||||||
modelKey,
|
modelKey,
|
||||||
resolveConfiguredModelRef,
|
resolveDefaultModelForAgent,
|
||||||
resolveModelRefFromString,
|
resolveModelRefFromString,
|
||||||
} from "../../agents/model-selection.js";
|
} from "../../agents/model-selection.js";
|
||||||
import type { ClawdbotConfig } from "../../config/config.js";
|
import type { ClawdbotConfig } from "../../config/config.js";
|
||||||
@@ -239,36 +238,14 @@ export function resolveDefaultModel(params: { cfg: ClawdbotConfig; agentId?: str
|
|||||||
defaultModel: string;
|
defaultModel: string;
|
||||||
aliasIndex: ModelAliasIndex;
|
aliasIndex: ModelAliasIndex;
|
||||||
} {
|
} {
|
||||||
const agentModelOverride = params.agentId
|
const mainModel = resolveDefaultModelForAgent({
|
||||||
? resolveAgentModelPrimary(params.cfg, params.agentId)
|
cfg: params.cfg,
|
||||||
: undefined;
|
agentId: params.agentId,
|
||||||
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 defaultProvider = mainModel.provider;
|
const defaultProvider = mainModel.provider;
|
||||||
const defaultModel = mainModel.model;
|
const defaultModel = mainModel.model;
|
||||||
const aliasIndex = buildModelAliasIndex({
|
const aliasIndex = buildModelAliasIndex({
|
||||||
cfg,
|
cfg: params.cfg,
|
||||||
defaultProvider,
|
defaultProvider,
|
||||||
});
|
});
|
||||||
return { defaultProvider, defaultModel, aliasIndex };
|
return { defaultProvider, defaultModel, aliasIndex };
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import { SILENT_REPLY_TOKEN } from "../tokens.js";
|
|||||||
import type { GetReplyOptions, ReplyPayload } from "../types.js";
|
import type { GetReplyOptions, ReplyPayload } from "../types.js";
|
||||||
import { runReplyAgent } from "./agent-runner.js";
|
import { runReplyAgent } from "./agent-runner.js";
|
||||||
import { applySessionHints } from "./body.js";
|
import { applySessionHints } from "./body.js";
|
||||||
|
import { routeReply } from "./route-reply.js";
|
||||||
import type { buildCommandContext } from "./commands.js";
|
import type { buildCommandContext } from "./commands.js";
|
||||||
import type { InlineDirectives } from "./directive-handling.js";
|
import type { InlineDirectives } from "./directive-handling.js";
|
||||||
import { buildGroupIntro } from "./groups.js";
|
import { buildGroupIntro } from "./groups.js";
|
||||||
@@ -51,7 +52,7 @@ type AgentDefaults = NonNullable<ClawdbotConfig["agents"]>["defaults"];
|
|||||||
type ExecOverrides = Pick<ExecToolDefaults, "host" | "security" | "ask" | "node">;
|
type ExecOverrides = Pick<ExecToolDefaults, "host" | "security" | "ask" | "node">;
|
||||||
|
|
||||||
const BARE_SESSION_RESET_PROMPT =
|
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 = {
|
type RunPreparedReplyParams = {
|
||||||
ctx: MsgContext;
|
ctx: MsgContext;
|
||||||
@@ -92,9 +93,11 @@ type RunPreparedReplyParams = {
|
|||||||
};
|
};
|
||||||
typing: TypingController;
|
typing: TypingController;
|
||||||
opts?: GetReplyOptions;
|
opts?: GetReplyOptions;
|
||||||
|
defaultProvider: string;
|
||||||
defaultModel: string;
|
defaultModel: string;
|
||||||
timeoutMs: number;
|
timeoutMs: number;
|
||||||
isNewSession: boolean;
|
isNewSession: boolean;
|
||||||
|
resetTriggered: boolean;
|
||||||
systemSent: boolean;
|
systemSent: boolean;
|
||||||
sessionEntry?: SessionEntry;
|
sessionEntry?: SessionEntry;
|
||||||
sessionStore?: Record<string, SessionEntry>;
|
sessionStore?: Record<string, SessionEntry>;
|
||||||
@@ -220,9 +223,11 @@ export async function runPreparedReply(
|
|||||||
perMessageQueueOptions,
|
perMessageQueueOptions,
|
||||||
typing,
|
typing,
|
||||||
opts,
|
opts,
|
||||||
|
defaultProvider,
|
||||||
defaultModel,
|
defaultModel,
|
||||||
timeoutMs,
|
timeoutMs,
|
||||||
isNewSession,
|
isNewSession,
|
||||||
|
resetTriggered,
|
||||||
systemSent,
|
systemSent,
|
||||||
sessionKey,
|
sessionKey,
|
||||||
sessionId,
|
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 sessionIdFinal = sessionId ?? crypto.randomUUID();
|
||||||
const sessionFile = resolveSessionFilePath(sessionIdFinal, sessionEntry);
|
const sessionFile = resolveSessionFilePath(sessionIdFinal, sessionEntry);
|
||||||
const queueBodyBase = [threadStarterNote, baseBodyFinal].filter(Boolean).join("\n\n");
|
const queueBodyBase = [threadStarterNote, baseBodyFinal].filter(Boolean).join("\n\n");
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { handleInlineActions } from "./get-reply-inline-actions.js";
|
|||||||
import { runPreparedReply } from "./get-reply-run.js";
|
import { runPreparedReply } from "./get-reply-run.js";
|
||||||
import { finalizeInboundContext } from "./inbound-context.js";
|
import { finalizeInboundContext } from "./inbound-context.js";
|
||||||
import { initSessionState } from "./session.js";
|
import { initSessionState } from "./session.js";
|
||||||
|
import { applyResetModelOverride } from "./session-reset-model.js";
|
||||||
import { stageSandboxMedia } from "./stage-sandbox-media.js";
|
import { stageSandboxMedia } from "./stage-sandbox-media.js";
|
||||||
import { createTypingController } from "./typing.js";
|
import { createTypingController } from "./typing.js";
|
||||||
|
|
||||||
@@ -103,6 +104,7 @@ export async function getReplyFromConfig(
|
|||||||
sessionKey,
|
sessionKey,
|
||||||
sessionId,
|
sessionId,
|
||||||
isNewSession,
|
isNewSession,
|
||||||
|
resetTriggered,
|
||||||
systemSent,
|
systemSent,
|
||||||
abortedLastRun,
|
abortedLastRun,
|
||||||
storePath,
|
storePath,
|
||||||
@@ -110,8 +112,24 @@ export async function getReplyFromConfig(
|
|||||||
groupResolution,
|
groupResolution,
|
||||||
isGroup,
|
isGroup,
|
||||||
triggerBodyNormalized,
|
triggerBodyNormalized,
|
||||||
|
bodyStripped,
|
||||||
} = sessionState;
|
} = sessionState;
|
||||||
|
|
||||||
|
await applyResetModelOverride({
|
||||||
|
cfg,
|
||||||
|
resetTriggered,
|
||||||
|
bodyStripped,
|
||||||
|
sessionCtx,
|
||||||
|
ctx: finalized,
|
||||||
|
sessionEntry,
|
||||||
|
sessionStore,
|
||||||
|
sessionKey,
|
||||||
|
storePath,
|
||||||
|
defaultProvider,
|
||||||
|
defaultModel,
|
||||||
|
aliasIndex,
|
||||||
|
});
|
||||||
|
|
||||||
const directiveResult = await resolveReplyDirectives({
|
const directiveResult = await resolveReplyDirectives({
|
||||||
ctx: finalized,
|
ctx: finalized,
|
||||||
cfg,
|
cfg,
|
||||||
@@ -256,9 +274,11 @@ export async function getReplyFromConfig(
|
|||||||
perMessageQueueOptions,
|
perMessageQueueOptions,
|
||||||
typing,
|
typing,
|
||||||
opts,
|
opts,
|
||||||
|
defaultProvider,
|
||||||
defaultModel,
|
defaultModel,
|
||||||
timeoutMs,
|
timeoutMs,
|
||||||
isNewSession,
|
isNewSession,
|
||||||
|
resetTriggered,
|
||||||
systemSent,
|
systemSent,
|
||||||
sessionEntry,
|
sessionEntry,
|
||||||
sessionStore,
|
sessionStore,
|
||||||
|
|||||||
@@ -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");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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<string>;
|
||||||
|
}): 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<string, SessionEntry>;
|
||||||
|
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<string, SessionEntry>;
|
||||||
|
sessionKey?: string;
|
||||||
|
storePath?: string;
|
||||||
|
defaultProvider: string;
|
||||||
|
defaultModel: string;
|
||||||
|
aliasIndex: ModelAliasIndex;
|
||||||
|
}): Promise<ResetModelResult> {
|
||||||
|
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<string>();
|
||||||
|
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 };
|
||||||
|
}
|
||||||
@@ -151,6 +151,34 @@ describe("initSessionState RawBody", () => {
|
|||||||
expect(result.bodyStripped).toBe("");
|
expect(result.bodyStripped).toBe("");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("preserves argument casing while still matching reset triggers case-insensitively", async () => {
|
||||||
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-rawbody-reset-case-"));
|
||||||
|
const storePath = path.join(root, "sessions.json");
|
||||||
|
|
||||||
|
const cfg = {
|
||||||
|
session: {
|
||||||
|
store: storePath,
|
||||||
|
resetTriggers: ["/new"],
|
||||||
|
},
|
||||||
|
} as ClawdbotConfig;
|
||||||
|
|
||||||
|
const ctx = {
|
||||||
|
RawBody: "/NEW KeepThisCase",
|
||||||
|
ChatType: "direct",
|
||||||
|
SessionKey: "agent:main:whatsapp:dm:S1",
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await initSessionState({
|
||||||
|
ctx,
|
||||||
|
cfg,
|
||||||
|
commandAuthorized: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.isNewSession).toBe(true);
|
||||||
|
expect(result.bodyStripped).toBe("KeepThisCase");
|
||||||
|
expect(result.triggerBodyNormalized).toBe("/NEW KeepThisCase");
|
||||||
|
});
|
||||||
|
|
||||||
it("falls back to Body when RawBody is undefined", async () => {
|
it("falls back to Body when RawBody is undefined", async () => {
|
||||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-rawbody-fallback-"));
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-rawbody-fallback-"));
|
||||||
const storePath = path.join(root, "sessions.json");
|
const storePath = path.join(root, "sessions.json");
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ export type SessionInitResult = {
|
|||||||
sessionKey: string;
|
sessionKey: string;
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
isNewSession: boolean;
|
isNewSession: boolean;
|
||||||
|
resetTriggered: boolean;
|
||||||
systemSent: boolean;
|
systemSent: boolean;
|
||||||
abortedLastRun: boolean;
|
abortedLastRun: boolean;
|
||||||
storePath: string;
|
storePath: string;
|
||||||
@@ -135,7 +136,10 @@ export async function initSessionState(params: {
|
|||||||
// Prefer CommandBody/RawBody (clean message) for command detection; fall back
|
// Prefer CommandBody/RawBody (clean message) for command detection; fall back
|
||||||
// to Body which may contain structural context (history, sender labels).
|
// to Body which may contain structural context (history, sender labels).
|
||||||
const commandSource = ctx.BodyForCommands ?? ctx.CommandBody ?? ctx.RawBody ?? ctx.Body ?? "";
|
const commandSource = ctx.BodyForCommands ?? ctx.CommandBody ?? ctx.RawBody ?? ctx.Body ?? "";
|
||||||
const triggerBodyNormalized = stripStructuralPrefixes(commandSource).trim().toLowerCase();
|
// IMPORTANT: do NOT lowercase the entire command body.
|
||||||
|
// Users often pass case-sensitive arguments (e.g. filesystem paths on Linux).
|
||||||
|
// Command parsing downstream lowercases only the command token for matching.
|
||||||
|
const triggerBodyNormalized = stripStructuralPrefixes(commandSource).trim();
|
||||||
|
|
||||||
// Use CommandBody/RawBody for reset trigger matching (clean message without structural context).
|
// Use CommandBody/RawBody for reset trigger matching (clean message without structural context).
|
||||||
const rawBody = commandSource;
|
const rawBody = commandSource;
|
||||||
@@ -151,17 +155,27 @@ export async function initSessionState(params: {
|
|||||||
const strippedForReset = isGroup
|
const strippedForReset = isGroup
|
||||||
? stripMentions(triggerBodyNormalized, ctx, cfg, agentId)
|
? stripMentions(triggerBodyNormalized, ctx, cfg, agentId)
|
||||||
: triggerBodyNormalized;
|
: triggerBodyNormalized;
|
||||||
|
|
||||||
|
// Reset triggers are configured as lowercased commands (e.g. "/new"), but users may type
|
||||||
|
// "/NEW" etc. Match case-insensitively while keeping the original casing for any stripped body.
|
||||||
|
const trimmedBodyLower = trimmedBody.toLowerCase();
|
||||||
|
const strippedForResetLower = strippedForReset.toLowerCase();
|
||||||
|
|
||||||
for (const trigger of resetTriggers) {
|
for (const trigger of resetTriggers) {
|
||||||
if (!trigger) continue;
|
if (!trigger) continue;
|
||||||
if (!resetAuthorized) break;
|
if (!resetAuthorized) break;
|
||||||
if (trimmedBody === trigger || strippedForReset === trigger) {
|
const triggerLower = trigger.toLowerCase();
|
||||||
|
if (trimmedBodyLower === triggerLower || strippedForResetLower === triggerLower) {
|
||||||
isNewSession = true;
|
isNewSession = true;
|
||||||
bodyStripped = "";
|
bodyStripped = "";
|
||||||
resetTriggered = true;
|
resetTriggered = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
const triggerPrefix = `${trigger} `;
|
const triggerPrefixLower = `${triggerLower} `;
|
||||||
if (trimmedBody.startsWith(triggerPrefix) || strippedForReset.startsWith(triggerPrefix)) {
|
if (
|
||||||
|
trimmedBodyLower.startsWith(triggerPrefixLower) ||
|
||||||
|
strippedForResetLower.startsWith(triggerPrefixLower)
|
||||||
|
) {
|
||||||
isNewSession = true;
|
isNewSession = true;
|
||||||
bodyStripped = strippedForReset.slice(trigger.length).trimStart();
|
bodyStripped = strippedForReset.slice(trigger.length).trimStart();
|
||||||
resetTriggered = true;
|
resetTriggered = true;
|
||||||
@@ -327,6 +341,7 @@ export async function initSessionState(params: {
|
|||||||
sessionKey,
|
sessionKey,
|
||||||
sessionId: sessionId ?? crypto.randomUUID(),
|
sessionId: sessionId ?? crypto.randomUUID(),
|
||||||
isNewSession,
|
isNewSession,
|
||||||
|
resetTriggered,
|
||||||
systemSent,
|
systemSent,
|
||||||
abortedLastRun,
|
abortedLastRun,
|
||||||
storePath,
|
storePath,
|
||||||
|
|||||||
@@ -107,6 +107,12 @@ export function createGatewayCloseHandler(params: {
|
|||||||
await params.browserControl.stop().catch(() => {});
|
await params.browserControl.stop().catch(() => {});
|
||||||
}
|
}
|
||||||
await new Promise<void>((resolve) => params.wss.close(() => resolve()));
|
await new Promise<void>((resolve) => params.wss.close(() => resolve()));
|
||||||
|
const httpServer = params.httpServer as HttpServer & {
|
||||||
|
closeIdleConnections?: () => void;
|
||||||
|
};
|
||||||
|
if (typeof httpServer.closeIdleConnections === "function") {
|
||||||
|
httpServer.closeIdleConnections();
|
||||||
|
}
|
||||||
await new Promise<void>((resolve, reject) =>
|
await new Promise<void>((resolve, reject) =>
|
||||||
params.httpServer.close((err) => (err ? reject(err) : resolve())),
|
params.httpServer.close((err) => (err ? reject(err) : resolve())),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ export class CustomEditor extends Editor {
|
|||||||
constructor(theme: EditorTheme) {
|
constructor(theme: EditorTheme) {
|
||||||
super(theme);
|
super(theme);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleInput(data: string): void {
|
handleInput(data: string): void {
|
||||||
if (matchesKey(data, Key.alt("enter")) && this.onAltEnter) {
|
if (matchesKey(data, Key.alt("enter")) && this.onAltEnter) {
|
||||||
this.onAltEnter();
|
this.onAltEnter();
|
||||||
|
|||||||
Reference in new issue
Block a user