diff --git a/CHANGELOG.md b/CHANGELOG.md index 294c64aaf..9cb6155ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ - Config: add `channels..configWrites` gating for channel-initiated config writes; migrate Slack channel IDs. ### Fixes + #### Agents - Agents: make user time zone and 24-hour time explicit in the system prompt. (#859) — thanks @CashWilliams. - Agents: strip downgraded tool call text without eating adjacent replies and filter thinking-tag leaks. (#905) — thanks @erikpr1994. @@ -72,6 +73,7 @@ - WhatsApp: normalize user JIDs with device suffix for allowlist checks in groups. (#838) — thanks @peschee. #### Gateway / Daemon / Sessions +- Gateway/UI: ship session defaults in the hello snapshot so the Control UI canonicalizes main session keys (no bare `main` alias). - Gateway: forward termination signals to respawned CLI child processes to avoid orphaned systemd runs. (#933) — thanks @roshanasingh4. - Sessions: return deep clones (`structuredClone`) so cached session entries can't be mutated. (#934) — thanks @ronak-guliani. - Heartbeat: keep `updatedAt` monotonic when restoring heartbeat sessions. (#934) — thanks @ronak-guliani. diff --git a/src/gateway/protocol/schema/snapshot.ts b/src/gateway/protocol/schema/snapshot.ts index 3d624e125..efd899eca 100644 --- a/src/gateway/protocol/schema/snapshot.ts +++ b/src/gateway/protocol/schema/snapshot.ts @@ -22,6 +22,16 @@ export const PresenceEntrySchema = Type.Object( export const HealthSnapshotSchema = Type.Any(); +export const SessionDefaultsSchema = Type.Object( + { + defaultAgentId: NonEmptyString, + mainKey: NonEmptyString, + mainSessionKey: NonEmptyString, + scope: Type.Optional(NonEmptyString), + }, + { additionalProperties: false }, +); + export const StateVersionSchema = Type.Object( { presence: Type.Integer({ minimum: 0 }), @@ -38,6 +48,7 @@ export const SnapshotSchema = Type.Object( uptimeMs: Type.Integer({ minimum: 0 }), configPath: Type.Optional(NonEmptyString), stateDir: Type.Optional(NonEmptyString), + sessionDefaults: Type.Optional(SessionDefaultsSchema), }, { additionalProperties: false }, ); diff --git a/src/gateway/server/health-state.ts b/src/gateway/server/health-state.ts index a6d0c8774..62ff24616 100644 --- a/src/gateway/server/health-state.ts +++ b/src/gateway/server/health-state.ts @@ -1,5 +1,8 @@ +import { resolveDefaultAgentId } from "../../agents/agent-scope.js"; import { getHealthSnapshot, type HealthSummary } from "../../commands/health.js"; -import { CONFIG_PATH_CLAWDBOT, STATE_DIR_CLAWDBOT } from "../../config/config.js"; +import { CONFIG_PATH_CLAWDBOT, STATE_DIR_CLAWDBOT, loadConfig } from "../../config/config.js"; +import { resolveMainSessionKey } from "../../config/sessions.js"; +import { normalizeMainKey } from "../../routing/session-key.js"; import { listSystemPresence } from "../../infra/system-presence.js"; import type { Snapshot } from "../protocol/index.js"; @@ -10,6 +13,11 @@ let healthRefresh: Promise | null = null; let broadcastHealthUpdate: ((snap: HealthSummary) => void) | null = null; export function buildGatewaySnapshot(): Snapshot { + const cfg = loadConfig(); + const defaultAgentId = resolveDefaultAgentId(cfg); + const mainKey = normalizeMainKey(cfg.session?.mainKey); + const mainSessionKey = resolveMainSessionKey(cfg); + const scope = cfg.session?.scope ?? "per-sender"; const presence = listSystemPresence(); const uptimeMs = Math.round(process.uptime() * 1000); // Health is async; caller should await getHealthSnapshot and replace later if needed. @@ -22,6 +30,12 @@ export function buildGatewaySnapshot(): Snapshot { // Surface resolved paths so UIs can display the true config location. configPath: CONFIG_PATH_CLAWDBOT, stateDir: STATE_DIR_CLAWDBOT, + sessionDefaults: { + defaultAgentId, + mainKey, + mainSessionKey, + scope, + }, }; } diff --git a/ui/src/ui/app-gateway.ts b/ui/src/ui/app-gateway.ts index 076275793..61c9af086 100644 --- a/ui/src/ui/app-gateway.ts +++ b/ui/src/ui/app-gateway.ts @@ -5,14 +5,20 @@ import { GatewayBrowserClient } from "./gateway"; import type { EventLogEntry } from "./app-events"; import type { PresenceEntry, HealthSnapshot, StatusSummary } from "./types"; import type { Tab } from "./navigation"; +import type { UiSettings } from "./storage"; import { handleAgentEvent, resetToolStream, type AgentEventPayload } from "./app-tool-stream"; import { flushChatQueueForEvent } from "./app-chat"; -import { loadCron, refreshActiveTab, setLastActiveSessionKey } from "./app-settings"; +import { + applySettings, + loadCron, + refreshActiveTab, + setLastActiveSessionKey, +} from "./app-settings"; import { handleChatEvent, type ChatEventPayload } from "./controllers/chat"; import type { ClawdbotApp } from "./app"; type GatewayHost = { - settings: { gatewayUrl: string; token: string }; + settings: UiSettings; password: string; client: GatewayBrowserClient | null; connected: boolean; @@ -29,6 +35,60 @@ type GatewayHost = { chatRunId: string | null; }; +type SessionDefaultsSnapshot = { + defaultAgentId?: string; + mainKey?: string; + mainSessionKey?: string; + scope?: string; +}; + +function normalizeSessionKeyForDefaults( + value: string | undefined, + defaults: SessionDefaultsSnapshot, +): string { + const raw = (value ?? "").trim(); + const mainSessionKey = defaults.mainSessionKey?.trim(); + if (!mainSessionKey) return raw; + if (!raw) return mainSessionKey; + const mainKey = defaults.mainKey?.trim() || "main"; + const defaultAgentId = defaults.defaultAgentId?.trim(); + const isAlias = + raw === "main" || + raw === mainKey || + (defaultAgentId && + (raw === `agent:${defaultAgentId}:main` || + raw === `agent:${defaultAgentId}:${mainKey}`)); + return isAlias ? mainSessionKey : raw; +} + +function applySessionDefaults(host: GatewayHost, defaults?: SessionDefaultsSnapshot) { + if (!defaults?.mainSessionKey) return; + const resolvedSessionKey = normalizeSessionKeyForDefaults(host.sessionKey, defaults); + const resolvedSettingsSessionKey = normalizeSessionKeyForDefaults( + host.settings.sessionKey, + defaults, + ); + const resolvedLastActiveSessionKey = normalizeSessionKeyForDefaults( + host.settings.lastActiveSessionKey, + defaults, + ); + const nextSessionKey = resolvedSessionKey || resolvedSettingsSessionKey || host.sessionKey; + const nextSettings = { + ...host.settings, + sessionKey: resolvedSettingsSessionKey || nextSessionKey, + lastActiveSessionKey: resolvedLastActiveSessionKey || nextSessionKey, + }; + const shouldUpdateSettings = + nextSettings.sessionKey !== host.settings.sessionKey || + nextSettings.lastActiveSessionKey !== host.settings.lastActiveSessionKey; + if (nextSessionKey !== host.sessionKey) { + host.sessionKey = nextSessionKey; + } + if (shouldUpdateSettings) { + applySettings(host as unknown as Parameters[0], nextSettings); + } +} + export function connectGateway(host: GatewayHost) { host.lastError = null; host.hello = null; @@ -113,7 +173,11 @@ export function handleGatewayEvent(host: GatewayHost, evt: GatewayEventFrame) { export function applySnapshot(host: GatewayHost, hello: GatewayHelloOk) { const snapshot = hello.snapshot as - | { presence?: PresenceEntry[]; health?: HealthSnapshot } + | { + presence?: PresenceEntry[]; + health?: HealthSnapshot; + sessionDefaults?: SessionDefaultsSnapshot; + } | undefined; if (snapshot?.presence && Array.isArray(snapshot.presence)) { host.presenceEntries = snapshot.presence; @@ -121,4 +185,7 @@ export function applySnapshot(host: GatewayHost, hello: GatewayHelloOk) { if (snapshot?.health) { host.debugHealth = snapshot.health; } + if (snapshot?.sessionDefaults) { + applySessionDefaults(host, snapshot.sessionDefaults); + } }