diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d3c1bfcf53..84a6e8d12aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -253,6 +253,7 @@ Docs: https://docs.openclaw.ai - CLI/Claude: rename the trusted inbound metadata schema to `openclaw.inbound_meta.v2` so Claude CLI no longer trips Anthropic's blocked `openclaw.inbound_meta.v1` filter on channel-originated turns. (#65399) Thanks @SzyMig and @vincentkoc. - Agents/inbound metadata: strip NUL bytes from serialized inbound context blocks before they reach backend spawn args, so malformed message metadata cannot crash agent spawn with `ERR_INVALID_ARG_VALUE`. (#65389) Thanks @adminfedres and @vincentkoc. - iMessage: retry transient `watch.subscribe` startup failures before tearing down the monitor, so brief local transport stalls do not immediately bounce the channel. (#65393) Thanks @vincentkoc. +- Status/session_status: move shared session status text into a neutral internal status module and keep the tool importing a local runtime shim, so built `session_status` no longer depends on reply command internals or a bundler-opaque runtime import. (#65807) Thanks @dutifulbob. ## 2026.4.9 diff --git a/src/agents/openclaw-tools.session-status.test.ts b/src/agents/openclaw-tools.session-status.test.ts index f4516c6ec10..83267b8c395 100644 --- a/src/agents/openclaw-tools.session-status.test.ts +++ b/src/agents/openclaw-tools.session-status.test.ts @@ -249,7 +249,7 @@ vi.mock("../plugins/providers.runtime.js", () => ({ vi.mock("../agents/auth-profiles.js", createAuthProfilesModuleMock); vi.mock("../agents/model-auth.js", createModelAuthModuleMock); vi.mock("../infra/provider-usage.js", createProviderUsageModuleMock); -vi.mock("../auto-reply/reply/commands-status.runtime.js", createCommandsStatusRuntimeModuleMock); +vi.mock("./tools/session-status.runtime.js", createCommandsStatusRuntimeModuleMock); vi.mock("../auto-reply/group-activation.js", () => ({ normalizeGroupActivation: (value: unknown) => value ?? "always", })); diff --git a/src/agents/tools/session-status-tool.ts b/src/agents/tools/session-status-tool.ts index daab753b108..c366499c5a5 100644 --- a/src/agents/tools/session-status-tool.ts +++ b/src/agents/tools/session-status-tool.ts @@ -21,7 +21,6 @@ import { resolveAgentIdFromSessionKey, } from "../../routing/session-key.js"; import { applyModelOverrideToSessionEntry } from "../../sessions/model-overrides.js"; -import { importRuntimeModule } from "../../shared/runtime-import.js"; import { normalizeOptionalLowercaseString } from "../../shared/string-coerce.js"; import { buildTaskStatusSnapshotForRelatedSessionKeyForOwner } from "../../tasks/task-owner-access.js"; import { formatTaskStatusDetail, formatTaskStatusTitle } from "../../tasks/task-status.js"; @@ -84,18 +83,11 @@ type CommandsStatusRuntimeModule = { }) => Promise; }; -const COMMANDS_STATUS_RUNTIME_SPEC = [ - "../../auto-reply/reply/commands-status.runtime", - ".js", -] as const; - let commandsStatusRuntimePromise: Promise | null = null; function loadCommandsStatusRuntime(): Promise { - commandsStatusRuntimePromise ??= importRuntimeModule( - import.meta.url, - COMMANDS_STATUS_RUNTIME_SPEC, - ); + commandsStatusRuntimePromise ??= + import("./session-status.runtime.js") as Promise; return commandsStatusRuntimePromise; } diff --git a/src/agents/tools/session-status.runtime.ts b/src/agents/tools/session-status.runtime.ts new file mode 100644 index 00000000000..4ba362dae8c --- /dev/null +++ b/src/agents/tools/session-status.runtime.ts @@ -0,0 +1 @@ +export { buildStatusText } from "../../status/status-text.js"; diff --git a/src/auto-reply/fallback-state.ts b/src/auto-reply/fallback-state.ts index ccbffbbc523..7ee4e65e553 100644 --- a/src/auto-reply/fallback-state.ts +++ b/src/auto-reply/fallback-state.ts @@ -1,19 +1,18 @@ import { formatRawAssistantErrorForUi } from "../agents/pi-embedded-helpers.js"; -import type { SessionEntry } from "../config/sessions.js"; import { normalizeOptionalString } from "../shared/string-coerce.js"; +import type { FallbackNoticeState } from "../status/fallback-notice-state.js"; import { formatProviderModelRef } from "./model-runtime.js"; import type { RuntimeFallbackAttempt } from "./reply/agent-runner-execution.js"; +export { + resolveActiveFallbackState, + type FallbackNoticeState, +} from "../status/fallback-notice-state.js"; const FALLBACK_REASON_PART_MAX = 80; const TRANSIENT_FALLBACK_REASONS = new Set(["rate_limit", "overloaded", "timeout"]); const TRANSIENT_ERROR_DETAIL_HINT_RE = /\b(?:429|5\d\d|too many requests|usage limit|quota|try again in|retry[- ]after|seconds?|minutes?|hours?|temporarily unavailable|overloaded|service unavailable|throttl)\b/i; -export type FallbackNoticeState = Pick< - SessionEntry, - "fallbackNoticeSelectedModel" | "fallbackNoticeActiveModel" | "fallbackNoticeReason" ->; - function truncateFallbackReasonPart(value: string, max = FALLBACK_REASON_PART_MAX): string { const text = value.replace(/\s+/g, " ").trim(); if (text.length <= max) { @@ -110,24 +109,6 @@ export function buildFallbackClearedNotice(params: { return `鈫笍 Model Fallback cleared: ${selected}`; } -export function resolveActiveFallbackState(params: { - selectedModelRef: string; - activeModelRef: string; - state?: FallbackNoticeState; -}): { active: boolean; reason?: string } { - const selected = normalizeOptionalString(params.state?.fallbackNoticeSelectedModel); - const active = normalizeOptionalString(params.state?.fallbackNoticeActiveModel); - const reason = normalizeOptionalString(params.state?.fallbackNoticeReason); - const fallbackActive = - params.selectedModelRef !== params.activeModelRef && - selected === params.selectedModelRef && - active === params.activeModelRef; - return { - active: fallbackActive, - reason: fallbackActive ? reason : undefined, - }; -} - export type ResolvedFallbackTransition = { selectedModelRef: string; activeModelRef: string; diff --git a/src/auto-reply/reply/commands-status.runtime.ts b/src/auto-reply/reply/commands-status.runtime.ts index ab30dc9b4a8..f272eba9f9b 100644 --- a/src/auto-reply/reply/commands-status.runtime.ts +++ b/src/auto-reply/reply/commands-status.runtime.ts @@ -1 +1,2 @@ -export { buildStatusReply, buildStatusText } from "./commands-status.js"; +export { buildStatusReply } from "./commands-status.js"; +export { buildStatusText } from "../../status/status-text.js"; diff --git a/src/auto-reply/reply/commands-status.ts b/src/auto-reply/reply/commands-status.ts index 5664e858ef1..289dc97c585 100644 --- a/src/auto-reply/reply/commands-status.ts +++ b/src/auto-reply/reply/commands-status.ts @@ -1,153 +1,16 @@ -import { - resolveAgentConfig, - resolveAgentDir, - resolveDefaultAgentId, - resolveSessionAgentId, - resolveAgentModelFallbacksOverride, -} from "../../agents/agent-scope.js"; -import { resolveFastModeState } from "../../agents/fast-mode.js"; -import { resolveModelAuthLabel } from "../../agents/model-auth-label.js"; -import { - resolveInternalSessionKey, - resolveMainSessionAlias, -} from "../../agents/tools/sessions-helpers.js"; -import { toAgentModelListLike } from "../../config/model-input.js"; -import type { SessionEntry, SessionScope } from "../../config/sessions.js"; -import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { logVerbose } from "../../globals.js"; -import { - formatUsageWindowSummary, - loadProviderUsageSummary, - resolveUsageProviderId, -} from "../../infra/provider-usage.js"; -import type { MediaUnderstandingDecision } from "../../media-understanding/types.js"; -import { importRuntimeModule } from "../../shared/runtime-import.js"; -import { normalizeOptionalLowercaseString } from "../../shared/string-coerce.js"; -import { - listTasksForAgentIdForStatus, - listTasksForSessionKeyForStatus, -} from "../../tasks/task-status-access.js"; -import { - buildTaskStatusSnapshot, - formatTaskStatusDetail, - formatTaskStatusTitle, -} from "../../tasks/task-status.js"; -import { normalizeGroupActivation } from "../group-activation.js"; -import { resolveSelectedAndActiveModel } from "../model-runtime.js"; -import type { ElevatedLevel, ReasoningLevel, ThinkLevel, VerboseLevel } from "../thinking.js"; +import { buildStatusText, type BuildStatusTextParams } from "../../status/status-text.js"; import type { ReplyPayload } from "../types.js"; import type { CommandContext } from "./commands-types.js"; -import { getFollowupQueueDepth, resolveQueueSettings } from "./queue.js"; +export { buildStatusText } from "../../status/status-text.js"; -// Some usage endpoints only work with CLI/session OAuth tokens, not API keys. -// Skip those probes when the active auth mode cannot satisfy the endpoint. -const USAGE_OAUTH_ONLY_PROVIDERS = new Set([ - "anthropic", - "github-copilot", - "google-gemini-cli", - "openai-codex", -]); - -type StatusRuntimeModule = { - buildStatusMessage: (args: Record) => string; -}; -type CommandsStatusSubagentsModule = { - buildSubagentsStatusLine: (params: { - runs: Array<{ childSessionKey: string; endedAt?: number | null }>; - verboseEnabled: boolean; - pendingDescendantsForRun: (entry: { childSessionKey: string }) => number; - }) => string | undefined; - countPendingDescendantRuns: (rootSessionKey: string) => number; - listControlledSubagentRuns: ( - controllerSessionKey: string, - ) => Array<{ childSessionKey: string; endedAt?: number | null }>; -}; - -const STATUS_RUNTIME_SPEC = ["../status.runtime", ".js"] as const; -const COMMANDS_STATUS_DEPS_RUNTIME_SPEC = ["./commands-status-deps.runtime", ".js"] as const; - -let statusRuntimePromise: Promise | null = null; -let commandsStatusDepsRuntimePromise: Promise | null = null; - -function loadStatusRuntime(): Promise { - statusRuntimePromise ??= importRuntimeModule( - import.meta.url, - STATUS_RUNTIME_SPEC, - ); - return statusRuntimePromise; -} - -function loadCommandsStatusDepsRuntime(): Promise { - commandsStatusDepsRuntimePromise ??= importRuntimeModule( - import.meta.url, - COMMANDS_STATUS_DEPS_RUNTIME_SPEC, - ); - return commandsStatusDepsRuntimePromise; -} - -function shouldLoadUsageSummary(params: { - provider?: string; - selectedModelAuth?: string; -}): boolean { - if (!params.provider) { - return false; - } - if (!USAGE_OAUTH_ONLY_PROVIDERS.has(params.provider)) { - return true; - } - const auth = normalizeOptionalLowercaseString(params.selectedModelAuth); - return Boolean(auth?.startsWith("oauth") || auth?.startsWith("token")); -} - -function formatSessionTaskLine(sessionKey: string): string | undefined { - const snapshot = buildTaskStatusSnapshot(listTasksForSessionKeyForStatus(sessionKey)); - const task = snapshot.focus; - if (!task) { - return undefined; - } - const headline = - snapshot.activeCount > 0 - ? `${snapshot.activeCount} active 路 ${snapshot.totalCount} total` - : snapshot.recentFailureCount > 0 - ? `${snapshot.recentFailureCount} recent failure${snapshot.recentFailureCount === 1 ? "" : "s"}` - : "recently finished"; - const title = formatTaskStatusTitle(task); - const detail = formatTaskStatusDetail(task); - const parts = [headline, task.runtime, title, detail].filter(Boolean); - return parts.length ? `馃搶 Tasks: ${parts.join(" 路 ")}` : undefined; -} - -function formatAgentTaskCountsLine(agentId: string): string | undefined { - const snapshot = buildTaskStatusSnapshot(listTasksForAgentIdForStatus(agentId)); - if (snapshot.totalCount === 0) { - return undefined; - } - return `馃搶 Tasks: ${snapshot.activeCount} active 路 ${snapshot.totalCount} total 路 agent-local`; -} - -export async function buildStatusReply(params: { - cfg: OpenClawConfig; +type BuildStatusReplyParams = Omit & { command: CommandContext; - sessionEntry?: SessionEntry; - sessionKey: string; - parentSessionKey?: string; - sessionScope?: SessionScope; - storePath?: string; - provider: string; - model: string; - contextTokens: number; - resolvedThinkLevel?: ThinkLevel; - resolvedFastMode?: boolean; - resolvedVerboseLevel: VerboseLevel; - resolvedReasoningLevel: ReasoningLevel; - resolvedElevatedLevel?: ElevatedLevel; - resolveDefaultThinkingLevel: () => Promise; - isGroup: boolean; - defaultGroupActivation: () => "always" | "mention"; - mediaDecisions?: MediaUnderstandingDecision[]; - modelAuthOverride?: string; - activeModelAuthOverride?: string; -}): Promise { +}; + +export async function buildStatusReply( + params: BuildStatusReplyParams, +): Promise { const { command } = params; if (!command.isAuthorizedSender) { logVerbose(`Ignoring /status from unauthorized sender: ${command.senderId || ""}`); @@ -161,225 +24,3 @@ export async function buildStatusReply(params: { }), }; } - -export async function buildStatusText(params: { - cfg: OpenClawConfig; - sessionEntry?: SessionEntry; - sessionKey: string; - parentSessionKey?: string; - sessionScope?: SessionScope; - storePath?: string; - statusChannel: string; - provider: string; - model: string; - contextTokens?: number; - resolvedThinkLevel?: ThinkLevel; - resolvedFastMode?: boolean; - resolvedVerboseLevel: VerboseLevel; - resolvedReasoningLevel: ReasoningLevel; - resolvedElevatedLevel?: ElevatedLevel; - resolveDefaultThinkingLevel: () => Promise; - isGroup: boolean; - defaultGroupActivation: () => "always" | "mention"; - mediaDecisions?: MediaUnderstandingDecision[]; - taskLineOverride?: string; - skipDefaultTaskLookup?: boolean; - primaryModelLabelOverride?: string; - modelAuthOverride?: string; - activeModelAuthOverride?: string; - includeTranscriptUsage?: boolean; -}): Promise { - const { - cfg, - sessionEntry, - sessionKey, - parentSessionKey, - sessionScope, - storePath, - statusChannel, - provider, - model, - contextTokens, - resolvedThinkLevel, - resolvedFastMode, - resolvedVerboseLevel, - resolvedReasoningLevel, - resolvedElevatedLevel, - resolveDefaultThinkingLevel, - isGroup, - defaultGroupActivation, - } = params; - const statusAgentId = sessionKey - ? resolveSessionAgentId({ sessionKey, config: cfg }) - : resolveDefaultAgentId(cfg); - const statusAgentDir = resolveAgentDir(cfg, statusAgentId); - const modelRefs = resolveSelectedAndActiveModel({ - selectedProvider: provider, - selectedModel: model, - sessionEntry, - }); - const selectedModelAuth = Object.hasOwn(params, "modelAuthOverride") - ? params.modelAuthOverride - : resolveModelAuthLabel({ - provider, - cfg, - sessionEntry, - agentDir: statusAgentDir, - }); - const activeModelAuth = Object.hasOwn(params, "activeModelAuthOverride") - ? params.activeModelAuthOverride - : modelRefs.activeDiffers - ? resolveModelAuthLabel({ - provider: modelRefs.active.provider, - cfg, - sessionEntry, - agentDir: statusAgentDir, - }) - : selectedModelAuth; - const currentUsageProvider = (() => { - try { - return resolveUsageProviderId(provider); - } catch { - return undefined; - } - })(); - let usageLine: string | null = null; - if ( - currentUsageProvider && - shouldLoadUsageSummary({ - provider: currentUsageProvider, - selectedModelAuth, - }) - ) { - try { - const usageSummaryTimeoutMs = 3500; - let usageTimeout: NodeJS.Timeout | undefined; - const usageSummary = await Promise.race([ - loadProviderUsageSummary({ - timeoutMs: usageSummaryTimeoutMs, - providers: [currentUsageProvider], - agentDir: statusAgentDir, - }), - new Promise((_, reject) => { - usageTimeout = setTimeout( - () => reject(new Error("usage summary timeout")), - usageSummaryTimeoutMs, - ); - }), - ]).finally(() => { - if (usageTimeout) { - clearTimeout(usageTimeout); - } - }); - const usageEntry = usageSummary.providers[0]; - if (usageEntry && !usageEntry.error && usageEntry.windows.length > 0) { - const summaryLine = formatUsageWindowSummary(usageEntry, { - now: Date.now(), - maxWindows: 2, - includeResets: true, - }); - if (summaryLine) { - usageLine = `馃搳 Usage: ${summaryLine}`; - } - } - } catch { - usageLine = null; - } - } - const queueSettings = resolveQueueSettings({ - cfg, - channel: statusChannel, - sessionEntry, - }); - const queueKey = sessionKey ?? sessionEntry?.sessionId; - const queueDepth = queueKey ? getFollowupQueueDepth(queueKey) : 0; - const queueOverrides = Boolean( - sessionEntry?.queueDebounceMs ?? sessionEntry?.queueCap ?? sessionEntry?.queueDrop, - ); - - let subagentsLine: string | undefined; - let taskLine: string | undefined; - if (sessionKey) { - const { mainKey, alias } = resolveMainSessionAlias(cfg); - const requesterKey = resolveInternalSessionKey({ key: sessionKey, alias, mainKey }); - taskLine = params.skipDefaultTaskLookup - ? params.taskLineOverride - : (params.taskLineOverride ?? formatSessionTaskLine(requesterKey)); - if (!taskLine && !params.skipDefaultTaskLookup) { - taskLine = formatAgentTaskCountsLine(statusAgentId); - } - const { buildSubagentsStatusLine, countPendingDescendantRuns, listControlledSubagentRuns } = - await loadCommandsStatusDepsRuntime(); - const runs = listControlledSubagentRuns(requesterKey); - const verboseEnabled = resolvedVerboseLevel && resolvedVerboseLevel !== "off"; - subagentsLine = buildSubagentsStatusLine({ - runs, - verboseEnabled, - pendingDescendantsForRun: (entry) => countPendingDescendantRuns(entry.childSessionKey), - }); - } - const groupActivation = isGroup - ? (normalizeGroupActivation(sessionEntry?.groupActivation) ?? defaultGroupActivation()) - : undefined; - const agentDefaults = cfg.agents?.defaults ?? {}; - const agentConfig = resolveAgentConfig(cfg, statusAgentId); - const effectiveFastMode = - resolvedFastMode ?? - resolveFastModeState({ - cfg, - provider, - model, - agentId: statusAgentId, - sessionEntry, - }).enabled; - const agentFallbacksOverride = resolveAgentModelFallbacksOverride(cfg, statusAgentId); - const { buildStatusMessage } = await loadStatusRuntime(); - const statusText = buildStatusMessage({ - config: cfg, - agent: { - ...agentDefaults, - model: { - ...toAgentModelListLike(agentDefaults.model), - primary: params.primaryModelLabelOverride ?? `${provider}/${model}`, - ...(agentFallbacksOverride === undefined ? {} : { fallbacks: agentFallbacksOverride }), - }, - ...(typeof contextTokens === "number" && contextTokens > 0 ? { contextTokens } : {}), - thinkingDefault: agentConfig?.thinkingDefault ?? agentDefaults.thinkingDefault, - verboseDefault: agentDefaults.verboseDefault, - elevatedDefault: agentDefaults.elevatedDefault, - }, - agentId: statusAgentId, - explicitConfiguredContextTokens: - typeof agentDefaults.contextTokens === "number" && agentDefaults.contextTokens > 0 - ? agentDefaults.contextTokens - : undefined, - sessionEntry, - sessionKey, - parentSessionKey, - sessionScope, - sessionStorePath: storePath, - groupActivation, - resolvedThink: resolvedThinkLevel ?? (await resolveDefaultThinkingLevel()), - resolvedFast: effectiveFastMode, - resolvedVerbose: resolvedVerboseLevel, - resolvedReasoning: resolvedReasoningLevel, - resolvedElevated: resolvedElevatedLevel, - modelAuth: selectedModelAuth, - activeModelAuth, - usageLine: usageLine ?? undefined, - queue: { - mode: queueSettings.mode, - depth: queueDepth, - debounceMs: queueSettings.debounceMs, - cap: queueSettings.cap, - dropPolicy: queueSettings.dropPolicy, - showDetails: queueOverrides, - }, - subagentsLine, - taskLine, - mediaDecisions: params.mediaDecisions, - includeTranscriptUsage: params.includeTranscriptUsage ?? true, - }); - - return statusText; -} diff --git a/src/auto-reply/status.ts b/src/auto-reply/status.ts index ad838cca288..e64abd6381e 100644 --- a/src/auto-reply/status.ts +++ b/src/auto-reply/status.ts @@ -53,7 +53,7 @@ export { type CommandsMessageOptions, type CommandsMessageResult, } from "./command-status-builders.js"; -import { resolveActiveFallbackState } from "./fallback-state.js"; +import { resolveActiveFallbackState } from "../status/fallback-notice-state.js"; import { formatProviderModelRef, resolveSelectedAndActiveModel } from "./model-runtime.js"; import type { ElevatedLevel, ReasoningLevel, ThinkLevel, VerboseLevel } from "./thinking.js"; diff --git a/src/status/fallback-notice-state.ts b/src/status/fallback-notice-state.ts new file mode 100644 index 00000000000..3857899c365 --- /dev/null +++ b/src/status/fallback-notice-state.ts @@ -0,0 +1,25 @@ +import type { SessionEntry } from "../config/sessions.js"; +import { normalizeOptionalString } from "../shared/string-coerce.js"; + +export type FallbackNoticeState = Pick< + SessionEntry, + "fallbackNoticeSelectedModel" | "fallbackNoticeActiveModel" | "fallbackNoticeReason" +>; + +export function resolveActiveFallbackState(params: { + selectedModelRef: string; + activeModelRef: string; + state?: FallbackNoticeState; +}): { active: boolean; reason?: string } { + const selected = normalizeOptionalString(params.state?.fallbackNoticeSelectedModel); + const active = normalizeOptionalString(params.state?.fallbackNoticeActiveModel); + const reason = normalizeOptionalString(params.state?.fallbackNoticeReason); + const fallbackActive = + params.selectedModelRef !== params.activeModelRef && + selected === params.selectedModelRef && + active === params.activeModelRef; + return { + active: fallbackActive, + reason: fallbackActive ? reason : undefined, + }; +} diff --git a/src/status/status-message.runtime.ts b/src/status/status-message.runtime.ts new file mode 100644 index 00000000000..c2e751eed73 --- /dev/null +++ b/src/status/status-message.runtime.ts @@ -0,0 +1,3 @@ +export async function loadStatusMessageRuntimeModule() { + return await import("../auto-reply/status.runtime.js"); +} diff --git a/src/status/status-queue.runtime.ts b/src/status/status-queue.runtime.ts new file mode 100644 index 00000000000..49feeceb8b2 --- /dev/null +++ b/src/status/status-queue.runtime.ts @@ -0,0 +1 @@ +export { getFollowupQueueDepth, resolveQueueSettings } from "../auto-reply/reply/queue.js"; diff --git a/src/status/status-subagents.runtime.ts b/src/status/status-subagents.runtime.ts new file mode 100644 index 00000000000..ad5ea03e0fb --- /dev/null +++ b/src/status/status-subagents.runtime.ts @@ -0,0 +1,3 @@ +export { listControlledSubagentRuns } from "../agents/subagent-control.js"; +export { countPendingDescendantRuns } from "../agents/subagent-registry.js"; +export { buildSubagentsStatusLine } from "../auto-reply/reply/commands-status-subagents.js"; diff --git a/src/status/status-text.ts b/src/status/status-text.ts new file mode 100644 index 00000000000..b00dca5625f --- /dev/null +++ b/src/status/status-text.ts @@ -0,0 +1,335 @@ +import { + resolveAgentConfig, + resolveAgentDir, + resolveDefaultAgentId, + resolveSessionAgentId, + resolveAgentModelFallbacksOverride, +} from "../agents/agent-scope.js"; +import { resolveFastModeState } from "../agents/fast-mode.js"; +import { resolveModelAuthLabel } from "../agents/model-auth-label.js"; +import { + resolveInternalSessionKey, + resolveMainSessionAlias, +} from "../agents/tools/sessions-helpers.js"; +import { normalizeGroupActivation } from "../auto-reply/group-activation.js"; +import { resolveSelectedAndActiveModel } from "../auto-reply/model-runtime.js"; +import type { + ElevatedLevel, + ReasoningLevel, + ThinkLevel, + VerboseLevel, +} from "../auto-reply/thinking.js"; +import { toAgentModelListLike } from "../config/model-input.js"; +import type { SessionEntry, SessionScope } from "../config/sessions.js"; +import type { OpenClawConfig } from "../config/types.openclaw.js"; +import { + formatUsageWindowSummary, + loadProviderUsageSummary, + resolveUsageProviderId, +} from "../infra/provider-usage.js"; +import type { MediaUnderstandingDecision } from "../media-understanding/types.js"; +import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js"; +import { + listTasksForAgentIdForStatus, + listTasksForSessionKeyForStatus, +} from "../tasks/task-status-access.js"; +import { + buildTaskStatusSnapshot, + formatTaskStatusDetail, + formatTaskStatusTitle, +} from "../tasks/task-status.js"; + +export type BuildStatusTextParams = { + cfg: OpenClawConfig; + sessionEntry?: SessionEntry; + sessionKey: string; + parentSessionKey?: string; + sessionScope?: SessionScope; + storePath?: string; + statusChannel: string; + provider: string; + model: string; + contextTokens?: number; + resolvedThinkLevel?: ThinkLevel; + resolvedFastMode?: boolean; + resolvedVerboseLevel: VerboseLevel; + resolvedReasoningLevel: ReasoningLevel; + resolvedElevatedLevel?: ElevatedLevel; + resolveDefaultThinkingLevel: () => Promise; + isGroup: boolean; + defaultGroupActivation: () => "always" | "mention"; + mediaDecisions?: MediaUnderstandingDecision[]; + taskLineOverride?: string; + skipDefaultTaskLookup?: boolean; + primaryModelLabelOverride?: string; + modelAuthOverride?: string; + activeModelAuthOverride?: string; + includeTranscriptUsage?: boolean; +}; + +const USAGE_OAUTH_ONLY_PROVIDERS = new Set([ + "anthropic", + "github-copilot", + "google-gemini-cli", + "openai-codex", +]); + +let statusMessageRuntimePromise: Promise | null = + null; +let statusQueueRuntimePromise: Promise | null = null; +let statusSubagentsRuntimePromise: Promise | null = + null; + +function loadStatusMessageRuntime(): Promise { + const runtimePromise = (statusMessageRuntimePromise ??= + import("./status-message.runtime.js").then((module) => + module.loadStatusMessageRuntimeModule(), + )); + return runtimePromise; +} + +function loadStatusSubagentsRuntime(): Promise { + const runtimePromise = (statusSubagentsRuntimePromise ??= + import("./status-subagents.runtime.js")); + return runtimePromise; +} + +function loadStatusQueueRuntime(): Promise { + const runtimePromise = (statusQueueRuntimePromise ??= import("./status-queue.runtime.js")); + return runtimePromise; +} + +function shouldLoadUsageSummary(params: { + provider?: string; + selectedModelAuth?: string; +}): boolean { + if (!params.provider) { + return false; + } + if (!USAGE_OAUTH_ONLY_PROVIDERS.has(params.provider)) { + return true; + } + const auth = normalizeOptionalLowercaseString(params.selectedModelAuth); + return Boolean(auth?.startsWith("oauth") || auth?.startsWith("token")); +} + +function formatSessionTaskLine(sessionKey: string): string | undefined { + const snapshot = buildTaskStatusSnapshot(listTasksForSessionKeyForStatus(sessionKey)); + const task = snapshot.focus; + if (!task) { + return undefined; + } + const headline = + snapshot.activeCount > 0 + ? `${snapshot.activeCount} active 路 ${snapshot.totalCount} total` + : snapshot.recentFailureCount > 0 + ? `${snapshot.recentFailureCount} recent failure${snapshot.recentFailureCount === 1 ? "" : "s"}` + : "recently finished"; + const title = formatTaskStatusTitle(task); + const detail = formatTaskStatusDetail(task); + const parts = [headline, task.runtime, title, detail].filter(Boolean); + return parts.length ? `馃搶 Tasks: ${parts.join(" 路 ")}` : undefined; +} + +function formatAgentTaskCountsLine(agentId: string): string | undefined { + const snapshot = buildTaskStatusSnapshot(listTasksForAgentIdForStatus(agentId)); + if (snapshot.totalCount === 0) { + return undefined; + } + return `馃搶 Tasks: ${snapshot.activeCount} active 路 ${snapshot.totalCount} total 路 agent-local`; +} + +export async function buildStatusText(params: BuildStatusTextParams): Promise { + const { + cfg, + sessionEntry, + sessionKey, + parentSessionKey, + sessionScope, + storePath, + statusChannel, + provider, + model, + contextTokens, + resolvedThinkLevel, + resolvedFastMode, + resolvedVerboseLevel, + resolvedReasoningLevel, + resolvedElevatedLevel, + resolveDefaultThinkingLevel, + isGroup, + defaultGroupActivation, + } = params; + const statusAgentId = sessionKey + ? resolveSessionAgentId({ sessionKey, config: cfg }) + : resolveDefaultAgentId(cfg); + const statusAgentDir = resolveAgentDir(cfg, statusAgentId); + const modelRefs = resolveSelectedAndActiveModel({ + selectedProvider: provider, + selectedModel: model, + sessionEntry, + }); + const selectedModelAuth = Object.hasOwn(params, "modelAuthOverride") + ? params.modelAuthOverride + : resolveModelAuthLabel({ + provider, + cfg, + sessionEntry, + agentDir: statusAgentDir, + }); + const activeModelAuth = Object.hasOwn(params, "activeModelAuthOverride") + ? params.activeModelAuthOverride + : modelRefs.activeDiffers + ? resolveModelAuthLabel({ + provider: modelRefs.active.provider, + cfg, + sessionEntry, + agentDir: statusAgentDir, + }) + : selectedModelAuth; + const currentUsageProvider = (() => { + try { + return resolveUsageProviderId(provider); + } catch { + return undefined; + } + })(); + let usageLine: string | null = null; + if ( + currentUsageProvider && + shouldLoadUsageSummary({ + provider: currentUsageProvider, + selectedModelAuth, + }) + ) { + try { + const usageSummaryTimeoutMs = 3500; + let usageTimeout: NodeJS.Timeout | undefined; + const usageSummary = await Promise.race([ + loadProviderUsageSummary({ + timeoutMs: usageSummaryTimeoutMs, + providers: [currentUsageProvider], + agentDir: statusAgentDir, + }), + new Promise((_, reject) => { + usageTimeout = setTimeout( + () => reject(new Error("usage summary timeout")), + usageSummaryTimeoutMs, + ); + }), + ]).finally(() => { + if (usageTimeout) { + clearTimeout(usageTimeout); + } + }); + const usageEntry = usageSummary.providers[0]; + if (usageEntry && !usageEntry.error && usageEntry.windows.length > 0) { + const summaryLine = formatUsageWindowSummary(usageEntry, { + now: Date.now(), + maxWindows: 2, + includeResets: true, + }); + if (summaryLine) { + usageLine = `馃搳 Usage: ${summaryLine}`; + } + } + } catch { + usageLine = null; + } + } + const { getFollowupQueueDepth, resolveQueueSettings } = await loadStatusQueueRuntime(); + const queueSettings = resolveQueueSettings({ + cfg, + channel: statusChannel, + sessionEntry, + }); + const queueKey = sessionKey ?? sessionEntry?.sessionId; + const queueDepth = queueKey ? getFollowupQueueDepth(queueKey) : 0; + const queueOverrides = Boolean( + sessionEntry?.queueDebounceMs ?? sessionEntry?.queueCap ?? sessionEntry?.queueDrop, + ); + + let subagentsLine: string | undefined; + let taskLine: string | undefined; + if (sessionKey) { + const { mainKey, alias } = resolveMainSessionAlias(cfg); + const requesterKey = resolveInternalSessionKey({ key: sessionKey, alias, mainKey }); + taskLine = params.skipDefaultTaskLookup + ? params.taskLineOverride + : (params.taskLineOverride ?? formatSessionTaskLine(requesterKey)); + if (!taskLine && !params.skipDefaultTaskLookup) { + taskLine = formatAgentTaskCountsLine(statusAgentId); + } + const { buildSubagentsStatusLine, countPendingDescendantRuns, listControlledSubagentRuns } = + await loadStatusSubagentsRuntime(); + const runs = listControlledSubagentRuns(requesterKey); + const verboseEnabled = resolvedVerboseLevel && resolvedVerboseLevel !== "off"; + subagentsLine = buildSubagentsStatusLine({ + runs, + verboseEnabled, + pendingDescendantsForRun: (entry) => countPendingDescendantRuns(entry.childSessionKey), + }); + } + const groupActivation = isGroup + ? (normalizeGroupActivation(sessionEntry?.groupActivation) ?? defaultGroupActivation()) + : undefined; + const agentDefaults = cfg.agents?.defaults ?? {}; + const agentConfig = resolveAgentConfig(cfg, statusAgentId); + const effectiveFastMode = + resolvedFastMode ?? + resolveFastModeState({ + cfg, + provider, + model, + agentId: statusAgentId, + sessionEntry, + }).enabled; + const agentFallbacksOverride = resolveAgentModelFallbacksOverride(cfg, statusAgentId); + const { buildStatusMessage } = await loadStatusMessageRuntime(); + return buildStatusMessage({ + config: cfg, + agent: { + ...agentDefaults, + model: { + ...toAgentModelListLike(agentDefaults.model), + primary: params.primaryModelLabelOverride ?? `${provider}/${model}`, + ...(agentFallbacksOverride === undefined ? {} : { fallbacks: agentFallbacksOverride }), + }, + ...(typeof contextTokens === "number" && contextTokens > 0 ? { contextTokens } : {}), + thinkingDefault: agentConfig?.thinkingDefault ?? agentDefaults.thinkingDefault, + verboseDefault: agentDefaults.verboseDefault, + elevatedDefault: agentDefaults.elevatedDefault, + }, + agentId: statusAgentId, + explicitConfiguredContextTokens: + typeof agentDefaults.contextTokens === "number" && agentDefaults.contextTokens > 0 + ? agentDefaults.contextTokens + : undefined, + sessionEntry, + sessionKey, + parentSessionKey, + sessionScope, + sessionStorePath: storePath, + groupActivation, + resolvedThink: resolvedThinkLevel ?? (await resolveDefaultThinkingLevel()), + resolvedFast: effectiveFastMode, + resolvedVerbose: resolvedVerboseLevel, + resolvedReasoning: resolvedReasoningLevel, + resolvedElevated: resolvedElevatedLevel, + modelAuth: selectedModelAuth, + activeModelAuth, + usageLine: usageLine ?? undefined, + queue: { + mode: queueSettings.mode, + depth: queueDepth, + debounceMs: queueSettings.debounceMs, + cap: queueSettings.cap, + dropPolicy: queueSettings.dropPolicy, + showDetails: queueOverrides, + }, + subagentsLine, + taskLine, + mediaDecisions: params.mediaDecisions, + includeTranscriptUsage: params.includeTranscriptUsage ?? true, + }); +}