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
+1 -1
View File
@@ -21,7 +21,7 @@ prompt: |-
Sausage is a world of programs and durable agents. All agents see and change the same world.
Use the programs in /bin through bash. Use explicit repository paths. Worktrees live at /worktrees/<id>.
The platform source lives in /src/sausage. Extensions reload after a commit to main.
The platform source lives in /src/sausage. Saved extension file edits reload automatically. No Git commit is required.
/etc/sausage/config.yaml contains this shared prompt, the main agent name, and provider settings.
Agent definitions live in /etc/sausage/roster/<name>.md. Project definitions live in <repo>/.sausage/roster/*.md.
You can read and edit the config and the roster files. Changes apply at the next run.
+11 -15
View File
@@ -34,7 +34,7 @@ nix develop -c nu userland/bundle.nu
It builds the world commands and creates `target/userland/bundle` by default.
It snapshots committed source, not uncommitted edits.
The source archive includes the complete `config/`, `skills/`, and `extensions/` directories.
The bundle contains plain source files. `sausage init` creates their repository and its first commit on `main`.
The bundle contains plain source files. `sausage init` copies them without a Git repository or first commit.
The world copier preserves symlinks, including dangling links in the source tree.
A fresh development world needs no manual source copy.
The bundle includes `/bin/web.wasm` and an enabled `/etc/systemd/system/web.service`.
@@ -192,25 +192,21 @@ and [`models`](../extensions/models/README.md).
## Change extensions
The world checkout is `/src/sausage`. The loader follows its **`main`** branch, not the host checkout's `v3` branch.
Each extension lives at `extensions/NAME/server.ts` inside that checkout.
Commit extension edits to `main` with world git. A saved, uncommitted edit does not reload an extension.
Use `--no-gpg-sign` for commits.
The platform source is `/src/sausage`. Each extension lives at `extensions/NAME/server.ts`.
Save extension edits directly. No Git commit or restart is required.
The VFS watcher waits for a short quiet period, then hashes the extension files.
Unchanged contents do not reload.
The loader imports a detached snapshot under `/run/sausage/extensions/COMMIT`.
The loader copies the files under `/run/sausage/extensions/HASH`.
It validates the entire candidate before it replaces the live registry.
A broken candidate leaves the previous good commit active.
A broken candidate leaves the previous good set active.
An active tool call retains its implementation. The next call uses the new implementation.
Read `/proc/extensions` through the agent's file tools to inspect `current`, `good`, and `error`.
To restore the last good branch ref, ask the agent to execute:
```sh
extensions-revert
```
The command preserves editable files. It changes `refs/heads/main`, which triggers the normal reload.
At startup, the loader tries the tip and then the last good commit before it resumes work.
Use `undo` to restore earlier files. The normal file watch reloads them.
At startup, the loader hashes the current files, including edits made while the host was offline.
If they fail, it loads the last good copy from `host/harness.sqlite` and shows the error.
The fallback does not change editable files.
Source: [`extensions`](../extensions/README.md) and [`extensions.ts`](../host/js/extensions.ts).
+25 -29
View File
@@ -1,29 +1,35 @@
# World extensions
The harness loads `/src/sausage` from its `main` branch.
Each extension occupies `extensions/<name>/server.ts` in that repository.
The harness loads files under `/src/sausage/extensions/`.
Each extension occupies `<name>/server.ts` in that directory. No Git repository or commit is required.
The default export is an extension or a factory that receives a `Host`.
The extension name must match its folder.
An optional numeric `order` export controls order. Names break ties.
## Reload contract
A change to the branch ref or `packed-refs` triggers a reload.
The harness creates a detached world worktree at `/run/sausage/extensions/<sha>`.
Imports read that snapshot through the world VFS, not the editable checkout.
The VFS watcher observes the extension directory recursively.
After changes, the harness waits for 75 milliseconds of quiet before it reads the files.
It computes a SHA-256 hash over sorted paths and their byte contents, only at startup and after file changes.
Unchanged contents do not reload.
The harness copies the captured bytes to `/run/sausage/extensions/<hash>/extensions/`.
Imports read that snapshot through the world VFS, not the editable files.
Relative imports must include their file extensions.
TypeScript modules use the same `deno_ast` transpiler as the harness.
The harness imports and validates the whole commit before it changes the live registry.
An import error, factory error, name mismatch, or registry validation error retains the previous commit.
The harness imports and validates the whole file set before it changes the live registry.
An import error, factory error, name mismatch, or registry validation error retains the previous set.
A running tool call keeps its old implementation. The next call uses the new implementation.
Removed extensions leave the registry after a successful reload.
`/proc/extensions` contains JSON with `good`, `current`, and `error`.
A session document persists these values in the harness database.
On startup, the harness tries the tip, then the last good commit, before `resume()`.
`/proc/extensions` contains JSON with `good`, `current`, and `error`. Version values are content hashes.
A session document persists these values in the host database, `<dir>/host/harness.sqlite`.
After successful validation, another session document retains the complete last good file set in that database.
Broken edits do not replace that copy.
On startup, the harness hashes and loads the current files, including offline edits.
If they fail, it loads the host-side copy and reports the current error before `resume()`.
Tasks without a registered definition remain blocked.
The snapshot worktrees disappear with `/run` on restart. The loader prunes stale worktree registrations before it recreates them.
The snapshot directories disappear with `/run` on restart. The host-side copy survives.
## Host
@@ -35,7 +41,7 @@ The host supplies these members:
- `exec(command, options)` for shell strings or argument arrays. Its result contains `code`, `stdout`, and `stderr`.
- Exec options: `cwd`, `env` pairs, `timeout` in seconds, `conversations`, `signal`, and `onOutput(text, stream)`.
- `git(cwd, ...args)`, which returns trimmed stdout and throws with stderr on failure.
- `implement(api, value)`, which stages a service until the commit installs.
- `implement(api, value)`, which stages a service until the file set installs.
- `need(api)`, which returns a proxy. Every method call resolves the current provider, including a retained method reference.
- `want(api)`, which returns a proxy or `undefined` when no provider exists.
- `onOpen(callback)` and `onClose(callback)` for facet lifecycle.
@@ -58,25 +64,15 @@ The registry supplies the default extension selection for existing conversations
New root conversations do not pin the built-ins.
Explicit per-conversation extension selections remain explicit.
## Revert
## Restore files
Run this command inside the world:
```sh
extensions-revert
```
The Rust program lives in `userland/exec-tools/src/revert.rs`.
It reads `/src/sausage/.git/sausage-last-good` and calls world git to update `refs/heads/main`.
The loader writes this helper file after it persists the session status.
An empty helper file means no commit succeeded yet.
The command preserves the editable checkout. The branch change triggers the normal reload.
Use `undo` to restore earlier files. The normal file watch reloads the restored set.
The startup fallback does not change editable files.
## Tests
`host/tests/extensions.rs` runs `host/js/extensions-test-bundle.js` with world git and a faux model.
It tests A, broken B, changed C, an old call across reload, a retained service handle, and restart recovery.
It also tests the revert command, file operations, and exec environment options.
The world git and helper components use the executor's shared `CARGO_TARGET_DIR`.
The API and hook fixtures use that directory too.
`host/tests/it/extensions.rs` runs `host/js/extensions-test-bundle.js` with a faux model and a process interface that rejects all commands.
It tests file edits, broken edits, a multi-file burst, an old call across reload, and a retained service handle.
It also tests unchanged hashes, offline edits, and recovery from the host-side copy across restarts.
The init tests verify that `/src/sausage` contains no `.git` directory.
The Nix shell preserves the user's Cargo configuration and its kache wrapper.
+84 -71
View File
@@ -39914,28 +39914,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) {
@@ -40010,42 +40036,18 @@ async function loadExtensions(harness2, registry2, storage2, models2, context, b
};
return { host, open, close };
};
const load = async (sha) => {
const load = async (copy2) => {
const next2 = [];
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) {
@@ -40068,7 +40070,8 @@ async function loadExtensions(harness2, registry2, storage2, models2, context, b
}
const probe = createRegistry();
for (const extension of [...builtins, ...next2.map((f) => f.extension)]) probe.install(extension);
for (const old2 of facets) registry2.uninstall(old2.extension);
const names2 = new Set(next2.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, ...next2.map((f) => f.extension)]) registry2.install(extension);
const old = facets;
facets = next2;
@@ -40081,7 +40084,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(next2);
@@ -40089,14 +40093,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) {
@@ -40108,24 +40105,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() {
@@ -40140,14 +40152,14 @@ 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;
await dispose(facets);
} };
}
var ops2, encoder4, decoder4, ExtensionState, Metadata, fs;
var ops2, encoder4, decoder4, ExtensionState, ExtensionFiles, Metadata, fs;
var init_extensions = __esm({
"extensions.ts"() {
init_dist4();
@@ -40164,6 +40176,7 @@ var init_extensions = __esm({
encoder4 = new TextEncoder();
decoder4 = new TextDecoder();
ExtensionState = defineDoc({ kind: "sausage.extensions", version: 1, scope: "session", checkpointWhen: () => false, initial: () => ({ good: null, current: null, error: null }) });
ExtensionFiles = defineDoc({ kind: "sausage.extensions.files", version: 1, scope: "session", history: "latest", checkpointWhen: () => false, initial: () => ({ hash: null, files: [] }) });
Metadata = defineDoc({ kind: "sausage.metadata", version: 1, scope: "conversation", history: "latest", fork: "current", initial: () => ({ label: "", title: "" }), checkpointWhen: () => false });
fs = {
read: async (path) => decoder4.decode(new Uint8Array(await file("read", path))),
+83 -70
View File
@@ -78030,6 +78030,7 @@ var ops2 = Deno.core.ops;
var encoder5 = 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, path5, bytes2 = []) {
const result = await ops2.op_file(kind, path5, bytes2);
@@ -78102,28 +78103,54 @@ async function loadExtensions(harness2, registry3, storage2, models2, context, b
let state2 = { ...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)}`;
state2.error = message;
ops2.op_extension_status(JSON.stringify(state2));
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), state2);
if (copy2) Object.assign(await tx.doc(ExtensionFiles), copy2);
}, context);
ops2.op_extension_status(JSON.stringify(state2));
};
const readCopy = async () => {
const files = [];
const walk = async (relative) => {
const path5 = `${repo}/extensions${relative}`;
const entries2 = await fs2.list(path5).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", encoder5.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 (path5) => {
try {
await fs2.mkdir(path5);
} 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), state2), context);
ops2.op_extension_status(JSON.stringify(state2));
await fs2.write("/src/sausage/.git/sausage-last-good.tmp", state2.good ?? "");
await fs2.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 path5 of ["/run/sausage", "/run/sausage/extensions", base, `${base}/extensions`]) await mkdir(path5);
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) {
@@ -78198,42 +78225,18 @@ async function loadExtensions(harness2, registry3, storage2, models2, context, b
};
return { host, 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 path5 of ["/run/sausage", "/run/sausage/extensions"]) {
try {
await fs2.mkdir(path5);
} catch (error) {
if (error.code !== "already_exists") throw error;
}
}
try {
await fs2.stat(snapshot);
} catch {
const staging = `${snapshot}.tmp`, archive = `${snapshot}.tar`;
try {
await fs2.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 fs2.remove(archive);
}
await fs2.rename(staging, snapshot);
}
const pathRoot = await snapshot(copy2);
const modules = [];
for (const entry2 of await fs2.list(`${snapshot}/extensions`).catch((error) => {
for (const entry2 of await fs2.list(`${pathRoot}/extensions`).catch((error) => {
if (error.code === "not_found") return [];
throw error;
})) {
if (entry2.kind !== "directory") continue;
const path5 = `${snapshot}/extensions/${entry2.name}/server.ts`;
const path5 = `${pathRoot}/extensions/${entry2.name}/server.ts`;
try {
await fs2.stat(path5);
} catch (error) {
@@ -78256,7 +78259,8 @@ async function loadExtensions(harness2, registry3, storage2, models2, context, b
}
const probe = createRegistry();
for (const extension of [...builtins, ...next.map((f) => f.extension)]) probe.install(extension);
for (const old2 of facets) registry3.uninstall(old2.extension);
const names2 = new Set(next.map((f) => f.extension.name));
for (const old2 of facets) if (!names2.has(old2.extension.name)) registry3.uninstall(old2.extension);
for (const extension of [...builtins, ...next.map((f) => f.extension)]) registry3.install(extension);
const old = facets;
facets = next;
@@ -78269,7 +78273,8 @@ async function loadExtensions(harness2, registry3, storage2, models2, context, b
facet.host.notice(`onOpen: ${String(error)}`);
}
}
state2.good = sha;
installed = true;
state2.good = copy2.hash;
return null;
} catch (error) {
await dispose(next);
@@ -78277,14 +78282,7 @@ async function loadExtensions(harness2, registry3, storage2, models2, context, b
}
};
const reload = async () => {
let tip;
try {
tip = await loaderGit("rev-parse", "refs/heads/main");
} catch (error) {
state2.error = String(error);
ops2.op_extension_status(JSON.stringify(state2));
return;
}
let good;
try {
settings = JSON.parse(await fs2.read(`${repo}/settings.json`));
} catch (error) {
@@ -78296,24 +78294,39 @@ async function loadExtensions(harness2, registry3, storage2, models2, context, b
`);
}
}
if (state2.current === tip && facets.length) return;
state2.current = tip;
state2.error = await load(tip);
if (state2.error && !facets.length && state2.good && state2.good !== tip) {
const failed = state2.error;
const fallback = await load(state2.good);
state2.error = fallback ? `${failed}
Last good: ${fallback}` : failed;
try {
const copy2 = await readCopy();
if (state2.current === copy2.hash && installed) return;
state2.current = copy2.hash;
state2.error = await load(copy2);
if (!state2.error) good = copy2;
} catch (error) {
state2.error = String(error);
}
await publish2();
if (state2.error && !installed) {
const saved = await harness2.snapshot(ExtensionFiles, context);
if (saved?.hash) {
const failed = state2.error;
const fallback = await load(saved);
state2.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: () => ({ ...state2 }), async mainAgent() {
@@ -78328,7 +78341,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;
+1
View File
@@ -23713,6 +23713,7 @@ 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 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, path2, bytes2 = []) {
const result = await ops2.op_file(kind, path2, bytes2);
+83 -70
View File
@@ -38050,6 +38050,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, path2, bytes2 = []) {
const result = await ops2.op_file(kind, path2, bytes2);
@@ -38122,28 +38123,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 path2 = `${repo}/extensions${relative}`;
const entries2 = await fs.list(path2).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 (path2) => {
try {
await fs.mkdir(path2);
} 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 path2 of ["/run/sausage", "/run/sausage/extensions", base, `${base}/extensions`]) await mkdir(path2);
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) {
@@ -38218,42 +38245,18 @@ async function loadExtensions(harness2, registry2, storage2, models2, context, b
};
return { host, 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 path2 of ["/run/sausage", "/run/sausage/extensions"]) {
try {
await fs.mkdir(path2);
} 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 path2 = `${snapshot}/extensions/${entry2.name}/server.ts`;
const path2 = `${pathRoot}/extensions/${entry2.name}/server.ts`;
try {
await fs.stat(path2);
} catch (error) {
@@ -38276,7 +38279,8 @@ async function loadExtensions(harness2, registry2, storage2, models2, context, b
}
const probe = createRegistry();
for (const extension of [...builtins, ...next.map((f) => f.extension)]) probe.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;
@@ -38289,7 +38293,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);
@@ -38297,14 +38302,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) {
@@ -38316,24 +38314,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() {
@@ -38348,7 +38361,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;
+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 entries3 = await fs.list(path).catch((error) => {
if (error.code === "not_found") return [];
throw error;
});
entries3.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
for (const entry2 of entries3) {
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 base2 = `/run/sausage/extensions/${hash}`;
for (const path of ["/run/sausage", "/run/sausage/extensions", base2, `${base2}/extensions`]) await mkdir(path);
for (const [name, bytes2] of files) {
let parent = `${base2}/extensions`;
for (const part of name.slice(1).split("/").slice(0, -1)) {
parent += `/${part}`;
await mkdir(parent);
}
await file("write", `${base2}/extensions${name}`, bytes2);
}
return base2;
};
const dispose = async (entries3) => {
for (const facet of entries3) for (const cleanup of facet.close) {
@@ -37278,42 +37305,18 @@ async function loadExtensions(harness2, registry2, storage2, models2, context, b
};
return { host, 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 probe = createRegistry();
for (const extension of [...builtins, ...next.map((f) => f.extension)]) probe.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;
+83 -70
View File
@@ -38050,6 +38050,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);
@@ -38122,28 +38123,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) {
@@ -38218,42 +38245,18 @@ async function loadExtensions(harness2, registry2, storage2, models2, context, b
};
return { host, open: open2, 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) {
@@ -38276,7 +38279,8 @@ async function loadExtensions(harness2, registry2, storage2, models2, context, b
}
const probe = createRegistry();
for (const extension of [...builtins, ...next.map((f) => f.extension)]) probe.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;
@@ -38289,7 +38293,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);
@@ -38297,14 +38302,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) {
@@ -38316,24 +38314,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() {
@@ -38348,7 +38361,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;
File diff suppressed because it is too large Load Diff
+40 -57
View File
@@ -1,18 +1,17 @@
// A world git repo exercises commit reloads, old calls, registry defaults, and recovery.
// File edits replace complete sets. Running calls and host-side recovery retain valid code.
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 } from '@earendil-works/pi-durable';
import { openStorage } from './sqlite';
import { loadExtensions, fs, git, exec } from './extensions';
import { loadExtensions, fs } from './extensions';
const assert = (value:any,message:string)=>{if(!value)throw new Error(message);};
const until = async (test:()=>Promise<boolean>|boolean)=>{const deadline=Date.now()+15000;while(!await test()){if(Date.now()>deadline)throw new Error('Reload deadline');await new Promise(r=>setTimeout(r,0));}};
const models=createModels(), faux=fauxProvider({tokensPerSecond:100000});models.setProvider(faux.provider);
const registry=createRegistry(), storage=await openStorage(globalThis.storagePath);
const harness=await Harness.open(storage,{models,registry,settings:{get extensions(){return registry.snapshot().installed();}}},context);
const root=await harness.root(context,{agent:{model:{provider:'faux',modelId:'faux-1'}}});
globalThis.shared={gate:Promise.withResolvers(),started:false,wait:false};
const markerWrite=fs.write, markerBlocked=Promise.withResolvers(), markerRelease=Promise.withResolvers();
globalThis.shared={gate:Promise.withResolvers(),started:false,wait:false,loads:[]};
const loader=await loadExtensions(harness,registry,storage,models,context);
harness.resume();
const turn = async()=> {
@@ -20,72 +19,56 @@ const turn = async()=> {
const submission=await root.submit({type:'input',content:'call demo'},context);
await submission.wait(context);
const entries=(await root.context(context)).entries;
const result=entries.filter((entry:any)=>entry.kind==='pi.tool-result').at(-1);
return JSON.stringify(result);
return JSON.stringify(entries.filter((entry:any)=>entry.kind==='pi.tool-result').at(-1));
};
try {
if(globalThis.extensionRestart) {
assert(loader.state().error,'Broken tip must report its error');
assert(await fs.read('/src/sausage/.git/sausage-last-good')===globalThis.extensionGood,'Last good must survive');
assert((await turn()).includes('result C'),'Restart must use C');
} else {
const env = await exec(['env'],{cwd:'/',env:[['EXTENSION_OPTION','yes']],conversations:['1'],timeout:3});
assert(env.code===0 && env.stdout.includes('EXTENSION_OPTION=yes'),'Exec environment');
await fs.write('/src/sausage/temporary','one');await fs.append('/src/sausage/temporary','two');
await fs.rename('/src/sausage/temporary','/src/sausage/renamed');
assert(await fs.read('/src/sausage/renamed')==='onetwo','File mutation');
assert((await fs.lstat('/src/sausage/renamed')).kind==='file','lstat');await fs.remove('/src/sausage/renamed');
const source=(version:string)=>`import {defineExtension,defineTool} from '@earendil-works/pi-durable';
const source=(version:string,paired=false)=>`import {defineExtension,defineTool} from '@earendil-works/pi-durable';
import {obj} from '@earendil-works/pi-schema';
export default host=>{if(defineTool!==globalThis.__sausage.modules['@earendil-works/pi-durable'].defineTool)throw new Error('Duplicate durable instance');host.implement({name:'demo.service'},{value:()=> '${version}'});
${paired?"import {value} from './value.ts';":""}
export default host=>{globalThis.shared.loads.push('${version}');
${paired?`if(value!=='${version}')throw new Error('Mixed file set');`:''}
if(defineTool!==globalThis.__sausage.modules['@earendil-works/pi-durable'].defineTool)throw new Error('Duplicate durable instance');
host.implement({name:'demo.service'},{value:()=> '${version}'});
globalThis.shared.handle=host.resource('handle',()=>host.need({name:'demo.service'}));
return defineExtension({name:'demo',tools:[defineTool({name:'demo',description:'Demo',parameters:obj({}),execute:async()=>{if(globalThis.shared.wait){globalThis.shared.started=true;await globalThis.shared.gate.promise;}return {output:[{type:'text',text:'result ${version}'}]};}})]});};`;
try {
if(globalThis.extensionPhase!=='edits') {
const broken=globalThis.extensionPhase==='broken';
assert(!!loader.state().error===broken,`Startup status: ${JSON.stringify(loader.state())}`);
if(broken)assert(loader.state().current!==loader.state().good,'Broken current hash must differ from last good');
else assert(loader.state().current===loader.state().good,'Offline edit must become current');
assert((await turn()).includes(`result ${globalThis.extensionExpected}`),'Startup must use the expected file copy');
assert(!!JSON.parse(await fs.read('/proc/extensions')).error===broken,'Startup error must appear in proc');
} else {
await fs.mkdir('/src/sausage/extensions');await fs.mkdir('/src/sausage/extensions/demo');
await fs.write('/src/sausage/extensions/demo/server.ts',source('A'));
await git('/src/sausage','add','.');await git('/src/sausage','commit','-m','A');
const a=await git('/src/sausage','rev-parse','HEAD');
await until(()=>loader.state().good===a);
assert((await turn()).includes('result A'),'Existing conversation must see A');
const held=globalThis.shared.handle;
assert(held.value()==='A','A service');
const retainedMethod=held.value;
await until(()=>globalThis.shared.handle?.value()==='A');
const a=loader.state().good;
assert(a?.length===64,'Version must be a SHA-256 content hash');
assert((await turn()).includes('result A'),'Existing conversation must see the file edit');
const held=globalThis.shared.handle, retainedMethod=held.value;
await fs.write('/src/sausage/extensions/demo/server.ts','syntax !!! error');
await git('/src/sausage','add','.');await git('/src/sausage','commit','-m','B');
const b=await git('/src/sausage','rev-parse','HEAD');
await until(()=>loader.state().current===b && !!loader.state().error);
assert(loader.state().good===a,'Broken B must keep A');
assert(JSON.parse(await fs.read('/proc/extensions')).error,'Status must report B');
await until(()=>loader.state().current!==a && !!loader.state().error);
assert(loader.state().good===a,'Broken edit must keep A');
assert(JSON.parse(await fs.read('/proc/extensions')).error,'Status must report the broken edit');
assert(held.value()==='A','A service remains');
assert((await turn()).includes('result A'),'A tool remains after broken B');
assert((await turn()).includes('result A'),'A tool remains after the broken edit');
globalThis.shared.wait=true;
const running=turn();await until(()=>globalThis.shared.started);
await fs.write('/src/sausage/extensions/demo/server.ts',source('C'));
await git('/src/sausage','add','.');await git('/src/sausage','commit','-m','C');
const c=await git('/src/sausage','rev-parse','HEAD');
await until(()=>loader.state().good===c);
assert(held.value()==='C','A need handle must resolve C');
// The server and its new dependency must install once, as one valid set.
await Promise.all([
fs.write('/src/sausage/extensions/demo/server.ts',source('C',true)),
fs.write('/src/sausage/extensions/demo/value.ts',"export const value='C';"),
fs.write('/src/sausage/extensions/demo/other.ts','export const unused=true;'),
]);
await until(()=>held.value()==='C');
assert(JSON.stringify(globalThis.shared.loads)==='["A","C"]','A burst must load exactly one complete set');
assert(retainedMethod()==='C','A retained method must resolve C');
globalThis.shared.wait=false;globalThis.shared.gate.resolve();
assert((await running).includes('result A'),'Old call must finish on A');
assert((await turn()).includes('result C'),'Next call must use C');
const c=loader.state().good;
await loader.reload();
// Suspend a marker write after truncation, as the asynchronous VFS write can do under load.
fs.write=async(path,text)=>{
if(path.includes('sausage-last-good')) {
fs.write=markerWrite;
await markerWrite(path,'');markerBlocked.resolve();await markerRelease.promise;
}
return markerWrite(path,text);
};
await git('/src/sausage','update-ref','refs/heads/main',b);
await until(()=>loader.state().current===b && !!loader.state().error);
await markerBlocked.promise;
const reverted=await exec(['extensions-revert'],{cwd:'/'});
markerRelease.resolve();
assert(reverted.code===0,`Revert command: ${JSON.stringify(reverted)}`);
await until(()=>loader.state().current===c && !loader.state().error);
await git('/src/sausage','update-ref','refs/heads/main',b);
await until(()=>loader.state().current===b && !!loader.state().error);
await fs.write('/src/sausage/good.txt',c);
assert(loader.state().good===c && globalThis.shared.loads.length===2,'Unchanged files must not reload');
await fs.write('/src/sausage/offline-source.txt',source('D'));
}
} finally {markerRelease.resolve();fs.write=markerWrite;globalThis.shared.gate.resolve();await loader.close();await harness.close(context);}
} finally {globalThis.shared.gate.resolve();await loader.close();await harness.close(context);}
+71 -54
View File
@@ -13,6 +13,8 @@ const ops = Deno.core.ops;
const encoder = new TextEncoder();
const decoder = new TextDecoder();
export const ExtensionState = durable.defineDoc({ kind: 'sausage.extensions', version: 1, scope: 'session', checkpointWhen: () => false, initial: () => ({good:null, current:null, error:null}) });
// The host database retains the last valid bytes independently of editable world files.
const ExtensionFiles = durable.defineDoc({kind:'sausage.extensions.files',version:1,scope:'session',history:'latest',checkpointWhen:()=>false,initial:()=>({hash:null,files:[]})});
export const Metadata = durable.defineDoc({ kind: 'sausage.metadata', version: 1, scope: 'conversation', history:'latest', fork:'current', initial:()=>({label:'',title:''}), checkpointWhen:()=>false });
export type Host = events.EventHost & {
harness(): durable.Harness; storage: Pick<durable.Storage, 'scanSubmissions'>;
@@ -84,29 +86,43 @@ export async function loadExtensions(harness:any, registry:any, storage:any, mod
let state = {...(await harness.snapshot(ExtensionState,context) ?? {good:null,current:null,error:null})};
let settings:any = {};
let active = true;
const commands = new AbortController();
// Loader commands have no wall-clock deadline. close() cancels them, even under machine load.
const loaderGit = async (...args:string[]) => {
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;
ops.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 queue = Promise.resolve();
const publish = async () => {
await harness.commit(async (tx:any)=>Object.assign(await tx.doc(ExtensionState),state),context);
type Copy = {hash:string;files:[string,number[]][]};
const publish = async (copy?:Copy) => {
await harness.commit(async (tx:any)=>{
Object.assign(await tx.doc(ExtensionState),state);
if(copy)Object.assign(await tx.doc(ExtensionFiles),copy);
},context);
ops.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 readCopy = async ():Promise<Copy> => {
const files:[string,number[]][]=[];
const walk = async (relative:string) => {
const path=`${repo}/extensions${relative}`;
const entries=await fs.list(path).catch(error=>{if(error.code==='not_found')return [];throw error;});
entries.sort((a:any,b:any)=>a.name<b.name?-1:a.name>b.name?1:0);
for(const entry of entries) {
const name=`${relative}/${entry.name}`;
if(entry.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',encoder.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:string) => {try {await fs.mkdir(path);} catch(error) {if((error as any).code!=='already_exists')throw error;}};
const snapshot = async ({hash,files}:Copy) => {
const base=`/run/sausage/extensions/${hash}`;
for(const path of ['/run/sausage','/run/sausage/extensions',base,`${base}/extensions`])await mkdir(path);
for(const [name,bytes] 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}`,bytes);
}
return base;
};
const dispose = async (entries:any[]) => { for(const facet of entries) for(const cleanup of facet.close) { try {await cleanup();} catch(error) {ops.op_output(`Extension cleanup: ${String(error)}\n`);} } };
const scope = (name:string, staged:Map<string,any>) => {
@@ -147,28 +163,15 @@ export async function loadExtensions(harness:any, registry:any, storage:any, mod
};
return {host,open,close};
};
const load = async (sha:string) => {
const load = async (copy:Copy) => {
const next:any[] = [];
const staged = new Map<string,any>();
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 as any).code!=='already_exists')throw error;} }
try {await fs.stat(snapshot);} catch {
// Only publish a complete subtree. A cancelled extraction can leave its staging directory.
const staging=`${snapshot}.tmp`, archive=`${snapshot}.tar`;
try {await fs.mkdir(staging);} catch(error) {if((error as any).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(copy);
const modules:any[] = [];
for(const entry of await fs.list(`${snapshot}/extensions`).catch(error=>{if(error.code==='not_found')return [];throw error;})) {
for(const entry of await fs.list(`${pathRoot}/extensions`).catch(error=>{if(error.code==='not_found')return [];throw error;})) {
if(entry.kind!=='directory')continue;
const path = `${snapshot}/extensions/${entry.name}/server.ts`;
const path = `${pathRoot}/extensions/${entry.name}/server.ts`;
try {await fs.stat(path);} catch(error) {if((error as any).code==='not_found')continue;throw error;}
const module = await import(/* @vite-ignore */ `file://${path}`);
modules.push({name:entry.name,module});
@@ -180,36 +183,50 @@ export async function loadExtensions(harness:any, registry:any, storage:any, mod
facet.extension = typeof module.default==='function'?await module.default(facet.host):module.default;
if(!facet.extension || facet.extension.name!==name)throw new Error(`Extension name must match ${name}`);
}
// Validate the complete commit before the live registry changes.
// Validate the complete file set before the live registry changes.
const probe = durable.createRegistry();
for(const extension of [...builtins,...next.map(f=>f.extension)])probe.install(extension);
for(const old of facets)registry.uninstall(old.extension);
const names=new Set(next.map(f=>f.extension.name));
for(const old of facets)if(!names.has(old.extension.name))registry.uninstall(old.extension);
for(const extension of [...builtins,...next.map(f=>f.extension)])registry.install(extension);
const old = facets; facets=next; providers=staged;
await dispose(old);
for(const facet of next)for(const cb of facet.open) {try {await cb();} catch(error) {facet.host.notice(`onOpen: ${String(error)}`);} }
state.good=sha;
installed=true;
state.good=copy.hash;
return null;
} catch(error) {await dispose(next); return String(error);}
};
const reload = async () => {
let tip:string;
try {tip=await loaderGit('rev-parse','refs/heads/main');} catch(error) {state.error=String(error);ops.op_extension_status(JSON.stringify(state));return;}
let good:Copy|undefined;
try {settings=JSON.parse(await fs.read(`${repo}/settings.json`));} catch(error) {if((error as any).code==='not_found')settings={};else {state.notice=`Settings: ${String(error)}`;ops.op_extension_status(JSON.stringify(state));ops.op_output(`${state.notice}\n`);}}
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}\nLast good: ${fallback}`:failed;
try {
const copy=await readCopy();
if(state.current===copy.hash && installed)return;
state.current=copy.hash;
state.error=await load(copy);
if(!state.error)good=copy;
} catch(error) {state.error=String(error);}
if(state.error && !installed) {
const saved=await harness.snapshot(ExtensionFiles,context);
if(saved?.hash) {
const failed=state.error;
const fallback=await load(saved);
state.error=fallback?`${failed}\nLast good: ${fallback}`:failed;
}
}
await publish();
await publish(good);
};
const enqueue = () => queue=queue.then(reload);
const rid = ops.op_watch([`${repo}/.git/refs/heads/main`,`${repo}/.git/packed-refs`,`${repo}/settings.json`]);
const rid = ops.op_watch([`${repo}/extensions`,`${repo}/settings.json`]);
let timer:ReturnType<typeof setTimeout>|undefined;
await enqueue();
const pending = (async()=> {while(active){await ops.op_watch_next(rid);if(active)await enqueue();}})();
const pending = (async()=> {while(active){
const event=await ops.op_watch_next(rid);
if(!event || !active)break;
clearTimeout(timer);
timer=setTimeout(()=>{timer=undefined;void enqueue();},75);
}})();
return {reload:enqueue, state:()=>({...state}), async mainAgent() {
if (!providers.has('models') || !providers.has('subagents')) return undefined;
const {main_agent_name:name}=await providers.get('models').discover(repo);
@@ -220,5 +237,5 @@ export async function loadExtensions(harness:any, registry:any, storage:any, mod
const action = facet?.actions?.[key];
if (!action) throw Object.assign(new Error('Unknown user action'),{status:404});
return action(args,{host:facet.host,signal,conversationId,source:'ui'});
}, close:async()=>{active=false;commands.abort();Deno.core.close(rid);await pending;await queue;await dispose(facets);}};
}, close:async()=>{active=false;clearTimeout(timer);Deno.core.close(rid);await pending;await queue;await dispose(facets);}};
}
+84 -71
View File
@@ -39914,28 +39914,54 @@ async function loadExtensions(harness, registry, storage, models, context, built
let state = { ...await harness.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 harness.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 harness.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) {
@@ -40010,42 +40036,18 @@ async function loadExtensions(harness, registry, storage, models, context, built
};
return { host, open, close };
};
const load = async (sha) => {
const load = async (copy2) => {
const next2 = [];
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) {
@@ -40068,7 +40070,8 @@ async function loadExtensions(harness, registry, storage, models, context, built
}
const probe = createRegistry();
for (const extension of [...builtins, ...next2.map((f) => f.extension)]) probe.install(extension);
for (const old2 of facets) registry.uninstall(old2.extension);
const names2 = new Set(next2.map((f) => f.extension.name));
for (const old2 of facets) if (!names2.has(old2.extension.name)) registry.uninstall(old2.extension);
for (const extension of [...builtins, ...next2.map((f) => f.extension)]) registry.install(extension);
const old = facets;
facets = next2;
@@ -40081,7 +40084,8 @@ async function loadExtensions(harness, registry, storage, models, context, built
facet.host.notice(`onOpen: ${String(error)}`);
}
}
state.good = sha;
installed = true;
state.good = copy2.hash;
return null;
} catch (error) {
await dispose(next2);
@@ -40089,14 +40093,7 @@ async function loadExtensions(harness, registry, storage, models, context, built
}
};
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) {
@@ -40108,24 +40105,39 @@ async function loadExtensions(harness, registry, storage, models, context, built
`);
}
}
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 harness.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() {
@@ -40140,14 +40152,14 @@ 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;
await dispose(facets);
} };
}
var ops2, encoder4, decoder4, ExtensionState, Metadata, fs;
var ops2, encoder4, decoder4, ExtensionState, ExtensionFiles, Metadata, fs;
var init_extensions = __esm({
"extensions.ts"() {
init_dist4();
@@ -40164,6 +40176,7 @@ var init_extensions = __esm({
encoder4 = new TextEncoder();
decoder4 = new TextDecoder();
ExtensionState = defineDoc({ kind: "sausage.extensions", version: 1, scope: "session", checkpointWhen: () => false, initial: () => ({ good: null, current: null, error: null }) });
ExtensionFiles = defineDoc({ kind: "sausage.extensions.files", version: 1, scope: "session", history: "latest", checkpointWhen: () => false, initial: () => ({ hash: null, files: [] }) });
Metadata = defineDoc({ kind: "sausage.metadata", version: 1, scope: "conversation", history: "latest", fork: "current", initial: () => ({ label: "", title: "" }), checkpointWhen: () => false });
fs = {
read: async (path) => decoder4.decode(new Uint8Array(await file("read", path))),
+83 -70
View File
@@ -38050,6 +38050,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);
@@ -38122,28 +38123,54 @@ async function loadExtensions(harness2, registry, storage, models, context, buil
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) {
@@ -38218,42 +38245,18 @@ async function loadExtensions(harness2, registry, storage, models, context, buil
};
return { host, open, close };
};
const load = async (sha) => {
const load = async (copy2) => {
const next2 = [];
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) {
@@ -38276,7 +38279,8 @@ async function loadExtensions(harness2, registry, storage, models, context, buil
}
const probe = createRegistry();
for (const extension of [...builtins, ...next2.map((f2) => f2.extension)]) probe.install(extension);
for (const old2 of facets) registry.uninstall(old2.extension);
const names2 = new Set(next2.map((f2) => f2.extension.name));
for (const old2 of facets) if (!names2.has(old2.extension.name)) registry.uninstall(old2.extension);
for (const extension of [...builtins, ...next2.map((f2) => f2.extension)]) registry.install(extension);
const old = facets;
facets = next2;
@@ -38289,7 +38293,8 @@ async function loadExtensions(harness2, registry, storage, models, context, buil
facet.host.notice(`onOpen: ${String(error)}`);
}
}
state.good = sha;
installed = true;
state.good = copy2.hash;
return null;
} catch (error) {
await dispose(next2);
@@ -38297,14 +38302,7 @@ async function loadExtensions(harness2, registry, storage, models, context, buil
}
};
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) {
@@ -38316,24 +38314,39 @@ async function loadExtensions(harness2, registry, storage, models, context, buil
`);
}
}
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() {
@@ -38348,7 +38361,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;
+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 entries3 = await fs.list(path).catch((error) => {
if (error.code === "not_found") return [];
throw error;
});
entries3.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
for (const entry2 of entries3) {
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 snapshot2 = async ({ hash, files }) => {
const base2 = `/run/sausage/extensions/${hash}`;
for (const path of ["/run/sausage", "/run/sausage/extensions", base2, `${base2}/extensions`]) await mkdir(path);
for (const [name, bytes2] of files) {
let parent = `${base2}/extensions`;
for (const part of name.slice(1).split("/").slice(0, -1)) {
parent += `/${part}`;
await mkdir(parent);
}
await file("write", `${base2}/extensions${name}`, bytes2);
}
return base2;
};
const dispose = async (entries3) => {
for (const facet of entries3) for (const cleanup of facet.close) {
@@ -37278,42 +37305,18 @@ async function loadExtensions(harness2, registry2, storage2, models2, context, b
};
return { host, open, close };
};
const load = async (sha) => {
const load = async (copy2) => {
const next = [];
const staged = /* @__PURE__ */ new Map();
try {
const snapshot2 = `/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(snapshot2);
} catch {
const staging = `${snapshot2}.tmp`, archive = `${snapshot2}.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, snapshot2);
}
const pathRoot = await snapshot2(copy2);
const modules = [];
for (const entry2 of await fs.list(`${snapshot2}/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 = `${snapshot2}/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 probe = createRegistry();
for (const extension of [...builtins, ...next.map((f) => f.extension)]) probe.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;
+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(harness, registry, storage, models, context, built
let state = { ...await harness.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 harness.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 harness.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(harness, registry, storage, models, context, built
};
return { host, 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(harness, registry, storage, models, context, built
}
const probe = createRegistry();
for (const extension of [...builtins, ...next.map((f) => f.extension)]) probe.install(extension);
for (const old2 of facets) registry.uninstall(old2.extension);
const names2 = new Set(next.map((f) => f.extension.name));
for (const old2 of facets) if (!names2.has(old2.extension.name)) registry.uninstall(old2.extension);
for (const extension of [...builtins, ...next.map((f) => f.extension)]) registry.install(extension);
const old = facets;
facets = next;
@@ -37349,7 +37353,8 @@ async function loadExtensions(harness, registry, storage, models, context, built
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(harness, registry, storage, models, context, built
}
};
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(harness, registry, storage, models, context, built
`);
}
}
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 harness.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;
+83 -70
View File
@@ -38050,6 +38050,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);
@@ -38122,28 +38123,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 base2 = `/run/sausage/extensions/${hash}`;
for (const path of ["/run/sausage", "/run/sausage/extensions", base2, `${base2}/extensions`]) await mkdir(path);
for (const [name, bytes2] of files) {
let parent = `${base2}/extensions`;
for (const part of name.slice(1).split("/").slice(0, -1)) {
parent += `/${part}`;
await mkdir(parent);
}
await file("write", `${base2}/extensions${name}`, bytes2);
}
return base2;
};
const dispose = async (entries2) => {
for (const facet of entries2) for (const cleanup of facet.close) {
@@ -38218,42 +38245,18 @@ async function loadExtensions(harness2, registry2, storage2, models2, context, b
};
return { host, 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) {
@@ -38276,7 +38279,8 @@ async function loadExtensions(harness2, registry2, storage2, models2, context, b
}
const probe = createRegistry();
for (const extension of [...builtins, ...next.map((f) => f.extension)]) probe.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;
@@ -38289,7 +38293,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);
@@ -38297,14 +38302,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) {
@@ -38316,24 +38314,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() {
@@ -38348,7 +38361,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;
+1
View File
@@ -67033,6 +67033,7 @@ 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 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, path5, bytes2 = []) {
const result = await ops2.op_file(kind, path5, bytes2);
+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, path2, bytes2 = []) {
const result = await ops2.op_file(kind, path2, 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 path2 = `${repo}/extensions${relative}`;
const entries2 = await fs.list(path2).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 (path2) => {
try {
await fs.mkdir(path2);
} 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 snapshot2 = async ({ hash, files }) => {
const base = `/run/sausage/extensions/${hash}`;
for (const path2 of ["/run/sausage", "/run/sausage/extensions", base, `${base}/extensions`]) await mkdir(path2);
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, open, close };
};
const load = async (sha) => {
const load = async (copy2) => {
const next = [];
const staged = /* @__PURE__ */ new Map();
try {
const snapshot2 = `/run/sausage/extensions/${sha}`;
for (const path2 of ["/run/sausage", "/run/sausage/extensions"]) {
try {
await fs.mkdir(path2);
} catch (error) {
if (error.code !== "already_exists") throw error;
}
}
try {
await fs.stat(snapshot2);
} catch {
const staging = `${snapshot2}.tmp`, archive = `${snapshot2}.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, snapshot2);
}
const pathRoot = await snapshot2(copy2);
const modules = [];
for (const entry2 of await fs.list(`${snapshot2}/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 path2 = `${snapshot2}/extensions/${entry2.name}/server.ts`;
const path2 = `${pathRoot}/extensions/${entry2.name}/server.ts`;
try {
await fs.stat(path2);
} catch (error) {
@@ -37336,7 +37339,8 @@ async function loadExtensions(harness2, registry2, storage2, models2, context, b
}
const probe = createRegistry();
for (const extension of [...builtins, ...next.map((f) => f.extension)]) probe.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;
+1
View File
@@ -24632,6 +24632,7 @@ 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 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 exec(command, options) {
options.signal?.throwIfAborted();
+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 base2 = `/run/sausage/extensions/${hash}`;
for (const path of ["/run/sausage", "/run/sausage/extensions", base2, `${base2}/extensions`]) await mkdir(path);
for (const [name, bytes2] of files) {
let parent = `${base2}/extensions`;
for (const part of name.slice(1).split("/").slice(0, -1)) {
parent += `/${part}`;
await mkdir(parent);
}
await file("write", `${base2}/extensions${name}`, bytes2);
}
return base2;
};
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, open, close };
};
const load = async (sha2) => {
const load = async (copy2) => {
const next = [];
const staged = /* @__PURE__ */ new Map();
try {
const snapshot = `/run/sausage/extensions/${sha2}`;
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", sha2, "extensions")) {
await loaderGit("archive", "--format=tar", `--output=${archive}`, sha2, "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 probe = createRegistry();
for (const extension of [...builtins, ...next.map((f) => f.extension)]) probe.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 = sha2;
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;
+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;
+3 -39
View File
@@ -44,7 +44,7 @@ fn copy_tree(source: &Path, destination: &Path) -> std::io::Result<()> {
}
impl World {
/// Initialize a fresh world and commit its plain bundled source on main.
/// Initialize a fresh world with plain bundled source files, without a Git repository.
pub async fn init(dir: &Path, bundle: &Path) -> anyhow::Result<Self> {
Self::init_with_progress(dir, bundle, |_| {}).await
}
@@ -60,44 +60,8 @@ impl World {
"Create world folder failed: World already exists: {}",
dir.display()
);
let world = Self::open_with_progress(dir, Some(bundle), &mut progress)
.context("Create world folder and seed files failed")?;
progress("Create the first commit in /src/sausage");
let exec = crate::process::exec(Self {
root: world.root.clone(),
host_dir: world.host_dir.clone(),
processes: world.processes.clone(),
})
.context("Create source repository failed")?;
for args in [
vec!["init", "-b", "main"],
vec!["add", "."],
vec!["commit", "--no-gpg-sign", "-m", "Initial world source"],
] {
let argv = [
vec!["/bin/git.wasm".to_owned()],
args.into_iter().map(str::to_owned).collect(),
]
.concat();
let mut output = Vec::new();
let code = exec
.exec(
crate::exec::Command::Argv(argv),
crate::exec::Options {
cwd: "/src/sausage".into(),
..Default::default()
},
&mut |_, bytes| output.extend_from_slice(bytes),
)
.await
.context("Create source repository failed")?;
anyhow::ensure!(
code == 0,
"Cannot initialize source repository: {}",
String::from_utf8_lossy(&output)
);
}
Ok(world)
Self::open_with_progress(dir, Some(bundle), &mut progress)
.context("Create world folder and seed files failed")
}
/// Open (and create if missing) the world directory `dir`, including `/proc` and `/run` mounts.
+14 -28
View File
@@ -45,15 +45,9 @@ fn write(root: &Path, path: &str, text: &str) -> anyhow::Result<()> {
Ok(())
}
#[tokio::test(flavor = "current_thread")]
async fn init_seeds_config_tools_and_one_source_commit() -> anyhow::Result<()> {
async fn init_seeds_config_tools_and_plain_source() -> anyhow::Result<()> {
let dir = tempfile::tempdir()?;
let bundle = dir.path().join("bundle");
let target = crate::common::build_git();
std::fs::create_dir_all(bundle.join("bin"))?;
std::fs::copy(
target.join("wasm32-wasip2/release/git.wasm"),
bundle.join("bin/git.wasm"),
)?;
write(&bundle, "bin/unused.wasm", "not a component")?;
write(&bundle, "src/sausage/README.md", "World source")?;
write(
@@ -84,14 +78,13 @@ async fn init_seeds_config_tools_and_one_source_commit() -> anyhow::Result<()> {
lines,
[
format!("Create world folder: {}", destination.display()),
"Copy tools: 2".into(),
"Copy tools: 1".into(),
"Create /src/sausage".into(),
"Seed /etc and /srv/www".into(),
format!(
"Write config: {}",
destination.join("world/etc/sausage/config.yaml").display()
),
"Create the first commit in /src/sausage".into(),
"Next steps:".into(),
format!(
" Edit {} to configure providers and API keys.",
@@ -106,7 +99,7 @@ async fn init_seeds_config_tools_and_one_source_commit() -> anyhow::Result<()> {
"World page"
);
let world = World::open(&destination)?;
assert!(destination.join("world/bin/git").exists());
assert!(destination.join("world/bin/unused").exists());
let config = world
.root
.open("/etc/sausage/config.yaml", Default::default())
@@ -116,21 +109,11 @@ async fn init_seeds_config_tools_and_one_source_commit() -> anyhow::Result<()> {
assert!(!config.contains("fixture-key"));
assert_eq!(config, include_str!("../../../config/config.yaml"));
let source = destination.join("world/src/sausage");
for (args, expected) in [
(vec!["symbolic-ref", "HEAD"], "refs/heads/main"),
(vec!["rev-list", "--count", "HEAD"], "1"),
] {
let result = std::process::Command::new("git")
.args(args)
.current_dir(&source)
.output()?;
assert!(
result.status.success(),
"{}",
String::from_utf8_lossy(&result.stderr)
);
assert_eq!(String::from_utf8(result.stdout)?.trim(), expected);
}
assert!(!source.join(".git").exists());
assert_eq!(
std::fs::read_to_string(source.join("README.md"))?,
"World source"
);
assert!(
World::init(&destination, &bundle)
.await
@@ -148,11 +131,11 @@ fn init_errors_name_the_failed_step() -> anyhow::Result<()> {
let bundle = dir.path().join("bundle");
for (name, expected) in [
("missing", "Copy tools failed"),
("invalid", "Create source repository failed"),
("invalid", "Create /src/sausage failed"),
] {
if name == "invalid" {
write(&bundle, "bin/git.wasm", "not a component")?;
write(&bundle, "src/sausage/README.md", "Source")?;
write(&bundle, "src", "not a directory")?;
}
let output = std::process::Command::new(env!("CARGO_BIN_EXE_sausage"))
.arg("init")
@@ -212,7 +195,10 @@ async fn core_extensions() -> anyhow::Result<()> {
"etc/sausage/config.yaml",
include_str!("../../../config/config.yaml"),
)?;
copy_tree(&repo.join("config/roster"), &disk.join("etc/sausage/roster"))?;
copy_tree(
&repo.join("config/roster"),
&disk.join("etc/sausage/roster"),
)?;
write(
&source,
"skills/builtin/SKILL.md",
+27 -253
View File
@@ -1,272 +1,46 @@
//! Commit snapshots and last-good recovery with world git and a faux model.
//! File reloads, immutable calls, debounce, offline edits, and host-side recovery without subprocesses.
use sausage::{
exec::{Command, Exec, Options, Stream},
world::World,
};
use std::{path::Path, sync::Arc};
use std::sync::Arc;
const BUNDLE: &str = include_str!("../../js/extensions-test-bundle.js");
struct Author(Arc<dyn Exec>);
struct NoExec;
#[async_trait::async_trait]
impl Exec for Author {
impl Exec for NoExec {
async fn exec(
&self,
command: Command,
mut options: Options,
output: &mut (dyn for<'a> FnMut(Stream, &'a [u8]) + Send),
_: Options,
_: &mut (dyn for<'a> FnMut(Stream, &'a [u8]) + Send),
) -> anyhow::Result<i32> {
for (key, value) in [
("GIT_AUTHOR_NAME", "Test"),
("GIT_AUTHOR_EMAIL", "test@example.com"),
("GIT_COMMITTER_NAME", "Test"),
("GIT_COMMITTER_EMAIL", "test@example.com"),
] {
options.env.push((key.into(), value.into()));
}
let label = format!("{command:?}");
let budget = options.timeout;
let started = std::time::Instant::now();
let result = self.0.exec(command, options, output).await;
eprintln!(
"Extension {label}: deadline {budget:?}, elapsed {:?}, result {result:?}",
started.elapsed()
);
result
anyhow::bail!("Extension loading must not spawn commands: {command:?}")
}
}
#[tokio::test(flavor = "current_thread")]
async fn commits_and_restart() -> anyhow::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() {
crate::common::cwd_init();
}
assert!(
std::process::Command::new("cargo")
.args([
"build",
"--release",
"-p",
"git",
"-p",
"sausage-exec-tools",
"-p",
"sausage-archive-tools",
"--manifest-path"
])
.arg(userland.join("../Cargo.toml"))
.args(["--target", "wasm32-wasip2", "--target-dir"])
.arg(target)
.args(["-j", "2", "--locked"])
.current_dir(&userland)
.status()?
.success()
);
async fn edits_and_restart() -> anyhow::Result<()> {
let dir = tempfile::tempdir()?;
let world = World::open(dir.path())?;
let disk = dir.path().join("world");
std::fs::create_dir_all(disk.join("bin"))?;
std::fs::copy(
target.join("wasm32-wasip2/release/git.wasm"),
disk.join("bin/git.wasm"),
)?;
for name in ["extensions-revert", "env", "tar"] {
std::fs::copy(
target.join(format!("wasm32-wasip2/release/{name}.wasm")),
disk.join(format!("bin/{name}.wasm")),
)?;
}
let repo = disk.join("src/sausage");
let repo = dir.path().join("world/src/sausage");
std::fs::create_dir_all(&repo)?;
assert!(
std::process::Command::new("git")
.args(["init", "-b", "main"])
.arg(&repo)
.status()?
.success()
);
assert!(
std::process::Command::new("git")
.args([
"-c",
"user.name=Test",
"-c",
"user.email=test@example.com",
"commit",
"--no-gpg-sign",
"--allow-empty",
"-m",
"empty"
])
.current_dir(&repo)
.status()?
.success()
);
let exec: Arc<dyn Exec> = Arc::new(Author(sausage::process::exec(World {
root: world.root.clone(),
host_dir: world.host_dir.clone(),
processes: world.processes.clone(),
})?));
sausage::harness::run_module(world, exec, BUNDLE).await?;
let good = std::fs::read_to_string(repo.join("good.txt"))?;
let world = World::open(dir.path())?;
let exec: Arc<dyn Exec> = Arc::new(Author(sausage::process::exec(World {
root: world.root.clone(),
host_dir: world.host_dir.clone(),
processes: world.processes.clone(),
})?));
sausage::harness::run_module(
world,
exec,
&format!("globalThis.extensionRestart=true;globalThis.extensionGood={good:?};\n{BUNDLE}"),
)
.await?;
Ok(())
}
/// Model loader commands without delays, and require cancellation instead of a wall-clock deadline.
struct LoaderCommand {
root: Arc<dyn sausage_vfs::Layer>,
failures: usize,
attempts: std::sync::atomic::AtomicUsize,
timeout: bool,
}
#[async_trait::async_trait]
impl Exec for LoaderCommand {
async fn exec(
&self,
command: Command,
options: Options,
output: &mut (dyn for<'a> FnMut(Stream, &'a [u8]) + Send),
) -> anyhow::Result<i32> {
let Command::Argv(args) = command else {
anyhow::bail!("unexpected shell")
};
assert!(
options.timeout.is_none(),
"Loader work must not have a wall-clock deadline"
);
if args[0] == "tar" {
return Ok(0);
for (phase, expected) in [
("edits", "C"),
("broken", "C"),
("offline", "D"),
("broken", "D"),
] {
if phase == "offline" {
std::fs::copy(
repo.join("offline-source.txt"),
repo.join("extensions/demo/server.ts"),
)?;
} else if phase == "broken" {
std::fs::write(repo.join("extensions/demo/server.ts"), "syntax !!! error")?;
}
match args[1].as_str() {
"rev-parse" => output(Stream::Stdout, b"new\n"),
"ls-tree" => output(Stream::Stdout, b"extensions\n"),
"archive" => {
let attempt = self
.attempts
.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
if attempt < self.failures && self.timeout {
anyhow::bail!("command timed out");
}
if attempt < self.failures {
anyhow::bail!("checkout failed");
}
if attempt > 0 {
let status = self
.root
.open("/proc/extensions", Default::default())
.await?;
let status = String::from_utf8(status.read(0, 65536).await?)?;
assert!(
status.contains("command timed out"),
"Retry must publish its error: {status}"
);
}
let archive = args[3].strip_prefix("--output=").unwrap();
let file = self
.root
.open(
archive,
sausage_vfs::Open {
write: true,
create: true,
..Default::default()
},
)
.await?;
file.write(0, vec![]).await?;
let snapshot = format!("{}.tmp", archive.strip_suffix(".tar").unwrap());
for path in [
format!("{snapshot}/extensions"),
format!("{snapshot}/extensions/demo"),
] {
self.root.mkdir(&path).await?;
}
let file = self
.root
.open(
&format!("{snapshot}/extensions/demo/server.ts"),
sausage_vfs::Open {
write: true,
create: true,
..Default::default()
},
)
.await?;
file.write(0, b"export default {name:'demo'};".to_vec())
.await?;
}
_ => anyhow::bail!("unexpected command {args:?}"),
}
Ok(0)
let world = World::open(dir.path())?;
sausage::harness::run_module(world, Arc::new(NoExec), &format!(
"globalThis.extensionPhase={phase:?};globalThis.extensionExpected={expected:?};\n{BUNDLE}"
)).await?;
assert!(!repo.join(".git").exists());
}
}
async fn loader_command(failures: usize, timeout: bool) -> anyhow::Result<()> {
let dir = tempfile::tempdir()?;
let world = World::open(dir.path())?;
std::fs::create_dir_all(dir.path().join("world/src/sausage/.git"))?;
let exec = Arc::new(LoaderCommand {
root: world.root.clone(),
failures,
timeout,
attempts: std::sync::atomic::AtomicUsize::new(0),
});
let (library, _) = include_str!("../../js/bundle.js")
.split_once("// main.ts")
.unwrap();
let expected = if failures > 1 || !timeout && failures > 0 {
"old"
} else {
"new"
};
let source = format!(
r#"{library}
const testRegistry = createRegistry();
const testStorage = await openStorage(globalThis.storagePath);
const testHarness = await Harness.open(testStorage, {{models:createModels(),registry:testRegistry}}, BACKGROUND_CONTEXT);
await testHarness.root(BACKGROUND_CONTEXT);
await testHarness.commit(async tx => Object.assign(await tx.doc(ExtensionState), {{good:'old',current:'old',error:null}}), BACKGROUND_CONTEXT);
for (const path of ['/run/sausage','/run/sausage/extensions','/run/sausage/extensions/old','/run/sausage/extensions/old/extensions','/run/sausage/extensions/old/extensions/demo']) await fs2.mkdir(path);
await fs2.write('/run/sausage/extensions/old/extensions/demo/server.ts', "export default {{name:'demo'}};");
const testLoader = await loadExtensions(testHarness,testRegistry,testStorage,createModels(),BACKGROUND_CONTEXT);
const testState = testLoader.state();
if(testState.good !== '{expected}') throw new Error(JSON.stringify(testState));
if((testState.error !== null) !== ({expected:?} === 'old')) throw new Error(JSON.stringify(testState));
if(testState.error && !testState.error.includes('git archive')) throw new Error(testState.error);
await testLoader.close();
await testHarness.close(BACKGROUND_CONTEXT);
"#
);
sausage::harness::run_module(world, exec.clone(), &source).await?;
assert_eq!(
exec.attempts.load(std::sync::atomic::Ordering::SeqCst),
if timeout && failures > 0 { 2 } else { 1 }
);
Ok(())
}
#[tokio::test(flavor = "current_thread")]
async fn slow_loader_command() -> anyhow::Result<()> {
loader_command(0, true).await
}
#[tokio::test(flavor = "current_thread")]
async fn loader_timeout_retries_once() -> anyhow::Result<()> {
loader_command(1, true).await
}
#[tokio::test(flavor = "current_thread")]
async fn loader_timeout_keeps_last_good() -> anyhow::Result<()> {
loader_command(2, true).await
}
#[tokio::test(flavor = "current_thread")]
async fn loader_real_failure_is_not_retried() -> anyhow::Result<()> {
loader_command(1, false).await
}
+11 -51
View File
@@ -1,4 +1,4 @@
//! P1 integration: bundle contents, source commits, extension loading, and real shell calls.
//! P1 integration: plain bundled source, extension file snapshots, and real shell calls.
use sausage::{harness::run_module, world::World};
use sausage_vfs::Open;
use std::path::PathBuf;
@@ -46,21 +46,9 @@ async fn bundle_copy_and_real_shell() -> anyhow::Result<()> {
] {
assert!(snapshot.join(path).is_file(), "missing bundled {path}");
}
let git = |path: &std::path::Path, args: &[&str]| -> anyhow::Result<String> {
let output = std::process::Command::new("git")
.arg("-C")
.arg(path)
.args(args)
.output()?;
anyhow::ensure!(
output.status.success(),
"git {args:?}: {}",
String::from_utf8_lossy(&output.stderr)
);
Ok(String::from_utf8(output.stdout)?.trim().to_owned())
};
let message = git(&snapshot, &["log", "-1", "--format=%s"])?;
assert_eq!(message, "Initial world source");
assert!(!snapshot.join(".git").exists());
assert!(!dir.path().join("world/bin/extensions-revert").exists());
assert!(!dir.path().join("world/bin/extensions-revert.wasm").exists());
for path in ["config", "skills", "extensions"] {
let difference = std::process::Command::new("git")
.args(["diff", "--no-index", "--exit-code", "--"])
@@ -189,38 +177,6 @@ async fn bundle_copy_and_real_shell() -> anyhow::Result<()> {
assert_eq!(out, native.stdout, "{script}");
}
}
let mut history = Vec::new();
assert_eq!(
exec.exec(
Command::Shell("cd /src/sausage && git log --oneline".into()),
Options::default(),
&mut |stream, bytes| {
if matches!(stream, Stream::Stdout) {
history.extend_from_slice(bytes);
}
},
)
.await?,
0,
);
let history = String::from_utf8(history)?;
assert_eq!(history.lines().count(), 1);
assert!(history.contains("Initial world source"));
let mut status = Vec::new();
assert_eq!(
exec.exec(
Command::Shell("cd /src/sausage && git status --short".into()),
Options::default(),
&mut |stream, bytes| {
if matches!(stream, Stream::Stdout) {
status.extend_from_slice(bytes);
}
},
)
.await?,
0,
);
assert!(status.is_empty(), "{}", String::from_utf8_lossy(&status));
let source = format!(
"globalThis.testP1 = true;\n{}",
include_str!("../../js/test-bundle.js")
@@ -355,9 +311,13 @@ async fn bundled_extensions_load() -> anyhow::Result<()> {
run_module(world, std::sync::Arc::new(LoaderTiming(exec)), &source).await?;
let status = root.open("/proc/extensions", Open::default()).await?;
let status: serde_json::Value = serde_json::from_slice(&status.read(0, 65536).await?)?;
let commit =
std::fs::read_to_string(dir.path().join("world/src/sausage/.git/refs/heads/main"))?;
assert_eq!(status["good"].as_str(), Some(commit.trim()), "{status}");
let hash = status["good"].as_str().expect("extension content hash");
assert_eq!(hash.len(), 64, "{status}");
assert!(
hash.bytes().all(|byte| byte.is_ascii_hexdigit()),
"{status}"
);
assert_eq!(status["good"], status["current"], "{status}");
assert!(status["error"].is_null(), "{status}");
Ok(())
}
-4
View File
@@ -15,9 +15,5 @@ path = "src/which.rs"
name = "timeout"
path = "src/timeout.rs"
[[bin]]
name = "extensions-revert"
path = "src/revert.rs"
[dependencies]
sausage-spawn = { path = "../spawn" }
-25
View File
@@ -1,25 +0,0 @@
//! Move the world's extension branch to its last successful commit.
fn run() -> std::io::Result<i32> {
let good = std::fs::read_to_string("/src/sausage/.git/sausage-last-good")?;
if good.trim().is_empty() {
return Err(std::io::Error::other("No successful extension commit"));
}
sausage_spawn::Command::new("git")
.args([
"-C",
"/src/sausage",
"update-ref",
"refs/heads/main",
good.trim(),
])
.status()
}
fn main() {
std::process::exit(match run() {
Ok(code) => code,
Err(error) => {
eprintln!("extensions-revert: {error}");
1
}
});
}