fix: canonicalize main session keys
This commit is contained in:
@@ -36,6 +36,7 @@
|
|||||||
- Config: add `channels.<provider>.configWrites` gating for channel-initiated config writes; migrate Slack channel IDs.
|
- Config: add `channels.<provider>.configWrites` gating for channel-initiated config writes; migrate Slack channel IDs.
|
||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
|
|
||||||
#### Agents
|
#### Agents
|
||||||
- Agents: make user time zone and 24-hour time explicit in the system prompt. (#859) — thanks @CashWilliams.
|
- 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.
|
- 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.
|
- WhatsApp: normalize user JIDs with device suffix for allowlist checks in groups. (#838) — thanks @peschee.
|
||||||
|
|
||||||
#### Gateway / Daemon / Sessions
|
#### 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.
|
- 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.
|
- 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.
|
- Heartbeat: keep `updatedAt` monotonic when restoring heartbeat sessions. (#934) — thanks @ronak-guliani.
|
||||||
|
|||||||
@@ -22,6 +22,16 @@ export const PresenceEntrySchema = Type.Object(
|
|||||||
|
|
||||||
export const HealthSnapshotSchema = Type.Any();
|
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(
|
export const StateVersionSchema = Type.Object(
|
||||||
{
|
{
|
||||||
presence: Type.Integer({ minimum: 0 }),
|
presence: Type.Integer({ minimum: 0 }),
|
||||||
@@ -38,6 +48,7 @@ export const SnapshotSchema = Type.Object(
|
|||||||
uptimeMs: Type.Integer({ minimum: 0 }),
|
uptimeMs: Type.Integer({ minimum: 0 }),
|
||||||
configPath: Type.Optional(NonEmptyString),
|
configPath: Type.Optional(NonEmptyString),
|
||||||
stateDir: Type.Optional(NonEmptyString),
|
stateDir: Type.Optional(NonEmptyString),
|
||||||
|
sessionDefaults: Type.Optional(SessionDefaultsSchema),
|
||||||
},
|
},
|
||||||
{ additionalProperties: false },
|
{ additionalProperties: false },
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
|
import { resolveDefaultAgentId } from "../../agents/agent-scope.js";
|
||||||
import { getHealthSnapshot, type HealthSummary } from "../../commands/health.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 { listSystemPresence } from "../../infra/system-presence.js";
|
||||||
import type { Snapshot } from "../protocol/index.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;
|
let broadcastHealthUpdate: ((snap: HealthSummary) => void) | null = null;
|
||||||
|
|
||||||
export function buildGatewaySnapshot(): Snapshot {
|
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 presence = listSystemPresence();
|
||||||
const uptimeMs = Math.round(process.uptime() * 1000);
|
const uptimeMs = Math.round(process.uptime() * 1000);
|
||||||
// Health is async; caller should await getHealthSnapshot and replace later if needed.
|
// 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.
|
// Surface resolved paths so UIs can display the true config location.
|
||||||
configPath: CONFIG_PATH_CLAWDBOT,
|
configPath: CONFIG_PATH_CLAWDBOT,
|
||||||
stateDir: STATE_DIR_CLAWDBOT,
|
stateDir: STATE_DIR_CLAWDBOT,
|
||||||
|
sessionDefaults: {
|
||||||
|
defaultAgentId,
|
||||||
|
mainKey,
|
||||||
|
mainSessionKey,
|
||||||
|
scope,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,14 +5,20 @@ import { GatewayBrowserClient } from "./gateway";
|
|||||||
import type { EventLogEntry } from "./app-events";
|
import type { EventLogEntry } from "./app-events";
|
||||||
import type { PresenceEntry, HealthSnapshot, StatusSummary } from "./types";
|
import type { PresenceEntry, HealthSnapshot, StatusSummary } from "./types";
|
||||||
import type { Tab } from "./navigation";
|
import type { Tab } from "./navigation";
|
||||||
|
import type { UiSettings } from "./storage";
|
||||||
import { handleAgentEvent, resetToolStream, type AgentEventPayload } from "./app-tool-stream";
|
import { handleAgentEvent, resetToolStream, type AgentEventPayload } from "./app-tool-stream";
|
||||||
import { flushChatQueueForEvent } from "./app-chat";
|
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 { handleChatEvent, type ChatEventPayload } from "./controllers/chat";
|
||||||
import type { ClawdbotApp } from "./app";
|
import type { ClawdbotApp } from "./app";
|
||||||
|
|
||||||
type GatewayHost = {
|
type GatewayHost = {
|
||||||
settings: { gatewayUrl: string; token: string };
|
settings: UiSettings;
|
||||||
password: string;
|
password: string;
|
||||||
client: GatewayBrowserClient | null;
|
client: GatewayBrowserClient | null;
|
||||||
connected: boolean;
|
connected: boolean;
|
||||||
@@ -29,6 +35,60 @@ type GatewayHost = {
|
|||||||
chatRunId: string | null;
|
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) {
|
export function connectGateway(host: GatewayHost) {
|
||||||
host.lastError = null;
|
host.lastError = null;
|
||||||
host.hello = null;
|
host.hello = null;
|
||||||
@@ -113,7 +173,11 @@ export function handleGatewayEvent(host: GatewayHost, evt: GatewayEventFrame) {
|
|||||||
|
|
||||||
export function applySnapshot(host: GatewayHost, hello: GatewayHelloOk) {
|
export function applySnapshot(host: GatewayHost, hello: GatewayHelloOk) {
|
||||||
const snapshot = hello.snapshot as
|
const snapshot = hello.snapshot as
|
||||||
| { presence?: PresenceEntry[]; health?: HealthSnapshot }
|
| {
|
||||||
|
presence?: PresenceEntry[];
|
||||||
|
health?: HealthSnapshot;
|
||||||
|
sessionDefaults?: SessionDefaultsSnapshot;
|
||||||
|
}
|
||||||
| undefined;
|
| undefined;
|
||||||
if (snapshot?.presence && Array.isArray(snapshot.presence)) {
|
if (snapshot?.presence && Array.isArray(snapshot.presence)) {
|
||||||
host.presenceEntries = snapshot.presence;
|
host.presenceEntries = snapshot.presence;
|
||||||
@@ -121,4 +185,7 @@ export function applySnapshot(host: GatewayHost, hello: GatewayHelloOk) {
|
|||||||
if (snapshot?.health) {
|
if (snapshot?.health) {
|
||||||
host.debugHealth = snapshot.health;
|
host.debugHealth = snapshot.health;
|
||||||
}
|
}
|
||||||
|
if (snapshot?.sessionDefaults) {
|
||||||
|
applySessionDefaults(host, snapshot.sessionDefaults);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user