{"record":{"id":"a43f4205da4ec5c8","repo":"TencentCloud/TencentDB-Agent-Memory","slug":"invalid-generation-log-cursor","errorCode":null,"errorMessage":"Invalid generation log cursor","messagePattern":"Invalid generation log cursor","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"MemoryCore/src/core/memory-generation-log/store.ts","lineNumber":146,"sourceCode":"  async getByLogId(logId: string): Promise<MemoryGenerationLog | null> {\n    const match = logId.match(/^mgl_(l1|l2|l3)_(succeeded|failed)_(\\d+)_([A-Za-z0-9._-]+)_([a-f0-9]+)$/);\n    if (!match) return null;\n    const layer = match[1] as MemoryGenerationLayer;\n    const timestamp = Number(match[3]);\n    const anchor = match[4];\n    const { date, hour } = utcParts(timestamp);\n    const key = `${ROOT}/layer=${layer}/date=${date}/hour=${hour}/${reverseTimestamp(timestamp)}__mid=${anchor}__lid=${logId}.json`;\n    return this.getByKey(key);\n  }\n\n  async list(filter: MemoryGenerationLogListFilter): Promise<{ items: MemoryGenerationLogListItem[]; next_cursor?: string }> {\n    const layers: MemoryGenerationLayer[] = filter.layer ? [filter.layer] : [\"l1\", \"l2\", \"l3\"];\n    const items: MemoryGenerationLogListItem[] = [];\n    let boundary: { beforeMs: number; afterLogId: string } | undefined;\n    if (filter.cursor) {\n      const decoded = JSON.parse(Buffer.from(filter.cursor, \"base64url\").toString(\"utf8\")) as Record<string, unknown>;\n      if (!Number.isFinite(decoded.beforeMs) || typeof decoded.afterLogId !== \"string\") {\n        throw new Error(\"Invalid generation log cursor\");\n      }\n      boundary = { beforeMs: Number(decoded.beforeMs), afterLogId: decoded.afterLogId };\n    }\n    let hourMs = Math.floor(filter.endTimeMs / 3_600_000) * 3_600_000;\n\n    while (hourMs + 3_600_000 - 1 >= filter.startTimeMs && items.length <= filter.limit) {\n      const { date, hour } = utcParts(hourMs);\n      for (const layer of layers) {\n        let marker: string | undefined;\n        do {\n          const result = await this.storage.getBackend().listObjects(\n            `${ROOT}/layer=${layer}/date=${date}/hour=${hour}/`,\n            { maxKeys: 1000, marker, recursive: true },\n          );\n          for (const entry of result.entries) {\n            if (entry.isDirectory) continue;\n            const parsed = parseListItem(entry.key, entry.size);\n            if (!parsed || parsed.finished_at_ms < filter.startTimeMs || parsed.finished_at_ms > filter.endTimeMs) continue;","sourceCodeStart":128,"sourceCodeEnd":164,"githubUrl":"https://github.com/TencentCloud/TencentDB-Agent-Memory/blob/3efcd317b84146d6a08518ac0f7ee7c8a8d200ec/MemoryCore/src/core/memory-generation-log/store.ts#L128-L164","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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>\" }"],"exampleFix":"// before\nawait store.list({ startTimeMs: s, endTimeMs: e, limit: 100, cursor: staleCursorFromOldVersion });\n// after\nawait store.list({ startTimeMs: s, endTimeMs: e, limit: 100 }); // restart pagination, reuse fresh cursor from response","handlingStrategy":"try-catch","validationCode":"function isValidCursor(cursor) {\n  try {\n    const d = JSON.parse(Buffer.from(cursor, \"base64url\").toString(\"utf8\"));\n    return Number.isFinite(d.beforeMs) && typeof d.afterLogId === \"string\";\n  } catch { return false; }\n}","typeGuard":"function isDecodableCursor(cursor) {\n  try {\n    const d = JSON.parse(Buffer.from(cursor, \"base64url\").toString(\"utf8\"));\n    return typeof d === \"object\" && d !== null && Number.isFinite(d.beforeMs) && typeof d.afterLogId === \"string\";\n  } catch { return false; }\n}","tryCatchPattern":"try {\n  await store.list({ ...filter, cursor });\n} catch (e) {\n  if (e.message === \"Invalid generation log cursor\") {\n    logger.warn(\"cursor unreadable; restarting pagination from scratch\");\n    return store.list({ ...filter, cursor: undefined });\n  }\n  throw e;\n}","preventionTips":["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"],"tags":["pagination","validation","cursor","storage"],"backgroundTag":"invalid-pagination-cursor","analyzedSha":"3efcd317b84146d6a08518ac0f7ee7c8a8d200ec","analyzedAt":"2026-09-01T05:44:22.276Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}