fix: normalize heartbeat targets
This commit is contained in:
@@ -32,6 +32,7 @@ Docs: https://docs.clawd.bot
|
|||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
- Sessions: accept non-UUID sessionIds for history/send/status while preserving agent scoping. (#1518)
|
- Sessions: accept non-UUID sessionIds for history/send/status while preserving agent scoping. (#1518)
|
||||||
|
- Heartbeat: accept plugin channel ids for heartbeat target validation + UI hints.
|
||||||
- Messaging/Sessions: mirror outbound sends into target session keys (threads + dmScope), create session entries on send, and normalize session key casing. (#1520, commit 4b6cdd1d3)
|
- Messaging/Sessions: mirror outbound sends into target session keys (threads + dmScope), create session entries on send, and normalize session key casing. (#1520, commit 4b6cdd1d3)
|
||||||
- Sessions: reject array-backed session stores to prevent silent wipes. (#1469)
|
- Sessions: reject array-backed session stores to prevent silent wipes. (#1469)
|
||||||
- Gateway: compare Linux process start time to avoid PID recycling lock loops; keep locks unless stale. (#1572) Thanks @steipete.
|
- Gateway: compare Linux process start time to avoid PID recycling lock loops; keep locks unless stale. (#1572) Thanks @steipete.
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ and logged; a message that is only `HEARTBEAT_OK` is dropped.
|
|||||||
every: "30m", // default: 30m (0m disables)
|
every: "30m", // default: 30m (0m disables)
|
||||||
model: "anthropic/claude-opus-4-5",
|
model: "anthropic/claude-opus-4-5",
|
||||||
includeReasoning: false, // default: false (deliver separate Reasoning: message when available)
|
includeReasoning: false, // default: false (deliver separate Reasoning: message when available)
|
||||||
target: "last", // last | whatsapp | telegram | discord | slack | signal | imessage | none
|
target: "last", // last | none | <channel id> (core or plugin, e.g. "bluebubbles")
|
||||||
to: "+15551234567", // optional channel-specific override
|
to: "+15551234567", // optional channel-specific override
|
||||||
prompt: "Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.",
|
prompt: "Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.",
|
||||||
ackMaxChars: 300 // max chars allowed after HEARTBEAT_OK
|
ackMaxChars: 300 // max chars allowed after HEARTBEAT_OK
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { Command } from "commander";
|
|||||||
import type { CronJob } from "../../cron/types.js";
|
import type { CronJob } from "../../cron/types.js";
|
||||||
import { danger } from "../../globals.js";
|
import { danger } from "../../globals.js";
|
||||||
import { defaultRuntime } from "../../runtime.js";
|
import { defaultRuntime } from "../../runtime.js";
|
||||||
|
import { normalizeAgentId } from "../../routing/session-key.js";
|
||||||
import type { GatewayRpcOpts } from "../gateway-rpc.js";
|
import type { GatewayRpcOpts } from "../gateway-rpc.js";
|
||||||
import { addGatewayClientOptions, callGatewayFromCli } from "../gateway-rpc.js";
|
import { addGatewayClientOptions, callGatewayFromCli } from "../gateway-rpc.js";
|
||||||
import { parsePositiveIntOrUndefined } from "../program/helpers.js";
|
import { parsePositiveIntOrUndefined } from "../program/helpers.js";
|
||||||
@@ -138,7 +139,9 @@ export function registerCronAddCommand(cron: Command) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const agentId =
|
const agentId =
|
||||||
typeof opts.agent === "string" && opts.agent.trim() ? opts.agent.trim() : undefined;
|
typeof opts.agent === "string" && opts.agent.trim()
|
||||||
|
? normalizeAgentId(opts.agent.trim())
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const payload = (() => {
|
const payload = (() => {
|
||||||
const systemEvent = typeof opts.systemEvent === "string" ? opts.systemEvent.trim() : "";
|
const systemEvent = typeof opts.systemEvent === "string" ? opts.systemEvent.trim() : "";
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { Command } from "commander";
|
import type { Command } from "commander";
|
||||||
import { danger } from "../../globals.js";
|
import { danger } from "../../globals.js";
|
||||||
import { defaultRuntime } from "../../runtime.js";
|
import { defaultRuntime } from "../../runtime.js";
|
||||||
|
import { normalizeAgentId } from "../../routing/session-key.js";
|
||||||
import { addGatewayClientOptions, callGatewayFromCli } from "../gateway-rpc.js";
|
import { addGatewayClientOptions, callGatewayFromCli } from "../gateway-rpc.js";
|
||||||
import {
|
import {
|
||||||
getCronChannelOptions,
|
getCronChannelOptions,
|
||||||
@@ -90,7 +91,7 @@ export function registerCronEditCommand(cron: Command) {
|
|||||||
throw new Error("Use --agent or --clear-agent, not both");
|
throw new Error("Use --agent or --clear-agent, not both");
|
||||||
}
|
}
|
||||||
if (typeof opts.agent === "string" && opts.agent.trim()) {
|
if (typeof opts.agent === "string" && opts.agent.trim()) {
|
||||||
patch.agentId = opts.agent.trim();
|
patch.agentId = normalizeAgentId(opts.agent.trim());
|
||||||
}
|
}
|
||||||
if (opts.clearAgent) {
|
if (opts.clearAgent) {
|
||||||
patch.agentId = null;
|
patch.agentId = null;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ async function writePluginFixture(params: {
|
|||||||
dir: string;
|
dir: string;
|
||||||
id: string;
|
id: string;
|
||||||
schema: Record<string, unknown>;
|
schema: Record<string, unknown>;
|
||||||
|
channels?: string[];
|
||||||
}) {
|
}) {
|
||||||
await fs.mkdir(params.dir, { recursive: true });
|
await fs.mkdir(params.dir, { recursive: true });
|
||||||
await fs.writeFile(
|
await fs.writeFile(
|
||||||
@@ -16,16 +17,16 @@ async function writePluginFixture(params: {
|
|||||||
`export default { id: "${params.id}", register() {} };`,
|
`export default { id: "${params.id}", register() {} };`,
|
||||||
"utf-8",
|
"utf-8",
|
||||||
);
|
);
|
||||||
|
const manifest: Record<string, unknown> = {
|
||||||
|
id: params.id,
|
||||||
|
configSchema: params.schema,
|
||||||
|
};
|
||||||
|
if (params.channels) {
|
||||||
|
manifest.channels = params.channels;
|
||||||
|
}
|
||||||
await fs.writeFile(
|
await fs.writeFile(
|
||||||
path.join(params.dir, "clawdbot.plugin.json"),
|
path.join(params.dir, "clawdbot.plugin.json"),
|
||||||
JSON.stringify(
|
JSON.stringify(manifest, null, 2),
|
||||||
{
|
|
||||||
id: params.id,
|
|
||||||
configSchema: params.schema,
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
),
|
|
||||||
"utf-8",
|
"utf-8",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -149,4 +150,43 @@ describe("config plugin validation", () => {
|
|||||||
expect(res.ok).toBe(true);
|
expect(res.ok).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("accepts plugin heartbeat targets", async () => {
|
||||||
|
await withTempHome(async (home) => {
|
||||||
|
process.env.CLAWDBOT_STATE_DIR = path.join(home, ".clawdbot");
|
||||||
|
const pluginDir = path.join(home, "bluebubbles-plugin");
|
||||||
|
await writePluginFixture({
|
||||||
|
dir: pluginDir,
|
||||||
|
id: "bluebubbles-plugin",
|
||||||
|
channels: ["bluebubbles"],
|
||||||
|
schema: { type: "object" },
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.resetModules();
|
||||||
|
const { validateConfigObjectWithPlugins } = await import("./config.js");
|
||||||
|
const res = validateConfigObjectWithPlugins({
|
||||||
|
agents: { defaults: { heartbeat: { target: "bluebubbles" } }, list: [{ id: "pi" }] },
|
||||||
|
plugins: { enabled: false, load: { paths: [pluginDir] } },
|
||||||
|
});
|
||||||
|
expect(res.ok).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects unknown heartbeat targets", async () => {
|
||||||
|
await withTempHome(async (home) => {
|
||||||
|
process.env.CLAWDBOT_STATE_DIR = path.join(home, ".clawdbot");
|
||||||
|
vi.resetModules();
|
||||||
|
const { validateConfigObjectWithPlugins } = await import("./config.js");
|
||||||
|
const res = validateConfigObjectWithPlugins({
|
||||||
|
agents: { defaults: { heartbeat: { target: "not-a-channel" } }, list: [{ id: "pi" }] },
|
||||||
|
});
|
||||||
|
expect(res.ok).toBe(false);
|
||||||
|
if (!res.ok) {
|
||||||
|
expect(res.issues).toContainEqual({
|
||||||
|
path: "agents.defaults.heartbeat.target",
|
||||||
|
message: "unknown heartbeat target: not-a-channel",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -84,4 +84,22 @@ describe("config schema", () => {
|
|||||||
const channelProps = channelSchema?.properties as Record<string, unknown> | undefined;
|
const channelProps = channelSchema?.properties as Record<string, unknown> | undefined;
|
||||||
expect(channelProps?.accessToken).toBeTruthy();
|
expect(channelProps?.accessToken).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("adds heartbeat target hints with dynamic channels", () => {
|
||||||
|
const res = buildConfigSchema({
|
||||||
|
channels: [
|
||||||
|
{
|
||||||
|
id: "bluebubbles",
|
||||||
|
label: "BlueBubbles",
|
||||||
|
configSchema: { type: "object" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultsHint = res.uiHints["agents.defaults.heartbeat.target"];
|
||||||
|
const listHint = res.uiHints["agents.list.*.heartbeat.target"];
|
||||||
|
expect(defaultsHint?.help).toContain("bluebubbles");
|
||||||
|
expect(defaultsHint?.help).toContain("last");
|
||||||
|
expect(listHint?.help).toContain("bluebubbles");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { CHANNEL_IDS } from "../channels/registry.js";
|
||||||
import { VERSION } from "../version.js";
|
import { VERSION } from "../version.js";
|
||||||
import { ClawdbotSchema } from "./zod-schema.js";
|
import { ClawdbotSchema } from "./zod-schema.js";
|
||||||
|
|
||||||
@@ -807,6 +808,44 @@ function applyChannelHints(hints: ConfigUiHints, channels: ChannelUiMetadata[]):
|
|||||||
return next;
|
return next;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function listHeartbeatTargetChannels(channels: ChannelUiMetadata[]): string[] {
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const ordered: string[] = [];
|
||||||
|
for (const id of CHANNEL_IDS) {
|
||||||
|
const normalized = id.trim().toLowerCase();
|
||||||
|
if (!normalized || seen.has(normalized)) continue;
|
||||||
|
seen.add(normalized);
|
||||||
|
ordered.push(normalized);
|
||||||
|
}
|
||||||
|
for (const channel of channels) {
|
||||||
|
const normalized = channel.id.trim().toLowerCase();
|
||||||
|
if (!normalized || seen.has(normalized)) continue;
|
||||||
|
seen.add(normalized);
|
||||||
|
ordered.push(normalized);
|
||||||
|
}
|
||||||
|
return ordered;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyHeartbeatTargetHints(
|
||||||
|
hints: ConfigUiHints,
|
||||||
|
channels: ChannelUiMetadata[],
|
||||||
|
): ConfigUiHints {
|
||||||
|
const next: ConfigUiHints = { ...hints };
|
||||||
|
const channelList = listHeartbeatTargetChannels(channels);
|
||||||
|
const channelHelp = channelList.length ? ` Known channels: ${channelList.join(", ")}.` : "";
|
||||||
|
const help = `Delivery target ("last", "none", or a channel id).${channelHelp}`;
|
||||||
|
const paths = ["agents.defaults.heartbeat.target", "agents.list.*.heartbeat.target"];
|
||||||
|
for (const path of paths) {
|
||||||
|
const current = next[path] ?? {};
|
||||||
|
next[path] = {
|
||||||
|
...current,
|
||||||
|
help: current.help ?? help,
|
||||||
|
placeholder: current.placeholder ?? "last",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
function applyPluginSchemas(schema: ConfigSchema, plugins: PluginUiMetadata[]): ConfigSchema {
|
function applyPluginSchemas(schema: ConfigSchema, plugins: PluginUiMetadata[]): ConfigSchema {
|
||||||
const next = cloneSchema(schema);
|
const next = cloneSchema(schema);
|
||||||
const root = asSchemaObject(next);
|
const root = asSchemaObject(next);
|
||||||
@@ -908,7 +947,10 @@ export function buildConfigSchema(params?: {
|
|||||||
const channels = params?.channels ?? [];
|
const channels = params?.channels ?? [];
|
||||||
if (plugins.length === 0 && channels.length === 0) return base;
|
if (plugins.length === 0 && channels.length === 0) return base;
|
||||||
const mergedHints = applySensitiveHints(
|
const mergedHints = applySensitiveHints(
|
||||||
applyChannelHints(applyPluginHints(base.uiHints, plugins), channels),
|
applyHeartbeatTargetHints(
|
||||||
|
applyChannelHints(applyPluginHints(base.uiHints, plugins), channels),
|
||||||
|
channels,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
const mergedSchema = applyChannelSchemas(applyPluginSchemas(base.schema, plugins), channels);
|
const mergedSchema = applyChannelSchemas(applyPluginSchemas(base.schema, plugins), channels);
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type {
|
|||||||
HumanDelayConfig,
|
HumanDelayConfig,
|
||||||
TypingMode,
|
TypingMode,
|
||||||
} from "./types.base.js";
|
} from "./types.base.js";
|
||||||
|
import type { ChannelId } from "../channels/plugins/types.js";
|
||||||
import type {
|
import type {
|
||||||
SandboxBrowserSettings,
|
SandboxBrowserSettings,
|
||||||
SandboxDockerSettings,
|
SandboxDockerSettings,
|
||||||
@@ -177,18 +178,8 @@ export type AgentDefaultsConfig = {
|
|||||||
model?: string;
|
model?: string;
|
||||||
/** Session key for heartbeat runs ("main" or explicit session key). */
|
/** Session key for heartbeat runs ("main" or explicit session key). */
|
||||||
session?: string;
|
session?: string;
|
||||||
/** Delivery target (last|whatsapp|telegram|discord|slack|mattermost|msteams|signal|imessage|none). */
|
/** Delivery target ("last", "none", or a channel id). */
|
||||||
target?:
|
target?: "last" | "none" | ChannelId;
|
||||||
| "last"
|
|
||||||
| "whatsapp"
|
|
||||||
| "telegram"
|
|
||||||
| "discord"
|
|
||||||
| "slack"
|
|
||||||
| "mattermost"
|
|
||||||
| "msteams"
|
|
||||||
| "signal"
|
|
||||||
| "imessage"
|
|
||||||
| "none";
|
|
||||||
/** Optional delivery override (E.164 for WhatsApp, chat id for Telegram). */
|
/** Optional delivery override (E.164 for WhatsApp, chat id for Telegram). */
|
||||||
to?: string;
|
to?: string;
|
||||||
/** Override the heartbeat prompt body (default: "Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK."). */
|
/** Override the heartbeat prompt body (default: "Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK."). */
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
|
||||||
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js";
|
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js";
|
||||||
import { CHANNEL_IDS } from "../channels/registry.js";
|
import { CHANNEL_IDS, normalizeChatChannelId } from "../channels/registry.js";
|
||||||
import {
|
import {
|
||||||
normalizePluginsConfig,
|
normalizePluginsConfig,
|
||||||
resolveEnableState,
|
resolveEnableState,
|
||||||
@@ -226,6 +226,41 @@ export function validateConfigObjectWithPlugins(raw: unknown):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const heartbeatChannelIds = new Set<string>();
|
||||||
|
for (const channelId of CHANNEL_IDS) {
|
||||||
|
heartbeatChannelIds.add(channelId.toLowerCase());
|
||||||
|
}
|
||||||
|
for (const record of registry.plugins) {
|
||||||
|
for (const channelId of record.channels) {
|
||||||
|
const trimmed = channelId.trim();
|
||||||
|
if (trimmed) heartbeatChannelIds.add(trimmed.toLowerCase());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateHeartbeatTarget = (target: string | undefined, path: string) => {
|
||||||
|
if (typeof target !== "string") return;
|
||||||
|
const trimmed = target.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
issues.push({ path, message: "heartbeat target must not be empty" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const normalized = trimmed.toLowerCase();
|
||||||
|
if (normalized === "last" || normalized === "none") return;
|
||||||
|
if (normalizeChatChannelId(trimmed)) return;
|
||||||
|
if (heartbeatChannelIds.has(normalized)) return;
|
||||||
|
issues.push({ path, message: `unknown heartbeat target: ${target}` });
|
||||||
|
};
|
||||||
|
|
||||||
|
validateHeartbeatTarget(
|
||||||
|
config.agents?.defaults?.heartbeat?.target,
|
||||||
|
"agents.defaults.heartbeat.target",
|
||||||
|
);
|
||||||
|
if (Array.isArray(config.agents?.list)) {
|
||||||
|
for (const [index, entry] of config.agents.list.entries()) {
|
||||||
|
validateHeartbeatTarget(entry?.heartbeat?.target, `agents.list.${index}.heartbeat.target`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let selectedMemoryPluginId: string | null = null;
|
let selectedMemoryPluginId: string | null = null;
|
||||||
const seenPlugins = new Set<string>();
|
const seenPlugins = new Set<string>();
|
||||||
for (const record of registry.plugins) {
|
for (const record of registry.plugins) {
|
||||||
|
|||||||
@@ -22,19 +22,7 @@ export const HeartbeatSchema = z
|
|||||||
model: z.string().optional(),
|
model: z.string().optional(),
|
||||||
session: z.string().optional(),
|
session: z.string().optional(),
|
||||||
includeReasoning: z.boolean().optional(),
|
includeReasoning: z.boolean().optional(),
|
||||||
target: z
|
target: z.string().optional(),
|
||||||
.union([
|
|
||||||
z.literal("last"),
|
|
||||||
z.literal("whatsapp"),
|
|
||||||
z.literal("telegram"),
|
|
||||||
z.literal("discord"),
|
|
||||||
z.literal("slack"),
|
|
||||||
z.literal("msteams"),
|
|
||||||
z.literal("signal"),
|
|
||||||
z.literal("imessage"),
|
|
||||||
z.literal("none"),
|
|
||||||
])
|
|
||||||
.optional(),
|
|
||||||
to: z.string().optional(),
|
to: z.string().optional(),
|
||||||
prompt: z.string().optional(),
|
prompt: z.string().optional(),
|
||||||
ackMaxChars: z.number().int().nonnegative().optional(),
|
ackMaxChars: z.number().int().nonnegative().optional(),
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { normalizeAgentId } from "../routing/session-key.js";
|
||||||
import { parseAbsoluteTimeMs } from "./parse.js";
|
import { parseAbsoluteTimeMs } from "./parse.js";
|
||||||
import { migrateLegacyCronPayload } from "./payload-migration.js";
|
import { migrateLegacyCronPayload } from "./payload-migration.js";
|
||||||
import type { CronJobCreate, CronJobPatch } from "./types.js";
|
import type { CronJobCreate, CronJobPatch } from "./types.js";
|
||||||
@@ -75,7 +76,7 @@ export function normalizeCronJobInput(
|
|||||||
next.agentId = null;
|
next.agentId = null;
|
||||||
} else if (typeof agentId === "string") {
|
} else if (typeof agentId === "string") {
|
||||||
const trimmed = agentId.trim();
|
const trimmed = agentId.trim();
|
||||||
if (trimmed) next.agentId = trimmed;
|
if (trimmed) next.agentId = normalizeAgentId(trimmed);
|
||||||
else delete next.agentId;
|
else delete next.agentId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,7 +96,8 @@ function formatDiscordDeployErrorDetails(err: unknown): string {
|
|||||||
try {
|
try {
|
||||||
bodyText = JSON.stringify(rawBody);
|
bodyText = JSON.stringify(rawBody);
|
||||||
} catch {
|
} catch {
|
||||||
bodyText = inspect(rawBody, { depth: 3 });
|
bodyText =
|
||||||
|
typeof rawBody === "string" ? rawBody : inspect(rawBody, { depth: 3, breakLength: 120 });
|
||||||
}
|
}
|
||||||
if (bodyText) {
|
if (bodyText) {
|
||||||
const maxLen = 800;
|
const maxLen = 800;
|
||||||
|
|||||||
@@ -96,9 +96,9 @@ function pickFirstExistingAgentId(cfg: ClawdbotConfig, agentId: string): string
|
|||||||
if (!trimmed) return normalizeAgentId(resolveDefaultAgentId(cfg));
|
if (!trimmed) return normalizeAgentId(resolveDefaultAgentId(cfg));
|
||||||
const normalized = normalizeAgentId(trimmed);
|
const normalized = normalizeAgentId(trimmed);
|
||||||
const agents = listAgents(cfg);
|
const agents = listAgents(cfg);
|
||||||
if (agents.length === 0) return trimmed;
|
if (agents.length === 0) return normalized;
|
||||||
const match = agents.find((agent) => normalizeAgentId(agent.id) === normalized);
|
const match = agents.find((agent) => normalizeAgentId(agent.id) === normalized);
|
||||||
if (match?.id?.trim()) return match.id.trim();
|
if (match?.id?.trim()) return normalizeAgentId(match.id.trim());
|
||||||
return normalizeAgentId(resolveDefaultAgentId(cfg));
|
return normalizeAgentId(resolveDefaultAgentId(cfg));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user