

























@@ -6,6 +6,10 @@ import {
66acquireSessionWriteLock,
77resolveSessionWriteLockOptions,
88} from "../../agents/session-write-lock.js";
9+import {
10+resolveSessionStoreAgentId,
11+resolveSessionStoreKey,
12+} from "../../gateway/session-store-key.js";
913import { formatErrorMessage } from "../../infra/errors.js";
1014import { resolveRequiredHomeDir } from "../../infra/home-dir.js";
1115import { resolveAgentIdFromSessionKey } from "../../routing/session-key.js";
@@ -18,6 +22,7 @@ import { getRuntimeConfig } from "../io.js";
1822import type { OpenClawConfig } from "../types.openclaw.js";
1923import { formatSessionArchiveTimestamp } from "./artifacts.js";
2024import { extractGeneratedTranscriptSessionId } from "./generated-transcript-session-id.js";
25+import { resolveAgentMainSessionKey } from "./main-session.js";
2126import {
2227resolveSessionFilePath,
2328resolveSessionFilePathOptions,
@@ -68,6 +73,7 @@ import {
6873type SessionLifecycleArtifactCleanupResult,
6974type SessionLifecycleStoreTarget,
7075} from "./store.js";
76+import { resolveAllAgentSessionStoreTargetsSync, type SessionStoreTarget } from "./targets.js";
7177import { parseSessionThreadInfo } from "./thread-info.js";
7278import {
7379type AppendSessionTranscriptMessageParams,
@@ -124,6 +130,50 @@ export type SessionAccessScope = {
124130storePath?: string;
125131};
126132133+export type LogicalSessionAccessScope = {
134+/** Runtime config whose session store rules define the logical session owner. */
135+cfg: OpenClawConfig;
136+/** Environment override used when resolving configured/discovered agent stores. */
137+env?: NodeJS.ProcessEnv;
138+/** Canonical or alias session key for the logical entry being read or written. */
139+sessionKey: string;
140+};
141+142+export type ResolvedSessionEntryAccessTarget = {
143+/** Agent owner inferred from the canonical session key. */
144+agentId: string;
145+/** Canonical session key returned to callers even when an alias row won. */
146+canonicalKey: string;
147+/** Freshest matching entry, if any. */
148+entry?: SessionEntry;
149+/** Original caller-supplied key after trimming. */
150+requestedKey: string;
151+/** Persisted key for the selected row. */
152+storeKey: string;
153+};
154+155+type ResolvedSessionEntryStoreTarget = ResolvedSessionEntryAccessTarget & {
156+storePath: string;
157+};
158+159+export type ResolvedSessionEntryUpdateContext = Omit<ResolvedSessionEntryAccessTarget, "entry"> & {
160+/** Mutable entry inside the storage operation. */
161+entry: SessionEntry;
162+};
163+164+export type ResolvedSessionEntryUpdateResult<T> =
165+| {
166+canonicalKey: string;
167+found: false;
168+}
169+| {
170+canonicalKey: string;
171+entry: SessionEntry;
172+found: true;
173+result: T;
174+storeKey: string;
175+};
176+127177export type SessionTranscriptAccessScope = Omit<SessionAccessScope, "sessionKey"> & {
128178/** Explicit transcript file path; bypasses store lookup when already known. */
129179sessionFile?: string;
@@ -428,6 +478,182 @@ export type DeleteSessionEntryLifecycleParams = {
428478429479export { clearPluginOwnedSessionState };
430480481+function isStorePathTemplate(store?: string): boolean {
482+return typeof store === "string" && store.includes("{agentId}");
483+}
484+485+function resolveLogicalSessionStoreCandidates(params: {
486+agentId: string;
487+cfg: OpenClawConfig;
488+env?: NodeJS.ProcessEnv;
489+}): SessionStoreTarget[] {
490+const storeConfig = params.cfg.session?.store;
491+const defaultTarget = {
492+agentId: params.agentId,
493+storePath: resolveStorePath(storeConfig, { agentId: params.agentId, env: params.env }),
494+};
495+if (!isStorePathTemplate(storeConfig)) {
496+return [defaultTarget];
497+}
498+const targets = new Map<string, SessionStoreTarget>();
499+targets.set(defaultTarget.storePath, defaultTarget);
500+for (const target of resolveAllAgentSessionStoreTargetsSync(params.cfg, { env: params.env })) {
501+if (target.agentId === params.agentId) {
502+targets.set(target.storePath, target);
503+}
504+}
505+return [...targets.values()];
506+}
507+508+function buildLogicalSessionEntryCandidateKeys(params: {
509+agentId: string;
510+canonicalKey: string;
511+cfg: OpenClawConfig;
512+requestedKey: string;
513+}): string[] {
514+const targets = new Set<string>();
515+if (params.canonicalKey) {
516+targets.add(params.canonicalKey);
517+}
518+if (params.requestedKey && params.requestedKey !== params.canonicalKey) {
519+targets.add(params.requestedKey);
520+}
521+if (params.canonicalKey === "global" || params.canonicalKey === "unknown") {
522+return [...targets];
523+}
524+const agentMainKey = resolveAgentMainSessionKey({
525+cfg: params.cfg,
526+agentId: params.agentId,
527+});
528+if (params.canonicalKey === agentMainKey) {
529+targets.add(`agent:${params.agentId}:main`);
530+}
531+return [...targets];
532+}
533+534+function findFreshestSessionEntryMatch(
535+entries: SessionEntrySummary[],
536+candidateKeys: readonly string[],
537+): SessionEntrySummary | undefined {
538+let freshest: SessionEntrySummary | undefined;
539+for (const candidate of candidateKeys) {
540+const trimmed = candidate.trim();
541+if (!trimmed) {
542+continue;
543+}
544+const match = entries.find((entry) => entry.sessionKey === trimmed);
545+if (match && (!freshest || (match.entry.updatedAt ?? 0) >= (freshest.entry.updatedAt ?? 0))) {
546+freshest = match;
547+}
548+}
549+return freshest;
550+}
551+552+/**
553+ * Resolves a logical session key to the freshest matching entry across the
554+ * configured store and discovered same-agent stores.
555+ */
556+export function resolveSessionEntryAccessTarget(
557+scope: LogicalSessionAccessScope,
558+): ResolvedSessionEntryAccessTarget {
559+const target = resolveSessionEntryStoreTarget(scope);
560+return {
561+agentId: target.agentId,
562+canonicalKey: target.canonicalKey,
563+entry: target.entry,
564+requestedKey: target.requestedKey,
565+storeKey: target.storeKey,
566+};
567+}
568+569+function resolveSessionEntryStoreTarget(
570+scope: LogicalSessionAccessScope,
571+): ResolvedSessionEntryStoreTarget {
572+const requestedKey = scope.sessionKey.trim();
573+const canonicalKey = resolveSessionStoreKey({ cfg: scope.cfg, sessionKey: requestedKey });
574+const agentId = resolveSessionStoreAgentId(scope.cfg, canonicalKey);
575+const scanTargets = buildLogicalSessionEntryCandidateKeys({
576+ agentId,
577+ canonicalKey,
578+cfg: scope.cfg,
579+ requestedKey,
580+});
581+const candidates = resolveLogicalSessionStoreCandidates({
582+ agentId,
583+cfg: scope.cfg,
584+env: scope.env,
585+});
586+const fallback = candidates[0] ?? {
587+ agentId,
588+storePath: resolveStorePath(scope.cfg.session?.store, { agentId, env: scope.env }),
589+};
590+let selectedStorePath = fallback.storePath;
591+let selectedMatch = findFreshestSessionEntryMatch(
592+listSessionEntries({ storePath: fallback.storePath }),
593+scanTargets,
594+);
595+for (let index = 1; index < candidates.length; index += 1) {
596+const candidate = candidates[index];
597+if (!candidate) {
598+continue;
599+}
600+const match = findFreshestSessionEntryMatch(
601+listSessionEntries({ storePath: candidate.storePath }),
602+scanTargets,
603+);
604+if (
605+match &&
606+(!selectedMatch || (match.entry.updatedAt ?? 0) >= (selectedMatch.entry.updatedAt ?? 0))
607+) {
608+selectedStorePath = candidate.storePath;
609+selectedMatch = match;
610+}
611+}
612+return {
613+ agentId,
614+ canonicalKey,
615+entry: selectedMatch?.entry,
616+ requestedKey,
617+storeKey: selectedMatch?.sessionKey ?? canonicalKey,
618+storePath: selectedStorePath,
619+};
620+}
621+622+/**
623+ * Mutates the freshest matching logical session entry without exposing the
624+ * backing store map to callers.
625+ */
626+export async function updateResolvedSessionEntry<T>(
627+scope: LogicalSessionAccessScope,
628+update: (entry: SessionEntry, context: ResolvedSessionEntryUpdateContext) => Promise<T> | T,
629+): Promise<ResolvedSessionEntryUpdateResult<T>> {
630+const target = resolveSessionEntryStoreTarget(scope);
631+if (!target.entry) {
632+return { canonicalKey: target.canonicalKey, found: false };
633+}
634+return await updateSessionStore(target.storePath, async (store) => {
635+const entry = store[target.storeKey];
636+if (!entry) {
637+return { canonicalKey: target.canonicalKey, found: false };
638+}
639+const context: ResolvedSessionEntryUpdateContext = {
640+agentId: target.agentId,
641+canonicalKey: target.canonicalKey,
642+ entry,
643+requestedKey: target.requestedKey,
644+storeKey: target.storeKey,
645+};
646+const result = await update(entry, context);
647+return {
648+canonicalKey: target.canonicalKey,
649+entry: structuredClone(entry),
650+found: true,
651+ result,
652+storeKey: target.storeKey,
653+};
654+});
655+}
656+431657/** Returns the entry for a canonical or alias session key, if one exists. */
432658export function loadSessionEntry(scope: SessionAccessScope): SessionEntry | undefined {
433659if (scope.clone === false) {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。