Skip to content

Commit 408748b

Browse files
feat: add settings-backed generic listen-only mode and invoke gating
1 parent 7ebd1e1 commit 408748b

12 files changed

Lines changed: 760 additions & 16 deletions

File tree

interface/src/api/client.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -566,6 +566,10 @@ export interface BrowserSection {
566566
evaluate_enabled: boolean;
567567
}
568568

569+
export interface ChannelSection {
570+
listen_only_mode: boolean;
571+
}
572+
569573
export interface SandboxSection {
570574
mode: "enabled" | "disabled";
571575
writable_paths: string[];
@@ -584,6 +588,7 @@ export interface AgentConfigResponse {
584588
coalesce: CoalesceSection;
585589
memory_persistence: MemoryPersistenceSection;
586590
browser: BrowserSection;
591+
channel: ChannelSection;
587592
discord: DiscordSection;
588593
sandbox: SandboxSection;
589594
}
@@ -648,6 +653,10 @@ export interface BrowserUpdate {
648653
evaluate_enabled?: boolean;
649654
}
650655

656+
export interface ChannelUpdate {
657+
listen_only_mode?: boolean;
658+
}
659+
651660
export interface SandboxUpdate {
652661
mode?: "enabled" | "disabled";
653662
writable_paths?: string[];
@@ -666,6 +675,7 @@ export interface AgentConfigUpdateRequest {
666675
coalesce?: CoalesceUpdate;
667676
memory_persistence?: MemoryPersistenceUpdate;
668677
browser?: BrowserUpdate;
678+
channel?: ChannelUpdate;
669679
discord?: DiscordUpdate;
670680
sandbox?: SandboxUpdate;
671681
}

interface/src/routes/AgentConfig.tsx

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ function supportsAdaptiveThinking(modelId: string): boolean {
1515
|| id.includes("sonnet-4-6") || id.includes("sonnet-4.6");
1616
}
1717

18-
type SectionId = "soul" | "identity" | "user" | "routing" | "tuning" | "compaction" | "cortex" | "coalesce" | "memory" | "browser" | "sandbox";
18+
type SectionId = "soul" | "identity" | "user" | "routing" | "tuning" | "compaction" | "cortex" | "coalesce" | "memory" | "browser" | "channel" | "sandbox";
1919

2020
const SECTIONS: {
2121
id: SectionId;
@@ -34,6 +34,7 @@ const SECTIONS: {
3434
{ id: "coalesce", label: "Coalesce", group: "config", description: "Message batching", detail: "When multiple messages arrive in quick succession, coalescing batches them into a single LLM turn. This prevents the agent from responding to each message individually in fast-moving conversations." },
3535
{ id: "memory", label: "Memory Persistence", group: "config", description: "Auto-save interval", detail: "Spawns a silent background branch at regular intervals to recall existing memories and save new ones from the recent conversation. Runs without blocking the channel." },
3636
{ id: "browser", label: "Browser", group: "config", description: "Chrome automation", detail: "Controls browser automation tools available to workers. When enabled, workers can navigate web pages, take screenshots, and interact with sites. JavaScript evaluation is a separate permission." },
37+
{ id: "channel", label: "Channel Behavior", group: "config", description: "Reply behavior", detail: "Listen-only mode suppresses unsolicited replies in busy channels. The agent still responds to slash commands, @mentions, and replies to its own messages." },
3738
{ id: "sandbox", label: "Sandbox", group: "config", description: "Process containment", detail: "OS-level filesystem containment for shell and exec tool subprocesses. When enabled, worker processes run inside a kernel-enforced sandbox (bubblewrap on Linux, sandbox-exec on macOS) with an allowlist-only filesystem — only system paths, the workspace, and explicitly configured extra paths are accessible." },
3839
];
3940

@@ -64,7 +65,7 @@ export function AgentConfig({ agentId }: AgentConfigProps) {
6465
// Sync activeSection with URL search param
6566
useEffect(() => {
6667
if (search.tab) {
67-
const validSections: SectionId[] = ["soul", "identity", "user", "routing", "tuning", "compaction", "cortex", "coalesce", "memory", "browser", "sandbox"];
68+
const validSections: SectionId[] = ["soul", "identity", "user", "routing", "tuning", "compaction", "cortex", "coalesce", "memory", "browser", "channel", "sandbox"];
6869
if (validSections.includes(search.tab as SectionId)) {
6970
setActiveSection(search.tab as SectionId);
7071
}
@@ -414,6 +415,7 @@ const SANDBOX_DEFAULTS = { mode: "enabled" as const, writable_paths: [] as strin
414415
function ConfigSectionEditor({ sectionId, label, description, detail, config, onDirtyChange, saveHandlerRef, onSave }: ConfigSectionEditorProps) {
415416
type ConfigValues = Record<string, string | number | boolean | string[]>;
416417
const sandbox = config.sandbox ?? SANDBOX_DEFAULTS;
418+
const channel = config.channel ?? { listen_only_mode: false };
417419
const [localValues, setLocalValues] = useState<ConfigValues>(() => {
418420
// Initialize from config based on section
419421
switch (sectionId) {
@@ -431,6 +433,8 @@ function ConfigSectionEditor({ sectionId, label, description, detail, config, on
431433
return { ...config.memory_persistence } as ConfigValues;
432434
case "browser":
433435
return { ...config.browser } as ConfigValues;
436+
case "channel":
437+
return { ...channel } as ConfigValues;
434438
case "sandbox":
435439
return { mode: sandbox.mode, writable_paths: sandbox.writable_paths } as ConfigValues;
436440
default:
@@ -469,6 +473,9 @@ function ConfigSectionEditor({ sectionId, label, description, detail, config, on
469473
case "browser":
470474
setLocalValues({ ...config.browser });
471475
break;
476+
case "channel":
477+
setLocalValues({ ...channel });
478+
break;
472479
case "sandbox":
473480
setLocalValues({ mode: sandbox.mode, writable_paths: sandbox.writable_paths });
474481
break;
@@ -509,6 +516,9 @@ function ConfigSectionEditor({ sectionId, label, description, detail, config, on
509516
case "browser":
510517
setLocalValues({ ...config.browser });
511518
break;
519+
case "channel":
520+
setLocalValues({ ...channel });
521+
break;
512522
case "sandbox":
513523
setLocalValues({ mode: sandbox.mode, writable_paths: sandbox.writable_paths });
514524
break;
@@ -852,6 +862,17 @@ function ConfigSectionEditor({ sectionId, label, description, detail, config, on
852862
</div>
853863
</div>
854864
);
865+
case "channel":
866+
return (
867+
<div className="grid gap-4">
868+
<ConfigToggleField
869+
label="Listen-Only Mode"
870+
description="Only respond when explicitly invoked (slash command, @mention, or reply-to-bot)."
871+
value={localValues.listen_only_mode as boolean}
872+
onChange={(v) => handleChange("listen_only_mode", v)}
873+
/>
874+
</div>
875+
);
855876
default:
856877
return null;
857878
}

0 commit comments

Comments
 (0)