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?