diff --git a/CHANGELOG.md b/CHANGELOG.md index 222f10196c0..46e22429a03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Docs: https://docs.openclaw.ai ### Changes - PR triage: mark external pull requests with `proof: supplied` when Barnacle finds structured real behavior proof, keep stale negative proof labels in sync across CRLF-edited PR bodies, and let ClawSweeper own the stronger `proof: sufficient` judgement. +- Sessions CLI: show the selected agent runtime in the `openclaw sessions` table so terminal output matches the runtime visibility already present in JSON/status surfaces. Thanks @vincentkoc. - Google Meet/Voice Call: make Twilio dial-in joins speak through the realtime Gemini voice bridge with paced audio streaming, backpressure-aware buffering, barge-in queue clearing, same-session agent consult routing, duplicate-consult coalescing, and no TwiML fallback during realtime speech, giving Meet participants a much snappier OpenClaw voice agent. (#77064) Thanks @scoootscooob. - Voice Call/realtime: add opt-in OpenClaw agent voice context capsules and consult-cadence guidance so Gemini/OpenAI realtime calls can sound like the configured agent without consulting the full agent on every ordinary turn. Thanks @scoootscooob. - Docker/Gateway: harden the gateway container by dropping `NET_RAW` and `NET_ADMIN` capabilities and enabling `no-new-privileges` in the bundled `docker-compose.yml`. Thanks @VintageAyu. diff --git a/src/commands/sessions.test.ts b/src/commands/sessions.test.ts index 52c6d287ff0..b94ab42ea05 100644 --- a/src/commands/sessions.test.ts +++ b/src/commands/sessions.test.ts @@ -3,7 +3,9 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { makeRuntime, mockSessionsConfig, + resetMockSessionsConfig, runSessionsJson, + setMockSessionsConfig, writeStore, } from "./sessions.test-helpers.js"; @@ -21,6 +23,7 @@ describe("sessionsCommand", () => { }); afterEach(() => { + resetMockSessionsConfig(); vi.useRealTimers(); }); @@ -51,6 +54,75 @@ describe("sessionsCommand", () => { expect(row).toContain("pi:opus"); }); + it("renders the agent runtime in the tabular view", async () => { + setMockSessionsConfig(() => ({ + agents: { + defaults: { + agentRuntime: { id: "claude-cli" }, + model: { primary: "anthropic/claude-opus-4-7" }, + models: { "anthropic/claude-opus-4-7": {} }, + contextTokens: 200_000, + }, + }, + })); + const store = writeStore( + { + "agent:main:main": { + sessionId: "main-session", + updatedAt: Date.now() - 60_000, + modelProvider: "claude-cli", + model: "claude-opus-4-7", + }, + }, + "sessions-runtime-table", + ); + + const { runtime, logs } = makeRuntime(); + await sessionsCommand({ store }, runtime); + + fs.rmSync(store); + + const tableHeader = logs.find((line) => line.includes("Runtime")); + expect(tableHeader).toBeTruthy(); + + const row = logs.find((line) => line.includes("agent:main:main")) ?? ""; + expect(row).toContain("claude-opus-4-7"); + expect(row).toContain("Claude CLI"); + }); + + it("renders configured CLI runtime when the session stores a canonical provider", async () => { + setMockSessionsConfig(() => ({ + agents: { + defaults: { + agentRuntime: { id: "claude-cli" }, + model: { primary: "anthropic/claude-opus-4-7" }, + models: { "anthropic/claude-opus-4-7": {} }, + contextTokens: 200_000, + }, + }, + })); + const store = writeStore( + { + "agent:main:main": { + sessionId: "main-session", + updatedAt: Date.now() - 60_000, + modelProvider: "anthropic", + model: "claude-opus-4-7", + }, + }, + "sessions-runtime-canonical-provider", + ); + + const { runtime, logs } = makeRuntime(); + await sessionsCommand({ store }, runtime); + + fs.rmSync(store); + + const row = logs.find((line) => line.includes("agent:main:main")) ?? ""; + expect(row).toContain("claude-opus-4-7"); + expect(row).toContain("Claude CLI"); + }); + it("shows placeholder rows when tokens are missing", async () => { const store = writeStore({ "quietchat:group:demo": { diff --git a/src/commands/sessions.ts b/src/commands/sessions.ts index 96c41d418e3..0c9f829c7cd 100644 --- a/src/commands/sessions.ts +++ b/src/commands/sessions.ts @@ -1,12 +1,17 @@ import { resolveAgentRuntimeMetadata } from "../agents/agent-runtime-metadata.js"; import { DEFAULT_CONTEXT_TOKENS } from "../agents/defaults.js"; +import { selectAgentHarness } from "../agents/harness/selection.js"; import { getRuntimeConfig } from "../config/config.js"; import { loadSessionStore, resolveSessionTotalTokens } from "../config/sessions.js"; +import type { SessionEntry } from "../config/sessions/types.js"; +import type { OpenClawConfig } from "../config/types.openclaw.js"; import { info } from "../globals.js"; import { parseAgentSessionKey } from "../routing/session-key.js"; import { type RuntimeEnv, writeRuntimeJson } from "../runtime.js"; import { isCronSessionKey } from "../sessions/session-key-utils.js"; import { createLazyImportLoader } from "../shared/lazy-promise.js"; +import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js"; +import { resolveAgentRuntimeLabel } from "../status/agent-runtime-label.js"; import { isRich, theme } from "../terminal/theme.js"; import { resolveSessionStoreTargetsOrExit } from "./session-store-targets.js"; import { @@ -30,10 +35,12 @@ type SessionRow = SessionDisplayRow & { agentId: string; kind: "cron" | "direct" | "group" | "global" | "unknown"; agentRuntime: ReturnType; + runtimeLabel: string; }; const AGENT_PAD = 10; const KIND_PAD = 6; +const RUNTIME_PAD = 18; const TOKENS_PAD = 20; const DEFAULT_SESSIONS_LIMIT = 100; const TOP_N_SELECTION_LIMIT = 200; @@ -162,6 +169,64 @@ const formatKindCell = (kind: SessionRow["kind"], rich: boolean) => { return theme.muted(label); }; +function resolveSessionRuntimeLabel(params: { + cfg: OpenClawConfig; + entry: SessionEntry; + agentRuntime: ReturnType; + modelProvider: string; + model: string; + agentId: string; + sessionKey: string; +}): string { + const explicitRuntime = + normalizeOptionalLowercaseString(params.entry.agentRuntimeOverride) ?? + normalizeOptionalLowercaseString(params.entry.agentHarnessId) ?? + (params.agentRuntime.source === "implicit" + ? undefined + : normalizeOptionalLowercaseString(params.agentRuntime.id)); + if (explicitRuntime && explicitRuntime !== "auto" && explicitRuntime !== "default") { + return resolveAgentRuntimeLabel({ + config: params.cfg, + sessionEntry: params.entry, + resolvedHarness: explicitRuntime, + fallbackProvider: params.modelProvider, + }); + } + + let resolvedHarness: string | undefined; + try { + const selected = selectAgentHarness({ + provider: params.modelProvider, + modelId: params.model, + config: params.cfg, + agentId: params.agentId, + sessionKey: params.sessionKey, + agentHarnessId: params.entry.agentHarnessId, + }); + const id = normalizeOptionalLowercaseString(selected.id); + resolvedHarness = id && id !== "pi" ? id : undefined; + } catch { + resolvedHarness = undefined; + } + return resolveAgentRuntimeLabel({ + config: params.cfg, + sessionEntry: params.entry, + resolvedHarness, + fallbackProvider: params.modelProvider, + }); +} + +function formatRuntimeCell(runtimeLabel: string, rich: boolean): string { + const label = runtimeLabel.padEnd(RUNTIME_PAD); + return rich ? theme.info(label) : label; +} + +function toJsonSessionRow(row: SessionRow): Omit { + const { runtimeLabel, ...jsonRow } = row; + void runtimeLabel; + return jsonRow; +} + export async function sessionsCommand( opts: { json?: boolean; @@ -225,10 +290,21 @@ export async function sessionsCommand( .map(([key, entry]) => { const row = toSessionDisplayRow(key, entry); const agentId = parseAgentSessionKey(row.key)?.agentId ?? target.agentId; + const modelRef = resolveSessionDisplayModelRef(cfg, row); + const agentRuntime = resolveAgentRuntimeMetadata(cfg, agentId); return Object.assign({}, row, { agentId, - agentRuntime: resolveAgentRuntimeMetadata(cfg, agentId), + agentRuntime, kind: classifySessionKey(row.key, store[row.key]), + runtimeLabel: resolveSessionRuntimeLabel({ + cfg, + entry, + agentRuntime, + modelProvider: modelRef.provider, + model: modelRef.model, + agentId, + sessionKey: row.key, + }), }); }); }); @@ -254,7 +330,8 @@ export async function sessionsCommand( hasMore, activeMinutes: activeMinutes ?? null, sessions: await Promise.all( - rows.map(async (r) => { + rows.map(async (row) => { + const r = toJsonSessionRow(row); const modelRef = resolveSessionDisplayModelRef(cfg, r); return { ...r, @@ -306,6 +383,7 @@ export async function sessionsCommand( "Key".padEnd(SESSION_KEY_PAD), "Age".padEnd(SESSION_AGE_PAD), "Model".padEnd(SESSION_MODEL_PAD), + "Runtime".padEnd(RUNTIME_PAD), "Tokens (ctx %)".padEnd(TOKENS_PAD), "Flags", ].join(" "); @@ -329,6 +407,7 @@ export async function sessionsCommand( formatSessionKeyCell(row.key, rich), formatSessionAgeCell(row.updatedAt, rich), formatSessionModelCell(model, rich), + formatRuntimeCell(row.runtimeLabel, rich), formatTokensCell(total, contextTokens ?? null, rich), formatSessionFlagsCell(row, rich), ].join(" "); diff --git a/src/status/agent-runtime-label.ts b/src/status/agent-runtime-label.ts new file mode 100644 index 00000000000..ef35fa744d5 --- /dev/null +++ b/src/status/agent-runtime-label.ts @@ -0,0 +1,57 @@ +import { isCliProvider } from "../agents/model-selection.js"; +import type { SessionEntry } from "../config/sessions/types.js"; +import type { OpenClawConfig } from "../config/types.openclaw.js"; +import { + normalizeOptionalLowercaseString, + normalizeOptionalString, +} from "../shared/string-coerce.js"; +import { sanitizeTerminalText } from "../terminal/safe-text.js"; + +const AGENT_RUNTIME_LABELS: Readonly> = { + pi: "OpenClaw Pi Default", + codex: "OpenAI Codex", + "codex-cli": "OpenAI Codex", + "claude-cli": "Claude CLI", + "google-gemini-cli": "Gemini CLI", +}; + +export function resolveAgentRuntimeLabel(args: { + config?: OpenClawConfig; + sessionEntry?: Pick< + SessionEntry, + "acp" | "agentRuntimeOverride" | "agentHarnessId" | "modelProvider" | "providerOverride" + >; + resolvedHarness?: string; + 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 runtimeRaw = + normalizeOptionalString(args.resolvedHarness) ?? + normalizeOptionalString(args.sessionEntry?.agentRuntimeOverride) ?? + normalizeOptionalString(args.sessionEntry?.agentHarnessId); + const runtime = normalizeOptionalLowercaseString(runtimeRaw); + if (runtime && runtime !== "auto" && runtime !== "default") { + return AGENT_RUNTIME_LABELS[runtime] ?? sanitizeTerminalText(runtimeRaw ?? runtime); + } + + 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 ( + AGENT_RUNTIME_LABELS[normalizeOptionalLowercaseString(providerRaw) ?? ""] ?? + `${provider} (cli)` + ); + } + + return AGENT_RUNTIME_LABELS.pi; +}