diff --git a/.changeset/ground-voice-in-fast.md b/.changeset/ground-voice-in-fast.md deleted file mode 100644 index 982fe40eda..0000000000 --- a/.changeset/ground-voice-in-fast.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@roomote/web": patch ---- - -Route every voice utterance through Fast so product answers use the Session's tools, context, and safeguards instead of unverified direct voice output. diff --git a/.changeset/guide-select-focus-handoffs.md b/.changeset/guide-select-focus-handoffs.md deleted file mode 100644 index 08af8ca039..0000000000 --- a/.changeset/guide-select-focus-handoffs.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@roomote/web": patch ---- - -Move focus into required follow-up fields and dependent selectors after committed choices in guided setup flows. diff --git a/.changeset/quiet-drafts-stay-put.md b/.changeset/quiet-drafts-stay-put.md deleted file mode 100644 index 9713777be7..0000000000 --- a/.changeset/quiet-drafts-stay-put.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@roomote/web": patch ---- - -Keep pull request metadata refreshes state-neutral and distinguish later opt-in clean-review promotion from the refresh itself. diff --git a/.changeset/unify-skills-settings.md b/.changeset/unify-skills-settings.md deleted file mode 100644 index 577f59b404..0000000000 --- a/.changeset/unify-skills-settings.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@roomote/web": patch ---- - -Unify shared and environment-specific skills in one searchable Settings catalog with availability filters and a marketplace dialog. diff --git a/CHANGELOG.md b/CHANGELOG.md index ed2b670aa2..cdf7f22009 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,59 @@ This file tracks product releases for Roomote (single monorepo version). Automated release entries are prepended by `pnpm run version`. +## 1.7.0 (2026-09-11) + +Roomote 1.7 adds natural voice and email conversations, smarter follow-through for delegated work, richer automation controls, and clearer usage insights. + +### Highlights + +- Talk with Fast Sessions through configurable GPT-Live voices and previews, or continue Roomote work and private automation reports over email with AgentMail. +- Let Fast quietly follow delegated coding tasks, surface meaningful developments, and correct work that drifts from the request. +- Add natural-language routing rules to built-in automations and manage existing pull requests across supported providers. +- Track token usage alongside spend in Cost Analytics, including historical totals when tasks are deleted. + +### Minor changes + +- Use Email through AgentMail as a replyable Roomote channel for task requests and outcome-first custom automation reports, with inbox-scoped setup, durable threads, signed answers, verified-account safeguards, and verification status and resend controls in Linked Accounts. +- Built-in automations can use natural-language Additional rules for repository scope, per-repository routing, and report guidance. +- Track token usage alongside spend in Cost Analytics with a total-token trend line, provider and model totals and averages, and token-aware drilldowns. +- DeepSeek V4.1 Flash replaces the dated Flash recommendation across OpenRouter, Vercel AI Gateway, and OpenCode Go. +- Fast Sessions quietly follow launched coding tasks, report meaningful developments, apply evidence-backed corrections until work settles, and distinguish task-history checks from incoming task reports. +- Agents can close, reopen, retarget, and edit existing pull requests across supported source-control providers without creating replacements. +- Weekly Manager Stats includes a daily created-versus-merged pull request chart and starts enabled on new deployments once a destination is configured. +- Unify shared and environment-specific skills in one searchable Settings catalog with availability filters and a marketplace dialog. +- Talk naturally with Roomote from Fast Sessions, with deployment-wide voice selection and previews while GPT-Live handles audio and Fast handles every utterance with the Session's model, tools, and context. + +### Patch changes + +- Custom automation task models can be changed only by the automation owner or an administrator. +- Built-in automation switches clearly show whether each automation is enabled or off. +- Saved automation cards show cadence in the deployment scheduling timezone used for execution. +- Fast Sessions can manage reminders and automations directly again without a scheduling-discovery step. +- Fast Sessions can use deployment-wide custom remote MCP integrations without exposing upstream credentials. +- Fast Sessions honor saved Routing Rules when selecting an environment or delegated coding-task model. +- Source-control and Linear webhook redeliveries no longer risk repeating actions when the original audit outcome was not finalized. +- Route every voice utterance through Fast so product answers use the Session's tools, context, and safeguards instead of unverified direct voice output. +- Move focus into required follow-up fields and dependent selectors after committed choices in guided setup flows. +- Blank slate tasks no longer expose internal repository sentinels in task headers and filters. +- Independent Slack suggested tasks reliably start in separate execution threads and Sessions instead of sharing task context; rejected launches clean up misleading start messages and remain available to retry. +- Communication replies use compact live footers that keep running-task counts, pull request links, and Session links current as work starts and finishes. +- Preview Markdown artifacts directly in task and Session galleries before opening them. +- Coding tasks recover from exhausted transient OpenCode connection resets instead of stopping without a useful Session handoff. +- Stopping a coding task from a Session preserves its task, sandbox, and artifacts for later resumption instead of terminally cancelling it. +- Preview status and failure pages provide consistent responsive branding, explanations, and recovery actions across desktop and mobile. +- Keep pull request metadata refreshes state-neutral and distinguish later opt-in clean-review promotion from the refresh itself. +- Custom automation cards refresh persisted results after Run now without requiring a page reload. +- Slack requests no longer receive a redundant eyes reaction while Roomote is working. +- Cost Analytics keeps historical spend and token totals when tasks are deleted while hiding deleted-task details. +- Connected Sentry integrations expose the full admin-approved tool catalog instead of a stale static allowlist. +- Open a user's Sessions directly from their avatar in Session viewers and task messages. +- Session timer activity uses user-facing labels instead of exposing internal wakeup tool names. +- Automatic task follow-through timers stay out of the Session reminder list while user-created timer receipts clearly describe each action and reused schedule. +- Automations and Skills page controls stay visible on desktop while long settings lists scroll independently. +- The Automations list combines built-in and custom entries with consistent search, columns, responsive text, and alphabetical ordering. +- Voice transcripts no longer show pending spoken acknowledgements after a call ends when the associated request was never sent. + ## 1.6.0 (2026-09-09) Roomote 1.6 adds flexible sandbox and public-repository work, native conversation charts, leaner scheduling discovery, and more reliable Sessions and skill management. diff --git a/apps/docs/communications.mdx b/apps/docs/communications.mdx index 973a062df4..2682a74039 100644 --- a/apps/docs/communications.mdx +++ b/apps/docs/communications.mdx @@ -23,7 +23,7 @@ attention. | | Teams that use Microsoft 365, Teams channels, and Microsoft Entra accounts | Microsoft can also be used for sign-in and account linking. | | | Lightweight personal, group, or forum-topic chat access | Telegram is a messaging provider, not a sign-in provider. | | | Communities and teams that organize work in channels, threads, and forums | Discord is a messaging provider, not a sign-in provider. | -| | Low-frequency task requests and results over email | Email conversations are replyable in both directions; not a sign-in provider. | +| | Low-frequency task requests and results over email | Email conversations are replyable in both directions; not a sign-in provider. | Configure communications providers from **Settings > Communications**. diff --git a/apps/docs/cost-analytics.mdx b/apps/docs/cost-analytics.mdx index 8e89a03eb8..35a0672a66 100644 --- a/apps/docs/cost-analytics.mdx +++ b/apps/docs/cost-analytics.mdx @@ -5,9 +5,9 @@ description: Review Roomote inference spend by type, source, environment, provid --- Cost Analytics helps deployment teams understand how Roomote uses inference -across tasks, Fast sessions, and automations. It reports the cost of -recorded model usage in US dollars, so you can spot the environments, models, -and work types that drive spend. +across tasks, Fast sessions, and automations. It reports cost in US dollars and +token usage, so you can spot the environments, models, and work types that drive +spend or high-volume inference. Any signed-in deployment user can open **Analytics** from the dashboard. The page opens to **Costs** by default. @@ -23,6 +23,7 @@ recorded inference usage. Use it to answer questions such as: - whether a particular environment or model is driving costs - how usage differs between people and automations - how a provider's cost changes over time +- how token volume changes even when a request has no recorded price The view starts grouped by **Type**. It distinguishes manual and automated tasks, **Session** orchestration, **Memories** synthesis, and other **Non-task @@ -34,6 +35,11 @@ model and provider. The source also appears in the detail drilldown. Choose a time range before comparing periods so the chart and breakdown use the same window. +The chart overlays a **Total tokens** trend line on the cost bars. Its tooltip +shows token and cost totals for the same day and grouping. The breakdown table +includes **Total tokens** and **Avg tokens / task** alongside cost. Selecting a +chart segment opens the matching token-aware detail rows. + Analytics date presets and daily chart buckets use the web server's timezone, including daylight-saving transitions. Costs, Tasks, and PR analytics use the same calendar-day boundaries; these are not based on each viewer's browser @@ -52,9 +58,11 @@ providers. It is separate from [Anonymous Analytics](/anonymous-analytics): cost data stays inside your deployment for your team to review, while anonymous analytics is optional product telemetry sent to the Roomote team. -If a provider does not report pricing or usage for a request, that request -cannot contribute a cost to the view. Use provider and model filters to narrow -an investigation when totals do not match an external provider invoice. +If a provider reports tokens but no price for a request, that activity appears +in token totals even though it contributes no cost. A request with neither +pricing nor token usage cannot contribute to those metrics. Use provider and +model filters to narrow an investigation when totals do not match an external +provider invoice. Deleting a task does not remove its recorded inference usage from Cost Analytics. The historical usage still contributes to totals, task counts, and diff --git a/apps/docs/environments/definition.mdx b/apps/docs/environments/definition.mdx index a8b40594c4..255daef38f 100644 --- a/apps/docs/environments/definition.mdx +++ b/apps/docs/environments/definition.mdx @@ -381,11 +381,14 @@ repository has a known trap, state it plainly. Admins can configure natural-language routing rules from **Settings → Environments**. Each rule maps a description, such as “Messages sent in the hospital-bugs Slack channel,” to an environment or the broad **All repositories** -workspace. +workspace. A rule can also recommend a coding model for work that Fast delegates. Rules are guidance rather than exact string matchers. An environment explicitly named in a request takes precedence, and a specific rule takes precedence over a -catch-all default. +catch-all default. Fast evaluates the saved rules when it chooses an environment +or model for a delegated task; they do not override a model or environment the +user explicitly requested. Rules that point to a deleted environment are ignored +until an admin updates or removes them. ## MCP servers diff --git a/apps/docs/fast-sessions.mdx b/apps/docs/fast-sessions.mdx index c709965369..46fc917286 100644 --- a/apps/docs/fast-sessions.mdx +++ b/apps/docs/fast-sessions.mdx @@ -141,6 +141,12 @@ reply in the transcript. Delegated tasks also report their completion, blockers, and relevant results back into the parent Session; open the task workspace when you need its full logs or diff. +Fast quietly follows the coding tasks it launches after the conversation becomes +idle. It reports meaningful progress, blockers, and questions that need input, +and can send an evidence-backed correction when a task is drifting from the +request. Routine status stays quiet. Follow-through stops when the Session's +coding work settles instead of leaving a recurring monitor behind. + **Session info** shows the total inference cost for the conversation and its attached tasks. Open the cost breakdown to separate direct Session orchestration from each task's usage. diff --git a/apps/docs/integrations/custom-mcp-servers.mdx b/apps/docs/integrations/custom-mcp-servers.mdx index 204ad10c5c..c3daee5a61 100644 --- a/apps/docs/integrations/custom-mcp-servers.mdx +++ b/apps/docs/integrations/custom-mcp-servers.mdx @@ -5,8 +5,9 @@ description: 'Connect MCP servers that are not in the built-in catalog' Custom MCP servers let agents use services that Roomote does not ship a built-in integration for. Deployment admins configure them under -**Settings → Integrations → Custom MCP Servers**, and they become available to -agents in every task. +**Settings → Integrations → Custom MCP Servers**. Remote servers become +available to both Fast Sessions and sandbox tasks. Local stdio servers need a +task sandbox because Roomote launches their process inside that workspace. Most MCP servers publish a JSON snippet in their docs. The add dialog's **Import from JSON** button accepts a pasted `mcpServers` snippet (or a bare @@ -27,7 +28,7 @@ Three kinds of servers are supported: ## How remote servers are reached Remote custom servers are never handed to the task sandbox directly. The -sandbox sees an authenticated Roomote proxy URL, and the proxy injects the +sandbox and Fast use an authenticated Roomote proxy, and the proxy injects the real credentials per request. This means: - Header values and OAuth tokens never appear in sandbox-readable diff --git a/apps/docs/integrations/index.mdx b/apps/docs/integrations/index.mdx index c4694e85e0..7e0446cf19 100644 --- a/apps/docs/integrations/index.mdx +++ b/apps/docs/integrations/index.mdx @@ -72,7 +72,7 @@ from [Personal Settings](/personal-settings). | | Read-only database access in Supabase | Enable first, then teammates link accounts | | | Shared memory across tasks and Fast sessions | Admin connection once | | | Deployments, logs, and domain availability | Admin connection once | -| | Voice calls with Roomote on Fast Sessions | Admin connection once | +| | Voice calls with Roomote on Fast Sessions | Admin connection once | | | Public X posts, users, trends, and news | Admin connection once | | | Paid external capabilities via Zero | Admin connection once | diff --git a/apps/docs/integrations/voice.mdx b/apps/docs/integrations/voice.mdx index a3ee19fcb2..22338b50a3 100644 --- a/apps/docs/integrations/voice.mdx +++ b/apps/docs/integrations/voice.mdx @@ -1,7 +1,7 @@ --- title: Voice description: Let your team talk to Roomote on a voice call. -icon: 'audio-lines' +icon: 'openai' --- Connect Voice when you want people to [talk to Roomote](/voice) on a call from @@ -29,4 +29,5 @@ its WebRTC connection offer to Roomote and receives only the negotiated answer. Self-hosted operators can instead set `R_VOICE_OPENAI_API_KEY`. When both are present the environment variable is used. Roomote's general `OPENAI_API_KEY` is never used for voice, so enabling OpenAI for task inference does not turn -voice on. +voice on. An environment-managed connection is read-only in Settings and uses +Marin because it has no stored voice selection. diff --git a/apps/docs/logo/integrations/agentmail.svg b/apps/docs/logo/integrations/agentmail.svg index 5a462aa4a6..29cba51e61 100644 --- a/apps/docs/logo/integrations/agentmail.svg +++ b/apps/docs/logo/integrations/agentmail.svg @@ -1,14 +1,6 @@ - - - + + + + + diff --git a/apps/docs/logo/integrations/voice.svg b/apps/docs/logo/integrations/voice.svg deleted file mode 100644 index 1648229851..0000000000 --- a/apps/docs/logo/integrations/voice.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/apps/docs/models.mdx b/apps/docs/models.mdx index eb36d4f376..aea3f9b636 100644 --- a/apps/docs/models.mdx +++ b/apps/docs/models.mdx @@ -140,6 +140,14 @@ in **Available Models**, and assign it to a role or select it for a Session or task. Adding these routes does not change existing model defaults; access still depends on the connected provider account or plan. +**DeepSeek V4.1 Flash** is recommended through +[OpenRouter](/providers/inference/openrouter), +[Vercel AI Gateway](/providers/inference/vercel-ai-gateway), and +[OpenCode Go](/providers/inference/opencode-go). Roomote uses the provider's +matching route and no longer recommends the older V4 Flash 0731 model. Existing +defaults do not change automatically; enable V4.1 Flash and assign it to a role +when you want to use it. + ### Recommended default models Providers also carry recommended defaults for the model roles below — for diff --git a/apps/docs/snippets/integration-name.jsx b/apps/docs/snippets/integration-name.jsx index 99220c9dd9..aef60311e8 100644 --- a/apps/docs/snippets/integration-name.jsx +++ b/apps/docs/snippets/integration-name.jsx @@ -6,9 +6,9 @@ export function IntegrationName({ href, icon, name }) { box: '/logo/integrations/box.svg', azure: '/logo/integrations/azure.svg', granola: '/logo/integrations/granola.svg', + agentmail: '/logo/integrations/agentmail.svg', monday: '/logo/integrations/monday.svg', rippling: '/logo/integrations/rippling.svg', - voice: '/logo/integrations/voice.svg', }; const iconSrc = manualIcons[icon] ?? diff --git a/apps/docs/voice.mdx b/apps/docs/voice.mdx index 553318165a..04fac38102 100644 --- a/apps/docs/voice.mdx +++ b/apps/docs/voice.mdx @@ -1,6 +1,6 @@ --- title: Voice -icon: audio-lines +icon: openai description: Talk naturally with a Fast Session using GPT-Live. --- @@ -60,9 +60,9 @@ Session, so the timeline shows both the conversation and the work behind it. During a call the Fast Session returns its results to the voice rather than writing them as chat replies; Roomote then reports them in its own words, -keeping numbers, names, paths, and link labels exact. If a result cannot be -spoken, for example because the call drops, it is written to the transcript -as a normal reply so nothing is lost. Typed messages sent during a call are +keeping numbers, names, paths, and link labels exact. The exact Fast result stays +in the transcript as a collapsed **Reported result to voice** row, including when +the call drops or speech is interrupted. Typed messages sent during a call are answered in writing as usual. Each spoken request is cleaned up (filler words, false starts, and misheard diff --git a/apps/web/src/components/settings/ExperimentalSettings.tsx b/apps/web/src/components/settings/ExperimentalSettings.tsx deleted file mode 100644 index bf85a6c2fc..0000000000 --- a/apps/web/src/components/settings/ExperimentalSettings.tsx +++ /dev/null @@ -1,69 +0,0 @@ -'use client'; - -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; -import { toast } from 'sonner'; - -import { FlaskConical, Skeleton, Switch } from '@/components/system'; -import { useTRPC } from '@/trpc/client'; - -import { Section } from './Section'; -import type { ExperimentalSettings as ExperimentalSettingsData } from '@/trpc/commands/experimental-settings'; - -export function ExperimentalSettings() { - const trpc = useTRPC(); - const queryClient = useQueryClient(); - const queryKey = trpc.experimentalSettings.get.queryKey(); - const settingsQuery = useQuery(trpc.experimentalSettings.get.queryOptions()); - const updateMutation = useMutation( - trpc.experimentalSettings.setOpenCodeCodeMode.mutationOptions(), - ); - - const handleToggle = async (enabled: boolean) => { - const previous = settingsQuery.data; - queryClient.setQueryData(queryKey, { - openCodeCodeModeEnabled: enabled, - }); - - try { - const updated = await updateMutation.mutateAsync({ enabled }); - queryClient.setQueryData(queryKey, updated); - toast.success(`Code Mode ${enabled ? 'enabled' : 'disabled'}`); - } catch (error) { - queryClient.setQueryData(queryKey, previous); - toast.error( - error instanceof Error ? error.message : 'Failed to update Code Mode.', - ); - } - }; - - if (settingsQuery.isPending) { - return ; - } - - if (settingsQuery.isError || !settingsQuery.data) { - return ( -
- Failed to load experimental settings. -
- ); - } - - return ( -
-
- void handleToggle(checked === true)} - /> -
-

- Defer eligible tools and discover them when needed, reducing the - tool definitions sent with each request. -

-
-
-
- ); -} diff --git a/apps/web/src/components/settings/pages/ExperimentalSettingsPage.tsx b/apps/web/src/components/settings/pages/ExperimentalSettingsPage.tsx index f804970071..cb1c77bade 100644 --- a/apps/web/src/components/settings/pages/ExperimentalSettingsPage.tsx +++ b/apps/web/src/components/settings/pages/ExperimentalSettingsPage.tsx @@ -1,12 +1,11 @@ 'use client'; import { SettingsShell } from '@/components/settings/SettingsShell'; -import { ExperimentalSettings } from '@/components/settings/ExperimentalSettings'; export function ExperimentalSettingsPage() { return ( - + {null} ); } diff --git a/apps/web/src/components/settings/settings-navigation.test.ts b/apps/web/src/components/settings/settings-navigation.test.ts index 080d797a00..9c8e9d9006 100644 --- a/apps/web/src/components/settings/settings-navigation.test.ts +++ b/apps/web/src/components/settings/settings-navigation.test.ts @@ -3,9 +3,9 @@ import { describe, expect, it } from 'vitest'; import { getSettingsNavigationItem } from './settings-navigation'; describe('settings navigation', () => { - it('describes Experimental settings with active opt-in features', () => { + it('shows the empty state for Experimental settings', () => { expect(getSettingsNavigationItem('experimental')?.description).toBe( - 'Try opt-in features that may change while they are being evaluated.', + 'No experimental features at the moment. Check back soon.', ); }); }); diff --git a/apps/web/src/components/settings/settings-navigation.ts b/apps/web/src/components/settings/settings-navigation.ts index 160419ff70..a776c2946f 100644 --- a/apps/web/src/components/settings/settings-navigation.ts +++ b/apps/web/src/components/settings/settings-navigation.ts @@ -174,8 +174,7 @@ const SETTINGS_NAVIGATION_ITEMS: SettingsNavigationItem[] = [ id: 'experimental', label: 'Experimental', title: 'Experimental', - description: - 'Try opt-in features that may change while they are being evaluated.', + description: 'No experimental features at the moment. Check back soon.', href: SETTINGS_PATHS.experimental, icon: FlaskConical, adminOnly: true, diff --git a/apps/web/src/trpc/commands/experimental-settings/index.test.ts b/apps/web/src/trpc/commands/experimental-settings/index.test.ts deleted file mode 100644 index 7aa4bebbb5..0000000000 --- a/apps/web/src/trpc/commands/experimental-settings/index.test.ts +++ /dev/null @@ -1,86 +0,0 @@ -const { findFirst, insert, values, onConflictDoUpdate } = vi.hoisted(() => ({ - findFirst: vi.fn(), - insert: vi.fn(), - values: vi.fn(), - onConflictDoUpdate: vi.fn(), -})); - -vi.mock('@roomote/db/server', () => ({ - db: { - query: { deploymentSettings: { findFirst } }, - insert, - }, - deploymentSettings: { id: 'deployment_settings.id' }, - eq: vi.fn(), -})); - -vi.mock('../setup/shared', () => ({ - assertAdmin: (auth: { isAdmin: boolean }) => { - if (!auth.isAdmin) throw new Error('Unauthorized'); - }, -})); - -import type { UserAuthSuccess } from '@/types'; - -import { - getExperimentalSettingsCommand, - setOpenCodeCodeModeCommand, -} from './index'; - -const adminAuth = { userId: 'admin', isAdmin: true } as UserAuthSuccess; -const memberAuth = { userId: 'member', isAdmin: false } as UserAuthSuccess; - -describe('experimental settings commands', () => { - beforeEach(() => { - vi.clearAllMocks(); - insert.mockReturnValue({ values }); - values.mockReturnValue({ onConflictDoUpdate }); - onConflictDoUpdate.mockResolvedValue(undefined); - }); - - it('defaults Code Mode off and reads a persisted opt-in', async () => { - findFirst.mockResolvedValueOnce(undefined).mockResolvedValueOnce({ - metadata: { opencode_code_mode: true }, - }); - - await expect(getExperimentalSettingsCommand(adminAuth)).resolves.toEqual({ - openCodeCodeModeEnabled: false, - }); - await expect(getExperimentalSettingsCommand(adminAuth)).resolves.toEqual({ - openCodeCodeModeEnabled: true, - }); - }); - - it('persists the validated boolean without dropping other metadata', async () => { - findFirst.mockResolvedValue({ metadata: { keep_me: 'value' } }); - - await expect( - setOpenCodeCodeModeCommand(adminAuth, { enabled: true }), - ).resolves.toEqual({ openCodeCodeModeEnabled: true }); - - expect(values).toHaveBeenCalledWith( - expect.objectContaining({ - id: 'default', - metadata: { keep_me: 'value', opencode_code_mode: true }, - }), - ); - expect(onConflictDoUpdate).toHaveBeenCalledWith( - expect.objectContaining({ - target: 'deployment_settings.id', - set: expect.objectContaining({ - metadata: { keep_me: 'value', opencode_code_mode: true }, - }), - }), - ); - }); - - it('rejects non-admin reads and writes', async () => { - await expect(getExperimentalSettingsCommand(memberAuth)).rejects.toThrow( - 'Unauthorized', - ); - await expect( - setOpenCodeCodeModeCommand(memberAuth, { enabled: true }), - ).rejects.toThrow('Unauthorized'); - expect(findFirst).not.toHaveBeenCalled(); - }); -}); diff --git a/apps/web/src/trpc/commands/experimental-settings/index.ts b/apps/web/src/trpc/commands/experimental-settings/index.ts deleted file mode 100644 index 82b0f4072f..0000000000 --- a/apps/web/src/trpc/commands/experimental-settings/index.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { db, deploymentSettings, eq } from '@roomote/db/server'; -import { - isOpenCodeCodeModeEnabledFromMetadata, - OPENCODE_CODE_MODE_METADATA_KEY, -} from '@roomote/feature-flags'; - -import type { UserAuthSuccess } from '@/types'; - -import { assertAdmin } from '../setup/shared'; - -const DEFAULT_DEPLOYMENT_ID = 'default'; - -export type ExperimentalSettings = { - openCodeCodeModeEnabled: boolean; -}; - -function normalizeMetadata(value: unknown): Record { - return value && typeof value === 'object' && !Array.isArray(value) - ? { ...(value as Record) } - : {}; -} - -export async function getExperimentalSettingsCommand( - auth: UserAuthSuccess, -): Promise { - assertAdmin(auth); - const settings = await db.query.deploymentSettings.findFirst({ - where: eq(deploymentSettings.id, DEFAULT_DEPLOYMENT_ID), - columns: { metadata: true }, - }); - - return { - openCodeCodeModeEnabled: isOpenCodeCodeModeEnabledFromMetadata( - settings?.metadata, - ), - }; -} - -export async function setOpenCodeCodeModeCommand( - auth: UserAuthSuccess, - input: { enabled: boolean }, -): Promise { - assertAdmin(auth); - const existing = await db.query.deploymentSettings.findFirst({ - where: eq(deploymentSettings.id, DEFAULT_DEPLOYMENT_ID), - columns: { metadata: true }, - }); - const metadata = { - ...normalizeMetadata(existing?.metadata), - [OPENCODE_CODE_MODE_METADATA_KEY]: input.enabled, - }; - const now = new Date(); - - await db - .insert(deploymentSettings) - .values({ id: DEFAULT_DEPLOYMENT_ID, metadata, updatedAt: now }) - .onConflictDoUpdate({ - target: deploymentSettings.id, - set: { metadata, updatedAt: now }, - }); - - return { openCodeCodeModeEnabled: input.enabled }; -} diff --git a/apps/web/src/trpc/routers/_app.ts b/apps/web/src/trpc/routers/_app.ts index 631f8aa70f..6610d586e0 100644 --- a/apps/web/src/trpc/routers/_app.ts +++ b/apps/web/src/trpc/routers/_app.ts @@ -482,10 +482,6 @@ import { setDeploymentTimeZoneCommand, setAnonymousAnalyticsCommand, } from '../commands/misc-settings'; -import { - getExperimentalSettingsCommand, - setOpenCodeCodeModeCommand, -} from '../commands/experimental-settings'; import { backfillBrainTaskMemoriesCommand, getBrainPageCommand, @@ -3361,17 +3357,6 @@ export const appRouter = createRouter({ ), }), - experimentalSettings: createRouter({ - get: protectedProcedure.query(({ ctx: { auth } }) => - getExperimentalSettingsCommand(auth), - ), - setOpenCodeCodeMode: protectedProcedure - .input(z.object({ enabled: z.boolean() })) - .mutation(({ ctx: { auth }, input }) => - setOpenCodeCodeModeCommand(auth, input), - ), - }), - releases: createRouter({ status: protectedProcedure.query(({ ctx: { auth } }) => getReleaseStatusCommand(auth), diff --git a/package.json b/package.json index 1167a1d647..2595035aab 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "roomote", - "version": "1.6.0", + "version": "1.7.0", "license": "FCL-1.0-ALv2", "packageManager": "pnpm@10.29.3", "engines": { diff --git a/packages/db/package.json b/packages/db/package.json index f8d1c5c91d..9948b8807c 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -40,7 +40,6 @@ }, "dependencies": { "@roomote/env": "workspace:^", - "@roomote/feature-flags": "workspace:^", "@roomote/types": "workspace:^", "drizzle-orm": "^0.45.2", "nanoid": "^5.1.16", diff --git a/packages/db/src/lib/model-runtime-config.test.ts b/packages/db/src/lib/model-runtime-config.test.ts index c42da2e30a..d6b2263551 100644 --- a/packages/db/src/lib/model-runtime-config.test.ts +++ b/packages/db/src/lib/model-runtime-config.test.ts @@ -98,46 +98,6 @@ describe('resolveEffectiveModelRuntimeEnv', () => { mockGetFreshXaiAccessToken.mockResolvedValue(null); }); - it('enables only the direct OpenCode Code Mode experiment when configured', async () => { - mockDeploymentSettingsFindFirst.mockResolvedValue({ - metadata: { opencode_code_mode: true }, - runtimeModelConfig: {}, - taskModelSettings: {}, - }); - - const [controlPlaneEnv, sandboxEnv] = await Promise.all([ - resolveEffectiveModelRuntimeEnv({ - runtimeEnv: {}, - deploymentEnvVars: {}, - }), - resolveSandboxModelRuntimeEnv({ - runtimeEnv: {}, - deploymentEnvVars: {}, - }), - ]); - - for (const env of [controlPlaneEnv, sandboxEnv]) { - expect(env.OPENCODE_EXPERIMENTAL_CODE_MODE).toBe('1'); - expect(env.OPENCODE_EXPERIMENTAL).toBeUndefined(); - } - }); - - it('leaves OpenCode Code Mode disabled by default', async () => { - mockDeploymentSettingsFindFirst.mockResolvedValue({ - metadata: {}, - runtimeModelConfig: {}, - taskModelSettings: {}, - }); - - const env = await resolveSandboxModelRuntimeEnv({ - runtimeEnv: {}, - deploymentEnvVars: {}, - }); - - expect(env.OPENCODE_EXPERIMENTAL_CODE_MODE).toBeUndefined(); - expect(env.OPENCODE_EXPERIMENTAL).toBeUndefined(); - }); - it('prefers real runtime env values over persisted deployment config', async () => { mockDeploymentSettingsFindFirst.mockResolvedValue({ runtimeModelConfig: { diff --git a/packages/db/src/lib/model-runtime-config.ts b/packages/db/src/lib/model-runtime-config.ts index 5aa6be76e2..1a5177cc10 100644 --- a/packages/db/src/lib/model-runtime-config.ts +++ b/packages/db/src/lib/model-runtime-config.ts @@ -1,5 +1,4 @@ import { eq } from 'drizzle-orm'; -import { isOpenCodeCodeModeEnabledFromMetadata } from '@roomote/feature-flags'; import { applyImplicitLiteLlmModelPrefix, CHATGPT_FAST_MODE_ENV_VAR_NAME, @@ -113,7 +112,6 @@ async function loadPersistedRuntimeModelConfig( const deployment = await executor.query.deploymentSettings.findFirst({ where: eq(deploymentSettings.id, DEFAULT_DEPLOYMENT_ID), columns: { - metadata: true, runtimeModelConfig: true, taskModelSettings: true, }, @@ -126,9 +124,6 @@ async function loadPersistedRuntimeModelConfig( catalogModels: getTaskModelCatalog(deployment?.taskModelSettings), enabledCatalogModels: getEnabledTaskModels(deployment?.taskModelSettings), defaultModelId: getDefaultTaskModelId(deployment?.taskModelSettings), - openCodeCodeModeEnabled: isOpenCodeCodeModeEnabledFromMetadata( - deployment?.metadata, - ), }; } @@ -413,13 +408,7 @@ async function resolveModelRuntimeEnv( const executor = options.executor ?? db; const [ persistedEnvVars, - { - runtimeModelConfig, - catalogModels, - enabledCatalogModels, - defaultModelId, - openCodeCodeModeEnabled, - }, + { runtimeModelConfig, catalogModels, enabledCatalogModels, defaultModelId }, ] = await Promise.all([ resolveEffectiveDeploymentEnvVars({ deploymentEnvVars: options.deploymentEnvVars, @@ -714,9 +703,6 @@ async function resolveModelRuntimeEnv( return { ...resolvedRoleEnv, - ...(openCodeCodeModeEnabled - ? { OPENCODE_EXPERIMENTAL_CODE_MODE: '1' } - : {}), ...(providerKeyNames.length > 0 && { R_MODEL_ENV_KEYS: providerKeyNames.join(','), }), diff --git a/packages/feature-flags/src/__tests__/config.test.ts b/packages/feature-flags/src/__tests__/config.test.ts index 70eacd5ea6..3e6733b0d8 100644 --- a/packages/feature-flags/src/__tests__/config.test.ts +++ b/packages/feature-flags/src/__tests__/config.test.ts @@ -3,14 +3,15 @@ import { describe, expect, it } from 'vitest'; import { DEPLOYMENT_METADATA_BOOLEAN_CONFIG } from '../config'; describe('deployment metadata config', () => { - it('retains active metadata descriptors', () => { + it('retains the deployment-control metadata descriptors', () => { expect(Object.keys(DEPLOYMENT_METADATA_BOOLEAN_CONFIG).sort()).toEqual([ 'anonymous_analytics_enabled', 'deployment_disabled', - 'opencode_code_mode', ]); - expect(DEPLOYMENT_METADATA_BOOLEAN_CONFIG.opencode_code_mode?.kind).toBe( - 'experimental', - ); + for (const descriptor of Object.values( + DEPLOYMENT_METADATA_BOOLEAN_CONFIG, + )) { + expect(descriptor.kind).toBe('deployment-control'); + } }); }); diff --git a/packages/feature-flags/src/__tests__/metadata-descriptions.test.ts b/packages/feature-flags/src/__tests__/metadata-descriptions.test.ts index e83f44b114..f2569fb1b3 100644 --- a/packages/feature-flags/src/__tests__/metadata-descriptions.test.ts +++ b/packages/feature-flags/src/__tests__/metadata-descriptions.test.ts @@ -11,6 +11,7 @@ describe('metadata descriptions', () => { 'visual_proof_auto_screencast', 'background_subagents', 'opencode_background_subagents', + 'opencode_code_mode', 'composerSuggestions', ])('classifies removed experiment metadata %s as legacy', (key) => { expect(getBooleanMetadataDescriptorByKey(key)).toEqual({ @@ -20,15 +21,6 @@ describe('metadata descriptions', () => { }); }); - it('classifies Code Mode as an active experiment', () => { - expect(getBooleanMetadataDescriptorByKey('opencode_code_mode')).toEqual({ - kind: 'experimental', - description: - 'Defer eligible tools and discover them when needed, reducing the tool definitions sent with each request.', - group: 'Code Mode', - }); - }); - it('retains active deployment-control descriptors', () => { expect(getBooleanMetadataDescriptorByKey('deployment_disabled').kind).toBe( 'deployment-control', diff --git a/packages/feature-flags/src/config.ts b/packages/feature-flags/src/config.ts index 30155cefb0..c573741edb 100644 --- a/packages/feature-flags/src/config.ts +++ b/packages/feature-flags/src/config.ts @@ -20,10 +20,4 @@ export const DEPLOYMENT_METADATA_BOOLEAN_CONFIG: Record< description: 'Share anonymous usage analytics (instance and user activity identified only by random IDs) with the Roomote team. Enabled by default; absent means enabled.', }, - opencode_code_mode: { - kind: 'experimental', - group: 'Code Mode', - description: - 'Defer eligible tools and discover them when needed, reducing the tool definitions sent with each request.', - }, }; diff --git a/packages/feature-flags/src/index.ts b/packages/feature-flags/src/index.ts index 41b3ddbdcb..80179ad6da 100644 --- a/packages/feature-flags/src/index.ts +++ b/packages/feature-flags/src/index.ts @@ -32,8 +32,6 @@ export function getBooleanMetadataDescriptorByKey( export const ANONYMOUS_ANALYTICS_METADATA_KEY = 'anonymous_analytics_enabled' as const; -export const OPENCODE_CODE_MODE_METADATA_KEY = 'opencode_code_mode' as const; - export function isAnonymousAnalyticsEnabledFromMetadata( metadata: unknown, cloudEnabled = false, @@ -45,10 +43,3 @@ export function isAnonymousAnalyticsEnabledFromMetadata( return coerceToBoolean(normalizedMetadata[ANONYMOUS_ANALYTICS_METADATA_KEY]); } - -export function isOpenCodeCodeModeEnabledFromMetadata( - metadata: unknown, -): boolean { - const normalizedMetadata = normalizeMetadataRecord(metadata); - return coerceToBoolean(normalizedMetadata[OPENCODE_CODE_MODE_METADATA_KEY]); -} diff --git a/packages/feature-flags/src/types.ts b/packages/feature-flags/src/types.ts index 221040fa75..d3101c3850 100644 --- a/packages/feature-flags/src/types.ts +++ b/packages/feature-flags/src/types.ts @@ -1,7 +1,4 @@ -export type MetadataBooleanKind = - | 'deployment-control' - | 'experimental' - | 'legacy'; +export type MetadataBooleanKind = 'deployment-control' | 'legacy'; export interface MetadataBooleanDescriptor { description: string | null; @@ -13,6 +10,5 @@ export interface MetadataRecord { queue_parallel_task_limit?: boolean | number | string; deployment_disabled?: boolean; anonymous_analytics_enabled?: boolean; - opencode_code_mode?: boolean; [key: string]: unknown; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7f6300b4c8..43bca2f2aa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1343,9 +1343,6 @@ importers: '@roomote/env': specifier: workspace:^ version: link:../env - '@roomote/feature-flags': - specifier: workspace:^ - version: link:../feature-flags '@roomote/types': specifier: workspace:^ version: link:../types