mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 14:01:24 +08:00
Compare commits
51 Commits
codex/fix-
...
v2026.4.25
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
095e1a90f5 | ||
|
|
227a07558b | ||
|
|
773e302179 | ||
|
|
ec71b01f71 | ||
|
|
ca9fb36d53 | ||
|
|
1f194f1d55 | ||
|
|
a188d486dd | ||
|
|
a4266be808 | ||
|
|
90c40e9f90 | ||
|
|
b77514b6d9 | ||
|
|
a813219b6b | ||
|
|
4ac1406644 | ||
|
|
4d0e1470df | ||
|
|
6ecae22943 | ||
|
|
2c5ac5c0e2 | ||
|
|
8c309aa3de | ||
|
|
3c89b16fb0 | ||
|
|
ef447c43c7 | ||
|
|
ddb66a71af | ||
|
|
9b1583112a | ||
|
|
865fde8f72 | ||
|
|
ccc8d71461 | ||
|
|
a947464403 | ||
|
|
63803d78f4 | ||
|
|
dcad0256b2 | ||
|
|
12b1a63b84 | ||
|
|
6ea3f30b9b | ||
|
|
660dcf2c94 | ||
|
|
26ab654da2 | ||
|
|
5bc728d480 | ||
|
|
3779853ef9 | ||
|
|
b4ff947206 | ||
|
|
1e464867e7 | ||
|
|
ea9da71f03 | ||
|
|
1dbc246e29 | ||
|
|
41c7256420 | ||
|
|
b7733c48c0 | ||
|
|
50565b05aa | ||
|
|
2e10d87919 | ||
|
|
0ca3fae91a | ||
|
|
308ba59151 | ||
|
|
6ca5907692 | ||
|
|
b9758bf44a | ||
|
|
b923421129 | ||
|
|
c6276d6b19 | ||
|
|
399b41bbdb | ||
|
|
1ce1713139 | ||
|
|
1768995c37 | ||
|
|
ced0e96cf2 | ||
|
|
dd13141903 | ||
|
|
072a5ae4b0 |
@@ -41,9 +41,16 @@ Use this skill for release and publish-time workflow. Keep ordinary development
|
||||
recommended replacement can shift as plugin ownership, externalization, and
|
||||
config footprint move, so do not blindly copy stale replacement annotations
|
||||
into release notes.
|
||||
- Do not delete or rewrite beta tags after they leave the machine. If a
|
||||
published or pushed beta needs a fix, commit the fix on the release branch and
|
||||
- Do not delete or rewrite any beta tag after the matching npm package has been
|
||||
published, or after a GitHub release/prerelease was created from that tag. If
|
||||
an npm-published beta needs a fix, commit the fix on the release branch and
|
||||
increment to the next `-beta.N`.
|
||||
- Beta-only Git tags that were pushed for preflight but never published to npm
|
||||
may be moved or replaced when the operator explicitly approves it. Before
|
||||
retagging, verify `npm view openclaw@YYYY.M.D-beta.N version` is unpublished
|
||||
and no GitHub release/prerelease exists for the tag; after retagging, push the
|
||||
updated tag intentionally and rerun npm preflight because older preflight
|
||||
artifacts are tied to the previous tag SHA.
|
||||
- For a beta release train, run the fast local preflight first, publish the
|
||||
beta to npm `beta`, then run the expensive published-package roster focused
|
||||
on install/update/Docker/Parallels/NPM Telegram. If anything fails, fix it on
|
||||
|
||||
18
CHANGELOG.md
18
CHANGELOG.md
@@ -118,6 +118,24 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Auto-reply: poison inbound message dedupe after replay-unsafe provider/runtime failures so retries stay safe before visible progress but cannot duplicate messages after block output, tool side effects, or session progress. Fixes #69303; keeps #58549 and #64606 as duplicate validation. Thanks @martingarramon, @NikolaFC, and @zeroth-blip.
|
||||
- Plugins/CLI: let flag-driven `openclaw channels add` install the selected channel plugin from its default source without opening an interactive prompt, fixing published npm Telegram setup in stdin-closed automation. Thanks @codex.
|
||||
- Plugins/startup: load the default `memory-core` slot during Gateway startup when permitted so active-memory recall can call `memory_search` and `memory_get` without requiring an explicit `plugins.slots.memory` entry, while preserving `plugins.slots.memory: "none"`. Thanks @codex.
|
||||
- Plugins/CLI: prefer native require for compiled bundled plugin JavaScript before jiti so read-only config, status, device, and node commands avoid unnecessary transform overhead on slow hosts. Fixes #62842. Thanks @Effet.
|
||||
- Plugins/compat: inventory doctor-side deprecation migrations separately from runtime plugin compatibility so release sweeps preserve needed repairs while enforcing dated removal windows. Thanks @vincentkoc.
|
||||
- Plugins/compat: add missing dated compatibility records for legacy extension-api, memory registration, provider hook/type aliases, runtime aliases, channel SDK helpers, and approval/test utility shims. Thanks @vincentkoc.
|
||||
- Plugins/CLI: refresh the persisted registry after managed plugin files are removed so ClawHub uninstall cannot leave stale `plugins list` entries. Thanks @codex.
|
||||
- Plugins/CLI: make plugin install and uninstall config writes conflict-aware, clear stale denylist entries on explicit reinstall/removal, and delete managed plugin files only after config/index commit succeeds. Thanks @codex.
|
||||
- Plugins: fail `plugins update` when tracked plugin or hook updates error, keep bundled runtime-dependency repair behind restrictive allowlists, and reject package installs with unloadable extension entries. Thanks @codex.
|
||||
- Gateway/chat: keep duplicate attachment-backed `chat.send` retries with the same idempotency key on the documented in-flight path so aborts still target the real active run. Fixes #70139. Thanks @Feelw00.
|
||||
- Plugins: share package entrypoint resolution between install and discovery, reject mismatched `runtimeExtensions`, and cache bundled runtime-dependency manifest reads during scans. Thanks @codex.
|
||||
- WhatsApp/Web: keep quiet but healthy linked-device sessions connected by basing the watchdog on WhatsApp Web transport activity, while retaining a longer app-silence cap so frame activity cannot mask a stuck session forever. Fixes #70678; carries forward the focused #71466 approach and keeps #63939 as related configurable-timeout follow-up. Thanks @vincentkoc and @oromeis.
|
||||
- Onboarding/setup: keep first-run config reads, plugin compatibility notices, and post-model sanity checks on cold metadata paths unless the user chooses to browse all models, avoiding full plugin/runtime catalog work between prompts. Thanks @shakkernerd.
|
||||
- Onboarding/auth: run manifest-owned provider auth choices through scoped setup providers so selecting OpenAI Codex browser/device auth no longer loads every provider runtime before OAuth starts. Thanks @shakkernerd.
|
||||
- Onboarding/auth: keep the post-auth default-model policy lookup on manifest/setup metadata so the next prompt appears without loading broad provider runtime. Thanks @shakkernerd.
|
||||
- Onboarding/models: keep skip-auth and provider-scoped model picker prompts off the full global model catalog path, and cache provider catalog hook resolution so setup no longer stalls after auth on large plugin registries. Thanks @shakkernerd.
|
||||
- Gateway/Bonjour: suppress known @homebridge/ciao cancellation and network assertion failures through scoped process handlers so malformed mDNS packets or restricted VPS networking disable/restart Bonjour instead of crashing the gateway. Fixes #67578. Thanks @zenassist26-create.
|
||||
- Discord: keep late clicks on already-resolved exec approval buttons quiet when elevated mode auto-resolved the request, while still surfacing real approval submission failures. Fixes #66906. Thanks @rlerikse.
|
||||
- Agents/subagents: deliver completed yielded-subagent results back to no-thread requester routes via direct fallback when the dormant parent announce turn produces no visible reply, and add QA-lab coverage for the regression. Thanks @vincentkoc.
|
||||
- Gateway/Tailscale: let Tailscale-authenticated Control UI operator sessions with browser device identity skip the device-pairing round trip while still rejecting device-less and node-role connections. Refs #71986. Thanks @jokedul.
|
||||
- Doctor: honor `OPENCLAW_SERVICE_REPAIR_POLICY=external` by reporting gateway service health while skipping service install/start/restart/bootstrap, supervisor rewrites, and legacy service cleanup for externally managed environments. Thanks @shakkernerd.
|
||||
|
||||
@@ -65,8 +65,8 @@ android {
|
||||
applicationId = "ai.openclaw.app"
|
||||
minSdk = 31
|
||||
targetSdk = 36
|
||||
versionCode = 2026042600
|
||||
versionName = "2026.4.26"
|
||||
versionCode = 2026042500
|
||||
versionName = "2026.4.25"
|
||||
ndk {
|
||||
// Support all major ABIs — native libs are tiny (~47 KB per ABI)
|
||||
abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
# OpenClaw iOS Changelog
|
||||
|
||||
## 2026.4.26 - 2026-04-26
|
||||
|
||||
Maintenance update for the current OpenClaw development release.
|
||||
|
||||
## 2026.4.25 - 2026-04-25
|
||||
|
||||
Maintenance update for the current OpenClaw development release.
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// Source of truth: apps/ios/version.json
|
||||
// Generated by scripts/ios-sync-versioning.ts.
|
||||
|
||||
OPENCLAW_IOS_VERSION = 2026.4.26
|
||||
OPENCLAW_MARKETING_VERSION = 2026.4.26
|
||||
OPENCLAW_IOS_VERSION = 2026.4.25
|
||||
OPENCLAW_MARKETING_VERSION = 2026.4.25
|
||||
OPENCLAW_BUILD_VERSION = 1
|
||||
|
||||
#include? "../build/Version.xcconfig"
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"version": "2026.4.26"
|
||||
"version": "2026.4.25"
|
||||
}
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2026.4.26</string>
|
||||
<string>2026.4.25</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>2026042600</string>
|
||||
<string>2026042500</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>OpenClaw</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
7fa6e35bb9f9d3096d6281f141488be0dcfe15de40dc4f5c0305eb1ff2bc60b6 config-baseline.json
|
||||
91a6cec6c5bdf4b0bf955a058955278990a1af31f32f8fcf2ac26d7548fb99e5 config-baseline.json
|
||||
5f5fb87fd46f9cbb84d8af17e00ae3c4b74062e8ad517bc2260ba83da2e9014f config-baseline.core.json
|
||||
7cd9c908f066c143eab2a201efbc9640f483ab28bba92ddeca1d18cc2b528bc3 config-baseline.channel.json
|
||||
080c0a4f2d4175d6d7ab1e38f76b21de32669055c518d75c96e784865d89bf25 config-baseline.channel.json
|
||||
f9e0174988718959fe1923a54496ec5b9262721fe1e7306f32ccb1316d9d9c3f config-baseline.plugin.json
|
||||
|
||||
@@ -146,6 +146,7 @@ OpenClaw recommends running WhatsApp on a separate number when possible. (The ch
|
||||
## Runtime model
|
||||
|
||||
- Gateway owns the WhatsApp socket and reconnect loop.
|
||||
- The reconnect watchdog uses WhatsApp Web transport activity, not only inbound app-message volume, so a quiet linked-device session is not restarted solely because nobody has sent a message recently. A longer application-silence cap still forces a reconnect if transport frames keep arriving but no application messages are handled for the watchdog window.
|
||||
- Outbound sends require an active WhatsApp listener for the target account.
|
||||
- Status and broadcast chats are ignored (`@status`, `@broadcast`).
|
||||
- Direct chats use DM session rules (`session.dmScope`; default `main` collapses DMs to the agent main session).
|
||||
@@ -510,6 +511,10 @@ Behavior notes:
|
||||
<Accordion title="Linked but disconnected / reconnect loop">
|
||||
Symptom: linked account with repeated disconnects or reconnect attempts.
|
||||
|
||||
Quiet accounts can stay connected past the normal message timeout; the watchdog
|
||||
restarts when WhatsApp Web transport activity stops, the socket closes, or
|
||||
application-level activity stays silent beyond the longer safety window.
|
||||
|
||||
Fix:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import "./test-helpers.js";
|
||||
import { EventEmitter } from "node:events";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
@@ -42,25 +43,57 @@ type WebAutoReplyMonitorHarness = {
|
||||
controller: AbortController;
|
||||
run: Promise<unknown>;
|
||||
};
|
||||
type MockSessionSocket = {
|
||||
ev: { on: ReturnType<typeof vi.fn>; off: ReturnType<typeof vi.fn> };
|
||||
ws: EventEmitter & { close: ReturnType<typeof vi.fn> };
|
||||
user: { id: string };
|
||||
};
|
||||
|
||||
export const TEST_NET_IP = "93.184.216.34";
|
||||
const WEB_AUTO_REPLY_SOCKETS_KEY = Symbol.for("openclaw:webAutoReplySessionSockets");
|
||||
|
||||
function getSessionSockets(): MockSessionSocket[] {
|
||||
const store = globalThis as Record<PropertyKey, unknown>;
|
||||
if (!Array.isArray(store[WEB_AUTO_REPLY_SOCKETS_KEY])) {
|
||||
store[WEB_AUTO_REPLY_SOCKETS_KEY] = [];
|
||||
}
|
||||
return store[WEB_AUTO_REPLY_SOCKETS_KEY] as MockSessionSocket[];
|
||||
}
|
||||
|
||||
vi.mock("./session.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("./session.js")>("./session.js");
|
||||
return {
|
||||
...actual,
|
||||
createWaSocket: vi.fn(async () => ({
|
||||
ev: {
|
||||
on: vi.fn(),
|
||||
off: vi.fn(),
|
||||
},
|
||||
ws: { close: vi.fn() },
|
||||
user: { id: "123@s.whatsapp.net" },
|
||||
})),
|
||||
createWaSocket: vi.fn(async () => {
|
||||
const ws = new EventEmitter() as MockSessionSocket["ws"];
|
||||
ws.close = vi.fn();
|
||||
const sock: MockSessionSocket = {
|
||||
ev: {
|
||||
on: vi.fn(),
|
||||
off: vi.fn(),
|
||||
},
|
||||
ws,
|
||||
user: { id: "123@s.whatsapp.net" },
|
||||
};
|
||||
getSessionSockets().push(sock);
|
||||
return sock;
|
||||
}),
|
||||
waitForWaConnection: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
});
|
||||
|
||||
export function getLastWebAutoReplySessionSocket(): MockSessionSocket {
|
||||
const last = getSessionSockets().at(-1);
|
||||
if (!last) {
|
||||
throw new Error("No WhatsApp Web auto-reply test socket created");
|
||||
}
|
||||
return last;
|
||||
}
|
||||
|
||||
export function resetWebAutoReplySessionSockets() {
|
||||
getSessionSockets().length = 0;
|
||||
}
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/agent-runtime", () => ({
|
||||
abortEmbeddedPiRun: vi.fn().mockReturnValue(false),
|
||||
appendCronStyleCurrentTimeLine: (text: string) => text,
|
||||
@@ -166,6 +199,7 @@ export function installWebAutoReplyUnitTestHooks(opts?: { pinDns?: boolean }) {
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
resetWebAutoReplySessionSockets();
|
||||
_resetBaileysMocks();
|
||||
_resetLoadConfigMock();
|
||||
if (opts?.pinDns) {
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
createMockWebListener,
|
||||
createScriptedWebListenerFactory,
|
||||
createWebListenerFactoryCapture,
|
||||
getLastWebAutoReplySessionSocket,
|
||||
installWebAutoReplyTestHomeHooks,
|
||||
installWebAutoReplyUnitTestHooks,
|
||||
makeSessionStore,
|
||||
@@ -255,6 +256,92 @@ describe("web auto-reply connection", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps quiet linked-device sessions open when transport frames keep arriving", async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const sleep = vi.fn(async () => {});
|
||||
const scripted = createScriptedWebListenerFactory();
|
||||
const { controller, run } = startWebAutoReplyMonitor({
|
||||
monitorWebChannelFn: monitorWebChannel as never,
|
||||
listenerFactory: scripted.listenerFactory,
|
||||
sleep,
|
||||
heartbeatSeconds: 60,
|
||||
messageTimeoutMs: 30,
|
||||
watchdogCheckMs: 5,
|
||||
});
|
||||
|
||||
await vi.waitFor(
|
||||
() => {
|
||||
expect(scripted.getListenerCount()).toBe(1);
|
||||
},
|
||||
{ timeout: 250, interval: 2 },
|
||||
);
|
||||
|
||||
const socket = getLastWebAutoReplySessionSocket();
|
||||
await vi.advanceTimersByTimeAsync(20);
|
||||
socket.ws.emit("frame");
|
||||
await vi.advanceTimersByTimeAsync(20);
|
||||
socket.ws.emit("frame");
|
||||
await vi.advanceTimersByTimeAsync(20);
|
||||
|
||||
expect(scripted.getListenerCount()).toBe(1);
|
||||
|
||||
controller.abort();
|
||||
scripted.resolveClose(0, { status: 499, isLoggedOut: false });
|
||||
await Promise.resolve();
|
||||
await run;
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it("does not let transport frames mask application silence forever", async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const sleep = vi.fn(async () => {});
|
||||
const scripted = createScriptedWebListenerFactory();
|
||||
const { controller, run } = startWebAutoReplyMonitor({
|
||||
monitorWebChannelFn: monitorWebChannel as never,
|
||||
listenerFactory: scripted.listenerFactory,
|
||||
sleep,
|
||||
heartbeatSeconds: 60,
|
||||
messageTimeoutMs: 30,
|
||||
watchdogCheckMs: 5,
|
||||
});
|
||||
|
||||
await vi.waitFor(
|
||||
() => {
|
||||
expect(scripted.getListenerCount()).toBe(1);
|
||||
},
|
||||
{ timeout: 250, interval: 2 },
|
||||
);
|
||||
|
||||
const socket = getLastWebAutoReplySessionSocket();
|
||||
for (let elapsedMs = 0; elapsedMs < 140; elapsedMs += 20) {
|
||||
socket.ws.emit("frame");
|
||||
await vi.advanceTimersByTimeAsync(20);
|
||||
}
|
||||
|
||||
await vi.waitFor(
|
||||
() => {
|
||||
expect(scripted.getListenerCount()).toBeGreaterThanOrEqual(2);
|
||||
},
|
||||
{ timeout: 250, interval: 2 },
|
||||
);
|
||||
|
||||
controller.abort();
|
||||
scripted.resolveClose(scripted.getListenerCount() - 1, {
|
||||
status: 499,
|
||||
isLoggedOut: false,
|
||||
error: "aborted",
|
||||
});
|
||||
await Promise.resolve();
|
||||
await run;
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it("gives a reconnected listener a fresh watchdog window", async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
|
||||
@@ -280,6 +280,7 @@ export async function monitorWebChannel(
|
||||
reconnectAttempts: snapshot.reconnectAttempts,
|
||||
messagesHandled: snapshot.handledMessages,
|
||||
lastInboundAt: snapshot.lastInboundAt,
|
||||
lastTransportActivityAt: snapshot.lastTransportActivityAt,
|
||||
authAgeMs,
|
||||
uptimeMs: snapshot.uptimeMs,
|
||||
...(minutesSinceLastMessage !== null && minutesSinceLastMessage > 30
|
||||
@@ -297,20 +298,28 @@ export async function monitorWebChannel(
|
||||
}
|
||||
},
|
||||
onWatchdogTimeout: (snapshot) => {
|
||||
const watchdogBaselineAt = snapshot.lastInboundAt ?? snapshot.startedAt;
|
||||
const minutesSinceLastMessage = Math.floor((Date.now() - watchdogBaselineAt) / 60000);
|
||||
const now = Date.now();
|
||||
const transportSilentMs = now - snapshot.lastTransportActivityAt;
|
||||
const appBaselineAt = snapshot.lastInboundAt ?? snapshot.startedAt;
|
||||
const minutesSinceTransportActivity = Math.floor(transportSilentMs / 60000);
|
||||
const minutesSinceAppActivity = Math.floor((now - appBaselineAt) / 60000);
|
||||
const watchdogReason =
|
||||
transportSilentMs > messageTimeoutMs ? "transport-inactive" : "app-silent";
|
||||
statusController.noteWatchdogStale();
|
||||
heartbeatLogger.warn(
|
||||
{
|
||||
connectionId: snapshot.connectionId,
|
||||
minutesSinceLastMessage,
|
||||
watchdogReason,
|
||||
minutesSinceTransportActivity,
|
||||
minutesSinceAppActivity,
|
||||
lastInboundAt: snapshot.lastInboundAt ? new Date(snapshot.lastInboundAt) : null,
|
||||
lastTransportActivityAt: new Date(snapshot.lastTransportActivityAt),
|
||||
messagesHandled: snapshot.handledMessages,
|
||||
},
|
||||
"Message timeout detected - forcing reconnect",
|
||||
"WhatsApp watchdog timeout detected - forcing reconnect",
|
||||
);
|
||||
whatsappHeartbeatLog.warn(
|
||||
`No messages received in ${minutesSinceLastMessage}m - restarting connection`,
|
||||
`WhatsApp watchdog timeout (${watchdogReason}) - restarting connection`,
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -40,8 +40,10 @@ export type WhatsAppLiveConnection = {
|
||||
heartbeat: TimerHandle | null;
|
||||
watchdogTimer: TimerHandle | null;
|
||||
lastInboundAt: number | null;
|
||||
lastTransportActivityAt: number;
|
||||
handledMessages: number;
|
||||
unregisterUnhandled: (() => void) | null;
|
||||
unregisterTransportActivity: (() => void) | null;
|
||||
backgroundTasks: Set<Promise<unknown>>;
|
||||
closePromise: Promise<WebListenerCloseReason>;
|
||||
resolveClose: (reason: WebListenerCloseReason) => void;
|
||||
@@ -51,6 +53,7 @@ export type WhatsAppConnectionSnapshot = {
|
||||
connectionId: string;
|
||||
startedAt: number;
|
||||
lastInboundAt: number | null;
|
||||
lastTransportActivityAt: number;
|
||||
handledMessages: number;
|
||||
reconnectAttempts: number;
|
||||
uptimeMs: number;
|
||||
@@ -83,6 +86,12 @@ function createNeverResolvePromise<T>(): Promise<T> {
|
||||
return new Promise<T>(() => {});
|
||||
}
|
||||
|
||||
type SocketActivityEmitter = {
|
||||
on?: (event: string, listener: (...args: unknown[]) => void) => void;
|
||||
off?: (event: string, listener: (...args: unknown[]) => void) => void;
|
||||
removeListener?: (event: string, listener: (...args: unknown[]) => void) => void;
|
||||
};
|
||||
|
||||
function createLiveConnection(params: {
|
||||
connectionId: string;
|
||||
sock: WASocket;
|
||||
@@ -108,8 +117,10 @@ function createLiveConnection(params: {
|
||||
heartbeat: null,
|
||||
watchdogTimer: null,
|
||||
lastInboundAt: null,
|
||||
lastTransportActivityAt: Date.now(),
|
||||
handledMessages: 0,
|
||||
unregisterUnhandled: null,
|
||||
unregisterTransportActivity: null,
|
||||
backgroundTasks: new Set<Promise<unknown>>(),
|
||||
closePromise,
|
||||
resolveClose: resolveClosePromise,
|
||||
@@ -232,6 +243,7 @@ export class WhatsAppConnectionController {
|
||||
private readonly heartbeatSeconds: number;
|
||||
private readonly keepAlive: boolean;
|
||||
private readonly messageTimeoutMs: number;
|
||||
private readonly appSilenceTimeoutMs: number;
|
||||
private readonly watchdogCheckMs: number;
|
||||
private readonly verbose: boolean;
|
||||
private readonly abortSignal?: AbortSignal;
|
||||
@@ -262,6 +274,7 @@ export class WhatsAppConnectionController {
|
||||
this.keepAlive = params.keepAlive;
|
||||
this.heartbeatSeconds = params.heartbeatSeconds;
|
||||
this.messageTimeoutMs = params.messageTimeoutMs;
|
||||
this.appSilenceTimeoutMs = Math.max(params.messageTimeoutMs, params.messageTimeoutMs * 4);
|
||||
this.watchdogCheckMs = params.watchdogCheckMs;
|
||||
this.reconnectPolicy = params.reconnectPolicy;
|
||||
this.abortSignal = params.abortSignal;
|
||||
@@ -311,6 +324,14 @@ export class WhatsAppConnectionController {
|
||||
}
|
||||
this.current.handledMessages += 1;
|
||||
this.current.lastInboundAt = timestamp;
|
||||
this.current.lastTransportActivityAt = timestamp;
|
||||
}
|
||||
|
||||
noteTransportActivity(timestamp = Date.now()): void {
|
||||
if (!this.current) {
|
||||
return;
|
||||
}
|
||||
this.current.lastTransportActivityAt = timestamp;
|
||||
}
|
||||
|
||||
getCurrentSnapshot(
|
||||
@@ -323,6 +344,7 @@ export class WhatsAppConnectionController {
|
||||
connectionId: connection.connectionId,
|
||||
startedAt: connection.startedAt,
|
||||
lastInboundAt: connection.lastInboundAt,
|
||||
lastTransportActivityAt: connection.lastTransportActivityAt,
|
||||
handledMessages: connection.handledMessages,
|
||||
reconnectAttempts: this.reconnectAttempts,
|
||||
uptimeMs: Date.now() - connection.startedAt,
|
||||
@@ -369,6 +391,7 @@ export class WhatsAppConnectionController {
|
||||
const listener = await params.createListener({ sock, connection });
|
||||
connection.listener = listener;
|
||||
this.current = connection;
|
||||
connection.unregisterTransportActivity = this.attachTransportActivityListener(sock);
|
||||
registerWhatsAppConnectionController(this.accountId, this);
|
||||
this.startTimers(connection, {
|
||||
onHeartbeat: params.onHeartbeat,
|
||||
@@ -383,6 +406,7 @@ export class WhatsAppConnectionController {
|
||||
if (connection?.unregisterUnhandled) {
|
||||
connection.unregisterUnhandled();
|
||||
}
|
||||
connection?.unregisterTransportActivity?.();
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
@@ -515,6 +539,7 @@ export class WhatsAppConnectionController {
|
||||
this.socketRef.current = null;
|
||||
}
|
||||
connection.unregisterUnhandled?.();
|
||||
connection.unregisterTransportActivity?.();
|
||||
if (connection.heartbeat) {
|
||||
clearInterval(connection.heartbeat);
|
||||
}
|
||||
@@ -563,9 +588,14 @@ export class WhatsAppConnectionController {
|
||||
}, this.heartbeatSeconds * 1000);
|
||||
|
||||
connection.watchdogTimer = setInterval(() => {
|
||||
const baselineAt = connection.lastInboundAt ?? connection.startedAt;
|
||||
const staleForMs = Date.now() - baselineAt;
|
||||
if (staleForMs <= this.messageTimeoutMs) {
|
||||
const now = Date.now();
|
||||
const transportStaleForMs = now - connection.lastTransportActivityAt;
|
||||
const appBaselineAt = connection.lastInboundAt ?? connection.startedAt;
|
||||
const appSilentForMs = now - appBaselineAt;
|
||||
if (
|
||||
transportStaleForMs <= this.messageTimeoutMs &&
|
||||
appSilentForMs <= this.appSilenceTimeoutMs
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const snapshot = this.getCurrentSnapshot(connection);
|
||||
@@ -581,6 +611,24 @@ export class WhatsAppConnectionController {
|
||||
}, this.watchdogCheckMs);
|
||||
}
|
||||
|
||||
private attachTransportActivityListener(sock: WASocket): (() => void) | null {
|
||||
const ws = sock.ws as SocketActivityEmitter | undefined;
|
||||
if (!ws || typeof ws.on !== "function") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const noteActivity = () => this.noteTransportActivity();
|
||||
ws.on("frame", noteActivity);
|
||||
|
||||
return () => {
|
||||
if (typeof ws.off === "function") {
|
||||
ws.off("frame", noteActivity);
|
||||
return;
|
||||
}
|
||||
ws.removeListener?.("frame", noteActivity);
|
||||
};
|
||||
}
|
||||
|
||||
private stopDisconnectRetries(): void {
|
||||
if (!this.disconnectRetryController.signal.aborted) {
|
||||
this.disconnectRetryController.abort();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "openclaw",
|
||||
"version": "2026.4.26",
|
||||
"version": "2026.4.25-beta.8",
|
||||
"description": "Multi-channel AI gateway with extensible messaging integrations",
|
||||
"keywords": [],
|
||||
"homepage": "https://github.com/openclaw/openclaw#readme",
|
||||
|
||||
@@ -18,7 +18,7 @@ $ErrorActionPreference = "Stop"
|
||||
$ACCENT = "`e[38;2;255;77;77m" # coral-bright
|
||||
$SUCCESS = "`e[38;2;0;229;204m" # cyan-bright
|
||||
$WARN = "`e[38;2;255;176;32m" # amber
|
||||
$ERROR = "`e[38;2;230;57;70m" # coral-mid
|
||||
$ERROR_COLOR = "`e[38;2;230;57;70m" # coral-mid
|
||||
$MUTED = "`e[38;2;90;100;128m" # text-muted
|
||||
$NC = "`e[0m" # No Color
|
||||
|
||||
@@ -27,7 +27,7 @@ function Write-Host {
|
||||
$msg = switch ($Level) {
|
||||
"success" { "$SUCCESS✓$NC $Message" }
|
||||
"warn" { "$WARN!$NC $Message" }
|
||||
"error" { "$ERROR✗$NC $Message" }
|
||||
"error" { "$ERROR_COLOR✗$NC $Message" }
|
||||
default { "$MUTED·$NC $Message" }
|
||||
}
|
||||
Microsoft.PowerShell.Utility\Write-Host $msg
|
||||
|
||||
@@ -3392,6 +3392,95 @@ describe("dispatchReplyFromConfig", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("poisons inbound dedupe when dispatch fails after a block reply", async () => {
|
||||
setNoAbort();
|
||||
const ctx = buildTestCtx({
|
||||
Provider: "whatsapp",
|
||||
OriginatingChannel: "whatsapp",
|
||||
OriginatingTo: "whatsapp:+15555550125",
|
||||
To: "whatsapp:+15555550125",
|
||||
AccountId: "default",
|
||||
MessageSid: "msg-dup-block-error",
|
||||
SessionKey: "agent:main:whatsapp:direct:+15555550125",
|
||||
CommandBody: "hello",
|
||||
RawBody: "hello",
|
||||
Body: "hello",
|
||||
});
|
||||
const firstDispatcher = createDispatcher();
|
||||
const replyResolver = vi.fn(
|
||||
async (_ctx: MsgContext, opts?: GetReplyOptions): Promise<ReplyPayload | undefined> => {
|
||||
await opts?.onBlockReply?.({ text: "partial answer" });
|
||||
throw new Error("provider failed after block");
|
||||
},
|
||||
);
|
||||
|
||||
await expect(
|
||||
dispatchReplyFromConfig({
|
||||
ctx,
|
||||
cfg: emptyConfig,
|
||||
dispatcher: firstDispatcher,
|
||||
replyResolver,
|
||||
}),
|
||||
).rejects.toThrow("provider failed after block");
|
||||
|
||||
await dispatchReplyFromConfig({
|
||||
ctx,
|
||||
cfg: emptyConfig,
|
||||
dispatcher: createDispatcher(),
|
||||
replyResolver,
|
||||
});
|
||||
|
||||
expect(firstDispatcher.sendBlockReply).toHaveBeenCalledWith({ text: "partial answer" });
|
||||
expect(replyResolver).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("poisons inbound dedupe when dispatch fails after a suppressed tool result", async () => {
|
||||
setNoAbort();
|
||||
sessionStoreMocks.currentEntry = {
|
||||
sessionId: "s1",
|
||||
updatedAt: 0,
|
||||
sendPolicy: "deny",
|
||||
};
|
||||
const ctx = buildTestCtx({
|
||||
Provider: "whatsapp",
|
||||
OriginatingChannel: "whatsapp",
|
||||
OriginatingTo: "whatsapp:+15555550126",
|
||||
To: "whatsapp:+15555550126",
|
||||
AccountId: "default",
|
||||
MessageSid: "msg-dup-tool-error",
|
||||
SessionKey: "agent:main:whatsapp:direct:+15555550126",
|
||||
CommandBody: "hello",
|
||||
RawBody: "hello",
|
||||
Body: "hello",
|
||||
});
|
||||
const firstDispatcher = createDispatcher();
|
||||
const replyResolver = vi.fn(
|
||||
async (_ctx: MsgContext, opts?: GetReplyOptions): Promise<ReplyPayload | undefined> => {
|
||||
await opts?.onToolResult?.({ text: "tool touched external state" });
|
||||
throw new Error("provider failed after tool");
|
||||
},
|
||||
);
|
||||
|
||||
await expect(
|
||||
dispatchReplyFromConfig({
|
||||
ctx,
|
||||
cfg: emptyConfig,
|
||||
dispatcher: firstDispatcher,
|
||||
replyResolver,
|
||||
}),
|
||||
).rejects.toThrow("provider failed after tool");
|
||||
|
||||
await dispatchReplyFromConfig({
|
||||
ctx,
|
||||
cfg: emptyConfig,
|
||||
dispatcher: createDispatcher(),
|
||||
replyResolver,
|
||||
});
|
||||
|
||||
expect(firstDispatcher.sendToolResult).not.toHaveBeenCalled();
|
||||
expect(replyResolver).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("passes configOverride to replyResolver when provided", async () => {
|
||||
setNoAbort();
|
||||
const cfg = emptyConfig;
|
||||
|
||||
@@ -343,6 +343,10 @@ export async function dispatchReplyFromConfig(
|
||||
recordProcessed("skipped", { reason: "duplicate" });
|
||||
return { queuedFinal: false, counts: dispatcher.getQueuedCounts() };
|
||||
}
|
||||
let inboundDedupeReplayUnsafe = false;
|
||||
const markInboundDedupeReplayUnsafe = () => {
|
||||
inboundDedupeReplayUnsafe = true;
|
||||
};
|
||||
|
||||
const initialSessionStoreEntry = resolveSessionStoreLookup(ctx, cfg);
|
||||
const boundAcpDispatchSessionKey = resolveBoundAcpDispatchSessionKey({ ctx, cfg });
|
||||
@@ -473,6 +477,7 @@ export async function dispatchReplyFromConfig(
|
||||
if (!shouldRouteToOriginating || !routeReplyChannel || !routeReplyTo || !routeReplyRuntime) {
|
||||
return null;
|
||||
}
|
||||
markInboundDedupeReplayUnsafe();
|
||||
return await routeReplyRuntime.routeReply({
|
||||
payload,
|
||||
channel: routeReplyChannel,
|
||||
@@ -538,6 +543,7 @@ export async function dispatchReplyFromConfig(
|
||||
}
|
||||
return result.ok;
|
||||
}
|
||||
markInboundDedupeReplayUnsafe();
|
||||
return mode === "additive"
|
||||
? dispatcher.sendToolResult(payload)
|
||||
: dispatcher.sendFinalReply(payload);
|
||||
@@ -721,6 +727,7 @@ export async function dispatchReplyFromConfig(
|
||||
);
|
||||
}
|
||||
} else {
|
||||
markInboundDedupeReplayUnsafe();
|
||||
queuedFinal = dispatcher.sendFinalReply(payload);
|
||||
}
|
||||
} else {
|
||||
@@ -744,6 +751,9 @@ export async function dispatchReplyFromConfig(
|
||||
const sendFinalPayload = async (
|
||||
payload: ReplyPayload,
|
||||
): Promise<{ queuedFinal: boolean; routedFinalCount: number }> => {
|
||||
if (resolveSendableOutboundReplyParts(payload).hasContent) {
|
||||
markInboundDedupeReplayUnsafe();
|
||||
}
|
||||
const ttsPayload = await maybeApplyTtsToReplyPayload({
|
||||
payload,
|
||||
cfg,
|
||||
@@ -767,6 +777,7 @@ export async function dispatchReplyFromConfig(
|
||||
routedFinalCount: result.ok ? 1 : 0,
|
||||
};
|
||||
}
|
||||
markInboundDedupeReplayUnsafe();
|
||||
return {
|
||||
queuedFinal: dispatcher.sendFinalReply(normalizedPayload),
|
||||
routedFinalCount: 0,
|
||||
@@ -898,6 +909,7 @@ export async function dispatchReplyFromConfig(
|
||||
await sendPayloadAsync(payload, undefined, false);
|
||||
return;
|
||||
}
|
||||
markInboundDedupeReplayUnsafe();
|
||||
dispatcher.sendToolResult(payload);
|
||||
};
|
||||
const sendPlanUpdate = async (payload: {
|
||||
@@ -914,6 +926,7 @@ export async function dispatchReplyFromConfig(
|
||||
await sendPayloadAsync(replyPayload, undefined, false);
|
||||
return;
|
||||
}
|
||||
markInboundDedupeReplayUnsafe();
|
||||
dispatcher.sendToolResult(replyPayload);
|
||||
};
|
||||
const summarizeApprovalLabel = (payload: {
|
||||
@@ -1019,6 +1032,7 @@ export async function dispatchReplyFromConfig(
|
||||
suppressTyping: typing.suppressTyping,
|
||||
onToolResult: (payload: ReplyPayload) => {
|
||||
const run = async () => {
|
||||
markInboundDedupeReplayUnsafe();
|
||||
await onToolResultFromReplyOptions?.(payload);
|
||||
if (suppressDelivery) {
|
||||
return;
|
||||
@@ -1055,12 +1069,14 @@ export async function dispatchReplyFromConfig(
|
||||
if (shouldRouteToOriginating) {
|
||||
await sendPayloadAsync(deliveryPayload, undefined, false);
|
||||
} else {
|
||||
markInboundDedupeReplayUnsafe();
|
||||
dispatcher.sendToolResult(deliveryPayload);
|
||||
}
|
||||
};
|
||||
return run();
|
||||
},
|
||||
onPlanUpdate: async (payload) => {
|
||||
markInboundDedupeReplayUnsafe();
|
||||
await onPlanUpdateFromReplyOptions?.(payload);
|
||||
if (payload.phase !== "update" || suppressDefaultToolProgressMessages) {
|
||||
return;
|
||||
@@ -1068,6 +1084,7 @@ export async function dispatchReplyFromConfig(
|
||||
await sendPlanUpdate({ explanation: payload.explanation, steps: payload.steps });
|
||||
},
|
||||
onApprovalEvent: async (payload) => {
|
||||
markInboundDedupeReplayUnsafe();
|
||||
await onApprovalEventFromReplyOptions?.(payload);
|
||||
if (payload.phase !== "requested" || suppressDefaultToolProgressMessages) {
|
||||
return;
|
||||
@@ -1083,6 +1100,7 @@ export async function dispatchReplyFromConfig(
|
||||
await maybeSendWorkingStatus(label);
|
||||
},
|
||||
onPatchSummary: async (payload) => {
|
||||
markInboundDedupeReplayUnsafe();
|
||||
await onPatchSummaryFromReplyOptions?.(payload);
|
||||
if (payload.phase !== "end" || suppressDefaultToolProgressMessages) {
|
||||
return;
|
||||
@@ -1095,6 +1113,12 @@ export async function dispatchReplyFromConfig(
|
||||
},
|
||||
onBlockReply: (payload: ReplyPayload, context?: BlockReplyContext) => {
|
||||
const run = async () => {
|
||||
if (
|
||||
payload.isReasoning !== true &&
|
||||
resolveSendableOutboundReplyParts(payload).hasContent
|
||||
) {
|
||||
markInboundDedupeReplayUnsafe();
|
||||
}
|
||||
if (suppressDelivery) {
|
||||
return;
|
||||
}
|
||||
@@ -1156,6 +1180,7 @@ export async function dispatchReplyFromConfig(
|
||||
if (shouldRouteToOriginating) {
|
||||
await sendPayloadAsync(normalizedPayload, context?.abortSignal, false);
|
||||
} else {
|
||||
markInboundDedupeReplayUnsafe();
|
||||
dispatcher.sendBlockReply(normalizedPayload);
|
||||
}
|
||||
};
|
||||
@@ -1268,6 +1293,7 @@ export async function dispatchReplyFromConfig(
|
||||
);
|
||||
}
|
||||
} else {
|
||||
markInboundDedupeReplayUnsafe();
|
||||
const didQueue = dispatcher.sendFinalReply(normalizedTtsOnlyPayload);
|
||||
queuedFinal = didQueue || queuedFinal;
|
||||
}
|
||||
@@ -1293,7 +1319,11 @@ export async function dispatchReplyFromConfig(
|
||||
return { queuedFinal, counts };
|
||||
} catch (err) {
|
||||
if (inboundDedupeClaim.status === "claimed") {
|
||||
releaseInboundDedupe(inboundDedupeClaim.key);
|
||||
if (inboundDedupeReplayUnsafe) {
|
||||
commitInboundDedupe(inboundDedupeClaim.key);
|
||||
} else {
|
||||
releaseInboundDedupe(inboundDedupeClaim.key);
|
||||
}
|
||||
}
|
||||
recordProcessed("error", { error: String(err) });
|
||||
markIdle("message_error");
|
||||
|
||||
@@ -72,4 +72,33 @@ describe("inbound dedupe", () => {
|
||||
inboundB.resetInboundDedupe();
|
||||
}
|
||||
});
|
||||
|
||||
it("shares claim/commit state across distinct module instances", async () => {
|
||||
const inboundA = await importFreshModule<typeof import("./inbound-dedupe.js")>(
|
||||
import.meta.url,
|
||||
"./inbound-dedupe.js?scope=commit-a",
|
||||
);
|
||||
const inboundB = await importFreshModule<typeof import("./inbound-dedupe.js")>(
|
||||
import.meta.url,
|
||||
"./inbound-dedupe.js?scope=commit-b",
|
||||
);
|
||||
|
||||
inboundA.resetInboundDedupe();
|
||||
inboundB.resetInboundDedupe();
|
||||
|
||||
try {
|
||||
const firstClaim = inboundA.claimInboundDedupe(sharedInboundContext);
|
||||
expect(firstClaim).toMatchObject({ status: "claimed" });
|
||||
if (firstClaim.status !== "claimed") {
|
||||
throw new Error("expected claimed inbound dedupe result");
|
||||
}
|
||||
inboundA.commitInboundDedupe(firstClaim.key);
|
||||
expect(inboundB.claimInboundDedupe(sharedInboundContext)).toMatchObject({
|
||||
status: "duplicate",
|
||||
});
|
||||
} finally {
|
||||
inboundA.resetInboundDedupe();
|
||||
inboundB.resetInboundDedupe();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1502,6 +1502,181 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
|
||||
type: "string",
|
||||
minLength: 1,
|
||||
},
|
||||
persona: {
|
||||
type: "string",
|
||||
},
|
||||
personas: {
|
||||
type: "object",
|
||||
propertyNames: {
|
||||
type: "string",
|
||||
},
|
||||
additionalProperties: {
|
||||
type: "object",
|
||||
properties: {
|
||||
label: {
|
||||
type: "string",
|
||||
},
|
||||
description: {
|
||||
type: "string",
|
||||
},
|
||||
provider: {
|
||||
type: "string",
|
||||
minLength: 1,
|
||||
},
|
||||
fallbackPolicy: {
|
||||
anyOf: [
|
||||
{
|
||||
type: "string",
|
||||
const: "preserve-persona",
|
||||
},
|
||||
{
|
||||
type: "string",
|
||||
const: "provider-defaults",
|
||||
},
|
||||
{
|
||||
type: "string",
|
||||
const: "fail",
|
||||
},
|
||||
],
|
||||
},
|
||||
prompt: {
|
||||
type: "object",
|
||||
properties: {
|
||||
profile: {
|
||||
type: "string",
|
||||
},
|
||||
scene: {
|
||||
type: "string",
|
||||
},
|
||||
sampleContext: {
|
||||
type: "string",
|
||||
},
|
||||
style: {
|
||||
type: "string",
|
||||
},
|
||||
accent: {
|
||||
type: "string",
|
||||
},
|
||||
pacing: {
|
||||
type: "string",
|
||||
},
|
||||
constraints: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
providers: {
|
||||
type: "object",
|
||||
propertyNames: {
|
||||
type: "string",
|
||||
},
|
||||
additionalProperties: {
|
||||
type: "object",
|
||||
properties: {
|
||||
apiKey: {
|
||||
anyOf: [
|
||||
{
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
oneOf: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
source: {
|
||||
type: "string",
|
||||
const: "env",
|
||||
},
|
||||
provider: {
|
||||
type: "string",
|
||||
pattern: "^[a-z][a-z0-9_-]{0,63}$",
|
||||
},
|
||||
id: {
|
||||
type: "string",
|
||||
pattern: "^[A-Z][A-Z0-9_]{0,127}$",
|
||||
},
|
||||
},
|
||||
required: ["source", "provider", "id"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
source: {
|
||||
type: "string",
|
||||
const: "file",
|
||||
},
|
||||
provider: {
|
||||
type: "string",
|
||||
pattern: "^[a-z][a-z0-9_-]{0,63}$",
|
||||
},
|
||||
id: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
required: ["source", "provider", "id"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
source: {
|
||||
type: "string",
|
||||
const: "exec",
|
||||
},
|
||||
provider: {
|
||||
type: "string",
|
||||
pattern: "^[a-z][a-z0-9_-]{0,63}$",
|
||||
},
|
||||
id: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
required: ["source", "provider", "id"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
additionalProperties: {
|
||||
anyOf: [
|
||||
{
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
type: "number",
|
||||
},
|
||||
{
|
||||
type: "boolean",
|
||||
},
|
||||
{
|
||||
type: "null",
|
||||
},
|
||||
{
|
||||
type: "array",
|
||||
items: {},
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
propertyNames: {
|
||||
type: "string",
|
||||
},
|
||||
additionalProperties: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
summaryModel: {
|
||||
type: "string",
|
||||
},
|
||||
@@ -2682,6 +2857,181 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
|
||||
type: "string",
|
||||
minLength: 1,
|
||||
},
|
||||
persona: {
|
||||
type: "string",
|
||||
},
|
||||
personas: {
|
||||
type: "object",
|
||||
propertyNames: {
|
||||
type: "string",
|
||||
},
|
||||
additionalProperties: {
|
||||
type: "object",
|
||||
properties: {
|
||||
label: {
|
||||
type: "string",
|
||||
},
|
||||
description: {
|
||||
type: "string",
|
||||
},
|
||||
provider: {
|
||||
type: "string",
|
||||
minLength: 1,
|
||||
},
|
||||
fallbackPolicy: {
|
||||
anyOf: [
|
||||
{
|
||||
type: "string",
|
||||
const: "preserve-persona",
|
||||
},
|
||||
{
|
||||
type: "string",
|
||||
const: "provider-defaults",
|
||||
},
|
||||
{
|
||||
type: "string",
|
||||
const: "fail",
|
||||
},
|
||||
],
|
||||
},
|
||||
prompt: {
|
||||
type: "object",
|
||||
properties: {
|
||||
profile: {
|
||||
type: "string",
|
||||
},
|
||||
scene: {
|
||||
type: "string",
|
||||
},
|
||||
sampleContext: {
|
||||
type: "string",
|
||||
},
|
||||
style: {
|
||||
type: "string",
|
||||
},
|
||||
accent: {
|
||||
type: "string",
|
||||
},
|
||||
pacing: {
|
||||
type: "string",
|
||||
},
|
||||
constraints: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
providers: {
|
||||
type: "object",
|
||||
propertyNames: {
|
||||
type: "string",
|
||||
},
|
||||
additionalProperties: {
|
||||
type: "object",
|
||||
properties: {
|
||||
apiKey: {
|
||||
anyOf: [
|
||||
{
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
oneOf: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
source: {
|
||||
type: "string",
|
||||
const: "env",
|
||||
},
|
||||
provider: {
|
||||
type: "string",
|
||||
pattern: "^[a-z][a-z0-9_-]{0,63}$",
|
||||
},
|
||||
id: {
|
||||
type: "string",
|
||||
pattern: "^[A-Z][A-Z0-9_]{0,127}$",
|
||||
},
|
||||
},
|
||||
required: ["source", "provider", "id"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
source: {
|
||||
type: "string",
|
||||
const: "file",
|
||||
},
|
||||
provider: {
|
||||
type: "string",
|
||||
pattern: "^[a-z][a-z0-9_-]{0,63}$",
|
||||
},
|
||||
id: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
required: ["source", "provider", "id"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
source: {
|
||||
type: "string",
|
||||
const: "exec",
|
||||
},
|
||||
provider: {
|
||||
type: "string",
|
||||
pattern: "^[a-z][a-z0-9_-]{0,63}$",
|
||||
},
|
||||
id: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
required: ["source", "provider", "id"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
additionalProperties: {
|
||||
anyOf: [
|
||||
{
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
type: "number",
|
||||
},
|
||||
{
|
||||
type: "boolean",
|
||||
},
|
||||
{
|
||||
type: "null",
|
||||
},
|
||||
{
|
||||
type: "array",
|
||||
items: {},
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
propertyNames: {
|
||||
type: "string",
|
||||
},
|
||||
additionalProperties: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
summaryModel: {
|
||||
type: "string",
|
||||
},
|
||||
@@ -3792,6 +4142,78 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
|
||||
default: true,
|
||||
type: "boolean",
|
||||
},
|
||||
tts: {
|
||||
type: "object",
|
||||
properties: {
|
||||
auto: {
|
||||
type: "string",
|
||||
enum: ["off", "always", "inbound", "tagged"],
|
||||
},
|
||||
enabled: {
|
||||
type: "boolean",
|
||||
},
|
||||
mode: {
|
||||
type: "string",
|
||||
enum: ["final", "all"],
|
||||
},
|
||||
provider: {
|
||||
type: "string",
|
||||
},
|
||||
persona: {
|
||||
type: "string",
|
||||
},
|
||||
personas: {
|
||||
type: "object",
|
||||
propertyNames: {
|
||||
type: "string",
|
||||
},
|
||||
additionalProperties: {
|
||||
type: "object",
|
||||
propertyNames: {
|
||||
type: "string",
|
||||
},
|
||||
additionalProperties: {},
|
||||
},
|
||||
},
|
||||
summaryModel: {
|
||||
type: "string",
|
||||
},
|
||||
modelOverrides: {
|
||||
type: "object",
|
||||
propertyNames: {
|
||||
type: "string",
|
||||
},
|
||||
additionalProperties: {},
|
||||
},
|
||||
providers: {
|
||||
type: "object",
|
||||
propertyNames: {
|
||||
type: "string",
|
||||
},
|
||||
additionalProperties: {
|
||||
type: "object",
|
||||
propertyNames: {
|
||||
type: "string",
|
||||
},
|
||||
additionalProperties: {},
|
||||
},
|
||||
},
|
||||
prefsPath: {
|
||||
type: "string",
|
||||
},
|
||||
maxTextLength: {
|
||||
type: "integer",
|
||||
minimum: 1,
|
||||
maximum: 9007199254740991,
|
||||
},
|
||||
timeoutMs: {
|
||||
type: "integer",
|
||||
minimum: 1000,
|
||||
maximum: 120000,
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
groupSessionScope: {
|
||||
type: "string",
|
||||
enum: ["group", "group_sender", "group_topic", "group_topic_sender"],
|
||||
@@ -4345,6 +4767,78 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
|
||||
resolveSenderNames: {
|
||||
type: "boolean",
|
||||
},
|
||||
tts: {
|
||||
type: "object",
|
||||
properties: {
|
||||
auto: {
|
||||
type: "string",
|
||||
enum: ["off", "always", "inbound", "tagged"],
|
||||
},
|
||||
enabled: {
|
||||
type: "boolean",
|
||||
},
|
||||
mode: {
|
||||
type: "string",
|
||||
enum: ["final", "all"],
|
||||
},
|
||||
provider: {
|
||||
type: "string",
|
||||
},
|
||||
persona: {
|
||||
type: "string",
|
||||
},
|
||||
personas: {
|
||||
type: "object",
|
||||
propertyNames: {
|
||||
type: "string",
|
||||
},
|
||||
additionalProperties: {
|
||||
type: "object",
|
||||
propertyNames: {
|
||||
type: "string",
|
||||
},
|
||||
additionalProperties: {},
|
||||
},
|
||||
},
|
||||
summaryModel: {
|
||||
type: "string",
|
||||
},
|
||||
modelOverrides: {
|
||||
type: "object",
|
||||
propertyNames: {
|
||||
type: "string",
|
||||
},
|
||||
additionalProperties: {},
|
||||
},
|
||||
providers: {
|
||||
type: "object",
|
||||
propertyNames: {
|
||||
type: "string",
|
||||
},
|
||||
additionalProperties: {
|
||||
type: "object",
|
||||
propertyNames: {
|
||||
type: "string",
|
||||
},
|
||||
additionalProperties: {},
|
||||
},
|
||||
},
|
||||
prefsPath: {
|
||||
type: "string",
|
||||
},
|
||||
maxTextLength: {
|
||||
type: "integer",
|
||||
minimum: 1,
|
||||
maximum: 9007199254740991,
|
||||
},
|
||||
timeoutMs: {
|
||||
type: "integer",
|
||||
minimum: 1000,
|
||||
maximum: 120000,
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
groupSessionScope: {
|
||||
type: "string",
|
||||
enum: ["group", "group_sender", "group_topic", "group_topic_sender"],
|
||||
|
||||
@@ -28617,6 +28617,6 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
|
||||
tags: ["advanced", "url-secret"],
|
||||
},
|
||||
},
|
||||
version: "2026.4.26",
|
||||
version: "2026.4.25-beta.8",
|
||||
generatedAt: "2026-03-22T21:17:33.302Z",
|
||||
};
|
||||
|
||||
@@ -290,7 +290,7 @@ async function bindConversationAndWait(params: {
|
||||
if (runtime?.probeAvailability) {
|
||||
await runtime.probeAvailability().catch(() => {});
|
||||
}
|
||||
if (!(backend?.healthy?.() ?? false)) {
|
||||
if (!backend || (backend.healthy && !backend.healthy())) {
|
||||
if (runtime?.doctor && (attempt === 1 || attempt % 6 === 0)) {
|
||||
const report = await runtime.doctor().catch((error) => ({
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
|
||||
@@ -312,10 +312,6 @@ describe("getCachedPluginJitiLoader", () => {
|
||||
|
||||
const loose = loader as unknown as (t: string, ...a: unknown[]) => unknown;
|
||||
loose("/repo/dist/extensions/demo/api.js", { hint: "x" }, 42);
|
||||
expect(jitiLoader).toHaveBeenCalledWith(
|
||||
"/repo/dist/extensions/demo/api.js",
|
||||
{ hint: "x" },
|
||||
42,
|
||||
);
|
||||
expect(jitiLoader).toHaveBeenCalledWith("/repo/dist/extensions/demo/api.js", { hint: "x" }, 42);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -363,7 +363,7 @@ export async function waitForChatFinalEvent(params: {
|
||||
sessionKey: string;
|
||||
timeoutMs?: number;
|
||||
}): Promise<ChatEventPayload> {
|
||||
const deadline = Date.now() + (params.timeoutMs ?? 15_000);
|
||||
const deadline = Date.now() + (params.timeoutMs ?? 45_000);
|
||||
while (Date.now() < deadline) {
|
||||
const match = params.events.find(
|
||||
(evt) =>
|
||||
|
||||
Reference in New Issue
Block a user