paperclipai/paperclip · error

Durable PRP bootstrap TTL is invalid.

Error message

Durable PRP bootstrap TTL is invalid.

What it means

issueBootstrapTicket(ttlMs) validates that the TTL is an integer between 1,000 and 60,000 milliseconds inclusive. A non-integer, NaN, zero/negative, or over-60-second TTL fails this range check and the ticket is not created.

Source

Thrown at packages/paperclip-runner/src/control-plane/durable-prp-control-plane.ts:1736

      existing !== null &&
      canonicalJson(existing) !== canonicalJson(runAttachTemplate)
    ) {
      throw new Error("Durable PRP run attachment template conflicts.");
    }
    if (existing !== undefined && existing !== null) return;
    this.#store.state.runAttachTemplate = structuredClone(runAttachTemplate);
    this.#store.save();
  }

  issueBootstrapTicket(ttlMs = 5_000): string {
    this.#store.assertWritable();
    if (this.#store.state.warmTransition) {
      throw new Error(
        "Warm transition recovery requires its explicit one-use bootstrap capability.",
      );
    }
    if (!Number.isInteger(ttlMs) || ttlMs < 1_000 || ttlMs > 60_000) {
      throw new Error("Durable PRP bootstrap TTL is invalid.");
    }
    this.#pruneCredentials();
    const ticket = `bootstrap_${randomUUID()}`;
    const material = credentialMaterial(ticket);
    const expiresAtUnixMs = Date.now() + ttlMs;
    this.#store.state.tickets[material.credentialId] = {
      recordId: `bootstrap_ticket_${randomUUID()}`,
      credentialId: material.credentialId,
      authKeyDigest: `sha256:${material.authKey.toString("hex")}`,
      identity: structuredClone(this.#identity),
      runnerVersion: this.#expectedRunnerVersion,
      runnerDigest: this.#expectedRunnerDigest,
      expiresAt: new Date(expiresAtUnixMs).toISOString(),
      expiresAtUnixMs,
      usedAt: null,
    };
    this.#store.state.freshBootstraps += 1;
    this.#store.save();

View on GitHub (pinned to 01ad858492)

Solutions

  1. Pass an integer TTL in milliseconds within [1000, 60000], or omit the argument to use the 5000 ms default.
  2. Convert seconds to milliseconds explicitly (seconds * 1000) when the config is expressed in seconds.
  3. Clamp or validate the configured value before calling: Number.isInteger(ttl) && ttl >= 1000 && ttl <= 60000.
  4. Handle unset config by falling back to the default instead of passing Number(undefined) (NaN).

Example fix

// before
controlPlane.issueBootstrapTicket(Number(process.env.BOOTSTRAP_TTL_SECONDS)); // NaN / seconds unit
// after
const ttlMs = process.env.BOOTSTRAP_TTL_SECONDS
  ? Math.min(60000, Math.max(1000, Number(process.env.BOOTSTRAP_TTL_SECONDS) * 1000))
  : undefined;
controlPlane.issueBootstrapTicket(ttlMs);
Defensive patterns

Strategy: validation

Validate before calling

function safeTtlMs(raw: string | undefined, fallback = 5000): number {
  if (raw === undefined) return fallback;
  const n = Number(raw) * (raw.includes("ms") ? 1 : 1000); // beware unit mix
  return Number.isInteger(n) && n >= 1000 && n <= 60000 ? n : fallback;
}
controlPlane.issueBootstrapTicket(safeTtlMs(process.env.BOOTSTRAP_TTL));

Type guard

function isValidTtl(v: unknown): v is number {
  return typeof v === "number" && Number.isInteger(v) && v >= 1000 && v <= 60000;
}

Try / catch

try {
  ticket = controlPlane.issueBootstrapTicket(ttlMs);
} catch (err) {
  if (err instanceof Error && err.message.includes("bootstrap TTL is invalid")) {
    ticket = controlPlane.issueBootstrapTicket(); // default 5s
  } else throw err;
}

Prevention

When it happens

Trigger: Calling issueBootstrapTicket(0), issueBootstrapTicket(500), issueBootstrapTicket(120000), issueBootstrapTicket(2.5), or passing a value parsed from config as a string/NaN (e.g. issueBootstrapTicket(Number(process.env.TTL)) with TTL unset).

Common situations: TTL read from env/config in the wrong unit (seconds vs milliseconds); unset env var becoming NaN; a default of 0 used to mean "use default" instead of omitting the argument; fractional seconds passed through unconverted.

Understand the failure class

Background: "value must be between 0 and 1" / "out of range" / "must not be negative" errors: fixing range-validation failures across open-source libraries — this error's family across 42 libraries.

Related errors


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