different-ai/openwork · error

Automation engine event cursor must be a non-negative intege

Error message

Automation engine event cursor must be a non-negative integer

What it means

createAutomationEngineEventSequenceValidator replays engine events starting from a cursor (afterSequence). The cursor must be a non-negative safe integer because it is compared against event.sequence to enforce contiguity. Passing a negative, fractional, or non-integer value (including NaN, Infinity, or a string parsed from persistence) is rejected up front.

Source

Thrown at packages/automations/src/engine.ts:217

  ): Promise<AutomationEngineReadResult | null>
  cancel(
    receipt: AutomationEngineAdmissionReceipt,
  ): Promise<AutomationEngineCancellationResult>
}

export interface AutomationEngineEventSequenceValidator {
  readonly cursor: number
  accept(event: AutomationEngineEvent): void
}

/** Validates events before Den persists them and advances its durable cursor. */
export function createAutomationEngineEventSequenceValidator(
  rawReceipt: AutomationEngineAdmissionReceipt,
  afterSequence = 0,
): AutomationEngineEventSequenceValidator {
  const receipt = automationEngineAdmissionReceiptSchema.parse(rawReceipt)
  if (!Number.isSafeInteger(afterSequence) || afterSequence < 0) {
    throw new Error("Automation engine event cursor must be a non-negative integer")
  }
  let cursor = afterSequence
  const eventKeys = new Set<string>()
  return {
    get cursor() {
      return cursor
    },
    accept(rawEvent) {
      const event = automationEngineEventSchema.parse(rawEvent)
      if (event.executionId !== receipt.executionId || event.runId !== receipt.runId) {
        throw new Error("Automation engine event receipt mismatch")
      }
      if (event.sequence !== cursor + 1) {
        throw new Error("Automation engine event sequence is not contiguous")
      }
      if (eventKeys.has(event.idempotencyKey)) {
        throw new Error("Automation engine event idempotency key was repeated")
      }

View on GitHub (pinned to 2b7df46e8a)

Solutions

  1. Default the cursor to 0 when no checkpoint exists instead of using -1 or undefined
  2. Coerce persisted values with Number() and validate Number.isSafeInteger(v) && v >= 0 before calling
  3. Pass no second argument to restart validation from the beginning (default 0)

Example fix

// before
const v = createAutomationEngineEventSequenceValidator(receipt, checkpoint.cursor) // -1
// after
const start = Number.isSafeInteger(checkpoint?.cursor) && checkpoint.cursor >= 0 ? checkpoint.cursor : 0
const v = createAutomationEngineEventSequenceValidator(receipt, start)
Defensive patterns

Strategy: validation

Validate before calling

function safeCursor(v: unknown): number {
  const n = typeof v === "string" ? Number(v) : v
  return typeof n === "number" && Number.isSafeInteger(n) && n >= 0 ? n : 0
}
createAutomationEngineEventSequenceValidator(receipt, safeCursor(checkpoint?.cursor))

Type guard

const isValidCursor = (v: unknown): v is number =>
  typeof v === "number" && Number.isSafeInteger(v) && v >= 0

Try / catch

try {
  validator = createAutomationEngineEventSequenceValidator(receipt, cursor)
} catch (e) {
  if (e instanceof Error && e.message.includes("non-negative integer")) {
    validator = createAutomationEngineEventSequenceValidator(receipt, 0)
  } else throw e
}

Prevention

When it happens

Trigger: Calling createAutomationEngineEventSequenceValidator(receipt, afterSequence) where afterSequence is negative, not a safe integer (e.g. 1.5, NaN, Infinity), or was loaded from storage as a string/undefined-coerced value.

Common situations: Persisting the cursor in a JSON store and reading it back as a string; subtracting with undefined fields; a stale checkpoint row holding -1 as a sentinel for 'no cursor'.

Related errors


AI-assisted analysis of different-ai/openwork@2b7df46e8a (2026-09-01). Data as JSON: /api/errors/c6729b51e63a69b1. Report an issue: GitHub.