Skip to content

Commit a792068

Browse files
Eva (agent)obviyus
authored andcommitted
fix: harden release stability diagnostics
1 parent a51ee5b commit a792068

9 files changed

Lines changed: 285 additions & 16 deletions

src/commands/models/list.status-command.ts

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -402,12 +402,14 @@ export async function modelsStatusCommand(
402402
const syntheticProvidersToProbe = new Set(
403403
providers.map((provider) => normalizeProviderId(provider)),
404404
);
405+
const codexRuntimeAuthProviders = new Set<string>();
405406
for (const usage of providerUses) {
406407
if (
407408
usage.allowCodexRuntimeFallback &&
408409
openAIProviderUsesCodexRuntimeByDefault({ provider: usage.provider, config: cfg })
409410
) {
410411
const codexProvider = normalizeProviderId(OPENAI_CODEX_PROVIDER_ID);
412+
codexRuntimeAuthProviders.add(codexProvider);
411413
syntheticProvidersToProbe.add(codexProvider);
412414
syntheticProvidersToProbe.add(aliasMap[codexProvider] ?? codexProvider);
413415
syntheticProvidersToProbe.add("codex");
@@ -453,7 +455,15 @@ export async function modelsStatusCommand(
453455
const shellFallbackEnabled =
454456
shouldEnableShellEnvFallback(process.env) || cfg.env?.shellEnv?.enabled === true;
455457

456-
const providerAuth = providers
458+
const providerAuthProviders = new Set(providers);
459+
for (const provider of codexRuntimeAuthProviders) {
460+
if (syntheticAuthByProvider.has(provider)) {
461+
providerAuthProviders.add(provider);
462+
}
463+
}
464+
465+
const providerAuth = Array.from(providerAuthProviders)
466+
.toSorted((a, b) => a.localeCompare(b))
457467
.map((provider) =>
458468
resolveProviderAuthOverview({
459469
provider,
@@ -523,6 +533,33 @@ export async function modelsStatusCommand(
523533
hasUsableProviderAuth(OPENAI_CODEX_PROVIDER_ID)
524534
);
525535
};
536+
const runtimeAuthRoutes = Array.from(
537+
new Map(
538+
providerUses
539+
.filter(
540+
(usage) =>
541+
usage.allowCodexRuntimeFallback &&
542+
openAIProviderUsesCodexRuntimeByDefault({ provider: usage.provider, config: cfg }),
543+
)
544+
.map((usage) => {
545+
const authProvider = normalizeProviderId(OPENAI_CODEX_PROVIDER_ID);
546+
const effective = providerAuthMap.get(authProvider)?.effective ?? {
547+
kind: "missing" as const,
548+
detail: "missing",
549+
};
550+
return [
551+
`${usage.provider}:codex:${authProvider}`,
552+
{
553+
provider: usage.provider,
554+
runtime: "codex",
555+
authProvider,
556+
status: hasUsableProviderAuth(authProvider) ? "usable" : "missing",
557+
effective,
558+
},
559+
] as const;
560+
}),
561+
).values(),
562+
).toSorted((a, b) => a.provider.localeCompare(b.provider));
526563
const missingProvidersInUse = Array.from(
527564
new Set(
528565
providerUses
@@ -730,6 +767,7 @@ export async function modelsStatusCommand(
730767
},
731768
providersWithOAuth: providersWithOauth,
732769
missingProvidersInUse,
770+
runtimeAuthRoutes,
733771
providers: providerAuth,
734772
unusableProfiles,
735773
oauth: {
@@ -915,6 +953,27 @@ export async function modelsStatusCommand(
915953
runtime.log(`- ${theme.heading(entry.provider)} ${bits.join(separator)}`);
916954
}
917955

956+
if (runtimeAuthRoutes.length > 0) {
957+
runtime.log("");
958+
runtime.log(colorize(rich, theme.heading, "Runtime auth"));
959+
for (const route of runtimeAuthRoutes) {
960+
runtime.log(
961+
`- ${theme.heading(route.provider)} via ${colorize(
962+
rich,
963+
theme.accentBright,
964+
route.runtime,
965+
)} uses ${theme.heading(route.authProvider)} ${formatKeyValue(
966+
"effective",
967+
`${colorize(rich, theme.accentBright, route.effective.kind)}:${colorize(
968+
rich,
969+
theme.muted,
970+
route.effective.detail,
971+
)}`,
972+
)}${formatSeparator()}${formatKeyValue("status", route.status)}`,
973+
);
974+
}
975+
}
976+
918977
if (missingProvidersInUse.length > 0) {
919978
const { buildProviderAuthRecoveryHint } = await import("../provider-auth-guidance.js");
920979
runtime.log("");

src/commands/models/list.status.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -558,6 +558,18 @@ describe("modelsStatusCommand auth overview", () => {
558558
.slice(syntheticProbeStart)
559559
.map(([arg]) => (arg as { provider: string }).provider);
560560
expect(payload.auth.missingProvidersInUse).toStrictEqual([]);
561+
expect(payload.auth.runtimeAuthRoutes).toEqual([
562+
{
563+
provider: "openai",
564+
runtime: "codex",
565+
authProvider: "openai-codex",
566+
status: "usable",
567+
effective: {
568+
kind: "synthetic",
569+
detail: "codex-app-server",
570+
},
571+
},
572+
]);
561573
expect(localRuntime.exit).not.toHaveBeenCalledWith(1);
562574
expect(syntheticProbeProviders).toContain("codex");
563575
} finally {

src/commands/sessions-table.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export type SessionDisplayRow = {
2525
providerOverride?: string;
2626
modelOverride?: string;
2727
contextTokens?: number;
28+
runtimePolicySessionKey?: string;
2829
};
2930

3031
export const SESSION_KEY_PAD = 26;
@@ -102,6 +103,7 @@ export function formatSessionFlagsCell(
102103
| "systemSent"
103104
| "abortedLastRun"
104105
| "sessionId"
106+
| "runtimePolicySessionKey"
105107
>,
106108
rich: boolean,
107109
): string {
@@ -115,6 +117,7 @@ export function formatSessionFlagsCell(
115117
row.groupActivation ? `activation:${row.groupActivation}` : null,
116118
row.systemSent ? "system" : null,
117119
row.abortedLastRun ? "aborted" : null,
120+
row.runtimePolicySessionKey ? `policy:${row.runtimePolicySessionKey}` : null,
118121
row.sessionId ? `id:${row.sessionId}` : null,
119122
].filter(Boolean);
120123
const label = flags.join(" ");

src/commands/sessions.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,34 @@ describe("sessionsCommand", () => {
228228
expect(payload.sessions?.map((row) => row.key)).toEqual(["recent"]);
229229
});
230230

231+
it("exports runtime policy aliases for collapsed external direct sessions", async () => {
232+
const store = writeStore(
233+
{
234+
"agent:main:main": {
235+
sessionId: "telegram-main",
236+
updatedAt: Date.now() - 60_000,
237+
origin: {
238+
provider: "telegram",
239+
chatType: "direct",
240+
to: "telegram:42",
241+
accountId: "default",
242+
},
243+
},
244+
},
245+
"sessions-runtime-policy-alias",
246+
);
247+
248+
const payload = await runSessionsJson<{
249+
sessions?: Array<{
250+
key: string;
251+
runtimePolicySessionKey?: string;
252+
}>;
253+
}>(sessionsCommand, store, { active: "10" });
254+
255+
const main = payload.sessions?.find((row) => row.key === "agent:main:main");
256+
expect(main?.runtimePolicySessionKey).toBe("agent:main:telegram:default:direct:42");
257+
});
258+
231259
it("uses a default JSON output limit of 100 sessions", () => {
232260
expect(__testing.parseSessionsLimit(undefined)).toBe(100);
233261
});

src/commands/sessions.ts

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { resolveModelAgentRuntimeMetadata } from "../agents/agent-runtime-metadata.js";
22
import { DEFAULT_CONTEXT_TOKENS } from "../agents/defaults.js";
3+
import { resolveRuntimePolicySessionKey } from "../auto-reply/reply/runtime-policy-session-key.js";
4+
import { normalizeChatType } from "../channels/chat-type.js";
35
import { getRuntimeConfig } from "../config/config.js";
46
import { loadSessionStore, resolveSessionTotalTokens } from "../config/sessions.js";
57
import type { SessionEntry } from "../config/sessions/types.js";
@@ -10,7 +12,10 @@ import { type RuntimeEnv, writeRuntimeJson } from "../runtime.js";
1012
import { classifySessionKind, type SessionKind } from "../sessions/classify-session-kind.js";
1113
import { isAcpSessionKey } from "../sessions/session-key-utils.js";
1214
import { createLazyImportLoader } from "../shared/lazy-promise.js";
13-
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
15+
import {
16+
normalizeOptionalLowercaseString,
17+
normalizeOptionalString,
18+
} from "../shared/string-coerce.js";
1419
import { resolveAgentRuntimeLabel } from "../status/agent-runtime-label.js";
1520
import { isRich, theme } from "../terminal/theme.js";
1621
import { resolveSessionStoreTargetsOrExit } from "./session-store-targets.js";
@@ -220,6 +225,76 @@ function toJsonSessionRow(row: SessionRow): Omit<SessionRow, "runtimeLabel"> {
220225
return jsonRow;
221226
}
222227

228+
function stripChannelRecipientPrefix(
229+
value: string | undefined,
230+
channel: string | undefined,
231+
): string | undefined {
232+
const raw = normalizeOptionalString(value);
233+
const normalizedChannel = normalizeOptionalLowercaseString(channel);
234+
if (!raw || !normalizedChannel) {
235+
return raw;
236+
}
237+
const prefix = `${normalizedChannel}:`;
238+
if (!raw.toLowerCase().startsWith(prefix)) {
239+
return raw;
240+
}
241+
const stripped = raw.slice(prefix.length);
242+
const topicMarkerIndex = stripped.toLowerCase().indexOf(":topic:");
243+
return topicMarkerIndex >= 0 ? stripped.slice(0, topicMarkerIndex) : stripped;
244+
}
245+
246+
function resolveDisplayRuntimePolicySessionKey(params: {
247+
cfg: OpenClawConfig;
248+
key: string;
249+
entry: SessionEntry;
250+
}): string | undefined {
251+
const { cfg, entry, key } = params;
252+
const origin = entry.origin;
253+
const deliveryContext = entry.deliveryContext;
254+
const chatType = normalizeChatType(origin?.chatType ?? entry.chatType);
255+
if (chatType !== "direct") {
256+
return undefined;
257+
}
258+
259+
const channel = normalizeOptionalString(
260+
origin?.provider ??
261+
deliveryContext?.channel ??
262+
entry.lastChannel ??
263+
entry.channel ??
264+
origin?.surface,
265+
);
266+
const to = normalizeOptionalString(origin?.to ?? deliveryContext?.to ?? entry.lastTo);
267+
const from = normalizeOptionalString(origin?.from);
268+
const nativeDirectUserId = normalizeOptionalString(origin?.nativeDirectUserId);
269+
const peerId =
270+
nativeDirectUserId ??
271+
stripChannelRecipientPrefix(to, channel) ??
272+
stripChannelRecipientPrefix(from, channel);
273+
274+
const runtimePolicySessionKey = resolveRuntimePolicySessionKey({
275+
cfg,
276+
sessionKey: key,
277+
ctx: {
278+
SessionKey: key,
279+
Provider: channel,
280+
Surface: normalizeOptionalString(origin?.surface),
281+
AccountId: normalizeOptionalString(
282+
origin?.accountId ?? deliveryContext?.accountId ?? entry.lastAccountId,
283+
),
284+
ChatType: chatType,
285+
NativeDirectUserId: nativeDirectUserId,
286+
SenderId: peerId,
287+
OriginatingTo: to,
288+
From: from,
289+
To: to,
290+
},
291+
});
292+
293+
return runtimePolicySessionKey && runtimePolicySessionKey !== key
294+
? runtimePolicySessionKey
295+
: undefined;
296+
}
297+
223298
export async function sessionsCommand(
224299
opts: {
225300
json?: boolean;
@@ -303,6 +378,11 @@ export async function sessionsCommand(
303378
acpRuntime,
304379
agentRuntime,
305380
kind: classifySessionKind(row.key, store[row.key]),
381+
runtimePolicySessionKey: resolveDisplayRuntimePolicySessionKey({
382+
cfg,
383+
key: row.key,
384+
entry,
385+
}),
306386
runtimeLabel: resolveSessionRuntimeLabel({
307387
cfg,
308388
entry,

src/logging/diagnostic-session-attention.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,22 @@ describe("classifySessionAttention", () => {
5555
recoveryEligible: false,
5656
},
5757
},
58+
{
59+
name: "queued behind terminal embedded progress",
60+
queueDepth: 1,
61+
activity: {
62+
activeWorkKind: "embedded_run" as const,
63+
lastProgressAgeMs: 100,
64+
lastProgressReason: "codex_app_server:notification:rawResponseItem/completed",
65+
},
66+
expected: {
67+
eventType: "session.stalled",
68+
reason: "queued_behind_terminal_active_work",
69+
classification: "stalled_agent_run",
70+
activeWorkKind: "embedded_run",
71+
recoveryEligible: false,
72+
},
73+
},
5874
{
5975
name: "active work without progress",
6076
queueDepth: 0,

src/logging/diagnostic-session-attention.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,19 @@ export function classifySessionAttention(params: {
4343
recoveryEligible: false,
4444
};
4545
}
46+
if (
47+
params.queueDepth > 0 &&
48+
params.activity.activeWorkKind === "embedded_run" &&
49+
isTerminalDiagnosticProgressReason(params.activity.lastProgressReason)
50+
) {
51+
return {
52+
eventType: "session.stalled",
53+
reason: "queued_behind_terminal_active_work",
54+
classification: "stalled_agent_run",
55+
activeWorkKind: params.activity.activeWorkKind,
56+
recoveryEligible: false,
57+
};
58+
}
4659
if ((params.activity.lastProgressAgeMs ?? 0) > params.staleMs) {
4760
return {
4861
eventType: "session.stalled",
@@ -68,3 +81,17 @@ export function classifySessionAttention(params: {
6881
recoveryEligible: true,
6982
};
7083
}
84+
85+
export function isTerminalDiagnosticProgressReason(reason: string | undefined): boolean {
86+
if (!reason) {
87+
return false;
88+
}
89+
return (
90+
reason === "run:completed" ||
91+
reason === "embedded_run:ended" ||
92+
reason.includes("response.completed") ||
93+
reason.includes("rawResponseItem/completed") ||
94+
reason.includes("raw_response_item.completed") ||
95+
reason.includes("output_item.done")
96+
);
97+
}

0 commit comments

Comments
 (0)