UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

367 lines • 15.4 kB
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. ========= // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // ========= Copyright 2026 @ Strukto.AI All Rights Reserved. ========= import { enotsup, staleWrite } from "../errors/fs.js"; import { markLost } from "../observe/context.js"; import { currentCaller, liveCallers, runWithCaller } from "../context/session_context.js"; import { keyPath, underPath } from "../utils/key_prefix.js"; import { rstripSlash } from "../utils/slash.js"; import { ANY_WRITE, ABSENT_WRITE, OwnRead, } from "./types.js"; /** * Run `fn` with `manager` active for the current async context. * Mirrors `runWithRevisions`: the mount entry point wraps command * dispatch. */ export function runWithCacheManager(manager, fn) { return runWithCaller({ ...currentCaller(), cache: { manager } }, fn); } /** * Return the active cache manager for the current async context. * * Serves the read-through paths, so a wrong manager is worse than * none: a warm hit from another mount's cache is another mount's * bytes, where a miss just reads the backend. On an isolating runtime * one binding is live and answers as bound; on the fallback storage * the manager answers only while every live frame agrees on it, and a * disagreement (overlapping commands on different mounts) reads as no * manager, failing toward the cold read. */ export function activeCacheManager() { const states = liveCallers().flatMap((caller) => (caller.cache === null ? [] : [caller.cache])); const first = states[0]; if (first === undefined) return null; for (const state of states) { if (state.manager !== first.manager) return null; } return first.manager; } /** * Run `op`, then `settle`, also when `op` fails. * * An op that fails partway (a paginated delete, a folder copy that merged * some children) has already changed the backend, so its record is owed * either way. `settle` gets the op's result, or undefined when the op * failed. After a failed op a settle error is reported, not thrown, so the * caller still learns why the op failed. Failure records invalidate shared * caches but preserve session versions for untouched files. Mirrors Python's `settle_after`. * * Args: * op: the backend change. * settle: records what the op changed, given its result. */ export async function settleAfter(op, settle) { let result; try { result = await op(); } catch (error) { await runWithCaller({ ...currentCaller(), versionReads: false }, () => settle(undefined)).catch((settleError) => { console.warn(`settling after a failed op: ${String(settleError)}`); }); throw error; } await settle(result); return result; } /** Collect tokens belonging to the exact bytes returned by a fetch. */ export async function captureRead(path, fetch) { const facts = new WeakMap(); return runWithCaller({ ...currentCaller(), readFacts: { path, facts } }, async () => { const data = await fetch(); return [data, data instanceof Uint8Array ? (facts.get(data) ?? []) : []]; }); } /** Publish a backend-verified token without activating observation. */ export function publishRead(path, data, fingerprint) { // Fallback contexts overlap; exact result identity makes broadcast safe. for (const capture of liveCallers().flatMap((caller) => caller.readFacts === null ? [] : [caller.readFacts])) { if (capture.path !== path) continue; const tokens = capture.facts.get(data) ?? []; tokens.push(fingerprint); capture.facts.set(data, tokens); } } /** Run `fn` with `context` bound as the write context of the mount at `prefix`. */ export function runWithWriteContext(prefix, context, fn) { return runWithCaller({ ...currentCaller(), write: { prefix, context } }, fn); } /** * The write context of the current async context. On the fallback storage * overlapping lines on different mounts hold frames at once; the mount * that owns the path answers, the longest prefix covering it as the mount * table routes. Frames there that still disagree, or none covering the * path, cannot read as "none", which would send a conditional mount's write * unconditioned, so the write is refused instead. */ function activeWriteContext(path) { const states = liveCallers().flatMap((caller) => (caller.write === null ? [] : [caller.write])); if (agree(states, (state) => state.context)) return states[0]?.context ?? null; const owners = ownersOf(path, states); if (owners.length === 0 || !agree(owners, (state) => state.context)) throw overlapping(path); return owners[0]?.context ?? null; } /** The live frames of the longest prefix covering `path`, as the mount table routes. */ function ownersOf(path, states) { let best = -1; let owners = []; for (const state of states) { if (!underPath(path.virtual, state.prefix)) continue; const length = rstripSlash(state.prefix).length; if (length > best) [best, owners] = [length, [state]]; else if (length === best) owners.push(state); } return owners; } /** Whether every state picks the same value. */ function agree(states, pick) { return states.every((state, i) => i === 0 || pick(state) === pick(states[0])); } function overlapping(path) { return enotsup('workspace', 'conditional write (overlapping lines)', path); } export function conditionVersion(condition) { return condition?.kind === 'match' ? condition.version : null; } /** * What a write's condition asked for: a version, ABSENT for a create. Mirrors * Python's `condition_basis`. */ export function conditionBasis(condition) { return condition?.kind === 'absent' ? OwnRead.ABSENT : conditionVersion(condition); } export function requireSupported(support, vfs, path, kind, condition) { if (condition.kind !== 'any' && !(support[kind] ?? []).includes(condition.kind)) { throw enotsup(vfs, `conditional ${kind} (${condition.kind})`, path); } } /** * Run a read and return its bytes with the token they carried: the one its * backend published for these exact bytes, null when it published none or * two that disagree. Mirrors Python's `read_versioned`. */ export async function readVersioned(path, fetch) { const [data, facts] = await captureRead(path.virtual, fetch); const first = facts[0]; const token = first !== undefined && facts.every((f) => f === first) ? first : null; return [data, token]; } export async function writeCondition(path, kind, own = null) { const context = activeWriteContext(path); if (context === null) return kind === 'put' ? (own === OwnRead.ABSENT ? ABSENT_WRITE : ANY_WRITE) : null; return settle(context, path, kind, own, await context.readVersion(path)); } /** * The condition a delete of `path` carries, null when unconditional: a * delete needs no read of its own, only that nobody wrote since, so the * version the mount holds, else the one its own lookup found. Mirrors * Python's `delete_condition`. * * @throws a stale-write error when the session found no file there, and one * has appeared since * @throws an ENOTSUP error when the backend cannot condition a delete */ export async function deleteCondition(path, lookedUp) { const context = activeWriteContext(path); if (context === null) return null; let cached = await context.readVersion(path); if (cached === OwnRead.ABSENT) { if (lookedUp !== null && lookedUp !== '') throw await stale(path); cached = null; } const version = cached !== null && cached !== '' ? cached : lookedUp; return condition(context, path, 'delete', version !== '' ? version : null); } /** * The conditions a move carries: the copy onto `dst` and the source's pin, * both versions from one store lookup. Null and null when unconditional. * Mirrors Python's `move_condition`. * * @throws a stale-write error when the op found removed a file the mount saw * @throws an ENOTSUP error when the backend cannot condition the move */ export async function moveCondition(src, dst) { const context = activeWriteContext(dst); if (context === null) return [null, null]; const [dstCached = null, srcCached = null] = await context.readVersions([dst, src]); const cond = await settle(context, dst, 'copy', null, dstCached); const source = await settle(context, src, 'delete', null, srcCached); return [cond, conditionVersion(source)]; } async function settle(context, path, kind, read, cached) { if (read === OwnRead.UNKNOWN) return condition(context, path, kind, null); // The op's own read found no file: one the mount saw was removed since. if (read === OwnRead.ABSENT && typeof cached === 'string' && cached !== '') { throw await stale(path); } if (read === OwnRead.ABSENT && kind === 'put') return ABSENT_WRITE; const mine = read === OwnRead.ABSENT ? null : read; const ownToken = mine === '' ? null : mine; // The session last found no file here. if (cached === OwnRead.ABSENT) { if (ownToken === null && (context.creates ?? []).includes(kind)) return ABSENT_WRITE; return condition(context, path, kind, ownToken); } const cachedToken = cached === '' ? null : cached; return condition(context, path, kind, ownToken ?? cachedToken); } function condition(context, path, kind, version) { // A put with no version goes out plain, so it needs no condition. if (kind !== 'put' || version !== null) require(context, path, kind); return version !== null ? { kind: 'match', version } : ANY_WRITE; } function require(context, path, kind) { if (!context.conditions.includes(kind)) throw enotsup(context.vfs, `conditional ${kind}`, path); } /** * The native token a write sends in place of the held content token, null when * the write goes plain. Mirrors Python's `native_condition`. */ export async function nativeCondition(path, cond, live, kind) { if (cond?.kind === 'absent') throw enotsup(activeWriteContext(path)?.vfs ?? 'native', `conditional ${kind} (absent)`, path); const held = conditionVersion(cond); if (held === null || held === '') return null; if (live?.content !== held) throw await stale(path, { version: held }); if (live.native === null || live.native === '') { throw enotsup(activeWriteContext(path)?.vfs ?? 'native', `conditional ${kind}`, path); } return live.native; } /** * Keep the version of every file a walk left behind; refuse the first. Mirrors * Python's `keep_refused`. */ export async function keepRefused(lost) { const refusals = []; for (const [spec, version] of lost) refusals.push(await stale(spec, { version })); return refusals[0] ?? null; } /** * The version the mount holds for each path, in one store round trip. Mirrors * Python's `held_versions`. */ export async function heldVersions(paths) { const first = paths[0]; const context = first === undefined ? null : activeWriteContext(first); if (context === null) return paths.map(() => null); return (await context.readVersions(paths)).map((token) => typeof token === 'string' ? token : null); } /** * Whether the mount that owns `path` is a `write: conditional` one; true while * overlapping lines leave the owner unclear. Mirrors Python's * `writes_conditioned`. */ export function writesConditioned(path) { const states = liveCallers().flatMap((caller) => (caller.write === null ? [] : [caller.write])); if (agree(states, (state) => state.context)) return (states[0]?.context ?? null) !== null; const owners = ownersOf(path, states); return owners.length === 0 || owners.some((state) => state.context !== null); } /** * Whether a `kind` on `path` goes out conditioned, for a prefix walk, which * conditions each key itself and needs no version for the operand. Mirrors * Python's `conditioned`. * * @throws an ENOTSUP error when the backend cannot condition this op */ export function conditioned(path, kind) { const context = activeWriteContext(path); if (context === null) return false; require(context, path, kind); return true; } /** * Drop the write context's cached copy of `path`, if there is one. `keep` is * the version to keep without bytes, ABSENT for a refused create. */ export async function dropCached(path, keep = null) { markLost(path, typeof keep === 'string' ? keep : null); const context = activeWriteContext(path); if (context === null) return; await context.drop(path); if (keep !== null && keep !== '') await context.keep(path, keep); } /** * Drop `path`'s cached copy, keeping what a refused write sent. The condition * stays, so a retry without a read sends it again and is refused again. That * holds for a file found gone too: a peer may create it again before the * retry. `version` is the version the write sent, ABSENT for a create; null * keeps what the session last saw. Mirrors Python's `keep_sent`. */ export async function keepSent(path, version = null) { const context = activeWriteContext(path); let keep = version; if (keep === null && context !== null) keep = await context.readVersion(path); await dropCached(path, keep); } /** * The refusal for a lost condition, after `keepSent`. `landed` marks a move * whose copy landed before its source's delete lost, which `mv` reports as a * failed removal; `version` is the one the write sent, ABSENT for a create, * when the line no longer names it (a move retracts both paths). Mirrors * Python's `stale`. */ export async function stale(path, options = {}) { const { landed = false, version = null } = options; await keepSent(path, version); return staleWrite(path, landed); } /** * The versions a prefix walk under `root` measures its keys against: a key * the agent read is held to the version it read, and the walk's own listing * only stands in for keys it never saw. Mirrors Python's `known_versions`. */ export function knownVersions(root, keyPrefix) { return async (keys) => { const known = new Map(); if (keys.length === 0) return known; const context = activeWriteContext(root); if (context === null) return known; const tokens = await context.readVersions(keys.map((key) => keyPath(root, keyPrefix, key))); keys.forEach((key, i) => { const token = tokens[i]; if (typeof token === 'string' && token !== '') known.set(key, token); }); return known; }; } //# sourceMappingURL=context.js.map