diff --git a/host/js/bundle.js b/host/js/bundle.js index 401879f..00b6cc1 100644 --- a/host/js/bundle.js +++ b/host/js/bundle.js @@ -53126,6 +53126,31 @@ async function openStorage(path3) { } // environment.ts +var Started = defineDoc({ + kind: "board.started", + version: 1, + checkpointWhen: () => false, + scope: "conversation", + history: "latest", + fork: "initial", + initial: () => ({}) +}); +function conversationChains(harness2, context) { + const cache = /* @__PURE__ */ new Map(); + const chain2 = (id) => { + let result = cache.get(id); + if (!result) { + result = (async () => { + const record = await harness2.commit((tx) => tx.conversation(id), context); + const up = record?.owner?.conversationId ?? record?.parent?.conversationId ?? (await harness2.snapshot(Started, id, context))?.by; + return [String(id), ...up === void 0 ? [] : await chain2(up)]; + })(); + cache.set(id, result); + } + return result; + }; + return chain2; +} var ops = Deno.core.ops; var ok = (value2) => ({ ok: true, value: value2 }); var path2 = (cwd, input) => { @@ -53137,6 +53162,7 @@ var path2 = (cwd, input) => { return "/" + parts.join("/"); }; function environment(cwd = "/", conversation) { + const conversations = Array.isArray(conversation) ? conversation : conversation === void 0 ? [] : [String(conversation)]; const call = (kind, p, bytes2 = []) => ops.op_file(kind, path2(cwd, p), bytes2); const env = { id: "world", @@ -53215,7 +53241,7 @@ function environment(cwd = "/", conversation) { } }; try { - const rid = ops.op_exec(command, options?.cwd ?? cwd, options?.timeout, conversation === void 0 ? void 0 : String(conversation)); + const rid = ops.op_exec(command, options?.cwd ?? cwd, options?.timeout, void 0, void 0, conversations); const decoders = { stdout: new TextDecoder(), stderr: new TextDecoder() }; let result; while (true) { @@ -54411,8 +54437,9 @@ var harness = await Harness.open(storage, { settings: { followUpMode: "all", get extensions() { return registry.snapshot().installed(); } }, - env: (target) => environment(target.cwd ?? "/", target.conversationId) + env: async (target) => environment(target.cwd ?? "/", await chain(target.conversationId)) }, BACKGROUND_CONTEXT); +var chain = conversationChains(harness, BACKGROUND_CONTEXT); var root = await harness.root(BACKGROUND_CONTEXT, { agent: { model: { provider: "openai", modelId: model2.id }, thinkingLevel: "low", cwd: "/" } }); var storedAgent = await harness.snapshot(AgentDoc, root.id, BACKGROUND_CONTEXT); if (Array.isArray(storedAgent?.extensions) && storedAgent.extensions.length === 2 && [CodingTools, CredentialRequests].every((extension) => storedAgent.extensions.includes(extension.name))) { diff --git a/host/js/chain-test-bundle.js b/host/js/chain-test-bundle.js new file mode 100644 index 0000000..c65cb71 --- /dev/null +++ b/host/js/chain-test-bundle.js @@ -0,0 +1,22480 @@ +var __create = Object.create; +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __getProtoOf = Object.getPrototypeOf; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __commonJS = (cb, mod) => function __require() { + try { + return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; + } catch (e) { + throw mod = 0, e; + } +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( + // If the importer is in node compatibility mode or this is not an ESM + // file that has been converted to a CommonJS file using a Babel- + // compatible transform (i.e. "__esModule" has not been set), then set + // "default" to the CommonJS "module.exports" for node compatibility. + isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, + mod +)); + +// node_modules/partial-json/dist/options.js +var require_options = __commonJS({ + "node_modules/partial-json/dist/options.js"(exports) { + "use strict"; + Object.defineProperty(exports, "__esModule", { value: true }); + exports.Allow = exports.ALL = exports.COLLECTION = exports.ATOM = exports.SPECIAL = exports.INF = exports._INFINITY = exports.INFINITY = exports.NAN = exports.BOOL = exports.NULL = exports.OBJ = exports.ARR = exports.NUM = exports.STR = void 0; + exports.STR = 1; + exports.NUM = 2; + exports.ARR = 4; + exports.OBJ = 8; + exports.NULL = 16; + exports.BOOL = 32; + exports.NAN = 64; + exports.INFINITY = 128; + exports._INFINITY = 256; + exports.INF = exports.INFINITY | exports._INFINITY; + exports.SPECIAL = exports.NULL | exports.BOOL | exports.INF | exports.NAN; + exports.ATOM = exports.STR | exports.NUM | exports.SPECIAL; + exports.COLLECTION = exports.ARR | exports.OBJ; + exports.ALL = exports.ATOM | exports.COLLECTION; + exports.Allow = { STR: exports.STR, NUM: exports.NUM, ARR: exports.ARR, OBJ: exports.OBJ, NULL: exports.NULL, BOOL: exports.BOOL, NAN: exports.NAN, INFINITY: exports.INFINITY, _INFINITY: exports._INFINITY, INF: exports.INF, SPECIAL: exports.SPECIAL, ATOM: exports.ATOM, COLLECTION: exports.COLLECTION, ALL: exports.ALL }; + exports.default = exports.Allow; + } +}); + +// node_modules/partial-json/dist/index.js +var require_dist = __commonJS({ + "node_modules/partial-json/dist/index.js"(exports) { + "use strict"; + var __createBinding = exports && exports.__createBinding || (Object.create ? (function(o, m, k, k2) { + if (k2 === void 0) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { + return m[k]; + } }; + } + Object.defineProperty(o, k2, desc); + }) : (function(o, m, k, k2) { + if (k2 === void 0) k2 = k; + o[k2] = m[k]; + })); + var __exportStar = exports && exports.__exportStar || function(m, exports2) { + for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports2, p)) __createBinding(exports2, m, p); + }; + Object.defineProperty(exports, "__esModule", { value: true }); + exports.Allow = exports.MalformedJSON = exports.PartialJSON = exports.parseJSON = exports.parse = void 0; + var options_1 = require_options(); + Object.defineProperty(exports, "Allow", { enumerable: true, get: function() { + return options_1.Allow; + } }); + __exportStar(require_options(), exports); + var PartialJSON = class extends Error { + }; + exports.PartialJSON = PartialJSON; + var MalformedJSON = class extends Error { + }; + exports.MalformedJSON = MalformedJSON; + function parseJSON(jsonString, allowPartial = options_1.Allow.ALL) { + if (typeof jsonString !== "string") { + throw new TypeError(`expecting str, got ${typeof jsonString}`); + } + if (!jsonString.trim()) { + throw new Error(`${jsonString} is empty`); + } + return _parseJSON(jsonString.trim(), allowPartial); + } + exports.parseJSON = parseJSON; + var _parseJSON = (jsonString, allow) => { + const length = jsonString.length; + let index = 0; + const markPartialJSON = (msg) => { + throw new PartialJSON(`${msg} at position ${index}`); + }; + const throwMalformedError = (msg) => { + throw new MalformedJSON(`${msg} at position ${index}`); + }; + const parseAny = () => { + skipBlank(); + if (index >= length) + markPartialJSON("Unexpected end of input"); + if (jsonString[index] === '"') + return parseStr(); + if (jsonString[index] === "{") + return parseObj(); + if (jsonString[index] === "[") + return parseArr(); + if (jsonString.substring(index, index + 4) === "null" || options_1.Allow.NULL & allow && length - index < 4 && "null".startsWith(jsonString.substring(index))) { + index += 4; + return null; + } + if (jsonString.substring(index, index + 4) === "true" || options_1.Allow.BOOL & allow && length - index < 4 && "true".startsWith(jsonString.substring(index))) { + index += 4; + return true; + } + if (jsonString.substring(index, index + 5) === "false" || options_1.Allow.BOOL & allow && length - index < 5 && "false".startsWith(jsonString.substring(index))) { + index += 5; + return false; + } + if (jsonString.substring(index, index + 8) === "Infinity" || options_1.Allow.INFINITY & allow && length - index < 8 && "Infinity".startsWith(jsonString.substring(index))) { + index += 8; + return Infinity; + } + if (jsonString.substring(index, index + 9) === "-Infinity" || options_1.Allow._INFINITY & allow && 1 < length - index && length - index < 9 && "-Infinity".startsWith(jsonString.substring(index))) { + index += 9; + return -Infinity; + } + if (jsonString.substring(index, index + 3) === "NaN" || options_1.Allow.NAN & allow && length - index < 3 && "NaN".startsWith(jsonString.substring(index))) { + index += 3; + return NaN; + } + return parseNum(); + }; + const parseStr = () => { + const start = index; + let escape = false; + index++; + while (index < length && (jsonString[index] !== '"' || escape && jsonString[index - 1] === "\\")) { + escape = jsonString[index] === "\\" ? !escape : false; + index++; + } + if (jsonString.charAt(index) == '"') { + try { + return JSON.parse(jsonString.substring(start, ++index - Number(escape))); + } catch (e) { + throwMalformedError(String(e)); + } + } else if (options_1.Allow.STR & allow) { + try { + return JSON.parse(jsonString.substring(start, index - Number(escape)) + '"'); + } catch (e) { + return JSON.parse(jsonString.substring(start, jsonString.lastIndexOf("\\")) + '"'); + } + } + markPartialJSON("Unterminated string literal"); + }; + const parseObj = () => { + index++; + skipBlank(); + const obj2 = {}; + try { + while (jsonString[index] !== "}") { + skipBlank(); + if (index >= length && options_1.Allow.OBJ & allow) + return obj2; + const key = parseStr(); + skipBlank(); + index++; + try { + const value2 = parseAny(); + obj2[key] = value2; + } catch (e) { + if (options_1.Allow.OBJ & allow) + return obj2; + else + throw e; + } + skipBlank(); + if (jsonString[index] === ",") + index++; + } + } catch (e) { + if (options_1.Allow.OBJ & allow) + return obj2; + else + markPartialJSON("Expected '}' at end of object"); + } + index++; + return obj2; + }; + const parseArr = () => { + index++; + const arr2 = []; + try { + while (jsonString[index] !== "]") { + arr2.push(parseAny()); + skipBlank(); + if (jsonString[index] === ",") { + index++; + } + } + } catch (e) { + if (options_1.Allow.ARR & allow) { + return arr2; + } + markPartialJSON("Expected ']' at end of array"); + } + index++; + return arr2; + }; + const parseNum = () => { + if (index === 0) { + if (jsonString === "-") + throwMalformedError("Not sure what '-' is"); + try { + return JSON.parse(jsonString); + } catch (e) { + if (options_1.Allow.NUM & allow) + try { + return JSON.parse(jsonString.substring(0, jsonString.lastIndexOf("e"))); + } catch (e2) { + } + throwMalformedError(String(e)); + } + } + const start = index; + if (jsonString[index] === "-") + index++; + while (jsonString[index] && ",]}".indexOf(jsonString[index]) === -1) + index++; + if (index == length && !(options_1.Allow.NUM & allow)) + markPartialJSON("Unterminated number literal"); + try { + return JSON.parse(jsonString.substring(start, index)); + } catch (e) { + if (jsonString.substring(start, index) === "-") + markPartialJSON("Not sure what '-' is"); + try { + return JSON.parse(jsonString.substring(start, jsonString.lastIndexOf("e"))); + } catch (e2) { + throwMalformedError(String(e2)); + } + } + }; + const skipBlank = () => { + while (index < length && " \n\r ".includes(jsonString[index])) { + index++; + } + }; + return parseAny(); + }; + var parse = parseJSON; + exports.parse = parse; + } +}); + +// node_modules/@earendil-works/pi-telemetry/dist/metrics.js +function createTypedMetrics(metrics, schemas) { + const definitions = /* @__PURE__ */ new Map(); + for (const schema of schemas) { + for (const [name, definition] of Object.entries(schema.metrics ?? {})) + definitions.set(name, definition); + } + return { + counter: (name) => metrics.counter(name, definitions.get(name)), + upDownCounter: (name) => metrics.upDownCounter(name, definitions.get(name)), + histogram: (name) => metrics.histogram(name, definitions.get(name)) + }; +} + +// node_modules/@earendil-works/pi-telemetry/dist/noop.js +async function startNoopSpan(_options, callback) { + return callback(noopTelemetrySpan); +} +var noopCounter = Object.freeze({ add: () => { +} }); +var noopUpDownCounter = Object.freeze({ add: () => { +} }); +var noopHistogram = Object.freeze({ record: () => { +} }); +var NOOP_TELEMETRY_METRICS = Object.freeze({ + counter: () => noopCounter, + upDownCounter: () => noopUpDownCounter, + histogram: () => noopHistogram +}); +var NOOP_TELEMETRY_PROPAGATION = Object.freeze({ + inject: () => void 0, + extract: () => NOOP_TELEMETRY_CONTEXT +}); +var noopTelemetrySpan = { + metrics: NOOP_TELEMETRY_METRICS, + propagation: NOOP_TELEMETRY_PROPAGATION, + startSpan: startNoopSpan, + addLink: () => { + }, + addEvent: () => { + }, + setAttributes: () => { + }, + setStatus: () => { + } +}; +Object.freeze(noopTelemetrySpan); +var NOOP_TELEMETRY_CONTEXT = noopTelemetrySpan; + +// node_modules/@earendil-works/pi-telemetry/dist/index.js +function defineTelemetrySchema(schema) { + return schema; +} +function bindTypedSpanStarter(telemetryContext) { + const startSpan = (name, attributes, callback) => telemetryContext.startSpan({ name, attributes }, (span) => callback(span, bindTypedSpanStarter(span))); + return startSpan; +} +function createTypedSpanStarter(telemetryContext, _schemas) { + return bindTypedSpanStarter(telemetryContext); +} + +// node_modules/@earendil-works/chord/dist/context/index.js +var ABORT_SIGNAL_CONTEXT_KEY = Object.freeze({ + token: /* @__PURE__ */ Symbol("chord.abortSignal") +}); +var TELEMETRY_CONTEXT_KEY = Object.freeze({ + token: /* @__PURE__ */ Symbol("chord.telemetry") +}); +var BaseContext = class { + get abortSignal() { + return this.value(ABORT_SIGNAL_CONTEXT_KEY); + } + get telemetry() { + return this.value(TELEMETRY_CONTEXT_KEY) ?? NOOP_TELEMETRY_CONTEXT; + } +}; +var EmptyContext = class extends BaseContext { + #name; + constructor(name) { + super(); + this.#name = name; + } + value(_key) { + return void 0; + } + toString() { + return this.#name; + } +}; +var ContextValue = class extends BaseContext { + #parent; + #key; + #value; + constructor(parent, key, value2) { + super(); + this.#parent = parent; + this.#key = key; + this.#value = value2; + } + value(key) { + if (key.token === this.#key.token) + return this.#value; + return this.#parent.value(key); + } + toString() { + return `${this.#parent}.WithValue(${this.#key.token.description ?? "anonymous"})`; + } +}; +var BACKGROUND_CONTEXT = new EmptyContext("[Context BACKGROUND_CONTEXT]"); +var TODO_CONTEXT = new EmptyContext("[Context TODO_CONTEXT]"); +function withContextValue(key, value2, parent) { + return new ContextValue(parent, key, value2); +} +function withTelemetry(telemetry, context) { + return withContextValue(TELEMETRY_CONTEXT_KEY, telemetry, context); +} +function withAbortSignal(signal, context) { + const parentSignal = context.abortSignal; + const combined = parentSignal === void 0 ? signal : AbortSignal.any([parentSignal, signal]); + return withContextValue(ABORT_SIGNAL_CONTEXT_KEY, combined, context); +} +function withoutAbortSignal(context) { + return withContextValue(ABORT_SIGNAL_CONTEXT_KEY, void 0, context); +} +function awaitWithContext(promise, context) { + const signal = context.abortSignal; + if (signal === void 0) + return promise; + if (signal.aborted) + return Promise.reject(abortError(signal)); + return new Promise((resolve2, reject2) => { + const onAbort = () => reject2(abortError(signal)); + signal.addEventListener("abort", onAbort, { once: true }); + void promise.then((value2) => { + signal.removeEventListener("abort", onAbort); + resolve2(value2); + }, (error) => { + signal.removeEventListener("abort", onAbort); + reject2(error); + }); + }); +} +function abortError(signal) { + const reason = signal.reason; + return reason instanceof Error ? reason : new DOMException("The operation was aborted", "AbortError"); +} + +// node_modules/@earendil-works/pi-ai/dist/utils/event-stream.js +var FifoQueue = class { + incoming = []; + outgoing = []; + get length() { + return this.incoming.length + this.outgoing.length; + } + enqueue(value2) { + this.incoming.push(value2); + } + dequeue() { + if (this.outgoing.length === 0) { + while (this.incoming.length > 0) { + this.outgoing.push(this.incoming.pop()); + } + } + return this.outgoing.pop(); + } +}; +var EventStream = class { + queue = new FifoQueue(); + waiting = new FifoQueue(); + done = false; + finalResultPromise; + resolveFinalResult; + isComplete; + extractResult; + constructor(isComplete, extractResult) { + this.isComplete = isComplete; + this.extractResult = extractResult; + this.finalResultPromise = new Promise((resolve2) => { + this.resolveFinalResult = resolve2; + }); + } + push(event) { + if (this.done) + return; + if (this.isComplete(event)) { + this.done = true; + this.resolveFinalResult(this.extractResult(event)); + } + const waiter = this.waiting.dequeue(); + if (waiter) { + waiter({ value: event, done: false }); + } else { + this.queue.enqueue(event); + } + } + end(result) { + this.done = true; + if (result !== void 0) { + this.resolveFinalResult(result); + } + while (this.waiting.length > 0) { + const waiter = this.waiting.dequeue(); + waiter({ value: void 0, done: true }); + } + } + async *[Symbol.asyncIterator]() { + while (true) { + if (this.queue.length > 0) { + yield this.queue.dequeue(); + } else if (this.done) { + return; + } else { + const result = await new Promise((resolve2) => this.waiting.enqueue(resolve2)); + if (result.done) + return; + yield result.value; + } + } + } + result() { + return this.finalResultPromise; + } +}; +var AssistantMessageEventStream = class extends EventStream { + #startedAt = Date.now(); + #startedAtMonotonic = performance.now(); + constructor() { + super((event) => event.type === "done" || event.type === "error", (event) => { + if (event.type === "done") { + return event.message; + } else if (event.type === "error") { + return event.error; + } + throw new Error("Unexpected event type for final result"); + }); + } + push(event) { + if (event.type === "done") + this.#time(event.message); + else if (event.type === "error") + this.#time(event.error); + super.push(event); + } + end(result) { + if (result !== void 0) + this.#time(result); + super.end(result); + } + #time(message) { + if (this.done || message.durationMs !== void 0 || message.timestamp < this.#startedAt) + return; + message.durationMs = Math.max(0, Math.round(performance.now() - this.#startedAtMonotonic)); + } +}; +function createAssistantMessageEventStream() { + return new AssistantMessageEventStream(); +} + +// node_modules/@earendil-works/pi-ai/dist/api/lazy.js +function createSetupErrorMessage(model2, error, timestamp) { + return { + role: "assistant", + content: [], + api: model2.api, + provider: model2.provider, + model: model2.id, + usage: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + totalTokens: 0, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } + }, + stopReason: "error", + errorMessage: error instanceof Error ? error.message : String(error), + timestamp + }; +} +function hasResult(source) { + return typeof source.result === "function"; +} +async function forwardStream(target, source, observer, observeResult) { + let result; + for await (const event of source) { + target.push(event); + observer?.onEvent?.(event); + if (event.type === "done") + result = event.message; + else if (event.type === "error") + result = event.error; + } + if (hasResult(source)) + result = await source.result(); + target.end(result); + observeResult(result); +} +function lazyStream(model2, setup, observer) { + const startedAt = Date.now(); + const outer = new AssistantMessageEventStream(); + let observedResult = false; + const observeResult = (result) => { + if (observedResult) + return; + observedResult = true; + observer?.onResult?.(result); + }; + setup().then((inner) => forwardStream(outer, inner, observer, observeResult)).catch((error) => { + const message = createSetupErrorMessage(model2, error, startedAt); + const event = { type: "error", reason: "error", error: message }; + outer.push(event); + observer?.onEvent?.(event); + outer.end(message); + observeResult(message); + }); + return outer; +} + +// node_modules/@earendil-works/pi-ai/dist/auth/context.js +var __rewriteRelativeImportExtension = function(path2, preserveJsx) { + if (typeof path2 === "string" && /^\.\.?\//.test(path2)) { + return path2.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function(m, tsx, d, ext, cm) { + return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : d + ext + "." + cm.toLowerCase() + "js"; + }); + } + return path2; +}; +var importNodeModule = (specifier) => import(__rewriteRelativeImportExtension(specifier)); +function getProcessEnv() { + const proc = globalThis.process; + return proc?.env; +} +function defaultProviderAuthContext() { + return { + async env(name) { + const value2 = getProcessEnv()?.[name]; + return typeof value2 === "string" && value2.trim().length > 0 ? value2 : void 0; + }, + async fileExists(path2) { + try { + const fs2 = await importNodeModule("node:fs/promises"); + let resolved = path2; + if (resolved.startsWith("~")) { + const os = await importNodeModule("node:os"); + resolved = os.homedir() + resolved.slice(1); + } + await fs2.access(resolved); + return true; + } catch { + return false; + } + } + }; +} + +// node_modules/@earendil-works/pi-ai/dist/utils/abort.js +function abortReason(signal) { + if (signal.reason !== void 0) + return signal.reason; + const error = new Error("The operation was aborted"); + error.name = "AbortError"; + return error; +} +function operationSignal(signal) { + return signal ?? new AbortController().signal; +} +function raceWithAbortSignal(operation, signal) { + if (signal.aborted) { + void operation.catch(() => { + }); + return Promise.reject(abortReason(signal)); + } + return new Promise((resolve2, reject2) => { + let settled = false; + const cleanup = () => signal.removeEventListener("abort", onAbort); + const onAbort = () => { + if (settled) + return; + settled = true; + cleanup(); + reject2(abortReason(signal)); + }; + signal.addEventListener("abort", onAbort, { once: true }); + void operation.then((value2) => { + if (settled) + return; + settled = true; + cleanup(); + resolve2(value2); + }, (error) => { + if (settled) + return; + settled = true; + cleanup(); + reject2(error); + }); + if (signal.aborted) + onAbort(); + }); +} + +// node_modules/@earendil-works/pi-ai/dist/auth/credential-store.js +var InMemoryCredentialStore = class { + credentials = /* @__PURE__ */ new Map(); + chains = /* @__PURE__ */ new Map(); + /** Serialize tasks per provider id without releasing the chain before active work settles. */ + enqueue(providerId, task, options) { + const signal = operationSignal(options?.signal); + const previous = this.chains.get(providerId) ?? Promise.resolve(); + const queued = (async () => { + await previous.catch(() => { + }); + signal.throwIfAborted(); + return task(); + })(); + const tail = queued.catch(() => { + }); + this.chains.set(providerId, tail); + void tail.then(() => { + if (this.chains.get(providerId) === tail) + this.chains.delete(providerId); + }); + return raceWithAbortSignal(queued, signal); + } + async read(providerId, options) { + options?.signal?.throwIfAborted(); + return this.credentials.get(providerId); + } + async list(options) { + options?.signal?.throwIfAborted(); + return [...this.credentials].map(([providerId, credential]) => ({ providerId, type: credential.type })); + } + modify(providerId, fn, options) { + return this.enqueue(providerId, async () => { + const current = this.credentials.get(providerId); + const next = await fn(current); + options?.signal?.throwIfAborted(); + if (next !== void 0) + this.credentials.set(providerId, next); + return next ?? current; + }, options); + } + delete(providerId, options) { + return this.enqueue(providerId, async () => { + this.credentials.delete(providerId); + }, options); + } +}; + +// node_modules/@earendil-works/pi-ai/dist/utils/diagnostics.js +function formatThrownValue(value2) { + if (value2 instanceof Error) + return value2.message || value2.name; + if (typeof value2 === "string") + return value2; + return String(value2); +} + +// node_modules/@earendil-works/pi-ai/dist/utils/models-error.js +var ModelsError = class extends Error { + code; + constructor(code, message, options) { + super(withCauseDetail(message, options?.cause), options); + this.name = "ModelsError"; + this.code = code; + } +}; +function withCauseDetail(message, cause) { + if (cause === void 0 || cause === null) + return message; + const detail = formatThrownValue(cause).trim(); + if (!detail || message.includes(detail)) + return message; + return `${message}: ${detail}`; +} + +// node_modules/@earendil-works/pi-ai/dist/auth/resolve.js +function resolveProviderAuth(provider2, credentials, authContext, overrides) { + const signal = operationSignal(overrides?.signal); + return raceWithAbortSignal(resolveProviderAuthWithSignal(provider2, credentials, authContext, overrides, signal), signal); +} +async function resolveProviderAuthWithSignal(provider2, credentials, authContext, overrides, signal) { + signal.throwIfAborted(); + const requestAuthContext = overrides?.env ? overlayEnvAuthContext(authContext, overrides.env) : authContext; + if (overrides?.apiKey !== void 0 && provider2.auth.apiKey) { + return resolveApiKey(requestAuthContext, provider2.auth.apiKey, provider2.id, { + type: "api_key", + key: overrides.apiKey, + env: overrides.env + }, signal); + } + const stored = await readCredential(credentials, provider2.id, signal); + if (stored) { + if (stored.type === "oauth" && provider2.auth.oauth) { + return resolveStoredOAuth(credentials, provider2.id, provider2.auth.oauth, stored, signal, overrides?.minOAuthValidityMs); + } + if (stored.type === "api_key" && provider2.auth.apiKey) { + const credential = overrides?.env ? { ...stored, env: { ...stored.env, ...overrides.env } } : stored; + return resolveApiKey(requestAuthContext, provider2.auth.apiKey, provider2.id, credential, signal); + } + return void 0; + } + return provider2.auth.apiKey ? resolveApiKey(requestAuthContext, provider2.auth.apiKey, provider2.id, void 0, signal) : void 0; +} +function overlayEnvAuthContext(base, env) { + return { + env: async (name) => env[name] || await base.env(name), + fileExists: (path2) => base.fileExists(path2) + }; +} +var DEFAULT_OAUTH_MINIMUM_VALIDITY_MS = 5 * 60 * 1e3; +var DEFAULT_OAUTH_REFRESH_TIMEOUT_MS = 15e3; +async function refreshStoredOAuthCredential(credentials, providerId, oauth, needsRefresh, signal) { + let post; + const lockWait = new AbortController(); + const cancelLockWait = () => lockWait.abort(signal.reason); + signal.addEventListener("abort", cancelLockWait, { once: true }); + if (signal.aborted) + cancelLockWait(); + try { + post = await credentials.modify(providerId, async (current) => { + signal.removeEventListener("abort", cancelLockWait); + signal.throwIfAborted(); + if (current?.type !== "oauth") + return void 0; + if (!needsRefresh(current)) + return void 0; + try { + return await oauth.refresh(current, AbortSignal.timeout(DEFAULT_OAUTH_REFRESH_TIMEOUT_MS)); + } catch (error) { + throw new ModelsError("oauth", `OAuth refresh failed for ${providerId}`, { cause: error }); + } + }, { signal: lockWait.signal }); + } catch (error) { + if (error instanceof ModelsError) + throw error; + signal.throwIfAborted(); + throw new ModelsError("auth", `Credential store modify failed for ${providerId}`, { cause: error }); + } finally { + signal.removeEventListener("abort", cancelLockWait); + } + return post?.type === "oauth" ? post : void 0; +} +async function resolveStoredOAuth(credentials, providerId, oauth, stored, signal, minOAuthValidityMs) { + const minimumValidityMs = Math.max(DEFAULT_OAUTH_MINIMUM_VALIDITY_MS, minOAuthValidityMs ?? 0); + const expiresSoon = (credential2) => Date.now() + minimumValidityMs >= credential2.expires; + let credential = stored; + if (expiresSoon(credential)) { + const post = await refreshStoredOAuthCredential(credentials, providerId, oauth, expiresSoon, signal); + if (!post) + return void 0; + credential = post; + if (minOAuthValidityMs !== void 0 && expiresSoon(credential)) { + throw new ModelsError("oauth", `OAuth refresh returned a token that expires too soon for ${providerId}`); + } + } + try { + return { auth: await oauth.toAuth(credential), source: "OAuth" }; + } catch (error) { + throw new ModelsError("oauth", `OAuth auth derivation failed for ${providerId}`, { cause: error }); + } +} +async function resolveApiKey(authContext, apiKey, providerId, credential, signal) { + try { + return await apiKey.resolve({ ctx: authContext, credential, signal }); + } catch (error) { + throw new ModelsError("auth", `API key auth failed for provider ${providerId}`, { cause: error }); + } +} +async function readCredential(credentials, providerId, signal) { + try { + return await credentials.read(providerId, { signal }); + } catch (error) { + throw new ModelsError("auth", `Credential store read failed for ${providerId}`, { cause: error }); + } +} + +// node_modules/@earendil-works/pi-ai/dist/models-store.js +var InMemoryModelsStore = class { + entries = /* @__PURE__ */ new Map(); + async read(providerId, options) { + options?.signal?.throwIfAborted(); + const entry2 = this.entries.get(providerId); + return entry2 ? structuredClone(entry2) : void 0; + } + async write(providerId, entry2, options) { + options?.signal?.throwIfAborted(); + this.entries.set(providerId, structuredClone(entry2)); + } + async delete(providerId, options) { + options?.signal?.throwIfAborted(); + this.entries.delete(providerId); + } +}; + +// node_modules/@earendil-works/pi-ai/dist/utils/text.js +function contentText(content, separator = "\n") { + if (typeof content === "string") + return content; + return content.filter((block) => block.type === "text").map((block) => block.text).join(separator); +} +function getSystemMessageText(message) { + const parts = [contentText(message.content)]; + for (const text of Object.values(message.sections ?? {})) { + if (text !== null) + parts.push(text); + } + return parts.filter((part) => part.length > 0).join("\n\n"); +} +function ownedSlice(text, start, end = text.length) { + return `${text.slice(start, end)} `.slice(0, -1); +} + +// node_modules/@earendil-works/pi-ai/dist/telemetry.js +var provider = { type: "string", description: "Provider identifier.", required: true }; +var model = { type: "string", description: "Requested model identifier.", required: true }; +var stop = { + type: "string", + description: "Normalized stop reason.", + values: ["pending", "stop", "length", "toolUse", "error", "aborted", "deferred"] +}; +var request = { + description: "One logical model request, including authentication and provider setup.", + parents: { kind: "any" }, + startAttributes: { + provider, + api: { type: "string", description: "API identifier.", required: true }, + model, + reasoning: { type: "string", description: "Scalar requested reasoning level.", required: false } + }, + endAttributes: { + stop, + "usage.input": { type: "number", description: "Uncached input tokens." }, + "usage.output": { type: "number", description: "Output tokens." }, + "usage.cache_read": { type: "number", description: "Cache-read tokens." }, + "usage.cache_write": { type: "number", description: "Cache-write tokens." }, + "cost.total": { type: "number", description: "Total request cost in USD." }, + "response.id": { type: "string", description: "Provider response identifier.", cardinality: "high" } + }, + events: { + "pi.ai.first_chunk": { description: "First normalized stream event delivered.", attributes: {} }, + "pi.ai.block": { + description: "A completed content block; content is never recorded.", + attributes: { + type: { + type: "string", + description: "Block kind.", + values: ["text", "thinking", "tool_call"], + required: true + }, + index: { type: "number", description: "Content block index.", required: true }, + chars: { type: "number", description: "UTF-16 character count.", required: true }, + tool: { type: "string", description: "Tool name, for tool-call blocks only.", required: false } + } + } + }, + status: { + default: "ok", + errorWhen: "Stop reason is error: provider_error with at most 500 error-message characters. Aborted stays ok." + } +}; +var AI_TELEMETRY_SCHEMA = defineTelemetrySchema({ + version: 1, + spans: { + "pi.ai.request": request, + "pi.ai.poll": { ...request, description: "One deferred-response poll, not the deferred request's lifetime." } + }, + metrics: { + "pi.ai.request.duration": { + kind: "histogram", + description: "Logical request or poll duration.", + unit: "ms", + attributes: { provider, model, stop: { ...stop, required: true } } + }, + "pi.ai.first_chunk.duration": { + kind: "histogram", + description: "Time to the first normalized stream event.", + unit: "ms", + attributes: { provider, model } + }, + "pi.ai.tokens": { + kind: "counter", + description: "Reported model token usage; pending and deferred polls are excluded.", + unit: "token", + attributes: { + provider, + model, + kind: { + type: "string", + description: "Token usage category.", + values: ["input", "output", "cache_read", "cache_write"], + required: true + } + } + }, + "pi.ai.cost": { + kind: "counter", + description: "Reported model cost; pending and deferred polls are excluded.", + unit: "USD", + attributes: { provider, model } + } + } +}); +function createInstruments(adapter) { + const metrics = createTypedMetrics(adapter, [AI_TELEMETRY_SCHEMA]); + return { + requestDuration: metrics.histogram("pi.ai.request.duration"), + firstChunkDuration: metrics.histogram("pi.ai.first_chunk.duration"), + tokens: metrics.counter("pi.ai.tokens"), + cost: metrics.counter("pi.ai.cost") + }; +} +var instrumentCache = /* @__PURE__ */ new WeakMap(); +function observeModelRequest(requestedModel, options, poll) { + const started = performance.now(); + const attributes = { provider: requestedModel.provider, model: requestedModel.id }; + const adapter = options?.telemetry?.metrics ?? NOOP_TELEMETRY_METRICS; + let instruments = instrumentCache.get(adapter); + if (!instruments) { + instruments = createInstruments(adapter); + instrumentCache.set(adapter, instruments); + } + const { requestDuration, firstChunkDuration, tokens, cost } = instruments; + let span; + let finish; + let first = true; + const toolChars = /* @__PURE__ */ new Map(); + const finished = new Promise((resolve2) => { + finish = resolve2; + }); + const run2 = (activeSpan) => { + span = activeSpan; + return finished; + }; + const startAttributes = { + ...attributes, + api: requestedModel.api, + ...typeof options?.reasoning === "string" ? { reasoning: options.reasoning } : {} + }; + const start = createTypedSpanStarter(options?.telemetry ?? NOOP_TELEMETRY_CONTEXT, [AI_TELEMETRY_SCHEMA]); + if (poll) + void start("pi.ai.poll", startAttributes, run2); + else + void start("pi.ai.request", startAttributes, run2); + return { + onEvent(event) { + if (first) { + first = false; + span.addEvent("pi.ai.first_chunk"); + firstChunkDuration.record(performance.now() - started, attributes, span); + } + if (event.type === "toolcall_delta") { + toolChars.set(event.contentIndex, (toolChars.get(event.contentIndex) ?? 0) + event.delta.length); + } else if (event.type === "text_end" || event.type === "thinking_end") { + span.addEvent("pi.ai.block", { + type: event.type === "text_end" ? "text" : "thinking", + index: event.contentIndex, + chars: event.content.length + }); + } else if (event.type === "toolcall_end") { + const chars = toolChars.get(event.contentIndex) ?? 0; + toolChars.delete(event.contentIndex); + span.addEvent("pi.ai.block", { + type: "tool_call", + index: event.contentIndex, + chars, + tool: event.toolCall.name + }); + } + }, + onResult(message) { + if (message) { + span.setAttributes({ + stop: message.stopReason, + "usage.input": message.usage.input, + "usage.output": message.usage.output, + "usage.cache_read": message.usage.cacheRead, + "usage.cache_write": message.usage.cacheWrite, + "cost.total": message.usage.cost.total, + "response.id": message.responseId + }); + if (message.stopReason === "error") { + span.setStatus({ + status: "error", + error: { name: "provider_error", message: ownedSlice(message.errorMessage ?? "", 0, 500) } + }); + } + requestDuration.record(performance.now() - started, { ...attributes, stop: message.stopReason }, span); + if (!poll || message.stopReason !== "pending" && message.stopReason !== "deferred") { + tokens.add(message.usage.input, { ...attributes, kind: "input" }); + tokens.add(message.usage.output, { ...attributes, kind: "output" }); + tokens.add(message.usage.cacheRead, { ...attributes, kind: "cache_read" }); + tokens.add(message.usage.cacheWrite, { ...attributes, kind: "cache_write" }); + cost.add(message.usage.cost.total, attributes); + } + } + finish(); + } + }; +} + +// node_modules/@earendil-works/pi-ai/dist/utils/identity.js +var PI_ATTRIBUTION = { name: "pi", url: "https://pi.dev", categories: ["cli-agent"] }; +var OPENCODE_HOST = "opencode.ai"; +function mergeClientIdentity(...sources) { + let merged; + for (const source of sources) { + if (!source) + continue; + merged = { ...merged }; + if (source.userAgent !== void 0) + merged.userAgent = source.userAgent; + if (source.attribution !== void 0) + merged.attribution = source.attribution; + if (source.version !== void 0) + merged.version = source.version; + } + return merged; +} +function matchesHost(baseUrl, expectedHost) { + try { + return new URL(baseUrl).hostname === expectedHost; + } catch { + return false; + } +} +function isOpenCode(target) { + return target.provider === "opencode" || target.provider === "opencode-go" || matchesHost(target.baseUrl, OPENCODE_HOST); +} +function getOpenCodeSessionHeaders(target, sessionId) { + return sessionId && isOpenCode(target) ? { "x-opencode-session": sessionId } : void 0; +} +function isMutableModels(models2) { + return "setProvider" in models2 && "deleteProvider" in models2 && "clearProviders" in models2; +} +function withClientIdentity(models2, identity) { + const withIdentity = (options) => ({ ...options, identity: mergeClientIdentity(identity, options?.identity) }); + const wrapped = { + getProviders: () => models2.getProviders(), + getProvider: (id) => models2.getProvider(id), + getModels: (provider2) => models2.getModels(provider2), + getModel: (provider2, id) => models2.getModel(provider2, id), + getModelsOfType: (type, provider2) => models2.getModelsOfType(type, provider2), + getModelOfType: (type, provider2, id) => models2.getModelOfType(type, provider2, id), + getAllModels: (provider2) => models2.getAllModels(provider2), + refresh: (options) => models2.refresh(options), + checkAuth: (providerId, options) => models2.checkAuth(providerId, options), + getAvailable: (providerId, options) => models2.getAvailable(providerId, options), + getAvailableOfType: (type, providerId, options) => models2.getAvailableOfType(type, providerId, options), + getAllAvailable: (providerId, options) => models2.getAllAvailable(providerId, options), + getAuth: models2.getAuth.bind(models2), + login: (providerId, type, interaction, options) => models2.login(providerId, type, interaction, options), + logout: (providerId, options) => models2.logout(providerId, options), + stream: (model2, context, options) => models2.stream(model2, context, withIdentity(options)), + complete: (model2, context, options) => models2.complete(model2, context, withIdentity(options)), + streamSimple: (model2, context, options) => models2.streamSimple(model2, context, withIdentity(options)), + completeSimple: (model2, context, options) => models2.completeSimple(model2, context, withIdentity(options)), + streamDeferred: (model2, handle, options) => models2.streamDeferred(model2, handle, withIdentity(options)), + fetchDeferred: (model2, handle, options) => models2.fetchDeferred(model2, handle, withIdentity(options)), + cancelDeferred: (model2, handle, options) => models2.cancelDeferred(model2, handle, withIdentity(options)), + generateImages: (model2, context, options) => models2.generateImages(model2, context, withIdentity(options)), + classify: (model2, context, options) => models2.classify(model2, context, withIdentity(options)) + }; + if (!isMutableModels(models2)) + return wrapped; + const mutable = { + ...wrapped, + setProvider: (provider2) => models2.setProvider(provider2), + deleteProvider: (id) => models2.deleteProvider(id), + clearProviders: () => models2.clearProviders() + }; + return mutable; +} + +// node_modules/@earendil-works/pi-ai/dist/utils/model-operations.js +function getModelType(model2) { + return model2.type ?? "chat"; +} +function isModelType(model2, type) { + return getModelType(model2) === type; +} +function assertChatModel(model2) { + if (!isModelType(model2, "chat")) { + throw new ModelsError("provider", `Model ${model2.provider}/${model2.id} is not a chat model`); + } +} +function assertImageModel(model2) { + if (!isModelType(model2, "image")) { + throw new ModelsError("provider", `Model ${model2.provider}/${model2.id} is not an image model`); + } +} +function assertClassifierModel(model2) { + if (!isModelType(model2, "classifier")) { + throw new ModelsError("provider", `Model ${model2.provider}/${model2.id} is not a classifier model`); + } +} +function assertClassifierInputSupported(model2, context) { + if (context.images?.length && !model2.input.includes("image")) { + throw new ModelsError("provider", `Model ${model2.provider}/${model2.id} does not accept image input`); + } +} +function imageErrorResult(model2, error, aborted = false) { + return { + api: model2.api, + provider: model2.provider, + model: model2.id, + output: [], + stopReason: aborted ? "aborted" : "error", + errorMessage: error instanceof Error ? error.message : String(error), + timestamp: Date.now() + }; +} +function classifierErrorResult(model2, error, aborted = false) { + return { + api: model2.api, + provider: model2.provider, + model: model2.id, + answers: {}, + stopReason: aborted ? "aborted" : "error", + errorMessage: error instanceof Error ? error.message : String(error), + timestamp: Date.now() + }; +} + +// node_modules/@earendil-works/pi-ai/dist/utils/transcript.js +function createInitialSystemMessage(systemPrompt, tools) { + const hasSystemPrompt = systemPrompt !== void 0 && systemPrompt.length > 0; + const hasTools = tools !== void 0 && tools.length > 0; + if (!hasSystemPrompt && !hasTools) + return void 0; + return { + role: "system", + content: systemPrompt ?? "", + ...hasTools ? { toolsAdded: tools } : {}, + timestamp: 0 + }; +} +function normalizeContext(context) { + const initialMessage = createInitialSystemMessage(context.systemPrompt, context.tools); + const messages = initialMessage ? [initialMessage, ...context.messages] : context.messages; + return { messages }; +} +function isSystemMessage(message) { + return message.role === "system"; +} +function getCurrentTools(messages) { + const tools = /* @__PURE__ */ new Map(); + for (const message of messages) { + if (!isSystemMessage(message)) + continue; + for (const tool of message.toolsRemoved ?? []) + tools.delete(tool.name); + for (const tool of message.toolsAdded ?? []) + tools.set(tool.name, tool); + } + return [...tools.values()]; +} +function toToolDeclaration(tool) { + return { + name: tool.name, + description: tool.description, + parameters: JSON.parse(JSON.stringify(tool.parameters)), + ...tool.constrainedSampling === void 0 ? {} : { constrainedSampling: tool.constrainedSampling } + }; +} +function declarationsEqual(left, right) { + return JSON.stringify(toToolDeclaration(left)) === JSON.stringify(toToolDeclaration(right)); +} + +// node_modules/@earendil-works/pi-ai/dist/models.js +var KNOWN_MODEL_TYPES = { chat: true, image: true, classifier: true }; +function hasKnownModelType(model2) { + return Object.hasOwn(KNOWN_MODEL_TYPES, getModelType(model2)); +} +function withKnownModelTypes(entry2) { + return { ...entry2, models: entry2.models.filter(hasKnownModelType) }; +} +function mergeHeaders(base, override) { + if (!base && !override) + return void 0; + const merged = { ...base }; + for (const [name, value2] of Object.entries(override ?? {})) { + const lowerName = name.toLowerCase(); + for (const existingName of Object.keys(merged)) { + if (existingName.toLowerCase() === lowerName) + delete merged[existingName]; + } + merged[name] = value2; + } + return merged; +} +var ModelsImpl = class { + providers = /* @__PURE__ */ new Map(); + credentials; + modelsStore; + authContext; + identity; + refreshGenerations = /* @__PURE__ */ new Map(); + refreshControllers = /* @__PURE__ */ new Map(); + publicationChains = /* @__PURE__ */ new Map(); + constructor(options) { + this.credentials = options?.credentials ?? new InMemoryCredentialStore(); + this.modelsStore = options?.modelsStore ?? new InMemoryModelsStore(); + this.authContext = options?.authContext ?? defaultProviderAuthContext(); + this.identity = options?.identity; + } + setProvider(provider2) { + this.supersedeProviderRefresh(provider2.id); + this.providers.set(provider2.id, provider2); + } + deleteProvider(id) { + this.supersedeProviderRefresh(id); + this.providers.delete(id); + } + clearProviders() { + for (const id of /* @__PURE__ */ new Set([...this.providers.keys(), ...this.refreshControllers.keys()])) { + this.supersedeProviderRefresh(id); + } + this.providers.clear(); + } + getProviders() { + return Array.from(this.providers.values()); + } + getProvider(id) { + return this.providers.get(id); + } + getModels(provider2) { + if (provider2 !== void 0) { + const entry2 = this.providers.get(provider2); + if (!entry2) + return []; + try { + return entry2.getModels(); + } catch { + return []; + } + } + const models2 = []; + for (const entry2 of this.providers.values()) { + try { + models2.push(...entry2.getModels()); + } catch { + } + } + return models2; + } + getAllModels(provider2) { + if (provider2 !== void 0) { + const entry2 = this.providers.get(provider2); + if (!entry2) + return []; + try { + return entry2.getAllModels?.() ?? entry2.getModels(); + } catch { + return []; + } + } + const models2 = []; + for (const entry2 of this.providers.values()) { + try { + models2.push(...entry2.getAllModels?.() ?? entry2.getModels()); + } catch { + } + } + return models2; + } + getModelsOfType(type, provider2) { + return this.getAllModels(provider2).filter((model2) => isModelType(model2, type)); + } + getModel(provider2, id) { + return this.getModels(provider2).find((model2) => model2.id === id); + } + getModelOfType(type, provider2, id) { + return this.getModelsOfType(type, provider2).find((model2) => model2.id === id); + } + supersedeProviderRefresh(providerId) { + const generation = (this.refreshGenerations.get(providerId) ?? 0) + 1; + this.refreshGenerations.set(providerId, generation); + const previous = this.refreshControllers.get(providerId); + if (previous) { + this.refreshControllers.delete(providerId); + previous.abort(); + } + return generation; + } + beginProviderRefresh(providerId) { + const generation = this.supersedeProviderRefresh(providerId); + const controller = new AbortController(); + this.refreshControllers.set(providerId, controller); + return { generation, controller }; + } + publishProviderModels(providerId, generation, signal, publication) { + const previous = this.publicationChains.get(providerId) ?? Promise.resolve(); + const queued = (async () => { + await previous.catch(() => { + }); + if (signal.aborted || this.refreshGenerations.get(providerId) !== generation) + return false; + if (publication.persist === null) { + await this.modelsStore.delete(providerId, { signal }); + } else if (publication.persist !== void 0) { + await this.modelsStore.write(providerId, structuredClone(publication.persist), { signal }); + } + if (signal.aborted || this.refreshGenerations.get(providerId) !== generation) + return false; + publication.update?.(); + return true; + })(); + const tail = queued.catch(() => { + }); + this.publicationChains.set(providerId, tail); + void tail.then(() => { + if (this.publicationChains.get(providerId) === tail) + this.publicationChains.delete(providerId); + }); + return raceWithAbortSignal(queued, signal); + } + async runProviderRefreshPhase(provider2, credential, allowNetwork, force, generation, signal) { + const stored = await this.modelsStore.read(provider2.id, { signal }); + await provider2.refreshModels({ + credential, + stored: stored ? withKnownModelTypes(structuredClone(stored)) : void 0, + publish: (publication) => this.publishProviderModels(provider2.id, generation, signal, publication), + allowNetwork, + force: allowNetwork ? force : void 0, + signal, + baseUrl: provider2.baseUrl + }); + } + async refresh(options = {}) { + const allowNetwork = options.allowNetwork ?? true; + const callerSignal = operationSignal(options.signal); + const errors = /* @__PURE__ */ new Map(); + if (callerSignal.aborted) + return { aborted: true, errors }; + const selected = options.providers ? new Set(options.providers) : void 0; + const refreshable = Array.from(this.providers.values()).filter((provider2) => provider2.refreshModels !== void 0 && (!selected || selected.has(provider2.id))); + const refresh = Promise.all(refreshable.map(async (provider2) => { + const { generation, controller } = this.beginProviderRefresh(provider2.id); + const signal = AbortSignal.any([callerSignal, controller.signal]); + const operation = (async () => { + let storedCredential; + let credentialError; + try { + storedCredential = await this.readCredential(provider2.id, signal); + } catch (error) { + credentialError = error; + } + await this.runProviderRefreshPhase(provider2, storedCredential, false, void 0, generation, signal); + if (credentialError !== void 0) + throw credentialError; + if (!allowNetwork || signal.aborted) + return; + const credential = await this.resolveRefreshCredential(provider2, storedCredential, signal); + if (!credential) + return; + await this.runProviderRefreshPhase(provider2, credential, true, options.force, generation, signal); + })(); + try { + await raceWithAbortSignal(operation, signal); + } catch (error) { + if (!signal.aborted) { + errors.set(provider2.id, error instanceof Error ? error : new ModelsError("model_source", `Model refresh failed for ${provider2.id}`, { cause: error })); + } + } finally { + if (this.refreshControllers.get(provider2.id) === controller) { + this.refreshControllers.delete(provider2.id); + } + } + })); + try { + await raceWithAbortSignal(refresh, callerSignal); + } catch (error) { + if (!callerSignal.aborted) + throw error; + } + return { aborted: callerSignal.aborted, errors: new Map(errors) }; + } + async resolveRefreshCredential(provider2, stored, signal) { + if (stored?.type === "oauth") { + const oauth = provider2.auth.oauth; + if (!oauth) + return void 0; + if (Date.now() < stored.expires) + return stored; + if (signal.aborted) + return void 0; + return refreshStoredOAuthCredential(this.credentials, provider2.id, oauth, (current) => Date.now() >= current.expires, signal); + } + const apiKey = provider2.auth.apiKey; + if (!apiKey) + return void 0; + const credential = stored?.type === "api_key" ? stored : void 0; + const result = await apiKey.resolve({ ctx: this.authContext, credential, signal }); + if (!result) + return void 0; + return { type: "api_key", key: result.auth.apiKey, env: result.env }; + } + async readCredential(providerId, signal) { + try { + return await this.credentials.read(providerId, { signal }); + } catch (error) { + throw new ModelsError("auth", `Credential store read failed for ${providerId}`, { cause: error }); + } + } + async checkProviderAuth(provider2, credential, signal) { + if (credential?.type === "oauth") { + return provider2.auth.oauth ? { source: "OAuth", type: "oauth" } : void 0; + } + const apiKey = provider2.auth.apiKey; + if (!apiKey) + return void 0; + if (apiKey.check) { + try { + return await apiKey.check({ + ctx: this.authContext, + credential: credential?.type === "api_key" ? credential : void 0, + signal + }); + } catch (error) { + throw new ModelsError("auth", `API key auth check failed for provider ${provider2.id}`, { cause: error }); + } + } + const resolution = await resolveProviderAuth(provider2, this.credentials, this.authContext, { signal }); + return resolution ? { source: resolution.source, type: "api_key" } : void 0; + } + checkAuth(providerId, options) { + const signal = operationSignal(options?.signal); + const check = (async () => { + signal.throwIfAborted(); + const provider2 = this.providers.get(providerId); + if (!provider2) + return void 0; + return this.checkProviderAuth(provider2, await this.readCredential(providerId, signal), signal); + })(); + return raceWithAbortSignal(check, signal); + } + async getAuthenticatedProviders(providerId, signal) { + signal.throwIfAborted(); + const providers = providerId ? [this.providers.get(providerId)].filter((entry2) => entry2 !== void 0) : this.getProviders(); + const checks = await Promise.all(providers.map(async (provider2) => { + const credential = await this.readCredential(provider2.id, signal); + return { provider: provider2, credential, auth: await this.checkProviderAuth(provider2, credential, signal) }; + })); + return checks.filter((entry2) => entry2.auth !== void 0); + } + getAvailable(providerId, options) { + const signal = operationSignal(options?.signal); + const available = (async () => { + const providers = await this.getAuthenticatedProviders(providerId, signal); + return providers.flatMap(({ provider: provider2, credential }) => { + const models2 = provider2.getModels(); + return provider2.filterModels?.(models2, credential) ?? models2; + }); + })(); + return raceWithAbortSignal(available, signal); + } + async getAvailableOfType(type, providerId, options) { + return (await this.getAllAvailable(providerId, options)).filter((model2) => isModelType(model2, type)); + } + getAllAvailable(providerId, options) { + const signal = operationSignal(options?.signal); + const available = (async () => { + const providers = await this.getAuthenticatedProviders(providerId, signal); + return providers.flatMap(({ provider: provider2, credential }) => { + const models2 = provider2.getAllModels?.() ?? provider2.getModels(); + if (provider2.filterAllModels) + return provider2.filterAllModels(models2, credential); + if (!provider2.filterModels) + return models2; + const availableChatIds = new Set(provider2.filterModels(provider2.getModels(), credential).map((model2) => model2.id)); + return models2.filter((model2) => !isModelType(model2, "chat") || availableChatIds.has(model2.id)); + }); + })(); + return raceWithAbortSignal(available, signal); + } + async getAuth(providerOrModel, overrides) { + const signal = operationSignal(overrides?.signal); + const providerId = typeof providerOrModel === "string" ? providerOrModel : providerOrModel.provider; + const provider2 = this.providers.get(providerId); + if (!provider2) + return void 0; + const result = await resolveProviderAuth(provider2, this.credentials, this.authContext, { ...overrides, signal }); + if (!result || typeof providerOrModel === "string" || !providerOrModel.headers) + return result; + return { + ...result, + auth: { + ...result.auth, + headers: mergeHeaders(result.auth.headers, providerOrModel.headers) + } + }; + } + async login(providerId, type, interaction, options) { + const signal = operationSignal(interaction.signal); + signal.throwIfAborted(); + const provider2 = this.providers.get(providerId); + if (!provider2) + throw new ModelsError("provider", `Unknown provider: ${providerId}`); + const method = type === "oauth" ? provider2.auth.oauth : provider2.auth.apiKey; + if (!method?.login) { + throw new ModelsError("auth", `${provider2.name} does not support ${type} login`); + } + const loginOperation = method.login({ ...interaction, signal }, options); + const credential = await raceWithAbortSignal(loginOperation, signal); + let mutationStarted = false; + let markMutationStarted; + const started = new Promise((resolve2) => { + markMutationStarted = resolve2; + }); + const mutation = this.credentials.modify(providerId, async () => { + mutationStarted = true; + markMutationStarted?.(); + return credential; + }, { signal }); + void mutation.catch(() => { + }); + try { + await new Promise((resolve2, reject2) => { + const onAbort = () => { + if (!mutationStarted) + reject2(signal.reason); + }; + signal.addEventListener("abort", onAbort, { once: true }); + void Promise.race([started, mutation]).then(() => { + signal.removeEventListener("abort", onAbort); + resolve2(); + }, (error) => { + signal.removeEventListener("abort", onAbort); + reject2(error); + }); + if (signal.aborted) + onAbort(); + }); + await mutation; + } catch (error) { + signal.throwIfAborted(); + throw new ModelsError("auth", `Credential store modify failed for ${providerId}`, { cause: error }); + } + return credential; + } + async logout(providerId, options) { + const signal = operationSignal(options?.signal); + signal.throwIfAborted(); + try { + await this.credentials.delete(providerId, { signal }); + } catch (error) { + signal.throwIfAborted(); + throw new ModelsError("auth", `Credential store delete failed for ${providerId}`, { cause: error }); + } + } + requireProvider(model2) { + const provider2 = this.providers.get(model2.provider); + if (!provider2) { + throw new ModelsError("provider", `Unknown provider: ${model2.provider}`); + } + return provider2; + } + requireChatProvider(model2) { + assertChatModel(model2); + return this.requireProvider(model2); + } + async applyAuth(model2, options) { + this.requireProvider(model2); + const resolution = await this.getAuth(model2, { + apiKey: options?.apiKey, + env: options?.env, + signal: options?.signal + }); + if (!resolution) { + throw new ModelsError("auth", `Provider is not configured: ${model2.provider}`); + } + const auth = resolution.auth; + const apiKey = options?.apiKey ?? auth.apiKey; + const requestModel = auth.baseUrl ? { ...model2, baseUrl: auth.baseUrl } : model2; + const sessionId = options && "sessionId" in options && typeof options.sessionId === "string" ? options.sessionId : void 0; + let headers = mergeHeaders(mergeHeaders(getOpenCodeSessionHeaders(requestModel, sessionId), auth.headers), options?.headers); + if (options?.transformHeaders) + headers = await options.transformHeaders(headers ?? {}); + const env = resolution.env || options?.env ? { ...resolution.env ?? {}, ...options?.env ?? {} } : void 0; + const identity = mergeClientIdentity(this.identity, options?.identity); + const { transformHeaders: _transformHeaders, ...providerOptions } = options ?? {}; + const requestOptions = { + ...providerOptions, + apiKey, + headers, + env, + ...identity === void 0 ? {} : { identity } + }; + return { requestModel, requestOptions }; + } + stream(model2, context, options) { + const transcript = normalizeContext(context); + return lazyStream(model2, async () => { + const provider2 = this.requireChatProvider(model2); + const { requestModel, requestOptions } = await this.applyAuth(model2, options); + return provider2.stream(requestModel, transcript, requestOptions); + }, observeModelRequest(model2, options, false)); + } + async complete(model2, context, options) { + return this.stream(model2, context, options).result(); + } + streamSimple(model2, context, options) { + const transcript = normalizeContext(context); + return lazyStream(model2, async () => { + const provider2 = this.requireChatProvider(model2); + const { requestModel, requestOptions } = await this.applyAuth(model2, options); + return provider2.streamSimple(requestModel, transcript, requestOptions); + }, observeModelRequest(model2, options, false)); + } + async completeSimple(model2, context, options) { + return this.streamSimple(model2, context, options).result(); + } + streamDeferred(model2, handle, options) { + return lazyStream(model2, async () => { + const provider2 = this.requireChatProvider(model2); + if (!provider2.fetchDeferred) { + throw new ModelsError("provider", `Provider ${model2.provider} does not support deferred responses`); + } + const { requestModel, requestOptions } = await this.applyAuth(model2, options); + return provider2.fetchDeferred(requestModel, handle, requestOptions); + }, observeModelRequest(model2, options, true)); + } + async fetchDeferred(model2, handle, options) { + return this.streamDeferred(model2, handle, options).result(); + } + async cancelDeferred(model2, handle, options) { + const provider2 = this.requireChatProvider(model2); + if (!provider2.cancelDeferred) { + throw new ModelsError("provider", `Provider ${model2.provider} does not support deferred responses`); + } + const { requestModel, requestOptions } = await this.applyAuth(model2, options); + await provider2.cancelDeferred(requestModel, handle, requestOptions); + } + async generateImages(model2, context, options) { + try { + assertImageModel(model2); + const provider2 = this.requireProvider(model2); + if (!provider2.generateImages) { + throw new ModelsError("provider", `Provider ${model2.provider} does not support image generation`); + } + const { requestModel, requestOptions } = await this.applyAuth(model2, options); + return await provider2.generateImages(requestModel, context, requestOptions); + } catch (error) { + return imageErrorResult(model2, error, options?.signal?.aborted); + } + } + async classify(model2, context, options) { + try { + assertClassifierModel(model2); + assertClassifierInputSupported(model2, context); + const provider2 = this.requireProvider(model2); + if (!provider2.classify) { + throw new ModelsError("provider", `Provider ${model2.provider} does not support classification`); + } + const { requestModel, requestOptions } = await this.applyAuth(model2, options); + return await provider2.classify(requestModel, context, requestOptions); + } catch (error) { + return classifierErrorResult(model2, error, options?.signal?.aborted); + } + } +}; +function createModels(options) { + return new ModelsImpl(options); +} +function createProvider(input) { + const single = input.api && typeof input.api.stream === "function" ? input.api : void 0; + const byApi = single || !input.api ? void 0 : input.api; + const images = input.images; + const classifiers = input.classifiers; + const streams = single ? [single] : Object.values(byApi ?? {}).filter((entry2) => entry2 !== void 0); + const imageImplementations = Object.values(images ?? {}).filter((entry2) => entry2 !== void 0); + const classifierImplementations = Object.values(classifiers ?? {}).filter((entry2) => entry2 !== void 0); + if (streams.length === 0 && imageImplementations.length === 0 && classifierImplementations.length === 0) { + throw new Error(`Provider ${input.id}: at least one of "api", "images", or "classifiers" is required.`); + } + const baselineModels = input.models; + let dynamicModels = []; + const fetchModels = input.fetchModels; + const currentModels2 = () => { + const merged = [...baselineModels]; + for (const model2 of dynamicModels) { + const index = merged.findIndex((entry2) => getModelType(entry2) === getModelType(model2) && entry2.id === model2.id); + if (index >= 0) + merged[index] = model2; + else + merged.push(model2); + } + return merged; + }; + const apiFor = (model2) => single ?? byApi?.[model2.api]; + const dispatch = (model2, run2) => { + const streams2 = apiFor(model2); + if (!streams2) { + return lazyStream(model2, async () => { + throw new ModelsError("stream", `Provider ${input.id} has no API implementation for "${model2.api}"`); + }); + } + return run2(streams2); + }; + const provider2 = { + id: input.id, + name: input.name ?? input.id, + baseUrl: input.baseUrl, + headers: input.headers, + auth: input.auth, + getModels: () => currentModels2().filter((model2) => isModelType(model2, "chat")), + getAllModels: currentModels2, + refreshModels: fetchModels ? async (context) => { + if (context.stored) { + const restored = context.stored.models.filter((model2) => model2.provider === input.id).map((model2) => model2); + if (!await context.publish({ + update: () => { + dynamicModels = restored; + } + })) { + return; + } + } + if (!context.allowNetwork || context.signal.aborted) + return; + const fetched = await fetchModels(context); + if (context.signal.aborted) + return; + const refreshed = fetched.filter(hasKnownModelType); + await context.publish({ + persist: { models: refreshed, checkedAt: Date.now() }, + update: () => { + dynamicModels = refreshed; + } + }); + } : void 0, + filterModels: input.filterModels, + filterAllModels: input.filterAllModels, + stream: (model2, context, options) => dispatch(model2, (streams2) => streams2.stream(model2, context, options)), + streamSimple: (model2, context, options) => dispatch(model2, (streams2) => streams2.streamSimple(model2, context, options)) + }; + if (streams.some((entry2) => entry2.fetchDeferred !== void 0)) { + provider2.fetchDeferred = (model2, handle, options) => lazyStream(model2, async () => { + const implementation = apiFor(model2); + if (!implementation?.fetchDeferred) { + throw new ModelsError("provider", `Provider ${input.id} does not support deferred responses for "${model2.api}"`); + } + return implementation.fetchDeferred(model2, handle, options); + }); + } + if (streams.some((entry2) => entry2.cancelDeferred !== void 0)) { + provider2.cancelDeferred = async (model2, handle, options) => { + const implementation = apiFor(model2); + if (!implementation?.cancelDeferred) { + throw new ModelsError("provider", `Provider ${input.id} cannot cancel deferred responses for "${model2.api}"`); + } + await implementation.cancelDeferred(model2, handle, options); + }; + } + if (images && imageImplementations.length > 0) { + provider2.generateImages = async (model2, context, options) => { + const implementation = images[model2.api]; + if (!implementation) { + return imageErrorResult(model2, new ModelsError("provider", `Provider ${input.id} has no image generation implementation for "${model2.api}"`)); + } + return implementation.generateImages(model2, context, options); + }; + } + if (classifiers && classifierImplementations.length > 0) { + provider2.classify = async (model2, context, options) => { + const implementation = classifiers[model2.api]; + if (!implementation) { + return classifierErrorResult(model2, new ModelsError("provider", `Provider ${input.id} has no classifier implementation for "${model2.api}"`)); + } + return implementation.classify(model2, context, options); + }; + } + return provider2; +} + +// node_modules/@earendil-works/pi-ai/dist/utils/json-parse.js +var import_partial_json = __toESM(require_dist(), 1); +var VALID_JSON_ESCAPES = /* @__PURE__ */ new Set(['"', "\\", "/", "b", "f", "n", "r", "t"]); +function isControlCharacter(char) { + const codePoint = char.codePointAt(0); + return codePoint !== void 0 && codePoint >= 0 && codePoint <= 31; +} +function escapeControlCharacter(char) { + switch (char) { + case "\b": + return "\\b"; + case "\f": + return "\\f"; + case "\n": + return "\\n"; + case "\r": + return "\\r"; + case " ": + return "\\t"; + default: + return `\\u${char.codePointAt(0)?.toString(16).padStart(4, "0") ?? "0000"}`; + } +} +function repairJson(json) { + let repaired = ""; + let inString = false; + for (let index = 0; index < json.length; index++) { + const char = json[index]; + if (!inString) { + repaired += char; + if (char === '"') { + inString = true; + } + continue; + } + if (char === '"') { + repaired += char; + inString = false; + continue; + } + if (char === "\\") { + const nextChar = json[index + 1]; + if (nextChar === void 0) { + repaired += "\\\\"; + continue; + } + if (nextChar === "u") { + const unicodeDigits = json.slice(index + 2, index + 6); + if (/^[0-9a-fA-F]{4}$/.test(unicodeDigits)) { + repaired += `\\u${unicodeDigits}`; + index += 5; + continue; + } + } + if (VALID_JSON_ESCAPES.has(nextChar)) { + repaired += `\\${nextChar}`; + index += 1; + continue; + } + repaired += "\\\\"; + continue; + } + repaired += isControlCharacter(char) ? escapeControlCharacter(char) : char; + } + return repaired; +} +function parseJsonWithRepair(json) { + try { + return JSON.parse(json); + } catch (error) { + const repairedJson = repairJson(json); + if (repairedJson !== json) { + return JSON.parse(repairedJson); + } + throw error; + } +} +function parseToolCallArguments(json) { + if (!json?.trim()) + return { arguments: {} }; + try { + const parsed = parseJsonWithRepair(json); + if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) { + throw new Error("Tool arguments must be a JSON object"); + } + return { arguments: parsed }; + } catch (error) { + return { + arguments: {}, + argumentsError: `${error instanceof Error ? error.message : String(error)} + +Received arguments: +${json}` + }; + } +} + +// node_modules/@earendil-works/pi-ai/dist/utils/streaming-json.js +var MAX_REPORTED_DEPTH = 32; +var ROOT_SLOT = { kind: "root" }; +var CREDIT_PER_APPEND = 256; +var CREDIT_PER_CHARACTER = 8; +var MAX_PENDING_SCALAR_LENGTH = 64; +var NUMBER = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/; +var NUMBER_WITHOUT_EXPONENT_DIGITS = /^(-?(?:0|[1-9]\d*)(?:\.\d+)?)[eE][+-]?$/; +var LITERALS = [ + ["true", true], + ["false", false], + ["null", null] +]; +var QUOTE = 34; +var BACKSLASH = 92; +var StreamingJsonParser = class { + #root = void 0; + #done = false; + #stack = []; + #string = void 0; + #scalar = ""; + #scalarSlot = void 0; + /** Sum of `size + 1` over the open path: what a fresh snapshot copies. */ + #pathCost = 0; + #credit = 0; + #dirty = false; + #snapshot = void 0; + /** Changes of the piece being read. */ + #changes = []; + /** + * Read the next piece of text; returns the changes it made, in order. Costs time linear in `text.length`. + */ + append(text) { + this.#credit += CREDIT_PER_APPEND + CREDIT_PER_CHARACTER * text.length; + this.#changes = []; + if (this.#string !== void 0) + this.#string.change = void 0; + let index = 0; + while (index < text.length && !this.#done) { + const open = this.#string; + if (open !== void 0) + index = this.#readString(open, text, index); + else if (this.#scalar !== "") + index = this.#readScalar(text, index); + else + this.#structural(text[index++]); + } + return this.#changes; + } + /** + * The value of the text read so far: `undefined` before a value starts, otherwise a plain JSON value that never + * changes afterwards. Returns the previous snapshot while the text read since did not change the value, or while a + * fresh copy of the open path is not yet paid for (see the module comment). + */ + snapshot() { + if (!this.#dirty) + return this.#snapshot; + const pending = this.#pendingScalar(); + const cost = this.#pathCost + (pending === void 0 ? 0 : 1); + if (cost > this.#credit) + return this.#snapshot; + this.#credit -= cost; + this.#dirty = false; + this.#snapshot = this.#copyOpenPath(pending); + return this.#snapshot; + } + /** Read string characters from `start` until the string ends or the text does; returns the next index. */ + #readString(open, text, start) { + let runStart = start; + for (let index = start; index < text.length; index++) { + const code = text.charCodeAt(index); + if (open.escape !== "") { + if (this.#continueEscape(open, text[index])) { + runStart = index + 1; + continue; + } + runStart = index; + } + if (code === QUOTE) { + if (index > runStart) + this.#appendString(open, text.slice(runStart, index)); + this.#endString(open); + return index + 1; + } + if (code === BACKSLASH) { + if (index > runStart) + this.#appendString(open, text.slice(runStart, index)); + open.escape = "\\"; + runStart = index + 1; + } + } + if (text.length > runStart) + this.#appendString(open, text.slice(runStart)); + return text.length; + } + /** Read number or literal characters from `start` up to the next delimiter; returns the delimiter's index. */ + #readScalar(text, start) { + let end = start; + while (end < text.length && !isDelimiter(text[end])) + end++; + if (end > start) { + this.#scalar += text.slice(start, end); + if (this.#scalarSlot !== void 0) + this.#dirty = true; + } + if (end < text.length) + this.#endScalar(); + return end; + } + /** Handle one character outside strings. */ + #structural(char) { + switch (char) { + case " ": + case " ": + case "\n": + case "\r": + return; + case '"': + this.#startString(); + return; + case "{": + case "[": + this.#openContainer(char === "{" ? {} : []); + return; + case "}": + case "]": + this.#closeContainer(); + return; + case ":": { + const frame = this.#stack.at(-1); + if (frame !== void 0 && !Array.isArray(frame.container) && frame.phase === "colon") + frame.phase = "value"; + return; + } + case ",": { + const frame = this.#stack.at(-1); + if (frame !== void 0) + frame.phase = Array.isArray(frame.container) ? "value" : "key"; + return; + } + default: + this.#scalar = char; + this.#scalarSlot = this.#valueSlot(); + if (this.#scalarSlot !== void 0) + this.#dirty = true; + } + } + /** Where a value starting now goes, or `undefined` when this position takes no value. */ + #valueSlot() { + const frame = this.#stack.at(-1); + if (frame === void 0) + return this.#root === void 0 ? ROOT_SLOT : void 0; + if (Array.isArray(frame.container)) + return { kind: "element", index: frame.container.length }; + if ((frame.phase === "value" || frame.phase === "colon") && frame.key !== void 0) { + return { kind: "member", key: frame.key }; + } + return void 0; + } + #startString() { + const frame = this.#stack.at(-1); + if (frame !== void 0 && !Array.isArray(frame.container) && (frame.phase === "key" || frame.phase === "comma")) { + this.#string = { target: "key", slot: void 0, text: "", escape: "", path: void 0, change: void 0 }; + return; + } + const slot = this.#valueSlot(); + const path2 = this.#reportedPath(slot); + this.#string = { target: "value", slot, text: "", escape: "", path: path2, change: void 0 }; + this.#place(slot, ""); + if (path2 !== void 0) + this.#string.change = this.#report({ kind: "set", path: path2, value: "" }); + } + /** The path of a value at `slot` in the innermost open container, when its changes are reported. */ + #reportedPath(slot) { + if (slot === void 0) + return void 0; + if (slot.kind === "root") + return []; + const frame = this.#stack.at(-1); + if (!frame.reportsInside) + return void 0; + return [...frame.path, slot.kind === "member" ? slot.key : slot.index]; + } + #report(change) { + this.#changes.push(change); + return change; + } + /** Continue an escape with `char`; returns false when `char` is not part of it and must be read normally. */ + #continueEscape(open, char) { + if (open.escape === "\\") { + const decoded = SIMPLE_ESCAPES[char]; + if (decoded !== void 0) { + open.escape = ""; + this.#appendString(open, decoded); + return true; + } + if (char === "u") { + open.escape = "\\u"; + return true; + } + open.escape = ""; + this.#appendString(open, "\\"); + return false; + } + if (isHexDigit(char)) { + open.escape += char; + if (open.escape.length < 6) + return true; + const decoded = String.fromCharCode(Number.parseInt(open.escape.slice(2), 16)); + open.escape = ""; + this.#appendString(open, decoded); + return true; + } + const literal = open.escape; + open.escape = ""; + this.#appendString(open, literal); + return false; + } + #appendString(open, text) { + open.text += text; + if (open.target !== "value") + return; + this.#place(open.slot, open.text); + if (open.path === void 0) + return; + const change = open.change; + if (change !== void 0 && this.#changes.at(-1) === change) { + if (change.kind === "set") + change.value = change.value + text; + else + change.text += text; + return; + } + open.change = this.#report({ kind: "append", path: open.path, text }); + } + #endString(open) { + this.#string = void 0; + const frame = this.#stack.at(-1); + if (open.target === "key") { + if (frame === void 0) + return; + frame.key = open.text; + frame.phase = "colon"; + return; + } + this.#valueEnded(frame); + } + #endScalar() { + const token = this.#scalar; + const slot = this.#scalarSlot; + this.#scalar = ""; + this.#scalarSlot = void 0; + const value2 = scalarValue(token); + if (value2 !== void 0) { + this.#place(slot, value2); + const path2 = this.#reportedPath(slot); + if (path2 !== void 0) + this.#report({ kind: "set", path: path2, value: value2 }); + } + this.#valueEnded(this.#stack.at(-1)); + } + /** The value of the unterminated scalar at the end of the text, if it shows. */ + #pendingScalar() { + const slot = this.#scalarSlot; + const token = this.#scalar; + if (slot === void 0 || token === "" || token.length > MAX_PENDING_SCALAR_LENGTH) + return void 0; + const value2 = pendingScalarValue(token); + return value2 === void 0 ? void 0 : { slot, value: value2 }; + } + #openContainer(container) { + const slot = this.#valueSlot(); + const path2 = this.#reportedPath(slot); + this.#place(slot, container); + this.#stack.push({ + container, + slot, + path: path2, + reportsInside: path2 !== void 0 && path2.length < MAX_REPORTED_DEPTH, + size: 0, + key: void 0, + phase: Array.isArray(container) ? "value" : "key" + }); + this.#pathCost += 1; + this.#dirty = true; + if (path2 !== void 0) + this.#report({ kind: "set", path: path2, value: Array.isArray(container) ? [] : {} }); + } + #closeContainer() { + const frame = this.#stack.pop(); + if (frame === void 0) + return; + this.#pathCost -= frame.size + 1; + this.#dirty = true; + if (frame.path !== void 0 && !frame.reportsInside) { + this.#report({ kind: "set", path: frame.path, value: frame.container }); + } + this.#valueEnded(this.#stack.at(-1)); + } + /** A value in `frame`, or the root value when there is no frame, has ended. */ + #valueEnded(frame) { + if (frame === void 0) { + if (this.#root !== void 0) + this.#done = true; + return; + } + frame.phase = "comma"; + } + /** Set the value at `slot` in the open path; nothing for a value in an invalid position. */ + #place(slot, value2) { + if (slot === void 0) + return; + this.#dirty = true; + if (slot.kind === "root") { + this.#root = value2; + return; + } + const frame = this.#stack.at(-1); + if (slot.kind === "element") { + const elements = frame.container; + if (slot.index === elements.length) { + frame.size++; + this.#pathCost++; + } + elements[slot.index] = value2; + return; + } + const members = frame.container; + if (!Object.hasOwn(members, slot.key)) { + frame.size++; + this.#pathCost++; + } + setMember(members, slot.key, value2); + } + /** A fresh snapshot: copies of the open containers, innermost first, each holding the copy of its open child. */ + #copyOpenPath(pending) { + if (this.#stack.length === 0) + return pending?.slot.kind === "root" ? pending.value : this.#root; + let child; + let childSlot; + for (let depth = this.#stack.length - 1; depth >= 0; depth--) { + const frame = this.#stack[depth]; + const copy2 = Array.isArray(frame.container) ? frame.container.slice() : { ...frame.container }; + if (depth === this.#stack.length - 1 && pending !== void 0) + setSlot(copy2, pending.slot, pending.value); + if (child !== void 0 && childSlot !== void 0) + setSlot(copy2, childSlot, child); + child = copy2; + childSlot = frame.slot; + } + return childSlot?.kind === "root" ? child : this.#root; + } +}; +var ToolCallArgumentsStream = class { + #text = ""; + #parser = new StreamingJsonParser(); + #snapshot = void 0; + #arguments = {}; + /** The raw argument text received so far. */ + get text() { + return this.#text; + } + /** Append a piece of argument text; returns the partial arguments, a new object whenever they changed. */ + append(delta) { + this.#text += delta; + this.#parser.append(delta); + return this.#partial(); + } + /** + * Take the provider's complete argument text. When it continues the text so far, returns the missing piece as + * `delta`; otherwise starts over with `text`. Either way returns the partial arguments of `text`. + */ + replace(text) { + if (text.startsWith(this.#text)) { + const delta = text.slice(this.#text.length); + return { delta, arguments: delta === "" ? this.#partial() : this.append(delta) }; + } + this.#text = ""; + this.#parser = new StreamingJsonParser(); + return { delta: void 0, arguments: this.append(text) }; + } + /** The final arguments: one full parse of the complete text, which reports malformed arguments. */ + finish() { + return parseToolCallArguments(this.#text); + } + #partial() { + const snapshot = this.#parser.snapshot(); + if (snapshot === this.#snapshot) + return this.#arguments; + this.#snapshot = snapshot; + this.#arguments = typeof snapshot === "object" && snapshot !== null && !Array.isArray(snapshot) ? snapshot : {}; + return this.#arguments; + } +}; +var SIMPLE_ESCAPES = { + '"': '"', + "\\": "\\", + "/": "/", + b: "\b", + f: "\f", + n: "\n", + r: "\r", + t: " " +}; +function isDelimiter(char) { + return char === " " || char === " " || char === "\n" || char === "\r" || char === "," || char === ":" || char === "{" || char === "}" || char === "[" || char === "]" || char === '"'; +} +function isHexDigit(char) { + return char >= "0" && char <= "9" || char >= "a" && char <= "f" || char >= "A" && char <= "F"; +} +function scalarValue(token) { + for (const [literal, value2] of LITERALS) + if (token === literal) + return value2; + return NUMBER.test(token) ? Number(token) : void 0; +} +function pendingScalarValue(token) { + for (const [literal, value2] of LITERALS) + if (literal.startsWith(token)) + return value2; + if (NUMBER.test(token)) + return Number(token); + const mantissa = NUMBER_WITHOUT_EXPONENT_DIGITS.exec(token); + return mantissa === null ? void 0 : Number(mantissa[1]); +} +function setSlot(container, slot, value2) { + if (slot.kind === "element") + container[slot.index] = value2; + else if (slot.kind === "member") + setMember(container, slot.key, value2); +} +function setMember(members, key, value2) { + if (key === "__proto__") { + Object.defineProperty(members, key, { value: value2, writable: true, enumerable: true, configurable: true }); + } else { + members[key] = value2; + } +} + +// node_modules/@earendil-works/pi-ai/dist/providers/faux.js +var DEFAULT_API = "faux"; +var DEFAULT_PROVIDER = "faux"; +var DEFAULT_MODEL_ID = "faux-1"; +var DEFAULT_MODEL_NAME = "Faux Model"; +var DEFAULT_BASE_URL = "http://localhost:0"; +var DEFAULT_MIN_TOKEN_SIZE = 3; +var DEFAULT_MAX_TOKEN_SIZE = 5; +var DEFAULT_USAGE = { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + totalTokens: 0, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } +}; +function fauxText(text) { + return { type: "text", text }; +} +function fauxToolCall(name, arguments_, options = {}) { + return { + type: "toolCall", + id: options.id ?? randomId("tool"), + name, + arguments: arguments_ + }; +} +function normalizeFauxAssistantContent(content) { + if (typeof content === "string") { + return [fauxText(content)]; + } + return Array.isArray(content) ? content : [content]; +} +function fauxAssistantMessage(content, options = {}) { + return { + role: "assistant", + content: normalizeFauxAssistantContent(content), + api: DEFAULT_API, + provider: DEFAULT_PROVIDER, + model: DEFAULT_MODEL_ID, + usage: DEFAULT_USAGE, + stopReason: options.stopReason ?? "stop", + ...options.deferred === void 0 ? {} : { deferred: options.deferred }, + ...options.errorMessage === void 0 ? {} : { errorMessage: options.errorMessage }, + ...options.responseId === void 0 ? {} : { responseId: options.responseId }, + timestamp: options.timestamp ?? Date.now() + }; +} +function estimateTokens(text) { + return Math.ceil(text.length / 4); +} +function randomId(prefix) { + return `${prefix}:${Date.now()}:${Math.random().toString(36).slice(2)}`; +} +function contentToText(content) { + if (typeof content === "string") { + return content; + } + return content.map((block) => { + if (block.type === "text") { + return block.text; + } + return `[image:${block.mimeType}:${block.data.length}]`; + }).join("\n"); +} +function assistantContentToText(content) { + return content.map((block) => { + if (block.type === "text") { + return block.text; + } + if (block.type === "thinking") { + return block.thinking; + } + return `${block.name}:${JSON.stringify(block.arguments)}`; + }).join("\n"); +} +function toolResultToText(message) { + return [message.toolName, ...message.content.map((block) => contentToText([block]))].join("\n"); +} +function messageToText(message) { + if (message.role === "system") { + return [ + getSystemMessageText(message), + ...message.toolsRemoved?.map((tool) => `tool-:${JSON.stringify(tool)}`) ?? [], + ...message.toolsAdded?.map((tool) => `tool+:${JSON.stringify(tool)}`) ?? [] + ].filter((part) => part.length > 0).join("\n"); + } + if (message.role === "user") { + return contentToText(message.content); + } + if (message.role === "assistant") { + return assistantContentToText(message.content); + } + return toolResultToText(message); +} +function joinedLength(messages, count = messages.length) { + let length = count > 0 ? (count - 1) * 2 : 0; + for (let index = 0; index < count; index++) + length += messages[index].length; + return length; +} +function commonPromptPrefixLength(previous, current) { + let index = 0; + while (index < previous.length && index < current.length && previous[index] === current[index]) + index++; + const rest = (messages) => index === messages.length ? "" : (index > 0 ? "\n\n" : "") + messages.slice(index).join("\n\n"); + return joinedLength(previous, index) + commonPrefixLength(rest(previous), rest(current)); +} +function commonPrefixLength(a, b) { + const length = Math.min(a.length, b.length); + let index = 0; + while (index < length && a[index] === b[index]) { + index++; + } + return index; +} +function withUsageEstimate(message, context, options, promptCache) { + const prompt = context.messages.map((message2) => `${message2.role}:${messageToText(message2)}`); + const promptLength = joinedLength(prompt); + const promptTokens = Math.ceil(promptLength / 4); + const outputTokens = estimateTokens(assistantContentToText(message.content)); + let input = promptTokens; + let cacheRead = 0; + let cacheWrite = 0; + const sessionId = options?.sessionId; + if (sessionId && options?.cacheRetention !== "none") { + const previousPrompt = promptCache.get(sessionId); + if (previousPrompt) { + const cachedChars = commonPromptPrefixLength(previousPrompt, prompt); + cacheRead = Math.ceil(cachedChars / 4); + cacheWrite = Math.max(0, promptTokens - cacheRead); + } else { + cacheWrite = promptTokens; + } + input = 0; + promptCache.set(sessionId, prompt); + } + return { + ...message, + usage: { + input, + output: outputTokens, + cacheRead, + cacheWrite, + totalTokens: input + outputTokens + cacheRead + cacheWrite, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } + } + }; +} +function splitStringByTokenSize(text, minTokenSize, maxTokenSize) { + const chunks = []; + let index = 0; + while (index < text.length) { + const tokenSize = minTokenSize + Math.floor(Math.random() * (maxTokenSize - minTokenSize + 1)); + const charSize = Math.max(1, tokenSize * 4); + chunks.push(text.slice(index, index + charSize)); + index += charSize; + } + return chunks.length > 0 ? chunks : [""]; +} +function cloneMessage(message, api, provider2, modelId) { + const cloned = structuredClone(message); + return { + ...cloned, + api, + provider: provider2, + model: modelId, + timestamp: cloned.timestamp ?? Date.now(), + usage: cloned.usage ?? DEFAULT_USAGE + }; +} +function createDeferredMessage(model2, handle) { + return { + role: "assistant", + content: [], + api: model2.api, + provider: model2.provider, + model: model2.id, + usage: DEFAULT_USAGE, + stopReason: "deferred", + deferred: handle, + timestamp: Date.now() + }; +} +function createErrorMessage(error, api, provider2, modelId) { + return { + role: "assistant", + content: [], + api, + provider: provider2, + model: modelId, + usage: DEFAULT_USAGE, + stopReason: "error", + errorMessage: error instanceof Error ? error.message : String(error), + timestamp: Date.now() + }; +} +function createAbortedMessage(partial) { + return { + ...partial, + stopReason: "aborted", + errorMessage: "Request was aborted", + timestamp: Date.now() + }; +} +function scheduleChunk(chunk, tokensPerSecond) { + if (!tokensPerSecond || tokensPerSecond <= 0) { + return new Promise((resolve2) => queueMicrotask(resolve2)); + } + const delayMs = estimateTokens(chunk) / tokensPerSecond * 1e3; + return new Promise((resolve2) => setTimeout(resolve2, delayMs)); +} +async function streamWithDeltas(stream, message, minTokenSize, maxTokenSize, tokensPerSecond, signal) { + const partial = { ...message, content: [], stopReason: "pending" }; + if (signal?.aborted) { + const aborted = createAbortedMessage(partial); + stream.push({ type: "error", reason: "aborted", error: aborted }); + stream.end(aborted); + return; + } + stream.push({ type: "start", partial: { ...partial } }); + for (let index = 0; index < message.content.length; index++) { + if (signal?.aborted) { + const aborted = createAbortedMessage(partial); + stream.push({ type: "error", reason: "aborted", error: aborted }); + stream.end(aborted); + return; + } + const block = message.content[index]; + if (block.type === "thinking") { + partial.content = [...partial.content, { type: "thinking", thinking: "" }]; + stream.push({ type: "thinking_start", contentIndex: index, partial: { ...partial } }); + for (const chunk of splitStringByTokenSize(block.thinking, minTokenSize, maxTokenSize)) { + await scheduleChunk(chunk, tokensPerSecond); + if (signal?.aborted) { + const aborted = createAbortedMessage(partial); + stream.push({ type: "error", reason: "aborted", error: aborted }); + stream.end(aborted); + return; + } + partial.content[index].thinking += chunk; + stream.push({ type: "thinking_delta", contentIndex: index, delta: chunk, partial: { ...partial } }); + } + stream.push({ + type: "thinking_end", + contentIndex: index, + content: block.thinking, + partial: { ...partial } + }); + continue; + } + if (block.type === "text") { + partial.content = [...partial.content, { type: "text", text: "" }]; + stream.push({ type: "text_start", contentIndex: index, partial: { ...partial } }); + for (const chunk of splitStringByTokenSize(block.text, minTokenSize, maxTokenSize)) { + await scheduleChunk(chunk, tokensPerSecond); + if (signal?.aborted) { + const aborted = createAbortedMessage(partial); + stream.push({ type: "error", reason: "aborted", error: aborted }); + stream.end(aborted); + return; + } + partial.content[index].text += chunk; + stream.push({ type: "text_delta", contentIndex: index, delta: chunk, partial: { ...partial } }); + } + stream.push({ type: "text_end", contentIndex: index, content: block.text, partial: { ...partial } }); + continue; + } + partial.content = [...partial.content, { type: "toolCall", id: block.id, name: block.name, arguments: {} }]; + stream.push({ type: "toolcall_start", contentIndex: index, partial: { ...partial } }); + const toolArguments = new ToolCallArgumentsStream(); + for (const chunk of splitStringByTokenSize(JSON.stringify(block.arguments), minTokenSize, maxTokenSize)) { + await scheduleChunk(chunk, tokensPerSecond); + if (signal?.aborted) { + const aborted = createAbortedMessage(partial); + stream.push({ type: "error", reason: "aborted", error: aborted }); + stream.end(aborted); + return; + } + partial.content[index].arguments = toolArguments.append(chunk); + stream.push({ type: "toolcall_delta", contentIndex: index, delta: chunk, partial: { ...partial } }); + } + partial.content[index].arguments = block.arguments; + stream.push({ type: "toolcall_end", contentIndex: index, toolCall: block, partial: { ...partial } }); + } + if (message.stopReason === "pending") { + throw new Error("Faux response ended without a stop reason"); + } + if (message.stopReason === "error" || message.stopReason === "aborted") { + stream.push({ type: "error", reason: message.stopReason, error: message }); + stream.end(message); + return; + } + stream.push({ type: "done", reason: message.stopReason, message }); + stream.end(message); +} +function createFauxCore(options) { + const api = options.api ?? randomId(DEFAULT_API); + const provider2 = options.provider ?? DEFAULT_PROVIDER; + const minTokenSize = Math.max(1, Math.min(options.tokenSize?.min ?? DEFAULT_MIN_TOKEN_SIZE, options.tokenSize?.max ?? DEFAULT_MAX_TOKEN_SIZE)); + const maxTokenSize = Math.max(minTokenSize, options.tokenSize?.max ?? DEFAULT_MAX_TOKEN_SIZE); + let pendingResponses = []; + const tokensPerSecond = options.tokensPerSecond; + const state = { callCount: 0, deferredFetchCount: 0, cancelledDeferred: [] }; + const promptCache = /* @__PURE__ */ new Map(); + const deferredResponses = /* @__PURE__ */ new Map(); + const modelDefinitions = options.models?.length ? options.models : [ + { + id: DEFAULT_MODEL_ID, + name: DEFAULT_MODEL_NAME, + reasoning: false, + input: ["text", "image"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 128e3, + maxTokens: 16384 + } + ]; + const models2 = modelDefinitions.map((definition) => ({ + id: definition.id, + name: definition.name ?? definition.id, + api, + provider: provider2, + baseUrl: DEFAULT_BASE_URL, + reasoning: definition.reasoning ?? false, + input: definition.input ?? ["text", "image"], + inputLimits: definition.inputLimits, + cost: definition.cost ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: definition.contextWindow ?? 128e3, + maxTokens: definition.maxTokens ?? 16384 + })); + const resolveResponse = async (step, context, streamOptions, requestModel) => { + const resolved = typeof step === "function" ? await step(context, streamOptions, state, requestModel) : step; + return withUsageEstimate(cloneMessage(resolved, api, provider2, requestModel.id), context, streamOptions, promptCache); + }; + const stream = (requestModel, context, streamOptions) => { + const outer = createAssistantMessageEventStream(); + const step = pendingResponses.shift(); + state.callCount++; + queueMicrotask(async () => { + try { + await streamOptions?.onResponse?.({ status: 200, headers: {} }, requestModel); + if (!step) { + let message2 = createErrorMessage(new Error("No more faux responses queued"), api, provider2, requestModel.id); + message2 = withUsageEstimate(message2, context, streamOptions, promptCache); + outer.push({ type: "error", reason: "error", error: message2 }); + outer.end(message2); + return; + } + if (streamOptions?.deferred) { + const handle = { + provider: requestModel.provider, + modelId: requestModel.id, + api: requestModel.api, + id: randomId("deferred"), + ...options.deferred?.pollAfterMs !== void 0 ? { pollAfterMs: options.deferred.pollAfterMs } : {} + }; + deferredResponses.set(handle.id, { + handle, + step, + context, + options: streamOptions, + model: requestModel, + pendingFetches: Math.max(0, Math.floor(options.deferred?.pendingFetches ?? 0)), + cancelled: false + }); + await streamWithDeltas(outer, createDeferredMessage(requestModel, handle), minTokenSize, maxTokenSize, tokensPerSecond, streamOptions.signal); + return; + } + const message = await resolveResponse(step, context, streamOptions, requestModel); + await streamWithDeltas(outer, message, minTokenSize, maxTokenSize, tokensPerSecond, streamOptions?.signal); + } catch (error) { + const message = createErrorMessage(error, api, provider2, requestModel.id); + outer.push({ type: "error", reason: "error", error: message }); + outer.end(message); + } + }); + return outer; + }; + const streamSimple = (streamModel, context, streamOptions) => stream(streamModel, context, streamOptions); + const fetchDeferred = (requestModel, handle, fetchOptions) => { + const outer = createAssistantMessageEventStream(); + state.deferredFetchCount++; + queueMicrotask(async () => { + try { + await fetchOptions?.onResponse?.({ status: 200, headers: {} }, requestModel); + const entry2 = deferredResponses.get(handle.id); + if (!entry2 || entry2.handle.provider !== handle.provider || entry2.handle.modelId !== handle.modelId || entry2.handle.api !== handle.api) { + throw new Error(`Unknown faux deferred response: ${handle.id}`); + } + if (entry2.cancelled) + throw new Error(`Faux deferred response was cancelled: ${handle.id}`); + if (entry2.pendingFetches > 0) { + entry2.pendingFetches--; + await streamWithDeltas(outer, createDeferredMessage(requestModel, entry2.handle), minTokenSize, maxTokenSize, tokensPerSecond, fetchOptions?.signal); + return; + } + if (!entry2.final) { + const { deferred: _deferred, signal: _submissionSignal, onResponse: _submissionOnResponse, ...submissionOptions } = entry2.options ?? {}; + try { + entry2.final = await resolveResponse(entry2.step, entry2.context, submissionOptions, entry2.model); + } catch (error) { + entry2.final = createErrorMessage(error, api, provider2, entry2.model.id); + } + } + await streamWithDeltas(outer, entry2.final, minTokenSize, maxTokenSize, tokensPerSecond, fetchOptions?.signal); + } catch (error) { + const message = createErrorMessage(error, api, provider2, requestModel.id); + outer.push({ type: "error", reason: "error", error: message }); + outer.end(message); + } + }); + return outer; + }; + const cancelDeferred2 = async (requestModel, handle, cancelOptions) => { + state.cancelledDeferred.push(structuredClone(handle)); + const entry2 = deferredResponses.get(handle.id); + if (entry2) + entry2.cancelled = true; + await cancelOptions?.onResponse?.({ status: 200, headers: {} }, requestModel); + }; + function getModel(requestedModelId) { + if (!requestedModelId) { + return models2[0]; + } + return models2.find((candidate) => candidate.id === requestedModelId); + } + return { + api, + provider: provider2, + models: models2, + stream, + streamSimple, + fetchDeferred, + cancelDeferred: cancelDeferred2, + getModel, + state, + setResponses(responses) { + pendingResponses = [...responses]; + }, + appendResponses(responses) { + pendingResponses.push(...responses); + }, + getPendingResponseCount() { + return pendingResponses.length; + } + }; +} +function fauxProvider(options = {}) { + const core = createFauxCore(options); + const provider2 = createProvider({ + id: core.provider, + auth: { apiKey: { name: "Faux", resolve: async () => ({ auth: {} }) } }, + models: core.models, + api: { + stream: core.stream, + streamSimple: core.streamSimple, + fetchDeferred: core.fetchDeferred, + cancelDeferred: core.cancelDeferred + } + }); + return { + provider: provider2, + api: core.api, + models: core.models, + getModel: core.getModel, + state: core.state, + setResponses: core.setResponses, + appendResponses: core.appendResponses, + getPendingResponseCount: core.getPendingResponseCount + }; +} + +// node_modules/@earendil-works/pi-schema/dist/builders.js +function str(options = {}) { + return { type: "string", ...options }; +} +function num(options = {}) { + return { type: "number", ...options }; +} +function bool(options = {}) { + return { type: "boolean", ...options }; +} +function lit(value2, options = {}) { + return { const: value2, ...options }; +} +function union(schemas, options = {}) { + return { anyOf: schemas, ...options }; +} +function arr(items, options = {}) { + return { type: "array", items, ...options }; +} +var Optional = class { + schema; + constructor(schema) { + this.schema = schema; + } +}; +function opt(schema) { + return new Optional(schema); +} +function obj(properties, options = {}) { + const { open, ...annotations } = options; + const props = {}; + const required = []; + for (const [name, property] of Object.entries(properties)) { + if (property instanceof Optional) + props[name] = property.schema; + else { + props[name] = property; + required.push(name); + } + } + return { + type: "object", + properties: props, + ...required.length > 0 ? { required } : {}, + ...open === true ? {} : { additionalProperties: false }, + ...annotations + }; +} + +// node_modules/@earendil-works/pi-schema/dist/resolver.js +var DEFAULT_BASE = "https://json-schema.invalid/root.json"; +var Resolver = class { + draft; + rootBase; + /** Schema object to the base URI it was indexed under. */ + baseOf = /* @__PURE__ */ new Map(); + /** URI (with a `#name` fragment) of every `$dynamicAnchor`, to its schema. */ + dynamicAnchors = /* @__PURE__ */ new Map(); + root; + options; + registry = /* @__PURE__ */ new Map(); + rejectUnresolved; + /** The references that named nothing and resolved to `false` (with `unresolvedRefs: "reject"`). */ + unresolved = /* @__PURE__ */ new Set(); + resources = /* @__PURE__ */ new Map(); + anchors = /* @__PURE__ */ new Map(); + indexed = false; + constructor(root2, options) { + this.draft = options.draft === "7" ? "7" : "2020-12"; + this.root = root2; + this.options = options; + this.rejectUnresolved = options.unresolvedRefs === "reject"; + this.rootBase = isSchemaObject(root2) ? this.baseFor(root2, DEFAULT_BASE) : DEFAULT_BASE; + } + /** + * The base URI of `node`, whose parent has `base`: its `$id` changes it, unless draft 7 makes the `$id` inert. Once + * the schema is indexed, the indexed base (a resolved reference target already has its own). + */ + baseFor(node, base) { + const indexed = this.baseOf.get(node); + if (indexed !== void 0) + return indexed; + const id = node.$id; + if (typeof id !== "string") + return base; + if (this.draft === "7" && (typeof node.$ref === "string" || id.startsWith("#"))) + return base; + return documentOf(resolveUri(id, base)); + } + /** Indexes the root schema and the registered documents' URIs (once). */ + ensureIndexed() { + if (this.indexed) + return; + this.indexed = true; + for (const [uri, schema] of Object.entries(this.options.schemas ?? {})) { + this.registry.set(documentOf(resolveUri(uri, DEFAULT_BASE)), schema); + } + this.resources.set(DEFAULT_BASE, this.root); + this.index(this.root, DEFAULT_BASE); + } + /** + * The schema a reference names, with the base URI it compiles under. Throws if it names nothing, or, with + * `unresolvedRefs: "reject"`, resolves to `false`, the schema no value passes. + */ + resolve(ref, base) { + this.ensureIndexed(); + if (!this.rejectUnresolved) + return this.find(ref, base); + try { + return this.find(ref, base); + } catch { + this.unresolved.add(resolveUri(ref, base)); + return { node: false, base }; + } + } + /** Whether `ref`, from `base`, named nothing and resolved to `false`. */ + isUnresolved(ref, base) { + return this.unresolved.has(resolveUri(ref, base)); + } + find(ref, base) { + const uri = resolveUri(ref, base); + const doc = documentOf(uri); + const fragment = uri.length > doc.length ? decodeURIComponent(uri.slice(doc.length + 1)) : ""; + const root2 = this.resources.has(doc) ? this.resources.get(doc) : this.load(doc); + if (root2 === void 0) + throw new Error(`Unresolved $ref ${ref} (no schema registered for ${doc})`); + if (fragment === "") + return { node: root2, base: doc }; + if (fragment.startsWith("/")) + return this.pointer(root2, doc, fragment); + const target = this.anchors.get(`${doc}#${fragment}`); + if (target === void 0) + throw new Error(`Unresolved $ref ${ref} (no anchor ${fragment} in ${doc})`); + return { node: target, base: this.baseOf.get(target) ?? doc }; + } + /** Whether `node` is the root of the schema resource at `base`. */ + isResource(node, base) { + this.ensureIndexed(); + return this.resources.get(base) === node; + } + // Walks only keywords that hold subschemas: an `$id` inside `enum`, `const` or an unknown keyword names nothing. + index(node, base) { + if (!isSchemaObject(node) || this.baseOf.has(node)) + return; + let b = base; + const refOnly = this.draft === "7" && typeof node.$ref === "string"; + if (!refOnly && typeof node.$id === "string") { + if (this.draft === "7" && node.$id.startsWith("#")) + this.anchors.set(documentOf(b) + node.$id, node); + else { + const uri = resolveUri(node.$id, b); + b = documentOf(uri); + this.resources.set(b, node); + if (uri.length > b.length + 1) + this.anchors.set(uri, node); + } + } + this.baseOf.set(node, b); + if (refOnly) + return; + if (this.draft !== "7") { + if (typeof node.$anchor === "string") + this.anchors.set(`${b}#${node.$anchor}`, node); + if (typeof node.$dynamicAnchor === "string") { + this.anchors.set(`${b}#${node.$dynamicAnchor}`, node); + this.dynamicAnchors.set(`${b}#${node.$dynamicAnchor}`, node); + } + } + for (const key of SCHEMA_KEYWORDS) + this.index(node[key], b); + for (const key of MAP_KEYWORDS) { + const map2 = node[key]; + if (isSchemaObject(map2)) + for (const k of Object.keys(map2)) + this.index(map2[k], b); + } + for (const key of ARRAY_KEYWORDS) { + const list = node[key]; + if (Array.isArray(list)) + for (const s of list) + this.index(s, b); + } + } + load(doc) { + const schema = this.registry.get(doc); + if (schema === void 0) + return void 0; + this.resources.set(doc, schema); + this.index(schema, doc); + return schema; + } + pointer(root2, base, fragment) { + let node = root2; + let b = base; + for (const raw of fragment.split("/").slice(1)) { + const key = raw.replace(/~1/g, "/").replace(/~0/g, "~"); + node = typeof node === "object" && node !== null && Object.hasOwn(node, key) ? node[key] : void 0; + if (node === void 0) + throw new Error(`Unresolved $ref #${fragment} in ${base}`); + if (typeof node === "object" && node !== null) + b = this.baseOf.get(node) ?? b; + } + return { node, base: b }; + } +}; +var SCHEMA_KEYWORDS = [ + "additionalProperties", + "propertyNames", + "contains", + "not", + "if", + "then", + "else", + "unevaluatedProperties", + "unevaluatedItems", + "additionalItems", + "contentSchema", + "items" +]; +var MAP_KEYWORDS = ["properties", "patternProperties", "$defs", "definitions", "dependentSchemas", "dependencies"]; +var ARRAY_KEYWORDS = ["allOf", "anyOf", "oneOf", "prefixItems", "items"]; +function isSchemaObject(value2) { + return typeof value2 === "object" && value2 !== null && !Array.isArray(value2); +} +function resolveUri(ref, base) { + try { + return new URL(ref, base).href; + } catch { + return ref.startsWith("#") ? documentOf(base) + ref : ref; + } +} +function documentOf(uri) { + const hash = uri.indexOf("#"); + return hash < 0 ? uri : uri.slice(0, hash); +} + +// node_modules/@earendil-works/pi-schema/dist/shared.js +function sharedRegex(source) { + const hit = regexes.get(source)?.deref(); + if (hit !== void 0) + return hit; + let re; + try { + re = new RegExp(source, "u"); + } catch { + re = new RegExp(source); + } + regexes.set(source, new WeakRef(re)); + regexCleanup.register(re, source); + return re; +} +var regexes = /* @__PURE__ */ new Map(); +var regexCleanup = new FinalizationRegistry((source) => { + if (regexes.get(source)?.deref() === void 0) + regexes.delete(source); +}); + +// node_modules/@earendil-works/pi-schema/dist/coerce.js +var Coercer = class { + c; + schema; + /** + * The (schema node, value) pairs pass 2 is converting through a re-entry point, as a stack. Conversion can enter + * a node again with the same value only through a `$ref` or by wrapping a single value into an array (also for + * schemas that cycle through shared JS objects): a recursive schema that meets a value no branch accepts would + * convert it without end. A pair already on the stack (primitives by value, objects by reference) leaves the value + * unchanged. Only those two points push, so schemas without references or wraps pay nothing. + */ + entered = []; + /** How often a re-entry was cut short in this coerce() call; results computed across one are not memoized. */ + cuts = 0; + /** + * Results of `$ref` targets by (node, value) within one coerce() call: a recursive union whose branches all + * convert the same children (and all fail) would convert them once per branch, exponentially in the depth. + */ + memo = void 0; + constructor(compiler2, schema) { + this.c = compiler2; + this.schema = schema; + } + /** `value` coerced: a copy along the changed paths only, or `value` itself when nothing changes. */ + coerce(value2) { + const base = this.c.r.rootBase; + const withoutNulls = this.dropOptionalNulls(value2, this.schema, base); + return this.coerceTypes(this.convertOnly(withoutNulls), this.schema, base); + } + /** Pass 2 alone, as TypeBox's `Value.Convert` (the TypeBox conformance tests run it). */ + convertOnly(value2) { + try { + return this.convert(this.schema, this.c.r.rootBase, value2); + } finally { + this.memo = void 0; + this.cuts = 0; + } + } + /** `node` converted for `value` at a re-entry point: unchanged when that pair is already being converted. */ + reenter(node, base, value2, memoize) { + const entered = this.entered; + for (let i = 0; i < entered.length; i += 2) { + if (entered[i] === node && Object.is(entered[i + 1], value2)) { + this.cuts++; + return value2; + } + } + const memo = memoize ? this.memo?.get(node) : void 0; + if (memo?.has(value2)) + return memo.get(value2); + const cuts = this.cuts; + entered.push(node, value2); + let result; + try { + result = this.convert(node, base, value2); + } finally { + entered.length -= 2; + } + if (memoize && this.cuts === cuts) { + this.memo ??= /* @__PURE__ */ new Map(); + let values = this.memo.get(node); + if (values === void 0) { + values = /* @__PURE__ */ new Map(); + this.memo.set(node, values); + } + values.set(value2, result); + } + return result; + } + /** Whether `value` passes the schema `node`; undefined when `node` cannot be compiled. */ + passes(node, base, value2) { + try { + return this.c.check(node, this.baseOf(node, base))(value2); + } catch { + return void 0; + } + } + baseOf(node, base) { + return isSchemaObject(node) ? this.c.r.baseOf.get(node) ?? base : base; + } + // --------------------------------------------------------------------------------------------------------------- + // Pass 1: optional nulls + // --------------------------------------------------------------------------------------------------------------- + dropOptionalNulls(value2, schema, base) { + if (!isSchemaObject(schema)) + return value2; + const b = this.baseOf(schema, base); + if (Array.isArray(value2)) { + return mapItems(value2, (item, i) => { + const itemSchema = itemSchemaAt(schema, i); + return itemSchema === void 0 ? item : this.dropOptionalNulls(item, itemSchema, b); + }); + } + if (!isPlainObject(value2)) + return value2; + const properties = isSchemaObject(schema.properties) ? schema.properties : {}; + const patterns = isSchemaObject(schema.patternProperties) ? Object.entries(schema.patternProperties).map(([pattern, item]) => [sharedRegex(pattern), item]) : []; + const additional = isSchemaObject(schema.additionalProperties) ? schema.additionalProperties : void 0; + if (patterns.length === 0 && additional === void 0 && !isSchemaObject(schema.properties)) + return value2; + const required = new Set(Array.isArray(schema.required) ? schema.required : []); + let out; + const set = (key, next) => { + if (next === (out ?? value2)[key]) + return; + out ??= { ...value2 }; + out[key] = next; + }; + for (const [key, property] of Object.entries(properties)) { + if (!Object.hasOwn(value2, key)) + continue; + const isRef = isSchemaObject(property) && typeof property.$ref === "string"; + if (value2[key] === null && !required.has(key) && !isRef && this.passes(property, b, null) === false) { + out ??= { ...value2 }; + delete out[key]; + continue; + } + set(key, this.dropOptionalNulls(value2[key], property, b)); + } + if (patterns.length > 0 || additional !== void 0) { + for (const key of Object.keys(value2)) { + let matched = Object.hasOwn(properties, key); + for (const [pattern, item] of patterns) { + if (!pattern.test(key)) + continue; + matched = true; + set(key, this.dropOptionalNulls((out ?? value2)[key], item, b)); + } + if (!matched && additional !== void 0) + set(key, this.dropOptionalNulls(value2[key], additional, b)); + } + } + return out ?? value2; + } + // --------------------------------------------------------------------------------------------------------------- + // Pass 2: TypeBox's conversion rules + // --------------------------------------------------------------------------------------------------------------- + convert(node, base, value2) { + if (!isSchemaObject(node)) + return value2; + const b = this.baseOf(node, base); + if (typeof node.$ref === "string") { + let target; + try { + target = this.c.r.resolve(node.$ref, b); + } catch { + return value2; + } + return isSchemaObject(target.node) ? this.reenter(target.node, target.base, value2, true) : value2; + } + return this.convertNode(node, b, value2); + } + /** One schema node's conversion: its first keyword among `const`, `enum`, the unions, `allOf` and `type`. */ + convertNode(node, b, value2) { + if (Object.hasOwn(node, "const")) + return convertLiteral(node.const, value2); + if (Array.isArray(node.enum)) + return this.convertEnum(node, node.enum, b, value2); + if (Array.isArray(node.anyOf)) + return this.convertUnion(node, node.anyOf, b, value2); + if (Array.isArray(node.oneOf)) + return this.convertUnion(node, node.oneOf, b, value2); + if (Array.isArray(node.allOf)) { + let next = value2; + for (const member of node.allOf) + next = this.convert(member, b, next); + return this.convertMembers(node, b, next); + } + switch (node.type) { + case "string": + return orValue(tryString(value2), value2); + case "number": + return orValue(tryNumber(value2), value2); + case "integer": { + const result = tryNumber(value2); + return result === void 0 ? value2 : Math.trunc(result.value); + } + case "boolean": + return orValue(tryBoolean(value2), value2); + case "null": + return orValue(tryNull(value2), value2); + case "array": + return this.convertArray(node, b, value2); + case "object": + return this.convertObject(node, b, value2); + case void 0: + if (node.properties !== void 0 || node.patternProperties !== void 0) { + return this.convertObject(node, b, value2); + } + if (node.items !== void 0 || node.prefixItems !== void 0) + return this.convertArray(node, b, value2); + return value2; + default: + return value2; + } + } + /** An enum is a union of its values: a member stays, else the first value the input converts to. */ + convertEnum(node, values, base, value2) { + if (values.some((member) => member === value2)) + return value2; + for (const member of values) { + const candidate = convertLiteral(member, value2); + if (candidate !== value2 && this.passes(node, base, candidate) === true) + return candidate; + } + return value2; + } + /** + * A value one branch accepts stays; else the first branch's conversion that the whole union accepts. Branches are + * converted in order and only until one is accepted, so a later recursive branch is not entered needlessly. A node + * that also declares members (plain JSON Schema; TypeBox's never do) converts them into each candidate before it is + * checked, so a branch is not discarded for a sibling property that is converted after it. + */ + convertUnion(node, branches, base, value2) { + if (branches.some((branch) => this.passes(branch, base, value2) === true)) + return this.convertMembers(node, base, value2); + for (const branch of branches) { + const candidate = this.convertMembers(node, base, this.convert(branch, base, value2)); + if (this.passes(node, base, candidate) === true) + return candidate; + } + return this.convertMembers(node, base, value2); + } + /** The object members of a union or intersection node, when it declares any. */ + convertMembers(node, base, value2) { + const declares = node.properties !== void 0 || node.patternProperties !== void 0 || node.additionalProperties !== void 0; + return declares ? this.convertObject(node, base, value2) : value2; + } + convertArray(node, base, value2) { + const prefix = Array.isArray(node.items) ? node.items : Array.isArray(node.prefixItems) ? node.prefixItems : void 0; + if (prefix !== void 0) { + if (!Array.isArray(value2)) + return value2; + const rest = Array.isArray(node.items) ? void 0 : node.items; + return mapItems(value2, (item2, i) => { + const schema = i < prefix.length ? prefix[i] : rest; + return schema === void 0 ? item2 : this.convert(schema, base, item2); + }); + } + if (Array.isArray(value2)) { + return node.items === void 0 ? value2 : mapItems(value2, (item2) => this.convert(node.items, base, item2)); + } + const item = node.items; + return [isSchemaObject(item) ? this.reenter(item, base, value2, false) : value2]; + } + convertObject(node, base, value2) { + if (!isPlainObject(value2)) + return value2; + const properties = isSchemaObject(node.properties) ? node.properties : {}; + const patterns = isSchemaObject(node.patternProperties) ? Object.entries(node.patternProperties).map(([pattern, schema]) => [sharedRegex(pattern), schema]) : []; + const additional = isSchemaObject(node.additionalProperties) ? node.additionalProperties : void 0; + let out; + for (const key of Object.keys(value2)) { + if (value2[key] === void 0) + continue; + let next = value2[key]; + let matched = Object.hasOwn(properties, key); + if (matched) + next = this.convert(properties[key], base, next); + for (const [pattern, schema] of patterns) { + if (!pattern.test(key)) + continue; + matched = true; + next = this.convert(schema, base, next); + } + if (!matched && additional !== void 0) + next = this.convert(additional, base, next); + if (next !== value2[key]) { + out ??= { ...value2 }; + out[key] = next; + } + } + return out ?? value2; + } + // --------------------------------------------------------------------------------------------------------------- + // Pass 3: primitive coercion by type + // --------------------------------------------------------------------------------------------------------------- + coerceTypes(value2, schema, base) { + if (!isSchemaObject(schema)) + return value2; + const b = this.baseOf(schema, base); + let next = value2; + if (Array.isArray(schema.allOf)) + for (const member of schema.allOf) + next = this.coerceTypes(next, member, b); + if (Array.isArray(schema.anyOf)) + next = this.coerceUnion(next, schema.anyOf, b); + if (Array.isArray(schema.oneOf)) + next = this.coerceUnion(next, schema.oneOf, b); + const types = typesOf(schema); + const matchesUnionMember = types.length > 1 && types.some((type) => matchesType(next, type)); + if (types.length > 0 && !matchesUnionMember) { + for (const type of types) { + const candidate = coercePrimitive(next, type); + if (candidate !== next) { + next = candidate; + break; + } + } + } + if (types.includes("object") && isPlainObject(next)) + next = this.coerceProperties(next, schema, b); + if (types.includes("array") && Array.isArray(next)) + next = this.coerceItems(next, schema, b); + return next; + } + coerceUnion(value2, branches, base) { + for (const branch of branches) + if (this.passes(branch, base, value2) === true) + return value2; + for (const branch of branches) { + const coerced = this.coerceTypes(value2, branch, base); + if (this.passes(branch, base, coerced) === true) + return coerced; + } + return value2; + } + coerceProperties(value2, schema, base) { + const properties = isSchemaObject(schema.properties) ? schema.properties : {}; + const patterns = isSchemaObject(schema.patternProperties) ? Object.entries(schema.patternProperties).map(([pattern, item]) => [sharedRegex(pattern), item]) : []; + const additional = isSchemaObject(schema.additionalProperties) ? schema.additionalProperties : void 0; + let out; + const set = (key, next) => { + if (next === (out ?? value2)[key]) + return; + out ??= { ...value2 }; + out[key] = next; + }; + for (const [key, property] of Object.entries(properties)) { + if (key in value2) + set(key, this.coerceTypes(value2[key], property, base)); + } + for (const key of Object.keys(value2)) { + let matched = Object.hasOwn(properties, key); + for (const [pattern, item] of patterns) { + if (!pattern.test(key)) + continue; + matched = true; + set(key, this.coerceTypes((out ?? value2)[key], item, base)); + } + if (!matched && additional !== void 0) + set(key, this.coerceTypes(value2[key], additional, base)); + } + return out ?? value2; + } + coerceItems(value2, schema, base) { + return mapItems(value2, (item, i) => { + const itemSchema = itemSchemaAt(schema, i); + return isSchemaObject(itemSchema) ? this.coerceTypes(item, itemSchema, base) : item; + }); + } +}; +function itemSchemaAt(schema, index) { + if (Array.isArray(schema.items)) + return schema.items[index]; + if (Array.isArray(schema.prefixItems) && index < schema.prefixItems.length) + return schema.prefixItems[index]; + return schema.items; +} +function mapItems(items, map2) { + let out; + for (let i = 0; i < items.length; i++) { + const next = map2(items[i], i); + if (next === items[i]) + continue; + out ??= items.slice(); + out[i] = next; + } + return out ?? items; +} +function orValue(result, value2) { + return result === void 0 ? value2 : result.value; +} +function convertLiteral(literal, value2) { + if (literal === value2) + return value2; + const result = typeof literal === "string" ? tryString(value2) : typeof literal === "number" ? tryNumber(value2) : typeof literal === "boolean" ? tryBoolean(value2) : void 0; + return result !== void 0 && result.value === literal ? result.value : value2; +} +function tryString(value2) { + if (typeof value2 === "bigint" || typeof value2 === "boolean" || isFiniteNumber(value2)) + return { value: String(value2) }; + if (value2 === null) + return { value: "null" }; + if (typeof value2 === "string") + return { value: value2 }; + if (value2 === void 0) + return { value: "" }; + return void 0; +} +var MAX_BIGINT = BigInt(Number.MAX_SAFE_INTEGER); +var MIN_BIGINT = BigInt(Number.MIN_SAFE_INTEGER); +function tryNumber(value2) { + if (typeof value2 === "bigint") + return value2 <= MAX_BIGINT && value2 >= MIN_BIGINT ? { value: Number(value2) } : void 0; + if (typeof value2 === "boolean") + return { value: value2 ? 1 : 0 }; + if (isFiniteNumber(value2)) + return { value: value2 }; + if (value2 === null || value2 === void 0) + return { value: 0 }; + if (typeof value2 !== "string") + return void 0; + const coerced = +value2; + if (Number.isFinite(coerced)) + return { value: coerced }; + const lowercase = value2.toLowerCase(); + if (lowercase === "false") + return { value: 0 }; + if (lowercase === "true") + return { value: 1 }; + const big = bigIntOfString(value2); + return big !== void 0 && big <= MAX_BIGINT && big >= MIN_BIGINT ? { value: Number(big) } : void 0; +} +function bigIntOfString(value2) { + if (/^-?(0|[1-9]\d*)n$/.test(value2)) + return BigInt(value2.slice(0, -1)); + if (/^-?(0|[1-9]\d*)\.\d+$/.test(value2)) + return BigInt(value2.split(".")[0]); + if (/^-?(0|[1-9]\d*)$/.test(value2)) + return BigInt(value2); + return void 0; +} +function tryBoolean(value2) { + if (typeof value2 === "bigint") + return value2 === 0n ? { value: false } : value2 === 1n ? { value: true } : void 0; + if (typeof value2 === "boolean") + return { value: value2 }; + if (isFiniteNumber(value2)) + return value2 === 0 ? { value: false } : value2 === 1 ? { value: true } : void 0; + if (value2 === null || value2 === void 0) + return { value: false }; + if (typeof value2 !== "string") + return void 0; + const lowercase = value2.toLowerCase(); + if (lowercase === "false" || value2 === "0") + return { value: false }; + if (lowercase === "true" || value2 === "1") + return { value: true }; + return void 0; +} +function tryNull(value2) { + if (typeof value2 === "bigint") + return value2 === 0n ? { value: null } : void 0; + if (typeof value2 === "boolean") + return value2 === false ? { value: null } : void 0; + if (isFiniteNumber(value2)) + return value2 === 0 ? { value: null } : void 0; + if (value2 === null || value2 === void 0) + return { value: null }; + if (typeof value2 !== "string") + return void 0; + const lowercase = value2.toLowerCase(); + return lowercase === "undefined" || lowercase === "null" || value2 === "" || value2 === "0" ? { value: null } : void 0; +} +function isFiniteNumber(value2) { + return typeof value2 === "number" && Number.isFinite(value2); +} +function typesOf(schema) { + if (typeof schema.type === "string") + return [schema.type]; + if (Array.isArray(schema.type)) + return schema.type.filter((type) => typeof type === "string"); + return []; +} +function matchesType(value2, type) { + switch (type) { + case "number": + return typeof value2 === "number"; + case "integer": + return typeof value2 === "number" && Number.isInteger(value2); + case "boolean": + return typeof value2 === "boolean"; + case "string": + return typeof value2 === "string"; + case "null": + return value2 === null; + case "array": + return Array.isArray(value2); + case "object": + return isPlainObject(value2); + default: + return false; + } +} +function coercePrimitive(value2, type) { + switch (type) { + case "number": + case "integer": { + if (value2 === null) + return 0; + if (typeof value2 === "string" && value2.trim() !== "") { + const parsed = Number(value2); + if (type === "number" ? Number.isFinite(parsed) : Number.isInteger(parsed)) + return parsed; + } + if (typeof value2 === "boolean") + return value2 ? 1 : 0; + return value2; + } + case "boolean": + if (value2 === null) + return false; + if (value2 === "true" || value2 === 1) + return true; + if (value2 === "false" || value2 === 0) + return false; + return value2; + case "string": + if (value2 === null) + return ""; + if (typeof value2 === "number" || typeof value2 === "boolean") + return String(value2); + return value2; + case "null": + return value2 === "" || value2 === 0 || value2 === false ? null : value2; + default: + return value2; + } +} +function isPlainObject(value2) { + return typeof value2 === "object" && value2 !== null && !Array.isArray(value2); +} + +// node_modules/@earendil-works/pi-schema/dist/checks.js +var ALWAYS = () => true; +var NEVER = () => false; +var IS_STRING = (v) => typeof v === "string"; +var IS_NUMBER = (v) => typeof v === "number"; +var IS_INTEGER = (v) => Number.isInteger(v); +var IS_BOOLEAN = (v) => typeof v === "boolean"; +var IS_NULL = (v) => v === null; +var IS_ARRAY = (v) => Array.isArray(v); +var IS_BYTES = (v) => v instanceof Uint8Array; +var IS_OBJECT = (v) => isObject(v); +function isObject(v) { + return typeof v === "object" && v !== null && !Array.isArray(v) && !(v instanceof Uint8Array); +} +var TYPE_CHECKS = /* @__PURE__ */ new Map([ + ["string", IS_STRING], + ["number", IS_NUMBER], + ["integer", IS_INTEGER], + ["boolean", IS_BOOLEAN], + ["null", IS_NULL], + ["array", IS_ARRAY], + ["object", IS_OBJECT], + ["bytes", IS_BYTES] +]); +var LEAF_CODES = /* @__PURE__ */ new Map([ + [IS_STRING, 1], + [IS_NUMBER, 2], + [IS_INTEGER, 3], + [IS_BOOLEAN, 4], + [IS_NULL, 5], + [ALWAYS, 6] +]); +function leafOk(code, x) { + switch (code) { + case 1: + return typeof x === "string"; + case 2: + return typeof x === "number"; + case 3: + return Number.isInteger(x); + case 4: + return typeof x === "boolean"; + case 5: + return x === null; + default: + return true; + } +} +var GUARD_NONE = 0; +var GUARD_STRICT = 1; +var GUARD_LOOSE = 2; +var CODE = 7; +var REQUIRED = 8; +var INHERITED = 16; +function and(parts) { + switch (parts.length) { + case 0: + return ALWAYS; + case 1: + return parts[0]; + case 2: { + const [a, b] = parts; + return (v) => a(v) && b(v); + } + case 3: { + const [a, b, c] = parts; + return (v) => a(v) && b(v) && c(v); + } + default: + return (v) => { + for (let i = 0; i < parts.length; i++) + if (!parts[i](v)) + return false; + return true; + }; + } +} +function indirect(slot) { + return (v) => slot.check(v); +} +function scopedCheck(scope, uri, check) { + return (v) => { + scope.push(uri); + const ok2 = check(v); + scope.pop(); + return ok2; + }; +} +function typeCheck(types) { + const checks = types.map((t) => (typeof t === "string" ? TYPE_CHECKS.get(t) : void 0) ?? NEVER); + if (checks.length === 1) + return checks[0]; + return (v) => { + for (let i = 0; i < checks.length; i++) + if (checks[i](v)) + return true; + return false; + }; +} +function bytesCheck(min, max) { + return (v) => v instanceof Uint8Array && v.length >= min && v.length <= max; +} +function codePoints(s) { + let n = 0; + for (let i = 0; i < s.length; i++, n++) { + const c = s.charCodeAt(i); + if (c >= 55296 && c <= 56319 && i + 1 < s.length) { + const next = s.charCodeAt(i + 1); + if (next >= 56320 && next <= 57343) + i++; + } + } + return n; +} +function lengthOk(s, min, max) { + const units = s.length; + if (units < min) + return false; + if (units < 2 * min && codePoints(s) < min) + return false; + return units <= max || codePoints(s) <= max; +} +function stringCheck(strict, min, max, re, format) { + if (re !== void 0 && min === 0 && max === Number.POSITIVE_INFINITY && format === void 0) { + return strict ? (v) => typeof v === "string" && re.test(v) : (v) => typeof v !== "string" || re.test(v); + } + if (re === void 0 && format === void 0) { + return strict ? (v) => typeof v === "string" && lengthOk(v, min, max) : (v) => typeof v !== "string" || lengthOk(v, min, max); + } + return (v) => { + if (typeof v !== "string") + return !strict; + return lengthOk(v, min, max) && (re === void 0 || re.test(v)) && (format === void 0 || format(v)); + }; +} +function numberCheck(mode, min, max, xmin, xmax, multipleOf) { + return (v) => { + if (typeof v !== "number") + return mode === 0; + if (mode === 2 && !Number.isInteger(v)) + return false; + return v >= min && v <= max && v > xmin && v < xmax && (multipleOf === void 0 || isMultiple(v, multipleOf)); + }; +} +function isMultiple(v, d) { + const q = v / d; + if (Number.isInteger(q)) + return true; + if (!Number.isFinite(q)) + return v % d === 0; + const [vc, ve] = decimal(v); + const [dc, de] = decimal(d); + return ve >= de ? vc * 10n ** BigInt(ve - de) % dc === 0n : vc % (dc * 10n ** BigInt(de - ve)) === 0n; +} +function decimal(x) { + const [mantissa, exponent = "0"] = Math.abs(x).toString().split("e"); + const point = mantissa.indexOf("."); + const fraction = point < 0 ? 0 : mantissa.length - point - 1; + return [BigInt(mantissa.replace(".", "")), Number(exponent) - fraction]; +} +function guardObject(inner, strict) { + return strict ? (v) => isObject(v) && inner(v) : (v) => !isObject(v) || inner(v); +} +function guardArray(inner, strict) { + return strict ? (v) => Array.isArray(v) && inner(v) : (v) => !Array.isArray(v) || inner(v); +} +function declaredObject(entries2, requiredCount, closed, guard) { + const n = entries2.length; + return (v) => { + if (guard !== GUARD_NONE && !isObject(v)) + return guard === GUARD_LOOSE; + const o = v; + let required = 0; + for (const k in o) { + let i = 0; + while (i < n && entries2[i] !== k) + i += 3; + if (i === n) { + if (closed) + return false; + continue; + } + const x = o[k]; + if (x === void 0) + continue; + const f = entries2[i + 2]; + const c = f & CODE; + if (c !== 0 ? !leafOk(c, x) : !entries2[i + 1](x)) + return false; + if (f & REQUIRED) + required++; + } + return required === requiredCount; + }; +} +function openObject(entries2, keys, extraRequired, patterns, additional, guard) { + const known = patterns.length > 0 || additional !== void 0 ? new Set(keys) : void 0; + return (v) => { + if (guard !== GUARD_NONE && !isObject(v)) + return guard === GUARD_LOOSE; + const o = v; + for (let i = 0; i < entries2.length; i += 3) { + const k = entries2[i]; + const x = o[k]; + const f = entries2[i + 2]; + if (x === void 0 || f & INHERITED && !Object.hasOwn(o, k)) { + if (f & REQUIRED) + return false; + continue; + } + const c = f & CODE; + if (c !== 0 ? !leafOk(c, x) : !entries2[i + 1](x)) + return false; + } + for (let i = 0; i < extraRequired.length; i++) + if (!Object.hasOwn(o, extraRequired[i])) + return false; + if (known === void 0) + return true; + for (const k of Object.keys(o)) { + let matched = known.has(k); + for (let i = 0; i < patterns.length; i++) { + if (patterns[i][0].test(k)) { + matched = true; + if (!patterns[i][1](o[k])) + return false; + } + } + if (!matched && additional !== void 0 && !additional(o[k])) + return false; + } + return true; + }; +} +function propertyNamesCheck(check) { + return (v) => { + for (const k in v) + if (!check(k)) + return false; + return true; + }; +} +function propertyCountCheck(min, max) { + return (v) => { + let n = 0; + for (const _ in v) + n++; + return n >= min && n <= max; + }; +} +function dependentRequiredCheck(entries2) { + return (v) => { + const o = v; + for (const [k, list] of entries2) + if (Object.hasOwn(o, k)) { + for (const r of list) + if (!Object.hasOwn(o, r)) + return false; + } + return true; + }; +} +function dependentSchemasCheck(entries2) { + return (v) => { + for (const [k, check] of entries2) + if (Object.hasOwn(v, k) && !check(v)) + return false; + return true; + }; +} +function listCheck(guard, min, max, prefix, rest) { + const n = prefix.length; + const code = rest === void 0 ? 0 : LEAF_CODES.get(rest) ?? 0; + return (v) => { + if (!Array.isArray(v)) + return guard === GUARD_LOOSE; + const length = v.length; + if (length < min || length > max) + return false; + const m = length < n ? length : n; + for (let i = 0; i < m; i++) + if (!prefix[i](v[i])) + return false; + if (rest === void 0) + return true; + if (code !== 0) { + for (let i = n; i < length; i++) + if (!leafOk(code, v[i])) + return false; + return true; + } + for (let i = n; i < length; i++) + if (!rest(v[i])) + return false; + return true; + }; +} +function itemsCheck(prefix, rest) { + return listCheck(GUARD_NONE, 0, Number.POSITIVE_INFINITY, prefix, rest); +} +function lengthCheck(min, max) { + return (v) => v.length >= min && v.length <= max; +} +function containsCheck(check, min, max) { + return (v) => { + const list = v; + let n = 0; + for (let i = 0; i < list.length; i++) + if (check(list[i]) && ++n > max) + return false; + return n >= min; + }; +} +function uniqueCheck(v) { + const list = v; + for (let i = 1; i < list.length; i++) + for (let j = 0; j < i; j++) + if (deepEqual(list[i], list[j])) + return false; + return true; +} +function enumCheck(values) { + const primitives = values.filter((x) => typeof x !== "object" || x === null); + if (primitives.length === values.length && primitives.length <= 8) { + return (v) => { + for (let i = 0; i < primitives.length; i++) + if (primitives[i] === v) + return true; + return false; + }; + } + const set = new Set(primitives); + const objects = values.filter((x) => typeof x === "object" && x !== null); + return (v) => set.has(v) || typeof v === "object" && objects.some((o) => deepEqual(v, o)); +} +function constCheck(value2) { + if (typeof value2 !== "object" || value2 === null) + return (v) => v === value2; + return (v) => deepEqual(v, value2); +} +function anyCheck(checks) { + return (v) => { + for (let i = 0; i < checks.length; i++) + if (checks[i](v)) + return true; + return false; + }; +} +function oneCheck(checks) { + return (v) => { + let n = 0; + for (let i = 0; i < checks.length; i++) + if (checks[i](v) && ++n > 1) + return false; + return n === 1; + }; +} +function notCheck(check) { + return (v) => !check(v); +} +function ifCheck(cond, then, otherwise) { + return (v) => cond(v) ? then(v) : otherwise(v); +} +function taggedCheck(key, tags, checks) { + return (v) => { + if (!isObject(v)) + return false; + const tag = v[key]; + for (let i = 0; i < tags.length; i++) + if (tags[i] === tag) + return checks[i](v); + return false; + }; +} +function deepEqual(a, b) { + if (a === b) + return true; + if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) + return false; + const array = Array.isArray(a); + if (array !== Array.isArray(b)) + return false; + if (array) { + const x2 = a; + const y2 = b; + if (x2.length !== y2.length) + return false; + for (let i = 0; i < x2.length; i++) + if (!deepEqual(x2[i], y2[i])) + return false; + return true; + } + const x = a; + const y = b; + const keys = Object.keys(x); + if (keys.length !== Object.keys(y).length) + return false; + for (const k of keys) + if (!Object.hasOwn(y, k) || !deepEqual(x[k], y[k])) + return false; + return true; +} + +// node_modules/@earendil-works/pi-schema/dist/annotations.js +function newAnnotations() { + return { props: null, allProps: false, items: 0, allItems: false, indexes: null }; +} +function merge(a, b) { + if (b.props !== null) + for (const k of b.props) + addProp(a, k); + a.allProps ||= b.allProps; + if (b.items > a.items) + a.items = b.items; + a.allItems ||= b.allItems; + if (b.indexes !== null) + for (const i of b.indexes) + addIndex(a, i); +} +function addProp(a, key) { + if (a.props === null) + a.props = /* @__PURE__ */ new Set(); + a.props.add(key); +} +function addIndex(a, index) { + if (a.indexes === null) + a.indexes = /* @__PURE__ */ new Set(); + a.indexes.add(index); +} +var ANNOTATING_TRUE = () => true; +var ANNOTATING_FALSE = () => false; +function indirectAnnotating(slot) { + return (v, a) => slot.check(v, a); +} +function ignoreAnnotations(check) { + return (v) => check(v); +} +function withFreshAnnotations(check) { + return (v) => check(v, newAnnotations()); +} +function scopedAnnotating(scope, uri, check) { + return (v, a) => { + scope.push(uri); + const ok2 = check(v, a); + scope.pop(); + return ok2; + }; +} +function isolateAnnotations(check) { + return (v, a) => { + const b = newAnnotations(); + if (!check(v, b)) + return false; + merge(a, b); + return true; + }; +} +function annotateAll(plain, parts) { + return (v, a) => { + if (!plain(v)) + return false; + for (let i = 0; i < parts.length; i++) + if (!parts[i](v, a)) + return false; + return true; + }; +} +function annotateObject(props, patterns, additional) { + const declared = new Set(props.map(([k]) => k)); + return (v, a) => { + if (!isObject(v)) + return true; + const seen = []; + for (const [k, check] of props) { + if (!Object.hasOwn(v, k)) + continue; + if (!check(v[k])) + return false; + seen.push(k); + } + for (const k of Object.keys(v)) { + let matched = declared.has(k); + for (const [re, check] of patterns) { + if (re.test(k)) { + if (!check(v[k])) + return false; + matched = true; + seen.push(k); + } + } + if (!matched && additional !== void 0) { + if (!additional(v[k])) + return false; + seen.push(k); + } + } + for (const k of seen) + addProp(a, k); + return true; + }; +} +function annotateArray(prefix, rest, contains, minContains, maxContains) { + return (v, a) => { + if (!Array.isArray(v)) + return true; + const m = Math.min(v.length, prefix.length); + for (let i = 0; i < m; i++) + if (!prefix[i](v[i])) + return false; + if (rest !== void 0) { + for (let i = prefix.length; i < v.length; i++) + if (!rest(v[i])) + return false; + a.allItems = true; + } + if (m > a.items) + a.items = m; + if (contains !== void 0) { + let n = 0; + for (let i = 0; i < v.length; i++) { + if (contains(v[i])) { + n++; + addIndex(a, i); + } + } + if (n < minContains || n > maxContains) + return false; + } + return true; + }; +} +function annotateAny(branches, exactlyOne) { + return (v, a) => { + let n = 0; + for (const branch of branches) { + const b = newAnnotations(); + if (branch(v, b)) { + n++; + merge(a, b); + } + } + return exactlyOne ? n === 1 : n > 0; + }; +} +function annotateIf(cond, then, otherwise) { + return (v, a) => { + const b = newAnnotations(); + if (cond(v, b)) { + merge(a, b); + return then === void 0 || then(v, a); + } + return otherwise === void 0 || otherwise(v, a); + }; +} +function annotateDependent(key, check) { + return (v, a) => !isObject(v) || !Object.hasOwn(v, key) || check(v, a); +} +function unevaluatedProperties(check) { + return (v, a) => { + if (!isObject(v) || a.allProps) + return true; + for (const k of Object.keys(v)) + if (!(a.props?.has(k) ?? false) && !check(v[k])) + return false; + a.allProps = true; + return true; + }; +} +function unevaluatedItems(check) { + return (v, a) => { + if (!Array.isArray(v) || a.allItems) + return true; + for (let i = a.items; i < v.length; i++) + if (!(a.indexes?.has(i) ?? false) && !check(v[i])) + return false; + a.allItems = true; + return true; + }; +} + +// node_modules/@earendil-works/pi-schema/dist/compiler.js +var RESTART = /* @__PURE__ */ Symbol("restart"); +var Compiler = class { + r; + draft; + formats; + /** Whether checks track the dynamic scope (the schema has a bookended `$dynamicRef`). */ + dynamic; + schema; + scope; + cache = /* @__PURE__ */ new Map(); + annotatingCache = /* @__PURE__ */ new Map(); + constructor(schema, options, dynamic) { + this.schema = schema; + this.r = new Resolver(schema, options); + this.draft = this.r.draft; + this.formats = options.formats; + this.dynamic = dynamic; + this.scope = []; + } + /** The check of the root schema. */ + root() { + return this.check(this.schema, this.r.rootBase); + } + /** The check of a schema (cached by schema object; a recursive reference goes through an indirection). */ + check(node, base) { + if (node === true) + return ALWAYS; + if (node === false) + return NEVER; + if (!isSchemaObject(node)) + throw new Error(`A schema must be an object or a boolean, got ${JSON.stringify(node)}`); + const cached = this.cache.get(node); + if (cached !== void 0) + return cached; + const bare = this.bareType(node); + if (bare !== void 0) + return bare; + const slot = { check: void 0 }; + this.cache.set(node, indirect(slot)); + const check = this.build(node, this.r.baseFor(node, base)); + slot.check = check; + this.cache.set(node, check); + return check; + } + build(node, base) { + if (this.draft === "7" && typeof node.$ref === "string") + return this.ref(node.$ref, base); + if (this.draft !== "7" && (node.unevaluatedProperties !== void 0 || node.unevaluatedItems !== void 0)) { + return withFreshAnnotations(this.annotating(node, base)); + } + const check = this.plain(node, base); + return this.dynamic && this.r.isResource(node, base) ? scopedCheck(this.scope, base, check) : check; + } + /** + * The module-level check of a schema that is only a `type` (with annotations), `ALWAYS` for one with no assertion + * at all; undefined for every other schema. These are the most common subschemas and cost nothing to share. + */ + bareType(node) { + let type; + for (const k in node) { + if (k === "type") + type = node.type; + else if (!NEUTRAL_KEYWORDS.has(k)) + return void 0; + } + if (type === void 0) + return ALWAYS; + return typeof type === "string" ? TYPE_CHECKS.get(type) : void 0; + } + /** Every keyword of `node`, not tracking annotations. */ + plain(node, base) { + const parts = []; + const types = node.type === void 0 ? void 0 : Array.isArray(node.type) ? node.type : [node.type]; + const single = types?.length === 1 ? types[0] : void 0; + let typeInGroup = false; + if (single === "bytes") { + parts.push(bytesCheck(number(node.minLength) ?? 0, number(node.maxLength) ?? Number.POSITIVE_INFINITY)); + typeInGroup = true; + } else { + const string = this.stringGroup(node, single === "string"); + if (string !== void 0) + parts.push(string); + const numeric = single === "number" || single === "integer"; + const number2 = this.numberGroup(node, single === "number" ? 1 : single === "integer" ? 2 : 0); + if (number2 !== void 0) + parts.push(number2); + const object = this.objectGroup(node, base, single === "object"); + if (object !== void 0) + parts.push(object); + const array = this.arrayGroup(node, base, single === "array"); + if (array !== void 0) + parts.push(array); + typeInGroup = single === "string" && string !== void 0 || numeric && number2 !== void 0 || single === "object" && object !== void 0 || single === "array" && array !== void 0; + } + if (types !== void 0 && !typeInGroup) + parts.unshift(typeCheck(types)); + this.inPlace(node, base, parts); + return and(parts); + } + // The keywords that apply to the value itself, whatever its type. + inPlace(node, base, parts) { + if (Array.isArray(node.enum)) + parts.push(enumCheck(node.enum)); + if (Object.hasOwn(node, "const")) + parts.push(constCheck(node.const)); + if (Array.isArray(node.allOf)) + parts.push(and(node.allOf.map((s) => this.check(s, base)))); + if (Array.isArray(node.anyOf)) + parts.push(this.union(node.anyOf, base, false)); + if (Array.isArray(node.oneOf)) + parts.push(this.union(node.oneOf, base, true)); + if (node.not !== void 0) + parts.push(notCheck(this.check(node.not, base))); + if (node.if !== void 0 && (node.then !== void 0 || node.else !== void 0)) { + const then = node.then === void 0 ? ALWAYS : this.check(node.then, base); + const otherwise = node.else === void 0 ? ALWAYS : this.check(node.else, base); + parts.push(ifCheck(this.check(node.if, base), then, otherwise)); + } + if (typeof node.$ref === "string") + parts.push(this.ref(node.$ref, base)); + if (this.draft !== "7" && typeof node.$dynamicRef === "string") { + parts.push(this.dynamicRef(node.$dynamicRef, base)); + } + } + union(branches, base, exactlyOne) { + const checks = () => branches.map((s) => this.check(s, base)); + return constUnion(branches, exactlyOne) ?? this.tagged(branches, base) ?? (exactlyOne ? oneCheck(checks()) : anyCheck(checks())); + } + ref(ref, base) { + const target = this.r.resolve(ref, base); + const check = this.check(target.node, target.base); + return this.dynamic && target.base !== base ? scopedCheck(this.scope, target.base, check) : check; + } + dynamicRef(ref, base) { + const resolved = this.bookended(ref, base); + if (resolved === void 0) + return this.ref(ref, base); + const { initial, name } = resolved; + const fallback = this.check(initial.node, initial.base); + const lookup = this.dynamicLookup(name, (node, b) => this.check(node, b)); + const scope = this.scope; + return (v) => { + for (let i = 0; i < scope.length; i++) { + const check = lookup(scope[i]); + if (check !== null) + return check(v); + } + return fallback(v); + }; + } + /** The initial target and anchor name of a `$dynamicRef` that resolves dynamically, or undefined for a plain ref. */ + bookended(ref, base) { + const initial = this.r.resolve(ref, base); + const uri = resolveUri(ref, base); + const name = uri.slice(documentOf(uri).length + 1); + const anchored = isSchemaObject(initial.node) && initial.node.$dynamicAnchor === name; + if (name === "" || name.startsWith("/") || !anchored) + return void 0; + if (!this.dynamic) + throw RESTART; + return { initial, name }; + } + // Resolved at run time against the dynamic scope, so a schema with a bookended $dynamicRef keeps its compiler. + dynamicLookup(name, compile2) { + const found = /* @__PURE__ */ new Map(); + return (uri) => { + const key = `${uri}#${name}`; + let check = found.get(key); + if (check === void 0) { + const target = this.r.dynamicAnchors.get(key); + check = target === void 0 ? null : compile2(target, this.r.baseOf.get(target) ?? uri); + found.set(key, check); + } + return check; + }; + } + /** anyOf/oneOf whose branches are all objects with a distinct required const on one key: the tag picks the branch. */ + tagged(branches, base) { + if (branches.length < 2) + return void 0; + const resolved = []; + for (const branch of branches) { + if (!isSchemaObject(branch)) + return void 0; + const ref = typeof branch.$ref === "string" && Object.keys(branch).length === 1 ? branch.$ref : void 0; + const node = ref === void 0 ? branch : this.r.resolve(ref, this.r.baseFor(branch, base)).node; + if (!isSchemaObject(node)) + return void 0; + resolved.push(node); + } + const first = resolved[0].properties; + if (!isSchemaObject(first)) + return void 0; + for (const key of Object.keys(first)) { + const tags = resolved.map((s) => tagOf(s, key)); + if (tags.some((t) => t === void 0) || new Set(tags).size !== tags.length) + continue; + return taggedCheck(key, tags, branches.map((s) => this.check(s, base))); + } + return void 0; + } + stringGroup(node, strict) { + const min = number(node.minLength) ?? 0; + const max = number(node.maxLength) ?? Number.POSITIVE_INFINITY; + const re = typeof node.pattern === "string" ? sharedRegex(node.pattern) : void 0; + const format = this.formats !== void 0 && typeof node.format === "string" && Object.hasOwn(this.formats, node.format) ? this.formats[node.format] : void 0; + if (min === 0 && max === Number.POSITIVE_INFINITY && re === void 0 && format === void 0) + return void 0; + return stringCheck(strict, min, max, re, format); + } + numberGroup(node, mode) { + const min = number(node.minimum); + const max = number(node.maximum); + const xmin = number(node.exclusiveMinimum); + const xmax = number(node.exclusiveMaximum); + const multipleOf = number(node.multipleOf); + if ([min, max, xmin, xmax, multipleOf].every((x) => x === void 0)) + return void 0; + const inf = Number.POSITIVE_INFINITY; + return numberCheck(mode, min ?? -inf, max ?? inf, xmin ?? -inf, xmax ?? inf, multipleOf); + } + objectGroup(node, base, strict) { + const onlyCore = OBJECT_EXTRAS.every((k) => node[k] === void 0); + const core = this.objectCore(node, base, onlyCore ? strict ? GUARD_STRICT : GUARD_LOOSE : GUARD_NONE); + if (core !== void 0 && onlyCore) + return core; + const parts = core === void 0 ? [] : [core]; + if (node.propertyNames !== void 0) + parts.push(propertyNamesCheck(this.check(node.propertyNames, base))); + const minProperties = number(node.minProperties); + const maxProperties = number(node.maxProperties); + if (minProperties !== void 0 || maxProperties !== void 0) { + parts.push(propertyCountCheck(minProperties ?? 0, maxProperties ?? Number.POSITIVE_INFINITY)); + } + const dependentRequired = []; + const dependentSchemas = []; + if (this.draft !== "7") { + for (const [k, list] of entries(node.dependentRequired)) + if (Array.isArray(list)) + dependentRequired.push([k, list]); + for (const [k, s] of entries(node.dependentSchemas)) + dependentSchemas.push([k, this.check(s, base)]); + } + for (const [k, d] of entries(node.dependencies)) { + if (Array.isArray(d)) + dependentRequired.push([k, d]); + else + dependentSchemas.push([k, this.check(d, base)]); + } + if (dependentRequired.length > 0) + parts.push(dependentRequiredCheck(dependentRequired)); + if (dependentSchemas.length > 0) + parts.push(dependentSchemasCheck(dependentSchemas)); + if (parts.length === 0) + return strict ? IS_OBJECT : void 0; + return guardObject(and(parts), strict); + } + objectCore(node, base, guard) { + const properties = isSchemaObject(node.properties) ? node.properties : {}; + const required = Array.isArray(node.required) ? node.required.filter((k) => typeof k === "string") : []; + const patterns = isSchemaObject(node.patternProperties) ? node.patternProperties : {}; + const additional = node.additionalProperties; + const keys = Object.keys(properties); + const patternKeys = Object.keys(patterns); + if (keys.length + required.length + patternKeys.length === 0 && (additional === void 0 || additional === true)) { + return void 0; + } + const entries2 = []; + for (const k of keys) { + const check = this.check(properties[k], base); + const flags = (LEAF_CODES.get(check) ?? 0) | (required.includes(k) ? REQUIRED : 0) | (k in Object.prototype ? INHERITED : 0); + entries2.push(k, check, flags); + } + const extraRequired = required.filter((k) => !Object.hasOwn(properties, k)); + const pats = patternKeys.map((p) => [sharedRegex(p), this.check(patterns[p], base)]); + const extra = additional === void 0 || additional === true ? void 0 : this.check(additional, base); + if (pats.length === 0 && extraRequired.length === 0 && (extra === void 0 || additional === false)) { + const requiredCount = keys.filter((k) => required.includes(k)).length; + return declaredObject(entries2, requiredCount, additional === false, guard); + } + return openObject(entries2, keys, extraRequired, pats, extra, guard); + } + arrayGroup(node, base, strict) { + let prefix = []; + let rest; + if (Array.isArray(node.items)) { + prefix = node.items; + rest = node.additionalItems; + } else { + if (this.draft !== "7" && Array.isArray(node.prefixItems)) + prefix = node.prefixItems; + rest = node.items; + } + const prefixChecks = prefix.map((s) => this.check(s, base)); + const restCheck = rest === void 0 || rest === true ? void 0 : this.check(rest, base); + const minItems = number(node.minItems); + const maxItems = number(node.maxItems); + if (node.contains === void 0 && node.uniqueItems !== true && (prefixChecks.length > 0 || restCheck !== void 0)) { + const guard = strict ? GUARD_STRICT : GUARD_LOOSE; + return listCheck(guard, minItems ?? 0, maxItems ?? Number.POSITIVE_INFINITY, prefixChecks, restCheck); + } + const parts = []; + if (prefixChecks.length > 0 || restCheck !== void 0) + parts.push(itemsCheck(prefixChecks, restCheck)); + if (minItems !== void 0 || maxItems !== void 0) { + parts.push(lengthCheck(minItems ?? 0, maxItems ?? Number.POSITIVE_INFINITY)); + } + if (node.contains !== void 0) { + const min = this.draft === "7" ? 1 : number(node.minContains) ?? 1; + const max = this.draft === "7" ? Number.POSITIVE_INFINITY : number(node.maxContains) ?? Number.POSITIVE_INFINITY; + parts.push(containsCheck(this.check(node.contains, base), min, max)); + } + if (node.uniqueItems === true) + parts.push(uniqueCheck); + if (parts.length === 0) + return strict ? IS_ARRAY : void 0; + return guardArray(and(parts), strict); + } + /** The annotating check of a schema evaluated in place (only below `unevaluatedProperties`/`unevaluatedItems`). */ + annotating(node, base) { + if (node === true) + return ANNOTATING_TRUE; + if (node === false) + return ANNOTATING_FALSE; + if (!isSchemaObject(node)) + throw new Error(`A schema must be an object or a boolean, got ${JSON.stringify(node)}`); + const cached = this.annotatingCache.get(node); + if (cached !== void 0) + return cached; + const slot = { check: void 0 }; + this.annotatingCache.set(node, indirectAnnotating(slot)); + const check = this.buildAnnotating(node, this.r.baseFor(node, base)); + slot.check = check; + this.annotatingCache.set(node, check); + return check; + } + buildAnnotating(node, base) { + const rest = {}; + let annotates = false; + for (const k of Object.keys(node)) { + if (ANNOTATING.has(k)) + annotates = true; + else + rest[k] = node[k]; + } + const plain = this.plain(rest, base); + const parts = []; + if (annotates) + this.annotatingParts(node, base, parts); + const all = parts.length === 0 ? ignoreAnnotations(plain) : annotateAll(plain, parts); + const own = node.unevaluatedProperties !== void 0 || node.unevaluatedItems !== void 0 ? isolateAnnotations(all) : all; + return this.dynamic && this.r.isResource(node, base) ? scopedAnnotating(this.scope, base, own) : own; + } + annotatingParts(node, base, parts) { + if (node.properties !== void 0 || node.patternProperties !== void 0 || node.additionalProperties !== void 0) { + const props = entries(node.properties).map(([k, s]) => [k, this.check(s, base)]); + const pats = entries(node.patternProperties).map(([p, s]) => [sharedRegex(p), this.check(s, base)]); + const extra = node.additionalProperties === void 0 ? void 0 : this.check(node.additionalProperties, base); + parts.push(annotateObject(props, pats, extra)); + } + if (node.prefixItems !== void 0 || node.items !== void 0 || node.contains !== void 0) { + const prefix = Array.isArray(node.prefixItems) ? node.prefixItems : Array.isArray(node.items) ? node.items : []; + const rest = Array.isArray(node.items) ? node.additionalItems : node.items; + parts.push(annotateArray(prefix.map((s) => this.check(s, base)), rest === void 0 ? void 0 : this.check(rest, base), node.contains === void 0 ? void 0 : this.check(node.contains, base), number(node.minContains) ?? 1, number(node.maxContains) ?? Number.POSITIVE_INFINITY)); + } + if (Array.isArray(node.allOf)) + for (const s of node.allOf) + parts.push(this.annotating(s, base)); + if (Array.isArray(node.anyOf)) + parts.push(annotateAny(node.anyOf.map((s) => this.annotating(s, base)), false)); + if (Array.isArray(node.oneOf)) + parts.push(annotateAny(node.oneOf.map((s) => this.annotating(s, base)), true)); + if (node.if !== void 0) { + const then = node.then === void 0 ? void 0 : this.annotating(node.then, base); + const otherwise = node.else === void 0 ? void 0 : this.annotating(node.else, base); + parts.push(annotateIf(this.annotating(node.if, base), then, otherwise)); + } + if (typeof node.$ref === "string") + parts.push(this.annotatingRef(node.$ref, base)); + if (typeof node.$dynamicRef === "string") + parts.push(this.annotatingDynamicRef(node.$dynamicRef, base)); + for (const [k, s] of entries(node.dependentSchemas)) + parts.push(annotateDependent(k, this.annotating(s, base))); + if (node.unevaluatedProperties !== void 0) { + parts.push(unevaluatedProperties(this.check(node.unevaluatedProperties, base))); + } + if (node.unevaluatedItems !== void 0) + parts.push(unevaluatedItems(this.check(node.unevaluatedItems, base))); + } + annotatingRef(ref, base) { + const target = this.r.resolve(ref, base); + const check = this.annotating(target.node, target.base); + return this.dynamic && target.base !== base ? scopedAnnotating(this.scope, target.base, check) : check; + } + annotatingDynamicRef(ref, base) { + const resolved = this.bookended(ref, base); + if (resolved === void 0) + return this.annotatingRef(ref, base); + const { initial, name } = resolved; + const fallback = this.annotating(initial.node, initial.base); + const lookup = this.dynamicLookup(name, (node, b) => this.annotating(node, b)); + const scope = this.scope; + return (v, a) => { + for (let i = 0; i < scope.length; i++) { + const check = lookup(scope[i]); + if (check !== null) + return check(v, a); + } + return fallback(v, a); + }; + } +}; +var NEUTRAL_KEYWORDS = /* @__PURE__ */ new Set([ + "$schema", + "$comment", + "title", + "description", + "default", + "examples", + "deprecated", + "readOnly", + "writeOnly", + "$defs", + "definitions", + "contentEncoding", + "contentMediaType", + "minContains", + "maxContains", + "$vocabulary" +]); +var OBJECT_EXTRAS = [ + "propertyNames", + "minProperties", + "maxProperties", + "dependentRequired", + "dependentSchemas", + "dependencies" +]; +var ANNOTATING = /* @__PURE__ */ new Set([ + "properties", + "patternProperties", + "additionalProperties", + "prefixItems", + "items", + "additionalItems", + "contains", + "minContains", + "maxContains", + "allOf", + "anyOf", + "oneOf", + "if", + "then", + "else", + "$ref", + "$dynamicRef", + "dependentSchemas", + "unevaluatedProperties", + "unevaluatedItems" +]); +function constUnion(branches, exactlyOne) { + const values = []; + for (const branch of branches) { + if (!isSchemaObject(branch) || !Object.hasOwn(branch, "const")) + return void 0; + for (const k of Object.keys(branch)) + if (k !== "const" && !NEUTRAL_KEYWORDS.has(k)) + return void 0; + if (typeof branch.const === "object" && branch.const !== null) + return void 0; + if (exactlyOne && values.includes(branch.const)) + return void 0; + values.push(branch.const); + } + return enumCheck(values); +} +function tagOf(branch, key) { + const properties = branch.properties; + if (branch.type !== "object" || !Array.isArray(branch.required) || !branch.required.includes(key)) + return void 0; + const tag = isSchemaObject(properties) ? properties[key] : void 0; + if (!isSchemaObject(tag) || !Object.hasOwn(tag, "const")) + return void 0; + return typeof tag.const === "object" && tag.const !== null ? void 0 : tag.const; +} +function number(value2) { + return typeof value2 === "number" ? value2 : void 0; +} +function entries(value2) { + return isSchemaObject(value2) ? Object.entries(value2) : []; +} + +// node_modules/@earendil-works/pi-schema/dist/explain.js +var Explainer = class { + c; + schema; + /** + * The errors of `$ref` targets for object values, within one errors() call: a recursive union explains each branch + * to pick the one to blame, and every branch would explain the same children again, exponentially in the depth. + */ + memo = void 0; + constructor(compiler2, schema) { + this.c = compiler2; + this.schema = schema; + compiler2.root(); + } + /** The errors of `value`, at least one (`value` failed the check). */ + errors(value2) { + const out = []; + try { + this.walk(this.schema, this.c.r.rootBase, value2, "", out); + } finally { + this.memo = void 0; + } + if (out.length === 0) + out.push({ path: "", keyword: "schema", message: "invalid" }); + return out; + } + ok(node, base, v) { + return this.c.check(node, base)(v); + } + // For $dynamicRef schemas this check runs outside the original dynamic scope, so their errors are approximate. + walk(node, base, v, path2, out) { + if (node === true) + return; + if (node === false) { + out.push({ path: path2, keyword: "false", message: "no value is allowed here" }); + return; + } + if (!isSchemaObject(node)) + return; + const b = this.c.r.baseFor(node, base); + if (this.ok(node, b, v)) + return; + const start = out.length; + const push = (keyword, message, at = path2) => { + out.push({ path: at, keyword, message }); + }; + if (this.c.draft === "7" && typeof node.$ref === "string") { + this.walkRef(node.$ref, b, v, path2, out); + return; + } + if (node.type !== void 0) { + const types = Array.isArray(node.type) ? node.type : [node.type]; + const checks = types.map((t) => (typeof t === "string" ? TYPE_CHECKS.get(t) : void 0) ?? NEVER); + if (!checks.some((check) => check(v))) { + push("type", `expected ${types.join(" or ")}, got ${typeName(v)}`); + return; + } + } + if (Array.isArray(node.enum) && !enumCheck(node.enum)(v)) + push("enum", `expected one of ${listValues(node.enum)}`); + if (Object.hasOwn(node, "const") && !constCheck(node.const)(v)) + push("const", `expected ${JSON.stringify(node.const)}`); + if (typeof v === "string" && node.type !== "bytes") + this.walkString(node, v, push); + if (v instanceof Uint8Array && node.type === "bytes") + this.walkBytes(node, v, push); + if (typeof v === "number") + this.walkNumber(node, v, push); + if (isObject(v)) + this.walkObject(node, b, v, path2, out, push); + if (Array.isArray(v)) + this.walkArray(node, b, v, path2, out, push); + if (Array.isArray(node.allOf)) + for (const s of node.allOf) + this.walk(s, b, v, path2, out); + if (Array.isArray(node.anyOf)) + this.walkUnion(node.anyOf, b, v, path2, out, false); + if (Array.isArray(node.oneOf)) + this.walkUnion(node.oneOf, b, v, path2, out, true); + if (node.not !== void 0 && this.ok(node.not, b, v)) + push("not", "must not match the schema in not"); + if (node.if !== void 0) { + const branch = this.ok(node.if, b, v) ? node.then : node.else; + if (branch !== void 0) + this.walk(branch, b, v, path2, out); + } + if (typeof node.$ref === "string") + this.walkRef(node.$ref, b, v, path2, out); + if (out.length === start && (node.unevaluatedProperties !== void 0 || node.unevaluatedItems !== void 0)) { + this.walkUnevaluated(node, b, v, path2, out); + } + if (out.length === start && typeof node.$dynamicRef === "string") { + push("$dynamicRef", "does not match the dynamically referenced schema"); + } + } + walkRef(ref, base, v, path2, out) { + const target = this.c.r.resolve(ref, base); + if (target.node === false && this.c.r.isUnresolved(ref, base)) { + out.push({ path: path2, keyword: "$ref", message: `unresolved reference ${ref}` }); + return; + } + if (!isSchemaObject(target.node) || typeof v !== "object" || v === null) { + this.walk(target.node, target.base, v, path2, out); + return; + } + this.memo ??= /* @__PURE__ */ new Map(); + let values = this.memo.get(target.node); + const known = values?.get(v); + if (known !== void 0 && known.path === path2) { + out.push(...known.errors); + return; + } + const errors = []; + this.walk(target.node, target.base, v, path2, errors); + if (values === void 0) { + values = /* @__PURE__ */ new Map(); + this.memo.set(target.node, values); + } + values.set(v, { path: path2, errors }); + out.push(...errors); + } + walkString(node, v, push) { + const n = codePoints(v); + if (typeof node.minLength === "number" && n < node.minLength) { + push("minLength", `expected at least ${node.minLength} characters`); + } + if (typeof node.maxLength === "number" && n > node.maxLength) { + push("maxLength", `expected at most ${node.maxLength} characters`); + } + if (typeof node.pattern === "string" && !sharedRegex(node.pattern).test(v)) { + push("pattern", `expected to match ${node.pattern}`); + } + const formats2 = this.c.formats; + const format = formats2 !== void 0 && typeof node.format === "string" && Object.hasOwn(formats2, node.format) ? formats2[node.format] : void 0; + if (format !== void 0 && !format(v)) + push("format", `expected format ${node.format}`); + } + walkBytes(node, v, push) { + if (typeof node.minLength === "number" && v.length < node.minLength) { + push("minLength", `expected at least ${node.minLength} bytes`); + } + if (typeof node.maxLength === "number" && v.length > node.maxLength) { + push("maxLength", `expected at most ${node.maxLength} bytes`); + } + } + walkNumber(node, v, push) { + if (typeof node.minimum === "number" && v < node.minimum) + push("minimum", `expected >= ${node.minimum}`); + if (typeof node.maximum === "number" && v > node.maximum) + push("maximum", `expected <= ${node.maximum}`); + if (typeof node.exclusiveMinimum === "number" && v <= node.exclusiveMinimum) { + push("exclusiveMinimum", `expected > ${node.exclusiveMinimum}`); + } + if (typeof node.exclusiveMaximum === "number" && v >= node.exclusiveMaximum) { + push("exclusiveMaximum", `expected < ${node.exclusiveMaximum}`); + } + if (typeof node.multipleOf === "number" && !isMultiple(v, node.multipleOf)) { + push("multipleOf", `expected a multiple of ${node.multipleOf}`); + } + } + walkObject(node, b, v, path2, out, push) { + const properties = isSchemaObject(node.properties) ? node.properties : {}; + const required = Array.isArray(node.required) ? node.required : []; + for (const k of required) { + if (typeof k === "string" && (!Object.hasOwn(v, k) || v[k] === void 0)) + push("required", "missing", pointer(path2, k)); + } + for (const k of Object.keys(properties)) { + if (Object.hasOwn(v, k) && v[k] !== void 0) + this.walk(properties[k], b, v[k], pointer(path2, k), out); + } + const patterns = isSchemaObject(node.patternProperties) ? Object.entries(node.patternProperties) : []; + for (const k of Object.keys(v)) { + let matched = Object.hasOwn(properties, k); + for (const [p, s] of patterns) { + if (sharedRegex(p).test(k)) { + matched = true; + this.walk(s, b, v[k], pointer(path2, k), out); + } + } + if (!matched && node.additionalProperties !== void 0) { + if (node.additionalProperties === false) + push("additionalProperties", "unknown field", pointer(path2, k)); + else + this.walk(node.additionalProperties, b, v[k], pointer(path2, k), out); + } + if (node.propertyNames !== void 0 && !this.ok(node.propertyNames, b, k)) { + push("propertyNames", "invalid property name", pointer(path2, k)); + } + } + const count = Object.keys(v).length; + if (typeof node.minProperties === "number" && count < node.minProperties) { + push("minProperties", `expected at least ${node.minProperties} properties`); + } + if (typeof node.maxProperties === "number" && count > node.maxProperties) { + push("maxProperties", `expected at most ${node.maxProperties} properties`); + } + const dependencies = { + ...this.c.draft === "7" || !isSchemaObject(node.dependentRequired) ? {} : node.dependentRequired, + ...isSchemaObject(node.dependencies) ? node.dependencies : {} + }; + for (const [k, d] of Object.entries(dependencies)) { + if (!Object.hasOwn(v, k)) + continue; + if (!Array.isArray(d)) + this.walk(d, b, v, path2, out); + else + for (const r of d) + if (!Object.hasOwn(v, r)) + push("dependentRequired", `required when ${k} is present`, pointer(path2, r)); + } + if (this.c.draft !== "7" && isSchemaObject(node.dependentSchemas)) { + for (const [k, s] of Object.entries(node.dependentSchemas)) + if (Object.hasOwn(v, k)) + this.walk(s, b, v, path2, out); + } + } + walkArray(node, b, v, path2, out, push) { + let prefix = []; + let rest; + if (Array.isArray(node.items)) { + prefix = node.items; + rest = node.additionalItems; + } else { + if (this.c.draft !== "7" && Array.isArray(node.prefixItems)) + prefix = node.prefixItems; + rest = node.items; + } + for (let i = 0; i < v.length; i++) { + const s = i < prefix.length ? prefix[i] : rest; + if (s !== void 0) + this.walk(s, b, v[i], `${path2}/${i}`, out); + } + if (typeof node.minItems === "number" && v.length < node.minItems) { + push("minItems", `expected at least ${node.minItems} items`); + } + if (typeof node.maxItems === "number" && v.length > node.maxItems) { + push("maxItems", `expected at most ${node.maxItems} items`); + } + if (node.uniqueItems === true && !uniqueCheck(v)) + push("uniqueItems", "expected unique items"); + if (node.contains !== void 0) { + const draft7 = this.c.draft === "7"; + const min = draft7 || typeof node.minContains !== "number" ? 1 : node.minContains; + const max = draft7 || typeof node.maxContains !== "number" ? Number.POSITIVE_INFINITY : node.maxContains; + if (!containsCheck(this.c.check(node.contains, b), min, max)(v)) { + push("contains", containsMessage(min, max)); + } + } + } + walkUnion(branches, b, v, path2, out, exactlyOne) { + const matches = branches.filter((s) => this.ok(s, b, v)).length; + if (matches === 1 || !exactlyOne && matches > 0) + return; + const keyword = exactlyOne ? "oneOf" : "anyOf"; + if (matches > 1) { + out.push({ path: path2, keyword, message: "matches more than one alternative" }); + return; + } + const tag = this.tagOf(branches, b); + if (tag !== void 0 && isObject(v)) { + const i = tag.values.indexOf(v[tag.key]); + if (i < 0) + out.push({ + path: pointer(path2, tag.key), + keyword, + message: `expected one of ${listValues(tag.values)}` + }); + else + this.walk(branches[i], b, v, path2, out); + return; + } + let best; + for (const s of branches) { + const errors = []; + this.walk(s, b, v, path2, errors); + if (errors.length === 0) + continue; + const declared = isObject(v) ? this.declaredKeys(s, b, v) : 0; + const better = best === void 0 || declared > best.declared || declared === best.declared && errors[0].path.length > best.errors[0].path.length; + if (better) + best = { errors, declared }; + } + out.push(...best?.errors ?? [{ path: path2, keyword, message: "matches no alternative" }]); + } + /** How many of the keys of `v` the branch `s` (through a `$ref`) declares in `properties`. */ + declaredKeys(s, b, v) { + const node = isSchemaObject(s) && typeof s.$ref === "string" ? this.c.r.resolve(s.$ref, b).node : s; + const properties = isSchemaObject(node) && isSchemaObject(node.properties) ? node.properties : {}; + return Object.keys(v).filter((k) => Object.hasOwn(properties, k)).length; + } + tagOf(branches, b) { + const resolved = branches.map((s) => isSchemaObject(s) && typeof s.$ref === "string" ? this.c.r.resolve(s.$ref, b).node : s); + const first = resolved[0]; + if (!isSchemaObject(first) || !isSchemaObject(first.properties)) + return void 0; + for (const key of Object.keys(first.properties)) { + const values = resolved.map((s) => { + const tag = isSchemaObject(s) && isSchemaObject(s.properties) ? s.properties[key] : void 0; + return isSchemaObject(tag) ? tag.const : void 0; + }); + const primitive = values.every((x) => x !== void 0 && (typeof x !== "object" || x === null)); + if (primitive && new Set(values).size === values.length) + return { key, values }; + } + return void 0; + } + walkUnevaluated(node, b, v, path2, out) { + const { unevaluatedProperties: unevaluatedProperties2, unevaluatedItems: unevaluatedItems2, ...rest } = node; + const a = newAnnotations(); + this.c.annotating(rest, b)(v, a); + if (isObject(v) && unevaluatedProperties2 !== void 0 && !a.allProps) { + for (const k of Object.keys(v)) { + if (a.props?.has(k)) + continue; + if (unevaluatedProperties2 === false) { + out.push({ path: pointer(path2, k), keyword: "unevaluatedProperties", message: "unknown field" }); + } else + this.walk(unevaluatedProperties2, b, v[k], pointer(path2, k), out); + } + } + if (Array.isArray(v) && unevaluatedItems2 !== void 0 && !a.allItems) { + for (let i = a.items; i < v.length; i++) { + if (a.indexes?.has(i)) + continue; + if (unevaluatedItems2 === false) { + out.push({ path: `${path2}/${i}`, keyword: "unevaluatedItems", message: "unexpected item" }); + } else + this.walk(unevaluatedItems2, b, v[i], `${path2}/${i}`, out); + } + } + } +}; +var LISTED_VALUES = 20; +function listValues(values) { + if (values.length <= LISTED_VALUES) + return JSON.stringify(values); + const listed = JSON.stringify(values.slice(0, LISTED_VALUES)); + return `${listed.slice(0, -1)}, ... and ${values.length - LISTED_VALUES} more]`; +} +function containsMessage(min, max) { + if (max === Number.POSITIVE_INFINITY) + return `expected at least ${min} matching ${min === 1 ? "item" : "items"}`; + if (min === 0) + return `expected at most ${max} matching ${max === 1 ? "item" : "items"}`; + return `expected between ${min} and ${max} matching items`; +} +function pointer(path2, key) { + return `${path2}/${String(key).replace(/~/g, "~0").replace(/\//g, "~1")}`; +} +function typeName(v) { + if (v === null) + return "null"; + if (Array.isArray(v)) + return "array"; + if (v instanceof Uint8Array) + return "bytes"; + if (typeof v === "number" && Number.isInteger(v)) + return "integer"; + return typeof v; +} + +// node_modules/@earendil-works/pi-schema/dist/validator.js +function compile(schema, options = NO_OPTIONS) { + const check = compileCheck(schema, options); + return options.coerce === true ? new CoercingValidator(schema, options, check) : new Validator(schema, options, check); +} +var NO_OPTIONS = Object.freeze({}); +var Validator = class { + /** Whether `value` is valid. A plain function: it may be passed around without its validator. */ + check; + schema; + options; + explainer = void 0; + constructor(schema, options, check) { + this.check = check; + this.schema = schema; + this.options = options; + } + /** + * Why `value` is invalid: one error per failing keyword, `[]` when it is valid. Built on the first failure and + * kept, so a validator whose values always pass never pays for it. + */ + errors(value2) { + if (this.check(value2)) + return []; + this.explainer ??= new Explainer(compiler(this.schema, this.options), this.schema); + return this.explainer.errors(value2); + } +}; +var CoercingValidator = class extends Validator { + coercer = void 0; + /** + * `value` coerced towards the schema: numeric strings to numbers, `"true"` to `true`, a single value to a one-item + * array, an optional `null` dropped, the first union branch the value converts to (the README lists every rule). + * Only the changed paths are copied; `value` comes back itself when nothing changes, and is never modified. Values + * that cannot be coerced stay as they are. The rules are built on the first call. + */ + coerce(value2) { + this.coercer ??= new Coercer(compiler(this.schema, this.options), this.schema); + return this.coercer.coerce(value2); + } + /** Coerces `value`, then checks it: the coerced value when it is valid, else its errors. */ + parse(value2) { + const coerced = this.coerce(value2); + return this.check(coerced) ? { ok: true, value: coerced } : { ok: false, errors: this.errors(coerced) }; + } +}; +function compileCheck(schema, options) { + return compiler(schema, options).root(); +} +function compiler(schema, options) { + const plain = new Compiler(schema, options, false); + try { + plain.root(); + return plain; + } catch (error) { + if (error !== RESTART) + throw error; + return new Compiler(schema, options, true); + } +} + +// node_modules/@earendil-works/chord/dist/json.js +var DATA_DESCRIPTOR = { + value: void 0, + writable: true, + enumerable: true, + configurable: true +}; +function copyJson(value2, options) { + return copy(value2, void 0, options?.omitUndefinedProperties === true); +} +function copy(value2, ancestors, omitUndefinedProperties) { + if (value2 === null || typeof value2 === "string" || typeof value2 === "boolean") + return value2; + if (typeof value2 === "number") { + if (Number.isFinite(value2)) + return value2; + throw new TypeError("Value contains a non-finite number and is not strict JSON"); + } + if (typeof value2 !== "object") + throw new TypeError(`Value contains a non-JSON ${typeof value2}; expected strict JSON`); + const active = ancestors ?? /* @__PURE__ */ new Set(); + if (active.has(value2)) + throw new TypeError("Value contains cycles and is not strict JSON"); + active.add(value2); + try { + if (Array.isArray(value2)) { + if (Object.getPrototypeOf(value2) !== Array.prototype || Reflect.ownKeys(value2).length !== value2.length + 1) { + throw new TypeError("Value must contain strict JSON dense plain arrays"); + } + const result2 = new Array(value2.length); + for (let index = 0; index < value2.length; index++) { + const descriptor = Object.getOwnPropertyDescriptor(value2, index); + if (descriptor === void 0 || !descriptor.enumerable || !("value" in descriptor)) { + throw new TypeError("Value must contain strict JSON enumerable indexed data properties"); + } + defineData(result2, String(index), copy(descriptor.value, active, omitUndefinedProperties)); + } + return result2; + } + const prototype = Object.getPrototypeOf(value2); + if (prototype !== Object.prototype && prototype !== null) { + throw new TypeError("Value must contain strict JSON plain objects or arrays"); + } + const result = Object.create(prototype); + for (const key of Reflect.ownKeys(value2)) { + if (typeof key === "symbol") + throw new TypeError("Value contains a symbol key and is not strict JSON"); + const descriptor = Object.getOwnPropertyDescriptor(value2, key); + if (!descriptor.enumerable || !("value" in descriptor)) { + throw new TypeError("Value must contain strict JSON enumerable data properties"); + } + if (descriptor.value === void 0 && omitUndefinedProperties) + continue; + defineData(result, key, copy(descriptor.value, active, omitUndefinedProperties)); + } + return result; + } finally { + active.delete(value2); + } +} +function defineData(target, key, value2) { + DATA_DESCRIPTOR.value = value2; + Object.defineProperty(target, key, DATA_DESCRIPTOR); + DATA_DESCRIPTOR.value = void 0; +} + +// node_modules/@earendil-works/chord/dist/delta/apply-immutable-trusted.js +var DATA_DESCRIPTOR2 = { + value: void 0, + writable: true, + enumerable: true, + configurable: true +}; +function applyImmutableTrusted(target, operations) { + let root2 = target; + const owned2 = /* @__PURE__ */ new WeakSet(); + for (const operation of operations) { + if (operation[0] === "r") { + root2 = operation[1]; + continue; + } + if (operation[0] === "p" || operation[0] === "m") { + const copied2 = copyPath(root2, operation[1], operation[1].length, owned2); + root2 = copied2.root; + if (!Array.isArray(copied2.target)) + throw new TypeError("Trusted array operation target is not an array"); + if (operation[0] === "p") + spliceTrusted(copied2.target, operation[2], operation[3], operation[4]); + else + permuteTrusted(copied2.target, operation[2]); + continue; + } + const path2 = operation[1]; + const copied = copyPath(root2, path2, path2.length - 1, owned2); + root2 = copied.root; + const key = path2[path2.length - 1]; + switch (operation[0]) { + case "s": + defineData2(copied.target, key, operation[2]); + break; + case "d": + if (Array.isArray(copied.target)) { + if (typeof key !== "number") + throw new TypeError("Trusted array deletion key is not numeric"); + spliceTrusted(copied.target, key, 1, []); + } else + Reflect.deleteProperty(copied.target, key); + break; + case "a": { + const current = read(copied.target, key); + if (typeof current !== "string") + throw new TypeError("Trusted append target is not a string"); + defineData2(copied.target, key, `${current}${operation[2]}`); + break; + } + case "t": { + const current = read(copied.target, key); + if (typeof current !== "string") + throw new TypeError("Trusted truncate target is not a string"); + defineData2(copied.target, key, current.slice(operation[2])); + break; + } + } + } + return root2; +} +function copyPath(root2, path2, length, owned2) { + if (!isContainer(root2)) + throw new TypeError("Trusted operation root is not a container"); + let copiedRoot = root2; + if (!owned2.has(root2)) { + copiedRoot = shallowCopy(root2); + owned2.add(copiedRoot); + } + let destination = copiedRoot; + for (let index = 0; index < length; index++) { + const segment = path2[index]; + const child = read(destination, segment); + if (!isContainer(child)) + throw new TypeError("Trusted operation path is not a container"); + if (owned2.has(child)) { + destination = child; + continue; + } + const copy2 = shallowCopy(child); + defineData2(destination, segment, copy2); + owned2.add(copy2); + destination = copy2; + } + return { root: copiedRoot, target: destination }; +} +function shallowCopy(value2) { + if (Array.isArray(value2)) + return Array.from(value2); + return Object.getPrototypeOf(value2) === null ? Object.assign(/* @__PURE__ */ Object.create(null), value2) : { ...value2 }; +} +function read(target, key) { + return target[key]; +} +function defineData2(target, key, value2) { + DATA_DESCRIPTOR2.value = value2; + Object.defineProperty(target, key, DATA_DESCRIPTOR2); + DATA_DESCRIPTOR2.value = void 0; +} +function spliceTrusted(target, start, remove, items) { + const oldLength = target.length; + const delta = items.length - remove; + if (delta > 0) { + target.length = oldLength + delta; + target.copyWithin(start + items.length, start + remove, oldLength); + } else if (delta < 0) { + target.copyWithin(start + items.length, start + remove, oldLength); + target.length = oldLength + delta; + } + for (let index = 0; index < items.length; index++) + defineData2(target, start + index, items[index]); +} +function permuteTrusted(target, permutation) { + if (target.length !== permutation.length) + throw new TypeError("Trusted permutation length mismatch"); + const previous = target.slice(); + for (let index = 0; index < permutation.length; index++) + target[index] = previous[permutation[index]]; +} +function isContainer(value2) { + return value2 !== null && typeof value2 === "object"; +} + +// node_modules/@earendil-works/chord/dist/delta/tracker.js +var NODE = /* @__PURE__ */ Symbol("chord.delta.overlay.node"); +var PREPARED = /* @__PURE__ */ new WeakMap(); +var RELEASED = {}; +var ARRAY_MUTATORS = /* @__PURE__ */ new Set([ + "push", + "pop", + "shift", + "unshift", + "splice", + "reverse", + "sort", + "fill", + "copyWithin" +]); +var MAX_DELTA_OPERATIONS = 4096; +var MAX_SIMPLE_OBJECT_NODES = 128; +var DATA_DESCRIPTOR3 = { + value: void 0, + writable: true, + enumerable: true, + configurable: true +}; +var PreparedImpl = class { + #context; + value; + base; + ops; + constructor(context, value2, operations) { + this.#context = context; + this.value = value2; + this.base = context.baseValue; + this.ops = operations; + PREPARED.set(this, context); + } + get baseRevision() { + return this.#context.baseRevision; + } + abort() { + abortContext(this.#context); + } +}; +var ChangeImpl = class { + #context; + #preparedStatus; + #settled = false; + constructor(context) { + this.#context = context; + } + get state() { + const state = this.#context?.root?.proxy; + if (state === void 0) + throw new TypeError("Cannot use a settled overlay"); + return state; + } + prepare() { + if (this.#settled) + throw new Error("Change has already been settled"); + const context = this.#context; + assertWritable(context); + context.status.value = "prepared"; + try { + if (!context.replacement) + context.ops = context.dirty.length === 0 ? [] : emitOperations(context); + const prepared = materializePrepared(context); + this.#preparedStatus = context.status; + this.#settled = true; + return prepared; + } catch (error) { + context.status.value = "aborted"; + clearContext(context); + this.#settled = true; + throw error; + } finally { + this.#context = void 0; + } + } + abort() { + if (this.#settled) { + if (this.#preparedStatus?.value === "prepared") + this.#preparedStatus.value = "aborted"; + this.#preparedStatus = void 0; + return; + } + this.#settled = true; + abortContext(this.#context); + this.#context = void 0; + } +}; +var TrackerImpl = class { + #owner = {}; + #open; + #competitors; + #selfRef; + #value; + #revision = 0; + #pruneBudget = 256; + constructor(initial) { + this.#value = initial; + this.#selfRef = new WeakRef(this); + } + get value() { + return this.#value; + } + get revision() { + return this.#revision; + } + beginChange() { + const context = createContext(this.#selfRef, this.#owner, this.#revision, this.#value, false, this.#value); + if (this.#open !== void 0) + this.#demote(this.#open); + this.#open = context; + return new ChangeImpl(context); + } + prepareReplace(value2) { + const context = createContext(this.#selfRef, this.#owner, this.#revision, value2, true, this.#value); + context.status.value = "prepared"; + try { + return materializePrepared(context); + } catch (error) { + context.status.value = "aborted"; + clearContext(context); + throw error; + } + } + adopt(prepared) { + const context = PREPARED.get(prepared); + if (context?.owner !== this.#owner) + throw new Error("Prepared change belongs to a different tracker"); + if (context.status.value === "consumed") + throw new Error("Prepared change has already been used"); + if (context.status.value === "aborted") + throw new Error("Prepared change has been aborted"); + if (context.status.value === "stale") + throw new Error("Prepared change is stale"); + if (context.status.value !== "prepared") + throw new Error("Prepared change is not ready"); + if (context.baseRevision !== this.#revision) { + context.status.value = "stale"; + clearContext(context); + throw new Error("Prepared change is stale"); + } + if (this.#value !== prepared.base) { + context.status.value = "stale"; + throw new Error("Prepared change is stale"); + } + this.#value = prepared.value; + context.status.value = "consumed"; + this.#revision += 1; + this.#invalidate(context); + } + releaseContext(context) { + if (this.#open === context) + this.#open = void 0; + else if (context.registryRef !== void 0) + this.#competitors?.delete(context.registryRef); + context.registryRef = void 0; + } + #demote(context) { + const reference = new WeakRef(context); + context.registryRef = reference; + this.#competitors ??= /* @__PURE__ */ new Set(); + this.#competitors.add(reference); + this.#pruneBudget -= 1; + if (this.#pruneBudget === 0) { + for (const entry2 of this.#competitors) { + if (entry2.deref() === void 0) + this.#competitors.delete(entry2); + } + this.#pruneBudget = Math.max(256, this.#competitors.size); + } + } + #invalidate(winner) { + const open = this.#open; + const competitors = this.#competitors; + this.#open = void 0; + this.#competitors = void 0; + this.#pruneBudget = 256; + if (open !== void 0) + staleCompetitor(open, winner); + if (competitors === void 0) + return; + for (const reference of competitors) { + const context = reference.deref(); + if (context !== void 0) + staleCompetitor(context, winner); + } + } + replacementBase(context) { + return context.baseRevision === this.#revision ? this.#value : void 0; + } +}; +function staleCompetitor(context, winner) { + if (context === winner) + return; + if (context.status.value === "open" || context.status.value === "prepared") + context.status.value = "stale"; + context.registryRef = void 0; + releaseOverlayReferences(context); +} +function track(initial) { + return new TrackerImpl(initial); +} +function appendText(draft, key, text) { + const node = draft[NODE]; + if (node === void 0) + throw new TypeError("appendText() needs a draft"); + assertWritable(node.context); + const name = String(key); + const current = Array.isArray(nodeBase(node)) || !objectHas(node, name) ? void 0 : objectValue(node, name); + if (typeof current !== "string") + throw new TypeError(`appendText() needs a string at ${JSON.stringify(name)}`); + if (text.length === 0) + return; + const owned2 = ownedSlice2(text, 0); + const appended = node.appends?.get(name) ?? (hasObjectWrite(node, name) ? void 0 : ""); + setObjectWrite(node, name, current + owned2); + markDirty(node); + if (appended !== void 0) { + node.appends ??= /* @__PURE__ */ new Map(); + node.appends.set(name, appended + owned2); + } +} +function materializePrepared(context) { + const operations = ensureOperations(context); + const base = context.baseValue; + const value2 = operations.length === 0 ? base : context.simpleObjectMaterialization ? cloneNode(context.root) : materializeOperations(base, operations); + const prepared = new PreparedImpl(context, value2, operations); + if (context.nodes.length > 4096) + releaseOverlayReferences(context); + else + clearContext(context); + return prepared; +} +function materializeOperations(base, operations) { + for (const operation of operations) { + if (operation[0] === "p" || operation[0] === "m") + return applyImmutable(base, operations); + } + return applyImmutableTrusted(base, operations); +} +function createContext(tracker, owner, baseRevision, root2, replacement, base) { + const context = { + owner, + tracker, + baseRevision, + status: { value: "open" }, + root: void 0, + bases: [], + stored: void 0, + dirty: [], + nodes: [], + rawNodes: void 0, + ops: void 0, + replacement, + replacementNoop: false, + baseValue: base, + overlayReleased: false, + simpleObjectMaterialization: false, + registryRef: void 0 + }; + context.root = createNode(context, root2, void 0); + return context; +} +var sharedObjectHandler = { + deleteProperty(target, property) { + return deleteProperty(nodeForTarget(target), property); + }, + defineProperty(target) { + assertWritable(nodeForTarget(target).context); + throw new TypeError("Defining overlay properties is not supported"); + }, + get(target, property) { + if (property === "then") { + const node2 = target[NODE]; + if (node2 === void 0 || isSettledContext(node2.context)) + return void 0; + } + const node = nodeForTarget(target); + if (property === NODE) + return node; + return getProperty(node, property); + }, + getOwnPropertyDescriptor(target, property) { + return getDescriptor(nodeForTarget(target), property); + }, + getPrototypeOf(target) { + const node = nodeForTarget(target); + assertReadable(node.context); + return Object.getPrototypeOf(nodeBase(node)); + }, + has(target, property) { + return hasProperty(nodeForTarget(target), property); + }, + isExtensible(target) { + assertReadable(nodeForTarget(target).context); + return true; + }, + ownKeys(target) { + return ownKeys(nodeForTarget(target)); + }, + preventExtensions(target) { + assertWritable(nodeForTarget(target).context); + throw new TypeError("Overlays cannot be made non-extensible"); + }, + set(target, property, value2) { + return setProperty(nodeForTarget(target), property, value2); + }, + setPrototypeOf(target) { + assertWritable(nodeForTarget(target).context); + throw new TypeError("Changing an overlay prototype is not supported"); + } +}; +var sharedArrayHandler = sharedObjectHandler; +function createNode(context, base, parent, parentKind = 0, parentKey = "", parentSource = void 0, parentPlacement = false) { + const existing = context.rawNodes?.get(base); + if (existing !== void 0) + return existing; + const target = Array.isArray(base) ? [] : {}; + const baseIndex = context.bases.length; + const parentIndex = parent?.baseIndex ?? -1; + context.bases.push(base); + const node = { + context, + baseIndex, + parentIndex, + parentKind, + parentKey, + parentPlacement, + target, + proxy: target + }; + if (parentSource !== void 0) + node.parentSource = parentSource; + target[NODE] = node; + node.proxy = new Proxy(target, Array.isArray(base) ? sharedArrayHandler : sharedObjectHandler); + if (parent !== void 0) { + context.rawNodes ??= /* @__PURE__ */ new WeakMap(); + context.rawNodes.set(base, node); + } + context.nodes.push(node); + return node; +} +function nodeBase(node) { + return node.context.bases[node.baseIndex]; +} +function nodeParent(node) { + return node.parentIndex < 0 ? void 0 : node.context.nodes[node.parentIndex]; +} +function storeValue(context, value2) { + if (!isContainer2(value2)) + return value2; + context.stored ??= []; + const index = context.stored.length; + context.stored.push(value2); + return { index }; +} +function storedValue(context, reference) { + return typeof reference === "object" && reference !== null ? context.stored[reference.index] : reference; +} +function replaceStoredValue(context, reference, value2) { + if (isContainer2(value2)) { + if (typeof reference === "object" && reference !== null) { + context.stored[reference.index] = value2; + return reference; + } + return storeValue(context, value2); + } + if (typeof reference === "object" && reference !== null) + context.stored[reference.index] = RELEASED; + return value2; +} +function releaseStoredValue(context, reference) { + if (typeof reference === "object" && reference !== null) + context.stored[reference.index] = RELEASED; +} +function nodeForTarget(target) { + const node = target[NODE]; + if (node === void 0) + throw new TypeError("Cannot use a settled overlay"); + return node; +} +function isSettledContext(context) { + return context.overlayReleased || context.status.value === "consumed" || context.status.value === "aborted" || context.status.value === "stale"; +} +function assertReadable(context) { + if (isSettledContext(context)) + throw new TypeError("Cannot use a settled overlay"); +} +function assertWritable(context) { + assertReadable(context); + if (context.status.value !== "open") + throw new TypeError("Prepared overlays are read-only"); +} +function getProperty(node, property) { + assertReadable(node.context); + if (Array.isArray(nodeBase(node))) { + const overlay = arrayOverlay(node); + if (property === "length") + return arrayLength(overlay); + if (ARRAY_MUTATORS.has(property)) + return arrayMutators[property]; + const index = arrayIndex(property); + if (index !== void 0) + return getArrayIndex(node, index); + return Reflect.get(Array.prototype, property, node.proxy); + } + if (typeof property === "symbol") + return Reflect.get(nodeBase(node), property, node.proxy); + const key = String(property); + if (!objectHas(node, key)) { + if (isObjectDeleted(node, key) || Object.hasOwn(nodeBase(node), key)) + return void 0; + return Reflect.get(nodeBase(node), property, node.proxy); + } + const value2 = objectValue(node, key); + if (!isContainer2(value2)) + return value2; + return createNode(node.context, value2, node, 0, key, void 0, hasObjectWrite(node, key)).proxy; +} +function getArrayIndex(node, index) { + const overlay = arrayOverlay(node); + if (index >= arrayLength(overlay)) + return void 0; + const piece = locatePiece(overlay, index); + const sourceIndex = piece.start + piece.step * overlay.locatedOffset; + const value2 = entryValueAt(node, piece, sourceIndex); + if (!isContainer2(value2)) + return value2; + return createNode(node.context, value2, node, piece.kind === "base" ? 1 : 2, sourceIndex, piece.kind === "insert" ? piece.source : void 0, piece.kind === "insert" || hasEntryOverrideAt(overlay, piece, sourceIndex)).proxy; +} +function setProperty(node, property, supplied) { + assertWritable(node.context); + if (typeof property === "symbol") + throw new TypeError("Symbol writes are not supported"); + if (Array.isArray(nodeBase(node))) { + if (property === "length") { + setArrayLength(node, toArrayLength(supplied)); + return true; + } + const index = arrayIndex(property); + if (index === void 0) + throw new TypeError("Only array indices and length can be written"); + if (index > arrayLength(arrayOverlay(node))) + throw new TypeError("Overlay arrays cannot contain holes"); + setArrayIndex(node, index, clonePlacement(supplied)); + return true; + } + const key = String(property); + if (supplied === void 0) + return deleteProperty(node, key); + const stored = clonePlacement(supplied); + const current = objectValue(node, key); + const wasDeleted = isObjectDeleted(node, key); + if (!wasDeleted && !isContainer2(stored) && current === stored) + return true; + node.appends?.delete(key); + setObjectWrite(node, key, stored); + if (wasDeleted && Object.hasOwn(nodeBase(node), key)) { + if (node.readded === void 0) + node.readded = /* @__PURE__ */ new Set(); + node.readded.add(key); + } + deleteObjectDeletion(node, key); + markDirty(node); + return true; +} +function deleteProperty(node, property) { + assertWritable(node.context); + if (typeof property === "symbol") + throw new TypeError("Symbol writes are not supported"); + if (Array.isArray(nodeBase(node))) + throw new TypeError("Overlay arrays cannot contain holes"); + const key = String(property); + if (!objectHas(node, key)) + return true; + node.appends?.delete(key); + deleteObjectWrite(node, key); + node.readded?.delete(key); + setObjectDeletion(node, key); + markDirty(node); + return true; +} +function hasProperty(node, property) { + assertReadable(node.context); + if (Array.isArray(nodeBase(node))) { + if (property === "length") + return true; + const index = arrayIndex(property); + if (index !== void 0) + return index < arrayLength(arrayOverlay(node)); + return property in Array.prototype; + } + return typeof property === "symbol" ? property in nodeBase(node) : objectHas(node, String(property)); +} +function ownKeys(node) { + assertReadable(node.context); + if (Array.isArray(nodeBase(node))) { + const length = arrayLength(arrayOverlay(node)); + const keys2 = new Array(length + 1); + for (let index = 0; index < length; index++) + keys2[index] = String(index); + keys2[length] = "length"; + return keys2; + } + let existingKeysOnly = node.deleteKey === void 0 && node.deletes === void 0 && node.readded === void 0 && (node.writeKey === void 0 || Object.hasOwn(nodeBase(node), node.writeKey)); + if (existingKeysOnly && node.writes !== void 0) { + for (const key of node.writes.keys()) { + if (!Object.hasOwn(nodeBase(node), key)) { + existingKeysOnly = false; + break; + } + } + } + if (existingKeysOnly) + return Object.keys(nodeBase(node)); + const keys = Object.keys(nodeBase(node)).filter((key) => !isObjectDeleted(node, key) && !node.readded?.has(key)); + const seen = new Set(keys); + if (node.writeKey !== void 0 && !seen.has(node.writeKey)) { + keys.push(node.writeKey); + seen.add(node.writeKey); + } + if (node.writes !== void 0) { + for (const key of node.writes.keys()) { + if (seen.has(key)) + continue; + keys.push(key); + seen.add(key); + } + } + const indices = []; + const strings = []; + for (const key of keys) { + const index = arrayIndex(key); + if (index === void 0) + strings.push(key); + else + indices.push(index); + } + indices.sort((left, right) => left - right); + return [...indices.map(String), ...strings]; +} +function getDescriptor(node, property) { + assertReadable(node.context); + if (Array.isArray(nodeBase(node))) { + if (property === "length") { + const length = arrayLength(arrayOverlay(node)); + node.target.length = length; + return Reflect.getOwnPropertyDescriptor(node.target, "length"); + } + const index = arrayIndex(property); + if (index === void 0 || index >= arrayLength(arrayOverlay(node))) + return void 0; + } else { + if (typeof property === "symbol" || !objectHas(node, String(property))) + return void 0; + } + return { + configurable: true, + enumerable: true, + writable: node.context.status.value === "open", + value: getProperty(node, property) + }; +} +function hasObjectWrite(node, key) { + return node.writeKey === key || (node.writes?.has(key) ?? false); +} +function setObjectWrite(node, key, value2) { + if (node.writes !== void 0) { + const reference = node.writes.get(key); + node.writes.set(key, reference === void 0 ? storeValue(node.context, value2) : replaceStoredValue(node.context, reference, value2)); + return; + } + if (node.writeKey === void 0 || node.writeKey === key) { + node.writeKey = key; + node.writeValue = node.writeValue === void 0 ? storeValue(node.context, value2) : replaceStoredValue(node.context, node.writeValue, value2); + return; + } + node.writes = /* @__PURE__ */ new Map(); + node.writes.set(node.writeKey, node.writeValue); + node.writes.set(key, storeValue(node.context, value2)); + node.writeKey = void 0; + node.writeValue = void 0; +} +function deleteObjectWrite(node, key) { + if (node.writeKey === key) { + node.writeKey = void 0; + releaseStoredValue(node.context, node.writeValue); + node.writeValue = void 0; + } else { + const reference = node.writes?.get(key); + if (reference !== void 0) + releaseStoredValue(node.context, reference); + node.writes?.delete(key); + } +} +function isObjectDeleted(node, key) { + return node.deleteKey === key || (node.deletes?.has(key) ?? false); +} +function setObjectDeletion(node, key) { + if (node.deletes !== void 0) { + node.deletes.add(key); + return; + } + if (node.deleteKey === void 0 || node.deleteKey === key) { + node.deleteKey = key; + return; + } + node.deletes = /* @__PURE__ */ new Set(); + node.deletes.add(node.deleteKey); + node.deletes.add(key); + node.deleteKey = void 0; +} +function deleteObjectDeletion(node, key) { + if (node.deleteKey === key) + node.deleteKey = void 0; + else + node.deletes?.delete(key); +} +function objectHas(node, key) { + if (isObjectDeleted(node, key)) + return false; + return hasObjectWrite(node, key) || Object.hasOwn(nodeBase(node), key); +} +function objectValue(node, key) { + if (node.writeKey === key) + return storedValue(node.context, node.writeValue); + const reference = node.writes?.get(key); + if (reference !== void 0) + return storedValue(node.context, reference); + return nodeBase(node)[key]; +} +function markDirty(node) { + if (node.dirty) + return; + node.dirty = true; + node.context.dirty.push(node); + for (let parent = nodeParent(node); parent !== void 0; parent = nodeParent(parent)) + parent.subtreeDirty = true; +} +function arrayOverlay(node) { + if (node.array !== void 0) + return node.array; + const base = nodeBase(node); + const overlay = { + root: void 0, + pieces: void 0, + baseOverrides: void 0, + insertOverrides: void 0, + structural: false, + generation: 0, + plan: void 0, + seed: 2654435769, + locatedOffset: 0, + baseLocations: void 0, + insertLocations: void 0 + }; + if (base.length > 0) + overlay.root = createPieceNode(overlay, { kind: "base", start: 0, length: base.length, step: 1 }); + node.array = overlay; + return overlay; +} +function baseOverridesForWrite(overlay) { + if (overlay.baseOverrides === void 0) + overlay.baseOverrides = /* @__PURE__ */ new Map(); + return overlay.baseOverrides; +} +function insertOverridesForWrite(overlay) { + if (overlay.insertOverrides === void 0) + overlay.insertOverrides = /* @__PURE__ */ new Map(); + return overlay.insertOverrides; +} +function nextPiecePriority(overlay) { + let value2 = overlay.seed; + value2 ^= value2 << 13; + value2 ^= value2 >>> 17; + value2 ^= value2 << 5; + overlay.seed = value2 >>> 0; + return overlay.seed; +} +function treeElements(node) { + return node?.elements ?? 0; +} +function updatePieceNode(node) { + node.elements = treeElements(node.left) + node.piece.length + treeElements(node.right); +} +function createPieceNode(overlay, piece) { + return { piece, left: void 0, right: void 0, priority: nextPiecePriority(overlay), elements: piece.length }; +} +function mergePieceTrees(left, right) { + if (left === void 0) + return right; + if (right === void 0) + return left; + if (left.priority >= right.priority) { + left.right = mergePieceTrees(left.right, right); + updatePieceNode(left); + return left; + } + right.left = mergePieceTrees(left, right.left); + updatePieceNode(right); + return right; +} +function splitPieceTree(overlay, root2, index) { + if (root2 === void 0) + return [void 0, void 0]; + const leftLength = treeElements(root2.left); + if (index < leftLength) { + const [left, right] = splitPieceTree(overlay, root2.left, index); + root2.left = right; + updatePieceNode(root2); + return [left, root2]; + } + const pieceEnd = leftLength + root2.piece.length; + if (index > pieceEnd) { + const [left, right] = splitPieceTree(overlay, root2.right, index - pieceEnd); + root2.right = left; + updatePieceNode(root2); + return [root2, right]; + } + if (index === leftLength) { + const left = root2.left; + root2.left = void 0; + updatePieceNode(root2); + return [left, root2]; + } + if (index === pieceEnd) { + const right = root2.right; + root2.right = void 0; + updatePieceNode(root2); + return [root2, right]; + } + const offset = index - leftLength; + const first = { ...root2.piece, length: offset }; + const second = { + ...root2.piece, + start: root2.piece.start + root2.piece.step * offset, + length: root2.piece.length - offset + }; + return [ + mergePieceTrees(root2.left, createPieceNode(overlay, first)), + mergePieceTrees(createPieceNode(overlay, second), root2.right) + ]; +} +function leftmostPieceNode(node) { + while (node.left !== void 0) + node = node.left; + return node; +} +function rightmostPieceNode(node) { + while (node.right !== void 0) + node = node.right; + return node; +} +function mergeablePieces(left, right) { + if (left.kind !== right.kind) + return false; + if (left.kind === "insert" && left.source !== right.source) + return false; + if (left.length === 1 && right.length === 1) + return Math.abs(right.start - left.start) === 1; + return left.step === right.step && left.start + left.step * left.length === right.start; +} +function joinNormalized(overlay, left, right) { + if (left === void 0) + return right; + if (right === void 0) + return left; + const leftPiece = rightmostPieceNode(left).piece; + const rightPiece = leftmostPieceNode(right).piece; + if (!mergeablePieces(leftPiece, rightPiece)) + return mergePieceTrees(left, right); + const [leftRest] = splitPieceTree(overlay, left, treeElements(left) - leftPiece.length); + const [, rightRest] = splitPieceTree(overlay, right, rightPiece.length); + const step = leftPiece.length === 1 ? rightPiece.start - leftPiece.start : leftPiece.step; + const combined = leftPiece.kind === "base" ? { kind: "base", start: leftPiece.start, length: leftPiece.length + rightPiece.length, step } : { + kind: "insert", + source: leftPiece.source, + start: leftPiece.start, + length: leftPiece.length + rightPiece.length, + step + }; + return joinNormalized(overlay, joinNormalized(overlay, leftRest, createPieceNode(overlay, combined)), rightRest); +} +function flattenPieceTree(node, output) { + if (node === void 0) + return; + flattenPieceTree(node.left, output); + output.push(node.piece); + flattenPieceTree(node.right, output); +} +function piecesOf(overlay) { + if (overlay.pieces === void 0) { + overlay.pieces = []; + flattenPieceTree(overlay.root, overlay.pieces); + } + return overlay.pieces; +} +function treeFromPieces(overlay, pieces) { + mergePieces(pieces); + let root2; + for (const piece of pieces) + root2 = mergePieceTrees(root2, createPieceNode(overlay, piece)); + return root2; +} +function replaceAllPieces(overlay, pieces) { + overlay.root = treeFromPieces(overlay, pieces); + overlay.pieces = void 0; + overlay.baseLocations = void 0; + overlay.insertLocations = void 0; +} +function arrayLength(overlay) { + return treeElements(overlay.root); +} +function locatePiece(overlay, index) { + let node = overlay.root; + while (node !== void 0) { + const leftLength = treeElements(node.left); + if (index < leftLength) + node = node.left; + else if (index >= leftLength + node.piece.length) { + index -= leftLength + node.piece.length; + node = node.right; + } else { + overlay.locatedOffset = index - leftLength; + return node.piece; + } + } + throw new RangeError("Array overlay index is out of range"); +} +function arrayIndex(property) { + if (typeof property !== "string" || property.length === 0 || property.length > 10) + return void 0; + if (property === "0") + return 0; + const first = property.charCodeAt(0); + if (first < 49 || first > 57) + return void 0; + let index = first - 48; + for (let offset = 1; offset < property.length; offset++) { + const digit = property.charCodeAt(offset) - 48; + if (digit < 0 || digit > 9) + return void 0; + index = index * 10 + digit; + if (index >= 4294967295) + return void 0; + } + return index; +} +function entryValueAt(node, piece, sourceIndex) { + const overlay = arrayOverlay(node); + if (piece.kind === "base") { + const valueIndex = overlay.baseOverrides?.get(sourceIndex); + return valueIndex === void 0 ? nodeBase(node)[sourceIndex] : storedValue(node.context, valueIndex); + } + const overrides = overlay.insertOverrides?.get(piece.source); + const reference = overrides?.has(sourceIndex) ? overrides.get(sourceIndex) : piece.source.refs[sourceIndex]; + return storedValue(node.context, reference); +} +function hasEntryOverrideAt(overlay, piece, sourceIndex) { + return piece.kind === "base" ? overlay.baseOverrides?.has(sourceIndex) ?? false : overlay.insertOverrides?.get(piece.source)?.has(sourceIndex) ?? false; +} +function extendRightmostPiece(node, amount) { + if (node.right !== void 0) + extendRightmostPiece(node.right, amount); + else + node.piece.length += amount; + updatePieceNode(node); +} +function invalidatePieceCaches(overlay) { + overlay.pieces = void 0; + overlay.baseLocations = void 0; + overlay.insertLocations = void 0; + overlay.plan = void 0; +} +function replacePieceRange(node, index, remove, inserted) { + if (remove === 0 && inserted.length === 0) + return; + const overlay = arrayOverlay(node); + if (remove === 0 && index === arrayLength(overlay) && inserted.length === 1 && overlay.root !== void 0) { + const addition = inserted[0]; + const tail = rightmostPieceNode(overlay.root).piece; + if (addition.kind === "insert" && tail.kind === "insert" && tail.step === 1 && tail.start + tail.length === tail.source.refs.length) { + for (let offset = 0; offset < addition.length; offset++) { + tail.source.refs.push(addition.source.refs[addition.start + offset]); + } + extendRightmostPiece(overlay.root, addition.length); + invalidatePieceCaches(overlay); + overlay.structural = true; + overlay.generation += 1; + markDirty(node); + return; + } + } + const [left, rest] = splitPieceTree(overlay, overlay.root, index); + const [, right] = splitPieceTree(overlay, rest, remove); + const middle = treeFromPieces(overlay, inserted); + overlay.root = joinNormalized(overlay, joinNormalized(overlay, left, middle), right); + invalidatePieceCaches(overlay); + overlay.structural = true; + overlay.generation += 1; + markDirty(node); +} +function mergePieces(pieces) { + for (let index = 1; index < pieces.length; ) { + const left = pieces[index - 1]; + const right = pieces[index]; + const sameSource = left.kind === right.kind && (left.kind === "base" || left.source === right.source); + if (sameSource && left.length === 1 && right.length === 1 && Math.abs(right.start - left.start) === 1) { + left.step = right.start - left.start; + left.length = 2; + pieces.splice(index, 1); + } else if (sameSource && left.step === right.step && left.start + left.step * left.length === right.start) { + left.length += right.length; + pieces.splice(index, 1); + } else + index += 1; + } +} +function insertPiece(node, items, start = 0) { + if (items.length === start) + return []; + const refs = []; + for (let index = start; index < items.length; index++) + refs.push(storeValue(node.context, items[index])); + return [{ kind: "insert", source: { refs }, start: 0, length: refs.length, step: 1 }]; +} +function insertPlacementPiece(node, values, start = 0) { + if (values.length === start) + return []; + const storedLength = node.context.stored?.length ?? 0; + try { + for (let index = start; index < values.length; index++) + values[index] = storeValue(node.context, clonePlacement(values[index])); + } catch (error) { + if (node.context.stored !== void 0) + node.context.stored.length = storedLength; + throw error; + } + return [ + { + kind: "insert", + source: { refs: values }, + start, + length: values.length - start, + step: 1 + } + ]; +} +function setArrayIndex(node, index, stored) { + const overlay = arrayOverlay(node); + const length = arrayLength(overlay); + if (index === length) { + replacePieceRange(node, length, 0, insertPiece(node, [stored])); + return; + } + const piece = locatePiece(overlay, index); + const sourceIndex = piece.start + piece.step * overlay.locatedOffset; + const current = entryValueAt(node, piece, sourceIndex); + if (!isContainer2(stored) && current === stored) + return; + if (piece.kind === "base") { + const existingIndex = overlay.baseOverrides?.get(sourceIndex); + if (!isContainer2(stored) && stored === nodeBase(node)[sourceIndex]) { + if (existingIndex !== void 0) + releaseStoredValue(node.context, existingIndex); + overlay.baseOverrides?.delete(sourceIndex); + } else if (existingIndex === void 0) { + baseOverridesForWrite(overlay).set(sourceIndex, storeValue(node.context, stored)); + } else + overlay.baseOverrides.set(sourceIndex, replaceStoredValue(node.context, existingIndex, stored)); + } else { + let overrides = overlay.insertOverrides?.get(piece.source); + const existingIndex = overrides?.get(sourceIndex); + if (!isContainer2(stored) && stored === storedValue(node.context, piece.source.refs[sourceIndex])) { + if (existingIndex !== void 0) + releaseStoredValue(node.context, existingIndex); + overrides?.delete(sourceIndex); + if (overrides?.size === 0) + overlay.insertOverrides?.delete(piece.source); + } else { + if (overrides === void 0) { + overrides = /* @__PURE__ */ new Map(); + insertOverridesForWrite(overlay).set(piece.source, overrides); + } + if (existingIndex === void 0) + overrides.set(sourceIndex, storeValue(node.context, stored)); + else + overrides.set(sourceIndex, replaceStoredValue(node.context, existingIndex, stored)); + } + } + markDirty(node); +} +function setArrayLength(node, next) { + const current = arrayLength(arrayOverlay(node)); + if (next === current) + return; + if (next < current) + replacePieceRange(node, next, current - next, []); + else + replacePieceRange(node, current, 0, insertPiece(node, Array.from({ length: next - current }, () => null))); +} +function toArrayLength(value2) { + const number2 = Number(value2); + if (!Number.isInteger(number2) || number2 < 0 || number2 >= 4294967296) + throw new RangeError("Invalid array length"); + return number2; +} +function toIntegerOrInfinity(value2) { + const number2 = Number(value2); + if (Number.isNaN(number2) || number2 === 0) + return 0; + return Number.isFinite(number2) ? Math.trunc(number2) : number2; +} +function clampIndex(value2, length) { + if (value2 === Number.NEGATIVE_INFINITY) + return 0; + if (value2 < 0) + return Math.max(length + value2, 0); + return Math.min(value2, length); +} +function mutatorNode(receiver) { + if (!isContainer2(receiver)) + return void 0; + const node = Reflect.get(receiver, NODE); + if (node === void 0) + return void 0; + if (!Array.isArray(nodeBase(node))) + throw new TypeError("Array mutator called on incompatible receiver"); + assertWritable(node.context); + return node; +} +var arrayMutators = { + push(...items) { + const node = mutatorNode(this); + if (node === void 0) + return Reflect.apply(Array.prototype.push, this, items); + const length = arrayLength(arrayOverlay(node)); + replacePieceRange(node, length, 0, insertPlacementPiece(node, items)); + return length + items.length; + }, + pop() { + const node = mutatorNode(this); + if (node === void 0) + return Reflect.apply(Array.prototype.pop, this, []); + const length = arrayLength(arrayOverlay(node)); + if (length === 0) + return void 0; + const value2 = getArrayIndex(node, length - 1); + replacePieceRange(node, length - 1, 1, []); + return value2; + }, + shift() { + const node = mutatorNode(this); + if (node === void 0) + return Reflect.apply(Array.prototype.shift, this, []); + const length = arrayLength(arrayOverlay(node)); + if (length === 0) + return void 0; + const value2 = getArrayIndex(node, 0); + replacePieceRange(node, 0, 1, []); + return value2; + }, + unshift(...items) { + const node = mutatorNode(this); + if (node === void 0) + return Reflect.apply(Array.prototype.unshift, this, items); + replacePieceRange(node, 0, 0, insertPlacementPiece(node, items)); + return arrayLength(arrayOverlay(node)); + }, + splice(...args) { + const node = mutatorNode(this); + if (node === void 0) + return Reflect.apply(Array.prototype.splice, this, args); + const length = arrayLength(arrayOverlay(node)); + const start = args.length === 0 ? 0 : clampIndex(toIntegerOrInfinity(args[0]), length); + const remove = args.length === 0 ? 0 : args.length === 1 ? length - start : Math.min(Math.max(toIntegerOrInfinity(args[1]), 0), length - start); + const removed = Array.from({ length: remove }, (_, offset) => getArrayIndex(node, start + offset)); + const itemStart = Math.min(2, args.length); + const itemCount = Math.max(args.length - 2, 0); + replacePieceRange(node, start, remove, insertPlacementPiece(node, args, itemStart)); + setArrayLength(node, length - remove + itemCount); + return removed; + }, + reverse() { + const node = mutatorNode(this); + if (node === void 0) + return Reflect.apply(Array.prototype.reverse, this, []); + const overlay = arrayOverlay(node); + if (arrayLength(overlay) < 2) + return node.proxy; + const pieces = [...piecesOf(overlay)].reverse(); + for (const piece of pieces) { + piece.start += piece.step * (piece.length - 1); + piece.step = piece.step === 1 ? -1 : 1; + } + replaceAllPieces(overlay, pieces); + overlay.structural = true; + overlay.generation += 1; + overlay.plan = void 0; + markDirty(node); + return node.proxy; + }, + sort(comparator) { + const node = mutatorNode(this); + if (node === void 0) + return Reflect.apply(Array.prototype.sort, this, [comparator]); + if (comparator !== void 0 && typeof comparator !== "function") + throw new TypeError("Comparator must be a function"); + const overlay = arrayOverlay(node); + const insertedSources = []; + const insertedIndices = []; + const insertedValues = []; + const baseValues = new Array(nodeBase(node).length); + const order = []; + for (const piece of piecesOf(overlay)) { + for (let offset = 0; offset < piece.length; offset++) { + const sourceIndex = piece.start + piece.step * offset; + if (piece.kind === "base") { + order.push(sourceIndex); + baseValues[sourceIndex] = publicSortValue(node, sourceIndex, insertedSources, insertedIndices); + } else { + insertedSources.push(piece.source); + insertedIndices.push(sourceIndex); + const token = -insertedSources.length; + order.push(token); + insertedValues.push(publicSortValue(node, token, insertedSources, insertedIndices)); + } + } + } + const baseSnapshot = /* @__PURE__ */ new Map(); + for (const [index, valueIndex] of overlay.baseOverrides ?? []) + baseSnapshot.set(index, storedValue(node.context, valueIndex)); + const insertSnapshots = /* @__PURE__ */ new Map(); + for (const [source, overrides] of overlay.insertOverrides ?? []) { + const snapshot = /* @__PURE__ */ new Map(); + for (const [index, valueIndex] of overrides) + snapshot.set(index, storedValue(node.context, valueIndex)); + insertSnapshots.set(source, snapshot); + } + const generation = overlay.generation; + order.sort((left, right) => { + const leftValue = left < 0 ? insertedValues[-left - 1] : baseValues[left]; + const rightValue = right < 0 ? insertedValues[-right - 1] : baseValues[right]; + if (typeof comparator === "function") + return Number(comparator(leftValue, rightValue)); + const a = String(leftValue); + const b = String(rightValue); + return a < b ? -1 : a > b ? 1 : 0; + }); + if (baseSnapshot.size > 0 || insertSnapshots.size > 0 || (overlay.baseOverrides?.size ?? 0) > 0 || (overlay.insertOverrides?.size ?? 0) > 0) { + for (const token of order) { + restoreSortOverride(node, token, insertedSources, insertedIndices, baseSnapshot, insertSnapshots); + } + } + const comparatorWasStructural = overlay.generation !== generation; + const currentLength = arrayLength(overlay); + const samePrefix = currentLength >= order.length && order.every((token, index) => sameSortTokenAt(overlay, index, token, insertedSources, insertedIndices)); + if (!samePrefix) { + replacePieceRange(node, 0, Math.min(order.length, currentLength), piecesFromSortOrder(order, insertedSources, insertedIndices)); + } + if (comparatorWasStructural) + deduplicateArrayEntries(node); + return node.proxy; + }, + fill(supplied, startArg, endArg) { + const node = mutatorNode(this); + if (node === void 0) + return Reflect.apply(Array.prototype.fill, this, [supplied, startArg, endArg]); + const length = arrayLength(arrayOverlay(node)); + const start = startArg === void 0 ? 0 : clampIndex(toIntegerOrInfinity(startArg), length); + const end = endArg === void 0 ? length : clampIndex(toIntegerOrInfinity(endArg), length); + if (end <= start) + return node.proxy; + const items = Array.from({ length: end - start }, () => clonePlacement(supplied)); + replacePieceRange(node, start, end - start, insertPiece(node, items)); + return node.proxy; + }, + copyWithin(targetArg, startArg, endArg) { + const node = mutatorNode(this); + if (node === void 0) + return Reflect.apply(Array.prototype.copyWithin, this, [targetArg, startArg, endArg]); + const length = arrayLength(arrayOverlay(node)); + const target = clampIndex(toIntegerOrInfinity(targetArg), length); + const start = clampIndex(toIntegerOrInfinity(startArg), length); + const end = endArg === void 0 ? length : clampIndex(toIntegerOrInfinity(endArg), length); + const count = Math.min(Math.max(end - start, 0), length - target); + const values = Array.from({ length: count }, (_, offset) => clonePlacement(getArrayIndex(node, start + offset))); + replacePieceRange(node, target, count, insertPiece(node, values)); + return node.proxy; + } +}; +function sortTokenSource(token, sources) { + return token < 0 ? sources[-token - 1] : void 0; +} +function sortTokenIndex(token, indices) { + return token < 0 ? indices[-token - 1] : token; +} +function publicSortValue(node, token, sources, indices) { + const overlay = arrayOverlay(node); + const source = sortTokenSource(token, sources); + const sourceIndex = sortTokenIndex(token, indices); + const overrideIndex = source === void 0 ? overlay.baseOverrides?.get(sourceIndex) : overlay.insertOverrides?.get(source)?.get(sourceIndex); + const value2 = overrideIndex !== void 0 ? storedValue(node.context, overrideIndex) : source === void 0 ? nodeBase(node)[sourceIndex] : storedValue(node.context, source.refs[sourceIndex]); + if (!isContainer2(value2)) + return value2; + return createNode(node.context, value2, node, source === void 0 ? 1 : 2, sourceIndex, source, source !== void 0 || (overlay.baseOverrides?.has(sourceIndex) ?? false)).proxy; +} +function restoreSortOverride(node, token, sources, indices, baseSnapshot, insertSnapshots) { + const overlay = arrayOverlay(node); + const source = sortTokenSource(token, sources); + const sourceIndex = sortTokenIndex(token, indices); + if (source === void 0) { + const existingIndex2 = overlay.baseOverrides?.get(sourceIndex); + const snapshotValue2 = baseSnapshot.get(sourceIndex); + if (snapshotValue2 !== void 0) { + if (existingIndex2 === void 0) + baseOverridesForWrite(overlay).set(sourceIndex, storeValue(node.context, snapshotValue2)); + else + overlay.baseOverrides.set(sourceIndex, replaceStoredValue(node.context, existingIndex2, snapshotValue2)); + } else { + if (existingIndex2 !== void 0) + releaseStoredValue(node.context, existingIndex2); + overlay.baseOverrides?.delete(sourceIndex); + } + return; + } + let overrides = overlay.insertOverrides?.get(source); + const existingIndex = overrides?.get(sourceIndex); + const snapshotValue = insertSnapshots.get(source)?.get(sourceIndex); + if (snapshotValue !== void 0) { + if (overrides === void 0) { + overrides = /* @__PURE__ */ new Map(); + insertOverridesForWrite(overlay).set(source, overrides); + } + if (existingIndex === void 0) + overrides.set(sourceIndex, storeValue(node.context, snapshotValue)); + else + overrides.set(sourceIndex, replaceStoredValue(node.context, existingIndex, snapshotValue)); + } else { + if (existingIndex !== void 0) + releaseStoredValue(node.context, existingIndex); + overrides?.delete(sourceIndex); + if (overrides?.size === 0) + overlay.insertOverrides?.delete(source); + } +} +function sameSortTokenAt(overlay, logicalIndex, token, sources, indices) { + const piece = locatePiece(overlay, logicalIndex); + const sourceIndex = piece.start + piece.step * overlay.locatedOffset; + const tokenSource = sortTokenSource(token, sources); + return sourceIndex === sortTokenIndex(token, indices) && (piece.kind === "base" && tokenSource === void 0 || piece.kind === "insert" && piece.source === tokenSource); +} +function deduplicateArrayEntries(node) { + const overlay = arrayOverlay(node); + const seenBase = /* @__PURE__ */ new Set(); + const seenInsert = /* @__PURE__ */ new Map(); + const next = []; + let duplicated = false; + for (const piece of piecesOf(overlay)) { + for (let offset = 0; offset < piece.length; offset++) { + const sourceIndex = piece.start + piece.step * offset; + let seen; + if (piece.kind === "base") { + seen = seenBase.has(sourceIndex); + seenBase.add(sourceIndex); + } else { + let indices = seenInsert.get(piece.source); + if (indices === void 0) { + indices = /* @__PURE__ */ new Set(); + seenInsert.set(piece.source, indices); + } + seen = indices.has(sourceIndex); + indices.add(sourceIndex); + } + if (seen) { + duplicated = true; + appendMergedPiece(next, insertPiece(node, [clonePlacementStored(entryValueAt(node, piece, sourceIndex), node.context)])[0]); + } else + appendMergedPiece(next, singletonPiece(piece, sourceIndex)); + } + } + if (!duplicated) + return; + replaceAllPieces(overlay, next); + overlay.structural = true; + overlay.generation += 1; + overlay.plan = void 0; + markDirty(node); +} +function piecesFromSortOrder(order, sources, indices) { + const pieces = []; + for (const token of order) { + const source = token < 0 ? sources[-token - 1] : void 0; + const sourceIndex = token < 0 ? indices[-token - 1] : token; + const previous = pieces.at(-1); + const sameSource = previous !== void 0 && (source === void 0 && previous.kind === "base" || source !== void 0 && previous.kind === "insert" && previous.source === source); + if (previous !== void 0 && sameSource) { + if (previous.length === 1) { + const step = sourceIndex - previous.start; + if (step === 1 || step === -1) { + previous.step = step; + previous.length = 2; + continue; + } + } else if (previous.start + previous.step * previous.length === sourceIndex) { + previous.length += 1; + continue; + } + } + pieces.push(source === void 0 ? { kind: "base", start: sourceIndex, length: 1, step: 1 } : { kind: "insert", source, start: sourceIndex, length: 1, step: 1 }); + } + return pieces; +} +function appendMergedPiece(pieces, piece) { + const previous = pieces.at(-1); + const sameSource = previous !== void 0 && previous.kind === piece.kind && (previous.kind === "base" || previous.source === piece.source); + if (previous !== void 0 && sameSource && previous.length === 1 && piece.length === 1) { + const step = piece.start - previous.start; + if (step === 1 || step === -1) { + previous.step = step; + previous.length = 2; + return; + } + } + if (previous !== void 0 && sameSource && previous.step === piece.step && previous.start + previous.step * previous.length === piece.start) { + previous.length += piece.length; + } else + pieces.push(piece); +} +function singletonPiece(piece, sourceIndex) { + return piece.kind === "base" ? { kind: "base", start: sourceIndex, length: 1, step: 1 } : { kind: "insert", source: piece.source, start: sourceIndex, length: 1, step: 1 }; +} +function clonePlacement(value2) { + const proxyNode = isContainer2(value2) ? Reflect.get(value2, NODE) : void 0; + return proxyNode === void 0 ? copyJson(value2) : clonePlacementNode(proxyNode); +} +function cloneNode(node) { + assertReadable(node.context); + if (Array.isArray(nodeBase(node))) { + const overlay = arrayOverlay(node); + const result2 = []; + for (const piece of piecesOf(overlay)) { + for (let offset = 0; offset < piece.length; offset++) { + const sourceIndex = piece.start + piece.step * offset; + result2.push(cloneStored(entryValueAt(node, piece, sourceIndex), node.context)); + } + } + return result2; + } + const result = Object.create(Object.getPrototypeOf(nodeBase(node))); + for (const key of ownKeys(node)) + defineData3(result, key, cloneStored(objectValue(node, key), node.context)); + return result; +} +function cloneStored(value2, context) { + if (!isContainer2(value2)) + return value2; + const node = context.rawNodes?.get(value2); + return node === void 0 || !node.dirty && !node.subtreeDirty ? value2 : cloneNode(node); +} +function clonePlacementNode(node) { + assertReadable(node.context); + if (Array.isArray(nodeBase(node))) { + const overlay = arrayOverlay(node); + const result2 = []; + for (const piece of piecesOf(overlay)) { + for (let offset = 0; offset < piece.length; offset++) { + const sourceIndex = piece.start + piece.step * offset; + result2.push(clonePlacementStored(entryValueAt(node, piece, sourceIndex), node.context)); + } + } + return result2; + } + const result = Object.create(Object.getPrototypeOf(nodeBase(node))); + for (const key of ownKeys(node)) + defineData3(result, key, clonePlacementStored(objectValue(node, key), node.context)); + return result; +} +function clonePlacementStored(value2, context) { + if (!isContainer2(value2)) + return copyJson(value2); + const node = context.rawNodes?.get(value2); + return node === void 0 ? copyJson(value2) : clonePlacementNode(node); +} +function defineData3(target, key, value2) { + DATA_DESCRIPTOR3.value = value2; + Object.defineProperty(target, key, DATA_DESCRIPTOR3); + DATA_DESCRIPTOR3.value = void 0; +} +function emitOperations(context) { + const simple = emitSimpleObjectOperations(context); + if (simple !== void 0) + return simple; + const operations = []; + const forcedFolds = /* @__PURE__ */ new Map(); + const emissionPaths = /* @__PURE__ */ new Map(); + const denseIndices = /* @__PURE__ */ new Map(); + for (const node of context.dirty) { + recordDenseArrayPosition(node, denseIndices); + if (Array.isArray(nodeBase(node))) { + const overlay = arrayOverlay(node); + if (!overlay.structural && (overlay.baseOverrides?.size ?? 0) > 0) { + let candidates = denseIndices.get(node); + if (candidates === void 0) { + candidates = { indices: [], bits: void 0, length: nodeBase(node).length }; + denseIndices.set(node, candidates); + } + for (const index of overlay.baseOverrides.keys()) + addDenseCandidate(candidates, index); + } + } + } + const denseRegions = /* @__PURE__ */ new Map(); + for (const [array, candidates] of denseIndices) { + const path2 = resolvePath(array); + if (path2 === void 0 || path2.some((segment) => typeof segment === "string" && RESERVED_SEGMENTS.has(segment))) { + continue; + } + const regions = buildDenseRegions(candidates); + if (regions.length > 0) + denseRegions.set(array, regions); + } + for (const node of context.dirty) { + const path2 = resolvePath(node); + if (path2 === void 0) + continue; + if (hasCoveringDenseRegion(node, path2, denseRegions)) + continue; + emissionPaths.set(node, path2); + if (!Array.isArray(nodeBase(node)) && hasReservedMutation(node)) + forcedFolds.set(node, path2); + const reservedAt = path2.findIndex((segment) => typeof segment === "string" && RESERVED_SEGMENTS.has(segment)); + if (reservedAt >= 0) { + let ancestor = node; + for (let depth = path2.length; depth > reservedAt; depth--) + ancestor = nodeParent(ancestor); + forcedFolds.set(ancestor, path2.slice(0, reservedAt)); + } + } + for (const [node, path2] of forcedFolds) + emissionPaths.set(node, path2); + for (const node of denseRegions.keys()) { + const path2 = resolvePath(node); + if (path2 !== void 0 && !hasCoveringDenseRegion(node, path2, denseRegions)) + emissionPaths.set(node, path2); + } + let maxDepth = 0; + for (const path2 of emissionPaths.values()) + maxDepth = Math.max(maxDepth, path2.length); + const buckets = Array.from({ length: maxDepth + 1 }, () => []); + for (const [node, path2] of emissionPaths) + buckets[path2.length].push(node); + const folded = /* @__PURE__ */ new Set(); + for (const bucket of buckets) { + for (const node of bucket) { + const path2 = emissionPaths.get(node); + if (hasPlacementAncestor(node) || hasFoldedAncestor(node, folded)) + continue; + if (forcedFolds.has(node)) { + emitSet(operations, path2, cloneNode(node)); + folded.add(node); + continue; + } + if (Array.isArray(nodeBase(node))) + emitArrayOperations(node, path2, operations, denseRegions.get(node)); + else + emitObjectOperations(node, path2, operations); + if (operations.length > MAX_DELTA_OPERATIONS) { + locatedDenseArray = void 0; + return [["r", cloneNode(context.root)]]; + } + } + } + locatedDenseArray = void 0; + return operations; +} +function emitSimpleObjectOperations(context) { + const nodes = []; + for (const node of context.dirty) { + if (Array.isArray(nodeBase(node)) || hasReservedMutation(node) || hasPlacementAncestor(node)) + return void 0; + for (let parent = nodeParent(node); parent !== void 0; parent = nodeParent(parent)) { + if (Array.isArray(nodeBase(parent))) + return void 0; + } + const path2 = resolvePath(node); + if (path2 === void 0) + continue; + for (const segment of path2) { + if (typeof segment === "string" && RESERVED_SEGMENTS.has(segment)) + return void 0; + } + nodes.push(node); + if (nodes.length > MAX_SIMPLE_OBJECT_NODES) + return void 0; + } + for (let index = 1; index < nodes.length; index++) { + const node = nodes[index]; + const depth = node.preparedPath.length; + let at = index; + while (at > 0 && nodes[at - 1].preparedPath.length > depth) { + nodes[at] = nodes[at - 1]; + at -= 1; + } + nodes[at] = node; + } + const operations = []; + let canMaterializeDirectly = true; + for (const node of nodes) { + if (emitObjectOperations(node, node.preparedPath, operations)) + canMaterializeDirectly = false; + if (operations.length > MAX_DELTA_OPERATIONS) + return [["r", cloneNode(context.root)]]; + } + context.simpleObjectMaterialization = canMaterializeDirectly; + return operations; +} +var locatedDenseArray; +var locatedDenseIndex = 0; +function locateDenseArrayPosition(node) { + let child = node; + for (let parent = nodeParent(child); parent !== void 0; parent = nodeParent(child)) { + if (Array.isArray(nodeBase(parent))) { + const overlay = arrayOverlay(parent); + if (child.parentKind !== 1 || overlay.structural) + return false; + const sourceIndex = child.parentKey; + const valueIndex = overlay.baseOverrides?.get(sourceIndex); + const current = valueIndex === void 0 ? nodeBase(parent)[sourceIndex] : storedValue(parent.context, valueIndex); + if (current !== nodeBase(child)) + return false; + locatedDenseArray = parent; + locatedDenseIndex = sourceIndex; + return true; + } + child = parent; + } + return false; +} +function hasCoveringDenseRegion(node, path2, denseRegions) { + for (let parent = nodeParent(node); parent !== void 0; parent = nodeParent(parent)) { + const regions = denseRegions.get(parent); + if (regions === void 0) + continue; + const parentPath = resolvePath(parent); + if (parentPath === void 0 || path2.length <= parentPath.length) + continue; + let matches = true; + for (let index2 = 0; index2 < parentPath.length; index2++) { + if (path2[index2] !== parentPath[index2]) { + matches = false; + break; + } + } + if (!matches) + continue; + const index = path2[parentPath.length]; + if (typeof index === "number" && regionContaining(regions, index)) + return true; + } + return false; +} +function addDenseCandidate(candidates, index) { + if (candidates.bits !== void 0) { + candidates.bits[index] = 1; + return; + } + candidates.indices.push(index); + if (candidates.indices.length < 256) + return; + candidates.bits = new Uint8Array(candidates.length); + for (const existing of candidates.indices) + candidates.bits[existing] = 1; + candidates.indices.length = 0; +} +function recordDenseArrayPosition(node, groups) { + if (!locateDenseArrayPosition(node)) + return; + let candidates = groups.get(locatedDenseArray); + if (candidates === void 0) { + candidates = { indices: [], bits: void 0, length: nodeBase(locatedDenseArray).length }; + groups.set(locatedDenseArray, candidates); + } + addDenseCandidate(candidates, locatedDenseIndex); +} +function buildDenseRegions(candidates) { + if (candidates.bits === void 0) + return []; + const regions = []; + const bits = candidates.bits; + for (let at = 0; at < bits.length; ) { + while (at < bits.length && bits[at] === 0) + at += 1; + if (at === bits.length) + break; + const start = at; + let end = at; + let count = 0; + let gap = 0; + while (at < bits.length) { + if (bits[at] !== 0) { + count += 1; + end = at; + gap = 0; + } else if (++gap > 1) + break; + at += 1; + } + const length = end - start + 1; + if (count >= 256 && count * 2 >= length) + regions.push({ start, length }); + } + return regions; +} +function regionContaining(regions, index) { + for (const region of regions) + if (index >= region.start && index < region.start + region.length) + return true; + return false; +} +function hasReservedMutation(node) { + if (node.writeKey !== void 0 && RESERVED_SEGMENTS.has(node.writeKey)) + return true; + if (node.deleteKey !== void 0 && RESERVED_SEGMENTS.has(node.deleteKey)) + return true; + if (node.writes !== void 0) { + for (const key of node.writes.keys()) + if (RESERVED_SEGMENTS.has(key)) + return true; + } + if (node.deletes !== void 0) { + for (const key of node.deletes) + if (RESERVED_SEGMENTS.has(key)) + return true; + } + return false; +} +function hasFoldedAncestor(node, folded) { + for (let parent = nodeParent(node); parent !== void 0; parent = nodeParent(parent)) + if (folded.has(parent)) + return true; + return false; +} +function hasPlacementAncestor(node) { + for (let current = node; current !== void 0; current = nodeParent(current)) { + if (nodeParent(current) !== void 0 && current.parentPlacement) + return true; + } + return false; +} +function emitObjectWrite(node, path2, operations, key, value2) { + const nextPath = [...path2, key]; + const appended = node.appends?.get(key); + if (appended !== void 0) { + operations.push(["a", nextPath, appended]); + return false; + } + const before = node.readded?.has(key) || !Object.hasOwn(nodeBase(node), key) ? void 0 : nodeBase(node)[key]; + const after = cloneStored(value2, node.context); + const emitted = emitChangedValue(operations, nextPath, before, after); + return isContainer2(before) && isContainer2(after) && !emitted; +} +function emitObjectOperations(node, path2, operations) { + if (node.readded !== void 0) { + for (const key of node.readded) { + if (Object.hasOwn(nodeBase(node), key)) + operations.push(["d", [...path2, key]]); + } + } + let normalizedContainerWrite = false; + if (node.writeKey !== void 0) + normalizedContainerWrite = emitObjectWrite(node, path2, operations, node.writeKey, objectValue(node, node.writeKey)); + if (node.writes !== void 0) { + for (const [key, valueIndex] of node.writes) { + if (operations.length > MAX_DELTA_OPERATIONS) + return normalizedContainerWrite; + if (emitObjectWrite(node, path2, operations, key, storedValue(node.context, valueIndex))) + normalizedContainerWrite = true; + } + } + if (node.deleteKey !== void 0 && Object.hasOwn(nodeBase(node), node.deleteKey)) { + operations.push(["d", [...path2, node.deleteKey]]); + } + if (node.deletes !== void 0) { + for (const key of node.deletes) { + if (operations.length > MAX_DELTA_OPERATIONS) + return normalizedContainerWrite; + if (Object.hasOwn(nodeBase(node), key)) + operations.push(["d", [...path2, key]]); + } + } + return normalizedContainerWrite; +} +function buildArrayPlan(node) { + const overlay = arrayOverlay(node); + if (overlay.plan !== void 0) + return overlay.plan; + const base = nodeBase(node); + const pieces = piecesOf(overlay); + overlay.plan = orderedArrayPlan(base.length, pieces) ?? elementArrayPlan(base.length, pieces); + return overlay.plan; +} +function orderedArrayPlan(baseLength, pieces) { + const ranges = []; + for (const piece of pieces) { + if (piece.kind !== "base" || piece.length === 0) + continue; + if (piece.step !== 1 && piece.length > 1) + return void 0; + if (ranges.length > 0 && piece.start <= ranges[ranges.length - 1]) + return void 0; + ranges.push(piece.start, piece.start + piece.length - 1); + } + const removeRuns = []; + let end = baseLength; + for (let index = ranges.length - 2; index >= -2; index -= 2) { + const start = index >= 0 ? ranges[index + 1] + 1 : 0; + if (end > start) + removeRuns.push(start, end - start); + if (index >= 0) + end = ranges[index]; + } + return { removeRuns, permutation: void 0, insertRuns: insertRunsOf(pieces) }; +} +function elementArrayPlan(baseLength, pieces) { + const retained = new Uint8Array(baseLength); + const targetBase = []; + for (const piece of pieces) { + if (piece.kind !== "base") + continue; + for (let offset = 0; offset < piece.length; offset++) { + const index = piece.start + piece.step * offset; + retained[index] = 1; + targetBase.push(index); + } + } + const removeRuns = []; + for (let end = baseLength; end > 0; ) { + if (retained[end - 1] !== 0) { + end -= 1; + continue; + } + let start = end - 1; + while (start > 0 && retained[start - 1] === 0) + start -= 1; + removeRuns.push(start, end - start); + end = start; + } + const retainedBase = []; + for (let index = 0; index < retained.length; index++) + if (retained[index] !== 0) + retainedBase.push(index); + let permutation; + if (targetBase.some((value2, index) => value2 !== retainedBase[index])) { + const positions = new Map(retainedBase.map((value2, index) => [value2, index])); + permutation = targetBase.map((value2) => positions.get(value2)); + } + return { removeRuns, permutation, insertRuns: insertRunsOf(pieces) }; +} +function insertRunsOf(pieces) { + const insertRuns = []; + let logicalIndex = 0; + for (let pieceIndex = 0; pieceIndex < pieces.length; ) { + const piece = pieces[pieceIndex]; + if (piece.kind === "base") { + logicalIndex += piece.length; + pieceIndex += 1; + continue; + } + const startPiece = pieceIndex; + while (pieceIndex < pieces.length && pieces[pieceIndex].kind === "insert") { + logicalIndex += pieces[pieceIndex].length; + pieceIndex += 1; + } + insertRuns.push(logicalIndex - rangeLength(pieces, startPiece, pieceIndex), startPiece, pieceIndex); + } + return insertRuns; +} +function rangeLength(pieces, start, end) { + let length = 0; + for (let index = start; index < end; index++) + length += pieces[index].length; + return length; +} +function emitArrayOperations(node, path2, operations, denseRegions) { + const overlay = arrayOverlay(node); + const base = nodeBase(node); + if (denseRegions !== void 0) { + for (const region of denseRegions) { + if (operations.length > MAX_DELTA_OPERATIONS) + return; + operations.push(["p", path2, region.start, region.length, cloneArrayRegion(node, region)]); + } + } + if (overlay.structural) { + const plan = buildArrayPlan(node); + for (let index = 0; index < plan.removeRuns.length; index += 2) { + if (operations.length > MAX_DELTA_OPERATIONS) + return; + operations.push(["p", path2, plan.removeRuns[index], plan.removeRuns[index + 1], []]); + } + if (plan.permutation !== void 0) + operations.push(["m", path2, plan.permutation]); + const pieces = piecesOf(overlay); + for (let run2 = 0; run2 < plan.insertRuns.length; run2 += 3) { + if (operations.length > MAX_DELTA_OPERATIONS) + return; + const logicalIndex = plan.insertRuns[run2]; + const items = []; + for (let pieceIndex = plan.insertRuns[run2 + 1]; pieceIndex < plan.insertRuns[run2 + 2]; pieceIndex++) { + const piece = pieces[pieceIndex]; + for (let offset = 0; offset < piece.length; offset++) { + const sourceIndex = piece.start + piece.step * offset; + items.push(cloneStored(entryValueAt(node, piece, sourceIndex), node.context)); + } + } + operations.push(["p", path2, logicalIndex, 0, items]); + } + } + for (const [baseIndex, valueIndex] of overlay.baseOverrides ?? []) { + if (operations.length > MAX_DELTA_OPERATIONS) + return; + const index = findEntryIndex(overlay, 1, baseIndex, void 0); + if (index === void 0 || denseRegions !== void 0 && regionContaining(denseRegions, index)) + continue; + emitChangedValue(operations, [...path2, index], base[baseIndex], cloneStored(storedValue(node.context, valueIndex), node.context)); + } +} +function cloneArrayRegion(node, region) { + const overlay = arrayOverlay(node); + const result = []; + for (let index = region.start; index < region.start + region.length; index++) { + const piece = locatePiece(overlay, index); + const sourceIndex = piece.start + piece.step * overlay.locatedOffset; + result.push(cloneStored(entryValueAt(node, piece, sourceIndex), node.context)); + } + return result; +} +function emitChangedValue(operations, path2, before, after) { + if (!isContainer2(after) && before === after) + return false; + if (isContainer2(before) && isContainer2(after) && equalTrustedJson(before, after)) + return false; + if (typeof before === "string" && typeof after === "string") { + if (after.length > before.length && after.slice(0, before.length) === before) { + operations.push(["a", path2, ownedSlice2(after, before.length)]); + return true; + } + const shared = overlap(before, after, 65536); + if (shared > 0) { + operations.push(["t", path2, before.length - shared]); + if (after.length > shared) + operations.push(["a", path2, ownedSlice2(after, shared)]); + return true; + } + } + operations.push(["s", path2, after]); + return true; +} +function emitSet(operations, path2, value2) { + if (path2.length === 0) + operations.push(["r", value2]); + else + operations.push(["s", path2, value2]); +} +function resolvePath(node) { + if (node.preparedPath !== void 0) + return node.preparedPath; + const parent = nodeParent(node); + if (parent === void 0) { + node.preparedPath = []; + return node.preparedPath; + } + const parentPath = resolvePath(parent); + if (parentPath === void 0) + return void 0; + if (node.parentKind === 0) { + const key = node.parentKey; + if (!objectHas(parent, key) || objectValue(parent, key) !== nodeBase(node)) + return void 0; + node.preparedPath = [...parentPath, key]; + return node.preparedPath; + } + const overlay = arrayOverlay(parent); + const index = findEntryIndex(overlay, node.parentKind, node.parentKey, node.parentSource); + if (index === void 0) + return void 0; + const piece = locatePiece(overlay, index); + if (entryValueAt(parent, piece, node.parentKey) !== nodeBase(node)) + return void 0; + node.preparedPath = [...parentPath, index]; + return node.preparedPath; +} +function ensurePieceLocations(overlay) { + if (overlay.baseLocations !== void 0) + return; + overlay.baseLocations = []; + overlay.insertLocations = /* @__PURE__ */ new Map(); + let logicalStart = 0; + for (const piece of piecesOf(overlay)) { + const last = piece.start + piece.step * (piece.length - 1); + const location = { + piece, + logicalStart, + minimum: Math.min(piece.start, last), + maximum: Math.max(piece.start, last) + }; + if (piece.kind === "base") + overlay.baseLocations.push(location); + else { + let locations = overlay.insertLocations.get(piece.source); + if (locations === void 0) { + locations = []; + overlay.insertLocations.set(piece.source, locations); + } + locations.push(location); + } + logicalStart += piece.length; + } + const byMinimum = (left, right) => left.minimum - right.minimum; + overlay.baseLocations.sort(byMinimum); + for (const locations of overlay.insertLocations.values()) + locations.sort(byMinimum); +} +function findEntryIndex(overlay, kind, sourceIndex, source) { + if (kind === 1 && !overlay.structural) + return sourceIndex; + ensurePieceLocations(overlay); + const locations = kind === 1 ? overlay.baseLocations : overlay.insertLocations.get(source); + if (locations === void 0) + return void 0; + let low = 0; + let high = locations.length; + while (low < high) { + const middle = low + high >>> 1; + if (locations[middle].minimum <= sourceIndex) + low = middle + 1; + else + high = middle; + } + const location = locations[low - 1]; + if (location === void 0 || sourceIndex > location.maximum) + return void 0; + const offset = (sourceIndex - location.piece.start) / location.piece.step; + return offset >= 0 && offset < location.piece.length ? location.logicalStart + offset : void 0; +} +function ensureOperations(context) { + if (context.ops !== void 0) + return context.ops; + assertReadable(context); + if (context.replacement) { + const base = context.tracker.deref()?.replacementBase(context); + context.replacementNoop = base !== void 0 && equalTrustedJson(base, nodeBase(context.root)); + context.ops = context.replacementNoop ? [] : [["r", nodeBase(context.root)]]; + } else + context.ops = emitOperations(context); + return context.ops; +} +function equalTrustedJson(left, right) { + if (left === right) + return true; + if (!isContainer2(left) || !isContainer2(right) || Array.isArray(left) !== Array.isArray(right)) + return false; + if (Array.isArray(left)) { + const other2 = right; + if (left.length !== other2.length) + return false; + for (let index = 0; index < left.length; index++) + if (!equalTrustedJson(left[index], other2[index])) + return false; + return true; + } + const other = right; + const keys = Object.keys(left); + if (keys.length !== Object.keys(other).length) + return false; + for (const key of keys) + if (!Object.hasOwn(other, key) || !equalTrustedJson(left[key], other[key])) + return false; + return true; +} +function abortContext(context) { + if (context.status.value === "aborted" || context.status.value === "consumed" || context.status.value === "stale") + return; + context.status.value = "aborted"; + clearContext(context); +} +function releaseOverlayReferences(context) { + context.tracker.deref()?.releaseContext(context); + if (locatedDenseArray?.context === context) + locatedDenseArray = void 0; + context.overlayReleased = true; + context.dirty.length = 0; + context.nodes.length = 0; + context.rawNodes = void 0; + context.ops = void 0; + context.root = void 0; + context.bases = void 0; + context.stored = void 0; + context.baseValue = void 0; +} +function clearContext(context) { + context.tracker.deref()?.releaseContext(context); + if (locatedDenseArray?.context === context) + locatedDenseArray = void 0; + for (const node of context.nodes) { + if (Array.isArray(node.target)) + node.target.length = 0; + Reflect.deleteProperty(node.target, NODE); + node.writes?.clear(); + node.deletes?.clear(); + node.readded?.clear(); + if (node.array?.pieces !== void 0) + node.array.pieces.length = 0; + if (node.array !== void 0) { + node.array.root = void 0; + node.array.baseLocations = void 0; + node.array.insertLocations = void 0; + } + node.array?.baseOverrides?.clear(); + node.array?.insertOverrides?.clear(); + node.parentSource = void 0; + node.target = RELEASED; + node.proxy = RELEASED; + node.writeKey = void 0; + node.writeValue = void 0; + node.writes = void 0; + node.deleteKey = void 0; + node.deletes = void 0; + node.readded = void 0; + node.array = void 0; + node.subtreeDirty = void 0; + node.preparedPath = void 0; + } + context.dirty.length = 0; + context.nodes.length = 0; + context.rawNodes = void 0; + context.ops = void 0; + context.root = void 0; + context.bases = void 0; + context.stored = void 0; + context.baseValue = void 0; + context.overlayReleased = true; +} +function isContainer2(value2) { + return value2 !== null && typeof value2 === "object"; +} + +// node_modules/@earendil-works/chord/dist/delta/pending.js +var MAX_PENDING_OPS = 1e4; +var EMPTY = { kind: "empty" }; +var REPLACE = { kind: "replace" }; +var NO_OPS = { kind: "ops", ops: [] }; +var REPLACE_TAKE = { kind: "replace" }; +var PendingOps = class { + #state = EMPTY; + /** Queue the ops of one commit. */ + push(ops3) { + if (ops3.length === 0) + return; + const state = this.#state; + if (ops3[0][0] === "r") { + this.#state = { kind: "batch", ops: ops3 }; + return; + } + switch (state.kind) { + case "empty": + this.#state = { kind: "batch", ops: ops3 }; + return; + case "batch": { + const folding = { kind: "folding", ops: [], leaves: /* @__PURE__ */ new Map() }; + this.#state = folding; + if (this.#fold(state.ops, folding)) + this.#fold(ops3, folding); + return; + } + case "folding": + this.#fold(ops3, state); + return; + case "replace": + return; + } + } + /** + * Pending ops after folding; 1 once overflowed (the replacement the owner sends). One batch is counted as received, + * so a single commit larger than `MAX_PENDING_OPS` counts in full until a second one arrives and overflows. + */ + get size() { + const state = this.#state; + if (state.kind === "empty") + return 0; + return state.kind === "replace" ? 1 : state.ops.length; + } + /** Hand over everything pending and start empty. */ + take() { + const state = this.#state; + this.#state = EMPTY; + switch (state.kind) { + case "empty": + return NO_OPS; + case "batch": + case "folding": + return { kind: "ops", ops: state.ops }; + case "replace": + return REPLACE_TAKE; + } + } + /** Fold `ops` into `folding`; false once the queue overflowed into `replace`. */ + #fold(ops3, folding) { + for (const op2 of ops3) { + switch (op2[0]) { + case "r": + folding.ops.length = 0; + folding.leaves.clear(); + break; + case "a": + if (mergeLeaf(op2, folding)) + continue; + break; + case "s": + if (isPrimitive(op2[2])) { + if (mergeLeaf(op2, folding)) + continue; + } else if (folding.leaves.size > 0) + folding.leaves.clear(); + break; + default: + if (folding.leaves.size > 0) + folding.leaves.clear(); + } + folding.ops.push(op2); + if (folding.ops.length > MAX_PENDING_OPS) { + this.#state = REPLACE; + return false; + } + } + return true; + } +}; +var isPrimitive = (value2) => value2 === null || typeof value2 !== "object"; +function mergeLeaf(op2, folding) { + const key = pathKey(op2[1]); + const at = folding.leaves.get(key); + if (at !== void 0) { + const merged = merge2(folding.ops[at], op2); + if (merged !== void 0) { + folding.ops[at] = merged; + return true; + } + } + folding.leaves.set(key, folding.ops.length); + return false; +} +function pathKey(path2) { + return JSON.stringify(path2); +} +function merge2(previous, next) { + if (next[0] === "s") + return next; + if (previous[0] === "a") + return ["a", previous[1], previous[2] + next[2]]; + if (typeof previous[2] === "string") + return ["s", previous[1], previous[2] + next[2]]; + return void 0; +} + +// node_modules/@earendil-works/chord/dist/delta/index.js +var isObj = (value2) => value2 !== null && typeof value2 === "object"; +function overlap(a, b, scan, probe = 64, maxCandidates = 8) { + if (a.length === 0 || b.length === 0 || scan === 0) + return 0; + const tail = a.length > scan ? a.slice(a.length - scan) : a; + for (const h of [Math.min(probe, b.length), 1]) { + const head = b.slice(0, h); + let tried = 0; + for (let k = tail.indexOf(head); k !== -1; k = tail.indexOf(head, k + 1)) { + if (++tried > maxCandidates) + break; + const n = tail.length - k; + if (n <= b.length && tail.slice(k) === b.slice(0, n)) + return n; + } + if (h === 1) + break; + } + return 0; +} +function ownedSlice2(text, start, end = text.length) { + return `${text.slice(start, end)} `.slice(0, -1); +} +var RESERVED_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]); +var UnsafePathError = class extends Error { + // Not a parameter property: Node's --experimental-strip-types rejects those, + // and these files are meant to run under it directly. + segment; + constructor(segment) { + super(`unsafe path segment: ${String(segment)}`); + this.segment = segment; + this.name = "UnsafePathError"; + } +}; +function assertValidOp(op2) { + if (!Array.isArray(op2) || op2.length === 0) + throw new TypeError("op is not a tuple"); + switch (op2[0]) { + case "r": + if (op2.length !== 2) + throw new TypeError("r arity"); + return; + case "s": + if (op2.length !== 3) + throw new TypeError("s arity"); + assertPathArg(op2[1], true); + return; + case "d": + if (op2.length !== 2) + throw new TypeError("d arity"); + assertPathArg(op2[1], true); + return; + case "a": + if (op2.length !== 3 || typeof op2[2] !== "string") + throw new TypeError("a shape"); + assertPathArg(op2[1], true); + return; + case "t": + if (op2.length !== 3 || !Number.isInteger(op2[2]) || op2[2] < 0) + throw new TypeError("t shape"); + assertPathArg(op2[1], true); + return; + case "p": { + if (op2.length !== 5) + throw new TypeError("p arity"); + assertPathArg(op2[1]); + if (!Number.isInteger(op2[2]) || op2[2] < 0) + throw new TypeError("p index"); + if (!Number.isInteger(op2[3]) || op2[3] < 0) + throw new TypeError("p remove"); + if (!Array.isArray(op2[4])) + throw new TypeError("p items"); + return; + } + case "m": + if (op2.length !== 3) + throw new TypeError("m arity"); + assertPathArg(op2[1]); + assertPermutation(op2[2]); + return; + // Silently skipping an unknown verb is how a newer producer's op vanishes. + default: + throw new TypeError(`unknown op verb: ${String(op2[0])}`); + } +} +function assertPathArg(p, nonEmpty = false) { + if (!Array.isArray(p)) + throw new TypeError("path is not an array"); + if (nonEmpty && p.length === 0) + throw new TypeError("path is empty"); + assertSafePath(p); +} +function assertPermutation(value2) { + if (!Array.isArray(value2)) + throw new TypeError("m permutation is not an array"); + const seen = new Uint8Array(value2.length); + for (const index of value2) { + if (!Number.isInteger(index) || index < 0 || index >= value2.length || seen[index] !== 0) { + throw new TypeError("m permutation is not a bijection"); + } + seen[index] = 1; + } +} +function assertSafePath(path2) { + for (const seg of path2) { + if (typeof seg === "string") { + if (RESERVED_SEGMENTS.has(seg)) + throw new UnsafePathError(seg); + } else if (!Number.isInteger(seg) || seg < 0) { + throw new UnsafePathError(seg); + } + } +} +function assertIndexInRange(parent, index) { + if (index > parent.length) + throw new UnsafePathError(index); +} +var PathError = class extends Error { + path; + constructor(path2) { + super(`unresolvable path: ${JSON.stringify(path2)}`); + this.path = path2; + this.name = "PathError"; + } +}; +function apply(target, ops3) { + return applyOps(target, ops3); +} +function applyOps(target, ops3) { + let root2 = target; + for (const op2 of ops3) { + assertValidOp(op2); + if (op2[0] === "r") { + root2 = op2[1]; + continue; + } + const path2 = op2[1]; + assertSafePath(path2); + if (op2[0] === "p") { + const target_ = path2.length === 0 ? root2 : resolve(root2, path2); + if (!Array.isArray(target_)) + throw new PathError(path2); + target_.splice(op2[2], op2[3]); + const chunkSize = 1e4; + for (let offset = 0; offset < op2[4].length; offset += chunkSize) { + target_.splice(op2[2] + offset, 0, ...op2[4].slice(offset, offset + chunkSize)); + } + continue; + } + if (op2[0] === "m") { + const target_ = path2.length === 0 ? root2 : resolve(root2, path2); + if (!Array.isArray(target_) || target_.length !== op2[2].length) + throw new PathError(path2); + const previous = target_.slice(); + for (let index = 0; index < op2[2].length; index++) + target_[index] = previous[op2[2][index]]; + continue; + } + const parent = resolve(root2, path2.slice(0, -1)); + const key = path2[path2.length - 1]; + if (Array.isArray(parent)) { + if (typeof key !== "number") + throw new UnsafePathError(key); + assertIndexInRange(parent, key); + } + const write = (value2) => { + Object.defineProperty(parent, key, { value: value2, writable: true, enumerable: true, configurable: true }); + }; + const read2 = () => Object.hasOwn(parent, key) ? parent[key] : void 0; + switch (op2[0]) { + case "s": + write(op2[2]); + break; + case "d": + if (Array.isArray(parent)) { + if (typeof key !== "number" || key >= parent.length) + throw new PathError(path2); + parent.splice(key, 1); + } else + delete parent[key]; + break; + case "a": { + const current = read2(); + if (typeof current !== "string") + throw new PathError(path2); + write(`${current}${op2[2]}`); + break; + } + case "t": { + const current = read2(); + if (typeof current !== "string") + throw new PathError(path2); + write(current.slice(op2[2])); + break; + } + } + } + return root2; +} +function applyImmutable(target, ops3) { + return applyImmutableBatches(target, [ops3]); +} +function applyImmutableBatches(target, batches) { + let root2 = target; + const owned2 = /* @__PURE__ */ new WeakSet(); + for (const ops3 of batches) { + for (const op2 of ops3) { + assertValidOp(op2); + if (op2[0] === "r") { + root2 = op2[1]; + continue; + } + if (op2[0] === "p") { + const appended = appendCopy(root2, op2, owned2); + if (appended !== void 0) { + root2 = appended; + continue; + } + } + root2 = copyContainers(root2, op2[0] === "p" || op2[0] === "m" ? op2[1] : op2[1].slice(0, -1), owned2); + root2 = applyOps(root2, [op2]); + } + } + return root2; +} +function appendCopy(root2, op2, owned2) { + const [, path2, index, deleteCount, items] = op2; + if (deleteCount !== 0) + return void 0; + let target = root2; + for (const segment of path2) { + const valid = typeof segment === "string" ? !RESERVED_SEGMENTS.has(segment) && isObj(target) && !Array.isArray(target) : Number.isInteger(segment) && segment >= 0 && Array.isArray(target); + if (!valid || !Object.hasOwn(target, segment)) + return void 0; + target = target[segment]; + } + if (!Array.isArray(target) || owned2.has(target) || index !== target.length) + return void 0; + const appended = target.concat(items); + owned2.add(appended); + if (path2.length === 0) + return appended; + const parentPath = path2.slice(0, -1); + const copiedRoot = copyContainers(root2, parentPath, owned2); + Object.defineProperty(resolve(copiedRoot, parentPath), path2[path2.length - 1], { + value: appended, + writable: true, + enumerable: true, + configurable: true + }); + return copiedRoot; +} +function copyContainers(root2, path2, owned2) { + const copy2 = (value2) => { + if (Array.isArray(value2)) + return value2.slice(); + if (!isObj(value2)) + throw new PathError(path2); + return Object.getPrototypeOf(value2) === null ? Object.assign(/* @__PURE__ */ Object.create(null), value2) : { ...value2 }; + }; + if (!isObj(root2)) + throw new PathError(path2); + let copiedRoot; + if (owned2.has(root2)) + copiedRoot = root2; + else { + copiedRoot = copy2(root2); + owned2.add(copiedRoot); + } + let destination = copiedRoot; + for (const segment of path2) { + if (!Object.hasOwn(destination, segment)) + throw new PathError(path2); + if (Array.isArray(destination) && typeof segment !== "number") + throw new UnsafePathError(segment); + const child = destination[segment]; + if (!isObj(child)) + throw new PathError(path2); + if (owned2.has(child)) { + destination = child; + continue; + } + const copiedChild = copy2(child); + Object.defineProperty(destination, segment, { + value: copiedChild, + writable: true, + enumerable: true, + configurable: true + }); + owned2.add(copiedChild); + destination = copiedChild; + } + return copiedRoot; +} +function resolveValue(root2, path2) { + let node = root2; + for (const seg of path2) { + if (!isObj(node)) + throw new PathError(path2); + if (Array.isArray(node) && typeof seg !== "number") + throw new UnsafePathError(seg); + if (!Object.hasOwn(node, seg)) + throw new PathError(path2); + node = node[seg]; + } + return node; +} +function resolve(root2, path2) { + const node = resolveValue(root2, path2); + if (!isObj(node)) + throw new PathError(path2); + return node; +} + +// node_modules/@earendil-works/chord/dist/watch.js +var STOPPED = { kind: "stopped" }; +var ABORTED = { kind: "aborted" }; +var ENDED = { kind: "ended" }; +var WatchLifecycle = class { + #detach; + #report; + #closed = Promise.withResolvers(); + #value; + #listener; + #started = false; + #scheduled = false; + #running = false; + /** Set by `endAfterDelivery()`: the source sends nothing more; the watch ends `ended` once it delivered the rest. */ + #ending = false; + /** Set once, by `#terminate()`. */ + #end; + #abortSignal; + #abortListener; + constructor(value2, detach, report2) { + this.#value = value2; + this.#detach = detach; + this.#report = report2; + } + get value() { + return this.#value; + } + get closed() { + return this.#closed.promise; + } + start(listener) { + if (this.#started) + throw new Error("Watch is already started"); + if (this.#end !== void 0) + throw new Error("Watch is stopped"); + this.#started = true; + this.#listener = listener; + this.#schedule(); + } + stop() { + this.#terminate(STOPPED); + return this.#closed.promise; + } + /** End the watch `aborted` when `signal` aborts; installed once. */ + observeAbort(signal) { + if (this.#abortSignal !== void 0) + throw new Error("Watch abort is already installed"); + if (this.#end !== void 0) + return; + this.#abortSignal = signal; + this.#abortListener = () => this.abort(); + signal.addEventListener("abort", this.#abortListener, { once: true }); + if (signal.aborted) + this.abort(); + } + /** End the watch `aborted`: its acquisition context was aborted. */ + abort() { + this.#terminate(ABORTED); + } + /** + * End the watch with `end` at once, dropping what is pending: the source closed (`ended`), or failed for good + * (`failed`). A failure the source may recover from is a `failed` call instead (`fail()`). + */ + end(end) { + this.#terminate(end); + } + /** + * The source sends nothing more after its last frames, such as a retirement: take no more frames, deliver what is + * pending, then end `ended`, after the running call returns, if one runs. Until then the watch stays attached, so + * `end()`, `stop()`, an abort, and a throwing listener still end it first; a watch never started keeps what is + * pending until one of them or `start()`. + */ + endAfterDelivery() { + if (this.#end !== void 0 || this.#ending) + return; + this.#ending = true; + if (!this.#running && this.pendingContext === void 0) + this.#terminate(ENDED); + } + /** Whether the watch still takes frames: it has not ended, and its source has not ended. */ + get open() { + return this.#end === void 0 && !this.#ending; + } + /** Something became pending: deliver it once started, never inline. */ + pending() { + if (this.#started) + this.#schedule(); + } + #schedule() { + if (this.#scheduled || this.#running || this.#end !== void 0) + return; + this.#scheduled = true; + queueMicrotask(() => { + this.#scheduled = false; + void this.#drain(); + }); + } + /** + * Deliver until nothing is pending; what arrives during a call is taken after it returns. A watch whose source ended + * ends once it delivered the rest and its last call returned. + */ + async #drain() { + if (this.#running || this.#end !== void 0 || !this.#started) + return; + this.#running = true; + try { + while (this.#end === void 0) { + const listener = this.#listener; + if (listener === void 0) + break; + const frameContext = this.pendingContext; + if (frameContext === void 0) { + if (this.#ending) + this.#terminate(ENDED); + break; + } + const call = this.next(); + if (call.kind === "value") + this.#value = call.value; + try { + await listener(call, withoutAbortSignal(frameContext)); + } catch (error) { + if (this.#end === void 0) { + this.#report?.(error); + this.#terminate({ kind: "listener_error", error: toError(error) }); + } + break; + } + } + } finally { + this.#running = false; + } + } + /** + * End the watch, once: detach, drop what is pending and every reference into the source, and settle `closed`. A + * callback already running keeps running. + */ + #terminate(end) { + if (this.#end !== void 0) + return; + this.#end = end; + const detach = this.#detach; + this.#detach = void 0; + this.#report = void 0; + this.#listener = void 0; + this.release(); + if (this.#abortListener !== void 0) + this.#abortSignal?.removeEventListener("abort", this.#abortListener); + this.#abortSignal = void 0; + this.#abortListener = void 0; + detach?.(); + this.#closed.resolve(end); + } +}; +function toError(error) { + if (error instanceof Error) + return error; + try { + return new Error(String(error)); + } catch { + return new Error(Object.prototype.toString.call(error)); + } +} +var Watch = class extends WatchLifecycle { + #ops = new PendingOps(); + /** + * The source's call while it is the only frame pending: the call passes it on, with no allocation. A second frame + * drops it, so its ops stay only as long as `PendingOps` keeps them (folded, or dropped by an overflow). + */ + #lone; + /** The newest frame's value and context, while a call is pending; the context also marks that one is. */ + #newestValue; + #newestContext; + /** A pending frame was a retirement. */ + #retired = false; + /** The newest frame was a failure: the next call is this. */ + #failure; + /** A failure came after the last value call: the next value call replaces the root, and ops are not kept. */ + #replace = false; + /** + * Queue the frame `call`. A value's ops turn the previous value into it, and `retired` says the document the watch + * follows was retired, which the next value call reports however many frames it folds; a failure is `fail()`. + */ + advance(call, context) { + if (call.kind === "failed") { + this.#fail(call, context); + return; + } + if (!this.open) + return; + if (this.#replace) { + this.#failure = void 0; + } else { + this.#lone = this.#newestContext === void 0 ? call : void 0; + this.#ops.push(call.ops); + } + this.#newestValue = call.value; + this.#newestContext = context; + if (call.retired) + this.#retired = true; + this.pending(); + } + /** + * Queue a failure the source may recover from: the pending frames are dropped (`value` keeps the last delivered + * value), the listener gets `{ kind: "failed", error }`, and the next value arrives as `[["r", value]]`. + */ + fail(error, context) { + this.#fail({ kind: "failed", error }, context); + } + #fail(call, context) { + if (!this.open) + return; + this.#ops.take(); + this.#lone = void 0; + this.#newestValue = void 0; + this.#failure = call; + this.#newestContext = context; + this.#replace = true; + this.pending(); + } + get pendingContext() { + return this.#newestContext; + } + next() { + if (this.#replace) + return this.#nextAfterFailure(); + const lone = this.#lone; + const value2 = this.#newestValue; + const batch = this.#ops.take(); + const retired = this.#retired; + this.#lone = void 0; + this.#newestValue = void 0; + this.#newestContext = void 0; + this.#retired = false; + if (lone !== void 0) + return lone; + const ops3 = batch.kind === "ops" ? batch.ops : [["r", value2]]; + return { kind: "value", value: value2, ops: ops3, retired }; + } + /** The pending failure, or the value after one as a root replacement (no ops were kept since the failure). */ + #nextAfterFailure() { + const failure = this.#failure; + this.#newestContext = void 0; + if (failure !== void 0) { + this.#failure = void 0; + return failure; + } + const value2 = this.#newestValue; + const retired = this.#retired; + this.#newestValue = void 0; + this.#retired = false; + this.#replace = false; + return { kind: "value", value: value2, ops: [["r", value2]], retired }; + } + release() { + this.#ops.take(); + this.#lone = void 0; + this.#newestValue = void 0; + this.#newestContext = void 0; + this.#retired = false; + this.#failure = void 0; + this.#replace = false; + } +}; + +// node_modules/@earendil-works/pi-durable/dist/ids.js +function idFromNumber(value2) { + return value2; +} +function seqFromNumber(value2) { + return value2; +} + +// node_modules/@earendil-works/pi-durable/dist/documents.js +function defineDoc(definition) { + validateDefinition(definition); + return { definition }; +} +function defineDocFamily(definition) { + validateDefinition(definition); + return { definition }; +} +function validateDefinition(definition) { + if (!Number.isSafeInteger(definition.version) || definition.version < 1) { + throw new TypeError(`Document ${definition.kind} version must be a positive integer`); + } +} +function resolveAddress(definition, args) { + let index = 0; + let scope; + switch (definition.scope) { + case "session": + scope = { kind: "session" }; + break; + case "conversation": + scope = { kind: "conversation", conversationId: ownerId(args[index++], definition) }; + break; + case "task": + scope = { kind: "task", taskId: ownerId(args[index++], definition) }; + break; + } + const key = definition.family === true ? args[index++] : void 0; + const address = key === void 0 ? { kind: definition.kind, scope } : { kind: definition.kind, scope, key }; + return { address, id: addressId(address), nextArgument: index }; +} +function ownerId(value2, definition) { + if (typeof value2 !== "number" || !Number.isSafeInteger(value2)) { + throw new TypeError(`Document ${definition.kind} requires a ${definition.scope} ID`); + } + return idFromNumber(value2); +} +function addressId(address) { + const owner = address.scope.kind === "session" ? null : address.scope.kind === "conversation" ? address.scope.conversationId : address.scope.taskId; + return JSON.stringify([address.kind, address.scope.kind, owner, address.key ?? null]); +} +function documentCreate(definition, address, id) { + const key = address.key === void 0 ? {} : { key: address.key }; + switch (address.scope.kind) { + case "session": + return { id, kind: address.kind, ...key, scope: address.scope }; + case "task": + return { id, kind: address.kind, ...key, scope: address.scope }; + case "conversation": + return { + id, + kind: address.kind, + ...key, + scope: address.scope, + history: definition.history, + fork: definition.fork + }; + } +} +function checkRecordScope(definition, record) { + if (record.scope.kind !== definition.scope || record.scope.kind === "conversation" && (record.history !== definition.history || record.fork !== definition.fork)) { + throw new TypeError(`Document ${record.id} (${record.kind}) does not match the supplied definition semantics`); + } +} +function checkRecordVersion(definition, record, version) { + if (version > definition.version) { + throw new Error(`Document ${record.id} (${record.kind}) has newer version ${version} than ${definition.version}`); + } + if (version < definition.version && definition.migrate === void 0) { + throw new Error(`Document ${record.id} (${record.kind}) requires migration from version ${version}`); + } +} +function materializeDocument(definition, stored) { + return materializeDocumentValue(definition, stored.record, stored.version, stored.value); +} +function materializeDocumentValue(definition, record, version, value2) { + checkRecordScope(definition, record); + checkRecordVersion(definition, record, version); + if (version === definition.version) + return value2; + return copyJson(definition.migrate(value2, version)); +} + +// node_modules/@earendil-works/pi-durable/dist/entries.js +function defineEntry(kind) { + if (typeof kind !== "string" || kind.length === 0) + throw new TypeError("Entry kind must be a non-empty string"); + return { + kind, + is: (entry2) => entry2 !== void 0 && entry2.kind === kind + }; +} +var UserEntry = defineEntry("pi.user"); +var AssistantEntry = defineEntry("pi.assistant"); +var SystemEntry = defineEntry("pi.system"); +var ToolResultEntry = defineEntry("pi.tool-result"); +var ResetEntry = defineEntry("pi.reset"); +var CompactionEntry = defineEntry("pi.compaction"); + +// node_modules/@earendil-works/pi-durable/dist/errors.js +function errorMessage(error) { + if (error instanceof Error) + return error.message; + try { + return String(error); + } catch { + return Object.prototype.toString.call(error); + } +} +var ReadAfterWrite = class extends Error { + /** `operation` names the call, such as `Tx.task()`. */ + constructor(operation) { + super(`${operation} cannot read tables after the first table write`); + this.name = "ReadAfterWrite"; + } +}; +var StorageRequestError = class extends Error { + constructor(message) { + super(message); + this.name = "StorageRequestError"; + } +}; +var SessionFailed = class extends Error { + constructor(cause) { + super("Session failed after a storage error; close and reopen it", { cause }); + this.name = "SessionFailed"; + } +}; +var ReadOnlySession = class extends Error { + constructor(what) { + super(`Read-only Harness: ${what} refused`); + this.name = "ReadOnlySession"; + } +}; +var ConversationBusy = class extends Error { + conversationId; + constructor(conversationId) { + super(`Conversation ${conversationId} is busy`); + this.name = "ConversationBusy"; + this.conversationId = conversationId; + } +}; +var ConversationIdle = class extends Error { + conversationId; + constructor(conversationId) { + super(`Conversation ${conversationId} is idle`); + this.name = "ConversationIdle"; + this.conversationId = conversationId; + } +}; + +// node_modules/@earendil-works/pi-durable/dist/timers.js +var MAX_TIMER_DELAY = 2147483647; +var inWorkers = typeof navigator !== "undefined" && navigator.userAgent === "Cloudflare-Workers"; +var nativeTimer = (callback, delayMs, options) => { + if (options.background && inWorkers) + return void 0; + const arm = (remaining) => { + const next = setTimeout(() => { + if (remaining > MAX_TIMER_DELAY) + handle = arm(remaining - MAX_TIMER_DELAY); + else + callback(); + }, Math.min(remaining, MAX_TIMER_DELAY)); + if (options.background) + next.unref?.(); + return next; + }; + let handle = arm(Math.max(0, delayMs)); + if (options.background && typeof handle.unref !== "function") { + clearTimeout(handle); + return void 0; + } + return () => clearTimeout(handle); +}; +function hostTimers(setTimer, report2) { + const arm = setTimer ?? nativeTimer; + let reported = false; + const fallBack = (error) => { + if (reported) + return; + reported = true; + report2(error); + }; + return { + foreground: (callback, delayMs) => { + try { + const disarm = arm(callback, delayMs, { background: false }); + if (disarm !== void 0) + return disarm; + fallBack(new Error("HarnessOptions.setTimer declined a timer that paces work; using setTimeout")); + } catch (error) { + fallBack(error); + } + return nativeTimer(callback, delayMs, { background: false }); + }, + background: (callback, delayMs) => { + try { + return arm(callback, delayMs, { background: true }); + } catch (error) { + report2(error); + return void 0; + } + } + }; +} + +// node_modules/@earendil-works/pi-durable/dist/session/cached-storage.js +var CachedStorage = class { + #storage; + #conversations = new Lru(4096); + /** Live (non-terminal) task records, until their task ends. */ + #liveTasks = /* @__PURE__ */ new Map(); + /** + * Tasks some task waits on, or waited on until its wait ended and it has not moved on since: how many such waits name + * each, and its ended record once it ended. + */ + #awaited = /* @__PURE__ */ new Map(); + /** Per task that waits, or left its wait and has not changed since: what it waited on, and whether it left. */ + #waits = /* @__PURE__ */ new Map(); + // Ended task records carry inputs and outcomes, such as a tool call's arguments, so fewer are kept. + #tasks = new Lru(256); + #submissions = new Lru(256); + /** Per record with a read in flight: the reads, and how many published commits wrote it since the first began. */ + #reads = /* @__PURE__ */ new Map(); + constructor(storage) { + this.#storage = storage; + } + /** The backend's own scope for a publication (`Storage.publishing`), when it has one. */ + get publishing() { + return scopeOf(this.#storage); + } + /** Pass the batches through; the Session applies each one with `published()` when it publishes it. */ + commit(batches, receive, context) { + return this.#storage.commit(batches, receive, context); + } + /** + * Apply a batch `commit()` stored, when the Session publishes it. Its records are frozen in place and kept as + * they are, no copies: the Session's components that follow the publication, such as the scheduler's live tasks, + * hold the same objects, so a live task's record is in memory once. + */ + published(writes) { + for (const write of writes) { + switch (write.type) { + case "conversation": + this.#wrote(write.value.id); + this.#conversations.set(write.value.id, freeze(write.value)); + break; + case "task": + this.#wrote(write.value.id); + this.#followWait(write.value); + this.#keepTask(freeze(write.value)); + break; + case "submission": + this.#wrote(write.value.id); + this.#submissions.set(write.value.id, freeze(write.value)); + break; + } + } + } + mintId() { + return this.#storage.mintId(); + } + conversation(id, context) { + const cached = this.#conversations.get(id); + if (cached !== void 0) + return Promise.resolve(cached); + return this.#keep(id, () => this.#storage.conversation(id, context), (record) => this.#conversations.set(id, record)); + } + scanConversations(query, limit, cursor, context) { + return this.#storage.scanConversations(query, limit, cursor, context); + } + entry(first, second, third) { + if (third === void 0) + return this.#storage.entry(first, second); + return this.#storage.entry(first, second, third); + } + findLatestHeadMarker(conversationId, atOrBeforeEntryId, context) { + return this.#storage.findLatestHeadMarker(conversationId, atOrBeforeEntryId, context); + } + scanEntries(query, limit, cursor, context) { + return this.#storage.scanEntries(query, limit, cursor, context); + } + task(id, context) { + const cached = this.#liveTasks.get(id) ?? this.#awaited.get(id)?.record ?? this.#tasks.get(id); + if (cached !== void 0) + return Promise.resolve(cached); + return this.#keep(id, () => this.#storage.task(id, context), (record) => this.#keepTask(record)); + } + scanTasks(query, limit, cursor, context) { + return this.#storage.scanTasks(query, limit, cursor, context); + } + /** Cached as written only: this process writes every submission it settles, and a read one is usually settled. */ + submission(id, context) { + const cached = this.#submissions.get(id); + return cached === void 0 ? this.#storage.submission(id, context) : Promise.resolve(cached); + } + scanSubmissions(query, limit, cursor, context) { + return this.#storage.scanSubmissions(query, limit, cursor, context); + } + submissionByRequest(conversationId, requestId, context) { + return this.#storage.submissionByRequest(conversationId, requestId, context); + } + findDocument(address, at, context) { + return this.#storage.findDocument(address, at, context); + } + document(id, at, context) { + return this.#storage.document(id, at, context); + } + scanDocuments(query, limit, cursor, context) { + return this.#storage.scanDocuments(query, limit, cursor, context); + } + close(context) { + return this.#storage.close(context); + } + /** Records kept per table, for tests that bound memory. */ + get sizes() { + return { + conversations: this.#conversations.size, + tasks: this.#tasks.size, + liveTasks: this.#liveTasks.size, + awaited: this.#awaited.size, + waits: this.#waits.size, + submissions: this.#submissions.size + }; + } + /** Keep a frozen task record: a live one until its task ends, an ended one in the bounded table. */ + #keepTask(record) { + if (record.state.status === "terminal") { + this.#liveTasks.delete(record.id); + const awaited = this.#awaited.get(record.id); + if (awaited !== void 0) + awaited.record = record; + else + this.#tasks.set(record.id, record); + return; + } + this.#tasks.delete(record.id); + this.#liveTasks.set(record.id, record); + } + /** + * Follow a task's waits from its written record: a wait holds the ended records of its `on` until the task, after + * leaving the wait, changes again (or ends), as the phase that resumes reads their outcomes before its next commit. + */ + #followWait(record) { + const wait = this.#waits.get(record.id); + if (record.state.status === "waiting") { + if (wait !== void 0) + this.#release(wait.on); + const on = record.state.on; + this.#waits.set(record.id, { on, left: false }); + for (const id of on) { + let awaited = this.#awaited.get(id); + if (awaited === void 0) { + const ended = this.#tasks.get(id); + this.#tasks.delete(id); + awaited = ended === void 0 ? { count: 0 } : { count: 0, record: ended }; + this.#awaited.set(id, awaited); + } + awaited.count++; + } + return; + } + if (wait === void 0) + return; + if (!wait.left && record.state.status !== "terminal") { + wait.left = true; + return; + } + this.#waits.delete(record.id); + this.#release(wait.on); + } + /** A wait on `on` is over: what no other wait names goes back to the bounded table. */ + #release(on) { + for (const id of on) { + const awaited = this.#awaited.get(id); + if (--awaited.count > 0) + continue; + this.#awaited.delete(id); + if (awaited.record !== void 0) + this.#tasks.set(id, awaited.record); + } + } + /** A published commit wrote record `id`: a read of it in flight may have missed that. */ + #wrote(id) { + const reads = this.#reads.get(id); + if (reads !== void 0) + reads.version++; + } + /** Resolve with a read's result, frozen; keep a found record unless a commit published since wrote it. */ + async #keep(id, read2, keep) { + let reads = this.#reads.get(id); + if (reads === void 0) { + reads = { count: 0, version: 0 }; + this.#reads.set(id, reads); + } + reads.count++; + const version = reads.version; + let value2; + try { + value2 = await read2(); + } finally { + if (--reads.count === 0) + this.#reads.delete(id); + } + if (value2 === void 0) + return void 0; + freeze(value2); + if (reads.version === version) + keep(value2); + return value2; + } + /** Records with a read in flight, for tests that bound memory. */ + get readsInFlight() { + return this.#reads.size; + } +}; +function scopeOf(storage) { + if (storage.publishing === void 0) + return void 0; + return (publish2, context) => storage.publishing(publish2, context); +} +var Lru = class { + #map = /* @__PURE__ */ new Map(); + #capacity; + constructor(capacity) { + this.#capacity = capacity; + } + get(key) { + const value2 = this.#map.get(key); + if (value2 === void 0) + return void 0; + this.#map.delete(key); + this.#map.set(key, value2); + return value2; + } + get size() { + return this.#map.size; + } + delete(key) { + this.#map.delete(key); + } + set(key, value2) { + this.#map.delete(key); + this.#map.set(key, value2); + if (this.#map.size > this.#capacity) + this.#map.delete(this.#map.keys().next().value); + } +}; +function freeze(value2) { + if (value2 === null || typeof value2 !== "object" || Object.isFrozen(value2)) + return value2; + for (const child of Object.values(value2)) + freeze(child); + return Object.freeze(value2); +} + +// node_modules/@earendil-works/pi-durable/dist/session/conversation-tails.js +var LATEST_CAPACITY = 4096; +var MARKER_CAPACITY = 256; +var ConversationTails = class { + /** Newest entry ID per conversation, `null` for none. */ + #latest = /* @__PURE__ */ new Map(); + /** Newest head marker per conversation, `null` for none. */ + #markers = /* @__PURE__ */ new Map(); + /** The newest entry ID visible from `conversationId`, read with `read` when not known. */ + async latest(conversationId, read2) { + const known = use(this.#latest, conversationId); + if (known !== void 0) + return known ?? void 0; + const latest = await read2(); + put(this.#latest, conversationId, latest ?? null, LATEST_CAPACITY); + return latest; + } + /** + * The newest visible head marker at or before `atOrBefore` (any when `undefined`), read with `read` when not known. + * The newest marker answers every cutoff at or after it, and no marker answers every cutoff. + */ + async marker(conversationId, atOrBefore, read2) { + const marker = use(this.#markers, conversationId); + if (marker === null) + return void 0; + if (marker !== void 0 && (atOrBefore === void 0 || atOrBefore >= marker.id)) + return marker; + const latest = this.#latest.get(conversationId); + const found = await read2(); + if (atOrBefore === void 0 || latest !== void 0 && (latest ?? 0) <= atOrBefore) { + put(this.#markers, conversationId, found === void 0 ? null : freeze2(found), MARKER_CAPACITY); + } + return found; + } + /** The newest entry ID of `conversationId` if known, without reading: `null` for none, `undefined` when unknown. */ + peek(conversationId) { + return this.#latest.get(conversationId); + } + /** At a commit's publication: apply a durable batch. */ + apply(writes) { + for (const write of writes) { + if (write.type === "conversation") { + if (write.value.parent !== void 0) + continue; + put(this.#latest, write.value.id, null, LATEST_CAPACITY); + put(this.#markers, write.value.id, null, MARKER_CAPACITY); + } else if (write.type === "entry") { + const entry2 = write.value; + put(this.#latest, entry2.conversationId, entry2.id, LATEST_CAPACITY); + if (entry2.head !== void 0) { + put(this.#markers, entry2.conversationId, freeze2(entry2), MARKER_CAPACITY); + } + } + } + } + /** How many conversations have their newest entry kept, for tests that bound memory. */ + get size() { + return this.#latest.size; + } + /** How many conversations have their newest head marker kept, for tests that bound memory. */ + get markers() { + return this.#markers.size; + } +}; +function use(map2, key) { + const value2 = map2.get(key); + if (value2 === void 0) + return void 0; + map2.delete(key); + map2.set(key, value2); + return value2; +} +function put(map2, key, value2, capacity) { + map2.delete(key); + map2.set(key, value2); + if (map2.size > capacity) + map2.delete(map2.keys().next().value); +} +function freeze2(value2) { + if (value2 === null || typeof value2 !== "object" || Object.isFrozen(value2)) + return value2; + for (const child of Object.values(value2)) + freeze2(child); + return Object.freeze(value2); +} + +// node_modules/@earendil-works/pi-durable/dist/session/keys.js +function taskKey(id) { + return `task:${id}`; +} +function conversationKey(id) { + return `conv:${id}`; +} +function entryKey(id) { + return `entry:${id}`; +} +function appendKey(id) { + return `entry-append:${id}`; +} +function submissionKey(id) { + return `sub:${id}`; +} +function requestKey(conversationId, requestId) { + return `req:${conversationId}:${requestId}`; +} +function documentKey(addressId2) { + return `doc:${addressId2}`; +} +function incarnationKey(id) { + return `docid:${id}`; +} +function scopeKey(scope) { + switch (scope.kind) { + case "session": + return "docscope:session"; + case "conversation": + return `docscope:c${scope.conversationId}`; + case "task": + return `docscope:t${scope.taskId}`; + } +} +function ownedKey(node) { + return `owned:${node}`; +} +var ROOTS_KEY = "roots"; +var INTENT_KEY = "intent"; +var ALL_KEYS = "*"; +function tableKeys(writes, keys) { + for (const write of writes) { + switch (write.type) { + case "conversation": + keys.push(conversationKey(write.value.id)); + break; + case "entry": + keys.push(entryKey(write.value.id), appendKey(write.value.conversationId)); + break; + case "task": + keys.push(taskKey(write.value.id)); + break; + case "submission": + keys.push(submissionKey(write.value.id)); + if (write.value.requestId !== void 0) { + keys.push(requestKey(write.value.conversationId, write.value.requestId)); + } + break; + } + } +} +var GatedStorage = class { + #storage; + #touch; + constructor(storage, touch) { + this.#storage = storage; + this.#touch = touch; + } + commit(batches, receive, context) { + return this.#storage.commit(batches, receive, context); + } + mintId() { + return this.#storage.mintId(); + } + conversation(id, context) { + return this.#after(conversationKey(id), () => this.#storage.conversation(id, context)); + } + scanConversations(query, limit, cursor, context) { + return this.#after(ALL_KEYS, () => this.#storage.scanConversations(query, limit, cursor, context)); + } + entry(first, second, third) { + if (third === void 0) { + const id2 = first; + return this.#after(entryKey(id2), () => this.#storage.entry(id2, second)); + } + const conversationId = first; + const id = second; + return this.#after(conversationKey(conversationId), () => this.#after(entryKey(id), () => this.#storage.entry(conversationId, id, third))); + } + findLatestHeadMarker(conversationId, atOrBeforeEntryId, context) { + return this.#afterEntries(conversationId, () => this.#storage.findLatestHeadMarker(conversationId, atOrBeforeEntryId, context)); + } + scanEntries(query, limit, cursor, context) { + return this.#afterEntries(query.conversationId, () => this.#storage.scanEntries(query, limit, cursor, context)); + } + task(id, context) { + return this.#after(taskKey(id), () => this.#storage.task(id, context)); + } + scanTasks(query, limit, cursor, context) { + return this.#after(ALL_KEYS, () => this.#storage.scanTasks(query, limit, cursor, context)); + } + submission(id, context) { + return this.#after(submissionKey(id), () => this.#storage.submission(id, context)); + } + scanSubmissions(query, limit, cursor, context) { + return this.#after(ALL_KEYS, () => this.#storage.scanSubmissions(query, limit, cursor, context)); + } + submissionByRequest(conversationId, requestId, context) { + return this.#after(requestKey(conversationId, requestId), () => this.#storage.submissionByRequest(conversationId, requestId, context)); + } + findDocument(address, at, context) { + return this.#after(documentKey(addressId(address)), () => this.#storage.findDocument(address, at, context)); + } + document(id, at, context) { + return this.#after(incarnationKey(id), () => this.#storage.document(id, at, context)); + } + scanDocuments(query, limit, cursor, context) { + return this.#after(scopeKey(query.scope), () => this.#storage.scanDocuments(query, limit, cursor, context)); + } + close(context) { + return this.#storage.close(context); + } + /** + * Run a read of a conversation's entries once neither its appends nor its creation has an unpublished writer: Storage + * refuses the read of a conversation it does not have yet. + */ + #afterEntries(conversationId, read2) { + return this.#after(conversationKey(conversationId), () => this.#after(appendKey(conversationId), read2)); + } + /** Run `read` once `key` has no unpublished writer. */ + #after(key, read2) { + const gate = this.#touch(key); + return gate === void 0 ? read2() : gate.then(read2); + } +}; + +// node_modules/@earendil-works/pi-durable/dist/types.js +var ROOT_CONVERSATION_ID = 1; + +// node_modules/@earendil-works/pi-durable/dist/session/forks.js +var SCAN_PAGE_SIZE = 256; +async function prepareForkDocumentCopies(storage, parentConversationId, at, childConversationId, context) { + const entry2 = await storage.entry(parentConversationId, at, context); + if (entry2 === void 0) + throw new Error(`Entry ${at} is not visible from conversation ${parentConversationId}`); + const copies = []; + const copiedAddresses = /* @__PURE__ */ new Set(); + await collectCopies(storage, { kind: "conversation", conversationId: entry2.entry.conversationId }, entry2.commitSeq, "asOf", childConversationId, copies, copiedAddresses, context); + await collectCopies(storage, { kind: "conversation", conversationId: parentConversationId }, "current", "current", childConversationId, copies, copiedAddresses, context); + return copies; +} +async function collectCopies(storage, scope, at, policy, childConversationId, copies, copiedAddresses, context) { + let cursor; + do { + const page2 = await storage.scanDocuments({ scope, at }, SCAN_PAGE_SIZE, cursor, context); + for (const source of page2.items) { + if (source.scope.kind !== "conversation" || source.fork !== policy) + continue; + const id = await storage.mintId(); + const identity = { + id, + kind: source.kind, + ...source.key === void 0 ? {} : { key: source.key }, + scope: { kind: "conversation", conversationId: childConversationId } + }; + const record = source.history === "latest" ? { ...identity, history: "latest", fork: source.fork } : { ...identity, history: "rewindable", fork: source.fork }; + const copyAddress = addressId(record); + if (copiedAddresses.has(copyAddress)) { + const member = record.key === void 0 ? record.kind : `${record.kind}/${record.key}`; + throw new Error(`Fork selects multiple source documents for ${member}`); + } + copiedAddresses.add(copyAddress); + copies.push({ record, source: { id: source.id, at } }); + } + cursor = page2.next; + } while (cursor !== void 0); +} + +// node_modules/@earendil-works/pi-durable/dist/session/transaction.js +function applySubmissionChange(current, change) { + if (current.status === "done" || current.status === "unanswered") + return current; + if (change.status === "placed") { + if (current.status !== "queued") + throw new Error(`Submission ${current.id} is not queued`); + const status = current.type === "input" ? "placed" : "done"; + return { ...current, status, entry: change.entry }; + } + if (change.status === "done" && current.status !== "placed") { + throw new Error(`Submission ${current.id} is not a placed input or continue`); + } + return { ...current, ...change }; +} +var INTERNAL_SCAN_PAGE_SIZE = 256; +var EMPTY_OPERATIONS = []; +var TABLE_JSON_COPY_OPTIONS = { omitUndefinedProperties: true }; +var Transaction = class { + #host; + #context; + #scope; + #pendingOperations = /* @__PURE__ */ new Set(); + #sealed = false; + #hasTableWrite = false; + /** Atomic batch; conversation and entry writes stage eagerly, while task and document writes assemble later. */ + #writes = []; + #createdConversationIds = /* @__PURE__ */ new Set(); + #forkSourceConversationIds = /* @__PURE__ */ new Set(); + #forkSourceDocumentIds = /* @__PURE__ */ new Set(); + /** One entry per task touched by a public read, candidate write, or document-owner lookup. */ + #tasksById = /* @__PURE__ */ new Map(); + /** Submissions created by this transaction, by ID. */ + #submissions = /* @__PURE__ */ new Map(); + /** Submission settlements and placements in staging order; resolved against the latest candidate record during assembly. */ + #submissionChanges = []; + /** Write and publication plans of every staged incarnation, built during assembly. */ + #plans = []; + /** Every document acquisition or retirement marker in staging order. */ + #documents = []; + /** Latest transaction-local incarnation or retirement marker at each logical address. */ + #latestDocumentByAddress = /* @__PURE__ */ new Map(); + constructor(host, context, scope = {}) { + this.#host = host; + this.#context = context; + this.#scope = scope; + } + /** Internal: the context of the commit this transaction belongs to. */ + get context() { + return this.#context; + } + /** Internal: whether a table write was staged, after which table reads throw `ReadAfterWrite`. */ + get hasTableWrite() { + return this.#hasTableWrite; + } + /** + * Internal: whether `tx` is a transaction of a commit of the Session behind `host`. A brand check, so a proxy or + * another wrapper of a transaction is not one. + */ + static ownedBy(tx, host) { + return #host in tx && tx.#host === host; + } + // ─── Table reads ──────────────────────────────────────────────────────── + conversation(id) { + return this.#read("conversation", () => this.#host.storage.conversation(id, this.#context)); + } + entry(first, second) { + const kind = typeof first === "number" ? void 0 : first.kind; + const id = typeof first === "number" ? first : second; + return this.#read("entry", async () => { + const entry2 = (await this.#host.storage.entry(id, this.#context))?.entry; + return kind === void 0 || entry2?.kind === kind ? entry2 : void 0; + }); + } + task(id) { + return this.#read("task", () => this.#committedTask(id)); + } + scanConversations(query, limit, cursor) { + return this.#read("scanConversations", () => this.#host.storage.scanConversations(query, limit, cursor, this.#context)); + } + scanEntries(query, limit, cursor) { + return this.#read("scanEntries", () => this.#host.storage.scanEntries(query, limit, cursor, this.#context)); + } + latestHeadMarker(conversationId) { + return this.#read("latestHeadMarker", () => this.#host.latestHeadMarker(conversationId, this.#context)); + } + latestEntryId(conversationId) { + return this.#read("latestEntryId", () => this.#host.latestEntryId(conversationId, this.#context)); + } + scanTasks(query, limit, cursor) { + return this.#read("scanTasks", () => this.#host.storage.scanTasks(query, limit, cursor, this.#context)); + } + /** Internal: committed submission record. */ + submission(id) { + return this.#read("submission", () => this.#host.storage.submission(id, this.#context)); + } + /** Committed submission with a conversation-scoped request ID. */ + submissionByRequest(conversationId, requestId) { + return this.#read("submissionByRequest", () => this.#host.storage.submissionByRequest(conversationId, requestId, this.#context)); + } + // ─── Table writes ─────────────────────────────────────────────────────── + createConversation(options) { + return this.#write(() => this.#stageConversation(void 0, options.ownership)); + } + /** Internal final-form bootstrap path for the reserved root identity. */ + createRootConversation() { + return this.#write(() => this.#stageConversation(void 0, { kind: "ownerless" }, ROOT_CONVERSATION_ID)); + } + forkConversation(parentConversationId, at, options) { + return this.#write(() => this.#stageConversation({ conversationId: parentConversationId, at }, options.ownership)); + } + async #stageConversation(parent, ownership, reservedId) { + const ownerTaskId = ownership.kind === "task" ? ownership.taskId : void 0; + const id = reservedId ?? await this.#host.storage.mintId(); + this.#assertOpen(); + let owner; + if (ownerTaskId !== void 0) { + const task = await this.#currentTask(ownerTaskId); + this.#assertOpen(); + if (task === void 0) + throw new Error(`Conversation owner task ${ownerTaskId} does not exist`); + owner = { conversationId: task.conversationId, taskId: ownerTaskId }; + } + const record = { + id, + ...parent === void 0 ? {} : { parent }, + ...owner === void 0 ? {} : { owner } + }; + const copies = parent === void 0 ? [] : await prepareForkDocumentCopies(this.#host.storage, parent.conversationId, parent.at, id, this.#context); + this.#assertOpen(); + for (const copy2 of copies) { + this.#forkSourceDocumentIds.add(copy2.source.id); + const entry2 = { + addressId: addressId(copy2.record), + address: copy2.record, + target: { kind: "fork-copy", ...copy2 }, + retireOnCommit: false + }; + this.#documents.push(entry2); + this.#latestDocumentByAddress.set(entry2.addressId, entry2); + } + if (parent !== void 0) + this.#forkSourceConversationIds.add(parent.conversationId); + this.#createdConversationIds.add(id); + this.#writes.push({ type: "conversation", value: record }); + await this.#host.conversationCreated(this, record); + this.#assertOpen(); + return record; + } + beforeInput(conversationId, input) { + return this.#read("beforeInput", () => this.#host.beforeInput(this, conversationId, input, this.#context)); + } + appendEntry(first, second, third) { + if (typeof first === "number") + return this.#appendEntry(first, second); + return this.#appendEntry(second, { ...third, kind: first.kind }); + } + #appendEntry(conversationId, value2) { + return this.#write(async () => { + await this.#requireConversation(conversationId); + this.#assertOpen(); + const id = await this.#host.storage.mintId(); + this.#assertOpen(); + const { head, ...rest } = value2; + const record = copyJson({ + ...rest, + id, + conversationId, + head: head === "self" ? id : head, + byTaskId: this.#scope.taskId + }, TABLE_JSON_COPY_OPTIONS); + this.#writes.push({ type: "entry", value: record }); + return record; + }); + } + createTask(task, input, options) { + return this.#write(async () => { + if (options.submissionIds !== void 0) { + let trace; + let links; + for (let index = 0; index < options.submissionIds.length; index++) { + const id2 = options.submissionIds[index]; + const submission2 = this.#submissions.get(id2) ?? await this.#host.storage.submission(id2, this.#context); + if (index === 0) + trace = submission2?.trace ?? "root"; + else if (submission2?.trace !== void 0) { + links ??= []; + links.push(submission2.trace); + } + } + options = { ...options, trace, links }; + } + const ownership = options.ownership; + let owner; + if (ownership.kind === "task") { + owner = await this.#currentTask(ownership.taskId); + this.#assertOpen(); + if (owner === void 0) + throw new Error(`Task owner ${ownership.taskId} does not exist`); + if (options.background === true) + throw new TypeError("A child task cannot be background"); + if (options.conversationId !== void 0 && options.conversationId !== owner.conversationId) { + throw new Error(`A child task lives in its owner's conversation ${owner.conversationId}`); + } + } + const conversationId = owner?.conversationId ?? options.conversationId ?? this.#scope.conversationId; + if (conversationId === void 0) + throw new TypeError("Tx.createTask() requires options.conversationId"); + await this.#requireConversation(conversationId); + this.#assertOpen(); + const definition = task.definition; + const checkpoint = definition.initial(input); + const id = await this.#host.storage.mintId(); + this.#assertOpen(); + const record = copyJson({ + id, + conversationId, + kind: definition.name, + version: definition.version, + input, + trace: options.trace === "root" ? void 0 : options.trace ?? this.#context.telemetry.propagation?.inject(this.#context.telemetry), + links: options.links, + ...owner === void 0 ? {} : { owner: owner.id }, + background: options.background ?? false, + abortRequested: false, + ...options.abandonOnRestart === true ? { abandonOnRestart: true } : {}, + state: { status: "pending", checkpoint } + }, TABLE_JSON_COPY_OPTIONS); + this.#tasksById.set(id, { write: { kind: "create", record } }); + return id; + }); + } + /** Create a raw submission record with a fresh ID; no admission rules apply. */ + createSubmission(create) { + return this.#write(async () => { + await this.#requireConversation(create.conversationId); + this.#assertOpen(); + const id = await this.#host.storage.mintId(); + this.#assertOpen(); + const record = copyJson({ ...create, id }, TABLE_JSON_COPY_OPTIONS); + this.#submissions.set(id, record); + return record; + }); + } + /** + * Settle a submission. Resolved during assembly against the transaction's latest candidate record, falling back to + * committed state, so it is not a caller table read and works after the first table write. + */ + settleSubmission(id, settlement) { + this.#assertOpen(); + this.#hasTableWrite = true; + this.#submissionChanges.push({ id, change: copyJson(settlement) }); + } + /** Place a queued submission at `entry`; resolved during assembly like `settleSubmission()`. */ + placeSubmission(id, entry2) { + this.#assertOpen(); + this.#hasTableWrite = true; + this.#submissionChanges.push({ id, change: { status: "placed", entry: entry2 } }); + } + /** Internal: replace one task record completely. Tasks change their own state through their runtime. */ + setTask(value2) { + this.#assertOpen(); + this.#hasTableWrite = true; + const task = this.#taskEntry(value2.id); + const candidate = task.write?.record; + if (candidate?.state.status === "terminal") { + throw new Error(`Task ${value2.id} already has a terminal candidate`); + } + if (candidate !== void 0 && candidate.conversationId !== value2.conversationId) { + throw new Error(`Task ${value2.id} cannot change conversations`); + } + task.write = { + kind: task.write?.kind === "create" ? "create" : "replace", + record: copyJson(this.#stampTimes(value2, candidate), TABLE_JSON_COPY_OPTIONS) + }; + } + /** Internal: candidate records of the tasks this transaction created or replaced so far. */ + stagedTasks() { + const records2 = []; + for (const task of this.#tasksById.values()) + if (task.write !== void 0) + records2.push(task.write.record); + return records2; + } + /** Internal: IDs of the tasks this transaction creates. */ + createdTaskIds() { + const ids = []; + for (const [id, task] of this.#tasksById) + if (task.write?.kind === "create") + ids.push(id); + return ids; + } + /** Internal: conversations this transaction created or forked so far. */ + stagedConversations() { + const records2 = []; + for (const write of this.#writes) + if (write.type === "conversation") + records2.push(write.value); + return records2; + } + doc(token, ...args) { + try { + this.#assertOpen(); + const definition = token.definition; + const resolved = resolveAddress(definition, args); + this.#assertTaskDocumentsOpen(resolved); + const latest = this.#latestDocumentByAddress.get(resolved.id); + if (latest !== void 0 && !latest.retireOnCommit) { + if (latest.draftPromise !== void 0) + return latest.draftPromise; + if (latest.target?.kind === "fork-copy") { + latest.draftPromise = this.#track(this.#acquireForkCopy(latest, definition, latest.target)); + return latest.draftPromise; + } + } + const seed = definition.family === true ? copyJson(args[resolved.nextArgument]) : void 0; + const docEntry = { + addressId: resolved.id, + address: resolved.address, + definition, + retireOnCommit: false + }; + this.#documents.push(docEntry); + this.#latestDocumentByAddress.set(docEntry.addressId, docEntry); + docEntry.draftPromise = this.#track(this.#acquire(docEntry, seed, latest?.retireOnCommit === true)); + return docEntry.draftPromise; + } catch (error) { + return Promise.reject(error); + } + } + retireDoc(token, ...args) { + try { + this.#assertOpen(); + const definition = token.definition; + const resolved = resolveAddress(definition, args); + const latest = this.#latestDocumentByAddress.get(resolved.id); + if (latest?.retireOnCommit) + return Promise.resolve(); + if (latest?.target?.kind === "fork-copy") { + checkRecordScope(definition, latest.target.record); + latest.retireOnCommit = true; + return Promise.resolve(); + } + if (latest?.draftPromise !== void 0) { + latest.retireOnCommit = true; + return this.#track(latest.draftPromise.then(() => void 0)); + } + const entry2 = { + addressId: resolved.id, + address: resolved.address, + definition, + retireOnCommit: true + }; + this.#documents.push(entry2); + this.#latestDocumentByAddress.set(entry2.addressId, entry2); + return this.#track(this.#findRetirement(entry2)); + } catch (error) { + return Promise.reject(error); + } + } + async #acquire(entry2, seed, skipLoad) { + const definition = entry2.definition; + const loaded = skipLoad ? void 0 : await this.#host.load(definition, entry2.addressId, entry2.address, this.#context); + this.#assertOpen(); + if (loaded !== void 0) { + checkRecordScope(definition, loaded.record); + checkRecordVersion(definition, loaded.record, loaded.storedVersion); + entry2.target = { kind: "loaded", document: loaded }; + entry2.change = loaded.tracker.beginChange(); + return entry2.change.state; + } + const scope = entry2.address.scope; + if (scope.kind === "conversation") + await this.#requireConversation(scope.conversationId); + if (scope.kind === "task") { + const task = await this.#currentTask(scope.taskId); + if (task === void 0) + throw new Error(`Task ${scope.taskId} does not exist`); + if (task.state.status === "terminal") + throw new Error(`Task ${scope.taskId} is terminal`); + } + this.#assertOpen(); + const value2 = copyJson(definition.family === true ? definition.initial(seed) : definition.initial()); + const id = await this.#host.storage.mintId(); + this.#assertOpen(); + const tracker = track(value2); + entry2.target = { + kind: "created", + record: documentCreate(definition, entry2.address, id), + version: definition.version, + tracker + }; + entry2.change = tracker.beginChange(); + return entry2.change.state; + } + async #acquireForkCopy(entry2, definition, target) { + const stored = await this.#host.storage.document(target.source.id, target.source.at, this.#context); + this.#assertOpen(); + if (stored === void 0) { + throw new Error(`Fork source document ${target.source.id} cannot be read`); + } + if (stored.record.scope.kind !== "conversation" || stored.record.kind !== target.record.kind || stored.record.key !== target.record.key || stored.record.history !== target.record.history || stored.record.fork !== target.record.fork) { + throw new Error(`Fork source document ${target.source.id} does not match the copied record`); + } + const value2 = materializeDocumentValue(definition, target.record, stored.version, stored.value); + const tracker = track(value2); + entry2.definition = definition; + entry2.target = { kind: "created", record: target.record, version: definition.version, tracker }; + entry2.change = tracker.beginChange(); + return entry2.change.state; + } + async #findRetirement(entry2) { + const gate = this.#host.touch(documentKey(entry2.addressId)); + if (gate !== void 0) + await gate; + const record = this.#host.cached(entry2.addressId)?.record ?? await this.#host.storage.findDocument(entry2.address, "current", this.#context); + this.#assertOpen(); + if (record === void 0) + return; + checkRecordScope(entry2.definition, record); + entry2.target = { kind: "retire-only", record }; + } + // ─── Settlement ───────────────────────────────────────────────────────── + /** Seal after callback failure: abort every change and observe every pending operation. */ + async settleFailure() { + this.#sealed = true; + this.#abortChanges(); + await Promise.allSettled(this.#pendingOperations); + this.#context = BACKGROUND_CONTEXT; + } + /** + * Seal after callback success, prepare every open change, and assemble the atomic batch. + * Any failure aborts every change before Storage admission. + */ + async settleSuccess() { + this.#sealed = true; + if (this.#pendingOperations.size > 0) { + this.#abortChanges(); + await Promise.allSettled(this.#pendingOperations); + this.#context = BACKGROUND_CONTEXT; + throw new Error("Session commit callback settled before its pending Tx operations"); + } + try { + for (const document of this.#documents) { + if (document.change !== void 0) + document.prepared = document.change.prepare(); + } + return await this.#assemble(); + } catch (error) { + this.#abortChanges(); + throw error; + } finally { + this.#context = BACKGROUND_CONTEXT; + } + } + /** Abort every prepared change after Storage failure or when no write is required. */ + discard() { + this.#abortChanges(); + } + /** + * Internal: what a deferred commit holds in memory instead of storing (`CommitPersistence`), each changed incarnation + * with its operations and whether they selected a base; `undefined` when the commit must be stored. It may be held + * only when every write changes an existing `latest`, `initial`-fork conversation document at its stored version. + */ + holdable() { + if (this.#writes.some((write) => write.type !== "document.change")) + return void 0; + const held = []; + for (const plan of this.#plans) { + const { content, change } = plan; + if (content === void 0 && !plan.retire) + continue; + if (plan.retire || change?.loaded === void 0 || content?.type !== "document.change") + return void 0; + const { loaded, prepared, version } = change; + const record = loaded.record; + if (record.scope.kind !== "conversation" || record.history !== "latest" || record.fork !== "initial") { + return void 0; + } + if (loaded.storedVersion !== version) + return void 0; + held.push({ loaded, ops: prepared.ops, base: content.content.kind === "base" }); + } + return held; + } + /** + * Internal: adopt a held commit (`holdable()`) in memory only. Trackers advance by pointer swap; stored versions and + * delta counts stay, as nothing was stored. Returns its publication for the Session's own observers. + */ + adoptHeld() { + const publications = []; + for (const plan of this.#plans) { + const { tracker, prepared, version, loaded } = plan.change; + if (prepared.ops.length === 0) { + prepared.abort(); + continue; + } + tracker.adopt(prepared); + const record = loaded.record; + const conversationId = plan.conversationId; + publications.push({ + type: "document", + kind: "change", + record, + conversationId, + version, + value: prepared.value, + ops: prepared.ops + }); + } + return publications; + } + /** + * Internal: store the held changes of every incarnation this commit reads or writes in front of its own, so a stored + * document never misses an operation its later ones build on. Called right before Storage admission, after every + * fallible step, because it empties the holds: a failure earlier keeps them. Returns the holds it took. + */ + takeHeld(held) { + const taken = []; + for (const plan of this.#plans) { + if (!("createdAt" in plan.record)) + continue; + const hold = held(plan.record.id); + if (hold === void 0) + continue; + taken.push(hold); + const own = plan.content?.type === "document.change" ? plan.content : void 0; + const change = plan.change; + const value2 = change?.prepared.value ?? hold.loaded.tracker.value; + const stored = storeHeld(hold, change?.prepared.ops ?? EMPTY_OPERATIONS, value2, hold.loaded.valueVersion); + const content = own?.content.kind === "base" ? own.content : stored.content; + const write = { type: "document.change", id: plan.record.id, content }; + if (own !== void 0) + this.#writes[this.#writes.indexOf(own)] = write; + else + this.#insertBeforeRetirement(write); + if (!plan.retire) + plan.held = { ops: stored.ops, observed: own !== void 0 }; + plan.content = write; + if (plan.record.scope.kind === "conversation") + plan.conversationId = plan.record.scope.conversationId; + } + return taken; + } + /** + * Internal: the keys this commit writes, once settled (and after `takeHeld()`), for the Session's unpublished write + * set: its table records, and the address and incarnation of every document it creates, changes, or retires, with + * the scope of every incarnation it creates or retires. + */ + writeKeys() { + const keys = []; + tableKeys(this.#writes, keys); + for (const plan of this.#plans) { + if (plan.content === void 0 && !plan.retire) + continue; + keys.push(documentKey(plan.addressId), incarnationKey(plan.record.id)); + if (plan.retire || !("createdAt" in plan.record)) + keys.push(scopeKey(plan.record.scope)); + } + return keys; + } + /** Stage a content write for an incarnation, before its retirement when it retires. */ + #insertBeforeRetirement(write) { + const at = this.#writes.findIndex((candidate) => candidate.type === "document.retire" && candidate.id === write.id); + if (at < 0) + this.#writes.push(write); + else + this.#writes.splice(at, 0, write); + } + /** Adopt every prepared change by pointer swap after Storage success and describe the publication. */ + adopt(seq) { + const publications = []; + let stored; + for (const plan of this.#plans) { + const committed = "createdAt" in plan.record; + let record = committed ? plan.record : { ...plan.record, createdAt: seq }; + if (plan.retire) + record = { ...record, retiredAt: seq }; + const change = plan.change; + if (change !== void 0) { + const { tracker, prepared, loaded, version } = change; + if (loaded === void 0 ? !plan.retire : prepared.ops.length > 0) + tracker.adopt(prepared); + else + prepared.abort(); + if (loaded !== void 0) { + if (loaded.storedVersion < version) + loaded.storedVersion = version; + if (plan.content?.type === "document.change") { + if (plan.content.content.kind === "base") + loaded.deltasSinceBase = 0; + else + loaded.deltasSinceBase++; + } + } else if (!plan.retire) { + this.#host.install({ + addressId: plan.addressId, + record, + storedVersion: version, + valueVersion: version, + deltasSinceBase: 0, + tracker + }); + } + } + if (plan.retire && committed) + this.#host.evict(plan.addressId, record.id); + const publication = describe(plan, record); + if (publication === void 0) + continue; + const held = plan.held; + if (held === void 0) { + publications.push(publication); + stored?.push(publication); + continue; + } + stored ??= [...publications]; + if (held.observed) + publications.push(publication); + stored.push(publication.type === "document" && publication.kind === "change" ? { ...publication, ops: held.ops } : publication); + } + return { observed: publications, stored: stored ?? publications }; + } + async #assemble() { + const storage = this.#host.storage; + const plans = this.#plans; + for (const document of this.#documents) { + const plan = planDocument(document); + if (plan !== void 0) + plans.push(plan); + } + this.#rejectForkSourceWrites(plans); + await this.#validateOwners(); + for (const [id, task] of this.#tasksById) { + if (task.write?.kind !== "replace") + continue; + const committed = await this.#committedTask(id); + if (committed === void 0) + throw new Error(`Task ${id} does not exist`); + if (committed.state.status === "terminal") + throw new Error(`Task ${id} is already terminal`); + if (committed.conversationId !== task.write.record.conversationId) { + throw new Error(`Task ${id} cannot change conversations`); + } + } + const terminalTaskIds = /* @__PURE__ */ new Set(); + for (const task of this.#tasksById.values()) { + if (task.write?.record.state.status === "terminal") + terminalTaskIds.add(task.write.record.id); + } + if (terminalTaskIds.size > 0) { + const retiring = /* @__PURE__ */ new Set(); + for (const plan of plans) { + const scope = plan.record.scope; + if (scope.kind !== "task" || !terminalTaskIds.has(scope.taskId)) + continue; + plan.retire = true; + retiring.add(plan.record.id); + } + const retire = (record) => { + if (retiring.has(record.id)) + return; + plans.push({ addressId: addressId(record), record, retire: true }); + retiring.add(record.id); + }; + for (const taskId of terminalTaskIds) { + if (this.#tasksById.get(taskId)?.write?.kind === "create") + continue; + const gate = this.#host.touch(scopeKey({ kind: "task", taskId })); + if (gate !== void 0) + await gate; + const known = this.#host.taskDocuments(taskId); + if (known !== void 0) { + for (const record of known) + retire(record); + continue; + } + let cursor; + do { + const page2 = await storage.scanDocuments({ scope: { kind: "task", taskId }, at: "current" }, INTERNAL_SCAN_PAGE_SIZE, cursor, this.#context); + for (const record of page2.items) + retire(record); + cursor = page2.next; + } while (cursor !== void 0); + } + } + for (const plan of plans) { + if (!publishes(plan)) + continue; + const scope = plan.record.scope; + if (scope.kind === "conversation") + plan.conversationId = scope.conversationId; + if (scope.kind !== "task") + continue; + const task = this.#taskEntry(scope.taskId); + if (task.publicationConversationId === void 0) { + const current = await this.#currentTask(scope.taskId); + if (current !== void 0) + task.publicationConversationId = current.conversationId; + } + plan.conversationId = task.publicationConversationId; + } + for (const { id, change } of this.#submissionChanges) { + const current = this.#submissions.get(id) ?? await storage.submission(id, this.#context); + if (current === void 0) + throw new Error(`Submission ${id} does not exist`); + const next = applySubmissionChange(current, change); + if (next !== current) + this.#submissions.set(id, next); + } + const writes = this.#writes; + for (const value2 of this.#submissions.values()) + writes.push({ type: "submission", value: value2 }); + for (const task of this.#tasksById.values()) { + if (task.write !== void 0) + writes.push({ type: "task", value: task.write.record }); + } + for (const plan of plans) { + const change = plan.change; + if (plan.content?.type === "document.change" && plan.content.content.kind === "delta" && change?.loaded) { + const { prepared, version } = change; + const info = { deltasSinceBase: change.loaded.deltasSinceBase }; + if (change.definition.checkpointWhen(prepared.value, prepared.ops, info)) { + plan.content = { ...plan.content, content: { version, kind: "base", value: prepared.value } }; + } + } + if (plan.content !== void 0) + writes.push(plan.content); + if (plan.retire) + writes.push({ type: "document.retire", id: plan.record.id }); + } + return writes; + } + // ─── Helpers ──────────────────────────────────────────────────────────── + /** + * New owned work needs a live owner, judged on the owner's final candidate: not `completing`, terminal, or + * abort-marked. A task therefore cannot create owned work in the commit that finishes it (spec §5.5). + */ + async #validateOwners() { + const owners = []; + for (const write of this.#writes) { + if (write.type !== "conversation" || write.value.owner === void 0) + continue; + owners.push({ what: "Conversation owner task", taskId: write.value.owner.taskId }); + } + for (const task of this.#tasksById.values()) { + const record = task.write?.kind === "create" ? task.write.record : void 0; + if (record?.owner !== void 0) + owners.push({ what: "Task owner", taskId: record.owner }); + } + for (const { what, taskId } of owners) { + const task = await this.#currentTask(taskId); + if (task === void 0) + throw new Error(`${what} ${taskId} does not exist`); + if (task.state.status === "terminal" || task.state.status === "completing") { + throw new Error(`${what} ${taskId} is ${task.state.status}`); + } + if (task.abortRequested) + throw new Error(`${what} ${taskId} is abort-marked`); + } + } + #rejectForkSourceWrites(plans) { + for (const plan of plans) { + if (plan.content === void 0 && !plan.retire) + continue; + const record = plan.record; + if (this.#forkSourceDocumentIds.has(record.id)) { + throw new Error(`Cannot change fork source document ${record.id} in the fork transaction`); + } + const scope = plan.record.scope; + if (scope.kind === "conversation" && this.#forkSourceConversationIds.has(scope.conversationId) && record.scope.kind === "conversation" && record.fork === "current") { + throw new Error(`Cannot fork conversation ${scope.conversationId} while changing its current-policy documents`); + } + } + } + #abortChanges() { + for (const document of this.#documents) + document.change?.abort(); + } + #assertOpen() { + if (this.#sealed) + throw new Error("Transaction has settled"); + } + #assertTaskDocumentsOpen(resolved) { + const scope = resolved.address.scope; + if (scope.kind === "task" && this.#tasksById.get(scope.taskId)?.write?.record.state.status === "terminal") { + throw new Error(`Task ${scope.taskId} is terminal`); + } + } + /** Register an operation so callback settlement can reject and drain it. */ + #track(operation) { + this.#pendingOperations.add(operation); + const settle3 = () => { + this.#pendingOperations.delete(operation); + }; + operation.then(settle3, settle3); + return operation; + } + #read(method, read2) { + try { + this.#assertOpen(); + if (this.#hasTableWrite) + throw new ReadAfterWrite(`Tx.${method}()`); + return this.#track(read2()); + } catch (error) { + return Promise.reject(error); + } + } + #write(write) { + try { + this.#assertOpen(); + this.#hasTableWrite = true; + return this.#track(write()); + } catch (error) { + return Promise.reject(error); + } + } + async #requireConversation(id) { + if (this.#createdConversationIds.has(id)) + return; + if (await this.#host.storage.conversation(id, this.#context) === void 0) { + throw new Error(`Conversation ${id} does not exist`); + } + } + /** + * Lifecycle times: `startedAt` on the first change to `running`, `endedAt` on the change to `terminal`. Once set, + * they carry over from the replaced record; records written before they existed lack them. + */ + #stampTimes(value2, candidate) { + const status = value2.state.status; + const now = (stamps) => stamps ? this.#host.now() : void 0; + const startedAt = candidate?.startedAt ?? value2.startedAt ?? now(status === "running"); + const endedAt = candidate?.endedAt ?? value2.endedAt ?? now(status === "terminal"); + if (startedAt === value2.startedAt && endedAt === value2.endedAt) + return value2; + return { + ...value2, + ...startedAt === void 0 ? {} : { startedAt }, + ...endedAt === void 0 ? {} : { endedAt } + }; + } + #taskEntry(id) { + let task = this.#tasksById.get(id); + if (task === void 0) { + task = {}; + this.#tasksById.set(id, task); + } + return task; + } + /** Latest candidate task record, falling back to committed state; not a caller table read. */ + async #currentTask(id) { + return this.#tasksById.get(id)?.write?.record ?? await this.#committedTask(id); + } + #committedTask(id) { + const task = this.#taskEntry(id); + task.committedRead ??= this.#host.storage.task(id, this.#context); + return task.committedRead; + } +}; +function planDocument(document) { + const target = document.target; + if (target === void 0) + return void 0; + const plan = { addressId: document.addressId, retire: document.retireOnCommit }; + switch (target.kind) { + case "created": { + const prepared = document.prepared; + const { record, version, tracker } = target; + const content = { version, kind: "base", value: prepared.value }; + return { + ...plan, + record, + content: { type: "document.create", record, content }, + change: { tracker, prepared, version } + }; + } + case "fork-copy": + return { + ...plan, + record: target.record, + content: { type: "document.copy", record: target.record, source: target.source } + }; + case "retire-only": + return { ...plan, record: target.record }; + case "loaded": { + const loaded = target.document; + const definition = document.definition; + const prepared = document.prepared; + const version = definition.version; + const id = loaded.record.id; + const content = loaded.storedVersion < version ? { type: "document.change", id, content: { version, kind: "base", value: prepared.value } } : prepared.ops.length > 0 ? { type: "document.change", id, content: { version, kind: "delta", ops: prepared.ops } } : void 0; + const change = { + tracker: loaded.tracker, + prepared, + version, + loaded, + definition + }; + return { ...plan, record: loaded.record, change, ...content === void 0 ? {} : { content } }; + } + } +} +function describe(plan, record) { + const conversationId = plan.conversationId; + if (plan.retire) + return { type: "document", kind: "retire", record, conversationId }; + if (plan.content?.type === "document.copy") { + return { type: "document.copy", record, conversationId, source: plan.content.source }; + } + const change = plan.change; + if (change === void 0 || !publishes(plan)) + return void 0; + const ops3 = change.loaded === void 0 ? EMPTY_OPERATIONS : change.prepared.ops; + return { + type: "document", + kind: "change", + record, + conversationId, + version: change.version, + value: change.prepared.value, + ops: ops3 + }; +} +function storeHeld(hold, ops3, value2, version) { + hold.pending.push(ops3); + const taken = hold.pending.take(); + if (taken.kind === "replace") + return { content: { version, kind: "base", value: value2 }, ops: [["r", value2]] }; + if (hold.base) + return { content: { version, kind: "base", value: value2 }, ops: taken.ops }; + return { content: { version, kind: "delta", ops: taken.ops }, ops: taken.ops }; +} +function publishes(plan) { + return plan.retire || plan.change?.loaded === void 0 || plan.content !== void 0; +} + +// node_modules/@earendil-works/pi-durable/dist/session/writer.js +var SESSION_FAILED = { kind: "failed" }; +var Writer = class { + #host; + /** Admitted parts not yet sent, in line order. */ + #queue = []; + #flight = false; + /** Per key, the newest unpublished part that writes it. */ + #unpublished = /* @__PURE__ */ new Map(); + /** The newest admitted part that is not yet published, whose gate opens after every earlier one's. */ + #newest; + constructor(host) { + this.#host = host; + } + /** Admit a part: register its keys, queue it, and send it at once unless a call is in flight. */ + admit(spec) { + const outcome = Promise.withResolvers(); + outcome.promise.catch(() => { + }); + const part = { + ...spec, + outcome: outcome.promise, + settle: { + resolve: () => { + part.done = true; + outcome.resolve(); + }, + reject: (error) => { + part.done = true; + outcome.reject(error); + } + }, + done: false, + gate: Promise.withResolvers() + }; + for (const key of spec.keys) + this.#unpublished.set(key, part); + this.#newest = part; + this.#queue.push(part); + this.#pump(); + return part; + } + /** The gate of the newest unpublished writer of `key`, or `undefined` when it has none. */ + gate(key) { + return this.#unpublished.get(key)?.gate.promise; + } + /** Opens once every part admitted so far is published or never will be. */ + drained() { + return this.#newest?.gate.promise; + } + /** Keys with an unpublished writer, for tests that bound memory. */ + get unpublished() { + return this.#unpublished.size; + } + #pump() { + if (this.#flight || this.#queue.length === 0) + return; + this.#flight = true; + const parts = this.#queue; + this.#queue = []; + void this.#flush(parts); + } + /** + * Send and publish `parts`, then what was admitted meanwhile. Never rejects: a throw the writer did not expect fails + * the Session and settles the parts (`#abandon()`), so no outcome or gate is stranded. + */ + async #flush(parts) { + try { + await this.#send(parts); + } catch (error) { + this.#abandon(parts, error); + } + this.#flight = false; + this.#pump(); + } + /** Store `parts` in one call, then publish them in order; settles every one of them. */ + async #send(parts) { + const stored = parts.filter((part) => part.writes !== void 0); + const seqs = []; + let result; + let rejected; + const failed = this.#host.failure(); + if (failed !== void 0) { + for (const part of parts) + this.#unpublish(part, new SessionFailed(failed.error)); + return; + } + if (stored.length > 0) { + try { + result = await this.#host.commit(stored.map((part) => part.writes), (index, seq) => { + seqs[index] = seq; + }, withoutAbortSignal(stored[0].context)); + } catch (error) { + rejected = { error }; + } + } + const failedMeanwhile = this.#host.failure(); + if (failedMeanwhile !== void 0) { + for (const part of parts) + this.#unpublish(part, new SessionFailed(failedMeanwhile.error)); + return; + } + await this.#publish(parts, seqs, result, rejected); + } + /** + * Publish the received parts and held ones in order, a macrotask apart, and settle every part (group commit §3.4). A + * refusal, a rejection, or a part whose adoption or own observer throws fails the Session once the parts before it + * are published and the host observers got the last one; the parts after it are not published, and reject with + * `SessionFailed`. What the failure decides (the outcomes, and the gates of the parts it concerns) is settled only + * once the Session failed, so no job waiting at such a gate, and no later outcome, goes on as if it had not. + */ + async #publish(parts, seqs, result, rejected) { + let stop2; + const failing = []; + let storedIndex = 0; + let published = 0; + for (const part of parts) { + const index = part.writes === void 0 ? void 0 : storedIndex++; + if (stop2 !== void 0) { + const outcome = rejected !== void 0 && index !== void 0 && stop2.external !== true ? { kind: "own", error: rejected.error } : SESSION_FAILED; + failing.push({ part, outcome, published: false }); + continue; + } + const seq = index === void 0 ? void 0 : seqs[index]; + if (index !== void 0 && seq === void 0) { + const error = rejected !== void 0 ? rejected.error : index === result?.stored && "refused" in result ? result.refused : new Error("Storage committed without adopting the batch"); + failing.push({ part, outcome: { kind: "own", error }, published: false }); + stop2 = { error, at: part.index }; + continue; + } + if (published++ > 0) + await hop(); + let failure2; + let external; + const publication = this.#host.publishing(() => { + external = this.#host.failure(); + if (external !== void 0) + return; + this.#unregister(part); + try { + if (part.writes !== void 0) + this.#host.stored(part.writes); + failure2 = part.publish(seq); + } catch (error) { + failure2 = { kind: "adoption", error }; + } + }, part.context); + if (publication !== void 0) + await publication; + if (external !== void 0) { + stop2 = { error: external.error, at: part.index, external: true }; + failing.push({ part, outcome: SESSION_FAILED, published: false }); + continue; + } + if (failure2?.kind === "adoption") { + failing.push({ part, outcome: { kind: "own", error: failure2.error }, published: true }); + stop2 = { error: failure2.error, at: part.index }; + continue; + } + if (rejected !== void 0) { + if (failure2 !== void 0) + this.#host.report(failure2.error, part.context); + failing.push({ part, outcome: { kind: "own", error: rejected.error }, published: true }); + continue; + } + if (failure2 !== void 0) + stop2 = { error: failure2.error, at: part.index }; + const failedMeanwhile = this.#host.failure(); + if (failedMeanwhile !== void 0 && failure2 === void 0) { + part.settle.reject(new SessionFailed(failedMeanwhile.error)); + } else + part.settle.resolve(); + this.#open(part); + } + if (rejected !== void 0 || stop2 !== void 0) { + await Promise.resolve(); + const own = failing.flatMap(({ part, outcome }) => outcome.kind === "own" ? [part.index] : []); + if (rejected !== void 0) + this.#host.fail(rejected.error, own[0], own); + else + this.#host.fail(stop2.error, stop2.at, own); + } + for (const { part, outcome, published: stored } of failing) { + const error = outcome.kind === "own" ? outcome.error : this.#failed(stop2.error); + if (stored) { + part.settle.reject(error); + this.#open(part); + } else + this.#unpublish(part, error); + } + const failure = this.#host.failure(); + if (failure !== void 0) { + const queued = this.#queue; + this.#queue = []; + for (const part of queued) + this.#unpublish(part, new SessionFailed(failure.error)); + } + } + /** + * A throw the writer itself did not expect while sending or publishing `parts` (a bug): fail the Session at the first + * of them and settle every one not settled yet, so no outcome, gate, or close waits on it forever. + */ + #abandon(parts, error) { + this.#host.fail(error, parts[0].index, []); + for (const part of parts) { + if (!part.done) + this.#unpublish(part, this.#failed(error)); + this.#unregister(part); + this.#open(part); + } + } + /** `SessionFailed` with the Session's failure, or with `error` until the Session records it. */ + #failed(error) { + const failure = this.#host.failure(); + return new SessionFailed(failure !== void 0 ? failure.error : error); + } + /** Settle a part that will never be published: discard it, reject it with `error`, and open its gate. */ + #unpublish(part, error) { + part.discard(); + part.settle.reject(error); + this.#unregister(part); + this.#open(part); + } + /** Drop the keys a part is still the newest writer of: it is published, or never will be. */ + #unregister(part) { + for (const key of part.keys) + if (this.#unpublished.get(key) === part) + this.#unpublished.delete(key); + } + /** Open a part's gate: the jobs waiting for it go on. The newest part's opens `drained()` too. */ + #open(part) { + part.gate.resolve(); + if (this.#newest === part) + this.#newest = void 0; + } +}; +function hop() { + if (typeof setImmediate === "function") + return new Promise((resolve2) => setImmediate(resolve2)); + const channel = new MessageChannel(); + return new Promise((resolve2) => { + channel.port1.onmessage = () => { + channel.port1.close(); + resolve2(); + }; + channel.port2.postMessage(void 0); + }); +} + +// node_modules/@earendil-works/pi-durable/dist/session/session.js +var NO_HOLDS = []; +var NO_OPERATIONS = []; +var NO_KEYS = []; +var MAX_OUTSTANDING = 256; +var ignore = () => { +}; +var SETTLED = Promise.resolve(); +var LISTENER = { kind: "listener_error" }; +var TELEMETRY = { kind: "host_error", callback: "telemetry" }; +var defaultRetention = 6e5; +function documentRetention(ms) { + return typeof ms === "number" && ms >= 0 ? ms : defaultRetention; +} +var DEFAULT_PERSIST_WITHIN_MS = 1e3; +function persistWithin(ms) { + return typeof ms === "number" && ms >= 0 ? ms : DEFAULT_PERSIST_WITHIN_MS; +} +var SessionImpl = class { + #storage; + /** Newest entry and head marker per conversation; none for a read-only Session, whose Storage another process writes. */ + #tails; + /** + * The current documents of each live task this Session created, so the commit that ends a task retires them without + * a scan. An entry starts with its task's creation and goes when the task is terminal: bounded by live tasks. + */ + #taskDocuments = /* @__PURE__ */ new Map(); + #documents = /* @__PURE__ */ new Map(); + /** Cached documents with no observer attached, by address, oldest use first, with the time of that use. */ + #idleDocuments = /* @__PURE__ */ new Map(); + /** Observers attached per document incarnation (record ID); a cached incarnation with any is never evicted. */ + #attachedDocuments = /* @__PURE__ */ new Map(); + #clock; + #documentRetentionMs; + #persistWithinMs; + /** Documents with held changes, by incarnation (record ID); each is pinned in the cache until they are stored. */ + #holds = /* @__PURE__ */ new Map(); + #holdTimer; + /** Refuse every commit with writes (`HarnessOptions.readOnly`). */ + #readOnly; + #timers; + /** The pending eviction: a timer for the oldest idle document, or a line job when the retention is 0. */ + #documentSweep; + #sweepJobs = 0; + /** Set once the host declined a background timer: then none is kept, and the end of each commit evicts what expired. */ + #documentTimersUnavailable = false; + /** Host commit observers (`observeCommits()`), each behind `guardObserver()`. */ + #commitListeners = /* @__PURE__ */ new Set(); + /** The Session's own listeners, such as its scheduler's: they keep memory in step with storage, so a throw fails it. */ + #internalListeners = /* @__PURE__ */ new Set(); + #closeListeners = /* @__PURE__ */ new Set(); + #host; + /** The record cache under the guard, which the writer applies each batch to at its publication. */ + #cached; + #writer; + /** The Storage jobs on the line read through: each read waits while its key has an unpublished writer. */ + #gated; + /** The end of the last line job: the line runs one job at a time. */ + #tail = Promise.resolve(); + /** Line index of the next job; outcomes are delivered in this order. */ + #nextIndex = 0; + /** The job on the line, while one runs: its index, and whether it waited at a gate. */ + #current; + /** Settles once every job so far has its outcome delivered. */ + #fence = Promise.resolve(); + #lastDelivered = -1; + /** Jobs that started on the line and have no outcome delivered yet (`MAX_OUTSTANDING`). */ + #outstanding = 0; + #room; + /** The line index whose outcome the failure is: every later job's outcome is `SessionFailed`, except `#ownOutcomes`. */ + #failAt; + /** Later jobs whose outcome is their own error all the same: the other parts of a storage call that rejected. */ + #ownOutcomes = /* @__PURE__ */ new Set(); + #closing; + #failure; + /** Set when close reaches the backend: calls that fail from then on fail because it closes. */ + #failed = Promise.withResolvers(); + #closed = Promise.withResolvers(); + get closed() { + return this.#closed.promise; + } + /** Internal: the error that failed the Session, if one did. */ + get failure() { + return this.#failure; + } + /** Internal: rejects with `SessionFailed` the moment the Session fails, before work underway has ended; never resolves. */ + get failed() { + return this.#failed.promise; + } + constructor(storage, now = Date.now, documentRetentionMs = () => void 0, setTimer, readOnly = false, persistWithinMs = () => void 0) { + this.#failed.promise.catch(() => { + }); + this.#readOnly = readOnly; + this.#cached = readOnly ? void 0 : new CachedStorage(storage); + this.#storage = new GuardedStorage(this.#cached ?? storage, this); + this.#gated = new GatedStorage(this.#storage, (key) => this.touch(key)); + this.#writer = new Writer({ + commit: (batches, receive, context) => this.#storage.commit(batches, receive, context), + stored: (writes) => { + this.#cached?.published(writes); + this.#tails?.apply(writes); + }, + publishing: (publish2, context) => this.#publishing(publish2, context), + failure: () => this.#failure, + fail: (error, at, own) => this.fail(error, void 0, at, own), + report: (error, context) => this.report(error, LISTENER, context) + }); + this.#tails = readOnly ? void 0 : new ConversationTails(); + this.#timers = hostTimers(setTimer, (error) => this.report(error, { kind: "host_error", callback: "setTimer" })); + this.#clock = () => { + try { + return now(); + } catch { + return Date.now(); + } + }; + this.#documentRetentionMs = () => { + try { + return documentRetention(documentRetentionMs()); + } catch { + return documentRetention(void 0); + } + }; + this.#persistWithinMs = () => { + try { + return persistWithin(persistWithinMs()); + } catch { + return persistWithin(void 0); + } + }; + this.#host = { + storage: this.#gated, + touch: (key) => this.touch(key), + now, + cached: (id) => { + const cached = this.#documents.get(id); + if (cached !== void 0) + this.#touchDocument(id); + return cached; + }, + load: (definition, addressId2, address, context) => this.#loadDocument(definition, addressId2, address, context), + install: (document) => { + this.#documents.set(document.addressId, document); + this.#touchDocument(document.addressId); + }, + evict: (id, recordId) => { + if (this.#documents.get(id)?.record.id === recordId) + this.#dropDocument(id); + }, + beforeInput: (tx, id, input, context) => this.beforeInput(tx, id, input, context), + conversationCreated: (tx, record) => this.conversationCreated(tx, record), + taskDocuments: (taskId) => this.#taskDocuments.get(taskId)?.values(), + latestEntryId: (conversationId, context) => this.latestEntryId(conversationId, context), + latestHeadMarker: (conversationId, context) => this.latestHeadMarker(conversationId, void 0, context) + }; + } + /** The Storage behind the failure guard; every component reads and writes through it. */ + get storage() { + return this.#storage; + } + /** + * Internal, on the Session line: the newest entry ID visible from `conversationId`. Known from the Session's own + * commits when it owns its Storage, so it costs no Storage read after the first. + */ + latestEntryId(conversationId, context) { + const gate = this.#touchEntries(conversationId); + const latest = () => { + const read2 = async () => (await this.#storage.scanEntries({ conversationId }, 1, void 0, context)).items[0]?.id; + return this.#tails === void 0 ? read2() : this.#tails.latest(conversationId, read2); + }; + return gate === void 0 ? latest() : gate.then(latest); + } + /** + * Internal: the newest entry ID of `conversationId` as known from the Session's commits right now, without reading; + * `undefined` when not known (and always for a read-only Session). + */ + knownLatestEntryId(conversationId) { + return this.#tails?.peek(conversationId); + } + /** + * Internal, on the Session line: the newest visible head marker of `conversationId` at or before `atOrBefore`, any + * when `undefined`; known from the Session's own commits as `latestEntryId()` is. + */ + latestHeadMarker(conversationId, atOrBefore, context) { + const gate = this.#touchEntries(conversationId); + const marker = () => { + const read2 = () => this.#storage.findLatestHeadMarker(conversationId, atOrBefore, context); + return this.#tails === void 0 ? read2() : this.#tails.marker(conversationId, atOrBefore, read2); + }; + return gate === void 0 ? marker() : gate.then(marker); + } + /** + * On the line: wait for what an unpublished commit wrote of a conversation's entries, its appends and its creation, + * as reading them from Storage fails for a conversation not stored yet. + */ + #touchEntries(conversationId) { + const created = this.touch(conversationKey(conversationId)); + const appended = this.touch(appendKey(conversationId)); + if (created === void 0) + return appended; + return appended === void 0 ? created : Promise.all([created, appended]).then(ignore); + } + /** + * Internal, on the line: wait, while `key` has an unpublished writer (for `ALL_KEYS`, while anything is unpublished), + * until it is published, then check that the Session is healthy; `undefined` when there is nothing to wait for. A + * job's every read touches its key first, so it never observes state an earlier job admitted and the writer has not + * published. + */ + touch(key) { + const gate = key === ALL_KEYS ? this.#writer.drained() : this.#writer.gate(key); + if (gate === void 0) + return void 0; + if (this.#current !== void 0) + this.#current.hit = true; + return gate.then(() => this.#assertHealthy()); + } + commit(change, context, options) { + return this.commitWith(change, context, void 0, options); + } + /** + * Internal: fail the Session with `error`, the first wins, and report it once. Admission ends, and close listeners run + * now; they read the error from `failure`. The Storage guard calls this; the Harness also calls it for a throw in its + * scheduler's own commits. `at` is the line index of the job whose outcome the failure is, by default the job on the + * line: later jobs' outcomes become `SessionFailed`. + */ + fail(error, context, at, own) { + if (this.#failure !== void 0) + return; + this.#failure = { error }; + this.#failAt = at ?? this.#current?.index ?? this.#lastDelivered; + if (own !== void 0) + for (const index of own) + this.#ownOutcomes.add(index); + this.#failed.reject(new SessionFailed(error)); + this.close(BACKGROUND_CONTEXT).catch(() => { + }); + this.report(error, { kind: "storage_failed" }, context); + } + /** + * Internal commit exposing the concrete transaction and its internal operations, such as the reserved-ID root + * bootstrap and task replacement. `scope` sets the default `tx.createTask()` conversation and the task attributed to + * appended entries. + */ + commitWith(change, context, scope, options) { + let withinMs; + try { + this.assertUsable(); + withinMs = this.#deferral(options?.persistence); + } catch (error) { + return Promise.reject(error); + } + return this.#enqueue((index) => this.#runCommit(change, context, scope, withinMs, index)); + } + /** + * Internal: `commitWith()` for a commit of the scheduler's own whose outcome goes only to scheduler code. When it + * writes nothing and waited at no gate, it is equivalent to having run before every unpublished commit, so its + * outcome is delivered at once, ahead of theirs. One that writes is never exempt: its outcome waits for its own + * publication and every earlier outcome, so no invocation starts on a reservation that is not durable. + */ + commitInternal(change, context) { + try { + this.assertUsable(); + } catch (error) { + return Promise.reject(error); + } + return this.#enqueue((index) => this.#runCommit(change, context, void 0, void 0, index), true); + } + /** + * Internal, on the line: call `end` once the outcome of the job on the line settles, its commit published or never to + * be; at once off the line. + */ + onJobEnd(end) { + const job = this.#current; + if (job === void 0) { + end(); + return; + } + if (job.ends === void 0) + job.ends = [end]; + else + job.ends.push(end); + } + /** Internal: the Storage jobs on the line read through (`touch()`); off the line, use the plain one. */ + get lineStorage() { + return this.#gated; + } + /** How long a commit may stay in memory before it is stored; `undefined` when it is immediate. */ + #deferral(persistence) { + if (persistence?.kind !== "deferred") + return void 0; + const ms = persistence.withinMs; + if (ms === void 0) + return this.#persistWithinMs(); + if (typeof ms !== "number" || !(ms >= 0)) { + throw new TypeError("CommitPersistence.withinMs must be a number of milliseconds, 0 or more"); + } + return ms; + } + /** Internal: whether `tx` is the transaction of one of this Session's commits. */ + ownsTransaction(tx) { + return Transaction.ownedBy(tx, this.#host); + } + /** + * Internal: run a read-only job on the mutation line so multi-read derivations observe one committed state. It reads + * through `storage`, whose reads wait for the publication of what the line admitted before; its outcome comes after + * theirs. + */ + readOnLine(job) { + try { + this.assertUsable(); + } catch (error) { + return Promise.reject(error); + } + return this.#enqueue(async () => { + this.#assertHealthy(); + return { value: await job(this.#gated) }; + }); + } + /** + * Internal: a conversation document's current incarnation and value, for a job already running on the line (see + * `readOnLine()`). Absent documents are `undefined`. + */ + async conversationDocumentOnLine(token, conversationId, context) { + const definition = token.definition; + const resolved = resolveAddress(definition, [conversationId, context]); + const loaded = await this.#loadDocument(definition, resolved.id, resolved.address, context); + if (loaded === void 0) + return void 0; + checkRecordScope(definition, loaded.record); + checkRecordVersion(definition, loaded.record, loaded.storedVersion); + return { record: loaded.record, version: loaded.valueVersion, value: loaded.tracker.value }; + } + async snapshot(token, ...args) { + this.assertUsable(); + const definition = token.definition; + const resolved = resolveAddress(definition, args); + const context = args[resolved.nextArgument]; + const cached = this.#documents.get(resolved.id); + if (cached?.valueVersion === definition.version) + this.#touchDocument(resolved.id); + const loaded = cached?.valueVersion === definition.version ? cached : await this.#enqueue(async () => { + this.#assertHealthy(); + return { value: await this.#loadDocument(definition, resolved.id, resolved.address, context) }; + }); + if (loaded === void 0) + return void 0; + checkRecordScope(definition, loaded.record); + checkRecordVersion(definition, loaded.record, loaded.storedVersion); + return loaded.tracker.value; + } + watchDoc(token, ...args) { + try { + this.assertUsable(); + const definition = token.definition; + const resolved = resolveAddress(definition, args); + const context = args[resolved.nextArgument]; + const signal = context.abortSignal; + return this.#attachDocument(definition, resolved, context, (loaded) => { + const watch = this.#observeDocument(definition, loaded, createWatch, endWatch); + if (signal !== void 0) + watch.observeAbort(signal); + return watch; + }, abortWatch); + } catch (error) { + return Promise.reject(error); + } + } + observeDoc(token, ...args) { + try { + this.assertUsable(); + const definition = token.definition; + const resolved = resolveAddress(definition, args); + const observer = args[resolved.nextArgument]; + const context = args[resolved.nextArgument + 1]; + return this.#attachDocument(definition, resolved, context, (loaded) => { + const guarded = this.#observeDocument(definition, loaded, (_value, detach, report2) => guardObserver(observer, detach, report2), endGuarded); + return observation(loaded.tracker.value, guarded); + }, detachObservation); + } catch (error) { + return Promise.reject(error); + } + } + async snapshotAsOf(token, ...args) { + this.assertUsable(); + const definition = token.definition; + const resolved = resolveAddress(definition, args); + if (resolved.address.scope.kind !== "conversation") { + throw new TypeError("Session.snapshotAsOf() requires a conversation document"); + } + const conversationId = resolved.address.scope.conversationId; + const atValue = args[resolved.nextArgument]; + if (typeof atValue !== "number" || !Number.isSafeInteger(atValue)) { + throw new TypeError("Session.snapshotAsOf() requires an entry ID"); + } + const at = idFromNumber(atValue); + const context = args[resolved.nextArgument + 1]; + return this.#enqueue(async () => { + this.#assertHealthy(); + const storedEntry = await this.#gated.entry(conversationId, at, context); + if (storedEntry === void 0) { + throw new Error(`Entry ${at} is not visible from conversation ${conversationId}`); + } + const address = { + ...resolved.address, + scope: { kind: "conversation", conversationId: storedEntry.entry.conversationId } + }; + const record = await this.#gated.findDocument(address, storedEntry.commitSeq, context); + if (record === void 0) + return { value: void 0 }; + const stored = await this.#gated.document(record.id, storedEntry.commitSeq, context); + if (stored === void 0) { + throw new Error(`Historical document ${record.id} (${record.kind}) cannot be read`); + } + return { value: materializeDocument(definition, stored) }; + }); + } + close(context) { + if (this.#closing === void 0) { + const cleanup = withoutAbortSignal(context); + this.#closing = Promise.resolve().then(() => this.beforeClose()).then(() => this.#maintain(async () => { + await this.#drained(); + let unstored; + if (this.#failure === void 0 && this.#holds.size > 0) { + await this.#storeHolds(() => [...this.#holds.values()], cleanup).catch((error) => { + unstored = { error }; + }); + } + const observers = [...this.#commitListeners]; + this.#commitListeners.clear(); + const end = closeEnds(this.#failure).observer; + for (const observer of observers) + observer.end(end); + this.#internalListeners.clear(); + this.#documents.clear(); + this.#idleDocuments.clear(); + this.#attachedDocuments.clear(); + this.#holds.clear(); + this.#taskDocuments.clear(); + try { + await this.#storage.close(cleanup); + } catch (error) { + if (this.#failure === void 0) { + this.#failure = { error }; + this.report(error, { kind: "storage_failed" }); + } + throw error; + } + if (unstored !== void 0) + throw unstored.error; + })).finally(() => { + const failure = this.#failure; + this.#closed.resolve(failure === void 0 ? { kind: "closed" } : { kind: "failed", error: failure.error }); + }); + this.#notifyClose(); + } + return awaitWithContext(this.#closing, context); + } + /** Run the close listeners once, each on its own: one that fails is reported, and the rest still run. */ + #notifyClose() { + this.#documentSweep?.disarm?.(); + this.#documentSweep = void 0; + this.#holdTimer?.disarm(); + this.#holdTimer = void 0; + const listeners = [...this.#closeListeners]; + this.#closeListeners.clear(); + for (const listener of listeners) + contained(listener, (error) => this.report(error)); + } + /** + * Internal: the Session's failure, and errors of listeners, which never fail what ran them; `info` defaults to a + * listener. A plain Session drops them. + */ + report(_error, _info = LISTENER, _context) { + } + async beforeInput(_tx, _id, _input, _context) { + return []; + } + /** + * Runs inside every transaction that creates or forks a conversation, after the conversation record is staged. A + * plain Session stages nothing; a Harness stages its built-in documents. + */ + conversationCreated(_tx, _record) { + return Promise.resolve(); + } + /** + * Internal, for `beforeClose()`: on the line, after every commit admitted before the seal has settled, write what + * `writes()` returns then straight to Storage. Nothing is published, since the Session is closing; a failed Session + * writes nothing. + */ + writeAtClose(writes) { + return this.#maintain(async (index) => { + await this.#drained(); + if (this.#failure !== void 0) + return; + const batch = writes(); + if (batch.length === 0) + return; + const context = BACKGROUND_CONTEXT; + await this.#writer.admit({ + index, + writes: batch, + keys: [], + context, + publish: () => void 0, + discard: ignore + }).outcome; + }); + } + /** + * Keys a commit writes besides its records' (`Transaction.writeKeys()`), at its admission, on the line: a Harness + * adds the ownership memberships its task changes write. A plain Session adds none. + */ + membershipKeys(_tx) { + return NO_KEYS; + } + /** Runs after close seals admission and before the line closes Storage; must not reject. */ + beforeClose() { + return Promise.resolve(); + } + /** + * Attach a synchronous observer of every stored commit, with exactly what it stored (`CommitPublication`). Its + * `advance` runs one microtask after the commit's publication, which follows its storage call, in line order, before + * every wait the commit resolves and every abort it queues (in no order with watch listeners); it must not block or + * call Session operations. A throw is reported, detaches it, and ends it `listener_error`; `end` gets the Session's + * close after its last publication. + */ + observeCommits(observer) { + this.assertUsable(); + const guarded = guardObserver(observer, () => this.#commitListeners.delete(guarded), (error, context) => this.report(error, LISTENER, context)); + this.#commitListeners.add(guarded); + return { detach: () => guarded.detach() }; + } + /** + * Internal: follow every commit as memory adopts it (`ObservedCommit`), for the Session's own components, whose state + * follows memory. A throw leaves that state behind, so it fails the Session instead of being reported. They run at + * the commit's publication, synchronously, so they call no Storage method, run no task code, and contain every host + * callback; the waits they resolve and the aborts they queue run after host observers. + */ + followCommits(listener) { + this.assertUsable(); + this.#internalListeners.add(listener); + return () => this.#internalListeners.delete(listener); + } + /** + * Register a listener called synchronously when close begins, also when a failure closes the Session (`failure` is + * then set). It must not block or call Session operations; a throw is reported. + */ + subscribeClose(listener) { + this.assertUsable(); + this.#closeListeners.add(listener); + return () => this.#closeListeners.delete(listener); + } + /** Drop every loaded tracker on the mutation line, held changes stored first; later access cold-loads from Storage. */ + unloadDocuments() { + return this.#maintain(async () => { + if (this.#failure === void 0) + await this.#storeHolds(() => [...this.#holds.values()], BACKGROUND_CONTEXT); + this.#documents.clear(); + this.#idleDocuments.clear(); + }); + } + /** + * Internal, for tests: how many documents the cache holds, how many are idle, how many incarnations are pinned, how + * many sweep jobs were queued on the line so far, how many documents have held changes, how many live tasks have + * their documents indexed, and how many conversations have their newest entry kept. + */ + get documentCacheSizes() { + return { + cached: this.#documents.size, + idle: this.#idleDocuments.size, + attached: this.#attachedDocuments.size, + sweeps: this.#sweepJobs, + held: this.#holds.size, + taskDocuments: this.#taskDocuments.size, + tails: this.#tails?.size ?? 0 + }; + } + /** + * Internal, for tests that bound memory: jobs whose outcome is not delivered yet, keys with an unpublished writer, + * and record reads in flight in the cache. All three are 0 once the line is idle. + */ + get lineSizes() { + return { + outstanding: this.#outstanding, + unpublished: this.#writer.unpublished, + reads: this.#cached?.readsInFlight ?? 0 + }; + } + /** Note a use of a cached document: an unattached one becomes the newest idle one, and an eviction is scheduled. */ + #touchDocument(addressId2) { + const cached = this.#documents.get(addressId2); + if (cached === void 0 || this.#attachedDocuments.has(cached.record.id)) + return; + const now = this.#clock(); + const since = this.#idleDocuments.get(addressId2); + if (since !== void 0 && now - since < Math.min(1e3, this.#documentRetentionMs() / 10)) + return; + this.#idleDocuments.delete(addressId2); + this.#idleDocuments.set(addressId2, now); + if (this.#documentSweep === void 0 || this.#documentSweep.at > now + this.#documentRetentionMs()) { + this.#scheduleDocumentSweep(); + } + } + #dropDocument(addressId2) { + this.#documents.delete(addressId2); + this.#idleDocuments.delete(addressId2); + } + /** + * Schedule the eviction of the oldest idle document: on the line at once when the retention is 0, otherwise when it + * expires, by a background timer (`HarnessOptions.setTimer`), which never keeps the process alive. Where the host + * declines it, as the default does in Cloudflare Workers, none is kept: the end of each commit evicts what expired. + */ + #scheduleDocumentSweep() { + if (this.#closing !== void 0) + return; + let oldest; + for (const [addressId2, since] of this.#idleDocuments) { + if (this.#writer.gate(documentKey(addressId2)) !== void 0) + continue; + oldest = since; + break; + } + if (oldest === void 0) + return; + const at = oldest + this.#documentRetentionMs(); + if (this.#documentSweep !== void 0 && this.#documentSweep.at <= at) + return; + this.#documentSweep?.disarm?.(); + const sweep = { at, disarm: void 0 }; + const run2 = () => { + this.#sweepJobs++; + this.#maintain(async () => { + if (this.#documentSweep === sweep) + this.#documentSweep = void 0; + if (this.#closing === void 0) + this.#sweepDocuments(); + }).catch(() => { + }); + }; + const delay = at - this.#clock(); + this.#documentSweep = sweep; + if (delay <= 0) { + queueMicrotask(run2); + return; + } + if (this.#documentTimersUnavailable) { + this.#documentSweep = void 0; + return; + } + const disarm = this.#timers.background(run2, delay); + if (disarm === void 0) { + this.#documentTimersUnavailable = true; + this.#documentSweep = void 0; + return; + } + sweep.disarm = disarm; + } + /** On the line: evict the idle documents whose retention passed, oldest first, then schedule the next eviction. */ + #sweepDocuments() { + const expired = this.#clock() - this.#documentRetentionMs(); + for (const [addressId2, since] of this.#idleDocuments) { + if (since > expired) + break; + if (this.#writer.gate(documentKey(addressId2)) !== void 0) + continue; + this.#dropDocument(addressId2); + } + this.#scheduleDocumentSweep(); + } + /** Pin a cached incarnation for one observer; the returned release unpins it once, idle again after the last. */ + #pinDocument(addressId2, recordId) { + this.#attachedDocuments.set(recordId, (this.#attachedDocuments.get(recordId) ?? 0) + 1); + this.#idleDocuments.delete(addressId2); + let pinned = true; + return () => { + if (!pinned) + return; + pinned = false; + const count = (this.#attachedDocuments.get(recordId) ?? 1) - 1; + if (count > 0) { + this.#attachedDocuments.set(recordId, count); + return; + } + this.#attachedDocuments.delete(recordId); + if (this.#documents.get(addressId2)?.record.id === recordId) + this.#touchDocument(addressId2); + }; + } + async #runCommit(change, context, scope, withinMs, index) { + this.#assertHealthy(); + context.abortSignal?.throwIfAborted(); + const tx = new Transaction(this.#host, context, scope); + let result; + try { + result = await change(tx); + this.#assertHealthy(); + } catch (error) { + await tx.settleFailure(); + this.#sweepDocuments(); + throw error; + } + let writes; + try { + writes = await tx.settleSuccess(); + } catch (error) { + this.#sweepDocuments(); + throw error; + } + if (writes.length === 0) { + tx.discard(); + this.#sweepDocuments(); + return { value: result }; + } + if (this.#readOnly) { + tx.discard(); + this.#sweepDocuments(); + throw new ReadOnlySession("a commit with writes"); + } + const discard = () => tx.discard(); + if (withinMs !== void 0) { + const held = tx.holdable(); + if (held !== void 0) { + const part2 = this.#writer.admit({ + index, + writes: void 0, + keys: tx.writeKeys(), + context, + publish: () => this.#hold(tx, held, withinMs, context), + discard + }); + this.#sweepDocuments(); + return { value: result, after: part2.outcome }; + } + } + let membership = this.membershipKeys(tx); + if (!Array.isArray(membership)) { + try { + membership = await membership; + } catch (error) { + tx.discard(); + this.#sweepDocuments(); + throw error; + } + } + const taken = this.#holds.size === 0 ? NO_HOLDS : tx.takeHeld((id) => this.#holds.get(id)); + const keys = tx.writeKeys(); + for (const key of membership) + keys.push(key); + const part = this.#writer.admit({ + index, + writes, + keys, + context, + publish: (seq) => this.#adopt(tx, writes, taken, seq, context), + discard + }); + this.#sweepDocuments(); + return { value: result, after: part.outcome }; + } + /** + * Publish a stored commit (`Writer`), after the read caches took its writes: adopt it in memory and run its + * publication. A throw of the adoption leaves memory behind durable state; the writer fails the Session with it. + */ + #adopt(tx, writes, taken, seq, context) { + let documents; + try { + documents = tx.adopt(seq); + } catch (error) { + return { kind: "adoption", error }; + } + this.#indexTaskDocuments(tx, writes, documents, seq); + const failure = this.#publish(seq, writes, documents, context); + if (taken.length > 0) + this.#release(taken); + for (const document of documents.stored) + this.#touchDocument(addressId(document.record)); + return failure === void 0 ? void 0 : { kind: "listener", error: failure.error }; + } + /** + * At publication: keep `#taskDocuments` in step with a stored commit: the tasks it creates start with no documents, + * task documents it creates join their task, ones it retires leave, and tasks it ends leave the index. + */ + #indexTaskDocuments(tx, writes, documents, seq) { + for (const id of tx.createdTaskIds()) + this.#taskDocuments.set(id, /* @__PURE__ */ new Map()); + if (this.#taskDocuments.size === 0) + return; + for (const change of documents.stored) { + if (change.type !== "document" || change.record.scope.kind !== "task") + continue; + const known = this.#taskDocuments.get(change.record.scope.taskId); + if (known === void 0) + continue; + if (change.kind === "retire") + known.delete(change.record.id); + else if (change.record.createdAt === seq) + known.set(change.record.id, change.record); + } + for (const write of writes) { + if (write.type === "task" && write.value.state.status === "terminal") + this.#taskDocuments.delete(write.value.id); + } + } + /** + * Hold a deferred commit (`Transaction.holdable()`): adopt it in memory, queue its changes per document, pinned, by + * the earliest deadline of the document's held commits, and show it to the Session's own observers. + */ + #hold(tx, held, withinMs, context) { + let documents; + try { + documents = tx.adoptHeld(); + } catch (error) { + return { kind: "adoption", error }; + } + const deadline = this.#clock() + withinMs; + for (const { loaded, ops: ops3, base } of held) { + const id = loaded.record.id; + let hold = this.#holds.get(id); + if (hold === void 0) { + const unpin = this.#pinDocument(loaded.addressId, id); + hold = { loaded, pending: new PendingOps(), base: false, deadline, unpin }; + this.#holds.set(id, hold); + } + hold.pending.push(ops3); + if (base) + hold.base = true; + if (deadline < hold.deadline) + hold.deadline = deadline; + } + this.#armHoldTimer(); + const publication = { kind: "memory", changes: documents }; + let failure; + for (const listener of [...this.#internalListeners]) { + try { + listener(publication, context); + } catch (error) { + failure ??= { kind: "listener", error }; + } + } + return failure; + } + /** + * Store the held changes `select` returns in one storage transaction, through the writer, from a job on the line. A + * barrier: they are selected once every commit admitted before is published. Memory already has them, so only + * `observeCommits()` observers see it, published as the writer publishes it. A failure fails the Session, as every + * Storage failure does. + */ + async #storeHolds(select, context) { + await this.#drained(); + const holds = select(); + if (holds.length === 0) + return; + const writes = []; + const changes = []; + const keys = []; + for (const hold of holds) { + const { record, tracker, valueVersion: version } = hold.loaded; + const value2 = tracker.value; + const stored = storeHeld(hold, NO_OPERATIONS, value2, version); + writes.push({ type: "document.change", id: record.id, content: stored.content }); + keys.push(documentKey(hold.loaded.addressId), incarnationKey(record.id)); + const conversationId = record.scope.kind === "conversation" ? record.scope.conversationId : void 0; + changes.push({ type: "document", kind: "change", record, conversationId, version, value: value2, ops: stored.ops }); + } + const publish2 = (seq) => { + for (const [index2, hold] of holds.entries()) { + if (writes[index2].content.kind === "base") + hold.loaded.deltasSinceBase = 0; + else + hold.loaded.deltasSinceBase++; + } + if (this.#commitListeners.size > 0) + this.#notifyHosts({ seq, changes }, context); + this.#release(holds); + return void 0; + }; + const index = this.#current.index; + await this.#writer.admit({ index, writes, keys, context, publish: publish2, discard: ignore }).outcome; + } + /** After their store: forget `holds`, unpin their documents, and move the timer to the next deadline. */ + #release(holds) { + for (const hold of holds) { + this.#holds.delete(hold.loaded.record.id); + hold.unpin(); + } + this.#armHoldTimer(); + } + /** + * Keep a foreground timer (Workers refuse background ones) for the earliest deadline of the held documents. When it + * fires, every held document is stored, in one storage transaction: a deadline is the latest time, and the others + * cost little more in the same transaction. Once fired, it stays until its job ran. + */ + #armHoldTimer() { + if (this.#closing !== void 0) + return; + let at = Infinity; + for (const hold of this.#holds.values()) + if (hold.deadline < at) + at = hold.deadline; + const armed = this.#holdTimer; + if (armed !== void 0 && (armed.fired || armed.at === at)) + return; + armed?.disarm(); + this.#holdTimer = void 0; + if (at === Infinity) + return; + const timer = { at, disarm: () => { + }, fired: false }; + const run2 = () => { + timer.fired = true; + this.#maintain(async () => { + if (this.#holdTimer === timer) + this.#holdTimer = void 0; + if (this.#closing !== void 0 || this.#failure !== void 0) + return; + await this.#storeHolds(() => [...this.#holds.values()], BACKGROUND_CONTEXT); + }).catch(() => { + }); + }; + this.#holdTimer = timer; + timer.disarm = this.#timers.foreground(run2, Math.max(0, at - this.#clock())); + } + /** + * At publication: publish a stored commit. Its telemetry events first, then the dispatch to `observeCommits()` + * observers, queued before the waits and aborts the observers wake, then the Session's own observers. Never throws: + * returns the first error of an own observer, which fails the Session once its storage call settled. + */ + #publish(seq, writes, documents, context) { + const changes = this.#commitListeners.size > 0 || this.#internalListeners.size > 0 ? [] : void 0; + for (const write of writes) { + switch (write.type) { + case "entry": + this.#event("pi.durable.entry", { "entry.id": write.value.id, "entry.kind": write.value.kind }, context); + changes?.push(write); + break; + case "submission": + if (write.value.status === "done" || write.value.status === "unanswered") { + this.#event("pi.durable.submission", { + "submission.id": write.value.id, + status: write.value.status, + "answer.entry_id": write.value.answer + }, context); + } + changes?.push(write); + break; + case "conversation": + case "task": + changes?.push(write); + } + } + if (changes === void 0) + return void 0; + const tables = changes.length; + for (const document of documents.observed) + changes.push(document); + const observed = { kind: "stored", seq, changes }; + if (this.#commitListeners.size > 0) { + const stored = documents.stored === documents.observed ? observed : { seq, changes: [...changes.slice(0, tables), ...documents.stored] }; + this.#notifyHosts(stored, context); + } + let failure; + for (const listener of [...this.#internalListeners]) { + try { + listener(observed, context); + } catch (error) { + failure ??= { error }; + } + } + return failure; + } + /** At publication: record a commit's telemetry event. A throwing adapter is reported, as publication must not throw. */ + #event(name, attributes, context) { + try { + context.telemetry.addEvent(name, attributes); + } catch (error) { + this.report(error, TELEMETRY, context); + } + } + /** + * At publication: call the `observeCommits()` observers with `publication` one microtask from now, and, queued + * before the Session's own observers run, before the waits they resolve and the aborts they queue. The observers are + * those attached when it runs; their guards report a throw. + */ + #notifyHosts(publication, context) { + void SETTLED.then(() => { + for (const observer of [...this.#commitListeners]) + observer.advance(publication, context); + }); + } + /** + * On the line, as one job: load the document at `resolved` and `attach` to it, so the attachment sees every commit + * after the value it starts from; `undefined` when absent. Cancelling `context` meanwhile rejects, after `cancel` + * undid an attachment made. + */ + async #attachDocument(definition, resolved, context, attach, cancel) { + const signal = context.abortSignal; + let cancelled = signal?.aborted ?? false; + const markCancelled = () => { + cancelled = true; + }; + signal?.addEventListener("abort", markCancelled, { once: true }); + try { + const attached = await this.#enqueue(async () => { + this.#assertHealthy(); + if (cancelled) + throw cancellationError(signal); + const loaded = await this.#loadDocument(definition, resolved.id, resolved.address, context); + if (cancelled) + throw cancellationError(signal); + return { value: loaded === void 0 ? void 0 : attach(loaded) }; + }); + if (attached === void 0) + return void 0; + if (cancelled) { + cancel(attached); + throw cancellationError(signal); + } + return attached; + } finally { + signal?.removeEventListener("abort", markCancelled); + } + } + /** + * Follow the address of the incarnation `loaded`, after checking the definition, with the observer `create` makes + * from its value, the idempotent detach, and the report of a listener error; returns that observer. It gets each + * committed change there, a retirement as a `retired` change with the last value it was given and no ops (a change + * in the retiring commit is not published), and a later incarnation as a root replacement. When the commit makes the + * owner task of a task document terminal, no later commit can change the address: after its changes the commit + * subscription and the pin go, and `ownerEnded` tells the observer, which stays subscribed to the Session's close + * until it ends (its detach drops that). `end` gets the Session's close. The current incarnation is pinned in the + * cache. The detach runs before `end`; once it ran, nothing more of the commit underway reaches the observer or pins + * anything for it. `create` and `ownerEnded` are module functions where they can be, so a watch keeps no closure of + * its caller. + */ + #observeDocument(definition, loaded, create, ownerEnded) { + checkRecordScope(definition, loaded.record); + checkRecordVersion(definition, loaded.record, loaded.storedVersion); + const addressId2 = loaded.addressId; + const address = loaded.record; + let incarnation = loaded.record.id; + let value2 = loaded.tracker.value; + let version = loaded.valueVersion; + let unpin = this.#pinDocument(addressId2, incarnation); + let unsubscribeCommit = () => { + }; + let unsubscribeClose = () => { + }; + let attached = true; + const detach = () => { + if (!attached) + return; + attached = false; + unsubscribeCommit(); + unsubscribeClose(); + unpin(); + }; + const observer = create(loaded.tracker.value, detach, (error) => this.report(error)); + unsubscribeCommit = this.followCommits((publication, context) => { + if (!attached) + return; + let ownerEnds = false; + for (const change of publication.changes) { + if (change.type === "task") { + const scope = address.scope; + if (scope.kind === "task" && change.value.id === scope.taskId && change.value.state.status === "terminal") { + ownerEnds = true; + } + continue; + } + if (change.type !== "document") + continue; + if (change.record.id !== incarnation && !atAddress(change.record, address)) + continue; + if (change.kind === "retire") { + if (change.record.id !== incarnation) + continue; + const last = value2; + incarnation = void 0; + value2 = void 0; + unpin(); + observer.advance({ kind: "value", value: last, ops: NO_OPERATIONS, retired: true }, context); + } else if (change.record.id !== incarnation || change.version !== version) { + if (change.record.id !== incarnation) { + incarnation = change.record.id; + unpin(); + unpin = this.#pinDocument(addressId2, incarnation); + } + version = change.version; + value2 = change.value; + observer.advance({ kind: "value", value: value2, ops: [["r", value2]], retired: false }, context); + } else { + if (change.ops.length === 0) + continue; + value2 = change.value; + observer.advance({ kind: "value", value: value2, ops: change.ops, retired: false }, context); + } + if (!attached) + return; + } + if (!ownerEnds) + return; + unsubscribeCommit(); + unpin(); + ownerEnded(observer); + }); + unsubscribeClose = this.subscribeClose(() => { + detach(); + observer.end(closeEndOf(observer, closeEnds(this.#failure))); + }); + return observer; + } + async #loadDocument(definition, addressId2, address, context) { + const gate = this.touch(documentKey(addressId2)); + if (gate !== void 0) + await gate; + const cached = this.#documents.get(addressId2); + if (cached?.valueVersion === definition.version) { + this.#touchDocument(addressId2); + return cached; + } + if (cached !== void 0) { + const id = cached.record.id; + await this.#storeHolds(() => { + const hold = this.#holds.get(id); + return hold === void 0 ? NO_HOLDS : [hold]; + }, context); + this.#dropDocument(addressId2); + } + const record = await this.#storage.findDocument(address, "current", context); + if (record === void 0) + return void 0; + const stored = await this.#storage.document(record.id, "current", context); + if (stored === void 0) + throw new Error(`Current document ${record.id} (${record.kind}) cannot be read`); + const value2 = materializeDocument(definition, stored); + const loaded = { + addressId: addressId2, + record: stored.record, + storedVersion: stored.version, + valueVersion: definition.version, + deltasSinceBase: stored.deltasSinceBase, + tracker: track(value2) + }; + this.#documents.set(addressId2, loaded); + this.#touchDocument(addressId2); + return loaded; + } + /** + * Run `job` on the line, after the jobs before it, and deliver its outcome in line order: once the jobs before it + * have theirs, and once its commit, if it admitted one, is published. With `exempt`, a job that admitted no commit and + * waited at no gate is delivered as soon as it has its outcome: it is equivalent to having run before every + * unpublished commit. A job + * whose outcome comes after the one a failure is gets `SessionFailed` instead. At most `MAX_OUTSTANDING` jobs wait + * for their delivery; beyond it the line waits. + */ + #enqueue(work, exempt = false) { + const job = { index: this.#nextIndex++, hit: false }; + const ran = this.#tail.then(async () => { + while (this.#outstanding >= MAX_OUTSTANDING) { + this.#room ??= Promise.withResolvers(); + await this.#room.promise; + } + this.#outstanding++; + return this.#onLine(job, work); + }); + this.#tail = ran.then(ignore, ignore); + const outcome = ran.then((result) => { + if (result.after === void 0) { + this.#ended(job); + return result.value; + } + return result.after.then(() => { + this.#ended(job); + return result.value; + }, (error) => { + this.#ended(job); + throw error; + }); + }, (error) => { + this.#ended(job); + throw error; + }); + outcome.catch(ignore); + const fence = this.#fence; + this.#fence = fence.then(() => outcome).then(ignore, ignore); + const index = job.index; + const deliver = (fenced) => (fenced ? fence.then(() => outcome) : outcome).then((value2) => { + this.#delivered(index, fenced); + if (this.#poisoned(index)) + throw new SessionFailed(this.#failure.error); + return value2; + }, (error) => { + this.#delivered(index, fenced); + throw this.#poisoned(index) ? new SessionFailed(this.#failure.error) : error; + }); + return exempt ? ran.then((result) => deliver(job.hit || result.after !== void 0), () => deliver(job.hit)) : deliver(true); + } + /** Whether job `index` comes after the failure's boundary without an outcome of its own: it gets `SessionFailed`. */ + #poisoned(index) { + return this.#failAt !== void 0 && index > this.#failAt && !this.#ownOutcomes.has(index); + } + /** The job's outcome settled: run what waited for it (`onJobEnd()`). */ + #ended(job) { + const ends = job.ends; + if (ends === void 0) + return; + job.ends = void 0; + for (const end of ends) + end(); + } + /** Run a job of the Session's own upkeep on the line: no outcome to deliver, so no fence and no `SessionFailed`. */ + #maintain(work) { + const job = { index: this.#nextIndex++, hit: false }; + const ran = this.#tail.then(() => this.#onLine(job, work)); + this.#tail = ran.then(ignore, ignore); + return ran; + } + /** Hold the line for job `index` until its line part, through admission, is done. Gates are its touches' own. */ + async #onLine(job, work) { + this.#current = job; + try { + return await work(job.index); + } finally { + this.#current = void 0; + } + } + /** A `*` barrier: once every part admitted so far is published, or never will be. */ + async #drained() { + const drained = this.#writer.drained(); + if (drained !== void 0) + await drained; + } + /** + * Run one commit's publication inside the Storage's own scope (`Storage.publishing`), such as a store host's batch; + * directly without one. `undefined` when it ran at once, else settles once it ran. A Storage that throws instead of + * running it is reported, and the publication runs directly: the commit is stored, so it must be published. + */ + #publishing(publish2, context) { + const publishing = this.#storage.publishing; + if (publishing === void 0) { + publish2(); + return void 0; + } + const done = Promise.withResolvers(); + let ran = false; + const once = () => { + if (ran) + return; + ran = true; + try { + publish2(); + } finally { + done.resolve(); + } + }; + try { + publishing(once, context); + } catch (error) { + this.report(error, LISTENER, context); + once(); + } + return done.promise; + } + /** Job `index` has its outcome: let the line go on past `MAX_OUTSTANDING`. */ + #delivered(index, fenced) { + if (fenced) + this.#lastDelivered = index; + this.#outstanding--; + const room = this.#room; + if (room !== void 0 && this.#outstanding < MAX_OUTSTANDING) { + this.#room = void 0; + room.resolve(); + } + } + /** Internal: throw `SessionFailed` when failed, else an error when closing. */ + assertUsable() { + this.#assertHealthy(); + if (this.#closing !== void 0) + throw new Error("Session is closed"); + } + #assertHealthy() { + if (this.#failure !== void 0) + throw new SessionFailed(this.#failure.error); + } +}; +function contained(callback, onError) { + try { + const result = callback(); + if (typeof result?.then === "function") + Promise.resolve(result).catch(onError); + } catch (error) { + onError(error); + } +} +function guardObserver(observer, detach, report2) { + let attached = { observer, detach, report: report2 }; + const release = () => { + const released = attached; + attached = void 0; + released?.detach(); + return released; + }; + const fault = (current, error, context) => { + current.report(error, context); + if (release() !== void 0) { + const end = { kind: "listener_error", error: toError2(error) }; + contained(() => current.observer.end(end), current.report); + } + }; + return { + advance: (call, context) => { + const current = attached; + if (current === void 0) + return; + try { + const result = current.observer.advance(call, context); + if (typeof result?.then === "function") { + Promise.resolve(result).catch((error) => fault(current, error, context)); + } + } catch (error) { + fault(current, error, context); + } + }, + end: (end) => { + const ending = release(); + if (ending !== void 0) + contained(() => ending.observer.end(end), ending.report); + }, + detach: () => { + release(); + } + }; +} +function toError2(error) { + if (error instanceof Error) + return error; + try { + return new Error(String(error)); + } catch { + return new Error(Object.prototype.toString.call(error)); + } +} +var ENDED2 = { kind: "ended" }; +function closeEnds(failure) { + if (failure !== void 0) { + const failed = { kind: "failed", error: failure.error }; + return { watch: failed, observer: failed }; + } + return { watch: ENDED2, observer: { kind: "failed", error: new Error("session closed") } }; +} +function closeEndOf(follower, ends) { + return follower instanceof Watch ? ends.watch : ends.observer; +} +function observation(value2, guarded) { + return { value: value2, detach: () => guarded.detach() }; +} +function createWatch(value2, detach, report2) { + return new Watch(value2, detach, report2); +} +function abortWatch(watch) { + watch.abort(); +} +function detachObservation(attached) { + attached.detach(); +} +function endWatch(watch) { + watch.endAfterDelivery(); +} +function endGuarded(guarded) { + guarded.end(ENDED2); +} +function atAddress(record, address) { + if (record.kind !== address.kind || record.key !== address.key) + return false; + switch (record.scope.kind) { + case "session": + return address.scope.kind === "session"; + case "conversation": + return address.scope.kind === "conversation" && address.scope.conversationId === record.scope.conversationId; + case "task": + return address.scope.kind === "task" && address.scope.taskId === record.scope.taskId; + } +} +function cancellationError(signal) { + return signal.reason instanceof Error ? signal.reason : new DOMException("The operation was aborted", "AbortError"); +} +var GuardedStorage = class { + #storage; + #latch; + /** Backend calls underway, also those off the Session line, which close waits for before it closes the backend. */ + #underway = /* @__PURE__ */ new Set(); + #closing = false; + constructor(storage, latch) { + this.#storage = storage; + this.#latch = latch; + } + /** The backend's own scope for a publication (`Storage.publishing`), when it has one; not a call to guard. */ + get publishing() { + return scopeOf(this.#storage); + } + /** + * Guarded like every call, but its outcome is the Session writer's to judge: it publishes the batches received first, + * then fails the Session for a refusal or a rejection, and for a Session that failed meanwhile. + */ + async commit(batches, receive, context) { + this.#assertHealthy(); + if (this.#closing) + throw new Error("Session is closed"); + const call = (async () => this.#storage.commit(batches, receive, context))(); + this.#underway.add(call); + try { + return await call; + } finally { + this.#underway.delete(call); + } + } + mintId() { + return this.#call(void 0, () => this.#storage.mintId()); + } + conversation(id, context) { + return this.#call(context, () => this.#storage.conversation(id, context)); + } + scanConversations(query, limit, cursor, context) { + return this.#call(context, () => this.#storage.scanConversations(query, limit, cursor, context)); + } + entry(first, second, third) { + if (third === void 0) { + const context = second; + return this.#call(context, () => this.#storage.entry(first, context)); + } + return this.#call(third, () => this.#storage.entry(first, second, third)); + } + findLatestHeadMarker(conversationId, atOrBeforeEntryId, context) { + return this.#call(context, () => this.#storage.findLatestHeadMarker(conversationId, atOrBeforeEntryId, context)); + } + scanEntries(query, limit, cursor, context) { + return this.#call(context, () => this.#storage.scanEntries(query, limit, cursor, context)); + } + task(id, context) { + return this.#call(context, () => this.#storage.task(id, context)); + } + scanTasks(query, limit, cursor, context) { + return this.#call(context, () => this.#storage.scanTasks(query, limit, cursor, context)); + } + submission(id, context) { + return this.#call(context, () => this.#storage.submission(id, context)); + } + scanSubmissions(query, limit, cursor, context) { + return this.#call(context, () => this.#storage.scanSubmissions(query, limit, cursor, context)); + } + submissionByRequest(conversationId, requestId, context) { + return this.#call(context, () => this.#storage.submissionByRequest(conversationId, requestId, context)); + } + findDocument(address, at, context) { + return this.#call(context, () => this.#storage.findDocument(address, at, context)); + } + document(id, at, context) { + return this.#call(context, () => this.#storage.document(id, at, context)); + } + scanDocuments(query, limit, cursor, context) { + return this.#call(context, () => this.#storage.scanDocuments(query, limit, cursor, context)); + } + /** + * Close the backend once every call underway has settled, so none outlives it or fails after `closed` settles. New + * calls are refused from here on: a paged read must not start its next page while the backend closes. + */ + async close(context) { + this.#closing = true; + await Promise.allSettled([...this.#underway]); + return this.#storage.close(context); + } + /** + * Run one backend call under the guard. `read` is a read's context; `commit()` and `mintId()` pass none, so nothing + * exempts their errors: once a batch is admitted, whether it committed is unknown. A call still underway when another + * fails the Session ends with `SessionFailed` too, whatever it returns: nothing it read or wrote is used. + */ + async #call(read2, run2) { + this.#assertHealthy(); + if (this.#closing) + throw new Error("Session is closed"); + let result; + const call = (async () => run2())(); + this.#underway.add(call); + try { + result = await call; + } catch (error) { + this.#assertHealthy(); + const exempt = read2 !== void 0 && (error instanceof StorageRequestError || read2.abortSignal?.aborted === true); + if (!exempt) + this.#latch.fail(error); + throw error; + } finally { + this.#underway.delete(call); + } + this.#assertHealthy(); + return result; + } + #assertHealthy() { + const failure = this.#latch.failure; + if (failure !== void 0) + throw new SessionFailed(failure.error); + } +}; + +// node_modules/@earendil-works/pi-durable/dist/harness/agent.js +var DEFAULT_RETRY_POLICY = { + enabled: true, + maxRetries: 3, + baseDelayMs: 2e3, + maxAgentDelayMs: 6e4 +}; +var DEFAULT_COMPACTION_POLICY = { + enabled: true, + reserveTokens: 16384, + keepRecentTokens: 2e4, + backgroundTokens: 32768 +}; +var DEFAULT_PROGRESS_POLICY = { + partialIntervalMs: 100, + outputIntervalMs: 100, + persistWithinMs: 1e3 +}; +var INSTRUCTIONS_KEY = "instructions"; +var AgentDoc = defineDoc({ + kind: "pi.agent", + version: 1, + scope: "conversation", + history: "rewindable", + fork: "asOf", + initial: () => ({}), + checkpointWhen: () => true +}); +var DEFAULT_CONTEXT_RETENTION_MS = 6e5; +var DEFAULT_RESTART_LIMIT = 5; +function restartLimit(limit) { + return typeof limit === "number" && limit >= 1 ? Math.floor(limit) : DEFAULT_RESTART_LIMIT; +} +function resolveSettings(settings) { + const extensions = settings?.extensions; + return { + ...extensions === void 0 ? {} : { extensions }, + stream: { ...settings?.stream }, + retry: { ...DEFAULT_RETRY_POLICY, ...settings?.retry }, + compaction: { ...DEFAULT_COMPACTION_POLICY, ...settings?.compaction }, + // Field by field, so an explicitly undefined interval keeps its default. + progress: { + partialIntervalMs: settings?.progress?.partialIntervalMs ?? DEFAULT_PROGRESS_POLICY.partialIntervalMs, + outputIntervalMs: settings?.progress?.outputIntervalMs ?? DEFAULT_PROGRESS_POLICY.outputIntervalMs, + persistWithinMs: persistWithin(settings?.progress?.persistWithinMs) + }, + toolExecution: settings?.toolExecution ?? "parallel", + steeringMode: settings?.steeringMode ?? "one-at-a-time", + followUpMode: settings?.followUpMode ?? "one-at-a-time", + contextRetentionMs: settings?.contextRetentionMs ?? DEFAULT_CONTEXT_RETENTION_MS, + documentRetentionMs: documentRetention(settings?.documentRetentionMs), + afterFailedRun: settings?.afterFailedRun ?? "keep", + restartLimit: restartLimit(settings?.restartLimit) + }; +} +async function configure(tx, conversationId, change) { + const state = await tx.doc(AgentDoc, conversationId); + applyChange(state, change); +} +async function addTools(tx, conversationId, added) { + const state = await tx.doc(AgentDoc, conversationId); + const tools = state.tools; + if (Array.isArray(tools)) { + for (const name of added) + if (!tools.includes(name)) + tools.push(name); + } else if (tools?.remove.some((name) => added.includes(name))) { + state.tools = { remove: tools.remove.filter((name) => !added.includes(name)) }; + } + const modelTools = state.modelTools; + if (Array.isArray(modelTools)) { + for (const name of added) + if (!modelTools.includes(name)) + modelTools.push(name); + return; + } + const remove = (modelTools?.remove ?? []).filter((name) => !added.includes(name)); + const add = [...modelTools?.add ?? []]; + for (const name of added) + if (!add.includes(name)) + add.push(name); + state.modelTools = { ...remove.length === 0 ? {} : { remove }, add }; +} +function applyChange(state, change) { + const set = (key, value2) => { + if (value2 === void 0) + return; + if (value2 === null) + delete state[key]; + else + state[key] = value2; + }; + set("model", change.model === void 0 || change.model === null ? change.model : { ...change.model }); + set("thinkingLevel", change.thinkingLevel); + const extensions = change.extensions; + set("extensions", extensions === void 0 || extensions === null ? extensions : isList(extensions) ? names(extensions) : { + ...extensions.add === void 0 ? {} : { add: names(extensions.add) }, + ...extensions.remove === void 0 ? {} : { remove: names(extensions.remove) } + }); + const tools = change.tools; + set("tools", tools === void 0 || tools === null ? tools : isList(tools) ? names(tools) : { remove: names(tools.remove) }); + const modelTools = change.modelTools; + set("modelTools", modelTools === void 0 || modelTools === null ? modelTools : isList(modelTools) ? names(modelTools) : { + ...modelTools.remove === void 0 ? {} : { remove: names(modelTools.remove) }, + ...modelTools.add === void 0 ? {} : { add: names(modelTools.add) } + }); + set("instructions", change.instructions); + set("cwd", change.cwd); + const maxTurns = change.maxTurns; + if (maxTurns !== void 0 && maxTurns !== null && !(Number.isSafeInteger(maxTurns) && maxTurns >= 1)) { + throw new RangeError(`maxTurns must be an integer 1 or more, got ${String(maxTurns)}`); + } + set("maxTurns", maxTurns); +} +function isList(value2) { + return Array.isArray(value2); +} +function names(items) { + return items.map((item) => item.name); +} +async function createAgent(tx, conversation) { + if (conversation.parent !== void 0) + return; + const agent2 = await tx.doc(AgentDoc, conversation.id); + if (conversation.owner === void 0) + return; + const owner = await tx.doc(AgentDoc, conversation.owner.conversationId); + Object.assign(agent2, copyJson(owner)); +} +function agentHooks(agent2, taskName) { + const handlers = []; + for (const extension of agent2.extensions) { + for (const hook2 of extension.hooks ?? []) { + if (hook2.task === taskName) + handlers.push({ extension: extension.name, handlers: hook2.handlers }); + } + } + return handlers; +} +function sectionExtension(agent2, key) { + let found; + for (const extension of agent2.extensions) { + const provides = extension.sections?.some((section2) => section2.key === key) === true; + const wraps = extension.wraps?.some((wrap) => "section" in wrap && wrap.section === key) === true; + if (provides || wraps) + found = extension.name; + } + return found; +} +function toolExtension(agent2, name) { + let found; + for (const extension of agent2.extensions) { + const provides = extension.tools?.some((tool) => tool.name === name) === true; + const wraps = extension.wraps?.some((wrap) => "tool" in wrap && wrap.tool === name) === true; + if (provides || wraps) + found = extension.name; + } + return found; +} +function resolveAgent(state, snapshot, settings, report2) { + const extensions = selectExtensions(state?.extensions, snapshot, settings); + const composed = /* @__PURE__ */ new Map(); + for (const extension of extensions) + for (const tool of extension.tools ?? []) + composed.set(tool.name, tool); + const sections = /* @__PURE__ */ new Map(); + for (const extension of extensions) { + for (const section2 of extension.sections ?? []) + sections.set(section2.key, section2); + } + for (const extension of extensions) { + for (const wrap of extension.wraps ?? []) { + const wrapReport = (error) => report2(error, extension.name); + if ("tool" in wrap) + applyWrap(composed, wrap.tool, (tool) => wrap.wrap(tool), (tool) => tool.name, wrapReport); + else + applyWrap(sections, wrap.section, (section2) => wrap.wrap(section2), (section2) => section2.key, wrapReport); + } + } + const enabled = filterTools([...composed.values()], state?.tools); + const callableBy = (caller) => (tool) => tool.callers === void 0 || tool.callers.includes(caller); + const modelCallable = enabled.filter(callableBy("model")); + const tools = offeredTools(modelCallable, state?.modelTools); + const offered = new Set(tools); + const deferred = modelCallable.filter((tool) => tool.deferred === true && !offered.has(tool)); + const callable = enabled.filter(callableBy("tools")); + const instructions = state?.instructions; + const agentSections = [...sections.values()]; + if (instructions !== void 0) + agentSections.push({ key: INSTRUCTIONS_KEY, render: () => instructions }); + const agent2 = { + ...state?.model === void 0 ? {} : { model: state.model }, + thinkingLevel: state?.thinkingLevel ?? "off", + extensions, + tools, + callable, + deferred, + sections: agentSections, + ...instructions === void 0 ? {} : { instructions }, + ...state?.cwd === void 0 ? {} : { cwd: state.cwd }, + ...state?.maxTurns === void 0 ? {} : { maxTurns: state.maxTurns } + }; + return agent2; +} +function offeredTools(tools, filter) { + if (Array.isArray(filter)) + return filterTools(tools, filter); + const removed = new Set(filter?.remove ?? []); + const kept = tools.filter((tool) => tool.deferred !== true && !removed.has(tool.name)); + const byName = new Map(tools.map((tool) => [tool.name, tool])); + const loaded = [...new Set(filter?.add ?? [])].flatMap((name) => { + const tool = byName.get(name); + return tool?.deferred === true && !removed.has(name) ? [tool] : []; + }); + return [...kept, ...loaded]; +} +function filterTools(tools, filter) { + if (filter === void 0) + return tools; + if (Array.isArray(filter)) { + const byName = new Map(tools.map((tool) => [tool.name, tool])); + return [...new Set(filter)].flatMap((name) => { + const tool = byName.get(name); + return tool === void 0 ? [] : [tool]; + }); + } + const removed = new Set(filter.remove); + return tools.filter((tool) => !removed.has(tool.name)); +} +function selectExtensions(stored, snapshot, settings) { + let selected; + if (Array.isArray(stored)) + selected = stored; + else { + const base = settings.extensions?.map((extension) => extension.name) ?? snapshot.installed().map((e) => e.name); + const edit = stored; + const removed = new Set(edit?.remove ?? []); + selected = [...base, ...edit?.add ?? []].filter((name) => !removed.has(name)); + } + const extensions = []; + for (const name of new Set(selected)) { + const extension = snapshot.extension(name); + if (extension !== void 0) + extensions.push(extension); + } + return extensions; +} +function applyWrap(items, target, wrap, nameOf, report2) { + const item = items.get(target); + if (item === void 0) + return; + try { + const wrapped = wrap(item); + if (nameOf(wrapped) !== target) + throw new Error(`Wrapper renamed ${target} to ${nameOf(wrapped)}`); + items.set(target, wrapped); + } catch (error) { + items.delete(target); + report2(error); + } +} + +// node_modules/@earendil-works/pi-ai/dist/utils/estimate.js +var CHARS_PER_TOKEN = 3.5; +var ESTIMATED_IMAGE_CHARS = 4800; +function calculateContextTokens(usage) { + return usage.totalTokens || usage.input + usage.output + usage.cacheRead + usage.cacheWrite; +} +function safeJsonStringify(value2) { + try { + return JSON.stringify(value2) ?? "undefined"; + } catch { + return "[unserializable]"; + } +} +function estimateTextAndImageContentChars(content) { + if (typeof content === "string") + return content.length; + let chars = 0; + for (const block of content) + chars += block.type === "text" ? block.text.length : ESTIMATED_IMAGE_CHARS; + return chars; +} +function estimateTextTokens(text) { + return Math.ceil(text.length / CHARS_PER_TOKEN); +} +function estimateTextAndImageContentTokens(content) { + return Math.ceil(estimateTextAndImageContentChars(content) / CHARS_PER_TOKEN); +} +function estimateMessageTokens(message) { + let chars = 0; + if (message.role === "system") { + return estimateTextTokens(getSystemMessageText(message)) + estimateToolsTokens(message.toolsAdded) + estimateToolsTokens(message.toolsRemoved); + } + if (message.role === "user") + return estimateTextAndImageContentTokens(message.content); + if (message.role === "toolResult") + return estimateTextAndImageContentTokens(message.content); + for (const block of message.content) { + if (block.type === "text") { + chars += block.text.length; + } else if (block.type === "thinking") { + chars += block.thinking.length; + } else { + chars += block.name.length + safeJsonStringify(block.arguments).length; + } + } + return Math.ceil(chars / CHARS_PER_TOKEN); +} +function estimateToolsTokens(tools) { + if (!tools || tools.length === 0) + return 0; + return estimateTextTokens(safeJsonStringify(tools)); +} + +// node_modules/@earendil-works/pi-ai/dist/utils/retry.js +function buildProviderErrorPattern(patterns) { + return new RegExp(patterns.join("|"), "i"); +} +var NON_RETRYABLE_PROVIDER_LIMIT_ERROR_PATTERN = buildProviderErrorPattern([ + // OpenCode Go/free-tier limits returned as 429 JSON error types by OpenCode's + // Zen API. These are subscription/account limits, not transient throttles. + "GoUsageLimitError", + "FreeUsageLimitError", + // OpenCode Go subscription-limit text asks users to enable available-balance + // usage after rolling/weekly/monthly limits are reached. + "Monthly usage limit reached", + "available balance", + // Generic quota/budget/billing exhaustion. `insufficient_quota` is OpenAI's + // quota/billing error code; the other strings cover common gateway wording. + "insufficient_quota", + "out of budget", + "quota exceeded", + "billing", + // Sign in with ChatGPT: the subscription's shared usage limit, which resets + // after hours rather than seconds. + "subscription_sharing_usage_limit_exceeded", + // Sign in with ChatGPT on a ChatGPT Business plan: a usage limit set by the + // workspace admin. Only the admin can raise it. + "subscription_sharing_workplace_individual_usage_limit_exceeded", + // Zhipu/Z.ai (open.bigmodel.cn) error code 1308: the plan's usage limit is + // reached, sent as HTTP 429. It resets after hours, not seconds (#11537). + '"code"\\s*:\\s*"?1308(?![0-9])' +]); +var RETRYABLE_PROVIDER_ERROR_PATTERN = buildProviderErrorPattern([ + // Generic provider load, HTTP status, and server-side transient failures. + "overloaded", + "server_busy", + "servers are currently busy", + "currently experiencing high demand", + "model is at capacity", + "rate.?limit", + "too many requests", + // Status codes must not be part of a longer word or number, e.g. a request id or byte count (#11057). + "(? captureContextBounds(session, line, conversationId, context, at, visible)); + if (bounds === void 0) + return { view: emptyView(), range: void 0 }; + freezeJson(bounds.head); + const current = at === void 0; + let range; + if (previous === void 0 || previous.bounds.head?.id !== bounds.head?.id) { + const entries2 = freezeJson(await scanRange(storage, conversationId, rangeQuery(bounds), context)); + range = deriveRange(bounds, entries2, current); + } else if (bounds.tail === previous.bounds.tail) { + range = previous; + } else if (bounds.tail < previous.bounds.tail) { + range = deriveRange(bounds, previous.entries.filter((entry2) => entry2.id <= bounds.tail), false); + } else { + const minEntryId = idFromNumber(previous.bounds.tail + 1); + const added = await scanRange(storage, conversationId, { minEntryId, maxEntryId: bounds.tail }, context); + range = extendRange(previous, bounds, freezeJson(added), current); + } + if (range.current && session.knownLatestEntryId(conversationId) !== range.bounds.tail) { + range = { ...range, current: false }; + } + const { view: view2 } = range; + return { + view: { + ...view2, + entries: [...view2.entries], + contributions: [...view2.contributions], + messages: [...view2.messages] + }, + range + }; +} +async function readContext(session, storage, conversationId, context, at) { + const bounds = await session.readOnLine((line) => captureContextBounds(session, line, conversationId, context, at)); + return deriveContext(storage, conversationId, bounds, context); +} +async function deriveContext(storage, conversationId, bounds, context) { + if (bounds === void 0) + return emptyView(); + return deriveRange(bounds, await scanRange(storage, conversationId, rangeQuery(bounds), context), false).view; +} +function emptyView() { + return { head: void 0, entries: [], contributions: [], messages: [] }; +} +function rangeEntry(range, id) { + const entries2 = range.entries; + let low = 0; + let high = entries2.length - 1; + while (low <= high) { + const middle = low + high >>> 1; + const entry2 = entries2[middle]; + if (entry2.id === id) + return entry2; + if (entry2.id < id) + low = middle + 1; + else + high = middle - 1; + } + return void 0; +} +function advanceRange(range, appended) { + if (!range.current || appended.length === 0) + return range; + const added = freezeJson(appended); + let head = range.bounds.head; + for (const entry2 of added) + if (entry2.head !== void 0) + head = entry2; + const bounds = { head, tail: added.at(-1).id }; + if (head?.id === range.bounds.head?.id) + return extendRange(range, bounds, added, true); + const start = head.head; + if (range.bounds.head !== void 0 && start < range.bounds.head.head) + return void 0; + const entries2 = [...range.entries, ...added].filter((entry2) => entry2.id >= start); + return deriveRange(bounds, entries2, true); +} +function deriveRange(bounds, entries2, current) { + const edits = /* @__PURE__ */ new Map(); + for (const entry2 of entries2) + for (const edit of entry2.edits ?? []) + edits.set(edit.target, edit); + const active = selectActive(bounds.head, entries2); + const contributions = active.map((entry2) => contribute(entry2, edits.get(entry2.id))); + const { settled, open } = settle([], contributions.flat()); + const baseline = findBaseline(bounds.head, active); + const messages = leadWithSystem([...settled, ...orderToolResults(open)], baseline); + const view2 = { head: bounds.head, entries: active, contributions, messages }; + return { bounds, entries: entries2, view: view2, edited: new Set(edits.keys()), settled, open, baseline, current }; +} +function extendRange(previous, bounds, added, current) { + const entries2 = [...previous.entries, ...added]; + if (added.some((entry2) => entry2.edits !== void 0 || entry2.head !== void 0 || previous.edited.has(entry2.id))) { + return deriveRange(bounds, entries2, current); + } + const contributions = added.map((entry2) => contribute(entry2, void 0)); + const { settled, open } = settle(previous.settled, [...previous.open, ...contributions.flat()]); + const baseline = previous.baseline !== void 0 ? previous.baseline : findBaseline(bounds.head, added); + const view2 = { + head: bounds.head, + entries: [...previous.view.entries, ...added], + contributions: [...previous.view.contributions, ...contributions], + messages: leadWithSystem([...settled, ...orderToolResults(open)], baseline) + }; + return { bounds, entries: entries2, view: view2, edited: previous.edited, settled, open, baseline, current }; +} +function findBaseline(head, entries2) { + for (const entry2 of entries2) { + if (head !== void 0 && entry2.id <= head.id) + continue; + if (SystemEntry.is(entry2)) + return entry2.model?.find((message) => message.role === "system") ?? null; + if (head === void 0 && carriesSystemMessage(entry2)) + return null; + } + return void 0; +} +function carriesSystemMessage(entry2) { + const isSystem = (message) => message.role === "system"; + return (entry2.model?.some(isSystem) ?? false) || (entry2.edits?.some((edit) => edit.action === "replace" && edit.messages.some(isSystem)) ?? false); +} +function leadWithSystem(messages, baseline) { + const index = messages.findIndex((message) => message.role === "system"); + if (index <= 0) + return messages; + const system = messages[index]; + if (system !== baseline && messages.slice(0, index).some((message) => message.role !== "user")) { + return messages; + } + return [system, ...messages.slice(0, index), ...messages.slice(index + 1)]; +} +function contribute(entry2, edit) { + if (edit?.action === "omit") + return Object.freeze([]); + const contributed = edit?.action === "replace" ? edit.messages : entry2.model ?? []; + return Object.freeze(contributed.filter((message) => message.role !== "assistant" || !EXCLUDED_STOP_REASONS.has(message.stopReason))); +} +var deeplyFrozen = /* @__PURE__ */ new WeakSet(); +function freezeJson(value2) { + if (value2 === null || typeof value2 !== "object" || deeplyFrozen.has(value2)) + return value2; + for (const child of Object.values(value2)) + freezeJson(child); + Object.freeze(value2); + deeplyFrozen.add(value2); + return value2; +} +function settle(settled, open) { + const last = open.findLastIndex((message) => message.role === "assistant"); + if (last <= 0) + return { settled, open }; + return { settled: [...settled, ...orderToolResults(open.slice(0, last))], open: open.slice(last) }; +} +function rangeQuery(bounds) { + const head = bounds.head; + return head === void 0 ? { maxEntryId: bounds.tail } : { minEntryId: head.head, maxEntryId: bounds.tail }; +} +async function scanRange(storage, conversationId, range, context) { + const entries2 = []; + let cursor; + do { + const page2 = await storage.scanEntries({ conversationId, ...range }, SCAN_PAGE_SIZE2, cursor, context); + entries2.push(...page2.items); + cursor = page2.next; + } while (cursor !== void 0); + return entries2.reverse(); +} +function selectActive(head, range) { + return head === void 0 ? range : [head, ...range.filter((entry2) => entry2.head === void 0)]; +} +function orderToolResults(messages) { + const ordered = []; + for (let index = 0; index < messages.length; index++) { + const message = messages[index]; + if (message.role === "toolResult") + continue; + ordered.push(message); + if (message.role !== "assistant") + continue; + const calls = message.content.filter((content) => content.type === "toolCall"); + if (calls.length === 0) + continue; + const results = /* @__PURE__ */ new Map(); + for (let next = index + 1; next < messages.length && messages[next].role !== "assistant"; next++) { + const candidate = messages[next]; + if (candidate.role === "toolResult" && !results.has(candidate.toolCallId)) { + results.set(candidate.toolCallId, next); + } + } + for (const call of calls) { + const resultIndex = results.get(call.id); + ordered.push(resultIndex === void 0 ? missingResult(call, message.timestamp) : messages[resultIndex]); + } + } + return ordered; +} +function missingResult(call, timestamp) { + return freezeJson({ + role: "toolResult", + toolCallId: call.id, + toolName: call.name, + content: [{ type: "text", text: MISSING_RESULT_TEXT }], + isError: true, + details: { reason: "missing_result" }, + timestamp + }); +} + +// node_modules/@earendil-works/pi-ai/dist/utils/overflow.js +var OVERFLOW_PATTERNS = [ + /prompt (?:is )?too long/i, + // Anthropic and z.ai token overflow + /prompt exceeds max length/i, + // z.ai CN endpoint token overflow + /request_too_large/i, + // Anthropic request byte-size overflow (HTTP 413) + /input is too long for requested model/i, + // Amazon Bedrock + /exceeds the context window/i, + // OpenAI (Completions & Responses API) + /exceeds (?:the )?(?:model'?s )?maximum context length(?: of [\d,]+ tokens?|\s*\([\d,]+\))/i, + // OpenAI-compatible proxies (LiteLLM) + /context window exceeded/i, + // LiteLLM pre-call check (ContextWindowExceededError) + /input token count.*exceeds the maximum/i, + // Google (Gemini) + /maximum prompt length is \d+/i, + // xAI (Grok) + /reduce the length of the messages/i, + // Groq + /maximum context length is \d+ tokens/i, + // OpenRouter (most backends) + /exceeds (?:the )?maximum allowed input length of [\d,]+ tokens?/i, + // OpenRouter/Poolside + /input \(\d+ tokens\) is longer than the model'?s context length \(\d+ tokens\)/i, + // Together AI + /exceeds the limit of \d+/i, + // GitHub Copilot + /exceeds the available context size/i, + // llama.cpp server + /greater than the context length/i, + // LM Studio + /context window exceeds limit/i, + // MiniMax + /exceeded model token limit/i, + // Kimi For Coding + /too large for model with \d+ maximum context length/i, + // Mistral + /prompt has [\d,]+ tokens?, but the configured context size is [\d,]+ tokens?/i, + // DS4 server + /model_context_window_exceeded/i, + // z.ai non-standard finish_reason surfaced as error text + /prompt too long; exceeded (?:max )?context length/i, + // Ollama explicit overflow error + /range of input length should be/i, + // DashScope / Qwen Token Plan + /context[_ ]length[_ ]exceeded/i, + // Generic fallback + /too many tokens/i, + // Generic fallback + /token limit exceeded/i, + // Generic fallback + // HTTP 413 means request-size overflow regardless of provider or body text. + // Match raw SDK errors and formatProviderError's prefix, not a status mentioned in the body. + /^(?:[\w -]+ API error \(413\):|413(?:\s|:|$))/i +]; +var CEREBRAS_BODYLESS_OVERFLOW_PATTERN = /^4(?:00|13)\s*(?:status code)?\s*\(no body\)/i; +var NON_OVERFLOW_PATTERNS = [ + /^(Throttling error|Service unavailable):/i, + // AWS Bedrock non-overflow errors (human-readable prefixes from formatBedrockError) + /rate limit/i, + // Generic rate limiting + /too many requests/i + // Generic HTTP 429 style +]; +function isContextOverflow(message, contextWindow) { + if (message.stopReason === "error" && message.errorMessage) { + const isNonOverflow = NON_OVERFLOW_PATTERNS.some((p) => p.test(message.errorMessage)); + if (!isNonOverflow) { + if (OVERFLOW_PATTERNS.some((p) => p.test(message.errorMessage))) { + return true; + } + if (message.provider === "cerebras" && CEREBRAS_BODYLESS_OVERFLOW_PATTERN.test(message.errorMessage)) { + return true; + } + } + } + if (contextWindow && message.stopReason === "stop") { + const inputTokens = message.usage.input + message.usage.cacheRead; + if (inputTokens > contextWindow) { + return true; + } + } + if (contextWindow && message.stopReason === "length" && message.usage.output === 0) { + const inputTokens = message.usage.input + message.usage.cacheRead; + if (inputTokens >= contextWindow * 0.99) { + return true; + } + } + return false; +} + +// node_modules/@earendil-works/pi-durable/dist/harness/inbox.js +var InboxDoc = defineDoc({ + kind: "pi.inbox", + version: 1, + scope: "conversation", + history: "latest", + fork: "initial", + initial: () => ({ items: [] }), + checkpointWhen: (value2) => value2.items.length === 0 +}); +async function prepareBoundary(tx, conversationId, modes, at, now, incoming) { + const head = (await tx.latestHeadMarker(conversationId))?.head; + const inbox = await tx.doc(InboxDoc, conversationId); + const first = inbox.items.find((item) => item.mode === "steer" || item.mode === "followUp"); + const input = first === void 0 ? incoming : inputDraft(first.content, first.entry, now); + const entries2 = input !== void 0 && (at === "final" || hasQueuedReset(inbox.items)) ? await tx.beforeInput(conversationId, input) : []; + return { + entries: entries2, + conversationId, + inbox, + steeringMode: modes.steeringMode, + followUpMode: modes.followUpMode, + head + }; +} +async function applyBoundary(tx, boundary, at, now) { + const { conversationId, inbox } = boundary; + const items = inbox.items; + const reset = hasQueuedReset(items); + const final = at === "final" || reset; + const pick = (mode, queueMode) => { + const indexes = items.flatMap((item, index) => item.mode === mode ? [index] : []); + return queueMode === "all" ? indexes : indexes.slice(0, 1); + }; + const writes = items.flatMap((item, index) => item.mode === "write" ? [index] : []); + const users = [...pick("steer", boundary.steeringMode), ...final ? pick("followUp", boundary.followUpMode) : []]; + users.sort((a, b) => a - b); + let first; + for (const index of writes) { + const item = items[index]; + const draft = item.entry; + if (isStale(boundary, draft)) { + tx.settleSubmission(item.id, { status: "unanswered", reason: "stale" }); + continue; + } + const entry2 = await tx.appendEntry(conversationId, draft); + if (draft.head !== void 0) + boundary.head = draft.head === "self" ? entry2.id : draft.head; + tx.placeSubmission(item.id, entry2.id); + first ??= entry2.id; + } + const placed = []; + for (const index of users) { + if (placed.length === 0) + for (const draft of boundary.entries) + await tx.appendEntry(conversationId, draft); + const item = items[index]; + const entry2 = await appendInput(tx, conversationId, item.content, item.entry, now); + tx.placeSubmission(item.id, entry2.id); + placed.push(item.id); + first ??= entry2.id; + } + const removed = [...writes, ...users].sort((a, b) => b - a); + for (const index of removed) + items.splice(index, 1); + return { users: placed, first, reset }; +} +function appendInput(tx, conversationId, content, entry2, now) { + return tx.appendEntry(conversationId, inputDraft(content, entry2, now)); +} +function inputDraft(content, entry2, now) { + const message = { role: "user", content, timestamp: now }; + return { ...entry2 === void 0 ? { kind: UserEntry.kind } : pickInputEntry(entry2), input: true, model: [message] }; +} +function pickInputEntry({ kind, data }) { + const picked = data === void 0 ? { kind } : { kind, data }; + return copyJson(picked, { omitUndefinedProperties: true }); +} +function isDisplayOnly(entry2) { + return entry2.model === void 0 && entry2.head === void 0 && entry2.edits === void 0; +} +function hasQueuedReset(items) { + return items.some((item) => item.mode === "write" && item.entry.head === "self"); +} +function isStale(boundary, entry2) { + return typeof entry2.head === "number" && boundary.head !== void 0 && entry2.head < boundary.head; +} +async function removeInboxItem(tx, conversationId, id) { + const items = (await tx.doc(InboxDoc, conversationId)).items; + const index = items.findIndex((item) => item.id === id); + if (index >= 0) + items.splice(index, 1); +} +async function withdrawQueuedInputs(tx, conversationId, reason = "aborted") { + const items = (await tx.doc(InboxDoc, conversationId)).items; + for (let index = items.length - 1; index >= 0; index--) { + const item = items[index]; + if (item.mode === "write") + continue; + tx.settleSubmission(item.id, { status: "unanswered", reason }); + items.splice(index, 1); + } +} + +// node_modules/@earendil-works/pi-durable/dist/harness/json.js +function assignJson(target, key, value2, plan) { + if (plan?.kind === "keep") + return; + const slots = target; + const current = key === "__proto__" && !Object.hasOwn(slots, key) ? void 0 : slots[key]; + const fields = plan?.kind === "fields" ? plan.fields : void 0; + if (isRecord(current) && isRecord(value2)) { + for (const name of Object.keys(current)) + if (!Object.hasOwn(value2, name)) + delete current[name]; + for (const [name, child] of Object.entries(value2)) + assignJson(current, name, child, fields?.get(name)); + return; + } + if (Array.isArray(current) && Array.isArray(value2) && current.length <= value2.length) { + const items = current; + for (let index = 0; index < value2.length; index++) { + if (index < items.length) + assignJson(items, index, value2[index], fields?.get(index)); + else + items.push(value2[index]); + } + return; + } + if (plan?.kind === "append" && typeof current === "string" && typeof value2 === "string" && !Array.isArray(target) && current.length + plan.text.length === value2.length) { + appendText(target, String(key), plan.text); + return; + } + if (current !== value2) + slots[key] = value2; +} +function isRecord(value2) { + return typeof value2 === "object" && value2 !== null && !Array.isArray(value2); +} + +// node_modules/@earendil-works/pi-durable/dist/harness/partial-changes.js +var PartialChanges = class { + #text = /* @__PURE__ */ new Map(); + #toolCalls = /* @__PURE__ */ new Map(); + /** Record what `event` changed. */ + observe(event) { + switch (event.type) { + case "text_delta": + case "thinking_delta": { + const field = event.type === "text_delta" ? "text" : "thinking"; + const pending = this.#text.get(event.contentIndex); + if (pending?.field === field) + pending.text += event.delta; + else + this.#text.set(event.contentIndex, { field, text: event.delta }); + return; + } + case "toolcall_start": + this.#toolCalls.set(event.contentIndex, { + parser: new StreamingJsonParser(), + changes: [], + committed: false + }); + return; + case "toolcall_delta": { + const call = this.#toolCalls.get(event.contentIndex); + if (call === void 0) + return; + for (const change of call.parser.append(event.delta)) { + if (change.path.length === 0 && !isRecord2(change.kind === "set" ? value(change) : null)) { + this.#toolCalls.delete(event.contentIndex); + return; + } + call.changes.push(change); + } + return; + } + case "text_end": + case "thinking_end": + this.#text.delete(event.contentIndex); + return; + case "toolcall_end": + this.#toolCalls.delete(event.contentIndex); + return; + } + } + /** + * Take what changed since the last call, for a commit of `message`, a copy of the partial. Clears the arguments of + * tracked tool calls in `message`: the commit writes them from the changes. Call `restore()` if the commit fails. + */ + take(message) { + const blocks = /* @__PURE__ */ new Map(); + for (const [index, { field, text }] of this.#text) { + blocks.set(index, { kind: "fields", fields: /* @__PURE__ */ new Map([[field, { kind: "append", text }]]) }); + } + this.#text = /* @__PURE__ */ new Map(); + const toolCalls = /* @__PURE__ */ new Map(); + for (const [index, call] of this.#toolCalls) { + const block = message.content[index]; + if (block?.type !== "toolCall") + continue; + toolCalls.set(index, { changes: call.changes, snapshot: block.arguments }); + block.arguments = {}; + blocks.set(index, { kind: "fields", fields: /* @__PURE__ */ new Map([["arguments", { kind: "keep" }]]) }); + call.changes = []; + } + return { + plan: { kind: "fields", fields: /* @__PURE__ */ new Map([["content", { kind: "fields", fields: blocks }]]) }, + toolCalls + }; + } + /** + * Write `message` with what `commit` took into `generation.message`: the message leaf by leaf with the plan, then the + * argument changes. A tracked call whose block is new here gets its arguments from its changes, which then start at + * its first piece; when the committed message lost a call whose changes were already committed (another writer reset + * it), the call falls back to its snapshot from now on. + */ + commit(generation, message, commit) { + const committedBlocks = generation.message?.content.length ?? 0; + const apply2 = []; + for (const [index, { changes, snapshot }] of commit.toolCalls) { + const call = this.#toolCalls.get(index); + if (call === void 0) + continue; + const block = message.content[index]; + if (index >= committedBlocks && call.committed && block?.type === "toolCall") { + this.#toolCalls.delete(index); + block.arguments = snapshot; + continue; + } + call.committed = true; + apply2.push([index, changes]); + } + assignJson(generation, "message", message, commit.plan); + const content = generation.message?.content ?? []; + for (const [index, changes] of apply2) { + const block = content[index]; + if (block?.type !== "toolCall") + continue; + for (const change of changes) + applyChange2(block, change); + } + } + /** Put back what a failed commit took, so the next commit applies it first. */ + restore(commit) { + for (const [index, { changes }] of commit.toolCalls) { + const call = this.#toolCalls.get(index); + if (call !== void 0) + call.changes = [...changes, ...call.changes]; + } + } +}; +function applyChange2(block, change) { + const key = change.path.at(-1); + if (key === void 0) { + const root2 = change.kind === "set" ? value(change) : void 0; + if (isRecord2(root2)) + block.arguments = root2; + return; + } + let parent = block.arguments; + for (const segment of change.path.slice(0, -1)) { + parent = parent[segment]; + } + const slots = parent; + if (change.kind === "set") + slots[key] = value(change); + else if (Array.isArray(parent)) + slots[key] = `${slots[key]}${change.text}`; + else + appendText(parent, String(key), change.text); +} +function value(change) { + return change.value; +} +function isRecord2(value2) { + return typeof value2 === "object" && value2 !== null && !Array.isArray(value2); +} + +// node_modules/@earendil-works/pi-durable/dist/harness/prompt.js +function replaySections(messages) { + const shown = /* @__PURE__ */ new Map(); + for (const message of messages) { + if (message.role !== "system" || message.sections === void 0) + continue; + for (const [key, value2] of Object.entries(message.sections)) { + if (value2 === null) + shown.delete(key); + else + shown.set(key, value2); + } + } + return shown; +} +async function renderSections(sections, input, shown, report2, context) { + const desired = /* @__PURE__ */ new Map(); + for (const section2 of sections) { + let text; + try { + text = await section2.render(input, context); + } catch (error) { + if (context.abortSignal?.aborted) + throw error; + report2(error, section2); + const kept = shown.get(section2.key); + if (kept !== void 0) + desired.set(section2.key, kept); + continue; + } + if (text === void 0) + continue; + desired.set(section2.key, section2.tag === false ? text : `<${section2.key}> +${text} +`); + } + return desired; +} +function planSystemEntries(view2, desired, tools, timestamp) { + const head = view2.head; + if (head !== void 0 && !view2.entries.some((entry2) => SystemEntry.is(entry2) && entry2.id > head.id)) { + const edits = view2.entries.filter((entry2) => SystemEntry.is(entry2)).map((entry2) => ({ target: entry2.id, action: "omit" })); + const baseline = systemEntry(Object.fromEntries(desired), { toolsRemoved: [], toolsAdded: tools.map(toToolDeclaration) }, timestamp); + return [edits.length === 0 ? baseline : { ...baseline, edits }]; + } + const sections = planSections(replaySections(view2.messages), desired); + const changes = planTools(getCurrentTools(view2.messages), tools); + if (changes.toolsRemoved.length === 0 && changes.toolsAdded.length === 0) { + return sections.map((patch) => systemEntry(patch, void 0, timestamp)); + } + if (sections.length === 0) + return [systemEntry(void 0, changes, timestamp)]; + return sections.map((patch, index) => systemEntry(patch, index === sections.length - 1 ? changes : void 0, timestamp)); +} +function planTools(offered, desired) { + const wanted = new Map(desired.map((tool) => [tool.name, tool])); + const kept = offered.filter((tool) => { + const next = wanted.get(tool.name); + return next !== void 0 && declarationsEqual(tool, next); + }); + const keptNames = new Set(kept.map((tool) => tool.name)); + const added = desired.filter((tool) => !keptNames.has(tool.name)); + const replayed = [...kept, ...added]; + if (replayed.some((tool, index) => tool.name !== desired[index].name)) { + return { + toolsRemoved: offered.map((tool) => ({ name: tool.name })), + toolsAdded: desired.map(toToolDeclaration) + }; + } + return { + toolsRemoved: offered.filter((tool) => !keptNames.has(tool.name)).map((tool) => ({ name: tool.name })), + toolsAdded: added.map(toToolDeclaration) + }; +} +function planSections(shown, desired) { + const patchedOrder = [ + ...[...shown.keys()].filter((key) => desired.has(key)), + ...[...desired.keys()].filter((key) => !shown.has(key)) + ]; + const desiredOrder = [...desired.keys()]; + if (patchedOrder.some((key, index) => key !== desiredOrder[index])) { + return [Object.fromEntries([...shown.keys()].map((key) => [key, null])), Object.fromEntries(desired)]; + } + const patch = {}; + for (const [key, value2] of shown) { + const next = desired.get(key); + if (next !== value2) + patch[key] = next ?? null; + } + for (const [key, value2] of desired) + if (!shown.has(key)) + patch[key] = value2; + return Object.keys(patch).length === 0 ? [] : [patch]; +} +function systemEntry(sections, tools, timestamp) { + const message = { + role: "system", + content: "", + ...sections === void 0 ? {} : { sections }, + ...tools === void 0 || tools.toolsRemoved.length === 0 ? {} : { toolsRemoved: tools.toolsRemoved }, + ...tools === void 0 || tools.toolsAdded.length === 0 ? {} : { toolsAdded: tools.toolsAdded }, + timestamp + }; + return { model: [message] }; +} + +// node_modules/@earendil-works/pi-ai/dist/utils/uuid.js +var MAX_UUID_V7_TIMESTAMP = 281474976710655; +var MAX_SEQUENCE = (1n << 41n) - 1n; +var lastOrdinaryTimestamp = -1; +var sequence; +function uuidv7(timestampMs) { + const requestedTimestamp = timestampMs ?? Date.now(); + if (!Number.isInteger(requestedTimestamp) || requestedTimestamp < 0 || requestedTimestamp > MAX_UUID_V7_TIMESTAMP) { + throw new RangeError(`UUIDv7 timestamp must be an integer between 0 and ${MAX_UUID_V7_TIMESTAMP}`); + } + const effectiveTimestamp = timestampMs === void 0 ? Math.max(requestedTimestamp, lastOrdinaryTimestamp) : timestampMs; + if (timestampMs === void 0) + lastOrdinaryTimestamp = effectiveTimestamp; + const bytes2 = new Uint8Array(16); + globalThis.crypto.getRandomValues(bytes2); + if (sequence === void 0) { + sequence = BigInt(bytes2[1]) << 32n | BigInt(bytes2[2]) << 24n | BigInt(bytes2[3]) << 16n | BigInt(bytes2[4]) << 8n | BigInt(bytes2[5]); + } else { + if (sequence === MAX_SEQUENCE) + throw new RangeError("UUIDv7 generator sequence exhausted"); + sequence++; + } + const timestamp = BigInt(effectiveTimestamp); + for (let index = 5; index >= 0; index--) { + bytes2[index] = Number(timestamp >> BigInt((5 - index) * 8)) & 255; + } + bytes2[6] = 112 | Number(sequence >> 37n & 0x0fn); + bytes2[7] = Number(sequence >> 29n & 0xffn); + bytes2[8] = 128 | Number(sequence >> 23n & 0x3fn); + bytes2[9] = Number(sequence >> 15n & 0xffn); + bytes2[10] = Number(sequence >> 7n & 0xffn); + bytes2[11] = Number((sequence & 0x7fn) << 1n) | bytes2[11] & 1; + const hex = Array.from(bytes2, (byte) => byte.toString(16).padStart(2, "0")); + return `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10).join("")}`; +} + +// node_modules/@earendil-works/pi-durable/dist/harness/provider.js +var ProviderDoc = defineDoc({ + kind: "pi.provider", + version: 1, + scope: "conversation", + history: "latest", + fork: "initial", + initial: () => ({ sessionId: uuidv7() }), + checkpointWhen: () => true +}); +async function ensureProviderSessionId(runtime, context) { + const existing = await runtime.snapshot(ProviderDoc, runtime.conversationId, context); + if (existing !== void 0) + return existing.sessionId; + let created; + await runtime.commit(async (tx) => { + created = (await tx.doc(ProviderDoc, runtime.conversationId)).sessionId; + return void 0; + }, context); + if (created === void 0) + throw new Error(`Conversation ${runtime.conversationId} has no provider session ID`); + return created; +} + +// node_modules/@earendil-works/pi-durable/dist/harness/telemetry.js +var entry = { + description: "A transcript entry was committed; content remains in the transcript.", + attributes: { + "entry.id": { type: "number", required: true, description: "Transcript entry ID.", cardinality: "high" }, + "entry.kind": { type: "string", required: true, description: "Entry kind." } + } +}; +var reportAttributes = { + type: { + type: "string", + required: true, + description: "Report kind (`Report.kind`).", + values: [ + "storage_failed", + "task_faulted", + "task_interrupted", + "migration_failed", + "task_outdated", + "task_error", + "extension_error", + "host_error", + "listener_error" + ] + }, + reason: { + type: "string", + required: false, + description: "Fixed report reason, when present.", + values: ["threw", "restart_limit", "interrupted", "missing_task", "task_too_old", "migration_failed"] + }, + extension: { type: "string", required: false, description: "Registered extension name, when present." }, + "task.id": { type: "number", required: false, description: "Task ID, when present.", cardinality: "high" } +}; +var report = { + description: "A report was raised; no error payload is recorded.", + attributes: reportAttributes +}; +var submission = { + description: "A submission was settled.", + attributes: { + "submission.id": { type: "number", required: true, description: "Submission ID.", cardinality: "high" }, + status: { type: "string", required: true, values: ["done", "unanswered"], description: "Settlement." }, + "answer.entry_id": { type: "number", required: false, description: "Answer entry ID.", cardinality: "high" } + } +}; +var DURABLE_TELEMETRY_SCHEMA = defineTelemetrySchema({ + version: 1, + spans: { + "pi.durable.submit": { + description: "Public submission admission: with its commit for submit(draft, context); the admission only for submit(tx, draft), whose result is provisional (a later throw or storage failure of the caller's commit leaves no submission) and whose commit events go to the caller's commit context. Inputs and continues start root traces linked to their callers; writes are children. Internal admissions emit phase events only.", + parents: { kind: "any" }, + startAttributes: { + "submission.type": { + type: "string", + required: true, + values: ["input", "write", "continue"], + description: "Submission type." + }, + "conversation.id": { type: "number", required: true, description: "Conversation ID.", cardinality: "high" } + }, + endAttributes: { + "submission.id": { type: "number", description: "Admitted submission ID.", cardinality: "high" }, + result: { + type: "string", + values: ["queued", "placed", "done"], + description: "Admission result; stale writes report done." + } + }, + events: { "pi.durable.entry": entry, "pi.durable.submission": submission, "pi.durable.report": report }, + status: { default: "ok", errorWhen: "Admission failed." } + }, + "pi.durable.phase": { + description: "One task phase invocation, including its decision commit, never a wait or restart.", + parents: { kind: "any" }, + startAttributes: { + "task.id": { type: "number", required: true, description: "Task ID.", cardinality: "high" }, + "task.name": { type: "string", required: true, description: "Registered task name." }, + phase: { type: "string", required: true, description: "Invoked checkpoint phase." }, + kind: { type: "string", required: true, values: ["run", "abort"], description: "Invocation kind." }, + attempt: { type: "number", required: false, description: "Checkpoint attempt, when present." }, + resumed: { + type: "boolean", + required: true, + description: "First invocation of a task loaded in this process." + } + }, + endAttributes: { + outcome: { type: "string", description: "next=, waiting, sleeping, done, faulted, or aborted." }, + "tool.name": { type: "string", description: "Called tool name." }, + "tool.call_id": { type: "string", description: "Transcript tool call ID.", cardinality: "high" } + }, + events: { + "pi.durable.entry": entry, + "pi.durable.submission": submission, + "pi.durable.report": report, + "pi.durable.checkpoint": { + description: "The running checkpoint's phase changed while the invocation continued.", + attributes: { phase: { type: "string", required: true, description: "Committed checkpoint phase." } } + }, + "pi.durable.replay": { + description: "A nested tool call was answered from its stored result.", + attributes: { "tool.name": { type: "string", required: true, description: "Called tool name." } } + } + }, + status: { + default: "ok", + errorWhen: "Faulted, failed, and orphaned outcomes use task_faulted, task_failed, and task_orphaned with a diagnostic capped at 500 characters. Aborted outcomes keep status ok." + } + } + }, + metrics: { + "pi.durable.reports": { + kind: "counter", + description: "Reports raised, grouped by their fixed kind.", + attributes: { type: reportAttributes.type } + }, + "pi.durable.phase.duration": { + kind: "histogram", + unit: "ms", + description: "Duration of a phase invocation and its decision commit.", + attributes: { + task: { type: "string", required: true, description: "Registered task name." }, + phase: { type: "string", required: true, description: "Invoked checkpoint phase." }, + outcome: { type: "string", required: true, description: "Phase outcome." } + } + }, + "pi.durable.tasks.active": { + kind: "upDownCounter", + description: "Live tasks held by this Harness; decremented at terminal settlement or close.", + attributes: { task: { type: "string", required: true, description: "Registered task name." } } + } + } +}); + +// node_modules/@earendil-works/pi-durable/dist/harness/util.js +var Waiters = class { + #sets = /* @__PURE__ */ new Map(); + add(key, context) { + const signal = context.abortSignal; + if (signal?.aborted) + return Promise.reject(signal.reason); + const waiter = Promise.withResolvers(); + const set = this.#sets.get(key) ?? /* @__PURE__ */ new Set(); + this.#sets.set(key, set.add(waiter)); + if (signal !== void 0) { + const onAbort = () => { + set.delete(waiter); + if (set.size === 0 && this.#sets.get(key) === set) + this.#sets.delete(key); + waiter.reject(signal.reason); + }; + signal.addEventListener("abort", onAbort, { once: true }); + const detach = () => signal.removeEventListener("abort", onAbort); + waiter.promise.then(detach, detach); + } + return waiter.promise; + } + keys() { + return [...this.#sets.keys()]; + } + resolve(key, value2) { + const set = this.#sets.get(key); + this.#sets.delete(key); + for (const waiter of set ?? []) + waiter.resolve(value2); + } + rejectAll(error) { + const sets = [...this.#sets.values()]; + this.#sets.clear(); + for (const set of sets) + for (const waiter of set) + waiter.reject(error); + } +}; +async function scanAll(scan) { + const items = []; + let cursor; + do { + const page2 = await scan(cursor); + items.push(...page2.items); + cursor = page2.next; + } while (cursor !== void 0); + return items; +} +function closedError(session) { + const failure = session.failure; + return failure === void 0 ? new Error("Harness is closed") : new SessionFailed(failure.error); +} +function optionalExtension(extension) { + return extension === void 0 ? {} : { extension }; +} +function conversationEntries(conversationId, query) { + return { + conversationId, + ...query.minEntryId === void 0 ? {} : { minEntryId: query.minEntryId }, + ...query.maxEntryId === void 0 ? {} : { maxEntryId: query.maxEntryId }, + ...query.order === void 0 ? {} : { order: query.order } + }; +} + +// node_modules/@earendil-works/pi-durable/dist/harness/scheduler.js +var SCAN_PAGE_SIZE3 = 256; +var NO_KEYS2 = []; +var LIVE_STATUSES = ["pending", "running", "waiting", "sleeping", "completing"]; +function blockedSubject(record) { + return { + id: record.id, + kind: record.kind, + version: record.version, + abortRequested: record.abortRequested, + ...record.abortReason === void 0 ? {} : { abortReason: record.abortReason }, + status: record.state.status + }; +} +var CONTINUE = { kind: "continue" }; +var PROGRESS = { kind: "progress" }; +var END = { kind: "end" }; +var CASCADE = { kind: "cascade" }; +function conversationNode(id) { + return `c${id}`; +} +function conversationOf(node) { + return Number(node.slice(1)); +} +function parentNode(node) { + return node.owner ?? conversationNode(node.conversationId); +} +var TaskScheduler = class { + #telemetry; + #activeMetric; + #durationMetric; + #resumed = /* @__PURE__ */ new Set(); + #session; + #storage; + #registry; + #models; + #agent; + #settings; + #env; + #now; + #onWake; + #timers; + #writeAtClose; + #report; + #settleOutcome; + #limitsRestarts; + #withdrawInputs; + #conversation; + #context; + #live = /* @__PURE__ */ new Map(); + /** + * Context range last read through a task runtime, per conversation: a later read, by any of its tasks, scans only + * newer entries. `idleSince` is the Harness time it was first seen idle. Derived and never persisted; dropped at the + * first idle check after `settings.contextRetentionMs` of idleness, and at close. + */ + #contexts = /* @__PURE__ */ new Map(); + /** + * The idle kept contexts by `idleSince`, oldest first: the expiry order, so neither an expiry nor finding the next one + * scans `#contexts`. Kept sorted also when the Harness clock goes back (see `#markIdle`). + */ + #idleOrder = /* @__PURE__ */ new Map(); + /** At least the newest `idleSince` in `#idleOrder` (drops do not lower it): an older one added means the clock went back. */ + #idleNewest = Number.NEGATIVE_INFINITY; + /** + * A chain load resolved what `#idle()` treats as unknown for a live task whose chain is not loaded, which can make a + * scope idle without a task change below it; the next idle check then covers every scope. + */ + #unknownsChanged = false; + /** Idle checks that covered every scope (`idleState`, a test diagnostic). */ + #fullIdleChecks = 0; + /** Background timer for the earliest expiry of an idle context, unless the host declined it (see `#scheduleExpiry`). */ + #expiry; + #invocations = /* @__PURE__ */ new Map(); + #taskWaiters = new Waiters(); + /** Idle waiters by conversation; `undefined` waits for the whole Harness. */ + #idleWaiters = new Waiters(); + /** Definition whose migration failed per task; retried only once the registry resolves another definition. */ + #failedMigrations = /* @__PURE__ */ new Map(); + /** Owner task of each known conversation, `null` when ownerless. */ + #edges = /* @__PURE__ */ new Map(); + /** Ownership fields of the known terminal tasks, which walks pass through. */ + #settled = /* @__PURE__ */ new Map(); + /** + * Ended tasks and conversations found unneeded, dropped by `#sweep()` at the end of the commit's publication unless + * needed again by then: a chain load installs nodes before the work below them is listed. + */ + #dropQueue = /* @__PURE__ */ new Set(); + /** + * The tree downward around live work: each node's child nodes that are live tasks or have nodes listed below them. + * A conversation's parent is its owner task; ownerless ones with work are in `#roots`. + */ + #below = /* @__PURE__ */ new Map(); + #roots = /* @__PURE__ */ new Set(); + /** Known tasks whose chain may not be loaded; every other known task's chain is, and stays, loaded. */ + #unloaded = /* @__PURE__ */ new Set(); + /** Live tasks with cancellation intent, the sources of abort cascades (spec §5.4). */ + #intent = /* @__PURE__ */ new Set(); + /** Live tasks that are not `completing`, the candidates of a reservation pass; ones found waiting leave it. */ + #runnable = /* @__PURE__ */ new Set(); + /** `completing` tasks the next finalize pass checks: those that started holding, and holders of ended work. */ + #finalizeChecks = /* @__PURE__ */ new Set(); + /** Live waiting tasks by each task in their `on`. */ + #waiters = /* @__PURE__ */ new Map(); + /** + * `failFast` waiters the next reconcile checks for a failed task in `on`: at open, when they start waiting, and when + * one of their tasks fails. + */ + #failFastChecks = /* @__PURE__ */ new Set(); + /** + * Live tasks with `abandonOnRestart` found at open, from an earlier Harness. The first reservation pass abort-marks + * them with reason `restart`, so nothing of them or below them runs a phase again. + */ + #abandoned = /* @__PURE__ */ new Set(); + /** Sleeping live tasks, due or not: the candidates of `#wakeDue()`. */ + #sleeping = /* @__PURE__ */ new Set(); + /** Timer for the earliest sleep, without `onWake` while scheduling runs. */ + #timer; + /** Last value given to `onWake`, so an unchanged one is not repeated. */ + #lastWake; + #wakeScheduled = false; + #blockedListeners = /* @__PURE__ */ new Set(); + /** A registry snapshot that threw for the task graph was reported; it is read per record, so only once. */ + #snapshotReported = false; + #reconcileScheduled = false; + /** Reconcile passes underway with work (they may overlap): their commits finalize held outcomes and mark cascades. */ + #reconcilingWork = 0; + #sweepScheduled = false; + /** + * Nodes chain loads installed for jobs whose commits are not published yet, with how many such jobs: the sweep keeps + * them, as work a pending commit creates below them needs them. + */ + #pins = /* @__PURE__ */ new Map(); + /** Keys the decision region underway reads (`#region()`); set only while one runs. */ + #reading; + #cascadePending = false; + #unsubscribeRegistry = () => { + }; + #enabled = false; + #held = false; + #readOnly; + #closing = false; + #dirty = false; + #draining = false; + constructor(options) { + this.#telemetry = options.telemetry ?? NOOP_TELEMETRY_CONTEXT; + const metrics = createTypedMetrics(this.#telemetry.metrics ?? NOOP_TELEMETRY_METRICS, [DURABLE_TELEMETRY_SCHEMA]); + this.#activeMetric = metrics.upDownCounter("pi.durable.tasks.active"); + this.#durationMetric = metrics.histogram("pi.durable.phase.duration"); + this.#session = options.session; + this.#storage = options.storage; + this.#registry = options.registry; + this.#models = options.models; + this.#agent = options.agent; + this.#settings = options.settings; + this.#env = options.env; + this.#now = options.now; + this.#onWake = options.onWake; + this.#timers = options.timers; + this.#writeAtClose = options.writeAtClose; + this.#report = options.report; + this.#settleOutcome = options.settleOutcome; + this.#limitsRestarts = options.limitsRestarts; + this.#withdrawInputs = options.withdrawInputs; + this.#conversation = options.conversation; + this.#context = options.context; + this.#readOnly = options.readOnly === true; + } + /** Start no run invocation until `release()`; running invocations and abort handlers go on. Overrides `resume()`. */ + hold() { + this.#held = true; + } + release() { + this.#held = false; + this.#kick(); + } + /** + * Load live tasks and change surviving `running` tasks back to `pending`, counting the restart. Dispatches nothing; + * a task cut off `settings.restartLimit` times ends when it is next reserved. Read-only, it only loads them: a + * `running` task stays as committed, running in another process. + */ + async open(context) { + this.#session.followCommits((publication) => this.#observe(publication)); + this.#session.subscribeClose(() => this.#seal()); + this.#unsubscribeRegistry = this.#registry.subscribe(() => { + this.#notifyBlocked(); + this.#kick(); + }); + await this.#session.commitWith(async (tx) => { + await this.#published(); + const scans = []; + for (const status of LIVE_STATUSES) { + scans.push(await scanAll((cursor) => tx.scanTasks({ status }, SCAN_PAGE_SIZE3, cursor))); + } + for (const records2 of scans) { + for (const record of records2) { + this.#track(void 0, record); + this.#resumed.add(record.id); + if (this.#readOnly) + continue; + if (record.abandonOnRestart === true) + this.#abandoned.add(record.id); + if (record.state.status === "running") { + const restarts = (record.restarts ?? 0) + 1; + const pending = withState(record, { status: "pending", checkpoint: record.state.checkpoint }); + tx.setTask({ ...pending, restarts }); + } + if (record.state.status === "waiting" && record.state.policy === "failFast") { + this.#failFastChecks.add(record.id); + } + } + } + }, context); + if (this.#readOnly) + return; + this.#cascadePending = true; + this.#scheduleReconcile(); + this.#scheduleWake(); + } + /** + * Enable scheduling, and make sleeping tasks that are due runnable: a host wakeup, such as an alarm, calls this. + * Idempotent; the kick does nothing once closing. + */ + resume() { + if (this.#readOnly) + throw new ReadOnlySession("running tasks"); + this.#enabled = true; + this.#wakeDue(); + this.#kick(); + } + /** + * Why a live task cannot run here, if it cannot: its definition is missing, too old, or failed to migrate. A running + * task, an abort-marked one, and one holding its outcome are never blocked. + */ + blockedReason(record) { + const status = record.status; + if (status === "running" || status === "completing" || status === "terminal") + return void 0; + if (record.abortRequested && record.abortReason === void 0) + return void 0; + let snapshot; + try { + snapshot = this.#registry.snapshot(); + } catch (error) { + if (!this.#snapshotReported) + this.#report(error, REGISTRY); + this.#snapshotReported = true; + return void 0; + } + const fit = this.#fit(record, snapshot.task(record.kind)); + if ("reason" in fit) + return fit.reason; + return fit.migrates && fit.task.definition.migrate === void 0 ? "migration_failed" : void 0; + } + /** Call `listener` when blocked reasons may have changed without a commit: a registry change or a failed migration. */ + subscribeBlocked(listener) { + this.#blockedListeners.add(listener); + return () => this.#blockedListeners.delete(listener); + } + /** + * When the Session next needs a process: `active` while some task can run now or is running, `at` for the earliest + * sleep not yet due. Synchronous, from memory; a task whose chain is not loaded yet counts as it would alone. + */ + wake() { + if (this.#readOnly) + return { active: false }; + const now = this.#now(); + const active = this.#invocations.size > 0 || this.#reconcilingWork > 0 || this.#reconcileWork() || this.#runsNow(now); + let at; + for (const id of this.#sleeping) { + const record = this.#live.get(id); + const state = record?.state; + if (state?.status === "sleeping" && !record.abortRequested && state.until > now) { + at = at === void 0 ? state.until : Math.min(at, state.until); + } + } + return at === void 0 ? { active } : { active, at }; + } + /** Whether a live task can run now: the scan `wake()` needs only while no invocation runs. */ + #runsNow(now) { + let snapshot; + let snapshotFailed = false; + for (const record of this.#live.values()) { + const state = record.state; + if (state.status === "completing") + continue; + if (state.status === "waiting" && !record.abortRequested && state.on.some((id) => this.#live.has(id))) + continue; + if (state.status === "sleeping" && !record.abortRequested && state.until > now) + continue; + if (record.abortRequested && this.#waitingOn(record, true).length > 0) + continue; + if (!record.abortRequested || record.abortReason !== void 0) { + if (snapshot === void 0 && !snapshotFailed) { + try { + snapshot = this.#registry.snapshot(); + } catch (error) { + snapshotFailed = true; + this.#report(error, REGISTRY); + } + } + if (snapshot !== void 0 && "reason" in this.#fit(record, snapshot.task(record.kind))) + continue; + } + return true; + } + return false; + } + /** + * Signal every invocation and wait for them, then write the tasks they ran back as `pending` (`writeAtClose`), as a + * close is not a crash. Close calls this after every close listener has run, so waits, watches, and streams a task + * holds end with the Session's reason, not as cancelled by its signal. + */ + async join() { + const stopped = [...this.#invocations.values()]; + for (const invocation of stopped) + invocation.controller.abort(); + await Promise.allSettled(stopped.map((invocation) => invocation.done)); + if (stopped.length === 0) + return; + await this.#writeAtClose(() => stopped.flatMap((invocation) => { + const record = this.#live.get(invocation.taskId); + if (record?.state.status !== "running") + return []; + const value2 = withState(record, { status: "pending", checkpoint: record.state.checkpoint }); + return [{ type: "task", value: value2 }]; + })).catch(() => { + }); + } + /** + * Commit the abort mark, or settle a task that no registered definition can take as `orphaned` when nothing it owns + * is live, then join the run invocation seen on the line; the commit listener signalled it. The abort invocation + * starts once the task's ordinary owned work is gone. A `completing` task is only marked. A request replaces a + * `restart` mark, so a task abandoned after a restart that waits for its definition is orphaned; with `keepRestart`, + * as for an owner's own cleanup, such a task keeps its mark and waits on. + */ + async abort(id, context, keepRestart = false) { + const marked = await this.#session.commitWith(async (tx) => { + const current = await tx.task(id); + if (current === void 0) + throw new Error(`Task ${id} does not exist`); + if (current.state.status === "terminal") + return { result: "terminal" }; + const invocation = this.#invocations.get(id); + if (keepRestart && current.abortReason === "restart") + return { result: "marked" }; + if (invocation === void 0 && current.state.status !== "completing") { + await this.#loadScopes(false); + const owns = await this.#region(() => this.#hasOwnedLive(id), this.#reloadScopes); + const resolution = owns ? void 0 : this.#resolve(current, this.#registry.snapshot()); + if (resolution?.kind === "blocked") { + await this.#terminate(tx, current, { status: "orphaned", reason: resolution.reason }); + return { result: "marked" }; + } + } + if (!current.abortRequested || current.abortReason !== void 0) + tx.setTask(withAbortMark(current)); + return { result: "marked", run: invocation?.mode === "run" ? invocation : void 0 }; + }, context); + if (marked.run !== void 0) { + await awaitWithContext(Promise.race([marked.run.done, this.#session.failed]), context); + } + return marked.result; + } + async waitForTask(id, context) { + const found = await this.#session.readOnLine(async (storage) => { + if (this.#closing) + throw closedError(this.#session); + if (await this.#region(() => this.#liveRecord(id) !== void 0)) { + return { promise: this.#taskWaiters.add(id, context) }; + } + const record = await storage.task(id, context); + if (record === void 0) + throw new Error(`Task ${id} does not exist`); + return { promise: Promise.resolve(record) }; + }); + return found.promise; + } + /** + * Resolve when ordinary traversal from the conversation, or from every ownerless conversation, reaches no live + * non-background task. + */ + waitForIdle(conversationId, context) { + if (this.#closing) + return Promise.reject(closedError(this.#session)); + if (this.#idle(conversationId)) + return Promise.resolve(); + this.#scheduleReconcile(); + return this.#idleWaiters.add(conversationId, context); + } + /** + * `Conversation.abort()`: in one commit, withdraw the queued inputs and mark every live non-background task that + * ordinary traversal from the conversation reaches; resolves once the scope is idle. With `background`, traversal + * crosses background boundaries, and the wait also covers every task it reached. + */ + async abortConversation(conversationId, background, context) { + const reached = await this.#session.commitWith(async (tx) => { + await this.#published(); + const queued = await this.#loadScopes(true); + const scope = { conversation: conversationId }; + const reached2 = []; + const walk = this.#walkDown(conversationNode(conversationId), { crossBackground: background }); + for (const { node, live } of walk) { + if (live === void 0 || node.background && !background) + continue; + reached2.push(live.id); + if (!live.abortRequested || live.abortReason !== void 0) + tx.setTask(withAbortMark(live)); + } + for (const id of queued) { + if (this.#inScope({ conversation: id }, scope, background) === true) + await this.#withdrawInputs(tx, id); + } + return reached2; + }, context); + if (background) + for (const id of reached) + await this.waitForTask(id, context); + await this.waitForIdle(conversationId, context); + } + /** + * The ownership memberships a commit's task and conversation changes write (group commit §4.2), at its admission on + * the line: for each task created or made terminal, every node from its parent up to and including the nearest live + * task, or the root conversation, and then the roots too, conservatively, as a pending end may unlist the root a + * creation finds listed; for each conversation created under a task, that task; and the intent set when + * a change sets, upgrades, or clears a mark or a held or ended outcome other than `completed`. Loads the chains it + * walks; the nodes it installs stay until the commit is published. + */ + membershipKeys(tx) { + const tasks = tx.stagedTasks(); + const conversations = tx.stagedConversations(); + if (tasks.length === 0 && conversations.every((record) => record.owner === void 0)) + return NO_KEYS2; + return this.#membershipKeys(tx, tasks, conversations); + } + async #membershipKeys(tx, tasks, conversations) { + const keys = /* @__PURE__ */ new Set(); + const overlay = overlayOf(tx); + const created = new Set(tx.createdTaskIds()); + const reached = /* @__PURE__ */ new Map(); + for (const record of tasks) { + if (intentChanges(this.#live.get(record.id), record)) + keys.add(INTENT_KEY); + if (!created.has(record.id) && record.state.status !== "terminal") + continue; + await this.#loadChain(parentOf(record), overlay); + if (await this.#chainKeys(parentNode(record), overlay, keys, reached) !== void 0) + keys.add(ROOTS_KEY); + } + for (const conversation of conversations) { + const owner = conversation.owner?.taskId; + if (owner === void 0) + continue; + await this.#loadChain({ task: owner }, overlay); + await this.#chainKeys(owner, overlay, keys, reached); + } + return [...keys]; + } + /** + * Add the memberships from `start` up to and including the nearest live task, or the root conversation, which it + * returns. `reached` keeps, per node walked, where its walk ended, so a chain several tasks of a commit share is + * walked once. Whether an ancestor is live can change under a pending commit, so it is read after a touch of the + * ancestor's record: a pending end must not stop the chain early. Ownership fields are immutable, so not keyed. + */ + async #chainKeys(start, overlay, keys, reached) { + const walked = []; + let root2; + let node = start; + while (node !== void 0 && node !== null) { + const key = ownedKey(node); + if (reached.has(key)) { + root2 = reached.get(key); + break; + } + keys.add(key); + walked.push(key); + if (typeof node === "string") { + const conversation = conversationOf(node); + node = this.#edge(conversation, overlay); + if (node === null) + root2 = conversation; + continue; + } + let record = overlay.tasks.get(node); + if (record === void 0) { + const gate = this.#session.touch(taskKey(node)); + if (gate !== void 0) { + await gate; + await this.#loadChain({ task: node }, overlay); + } + record = this.#live.get(node); + } + if (record !== void 0 && record.state.status !== "terminal") + break; + const above = record ?? this.#settled.get(node); + node = above === void 0 ? void 0 : parentNode(above); + } + for (const key of walked) + reached.set(key, root2); + return root2; + } + // ─── Scheduling ──────────────────────────────────────────────────────── + #observe(publication) { + if (publication.kind === "memory") + return; + this.#advanceContexts(publication); + const updated = []; + const failed = []; + let changed = false; + let dirty = /* @__PURE__ */ new Set(); + for (const change of publication.changes) { + if (change.type === "conversation" && !this.#edges.has(change.value.id)) { + this.#setEdge(change.value.id, change.value.owner?.taskId ?? null); + } + } + for (const change of publication.changes) { + if (change.type !== "task") + continue; + changed = true; + const record = change.value; + const previous = this.#live.get(record.id); + if (failedOutcome(record) && (previous === void 0 || !failedOutcome(previous))) + failed.push(record.id); + this.#track(previous, record); + const liveness = previous === void 0 || record.state.status === "terminal"; + if (liveness && dirty !== void 0 && !this.#addScopesAbove(record, dirty)) + dirty = void 0; + if (record.state.status === "terminal") { + this.#failedMigrations.delete(record.id); + this.#failFastChecks.delete(record.id); + this.#taskWaiters.resolve(record.id, record); + this.#scheduleReconcile(); + continue; + } + if (record.abortRequested && previous?.abortRequested !== true) { + this.#cascadePending = true; + const invocation = this.#invocations.get(record.id); + if (invocation?.mode === "run") + queueMicrotask(() => invocation.controller.abort()); + } + if (previous?.abortReason !== void 0 && record.abortReason === void 0) + this.#cascadePending = true; + const status = record.state.status; + if (status === "completing" && previous?.state.status !== "completing") { + if (cancellationIntent(record)) + this.#cascadePending = true; + this.#scheduleReconcile(); + } + if (status === "waiting" && record.state.policy === "failFast" && previous?.state.status !== "waiting") { + this.#failFastChecks.add(record.id); + this.#scheduleReconcile(); + } + if (previous === void 0) + updated.push(record); + } + for (const id of failed) { + for (const waiter of this.#waiters.get(id) ?? []) { + const state = this.#live.get(waiter)?.state; + if (state?.status === "waiting" && state.policy === "failFast") { + this.#failFastChecks.add(waiter); + this.#scheduleReconcile(); + } + } + } + for (const change of publication.changes) { + if (change.type !== "submission" || change.value.status !== "queued" || change.value.type !== "input") + continue; + if (this.#intent.size === 0) + continue; + const conversation = change.value.conversationId; + if (!this.#known(conversationNode(conversation)) || this.#belowCancelled({ conversation })) { + this.#cascadePending = true; + } + } + for (const record of updated) { + if (this.#unloaded.has(record.id)) + this.#scheduleReconcile(); + else if (!record.background && !record.abortRequested && this.#belowCancelled(parentOf(record))) { + this.#cascadePending = true; + } + } + if (this.#cascadePending) + this.#scheduleReconcile(); + this.#sweep(); + if (!changed) + return; + this.#settleIdle(dirty); + this.#kick(); + } + /** + * Add the conversations above `record` to `scopes`: those whose idleness a change of it can change. False when a node + * on the way up is not known. A conversation already in `scopes` has everything above it there too. + */ + #addScopesAbove(record, scopes) { + for (const step of this.#above(parentOf(record))) { + if ("unknown" in step) + return false; + if (!("conversation" in step)) + continue; + if (scopes.has(step.conversation)) + return true; + scopes.add(step.conversation); + } + return true; + } + /** + * Extend each kept context range with the entries a stored commit appended to its conversation, so a later read + * needs no Storage scan. Entries are published in ID order. + */ + #advanceContexts(publication) { + if (this.#contexts.size === 0) + return; + let appended; + for (const change of publication.changes) { + if (change.type !== "entry" || !this.#contexts.has(change.value.conversationId)) + continue; + appended ??= /* @__PURE__ */ new Map(); + const entries2 = appended.get(change.value.conversationId); + if (entries2 === void 0) + appended.set(change.value.conversationId, [change.value]); + else + entries2.push(change.value); + } + if (appended === void 0) + return; + for (const [conversationId, entries2] of appended) { + const kept = this.#contexts.get(conversationId); + if (kept.idleSince !== void 0 && kept.expiresAt !== void 0 && kept.expiresAt <= this.#now()) { + this.#dropContext(conversationId); + continue; + } + const range = advanceRange(kept.range, entries2); + if (range === void 0) + this.#dropContext(conversationId); + else + kept.range = range; + } + } + /** + * `settings.contextRetentionMs`, or 0 when the host's settings throw. Kept contexts are only a cache, so dropping them + * is safe, while a throw here would escape commit listeners, reconciliation, and the expiry timer. + */ + #contextRetentionMs() { + try { + return this.#settings().contextRetentionMs; + } catch (error) { + this.#report(error, SETTINGS); + return 0; + } + } + /** + * After task changes below the conversations in `scopes` (every conversation when undefined): resolve their idle + * waiters and the Harness-wide ones, drop the kept contexts whose idle period passed the retention, then start or end + * the idle periods of the kept contexts in `scopes`. A task change can change the idleness only of the conversations + * above it, so no check visits every conversation. + */ + #settleIdle(scopes) { + const all = scopes === void 0 || this.#unknownsChanged; + this.#unknownsChanged = false; + if (all) + this.#fullIdleChecks++; + this.#resolveIdleWaiters(all ? void 0 : scopes); + const now = this.#now(); + const retention = this.#contextRetentionMs(); + this.#expireContexts(now, retention); + for (const conversationId of all ? [...this.#contexts.keys()] : scopes) { + const kept = this.#contexts.get(conversationId); + if (kept === void 0) + continue; + if (!this.#idle(conversationId)) { + if (kept.idleSince === void 0) + continue; + kept.idleSince = void 0; + this.#idleOrder.delete(conversationId); + } else if (retention <= 0) { + this.#dropContext(conversationId); + } else if (kept.idleSince === void 0) { + kept.idleSince = now; + kept.expiresAt = now + retention; + this.#markIdle(conversationId, now); + } + } + this.#scheduleExpiry(); + } + /** Resolve the idle waiters of the conversations in `scopes` (all when undefined) and the Harness-wide ones. */ + #resolveIdleWaiters(scopes) { + for (const conversationId of this.#idleWaiters.keys()) { + if (conversationId !== void 0 && scopes !== void 0 && !scopes.has(conversationId)) + continue; + if (this.#idle(conversationId)) + this.#idleWaiters.resolve(conversationId); + } + } + /** + * Drop the kept contexts idle for at least `retention`, oldest first, with the retention setting of now; with no + * retention, every idle one, also those whose idle period a clock that went back makes start after now. + */ + #expireContexts(now, retention) { + for (const [conversationId, idleSince] of this.#idleOrder) { + if (retention > 0 && now - idleSince < retention) + return; + this.#dropContext(conversationId); + } + } + /** + * Start the idle period of a kept context in the expiry order. An idle period older than the newest one kept, which + * a clock that went back starts, sorts the order again, and the newest is taken from what is kept then. + */ + #markIdle(conversationId, idleSince) { + this.#idleOrder.delete(conversationId); + this.#idleOrder.set(conversationId, idleSince); + if (this.#idleOrder.size === 1 || idleSince >= this.#idleNewest) { + this.#idleNewest = idleSince; + return; + } + const sorted = [...this.#idleOrder].sort((a, b) => a[1] - b[1]); + this.#idleOrder.clear(); + for (const [id, since] of sorted) + this.#idleOrder.set(id, since); + this.#idleNewest = sorted.at(-1)[1]; + } + /** Forget a kept context, idle or not, and its place in the expiry order. */ + #dropContext(conversationId) { + this.#contexts.delete(conversationId); + this.#idleOrder.delete(conversationId); + } + /** + * Drop the expired contexts when the earliest idle context expires, on a background timer, which never keeps the + * process alive. Where the host declines it, as the default does in Cloudflare Workers, none is kept: a pending timer + * could keep a Durable Object from being evicted, and eviction frees the contexts. There, task changes alone check + * expiry. + */ + #scheduleExpiry() { + const oldest = this.#idleOrder.values().next(); + const at = oldest.done === true ? void 0 : oldest.value + this.#contextRetentionMs(); + if (this.#expiry !== void 0 && this.#expiry.at === at) + return; + this.#expiry?.disarm(); + this.#expiry = void 0; + if (at === void 0 || this.#closing) + return; + const disarm = this.#timers.background(() => { + this.#expiry = void 0; + this.#expireContexts(this.#now(), this.#contextRetentionMs()); + this.#scheduleExpiry(); + }, at - this.#now()); + if (disarm !== void 0) + this.#expiry = { at, disarm }; + } + // ─── Ownership ─────────────────────────────────────────────────────────── + #scheduleReconcile() { + if (this.#reconcileScheduled || this.#closing || this.#readOnly) + return; + this.#reconcileScheduled = true; + queueMicrotask(() => void this.#reconcile()); + } + /** + * One commit that applies what committed records imply: abort marks below live owners with cancellation intent + * (spec §5.4), `failFast` marks (spec §5.5), withdrawn queued inputs below cancelled owners, and the final terminal + * record of every `completing` task whose ordinary owned work is gone. The durable records are the intent, so this + * also repairs whatever a crash left unapplied. Resolves idle waiters that the loaded edges decide. + */ + async #reconcile() { + this.#reconcileScheduled = false; + const work = this.#reconcileWork(); + if (work) + this.#reconcilingWork++; + try { + await this.#session.commitInternal(async (tx) => { + if (this.#closing) + return; + const cascade = this.#cascadePending; + const queued = await this.#loadScopes(cascade); + if (cascade) + this.#cascadePending = false; + const marks = await this.#marks(); + for (const { record, reason } of marks.values()) + tx.setTask(withAbortMark(record, reason)); + const withdrawn = await this.#region(() => queued.filter((id) => this.#belowCancelled({ conversation: id })), this.#reloadScopes); + for (const id of withdrawn) + await this.#withdrawInputs(tx, id); + await this.#finalize(tx); + }, this.#context); + } catch (error) { + this.#failSession(error); + } + if (work) + this.#reconcilingWork--; + this.#scheduleWake(); + if (this.#unknownsChanged) { + this.#settleIdle(void 0); + return; + } + this.#resolveIdleWaiters(void 0); + this.#expireContexts(this.#now(), this.#contextRetentionMs()); + this.#scheduleExpiry(); + } + /** + * The abort marks committed records imply (spec §5.4, §5.5): every live foreground task below a live owner with + * cancellation intent, which is its nearest one (a walk down from each owner with intent does not enter another, + * whose own walk covers what is below it), and every other live task in the `on` of a `failFast` waiter one of whose + * tasks failed. A cascade from an abandoned owner passes its `restart` reason on; any other intent marks, or upgrades + * a `restart` mark, as a request. Collected first, so a request wins over a `restart` mark of the same pass. Derived + * again on every pass, as loading edges can reveal a cancelled owner. Takes the `failFast` checks once its decision + * stands. + */ + async #marks() { + const ended = /* @__PURE__ */ new Map(); + for (; ; ) { + const plan = await this.#region(() => this.#planMarks(ended), this.#reloadScopes); + if ("missing" in plan) { + for (const id of plan.missing) + ended.set(id, await this.#session.lineStorage.task(id, this.#context) ?? null); + continue; + } + for (const id of plan.checked) + this.#failFastChecks.delete(id); + return plan.marks; + } + } + /** `#marks()`' decision region; the `failFast` members it needs from Storage first, when `ended` lacks them. */ + #planMarks(ended) { + const marks = /* @__PURE__ */ new Map(); + const mark = (found, reason) => { + const record = this.#liveRecord(found.id); + const staged = marks.get(record.id); + if (staged !== void 0 && staged.reason === void 0) + return; + if (record.abortRequested && (record.abortReason === void 0 || reason !== void 0)) + return; + marks.set(record.id, { record, reason }); + }; + this.#record(INTENT_KEY); + for (const id of this.#intent) { + const owner = this.#liveRecord(id); + const restart = owner.abortReason === "restart" && !failedOutcome(owner); + for (const { node, live } of this.#walkDown(id, { enter: (below) => !this.#intent.has(below) })) { + if (live !== void 0 && !node.background) + mark(live, restart ? "restart" : void 0); + } + } + const checked = [...this.#failFastChecks]; + const missing = []; + for (const id of checked) { + const waiter = this.#liveRecord(id); + if (waiter?.state.status !== "waiting") + continue; + let failed = false; + for (const member of waiter.state.on) { + const record = this.#liveRecord(member) ?? ended.get(member); + if (record === void 0) + missing.push(member); + else if (record !== null && failedOutcome(record)) + failed = true; + } + if (!failed) + continue; + for (const member of waiter.state.on) { + const record = this.#liveRecord(member); + if (record !== void 0 && !failedOutcome(record)) + mark(record, void 0); + } + } + return missing.length > 0 ? { missing } : { marks, checked }; + } + /** Whether a reconcile pass would have work: a cascade to derive, held outcomes to check, or `failFast` waits. */ + #reconcileWork() { + return this.#cascadePending || this.#finalizeChecks.size > 0 || this.#failFastChecks.size > 0; + } + /** + * Write the terminal record of every `completing` task without live ordinary owned work. Finalizing one can free its + * owner, so this repeats over the commit's candidates until nothing changes. A held scheduler outcome gets its + * Harness cleanup here. In rounds, one decision region each: the holders the tasks of one round free are checked once, + * in the next, so a holder of many tasks that end together is walked once, not once per task. The first round takes + * the `completing` tasks to check once its decision stands. + */ + async #finalize(tx) { + const done = /* @__PURE__ */ new Set(); + const overlay = overlayOf(tx); + let round; + for (; ; ) { + const ids = round; + const plan = await this.#region(() => this.#planRound(ids ?? [...this.#finalizeChecks], overlay, done), this.#reloadScopes); + if (ids === void 0) + this.#finalizeChecks.clear(); + for (const { record, terminal } of plan.finalized) { + tx.setTask(terminal); + overlay.tasks.set(record.id, terminal); + done.add(record.id); + const outcome = record.state.outcome; + if (outcome.status === "faulted" || outcome.status === "orphaned") { + await this.#settleOutcome(tx, record, outcome); + } + } + if (plan.next.length === 0) + return; + round = plan.next; + } + } + /** + * One finalize round's decision: which of `ids` are `completing` without live ordinary owned work below them, in the + * commit's candidates with the round's own finalizations applied in order, and the holders above them to check next. + * Tasks finalized have no live work below them, so walks down skip them. It applies the round's terminal records to + * `overlay` while it decides and takes them back before it returns, so a run again starts from the same candidates. + */ + #planRound(ids, overlay, finalized) { + const tasks = overlay.tasks; + const round = /* @__PURE__ */ new Set(); + const done = { has: (id) => round.has(id) || finalized.has(id) }; + const ended = []; + const next = /* @__PURE__ */ new Set(); + try { + for (const id of ids) { + const staged = tasks.get(id); + const record = staged ?? this.#liveRecord(id); + if (record?.state.status !== "completing" || done.has(id)) + continue; + if (this.#hasOwnedLive(id, overlay, done)) + continue; + const terminal = withState(record, { status: "terminal", outcome: record.state.outcome }); + tasks.set(id, terminal); + round.add(id); + ended.push({ record, terminal, staged: staged !== void 0 }); + const above = this.#nearestLiveAbove(record, overlay); + if (above?.state.status === "completing") + next.add(above.id); + } + } finally { + for (const { record, staged } of ended) { + if (staged) + tasks.set(record.id, record); + else + tasks.delete(record.id); + } + } + return { finalized: ended, next: [...next] }; + } + /** + * Load the owner chains of the tasks in `#unloaded` and, with `queued`, of every conversation with queued inputs, + * on the Session line; returns the latter. Reads through the line's Storage, so it may run inside a commit callback. + */ + async #loadScopes(queued) { + for (const id of [...this.#unloaded]) { + if (this.#unloaded.has(id)) + await this.#loadChain({ task: id }); + } + if (!queued) + return []; + const submissions = await scanAll((cursor) => this.#session.lineStorage.scanSubmissions({ status: "queued" }, SCAN_PAGE_SIZE3, cursor, this.#context)); + const inputs = submissions.filter((submission2) => submission2.type === "input"); + const conversations = [...new Set(inputs.map((submission2) => submission2.conversationId))]; + for (const id of conversations) + await this.#loadChain({ conversation: id }); + return conversations; + } + /** + * Load the owner edges and task nodes from `start` up to its ownerless root. A known task outside `#unloaded` ends the + * walk, as its chain is loaded; the `#unloaded` tasks passed have a loaded chain afterwards. Loading a node can reveal + * a cancelled owner, so it makes the next reconcile derive cascade marks. The nodes it installs stay until the job's + * commit is published (`#pin()`). + */ + async #loadChain(start, overlay) { + const passed = []; + let installed = false; + let at = start; + try { + while (at !== void 0) { + if ("task" in at) { + const id = at.task; + let node = this.#live.get(id) ?? this.#settled.get(id); + if (node === void 0 && overlay?.tasks.has(id) === true) + break; + if (node !== void 0 && !this.#unloaded.has(id)) + break; + if (node === void 0) { + const record = await this.#session.lineStorage.task(id, this.#context); + if (record === void 0) + return; + node = nodeOf(record); + if (record.state.status === "terminal") { + this.#settle(id, node); + this.#pin(id); + installed = true; + } + this.#scheduleReconcile(); + } + passed.push(id); + at = parentOf(node); + } else { + let edge = this.#edge(at.conversation, overlay); + if (edge === void 0) { + const record = await this.#session.lineStorage.conversation(at.conversation, this.#context); + edge = record?.owner?.taskId ?? null; + this.#setEdge(at.conversation, edge); + this.#pin(conversationNode(at.conversation)); + installed = true; + this.#scheduleReconcile(); + } + at = edge === null ? void 0 : { task: edge }; + } + } + } finally { + if (passed.some((id) => this.#live.has(id)) || installed && this.#liveUnloaded()) { + this.#unknownsChanged = true; + this.#scheduleReconcile(); + } + } + for (const id of passed) + this.#unloaded.delete(id); + } + /** Whether a live task's chain is not loaded yet: one `#idle()` counts as inside every scope it may be in. */ + #liveUnloaded() { + for (const id of this.#unloaded) + if (this.#live.has(id)) + return true; + return false; + } + #setEdge(conversationId, owner) { + this.#edges.set(conversationId, owner); + this.#relist(conversationNode(conversationId)); + this.#dropIfUnneeded(conversationNode(conversationId)); + } + /** Know a terminal task a chain load read, so walks pass through it; its own chain is not loaded yet. */ + #settle(id, node) { + this.#settled.set(id, node); + this.#unloaded.add(id); + this.#relist(id); + this.#dropIfUnneeded(id); + } + /** Queue an ended task or a conversation for the sweep when nothing is listed below it. */ + #dropIfUnneeded(node) { + if (typeof node !== "string" && this.#live.has(node)) + return; + if (this.#below.has(node)) + return; + this.#dropQueue.add(node); + this.#scheduleSweep(); + } + /** + * Sweep in a job of its own on the Session line, after the operation underway: a rejected commit, one with nothing to + * write, and a read publish nothing, so the sweep at the end of `#observe` would not run for what they loaded. + */ + #scheduleSweep() { + if (this.#sweepScheduled || this.#closing) + return; + this.#sweepScheduled = true; + this.#session.readOnLine(async () => { + this.#sweepScheduled = false; + this.#sweep(); + }).catch(() => { + }); + } + /** + * Drop the queued nodes that are still unneeded: not live, nothing listed below. Runs once a publication is fully + * tracked, and as a job of its own on the line, never inside an operation: work a commit creates below a chain its + * own callback loaded keeps that chain. A flat loop: unlisting already queued every node on the way up that lost its + * last listed node. + */ + #sweep() { + for (const node of this.#dropQueue) { + if (this.#below.has(node) || this.#pins.has(node)) + continue; + if (typeof node === "string") { + this.#edges.delete(conversationOf(node)); + this.#roots.delete(conversationOf(node)); + } else if (!this.#live.has(node)) { + this.#settled.delete(node); + this.#unloaded.delete(node); + } + } + this.#dropQueue.clear(); + } + /** + * Run `decide`, a decision region (group commit §6): synchronous, reading scheduler memory and recording the keys it + * reads, staging nothing and changing no scheduler state. When a key it read has an unpublished writer, its result, + * or its error, is dropped: once that writer is published, `refresh` reloads what the region needs and it runs + * again. A waiting job holds the line, so unpublished writes only shrink, and the runs converge. What a region read + * that had no unpublished writer gets none for the rest of the job, so its result stands while the job applies it. + */ + async #region(decide, refresh) { + for (; ; ) { + const keys = /* @__PURE__ */ new Set(); + this.#reading = keys; + let result; + try { + result = { value: decide() }; + } catch (error) { + result = { error }; + } finally { + this.#reading = void 0; + } + const gates = []; + for (const key of keys) { + const gate = this.#session.touch(key); + if (gate !== void 0) + gates.push(gate); + } + if (gates.length === 0) { + if ("error" in result) + throw result.error; + return result.value; + } + await Promise.all(gates); + await refresh?.(); + } + } + /** Load the chains a region walks, again after it waited: what was published meanwhile may list new work. */ + #reloadScopes = () => this.#loadScopes(false); + /** In a decision region, note that it reads `key`; anywhere else, nothing. */ + #record(key) { + this.#reading?.add(key); + } + /** A live task's committed record, read by a decision region. */ + #liveRecord(id) { + this.#record(taskKey(id)); + return this.#live.get(id); + } + /** + * Keep `node`, which a chain load of the job on the line installed, until that job's commit is published or fails: + * work it creates below the node needs it, and the sweep would drop it as unneeded before then. + */ + #pin(node) { + this.#pins.set(node, (this.#pins.get(node) ?? 0) + 1); + this.#session.onJobEnd(() => { + const count = this.#pins.get(node) - 1; + if (count > 0) { + this.#pins.set(node, count); + return; + } + this.#pins.delete(node); + this.#dropIfUnneeded(node); + }); + } + /** + * Bring the scheduler's sets in line with a task's committed record. A task that ended may free the nearest live task + * above it: one holding its outcome finalizes, an abort-marked one runs its abort handler; its waiters may run. + */ + #track(previous, record) { + const id = record.id; + const invocation = this.#invocations.get(id); + if (invocation !== void 0) + invocation.record = record; + if (record.state.status === "terminal") { + if (previous !== void 0 && !this.#closing) + this.#countActive(-1, record.kind); + this.#resumed.delete(id); + } else if (previous === void 0 && !this.#closing) { + this.#countActive(1, record.kind); + } + if (previous?.state.status === "waiting") + for (const on of previous.state.on) + deleteFrom(this.#waiters, on, id); + if (record.state.status === "terminal") { + const node = nodeOf(record); + this.#live.delete(id); + this.#sleeping.delete(id); + this.#intent.delete(id); + this.#runnable.delete(id); + this.#finalizeChecks.delete(id); + this.#settled.set(id, node); + this.#release(node); + for (const waiter of this.#waiters.get(id) ?? []) + this.#runnable.add(waiter); + this.#relist(id); + this.#dropIfUnneeded(id); + return; + } + this.#live.set(id, record); + if (previous === void 0) { + if (!this.#known(parentNode(record))) + this.#unloaded.add(id); + this.#relist(id); + } + if (cancellationIntent(record)) + this.#intent.add(id); + else + this.#intent.delete(id); + if (record.state.status === "completing") { + this.#runnable.delete(id); + this.#finalizeChecks.add(id); + } else { + this.#runnable.add(id); + } + if (record.state.status === "waiting") + for (const on of record.state.on) + addTo(this.#waiters, on, id); + if (record.state.status === "sleeping") + this.#sleeping.add(id); + else + this.#sleeping.delete(id); + } + /** The nearest live task above ended ordinary work may be free now: one holding its outcome, or an aborting one. */ + #release(node, overlay) { + const above = this.#nearestLiveAbove(node, overlay); + if (above?.state.status === "completing") + this.#finalizeChecks.add(above.id); + else if (above?.abortRequested === true) + this.#runnable.add(above.id); + return above; + } + /** + * List `start` under its parent node in `#below` while it is a live task or has nodes listed below it, and unlist it + * otherwise; then its parent, as far up as that changes. So `#below` holds exactly the tree around live work; a + * parent that loses its last listed node is queued for the sweep. A loop, as ownership chains can be deeper than the + * call stack. + */ + #relist(start) { + let node = start; + for (; ; ) { + const needed = typeof node !== "string" && this.#live.has(node) || this.#below.has(node); + let parent; + if (typeof node === "string") { + parent = this.#edges.get(conversationOf(node)); + } else { + const known = this.#live.get(node) ?? this.#settled.get(node); + if (known === void 0) + return; + parent = parentNode(known); + } + if (parent === void 0) + return; + if (parent === null) { + const root2 = conversationOf(node); + if (needed) + this.#roots.add(root2); + else + this.#roots.delete(root2); + return; + } + if (this.#below.get(parent)?.has(node) === true === needed) + return; + if (needed) { + const first = !this.#below.has(parent); + addTo(this.#below, parent, node); + if (!first) + return; + } else { + deleteFrom(this.#below, parent, node); + if (this.#below.has(parent)) + return; + this.#dropIfUnneeded(parent); + } + node = parent; + } + } + /** + * The nearest live task above ended ordinary work `node`, in the overlay's view: the one that work held, if any. Work + * at or below a background task holds nothing above it. + */ + #nearestLiveAbove(node, overlay) { + this.#record(INTENT_KEY); + for (let current = node; !current.background; ) { + const owner = current.owner ?? this.#edge(current.conversationId, overlay); + if (owner === void 0 || owner === null) + return void 0; + const record = overlay?.tasks.get(owner) ?? this.#liveRecord(owner); + if (record !== void 0 && record.state.status !== "terminal") + return record; + const above = record ?? this.#settled.get(owner); + if (above === void 0) + return void 0; + current = above; + } + return void 0; + } + /** Whether the chain above `node` is loaded, without walking: known tasks outside `#unloaded` have a loaded chain. */ + #known(node) { + if (typeof node !== "string") { + return (this.#live.has(node) || this.#settled.has(node)) && !this.#unloaded.has(node); + } + const owner = this.#edges.get(conversationOf(node)); + return owner === null || owner !== void 0 && this.#known(owner); + } + /** Owner task of a conversation, `null` when ownerless, `undefined` while not loaded. */ + #edge(id, overlay) { + if (overlay?.edges.has(id) === true) + return overlay.edges.get(id); + this.#record(conversationKey(id)); + return this.#edges.get(id); + } + #node(id, overlay) { + return overlay?.tasks.get(id) ?? this.#live.get(id) ?? this.#settled.get(id); + } + /** Walk up from `start`: owner tasks and conversations, ending at an ownerless root or an edge not loaded yet. */ + *#above(start, overlay) { + let at = start; + while (at !== void 0) { + if ("task" in at) { + const node = this.#node(at.task, overlay); + if (node === void 0) { + yield { unknown: true }; + return; + } + yield { task: at.task, node }; + at = parentOf(node); + } else { + yield { conversation: at.conversation }; + const edge = this.#edge(at.conversation, overlay); + if (edge === void 0) { + yield { unknown: true }; + return; + } + at = edge === null ? void 0 : { task: edge }; + } + } + } + /** + * The tasks below `start` that a walk down reaches: those `#below` lists and, under an overlay, the commit's new tasks, + * with its candidates in place of committed records. Background tasks are reached but not entered, unless + * `crossBackground`, and neither are the tasks `enter` rejects. A loop, as chains can be deeper than the call stack. + */ + *#walkDown(start, options = {}) { + const { overlay, crossBackground = false, enter } = options; + const links = overlay === void 0 ? void 0 : this.#links(overlay); + const pending = [start]; + for (let at = pending.pop(); at !== void 0; at = pending.pop()) { + this.#record(ownedKey(at)); + for (const children of [this.#below.get(at), links?.get(at)]) { + for (const child of children ?? []) { + if (typeof child === "string") { + pending.push(child); + continue; + } + const record = overlay?.tasks.get(child) ?? this.#live.get(child); + const node = record ?? this.#settled.get(child); + if (node === void 0) + continue; + const live = record !== void 0 && record.state.status !== "terminal" ? record : void 0; + yield { id: child, node, live }; + if ((crossBackground || !node.background) && (enter === void 0 || enter(child))) + pending.push(child); + } + } + } + } + /** + * The links walks down need for the commit's new tasks: each one under its parent node, then each node above it that + * `#below` does not list yet, until one it lists, a live task (always listed), or the chain's end. + */ + #links(overlay) { + if (overlay.links !== void 0) + return overlay.links; + const links = /* @__PURE__ */ new Map(); + for (const record of overlay.tasks.values()) { + if (this.#live.has(record.id) || this.#settled.has(record.id)) + continue; + let child = record.id; + let parent = parentNode(record); + while (parent !== void 0 && parent !== null && links.get(parent)?.has(child) !== true) { + addTo(links, parent, child); + if (typeof parent !== "string" && (this.#live.has(parent) || overlay.tasks.has(parent))) + break; + let above; + if (typeof parent === "string") { + above = this.#edge(conversationOf(parent), overlay); + } else { + const fields = this.#settled.get(parent); + above = fields === void 0 ? void 0 : parentNode(fields); + } + if (above !== void 0 && above !== null && this.#below.get(above)?.has(parent) === true) + break; + child = parent; + parent = above; + } + } + overlay.links = links; + return links; + } + /** + * The live ordinary owned work of `owner` (spec §5.5): the live non-background tasks below it that a walk down reaches + * without entering a background task; with `first`, only the first. Candidates in `overlay` replace committed records; + * the walk does not enter tasks in `done`, which have none. Owner chains must be loaded. + */ + #ownedLive(owner, options = {}) { + const { overlay, first = false, done } = options; + const found = []; + const walk = this.#walkDown(owner, { + ...overlay === void 0 ? {} : { overlay }, + ...done === void 0 ? {} : { enter: (id) => !done.has(id) } + }); + for (const { id, node, live } of walk) { + if (live === void 0 || node.background) + continue; + found.push(id); + if (first) + break; + } + return found; + } + #hasOwnedLive(owner, overlay, done) { + const options = { + first: true, + ...overlay === void 0 ? {} : { overlay }, + ...done === void 0 ? {} : { done } + }; + return this.#ownedLive(owner, options).length > 0; + } + /** + * Whether ordinary traversal from `scope` reaches `start`: walking up reaches the scope's conversation, or an + * ownerless one for `roots`, without crossing a background owner, unless `crossBackground`. `undefined` while an + * edge is not loaded. + */ + #inScope(start, scope, crossBackground = false) { + this.#record(INTENT_KEY); + for (const step of this.#above(start)) { + if ("unknown" in step) + return void 0; + if ("conversation" in step) { + if ("conversation" in scope && step.conversation === scope.conversation) + return true; + } else if (step.node.background && !crossBackground) { + return false; + } + } + return "roots" in scope; + } + /** + * Whether a live owner's cancellation intent reaches `start`: walking up finds an owner with intent before a + * background owner without it. Terminal owners never cascade (spec §5.4). + */ + #belowCancelled(start) { + return this.#cancellingOwner(start) !== void 0; + } + /** The nearest live owner above `start` whose cancellation intent reaches it, if any; see `#belowCancelled`. */ + #cancellingOwner(start) { + this.#record(INTENT_KEY); + if (this.#intent.size === 0) + return void 0; + for (const step of this.#above(start)) { + if ("unknown" in step) + return void 0; + if (!("task" in step)) + continue; + const live = this.#live.get(step.task); + if (live !== void 0 && cancellationIntent(live)) + return live; + if (step.node.background) + return void 0; + } + return void 0; + } + /** + * Count a task in or out of `pi.durable.tasks.active`. Commit observers count at a commit's publication, which must not + * throw, so a throwing adapter is reported. + */ + #countActive(delta, kind) { + try { + this.#activeMetric.add(delta, { task: kind }); + } catch (error) { + this.#report(error, TELEMETRY2); + } + } + /** Fail the Session, which reports it, for a throw in one of the scheduler's own commits; ignored once closing. */ + #failSession(error, context) { + if (!this.#closing) + this.#session.fail(error, context); + } + /** Close listener: runs synchronously once admission is sealed, before `join()`, or when the Session fails. */ + #seal() { + for (const record of this.#live.values()) + this.#countActive(-1, record.kind); + this.#resumed.clear(); + this.#closing = true; + try { + this.#unsubscribeRegistry(); + } catch (error2) { + this.#report(error2, REGISTRY); + } + const error = closedError(this.#session); + this.#taskWaiters.rejectAll(error); + this.#idleWaiters.rejectAll(error); + this.#contexts.clear(); + this.#idleOrder.clear(); + this.#expiry?.disarm(); + this.#expiry = void 0; + this.#timer?.disarm(); + this.#timer = void 0; + } + /** + * Recompute the wake in a microtask the line never waits for, with no order relative to host commit listeners (one + * already queued serves later changes too): arm the built-in timer, or tell `onWake` of a change. + */ + #scheduleWake() { + if (this.#wakeScheduled || this.#closing || this.#readOnly) + return; + this.#wakeScheduled = true; + queueMicrotask(() => { + this.#wakeScheduled = false; + if (this.#closing) + return; + const wake = this.wake(); + if (this.#onWake === void 0) { + this.#armTimer(this.#enabled ? wake.at : void 0); + return; + } + const last = this.#lastWake; + if (last !== void 0 && last.active === wake.active && last.at === wake.at) + return; + try { + this.#onWake(wake); + this.#lastWake = wake; + } catch (error) { + this.#report(error, ON_WAKE); + } + }); + } + /** + * Keep the timer for the earliest sleep. It keeps the process alive, unlike the context expiry timer: a process with a + * sleeping task has work left, such as a retry backoff. Rechecks the clock when it fires, so a wake before `until` + * only arms it again. + */ + #armTimer(at) { + if (this.#timer !== void 0 && this.#timer.at === at) + return; + this.#timer?.disarm(); + this.#timer = void 0; + if (at === void 0 || this.#closing) + return; + const disarm = this.#timers.foreground(() => { + this.#timer = void 0; + this.#wakeDue(); + this.#kick(); + }, at - this.#now()); + this.#timer = { at, disarm }; + } + /** Make every sleeping task whose time has come a reservation candidate. */ + #wakeDue() { + for (const id of this.#sleeping) { + const record = this.#live.get(id); + if (record !== void 0 && !this.#notDue(record)) + this.#runnable.add(id); + } + } + /** + * The report info of a task's failure, with the extension that installed its definition: the code that failed is + * that extension's. Built-in tasks have none. + */ + /** What a report about `task`'s own code says of it: the task, and the extension that installed its definition. */ + #aboutTask(task) { + let extension; + if ("extension" in task) + extension = task.extension; + else { + try { + const snapshot = this.#registry.snapshot(); + extension = installerOf(snapshot, snapshot.task(task.kind)); + } catch { + } + } + return { task: reportTask(task), ...optionalExtension(extension) }; + } + /** `settings.restartLimit`, or the default when the host's settings throw. */ + #restartLimit() { + try { + return this.#settings().restartLimit; + } catch (error) { + this.#report(error, SETTINGS); + return DEFAULT_RESTART_LIMIT; + } + } + #notifyBlocked() { + for (const listener of [...this.#blockedListeners]) { + try { + listener(); + } catch (error) { + this.#report(error, { kind: "listener_error" }); + } + } + } + /** A sleep whose `until` the clock has not reached; an abort mark ends a sleep. */ + #notDue(record) { + const state = record.state; + return state.status === "sleeping" && !record.abortRequested && state.until > this.#now(); + } + #kick() { + this.#scheduleWake(); + this.#dirty = true; + if (this.#draining || !this.#enabled || this.#closing) + return; + this.#draining = true; + queueMicrotask(() => void this.#drain()); + } + async #drain() { + try { + while (this.#dirty && this.#enabled && !this.#closing) { + this.#dirty = false; + for (const reservation of await this.#reserve()) + this.#start(reservation); + } + } catch (error) { + this.#failSession(error); + } finally { + this.#draining = false; + if (this.#dirty) + this.#kick(); + } + } + /** + * Reserve every eligible task in one commit; orphan abort-marked tasks no definition can take, unless abandoned after + * a restart. The first pass after open only abort-marks the abandoned tasks, so later passes see the marks. + */ + async #reserve() { + const reservations = []; + const faulted = []; + let abandoned = []; + try { + await this.#session.commitInternal(async (tx) => { + if (!this.#enabled || this.#closing) + return; + if (this.#abandoned.size > 0) { + const ids = [...this.#abandoned]; + const unmarked = await this.#region(() => ids.map((id) => this.#liveRecord(id)).filter((record) => record !== void 0 && !record.abortRequested)); + for (const record of unmarked) + tx.setTask(withAbortMark(record, "restart")); + abandoned = ids; + return; + } + await this.#loadScopes(false); + this.#wakeDue(); + const plan = await this.#region(() => this.#planReservation(), this.#reloadScopes); + for (const id of plan.dropped) + this.#runnable.delete(id); + if (plan.cascade) { + this.#cascadePending = true; + this.#scheduleReconcile(); + } + let snapshot; + for (const record of plan.ready) { + const id = record.id; + const mode = record.abortRequested ? "abort" : "run"; + if (mode === "run" && this.#held) + continue; + const cutOff = record.restarts !== void 0 && record.restarts >= this.#restartLimit(); + if (cutOff && (mode === "abort" || !this.#limitsRestarts(record.kind))) { + const message = mode === "abort" ? `Abort handler of task ${id} (${record.kind}) was cut off ${record.restarts} times` : `Task ${id} (${record.kind}) was cut off ${record.restarts} times without progress`; + await this.#terminate(tx, record, { + status: "faulted", + error: { message, detail: { reason: "restart_limit" } } + }); + faulted.push({ record, message }); + continue; + } + snapshot ??= this.#registry.snapshot(); + const resolution = this.#resolve(record, snapshot); + if (resolution.kind === "blocked") { + if (mode === "abort" && record.abortReason === void 0) { + await this.#terminate(tx, record, { status: "orphaned", reason: resolution.reason }); + } + continue; + } + if (resolution.record !== record || record.state.status !== "running") { + tx.setTask(withState(resolution.record, { + status: "running", + checkpoint: resolution.record.state.checkpoint + })); + } + this.#runnable.delete(id); + const invocation = this.#createInvocation(record, mode, installerOf(snapshot, resolution.task)); + reservations.push({ invocation, task: resolution.task, snapshot }); + } + }, this.#context); + } catch (error) { + for (const { invocation } of reservations) { + this.#invocations.delete(invocation.taskId); + this.#runnable.add(invocation.taskId); + invocation.finish(); + } + throw error; + } + for (const { record, message } of faulted) { + this.#report(new Error(message), { + kind: "task_faulted", + ...this.#aboutTask(recordTask(record)), + reason: "restart_limit" + }); + } + for (const id of abandoned) + this.#abandoned.delete(id); + if (abandoned.length > 0) + this.#dirty = true; + return reservations; + } + /** + * A reservation pass's decision region: the candidates it drops (gone, holding, waiting, asleep, or with an + * invocation), whether one waits for a cascade, and the runnable records to resolve. Iterating the candidates is not + * keyed: a pass that misses a task a pending commit makes runnable only delays it, as does skipping one with an + * invocation without reading its record, or a waiter's check whether what it waits on is still live. + */ + #planReservation() { + const dropped = []; + const ready = []; + let cascade = false; + for (const id of this.#runnable) { + if (this.#invocations.has(id)) { + dropped.push(id); + continue; + } + const record = this.#liveRecord(id); + if (record === void 0 || record.state.status === "completing" || this.#waitingOn(record, true).length > 0 || this.#notDue(record)) { + dropped.push(id); + continue; + } + if (!record.abortRequested && !record.background && this.#belowCancelled(parentOf(record))) { + cascade = true; + continue; + } + ready.push(record); + } + return { dropped, cascade, ready }; + } + /** + * Live tasks a task waits for before its next invocation: its live ordinary owned work when abort-marked, since + * abort runs bottom-up, otherwise the live part of the `on` of a wait. + */ + #waitingOn(record, first = false) { + if (record.abortRequested) + return this.#ownedLive(record.id, { first }); + if (record.state.status !== "waiting") + return []; + if (!first) + return record.state.on.filter((id) => this.#live.has(id)); + const live = record.state.on.find((id) => this.#live.has(id)); + return live === void 0 ? [] : [live]; + } + /** Resolve the record's definition by kind, migrating an older stored version. */ + #resolve(record, snapshot) { + const fit = this.#fit(record, snapshot.task(record.kind)); + if ("reason" in fit) + return { kind: "blocked", reason: fit.reason }; + if (!fit.migrates) + return { kind: "ready", task: fit.task, record }; + const definition = erased(fit.task); + try { + if (definition.migrate === void 0) + throw missingMigration(record, definition); + const migrated = definition.migrate(record.input, record.state.checkpoint, record.version); + const state = { ...record.state, checkpoint: copyJson(migrated.checkpoint) }; + const migratedRecord = { ...record, version: definition.version, input: copyJson(migrated.input), state }; + return { kind: "ready", task: fit.task, record: migratedRecord }; + } catch (error) { + this.#failedMigrations.set(record.id, { task: fit.task, error }); + queueMicrotask(() => this.#notifyBlocked()); + this.#scheduleWake(); + const extension = installerOf(snapshot, fit.task); + this.#report(error, { kind: "migration_failed", ...this.#aboutTask({ ...recordTask(record), extension }) }); + return { kind: "blocked", reason: "migration_failed" }; + } + } + #fit(record, task) { + if (task === void 0) + return { reason: "missing_task" }; + const version = task.definition.version; + if (version === record.version) + return { task, migrates: false }; + if (version < record.version) + return { reason: "task_too_old" }; + const failed = this.#failedMigrations.get(record.id); + if (failed?.task === task) + return { reason: "migration_failed", error: failed.error }; + return { task, migrates: true }; + } + /** Sizes of the ownership indexes, for tests that bound the scheduler's memory. */ + get indexSizes() { + let below = 0; + for (const children of this.#below.values()) + below += children.size; + return { + live: this.#live.size, + settled: this.#settled.size, + edges: this.#edges.size, + below, + roots: this.#roots.size, + unloaded: this.#unloaded.size, + dropQueue: this.#dropQueue.size, + pins: this.#pins.size + }; + } + /** + * The kept contexts' idle state, for tests (a diagnostic, computed on demand): how many are kept and how many are + * idle, how many disagree with a full recompute (an idle period without `#idle()`, or `#idle()` without one; an + * expiry order that differs from the idle periods, or is not sorted), and how many idle checks covered every scope. + */ + get idleState() { + let mismatches = 0; + let idle = 0; + for (const [conversationId, kept] of this.#contexts) { + if (kept.idleSince !== void 0) + idle++; + if (kept.idleSince !== void 0 !== this.#idle(conversationId)) + mismatches++; + if (this.#idleOrder.get(conversationId) !== kept.idleSince) + mismatches++; + } + let previous = Number.NEGATIVE_INFINITY; + for (const [conversationId, idleSince] of this.#idleOrder) { + if (idleSince < previous || this.#contexts.get(conversationId)?.idleSince !== idleSince) + mismatches++; + previous = idleSince; + } + return { + contexts: this.#contexts.size, + idleContexts: idle, + idleMismatches: mismatches, + fullIdleChecks: this.#fullIdleChecks + }; + } + /** + * Scheduling state and every live task with its derived state, read on the Session line. Runs no task code: a + * pending migration shows as `ready` with `migrates`, and only a migration the scheduler already tried, or one that + * cannot exist, shows as failed. + */ + async inspect(snapshot) { + await this.#published(); + await this.#loadScopes(false); + const tasks = []; + for (const record of this.#live.values()) + tasks.push({ record, state: this.#inspectTask(record, snapshot) }); + const scheduling = this.#closing ? "closing" : this.#enabled && !this.#held ? "running" : "paused"; + return { scheduling, tasks }; + } + #inspectTask(record, snapshot) { + if (this.#invocations.has(record.id)) + return { kind: "running" }; + if (record.state.status === "completing") + return { kind: "completing" }; + const on = this.#waitingOn(record); + if (on.length > 0) + return { kind: "waiting", on }; + if (this.#notDue(record)) + return { kind: "sleeping", until: record.state.until }; + const fit = this.#fit(record, snapshot.task(record.kind)); + if ("reason" in fit) + return { kind: "blocked", ...fit }; + if (fit.migrates && fit.task.definition.migrate === void 0) { + return { kind: "blocked", reason: "migration_failed", error: missingMigration(record, erased(fit.task)) }; + } + return { kind: "pending", migrates: fit.migrates }; + } + #createInvocation(record, mode, extension) { + const controller = new AbortController(); + const { promise: done, resolve: finish } = Promise.withResolvers(); + const invocation = { + taskId: record.id, + kind: record.kind, + extension, + conversationId: record.conversationId, + mode, + controller, + links: record.startedAt === void 0 ? record.links : void 0, + context: withAbortSignal(controller.signal, this.#context), + record, + watches: /* @__PURE__ */ new Set(), + timers: /* @__PURE__ */ new Set(), + ended: false, + done, + finish: () => finish() + }; + this.#invocations.set(record.id, invocation); + return invocation; + } + #start(reservation) { + const invocation = reservation.invocation; + void (async () => { + try { + if (invocation.mode === "run") + await this.#run(reservation); + else + await this.#runAbort(reservation); + } catch (error) { + this.#report(error, { kind: "task_error", ...this.#aboutTask(invocation) }, invocation.context); + } finally { + this.#end(invocation); + invocation.record = void 0; + invocation.links = void 0; + invocation.fault = void 0; + invocation.finish(); + this.#kick(); + } + })(); + } + /** Run phase handlers, each preceded by a step that decides on the line whether the invocation continues. */ + async #run(reservation) { + const invocation = reservation.invocation; + const state = { task: reservation.task, snapshot: reservation.snapshot, reported: void 0 }; + const phase = { snapshot: () => state.snapshot, task: () => state.task, agent: void 0 }; + const runtime = this.#runtime(invocation, phase); + const progress = (tx, current2) => this.#progress(tx, current2, state, invocation.extension, invocation.context); + let current = await this.#step(invocation, (task) => this.#rules(task, void 0), progress); + while (current !== void 0 && !this.#closing) { + const task = current; + current = await this.#phase(invocation, task, async () => { + const checkpoint = task.state.checkpoint; + phase.agent = void 0; + let previous; + try { + await erased(state.task).phases[checkpoint.phase](task, runtime, invocation.context); + previous = { checkpoint }; + } catch (error) { + previous = { checkpoint, failure: { error } }; + } + return this.#step(invocation, (next) => this.#rules(next, previous), progress); + }); + } + } + /** A phase owns its decision commit, not the next handler, a durable wait, or recovery. */ + #phase(invocation, task, run2) { + const parent = task.trace === void 0 ? this.#telemetry : this.#telemetry.propagation?.extract(task.trace) ?? this.#telemetry; + const checkpoint = task.state.checkpoint; + const attempt = checkpoint.attempt; + const resumed = this.#resumed.delete(task.id); + return createTypedSpanStarter(parent, [DURABLE_TELEMETRY_SCHEMA])("pi.durable.phase", { + "task.id": task.id, + "task.name": task.kind, + phase: checkpoint.phase, + kind: invocation.mode, + resumed, + ...attempt === void 0 ? {} : { attempt } + }, async (span) => { + if (span.propagation !== void 0 && invocation.links !== void 0) { + for (const carrier of invocation.links) + span.addLink({ context: span.propagation.extract(carrier) }); + } + const previous = invocation.context; + invocation.context = withTelemetry(span, previous); + const started = performance.now(); + try { + return await run2(); + } finally { + invocation.context = previous; + const state = invocation.record.state; + const terminal = invocation.fault ?? (state.status === "terminal" || state.status === "completing" ? state.outcome : void 0); + const outcome = terminal !== void 0 ? terminal.status === "completed" ? "done" : terminal.status === "aborted" ? "aborted" : "faulted" : state.status === "waiting" || state.status === "sleeping" ? state.status : invocation.ended || this.#closing ? "aborted" : `next=${state.checkpoint.phase}`; + span.setAttributes({ outcome }); + if (terminal?.status === "faulted" || terminal?.status === "failed" || terminal?.status === "orphaned") { + const message = terminal.status === "orphaned" ? terminal.reason : terminal.error.message; + span.setStatus({ + status: "error", + error: { name: `task_${terminal.status}`, message: ownedSlice2(message, 0, 500) } + }); + } else if (outcome === "aborted") { + span.setStatus({ status: "ok" }); + } + this.#durationMetric.record(performance.now() - started, { task: task.kind, phase: checkpoint.phase, outcome }, span); + } + }); + } + /** + * Precedence rules for a run invocation, a decision region of its step. Rules 1 (terminal, `completing`, `waiting`, or + * `sleeping`) and 2 (closing) are applied by `#step`. Progress (rule 5) is decided by `#progress()` once the decision + * stands, as it takes a registry snapshot. + */ + #rules(current, previous) { + if (current.abortRequested) + return END; + if (!current.background && this.#belowCancelled(parentOf(current))) + return CASCADE; + if (previous === void 0) + return CONTINUE; + if (previous.failure !== void 0) + return { kind: "fault", error: previous.failure.error }; + if (jsonEqual(current.state.checkpoint, previous.checkpoint)) { + const message = `Task ${current.kind} phase ${previous.checkpoint.phase} returned without durable progress`; + return { kind: "fault", error: new Error(message) }; + } + return PROGRESS; + } + /** + * Rule 5, progress: refresh the snapshot; hand over to a replacement definition that can take the task, writing it + * back as `pending`, or keep running under the old one, reported once per resolved definition. Returns whether the + * invocation continues. + */ + #progress(tx, current, state, extension, context) { + if (this.#held) { + tx.setTask(withState(current, { status: "pending", checkpoint: current.state.checkpoint })); + return false; + } + state.snapshot = this.#registry.snapshot(); + const next = state.snapshot.task(current.kind); + if (next === state.task) + return true; + if (next !== void 0 && canReserve(next, current)) { + tx.setTask(withState(current, { status: "pending", checkpoint: current.state.checkpoint })); + return false; + } + if (state.reported === void 0 || state.reported.task !== next) { + state.reported = { task: next }; + this.#report(new Error(`Task ${current.id} keeps running under its old ${current.kind} definition`), { + kind: "task_outdated", + ...this.#aboutTask({ ...recordTask(current), extension }), + reason: outdatedReason(next, current) + }, context); + } + return true; + } + /** Run the abort handler once; rules 1, 2, and 4 apply, and returning without an outcome faults. */ + async #runAbort(reservation) { + const invocation = reservation.invocation; + const current = this.#live.get(invocation.taskId); + if (current === void 0 || this.#closing) + return; + await this.#phase(invocation, current, async () => { + let failure; + try { + const phase = { + snapshot: () => reservation.snapshot, + task: () => reservation.task, + agent: void 0 + }; + const runtime = this.#runtime(invocation, phase); + await erased(reservation.task).abort(current, runtime, invocation.context); + } catch (error) { + failure = { error }; + } + const message = `Abort handler of task ${invocation.taskId} returned without a terminal outcome`; + await this.#step(invocation, () => ({ kind: "fault", error: failure?.error ?? new Error(message) })); + }); + } + /** + * One decision on the Session line, before a phase: a decision region (`#region()`) that reads the task's record. A + * task that is no longer running (rule 1: terminal, `completing`, `waiting`, or `sleeping`) or a closing Harness (rule + * 2) ends the invocation without a write; otherwise `plan` decides, and a decision that stands is applied: the + * invocation continues, with `progress` for rule 5, or ends, with a fault written. Ending happens inside the callback, + * before a fault's Harness cleanup. A rejected step, such as admission after close, also ends the invocation. Its + * outcome goes to the invocation only, so it is a scheduler commit (`commitInternal()`). + */ + async #step(invocation, plan, progress) { + let fault; + try { + const continued = await this.#session.commitInternal(async (tx) => { + const decided = await this.#region(() => { + const found = this.#liveRecord(invocation.taskId); + if (found?.state.status !== "running" || this.#closing) + return { plan: END }; + const current2 = found; + return { current: current2, plan: plan(current2) }; + }); + const { current } = decided; + const step = decided.plan; + if (step.kind === "continue") + return current; + if (step.kind === "progress" && progress(tx, current)) + return current; + this.#end(invocation); + if (step.kind === "cascade") { + this.#cascadePending = true; + this.#scheduleReconcile(); + } + if (step.kind === "fault") { + fault = { error: step.error }; + const message = errorMessage(step.error); + invocation.fault = { status: "faulted", error: { message } }; + await this.#terminate(tx, current, invocation.fault); + } + return void 0; + }, withoutAbortSignal(invocation.context)); + if (fault !== void 0) + this.#report(fault.error, { kind: "task_faulted", ...this.#aboutTask(invocation), reason: "threw" }, invocation.context); + return continued; + } catch (error) { + this.#end(invocation); + this.#failSession(error, invocation.context); + return void 0; + } + } + /** + * Write an outcome the scheduler decided. While the task's ordinary owned work is live it holds as `completing` and + * its Harness cleanup waits for the final commit (spec §5.5, rule 4); otherwise it is terminal with its cleanup. + */ + async #terminate(tx, record, outcome) { + await this.#loadScopes(false); + if (await this.#region(() => this.#hasOwnedLive(record.id, overlayOf(tx)), this.#reloadScopes)) { + tx.setTask(withState(record, { status: "completing", outcome })); + return; + } + tx.setTask(withState(record, { status: "terminal", outcome })); + await this.#settleOutcome(tx, record, outcome); + } + /** + * Replace a running task's state with what it committed. A terminal state holds as `completing` while ordinary owned + * work is live, judged on the commit's candidates, so work the same commit creates below the task counts. A wait is + * validated first. + */ + async #commitState(tx, invocation, current, next) { + if (next.status === "waiting") + await this.#validateWait(tx, invocation, current, next.on, next.policy); + if (next.status === "sleeping") + validateSleep(invocation, current.id, next.until); + if (next.status === "terminal") { + const overlay = overlayOf(tx); + await this.#loadScopes(false); + for (const record of overlay.tasks.values()) + await this.#loadChain(parentOf(record), overlay); + if (await this.#region(() => this.#hasOwnedLive(current.id, overlayOf(tx)), this.#reloadScopes)) { + tx.setTask(withState(progressed(current), { status: "completing", outcome: next.outcome })); + return; + } + } + const same = jsonEqual(current.state, next); + tx.setTask(withState(same ? current : progressed(current), next)); + } + /** + * A wait names existing tasks, and `failFast` only tasks the waiter owns. It may not close a cycle in the wait-for + * graph, whose edges run from each task to what it waits on and to its live ordinary owned work (spec §5.5); waiting + * on itself or on an owner holding it are the shortest such cycles. An abort handler cannot wait. + * + * Invariant: the wait-for graph is acyclic. Only a wait adds an edge between existing tasks, as a new task has only + * incoming edges, so checking each wait maintains it. + */ + async #validateWait(tx, invocation, current, on, policy) { + if (invocation.mode === "abort") + throw new Error(`Abort handler of task ${current.id} cannot wait`); + await this.#loadScopes(false); + const ended = /* @__PURE__ */ new Map(); + for (; ; ) { + const checked = await this.#region(() => this.#checkWait(current, on, policy, overlayOf(tx), ended), this.#reloadScopes); + if (checked === void 0) + return; + if ("missing" in checked) { + for (const id of checked.missing) + ended.set(id, await this.#session.lineStorage.task(id, this.#context) ?? null); + continue; + } + throw checked.error; + } + } + /** + * `#validateWait()`'s decision region: why the wait is refused, `undefined` when it may be stored, or the tasks in + * `on` it needs from Storage first, when `ended` lacks them. + */ + #checkWait(current, on, policy, overlay, ended) { + const targets = []; + const missing = []; + for (const id of on) { + const member = overlay.tasks.get(id) ?? this.#liveRecord(id) ?? ended.get(id); + if (member === void 0) { + missing.push(id); + continue; + } + if (member === null) + return { error: new Error(`Task ${id} does not exist`) }; + if (policy === "failFast" && member.owner !== current.id) { + return { error: new Error(`Task ${current.id} can wait failFast only on tasks it owns; ${id} is not one`) }; + } + if (member.state.status !== "terminal" && member.owner !== current.id) + targets.push(id); + } + if (missing.length > 0) + return { missing }; + if (targets.length === 0) + return void 0; + const cycle = this.#waitCycle(current.id, targets, overlay); + if (cycle === void 0) + return void 0; + return { + error: new Error(`Task ${current.id} cannot wait on ${cycle[0]}: ${[current.id, ...cycle].join(" -> ")} is a wait cycle`) + }; + } + /** + * The path from a member of `on` back to `task` in the wait-for graph, if the wait would close a cycle (spec §5.1). + * An edge X -> Y means X cannot settle before Y: Y is in the `on` of waiting X that is not abort-marked, or Y is live + * ordinary owned work of X, which holds X as `completing`. Each task is visited once and each subtree walked once, + * so a check is bounded by the live work it reaches. Part of a decision region: it records every record it reads. + * Owned work is found by walks down, which need the chains of every live task loaded. + */ + #waitCycle(task, on, overlay) { + const from = /* @__PURE__ */ new Map(); + const walked = /* @__PURE__ */ new Set(); + const pending = []; + const reach = (id, via) => { + if (from.has(id)) + return; + from.set(id, via); + pending.push(id); + }; + const path2 = (end) => { + const steps = []; + for (let at = end; at !== void 0; at = from.get(at)) + steps.unshift(at); + return steps; + }; + for (const id of on) + reach(id, void 0); + for (let id = pending.pop(); id !== void 0; id = pending.pop()) { + if (id === task) + return path2(id); + const record = overlay.tasks.get(id) ?? this.#liveRecord(id); + if (record === void 0 || record.state.status === "terminal") + continue; + if (record.state.status === "waiting" && !record.abortRequested) { + for (const member of record.state.on) + reach(member, id); + } + if (walked.has(id)) + continue; + walked.add(id); + const found = []; + const walk = this.#walkDown(id, { overlay, enter: (below) => !walked.has(below) }); + for (const { id: below, node, live } of walk) { + if (live === void 0 || node.background) + continue; + found.push(below); + reach(below, id); + } + for (const below of found) + walked.add(below); + } + return void 0; + } + /** End an invocation: its runtime operations reject from now on, its signal aborts, its watches stop, and its task is free. */ + #end(invocation) { + if (invocation.ended) + return; + invocation.ended = true; + if (this.#invocations.get(invocation.taskId) === invocation) { + this.#invocations.delete(invocation.taskId); + if (this.#live.has(invocation.taskId)) + this.#runnable.add(invocation.taskId); + } + for (const watch of invocation.watches) + void watch.stop(); + for (const disarm of invocation.timers) + disarm(); + invocation.timers.clear(); + invocation.controller.abort(endedError(invocation)); + } + /** No live non-background task in the scope; a task whose owner edges are not loaded yet counts as inside. */ + #idle(conversationId) { + const scope = conversationId === void 0 ? { roots: true } : { conversation: conversationId }; + if (conversationId === void 0) + this.#record(ROOTS_KEY); + for (const id of this.#unloaded) { + const record = this.#live.get(id); + if (record !== void 0 && !record.background && this.#inScope(parentOf(record), scope) !== false) + return false; + } + const starts = conversationId === void 0 ? this.#roots : [conversationId]; + for (const start of starts) { + for (const { node, live } of this.#walkDown(conversationNode(start))) { + if (live !== void 0 && !node.background) + return false; + } + } + return true; + } + // ─── Invocation runtime ────────────────────────────────────────────────── + #runtime(invocation, phase) { + const agent2 = () => { + if (invocation.ended) + return Promise.reject(endedError(invocation)); + if (phase.agent === void 0) { + phase.agent = this.#agent(invocation.conversationId, phase.snapshot(), invocation.context); + phase.agent.catch(() => { + }); + } + return phase.agent; + }; + const hooks = { + each: async (name, invoke) => { + for (const { extension, handlers } of agentHooks(await agent2(), phase.task().definition.name)) { + const handler = handlers[name]; + if (typeof handler !== "function") + continue; + try { + await invoke(handler.bind(handlers)); + } catch (error) { + if (invocation.controller.signal.aborted) + throw error; + this.#report(error, { kind: "extension_error", extension, task: reportTask(invocation) }, invocation.context); + } + } + } + }; + const settings = this.#settings; + const models2 = this.#models; + return { + taskId: invocation.taskId, + conversationId: invocation.conversationId, + signal: invocation.controller.signal, + get models() { + return models2(); + }, + agent: (context) => invocation.ended ? Promise.reject(endedError(invocation)) : awaitWithContext(agent2(), context), + get settings() { + return settings(); + }, + env: (context) => this.#read(invocation, () => this.#env(invocation.conversationId, context)), + hooks, + get registry() { + return phase.snapshot(); + }, + commit: async (change, context, options) => { + let checkpoint; + let interrupted2; + await this.#gated(invocation, async (tx, current) => { + const next = await change(tx, current); + if (next !== void 0) + await this.#commitState(tx, invocation, current, next); + if (next?.status === "running" && current.state.checkpoint.phase !== next.checkpoint.phase) { + checkpoint = next.checkpoint.phase; + } + if (next?.status === "terminal") { + const restarts = current.restarts ?? 0; + const reason = next.outcome.status === "failed" ? interruptionOf(next.outcome.error) : void 0; + if (reason !== void 0 && next.outcome.status === "failed") { + interrupted2 = { message: next.outcome.error.message, reason, restarts }; + } else { + const message = endedByCrashesStates.get(next); + if (message !== void 0) + interrupted2 = { message, reason: "restart_limit", restarts }; + } + } + }, context, options); + if (interrupted2 !== void 0) { + this.#report(new Error(interrupted2.message), { + kind: "task_interrupted", + ...this.#aboutTask(invocation), + reason: interrupted2.reason, + restarts: interrupted2.restarts + }, invocation.context); + } + if (checkpoint !== void 0) { + invocation.context.telemetry.addEvent("pi.durable.checkpoint", { + phase: checkpoint + }); + } + }, + memo: ((name, ...rest) => { + if (rest.length === 1) { + return this.#read(invocation, async () => memoOf(this.#live.get(invocation.taskId), name)); + } + const candidate = rest[0]; + return this.#gated(invocation, (tx, current) => { + const winner = memoOf(current, name); + if (winner !== void 0) + return winner; + tx.setTask({ ...current, memos: { ...current.memos, [name]: candidate } }); + return candidate; + }, rest[1]); + }), + sleepUntil: (until, checkpoint, context) => this.#gated(invocation, (tx, current) => this.#commitState(tx, invocation, current, { status: "sleeping", checkpoint, until }), context), + watchDoc: ((...args) => this.#watchDoc(invocation, args)), + snapshot: ((...args) => this.#read(invocation, () => sessionMethod(this.#session, "snapshot")(...args))), + snapshotAsOf: ((...args) => this.#read(invocation, () => sessionMethod(this.#session, "snapshotAsOf")(...args))), + getTask: ((id, context) => this.#read(invocation, () => this.#session.readOnLine((storage) => storage.task(id, context)))), + waitForTask: ((id, context) => this.#read(invocation, () => this.waitForTask(id, withAbortSignal(invocation.controller.signal, context)))), + ownedTasks: (context) => this.#read(invocation, () => this.#session.readOnLine(async () => { + if (invocation.ended) + throw endedError(invocation); + context.abortSignal?.throwIfAborted(); + const owned2 = await this.#region(() => { + this.#record(ownedKey(invocation.taskId)); + const records2 = []; + for (const node of this.#below.get(invocation.taskId) ?? []) { + const record = typeof node === "string" ? void 0 : this.#liveRecord(node); + if (record?.owner === invocation.taskId) + records2.push(record); + } + return records2; + }); + return owned2.sort((a, b) => a.id - b.id).map((record) => copyJson(record)); + })), + abortOwned: (id, context) => this.#read(invocation, async () => { + const bound = withAbortSignal(invocation.controller.signal, context); + const record = await this.#session.readOnLine((storage) => storage.task(id, bound)); + if (record?.owner !== invocation.taskId) { + throw new Error(`Task ${id} is not owned by task ${invocation.taskId}`); + } + if (record.state.status === "terminal") + return; + await this.abort(id, bound, true); + await this.waitForTask(id, bound); + }), + outcomes: ((ids, context) => this.#read(invocation, () => this.#session.readOnLine(async (storage) => { + const outcomes = []; + for (const id of ids) { + const state = (await storage.task(id, context))?.state; + if (state?.status !== "terminal") + throw new Error(`Task ${id} is not terminal`); + outcomes.push(state.outcome); + } + return outcomes; + }))), + conversation: (id, context) => this.#read(invocation, () => this.#conversation(id, { + signal: invocation.controller.signal, + check: () => { + if (invocation.ended) + throw endedError(invocation); + } + }, context)), + entry: ((...args) => { + const [token, id, context] = args.length === 2 ? [void 0, args[0], args[1]] : [args[0], args[1], args[2]]; + return this.#read(invocation, async () => { + const kept = this.#contexts.get(invocation.conversationId); + const entry2 = (kept === void 0 ? void 0 : rangeEntry(kept.range, id)) ?? (await this.#session.readOnLine((storage) => storage.entry(invocation.conversationId, id, context)))?.entry; + return token === void 0 || entry2?.kind === token.kind ? entry2 : void 0; + }); + }), + entries: (query, limit, cursor, context) => this.#read(invocation, () => this.#session.readOnLine((storage) => storage.scanEntries(conversationEntries(invocation.conversationId, query), limit, cursor, context))), + context: (conversationId, context, options) => this.#read(invocation, async () => { + const read2 = await readContextFrom(this.#session, this.#storage, conversationId, context, options?.at, this.#contexts.get(conversationId)?.range); + const { view: view2 } = read2; + const range = read2.range?.current === true && this.#session.knownLatestEntryId(conversationId) !== read2.range.bounds.tail ? { ...read2.range, current: false } : read2.range; + const kept = this.#contexts.get(conversationId); + if (range !== void 0 && !this.#closing && !invocation.ended && (kept === void 0 || kept.range.bounds.tail <= range.bounds.tail)) { + if (!this.#idle(conversationId)) { + this.#contexts.set(conversationId, { range, idleSince: void 0 }); + this.#idleOrder.delete(conversationId); + } else if (this.#contextRetentionMs() > 0) { + const idleSince = kept?.idleSince ?? this.#now(); + const expiresAt = kept?.idleSince !== void 0 && kept.expiresAt !== void 0 ? kept.expiresAt : idleSince + this.#contextRetentionMs(); + this.#contexts.set(conversationId, { range, idleSince, expiresAt }); + if (kept?.idleSince === void 0) + this.#markIdle(conversationId, idleSince); + this.#scheduleExpiry(); + } + } + return view2; + }), + now: () => { + if (invocation.ended) + throw endedError(invocation); + return this.#now(); + }, + setTimer: (callback, delayMs) => { + if (invocation.ended) + return () => { + }; + const disarm = this.#timers.foreground(() => { + invocation.timers.delete(disarm); + if (!invocation.ended) + callback(); + }, delayMs); + invocation.timers.add(disarm); + return () => { + invocation.timers.delete(disarm); + disarm(); + }; + }, + report: (error, from) => { + if (invocation.ended) + throw endedError(invocation); + const task = reportTask(invocation); + this.#report(error, from === void 0 ? { kind: "task_error", ...this.#aboutTask(invocation) } : from.kind === "extension" ? { kind: "extension_error", task, ...optionalExtension(from.extension) } : { kind: "host_error", callback: from.callback, task }, invocation.context); + } + }; + } + /** + * On the line: wait until every commit admitted before is published, for a decision that reads what cannot be keyed, + * every live task (open, inspection) or every task a conversation abort reaches. + */ + async #published() { + const gate = this.#session.touch(ALL_KEYS); + if (gate !== void 0) + await gate; + } + /** Run a committed-state read unless the invocation has ended. */ + async #read(invocation, read2) { + if (invocation.ended) + throw endedError(invocation); + return read2(); + } + /** Commit after rereading the task on the line and gating the invocation. */ + #gated(invocation, change, context, options) { + if (invocation.ended) + return Promise.reject(endedError(invocation)); + return this.#session.commitWith(async (tx) => { + if (invocation.ended) + throw endedError(invocation); + if (this.#closing) + throw closedError(this.#session); + const current = await this.#region(() => { + const found = this.#liveRecord(invocation.taskId); + if (found === void 0) + throw new Error(`Task ${invocation.taskId} is terminal`); + if (found.state.status !== "running") + throw new Error(`Task ${invocation.taskId} is ${found.state.status}`); + if (invocation.mode === "run" && found.abortRequested) { + throw new Error(`Task ${invocation.taskId} has a durable abort mark`); + } + return found; + }); + if (invocation.ended) + throw endedError(invocation); + return change(tx, current); + }, context, { conversationId: invocation.conversationId, taskId: invocation.taskId }, options); + } + async #watchDoc(invocation, args) { + if (invocation.ended) + throw endedError(invocation); + const watch = await sessionMethod(this.#session, "watchDoc")(...args); + if (watch === void 0) + return void 0; + if (invocation.ended) { + void watch.stop(); + throw endedError(invocation); + } + invocation.watches.add(watch); + void watch.closed.then(() => invocation.watches.delete(watch)); + return watch; + } +}; +var REGISTRY = { kind: "host_error", callback: "registry" }; +var SETTINGS = { kind: "host_error", callback: "settings" }; +var ON_WAKE = { kind: "host_error", callback: "onWake" }; +var TELEMETRY2 = { kind: "host_error", callback: "telemetry" }; +function installerOf(snapshot, task) { + if (task === void 0) + return void 0; + return snapshot.installed().findLast((installed) => installed.tasks?.includes(task) === true)?.name; +} +function reportTask(task) { + return { id: task.taskId, kind: task.kind, conversationId: task.conversationId }; +} +function recordTask(record) { + return { taskId: record.id, kind: record.kind, conversationId: record.conversationId }; +} +var endedByCrashesStates = /* @__PURE__ */ new WeakMap(); +function endedByCrashes(state, message) { + endedByCrashesStates.set(state, message); + return state; +} +function interruptionOf(error) { + const detail = error.detail; + if (typeof detail !== "object" || detail === null || !("reason" in detail)) + return void 0; + return detail.reason === "restart_limit" || detail.reason === "interrupted" ? detail.reason : void 0; +} +function withAbortMark(record, reason) { + const { abortReason: _reason, restarts: _restarts, ...rest } = record; + return { ...rest, abortRequested: true, ...reason === void 0 ? {} : { abortReason: reason } }; +} +function cancellationIntent(record) { + return record.state.status !== "terminal" && (record.abortRequested || failedOutcome(record)); +} +function intentChanges(previous, next) { + return (previous?.abortRequested ?? false) !== next.abortRequested || previous?.abortReason !== next.abortReason || (previous !== void 0 && failedOutcome(previous)) !== failedOutcome(next) || (previous !== void 0 && cancellationIntent(previous)) !== cancellationIntent(next); +} +function failedOutcome(record) { + const state = record.state; + return (state.status === "completing" || state.status === "terminal") && state.outcome.status !== "completed"; +} +function parentOf(node) { + return node.owner !== void 0 ? { task: node.owner } : { conversation: node.conversationId }; +} +function nodeOf(record) { + return { + conversationId: record.conversationId, + ...record.owner === void 0 ? {} : { owner: record.owner }, + background: record.background + }; +} +function overlayOf(tx) { + const tasks = new Map(tx.stagedTasks().map((record) => [record.id, record])); + const edges = new Map(tx.stagedConversations().map((record) => [record.id, record.owner?.taskId ?? null])); + return { tasks, edges }; +} +function addTo(map2, key, value2) { + const values = map2.get(key); + if (values === void 0) + map2.set(key, /* @__PURE__ */ new Set([value2])); + else + values.add(value2); +} +function deleteFrom(map2, key, value2) { + const values = map2.get(key); + if (values === void 0) + return; + values.delete(value2); + if (values.size === 0) + map2.delete(key); +} +function memoOf(record, name) { + const memos = record?.memos; + return memos !== void 0 && Object.hasOwn(memos, name) ? memos[name] : void 0; +} +function sessionMethod(session, name) { + return session[name].bind(session); +} +function missingMigration(record, definition) { + return new Error(`Task ${record.kind} version ${definition.version} has no migration from ${record.version}`); +} +function erased(task) { + return task.definition; +} +function endedError(invocation) { + return new Error(`Task ${invocation.taskId} invocation has ended`); +} +function progressed(record) { + if (record.restarts === void 0) + return record; + const { restarts: _restarts, ...rest } = record; + return rest; +} +function withState(record, state) { + if (state.status !== "terminal" && state.status !== "completing") + return { ...record, state }; + const { memos: _memos, restarts: _restarts, ...rest } = record; + return { ...rest, state }; +} +function outdatedReason(task, record) { + if (task === void 0) + return "missing_task"; + return task.definition.version < record.version ? "task_too_old" : "migration_failed"; +} +function canReserve(task, record) { + const definition = task.definition; + return definition.version === record.version || definition.version > record.version && definition.migrate !== void 0; +} +function validateSleep(invocation, task, until) { + if (invocation.mode === "abort") + throw new Error(`Abort handler of task ${task} cannot sleep`); + if (typeof until !== "number" || !Number.isFinite(until) || until < 0) { + throw new Error(`Task ${task} cannot sleep until ${until}: not a time in milliseconds`); + } +} +function jsonEqual(left, right) { + if (left === right) + return true; + if (typeof left !== "object" || typeof right !== "object" || left === null || right === null) + return false; + if (Array.isArray(left) || Array.isArray(right)) { + if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) + return false; + return left.every((value2, index) => jsonEqual(value2, right[index])); + } + const keys = Object.keys(left); + if (keys.length !== Object.keys(right).length) + return false; + return keys.every((key) => Object.hasOwn(right, key) && jsonEqual(left[key], right[key])); +} + +// node_modules/@earendil-works/chord/dist/output.js +var NEWLINE = 10; +var INVALID_OUTPUT = /[\x00-\x08\x0b-\x1f\ufff9-\ufffb]/g; +var ESCAPE_SEQUENCE = /\x1b[\]PX^_][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b\[[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e]|\x1b[\x20-\x2f]*[\x30-\x7e]/g; +var encoder = new TextEncoder(); +var decoder = new TextDecoder("utf-8", { ignoreBOM: true }); +function sanitizeOutput(text) { + return text.replace(ESCAPE_SEQUENCE, "").replace(INVALID_OUTPUT, ""); +} +function boundOutput(text, limits) { + const bytes2 = encoder.encode(text); + const [from, to] = limits.retain === "head" ? headRange(bytes2, limits) : tailRange(bytes2, limits); + const kept = bytes2.subarray(from, to); + return { + text: kept.length === bytes2.length ? text : decoder.decode(kept), + bytes: kept.length, + droppedBytes: bytes2.length - kept.length, + droppedLines: lineCount(bytes2) - lineCount(kept) + }; +} +function headRange(bytes2, limits) { + if (limits.maxLines === 0 || limits.maxBytes === 0) + return [0, 0]; + let end = bytes2.length; + let lines2 = 0; + for (let index = bytes2.indexOf(NEWLINE); index !== -1; index = bytes2.indexOf(NEWLINE, index + 1)) { + if (++lines2 === limits.maxLines) { + end = index + 1; + break; + } + } + if (end > limits.maxBytes) { + const newline = bytes2.lastIndexOf(NEWLINE, limits.maxBytes - 1); + end = newline === -1 ? characterEnd(bytes2, limits.maxBytes) : newline + 1; + } + return [0, end]; +} +function tailRange(bytes2, limits) { + if (limits.maxLines === 0 || limits.maxBytes === 0) + return [bytes2.length, bytes2.length]; + const last = bytes2[bytes2.length - 1] === NEWLINE ? bytes2.length - 2 : bytes2.length - 1; + let start = 0; + let lines2 = 1; + for (let index = last < 0 ? -1 : bytes2.lastIndexOf(NEWLINE, last); index !== -1; ) { + if (lines2 === limits.maxLines) { + start = index + 1; + break; + } + lines2++; + index = index === 0 ? -1 : bytes2.lastIndexOf(NEWLINE, index - 1); + } + if (bytes2.length - start > limits.maxBytes) { + const from = bytes2.length - limits.maxBytes; + const newline = bytes2.indexOf(NEWLINE, from - 1); + start = newline !== -1 && newline + 1 < bytes2.length ? newline + 1 : characterStart(bytes2, from); + } + return [start, bytes2.length]; +} +function characterEnd(bytes2, index) { + let end = index; + while (end > 0 && ((bytes2[end] ?? 0) & 192) === 128) + end--; + return end; +} +function characterStart(bytes2, index) { + let start = index; + while (start < bytes2.length && ((bytes2[start] ?? 0) & 192) === 128) + start++; + return start; +} +function lineCount(bytes2) { + if (bytes2.length === 0) + return 0; + let newlines = 0; + for (let index = bytes2.indexOf(NEWLINE); index !== -1; index = bytes2.indexOf(NEWLINE, index + 1)) + newlines++; + return newlines + (bytes2[bytes2.length - 1] === NEWLINE ? 0 : 1); +} +var OutputBuffer = class { + #limits; + /** + * Decoder of byte chunks. A string chunk or a skip ends an incomplete character of earlier bytes (it becomes U+FFFD); + * the next byte chunk then starts a new character. As in `StreamDecoder`, only a byte-order mark at the very start of + * the output is dropped, never a U+FEFF later in it. + */ + #decoder = new TextDecoder("utf-8", { ignoreBOM: true }); + #started = false; + /** Stored chunks: for head the start of the stream, for tail a suffix that still contains the next window. */ + #chunks = []; + #storedBytes = 0; + #storedNewlines = 0; + #full = false; + #totalBytes = 0; + #totalNewlines = 0; + #endsWithNewline = true; + constructor(limits) { + this.#limits = limits; + } + /** Bytes currently held; bounded by the limits plus one chunk. */ + get storedBytes() { + return this.#storedBytes; + } + /** + * Accept a chunk; returns whether anything was accepted. `skipped` is output omitted right before the chunk, which + * must be more than the tail window by at least one byte or line; only tail retention accepts it. + */ + push(chunk, skipped) { + const pending = typeof chunk === "string" || skipped !== void 0 ? this.#decoder.decode() : ""; + let text = typeof chunk === "string" ? chunk : this.#decoder.decode(chunk, { stream: true }); + const first = !this.#started && pending === "" && skipped === void 0; + if (pending !== "" || text !== "" || skipped !== void 0) + this.#started = true; + if (first && typeof chunk !== "string" && text.startsWith("\uFEFF")) + text = text.slice(1); + if (skipped === void 0) + return this.#accept(pending + text); + if (this.#limits.retain !== "tail") + throw new Error("Skipped output requires tail retention"); + this.#accept(pending); + this.#skip(skipped); + this.#accept(text); + return true; + } + /** Count omitted output; nothing stored before it can be in the window once the text after it arrives. */ + #skip(skipped) { + if (skipped.bytes === 0) + return; + this.#totalBytes += skipped.bytes; + this.#totalNewlines += skipped.newlines; + this.#endsWithNewline = skipped.endsWithNewline; + this.#chunks = []; + this.#storedBytes = 0; + this.#storedNewlines = 0; + } + /** Flush an incomplete trailing character as a replacement character; call when the stream ends. */ + end() { + this.#accept(this.#decoder.decode()); + } + #accept(text) { + if (text.length === 0) + return false; + const bytes2 = utf8ByteLength(text); + const newlines = countNewlines(text); + this.#totalBytes += bytes2; + this.#totalNewlines += newlines; + this.#endsWithNewline = text.endsWith("\n"); + if (this.#full) + return true; + this.#chunks.push({ text, bytes: bytes2, newlines }); + this.#storedBytes += bytes2; + this.#storedNewlines += newlines; + if (this.#limits.retain === "head") { + this.#full = this.#storedBytes > this.#limits.maxBytes || this.#storedNewlines >= this.#limits.maxLines; + return true; + } + while (this.#chunks.length > 1) { + const first = this.#chunks[0]; + const bytesAfter = this.#storedBytes - first.bytes; + const newlinesAfter = this.#storedNewlines - first.newlines; + if (bytesAfter <= this.#limits.maxBytes + 1 && newlinesAfter <= this.#limits.maxLines + 1) + break; + this.#chunks.shift(); + this.#storedBytes = bytesAfter; + this.#storedNewlines = newlinesAfter; + } + return true; + } + /** Retained, sanitized output and what the limits dropped from the whole stream. */ + snapshot() { + const stored = this.#chunks.length === 1 ? this.#chunks[0].text : this.#chunks.map((chunk) => chunk.text).join(""); + const kept = boundOutput(stored, this.#limits); + const storedLines = lines(this.#storedNewlines, stored === "" || stored.endsWith("\n")); + const keptLines = storedLines - kept.droppedLines; + if (this.#limits.retain === "tail" || this.#chunks.length > 1) { + const text = this.#limits.retain === "tail" ? tailMargin(stored, this.#limits) : stored; + const bytes2 = this.#limits.retain === "tail" ? utf8ByteLength(text) : this.#storedBytes; + this.#chunks = text === "" ? [] : [{ text, bytes: bytes2, newlines: countNewlines(text) }]; + this.#storedBytes = bytes2; + this.#storedNewlines = this.#chunks[0]?.newlines ?? 0; + } + return { + text: sanitizeOutput(kept.text), + droppedBytes: this.#totalBytes - kept.bytes, + droppedLines: lines(this.#totalNewlines, this.#endsWithNewline) - keptLines + }; + } +}; +function tailMargin(text, limits) { + const bytes2 = encoder.encode(text); + const byteStart = bytes2.length > limits.maxBytes ? characterEnd(bytes2, bytes2.length - limits.maxBytes - 1) : 0; + let lineStart = 0; + let newlines = 0; + for (let index = bytes2.lastIndexOf(NEWLINE); index !== -1; index = bytes2.lastIndexOf(NEWLINE, index - 1)) { + if (++newlines > limits.maxLines) { + lineStart = index; + break; + } + if (index === 0) + break; + } + const start = Math.max(byteStart, lineStart); + return start === 0 ? text : decoder.decode(bytes2.subarray(start)); +} +function lines(newlines, terminated) { + return newlines + (terminated ? 0 : 1); +} +var runtimeBuffer = globalThis.Buffer; +var nonAsciiPattern = /[^\x00-\x7f]/; +function utf8ByteLength(content) { + if (runtimeBuffer) + return runtimeBuffer.byteLength(content, "utf8"); + const firstNonAscii = content.search(nonAsciiPattern); + if (firstNonAscii === -1) + return content.length; + let bytes2 = firstNonAscii; + for (let i = firstNonAscii; i < content.length; i++) { + const code = content.charCodeAt(i); + if (code <= 127) + bytes2 += 1; + else if (code <= 2047) + bytes2 += 2; + else if (code >= 55296 && code <= 56319 && i + 1 < content.length) { + const next = content.charCodeAt(i + 1); + if (next >= 56320 && next <= 57343) { + bytes2 += 4; + i++; + } else + bytes2 += 3; + } else + bytes2 += 3; + } + return bytes2; +} +function countNewlines(text) { + let count = 0; + for (let index = text.indexOf("\n"); index !== -1; index = text.indexOf("\n", index + 1)) + count++; + return count; +} +var PROGRESS_BYTES_PER_SECOND = 100 * 1024; +var SYSTEM_CLOCK = { + now: () => Date.now(), + setTimer: (callback, delayMs) => { + const timer = setTimeout(callback, delayMs); + return () => clearTimeout(timer); + } +}; +var Progress = class { + #write; + #onError; + #minIntervalMs; + #clock; + #waiters = []; + #timer; + #inFlight; + #nextAt = 0; + #dirty = false; + #stopped = false; + constructor(write, onError, minIntervalMs, clock = SYSTEM_CLOCK) { + this.#write = write; + this.#onError = onError; + this.#minIntervalMs = minIntervalMs; + this.#clock = clock; + } + /** Schedule a commit. */ + mark() { + this.#dirty = true; + this.#schedule(); + } + /** Schedule a commit; the promise settles with the commit that includes this change. */ + markAndWait() { + const waiter = Promise.withResolvers(); + this.#waiters.push(waiter); + this.mark(); + return waiter.promise; + } + /** Stop committing and wait for the commit in flight; returns the waiters the final commit must settle. */ + async stop() { + this.#stopped = true; + this.#timer?.(); + this.#timer = void 0; + await this.#inFlight; + return this.#waiters.splice(0); + } + #schedule() { + if (this.#stopped || this.#timer !== void 0 || this.#inFlight !== void 0) + return; + const wait = this.#nextAt - this.#clock.now(); + if (wait <= 0) + this.#flush(); + else + this.#timer = this.#clock.setTimer(() => { + this.#timer = void 0; + this.#schedule(); + }, wait); + } + #flush() { + if (this.#stopped || !this.#dirty) + return; + this.#dirty = false; + const waiters = this.#waiters.splice(0); + const started = this.#clock.now(); + this.#inFlight = this.#write().then((bytes2) => { + this.#nextAt = started + Math.max(this.#minIntervalMs, bytes2 * 1e3 / PROGRESS_BYTES_PER_SECOND); + for (const waiter of waiters) + waiter.resolve(); + }, (error) => { + this.#nextAt = started + this.#minIntervalMs; + for (const waiter of waiters) + waiter.reject(error); + this.#onError(error); + }).finally(() => { + this.#inFlight = void 0; + if (this.#dirty) + this.#schedule(); + }); + } +}; + +// node_modules/@earendil-works/pi-schema/dist/formats/typebox/date.js +var DAYS = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; +var DATE = /^(\d\d\d\d)-(\d\d)-(\d\d)$/; +function IsLeapYear(year) { + return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0); +} +function IsDate(value2) { + const matches = DATE.exec(value2); + if (!matches) + return false; + const year = +matches[1]; + const month = +matches[2]; + const day = +matches[3]; + return month >= 1 && month <= 12 && day >= 1 && day <= (month === 2 && IsLeapYear(year) ? 29 : DAYS[month]); +} + +// node_modules/@earendil-works/pi-schema/dist/formats/typebox/time.js +var TIME = /^(\d\d):(\d\d):(\d\d)(?:\.\d+)?(?:([Zz])|([+-])(\d\d):(\d\d))?$/; +function IsTime(value2, strictTimeZone = true) { + const matches = TIME.exec(value2); + if (!matches) + return false; + if (strictTimeZone && !matches[4] && !matches[5]) + return false; + const hr = +matches[1]; + const min = +matches[2]; + const sec = +matches[3]; + if (hr > 23 || min > 59 || sec > 60) + return false; + if (matches[5]) { + const tzH2 = +matches[6]; + const tzM2 = +matches[7]; + if (tzH2 > 23 || tzM2 > 59) + return false; + } + if (sec < 60) + return true; + const tzSign = matches[5] === "-" ? -1 : 1; + const tzH = +(matches[6] || 0); + const tzM = +(matches[7] || 0); + const totalUtcMin = hr * 60 + min - tzSign * (tzH * 60 + tzM); + return (totalUtcMin % 1440 + 1440) % 1440 === 1439; +} + +// node_modules/@earendil-works/pi-schema/dist/formats/typebox/date_time.js +function IsDateTime(value2) { + const dateTime = value2.split(/T/i); + return dateTime.length === 2 && IsDate(dateTime[0]) && IsTime(dateTime[1]); +} + +// node_modules/@earendil-works/pi-schema/dist/formats/typebox/duration.js +var Duration = /^P((\d+Y(\d+M(\d+D)?)?|\d+M(\d+D)?|\d+D)(T(\d+H(\d+M(\d+S)?)?|\d+M(\d+S)?|\d+S))?|T(\d+H(\d+M(\d+S)?)?|\d+M(\d+S)?|\d+S)|\d+W)$/; +function IsDuration(value2) { + return Duration.test(value2); +} + +// node_modules/@earendil-works/pi-schema/dist/formats/typebox/email.js +var Email = /^(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[^"\\]|\\[\x20-\x7e])*")@(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*|\[(?:IPv6:[a-f0-9:]+|(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3})\])$/i; +function IsEmail(value2) { + return Email.test(value2); +} + +// node_modules/@earendil-works/pi-schema/dist/formats/typebox/idna/pattern/pattern.js +var RE_RULE_HYPHEN_PLACEMENT = /^(?!-).*(? regexp.source).join("|"), "u"); +var RE_PERMITTED_CATEGORY = new RegExp([ + RE_LETTER, + RE_EUROPEAN_SEPARATOR, + RE_COMMON_SEPARATOR, + RE_NUMBER_DECIMAL, + RE_MARK_NONSPACING, + RE_MARK_SPACING_COMBINING, + RE_CONTEXTO_EXCEPTIONS, + RE_PVALID_EXCEPTIONS +].map((regexp) => regexp.source).join("|"), "u"); + +// node_modules/@earendil-works/pi-schema/dist/formats/typebox/idna/label/ascii.js +function IsAsciiLabel(value2) { + return RE_RULE_HYPHEN_PLACEMENT.test(value2) && RE_RULE_NOT_RESERVED_ACE.test(value2) && RE_ASCII_LDH.test(value2); +} + +// node_modules/@earendil-works/pi-schema/dist/formats/typebox/idna/format/puny.js +var PUNYCODE_BASE = 36; +var PUNYCODE_TMIN = 1; +var PUNYCODE_TMAX = 26; +var PUNYCODE_SKEW = 38; +var PUNYCODE_DAMP = 700; +var PUNYCODE_INITIAL_BIAS = 72; +var PUNYCODE_INITIAL_N = 128; +function ThrowDontCare() { + throw null; +} +function IsAcePrefixed(value2) { + return value2.toLowerCase().startsWith("xn--"); +} +function Adapt(delta, numPoints, firstTime) { + delta = firstTime ? Math.floor(delta / PUNYCODE_DAMP) : delta >> 1; + delta += Math.floor(delta / numPoints); + let k = 0; + while (delta > (PUNYCODE_BASE - PUNYCODE_TMIN) * PUNYCODE_TMAX >> 1) { + delta = Math.floor(delta / (PUNYCODE_BASE - PUNYCODE_TMIN)); + k += PUNYCODE_BASE; + } + return k + Math.floor((PUNYCODE_BASE - PUNYCODE_TMIN + 1) * delta / (delta + PUNYCODE_SKEW)); +} +function Decode(value2) { + const output = []; + let n = PUNYCODE_INITIAL_N; + let i = 0; + let bias = PUNYCODE_INITIAL_BIAS; + const delimIdx = value2.lastIndexOf("-"); + if (delimIdx > 0) { + for (let j = 0; j < delimIdx; j++) { + const cp = value2.charCodeAt(j); + if (cp >= 128) + ThrowDontCare(); + output.push(cp); + } + } + let inIdx = delimIdx < 0 ? 0 : delimIdx + 1; + while (inIdx < value2.length) { + const oldi = i; + let w = 1; + let k = PUNYCODE_BASE; + while (true) { + if (inIdx >= value2.length) + ThrowDontCare(); + const ch = value2.charCodeAt(inIdx++); + let digit; + if (ch >= 97 && ch <= 122) + digit = ch - 97; + else if (ch >= 48 && ch <= 57) + digit = ch - 48 + 26; + else + ThrowDontCare(); + i += digit * w; + const t = k <= bias ? PUNYCODE_TMIN : k >= bias + PUNYCODE_TMAX ? PUNYCODE_TMAX : k - bias; + if (digit < t) + break; + w *= PUNYCODE_BASE - t; + k += PUNYCODE_BASE; + } + const outLen = output.length + 1; + bias = Adapt(i - oldi, outLen, oldi === 0); + n += Math.floor(i / outLen); + i %= outLen; + output.splice(i, 0, n); + i++; + } + return String.fromCodePoint(...output); +} +function DigitToChar(digit) { + return digit < 26 ? String.fromCharCode(digit + 97) : String.fromCharCode(digit - 26 + 48); +} +var RE_REPLACE_NON_ASCII = new RegExp(RE_NON_ASCII.source, "gu"); +function Encode(input) { + const basic = input.replace(RE_REPLACE_NON_ASCII, ""); + const basicLength = basic.length; + const result = basicLength > 0 ? [basic, "-"] : []; + const codePoints2 = Array.from(input, (char) => char.codePointAt(0)); + let n = PUNYCODE_INITIAL_N; + let delta = 0; + let bias = PUNYCODE_INITIAL_BIAS; + let handledCPCount = basicLength; + while (handledCPCount < codePoints2.length) { + let m = Infinity; + for (const cp of codePoints2) { + if (cp >= n && cp < m) + m = cp; + } + delta += (m - n) * (handledCPCount + 1); + n = m; + for (const cp of codePoints2) { + if (cp < n) + delta++; + if (cp === n) { + let q = delta; + for (let k = PUNYCODE_BASE; ; k += PUNYCODE_BASE) { + const t = k <= bias ? PUNYCODE_TMIN : k >= bias + PUNYCODE_TMAX ? PUNYCODE_TMAX : k - bias; + if (q < t) + break; + const digit = t + (q - t) % (PUNYCODE_BASE - t); + result.push(DigitToChar(digit)); + q = Math.floor((q - t) / (PUNYCODE_BASE - t)); + } + result.push(DigitToChar(q)); + bias = Adapt(delta, handledCPCount + 1, handledCPCount === basicLength); + delta = 0; + handledCPCount++; + } + } + delta++; + n++; + } + return result.join(""); +} + +// node_modules/@earendil-works/pi-schema/dist/formats/typebox/idna/format/bidi.js +var RE_RTL_ALLOWED = /^(?:R|AL|AN|EN|ES|CS|ET|ON|BN|NSM)$/; +var RE_LTR_ALLOWED = /^(?:L|EN|ES|CS|ET|ON|BN|NSM)$/; +var RE_RTL_CLASSES = /^(?:R|AL|AN)$/; +function HasBidiChars(value2) { + if (IsAcePrefixed(value2)) { + try { + return HasRightToLeftCharacters(Decode(value2.slice(4).toLowerCase())); + } catch { + return false; + } + } + return HasRightToLeftCharacters(value2); +} +function GetBidiClass(codePoint) { + const char = String.fromCodePoint(codePoint); + return RE_EUROPEAN_NUMBER.test(char) ? "EN" : RE_ARABIC_INDIC_DIGIT.test(char) ? "AN" : ( + // Pattern.RE_EUROPEAN_SEPARATOR.test(char) ? 'ES' : // (no-spec-coverage) + // Pattern.RE_COMMON_SEPARATOR.test(char) ? 'CS' : // (no-spec-coverage) + RE_MARK_NONSPACING.test(char) ? "NSM" : RE_SCRIPT_HEBREW.test(char) ? "R" : RE_SCRIPT_ARABIC_LETTER.test(char) ? "AL" : RE_LETTER.test(char) ? "L" : "ON" + ); +} +function HasRightToLeftCharacters(value2) { + for (const ch of value2) + if (RE_RTL_CLASSES.test(GetBidiClass(ch.codePointAt(0)))) + return true; + return false; +} +function SatisfiesBidiRule(value2) { + let isRtl = false; + let allowed = RE_LTR_ALLOWED; + let sawEN = false; + let sawAN = false; + let isFirst = true; + for (const ch of value2) { + const bidiClass = GetBidiClass(ch.codePointAt(0)); + if (isFirst) { + if (bidiClass !== "L" && bidiClass !== "R" && bidiClass !== "AL") + return false; + isRtl = bidiClass === "R" || bidiClass === "AL"; + allowed = isRtl ? RE_RTL_ALLOWED : RE_LTR_ALLOWED; + isFirst = false; + } + if (!allowed.test(bidiClass)) + return false; + if (bidiClass === "EN") + sawEN = true; + else if (bidiClass === "AN") + sawAN = true; + } + if (isRtl && sawEN && sawAN) + return false; + return true; +} + +// node_modules/@earendil-works/pi-schema/dist/formats/typebox/idna/label/unicode.js +function ExceedsMaxALabelLength(value2) { + return RE_NON_ASCII.test(value2) && Encode(value2).length + 4 > 63; +} +function HasInvalidHyphens(chars) { + if (chars[0] === "-" || chars[chars.length - 1] === "-") + return true; + return chars.slice(2).join("").startsWith("--"); +} +function IsUnicodeLabel(value2) { + if (ExceedsMaxALabelLength(value2)) + return false; + if (HasRightToLeftCharacters(value2) && !SatisfiesBidiRule(value2)) + return false; + const chars = [...value2]; + const codePoints2 = chars.map((c) => c.codePointAt(0)); + const length = codePoints2.length; + if (HasInvalidHyphens(chars)) + return false; + if (RE_COMBINING_MARK.test(chars[0])) + return false; + let hasJapanese = false; + for (let i = 0; i < length; i++) { + const codePoint = codePoints2[i]; + const char = chars[i]; + if (RE_RFC5892_DISALLOWED.test(char)) + return false; + if (!RE_PERMITTED_CATEGORY.test(char)) + return false; + if (RE_SCRIPT_JAPANESE.test(char)) + hasJapanese = true; + const prev = codePoints2[i - 1], next = codePoints2[i + 1]; + switch (codePoint) { + case 183: + if (prev !== 108 || next !== 108) + return false; + break; + // MIDDLE DOT (Catalan) + case 885: + if (!next || !RE_SCRIPT_GREEK.test(chars[i + 1])) + return false; + break; + // Greek KERAIA + case 1523: + case 1524: + if (!prev || !RE_SCRIPT_HEBREW.test(chars[i - 1])) + return false; + break; + // Hebrew GERESH + case 8204: + if (!prev || prev < 128 && !RE_VIRAMA.test(chars[i - 1])) + return false; + break; + case 8205: + if (!prev || !RE_VIRAMA.test(chars[i - 1])) + return false; + break; + case 12539: + break; + } + } + if (value2.includes("\u30FB") && !hasJapanese) + return false; + return true; +} + +// node_modules/@earendil-works/pi-schema/dist/formats/typebox/idna/label/puny.js +function IsPunyLabel(value2) { + if (!IsAcePrefixed(value2)) + return false; + try { + const body = value2.slice(4).toLowerCase(); + if (body.lastIndexOf("-") === 0) + return false; + const decoded = Decode(body); + if (!RE_NON_ASCII.test(decoded)) + return false; + return IsUnicodeLabel(decoded); + } catch { + return false; + } +} + +// node_modules/@earendil-works/pi-schema/dist/formats/typebox/idna/hostname.js +function IsValidLabelLength(value2) { + return value2.length > 0 && value2.length <= 63; +} +function IsLabel(value2) { + return IsValidLabelLength(value2) && (IsPunyLabel(value2) || IsAsciiLabel(value2)); +} +function IsHostname(value2) { + if (value2.length === 0 || value2.length > 253) + return false; + if (value2.charCodeAt(value2.length - 1) === 46) + return false; + return value2.split(".").every((label) => IsLabel(label)); +} + +// node_modules/@earendil-works/pi-schema/dist/formats/typebox/idna/idn-hostname.js +function IsValidLabelLength2(value2) { + return value2.length > 0 && value2.length <= 63; +} +function IsLabel2(value2) { + return IsValidLabelLength2(value2) && (IsPunyLabel(value2) || IsUnicodeLabel(value2)); +} +function NormalizeHostname(value2) { + return value2.replace(/[\uff01-\uff5e]/g, (char) => String.fromCharCode(char.charCodeAt(0) - 65248)).normalize("NFC").replace(/[\u00ad\u034f\u180b-\u180d\u200b\ufe00-\ufe0f\u{e0100}-\u{e01ef}]/gu, "").replace(/[\u002E\u3002\uFF0E\uFF61]/g, "."); +} +function IsIdnHostname(value2) { + if (value2.length === 0 || value2.includes(" ")) + return false; + const normalized = NormalizeHostname(value2); + if (normalized.length > 253) + return false; + const labels = normalized.split("."); + const hasBidiChars = labels.some((label) => HasBidiChars(label)); + return labels.every((label) => IsLabel2(label) && (!hasBidiChars || SatisfiesBidiRule(label))); +} + +// node_modules/@earendil-works/pi-schema/dist/formats/typebox/hostname.js +function IsHostname2(value2) { + return IsHostname(value2); +} + +// node_modules/@earendil-works/pi-schema/dist/formats/typebox/idn_email.js +var IdnEmail = /^(?:[A-Za-z0-9!#$%&'*+/=?^_`{|}~\u{0080}-\u{10FFFF}-]+(?:\.[A-Za-z0-9!#$%&'*+/=?^_`{|}~\u{0080}-\u{10FFFF}-]+)*|"(?:[^"\\]|\\.)*")@[\p{L}\p{N}](?:[\p{L}\p{N}-]{0,62})(?^`{|}\\]/; +var InvalidPercentEncoding = /%(?![0-9a-fA-F]{2})/; +function NarrowIpvFuture(value2) { + return value2.length < IpvFutureMatchMaxLength ? value2.replace(IpvFutureMatch, "[::1]") : value2; +} +function IsIri(value2) { + if (InvalidIriChars.test(value2)) + return false; + if (InvalidPercentEncoding.test(value2)) + return false; + return URL.canParse(NarrowIpvFuture(value2)); +} + +// node_modules/@earendil-works/pi-schema/dist/formats/typebox/iri_reference.js +var InvalidIriChars2 = /[\x00-\x20\x7F\\]|%(?![0-9a-fA-F]{2})/; +var MalformedScheme = /^[a-zA-Z][a-zA-Z0-9+\-.]*\/\//; +function IsIriReference(value2) { + return !InvalidIriChars2.test(value2) && !MalformedScheme.test(value2) && URL.canParse(value2, "http://example.com"); +} + +// node_modules/@earendil-works/pi-schema/dist/formats/typebox/json_pointer.js +var JsonPointer = /^(?:\/(?:[^~/]|~0|~1)*)*$/; +function IsJsonPointer(value2) { + return JsonPointer.test(value2); +} + +// node_modules/@earendil-works/pi-schema/dist/formats/typebox/json_pointer_uri_fragment.js +var JsonPointerUriFragment = /^#(?:\/(?:[a-z0-9_\-.!$&'()*+,;:=@]|%[0-9a-f]{2}|~0|~1)*)*$/i; +function IsJsonPointerUriFragment(value2) { + return JsonPointerUriFragment.test(value2); +} + +// node_modules/@earendil-works/pi-schema/dist/formats/typebox/regex.js +function IsRegex(value2) { + try { + new RegExp(value2, "u"); + return true; + } catch { + return false; + } +} + +// node_modules/@earendil-works/pi-schema/dist/formats/typebox/relative_json_pointer.js +var RelativeJsonPointer = /^(?:0|[1-9][0-9]*)(?:#|(?:\/(?:[^~/]|~0|~1)*)*)$/; +function IsRelativeJsonPointer(value2) { + return RelativeJsonPointer.test(value2); +} + +// node_modules/@earendil-works/pi-schema/dist/formats/typebox/uri.js +var Uri = /^[a-z][a-z0-9+\-.]*:(?:\/\/(?:(?:[-a-z0-9._~!$&'()*+,;=:]|%[0-9a-f]{2})*@)?(?:\[(?:(?:(?:[\da-f]{1,4}:){6}(?:[\da-f]{1,4}:[\da-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d))|::(?:[\da-f]{1,4}:){5}(?:[\da-f]{1,4}:[\da-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d))|(?:[\da-f]{1,4})?::(?:[\da-f]{1,4}:){4}(?:[\da-f]{1,4}:[\da-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d))|(?:(?:[\da-f]{1,4}:){0,1}[\da-f]{1,4})?::(?:[\da-f]{1,4}:){3}(?:[\da-f]{1,4}:[\da-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d))|(?:(?:[\da-f]{1,4}:){0,2}[\da-f]{1,4})?::(?:[\da-f]{1,4}:){2}(?:[\da-f]{1,4}:[\da-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d))|(?:(?:[\da-f]{1,4}:){0,3}[\da-f]{1,4})?::[\da-f]{1,4}:(?:[\da-f]{1,4}:[\da-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d))|(?:(?:[\da-f]{1,4}:){0,4}[\da-f]{1,4})?::(?:[\da-f]{1,4}:[\da-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d))|(?:(?:[\da-f]{1,4}:){0,5}[\da-f]{1,4})?::[\da-f]{1,4}|(?:(?:[\da-f]{1,4}:){0,6}[\da-f]{1,4})?::)|v[0-9a-f]+\.[-a-z0-9._~!$&'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)|(?:[-a-z0-9._~!$&'()*+,;=]|%[0-9a-f]{2})*)(?::\d*)?(?:\/(?:[-a-z0-9._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*|\/(?:(?:[-a-z0-9._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[-a-z0-9._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)?|(?:[-a-z0-9._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[-a-z0-9._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)?(?:\?(?:[-a-z0-9._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?(?:#(?:[-a-z0-9._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?$/i; +function IsUri(value2) { + return Uri.test(value2); +} + +// node_modules/@earendil-works/pi-schema/dist/formats/typebox/uri_reference.js +var UriReference = /^(?:[a-z][a-z0-9+\-.]*:(?:\/\/(?:(?:[-a-z0-9._~!$&'()*+,;=:]|%[0-9a-f]{2})*@)?(?:\[(?:(?:(?:[\da-f]{1,4}:){6}(?:[\da-f]{1,4}:[\da-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d))|::(?:[\da-f]{1,4}:){5}(?:[\da-f]{1,4}:[\da-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d))|(?:[\da-f]{1,4})?::(?:[\da-f]{1,4}:){4}(?:[\da-f]{1,4}:[\da-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d))|(?:(?:[\da-f]{1,4}:){0,1}[\da-f]{1,4})?::(?:[\da-f]{1,4}:){3}(?:[\da-f]{1,4}:[\da-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d))|(?:(?:[\da-f]{1,4}:){0,2}[\da-f]{1,4})?::(?:[\da-f]{1,4}:){2}(?:[\da-f]{1,4}:[\da-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d))|(?:(?:[\da-f]{1,4}:){0,3}[\da-f]{1,4})?::[\da-f]{1,4}:(?:[\da-f]{1,4}:[\da-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d))|(?:(?:[\da-f]{1,4}:){0,4}[\da-f]{1,4})?::(?:[\da-f]{1,4}:[\da-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d))|(?:(?:[\da-f]{1,4}:){0,5}[\da-f]{1,4})?::[\da-f]{1,4}|(?:(?:[\da-f]{1,4}:){0,6}[\da-f]{1,4})?::)|v[0-9a-f]+\.[-a-z0-9._~!$&'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)|(?:[-a-z0-9._~!$&'()*+,;=]|%[0-9a-f]{2})*)(?::\d*)?(?:\/(?:[-a-z0-9._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*|\/(?:(?:[-a-z0-9._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[-a-z0-9._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)?|(?:[-a-z0-9._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[-a-z0-9._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)?|(?:\/\/(?:(?:[-a-z0-9._~!$&'()*+,;=:]|%[0-9a-f]{2})*@)?(?:\[(?:(?:(?:[\da-f]{1,4}:){6}(?:[\da-f]{1,4}:[\da-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d))|::(?:[\da-f]{1,4}:){5}(?:[\da-f]{1,4}:[\da-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d))|(?:[\da-f]{1,4})?::(?:[\da-f]{1,4}:){4}(?:[\da-f]{1,4}:[\da-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d))|(?:(?:[\da-f]{1,4}:){0,1}[\da-f]{1,4})?::(?:[\da-f]{1,4}:){3}(?:[\da-f]{1,4}:[\da-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d))|(?:(?:[\da-f]{1,4}:){0,2}[\da-f]{1,4})?::(?:[\da-f]{1,4}:){2}(?:[\da-f]{1,4}:[\da-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d))|(?:(?:[\da-f]{1,4}:){0,3}[\da-f]{1,4})?::[\da-f]{1,4}:(?:[\da-f]{1,4}:[\da-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d))|(?:(?:[\da-f]{1,4}:){0,4}[\da-f]{1,4})?::(?:[\da-f]{1,4}:[\da-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d))|(?:(?:[\da-f]{1,4}:){0,5}[\da-f]{1,4})?::[\da-f]{1,4}|(?:(?:[\da-f]{1,4}:){0,6}[\da-f]{1,4})?::)|v[0-9a-f]+\.[-a-z0-9._~!$&'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)|(?:[-a-z0-9._~!$&'()*+,;=]|%[0-9a-f]{2})*)(?::\d*)?(?:\/(?:[-a-z0-9._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*|\/(?:(?:[-a-z0-9._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[-a-z0-9._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)?|(?:[-a-z0-9._~!$&'()*+,;=@]|%[0-9a-f]{2})+(?:\/(?:[-a-z0-9._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)?)(?:\?(?:[-a-z0-9._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?(?:#(?:[-a-z0-9._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?$/i; +function IsUriReference(value2) { + return UriReference.test(value2); +} + +// node_modules/@earendil-works/pi-schema/dist/formats/typebox/uri_template.js +var UriTemplate = /^(?:(?:[^\x00-\x20"<>%\\^`{|}\x7f]|%[0-9a-f]{2})|\{[+#./;?&=,!@|]?(?:[a-z0-9_]|%[0-9a-f]{2})+(?:\.(?:[a-z0-9_]|%[0-9a-f]{2})+)*(?::[1-9]\d{0,3}|\*)?(?:,(?:[a-z0-9_]|%[0-9a-f]{2})+(?:\.(?:[a-z0-9_]|%[0-9a-f]{2})+)*(?::[1-9]\d{0,3}|\*)?)*\})*$/i; +function IsUriTemplate(value2) { + return UriTemplate.test(value2); +} + +// node_modules/@earendil-works/pi-schema/dist/formats/typebox/url.js +function IsUrl(value2) { + return URL.canParse(value2); +} + +// node_modules/@earendil-works/pi-schema/dist/formats/typebox/uuid.js +var Uuid = /^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i; +function IsUuid(value2) { + return Uuid.test(value2); +} + +// node_modules/@earendil-works/pi-schema/dist/formats/index.js +var formats = Object.freeze({ + date: IsDate, + "date-time": IsDateTime, + duration: IsDuration, + email: IsEmail, + hostname: IsHostname2, + "idn-email": IsIdnEmail, + "idn-hostname": IsIdnHostname2, + ipv4: IsIPv4, + ipv6: IsIPv6, + iri: IsIri, + "iri-reference": IsIriReference, + "json-pointer": IsJsonPointer, + "json-pointer-uri-fragment": IsJsonPointerUriFragment, + regex: IsRegex, + "relative-json-pointer": IsRelativeJsonPointer, + time: IsTime, + uri: IsUri, + "uri-reference": IsUriReference, + "uri-template": IsUriTemplate, + url: IsUrl, + uuid: IsUuid +}); + +// node_modules/@earendil-works/pi-ai/dist/utils/validation.js +var validatorCache = /* @__PURE__ */ new WeakMap(); +function getValidator(schema) { + const cached = validatorCache.get(schema); + if (cached) { + return cached; + } + const validator = compile(schema, { formats, coerce: true, unresolvedRefs: "reject" }); + validatorCache.set(schema, validator); + return validator; +} +function formatValidationPath(pointer2) { + if (pointer2 === "") + return "root"; + return pointer2.slice(1).split("/").map((segment) => segment.replace(/~1/g, "/").replace(/~0/g, "~")).join("."); +} +function validateToolArguments(tool, toolCall) { + if (toolCall.argumentsError) { + throw new Error(`Invalid arguments for tool "${toolCall.name}": ${toolCall.argumentsError}`); + } + const result = getValidator(tool.parameters).parse(structuredClone(toolCall.arguments)); + if (result.ok) { + return result.value; + } + const errors = result.errors.map((error) => ` - ${formatValidationPath(error.path)}: ${error.message}`).join("\n"); + const errorMessage2 = `Validation failed for tool "${toolCall.name}": +${errors} + +Received arguments: +${JSON.stringify(toolCall.arguments, null, 2)}`; + throw new Error(errorMessage2); +} + +// node_modules/@earendil-works/pi-durable/dist/truncate.js +var DEFAULT_MAX_LINES = 2e3; +var DEFAULT_MAX_BYTES = 50 * 1024; +function splitLinesForCounting(content) { + if (content.length === 0) + return []; + const lines2 = content.split("\n"); + if (content.endsWith("\n")) + lines2.pop(); + return lines2; +} +function formatSize(bytes2) { + if (bytes2 < 1024) { + return `${bytes2}B`; + } else if (bytes2 < 1024 * 1024) { + return `${(bytes2 / 1024).toFixed(1)}KB`; + } else { + return `${(bytes2 / (1024 * 1024)).toFixed(1)}MB`; + } +} +function truncateHeadOf(prefix, totals, options = {}) { + const maxLines = options.maxLines ?? DEFAULT_MAX_LINES; + const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES; + const totalBytes = totals.bytes; + const lines2 = splitLinesForCounting(prefix); + const totalLines = totals.lines; + if (totalLines <= maxLines && totalBytes <= maxBytes) { + return { + content: prefix, + truncated: false, + truncatedBy: null, + totalLines, + totalBytes, + outputLines: totalLines, + outputBytes: totalBytes, + lastLinePartial: false, + firstLineExceedsLimit: false, + maxLines, + maxBytes + }; + } + const firstLineBytes = utf8ByteLength(lines2[0]); + if (firstLineBytes > maxBytes) { + return { + content: "", + truncated: true, + truncatedBy: "bytes", + totalLines, + totalBytes, + outputLines: 0, + outputBytes: 0, + lastLinePartial: false, + firstLineExceedsLimit: true, + maxLines, + maxBytes + }; + } + const outputLinesArr = []; + let outputBytesCount = 0; + let truncatedBy = "lines"; + for (let i = 0; i < lines2.length && i < maxLines; i++) { + const line = lines2[i]; + const lineBytes = utf8ByteLength(line) + (i > 0 ? 1 : 0); + if (outputBytesCount + lineBytes > maxBytes) { + truncatedBy = "bytes"; + break; + } + outputLinesArr.push(line); + outputBytesCount += lineBytes; + } + if (truncatedBy !== "bytes") + truncatedBy = outputLinesArr.length < totalLines ? "lines" : "bytes"; + const outputContent = outputLinesArr.join("\n"); + const finalOutputBytes = utf8ByteLength(outputContent); + return { + content: outputContent, + truncated: true, + truncatedBy, + totalLines, + totalBytes, + outputLines: outputLinesArr.length, + outputBytes: finalOutputBytes, + lastLinePartial: false, + firstLineExceedsLimit: false, + maxLines, + maxBytes + }; +} + +// node_modules/@earendil-works/pi-durable/dist/harness/usage.js +var UsageDoc = defineDoc({ + kind: "pi.usage", + version: 1, + scope: "conversation", + history: "latest", + fork: "initial", + initial: () => ({ models: {}, tools: {} }), + checkpointWhen: () => true +}); +async function recordUsage(tx, conversationId, bucket, key, usage) { + const totals = (await tx.doc(UsageDoc, conversationId))[bucket]; + const total = Object.hasOwn(totals, key) ? totals[key] : void 0; + if (total === void 0) + totals[key] = copyJson(usage, { omitUndefinedProperties: true }); + else + addUsage(total, usage); +} +function addUsage(total, usage) { + total.input += usage.input; + total.output += usage.output; + total.cacheRead += usage.cacheRead; + total.cacheWrite += usage.cacheWrite; + total.totalTokens += usage.totalTokens; + if (usage.cacheWrite1h !== void 0) + total.cacheWrite1h = (total.cacheWrite1h ?? 0) + usage.cacheWrite1h; + if (usage.reasoning !== void 0) + total.reasoning = (total.reasoning ?? 0) + usage.reasoning; + total.cost.input += usage.cost.input; + total.cost.output += usage.cost.output; + total.cost.cacheRead += usage.cost.cacheRead; + total.cost.cacheWrite += usage.cost.cacheWrite; + total.cost.total += usage.cost.total; +} +function addUsageState(sum, state) { + for (const bucket of ["models", "tools"]) { + for (const [key, usage] of Object.entries(state[bucket])) { + const total = Object.hasOwn(sum[bucket], key) ? sum[bucket][key] : void 0; + if (total !== void 0) + addUsage(total, usage); + else + Object.defineProperty(sum[bucket], key, { + value: structuredClone(usage), + enumerable: true, + writable: true + }); + } + } +} + +// node_modules/@earendil-works/pi-durable/dist/harness/tool.js +var MADE_NESTED_CALLS = "pi.tool.madeNestedCalls"; +var NestedCallDoc = defineDocFamily({ + kind: "pi.tool.nested-call", + version: 1, + scope: "task", + family: true, + initial: () => ({}), + // The document retires with its task. + checkpointWhen: () => false +}); +var ToolTask = defineTask({ + name: "pi.tool", + version: 3, + initial: () => ({ phase: "call" }), + // Version 1 had only model-issued calls, without `kind`; version 3 added `unoffered`, absent in version 2 inputs. + migrate: (input, checkpoint, fromVersion) => ({ + input: fromVersion === 1 ? { kind: "model", ...input } : input, + checkpoint + }), + phases: { + call: async (task, runtime, context) => { + const call = await readCall(runtime, task.input, context); + if (cutOffTooOften(task, runtime)) + return endCutOff(runtime, task, call, context); + const tool = isUnoffered(task.input) ? void 0 : await resolveTool(runtime, task.input, call.name, context); + if (tool === void 0) { + const error = harnessError("tool_unavailable", `Tool ${call.name} is not available`); + return reject(runtime, task.input, call, void 0, error, context); + } + const prepared = prepare(tool, call.arguments); + const checked = "error" in prepared ? prepared : validate(tool, call, prepared.args); + if ("error" in checked) + return reject(runtime, task.input, call, tool, invalid(checked.error), context); + let args = checked.args; + let block; + await runtime.hooks.each("beforeTool", async (hook2) => { + if (block !== void 0) + return; + try { + const decision = await hook2({ ...call, arguments: args }, runtime, context); + if (decision?.block !== void 0) + block = decision.block; + else if (decision?.arguments !== void 0) + args = decision.arguments; + } catch (error) { + if (runtime.signal.aborted) + throw error; + block = errorMessage(error); + } + }); + if (block !== void 0) { + const blocked = harnessError("blocked", `Tool call blocked: ${block}`); + return reject(runtime, task.input, call, tool, blocked, context); + } + const validated = validate(tool, call, args); + if ("error" in validated) + return reject(runtime, task.input, call, tool, invalid(validated.error), context); + const final = copyJson(validated.args, { omitUndefinedProperties: true }); + await runtime.commit(async (tx) => { + const slot = toolSlot(await tx.doc(LiveDoc, runtime.conversationId), runtime.taskId); + if (slot !== void 0) + slot.status = "running"; + if (slot !== void 0 && "parentTaskId" in slot) + slot.arguments = final; + return void 0; + }, context, PROGRESS_COMMIT); + await runtime.commit(() => { + const intent = { phase: "execute", arguments: final, replay: tool.replay ?? "unsafe" }; + return { status: "running", checkpoint: intent }; + }, context); + await run(runtime, task.input, call, tool, final, context); + }, + /** Recovery after intent: rerun only when the stored and the current policy both say `safe`. */ + execute: async (task, runtime, context) => { + const { arguments: args, replay } = task.state.checkpoint; + const call = await readCall(runtime, task.input, context); + if (cutOffTooOften(task, runtime)) + return endCutOff(runtime, task, call, context); + const tool = await resolveTool(runtime, task.input, call.name, context); + if (replay === "safe" && tool?.replay === "safe") { + await runtime.commit(async (tx) => { + const slot = toolSlot(await tx.doc(LiveDoc, runtime.conversationId), runtime.taskId); + if (slot !== void 0) { + clearProgress(slot); + slot.status = "running"; + if ("parentTaskId" in slot) + slot.arguments = args; + } + return void 0; + }, context); + return run(runtime, task.input, call, tool, args, context); + } + const message = `Tool ${call.name} was interrupted and may have partially run`; + const ending = { status: "failed", message }; + await settleFromSlot(runtime, task.input, call, { tool }, ending, "interrupted", message, context); + } + }, + abort: async (task, runtime, context) => { + const call = await readCall(runtime, task.input, context); + const [code, message] = isUnoffered(task.input) ? ["tool_unavailable", `Tool ${call.name} is not available`] : task.abortReason !== "restart" ? ["aborted", `Tool ${call.name} was aborted`] : task.state.checkpoint.phase === "execute" ? ["interrupted", `Tool ${call.name} was interrupted by a restart and may have partially run`] : ["abandoned", `Tool ${call.name} was not started: its caller ended with a restart`]; + const hooks = (task.restarts ?? 0) < runtime.settings.restartLimit - 1 ? { tool: await toolForLimits(runtime, task.input, call.name, context) } : void 0; + await settleFromSlot(runtime, task.input, call, hooks, { status: "aborted" }, code, message, context); + } +}); +async function endCutOff(runtime, task, call, context) { + const message = `Tool ${call.name} was interrupted ${cutOffCount(task)}; it is not run again`; + const ending = { status: "failed", message, reason: "restart_limit" }; + const hooks = salvageable(task, runtime) ? { tool: await toolForLimits(runtime, task.input, call.name, context) } : void 0; + await settleFromSlot(runtime, task.input, call, hooks, ending, "restart_limit", message, context); +} +async function reject(runtime, input, call, tool, result, context) { + const settled = await afterHarnessResult(runtime, input, call, tool, result, void 0, context); + await settle2(runtime, input, call, COMPLETED, settled, context); +} +async function settleFromSlot(runtime, input, call, hooks, ending, code, message, context) { + const live = await runtime.snapshot(LiveDoc, runtime.conversationId, context); + const { result, truncation } = fromSlot(live === void 0 ? void 0 : toolSlot(live, runtime.taskId), code, message); + const settled = hooks !== void 0 ? await afterHarnessResult(runtime, input, call, hooks.tool, result, truncation, context) : truncation === void 0 ? result : { ...result, diagnostics: [...result.diagnostics ?? [], truncation] }; + await settle2(runtime, input, call, ending, settled, context); +} +async function toolForLimits(runtime, input, name, context) { + if (isUnoffered(input)) + return void 0; + return resolveTool(runtime, input, name, context).catch(() => void 0); +} +function isUnoffered(input) { + return input.kind === "model" && input.unoffered === true; +} +async function resolveTool(runtime, input, name, context) { + const agent2 = await runtime.agent(context); + return (input.kind === "model" ? agent2.tools : agent2.callable).find((tool) => tool.name === name); +} +function outputLimits(tool) { + return { + maxBytes: tool?.outputLimits?.maxBytes ?? DEFAULT_MAX_BYTES, + maxLines: tool?.outputLimits?.maxLines ?? DEFAULT_MAX_LINES, + retain: tool?.outputLimits?.retain ?? "head" + }; +} +async function readCall(runtime, input, context) { + if (input.kind === "nested") { + context.telemetry.setAttributes({ + "tool.name": input.call.name, + "tool.call_id": input.call.id + }); + return { ...input.call, parent: { taskId: input.parent, callId: input.parentCallId } }; + } + const entry2 = await runtime.entry(AssistantEntry, input.assistant, context); + const message = entry2?.model?.[0]; + const call = message?.role === "assistant" ? message.content.find((content) => content.type === "toolCall" && content.id === input.callId) : void 0; + if (call === void 0) + throw new Error(`Entry ${input.assistant} has no tool call ${input.callId}`); + context.telemetry.setAttributes({ + "tool.name": call.name, + "tool.call_id": call.id + }); + return call; +} +function prepare(tool, args) { + if (tool.prepareArguments === void 0) + return { args }; + try { + return { args: tool.prepareArguments(args) }; + } catch (error) { + return { error: errorMessage(error) }; + } +} +function validate(tool, call, args) { + try { + return { args: validateToolArguments(tool, { ...call, arguments: args }) }; + } catch (error) { + return { error: errorMessage(error) }; + } +} +function invalid(message) { + return harnessError("invalid_arguments", message); +} +async function run(runtime, input, call, tool, args, context) { + const limits = outputLimits(tool); + const reported = { output: new OutputBuffer(limits), limits, diagnostics: [], details: void 0 }; + const streams = input.kind === "model" || input.progress !== false; + const progress = publishProgress(runtime, reported, streams, context); + const admissions = []; + const resumes = tool.replay === "safe"; + let sequence2 = 0; + let madeNestedMemo; + let ended = false; + const assertLive = () => { + if (ended) + throw new Error(`Tool call ${call.id} has settled`); + }; + let env; + const api = { + taskId: runtime.taskId, + conversationId: runtime.conversationId, + callId: call.id, + registry: runtime.registry, + agent: runtime.agent, + get models() { + return runtime.models; + }, + get env() { + return env; + }, + now: () => runtime.now(), + output: (chunk, skipped) => { + assertLive(); + if (reported.output.push(chunk, skipped)) + progress.mark(); + }, + outputWindow: limits.retain === "tail" ? { + maxBytes: limits.maxBytes, + maxLines: limits.maxLines, + minIntervalMs: runtime.settings.progress.outputIntervalMs, + bytesPerSecond: PROGRESS_BYTES_PER_SECOND + } : void 0, + retainedOutput: () => { + assertLive(); + const retained = reported.output.snapshot(); + return { text: retained.text, truncated: retained.droppedBytes > 0 }; + }, + diagnostic: (diagnostic) => { + assertLive(); + reported.diagnostics.push(copyJson(diagnostic, { omitUndefinedProperties: true })); + progress.mark(); + }, + details: async (value2, detailsContext) => { + assertLive(); + detailsContext.abortSignal?.throwIfAborted(); + reported.details = copyJson(value2, { omitUndefinedProperties: true }); + const committed = progress.markAndWait(); + committed.catch(() => { + }); + return awaitWithContext(committed, detailsContext); + }, + commit: async (change, commitContext, options) => { + let result2; + await runtime.commit(async (tx) => { + result2 = await change(tx); + return void 0; + }, commitContext, options); + return result2; + }, + memo: runtime.memo, + createTask: async (task, input2, options, taskContext) => { + const child = options.ownership.kind === "task" && !resumes ? { abandonOnRestart: true, ...options } : options; + let id; + await runtime.commit(async (tx) => { + id = await tx.createTask(task, input2, child); + return void 0; + }, taskContext); + return id; + }, + getTask: runtime.getTask, + waitForTask: runtime.waitForTask, + conversation: runtime.conversation, + snapshot: runtime.snapshot, + snapshotAsOf: runtime.snapshotAsOf, + watchDoc: runtime.watchDoc, + executeTool: async (name, toolArgs, callContext, options = {}) => { + assertLive(); + const key = options.key ?? String(++sequence2); + if (options.key !== void 0) + checkKey(options.key); + madeNestedMemo ??= runtime.memo(MADE_NESTED_CALLS, true, callContext).catch((error) => { + madeNestedMemo = void 0; + throw error; + }); + const admission = madeNestedMemo.then(() => startNestedCall(runtime, call.id, name, toolArgs, { key, progress: options.progress, abandonOnRestart: !resumes }, callContext)); + admissions.push(admission); + const id = await admission; + const settled = await runtime.waitForTask(id, callContext); + const stored = (await runtime.snapshot(NestedCallDoc, runtime.taskId, key, callContext))?.result; + return stored === void 0 ? fallbackResult(id, name, settled.state.outcome) : copyJson(stored); + } + }; + let result; + let ending = COMPLETED; + let durationMs; + try { + env = await runtime.env(context); + const startedAt = performance.now(); + try { + result = await tool.execute(args, api, context); + } finally { + durationMs = Math.round(performance.now() - startedAt); + } + } catch (error) { + if (runtime.signal.aborted) { + ended = true; + for (const waiter of await progress.stop()) + waiter.reject(error); + throw error; + } + result = { isError: true, diagnostics: [toolDiagnostic("tool_error", errorMessage(error))] }; + ending = { status: "failed", message: `Tool ${call.name} threw` }; + } + ended = true; + await Promise.allSettled(admissions); + reported.output.end(); + const pending = await progress.stop(); + try { + const settled = await finalResult(runtime, input, call, tool, result, reported, context); + await settle2(runtime, input, call, ending, settled, context, durationMs); + } catch (error) { + for (const waiter of pending) + waiter.reject(error); + throw error; + } + for (const waiter of pending) + waiter.resolve(); +} +function progressClock(runtime) { + let last = 0; + return { + now: () => { + try { + last = runtime.now(); + } catch { + } + return last; + }, + setTimer: (callback, delayMs) => runtime.setTimer(callback, delayMs) + }; +} +function publishProgress(runtime, reported, enabled, context) { + let written = { text: "", details: void 0, diagnostics: 0 }; + return new Progress(async () => { + if (!enabled) + return 0; + const snapshot = reported.output.snapshot(); + const current = { text: snapshot.text, details: reported.details, diagnostics: reported.diagnostics.length }; + const added = reported.diagnostics.slice(written.diagnostics, current.diagnostics); + const detailsChanged = current.details !== written.details; + let bytes2 = 0; + if (snapshot.text !== written.text) { + const shared = snapshot.text.startsWith(written.text) ? written.text.length : overlap(written.text, snapshot.text, 65536); + bytes2 += utf8ByteLength(snapshot.text.slice(shared)); + } + if (detailsChanged) + bytes2 += utf8ByteLength(JSON.stringify(current.details ?? null)); + if (added.length > 0) + bytes2 += utf8ByteLength(JSON.stringify(added)); + await runtime.commit(async (tx) => { + const slot = toolSlot(await tx.doc(LiveDoc, runtime.conversationId), runtime.taskId); + if (slot === void 0) + return void 0; + if ((slot.output ?? "") !== snapshot.text) + slot.output = snapshot.text; + if (snapshot.droppedBytes > 0) + slot.droppedBytes = snapshot.droppedBytes; + if (snapshot.droppedLines > 0) + slot.droppedLines = snapshot.droppedLines; + if (detailsChanged && current.details !== void 0) { + assignJson(slot, "details", current.details); + } + if (added.length > 0) { + if (slot.diagnostics === void 0) + slot.diagnostics = []; + for (const diagnostic of added) + slot.diagnostics.push(diagnostic); + } + return void 0; + }, context, PROGRESS_COMMIT); + written = current; + return bytes2; + }, (error) => { + if (!runtime.signal.aborted) + runtime.report(error); + }, runtime.settings.progress.outputIntervalMs, progressClock(runtime)); +} +async function finalResult(runtime, input, call, tool, result, reported, context) { + const harness2 = []; + const retained = result.output === void 0 ? reported.output.snapshot() : void 0; + const output = retained === void 0 ? result.output : retained.text === "" ? [] : [{ type: "text", text: retained.text }]; + let final = { + ...result, + output, + details: result.details === void 0 ? reported.details : result.details, + diagnostics: [...reported.diagnostics, ...result.diagnostics ?? []] + }; + await runtime.hooks.each("afterTool", async (hook2) => { + final = await hook2(call, final, runtime, context) ?? final; + }); + if (retained !== void 0 && retained.droppedBytes > 0 && final.output === output) { + harness2.push(truncated(retained, reported.limits.retain)); + } + const bounded = boundContent(final.output ?? [], reported.limits); + if (bounded.droppedBytes > 0) + harness2.push(truncated(bounded, reported.limits.retain)); + const broken = structuredOutputError(tool, final); + if (broken !== void 0) { + const { structuredOutput: _dropped, ...rest } = final; + if (input.kind === "nested") { + final = { ...rest, isError: true }; + harness2.push(toolDiagnostic("invalid_structured_output", broken)); + } else { + final = rest; + const extension = await runtime.agent(context).then((agent2) => toolExtension(agent2, tool.name), () => void 0); + runtime.report(new Error(broken), { kind: "extension", ...optionalExtension(extension) }); + } + } + if (input.kind === "nested" && tool.structuredOutputSchema === void 0) { + final = { ...final, structuredOutput: outputValue(bounded.content) }; + } + if (input.kind === "nested" && tool.structuredOutputSchema !== void 0 && final.isError === true && final.structuredOutput === void 0 && ![...final.diagnostics ?? [], ...harness2].some((diagnostic) => diagnostic.severity === "error")) { + const text = bounded.content.flatMap((item) => item.type === "text" ? [item.text] : []).join("\n"); + if (text !== "") + harness2.push(toolDiagnostic("tool_error", text)); + } + return { ...final, output: bounded.content, diagnostics: [...final.diagnostics ?? [], ...harness2] }; +} +async function afterHarnessResult(runtime, input, call, tool, result, truncation, context) { + let final = result; + await runtime.hooks.each("afterTool", async (hook2) => { + final = await hook2(call, final, runtime, context) ?? final; + }); + const harness2 = []; + if (truncation !== void 0 && final.output === result.output) + harness2.push(truncation); + const limits = outputLimits(tool); + const bounded = boundContent(final.output ?? [], limits); + if (bounded.droppedBytes > 0) + harness2.push(truncated(bounded, limits.retain)); + const { structuredOutput: _dropped, ...rest } = final; + const value2 = input.kind === "nested" && final.isError !== true && tool?.structuredOutputSchema === void 0 ? { structuredOutput: outputValue(bounded.content) } : {}; + return { ...rest, ...value2, output: bounded.content, diagnostics: [...rest.diagnostics ?? [], ...harness2] }; +} +var structuredValidators = /* @__PURE__ */ new WeakMap(); +function structuredOutputError(tool, result) { + const schema = tool.structuredOutputSchema; + const value2 = result.structuredOutput; + if (schema === void 0) { + return value2 === void 0 ? void 0 : `Tool ${tool.name} returned structuredOutput but declares no structuredOutputSchema`; + } + if (value2 === void 0) { + return result.isError === true ? void 0 : `Tool ${tool.name} returned no structuredOutput`; + } + let validator = structuredValidators.get(schema); + if (validator === void 0) { + validator = compile(schema, { formats, unresolvedRefs: "reject" }); + structuredValidators.set(schema, validator); + } + if (validator.check(value2)) + return void 0; + const [first] = validator.errors(value2); + const path2 = first?.path.slice(1).split("/").map((segment) => segment.replace(/~1/g, "/").replace(/~0/g, "~")).join(".") || "root"; + return `Tool ${tool.name} returned structuredOutput that does not match its schema: ${path2}: ${first?.message ?? "invalid"}`; +} +async function settle2(runtime, input, call, ending, result, context, durationMs) { + const nestedIds = (await runtime.ownedTasks(context)).filter((record) => record.kind === ToolTask.definition.name && record.input.kind === "nested").map((record) => record.id); + const madeNested = await runtime.memo(MADE_NESTED_CALLS, context) === true; + await Promise.all(nestedIds.map((id) => runtime.abortOwned(id, context))); + await runtime.commit(async (tx) => { + const conversationId = runtime.conversationId; + const live = await tx.doc(LiveDoc, conversationId); + const slot = toolSlot(live, runtime.taskId); + let settled; + if (input.kind === "nested") { + const nested = nestedResult(runtime.taskId, result, durationMs); + if (nested.usage !== void 0) + await recordUsage(tx, conversationId, "tools", call.name, nested.usage); + const state = await tx.doc(NestedCallDoc, input.parent, input.key, null); + if (state.taskId !== runtime.taskId) + throw new Error(`Nested call ${call.id} is not in its caller's index`); + state.result = nested; + if (slot !== void 0 && "parentTaskId" in slot) { + finishSlot(slot); + slot.summary = summaryOf(nested, result.output ?? []); + } + settled = { kind: "nested" }; + } else { + const timing = { timestamp: runtime.now(), durationMs }; + const entry2 = await appendToolResult(tx, conversationId, call, result, timing); + if (slot !== void 0 && !("parentTaskId" in slot)) { + finishSlot(slot); + slot.entry = entry2.id; + } + const control = result.control === void 0 || ending.status !== "completed" ? {} : { control: copyJson(result.control, { omitUndefinedProperties: true }) }; + settled = { kind: "model", entryId: entry2.id, ...control }; + } + if (madeNested) + removeNestedSlots(live, runtime.taskId); + if (ending.status === "aborted") + return { status: "terminal", outcome: { status: "aborted", result: settled } }; + if (ending.status === "failed") { + const error = { + message: ending.message, + ...ending.reason === void 0 ? {} : { detail: { reason: ending.reason } } + }; + return { status: "terminal", outcome: { status: "failed", error, result: settled } }; + } + return { status: "terminal", outcome: { status: "completed", result: settled } }; + }, context); +} +function nestedResult(taskId, result, durationMs) { + const nested = { + taskId, + structuredOutput: result.structuredOutput, + isError: result.isError ?? false, + details: result.details, + diagnostics: result.diagnostics ?? [], + usage: result.usage, + durationMs + }; + return copyJson(nested, { + omitUndefinedProperties: true + }); +} +async function startNestedCall(runtime, parentCallId, name, args, options, context) { + const key = options.key; + const call = { + type: "toolCall", + id: `${parentCallId}/${key}`, + name, + arguments: copyJson(args, { omitUndefinedProperties: true }) + }; + let id; + let replayed = false; + await runtime.commit(async (tx) => { + const state = await tx.doc(NestedCallDoc, runtime.taskId, key, null); + const existing = state.taskId; + if (existing !== void 0) { + const input2 = (await tx.task(existing))?.input; + const same = input2?.kind === "nested" && input2.call.name === name && jsonEqual2(input2.call.arguments, call.arguments); + if (!same) + throw new Error(`Nested call ${call.id} was already made with another tool or other arguments`); + id = existing; + replayed = state.result !== void 0; + return void 0; + } + const input = { + kind: "nested", + parent: runtime.taskId, + parentCallId, + key, + call, + ...options.progress === false ? { progress: false } : {} + }; + const created = await tx.createTask(ToolTask, input, { + ownership: { kind: "task", taskId: runtime.taskId }, + abandonOnRestart: options.abandonOnRestart + }); + state.taskId = created; + const live = await tx.doc(LiveDoc, runtime.conversationId); + live.nestedTools ??= []; + const slot = { + callId: call.id, + parentCallId, + parentTaskId: runtime.taskId, + name, + taskId: created, + arguments: call.arguments, + status: "pending" + }; + live.nestedTools.push(slot); + id = created; + return void 0; + }, context); + if (replayed) { + context.telemetry.addEvent("pi.durable.replay", { "tool.name": name }); + } + return id; +} +function outputValue(output) { + if (output.length === 0) + return ""; + if (output.length > 1) + return output; + const [only] = output; + return only.type === "text" ? only.text : only; +} +function fallbackResult(taskId, name, outcome) { + const message = outcome.status === "faulted" ? `Tool ${name} failed: ${outcome.error.message}` : outcome.status === "orphaned" ? `Tool ${name} could not resume: ${outcome.reason}` : `Tool ${name} ended without a result`; + const error = harnessError(outcome.status, message); + return { taskId, isError: true, diagnostics: error.diagnostics ?? [] }; +} +var COMPLETED = { status: "completed" }; +function fromSlot(slot, code, message) { + const droppedBytes = slot?.droppedBytes ?? 0; + const result = { + output: slot?.output === void 0 || slot.output === "" ? [] : [{ type: "text", text: slot.output }], + isError: true, + ...slot?.details === void 0 ? {} : { details: slot.details }, + diagnostics: [...slot?.diagnostics ?? [], toolDiagnostic(code, message)] + }; + if (droppedBytes === 0) + return { result }; + return { result, truncation: truncated({ droppedBytes, droppedLines: slot?.droppedLines ?? 0 }) }; +} +function harnessError(code, message) { + return { output: [], isError: true, diagnostics: [toolDiagnostic(code, message)] }; +} +function toolDiagnostic(code, message) { + return { severity: "error", code, message }; +} +function truncated(dropped, retain) { + const kept = retain === void 0 ? "" : ` to its ${retain === "head" ? "beginning" : "end"}`; + return { + severity: "warn", + code: "truncated", + message: `Output truncated${kept}: ${dropped.droppedLines} lines, ${dropped.droppedBytes} bytes dropped` + }; +} +var MAX_SUMMARY_ERROR_CHARS = 500; +function summaryOf(result, output) { + const error = result.isError ? ownedSlice2(errorTextOf(result, output), 0, MAX_SUMMARY_ERROR_CHARS) : ""; + return { + isError: result.isError, + ...result.durationMs === void 0 ? {} : { durationMs: result.durationMs }, + ...result.usage === void 0 ? {} : { usage: result.usage }, + ...error === "" ? {} : { error } + }; +} +function errorTextOf(result, output) { + const errors = result.diagnostics.filter((diagnostic) => diagnostic.severity === "error"); + if (errors.length > 0) + return errors.map((diagnostic) => diagnostic.message).join("\n"); + return output.flatMap((item) => item.type === "text" ? [item.text] : []).join("\n"); +} +async function appendToolResult(tx, conversationId, call, result, meta) { + const { timestamp, durationMs } = meta; + const diagnostics = [...result.diagnostics ?? []]; + const content = [...result.output ?? []]; + if (diagnostics.length > 0) + content.push({ type: "text", text: renderDiagnostics(diagnostics) }); + const message = { + role: "toolResult", + toolCallId: call.id, + toolName: call.name, + content, + ...result.details === void 0 ? {} : { details: result.details }, + ...result.usage === void 0 ? {} : { usage: result.usage }, + isError: result.isError ?? false, + ...durationMs === void 0 ? {} : { durationMs }, + timestamp + }; + if (result.usage !== void 0) + await recordUsage(tx, conversationId, "tools", call.name, result.usage); + return tx.appendEntry(ToolResultEntry, conversationId, { model: [message], data: { diagnostics } }); +} +function renderDiagnostics(diagnostics) { + return ` +${diagnostics.map((diagnostic) => `[${diagnostic.severity}] ${diagnostic.message}`).join("\n")} +`; +} +function boundContent(content, limits) { + const texts = content.filter((item) => item.type === "text"); + const bounded = boundOutput(texts.map((item) => item.text).join(""), limits); + if (bounded.droppedBytes === 0) + return { content, droppedBytes: 0, droppedLines: 0 }; + const keep = limits.retain === "head" ? texts[0] : texts.at(-1); + const result = []; + for (const item of content) { + if (item.type !== "text") + result.push(item); + else if (item === keep) + result.push({ ...item, text: bounded.text }); + } + return { content: result, droppedBytes: bounded.droppedBytes, droppedLines: bounded.droppedLines }; +} +function checkKey(key) { + if (key === "" || key.includes("/") || key === "__proto__" || /^[1-9][0-9]*$/.test(key)) { + throw new Error(`Nested call key ${JSON.stringify(key)} must be non-empty, without "/", not "__proto__", and not a positive integer`); + } +} +function jsonEqual2(a, b) { + if (a === b) + return true; + if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) + return false; + if (Array.isArray(a) || Array.isArray(b)) { + return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((item, i) => jsonEqual2(item, b[i])); + } + const keys = Object.keys(a); + return keys.length === Object.keys(b).length && keys.every((key) => Object.hasOwn(b, key) && jsonEqual2(a[key], b[key])); +} + +// node_modules/@earendil-works/pi-durable/dist/harness/generation.js +var DEFAULT_POLL_AFTER_MS = 5e3; +var GenerationTask = defineTask({ + name: "pi.generation", + version: 2, + initial: () => ({ phase: "prepare", attempt: 1 }), + // Version 2 leaves calls to tools not offered in a sequential round's `pending`, flagged `unoffered` in their slots. + // A version 1 Harness would start their tool tasks unflagged, resolving the name, so it leaves version 2 blocked + // instead. A version 1 checkpoint has no such calls and is kept as it is. + migrate: (input, checkpoint) => ({ + input, + checkpoint + }), + phases: { + /** + * Render the system prompt and tool loadout and append the positional `pi.system` entries they need, then move to + * `request`. The agent and settings resolved here are fixed for this request. Only the Harness writes model + * context to a busy conversation, so the context read here is still current at the commit; display-only entries + * may land meanwhile. + */ + prepare: async (task, runtime, context) => { + if (cutOffTooOften(task, runtime)) + return endCutOff2(runtime, task, context); + const { conversationId } = runtime; + const agent2 = await runtime.agent(context); + const settings = runtime.settings; + const { model: model2, thinkingLevel } = agent2; + const resolved = model2 === void 0 ? void 0 : runtime.models.getModel(model2.provider, model2.modelId); + if (model2 === void 0 || resolved === void 0) + return failNoModel(runtime, model2, context); + const { attempt, compacted, overflow, threshold: blocked } = task.state.checkpoint; + if (compacted !== void 0 && overflow !== void 0) { + const [outcome] = await runtime.outcomes([compacted], context); + if (outcome?.status !== "completed" || outcome.result.entryId === void 0) { + return failModelError(runtime, overflow, context); + } + } + if (compacted !== void 0 && blocked === true) { + const [outcome] = await runtime.outcomes([compacted], context); + if (outcome !== void 0 && outcome.status !== "completed") { + return failModelError(runtime, compactionFailure(outcome), context); + } + } + const view2 = await runtime.context(conversationId, context); + const last = view2.messages.findLast((message) => message.role !== "system"); + if (last?.role !== "user" && last?.role !== "toolResult") + return failNothingToAnswer(runtime, context); + const shown = replaySections(view2.messages); + let env; + try { + env = await runtime.env(context); + } catch (error) { + if (context.abortSignal?.aborted) + throw error; + runtime.report(error, { kind: "host", callback: "env" }); + } + const input = { conversationId, agent: agent2, env, shown: Object.fromEntries(shown), read: runtime }; + const desired = await renderSections(agent2.sections, input, shown, (error, section2) => runtime.report(error, { + kind: "extension", + ...optionalExtension(sectionExtension(agent2, section2.key)) + }), context); + const entries2 = planSystemEntries(view2, desired, agent2.tools, runtime.now()); + const threshold = compacted === void 0 ? thresholdCompaction(view2, entries2, resolved.contextWindow, compactionPolicy(settings, model2)) : void 0; + if (threshold === "blocking") { + await runtime.commit(async (tx) => { + const child = await createCompaction(tx, conversationId, { reason: "threshold" }, runtime.taskId); + const checkpoint = { phase: "prepare", attempt, compacted: child, threshold: true }; + return { status: "waiting", checkpoint, on: [child], policy: "allSettled" }; + }, context); + return; + } + await runtime.commit(async (tx) => { + let cutoff = await tx.latestEntryId(conversationId); + for (const entry2 of entries2) + cutoff = (await tx.appendEntry(SystemEntry, conversationId, entry2)).id; + if (threshold === "background" && (await tx.doc(LiveDoc, conversationId)).compactions === void 0) { + await createCompaction(tx, conversationId, { reason: "threshold" }); + } + const request2 = { + attempt, + ...compacted === void 0 ? {} : { compacted }, + model: model2, + thinkingLevel, + streamOptions: settings.stream, + cutoff + }; + return { status: "running", checkpoint: { phase: "request", ...request2 } }; + }, context); + }, + request: async (task, runtime, context) => { + if (cutOffTooOften(task, runtime)) + return endCutOff2(runtime, task, context); + if (task.restarts !== void 0) + return interrupted(runtime, task.state.checkpoint, context); + const { attempt, compacted, model: ref, thinkingLevel, streamOptions, cutoff } = task.state.checkpoint; + const conversationId = runtime.conversationId; + await runtime.commit(async (tx) => { + const live = await tx.doc(LiveDoc, conversationId); + await convertPartial(tx, live, conversationId); + live.generation = { attempt }; + return void 0; + }, context, PROGRESS_COMMIT); + const model2 = runtime.models.getModel(ref.provider, ref.modelId); + if (model2 === void 0) + return failNoModel(runtime, ref, context); + const view2 = await runtime.context(conversationId, context, { at: cutoff }); + let messages = view2.messages; + await runtime.hooks.each("beforeRequest", async (hook2) => { + const replaced = await hook2({ messages }, runtime, context); + if (replaced !== void 0) + messages = replaced.messages; + }); + const options = { + ...streamOptions, + signal: runtime.signal, + telemetry: context.telemetry, + sessionId: await ensureProviderSessionId(runtime, context), + ...thinkingLevel === "off" ? {} : { reasoning: thinkingLevel }, + onPayload: (payload) => providerPayload(runtime, model2, payload, context) + }; + const message = await streamResponse(runtime, model2, messages, options, attempt, context); + const request2 = { attempt, compacted, model: ref, thinkingLevel, cutoff, messages: view2.messages }; + await classify(runtime, request2, message, context); + }, + /** After the backoff's sleep; a run before `until` (an early wake, or a record of an earlier version) sleeps again. */ + retry: async (task, runtime, context) => { + const { attempt, compacted, until, poll } = task.state.checkpoint; + if (cutOffTooOften(task, runtime)) + return endCutOff2(runtime, task, context); + if (until > runtime.now()) + return runtime.sleepUntil(until, task.state.checkpoint, context); + await runtime.commit(async (tx) => { + const live = await tx.doc(LiveDoc, runtime.conversationId); + const kept = compacted === void 0 ? {} : { compacted }; + if (poll !== void 0) { + const pollAt = runtime.now(); + live.generation = { attempt: attempt + 1, deferred: { pollAt } }; + return { + status: "running", + checkpoint: { phase: "poll", attempt: attempt + 1, ...kept, ...poll, pollAt } + }; + } + live.generation = { attempt: attempt + 1 }; + return { status: "running", checkpoint: { phase: "prepare", attempt: attempt + 1, ...kept } }; + }, context); + }, + /** After the sleep until `pollAt`; a run before it (an early wake, or a record of an earlier version) sleeps again. */ + poll: async (task, runtime, context) => { + const { attempt, compacted, model: ref, thinkingLevel, cutoff, handle, pollAt } = task.state.checkpoint; + if (cutOffTooOften(task, runtime)) + return endCutOff2(runtime, task, context); + if (task.restarts !== void 0) + return interrupted(runtime, task.state.checkpoint, context); + if (pollAt > runtime.now()) + return runtime.sleepUntil(pollAt, task.state.checkpoint, context); + const model2 = runtime.models.getModel(ref.provider, ref.modelId); + if (model2 === void 0) + return failNoModel(runtime, ref, context); + const message = await runtime.models.fetchDeferred(model2, handle, { + signal: runtime.signal, + telemetry: context.telemetry + }); + const request2 = { attempt, compacted, model: ref, thinkingLevel, cutoff, pollAt }; + await classify(runtime, request2, message, context); + }, + tools: async (task, runtime, context) => { + if (cutOffTooOften(task, runtime)) + return endCutOff2(runtime, task, context); + const { assistant, tools, pending } = task.state.checkpoint; + const [next, ...rest] = pending; + if (next === void 0) + return finishToolRound(runtime, assistant, tools, context); + await runtime.commit(async (tx) => { + const live = await tx.doc(LiveDoc, runtime.conversationId); + const slot = live.tools?.find((slot2) => slot2.callId === next && slot2.taskId === void 0); + const taskId = await createToolTask(tx, runtime, assistant, next, slot?.unoffered); + if (slot !== void 0) + slot.taskId = taskId; + const checkpoint = { + phase: "tools", + assistant, + tools: [...tools, taskId], + pending: rest + }; + return { status: "waiting", checkpoint, on: [taskId], policy: "allSettled" }; + }, context); + }, + /** + * Ask the `afterRun` hooks for tasks and create them, as the generation's children, in the commit that moves to + * `drain`; without any, end the run now. + */ + finish: async (task, runtime, context) => { + const { run: run2 } = task.state.checkpoint; + if (cutOffTooOften(task, runtime)) + return endAnsweredCutOff(runtime, task, run2.answer, context); + const prepared = []; + await runtime.hooks.each("afterRun", async (hook2) => { + const asked = await hook2(run2, runtime, context); + if (asked !== void 0) + prepareRunEndTasks(runtime, asked.tasks, prepared); + }); + if (prepared.length === 0) + return endAnsweredRun(runtime, run2.answer, context); + await runtime.commit(async (tx) => { + const tasks = []; + for (const { task: task2, input } of prepared) { + tasks.push(await tx.createTask(task2, input, { ownership: { kind: "task", taskId: runtime.taskId } })); + } + const live = await tx.doc(LiveDoc, runtime.conversationId); + if (live.run?.taskId === runtime.taskId && live.run.afterRun !== void 0) + live.run.afterRun.tasks = tasks; + const checkpoint = { phase: "drain", answer: run2.answer, tasks }; + return { status: "waiting", checkpoint, on: tasks, policy: "allSettled" }; + }, context); + }, + /** The after-run tasks are terminal, whatever their outcomes: end the run. */ + drain: async (task, runtime, context) => { + const { answer: answer2 } = task.state.checkpoint; + if (cutOffTooOften(task, runtime)) + return endAnsweredCutOff(runtime, task, answer2, context); + return endAnsweredRun(runtime, answer2, context); + } + }, + abort: async (task, runtime, context) => { + const checkpoint = task.state.checkpoint; + const polled = deferredOf(checkpoint); + if (polled !== void 0) + await cancelDeferred(runtime, polled, context); + const conversationId = runtime.conversationId; + const unstarted = checkpoint.phase === "tools" ? await readCalls(runtime, checkpoint.assistant, checkpoint.pending, context) : []; + await runtime.commit(async (tx) => { + const live = await tx.doc(LiveDoc, conversationId); + await convertPartial(tx, live, conversationId); + for (const call of unstarted) { + const unoffered = live.tools?.find((slot) => slot.callId === call.id)?.unoffered === true; + const result = unoffered ? harnessError("tool_unavailable", `Tool ${call.name} is not available`) : harnessError("aborted", `Tool ${call.name} was aborted`); + await appendToolResult(tx, conversationId, call, result, { timestamp: runtime.now() }); + } + endRun(tx, live, runtime.taskId, { status: "unanswered", reason: "aborted" }); + return { status: "terminal", outcome: { status: "aborted" } }; + }, context); + } +}); +async function readCalls(runtime, assistant, callIds, context) { + const message = (await runtime.entry(AssistantEntry, assistant, context))?.model?.[0]; + const calls = message?.role === "assistant" ? message.content.filter((content) => content.type === "toolCall") : []; + return callIds.flatMap((id) => calls.filter((call) => call.id === id).slice(0, 1)); +} +function createToolTask(tx, runtime, assistant, callId, unoffered) { + return tx.createTask(ToolTask, { kind: "model", assistant, callId, ...unoffered === void 0 ? {} : { unoffered } }, { ownership: { kind: "task", taskId: runtime.taskId } }); +} +function thresholdCompaction(view2, planned, contextWindow, policy) { + if (!policy.enabled || contextWindow <= 0) + return void 0; + const tokens = estimateContext(view2, planned.flatMap((entry2) => entry2.model ?? [])); + const blocking = contextWindow - policy.reserveTokens; + const background = blocking - policy.backgroundTokens; + const over = tokens > blocking ? "blocking" : policy.backgroundTokens > 0 && tokens > background ? "background" : void 0; + if (over === void 0 || selectCut(view2, policy.keepRecentTokens) === void 0) + return void 0; + return over; +} +function compactionFailure(outcome) { + switch (outcome.status) { + case "failed": + case "faulted": + return `Compaction failed: ${outcome.error.message}`; + case "orphaned": + return `Compaction failed: ${outcome.reason}`; + case "aborted": + return "Compaction was aborted"; + } +} +async function failModelError(runtime, text, context) { + await runtime.commit(async (tx) => { + const failure = await prepareFailure(tx, runtime); + const live = await tx.doc(LiveDoc, runtime.conversationId); + await endFailedRun(tx, runtime, live, failure, { status: "unanswered", reason: "model_error", detail: text }); + return { + status: "terminal", + outcome: { status: "failed", error: { message: text, detail: { reason: "model_error" } } } + }; + }, context); +} +async function interrupted(runtime, checkpoint, context) { + const { attempt, compacted } = checkpoint; + const policy = runtime.settings.retry; + const retry = policy.enabled && attempt <= policy.maxRetries; + const until = retry ? runtime.now() + retryDelayMs(policy, attempt) : 0; + const polled = checkpoint.phase === "poll" ? checkpoint : void 0; + const text = polled === void 0 ? `The host stopped while the model was answering, on attempt ${attempt}` : `The host stopped while the deferred answer was fetched, on attempt ${attempt}`; + if (!retry && polled !== void 0) + await cancelDeferred(runtime, polled, context); + await runtime.commit(async (tx) => { + const failure = retry ? void 0 : await prepareFailure(tx, runtime); + const live = await tx.doc(LiveDoc, runtime.conversationId); + await convertPartial(tx, live, runtime.conversationId); + if (failure === void 0) { + live.generation = { attempt, retry: { at: until, error: text } }; + const poll = polled === void 0 ? {} : { + poll: { + model: polled.model, + ...polled.thinkingLevel === void 0 ? {} : { thinkingLevel: polled.thinkingLevel }, + cutoff: polled.cutoff, + handle: polled.handle + } + }; + const next = { phase: "retry", attempt, ...compacted === void 0 ? {} : { compacted }, until, ...poll }; + return { status: "sleeping", checkpoint: next, until }; + } + await endFailedRun(tx, runtime, live, failure, { status: "unanswered", reason: "interrupted", detail: text }); + return { + status: "terminal", + outcome: { status: "failed", error: { message: text, detail: { reason: "interrupted" } } } + }; + }, context); +} +async function endCutOff2(runtime, task, context) { + const checkpoint = task.state.checkpoint; + const text = `The generation was interrupted ${cutOffCount(task)} in its ${checkpoint.phase} phase`; + const salvage = salvageable(task, runtime); + const polled = deferredOf(checkpoint); + if (salvage && polled !== void 0) + await cancelDeferred(runtime, polled, context); + await runtime.commit(async (tx) => { + const failure = await prepareFailure(tx, runtime); + const live = await tx.doc(LiveDoc, runtime.conversationId); + if (salvage) + await convertPartial(tx, live, runtime.conversationId); + await endFailedRun(tx, runtime, live, failure, { status: "unanswered", reason: "restart_limit", detail: text }); + return { + status: "terminal", + outcome: { status: "failed", error: { message: text, detail: { reason: "restart_limit" } } } + }; + }, context); +} +function deferredOf(checkpoint) { + if (checkpoint.phase === "poll") + return checkpoint; + return checkpoint.phase === "retry" ? checkpoint.poll : void 0; +} +async function cancelDeferred(runtime, checkpoint, context) { + const model2 = runtime.models.getModel(checkpoint.model.provider, checkpoint.model.modelId); + if (model2 === void 0) + return; + try { + await runtime.models.cancelDeferred(model2, checkpoint.handle, { + signal: runtime.signal, + telemetry: context.telemetry + }); + } catch (error) { + runtime.report(error, { kind: "host", callback: "models" }); + } +} +async function failNoModel(runtime, ref, context) { + const message = ref === void 0 ? "No model is configured" : `Model ${ref.provider}/${ref.modelId} is not available`; + await runtime.commit(async (tx) => { + const failure = await prepareFailure(tx, runtime); + const live = await tx.doc(LiveDoc, runtime.conversationId); + await endFailedRun(tx, runtime, live, failure, { + status: "unanswered", + reason: "no_model", + detail: message, + settlement: { status: "unanswered", reason: "no_model" } + }); + return { status: "terminal", outcome: { status: "failed", error: { message, detail: { reason: "no_model" } } } }; + }, context); +} +async function failNothingToAnswer(runtime, context) { + const message = "The conversation has nothing to answer"; + await runtime.commit(async (tx) => { + const failure = await prepareFailure(tx, runtime); + const live = await tx.doc(LiveDoc, runtime.conversationId); + await endFailedRun(tx, runtime, live, failure, { status: "unanswered", reason: "nothing_to_answer" }); + const error = { message, detail: { reason: "nothing_to_answer" } }; + return { status: "terminal", outcome: { status: "failed", error } }; + }, context); +} +async function prepareFailure(tx, runtime) { + const settings = runtime.settings; + const policy = settings.afterFailedRun; + if (policy !== "continue") + return { policy }; + return { policy, boundary: await prepareBoundary(tx, runtime.conversationId, settings, "final", runtime.now()) }; +} +async function endFailedRun(tx, runtime, live, failure, end) { + const conversationId = runtime.conversationId; + endRun(tx, live, runtime.taskId, end); + if (failure.policy === "withdraw") + await withdrawQueuedInputs(tx, conversationId, "run_failed"); + if (failure.boundary === void 0) + return; + const { users, first } = await applyBoundary(tx, failure.boundary, "final", runtime.now()); + if (users.length > 0) + await startRun(tx, conversationId, live, { inputs: users, first }, { keepFailure: true }); +} +async function convertPartial(tx, live, conversationId) { + const partial = live.generation?.message; + if (partial === void 0) + return; + const message = copyJson(partial); + await appendAssistant(tx, conversationId, { ...message, stopReason: "aborted" }); +} +async function providerPayload(runtime, model2, payload, context) { + let current = payload; + await runtime.hooks.each("beforeProviderRequest", async (hook2) => { + const replaced = await hook2({ model: model2, payload: current }, runtime, context); + if (replaced !== void 0) + current = replaced.payload; + }); + return current; +} +async function streamResponse(runtime, model2, messages, options, attempt, context) { + const interval = runtime.settings.progress.partialIntervalMs; + let pending; + let timer; + let inFlight; + let stopped = false; + const changes = new PartialChanges(); + const flush = () => { + timer = void 0; + const partial = pending; + pending = void 0; + if (partial === void 0 || stopped) + return; + inFlight = (async () => { + const message = copyJson(partial, { omitUndefinedProperties: true }); + const taken = changes.take(message); + try { + await runtime.commit(async (tx) => { + const live = await tx.doc(LiveDoc, runtime.conversationId); + live.generation ??= { attempt }; + changes.commit(live.generation, message, taken); + return void 0; + }, context, PROGRESS_COMMIT); + } catch (error) { + changes.restore(taken); + throw error; + } + })().catch((error) => { + if (!runtime.signal.aborted) + runtime.report(error); + }).finally(() => { + inFlight = void 0; + if (pending !== void 0 && !stopped) + timer = runtime.setTimer(flush, interval); + }); + }; + try { + const events = runtime.models.streamSimple(model2, { messages: [...messages] }, options); + for await (const event of events) { + if (event.type === "done" || event.type === "error" || event.partial.content.length === 0) + continue; + changes.observe(event); + pending = event.partial; + if (timer === void 0 && inFlight === void 0) + timer = runtime.setTimer(flush, interval); + } + return await events.result(); + } finally { + stopped = true; + timer?.(); + await inFlight; + } +} +async function classify(runtime, request2, message, context) { + runtime.signal.throwIfAborted(); + const conversationId = runtime.conversationId; + const { attempt, compacted, model: ref, thinkingLevel, cutoff } = request2; + if (thinkingLevel !== void 0) + message.thinkingLevel = thinkingLevel; + if (message.stopReason === "deferred" && message.deferred !== void 0) { + const handle = message.deferred; + const pollAt = Math.max(runtime.now() + (handle.pollAfterMs ?? DEFAULT_POLL_AFTER_MS), request2.pollAt === void 0 ? Number.NEGATIVE_INFINITY : request2.pollAt + 1); + await runtime.commit(async (tx) => { + (await tx.doc(LiveDoc, conversationId)).generation = { attempt, deferred: { pollAt } }; + const checkpoint = { + phase: "poll", + attempt, + ...compacted === void 0 ? {} : { compacted }, + model: ref, + ...thinkingLevel === void 0 ? {} : { thinkingLevel }, + cutoff, + handle, + pollAt + }; + return { status: "sleeping", checkpoint, until: pollAt }; + }, context); + return; + } + await runtime.hooks.each("afterResponse", (hook2) => hook2(message, runtime, context)); + const calls = message.content.filter((content) => content.type === "toolCall"); + if (message.stopReason === "toolUse" && calls.length > 0) { + return startToolRound(runtime, request2, message, calls, context); + } + if (message.stopReason === "stop" || message.stopReason === "length" || message.stopReason === "toolUse") { + return answer(runtime, message, context); + } + const settings = runtime.settings; + const overflow = message.stopReason === "error" && isContextOverflow(message); + const compaction = compactionPolicy(settings, ref); + if (overflow && compacted === void 0 && compaction.enabled) { + const view2 = await runtime.context(conversationId, context, { at: cutoff }); + if (selectCut(view2, compaction.keepRecentTokens) !== void 0) { + const text = message.errorMessage ?? "Context overflow"; + await runtime.commit(async (tx) => { + const live = await tx.doc(LiveDoc, conversationId); + await appendAssistant(tx, conversationId, message); + delete live.generation; + const child = await createCompaction(tx, conversationId, { reason: "overflow" }, runtime.taskId); + const checkpoint = { phase: "prepare", attempt, compacted: child, overflow: text }; + return { status: "waiting", checkpoint, on: [child], policy: "allSettled" }; + }, context); + return; + } + } + const policy = settings.retry; + const retry = message.stopReason === "error" && !overflow && isRetryableAssistantError(message) && policy.enabled && attempt <= policy.maxRetries; + const until = retry ? runtime.now() + retryDelayMs(policy, attempt) : 0; + await runtime.commit(async (tx) => { + const failure = retry ? void 0 : await prepareFailure(tx, runtime); + const live = await tx.doc(LiveDoc, conversationId); + await appendAssistant(tx, conversationId, message); + if (failure === void 0) { + live.generation = { attempt, retry: { at: until, error: message.errorMessage ?? "" } }; + const checkpoint = { + phase: "retry", + attempt, + ...compacted === void 0 ? {} : { compacted }, + until + }; + return { status: "sleeping", checkpoint, until }; + } + const text = message.errorMessage ?? `Model response ended with stop reason ${message.stopReason}`; + await endFailedRun(tx, runtime, live, failure, { status: "unanswered", reason: "model_error", detail: text }); + return { + status: "terminal", + outcome: { status: "failed", error: { message: text, detail: { reason: "model_error" } } } + }; + }, context); +} +async function answer(runtime, message, context) { + let continuation; + await runtime.hooks.each("onYield", async (hook2) => { + if (continuation !== void 0) + return; + continuation = (await hook2(message, runtime, context))?.continue; + }); + const afterRun = await asksAfterRun(runtime, context); + const conversationId = runtime.conversationId; + await runtime.commit(async (tx) => { + const boundary = await prepareBoundary(tx, conversationId, runtime.settings, "final", runtime.now()); + const live = await tx.doc(LiveDoc, conversationId); + const entry2 = await appendAssistant(tx, conversationId, message); + const result = { status: "terminal", outcome: { status: "completed", result: { entryId: entry2.id } } }; + const continues = takesContinuation(continuation, boundary) ? continuation : void 0; + if (continues === void 0 && afterRun) { + const held = holdRun(tx, live, runtime, { reason: "answer", answer: entry2.id, last: entry2.id }); + if (held !== void 0) + return held; + } + const { users, first } = await applyBoundary(tx, boundary, "final", runtime.now()); + if (continues !== void 0) { + const user = { role: "user", content: continues, timestamp: runtime.now() }; + await tx.appendEntry(UserEntry, conversationId, { model: [user] }); + if (live.run?.taskId === runtime.taskId) + live.run.turns = 0; + handOver(live, runtime.taskId, await createGeneration(tx, conversationId)); + delete live.generation; + return result; + } + endRun(tx, live, runtime.taskId, { status: "done", answer: entry2.id }); + if (users.length > 0) + await startRun(tx, conversationId, live, { inputs: users, first }); + return result; + }, context); +} +async function asksAfterRun(runtime, context) { + const hooks = agentHooks(await runtime.agent(context), GenerationTask.definition.name); + return hooks.some(({ handlers }) => typeof handlers.afterRun === "function"); +} +function holdRun(tx, live, runtime, end) { + const run2 = live.run; + if (run2?.taskId !== runtime.taskId) + return void 0; + for (const id of run2.inputs) + tx.settleSubmission(id, { status: "done", answer: end.answer }); + run2.inputs = []; + run2.afterRun = {}; + delete live.generation; + delete live.tools; + delete live.nestedTools; + const summary = { ...end, first: run2.first ?? end.answer }; + return { status: "running", checkpoint: { phase: "finish", run: summary } }; +} +function prepareRunEndTasks(runtime, requests, prepared) { + const rejected = []; + for (const request2 of requests) { + try { + const name = request2.task.definition.name; + if (name.startsWith("pi.")) + throw new Error(`An afterRun hook cannot start the built-in task ${name}`); + const installed = runtime.registry.task(name); + if (installed === void 0) + throw new Error(`Task ${name} is not installed`); + const input = copyJson(request2.input, { omitUndefinedProperties: true }); + const definition = installed.definition; + const checkpoint = copyJson(definition.initial(input), { omitUndefinedProperties: true }); + if (!Object.hasOwn(definition.phases, checkpoint.phase)) { + throw new Error(`Task ${name} starts at the unknown phase ${String(checkpoint.phase)}`); + } + prepared.push({ task: { definition: { ...definition, initial: () => checkpoint } }, input }); + } catch (error) { + rejected.push(error); + } + } + if (rejected.length === 1) + throw rejected[0]; + if (rejected.length > 1) + throw new AggregateError(rejected, `${rejected.length} afterRun task requests were rejected`); +} +async function endAnsweredRun(runtime, answer2, context) { + const conversationId = runtime.conversationId; + await runtime.commit(async (tx) => { + const boundary = await prepareBoundary(tx, conversationId, runtime.settings, "final", runtime.now()); + const live = await tx.doc(LiveDoc, conversationId); + const { users, first } = await applyBoundary(tx, boundary, "final", runtime.now()); + endRun(tx, live, runtime.taskId, { status: "done", answer: answer2 }); + if (users.length > 0) + await startRun(tx, conversationId, live, { inputs: users, first }); + return { status: "terminal", outcome: { status: "completed", result: { entryId: answer2 } } }; + }, context); +} +async function endAnsweredCutOff(runtime, task, answer2, context) { + const text = `The generation was interrupted ${cutOffCount(task)} in its ${task.state.checkpoint.phase} phase`; + await runtime.commit(async (tx) => { + const live = await tx.doc(LiveDoc, runtime.conversationId); + endRun(tx, live, runtime.taskId, { status: "done", answer: answer2 }); + const completed = { status: "terminal", outcome: { status: "completed", result: { entryId: answer2 } } }; + return endedByCrashes(completed, text); + }, context); +} +function takesContinuation(continuation, boundary) { + const items = boundary.inbox.items; + return continuation !== void 0 && !items.some((item) => item.mode === "steer" || item.mode === "followUp") && !hasQueuedReset(items); +} +async function startToolRound(runtime, request2, message, calls, context) { + const conversationId = runtime.conversationId; + const messages = request2.messages ?? (await runtime.context(conversationId, context, { at: request2.cutoff })).messages; + const offered = new Set(getCurrentTools(messages).map((tool) => tool.name)); + const tools = (await runtime.agent(context)).tools; + const sequential = runtime.settings.toolExecution === "sequential" || calls.some((call) => offered.has(call.name) && tools.find((tool) => tool.name === call.name)?.executionMode === "sequential"); + await runtime.commit(async (tx) => { + const live = await tx.doc(LiveDoc, conversationId); + const entry2 = await appendAssistant(tx, conversationId, message); + const slots = []; + const tools2 = []; + const pending = []; + for (const call of calls) { + const unoffered = offered.has(call.name) ? {} : { unoffered: true }; + if (sequential && tools2.length > 0) { + pending.push(call.id); + slots.push({ callId: call.id, name: call.name, status: "pending", ...unoffered }); + continue; + } + const taskId = await createToolTask(tx, runtime, entry2.id, call.id, unoffered.unoffered); + tools2.push(taskId); + slots.push({ callId: call.id, name: call.name, taskId, status: "pending", ...unoffered }); + } + delete live.generation; + live.tools = slots; + if (live.run?.taskId === runtime.taskId) { + live.run.turns += 1; + live.run.first ??= entry2.id; + } + const checkpoint = { phase: "tools", assistant: entry2.id, tools: tools2, pending }; + return { status: "waiting", checkpoint, on: tools2, policy: "allSettled" }; + }, context); +} +async function finishToolRound(runtime, assistant, tools, context) { + const conversationId = runtime.conversationId; + const controls = /* @__PURE__ */ new Map(); + const outcomes = await runtime.outcomes(tools, context); + tools.forEach((id, index) => { + const outcome = outcomes[index]; + controls.set(id, outcome.status === "completed" && outcome.result.kind !== "nested" ? outcome.result.control : void 0); + }); + const slots = (await runtime.snapshot(LiveDoc, conversationId, context))?.tools ?? []; + const results = slots.flatMap((slot) => slot.entry === void 0 ? [] : [slot.entry]); + await runtime.hooks.each("afterTools", (hook2) => hook2(assistant, results, runtime, context)); + const ordered = slots.map((slot) => slot.taskId === void 0 ? void 0 : controls.get(slot.taskId)); + const terminate = slots.length > 0 && ordered.every((control) => control?.terminate === true); + const added = ordered.flatMap((control) => control?.addTools ?? []); + const handoff = ordered.findLast((control) => control?.handoff !== void 0)?.handoff; + const maxTurns = (await runtime.agent(context)).maxTurns; + const afterRun = terminate && await asksAfterRun(runtime, context); + await runtime.commit(async (tx) => { + const boundary = await prepareBoundary(tx, conversationId, runtime.settings, terminate || handoff !== void 0 ? "final" : "postTools", runtime.now()); + if (added.length > 0) + await addTools(tx, conversationId, added); + const live = await tx.doc(LiveDoc, conversationId); + const now = runtime.now(); + let last = results.reduce((newest, id) => id > newest ? id : newest, assistant); + if (handoff !== void 0) { + const message = { role: "user", content: handoff, timestamp: now }; + const entry2 = await tx.appendEntry(ResetEntry, conversationId, { head: "self", model: [message] }); + boundary.head = entry2.id; + last = entry2.id; + } + const turns = live.run?.taskId === runtime.taskId ? live.run.turns : 0; + const held = afterRun ? holdRun(tx, live, runtime, { reason: "terminate", answer: assistant, last }) : void 0; + if (held !== void 0) + return held; + if (terminate) { + const { users, first } = await applyBoundary(tx, boundary, "final", now); + endRun(tx, live, runtime.taskId, { status: "done", answer: assistant }); + if (users.length > 0) + await startRun(tx, conversationId, live, { inputs: users, first }); + } else if (maxTurns !== void 0 && turns >= maxTurns) { + const policy = runtime.settings.afterFailedRun; + const failure = { policy, ...policy === "continue" ? { boundary } : {} }; + await endFailedRun(tx, runtime, live, failure, { + status: "unanswered", + reason: "turn_limit", + detail: turns === 1 ? "1 turn" : `${turns} turns` + }); + } else { + const { users, first, reset } = await applyBoundary(tx, boundary, "postTools", now); + if (reset) { + endRun(tx, live, runtime.taskId, { status: "unanswered", reason: "reset" }); + if (users.length > 0) { + await startRun(tx, conversationId, live, { inputs: users, first }, { keepFailure: true }); + } + } else { + delete live.tools; + delete live.nestedTools; + if (live.run?.taskId === runtime.taskId) + live.run.inputs.push(...users); + handOver(live, runtime.taskId, await createGeneration(tx, conversationId)); + } + } + return { status: "terminal", outcome: { status: "completed", result: { entryId: assistant } } }; + }, context); +} +async function appendAssistant(tx, conversationId, message) { + await recordUsage(tx, conversationId, "models", `${message.provider}/${message.model}`, message.usage); + return tx.appendEntry(AssistantEntry, conversationId, { model: [message] }); +} +async function startRun(tx, conversationId, live, run2, options) { + const { inputs, first } = run2; + const taskId = await createGeneration(tx, conversationId, { submissionIds: inputs }); + live.run = { taskId, inputs, turns: 0, ...first === void 0 ? {} : { first } }; + if (options?.keepFailure !== true) + delete live.runFailure; +} +function createGeneration(tx, conversationId, telemetry = {}) { + return tx.createTask(GenerationTask, {}, { ownership: { kind: "conversation" }, conversationId, ...telemetry }); +} +function handOver(live, from, to) { + if (live.run?.taskId === from) + live.run.taskId = to; +} + +// node_modules/@earendil-works/pi-durable/dist/harness/live.js +var LiveDoc = defineDoc({ + kind: "pi.live", + version: 3, + scope: "conversation", + history: "latest", + fork: "initial", + initial: () => ({}), + // Version 1 named a compaction's trigger `reason`, in `compactions` and `compactionFailure`. Versions 1 and 2 (as + // builds before `run.turns` stored it) may lack `run.turns`: a run in flight across the upgrade counts its turns + // from 0. Version 2 already names the trigger `trigger`, so renaming leaves it alone. + migrate: (value2) => { + const live = value2; + const { run: run2, compactions, compactionFailure: compactionFailure2 } = live; + return { + ...value2, + ...run2 === void 0 ? {} : { run: { ...run2, turns: typeof run2.turns === "number" ? run2.turns : 0 } }, + ...compactions === void 0 ? {} : { compactions: compactions.map(renameReason) }, + ...compactionFailure2 === void 0 ? {} : { compactionFailure: renameReason(compactionFailure2) } + }; + }, + // REMINDER: a complete base whenever nothing runs (spec §8.2): no generation and no running tool slot. That holds + // while idle, in the commit handing a generation over to its tool round, and between tools, so the delta chain + // spans at most one generation or the overlapping execution of one round's tools. A slot holds output only while + // running, so every base is small; a nested call's result lives in its caller's task documents, never here. Do not + // add a delta-count bound; the tool output benchmark checks this rule. + checkpointWhen: (value2) => value2.generation === void 0 && !(value2.tools ?? []).some((slot) => slot.status === "running") && !(value2.nestedTools ?? []).some((slot) => slot.status === "running") +}); +function renameReason(value2) { + return Object.fromEntries(Object.entries(value2).map(([key, field]) => [key === "reason" ? "trigger" : key, field])); +} +var PROGRESS_COMMIT = { persistence: { kind: "deferred" } }; +var RUN_TASK_KINDS = /* @__PURE__ */ new Set(["pi.generation"]); +var TOOL_TASK_KIND = "pi.tool"; +var COMPACTION_TASK_KIND = "pi.compaction"; +var LIMITS_RESTARTS = /* @__PURE__ */ new Set([...RUN_TASK_KINDS, TOOL_TASK_KIND, COMPACTION_TASK_KIND]); +function cutOffTooOften(task, runtime) { + return task.restarts !== void 0 && task.restarts >= runtime.settings.restartLimit; +} +function salvageable(task, runtime) { + return (task.restarts ?? 0) <= runtime.settings.restartLimit; +} +function cutOffCount(task) { + const restarts = task.restarts ?? 0; + return restarts === 1 ? "once" : `${restarts} times in a row`; +} +function endRun(tx, live, taskId, end) { + if (live.run?.taskId === taskId) { + const inputs = [...live.run.inputs]; + const { first } = live.run; + if (end.status === "done") { + for (const id of inputs) + tx.settleSubmission(id, { status: "done", answer: end.answer }); + } else if (live.run.afterRun === void 0) { + const { reason, detail } = end; + const settlement = end.settlement ?? { + status: "unanswered", + reason, + ...detail === void 0 ? {} : { detail } + }; + for (const id of inputs) + tx.settleSubmission(id, settlement); + live.runFailure = { + taskId, + inputs, + reason, + ...detail === void 0 ? {} : { detail }, + ...first === void 0 ? {} : { first } + }; + } + delete live.run; + } + delete live.generation; + delete live.tools; + delete live.nestedTools; +} +function addCompactionStatus(live, status) { + live.compactions ??= []; + live.compactions.push(status); +} +function compactionStatus(live, taskId) { + return live.compactions?.find((status) => status.taskId === taskId); +} +function removeCompactionStatus(live, taskId) { + const statuses = live.compactions; + if (statuses === void 0) + return; + const index = statuses.findIndex((status) => status.taskId === taskId); + if (index >= 0) + statuses.splice(index, 1); + if (statuses.length === 0) + delete live.compactions; +} +function failCompactionStatus(live, taskId, error) { + const status = compactionStatus(live, taskId); + if (status !== void 0) + live.compactionFailure = { taskId: status.taskId, trigger: status.trigger, error }; + removeCompactionStatus(live, taskId); +} +function toolSlot(live, taskId) { + const slot = live.tools?.find((each) => each.taskId === taskId); + if (slot !== void 0 || live.nestedTools === void 0) + return slot; + const nested = live.nestedTools; + let low = 0; + let high = nested.length - 1; + while (low <= high) { + const middle = low + high >>> 1; + const candidate = nested[middle]; + if (candidate.taskId === taskId) + return candidate; + if (candidate.taskId < taskId) + low = middle + 1; + else + high = middle - 1; + } + return void 0; +} +function finishSlot(slot) { + slot.status = "done"; + clearProgress(slot); +} +function removeNestedSlots(live, taskId) { + const nested = live.nestedTools; + if (nested === void 0) + return; + const removed = /* @__PURE__ */ new Set([taskId]); + for (const slot of nested) + if (removed.has(slot.parentTaskId)) + removed.add(slot.taskId); + for (let index = nested.length - 1; index >= 0; index--) { + if (removed.has(nested[index].parentTaskId)) + nested.splice(index, 1); + } + if (nested.length === 0) + delete live.nestedTools; +} +function clearProgress(slot) { + delete slot.output; + delete slot.droppedBytes; + delete slot.droppedLines; + delete slot.details; + delete slot.diagnostics; +} +async function settleSchedulerOutcome(tx, record, outcome) { + if (record.kind === TOOL_TASK_KIND) { + const live2 = await tx.doc(LiveDoc, record.conversationId); + const slot = toolSlot(live2, record.id); + if (slot !== void 0) + finishSlot(slot); + removeNestedSlots(live2, record.id); + return; + } + const detail = outcome.status === "faulted" ? outcome.error.detail : void 0; + const cutOff = typeof detail === "object" && detail !== null && "reason" in detail && detail.reason === "restart_limit"; + if (record.kind === COMPACTION_TASK_KIND) { + const live2 = await tx.doc(LiveDoc, record.conversationId); + if (cutOff) + removeCompactionStatus(live2, record.id); + else + failCompactionStatus(live2, record.id, outcome.status === "faulted" ? outcome.error.message : outcome.reason); + return; + } + if (!RUN_TASK_KINDS.has(record.kind)) + return; + const live = await tx.doc(LiveDoc, record.conversationId); + if (live.run?.taskId !== record.id) + return; + if (!cutOff) + await convertPartial(tx, live, record.conversationId); + endRun(tx, live, record.id, outcome.status === "faulted" ? { status: "unanswered", reason: cutOff ? "aborted" : "faulted", detail: outcome.error.message } : { + status: "unanswered", + reason: "orphaned", + detail: outcome.reason, + settlement: { status: "unanswered", reason: outcome.reason } + }); +} + +// node_modules/@earendil-works/pi-durable/dist/harness/submissions.js +var Submissions = class { + #telemetry; + #session; + #now; + /** Read at each admission, on the Session line. */ + #queueModes; + /** Enable task scheduling; submitting or waiting asks for progress. */ + #resume; + #waiters = new Waiters(); + #closed = false; + constructor(session, now, queueModes, resume, telemetry = NOOP_TELEMETRY_CONTEXT) { + this.#telemetry = telemetry; + this.#session = session; + this.#now = now; + this.#queueModes = queueModes; + this.#resume = resume; + session.followCommits((publication) => this.#observe(publication)); + session.subscribeClose(() => { + this.#closed = true; + this.#waiters.rejectAll(closedError(this.#session)); + }); + } + /** Admit a submission in one commit; see `admitSubmission()`. */ + async submit(conversationId, draft, context) { + const { id } = await this.#traced(conversationId, draft, context, (span) => this.#session.commitWith((tx) => admitSubmission(tx, conversationId, draft, this.#now(), this.#queueModes(), span), withTelemetry(span, context))); + return new SubmissionHandle(id, this); + } + /** + * Admit a submission inside the caller's commit `tx`, with the rules of `submit()`. The admission is stored or lost + * with that commit. It must come before the commit's first table write, another admission's included. + */ + async admit(tx, conversationId, draft) { + if (!this.#session.ownsTransaction(tx)) + throw new Error("The transaction belongs to another Session"); + if (tx.hasTableWrite) + throw new ReadAfterWrite("submit(tx, draft)"); + const admission = await this.#traced(conversationId, draft, tx.context, (span) => admitSubmission(tx, conversationId, draft, this.#now(), this.#queueModes(), span)); + return admission.id; + } + /** Run one admission in its `pi.durable.submit` span, asking for progress first, as the run it starts needs it. */ + #traced(conversationId, draft, context, admit) { + this.#resume(); + const startsRun = draft.type !== "write"; + const parent = startsRun ? this.#telemetry : context.telemetry; + return createTypedSpanStarter(parent, [DURABLE_TELEMETRY_SCHEMA])("pi.durable.submit", { + "submission.type": draft.type, + "conversation.id": conversationId + }, async (span) => { + if (startsRun) + span.addLink({ context: context.telemetry }); + try { + const admission = await admit(span); + span.setAttributes({ "submission.id": admission.id, result: admission.status }); + return admission; + } catch (error) { + span.setStatus({ status: "error" }); + throw error; + } + }); + } + /** Handle for an existing submission, or `undefined`. */ + async get(id, context) { + const record = await this.#session.readOnLine((storage) => storage.submission(id, context)); + return record === void 0 ? void 0 : new SubmissionHandle(record.id, this); + } + async status(id, context) { + const record = await this.#session.readOnLine((storage) => storage.submission(id, context)); + if (record === void 0) + throw new Error(`Submission ${id} does not exist`); + return record; + } + async wait(id, context) { + this.#resume(); + const found = await this.#session.readOnLine(async (storage) => { + const record = await storage.submission(id, context); + if (record === void 0) + throw new Error(`Submission ${id} does not exist`); + if (isSettled(record)) + return { promise: Promise.resolve(record) }; + if (this.#closed) + throw closedError(this.#session); + return { promise: this.#waiters.add(id, context) }; + }); + return found.promise; + } + /** Withdraw a queued submission and remove its inbox item; placed inputs and settled submissions are reported. */ + abort(id, context, conversationId) { + return this.#session.commitWith(async (tx) => { + const record = await tx.submission(id); + if (record === void 0 || conversationId !== void 0 && record.conversationId !== conversationId) { + return "not_found"; + } + if (record.status === "queued") { + tx.settleSubmission(id, { status: "unanswered", reason: "aborted" }); + await removeInboxItem(tx, record.conversationId, id); + return "aborted"; + } + return record.status === "placed" ? "already_placed" : "settled"; + }, context); + } + #observe(publication) { + if (publication.kind === "memory") + return; + for (const change of publication.changes) { + if (change.type !== "submission" || !isSettled(change.value)) + continue; + this.#waiters.resolve(change.value.id, change.value); + } + } +}; +var SubmissionHandle = class { + id; + #submissions; + constructor(id, submissions) { + this.id = id; + this.#submissions = submissions; + } + status(context) { + return this.#submissions.status(this.id, context); + } + wait(context) { + return this.#submissions.wait(this.id, context); + } + async abort(context) { + const result = await this.#submissions.abort(this.id, context); + if (result === "not_found") + throw new Error(`Submission ${this.id} does not exist`); + return result; + } +}; +function isSettled(record) { + return record.status === "done" || record.status === "unanswered"; +} +function checkInputEntry(entry2) { + const { kind } = entry2; + if (typeof kind !== "string" || kind.length === 0 || kind.startsWith("pi.")) { + throw new TypeError(`An input entry's kind must be a non-empty string not starting with "pi.", got ${String(kind)}`); + } +} +async function admitSubmission(tx, conversationId, draft, now, queueModes, span) { + if (draft.type === "input" && draft.entry !== void 0) + checkInputEntry(draft.entry); + if (draft.requestId !== void 0) { + const existing = await tx.submissionByRequest(conversationId, draft.requestId); + if (existing !== void 0) { + if (existing.type !== draft.type) { + throw new Error(`Request ${draft.requestId} already identifies a submission of type ${existing.type}`); + } + return { id: existing.id, status: existing.status === "unanswered" ? "done" : existing.status }; + } + } + const fields = { conversationId, requestId: draft.requestId, trace: span?.propagation?.inject(span) }; + if (draft.type === "write" && isDisplayOnly(draft.entry) && !hasQueuedReset((await tx.doc(InboxDoc, conversationId)).items)) { + const entry3 = await tx.appendEntry(conversationId, draft.entry); + const write = { ...fields, type: "write", status: "done", entry: entry3.id }; + return { id: (await tx.createSubmission(write)).id, status: "done" }; + } + const live = await tx.doc(LiveDoc, conversationId); + const busy = live.run !== void 0; + if (draft.type === "input" && draft.whenBusy === "steer" && live.run !== void 0 && span?.propagation !== void 0) { + const run2 = await tx.task(live.run.taskId); + if (run2?.trace !== void 0) + span.addLink({ context: span.propagation.extract(run2.trace) }); + } + if (busy && draft.type === "input" && draft.whenBusy === "reject") + throw new ConversationBusy(conversationId); + if (!busy && draft.type === "input" && draft.whenIdle === "reject") + throw new ConversationIdle(conversationId); + if (draft.type === "continue") { + if (busy) + throw new ConversationBusy(conversationId); + const boundary2 = await prepareBoundary(tx, conversationId, queueModes, "final", now); + const { id: id2 } = await tx.createSubmission({ ...fields, type: "continue", status: "placed" }); + const { users, first } = await applyBoundary(tx, boundary2, "final", now); + await startRun(tx, conversationId, live, { inputs: [...users, id2], first: live.runFailure?.first ?? first }); + return { id: id2, status: "placed" }; + } + const boundary = busy ? void 0 : await prepareBoundary(tx, conversationId, queueModes, "final", now, draft.type === "input" ? inputDraft(draft.content, draft.entry, now) : void 0); + if (boundary === void 0 || boundary.inbox.items.length > 0) { + const { id: id2 } = await tx.createSubmission({ + ...fields, + type: draft.type, + status: "queued" + }); + const value2 = copyJson(draft.type === "write" ? draft.entry : draft.content, { + omitUndefinedProperties: true + }); + const items = (boundary?.inbox ?? await tx.doc(InboxDoc, conversationId)).items; + if (draft.type === "write") + items.push({ id: id2, mode: "write", entry: value2 }); + else { + const mode = draft.whenBusy === "steer" ? "steer" : "followUp"; + const content = value2; + const entry3 = draft.entry === void 0 ? {} : { entry: pickInputEntry(draft.entry) }; + items.push({ id: id2, mode, content, ...entry3 }); + } + if (boundary === void 0) + return { id: id2, status: "queued" }; + const { users, first } = await applyBoundary(tx, boundary, "final", now); + if (users.length > 0) + await startRun(tx, conversationId, live, { inputs: users, first }); + return { id: id2, status: draft.type === "write" ? "done" : users.at(-1) === id2 ? "placed" : "queued" }; + } + if (draft.type === "write") { + if (isStale(boundary, draft.entry)) { + const stale = { status: "unanswered", reason: "stale" }; + return { + id: (await tx.createSubmission({ ...fields, type: "write", ...stale })).id, + status: "done" + }; + } + const entry3 = await tx.appendEntry(conversationId, draft.entry); + const write = { ...fields, type: "write", status: "done", entry: entry3.id }; + return { id: (await tx.createSubmission(write)).id, status: "done" }; + } + for (const before of boundary.entries) + await tx.appendEntry(conversationId, before); + const entry2 = await appendInput(tx, conversationId, draft.content, draft.entry, now); + const input = { ...fields, type: "input", status: "placed", entry: entry2.id }; + const { id } = await tx.createSubmission(input); + await startRun(tx, conversationId, live, { inputs: [id], first: entry2.id }); + return { id, status: "placed" }; +} + +// node_modules/@earendil-works/pi-durable/dist/harness/compaction.js +var TOOL_RESULT_MAX_CHARS = 2e3; +var SUMMARY_PREFIX = "The conversation history before this point was compacted into the following summary:\n\n\n"; +var SUMMARY_SUFFIX = "\n"; +var SUMMARIZATION_SYSTEM_PROMPT = `You are a context summarization assistant. Your task is to read a conversation between a user and an AI assistant, then produce a structured summary following the exact format specified. + +Do NOT continue the conversation. Do NOT respond to any questions in the conversation. ONLY output the structured summary.`; +var SUMMARIZATION_PROMPT = `The messages above are a conversation to summarize. Create a structured context checkpoint summary that another LLM will use to continue the work. If the conversation starts with an earlier summary, preserve its information and fold the newer messages into it. + +Use this EXACT format: + +## Goal +[What is the user trying to accomplish? Can be multiple items if the session covers different tasks.] + +## Constraints & Preferences +- [Any constraints, preferences, or requirements mentioned by user] +- [Or "(none)" if none were mentioned] + +## Progress +### Done +- [x] [Completed tasks/changes] + +### In Progress +- [ ] [Current work] + +### Blocked +- [Issues preventing progress, if any] + +## Key Decisions +- **[Decision]**: [Brief rationale] + +## Next Steps +1. [Ordered list of what should happen next] + +## Critical Context +- [Any data, examples, or references needed to continue] +- [Or "(none)" if not applicable] + +Keep each section concise. Preserve exact file paths, function names, and error messages.`; +var CompactionTask = defineTask({ + name: "pi.compaction", + version: 1, + initial: () => ({ phase: "select" }), + phases: { + select: async (task, runtime, context) => { + if (cutOffTooOften(task, runtime)) + return failCutOff(runtime, task, context); + const { conversationId } = runtime; + const agent2 = await runtime.agent(context); + const settings = runtime.settings; + const ref = agent2.model; + const model2 = ref === void 0 ? void 0 : runtime.models.getModel(ref.provider, ref.modelId); + if (ref === void 0 || model2 === void 0) + return failNoModel2(runtime, ref, context); + const policy = compactionPolicy(settings, ref); + const view2 = await runtime.context(conversationId, context); + const cut = selectCut(view2, policy.keepRecentTokens); + if (cut === void 0) + return complete(runtime, context); + const firstKept = view2.entries[cut].id; + const { reason, instructions } = task.input; + const compaction = { + reason, + entries: view2.entries.slice(0, cut), + messages: summarizedMessages(view2, cut), + firstKept, + ...instructions === void 0 ? {} : { instructions } + }; + let decision; + await runtime.hooks.each("beforeCompact", async (hook2) => { + if (decision === void 0) + decision = await hook2(compaction, runtime, context); + }); + if (decision !== void 0 && "decline" in decision) + return complete(runtime, context); + if (decision !== void 0) + return place(runtime, firstKept, decision.summary, context); + const request2 = { + attempt: 1, + model: ref, + thinkingLevel: agent2.thinkingLevel, + streamOptions: settings.stream, + maxTokens: Math.min(Math.floor(0.8 * policy.reserveTokens), model2.maxTokens > 0 ? model2.maxTokens : Number.POSITIVE_INFINITY), + tail: view2.entries.reduce((tail, entry2) => entry2.id > tail ? entry2.id : tail, firstKept), + firstKept + }; + await runtime.commit(() => ({ status: "running", checkpoint: { phase: "summarize", ...request2 } }), context); + }, + summarize: async (task, runtime, context) => { + if (cutOffTooOften(task, runtime)) + return failCutOff(runtime, task, context); + const { phase: _, ...request2 } = task.state.checkpoint; + const { model: ref, thinkingLevel, streamOptions, maxTokens, tail, firstKept, attempt } = request2; + const model2 = runtime.models.getModel(ref.provider, ref.modelId); + if (model2 === void 0) + return failNoModel2(runtime, ref, context); + const view2 = await runtime.context(runtime.conversationId, context, { at: tail }); + const cut = view2.entries.findIndex((entry2) => entry2.id === firstKept); + const now = runtime.now(); + const messages = [ + { role: "system", content: SUMMARIZATION_SYSTEM_PROMPT, timestamp: now }, + { + role: "user", + content: [{ type: "text", text: summaryPrompt(summarizedMessages(view2, cut), task.input.instructions) }], + timestamp: now + } + ]; + const { deferred: _deferred, ...forwarded } = streamOptions; + const options = { + ...forwarded, + cacheRetention: "none", + maxTokens, + signal: runtime.signal, + telemetry: context.telemetry, + sessionId: await ensureProviderSessionId(runtime, context), + ...thinkingLevel === "off" ? {} : { reasoning: thinkingLevel } + }; + const message = await runtime.models.completeSimple(model2, { messages }, options); + runtime.signal.throwIfAborted(); + const summary = summaryText(message); + const policy = runtime.settings.retry; + const retry = message.stopReason === "error" && isRetryableAssistantError(message) && policy.enabled && attempt <= policy.maxRetries; + const until = retry ? runtime.now() + retryDelayMs(policy, attempt) : 0; + await runtime.commit(async (tx, current) => { + await recordUsage(tx, runtime.conversationId, "models", `${message.provider}/${message.model}`, message.usage); + const live = await tx.doc(LiveDoc, runtime.conversationId); + if (summary !== void 0) + return placeSummary(tx, runtime, current, live, firstKept, summary); + if (retry) { + const status = compactionStatus(live, runtime.taskId); + if (status !== void 0) + status.retry = { at: until, error: message.errorMessage ?? "" }; + const checkpoint = { phase: "retry", ...request2, until }; + return { status: "sleeping", checkpoint, until }; + } + const text = summaryFailure(message); + failCompactionStatus(live, runtime.taskId, text); + return { + status: "terminal", + outcome: { status: "failed", error: { message: text, detail: { reason: "model_error" } } } + }; + }, context); + }, + /** After the backoff's sleep; a run before `until` (an early wake, or a record of an earlier version) sleeps again. */ + retry: async (task, runtime, context) => { + const { phase: _, until, ...request2 } = task.state.checkpoint; + if (cutOffTooOften(task, runtime)) + return failCutOff(runtime, task, context); + if (until > runtime.now()) + return runtime.sleepUntil(until, task.state.checkpoint, context); + const attempt = request2.attempt + 1; + await runtime.commit(async (tx) => { + const status = compactionStatus(await tx.doc(LiveDoc, runtime.conversationId), runtime.taskId); + if (status !== void 0) { + status.attempt = attempt; + delete status.retry; + } + return { status: "running", checkpoint: { phase: "summarize", ...request2, attempt } }; + }, context); + } + }, + abort: async (_task, runtime, context) => { + await runtime.commit(async (tx) => { + removeCompactionStatus(await tx.doc(LiveDoc, runtime.conversationId), runtime.taskId); + return { status: "terminal", outcome: { status: "aborted" } }; + }, context); + } +}); +async function createCompaction(tx, conversationId, input, owner) { + const ownership = owner === void 0 ? { kind: "conversation" } : { kind: "task", taskId: owner }; + const background = owner === void 0 && input.reason !== "manual"; + const taskId = await tx.createTask(CompactionTask, input, { ownership, conversationId, background }); + const status = { taskId, trigger: input.reason, blocking: owner !== void 0, attempt: 1 }; + addCompactionStatus(await tx.doc(LiveDoc, conversationId), status); + return taskId; +} +function compactionPolicy(settings, model2) { + const { modelOverrides, ...policy } = settings.compaction; + const override = modelOverrides?.[`${model2.provider}/${model2.modelId}`]; + return { + enabled: override?.enabled ?? policy.enabled, + reserveTokens: override?.reserveTokens ?? policy.reserveTokens, + keepRecentTokens: override?.keepRecentTokens ?? policy.keepRecentTokens, + backgroundTokens: override?.backgroundTokens ?? policy.backgroundTokens + }; +} +function selectCut(view2, keepRecentTokens) { + const { contributions } = view2; + const start = view2.head === void 0 ? 0 : 1; + const candidates = []; + for (let index = start; index < contributions.length; index++) { + if (isCandidate(contributions, index)) + candidates.push(index); + } + let kept = 0; + let cut; + for (let index = contributions.length - 1; index >= start; index--) { + for (const message of contributions[index]) + kept += estimateMessageTokens(message); + if (kept < keepRecentTokens) + continue; + cut = candidates.find((candidate) => candidate >= index) ?? candidates.at(-1); + break; + } + if (cut === void 0) + return void 0; + for (let index = start; index < cut; index++) + if (contributions[index].length > 0) + return cut; + return void 0; +} +function isCandidate(contributions, index) { + const first = contributions[index][0]; + if (first?.role === "assistant") + return true; + if (first?.role !== "user") + return false; + let calls = /* @__PURE__ */ new Set(); + for (let before = index - 1; before >= 0; before--) { + const assistant = contributions[before].findLast((message) => message.role === "assistant"); + if (assistant === void 0) + continue; + calls = new Set(assistant.content.flatMap((content) => content.type === "toolCall" ? [content.id] : [])); + break; + } + if (calls.size === 0) + return true; + for (let after = index; after < contributions.length; after++) { + for (const [position, message] of contributions[after].entries()) { + if (message.role === "assistant" && (after > index || position > 0)) + return true; + if (message.role === "toolResult" && calls.has(message.toolCallId)) + return false; + } + } + return true; +} +function summarizedMessages(view2, cut) { + return orderToolResults(view2.contributions.slice(0, cut).flat()); +} +function estimateContext(view2, extra) { + let measured; + const after = view2.head?.id ?? Number.NEGATIVE_INFINITY; + for (let index = view2.entries.length - 1; index >= 0 && measured === void 0; index--) { + if (view2.entries[index].id <= after) + continue; + measured = view2.contributions[index].findLast((message) => message.role === "assistant" && calculateContextTokens(message.usage) > 0); + } + const from = measured === void 0 ? 0 : view2.messages.lastIndexOf(measured) + 1; + let tokens = measured === void 0 ? 0 : calculateContextTokens(measured.usage); + for (const message of view2.messages.slice(from)) + tokens += estimateMessageTokens(message); + for (const message of extra) + tokens += estimateMessageTokens(message); + return tokens; +} +function summaryText(message) { + if (message.stopReason !== "stop" || message.content.some((content) => content.type === "toolCall")) + return void 0; + const text = message.content.flatMap((content) => content.type === "text" ? [content.text] : []).join("\n").trim(); + return text.length === 0 ? void 0 : text; +} +function summaryFailure(message) { + if (message.stopReason === "error" || message.stopReason === "aborted") { + return `Summarization failed: ${message.errorMessage ?? message.stopReason}`; + } + if (message.stopReason === "length") + return "Summarization hit the token limit; the summary is incomplete"; + if (message.content.some((content) => content.type === "toolCall")) + return "Summarization attempted to call a tool"; + return "Summarization produced no text"; +} +function summaryPrompt(messages, instructions) { + const focus = instructions === void 0 ? "" : ` + +Additional focus: ${instructions}`; + return ` +${serializeConversation(messages)} + + +${SUMMARIZATION_PROMPT}${focus}`; +} +function serializeConversation(messages) { + const parts = []; + for (const message of messages) { + if (message.role === "user") { + const text = contentText2(message.content); + if (text.length > 0) + parts.push(`[User]: ${text}`); + } else if (message.role === "assistant") { + const text = message.content.flatMap((content) => content.type === "text" ? [content.text] : []); + const calls = message.content.flatMap((content) => content.type === "toolCall" ? [ + `${content.name}(${Object.entries(content.arguments).map(([key, value2]) => `${key}=${JSON.stringify(value2)}`).join(", ")})` + ] : []); + if (text.length > 0) + parts.push(`[Assistant]: ${text.join("\n")}`); + if (calls.length > 0) + parts.push(`[Assistant tool calls]: ${calls.join("; ")}`); + } else if (message.role === "toolResult") { + const text = contentText2(message.content); + if (text.length > 0) + parts.push(`[Tool result]: ${truncate(text, TOOL_RESULT_MAX_CHARS)}`); + } + } + return parts.join("\n\n"); +} +function contentText2(content) { + if (typeof content === "string") + return content; + return content.flatMap((block) => block.type === "text" && block.text !== void 0 ? [block.text] : []).join("\n"); +} +function truncate(text, maxChars) { + if (text.length <= maxChars) + return text; + return `${text.slice(0, maxChars)} + +[... ${text.length - maxChars} more characters truncated]`; +} +async function place(runtime, firstKept, summary, context) { + await runtime.commit(async (tx, current) => placeSummary(tx, runtime, current, await tx.doc(LiveDoc, runtime.conversationId), firstKept, summary), context); +} +async function placeSummary(tx, runtime, current, live, firstKept, summary) { + removeCompactionStatus(live, runtime.taskId); + delete live.compactionFailure; + const text = `${SUMMARY_PREFIX}${summary}${SUMMARY_SUFFIX}`; + const entry2 = { + kind: CompactionEntry.kind, + head: firstKept, + model: [{ role: "user", content: [{ type: "text", text }], timestamp: runtime.now() }], + data: { reason: current.input.reason } + }; + const result = current.owner === void 0 ? { + submissionId: (await admitSubmission(tx, runtime.conversationId, { type: "write", requestId: `compaction:${runtime.taskId}`, entry: entry2 }, runtime.now(), runtime.settings)).id + } : { entryId: (await tx.appendEntry(runtime.conversationId, entry2)).id }; + return { status: "terminal", outcome: { status: "completed", result } }; +} +async function complete(runtime, context) { + await runtime.commit(async (tx) => { + removeCompactionStatus(await tx.doc(LiveDoc, runtime.conversationId), runtime.taskId); + return { status: "terminal", outcome: { status: "completed", result: {} } }; + }, context); +} +async function failCutOff(runtime, task, context) { + const message = `Compaction was interrupted ${cutOffCount(task)}`; + await runtime.commit(async (tx) => { + failCompactionStatus(await tx.doc(LiveDoc, runtime.conversationId), runtime.taskId, message); + return { + status: "terminal", + outcome: { status: "failed", error: { message, detail: { reason: "restart_limit" } } } + }; + }, context); +} +async function failNoModel2(runtime, ref, context) { + const message = ref === void 0 ? "No model is configured" : `Model ${ref.provider}/${ref.modelId} is not available`; + await runtime.commit(async (tx) => { + failCompactionStatus(await tx.doc(LiveDoc, runtime.conversationId), runtime.taskId, message); + return { status: "terminal", outcome: { status: "failed", error: { message, detail: { reason: "no_model" } } } }; + }, context); +} + +// node_modules/@earendil-works/pi-durable/dist/harness/define.js +function defineExtension(extension) { + return extension; +} +function defineTool(tool) { + return tool; +} + +// node_modules/@earendil-works/pi-durable/dist/harness/registry.js +var SECTION_KEY = /^[a-z][a-z0-9_-]*$/; +var BUILTIN_TASKS = [GenerationTask, ToolTask, CompactionTask]; +var RegistryState = class { + #extensions; + #byName; + #tasks; + constructor(extensions) { + this.#extensions = extensions; + this.#byName = new Map(extensions.map((extension) => [extension.name, extension])); + const tasks = /* @__PURE__ */ new Map(); + for (const task of BUILTIN_TASKS) + tasks.set(task.definition.name, task); + for (const extension of extensions) { + for (const task of extension.tasks ?? []) { + const name = task.definition.name; + if (tasks.has(name)) + throw new Error(`Task ${name} of extension ${extension.name} is already installed`); + tasks.set(name, task); + } + } + this.#tasks = tasks; + } + installed() { + return this.#extensions; + } + extension(name) { + return this.#byName.get(name); + } + tools() { + return this.#extensions.flatMap((extension) => (extension.tools ?? []).map((tool) => ({ extension, tool }))); + } + sections() { + return this.#extensions.flatMap((extension) => (extension.sections ?? []).map((section2) => ({ extension, section: section2 }))); + } + tasks() { + return [...this.#tasks.values()]; + } + task(name) { + return this.#tasks.get(name); + } +}; +var RegistryImpl = class { + #current = new RegistryState([]); + #listeners = /* @__PURE__ */ new Set(); + snapshot() { + return this.#current; + } + subscribe(listener) { + this.#listeners.add(listener); + return () => this.#listeners.delete(listener); + } + install(extension) { + validateExtension(extension); + const current = this.#current.installed(); + const index = current.findIndex((installed) => installed.name === extension.name); + const next = index < 0 ? [...current, extension] : current.map((installed, at) => at === index ? extension : installed); + this.#publish(next); + } + uninstall(extension) { + const current = this.#current.installed(); + if (!current.some((installed) => installed.name === extension.name)) + return; + this.#publish(current.filter((installed) => installed.name !== extension.name)); + } + /** Build and validate the next state, which throws on a task name collision, then publish it synchronously. */ + #publish(extensions) { + this.#current = new RegistryState(extensions); + for (const listener of [...this.#listeners]) + listener(); + } +}; +function validateExtension(extension) { + const tools = /* @__PURE__ */ new Set(); + for (const tool of extension.tools ?? []) { + if (tools.has(tool.name)) + throw new Error(`Extension ${extension.name} has two tools named ${tool.name}`); + tools.add(tool.name); + } + const sections = /* @__PURE__ */ new Set(); + for (const { key } of extension.sections ?? []) { + if (!SECTION_KEY.test(key)) + throw new TypeError(`Section key ${JSON.stringify(key)} must match ${SECTION_KEY}`); + if (key === INSTRUCTIONS_KEY) + throw new Error(`Section key ${key} is reserved for the agent's instructions`); + if (sections.has(key)) + throw new Error(`Extension ${extension.name} has two sections with key ${key}`); + sections.add(key); + } +} +function createRegistry() { + return new RegistryImpl(); +} + +// node_modules/@earendil-works/pi-durable/dist/harness/task-graph.js +var LIVE_STATUSES2 = ["pending", "running", "waiting", "sleeping", "completing"]; +var SCAN_PAGE_SIZE4 = 256; +var TaskGraphView = class { + #session; + #blocking; + #mount; + #closed = false; + constructor(session, blocking) { + this.#session = session; + this.#blocking = blocking; + session.followCommits((publication, context) => { + if (this.#mount === void 0 || publication.kind === "memory") + return; + advance(this.#mount, publication, context, blocking.blocked, (error) => session.report(error)); + }); + blocking.subscribeBlocked(() => this.#refreshBlocked()); + session.subscribeClose(() => { + this.#closed = true; + const ends = closeEnds(this.#session.failure); + for (const observer of [...this.#mount?.observers ?? []]) { + contained(() => observer.end(closeEndOf(observer, ends)), (error) => session.report(error)); + } + this.#mount = void 0; + }); + } + /** + * Attach a synchronous observer of the graph (`Harness.observeTaskGraph()`), behind `guardObserver()`; `context` + * governs only the attachment. + */ + async observe(observer, context) { + let value2; + const attached = await this.#attach((current, release) => { + value2 = current; + return guardObserver(observer, release, (error) => this.#session.report(error)); + }, context); + const signal = context.abortSignal; + if (signal?.aborted) { + attached.detach(); + throw signal.reason; + } + return observation(value2, attached.observer); + } + /** A serialized watch of the graph, an observer of its mount; aborting `context` ends it `aborted`. */ + async watch(context) { + const { observer } = await this.#attach((value2, release) => new Watch(value2, release, (error) => this.#session.report(error)), context); + const signal = context.abortSignal; + if (signal?.aborted) { + observer.abort(); + throw signal.reason; + } + if (signal !== void 0) + observer.observeAbort(signal); + return observer; + } + /** Register an observer created from the current revision, atomically on the Session line. */ + #attach(create, context) { + return this.#session.readOnLine(async (storage) => { + const mount = this.#mount ?? { + ...await this.#build(storage, context), + observers: /* @__PURE__ */ new Set() + }; + const detach = () => { + mount.observers.delete(observer); + if (mount.observers.size === 0 && this.#mount === mount) + this.#mount = void 0; + }; + const observer = create(mount.value, detach); + if (this.#closed) + throw closedError(this.#session); + context.abortSignal?.throwIfAborted(); + this.#mount = mount; + mount.observers.add(observer); + return { observer, detach }; + }); + } + /** On the Session line: derive every node's blocked reason again, and publish the ones that changed. */ + #refreshBlocked() { + if (this.#mount === void 0 || this.#closed) + return; + this.#session.readOnLine(async () => { + const mount = this.#mount; + if (mount === void 0) + return; + const ops3 = []; + for (const [key, record] of mount.records) { + const blocked = this.#blocking.blocked(record); + if (blocked === mount.value.tasks[key]?.blocked) + continue; + ops3.push(blocked === void 0 ? ["d", ["tasks", key, "blocked"]] : ["s", ["tasks", key, "blocked"], blocked]); + } + publish(mount, ops3, BACKGROUND_CONTEXT, (error) => this.#session.report(error)); + }).catch(() => { + }); + } + /** Read the graph on the line, through the line's `storage`. */ + async #build(storage, context) { + const tasks = {}; + const live = /* @__PURE__ */ new Map(); + const records2 = []; + for (const status of LIVE_STATUSES2) { + records2.push(...await scanAll((cursor) => storage.scanTasks({ status }, SCAN_PAGE_SIZE4, cursor, context))); + } + records2.sort((a, b) => a.id - b.id); + for (const record of records2) { + const owned2 = await scanAll((cursor) => storage.scanConversations({ ownerTaskId: record.id }, SCAN_PAGE_SIZE4, cursor, context)); + const conversations = owned2.map((conversation) => conversation.id).sort((a, b) => a - b); + const subject = blockedSubject(record); + tasks[String(record.id)] = nodeOf2(record, conversations, this.#blocking.blocked(subject)); + live.set(String(record.id), subject); + } + return { value: { tasks }, records: live }; + } +}; +function advance(mount, publication, context, blocked, report2) { + const ops3 = []; + const changed = /* @__PURE__ */ new Map(); + const node = (key) => changed.has(key) ? changed.get(key) : mount.value.tasks[key]; + for (const change of publication.changes) { + if (change.type !== "task") + continue; + const record = change.value; + const key = String(record.id); + const previous = node(key); + if (record.state.status === "terminal") { + mount.records.delete(key); + if (previous === void 0) + continue; + ops3.push(["d", ["tasks", key]]); + changed.set(key, void 0); + continue; + } + const subject = blockedSubject(record); + mount.records.set(key, subject); + const next = nodeOf2(record, previous?.conversations ?? [], blocked(subject)); + if (previous !== void 0 && JSON.stringify(next) === JSON.stringify(previous)) + continue; + ops3.push(["s", ["tasks", key], next]); + changed.set(key, next); + } + const created = /* @__PURE__ */ new Map(); + for (const change of publication.changes) { + if (change.type !== "conversation" || change.value.owner === void 0) + continue; + const key = String(change.value.owner.taskId); + if (node(key) !== void 0) + created.set(key, [...created.get(key) ?? [], change.value.id]); + } + for (const [key, ids] of created) { + const conversations = [...node(key).conversations, ...ids].sort((a, b) => a - b); + ops3.push(["s", ["tasks", key, "conversations"], conversations]); + } + publish(mount, ops3, context, report2); +} +function publish(mount, ops3, context, report2) { + if (ops3.length === 0) + return; + mount.value = applyImmutable(mount.value, ops3); + const frameContext = withoutAbortSignal(context); + const call = { kind: "value", value: mount.value, ops: ops3, retired: false }; + for (const observer of [...mount.observers]) + contained(() => observer.advance(call, frameContext), report2); +} +function nodeOf2(record, conversations, blocked) { + return { + id: record.id, + kind: record.kind, + conversationId: record.conversationId, + ...record.owner === void 0 ? {} : { owner: record.owner }, + background: record.background, + abortRequested: record.abortRequested, + state: stateOf(record), + conversations, + ...blocked === void 0 ? {} : { blocked } + }; +} +function stateOf(record) { + const state = record.state; + switch (state.status) { + case "pending": + case "running": + return { status: state.status, phase: phaseOf(state.checkpoint) }; + case "waiting": + return { status: "waiting", phase: phaseOf(state.checkpoint), on: [...state.on], policy: state.policy }; + case "sleeping": + return { status: "sleeping", phase: phaseOf(state.checkpoint), until: state.until }; + // Terminal records never reach here: they leave the graph. + case "completing": + case "terminal": + return { status: "completing", outcome: state.outcome.status }; + } +} +function phaseOf(checkpoint) { + return checkpoint.phase; +} + +// node_modules/@earendil-works/pi-durable/dist/harness/view.js +var DEFAULT_WINDOW_MIN = 50; +var MOUNTED = [ + AgentDoc, + LiveDoc, + InboxDoc, + ProviderDoc, + UsageDoc +]; +var MOUNTED_KINDS = new Set(MOUNTED.map((token) => token.definition.kind)); +var SCAN_PAGE_SIZE5 = 256; +var VIEWS = /* @__PURE__ */ new WeakMap(); +function resolveWindow(window) { + const min = window?.min ?? DEFAULT_WINDOW_MIN; + if (!Number.isSafeInteger(min) || min < 1) + throw new RangeError(`Window min must be an integer >= 1, got ${min}`); + const max = window?.max ?? 2 * min; + if (!Number.isSafeInteger(max) || max < min) { + throw new RangeError(`Window max must be an integer >= min (${min}), got ${max}`); + } + const from = window?.from; + if (from !== void 0 && !Number.isSafeInteger(from)) { + throw new RangeError(`Window from must be an entry ID, got ${from}`); + } + return { from, min, max }; +} +var ConversationViews = class { + #session; + #mounts = /* @__PURE__ */ new Map(); + #closed = false; + constructor(session) { + this.#session = session; + VIEWS.set(session, this); + session.followCommits((publication, context) => { + for (const [id, mount] of this.#mounts) + advance2(id, mount, publication, context, (error) => this.report(error)); + }); + session.subscribeClose(() => { + this.#closed = true; + const ends = closeEnds(this.#session.failure); + for (const mount of this.#mounts.values()) + for (const windowed of [...mount.observers]) { + contained(() => windowed.observer.end(closeEndOf(windowed.observer, ends)), (error) => this.report(error)); + } + this.#mounts.clear(); + }); + } + /** Report an observer's listener error through the Harness. */ + report(error) { + this.#session.report(error); + } + /** + * Attach a synchronous observer of the view (`Harness.observeConversation()`), behind `guardObserver()`; `context` + * governs only the attachment. + */ + async observe(id, observer, context, window) { + let value2; + const attached = await this.attach(id, window, (current, release) => { + value2 = current; + return guardObserver(observer, release, (error) => this.report(error)); + }, context); + const signal = context.abortSignal; + if (signal?.aborted) { + attached.detach(); + throw signal.reason; + } + return observation(value2, attached.observer); + } + /** A serialized watch of the view, an observer of its mount; aborting `context` ends it `aborted`. */ + async watch(id, context, window) { + const { observer } = await this.attach(id, window, (value2, release) => new Watch(value2, release, (error) => this.#session.report(error)), context); + const signal = context.abortSignal; + if (signal?.aborted) { + observer.abort(); + throw signal.reason; + } + if (signal !== void 0) + observer.observeAbort(signal); + return observer; + } + /** + * Register an observer created from its window's first revision, atomically on the Session line: it sees every later + * publication and nothing earlier. `release` drops it, and the mount with its last observer. + */ + async attach(id, window, create, context) { + const bounds = resolveWindow(window); + return this.#session.readOnLine(async (storage) => { + const mount = this.#mounts.get(id) ?? await this.#build(storage, id, context); + const { entries: entries2, before } = await loadWindow(storage, id, bounds, context); + const value2 = view(mount.conversation, entries2, before, mount.docs); + let windowed; + const detach = () => { + if (windowed !== void 0) + mount.observers.delete(windowed); + if (mount.observers.size === 0 && this.#mounts.get(id) === mount) + this.#mounts.delete(id); + }; + const observer = create(value2, detach); + if (this.#closed) + throw closedError(this.#session); + context.abortSignal?.throwIfAborted(); + windowed = { min: bounds.min, max: bounds.max, value: value2, observer }; + this.#mounts.set(id, mount); + mount.observers.add(windowed); + return { observer, detach }; + }); + } + /** Hydrate a mount on the line, through the line's `storage`. */ + async #build(storage, id, context) { + const conversation = await storage.conversation(id, context); + if (conversation === void 0) + throw new Error(`Conversation ${id} does not exist`); + const docs = {}; + const incarnations = /* @__PURE__ */ new Map(); + for (const token of MOUNTED) { + const loaded = await this.#session.conversationDocumentOnLine(token, id, context); + if (loaded === void 0) + continue; + docs[token.definition.kind] = loaded.value; + incarnations.set(token.definition.kind, { id: loaded.record.id, version: loaded.version }); + } + return { conversation, docs, incarnations, observers: /* @__PURE__ */ new Set() }; + } +}; +function view(conversation, entries2, before, docs) { + return before === void 0 ? { conversation, entries: entries2, docs } : { conversation, entries: entries2, before, docs }; +} +async function loadWindow(storage, conversationId, bounds, context) { + const { from, min, max } = bounds; + if (from !== void 0) { + const since = await scanUpTo(storage, { conversationId, minEntryId: from, order: "ascending" }, max + 1, context); + if (since.length <= max) { + const query = { conversationId, maxEntryId: idFromNumber(from - 1) }; + const previous = (await storage.scanEntries(query, 1, void 0, context)).items[0]; + return { entries: since, before: previous?.id }; + } + } + const newest = await scanUpTo(storage, { conversationId }, min + 1, context); + const before = newest.length > min ? newest.pop().id : void 0; + return { entries: newest.reverse(), before }; +} +async function scanUpTo(storage, query, limit, context) { + const entries2 = []; + let cursor; + do { + const page2 = await storage.scanEntries(query, Math.min(SCAN_PAGE_SIZE5, limit - entries2.length), cursor, context); + entries2.push(...page2.items); + cursor = page2.next; + } while (cursor !== void 0 && entries2.length < limit); + return entries2; +} +function advance2(id, mount, publication, context, report2) { + const docOps = []; + const appended = []; + for (const change of publication.changes) { + if (change.type === "entry" && change.value.conversationId === id) { + appended.push(change.value); + continue; + } + if (change.type !== "document" || change.conversationId !== id) + continue; + const kind = change.record.kind; + if (!MOUNTED_KINDS.has(kind) || change.record.key !== void 0) + continue; + const path2 = ["docs", kind]; + const mounted = mount.incarnations.get(kind); + if (change.kind === "retire") { + if (mounted?.id !== change.record.id) + continue; + mount.incarnations.delete(kind); + docOps.push(["d", path2]); + } else if (mounted?.id === change.record.id && mounted.version === change.version) { + for (const op2 of change.ops) + docOps.push(prefixed(op2, path2)); + } else { + mount.incarnations.set(kind, { id: change.record.id, version: change.version }); + docOps.push(["s", path2, change.value]); + } + } + if (docOps.length === 0 && appended.length === 0) + return; + if (docOps.length > 0) + mount.docs = applyImmutable({ docs: mount.docs }, docOps).docs; + const frameContext = withoutAbortSignal(context); + const inserted = appended; + for (const windowed of [...mount.observers]) { + const previous = windowed.value; + let entries2 = previous.entries; + let before = previous.before; + let ops3 = docOps; + if (appended.length > 0) { + ops3 = [...docOps, ["p", ["entries"], entries2.length, 0, inserted]]; + entries2 = entries2.concat(appended); + if (entries2.length > windowed.max) { + const drop = entries2.length - windowed.min; + before = entries2[drop - 1].id; + entries2 = entries2.slice(drop); + ops3.push(["p", ["entries"], 0, drop, []], ["s", ["before"], before]); + } + } + windowed.value = view(mount.conversation, entries2, before, mount.docs); + const call = { kind: "value", value: windowed.value, ops: ops3, retired: false }; + contained(() => windowed.observer.advance(call, frameContext), report2); + } +} +function prefixed(op2, prefix) { + const at = (path2) => [...prefix, ...path2]; + switch (op2[0]) { + case "r": + return ["s", prefix, op2[1]]; + case "p": + return ["p", at(op2[1]), op2[2], op2[3], op2[4]]; + case "m": + return ["m", at(op2[1]), op2[2]]; + case "s": + return ["s", at(op2[1]), op2[2]]; + case "d": + return ["d", at(op2[1])]; + case "a": + return ["a", at(op2[1]), op2[2]]; + case "t": + return ["t", at(op2[1]), op2[2]]; + } +} + +// node_modules/@earendil-works/pi-durable/dist/harness/harness.js +var SCAN_PAGE_SIZE6 = 256; +var SCHEDULER_INDEX_SIZES = /* @__PURE__ */ Symbol.for("pi-durable.schedulerIndexSizes"); +var SCHEDULER_IDLE_STATE = /* @__PURE__ */ Symbol.for("pi-durable.schedulerIdleState"); +var ConversationImpl = class { + id; + #host; + constructor(id, host) { + this.id = id; + this.#host = host; + } + agent(context) { + return this.#host.harness.resolveAgent(this.id, void 0, context); + } + configure(change, context) { + return this.#host.harness.commitWith((tx) => configure(tx, this.id, change), context); + } + submit(first, second) { + if ("type" in first) + return this.#host.submissions.submit(this.id, first, second); + return this.#host.submissions.admit(first, this.id, second); + } + async compact(instructions, context) { + this.#host.tasks.resume(); + const input = { reason: "manual", ...instructions === void 0 ? {} : { instructions } }; + return this.#host.harness.commitWith((tx) => createCompaction(tx, this.id, input), context); + } + async reset(handoff, context) { + const model2 = handoff === void 0 ? {} : { model: [{ role: "user", content: handoff, timestamp: this.#host.now() }] }; + const entry2 = { kind: ResetEntry.kind, head: "self", ...model2 }; + await this.#host.submissions.submit(this.id, { type: "write", entry: entry2 }, context); + } + commit(change, context, options) { + return this.#host.harness.commitWith(change, context, { conversationId: this.id }, options); + } + context(context, options) { + return readContext(this.#host.harness, this.#host.storage, this.id, context, options?.at); + } + entries(query, limit, cursor, context) { + const scan = conversationEntries(this.id, query); + return this.#host.harness.readOnLine((storage) => storage.scanEntries(scan, limit, cursor, context)); + } + fork(at, options, context) { + return this.#host.create({ kind: "fork", parentId: this.id, at, ownership: options.ownership }, options, context); + } + async abort(context, options) { + this.#host.tasks.resume(); + return this.#host.tasks.abortConversation(this.id, options?.background === true, context); + } + async waitForIdle(context) { + this.#host.tasks.resume(); + return this.#host.tasks.waitForIdle(this.id, context); + } + watch(context, window) { + return this.#host.views.watch(this.id, context, window); + } +}; +var DEFAULT_HARNESS_IDENTITY = { attribution: PI_ATTRIBUTION }; +function currentModels(models2, identity) { + const merged = mergeClientIdentity(DEFAULT_HARNESS_IDENTITY, identity) ?? DEFAULT_HARNESS_IDENTITY; + if (typeof models2 === "function") + return () => withClientIdentity(models2(), merged); + const wrapped = withClientIdentity(models2, merged); + return () => wrapped; +} +function currentSettings(settings) { + return typeof settings === "function" ? settings : () => settings; +} +var HarnessImpl = class extends SessionImpl { + #options; + #hostSettings; + #report; + #host; + #tasks; + #submissions; + #taskGraph; + constructor(rawStorage, options, context) { + const report2 = safeReport(options.onReport, options.telemetry ?? NOOP_TELEMETRY_CONTEXT); + const now = safeNow(options.now, report2); + const hostSettings = currentSettings(options.settings); + super(rawStorage, now, () => hostSettings()?.documentRetentionMs, options.setTimer, options.readOnly === true, () => hostSettings()?.progress?.persistWithinMs); + const storage = this.storage; + this.#options = options; + this.#report = report2; + this.#hostSettings = hostSettings; + const settings = () => resolveSettings(hostSettings()); + this.#tasks = new TaskScheduler({ + telemetry: options.telemetry, + session: this, + storage, + registry: options.registry, + models: currentModels(options.models, options.identity), + agent: (id, snapshot, callContext) => this.resolveAgent(id, snapshot, callContext), + settings, + env: (id, callContext) => this.buildEnv(id, callContext), + now, + onWake: options.onWake, + timers: hostTimers(options.setTimer, (error) => report2(error, { kind: "host_error", callback: "setTimer" })), + writeAtClose: (writes) => this.writeAtClose(writes), + report: this.#report, + limitsRestarts: (kind) => LIMITS_RESTARTS.has(kind), + settleOutcome: settleSchedulerOutcome, + withdrawInputs: withdrawQueuedInputs, + conversation: async (id, binding, callContext) => { + const record = await this.readOnLine((line) => line.conversation(id, callContext)); + return record === void 0 ? void 0 : boundConversation(id, binding, this.#submissions, this.#tasks); + }, + context: withoutAbortSignal(context), + readOnly: options.readOnly === true + }); + this.#submissions = new Submissions(this, now, settings, () => this.#tasks.resume(), options.telemetry); + this.#taskGraph = new TaskGraphView(this, { + blocked: (record) => this.#tasks.blockedReason(record), + subscribeBlocked: (listener) => this.#tasks.subscribeBlocked(listener) + }); + this.#host = { + harness: this, + storage, + tasks: this.#tasks, + submissions: this.#submissions, + views: new ConversationViews(this), + now, + create: (target, createOptions, context2) => this.#create(target, createOptions, context2) + }; + } + async beforeInput(tx, conversationId, input, context) { + const agent2 = resolveAgent(await tx.doc(AgentDoc, conversationId), this.#options.registry.snapshot(), resolveSettings(this.#hostSettings()), (error, extension) => this.#report(error, { kind: "extension_error", conversationId, extension }, context)); + const entries2 = []; + for (const { handlers } of agentHooks(agent2, "pi.generation")) { + const result = await handlers.beforeInput?.(input, { conversationId, tx }, context); + entries2.push(...result?.entries ?? []); + } + return entries2; + } + /** Resolve a conversation's committed `pi.agent` against `snapshot`, or the current one, and the current settings. */ + async resolveAgent(id, snapshot, context) { + const registry2 = snapshot ?? this.#options.registry.snapshot(); + const state = await this.snapshot(AgentDoc, id, context); + return resolveAgent(state, registry2, resolveSettings(this.#hostSettings()), (error, extension) => this.#report(error, { kind: "extension_error", conversationId: id, extension }, context)); + } + /** Build a conversation's environment from its current `cwd`; `undefined` without an `env` option. */ + async buildEnv(id, context) { + const build = this.#options.env; + if (build === void 0) + return void 0; + const cwd = (await this.snapshot(AgentDoc, id, context))?.cwd; + return build({ conversationId: id, ...cwd === void 0 ? {} : { cwd }, read: this }, context); + } + /** Reconcile surviving `running` tasks to `pending`; part of open. */ + openTasks(context) { + return this.#tasks.open(context); + } + resume() { + this.assertUsable(); + this.#tasks.resume(); + } + hold() { + this.#tasks.hold(); + } + release() { + this.#tasks.release(); + } + wake() { + this.assertUsable(); + return this.#tasks.wake(); + } + getTask(id, context) { + return this.readOnLine((storage) => storage.task(id, context)); + } + /** Internal, for tests: sizes of the scheduler's ownership indexes. */ + get [SCHEDULER_INDEX_SIZES]() { + return this.#tasks.indexSizes; + } + /** Internal, for tests: the kept contexts' idle state and its disagreements with a full recompute. */ + get [SCHEDULER_IDLE_STATE]() { + return this.#tasks.idleState; + } + inspect(context) { + return this.readOnLine(async (storage) => { + const { scheduling, tasks } = await this.#tasks.inspect(this.#options.registry.snapshot()); + const scan = (status) => scanAll((cursor) => storage.scanSubmissions({ status }, SCAN_PAGE_SIZE6, cursor, context)); + const submissions = [...await scan("queued"), ...await scan("placed")].sort((a, b) => a.id - b.id); + return { scheduling, tasks, submissions }; + }); + } + submission(id, context) { + return this.#submissions.get(id, context); + } + abortSubmission(id, context, conversationId) { + return this.#submissions.abort(id, context, conversationId); + } + abortTask(id, context) { + return this.#tasks.abort(id, context); + } + async waitForTask(id, context) { + this.#tasks.resume(); + return this.#tasks.waitForTask(id, context); + } + async waitForIdle(context) { + this.#tasks.resume(); + return this.#tasks.waitForIdle(void 0, context); + } + /** Sum every conversation's committed `pi.usage`. Each document is read at its own point; totals only grow. */ + async usage(context) { + const conversations = await this.readOnLine((storage) => scanAll((cursor) => storage.scanConversations({}, SCAN_PAGE_SIZE6, cursor, context))); + const total = UsageDoc.definition.initial(); + for (const { id } of conversations) { + const state = await this.snapshot(UsageDoc, id, context); + if (state !== void 0) + addUsageState(total, state); + } + return total; + } + watchTaskGraph(context) { + return this.#taskGraph.watch(context); + } + async observeTaskGraph(observer, context) { + this.assertUsable(); + return this.#taskGraph.observe(observer, context); + } + async observeConversation(id, observer, context, window) { + this.assertUsable(); + return this.#host.views.observe(id, observer, context, window); + } + root(context, options) { + return this.#create({ kind: "root" }, options ?? {}, context); + } + async conversation(id, context) { + this.assertUsable(); + const record = await this.readOnLine((storage) => storage.conversation(id, context)); + return record === void 0 ? void 0 : new ConversationImpl(record.id, this.#host); + } + createConversation(options, context) { + return this.#create({ kind: "independent", ownership: options.ownership }, options, context); + } + report(error, info = { kind: "listener_error" }, context) { + this.#report(error, info, context); + } + /** The ownership memberships a commit's task changes write, from the scheduler's index. */ + membershipKeys(tx) { + return this.#tasks.membershipKeys(tx); + } + /** Signal and join task invocations after the close listeners and before Storage closes; writes no task outcome. */ + beforeClose() { + return this.#tasks.join(); + } + async #create(target, options, context) { + this.assertUsable(); + const id = await this.commitWith(async (tx) => { + if (target.kind === "root" && await tx.conversation(ROOT_CONVERSATION_ID) !== void 0) { + return ROOT_CONVERSATION_ID; + } + const record = target.kind === "root" ? await tx.createRootConversation() : target.kind === "fork" ? await tx.forkConversation(target.parentId, target.at, { ownership: target.ownership }) : await tx.createConversation({ ownership: target.ownership }); + if (options.agent !== void 0) + await configure(tx, record.id, options.agent); + if (options.init !== void 0) + await options.init(tx, record.id); + return record.id; + }, context); + return new ConversationImpl(id, this.#host); + } + /** + * The built-in creation hook, in every commit that creates or forks a conversation: empty `pi.live`, `pi.inbox`, and + * `pi.usage`, a fresh `pi.provider`, the conversation's `pi.agent` (see `createAgent()`), then + * `HarnessOptions.conversationCreated`. + */ + async conversationCreated(tx, record) { + await tx.doc(LiveDoc, record.id); + await tx.doc(InboxDoc, record.id); + await tx.doc(UsageDoc, record.id); + await tx.doc(ProviderDoc, record.id); + await createAgent(tx, record); + await this.#options.conversationCreated?.(tx, record); + } +}; +function safeReport(onReport, telemetry) { + const reports = createTypedMetrics(telemetry.metrics ?? NOOP_TELEMETRY_METRICS, [DURABLE_TELEMETRY_SCHEMA]).counter("pi.durable.reports"); + return (error, info, context) => { + if (onReport !== void 0) { + contained(() => onReport({ ...info, error }), () => { + }); + } + const attributes = { + type: info.kind + }; + switch (info.kind) { + case "task_faulted": + case "task_interrupted": + case "task_outdated": + attributes.reason = info.reason; + attributes.extension = info.extension; + attributes["task.id"] = info.task.id; + break; + case "migration_failed": + case "task_error": + case "extension_error": + attributes.extension = info.extension; + attributes["task.id"] = info.task?.id; + break; + case "host_error": + attributes["task.id"] = info.task?.id; + break; + } + try { + reports.add(1, { type: info.kind }); + context?.telemetry.addEvent("pi.durable.report", attributes); + } catch { + } + }; +} +function safeNow(now, report2) { + if (now === void 0) + return Date.now; + let reported = false; + return () => { + try { + return now(); + } catch (error) { + if (!reported) { + reported = true; + report2(error, { kind: "host_error", callback: "now" }); + } + return Date.now(); + } + }; +} +function boundConversation(id, binding, submissions, tasks) { + const bind = (context) => withAbortSignal(binding.signal, context); + const bound = (operation) => { + return async (context) => { + binding.check(); + return operation(bind(context)); + }; + }; + async function submit(first, second) { + binding.check(); + if (!("type" in first)) { + binding.signal.throwIfAborted(); + return submissions.admit(first, id, second); + } + const submission2 = await submissions.submit(id, first, bind(second)); + return { + id: submission2.id, + status: bound((callContext) => submission2.status(callContext)), + wait: bound((callContext) => submission2.wait(callContext)), + abort: bound((callContext) => submission2.abort(callContext)) + }; + } + return { + id, + submit, + abort: async (context, options) => { + binding.check(); + return tasks.abortConversation(id, options?.background === true, bind(context)); + }, + waitForIdle: bound((callContext) => tasks.waitForIdle(id, callContext)) + }; +} +var Harness = { + /** Open a Harness over storage. The registry may keep changing while the Harness runs. */ + async open(storage, options, context) { + context.abortSignal?.throwIfAborted(); + const snapshot = options.registry.snapshot(); + const missing = BUILTIN_TASKS.filter((task) => snapshot.task(task.definition.name) === void 0); + if (missing.length > 0) { + const names2 = missing.map((task) => task.definition.name).join(", "); + throw new Error(`Registry lacks built-in tasks ${names2}; create it with createRegistry()`); + } + const harness2 = new HarnessImpl(storage, options, context); + try { + await harness2.openTasks(context); + } catch (error) { + await harness2.close(withoutAbortSignal(context)).catch(() => { + }); + throw error; + } + return harness2; + } +}; + +// node_modules/@earendil-works/pi-durable/dist/storage/scan.js +function scanStart(requested, cursor, fallback) { + if (requested !== void 0 && requested !== "ascending" && requested !== "descending") { + throw new StorageRequestError(`Invalid scan order: ${String(requested)}`); + } + if (cursor === void 0) + return { order: requested ?? fallback, after: void 0 }; + const { after, order: stored } = cursor; + if (typeof after !== "number" || !Number.isSafeInteger(after)) + throw new StorageRequestError("Invalid storage cursor"); + if (stored !== void 0 && stored !== "ascending" && stored !== "descending") { + throw new StorageRequestError("Invalid storage cursor"); + } + const order = stored ?? fallback; + if (requested !== void 0 && requested !== order) { + throw new StorageRequestError(`The cursor continues a ${order} scan; the query asks for ${requested}`); + } + return { order, after }; +} +function nextCursor(id, order) { + return { after: id, order }; +} + +// node_modules/@earendil-works/pi-durable/dist/tools/env.js +function requireEnv(api) { + if (api.env === void 0) + throw new Error("No execution environment is configured"); + return api.env; +} + +// node_modules/@earendil-works/pi-durable/dist/tools/bash.js +var MAX_TIMEOUT_SECONDS = 2147483647 / 1e3; +var bashSchema = obj({ + command: str({ description: "Bash command to execute" }), + timeout: opt(num({ description: "Timeout in seconds (optional, no default timeout)" })) +}, { open: true }); +var powershellSchema = obj({ + command: str({ description: "PowerShell command to execute" }), + timeout: opt(num({ description: "Timeout in seconds (optional, no default timeout)" })) +}, { open: true }); +var shellStructuredOutputSchema = obj({ + output: str({ description: "Combined stdout and stderr: the retained tail, as the model sees it" }), + truncated: bool({ description: "Whether earlier output was dropped" }), + fullOutputPath: opt(str({ description: "File with the complete output, when it was spilled" })), + exitCode: num() +}, { open: true }); +function validateTimeout(timeout) { + if (timeout === void 0) + return; + if (!Number.isFinite(timeout) || timeout <= 0) { + throw new Error("Invalid timeout: must be a finite number of seconds"); + } + if (timeout > MAX_TIMEOUT_SECONDS) { + throw new Error(`Invalid timeout: maximum is ${MAX_TIMEOUT_SECONDS} seconds`); + } +} +async function prepareExecution(command, options, api, context) { + const execution = { + command: options?.commandPrefix ? `${options.commandPrefix} +${command}` : command, + cwd: requireEnv(api).cwd, + env: {}, + inheritEnv: true + }; + await options?.prepare?.(execution, api, context); + return execution; +} +async function runCommand(commands, execution, timeout, api, context) { + const env = requireEnv(api); + let result; + for (const command of commands) { + result = await env.exec(command, { + cwd: execution.cwd, + env: execution.env, + inheritEnv: execution.inheritEnv, + ...timeout === void 0 ? {} : { timeout }, + onOutput: (text, _context, info) => api.output(text, info.skipped), + spill: { afterBytes: DEFAULT_MAX_BYTES, afterLines: DEFAULT_MAX_LINES }, + // An environment may then omit output outside the retained tail and report the omission. + ...api.outputWindow === void 0 ? {} : { window: api.outputWindow } + }, context); + if (result.ok || result.error.code !== "spawn_error") + break; + } + if (result === void 0) + throw new Error("No command to run"); + const spillPath = result.ok ? result.value.spillPath : result.error.spillPath; + if (spillPath !== void 0) { + api.diagnostic({ severity: "info", code: "full_output", message: `Full output: ${spillPath}` }); + } + if (!result.ok) { + if (result.error.code === "aborted" && context.abortSignal?.aborted) + throw result.error; + if (result.error.code === "timeout") + throw new Error(`Command timed out after ${timeout} seconds`); + if (result.error.code === "aborted") + throw new Error("Command aborted"); + throw result.error; + } + const exitCode = result.value.exitCode; + const retained = api.retainedOutput(); + const structuredOutput = { + output: retained.text, + truncated: retained.truncated, + ...spillPath === void 0 ? {} : { fullOutputPath: spillPath }, + exitCode + }; + if (exitCode === 0) + return { structuredOutput }; + const exited = { severity: "error", code: "exit_code", message: `Command exited with code ${exitCode}` }; + return { structuredOutput, isError: true, diagnostics: [exited] }; +} +function createBashTool(options) { + return defineTool({ + name: "bash", + description: `Execute a bash command in the current working directory. Returns combined stdout and stderr. Output is truncated to last ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). If truncated, full output is saved to a temp file. Optionally provide a timeout in seconds.`, + parameters: bashSchema, + structuredOutputSchema: shellStructuredOutputSchema, + outputLimits: { retain: "tail" }, + async execute(args, api, context) { + validateTimeout(args.timeout); + const execution = await prepareExecution(args.command, options, api, context); + return runCommand([execution.command], execution, args.timeout, api, context); + } + }); +} + +// node_modules/diff/libesm/diff/base.js +var Diff = class { + diff(oldStr, newStr, options = {}) { + let callback; + if (typeof options === "function") { + callback = options; + options = {}; + } else if ("callback" in options) { + callback = options.callback; + } + const oldString = this.castInput(oldStr, options); + const newString = this.castInput(newStr, options); + const oldTokens = this.removeEmpty(this.tokenize(oldString, options)); + const newTokens = this.removeEmpty(this.tokenize(newString, options)); + return this.diffWithOptionsObj(oldTokens, newTokens, options, callback); + } + diffWithOptionsObj(oldTokens, newTokens, options, callback) { + var _a; + const done = (value2) => { + value2 = this.postProcess(value2, options); + if (callback) { + setTimeout(function() { + callback(value2); + }, 0); + return void 0; + } else { + return value2; + } + }; + const newLen = newTokens.length, oldLen = oldTokens.length; + let editLength = 1; + let maxEditLength = newLen + oldLen; + if (options.maxEditLength != null) { + maxEditLength = Math.min(maxEditLength, options.maxEditLength); + } + const maxExecutionTime = (_a = options.timeout) !== null && _a !== void 0 ? _a : Infinity; + const abortAfterTimestamp = Date.now() + maxExecutionTime; + const bestPath = [{ oldPos: -1, lastComponent: void 0 }]; + let newPos = this.extractCommon(bestPath[0], newTokens, oldTokens, 0, options); + if (bestPath[0].oldPos + 1 >= oldLen && newPos + 1 >= newLen) { + return done(this.buildValues(bestPath[0].lastComponent, newTokens, oldTokens)); + } + let minDiagonalToConsider = -Infinity, maxDiagonalToConsider = Infinity; + const execEditLength = () => { + for (let diagonalPath = Math.max(minDiagonalToConsider, -editLength); diagonalPath <= Math.min(maxDiagonalToConsider, editLength); diagonalPath += 2) { + let basePath; + const removePath = bestPath[diagonalPath - 1], addPath = bestPath[diagonalPath + 1]; + if (removePath) { + bestPath[diagonalPath - 1] = void 0; + } + let canAdd = false; + if (addPath) { + const addPathNewPos = addPath.oldPos - diagonalPath; + canAdd = addPath && 0 <= addPathNewPos && addPathNewPos < newLen; + } + const canRemove = removePath && removePath.oldPos + 1 < oldLen; + if (!canAdd && !canRemove) { + bestPath[diagonalPath] = void 0; + continue; + } + if (!canRemove || canAdd && removePath.oldPos < addPath.oldPos) { + basePath = this.addToPath(addPath, true, false, 0, options); + } else { + basePath = this.addToPath(removePath, false, true, 1, options); + } + newPos = this.extractCommon(basePath, newTokens, oldTokens, diagonalPath, options); + if (basePath.oldPos + 1 >= oldLen && newPos + 1 >= newLen) { + return done(this.buildValues(basePath.lastComponent, newTokens, oldTokens)) || true; + } else { + bestPath[diagonalPath] = basePath; + if (basePath.oldPos + 1 >= oldLen) { + maxDiagonalToConsider = Math.min(maxDiagonalToConsider, diagonalPath - 1); + } + if (newPos + 1 >= newLen) { + minDiagonalToConsider = Math.max(minDiagonalToConsider, diagonalPath + 1); + } + } + } + editLength++; + }; + if (callback) { + (function exec2() { + setTimeout(function() { + if (editLength > maxEditLength || Date.now() > abortAfterTimestamp) { + return callback(void 0); + } + if (!execEditLength()) { + exec2(); + } + }, 0); + })(); + } else { + while (editLength <= maxEditLength && Date.now() <= abortAfterTimestamp) { + const ret = execEditLength(); + if (ret) { + return ret; + } + } + } + } + addToPath(path2, added, removed, oldPosInc, options) { + const last = path2.lastComponent; + if (last && !options.oneChangePerToken && last.added === added && last.removed === removed) { + return { + oldPos: path2.oldPos + oldPosInc, + lastComponent: { count: last.count + 1, added, removed, previousComponent: last.previousComponent } + }; + } else { + return { + oldPos: path2.oldPos + oldPosInc, + lastComponent: { count: 1, added, removed, previousComponent: last } + }; + } + } + extractCommon(basePath, newTokens, oldTokens, diagonalPath, options) { + const newLen = newTokens.length, oldLen = oldTokens.length; + let oldPos = basePath.oldPos, newPos = oldPos - diagonalPath, commonCount = 0; + while (newPos + 1 < newLen && oldPos + 1 < oldLen && this.equals(oldTokens[oldPos + 1], newTokens[newPos + 1], options)) { + newPos++; + oldPos++; + commonCount++; + if (options.oneChangePerToken) { + basePath.lastComponent = { count: 1, previousComponent: basePath.lastComponent, added: false, removed: false }; + } + } + if (commonCount && !options.oneChangePerToken) { + basePath.lastComponent = { count: commonCount, previousComponent: basePath.lastComponent, added: false, removed: false }; + } + basePath.oldPos = oldPos; + return newPos; + } + equals(left, right, options) { + if (options.comparator) { + return options.comparator(left, right); + } else { + return left === right || !!options.ignoreCase && left.toLowerCase() === right.toLowerCase(); + } + } + removeEmpty(array) { + const ret = []; + for (let i = 0; i < array.length; i++) { + if (array[i]) { + ret.push(array[i]); + } + } + return ret; + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + castInput(value2, options) { + return value2; + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + tokenize(value2, options) { + return Array.from(value2); + } + join(chars) { + return chars.join(""); + } + postProcess(changeObjects, options) { + return changeObjects; + } + get useLongestToken() { + return false; + } + buildValues(lastComponent, newTokens, oldTokens) { + const components = []; + let nextComponent; + while (lastComponent) { + components.push(lastComponent); + nextComponent = lastComponent.previousComponent; + delete lastComponent.previousComponent; + lastComponent = nextComponent; + } + components.reverse(); + const componentLen = components.length; + let componentPos = 0, newPos = 0, oldPos = 0; + for (; componentPos < componentLen; componentPos++) { + const component = components[componentPos]; + if (!component.removed) { + if (!component.added && this.useLongestToken) { + let value2 = newTokens.slice(newPos, newPos + component.count); + value2 = value2.map(function(value3, i) { + const oldValue = oldTokens[oldPos + i]; + return oldValue.length > value3.length ? oldValue : value3; + }); + component.value = this.join(value2); + } else { + component.value = this.join(newTokens.slice(newPos, newPos + component.count)); + } + newPos += component.count; + if (!component.added) { + oldPos += component.count; + } + } else { + component.value = this.join(oldTokens.slice(oldPos, oldPos + component.count)); + oldPos += component.count; + } + } + return components; + } +}; + +// node_modules/diff/libesm/diff/line.js +var LineDiff = class extends Diff { + constructor() { + super(...arguments); + this.tokenize = tokenize; + } + equals(left, right, options) { + if (options.ignoreWhitespace) { + if (!options.newlineIsToken || !left.includes("\n")) { + left = left.trim(); + } + if (!options.newlineIsToken || !right.includes("\n")) { + right = right.trim(); + } + } else if (options.ignoreNewlineAtEof && !options.newlineIsToken) { + if (left.endsWith("\n")) { + left = left.slice(0, -1); + } + if (right.endsWith("\n")) { + right = right.slice(0, -1); + } + } + return super.equals(left, right, options); + } +}; +var lineDiff = new LineDiff(); +function diffLines(oldStr, newStr, options) { + return lineDiff.diff(oldStr, newStr, options); +} +function tokenize(value2, options) { + if (options.stripTrailingCr) { + value2 = value2.replace(/\r\n/g, "\n"); + } + const retLines = [], linesAndNewlines = value2.split(/(\n|\r\n)/); + if (!linesAndNewlines[linesAndNewlines.length - 1]) { + linesAndNewlines.pop(); + } + for (let i = 0; i < linesAndNewlines.length; i++) { + const line = linesAndNewlines[i]; + if (i % 2 && !options.newlineIsToken) { + retLines[retLines.length - 1] += line; + } else { + retLines.push(line); + } + } + return retLines; +} + +// node_modules/diff/libesm/patch/create.js +var INCLUDE_HEADERS = { + includeIndex: true, + includeUnderline: true, + includeFileHeaders: true +}; +var FILE_HEADERS_ONLY = { + includeIndex: false, + includeUnderline: false, + includeFileHeaders: true +}; +function structuredPatch(oldFileName, newFileName, oldStr, newStr, oldHeader, newHeader, options) { + let optionsObj; + if (!options) { + optionsObj = {}; + } else if (typeof options === "function") { + optionsObj = { callback: options }; + } else { + optionsObj = options; + } + if (typeof optionsObj.context === "undefined") { + optionsObj.context = 4; + } + const context = optionsObj.context; + if (optionsObj.newlineIsToken) { + throw new Error("newlineIsToken may not be used with patch-generation functions, only with diffing functions"); + } + if (!optionsObj.callback) { + return diffLinesResultToPatch(diffLines(oldStr, newStr, optionsObj)); + } else { + const { callback } = optionsObj; + diffLines(oldStr, newStr, Object.assign(Object.assign({}, optionsObj), { callback: (diff) => { + const patch = diffLinesResultToPatch(diff); + callback(patch); + } })); + } + function diffLinesResultToPatch(diff) { + if (!diff) { + return; + } + diff.push({ value: "", lines: [] }); + function contextLines(lines2) { + return lines2.map(function(entry2) { + return " " + entry2; + }); + } + const hunks = []; + let oldRangeStart = 0, newRangeStart = 0, curRange = [], oldLine = 1, newLine = 1; + for (let i = 0; i < diff.length; i++) { + const current = diff[i], lines2 = current.lines || splitLines(current.value); + current.lines = lines2; + if (current.added || current.removed) { + if (!oldRangeStart) { + const prev = diff[i - 1]; + oldRangeStart = oldLine; + newRangeStart = newLine; + if (prev) { + curRange = context > 0 ? contextLines(prev.lines.slice(-context)) : []; + oldRangeStart -= curRange.length; + newRangeStart -= curRange.length; + } + } + for (const line of lines2) { + curRange.push((current.added ? "+" : "-") + line); + } + if (current.added) { + newLine += lines2.length; + } else { + oldLine += lines2.length; + } + } else { + if (oldRangeStart) { + if (lines2.length <= context * 2 && i < diff.length - 2) { + for (const line of contextLines(lines2)) { + curRange.push(line); + } + } else { + const contextSize = Math.min(lines2.length, context); + for (const line of contextLines(lines2.slice(0, contextSize))) { + curRange.push(line); + } + const hunk = { + oldStart: oldRangeStart, + oldLines: oldLine - oldRangeStart + contextSize, + newStart: newRangeStart, + newLines: newLine - newRangeStart + contextSize, + lines: curRange + }; + hunks.push(hunk); + oldRangeStart = 0; + newRangeStart = 0; + curRange = []; + } + } + oldLine += lines2.length; + newLine += lines2.length; + } + } + for (const hunk of hunks) { + for (let i = 0; i < hunk.lines.length; i++) { + if (hunk.lines[i].endsWith("\n")) { + hunk.lines[i] = hunk.lines[i].slice(0, -1); + } else { + hunk.lines.splice(i + 1, 0, "\\ No newline at end of file"); + i++; + } + } + } + return { + oldFileName, + newFileName, + oldHeader, + newHeader, + hunks + }; + } +} +function formatPatch(patch, headerOptions) { + if (!headerOptions) { + headerOptions = INCLUDE_HEADERS; + } + if (Array.isArray(patch)) { + if (patch.length > 1 && !headerOptions.includeFileHeaders) { + throw new Error("Cannot omit file headers on a multi-file patch. (The result would be unparseable; how would a tool trying to apply the patch know which changes are to which file?)"); + } + return patch.map((p) => formatPatch(p, headerOptions)).join("\n"); + } + const ret = []; + if (headerOptions.includeIndex && patch.oldFileName == patch.newFileName) { + ret.push("Index: " + patch.oldFileName); + } + if (headerOptions.includeUnderline) { + ret.push("==================================================================="); + } + if (headerOptions.includeFileHeaders) { + ret.push("--- " + patch.oldFileName + (typeof patch.oldHeader === "undefined" ? "" : " " + patch.oldHeader)); + ret.push("+++ " + patch.newFileName + (typeof patch.newHeader === "undefined" ? "" : " " + patch.newHeader)); + } + for (let i = 0; i < patch.hunks.length; i++) { + const hunk = patch.hunks[i]; + if (hunk.oldLines === 0) { + hunk.oldStart -= 1; + } + if (hunk.newLines === 0) { + hunk.newStart -= 1; + } + ret.push("@@ -" + hunk.oldStart + "," + hunk.oldLines + " +" + hunk.newStart + "," + hunk.newLines + " @@"); + for (const line of hunk.lines) { + ret.push(line); + } + } + return ret.join("\n") + "\n"; +} +function createTwoFilesPatch(oldFileName, newFileName, oldStr, newStr, oldHeader, newHeader, options) { + if (typeof options === "function") { + options = { callback: options }; + } + if (!(options === null || options === void 0 ? void 0 : options.callback)) { + const patchObj = structuredPatch(oldFileName, newFileName, oldStr, newStr, oldHeader, newHeader, options); + if (!patchObj) { + return; + } + return formatPatch(patchObj, options === null || options === void 0 ? void 0 : options.headerOptions); + } else { + const { callback } = options; + structuredPatch(oldFileName, newFileName, oldStr, newStr, oldHeader, newHeader, Object.assign(Object.assign({}, options), { callback: (patchObj) => { + if (!patchObj) { + callback(void 0); + } else { + callback(formatPatch(patchObj, options.headerOptions)); + } + } })); + } +} +function splitLines(text) { + const hasTrailingNl = text.endsWith("\n"); + const result = text.split("\n").map((line) => line + "\n"); + if (hasTrailingNl) { + result.pop(); + } else { + result.push(result.pop().slice(0, -1)); + } + return result; +} + +// node_modules/@earendil-works/pi-durable/dist/tools/edit-diff.js +function detectLineEnding(content) { + const crlfIdx = content.indexOf("\r\n"); + const lfIdx = content.indexOf("\n"); + if (lfIdx === -1) + return "\n"; + if (crlfIdx === -1) + return "\n"; + return crlfIdx < lfIdx ? "\r\n" : "\n"; +} +function normalizeToLF(text) { + return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n"); +} +function restoreLineEndings(text, ending) { + return ending === "\r\n" ? text.replace(/\n/g, "\r\n") : text; +} +function normalizeForFuzzyMatch(text) { + return text.normalize("NFKC").split("\n").map((line) => line.trimEnd()).join("\n").replace(/[\u2018\u2019\u201A\u201B]/g, "'").replace(/[\u201C\u201D\u201E\u201F]/g, '"').replace(/[\u2010\u2011\u2012\u2013\u2014\u2015\u2212]/g, "-").replace(/[\u00A0\u2002-\u200A\u202F\u205F\u3000]/g, " "); +} +function splitLinesWithEndings(content) { + return content.match(/[^\n]*\n|[^\n]+/g) ?? []; +} +function getLineSpans(content) { + let offset = 0; + return splitLinesWithEndings(content).map((line) => { + const span = { start: offset, end: offset + line.length }; + offset = span.end; + return span; + }); +} +function getReplacementLineRange(lines2, replacement) { + const replacementStart = replacement.matchIndex; + const replacementEnd = replacement.matchIndex + replacement.matchLength; + let startLine = -1; + for (let i = 0; i < lines2.length; i++) { + const line = lines2[i]; + if (replacementStart >= line.start && replacementStart < line.end) { + startLine = i; + break; + } + } + if (startLine === -1) { + throw new Error("Replacement range is outside the base content."); + } + let endLine = startLine; + while (endLine < lines2.length && lines2[endLine].end < replacementEnd) { + endLine++; + } + if (endLine >= lines2.length) { + throw new Error("Replacement range is outside the base content."); + } + return { startLine, endLine: endLine + 1 }; +} +function applyReplacements(content, replacements, offset = 0) { + let result = content; + for (let i = replacements.length - 1; i >= 0; i--) { + const replacement = replacements[i]; + const matchIndex = replacement.matchIndex - offset; + result = result.substring(0, matchIndex) + replacement.newText + result.substring(matchIndex + replacement.matchLength); + } + return result; +} +function applyReplacementsPreservingUnchangedLines(originalContent, baseContent, replacements) { + const originalLines = splitLinesWithEndings(originalContent); + const baseLines = getLineSpans(baseContent); + if (originalLines.length !== baseLines.length) { + throw new Error("Cannot preserve unchanged lines because the base content has a different line count."); + } + const groups = []; + const sortedReplacements = [...replacements].sort((a, b) => a.matchIndex - b.matchIndex); + for (const replacement of sortedReplacements) { + const range = getReplacementLineRange(baseLines, replacement); + const current = groups[groups.length - 1]; + if (current && range.startLine < current.endLine) { + current.endLine = Math.max(current.endLine, range.endLine); + current.replacements.push(replacement); + continue; + } + groups.push({ ...range, replacements: [replacement] }); + } + let originalLineIndex = 0; + let result = ""; + for (const group of groups) { + result += originalLines.slice(originalLineIndex, group.startLine).join(""); + const groupStartOffset = baseLines[group.startLine].start; + const groupEndOffset = baseLines[group.endLine - 1].end; + result += applyReplacements(baseContent.slice(groupStartOffset, groupEndOffset), group.replacements, groupStartOffset); + originalLineIndex = group.endLine; + } + result += originalLines.slice(originalLineIndex).join(""); + return result; +} +function fuzzyFindText(content, oldText) { + const exactIndex = content.indexOf(oldText); + if (exactIndex !== -1) { + return { + found: true, + index: exactIndex, + matchLength: oldText.length, + usedFuzzyMatch: false, + contentForReplacement: content + }; + } + const fuzzyContent = normalizeForFuzzyMatch(content); + const fuzzyOldText = normalizeForFuzzyMatch(oldText); + const fuzzyIndex = fuzzyContent.indexOf(fuzzyOldText); + if (fuzzyIndex === -1) { + return { + found: false, + index: -1, + matchLength: 0, + usedFuzzyMatch: false, + contentForReplacement: content + }; + } + return { + found: true, + index: fuzzyIndex, + matchLength: fuzzyOldText.length, + usedFuzzyMatch: true, + contentForReplacement: fuzzyContent + }; +} +function stripBom(content) { + return content.startsWith("\uFEFF") ? { bom: "\uFEFF", text: content.slice(1) } : { bom: "", text: content }; +} +function countOccurrences(content, oldText) { + const fuzzyContent = normalizeForFuzzyMatch(content); + const fuzzyOldText = normalizeForFuzzyMatch(oldText); + return fuzzyContent.split(fuzzyOldText).length - 1; +} +function getNotFoundError(path2, editIndex, totalEdits) { + if (totalEdits === 1) { + return new Error(`Could not find the exact text in ${path2}. The old text must match exactly including all whitespace and newlines.`); + } + return new Error(`Could not find edits[${editIndex}] in ${path2}. The oldText must match exactly including all whitespace and newlines.`); +} +function getDuplicateError(path2, editIndex, totalEdits, occurrences) { + if (totalEdits === 1) { + return new Error(`Found ${occurrences} occurrences of the text in ${path2}. The text must be unique. Please provide more context to make it unique.`); + } + return new Error(`Found ${occurrences} occurrences of edits[${editIndex}] in ${path2}. Each oldText must be unique. Please provide more context to make it unique.`); +} +function getEmptyOldTextError(path2, editIndex, totalEdits) { + if (totalEdits === 1) { + return new Error(`oldText must not be empty in ${path2}.`); + } + return new Error(`edits[${editIndex}].oldText must not be empty in ${path2}.`); +} +function getNoChangeError(path2, totalEdits) { + if (totalEdits === 1) { + return new Error(`No changes made to ${path2}. The replacement produced identical content. This might indicate an issue with special characters or the text not existing as expected.`); + } + return new Error(`No changes made to ${path2}. The replacements produced identical content.`); +} +function applyEditsToNormalizedContent(normalizedContent, edits, path2) { + const normalizedEdits = edits.map((edit) => ({ + oldText: normalizeToLF(edit.oldText), + newText: normalizeToLF(edit.newText) + })); + for (let i = 0; i < normalizedEdits.length; i++) { + if (normalizedEdits[i].oldText.length === 0) { + throw getEmptyOldTextError(path2, i, normalizedEdits.length); + } + } + const initialMatches = normalizedEdits.map((edit) => fuzzyFindText(normalizedContent, edit.oldText)); + const usedFuzzyMatch = initialMatches.some((match) => match.usedFuzzyMatch); + const replacementBaseContent = usedFuzzyMatch ? normalizeForFuzzyMatch(normalizedContent) : normalizedContent; + const matchedEdits = []; + for (let i = 0; i < normalizedEdits.length; i++) { + const edit = normalizedEdits[i]; + const matchResult = fuzzyFindText(replacementBaseContent, edit.oldText); + if (!matchResult.found) { + throw getNotFoundError(path2, i, normalizedEdits.length); + } + const occurrences = countOccurrences(replacementBaseContent, edit.oldText); + if (occurrences > 1) { + throw getDuplicateError(path2, i, normalizedEdits.length, occurrences); + } + matchedEdits.push({ + editIndex: i, + matchIndex: matchResult.index, + matchLength: matchResult.matchLength, + newText: edit.newText + }); + } + matchedEdits.sort((a, b) => a.matchIndex - b.matchIndex); + for (let i = 1; i < matchedEdits.length; i++) { + const previous = matchedEdits[i - 1]; + const current = matchedEdits[i]; + if (previous.matchIndex + previous.matchLength > current.matchIndex) { + throw new Error(`edits[${previous.editIndex}] and edits[${current.editIndex}] overlap in ${path2}. Merge them into one edit or target disjoint regions.`); + } + } + const baseContent = normalizedContent; + const newContent = usedFuzzyMatch ? applyReplacementsPreservingUnchangedLines(normalizedContent, replacementBaseContent, matchedEdits) : applyReplacements(replacementBaseContent, matchedEdits); + if (baseContent === newContent) { + throw getNoChangeError(path2, normalizedEdits.length); + } + return { baseContent, newContent }; +} +function generateUnifiedPatch(path2, oldContent, newContent, contextLines = 4) { + return createTwoFilesPatch(path2, path2, oldContent, newContent, void 0, void 0, { + context: contextLines, + headerOptions: FILE_HEADERS_ONLY + }); +} +function generateDiffString(oldContent, newContent, contextLines = 4) { + const parts = diffLines(oldContent, newContent); + const output = []; + const oldLines = oldContent.split("\n"); + const newLines = newContent.split("\n"); + const maxLineNum = Math.max(oldLines.length, newLines.length); + const lineNumWidth = String(maxLineNum).length; + let oldLineNum = 1; + let newLineNum = 1; + let lastWasChange = false; + let firstChangedLine; + for (let i = 0; i < parts.length; i++) { + const part = parts[i]; + const raw = part.value.split("\n"); + if (raw[raw.length - 1] === "") { + raw.pop(); + } + if (part.added || part.removed) { + if (firstChangedLine === void 0) { + firstChangedLine = newLineNum; + } + for (const line of raw) { + if (part.added) { + const lineNum = String(newLineNum).padStart(lineNumWidth, " "); + output.push(`+${lineNum} ${line}`); + newLineNum++; + } else { + const lineNum = String(oldLineNum).padStart(lineNumWidth, " "); + output.push(`-${lineNum} ${line}`); + oldLineNum++; + } + } + lastWasChange = true; + } else { + const nextPartIsChange = i < parts.length - 1 && (parts[i + 1].added || parts[i + 1].removed); + const hasLeadingChange = lastWasChange; + const hasTrailingChange = nextPartIsChange; + if (hasLeadingChange && hasTrailingChange) { + if (raw.length <= contextLines * 2) { + for (const line of raw) { + const lineNum = String(oldLineNum).padStart(lineNumWidth, " "); + output.push(` ${lineNum} ${line}`); + oldLineNum++; + newLineNum++; + } + } else { + const leadingLines = raw.slice(0, contextLines); + const trailingLines = raw.slice(raw.length - contextLines); + const skippedLines = raw.length - leadingLines.length - trailingLines.length; + for (const line of leadingLines) { + const lineNum = String(oldLineNum).padStart(lineNumWidth, " "); + output.push(` ${lineNum} ${line}`); + oldLineNum++; + newLineNum++; + } + output.push(` ${"".padStart(lineNumWidth, " ")} ...`); + oldLineNum += skippedLines; + newLineNum += skippedLines; + for (const line of trailingLines) { + const lineNum = String(oldLineNum).padStart(lineNumWidth, " "); + output.push(` ${lineNum} ${line}`); + oldLineNum++; + newLineNum++; + } + } + } else if (hasLeadingChange) { + const shownLines = raw.slice(0, contextLines); + const skippedLines = raw.length - shownLines.length; + for (const line of shownLines) { + const lineNum = String(oldLineNum).padStart(lineNumWidth, " "); + output.push(` ${lineNum} ${line}`); + oldLineNum++; + newLineNum++; + } + if (skippedLines > 0) { + output.push(` ${"".padStart(lineNumWidth, " ")} ...`); + oldLineNum += skippedLines; + newLineNum += skippedLines; + } + } else if (hasTrailingChange) { + const skippedLines = Math.max(0, raw.length - contextLines); + if (skippedLines > 0) { + output.push(` ${"".padStart(lineNumWidth, " ")} ...`); + oldLineNum += skippedLines; + newLineNum += skippedLines; + } + for (const line of raw.slice(skippedLines)) { + const lineNum = String(oldLineNum).padStart(lineNumWidth, " "); + output.push(` ${lineNum} ${line}`); + oldLineNum++; + newLineNum++; + } + } else { + oldLineNum += raw.length; + newLineNum += raw.length; + } + lastWasChange = false; + } + } + return { diff: output.join("\n"), firstChangedLine }; +} + +// node_modules/@earendil-works/pi-durable/dist/env/decode.js +function rangeDecoder() { + return new TextDecoder("utf-8", { ignoreBOM: true }); +} +function startsWithBom(firstBytes) { + return firstBytes[0] === 239 && firstBytes[1] === 187 && firstBytes[2] === 191; +} + +// node_modules/@earendil-works/pi-durable/dist/env/line-scan.js +var NEWLINE2 = 10; +var encoder2 = new TextEncoder(); +function decodedBytes(text) { + return text === "" ? 0 : encoder2.encode(text).length; +} +var LineScanner = class { + #startLine; + #endLine; + #position = 0; + #newlines = 0; + #lineStart = 0; + #start; + #end; + #firstLineEnd; + #lastLineStart; + #selectedBytes = 0; + #firstLineBytes = 0; + #selection; + #firstLine; + /** The first bytes, held until it is known whether they are a byte-order mark. */ + #head = []; + #bom = false; + /** `startLine` and `endLine` must be non-negative integers with `endLine > startLine`; `endLine` absent: to the end. */ + constructor(startLine, endLine = Number.POSITIVE_INFINITY) { + if (!Number.isSafeInteger(startLine) || startLine < 0 || !(endLine > startLine)) { + throw new RangeError("Invalid line range"); + } + if (endLine !== Number.POSITIVE_INFINITY && !Number.isSafeInteger(endLine)) + throw new RangeError("Invalid line range"); + this.#startLine = startLine; + this.#endLine = endLine; + if (startLine === 0) + this.#begin(0); + } + push(chunk) { + if (this.#head !== void 0) { + const take = Math.min(3 - this.#head.length, chunk.length); + this.#head.push(...chunk.subarray(0, take)); + if (this.#head.length < 3) + return; + this.#releaseHead(); + chunk = chunk.subarray(take); + } + this.#process(chunk); + } + #releaseHead() { + const head = Uint8Array.from(this.#head ?? []); + this.#head = void 0; + this.#bom = startsWithBom(head); + this.#process(head); + } + #process(chunk) { + const base = this.#position; + let from = 0; + for (let index = chunk.indexOf(NEWLINE2); index !== -1; index = chunk.indexOf(NEWLINE2, index + 1)) { + this.#feed(chunk, base, from, index); + const line = this.#newlines; + const position = base + index; + if (line === this.#startLine) + this.#endFirstLine(position); + if (line === this.#endLine - 1) + this.#endSelection(position); + this.#feed(chunk, base, index, index + 1); + from = index + 1; + this.#newlines++; + this.#lineStart = position + 1; + if (this.#newlines === this.#startLine) + this.#begin(this.#lineStart); + if (this.#newlines === this.#endLine - 1) + this.#lastLineStart = this.#lineStart; + } + this.#feed(chunk, base, from, chunk.length); + this.#position += chunk.length; + } + finish() { + if (this.#head !== void 0) + this.#releaseHead(); + const size = this.#position; + if (this.#start === void 0) { + return { + newlines: this.#newlines, + start: size, + end: size, + firstLineEnd: size, + lastLineStart: size, + selectedBytes: 0, + firstLineBytes: 0 + }; + } + if (this.#firstLineEnd === void 0) + this.#endFirstLine(size); + if (this.#end === void 0) + this.#endSelection(size); + return { + newlines: this.#newlines, + start: this.#start, + end: this.#end ?? size, + firstLineEnd: this.#firstLineEnd ?? size, + // A selection that reaches past the last line ends with the last line. + lastLineStart: this.#lastLineStart ?? this.#lineStart, + selectedBytes: this.#selectedBytes, + firstLineBytes: this.#firstLineBytes + }; + } + #begin(start) { + this.#start = start; + if (this.#startLine === this.#endLine - 1) + this.#lastLineStart = start; + this.#selection = rangeDecoder(); + this.#firstLine = rangeDecoder(); + } + #endFirstLine(position) { + this.#firstLineEnd = position; + if (this.#firstLine !== void 0) + this.#firstLineBytes += decodedBytes(this.#firstLine.decode()); + this.#firstLine = void 0; + } + #endSelection(position) { + this.#end = position; + if (this.#selection !== void 0) + this.#selectedBytes += decodedBytes(this.#selection.decode()); + this.#selection = void 0; + } + /** Feed `chunk[from, to)`, which starts at file offset `base`, to the decoders of the ranges still open. */ + #feed(chunk, base, from, to) { + if (this.#bom && base + from < 3) + from = Math.min(to, 3 - base); + if (to <= from) + return; + const bytes2 = chunk.subarray(from, to); + if (this.#selection !== void 0) { + this.#selectedBytes += decodedBytes(this.#selection.decode(bytes2, { stream: true })); + } + if (this.#firstLine !== void 0) { + this.#firstLineBytes += decodedBytes(this.#firstLine.decode(bytes2, { stream: true })); + } + } +}; + +// node_modules/@earendil-works/pi-durable/dist/env/index.js +function getOrThrow(result) { + if (!result.ok) + throw result.error; + return result.value; +} + +// node_modules/@earendil-works/pi-durable/dist/tools/file-mutation-queue.js +var queues = /* @__PURE__ */ new Map(); +async function mutationKey(env, path2, context) { + const absolutePath = getOrThrow(await env.absolutePath(path2, context)); + return `${env.id}\0${await canonical(env, absolutePath, context)}`; +} +async function canonical(env, absolutePath, context) { + const result = await env.canonicalPath(absolutePath, context); + if (result.ok) + return result.value; + if (result.error.code === "not_supported") + return absolutePath; + if (result.error.code !== "not_found") + throw result.error; + const parent = getOrThrow(await env.joinPath([absolutePath, ".."], context)); + if (parent === absolutePath || !absolutePath.startsWith(parent)) + return absolutePath; + const name = absolutePath.slice(parent.length + (/[/\\]$/.test(parent) ? 0 : 1)); + return getOrThrow(await env.joinPath([await canonical(env, parent, context), name], context)); +} +async function withFileMutationQueue(env, path2, fn, context) { + const key = await mutationKey(env, path2, context); + const previous = queues.get(key) ?? Promise.resolve(); + let release = () => { + }; + const done = new Promise((resolve2) => { + release = resolve2; + }); + const tail = previous.then(() => done); + queues.set(key, tail); + await previous; + try { + return await fn(); + } finally { + release(); + if (queues.get(key) === tail) + queues.delete(key); + } +} + +// node_modules/@earendil-works/pi-durable/dist/tools/path-utils.js +var UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g; +var NARROW_NO_BREAK_SPACE = "\u202F"; +function normalizeToolPath(path2) { + const normalized = path2.replace(UNICODE_SPACES, " "); + return normalized.startsWith("@") ? normalized.slice(1) : normalized; +} +async function resolveToolPath(env, path2, context) { + return getOrThrow(await env.absolutePath(normalizeToolPath(path2), context)); +} +async function resolveReadToolPath(env, path2, context) { + const resolved = await resolveToolPath(env, path2, context); + const variants = [ + resolved, + resolved.replace(/ (AM|PM)\./gi, `${NARROW_NO_BREAK_SPACE}$1.`), + resolved.normalize("NFD"), + resolved.replace(/'/g, "\u2019"), + resolved.normalize("NFD").replace(/'/g, "\u2019") + ]; + for (const variant of new Set(variants)) { + if (getOrThrow(await env.exists(variant, context))) + return variant; + } + return resolved; +} + +// node_modules/@earendil-works/pi-durable/dist/tools/edit.js +var replaceEditSchema = obj({ + oldText: str({ + description: "Exact text for one targeted replacement. It must be unique in the original file and must not overlap with any other edits[].oldText in the same call." + }), + newText: str({ description: "Replacement text for this targeted edit." }) +}, { open: true }); +var editSchema = obj({ + path: str({ description: "Path to the file to edit (relative or absolute)" }), + edits: arr(replaceEditSchema, { + description: "One or more targeted replacements. Each edit is matched against the original file, not incrementally. Do not include overlapping or nested edits. If two changes touch the same block or nearby lines, merge them into one edit instead." + }) +}, { open: true }); +function isSingleEditInput(value2) { + if (!value2 || typeof value2 !== "object" || Array.isArray(value2)) + return false; + const edit = value2; + return typeof edit.oldText === "string" && typeof edit.newText === "string"; +} +function prepareEditArguments(input) { + if (!input || typeof input !== "object" || Array.isArray(input)) + return input; + const args = { ...input }; + if (typeof args.edits === "string") { + try { + const parsed = JSON.parse(args.edits); + if (Array.isArray(parsed)) { + args.edits = parsed; + } else if (isSingleEditInput(parsed)) { + args.edits = [parsed]; + } + } catch { + } + } else if (isSingleEditInput(args.edits)) { + args.edits = [args.edits]; + } + const legacy = args; + if (typeof legacy.oldText !== "string" || typeof legacy.newText !== "string") + return legacy; + const edits = Array.isArray(legacy.edits) ? [...legacy.edits] : []; + edits.push({ oldText: legacy.oldText, newText: legacy.newText }); + const { oldText: _oldText, newText: _newText, ...rest } = legacy; + return { ...rest, edits }; +} +function validateEditInput(input) { + if (!Array.isArray(input.edits) || input.edits.length === 0) { + throw new Error("Edit tool input is invalid. edits must contain at least one replacement."); + } + return { path: input.path, edits: input.edits }; +} +function editAccessError(path2, error) { + return new Error(`Could not edit file: ${path2}. Error code: ${error.code}.`, { cause: error }); +} +function createEditTool() { + return defineTool({ + name: "edit", + description: "Edit a single file using exact text replacement. Every edits[].oldText must match a unique, non-overlapping region of the original file. If two changes affect the same block or nearby lines, merge them into one edit instead of emitting overlapping edits. Do not include large unchanged regions just to connect distant changes.", + parameters: editSchema, + prepareArguments: prepareEditArguments, + async execute(args, api, context) { + const { path: path2, edits } = validateEditInput(args); + const env = requireEnv(api); + const absolutePath = await resolveToolPath(env, path2, context); + return withFileMutationQueue(env, absolutePath, async () => { + if (context.abortSignal?.aborted) + throw new Error("Operation aborted"); + const info = await env.fileInfo(absolutePath, context); + if (!info.ok) + throw editAccessError(path2, info.error); + if (info.value.kind !== "file" && info.value.kind !== "symlink") { + throw new Error(`Could not edit file: ${path2}. Path is not a file.`); + } + const readResult = await env.readTextFile(absolutePath, context); + if (!readResult.ok) + throw editAccessError(path2, readResult.error); + if (context.abortSignal?.aborted) + throw new Error("Operation aborted"); + const { bom, text: content } = stripBom(readResult.value); + const originalEnding = detectLineEnding(content); + const normalizedContent = normalizeToLF(content); + const { baseContent, newContent } = applyEditsToNormalizedContent(normalizedContent, edits, path2); + if (context.abortSignal?.aborted) + throw new Error("Operation aborted"); + const finalContent = bom + restoreLineEndings(newContent, originalEnding); + const writeResult = await env.writeFile(absolutePath, finalContent, context); + if (!writeResult.ok) + throw editAccessError(path2, writeResult.error); + if (context.abortSignal?.aborted) + throw new Error("Operation aborted"); + const diffResult = generateDiffString(baseContent, newContent); + const details = { + diff: diffResult.diff, + patch: generateUnifiedPatch(path2, baseContent, newContent), + ...diffResult.firstChangedLine === void 0 ? {} : { firstChangedLine: diffResult.firstChangedLine } + }; + return { + output: [{ type: "text", text: `Successfully replaced ${edits.length} block(s) in ${path2}.` }], + details + }; + }, context); + } + }); +} + +// node_modules/@earendil-works/pi-durable/dist/tools/image.js +var PNG_SIGNATURE = [137, 80, 78, 71, 13, 10, 26, 10]; +var HEADER_BYTES = 32; +var BLOCK_BYTES = 64 * 1024; +async function detectSupportedImageMimeTypeOf(source) { + const header = await source.read(0, HEADER_BYTES); + if (!startsWith(header, PNG_SIGNATURE)) + return detectSupportedImageMimeType(header); + return isPng(header) && !await isAnimatedPngOf(source) ? "image/png" : void 0; +} +async function isAnimatedPngOf(source) { + let block = new Uint8Array(0); + let blockStart = 0; + const bytesAt = async (offset2, length) => { + if (offset2 < blockStart || offset2 + length > blockStart + block.length) { + blockStart = offset2; + block = await source.read(offset2, BLOCK_BYTES); + } + return block.subarray(offset2 - blockStart, offset2 - blockStart + length); + }; + let offset = PNG_SIGNATURE.length; + while (offset + 8 <= source.size) { + const chunkHeader = await bytesAt(offset, 8); + const chunkLength = readUint32BE(chunkHeader, 0); + if (startsWithAscii(chunkHeader, 4, "acTL")) + return true; + if (startsWithAscii(chunkHeader, 4, "IDAT")) + return false; + const nextOffset = offset + 8 + chunkLength + 4; + if (nextOffset <= offset || nextOffset > source.size) + return false; + offset = nextOffset; + } + return false; +} +function detectSupportedImageMimeType(buffer) { + if (startsWith(buffer, [255, 216, 255])) + return buffer[3] === 247 ? void 0 : "image/jpeg"; + if (startsWith(buffer, PNG_SIGNATURE)) + return isPng(buffer) && !isAnimatedPng(buffer) ? "image/png" : void 0; + if (startsWithAscii(buffer, 0, "GIF87a") || startsWithAscii(buffer, 0, "GIF89a")) + return "image/gif"; + if (startsWithAscii(buffer, 0, "RIFF") && startsWithAscii(buffer, 8, "WEBP")) + return "image/webp"; + if (startsWithAscii(buffer, 0, "BM") && isBmp(buffer)) + return "image/bmp"; + return void 0; +} +function isPng(buffer) { + return buffer.length >= 16 && readUint32BE(buffer, PNG_SIGNATURE.length) === 13 && startsWithAscii(buffer, 12, "IHDR"); +} +function isAnimatedPng(buffer) { + let offset = PNG_SIGNATURE.length; + while (offset + 8 <= buffer.length) { + const chunkLength = readUint32BE(buffer, offset); + const chunkTypeOffset = offset + 4; + if (startsWithAscii(buffer, chunkTypeOffset, "acTL")) + return true; + if (startsWithAscii(buffer, chunkTypeOffset, "IDAT")) + return false; + const nextOffset = offset + 8 + chunkLength + 4; + if (nextOffset <= offset || nextOffset > buffer.length) + return false; + offset = nextOffset; + } + return false; +} +function imageDimensions(bytes2, mimeType) { + switch (mimeType) { + case "image/png": + return bytes2.length < 24 ? void 0 : { width: readUint32BE(bytes2, 16), height: readUint32BE(bytes2, 20) }; + case "image/gif": + return bytes2.length < 10 ? void 0 : { width: readUint16LE(bytes2, 6), height: readUint16LE(bytes2, 8) }; + case "image/webp": + if (bytes2.length < 30) + return void 0; + if (startsWithAscii(bytes2, 12, "VP8 ")) + return { width: readUint16LE(bytes2, 26) & 16383, height: readUint16LE(bytes2, 28) & 16383 }; + if (startsWithAscii(bytes2, 12, "VP8L")) { + const bits = readUint32LE(bytes2, 21); + return { width: (bits & 16383) + 1, height: (bits >>> 14 & 16383) + 1 }; + } + if (startsWithAscii(bytes2, 12, "VP8X")) { + const uint24 = (offset) => readUint16LE(bytes2, offset) + ((bytes2[offset + 2] ?? 0) << 16); + return { width: uint24(24) + 1, height: uint24(27) + 1 }; + } + return void 0; + case "image/jpeg": + for (let offset = 2; offset + 9 <= bytes2.length; ) { + if (bytes2[offset] !== 255) + return void 0; + const marker = bytes2[offset + 1]; + if (marker === 255) { + offset++; + continue; + } + if (marker >= 192 && marker <= 207 && marker !== 196 && marker !== 200 && marker !== 204) { + return { width: readUint16BE(bytes2, offset + 7), height: readUint16BE(bytes2, offset + 5) }; + } + offset += 2 + readUint16BE(bytes2, offset + 2); + } + return void 0; + default: + return void 0; + } +} +function isBmp(buffer) { + if (buffer.length < 26) + return false; + const declaredFileSize = readUint32LE(buffer, 2); + const pixelDataOffset = readUint32LE(buffer, 10); + const dibHeaderSize = readUint32LE(buffer, 14); + if (declaredFileSize !== 0 && declaredFileSize < 26) + return false; + if (pixelDataOffset < 14 + dibHeaderSize) + return false; + if (declaredFileSize !== 0 && pixelDataOffset >= declaredFileSize) + return false; + let colorPlanes; + let bitsPerPixel; + if (dibHeaderSize === 12) { + colorPlanes = readUint16LE(buffer, 22); + bitsPerPixel = readUint16LE(buffer, 24); + } else if (dibHeaderSize >= 40 && dibHeaderSize <= 124) { + if (buffer.length < 30) + return false; + colorPlanes = readUint16LE(buffer, 26); + bitsPerPixel = readUint16LE(buffer, 28); + } else { + return false; + } + return colorPlanes === 1 && [1, 4, 8, 16, 24, 32].includes(bitsPerPixel); +} +function readUint16LE(buffer, offset) { + return (buffer[offset] ?? 0) + ((buffer[offset + 1] ?? 0) << 8); +} +function readUint16BE(buffer, offset) { + return ((buffer[offset] ?? 0) << 8) + (buffer[offset + 1] ?? 0); +} +function readUint32BE(buffer, offset) { + return (buffer[offset] ?? 0) * 16777216 + ((buffer[offset + 1] ?? 0) << 16) + ((buffer[offset + 2] ?? 0) << 8) + (buffer[offset + 3] ?? 0); +} +function readUint32LE(buffer, offset) { + return (buffer[offset] ?? 0) + ((buffer[offset + 1] ?? 0) << 8) + ((buffer[offset + 2] ?? 0) << 16) + (buffer[offset + 3] ?? 0) * 16777216; +} +function startsWith(buffer, bytes2) { + if (buffer.length < bytes2.length) + return false; + return bytes2.every((byte, index) => buffer[index] === byte); +} +function startsWithAscii(buffer, offset, text) { + if (buffer.length < offset + text.length) + return false; + for (let index = 0; index < text.length; index++) { + if (buffer[offset + index] !== text.charCodeAt(index)) + return false; + } + return true; +} + +// node_modules/@earendil-works/pi-durable/dist/tools/image-processor.js +var DEFAULT_IMAGE_LIMITS = { maxWidth: 2e3, maxHeight: 2e3, maxBytes: 4.5 * 1024 * 1024 }; +var INLINE_IMAGE_TYPES = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]); +function toBase64(bytes2) { + const parts = []; + for (let i = 0; i < bytes2.length; i += 8192) { + parts.push(String.fromCharCode.apply(null, bytes2.subarray(i, i + 8192))); + } + return btoa(parts.join("")); +} +function base64Length(byteLength) { + return Math.ceil(byteLength / 3) * 4; +} + +// node_modules/@earendil-works/pi-durable/dist/tools/read.js +var readSchema = obj({ + path: str({ description: "Path to the file to read (relative or absolute)" }), + offset: opt(num({ description: "Line number to start reading from (1-indexed)" })), + limit: opt(num({ description: "Maximum number of lines to read" })) +}, { open: true }); +var readStructuredOutputSchema = union([ + str(), + obj({ type: lit("image"), data: str(), mimeType: str() }, { open: true }) +]); +var READ_CHUNK = 64 * 1024; +function sliceIndex(value2) { + return Number.isNaN(value2) ? 0 : Math.trunc(value2); +} +async function readHead(reader, start, end, skipBom, context) { + const decoder3 = rangeDecoder(); + let text = ""; + let newlines = 0; + for (let position = skipBom && start === 0 ? 3 : start; position < end; ) { + const bytes2 = getOrThrow(await reader.read(position, Math.min(READ_CHUNK, end - position), context)); + if (bytes2.length === 0) + break; + position += bytes2.length; + const decoded = decoder3.decode(bytes2, { stream: true }); + text += decoded; + for (let index = decoded.indexOf("\n"); index !== -1; index = decoded.indexOf("\n", index + 1)) + newlines++; + if (newlines >= DEFAULT_MAX_LINES || utf8ByteLength(text) > DEFAULT_MAX_BYTES + 1) + return text; + } + return text + decoder3.decode(); +} +function createReadTool(options = {}) { + const formats2 = options.images === void 0 ? "jpg, png, gif, webp" : "jpg, png, gif, webp, bmp"; + return defineTool({ + name: "read", + description: `Read the contents of a file. Supports text files and images (${formats2}). Images are sent as attachments. For text files, output is truncated to ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Use offset/limit for large files. When you need the full file, continue with offset until complete.`, + parameters: readSchema, + structuredOutputSchema: readStructuredOutputSchema, + async execute(args, api, context) { + const { path: path2, offset, limit } = args; + const env = requireEnv(api); + const absolutePath = await resolveReadToolPath(env, path2, context); + const reader = getOrThrow(await env.openBinaryReader(absolutePath, void 0, context)); + try { + for (let attempt = 0; ; attempt++) { + const before = getOrThrow(await reader.info(context)); + const mimeType = await detectSupportedImageMimeTypeOf({ + size: before.size, + read: async (position, length) => getOrThrow(await reader.read(position, length, context)) + }); + const result = mimeType === void 0 ? await readText(reader, path2, offset, limit, context) : await readImage(reader, before, path2, mimeType, options.images, await imageModel(api, context), context); + const after = getOrThrow(await reader.info(context)); + const unchanged = after.size === before.size && after.mtimeMs === before.mtimeMs; + if (unchanged || mimeType === void 0 && after.size > before.size) + return result; + if (attempt === 1) + throw new Error(`${path2} changed while it was read`); + } + } finally { + await reader.close(context); + } + } + }); +} +async function readImage(reader, info, path2, mimeType, processor, { vision, limits }, context) { + let image; + const notes = []; + if (processor !== void 0) { + const bytes2 = getOrThrow(await reader.read(0, info.size, context)); + const prepared = await processor.prepare(bytes2, mimeType, limits); + if (prepared === void 0) + return imageError(`${path2} is an image (${mimeType}) that cannot be prepared for the model`); + image = prepared; + if (prepared.convertedFrom !== void 0) + notes.push(`Converted from ${prepared.convertedFrom} to ${prepared.mimeType}.`); + if (prepared.resized !== void 0) { + const { from, to } = prepared.resized; + const scale = (from.width / to.width).toFixed(2); + notes.push(`Resized from ${from.width}x${from.height} to ${to.width}x${to.height}. Multiply coordinates by ${scale} to map them to the original.`); + } + } else { + if (!INLINE_IMAGE_TYPES.has(mimeType)) { + return imageError(`${path2} is an image (${mimeType}) that needs converting, and no image processor is configured`); + } + const unshrinkable = "and no image processor is configured to shrink it"; + if (base64Length(info.size) > limits.maxBytes) { + return imageError(`${path2} is an image (${mimeType}) of ${formatSize(info.size)}, too large to send, ${unshrinkable}`); + } + const bytes2 = getOrThrow(await reader.read(0, info.size, context)); + const size = imageDimensions(bytes2, mimeType); + if (size === void 0) + return imageError(`${path2} is an image (${mimeType}) whose size cannot be read`); + if (size.width > limits.maxWidth || size.height > limits.maxHeight) { + return imageError(`${path2} is an image (${mimeType}) of ${size.width}x${size.height}, larger than ${limits.maxWidth}x${limits.maxHeight}, ${unshrinkable}`); + } + image = { data: toBase64(bytes2), mimeType }; + } + if (!vision) + notes.push("The current model does not support images; it sees a placeholder instead."); + const diagnostics = [ + { severity: "info", code: "image", message: [`Read image file [${image.mimeType}].`, ...notes].join(" ") } + ]; + const block = { type: "image", data: image.data, mimeType: image.mimeType }; + return { output: [block], structuredOutput: block, diagnostics }; +} +async function imageModel(api, context) { + const ref = (await api.agent(context)).model; + const model2 = ref === void 0 ? void 0 : api.models.getModel(ref.provider, ref.modelId); + const resize = model2?.inputLimits?.images?.resize; + return { + vision: model2?.input.includes("image") ?? true, + limits: { + maxWidth: resize?.maxWidth ?? DEFAULT_IMAGE_LIMITS.maxWidth, + maxHeight: resize?.maxHeight ?? DEFAULT_IMAGE_LIMITS.maxHeight, + maxBytes: resize?.maxBytes ?? DEFAULT_IMAGE_LIMITS.maxBytes + } + }; +} +function imageError(message) { + return { output: [], isError: true, diagnostics: [{ severity: "error", code: "unsupported_image", message }] }; +} +async function readText(reader, path2, offset, limit, context) { + const startLine = offset ? Math.max(0, offset - 1) : 0; + const startLineDisplay = startLine + 1; + const sliceStart = sliceIndex(startLine); + const scanStart2 = Number.isSafeInteger(sliceStart) ? sliceStart : 0; + const requestedEnd = limit === void 0 ? void 0 : Math.max(scanStart2 + 1, sliceIndex(startLine + limit)); + const scanEnd = requestedEnd !== void 0 && Number.isSafeInteger(requestedEnd) ? requestedEnd : void 0; + const scanOf = async (endLine) => getOrThrow(await reader.scanLines({ startLine: scanStart2, ...endLine === void 0 ? {} : { endLine } }, context)); + let scan = await scanOf(scanEnd); + const totalFileLines = scan.newlines + 1; + if (startLine >= totalFileLines) { + throw new Error(`Offset ${offset} is beyond end of file (${totalFileLines} lines total)`); + } + let userLimitedLines; + let selectedLineCount = totalFileLines - sliceStart; + if (limit !== void 0) { + const endLine = Math.min(startLine + limit, totalFileLines); + userLimitedLines = endLine - startLine; + const relativeEnd = sliceIndex(endLine); + const sliceEnd = relativeEnd < 0 ? Math.max(totalFileLines + relativeEnd, 0) : relativeEnd; + selectedLineCount = Math.max(0, sliceEnd - sliceStart); + if (selectedLineCount > 0 && relativeEnd < 0) + scan = await scanOf(sliceEnd); + } + const empty = selectedLineCount === 0; + const endsWithNewline = !empty && scan.lastLineStart === scan.end && scan.lastLineStart > scan.start; + const totals = { + lines: empty || scan.selectedBytes === 0 ? 0 : selectedLineCount - (endsWithNewline ? 1 : 0), + bytes: empty ? 0 : scan.selectedBytes + }; + const firstBytes = getOrThrow(await reader.read(0, 3, context)); + const head = empty ? "" : await readHead(reader, scan.start, scan.end, startsWithBom(firstBytes), context); + const { content: headText, ...truncation } = truncateHeadOf(head, totals); + const diagnostics = []; + let outputText = headText; + let details; + if (truncation.firstLineExceedsLimit) { + const integral = Number.isInteger(startLine); + const lineBytes = new TextEncoder().encode(integral ? head.split("\n")[0] ?? "" : ""); + const lineSize = integral ? scan.firstLineBytes : 0; + const end = characterEnd(lineBytes, DEFAULT_MAX_BYTES); + outputText = new TextDecoder().decode(lineBytes.subarray(0, end)); + diagnostics.push({ + severity: "warn", + code: "truncated", + message: `Line ${startLineDisplay} is ${formatSize(lineSize)}, exceeds the ${formatSize(DEFAULT_MAX_BYTES)} limit; showing its first ${formatSize(end)}. Use bash: sed -n '${startLineDisplay}p' ${path2} | tail -c +${end + 1}` + }); + details = { truncation: { ...truncation, outputBytes: end, outputLines: 1 } }; + } else if (truncation.truncated) { + const endLineDisplay = startLineDisplay + truncation.outputLines - 1; + const nextOffset = endLineDisplay + 1; + const limitText = truncation.truncatedBy === "lines" ? "" : ` (${formatSize(DEFAULT_MAX_BYTES)} limit)`; + diagnostics.push({ + severity: "info", + code: "truncated", + message: `Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}${limitText}. Use offset=${nextOffset} to continue.` + }); + details = { truncation }; + } else if (userLimitedLines !== void 0 && startLine + userLimitedLines < totalFileLines) { + const remaining = totalFileLines - (startLine + userLimitedLines); + const nextOffset = startLine + userLimitedLines + 1; + diagnostics.push({ + severity: "info", + message: `${remaining} more lines in file. Use offset=${nextOffset} to continue.` + }); + } + return { + output: outputText === "" ? [] : [{ type: "text", text: outputText }], + structuredOutput: outputText, + ...details === void 0 ? {} : { details }, + diagnostics + }; +} + +// node_modules/@earendil-works/pi-durable/dist/tools/write.js +var writeSchema = obj({ + path: str({ description: "Path to the file to write (relative or absolute)" }), + content: str({ description: "Content to write to the file" }) +}, { open: true }); +function createWriteTool() { + return defineTool({ + name: "write", + description: "Write content to a file. Creates the file if it doesn't exist, overwrites if it does. Automatically creates parent directories.", + parameters: writeSchema, + async execute(args, api, context) { + const { path: path2, content } = args; + const env = requireEnv(api); + const absolutePath = await resolveToolPath(env, path2, context); + return withFileMutationQueue(env, absolutePath, async () => { + if (context.abortSignal?.aborted) + throw new Error("Operation aborted"); + getOrThrow(await env.writeFile(absolutePath, content, context)); + if (context.abortSignal?.aborted) + throw new Error("Operation aborted"); + return { output: [{ type: "text", text: `Successfully wrote to ${path2}` }] }; + }, context); + } + }); +} + +// node_modules/@earendil-works/pi-durable/dist/tools/index.js +function createCodingTools(options = {}) { + return defineExtension({ + name: "coding-tools", + tools: [createReadTool(options), createWriteTool(), createEditTool(), createBashTool()] + }); +} +var CodingTools = createCodingTools(); + +// environment.ts +var Started = defineDoc({ + kind: "board.started", + version: 1, + checkpointWhen: () => false, + scope: "conversation", + history: "latest", + fork: "initial", + initial: () => ({}) +}); +function conversationChains(harness2, context) { + const cache = /* @__PURE__ */ new Map(); + const chain2 = (id) => { + let result = cache.get(id); + if (!result) { + result = (async () => { + const record = await harness2.commit((tx) => tx.conversation(id), context); + const up = record?.owner?.conversationId ?? record?.parent?.conversationId ?? (await harness2.snapshot(Started, id, context))?.by; + return [String(id), ...up === void 0 ? [] : await chain2(up)]; + })(); + cache.set(id, result); + } + return result; + }; + return chain2; +} +var ops = Deno.core.ops; +var ok = (value2) => ({ ok: true, value: value2 }); +var path = (cwd, input) => { + const parts = []; + for (const p of (input.startsWith("/") ? input : `${cwd}/${input}`).split("/")) { + if (p === "..") parts.pop(); + else if (p && p !== ".") parts.push(p); + } + return "/" + parts.join("/"); +}; +function environment(cwd = "/", conversation) { + const conversations = Array.isArray(conversation) ? conversation : conversation === void 0 ? [] : [String(conversation)]; + const call = (kind, p, bytes2 = []) => ops.op_file(kind, path(cwd, p), bytes2); + const env = { + id: "world", + cwd, + absolutePath: async (p) => ok(path(cwd, p)), + canonicalPath: async () => ({ ok: false, error: { code: "not_supported", message: "The VFS does not expose canonical paths" } }), + joinPath: async (parts) => ok(path(cwd, parts.join("/"))), + fileInfo: (p) => call("stat", p), + exists: async (p) => { + const result = await call("stat", p); + return result.ok ? ok(true) : result.error.code === "not_found" ? ok(false) : result; + }, + readBinaryFile: async (p) => { + const r = await call("read", p); + return r.ok ? ok(new Uint8Array(r.value)) : r; + }, + readTextFile: async (p) => { + const r = await env.readBinaryFile(p); + return r.ok ? ok(new TextDecoder().decode(r.value)) : r; + }, + writeFile: async (p, content) => { + const full = path(cwd, p); + const parent = full.slice(0, full.lastIndexOf("/")) || "/"; + const created = await env.createDir(parent, { recursive: true }); + if (!created.ok) return created; + return call("write", full, Array.from(typeof content === "string" ? new TextEncoder().encode(content) : content)); + }, + createDir: async (p, options) => { + const full = path(cwd, p); + if (!options?.recursive) return call("mkdir", full); + let current = ""; + for (const part of full.split("/").filter(Boolean)) { + current += "/" + part; + const exists = await call("stat", current); + if (!exists.ok) { + const r = await call("mkdir", current); + if (!r.ok && (await call("stat", current)).value?.kind !== "directory") return r; + } + } + return ok(void 0); + }, + openBinaryReader: async (p) => { + const data = await env.readBinaryFile(p); + if (!data.ok) return data; + const info = await env.fileInfo(p); + return ok({ + info: async () => info, + read: async (offset, length) => ok(data.value.slice(offset, offset + length)), + scanLines: async (options) => { + const scanner = new LineScanner(options.startLine, options.endLine); + scanner.push(data.value); + return ok(scanner.finish()); + }, + close: async () => { + } + }); + }, + exec: async (command, options, context) => { + let spillPath; + let pending = "", bytes2 = 0, lines2 = 0; + const output = async (text, stream) => { + options?.onOutput?.(text, context, { stream }); + if (!options?.spill) return; + bytes2 += new TextEncoder().encode(text).length; + lines2 += text.split("\n").length - 1; + if (!spillPath) { + pending += text; + if (bytes2 <= options.spill.afterBytes && lines2 + (pending.endsWith("\n") ? 0 : 1) <= options.spill.afterLines) return; + spillPath = `/tmp/bash-${crypto.randomUUID()}.log`; + const created = await env.writeFile(spillPath, pending); + if (!created.ok) throw new Error(created.error.message); + pending = ""; + } else { + const appended = await call("append", spillPath, Array.from(new TextEncoder().encode(text))); + if (!appended.ok) throw new Error(appended.error.message); + } + }; + try { + const rid = ops.op_exec(command, options?.cwd ?? cwd, options?.timeout, void 0, void 0, conversations); + const decoders = { stdout: new TextDecoder(), stderr: new TextDecoder() }; + let result; + while (true) { + const chunk = await ops.op_exec_next(rid); + if (!chunk.stream) { + result = chunk; + break; + } + await output(decoders[chunk.stream].decode(new Uint8Array(chunk.bytes), { stream: true }), chunk.stream); + } + if (result.error) throw new Error(result.error); + for (const stream of ["stdout", "stderr"]) { + const text = decoders[stream].decode(); + if (text) await output(text, stream); + } + return ok({ exitCode: result.exitCode, ...spillPath ? { spillPath } : {} }); + } catch (error) { + return { ok: false, error: Object.assign(new Error(String(error)), { code: "unknown", ...spillPath ? { spillPath } : {} }) }; + } + }, + cleanup: async () => { + } + }; + return env; +} + +// extensions.ts +var ops2 = Deno.core.ops; +var encoder3 = new TextEncoder(); +var decoder2 = new TextDecoder(); +var ExtensionState = defineDoc({ kind: "sausage.extensions", version: 1, scope: "session", checkpointWhen: () => false, initial: () => ({ good: null, current: null, error: null }) }); +var Metadata = defineDoc({ kind: "sausage.metadata", version: 1, scope: "conversation", history: "latest", fork: "current", initial: () => ({ label: "", title: "" }), checkpointWhen: () => false }); +async function file(kind, path2, bytes2 = []) { + const result = await ops2.op_file(kind, path2, bytes2); + if (!result.ok) throw Object.assign(new Error(result.error.message), result.error); + return result.value; +} +var fs = { + read: async (path2) => decoder2.decode(new Uint8Array(await file("read", path2))), + write: (path2, text) => file("write", path2, Array.from(encoder3.encode(text))), + append: (path2, text) => file("append", path2, Array.from(encoder3.encode(text))), + list: (path2) => file("list", path2), + stat: (path2) => file("stat", path2), + lstat: (path2) => file("lstat", path2), + mkdir: (path2) => file("mkdir", path2), + remove: (path2) => file("remove", path2), + rename: (from, to) => file("rename", from, Array.from(encoder3.encode(to))) +}; +async function exec(command, options) { + options.signal?.throwIfAborted(); + const rid = ops2.op_exec(command, options.cwd, options.timeout, void 0, options.env, options.conversations); + const abort = () => Deno.core.close(rid); + options.signal?.addEventListener("abort", abort, { once: true }); + const decoders = { stdout: new TextDecoder(), stderr: new TextDecoder() }; + const result = { code: 0, stdout: "", stderr: "" }; + try { + for (; ; ) { + const chunk = await ops2.op_exec_next(rid); + if (!chunk.stream) { + if (chunk.error) throw new Error(chunk.error); + result.code = chunk.exitCode; + for (const stream of ["stdout", "stderr"]) { + const text2 = decoders[stream].decode(); + result[stream] += text2; + if (text2) options.onOutput?.(text2, stream); + } + return result; + } + const text = decoders[chunk.stream].decode(new Uint8Array(chunk.bytes), { stream: true }); + result[chunk.stream] += text; + options.onOutput?.(text, chunk.stream); + } + } finally { + options.signal?.removeEventListener("abort", abort); + } +} + +// node_modules/@earendil-works/chord/dist/sqlite/schema.js +function validateSqliteSchemaPrefix(prefix) { + if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(prefix)) { + throw new Error(`Invalid SQLite schema prefix ${JSON.stringify(prefix)}`); + } + if (prefix.toLowerCase().startsWith("sqlite_")) { + throw new Error(`SQLite schema prefix ${JSON.stringify(prefix)} is reserved`); + } +} + +// node_modules/@earendil-works/chord/dist/sqlite/migrations.js +async function applySqliteMigrations(database, migrations, options) { + const { table } = options; + validateSqliteSchemaPrefix(table); + for (let index = 0; index < migrations.length; index++) { + if (migrations[index]?.version !== index + 1) { + throw new Error("SQLite migrations must have contiguous versions starting at 1"); + } + } + await database.transaction(async (transaction) => { + await transaction.exec(`CREATE TABLE IF NOT EXISTS ${table} ( + singleton INTEGER PRIMARY KEY CHECK (singleton = 1), + version INTEGER NOT NULL CHECK (version >= 0) + ) STRICT`); + await transaction.run(`INSERT OR IGNORE INTO ${table} (singleton, version) VALUES (1, 0)`); + const row = await transaction.get(`SELECT version FROM ${table} WHERE singleton = 1`); + if (row === void 0) + throw new Error("SQLite schema metadata is missing"); + const currentVersion = migrations.at(-1)?.version ?? 0; + if (row.version > currentVersion) { + throw new Error(`SQLite schema version ${row.version} is newer than supported version ${currentVersion}`); + } + for (const migration of migrations) { + if (migration.version <= row.version) + continue; + for (const statement of migration.statements) + await transaction.exec(statement); + await transaction.run(`UPDATE ${table} SET version = ? WHERE singleton = 1`, migration.version); + } + }); +} + +// node_modules/@earendil-works/pi-durable/dist/storage/sqlite/schema.js +var TABLE_NAMES = { + durableSchema: "durable_schema", + durableMetadata: "durable_metadata", + recordIds: "record_ids", + conversations: "conversations", + entries: "entries", + tasks: "tasks", + submissions: "submissions", + documents: "documents", + documentRevisions: "document_revisions" +}; +var INDEX_NAMES = { + conversationsByOwnerConversation: "conversations_by_owner_conversation", + conversationsByOwnerTask: "conversations_by_owner_task", + entriesByConversation: "entries_by_conversation", + entryHeadsByConversation: "entry_heads_by_conversation", + tasksByStatus: "tasks_by_status", + tasksByConversation: "tasks_by_conversation", + tasksByKind: "tasks_by_kind", + tasksByAbortRequested: "tasks_by_abort_requested", + tasksByBackground: "tasks_by_background", + submissionsByRequest: "submissions_by_request", + submissionsByConversation: "submissions_by_conversation", + submissionsByStatus: "submissions_by_status", + documentsByAddress: "documents_by_address", + documentsByScope: "documents_by_scope", + documentsByScopeKind: "documents_by_scope_kind", + documentRevisionsByKind: "document_revisions_by_kind" +}; +function prefixIdentifiers(names2, prefix) { + return Object.fromEntries(Object.entries(names2).map(([key, name]) => [key, `${prefix}${name}`])); +} +function createSqliteSchema(prefix) { + if (prefix !== void 0) + validateSqliteSchemaPrefix(prefix); + const actualPrefix = prefix ?? ""; + return { + tables: prefixIdentifiers(TABLE_NAMES, actualPrefix), + indexes: prefixIdentifiers(INDEX_NAMES, actualPrefix) + }; +} +var DEFAULT_SQLITE_SCHEMA = createSqliteSchema(); + +// node_modules/@earendil-works/pi-durable/dist/storage/sqlite/migrations.js +function createSqliteMigrations(schema) { + const { tables, indexes } = schema; + const initialSchema = [ + `CREATE TABLE ${tables.durableMetadata} ( + singleton INTEGER PRIMARY KEY CHECK (singleton = 1), + next_id TEXT NOT NULL, + next_seq INTEGER NOT NULL + ) STRICT`, + `INSERT INTO ${tables.durableMetadata} (singleton, next_id, next_seq) VALUES (1, '2', 1)`, + `CREATE TABLE ${tables.recordIds} ( + id INTEGER PRIMARY KEY, + record_type TEXT NOT NULL CHECK (record_type IN ('conversation', 'entry', 'task', 'submission', 'document')) + ) STRICT`, + `CREATE TABLE ${tables.conversations} ( + id INTEGER PRIMARY KEY, + owner_conversation_id INTEGER, + owner_task_id INTEGER, + record TEXT NOT NULL CHECK (json_valid(record)) + ) STRICT`, + `CREATE INDEX ${indexes.conversationsByOwnerConversation} ON ${tables.conversations} (owner_conversation_id, id)`, + `CREATE INDEX ${indexes.conversationsByOwnerTask} ON ${tables.conversations} (owner_task_id, id)`, + `CREATE TABLE ${tables.entries} ( + id INTEGER PRIMARY KEY, + conversation_id INTEGER NOT NULL, + head INTEGER, + commit_seq INTEGER NOT NULL, + record TEXT NOT NULL CHECK (json_valid(record)) + ) STRICT`, + `CREATE INDEX ${indexes.entriesByConversation} ON ${tables.entries} (conversation_id, id DESC)`, + `CREATE INDEX ${indexes.entryHeadsByConversation} ON ${tables.entries} (conversation_id, id DESC) WHERE head IS NOT NULL`, + `CREATE TABLE ${tables.tasks} ( + id INTEGER PRIMARY KEY, + conversation_id INTEGER NOT NULL, + kind TEXT NOT NULL, + status TEXT NOT NULL CHECK (status IN ('pending', 'running', 'waiting', 'completing', 'terminal')), + abort_requested INTEGER NOT NULL CHECK (abort_requested IN (0, 1)), + background INTEGER NOT NULL CHECK (background IN (0, 1)), + record TEXT NOT NULL CHECK (json_valid(record)) + ) STRICT`, + `CREATE INDEX ${indexes.tasksByStatus} ON ${tables.tasks} (status, id)`, + `CREATE INDEX ${indexes.tasksByConversation} ON ${tables.tasks} (conversation_id, id)`, + `CREATE INDEX ${indexes.tasksByKind} ON ${tables.tasks} (kind, id)`, + `CREATE INDEX ${indexes.tasksByAbortRequested} ON ${tables.tasks} (abort_requested, id)`, + `CREATE INDEX ${indexes.tasksByBackground} ON ${tables.tasks} (background, id)`, + `CREATE TABLE ${tables.submissions} ( + id INTEGER PRIMARY KEY, + conversation_id INTEGER NOT NULL, + request_id TEXT, + status TEXT NOT NULL CHECK (status IN ('queued', 'placed', 'done', 'unanswered')), + record TEXT NOT NULL CHECK (json_valid(record)) + ) STRICT`, + `CREATE INDEX ${indexes.submissionsByRequest} ON ${tables.submissions} (conversation_id, request_id)`, + `CREATE INDEX ${indexes.submissionsByConversation} ON ${tables.submissions} (conversation_id, id)`, + `CREATE INDEX ${indexes.submissionsByStatus} ON ${tables.submissions} (status, id)`, + `CREATE TABLE ${tables.documents} ( + id INTEGER PRIMARY KEY, + kind TEXT NOT NULL, + family INTEGER NOT NULL CHECK (family IN (0, 1)), + key_value TEXT NOT NULL, + scope_kind TEXT NOT NULL CHECK (scope_kind IN ('session', 'conversation', 'task')), + owner_id INTEGER NOT NULL, + created_at INTEGER NOT NULL, + retired_at INTEGER, + record TEXT NOT NULL CHECK (json_valid(record)) + ) STRICT`, + `CREATE INDEX ${indexes.documentsByAddress} + ON ${tables.documents} (kind, scope_kind, owner_id, family, key_value, created_at DESC, retired_at)`, + `CREATE INDEX ${indexes.documentsByScope} ON ${tables.documents} (scope_kind, owner_id, id)`, + `CREATE INDEX ${indexes.documentsByScopeKind} ON ${tables.documents} (scope_kind, owner_id, kind, id)`, + `CREATE TABLE ${tables.documentRevisions} ( + document_id INTEGER NOT NULL, + seq INTEGER NOT NULL, + kind TEXT NOT NULL CHECK (kind IN ('base', 'delta')), + version INTEGER NOT NULL, + content TEXT NOT NULL CHECK (json_valid(content)), + PRIMARY KEY (document_id, seq) + ) STRICT`, + `CREATE INDEX ${indexes.documentRevisionsByKind} ON ${tables.documentRevisions} (document_id, kind, seq DESC)` + ]; + const rebuiltTasks = `${tables.tasks}_v2`; + const sleepingStatus = [ + `CREATE TABLE ${rebuiltTasks} ( + id INTEGER PRIMARY KEY, + conversation_id INTEGER NOT NULL, + kind TEXT NOT NULL, + status TEXT NOT NULL CHECK (status IN ('pending', 'running', 'waiting', 'sleeping', 'completing', 'terminal')), + abort_requested INTEGER NOT NULL CHECK (abort_requested IN (0, 1)), + background INTEGER NOT NULL CHECK (background IN (0, 1)), + record TEXT NOT NULL CHECK (json_valid(record)) + ) STRICT`, + `INSERT INTO ${rebuiltTasks} (id, conversation_id, kind, status, abort_requested, background, record) + SELECT id, conversation_id, kind, status, abort_requested, background, record FROM ${tables.tasks}`, + `DROP TABLE ${tables.tasks}`, + `ALTER TABLE ${rebuiltTasks} RENAME TO ${tables.tasks}`, + `CREATE INDEX ${indexes.tasksByStatus} ON ${tables.tasks} (status, id)`, + `CREATE INDEX ${indexes.tasksByConversation} ON ${tables.tasks} (conversation_id, id)`, + `CREATE INDEX ${indexes.tasksByKind} ON ${tables.tasks} (kind, id)`, + `CREATE INDEX ${indexes.tasksByAbortRequested} ON ${tables.tasks} (abort_requested, id)`, + `CREATE INDEX ${indexes.tasksByBackground} ON ${tables.tasks} (background, id)` + ]; + return [ + { version: 1, statements: initialSchema }, + { version: 2, statements: sleepingStatus } + ]; +} +var SQLITE_MIGRATIONS = createSqliteMigrations(DEFAULT_SQLITE_SCHEMA); +var CURRENT_SQLITE_SCHEMA_VERSION = SQLITE_MIGRATIONS.at(-1)?.version ?? 0; +async function applySqliteMigrationsForSchema(database, schema, migrations = createSqliteMigrations(schema)) { + await applySqliteMigrations(database, migrations, { table: schema.tables.durableSchema }); +} + +// node_modules/@earendil-works/pi-durable/dist/storage/sqlite/storage.js +var parseJson = (value2) => JSON.parse(value2); +var encodeJson = (value2) => JSON.stringify(value2); +var encodeIndexedString = (value2) => JSON.stringify(value2); +var cursorId = (cursor) => { + const after = cursor?.after; + if (after === void 0) + return void 0; + if (typeof after !== "number" || !Number.isSafeInteger(after)) + throw new StorageRequestError("Invalid storage cursor"); + return idFromNumber(after); +}; +var page = (values, limit, order = "ascending") => { + const items = values.slice(0, limit); + if (values.length <= limit) + return { items }; + return { items, next: nextCursor(items.at(-1).id, order) }; +}; +var scanSql = (start) => start.order === "ascending" ? { clause: "id > ?", param: start.after ?? -1, direction: "ASC" } : { clause: "id < ?", param: start.after ?? Number.MAX_SAFE_INTEGER, direction: "DESC" }; +var scopeColumns = (scope) => { + switch (scope.kind) { + case "session": + return { scopeKind: "session", ownerId: 0 }; + case "conversation": + return { scopeKind: "conversation", ownerId: scope.conversationId }; + case "task": + return { scopeKind: "task", ownerId: scope.taskId }; + } +}; +var addressParts = (address) => { + const scope = scopeColumns(address.scope); + return { + kind: encodeIndexedString(address.kind), + ...scope, + family: address.key === void 0 ? 0 : 1, + keyValue: encodeIndexedString(address.key ?? "") + }; +}; +var addressKey = (address) => { + const parts = addressParts(address); + return JSON.stringify([parts.kind, parts.scopeKind, parts.ownerId, parts.family, parts.keyValue]); +}; +var isAliveAt = (record, at) => { + if (at === "current") + return record.retiredAt === void 0; + return record.createdAt <= at && (record.retiredAt === void 0 || at < record.retiredAt); +}; +var isCurrentOnly = (record) => record.scope.kind !== "conversation" || record.history === "latest"; +var writeId = (write) => { + switch (write.type) { + case "conversation": + case "entry": + case "task": + case "submission": + return write.value.id; + case "document.create": + case "document.copy": + return write.record.id; + case "document.change": + case "document.retire": + return void 0; + } +}; +function watchCalls(executor, calls) { + const watched = async (call) => { + try { + return await call(); + } catch (error) { + calls.failed = true; + throw error; + } + }; + return { + exec: (sql) => watched(() => executor.exec(sql)), + run: (sql, ...params) => watched(() => executor.run(sql, ...params)), + runCounted: (sql, ...params) => watched(() => executor.runCounted(sql, ...params)), + get: (sql, ...params) => watched(() => executor.get(sql, ...params)), + all: (sql, ...params) => watched(() => executor.all(sql, ...params)) + }; +} +var SqliteStorage = class _SqliteStorage { + db; + schema; + nextId; + closed = false; + closing; + admittedReads = 0; + readsDrained; + constructor(db, schema, nextId) { + this.db = db; + this.schema = schema; + this.nextId = nextId; + } + /** Initialize storage over an owned SQLite database facade. */ + static async open(db, options = {}) { + try { + const schema = createSqliteSchema(options.prefix); + await applySqliteMigrationsForSchema(db, schema); + const metadata = await db.get(`SELECT next_id, next_seq FROM ${schema.tables.durableMetadata} WHERE singleton = 1`); + if (metadata === void 0) + throw new Error("Durable SQLite metadata is missing"); + return new _SqliteStorage(db, schema, Number(metadata.next_id)); + } catch (error) { + try { + await db.close(); + } catch { + } + throw error; + } + } + /** + * Commit `batches` in one SQL transaction, each with its own seq. Each batch is checked before it writes anything, + * against the state the earlier ones left, and the first one refused ends the batch: the transaction commits the ones + * before it. No savepoints, which Durable Objects do not allow: the checks run before the writes instead. + */ + async commit(batches, receive, _context) { + this.assertOpen(); + const seqs = []; + let refused; + let candidateNextId = this.nextId; + await this.db.transaction(async (transaction) => { + const metadata = await this.readMetadata(transaction); + let next = metadata.next_seq; + const calls = { failed: false }; + const checked = watchCalls(transaction, calls); + for (const writes of batches) { + let documentActions; + calls.failed = false; + try { + documentActions = this.prepareDocumentActions(writes); + await this.checkBatch(checked, writes, documentActions); + } catch (error) { + if (calls.failed) + throw error; + refused = { error }; + break; + } + const seq = seqFromNumber(next++); + await this.applyBatch(transaction, writes, documentActions, seq); + seqs.push(seq); + candidateNextId = Math.max(candidateNextId, this.candidateNextId(writes)); + } + if (seqs.length > 0) { + await this.writeMetadata(transaction, Math.max(Number(metadata.next_id), candidateNextId), next); + } + }); + this.nextId = Math.max(this.nextId, candidateNextId); + for (const [index, seq] of seqs.entries()) + receive(index, seq); + return refused === void 0 ? { stored: seqs.length } : { stored: seqs.length, refused: refused.error }; + } + async readMetadata(executor) { + const metadata = await executor.get(`SELECT next_id, next_seq FROM ${this.schema.tables.durableMetadata} WHERE singleton = 1`); + if (metadata === void 0) + throw new Error("Durable SQLite metadata is missing"); + return metadata; + } + async writeMetadata(executor, nextId, nextSeq) { + await executor.run(`UPDATE ${this.schema.tables.durableMetadata} SET next_id = ?, next_seq = ? WHERE singleton = 1`, String(nextId), nextSeq); + } + /** Durable's own checks of one batch, copy resolution included, before it writes anything. */ + async checkBatch(executor, writes, documentActions) { + await this.checkGlobalIds(executor, writes); + await this.checkDocumentActions(executor, documentActions); + await this.resolveCopies(executor, documentActions); + } + async applyBatch(executor, writes, documentActions, seq) { + for (const write of writes) + await this.applyTableWrite(executor, write, seq); + await this.applyDocumentActions(executor, documentActions, seq); + } + async mintId() { + this.assertOpen(); + if (!Number.isSafeInteger(this.nextId)) + throw new Error("ID space is exhausted"); + return idFromNumber(this.nextId++); + } + async conversation(id, _context) { + this.assertOpen(); + const row = await this.db.get(`SELECT record FROM ${this.schema.tables.conversations} WHERE id = ?`, id); + return row === void 0 ? void 0 : parseJson(row.record); + } + async scanConversations(query, limit, cursor, _context) { + this.assertOpen(); + const start = scanStart(query.order, cursor, "ascending"); + const scan = scanSql(start); + const clauses = [scan.clause]; + const params = [scan.param]; + if (query.ownerConversationId !== void 0) { + clauses.push("owner_conversation_id = ?"); + params.push(query.ownerConversationId); + } + if (query.ownerTaskId !== void 0) { + clauses.push("owner_task_id = ?"); + params.push(query.ownerTaskId); + } + params.push(limit + 1); + const rows = await this.db.all(`SELECT record FROM ${this.schema.tables.conversations} WHERE ${clauses.join(" AND ")} ORDER BY id ${scan.direction} LIMIT ?`, ...params); + return page(rows.map((row) => parseJson(row.record)), limit, start.order); + } + entry(idOrConversationId, idOrContext, context) { + return this.admitRead(() => this.readEntry(idOrConversationId, idOrContext, context)); + } + findLatestHeadMarker(conversationId, atOrBeforeEntryId, _context) { + return this.admitRead(() => this.readLatestHeadMarker(conversationId, atOrBeforeEntryId)); + } + scanEntries(query, limit, cursor, _context) { + return this.admitRead(() => this.readEntries(query, limit, cursor)); + } + async readEntry(idOrConversationId, idOrContext, context) { + const id = context === void 0 ? idFromNumber(idOrConversationId) : typeof idOrContext === "number" ? idFromNumber(idOrContext) : void 0; + if (id === void 0) + throw new StorageRequestError("Storage.entry() requires an entry ID"); + let conversation; + if (context !== void 0) { + const conversationId = idFromNumber(idOrConversationId); + conversation = await this.readConversation(conversationId); + if (conversation === void 0) + throw new StorageRequestError(`Unknown conversation: ${conversationId}`); + } + const row = await this.db.get(`SELECT record, commit_seq FROM ${this.schema.tables.entries} WHERE id = ?`, id); + if (row === void 0) + return void 0; + const entry2 = parseJson(row.record); + if (conversation !== void 0) { + let upperEntryId = Number.POSITIVE_INFINITY; + while (conversation.id !== entry2.conversationId) { + if (conversation.parent === void 0) + return void 0; + upperEntryId = Math.min(upperEntryId, conversation.parent.at); + conversation = await this.readConversation(conversation.parent.conversationId); + } + if (entry2.id > upperEntryId) + return void 0; + } + return { entry: entry2, commitSeq: seqFromNumber(row.commit_seq) }; + } + async readLatestHeadMarker(conversationId, atOrBeforeEntryId) { + let conversation = await this.readConversation(conversationId); + if (conversation === void 0) + throw new StorageRequestError(`Unknown conversation: ${conversationId}`); + let upper = atOrBeforeEntryId; + while (true) { + const row = upper === void 0 ? await this.db.get(`SELECT record FROM ${this.schema.tables.entries} WHERE conversation_id = ? AND head IS NOT NULL ORDER BY id DESC LIMIT 1`, conversation.id) : await this.db.get(`SELECT record FROM ${this.schema.tables.entries} WHERE conversation_id = ? AND head IS NOT NULL AND id <= ? ORDER BY id DESC LIMIT 1`, conversation.id, upper); + if (row !== void 0) + return parseJson(row.record); + if (conversation.parent === void 0) + return void 0; + upper = upper === void 0 ? conversation.parent.at : Math.min(upper, conversation.parent.at); + conversation = await this.readConversation(conversation.parent.conversationId); + } + } + async readEntries(query, limit, cursor) { + const { order, after } = scanStart(query.order, cursor, "descending"); + if (order === "ascending") + return this.readEntriesAscending(query, limit, after); + let conversation = await this.readConversation(query.conversationId); + if (conversation === void 0) + throw new StorageRequestError(`Unknown conversation: ${query.conversationId}`); + let upper = query.maxEntryId; + if (after !== void 0) + upper = Math.min(upper ?? Number.MAX_SAFE_INTEGER, after - 1); + const values = []; + while (true) { + const clauses = ["conversation_id = ?"]; + const params = [conversation.id]; + if (query.minEntryId !== void 0) { + clauses.push("id >= ?"); + params.push(query.minEntryId); + } + if (upper !== void 0) { + clauses.push("id <= ?"); + params.push(upper); + } + params.push(limit + 1 - values.length); + const rows = await this.db.all(`SELECT record FROM ${this.schema.tables.entries} WHERE ${clauses.join(" AND ")} ORDER BY id DESC LIMIT ?`, ...params); + values.push(...rows.map((row) => parseJson(row.record))); + if (values.length > limit || conversation.parent === void 0) + break; + upper = upper === void 0 ? conversation.parent.at : Math.min(upper, conversation.parent.at); + if (query.minEntryId !== void 0 && upper < query.minEntryId) + break; + conversation = await this.readConversation(conversation.parent.conversationId); + } + return page(values, limit, "descending"); + } + /** Oldest first: the fork chain's segments from the root conversation forward, each up to its fork point. */ + async readEntriesAscending(query, limit, after) { + const segments = []; + let conversation = await this.readConversation(query.conversationId); + if (conversation === void 0) + throw new StorageRequestError(`Unknown conversation: ${query.conversationId}`); + let upper = query.maxEntryId; + while (true) { + segments.push({ conversationId: conversation.id, upper }); + if (conversation.parent === void 0) + break; + upper = upper === void 0 ? conversation.parent.at : Math.min(upper, conversation.parent.at); + if (query.minEntryId !== void 0 && upper < query.minEntryId) + break; + conversation = await this.readConversation(conversation.parent.conversationId); + } + let lower = query.minEntryId; + if (after !== void 0) + lower = Math.max(lower ?? after + 1, after + 1); + const values = []; + for (const segment of segments.reverse()) { + const clauses = ["conversation_id = ?"]; + const params = [segment.conversationId]; + if (lower !== void 0) { + clauses.push("id >= ?"); + params.push(lower); + } + if (segment.upper !== void 0) { + clauses.push("id <= ?"); + params.push(segment.upper); + } + params.push(limit + 1 - values.length); + const rows = await this.db.all(`SELECT record FROM ${this.schema.tables.entries} WHERE ${clauses.join(" AND ")} ORDER BY id ASC LIMIT ?`, ...params); + values.push(...rows.map((row) => parseJson(row.record))); + if (values.length > limit) + break; + } + return page(values, limit, "ascending"); + } + async task(id, _context) { + this.assertOpen(); + const row = await this.db.get(`SELECT record FROM ${this.schema.tables.tasks} WHERE id = ?`, id); + return row === void 0 ? void 0 : parseJson(row.record); + } + async scanTasks(query, limit, cursor, _context) { + this.assertOpen(); + const start = scanStart(query.order, cursor, "ascending"); + const scan = scanSql(start); + const clauses = [scan.clause]; + const params = [scan.param]; + if (query.conversationId !== void 0) { + clauses.push("conversation_id = ?"); + params.push(query.conversationId); + } + if (query.kind !== void 0) { + clauses.push("kind = ?"); + params.push(encodeIndexedString(query.kind)); + } + if (query.status !== void 0) { + clauses.push("status = ?"); + params.push(query.status); + } + if (query.abortRequested !== void 0) { + clauses.push("abort_requested = ?"); + params.push(query.abortRequested ? 1 : 0); + } + if (query.background !== void 0) { + clauses.push("background = ?"); + params.push(query.background ? 1 : 0); + } + params.push(limit + 1); + const rows = await this.db.all(`SELECT record FROM ${this.schema.tables.tasks} WHERE ${clauses.join(" AND ")} ORDER BY id ${scan.direction} LIMIT ?`, ...params); + return page(rows.map((row) => parseJson(row.record)), limit, start.order); + } + async submission(id, _context) { + this.assertOpen(); + const row = await this.db.get(`SELECT record FROM ${this.schema.tables.submissions} WHERE id = ?`, id); + return row === void 0 ? void 0 : parseJson(row.record); + } + async scanSubmissions(query, limit, cursor, _context) { + this.assertOpen(); + const start = scanStart(query.order, cursor, "ascending"); + const scan = scanSql(start); + const clauses = [scan.clause]; + const params = [scan.param]; + if (query.conversationId !== void 0) { + clauses.push("conversation_id = ?"); + params.push(query.conversationId); + } + if (query.status !== void 0) { + clauses.push("status = ?"); + params.push(query.status); + } + params.push(limit + 1); + const rows = await this.db.all(`SELECT record FROM ${this.schema.tables.submissions} WHERE ${clauses.join(" AND ")} ORDER BY id ${scan.direction} LIMIT ?`, ...params); + return page(rows.map((row) => parseJson(row.record)), limit, start.order); + } + async submissionByRequest(conversationId, requestId, _context) { + this.assertOpen(); + const row = await this.db.get(`SELECT record FROM ${this.schema.tables.submissions} WHERE conversation_id = ? AND request_id = ?`, conversationId, encodeIndexedString(requestId)); + return row === void 0 ? void 0 : parseJson(row.record); + } + async findDocument(address, at, _context) { + this.assertOpen(); + const parts = addressParts(address); + const sql = at === "current" ? `SELECT record FROM ${this.schema.tables.documents} + WHERE kind = ? AND scope_kind = ? AND owner_id = ? AND family = ? AND key_value = ? + AND retired_at IS NULL ORDER BY created_at DESC LIMIT 1` : `SELECT record FROM ${this.schema.tables.documents} + WHERE kind = ? AND scope_kind = ? AND owner_id = ? AND family = ? AND key_value = ? + AND created_at <= ? AND (retired_at IS NULL OR retired_at > ?) + ORDER BY created_at DESC LIMIT 1`; + const params = [parts.kind, parts.scopeKind, parts.ownerId, parts.family, parts.keyValue]; + if (at !== "current") + params.push(at, at); + const row = await this.db.get(sql, ...params); + return row === void 0 ? void 0 : parseJson(row.record); + } + async document(id, at, _context) { + this.assertOpen(); + return this.db.transaction((transaction) => this.materializeDocument(transaction, id, at)); + } + async scanDocuments(query, limit, cursor, _context) { + this.assertOpen(); + const scope = scopeColumns(query.scope); + const clauses = ["scope_kind = ?", "owner_id = ?", "id > ?"]; + const params = [scope.scopeKind, scope.ownerId, cursorId(cursor) ?? -1]; + if (query.kind !== void 0) { + clauses.push("kind = ?"); + params.push(encodeIndexedString(query.kind)); + } + if (query.at === "current") { + clauses.push("retired_at IS NULL"); + } else { + clauses.push("created_at <= ?", "(retired_at IS NULL OR retired_at > ?)"); + params.push(query.at, query.at); + } + params.push(limit + 1); + const rows = await this.db.all(`SELECT record FROM ${this.schema.tables.documents} WHERE ${clauses.join(" AND ")} ORDER BY id LIMIT ?`, ...params); + return page(rows.map((row) => parseJson(row.record)), limit); + } + close(_context) { + if (this.closing === void 0) { + this.closed = true; + this.closing = this.closeDatabase(); + } + return this.closing; + } + async closeDatabase() { + if (this.admittedReads > 0) { + await new Promise((resolve2) => { + this.readsDrained = resolve2; + }); + } + await this.db.close(); + } + /** + * Run a read that issues several queries. Close waits for admitted reads, so their later queries never reach a + * closed database. Single-query reads and transactions are already ordered before close by the database. + */ + async admitRead(read2) { + this.assertOpen(); + this.admittedReads++; + try { + return await read2(); + } finally { + if (--this.admittedReads === 0) + this.readsDrained?.(); + } + } + async readConversation(id) { + const row = await this.db.get(`SELECT record FROM ${this.schema.tables.conversations} WHERE id = ?`, id); + return row === void 0 ? void 0 : parseJson(row.record); + } + async materializeDocument(executor, id, at) { + const row = await executor.get(`SELECT record FROM ${this.schema.tables.documents} WHERE id = ?`, id); + if (row === void 0) + return void 0; + const record = parseJson(row.record); + if (at !== "current" && isCurrentOnly(record)) { + throw new StorageRequestError(`Document ${id} does not retain historical content`); + } + if (!isAliveAt(record, at)) + return void 0; + const upper = at === "current" ? Number.MAX_SAFE_INTEGER : at; + const base = await executor.get(`SELECT seq, kind, version, content FROM ${this.schema.tables.documentRevisions} + WHERE document_id = ? AND kind = 'base' AND seq <= ? ORDER BY seq DESC LIMIT 1`, id, upper); + if (base === void 0) + throw new Error(`Document ${id} is missing a required base`); + let value2 = parseJson(base.content); + const tail = await executor.all(`SELECT seq, kind, version, content FROM ${this.schema.tables.documentRevisions} + WHERE document_id = ? AND seq > ? AND seq <= ? ORDER BY seq`, id, base.seq, upper); + for (const revision of tail) { + if (revision.kind !== "delta" || revision.version !== base.version) { + throw new Error(`Document ${id} crosses a stored version boundary without a base`); + } + value2 = apply(value2, parseJson(revision.content)); + } + return { record, version: base.version, value: value2, deltasSinceBase: tail.length }; + } + candidateNextId(writes) { + let nextId = this.nextId; + for (const write of writes) { + const id = writeId(write); + if (id !== void 0) + nextId = Math.max(nextId, id + 1); + } + return nextId; + } + async checkGlobalIds(executor, writes) { + const claimed = /* @__PURE__ */ new Map(); + for (const write of writes) { + if (write.type === "document.change" || write.type === "document.retire") + continue; + const document = write.type === "document.create" || write.type === "document.copy"; + const table = document ? "document" : write.type; + const id = document ? write.record.id : write.value.id; + const existing = (await executor.get(`SELECT record_type FROM ${this.schema.tables.recordIds} WHERE id = ?`, id))?.record_type; + const earlier = claimed.get(id); + if (table === "conversation" || table === "entry" || table === "document") { + if (existing !== void 0) + throw new Error(`ID ${id} already belongs to ${existing}`); + if (earlier !== void 0) + throw new Error(`ID ${id} is written more than once`); + } else { + if (existing !== void 0 && existing !== table) + throw new Error(`ID ${id} already belongs to ${existing}`); + if (earlier !== void 0 && earlier !== table) + throw new Error(`ID ${id} is written as two record types`); + } + claimed.set(id, table); + } + } + prepareDocumentActions(writes) { + const actions = /* @__PURE__ */ new Map(); + for (const write of writes) { + if (write.type !== "document.create" && write.type !== "document.copy" && write.type !== "document.change" && write.type !== "document.retire") { + continue; + } + const id = write.type === "document.create" || write.type === "document.copy" ? write.record.id : write.id; + let action = actions.get(id); + if (action === void 0) { + action = { retire: false }; + actions.set(id, action); + } + switch (write.type) { + case "document.create": + if (action.create !== void 0 || action.content !== void 0 || action.copy !== void 0) { + throw new Error(`Document ${id} has more than one content command`); + } + action.create = write.record; + action.content = write.content; + break; + case "document.copy": + if (action.create !== void 0 || action.content !== void 0 || action.copy !== void 0) { + throw new Error(`Document ${id} has more than one content command`); + } + action.create = write.record; + action.copy = write.source; + break; + case "document.change": + if (action.content !== void 0 || action.copy !== void 0) { + throw new Error(`Document ${id} has more than one content command`); + } + action.content = write.content; + break; + case "document.retire": + if (action.retire) + throw new Error(`Document ${id} is retired more than once`); + action.retire = true; + break; + } + } + return actions; + } + async checkDocumentActions(executor, actions) { + const liveCounts = /* @__PURE__ */ new Map(); + for (const [id, action] of actions) { + if (action.copy !== void 0 && actions.has(action.copy.id)) { + throw new Error(`Document copy ${id} source is changed in the copy batch`); + } + const row = await executor.get(`SELECT record FROM ${this.schema.tables.documents} WHERE id = ?`, id); + const existing = row === void 0 ? void 0 : parseJson(row.record); + if (action.create === void 0 && existing === void 0) + throw new Error(`Unknown document: ${id}`); + if (action.create !== void 0 && existing !== void 0) + throw new Error(`Document ${id} already exists`); + if (existing?.retiredAt !== void 0) + throw new Error(`Document ${id} is retired`); + if (action.content?.kind === "delta") { + const previous = await executor.get(`SELECT version FROM ${this.schema.tables.documentRevisions} WHERE document_id = ? ORDER BY seq DESC LIMIT 1`, id); + if (previous === void 0) + throw new Error(`Document ${id} delta has no base`); + if (previous.version !== action.content.version) { + throw new Error(`Document ${id} version transition requires a base`); + } + } + const record = action.create ?? existing; + const key = addressKey(record); + let live = liveCounts.get(key); + if (live === void 0) + live = await this.currentDocumentId(executor, record) === void 0 ? 0 : 1; + if (action.retire && existing !== void 0) + live--; + if (action.create !== void 0 && !action.retire) + live++; + liveCounts.set(key, live); + } + for (const live of liveCounts.values()) { + if (live > 1) + throw new Error("Document address already has a current incarnation"); + } + } + async currentDocumentId(executor, address) { + const parts = addressParts(address); + const id = (await executor.get(`SELECT id FROM ${this.schema.tables.documents} + WHERE kind = ? AND scope_kind = ? AND owner_id = ? AND family = ? AND key_value = ? AND retired_at IS NULL + LIMIT 1`, parts.kind, parts.scopeKind, parts.ownerId, parts.family, parts.keyValue))?.id; + return id === void 0 ? void 0 : idFromNumber(id); + } + async applyTableWrite(executor, write, seq) { + switch (write.type) { + case "conversation": + await this.claimId(executor, write.value.id, "conversation"); + await executor.run(`INSERT INTO ${this.schema.tables.conversations} (id, owner_conversation_id, owner_task_id, record) VALUES (?, ?, ?, ?)`, write.value.id, write.value.owner?.conversationId ?? null, write.value.owner?.taskId ?? null, encodeJson(write.value)); + break; + case "entry": + await this.claimId(executor, write.value.id, "entry"); + await executor.run(`INSERT INTO ${this.schema.tables.entries} (id, conversation_id, head, commit_seq, record) VALUES (?, ?, ?, ?, ?)`, write.value.id, write.value.conversationId, write.value.head ?? null, seq, encodeJson(write.value)); + break; + case "task": + await this.claimId(executor, write.value.id, "task"); + await executor.run(`INSERT INTO ${this.schema.tables.tasks} (id, conversation_id, kind, status, abort_requested, background, record) + VALUES (?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(id) DO UPDATE SET conversation_id = excluded.conversation_id, kind = excluded.kind, + status = excluded.status, abort_requested = excluded.abort_requested, + background = excluded.background, record = excluded.record`, write.value.id, write.value.conversationId, encodeIndexedString(write.value.kind), write.value.state.status, write.value.abortRequested ? 1 : 0, write.value.background ? 1 : 0, encodeJson(write.value)); + break; + case "submission": + await this.claimId(executor, write.value.id, "submission"); + await executor.run(`INSERT INTO ${this.schema.tables.submissions} (id, conversation_id, request_id, status, record) VALUES (?, ?, ?, ?, ?) + ON CONFLICT(id) DO UPDATE SET conversation_id = excluded.conversation_id, + request_id = excluded.request_id, status = excluded.status, record = excluded.record`, write.value.id, write.value.conversationId, write.value.requestId === void 0 ? null : encodeIndexedString(write.value.requestId), write.value.status, encodeJson(write.value)); + break; + case "document.create": + case "document.copy": + case "document.change": + case "document.retire": + break; + } + } + async claimId(executor, id, table) { + await executor.run(`INSERT OR IGNORE INTO ${this.schema.tables.recordIds} (id, record_type) VALUES (?, ?)`, id, table); + } + /** Resolve each copy to a base of its source's state before this batch, which may not change the source. */ + async resolveCopies(executor, actions) { + for (const [id, action] of actions) { + if (action.copy === void 0) + continue; + try { + const stored = await this.materializeDocument(executor, action.copy.id, action.copy.at); + if (stored === void 0) + throw new Error(`Fork source document ${action.copy.id} cannot be read`); + const create = action.create; + if (stored.record.scope.kind !== "conversation" || create.scope.kind !== "conversation" || stored.record.kind !== create.kind || stored.record.key !== create.key || stored.record.history !== create.history || stored.record.fork !== create.fork) { + throw new Error(`Fork source document ${action.copy.id} does not match the copied record`); + } + action.content = { kind: "base", version: stored.version, value: stored.value }; + } catch (error) { + throw new Error(`Document copy ${id} was rejected`, { cause: error }); + } + } + } + async applyDocumentActions(executor, actions, seq) { + for (const [id, action] of actions) { + const content = action.content; + let record; + if (action.create !== void 0) { + record = { + ...action.create, + createdAt: seq, + ...action.retire ? { retiredAt: seq } : {} + }; + const parts = addressParts(record); + await this.claimId(executor, id, "document"); + await executor.run(`INSERT INTO ${this.schema.tables.documents} + (id, kind, family, key_value, scope_kind, owner_id, created_at, retired_at, record) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, id, parts.kind, parts.family, parts.keyValue, parts.scopeKind, parts.ownerId, seq, action.retire ? seq : null, encodeJson(record)); + } else { + const row = await executor.get(`SELECT record FROM ${this.schema.tables.documents} WHERE id = ?`, id); + record = parseJson(row.record); + } + if (content !== void 0) { + if (content.kind === "base" && isCurrentOnly(record)) { + await executor.run(`DELETE FROM ${this.schema.tables.documentRevisions} WHERE document_id = ?`, id); + } + const encodedContent = content.kind === "base" ? encodeJson(content.value) : encodeJson(content.ops); + await executor.run(`INSERT INTO ${this.schema.tables.documentRevisions} (document_id, seq, kind, version, content) VALUES (?, ?, ?, ?, ?)`, id, seq, content.kind, content.version, encodedContent); + } + if (action.retire) { + if (action.create === void 0) { + record = { ...record, retiredAt: seq }; + await executor.run(`UPDATE ${this.schema.tables.documents} SET retired_at = ?, record = ? WHERE id = ?`, seq, encodeJson(record), id); + } + if (isCurrentOnly(record)) { + await executor.run(`DELETE FROM ${this.schema.tables.documentRevisions} WHERE document_id = ?`, id); + } + } + } + } + assertOpen() { + if (this.closed) + throw new Error("SqliteStorage is closed"); + } +}; + +// sqlite.ts +var op = (kind, sql = "", params = []) => Deno.core.ops.op_sql(kind, sql, params.map((p) => p instanceof Uint8Array ? Array.from(p) : typeof p === "bigint" ? Number(p) : p)); +async function openStorage(path2) { + op("open", path2); + let tail = Promise.resolve(); + const queue = (call) => { + const result = tail.then(call); + tail = result.then(() => { + }, () => { + }); + return result; + }; + const executor = { + exec: async (sql) => { + op("exec", sql); + }, + run: async (sql, ...params) => { + op("run", sql, params); + }, + runCounted: async (sql, ...params) => op("run", sql, params).changes, + get: async (sql, ...params) => (await executor.all(sql, ...params))[0], + all: async (sql, ...params) => op("all", sql, params).map((row) => Object.fromEntries(Object.entries(row).map(([key, value2]) => [key, Array.isArray(value2) ? new Uint8Array(value2) : value2]))) + }; + const database = Object.fromEntries(Object.entries(executor).map(([name, method]) => [name, (...args) => queue(() => method(...args))])); + database.transaction = (callback) => queue(async () => { + op("exec", "BEGIN IMMEDIATE"); + try { + const result = await callback(executor); + op("exec", "COMMIT"); + return result; + } catch (error) { + op("exec", "ROLLBACK"); + throw error; + } + }); + database.close = () => queue(async () => { + op("close"); + }); + return SqliteStorage.open(database); +} + +// chain-test.ts +var assert = (condition, message) => { + if (!condition) throw new Error(message); +}; +var equal = (a, b) => assert(JSON.stringify(a) === JSON.stringify(b), "Wrong conversation chain"); +var Started2 = defineDoc({ + kind: "board.started", + version: 1, + checkpointWhen: () => false, + scope: "conversation", + history: "latest", + fork: "initial", + initial: () => ({}) +}); +var Anchor = defineTask({ + name: "chain.anchor", + version: 1, + initial: () => ({ phase: "done" }), + phases: { done: (_task, runtime, ctx) => runtime.commit(() => ({ status: "terminal", outcome: { status: "completed", result: null } }), ctx) } +}); +var models = createModels(); +var faux = fauxProvider({ tokensPerSecond: 1e5 }); +models.setProvider(faux.provider); +var registry = createRegistry(); +registry.install(CodingTools); +registry.install(defineExtension({ name: "chain", tasks: [Anchor] })); +var harness = await Harness.open(await openStorage(globalThis.storagePath), { + models, + registry, + env: async (target) => environment("/", await chain(target.conversationId)) +}, BACKGROUND_CONTEXT); +var chain = conversationChains(harness, BACKGROUND_CONTEXT); +var agent = { model: { provider: "faux", modelId: "faux-1" }, extensions: [CodingTools], cwd: "/" }; +var root = await harness.root(BACKGROUND_CONTEXT, { agent }); +var owned = async (parent) => parent.commit(async (tx) => { + const taskId = await tx.createTask(Anchor, null, { ownership: { kind: "conversation" } }); + return tx.createConversation({ ownership: { kind: "task", taskId } }); +}, BACKGROUND_CONTEXT); +var records; +if (!globalThis.chainReady) { + const parentRecord = await owned(root); + const parent = await harness.conversation(parentRecord.id, BACKGROUND_CONTEXT); + const child = await owned(parent); + const entry2 = await parent.commit((tx) => tx.appendEntry(parent.id, { kind: "chain.origin", data: {} }), BACKGROUND_CONTEXT); + const fork = await parent.commit((tx) => tx.forkConversation(parent.id, entry2.id, { ownership: { kind: "ownerless" } }), BACKGROUND_CONTEXT); + const unrelated = await harness.commit((tx) => tx.createConversation({ ownership: { kind: "ownerless" } }), BACKGROUND_CONTEXT); + const branch = await harness.commit(async (tx) => { + const record = await tx.createConversation({ ownership: { kind: "ownerless" } }); + (await tx.doc(Started2, record.id)).by = parent.id; + return record; + }, BACKGROUND_CONTEXT); + const ownedFork = await root.commit(async (tx) => { + const taskId = await tx.createTask(Anchor, null, { ownership: { kind: "conversation" } }); + (await tx.doc(Started2, fork.id)).by = unrelated.id; + return tx.forkConversation(parent.id, fork.parent.at, { ownership: { kind: "task", taskId } }); + }, BACKGROUND_CONTEXT); + records = { parent: parentRecord, child, fork, branch, unrelated, ownedFork }; + await fs.write("/chain-ids", JSON.stringify(records)); + await harness.close(BACKGROUND_CONTEXT); +} else { + records = JSON.parse(await fs.read("/chain-ids")); + const { parent, child, fork, branch, unrelated, ownedFork } = records; + harness.resume(); + try { + for (const record of [child, fork, branch, unrelated]) { + const conversation = await harness.conversation(record.id, BACKGROUND_CONTEXT); + await conversation.configure(agent, BACKGROUND_CONTEXT); + faux.setResponses([ + fauxAssistantMessage([fauxToolCall("bash", { command: `curl http://host.sausage/whoami` }, { id: `identity-${record.id}` })], { stopReason: "toolUse" }), + fauxAssistantMessage([fauxToolCall("bash", { command: `curl -i http://127.0.0.1:${globalThis.chainPort}/token` }, { id: `fetch-${record.id}` })], { stopReason: "toolUse" }), + fauxAssistantMessage("done") + ]); + await (await conversation.submit({ type: "input", content: "Check inherited access" }, BACKGROUND_CONTEXT)).wait(BACKGROUND_CONTEXT); + const entries2 = (await conversation.context(BACKGROUND_CONTEXT)).entries; + const tools = entries2.filter((entry2) => entry2.kind === "pi.tool-result"); + const output = (entry2) => entry2.model.flatMap((message) => message.content ?? []).filter((block) => block.type === "text").map((block) => block.text).join(""); + const identity = JSON.parse(output(tools[0])); + equal(identity.conversations, record.id === unrelated.id ? [String(record.id)] : [String(record.id), String(parent.id), String(root.id)]); + const response = output(tools[1]); + assert(response.includes(record.id === unrelated.id ? "403" : "accepted"), "Wrong inherited authorization"); + } + equal(await chain(ownedFork.id), [String(ownedFork.id), String(root.id)]); + assert(await harness.snapshot(Started2, unrelated.id, BACKGROUND_CONTEXT) === void 0, "Missing starter must remain absent"); + assert(chain(child.id) === chain(child.id), "The chain must use its cached promise"); + const denied = await exec(["curl", "-i", `http://127.0.0.1:${globalThis.chainPort}/unrelated`], { cwd: "/", conversations: await chain(child.id) }); + assert(denied.stdout.includes("403"), "An unrelated grant must not cover the child"); + const delegated = await exec(["curl", "http://host.sausage/whoami"], { cwd: "/", conversations: await chain(child.id) }); + assert(delegated.code === 0, "Extension exec failed"); + equal(JSON.parse(delegated.stdout).conversations, [String(child.id), String(parent.id), String(root.id)]); + const cleared = await exec(["curl", "http://host.sausage/whoami"], { cwd: "/", conversations: [] }); + assert(cleared.code === 0, "Explicit empty chain failed"); + equal(JSON.parse(cleared.stdout).conversations, []); + } finally { + await harness.close(BACKGROUND_CONTEXT); + } +} diff --git a/host/js/chain-test.ts b/host/js/chain-test.ts new file mode 100644 index 0000000..b5fb4b5 --- /dev/null +++ b/host/js/chain-test.ts @@ -0,0 +1,81 @@ +// Real bash tools and extension commands retain the nearest-first grant ancestry (D47, D63). +import { BACKGROUND_CONTEXT as context } from '@earendil-works/chord/context'; +import { createModels } from '@earendil-works/pi-ai/models'; +import { fauxProvider, fauxAssistantMessage, fauxToolCall } from '@earendil-works/pi-ai/providers/faux'; +import { Harness, createRegistry, defineDoc, defineExtension, defineTask } from '@earendil-works/pi-durable'; +import { CodingTools } from '@earendil-works/pi-durable/tools'; +import { environment, conversationChains } from './environment'; +import { exec, fs } from './extensions'; +import { openStorage } from './sqlite'; +const assert = (condition: any, message: string) => { if (!condition) throw new Error(message); }; +const equal = (a: any, b: any) => assert(JSON.stringify(a) === JSON.stringify(b), 'Wrong conversation chain'); +const Started = defineDoc<{ by?: number }>({ kind: 'board.started', version: 1, checkpointWhen: () => false, + scope: 'conversation', history: 'latest', fork: 'initial', initial: () => ({}) }); +const Anchor = defineTask({ name: 'chain.anchor', version: 1, initial: () => ({ phase: 'done' }), + phases: { done: (_task: any, runtime: any, ctx: any) => runtime.commit(() => ({ status: 'terminal', outcome: { status: 'completed', result: null } }), ctx) } }); +const models = createModels(), faux = fauxProvider({ tokensPerSecond: 100000 }); models.setProvider(faux.provider); +const registry = createRegistry(); registry.install(CodingTools); registry.install(defineExtension({ name: 'chain', tasks: [Anchor] })); +const harness = await Harness.open(await openStorage(globalThis.storagePath), { models, registry, + env: async (target: any) => environment('/', await chain(target.conversationId)) }, context); +const chain = conversationChains(harness, context); +const agent = { model: { provider: 'faux', modelId: 'faux-1' }, extensions: [CodingTools], cwd: '/' }; +const root = await harness.root(context, { agent }); +const owned = async (parent: any) => parent.commit(async (tx: any) => { + const taskId = await tx.createTask(Anchor, null, { ownership: { kind: 'conversation' } }); + return tx.createConversation({ ownership: { kind: 'task', taskId } }); +}, context); +let records: any; +if (!globalThis.chainReady) { +const parentRecord = await owned(root); +const parent = await harness.conversation(parentRecord.id, context); +const child = await owned(parent); +const entry = await parent.commit((tx: any) => tx.appendEntry(parent.id, { kind: 'chain.origin', data: {} }), context); +const fork = await parent.commit((tx: any) => tx.forkConversation(parent.id, entry.id, { ownership: { kind: 'ownerless' } }), context); +const unrelated = await harness.commit((tx: any) => tx.createConversation({ ownership: { kind: 'ownerless' } }), context); +const branch = await harness.commit(async (tx: any) => { + const record = await tx.createConversation({ ownership: { kind: 'ownerless' } }); + (await tx.doc(Started, record.id)).by = parent.id; + return record; +}, context); +const ownedFork = await root.commit(async (tx: any) => { + const taskId = await tx.createTask(Anchor, null, { ownership: { kind: 'conversation' } }); + (await tx.doc(Started, fork.id)).by = unrelated.id; + return tx.forkConversation(parent.id, fork.parent.at, { ownership: { kind: 'task', taskId } }); +}, context); +records = { parent: parentRecord, child, fork, branch, unrelated, ownedFork }; +await fs.write('/chain-ids', JSON.stringify(records)); +await harness.close(context); +} else { +records = JSON.parse(await fs.read('/chain-ids')); +const { parent, child, fork, branch, unrelated, ownedFork } = records; +harness.resume(); +try { + for (const record of [child, fork, branch, unrelated]) { + const conversation = await harness.conversation(record.id, context); + await conversation.configure(agent, context); + faux.setResponses([ + fauxAssistantMessage([fauxToolCall('bash', { command: `curl http://host.sausage/whoami` }, { id: `identity-${record.id}` })], { stopReason: 'toolUse' }), + fauxAssistantMessage([fauxToolCall('bash', { command: `curl -i http://127.0.0.1:${globalThis.chainPort}/token` }, { id: `fetch-${record.id}` })], { stopReason: 'toolUse' }), + fauxAssistantMessage('done'), + ]); + await (await conversation.submit({ type: 'input', content: 'Check inherited access' }, context)).wait(context); + const entries = (await conversation.context(context)).entries; + const tools = entries.filter((entry: any) => entry.kind === 'pi.tool-result'); + const output = (entry: any) => entry.model.flatMap((message: any) => message.content ?? []).filter((block: any) => block.type === 'text').map((block: any) => block.text).join(''); + const identity = JSON.parse(output(tools[0])); + equal(identity.conversations, record.id === unrelated.id ? [String(record.id)] : [String(record.id), String(parent.id), String(root.id)]); + const response = output(tools[1]); + assert(response.includes(record.id === unrelated.id ? '403' : 'accepted'), 'Wrong inherited authorization'); + } + equal(await chain(ownedFork.id), [String(ownedFork.id), String(root.id)]); + assert(await harness.snapshot(Started, unrelated.id, context) === undefined, 'Missing starter must remain absent'); + assert(chain(child.id) === chain(child.id), 'The chain must use its cached promise'); + const denied = await exec(['curl', '-i', `http://127.0.0.1:${globalThis.chainPort}/unrelated`], { cwd: '/', conversations: await chain(child.id) }); + assert(denied.stdout.includes('403'), 'An unrelated grant must not cover the child'); + const delegated = await exec(['curl', 'http://host.sausage/whoami'], { cwd: '/', conversations: await chain(child.id) }); + assert(delegated.code === 0, 'Extension exec failed'); + equal(JSON.parse(delegated.stdout).conversations, [String(child.id), String(parent.id), String(root.id)]); + const cleared = await exec(['curl', 'http://host.sausage/whoami'], { cwd: '/', conversations: [] }); + assert(cleared.code === 0, 'Explicit empty chain failed'); equal(JSON.parse(cleared.stdout).conversations, []); +} finally { await harness.close(context); } +} diff --git a/host/js/environment.ts b/host/js/environment.ts index c0378ba..171fec2 100644 --- a/host/js/environment.ts +++ b/host/js/environment.ts @@ -1,5 +1,27 @@ // The coding tools see only world paths. Rust owns the VFS and process execution. import { LineScanner } from '@earendil-works/pi-durable/env'; +import { defineDoc } from '@earendil-works/pi-durable'; + +// D63: read the optional branch starter without creating a document. +const Started = defineDoc<{ by?: number }>({ kind: 'board.started', version: 1, + checkpointWhen: () => false, scope: 'conversation', history: 'latest', fork: 'initial', initial: () => ({}) }); +export function conversationChains(harness: any, context: any) { + const cache = new Map>(); + const chain = (id: number): Promise => { + let result = cache.get(id); + if (!result) { + result = (async () => { + const record = await harness.commit((tx: any) => tx.conversation(id), context); + const up = record?.owner?.conversationId ?? record?.parent?.conversationId ?? + (await harness.snapshot(Started, id, context))?.by; + return [String(id), ...(up === undefined ? [] : await chain(up))]; + })(); + cache.set(id, result); + } + return result; + }; + return chain; +} const ops = Deno.core.ops; const ok = (value: any) => ({ ok: true, value }); const path = (cwd: string, input: string) => { @@ -10,7 +32,8 @@ const path = (cwd: string, input: string) => { } return '/' + parts.join('/'); }; -export function environment(cwd = '/', conversation?: string | number) { +export function environment(cwd = '/', conversation?: string | number | string[]) { + const conversations = Array.isArray(conversation) ? conversation : conversation === undefined ? [] : [String(conversation)]; const call = (kind: string, p: string, bytes: number[] = []) => ops.op_file(kind, path(cwd, p), bytes); const env: any = { id: 'world', cwd, @@ -87,7 +110,7 @@ export function environment(cwd = '/', conversation?: string | number) { } }; try { - const rid = ops.op_exec(command, options?.cwd ?? cwd, options?.timeout, conversation === undefined ? undefined : String(conversation)); + const rid = ops.op_exec(command, options?.cwd ?? cwd, options?.timeout, undefined, undefined, conversations); const decoders = { stdout: new TextDecoder(), stderr: new TextDecoder() }; let result; while (true) { diff --git a/host/js/main.ts b/host/js/main.ts index eac2619..e564280 100644 --- a/host/js/main.ts +++ b/host/js/main.ts @@ -5,7 +5,7 @@ import { openaiProvider } from '@earendil-works/pi-ai/providers/openai'; import { Harness, AgentDoc, createRegistry } from '@earendil-works/pi-durable'; import { CodingTools } from '@earendil-works/pi-durable/tools'; import { openStorage } from './sqlite'; -import { environment } from './environment'; +import { environment, conversationChains } from './environment'; import { CredentialRequests, startRequests } from './credentials'; import { serve } from './input'; import { loadExtensions } from './extensions'; @@ -32,8 +32,9 @@ registry.install(CredentialRequests); export const storage = await openStorage(globalThis.storagePath); const harness = await Harness.open(storage, { models, registry, settings: { followUpMode: 'all', get extensions() { return registry.snapshot().installed(); } }, - env: (target: any) => environment(target.cwd ?? '/', target.conversationId), + env: async (target: any) => environment(target.cwd ?? '/', await chain(target.conversationId)), }, context); +const chain = conversationChains(harness, context); const root = await harness.root(context, { agent: { model: { provider: 'openai', modelId: model.id }, thinkingLevel: 'low', cwd: '/' } }); // T32 stored this exact built-in list in the console root. Use registry defaults now. const storedAgent = await harness.snapshot(AgentDoc, root.id, context); diff --git a/host/js/p2-test-bundle.js b/host/js/p2-test-bundle.js index e287af4..00e102d 100644 --- a/host/js/p2-test-bundle.js +++ b/host/js/p2-test-bundle.js @@ -21249,6 +21249,15 @@ function createCodingTools(options = {}) { var CodingTools = createCodingTools(); // environment.ts +var Started = defineDoc({ + kind: "board.started", + version: 1, + checkpointWhen: () => false, + scope: "conversation", + history: "latest", + fork: "initial", + initial: () => ({}) +}); var ops = Deno.core.ops; var ok = (value2) => ({ ok: true, value: value2 }); var path = (cwd, input) => { @@ -21260,6 +21269,7 @@ var path = (cwd, input) => { return "/" + parts.join("/"); }; function environment(cwd = "/", conversation) { + const conversations = Array.isArray(conversation) ? conversation : conversation === void 0 ? [] : [String(conversation)]; const call = (kind, p, bytes2 = []) => ops.op_file(kind, path(cwd, p), bytes2); const env = { id: "world", @@ -21338,7 +21348,7 @@ function environment(cwd = "/", conversation) { } }; try { - const rid = ops.op_exec(command, options?.cwd ?? cwd, options?.timeout, conversation === void 0 ? void 0 : String(conversation)); + const rid = ops.op_exec(command, options?.cwd ?? cwd, options?.timeout, void 0, void 0, conversations); const decoders = { stdout: new TextDecoder(), stderr: new TextDecoder() }; let result; while (true) { diff --git a/host/js/test-bundle.js b/host/js/test-bundle.js index e39606f..a7e35ca 100644 --- a/host/js/test-bundle.js +++ b/host/js/test-bundle.js @@ -21249,6 +21249,15 @@ function createCodingTools(options = {}) { var CodingTools = createCodingTools(); // environment.ts +var Started = defineDoc({ + kind: "board.started", + version: 1, + checkpointWhen: () => false, + scope: "conversation", + history: "latest", + fork: "initial", + initial: () => ({}) +}); var ops = Deno.core.ops; var ok = (value2) => ({ ok: true, value: value2 }); var path = (cwd, input) => { @@ -21260,6 +21269,7 @@ var path = (cwd, input) => { return "/" + parts.join("/"); }; function environment(cwd = "/", conversation) { + const conversations = Array.isArray(conversation) ? conversation : conversation === void 0 ? [] : [String(conversation)]; const call = (kind, p, bytes2 = []) => ops.op_file(kind, path(cwd, p), bytes2); const env = { id: "world", @@ -21338,7 +21348,7 @@ function environment(cwd = "/", conversation) { } }; try { - const rid = ops.op_exec(command, options?.cwd ?? cwd, options?.timeout, conversation === void 0 ? void 0 : String(conversation)); + const rid = ops.op_exec(command, options?.cwd ?? cwd, options?.timeout, void 0, void 0, conversations); const decoders = { stdout: new TextDecoder(), stderr: new TextDecoder() }; let result2; while (true) { diff --git a/host/tests/chain.rs b/host/tests/chain.rs new file mode 100644 index 0000000..5662f3f --- /dev/null +++ b/host/tests/chain.rs @@ -0,0 +1,170 @@ +//! A faux model runs real bash and curl with inherited conversation grants through the host hook. +use anyhow::{Result, ensure}; +use sausage::{ + api::Api, + cred::{Caller, Cred, grants::Grant, types::Subject}, + world::World, +}; +use serde_json::json; +use std::{ + path::{Path, PathBuf}, + sync::Arc, + time::Duration, +}; + +fn components() -> Result { + let userland = Path::new(env!("CARGO_MANIFEST_DIR")).join("../userland"); + let target = Path::new(env!("CARGO_TARGET_TMPDIR")).parent().unwrap(); + if !userland.join("cwd-init/init.o").is_file() { + ensure!( + std::process::Command::new("nu") + .args(["--no-config-file", "cwd-init/build.nu"]) + .current_dir(&userland) + .status()? + .success(), + "cwd fixture build failed" + ); + } + ensure!( + std::process::Command::new("cargo") + .args([ + "build", + "--release", + "--locked", + "--target", + "wasm32-wasip2", + "-j", + "2", + "-p", + "curl", + "--target-dir" + ]) + .arg(target) + .current_dir(&userland) + .status()? + .success(), + "curl fixture build failed" + ); + let release = target.join("wasm32-wasip2/release"); + if !release.join("brush.wasm").is_file() { + ensure!( + std::process::Command::new("nu") + .arg("brush/build.nu") + .current_dir(&userland) + .status()? + .success(), + "shell fixture build failed" + ); + } + Ok(release) +} + +#[tokio::test(flavor = "current_thread")] +async fn ancestry_reaches_the_real_hook() -> Result<()> { + let components = components()?; + let temp = tempfile::tempdir()?; + let world = World::open(temp.path())?; + let disk = temp.path().join("world"); + std::fs::create_dir_all(disk.join("bin"))?; + for (source, name) in [("curl.wasm", "curl.wasm"), ("brush.wasm", "sh.wasm")] { + std::fs::copy(components.join(source), disk.join("bin").join(name))?; + } + let server = Arc::new( + tiny_http::Server::http("127.0.0.1:0") + .map_err(|_| anyhow::anyhow!("fixture bind failed"))?, + ); + let port = server.server_addr().to_ip().unwrap().port(); + let cred = Arc::new(Cred::open(&world.host_dir)?); + let caller = Caller::default(); + let token = uuid::Uuid::new_v4().to_string(); + let api = Arc::new(Api::open( + cred.clone(), + &world.host_dir, + "localhost", + "http://localhost:8081", + )?); + let runtime = Arc::new(sausage::process::Runtime::new_with_hooks( + World { + root: world.root.clone(), + host_dir: world.host_dir.clone(), + processes: world.processes.clone(), + }, + cred.clone(), + sausage::process::default_script(), + Some(api), + )?); + sausage::harness::run_module_with_credentials( + World { + root: world.root.clone(), + host_dir: world.host_dir.clone(), + processes: world.processes.clone(), + }, + runtime.clone(), + cred.clone(), + include_str!("../js/chain-test-bundle.js"), + ) + .await?; + let records: serde_json::Value = + serde_json::from_slice(&std::fs::read(disk.join("chain-ids"))?)?; + for (path, subject) in [ + ("/token", records["parent"]["id"].to_string()), + ("/unrelated", records["unrelated"]["id"].to_string()), + ] { + let item = cred.create_item(serde_json::from_value(json!({"name":path,"rules":[{"scheme":"http","hosts":["127.0.0.1"],"port":port,"path_prefixes":[path]}],"transform_js":sausage::cred::summary::script("bearer")?,"fields":[{"name":"token","field_type":"text"}]}))?, &caller, cred.unlocked()?)?; + cred.set_field( + &item.id, + &item.fields[0].field_id, + &token, + &caller, + cred.unlocked()?, + )?; + cred.grant_create( + Grant { + id: String::new(), + items: vec![item.id], + subject: Subject::Conversation(subject), + exp: None, + max_uses: None, + uses: 0, + revoked: false, + validator: None, + note: None, + }, + &caller, + cred.unlocked()?, + )?; + } + let serving = server.clone(); + let expected = format!("Bearer {token}"); + let thread = std::thread::spawn(move || { + let mut accepted = 0; + while let Ok(request) = serving.recv() { + let valid = request.headers().iter().any(|header| { + header.field.equiv("Authorization") && header.value.as_str() == expected + }); + if valid { + accepted += 1; + } + let _ = request.respond(tiny_http::Response::from_string(if valid { + "accepted" + } else { + "missing" + })); + } + accepted + }); + let source = format!( + "globalThis.chainReady = true; globalThis.chainPort = {port};\n{}", + include_str!("../js/chain-test-bundle.js") + ); + let result = tokio::time::timeout( + Duration::from_secs(90), + sausage::harness::run_module_with_credentials(world, runtime, cred, &source), + ) + .await; + server.unblock(); + let accepted = thread.join().unwrap(); + result??; + ensure!(accepted == 3, "unexpected number of injected requests"); + Ok(()) +} diff --git a/userland/Cargo.lock b/userland/Cargo.lock index fb2af4f..c71515c 100644 --- a/userland/Cargo.lock +++ b/userland/Cargo.lock @@ -4340,6 +4340,14 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "sausage-archive-tools" +version = "0.1.0" +dependencies = [ + "flate2", + "tar", +] + [[package]] name = "sausage-coreutils" version = "0.1.0" @@ -4447,6 +4455,15 @@ dependencies = [ "walkdir", ] +[[package]] +name = "sausage-fs-tools" +version = "0.1.0" +dependencies = [ + "chrono", + "clap", + "sausage-spawn", +] + [[package]] name = "sausage-grep" version = "0.1.0" diff --git a/userland/Cargo.toml b/userland/Cargo.toml index 9a802a7..c569339 100644 --- a/userland/Cargo.toml +++ b/userland/Cargo.toml @@ -1,7 +1,7 @@ # The default CLIs of a world, built for wasm32-wasip2. Its own workspace, outside the host's. [workspace] resolver = "3" -members = ["git", "coreutils", "procps", "cwd-init/probe", "ripgrep", "diffutils", "jaq", "sed", "grep", "fd", "spawn", "findutils", "exec-tools", "ssh-client", "ssh", "curl", "init", "box"] +members = ["git", "coreutils", "procps", "cwd-init/probe", "ripgrep", "diffutils", "jaq", "sed", "grep", "fd", "spawn", "findutils", "exec-tools", "ssh-client", "ssh", "curl", "init", "box", "archive-tools", "fs-tools"] # WASI compatibility patches: git/vendor/PATCHES.md. [patch.crates-io] diff --git a/userland/archive-tools/Cargo.toml b/userland/archive-tools/Cargo.toml new file mode 100644 index 0000000..e017cf1 --- /dev/null +++ b/userland/archive-tools/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "sausage-archive-tools" +version = "0.1.0" +edition = "2024" + +[dependencies] +tar = { version = "0.4", default-features = false } +flate2 = "1" + +[[bin]] +name = "tar" +path = "src/tar.rs" +[[bin]] +name = "gzip" +path = "src/gzip.rs" +[[bin]] +name = "gunzip" +path = "src/gzip.rs" +[[bin]] +name = "zcat" +path = "src/gzip.rs" diff --git a/userland/archive-tools/README.md b/userland/archive-tools/README.md new file mode 100644 index 0000000..33e8d0a --- /dev/null +++ b/userland/archive-tools/README.md @@ -0,0 +1,38 @@ +# Archive tools + +These Rust commands use standard WASI files and streams. + +```sh +tar -czf archive.tgz -C /repos project +tar -xzf archive.tgz -C /work --strip-components 1 +tar -tf archive.tar +tar -cf - project | gzip -c > project.tgz +gzip -k file +gunzip -fk file.gz +zcat file.gz +``` + +The tar parser accepts short combined flags, `-C`, `-f -`, and `--strip-components`. +Extraction supports regular files and directories. It rejects special files and paths outside the destination. +Creation uses portable modes because WASI does not expose Unix mode bits. +It includes symbolic links in archives, but world extraction does not create symbolic links or hardlinks. +The selected `-C` applies to all input paths. +With `-v`, creation prints names to stdout for named archives and to stderr for `-f -`. + +Gzip supports `-d`, `-c`, `-k`, and `-f`. +Gunzip defaults to decompression. Zcat defaults to decompression to stdout. +Files use the `.gz` suffix. Decompression accepts concatenated gzip members. +Existing output files require `-f`. A successful file operation removes its input unless `-k` is set. + +From `userland/`, with the executor's `CARGO_TARGET_DIR`: + +```sh +nix develop -c cargo build -p sausage-archive-tools --release --locked +nix develop -c nu archive-tools/test.nu +nix develop -c cargo test -p sausage-archive-tools --target aarch64-unknown-linux-gnu --locked +``` + +The test script compares cross-format tar and gzip transfers with native tools. +It also compares common GoAWK one-liners and checks the pinned QuickJS command. +The real-world tests live in `fs-tools/world-test.nu`. +Nix packaging remains deferred until T17 merges. diff --git a/userland/archive-tools/src/gzip.rs b/userland/archive-tools/src/gzip.rs new file mode 100644 index 0000000..7baf85d --- /dev/null +++ b/userland/archive-tools/src/gzip.rs @@ -0,0 +1,106 @@ +//! Compress or decompress files and streams with the gzip, gunzip, and zcat names. +use flate2::{Compression, read::MultiGzDecoder, write::GzEncoder}; +use std::{ + fs::File, + io::{self, Read, Write}, +}; +fn transfer(input: &mut dyn Read, output: &mut dyn Write, decompress: bool) -> io::Result<()> { + if decompress { + io::copy(&mut MultiGzDecoder::new(input), output)?; + } else { + let mut encoder = GzEncoder::new(output, Compression::default()); + io::copy(input, &mut encoder)?; + encoder.finish()?; + } + Ok(()) +} +fn run() -> Result<(), Box> { + let mut args = std::env::args(); + let program = args.next().unwrap_or_default(); + let name = std::path::Path::new(&program) + .file_stem() + .unwrap_or_default() + .to_string_lossy(); + let mut decompress = name == "gunzip" || name == "zcat"; + let mut stdout = name == "zcat"; + let mut keep = false; + let mut force = false; + let mut paths = Vec::new(); + while let Some(arg) = args.next() { + if arg == "--" { + paths.extend(args); + break; + } + if arg.starts_with('-') && arg != "-" { + for c in arg[1..].chars() { + match c { + 'd' => decompress = true, + 'c' => stdout = true, + 'k' => keep = true, + 'f' => force = true, + _ => return Err(format!("unsupported option: {c}").into()), + } + } + } else { + paths.push(arg); + } + } + if paths.is_empty() { + paths.push("-".into()); + } + for path in paths { + let mut input: Box = if path == "-" { + Box::new(io::stdin()) + } else { + Box::new(File::open(&path)?) + }; + if stdout || path == "-" { + transfer(&mut input, &mut io::stdout(), decompress)?; + } else { + let dest = if decompress { + path.strip_suffix(".gz") + .ok_or("unknown suffix (expected .gz)")? + .to_owned() + } else { + format!("{path}.gz") + }; + let mut output = File::options() + .write(true) + .create(force) + .truncate(force) + .create_new(!force) + .open(&dest)?; + let result = + transfer(&mut input, &mut output, decompress).and_then(|()| output.flush()); + if result.is_err() { + drop(output); + let _ = std::fs::remove_file(&dest); + } + result?; + if !keep { + std::fs::remove_file(path)?; + } + } + } + Ok(()) +} +fn main() { + if let Err(e) = run() { + eprintln!("gzip: {e}"); + std::process::exit(1); + } +} +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn binary_round_trip_and_corrupt_input() { + let source = [0, 255, 1, 42]; + let mut encoded = Vec::new(); + transfer(&mut source.as_slice(), &mut encoded, false).unwrap(); + let mut decoded = Vec::new(); + transfer(&mut encoded.as_slice(), &mut decoded, true).unwrap(); + assert_eq!(decoded, source); + assert!(transfer(&mut &b"bad gzip"[..], &mut Vec::new(), true).is_err()); + } +} diff --git a/userland/archive-tools/src/tar.rs b/userland/archive-tools/src/tar.rs new file mode 100644 index 0000000..a0ebde8 --- /dev/null +++ b/userland/archive-tools/src/tar.rs @@ -0,0 +1,221 @@ +//! Create, list, and extract portable tar archives through standard WASI files. +use flate2::{Compression, read::GzDecoder, write::GzEncoder}; +use std::{ + fs::File, + io::{self, Read, Write}, + path::{Path, PathBuf}, +}; + +fn append( + builder: &mut tar::Builder, + source: &Path, + name: &Path, + verbose: Option, +) -> io::Result<()> { + let meta = std::fs::symlink_metadata(source)?; + let mut header = tar::Header::new_gnu(); + header.set_uid(0); + header.set_gid(0); + header.set_mtime( + meta.modified()? + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs(), + ); + if let Some(stdout) = verbose { + let suffix = if meta.is_dir() { "/" } else { "" }; + if stdout { + println!("{}{suffix}", name.display()); + } else { + eprintln!("{}{suffix}", name.display()); + } + } + if meta.is_dir() { + header.set_entry_type(tar::EntryType::Directory); + header.set_size(0); + header.set_mode(0o755); + header.set_cksum(); + builder.append_data(&mut header, name, io::empty())?; + let mut entries = std::fs::read_dir(source)?.collect::>>()?; + entries.sort_by_key(|e| e.file_name()); + for entry in entries { + append( + builder, + &entry.path(), + &name.join(entry.file_name()), + verbose, + )?; + } + } else if meta.is_file() { + header.set_size(meta.len()); + header.set_mode(0o644); + header.set_cksum(); + builder.append_data(&mut header, name, File::open(source)?)?; + } else if meta.file_type().is_symlink() { + header.set_entry_type(tar::EntryType::Symlink); + header.set_size(0); + header.set_mode(0o777); + header.set_cksum(); + builder.append_link(&mut header, name, std::fs::read_link(source)?)?; + } else { + return Err(io::Error::other("unsupported special file")); + } + Ok(()) +} +fn create( + writer: W, + cwd: &Path, + paths: &[String], + verbose: Option, +) -> io::Result { + let mut builder = tar::Builder::new(writer); + for path in paths { + let name = Path::new(path).strip_prefix("/").unwrap_or(Path::new(path)); + append(&mut builder, &cwd.join(path), name, verbose)?; + } + builder.finish()?; + builder.into_inner() +} +fn run() -> Result<(), Box> { + let mut args = std::env::args().skip(1); + let mut mode = None; + let mut compressed = false; + let mut verbose = false; + let mut archive = None; + let mut cwd = PathBuf::from("."); + let mut strip = 0usize; + let mut paths = Vec::new(); + while let Some(arg) = args.next() { + if arg == "--" { + paths.extend(args); + break; + } + if arg == "--strip-components" { + strip = args.next().ok_or("missing strip count")?.parse()?; + continue; + } + if let Some(n) = arg.strip_prefix("--strip-components=") { + strip = n.parse()?; + continue; + } + if arg.starts_with('-') || (mode.is_none() && arg.chars().all(|c| "cxtzvf".contains(c))) { + let flags = arg.trim_start_matches('-'); + let mut chars = flags.chars(); + while let Some(flag) = chars.next() { + match flag { + 'c' | 'x' | 't' => { + if mode.replace(flag).is_some() { + return Err("choose one archive operation".into()); + } + } + 'z' => compressed = true, + 'v' => verbose = true, + 'f' | 'C' => { + let tail = chars.as_str(); + let value = if tail.is_empty() { + args.next().ok_or("missing option value")? + } else { + tail.to_owned() + }; + if flag == 'f' { + archive = Some(value); + } else { + cwd = PathBuf::from(value); + } + break; + } + _ => return Err(format!("unsupported option: {flag}").into()), + } + } + } else { + paths.push(arg); + } + } + let archive = archive.ok_or("-f is required")?; + match mode.ok_or("an operation is required")? { + 'c' => { + if paths.is_empty() { + return Err("refusing to create an empty archive".into()); + } + let verbose = verbose.then_some(archive != "-"); + let writer: Box = if archive == "-" { + Box::new(io::stdout()) + } else { + Box::new(File::create(archive)?) + }; + if compressed { + create( + GzEncoder::new(writer, Compression::default()), + &cwd, + &paths, + verbose, + )? + .finish()? + .flush()?; + } else { + create(writer, &cwd, &paths, verbose)?.flush()?; + } + } + operation => { + let reader: Box = if archive == "-" { + Box::new(io::stdin()) + } else { + Box::new(File::open(archive)?) + }; + let reader: Box = if compressed { + Box::new(GzDecoder::new(reader)) + } else { + reader + }; + let mut archive = tar::Archive::new(reader); + for entry in archive.entries()? { + let mut entry = entry?; + let name = entry.path()?.into_owned(); + if !paths.is_empty() && !paths.iter().any(|p| name.starts_with(p)) { + continue; + } + if operation == 't' { + println!("{}", name.display()); + continue; + } + let name: PathBuf = name + .components() + .filter(|c| !matches!(c, std::path::Component::CurDir)) + .skip(strip) + .collect(); + if name.as_os_str().is_empty() { + continue; + } + if name + .components() + .any(|c| !matches!(c, std::path::Component::Normal(_))) + { + return Err("archive path escapes destination".into()); + } + if verbose { + println!("{}", name.display()); + } + let dest = cwd.join(name); + if entry.header().entry_type().is_dir() { + std::fs::create_dir_all(dest)?; + } else if entry.header().entry_type().is_file() { + if let Some(parent) = dest.parent() { + std::fs::create_dir_all(parent)?; + } + io::copy(&mut entry, &mut File::create(dest)?)?; + } else { + return Err( + "world extraction supports regular files and directories only".into(), + ); + } + } + } + } + Ok(()) +} +fn main() { + if let Err(e) = run() { + eprintln!("tar: {e}"); + std::process::exit(2); + } +} diff --git a/userland/archive-tools/test.nu b/userland/archive-tools/test.nu new file mode 100644 index 0000000..18f18eb --- /dev/null +++ b/userland/archive-tools/test.nu @@ -0,0 +1,64 @@ +#!/usr/bin/env nu +# Compare archive tools and pinned preview1 commands against native tools. +def check [ok: bool, message: string] { if not $ok { error make {msg: $message} }; print $'ok - ($message)' } +def call [target: path, root: path, tool: string, args: list, input?: any] { + let input = ($input | default '') + $input | ^wasmtime run -S cli-exit-with-code=y --dir $'($root)::/' ($target | path join wasm32-wasip2 release $'($tool).wasm') ...$args | complete +} +def success [] { let r = $in; if $r.exit_code != 0 { error make {msg: $r.stderr} }; $r } +def main [] { + let target = ($env.CARGO_TARGET_DIR | path expand) + let root = (^mktemp -d /tmp/t42-tools-XXXXXXXX | str trim) + mkdir ($root | path join input sub) + 'hello from a nested file' | save ($root | path join input sub file) + let created = (call $target $root tar [-czf /ours.tgz -C / input] | success) + check ($created.stdout == '') 'tar create stdout' + ^tar -xzf ($root | path join ours.tgz) -C $root + check ($env.LAST_EXIT_CODE == 0) 'native tar reads world gzip archive' + ^tar -czf ($root | path join native.tgz) -C $root input + mkdir ($root | path join extracted) + call $target $root tar [-xzf /native.tgz -C /extracted --strip-components 1] | success | ignore + check ((open --raw ($root | path join extracted sub file)) == 'hello from a nested file') 'world extracts native archive with strip-components' + let plain = (call $target $root tar [-cf /plain.tar -C /input/sub file] | success) + check ($plain.stdout == '') 'tar -cf' + let native_list = (^tar -tf ($root | path join plain.tar) | complete) + let world_list = (call $target $root tar [-tf /plain.tar]) + check ($native_list.stdout == $world_list.stdout and $native_list.exit_code == $world_list.exit_code) 'tar -tf matches native' + let native_verbose = (^tar -cvf ($root | path join verbose-native.tar) -C ($root | path join input sub) file | complete) + let world_verbose = (call $target $root tar [-cvf /verbose.tar -C /input/sub file]) + check ($native_verbose.stdout == $world_verbose.stdout and $native_verbose.exit_code == $world_verbose.exit_code and $world_verbose.stderr == '') 'tar create verbose named archive' + let stream = (call $target $root tar [-cvf - -C /input/sub file] | success) + check (($stream.stderr | str trim) == 'file') 'tar create verbose stream uses stderr' + let native_stream = ($stream.stdout | ^tar -tf - | complete) + check ($native_stream.stdout == $native_list.stdout and $native_stream.exit_code == 0) 'native reads tar stdout stream' + mkdir ($root | path join stream-extract) + call $target $root tar [-xf - -C /stream-extract] $stream.stdout | success | ignore + check ((open --raw ($root | path join stream-extract file)) == 'hello from a nested file') 'tar -xf stdin stream' + let listed = (call $target $root tar [-tf /missing.tar]) + let native_error = (^tar -tf ($root | path join missing.tar) | complete) + check ($listed.exit_code == $native_error.exit_code) 'tar missing archive exit code' + let compressed = (call $target $root gzip [-c] 'binary payload' | success) + let decoded = ($compressed.stdout | ^gzip -dc | complete) + check ($decoded.exit_code == 0 and $decoded.stdout == 'binary payload') 'native gzip decodes world stream' + let native_compressed = ('reverse payload' | ^gzip -c | complete) + let decoded = (call $target $root gunzip [-c] $native_compressed.stdout | success) + check ($decoded.stdout == 'reverse payload') 'world gunzip decodes native stream' + for tool in [gzip zcat] { + let flags = if $tool == 'gzip' { [-dc] } else { [] } + let decoded = (call $target $root $tool $flags $native_compressed.stdout | success) + check ($decoded.stdout == 'reverse payload') $'($tool) native gzip stream' + } + call $target $root gzip [-k /input/sub/file] | success | ignore + check (($root | path join input sub file.gz | path exists) and ($root | path join input sub file | path exists)) 'gzip -k keeps input' + let conflict = (call $target $root gzip [/input/sub/file]) + check ($conflict.exit_code == 1) 'gzip refuses overwrite' + call $target $root gunzip [-fk /input/sub/file.gz] | success | ignore + for program in ['{sum += $1} END {print sum}' '{print NR, NF, $2}' 'BEGIN {for (i=1;i<=4;i++) print i*i}'] { + let native = ("2 a\n3 b\n" | ^awk $program | complete) + let wasm = ("2 a\n3 b\n" | ^wasmtime run --dir $'($root)::/' ($target | path join awk.wasm) $program | complete) + check ($native.stdout == $wasm.stdout and $native.exit_code == $wasm.exit_code) $'awk ($program)' + } + let qjs = (^wasmtime run ($env.FILE_PWD | path join .. qjs qjs.wasm) -e 'console.log(6*7)' | complete) + check ($qjs.exit_code == 0 and ($qjs.stdout | str trim) == '42') 'qjs -e' + print $'evidence: ($root)' +} diff --git a/userland/bundle.nu b/userland/bundle.nu index e283939..7ca2c0c 100644 --- a/userland/bundle.nu +++ b/userland/bundle.nu @@ -16,6 +16,8 @@ def main [destination?: path] { ^nu ($here | path join brush build.nu) if $env.LAST_EXIT_CODE != 0 { error make {msg: 'brush build failed'} } mkdir $bin + ^nu ($here | path join preview1.nu) $bin + if $env.LAST_EXIT_CODE != 0 { error make {msg: 'preview1 build failed'} } for package in ($metadata.packages | where {|p| $p.id in $metadata.workspace_members }) { let tools = ($package.manifest_path | path dirname | path join tools.txt) for binary in ($package.targets | where {|t| 'bin' in $t.kind }) { diff --git a/userland/esbuild/README.md b/userland/esbuild/README.md new file mode 100644 index 0000000..ef3b3e1 --- /dev/null +++ b/userland/esbuild/README.md @@ -0,0 +1,14 @@ +# esbuild + +`source/` contains esbuild v0.27.7 from https://github.com/evanw/esbuild. +The pinned commit is `2025c9ff6ab15ba6b0f9d074fd732250cc46e4a3`. +The source includes its MIT license. +The `vendor/` directory contains the Go dependency selected by the upstream `go.mod` and `go.sum`. + +`userland/preview1.nu` builds this source for `GOOS=wasip1 GOARCH=wasm` with the native Go compiler. +It disables the module proxy and uses the vendored dependency. +It copies the result to `bin/esbuild.wasm` without a guest adapter, as D41 requires. +The bundle source snapshot includes this source under `/src/sausage/userland/esbuild/source`. + +The build follows the accepted S6 source recipe. +It does not install npm dependencies in the world. diff --git a/userland/esbuild/source/.editorconfig b/userland/esbuild/source/.editorconfig new file mode 100644 index 0000000..ab77668 --- /dev/null +++ b/userland/esbuild/source/.editorconfig @@ -0,0 +1,7 @@ +[*] +indent_style = tab +indent_size = 2 + +[*.{js,json,ts}] +indent_style = space +indent_size = 2 diff --git a/userland/esbuild/source/.github/ISSUE_TEMPLATE/new-issue.md b/userland/esbuild/source/.github/ISSUE_TEMPLATE/new-issue.md new file mode 100644 index 0000000..622eac8 --- /dev/null +++ b/userland/esbuild/source/.github/ISSUE_TEMPLATE/new-issue.md @@ -0,0 +1,16 @@ +--- +name: New issue +about: This is the template for new issues. +title: '' +labels: '' +assignees: '' + +--- + + diff --git a/userland/esbuild/source/.github/workflows/ci.yml b/userland/esbuild/source/.github/workflows/ci.yml new file mode 100644 index 0000000..d9386d9 --- /dev/null +++ b/userland/esbuild/source/.github/workflows/ci.yml @@ -0,0 +1,331 @@ +name: CI + +on: + push: + branches: ['*'] + pull_request: + branches: ['*'] + workflow_dispatch: + +permissions: + contents: read # to fetch code (actions/checkout) + +jobs: + esbuild-platforms: + # Split this out into its own runner because it's slow + name: esbuild CI (All Platforms) + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Read go.version + run: | + echo "GO_VERSION=$(cat go.version)" >> $GITHUB_ENV + + - name: Set up Go ${{ env.GO_VERSION }} + uses: actions/setup-go@v3 + with: + go-version: ${{ env.GO_VERSION }} + id: go + + - name: Setup Node.js environment + uses: actions/setup-node@v3 + with: + node-version: 18 + + - name: Ensure all platforms can be built + run: | + git status + make platform-all + + # Plan 9 is not a supported platform, but someone wanted esbuild to be able to build for it anyway... + - name: Ensure esbuild can be built for Plan 9 + run: | + GOOS=plan9 GOARCH=386 go build ./cmd/esbuild + GOOS=plan9 GOARCH=amd64 go build ./cmd/esbuild + GOOS=plan9 GOARCH=arm go build ./cmd/esbuild + + - name: Test an old TypeScript version + run: make test-old-ts + + esbuild-slow: + # Split these out into their own runner because they're very slow + name: esbuild CI (Slow Tests) + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Read go.version + run: | + echo "GO_VERSION=$(cat go.version)" >> $GITHUB_ENV + + - name: Set up Go ${{ env.GO_VERSION }} + uses: actions/setup-go@v3 + with: + go-version: ${{ env.GO_VERSION }} + id: go + + - name: Setup Node.js environment + uses: actions/setup-node@v3 + with: + node-version: 16 + + # Note: These tests break with node version 18. Something about WebAssembly. + - name: Rollup Tests + run: make test-rollup + + - name: Uglify Tests + run: CI=1 make uglify + + - name: Type check tsc using tsc + run: make test-tsc + + esbuild: + name: esbuild CI + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Read go.version (non-Windows) + if: matrix.os != 'windows-latest' + run: | + echo "GO_VERSION=$(cat go.version)" >> $GITHUB_ENV + + - name: Read go.version (Windows) + if: matrix.os == 'windows-latest' + run: | + echo "GO_VERSION=$(cat go.version)" >> $Env:GITHUB_ENV + + - name: Set up Go ${{ env.GO_VERSION }} + uses: actions/setup-go@v3 + with: + go-version: ${{ env.GO_VERSION }} + id: go + + - name: Setup Node.js environment + uses: actions/setup-node@v3 + with: + node-version: 18 + + - name: Setup Deno 1.40.0 + uses: denoland/setup-deno@main + with: + deno-version: v1.40.0 + + - name: go test + run: go test -race ./internal/... + + - name: go vet + run: go vet ./cmd/... ./internal/... ./pkg/... + + - name: Deno Tests (non-Windows) + if: matrix.os != 'windows-latest' + run: make test-deno + + - name: Deno Tests (Windows) + if: matrix.os == 'windows-latest' + run: make test-deno-windows + + - name: Test for path/filepath + if: matrix.os == 'ubuntu-latest' + run: make no-filepath + + - name: Make sure "check-go-version" works (non-Windows) + if: matrix.os != 'windows-latest' + run: make check-go-version + + - name: go fmt + if: matrix.os == 'macos-latest' + run: make fmt-go + + - name: npm ci + run: cd scripts && npm ci + + - name: Register Test (ESBUILD_WORKER_THREADS=0, non-Windows) + if: matrix.os != 'windows-latest' + run: ESBUILD_WORKER_THREADS=0 node scripts/register-test.js + + - name: Register Test + run: node scripts/register-test.js + + - name: Verify Source Map + run: node scripts/verify-source-map.js + + - name: E2E Tests + run: node scripts/end-to-end-tests.js + + - name: JS API Tests (ESBUILD_WORKER_THREADS=0, non-Windows) + if: matrix.os != 'windows-latest' + run: ESBUILD_WORKER_THREADS=0 node scripts/js-api-tests.js + + - name: JS API Tests + run: node scripts/js-api-tests.js + + - name: NodeJS Unref Tests + run: node scripts/node-unref-tests.js + + - name: Plugin Tests + run: node scripts/plugin-tests.js + + - name: TypeScript Type Definition Tests + if: matrix.os == 'ubuntu-latest' + run: node scripts/ts-type-tests.js + + - name: JS API Type Check + if: matrix.os == 'ubuntu-latest' + run: make lib-typecheck + + - name: Decorator Tests + if: matrix.os == 'ubuntu-latest' + run: make decorator-tests + + - name: WebAssembly API Tests (browser) + if: matrix.os == 'ubuntu-latest' + run: make test-wasm-browser + + - name: WebAssembly API Tests (node, Linux) + if: matrix.os == 'ubuntu-latest' + run: make test-wasm-node + + - name: WebAssembly API Tests (node, non-Linux) + if: matrix.os != 'ubuntu-latest' + run: node scripts/wasm-tests.js + + - name: Sucrase Tests + if: matrix.os == 'ubuntu-latest' + run: make test-sucrase + + - name: Esprima Tests + if: matrix.os == 'ubuntu-latest' + run: make test-esprima + + - name: Preact Splitting Tests + if: matrix.os == 'ubuntu-latest' + run: make test-preact-splitting + + - name: Check the unicode table generator + if: matrix.os == 'ubuntu-latest' + run: cd scripts && node gen-unicode-table.js + + - name: Yarn PnP tests + run: | + # Note that Yarn recently deliberately broke "npm install -g yarn". + # They say you now have to run "corepack enable" to fix it. They have + # written about this here: https://yarnpkg.com/corepack + corepack enable + + make test-yarnpnp + + esbuild-old-go-version: + name: esbuild CI (old Go version) + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Go 1.13 (the minimum required Go version for esbuild) + uses: actions/setup-go@v3 + with: + go-version: 1.13 + id: go + + - name: go build + run: go build ./cmd/esbuild + + - name: go test + run: go test ./internal/... + + esbuild-old-deno-version: + name: esbuild CI (old Deno version) + runs-on: ${{ matrix.os }} + strategy: + matrix: + # Note: I'm excluding "macos-latest" here because GitHub recently + # changed their macOS CI VMs from x86_64 to aarch64 (i.e. from Intel + # to ARM) and it looks like old Deno versions have WASM bugs on ARM. + # Specifically, this test now crashes like this when run on macOS: + # + # # + # # Fatal error in , line 0 + # # Check failed: RwxMemoryWriteScope::IsAllowed(). + # # + # # + # # + # #FailureMessage Object: 0x16f282368 + # ==== C stack trace =============================== + # + # 0 deno 0x0000000101bb8e78 v8::base::debug::StackTrace::StackTrace() + 24 + # 1 deno 0x0000000101bbda84 v8::platform::(anonymous namespace)::PrintStackTrace() + 24 + # 2 deno 0x0000000101bb6230 V8_Fatal(char const*, ...) + 268 + # 3 deno 0x000000010227e468 v8::internal::wasm::WasmCodeManager::MemoryProtectionKeysEnabled() const + 0 + # 4 deno 0x0000000102299994 v8::internal::wasm::WasmEngine::InitializeOncePerProcess() + 44 + # 5 deno 0x0000000101e78fd0 v8::internal::V8::Initialize() + 1576 + # 6 deno 0x0000000101c3b7d8 v8::V8::Initialize(int) + 32 + # 7 deno 0x00000001011833dc _ZN3std4sync4once4Once9call_once28_$u7b$$u7b$closure$u7d$$u7d$17h2bbe74d315ab3e84E + 488 + # 8 deno 0x00000001017f8854 std::sync::once::Once::call_inner::h70fbdd48fe002a01 + 724 + # 9 deno 0x000000010115ca80 deno_core::runtime::JsRuntime::new::h9c5f1a9c910f1eed + 192 + # 10 deno 0x00000001014d3b50 deno_runtime::worker::MainWorker::bootstrap_from_options::h91a0eaac48dfc18e + 4260 + # 11 deno 0x0000000100ee692c deno::create_main_worker::h0d1622755821ae7f + 1608 + # 12 deno 0x0000000100f6c688 _ZN97_$LT$core..future..from_generator..GenFuture$LT$T$GT$$u20$as$u20$core..future..future..Future$GT$4poll17h87ddfac9566887c8E + 492 + # 13 deno 0x0000000100f6ba18 tokio::runtime::task::raw::poll::h7d51f1a7d5a61c15 + 1396 + # 14 deno 0x0000000101917b98 std::sys_common::backtrace::__rust_begin_short_backtrace::hd384935dcffe6f2d + 332 + # 15 deno 0x0000000101917954 _ZN4core3ops8function6FnOnce40call_once$u7b$$u7b$vtable.shim$u7d$$u7d$17he2755732d5d29cf0E + 124 + # 16 deno 0x0000000101829684 std::sys::unix::thread::Thread::new::thread_start::h432bc30153e41f60 + 48 + # 17 libsystem_pthread.dylib 0x000000018a436f94 _pthread_start + 136 + # 18 libsystem_pthread.dylib 0x000000018a431d34 thread_start + 8 + # + # Hopefully running this old Deno version on Linux is sufficiently + # close to running it on macOS. For reference, I believe this is the + # change that GitHub made which broke this test: + # https://github.blog/changelog/2023-10-02-github-actions-apple-silicon-m1-macos-runners-are-now-available-in-public-beta/ + os: [ubuntu-latest, windows-latest] + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Read go.version (non-Windows) + if: matrix.os != 'windows-latest' + run: | + echo "GO_VERSION=$(cat go.version)" >> $GITHUB_ENV + + - name: Read go.version (Windows) + if: matrix.os == 'windows-latest' + run: | + echo "GO_VERSION=$(cat go.version)" >> $Env:GITHUB_ENV + + - name: Set up Go ${{ env.GO_VERSION }} + uses: actions/setup-go@v3 + with: + go-version: ${{ env.GO_VERSION }} + id: go + + # Make sure esbuild works with old versions of Deno. Note: It's important + # to test a version before 1.31.0, which introduced the "Deno.Command" API. + - name: Setup Deno 1.24.0 + uses: denoland/setup-deno@main + with: + deno-version: v1.24.0 + + - name: Setup Node.js environment + uses: actions/setup-node@v3 + with: + node-version: 18 + + - name: Deno Tests (non-Windows) + if: matrix.os != 'windows-latest' + run: make test-deno + + - name: Deno Tests (Windows) + if: matrix.os == 'windows-latest' + run: make test-deno-windows diff --git a/userland/esbuild/source/.github/workflows/e2e.yml b/userland/esbuild/source/.github/workflows/e2e.yml new file mode 100644 index 0000000..594beac --- /dev/null +++ b/userland/esbuild/source/.github/workflows/e2e.yml @@ -0,0 +1,52 @@ +name: End-to-end install tests + +on: + schedule: + - cron: '0 */6 * * *' + workflow_dispatch: + +permissions: + contents: read # to fetch code (actions/checkout) + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup Node.js environment + uses: actions/setup-node@v3 + with: + node-version: 18 + + # The version of Deno is pinned because version 1.25.1 was causing test + # flakes due to random segfaults. + - name: Setup Deno 1.24.0 + uses: denoland/setup-deno@main + with: + deno-version: v1.24.0 + + - name: Test npm + run: | + npm i -g npm@next-7 + time make test-e2e-npm + + - name: Test pnpm + run: | + npm i -g pnpm@next-7 + time make test-e2e-pnpm + + - name: Test yarn (classic) + run: | + npm i -g yarn@latest + time make test-e2e-yarn + + - name: Test yarn (berry) + run: | + npm i -g yarn@latest + time make test-e2e-yarn-berry + + - name: Test deno + run: | + time make test-e2e-deno diff --git a/userland/esbuild/source/.github/workflows/publish.yml b/userland/esbuild/source/.github/workflows/publish.yml new file mode 100644 index 0000000..47e6830 --- /dev/null +++ b/userland/esbuild/source/.github/workflows/publish.yml @@ -0,0 +1,89 @@ +name: Publish + +permissions: + id-token: write + contents: write + +on: + push: + branches: + - main + paths: + - version.txt + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Read version info + run: | + echo "GO_VERSION=$(cat go.version)" >> $GITHUB_ENV + echo "ESBUILD_VERSION=$(cat version.txt)" >> $GITHUB_ENV + + # This is here to fail quickly if the release already exists + - name: Try to create the "v${{ env.ESBUILD_VERSION }}" tag + run: | + git fetch --tags + git tag "v$ESBUILD_VERSION" + + - name: Extract the release notes + run: | + CHANGELOG=$(awk -v "ver=$ESBUILD_VERSION" '/^## / { if (p) { exit }; if ($2 == ver) { p=1; next} } p' CHANGELOG.md) + echo "CHANGELOG<> $GITHUB_ENV + echo "$CHANGELOG" >> $GITHUB_ENV + echo "EOF" >> $GITHUB_ENV + + # Make sure we'll be able to generate release notes later on below + - name: Release notes must not be empty + run: | + test -n "$CHANGELOG" + + - name: Set up Go ${{ env.GO_VERSION }} + uses: actions/setup-go@v3 + with: + go-version: ${{ env.GO_VERSION }} + + - name: Setup Node.js environment + uses: actions/setup-node@v3 + with: + node-version: 24 + + # This updates the version in all "package.json" files + - name: Build for all platforms + run: | + git status + make platform-all + + # All "package.json" files should have been updated already by running "make platform-all" and committing the results + - name: Reject uncommitted/untracked changes + run: | + git status --porcelain + test -z "$(git status --porcelain)" + + # Trusted publishing requires this specific version of npm + - name: Install npm + run: | + npm install -g npm@11.5.1 + + - name: Publish packages + run: | + make publish-all + + - name: Push the tag to GitHub + run: | + git push origin tag "v$ESBUILD_VERSION" + + # Only do this after publishing was successful + - name: Create a GitHub Release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: v${{ env.ESBUILD_VERSION }} + release_name: v${{ env.ESBUILD_VERSION }} + body: ${{ env.CHANGELOG }} + draft: false + prerelease: false diff --git a/userland/esbuild/source/.github/workflows/validate.yml b/userland/esbuild/source/.github/workflows/validate.yml new file mode 100644 index 0000000..d39bb41 --- /dev/null +++ b/userland/esbuild/source/.github/workflows/validate.yml @@ -0,0 +1,30 @@ +name: Validate release builds + +on: + push: + tags: ['v*'] + workflow_dispatch: + +permissions: + contents: read # to fetch code (actions/checkout) + +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Read go.version + run: | + echo "GO_VERSION=$(cat go.version)" >> $GITHUB_ENV + + - name: Set up Go 1.x + uses: actions/setup-go@v3 + with: + go-version: ${{ env.GO_VERSION }} + id: go + + - name: Validation checks + run: | + make validate-builds diff --git a/userland/esbuild/source/.gitignore b/userland/esbuild/source/.gitignore new file mode 100644 index 0000000..7e9385d --- /dev/null +++ b/userland/esbuild/source/.gitignore @@ -0,0 +1,36 @@ +.DS_Store +.idea/ +.vscode/ +/bench/ +/demo/ +/deno/ +/esbuild +/github/ +/go/ +/lib/deno/lib.deno.d.ts +/npm/@esbuild/android-arm/esbuild.wasm +/npm/@esbuild/android-arm/wasm_exec_node.js +/npm/@esbuild/android-arm/wasm_exec.js +/npm/@esbuild/android-x64/esbuild.wasm +/npm/@esbuild/android-x64/wasm_exec_node.js +/npm/@esbuild/android-x64/wasm_exec.js +/npm/@esbuild/openharmony-arm64/esbuild.wasm +/npm/@esbuild/openharmony-arm64/wasm_exec_node.js +/npm/@esbuild/openharmony-arm64/wasm_exec.js +/npm/@esbuild/wasi-preview1/esbuild.wasm +/npm/esbuild-wasm/browser.js +/npm/esbuild-wasm/esbuild.wasm +/npm/esbuild-wasm/esm/ +/npm/esbuild-wasm/lib/ +/npm/esbuild-wasm/wasm_exec_node.js +/npm/esbuild-wasm/wasm_exec.js +/npm/esbuild/install.js +/npm/esbuild/lib/ +/require/*/bench/ +/require/*/demo/ +/scripts/.*/ +/validate/ +/www +bin +esbuild.exe +node_modules/ diff --git a/userland/esbuild/source/CHANGELOG-2020.md b/userland/esbuild/source/CHANGELOG-2020.md new file mode 100644 index 0000000..bb6c812 --- /dev/null +++ b/userland/esbuild/source/CHANGELOG-2020.md @@ -0,0 +1,3320 @@ +# Changelog: 2020 + +This changelog documents all esbuild versions published in the year 2020 (versions 0.3.0 through 0.8.28). + +## 0.8.28 + +* Add a `--summary` flag that prints helpful information after a build ([#631](https://github.com/evanw/esbuild/issues/631)) + + Normally esbuild's CLI doesn't print anything after doing a build if nothing went wrong. This allows esbuild to be used as part of a more complex chain of tools without the output cluttering the terminal. However, sometimes it is nice to have a quick overview in your terminal of what the build just did. You can now add the `--summary` flag when using the CLI and esbuild will print a summary of what the build generated. It looks something like this: + + ``` + $ ./esbuild --summary --bundle src/Three.js --outfile=build/three.js --sourcemap + + build/three.js 1.0mb ⚠️ + build/three.js.map 1.8mb + + ⚡ Done in 43ms + ``` + +* Keep unused imports in TypeScript code in one specific case ([#604](https://github.com/evanw/esbuild/issues/604)) + + The official TypeScript compiler always removes imported symbols that aren't used as values when converting TypeScript to JavaScript. This is because these symbols could be types and not removing them could result in a run-time module instantiation failure because of missing exports. This even happens when the `tsconfig.json` setting `"importsNotUsedAsValues"` is set to `"preserve"`. Doing this just keeps the import statement itself but confusingly still removes the imports that aren't used as values. + + Previously esbuild always exactly matched the behavior of the official TypeScript compiler regarding import removal. However, that is problematic when trying to use esbuild to compile a partial module such as when converting TypeScript to JavaScript inside a file written in the [Svelte](https://svelte.dev/) programming language. Here is an example: + + ```html + +
+

Hello {name}!

+ +
+ ``` + + The current Svelte compiler plugin for TypeScript only provides esbuild with the contents of the ` + + ``` + + In addition to this approach, you can now also use esbuild in the browser from a module-type script (note the use of `esm/browser.js` instead of `lib/browser.js`): + + ```html + + ``` + + Part of this fix was contributed by [@calebeby](https://github.com/calebeby). + +## 0.7.16 + +* Fix backward slashes in source maps on Windows ([#463](https://github.com/evanw/esbuild/issues/463)) + + The relative path fix in the previous release caused a regression where paths in source maps contained `\` instead of `/` on Windows. That is incorrect because source map paths are URLs, not file system paths. This release replaces `\` with `/` for consistency on Windows. + +* `module.require()` is now an alias for `require()` ([#455](https://github.com/evanw/esbuild/issues/455)) + + Some packages such as [apollo-server](https://github.com/apollographql/apollo-server) use `module.require()` instead of `require()` with the intent of bypassing the bundler's `require` and calling the underlying function from `node` instead. Unfortunately that doesn't actually work because CommonJS module semantics means `module` is a variable local to that file's CommonJS closure instead of the host's `module` object. + + This wasn't an issue when using `apollo-server` with Webpack because the literal expression `module.require()` is automatically rewritten to `require()` by Webpack: [webpack/webpack#7750](https://github.com/webpack/webpack/pull/7750). To get this package to work, esbuild now matches Webpack's behavior here. Calls to `module.require()` will become external calls to `require()` as long as the required path has been marked as external. + +## 0.7.15 + +* Lower `export * as` syntax for ES2019 and below + + The `export * from 'path'` syntax was added in ES2015 but the `export * as name from 'path'` syntax was added more recently in ES2020. This is a shorthand for an import followed by an export: + + ```js + // ES2020 + export * as name from 'path' + + // ES2019 + import * as name from 'path' + export {name} + ``` + + With this release, esbuild will now undo this shorthand syntax when using `--target=es2019` or below. + +* Better code generation for TypeScript files with type-only exports ([#447](https://github.com/evanw/esbuild/issues/447)) + + Previously TypeScript files could have an unnecessary CommonJS wrapper in certain situations. The specific situation is bundling a file that re-exports something from another file without any exports. This happens because esbuild automatically considers a module to be a CommonJS module if there is no ES6 `import`/`export` syntax. + + This behavior is undesirable because the CommonJS wrapper is usually unnecessary. It's especially undesirable for cases where the re-export uses `export * from` because then the re-exporting module is also converted to a CommonJS wrapper (since re-exporting everything from a CommonJS module must be done at run-time). That can also impact the bundle's exports itself if the entry point does this and the format is `esm`. + + It is generally equivalent to avoid the CommonJS wrapper and just rewrite the imports to an `undefined` literal instead: + + ```js + import {name} from './empty-file' + console.log(name) + ``` + + This can be rewritten to this instead (with a warning generated about `name` being missing): + + ```js + console.log(void 0) + ``` + + With this release, this is now how cases like these are handled. The only case where this can't be done is when the import uses the `import * as` syntax. In that case a CommonJS wrapper is still necessary because the namespace cannot be rewritten to `undefined`. + +* Add support for `importsNotUsedAsValues` in TypeScript ([#448](https://github.com/evanw/esbuild/issues/448)) + + The `importsNotUsedAsValues` field in `tsconfig.json` is now respected. Setting it to `"preserve"` means esbuild will no longer remove unused imports in TypeScript files. This field was added in TypeScript 3.8. + +* Fix relative paths in generated source maps ([#444](https://github.com/evanw/esbuild/issues/444)) + + Currently paths in generated source map files don't necessarily correspond to real file system paths. They are really only meant to be human-readable when debugging in the browser. + + However, the Visual Studio Code debugger expects these paths to point back to the original files on the file system. With this release, it should now always be possible to get back to the original source file by joining the directory containing the source map file with the relative path in the source map. + + This fix was contributed by [@yoyo930021](https://github.com/yoyo930021). + +## 0.7.14 + +* Fix a bug with compound import statements ([#446](https://github.com/evanw/esbuild/issues/446)) + + Import statements can simultaneously contain both a default import and a namespace import like this: + + ```js + import defVal, * as nsVal from 'path' + ``` + + These statements were previously miscompiled when bundling if the import path was marked as external, or when converting to a specific output format, and the namespace variable itself was used for something other than a property access. The generated code contained a syntax error because it generated a `{...}` import clause containing the default import. + + This particular problem was caused by code that converts namespace imports into import clauses for more efficient bundling. This transformation should not be done if the namespace import cannot be completely removed: + + ```js + // Can convert namespace to clause + import defVal, * as nsVal from 'path' + console.log(defVal, nsVal.prop) + ``` + + ```js + // Cannot convert namespace to clause + import defVal, * as nsVal from 'path' + console.log(defVal, nsVal) + ``` + +## 0.7.13 + +* Fix `mainFields` in the JavaScript API ([#440](https://github.com/evanw/esbuild/issues/440) and [#441](https://github.com/evanw/esbuild/pull/441)) + + It turns out the JavaScript bindings for the `mainFields` API option didn't work due to a copy/paste error. The fix for this was contributed by [@yoyo930021](https://github.com/yoyo930021). + +* The benchmarks have been updated + + The benchmarks now include Parcel 2 and Webpack 5 (in addition to Parcel 1 and Webpack 4, which were already included). It looks like Parcel 2 is slightly faster than Parcel 1 and Webpack 5 is significantly slower than Webpack 4. + +## 0.7.12 + +* Fix another subtle ordering issue with `import` statements + + When importing a file while bundling, the import statement was ordered before the imported code. This could affect import execution order in complex scenarios involving nested hybrid ES6/CommonJS modules. The fix was to move the import statement to after the imported code instead. This issue affected the `@sentry/browser` package. + +## 0.7.11 + +* Fix regression in 0.7.9 when minifying with code splitting ([#437](https://github.com/evanw/esbuild/issues/437)) + + In certain specific cases, bundling and minifying with code splitting active can cause a crash. This is a regression that was introduced in version 0.7.9 due to the fix for issue [#421](https://github.com/evanw/esbuild/issues/421). The crash has been fixed and this case now has test coverage. + +## 0.7.10 + +* Recover from bad `main` field in `package.json` ([#423](https://github.com/evanw/esbuild/issues/423)) + + Some packages are published with invalid information in the `main` field of `package.json`. In that case, path resolution should fall back to searching for a file named `index.js` before giving up. This matters for the `simple-exiftool` package, for example. + +* Ignore TypeScript types on `catch` clause bindings ([435](https://github.com/evanw/esbuild/issues/435)) + + This fixes an issue where using a type annotation in a `catch` clause like this was a syntax error: + + ```ts + try { + } catch (x: unknown) { + } + ``` + +## 0.7.9 + +* Fixed panic when using a `url()` import in CSS with the `--metafile` option + + This release fixes a crash that happens when `metafile` output is enabled and the `url()` syntax is used in a CSS file to import a successfully-resolved file. + +* Minify some CSS colors + + The minifier can now reduce the size of some CSS colors. This is the initial work to start CSS minification in general beyond whitespace removal. There is currently support for minifying hex, `rgb()/rgba()`, and `hsl()/hsla()` into hex or shorthand hex. The minification process respects the configured target browser and doesn't use any syntax that wouldn't be supported. + +* Lower newer CSS syntax for older browsers + + Newer color syntax such as `rgba(255 0 0 / 50%)` will be converted to older syntax (in this case `rgba(255, 0, 0, 0.5)`) when the target browser doesn't support the newer syntax. For example, this happens when using `--target=chrome60`. + +* Fix an ordering issue with `import` statements ([#421](https://github.com/evanw/esbuild/issues/421)) + + Previously `import` statements that resolved to a CommonJS module turned into a call to `require()` inline. This was subtly incorrect when combined with tree shaking because it could sometimes cause imported modules to be reordered: + + ```js + import {foo} from './cjs-file' + import {bar} from './esm-file' + console.log(foo, bar) + ``` + + That code was previously compiled into something like this, which is incorrect because the evaluation of `bar` may depend on side effects from importing `cjs-file.js`: + + ```js + // ./cjs-file.js + var require_cjs_file = __commonJS(() => { + ... + }) + + // ./esm-file.js + let bar = ...; + + // ./example.js + const cjs_file = __toModule(require_cjs_file()) + console.log(cjs_file.foo, bar) + ``` + + That code is now compiled into something like this: + + ```js + // ./cjs-file.js + var require_cjs_file = __commonJS(() => { + ... + }) + + // ./example.js + const cjs_file = __toModule(require_cjs_file()) + + // ./esm-file.js + let bar = ...; + + // ./example.js + console.log(cjs_file.foo, bar) + ``` + + This now means that a single input file can end up in multiple discontiguous regions in the output file as is the case with `example.js` here, which wasn't the case before this bug fix. + +## 0.7.8 + +* Move external `@import` rules to the top + + Bundling could cause `@import` rules for paths that have been marked as external to be inserted in the middle of the CSS file. This would cause them to become invalid and be ignored by the browser since all `@import` rules must come first at the top of the file. These `@import` rules are now always moved to the top of the file so they stay valid. + +* Better support for `@keyframes` rules + + The parser now directly understands `@keyframes` rules, which means it can now format them more accurately and report more specific syntax errors. + +* Minify whitespace around commas in CSS + + Whitespace around commas in CSS will now be pretty-printed when not minifying and removed when minifying. So `a , b` becomes `a, b` when pretty-printed and `a,b` when minified. + +* Warn about unknown at-rules in CSS + + Using an `@rule` in a CSS file that isn't known by esbuild now generates a warning and these rules will be passed through unmodified. If they aren't known to esbuild, they are probably part of a CSS preprocessor syntax that should have been compiled away before giving the file to esbuild to parse. + +* Recoverable CSS syntax errors are now warnings + + The base CSS syntax can preserve nonsensical rules as long as they contain valid tokens and have matching opening and closing brackets. These rule with incorrect syntax now generate a warning instead of an error and esbuild preserves the syntax in the output file. This makes it possible to use esbuild to process CSS that was generated by another tool that contains bugs. + + For example, the following code is invalid CSS, and was presumably generated by a bug in an automatic prefix generator: + + ```css + div { + -webkit-undefined; + -moz-undefined; + -undefined; + } + ``` + + This code will no longer prevent esbuild from processing the CSS file. + +* Treat `url(...)` in CSS files as an import ([#415](https://github.com/evanw/esbuild/issues/415)) + + When bundling, the `url(...)` syntax in CSS now tries to resolve the URL as a path using the bundler's built in path resolution logic. The following loaders can be used with this syntax: `text`, `base64`, `file`, `dataurl`, and `binary`. + +* Automatically treat certain paths as external + + The following path forms are now automatically considered external: + + * `http://example.com/image.png` + * `https://example.com/image.png` + * `//example.com/image.png` + * `data:image/png;base64,iVBORw0KGgo=` + + In addition, paths starting with `#` are considered external in CSS files, which allows the following syntax to continue to work: + + ```css + path { + /* This can be useful with SVG DOM content */ + fill: url(#filter); + } + ``` + +## 0.7.7 + +* Fix TypeScript decorators on static members + + This release fixes a bug with the TypeScript transform for the `experimentalDecorators` setting. Previously the target object for all decorators was the class prototype, which was incorrect for static members. Static members now correctly use the class object itself as a target object. + +* Experimental support for CSS syntax ([#20](https://github.com/evanw/esbuild/issues/20)) + + This release introduces the new `css` loader, enabled by default for `.css` files. It has the following features: + + * You can now use esbuild to process CSS files by passing a CSS file as an entry point. This means CSS is a new first-class file type and you can use it without involving any JavaScript code at all. + + * When bundling is enabled, esbuild will bundle multiple CSS files together if they are referenced using the `@import "./file.css";` syntax. CSS files can be excluded from the bundle by marking them as external similar to JavaScript files. + + * There is basic support for pretty-printing CSS, and for whitespace removal when the `--minify` flag is present. There isn't any support for CSS syntax compression yet. Note that pretty-printing and whitespace removal both rely on the CSS syntax being recognized. Currently esbuild only recognizes certain CSS syntax and passes through unrecognized syntax unchanged. + + Some things to keep in mind: + + * CSS support is a significant undertaking and this is the very first release. There are almost certainly going to be issues. This is an experimental release to land the code and get feedback. + + * There is no support for CSS modules yet. Right now all class names are in the global namespace. Importing a CSS file into a JavaScript file will not result in any import names. + + * There is currently no support for code splitting of CSS. I haven't tested multiple entry-point scenarios yet and code splitting will require additional changes to the AST format. + +## 0.7.6 + +* Fix JSON files with multiple entry points ([#413](https://github.com/evanw/esbuild/issues/413)) + + This release fixes an issue where a single build operation containing multiple entry points and a shared JSON file which is used by more than one of those entry points can generate incorrect code for the JSON file when code splitting is disabled. The problem was not cloning the AST representing the JSON file before mutating it. + +* Silence warnings about `require.resolve()` for external paths ([#410](https://github.com/evanw/esbuild/issues/410)) + + Bundling code containing a call to node's [`require.resolve()`](https://nodejs.org/api/modules.html#modules_require_resolve_request_options) function causes a warning because it's an unsupported use of `require` that does not end up being bundled. For example, the following code will likely have unexpected behavior if `foo` ends up being bundled because the `require()` call is evaluated at bundle time but the `require.resolve()` call is evaluated at run time: + + ```js + let foo = { + path: require.resolve('foo'), + module: require('foo'), + }; + ``` + + These warnings can already be disabled by surrounding the code with a `try`/`catch` statement. With this release, these warnings can now also be disabled by marking the path as external. + +* Ensure external relative paths start with `./` or `../` + + Individual file paths can be marked as external in addition to package paths. In that case, the path to the file is rewritten to be relative to the output directory. However, previously the relative path for files in the output directory itself did not start with `./`, meaning they could potentially be interpreted as a package path instead of a relative path. These paths are now prefixed with `./` to avoid this edge case. + +## 0.7.5 + +* Fix an issue with automatic semicolon insertion after `let` ([#409](https://github.com/evanw/esbuild/issues/409)) + + The character sequence `let` can be considered either a keyword or an identifier depending on the context. A fix was previously landed in version 0.6.31 to consider `let` as an identifier in code like this: + + ```js + if (0) let + x = 0 + ``` + + Handling this edge case is useless but the behavior is required by the specification. However, that fix also unintentionally caused `let` to be considered an identifier in code like this: + + ```js + let + x = 0 + ``` + + In this case, `let` should be considered a keyword instead. This has been fixed. + +* Fix some additional conformance tests + + Some additional syntax edge cases are now forbidden including `let let`, `import {eval} from 'path'`, and `if (1) x: function f() {}`. + +## 0.7.4 + +* Undo an earlier change to try to improve yarn compatibility ([#91](https://github.com/evanw/esbuild/pull/91) and [#407](https://github.com/evanw/esbuild/issues/407)) + + The [yarn package manager](https://github.com/yarnpkg/yarn) behaves differently from npm and is not compatible in many ways. While npm is the only officially supported package manager for esbuild, people have contributed fixes for other package managers including yarn. One such fix is PR [#91](https://github.com/evanw/esbuild/pull/91) which makes sure the install script only runs once for a given installation directory. + + I suspect this fix is actually incorrect, and is the cause of issue [#407](https://github.com/evanw/esbuild/issues/407). The problem seems to be that if you change the version of a package using `yarn add esbuild@version`, yarn doesn't clear out the installation directory before reinstalling the package so the package ends up with a mix of files from both package versions. This is not how npm behaves and seems like a pretty severe bug in yarn. I am reverting PR [#91](https://github.com/evanw/esbuild/pull/91) in an attempt to fix this issue. + +* Disable some warnings for code inside `node_modules` directories ([#395](https://github.com/evanw/esbuild/issues/395) and [#402](https://github.com/evanw/esbuild/issues/402)) + + Using esbuild to build code with certain suspicious-looking syntax may generate a warning. These warnings don't fail the build (the build still succeeds) but they point out code that is very likely to not behave as intended. This has caught real bugs in the past: + + * [rollup/rollup#3729](https://github.com/rollup/rollup/issues/3729): Invalid dead code removal for return statement due to ASI + * [aws/aws-sdk-js#3325](https://github.com/aws/aws-sdk-js/issues/3325): Array equality bug in the Node.js XML parser + * [olifolkerd/tabulator#2962](https://github.com/olifolkerd/tabulator/issues/2962): Nonsensical comparisons with typeof and "null" + * [mrdoob/three.js#11183](https://github.com/mrdoob/three.js/pull/11183): Comparison with -0 in Math.js + * [mrdoob/three.js#11182](https://github.com/mrdoob/three.js/pull/11182): Operator precedence bug in WWOBJLoader2.js + + However, it's not esbuild's job to find bugs in other libraries, and these warnings are problematic for people using these libraries with esbuild. The only fix is to either disable all esbuild warnings and not get warnings about your own code, or to try to get the warning fixed in the affected library. This is especially annoying if the warning is a false positive as was the case in https://github.com/firebase/firebase-js-sdk/issues/3814. So these warnings are now disabled for code inside `node_modules` directories. + +## 0.7.3 + +* Fix compile error due to missing `unix.SYS_IOCTL` in the latest `golang.org/x/sys` ([#396](https://github.com/evanw/esbuild/pull/396)) + + The `unix.SYS_IOCTL` export was apparently removed from `golang.org/x/sys` recently, which affected code in esbuild that gets the width of the terminal. This code now uses another method of getting the terminal width. The fix was contributed by [@akayj](https://github.com/akayj). + +* Validate that the versions of the host code and the binary executable match ([#407](https://github.com/evanw/esbuild/issues/407)) + + After the install script runs, the version of the downloaded binary should always match the version of the package being installed. I have added some additional checks to verify this in case this invariant is ever broken. Breaking this invariant is very bad because it means the code being run is a mix of code from different package versions. + +## 0.7.2 + +* Transform arrow functions to function expressions with `--target=es5` ([#182](https://github.com/evanw/esbuild/issues/182) and [#297](https://github.com/evanw/esbuild/issues/297)) + + Arrow functions are now transformed into function expressions when targeting `es5`. For example, this code: + + ```js + function foo() { + var x = () => [this, arguments] + return x() + } + ``` + + is transformed into this code: + + ```js + function foo() { + var _this = this, _arguments = arguments; + var x = function() { + return [_this, _arguments]; + }; + return x(); + } + ``` + +* Parse template literal types from TypeScript 4.1 + + TypeScript 4.1 includes a new feature called template literal types. You can read [the announcement](https://devblogs.microsoft.com/typescript/announcing-typescript-4-1-beta/#template-literal-types) for more details. The following syntax can now be parsed correctly by esbuild: + + ```ts + let foo: `${'a' | 'b'}-${'c' | 'd'}` = 'a-c' + ``` + +* Parse key remapping in mapped types from TypeScript 4.1 + + TypeScript 4.1 includes a new feature called key remapping in mapped types. You can read [the announcement](https://devblogs.microsoft.com/typescript/announcing-typescript-4-1-beta/#key-remapping-mapped-types) for more details. The following syntax can now be parsed correctly by esbuild: + + ```ts + type RemoveField = { [K in keyof T as Exclude]: T[K] } + ``` + +* Allow automatic semicolon insertion before the TypeScript `as` operator + + The following code now correctly parses as two separate statements instead of one statement with a newline in the middle: + + ```ts + let foo = bar + as (null); + ``` + +* Fix a bug where `module` was incorrectly minified for non-JavaScript loaders + + If you pass a non-JavaScript file such as a `.json` file to esbuild, it will by default generate `module.exports = {...}`. However, the `module` variable would incorrectly be minified when `--minify` is present. This issue has been fixed. This bug did not appear if `--format=cjs` was also present, only if no `--format` flag was specified. + +* Fix bugs with `async` functions ([#388](https://github.com/evanw/esbuild/issues/388)) + + This release contains correctness fixes for `async` arrow functions with regard to the `arguments` variable. This affected `async` arrow functions nested inside `function` expressions or statements. Part of this fix was contributed by [@rtsao](https://github.com/rtsao). + +* Fix `export` clause when converting to CommonJS in transform API calls ([#393](https://github.com/evanw/esbuild/issues/393)) + + This release fixes some bugs with the recently-released feature in version 0.6.32 where you can specify an output format even when bundling is disabled. This is the case when using the transform API call, for example. Previously esbuild could generate code that crashed at run time while trying to export something incorrectly. This only affected code with top-level `export` statements. This has been fixed and these cases now have test coverage. + +## 0.7.1 + +* Fix bug that forbids `undefined` values in the JavaScript API + + The validation added in the previous release was accidentally overly restrictive and forbids `undefined` values for optional properties. This release allows `undefined` values again (which are simply ignored). + +## 0.7.0 + +* Mark output files with a hashbang as executable ([#364](https://github.com/evanw/esbuild/issues/364)) + + Output files that start with a hashbang line such as `#!/usr/bin/env node` will now automatically be marked as executable. This lets you run them directly in a Unix-like shell without using the `node` command. + +* Use `"main"` for `require()` and `"module"` for `import` ([#363](https://github.com/evanw/esbuild/issues/363)) + + The [node module resolution algorithm](https://nodejs.org/api/modules.html#modules_all_together) uses the `"main"` field in `package.json` to determine which file to load when a package is loaded with `require()`. Independent of node, most bundlers have converged on a convention where the `"module"` field takes precedence over the `"main"` field when present. Package authors can then use the `"module"` field to publish the same code in a different format for bundlers than for node. + + This is commonly used to publish "dual packages" that appear to use ECMAScript modules to bundlers but that appear to use CommonJS modules to node. This is useful because ECMAScript modules improve bundler output by taking advantage of "tree shaking" (basically dead-code elimination) and because ECMAScript modules cause lots of problems in node (for example, node doesn't support importing ECMAScript modules using `require()`). + + The problem is that if code using `require()` resolves to the `"module"` field in esbuild, the resulting value is currently always an object. ECMAScript modules export a namespace containing all exported properties. There is no direct equivalent of `module.exports = value` in CommonJS. The closest is `export default value` but the CommonJS equivalent of that is `exports.default = value`. This is problematic for code containing `module.exports = function() {}` which is a frequently-used CommonJS library pattern. An example of such an issue is Webpack issue [#6584](https://github.com/webpack/webpack/issues/6584). + + An often-proposed way to fix this is to map `require()` to `"main"` and map `import` to `"module"`. The problem with this is that it means the same package would be loaded into memory more than once if it is loaded both with `require()` and with `import` (perhaps from separate packages). An example of such an issue is GraphQL issue [#1479](https://github.com/graphql/graphql-js/issues/1479#issuecomment-416718578). + + The workaround for these problems in this release is that esbuild will now exclusively use `"main"` for a package that is loaded using `require()` at least once. Otherwise, if a package is only loaded using `import`, esbuild will exclusively use the `"module"` field. This still takes advantage of tree shaking for ECMAScript modules but gracefully falls back to CommonJS for compatibility. + + Keep in mind that the [`"browser"` field](https://github.com/defunctzombie/package-browser-field-spec) still takes precedence over both `"module"` and `"main"` when building for the browser platform. + +* Add the `--main-fields=` flag ([#363](https://github.com/evanw/esbuild/issues/363)) + + This adopts a configuration option from Webpack that lets you specify the order of "main fields" from `package.json` to use when determining the main module file for a package. Node only uses `main` but bundlers often respect other ones too such as `module` or `browser`. You can read more about this feature in the Webpack documentation [here](https://webpack.js.org/configuration/resolve/#resolvemainfields). + + The default order when targeting the browser is essentially `browser,module,main` with the caveat that `main` may be chosen over `module` for CommonJS compatibility as described above. If choosing `module` over `main` at the expense of CommonJS compatibility is important to you, this behavior can be disabled by explicitly specifying `--main-fields=browser,module,main`. + + The default order when targeting node is `main,module`. Note that this is different than Webpack, which defaults to `module,main`. This is also for compatibility because some packages incorrectly treat `module` as meaning "code for the browser" instead of what it actually means, which is "code for ES6 environments". Unfortunately this disables most tree shaking that would otherwise be possible because it means CommonJS modules will be chosen over ECMAScript modules. If choosing `module` over `main` is important to you (e.g. to potentially take advantage of improved tree shaking), this behavior can be disabled by explicitly specifying `--main-fields=module,main`. + +* Additional validation of arguments to JavaScript API calls ([#381](https://github.com/evanw/esbuild/issues/381)) + + JavaScript API calls each take an object with many optional properties as an argument. Previously there was only minimal validation of the contents of that object. If you aren't using TypeScript, this can lead to confusing situations when the data on the object is invalid. Now there is some additional validation done to the shape of the object and the types of the properties. + + It is now an error to pass an object with a property that esbuild won't use. This should help to catch typos. It is also now an error if a property on the object has an unexpected type. + +## 0.6.34 + +* Fix parsing of `type;` statements followed by an identifier in TypeScript ([#377](https://github.com/evanw/esbuild/pull/377)) + + The following TypeScript code is now correctly parsed as two separate expression statements instead of one type declaration statement: + + ```ts + type + Foo = {} + ``` + + This was contributed by [@rtsao](https://github.com/rtsao). + +* Fix `export {Type}` in TypeScript when bundling ([#379](https://github.com/evanw/esbuild/issues/379)) + + In TypeScript, `export {Type}` is supposed to be silently removed by the compiler if `Type` does not refer to a value declared locally in the file. Previously this behavior was incompletely implemented. The statement itself was removed but the export record was not, so later stages of the pipeline could sometimes add the export statement back. This release removes the export record as well as the statement so it should stay removed in all cases. + +* Forbid exporting non-local symbols in JavaScript + + It is now an error to export an identifier using `export {foo}` if `foo` is not declared locally in the same file. This error matches the error that would happen at run-time if the code were to be evaluated in a JavaScript environment that supports ES6 module syntax. This is only an error in JavaScript. In TypeScript, the missing identifier is silently removed instead since it's assumed to be a type name. + +* Handle source maps with out-of-order mappings ([#378](https://github.com/evanw/esbuild/issues/378)) + + Almost all tools that generate source maps write out the mappings in increasing order by generated position since the mappings are generated along with the output. However, some tools can apparently generate source maps with out-of-order mappings. It's impossible for generated line numbers to be out of order due to the way the source map format works, but it's possible for generated column numbers to be out of order. This release fixes this issue by sorting the mappings by generated position after parsing if necessary. + +## 0.6.33 + +* Fix precedence of tagged template expressions ([#372](https://github.com/evanw/esbuild/issues/372)) + + Previously `` await tag`text` `` and `` new tag`text` `` were incorrectly parsed as `` (await tag)`text` `` and `` (new tag)`text` ``. They are now correctly parsed as `` await (tag`text`) `` and `` new (tag`text`) `` instead. + +* Fix invalid syntax when lowering `super` inside `async` to `es2016` or earlier ([#375](https://github.com/evanw/esbuild/issues/375)) + + This release fixes a bug where using `super.prop` inside an `async` function with `--target=es2016` or earlier generated code that contained a syntax error. This was because `async` functions are converted to generator functions inside a wrapper function in this case, and `super` is not available inside the wrapper function. The fix is to move the reference to `super` outside of the wrapper function. + +* Fix duplicate definition of `module` when targeting CommonJS ([#370](https://github.com/evanw/esbuild/issues/370)) + + The bundler didn't properly reserve the identifier `module` when using `--format=cjs`. This meant automatically-generated variables named `module` could potentially not be renamed to avoid collisions with the CommonJS `module` variable. It was possible to get into this situation when importing a module named `module`, such as the [node built-in module by that name](https://nodejs.org/api/module.html). This name is now marked as reserved when bundling to CommonJS, so automatically-generated variables named `module` will now be renamed to `module2` to avoid collisions. + +## 0.6.32 + +* Allow `--format` when bundling is disabled ([#109](https://github.com/evanw/esbuild/issues/109)) + + This change means esbuild can be used to convert ES6 import and export syntax to CommonJS syntax. The following code: + + ```js + import foo from 'foo' + export const bar = foo + ``` + + will be transformed into the following code with `--format=cjs` (the code for `__export` and `__toModule` was omitted for brevity): + + ```js + __export(exports, { + bar: () => bar + }); + const foo = __toModule(require("foo")); + const bar = foo.default; + ``` + + This also applies to non-JavaScript loaders too. The following JSON: + + ```json + {"foo": true, "bar": false} + ``` + + is normally converted to the following code with `--loader=json`: + + ```js + module.exports = {foo: true, bar: false}; + ``` + + but will be transformed into the following code instead with `--loader=json --format=esm`: + + ```js + var foo = true; + var bar = false; + var stdin_default = {foo, bar}; + export { + bar, + stdin_default as default, + foo + }; + ``` + + Note that converting CommonJS `require()` calls to ES6 imports is not currently supported. Code containing a reference to `require` in these situations will generate a warning. + +* Change the flag for boolean and string minification ([#371](https://github.com/evanw/esbuild/issues/371)) + + Previously setting the `--minify-whitespace` flag shortened `true` and `false` to `!0` and `!1` and shortened string literals containing many newlines by writing them as template literals instead. These shortening operations have been changed to the `--minify-syntax` flag instead. There is no change in behavior for the `--minify` flag because that flag already implies both `--minify-whitespace` and `--minify-syntax`. + +* Remove trailing `()` from `new` when minifying + + Now `new Foo()` will be printed as `new Foo` when minifying (as long as it's safe to do so), resulting in slightly shorter minified code. + +* Forbid `async` functions when the target is `es5` + + Previously using `async` functions did not cause a compile error when targeting `es5` since if they are unavailable, they are rewritten to use generator functions instead. However, generator functions may also be unsupported. It is now an error to use `async` functions if generator functions are unsupported. + +* Fix subtle issue with transforming `async` functions when targeting `es2016` or below + + The TypeScript compiler has a bug where, when the language target is set to `ES2016` or earlier, exceptions thrown during argument evaluation are incorrectly thrown immediately instead of later causing the returned promise to be rejected. Since esbuild replicates TypeScript's `async` function transformation pass, esbuild inherited this same bug. The behavior of esbuild has been changed to match the JavaScript specification. + + Here's an example of code that was affected: + + ```js + async function test(value = getDefaultValue()) {} + let promise = test() + ``` + + The call to `test()` here should never throw, even if `getDefaultValue()` throws an exception. + +## 0.6.31 + +* Invalid source maps are no longer an error ([#367](https://github.com/evanw/esbuild/issues/367)) + + Previously esbuild would fail the build with an error if it encountered a source map that failed validation according to [the specification](https://sourcemaps.info/spec.html). Now invalid source maps will be validated with an error-tolerant validator that will either silently ignore errors or generate a warning, but will never fail the build. + +* Fix various edge cases for conformance tests + + * Hoisted function declarations in nested scopes can now shadow symbols in the enclosing scope without a syntax error: + + ```js + let foo + { + function foo() {} + } + ``` + + * If statements directly containing function declarations now introduce a nested scope so this code is no longer a syntax error: + + ```js + let foo + if (true) + function foo() {} + ``` + + * Keywords can now be used as export aliases with `export * as` statements: + + ```js + export * as class from 'path' + ``` + + * It is now a syntax error to use `break` or `continue` in invalid locations: + + ```js + function foo() { break } + ``` + + * Using `yield` as an identifier outside of a generator function is now allowed: + + ```js + var yield = null + ``` + + * It is now a syntax error to use `yield` or `await` inside a generator or `async` function if it contains an escape sequence: + + ```js + async function foo() { + return \u0061wait; + } + ``` + + * It is now a syntax error to use an `import()` expression with the `new` operator without parentheses: + + ```js + new import('path') + ``` + + * Using `let` as an identifier is now allowed: + + ```js + let = null + ``` + + * It is no longer a compile-time error to assign to an import when not bundling: + + ```js + import {foo} from 'path' + foo = null + ``` + + Instead the behavior will be left up to the host environment at run-time, which should cause a run-time error. However, this will still be treated as a compile-time error when bundling because the scope-hoisting optimization that happens during bundling means the host may no longer cause run-time errors. + + * You can now declare a variable named `arguments` inside a function without an error: + + ```js + function foo() { + let arguments = null + } + ``` + + * Comma expressions in the iterable position of for-of loops are now a syntax error: + + ```js + for (var a of b, c) { + } + ``` + + * It is now a syntax error to use `||` or `&&` with `??` without parentheses + + ```js + a ?? b || c // Syntax error + a ?? (b || c) // Allowed + (a ?? b) || c // Allowed + ``` + + * It is now a syntax error to use `arguments` inside a `class` field initializer + + ```js + class Foo { + foo = arguments + } + ``` + + * It is now a syntax error to a strict mode reserved word to name a `class` + + ```js + class static {} + ``` + +## 0.6.30 + +* Fix optional call of `super` property ([#362](https://github.com/evanw/esbuild/issues/362)) + + This fixes a bug where lowering the code `super.foo?.()` was incorrectly transformed to this: + + ```js + var _a, _b; + (_b = (_a = super).foo) == null ? void 0 : _b.call(_a); + ``` + + This is invalid code because a bare `super` keyword is not allowed. Now that code is transformed to this instead: + + ```js + var _a; + (_a = super.foo) == null ? void 0 : _a.call(this); + ``` + +* Add a `--strict:optional-chaining` option + + This affects the transform for the `?.` optional chaining operator. In loose mode (the default), `a?.b` is transformed to `a == null ? void 0 : a.b`. This works fine in all cases except when `a` is the special object `document.all`. In strict mode, `a?.b` is transformed to `a === null || a === void 0 ? void 0 : a.b` which works correctly with `document.all`. Enable `--strict:optional-chaining` if you need to use `document.all` with the `?.` operator. + +## 0.6.29 + +* Add a warning for comparison with `NaN` + + This warning triggers for code such as `x === NaN`. Code that does this is almost certainly a bug because `NaN === NaN` is false in JavaScript. + +* Add a warning for duplicate switch case clauses + + This warning detects situations when multiple `case` clauses in the same `switch` statement match on the same expression. This almost certainly indicates a problem with the code. This warning protects against situations like this: + + ```js + switch (typeof x) { + case 'object': + // ... + case 'function': + // ... + case 'boolean': + // ... + case 'object': + // ... + } + ``` + +* Allow getters and setters in ES5 ([#356](https://github.com/evanw/esbuild/issues/356)) + + This was an oversight. I incorrectly thought getters and setters were added in ES6, not in ES5. This release allows getter and setter method syntax even when `--target=es5`. + +* Fix a Windows-only regression with missing directory errors ([#359](https://github.com/evanw/esbuild/issues/359)) + + Various Go file system APIs return `ENOTDIR` for missing file system entries on Windows instead of `ENOENT` like they do on other platforms. This interfered with code added in the previous release that makes unexpected file system errors no longer silent. `ENOTDIR` is usually an unexpected error because it's supposed to happen when the file system entry is present but just unexpectedly a file instead of a directory. This release changes `ENOTDIR` to `ENOENT` in certain cases so that these Windows-only errors are no longer treated as unexpected errors. + +* Enforce object accessor argument counts + + According to the JavaScript specification, getter methods must have zero arguments and setter methods must have exactly one argument. This release enforces these rules. + +* Validate assignment targets + + Code containing invalid assignments such as `1 = 2` will now be correctly rejected as a syntax error. Previously such code was passed through unmodified and the output file would contain a syntax error (i.e. "garbage in, garbage out"). + +## 0.6.28 + +* Avoid running out of file handles when ulimit is low ([#348](https://github.com/evanw/esbuild/issues/348)) + + When esbuild uses aggressive concurrency, it can sometimes simultaneously use more file handles than allowed by the system. This can be a problem when the limit is low (e.g. using `ulimit -n 32`). In this release, esbuild now limits itself to using a maximum of 32 file operations simultaneously (in practice this may use up to 64 file handles since some file operations need two handles). This limit was chosen to be low enough to not cause issues with normal ulimit values but high enough to not impact benchmark times. + +* Unexpected file system errors are no longer silent ([#348](https://github.com/evanw/esbuild/issues/348)) + + All file system errors were previously treated the same; any error meant the file or directory was considered to not exist. This was problematic when the process ran out of available file handles because it meant esbuild could ignore files that do actually exist if file handles are exhausted. Then esbuild could potentially generate a different output instead of failing with an error. Now if esbuild gets into this situation, it should report unexpected file system errors and fail to build instead of continuing to build and potentially producing incorrect output. + +* Install script tries `npm install` before a direct download ([#347](https://github.com/evanw/esbuild/issues/347)) + + The `esbuild` package has a post-install script that downloads the native binary for the current platform over HTTP. Some people have configured their environments such that HTTP requests to npmjs.org will hang, and configured npm to use a proxy for HTTP requests instead. In this case, esbuild's install script will still work as long as `npm install` works because the HTTP request will eventually time out, at which point the install script will run `npm install` as a fallback. The timeout is of course undesirable. + + This release changes the order of attempted download methods in the install script. Now `npm install` is tried first and directly downloading the file over HTTP will be tried as a fallback. This means installations will be slightly slower since npm is slow, but it should avoid the situation where the install script takes a long time because it's waiting for a HTTP timeout. This should still support the scenarios where there is a HTTP proxy configured, where there is a custom registry configured, and where the `npm` command isn't available. + +## 0.6.27 + +* Add parentheses when calling `require()` inside `new` ([#339](https://github.com/evanw/esbuild/issues/339)) + + This release fixes an issue where `new (require('path')).ctor()` became `new require_path().ctor()` after bundling, which caused `require_path()` to be invoked as the constructor instead of `ctor()`. With this fix the code `new (require_path()).ctor()` is generated instead, which correctly invokes `ctor()` as the constructor. This was contributed by [@rtsao](https://github.com/rtsao). + +## 0.6.26 + +* Fix syntax error when minifying and bundling CommonJS to ES5 ([#335](https://github.com/evanw/esbuild/issues/335)) + + With the flags `--minify --bundle --target=es5`, esbuild had a bug where the arrow function for the closure used to wrap CommonJS modules was not correctly printed as an ES5 function expression, causing a syntax error. This bug has been fixed. + +## 0.6.25 + +* Avoid the `\v` escape sequence in JSON strings + + Source maps are JSON files, and must obey the [JSON specification](https://www.json.org/). The escape sequence `\v` (for the ASCII control character 11) is valid in JavaScript but not in JSON. Previously esbuild contained a bug where source maps for files containing this ASCII control character were invalid JSON. This release fixes the bug by printing this character as `\u000B` instead. + +* Speedup for `esbuild-wasm` when using the command line + + The [esbuild-wasm](https://www.npmjs.com/package/esbuild-wasm) package includes a WebAssembly command-line tool called `esbuild` which functions the same as the native command-line tool called `esbuild` in the [esbuild](https://www.npmjs.com/package/esbuild) package. The difference is that the WebAssembly implementation is around an order of magnitude slower than the native version. + + This release changes the API used to instantiate the WebAssembly module from [WebAssembly.instantiate](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/instantiate) to [WebAssembly.Module](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/Module/Module), which reduces end-to-end build time by around 1 second on my development laptop. The WebAssembly version is still much slower than the native version, but now it's a little faster than before. + +* Optimize for the [@material-ui/icons](https://www.npmjs.com/package/@material-ui/icons) package + + This package has a directory containing over 11,000 files. Certain optimizations in esbuild that worked fine for common cases severely impacted performance for this edge case. This release changes some aspects of path resolution caching to fix these problems. Build time for a certain benchmark involving this package improved from 1.01s for the previous release to 0.22s for this release. Other benchmark times appear to be unaffected. + +## 0.6.24 + +* Switch from base64 encoding to base32 encoding for file hashes + + Certain output files contain hashes in their name both to prevent collisions and to improve caching. For example, an SVG file named `example.svg` that is loaded using the `file` loader might be copied to a file named `example.T3K5TRK4.svg` in the build directory. The hashes are based on the file's contents so they only change when the file content itself changes. + + The hashes previously used [base64 encoding](https://en.wikipedia.org/wiki/Base64) but I recently realized that since certain file systems (e.g. Windows) are case-insensitive, this could lead to confusing situations where esbuild could theoretically generate two files with different case-sensitive names but with the same case-insensitive name. Hashes now use [base32 encoding](https://en.wikipedia.org/wiki/Base32) which only includes uppercase letters, not lowercase letters, which should avoid this confusing situation. + +* Optimize character frequency for better gzip compression + + The character sequence used to generate minified names is now the characters in the input files sorted descending by frequency. Previously it was just the valid identifier characters in alphabetic order. This means minified names are more likely to contain characters found elsewhere in the output file (e.g. in keywords and strings). This is a pretty small win but it was added because it's a consistent win, it's simple to implement, and it's very fast to compute. + +* Minor syntax minification improvements + + This release contains these additional rules for syntax minification: + + * `a ? b : b` is minified to `a, b` + * `a ? a : b` is minified to `a || b` + * `a ? b : a` is minified to `a && b` + * `a == void 0` is minified to `a == null` + * `a && (b && c)` is minified to `a && b && c` (same for `||`) + * `a ? c : (b, c)` is minified to `(a || b), c` + * `a ? (b, c) : c` is minified to `(a && b), c` + * `a ? b || c : c` is minified to `(a && b) || c` + * `a ? c : b && c` is minified to `(a || b) && c` + * `a ? b(c) : b(d)` is minified to `b(a ? c : d)` + * `a ? true : false` is minified to `!!a` + * `a != null ? a : b` is minified to `a ?? b` if it's supported in the target environment + * `a ? (b ? c : d) : d` is minified to `(a && b) ? c : d` + * `a ? b : (c ? b : d)` is minified to `(a || c) ? b : d` + * `(function foo() {})` is minified to `(function() {})` + * `typeof a === "string"` is minified to `typeof a == "string"` + * `if (a) if (b) return c` is minified to `if (a && b) return c` + * `while (a) if (!b) break;` is minified to `for (; a && b; ) ;` + * `a === null || a === undefined` is minified to `a == null` + + These improvements cause minified code to be slightly smaller. + +## 0.6.23 + +* Add an error message for a missing `--tsconfig` file ([#330](https://github.com/evanw/esbuild/issues/330)) + + The `--tsconfig` flag that was added in version 0.6.1 didn't report an error if the provided file doesn't actually exist. This release makes doing this an error that will fail the build. + +* Avoid generating the minified label name `if` ([#332](https://github.com/evanw/esbuild/issues/332)) + + The recent minification changes in 0.6.20 introduced a regression where input files containing 333 or more label statements resulted in a label being assigned the minified name `if`, which is a JavaScript keyword. This is the first JavaScript keyword in the minified name sequence that esbuild uses for label names: `a b c ... aa ba ca ...`. The regression has been fixed and there is now test coverage for this case. + +## 0.6.22 + +* The bell character is now escaped + + In most terminals, printing the bell character (ASCII code 7) will trigger a sound. The macOS terminal will also flash the screen if sound is muted. This is annoying, and can happen when dumping the output of esbuild to the terminal if the input contains a bell character. Now esbuild will always escape bell characters in the output to avoid this problem. + +* CommonJS modules now export properties of prototype ([#326](https://github.com/evanw/esbuild/issues/326)) + + This change is for compatibility with Webpack. You can now assign an object with a custom prototype to `module.exports` and esbuild will consider all enumerable properties on the prototype as exports. This behavior is necessary to correctly bundle the [paper.js](https://github.com/paperjs/paper.js) library, for example. + +## 0.6.21 + +* Upgrade from Go 1.14 to Go 1.15 + + This change isn't represented by a commit in the repo, but from now on I will be using Go 1.15 to build the distributed binaries instead of Go 1.14. The [release notes for Go 1.15](https://golang.org/doc/go1.15) mention improvements to binary size: + + > Go 1.15 reduces typical binary sizes by around 5% compared to Go 1.14 by eliminating certain types of GC metadata and more aggressively eliminating unused type metadata. + + Initial testing shows that upgrading Go reduces the esbuild binary size on macOS from 7.4mb to 5.3mb, which is a 30% smaller binary! I assume the binary size savings are similar for other platforms. Run-time performance on the esbuild benchmarks seems consistent with previous releases. + +* Lower non-tag template literals to ES5 ([#297](https://github.com/evanw/esbuild/issues/297)) + + You can now use non-tag template literals such as `` `abc` `` and `` `a${b}c` `` with `--target=es5` and esbuild will convert them to string addition such as `"abc"` and `"a" + b + "c"` instead of reporting an error. + +* Newline normalization in template literals + + This fixes a bug with esbuild that caused carriage-return characters to incorrectly end up in multi-line template literals if the source file used Windows-style line endings (i.e. `\r\n`). The ES6 language specification says that both carriage-return characters and Windows carriage-return line-feed sequences must be converted to line-feed characters instead. With this change, esbuild's parsing of multi-line template literals should no longer be platform-dependent. + +* Fix minification bug with variable hoisting + + Hoisted variables that are declared with `var` in a nested scope but hoisted to the top-level scope were incorrectly minified as a nested scope symbol instead of a top-level symbol, which could potentially cause a name collision. This bug has been fixed. + +## 0.6.20 + +* Symbols are now renamed separately per chunk ([#16](https://github.com/evanw/esbuild/issues/16)) + + Previously, bundling with code splitting assigned minified names using a single frequency distribution calculated across all chunks. This meant that typical code changes in one chunk would often cause the contents of all chunks to change, which negated some of the benefits of the browser cache. + + Now symbol renaming (both minified and not minified) is done separately per chunk. It was challenging to implement this without making esbuild a lot slower and causing it to use a lot more memory. Symbol renaming has been mostly rewritten to accomplish this and appears to actually usually use a little less memory and run a bit faster than before, even for code splitting builds that generate a lot of chunks. In addition, minified chunks are now slightly smaller because a given minified name can now be reused by multiple chunks. + +## 0.6.19 + +* Reduce memory usage for large builds by 30-40% ([#304](https://github.com/evanw/esbuild/issues/304)) + + This release reduces memory usage. These specific percentages are likely only accurate for builds with a large number of files. Memory is reduced by ~30% for all builds by avoiding unnecessary per-file symbol maps, and is reduced by an additional ~10% for builds with source maps by preallocating some large arrays relating to source map output. + +* Replace `.js` and `.jsx` with `.ts` or `.tsx` when resolving ([#118](https://github.com/evanw/esbuild/issues/118)) + + This adds an import path resolution behavior that's specific to the TypeScript compiler where you can use an import path that ends in `.js` or `.jsx` when the correct import path actually ends in `.ts` or `.tsx` instead. See the discussion here for more historical context: https://github.com/microsoft/TypeScript/issues/4595. + +## 0.6.18 + +* Install script falls back to `npm install` ([#319](https://github.com/evanw/esbuild/issues/319)) + + The `esbuild` package has a post-install script that downloads the esbuild binary. However, this will fail if `registry.npmjs.org` (or the configured custom npm registry) is inaccessible. + + This release adds an additional fallback for when the download fails. It tries to use the `npm install` command to download the esbuild binary instead. This handles situations where users have either configured npm with a proxy or have a custom command in their path called `npm`. + +## 0.6.17 + +* Add a download cache to the install script + + This speeds up repeated esbuild installs for the same version by only downloading the binary from npm the first time and then reusing it for subsequent installs. The binary files are cached in these locations, which are the same locations as the Electron install script: + + * Windows: `%USERPROFILE%\AppData\Local\Cache\esbuild\bin` + * macOS: `~/Library/Caches/esbuild/bin` + * Other: `~/.cache/esbuild/bin` + + The cache holds a maximum of 5 entries and purges least-recently-used entries above that limit. + +* Omit `export default` of local type names ([#316](https://github.com/evanw/esbuild/issues/316)) + + Normally the `export default` syntax takes a value expression to export. However, TypeScript has a special case for `export default ` where the identifier is allowed to be a type expression instead of a value expression. In that case, the type expression should not be emitted in the resulting bundle. This release improves support for this case by omitting the export when the identifier matches a local type name. + +## 0.6.16 + +* Colors for Windows console output + + Console output on Windows now uses color instead of being monochrome. This should make log messages easier to read. + +* Parenthesize destructuring assignment in arrow function expressions ([#313](https://github.com/evanw/esbuild/issues/313)) + + This fixes a bug where `() => ({} = {})` was incorrectly printed as `() => ({}) = {}`, which is a syntax error. This case is now printed correctly. + +## 0.6.15 + +* Support symlinks with absolute paths in `node_modules` ([#310](https://github.com/evanw/esbuild/issues/310)) + + Previously esbuild only supported symlinks with relative paths, not absolute paths. Adding support for absolute paths in symlinks fixes issues with esbuild and [pnpm](https://github.com/pnpm/pnpm) on Windows. + +* Preserve leading comments inside `import()` expressions ([#309](https://github.com/evanw/esbuild/issues/309)) + + This makes it possible to use esbuild as a faster TypeScript-to-JavaScript frontend for Webpack, which has special [magic comments](https://webpack.js.org/api/module-methods/#magic-comments) inside `import()` expressions that affect Webpack's behavior. + +* Fix crash for source files beginning with `\r\n` when using source maps ([#311](https://github.com/evanw/esbuild/issues/311)) + + The source map changes in version 0.6.13 introduced a regression that caused source files beginning with `\r\n` to crash esbuild when source map generation was enabled. This was not caught during testing both because not many source files begin with a newline and not many source files have Windows-style line endings in them. This regression has been fixed and Windows-style line endings now have test coverage. + +## 0.6.14 + +* Add support for parsing top-level await ([#253](https://github.com/evanw/esbuild/issues/253)) + + It seems appropriate for esbuild to support top-level await syntax now that [node is supporting top-level await syntax by default](https://github.com/nodejs/node/issues/34551) (it's the first widely-used platform to do so). This syntax can now be parsed by esbuild and is always passed through untransformed. It's only allowed when the target is `esnext` because the proposal is still in stage 3. It also cannot be used when bundling. Adding support for top-level await to the bundler is complicated since it causes imports to be asynchronous, which has far-reaching implications. This change is mainly for people using esbuild as a library to transform TypeScript into JavaScript one file at a time. + +## 0.6.13 + +* Exclude non-JavaScript files from source maps ([#304](https://github.com/evanw/esbuild/issues/304)) + + Previously all input files were eligible for source map generation, even binary files included using loaders such as `dataurl`. This was not intentional. Doing this doesn't serve a purpose and can massively bloat the resulting source maps. Now all files are excluded except those loaded by the `js`, `jsx`, `ts`, and `tsx` loaders. + +* Fix incorrect source maps with code splitting ([#303](https://github.com/evanw/esbuild/issues/303)) + + Source maps were completely incorrect when code splitting was enabled for chunk files that imported other chunk files. The source map offsets were not being adjusted past the automatically-generated cross-chunk import statements. This has been fixed. + +* Change source map column offsets from bytes to UTF-16 code units + + The [source map specification](https://sourcemaps.info/spec.html) leaves many things unspecified including what column numbers mean. Until now esbuild has been generating byte offsets for column numbers, but Mozilla's popular [source-map](https://github.com/mozilla/source-map) library appears to use UTF-16 code unit counts for column numbers instead. With this release, esbuild now also uses UTF-16 code units for column numbers in source maps. This should help esbuild's compatibility with other tools in the ecosystem. + +* Fix a bug with partial source mappings + + The source map specification makes it valid to have mappings that don't actually map to anything. These mappings were never generated by esbuild but they are sometimes present in source maps generated by other tools. There was a bug where the source map line number would be thrown off if one of these mappings was present at the end of a line. This bug has been fixed. + +## 0.6.12 + +* Fix bugs with cross-chunk assignment handling ([#302](https://github.com/evanw/esbuild/issues/302)) + + The code splitting process may end up moving the declaration of a file-local variable into a separate chunk from an assignment to that variable. However, it's not possible to assign to a variable in another chunk because assigning to an import is not allowed in ES6. To avoid generating invalid code, esbuild runs an additional pass after code splitting to force all code involved in cross-chunk assignments into the same chunk. + + The logic to do this is quite tricky. For example, moving code between chunks may introduce more cross-chunk assignments that also need to be handled. In this case the bug was caused by not handling complex cases with three or more levels of cross-chunk assignment dependency recursion. These cases now have test coverage and should be handled correctly. + +## 0.6.11 + +* Code splitting chunks now use content hashes ([#16](https://github.com/evanw/esbuild/issues/16)) + + Code that is shared between multiple entry points is separated out into "chunk" files when code splitting is enabled. These files are named `chunk.HASH.js` where `HASH` is a string of characters derived from a hash (e.g. `chunk.iJkFSV6U.js`). + + Previously the hash was computed from the paths of all entry points which needed that chunk. This was done because it was a simple way to ensure that each chunk was unique, since each chunk represents shared code from a unique set of entry points. But it meant that changing the contents of the chunk did not cause the chunk name to change. + + Now the hash is computed from the contents of the chunk file instead. This better aligns esbuild with the behavior of other bundlers. If changing the contents of the file always causes the name to change, you can serve these files with a very large `max-age` so the browser knows to never re-request them from your server if they are already cached. + + Note that the names of entry points _do not_ currently contain a hash, so this optimization does not apply to entry points. Do not serve entry point files with a very large `max-age` or the browser may not re-request them even when they are updated. Including a hash in the names of entry point files has not been done in this release because that would be a breaking change. This release is an intermediate step to a state where all output file names contain content hashes. + + The reason why this hasn't been done before now is because this change makes chunk generation more complex. Generating the contents of a chunk involves generating import statements for the other chunks which that chunk depends on. However, if chunk names now include a content hash, chunk generation must wait until the dependency chunks have finished. This more complex behavior has now been implemented. + + Care was taken to still parallelize as much as possible despite parts of the code having to block. Each input file in a chunk is still printed to a string fully in parallel. Waiting was only introduced in the chunk assembly stage where input file strings are joined together. In practice, this change doesn't appear to have slowed down esbuild by a noticeable amount. + +* Fix an off-by-one error with source map generation ([#289](https://github.com/evanw/esbuild/issues/289)) + + The nested source map support added in version 0.6.5 contained a bug. Input files that were included in the bundle but that didn't themselves contain any generated code caused the source index to shift by one, throwing off the source names of all files after it. This could happen with files consisting only of re-export statements (e.g. `export {name} from 'path'`). This bug has been fixed and this specific scenario now has test coverage. + +## 0.6.10 + +* Revert the binary operator chain change + + It turns out this caused some behavior bugs in the generated code. + +## 0.6.9 + +* Performance optimizations for large file transforms + + There are two main JavaScript APIs: `build()` which operates on the file system and `transform()` which operates on in-memory data. Previously transforming large files using the JavaScript `transform()` API could be significantly slower than just writing the in-memory string to the file system, calling `build()`, and reading the result back from the file system. This is based on performance tests done on macOS 10.15. + + Now esbuild will go through the file system when transforming large files (currently >1mb). This approach is only faster for large files, and can be significantly slower for small files, so small files still keep everything in memory. + +* Avoid stack overflow for binary operator chains + + Syntax trees with millions of sequential binary operators nested inside each other can cause the parser to stack overflow because it uses a recursive visitor pattern, so each binary operator added an entry to the call stack. Now code like this no longer triggers a stack overflow because the visitor uses the heap instead of the stack in this case. This is unlikely to matter in real-world code but can show up in certain artificial test cases, especially when `--minify-syntax` is enabled. + +* Resolve implicitly-named `tsconfig.json` base files ([#279](https://github.com/evanw/esbuild/issues/279)) + + The official TypeScript compiler lets you specify a package path as the `extends` property of a `tsconfig.json` file. The base file is then searched for in the relevant `node_modules` directory. Previously the package path had to end with the name of the base file. Now you can additionally omit the name of the base file if the file name is `tsconfig.json`. This more closely matches the behavior of the official TypeScript compiler. + +* Support for 32-bit Windows systems ([#285](https://github.com/evanw/esbuild/issues/285)) + + You can now install the esbuild npm package on 32-bit Windows systems. + +## 0.6.8 + +* Attempt to support the taobao.org registry ([#291](https://github.com/evanw/esbuild/issues/291)) + + This release attempts to add support for the registry at https://registry.npm.taobao.org, which uses a different URL structure than the official npm registry. Also, the install script will now fall back to the official npm registry if installing with the configured custom registry fails. + +## 0.6.7 + +* Custom registry can now have a path ([#286](https://github.com/evanw/esbuild/issues/286)) + + This adds support for custom registries hosted at a path other than `/`. Previously the registry had to be hosted at the domain level, like npm itself. + +* Nested source maps use relative paths ([#289](https://github.com/evanw/esbuild/issues/289)) + + The original paths in nested source maps are now modified to be relative to the directory containing the source map. This means source maps from packages inside `node_modules` will stay inside `node_modules` in browser developer tools instead of appearing at the root of the virtual file system where they might collide with the original paths of files in other packages. + +* Support for 32-bit Linux systems ([#285](https://github.com/evanw/esbuild/issues/285)) + + You can now install the esbuild npm package on 32-bit Linux systems. + +## 0.6.6 + +* Fix minification bug with `this` values for function calls ([#282](https://github.com/evanw/esbuild/issues/282)) + + Previously `(0, this.fn)()` was incorrectly minified to `this.fn()`, which changes the value of `this` used for the function call. Now syntax like this is preserved during minification. + +* Install script now respects the npm registry setting ([#286](https://github.com/evanw/esbuild/issues/286)) + + If you have configured npm to use a custom registry using `npm config set registry ` or by installing esbuild using `npm install --registry= ...`, this custom registry URL should now be respected by the esbuild install script. + + Specifically, the install script now uses the URL from the `npm_config_registry` environment variable if present instead of the default registry URL `https://registry.npmjs.org/`. Note that the URL must have both a protocol and a host name. + +* Fixed ordering between `node_modules` and a force-overridden `tsconfig.json` ([#278](https://github.com/evanw/esbuild/issues/278)) + + When the `tsconfig.json` settings have been force-overridden using the new `--tsconfig` flag, the path resolution behavior behaved subtly differently than if esbuild naturally discovers the `tsconfig.json` file without the flag. The difference caused package paths present in a `node_modules` directory to incorrectly take precedence over custom path aliases configured in `tsconfig.json`. The ordering has been corrected such that custom path aliases always take place over `node_modules`. + +* Add the `--out-extension` flag for custom output extensions ([#281](https://github.com/evanw/esbuild/issues/281)) + + Previously esbuild could only output files ending in `.js`. Now you can override this to another extension by passing something like `--out-extension:.js=.mjs`. This allows generating output files with the node-specific `.cjs` and `.mjs` extensions without having to use a separate command to rename them afterwards. + +## 0.6.5 + +* Fix IIFE wrapper for ES5 + + The wrapper for immediately-invoked function expressions is hard-coded to an arrow function and was not updated when the ES5 target was added. This meant that bundling ES5 code would generate a bundle what wasn't ES5-compatible. Doing this now uses a function expression instead. + +* Add support for nested source maps ([#211](https://github.com/evanw/esbuild/issues/211)) + + Source map comments of the form `//# sourceMappingURL=...` inside input files are now respected. This means you can bundle files with source maps and esbuild will generate a source map that maps all the way back to the original files instead of to the intermediate file with the source map. + +## 0.6.4 + +* Allow extending `tsconfig.json` paths inside packages ([#269](https://github.com/evanw/esbuild/issues/269)) + + Previously the `extends` field in `tsconfig.json` only worked with relative paths (paths starting with `./` or `../`). Now this field can also take a package path, which will be resolved by looking for the package in the `node_modules` directory. + +* Install script now avoids the `npm` command ([#274](https://github.com/evanw/esbuild/issues/274)) + + The install script now downloads the binary directly from npmjs.org instead of using the `npm` command to install the package. This should be more compatible with unusual node environments (e.g. having multiple old copies of npm installed). + +* Fix a code splitting bug with re-exported symbols ([#273](https://github.com/evanw/esbuild/issues/273)) + + Re-exporting a symbol in an entry point didn't correctly track the cross-chunk dependency, which caused the output file to be missing a required import. This bug has been fixed. + +* Fix code splitting if a dynamic entry point is doubled as a normal entry point ([#272](https://github.com/evanw/esbuild/issues/272)) + + Using a dynamic `import()` expression automatically adds the imported path as an entry point. However, manually adding the imported path to the bundler entry point list resulted in a build failure. This case is now handled. + +* Fix dynamic imports from a parent directory ([#264](https://github.com/evanw/esbuild/issues/264)) + + The nested output directory feature interacted badly with the code splitting feature when an entry point contained a dynamic `import()` to a file from a directory that was a parent directory to all entry points. This caused esbuild to generate output paths starting with `../` which stepped outside of the output directory. + + The directory structure of the input files is mirrored in the output directory relative to the [lowest common ancestor](https://en.wikipedia.org/wiki/Lowest_common_ancestor) among all entry point paths. However, code splitting introduces a new entry point for each dynamic import. These additional entry points are not in the original entry point list so they were ignored by the lowest common ancestor algorithm. The fix is to make sure all entry points are included, user-specified and dynamic. + +## 0.6.3 + +* Fix `/* @__PURE__ */` IIFEs at start of statement ([#258](https://github.com/evanw/esbuild/issues/258)) + + The introduction of support for `/* @__PURE__ */` comments in an earlier release introduced a bug where parentheses were no longer inserted if a statement started with a function expression that was immediately invoked. This bug has been fixed and parentheses are now inserted correctly. + +* Add support for `@jsx` and `@jsxFrag` comments ([#138](https://github.com/evanw/esbuild/issues/138)) + + You can now override the JSX factory and fragment values on a per-file basis using comments: + + ```jsx + // @jsx h + // @jsxFrag Fragment + import {h, Fragment} from 'preact' + console.log(<>) + ``` + + This now generates the following code: + + ```js + import {h, Fragment} from "preact"; + console.log(h(Fragment, null, h("a", null))); + ``` + +* Add the `Write` option to the Go API + + This brings the Go API to parity with the JavaScript API, and makes certain uses of the `api.Build()` call simpler. You can now specify `Write: true` to have the output files written to the file system during the build instead of having to do that yourself. + +## 0.6.2 + +* Fix code splitting bug with re-export cycles ([#251](https://github.com/evanw/esbuild/issues/251)) + + Two files that both re-export each other could cause invalid code to be generated when code splitting is enabled. The specific failure was an export statement without a matching import statement from the shared code chunk. This bug has been fixed. + + Semantically a `export * from 'path'` statement should behave like a `export {name} from 'path'` statement with the export list determined automatically. And likewise `export {name} from 'path'` should behave like `import {name} from 'path'; export {name}`. + + This issue was caused by the re-exported symbols not registering themselves as if they were imported with an import statement. That caused code splitting to fail to generate an import statement when the definition of the symbol ended up in a different chunk than the use of the symbol. + +* Fix code splitting bug with missing generated imports + + An ES6 module that doesn't import or export anything but that still uses ES6 module syntax (e.g. `import.meta`) interacted badly with some optimizations and caused invalid code to be generated. This generated an import statement without a matching export statement. The bug has been fixed. + + To improve tree shaking, esbuild automatically converts `import * as ns from 'path'; use(ns.prop)` into `import {prop} from 'path'; use(prop)` at parse time. The parser doesn't yet know anything about `path` because parsing happens in parallel, so this transformation is always performed. + + Later on `path` is determined to be an ES6 module with no exports. This means that there is no symbol to bind `prop` to. Since it was originally a property access on what is now known to be an empty exports object, its value is guaranteed to be undefined. It's no longer a property access so esbuild inlines the undefined value at all uses by replacing `prop` with `void 0`. + + However, code splitting wasn't aware of this and still thought imports needed to be generated for uses of `prop`, even though it doesn't actually exist. That caused invalid and unnecessary import statements to be generated. Now code splitting is aware of this undefined substitution behavior and ignores these symbol uses. + +## 0.6.1 + +* Allow bundling with stdin as input ([#212](https://github.com/evanw/esbuild/issues/212)) + + You can now use `--bundle` without providing any input files and the input will come from stdin instead. Use `--sourcefile=...` to set the name of the input file for error messages and source maps. Dependencies of the input file will be resolved relative to the current working directory. + + ``` + # These two commands are now basically equivalent + esbuild --bundle example.js + esbuild --bundle < example.js --sourcefile=example.js + ``` + + This option has also been added to the JavaScript and Go APIs. If needed, you can customize the resolve directory with the `resolveDir` option: + + ```js + const {outputFiles: [stdout]} = await build({ + stdin: { + contents: ` + import {version} from './package.json' + console.log(version as string) + `, + sourcefile: 'example.ts', + resolveDir: __dirname, + loader: 'ts', + }, + bundle: true, + write: false, + }) + console.log(stdout) + ``` + +* Implement `extends` for `tsconfig.json` ([#233](https://github.com/evanw/esbuild/issues/233)) + + A `tsconfig.json` file can inherit configurations from another file using the `extends` property. Before this release, esbuild didn't support this property and any inherited settings were missing. Now esbuild should include these inherited settings. + +* Allow manually overriding `tsconfig.json` ([#226](https://github.com/evanw/esbuild/issues/226)) + + Normally esbuild finds the appropriate `tsconfig.json` file by walking up the directory tree. This release adds the `--tsconfig=...` flag which lets you disable this feature and force esbuild to use the provided configuration file instead. This corresponds to the TypeScript compiler's `--project` flag. + +* Remove gaps in source maps within a file ([#249](https://github.com/evanw/esbuild/issues/249)) + + The widely-used [source-map](https://github.com/mozilla/source-map) library for parsing source maps [has a bug](https://github.com/mozilla/source-map/issues/261) where it doesn't return mappings from previous lines. This can cause queries within generated code to fail even though there are valid mappings on both sides of the query. + + To work around this issue with the source-map library, esbuild now generates a mapping for every line of code that is generated from an input file. This means that queries with the source-map library should be more robust. For example, you should now be able to query within a multi-line template literal and not have the query fail. + + Note that some lines of code generated during bundling will still not have source mappings. Examples include run-time library code and cross-chunk imports and exports. + +## 0.6.0 + +* Output directory may now contain nested directories ([#224](https://github.com/evanw/esbuild/issues/224)) + + Note: This is a breaking change if you use multiple entry points from different directories. Output paths may change with this upgrade. + + Previously esbuild would fail to bundle multiple entry points with the same name because all output files were written to the same directory. This can happen if your entry points are in different nested directories like this: + + ``` + src/ + ├─ a/ + │ └─ page.js + └─ b/ + └─ page.js + ``` + + With this release, esbuild will now generate nested directories in the output directory that mirror the directory structure of the original entry points. This avoids collisions because the output files will now be in separate directories. The directory structure is mirrored relative to the [lowest common ancestor](https://en.wikipedia.org/wiki/Lowest_common_ancestor) among all entry point paths. This is the same behavior as [Parcel](https://github.com/parcel-bundler/parcel) and the TypeScript compiler. + +* Silence errors about missing dependencies inside try/catch blocks ([#247](https://github.com/evanw/esbuild/issues/247)) + + This release makes it easier to use esbuild with libraries such as [debug](npmjs.com/package/debug) which contain a use of `require()` inside a `try`/`catch` statement for a module that isn't listed in its dependencies. Normally you need to mark the library as `--external` to silence this error. However, calling `require()` and catching errors is a common pattern for conditionally importing an unknown module, so now esbuild automatically treats the missing module as external in these cases. + +* TypeScript type definitions for the browser API + + The node-based JavaScript API already ships with TypeScript type checking for the `esbuild` and `esbuild-wasm` packages. However, up until now the browser-based JavaScript API located in `esbuild-wasm/lib/browser` did not have type definitions. This release adds type definitions so you can now import `esbuild-wasm/lib/browser` in TypeScript and get type checking. + +* Add chunk imports to metadata file ([#225](https://github.com/evanw/esbuild/issues/225)) + + With code splitting, it's sometimes useful to list out the chunks that will be needed by a given entry point. For example, you may want to use that list to insert one `` tag for each chunk in your page header. This information is now present in the JSON metadata file that's generated with the `--metafile` flag. Each object in the `outputs` map now has an `imports` array, and each import has a `path`. + +## 0.5.26 + +* Allow disabling non-existent modules with the `browser` package.json field ([#238](https://github.com/evanw/esbuild/issues/238)) + + The [browser field](https://github.com/defunctzombie/package-browser-field-spec) in package.json allows you to disable a module (i.e. force it to become empty) by adding an override that points to `false`. Previously esbuild still required it to have an existing absolute path on the file system so that the disabled module could have a consistent identity. Now this is no longer required, so you can disable modules that don't exist on the file system. For example, you can now use this feature to disable the `fs` module. + +* Fix a bug with syntax transformation and `super()` calls ([#242](https://github.com/evanw/esbuild/issues/242)) + + In certain situations, esbuild accidentally transformed a class constructor such that a call to `super()` that came first in the original code no longer came first in the generated code. This code generation bug has now been fixed. Calls to `super()` that come first are should now stay that way. + +## 0.5.25 + +* Performance improvment for repeated API calls + + Previously every build or transform API call required parsing a new copy of the [esbuild JavaScript runtime code](internal/runtime/runtime.go). This added a constant overhead for every operation. Now the parsing of the runtime code is cached across API calls. The effect on performance depends on the size of the files you're transforming. Transform API calls appear to be >2x faster for small files, around ~10% faster for normal-sized files, and insignificant for large files. + +* Add a binary loader + + You can now assign the `binary` loader to a file extension to load all files of that type into a Uint8Array. The data is encoded as a base64 string and decoded into a Uint8Array at run time. The decoder defaults to a custom platform-independent implementation (faster than `atob`) but it switches to using the `Buffer` API with `--platform=node`. + +* Add fine-grained `--target` environments ([#231](https://github.com/evanw/esbuild/issues/231)) + + You can now configure individual JavaScript environments as targets. The `--target` flag now takes a comma-separated list of values like this: `--target=chrome58,firefox57,safari11,edge16`. Compatibility data was mainly sourced from [this widely-used compatibility table](https://kangax.github.io/compat-table/es2016plus/). + + There is also now an additional `es5` target. Since no transforms to ES5 are implemented yet, its purpose is mainly to prevent ES6 syntax from accidentally being compiled. This target also prevents esbuild from doing some ES6-specific optimizations that would unintentionally change ES5 code into ES6 code. + +## 0.5.24 + +* Smaller code for loaders that generate expressions + + Loaders that generate expressions (`json`, `text`, `base64`, `file`, and `dataurl`) export them using an assignment to `module.exports`. However, that forces the creation of a CommonJS module which adds unnecessary extra code. Now if the file for that loader is only imported using ES6 import statements instead of `require()`, the expression is exported using an `export default` statement instead. This generates smaller code. The bundler still falls back to the old `module.exports` behavior if the file is imported using `require()` instead of an ES6 import statement. + + Example input file: + + ```js + import txt from './example.txt' + console.log(txt) + ``` + + Old bundling behavior: + + ```js + // ...code for __commonJS() and __toModule() omitted... + + // example.txt + var require_example = __commonJS((exports, module) => { + module.exports = "This is a text file."; + }); + + // example.ts + const example = __toModule(require_example()); + console.log(example.default); + ``` + + New bundling behavior: + + ```js + // example.txt + var example_default = "This is a text file."; + + // example.ts + console.log(example_default); + ``` + + In addition, top-level properties of imported JSON files are now converted into individual ES6 exports for better tree shaking. For example, that means you can now import the `version` property from your `package.json` file and the entire JSON file will be removed from the bundle: + + ```js + import {version} from './package.json' + console.log(version) + ``` + + The example above will now generate code that looks like this: + + ```js + // package.json + var version = "1.0.0"; + + // example.ts + console.log(version); + ``` + +## 0.5.23 + +* Fix `export declare` inside `namespace` in TypeScript ([#227](https://github.com/evanw/esbuild/issues/227)) + + The TypeScript parser assumed that ambient declarations (the `declare` keyword) just declared types and did not affect the output. This was an incorrect assumption for exported declarations of local variables inside namespaces. The assignment to `foo` in the example below must be rewritten to an assignment to `ns.foo`: + + ```ts + namespace ns { + export declare let foo: number + foo = 123 + } + ``` + + This should now work correctly. + +* Preserve certain statement-level comments ([#221](https://github.com/evanw/esbuild/issues/221)) + + Statement-level comments starting with `//!` or `/*!` or containing `@preserve` or `@license` are now preserved in the output. This matches the behavior of other JavaScript tools such as [Terser](https://github.com/terser/terser). + +* Higher memory limit for synchronous JavaScript APIs ([#228](https://github.com/evanw/esbuild/issues/228)) + + Apparently the synchronous APIs in node's child process module that esbuild relies on will fail with `ENOBUFS` if the output is larger than a certain size. This caused issues with the `write: false` feature from the previous release. The memory limit has been raised to 16mb which should hopefully avoid these crashes. If that limit is still too low, it can be overridden with the `ESBUILD_MAX_BUFFER` environment variable. + +## 0.5.22 + +* JavaScript build API can now avoid writing to the file system ([#139](https://github.com/evanw/esbuild/issues/139) and [#220](https://github.com/evanw/esbuild/issues/220)) + + You can now pass `write: false` to the JavaScript build API to avoid writing to the file system. Instead, the returned object will have the `outputFiles` property with an array of output files, each of which has a string `path` property and a Uint8Array `contents` property. This brings the JavaScript API to parity with the Go API, which already had this feature. + +* Support `/* @__PURE__ */` annotations for tree shaking + + You can now annotate call expressions and new expressions with a `/* @__PURE__ */` comment, which tells esbuild that the function call is allowed to be removed if the result is not used. This is a convention from other tools (e.g. UglifyJS and Rollup). + + For example, the code below will now be completely removed during bundling if the `fib` variable is never used. The initializer is a function call and esbuild cannot determine that it has no side effects, but the annotation forces esbuild to consider it removable anyway: + + ```js + let fib = /* @__PURE__ */ (() => { + let cache = {} + return function f(n) { + return cache[n] || (cache[n] = + n <= 2 ? 1 : f(n - 1) + f(n - 2)); + } + })() + ``` + +* Add `--pure:name` to annotate calls to globals ([#28](https://github.com/evanw/esbuild/issues/28)) + + This flag makes calls to the named function behave as if that call was prefixed by `/* @__PURE__ */`. For example, `--pure:console.log` means calls to `console.log()` will behave as if they were calls to `/* @__PURE__ */ console.log()` instead. This means when `--minify` is active, the calls will be removed as long as the return value is unused (any function arguments with side effects will be kept, however). + +* Add basic tree shaking of JSX elements + + Automatically-generated calls to the JSX factory function (usually `React.createElement`) are now marked as `/* @__PURE__ */`. This means the construction of a JSX element is now not considered to have side effects. For example, the code below will be completely removed during bundling if the `element` variable is never used: + + ```jsx + let element =
an unused element
+ ``` + +* Fixed a concurrency issue with the JavaScript API + + Before this release, multiple concurrent JavaScript API calls that used different values for the `define` option could end up using the value from another API call. This bug was due to inverted boolean logic in code that was intended to cache the define map only when there were no user-specified defines. The issue has been fixed. + +## 0.5.21 + +* Binaries for FreeBSD ([#217](https://github.com/evanw/esbuild/pull/217)) + + There are now esbuild binaries for FreeBSD, both for AMD64 and ARM64. This was contributed by [@kikuchan](https://github.com/kikuchan). + +* Remove nested `node_modules` directory + + The install script for the `esbuild` npm package invokes `npm` recursively to install the binary for the current platform. However, the left over nested `node_modules` directory could potentially cause problems with tools that scan for nested `node_modules` directories. Now the install script no longer leaves a nested `node_modules` directory around after finishing. + +## 0.5.20 + +* Allow multiple `.` characters in loader extensions ([#215](https://github.com/evanw/esbuild/issues/215)) + + You are now able to configure two loaders such that one is the suffix of the other. For example, you can now configure both `--loader:.txt=text` and `--loader:.base64.txt=base64`. The loader with the longer matching suffix will be used. + +* Add support for scoped external packages ([#214](https://github.com/evanw/esbuild/issues/214)) + + You can now mark scoped packages as external. For example, `--external:@babel/core` marks the package `@babel/core` as external. This was contributed by [@floydspace](https://github.com/floydspace). + +* Add support for external paths ([#127](https://github.com/evanw/esbuild/issues/127) and [#191](https://github.com/evanw/esbuild/issues/191)) + + Previously the `--external:M` flag only worked if `M` was a package name. For example, you can mark the `fs` package as external with `--external:fs`. + + With this release, you can now also mark file paths as external using the same syntax. For example, `--external:./index.js` marks the file `index.js` in the current working directory as external. The path to the external module used in the output file will be relative to the output directory. + +## 0.5.19 + +* Fix bug with TypeScript `typeof` operator ([#213](https://github.com/evanw/esbuild/issues/213)) + + The TypeScript parser in esbuild incorrectly treated `readonly` in `typeof readonly` as a type operator instead of an identifier, which meant that it expected a type expression to follow the `readonly` identifier. Type expressions containing `typeof readonly` are now parsed correctly. + +## 0.5.18 + +* Fix bug with code splitting and side effects + + This release fixes a bug with experimental code splitting. Chunks with side effects but without any exports were not imported by the entry points that depended on them, which meant that their side effects accidentally did not occur. The fix ensures that all entry points import all relevant chunks regardless of whether or not the chunks have exports, so their side effects should never be omitted. + +## 0.5.17 + +* Pass through `import.meta` syntax ([#208](https://github.com/evanw/esbuild/issues/208)) + + The `import.meta` syntax is a way for code in an ES6 module to access metadata about itself. For example, `import.meta.url` in the browser is the URL of the current module. + + It's a new feature that doesn't work in older browsers, so esbuild converts it to a module-local variable to avoid generating code with a syntax error. However, this is only necessary when targeting older browsers or if the output format doesn't support `import.meta`. + + The `import.meta` syntax is now passed through unmodified when the target is `es2020` or newer and the output format is `esm`. This lets you use features such as `import.meta.url` in those situations. + +## 0.5.16 + +* Experimental code splitting with `--splitting` ([#16](https://github.com/evanw/esbuild/issues/16)) + + This release includes experimental support for code splitting. Enable it with the `--splitting` flag. This currently only works with the `esm` output format. Support for the `cjs` and `iife` formats will come later. It's being released early so people can try it out and provide feedback. + + When enabled, code splitting does two things: + + * An asynchronous `import('path')` expression will create another chunk that will only be loaded when that expression is evaluated. This is intended to be used for lazily loading additional code. All additional chunks will be written to the directory configured with `outdir`. + + Note that when code splitting is disabled (i.e. the default behavior), an `import('path')` expression behaves similar to `Promise.resolve(require('path'))` and still bundles the imported file into the entry point bundle. No additional chunks are generated in this case. + + * Multiple entry points will cause additional chunks to be created for code that is shared between entry points. Chunks are generated automatically based on simple principles: code should only ever be in one chunk (i.e. no duplication) and no unnecessary code should be loaded (i.e. chunk boundaries are minimal). + + The way this works is by traversing through the module dependency graph and marking which top-level statements are reachable from which entry points. The set of entry points for a given top-level statement determines which chunk that statement is in. + + This is an advanced form of code splitting where even a single file may end up being split into different chunks. This is not something most other bundlers can do at the moment. + + Note that using code splitting with many entry points may generate many chunks for shared code reachable from different combinations of entry points. This should work fine and should still be efficient with HTTP/2. If you want to only let certain entry points share code, you can run esbuild multiple times for different groups of entry points. + + Please try it out and report any issues on [#16](https://github.com/evanw/esbuild/issues/16). + +## 0.5.15 + +* Remove some unnecessary helper functions ([#206](https://github.com/evanw/esbuild/issues/206)) + + Certain unnecessary helper functions were sometimes generated when the output format was `esm`. These helper functions should now only be generated when necessary. + +* Optimize CommonJS-to-ES6 module conversion + + CommonJS modules that exported raw strings were unnecessarily slow when imported using an ES6 import statement. This scenario should now be much faster. + + The CommonJS-to-ES6 module conversion in esbuild copies properties off the object one-by-one onto a new object. This is the same approach that the TypeScript compiler uses. However, strings have numeric properties 0 to N-1 where N is the length of the string. Copying all of these numeric properties can take a significantly long time for long strings and is almost certainly unhelpful. Now esbuild's CommonJS-to-ES6 module conversion only copies properties if the export is an object. + +* Support JSX fields in `tsconfig.json` + + This release adds support for the `jsxFactory` and `jsxFragmentFactory` fields in `tsconfig.json`. Now you do not have to configure JSX again for esbuild if you have already configured it for TypeScript. The `jsxFragmentFactory` field is a [new feature in the upcoming TypeScript 4.0 release](https://devblogs.microsoft.com/typescript/announcing-typescript-4-0-beta/#custom-jsx-factories). + +## 0.5.14 + +* Prevent assignment to ES6 imports ([#202](https://github.com/evanw/esbuild/issues/202)) + + ES6 imports are live bindings to other values, sort of like a getter-only property on an object. An assignment to an import identifier should cause a `TypeError` at run time according to the specification. However, when bundling esbuild performs the "scope hoisting" optimization and merges all modules into a single scope. Imports inside the bundle refer to the imported identifiers without any indirection and an assignment will not throw a `TypeError` at run time. + + This release turns assignments to imports into compile-time errors to reject invalid code instead of allowing it to cause this non-conforming behavior. Handling this at compile-time is consistent with other tools such as TypeScript and Rollup. + +* Exclude external child paths from the bundle ([#186](https://github.com/evanw/esbuild/pull/186)) + + Marking a module as external via `--external:foo` means any imports for the module `foo` will be preserved in the output instead of being traversed by the bundler. This is helpful if the module contains problematic code such as a native node module that can't be bundled. + + However, code often uses child paths to import a file within a module directly such as `import "foo/bar"`. These paths accidentally bypassed the external module check. The fix means all paths under an external module are now also considered external. This was contributed by [@floydspace](https://github.com/floydspace). + +## 0.5.13 + +* Add support for TypeScript labelled tuples + + This is a new TypeScript feature to be released in TypeScript 4. Tuple types can now have labels: + + ```ts + let foo: [number, number] // Without labels + let bar: [min: number, max: number] // With labels + ``` + + These labels are ignored by the TypeScript compiler and are only there to improve readability. You can read more here: https://devblogs.microsoft.com/typescript/announcing-typescript-4-0-beta/. + +## 0.5.12 + +* Fix a JSX whitespace bug ([#195](https://github.com/evanw/esbuild/issues/195)) + + Whitespace behavior in JSX has unfortunately been [left out of the JSX specification](https://github.com/facebook/jsx/issues/6), so it's up to each implementation to determine how to handle whitespace characters. Most of the JSX parsers in the ecosystem have converged on similar behavior. When they differ, esbuild follows the behavior of the TypeScript JSX parser. + + This release fixes a bug where esbuild's JSX parser behaved differently than TypeScript. Certain whitespace characters between JSX elements were incorrectly removed. For example, the space in `
` must be preserved to match the TypeScript JSX parser. These cases now have test coverage. + +## 0.5.11 + +* Fix a JavaScript API crash on node 10.x + + The current LTS version of node is 12.x, but some people are still running 10.x and want to use esbuild. Before this fix, attempting to use the esbuild JavaScript API with node 10.x would crash with `ReferenceError: TextEncoder is not defined`. The JavaScript API has been changed to not require `TextEncoder` and now works fine with node 10.x. + +## 0.5.10 + +* Transform object rest properties + + This release transforms object rest property bindings such as `let {...x} = y` when the language target is set to `--target=es2017` or earlier. + + If you're using Babel to transform your source code to ES6 for older browsers, this probably means esbuild's JavaScript API could now be a suitable replacement for Babel in your case. The only remaining features that esbuild can't yet transform to ES6 are a few very rarely used features that don't matter for the vast majority of real-world code (`for async` loops and `async` generators). + +## 0.5.9 + +* Add the `--strict:nullish-coalescing` option + + This affects the transform for the `??` nullish coalescing operator. In loose mode (the default), `a ?? b` is transformed to `a != null ? a : b`. This works fine in all cases except when `a` is the special object `document.all`. In strict mode, `a ?? b` is transformed to `a !== null && a !== void 0 ? a : b` which works correctly with `document.all`. Enable `--strict:nullish-coalescing` if you need to use `document.all` with the `??` operator. + +* Add the `--strict:class-fields` option + + This affects the transform for instance and static class fields. In loose mode (the default), class field initialization is transformed to a normal assignment. This is what the TypeScript compiler does by default. However, it doesn't follow the JavaScript specification exactly (e.g. it may call setter methods). Either enable `--strict:class-fields` or add `useDefineForClassFields` to your `tsconfig.json` file if you need accurate class field initialization. + +Note that you can also just use `--strict` to enable strictness for all transforms instead of using `--strict:...` for each transform. + +## 0.5.8 + +* Transform async functions ([#137](https://github.com/evanw/esbuild/issues/137)) + + This release transforms async functions into generator functions for older browsers when the language target is set to `--target=es2016` or below. The transform esbuild uses is similar to the one used by the TypeScript compiler. + +## 0.5.7 + +* Transform private fields and private methods ([#47](https://github.com/evanw/esbuild/issues/47)) + + Private names are an access control mechanism for classes. They begin with a `#` and are not accessible outside of the class they are declared in. Support for parsing this syntax was added in esbuild version 0.4.9 but the syntax was passed through unmodified, meaning it didn't work in older browsers. + + This release adds support for transforming private fields and private methods for older browsers that don't support this syntax. This transform uses `WeakMap` and `WeakSet` to preserve the privacy properties of this feature, similar to the corresponding transforms in the Babel and TypeScript compilers. + + This code: + + ```js + class Counter { + #count = 1 + get value() { return this.#count } + increment() { ++this.#count } + } + ``` + + is transformed into this code when using `--target=es2020`: + + ```js + var _count; + class Counter { + constructor() { _count.set(this, 1); } + get value() { return __privateGet(this, _count); } + increment() { __privateSet(this, _count, +__privateGet(this, _count) + 1); } + } + _count = new WeakMap(); + ``` + + Note that most modern JavaScript engines (V8, JavaScriptCore, and SpiderMonkey but not ChakraCore) may not have good performance characteristics for large `WeakMap` and `WeakSet` objects. Creating many instances of classes with private fields or private methods with this syntax transform active may cause a lot of overhead for the garbage collector. This is because modern engines (other than ChakraCore) store weak values in an actual map object instead of as hidden properties on the keys themselves, and large map objects can cause performance issues with garbage collection. See [this reference](https://github.com/tc39/ecma262/issues/1657#issuecomment-518916579) for more information. + +* Fix re-exports when bundling + + This is similar to the fix for re-exports in version 0.5.6 except that it applies when bundling, instead of just when transforming. It needed to be fixed differently because of how cross-file linking works when bundling. + +## 0.5.6 + +* Fix re-export statements ([#190](https://github.com/evanw/esbuild/issues/190)) + + The previous release caused a regression due to some behind-the-scenes work for the upcoming code splitting feature. The re-export alias in statements of the form `export { foo as bar } from 'path'` could sometimes incorrectly be renamed to something else, such as `foo` becoming `foo2`. This release fixes the bug. + +## 0.5.5 + +* Implement logical assignment operator transforms + + There are three new logical assignment operators: `??=`, `&&=`, and `||=`. With this release, you can now use them in older browsers by setting `--target` to a language version other than `esnext`. See [the V8 blog post](https://v8.dev/features/logical-assignment) for more information about how they work. + +* Fix re-exports of a CommonJS module in `esm` format + + Previously re-exports of an individual identifier from a CommonJS module generated JavaScript that crashed at run-time when using the `esm` output format. This was because esbuild always tries to generate "live" exports for CommonJS modules that always return the current value of the export instead of "dead" bindings that only return the initial value of the export. The bug happened because the ES6 module format doesn't have a way to forward a live binding to a CommonJS module as an ES6 export. The fix is to generate "dead" exports instead, which is the only available option in this edge case. + + These input files: + + ```js + // entry_point.js + export {foo} from './cjs-format.js' + ``` + + ```js + // cjs-format.js + Object.defineProperty(exports, 'foo', { + enumerable: true, + get: () => Math.random(), + }) + ``` + + Now become this output file: + + ```js + // cjs-format.js + var require_cjs_format = __commonJS((exports) => { + Object.defineProperty(exports, "foo", { + enumerable: true, + get: () => Math.random() + }); + }); + + // entry_point.js + const cjs_format = __toModule(require_cjs_format()); + const export_foo = cjs_format.foo; // This is a "dead" re-export + export { + export_foo as foo + }; + ``` + +## 0.5.4 + +* Source maps use `/` on Windows ([#188](https://github.com/evanw/esbuild/issues/188)) + + Before generated source maps used `\` on Windows, which meant that tools consuming these source maps (e.g. Chrome) didn't recognize these characters as path separators. Now all platforms consistently use `/` as a path separator. + +* Prevent input files from being overwritten + + There are now checks in place to avoid input files being accidentally overwritten. This could easily happen with `--bundle --outdir=.` when bundling JavaScript files since the output file name ends up being the same as the entry point name, and is written to the same directory. + +## 0.5.3 + +* Special-case `require` in browserify bundles ([#80](https://github.com/evanw/esbuild/issues/80) and [#90](https://github.com/evanw/esbuild/issues/90)) + + [Browserify](https://browserify.org/) generates code containing the expression `typeof require == "function" && require` which then ends up in a lot of npm packages. This expression is problematic because bundling involves statically determining all source files and their dependencies. Using `require` dynamically like this defeats the static analysis. It's also problematic because esbuild replaces `typeof require == "function"` with `true` since `require` is a function at compile-time when bundling. Then `true && require` becomes `require` in the generated code, which crashes at run time. + + Previously esbuild would generate an error for these expressions. Now esbuild replaces `typeof require == "function" && require` with `false` when targeting the browser and `require` when targeting node. This matches the intent of the browserify prelude snippet and allows esbuild to build libraries containing this code without errors or warnings. + +* Allow dynamic dependencies ([#113](https://github.com/evanw/esbuild/issues/113)) + + Bundling `require()` or `import()` when the argument isn't a string literal is a dynamic dependency. The dependency path relies on dynamic run-time behavior and cannot be statically determined by esbuild at bundle time. + + Dynamic dependencies used to be an error but are now just a warning. Builds containing them now succeed and the generated code contains the `require()` or `import()` expression. This is useful either when the dynamic dependency is intentional or when you know the dynamic dependency won't ever be triggered. Doing this still generates a warning to alert you that some code was excluded from the bundle and because these expressions may still crash at run time if the imported path isn't valid. + +## 0.5.2 + +* Fix a regression with `--define` and identifiers + + The API refactor introduced a regression where using a `--define` flag to replace something with an identifier followed by another `--define` flag unintentionally caused the first `--define` to use the value from the second `--define` for replacement. This regression was caused by a loop that was added around a Go closure, which caused all closures in that loop to close over the same variable. The bug has been fixed. + +* Fix interpretation of legacy `-->` single-line HTML comments + + The `-->` sequence starts a single-line comment similar to `//`. This is legacy behavior from [annex B](https://www.ecma-international.org/ecma-262/6.0/#sec-html-like-comments) under the name `SingleLineHTMLCloseComment`. However, `-->` was incorrectly treated as the start of a comment even when it didn't come at the beginning of the line. Now `-->` only starts a comment if there are no tokens before it on that line. + +* Allow shadowing of CommonJS variables ([#165](https://github.com/evanw/esbuild/issues/165)) + + It's now no longer an error to re-declare `exports`, `module`, or `require` in a module scope. The re-declared symbol will just silently shadow the CommonJS variable with that name. This allows to use a variable called `exports` in an ES6 module, for example. + +## 0.5.1 + +* Go documentation was moved to godoc ([#177](https://github.com/evanw/esbuild/pull/177)) + + The Go documentation is now in the source files itself instead of in an external Markdown file. View it online at https://godoc.org/github.com/evanw/esbuild/pkg/api and https://godoc.org/github.com/evanw/esbuild/pkg/cli. + +* The browser API now works in a script tag + + The initial release of the browser API required a bundler to use correctly since it was in CommonJS format. This release adds the ability to use the browser API directly in HTML. + + Here's an example using https://unpkg.com/ for simplicity, although you should consider hosting the files yourself: + + ```html + + + ``` + +## 0.5.0 + +* Overhaul public-facing API code + + This is a rewrite of all externally facing API code. It fixes some bugs and inconsistencies, adds some new features, and makes it easier to support various use cases going forward. + + At a high-level, esbuild's API supports two separate operations: "build" and "transform". Building means reading from the file system and writing back to the file system. Transforming takes an input string and generates an output string. You should use the build API if you want to take advantage of esbuild's bundling capability, and you should use the transform API if you want to integrate esbuild as a library inside another tool (e.g. a "minify" plugin). This rewrite ensures the APIs for these two operations are exposed consistently for all ways of interacting with esbuild (both through the CLI and as a library). + + Here are some of the highlights: + + * There is now a public Go API ([#152](https://github.com/evanw/esbuild/issues/152)) + + The main API can be found in the [`github.com/evanw/esbuild/pkg/api`](pkg/api/api.go) module. It exposes the exact same features as the JavaScript API. This means you can use esbuild as a JavaScript transformation and bundling library from Go code without having to run esbuild as a child process. There is also the [`github.com/evanw/esbuild/pkg/cli`](pkg/cli/cli.go) module which can be used to wrap the esbuild CLI itself. + + * There are now synchronous JavaScript APIs ([#136](https://github.com/evanw/esbuild/issues/136)) + + Sometimes JavaScript source transformations must be synchronous. For example, using esbuild's API to shim `require()` for `.ts` files was previously not possible because esbuild only had an asynchronous transform API. + + This release adds the new `transformSync()` and `buildSync()` synchronous functions to mirror the existing `transform()` and `build()` asynchronous functions. Note that these synchronous calls incur the cost of starting up a new child process each time, so you should only use these instead of `startService()` if you have to (or if you don't care about optimal performance). + + * There is now an experimental browser-based API ([#172](https://github.com/evanw/esbuild/issues/172)) + + The `esbuild-wasm` package now has a file called `browser.js` that exposes a `startService()` API which is similar to the esbuild API available in node. You can either import the `esbuild-wasm` package using a bundler that respects the `browser` field in `package.json` or import the `esbuild-wasm/lib/browser.js` file directly. + + This is what esbuild's browser API looks like: + + ```ts + interface BrowserOptions { + wasmURL: string + worker?: boolean + } + + interface BrowserService { + transform(input: string, options: TransformOptions): Promise + stop(): void + } + + declare function startService(options: BrowserOptions): Promise + ``` + + You must provide the URL to the `esbuild-wasm/esbuild.wasm` file in `wasmURL`. The optional `worker` parameter can be set to `false` to load the WebAssembly module in the same thread instead of creating a worker thread. Using a worker thread is recommended because it means transforming will not block the main thread. + + This API is experimental and may be changed in the future depending on the feedback it gets. + + * Error messages now use `sourcefile` ([#131](https://github.com/evanw/esbuild/issues/131)) + + Errors from transform API calls now use `sourcefile` as the original file name if present. Previously the file name in error messages was always `/input.js`. + +## 0.4.14 + +* Do not reorder `"use strict"` after support code ([#173](https://github.com/evanw/esbuild/issues/173)) + + Even when not in bundling mode, esbuild sometimes adds automatically-generated support code at the start of the output file. For example, using the `**` operator with `--target=es2015` causes `let __pow = Math.pow` to be inserted at the start of the file. This interfered with `"use strict"` directives, which must come first. Now `"use strict"` directives are written out first before any automatically-generated support code. + +* Fix bug with export star pointing to a re-export ([#176](https://github.com/evanw/esbuild/issues/176)) + + This fixes a tree shaking bug that involves an `export * from ...` statement pointing to a file with a `export {name} from ...` statement. Now `name` will no longer be incorrectly removed from the bundle. + +## 0.4.13 + +* Fix possible name collision with CommonJS the target ([#174](https://github.com/evanw/esbuild/issues/174)) + + A bug meant that the export objects for individual modules with the same filename could in some cases end up reusing the same name in the output file, which then caused a syntax error. This only happened with the `cjs` target. The bug has been fixed. + +## 0.4.12 + +* Support `export * from ...` for CommonJS modules ([#159](https://github.com/evanw/esbuild/issues/159)) + + Wildcard re-exports are now supported when the exports come from a CommonJS or external module. Since CommonJS modules are not statically analyzable, this means in these cases the re-exports are evaluated at run time instead of at bundle time. Modules that re-export symbols this way will also be considered CommonJS modules during bundling because their exports are now also not statically analyzable. + +* Add 3rd-party library test coverage + + From the esbuild repo, you can now run `make test-extra` to build some 3rd-party libraries (Rollup, Sucrase, and Esprima) with esbuild and run their test suites. This ensures that these libraries will continue to work as esbuild releases new features. + +## 0.4.11 + +* Fix top-level name minification with runtime + + When not bundling, esbuild only minifies top-level names if the file is an ES6 module (as determined by the presence of an ES6 import or export statement). This determination had a bug where a non-module file was considered a module if esbuild automatically generated an import to some internal support code called the "runtime". For example, using the `**` operator with `--target=es2015` generates an import for the `__pow` runtime function. Runtime imports are now ignored for module determination, so an automatically-generated runtime import no longer causes top-level names to be minified. + +* Fix class name generation for default exports + + Some changes to name generation for TypeScript decorators caused the generated class name for `export default class` statements to sometimes not match the name used for other references to that class in the same file. This bug has been fixed. + +## 0.4.10 + +* Initial implementation of TypeScript decorators ([#104](https://github.com/evanw/esbuild/issues/104)) + + This release contains an initial implementation of the non-standard TypeScript-specific decorator syntax. This syntax transformation is enabled by default in esbuild, so no extra configuration is needed. The TypeScript compiler will need `"experimentalDecorators": true` configured in `tsconfig.json` for type checking to work with TypeScript decorators. + + Here's an example of a method decorator: + + ```ts + function logged(target, key, descriptor) { + let method = descriptor.value + descriptor.value = function(...args) { + let result = method.apply(this, args) + let joined = args.map(x => JSON.stringify(x)).join(', ') + console.log(`${key}(${joined}) => ${JSON.stringify(result)}`) + return result + } + } + + class Example { + @logged + method(text: string) { + return text + '!' + } + } + + const x = new Example + x.method('text') + ``` + + There are four kinds of TypeScript decorators: class, method, parameter, and field decorators. See [the TypeScript decorator documentation](https://www.typescriptlang.org/docs/handbook/decorators.html) for more information. Note that esbuild only implements TypeScript's `experimentalDecorators` setting. It does not implement the `emitDecoratorMetadata` setting because that requires type information. + +* Fix order of side effects for computed fields + + When transforming computed class fields, esbuild had a bug where the side effects of the field property names were not evaluated in source code order. The order of side effects now matches the order in the source code. + +* Fix private fields in TypeScript + + This fixes a bug with private instance fields in TypeScript where the private field declaration was incorrectly removed during the TypeScript class field transform, which inlines the initializers into the constructor. Now the initializers are still moved to the constructor but the private field declaration is preserved without the initializer. + + Note that since static private fields are not currently supported by the official TypeScript compiler, they are also not supported by esbuild in TypeScript files. They are supported by esbuild in JavaScript files, however. + +## 0.4.9 + +* Initial support for private names ([#47](https://github.com/evanw/esbuild/issues/47)) + + Private names are an access control mechanism for classes. They begin with a `#` and are not accessible outside of the class they are declared in. The private name syntax can now be parsed, printed, and minified correctly. Transforming this syntax for older browsers is not supported yet. This is what the syntax looks like: + + ```js + class Counter { + #count = 1 + get value() { return this.#count } + increment() { this.#count++ } + } + ``` + + You can read more about these features here: + + * https://github.com/tc39/proposal-private-methods + * https://github.com/tc39/proposal-class-fields + * https://github.com/tc39/proposal-static-class-features + +* Initial support for logical assignment operators + + This adds support for the three new logical assignment operators `||=`, `&&=`, and `??=`, which can now be parsed and passed through to the output. Transforming this syntax for older browsers is not supported yet. You can read more about these operators here: https://github.com/tc39/proposal-logical-assignment. + +* Data loaders now set "no side effects" + + Files loaded using the `json`, `text`, `base64`, `dataurl`, and `file` loaders are now removed from the bundle if the files that import them never use the imports. This is the same behavior as the `"sideEffects": false` setting in `package.json`. + +## 0.4.8 + +* Add the `--metafile` flag ([#140](https://github.com/evanw/esbuild/issues/140)) + + Pass `--metafile=meta.json` to write metadata about the build to the file `meta.json`. This includes information such as which files are in the bundle, what other files a given file depends on, and how much of each file ended up in the bundle. This is similar to the [stats option in Webpack](https://webpack.js.org/api/stats/). + + The format looks like this: + + ```ts + interface Metadata { + inputs: { + [path: string]: { + bytes: number + imports: { + path: string + }[] + } + } + outputs: { + [path: string]: { + bytes: number + inputs: { + [path: string]: { + bytesInOutput: number + } + } + } + } + } + ``` + +* Shorten numeric literals ([#122](https://github.com/evanw/esbuild/issues/122)) + + Certain numeric literals now use shorter representations in the generated JavaScript code. For example, `123400000` is now written out as `1234e5`. + +## 0.4.7 + +* Fixed `sideEffects` and nested directories + + This fixes a bug where `package.json` files with `"sideEffects": false` were not respected for files in nested directories. When this bug occurred, bundles could be bigger than necessary. The `sideEffects` hint is now respected if any parent directory contains the hint instead of just the immediate enclosing directory. + +* Fixed `sideEffects` and default exports with side effects + + This fixes a bug with default exports with side effects inside a `"sideEffects": false` context that were imported and used. These exports were incorrectly discarded instead of being retained, which could cause the resulting bundle to crash. + +## 0.4.6 + +* Respect the `sideEffects` field when tree shaking ([#50](https://github.com/evanw/esbuild/issues/50)) + + Tree shaking now respects `"sideEffects": false` in `package.json`, which means esbuild now generates smaller bundles with certain libraries such as [lodash-es](https://www.npmjs.com/package/lodash-es). This setting is a [convention from Webpack](https://webpack.js.org/guides/tree-shaking/#mark-the-file-as-side-effect-free). Any files in a package with this setting will not be included in the bundle if they are imported using an ES6 import and then never used. + +## 0.4.5 + +* Fix a crash with more than 8 entry points ([#162](https://github.com/evanw/esbuild/pull/162)) + + This bug was due to the wrong index being used for an internal bit set. That caused a crash due to an out-of-bounds array read when esbuild is run with more than 8 entry points. I now have test coverage for large numbers of entry points, so this should not happen again. + +* Fix slash characters in file loader ([#164](https://github.com/evanw/esbuild/pull/164)) + + This fixes a bug where the base64-encoded hash included in the file name could sometimes contain a `/` character. The fix is to use the base64 character set for URL-encoding, which replaces the `/` character with a `_` character. + +## 0.4.4 + +* Fix optional chaining with TypeScript operators ([#168](https://github.com/evanw/esbuild/issues/168)) + + The work on optional chaining in the previous release introduced a regression where the TypeScript infix operators `!` and `<>` incorrectly stopped the propagation of optional chaining. That meant `a?.b!()` and `a?.b()` incorrectly behaved like `(a?.b)()` instead of `a?.b()`. This now has test coverage. + +* Add support for the `"paths"` field in `tsconfig.json` ([#60](https://github.com/evanw/esbuild/issues/60) and [#144](https://github.com/evanw/esbuild/issues/144)) + + This provides a way of remapping module paths to local file paths. It's relatively powerful because it supports wildcard patterns and multiple fallback locations. See [the documentation in the TypeScript handbook](https://www.typescriptlang.org/docs/handbook/module-resolution.html#path-mapping) for more information about how this feature works. This was contributed by [@viankakrisna](https://github.com/viankakrisna). + +* Add the `file` loader ([#14](https://github.com/evanw/esbuild/issues/14) and [#135](https://github.com/evanw/esbuild/pull/135)) + + The `file` loader copies the input file to the output directory and exports the path of the file as a string to any modules that import the file. For example, `--loader:.png=file` enables this loader for all imported `.png` files. This was contributed by [@viankakrisna](https://github.com/viankakrisna). + +* Add the `--resolve-extensions` flag ([#142](https://github.com/evanw/esbuild/pull/142)) + + This lets you override the implicit extensions that are tested when importing a file. It must be a comma-separated list of extensions. For example, setting `--resolve-extensions=.jsx,.js` means `import "./foo"` will check for `./foo` then `./foo.jsx` then `./foo.js` in that order. The behavior corresponds to [the similarly-named feature in Webpack](https://webpack.js.org/configuration/resolve/#resolveextensions). This was contributed by [@viankakrisna](https://github.com/viankakrisna). + +## 0.4.3 + +* Fix bug with optional chaining parentheses ([#156](https://github.com/evanw/esbuild/issues/156)) + + One edge case with JavaScript optional chaining syntax is that parentheses stop the chain. So `a?.b.c` will be `undefined` if `a` is nullish but `(a?.b).c` will crash if `a` is nullish. + + This was handled correctly when lowering is enabled (i.e. when the language target is `es2019` or below) but was not handled correctly when lowering is disabled (i.e. when the language target is `es2020` or above). The output for `(a?.b).c` was incorrectly `a?.b.c` instead of `(a?.b).c`, which would no longer crash if `a` is nullish. The fix is to preserve the parentheses in the output. + +* Support for the PowerPC 64-bit Little Endian architecture on Linux ([#146](https://github.com/evanw/esbuild/pull/146)) + + This was contributed by [@runlevel5](https://github.com/runlevel5). + +## 0.4.2 + +* Bind imports to re-exports ([#149](https://github.com/evanw/esbuild/issues/149)) + + This fixes a bug where imports of re-exported symbols were not correctly merged in some cases. This resulted in the generated code referencing symbols that were not declared, resulting in a crash. + +## 0.4.1 + +* Add a log level setting ([#117](https://github.com/evanw/esbuild/issues/117)) + + You can now silence esbuild except for errors with `--log-level=error`, or except for errors and warnings with `--log-level=warning`. + +* Now `jsconfig.json` is an alternative to `tsconfig.json` ([#132](https://github.com/evanw/esbuild/pull/132)) + + The `"baseUrl"` setting in `tsconfig.json`, which lets you avoid `../../` relative import paths, is respected by esbuild. With this change, esbuild will also check for this setting in `jsconfig.json` if no `tsconfig.json` file is found. This is relevant to some projects that use the TypeScript compiler with JavaScript files instead of TypeScript files. You can read more about this feature [here](https://code.visualstudio.com/docs/languages/jsconfig). This was contributed by [@viankakrisna](https://github.com/viankakrisna). + +* Chinese translation of documentation ([#129](https://github.com/evanw/esbuild/pull/129)) + + Both the readme and the architecture documentation have been translated into Chinese, which is available here: http://docs.breword.com/evanw-esbuild. This was contributed by [@92hackers](https://github.com/92hackers). + +* Async generator functions require `--target=es2018` + + This fixes a bug where async generator functions were incorrectly allowed with `--target=es2017`, which is incorrect because the [asynchronous iteration spec](https://github.com/tc39/proposal-async-iteration) is part of ES2018. + +## 0.4.0 + +* Add the `esm` output format ([#48](https://github.com/evanw/esbuild/issues/48)) + + It is now possible to generate a bundle in ES6 module format using `--format=esm`. The generated code uses ES6 import and export statements. This is useful for bundling code to be used as a library, for using in a ` + + ``` + + Previously the implementers of these languages had to use the `importsNotUsedAsValues` setting as a hack for esbuild to preserve the import statements. With this release, esbuild now follows the behavior of the TypeScript compiler so implementers will need to use the new `preserveValueImports` setting to do this instead. This is the breaking change. + +* TypeScript code follows JavaScript class field semantics with `--target=esnext` ([#1480](https://github.com/evanw/esbuild/issues/1480)) + + TypeScript 4.3 included a subtle breaking change that wasn't mentioned in the [TypeScript 4.3 blog post](https://devblogs.microsoft.com/typescript/announcing-typescript-4-3/): class fields will now be compiled with different semantics if `"target": "ESNext"` is present in `tsconfig.json`. Specifically in this case `useDefineForClassFields` will default to `true` when not specified instead of `false`. This means class field behavior in TypeScript code will now match JavaScript instead of doing something else: + + ```js + class Base { + set foo(value) { console.log('set', value) } + } + class Derived extends Base { + foo = 123 + } + new Derived() + ``` + + In TypeScript 4.2 and below, the TypeScript compiler would generate code that prints `set 123` when `tsconfig.json` contains `"target": "ESNext"` but in TypeScript 4.3 and above, the TypeScript compiler will now generate code that doesn't print anything. This is the difference between "assign" semantics and "define" semantics. + + Previously you had to create a `tsconfig.json` file and specify `"target": "ESNext"` to get this behavior in esbuild. With this release, you can now also just pass `--target=esnext` to esbuild to force-enable this behavior. Note that esbuild doesn't do this by default even though the default value of `--target=` otherwise behaves like `esnext`. Since TypeScript's compiler doesn't do this behavior by default, it seems like a good idea for esbuild to not do this behavior by default either. + +In addition to the breaking changes above, the following changes are also included in this release: + +* Allow certain keywords as tuple type labels in TypeScript ([#1797](https://github.com/evanw/esbuild/issues/1797)) + + Apparently TypeScript lets you use certain keywords as tuple labels but not others. For example, `type x = [function: number]` is allowed while `type x = [class: number]` isn't. This release replicates this behavior in esbuild's TypeScript parser: + + * Allowed keywords: `false`, `function`, `import`, `new`, `null`, `this`, `true`, `typeof`, `void` + + * Forbidden keywords: `break`, `case`, `catch`, `class`, `const`, `continue`, `debugger`, `default`, `delete`, `do`, `else`, `enum`, `export`, `extends`, `finally`, `for`, `if`, `in`, `instanceof`, `return`, `super`, `switch`, `throw`, `try`, `var`, `while`, `with` + +* Support sibling namespaces in TypeScript ([#1410](https://github.com/evanw/esbuild/issues/1410)) + + TypeScript has a feature where sibling namespaces with the same name can implicitly reference each other's exports without an explicit property access. This goes against how scope lookup works in JavaScript, so it previously didn't work with esbuild. This release adds support for this feature: + + ```ts + // Original TypeScript code + namespace x { + export let y = 123 + } + namespace x { + export let z = y + } + + // Old JavaScript output + var x; + (function(x2) { + x2.y = 123; + })(x || (x = {})); + (function(x2) { + x2.z = y; + })(x || (x = {})); + + // New JavaScript output + var x; + (function(x2) { + x2.y = 123; + })(x || (x = {})); + (function(x2) { + x2.z = x2.y; + })(x || (x = {})); + ``` + + Notice how the identifier `y` is now compiled to the property access `x2.y` which references the export named `y` on the namespace, instead of being left as the identifier `y` which references the global named `y`. This matches how the TypeScript compiler treats namespace objects. This new behavior also works for enums: + + ```ts + // Original TypeScript code + enum x { + y = 123 + } + enum x { + z = y + 1 + } + + // Old JavaScript output + var x; + (function(x2) { + x2[x2["y"] = 123] = "y"; + })(x || (x = {})); + (function(x2) { + x2[x2["z"] = y + 1] = "z"; + })(x || (x = {})); + + // New JavaScript output + var x; + (function(x2) { + x2[x2["y"] = 123] = "y"; + })(x || (x = {})); + (function(x2) { + x2[x2["z"] = 124] = "z"; + })(x || (x = {})); + ``` + + Note that this behavior does **not** work across files. Each file is still compiled independently so the namespaces in each file are still resolved independently per-file. Implicit namespace cross-references still do not work across files. Getting this to work is counter to esbuild's parallel architecture and does not fit in with esbuild's design. It also doesn't make sense with esbuild's bundling model where input files are either in ESM or CommonJS format and therefore each have their own scope. + +* Change output for top-level TypeScript enums + + The output format for top-level TypeScript enums has been changed to reduce code size and improve tree shaking, which means that esbuild's enum output is now somewhat different than TypeScript's enum output. The behavior of both output formats should still be equivalent though. Here's an example that shows the difference: + + ```ts + // Original code + enum x { + y = 1, + z = 2 + } + + // Old output + var x; + (function(x2) { + x2[x2["y"] = 1] = "y"; + x2[x2["z"] = 2] = "z"; + })(x || (x = {})); + + // New output + var x = /* @__PURE__ */ ((x2) => { + x2[x2["y"] = 1] = "y"; + x2[x2["z"] = 2] = "z"; + return x2; + })(x || {}); + ``` + + The function expression has been changed to an arrow expression to reduce code size and the enum initializer has been moved into the variable declaration to make it possible to be marked as `/* @__PURE__ */` to improve tree shaking. The `/* @__PURE__ */` annotation is now automatically added when all of the enum values are side-effect free, which means the entire enum definition can be removed as dead code if it's never referenced. Direct enum value references within the same file that have been inlined do not count as references to the enum definition so this should eliminate enums from the output in many cases: + + ```ts + // Original code + enum Foo { FOO = 1 } + enum Bar { BAR = 2 } + console.log(Foo, Bar.BAR) + + // Old output (with --bundle --minify) + var n;(function(e){e[e.FOO=1]="FOO"})(n||(n={}));var l;(function(e){e[e.BAR=2]="BAR"})(l||(l={}));console.log(n,2); + + // New output (with --bundle --minify) + var n=(e=>(e[e.FOO=1]="FOO",e))(n||{});console.log(n,2); + ``` + + Notice how the new output is much shorter because the entire definition for `Bar` has been completely removed as dead code by esbuild's tree shaking. + + The output may seem strange since it would be simpler to just have a plain object literal as an initializer. However, TypeScript's enum feature behaves similarly to TypeScript's namespace feature which means enums can merge with existing enums and/or existing namespaces (and in some cases also existing objects) if the existing definition has the same name. This new output format keeps its similarity to the original output format so that it still handles all of the various edge cases that TypeScript's enum feature supports. Initializing the enum using a plain object literal would not merge with existing definitions and would break TypeScript's enum semantics. + +* Fix legal comment parsing in CSS ([#1796](https://github.com/evanw/esbuild/issues/1796)) + + Legal comments in CSS either start with `/*!` or contain `@preserve` or `@license` and are preserved by esbuild in the generated CSS output. This release fixes a bug where non-top-level legal comments inside a CSS file caused esbuild to skip any following legal comments even if those following comments are top-level: + + ```css + /* Original code */ + .example { + --some-var: var(--tw-empty, /*!*/ /*!*/); + } + /*! Some legal comment */ + body { + background-color: red; + } + + /* Old output (with --minify) */ + .example{--some-var: var(--tw-empty, )}body{background-color:red} + + /* New output (with --minify) */ + .example{--some-var: var(--tw-empty, )}/*! Some legal comment */body{background-color:red} + ``` + +* Fix panic when printing invalid CSS ([#1803](https://github.com/evanw/esbuild/issues/1803)) + + This release fixes a panic caused by a conditional CSS `@import` rule with a URL token. Code like this caused esbuild to enter an unexpected state because the case where tokens in the import condition with associated import records wasn't handled. This case is now handled correctly: + + ```css + @import "example.css" url(foo); + ``` + +* Mark `Set` and `Map` with array arguments as pure ([#1791](https://github.com/evanw/esbuild/issues/1791)) + + This release introduces special behavior for references to the global `Set` and `Map` constructors that marks them as `/* @__PURE__ */` if they are known to not have any side effects. These constructors evaluate the iterator of whatever is passed to them and the iterator could have side effects, so this is only safe if whatever is passed to them is an array, since the array iterator has no side effects. + + Marking a constructor call as `/* @__PURE__ */` means it's safe to remove if the result is unused. This is an existing feature that you can trigger by manually adding a `/* @__PURE__ */` comment before a constructor call. The difference is that this release contains special behavior to automatically mark `Set` and `Map` as pure for you as long as it's safe to do so. As with all constructor calls that are marked `/* @__PURE__ */`, any internal expressions which could cause side effects are still preserved even though the constructor call itself is removed: + + ```js + // Original code + new Map([ + ['a', b()], + [c(), new Set(['d', e()])], + ]); + + // Old output (with --minify) + new Map([["a",b()],[c(),new Set(["d",e()])]]); + + // New output (with --minify) + b(),c(),e(); + ``` + +## 0.13.15 + +* Fix `super` in lowered `async` arrow functions ([#1777](https://github.com/evanw/esbuild/issues/1777)) + + This release fixes an edge case that was missed when lowering `async` arrow functions containing `super` property accesses for compile targets that don't support `async` such as with `--target=es6`. The problem was that lowering transforms `async` arrow functions into generator function expressions that are then passed to an esbuild helper function called `__async` that implements the `async` state machine behavior. Since function expressions do not capture `this` and `super` like arrow functions do, this led to a mismatch in behavior which meant that the transform was incorrect. The fix is to introduce a helper function to forward `super` access into the generator function expression body. Here's an example: + + ```js + // Original code + class Foo extends Bar { + foo() { return async () => super.bar() } + } + + // Old output (with --target=es6) + class Foo extends Bar { + foo() { + return () => __async(this, null, function* () { + return super.bar(); + }); + } + } + + // New output (with --target=es6) + class Foo extends Bar { + foo() { + return () => { + var __superGet = (key) => super[key]; + return __async(this, null, function* () { + return __superGet("bar").call(this); + }); + }; + } + } + ``` + +* Avoid merging certain CSS rules with different units ([#1732](https://github.com/evanw/esbuild/issues/1732)) + + This release no longer collapses `border-radius`, `margin`, `padding`, and `inset` rules when they have units with different levels of browser support. Collapsing multiple of these rules into a single rule is not equivalent if the browser supports one unit but not the other unit, since one rule would still have applied before the collapse but no longer applies after the collapse due to the whole rule being ignored. For example, Chrome 10 supports the `rem` unit but not the `vw` unit, so the CSS code below should render with rounded corners in Chrome 10. However, esbuild previously merged everything into a single rule which would cause Chrome 10 to ignore the rule and not round the corners. This issue is now fixed: + + ```css + /* Original CSS */ + div { + border-radius: 1rem; + border-top-left-radius: 1vw; + margin: 0; + margin-top: 1Q; + left: 10Q; + top: 20Q; + right: 10Q; + bottom: 20Q; + } + + /* Old output (with --minify) */ + div{border-radius:1vw 1rem 1rem;margin:1Q 0 0;inset:20Q 10Q} + + /* New output (with --minify) */ + div{border-radius:1rem;border-top-left-radius:1vw;margin:0;margin-top:1Q;inset:20Q 10Q} + ``` + + Notice how esbuild can still collapse rules together when they all share the same unit, even if the unit is one that doesn't have universal browser support such as the unit `Q`. One subtlety is that esbuild now distinguishes between "safe" and "unsafe" units where safe units are old enough that they are guaranteed to work in any browser a user might reasonably use, such as `px`. Safe units are allowed to be collapsed together even if there are multiple different units while multiple different unsafe units are not allowed to be collapsed together. Another detail is that esbuild no longer minifies zero lengths by removing the unit if the unit is unsafe (e.g. `0rem` into `0`) since that could cause a rendering difference if a previously-ignored rule is now no longer ignored due to the unit change. If you are curious, you can learn more about browser support levels for different CSS units in [Mozilla's documentation about CSS length units](https://developer.mozilla.org/en-US/docs/Web/CSS/length). + +* Avoid warning about ignored side-effect free imports for empty files ([#1785](https://github.com/evanw/esbuild/issues/1785)) + + When bundling, esbuild warns about bare imports such as `import "lodash-es"` when the package has been marked as `"sideEffects": false` in its `package.json` file. This is because the only reason to use a bare import is because you are relying on the side effects of the import, but imports for packages marked as side-effect free are supposed to be removed. If the package indicates that it has no side effects, then this bare import is likely a bug. + + However, some people have packages just for TypeScript type definitions. These package can actually have a side effect as they can augment the type of the global object in TypeScript, even if they are marked with `"sideEffects": false`. To avoid warning in this case, esbuild will now only issue this warning if the imported file is non-empty. If the file is empty, then it's irrelevant whether you import it or not so any import of that file does not indicate a bug. This fixes this case because `.d.ts` files typically end up being empty after esbuild parses them since they typically only contain type declarations. + +* Attempt to fix packages broken due to the `node:` prefix ([#1760](https://github.com/evanw/esbuild/issues/1760)) + + Some people have started using the node-specific `node:` path prefix in their packages. This prefix forces the following path to be interpreted as a node built-in module instead of a package on the file system. So `require("node:path")` will always import [node's `path` module](https://nodejs.org/api/path.html) and never import [npm's `path` package](https://www.npmjs.com/package/path). + + Adding the `node:` prefix breaks that code with older node versions that don't understand the `node:` prefix. This is a problem with the package, not with esbuild. The package should be adding a fallback if the `node:` prefix isn't available. However, people still want to be able to use these packages with older node versions even though the code is broken. Now esbuild will automatically strip this prefix if it detects that the code will break in the configured target environment (as specified by `--target=`). Note that this only happens during bundling, since import paths are only examined during bundling. + +## 0.13.14 + +* Fix dynamic `import()` on node 12.20+ ([#1772](https://github.com/evanw/esbuild/issues/1772)) + + When you use flags such as `--target=node12.20`, esbuild uses that version number to see what features the target environment supports. This consults an internal table that stores which target environments are supported for each feature. For example, `import(x)` is changed into `Promise.resolve().then(() => require(x))` if dynamic `import` expressions are unsupported. + + Previously esbuild's internal table only stored one version number, since features are rarely ever removed in newer versions of software. Either the target environment is before that version and the feature is unsupported, or the target environment is after that version and the feature is supported. This approach has work for all relevant features in all cases except for one: dynamic `import` support in node. This feature is supported in node 12.20.0 up to but not including node 13.0.0, and then is also supported in node 13.2.0 up. The feature table implementation has been changed to store an array of potentially discontiguous version ranges instead of one version number. + + Up until now, esbuild used 13.2.0 as the lowest supported version number to avoid generating dynamic `import` expressions when targeting node versions that don't support it. But with this release, esbuild will now use the more accurate discontiguous version range in this case. This means dynamic `import` expressions can now be generated when targeting versions of node 12.20.0 up to but not including node 13.0.0. + +* Avoid merging certain qualified rules in CSS ([#1776](https://github.com/evanw/esbuild/issues/1776)) + + A change was introduced in the previous release to merge adjacent CSS rules that have the same content: + + ```css + /* Original code */ + a { color: red } + b { color: red } + + /* Minified output */ + a,b{color:red} + ``` + + However, that introduced a regression in cases where the browser considers one selector to be valid and the other selector to be invalid, such as in the following example: + + ```css + /* This rule is valid, and is applied */ + a { color: red } + + /* This rule is invalid, and is ignored */ + b:-x-invalid { color: red } + ``` + + Merging these two rules into one causes the browser to consider the entire merged rule to be invalid, which disables both rules. This is a change in behavior from the original code. + + With this release, esbuild will now only merge adjacent duplicate rules together if they are known to work in all browsers (specifically, if they are known to work in IE 7 and up). Adjacent duplicate rules will no longer be merged in all other cases including modern pseudo-class selectors such as `:focus`, HTML5 elements such as `video`, and combinators such as `a + b`. + +* Minify syntax in the CSS `font`, `font-family`, and `font-weight` properties ([#1756](https://github.com/evanw/esbuild/pull/1756)) + + This release includes size reductions for CSS font syntax when minification is enabled: + + ```css + /* Original code */ + div { + font: bold 1rem / 1.2 "Segoe UI", sans-serif, "Segoe UI Emoji"; + } + + /* Output with "--minify" */ + div{font:700 1rem/1.2 Segoe UI,sans-serif,"Segoe UI Emoji"} + ``` + + Notice how `bold` has been changed to `700` and the quotes were removed around `"Segoe UI"` since it was safe to do so. + + This feature was contributed by [@sapphi-red](https://github.com/sapphi-red). + +## 0.13.13 + +* Add more information about skipping `"main"` in `package.json` ([#1754](https://github.com/evanw/esbuild/issues/1754)) + + Configuring `mainFields: []` breaks most npm packages since it tells esbuild to ignore the `"main"` field in `package.json`, which most npm packages use to specify their entry point. This is not a bug with esbuild because esbuild is just doing what it was told to do. However, people may do this without understanding how npm packages work, and then be confused about why it doesn't work. This release now includes additional information in the error message: + + ``` + > foo.js:1:27: error: Could not resolve "events" (use "--platform=node" when building for node) + 1 │ var EventEmitter = require('events') + ╵ ~~~~~~~~ + node_modules/events/package.json:20:2: note: The "main" field was ignored because the list of main fields to use is currently set to [] + 20 │ "main": "./events.js", + ╵ ~~~~~~ + ``` + +* Fix a tree-shaking bug with `var exports` ([#1739](https://github.com/evanw/esbuild/issues/1739)) + + This release fixes a bug where a variable named `var exports = {}` was incorrectly removed by tree-shaking (i.e. dead code elimination). The `exports` variable is a special variable in CommonJS modules that is automatically provided by the CommonJS runtime. CommonJS modules are transformed into something like this before being run: + + ```js + function(exports, module, require) { + var exports = {} + } + ``` + + So using `var exports = {}` should have the same effect as `exports = {}` because the variable `exports` should already be defined. However, esbuild was incorrectly overwriting the definition of the `exports` variable with the one provided by CommonJS. This release merges the definitions together so both are included, which fixes the bug. + +* Merge adjacent CSS selector rules with duplicate content ([#1755](https://github.com/evanw/esbuild/issues/1755)) + + With this release, esbuild will now merge adjacent selectors when minifying if they have the same content: + + ```css + /* Original code */ + a { color: red } + b { color: red } + + /* Old output (with --minify) */ + a{color:red}b{color:red} + + /* New output (with --minify) */ + a,b{color:red} + ``` + +* Shorten `top`, `right`, `bottom`, `left` CSS property into `inset` when it is supported ([#1758](https://github.com/evanw/esbuild/pull/1758)) + + This release enables collapsing of `inset` related properties: + + ```css + /* Original code */ + div { + top: 0; + right: 0; + bottom: 0; + left: 0; + } + + /* Output with "--minify-syntax" */ + div { + inset: 0; + } + ``` + + This minification rule is only enabled when `inset` property is supported by the target environment. Make sure to set esbuild's `target` setting correctly when minifying if the code will be running in an older environment (e.g. earlier than Chrome 87). + + This feature was contributed by [@sapphi-red](https://github.com/sapphi-red). + +## 0.13.12 + +* Implement initial support for simplifying `calc()` expressions in CSS ([#1607](https://github.com/evanw/esbuild/issues/1607)) + + This release includes basic simplification of `calc()` expressions in CSS when minification is enabled. The approach mainly follows the official CSS specification, which means it should behave the way browsers behave: https://www.w3.org/TR/css-values-4/#calc-func. This is a basic implementation so there are probably some `calc()` expressions that can be reduced by other tools but not by esbuild. This release mainly focuses on setting up the parsing infrastructure for `calc()` expressions to make it straightforward to implement additional simplifications in the future. Here's an example of this new functionality: + + ```css + /* Input CSS */ + div { + width: calc(60px * 4 - 5px * 2); + height: calc(100% / 4); + } + + /* Output CSS (with --minify-syntax) */ + div { + width: 230px; + height: 25%; + } + ``` + + Expressions that can't be fully simplified will still be partially simplified into a reduced `calc()` expression: + + ```css + /* Input CSS */ + div { + width: calc(100% / 5 - 2 * 1em - 2 * 1px); + } + + /* Output CSS (with --minify-syntax) */ + div { + width: calc(20% - 2em - 2px); + } + ``` + + Note that this transformation doesn't attempt to modify any expression containing a `var()` CSS variable reference. These variable references can contain any number of tokens so it's not safe to move forward with a simplification assuming that `var()` is a single token. For example, `calc(2px * var(--x) * 3)` is not transformed into `calc(6px * var(--x))` in case `var(--x)` contains something like `4 + 5px` (`calc(2px * 4 + 5px * 3)` evaluates to `23px` while `calc(6px * 4 + 5px)` evaluates to `29px`). + +* Fix a crash with a legal comment followed by an import ([#1730](https://github.com/evanw/esbuild/issues/1730)) + + Version 0.13.10 introduced parsing for CSS legal comments but caused a regression in the code that checks whether there are any rules that come before `@import`. This is not desired because browsers ignore `@import` rules after other non-`@import` rules, so esbuild warns you when you do this. However, legal comments are modeled as rules in esbuild's internal AST even though they aren't actual CSS rules, and the code that performs this check wasn't updated. This release fixes the crash. + +## 0.13.11 + +* Implement class static blocks ([#1558](https://github.com/evanw/esbuild/issues/1558)) + + This release adds support for a new upcoming JavaScript feature called [class static blocks](https://github.com/tc39/proposal-class-static-block) that lets you evaluate code inside of a class body. It looks like this: + + ```js + class Foo { + static { + this.foo = 123 + } + } + ``` + + This can be useful when you want to use `try`/`catch` or access private `#name` fields during class initialization. Doing that without this feature is quite hacky and basically involves creating temporary static fields containing immediately-invoked functions and then deleting the fields after class initialization. Static blocks are much more ergonomic and avoid performance loss due to `delete` changing the object shape. + + Static blocks are transformed for older browsers by moving the static block outside of the class body and into an immediately invoked arrow function after the class definition: + + ```js + // The transformed version of the example code above + const _Foo = class { + }; + let Foo = _Foo; + (() => { + _Foo.foo = 123; + })(); + ``` + + In case you're wondering, the additional `let` variable is to guard against the potential reassignment of `Foo` during evaluation such as what happens below. The value of `this` must be bound to the original class, not to the current value of `Foo`: + + ```js + let bar + class Foo { + static { + bar = () => this + } + } + Foo = null + console.log(bar()) // This should not be "null" + ``` + +* Fix issues with `super` property accesses + + Code containing `super` property accesses may need to be transformed even when they are supported. For example, in ES6 `async` methods are unsupported while `super` properties are supported. An `async` method containing `super` property accesses requires those uses of `super` to be transformed (the `async` function is transformed into a nested generator function and the `super` keyword cannot be used inside nested functions). + + Previously esbuild transformed `super` property accesses into a function call that returned the corresponding property. However, this was incorrect for uses of `super` that write to the inherited setter since a function call is not a valid assignment target. This release fixes writing to a `super` property: + + ```js + // Original code + class Base { + set foo(x) { console.log('set foo to', x) } + } + class Derived extends Base { + async bar() { super.foo = 123 } + } + new Derived().bar() + + // Old output with --target=es6 (contains a syntax error) + class Base { + set foo(x) { + console.log("set foo to", x); + } + } + class Derived extends Base { + bar() { + var __super = (key) => super[key]; + return __async(this, null, function* () { + __super("foo") = 123; + }); + } + } + new Derived().bar(); + + // New output with --target=es6 (works correctly) + class Base { + set foo(x) { + console.log("set foo to", x); + } + } + class Derived extends Base { + bar() { + var __superSet = (key, value) => super[key] = value; + return __async(this, null, function* () { + __superSet("foo", 123); + }); + } + } + new Derived().bar(); + ``` + + All known edge cases for assignment to a `super` property should now be covered including destructuring assignment and using the unary assignment operators with BigInts. + + In addition, this release also fixes a bug where a `static` class field containing a `super` property access was not transformed when it was moved outside of the class body, which can happen when `static` class fields aren't supported. + + ```js + // Original code + class Base { + static get foo() { + return 123 + } + } + class Derived extends Base { + static bar = super.foo + } + + // Old output with --target=es6 (contains a syntax error) + class Base { + static get foo() { + return 123; + } + } + class Derived extends Base { + } + __publicField(Derived, "bar", super.foo); + + // New output with --target=es6 (works correctly) + class Base { + static get foo() { + return 123; + } + } + const _Derived = class extends Base { + }; + let Derived = _Derived; + __publicField(Derived, "bar", __superStaticGet(_Derived, "foo")); + ``` + + All known edge cases for `super` inside `static` class fields should be handled including accessing `super` after prototype reassignment of the enclosing class object. + +## 0.13.10 + +* Implement legal comment preservation for CSS ([#1539](https://github.com/evanw/esbuild/issues/1539)) + + This release adds support for legal comments in CSS the same way they are already supported for JS. A legal comment is one that starts with `/*!` or that contains the text `@license` or `@preserve`. These comments are preserved in output files by esbuild since that follows the intent of the original authors of the code. The specific behavior is controlled via `--legal-comments=` in the CLI and `legalComments` in the JS API, which can be set to any of the following options: + + * `none`: Do not preserve any legal comments + * `inline`: Preserve all rule-level legal comments + * `eof`: Move all rule-level legal comments to the end of the file + * `linked`: Move all rule-level legal comments to a `.LEGAL.txt` file and link to them with a comment + * `external`: Move all rule-level legal comments to a `.LEGAL.txt` file but to not link to them + + The default behavior is `eof` when bundling and `inline` otherwise. + +* Allow uppercase `es*` targets ([#1717](https://github.com/evanw/esbuild/issues/1717)) + + With this release, you can now use target names such as `ESNext` instead of `esnext` as the target name in the CLI and JS API. This is important because people don't want to have to call `.toLowerCase()` on target strings from TypeScript's `tsconfig.json` file before passing it to esbuild (TypeScript uses case-agnostic target names). + + This feature was contributed by [@timse](https://github.com/timse). + +* Update to Unicode 14.0.0 + + The character tables that determine which characters form valid JavaScript identifiers have been updated from Unicode version 13.0.0 to the newly-released Unicode version 14.0.0. I'm not putting an example in the release notes because all of the new characters will likely just show up as little squares since fonts haven't been updated yet. But you can read https://www.unicode.org/versions/Unicode14.0.0/#Summary for more information about the changes. + +## 0.13.9 + +* Add support for `imports` in `package.json` ([#1691](https://github.com/evanw/esbuild/issues/1691)) + + This release adds basic support for the `imports` field in `package.json`. It behaves similarly to the `exports` field but only applies to import paths that start with `#`. The `imports` field provides a way for a package to remap its own internal imports for itself, while the `exports` field provides a way for a package to remap its external exports for other packages. This is useful because the `imports` field respects the currently-configured conditions which means that the import mapping can change at run-time. For example: + + ``` + $ cat entry.mjs + import '#example' + + $ cat package.json + { + "imports": { + "#example": { + "foo": "./example.foo.mjs", + "default": "./example.mjs" + } + } + } + + $ cat example.foo.mjs + console.log('foo is enabled') + + $ cat example.mjs + console.log('foo is disabled') + + $ node entry.mjs + foo is disabled + + $ node --conditions=foo entry.mjs + foo is enabled + ``` + + Now that esbuild supports this feature too, import paths starting with `#` and any provided conditions will be respected when bundling: + + ``` + $ esbuild --bundle entry.mjs | node + foo is disabled + + $ esbuild --conditions=foo --bundle entry.mjs | node + foo is enabled + ``` + +* Fix using `npm rebuild` with the `esbuild` package ([#1703](https://github.com/evanw/esbuild/issues/1703)) + + Version 0.13.4 accidentally introduced a regression in the install script where running `npm rebuild` multiple times could fail after the second time. The install script creates a copy of the binary executable using [`link`](https://man7.org/linux/man-pages/man2/link.2.html) followed by [`rename`](https://www.man7.org/linux/man-pages/man2/rename.2.html). Using `link` creates a hard link which saves space on the file system, and `rename` is used for safety since it atomically replaces the destination. + + However, the `rename` syscall has an edge case where it silently fails if the source and destination are both the same link. This meant that the install script would fail after being run twice in a row. With this release, the install script now deletes the source after calling `rename` in case it has silently failed, so this issue should now be fixed. It should now be safe to use `npm rebuild` with the `esbuild` package. + +* Fix invalid CSS minification of `border-radius` ([#1702](https://github.com/evanw/esbuild/issues/1702)) + + CSS minification does collapsing of `border-radius` related properties. For example: + + ```css + /* Original CSS */ + div { + border-radius: 1px; + border-top-left-radius: 5px; + } + + /* Minified CSS */ + div{border-radius:5px 1px 1px} + ``` + + However, this only works for numeric tokens, not identifiers. For example: + + ```css + /* Original CSS */ + div { + border-radius: 1px; + border-top-left-radius: inherit; + } + + /* Minified CSS */ + div{border-radius:1px;border-top-left-radius:inherit} + ``` + + Transforming this to `div{border-radius:inherit 1px 1px}`, as was done in previous releases of esbuild, is an invalid transformation and results in incorrect CSS. This release of esbuild fixes this CSS transformation bug. + +## 0.13.8 + +* Fix `super` inside arrow function inside lowered `async` function ([#1425](https://github.com/evanw/esbuild/issues/1425)) + + When an `async` function is transformed into a regular function for target environments that don't support `async` such as `--target=es6`, references to `super` inside that function must be transformed too since the `async`-to-regular function transformation moves the function body into a nested function, so the `super` references are no longer syntactically valid. However, this transform didn't handle an edge case and `super` references inside of an arrow function were overlooked. This release fixes this bug: + + ```js + // Original code + class Foo extends Bar { + async foo() { + return () => super.foo() + } + } + + // Old output (with --target=es6) + class Foo extends Bar { + foo() { + return __async(this, null, function* () { + return () => super.foo(); + }); + } + } + + // New output (with --target=es6) + class Foo extends Bar { + foo() { + var __super = (key) => super[key]; + return __async(this, null, function* () { + return () => __super("foo").call(this); + }); + } + } + ``` + +* Remove the implicit `/` after `[dir]` in entry names ([#1661](https://github.com/evanw/esbuild/issues/1661)) + + The "entry names" feature lets you customize the way output file names are generated. The `[dir]` and `[name]` placeholders are filled in with the directory name and file name of the corresponding entry point file, respectively. + + Previously `--entry-names=[dir]/[name]` and `--entry-names=[dir][name]` behaved the same because the value used for `[dir]` always had an implicit trailing slash, since it represents a directory. However, some people want to be able to remove the file name with `--entry-names=[dir]` and the implicit trailing slash gets in the way. + + With this release, you can now use the `[dir]` placeholder without an implicit trailing slash getting in the way. For example, the command `esbuild foo/bar/index.js --outbase=. --outdir=out --entry-names=[dir]` previously generated the file `out/foo/bar/.js` but will now generate the file `out/foo/bar.js`. + +## 0.13.7 + +* Minify CSS alpha values correctly ([#1682](https://github.com/evanw/esbuild/issues/1682)) + + When esbuild uses the `rgba()` syntax for a color instead of the 8-character hex code (e.g. when `target` is set to Chrome 61 or earlier), the 0-to-255 integer alpha value must be printed as a floating-point fraction between 0 and 1. The fraction was only printed to three decimal places since that is the minimal number of decimal places required for all 256 different alpha values to be uniquely determined. However, using three decimal places does not necessarily result in the shortest result. For example, `128 / 255` is `0.5019607843137255` which is printed as `".502"` using three decimal places, but `".5"` is equivalent because `round(0.5 * 255) == 128`, so printing `".5"` would be better. With this release, esbuild will always use the minimal numeric representation for the alpha value: + + ```css + /* Original code */ + a { color: #FF800080 } + + /* Old output (with --minify --target=chrome61) */ + a{color:rgba(255,128,0,.502)} + + /* New output (with --minify --target=chrome61) */ + a{color:rgba(255,128,0,.5)} + ``` + +* Match node's behavior for core module detection ([#1680](https://github.com/evanw/esbuild/issues/1680)) + + Node has a hard-coded list of core modules (e.g. `fs`) that, when required, short-circuit the module resolution algorithm and instead return the corresponding internal core module object. When you pass `--platform=node` to esbuild, esbuild also implements this short-circuiting behavior and doesn't try to bundle these import paths. This was implemented in esbuild using the existing `external` feature (e.g. essentially `--external:fs`). However, there is an edge case where esbuild's `external` feature behaved differently than node. + + Modules specified via esbuild's `external` feature also cause all sub-paths to be excluded as well, so for example `--external:foo` excludes both `foo` and `foo/bar` from the bundle. However, node's core module check is only an exact equality check, so for example `fs` is a core module and bypasses the module resolution algorithm but `fs/foo` is not a core module and causes the module resolution algorithm to search the file system. + + This behavior can be used to load a module on the file system with the same name as one of node's core modules. For example, `require('fs/')` will load the module `fs` from the file system instead of loading node's core `fs` module. With this release, esbuild will now match node's behavior in this edge case. This means the external modules that are automatically added by `--platform=node` now behave subtly differently than `--external:`, which allows code that relies on this behavior to be bundled correctly. + +* Fix WebAssembly builds on Go 1.17.2+ ([#1684](https://github.com/evanw/esbuild/pull/1684)) + + Go 1.17.2 introduces a change (specifically a [fix for CVE-2021-38297](https://go-review.googlesource.com/c/go/+/354591/)) that causes Go's WebAssembly bootstrap script to throw an error when it's run in situations with many environment variables. One such situation is when the bootstrap script is run inside [GitHub Actions](https://github.com/features/actions). This change was introduced because the bootstrap script writes a copy of the environment variables into WebAssembly memory without any bounds checking, and writing more than 4096 bytes of data ends up writing past the end of the buffer and overwriting who-knows-what. So throwing an error in this situation is an improvement. However, this breaks esbuild which previously (at least seemingly) worked fine. + + With this release, esbuild's WebAssembly bootstrap script that calls out to Go's WebAssembly bootstrap script will now delete all environment variables except for the ones that esbuild checks for, of which there are currently only four: `NO_COLOR`, `NODE_PATH`, `npm_config_user_agent`, and `WT_SESSION`. This should avoid a crash when esbuild is built using Go 1.17.2+ and should reduce the likelihood of memory corruption when esbuild is built using Go 1.17.1 or earlier. This release also updates the Go version that esbuild ships with to version 1.17.2. Note that this problem only affects the `esbuild-wasm` package. The `esbuild` package is not affected. + + See also: + + * https://github.com/golang/go/issues/48797 + * https://github.com/golang/go/issues/49011 + +## 0.13.6 + +* Emit decorators for `declare` class fields ([#1675](https://github.com/evanw/esbuild/issues/1675)) + + In version 3.7, TypeScript introduced the `declare` keyword for class fields that avoids generating any code for that field: + + ```ts + // TypeScript input + class Foo { + a: number + declare b: number + } + + // JavaScript output + class Foo { + a; + } + ``` + + However, it turns out that TypeScript still emits decorators for these omitted fields. With this release, esbuild will now do this too: + + ```ts + // TypeScript input + class Foo { + @decorator a: number; + @decorator declare b: number; + } + + // Old JavaScript output + class Foo { + a; + } + __decorateClass([ + decorator + ], Foo.prototype, "a", 2); + + // New JavaScript output + class Foo { + a; + } + __decorateClass([ + decorator + ], Foo.prototype, "a", 2); + __decorateClass([ + decorator + ], Foo.prototype, "b", 2); + ``` + +* Experimental support for esbuild on NetBSD ([#1624](https://github.com/evanw/esbuild/pull/1624)) + + With this release, esbuild now has a published binary executable for [NetBSD](https://www.netbsd.org/) in the [`esbuild-netbsd-64`](https://www.npmjs.com/package/esbuild-netbsd-64) npm package, and esbuild's installer has been modified to attempt to use it when on NetBSD. Hopefully this makes installing esbuild via npm work on NetBSD. This change was contributed by [@gdt](https://github.com/gdt). + + ⚠️ Note: NetBSD is not one of [Node's supported platforms](https://nodejs.org/api/process.html#process_process_platform), so installing esbuild may or may not work on NetBSD depending on how Node has been patched. This is not a problem with esbuild. ⚠️ + +* Disable the "esbuild was bundled" warning if `ESBUILD_BINARY_PATH` is provided ([#1678](https://github.com/evanw/esbuild/pull/1678)) + + The `ESBUILD_BINARY_PATH` environment variable allows you to substitute an alternate binary executable for esbuild's JavaScript API. This is useful in certain cases such as when debugging esbuild. The JavaScript API has some code that throws an error if it detects that it was bundled before being run, since bundling prevents esbuild from being able to find the path to its binary executable. However, that error is unnecessary if `ESBUILD_BINARY_PATH` is present because an alternate path has been provided. This release disables the warning when `ESBUILD_BINARY_PATH` is present so that esbuild can be used when bundled as long as you also manually specify `ESBUILD_BINARY_PATH`. + + This change was contributed by [@heypiotr](https://github.com/heypiotr). + +* Remove unused `catch` bindings when minifying ([#1660](https://github.com/evanw/esbuild/pull/1660)) + + With this release, esbuild will now remove unused `catch` bindings when minifying: + + ```js + // Original code + try { + throw 0; + } catch (e) { + } + + // Old output (with --minify) + try{throw 0}catch(t){} + + // New output (with --minify) + try{throw 0}catch{} + ``` + + This takes advantage of the new [optional catch binding](https://github.com/tc39/proposal-optional-catch-binding) syntax feature that was introduced in ES2019. This minification rule is only enabled when optional catch bindings are supported by the target environment. Specifically, it's not enabled when using `--target=es2018` or older. Make sure to set esbuild's `target` setting correctly when minifying if the code will be running in an older JavaScript environment. + + This change was contributed by [@sapphi-red](https://github.com/sapphi-red). + +## 0.13.5 + +* Improve watch mode accuracy ([#1113](https://github.com/evanw/esbuild/issues/1113)) + + Watch mode is enabled by `--watch` and causes esbuild to become a long-running process that automatically rebuilds output files when input files are changed. It's implemented by recording all calls to esbuild's internal file system interface and then invalidating the build whenever these calls would return different values. For example, a call to esbuild's internal `ReadFile()` function is considered to be different if either the presence of the file has changed (e.g. the file didn't exist before but now exists) or the presence of the file stayed the same but the content of the file has changed. + + Previously esbuild's watch mode operated at the `ReadFile()` and `ReadDirectory()` level. When esbuild checked whether a directory entry existed or not (e.g. whether a directory contains a `node_modules` subdirectory or a `package.json` file), it called `ReadDirectory()` which then caused the build to depend on that directory's set of entries. This meant the build would be invalidated even if a new unrelated entry was added or removed, since that still changes the set of entries. This is problematic when using esbuild in environments that constantly create and destroy temporary directory entries in your project directory. In that case, esbuild's watch mode would constantly rebuild as the directory was constantly considered to be dirty. + + With this release, watch mode now operates at the `ReadFile()` and `ReadDirectory().Get()` level. So when esbuild checks whether a directory entry exists or not, the build should now only depend on the presence status for that one directory entry. This should avoid unnecessary rebuilds due to unrelated directory entries being added or removed. The log messages generated using `--watch` will now also mention the specific directory entry whose presence status was changed if a build is invalidated for this reason. + + Note that this optimization does not apply to plugins using the `watchDirs` return value because those paths are only specified at the directory level and do not describe individual directory entries. You can use `watchFiles` or `watchDirs` on the individual entries inside the directory to get a similar effect instead. + +* Disallow certain uses of `<` in `.mts` and `.cts` files + + The upcoming version 4.5 of TypeScript is introducing the `.mts` and `.cts` extensions that turn into the `.mjs` and `.cjs` extensions when compiled. However, unlike the existing `.ts` and `.tsx` extensions, expressions that start with `<` are disallowed when they would be ambiguous depending on whether they are parsed in `.ts` or `.tsx` mode. The ambiguity is caused by the overlap between the syntax for JSX elements and the old deprecated syntax for type casts: + + | Syntax | `.ts` | `.tsx` | `.mts`/`.cts` | + |-------------------------------|----------------------|------------------|----------------------| + | `y` | ✅ Type cast | 🚫 Syntax error | 🚫 Syntax error | + | `() => {}` | ✅ Arrow function | 🚫 Syntax error | 🚫 Syntax error | + | `y` | 🚫 Syntax error | ✅ JSX element | 🚫 Syntax error | + | `() => {}` | 🚫 Syntax error | ✅ JSX element | 🚫 Syntax error | + | `() => {}` | 🚫 Syntax error | ✅ JSX element | 🚫 Syntax error | + | `() => {}` | 🚫 Syntax error | ✅ JSX element | 🚫 Syntax error | + | `() => {}` | ✅ Arrow function | ✅ Arrow function | ✅ Arrow function | + | `() => {}` | ✅ Arrow function | ✅ Arrow function | ✅ Arrow function | + + This release of esbuild introduces a syntax error for these ambiguous syntax constructs in `.mts` and `.cts` files to match the new behavior of the TypeScript compiler. + +* Do not remove empty `@keyframes` rules ([#1665](https://github.com/evanw/esbuild/issues/1665)) + + CSS minification in esbuild automatically removes empty CSS rules, since they have no effect. However, empty `@keyframes` rules still trigger JavaScript animation events so it's incorrect to remove them. To demonstrate that empty `@keyframes` rules still have an effect, here is a bug report for Firefox where it was incorrectly not triggering JavaScript animation events for empty `@keyframes` rules: https://bugzilla.mozilla.org/show_bug.cgi?id=1004377. + + With this release, empty `@keyframes` rules are now preserved during minification: + + ```css + /* Original CSS */ + @keyframes foo { + from {} + to {} + } + + /* Old output (with --minify) */ + + /* New output (with --minify) */ + @keyframes foo{} + ``` + + This fix was contributed by [@eelco](https://github.com/eelco). + +* Fix an incorrect duplicate label error ([#1671](https://github.com/evanw/esbuild/pull/1671)) + + When labeling a statement in JavaScript, the label must be unique within the enclosing statements since the label determines the jump target of any labeled `break` or `continue` statement: + + ```js + // This code is valid + x: y: z: break x; + + // This code is invalid + x: y: x: break x; + ``` + + However, an enclosing label with the same name *is* allowed as long as it's located in a different function body. Since `break` and `continue` statements can't jump across function boundaries, the label is not ambiguous. This release fixes a bug where esbuild incorrectly treated this valid code as a syntax error: + + ```js + // This code is valid, but was incorrectly considered a syntax error + x: (() => { + x: break x; + })(); + ``` + + This fix was contributed by [@nevkontakte](https://github.com/nevkontakte). + +## 0.13.4 + +* Fix permission issues with the install script ([#1642](https://github.com/evanw/esbuild/issues/1642)) + + The `esbuild` package contains a small JavaScript stub file that implements the CLI (command-line interface). Its only purpose is to spawn the binary esbuild executable as a child process and forward the command-line arguments to it. + + The install script contains an optimization that replaces this small JavaScript stub with the actual binary executable at install time to avoid the overhead of unnecessarily creating a new `node` process. This optimization can't be done at package publish time because there is only one `esbuild` package but there are many supported platforms, so the binary executable for the current platform must live outside of the `esbuild` package. + + However, the optimization was implemented with an [unlink](https://www.man7.org/linux/man-pages/man2/unlink.2.html) operation followed by a [link](https://www.man7.org/linux/man-pages/man2/link.2.html) operation. This means that if the first step fails, the package is left in a broken state since the JavaScript stub file is deleted but not yet replaced. + + With this release, the optimization is now implemented with a [link](https://www.man7.org/linux/man-pages/man2/link.2.html) operation followed by a [rename](https://www.man7.org/linux/man-pages/man2/rename.2.html) operation. This should always leave the package in a working state even if either step fails. + +* Add a fallback for `npm install esbuild --no-optional` ([#1647](https://github.com/evanw/esbuild/issues/1647)) + + The installation method for esbuild's platform-specific binary executable was recently changed in version 0.13.0. Before that version esbuild downloaded it in an install script, and after that version esbuild lets the package manager download it using the `optionalDependencies` feature in `package.json`. This change was made because downloading the binary executable in an install script never really fully worked. The reasons are complex but basically there are a variety of edge cases where people people want to install esbuild in environments that they have customized such that downloading esbuild isn't possible. Using `optionalDependencies` instead lets the package manager deal with it instead, which should work fine in all cases (either that or your package manager has a bug, but that's not esbuild's problem). + + There is one case where this new installation method doesn't work: if you pass the `--no-optional` flag to npm to disable the `optionalDependencies` feature. If you do this, you prevent esbuild from being installed. This is not a problem with esbuild because you are manually enabling a flag to change npm's behavior such that esbuild doesn't install correctly. However, people still want to do this. + + With this release, esbuild will now fall back to the old installation method if the new installation method fails. **THIS MAY NOT WORK.** The new `optionalDependencies` installation method is the only supported way to install esbuild with npm. The old downloading installation method was removed because it doesn't always work. The downloading method is only being provided to try to be helpful but it's not the supported installation method. If you pass `--no-optional` and the download fails due to some environment customization you did, the recommended fix is to just remove the `--no-optional` flag. + +* Support the new `.mts` and `.cts` TypeScript file extensions + + The upcoming version 4.5 of TypeScript has two new file extensions: `.mts` and `.cts`. Files with these extensions can be imported using the `.mjs` and `.cjs`, respectively. So the statement `import "./foo.mjs"` in TypeScript can actually succeed even if the file `./foo.mjs` doesn't exist on the file system as long as the file `./foo.mts` does exist. The import path with the `.mjs` extension is automatically re-routed to the corresponding file with the `.mts` extension at type-checking time by the TypeScript compiler. See [the TypeScript 4.5 beta announcement](https://devblogs.microsoft.com/typescript/announcing-typescript-4-5-beta/#new-file-extensions) for details. + + With this release, esbuild will also automatically rewrite `.mjs` to `.mts` and `.cjs` to `.cts` when resolving import paths to files on the file system. This should make it possible to bundle code written in this new style. In addition, the extensions `.mts` and `.cts` are now also considered valid TypeScript file extensions by default along with the `.ts` extension. + +* Fix invalid CSS minification of `margin` and `padding` ([#1657](https://github.com/evanw/esbuild/issues/1657)) + + CSS minification does collapsing of `margin` and `padding` related properties. For example: + + ```css + /* Original CSS */ + div { + margin: auto; + margin-top: 5px; + margin-left: 5px; + } + + /* Minified CSS */ + div{margin:5px auto auto 5px} + ``` + + However, while this works for the `auto` keyword, it doesn't work for other keywords. For example: + + ```css + /* Original CSS */ + div { + margin: inherit; + margin-top: 5px; + margin-left: 5px; + } + + /* Minified CSS */ + div{margin:inherit;margin-top:5px;margin-left:5px} + ``` + + Transforming this to `div{margin:5px inherit inherit 5px}`, as was done in previous releases of esbuild, is an invalid transformation and results in incorrect CSS. This release of esbuild fixes this CSS transformation bug. + +## 0.13.3 + +* Support TypeScript type-only import/export specifiers ([#1637](https://github.com/evanw/esbuild/pull/1637)) + + This release adds support for a new TypeScript syntax feature in the upcoming version 4.5 of TypeScript. This feature lets you prefix individual imports and exports with the `type` keyword to indicate that they are types instead of values. This helps tools such as esbuild omit them from your source code, and is necessary because esbuild compiles files one-at-a-time and doesn't know at parse time which imports/exports are types and which are values. The new syntax looks like this: + + ```ts + // Input TypeScript code + import { type Foo } from 'foo' + export { type Bar } + + // Output JavaScript code (requires "importsNotUsedAsValues": "preserve" in "tsconfig.json") + import {} from "foo"; + export {}; + ``` + + See [microsoft/TypeScript#45998](https://github.com/microsoft/TypeScript/pull/45998) for full details. From what I understand this is a purely ergonomic improvement since this was already previously possible using a type-only import/export statements like this: + + ```ts + // Input TypeScript code + import type { Foo } from 'foo' + export type { Bar } + import 'foo' + export {} + + // Output JavaScript code (requires "importsNotUsedAsValues": "preserve" in "tsconfig.json") + import "foo"; + export {}; + ``` + + This feature was contributed by [@g-plane](https://github.com/g-plane). + +## 0.13.2 + +* Fix `export {}` statements with `--tree-shaking=true` ([#1628](https://github.com/evanw/esbuild/issues/1628)) + + The new `--tree-shaking=true` option allows you to force-enable tree shaking in cases where it wasn't previously possible. One such case is when bundling is disabled and there is no output format configured, in which case esbuild just preserves the format of whatever format the input code is in. Enabling tree shaking in this context caused a bug where `export {}` statements were stripped. This release fixes the bug so `export {}` statements should now be preserved when you pass `--tree-shaking=true`. This bug only affected this new functionality and didn't affect existing scenarios. + +## 0.13.1 + +* Fix the `esbuild` package in yarn 2+ + + The [yarn package manager](https://yarnpkg.com/) version 2 and above has a mode called [PnP](https://next.yarnpkg.com/features/pnp/) that installs packages inside zip files instead of using individual files on disk, and then hijacks node's `fs` module to pretend that paths to files inside the zip file are actually individual files on disk so that code that wasn't written specifically for yarn still works. Unfortunately that hijacking is incomplete and it still causes certain things to break such as using these zip file paths to create a JavaScript worker thread or to create a child process. + + This was an issue for the new `optionalDependencies` package installation strategy that was just released in version 0.13.0 since the binary executable is now inside of an installed package instead of being downloaded using an install script. When it's installed with yarn 2+ in PnP mode the binary executable is inside a zip file and can't be run. To work around this, esbuild detects yarn's PnP mode and copies the binary executable to a real file outside of the zip file. + + Unfortunately the code to do this didn't create the parent directory before writing to the file path. That caused esbuild's API to crash when it was run for the first time. This didn't come up during testing because the parent directory already existed when the tests were run. This release changes the location of the binary executable from a shared cache directory to inside the esbuild package itself, which should fix this crash. This problem only affected esbuild's JS API when it was run through yarn 2+ with PnP mode active. + +## 0.13.0 + +**This release contains backwards-incompatible changes.** Since esbuild is before version 1.0.0, these changes have been released as a new minor version to reflect this (as [recommended by npm](https://docs.npmjs.com/cli/v6/using-npm/semver/)). You should either be pinning the exact version of `esbuild` in your `package.json` file or be using a version range syntax that only accepts patch upgrades such as `~0.12.0`. See the documentation about [semver](https://docs.npmjs.com/cli/v6/using-npm/semver/) for more information. + +* Allow tree shaking to be force-enabled and force-disabled ([#1518](https://github.com/evanw/esbuild/issues/1518), [#1610](https://github.com/evanw/esbuild/issues/1610), [#1611](https://github.com/evanw/esbuild/issues/1611), [#1617](https://github.com/evanw/esbuild/pull/1617)) + + This release introduces a breaking change that gives you more control over when tree shaking happens ("tree shaking" here refers to declaration-level dead code removal). Previously esbuild's tree shaking was automatically enabled or disabled for you depending on the situation and there was no manual override to change this. Specifically, tree shaking was only enabled either when bundling was enabled or when the output format was set to `iife` (i.e. wrapped in an immediately-invoked function expression). This was done to avoid issues with people appending code to output files in the `cjs` and `esm` formats and expecting that code to be able to reference code in the output file that isn't otherwise referenced. + + You now have the ability to explicitly force-enable or force-disable tree shaking to bypass this default behavior. This is a breaking change because there is already a setting for tree shaking that does something else, and it has been moved to a separate setting instead. The previous setting allowed you to control whether or not to ignore manual side-effect annotations, which is related to tree shaking since only side-effect free code can be removed as dead code. Specifically you can annotate function calls with `/* @__PURE__ */` to indicate that they can be removed if they are not used, and you can annotate packages with `"sideEffects": false` to indicate that imports of that package can be removed if they are not used. Being able to ignore these annotations is necessary because [they are sometimes incorrect](https://github.com/tensorflow/tfjs/issues/4248). This previous setting has been moved to a separate setting because it actually impacts dead-code removal within expressions, which also applies when minifying with tree-shaking disabled. + + ### Old behavior + + * CLI + * Ignore side-effect annotations: `--tree-shaking=ignore-annotations` + * JS + * Ignore side-effect annotations: `treeShaking: 'ignore-annotations'` + * Go + * Ignore side-effect annotations: `TreeShaking: api.TreeShakingIgnoreAnnotations` + + ### New behavior + + * CLI + * Ignore side-effect annotations: `--ignore-annotations` + * Force-disable tree shaking: `--tree-shaking=false` + * Force-enable tree shaking: `--tree-shaking=true` + * JS + * Ignore side-effect annotations: `ignoreAnnotations: true` + * Force-disable tree shaking: `treeShaking: false` + * Force-enable tree shaking: `treeShaking: true` + * Go + * Ignore side-effect annotations: `IgnoreAnnotations: true` + * Force-disable tree shaking: `TreeShaking: api.TreeShakingFalse` + * Force-enable tree shaking: `TreeShaking: api.TreeShakingTrue` + +* The npm package now uses `optionalDependencies` to install the platform-specific binary executable ([#286](https://github.com/evanw/esbuild/issues/286), [#291](https://github.com/evanw/esbuild/issues/291), [#319](https://github.com/evanw/esbuild/issues/319), [#347](https://github.com/evanw/esbuild/issues/347), [#369](https://github.com/evanw/esbuild/issues/369), [#547](https://github.com/evanw/esbuild/issues/547), [#565](https://github.com/evanw/esbuild/issues/565), [#789](https://github.com/evanw/esbuild/issues/789), [#921](https://github.com/evanw/esbuild/issues/921), [#1193](https://github.com/evanw/esbuild/issues/1193), [#1270](https://github.com/evanw/esbuild/issues/1270), [#1382](https://github.com/evanw/esbuild/issues/1382), [#1422](https://github.com/evanw/esbuild/issues/1422), [#1450](https://github.com/evanw/esbuild/issues/1450), [#1485](https://github.com/evanw/esbuild/issues/1485), [#1546](https://github.com/evanw/esbuild/issues/1546), [#1547](https://github.com/evanw/esbuild/pull/1547), [#1574](https://github.com/evanw/esbuild/issues/1574), [#1609](https://github.com/evanw/esbuild/issues/1609)) + + This release changes esbuild's installation strategy in an attempt to improve compatibility with edge cases such as custom registries, custom proxies, offline installations, read-only file systems, or when post-install scripts are disabled. It's being treated as a breaking change out of caution because it's a significant change to how esbuild works with JS package managers, and hasn't been widely tested yet. + + **The old installation strategy** manually downloaded the correct binary executable in a [post-install script](https://docs.npmjs.com/cli/v7/using-npm/scripts). The binary executable is hosted in a separate platform-specific npm package such as [`esbuild-darwin-64`](https://www.npmjs.com/package/esbuild-darwin-64). The install script first attempted to download the package via the `npm` command in case npm had custom network settings configured. If that didn't work, the install script attempted to download the package from https://registry.npmjs.org/ before giving up. This was problematic for many reasons including: + + * Not all of npm's settings can be forwarded due to npm bugs such as https://github.com/npm/cli/issues/2284, and npm has said these bugs will never be fixed. + * Some people have configured their network environments such that downloading from https://registry.npmjs.org/ will hang instead of either succeeding or failing. + * The installed package was broken if you used `npm --ignore-scripts` because then the post-install script wasn't run. Some people enable this option so that malicious packages must be run first before being able to do malicious stuff. + + **The new installation strategy** automatically downloads the correct binary executable using npm's `optionalDependencies` feature to depend on all esbuild packages for all platforms but only have the one for the current platform be installed. This is a built-in part of the package manager so my assumption is that it should work correctly in all of these edge cases that currently don't work. And if there's an issue with this, then the problem is with the package manager instead of with esbuild so this should hopefully reduce the maintenance burden on esbuild itself. Changing to this installation strategy has these drawbacks: + + * Old versions of certain package managers (specifically npm and yarn) print lots of useless log messages during the installation, at least one for each platform other than the current one. These messages are harmless and can be ignored. However, they are annoying. There is nothing I can do about this. If you have this problem, one solution is to upgrade your package manager to a newer version. + + * Installation will be significantly slower in old versions of npm, old versions of pnpm, and all versions of yarn. These package managers download all packages for all platforms even though they aren't needed and actually cannot be used. This problem has been fixed in npm and pnpm and the problem has been communicated to yarn: https://github.com/yarnpkg/berry/issues/3317. If you have this problem, one solution is to use a newer version of npm or pnpm as your package manager. + + * This installation strategy does not work if you use `npm --no-optional` since then the package with the binary executable is not installed. If you have this problem, the solution is to not pass the `--no-optional` flag when installing packages. + + * There is still a small post-install script but it's now optional in that the `esbuild` package should still function correctly if post-install scripts are disabled (such as with `npm --ignore-scripts`). This post-install script optimizes the installed package by replacing the `esbuild` JavaScript command shim with the actual binary executable at install time. This avoids the overhead of launching another `node` process when using the `esbuild` command. So keep in mind that installing with `--ignore-scripts` will result in a slower `esbuild` command. + + Despite the drawbacks of the new installation strategy, I believe this change is overall a good thing to move forward with. It should fix edge case scenarios where installing esbuild currently doesn't work at all, and this only comes at the expense of the install script working in a less-optimal way (but still working) if you are using an old version of npm. So I'm going to switch installation strategies and see how it goes. + + The platform-specific binary executables are still hosted on npm in the same way, so anyone who wrote code that downloads builds from npm using the instructions here should not have to change their code: https://esbuild.github.io/getting-started/#download-a-build. However, note that these platform-specific packages no longer specify the `bin` field in `package.json` so the `esbuild` command will no longer be automatically put on your path. The `bin` field had to be removed because of a collision with the `bin` field of the `esbuild` package (now that the `esbuild` package depends on all of these platform-specific packages as optional dependencies). + +In addition to the breaking changes above, the following features are also included in this release: + +* Treat `x` guarded by `typeof x !== 'undefined'` as side-effect free + + This is a small tree-shaking (i.e. dead code removal) improvement. Global identifier references are considered to potentially have side effects since they will throw a reference error if the global identifier isn't defined, and code with side effects cannot be removed as dead code. However, there's a somewhat-common case where the identifier reference is guarded by a `typeof` check to check that it's defined before accessing it. With this release, code that does this will now be considered to have no side effects which allows it to be tree-shaken: + + ```js + // Original code + var __foo = typeof foo !== 'undefined' && foo; + var __bar = typeof bar !== 'undefined' && bar; + console.log(__bar); + + // Old output (with --bundle, which enables tree-shaking) + var __foo = typeof foo !== 'undefined' && foo; + var __bar = typeof bar !== 'undefined' && bar; + console.log(__bar); + + // New output (with --bundle, which enables tree-shaking) + var __bar = typeof bar !== 'undefined' && bar; + console.log(__bar); + ``` + +## 0.12.29 + +* Fix compilation of abstract class fields in TypeScript ([#1623](https://github.com/evanw/esbuild/issues/1623)) + + This release fixes a bug where esbuild could incorrectly include a TypeScript abstract class field in the compiled JavaScript output. This is incorrect because the official TypeScript compiler never does this. Note that this only happened in scenarios where TypeScript's `useDefineForClassFields` setting was set to `true` (or equivalently where TypeScript's `target` setting was set to `ESNext`). Here is the difference: + + ```js + // Original code + abstract class Foo { + abstract foo: any; + } + + // Old output + class Foo { + foo; + } + + // New output + class Foo { + } + ``` + +* Proxy from the `__require` shim to `require` ([#1614](https://github.com/evanw/esbuild/issues/1614)) + + Some background: esbuild's bundler emulates a CommonJS environment. The bundling process replaces the literal syntax `require()` with the referenced module at compile-time. However, other uses of `require` such as `require(someFunction())` are not bundled since the value of `someFunction()` depends on code evaluation, and esbuild does not evaluate code at compile-time. So it's possible for some references to `require` to remain after bundling. + + This was causing problems for some CommonJS code that was run in the browser and that expected `typeof require === 'function'` to be true (see [#1202](https://github.com/evanw/esbuild/issues/1202)), since the browser does not provide a global called `require`. Thus esbuild introduced a shim `require` function called `__require` (shown below) and replaced all references to `require` in the bundled code with `__require`: + + ```js + var __require = x => { + if (typeof require !== 'undefined') return require(x); + throw new Error('Dynamic require of "' + x + '" is not supported'); + }; + ``` + + However, this broke code that referenced `require.resolve` inside the bundle, which could hypothetically actually work since you could assign your own implementation to `window.require.resolve` (see [#1579](https://github.com/evanw/esbuild/issues/1579)). So the implementation of `__require` was changed to this: + + ```js + var __require = typeof require !== 'undefined' ? require : x => { + throw new Error('Dynamic require of "' + x + '" is not supported'); + }; + ``` + + However, that broke code that assigned to `window.require` later on after the bundle was loaded ([#1614](https://github.com/evanw/esbuild/issues/1614)). So with this release, the code for `__require` now handles all of these edge cases: + + * `typeof require` is still `function` even if `window.require` is undefined + * `window.require` can be assigned to either before or after the bundle is loaded + * `require.resolve` and arbitrary other properties can still be accessed + * `require` will now forward any number of arguments, not just the first one + + Handling all of these edge cases is only possible with the [Proxy API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy). So the implementation of `__require` now looks like this: + + ```js + var __require = (x => + typeof require !== 'undefined' ? require : + typeof Proxy !== 'undefined' ? new Proxy(x, { + get: (a, b) => (typeof require !== 'undefined' ? require : a)[b] + }) : x + )(function(x) { + if (typeof require !== 'undefined') return require.apply(this, arguments); + throw new Error('Dynamic require of "' + x + '" is not supported'); + }); + ``` + +* Consider `typeof x` to have no side effects + + The `typeof` operator does not itself trigger any code evaluation so it can safely be removed if evaluating the operand does not cause any side effects. However, there is a special case of the `typeof` operator when the operand is an identifier expression. In that case no reference error is thrown if the referenced symbol does not exist (e.g. `typeof x` does not throw an error if there is no symbol named `x`). With this release, esbuild will now consider `typeof x` to have no side effects even if evaluating `x` would have side effects (i.e. would throw a reference error): + + ```js + // Original code + var unused = typeof React !== 'undefined'; + + // Old output + var unused = typeof React !== 'undefined'; + + // New output + ``` + + Note that there is actually an edge case where `typeof x` *can* throw an error: when `x` is being referenced inside of its TDZ, or temporal dead zone (i.e. before it's declared). This applies to `let`, `const`, and `class` symbols. However, esbuild doesn't currently handle TDZ rules so the possibility of errors thrown due to TDZ rules is not currently considered. This typically doesn't matter in real-world code so this hasn't been a priority to fix (and is actually tricky to fix with esbuild's current bundling approach). So esbuild may incorrectly remove a `typeof` expression that actually has side effects. However, esbuild already incorrectly did this in previous releases so its behavior regarding `typeof` and TDZ rules hasn't changed in this release. + +## 0.12.28 + +* Fix U+30FB and U+FF65 in identifier names in ES5 vs. ES6+ ([#1599](https://github.com/evanw/esbuild/issues/1599)) + + The ES6 specification caused two code points that were previously valid in identifier names in ES5 to no longer be valid in identifier names in ES6+. The two code points are: + + * `U+30FB` i.e. `KATAKANA MIDDLE DOT` i.e. `・` + * `U+FF65` i.e. `HALFWIDTH KATAKANA MIDDLE DOT` i.e. `・` + + This means that using ES6+ parsing rules will fail to parse some valid ES5 code, and generating valid ES5 code may fail to be parsed using ES6+ parsing rules. For example, esbuild would previously fail to parse `x.y・` even though it's valid ES5 code (since it's not valid ES6+ code) and esbuild could generate `{y・:x}` when minifying even though it's not valid ES6+ code (since it's valid ES5 code). This problem is the result of my incorrect assumption that ES6 is a superset of ES5. + + As of this release, esbuild will now parse a superset of ES5 and ES6+ and will now quote identifier names when possible if it's not considered to be a valid identifier name in either ES5 or ES6+. In other words, a union of ES5 and ES6 rules is used for parsing and the intersection of ES5 and ES6 rules is used for printing. + +* Fix `++` and `--` on class private fields when used with big integers ([#1600](https://github.com/evanw/esbuild/issues/1600)) + + Previously when esbuild lowered class private fields (e.g. `#foo`) to older JavaScript syntax, the transform of the `++` and `--` was not correct if the value is a big integer such as `123n`. The transform in esbuild is similar to Babel's transform which [has the same problem](https://github.com/babel/babel/issues/13756). Specifically, the code was transformed into code that either adds or subtracts the number `1` and `123n + 1` throws an exception in JavaScript. This problem has been fixed so this should now work fine starting with this release. + +## 0.12.27 + +* Update JavaScript syntax feature compatibility tables ([#1594](https://github.com/evanw/esbuild/issues/1594)) + + Most JavaScript syntax feature compatibility data is able to be obtained automatically via https://kangax.github.io/compat-table/. However, they are missing data for quite a few new JavaScript features (see ([kangax/compat-table#1034](https://github.com/kangax/compat-table/issues/1034))) so data on these new features has to be added manually. This release manually adds a few new entries: + + * Top-level await + + This feature lets you use `await` at the top level of a module, outside of an `async` function. Doing this holds up the entire module instantiation operation until the awaited expression is resolved or rejected. This release marks this feature as supported in Edge 89, Firefox 89, and Safari 15 (it was already marked as supported in Chrome 89 and Node 14.8). The data source for this is https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await. + + * Arbitrary module namespace identifier names + + This lets you use arbitrary strings as module namespace identifier names as long as they are valid UTF-16 strings. An example is `export { x as "🍕" }` which can then be imported as `import { "🍕" as y } from "./example.js"`. This release marks this feature as supported in Firefox 87 (it was already marked as supported in Chrome 90 and Node 16). The data source for this is https://bugzilla.mozilla.org/show_bug.cgi?id=1670044. + + I would also like to add data for Safari. They have recently added support for arbitrary module namespace identifier names (https://bugs.webkit.org/show_bug.cgi?id=217576) and `export * as` (https://bugs.webkit.org/show_bug.cgi?id=214379). However, I have no idea how to determine which Safari release these bugs correspond to so this compatibility data for Safari has been omitted. + +* Avoid unnecessary additional log messages after the server is stopped ([#1589](https://github.com/evanw/esbuild/issues/1589)) + + There is a development server built in to esbuild which is accessible via the `serve()` API call. This returns a promise that resolves to an object with a `stop()` method that immediately terminates the development server. Previously calling this could cause esbuild to print stray log messages since `stop()` could cause plugins to be unregistered while a build is still in progress. With this release, calling `stop()` no longer terminates the development server immediately. It now waits for any active builds to finish first so the builds are not interrupted and left in a confusing state. + +* Fix an accidental dependency on Go ≥1.17.0 ([#1585](https://github.com/evanw/esbuild/pull/1585)) + + The source code of this release no longer uses the `math.MaxInt` constant that was introduced in Go version 1.17.0. This constant was preventing esbuild from being compiled on Go version <1.17.0. This fix was contributed by [@davezuko](https://github.com/davezuko). + +## 0.12.26 + +* Add `--analyze` to print information about the bundle ([#1568](https://github.com/evanw/esbuild/issues/1568)) + + The `--metafile=` flag tells esbuild to write information about the bundle into the provided metadata file in JSON format. It contains information about the input files and which other files each one imports, as well as the output files and which input files they include. This information is sufficient to answer many questions such as: + + * Which files are in my bundle? + * What's are the biggest files in my bundle? + * Why is this file included in my bundle? + + Previously you had to either write your own code to answer these questions, or use another tool such as https://bundle-buddy.com/esbuild to visualize the data. Starting with this release you can now also use `--analyze` to enable esbuild's built-in visualizer. It looks like this: + + ``` + $ esbuild --bundle example.jsx --outfile=out.js --minify --analyze + + out.js 27.6kb + + ⚡ Done in 6ms + + out.js 27.6kb 100.0% + ├ node_modules/react-dom/cjs/react-dom-server.browser.production.min.js 19.2kb 69.8% + ├ node_modules/react/cjs/react.production.min.js 5.9kb 21.4% + ├ node_modules/object-assign/index.js 965b 3.4% + ├ example.jsx 137b 0.5% + ├ node_modules/react-dom/server.browser.js 50b 0.2% + └ node_modules/react/index.js 50b 0.2% + ``` + + This tells you what input files were bundled into each output file as well as the final minified size contribution of each input file as well as the percentage of the output file it takes up. You can also enable verbose analysis with `--analyze=verbose` to see why each input file was included (i.e. which files imported it from the entry point file): + + ``` + $ esbuild --bundle example.jsx --outfile=out.js --minify --analyze=verbose + + out.js 27.6kb + + ⚡ Done in 6ms + + out.js ─────────────────────────────────────────────────────────────────── 27.6kb ─ 100.0% + ├ node_modules/react-dom/cjs/react-dom-server.browser.production.min.js ─ 19.2kb ── 69.8% + │ └ node_modules/react-dom/server.browser.js + │ └ example.jsx + ├ node_modules/react/cjs/react.production.min.js ───────────────────────── 5.9kb ── 21.4% + │ └ node_modules/react/index.js + │ └ example.jsx + ├ node_modules/object-assign/index.js ──────────────────────────────────── 965b ──── 3.4% + │ └ node_modules/react-dom/cjs/react-dom-server.browser.production.min.js + │ └ node_modules/react-dom/server.browser.js + │ └ example.jsx + ├ example.jsx ──────────────────────────────────────────────────────────── 137b ──── 0.5% + ├ node_modules/react-dom/server.browser.js ──────────────────────────────── 50b ──── 0.2% + │ └ example.jsx + └ node_modules/react/index.js ───────────────────────────────────────────── 50b ──── 0.2% + └ example.jsx + ``` + + There is also a JS API for this: + + ```js + const result = await esbuild.build({ + metafile: true, + ... + }) + console.log(await esbuild.analyzeMetafile(result.metafile, { + verbose: true, + })) + ``` + + and a Go API: + + ```js + result := api.Build(api.BuildOptions{ + Metafile: true, + ... + }) + fmt.Println(api.AnalyzeMetafile(result.Metafile, api.AnalyzeMetafileOptions{ + Verbose: true, + })) + ``` + + Note that this is not the only way to visualize this data. If you want a visualization that's different than the information displayed here, you can easily build it yourself using the information in the metafile that is generated with the `--metafile=` flag. + + Also note that this data is intended for humans, not machines. The specific format of this data may change over time which will likely break any tools that try to parse it. You should not write a tool to parse this data. You should be using the information in the JSON metadata file instead. Everything in this visualization is derived from the JSON metadata so you are not losing out on any information by not using esbuild's output. + +* Allow `require.resolve` in non-node builds ([#1579](https://github.com/evanw/esbuild/issues/1579)) + + With this release, you can now use `require.resolve` in builds when the target platform is set to `browser` instead of `node` as long as the function `window.require.resolve` exists somehow. This was already possible when the platform is `node` but when the platform is `browser`, esbuild generates a no-op shim `require` function for compatibility reasons (e.g. because some code expects `typeof require` must be `"function"` even in the browser). The shim previously had a fallback to `window.require` if it exists, but additional properties of the `require` function such as `require.resolve` were not copied over to the shim. Now the shim function is only used if `window.require` is undefined so additional properties such as `require.resolve` should now work. + + This change was contributed by [@screetBloom](https://github.com/screetBloom). + +## 0.12.25 + +* Fix a TypeScript parsing edge case with the postfix `!` operator ([#1560](https://github.com/evanw/esbuild/issues/1560)) + + This release fixes a bug with esbuild's TypeScript parser where the postfix `!` operator incorrectly terminated a member expression after the `new` operator: + + ```js + // Original input + new Foo!.Bar(); + + // Old output + new Foo().Bar(); + + // New output + new Foo.Bar(); + ``` + + The problem was that `!` was considered a postfix operator instead of part of a member expression. It is now considered to be part of a member expression instead, which fixes this edge case. + +* Fix a parsing crash with nested private brand checks + + This release fixes a bug in the parser where code of the form `#a in #b in c` caused a crash. This code now causes a syntax error instead. Private identifiers are allowed when followed by `in`, but only if the operator precedence level is such that the `in` operator is allowed. The parser was missing the operator precedence check. + +* Publish x86-64 binary executables for illumos ([#1562](https://github.com/evanw/esbuild/pull/1562)) + + This release adds support for the [illumos](https://www.illumos.org/) operating system, which is related to Solaris and SunOS. Support for this platform was contributed by [@hadfl](https://github.com/hadfl). + +## 0.12.24 + +* Fix an edge case with direct `eval` and variable renaming + + Use of the direct `eval` construct causes all variable names in the scope containing the direct `eval` and all of its parent scopes to become "pinned" and unable to be renamed. This is because the dynamically-evaluated code is allowed to reference any of those variables by name. When this happens esbuild avoids renaming any of these variables, which effectively disables minification for most of the file, and avoids renaming any non-pinned variables to the name of a pinned variable. + + However, there was previously a bug where the pinned variable name avoidance only worked for pinned variables in the top-level scope but not in nested scopes. This could result in a non-pinned variable being incorrectly renamed to the name of a pinned variable in certain cases. For example: + + ```js + // Input to esbuild + return function($) { + function foo(arg) { + return arg + $; + } + // Direct "eval" here prevents "$" from being renamed + // Repeated "$" puts "$" at the top of the character frequency histogram + return eval(foo($$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$)) + }(2); + ``` + + When this code is minified with `--minify-identifiers`, the non-pinned variable `arg` is incorrectly transformed into `$` resulting in a name collision with the nested pinned variable `$`: + + ```js + // Old output from esbuild (incorrect) + return function($) { + function foo($) { + return $ + $; + } + return eval(foo($$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$)); + }(2); + ``` + + This is because the non-pinned variable `arg` is renamed to the top character in the character frequency histogram `$` (esbuild uses a character frequency histogram for smaller gzipped output sizes) and the pinned variable `$` was incorrectly not present in the list of variable names to avoid. With this release, the output is now correct: + + ```js + // New output from esbuild (correct) + return function($) { + function foo(n) { + return n + $; + } + return eval(foo($$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$)); + }(2); + ``` + + Note that even when esbuild handles direct `eval` correctly, using direct `eval` is not recommended because it disables minification for the file and likely won't work correctly in the presence of scope hoisting optimizations. See https://esbuild.github.io/link/direct-eval for more details. + +## 0.12.23 + +* Parsing of rest arguments in certain TypeScript types ([#1553](https://github.com/evanw/esbuild/issues/1553)) + + This release implements parsing of rest arguments inside object destructuring inside arrow functions inside TypeScript type declarations. Support for rest arguments in this specific syntax was not previously implemented. The following code was incorrectly considered a syntax error before this release, but is no longer considered a syntax error: + + ```ts + type F = ({ ...rest }) => void; + ``` + +* Fix error message for `watch: true` and `buildSync` ([#1552](https://github.com/evanw/esbuild/issues/1552)) + + Watch mode currently only works with the `build` API. Previously using watch mode with the `buildSync` API caused a confusing error message. This release explicitly disallows doing this, so the error message is now more clear. + +* Fix an minification bug with the `--keep-names` option ([#1552](https://github.com/evanw/esbuild/issues/1552)) + + This release fixes a subtle bug that happens with `--keep-names --minify` and nested function declarations in strict mode code. It can be triggered by the following code, which was being compiled incorrectly under those flags: + + ```js + export function outer() { + { + function inner() { + return Math.random(); + } + const x = inner(); + console.log(x); + } + } + outer(); + ``` + + The bug was caused by an unfortunate interaction between a few of esbuild's behaviors: + + 1. Function declarations inside of nested scopes behave differently in different situations, so esbuild rewrites this function declaration to a local variable initialized to a function expression instead so that it behaves the same in all situations. + + More specifically, the interpretation of such function declarations depends on whether or not it currently exists in a strict mode context: + + ``` + > (function(){ { function x(){} } return x })() + function x() {} + + > (function(){ 'use strict'; { function x(){} } return x })() + ❌ Uncaught ReferenceError: x is not defined + ``` + + The bundling process sometimes erases strict mode context. For example, different files may have different strict mode status but may be merged into a single file which all shares the same strict mode status. Also, files in ESM format are automatically in strict mode but a bundle output file in IIFE format may not be executed in strict mode. Transforming the nested `function` to a `let` in strict mode and a `var` in non-strict mode means esbuild's output will behave reliably in different environments. + + 2. The "keep names" feature adds automatic calls to the built-in `__name` helper function to assign the original name to the `.name` property of the minified function object at run-time. That transforms the code into this: + + ```js + let inner = function() { + return Math.random(); + }; + __name(inner, "inner"); + const x = inner(); + console.log(x); + ``` + + This injected helper call does not count as a use of the associated function object so that dead-code elimination will still remove the function object as dead code if nothing else uses it. Otherwise dead-code elimination would stop working when the "keep names" feature is enabled. + + 3. Minification enables an optimization where an initialized variable with a single use immediately following that variable is transformed by inlining the initializer into the use. So for example `var a = 1; return a` is transformed into `return 1`. This code matches this pattern (initialized single-use variable + use immediately following that variable) so the optimization does the inlining, which transforms the code into this: + + ```js + __name(function() { + return Math.random(); + }, "inner"); + const x = inner(); + console.log(x); + ``` + + The code is now incorrect because `inner` actually has two uses, although only one was actually counted. + + This inlining optimization will now be avoided in this specific case, which fixes the bug without regressing dead-code elimination or initialized variable inlining in any other cases. + +## 0.12.22 + +* Make HTTP range requests more efficient ([#1536](https://github.com/evanw/esbuild/issues/1536)) + + The local HTTP server built in to esbuild supports [range requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests), which are necessary for video playback in Safari. This means you can now use `