mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-05 22:50:22 +00:00
Session management improvements and dashboard API (#50101)
* fix: make cleanup "keep" persist subagent sessions indefinitely * feat: expose subagent session metadata in sessions list * fix: include status and timing in sessions_list tool * fix: hide injected timestamp prefixes in chat ui * feat: push session list updates over websocket * feat: expose child subagent sessions in subagents list * feat: add admin http endpoint to kill sessions * Emit session.message websocket events for transcript updates * Estimate session costs in sessions list * Add direct session history HTTP and SSE endpoints * Harden dashboard session events and history APIs * Add session lifecycle gateway methods * Add dashboard session API improvements * Add dashboard session model and parent linkage support * fix: tighten dashboard session API metadata * Fix dashboard session cost metadata * Persist accumulated session cost * fix: stop followup queue drain cfg crash * Fix dashboard session create and model metadata * fix: stop guessing session model costs * Gateway: cache OpenRouter pricing for configured models * Gateway: add timeout session status * Fix subagent spawn test config loading * Gateway: preserve operator scopes without device identity * Emit user message transcript events and deduplicate plugin warnings * feat: emit sessions.changed lifecycle event on subagent spawn Adds a session-lifecycle-events module (similar to transcript-events) that emits create events when subagents are spawned. The gateway server.impl.ts listens for these events and broadcasts sessions.changed with reason=create to SSE subscribers, so dashboards can pick up new subagent sessions without polling. * Gateway: allow persistent dashboard orchestrator sessions * fix: preserve operator scopes for token-authenticated backend clients Backend clients (like agent-dashboard) that authenticate with a valid gateway token but don't present a device identity were getting their scopes stripped. The scope-clearing logic ran before checking the device identity decision, so even when evaluateMissingDeviceIdentity returned 'allow' (because roleCanSkipDeviceIdentity passed for token-authed operators), scopes were already cleared. Fix: also check decision.kind before clearing scopes, so token-authenticated operators keep their requested scopes. * Gateway: allow operator-token session kills * Fix stale active subagent status after follow-up runs * Fix dashboard image attachments in sessions send * Fix completed session follow-up status updates * feat: stream session tool events to operator UIs * Add sessions.steer gateway coverage * Persist subagent timing in session store * Fix subagent session transcript event keys * Fix active subagent session status in gateway * bump session label max to 512 * Fix gateway send session reactivation * fix: publish terminal session lifecycle state * feat: change default session reset to effectively never - Change DEFAULT_RESET_MODE from "daily" to "idle" - Change DEFAULT_IDLE_MINUTES from 60 to 0 (0 = disabled/never) - Allow idleMinutes=0 through normalization (don't clamp to 1) - Treat idleMinutes=0 as "no idle expiry" in evaluateSessionFreshness - Default behavior: mode "idle" + idleMinutes 0 = sessions never auto-reset - Update test assertion for new default mode * fix: prep session management followups (#50101) (thanks @clay-datacurve) --------- Co-authored-by: Tyler Yust <TYTYYUST@YAHOO.COM>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js";
|
||||
import { lookupContextTokens } from "../agents/context.js";
|
||||
import { lookupContextTokens, resolveContextTokensForModel } from "../agents/context.js";
|
||||
import { DEFAULT_CONTEXT_TOKENS, DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js";
|
||||
import {
|
||||
inferUniqueProviderFromConfiguredModels,
|
||||
@@ -9,6 +9,13 @@ import {
|
||||
resolveConfiguredModelRef,
|
||||
resolveDefaultModelForAgent,
|
||||
} from "../agents/model-selection.js";
|
||||
import {
|
||||
getSubagentRunByChildSessionKey,
|
||||
getSubagentSessionRuntimeMs,
|
||||
getSubagentSessionStartedAt,
|
||||
listSubagentRunsForController,
|
||||
resolveSubagentSessionStatus,
|
||||
} from "../agents/subagent-registry.js";
|
||||
import { type OpenClawConfig, loadConfig } from "../config/config.js";
|
||||
import { resolveStateDir } from "../config/paths.js";
|
||||
import {
|
||||
@@ -40,7 +47,11 @@ import {
|
||||
resolveAvatarMime,
|
||||
} from "../shared/avatar-policy.js";
|
||||
import { normalizeSessionDeliveryFields } from "../utils/delivery-context.js";
|
||||
import { readSessionTitleFieldsFromTranscript } from "./session-utils.fs.js";
|
||||
import { estimateUsageCost, resolveModelCostConfig } from "../utils/usage-format.js";
|
||||
import {
|
||||
readLatestSessionUsageFromTranscript,
|
||||
readSessionTitleFieldsFromTranscript,
|
||||
} from "./session-utils.fs.js";
|
||||
import type {
|
||||
GatewayAgentRow,
|
||||
GatewaySessionRow,
|
||||
@@ -51,9 +62,11 @@ import type {
|
||||
export {
|
||||
archiveFileOnDisk,
|
||||
archiveSessionTranscripts,
|
||||
attachOpenClawTranscriptMeta,
|
||||
capArrayByJsonBytes,
|
||||
readFirstUserMessageFromTranscript,
|
||||
readLastMessagePreviewFromTranscript,
|
||||
readLatestSessionUsageFromTranscript,
|
||||
readSessionTitleFieldsFromTranscript,
|
||||
readSessionPreviewItemsFromTranscript,
|
||||
readSessionMessages,
|
||||
@@ -177,6 +190,149 @@ export function deriveSessionTitle(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function resolveSessionRuntimeMs(
|
||||
run: { startedAt?: number; endedAt?: number; accumulatedRuntimeMs?: number } | null,
|
||||
now: number,
|
||||
) {
|
||||
return getSubagentSessionRuntimeMs(run, now);
|
||||
}
|
||||
|
||||
function resolvePositiveNumber(value: number | null | undefined): number | undefined {
|
||||
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined;
|
||||
}
|
||||
|
||||
function resolveNonNegativeNumber(value: number | null | undefined): number | undefined {
|
||||
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
|
||||
}
|
||||
|
||||
function resolveEstimatedSessionCostUsd(params: {
|
||||
cfg: OpenClawConfig;
|
||||
provider?: string;
|
||||
model?: string;
|
||||
entry?: Pick<
|
||||
SessionEntry,
|
||||
"estimatedCostUsd" | "inputTokens" | "outputTokens" | "cacheRead" | "cacheWrite"
|
||||
>;
|
||||
explicitCostUsd?: number;
|
||||
}): number | undefined {
|
||||
const explicitCostUsd = resolveNonNegativeNumber(
|
||||
params.explicitCostUsd ?? params.entry?.estimatedCostUsd,
|
||||
);
|
||||
if (explicitCostUsd !== undefined) {
|
||||
return explicitCostUsd;
|
||||
}
|
||||
const input = resolvePositiveNumber(params.entry?.inputTokens);
|
||||
const output = resolvePositiveNumber(params.entry?.outputTokens);
|
||||
const cacheRead = resolvePositiveNumber(params.entry?.cacheRead);
|
||||
const cacheWrite = resolvePositiveNumber(params.entry?.cacheWrite);
|
||||
if (
|
||||
input === undefined &&
|
||||
output === undefined &&
|
||||
cacheRead === undefined &&
|
||||
cacheWrite === undefined
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
const cost = resolveModelCostConfig({
|
||||
provider: params.provider,
|
||||
model: params.model,
|
||||
config: params.cfg,
|
||||
});
|
||||
if (!cost) {
|
||||
return undefined;
|
||||
}
|
||||
const estimated = estimateUsageCost({
|
||||
usage: {
|
||||
...(input !== undefined ? { input } : {}),
|
||||
...(output !== undefined ? { output } : {}),
|
||||
...(cacheRead !== undefined ? { cacheRead } : {}),
|
||||
...(cacheWrite !== undefined ? { cacheWrite } : {}),
|
||||
},
|
||||
cost,
|
||||
});
|
||||
return resolveNonNegativeNumber(estimated);
|
||||
}
|
||||
|
||||
function resolveChildSessionKeys(
|
||||
controllerSessionKey: string,
|
||||
store: Record<string, SessionEntry>,
|
||||
): string[] | undefined {
|
||||
const childSessionKeys = new Set(
|
||||
listSubagentRunsForController(controllerSessionKey)
|
||||
.map((entry) => entry.childSessionKey)
|
||||
.filter((value) => typeof value === "string" && value.trim().length > 0),
|
||||
);
|
||||
for (const [key, entry] of Object.entries(store)) {
|
||||
if (!entry || key === controllerSessionKey) {
|
||||
continue;
|
||||
}
|
||||
const spawnedBy = entry.spawnedBy?.trim();
|
||||
const parentSessionKey = entry.parentSessionKey?.trim();
|
||||
if (spawnedBy === controllerSessionKey || parentSessionKey === controllerSessionKey) {
|
||||
childSessionKeys.add(key);
|
||||
}
|
||||
}
|
||||
const childSessions = Array.from(childSessionKeys);
|
||||
return childSessions.length > 0 ? childSessions : undefined;
|
||||
}
|
||||
|
||||
function resolveTranscriptUsageFallback(params: {
|
||||
cfg: OpenClawConfig;
|
||||
key: string;
|
||||
entry?: SessionEntry;
|
||||
storePath: string;
|
||||
fallbackProvider?: string;
|
||||
fallbackModel?: string;
|
||||
}): {
|
||||
estimatedCostUsd?: number;
|
||||
totalTokens?: number;
|
||||
totalTokensFresh?: boolean;
|
||||
contextTokens?: number;
|
||||
} | null {
|
||||
const entry = params.entry;
|
||||
if (!entry?.sessionId) {
|
||||
return null;
|
||||
}
|
||||
const parsed = parseAgentSessionKey(params.key);
|
||||
const agentId = parsed?.agentId
|
||||
? normalizeAgentId(parsed.agentId)
|
||||
: resolveDefaultAgentId(params.cfg);
|
||||
const snapshot = readLatestSessionUsageFromTranscript(
|
||||
entry.sessionId,
|
||||
params.storePath,
|
||||
entry.sessionFile,
|
||||
agentId,
|
||||
);
|
||||
if (!snapshot) {
|
||||
return null;
|
||||
}
|
||||
const modelProvider = snapshot.modelProvider ?? params.fallbackProvider;
|
||||
const model = snapshot.model ?? params.fallbackModel;
|
||||
const contextTokens = resolveContextTokensForModel({
|
||||
cfg: params.cfg,
|
||||
provider: modelProvider,
|
||||
model,
|
||||
});
|
||||
const estimatedCostUsd = resolveEstimatedSessionCostUsd({
|
||||
cfg: params.cfg,
|
||||
provider: modelProvider,
|
||||
model,
|
||||
explicitCostUsd: snapshot.costUsd,
|
||||
entry: {
|
||||
inputTokens: snapshot.inputTokens,
|
||||
outputTokens: snapshot.outputTokens,
|
||||
cacheRead: snapshot.cacheRead,
|
||||
cacheWrite: snapshot.cacheWrite,
|
||||
},
|
||||
});
|
||||
return {
|
||||
totalTokens: resolvePositiveNumber(snapshot.totalTokens),
|
||||
totalTokensFresh: snapshot.totalTokensFresh === true,
|
||||
contextTokens: resolvePositiveNumber(contextTokens),
|
||||
estimatedCostUsd,
|
||||
};
|
||||
}
|
||||
|
||||
export function loadSessionEntry(sessionKey: string) {
|
||||
const cfg = loadConfig();
|
||||
const canonicalKey = resolveSessionStoreKey({ cfg, sessionKey });
|
||||
@@ -816,6 +972,7 @@ export function resolveSessionModelIdentityRef(
|
||||
| SessionEntry
|
||||
| Pick<SessionEntry, "model" | "modelProvider" | "modelOverride" | "providerOverride">,
|
||||
agentId?: string,
|
||||
fallbackModelRef?: string,
|
||||
): { provider?: string; model: string } {
|
||||
const runtimeModel = entry?.model?.trim();
|
||||
const runtimeProvider = entry?.modelProvider?.trim();
|
||||
@@ -839,10 +996,202 @@ export function resolveSessionModelIdentityRef(
|
||||
}
|
||||
return { model: runtimeModel };
|
||||
}
|
||||
const fallbackRef = fallbackModelRef?.trim();
|
||||
if (fallbackRef) {
|
||||
const parsedFallback = parseModelRef(fallbackRef, DEFAULT_PROVIDER);
|
||||
if (parsedFallback) {
|
||||
return { provider: parsedFallback.provider, model: parsedFallback.model };
|
||||
}
|
||||
const inferredProvider = inferUniqueProviderFromConfiguredModels({
|
||||
cfg,
|
||||
model: fallbackRef,
|
||||
});
|
||||
if (inferredProvider) {
|
||||
return { provider: inferredProvider, model: fallbackRef };
|
||||
}
|
||||
return { model: fallbackRef };
|
||||
}
|
||||
const resolved = resolveSessionModelRef(cfg, entry, agentId);
|
||||
return { provider: resolved.provider, model: resolved.model };
|
||||
}
|
||||
|
||||
export function buildGatewaySessionRow(params: {
|
||||
cfg: OpenClawConfig;
|
||||
storePath: string;
|
||||
store: Record<string, SessionEntry>;
|
||||
key: string;
|
||||
entry?: SessionEntry;
|
||||
now?: number;
|
||||
includeDerivedTitles?: boolean;
|
||||
includeLastMessage?: boolean;
|
||||
}): GatewaySessionRow {
|
||||
const { cfg, storePath, store, key, entry } = params;
|
||||
const now = params.now ?? Date.now();
|
||||
const updatedAt = entry?.updatedAt ?? null;
|
||||
const parsed = parseGroupKey(key);
|
||||
const channel = entry?.channel ?? parsed?.channel;
|
||||
const subject = entry?.subject;
|
||||
const groupChannel = entry?.groupChannel;
|
||||
const space = entry?.space;
|
||||
const id = parsed?.id;
|
||||
const origin = entry?.origin;
|
||||
const originLabel = origin?.label;
|
||||
const displayName =
|
||||
entry?.displayName ??
|
||||
(channel
|
||||
? buildGroupDisplayName({
|
||||
provider: channel,
|
||||
subject,
|
||||
groupChannel,
|
||||
space,
|
||||
id,
|
||||
key,
|
||||
})
|
||||
: undefined) ??
|
||||
entry?.label ??
|
||||
originLabel;
|
||||
const deliveryFields = normalizeSessionDeliveryFields(entry);
|
||||
const parsedAgent = parseAgentSessionKey(key);
|
||||
const sessionAgentId = normalizeAgentId(parsedAgent?.agentId ?? resolveDefaultAgentId(cfg));
|
||||
const subagentRun = getSubagentRunByChildSessionKey(key);
|
||||
const subagentStatus = subagentRun ? resolveSubagentSessionStatus(subagentRun) : undefined;
|
||||
const subagentStartedAt = subagentRun ? getSubagentSessionStartedAt(subagentRun) : undefined;
|
||||
const subagentEndedAt = subagentRun ? subagentRun.endedAt : undefined;
|
||||
const subagentRuntimeMs = subagentRun ? resolveSessionRuntimeMs(subagentRun, now) : undefined;
|
||||
const resolvedModel = resolveSessionModelIdentityRef(
|
||||
cfg,
|
||||
entry,
|
||||
sessionAgentId,
|
||||
subagentRun?.model,
|
||||
);
|
||||
const modelProvider = resolvedModel.provider;
|
||||
const model = resolvedModel.model ?? DEFAULT_MODEL;
|
||||
const transcriptUsage =
|
||||
resolvePositiveNumber(resolveFreshSessionTotalTokens(entry)) === undefined ||
|
||||
resolvePositiveNumber(entry?.contextTokens) === undefined ||
|
||||
resolveEstimatedSessionCostUsd({
|
||||
cfg,
|
||||
provider: modelProvider,
|
||||
model,
|
||||
entry,
|
||||
}) === undefined
|
||||
? resolveTranscriptUsageFallback({
|
||||
cfg,
|
||||
key,
|
||||
entry,
|
||||
storePath,
|
||||
fallbackProvider: modelProvider,
|
||||
fallbackModel: model,
|
||||
})
|
||||
: null;
|
||||
const totalTokens =
|
||||
resolvePositiveNumber(resolveFreshSessionTotalTokens(entry)) ??
|
||||
resolvePositiveNumber(transcriptUsage?.totalTokens);
|
||||
const totalTokensFresh =
|
||||
typeof totalTokens === "number" && Number.isFinite(totalTokens) && totalTokens > 0
|
||||
? true
|
||||
: transcriptUsage?.totalTokensFresh === true;
|
||||
const childSessions = resolveChildSessionKeys(key, store);
|
||||
const estimatedCostUsd =
|
||||
resolveEstimatedSessionCostUsd({
|
||||
cfg,
|
||||
provider: modelProvider,
|
||||
model,
|
||||
entry,
|
||||
}) ?? resolveNonNegativeNumber(transcriptUsage?.estimatedCostUsd);
|
||||
const contextTokens =
|
||||
resolvePositiveNumber(entry?.contextTokens) ??
|
||||
resolvePositiveNumber(transcriptUsage?.contextTokens) ??
|
||||
resolvePositiveNumber(
|
||||
resolveContextTokensForModel({
|
||||
cfg,
|
||||
provider: modelProvider,
|
||||
model,
|
||||
}),
|
||||
);
|
||||
|
||||
let derivedTitle: string | undefined;
|
||||
let lastMessagePreview: string | undefined;
|
||||
if (entry?.sessionId && (params.includeDerivedTitles || params.includeLastMessage)) {
|
||||
const fields = readSessionTitleFieldsFromTranscript(
|
||||
entry.sessionId,
|
||||
storePath,
|
||||
entry.sessionFile,
|
||||
sessionAgentId,
|
||||
);
|
||||
if (params.includeDerivedTitles) {
|
||||
derivedTitle = deriveSessionTitle(entry, fields.firstUserMessage);
|
||||
}
|
||||
if (params.includeLastMessage && fields.lastMessagePreview) {
|
||||
lastMessagePreview = fields.lastMessagePreview;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
key,
|
||||
spawnedBy: entry?.spawnedBy,
|
||||
kind: classifySessionKey(key, entry),
|
||||
label: entry?.label,
|
||||
displayName,
|
||||
derivedTitle,
|
||||
lastMessagePreview,
|
||||
channel,
|
||||
subject,
|
||||
groupChannel,
|
||||
space,
|
||||
chatType: entry?.chatType,
|
||||
origin,
|
||||
updatedAt,
|
||||
sessionId: entry?.sessionId,
|
||||
systemSent: entry?.systemSent,
|
||||
abortedLastRun: entry?.abortedLastRun,
|
||||
thinkingLevel: entry?.thinkingLevel,
|
||||
verboseLevel: entry?.verboseLevel,
|
||||
reasoningLevel: entry?.reasoningLevel,
|
||||
elevatedLevel: entry?.elevatedLevel,
|
||||
sendPolicy: entry?.sendPolicy,
|
||||
inputTokens: entry?.inputTokens,
|
||||
outputTokens: entry?.outputTokens,
|
||||
totalTokens,
|
||||
totalTokensFresh,
|
||||
estimatedCostUsd,
|
||||
status: subagentRun ? subagentStatus : entry?.status,
|
||||
startedAt: subagentRun ? subagentStartedAt : entry?.startedAt,
|
||||
endedAt: subagentRun ? subagentEndedAt : entry?.endedAt,
|
||||
runtimeMs: subagentRun ? subagentRuntimeMs : entry?.runtimeMs,
|
||||
parentSessionKey: entry?.parentSessionKey,
|
||||
childSessions,
|
||||
responseUsage: entry?.responseUsage,
|
||||
modelProvider,
|
||||
model,
|
||||
contextTokens,
|
||||
deliveryContext: deliveryFields.deliveryContext,
|
||||
lastChannel: deliveryFields.lastChannel ?? entry?.lastChannel,
|
||||
lastTo: deliveryFields.lastTo ?? entry?.lastTo,
|
||||
lastAccountId: deliveryFields.lastAccountId ?? entry?.lastAccountId,
|
||||
};
|
||||
}
|
||||
|
||||
export function loadGatewaySessionRow(
|
||||
sessionKey: string,
|
||||
options?: { includeDerivedTitles?: boolean; includeLastMessage?: boolean; now?: number },
|
||||
): GatewaySessionRow | null {
|
||||
const { cfg, storePath, store, entry, canonicalKey } = loadSessionEntry(sessionKey);
|
||||
if (!entry) {
|
||||
return null;
|
||||
}
|
||||
return buildGatewaySessionRow({
|
||||
cfg,
|
||||
storePath,
|
||||
store,
|
||||
key: canonicalKey,
|
||||
entry,
|
||||
now: options?.now,
|
||||
includeDerivedTitles: options?.includeDerivedTitles,
|
||||
includeLastMessage: options?.includeLastMessage,
|
||||
});
|
||||
}
|
||||
|
||||
export function listSessionsFromStore(params: {
|
||||
cfg: OpenClawConfig;
|
||||
storePath: string;
|
||||
@@ -903,76 +1252,18 @@ export function listSessionsFromStore(params: {
|
||||
}
|
||||
return entry?.label === label;
|
||||
})
|
||||
.map(([key, entry]) => {
|
||||
const updatedAt = entry?.updatedAt ?? null;
|
||||
const total = resolveFreshSessionTotalTokens(entry);
|
||||
const totalTokensFresh =
|
||||
typeof entry?.totalTokens === "number" ? entry?.totalTokensFresh !== false : false;
|
||||
const parsed = parseGroupKey(key);
|
||||
const channel = entry?.channel ?? parsed?.channel;
|
||||
const subject = entry?.subject;
|
||||
const groupChannel = entry?.groupChannel;
|
||||
const space = entry?.space;
|
||||
const id = parsed?.id;
|
||||
const origin = entry?.origin;
|
||||
const originLabel = origin?.label;
|
||||
const displayName =
|
||||
entry?.displayName ??
|
||||
(channel
|
||||
? buildGroupDisplayName({
|
||||
provider: channel,
|
||||
subject,
|
||||
groupChannel,
|
||||
space,
|
||||
id,
|
||||
key,
|
||||
})
|
||||
: undefined) ??
|
||||
entry?.label ??
|
||||
originLabel;
|
||||
const deliveryFields = normalizeSessionDeliveryFields(entry);
|
||||
const parsedAgent = parseAgentSessionKey(key);
|
||||
const sessionAgentId = normalizeAgentId(parsedAgent?.agentId ?? resolveDefaultAgentId(cfg));
|
||||
const resolvedModel = resolveSessionModelIdentityRef(cfg, entry, sessionAgentId);
|
||||
const modelProvider = resolvedModel.provider;
|
||||
const model = resolvedModel.model ?? DEFAULT_MODEL;
|
||||
return {
|
||||
.map(([key, entry]) =>
|
||||
buildGatewaySessionRow({
|
||||
cfg,
|
||||
storePath,
|
||||
store,
|
||||
key,
|
||||
spawnedBy: entry?.spawnedBy,
|
||||
entry,
|
||||
kind: classifySessionKey(key, entry),
|
||||
label: entry?.label,
|
||||
displayName,
|
||||
channel,
|
||||
subject,
|
||||
groupChannel,
|
||||
space,
|
||||
chatType: entry?.chatType,
|
||||
origin,
|
||||
updatedAt,
|
||||
sessionId: entry?.sessionId,
|
||||
systemSent: entry?.systemSent,
|
||||
abortedLastRun: entry?.abortedLastRun,
|
||||
thinkingLevel: entry?.thinkingLevel,
|
||||
fastMode: entry?.fastMode,
|
||||
verboseLevel: entry?.verboseLevel,
|
||||
reasoningLevel: entry?.reasoningLevel,
|
||||
elevatedLevel: entry?.elevatedLevel,
|
||||
sendPolicy: entry?.sendPolicy,
|
||||
inputTokens: entry?.inputTokens,
|
||||
outputTokens: entry?.outputTokens,
|
||||
totalTokens: total,
|
||||
totalTokensFresh,
|
||||
responseUsage: entry?.responseUsage,
|
||||
modelProvider,
|
||||
model,
|
||||
contextTokens: entry?.contextTokens,
|
||||
deliveryContext: deliveryFields.deliveryContext,
|
||||
lastChannel: deliveryFields.lastChannel ?? entry?.lastChannel,
|
||||
lastTo: deliveryFields.lastTo ?? entry?.lastTo,
|
||||
lastAccountId: deliveryFields.lastAccountId ?? entry?.lastAccountId,
|
||||
};
|
||||
})
|
||||
now,
|
||||
includeDerivedTitles,
|
||||
includeLastMessage,
|
||||
}),
|
||||
)
|
||||
.toSorted((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0));
|
||||
|
||||
if (search) {
|
||||
@@ -992,37 +1283,11 @@ export function listSessionsFromStore(params: {
|
||||
sessions = sessions.slice(0, limit);
|
||||
}
|
||||
|
||||
const finalSessions: GatewaySessionRow[] = sessions.map((s) => {
|
||||
const { entry, ...rest } = s;
|
||||
let derivedTitle: string | undefined;
|
||||
let lastMessagePreview: string | undefined;
|
||||
if (entry?.sessionId) {
|
||||
if (includeDerivedTitles || includeLastMessage) {
|
||||
const parsed = parseAgentSessionKey(s.key);
|
||||
const agentId =
|
||||
parsed && parsed.agentId ? normalizeAgentId(parsed.agentId) : resolveDefaultAgentId(cfg);
|
||||
const fields = readSessionTitleFieldsFromTranscript(
|
||||
entry.sessionId,
|
||||
storePath,
|
||||
entry.sessionFile,
|
||||
agentId,
|
||||
);
|
||||
if (includeDerivedTitles) {
|
||||
derivedTitle = deriveSessionTitle(entry, fields.firstUserMessage);
|
||||
}
|
||||
if (includeLastMessage && fields.lastMessagePreview) {
|
||||
lastMessagePreview = fields.lastMessagePreview;
|
||||
}
|
||||
}
|
||||
}
|
||||
return { ...rest, derivedTitle, lastMessagePreview } satisfies GatewaySessionRow;
|
||||
});
|
||||
|
||||
return {
|
||||
ts: now,
|
||||
path: storePath,
|
||||
count: finalSessions.length,
|
||||
count: sessions.length,
|
||||
defaults: getSessionDefaults(cfg),
|
||||
sessions: finalSessions,
|
||||
sessions,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user