{"record":{"id":"e83deb3562dc01e2","repo":"pbakaus/impeccable","slug":"stale-manual-edit-apply-reply","errorCode":"stale_manual_edit_apply_reply","errorMessage":"stale_manual_edit_apply_reply","messagePattern":"stale_manual_edit_apply_reply","errorType":"http","errorClass":null,"httpStatus":409,"severity":"error","filePath":"skill/scripts/live-server.mjs","lineNumber":1310,"sourceCode":"        failed: summarizeManualApplyFailures(validation.result.failed),\n        fileCount: validation.result.files.length,\n        noteCount: validation.result.notes.length,\n      });\n      manualApply.resolveDeferred(msg.id, validation.result);\n      acknowledgePendingEvent(msg.id);\n      flushPendingPolls();\n      res.writeHead(200, { 'Content-Type': 'application/json' });\n      res.end(JSON.stringify({ ok: true }));\n      return;\n    }\n    if (manualApply.hasTimedOutId(msg.id)) {\n      const rollback = manualApply.rollbackTimedOutReply(msg);\n      recordManualEditActivity('manual_edit_apply_stale_reply_rejected', {\n        id: msg.id,\n        rolledBackFileCount: rollback.rolledBackFiles?.length || 0,\n        rollbackFailureCount: rollback.rollbackFailures?.length || 0,\n      });\n      res.writeHead(409, { 'Content-Type': 'application/json' });\n      res.end(JSON.stringify({ error: 'stale_manual_edit_apply_reply', ...rollback }));\n      return;\n    }\n    const sourceEventType = msg.sourceEventType || inferSourceEventType(msg);\n    if (msg.type === 'retry') {\n      const releasedEvent = releasePendingEvent(msg.id, sourceEventType);\n      if (!releasedEvent) {\n        res.writeHead(msg.id ? 404 : 400, { 'Content-Type': 'application/json' });\n        res.end(JSON.stringify({\n          error: msg.id ? 'unknown_poll_retry_id' : 'missing_poll_retry_id',\n          id: msg.id,\n        }));\n        return;\n      }\n      flushPendingPolls();\n      res.writeHead(200, { 'Content-Type': 'application/json' });\n      res.end(JSON.stringify({ ok: true, released: true }));\n      return;","sourceCodeStart":1292,"sourceCodeEnd":1328,"githubUrl":"https://github.com/pbakaus/impeccable/blob/f88b2837a7d7c3182e46307bbbb091a1ed547571/skill/scripts/live-server.mjs#L1292-L1328","documentation":"Manual-edit applies are hard-timed-out after 150s (IMPECCABLE_LIVE_APPLY_EVENT_HARD_TIMEOUT_MS) and the event id is tombstoned. If the agent's reply arrives after that window, the server treats it as stale: rollbackTimedOutReply reverts the file writes the late apply performed (restoring the pre-apply snapshot), records manual_edit_apply_stale_reply_rejected, and returns HTTP 409 with rolledBackFiles and rollbackFailures counts.","triggerScenarios":"The applier takes longer than 150s to reply (slow disk, huge diff, debugger paused, laptop suspended mid-apply) and its done-message lands after the server already gave up; or a duplicate/delayed delivery of the same reply arrives after the timeout fired.","commonSituations":"Applying to a project on network filesystem; agent process resumed after suspension; terminal agent retrying the reply command; timeout lowered via env var while the applier legitimately needs longer.","solutions":["Treat the 409 as terminal for that id — do not resend the same reply","Check rolledBackFileCount and rollbackFailureCount in the response; if rollbackFailures > 0, inspect those source files and git-restore them manually","Poll again — the server re-issues a fresh apply event id if the edits are still wanted","For legitimately slow applies, raise IMPECCABLE_LIVE_APPLY_EVENT_HARD_TIMEOUT_MS on the server before starting the next apply"],"exampleFix":"// before: reply assumed accepted if HTTP ended\nconst res = await fetch(replyUrl, { method: 'POST', body: data });\n// after: a 409 means the apply was rolled back — start over with a new event\nif (res.status === 409) {\n  const { rolledBackFiles, rollbackFailures } = await res.json();\n  console.log('stale apply rolled back:', rolledBackFiles, 'failures:', rollbackFailures);\n}","handlingStrategy":"validation","validationCode":"// Before replying, check whether the apply window already closed (default 150s)\nconst APPLY_TIMEOUT_MS = Number(process.env.IMPECCABLE_LIVE_APPLY_EVENT_HARD_TIMEOUT_MS || 150_000);\nif (Date.now() - applyStartedAt > APPLY_TIMEOUT_MS - 5_000) {\n  // reply would be stale: poll for a fresh event instead of replying\n}","typeGuard":null,"tryCatchPattern":"try {\n  const res = await reply();\n  if (res.status === 409) { /* terminal: id timed out, files rolled back — do not resend */ }\n} catch (err) { /* network error is NOT staleness; one retry is safe */ }","preventionTips":["Start the apply clock when the event is received and reply well inside the 150s hard timeout","Raise IMPECCABLE_LIVE_APPLY_EVENT_HARD_TIMEOUT_MS when the batch is known to be large","Never blindly re-send a done reply after a long pause — poll first and see what the server expects"],"tags":["manual-edit-apply","timeout","rollback","stale-reply","http-409"],"backgroundTag":"stale-response-after-timeout","analyzedSha":"f88b2837a7d7c3182e46307bbbb091a1ed547571","analyzedAt":"2026-08-18T04:58:36.608Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}