From 835c7720715b525a371c06745dc93aac249b01ee Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 23 Jul 2026 00:15:01 +0000
Subject: [PATCH 1/6] fix(mux): field-level merge on network policy egress
updates (#10338)
---
.stats.yml | 4 ++--
src/resources/network-policies.ts | 3 ++-
2 files changed, 4 insertions(+), 3 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 4e9c9d433..893d80de6 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 119
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/runloop-ai/runloop-0b7cd0c2fc193b18189cd7f44cf45ece7726b5d485fb72577f7d235266432ea0.yml
-openapi_spec_hash: 78c340dbfb9d3d58b24ef318fc2a657b
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/runloop-ai/runloop-d0efdaa19d8a5685def3947450df7f9bdaf9bad3243317d514add5d22129fc5f.yml
+openapi_spec_hash: 7e484ea9f60294503bf4691fe3efef3a
config_hash: 218b8d25038e627faab98532392ee9a0
diff --git a/src/resources/network-policies.ts b/src/resources/network-policies.ts
index fdf8ad8ae..5ffff2f5c 100644
--- a/src/resources/network-policies.ts
+++ b/src/resources/network-policies.ts
@@ -23,7 +23,8 @@ export class NetworkPolicies extends APIResource {
}
/**
- * Update an existing NetworkPolicy. All fields are optional.
+ * Update an existing NetworkPolicy. All fields are optional - null fields preserve
+ * existing values, provided fields replace entirely.
*/
update(
id: string,
From 429f2b1ea76284ab6006e6e819457dfeddaaf5c9 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 23 Jul 2026 23:29:47 +0000
Subject: [PATCH 2/6] feat(mux): wait_for_eviction endpoint for flex eviction
notifications (#10325)
---
.stats.yml | 8 +++---
api.md | 2 ++
src/index.ts | 2 ++
src/resources/devboxes/devboxes.ts | 25 +++++++++++++++++++
src/resources/devboxes/index.ts | 1 +
src/resources/index.ts | 1 +
tests/api-resources/devboxes/devboxes.test.ts | 18 +++++++++++++
7 files changed, 53 insertions(+), 4 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 893d80de6..8b2900ea6 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
-configured_endpoints: 119
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/runloop-ai/runloop-d0efdaa19d8a5685def3947450df7f9bdaf9bad3243317d514add5d22129fc5f.yml
-openapi_spec_hash: 7e484ea9f60294503bf4691fe3efef3a
-config_hash: 218b8d25038e627faab98532392ee9a0
+configured_endpoints: 120
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/runloop-ai/runloop-f18b1bba718700c841b3904207cd8ba2c066421a691845d6283bbbb361cf161d.yml
+openapi_spec_hash: 6044f4a4da13881c5f82fcc394c1f43b
+config_hash: d9a5b7a4227236a55c89de56b56f4aec
diff --git a/api.md b/api.md
index 38fb50a45..285113916 100644
--- a/api.md
+++ b/api.md
@@ -186,6 +186,7 @@ Types:
- DevboxReadFileContentsResponse
- DevboxRemoveTunnelResponse
- DevboxUploadFileResponse
+- DevboxWatchEvictionsResponse
Methods:
@@ -213,6 +214,7 @@ Methods:
- client.devboxes.suspend(id) -> DevboxView
- client.devboxes.uploadFile(id, { ...params }) -> unknown
- client.devboxes.waitForCommand(devboxId, executionId, { ...params }) -> DevboxAsyncExecutionDetailView
+- client.devboxes.watchEvictions() -> DevboxWatchEvictionsResponse
- client.devboxes.writeFileContents(id, { ...params }) -> DevboxExecutionDetailView
## DiskSnapshots
diff --git a/src/index.ts b/src/index.ts
index 0aeaca948..d3ce77973 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -223,6 +223,7 @@ import {
DevboxView,
DevboxViewsDevboxesCursorIDPage,
DevboxWaitForCommandParams,
+ DevboxWatchEvictionsResponse,
DevboxWriteFileContentsParams,
Devboxes,
PtyTunnelView,
@@ -690,6 +691,7 @@ export declare namespace Runloop {
type DevboxReadFileContentsResponse as DevboxReadFileContentsResponse,
type DevboxRemoveTunnelResponse as DevboxRemoveTunnelResponse,
type DevboxUploadFileResponse as DevboxUploadFileResponse,
+ type DevboxWatchEvictionsResponse as DevboxWatchEvictionsResponse,
DevboxViewsDevboxesCursorIDPage as DevboxViewsDevboxesCursorIDPage,
DevboxSnapshotViewsDiskSnapshotsCursorIDPage as DevboxSnapshotViewsDiskSnapshotsCursorIDPage,
type DevboxCreateParams as DevboxCreateParams,
diff --git a/src/resources/devboxes/devboxes.ts b/src/resources/devboxes/devboxes.ts
index d413e7ff2..53ad0dcdb 100644
--- a/src/resources/devboxes/devboxes.ts
+++ b/src/resources/devboxes/devboxes.ts
@@ -538,6 +538,17 @@ export class Devboxes extends APIResource {
});
}
+ /**
+ * Subscribe, via server-sent events, to pending infrastructure evictions for every
+ * Devbox in the account. On connect the stream emits one event per Devbox that
+ * currently has a pending eviction, then one event as each further eviction is
+ * scheduled. Best-effort and advisory: a Devbox stays running until its deadline,
+ * and delivery is not guaranteed.
+ */
+ watchEvictions(options?: Core.RequestOptions): Core.APIPromise {
+ return this._client.get('/v1/devboxes/watch_evictions', options);
+ }
+
/**
* Write UTF-8 string contents to a file at path on the Devbox. Note for large
* files (larger than 100MB), the upload_file endpoint must be used.
@@ -1094,6 +1105,19 @@ export type DevboxRemoveTunnelResponse = unknown;
export type DevboxUploadFileResponse = unknown;
+export interface DevboxWatchEvictionsResponse {
+ /**
+ * The ID of the Devbox with a pending eviction.
+ */
+ devbox_id: string;
+
+ /**
+ * Unix timestamp (milliseconds) after which the Devbox will be suspended. Advisory
+ * and best-effort.
+ */
+ eviction_deadline_ms: number;
+}
+
/**
* Parameters for creating a new Devbox.
*
@@ -1556,6 +1580,7 @@ export declare namespace Devboxes {
type DevboxReadFileContentsResponse as DevboxReadFileContentsResponse,
type DevboxRemoveTunnelResponse as DevboxRemoveTunnelResponse,
type DevboxUploadFileResponse as DevboxUploadFileResponse,
+ type DevboxWatchEvictionsResponse as DevboxWatchEvictionsResponse,
DevboxViewsDevboxesCursorIDPage as DevboxViewsDevboxesCursorIDPage,
DevboxSnapshotViewsDiskSnapshotsCursorIDPage as DevboxSnapshotViewsDiskSnapshotsCursorIDPage,
type DevboxCreateParams as DevboxCreateParams,
diff --git a/src/resources/devboxes/index.ts b/src/resources/devboxes/index.ts
index 016a2b2e6..5fcb72e19 100644
--- a/src/resources/devboxes/index.ts
+++ b/src/resources/devboxes/index.ts
@@ -22,6 +22,7 @@ export {
type DevboxReadFileContentsResponse,
type DevboxRemoveTunnelResponse,
type DevboxUploadFileResponse,
+ type DevboxWatchEvictionsResponse,
type DevboxCreateParams,
type DevboxUpdateParams,
type DevboxListParams,
diff --git a/src/resources/index.ts b/src/resources/index.ts
index 5e4e2439d..088b62a12 100644
--- a/src/resources/index.ts
+++ b/src/resources/index.ts
@@ -103,6 +103,7 @@ export {
type DevboxReadFileContentsResponse,
type DevboxRemoveTunnelResponse,
type DevboxUploadFileResponse,
+ type DevboxWatchEvictionsResponse,
type DevboxCreateParams,
type DevboxUpdateParams,
type DevboxListParams,
diff --git a/tests/api-resources/devboxes/devboxes.test.ts b/tests/api-resources/devboxes/devboxes.test.ts
index fdd846d80..d515eb839 100644
--- a/tests/api-resources/devboxes/devboxes.test.ts
+++ b/tests/api-resources/devboxes/devboxes.test.ts
@@ -596,6 +596,24 @@ describe('resource devboxes', () => {
});
});
+ test('watchEvictions', async () => {
+ const responsePromise = client.devboxes.watchEvictions();
+ const rawResponse = await responsePromise.asResponse();
+ expect(rawResponse).toBeInstanceOf(Response);
+ const response = await responsePromise;
+ expect(response).not.toBeInstanceOf(Response);
+ const dataAndResponse = await responsePromise.withResponse();
+ expect(dataAndResponse.data).toBe(response);
+ expect(dataAndResponse.response).toBe(rawResponse);
+ });
+
+ test('watchEvictions: request options instead of params are passed correctly', async () => {
+ // ensure the request options are being passed correctly by passing an invalid HTTP method in order to cause an error
+ await expect(client.devboxes.watchEvictions({ path: '/_stainless_unknown_path' })).rejects.toThrow(
+ Runloop.NotFoundError,
+ );
+ });
+
test('writeFileContents: only required params', async () => {
const responsePromise = client.devboxes.writeFileContents('id', {
contents: 'contents',
From 043ebbc407067c3d7d620b9a00fa1937dd3bcb3b Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 24 Jul 2026 00:08:31 +0000
Subject: [PATCH 3/6] chore(stainless): generate watch_evictions as an SSE
stream (#10404)
---
.stats.yml | 4 +--
api.md | 4 +--
src/index.ts | 4 +--
src/resources/devboxes/devboxes.ts | 36 ++++++++++---------
src/resources/devboxes/index.ts | 2 +-
src/resources/index.ts | 2 +-
tests/api-resources/devboxes/devboxes.test.ts | 7 ----
7 files changed, 28 insertions(+), 31 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 8b2900ea6..10c388023 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 120
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/runloop-ai/runloop-f18b1bba718700c841b3904207cd8ba2c066421a691845d6283bbbb361cf161d.yml
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/runloop-ai/runloop-6fb96a2de54d260cad2b76366cada4eda15335275dbab7f6c108c1bb29acf309.yml
openapi_spec_hash: 6044f4a4da13881c5f82fcc394c1f43b
-config_hash: d9a5b7a4227236a55c89de56b56f4aec
+config_hash: 92c04f0b12249cb2d2bcf9e503a9cbf9
diff --git a/api.md b/api.md
index 285113916..6cad8a571 100644
--- a/api.md
+++ b/api.md
@@ -169,6 +169,7 @@ Methods:
Types:
- DevboxAsyncExecutionDetailView
+- DevboxEvictionEventView
- DevboxExecutionDetailView
- DevboxKillExecutionRequest
- DevboxListView
@@ -186,7 +187,6 @@ Types:
- DevboxReadFileContentsResponse
- DevboxRemoveTunnelResponse
- DevboxUploadFileResponse
-- DevboxWatchEvictionsResponse
Methods:
@@ -214,7 +214,7 @@ Methods:
- client.devboxes.suspend(id) -> DevboxView
- client.devboxes.uploadFile(id, { ...params }) -> unknown
- client.devboxes.waitForCommand(devboxId, executionId, { ...params }) -> DevboxAsyncExecutionDetailView
-- client.devboxes.watchEvictions() -> DevboxWatchEvictionsResponse
+- client.devboxes.watchEvictions() -> DevboxEvictionEventView
- client.devboxes.writeFileContents(id, { ...params }) -> DevboxExecutionDetailView
## DiskSnapshots
diff --git a/src/index.ts b/src/index.ts
index d3ce77973..b4ac078fd 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -196,6 +196,7 @@ import {
DevboxDeleteDiskSnapshotResponse,
DevboxDownloadFileParams,
DevboxEnableTunnelParams,
+ DevboxEvictionEventView,
DevboxExecuteAsyncParams,
DevboxExecuteParams,
DevboxExecuteSyncParams,
@@ -223,7 +224,6 @@ import {
DevboxView,
DevboxViewsDevboxesCursorIDPage,
DevboxWaitForCommandParams,
- DevboxWatchEvictionsResponse,
DevboxWriteFileContentsParams,
Devboxes,
PtyTunnelView,
@@ -674,6 +674,7 @@ export declare namespace Runloop {
export {
Devboxes as Devboxes,
type DevboxAsyncExecutionDetailView as DevboxAsyncExecutionDetailView,
+ type DevboxEvictionEventView as DevboxEvictionEventView,
type DevboxExecutionDetailView as DevboxExecutionDetailView,
type DevboxKillExecutionRequest as DevboxKillExecutionRequest,
type DevboxListView as DevboxListView,
@@ -691,7 +692,6 @@ export declare namespace Runloop {
type DevboxReadFileContentsResponse as DevboxReadFileContentsResponse,
type DevboxRemoveTunnelResponse as DevboxRemoveTunnelResponse,
type DevboxUploadFileResponse as DevboxUploadFileResponse,
- type DevboxWatchEvictionsResponse as DevboxWatchEvictionsResponse,
DevboxViewsDevboxesCursorIDPage as DevboxViewsDevboxesCursorIDPage,
DevboxSnapshotViewsDiskSnapshotsCursorIDPage as DevboxSnapshotViewsDiskSnapshotsCursorIDPage,
type DevboxCreateParams as DevboxCreateParams,
diff --git a/src/resources/devboxes/devboxes.ts b/src/resources/devboxes/devboxes.ts
index 53ad0dcdb..b24eeaae6 100644
--- a/src/resources/devboxes/devboxes.ts
+++ b/src/resources/devboxes/devboxes.ts
@@ -2,6 +2,7 @@
import { APIResource } from '../../resource';
import { isRequestOptions } from '../../core';
+import { APIPromise } from '../../core';
import * as Core from '../../core';
import * as Shared from '../shared';
import * as DiskSnapshotsAPI from './disk-snapshots';
@@ -32,6 +33,7 @@ import {
DiskSnapshotsCursorIDPage,
type DiskSnapshotsCursorIDPageParams,
} from '../../pagination';
+import { Stream } from '../../streaming';
import { type Response } from '../../_shims/index';
import {
longPollUntil,
@@ -545,8 +547,10 @@ export class Devboxes extends APIResource {
* scheduled. Best-effort and advisory: a Devbox stays running until its deadline,
* and delivery is not guaranteed.
*/
- watchEvictions(options?: Core.RequestOptions): Core.APIPromise {
- return this._client.get('/v1/devboxes/watch_evictions', options);
+ watchEvictions(options?: Core.RequestOptions): APIPromise> {
+ return this._client.get('/v1/devboxes/watch_evictions', { ...options, stream: true }) as APIPromise<
+ Stream
+ >;
}
/**
@@ -630,6 +634,19 @@ export interface DevboxAsyncExecutionDetailView {
stdout_truncated?: boolean | null;
}
+export interface DevboxEvictionEventView {
+ /**
+ * The ID of the Devbox with a pending eviction.
+ */
+ devbox_id: string;
+
+ /**
+ * Unix timestamp (milliseconds) after which the Devbox will be suspended. Advisory
+ * and best-effort.
+ */
+ eviction_deadline_ms: number;
+}
+
export interface DevboxExecutionDetailView {
/**
* Devbox id where command was executed.
@@ -1105,19 +1122,6 @@ export type DevboxRemoveTunnelResponse = unknown;
export type DevboxUploadFileResponse = unknown;
-export interface DevboxWatchEvictionsResponse {
- /**
- * The ID of the Devbox with a pending eviction.
- */
- devbox_id: string;
-
- /**
- * Unix timestamp (milliseconds) after which the Devbox will be suspended. Advisory
- * and best-effort.
- */
- eviction_deadline_ms: number;
-}
-
/**
* Parameters for creating a new Devbox.
*
@@ -1563,6 +1567,7 @@ Devboxes.Executions = Executions;
export declare namespace Devboxes {
export {
type DevboxAsyncExecutionDetailView as DevboxAsyncExecutionDetailView,
+ type DevboxEvictionEventView as DevboxEvictionEventView,
type DevboxExecutionDetailView as DevboxExecutionDetailView,
type DevboxKillExecutionRequest as DevboxKillExecutionRequest,
type DevboxListView as DevboxListView,
@@ -1580,7 +1585,6 @@ export declare namespace Devboxes {
type DevboxReadFileContentsResponse as DevboxReadFileContentsResponse,
type DevboxRemoveTunnelResponse as DevboxRemoveTunnelResponse,
type DevboxUploadFileResponse as DevboxUploadFileResponse,
- type DevboxWatchEvictionsResponse as DevboxWatchEvictionsResponse,
DevboxViewsDevboxesCursorIDPage as DevboxViewsDevboxesCursorIDPage,
DevboxSnapshotViewsDiskSnapshotsCursorIDPage as DevboxSnapshotViewsDiskSnapshotsCursorIDPage,
type DevboxCreateParams as DevboxCreateParams,
diff --git a/src/resources/devboxes/index.ts b/src/resources/devboxes/index.ts
index 5fcb72e19..f52683f68 100644
--- a/src/resources/devboxes/index.ts
+++ b/src/resources/devboxes/index.ts
@@ -5,6 +5,7 @@ export {
DevboxViewsDevboxesCursorIDPage,
Devboxes,
type DevboxAsyncExecutionDetailView,
+ type DevboxEvictionEventView,
type DevboxExecutionDetailView,
type DevboxKillExecutionRequest,
type DevboxListView,
@@ -22,7 +23,6 @@ export {
type DevboxReadFileContentsResponse,
type DevboxRemoveTunnelResponse,
type DevboxUploadFileResponse,
- type DevboxWatchEvictionsResponse,
type DevboxCreateParams,
type DevboxUpdateParams,
type DevboxListParams,
diff --git a/src/resources/index.ts b/src/resources/index.ts
index 088b62a12..fe0f43691 100644
--- a/src/resources/index.ts
+++ b/src/resources/index.ts
@@ -86,6 +86,7 @@ export {
DevboxViewsDevboxesCursorIDPage,
Devboxes,
type DevboxAsyncExecutionDetailView,
+ type DevboxEvictionEventView,
type DevboxExecutionDetailView,
type DevboxKillExecutionRequest,
type DevboxListView,
@@ -103,7 +104,6 @@ export {
type DevboxReadFileContentsResponse,
type DevboxRemoveTunnelResponse,
type DevboxUploadFileResponse,
- type DevboxWatchEvictionsResponse,
type DevboxCreateParams,
type DevboxUpdateParams,
type DevboxListParams,
diff --git a/tests/api-resources/devboxes/devboxes.test.ts b/tests/api-resources/devboxes/devboxes.test.ts
index d515eb839..9f547d3bf 100644
--- a/tests/api-resources/devboxes/devboxes.test.ts
+++ b/tests/api-resources/devboxes/devboxes.test.ts
@@ -607,13 +607,6 @@ describe('resource devboxes', () => {
expect(dataAndResponse.response).toBe(rawResponse);
});
- test('watchEvictions: request options instead of params are passed correctly', async () => {
- // ensure the request options are being passed correctly by passing an invalid HTTP method in order to cause an error
- await expect(client.devboxes.watchEvictions({ path: '/_stainless_unknown_path' })).rejects.toThrow(
- Runloop.NotFoundError,
- );
- });
-
test('writeFileContents: only required params', async () => {
const responsePromise = client.devboxes.writeFileContents('id', {
contents: 'contents',
From 80d82cf7798ce423dff86876ff89bd752f80d0f0 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 24 Jul 2026 15:59:27 +0000
Subject: [PATCH 4/6] fix(mux): move eviction-watch SSE route off the /{id}
slot (#10409)
---
.stats.yml | 6 +++---
api.md | 2 +-
src/resources/devboxes/devboxes.ts | 2 +-
3 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 10c388023..8b4084be5 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 120
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/runloop-ai/runloop-6fb96a2de54d260cad2b76366cada4eda15335275dbab7f6c108c1bb29acf309.yml
-openapi_spec_hash: 6044f4a4da13881c5f82fcc394c1f43b
-config_hash: 92c04f0b12249cb2d2bcf9e503a9cbf9
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/runloop-ai/runloop-50686c7a06e432373c5dd7e771ab1483a6ca0531b1dd9a10fc31c0cf2833bcdf.yml
+openapi_spec_hash: a554b4d3815148c094778b53bfe20b21
+config_hash: 6d102da8b9fcc3d0deb42d7a707e78c6
diff --git a/api.md b/api.md
index 6cad8a571..3af4951ad 100644
--- a/api.md
+++ b/api.md
@@ -214,7 +214,7 @@ Methods:
- client.devboxes.suspend(id) -> DevboxView
- client.devboxes.uploadFile(id, { ...params }) -> unknown
- client.devboxes.waitForCommand(devboxId, executionId, { ...params }) -> DevboxAsyncExecutionDetailView
-- client.devboxes.watchEvictions() -> DevboxEvictionEventView
+- client.devboxes.watchEvictions() -> DevboxEvictionEventView
- client.devboxes.writeFileContents(id, { ...params }) -> DevboxExecutionDetailView
## DiskSnapshots
diff --git a/src/resources/devboxes/devboxes.ts b/src/resources/devboxes/devboxes.ts
index b24eeaae6..a009f2beb 100644
--- a/src/resources/devboxes/devboxes.ts
+++ b/src/resources/devboxes/devboxes.ts
@@ -548,7 +548,7 @@ export class Devboxes extends APIResource {
* and delivery is not guaranteed.
*/
watchEvictions(options?: Core.RequestOptions): APIPromise> {
- return this._client.get('/v1/devboxes/watch_evictions', { ...options, stream: true }) as APIPromise<
+ return this._client.get('/v1/devboxes/evictions/watch', { ...options, stream: true }) as APIPromise<
Stream
>;
}
From fc944fc921f4f3fb062dce5483cb8c310b30f48e Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 24 Jul 2026 16:00:17 +0000
Subject: [PATCH 5/6] release: 1.27.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 19 +++++++++++++++++++
package.json | 2 +-
packages/mcp-server/manifest.json | 2 +-
packages/mcp-server/package.json | 2 +-
packages/mcp-server/src/server.ts | 2 +-
src/version.ts | 2 +-
7 files changed, 25 insertions(+), 6 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index f97891a67..c775f946f 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "1.26.0"
+ ".": "1.27.0"
}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4daafd6b0..802696f52 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,24 @@
# Changelog
+## 1.27.0 (2026-07-24)
+
+Full Changelog: [v1.26.0...v1.27.0](https://github.com/runloopai/api-client-ts/compare/v1.26.0...v1.27.0)
+
+### Features
+
+* **mux:** wait_for_eviction endpoint for flex eviction notifications ([#10325](https://github.com/runloopai/api-client-ts/issues/10325)) ([429f2b1](https://github.com/runloopai/api-client-ts/commit/429f2b1ea76284ab6006e6e819457dfeddaaf5c9))
+
+
+### Bug Fixes
+
+* **mux:** field-level merge on network policy egress updates ([#10338](https://github.com/runloopai/api-client-ts/issues/10338)) ([835c772](https://github.com/runloopai/api-client-ts/commit/835c7720715b525a371c06745dc93aac249b01ee))
+* **mux:** move eviction-watch SSE route off the /{id} slot ([#10409](https://github.com/runloopai/api-client-ts/issues/10409)) ([80d82cf](https://github.com/runloopai/api-client-ts/commit/80d82cf7798ce423dff86876ff89bd752f80d0f0))
+
+
+### Chores
+
+* **stainless:** generate watch_evictions as an SSE stream ([#10404](https://github.com/runloopai/api-client-ts/issues/10404)) ([043ebbc](https://github.com/runloopai/api-client-ts/commit/043ebbc407067c3d7d620b9a00fa1937dd3bcb3b))
+
## 1.26.0 (2026-07-22)
Full Changelog: [v1.25.0...v1.26.0](https://github.com/runloopai/api-client-ts/compare/v1.25.0...v1.26.0)
diff --git a/package.json b/package.json
index f69fc5911..34552f541 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@runloop/api-client",
- "version": "1.26.0",
+ "version": "1.27.0",
"description": "The official TypeScript library for the Runloop API",
"author": "Runloop ",
"types": "dist/sdk.d.ts",
diff --git a/packages/mcp-server/manifest.json b/packages/mcp-server/manifest.json
index f5194782b..478bd1dfb 100644
--- a/packages/mcp-server/manifest.json
+++ b/packages/mcp-server/manifest.json
@@ -1,7 +1,7 @@
{
"dxt_version": "0.2",
"name": "@runloop/api-client-mcp",
- "version": "1.26.0",
+ "version": "1.27.0",
"description": "The official MCP Server for the Runloop API",
"author": {
"name": "Runloop",
diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json
index 532a609fe..a759828a0 100644
--- a/packages/mcp-server/package.json
+++ b/packages/mcp-server/package.json
@@ -1,6 +1,6 @@
{
"name": "@runloop/api-client-mcp",
- "version": "1.26.0",
+ "version": "1.27.0",
"description": "The official MCP Server for the Runloop API",
"author": "Runloop ",
"types": "dist/index.d.ts",
diff --git a/packages/mcp-server/src/server.ts b/packages/mcp-server/src/server.ts
index c494e9f2d..099bbe38f 100644
--- a/packages/mcp-server/src/server.ts
+++ b/packages/mcp-server/src/server.ts
@@ -16,7 +16,7 @@ export const newMcpServer = async (stainlessApiKey: string | undefined) =>
new McpServer(
{
name: 'runloop_api_client_api',
- version: '1.26.0',
+ version: '1.27.0',
},
{
instructions: await getInstructions(stainlessApiKey),
diff --git a/src/version.ts b/src/version.ts
index a58c5e122..f7cc16593 100644
--- a/src/version.ts
+++ b/src/version.ts
@@ -1 +1 @@
-export const VERSION = '1.26.0'; // x-release-please-version
+export const VERSION = '1.27.0'; // x-release-please-version
From c4117b75cd15963fbe0fa51706b2cae1c11d31e8 Mon Sep 17 00:00:00 2001
From: Rob von Behren
Date: Fri, 24 Jul 2026 11:39:15 -0700
Subject: [PATCH 6/6] feat(devbox): add devbox.onEvict eviction notifications
(SSE) (#820)
Co-authored-by: Reflex
Co-authored-by: Reflex
---
src/sdk/devbox.ts | 30 +++++
src/sdk/eviction.ts | 126 ++++++++++++++++++
src/sdk/index.ts | 1 +
.../object-oriented/eviction.test.ts | 105 +++++++++++++++
4 files changed, 262 insertions(+)
create mode 100644 src/sdk/eviction.ts
create mode 100644 tests/smoketests/object-oriented/eviction.test.ts
diff --git a/src/sdk/devbox.ts b/src/sdk/devbox.ts
index e34a938d3..6d8627f58 100644
--- a/src/sdk/devbox.ts
+++ b/src/sdk/devbox.ts
@@ -22,6 +22,7 @@ import { LongPollRequestOptions, PollingOptions } from '../lib/polling';
import { Snapshot } from './snapshot';
import { Execution } from './execution';
import { ExecutionResult } from './execution-result';
+import { EvictionCallback, getEvictionMonitor } from './eviction';
import { uuidv7 } from 'uuidv7';
// Re-export Execution and ExecutionResult for Devbox namespace
@@ -1029,6 +1030,35 @@ export class Devbox {
async keepAlive(options?: Core.RequestOptions): Promise {
return this.client.devboxes.keepAlive(this._id, options);
}
+
+ /**
+ * Register a callback fired once if this devbox gets a pending infrastructure eviction.
+ *
+ * The first `onEvict` across any devbox on this client opens a single account-wide
+ * notification stream; it closes automatically once every registered devbox has been
+ * notified. The callback runs at most once and is invoked as
+ * `callback(devbox, evictionDeadlineMs)` — this devbox and the Unix millisecond
+ * deadline by which it will be suspended. Use it to run cleanup before suspend.
+ *
+ * @param {EvictionCallback} callback - Invoked with this devbox and its eviction deadline (ms).
+ *
+ * @example
+ * ```typescript
+ * devbox.onEvict((devbox, evictionDeadlineMs) => {
+ * console.log(`${devbox.id} will be suspended at ${evictionDeadlineMs}`);
+ * });
+ * ```
+ */
+ onEvict(callback: EvictionCallback): void {
+ getEvictionMonitor(this.client).register(this, callback);
+ }
+
+ /**
+ * Withdraw this devbox's eviction interest registered via {@link onEvict}.
+ */
+ cancelOnEvict(): void {
+ getEvictionMonitor(this.client).unregister(this._id);
+ }
}
/**
diff --git a/src/sdk/eviction.ts b/src/sdk/eviction.ts
new file mode 100644
index 000000000..60eb93017
--- /dev/null
+++ b/src/sdk/eviction.ts
@@ -0,0 +1,126 @@
+import { Runloop } from '../index';
+import { Stream } from '../streaming';
+import type { DevboxEvictionEventView } from '../resources/devboxes/devboxes';
+import type { Devbox } from './devbox';
+
+/**
+ * Invoked once when the devbox it was registered for has a pending eviction.
+ *
+ * @param devbox - The devbox with a pending eviction.
+ * @param evictionDeadlineMs - Unix timestamp (ms) by which the devbox will be suspended.
+ */
+export type EvictionCallback = (devbox: Devbox, evictionDeadlineMs: number) => void;
+
+/**
+ * Fans account-wide eviction notifications out to per-devbox callbacks.
+ *
+ * One monitor is shared by every {@link Devbox} built from the same generated
+ * client (see {@link getEvictionMonitor}), so all registered devboxes are served
+ * by a single SSE connection. The stream opens the moment the first devbox
+ * registers interest and closes as soon as the last interested devbox has been
+ * notified.
+ *
+ * Delivery contract:
+ * - The server replays every currently-pending eviction on connect, so a devbox
+ * that registers after its eviction was scheduled is still notified.
+ * - Notifications for devboxes not in the interest set are discarded.
+ * - A devbox is removed from the interest set *before* its callback runs, so the
+ * callback fires at most once even if the server repeats the notification.
+ */
+export class EvictionMonitor {
+ private entries = new Map();
+ private stream: Stream | null = null;
+ private running = false;
+
+ constructor(private client: Runloop) {}
+
+ /** Add `devbox` to the interest set, opening the stream if idle. */
+ register(devbox: Devbox, callback: EvictionCallback): void {
+ this.entries.set(devbox.id, { devbox, callback });
+ if (!this.running) {
+ this.running = true;
+ void this.run();
+ }
+ }
+
+ /** Drop `devboxId`; close the stream if it was the last interested devbox. */
+ unregister(devboxId: string): void {
+ this.entries.delete(devboxId);
+ if (this.entries.size === 0) {
+ this.close();
+ }
+ }
+
+ /** Clear all interest and tear down the stream. */
+ close(): void {
+ this.entries.clear();
+ this.stream?.controller.abort();
+ this.stream = null;
+ this.running = false;
+ }
+
+ private async run(): Promise {
+ // The server force-closes the stream on purpose (leader change / slow consumer) and a
+ // long-lived HTTP/2 stream can drop; the client is expected to reconnect and re-read the
+ // snapshot, which re-delivers anything missed. So reconnect (with backoff) until no devbox is
+ // still interested.
+ const INITIAL_BACKOFF_MS = 500;
+ const MAX_BACKOFF_MS = 30_000;
+ let backoff = INITIAL_BACKOFF_MS;
+ try {
+ while (this.entries.size > 0) {
+ try {
+ // Force the SSE Accept header: the endpoint only streams for text/event-stream; the
+ // generated client's default (application/json) gets an empty text/plain response, so the
+ // feed would silently deliver nothing.
+ const stream = await this.client.devboxes.watchEvictions({
+ headers: { Accept: 'text/event-stream' },
+ });
+ this.stream = stream;
+ for await (const event of stream) {
+ this.dispatch(event);
+ if (this.entries.size === 0) return;
+ }
+ // Clean end: reset backoff and reconnect if still interested.
+ backoff = INITIAL_BACKOFF_MS;
+ } catch (error) {
+ // Aborting the stream on close surfaces as an AbortError; that is an intentional teardown.
+ if (error instanceof Error && error.name === 'AbortError') return;
+ if (this.entries.size === 0) return;
+ // Otherwise a routine disconnect — fall through to backoff + reconnect.
+ }
+ if (this.entries.size === 0) return;
+ await new Promise((resolve) => setTimeout(resolve, backoff));
+ backoff = Math.min(backoff * 2, MAX_BACKOFF_MS);
+ }
+ } finally {
+ this.stream = null;
+ this.running = false;
+ }
+ }
+
+ private dispatch(event: DevboxEvictionEventView): void {
+ const entry = this.entries.get(event.devbox_id);
+ if (!entry) return;
+ // Remove before signaling so a duplicate notification for the same devbox is
+ // discarded and the callback fires at most once.
+ this.entries.delete(event.devbox_id);
+ try {
+ entry.callback(entry.devbox, event.eviction_deadline_ms);
+ } catch (error) {
+ console.error(`Error in eviction callback for devbox ${event.devbox_id}:`, error);
+ }
+ }
+}
+
+const monitors = new WeakMap();
+
+/** Return the shared {@link EvictionMonitor} for `client`, creating it once. */
+export function getEvictionMonitor(client: Runloop): EvictionMonitor {
+ let monitor = monitors.get(client);
+ if (!monitor) {
+ monitor = new EvictionMonitor(client);
+ monitors.set(client, monitor);
+ }
+ return monitor;
+}
diff --git a/src/sdk/index.ts b/src/sdk/index.ts
index 088063279..4c8da4af8 100644
--- a/src/sdk/index.ts
+++ b/src/sdk/index.ts
@@ -1,4 +1,5 @@
export { Devbox, DevboxCmdOps, DevboxFileOps, DevboxNetOps, type ExecuteStreamingCallbacks } from './devbox';
+export { EvictionMonitor, getEvictionMonitor, type EvictionCallback } from './eviction';
export { Blueprint } from './blueprint';
export { Snapshot } from './snapshot';
export { StorageObject } from './storage-object';
diff --git a/tests/smoketests/object-oriented/eviction.test.ts b/tests/smoketests/object-oriented/eviction.test.ts
new file mode 100644
index 000000000..6cae58f2e
--- /dev/null
+++ b/tests/smoketests/object-oriented/eviction.test.ts
@@ -0,0 +1,105 @@
+import { Devbox } from '@runloop/api-client/sdk';
+import type { DevboxEvictionEventView } from '@runloop/api-client/resources/devboxes/devboxes';
+
+// Hermetic tests: they drive the shared EvictionMonitor through an in-memory fake
+// SSE stream instead of a live devbox eviction, so `devbox.onEvict` / `cancelOnEvict`
+// (and the monitor's dispatch + teardown) are exercised deterministically.
+
+// The generated watch_evictions stream: an async-iterable of eviction events plus an
+// AbortController the monitor aborts on close().
+type FakeStream = {
+ controller: AbortController;
+ [Symbol.asyncIterator](): AsyncIterator;
+};
+
+function streamOf(events: DevboxEvictionEventView[]): FakeStream {
+ return {
+ controller: new AbortController(),
+ async *[Symbol.asyncIterator]() {
+ for (const event of events) {
+ yield event;
+ }
+ },
+ };
+}
+
+// A stream that emits nothing until the monitor aborts it (i.e. on cancelOnEvict).
+function blockingStream(): FakeStream {
+ const controller = new AbortController();
+ return {
+ controller,
+ async *[Symbol.asyncIterator]() {
+ await new Promise((resolve) => {
+ if (controller.signal.aborted) {
+ resolve();
+ } else {
+ controller.signal.addEventListener('abort', () => resolve(), { once: true });
+ }
+ });
+ },
+ };
+}
+
+// Type the fake as whatever Devbox.fromId expects, without importing the generated client.
+type Client = Parameters[0];
+function fakeClient(watchEvictions: () => Promise): Client {
+ return { devboxes: { watchEvictions } } as unknown as Client;
+}
+
+const tick = (ms = 20) => new Promise((resolve) => setTimeout(resolve, ms));
+
+(process.env['RUN_SMOKETESTS'] ? describe : describe.skip)('object-oriented eviction notifications', () => {
+ test('onEvict fires once for a matching devbox and ignores others', async () => {
+ const events: DevboxEvictionEventView[] = [
+ { devbox_id: 'dbx_other', eviction_deadline_ms: 1 },
+ { devbox_id: 'dbx_match', eviction_deadline_ms: 1_720_000_000_000 },
+ ];
+ const devbox = Devbox.fromId(
+ fakeClient(async () => streamOf(events)),
+ 'dbx_match',
+ );
+
+ let receivedDevbox: Devbox | undefined;
+ const calls: Array<{ id: string; deadline: number }> = [];
+ const fired = new Promise((resolve) => {
+ devbox.onEvict((evicted, evictionDeadlineMs) => {
+ receivedDevbox = evicted;
+ calls.push({ id: evicted.id, deadline: evictionDeadlineMs });
+ resolve();
+ });
+ });
+
+ let guard: ReturnType;
+ const timeout = new Promise((_resolve, reject) => {
+ guard = setTimeout(() => reject(new Error('onEvict callback never fired')), 2000);
+ });
+ try {
+ await Promise.race([fired, timeout]);
+ } finally {
+ clearTimeout(guard!);
+ }
+ await tick();
+
+ // Fired once, with the devbox object and its deadline; the unmatched id was discarded.
+ expect(receivedDevbox).toBe(devbox);
+ expect(calls).toEqual([{ id: 'dbx_match', deadline: 1_720_000_000_000 }]);
+ });
+
+ test('cancelOnEvict stops watching and aborts the stream', async () => {
+ const stream = blockingStream();
+ const devbox = Devbox.fromId(
+ fakeClient(async () => stream),
+ 'dbx_cancel',
+ );
+
+ const callback = jest.fn();
+ devbox.onEvict(callback);
+ await tick(); // let the monitor open the stream
+
+ devbox.cancelOnEvict();
+
+ expect(stream.controller.signal.aborted).toBe(true);
+ await tick();
+ expect(callback).not.toHaveBeenCalled();
+ });
+});