diff --git a/.changeset/describe-compile-scope.md b/.changeset/describe-compile-scope.md
new file mode 100644
index 0000000000..a57cd5853c
--- /dev/null
+++ b/.changeset/describe-compile-scope.md
@@ -0,0 +1,5 @@
+---
+"executor": patch
+---
+
+Compile `describe.tool` previews against only the definitions a tool references, drop the compiler's per-call retained graph, and fall back to `unknown` for schemas over a node limit. Describing a tool from a large OpenAPI spec no longer burns seconds of CPU or leaks memory in the shared session isolate.
diff --git a/.changeset/lost-execution-visibility.md b/.changeset/lost-execution-visibility.md
new file mode 100644
index 0000000000..28e783ab13
--- /dev/null
+++ b/.changeset/lost-execution-visibility.md
@@ -0,0 +1,5 @@
+---
+"executor": patch
+---
+
+Report an MCP `execute` call that dies with a session reset as a JSON-RPC error instead of a silently closed stream. The front worker answers outstanding request ids when the session socket closes abnormally or a response deadline passes, and a rebuilt session answers ids stranded by a previous incarnation on the next stream. The plain memory-limit reset is now classified as transient.
diff --git a/.changeset/openapi-transport-unreachable.md b/.changeset/openapi-transport-unreachable.md
new file mode 100644
index 0000000000..8921b9b31f
--- /dev/null
+++ b/.changeset/openapi-transport-unreachable.md
@@ -0,0 +1,6 @@
+---
+"executor": patch
+"@executor-js/plugin-openapi": patch
+---
+
+OpenAPI tools that cannot reach the upstream server now return an `upstream_unreachable` error with an actionable network message instead of `Internal tool error [id]`.
diff --git a/.changeset/openapi-vendor-json-content-type.md b/.changeset/openapi-vendor-json-content-type.md
new file mode 100644
index 0000000000..24c4da544e
--- /dev/null
+++ b/.changeset/openapi-vendor-json-content-type.md
@@ -0,0 +1,5 @@
+---
+"@executor-js/plugin-openapi": patch
+---
+
+Preserve vendor +json Content-Type on OpenAPI object request bodies.
diff --git a/.changeset/self-hosted-fonts.md b/.changeset/self-hosted-fonts.md
new file mode 100644
index 0000000000..4d8e025451
--- /dev/null
+++ b/.changeset/self-hosted-fonts.md
@@ -0,0 +1,5 @@
+---
+"executor": patch
+---
+
+Bundle the Geist and Geist Mono fonts with the console instead of loading them from Google Fonts. The UI no longer stays blank when a self-hosted deployment cannot reach fonts.googleapis.com.
diff --git a/.changeset/toolkits-empty-grid.md b/.changeset/toolkits-empty-grid.md
new file mode 100644
index 0000000000..fa868f391f
--- /dev/null
+++ b/.changeset/toolkits-empty-grid.md
@@ -0,0 +1,5 @@
+---
+"executor": patch
+---
+
+Prevent the empty Toolkits page from scrolling past its visible add cards.
diff --git a/apps/cloud/src/mcp/session-build-semaphore.test.ts b/apps/cloud/src/mcp/session-build-semaphore.test.ts
index 3d4ad76343..584b65ee0e 100644
--- a/apps/cloud/src/mcp/session-build-semaphore.test.ts
+++ b/apps/cloud/src/mcp/session-build-semaphore.test.ts
@@ -1,4 +1,4 @@
-import { describe, expect, it, beforeEach } from "@effect/vitest";
+import { describe, expect, it, beforeEach, afterEach, vi } from "@effect/vitest";
import {
acquireBuildSlot,
@@ -13,6 +13,10 @@ describe("session-build-semaphore", () => {
resetBuildSlotsForTest();
});
+ afterEach(() => {
+ vi.useRealTimers();
+ });
+
it("grants up to the cap immediately, with no wait", async () => {
const results = await Promise.all([
acquireBuildSlot().promise,
@@ -214,6 +218,7 @@ describe("session-build-semaphore", () => {
});
it("proceeds without a slot when the queue wait exceeds the timeout, and does not count it as active", async () => {
+ vi.useFakeTimers();
await Promise.all([
acquireBuildSlot().promise,
acquireBuildSlot().promise,
@@ -223,6 +228,10 @@ describe("session-build-semaphore", () => {
expect(currentActiveBuildsForTest()).toBe(4);
const timedOutHandle = acquireBuildSlot(10);
+ await vi.advanceTimersByTimeAsync(9);
+ expect(currentQueueLengthForTest()).toBe(1);
+ expect(currentActiveBuildsForTest()).toBe(4);
+ await vi.advanceTimersByTimeAsync(1);
const result = await timedOutHandle.promise;
expect(result).toEqual({ acquired: false, waitMs: expect.any(Number), timedOut: true });
diff --git a/apps/cloud/src/observability/observability.test.ts b/apps/cloud/src/observability/observability.test.ts
index b95ea61d7a..581377d12f 100644
--- a/apps/cloud/src/observability/observability.test.ts
+++ b/apps/cloud/src/observability/observability.test.ts
@@ -385,15 +385,24 @@ describe("Durable Object platform reset noise", () => {
expect(beforeSendWithOtelCorrelation(defect)).not.toBeNull();
});
- // The memory-limit reset is deliberately absent from the classifier: the
- // runtime blames the application for it, so it is a defect, not noise.
- it("keeps the memory-limit reset the classifier deliberately excludes", () => {
+ // The storage-cache memory-limit variant stays absent from the classifier:
+ // the runtime blames the application for it (un-awaited writes, an oversized
+ // read), so it is a defect, not noise. Its plain sibling is a platform reset
+ // and IS classified — the two are separated only by that qualifier.
+ it("keeps the memory-limit variant the classifier deliberately excludes", () => {
const memory = doInstrumentationEvent(
"Durable Object's isolate exceeded its memory limit due to overflowing the storage cache. All objects in the isolate were reset.",
);
expect(beforeSendWithOtelCorrelation(memory)).not.toBeNull();
});
+ it("drops the plain memory-limit reset as platform noise", () => {
+ const memory = doInstrumentationEvent(
+ "Durable Object's isolate exceeded its memory limit and was reset.",
+ );
+ expect(beforeSendWithOtelCorrelation(memory)).toBeNull();
+ });
+
it("the hook the worker and DOs install drops the deploy reset", () => {
const options = cloudSentryOptions({ SENTRY_DSN: "https://public@example.invalid/1" } as Env);
const event = doInstrumentationEvent("Durable Object reset because its code was updated.");
diff --git a/apps/cloud/src/routes/__root.tsx b/apps/cloud/src/routes/__root.tsx
index 7f141e6072..1c0f8b8a10 100644
--- a/apps/cloud/src/routes/__root.tsx
+++ b/apps/cloud/src/routes/__root.tsx
@@ -116,12 +116,6 @@ export const Route = createRootRoute({
{ rel: "icon", type: "image/png", sizes: "32x32", href: "/favicon-32.png" },
{ rel: "icon", type: "image/png", sizes: "192x192", href: "/favicon-192.png" },
{ rel: "apple-touch-icon", sizes: "180x180", href: "/apple-touch-icon.png" },
- { rel: "preconnect", href: "https://fonts.googleapis.com" },
- { rel: "preconnect", href: "https://fonts.gstatic.com", crossOrigin: "anonymous" },
- {
- rel: "stylesheet",
- href: "https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&family=Geist+Mono:wght@400;500;700&display=swap",
- },
{ rel: "stylesheet", href: appCss },
],
}),
diff --git a/apps/host-cloudflare/web/index.html b/apps/host-cloudflare/web/index.html
index df417e4c33..82e1575dc3 100644
--- a/apps/host-cloudflare/web/index.html
+++ b/apps/host-cloudflare/web/index.html
@@ -8,12 +8,6 @@