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
- Pass an integer TTL in milliseconds within [1000, 60000], or omit the argument to use the 5000 ms default.
- Convert seconds to milliseconds explicitly (seconds * 1000) when the config is expressed in seconds.
- Clamp or validate the configured value before calling: Number.isInteger(ttl) && ttl >= 1000 && ttl <= 60000.
- 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
- Standardize TTL config in milliseconds and convert at the config boundary.
- Clamp configured TTLs into [1000, 60000] before calling.
- Treat unset/NaN config as "use default" rather than passing Number(undefined).
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
- Expected ACPX permission mode is invalid
- Verified runtime executable target descriptor is invalid
- codex_run_attach_invalid
- ${label} must be a positive finite number
- Invalid status '${String(rawStatus)}'. Must be one of: ${PLU
AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10).
Data as JSON: /api/errors/9b4dd0ee8b6963e5.
Report an issue: GitHub.