Reload extension file snapshots without Git

This commit is contained in:
2026-11-12 12:22:01 +00:00
parent a46d67e4ff
commit b4b2fbe33e
28 changed files with 1690 additions and 1886 deletions
+83 -70
View File
@@ -37110,6 +37110,7 @@ var ops2 = Deno.core.ops;
var encoder4 = new TextEncoder();
var decoder4 = new TextDecoder();
var ExtensionState = defineDoc({ kind: "sausage.extensions", version: 1, scope: "session", checkpointWhen: () => false, initial: () => ({ good: null, current: null, error: null }) });
var ExtensionFiles = defineDoc({ kind: "sausage.extensions.files", version: 1, scope: "session", history: "latest", checkpointWhen: () => false, initial: () => ({ hash: null, files: [] }) });
var Metadata = defineDoc({ kind: "sausage.metadata", version: 1, scope: "conversation", history: "latest", fork: "current", initial: () => ({ label: "", title: "" }), checkpointWhen: () => false });
async function file(kind, path, bytes2 = []) {
const result = await ops2.op_file(kind, path, bytes2);
@@ -37182,28 +37183,54 @@ async function loadExtensions(harness2, registry2, storage2, models2, context, b
let state = { ...await harness2.snapshot(ExtensionState, context) ?? { good: null, current: null, error: null } };
let settings = {};
let active = true;
const commands = new AbortController();
const loaderGit = async (...args) => {
for (let attempt = 0; ; attempt++) {
try {
const result = await exec(["git", ...args], { cwd: repo, signal: commands.signal });
if (result.code) throw new Error(result.stderr || `git exited ${result.code}`);
return result.stdout.trim();
} catch (error) {
const message = `git ${args.join(" ")}: ${String(error)}`;
state.error = message;
ops2.op_extension_status(JSON.stringify(state));
if (attempt === 0 && !commands.signal.aborted && String(error).includes("command timed out")) continue;
throw new Error(message);
let installed = false;
let queue2 = Promise.resolve();
const publish2 = async (copy2) => {
await harness2.commit(async (tx) => {
Object.assign(await tx.doc(ExtensionState), state);
if (copy2) Object.assign(await tx.doc(ExtensionFiles), copy2);
}, context);
ops2.op_extension_status(JSON.stringify(state));
};
const readCopy = async () => {
const files = [];
const walk = async (relative) => {
const path = `${repo}/extensions${relative}`;
const entries2 = await fs.list(path).catch((error) => {
if (error.code === "not_found") return [];
throw error;
});
entries2.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
for (const entry2 of entries2) {
const name = `${relative}/${entry2.name}`;
if (entry2.kind === "directory") await walk(name);
else files.push([name, await file("read", `${repo}/extensions${name}`)]);
}
};
await walk("");
const digest = await crypto.subtle.digest("SHA-256", encoder4.encode(JSON.stringify(files)));
const hash = Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
return { hash, files };
};
const mkdir = async (path) => {
try {
await fs.mkdir(path);
} catch (error) {
if (error.code !== "already_exists") throw error;
}
};
let queue2 = Promise.resolve();
const publish2 = async () => {
await harness2.commit(async (tx) => Object.assign(await tx.doc(ExtensionState), state), context);
ops2.op_extension_status(JSON.stringify(state));
await fs.write("/src/sausage/.git/sausage-last-good.tmp", state.good ?? "");
await fs.rename("/src/sausage/.git/sausage-last-good.tmp", "/src/sausage/.git/sausage-last-good");
const snapshot = async ({ hash, files }) => {
const base = `/run/sausage/extensions/${hash}`;
for (const path of ["/run/sausage", "/run/sausage/extensions", base, `${base}/extensions`]) await mkdir(path);
for (const [name, bytes2] of files) {
let parent = `${base}/extensions`;
for (const part of name.slice(1).split("/").slice(0, -1)) {
parent += `/${part}`;
await mkdir(parent);
}
await file("write", `${base}/extensions${name}`, bytes2);
}
return base;
};
const dispose = async (entries2) => {
for (const facet of entries2) for (const cleanup of facet.close) {
@@ -37278,42 +37305,18 @@ async function loadExtensions(harness2, registry2, storage2, models2, context, b
};
return { host: host2, open, close };
};
const load = async (sha) => {
const load = async (copy2) => {
const next = [];
const staged = /* @__PURE__ */ new Map();
try {
const snapshot = `/run/sausage/extensions/${sha}`;
for (const path of ["/run/sausage", "/run/sausage/extensions"]) {
try {
await fs.mkdir(path);
} catch (error) {
if (error.code !== "already_exists") throw error;
}
}
try {
await fs.stat(snapshot);
} catch {
const staging = `${snapshot}.tmp`, archive = `${snapshot}.tar`;
try {
await fs.mkdir(staging);
} catch (error) {
if (error.code !== "already_exists") throw error;
}
if (await loaderGit("ls-tree", "--name-only", sha, "extensions")) {
await loaderGit("archive", "--format=tar", `--output=${archive}`, sha, "extensions");
const result = await exec(["tar", "-xf", archive, "-C", staging], { cwd: repo, signal: commands.signal });
if (result.code) throw new Error(`tar: ${result.stderr || result.code}`);
await fs.remove(archive);
}
await fs.rename(staging, snapshot);
}
const pathRoot = await snapshot(copy2);
const modules = [];
for (const entry2 of await fs.list(`${snapshot}/extensions`).catch((error) => {
for (const entry2 of await fs.list(`${pathRoot}/extensions`).catch((error) => {
if (error.code === "not_found") return [];
throw error;
})) {
if (entry2.kind !== "directory") continue;
const path = `${snapshot}/extensions/${entry2.name}/server.ts`;
const path = `${pathRoot}/extensions/${entry2.name}/server.ts`;
try {
await fs.stat(path);
} catch (error) {
@@ -37336,7 +37339,8 @@ async function loadExtensions(harness2, registry2, storage2, models2, context, b
}
const probe2 = createRegistry();
for (const extension of [...builtins, ...next.map((f) => f.extension)]) probe2.install(extension);
for (const old2 of facets) registry2.uninstall(old2.extension);
const names2 = new Set(next.map((f) => f.extension.name));
for (const old2 of facets) if (!names2.has(old2.extension.name)) registry2.uninstall(old2.extension);
for (const extension of [...builtins, ...next.map((f) => f.extension)]) registry2.install(extension);
const old = facets;
facets = next;
@@ -37349,7 +37353,8 @@ async function loadExtensions(harness2, registry2, storage2, models2, context, b
facet.host.notice(`onOpen: ${String(error)}`);
}
}
state.good = sha;
installed = true;
state.good = copy2.hash;
return null;
} catch (error) {
await dispose(next);
@@ -37357,14 +37362,7 @@ async function loadExtensions(harness2, registry2, storage2, models2, context, b
}
};
const reload = async () => {
let tip;
try {
tip = await loaderGit("rev-parse", "refs/heads/main");
} catch (error) {
state.error = String(error);
ops2.op_extension_status(JSON.stringify(state));
return;
}
let good;
try {
settings = JSON.parse(await fs.read(`${repo}/settings.json`));
} catch (error) {
@@ -37376,24 +37374,39 @@ async function loadExtensions(harness2, registry2, storage2, models2, context, b
`);
}
}
if (state.current === tip && facets.length) return;
state.current = tip;
state.error = await load(tip);
if (state.error && !facets.length && state.good && state.good !== tip) {
const failed = state.error;
const fallback = await load(state.good);
state.error = fallback ? `${failed}
Last good: ${fallback}` : failed;
try {
const copy2 = await readCopy();
if (state.current === copy2.hash && installed) return;
state.current = copy2.hash;
state.error = await load(copy2);
if (!state.error) good = copy2;
} catch (error) {
state.error = String(error);
}
await publish2();
if (state.error && !installed) {
const saved = await harness2.snapshot(ExtensionFiles, context);
if (saved?.hash) {
const failed = state.error;
const fallback = await load(saved);
state.error = fallback ? `${failed}
Last good: ${fallback}` : failed;
}
}
await publish2(good);
};
const enqueue2 = () => queue2 = queue2.then(reload);
const rid = ops2.op_watch([`${repo}/.git/refs/heads/main`, `${repo}/.git/packed-refs`, `${repo}/settings.json`]);
const rid = ops2.op_watch([`${repo}/extensions`, `${repo}/settings.json`]);
let timer;
await enqueue2();
const pending = (async () => {
while (active) {
await ops2.op_watch_next(rid);
if (active) await enqueue2();
const event = await ops2.op_watch_next(rid);
if (!event || !active) break;
clearTimeout(timer);
timer = setTimeout(() => {
timer = void 0;
void enqueue2();
}, 75);
}
})();
return { reload: enqueue2, state: () => ({ ...state }), async mainAgent() {
@@ -37408,7 +37421,7 @@ Last good: ${fallback}` : failed;
return action(args, { host: facet.host, signal, conversationId, source: "ui" });
}, close: async () => {
active = false;
commands.abort();
clearTimeout(timer);
Deno.core.close(rid);
await pending;
await queue2;