can1357/oh-my-pi · error

tmpfiles option ttl must be a duration such as 1h

Error message

tmpfiles option ttl must be a duration such as 1h

What it means

tmpfilesTtlSeconds parses the tmpfiles `ttl` option as a duration string (number plus optional unit s/m/h/d, e.g. "1h", "90s", "2d") and throws when the format does not match the expected pattern. Unlike litterbox, tmpfiles accepts arbitrary durations, so the option is free-form — but it must still be a recognizable duration.

Source

Thrown at packages/coding-agent/src/blob-broker/uploaders-anonymous.ts:152

}

function createUguuUploader(config: DestinationRuntimeConfig): BlobUploader {
	return {
		destination: "uguu",
		async upload(request: BlobUploadRequest) {
			const { url } = await uploadTextUrl("uguu", config, UGUU_UPLOAD_URL, multipartFile(request, "files[]"));
			return publication("uguu", request, url, {
				expiresAt: Date.now() + 3 * HOUR_MS,
				remoteId: remoteName(url),
			});
		},
	};
}

function tmpfilesTtlSeconds(config: DestinationRuntimeConfig): number {
	const ttl = optionString(config, "ttl", "1h")?.trim().toLowerCase();
	const match = ttl?.match(/^(\d+(?:\.\d+)?)\s*(s|m|h|d)?$/);
	if (!match) throw new Error("tmpfiles option ttl must be a duration such as 1h");
	const amount = Number(match[1]);
	const multiplier = match[2] === "d" ? 86_400 : match[2] === "h" ? 3_600 : match[2] === "m" ? 60 : 1;
	const seconds = Math.round(amount * multiplier);
	if (!Number.isFinite(seconds) || seconds < 60 || seconds > 172_800) {
		throw new Error("tmpfiles option ttl must be between 60 seconds and 48 hours");
	}
	return seconds;
}

function recordValue(value: unknown): Readonly<Record<string, unknown>> | undefined {
	return typeof value === "object" && value !== null && !Array.isArray(value)
		? (value as Readonly<Record<string, unknown>>)
		: undefined;
}

function stringAtPath(value: unknown, path: string): string | undefined {
	let current = value;
	for (const segment of path.split(".").filter(Boolean)) {

View on GitHub (pinned to 9690622007)

Solutions

  1. Use a simple duration like "45m", "2h", or "1d" — one number plus one unit (s|m|h|d), unit optional (defaults to seconds).
  2. Spell units as single letters: m for minutes, h for hours, d for days — not "min" or "hour".
  3. Split compound durations ("1h30m") into a single unit ("90m").

Example fix

// before
{ "options": { "ttl": "1 hour" } }
// after
{ "options": { "ttl": "1h" } }
Defensive patterns

Strategy: validation

Validate before calling

const DURATION = /^\d+(?:\.\d+)?\s*(s|m|h|d)?$/;
const ttl = String(config.options.ttl ?? "1h");
if (!DURATION.test(ttl)) throw new Error(`tmpfiles ttl must match <number><s|m|h|d>, got: ${ttl}`);

Type guard

const isSimpleDuration = (v: unknown): v is string => typeof v === "string" && /^\d+(?:\.\d+)?\s*(s|m|h|d)?$/.test(v.trim().toLowerCase());

Try / catch

try {
  await uploadTmpfiles(config, req);
} catch (err) {
  if (String(err).includes("tmpfiles option ttl must be a duration")) {
    logger.warn("invalid ttl format; using 1h default");
    config.options.ttl = "1h";
  } else throw err;
}

Prevention

When it happens

Trigger: Configuring tmpfiles `ttl` with text that fails /^\d+(?:\.\d+)?\s*(s|m|h|d)?$/ — e.g. "1 hour" (word unit), "h1", "1w" (unsupported week unit), "", "24", or a plain number type rather than a duration string (which would first fail optionString at uploader-runtime.ts:56).

Common situations: Writing "1 week" or "1w" in config, omitting the unit while expecting defaults, or pasting "1h30m" compound durations which the simple regex cannot parse.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/295778e747d16ac8. Report an issue: GitHub.