diff --git a/apps/app/src/components/layout/AppLayout.plugin-panel-header.test.tsx b/apps/app/src/components/layout/AppLayout.plugin-panel-header.test.tsx
index f50d237aed..92e10cc62d 100644
--- a/apps/app/src/components/layout/AppLayout.plugin-panel-header.test.tsx
+++ b/apps/app/src/components/layout/AppLayout.plugin-panel-header.test.tsx
@@ -26,7 +26,6 @@ vi.mock("@/hooks/queries/system-queries", () => ({
changelogPreview: false,
editMessages: false,
mobileApp: false,
- providerSessionReaping: false,
timelineWindowing: false,
},
},
diff --git a/apps/app/src/components/layout/AppLayout.root-compose-project.test.tsx b/apps/app/src/components/layout/AppLayout.root-compose-project.test.tsx
index dd80969645..379c7e0c20 100644
--- a/apps/app/src/components/layout/AppLayout.root-compose-project.test.tsx
+++ b/apps/app/src/components/layout/AppLayout.root-compose-project.test.tsx
@@ -31,7 +31,6 @@ vi.mock("@/hooks/queries/system-queries", () => ({
changelogPreview: false,
editMessages: false,
mobileApp: false,
- providerSessionReaping: false,
timelineWindowing: false,
},
},
diff --git a/apps/app/src/components/layout/AppLayout.sidebar-resize.test.tsx b/apps/app/src/components/layout/AppLayout.sidebar-resize.test.tsx
index 3fc3d139a5..1e8a05a554 100644
--- a/apps/app/src/components/layout/AppLayout.sidebar-resize.test.tsx
+++ b/apps/app/src/components/layout/AppLayout.sidebar-resize.test.tsx
@@ -37,7 +37,6 @@ vi.mock("@/hooks/queries/system-queries", () => ({
data: {
experiments: {
editMessages: false,
- providerSessionReaping: false,
},
},
}),
diff --git a/apps/app/src/lib/system-config-atoms.ts b/apps/app/src/lib/system-config-atoms.ts
index 80efa1e4ca..3b8ef59273 100644
--- a/apps/app/src/lib/system-config-atoms.ts
+++ b/apps/app/src/lib/system-config-atoms.ts
@@ -24,7 +24,6 @@ const unavailableSystemConfig: SystemConfigResponse = {
changelogPreview: false,
editMessages: false,
mobileApp: false,
- providerSessionReaping: false,
timelineWindowing: false,
},
appearance: defaultAppTheme,
diff --git a/apps/app/src/views/SettingsView.experiments.test.tsx b/apps/app/src/views/SettingsView.experiments.test.tsx
index 037c808cdf..a2e70f7e4e 100644
--- a/apps/app/src/views/SettingsView.experiments.test.tsx
+++ b/apps/app/src/views/SettingsView.experiments.test.tsx
@@ -8,7 +8,6 @@ afterEach(cleanup);
function renderSection(overrides?: {
onChangelogPreviewEnabledChange?: (enabled: boolean) => void;
onMobileAppEnabledChange?: (enabled: boolean) => void;
- onProviderSessionReapingEnabledChange?: (enabled: boolean) => void;
onTimelineWindowingEnabledChange?: (enabled: boolean) => void;
}) {
return render(
@@ -17,16 +16,12 @@ function renderSection(overrides?: {
disabled={false}
editMessagesEnabled={false}
mobileAppEnabled={false}
- providerSessionReapingEnabled={false}
timelineWindowingEnabled={false}
onChangelogPreviewEnabledChange={
overrides?.onChangelogPreviewEnabledChange ?? vi.fn()
}
onEditMessagesEnabledChange={vi.fn()}
onMobileAppEnabledChange={overrides?.onMobileAppEnabledChange ?? vi.fn()}
- onProviderSessionReapingEnabledChange={
- overrides?.onProviderSessionReapingEnabledChange ?? vi.fn()
- }
onTimelineWindowingEnabledChange={
overrides?.onTimelineWindowingEnabledChange ?? vi.fn()
}
@@ -49,13 +44,6 @@ describe("ExperimentsSettingsSection", () => {
expect(onChange).toHaveBeenCalledWith(true);
});
- it("reports idle provider session release changes", () => {
- const onChange = vi.fn();
- renderSection({ onProviderSessionReapingEnabledChange: onChange });
- fireEvent.click(screen.getByLabelText("Idle provider session release"));
- expect(onChange).toHaveBeenCalledWith(true);
- });
-
it("reports timeline windowing changes", () => {
const onChange = vi.fn();
renderSection({ onTimelineWindowingEnabledChange: onChange });
diff --git a/apps/app/src/views/SettingsView.stories.tsx b/apps/app/src/views/SettingsView.stories.tsx
index 0c5d259188..6460b0798f 100644
--- a/apps/app/src/views/SettingsView.stories.tsx
+++ b/apps/app/src/views/SettingsView.stories.tsx
@@ -356,7 +356,6 @@ function ExperimentsStory() {
disabled={false}
editMessagesEnabled={state.experiments.editMessages}
mobileAppEnabled={state.experiments.mobileApp}
- providerSessionReapingEnabled={state.experiments.providerSessionReaping}
timelineWindowingEnabled={state.experiments.timelineWindowing}
onChangelogPreviewEnabledChange={(enabled) =>
state.setExperiments((current) => ({
@@ -376,12 +375,6 @@ function ExperimentsStory() {
mobileApp: enabled,
}))
}
- onProviderSessionReapingEnabledChange={(enabled) =>
- state.setExperiments((current) => ({
- ...current,
- providerSessionReaping: enabled,
- }))
- }
onTimelineWindowingEnabledChange={(enabled) =>
state.setExperiments((current) => ({
...current,
diff --git a/apps/app/src/views/SettingsView.tsx b/apps/app/src/views/SettingsView.tsx
index 044a8e9b18..cb11582ce6 100644
--- a/apps/app/src/views/SettingsView.tsx
+++ b/apps/app/src/views/SettingsView.tsx
@@ -186,12 +186,10 @@ interface ExperimentsSettingsSectionProps {
changelogPreviewEnabled: boolean;
editMessagesEnabled: boolean;
mobileAppEnabled: boolean;
- providerSessionReapingEnabled: boolean;
timelineWindowingEnabled: boolean;
onChangelogPreviewEnabledChange: (enabled: boolean) => void;
onEditMessagesEnabledChange: (enabled: boolean) => void;
onMobileAppEnabledChange: (enabled: boolean) => void;
- onProviderSessionReapingEnabledChange: (enabled: boolean) => void;
onTimelineWindowingEnabledChange: (enabled: boolean) => void;
}
@@ -883,20 +881,16 @@ export function ProviderSettingsSection({
const CHANGELOG_PREVIEW_EXPERIMENT_LABEL = "Changelog preview";
const EDIT_MESSAGES_EXPERIMENT_LABEL = "Edit messages";
const MOBILE_APP_EXPERIMENT_LABEL = "Mobile app";
-const PROVIDER_SESSION_REAPING_EXPERIMENT_LABEL =
- "Idle provider session release";
const TIMELINE_WINDOWING_EXPERIMENT_LABEL = "Timeline windowing";
export function ExperimentsSettingsSection({
changelogPreviewEnabled,
disabled,
editMessagesEnabled,
mobileAppEnabled,
- providerSessionReapingEnabled,
timelineWindowingEnabled,
onChangelogPreviewEnabledChange,
onEditMessagesEnabledChange,
onMobileAppEnabledChange,
- onProviderSessionReapingEnabledChange,
onTimelineWindowingEnabledChange,
}: ExperimentsSettingsSectionProps) {
return (
@@ -941,18 +935,6 @@ export function ExperimentsSettingsSection({
/>
-
-
-
-
- updateExperimentsMutation.mutate({
- ...experiments,
- providerSessionReaping: enabled,
- })
- }
timelineWindowingEnabled={experiments.timelineWindowing}
onTimelineWindowingEnabledChange={(enabled) =>
updateExperimentsMutation.mutate({
diff --git a/apps/desktop/scripts/smoke-packaged-app.mjs b/apps/desktop/scripts/smoke-packaged-app.mjs
index 140fd09965..6fb159bff1 100644
--- a/apps/desktop/scripts/smoke-packaged-app.mjs
+++ b/apps/desktop/scripts/smoke-packaged-app.mjs
@@ -145,7 +145,6 @@ async function startSmokeServer({
dataDir,
experiments: {
mobileApp: false,
- providerSessionReaping: false,
},
featureFlags: {
placeholder: false,
diff --git a/apps/desktop/test/preload-build.test.ts b/apps/desktop/test/preload-build.test.ts
index fd3c17762a..55a463ae3e 100644
--- a/apps/desktop/test/preload-build.test.ts
+++ b/apps/desktop/test/preload-build.test.ts
@@ -130,7 +130,6 @@ async function startDesktopSmokeServer(
changelogPreview: false,
editMessages: false,
mobileApp: false,
- providerSessionReaping: false,
timelineWindowing: false,
},
featureFlags: {
diff --git a/apps/host-daemon/src/app.test.ts b/apps/host-daemon/src/app.test.ts
index bc3689a9ea..fae691ca7c 100644
--- a/apps/host-daemon/src/app.test.ts
+++ b/apps/host-daemon/src/app.test.ts
@@ -632,7 +632,6 @@ describe("createHostDaemonApp", () => {
const reaper = startIdleProviderSessionReaper({
logger,
nowMs: () => nowMs,
- resolveProviderSessionReapingEnabled: async () => true,
runtimeManager: {
reapIdleProviderSessions,
},
@@ -648,7 +647,6 @@ describe("createHostDaemonApp", () => {
expect(reapIdleProviderSessions).toHaveBeenNthCalledWith(1, {
idleForMs: 1_800_000,
nowMs: 1_000,
- providerSessionReapingEnabled: true,
});
nowMs = 2_000;
@@ -688,7 +686,6 @@ describe("createHostDaemonApp", () => {
expect(reapIdleProviderSessions).toHaveBeenNthCalledWith(2, {
idleForMs: 1_800_000,
nowMs: 2_000,
- providerSessionReapingEnabled: true,
});
expect(logger.warn).toHaveBeenCalledWith(
{
diff --git a/apps/host-daemon/src/app.ts b/apps/host-daemon/src/app.ts
index 2a62a650c4..b2ae0fd264 100644
--- a/apps/host-daemon/src/app.ts
+++ b/apps/host-daemon/src/app.ts
@@ -97,7 +97,6 @@ interface IdleProviderSessionReaperRuntimeManager {
interface StartIdleProviderSessionReaperArgs {
logger: HostDaemonLogger;
nowMs: () => number;
- resolveProviderSessionReapingEnabled: () => Promise;
runtimeManager: IdleProviderSessionReaperRuntimeManager;
setIntervalFn: IdleProviderSessionReaperIntervalFn;
}
@@ -161,22 +160,11 @@ export function startIdleProviderSessionReaper(
return;
}
running = true;
- void args
- .resolveProviderSessionReapingEnabled()
- .catch((error) => {
- args.logger.warn(
- { ...runtimeErrorLogFields(error) },
- "Failed to read idle provider session experiment policy",
- );
- return false;
+ void args.runtimeManager
+ .reapIdleProviderSessions({
+ idleForMs: IDLE_PROVIDER_SESSION_REAP_AFTER_MS,
+ nowMs: args.nowMs(),
})
- .then((providerSessionReapingEnabled) =>
- args.runtimeManager.reapIdleProviderSessions({
- idleForMs: IDLE_PROVIDER_SESSION_REAP_AFTER_MS,
- nowMs: args.nowMs(),
- providerSessionReapingEnabled,
- }),
- )
.then((result) => {
if (result.reapedSessions.length === 0) {
return;
@@ -699,8 +687,6 @@ export async function createHostDaemonApp(
const idleProviderSessionReaper = startIdleProviderSessionReaper({
logger: options.logger,
nowMs: Date.now,
- resolveProviderSessionReapingEnabled: async () =>
- (await serverClient.getRuntimePolicy()).providerSessionReaping,
runtimeManager,
setIntervalFn: (callback, intervalMs) => {
const timer = setInterval(callback, intervalMs);
diff --git a/apps/host-daemon/src/runtime-manager.test.ts b/apps/host-daemon/src/runtime-manager.test.ts
index dc0e9d83dc..b46a2675b9 100644
--- a/apps/host-daemon/src/runtime-manager.test.ts
+++ b/apps/host-daemon/src/runtime-manager.test.ts
@@ -421,7 +421,6 @@ describe("RuntimeManager", () => {
manager.reapIdleProviderSessions({
idleForMs: 1_000,
nowMs: 5_000,
- providerSessionReapingEnabled: false,
}),
).resolves.toEqual({
reapedSessions: [
@@ -444,13 +443,11 @@ describe("RuntimeManager", () => {
expect(firstRuntime.reapIdleProviderSessions).toHaveBeenCalledWith({
idleForMs: 1_000,
nowMs: 5_000,
- providerSessionReapingEnabled: false,
runThreadExclusive: expect.any(Function),
});
expect(secondRuntime.reapIdleProviderSessions).toHaveBeenCalledWith({
idleForMs: 1_000,
nowMs: 5_000,
- providerSessionReapingEnabled: false,
runThreadExclusive: expect.any(Function),
});
});
@@ -486,7 +483,6 @@ describe("RuntimeManager", () => {
const result = await manager.reapIdleProviderSessions({
idleForMs: 1_000,
nowMs: 5_000,
- providerSessionReapingEnabled: true,
});
expect(result.reapedSessions).toEqual([]);
diff --git a/apps/host-daemon/src/runtime-manager.ts b/apps/host-daemon/src/runtime-manager.ts
index a0cd4d5440..9f8feccf27 100644
--- a/apps/host-daemon/src/runtime-manager.ts
+++ b/apps/host-daemon/src/runtime-manager.ts
@@ -225,7 +225,6 @@ export interface RuntimeManagerOptions {
export interface RuntimeManagerReapIdleProviderSessionsArgs {
idleForMs: number;
nowMs: number;
- providerSessionReapingEnabled: boolean;
}
interface RuntimeManagerReapedIdleProviderSession extends ReapedIdleProviderSession {
diff --git a/apps/host-daemon/src/server-client.test.ts b/apps/host-daemon/src/server-client.test.ts
index e4647ee52b..5f3ee36611 100644
--- a/apps/host-daemon/src/server-client.test.ts
+++ b/apps/host-daemon/src/server-client.test.ts
@@ -42,27 +42,6 @@ function createInteractiveRequest(): PendingInteractionCreate {
}
describe("createServerClient", () => {
- it("reads the current runtime policy", async () => {
- const fetchFn = vi.fn(async (input, init) => {
- expect(String(input)).toBe(
- "https://bb.example.test/internal/runtime-policy",
- );
- expect(init?.method).toBe("GET");
- return Response.json({ providerSessionReaping: true });
- });
- const client = createServerClient({
- fetchFn,
- getSessionId: () => "session-1",
- hostKey: "host-key",
- logger: createLogger(),
- serverUrl: "https://bb.example.test",
- });
-
- await expect(client.getRuntimePolicy()).resolves.toEqual({
- providerSessionReaping: true,
- });
- });
-
it("narrows a protocol update retry request from error details", async () => {
const fetchFn = vi.fn(async () =>
Response.json(
diff --git a/apps/host-daemon/src/server-client.ts b/apps/host-daemon/src/server-client.ts
index 4382c02bfc..ec4b72daf8 100644
--- a/apps/host-daemon/src/server-client.ts
+++ b/apps/host-daemon/src/server-client.ts
@@ -4,7 +4,6 @@ import {
hostDaemonEventBatchResponseSchema,
hostDaemonInteractiveInterruptResponseSchema,
hostDaemonInteractiveRequestResponseSchema,
- hostDaemonRuntimePolicySchema,
hostDaemonSessionOpenResponseSchema,
hostDaemonSkillTreeSchema,
hostDaemonToolCallResponseSchema,
@@ -17,7 +16,6 @@ import {
type HostDaemonInteractiveInterruptRequest,
type HostDaemonInteractiveRequest,
type HostDaemonLoadedEnvironment,
- type HostDaemonRuntimePolicy,
type HostDaemonProjectAttachmentContentQuery,
type HostDaemonSessionOpenRequest,
type HostDaemonSessionOpenResponse,
@@ -182,7 +180,6 @@ interface OpenSessionArgs {
}
export interface ServerClient {
- getRuntimePolicy(): Promise;
openSession(args: OpenSessionArgs): Promise;
fetchProjectAttachment(
args: FetchProjectAttachmentArgs,
@@ -449,17 +446,6 @@ export function createServerClient(
}
return {
- async getRuntimePolicy(): Promise {
- const response = await fetchFn(buildInternalUrl("/runtime-policy"), {
- method: "GET",
- headers: headers(),
- });
- if (!response.ok) {
- throw await createResponseError("get runtime policy", response);
- }
- return hostDaemonRuntimePolicySchema.parse(await response.json());
- },
-
async openSession(
args: OpenSessionArgs,
): Promise {
diff --git a/apps/host-daemon/src/server-connection.test.ts b/apps/host-daemon/src/server-connection.test.ts
index 9784121a5d..54f520c81e 100644
--- a/apps/host-daemon/src/server-connection.test.ts
+++ b/apps/host-daemon/src/server-connection.test.ts
@@ -84,7 +84,6 @@ function createServerClientFixture(args: CreateServerClientFixtureArgs = {}) {
};
const serverClient = {
openSession,
- getRuntimePolicy: unused,
fetchProjectAttachment: unused,
fetchSkillTree: unused,
fetchPluginHostArtifact: unused,
diff --git a/apps/mobile/e2e/scripts/phase7-settings-reset.js b/apps/mobile/e2e/scripts/phase7-settings-reset.js
index 9e3f1a67f4..25c3af19bf 100644
--- a/apps/mobile/e2e/scripts/phase7-settings-reset.js
+++ b/apps/mobile/e2e/scripts/phase7-settings-reset.js
@@ -8,7 +8,6 @@ const experiments = http.put(`${SERVER_URL}/api/v1/settings/experiments`, {
editMessages: true,
mobileApp: false,
newOnboarding: false,
- providerSessionReaping: false,
}),
});
if (!experiments.ok) {
diff --git a/apps/mobile/src/screens/settings/ExperimentsSettingsScreen.tsx b/apps/mobile/src/screens/settings/ExperimentsSettingsScreen.tsx
index fb4daf300b..5dde909702 100644
--- a/apps/mobile/src/screens/settings/ExperimentsSettingsScreen.tsx
+++ b/apps/mobile/src/screens/settings/ExperimentsSettingsScreen.tsx
@@ -27,12 +27,6 @@ const EXPERIMENT_ROWS: readonly ExperimentRow[] = [
description:
"Pair the bb mobile app over bb connect: shows Add mobile device under Remote access (web and desktop) and enables bb connect machine-code.",
},
- {
- key: "providerSessionReaping",
- label: "Idle provider session release",
- description:
- "Release restorable provider sessions after 30 idle minutes. A change can take up to five minutes.",
- },
];
/** `/settings/experiments`: the server-persisted opt-in toggles (`PUT /settings/experiments`). */
diff --git a/apps/server/src/internal/session.ts b/apps/server/src/internal/session.ts
index 1d6d728d65..40da66b19c 100644
--- a/apps/server/src/internal/session.ts
+++ b/apps/server/src/internal/session.ts
@@ -1,6 +1,5 @@
import {
getLatestSessionForHost,
- getExperiments,
listRetiredLoadedEnvironmentIdsOnHost,
openSession,
upsertHost,
@@ -37,13 +36,6 @@ export function registerInternalSessionRoutes(
onValidationError: (msg) => new ApiError(400, "invalid_request", msg),
});
- get("/runtime-policy", (context) => {
- getAuthenticatedDaemon(context);
- return context.json({
- providerSessionReaping: getExperiments(deps.db).providerSessionReaping,
- });
- });
-
post(
"/session/open",
hostDaemonSessionOpenRequestSchema,
diff --git a/apps/server/test/system/experiments.test.ts b/apps/server/test/system/experiments.test.ts
index 23748a98e8..7592c55307 100644
--- a/apps/server/test/system/experiments.test.ts
+++ b/apps/server/test/system/experiments.test.ts
@@ -3,8 +3,6 @@ import { getExperiments } from "@bb/db";
import { experimentsSchema } from "@bb/domain";
import { systemConfigResponseSchema } from "@bb/server-contract";
import { readJson } from "../helpers/json.js";
-import { internalAuthHeaders } from "../helpers/commands.js";
-import { seedHostSession } from "../helpers/seed.js";
import { withTestHarness } from "../helpers/test-app.js";
describe("experiments settings", () => {
@@ -17,7 +15,6 @@ describe("experiments settings", () => {
changelogPreview: false,
editMessages: true,
mobileApp: false,
- providerSessionReaping: false,
timelineWindowing: false,
});
});
@@ -32,7 +29,6 @@ describe("experiments settings", () => {
changelogPreview: true,
editMessages: true,
mobileApp: true,
- providerSessionReaping: true,
timelineWindowing: true,
}),
});
@@ -41,14 +37,12 @@ describe("experiments settings", () => {
changelogPreview: true,
editMessages: true,
mobileApp: true,
- providerSessionReaping: true,
timelineWindowing: true,
});
expect(getExperiments(harness.db)).toEqual({
changelogPreview: true,
editMessages: true,
mobileApp: true,
- providerSessionReaping: true,
timelineWindowing: true,
});
@@ -59,46 +53,11 @@ describe("experiments settings", () => {
changelogPreview: true,
editMessages: true,
mobileApp: true,
- providerSessionReaping: true,
timelineWindowing: true,
});
});
});
- it("serves the current provider session policy to the daemon", async () => {
- await withTestHarness(async (harness) => {
- const { host } = seedHostSession(harness.deps, {
- id: "host-runtime-policy",
- });
- const headers = internalAuthHeaders(harness, { hostId: host.id });
-
- const initial = await harness.app.request("/internal/runtime-policy", {
- headers,
- });
- expect(initial.status).toBe(200);
- await expect(readJson(initial)).resolves.toEqual({
- providerSessionReaping: false,
- });
- await harness.app.request("/api/v1/settings/experiments", {
- method: "PUT",
- headers: { "content-type": "application/json" },
- body: JSON.stringify({
- changelogPreview: false,
- editMessages: true,
- mobileApp: false,
- providerSessionReaping: true,
- timelineWindowing: false,
- }),
- });
- const updated = await harness.app.request("/internal/runtime-policy", {
- headers,
- });
- await expect(readJson(updated)).resolves.toEqual({
- providerSessionReaping: true,
- });
- });
- });
-
it("does not expose legacy direct bb connect routes", async () => {
await withTestHarness(async (harness) => {
const disabled = await harness.app.request("/api/v1/connect/status");
@@ -111,7 +70,6 @@ describe("experiments settings", () => {
changelogPreview: false,
editMessages: false,
mobileApp: false,
- providerSessionReaping: false,
timelineWindowing: false,
}),
});
diff --git a/docs/configuration.md b/docs/configuration.md
index 29b9f26907..2f57948956 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -670,12 +670,9 @@ The `mobileApp` experiment turns on pairing for the bb mobile app: the
`bb connect machine-code` command (see "Pairing the bb mobile app" above). It
is off by default while the app is in early access.
-The `providerSessionReaping` experiment extends idle session release to every
-restorable provider. BB releases those sessions after 30 idle minutes. The
-daemon reads the setting before each five-minute maintenance pass. Active
-turns, commands, agents, workflows, and monitors keep their sessions loaded.
-The experiment does not gate release: BB releases idle Codex sessions with the
-experiment off, which is the behavior it had before this setting.
+BB releases idle restorable provider sessions after 30 minutes. The daemon
+checks every five minutes. Active turns, commands, agents, workflows, and
+monitors keep their sessions loaded.
The `timelineWindowing` experiment is off by default. When enabled, long
timelines and large expanded timeline details retain stable height-preserving
diff --git a/packages/agent-runtime/src/runtime.process-lifecycle.test.ts b/packages/agent-runtime/src/runtime.process-lifecycle.test.ts
index b8f8066d0d..220bf722ab 100644
--- a/packages/agent-runtime/src/runtime.process-lifecycle.test.ts
+++ b/packages/agent-runtime/src/runtime.process-lifecycle.test.ts
@@ -1506,7 +1506,6 @@ rl.on("line", (line) => {
const result = await runtime.reapIdleProviderSessions({
idleForMs: 0,
nowMs: Date.now(),
- providerSessionReapingEnabled: false,
});
expect(result.reapedSessions).toEqual([
@@ -1586,7 +1585,6 @@ rl.on("line", (line) => {
const belowThresholdResult = await runtime.reapIdleProviderSessions({
idleForMs: 30 * 60 * 1000,
nowMs: Date.now() + 29 * 60 * 1000,
- providerSessionReapingEnabled: false,
});
expect(belowThresholdResult.reapedSessions).toEqual([]);
expect(runtime.hasThread("t1")).toBe(true);
@@ -1597,7 +1595,6 @@ rl.on("line", (line) => {
const result = await runtime.reapIdleProviderSessions({
idleForMs: 30 * 60 * 1000,
nowMs: Date.now() + 31 * 60 * 1000,
- providerSessionReapingEnabled: false,
});
const reapedSession = result.reapedSessions[0];
if (!reapedSession) {
@@ -1712,12 +1709,10 @@ rl.on("line", (line) => {
const firstResult = await runtime.reapIdleProviderSessions({
idleForMs: 0,
nowMs: Date.now() + 60 * 60 * 1000,
- providerSessionReapingEnabled: false,
});
const secondResult = await runtime.reapIdleProviderSessions({
idleForMs: 0,
nowMs: Date.now() + 60 * 60 * 1000,
- providerSessionReapingEnabled: false,
});
expect(firstResult.reapedSessions).toEqual([]);
@@ -1737,7 +1732,7 @@ rl.on("line", (line) => {
// sessions reapable is the `sessionRestorable` its bridge reports on
// thread/start. If that wire field stopped being read, idle release would
// silently stop for every graduated provider.
- it("reaps a restorable non-Codex session only when the experiment is on", async () => {
+ it("reaps a restorable non-Codex session", async () => {
const providerScript = join(tmpDir, "claude-idle-reaper-provider.cjs");
writeThreadScopedProviderScript({
logPath: join(tmpDir, "claude-idle-reaper-provider.log"),
@@ -1766,19 +1761,9 @@ rl.on("line", (line) => {
options: fullRuntimeOptions,
});
- await expect(
- runtime.reapIdleProviderSessions({
- idleForMs: 0,
- nowMs: Date.now(),
- providerSessionReapingEnabled: false,
- }),
- ).resolves.toEqual({ reapedSessions: [] });
- expect(runtime.hasThread("t1")).toBe(true);
-
const result = await runtime.reapIdleProviderSessions({
idleForMs: 0,
nowMs: Date.now(),
- providerSessionReapingEnabled: true,
});
expect(result.reapedSessions).toEqual([
expect.objectContaining({
@@ -1827,7 +1812,6 @@ rl.on("line", (line) => {
const result = await runtime.reapIdleProviderSessions({
idleForMs: 0,
nowMs: Date.now(),
- providerSessionReapingEnabled: true,
});
expect(result.reapedSessions).toEqual([
diff --git a/packages/agent-runtime/src/runtime.ts b/packages/agent-runtime/src/runtime.ts
index 76bd778d05..ed297f19d8 100644
--- a/packages/agent-runtime/src/runtime.ts
+++ b/packages/agent-runtime/src/runtime.ts
@@ -134,7 +134,6 @@ interface ReapIdleProviderSessionCandidate {
interface FindReapableIdleProviderSessionArgs {
idleForMs: number;
nowMs: number;
- providerSessionReapingEnabled: boolean;
threadId: string;
}
@@ -422,7 +421,7 @@ export function createAgentRuntimeWithAdapters(
* Codex runs one provider process per thread. The codex bridge now owns a
* per-thread `codex app-server` child internally, so this outer scoping is
* redundant for isolation — but it is still load-bearing: the account
- * restart below and the pre-experiment idle reap both key off
+ * restart below and the legacy Codex idle-reap fallback both key off
* `isThreadScopedCodexProcess`. Collapsing it means routing those through
* `thread/stop {release}` + resume, which is a refactor, not a deletion.
*/
@@ -758,12 +757,8 @@ export function createAgentRuntimeWithAdapters(
const runtimeConfig = threadRuntimeConfigs.get(args.threadId);
if (
!runtimeConfig ||
- // The experiment extends release to every restorable provider. It does
- // not gate release: Codex idle sessions are released without it, which
- // is the behavior BB shipped before the experiment.
- (args.providerSessionReapingEnabled
- ? !runtimeConfig.sessionRestorable
- : runtimeConfig.providerId !== CODEX_PROVIDER_ID)
+ (runtimeConfig.providerId !== CODEX_PROVIDER_ID &&
+ !runtimeConfig.sessionRestorable)
) {
return null;
}
@@ -2430,19 +2425,13 @@ export function createAgentRuntimeWithAdapters(
return threadIdentityRegistry.getProviderSession(threadId);
},
- async reapIdleProviderSessions({
- idleForMs,
- nowMs,
- providerSessionReapingEnabled,
- runThreadExclusive,
- }) {
+ async reapIdleProviderSessions({ idleForMs, nowMs, runThreadExclusive }) {
const reapedSessions: ReapedIdleProviderSession[] = [];
for (const threadId of [...threadRuntimeConfigs.keys()]) {
const release = async (): Promise => {
const candidate = findReapableIdleProviderSession({
idleForMs,
nowMs,
- providerSessionReapingEnabled,
threadId,
});
if (!candidate) {
@@ -2458,15 +2447,17 @@ export function createAgentRuntimeWithAdapters(
} catch {
return null;
}
+ const usesCodexRestoreFallback =
+ candidate.runtimeConfig.providerId === CODEX_PROVIDER_ID &&
+ !candidate.runtimeConfig.sessionRestorable;
if (
- providerSessionReapingEnabled
- ? backgroundWorkState.hasOpenThreadWork(candidate.threadId) ||
- (proc.adapter.hasOpenThreadWork?.({
- providerThreadId: candidate.providerThreadId,
- threadId: candidate.threadId,
- }) ??
- false)
- : !isThreadScopedCodexProcess(proc)
+ backgroundWorkState.hasOpenThreadWork(candidate.threadId) ||
+ (proc.adapter.hasOpenThreadWork?.({
+ providerThreadId: candidate.providerThreadId,
+ threadId: candidate.threadId,
+ }) ??
+ false) ||
+ (usesCodexRestoreFallback && !isThreadScopedCodexProcess(proc))
) {
return null;
}
diff --git a/packages/agent-runtime/src/types.ts b/packages/agent-runtime/src/types.ts
index 6919b9091f..59bb97fa06 100644
--- a/packages/agent-runtime/src/types.ts
+++ b/packages/agent-runtime/src/types.ts
@@ -308,7 +308,6 @@ export interface WaitForActiveTurnArgs {
export interface ReapIdleProviderSessionsArgs {
idleForMs: number;
nowMs: number;
- providerSessionReapingEnabled: boolean;
runThreadExclusive?: (
threadId: string,
work: () => Promise,
diff --git a/packages/db/test/experiments.test.ts b/packages/db/test/experiments.test.ts
index b4bb1d0ad3..f923025f3c 100644
--- a/packages/db/test/experiments.test.ts
+++ b/packages/db/test/experiments.test.ts
@@ -39,7 +39,6 @@ describe("experiments", () => {
"editMessages",
"futureExperiment",
"mobileApp",
- "providerSessionReaping",
"timelineWindowing",
]);
} finally {
diff --git a/packages/domain/src/experiments.ts b/packages/domain/src/experiments.ts
index be91da5eac..5e36061382 100644
--- a/packages/domain/src/experiments.ts
+++ b/packages/domain/src/experiments.ts
@@ -14,7 +14,6 @@ export const experimentKeys = [
"changelogPreview",
"editMessages",
"mobileApp",
- "providerSessionReaping",
"timelineWindowing",
] as const;
export const experimentKeySchema = z.enum(experimentKeys);
@@ -31,6 +30,5 @@ export const defaultExperiments: Experiments = {
changelogPreview: false,
editMessages: true,
mobileApp: false,
- providerSessionReaping: false,
timelineWindowing: false,
};
diff --git a/packages/host-daemon-contract/src/protocol.ts b/packages/host-daemon-contract/src/protocol.ts
index a0b3c4de9d..82ee79ed28 100644
--- a/packages/host-daemon-contract/src/protocol.ts
+++ b/packages/host-daemon-contract/src/protocol.ts
@@ -1,3 +1,8 @@
+// Version 147 makes idle release standard for every restorable provider and
+// removes the server-owned runtime-policy endpoint. Older daemons still request
+// that endpoint before each maintenance sweep and skip non-Codex release when
+// it is unavailable, so enrolled machines must update for the new policy.
+//
// Version 146 adds the lightweight `host.list_branch_options` RPC so branch
// pickers can read cached refs while the daemon refreshes remotes in the
// background. Older daemons cannot parse or serve that command.
@@ -110,7 +115,7 @@
//
// The version mismatch is what triggers the enrolled daemon's automatic update
// instead of an `invalid-message` reconnect loop.
-export const HOST_DAEMON_PROTOCOL_VERSION = 146 as const;
+export const HOST_DAEMON_PROTOCOL_VERSION = 147 as const;
/**
* Absolute ceiling for any executable artifact delivered to a host daemon —
diff --git a/packages/host-daemon-contract/src/session.ts b/packages/host-daemon-contract/src/session.ts
index dfea877ac2..de124d3b0d 100644
--- a/packages/host-daemon-contract/src/session.ts
+++ b/packages/host-daemon-contract/src/session.ts
@@ -47,15 +47,6 @@ export type HostDaemonLoadedEnvironment = z.infer<
typeof hostDaemonLoadedEnvironmentSchema
>;
-export const hostDaemonRuntimePolicySchema = z
- .object({
- providerSessionReaping: z.boolean(),
- })
- .strict();
-export type HostDaemonRuntimePolicy = z.infer<
- typeof hostDaemonRuntimePolicySchema
->;
-
const hostDaemonWatchSetWorkspaceTargetSchema = z
.object({
environmentId: z.string().min(1),
@@ -823,10 +814,6 @@ export const hostDaemonSkillTreeSchema = z
export type HostDaemonSkillTree = z.infer;
export type HostDaemonInternalSchema = {
- "/runtime-policy": {
- /** Returns current server-owned runtime policy before a daemon maintenance sweep. */
- $get: Endpoint, HostDaemonRuntimePolicy, 200>;
- };
"/skills/tree/:hash": {
/** Used by the daemon to pull a missing server-owned injected skill tree. */
$get: Endpoint, HostDaemonSkillTree, 200>;
diff --git a/packages/host-daemon-contract/test/contract.test.ts b/packages/host-daemon-contract/test/contract.test.ts
index 893dcc6006..5cd4099623 100644
--- a/packages/host-daemon-contract/test/contract.test.ts
+++ b/packages/host-daemon-contract/test/contract.test.ts
@@ -1123,7 +1123,7 @@ describe("host-daemon command schemas", () => {
// mixed version. Version 113 carried the Devin Desktop open target rename
// and remains part of the protocol lineage.
it("uses the current host-daemon protocol version", () => {
- expect(HOST_DAEMON_PROTOCOL_VERSION).toBe(146);
+ expect(HOST_DAEMON_PROTOCOL_VERSION).toBe(147);
expect(HOST_ARTIFACT_MAX_BYTES).toBe(256 * 1024 * 1024);
});
diff --git a/packages/templates/src/templates/bb-guide-customization.md b/packages/templates/src/templates/bb-guide-customization.md
index 5d6a8d2d13..50dd2d0234 100644
--- a/packages/templates/src/templates/bb-guide-customization.md
+++ b/packages/templates/src/templates/bb-guide-customization.md
@@ -114,11 +114,9 @@ client-local; submitting stops and settles a running thread, then replaces the
selected turn and all later conversation history while retaining workspace side
effects. Grouped multi-message requests are not yet editable.
-The default-off `providerSessionReaping` experiment extends idle release to
-every restorable provider. BB releases those sessions after 30 idle minutes.
-The daemon applies a changed value within five minutes. Active turns, commands,
-agents, workflows, and monitors keep their sessions loaded. BB releases idle
-Codex sessions with the experiment off as well.
+BB releases idle restorable provider sessions after 30 minutes. The daemon
+checks every five minutes. Active turns, commands, agents, workflows, and
+monitors keep their sessions loaded.
The default-off `timelineWindowing` experiment mounts only nearby rows in long
timelines and large expanded timeline details. Enable it with
diff --git a/plans/bb-mobile-research/settings-features.md b/plans/bb-mobile-research/settings-features.md
index 87042759e7..997dea133b 100644
--- a/plans/bb-mobile-research/settings-features.md
+++ b/plans/bb-mobile-research/settings-features.md
@@ -5,7 +5,7 @@ Settings buckets are declared in `apps/app/src/components/settings/settings-nav.
### Server-persisted (visible to any client)
All served by `GET /system/config` (`packages/server-contract/src/api/system.ts:197-233`) and written by `PUT /settings/general|keyboard|experiments|appearance` (`packages/server-contract/src/public-api.ts:1332-1358`; SDK `packages/sdk/src/areas/system.ts:207-217`, `theme.ts:53`):
- `AppSettings` (`packages/domain/src/app-settings.ts:7-51`): showKeyboardHints, steerActiveThreadOnEnter, showUnhandledProviderEvents, codexMemoryEnabled, claudeCodeMemoryEnabled, codexSubagentsDisabled, claudeCodeSubagentsDisabled, claudeCodeWorkflowsDisabled, onboardingCompletedAt. Used by General (`SettingsView.tsx:1256-1299`), Provider pages (`:930-991, 1107-1150`), Debug (`:903-917`).
-- Experiments (`packages/domain/src/experiments.ts:13-34`): claudeCodeMockCliTraffic, editMessages, newOnboarding, providerSessionReaping (`SettingsView.tsx:998-1066`).
+- Experiments (`packages/domain/src/experiments.ts`): changelogPreview, editMessages, mobileApp, timelineWindowing.
- Appearance palette + favicon color (`packages/domain/src/app-theme.ts:122-134, 145-160`); custom themes are server-resolved CSS strings; built-ins are CSS-with-`color-mix` in `apps/app/src/lib/themes/*.ts` (e.g. `nord.ts:9-45`).
- Keybinding overrides (`packages/domain/src/app-keybindings.ts:232-249`; `KeyboardSettingsSection.tsx`, uses `navigator.platform` at `:63-65`).
- Hosts: `GET/PATCH/DELETE /hosts/:id`, `PATCH /hosts/:id/permission-ceiling`, `POST /hosts/:id/retry-update`, `POST /hosts/join-codes` (`packages/server-contract/src/api/hosts.ts:59-100`; `MachinesSettingsSection.tsx:200-375`; `MachineSettingsView.tsx:165-471`).
@@ -160,4 +160,4 @@ Later: onboarding flow, keyboard shortcut editor, file openers/thread-list-provi
- Is push notification delivery in scope (would require new server/connect infra keyed off /system/attention or interactions-changed events)?
- Should client-local preferences (light/dark, rewrite localhost links, rich text editing, navigate-after-create) be promoted to server-persisted AppSettings so web/desktop/native stay in sync?
- Should partysocket be reused in RN or replaced with a purpose-built reconnecting WebSocket that handles AppState transitions?
-- Onboarding requires a primary host and the newOnboarding experiment — should native skip onboarding entirely or provide a connect-based 'pair to your server' first-run instead?
\ No newline at end of file
+- Onboarding requires a primary host and the newOnboarding experiment — should native skip onboarding entirely or provide a connect-based 'pair to your server' first-run instead?