Skip to content

Commit c874c08

Browse files
authored
fix(sessions): show runtime in sessions table
1 parent 1470b43 commit c874c08

4 files changed

Lines changed: 211 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ Docs: https://docs.openclaw.ai
77
### Changes
88

99
- 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.
10+
- 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.
1011
- 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.
1112
- 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.
1213
- 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.

src/commands/sessions.test.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
33
import {
44
makeRuntime,
55
mockSessionsConfig,
6+
resetMockSessionsConfig,
67
runSessionsJson,
8+
setMockSessionsConfig,
79
writeStore,
810
} from "./sessions.test-helpers.js";
911

@@ -21,6 +23,7 @@ describe("sessionsCommand", () => {
2123
});
2224

2325
afterEach(() => {
26+
resetMockSessionsConfig();
2427
vi.useRealTimers();
2528
});
2629

@@ -51,6 +54,75 @@ describe("sessionsCommand", () => {
5154
expect(row).toContain("pi:opus");
5255
});
5356

57+
it("renders the agent runtime in the tabular view", async () => {
58+
setMockSessionsConfig(() => ({
59+
agents: {
60+
defaults: {
61+
agentRuntime: { id: "claude-cli" },
62+
model: { primary: "anthropic/claude-opus-4-7" },
63+
models: { "anthropic/claude-opus-4-7": {} },
64+
contextTokens: 200_000,
65+
},
66+
},
67+
}));
68+
const store = writeStore(
69+
{
70+
"agent:main:main": {
71+
sessionId: "main-session",
72+
updatedAt: Date.now() - 60_000,
73+
modelProvider: "claude-cli",
74+
model: "claude-opus-4-7",
75+
},
76+
},
77+
"sessions-runtime-table",
78+
);
79+
80+
const { runtime, logs } = makeRuntime();
81+
await sessionsCommand({ store }, runtime);
82+
83+
fs.rmSync(store);
84+
85+
const tableHeader = logs.find((line) => line.includes("Runtime"));
86+
expect(tableHeader).toBeTruthy();
87+
88+
const row = logs.find((line) => line.includes("agent:main:main")) ?? "";
89+
expect(row).toContain("claude-opus-4-7");
90+
expect(row).toContain("Claude CLI");
91+
});
92+
93+
it("renders configured CLI runtime when the session stores a canonical provider", async () => {
94+
setMockSessionsConfig(() => ({
95+
agents: {
96+
defaults: {
97+
agentRuntime: { id: "claude-cli" },
98+
model: { primary: "anthropic/claude-opus-4-7" },
99+
models: { "anthropic/claude-opus-4-7": {} },
100+
contextTokens: 200_000,
101+
},
102+
},
103+
}));
104+
const store = writeStore(
105+
{
106+
"agent:main:main": {
107+
sessionId: "main-session",
108+
updatedAt: Date.now() - 60_000,
109+
modelProvider: "anthropic",
110+
model: "claude-opus-4-7",
111+
},
112+
},
113+
"sessions-runtime-canonical-provider",
114+
);
115+
116+
const { runtime, logs } = makeRuntime();
117+
await sessionsCommand({ store }, runtime);
118+
119+
fs.rmSync(store);
120+
121+
const row = logs.find((line) => line.includes("agent:main:main")) ?? "";
122+
expect(row).toContain("claude-opus-4-7");
123+
expect(row).toContain("Claude CLI");
124+
});
125+
54126
it("shows placeholder rows when tokens are missing", async () => {
55127
const store = writeStore({
56128
"quietchat:group:demo": {

src/commands/sessions.ts

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
import { resolveAgentRuntimeMetadata } from "../agents/agent-runtime-metadata.js";
22
import { DEFAULT_CONTEXT_TOKENS } from "../agents/defaults.js";
3+
import { selectAgentHarness } from "../agents/harness/selection.js";
34
import { getRuntimeConfig } from "../config/config.js";
45
import { loadSessionStore, resolveSessionTotalTokens } from "../config/sessions.js";
6+
import type { SessionEntry } from "../config/sessions/types.js";
7+
import type { OpenClawConfig } from "../config/types.openclaw.js";
58
import { info } from "../globals.js";
69
import { parseAgentSessionKey } from "../routing/session-key.js";
710
import { type RuntimeEnv, writeRuntimeJson } from "../runtime.js";
811
import { isCronSessionKey } from "../sessions/session-key-utils.js";
912
import { createLazyImportLoader } from "../shared/lazy-promise.js";
13+
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
14+
import { resolveAgentRuntimeLabel } from "../status/agent-runtime-label.js";
1015
import { isRich, theme } from "../terminal/theme.js";
1116
import { resolveSessionStoreTargetsOrExit } from "./session-store-targets.js";
1217
import {
@@ -30,10 +35,12 @@ type SessionRow = SessionDisplayRow & {
3035
agentId: string;
3136
kind: "cron" | "direct" | "group" | "global" | "unknown";
3237
agentRuntime: ReturnType<typeof resolveAgentRuntimeMetadata>;
38+
runtimeLabel: string;
3339
};
3440

3541
const AGENT_PAD = 10;
3642
const KIND_PAD = 6;
43+
const RUNTIME_PAD = 18;
3744
const TOKENS_PAD = 20;
3845
const DEFAULT_SESSIONS_LIMIT = 100;
3946
const TOP_N_SELECTION_LIMIT = 200;
@@ -162,6 +169,64 @@ const formatKindCell = (kind: SessionRow["kind"], rich: boolean) => {
162169
return theme.muted(label);
163170
};
164171

172+
function resolveSessionRuntimeLabel(params: {
173+
cfg: OpenClawConfig;
174+
entry: SessionEntry;
175+
agentRuntime: ReturnType<typeof resolveAgentRuntimeMetadata>;
176+
modelProvider: string;
177+
model: string;
178+
agentId: string;
179+
sessionKey: string;
180+
}): string {
181+
const explicitRuntime =
182+
normalizeOptionalLowercaseString(params.entry.agentRuntimeOverride) ??
183+
normalizeOptionalLowercaseString(params.entry.agentHarnessId) ??
184+
(params.agentRuntime.source === "implicit"
185+
? undefined
186+
: normalizeOptionalLowercaseString(params.agentRuntime.id));
187+
if (explicitRuntime && explicitRuntime !== "auto" && explicitRuntime !== "default") {
188+
return resolveAgentRuntimeLabel({
189+
config: params.cfg,
190+
sessionEntry: params.entry,
191+
resolvedHarness: explicitRuntime,
192+
fallbackProvider: params.modelProvider,
193+
});
194+
}
195+
196+
let resolvedHarness: string | undefined;
197+
try {
198+
const selected = selectAgentHarness({
199+
provider: params.modelProvider,
200+
modelId: params.model,
201+
config: params.cfg,
202+
agentId: params.agentId,
203+
sessionKey: params.sessionKey,
204+
agentHarnessId: params.entry.agentHarnessId,
205+
});
206+
const id = normalizeOptionalLowercaseString(selected.id);
207+
resolvedHarness = id && id !== "pi" ? id : undefined;
208+
} catch {
209+
resolvedHarness = undefined;
210+
}
211+
return resolveAgentRuntimeLabel({
212+
config: params.cfg,
213+
sessionEntry: params.entry,
214+
resolvedHarness,
215+
fallbackProvider: params.modelProvider,
216+
});
217+
}
218+
219+
function formatRuntimeCell(runtimeLabel: string, rich: boolean): string {
220+
const label = runtimeLabel.padEnd(RUNTIME_PAD);
221+
return rich ? theme.info(label) : label;
222+
}
223+
224+
function toJsonSessionRow(row: SessionRow): Omit<SessionRow, "runtimeLabel"> {
225+
const { runtimeLabel, ...jsonRow } = row;
226+
void runtimeLabel;
227+
return jsonRow;
228+
}
229+
165230
export async function sessionsCommand(
166231
opts: {
167232
json?: boolean;
@@ -225,10 +290,21 @@ export async function sessionsCommand(
225290
.map(([key, entry]) => {
226291
const row = toSessionDisplayRow(key, entry);
227292
const agentId = parseAgentSessionKey(row.key)?.agentId ?? target.agentId;
293+
const modelRef = resolveSessionDisplayModelRef(cfg, row);
294+
const agentRuntime = resolveAgentRuntimeMetadata(cfg, agentId);
228295
return Object.assign({}, row, {
229296
agentId,
230-
agentRuntime: resolveAgentRuntimeMetadata(cfg, agentId),
297+
agentRuntime,
231298
kind: classifySessionKey(row.key, store[row.key]),
299+
runtimeLabel: resolveSessionRuntimeLabel({
300+
cfg,
301+
entry,
302+
agentRuntime,
303+
modelProvider: modelRef.provider,
304+
model: modelRef.model,
305+
agentId,
306+
sessionKey: row.key,
307+
}),
232308
});
233309
});
234310
});
@@ -254,7 +330,8 @@ export async function sessionsCommand(
254330
hasMore,
255331
activeMinutes: activeMinutes ?? null,
256332
sessions: await Promise.all(
257-
rows.map(async (r) => {
333+
rows.map(async (row) => {
334+
const r = toJsonSessionRow(row);
258335
const modelRef = resolveSessionDisplayModelRef(cfg, r);
259336
return {
260337
...r,
@@ -306,6 +383,7 @@ export async function sessionsCommand(
306383
"Key".padEnd(SESSION_KEY_PAD),
307384
"Age".padEnd(SESSION_AGE_PAD),
308385
"Model".padEnd(SESSION_MODEL_PAD),
386+
"Runtime".padEnd(RUNTIME_PAD),
309387
"Tokens (ctx %)".padEnd(TOKENS_PAD),
310388
"Flags",
311389
].join(" ");
@@ -329,6 +407,7 @@ export async function sessionsCommand(
329407
formatSessionKeyCell(row.key, rich),
330408
formatSessionAgeCell(row.updatedAt, rich),
331409
formatSessionModelCell(model, rich),
410+
formatRuntimeCell(row.runtimeLabel, rich),
332411
formatTokensCell(total, contextTokens ?? null, rich),
333412
formatSessionFlagsCell(row, rich),
334413
].join(" ");

src/status/agent-runtime-label.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { isCliProvider } from "../agents/model-selection.js";
2+
import type { SessionEntry } from "../config/sessions/types.js";
3+
import type { OpenClawConfig } from "../config/types.openclaw.js";
4+
import {
5+
normalizeOptionalLowercaseString,
6+
normalizeOptionalString,
7+
} from "../shared/string-coerce.js";
8+
import { sanitizeTerminalText } from "../terminal/safe-text.js";
9+
10+
const AGENT_RUNTIME_LABELS: Readonly<Record<string, string>> = {
11+
pi: "OpenClaw Pi Default",
12+
codex: "OpenAI Codex",
13+
"codex-cli": "OpenAI Codex",
14+
"claude-cli": "Claude CLI",
15+
"google-gemini-cli": "Gemini CLI",
16+
};
17+
18+
export function resolveAgentRuntimeLabel(args: {
19+
config?: OpenClawConfig;
20+
sessionEntry?: Pick<
21+
SessionEntry,
22+
"acp" | "agentRuntimeOverride" | "agentHarnessId" | "modelProvider" | "providerOverride"
23+
>;
24+
resolvedHarness?: string;
25+
fallbackProvider?: string;
26+
}): string {
27+
const acpAgentRaw = normalizeOptionalString(args.sessionEntry?.acp?.agent);
28+
const acpAgent = acpAgentRaw ? sanitizeTerminalText(acpAgentRaw) : undefined;
29+
if (acpAgent) {
30+
const backendRaw = normalizeOptionalString(args.sessionEntry?.acp?.backend);
31+
const backend = backendRaw ? sanitizeTerminalText(backendRaw) : undefined;
32+
return backend ? `${acpAgent} (acp/${backend})` : `${acpAgent} (acp)`;
33+
}
34+
35+
const runtimeRaw =
36+
normalizeOptionalString(args.resolvedHarness) ??
37+
normalizeOptionalString(args.sessionEntry?.agentRuntimeOverride) ??
38+
normalizeOptionalString(args.sessionEntry?.agentHarnessId);
39+
const runtime = normalizeOptionalLowercaseString(runtimeRaw);
40+
if (runtime && runtime !== "auto" && runtime !== "default") {
41+
return AGENT_RUNTIME_LABELS[runtime] ?? sanitizeTerminalText(runtimeRaw ?? runtime);
42+
}
43+
44+
const providerRaw =
45+
normalizeOptionalString(args.sessionEntry?.modelProvider) ??
46+
normalizeOptionalString(args.sessionEntry?.providerOverride) ??
47+
normalizeOptionalString(args.fallbackProvider);
48+
const provider = providerRaw ? sanitizeTerminalText(providerRaw) : undefined;
49+
if (provider && isCliProvider(provider, args.config)) {
50+
return (
51+
AGENT_RUNTIME_LABELS[normalizeOptionalLowercaseString(providerRaw) ?? ""] ??
52+
`${provider} (cli)`
53+
);
54+
}
55+
56+
return AGENT_RUNTIME_LABELS.pi;
57+
}

0 commit comments

Comments
 (0)