fix(sessions): show runtime in sessions table

This commit is contained in:
Vincent Koc
2026-05-05 16:16:15 -07:00
committed by GitHub
parent 1470b439e2
commit c874c0863a
4 changed files with 211 additions and 2 deletions

View File

@@ -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.

View File

@@ -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": {

View File

@@ -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<typeof resolveAgentRuntimeMetadata>;
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<typeof resolveAgentRuntimeMetadata>;
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<SessionRow, "runtimeLabel"> {
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(" ");

View File

@@ -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<Record<string, string>> = {
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;
}