{"record":{"id":"91d53bfc177e448b","repo":"different-ai/openwork","slug":"automation-engine-event-sequence-is-not-contiguous","errorCode":null,"errorMessage":"Automation engine event sequence is not contiguous","messagePattern":"Automation engine event sequence is not contiguous","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/automations/src/engine.ts","lineNumber":231,"sourceCode":"  afterSequence = 0,\n): AutomationEngineEventSequenceValidator {\n  const receipt = automationEngineAdmissionReceiptSchema.parse(rawReceipt)\n  if (!Number.isSafeInteger(afterSequence) || afterSequence < 0) {\n    throw new Error(\"Automation engine event cursor must be a non-negative integer\")\n  }\n  let cursor = afterSequence\n  const eventKeys = new Set<string>()\n  return {\n    get cursor() {\n      return cursor\n    },\n    accept(rawEvent) {\n      const event = automationEngineEventSchema.parse(rawEvent)\n      if (event.executionId !== receipt.executionId || event.runId !== receipt.runId) {\n        throw new Error(\"Automation engine event receipt mismatch\")\n      }\n      if (event.sequence !== cursor + 1) {\n        throw new Error(\"Automation engine event sequence is not contiguous\")\n      }\n      if (eventKeys.has(event.idempotencyKey)) {\n        throw new Error(\"Automation engine event idempotency key was repeated\")\n      }\n      eventKeys.add(event.idempotencyKey)\n      cursor = event.sequence\n    },\n  }\n}\n","sourceCodeStart":213,"sourceCodeEnd":241,"githubUrl":"https://github.com/different-ai/openwork/blob/2b7df46e8ae1517d64c896c7793d2d52ec845669/packages/automations/src/engine.ts#L213-L241","documentation":"accept() enforces a strictly contiguous sequence: each accepted event's sequence must equal the current cursor + 1. This detects lost or reordered events during replay. A gap (skipped number) means events were dropped between the last accepted event and this one.","triggerScenarios":"Feeding accept() an event whose sequence !== cursor + 1 — skipping a number (event lost), replaying the same sequence twice, or starting the stream at sequence > afterSequence + 1.","commonSituations":"Reading events from a log that compacts/trims old entries; concurrent writers with gaps; resuming with the wrong afterSequence checkpoint; events published to a lossy transport.","solutions":["Replay the event stream from the correct checkpoint (afterSequence matching the last accepted event)","Ensure the event source preserves ordering and delivers every event exactly once","If a gap is expected (trimmed log), rebuild the validator from the earliest available sequence instead of mid-stream"],"exampleFix":"// before\nvalidator.accept(nextEvent) // sequence 5 while cursor is 3\n// after\nif (nextEvent.sequence === validator.cursor + 1) validator.accept(nextEvent)\nelse resumeStreamFrom(validator.cursor)","handlingStrategy":"validation","validationCode":"if (event.sequence !== validator.cursor + 1) {\n  // resync from the source instead of accepting out-of-order\n  restartReplayFrom(validator.cursor)\n} else {\n  validator.accept(event)\n}","typeGuard":"const isNextInSequence = (e: AutomationEngineEvent, cursor: number): boolean => e.sequence === cursor + 1","tryCatchPattern":"try {\n  validator.accept(event)\n} catch (e) {\n  if (e instanceof Error && e.message.includes(\"not contiguous\")) {\n    await replayFromSource(validator.cursor) // refill missing events then continue\n  } else throw e\n}","preventionTips":["Consume events from an ordered, lossless source (single-partition log) not an unordered set","Persist the cursor checkpoint atomically with event processing","Add an integration test that replays a full stream and asserts no gaps"],"tags":["event-stream","ordering","automations"],"backgroundTag":"event-sequence-gap","analyzedSha":"2b7df46e8ae1517d64c896c7793d2d52ec845669","analyzedAt":"2026-09-01T07:59:23.713Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}