fix(status): align session_status with /status

This commit is contained in:
Vincent Koc
2026-04-01 06:40:13 +09:00
parent 94d72efedc
commit 11318ef9b9
2 changed files with 106 additions and 133 deletions

View File

@@ -1,7 +1,11 @@
import { Type } from "@sinclair/typebox";
import { normalizeGroupActivation } from "../../auto-reply/group-activation.js";
import { getFollowupQueueDepth, resolveQueueSettings } from "../../auto-reply/reply/queue.js";
import { buildStatusMessage } from "../../auto-reply/status.js";
import { buildStatusText } from "../../auto-reply/reply/commands-status.js";
import type {
ElevatedLevel,
ReasoningLevel,
ThinkLevel,
VerboseLevel,
} from "../../auto-reply/thinking.js";
import type { OpenClawConfig } from "../../config/config.js";
import { loadConfig } from "../../config/config.js";
import {
@@ -11,11 +15,6 @@ import {
updateSessionStore,
} from "../../config/sessions.js";
import { resolveSessionModelIdentityRef } from "../../gateway/session-utils.js";
import {
formatUsageWindowSummary,
loadProviderUsageSummary,
resolveUsageProviderId,
} from "../../infra/provider-usage.js";
import {
buildAgentMainSessionKey,
DEFAULT_AGENT_ID,
@@ -24,9 +23,6 @@ import {
} from "../../routing/session-key.js";
import { applyModelOverrideToSessionEntry } from "../../sessions/model-overrides.js";
import { listTasksForRelatedSessionKeyForOwner } from "../../tasks/task-owner-access.js";
import { resolveAgentConfig, resolveAgentDir } from "../agent-scope.js";
import { formatUserTime, resolveUserTimeFormat, resolveUserTimezone } from "../date-time.js";
import { resolveModelAuthLabel } from "../model-auth-label.js";
import { loadModelCatalog } from "../model-catalog.js";
import {
buildAllowedModelSet,
@@ -449,7 +445,6 @@ export function createSessionStatusTool(opts?: {
}
}
const agentDir = resolveAgentDir(cfg, agentId);
const runtimeModelIdentity = resolveSessionModelIdentityRef(
cfg,
resolved.entry,
@@ -467,118 +462,54 @@ export function createSessionStatusTool(opts?: {
const defaultModelForCard = hasExplicitModelOverride
? configured.model
: runtimeModelForCard || configured.model;
// Preserve the "provider unknown" case for legacy runtime-only sessions so the
// status card does not synthesize a configured provider/model pair that never ran.
const statusSessionEntry =
!hasExplicitModelOverride && !runtimeProviderForCard && runtimeModelForCard
? { ...resolved.entry, providerOverride: "" }
: resolved.entry;
const providerOverrideForCard = statusSessionEntry.providerOverride?.trim();
const providerForCard = providerOverrideForCard ?? defaultProviderForCard;
const usageProvider = resolveUsageProviderId(providerForCard);
let usageLine: string | undefined;
if (usageProvider) {
try {
const usageSummary = await loadProviderUsageSummary({
timeoutMs: 3500,
providers: [usageProvider],
agentDir,
});
const snapshot = usageSummary.providers.find((entry) => entry.provider === usageProvider);
if (snapshot) {
const formatted = formatUsageWindowSummary(snapshot, {
now: Date.now(),
maxWindows: 2,
includeResets: true,
});
if (formatted && !formatted.startsWith("error:")) {
usageLine = `📊 Usage: ${formatted}`;
}
}
} catch {
// ignore
}
}
const primaryModelLabel =
providerForCard && defaultModelForCard
? `${providerForCard}/${defaultModelForCard}`
: defaultModelForCard;
const isGroup =
resolved.entry.chatType === "group" ||
resolved.entry.chatType === "channel" ||
statusSessionEntry.chatType === "group" ||
statusSessionEntry.chatType === "channel" ||
resolved.key.includes(":group:") ||
resolved.key.includes(":channel:");
const groupActivation = isGroup
? (normalizeGroupActivation(resolved.entry.groupActivation) ?? "mention")
: undefined;
const queueSettings = resolveQueueSettings({
cfg,
channel:
resolved.entry.channel ??
resolved.entry.lastChannel ??
resolved.entry.origin?.provider ??
"unknown",
sessionEntry: resolved.entry,
});
const queueKey = resolved.key ?? resolved.entry.sessionId;
const queueDepth = queueKey ? getFollowupQueueDepth(queueKey) : 0;
const queueOverrides = Boolean(
resolved.entry.queueDebounceMs ?? resolved.entry.queueCap ?? resolved.entry.queueDrop,
);
const userTimezone = resolveUserTimezone(cfg.agents?.defaults?.userTimezone);
const userTimeFormat = resolveUserTimeFormat(cfg.agents?.defaults?.timeFormat);
const userTime = formatUserTime(new Date(), userTimezone, userTimeFormat);
const timeLine = userTime
? `🕒 Time: ${userTime} (${userTimezone})`
: `🕒 Time zone: ${userTimezone}`;
const agentDefaults = cfg.agents?.defaults ?? {};
const agentConfig = resolveAgentConfig(cfg, agentId);
const defaultLabel = defaultProviderForCard
? `${defaultProviderForCard}/${defaultModelForCard}`
: defaultModelForCard;
const agentModel =
typeof agentDefaults.model === "object" && agentDefaults.model
? { ...agentDefaults.model, primary: defaultLabel }
: { primary: defaultLabel };
const statusText = buildStatusMessage({
config: cfg,
agent: {
...agentDefaults,
model: agentModel,
thinkingDefault: agentConfig?.thinkingDefault ?? agentDefaults.thinkingDefault,
},
agentId,
explicitConfiguredContextTokens:
typeof agentDefaults.contextTokens === "number" && agentDefaults.contextTokens > 0
? agentDefaults.contextTokens
: undefined,
sessionEntry: statusSessionEntry,
sessionKey: resolved.key,
sessionStorePath: storePath,
groupActivation,
modelAuth: resolveModelAuthLabel({
provider: providerForCard,
cfg,
sessionEntry: statusSessionEntry,
agentDir,
}),
usageLine,
timeLine,
queue: {
mode: queueSettings.mode,
depth: queueDepth,
debounceMs: queueSettings.debounceMs,
cap: queueSettings.cap,
dropPolicy: queueSettings.dropPolicy,
showDetails: queueOverrides,
},
includeTranscriptUsage: true,
});
const taskLine = formatSessionTaskLine({
relatedSessionKey: resolved.key,
callerOwnerKey: visibilityRequesterKey,
});
const fullStatusText = taskLine ? `${statusText}\n${taskLine}` : statusText;
const statusText = await buildStatusText({
cfg,
sessionEntry: statusSessionEntry,
sessionKey: resolved.key,
parentSessionKey: statusSessionEntry.parentSessionKey,
sessionScope: cfg.session?.scope,
storePath,
statusChannel:
statusSessionEntry.channel ??
statusSessionEntry.lastChannel ??
statusSessionEntry.origin?.provider ??
"unknown",
provider: providerForCard,
model: defaultModelForCard,
resolvedThinkLevel: statusSessionEntry.thinkingLevel as ThinkLevel | undefined,
resolvedFastMode: statusSessionEntry.fastMode,
resolvedVerboseLevel: (statusSessionEntry.verboseLevel ?? "off") as VerboseLevel,
resolvedReasoningLevel: (statusSessionEntry.reasoningLevel ?? "off") as ReasoningLevel,
resolvedElevatedLevel: statusSessionEntry.elevatedLevel as ElevatedLevel | undefined,
resolveDefaultThinkingLevel: async () => cfg.agents?.defaults?.thinkingDefault,
isGroup,
defaultGroupActivation: () => "mention",
taskLineOverride: taskLine,
skipDefaultTaskLookup: true,
primaryModelLabelOverride: primaryModelLabel,
...(providerForCard ? {} : { modelAuthOverride: undefined }),
});
const fullStatusText =
taskLine && !statusText.includes(taskLine) ? `${statusText}\n${taskLine}` : statusText;
return {
content: [{ type: "text", text: fullStatusText }],

View File

@@ -106,14 +106,54 @@ export async function buildStatusReply(params: {
defaultGroupActivation: () => "always" | "mention";
mediaDecisions?: MediaUnderstandingDecision[];
}): Promise<ReplyPayload | undefined> {
const { command } = params;
if (!command.isAuthorizedSender) {
logVerbose(`Ignoring /status from unauthorized sender: ${command.senderId || "<unknown>"}`);
return undefined;
}
return {
text: await buildStatusText({
...params,
statusChannel: command.channel,
}),
};
}
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<ThinkLevel | undefined>;
isGroup: boolean;
defaultGroupActivation: () => "always" | "mention";
mediaDecisions?: MediaUnderstandingDecision[];
taskLineOverride?: string;
skipDefaultTaskLookup?: boolean;
primaryModelLabelOverride?: string;
modelAuthOverride?: string;
activeModelAuthOverride?: string;
}): Promise<string> {
const {
cfg,
command,
sessionEntry,
sessionKey,
parentSessionKey,
sessionScope,
storePath,
statusChannel,
provider,
model,
contextTokens,
@@ -126,10 +166,6 @@ export async function buildStatusReply(params: {
isGroup,
defaultGroupActivation,
} = params;
if (!command.isAuthorizedSender) {
logVerbose(`Ignoring /status from unauthorized sender: ${command.senderId || "<unknown>"}`);
return undefined;
}
const statusAgentId = sessionKey
? resolveSessionAgentId({ sessionKey, config: cfg })
: resolveDefaultAgentId(cfg);
@@ -139,20 +175,24 @@ export async function buildStatusReply(params: {
selectedModel: model,
sessionEntry,
});
const selectedModelAuth = resolveModelAuthLabel({
provider,
cfg,
sessionEntry,
agentDir: statusAgentDir,
});
const activeModelAuth = modelRefs.activeDiffers
? resolveModelAuthLabel({
provider: modelRefs.active.provider,
const selectedModelAuth = Object.hasOwn(params, "modelAuthOverride")
? params.modelAuthOverride
: resolveModelAuthLabel({
provider,
cfg,
sessionEntry,
agentDir: statusAgentDir,
})
: selectedModelAuth;
});
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);
@@ -205,7 +245,7 @@ export async function buildStatusReply(params: {
}
const queueSettings = resolveQueueSettings({
cfg,
channel: command.channel,
channel: statusChannel,
sessionEntry,
});
const queueKey = sessionKey ?? sessionEntry?.sessionId;
@@ -219,8 +259,10 @@ export async function buildStatusReply(params: {
if (sessionKey) {
const { mainKey, alias } = resolveMainSessionAlias(cfg);
const requesterKey = resolveInternalSessionKey({ key: sessionKey, alias, mainKey });
taskLine = formatSessionTaskLine(requesterKey);
if (!taskLine) {
taskLine = params.skipDefaultTaskLookup
? params.taskLineOverride
: (params.taskLineOverride ?? formatSessionTaskLine(requesterKey));
if (!taskLine && !params.skipDefaultTaskLookup) {
taskLine = formatAgentTaskCountsLine(statusAgentId);
}
const runs = listControlledSubagentRuns(requesterKey);
@@ -262,9 +304,9 @@ export async function buildStatusReply(params: {
...agentDefaults,
model: {
...toAgentModelListLike(agentDefaults.model),
primary: `${provider}/${model}`,
primary: params.primaryModelLabelOverride ?? `${provider}/${model}`,
},
contextTokens,
...(typeof contextTokens === "number" && contextTokens > 0 ? { contextTokens } : {}),
thinkingDefault: agentConfig?.thinkingDefault ?? agentDefaults.thinkingDefault,
verboseDefault: agentDefaults.verboseDefault,
elevatedDefault: agentDefaults.elevatedDefault,
@@ -302,5 +344,5 @@ export async function buildStatusReply(params: {
includeTranscriptUsage: false,
});
return { text: statusText };
return statusText;
}