diff --git a/CHANGELOG.md b/CHANGELOG.md index c00142c6909..fecff10b421 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ Docs: https://docs.openclaw.ai - Codex harness/plugins: add a bundled-plugin Codex app-server extension seam for async `tool_result` middleware, fire `after_tool_call` for Codex tool runs, and route mirrored Codex transcript writes through `before_message_write` so tool integrations stop diverging from Pi. Thanks @vincentkoc. - Codex harness/hooks: fire `llm_input`, `llm_output`, and `agent_end` for native Codex app-server turns so lifecycle hooks stop drifting from Pi. Thanks @vincentkoc. - QA/Telegram: record per-scenario reply RTT in the live Telegram QA report and summary, starting with the canary response. (#70550) Thanks @obviyus. +- Status: add an explicit `Runner:` field to `/status` so sessions now report whether they are running on embedded Pi, a CLI-backed provider, or an ACP harness agent/backend such as `codex (acp/acpx)` or `gemini (acp/acpx)`. (#70595) ### Fixes diff --git a/src/auto-reply/status.test.ts b/src/auto-reply/status.test.ts index 332591ce155..c68da05ec00 100644 --- a/src/auto-reply/status.test.ts +++ b/src/auto-reply/status.test.ts @@ -95,12 +95,115 @@ describe("buildStatusMessage", () => { expect(normalized).toContain("Session: agent:main:main"); expect(normalized).toContain("updated 10m ago"); expect(normalized).toContain("Runtime: direct"); + expect(normalized).toContain("Runner: pi (embedded)"); expect(normalized).toContain("Think: medium"); expect(normalized).not.toContain("verbose"); expect(normalized).toContain("elevated"); expect(normalized).toContain("Queue: collect"); }); + it("shows the CLI runner for CLI-backed providers", () => { + const text = buildStatusMessage({ + config: { + agents: { + defaults: { + cliBackends: { + "claude-cli": {}, + }, + }, + }, + } as unknown as OpenClawConfig, + agent: { + model: "claude-cli/opus", + }, + sessionEntry: { + sessionId: "cli", + updatedAt: 0, + modelProvider: "claude-cli", + model: "opus", + }, + sessionKey: "agent:main:main", + queue: { mode: "collect", depth: 0 }, + }); + + expect(normalizeTestText(text)).toContain("Runner: claude-cli (cli)"); + }); + + it("falls back to the configured CLI provider when session provider fields are empty", () => { + const text = buildStatusMessage({ + config: { + agents: { + defaults: { + cliBackends: { + "claude-cli": {}, + }, + }, + }, + } as unknown as OpenClawConfig, + agent: { + model: "claude-cli/opus", + }, + sessionEntry: { + sessionId: "cli-default", + updatedAt: 0, + }, + sessionKey: "agent:main:main", + queue: { mode: "collect", depth: 0 }, + }); + + expect(normalizeTestText(text)).toContain("Runner: claude-cli (cli)"); + }); + + it("shows the ACP harness agent and backend when ACP owns the session", () => { + const text = buildStatusMessage({ + agent: { + model: "anthropic/claude-opus-4-6", + }, + sessionEntry: { + sessionId: "acp", + updatedAt: 0, + acp: { + backend: "acpx", + agent: "gemini", + runtimeSessionName: "status-test", + mode: "persistent", + state: "idle", + lastActivityAt: 0, + }, + }, + sessionKey: "agent:main:main", + queue: { mode: "collect", depth: 0 }, + }); + + expect(normalizeTestText(text)).toContain("Runner: gemini (acp/acpx)"); + }); + + it("sanitizes runner labels sourced from session metadata", () => { + const text = buildStatusMessage({ + agent: { + model: "anthropic/claude-opus-4-6", + }, + sessionEntry: { + sessionId: "acp-sanitized", + updatedAt: 0, + acp: { + backend: "acpx\nrewritten", + agent: "gemini\u001b[2K", + runtimeSessionName: "status-test", + mode: "persistent", + state: "idle", + lastActivityAt: 0, + }, + }, + sessionKey: "agent:main:main", + queue: { mode: "collect", depth: 0 }, + }); + + const normalized = normalizeTestText(text); + expect(normalized).toContain("Runner: gemini (acp/acpx\\nrewritten)"); + expect(normalized).not.toContain("\u001b"); + }); + it("falls back to sessionEntry levels when resolved levels are not passed", () => { const text = buildStatusMessage({ agent: { diff --git a/src/status/status-message.ts b/src/status/status-message.ts index a2f679e4a11..e3fbc1f4983 100644 --- a/src/status/status-message.ts +++ b/src/status/status-message.ts @@ -4,6 +4,7 @@ import { DEFAULT_CONTEXT_TOKENS, DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agen import { resolveModelAuthMode } from "../agents/model-auth.js"; import { buildModelAliasIndex, + isCliProvider, resolveConfiguredModelRef, resolveModelRefFromString, } from "../agents/model-selection.js"; @@ -45,6 +46,7 @@ import { normalizeOptionalLowercaseString, normalizeOptionalString, } from "../shared/string-coerce.js"; +import { sanitizeTerminalText } from "../terminal/safe-text.js"; import { resolveStatusTtsSnapshot } from "../tts/status-config.js"; import { estimateUsageCost, @@ -193,6 +195,29 @@ function resolveRuntimeLabel( return `${runtime}/${sandboxMode}`; } +function resolveRunnerLabel( + args: Pick & { fallbackProvider?: string }, +): string { + const acpAgentRaw = normalizeOptionalString(args.sessionEntry?.acp?.agent); + const acpAgent = acpAgentRaw ? sanitizeTerminalText(acpAgentRaw) : undefined; + if (acpAgent) { + const backendRaw = normalizeOptionalString(args.sessionEntry?.acp?.backend); + const backend = backendRaw ? sanitizeTerminalText(backendRaw) : undefined; + return backend ? `${acpAgent} (acp/${backend})` : `${acpAgent} (acp)`; + } + + const providerRaw = + normalizeOptionalString(args.sessionEntry?.modelProvider) ?? + normalizeOptionalString(args.sessionEntry?.providerOverride) ?? + normalizeOptionalString(args.fallbackProvider); + const provider = providerRaw ? sanitizeTerminalText(providerRaw) : undefined; + if (provider && isCliProvider(provider, args.config)) { + return `${provider} (cli)`; + } + + return "pi (embedded)"; +} + const formatTokens = (total: number | null | undefined, contextTokens: number | null) => { const ctx = contextTokens ?? null; if (total == null) { @@ -658,6 +683,11 @@ export function buildStatusMessage(args: StatusArgs): string { "on"; const runtime = { label: resolveRuntimeLabel(args) }; + const runnerLabel = resolveRunnerLabel({ + config: args.config, + sessionEntry: args.sessionEntry, + fallbackProvider: activeProvider, + }); const updatedAt = entry?.updatedAt; const sessionLine = [ @@ -711,6 +741,7 @@ export function buildStatusMessage(args: StatusArgs): string { }); const optionParts = [ `Runtime: ${runtime.label}`, + `Runner: ${runnerLabel}`, `Think: ${thinkLevel}`, formatFastModeLabel(fastMode), textVerbosity ? `Text: ${textVerbosity}` : null,