mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-12 16:20:44 +00:00
Summary:
- Roll up transcript-backed session usage across logical session lineage.
- Preserve lineage through /new and /reset rotations.
- Add Control UI usage scope controls with legacy gateway fallback.
- Refresh generated protocol and Control UI locale fallback surfaces.
Verification:
- pnpm test src/auto-reply/reply/session.test.ts ui/src/ui/controllers/usage.node.test.ts src/gateway/server-methods/usage.sessions-usage.test.ts
- pnpm protocol:check
- pnpm ui:i18n:check
- pnpm ui:build
- git diff --check
- PR CI green on 10f10850ee
Closes #50701.
143 lines
4.7 KiB
TypeScript
143 lines
4.7 KiB
TypeScript
import fs from "node:fs";
|
|
import type { SessionEntry } from "../../config/sessions.js";
|
|
import {
|
|
resolveAgentIdFromSessionKey,
|
|
resolveSessionFilePath,
|
|
resolveSessionFilePathOptions,
|
|
resolveSessionTranscriptPath,
|
|
updateSessionStore,
|
|
} from "../../config/sessions.js";
|
|
import { generateSecureUuid } from "../../infra/secure-random.js";
|
|
import { defaultRuntime } from "../../runtime.js";
|
|
import { refreshQueuedFollowupSession, type FollowupRun } from "./queue.js";
|
|
import { replayRecentUserAssistantMessages } from "./session-transcript-replay.js";
|
|
|
|
type ResetSessionOptions = {
|
|
failureLabel: string;
|
|
buildLogMessage: (nextSessionId: string) => string;
|
|
cleanupTranscripts?: boolean;
|
|
};
|
|
|
|
const deps = {
|
|
generateSecureUuid,
|
|
updateSessionStore,
|
|
refreshQueuedFollowupSession,
|
|
error: (message: string) => defaultRuntime.error(message),
|
|
};
|
|
|
|
export function setAgentRunnerSessionResetTestDeps(overrides?: Partial<typeof deps>): void {
|
|
Object.assign(deps, {
|
|
generateSecureUuid,
|
|
updateSessionStore,
|
|
refreshQueuedFollowupSession,
|
|
error: (message: string) => defaultRuntime.error(message),
|
|
...overrides,
|
|
});
|
|
}
|
|
|
|
export async function resetReplyRunSession(params: {
|
|
options: ResetSessionOptions;
|
|
sessionKey?: string;
|
|
queueKey: string;
|
|
activeSessionEntry?: SessionEntry;
|
|
activeSessionStore?: Record<string, SessionEntry>;
|
|
storePath?: string;
|
|
messageThreadId?: string;
|
|
followupRun: FollowupRun;
|
|
onActiveSessionEntry: (entry: SessionEntry) => void;
|
|
onNewSession: (newSessionId: string, nextSessionFile: string) => void;
|
|
}): Promise<boolean> {
|
|
if (!params.sessionKey || !params.activeSessionStore || !params.storePath) {
|
|
return false;
|
|
}
|
|
const prevEntry = params.activeSessionStore[params.sessionKey] ?? params.activeSessionEntry;
|
|
if (!prevEntry) {
|
|
return false;
|
|
}
|
|
const prevSessionId = params.options.cleanupTranscripts ? prevEntry.sessionId : undefined;
|
|
const nextSessionId = deps.generateSecureUuid();
|
|
const now = Date.now();
|
|
const nextEntry: SessionEntry = {
|
|
...prevEntry,
|
|
sessionId: nextSessionId,
|
|
updatedAt: now,
|
|
sessionStartedAt: now,
|
|
usageFamilyKey: prevEntry.usageFamilyKey ?? params.sessionKey,
|
|
usageFamilySessionIds: Array.from(
|
|
new Set([...(prevEntry.usageFamilySessionIds ?? []), prevEntry.sessionId, nextSessionId]),
|
|
),
|
|
lastInteractionAt: now,
|
|
systemSent: false,
|
|
abortedLastRun: false,
|
|
modelProvider: undefined,
|
|
model: undefined,
|
|
inputTokens: undefined,
|
|
outputTokens: undefined,
|
|
totalTokens: undefined,
|
|
totalTokensFresh: false,
|
|
estimatedCostUsd: undefined,
|
|
cacheRead: undefined,
|
|
cacheWrite: undefined,
|
|
contextTokens: undefined,
|
|
systemPromptReport: undefined,
|
|
fallbackNoticeSelectedModel: undefined,
|
|
fallbackNoticeActiveModel: undefined,
|
|
fallbackNoticeReason: undefined,
|
|
};
|
|
const agentId = resolveAgentIdFromSessionKey(params.sessionKey);
|
|
const nextSessionFile = resolveSessionTranscriptPath(
|
|
nextSessionId,
|
|
agentId,
|
|
params.messageThreadId,
|
|
);
|
|
nextEntry.sessionFile = nextSessionFile;
|
|
params.activeSessionStore[params.sessionKey] = nextEntry;
|
|
try {
|
|
await deps.updateSessionStore(params.storePath, (store) => {
|
|
store[params.sessionKey!] = nextEntry;
|
|
});
|
|
} catch (err) {
|
|
deps.error(
|
|
`Failed to persist session reset after ${params.options.failureLabel} (${params.sessionKey}): ${String(err)}`,
|
|
);
|
|
}
|
|
// Silent rotations (compaction/role-ordering) fire without user intent, so
|
|
// preserve recent user/assistant turns for direct-chat continuity.
|
|
await replayRecentUserAssistantMessages({
|
|
sourceTranscript: prevEntry.sessionFile,
|
|
targetTranscript: nextSessionFile,
|
|
newSessionId: nextSessionId,
|
|
});
|
|
params.followupRun.run.sessionId = nextSessionId;
|
|
params.followupRun.run.sessionFile = nextSessionFile;
|
|
deps.refreshQueuedFollowupSession({
|
|
key: params.queueKey,
|
|
previousSessionId: prevEntry.sessionId,
|
|
nextSessionId,
|
|
nextSessionFile,
|
|
});
|
|
params.onActiveSessionEntry(nextEntry);
|
|
params.onNewSession(nextSessionId, nextSessionFile);
|
|
deps.error(params.options.buildLogMessage(nextSessionId));
|
|
if (params.options.cleanupTranscripts && prevSessionId) {
|
|
const transcriptCandidates = new Set<string>();
|
|
const resolved = resolveSessionFilePath(
|
|
prevSessionId,
|
|
prevEntry,
|
|
resolveSessionFilePathOptions({ agentId, storePath: params.storePath }),
|
|
);
|
|
if (resolved) {
|
|
transcriptCandidates.add(resolved);
|
|
}
|
|
transcriptCandidates.add(resolveSessionTranscriptPath(prevSessionId, agentId));
|
|
for (const candidate of transcriptCandidates) {
|
|
try {
|
|
fs.unlinkSync(candidate);
|
|
} catch {
|
|
// Best-effort cleanup.
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|