import { HubSource, type HubSite, type ShardSource } from "./source"; import { buildSource, type SourceSpec } from "./sources"; // ─── Explicit, server-minted source handles ─── // // The 2026-07-28 revision removes protocol-level sessions and steers servers // that need cross-call state towards "explicit, server-minted handles passed as // ordinary tool arguments". This module is that: there is no active source and // nothing is persisted — every read tool takes an optional `source` handle and // each call resolves it independently. // // The handle is not an opaque token into a table. It IS the serialised spec, in // a canonical round-trippable form: // // default the CLI/env startup spec // local:/dir a composed public dir on disk // remote:https://site one deployed site origin // hub:https://hub federate every member of a hub // hub:https://hub#alpha,beta a hub subset ('#', so it can never collide // with a query string in the url) // // Opaque tokens would die with the process and mean nothing in a transcript; // these survive a restart, and a human reading `(corpus: remote:https://…)` in // a footer knows exactly what was searched. export type ResolvedSource = { // The canonical handle — what results echo and what a caller passes back. handle: string; spec: SourceSpec; source: ShardSource; // The live source's own human label (a hub's includes its subset size). label: string; // Anything worth saying about how the token was interpreted: a shorthand // that was normalised, a site token that resolved against the hub roster. notes: string[]; }; // The canonical handle for a spec. Round-trips through parseHandle. export function handleFor(spec: SourceSpec): string { switch (spec.kind) { case "local": return `local:${spec.dir}`; case "remote": return `remote:${spec.url}`; case "hub": return spec.sites && spec.sites.length > 0 ? `hub:${spec.url}#${spec.sites.join(",")}` : `hub:${spec.url}`; } } // Trailing slashes are meaningless to every source and would otherwise split // the instance cache ("remote:https://x/" vs "remote:https://x"). function trimUrl(u: string): string { return u.trim().replace(/\/+$/, ""); } function splitList(s: string): string[] { return s .split(",") .map((x) => x.trim()) .filter((x) => x !== ""); } // Parse an explicitly-prefixed handle. Returns undefined for anything that // isn't one of the canonical forms — the caller then tries the shorthands. function parseHandle(token: string): SourceSpec | undefined { const local = /^local:(.+)$/s.exec(token); if (local) return { kind: "local", dir: local[1].trim() }; const remote = /^remote:(.+)$/s.exec(token); if (remote) return { kind: "remote", url: trimUrl(remote[1]) }; const hub = /^hub:(.+)$/s.exec(token); if (hub) { const rest = hub[1].trim(); const hash = rest.indexOf("#"); if (hash === -1) return { kind: "hub", url: trimUrl(rest) }; const sites = splitList(rest.slice(hash + 1)); const url = trimUrl(rest.slice(0, hash)); return sites.length > 0 ? { kind: "hub", url, sites } : { kind: "hub", url }; } return undefined; } type OriginKind = "hub" | "remote"; // Classify an origin by its corpus.json: a federated hub ships `sites[]`, a // single site ships `channels[]`. Unreachable/unparseable falls back to a // single site, which is the safe guess (a hub that can't be read federates // nothing anyway). async function probeOriginKind(origin: string): Promise { try { const res = await fetch(`${origin}/corpus.json`); if (res.ok) { const j = (await res.json()) as { sites?: unknown[]; channels?: unknown[] }; if (Array.isArray(j.sites)) return "hub"; } } catch { // unreachable — treat it as a single site } return "remote"; } // Resolves source handles to live sources, caching one instance per canonical // handle for the life of the process. Holds no "current" source: `resolve()` is // a pure function of its argument plus the startup spec, so two calls in the // same session can read two different corpora and neither can surprise the // other. export class SourceRegistry { readonly defaultSpec: SourceSpec; readonly defaultHandle: string; private build: (spec: SourceSpec) => ShardSource; private probe: (origin: string) => Promise; private instances = new Map(); private rosters = new Map>(); constructor( defaultSpec: SourceSpec, opts: { build?: (spec: SourceSpec) => ShardSource; // Injectable so tests can resolve a bare origin without a live fetch. probe?: (origin: string) => Promise; } = {}, ) { this.defaultSpec = defaultSpec; this.defaultHandle = handleFor(defaultSpec); this.build = opts.build ?? buildSource; this.probe = opts.probe ?? probeOriginKind; } // Wrap an already-built ShardSource as a one-source registry: `default` // resolves to that exact instance. Lets `createServer(someSource)` keep // working unchanged — the tests' in-memory stubs, and any caller that has a // source but no spec. static forSource(source: ShardSource): SourceRegistry { const spec = parseHandle(source.label) ?? { kind: "local" as const, dir: source.label, }; const reg = new SourceRegistry(spec); reg.instances.set(reg.defaultHandle, source); // A pinned instance may not match what `build` would produce for its spec // (a stub, or a hub whose label carries its subset size), so pin the label // too — the handle is what callers pass back, the label is what humans read. reg.pinnedLabel = source.label; return reg; } private pinnedLabel?: string; // The live source for a canonical handle, built once and reused. This is the // cache that makes per-call source selection affordable: without it every // call would rebuild (and so re-fetch every corpus.json). private instanceFor(handle: string, spec: SourceSpec): ShardSource { const hit = this.instances.get(handle); if (hit) return hit; const built = this.build(spec); this.instances.set(handle, built); return built; } // The hub a bare site token resolves against: the startup spec when it is a // hub. (There is no remembered hub — a token for some other hub has to name // it, e.g. `hub:https://other#alpha`.) hubUrl(): string | undefined { return this.defaultSpec.kind === "hub" ? this.defaultSpec.url : undefined; } // A hub's member roster, fetched once per hub url. Cached as the promise so // concurrent resolutions share one fetch. listHubSites(hubUrl?: string): Promise { const url = hubUrl ?? this.hubUrl(); if (!url) return Promise.resolve([]); const hit = this.rosters.get(url); if (hit) return hit; const p = (async () => { const src = this.instanceFor(handleFor({ kind: "hub", url }), { kind: "hub", url, }); if (src instanceof HubSource) return src.listSites(); // A stubbed factory may return a non-HubSource that still lists sites. const maybe = src as unknown as { listSites?: () => Promise }; return typeof maybe.listSites === "function" ? maybe.listSites() : []; })().catch((e: unknown) => { // Don't cache a failure — a transient network blip shouldn't poison the // roster for the life of the process. this.rosters.delete(url); throw e; }); this.rosters.set(url, p); return p; } // Resolve a `source` argument to a live source. An absent/blank token — or // the literal "default" — is the startup spec. Everything else is either a // canonical handle or one of the shorthands, which are normalised to // canonical and echoed back so the model learns the canonical form. // // Throws only when a token names something that cannot exist (an unknown hub // member). Reachability is NOT checked here — a read tool surfaces that as // its own failure, and `resolve_source` checks it deliberately. async resolve(token?: unknown): Promise { const raw = typeof token === "string" ? token.trim() : ""; const notes: string[] = []; if (raw === "" || raw.toLowerCase() === "default") { return this.finish(this.defaultSpec, notes); } const direct = parseHandle(raw); if (direct) return this.finish(direct, notes); // ── shorthands ── // A bare origin: probe corpus.json to tell a hub from a single site. if (/^https?:\/\//i.test(raw)) { const url = trimUrl(raw); const kind = await this.probe(url); notes.push( `"${raw}" probed as a ${kind === "hub" ? "federated hub" : "single site"}`, ); return this.finish( kind === "hub" ? { kind: "hub", url } : { kind: "remote", url }, notes, ); } // `site:` / `sites:` — hub members, by siteId or title. const site = /^site:(.+)$/s.exec(raw); const sites = /^sites:(.+)$/s.exec(raw); if (site || sites) { const tokens = site ? [site[1].trim()] : splitList(sites![1]); return this.finish(await this.resolveSiteTokens(tokens, notes), notes); } // An explicit path — anything that looks like a directory. if (raw.startsWith("/") || raw.startsWith("./") || raw.startsWith("../")) { return this.finish({ kind: "local", dir: raw }, notes); } // A bare word: a hub member's siteId or title, when there is a hub to ask. if (this.hubUrl()) { return this.finish(await this.resolveSiteTokens([raw], notes), notes); } throw new Error( `unrecognised source "${raw}". Use a handle — default, local:, ` + `remote:, hub:, or hub:# — or a bare ` + `site URL.`, ); } // Resolve hub-member tokens (siteId or title, case-insensitive) to a spec. // One member becomes a plain remote source, which keeps full group/alias // support; several become a hub subset. private async resolveSiteTokens( tokens: string[], notes: string[], ): Promise { const hubUrl = this.hubUrl(); if (!hubUrl) { throw new Error( `no hub context to resolve the site token(s) [${tokens.join(", ")}] ` + `against — this server was not started against a hub. Name one ` + `explicitly: hub:#${tokens.join(",")}`, ); } const members = await this.listHubSites(hubUrl); const find = (t: string): HubSite | undefined => { const want = t.toLowerCase(); return members.find( (m) => m.siteId.toLowerCase() === want || m.title.toLowerCase() === want, ); }; const resolved: HubSite[] = []; const unknown: string[] = []; for (const t of tokens) { const m = find(t); if (m) resolved.push(m); else unknown.push(t); } if (resolved.length === 0) { throw new Error( `unknown hub site(s): ${unknown.join(", ")}. Known: ` + members.map((m) => m.siteId).join(", "), ); } if (unknown.length > 0) { notes.push(`unknown site token(s) skipped: ${unknown.join(", ")}`); } if (resolved.length === 1) { notes.push( `site "${resolved[0].siteId}" resolved to its origin ${resolved[0].url}`, ); return { kind: "remote", url: trimUrl(resolved[0].url) }; } return { kind: "hub", url: hubUrl, sites: resolved.map((m) => m.siteId) }; } private finish(spec: SourceSpec, notes: string[]): ResolvedSource { const handle = handleFor(spec); const source = this.instanceFor(handle, spec); return { handle, spec, source, label: handle === this.defaultHandle && this.pinnedLabel ? this.pinnedLabel : source.label, notes, }; } }