fix: configurable signal startup timeout
This commit is contained in:
@@ -30,6 +30,7 @@ Docs: https://docs.clawd.bot
|
|||||||
- Telegram: use wrapped fetch for long-polling on Node to normalize AbortSignal handling. (#1639)
|
- Telegram: use wrapped fetch for long-polling on Node to normalize AbortSignal handling. (#1639)
|
||||||
- Telegram: set fetch duplex="half" for uploads on Node 22 to avoid sendPhoto failures. (#1684) Thanks @commdata2338.
|
- Telegram: set fetch duplex="half" for uploads on Node 22 to avoid sendPhoto failures. (#1684) Thanks @commdata2338.
|
||||||
- Signal: repair reaction sends (group/UUID targets + CLI author flags). (#1651) Thanks @vilkasdev.
|
- Signal: repair reaction sends (group/UUID targets + CLI author flags). (#1651) Thanks @vilkasdev.
|
||||||
|
- Signal: add configurable signal-cli startup timeout + external daemon mode docs. (#1677) https://docs.clawd.bot/channels/signal
|
||||||
- Exec: keep approvals for elevated ask unless full mode. (#1616) Thanks @ivancasco.
|
- Exec: keep approvals for elevated ask unless full mode. (#1616) Thanks @ivancasco.
|
||||||
- Agents: auto-compact on context overflow prompt errors before failing. (#1627) Thanks @rodrigouroz.
|
- Agents: auto-compact on context overflow prompt errors before failing. (#1627) Thanks @rodrigouroz.
|
||||||
- Agents: use the active auth profile for auto-compaction recovery.
|
- Agents: use the active auth profile for auto-compaction recovery.
|
||||||
|
|||||||
@@ -74,6 +74,22 @@ Example:
|
|||||||
|
|
||||||
Multi-account support: use `channels.signal.accounts` with per-account config and optional `name`. See [`gateway/configuration`](/gateway/configuration#telegramaccounts--discordaccounts--slackaccounts--signalaccounts--imessageaccounts) for the shared pattern.
|
Multi-account support: use `channels.signal.accounts` with per-account config and optional `name`. See [`gateway/configuration`](/gateway/configuration#telegramaccounts--discordaccounts--slackaccounts--signalaccounts--imessageaccounts) for the shared pattern.
|
||||||
|
|
||||||
|
## External daemon mode (httpUrl)
|
||||||
|
If you want to manage `signal-cli` yourself (slow JVM cold starts, container init, or shared CPUs), run the daemon separately and point Clawdbot at it:
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
channels: {
|
||||||
|
signal: {
|
||||||
|
httpUrl: "http://127.0.0.1:8080",
|
||||||
|
autoStart: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This skips auto-spawn and the startup wait inside Clawdbot. For slow starts when auto-spawning, set `channels.signal.startupTimeoutMs`.
|
||||||
|
|
||||||
## Access control (DMs + groups)
|
## Access control (DMs + groups)
|
||||||
DMs:
|
DMs:
|
||||||
- Default: `channels.signal.dmPolicy = "pairing"`.
|
- Default: `channels.signal.dmPolicy = "pairing"`.
|
||||||
@@ -142,6 +158,7 @@ Provider options:
|
|||||||
- `channels.signal.httpUrl`: full daemon URL (overrides host/port).
|
- `channels.signal.httpUrl`: full daemon URL (overrides host/port).
|
||||||
- `channels.signal.httpHost`, `channels.signal.httpPort`: daemon bind (default 127.0.0.1:8080).
|
- `channels.signal.httpHost`, `channels.signal.httpPort`: daemon bind (default 127.0.0.1:8080).
|
||||||
- `channels.signal.autoStart`: auto-spawn daemon (default true if `httpUrl` unset).
|
- `channels.signal.autoStart`: auto-spawn daemon (default true if `httpUrl` unset).
|
||||||
|
- `channels.signal.startupTimeoutMs`: startup wait timeout in ms (cap 120000).
|
||||||
- `channels.signal.receiveMode`: `on-start | manual`.
|
- `channels.signal.receiveMode`: `on-start | manual`.
|
||||||
- `channels.signal.ignoreAttachments`: skip attachment downloads.
|
- `channels.signal.ignoreAttachments`: skip attachment downloads.
|
||||||
- `channels.signal.ignoreStories`: ignore stories from the daemon.
|
- `channels.signal.ignoreStories`: ignore stories from the daemon.
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ export type SignalAccountConfig = {
|
|||||||
cliPath?: string;
|
cliPath?: string;
|
||||||
/** Auto-start signal-cli daemon (default: true if httpUrl not set). */
|
/** Auto-start signal-cli daemon (default: true if httpUrl not set). */
|
||||||
autoStart?: boolean;
|
autoStart?: boolean;
|
||||||
|
/** Max time to wait for signal-cli daemon startup (ms, cap 120000). */
|
||||||
|
startupTimeoutMs?: number;
|
||||||
receiveMode?: "on-start" | "manual";
|
receiveMode?: "on-start" | "manual";
|
||||||
ignoreAttachments?: boolean;
|
ignoreAttachments?: boolean;
|
||||||
ignoreStories?: boolean;
|
ignoreStories?: boolean;
|
||||||
|
|||||||
@@ -486,6 +486,7 @@ export const SignalAccountSchemaBase = z
|
|||||||
httpPort: z.number().int().positive().optional(),
|
httpPort: z.number().int().positive().optional(),
|
||||||
cliPath: ExecutableTokenSchema.optional(),
|
cliPath: ExecutableTokenSchema.optional(),
|
||||||
autoStart: z.boolean().optional(),
|
autoStart: z.boolean().optional(),
|
||||||
|
startupTimeoutMs: z.number().int().min(1000).max(120000).optional(),
|
||||||
receiveMode: z.union([z.literal("on-start"), z.literal("manual")]).optional(),
|
receiveMode: z.union([z.literal("on-start"), z.literal("manual")]).optional(),
|
||||||
ignoreAttachments: z.boolean().optional(),
|
ignoreAttachments: z.boolean().optional(),
|
||||||
ignoreStories: z.boolean().optional(),
|
ignoreStories: z.boolean().optional(),
|
||||||
|
|||||||
@@ -126,6 +126,90 @@ describe("monitorSignalProvider tool results", () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("uses startupTimeoutMs override when provided", async () => {
|
||||||
|
const runtime = {
|
||||||
|
log: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
exit: ((code: number): never => {
|
||||||
|
throw new Error(`exit ${code}`);
|
||||||
|
}) as (code: number) => never,
|
||||||
|
};
|
||||||
|
config = {
|
||||||
|
...config,
|
||||||
|
channels: {
|
||||||
|
...config.channels,
|
||||||
|
signal: {
|
||||||
|
autoStart: true,
|
||||||
|
dmPolicy: "open",
|
||||||
|
allowFrom: ["*"],
|
||||||
|
startupTimeoutMs: 60_000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const abortController = new AbortController();
|
||||||
|
streamMock.mockImplementation(async () => {
|
||||||
|
abortController.abort();
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
|
||||||
|
await monitorSignalProvider({
|
||||||
|
autoStart: true,
|
||||||
|
baseUrl: "http://127.0.0.1:8080",
|
||||||
|
abortSignal: abortController.signal,
|
||||||
|
runtime,
|
||||||
|
startupTimeoutMs: 90_000,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(waitForTransportReadyMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(waitForTransportReadyMock).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
timeoutMs: 90_000,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("caps startupTimeoutMs at 2 minutes", async () => {
|
||||||
|
const runtime = {
|
||||||
|
log: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
exit: ((code: number): never => {
|
||||||
|
throw new Error(`exit ${code}`);
|
||||||
|
}) as (code: number) => never,
|
||||||
|
};
|
||||||
|
config = {
|
||||||
|
...config,
|
||||||
|
channels: {
|
||||||
|
...config.channels,
|
||||||
|
signal: {
|
||||||
|
autoStart: true,
|
||||||
|
dmPolicy: "open",
|
||||||
|
allowFrom: ["*"],
|
||||||
|
startupTimeoutMs: 180_000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const abortController = new AbortController();
|
||||||
|
streamMock.mockImplementation(async () => {
|
||||||
|
abortController.abort();
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
|
||||||
|
await monitorSignalProvider({
|
||||||
|
autoStart: true,
|
||||||
|
baseUrl: "http://127.0.0.1:8080",
|
||||||
|
abortSignal: abortController.signal,
|
||||||
|
runtime,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(waitForTransportReadyMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(waitForTransportReadyMock).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
timeoutMs: 120_000,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("sends tool summaries with responsePrefix", async () => {
|
it("sends tool summaries with responsePrefix", async () => {
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
replyMock.mockImplementation(async (_ctx, opts) => {
|
replyMock.mockImplementation(async (_ctx, opts) => {
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export type MonitorSignalOpts = {
|
|||||||
config?: ClawdbotConfig;
|
config?: ClawdbotConfig;
|
||||||
baseUrl?: string;
|
baseUrl?: string;
|
||||||
autoStart?: boolean;
|
autoStart?: boolean;
|
||||||
|
startupTimeoutMs?: number;
|
||||||
cliPath?: string;
|
cliPath?: string;
|
||||||
httpHost?: string;
|
httpHost?: string;
|
||||||
httpPort?: number;
|
httpPort?: number;
|
||||||
@@ -285,6 +286,10 @@ export async function monitorSignalProvider(opts: MonitorSignalOpts = {}): Promi
|
|||||||
const sendReadReceipts = Boolean(opts.sendReadReceipts ?? accountInfo.config.sendReadReceipts);
|
const sendReadReceipts = Boolean(opts.sendReadReceipts ?? accountInfo.config.sendReadReceipts);
|
||||||
|
|
||||||
const autoStart = opts.autoStart ?? accountInfo.config.autoStart ?? !accountInfo.config.httpUrl;
|
const autoStart = opts.autoStart ?? accountInfo.config.autoStart ?? !accountInfo.config.httpUrl;
|
||||||
|
const startupTimeoutMs = Math.min(
|
||||||
|
120_000,
|
||||||
|
Math.max(1_000, opts.startupTimeoutMs ?? accountInfo.config.startupTimeoutMs ?? 30_000),
|
||||||
|
);
|
||||||
const readReceiptsViaDaemon = Boolean(autoStart && sendReadReceipts);
|
const readReceiptsViaDaemon = Boolean(autoStart && sendReadReceipts);
|
||||||
let daemonHandle: ReturnType<typeof spawnSignalDaemon> | null = null;
|
let daemonHandle: ReturnType<typeof spawnSignalDaemon> | null = null;
|
||||||
|
|
||||||
@@ -315,7 +320,7 @@ export async function monitorSignalProvider(opts: MonitorSignalOpts = {}): Promi
|
|||||||
await waitForSignalDaemonReady({
|
await waitForSignalDaemonReady({
|
||||||
baseUrl,
|
baseUrl,
|
||||||
abortSignal: opts.abortSignal,
|
abortSignal: opts.abortSignal,
|
||||||
timeoutMs: 30_000,
|
timeoutMs: startupTimeoutMs,
|
||||||
logAfterMs: 10_000,
|
logAfterMs: 10_000,
|
||||||
logIntervalMs: 10_000,
|
logIntervalMs: 10_000,
|
||||||
runtime,
|
runtime,
|
||||||
|
|||||||
Reference in New Issue
Block a user