mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 14:30:45 +00:00
fix: extract shared session status runtime (#65807)
Merged via squash.
Prepared head SHA: f027bd640a
Co-authored-by: dutifulbob <261991368+dutifulbob@users.noreply.github.com>
Co-authored-by: osolmaz <2453968+osolmaz@users.noreply.github.com>
Reviewed-by: @osolmaz
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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",
|
||||
}));
|
||||
|
||||
@@ -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<string>;
|
||||
};
|
||||
|
||||
const COMMANDS_STATUS_RUNTIME_SPEC = [
|
||||
"../../auto-reply/reply/commands-status.runtime",
|
||||
".js",
|
||||
] as const;
|
||||
|
||||
let commandsStatusRuntimePromise: Promise<CommandsStatusRuntimeModule> | null = null;
|
||||
|
||||
function loadCommandsStatusRuntime(): Promise<CommandsStatusRuntimeModule> {
|
||||
commandsStatusRuntimePromise ??= importRuntimeModule<CommandsStatusRuntimeModule>(
|
||||
import.meta.url,
|
||||
COMMANDS_STATUS_RUNTIME_SPEC,
|
||||
);
|
||||
commandsStatusRuntimePromise ??=
|
||||
import("./session-status.runtime.js") as Promise<CommandsStatusRuntimeModule>;
|
||||
return commandsStatusRuntimePromise;
|
||||
}
|
||||
|
||||
|
||||
1
src/agents/tools/session-status.runtime.ts
Normal file
1
src/agents/tools/session-status.runtime.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { buildStatusText } from "../../status/status-text.js";
|
||||
@@ -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;
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export { buildStatusReply, buildStatusText } from "./commands-status.js";
|
||||
export { buildStatusReply } from "./commands-status.js";
|
||||
export { buildStatusText } from "../../status/status-text.js";
|
||||
|
||||
@@ -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, unknown>) => 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<StatusRuntimeModule> | null = null;
|
||||
let commandsStatusDepsRuntimePromise: Promise<CommandsStatusSubagentsModule> | null = null;
|
||||
|
||||
function loadStatusRuntime(): Promise<StatusRuntimeModule> {
|
||||
statusRuntimePromise ??= importRuntimeModule<StatusRuntimeModule>(
|
||||
import.meta.url,
|
||||
STATUS_RUNTIME_SPEC,
|
||||
);
|
||||
return statusRuntimePromise;
|
||||
}
|
||||
|
||||
function loadCommandsStatusDepsRuntime(): Promise<CommandsStatusSubagentsModule> {
|
||||
commandsStatusDepsRuntimePromise ??= importRuntimeModule<CommandsStatusSubagentsModule>(
|
||||
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<BuildStatusTextParams, "statusChannel"> & {
|
||||
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<ThinkLevel | undefined>;
|
||||
isGroup: boolean;
|
||||
defaultGroupActivation: () => "always" | "mention";
|
||||
mediaDecisions?: MediaUnderstandingDecision[];
|
||||
modelAuthOverride?: string;
|
||||
activeModelAuthOverride?: string;
|
||||
}): Promise<ReplyPayload | undefined> {
|
||||
};
|
||||
|
||||
export async function buildStatusReply(
|
||||
params: BuildStatusReplyParams,
|
||||
): Promise<ReplyPayload | undefined> {
|
||||
const { command } = params;
|
||||
if (!command.isAuthorizedSender) {
|
||||
logVerbose(`Ignoring /status from unauthorized sender: ${command.senderId || "<unknown>"}`);
|
||||
@@ -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<ThinkLevel | undefined>;
|
||||
isGroup: boolean;
|
||||
defaultGroupActivation: () => "always" | "mention";
|
||||
mediaDecisions?: MediaUnderstandingDecision[];
|
||||
taskLineOverride?: string;
|
||||
skipDefaultTaskLookup?: boolean;
|
||||
primaryModelLabelOverride?: string;
|
||||
modelAuthOverride?: string;
|
||||
activeModelAuthOverride?: string;
|
||||
includeTranscriptUsage?: boolean;
|
||||
}): Promise<string> {
|
||||
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<never>((_, 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;
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
25
src/status/fallback-notice-state.ts
Normal file
25
src/status/fallback-notice-state.ts
Normal file
@@ -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,
|
||||
};
|
||||
}
|
||||
3
src/status/status-message.runtime.ts
Normal file
3
src/status/status-message.runtime.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export async function loadStatusMessageRuntimeModule() {
|
||||
return await import("../auto-reply/status.runtime.js");
|
||||
}
|
||||
1
src/status/status-queue.runtime.ts
Normal file
1
src/status/status-queue.runtime.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { getFollowupQueueDepth, resolveQueueSettings } from "../auto-reply/reply/queue.js";
|
||||
3
src/status/status-subagents.runtime.ts
Normal file
3
src/status/status-subagents.runtime.ts
Normal file
@@ -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";
|
||||
335
src/status/status-text.ts
Normal file
335
src/status/status-text.ts
Normal file
@@ -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<ThinkLevel | undefined>;
|
||||
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<typeof import("../auto-reply/status.runtime.js")> | null =
|
||||
null;
|
||||
let statusQueueRuntimePromise: Promise<typeof import("./status-queue.runtime.js")> | null = null;
|
||||
let statusSubagentsRuntimePromise: Promise<typeof import("./status-subagents.runtime.js")> | null =
|
||||
null;
|
||||
|
||||
function loadStatusMessageRuntime(): Promise<typeof import("../auto-reply/status.runtime.js")> {
|
||||
const runtimePromise = (statusMessageRuntimePromise ??=
|
||||
import("./status-message.runtime.js").then((module) =>
|
||||
module.loadStatusMessageRuntimeModule(),
|
||||
));
|
||||
return runtimePromise;
|
||||
}
|
||||
|
||||
function loadStatusSubagentsRuntime(): Promise<typeof import("./status-subagents.runtime.js")> {
|
||||
const runtimePromise = (statusSubagentsRuntimePromise ??=
|
||||
import("./status-subagents.runtime.js"));
|
||||
return runtimePromise;
|
||||
}
|
||||
|
||||
function loadStatusQueueRuntime(): Promise<typeof import("./status-queue.runtime.js")> {
|
||||
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<string> {
|
||||
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<never>((_, 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,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user