TencentCloud/TencentDB-Agent-Memory · error
Invalid generation log cursor
Error message
Invalid generation log cursor
What it means
MemoryGenerationLogStore.list supports opaque base64url pagination cursors encoding { beforeMs, afterLogId }. list throws "Invalid generation log cursor" when the cursor decodes to JSON lacking a finite beforeMs number or a string afterLogId, or when JSON.parse/Buffer base64url decoding itself fails and surfaces through this validation path.
Source
Thrown at MemoryCore/src/core/memory-generation-log/store.ts:146
async getByLogId(logId: string): Promise<MemoryGenerationLog | null> {
const match = logId.match(/^mgl_(l1|l2|l3)_(succeeded|failed)_(\d+)_([A-Za-z0-9._-]+)_([a-f0-9]+)$/);
if (!match) return null;
const layer = match[1] as MemoryGenerationLayer;
const timestamp = Number(match[3]);
const anchor = match[4];
const { date, hour } = utcParts(timestamp);
const key = `${ROOT}/layer=${layer}/date=${date}/hour=${hour}/${reverseTimestamp(timestamp)}__mid=${anchor}__lid=${logId}.json`;
return this.getByKey(key);
}
async list(filter: MemoryGenerationLogListFilter): Promise<{ items: MemoryGenerationLogListItem[]; next_cursor?: string }> {
const layers: MemoryGenerationLayer[] = filter.layer ? [filter.layer] : ["l1", "l2", "l3"];
const items: MemoryGenerationLogListItem[] = [];
let boundary: { beforeMs: number; afterLogId: string } | undefined;
if (filter.cursor) {
const decoded = JSON.parse(Buffer.from(filter.cursor, "base64url").toString("utf8")) as Record<string, unknown>;
if (!Number.isFinite(decoded.beforeMs) || typeof decoded.afterLogId !== "string") {
throw new Error("Invalid generation log cursor");
}
boundary = { beforeMs: Number(decoded.beforeMs), afterLogId: decoded.afterLogId };
}
let hourMs = Math.floor(filter.endTimeMs / 3_600_000) * 3_600_000;
while (hourMs + 3_600_000 - 1 >= filter.startTimeMs && items.length <= filter.limit) {
const { date, hour } = utcParts(hourMs);
for (const layer of layers) {
let marker: string | undefined;
do {
const result = await this.storage.getBackend().listObjects(
`${ROOT}/layer=${layer}/date=${date}/hour=${hour}/`,
{ maxKeys: 1000, marker, recursive: true },
);
for (const entry of result.entries) {
if (entry.isDirectory) continue;
const parsed = parseListItem(entry.key, entry.size);
if (!parsed || parsed.finished_at_ms < filter.startTimeMs || parsed.finished_at_ms > filter.endTimeMs) continue;View on GitHub (pinned to 3efcd317b8)
Solutions
- Use a cursor exactly as returned by a previous list() response, unmodified
- Clear/reset the cursor and start pagination from the beginning (omit filter.cursor)
- If migrating from an old version, drop stored cursors — regenerate them with the current library
- Base64url-decode the cursor locally to inspect: JSON.parse(Buffer.from(c, "base64url")) should show { beforeMs: <number>, afterLogId: "<string>" }
Example fix
// before
await store.list({ startTimeMs: s, endTimeMs: e, limit: 100, cursor: staleCursorFromOldVersion });
// after
await store.list({ startTimeMs: s, endTimeMs: e, limit: 100 }); // restart pagination, reuse fresh cursor from response Defensive patterns
Strategy: try-catch
Validate before calling
function isValidCursor(cursor) {
try {
const d = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
return Number.isFinite(d.beforeMs) && typeof d.afterLogId === "string";
} catch { return false; }
} Type guard
function isDecodableCursor(cursor) {
try {
const d = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
return typeof d === "object" && d !== null && Number.isFinite(d.beforeMs) && typeof d.afterLogId === "string";
} catch { return false; }
} Try / catch
try {
await store.list({ ...filter, cursor });
} catch (e) {
if (e.message === "Invalid generation log cursor") {
logger.warn("cursor unreadable; restarting pagination from scratch");
return store.list({ ...filter, cursor: undefined });
}
throw e;
} Prevention
- Store cursors opaquely and return them unmodified; never parse, truncate, or re-encode them
- Drop persisted cursors after library upgrades (format may change)
- Fall back to cursor-less listing when a cursor fails validation
When it happens
Trigger: Calling list with filter.cursor set to: a cursor produced by a different/older schema, arbitrary text (not base64url), truncated/corrupted cursor strings, or valid base64 whose JSON is missing/wrong-typed beforeMs or afterLogId.
Common situations: Cursor persisted by a client across a library upgrade where the cursor format changed; URL-encoding mangling (base64url characters altered); hand-crafted cursors in scripts; copying a cursor truncated at a shell line break.
Related errors
- Generation log object key exceeds COS limit
- Invalid generation log key
- Invalid scoped storage key: ${JSON.stringify(key)}
- Path traversal rejected in scoped storage key: ${key}
- Storage key must be relative, got absolute: ${key}
AI-assisted analysis of TencentCloud/TencentDB-Agent-Memory@3efcd317b8 (2026-09-01).
Data as JSON: /api/errors/a43f4205da4ec5c8.
Report an issue: GitHub.