{"record":{"id":"040c3049c5daa05c","repo":"moeru-ai/airi","slug":"chat-sync-tombstone-drain-failed-for","errorCode":null,"errorMessage":"[chat-sync] tombstone drain failed for","messagePattern":"\\[chat-sync\\] tombstone drain failed for","errorType":"console","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"packages/stage-ui/src/stores/chat/session-store.ts","lineNumber":1278,"sourceCode":"    const tombstones = await chatSessionsRepo.getTombstones(userId)\n    if (tombstones.length === 0)\n      return\n\n    const mapper = getCloudMapper()\n    const succeeded: string[] = []\n    for (const cloudChatId of tombstones) {\n      try {\n        await mapper.deleteChat(cloudChatId)\n        succeeded.push(cloudChatId)\n      }\n      catch (err) {\n        const msg = errorMessageFrom(err) ?? ''\n        // 404 = server already cleared it, treat as success so the\n        // tombstone gets dropped instead of retried forever.\n        if (msg.includes('HTTP 404'))\n          succeeded.push(cloudChatId)\n        else\n          console.warn('[chat-sync] tombstone drain failed for', cloudChatId, msg)\n      }\n    }\n    if (succeeded.length > 0)\n      await enqueuePersist(() => chatSessionsRepo.removeTombstones(userId, succeeded))\n  }\n\n  async function initialize() {\n    if (ready.value) {\n      return\n    }\n    if (initializePromise) {\n      return initializePromise\n    }\n    initializing.value = true\n    initializePromise = (async () => {\n      await ensureActiveSessionForCharacter()\n      ready.value = true\n      // Surface any outbox left over from a previous session (closed tab","sourceCodeStart":1260,"sourceCodeEnd":1296,"githubUrl":"https://github.com/moeru-ai/airi/blob/b6d0809ecb6421d492c4ce38aa7a76146c83269d/packages/stage-ui/src/stores/chat/session-store.ts#L1260-L1296","documentation":"During chat sync, tombstones (locally deleted chats pending server-side deletion) are drained by calling mapper.deleteChat(cloudChatId) per ID. Any failure whose message does not contain 'HTTP 404' logs this warning, and the tombstone is kept, so deletion is retried on a later sync cycle. HTTP 404 is treated as success because the server already deleted the chat (idempotent delete).","triggerScenarios":"The delete API call fails with 5xx, network errors, expired auth (401/403), or validation rejections (409/422) - anything except HTTP 404. The caught error message is printed after the chat ID.","commonSituations":"Flaky connection during background sync; session token expired mid-drain; server restart while deletes are in flight; self-hosted API version where the delete endpoint moved or returns a non-404 for missing chats.","solutions":["Read the message logged after the cloudChatId - it carries the underlying network or HTTP error","Do nothing for transient failures: the tombstone stays queued and the drain re-runs on the next sync","Re-authenticate if the message shows 401/403, then trigger another sync","If the server genuinely lacks the chat but returns something other than 404, fix the server's delete endpoint contract to return 404 for missing resources","Correlate the logged ID with the failing DELETE request in the DevTools network tab"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"for (const id of tombstones) {\n  try {\n    await mapper.deleteChat(id)\n    succeeded.push(id)\n  }\n  catch (err) {\n    // 404 means the server already deleted it: idempotent success\n    if (errorMessageFrom(err)?.includes('HTTP 404'))\n      succeeded.push(id)\n    else\n      keepForNextSync(id) // leave the tombstone so deletion retries\n  }\n}","preventionTips":["Keep sessions authenticated before triggering syncs","Treat 404 on delete as success so tombstones cannot wedge the queue","Never clear tombstones on unknown errors - that silently loses deletions","Log the cloudChatId alongside the error for request correlation"],"tags":["chat-sync","tombstone","delete","http-status","retry-queue"],"backgroundTag":"sync-delete-failed","analyzedSha":"b6d0809ecb6421d492c4ce38aa7a76146c83269d","analyzedAt":"2026-08-18T17:29:58.153Z","schemaVersion":2},"datasetVersion":"2026-08-23T13:39:53.451Z"}