can1357/oh-my-pi · error

artifact:// URL requires a numeric ID: artifact://0

Error message

artifact:// URL requires a numeric ID: artifact://0

What it means

parseArtifactId (packages/coding-agent/src/internal-urls/artifact-protocol.ts:30) throws when an artifact:// URL has an empty host, meaning no artifact ID was supplied. Artifact IDs are per-session numeric counters (e.g. artifact://0, artifact://3), so an ID is mandatory. The message embeds the offending URL form for clarity.

Source

Thrown at packages/coding-agent/src/internal-urls/artifact-protocol.ts:30

import * as fs from "node:fs/promises";
import * as path from "node:path";
import { isEnoent } from "@oh-my-pi/pi-utils";
import { artifactsDirsFromRegistry } from "./registry-helpers";
import type { InternalResource, InternalUrl, ProtocolHandler, ResolveContext, UrlCompletion } from "./types";

const MAX_INLINE_ARTIFACT_BYTES = 8 * 1024 * 1024;

/** Filesystem location for a session artifact, resolved without materializing its content. */
export interface ResolvedArtifactFile {
	id: string;
	path: string;
	size: number;
}

function parseArtifactId(url: InternalUrl): string {
	const id = url.rawHost || url.hostname;
	if (!id) {
		throw new Error("artifact:// URL requires a numeric ID: artifact://0");
	}
	if (!/^\d+$/.test(id)) {
		throw new Error(`artifact:// ID must be numeric, got: ${id}`);
	}
	return id;
}

/** Resolve an `artifact://` URL to its backing file without reading artifact bytes. */
export async function resolveArtifactFile(url: InternalUrl, context?: ResolveContext): Promise<ResolvedArtifactFile> {
	const id = parseArtifactId(url);

	// Artifact ids are per-session counters; in multi-session hosts the same
	// id exists in several dirs. Pin resolution to the calling session's
	// artifacts dir first so `artifact://3` means *this* session's #3.
	const dirs = artifactsDirsFromRegistry();
	const pinnedDir = context?.localProtocolOptions?.getArtifactsDir?.() ?? null;
	if (pinnedDir) {
		const pinnedIndex = dirs.indexOf(pinnedDir);

View on GitHub (pinned to 9690622007)

Solutions

  1. Supply the numeric artifact ID in the URL host: artifact://0, artifact://3, etc.
  2. List available IDs first (ArtifactProtocolHandler.complete, or directory listing of the session artifacts dir with files named <id>.<ext>) and pick a valid one.
  3. Fix the interpolation site so the ID variable is defined before building the URL.
  4. If you meant file content rather than an artifact, use a different scheme (e.g. file:// or agent://).

Example fix

// before
const url = `artifact://${id}`; // id === undefined -> artifact://
// after
if (id === undefined || id === null) throw new Error("artifact ID required");
const url = `artifact://${id}`;
Defensive patterns

Strategy: validation

Validate before calling

if (!id || typeof id !== "string" || id.trim() === "") {
  throw new Error("artifact URL requires a numeric ID");
}
const url = new URL(`artifact://${id}`);

Type guard

function hasArtifactId(id: string | undefined | null): id is string {
  return typeof id === "string" && id.length > 0;
}

Try / catch

try {
  const res = await resolveUrl(new URL(`artifact://${id}`));
} catch (err) {
  if (err instanceof Error && err.message.includes("requires a numeric ID")) {
    // id was empty; prompt user or list available IDs via completion
  } else throw err;
}

Prevention

When it happens

Trigger: Resolving the bare URL artifact:// (no host), e.g. new URL("artifact://") or a tool expanding an empty/undefined ID into the scheme; also artifact:/// (empty host with a leading slash path).

Common situations: Template interpolation with an undefined/null variable: `artifact://${id}` with id undefined; a script stripping the ID when slicing a longer URL string; an LLM emitting artifact:// without an ID in a tool call.

Related errors


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