fix(gateway): align concurrent sessions.list fallback metadata

This commit is contained in:
user 2026-03-21 10:56:55 +09:00
parent 3cc89eabdd
commit 8b29ab5edb
2 changed files with 131 additions and 44 deletions

View File

@ -1166,6 +1166,89 @@ describe("listSessionsFromStore search", () => {
}
});
test("listSessionsFromStoreAsync uses subagent run model for child session transcript fallback", async () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-session-utils-async-subagent-"));
const storePath = path.join(tmpDir, "sessions.json");
const now = Date.now();
const cfg = {
gateway: {
sessionsList: {
fallbackConcurrency: 4,
},
},
session: { mainKey: "main" },
agents: {
list: [{ id: "main", default: true }],
defaults: {
models: {
"anthropic/claude-sonnet-4-6": { params: { context1m: true } },
},
},
},
} as unknown as OpenClawConfig;
fs.writeFileSync(
path.join(tmpDir, "sess-child.jsonl"),
[
JSON.stringify({ type: "session", version: 1, id: "sess-child" }),
JSON.stringify({
message: {
role: "assistant",
usage: {
input: 2_000,
output: 500,
cacheRead: 1_200,
cost: { total: 0.007725 },
},
},
}),
].join("\n"),
"utf-8",
);
addSubagentRunForTests({
runId: "run-child-async",
childSessionKey: "agent:main:subagent:child-async",
controllerSessionKey: "agent:main:main",
requesterSessionKey: "agent:main:main",
requesterDisplayKey: "main",
task: "child task",
cleanup: "keep",
createdAt: now - 5_000,
startedAt: now - 4_000,
model: "anthropic/claude-sonnet-4-6",
});
try {
const result = await listSessionsFromStoreAsync({
cfg,
storePath,
store: {
"agent:main:subagent:child-async": {
sessionId: "sess-child",
updatedAt: now,
spawnedBy: "agent:main:main",
totalTokens: 0,
totalTokensFresh: false,
} as SessionEntry,
},
opts: {},
});
expect(result.sessions[0]).toMatchObject({
key: "agent:main:subagent:child-async",
status: "running",
modelProvider: "anthropic",
model: "claude-sonnet-4-6",
totalTokens: 3_200,
totalTokensFresh: true,
contextTokens: 1_048_576,
});
expect(result.sessions[0]?.estimatedCostUsd).toBeCloseTo(0.007725, 8);
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});
test("listSessionsFromStoreAsync hydrates transcript fallbacks when concurrency is enabled", async () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-session-utils-async-"));
const storePath = path.join(tmpDir, "sessions.json");

View File

@ -49,13 +49,13 @@ import {
import { normalizeSessionDeliveryFields } from "../utils/delivery-context.js";
import { runTasksWithConcurrency } from "../utils/run-with-concurrency.js";
import { estimateUsageCost, resolveModelCostConfig } from "../utils/usage-format.js";
import type { SessionsListParams } from "./protocol/index.js";
import {
readLatestSessionUsageFromTranscript,
readLatestSessionUsageFromTranscriptAsync,
readSessionTitleFieldsFromTranscript,
type SessionTranscriptUsageSnapshot,
} from "./session-utils.fs.js";
import type { SessionsListParams } from "./protocol/index.js";
import type {
GatewayAgentRow,
GatewaySessionRow,
@ -424,7 +424,13 @@ function resolveSessionEntryModelIdentity(params: {
const sessionAgentId = normalizeAgentId(
parseAgentSessionKey(params.key)?.agentId ?? resolveDefaultAgentId(params.cfg),
);
const resolved = resolveSessionModelIdentityRef(params.cfg, params.entry, sessionAgentId);
const subagentRun = getSubagentRunByChildSessionKey(params.key);
const resolved = resolveSessionModelIdentityRef(
params.cfg,
params.entry,
sessionAgentId,
subagentRun?.model,
);
return {
provider: resolved.provider,
model: resolved.model ?? DEFAULT_MODEL,
@ -1422,19 +1428,18 @@ export function listSessionsFromStore(params: {
const { cfg, storePath, store, opts } = params;
const normalizedOpts = normalizeSessionsListOptions(opts);
let sessions = filterSessionListEntries(store, normalizedOpts)
.map(([key, entry]) =>
buildGatewaySessionRow({
cfg,
storePath,
store,
key,
entry,
now: normalizedOpts.now,
includeDerivedTitles: normalizedOpts.includeDerivedTitles,
includeLastMessage: normalizedOpts.includeLastMessage,
}),
);
let sessions = filterSessionListEntries(store, normalizedOpts).map(([key, entry]) =>
buildGatewaySessionRow({
cfg,
storePath,
store,
key,
entry,
now: normalizedOpts.now,
includeDerivedTitles: normalizedOpts.includeDerivedTitles,
includeLastMessage: normalizedOpts.includeLastMessage,
}),
);
sessions = finalizeSessionListRows(sessions, normalizedOpts);
return {
@ -1460,36 +1465,35 @@ export async function listSessionsFromStoreAsync(params: {
const normalizedOpts = normalizeSessionsListOptions(params.opts);
const filteredEntries = filterSessionListEntries(params.store, normalizedOpts);
const transcriptUsageByKey = new Map<string, GatewayTranscriptUsageFallback | null>();
const tasks = filteredEntries
.filter(([key, entry]) => {
const resolvedModel = resolveSessionEntryModelIdentity({
cfg: params.cfg,
key,
entry,
});
return shouldResolveTranscriptUsageFallback({
cfg: params.cfg,
entry,
fallbackProvider: resolvedModel.provider,
fallbackModel: resolvedModel.model,
});
})
.map(([key, entry]) => async () => {
const resolvedModel = resolveSessionEntryModelIdentity({
cfg: params.cfg,
key,
entry,
});
const usage = await resolveTranscriptUsageFallbackAsync({
cfg: params.cfg,
key,
entry,
storePath: params.storePath,
fallbackProvider: resolvedModel.provider,
fallbackModel: resolvedModel.model,
});
return { key, usage };
const entriesNeedingFallback = filteredEntries.flatMap(([key, entry]) => {
const resolvedModel = resolveSessionEntryModelIdentity({
cfg: params.cfg,
key,
entry,
});
if (
!shouldResolveTranscriptUsageFallback({
cfg: params.cfg,
entry,
fallbackProvider: resolvedModel.provider,
fallbackModel: resolvedModel.model,
})
) {
return [];
}
return [{ key, entry, resolvedModel }];
});
const tasks = entriesNeedingFallback.map(({ key, entry, resolvedModel }) => async () => {
const usage = await resolveTranscriptUsageFallbackAsync({
cfg: params.cfg,
key,
entry,
storePath: params.storePath,
fallbackProvider: resolvedModel.provider,
fallbackModel: resolvedModel.model,
});
return { key, usage };
});
if (tasks.length > 0) {
const { results } = await runTasksWithConcurrency({