paperclipai/paperclip · error

${label} must be a positive finite number

Error message

${label} must be a positive finite number

What it means

assertTtl validates that a TTL value is a finite number greater than zero. acquireLock, extendLock, and expiryFromTtl call it; a NaN, Infinity, 0, or negative TTL would produce unusable expiry math, so it is rejected at the boundary.

Source

Thrown at server/src/services/chat-sdk-state.ts:80

export interface PaperclipChatSdkStateOptions extends ChatSdkStateScope {
  maxValueBytes?: number;
  now?: () => Date;
  persistence: ChatSdkStatePersistence;
}

function assertBoundedIdentifier(
  label: string,
  value: string,
  maximum: number,
): void {
  if (!value || value.length > maximum) {
    throw new Error(`${label} must be between 1 and ${maximum} characters`);
  }
}

function assertTtl(label: string, ttlMs: number): void {
  if (!(Number.isFinite(ttlMs) && ttlMs > 0)) {
    throw new Error(`${label} must be a positive finite number`);
  }
}

function storageKey(kind: StateKind, logicalKey: string): string {
  assertBoundedIdentifier(
    "Chat SDK state key",
    logicalKey,
    MAX_LOGICAL_KEY_LENGTH,
  );
  const digest = createHash("sha256").update(logicalKey).digest("hex");
  return `${kind}:${digest}`;
}

function decodeEnvelope(
  record: ChatSdkStateRecord,
  expectedKind: StateKind,
): unknown {
  const envelope = record.value as Partial<StateEnvelope> | null;

View on GitHub (pinned to 01ad858492)

Solutions

  1. Validate ttlMs is a positive finite number before the call: Number.isFinite(ttlMs) && ttlMs > 0
  2. Fix the source computation (e.g. Math.max(minTtl, deadline - Date.now()))
  3. Coerce string config values with Number() and check for NaN

Example fix

// before
await state.acquireLock(key, ttlFromConfig);
// after
const ttl = Number(ttlFromConfig);
if (!Number.isFinite(ttl) || ttl <= 0) throw new Error(`bad ttl: ${ttlFromConfig}`);
await state.acquireLock(key, ttl);
Defensive patterns

Strategy: validation

Validate before calling

function validTtl(ttlMs: unknown): ttlMs is number { return typeof ttlMs === 'number' && Number.isFinite(ttlMs) && ttlMs > 0; }

Type guard

function isPositiveFiniteNumber(v: unknown): v is number { return typeof v === 'number' && Number.isFinite(v) && v > 0; }

Try / catch

try { await state.acquireLock(key, ttlMs); } catch (err) { if (/must be a positive finite number/.test((err as Error).message)) { ttlMs = DEFAULT_TTL_MS; return state.acquireLock(key, ttlMs); } throw err; }

Prevention

When it happens

Trigger: Calling acquireLock/extendLock with ttlMs = 0, negative values, NaN, or Infinity; computing ttlMs from a Date diff that is <= 0 (already expired) or from a misparsed config string.

Common situations: Config parsing where '30' (string) is not converted to a number; Date.now() - startedAt yielding 0 or negative on clock skew; a lock already expired so remaining TTL computes <= 0.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10). Data as JSON: /api/errors/6eb9767d810a5c31. Report an issue: GitHub.