fix: canonicalize main session keys

This commit is contained in:
Peter Steinberger
2026-01-15 08:04:27 +00:00
parent a5a9788b20
commit 53d0bf653a
4 changed files with 98 additions and 4 deletions

View File

@@ -36,6 +36,7 @@
- Config: add `channels.<provider>.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.

View File

@@ -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 },
);

View File

@@ -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<HealthSummary> | 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,
},
};
}

View File

@@ -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<typeof applySettings>[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);
}
}