diff --git a/README.md b/README.md index a28262bc5a6..6034a0df997 100644 --- a/README.md +++ b/README.md @@ -325,6 +325,7 @@ Send these in WhatsApp/Telegram/Slack/Google Chat/Microsoft Teams/WebChat (group - `/compact` — compact session context (summary) - `/think ` — off|minimal|low|medium|high|xhigh (GPT-5.2 + Codex models only) - `/verbose on|off` +- `/trace on|off` — plugin trace/debug lines only - `/usage off|tokens|full` — per-response usage footer - `/restart` — restart the gateway (owner-only in groups) - `/activation mention|always` — group activation toggle (groups only) diff --git a/docs/channels/group-messages.md b/docs/channels/group-messages.md index f62842ab89e..1d595044062 100644 --- a/docs/channels/group-messages.md +++ b/docs/channels/group-messages.md @@ -15,7 +15,7 @@ Note: `agents.list[].groupChat.mentionPatterns` is now used by Telegram/Discord/ - Activation modes: `mention` (default) or `always`. `mention` requires a ping (real WhatsApp @-mentions via `mentionedJids`, safe regex patterns, or the bot’s E.164 anywhere in the text). `always` wakes the agent on every message but it should reply only when it can add meaningful value; otherwise it returns the exact silent token `NO_REPLY` / `no_reply`. Defaults can be set in config (`channels.whatsapp.groups`) and overridden per group via `/activation`. When `channels.whatsapp.groups` is set, it also acts as a group allowlist (include `"*"` to allow all). - Group policy: `channels.whatsapp.groupPolicy` controls whether group messages are accepted (`open|disabled|allowlist`). `allowlist` uses `channels.whatsapp.groupAllowFrom` (fallback: explicit `channels.whatsapp.allowFrom`). Default is `allowlist` (blocked until you add senders). -- Per-group sessions: session keys look like `agent::whatsapp:group:` so commands such as `/verbose on` or `/think high` (sent as standalone messages) are scoped to that group; personal DM state is untouched. Heartbeats are skipped for group threads. +- Per-group sessions: session keys look like `agent::whatsapp:group:` so commands such as `/verbose on`, `/trace on`, or `/think high` (sent as standalone messages) are scoped to that group; personal DM state is untouched. Heartbeats are skipped for group threads. - Context injection: **pending-only** group messages (default 50) that _did not_ trigger a run are prefixed under `[Chat messages since your last reply - for context]`, with the triggering line under `[Current message - respond to this]`. Messages already in the session are not re-injected. - Sender surfacing: every group batch now ends with `[from: Sender Name (+E164)]` so Pi knows who is speaking. - Ephemeral/view-once: we unwrap those before extracting text/mentions, so pings inside them still trigger. @@ -67,7 +67,7 @@ Only the owner number (from `channels.whatsapp.allowFrom`, or the bot’s own E. 1. Add your WhatsApp account (the one running OpenClaw) to the group. 2. Say `@openclaw …` (or include the number). Only allowlisted senders can trigger it unless you set `groupPolicy: "open"`. 3. The agent prompt will include recent group context plus the trailing `[from: …]` marker so it can address the right person. -4. Session-level directives (`/verbose on`, `/think high`, `/new` or `/reset`, `/compact`) apply only to that group’s session; send them as standalone messages so they register. Your personal DM session remains independent. +4. Session-level directives (`/verbose on`, `/trace on`, `/think high`, `/new` or `/reset`, `/compact`) apply only to that group’s session; send them as standalone messages so they register. Your personal DM session remains independent. ## Testing / verification diff --git a/docs/cli/index.md b/docs/cli/index.md index 6f11e01cda1..687bf623b31 100644 --- a/docs/cli/index.md +++ b/docs/cli/index.md @@ -473,6 +473,7 @@ Chat messages support `/...` commands (text and native). See [/tools/slash-comma Highlights: - `/status` for quick diagnostics. +- `/trace` for session-scoped plugin trace/debug lines. - `/config` for persisted config changes. - `/debug` for runtime-only config overrides (memory, not disk; requires `commands.debug: true`). diff --git a/docs/concepts/active-memory.md b/docs/concepts/active-memory.md index 00b04c1f4bf..d47b35fd4ec 100644 --- a/docs/concepts/active-memory.md +++ b/docs/concepts/active-memory.md @@ -64,6 +64,7 @@ To inspect it live in a conversation: ```text /verbose on +/trace on ``` ## Turn active memory on @@ -148,21 +149,24 @@ The global form writes `plugins.entries.active-memory.config.enabled`. It leaves `plugins.entries.active-memory.enabled` on so the command remains available to turn active memory back on later. -If you want to see what active memory is doing in a live session, turn verbose -mode on for that session: +If you want to see what active memory is doing in a live session, turn on the +session toggles that match the output you want: ```text /verbose on +/trace on ``` -With verbose enabled, OpenClaw can show: +With those enabled, OpenClaw can show: -- an active memory status line such as `Active Memory: ok 842ms recent 34 chars` -- a readable debug summary such as `Active Memory Debug: Lemon pepper wings with blue cheese.` +- an active memory status line such as `Active Memory: ok 842ms recent 34 chars` when `/verbose on` +- a readable debug summary such as `Active Memory Debug: Lemon pepper wings with blue cheese.` when `/trace on` Those lines are derived from the same active memory pass that feeds the hidden system context, but they are formatted for humans instead of exposing raw prompt -markup. +markup. They are sent as a follow-up diagnostic message after the normal +assistant reply so channel clients like Telegram do not flash a separate +pre-reply diagnostic bubble. By default, the blocking memory sub-agent transcript is temporary and deleted after the run completes. @@ -171,6 +175,7 @@ Example flow: ```text /verbose on +/trace on what wings should i order? ``` @@ -568,8 +573,10 @@ Start with `recent`. } ``` -If you want to inspect live behavior while tuning, use `/verbose on` in the -session instead of looking for a separate active-memory debug command. +If you want to inspect live behavior while tuning, use `/verbose on` for the +normal status line and `/trace on` for the active-memory debug summary instead +of looking for a separate active-memory debug command. In chat channels, those +diagnostic lines are sent after the main assistant reply rather than before it. Then move to: @@ -597,6 +604,58 @@ If active memory is too slow: - reduce recent turn counts - reduce per-turn char caps +## Common issues + +### Embedding provider changed unexpectedly + +Active Memory relies on the normal memory search embedding provider under +`agents.defaults.memorySearch`. If you do not set that provider explicitly, +OpenClaw auto-detects the first available embedding provider. + +That can be confusing in real deployments: + +- a newly available API key can change which provider memory search uses +- one command or diagnostics surface may make the selected provider look + different from the path you are actually hitting during live memory sync or + search bootstrap +- hosted providers can fail with quota or rate-limit errors that only show up + once Active Memory starts issuing recall searches before each reply + +If you care about predictable behavior, pin the memory embedding provider +explicitly instead of relying on auto-detection. + +Example: + +```json5 +{ + agents: { + defaults: { + memorySearch: { + provider: "ollama", + model: "nomic-embed-text", + }, + }, + }, +} +``` + +Or, if you want Gemini embeddings: + +```json5 +{ + agents: { + defaults: { + memorySearch: { + provider: "gemini", + }, + }, + }, +} +``` + +After changing the provider, restart the gateway and run a fresh test with +`/trace on` so the Active Memory debug line reflects the new embedding path. + ## Related pages - [Memory Search](/concepts/memory-search) diff --git a/docs/concepts/agent-loop.md b/docs/concepts/agent-loop.md index 580187b5405..5671016f52e 100644 --- a/docs/concepts/agent-loop.md +++ b/docs/concepts/agent-loop.md @@ -24,7 +24,7 @@ wired end-to-end. 1. `agent` RPC validates params, resolves session (sessionKey/sessionId), persists session metadata, returns `{ runId, acceptedAt }` immediately. 2. `agentCommand` runs the agent: - - resolves model + thinking/verbose defaults + - resolves model + thinking/verbose/trace defaults - loads skills snapshot - calls `runEmbeddedPiAgent` (pi-agent-core runtime) - emits **lifecycle end/error** if the embedded loop does not emit one diff --git a/docs/concepts/context.md b/docs/concepts/context.md index 5d9bd60af69..348bb9d5366 100644 --- a/docs/concepts/context.md +++ b/docs/concepts/context.md @@ -136,7 +136,7 @@ Tools affect context in two ways: Slash commands are handled by the Gateway. There are a few different behaviors: - **Standalone commands**: a message that is only `/...` runs as a command. -- **Directives**: `/think`, `/verbose`, `/reasoning`, `/elevated`, `/model`, `/queue` are stripped before the model sees the message. +- **Directives**: `/think`, `/verbose`, `/trace`, `/reasoning`, `/elevated`, `/model`, `/queue` are stripped before the model sees the message. - Directive-only messages persist session settings. - Inline directives in a normal message act as per-message hints. - **Inline shortcuts** (allowlisted senders only): certain `/...` tokens inside a normal message can run immediately (example: “hey /status”), and are stripped before the model sees the remaining text. diff --git a/docs/gateway/security/index.md b/docs/gateway/security/index.md index 699ad3730a5..66c0ad8a4be 100644 --- a/docs/gateway/security/index.md +++ b/docs/gateway/security/index.md @@ -730,15 +730,16 @@ Recommendations: ## Reasoning & verbose output in groups -`/reasoning` and `/verbose` can expose internal reasoning or tool output that +`/reasoning`, `/verbose`, and `/trace` can expose internal reasoning, tool +output, or plugin diagnostics that was not meant for a public channel. In group settings, treat them as **debug only** and keep them off unless you explicitly need them. Guidance: -- Keep `/reasoning` and `/verbose` disabled in public rooms. +- Keep `/reasoning`, `/verbose`, and `/trace` disabled in public rooms. - If you enable them, do so only in trusted DMs or tightly controlled rooms. -- Remember: verbose output can include tool args, URLs, and data the model saw. +- Remember: verbose and trace output can include tool args, URLs, plugin diagnostics, and data the model saw. ## Configuration Hardening (examples) diff --git a/docs/help/debugging.md b/docs/help/debugging.md index dc6a83780cf..88de00e6ca2 100644 --- a/docs/help/debugging.md +++ b/docs/help/debugging.md @@ -29,6 +29,23 @@ Examples: `/debug reset` clears all overrides and returns to the on-disk config. +## Session trace output + +Use `/trace` when you want to see plugin-owned trace/debug lines in one session +without turning on full verbose mode. + +Examples: + +```text +/trace +/trace on +/trace off +``` + +Use `/trace` for plugin diagnostics such as Active Memory debug summaries. +Keep using `/verbose` for normal verbose status/tool output, and keep using +`/debug` for runtime-only config overrides. + ## Gateway watch mode For fast iteration, run the gateway under the file watcher: diff --git a/docs/help/faq.md b/docs/help/faq.md index 0b9bdda16a5..9dd5a8b0d09 100644 --- a/docs/help/faq.md +++ b/docs/help/faq.md @@ -3192,13 +3192,14 @@ Related: [/concepts/oauth](/concepts/oauth) (OAuth flows, token storage, multi-a - Most internal or tool messages only appear when **verbose** or **reasoning** is enabled + Most internal or tool messages only appear when **verbose**, **trace**, or **reasoning** is enabled for that session. Fix in the chat where you see it: ``` /verbose off + /trace off /reasoning off ``` diff --git a/docs/tools/slash-commands.md b/docs/tools/slash-commands.md index d77b0cd6c55..2a39e765f91 100644 --- a/docs/tools/slash-commands.md +++ b/docs/tools/slash-commands.md @@ -14,7 +14,7 @@ The host-only bash chat command uses `! ` (with `/bash ` as an alias). There are two related systems: - **Commands**: standalone `/...` messages. -- **Directives**: `/think`, `/fast`, `/verbose`, `/reasoning`, `/elevated`, `/exec`, `/model`, `/queue`. +- **Directives**: `/think`, `/fast`, `/verbose`, `/trace`, `/reasoning`, `/elevated`, `/exec`, `/model`, `/queue`. - Directives are stripped from the message before the model sees it. - In normal chat messages (not directive-only), they are treated as “inline hints” and do **not** persist session settings. - In directive-only messages (the message contains only directives), they persist to the session and reply with an acknowledgement. @@ -95,6 +95,7 @@ Built-in commands available today: - `/session idle ` and `/session max-age ` manage thread-binding expiry. - `/think ` sets the thinking level. 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. - `/reasoning [on|off|stream]` toggles reasoning visibility. Alias: `/reason`. - `/elevated [on|off|ask|full]` toggles elevated mode. Alias: `/elev`. @@ -183,10 +184,11 @@ Notes: - Discord thread-binding commands (`/focus`, `/unfocus`, `/agents`, `/session idle`, `/session max-age`) require effective thread bindings to be enabled (`session.threadBindings.enabled` and/or `channels.discord.threadBindings.enabled`). - ACP command reference and runtime behavior: [ACP Agents](/tools/acp-agents). - `/verbose` is meant for debugging and extra visibility; keep it **off** in normal use. +- `/trace` is narrower than `/verbose`: it only reveals plugin-owned trace/debug lines and keeps normal verbose tool chatter off. - `/fast on|off` persists a session override. Use the Sessions UI `inherit` option to clear it and fall back to config defaults. - `/fast` is provider-specific: OpenAI/OpenAI Codex map it to `service_tier=priority` on native Responses endpoints, while direct public Anthropic requests, including OAuth-authenticated traffic sent to `api.anthropic.com`, map it to `service_tier=auto` or `standard_only`. See [OpenAI](/providers/openai) and [Anthropic](/providers/anthropic). - Tool failure summaries are still shown when relevant, but detailed failure text is only included when `/verbose` is `on` or `full`. -- `/reasoning` (and `/verbose`) are risky in group settings: they may reveal internal reasoning or tool output you did not intend to expose. Prefer leaving them off, especially in group chats. +- `/reasoning`, `/verbose`, and `/trace` are risky in group settings: they may reveal internal reasoning, tool output, or plugin diagnostics you did not intend to expose. Prefer leaving them off, especially in group chats. - `/model` persists the new session model immediately. - If the agent is idle, the next run uses it right away. - If a run is already active, OpenClaw marks a live switch as pending and only restarts into the new model at a clean retry point. @@ -268,6 +270,27 @@ Notes: - Overrides apply immediately to new config reads, but do **not** write to `openclaw.json`. - Use `/debug reset` to clear all overrides and return to the on-disk config. +## Plugin trace output + +`/trace` lets you toggle **session-scoped plugin trace/debug lines** without turning on full verbose mode. + +Examples: + +```text +/trace +/trace on +/trace off +``` + +Notes: + +- `/trace` with no argument shows the current session trace state. +- `/trace on` enables plugin trace lines for the current session. +- `/trace off` disables them again. +- Plugin trace lines can appear in `/status` and as a follow-up diagnostic message after the normal assistant reply. +- `/trace` does not replace `/debug`; `/debug` still manages runtime-only config overrides. +- `/trace` does not replace `/verbose`; normal verbose tool/status output still belongs to `/verbose`. + ## Config updates `/config` writes to your on-disk config (`openclaw.json`). Owner-only. Disabled by default; enable with `commands.config: true`. diff --git a/docs/tools/thinking.md b/docs/tools/thinking.md index 196ded24f87..3fc3c06ae06 100644 --- a/docs/tools/thinking.md +++ b/docs/tools/thinking.md @@ -1,5 +1,5 @@ --- -summary: "Directive syntax for /think, /fast, /verbose, and reasoning visibility" +summary: "Directive syntax for /think, /fast, /verbose, /trace, and reasoning visibility" read_when: - Adjusting thinking, fast-mode, or verbose directive parsing or defaults title: "Thinking Levels" @@ -72,6 +72,15 @@ title: "Thinking Levels" - Tool failure summaries remain visible in normal mode, but raw error detail suffixes are hidden unless verbose is `on` or `full`. - When verbose is `full`, tool outputs are also forwarded after completion (separate bubble, truncated to a safe length). If you toggle `/verbose on|full|off` while a run is in-flight, subsequent tool bubbles honor the new setting. +## Plugin trace directives (/trace) + +- Levels: `on` | `off` (default). +- Directive-only message toggles session plugin trace output and replies `Plugin trace enabled.` / `Plugin trace disabled.`. +- Inline directive affects only that message; session/global defaults apply otherwise. +- Send `/trace` (or `/trace:`) with no argument to see the current trace level. +- `/trace` is narrower than `/verbose`: it only exposes plugin-owned trace/debug lines such as Active Memory debug summaries. +- Trace lines can appear in `/status` and as a follow-up diagnostic message after the normal assistant reply. + ## Reasoning visibility (/reasoning) - Levels: `on|off|stream`. diff --git a/docs/web/control-ui.md b/docs/web/control-ui.md index b88c4310ede..32623f9c6c8 100644 --- a/docs/web/control-ui.md +++ b/docs/web/control-ui.md @@ -88,7 +88,7 @@ locale picker lives in the Gateway Access card, not under Appearance. - Stream tool calls + live tool output cards in Chat (agent events) - Channels: built-in plus bundled/external plugin channels status, QR login, and per-channel config (`channels.status`, `web.login.*`, `config.patch`) - Instances: presence list + refresh (`system-presence`) -- Sessions: list + per-session model/thinking/fast/verbose/reasoning overrides (`sessions.list`, `sessions.patch`) +- Sessions: list + per-session model/thinking/fast/verbose/trace/reasoning overrides (`sessions.list`, `sessions.patch`) - Dreams: dreaming status, enable/disable toggle, and Dream Diary reader (`doctor.memory.status`, `doctor.memory.dreamDiary`, `config.patch`) - Cron jobs: list/add/edit/run/enable/disable + run history (`cron.*`) - Skills: status, enable/disable, install, API key updates (`skills.*`) diff --git a/docs/web/tui.md b/docs/web/tui.md index 4d3eaef7e5f..d643c6f10d6 100644 --- a/docs/web/tui.md +++ b/docs/web/tui.md @@ -37,7 +37,7 @@ Use `--password` if your Gateway uses password auth. - Header: connection URL, current agent, current session. - Chat log: user messages, assistant replies, system notices, tool cards. - Status line: connection/run state (connecting, running, streaming, idle, error). -- Footer: connection state + agent + session + model + think/fast/verbose/reasoning + token counts + deliver. +- Footer: connection state + agent + session + model + think/fast/verbose/trace/reasoning + token counts + deliver. - Input: text editor with autocomplete. ## Mental model: agents + sessions @@ -94,6 +94,7 @@ Session controls: - `/think ` - `/fast ` - `/verbose ` +- `/trace ` - `/reasoning ` - `/usage ` - `/elevated ` (alias: `/elev`) diff --git a/extensions/active-memory/index.test.ts b/extensions/active-memory/index.test.ts index 936f9e6a9cc..3e04ff52a53 100644 --- a/extensions/active-memory/index.test.ts +++ b/extensions/active-memory/index.test.ts @@ -1191,6 +1191,49 @@ describe("active-memory plugin", () => { }); }); + it("surfaces memory embedding quota warnings in plugin trace lines", async () => { + const sessionKey = "agent:main:memory-rate-limit"; + hoisted.sessionStore[sessionKey] = { + sessionId: "s-rate-limit", + updatedAt: 0, + }; + runEmbeddedPiAgent.mockImplementationOnce(async () => { + return { + meta: { + activeMemorySearchDebug: { + warning: + "Memory search is unavailable because the embedding provider quota is exhausted.", + action: "Top up or switch embedding provider, then retry memory_search.", + error: "gemini embeddings failed: 429 rate limited", + }, + }, + payloads: [{ text: "NONE" }], + }; + }); + + await hooks.before_prompt_build( + { prompt: "what should i eat tonight?", messages: [] }, + { + agentId: "main", + trigger: "user", + sessionKey, + messageProvider: "webchat", + }, + ); + + expect(hoisted.sessionStore[sessionKey]?.pluginDebugEntries).toEqual([ + { + pluginId: "active-memory", + lines: [ + expect.stringContaining("🧩 Active Memory: empty"), + expect.stringContaining( + "🔎 Active Memory Debug: Memory search is unavailable because the embedding provider quota is exhausted. Top up or switch embedding provider, then retry memory_search.", + ), + ], + }, + ]); + }); + it("prefers the resolved session channel over a wrapper channel hint", async () => { hoisted.sessionStore["agent:main:telegram:direct:12345"] = { sessionId: "session-a", diff --git a/extensions/active-memory/index.ts b/extensions/active-memory/index.ts index 36c8b0b0a63..679164d0969 100644 --- a/extensions/active-memory/index.ts +++ b/extensions/active-memory/index.ts @@ -139,6 +139,9 @@ type ActiveMemorySearchDebug = { fallback?: string; searchMs?: number; hits?: number; + warning?: string; + action?: string; + error?: string; }; type ActiveRecallResult = @@ -1016,6 +1019,9 @@ function buildPluginDebugLine(params: { searchDebug?: ActiveMemorySearchDebug; }): string | null { const cleaned = sanitizeDebugText(params.summary ?? ""); + const warning = sanitizeDebugText(params.searchDebug?.warning ?? ""); + const action = sanitizeDebugText(params.searchDebug?.action ?? ""); + const error = sanitizeDebugText(params.searchDebug?.error ?? ""); const debugParts: string[] = []; const backend = sanitizeDebugText(params.searchDebug?.backend ?? ""); if (backend) { @@ -1043,15 +1049,34 @@ function buildPluginDebugLine(params: { debugParts.push(`hits=${Math.max(0, Math.floor(params.searchDebug.hits))}`); } const prefix = debugParts.join(" "); - if (prefix && cleaned) { - return `${ACTIVE_MEMORY_DEBUG_PREFIX} ${prefix} | ${cleaned}`; + const warningAction = + warning && action && !cleaned + ? `${warning} ${action}` + : [warning, action && !cleaned ? action : ""] + .filter((value, index, values) => Boolean(value) && values.indexOf(value) === index) + .join(" | "); + const messages = [warningAction, cleaned] + .filter((value, index, values) => Boolean(value) && values.indexOf(value) === index) + .join(" | "); + const trailing = messages; + if (prefix && trailing) { + return `${ACTIVE_MEMORY_DEBUG_PREFIX} ${prefix} | ${trailing}`; } if (prefix) { return `${ACTIVE_MEMORY_DEBUG_PREFIX} ${prefix}`; } + if (messages) { + return `${ACTIVE_MEMORY_DEBUG_PREFIX} ${messages}`; + } + if (warning) { + return `${ACTIVE_MEMORY_DEBUG_PREFIX} ${warning}`; + } if (cleaned) { return `${ACTIVE_MEMORY_DEBUG_PREFIX} ${cleaned}`; } + if (error) { + return `${ACTIVE_MEMORY_DEBUG_PREFIX} ${error}`; + } return null; } @@ -1175,7 +1200,10 @@ async function readActiveMemorySearchDebug( } const details = asRecord(message.details); const debug = asRecord(details?.debug); - if (!debug) { + const warning = normalizeOptionalString(details?.warning); + const action = normalizeOptionalString(details?.action); + const error = normalizeOptionalString(details?.error); + if (!debug && !warning && !action && !error) { continue; } return { @@ -1189,6 +1217,9 @@ async function readActiveMemorySearchDebug( : undefined, hits: typeof debug.hits === "number" && Number.isFinite(debug.hits) ? debug.hits : undefined, + warning, + action, + error, }; } catch { continue; @@ -1212,13 +1243,19 @@ function normalizeSearchDebug(value: unknown): ActiveMemorySearchDebug | undefin ? debug.searchMs : undefined, hits: typeof debug.hits === "number" && Number.isFinite(debug.hits) ? debug.hits : undefined, + warning: normalizeOptionalString(debug.warning) ?? normalizeOptionalString(debug.reason), + action: normalizeOptionalString(debug.action), + error: normalizeOptionalString(debug.error), }; return normalized.backend || normalized.configuredMode || normalized.effectiveMode || normalized.fallback || typeof normalized.searchMs === "number" || - typeof normalized.hits === "number" + typeof normalized.hits === "number" || + normalized.warning || + normalized.action || + normalized.error ? normalized : undefined; } diff --git a/extensions/memory-core/src/tools.shared.ts b/extensions/memory-core/src/tools.shared.ts index fb5d36cc572..26e1d2aee67 100644 --- a/extensions/memory-core/src/tools.shared.ts +++ b/extensions/memory-core/src/tools.shared.ts @@ -133,6 +133,11 @@ export function buildMemorySearchUnavailableResult(error: string | undefined) { error: reason, warning, action, + debug: { + warning, + action, + error: reason, + }, }; } diff --git a/extensions/memory-core/src/tools.test-helpers.ts b/extensions/memory-core/src/tools.test-helpers.ts index 71c72d1a687..54c84deadff 100644 --- a/extensions/memory-core/src/tools.test-helpers.ts +++ b/extensions/memory-core/src/tools.test-helpers.ts @@ -59,5 +59,10 @@ export function expectUnavailableMemorySearchDetails( error: params.error, warning: params.warning, action: params.action, + debug: { + warning: params.warning, + action: params.action, + error: params.error, + }, }); } diff --git a/src/acp/commands.ts b/src/acp/commands.ts index a31a3c01ffe..b50b088d791 100644 --- a/src/acp/commands.ts +++ b/src/acp/commands.ts @@ -27,6 +27,7 @@ const BASE_AVAILABLE_COMMANDS: AvailableCommand[] = [ description: "Set thinking level (off|minimal|low|medium|high|xhigh).", }, { name: "verbose", description: "Set verbose mode (on|full|off)." }, + { name: "trace", description: "Set plugin trace mode (on|off)." }, { name: "reasoning", description: "Toggle reasoning output (on|off|stream)." }, { name: "elevated", description: "Toggle elevated mode (on|off)." }, { name: "model", description: "Select a model (list|status|)." }, diff --git a/src/acp/translator.ts b/src/acp/translator.ts index 146c00798ec..0cc512d7d87 100644 --- a/src/acp/translator.ts +++ b/src/acp/translator.ts @@ -56,6 +56,7 @@ const MAX_PROMPT_BYTES = 2 * 1024 * 1024; const ACP_THOUGHT_LEVEL_CONFIG_ID = "thought_level"; const ACP_FAST_MODE_CONFIG_ID = "fast_mode"; const ACP_VERBOSE_LEVEL_CONFIG_ID = "verbose_level"; +const ACP_TRACE_LEVEL_CONFIG_ID = "trace_level"; const ACP_REASONING_LEVEL_CONFIG_ID = "reasoning_level"; const ACP_RESPONSE_USAGE_CONFIG_ID = "response_usage"; const ACP_ELEVATED_LEVEL_CONFIG_ID = "elevated_level"; @@ -104,6 +105,7 @@ type GatewaySessionPresentationRow = Pick< | "modelProvider" | "model" | "verboseLevel" + | "traceLevel" | "reasoningLevel" | "responseUsage" | "elevatedLevel" @@ -272,6 +274,13 @@ function buildSessionPresentation(params: { currentValue: normalizeOptionalString(row.verboseLevel) || "off", values: ["off", "on", "full"], }), + buildSelectConfigOption({ + id: ACP_TRACE_LEVEL_CONFIG_ID, + name: "Plugin trace", + description: "Controls whether plugin-owned trace lines are shown for the session.", + currentValue: normalizeOptionalString(row.traceLevel) || "off", + values: ["off", "on"], + }), buildSelectConfigOption({ id: ACP_REASONING_LEVEL_CONFIG_ID, name: "Reasoning stream", @@ -1250,6 +1259,7 @@ export class AcpGatewayAgent implements Agent { model: session.model, fastMode: session.fastMode, verboseLevel: session.verboseLevel, + traceLevel: session.traceLevel, reasoningLevel: session.reasoningLevel, responseUsage: session.responseUsage, elevatedLevel: session.elevatedLevel, @@ -1287,6 +1297,11 @@ export class AcpGatewayAgent implements Agent { patch: { verboseLevel: value }, overrides: { verboseLevel: value }, }; + case ACP_TRACE_LEVEL_CONFIG_ID: + return { + patch: { traceLevel: value }, + overrides: { traceLevel: value }, + }; case ACP_REASONING_LEVEL_CONFIG_ID: return { patch: { reasoningLevel: value }, diff --git a/src/auto-reply/command-status-builders.ts b/src/auto-reply/command-status-builders.ts index 3246b63eb59..6f2cfdd6b79 100644 --- a/src/auto-reply/command-status-builders.ts +++ b/src/auto-reply/command-status-builders.ts @@ -58,7 +58,13 @@ export function buildHelpMessage(cfg?: OpenClawConfig): string { lines.push(" /new | /reset | /compact [instructions] | /stop"); lines.push(""); - const optionParts = ["/think ", "/model ", "/fast status|on|off", "/verbose on|off"]; + const optionParts = [ + "/think ", + "/model ", + "/fast status|on|off", + "/verbose on|off", + "/trace on|off", + ]; if (isCommandFlagEnabled(cfg, "config")) { optionParts.push("/config"); } diff --git a/src/auto-reply/commands-registry.shared.ts b/src/auto-reply/commands-registry.shared.ts index 9526f7fdf88..587c6107eee 100644 --- a/src/auto-reply/commands-registry.shared.ts +++ b/src/auto-reply/commands-registry.shared.ts @@ -671,6 +671,22 @@ export function buildBuiltinChatCommands(): ChatCommandDefinition[] { ], argsMenu: "auto", }), + defineChatCommand({ + key: "trace", + nativeName: "trace", + description: "Toggle plugin trace lines.", + textAlias: "/trace", + category: "options", + args: [ + { + name: "mode", + description: "on or off", + type: "string", + choices: ["on", "off"], + }, + ], + argsMenu: "auto", + }), defineChatCommand({ key: "fast", nativeName: "fast", diff --git a/src/auto-reply/reply.directive.directive-behavior.shows-current-verbose-level-verbose-has-no.test.ts b/src/auto-reply/reply.directive.directive-behavior.shows-current-verbose-level-verbose-has-no.test.ts index 003af7cefab..c813305851d 100644 --- a/src/auto-reply/reply.directive.directive-behavior.shows-current-verbose-level-verbose-has-no.test.ts +++ b/src/auto-reply/reply.directive.directive-behavior.shows-current-verbose-level-verbose-has-no.test.ts @@ -257,4 +257,22 @@ describe("directive behavior", () => { expect(reset.sessionEntry.queueDrop).toBeUndefined(); expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled(); }); + + it("shows current trace level and persists trace directives", async () => { + const { text: currentText } = await runDirectiveStatus("/trace", { + sessionEntry: { traceLevel: "on" }, + }); + expect(currentText).toContain("Current trace level: on"); + + const enabled = await runDirectiveStatus("/trace on"); + expect(enabled.text).toContain("Plugin trace enabled."); + expect(enabled.sessionEntry.traceLevel).toBe("on"); + + const disabled = await runDirectiveStatus("/trace off", { + sessionEntry: { traceLevel: "on" }, + }); + expect(disabled.text).toContain("Plugin trace disabled."); + expect(disabled.sessionEntry.traceLevel).toBe("off"); + expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled(); + }); }); diff --git a/src/auto-reply/reply.directive.parse.test.ts b/src/auto-reply/reply.directive.parse.test.ts index dc9fe0b1a2d..679ce0165d2 100644 --- a/src/auto-reply/reply.directive.parse.test.ts +++ b/src/auto-reply/reply.directive.parse.test.ts @@ -5,6 +5,7 @@ import { extractQueueDirective, extractReasoningDirective, extractReplyToTag, + extractTraceDirective, extractThinkDirective, extractVerboseDirective, } from "./reply.js"; @@ -38,6 +39,12 @@ describe("directive parsing", () => { expect(res.verboseLevel).toBe("on"); }); + it("matches trace with leading space", () => { + const res = extractTraceDirective(" please /trace on now"); + expect(res.hasDirective).toBe(true); + expect(res.traceLevel).toBe("on"); + }); + it("matches reasoning directive", () => { const res = extractReasoningDirective("/reasoning on please"); expect(res.hasDirective).toBe(true); diff --git a/src/auto-reply/reply.ts b/src/auto-reply/reply.ts index 56d13ff7d38..e99178d9df2 100644 --- a/src/auto-reply/reply.ts +++ b/src/auto-reply/reply.ts @@ -1,6 +1,7 @@ export { extractElevatedDirective, extractReasoningDirective, + extractTraceDirective, extractThinkDirective, extractVerboseDirective, } from "./reply/directives.js"; diff --git a/src/auto-reply/reply/agent-runner.misc.runreplyagent.test.ts b/src/auto-reply/reply/agent-runner.misc.runreplyagent.test.ts index 8e2e8d7c538..2a197da1761 100644 --- a/src/auto-reply/reply/agent-runner.misc.runreplyagent.test.ts +++ b/src/auto-reply/reply/agent-runner.misc.runreplyagent.test.ts @@ -485,13 +485,14 @@ describe("runReplyAgent block streaming", () => { }); describe("runReplyAgent Active Memory inline debug", () => { - it("appends inline Active Memory debug payload when verbose is enabled", async () => { + it("appends inline Active Memory status payload when verbose is enabled", async () => { const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-active-memory-inline-")); const storePath = path.join(tmp, "sessions.json"); const sessionKey = "main"; const sessionEntry: SessionEntry = { sessionId: "session", updatedAt: Date.now(), + verboseLevel: "on", }; await fs.writeFile( @@ -589,8 +590,229 @@ describe("runReplyAgent Active Memory inline debug", () => { expect(Array.isArray(result)).toBe(true); expect((result as { text?: string }[]).map((payload) => payload.text)).toEqual([ - "🧩 Active Memory: ok 842ms recent 34 chars\n🔎 Active Memory Debug: Lemon pepper wings with blue cheese.", "Normal reply", + "🧩 Active Memory: ok 842ms recent 34 chars", + ]); + }); + + it("appends inline Active Memory status and trace payloads when verbose and trace are enabled", async () => { + const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-active-memory-inline-")); + const storePath = path.join(tmp, "sessions.json"); + const sessionKey = "main"; + const sessionEntry: SessionEntry = { + sessionId: "session", + updatedAt: Date.now(), + verboseLevel: "on", + traceLevel: "on", + }; + + await fs.writeFile( + storePath, + JSON.stringify( + { + [sessionKey]: sessionEntry, + }, + null, + 2, + ), + "utf-8", + ); + + runEmbeddedPiAgentMock.mockImplementationOnce(async () => { + const latest = loadSessionStore(storePath, { skipCache: true }); + latest[sessionKey] = { + ...latest[sessionKey], + pluginDebugEntries: [ + { + pluginId: "active-memory", + lines: [ + "🧩 Active Memory: ok 842ms recent 34 chars", + "🔎 Active Memory Debug: Lemon pepper wings with blue cheese.", + ], + }, + ], + }; + await saveSessionStore(storePath, latest); + return { + payloads: [{ text: "Normal reply" }], + meta: {}, + }; + }); + + const typing = createMockTypingController(); + const sessionCtx = { + Provider: "telegram", + OriginatingTo: "chat:1", + AccountId: "primary", + MessageSid: "msg", + } as unknown as TemplateContext; + const resolvedQueue = { mode: "interrupt" } as unknown as QueueSettings; + const followupRun = { + prompt: "hello", + summaryLine: "hello", + enqueuedAt: Date.now(), + run: { + agentId: "main", + sessionId: "session", + sessionKey, + messageProvider: "telegram", + sessionFile: "/tmp/session.jsonl", + workspaceDir: "/tmp", + config: {}, + skillsSnapshot: {}, + provider: "anthropic", + model: "claude", + thinkLevel: "low", + verboseLevel: "on", + elevatedLevel: "off", + bashElevated: { + enabled: false, + allowed: false, + defaultLevel: "off", + }, + timeoutMs: 1_000, + blockReplyBreak: "message_end", + }, + } as unknown as FollowupRun; + + const result = await runReplyAgent({ + commandBody: "hello", + followupRun, + queueKey: sessionKey, + resolvedQueue, + shouldSteer: false, + shouldFollowup: false, + isActive: false, + isStreaming: false, + typing, + sessionCtx, + sessionEntry, + sessionStore: { [sessionKey]: sessionEntry }, + sessionKey, + storePath, + defaultModel: "anthropic/claude-opus-4-6", + resolvedVerboseLevel: "on", + isNewSession: false, + blockStreamingEnabled: false, + resolvedBlockStreamingBreak: "message_end", + shouldInjectGroupIntro: false, + typingMode: "instant", + }); + + expect(Array.isArray(result)).toBe(true); + expect((result as { text?: string }[]).map((payload) => payload.text)).toEqual([ + "Normal reply", + "🧩 Active Memory: ok 842ms recent 34 chars\n🔎 Active Memory Debug: Lemon pepper wings with blue cheese.", + ]); + }); + + it("appends inline Active Memory trace payload when only trace is enabled", async () => { + const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-active-memory-inline-")); + const storePath = path.join(tmp, "sessions.json"); + const sessionKey = "main"; + const sessionEntry: SessionEntry = { + sessionId: "session", + updatedAt: Date.now(), + traceLevel: "on", + }; + + await fs.writeFile( + storePath, + JSON.stringify( + { + [sessionKey]: sessionEntry, + }, + null, + 2, + ), + "utf-8", + ); + + runEmbeddedPiAgentMock.mockImplementationOnce(async () => { + const latest = loadSessionStore(storePath, { skipCache: true }); + latest[sessionKey] = { + ...latest[sessionKey], + pluginDebugEntries: [ + { + pluginId: "active-memory", + lines: [ + "🧩 Active Memory: ok 842ms recent 34 chars", + "🔎 Active Memory Debug: Lemon pepper wings with blue cheese.", + ], + }, + ], + }; + await saveSessionStore(storePath, latest); + return { + payloads: [{ text: "Normal reply" }], + meta: {}, + }; + }); + + const typing = createMockTypingController(); + const sessionCtx = { + Provider: "telegram", + OriginatingTo: "chat:1", + AccountId: "primary", + MessageSid: "msg", + } as unknown as TemplateContext; + const resolvedQueue = { mode: "interrupt" } as unknown as QueueSettings; + const followupRun = { + prompt: "hello", + summaryLine: "hello", + enqueuedAt: Date.now(), + run: { + agentId: "main", + sessionId: "session", + sessionKey, + messageProvider: "telegram", + sessionFile: "/tmp/session.jsonl", + workspaceDir: "/tmp", + config: {}, + skillsSnapshot: {}, + provider: "anthropic", + model: "claude", + thinkLevel: "low", + verboseLevel: "on", + elevatedLevel: "off", + bashElevated: { + enabled: false, + allowed: false, + defaultLevel: "off", + }, + timeoutMs: 1_000, + blockReplyBreak: "message_end", + }, + } as unknown as FollowupRun; + + const result = await runReplyAgent({ + commandBody: "hello", + followupRun, + queueKey: sessionKey, + resolvedQueue, + shouldSteer: false, + shouldFollowup: false, + isActive: false, + isStreaming: false, + typing, + sessionCtx, + sessionEntry, + sessionStore: { [sessionKey]: sessionEntry }, + sessionKey, + storePath, + defaultModel: "anthropic/claude-opus-4-6", + resolvedVerboseLevel: "on", + isNewSession: false, + blockStreamingEnabled: false, + resolvedBlockStreamingBreak: "message_end", + shouldInjectGroupIntro: false, + typingMode: "instant", + }); + + expect(Array.isArray(result)).toBe(true); + expect((result as { text?: string }[]).map((payload) => payload.text)).toEqual([ + "Normal reply", + "🔎 Active Memory Debug: Lemon pepper wings with blue cheese.", ]); }); diff --git a/src/auto-reply/reply/agent-runner.ts b/src/auto-reply/reply/agent-runner.ts index f4f0fac5123..339ca6bef16 100644 --- a/src/auto-reply/reply/agent-runner.ts +++ b/src/auto-reply/reply/agent-runner.ts @@ -6,7 +6,8 @@ import { queueEmbeddedPiMessage } from "../../agents/pi-embedded-runner/runs.js" import { hasNonzeroUsage } from "../../agents/usage.js"; import { loadSessionStore, - resolveSessionPluginDebugLines, + resolveSessionPluginStatusLines, + resolveSessionPluginTraceLines, type SessionEntry, updateSessionStoreEntry, } from "../../config/sessions.js"; @@ -71,7 +72,12 @@ import type { TypingController } from "./typing.js"; const BLOCK_REPLY_SEND_TIMEOUT_MS = 15_000; function buildInlinePluginStatusPayload(entry: SessionEntry | undefined): ReplyPayload | undefined { - const lines = resolveSessionPluginDebugLines(entry); + const statusLines = + entry?.verboseLevel && entry.verboseLevel !== "off" + ? resolveSessionPluginStatusLines(entry) + : []; + const traceLines = entry?.traceLevel === "on" ? resolveSessionPluginTraceLines(entry) : []; + const lines = [...statusLines, ...traceLines]; if (lines.length === 0) { return undefined; } @@ -806,15 +812,19 @@ export async function runReplyAgent(params: { } } const prefixPayloads = [...verboseNotices]; + let trailingPluginStatusPayload: ReplyPayload | undefined; if (verboseEnabled) { const pluginStatusPayload = buildInlinePluginStatusPayload(activeSessionEntry); if (pluginStatusPayload) { - prefixPayloads.push(pluginStatusPayload); + trailingPluginStatusPayload = pluginStatusPayload; } } if (prefixPayloads.length > 0) { finalPayloads = [...prefixPayloads, ...finalPayloads]; } + if (trailingPluginStatusPayload) { + finalPayloads = [...finalPayloads, trailingPluginStatusPayload]; + } if (responseUsageLine) { finalPayloads = appendUsageLine(finalPayloads, responseUsageLine); } diff --git a/src/auto-reply/reply/directive-handling.directive-only.ts b/src/auto-reply/reply/directive-handling.directive-only.ts index f10627dbe90..52180a2270b 100644 --- a/src/auto-reply/reply/directive-handling.directive-only.ts +++ b/src/auto-reply/reply/directive-handling.directive-only.ts @@ -15,6 +15,7 @@ export function isDirectiveOnly(params: { if ( !directives.hasThinkDirective && !directives.hasVerboseDirective && + !directives.hasTraceDirective && !directives.hasFastDirective && !directives.hasReasoningDirective && !directives.hasElevatedDirective && diff --git a/src/auto-reply/reply/directive-handling.impl.ts b/src/auto-reply/reply/directive-handling.impl.ts index 139ea02af72..aff0787f553 100644 --- a/src/auto-reply/reply/directive-handling.impl.ts +++ b/src/auto-reply/reply/directive-handling.impl.ts @@ -5,7 +5,7 @@ import { resolveFastModeState } from "../../agents/fast-mode.js"; import { resolveSandboxRuntimeStatus } from "../../agents/sandbox.js"; import { updateSessionStore } from "../../config/sessions.js"; import { enqueueSystemEvent } from "../../infra/system-events.js"; -import { applyVerboseOverride } from "../../sessions/level-overrides.js"; +import { applyTraceOverride, applyVerboseOverride } from "../../sessions/level-overrides.js"; import { applyModelOverrideToSessionEntry } from "../../sessions/model-overrides.js"; import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js"; import { formatThinkingLevels, formatXHighModelHint, supportsXHighThinking } from "../thinking.js"; @@ -152,6 +152,17 @@ export async function handleDirectiveOnly( text: `Unrecognized verbose level "${directives.rawVerboseLevel}". Valid levels: off, on, full.`, }; } + if (directives.hasTraceDirective && !directives.traceLevel) { + if (!directives.rawTraceLevel) { + const level = (sessionEntry.traceLevel as "on" | "off" | undefined) ?? "off"; + return { + text: withOptions(`Current trace level: ${level}.`, "on, off"), + }; + } + return { + text: `Unrecognized trace level "${directives.rawTraceLevel}". Valid levels: off, on.`, + }; + } if (directives.hasFastDirective && directives.fastMode === undefined) { if ( !directives.rawFastMode || @@ -303,6 +314,7 @@ export async function handleDirectiveOnly( (directives.hasVerboseDirective && Boolean(directives.verboseLevel) && allowInternalVerbosePersistence) || + (directives.hasTraceDirective && Boolean(directives.traceLevel)) || (directives.hasReasoningDirective && Boolean(directives.reasoningLevel)) || (directives.hasElevatedDirective && Boolean(directives.elevatedLevel)) || (directives.hasExecDirective && directives.hasExecOptions && allowInternalExecPersistence) || @@ -332,6 +344,9 @@ export async function handleDirectiveOnly( ) { applyVerboseOverride(sessionEntry, directives.verboseLevel); } + if (directives.hasTraceDirective && directives.traceLevel) { + applyTraceOverride(sessionEntry, directives.traceLevel); + } if (directives.hasReasoningDirective && directives.reasoningLevel) { if (directives.reasoningLevel === "off") { // Persist explicit off so it overrides model-capability defaults. @@ -455,6 +470,13 @@ export async function handleDirectiveOnly( : formatDirectiveAck("Verbose logging enabled."), ); } + if (directives.hasTraceDirective && directives.traceLevel) { + parts.push( + directives.traceLevel === "off" + ? formatDirectiveAck("Plugin trace disabled.") + : formatDirectiveAck("Plugin trace enabled."), + ); + } if ( directives.hasVerboseDirective && directives.verboseLevel && diff --git a/src/auto-reply/reply/directive-handling.parse.ts b/src/auto-reply/reply/directive-handling.parse.ts index 7cc5bd6058d..fe36e6f7324 100644 --- a/src/auto-reply/reply/directive-handling.parse.ts +++ b/src/auto-reply/reply/directive-handling.parse.ts @@ -1,12 +1,19 @@ import type { ExecAsk, ExecSecurity, ExecTarget } from "../../infra/exec-approvals.js"; import { extractModelDirective } from "../model.js"; -import type { ElevatedLevel, ReasoningLevel, ThinkLevel, VerboseLevel } from "./directives.js"; +import type { + ElevatedLevel, + ReasoningLevel, + ThinkLevel, + TraceLevel, + VerboseLevel, +} from "./directives.js"; import { extractElevatedDirective, extractExecDirective, extractFastDirective, extractReasoningDirective, extractStatusDirective, + extractTraceDirective, extractThinkDirective, extractVerboseDirective, } from "./directives.js"; @@ -21,6 +28,9 @@ export type InlineDirectives = { hasVerboseDirective: boolean; verboseLevel?: VerboseLevel; rawVerboseLevel?: string; + hasTraceDirective: boolean; + traceLevel?: TraceLevel; + rawTraceLevel?: string; hasFastDirective: boolean; fastMode?: boolean; rawFastMode?: string; @@ -81,12 +91,18 @@ export function parseInlineDirectives( rawLevel: rawVerboseLevel, hasDirective: hasVerboseDirective, } = extractVerboseDirective(thinkCleaned); + const { + cleaned: traceCleaned, + traceLevel, + rawLevel: rawTraceLevel, + hasDirective: hasTraceDirective, + } = extractTraceDirective(verboseCleaned); const { cleaned: fastCleaned, fastMode, rawLevel: rawFastMode, hasDirective: hasFastDirective, - } = extractFastDirective(verboseCleaned); + } = extractFastDirective(traceCleaned); const { cleaned: reasoningCleaned, reasoningLevel, @@ -158,6 +174,9 @@ export function parseInlineDirectives( hasVerboseDirective, verboseLevel, rawVerboseLevel, + hasTraceDirective, + traceLevel, + rawTraceLevel, hasFastDirective, fastMode, rawFastMode, diff --git a/src/auto-reply/reply/directive-handling.persist.ts b/src/auto-reply/reply/directive-handling.persist.ts index cb68972ee8a..633a5ec8c37 100644 --- a/src/auto-reply/reply/directive-handling.persist.ts +++ b/src/auto-reply/reply/directive-handling.persist.ts @@ -10,7 +10,7 @@ import { updateSessionStore } from "../../config/sessions/store.js"; import type { SessionEntry } from "../../config/sessions/types.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { enqueueSystemEvent } from "../../infra/system-events.js"; -import { applyVerboseOverride } from "../../sessions/level-overrides.js"; +import { applyTraceOverride, applyVerboseOverride } from "../../sessions/level-overrides.js"; import { applyModelOverrideToSessionEntry } from "../../sessions/model-overrides.js"; import { resolveModelSelectionFromDirective } from "./directive-handling.model-selection.js"; import type { InlineDirectives } from "./directive-handling.parse.js"; @@ -105,6 +105,10 @@ export async function persistInlineDirectives(params: { applyVerboseOverride(sessionEntry, directives.verboseLevel); updated = true; } + if (directives.hasTraceDirective && directives.traceLevel) { + applyTraceOverride(sessionEntry, directives.traceLevel); + updated = true; + } if (directives.hasReasoningDirective && directives.reasoningLevel) { if (directives.reasoningLevel === "off") { // Persist explicit off so it overrides model-capability defaults. diff --git a/src/auto-reply/reply/directives.ts b/src/auto-reply/reply/directives.ts index 96a4dbecb2e..85741d08c2f 100644 --- a/src/auto-reply/reply/directives.ts +++ b/src/auto-reply/reply/directives.ts @@ -1,11 +1,12 @@ import { escapeRegExp } from "../../utils.js"; -import type { NoticeLevel, ReasoningLevel } from "../thinking.js"; +import type { NoticeLevel, ReasoningLevel, TraceLevel } from "../thinking.js"; import { type ElevatedLevel, normalizeFastMode, normalizeElevatedLevel, normalizeNoticeLevel, normalizeReasoningLevel, + normalizeTraceLevel, normalizeThinkLevel, normalizeVerboseLevel, type ThinkLevel, @@ -125,6 +126,24 @@ export function extractVerboseDirective(body?: string): { }; } +export function extractTraceDirective(body?: string): { + cleaned: string; + traceLevel?: TraceLevel; + rawLevel?: string; + hasDirective: boolean; +} { + if (!body) { + return { cleaned: "", hasDirective: false }; + } + const extracted = extractLevelDirective(body, ["trace"], normalizeTraceLevel); + return { + cleaned: extracted.cleaned, + traceLevel: extracted.level, + rawLevel: extracted.rawLevel, + hasDirective: extracted.hasDirective, + }; +} + export function extractFastDirective(body?: string): { cleaned: string; fastMode?: boolean; @@ -207,5 +226,5 @@ export function extractStatusDirective(body?: string): { return extractSimpleDirective(body, ["status"]); } -export type { ElevatedLevel, NoticeLevel, ReasoningLevel, ThinkLevel, VerboseLevel }; +export type { ElevatedLevel, NoticeLevel, ReasoningLevel, ThinkLevel, TraceLevel, VerboseLevel }; export { extractExecDirective } from "./exec/directive.js"; diff --git a/src/auto-reply/reply/get-reply-directives-utils.ts b/src/auto-reply/reply/get-reply-directives-utils.ts index d507d71d86b..bf3375b6a7f 100644 --- a/src/auto-reply/reply/get-reply-directives-utils.ts +++ b/src/auto-reply/reply/get-reply-directives-utils.ts @@ -26,6 +26,9 @@ export function clearInlineDirectives(cleaned: string): InlineDirectives { hasVerboseDirective: false, verboseLevel: undefined, rawVerboseLevel: undefined, + hasTraceDirective: false, + traceLevel: undefined, + rawTraceLevel: undefined, hasFastDirective: false, fastMode: undefined, rawFastMode: undefined, diff --git a/src/auto-reply/reply/session.ts b/src/auto-reply/reply/session.ts index 5f30934674d..f9883254372 100644 --- a/src/auto-reply/reply/session.ts +++ b/src/auto-reply/reply/session.ts @@ -284,6 +284,7 @@ export async function initSessionState(params: { let persistedThinking: string | undefined; let persistedVerbose: string | undefined; + let persistedTrace: string | undefined; let persistedReasoning: string | undefined; let persistedTtsAuto: TtsAutoMode | undefined; let persistedModelOverride: string | undefined; @@ -438,6 +439,7 @@ export async function initSessionState(params: { abortedLastRun = entry.abortedLastRun ?? false; persistedThinking = entry.thinkingLevel; persistedVerbose = entry.verboseLevel; + persistedTrace = entry.traceLevel; persistedReasoning = entry.reasoningLevel; persistedTtsAuto = entry.ttsAuto; persistedModelOverride = entry.modelOverride; @@ -457,6 +459,7 @@ export async function initSessionState(params: { if (resetTriggered && entry) { persistedThinking = entry.thinkingLevel; persistedVerbose = entry.verboseLevel; + persistedTrace = entry.traceLevel; persistedReasoning = entry.reasoningLevel; persistedTtsAuto = entry.ttsAuto; persistedModelOverride = entry.modelOverride; @@ -528,6 +531,7 @@ export async function initSessionState(params: { // Persist previously stored thinking/verbose levels when present. thinkingLevel: persistedThinking ?? baseEntry?.thinkingLevel, verboseLevel: persistedVerbose ?? baseEntry?.verboseLevel, + traceLevel: persistedTrace ?? baseEntry?.traceLevel, reasoningLevel: persistedReasoning ?? baseEntry?.reasoningLevel, ttsAuto: persistedTtsAuto ?? baseEntry?.ttsAuto, responseUsage: baseEntry?.responseUsage, diff --git a/src/auto-reply/status.test.ts b/src/auto-reply/status.test.ts index 4a836162b7f..807ce74f94a 100644 --- a/src/auto-reply/status.test.ts +++ b/src/auto-reply/status.test.ts @@ -185,6 +185,48 @@ describe("buildStatusMessage", () => { expect(visible).toContain("Active Memory: ok 842ms recent 34 chars"); }); + it("shows trace lines only when trace is enabled", () => { + const hidden = normalizeTestText( + buildStatusMessage({ + agent: { + model: "anthropic/pi:opus", + }, + sessionEntry: { + sessionId: "abc", + updatedAt: 0, + verboseLevel: "on", + pluginDebugEntries: [ + { pluginId: "active-memory", lines: ["🔎 Active Memory Debug: spicy ramen; tacos"] }, + ], + }, + sessionKey: "agent:main:main", + queue: { mode: "collect", depth: 0 }, + }), + ); + const visible = normalizeTestText( + buildStatusMessage({ + agent: { + model: "anthropic/pi:opus", + }, + sessionEntry: { + sessionId: "abc", + updatedAt: 0, + verboseLevel: "off", + traceLevel: "on", + pluginDebugEntries: [ + { pluginId: "active-memory", lines: ["🔎 Active Memory Debug: spicy ramen; tacos"] }, + ], + }, + sessionKey: "agent:main:main", + queue: { mode: "collect", depth: 0 }, + }), + ); + + expect(hidden).not.toContain("Active Memory Debug: spicy ramen; tacos"); + expect(visible).toContain("Active Memory Debug: spicy ramen; tacos"); + expect(visible).toContain("trace"); + }); + it("shows fast mode when enabled", () => { const text = buildStatusMessage({ agent: { diff --git a/src/auto-reply/status.ts b/src/auto-reply/status.ts index 175075216ff..5ed602df216 100644 --- a/src/auto-reply/status.ts +++ b/src/auto-reply/status.ts @@ -16,7 +16,8 @@ import type { EffectiveToolInventoryResult } from "../agents/tools-effective-inv import { resolveChannelModelOverride } from "../channels/model-overrides.js"; import { resolveMainSessionKey, - resolveSessionPluginDebugLines, + resolveSessionPluginStatusLines, + resolveSessionPluginTraceLines, resolveSessionFilePath, resolveSessionFilePathOptions, type SessionEntry, @@ -678,8 +679,14 @@ export function buildStatusMessage(args: StatusArgs): string { const queueDetails = formatQueueDetails(args.queue); const verboseLabel = verboseLevel === "full" ? "verbose:full" : verboseLevel === "on" ? "verbose" : null; - const pluginDebugLines = verboseLevel !== "off" ? resolveSessionPluginDebugLines(entry) : []; - const pluginStatusLine = pluginDebugLines.length > 0 ? pluginDebugLines.join(" · ") : null; + const traceLevel = entry?.traceLevel === "on" ? "on" : "off"; + const traceLabel = traceLevel === "on" ? "trace" : null; + const pluginStatusLines = verboseLevel !== "off" ? resolveSessionPluginStatusLines(entry) : []; + const pluginTraceLines = traceLevel === "on" ? resolveSessionPluginTraceLines(entry) : []; + const pluginStatusLine = + pluginStatusLines.length > 0 || pluginTraceLines.length > 0 + ? [...pluginStatusLines, ...pluginTraceLines].join(" · ") + : null; const elevatedLabel = elevatedLevel && elevatedLevel !== "off" ? elevatedLevel === "on" @@ -698,6 +705,7 @@ export function buildStatusMessage(args: StatusArgs): string { fastMode ? "Fast: on" : null, textVerbosity ? `Text: ${textVerbosity}` : null, verboseLabel, + traceLabel, reasoningLevel !== "off" ? `Reasoning: ${reasoningLevel}` : null, elevatedLabel, ]; diff --git a/src/auto-reply/thinking.shared.ts b/src/auto-reply/thinking.shared.ts index 85a7e7e9068..2227f43b51e 100644 --- a/src/auto-reply/thinking.shared.ts +++ b/src/auto-reply/thinking.shared.ts @@ -5,6 +5,7 @@ import { export type ThinkLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "adaptive"; export type VerboseLevel = "off" | "on" | "full"; +export type TraceLevel = "off" | "on"; export type NoticeLevel = "off" | "on" | "full"; export type ElevatedLevel = "off" | "on" | "ask" | "full"; export type ElevatedMode = "off" | "ask" | "full"; @@ -135,6 +136,20 @@ export function normalizeVerboseLevel(raw?: string | null): VerboseLevel | undef return normalizeOnOffFullLevel(raw); } +export function normalizeTraceLevel(raw?: string | null): TraceLevel | undefined { + const key = normalizeOptionalLowercaseString(raw); + if (!key) { + return undefined; + } + if (["off", "false", "no", "0"].includes(key)) { + return "off"; + } + if (["on", "true", "yes", "1"].includes(key)) { + return "on"; + } + return undefined; +} + export function normalizeNoticeLevel(raw?: string | null): NoticeLevel | undefined { return normalizeOnOffFullLevel(raw); } diff --git a/src/auto-reply/thinking.ts b/src/auto-reply/thinking.ts index 39acf904e05..ce07047f727 100644 --- a/src/auto-reply/thinking.ts +++ b/src/auto-reply/thinking.ts @@ -12,6 +12,7 @@ export { normalizeFastMode, normalizeNoticeLevel, normalizeReasoningLevel, + normalizeTraceLevel, normalizeThinkLevel, normalizeUsageDisplay, normalizeVerboseLevel, @@ -23,6 +24,7 @@ export type { ElevatedMode, NoticeLevel, ReasoningLevel, + TraceLevel, ThinkLevel, ThinkingCatalogEntry, UsageDisplayLevel, diff --git a/src/commands/sessions-table.ts b/src/commands/sessions-table.ts index a9e47f664a2..d019361b3cb 100644 --- a/src/commands/sessions-table.ts +++ b/src/commands/sessions-table.ts @@ -16,6 +16,7 @@ export type SessionDisplayRow = { abortedLastRun?: boolean; thinkingLevel?: string; verboseLevel?: string; + traceLevel?: string; reasoningLevel?: string; elevatedLevel?: string; responseUsage?: string; @@ -52,6 +53,7 @@ export function toSessionDisplayRows(store: Record): Sessi abortedLastRun: entry?.abortedLastRun, thinkingLevel: entry?.thinkingLevel, verboseLevel: entry?.verboseLevel, + traceLevel: entry?.traceLevel, reasoningLevel: entry?.reasoningLevel, elevatedLevel: entry?.elevatedLevel, responseUsage: entry?.responseUsage, @@ -122,6 +124,7 @@ export function formatSessionFlagsCell( SessionDisplayRow, | "thinkingLevel" | "verboseLevel" + | "traceLevel" | "reasoningLevel" | "elevatedLevel" | "responseUsage" @@ -135,6 +138,7 @@ export function formatSessionFlagsCell( const flags = [ row.thinkingLevel ? `think:${row.thinkingLevel}` : null, row.verboseLevel ? `verbose:${row.verboseLevel}` : null, + row.traceLevel ? `trace:${row.traceLevel}` : null, row.reasoningLevel ? `reasoning:${row.reasoningLevel}` : null, row.elevatedLevel ? `elev:${row.elevatedLevel}` : null, row.responseUsage ? `usage:${row.responseUsage}` : null, diff --git a/src/commands/status.summary.ts b/src/commands/status.summary.ts index 980f78d5cb6..76d5e7c59bb 100644 --- a/src/commands/status.summary.ts +++ b/src/commands/status.summary.ts @@ -219,6 +219,7 @@ export async function getStatusSummary( thinkingLevel: entry?.thinkingLevel, fastMode: entry?.fastMode, verboseLevel: entry?.verboseLevel, + traceLevel: entry?.traceLevel, reasoningLevel: entry?.reasoningLevel, elevatedLevel: entry?.elevatedLevel, systemSent: entry?.systemSent, diff --git a/src/commands/status.types.ts b/src/commands/status.types.ts index 3a8db86b48c..59f118f9fc2 100644 --- a/src/commands/status.types.ts +++ b/src/commands/status.types.ts @@ -12,6 +12,7 @@ export type SessionStatus = { thinkingLevel?: string; fastMode?: boolean; verboseLevel?: string; + traceLevel?: string; reasoningLevel?: string; elevatedLevel?: string; systemSent?: boolean; diff --git a/src/config/sessions/types.ts b/src/config/sessions/types.ts index 0652bc1da99..c7742854f2d 100644 --- a/src/config/sessions/types.ts +++ b/src/config/sessions/types.ts @@ -163,6 +163,7 @@ export type SessionEntry = { thinkingLevel?: string; fastMode?: boolean; verboseLevel?: string; + traceLevel?: string; reasoningLevel?: string; elevatedLevel?: string; ttsAuto?: TtsAutoMode; @@ -257,14 +258,39 @@ export type SessionEntry = { acp?: SessionAcpMeta; }; -export function resolveSessionPluginDebugLines( +function isSessionPluginTraceLine(line: string): boolean { + const trimmed = line.trim(); + return trimmed.startsWith("🔎 ") || /(?:^|\s)(?:Debug|Trace):/.test(trimmed); +} + +export function resolveSessionPluginStatusLines( entry: Pick | undefined, ): string[] { return Array.isArray(entry?.pluginDebugEntries) ? entry.pluginDebugEntries.flatMap((pluginEntry) => Array.isArray(pluginEntry?.lines) ? pluginEntry.lines.filter( - (line): line is string => typeof line === "string" && line.trim().length > 0, + (line): line is string => + typeof line === "string" && + line.trim().length > 0 && + !isSessionPluginTraceLine(line), + ) + : [], + ) + : []; +} + +export function resolveSessionPluginTraceLines( + entry: Pick | undefined, +): string[] { + return Array.isArray(entry?.pluginDebugEntries) + ? entry.pluginDebugEntries.flatMap((pluginEntry) => + Array.isArray(pluginEntry?.lines) + ? pluginEntry.lines.filter( + (line): line is string => + typeof line === "string" && + line.trim().length > 0 && + isSessionPluginTraceLine(line), ) : [], ) diff --git a/src/gateway/protocol/schema/sessions.ts b/src/gateway/protocol/schema/sessions.ts index 7d8945ec1cd..e11194d8d74 100644 --- a/src/gateway/protocol/schema/sessions.ts +++ b/src/gateway/protocol/schema/sessions.ts @@ -135,6 +135,7 @@ export const SessionsPatchParamsSchema = Type.Object( thinkingLevel: Type.Optional(Type.Union([NonEmptyString, Type.Null()])), fastMode: Type.Optional(Type.Union([Type.Boolean(), Type.Null()])), verboseLevel: Type.Optional(Type.Union([NonEmptyString, Type.Null()])), + traceLevel: Type.Optional(Type.Union([NonEmptyString, Type.Null()])), reasoningLevel: Type.Optional(Type.Union([NonEmptyString, Type.Null()])), responseUsage: Type.Optional( Type.Union([ diff --git a/src/gateway/server-chat.ts b/src/gateway/server-chat.ts index a036198cf1f..bf8f2eb8ed8 100644 --- a/src/gateway/server-chat.ts +++ b/src/gateway/server-chat.ts @@ -546,6 +546,7 @@ export function createAgentEventHandler({ thinkingLevel: row?.thinkingLevel, fastMode: row?.fastMode, verboseLevel: row?.verboseLevel, + traceLevel: row?.traceLevel, reasoningLevel: row?.reasoningLevel, elevatedLevel: row?.elevatedLevel, sendPolicy: row?.sendPolicy, diff --git a/src/gateway/server-methods/agent.ts b/src/gateway/server-methods/agent.ts index 757108e89df..3ac792b7963 100644 --- a/src/gateway/server-methods/agent.ts +++ b/src/gateway/server-methods/agent.ts @@ -165,6 +165,7 @@ function emitSessionsChanged( thinkingLevel: sessionRow.thinkingLevel, fastMode: sessionRow.fastMode, verboseLevel: sessionRow.verboseLevel, + traceLevel: sessionRow.traceLevel, reasoningLevel: sessionRow.reasoningLevel, elevatedLevel: sessionRow.elevatedLevel, sendPolicy: sessionRow.sendPolicy, @@ -597,6 +598,7 @@ export const agentHandlers: GatewayRequestHandlers = { thinkingLevel: entry?.thinkingLevel, fastMode: entry?.fastMode, verboseLevel: entry?.verboseLevel, + traceLevel: entry?.traceLevel, reasoningLevel: entry?.reasoningLevel, systemSent: entry?.systemSent, sendPolicy: entry?.sendPolicy, diff --git a/src/gateway/server-methods/sessions.ts b/src/gateway/server-methods/sessions.ts index 5e20c3e7e9a..9919464ad89 100644 --- a/src/gateway/server-methods/sessions.ts +++ b/src/gateway/server-methods/sessions.ts @@ -179,6 +179,7 @@ function emitSessionsChanged( thinkingLevel: sessionRow.thinkingLevel, fastMode: sessionRow.fastMode, verboseLevel: sessionRow.verboseLevel, + traceLevel: sessionRow.traceLevel, reasoningLevel: sessionRow.reasoningLevel, elevatedLevel: sessionRow.elevatedLevel, sendPolicy: sessionRow.sendPolicy, diff --git a/src/gateway/session-reset-service.ts b/src/gateway/session-reset-service.ts index 407dee171b7..bbf98dc2b75 100644 --- a/src/gateway/session-reset-service.ts +++ b/src/gateway/session-reset-service.ts @@ -587,6 +587,7 @@ export async function performGatewaySessionReset(params: { thinkingLevel: currentEntry?.thinkingLevel, fastMode: currentEntry?.fastMode, verboseLevel: currentEntry?.verboseLevel, + traceLevel: currentEntry?.traceLevel, reasoningLevel: currentEntry?.reasoningLevel, elevatedLevel: currentEntry?.elevatedLevel, ttsAuto: currentEntry?.ttsAuto, diff --git a/src/gateway/session-utils.ts b/src/gateway/session-utils.ts index cd385d6341d..6e1e8f1f86b 100644 --- a/src/gateway/session-utils.ts +++ b/src/gateway/session-utils.ts @@ -1276,6 +1276,7 @@ export function buildGatewaySessionRow(params: { thinkingLevel: entry?.thinkingLevel, fastMode: entry?.fastMode, verboseLevel: entry?.verboseLevel, + traceLevel: entry?.traceLevel, reasoningLevel: entry?.reasoningLevel, elevatedLevel: entry?.elevatedLevel, sendPolicy: entry?.sendPolicy, diff --git a/src/gateway/session-utils.types.ts b/src/gateway/session-utils.types.ts index 8e67e890a7f..2133ccbfd30 100644 --- a/src/gateway/session-utils.types.ts +++ b/src/gateway/session-utils.types.ts @@ -41,6 +41,7 @@ export type GatewaySessionRow = { thinkingLevel?: string; fastMode?: boolean; verboseLevel?: string; + traceLevel?: string; reasoningLevel?: string; elevatedLevel?: string; sendPolicy?: "allow" | "deny"; diff --git a/src/gateway/sessions-patch.ts b/src/gateway/sessions-patch.ts index 42fc0b2e7a9..66b821172d1 100644 --- a/src/gateway/sessions-patch.ts +++ b/src/gateway/sessions-patch.ts @@ -26,7 +26,12 @@ import { normalizeAgentId, parseAgentSessionKey, } from "../routing/session-key.js"; -import { applyVerboseOverride, parseVerboseOverride } from "../sessions/level-overrides.js"; +import { + applyTraceOverride, + applyVerboseOverride, + parseTraceOverride, + parseVerboseOverride, +} from "../sessions/level-overrides.js"; import { applyModelOverrideToSessionEntry } from "../sessions/model-overrides.js"; import { normalizeSendPolicy } from "../sessions/send-policy.js"; import { parseSessionLabel } from "../sessions/session-label.js"; @@ -273,6 +278,15 @@ export async function applySessionsPatchToStore(params: { applyVerboseOverride(next, parsed.value); } + if ("traceLevel" in patch) { + const raw = patch.traceLevel; + const parsed = parseTraceOverride(raw); + if (!parsed.ok) { + return invalid(parsed.error); + } + applyTraceOverride(next, parsed.value); + } + if ("reasoningLevel" in patch) { const raw = patch.reasoningLevel; if (raw === null) { diff --git a/src/plugins/commands.test.ts b/src/plugins/commands.test.ts index 95631ad94a7..f64fedbacef 100644 --- a/src/plugins/commands.test.ts +++ b/src/plugins/commands.test.ts @@ -265,6 +265,23 @@ describe("registerPluginCommand", () => { ]); }); + it("matches underscore aliases for hyphenated command names", () => { + registerPluginCommand("demo-plugin", { + name: "active-memory", + description: "Active Memory command", + acceptsArgs: true, + handler: async () => ({ text: "ok" }), + }); + + expect(matchPluginCommand("/active_memory status")).toMatchObject({ + command: expect.objectContaining({ + name: "active-memory", + pluginId: "demo-plugin", + }), + args: "status", + }); + }); + it("supports provider-specific native command aliases", () => { const result = registerVoiceCommandForTest({ nativeNames: { diff --git a/src/plugins/commands.ts b/src/plugins/commands.ts index 945cc53157a..f13e0b0c427 100644 --- a/src/plugins/commands.ts +++ b/src/plugins/commands.ts @@ -69,11 +69,23 @@ export function matchPluginCommand( const args = spaceIndex === -1 ? undefined : trimmed.slice(spaceIndex + 1).trim(); const key = normalizeLowercaseStringOrEmpty(commandName); + const alternateKeys = [key]; + if (key.includes("_")) { + alternateKeys.push(key.replace(/_/g, "-")); + } + if (key.includes("-")) { + alternateKeys.push(key.replace(/-/g, "_")); + } const command = - pluginCommands.get(key) ?? - Array.from(pluginCommands.values()).find((candidate) => - listPluginInvocationNames(candidate).includes(key), - ); + alternateKeys + .map( + (candidateKey) => + pluginCommands.get(candidateKey) ?? + Array.from(pluginCommands.values()).find((candidate) => + listPluginInvocationNames(candidate).includes(candidateKey), + ), + ) + .find(Boolean) ?? null; if (!command) { return null; diff --git a/src/sessions/level-overrides.ts b/src/sessions/level-overrides.ts index 29add6f1955..e17230d293a 100644 --- a/src/sessions/level-overrides.ts +++ b/src/sessions/level-overrides.ts @@ -1,4 +1,9 @@ -import { normalizeVerboseLevel, type VerboseLevel } from "../auto-reply/thinking.js"; +import { + normalizeTraceLevel, + normalizeVerboseLevel, + type TraceLevel, + type VerboseLevel, +} from "../auto-reply/thinking.js"; import type { SessionEntry } from "../config/sessions.js"; export function parseVerboseOverride( @@ -30,3 +35,33 @@ export function applyVerboseOverride(entry: SessionEntry, level: VerboseLevel | } entry.verboseLevel = level; } + +export function parseTraceOverride( + raw: unknown, +): { ok: true; value: TraceLevel | null | undefined } | { ok: false; error: string } { + if (raw === null) { + return { ok: true, value: null }; + } + if (raw === undefined) { + return { ok: true, value: undefined }; + } + if (typeof raw !== "string") { + return { ok: false, error: 'invalid traceLevel (use "on"|"off")' }; + } + const normalized = normalizeTraceLevel(raw); + if (!normalized) { + return { ok: false, error: 'invalid traceLevel (use "on"|"off")' }; + } + return { ok: true, value: normalized }; +} + +export function applyTraceOverride(entry: SessionEntry, level: TraceLevel | null | undefined) { + if (level === undefined) { + return; + } + if (level === null) { + delete entry.traceLevel; + return; + } + entry.traceLevel = level; +} diff --git a/src/tui/commands.ts b/src/tui/commands.ts index 21907921c99..bade73d3429 100644 --- a/src/tui/commands.ts +++ b/src/tui/commands.ts @@ -5,6 +5,7 @@ import type { OpenClawConfig } from "../config/types.js"; import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; const VERBOSE_LEVELS = ["on", "off"]; +const TRACE_LEVELS = ["on", "off"]; const FAST_LEVELS = ["status", "on", "off"]; const REASONING_LEVELS = ["on", "off"]; const ELEVATED_LEVELS = ["on", "off", "ask", "full"]; @@ -55,6 +56,7 @@ export function parseCommand(input: string): ParsedCommand { export function getSlashCommands(options: SlashCommandOptions = {}): SlashCommand[] { const thinkLevels = listThinkingLevelLabels(options.provider, options.model); const verboseCompletions = createLevelCompletion(VERBOSE_LEVELS); + const traceCompletions = createLevelCompletion(TRACE_LEVELS); const fastCompletions = createLevelCompletion(FAST_LEVELS); const reasoningCompletions = createLevelCompletion(REASONING_LEVELS); const usageCompletions = createLevelCompletion(USAGE_FOOTER_LEVELS); @@ -91,6 +93,11 @@ export function getSlashCommands(options: SlashCommandOptions = {}): SlashComman description: "Set verbose on/off", getArgumentCompletions: verboseCompletions, }, + { + name: "trace", + description: "Set trace on/off", + getArgumentCompletions: traceCompletions, + }, { name: "reasoning", description: "Set reasoning on/off", @@ -156,6 +163,7 @@ export function helpText(options: SlashCommandOptions = {}): string { `/think <${thinkLevels}>`, "/fast ", "/verbose ", + "/trace ", "/reasoning ", "/usage ", "/elevated ", diff --git a/src/tui/tui-command-handlers.ts b/src/tui/tui-command-handlers.ts index d22c47a6270..91f471d0df4 100644 --- a/src/tui/tui-command-handlers.ts +++ b/src/tui/tui-command-handlers.ts @@ -353,6 +353,23 @@ export function createCommandHandlers(context: CommandHandlerContext) { chatLog.addSystem(`verbose failed: ${String(err)}`); } break; + case "trace": + if (!args) { + chatLog.addSystem("usage: /trace "); + break; + } + try { + const result = await client.patchSession({ + key: state.currentSessionKey, + traceLevel: args, + }); + chatLog.addSystem(`trace set to ${args}`); + applySessionInfoFromPatch(result); + await loadHistory(); + } catch (err) { + chatLog.addSystem(`trace failed: ${String(err)}`); + } + break; case "fast": if (!args || args === "status") { chatLog.addSystem(`fast mode: ${state.sessionInfo.fastMode ? "on" : "off"}`); diff --git a/src/tui/tui-session-actions.ts b/src/tui/tui-session-actions.ts index 2201393e4a8..4b291ebecb6 100644 --- a/src/tui/tui-session-actions.ts +++ b/src/tui/tui-session-actions.ts @@ -172,6 +172,9 @@ export function createSessionActions(context: SessionActionContext) { if (entry?.verboseLevel !== undefined) { next.verboseLevel = entry.verboseLevel; } + if (entry?.traceLevel !== undefined) { + next.traceLevel = entry.traceLevel; + } if (entry?.reasoningLevel !== undefined) { next.reasoningLevel = entry.reasoningLevel; } @@ -292,11 +295,13 @@ export function createSessionActions(context: SessionActionContext) { thinkingLevel?: string; fastMode?: boolean; verboseLevel?: string; + traceLevel?: string; }; state.currentSessionId = typeof record.sessionId === "string" ? record.sessionId : null; state.sessionInfo.thinkingLevel = record.thinkingLevel ?? state.sessionInfo.thinkingLevel; state.sessionInfo.fastMode = record.fastMode ?? state.sessionInfo.fastMode; state.sessionInfo.verboseLevel = record.verboseLevel ?? state.sessionInfo.verboseLevel; + state.sessionInfo.traceLevel = record.traceLevel ?? state.sessionInfo.traceLevel; const showTools = (state.sessionInfo.verboseLevel ?? "off") !== "off"; chatLog.clearAll(); btw.clear(); diff --git a/src/tui/tui-types.ts b/src/tui/tui-types.ts index 5523d8b1ec8..a9b3fcc094d 100644 --- a/src/tui/tui-types.ts +++ b/src/tui/tui-types.ts @@ -41,6 +41,7 @@ export type SessionInfo = { thinkingLevel?: string; fastMode?: boolean; verboseLevel?: string; + traceLevel?: string; reasoningLevel?: string; model?: string; modelProvider?: string;