From ffbcd83d1eca1a4c40973ba1b02c811057063873 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 12 Jan 2026 18:37:44 +0000 Subject: [PATCH] chore: log elevated and reasoning toggles --- CHANGELOG.md | 1 + src/auto-reply/reply.directive.test.ts | 61 +++++++++++++++ src/auto-reply/reply/directive-handling.ts | 88 ++++++++++++++++++++++ 3 files changed, 150 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e93de773e..93cd922a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ - Auto-reply: restore 300-char heartbeat ack limit and keep >300 char replies instead of dropping them; adjust long heartbeat test content accordingly. - Gateway: `agents.list` now honors explicit `agents.list` config without pulling stray agents from disk; GitHub Copilot CLI auth path uses the updated provider build. - Google: apply patched pi-ai `google-gemini-cli` function call handling (strips ids) after upgrading to pi-ai 0.43.0. +- Auto-reply: elevated/reasoning toggles now enqueue system events so the model sees the mode change immediately. ## 2026.1.11 diff --git a/src/auto-reply/reply.directive.test.ts b/src/auto-reply/reply.directive.test.ts index 6000e5d56..d8a7055f5 100644 --- a/src/auto-reply/reply.directive.test.ts +++ b/src/auto-reply/reply.directive.test.ts @@ -1913,6 +1913,67 @@ describe("directive behavior", () => { }); }); + it("queues a system event when toggling elevated", async () => { + await withTempHome(async (home) => { + drainSystemEvents(MAIN_SESSION_KEY); + const storePath = path.join(home, "sessions.json"); + + await getReplyFromConfig( + { + Body: "/elevated on", + From: "+1222", + To: "+1222", + Provider: "whatsapp", + }, + {}, + { + agents: { + defaults: { + model: { primary: "openai/gpt-4.1-mini" }, + workspace: path.join(home, "clawd"), + }, + }, + tools: { elevated: { allowFrom: { whatsapp: ["*"] } } }, + whatsapp: { allowFrom: ["*"] }, + session: { store: storePath }, + }, + ); + + const events = drainSystemEvents(MAIN_SESSION_KEY); + expect(events.some((e) => e.includes("Elevated ON"))).toBe(true); + }); + }); + + it("queues a system event when toggling reasoning", async () => { + await withTempHome(async (home) => { + drainSystemEvents(MAIN_SESSION_KEY); + const storePath = path.join(home, "sessions.json"); + + await getReplyFromConfig( + { + Body: "/reasoning stream", + From: "+1222", + To: "+1222", + Provider: "whatsapp", + }, + {}, + { + agents: { + defaults: { + model: { primary: "openai/gpt-4.1-mini" }, + workspace: path.join(home, "clawd"), + }, + }, + whatsapp: { allowFrom: ["*"] }, + session: { store: storePath }, + }, + ); + + const events = drainSystemEvents(MAIN_SESSION_KEY); + expect(events.some((e) => e.includes("Reasoning STREAM"))).toBe(true); + }); + }); + it("ignores inline /model and uses the default model", async () => { await withTempHome(async (home) => { const storePath = path.join(home, "sessions.json"); diff --git a/src/auto-reply/reply/directive-handling.ts b/src/auto-reply/reply/directive-handling.ts index 715715157..f769e10c5 100644 --- a/src/auto-reply/reply/directive-handling.ts +++ b/src/auto-reply/reply/directive-handling.ts @@ -68,6 +68,17 @@ const withOptions = (line: string, options: string) => const formatElevatedRuntimeHint = () => `${SYSTEM_MARK} Runtime is direct; sandboxing does not apply.`; +const formatElevatedEvent = (level: ElevatedLevel) => + level === "on" + ? "Elevated ON — exec runs on host; set elevated:false to stay sandboxed." + : "Elevated OFF — exec stays in sandbox."; + +const formatReasoningEvent = (level: ReasoningLevel) => { + if (level === "stream") return "Reasoning STREAM — emit live ."; + if (level === "on") return "Reasoning ON — include ."; + return "Reasoning OFF — hide ."; +}; + function formatElevatedUnavailableText(params: { runtimeSandboxed: boolean; failures?: Array<{ gate: string; key: string }>; @@ -1070,6 +1081,22 @@ export async function handleDirectiveOnly(params: { } if (sessionEntry && sessionStore && sessionKey) { + const prevElevatedLevel = + currentElevatedLevel ?? + (sessionEntry.elevatedLevel as ElevatedLevel | undefined) ?? + (elevatedAllowed ? ("on" as ElevatedLevel) : ("off" as ElevatedLevel)); + const prevReasoningLevel = + currentReasoningLevel ?? + (sessionEntry.reasoningLevel as ReasoningLevel | undefined) ?? + "off"; + let elevatedChanged = + directives.hasElevatedDirective && + directives.elevatedLevel !== undefined && + elevatedEnabled && + elevatedAllowed; + let reasoningChanged = + directives.hasReasoningDirective && + directives.reasoningLevel !== undefined; if (directives.hasThinkDirective && directives.thinkLevel) { if (directives.thinkLevel === "off") delete sessionEntry.thinkingLevel; else sessionEntry.thinkingLevel = directives.thinkLevel; @@ -1081,11 +1108,18 @@ export async function handleDirectiveOnly(params: { if (directives.reasoningLevel === "off") delete sessionEntry.reasoningLevel; else sessionEntry.reasoningLevel = directives.reasoningLevel; + reasoningChanged = + directives.reasoningLevel !== prevReasoningLevel && + directives.reasoningLevel !== undefined; } if (directives.hasElevatedDirective && directives.elevatedLevel) { // Unlike other toggles, elevated defaults can be "on". // Persist "off" explicitly so `/elevated off` actually overrides defaults. sessionEntry.elevatedLevel = directives.elevatedLevel; + elevatedChanged = + elevatedChanged || + (directives.elevatedLevel !== prevElevatedLevel && + directives.elevatedLevel !== undefined); } if (modelSelection) { if (modelSelection.isDefault) { @@ -1123,6 +1157,22 @@ export async function handleDirectiveOnly(params: { if (storePath) { await saveSessionStore(storePath, sessionStore); } + if (elevatedChanged) { + const nextElevated = + (sessionEntry.elevatedLevel ?? "off") as ElevatedLevel; + enqueueSystemEvent(formatElevatedEvent(nextElevated), { + sessionKey, + contextKey: "mode:elevated", + }); + } + if (reasoningChanged) { + const nextReasoning = + (sessionEntry.reasoningLevel ?? "off") as ReasoningLevel; + enqueueSystemEvent(formatReasoningEvent(nextReasoning), { + sessionKey, + contextKey: "mode:reasoning", + }); + } } const parts: string[] = []; @@ -1240,6 +1290,20 @@ export async function persistInlineDirectives(params: { const agentDir = resolveAgentDir(cfg, activeAgentId); if (sessionEntry && sessionStore && sessionKey) { + const prevElevatedLevel = + (sessionEntry.elevatedLevel as ElevatedLevel | undefined) ?? + (agentCfg?.elevatedDefault as ElevatedLevel | undefined) ?? + (elevatedAllowed ? ("on" as ElevatedLevel) : ("off" as ElevatedLevel)); + const prevReasoningLevel = + (sessionEntry.reasoningLevel as ReasoningLevel | undefined) ?? "off"; + let elevatedChanged = + directives.hasElevatedDirective && + directives.elevatedLevel !== undefined && + elevatedEnabled && + elevatedAllowed; + let reasoningChanged = + directives.hasReasoningDirective && + directives.reasoningLevel !== undefined; let updated = false; if (directives.hasThinkDirective && directives.thinkLevel) { if (directives.thinkLevel === "off") { @@ -1259,6 +1323,10 @@ export async function persistInlineDirectives(params: { } else { sessionEntry.reasoningLevel = directives.reasoningLevel; } + reasoningChanged = + reasoningChanged || + (directives.reasoningLevel !== prevReasoningLevel && + directives.reasoningLevel !== undefined); updated = true; } if ( @@ -1269,6 +1337,10 @@ export async function persistInlineDirectives(params: { ) { // Persist "off" explicitly so inline `/elevated off` overrides defaults. sessionEntry.elevatedLevel = directives.elevatedLevel; + elevatedChanged = + elevatedChanged || + (directives.elevatedLevel !== prevElevatedLevel && + directives.elevatedLevel !== undefined); updated = true; } const modelDirective = @@ -1341,6 +1413,22 @@ export async function persistInlineDirectives(params: { if (storePath) { await saveSessionStore(storePath, sessionStore); } + if (elevatedChanged) { + const nextElevated = + (sessionEntry.elevatedLevel ?? "off") as ElevatedLevel; + enqueueSystemEvent(formatElevatedEvent(nextElevated), { + sessionKey, + contextKey: "mode:elevated", + }); + } + if (reasoningChanged) { + const nextReasoning = + (sessionEntry.reasoningLevel ?? "off") as ReasoningLevel; + enqueueSystemEvent(formatReasoningEvent(nextReasoning), { + sessionKey, + contextKey: "mode:reasoning", + }); + } } }