mirror of
https://github.com/TencentCloud/TencentDB-Agent-Memory
synced 2026-07-11 12:54:29 +00:00
feat: release v0.3.3 — Hermes adapter, context offload, core refactor
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* SessionRegistry: Per-session OffloadStateManager routing.
|
||||
*
|
||||
* Maps sessionKey → { manager, lastAccessMs } with LRU eviction.
|
||||
* Eliminates the global singleton stateManager — each session gets
|
||||
* its own isolated OffloadStateManager + StorageContext.
|
||||
*/
|
||||
import { OffloadStateManager } from "./state-manager.js";
|
||||
import { parseSessionKey } from "./storage.js";
|
||||
|
||||
/** Matches internal memory-pipeline sessions (e.g. memory-{taskId}-session-{ts}). */
|
||||
const INTERNAL_SESSION_RE = /memory-.*-session-\d+/;
|
||||
|
||||
/** Returns true if the sessionKey belongs to an internal memory-pipeline session. */
|
||||
function isInternalMemorySession(sessionKey: string): boolean {
|
||||
return INTERNAL_SESSION_RE.test(sessionKey);
|
||||
}
|
||||
|
||||
/** Per-session context entry held by the registry. */
|
||||
export interface SessionCtx {
|
||||
readonly sessionKey: string;
|
||||
readonly manager: OffloadStateManager;
|
||||
lastAccessMs: number;
|
||||
}
|
||||
|
||||
/** Maximum number of cached sessions before LRU eviction kicks in. */
|
||||
const MAX_CACHED_SESSIONS = 20;
|
||||
|
||||
/** Routes sessionKey → per-session OffloadStateManager with LRU eviction. */
|
||||
export class SessionRegistry {
|
||||
private _sessions = new Map<string, SessionCtx>();
|
||||
private _dataRoot: string;
|
||||
readonly _registryId = ++SessionRegistry._registryCounter;
|
||||
private static _registryCounter = 0;
|
||||
|
||||
constructor(dataRoot: string) {
|
||||
this._dataRoot = dataRoot;
|
||||
}
|
||||
|
||||
/** Get the configured data root. */
|
||||
get dataRoot(): string {
|
||||
return this._dataRoot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create a per-session manager.
|
||||
* First access will create a new OffloadStateManager, call init() + switchSession()
|
||||
* to fully initialize storage paths and rebuild in-memory state from offload files.
|
||||
*/
|
||||
async resolve(sessionKey: string, realSessionId?: string): Promise<SessionCtx> {
|
||||
let entry = this._sessions.get(sessionKey);
|
||||
if (entry) {
|
||||
entry.lastAccessMs = Date.now();
|
||||
return entry;
|
||||
}
|
||||
|
||||
// New session — create manager and fully initialize
|
||||
const mgr = new OffloadStateManager();
|
||||
const parsed = parseSessionKey(sessionKey);
|
||||
if (parsed) {
|
||||
const effectiveSessionId = realSessionId || parsed.sessionId;
|
||||
await mgr.init(this._dataRoot, parsed.agentName, effectiveSessionId);
|
||||
// switchSession rebuilds confirmedOffloadIds, deletedOffloadIds,
|
||||
// processedToolCallIds from offload JSONL, registers sessionKey mapping,
|
||||
// and resets session-level runtime state.
|
||||
await mgr.switchSession(sessionKey, this._dataRoot, realSessionId);
|
||||
} else {
|
||||
// sessionKey doesn't match "agent:<name>:<id>" format.
|
||||
// Use a sanitized sessionKey as both agentName and sessionId
|
||||
// so ctx is always initialized (avoids "ctx not initialized" errors).
|
||||
const fallbackName = sessionKey.replace(/[<>:"/\\|?*\x00-\x1f]/g, "_").slice(0, 64) || "unknown";
|
||||
const fallbackSessionId = realSessionId || `fallback-${Date.now()}`;
|
||||
await mgr.init(this._dataRoot, fallbackName, fallbackSessionId);
|
||||
}
|
||||
|
||||
entry = { sessionKey, manager: mgr, lastAccessMs: Date.now() };
|
||||
this._sessions.set(sessionKey, entry);
|
||||
|
||||
// LRU eviction
|
||||
if (this._sessions.size > MAX_CACHED_SESSIONS) {
|
||||
this._evictOldest();
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a session only if it is NOT an internal memory-pipeline session.
|
||||
*
|
||||
* Returns null for memory sessions (e.g. `memory-{taskId}-session-{ts}`),
|
||||
* preventing unnecessary OffloadStateManager creation, disk I/O, and LRU
|
||||
* cache slot pollution for sessions that should never run offload.
|
||||
*
|
||||
* Callers that need unconditional resolve (e.g. tests) can still use resolve().
|
||||
*/
|
||||
async resolveIfAllowed(sessionKey: string, realSessionId?: string): Promise<SessionCtx | null> {
|
||||
if (isInternalMemorySession(sessionKey)) return null;
|
||||
return this.resolve(sessionKey, realSessionId);
|
||||
}
|
||||
|
||||
/** Look up an existing session (does not create). Updates lastAccessMs. */
|
||||
get(sessionKey: string): SessionCtx | undefined {
|
||||
const entry = this._sessions.get(sessionKey);
|
||||
if (entry) entry.lastAccessMs = Date.now();
|
||||
return entry;
|
||||
}
|
||||
|
||||
/** Number of cached sessions. */
|
||||
get size(): number {
|
||||
return this._sessions.size;
|
||||
}
|
||||
|
||||
/** Iterate over all session keys. */
|
||||
keys(): IterableIterator<string> {
|
||||
return this._sessions.keys();
|
||||
}
|
||||
|
||||
/** Iterate over all session entries. */
|
||||
values(): IterableIterator<SessionCtx> {
|
||||
return this._sessions.values();
|
||||
}
|
||||
|
||||
/** Evict the least-recently-accessed session. */
|
||||
private _evictOldest(): void {
|
||||
let oldestKey: string | null = null;
|
||||
let oldestMs = Infinity;
|
||||
for (const [key, entry] of this._sessions) {
|
||||
if (entry.lastAccessMs < oldestMs) {
|
||||
oldestMs = entry.lastAccessMs;
|
||||
oldestKey = key;
|
||||
}
|
||||
}
|
||||
if (oldestKey) this._sessions.delete(oldestKey);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user