From d58a256ee4ca528a24a5155d76f46bcf98e84e75 Mon Sep 17 00:00:00 2001 From: Andrew Maguire Date: Thu, 16 Jul 2026 12:05:30 +0100 Subject: [PATCH 1/4] fix(agent): make spoken narration strictly opt-in MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Spoken narration defaulted on for all cloud runs via resolveSpokenNarration falling back to isCloudRun(meta). That loaded the `speak` tool and its prompt instructions — and prompted the agent to call it — on headless runs (Slack threads, Signals scouts) where nothing can play audio, costing tokens with no listener and regardless of the feature flag, user setting, or ElevenLabs key. Make narration strictly opt-in: resolveSpokenNarration now returns true only when a caller explicitly sets spokenNarration. The desktop is the only client that can play audio and knows the flag/setting/key, so it computes that boolean and passes it; everything else stays silent and never loads the tool. Also require an ElevenLabs key alongside the setting on the desktop start/reconnect paths, and refresh the now-stale comments. Generated-By: PostHog Code Task-Id: 7a65b8a4-b69d-472f-9753-67d59715e332 --- .../src/adapters/local-tools/tools/speak.ts | 9 +++-- .../agent/src/adapters/session-meta.test.ts | 37 ++++++++----------- packages/agent/src/adapters/session-meta.ts | 17 +++++---- packages/core/src/sessions/sessionService.ts | 16 ++++++-- .../src/services/agent/schemas.ts | 5 ++- 5 files changed, 44 insertions(+), 40 deletions(-) diff --git a/packages/agent/src/adapters/local-tools/tools/speak.ts b/packages/agent/src/adapters/local-tools/tools/speak.ts index 83ef123eaf..7763baf0a3 100644 --- a/packages/agent/src/adapters/local-tools/tools/speak.ts +++ b/packages/agent/src/adapters/local-tools/tools/speak.ts @@ -48,10 +48,11 @@ export const SPEAK_TOOL_DESCRIPTION = * speakers, so it just acknowledges. The desktop renderer observes the * surfaced `tool_call` (carrying `text`/`needsUser` in its rawInput) and routes * it to the speech queue, exactly like completion/permission notifications are - * pure side effects off the event stream. Gated on `spokenNarration`: local - * sessions pass the user's setting at session start, while cloud sessions - * resolve it to true at the adapter (the sandbox can't know which clients are - * listening, so cloud emits always and consumers gate playback). + * pure side effects off the event stream. Gated on `spokenNarration`, which is + * strictly opt-in (see `resolveSpokenNarration`): the desktop passes it true + * only when the feature flag, the user's setting, and an ElevenLabs key are all + * in place. Headless cloud runs (Slack threads, Signals scouts) never enable + * it, so the tool and its instructions never load and never cost tokens there. */ export const speakTool = defineLocalTool({ name: SPEAK_TOOL_NAME, diff --git a/packages/agent/src/adapters/session-meta.test.ts b/packages/agent/src/adapters/session-meta.test.ts index e65730aa00..b3492eece0 100644 --- a/packages/agent/src/adapters/session-meta.test.ts +++ b/packages/agent/src/adapters/session-meta.test.ts @@ -6,25 +6,18 @@ describe("resolveSpokenNarration", () => { vi.unstubAllEnvs(); }); + // Narration is strictly opt-in: only an explicit `spokenNarration: true` + // enables it. Cloud/sandbox runs no longer default on, so headless runs + // (Slack threads, Signals scouts) never load the tool or its instructions. it.each([ { - name: "explicit true on local", - meta: { environment: "local" as const, spokenNarration: true }, + name: "explicit true", + meta: { spokenNarration: true }, expected: true, }, { - name: "explicit false on cloud", - meta: { environment: "cloud" as const, spokenNarration: false }, - expected: false, - }, - { - name: "cloud default", - meta: { environment: "cloud" as const }, - expected: true, - }, - { - name: "local default", - meta: { environment: "local" as const }, + name: "explicit false", + meta: { spokenNarration: false }, expected: false, }, { name: "no meta", meta: undefined, expected: false }, @@ -35,20 +28,20 @@ describe("resolveSpokenNarration", () => { }); it.each([ - { name: "no meta", meta: undefined, expected: true }, - { name: "empty meta", meta: {}, expected: true }, + { name: "no meta", meta: undefined, expected: false }, + { name: "empty meta", meta: {}, expected: false }, { - name: "explicit false", - meta: { spokenNarration: false }, - expected: false, + name: "explicit true", + meta: { spokenNarration: true }, + expected: true, }, { - name: "explicit local environment", - meta: { environment: "local" as const }, + name: "explicit false", + meta: { spokenNarration: false }, expected: false, }, ])( - "resolves $name to $expected in a sandbox without an environment tag", + "resolves $name to $expected in a sandbox (no default-on)", ({ meta, expected }) => { vi.stubEnv("IS_SANDBOX", "1"); expect(resolveSpokenNarration(meta)).toBe(expected); diff --git a/packages/agent/src/adapters/session-meta.ts b/packages/agent/src/adapters/session-meta.ts index f70de89744..859d60a818 100644 --- a/packages/agent/src/adapters/session-meta.ts +++ b/packages/agent/src/adapters/session-meta.ts @@ -1,5 +1,3 @@ -import { isCloudRun } from "../utils/common"; - /** Minimal shape needed to resolve the effective task id from session meta. */ interface TaskIdSource { taskId?: string; @@ -19,18 +17,21 @@ export function resolveTaskId( /** Minimal shape needed to resolve spoken narration from session meta. */ interface SpokenNarrationSource { - environment?: "local" | "cloud"; spokenNarration?: boolean; } /** - * An explicit setting wins; otherwise cloud runs (including sandbox runs - * detected via `IS_SANDBOX`) default to on because the sandbox can't know - * which clients are listening, so consumers gate playback. Local runs stay - * silent. Shared by the Claude and Codex adapters. + * Spoken narration is strictly opt-in: it is on only when a caller explicitly + * sets `spokenNarration` true at session start. The desktop is the only client + * that can play audio and the only place that knows the feature flag, the + * user's setting, and whether an ElevenLabs key is configured — so it computes + * that boolean and passes it. Everything else (headless cloud runs like Slack + * threads and Signals scouts, sandboxes, local runs without the setting) stays + * silent, so the `speak` tool and its instructions never load and never cost + * tokens where nothing is listening. Shared by the Claude and Codex adapters. */ export function resolveSpokenNarration( meta: SpokenNarrationSource | undefined, ): boolean { - return meta?.spokenNarration ?? isCloudRun(meta); + return meta?.spokenNarration === true; } diff --git a/packages/core/src/sessions/sessionService.ts b/packages/core/src/sessions/sessionService.ts index f96a13398d..4694203f93 100644 --- a/packages/core/src/sessions/sessionService.ts +++ b/packages/core/src/sessions/sessionService.ts @@ -336,6 +336,7 @@ export interface SessionServiceDeps { rtkEnabledLocal?: boolean; rtkEnabledCloud?: boolean; spokenNotifications?: boolean; + elevenLabsKeyConfigured?: boolean; }; usageLimit: { show: (...args: any[]) => any }; readonly addDirectoryDialog: { open: boolean }; @@ -1087,14 +1088,19 @@ export class SessionService { this.d.log.warn("Failed to verify workspace", { taskId, err }); }); - const { customInstructions, rtkEnabledLocal, spokenNotifications } = - this.d.settings; + const { + customInstructions, + rtkEnabledLocal, + spokenNotifications, + elevenLabsKeyConfigured, + } = this.d.settings; const result = await this.d.trpc.agent.reconnect.mutate({ taskId, taskRunId, repoPath, rtkEnabled: rtkEnabledLocal, - spokenNarration: spokenNotifications === true, + spokenNarration: + spokenNotifications === true && elevenLabsKeyConfigured === true, apiHost: auth.apiHost, projectId: auth.projectId, logUrl, @@ -1418,6 +1424,7 @@ export class SessionService { customInstructions: startCustomInstructions, rtkEnabledLocal, spokenNotifications, + elevenLabsKeyConfigured, } = this.d.settings; const preferredModel = model ?? this.d.DEFAULT_GATEWAY_MODEL; const result = await this.d.trpc.agent.start.mutate({ @@ -1430,7 +1437,8 @@ export class SessionService { adapter, customInstructions: startCustomInstructions || undefined, rtkEnabled: rtkEnabledLocal, - spokenNarration: spokenNotifications === true, + spokenNarration: + spokenNotifications === true && elevenLabsKeyConfigured === true, effort: effortLevelSchema.safeParse(reasoningLevel).success ? (reasoningLevel as EffortLevel) : undefined, diff --git a/packages/workspace-server/src/services/agent/schemas.ts b/packages/workspace-server/src/services/agent/schemas.ts index 9234cd7f6e..066a8314ba 100644 --- a/packages/workspace-server/src/services/agent/schemas.ts +++ b/packages/workspace-server/src/services/agent/schemas.ts @@ -93,8 +93,9 @@ export const startSessionInput = z.object({ rtkEnabled: z.boolean().optional(), /** * The user's spoken-narration setting at session start. Gates the agent's - * speak tool and its prompt instructions; when absent the adapter defaults - * by environment (cloud on, local off). + * speak tool and its prompt instructions. Strictly opt-in: only the desktop + * sets it true (feature flag + setting + ElevenLabs key); when absent the + * adapter leaves narration off, so headless runs never load the tool. */ spokenNarration: z.boolean().optional(), }); From 539c826cc8fe575519eea90da61286df03d75ee1 Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Thu, 16 Jul 2026 12:19:45 +0100 Subject: [PATCH 2/4] fix(agent): allow system voice narration fallback Generated-By: PostHog Code Task-Id: ac30fada-8c29-4973-a253-08d70be31be6 --- .../agent/src/adapters/local-tools/tools/speak.ts | 6 +++--- packages/agent/src/adapters/session-meta.ts | 12 ++++++------ packages/core/src/sessions/sessionService.ts | 10 +++++----- .../ui/src/features/sessions/sessionServiceHost.ts | 9 +++++++++ .../workspace-server/src/services/agent/schemas.ts | 4 ++-- 5 files changed, 25 insertions(+), 16 deletions(-) diff --git a/packages/agent/src/adapters/local-tools/tools/speak.ts b/packages/agent/src/adapters/local-tools/tools/speak.ts index 7763baf0a3..44f31320a0 100644 --- a/packages/agent/src/adapters/local-tools/tools/speak.ts +++ b/packages/agent/src/adapters/local-tools/tools/speak.ts @@ -50,9 +50,9 @@ export const SPEAK_TOOL_DESCRIPTION = * it to the speech queue, exactly like completion/permission notifications are * pure side effects off the event stream. Gated on `spokenNarration`, which is * strictly opt-in (see `resolveSpokenNarration`): the desktop passes it true - * only when the feature flag, the user's setting, and an ElevenLabs key are all - * in place. Headless cloud runs (Slack threads, Signals scouts) never enable - * it, so the tool and its instructions never load and never cost tokens there. + * only when the feature flag and the user's setting are both enabled. Headless + * cloud runs (Slack threads, Signals scouts) never enable it, so the tool and + * its instructions never load and never cost tokens there. */ export const speakTool = defineLocalTool({ name: SPEAK_TOOL_NAME, diff --git a/packages/agent/src/adapters/session-meta.ts b/packages/agent/src/adapters/session-meta.ts index 859d60a818..a4ccf9fdde 100644 --- a/packages/agent/src/adapters/session-meta.ts +++ b/packages/agent/src/adapters/session-meta.ts @@ -23,12 +23,12 @@ interface SpokenNarrationSource { /** * Spoken narration is strictly opt-in: it is on only when a caller explicitly * sets `spokenNarration` true at session start. The desktop is the only client - * that can play audio and the only place that knows the feature flag, the - * user's setting, and whether an ElevenLabs key is configured — so it computes - * that boolean and passes it. Everything else (headless cloud runs like Slack - * threads and Signals scouts, sandboxes, local runs without the setting) stays - * silent, so the `speak` tool and its instructions never load and never cost - * tokens where nothing is listening. Shared by the Claude and Codex adapters. + * that can play audio and the only place that knows the feature flag and the + * user's setting, so it computes that boolean and passes it. Everything else + * (headless cloud runs like Slack threads and Signals scouts, sandboxes, local + * runs without the setting) stays silent, so the `speak` tool and its + * instructions never load and never cost tokens where nothing is listening. + * Shared by the Claude and Codex adapters. */ export function resolveSpokenNarration( meta: SpokenNarrationSource | undefined, diff --git a/packages/core/src/sessions/sessionService.ts b/packages/core/src/sessions/sessionService.ts index 4694203f93..d27cf9086b 100644 --- a/packages/core/src/sessions/sessionService.ts +++ b/packages/core/src/sessions/sessionService.ts @@ -336,7 +336,7 @@ export interface SessionServiceDeps { rtkEnabledLocal?: boolean; rtkEnabledCloud?: boolean; spokenNotifications?: boolean; - elevenLabsKeyConfigured?: boolean; + spokenNarrationEnabled?: boolean; }; usageLimit: { show: (...args: any[]) => any }; readonly addDirectoryDialog: { open: boolean }; @@ -1092,7 +1092,7 @@ export class SessionService { customInstructions, rtkEnabledLocal, spokenNotifications, - elevenLabsKeyConfigured, + spokenNarrationEnabled, } = this.d.settings; const result = await this.d.trpc.agent.reconnect.mutate({ taskId, @@ -1100,7 +1100,7 @@ export class SessionService { repoPath, rtkEnabled: rtkEnabledLocal, spokenNarration: - spokenNotifications === true && elevenLabsKeyConfigured === true, + spokenNotifications === true && spokenNarrationEnabled === true, apiHost: auth.apiHost, projectId: auth.projectId, logUrl, @@ -1424,7 +1424,7 @@ export class SessionService { customInstructions: startCustomInstructions, rtkEnabledLocal, spokenNotifications, - elevenLabsKeyConfigured, + spokenNarrationEnabled, } = this.d.settings; const preferredModel = model ?? this.d.DEFAULT_GATEWAY_MODEL; const result = await this.d.trpc.agent.start.mutate({ @@ -1438,7 +1438,7 @@ export class SessionService { customInstructions: startCustomInstructions || undefined, rtkEnabled: rtkEnabledLocal, spokenNarration: - spokenNotifications === true && elevenLabsKeyConfigured === true, + spokenNotifications === true && spokenNarrationEnabled === true, effort: effortLevelSchema.safeParse(reasoningLevel).success ? (reasoningLevel as EffortLevel) : undefined, diff --git a/packages/ui/src/features/sessions/sessionServiceHost.ts b/packages/ui/src/features/sessions/sessionServiceHost.ts index 44bfc7a9f7..7aecf3caab 100644 --- a/packages/ui/src/features/sessions/sessionServiceHost.ts +++ b/packages/ui/src/features/sessions/sessionServiceHost.ts @@ -15,12 +15,17 @@ import { HOST_TRPC_CLIENT, type HostTrpcClient, } from "@posthog/host-router/client"; +import { SPOKEN_NARRATION_FLAG } from "@posthog/shared"; import { createAuthenticatedClient, getAuthenticatedClient, } from "@posthog/ui/features/auth/authClientImperative"; import { fetchAuthState } from "@posthog/ui/features/auth/authQueries"; import { useUsageLimitStore } from "@posthog/ui/features/billing/usageLimitStore"; +import { + FEATURE_FLAGS, + type FeatureFlags, +} from "@posthog/ui/features/feature-flags/identifiers"; import { useAddDirectoryDialogStore } from "@posthog/ui/features/folder-picker/addDirectoryDialogStore"; import { NotificationBus } from "@posthog/ui/features/notifications/notifications"; import { SpeechNotifier } from "@posthog/ui/features/notifications/speechNotifier"; @@ -117,6 +122,10 @@ function buildSessionServiceDeps(): SessionServiceDeps { return { ...state, customInstructions: getEffectiveCustomInstructions(state), + spokenNarrationEnabled: + resolveService(FEATURE_FLAGS).isEnabled( + SPOKEN_NARRATION_FLAG, + ) || import.meta.env.DEV, }; }, usageLimit: { diff --git a/packages/workspace-server/src/services/agent/schemas.ts b/packages/workspace-server/src/services/agent/schemas.ts index 066a8314ba..5d8eeab508 100644 --- a/packages/workspace-server/src/services/agent/schemas.ts +++ b/packages/workspace-server/src/services/agent/schemas.ts @@ -94,8 +94,8 @@ export const startSessionInput = z.object({ /** * The user's spoken-narration setting at session start. Gates the agent's * speak tool and its prompt instructions. Strictly opt-in: only the desktop - * sets it true (feature flag + setting + ElevenLabs key); when absent the - * adapter leaves narration off, so headless runs never load the tool. + * sets it true (feature flag + setting); when absent the adapter leaves + * narration off, so headless runs never load the tool. */ spokenNarration: z.boolean().optional(), }); From f6aec6ccee9fe2989f5b4ddb8b7a021e3e98322b Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Thu, 16 Jul 2026 12:43:42 +0100 Subject: [PATCH 3/4] fix(agent): preserve narration opt-in for cloud runs Generated-By: PostHog Code Task-Id: ac30fada-8c29-4973-a253-08d70be31be6 --- .../agent/src/adapters/session-meta.test.ts | 19 ++++++- packages/agent/src/adapters/session-meta.ts | 10 ++++ packages/agent/src/server/agent-server.ts | 2 + .../api-client/src/posthog-client.test.ts | 2 + packages/api-client/src/posthog-client.ts | 5 ++ packages/core/src/sessions/sessionService.ts | 16 ++---- .../src/task-detail/taskCreationApiClient.ts | 1 + .../src/task-detail/taskCreationSaga.test.ts | 2 + .../core/src/task-detail/taskCreationSaga.ts | 1 + .../core/src/task-detail/taskInput.test.ts | 15 +++++ packages/core/src/task-detail/taskInput.ts | 2 + packages/shared/src/task-creation-domain.ts | 2 + .../sessions/sessionServiceHost.test.ts | 56 ++++++++++++++++++- .../features/sessions/sessionServiceHost.ts | 15 ++++- .../task-detail/hooks/useTaskCreation.ts | 8 +++ 15 files changed, 141 insertions(+), 15 deletions(-) diff --git a/packages/agent/src/adapters/session-meta.test.ts b/packages/agent/src/adapters/session-meta.test.ts index b3492eece0..59617defe7 100644 --- a/packages/agent/src/adapters/session-meta.test.ts +++ b/packages/agent/src/adapters/session-meta.test.ts @@ -1,5 +1,8 @@ import { afterEach, describe, expect, it, vi } from "vitest"; -import { resolveSpokenNarration } from "./session-meta"; +import { + resolveCloudSpokenNarration, + resolveSpokenNarration, +} from "./session-meta"; describe("resolveSpokenNarration", () => { afterEach(() => { @@ -48,3 +51,17 @@ describe("resolveSpokenNarration", () => { }, ); }); + +describe("resolveCloudSpokenNarration", () => { + it.each([ + { state: { spoken_narration: true }, expected: true }, + { state: { spoken_narration: false }, expected: false }, + { state: {}, expected: false }, + { state: undefined, expected: false }, + ])( + "resolves explicit cloud run opt-in to $expected", + ({ state, expected }) => { + expect(resolveCloudSpokenNarration(state)).toBe(expected); + }, + ); +}); diff --git a/packages/agent/src/adapters/session-meta.ts b/packages/agent/src/adapters/session-meta.ts index a4ccf9fdde..fe126c37e9 100644 --- a/packages/agent/src/adapters/session-meta.ts +++ b/packages/agent/src/adapters/session-meta.ts @@ -20,6 +20,10 @@ interface SpokenNarrationSource { spokenNarration?: boolean; } +interface SpokenNarrationRunState { + spoken_narration?: unknown; +} + /** * Spoken narration is strictly opt-in: it is on only when a caller explicitly * sets `spokenNarration` true at session start. The desktop is the only client @@ -35,3 +39,9 @@ export function resolveSpokenNarration( ): boolean { return meta?.spokenNarration === true; } + +export function resolveCloudSpokenNarration( + state: SpokenNarrationRunState | undefined, +): boolean { + return state?.spoken_narration === true; +} diff --git a/packages/agent/src/server/agent-server.ts b/packages/agent/src/server/agent-server.ts index a8a0b25d4e..c9fdbb8a50 100644 --- a/packages/agent/src/server/agent-server.ts +++ b/packages/agent/src/server/agent-server.ts @@ -45,6 +45,7 @@ import { classifyAgentError, isPromptTooLongError, } from "../adapters/error-classification"; +import { resolveCloudSpokenNarration } from "../adapters/session-meta"; import { SIGNED_COMMIT_QUALIFIED_TOOL_NAME, SIGNED_MERGE_QUALIFIED_TOOL_NAME, @@ -1497,6 +1498,7 @@ export class AgentServer { allowedDomains: this.config.allowedDomains, jsonSchema: preTask?.json_schema ?? null, permissionMode: initialPermissionMode, + spokenNarration: resolveCloudSpokenNarration(runState), ...(this.config.baseBranch && { baseBranch: this.config.baseBranch }), ...this.buildClaudeCodeSessionMeta(runtimeAdapter), }; diff --git a/packages/api-client/src/posthog-client.test.ts b/packages/api-client/src/posthog-client.test.ts index 8c29ded301..b8b3981038 100644 --- a/packages/api-client/src/posthog-client.test.ts +++ b/packages/api-client/src/posthog-client.test.ts @@ -25,6 +25,7 @@ describe("PostHogAPIClient", () => { adapter: "codex", model: "gpt-5.4", reasoningLevel: "high", + spokenNarration: true, }); expect(post).toHaveBeenCalledWith( @@ -37,6 +38,7 @@ describe("PostHogAPIClient", () => { runtime_adapter: "codex", model: "gpt-5.4", reasoning_effort: "high", + spoken_narration: true, }), }), ); diff --git a/packages/api-client/src/posthog-client.ts b/packages/api-client/src/posthog-client.ts index 7543fb83d4..d99d58f081 100644 --- a/packages/api-client/src/posthog-client.ts +++ b/packages/api-client/src/posthog-client.ts @@ -613,6 +613,8 @@ interface CloudRunOptions { autoPublish?: boolean; /** Only false is sent: opts the run out of rtk command-output compression. */ rtkEnabled?: boolean; + /** Explicit desktop opt-in; omitted for headless/background callers. */ + spokenNarration?: boolean; runSource?: CloudRunSource; signalReportId?: string; initialPermissionMode?: ExecutionMode; @@ -708,6 +710,9 @@ function buildCloudRunRequestBody( if (options?.rtkEnabled === false) { body.rtk_enabled = false; } + if (options?.spokenNarration !== undefined) { + body.spoken_narration = options.spokenNarration; + } if (options?.runSource) { body.run_source = options.runSource; } diff --git a/packages/core/src/sessions/sessionService.ts b/packages/core/src/sessions/sessionService.ts index d27cf9086b..df868c43b4 100644 --- a/packages/core/src/sessions/sessionService.ts +++ b/packages/core/src/sessions/sessionService.ts @@ -1088,19 +1088,14 @@ export class SessionService { this.d.log.warn("Failed to verify workspace", { taskId, err }); }); - const { - customInstructions, - rtkEnabledLocal, - spokenNotifications, - spokenNarrationEnabled, - } = this.d.settings; + const { customInstructions, rtkEnabledLocal, spokenNarrationEnabled } = + this.d.settings; const result = await this.d.trpc.agent.reconnect.mutate({ taskId, taskRunId, repoPath, rtkEnabled: rtkEnabledLocal, - spokenNarration: - spokenNotifications === true && spokenNarrationEnabled === true, + spokenNarration: spokenNarrationEnabled === true, apiHost: auth.apiHost, projectId: auth.projectId, logUrl, @@ -1423,7 +1418,6 @@ export class SessionService { const { customInstructions: startCustomInstructions, rtkEnabledLocal, - spokenNotifications, spokenNarrationEnabled, } = this.d.settings; const preferredModel = model ?? this.d.DEFAULT_GATEWAY_MODEL; @@ -1437,8 +1431,7 @@ export class SessionService { adapter, customInstructions: startCustomInstructions || undefined, rtkEnabled: rtkEnabledLocal, - spokenNarration: - spokenNotifications === true && spokenNarrationEnabled === true, + spokenNarration: spokenNarrationEnabled === true, effort: effortLevelSchema.safeParse(reasoningLevel).success ? (reasoningLevel as EffortLevel) : undefined, @@ -3415,6 +3408,7 @@ export class SessionService { prAuthorshipMode, autoPublish: previousState.auto_publish === true || undefined, rtkEnabled: this.d.settings.rtkEnabledCloud, + spokenNarration: this.d.settings.spokenNarrationEnabled === true, runSource: getCloudRunSource(previousState), signalReportId: typeof previousState.signal_report_id === "string" diff --git a/packages/core/src/task-detail/taskCreationApiClient.ts b/packages/core/src/task-detail/taskCreationApiClient.ts index b09c5f75d2..2045ad657f 100644 --- a/packages/core/src/task-detail/taskCreationApiClient.ts +++ b/packages/core/src/task-detail/taskCreationApiClient.ts @@ -19,6 +19,7 @@ export interface CreateTaskRunClientOptions { prAuthorshipMode?: PrAuthorshipMode; autoPublish?: boolean; rtkEnabled?: boolean; + spokenNarration?: boolean; runSource?: CloudRunSource; signalReportId?: string; initialPermissionMode?: string; diff --git a/packages/core/src/task-detail/taskCreationSaga.test.ts b/packages/core/src/task-detail/taskCreationSaga.test.ts index f3d626053a..33d785fd8a 100644 --- a/packages/core/src/task-detail/taskCreationSaga.test.ts +++ b/packages/core/src/task-detail/taskCreationSaga.test.ts @@ -154,6 +154,7 @@ describe("TaskCreationSaga", () => { reasoningLevel: "high", cloudAutoPublish: true, cloudRtkEnabled: false, + spokenNarration: true, }); expect(result.success).toBe(true); @@ -172,6 +173,7 @@ describe("TaskCreationSaga", () => { prAuthorshipMode: "user", autoPublish: true, rtkEnabled: false, + spokenNarration: true, runSource: "manual", signalReportId: undefined, initialPermissionMode: "auto", diff --git a/packages/core/src/task-detail/taskCreationSaga.ts b/packages/core/src/task-detail/taskCreationSaga.ts index b3e0c5218a..82bd07f724 100644 --- a/packages/core/src/task-detail/taskCreationSaga.ts +++ b/packages/core/src/task-detail/taskCreationSaga.ts @@ -404,6 +404,7 @@ export class TaskCreationSaga extends Saga< prAuthorshipMode, autoPublish: input.cloudAutoPublish, rtkEnabled: input.cloudRtkEnabled, + spokenNarration: input.spokenNarration, runSource: input.cloudRunSource ?? "manual", signalReportId: input.signalReportId, homeQuickAction: input.homeQuickActionLabel, diff --git a/packages/core/src/task-detail/taskInput.test.ts b/packages/core/src/task-detail/taskInput.test.ts index f45243d608..5d0b6b293d 100644 --- a/packages/core/src/task-detail/taskInput.test.ts +++ b/packages/core/src/task-detail/taskInput.test.ts @@ -27,4 +27,19 @@ describe("prepareTaskInput", () => { }); expect(input.customInstructions).toBeUndefined(); }); + + it.each([ + { workspaceMode: "cloud" as const, expected: true }, + { workspaceMode: "local" as const, expected: undefined }, + { workspaceMode: "worktree" as const, expected: undefined }, + ])( + "passes spoken narration through only for cloud (%s)", + ({ workspaceMode, expected }) => { + const input = prepareTaskInput("do the thing", [], { + workspaceMode, + spokenNarration: true, + }); + expect(input.spokenNarration).toBe(expected); + }, + ); }); diff --git a/packages/core/src/task-detail/taskInput.ts b/packages/core/src/task-detail/taskInput.ts index 73bc9550d8..66d93f3d57 100644 --- a/packages/core/src/task-detail/taskInput.ts +++ b/packages/core/src/task-detail/taskInput.ts @@ -32,6 +32,7 @@ export interface PrepareTaskInputOptions { customInstructions?: string; autoPublishCloudRuns?: boolean; rtkEnabledCloud?: boolean; + spokenNarration?: boolean; allowNoRepo?: boolean; importedMcpServers?: CloudMcpServerImport[]; relayedMcpServers?: CloudMcpServerRelayDesignation[]; @@ -70,6 +71,7 @@ export function prepareTaskInput( options.signalReportId && isCloud ? "signal_report" : undefined, cloudAutoPublish: isCloud ? options.autoPublishCloudRuns : undefined, cloudRtkEnabled: isCloud ? options.rtkEnabledCloud : undefined, + spokenNarration: isCloud ? options.spokenNarration : undefined, signalReportId: options.signalReportId, additionalDirectories: isCloud ? undefined : options.additionalDirectories, channelContext: options.channelContext, diff --git a/packages/shared/src/task-creation-domain.ts b/packages/shared/src/task-creation-domain.ts index 8b073027a9..e3fd2a9269 100644 --- a/packages/shared/src/task-creation-domain.ts +++ b/packages/shared/src/task-creation-domain.ts @@ -52,6 +52,8 @@ export interface TaskCreationInput { * meaningful: it opts the run out of the server-side default (enabled). */ cloudRtkEnabled?: boolean; + /** Explicit desktop opt-in for spoken narration in interactive cloud runs. */ + spokenNarration?: boolean; signalReportId?: string; additionalDirectories?: string[]; /** diff --git a/packages/ui/src/features/sessions/sessionServiceHost.test.ts b/packages/ui/src/features/sessions/sessionServiceHost.test.ts index c17203b38b..d71fd2f14b 100644 --- a/packages/ui/src/features/sessions/sessionServiceHost.test.ts +++ b/packages/ui/src/features/sessions/sessionServiceHost.test.ts @@ -236,8 +236,14 @@ const mockSpeechNotifier = vi.hoisted(() => ({ speak: vi.fn(), })); +const mockFeatureFlags = vi.hoisted(() => ({ + isEnabled: vi.fn(() => false), + onFlagsLoaded: vi.fn(() => vi.fn()), +})); + const mockSettingsState = vi.hoisted(() => ({ customInstructions: "", + spokenNotifications: false, syncCustomInstructionsFromFile: false, syncedCustomInstructions: null as { path: string; @@ -314,6 +320,9 @@ vi.mock("@posthog/di/container", () => ({ if (typeof token === "function" && token.name === "SpeechNotifier") { return mockSpeechNotifier; } + if (token === Symbol.for("posthog.ui.featureFlags")) { + return mockFeatureFlags; + } throw new Error(`resolveService: unmocked token ${String(token)}`); }, })); @@ -382,7 +391,11 @@ vi.mock("@posthog/core/sessions/sessionEvents", async () => { }); import { toast } from "@posthog/ui/primitives/toast"; -import { getSessionService, resetSessionService } from "./sessionServiceHost"; +import { + getSessionService, + resetSessionService, + shouldEnableSpokenNarration, +} from "./sessionServiceHost"; // --- Test Fixtures --- @@ -426,6 +439,8 @@ describe("SessionService", () => { mockConvertStoredEntriesToEvents.mockImplementation(() => []); resetSessionService(); mockSettingsState.customInstructions = ""; + mockSettingsState.spokenNotifications = false; + mockFeatureFlags.isEnabled.mockReturnValue(false); mockSettingsState.syncCustomInstructionsFromFile = false; mockSettingsState.syncedCustomInstructions = null; mockGetIsOnline.mockReturnValue(true); @@ -514,6 +529,42 @@ describe("SessionService", () => { }); }); + describe("spoken narration availability", () => { + it.each([ + { + userOptedIn: true, + flagEnabled: true, + isDevelopment: false, + expected: true, + }, + { + userOptedIn: false, + flagEnabled: true, + isDevelopment: false, + expected: false, + }, + { + userOptedIn: true, + flagEnabled: false, + isDevelopment: false, + expected: false, + }, + { + userOptedIn: true, + flagEnabled: false, + isDevelopment: true, + expected: true, + }, + ])( + "returns $expected for opt-in=$userOptedIn flag=$flagEnabled dev=$isDevelopment", + ({ userOptedIn, flagEnabled, isDevelopment, expected }) => { + expect( + shouldEnableSpokenNarration(userOptedIn, flagEnabled, isDevelopment), + ).toBe(expected); + }, + ); + }); + describe("connectToTask", () => { it("skips local connection for cloud runs", async () => { const service = getSessionService(); @@ -5036,6 +5087,8 @@ describe("SessionService", () => { it("preserves codex runtime selection when resuming a terminal cloud run", async () => { const service = getSessionService(); + mockSettingsState.spokenNotifications = true; + mockFeatureFlags.isEnabled.mockReturnValue(true); mockSessionStoreSetters.getSessionByTaskId.mockReturnValue( createMockSession({ isCloud: true, @@ -5124,6 +5177,7 @@ describe("SessionService", () => { model: "gpt-5.4", reasoningLevel: "high", resumeFromRunId: "run-123", + spokenNarration: true, }), ); }); diff --git a/packages/ui/src/features/sessions/sessionServiceHost.ts b/packages/ui/src/features/sessions/sessionServiceHost.ts index 7aecf3caab..9811a82430 100644 --- a/packages/ui/src/features/sessions/sessionServiceHost.ts +++ b/packages/ui/src/features/sessions/sessionServiceHost.ts @@ -58,6 +58,14 @@ export { SessionService }; const log = logger.scope("session-service"); +export function shouldEnableSpokenNarration( + userOptedIn: boolean, + flagEnabled: boolean, + isDevelopment: boolean, +): boolean { + return userOptedIn && (flagEnabled || isDevelopment); +} + function hostClient(): HostTrpcClient { return resolveService(HOST_TRPC_CLIENT); } @@ -122,10 +130,13 @@ function buildSessionServiceDeps(): SessionServiceDeps { return { ...state, customInstructions: getEffectiveCustomInstructions(state), - spokenNarrationEnabled: + spokenNarrationEnabled: shouldEnableSpokenNarration( + state.spokenNotifications, resolveService(FEATURE_FLAGS).isEnabled( SPOKEN_NARRATION_FLAG, - ) || import.meta.env.DEV, + ), + import.meta.env.DEV, + ), }; }, usageLimit: { diff --git a/packages/ui/src/features/task-detail/hooks/useTaskCreation.ts b/packages/ui/src/features/task-detail/hooks/useTaskCreation.ts index 1baab22871..10fa554a34 100644 --- a/packages/ui/src/features/task-detail/hooks/useTaskCreation.ts +++ b/packages/ui/src/features/task-detail/hooks/useTaskCreation.ts @@ -15,6 +15,7 @@ import { type Adapter, ANALYTICS_EVENTS, PROJECT_BLUEBIRD_FLAG, + SPOKEN_NARRATION_FLAG, type TaskCreationInput, type WorkspaceMode, } from "@posthog/shared"; @@ -219,6 +220,10 @@ export function useTaskCreation({ PROJECT_BLUEBIRD_FLAG, import.meta.env.DEV, ); + const spokenNarrationEnabled = useFeatureFlag( + SPOKEN_NARRATION_FLAG, + import.meta.env.DEV, + ); const { personalChannel } = useTaskChannels({ enabled: bluebirdEnabled }); const hasRequiredPath = allowNoRepo @@ -372,6 +377,8 @@ export function useTaskCreation({ customInstructions: getEffectiveCustomInstructions(settings), autoPublishCloudRuns: settings.autoPublishCloudRuns, rtkEnabledCloud: settings.rtkEnabledCloud, + spokenNarration: + settings.spokenNotifications && spokenNarrationEnabled, allowNoRepo, importedMcpServers: localMcpServersForRun.imported, relayedMcpServers: localMcpServersForRun.relayed, @@ -544,6 +551,7 @@ export function useTaskCreation({ channelId, allowNoRepo, bluebirdEnabled, + spokenNarrationEnabled, personalChannel?.id, localMcpServers, localMcpServersLoading, From 7e2010c52a52b733975220bd0d0c9763075e9cc9 Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Thu, 16 Jul 2026 12:50:41 +0100 Subject: [PATCH 4/4] fix(agent): keep narration gating on local sessions Generated-By: PostHog Code Task-Id: ac30fada-8c29-4973-a253-08d70be31be6 --- .../agent/src/adapters/session-meta.test.ts | 19 +------------------ packages/agent/src/adapters/session-meta.ts | 10 ---------- packages/agent/src/server/agent-server.ts | 2 -- .../api-client/src/posthog-client.test.ts | 2 -- packages/api-client/src/posthog-client.ts | 5 ----- packages/core/src/sessions/sessionService.ts | 1 - .../src/task-detail/taskCreationApiClient.ts | 1 - .../src/task-detail/taskCreationSaga.test.ts | 2 -- .../core/src/task-detail/taskCreationSaga.ts | 1 - .../core/src/task-detail/taskInput.test.ts | 15 --------------- packages/core/src/task-detail/taskInput.ts | 2 -- packages/shared/src/task-creation-domain.ts | 2 -- .../sessions/sessionServiceHost.test.ts | 1 - .../task-detail/hooks/useTaskCreation.ts | 8 -------- 14 files changed, 1 insertion(+), 70 deletions(-) diff --git a/packages/agent/src/adapters/session-meta.test.ts b/packages/agent/src/adapters/session-meta.test.ts index 59617defe7..b3492eece0 100644 --- a/packages/agent/src/adapters/session-meta.test.ts +++ b/packages/agent/src/adapters/session-meta.test.ts @@ -1,8 +1,5 @@ import { afterEach, describe, expect, it, vi } from "vitest"; -import { - resolveCloudSpokenNarration, - resolveSpokenNarration, -} from "./session-meta"; +import { resolveSpokenNarration } from "./session-meta"; describe("resolveSpokenNarration", () => { afterEach(() => { @@ -51,17 +48,3 @@ describe("resolveSpokenNarration", () => { }, ); }); - -describe("resolveCloudSpokenNarration", () => { - it.each([ - { state: { spoken_narration: true }, expected: true }, - { state: { spoken_narration: false }, expected: false }, - { state: {}, expected: false }, - { state: undefined, expected: false }, - ])( - "resolves explicit cloud run opt-in to $expected", - ({ state, expected }) => { - expect(resolveCloudSpokenNarration(state)).toBe(expected); - }, - ); -}); diff --git a/packages/agent/src/adapters/session-meta.ts b/packages/agent/src/adapters/session-meta.ts index fe126c37e9..a4ccf9fdde 100644 --- a/packages/agent/src/adapters/session-meta.ts +++ b/packages/agent/src/adapters/session-meta.ts @@ -20,10 +20,6 @@ interface SpokenNarrationSource { spokenNarration?: boolean; } -interface SpokenNarrationRunState { - spoken_narration?: unknown; -} - /** * Spoken narration is strictly opt-in: it is on only when a caller explicitly * sets `spokenNarration` true at session start. The desktop is the only client @@ -39,9 +35,3 @@ export function resolveSpokenNarration( ): boolean { return meta?.spokenNarration === true; } - -export function resolveCloudSpokenNarration( - state: SpokenNarrationRunState | undefined, -): boolean { - return state?.spoken_narration === true; -} diff --git a/packages/agent/src/server/agent-server.ts b/packages/agent/src/server/agent-server.ts index c9fdbb8a50..a8a0b25d4e 100644 --- a/packages/agent/src/server/agent-server.ts +++ b/packages/agent/src/server/agent-server.ts @@ -45,7 +45,6 @@ import { classifyAgentError, isPromptTooLongError, } from "../adapters/error-classification"; -import { resolveCloudSpokenNarration } from "../adapters/session-meta"; import { SIGNED_COMMIT_QUALIFIED_TOOL_NAME, SIGNED_MERGE_QUALIFIED_TOOL_NAME, @@ -1498,7 +1497,6 @@ export class AgentServer { allowedDomains: this.config.allowedDomains, jsonSchema: preTask?.json_schema ?? null, permissionMode: initialPermissionMode, - spokenNarration: resolveCloudSpokenNarration(runState), ...(this.config.baseBranch && { baseBranch: this.config.baseBranch }), ...this.buildClaudeCodeSessionMeta(runtimeAdapter), }; diff --git a/packages/api-client/src/posthog-client.test.ts b/packages/api-client/src/posthog-client.test.ts index b8b3981038..8c29ded301 100644 --- a/packages/api-client/src/posthog-client.test.ts +++ b/packages/api-client/src/posthog-client.test.ts @@ -25,7 +25,6 @@ describe("PostHogAPIClient", () => { adapter: "codex", model: "gpt-5.4", reasoningLevel: "high", - spokenNarration: true, }); expect(post).toHaveBeenCalledWith( @@ -38,7 +37,6 @@ describe("PostHogAPIClient", () => { runtime_adapter: "codex", model: "gpt-5.4", reasoning_effort: "high", - spoken_narration: true, }), }), ); diff --git a/packages/api-client/src/posthog-client.ts b/packages/api-client/src/posthog-client.ts index d99d58f081..7543fb83d4 100644 --- a/packages/api-client/src/posthog-client.ts +++ b/packages/api-client/src/posthog-client.ts @@ -613,8 +613,6 @@ interface CloudRunOptions { autoPublish?: boolean; /** Only false is sent: opts the run out of rtk command-output compression. */ rtkEnabled?: boolean; - /** Explicit desktop opt-in; omitted for headless/background callers. */ - spokenNarration?: boolean; runSource?: CloudRunSource; signalReportId?: string; initialPermissionMode?: ExecutionMode; @@ -710,9 +708,6 @@ function buildCloudRunRequestBody( if (options?.rtkEnabled === false) { body.rtk_enabled = false; } - if (options?.spokenNarration !== undefined) { - body.spoken_narration = options.spokenNarration; - } if (options?.runSource) { body.run_source = options.runSource; } diff --git a/packages/core/src/sessions/sessionService.ts b/packages/core/src/sessions/sessionService.ts index df868c43b4..096826e4fb 100644 --- a/packages/core/src/sessions/sessionService.ts +++ b/packages/core/src/sessions/sessionService.ts @@ -3408,7 +3408,6 @@ export class SessionService { prAuthorshipMode, autoPublish: previousState.auto_publish === true || undefined, rtkEnabled: this.d.settings.rtkEnabledCloud, - spokenNarration: this.d.settings.spokenNarrationEnabled === true, runSource: getCloudRunSource(previousState), signalReportId: typeof previousState.signal_report_id === "string" diff --git a/packages/core/src/task-detail/taskCreationApiClient.ts b/packages/core/src/task-detail/taskCreationApiClient.ts index 2045ad657f..b09c5f75d2 100644 --- a/packages/core/src/task-detail/taskCreationApiClient.ts +++ b/packages/core/src/task-detail/taskCreationApiClient.ts @@ -19,7 +19,6 @@ export interface CreateTaskRunClientOptions { prAuthorshipMode?: PrAuthorshipMode; autoPublish?: boolean; rtkEnabled?: boolean; - spokenNarration?: boolean; runSource?: CloudRunSource; signalReportId?: string; initialPermissionMode?: string; diff --git a/packages/core/src/task-detail/taskCreationSaga.test.ts b/packages/core/src/task-detail/taskCreationSaga.test.ts index 33d785fd8a..f3d626053a 100644 --- a/packages/core/src/task-detail/taskCreationSaga.test.ts +++ b/packages/core/src/task-detail/taskCreationSaga.test.ts @@ -154,7 +154,6 @@ describe("TaskCreationSaga", () => { reasoningLevel: "high", cloudAutoPublish: true, cloudRtkEnabled: false, - spokenNarration: true, }); expect(result.success).toBe(true); @@ -173,7 +172,6 @@ describe("TaskCreationSaga", () => { prAuthorshipMode: "user", autoPublish: true, rtkEnabled: false, - spokenNarration: true, runSource: "manual", signalReportId: undefined, initialPermissionMode: "auto", diff --git a/packages/core/src/task-detail/taskCreationSaga.ts b/packages/core/src/task-detail/taskCreationSaga.ts index 82bd07f724..b3e0c5218a 100644 --- a/packages/core/src/task-detail/taskCreationSaga.ts +++ b/packages/core/src/task-detail/taskCreationSaga.ts @@ -404,7 +404,6 @@ export class TaskCreationSaga extends Saga< prAuthorshipMode, autoPublish: input.cloudAutoPublish, rtkEnabled: input.cloudRtkEnabled, - spokenNarration: input.spokenNarration, runSource: input.cloudRunSource ?? "manual", signalReportId: input.signalReportId, homeQuickAction: input.homeQuickActionLabel, diff --git a/packages/core/src/task-detail/taskInput.test.ts b/packages/core/src/task-detail/taskInput.test.ts index 5d0b6b293d..f45243d608 100644 --- a/packages/core/src/task-detail/taskInput.test.ts +++ b/packages/core/src/task-detail/taskInput.test.ts @@ -27,19 +27,4 @@ describe("prepareTaskInput", () => { }); expect(input.customInstructions).toBeUndefined(); }); - - it.each([ - { workspaceMode: "cloud" as const, expected: true }, - { workspaceMode: "local" as const, expected: undefined }, - { workspaceMode: "worktree" as const, expected: undefined }, - ])( - "passes spoken narration through only for cloud (%s)", - ({ workspaceMode, expected }) => { - const input = prepareTaskInput("do the thing", [], { - workspaceMode, - spokenNarration: true, - }); - expect(input.spokenNarration).toBe(expected); - }, - ); }); diff --git a/packages/core/src/task-detail/taskInput.ts b/packages/core/src/task-detail/taskInput.ts index 66d93f3d57..73bc9550d8 100644 --- a/packages/core/src/task-detail/taskInput.ts +++ b/packages/core/src/task-detail/taskInput.ts @@ -32,7 +32,6 @@ export interface PrepareTaskInputOptions { customInstructions?: string; autoPublishCloudRuns?: boolean; rtkEnabledCloud?: boolean; - spokenNarration?: boolean; allowNoRepo?: boolean; importedMcpServers?: CloudMcpServerImport[]; relayedMcpServers?: CloudMcpServerRelayDesignation[]; @@ -71,7 +70,6 @@ export function prepareTaskInput( options.signalReportId && isCloud ? "signal_report" : undefined, cloudAutoPublish: isCloud ? options.autoPublishCloudRuns : undefined, cloudRtkEnabled: isCloud ? options.rtkEnabledCloud : undefined, - spokenNarration: isCloud ? options.spokenNarration : undefined, signalReportId: options.signalReportId, additionalDirectories: isCloud ? undefined : options.additionalDirectories, channelContext: options.channelContext, diff --git a/packages/shared/src/task-creation-domain.ts b/packages/shared/src/task-creation-domain.ts index e3fd2a9269..8b073027a9 100644 --- a/packages/shared/src/task-creation-domain.ts +++ b/packages/shared/src/task-creation-domain.ts @@ -52,8 +52,6 @@ export interface TaskCreationInput { * meaningful: it opts the run out of the server-side default (enabled). */ cloudRtkEnabled?: boolean; - /** Explicit desktop opt-in for spoken narration in interactive cloud runs. */ - spokenNarration?: boolean; signalReportId?: string; additionalDirectories?: string[]; /** diff --git a/packages/ui/src/features/sessions/sessionServiceHost.test.ts b/packages/ui/src/features/sessions/sessionServiceHost.test.ts index d71fd2f14b..0c84840a5e 100644 --- a/packages/ui/src/features/sessions/sessionServiceHost.test.ts +++ b/packages/ui/src/features/sessions/sessionServiceHost.test.ts @@ -5177,7 +5177,6 @@ describe("SessionService", () => { model: "gpt-5.4", reasoningLevel: "high", resumeFromRunId: "run-123", - spokenNarration: true, }), ); }); diff --git a/packages/ui/src/features/task-detail/hooks/useTaskCreation.ts b/packages/ui/src/features/task-detail/hooks/useTaskCreation.ts index 10fa554a34..1baab22871 100644 --- a/packages/ui/src/features/task-detail/hooks/useTaskCreation.ts +++ b/packages/ui/src/features/task-detail/hooks/useTaskCreation.ts @@ -15,7 +15,6 @@ import { type Adapter, ANALYTICS_EVENTS, PROJECT_BLUEBIRD_FLAG, - SPOKEN_NARRATION_FLAG, type TaskCreationInput, type WorkspaceMode, } from "@posthog/shared"; @@ -220,10 +219,6 @@ export function useTaskCreation({ PROJECT_BLUEBIRD_FLAG, import.meta.env.DEV, ); - const spokenNarrationEnabled = useFeatureFlag( - SPOKEN_NARRATION_FLAG, - import.meta.env.DEV, - ); const { personalChannel } = useTaskChannels({ enabled: bluebirdEnabled }); const hasRequiredPath = allowNoRepo @@ -377,8 +372,6 @@ export function useTaskCreation({ customInstructions: getEffectiveCustomInstructions(settings), autoPublishCloudRuns: settings.autoPublishCloudRuns, rtkEnabledCloud: settings.rtkEnabledCloud, - spokenNarration: - settings.spokenNotifications && spokenNarrationEnabled, allowNoRepo, importedMcpServers: localMcpServersForRun.imported, relayedMcpServers: localMcpServersForRun.relayed, @@ -551,7 +544,6 @@ export function useTaskCreation({ channelId, allowNoRepo, bluebirdEnabled, - spokenNarrationEnabled, personalChannel?.id, localMcpServers, localMcpServersLoading,