diff --git a/CHANGELOG.md b/CHANGELOG.md index 3556c55ad58..53ddc9a19c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ Docs: https://docs.openclaw.ai - Agents/subagents: emit the subagent registry lazy-runtime stub on the stable dist path that both source and bundled runtime imports resolve, so the follow-up dist fix no longer still fails with `ERR_MODULE_NOT_FOUND` at runtime. (#66420) Thanks @obviyus. - Browser: keep loopback CDP readiness checks reachable under strict SSRF defaults so OpenClaw can reconnect to locally started managed Chrome. (#66354) Thanks @hxy91819. - Agents/context engine: compact engine-owned sessions from the first tool-loop delta and preserve ingest fallback when `afterTurn` is absent, so long-running tool loops can stay bounded without dropping engine state. (#63555) Thanks @Bikkies. +- Discord/native commands: return the real status card for native `/status` interactions instead of falling through to the synthetic `✅ Done.` ack when the generic dispatcher produces no visible reply. (#54629) Thanks @tkozzer and @vincentkoc. ## 2026.4.14-beta.1 diff --git a/docs/.generated/plugin-sdk-api-baseline.sha256 b/docs/.generated/plugin-sdk-api-baseline.sha256 index 23047c9a793..365a8adcd0a 100644 --- a/docs/.generated/plugin-sdk-api-baseline.sha256 +++ b/docs/.generated/plugin-sdk-api-baseline.sha256 @@ -1,2 +1,2 @@ -7003e0d0ba1cddb7eb388204825ac892206209a4a9c795e76c4e34b5fc7b50f0 plugin-sdk-api-baseline.json -14e39520459abc7db7993a700a4f07adfa0855d9233d123c4725477b91f1cb13 plugin-sdk-api-baseline.jsonl +7b121e2b694f80433fa91ce9037527ca58be546a7f18798470a4ade66593e5e1 plugin-sdk-api-baseline.json +7b802cc04f0eac0b498b50711e39a7afe93bbb6b682a2013d2c303583fb73f40 plugin-sdk-api-baseline.jsonl diff --git a/extensions/discord/src/monitor/native-command.plugin-dispatch.test.ts b/extensions/discord/src/monitor/native-command.plugin-dispatch.test.ts index 84887695426..7cd6e435924 100644 --- a/extensions/discord/src/monitor/native-command.plugin-dispatch.test.ts +++ b/extensions/discord/src/monitor/native-command.plugin-dispatch.test.ts @@ -19,6 +19,7 @@ const runtimeModuleMocks = vi.hoisted(() => ({ matchPluginCommand: vi.fn(), executePluginCommand: vi.fn(), dispatchReplyWithDispatcher: vi.fn(), + resolveDirectStatusReplyForSession: vi.fn(), })); vi.mock("openclaw/plugin-sdk/plugin-runtime", async () => { @@ -43,6 +44,11 @@ vi.mock("openclaw/plugin-sdk/reply-runtime", async () => { }; }); +vi.mock("openclaw/plugin-sdk/command-status-runtime", () => ({ + resolveDirectStatusReplyForSession: (...args: unknown[]) => + runtimeModuleMocks.resolveDirectStatusReplyForSession(...args), +})); + function createInteraction(params?: { channelType?: ChannelType; channelId?: string; @@ -306,35 +312,24 @@ function createDispatchSpy() { } as never); } -function expectBoundSessionDispatch( - dispatchSpy: ReturnType, - expectedPattern: RegExp, -) { - expect(dispatchSpy).toHaveBeenCalledTimes(1); - const dispatchCall = dispatchSpy.mock.calls[0]?.[0] as { - ctx?: { SessionKey?: string; CommandTargetSessionKey?: string }; - }; - if (!dispatchCall.ctx?.SessionKey || !dispatchCall.ctx.CommandTargetSessionKey) { - throw new Error("native command dispatch did not include bound session context"); - } - expect(dispatchCall.ctx.SessionKey).toMatch(expectedPattern); - expect(dispatchCall.ctx.CommandTargetSessionKey).toMatch(expectedPattern); -} - -async function expectBoundStatusCommandDispatch(params: { +async function expectBoundStatusCommandDirectReply(params: { cfg: OpenClawConfig; interaction: MockCommandInteraction; expectedPattern: RegExp; }) { runtimeModuleMocks.matchPluginCommand.mockReturnValue(null); - const dispatchSpy = createDispatchSpy(); + const dispatchSpy = runtimeModuleMocks.dispatchReplyWithDispatcher; + const statusSpy = runtimeModuleMocks.resolveDirectStatusReplyForSession; const command = await createStatusCommand(params.cfg); await (command as { run: (interaction: unknown) => Promise }).run( params.interaction as unknown, ); - expectBoundSessionDispatch(dispatchSpy, params.expectedPattern); + expect(dispatchSpy).not.toHaveBeenCalled(); + expect(statusSpy).toHaveBeenCalledTimes(1); + const statusCall = statusSpy.mock.calls[0]?.[0] as { sessionKey?: string }; + expect(statusCall.sessionKey).toMatch(params.expectedPattern); } describe("Discord native plugin command dispatch", () => { @@ -366,6 +361,10 @@ describe("Discord native plugin command dispatch", () => { tool: 0, }, } as never); + runtimeModuleMocks.resolveDirectStatusReplyForSession.mockReset(); + runtimeModuleMocks.resolveDirectStatusReplyForSession.mockResolvedValue({ + text: "status reply", + }); discordNativeCommandTesting.setMatchPluginCommand( runtimeModuleMocks.matchPluginCommand as typeof import("openclaw/plugin-sdk/plugin-runtime").matchPluginCommand, ); @@ -632,7 +631,7 @@ describe("Discord native plugin command dispatch", () => { }), ); - await expectBoundStatusCommandDispatch({ + await expectBoundStatusCommandDirectReply({ cfg, interaction, expectedPattern: /^agent:codex:acp:binding:discord:default:/, @@ -683,7 +682,8 @@ describe("Discord native plugin command dispatch", () => { }), ); runtimeModuleMocks.matchPluginCommand.mockReturnValue(null); - const dispatchSpy = createDispatchSpy(); + const dispatchSpy = runtimeModuleMocks.dispatchReplyWithDispatcher; + const statusSpy = runtimeModuleMocks.resolveDirectStatusReplyForSession; const command = await createStatusCommand(cfg); discordNativeCommandTesting.setResolveDiscordNativeInteractionRouteState(async () => ({ route: { @@ -712,14 +712,10 @@ describe("Discord native plugin command dispatch", () => { await (command as { run: (interaction: unknown) => Promise }).run(interaction as unknown); - expect(dispatchSpy).toHaveBeenCalledTimes(1); - const dispatchCall = dispatchSpy.mock.calls[0]?.[0] as { - ctx?: { SessionKey?: string; CommandTargetSessionKey?: string }; - }; - expect(dispatchCall.ctx?.SessionKey).toBe("agent:qwen:discord:slash:owner"); - expect(dispatchCall.ctx?.CommandTargetSessionKey).toBe( - "agent:qwen:discord:channel:1478836151241412759", - ); + expect(dispatchSpy).not.toHaveBeenCalled(); + expect(statusSpy).toHaveBeenCalledTimes(1); + const statusCall = statusSpy.mock.calls[0]?.[0] as { sessionKey?: string }; + expect(statusCall.sessionKey).toBe("agent:qwen:discord:channel:1478836151241412759"); }); it("routes Discord DM native slash commands through configured ACP bindings", async () => { @@ -735,7 +731,7 @@ describe("Discord native plugin command dispatch", () => { }), ); - await expectBoundStatusCommandDispatch({ + await expectBoundStatusCommandDirectReply({ cfg, interaction, expectedPattern: /^agent:codex:acp:binding:discord:default:/, diff --git a/extensions/discord/src/monitor/native-command.status-direct.test.ts b/extensions/discord/src/monitor/native-command.status-direct.test.ts new file mode 100644 index 00000000000..66c095e2493 --- /dev/null +++ b/extensions/discord/src/monitor/native-command.status-direct.test.ts @@ -0,0 +1,200 @@ +import { ChannelType } from "discord-api-types/v10"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { + createMockCommandInteraction, + type MockCommandInteraction, +} from "./native-command.test-helpers.js"; +import { createNoopThreadBindingManager } from "./thread-bindings.js"; + +const runtimeModuleMocks = vi.hoisted(() => ({ + dispatchReplyWithDispatcher: vi.fn(), + resolveDirectStatusReplyForSession: vi.fn(), +})); + +vi.mock("openclaw/plugin-sdk/reply-dispatch-runtime", async () => { + const actual = await vi.importActual( + "openclaw/plugin-sdk/reply-dispatch-runtime", + ); + return { + ...actual, + dispatchReplyWithDispatcher: (...args: unknown[]) => + runtimeModuleMocks.dispatchReplyWithDispatcher(...args), + }; +}); + +vi.mock("openclaw/plugin-sdk/command-status-runtime", () => ({ + resolveDirectStatusReplyForSession: (...args: unknown[]) => + runtimeModuleMocks.resolveDirectStatusReplyForSession(...args), +})); + +let createDiscordNativeCommand: typeof import("./native-command.js").createDiscordNativeCommand; +let discordNativeCommandTesting: typeof import("./native-command.js").__testing; + +function createInteraction(params?: { + channelType?: ChannelType; + channelId?: string; + threadParentId?: string | null; + guildId?: string | null; + guildName?: string; +}): MockCommandInteraction { + return createMockCommandInteraction({ + userId: "owner", + username: "tester", + globalName: "Tester", + channelType: params?.channelType ?? ChannelType.DM, + channelId: params?.channelId ?? "dm-1", + threadParentId: params?.threadParentId, + guildId: params?.guildId ?? null, + guildName: params?.guildName, + interactionId: "interaction-1", + }); +} + +function createConfig(params?: { requireMention?: boolean }): OpenClawConfig { + return { + commands: { + useAccessGroups: false, + }, + channels: { + discord: { + dm: { enabled: true, policy: "open" }, + guilds: { + guild1: { + requireMention: true, + channels: { + chan1: { + allow: true, + requireMention: params?.requireMention ?? true, + }, + }, + }, + }, + }, + }, + } as OpenClawConfig; +} + +async function createStatusCommand(cfg: OpenClawConfig) { + return createDiscordNativeCommand({ + command: { + name: "status", + description: "Status", + acceptsArgs: false, + }, + cfg, + discordConfig: cfg.channels?.discord ?? {}, + accountId: "default", + sessionPrefix: "discord:slash", + ephemeralDefault: true, + threadBindings: createNoopThreadBindingManager("default"), + }); +} + +function setDefaultRouteState() { + discordNativeCommandTesting.setResolveDiscordNativeInteractionRouteState(async (params) => ({ + route: { + agentId: "main", + channel: "discord", + accountId: params.accountId ?? "default", + sessionKey: "agent:main:main", + mainSessionKey: "agent:main:main", + lastRoutePolicy: "session", + matchedBy: "default", + }, + effectiveRoute: { + agentId: "main", + channel: "discord", + accountId: params.accountId ?? "default", + sessionKey: "agent:main:main", + mainSessionKey: "agent:main:main", + lastRoutePolicy: "session", + matchedBy: "default", + }, + boundSessionKey: undefined, + configuredRoute: null, + configuredBinding: null, + bindingReadiness: null, + })); +} + +function firstStatusCall(): { + cfg: OpenClawConfig; + sessionKey: string; + channel: string; + isGroup: boolean; + defaultGroupActivation: () => "always" | "mention"; +} { + const call = runtimeModuleMocks.resolveDirectStatusReplyForSession.mock.calls[0]?.[0]; + if (!call) { + throw new Error("expected resolveDirectStatusReplyForSession to be called"); + } + return call as { + cfg: OpenClawConfig; + sessionKey: string; + channel: string; + isGroup: boolean; + defaultGroupActivation: () => "always" | "mention"; + }; +} + +describe("discord native /status", () => { + beforeAll(async () => { + ({ createDiscordNativeCommand, __testing: discordNativeCommandTesting } = + await import("./native-command.js")); + }); + + beforeEach(() => { + vi.clearAllMocks(); + runtimeModuleMocks.dispatchReplyWithDispatcher.mockResolvedValue({ + counts: { + final: 0, + block: 0, + tool: 0, + }, + queuedFinal: false, + } as never); + runtimeModuleMocks.resolveDirectStatusReplyForSession.mockResolvedValue({ + text: "status reply", + }); + discordNativeCommandTesting.setDispatchReplyWithDispatcher( + runtimeModuleMocks.dispatchReplyWithDispatcher as typeof import("openclaw/plugin-sdk/reply-dispatch-runtime").dispatchReplyWithDispatcher, + ); + setDefaultRouteState(); + }); + + it("returns a direct status reply without falling through the generic dispatcher", async () => { + const cfg = createConfig(); + const command = await createStatusCommand(cfg); + const interaction = createInteraction(); + + await (command as { run: (interaction: unknown) => Promise }).run(interaction as unknown); + + expect(runtimeModuleMocks.resolveDirectStatusReplyForSession).toHaveBeenCalledTimes(1); + expect(runtimeModuleMocks.dispatchReplyWithDispatcher).not.toHaveBeenCalled(); + expect(interaction.followUp).toHaveBeenCalledWith( + expect.objectContaining({ + content: "status reply", + }), + ); + expect(interaction.reply).not.toHaveBeenCalled(); + }); + + it("passes through the effective guild activation when requireMention is disabled", async () => { + const cfg = createConfig({ requireMention: false }); + const command = await createStatusCommand(cfg); + const interaction = createInteraction({ + channelType: ChannelType.GuildText, + channelId: "chan1", + guildId: "guild1", + guildName: "Guild One", + }); + + await (command as { run: (interaction: unknown) => Promise }).run(interaction as unknown); + + const statusCall = firstStatusCall(); + expect(statusCall.channel).toBe("discord"); + expect(statusCall.isGroup).toBe(true); + expect(statusCall.defaultGroupActivation()).toBe("always"); + }); +}); diff --git a/extensions/discord/src/monitor/native-command.ts b/extensions/discord/src/monitor/native-command.ts index da3a4b594b5..22fb5e9c464 100644 --- a/extensions/discord/src/monitor/native-command.ts +++ b/extensions/discord/src/monitor/native-command.ts @@ -18,6 +18,7 @@ import { resolveCommandAuthorizedFromAuthorizers, resolveNativeCommandSessionTargets, } from "openclaw/plugin-sdk/command-auth-native"; +import { resolveDirectStatusReplyForSession } from "openclaw/plugin-sdk/command-status-runtime"; import type { OpenClawConfig, loadConfig } from "openclaw/plugin-sdk/config-runtime"; import { buildPairingReply } from "openclaw/plugin-sdk/conversation-runtime"; import { isDangerousNameMatchingEnabled } from "openclaw/plugin-sdk/dangerous-name-runtime"; @@ -755,6 +756,7 @@ async function dispatchDiscordCommandInteraction(params: { threadBindings, suppressReplies, } = params; + const commandName = command.nativeName ?? command.key; const respond = async (content: string, options?: { ephemeral?: boolean }) => { const payload = { content, @@ -869,15 +871,10 @@ async function dispatchDiscordCommandInteraction(params: { conversationId: rawChannelId || "unknown", parentConversationId: threadParentId, threadBinding: isThreadChannel ? threadBindings.getByThreadId(rawChannelId) : undefined, - enforceConfiguredBindingReadiness: !shouldBypassConfiguredAcpEnsure( - command.nativeName ?? command.key, - ), + enforceConfiguredBindingReadiness: !shouldBypassConfiguredAcpEnsure(commandName), })); const canBypassConfiguredAcpGuildGuards = async () => { - if ( - !interaction.guild || - !shouldBypassConfiguredAcpGuildGuards(command.nativeName ?? command.key) - ) { + if (!interaction.guild || !shouldBypassConfiguredAcpGuildGuards(commandName)) { return false; } const routeState = await getNativeRouteState(); @@ -1131,6 +1128,36 @@ async function dispatchDiscordCommandInteraction(params: { targetSessionKey: effectiveRoute.sessionKey, boundSessionKey, }); + const mediaLocalRoots = getAgentScopedMediaLocalRoots(cfg, effectiveRoute.agentId); + if (!suppressReplies && commandName === "status") { + const statusReply = await resolveDirectStatusReplyForSession({ + cfg, + sessionKey: commandTargetSessionKey?.trim() || sessionKey, + channel: "discord", + senderId: sender.id, + senderIsOwner: ownerOk, + isAuthorizedSender: commandAuthorized, + isGroup: isGuild || isGroupDm, + defaultGroupActivation: () => + !isGuild ? "always" : channelConfig?.requireMention === false ? "always" : "mention", + }); + if (statusReply && hasRenderableReplyPayload(statusReply)) { + await deliverDiscordInteractionReply({ + interaction, + payload: statusReply, + mediaLocalRoots, + textLimit: resolveTextChunkLimit(cfg, "discord", accountId, { + fallbackLimit: 2000, + }), + maxLinesPerMessage: resolveDiscordMaxLinesPerMessage({ cfg, discordConfig, accountId }), + preferFollowUp, + chunkMode: resolveChunkMode(cfg, "discord", accountId), + }); + return; + } + await respond("Status unavailable."); + return; + } const ctxPayload = buildDiscordNativeCommandContext({ prompt, commandArgs: commandArgs ?? {}, @@ -1164,7 +1191,6 @@ async function dispatchDiscordCommandInteraction(params: { channel: "discord", accountId: effectiveRoute.accountId, }); - const mediaLocalRoots = getAgentScopedMediaLocalRoots(cfg, effectiveRoute.agentId); const blockStreamingEnabled = resolveChannelStreamingBlockEnabled(discordConfig); let didReply = false; diff --git a/package.json b/package.json index 3d59181c34c..930b1b113cf 100644 --- a/package.json +++ b/package.json @@ -482,6 +482,10 @@ "types": "./dist/plugin-sdk/command-status.d.ts", "default": "./dist/plugin-sdk/command-status.js" }, + "./plugin-sdk/command-status-runtime": { + "types": "./dist/plugin-sdk/command-status-runtime.d.ts", + "default": "./dist/plugin-sdk/command-status-runtime.js" + }, "./plugin-sdk/command-detection": { "types": "./dist/plugin-sdk/command-detection.d.ts", "default": "./dist/plugin-sdk/command-detection.js" diff --git a/scripts/lib/plugin-sdk-doc-metadata.ts b/scripts/lib/plugin-sdk-doc-metadata.ts index abaf6a765f7..d3692cd2817 100644 --- a/scripts/lib/plugin-sdk-doc-metadata.ts +++ b/scripts/lib/plugin-sdk-doc-metadata.ts @@ -65,6 +65,9 @@ export const pluginSdkDocMetadata = { "command-status": { category: "channel", }, + "command-status-runtime": { + category: "runtime", + }, "secret-input": { category: "channel", }, diff --git a/scripts/lib/plugin-sdk-entrypoints.json b/scripts/lib/plugin-sdk-entrypoints.json index 3bf130d9277..13c8131216d 100644 --- a/scripts/lib/plugin-sdk-entrypoints.json +++ b/scripts/lib/plugin-sdk-entrypoints.json @@ -108,6 +108,7 @@ "command-auth", "command-auth-native", "command-status", + "command-status-runtime", "command-detection", "command-surface", "collection-runtime", diff --git a/src/plugin-sdk/command-status-runtime.ts b/src/plugin-sdk/command-status-runtime.ts new file mode 100644 index 00000000000..8d6e780f58e --- /dev/null +++ b/src/plugin-sdk/command-status-runtime.ts @@ -0,0 +1,13 @@ +import { createLazyRuntimeMethodBinder, createLazyRuntimeModule } from "../shared/lazy-runtime.js"; + +type CommandStatusRuntime = typeof import("./command-status.runtime.js"); + +const loadCommandStatusRuntime = createLazyRuntimeModule( + () => import("./command-status.runtime.js"), +); +const bindCommandStatusRuntime = createLazyRuntimeMethodBinder(loadCommandStatusRuntime); + +export type { ResolveDirectStatusReplyForSessionParams } from "./command-status.runtime.js"; + +export const resolveDirectStatusReplyForSession: CommandStatusRuntime["resolveDirectStatusReplyForSession"] = + bindCommandStatusRuntime((runtime) => runtime.resolveDirectStatusReplyForSession); diff --git a/src/plugin-sdk/command-status.runtime.ts b/src/plugin-sdk/command-status.runtime.ts new file mode 100644 index 00000000000..de65e91fca1 --- /dev/null +++ b/src/plugin-sdk/command-status.runtime.ts @@ -0,0 +1,125 @@ +import { listAgentEntries, resolveSessionAgentId } from "../agents/agent-scope.js"; +import { resolveDefaultModelForAgent } from "../agents/model-selection.js"; +import { buildStatusReply } from "../auto-reply/reply/commands-status.js"; +import type { CommandContext } from "../auto-reply/reply/commands-types.js"; +import { resolveDefaultModel } from "../auto-reply/reply/directive-handling.defaults.js"; +import { resolveCurrentDirectiveLevels } from "../auto-reply/reply/directive-handling.levels.js"; +import { createModelSelectionState } from "../auto-reply/reply/model-selection.js"; +import type { ReplyPayload } from "../auto-reply/types.js"; +import type { OpenClawConfig } from "../config/types.openclaw.js"; +import { loadSessionEntry } from "../gateway/session-utils.js"; + +export type ResolveDirectStatusReplyForSessionParams = { + cfg: OpenClawConfig; + sessionKey: string; + channel: string; + senderId?: string; + senderIsOwner: boolean; + isAuthorizedSender: boolean; + isGroup: boolean; + defaultGroupActivation: () => "always" | "mention"; +}; + +export async function resolveDirectStatusReplyForSession( + params: ResolveDirectStatusReplyForSessionParams, +): Promise { + const requestedSessionKey = params.sessionKey.trim(); + if (!requestedSessionKey) { + return undefined; + } + + const statusLoaded = loadSessionEntry(requestedSessionKey); + const statusCfg = statusLoaded.cfg ?? params.cfg; + const statusSessionKey = statusLoaded.canonicalKey; + const statusEntry = statusLoaded.entry; + const statusAgentId = resolveSessionAgentId({ + sessionKey: statusSessionKey, + config: statusCfg, + }); + const agentCfg = statusCfg.agents?.defaults; + const agentEntry = listAgentEntries(statusCfg).find( + (entry) => entry.id?.trim().toLowerCase() === statusAgentId, + ); + const statusModel = resolveDefaultModelForAgent({ + cfg: statusCfg, + agentId: statusAgentId, + }); + const { defaultProvider, defaultModel } = resolveDefaultModel({ + cfg: statusCfg, + agentId: statusAgentId, + }); + const selectedProvider = + statusEntry?.providerOverride?.trim() || + statusEntry?.modelProvider?.trim() || + statusModel.provider; + const selectedModel = + statusEntry?.modelOverride?.trim() || statusEntry?.model?.trim() || statusModel.model; + const modelState = await createModelSelectionState({ + cfg: statusCfg, + agentId: statusAgentId, + agentCfg, + sessionEntry: statusEntry, + sessionStore: statusLoaded.store, + sessionKey: statusSessionKey, + parentSessionKey: statusEntry?.parentSessionKey, + storePath: statusLoaded.storePath, + defaultProvider, + defaultModel, + provider: selectedProvider, + model: selectedModel, + hasModelDirective: false, + }); + const { + currentThinkLevel, + currentFastMode, + currentVerboseLevel, + currentReasoningLevel, + currentElevatedLevel, + } = await resolveCurrentDirectiveLevels({ + sessionEntry: statusEntry, + agentEntry, + agentCfg, + resolveDefaultThinkingLevel: () => modelState.resolveDefaultThinkingLevel(), + }); + let resolvedReasoningLevel = currentReasoningLevel; + const hasAgentReasoningDefault = + agentEntry?.reasoningDefault !== undefined && agentEntry.reasoningDefault !== null; + const reasoningExplicitlySet = + (statusEntry?.reasoningLevel !== undefined && statusEntry.reasoningLevel !== null) || + hasAgentReasoningDefault; + if (!reasoningExplicitlySet && resolvedReasoningLevel === "off" && currentThinkLevel === "off") { + resolvedReasoningLevel = await modelState.resolveDefaultReasoningLevel(); + } + + const command: CommandContext = { + surface: params.channel, + channel: params.channel, + ownerList: [], + senderIsOwner: params.senderIsOwner, + isAuthorizedSender: params.isAuthorizedSender, + senderId: params.senderId, + rawBodyNormalized: "/status", + commandBodyNormalized: "/status", + }; + + return await buildStatusReply({ + cfg: statusCfg, + command, + sessionEntry: statusEntry, + sessionKey: statusSessionKey, + parentSessionKey: statusEntry?.parentSessionKey, + sessionScope: statusCfg.session?.scope, + storePath: statusLoaded.storePath, + provider: selectedProvider, + model: selectedModel, + contextTokens: statusEntry?.contextTokens ?? 0, + resolvedThinkLevel: currentThinkLevel, + resolvedFastMode: currentFastMode, + resolvedVerboseLevel: currentVerboseLevel ?? "off", + resolvedReasoningLevel, + resolvedElevatedLevel: currentElevatedLevel, + resolveDefaultThinkingLevel: () => modelState.resolveDefaultThinkingLevel(), + isGroup: params.isGroup, + defaultGroupActivation: params.defaultGroupActivation, + }); +}