diff --git a/src/config/sessions.cache.test.ts b/src/config/sessions.cache.test.ts index cc3c6cb75a4..a77b1fdc2ea 100644 --- a/src/config/sessions.cache.test.ts +++ b/src/config/sessions.cache.test.ts @@ -9,6 +9,22 @@ import { saveSessionStore, } from "./sessions.js"; +function createSessionEntry(overrides: Partial = {}): SessionEntry { + return { + sessionId: "id-1", + updatedAt: Date.now(), + displayName: "Test Session 1", + ...overrides, + }; +} + +function createSingleSessionStore( + entry: SessionEntry = createSessionEntry(), + key = "session:1", +): Record { + return { [key]: entry }; +} + describe("Session Store Cache", () => { let fixtureRoot = ""; let caseId = 0; @@ -43,13 +59,7 @@ describe("Session Store Cache", () => { }); it("should load session store from disk on first call", async () => { - const testStore: Record = { - "session:1": { - sessionId: "id-1", - updatedAt: Date.now(), - displayName: "Test Session 1", - }, - }; + const testStore = createSingleSessionStore(); // Write test data await saveSessionStore(storePath, testStore); @@ -60,13 +70,7 @@ describe("Session Store Cache", () => { }); it("should cache session store on first load when file is unchanged", async () => { - const testStore: Record = { - "session:1": { - sessionId: "id-1", - updatedAt: Date.now(), - displayName: "Test Session 1", - }, - }; + const testStore = createSingleSessionStore(); await saveSessionStore(storePath, testStore); @@ -84,17 +88,15 @@ describe("Session Store Cache", () => { }); it("should not allow cached session mutations to leak across loads", async () => { - const testStore: Record = { - "session:1": { - sessionId: "id-1", - updatedAt: Date.now(), + const testStore = createSingleSessionStore( + createSessionEntry({ cliSessionIds: { openai: "sess-1" }, skillsSnapshot: { prompt: "skills", skills: [{ name: "alpha" }], }, - }, - }; + }), + ); await saveSessionStore(storePath, testStore); @@ -110,13 +112,7 @@ describe("Session Store Cache", () => { }); it("should refresh cache when store file changes on disk", async () => { - const testStore: Record = { - "session:1": { - sessionId: "id-1", - updatedAt: Date.now(), - displayName: "Test Session 1", - }, - }; + const testStore = createSingleSessionStore(); await saveSessionStore(storePath, testStore); @@ -138,13 +134,7 @@ describe("Session Store Cache", () => { }); it("should invalidate cache on write", async () => { - const testStore: Record = { - "session:1": { - sessionId: "id-1", - updatedAt: Date.now(), - displayName: "Test Session 1", - }, - }; + const testStore = createSingleSessionStore(); await saveSessionStore(storePath, testStore); @@ -172,13 +162,7 @@ describe("Session Store Cache", () => { process.env.OPENCLAW_SESSION_CACHE_TTL_MS = "0"; clearSessionStoreCacheForTest(); - const testStore: Record = { - "session:1": { - sessionId: "id-1", - updatedAt: Date.now(), - displayName: "Test Session 1", - }, - }; + const testStore = createSingleSessionStore(); await saveSessionStore(storePath, testStore); @@ -187,13 +171,10 @@ describe("Session Store Cache", () => { expect(loaded1).toEqual(testStore); // Modify file on disk - const modifiedStore: Record = { - "session:2": { - sessionId: "id-2", - updatedAt: Date.now(), - displayName: "Test Session 2", - }, - }; + const modifiedStore = createSingleSessionStore( + createSessionEntry({ sessionId: "id-2", displayName: "Test Session 2" }), + "session:2", + ); fs.writeFileSync(storePath, JSON.stringify(modifiedStore, null, 2)); // Second load - should read from disk (cache disabled) diff --git a/src/config/types.agent-defaults.ts b/src/config/types.agent-defaults.ts index aa3fbe41958..60b623db563 100644 --- a/src/config/types.agent-defaults.ts +++ b/src/config/types.agent-defaults.ts @@ -1,15 +1,11 @@ import type { ChannelId } from "../channels/plugins/types.js"; +import type { AgentModelConfig, AgentSandboxConfig } from "./types.agents-shared.js"; import type { BlockStreamingChunkConfig, BlockStreamingCoalesceConfig, HumanDelayConfig, TypingMode, } from "./types.base.js"; -import type { - SandboxBrowserSettings, - SandboxDockerSettings, - SandboxPruneSettings, -} from "./types.sandbox.js"; import type { MemorySearchConfig } from "./types.tools.js"; export type AgentModelEntryConfig = { @@ -248,40 +244,12 @@ export type AgentDefaultsConfig = { /** Auto-archive sub-agent sessions after N minutes (default: 60). */ archiveAfterMinutes?: number; /** Default model selection for spawned sub-agents (string or {primary,fallbacks}). */ - model?: string | { primary?: string; fallbacks?: string[] }; + model?: AgentModelConfig; /** Default thinking level for spawned sub-agents (e.g. "off", "low", "medium", "high"). */ thinking?: string; }; /** Optional sandbox settings for non-main sessions. */ - sandbox?: { - /** Enable sandboxing for sessions. */ - mode?: "off" | "non-main" | "all"; - /** - * Agent workspace access inside the sandbox. - * - "none": do not mount the agent workspace into the container; use a sandbox workspace under workspaceRoot - * - "ro": mount the agent workspace read-only; disables write/edit tools - * - "rw": mount the agent workspace read/write; enables write/edit tools - */ - workspaceAccess?: "none" | "ro" | "rw"; - /** - * Session tools visibility for sandboxed sessions. - * - "spawned": only allow session tools to target the current session and sessions spawned from it (default) - * - "all": allow session tools to target any session - */ - sessionToolsVisibility?: "spawned" | "all"; - /** Container/workspace scope for sandbox isolation. */ - scope?: "session" | "agent" | "shared"; - /** Legacy alias for scope ("session" when true, "shared" when false). */ - perSession?: boolean; - /** Root directory for sandbox workspaces. */ - workspaceRoot?: string; - /** Docker-specific sandbox settings. */ - docker?: SandboxDockerSettings; - /** Optional sandboxed browser settings. */ - browser?: SandboxBrowserSettings; - /** Auto-prune sandbox containers. */ - prune?: SandboxPruneSettings; - }; + sandbox?: AgentSandboxConfig; }; export type AgentCompactionMode = "default" | "safeguard"; diff --git a/src/config/types.agents-shared.ts b/src/config/types.agents-shared.ts new file mode 100644 index 00000000000..152c8973c11 --- /dev/null +++ b/src/config/types.agents-shared.ts @@ -0,0 +1,37 @@ +import type { + SandboxBrowserSettings, + SandboxDockerSettings, + SandboxPruneSettings, +} from "./types.sandbox.js"; + +export type AgentModelConfig = + | string + | { + /** Primary model (provider/model). */ + primary?: string; + /** Per-agent model fallbacks (provider/model). */ + fallbacks?: string[]; + }; + +export type AgentSandboxConfig = { + mode?: "off" | "non-main" | "all"; + /** Agent workspace access inside the sandbox. */ + workspaceAccess?: "none" | "ro" | "rw"; + /** + * Session tools visibility for sandboxed sessions. + * - "spawned": only allow session tools to target sessions spawned from this session (default) + * - "all": allow session tools to target any session + */ + sessionToolsVisibility?: "spawned" | "all"; + /** Container/workspace scope for sandbox isolation. */ + scope?: "session" | "agent" | "shared"; + /** Legacy alias for scope ("session" when true, "shared" when false). */ + perSession?: boolean; + workspaceRoot?: string; + /** Docker-specific sandbox settings. */ + docker?: SandboxDockerSettings; + /** Optional sandboxed browser settings. */ + browser?: SandboxBrowserSettings; + /** Auto-prune sandbox settings. */ + prune?: SandboxPruneSettings; +}; diff --git a/src/config/types.agents.ts b/src/config/types.agents.ts index 478e14e526b..11dd9bf4a2b 100644 --- a/src/config/types.agents.ts +++ b/src/config/types.agents.ts @@ -1,23 +1,10 @@ import type { ChatType } from "../channels/chat-type.js"; import type { AgentDefaultsConfig } from "./types.agent-defaults.js"; +import type { AgentModelConfig, AgentSandboxConfig } from "./types.agents-shared.js"; import type { HumanDelayConfig, IdentityConfig } from "./types.base.js"; import type { GroupChatConfig } from "./types.messages.js"; -import type { - SandboxBrowserSettings, - SandboxDockerSettings, - SandboxPruneSettings, -} from "./types.sandbox.js"; import type { AgentToolsConfig, MemorySearchConfig } from "./types.tools.js"; -export type AgentModelConfig = - | string - | { - /** Primary model (provider/model). */ - primary?: string; - /** Per-agent model fallbacks (provider/model). */ - fallbacks?: string[]; - }; - export type AgentConfig = { id: string; default?: boolean; @@ -38,30 +25,10 @@ export type AgentConfig = { /** Allow spawning sub-agents under other agent ids. Use "*" to allow any. */ allowAgents?: string[]; /** Per-agent default model for spawned sub-agents (string or {primary,fallbacks}). */ - model?: string | { primary?: string; fallbacks?: string[] }; - }; - sandbox?: { - mode?: "off" | "non-main" | "all"; - /** Agent workspace access inside the sandbox. */ - workspaceAccess?: "none" | "ro" | "rw"; - /** - * Session tools visibility for sandboxed sessions. - * - "spawned": only allow session tools to target sessions spawned from this session (default) - * - "all": allow session tools to target any session - */ - sessionToolsVisibility?: "spawned" | "all"; - /** Container/workspace scope for sandbox isolation. */ - scope?: "session" | "agent" | "shared"; - /** Legacy alias for scope ("session" when true, "shared" when false). */ - perSession?: boolean; - workspaceRoot?: string; - /** Docker-specific sandbox overrides for this agent. */ - docker?: SandboxDockerSettings; - /** Optional sandboxed browser overrides for this agent. */ - browser?: SandboxBrowserSettings; - /** Auto-prune overrides for this agent. */ - prune?: SandboxPruneSettings; + model?: AgentModelConfig; }; + /** Optional per-agent sandbox overrides. */ + sandbox?: AgentSandboxConfig; tools?: AgentToolsConfig; }; diff --git a/src/config/types.channel-messaging-common.ts b/src/config/types.channel-messaging-common.ts new file mode 100644 index 00000000000..5d927884bd6 --- /dev/null +++ b/src/config/types.channel-messaging-common.ts @@ -0,0 +1,50 @@ +import type { + BlockStreamingCoalesceConfig, + DmPolicy, + GroupPolicy, + MarkdownConfig, +} from "./types.base.js"; +import type { ChannelHeartbeatVisibilityConfig } from "./types.channels.js"; +import type { DmConfig } from "./types.messages.js"; + +export type CommonChannelMessagingConfig = { + /** Optional display name for this account (used in CLI/UI lists). */ + name?: string; + /** Optional provider capability tags used for agent/runtime guidance. */ + capabilities?: string[]; + /** Markdown formatting overrides (tables). */ + markdown?: MarkdownConfig; + /** Allow channel-initiated config writes (default: true). */ + configWrites?: boolean; + /** If false, do not start this account. Default: true. */ + enabled?: boolean; + /** Direct message access policy (default: pairing). */ + dmPolicy?: DmPolicy; + /** Optional allowlist for inbound DM senders. */ + allowFrom?: Array; + /** Default delivery target for CLI --deliver when no explicit --reply-to is provided. */ + defaultTo?: string; + /** Optional allowlist for group/channel senders. */ + groupAllowFrom?: Array; + /** Group/channel message handling policy. */ + groupPolicy?: GroupPolicy; + /** Max group/channel messages to keep as history context (0 disables). */ + historyLimit?: number; + /** Max DM turns to keep as history context. */ + dmHistoryLimit?: number; + /** Per-DM config overrides keyed by sender ID. */ + dms?: Record; + /** Outbound text chunk size (chars). */ + textChunkLimit?: number; + /** Chunking mode: "length" (default) splits by size; "newline" splits on every newline. */ + chunkMode?: "length" | "newline"; + blockStreaming?: boolean; + /** Merge streamed block replies before sending. */ + blockStreamingCoalesce?: BlockStreamingCoalesceConfig; + /** Heartbeat visibility settings for this channel. */ + heartbeat?: ChannelHeartbeatVisibilityConfig; + /** Outbound response prefix override for this channel/account. */ + responsePrefix?: string; + /** Max outbound media size in MB. */ + mediaMaxMb?: number; +}; diff --git a/src/config/types.irc.ts b/src/config/types.irc.ts index eff575d1918..61794523195 100644 --- a/src/config/types.irc.ts +++ b/src/config/types.irc.ts @@ -1,24 +1,7 @@ -import type { - BlockStreamingCoalesceConfig, - DmPolicy, - GroupPolicy, - MarkdownConfig, -} from "./types.base.js"; -import type { ChannelHeartbeatVisibilityConfig } from "./types.channels.js"; -import type { DmConfig } from "./types.messages.js"; +import type { CommonChannelMessagingConfig } from "./types.channel-messaging-common.js"; import type { GroupToolPolicyBySenderConfig, GroupToolPolicyConfig } from "./types.tools.js"; -export type IrcAccountConfig = { - /** Optional display name for this account (used in CLI/UI lists). */ - name?: string; - /** Optional provider capability tags used for agent/runtime guidance. */ - capabilities?: string[]; - /** Markdown formatting overrides (tables). */ - markdown?: MarkdownConfig; - /** Allow channel-initiated config writes (default: true). */ - configWrites?: boolean; - /** If false, do not start this IRC account. Default: true. */ - enabled?: boolean; +export type IrcAccountConfig = CommonChannelMessagingConfig & { /** IRC server hostname (example: irc.libera.chat). */ host?: string; /** IRC server port (default: 6697 with TLS, otherwise 6667). */ @@ -52,34 +35,8 @@ export type IrcAccountConfig = { }; /** Auto-join channel list at connect (example: ["#openclaw"]). */ channels?: string[]; - /** Direct message access policy (default: pairing). */ - dmPolicy?: DmPolicy; - /** Optional allowlist for inbound DM senders. */ - allowFrom?: Array; - /** Default delivery target for CLI --deliver when no explicit --reply-to is provided. */ - defaultTo?: string; - /** Optional allowlist for IRC channel senders. */ - groupAllowFrom?: Array; - /** - * Controls how channel messages are handled: - * - "open": channels bypass allowFrom; mention-gating applies - * - "disabled": block all channel messages entirely - * - "allowlist": only allow channel messages from senders in groupAllowFrom/allowFrom - */ - groupPolicy?: GroupPolicy; - /** Max channel messages to keep as history context (0 disables). */ - historyLimit?: number; - /** Max DM turns to keep as history context. */ - dmHistoryLimit?: number; - /** Per-DM config overrides keyed by sender ID. */ - dms?: Record; /** Outbound text chunk size (chars). Default: 350. */ textChunkLimit?: number; - /** Chunking mode: "length" (default) splits by size; "newline" splits on every newline. */ - chunkMode?: "length" | "newline"; - blockStreaming?: boolean; - /** Merge streamed block replies before sending. */ - blockStreamingCoalesce?: BlockStreamingCoalesceConfig; groups?: Record< string, { @@ -94,12 +51,6 @@ export type IrcAccountConfig = { >; /** Optional mention patterns specific to IRC channel messages. */ mentionPatterns?: string[]; - /** Heartbeat visibility settings for this channel. */ - heartbeat?: ChannelHeartbeatVisibilityConfig; - /** Outbound response prefix override for this channel/account. */ - responsePrefix?: string; - /** Max outbound media size in MB. */ - mediaMaxMb?: number; }; export type IrcConfig = { diff --git a/src/config/types.signal.ts b/src/config/types.signal.ts index 8103b409906..cf45fa34025 100644 --- a/src/config/types.signal.ts +++ b/src/config/types.signal.ts @@ -1,26 +1,9 @@ -import type { - BlockStreamingCoalesceConfig, - DmPolicy, - GroupPolicy, - MarkdownConfig, -} from "./types.base.js"; -import type { ChannelHeartbeatVisibilityConfig } from "./types.channels.js"; -import type { DmConfig } from "./types.messages.js"; +import type { CommonChannelMessagingConfig } from "./types.channel-messaging-common.js"; export type SignalReactionNotificationMode = "off" | "own" | "all" | "allowlist"; export type SignalReactionLevel = "off" | "ack" | "minimal" | "extensive"; -export type SignalAccountConfig = { - /** Optional display name for this account (used in CLI/UI lists). */ - name?: string; - /** Optional provider capability tags used for agent/runtime guidance. */ - capabilities?: string[]; - /** Markdown formatting overrides (tables). */ - markdown?: MarkdownConfig; - /** Allow channel-initiated config writes (default: true). */ - configWrites?: boolean; - /** If false, do not start this Signal account. Default: true. */ - enabled?: boolean; +export type SignalAccountConfig = CommonChannelMessagingConfig & { /** Optional explicit E.164 account for signal-cli. */ account?: string; /** Optional full base URL for signal-cli HTTP daemon. */ @@ -39,34 +22,8 @@ export type SignalAccountConfig = { ignoreAttachments?: boolean; ignoreStories?: boolean; sendReadReceipts?: boolean; - /** Direct message access policy (default: pairing). */ - dmPolicy?: DmPolicy; - allowFrom?: Array; - /** Default delivery target for CLI --deliver when no explicit --reply-to is provided. */ - defaultTo?: string; - /** Optional allowlist for Signal group senders (E.164). */ - groupAllowFrom?: Array; - /** - * Controls how group messages are handled: - * - "open": groups bypass allowFrom, no extra gating - * - "disabled": block all group messages - * - "allowlist": only allow group messages from senders in groupAllowFrom/allowFrom - */ - groupPolicy?: GroupPolicy; - /** Max group messages to keep as history context (0 disables). */ - historyLimit?: number; - /** Max DM turns to keep as history context. */ - dmHistoryLimit?: number; - /** Per-DM config overrides keyed by user ID. */ - dms?: Record; /** Outbound text chunk size (chars). Default: 4000. */ textChunkLimit?: number; - /** Chunking mode: "length" (default) splits by size; "newline" splits on every newline. */ - chunkMode?: "length" | "newline"; - blockStreaming?: boolean; - /** Merge streamed block replies before sending. */ - blockStreamingCoalesce?: BlockStreamingCoalesceConfig; - mediaMaxMb?: number; /** Reaction notification mode (off|own|all|allowlist). Default: own. */ reactionNotifications?: SignalReactionNotificationMode; /** Allowlist for reaction notifications when mode is allowlist. */ @@ -84,10 +41,6 @@ export type SignalAccountConfig = { * - "extensive": Agent can react liberally */ reactionLevel?: SignalReactionLevel; - /** Heartbeat visibility settings for this channel. */ - heartbeat?: ChannelHeartbeatVisibilityConfig; - /** Outbound response prefix override for this channel/account. */ - responsePrefix?: string; }; export type SignalConfig = {