fix: daemon status guidance and telegram fetch
This commit is contained in:
@@ -21,6 +21,7 @@
|
|||||||
### Fixes
|
### Fixes
|
||||||
- CLI/Daemon: add `clawdbot logs` tailing and improve restart/service hints across platforms.
|
- CLI/Daemon: add `clawdbot logs` tailing and improve restart/service hints across platforms.
|
||||||
- Gateway/CLI/Doctor: tighten LAN bind auth checks, warn/migrate mis-keyed gateway tokens, and surface last gateway error when daemon looks running but the port is closed.
|
- Gateway/CLI/Doctor: tighten LAN bind auth checks, warn/migrate mis-keyed gateway tokens, and surface last gateway error when daemon looks running but the port is closed.
|
||||||
|
- CLI/Daemon: add `clawdbot daemon install --force` and expand daemon status guidance for config mismatches and probe targets.
|
||||||
- Auto-reply: keep typing indicators alive during tool execution without changing typing-mode semantics. Thanks @thesash for PR #452.
|
- Auto-reply: keep typing indicators alive during tool execution without changing typing-mode semantics. Thanks @thesash for PR #452.
|
||||||
- macOS: harden Voice Wake tester/runtime (pause trigger, mic persistence, local-only tester) and keep transcript logs private. Thanks @xadenryan for PR #438.
|
- macOS: harden Voice Wake tester/runtime (pause trigger, mic persistence, local-only tester) and keep transcript logs private. Thanks @xadenryan for PR #438.
|
||||||
- macOS: preserve node bridge tunnel port override so remote nodes connect on the bridge port. Thanks @sircrumpet for PR #364.
|
- macOS: preserve node bridge tunnel port override so remote nodes connect on the bridge port. Thanks @sircrumpet for PR #364.
|
||||||
@@ -82,6 +83,7 @@
|
|||||||
- Sub-agents: allow `sessions_spawn` model overrides and error on invalid models. Thanks @azade-c for PR #298.
|
- Sub-agents: allow `sessions_spawn` model overrides and error on invalid models. Thanks @azade-c for PR #298.
|
||||||
- Sub-agents: skip invalid model overrides with a warning and keep the run alive; tool exceptions now return tool errors instead of crashing the agent.
|
- Sub-agents: skip invalid model overrides with a warning and keep the run alive; tool exceptions now return tool errors instead of crashing the agent.
|
||||||
- Sub-agents: allow `sessions_spawn` to target other agents via per-agent allowlists (`routing.agents.<agentId>.subagents.allowAgents`).
|
- Sub-agents: allow `sessions_spawn` to target other agents via per-agent allowlists (`routing.agents.<agentId>.subagents.allowAgents`).
|
||||||
|
- Sub-agents: treat `sessions_spawn` allowlists as case-insensitive.
|
||||||
- Tools: add `agents_list` to reveal allowed `sessions_spawn` targets for the current agent.
|
- Tools: add `agents_list` to reveal allowed `sessions_spawn` targets for the current agent.
|
||||||
- Sessions: forward explicit sessionKey through gateway/chat/node bridge to avoid sub-agent sessionId mixups.
|
- Sessions: forward explicit sessionKey through gateway/chat/node bridge to avoid sub-agent sessionId mixups.
|
||||||
- Heartbeat: default interval 30m; clarified default prompt usage and HEARTBEAT.md template behavior.
|
- Heartbeat: default interval 30m; clarified default prompt usage and HEARTBEAT.md template behavior.
|
||||||
@@ -107,8 +109,7 @@
|
|||||||
- Telegram: honor `/activation` session mode for group mention gating and clarify group activation docs. Thanks @julianengel for PR #377.
|
- Telegram: honor `/activation` session mode for group mention gating and clarify group activation docs. Thanks @julianengel for PR #377.
|
||||||
- Telegram: isolate forum topic transcripts per thread and validate Gemini turn ordering in multi-topic sessions. Thanks @hsrvc for PR #407.
|
- Telegram: isolate forum topic transcripts per thread and validate Gemini turn ordering in multi-topic sessions. Thanks @hsrvc for PR #407.
|
||||||
- Telegram: render Telegram-safe HTML for outbound formatting and fall back to plain text on parse errors. Thanks @RandyVentures for PR #435.
|
- Telegram: render Telegram-safe HTML for outbound formatting and fall back to plain text on parse errors. Thanks @RandyVentures for PR #435.
|
||||||
- Telegram: force grammY to use native fetch under Bun for BAN compatibility (avoids TLS chain errors).
|
- Telegram: prefer native fetch when available (Node 18+ + Bun) for BAN compatibility; still respects proxy override.
|
||||||
- Telegram: keep grammY default fetch on Node; only override under Bun to avoid Node 24 regressions.
|
|
||||||
- iMessage: ignore disconnect errors during shutdown (avoid unhandled promise rejections). Thanks @antons for PR #359.
|
- iMessage: ignore disconnect errors during shutdown (avoid unhandled promise rejections). Thanks @antons for PR #359.
|
||||||
- Messages: stop defaulting ack reactions to 👀 when identity emoji is missing.
|
- Messages: stop defaulting ack reactions to 👀 when identity emoji is missing.
|
||||||
- Auto-reply: require slash for control commands to avoid false triggers in normal text.
|
- Auto-reply: require slash for control commands to avoid false triggers in normal text.
|
||||||
|
|||||||
@@ -106,15 +106,14 @@ export function createSessionsSpawnTool(opts?: {
|
|||||||
const allowAgents =
|
const allowAgents =
|
||||||
resolveAgentConfig(cfg, requesterAgentId)?.subagents?.allowAgents ??
|
resolveAgentConfig(cfg, requesterAgentId)?.subagents?.allowAgents ??
|
||||||
[];
|
[];
|
||||||
const allowAny = allowAgents.some(
|
const allowAny = allowAgents.some((value) => value.trim() === "*");
|
||||||
(value) => value.trim() === "*",
|
const normalizedTargetId = targetAgentId.toLowerCase();
|
||||||
);
|
|
||||||
const allowSet = new Set(
|
const allowSet = new Set(
|
||||||
allowAgents
|
allowAgents
|
||||||
.filter((value) => value.trim() && value.trim() !== "*")
|
.filter((value) => value.trim() && value.trim() !== "*")
|
||||||
.map((value) => normalizeAgentId(value)),
|
.map((value) => normalizeAgentId(value).toLowerCase()),
|
||||||
);
|
);
|
||||||
if (!allowAny && !allowSet.has(targetAgentId)) {
|
if (!allowAny && !allowSet.has(normalizedTargetId)) {
|
||||||
const allowedText = allowAny
|
const allowedText = allowAny
|
||||||
? "*"
|
? "*"
|
||||||
: allowSet.size > 0
|
: allowSet.size > 0
|
||||||
|
|||||||
@@ -130,6 +130,7 @@ export type DaemonInstallOptions = {
|
|||||||
port?: string | number;
|
port?: string | number;
|
||||||
runtime?: string;
|
runtime?: string;
|
||||||
token?: string;
|
token?: string;
|
||||||
|
force?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
function parsePort(raw: unknown): number | null {
|
function parsePort(raw: unknown): number | null {
|
||||||
@@ -192,6 +193,14 @@ function safeDaemonEnv(env: Record<string, string> | undefined): string[] {
|
|||||||
return lines;
|
return lines;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeListenerAddress(raw: string): string {
|
||||||
|
let value = raw.trim();
|
||||||
|
if (!value) return value;
|
||||||
|
value = value.replace(/^TCP\s+/i, "");
|
||||||
|
value = value.replace(/\s+\(LISTEN\)\s*$/i, "");
|
||||||
|
return value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
async function probeGatewayStatus(opts: {
|
async function probeGatewayStatus(opts: {
|
||||||
url: string;
|
url: string;
|
||||||
token?: string;
|
token?: string;
|
||||||
@@ -330,7 +339,7 @@ async function gatherDaemonStatus(opts: {
|
|||||||
const serviceEnv = command?.environment ?? undefined;
|
const serviceEnv = command?.environment ?? undefined;
|
||||||
const mergedDaemonEnv = {
|
const mergedDaemonEnv = {
|
||||||
...(process.env as Record<string, string | undefined>),
|
...(process.env as Record<string, string | undefined>),
|
||||||
...(serviceEnv ?? {}),
|
...(serviceEnv ?? undefined),
|
||||||
} satisfies Record<string, string | undefined>;
|
} satisfies Record<string, string | undefined>;
|
||||||
|
|
||||||
const cliConfigPath = resolveConfigPath(
|
const cliConfigPath = resolveConfigPath(
|
||||||
@@ -389,7 +398,7 @@ async function gatherDaemonStatus(opts: {
|
|||||||
const probeUrl = probeUrlOverride ?? `ws://${probeHost}:${daemonPort}`;
|
const probeUrl = probeUrlOverride ?? `ws://${probeHost}:${daemonPort}`;
|
||||||
const probeNote =
|
const probeNote =
|
||||||
!probeUrlOverride && bindMode === "lan"
|
!probeUrlOverride && bindMode === "lan"
|
||||||
? "Local probe uses loopback (127.0.0.1); gateway bind=lan listens on 0.0.0.0."
|
? "Local probe uses loopback (127.0.0.1). bind=lan listens on 0.0.0.0 (all interfaces); use a LAN IP for remote clients."
|
||||||
: !probeUrlOverride && bindMode === "loopback"
|
: !probeUrlOverride && bindMode === "loopback"
|
||||||
? "Loopback-only gateway; only local clients can connect."
|
? "Loopback-only gateway; only local clients can connect."
|
||||||
: undefined;
|
: undefined;
|
||||||
@@ -539,6 +548,9 @@ function printDaemonStatus(status: DaemonStatus, opts: { json: boolean }) {
|
|||||||
defaultRuntime.error(
|
defaultRuntime.error(
|
||||||
"Root cause: CLI and daemon are using different config paths (likely a profile/state-dir mismatch).",
|
"Root cause: CLI and daemon are using different config paths (likely a profile/state-dir mismatch).",
|
||||||
);
|
);
|
||||||
|
defaultRuntime.error(
|
||||||
|
"Fix: rerun `clawdbot daemon install --force` from the same --profile / CLAWDBOT_STATE_DIR you expect.",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (status.gateway) {
|
if (status.gateway) {
|
||||||
@@ -610,7 +622,7 @@ function printDaemonStatus(status: DaemonStatus, opts: { json: boolean }) {
|
|||||||
const addrs = Array.from(
|
const addrs = Array.from(
|
||||||
new Set(
|
new Set(
|
||||||
status.port.listeners
|
status.port.listeners
|
||||||
.map((l) => l.address?.trim())
|
.map((l) => (l.address ? normalizeListenerAddress(l.address) : ""))
|
||||||
.filter((v): v is string => Boolean(v)),
|
.filter((v): v is string => Boolean(v)),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -729,8 +741,11 @@ export async function runDaemonInstall(opts: DaemonInstallOptions) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (loaded) {
|
if (loaded) {
|
||||||
defaultRuntime.log(`Gateway service already ${service.loadedText}.`);
|
if (!opts.force) {
|
||||||
return;
|
defaultRuntime.log(`Gateway service already ${service.loadedText}.`);
|
||||||
|
defaultRuntime.log("Reinstall with: clawdbot daemon install --force");
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const devMode =
|
const devMode =
|
||||||
@@ -896,6 +911,7 @@ export function registerDaemonCli(program: Command) {
|
|||||||
.option("--port <port>", "Gateway port")
|
.option("--port <port>", "Gateway port")
|
||||||
.option("--runtime <runtime>", "Daemon runtime (node|bun). Default: node")
|
.option("--runtime <runtime>", "Daemon runtime (node|bun). Default: node")
|
||||||
.option("--token <token>", "Gateway token (token auth)")
|
.option("--token <token>", "Gateway token (token auth)")
|
||||||
|
.option("--force", "Reinstall/overwrite if already installed", false)
|
||||||
.action(async (opts) => {
|
.action(async (opts) => {
|
||||||
await runDaemonInstall(opts);
|
await runDaemonInstall(opts);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,14 +19,18 @@ function formatSpawnDetail(result: {
|
|||||||
}): string {
|
}): string {
|
||||||
const clean = (value: string | Buffer | null | undefined) => {
|
const clean = (value: string | Buffer | null | undefined) => {
|
||||||
const text =
|
const text =
|
||||||
typeof value === "string"
|
typeof value === "string" ? value : value ? value.toString() : "";
|
||||||
? value
|
|
||||||
: value
|
|
||||||
? value.toString()
|
|
||||||
: "";
|
|
||||||
return text.replace(/\s+/g, " ").trim();
|
return text.replace(/\s+/g, " ").trim();
|
||||||
};
|
};
|
||||||
if (result.error) return String(result.error);
|
if (result.error) {
|
||||||
|
if (result.error instanceof Error) return result.error.message;
|
||||||
|
if (typeof result.error === "string") return result.error;
|
||||||
|
try {
|
||||||
|
return JSON.stringify(result.error);
|
||||||
|
} catch {
|
||||||
|
return "unknown error";
|
||||||
|
}
|
||||||
|
}
|
||||||
const stderr = clean(result.stderr);
|
const stderr = clean(result.stderr);
|
||||||
if (stderr) return stderr;
|
if (stderr) return stderr;
|
||||||
const stdout = clean(result.stdout);
|
const stdout = clean(result.stdout);
|
||||||
@@ -42,6 +46,9 @@ function normalizeSystemdUnit(raw?: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function triggerClawdbotRestart(): RestartAttempt {
|
export function triggerClawdbotRestart(): RestartAttempt {
|
||||||
|
if (process.env.VITEST || process.env.NODE_ENV === "test") {
|
||||||
|
return { ok: true, method: "supervisor", detail: "test mode" };
|
||||||
|
}
|
||||||
const tried: string[] = [];
|
const tried: string[] = [];
|
||||||
if (process.platform !== "darwin") {
|
if (process.platform !== "darwin") {
|
||||||
if (process.platform === "linux") {
|
if (process.platform === "linux") {
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
// Bun compatibility: force native fetch under Bun; keep grammY defaults on Node.
|
// Ensure native fetch is used when available (Bun + Node 18+).
|
||||||
export function resolveTelegramFetch(
|
export function resolveTelegramFetch(
|
||||||
proxyFetch?: typeof fetch,
|
proxyFetch?: typeof fetch,
|
||||||
): typeof fetch | undefined {
|
): typeof fetch | undefined {
|
||||||
if (proxyFetch) return proxyFetch;
|
if (proxyFetch) return proxyFetch;
|
||||||
const isBun = "Bun" in globalThis || Boolean(process?.versions?.bun);
|
|
||||||
if (!isBun) return undefined;
|
|
||||||
const fetchImpl = globalThis.fetch;
|
const fetchImpl = globalThis.fetch;
|
||||||
|
const isBun = "Bun" in globalThis || Boolean(process?.versions?.bun);
|
||||||
if (!fetchImpl) {
|
if (!fetchImpl) {
|
||||||
throw new Error("fetch is not available; set telegram.proxy in config");
|
if (isBun) {
|
||||||
|
throw new Error("fetch is not available; set telegram.proxy in config");
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
return fetchImpl;
|
return fetchImpl;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user