mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-11 05:36:09 +00:00
docs: document session metadata helpers
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
// Delivery lookup recovers routable channel context from persisted session stores.
|
||||
import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce";
|
||||
import {
|
||||
resolveSessionStoreAgentId,
|
||||
@@ -83,6 +84,8 @@ export function extractDeliveryInfo(
|
||||
function resolveDeliveryStorePaths(cfg: OpenClawConfig, agentId: string): string[] {
|
||||
const paths = new Set<string>();
|
||||
paths.add(resolveStorePath(cfg.session?.store, { agentId }));
|
||||
// Delivery can be restored from any resolved agent target; store order keeps the configured
|
||||
// primary path first while still covering per-agent stores.
|
||||
for (const target of resolveAllAgentSessionStoreTargetsSync(cfg)) {
|
||||
if (target.agentId === agentId) {
|
||||
paths.add(target.storePath);
|
||||
@@ -205,6 +208,8 @@ function buildFreshestSessionEntryIndex(
|
||||
) {
|
||||
index.set(normalized, entry);
|
||||
}
|
||||
// Lowercase aliases are only indexed when case folding is not proof-sensitive; Matrix-style
|
||||
// opaque ids must keep exact-case delivery evidence.
|
||||
const foldedLegacyKey = normalizeLowercaseStringOrEmpty(normalized);
|
||||
if (foldedLegacyKey === normalized || requiresFoldedSessionKeyAliasProof(normalized)) {
|
||||
continue;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Session disk-budget enforcement prunes orphaned artifacts before deleting store entries.
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import {
|
||||
@@ -144,6 +145,8 @@ function resolveSessionTranscriptPathForEntry(params: {
|
||||
const resolvedSessionsDir = canonicalizePathForComparison(params.sessionsDir);
|
||||
const resolvedPath = canonicalizePathForComparison(resolved);
|
||||
const relative = path.relative(resolvedSessionsDir, resolvedPath);
|
||||
// Cleanup only owns artifacts under the sessions directory; absolute/parent escapes are
|
||||
// ignored even if a stale entry points there.
|
||||
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
|
||||
return null;
|
||||
}
|
||||
@@ -373,6 +376,8 @@ async function removeFileForBudget(params: {
|
||||
const resolvedPath = path.resolve(params.filePath);
|
||||
const canonicalPath = params.canonicalPath ?? canonicalizePathForComparison(resolvedPath);
|
||||
if (params.dryRun) {
|
||||
// Dry-run deletion is path-deduped so a transcript and pointer alias cannot count the same
|
||||
// artifact twice against the simulated budget.
|
||||
if (params.simulatedRemovedPaths.has(canonicalPath)) {
|
||||
return 0;
|
||||
}
|
||||
@@ -453,6 +458,8 @@ export async function pruneUnreferencedSessionArtifacts(params: {
|
||||
sessionsDir,
|
||||
store: params.store,
|
||||
});
|
||||
// Prompt refs are projected through the persistence layer so inline snapshots and externalized
|
||||
// prompt blobs are judged against the bytes that would actually hit disk.
|
||||
const projectedPromptBlobRefCounts = buildProjectedPromptBlobRefCounts(
|
||||
projectSessionStoreForPersistence({
|
||||
storePath: params.storePath,
|
||||
@@ -583,6 +590,8 @@ export async function enforceSessionDiskBudget(params: {
|
||||
(sum, bytes) => sum + bytes,
|
||||
0,
|
||||
);
|
||||
// Budget starts from current files, then swaps in the projected store/prompt bytes that the next
|
||||
// persistence pass will write.
|
||||
let total =
|
||||
[...files, ...promptBlobFiles].reduce((sum, file) => sum + file.size, 0) -
|
||||
(storeFile?.size ?? 0) +
|
||||
@@ -642,6 +651,7 @@ export async function enforceSessionDiskBudget(params: {
|
||||
);
|
||||
})
|
||||
.toSorted((a, b) => a.mtimeMs - b.mtimeMs);
|
||||
// Cheapest cleanup first: orphaned prompt blobs can relieve pressure without losing sessions.
|
||||
for (const file of unreferencedPromptBlobQueue) {
|
||||
if (total <= highWaterBytes) {
|
||||
break;
|
||||
@@ -669,6 +679,7 @@ export async function enforceSessionDiskBudget(params: {
|
||||
isDiskBudgetRemovableSessionFile(file, referencedPaths, tempStaleCutoffMs, storeBasename),
|
||||
)
|
||||
.toSorted((a, b) => a.mtimeMs - b.mtimeMs);
|
||||
// Then remove stale artifacts already detached from live entries.
|
||||
for (const file of removableFileQueue) {
|
||||
if (total <= highWaterBytes) {
|
||||
break;
|
||||
@@ -698,6 +709,7 @@ export async function enforceSessionDiskBudget(params: {
|
||||
const bTime = getEntryUpdatedAt(params.store[b]);
|
||||
return aTime - bTime;
|
||||
});
|
||||
// Last resort: delete oldest non-preserved sessions, then their now-unreferenced artifacts.
|
||||
for (const key of keys) {
|
||||
if (total <= highWaterBytes) {
|
||||
break;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Explicit session keys are normalized by the channel that owns their opaque id shape.
|
||||
import {
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
normalizeOptionalLowercaseString,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Session goal state tracks objective progress and token budgets in the session store.
|
||||
import crypto from "node:crypto";
|
||||
import { getSessionEntry, patchSessionEntry } from "./store.js";
|
||||
import { resolveFreshSessionTotalTokens } from "./types.js";
|
||||
@@ -101,6 +102,8 @@ function accountGoalUsage(
|
||||
}
|
||||
const totalTokens = resolveEntryFreshTotalTokens(entry);
|
||||
const hasFreshStart = goal.tokenStartFresh !== false;
|
||||
// Old entries may have a stale token baseline; display-only reads can hold it, while persisted
|
||||
// reads adopt the fresh total so future budget checks use current accounting.
|
||||
const shouldHoldStaleStart = !hasFreshStart && options?.adoptFreshBaseline === false;
|
||||
const shouldAdoptFreshStart =
|
||||
!shouldHoldStaleStart && totalTokens !== undefined && !hasFreshStart;
|
||||
@@ -175,6 +178,7 @@ export async function getSessionGoal(
|
||||
): Promise<SessionGoalSnapshot> {
|
||||
const now = nowMs(options.now);
|
||||
if (options.persist === false) {
|
||||
// Status rendering should not write incidental budget/baseline adoption unless callers opt in.
|
||||
const entry =
|
||||
getSessionEntry({ sessionKey: options.sessionKey, storePath: options.storePath }) ??
|
||||
options.fallbackEntry;
|
||||
@@ -265,6 +269,7 @@ export async function updateSessionGoalStatus(
|
||||
(accounted.status === "budget_limited" ||
|
||||
accounted.status === "usage_limited" ||
|
||||
(accounted.tokenBudget !== undefined && accounted.tokensUsed >= accounted.tokenBudget));
|
||||
// Resuming from a limited state starts a new budget window at the current fresh token count.
|
||||
const freshTokenStart = resetsBudgetWindow ? resolveEntryFreshTotalTokens(entry) : undefined;
|
||||
const next: SessionGoal = {
|
||||
...accounted,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Group session keys convert channel-specific group metadata into stable store ids.
|
||||
import {
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
normalizeOptionalLowercaseString,
|
||||
@@ -17,6 +18,8 @@ type LegacyGroupSessionSurface = {
|
||||
};
|
||||
|
||||
function resolveLegacyGroupSessionKey(ctx: MsgContext): GroupKeyResolution | null {
|
||||
// Legacy plugin resolvers stay first-class because some channels still expose native group ids
|
||||
// only through channel-owned context parsing.
|
||||
for (const plugin of listChannelPlugins()) {
|
||||
const resolved = (
|
||||
plugin.messaging as LegacyGroupSessionSurface | undefined
|
||||
@@ -93,6 +96,7 @@ export function buildGroupDisplayName(params: {
|
||||
const fallbackId = normalizeOptionalString(params.id) ?? params.key;
|
||||
const rawLabel = detail || fallbackId;
|
||||
let token = normalizeGroupLabel(rawLabel);
|
||||
// Very long opaque ids become a readable stable token instead of leaking full route ids into UI.
|
||||
if (!token) {
|
||||
token = normalizeGroupLabel(shortenGroupId(rawLabel));
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Session lifecycle timestamps prefer store metadata and fall back to transcript headers.
|
||||
import fs from "node:fs";
|
||||
import { asDateTimestampMs } from "../../shared/number-coercion.js";
|
||||
import {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Session metadata derives stable origin, group, and display fields from message context.
|
||||
import {
|
||||
normalizeOptionalLowercaseString,
|
||||
normalizeOptionalString,
|
||||
@@ -137,6 +138,8 @@ export function deriveGroupSessionPatch(params: {
|
||||
const space = params.ctx.GroupSpace?.trim();
|
||||
const explicitChannel = params.ctx.GroupChannel?.trim();
|
||||
const subjectLooksChannel = Boolean(subject?.startsWith("#"));
|
||||
// Channel-looking subjects become `groupChannel` only for channel-capable providers; ordinary
|
||||
// group chats keep the subject as human-readable metadata.
|
||||
const normalizedChannel =
|
||||
subjectLooksChannel && resolution.chatType !== "channel" ? normalizeChannelId(channel) : null;
|
||||
const isChannelProvider = Boolean(
|
||||
|
||||
Reference in New Issue
Block a user