fix(gateway): align concurrent sessions.list fallback metadata
This commit is contained in:
parent
3cc89eabdd
commit
8b29ab5edb
@ -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");
|
||||
|
||||
@ -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({
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user