diff --git a/CHANGELOG.md b/CHANGELOG.md index 73355315d..1e2517fb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ Docs: https://docs.clawd.bot -## 2026.1.22 +## 2026.1.22 (unreleased) ### Fixes - BlueBubbles: stop typing indicator on idle/no-reply. (#1439) Thanks @Nicell. diff --git a/src/auto-reply/reply/directive-handling.fast-lane.ts b/src/auto-reply/reply/directive-handling.fast-lane.ts index 54f3f0375..74a690f89 100644 --- a/src/auto-reply/reply/directive-handling.fast-lane.ts +++ b/src/auto-reply/reply/directive-handling.fast-lane.ts @@ -15,8 +15,8 @@ export async function applyInlineDirectivesFastLane(params: { cfg: ClawdbotConfig; agentId?: string; isGroup: boolean; - sessionEntry?: SessionEntry; - sessionStore?: Record; + sessionEntry: SessionEntry; + sessionStore: Record; sessionKey: string; storePath?: string; elevatedEnabled: boolean; diff --git a/src/auto-reply/reply/directive-handling.impl.model-persist.test.ts b/src/auto-reply/reply/directive-handling.impl.model-persist.test.ts index b8a70b4a8..847ff7030 100644 --- a/src/auto-reply/reply/directive-handling.impl.model-persist.test.ts +++ b/src/auto-reply/reply/directive-handling.impl.model-persist.test.ts @@ -77,100 +77,6 @@ describe("handleDirectiveOnly model persist behavior (fixes #1435)", () => { expect(result?.text).not.toContain("failed"); }); - it("shows error message when sessionEntry is missing", async () => { - const directives = parseInlineDirectives("/model openai/gpt-4o"); - const sessionStore = {}; - - const result = await handleDirectiveOnly({ - cfg: baseConfig(), - directives, - sessionEntry: undefined, // Missing! - sessionStore, - sessionKey: "agent:main:dm:1", - storePath: "/tmp/sessions.json", - elevatedEnabled: false, - elevatedAllowed: false, - defaultProvider: "anthropic", - defaultModel: "claude-opus-4-5", - aliasIndex: baseAliasIndex(), - allowedModelKeys, - allowedModelCatalog, - resetModelOverride: false, - provider: "anthropic", - model: "claude-opus-4-5", - initialModelLabel: "anthropic/claude-opus-4-5", - formatModelSwitchEvent: (label) => `Switched to ${label}`, - }); - - expect(result?.text).toContain("failed"); - expect(result?.text).toContain("session state unavailable"); - }); - - it("shows error message when sessionStore is missing", async () => { - const directives = parseInlineDirectives("/model openai/gpt-4o"); - const sessionEntry: SessionEntry = { - sessionId: "s1", - updatedAt: Date.now(), - }; - - const result = await handleDirectiveOnly({ - cfg: baseConfig(), - directives, - sessionEntry, - sessionStore: undefined, // Missing! - sessionKey: "agent:main:dm:1", - storePath: "/tmp/sessions.json", - elevatedEnabled: false, - elevatedAllowed: false, - defaultProvider: "anthropic", - defaultModel: "claude-opus-4-5", - aliasIndex: baseAliasIndex(), - allowedModelKeys, - allowedModelCatalog, - resetModelOverride: false, - provider: "anthropic", - model: "claude-opus-4-5", - initialModelLabel: "anthropic/claude-opus-4-5", - formatModelSwitchEvent: (label) => `Switched to ${label}`, - }); - - expect(result?.text).toContain("failed"); - expect(result?.text).toContain("session state unavailable"); - }); - - it("shows error message when sessionKey is missing", async () => { - const directives = parseInlineDirectives("/model openai/gpt-4o"); - const sessionEntry: SessionEntry = { - sessionId: "s1", - updatedAt: Date.now(), - }; - const sessionStore = { "agent:main:dm:1": sessionEntry }; - - const result = await handleDirectiveOnly({ - cfg: baseConfig(), - directives, - sessionEntry, - sessionStore, - sessionKey: undefined, // Missing! - storePath: "/tmp/sessions.json", - elevatedEnabled: false, - elevatedAllowed: false, - defaultProvider: "anthropic", - defaultModel: "claude-opus-4-5", - aliasIndex: baseAliasIndex(), - allowedModelKeys, - allowedModelCatalog, - resetModelOverride: false, - provider: "anthropic", - model: "claude-opus-4-5", - initialModelLabel: "anthropic/claude-opus-4-5", - formatModelSwitchEvent: (label) => `Switched to ${label}`, - }); - - expect(result?.text).toContain("failed"); - expect(result?.text).toContain("session state unavailable"); - }); - it("shows no model message when no /model directive", async () => { const directives = parseInlineDirectives("hello world"); const sessionEntry: SessionEntry = { diff --git a/src/auto-reply/reply/directive-handling.impl.ts b/src/auto-reply/reply/directive-handling.impl.ts index a753085dc..0765d20dc 100644 --- a/src/auto-reply/reply/directive-handling.impl.ts +++ b/src/auto-reply/reply/directive-handling.impl.ts @@ -62,8 +62,8 @@ function resolveExecDefaults(params: { export async function handleDirectiveOnly(params: { cfg: ClawdbotConfig; directives: InlineDirectives; - sessionEntry?: SessionEntry; - sessionStore?: Record; + sessionEntry: SessionEntry; + sessionStore: Record; sessionKey: string; storePath?: string; elevatedEnabled: boolean; @@ -288,115 +288,111 @@ export async function handleDirectiveOnly(params: { nextThinkLevel === "xhigh" && !supportsXHighThinking(resolvedProvider, resolvedModel); - let didPersistModel = false; - 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; + 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; + } + if (shouldDowngradeXHigh) { + sessionEntry.thinkingLevel = "high"; + } + if (directives.hasVerboseDirective && directives.verboseLevel) { + applyVerboseOverride(sessionEntry, directives.verboseLevel); + } + if (directives.hasReasoningDirective && directives.reasoningLevel) { + 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 (directives.hasExecDirective && directives.hasExecOptions) { + if (directives.execHost) { + sessionEntry.execHost = directives.execHost; } - if (shouldDowngradeXHigh) { - sessionEntry.thinkingLevel = "high"; + if (directives.execSecurity) { + sessionEntry.execSecurity = directives.execSecurity; } - if (directives.hasVerboseDirective && directives.verboseLevel) { - applyVerboseOverride(sessionEntry, directives.verboseLevel); + if (directives.execAsk) { + sessionEntry.execAsk = directives.execAsk; } - if (directives.hasReasoningDirective && directives.reasoningLevel) { - if (directives.reasoningLevel === "off") delete sessionEntry.reasoningLevel; - else sessionEntry.reasoningLevel = directives.reasoningLevel; - reasoningChanged = - directives.reasoningLevel !== prevReasoningLevel && directives.reasoningLevel !== undefined; + if (directives.execNode) { + sessionEntry.execNode = directives.execNode; } - 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) { + applyModelOverrideToSessionEntry({ + entry: sessionEntry, + selection: modelSelection, + profileOverride, + }); + } + if (directives.hasQueueDirective && directives.queueReset) { + delete sessionEntry.queueMode; + delete sessionEntry.queueDebounceMs; + delete sessionEntry.queueCap; + delete sessionEntry.queueDrop; + } else if (directives.hasQueueDirective) { + if (directives.queueMode) sessionEntry.queueMode = directives.queueMode; + if (typeof directives.debounceMs === "number") { + sessionEntry.queueDebounceMs = directives.debounceMs; } - if (directives.hasExecDirective && directives.hasExecOptions) { - if (directives.execHost) { - sessionEntry.execHost = directives.execHost; - } - if (directives.execSecurity) { - sessionEntry.execSecurity = directives.execSecurity; - } - if (directives.execAsk) { - sessionEntry.execAsk = directives.execAsk; - } - if (directives.execNode) { - sessionEntry.execNode = directives.execNode; - } + if (typeof directives.cap === "number") { + sessionEntry.queueCap = directives.cap; } - if (modelSelection) { - applyModelOverrideToSessionEntry({ - entry: sessionEntry, - selection: modelSelection, - profileOverride, - }); - didPersistModel = true; + if (directives.dropPolicy) { + sessionEntry.queueDrop = directives.dropPolicy; } - if (directives.hasQueueDirective && directives.queueReset) { - delete sessionEntry.queueMode; - delete sessionEntry.queueDebounceMs; - delete sessionEntry.queueCap; - delete sessionEntry.queueDrop; - } else if (directives.hasQueueDirective) { - if (directives.queueMode) sessionEntry.queueMode = directives.queueMode; - if (typeof directives.debounceMs === "number") { - sessionEntry.queueDebounceMs = directives.debounceMs; - } - if (typeof directives.cap === "number") { - sessionEntry.queueCap = directives.cap; - } - if (directives.dropPolicy) { - sessionEntry.queueDrop = directives.dropPolicy; - } - } - sessionEntry.updatedAt = Date.now(); - sessionStore[sessionKey] = sessionEntry; - if (storePath) { - await updateSessionStore(storePath, (store) => { - store[sessionKey] = sessionEntry; - }); - } - if (modelSelection) { - const nextLabel = `${modelSelection.provider}/${modelSelection.model}`; - if (nextLabel !== initialModelLabel) { - enqueueSystemEvent(formatModelSwitchEvent(nextLabel, modelSelection.alias), { - sessionKey, - contextKey: `model:${nextLabel}`, - }); - } - } - if (elevatedChanged) { - const nextElevated = (sessionEntry.elevatedLevel ?? "off") as ElevatedLevel; - enqueueSystemEvent(formatElevatedEvent(nextElevated), { + } + sessionEntry.updatedAt = Date.now(); + sessionStore[sessionKey] = sessionEntry; + if (storePath) { + await updateSessionStore(storePath, (store) => { + store[sessionKey] = sessionEntry; + }); + } + if (modelSelection) { + const nextLabel = `${modelSelection.provider}/${modelSelection.model}`; + if (nextLabel !== initialModelLabel) { + enqueueSystemEvent(formatModelSwitchEvent(nextLabel, modelSelection.alias), { sessionKey, - contextKey: "mode:elevated", - }); - } - if (reasoningChanged) { - const nextReasoning = (sessionEntry.reasoningLevel ?? "off") as ReasoningLevel; - enqueueSystemEvent(formatReasoningEvent(nextReasoning), { - sessionKey, - contextKey: "mode:reasoning", + contextKey: `model:${nextLabel}`, }); } } + 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[] = []; if (directives.hasThinkDirective && directives.thinkLevel) { @@ -449,7 +445,7 @@ export async function handleDirectiveOnly(params: { `Thinking level set to high (xhigh not supported for ${resolvedProvider}/${resolvedModel}).`, ); } - if (modelSelection && didPersistModel) { + if (modelSelection) { const label = `${modelSelection.provider}/${modelSelection.model}`; const labelWithAlias = modelSelection.alias ? `${modelSelection.alias} (${label})` : label; parts.push( @@ -460,10 +456,6 @@ export async function handleDirectiveOnly(params: { if (profileOverride) { parts.push(`Auth profile set to ${profileOverride}.`); } - } else if (modelSelection && !didPersistModel) { - parts.push( - `Model switch to ${modelSelection.provider}/${modelSelection.model} failed (session state unavailable).`, - ); } if (directives.hasQueueDirective && directives.queueMode) { parts.push(formatDirectiveAck(`Queue mode set to ${directives.queueMode}.`)); diff --git a/src/auto-reply/reply/get-reply-directives-apply.ts b/src/auto-reply/reply/get-reply-directives-apply.ts index a03ade77d..8f3ac34f0 100644 --- a/src/auto-reply/reply/get-reply-directives-apply.ts +++ b/src/auto-reply/reply/get-reply-directives-apply.ts @@ -39,8 +39,8 @@ export async function applyInlineDirectiveOverrides(params: { agentId: string; agentDir: string; agentCfg: AgentDefaults; - sessionEntry?: SessionEntry; - sessionStore?: Record; + sessionEntry: SessionEntry; + sessionStore: Record; sessionKey: string; storePath?: string; sessionScope: Parameters[0]["sessionScope"]; diff --git a/src/auto-reply/reply/get-reply-directives.ts b/src/auto-reply/reply/get-reply-directives.ts index 04782c9eb..232d44e21 100644 --- a/src/auto-reply/reply/get-reply-directives.ts +++ b/src/auto-reply/reply/get-reply-directives.ts @@ -89,8 +89,8 @@ export async function resolveReplyDirectives(params: { workspaceDir: string; agentCfg: AgentDefaults; sessionCtx: TemplateContext; - sessionEntry?: SessionEntry; - sessionStore?: Record; + sessionEntry: SessionEntry; + sessionStore: Record; sessionKey: string; storePath?: string; sessionScope: Parameters[0]["sessionScope"];