fix: land mac node bridge ping loop (#572) (thanks @ngutman)
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
- macOS: add node bridge heartbeat pings to detect half-open sockets and reconnect cleanly. (#572) — thanks @ngutman
|
||||
- CLI: add `sandbox list` and `sandbox recreate` commands for managing Docker sandbox containers after image/config updates. (#563) — thanks @pasogott
|
||||
- Providers: add Microsoft Teams provider with polling, attachments, and CLI send support. (#404) — thanks @onutc
|
||||
- Commands: accept /models as an alias for /model.
|
||||
|
||||
@@ -252,12 +252,17 @@ actor MacNodeBridgeSession {
|
||||
}
|
||||
|
||||
private func send(_ obj: some Encodable) async throws {
|
||||
guard let connection = self.connection else {
|
||||
throw NSError(domain: "Bridge", code: 15, userInfo: [
|
||||
NSLocalizedDescriptionKey: "not connected",
|
||||
])
|
||||
}
|
||||
let data = try self.encoder.encode(obj)
|
||||
var line = Data()
|
||||
line.append(data)
|
||||
line.append(0x0A)
|
||||
try await withCheckedThrowingContinuation(isolation: self) { (cont: CheckedContinuation<Void, Error>) in
|
||||
self.connection?.send(content: line, completion: .contentProcessed { err in
|
||||
connection.send(content: line, completion: .contentProcessed { err in
|
||||
if let err { cont.resume(throwing: err) } else { cont.resume(returning: ()) }
|
||||
})
|
||||
}
|
||||
@@ -334,9 +339,8 @@ actor MacNodeBridgeSession {
|
||||
}
|
||||
|
||||
private func notePong(_ pong: BridgePong) {
|
||||
if pong.id == self.lastPingId || self.lastPingId == nil {
|
||||
self.lastPongAt = Date()
|
||||
}
|
||||
_ = pong
|
||||
self.lastPongAt = Date()
|
||||
}
|
||||
|
||||
private static func makeStateStream(
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import Testing
|
||||
@testable import Clawdbot
|
||||
|
||||
@Suite
|
||||
struct MacNodeBridgeSessionTests {
|
||||
@Test func sendEventThrowsWhenNotConnected() async {
|
||||
let session = MacNodeBridgeSession()
|
||||
|
||||
do {
|
||||
try await session.sendEvent(event: "test", payloadJSON: "{}")
|
||||
Issue.record("Expected sendEvent to throw when disconnected")
|
||||
} catch {
|
||||
let ns = error as NSError
|
||||
#expect(ns.domain == "Bridge")
|
||||
#expect(ns.code == 15)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,14 +179,23 @@ async function ensureDevGatewayConfig(opts: { reset?: boolean }) {
|
||||
mode: "local",
|
||||
bind: "loopback",
|
||||
},
|
||||
agent: {
|
||||
workspace,
|
||||
skipBootstrap: true,
|
||||
},
|
||||
identity: {
|
||||
name: DEV_IDENTITY_NAME,
|
||||
theme: DEV_IDENTITY_THEME,
|
||||
emoji: DEV_IDENTITY_EMOJI,
|
||||
agents: {
|
||||
defaults: {
|
||||
workspace,
|
||||
skipBootstrap: true,
|
||||
},
|
||||
list: [
|
||||
{
|
||||
id: "dev",
|
||||
default: true,
|
||||
workspace,
|
||||
identity: {
|
||||
name: DEV_IDENTITY_NAME,
|
||||
theme: DEV_IDENTITY_THEME,
|
||||
emoji: DEV_IDENTITY_EMOJI,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
await ensureDevWorkspace(workspace);
|
||||
|
||||
@@ -278,7 +278,7 @@ describe("doctor", () => {
|
||||
changes: ["Moved routing.allowFrom → whatsapp.allowFrom."],
|
||||
});
|
||||
|
||||
await doctorCommand(runtime);
|
||||
await doctorCommand(runtime, { nonInteractive: true });
|
||||
|
||||
expect(writeConfigFile).toHaveBeenCalledTimes(1);
|
||||
const written = writeConfigFile.mock.calls[0]?.[0] as Record<
|
||||
|
||||
@@ -114,10 +114,13 @@ export async function doctorCommand(
|
||||
.join("\n"),
|
||||
"Legacy config keys detected",
|
||||
);
|
||||
const migrate = await prompter.confirm({
|
||||
message: "Migrate legacy config entries now?",
|
||||
initialValue: true,
|
||||
});
|
||||
const migrate =
|
||||
options.nonInteractive === true
|
||||
? true
|
||||
: await prompter.confirm({
|
||||
message: "Migrate legacy config entries now?",
|
||||
initialValue: true,
|
||||
});
|
||||
if (migrate) {
|
||||
// Legacy migration (2026-01-02, commit: 16420e5b) — normalize per-provider allowlists; move WhatsApp gating into whatsapp.allowFrom.
|
||||
const { config: migrated, changes } = migrateLegacyConfig(
|
||||
|
||||
@@ -10,12 +10,6 @@ const DEFAULT_MINIMAX_CONTEXT_WINDOW = 200000;
|
||||
const DEFAULT_MINIMAX_MAX_TOKENS = 8192;
|
||||
export const MINIMAX_HOSTED_MODEL_REF = `minimax/${MINIMAX_HOSTED_MODEL_ID}`;
|
||||
|
||||
const DEFAULT_MINIMAX_BASE_URL = "https://api.minimax.io/v1";
|
||||
export const MINIMAX_HOSTED_MODEL_ID = "MiniMax-M2.1";
|
||||
const DEFAULT_MINIMAX_CONTEXT_WINDOW = 200000;
|
||||
const DEFAULT_MINIMAX_MAX_TOKENS = 8192;
|
||||
export const MINIMAX_HOSTED_MODEL_REF = `minimax/${MINIMAX_HOSTED_MODEL_ID}`;
|
||||
|
||||
export async function writeOAuthCredentials(
|
||||
provider: OAuthProvider,
|
||||
creds: OAuthCredentials,
|
||||
@@ -176,7 +170,7 @@ export function applyMinimaxHostedProviderConfig(
|
||||
cfg: ClawdbotConfig,
|
||||
params?: { baseUrl?: string },
|
||||
): ClawdbotConfig {
|
||||
const models = { ...cfg.agent?.models };
|
||||
const models = { ...cfg.agents?.defaults?.models };
|
||||
models[MINIMAX_HOSTED_MODEL_REF] = {
|
||||
...models[MINIMAX_HOSTED_MODEL_REF],
|
||||
alias: models[MINIMAX_HOSTED_MODEL_REF]?.alias ?? "Minimax",
|
||||
@@ -212,9 +206,12 @@ export function applyMinimaxHostedProviderConfig(
|
||||
|
||||
return {
|
||||
...cfg,
|
||||
agent: {
|
||||
...cfg.agent,
|
||||
models,
|
||||
agents: {
|
||||
...cfg.agents,
|
||||
defaults: {
|
||||
...cfg.agents?.defaults,
|
||||
models,
|
||||
},
|
||||
},
|
||||
models: {
|
||||
mode: cfg.models?.mode ?? "merge",
|
||||
@@ -254,17 +251,14 @@ export function applyMinimaxHostedConfig(
|
||||
const next = applyMinimaxHostedProviderConfig(cfg, params);
|
||||
return {
|
||||
...next,
|
||||
agent: {
|
||||
...next.agent,
|
||||
model: {
|
||||
...(next.agent?.model &&
|
||||
"fallbacks" in (next.agent.model as Record<string, unknown>)
|
||||
? {
|
||||
fallbacks: (next.agent.model as { fallbacks?: string[] })
|
||||
.fallbacks,
|
||||
}
|
||||
: undefined),
|
||||
primary: MINIMAX_HOSTED_MODEL_REF,
|
||||
agents: {
|
||||
...next.agents,
|
||||
defaults: {
|
||||
...next.agents?.defaults,
|
||||
model: {
|
||||
...(next.agents?.defaults?.model ?? {}),
|
||||
primary: MINIMAX_HOSTED_MODEL_REF,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -546,7 +546,8 @@ async function promptWhatsAppAllowFrom(
|
||||
"WhatsApp number",
|
||||
);
|
||||
const entry = await prompter.text({
|
||||
message: "Your personal WhatsApp number (the phone you will message from)",
|
||||
message:
|
||||
"Your personal WhatsApp number (the phone you will message from)",
|
||||
placeholder: "+15555550123",
|
||||
initialValue: existingAllowFrom[0],
|
||||
validate: (value) => {
|
||||
@@ -613,7 +614,8 @@ async function promptWhatsAppAllowFrom(
|
||||
"WhatsApp number",
|
||||
);
|
||||
const entry = await prompter.text({
|
||||
message: "Your personal WhatsApp number (the phone you will message from)",
|
||||
message:
|
||||
"Your personal WhatsApp number (the phone you will message from)",
|
||||
placeholder: "+15555550123",
|
||||
initialValue: existingAllowFrom[0],
|
||||
validate: (value) => {
|
||||
|
||||
@@ -1211,6 +1211,7 @@ export type AgentDefaultsConfig = {
|
||||
| "slack"
|
||||
| "signal"
|
||||
| "imessage"
|
||||
| "msteams"
|
||||
| "none";
|
||||
/** Optional delivery override (E.164 for WhatsApp, chat id for Telegram). */
|
||||
to?: string;
|
||||
|
||||
@@ -56,8 +56,9 @@ export async function monitorMSTeamsProvider(
|
||||
const textLimit = resolveTextChunkLimit(cfg, "msteams");
|
||||
const MB = 1024 * 1024;
|
||||
const mediaMaxBytes =
|
||||
typeof cfg.agent?.mediaMaxMb === "number" && cfg.agent.mediaMaxMb > 0
|
||||
? Math.floor(cfg.agent.mediaMaxMb * MB)
|
||||
typeof cfg.agents?.defaults?.mediaMaxMb === "number" &&
|
||||
cfg.agents.defaults.mediaMaxMb > 0
|
||||
? Math.floor(cfg.agents.defaults.mediaMaxMb * MB)
|
||||
: 8 * MB;
|
||||
const conversationStore =
|
||||
opts.conversationStore ?? createMSTeamsConversationStoreFs();
|
||||
|
||||
@@ -122,9 +122,7 @@ export async function sendMessageTelegram(
|
||||
const client: ApiClientOptions | undefined = fetchImpl
|
||||
? { fetch: fetchImpl as unknown as ApiClientOptions["fetch"] }
|
||||
: undefined;
|
||||
const api =
|
||||
opts.api ??
|
||||
new Bot(token, client ? { client } : undefined).api;
|
||||
const api = opts.api ?? new Bot(token, client ? { client } : undefined).api;
|
||||
const mediaUrl = opts.mediaUrl?.trim();
|
||||
|
||||
// Build optional params for forum topics and reply threading.
|
||||
@@ -296,9 +294,7 @@ export async function reactMessageTelegram(
|
||||
const client: ApiClientOptions | undefined = fetchImpl
|
||||
? { fetch: fetchImpl as unknown as ApiClientOptions["fetch"] }
|
||||
: undefined;
|
||||
const api =
|
||||
opts.api ??
|
||||
new Bot(token, client ? { client } : undefined).api;
|
||||
const api = opts.api ?? new Bot(token, client ? { client } : undefined).api;
|
||||
const request = createTelegramRetryRunner({
|
||||
retry: opts.retry,
|
||||
configRetry: account.config.retry,
|
||||
|
||||
@@ -11,10 +11,7 @@ export async function setTelegramWebhook(opts: {
|
||||
const client: ApiClientOptions | undefined = fetchImpl
|
||||
? { fetch: fetchImpl as unknown as ApiClientOptions["fetch"] }
|
||||
: undefined;
|
||||
const bot = new Bot(
|
||||
opts.token,
|
||||
client ? { client } : undefined,
|
||||
);
|
||||
const bot = new Bot(opts.token, client ? { client } : undefined);
|
||||
await bot.api.setWebhook(opts.url, {
|
||||
secret_token: opts.secret,
|
||||
drop_pending_updates: opts.dropPendingUpdates ?? false,
|
||||
@@ -26,9 +23,6 @@ export async function deleteTelegramWebhook(opts: { token: string }) {
|
||||
const client: ApiClientOptions | undefined = fetchImpl
|
||||
? { fetch: fetchImpl as unknown as ApiClientOptions["fetch"] }
|
||||
: undefined;
|
||||
const bot = new Bot(
|
||||
opts.token,
|
||||
client ? { client } : undefined,
|
||||
);
|
||||
const bot = new Bot(opts.token, client ? { client } : undefined);
|
||||
await bot.api.deleteWebhook();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user