fix(delivery): treat Matrix "User not in room" as permanent delivery error (#57426)

Merged via squash.

Prepared head SHA: 6a777197cb
Co-authored-by: dlardo <5000601+dlardo@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
This commit is contained in:
Douglas Lardo
2026-03-29 22:35:15 -07:00
committed by GitHub
parent 96ddf30cf1
commit bb2c010e07
4 changed files with 25 additions and 0 deletions

View File

@@ -93,6 +93,7 @@ Docs: https://docs.openclaw.ai
- Config/update: stop `openclaw doctor` write-backs from persisting plugin-injected channel defaults, so `openclaw update` no longer seeds config keys that later break service refresh validation. (#56834) Thanks @openperf.
- Agents/Anthropic failover: treat Anthropic `api_error` payloads with `An unexpected error occurred while processing the response` as transient so retry/fallback can engage instead of surfacing a terminal failure. (#57441) Thanks @zijiess and @vincentkoc.
- Agents/compaction: keep late compaction-retry rejections handled after the aggregate timeout path wins without swallowing real pre-timeout wait failures, so timed-out retries no longer surface an unhandled rejection on later unsubscribe. (#57451) Thanks @mpz4life and @vincentkoc.
- Matrix/delivery recovery: treat Synapse `User not in room` replay failures as permanent during startup recovery so poisoned queued messages move to `failed/` instead of crash-looping Matrix after restart. (#57426) thanks @dlardo.
## 2026.3.28

View File

@@ -50,6 +50,7 @@ const PERMANENT_ERROR_PATTERNS: readonly RegExp[] = [
/recipient is not a valid/i,
/outbound not configured for channel/i,
/ambiguous discord recipient/i,
/User .* not in room/i,
];
function createEmptyRecoverySummary(): RecoverySummary {

View File

@@ -16,6 +16,7 @@ describe("delivery-queue policy", () => {
"Forbidden: bot was kicked from the group chat",
"chat_id is empty",
"Outbound not configured for channel: demo-channel",
"MatrixError: [403] User @bot:matrix.example.com not in room !mixedCase:matrix.example.com",
])("returns true for permanent error: %s", (msg) => {
expect(isPermanentDeliveryError(msg)).toBe(true);
});

View File

@@ -116,6 +116,28 @@ describe("delivery-queue recovery", () => {
expect(log.warn).toHaveBeenCalledWith(expect.stringContaining("permanent error"));
});
it("treats Matrix 'User not in room' as a permanent error", async () => {
const id = await enqueueDelivery(
{ channel: "matrix", to: "!lowercased:matrix.example.com", payloads: [{ text: "hi" }] },
tmpDir(),
);
const deliver = vi
.fn()
.mockRejectedValue(
new Error(
"MatrixError: [403] User @bot:matrix.example.com not in room !lowercased:matrix.example.com",
),
);
const log = createRecoveryLog();
const { result } = await runRecovery({ deliver, log });
expect(result.failed).toBe(1);
expect(result.recovered).toBe(0);
expect(await loadPendingDeliveries(tmpDir())).toHaveLength(0);
expect(fs.existsSync(path.join(tmpDir(), "delivery-queue", "failed", `${id}.json`))).toBe(true);
expect(log.warn).toHaveBeenCalledWith(expect.stringContaining("permanent error"));
});
it("passes skipQueue: true to prevent re-enqueueing during recovery", async () => {
await enqueueDelivery(
{ channel: "demo-channel-a", to: "+1", payloads: [{ text: "a" }] },