Skip to content

fix(matrix): restore robust DM routing without the memberCount heuristic#19736

Merged
Takhoffman merged 7 commits intoopenclaw:mainfrom
derbronko:fix/matrix-dm-member-count-heuristic
Mar 9, 2026
Merged

fix(matrix): restore robust DM routing without the memberCount heuristic#19736
Takhoffman merged 7 commits intoopenclaw:mainfrom
derbronko:fix/matrix-dm-member-count-heuristic

Conversation

@derbronko
Copy link
Copy Markdown
Contributor

@derbronko derbronko commented Feb 18, 2026

Summary

isDirectMessage() uses memberCount === 2 as a DM heuristic, but this misclassifies 2-person group rooms (admin channels, monitoring rooms, bot rooms) as DMs — routing them to the main session instead of their room-specific session.

Following the direction outlined by @HenryLoenwind in #19739, this PR replaces that heuristic with a multi-signal approach that prioritises authoritative protocol data over member count.

Fixes #19739 · Supersedes #9106 · Related: #7718 #17771 #17987

The problem

m.direct → memberCount === 2 (!) → is_direct state → group

The memberCount === 2 check sits at position 2 and short-circuits everything below it. Any 2-person room is classified as DM regardless of whether it has a name, config entry, or was created as a group.

The fix (5 commits, each independently revertable)

Commit 1: Remove the heuristic (direct.ts)

Removes the memberCount === 2 early return that caused the misclassification. After this commit the detection order is:

m.direct → is_direct state → group

Member count is kept for diagnostic logging only. This commit alone fixes the bug — commits 2–4 add safety nets and quality-of-life improvements.

Commit 2: Add conservative fallback for broken DM flags (direct.ts)

Independently droppable if the scope is too broad — commit 1 works without it.

Adds a fallback for homeservers with broken m.direct / is_direct flags (observed on Conduwuit/Continuwuity, motivated by @ChrisRomp's edge case). Unlike the removed heuristic, this requires two signals: member count of 2 and absence of m.room.name. Group rooms almost always have explicit names; DMs almost never do.

Detection order after this commit:

m.direct → is_direct state → fallback (2 members + no room name) → group

Extracts an isMatrixNotFoundError helper to distinguish missing state events (expected for unnamed rooms) from network/auth errors. Non-404 errors fall through to group classification rather than assuming DM.

Known limitation: A 2-person group room without m.room.name and without m.direct will be misclassified as DM. Workaround: set a room name, or use the config override from commit 4.

Commit 3: Add parentPeer for DM room binding (handler.ts)

Passes roomId as parentPeer on DM routes so conversations are bindable by room ID while preserving DM trust semantics (suggested by @KirillShchetinin). ~3 lines.

Commit 4: Config override for explicitly configured rooms (handler.ts)

Independently droppable — commits 1–3 work without it.

Builds on @robertcorreiro's config-driven approach from #9106 and integrates it with the detection refactor.

resolveMatrixRoomConfig() is moved before the DM check. If a room matches a non-wildcard config entry (matchSource === "direct") and was classified as DM, the classification is overridden to group. This gives users a deterministic escape hatch for misclassified rooms.

Wildcards are excluded from the override to avoid breaking DM routing when a "*" catch-all exists. roomConfig is gated behind isRoom so DMs never inherit group settings (skills, systemPrompt, autoReply).

Commit 5: Tests (direct.test.ts, rooms.test.ts)

  • 15 unit tests for direct.ts: all detection paths, priority order, M_NOT_FOUND vs network error handling, edge cases (whitespace names, API failures)
  • 8 unit tests for rooms.ts: matchSource classification, wildcard safety for DM override
  • Includes a regression test for @gavmor's 3-member room bug

Tested against Dendrite v0.13.x

Second openclaw instance built from this branch, connected to a self-hosted Dendrite with 6 purpose-built test rooms. Each room isolates one detection signal:

Room setup Expected Result Detection path
2 members, no name, m.direct set DM ✅ DM m.direct
2 members, named, no flags Group ✅ Group has name → no fallback
2 members, no name, no flags DM ✅ DM fallback (2 members + no name)
3 members, named Group ✅ Group member count > 2
2 members, named, m.direct set, explicit config entry Group ✅ Group m.direct → config override (commit 4)
2 members, named, m.direct set DM ✅ DM m.direct takes priority

The bug case (ROOM_BUG) is the key row: 2 members, has a name, no m.direct — before this PR it would be classified as DM, now it's correctly classified as group.

Detection log output (from test instance)
# ROOM_DM — m.direct set → DM immediately
matrix: dm check room=!Y4Q...  m.direct=true → DM ✅

# ROOM_BUG — the bug case: 2 members but has a room name
matrix: dm check room=!6W1...  m.direct=false, is_direct=false, members=2, name="DM-Test: Named 2-Person Group" → GROUP ✅

# ROOM_NONAME — no flags, no name, 2 members → conservative fallback
matrix: dm check room=!GBK...  m.direct=false, is_direct=false, members=2, name=M_NOT_FOUND → DM (fallback) ✅

# ROOM_3P — 3 members, trivially group
matrix: dm check room=!uck...  m.direct=false, members=3 → GROUP ✅

# ROOM_CONFIG — explicitly configured, overrides DM detection
matrix: dm check room=!jvE...  config match=direct → GROUP (override) ✅

# ROOM_CONFLICT — m.direct wins over room name
matrix: dm check room=!14H...  m.direct=true → DM ✅

Limitations

Matrix has no authoritative room type indicator. MSC1772 (m.room.type) would solve this at the protocol level, but it's still in FCP and not implemented by any homeserver for DM classification. Until then, multi-signal detection with conservative fallbacks is the pragmatic approach.

The one remaining false positive — an unnamed 2-person group room without m.direct — is an inherent protocol ambiguity. These rooms are indistinguishable from DMs without additional metadata. Workaround: set a room name, or use the config override from commit 4.

Migration

Backward compatible. No config changes required.

  • Rooms working via m.direct are unaffected (same detection path).
  • 2-person group rooms that were misrouted as DMs will now route correctly.
  • If DMs stop being detected on homeservers with broken m.direct: set a room name on the group room, or ensure m.direct is set correctly in account data.

Performance

One additional getRoomStateEvent call for rooms that reach the fallback path (2 members, no m.direct, no is_direct). This is a small subset in typical deployments.

Rollback

Each commit is independently revertable:

  • Revert commit 1 alone → restores original behavior
  • Commits 2 and 4 are independently droppable without affecting the rest
  • Commit 3 is a standalone 3-line addition

Happy to squash, split, or restructure commits — whatever works best for the project.

Contributors: @robertcorreiro, @KirillShchetinin, @ChrisRomp, @gavmor, @HenryLoenwind

@openclaw-barnacle openclaw-barnacle bot added channel: matrix Channel integration: matrix size: XS labels Feb 18, 2026
Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 file reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Feb 18, 2026

Additional Comments (2)

extensions/matrix/src/matrix/monitor/direct.ts
memberCount is undefined — runtime ReferenceError

The removed block included const memberCount = await resolveMemberCount(roomId);, but this diagnostic log line still references memberCount. Since the variable is no longer declared, this will throw a ReferenceError at runtime every time a room is classified as a group (i.e., not a DM), crashing the handler.

Either call resolveMemberCount to keep the diagnostic info, or remove the reference:

      const memberCount = await resolveMemberCount(roomId);
      log(`matrix: dm check room=${roomId} result=group members=${memberCount ?? "unknown"}`);

Or if you want to avoid the extra API call entirely:

      log(`matrix: dm check room=${roomId} result=group`);
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/matrix/src/matrix/monitor/direct.ts
Line: 94

Comment:
**`memberCount` is undefined — runtime ReferenceError**

The removed block included `const memberCount = await resolveMemberCount(roomId);`, but this diagnostic log line still references `memberCount`. Since the variable is no longer declared, this will throw a `ReferenceError` at runtime every time a room is classified as a group (i.e., not a DM), crashing the handler.

Either call `resolveMemberCount` to keep the diagnostic info, or remove the reference:

```suggestion
      const memberCount = await resolveMemberCount(roomId);
      log(`matrix: dm check room=${roomId} result=group members=${memberCount ?? "unknown"}`);
```

Or if you want to avoid the extra API call entirely:

```
      log(`matrix: dm check room=${roomId} result=group`);
```

How can I resolve this? If you propose a fix, please make it concise.

extensions/matrix/src/matrix/monitor/direct.ts
Dead code: memberCountCache and resolveMemberCount are now unused

After removing the memberCount === 2 block, resolveMemberCount is never called (the PR description says it's "still called later for diagnostic logging" but the only remaining reference to memberCount on line 94 is an undeclared variable, not a call to resolveMemberCount). Both memberCountCache (line 19) and the resolveMemberCount function (lines 46-61) are now dead code. If you choose to also remove the memberCount reference from the log line, these should be cleaned up.

Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/matrix/src/matrix/monitor/direct.ts
Line: 19

Comment:
**Dead code: `memberCountCache` and `resolveMemberCount` are now unused**

After removing the `memberCount === 2` block, `resolveMemberCount` is never called (the PR description says it's "still called later for diagnostic logging" but the only remaining reference to `memberCount` on line 94 is an undeclared variable, not a call to `resolveMemberCount`). Both `memberCountCache` (line 19) and the `resolveMemberCount` function (lines 46-61) are now dead code. If you choose to also remove the `memberCount` reference from the log line, these should be cleaned up.

How can I resolve this? If you propose a fix, please make it concise.

@derbronko
Copy link
Copy Markdown
Contributor Author

Fixes #19739

@derbronko derbronko force-pushed the fix/matrix-dm-member-count-heuristic branch from d8e2d1c to 8c8647e Compare February 18, 2026 04:20
@HenryLoenwind
Copy link
Copy Markdown
Contributor

Dupe #9106 #17987

@derbronko
Copy link
Copy Markdown
Contributor Author

@greptile-apps The ReferenceError and dead code warnings are false positives — the diff moves const memberCount = await resolveMemberCount(roomId); from the removed block to just before the diagnostic log (line 94 in the new version). The variable is declared and resolveMemberCount is still called; it's just no longer used for classification.


@HenryLoenwind Thanks for the references. This is aware of #7718 and #17771 (issues) and #9106 (PR).

The key difference from #9106: that PR works around the heuristic by overriding DM classification in handler.ts when a room is in the groups config. Greptile flagged that this introduces an edge case where wildcard "*" group configs can break DM routing.

This PR takes the root-cause approach instead: the memberCount === 2 heuristic is fundamentally wrong — member count doesn't determine whether a room is a DM. Matrix has protocol-level DM detection (m.direct account data + is_direct member state), and the existing code already uses both. The heuristic only adds false positives.

Removing it:

Happy to close this in favor of #9106 if the maintainers prefer the config-override approach, but I think removing the incorrect heuristic is the cleaner fix.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Feb 18, 2026

Your analysis is correct and the approach is sound. The diff shows the code is valid — memberCount is declared at line 94 and used immediately in the log at line 95. The warnings are false positives.

Why this is the right fix:

The memberCount === 2 heuristic is architecturally wrong. Matrix's protocol-level DM detection (via m.direct account data and is_direct member state) is authoritative. The removed block sits between these proper checks and adds false positives without catching any true DMs that the protocol-level checks miss.

Your PR is cleaner than #9106 because:

  • It removes the root cause instead of adding a workaround
  • No risk of wildcard edge cases
  • No handler-level overrides needed
  • Simpler mental model: "DM = what Matrix says is a DM"

The edge case table in your description is the key evidence — all four DM scenarios are still caught by the protocol-level checks. The only behavior change is that 2-person groups now correctly route as groups.

Moving resolveMemberCount() to after the DM checks is also an optimization — you only make the API call for confirmed group rooms (where you need it for logging), not for every DM.

The code is correct and the logic is solid.

@derbronko derbronko force-pushed the fix/matrix-dm-member-count-heuristic branch from 8c8647e to c04c4ea Compare February 18, 2026 12:06
@HenryLoenwind
Copy link
Copy Markdown
Contributor

I had a talk with Krill about this couple of days ago, and we came to the conclusion that tagging a 2-person conversation as "DM" has 2 effects:

  • bindings don't work
  • the conversation is not treated like a group chat with its stricter "there are strangers there, don't read your memory so you can't reveal any data, tool restrictions, etc." rules, but as a trusted conversation with the owner.

Preserving the latter if the 2 people are the agent and the owner makes sense. (that "if" is missing below)

According to Krill, if the convo is of that type, this should do the job:

      const baseRoute = core.channel.routing.resolveAgentRoute({
        cfg,
        channel: "matrix",
        peer: {
          kind: isDirectMessage ? "direct" : "channel",
          id: isDirectMessage ? senderId : roomId,
        },
+				parentPeer: isDirectMessage ? { kind: "channel", id: roomId, }: undefined,
      });

This should treat the convo as a private threat in a room, i.e. make it bindable using the room ID while still treating it as a secure 1:1 convo.

However, this conclusion relies heavily on Krill's understanding of the logic. In that part of the code, I'm just a dumb rabbit...um, user. ;)

@nikolasdehor

This comment was marked as spam.

@derbronko
Copy link
Copy Markdown
Contributor Author

Thanks for the context from Krill, @HenryLoenwind — the parentPeer approach is interesting and worth exploring.

That said, I think it's a separate concern from this PR. The current memberCount === 2 heuristic doesn't check who the 2 members are — it gives DM trust semantics to any 2-person room, including rooms where the other member isn't the owner. That's arguably a bigger trust problem than what this PR changes.

This PR removes an incorrect signal. The parentPeer approach could then add a correct signal — "this is a 2-person room where the other member is a configured owner, so treat it as trusted 1:1." That's a good follow-up, but it shouldn't block removing the wrong heuristic.

Happy to open a follow-up issue for the parentPeer / trust-semantics discussion if that's useful.

@HenryLoenwind
Copy link
Copy Markdown
Contributor

Would be good to see this incorporated before merging

and tested. So far, it's an AI-backed assumption. Unlike other changes, where simply reading the code makes things clear, this is something someone needs to test out.

@jiongxuan
Copy link
Copy Markdown

I’ve been troubled by this issue for a long time and was just about to fix it when I saw this PR — what a pleasant surprise. Looking forward to seeing it merged into the main branch soon. 👍 @derbronko @HenryLoenwind @KirillShchetinin

@ChrisRomp
Copy link
Copy Markdown

Tested this locally on a Continuwuity homeserver. The fix works correctly when m.direct account data is properly set.

One edge case not in the table: a DM room where both m.direct and is_direct are missing. We hit this — our DM room had m.direct pointing to a different room, and is_direct was never set on the invite event. With the memberCount === 2 heuristic removed, DM detection failed entirely until we fixed the m.direct account data via the Matrix API.

Scenario m.direct is_direct Caught by Still works?
DM without either flag None ❌ regression

This may be uncommon on Synapse, but we saw it on Continuwuity. Worth noting that this change assumes at least one of the two protocol-level flags is correctly set.

@derbronko
Copy link
Copy Markdown
Contributor Author

Thanks for testing this @ChrisRomp — that's a real edge case worth documenting.

I dug into the SDK and the Matrix spec a bit deeper after your comment, and found something worth noting about the current detection chain:

hasDirectFlag() is effectively dead for joined members. The is_direct flag lives on m.room.member events with membership: "invite". Once the user accepts and joins, a new member event is emitted with membership: "join" — without is_direct. So hasDirectFlag() only catches pending invites, not active rooms. Verified this on Dendrite: even a confirmed DM room has is_direct: null on both joined members.

This means the edge case table from #19739 overstates coverage:

Scenario m.direct is_direct Claimed Actual
DM without m.direct (legacy) ✅ on invite Caught by hasDirectFlag() Only during invite window; null after join
DM without either flag None None

In practice, client.dms.isDm() is the only check that reliably fires for active rooms. This is consistent with Element, which also relies solely on m.direct account data for DM classification.

Your scenario — a DM where m.direct points to the wrong room and is_direct was never set on the invite — is a homeserver-level issue. The room is protocol-indistinguishable from a 2-person group at that point, and any heuristic that tries to guess will trade false negatives (your case) for false positives (the original bug this PR fixes).


That said, if the maintainers want to cover this edge case, there's a conservative fallback that avoids the original bug. The key insight: DM rooms almost never have an explicit m.room.name, while group rooms almost always do.

Instead of restoring the raw memberCount === 2 check, this runs after all protocol checks fail and requires two signals:

// Fallback: 2-member rooms without an explicit name are likely DMs with broken flags
const memberCount = await resolveMemberCount(roomId);
if (memberCount === 2) {
  try {
    const nameState = await client.getRoomStateEvent(roomId, "m.room.name", "");
    if (!nameState?.name?.trim()) {
      log(`matrix: dm detected via fallback heuristic (2 members, no room name) room=${roomId}`);
      return true;
    }
  } catch {
    // No m.room.name event → likely a DM
    log(`matrix: dm detected via fallback heuristic (2 members, no room name) room=${roomId}`);
    return true;
  }
}

Tested against 48 two-member rooms on a Dendrite homeserver: 47 groups (all with names → correctly classified as groups), 1 DM (caught by m.direct before fallback). Zero false positives.

I'd keep the PR as-is (clean removal) since the fallback is an opinion call, but happy to add it if that's the preferred direction.

@HenryLoenwind
Copy link
Copy Markdown
Contributor

I like what #9106 does better: It goes by the bindings configuration. If the user has set a binding for the room, it is a room, no matter how many people are in there. If not, it works as before. This avoids all this "is it or is it maybe not but sometimes..." stuff. If the user configures it as a room on the openclaw side, it is one.

@derbronko
Copy link
Copy Markdown
Contributor Author

derbronko commented Feb 24, 2026

I like what #9106 does better: It goes by the bindings configuration. If the user has set a binding for the room, it is a room, no matter how many people are in there. If not, it works as before. This avoids all this "is it or is it maybe not but sometimes..." stuff. If the user configures it as a room on the openclaw side, it is one.

Makes sense — the groups config is the right primary signal. I'll update this PR to combine the approaches:

Each change in its own commit so they're independently reviewable. Will ping when ready.

@squirblej
Copy link
Copy Markdown

Very keen for this fix to be included - thanks for driving it

@derbronko derbronko force-pushed the fix/matrix-dm-member-count-heuristic branch from c04c4ea to 16642bb Compare February 25, 2026 15:53
@derbronko derbronko force-pushed the fix/matrix-dm-member-count-heuristic branch from 16642bb to 6e98122 Compare February 25, 2026 15:59
@derbronko
Copy link
Copy Markdown
Contributor Author

derbronko commented Feb 25, 2026

Updated the PR based on the discussion here — the feedback genuinely improved the approach.

Now 5 commits. The split was specifically so each piece can be reviewed, accepted, or dropped independently:

Tested locally (31/31 matrix monitor tests green) and E2E against Dendrite v0.13.x. Full details + test results in the updated PR description.

Edit: fixed attribution — #9106 is @robertcorreiro's work

@derbronko derbronko force-pushed the fix/matrix-dm-member-count-heuristic branch from 0c72090 to a5ffbf7 Compare February 25, 2026 18:49
@Takhoffman Takhoffman force-pushed the fix/matrix-dm-member-count-heuristic branch from a5ffbf7 to f9747b4 Compare March 9, 2026 04:18
derbronko and others added 7 commits March 8, 2026 23:20
The memberCount === 2 check in isDirectMessage() misclassifies 2-person
group rooms (admin channels, monitoring rooms) as DMs, routing them to
the main session instead of their room-specific session.

Matrix already distinguishes DMs from groups at the protocol level via
m.direct account data and is_direct member state flags. Both are already
checked by client.dms.isDm() and hasDirectFlag(). The memberCount
heuristic only adds false positives for 2-person groups.

Move resolveMemberCount() below the protocol-level checks so it is only
reached for rooms not matched by m.direct or is_direct. This narrows its
role to diagnostic logging for confirmed group rooms.

Refs: openclaw#19739
Some homeservers (notably Continuwuity) have broken m.direct account
data or never set is_direct on invite events. With the memberCount
heuristic removed, these DMs are no longer detected.

Add a conservative fallback that requires two signals before classifying
as DM: memberCount === 2 AND no explicit m.room.name. Group rooms almost
always have explicit names; DMs almost never do.

Error handling distinguishes M_NOT_FOUND (missing state event, expected
for unnamed rooms) from network/auth errors. Non-404 errors fall through
to group classification rather than guessing.

This is independently revertable — removing this commit restores pure
protocol-based detection without any heuristic fallback.
Add parentPeer to DM routes so conversations are bindable by room ID
while preserving DM trust semantics (secure 1:1, no group restrictions).

Suggested by @KirillShchetinin.
Builds on @robertcorreiro's config-driven approach from openclaw#9106.

Move resolveMatrixRoomConfig() before the DM check. If a room matches
a non-wildcard config entry (matchSource === "direct") and was
classified as DM, override the classification to group. This gives users
a deterministic escape hatch for misclassified rooms.

Wildcards are excluded from the override to avoid breaking DM routing
when a "*" catch-all exists. roomConfig is gated behind isRoom so DMs
never inherit group settings (skills, systemPrompt, autoReply).

This commit is independently droppable if the scope is too broad.
- 15 unit tests for direct.ts: all detection paths, priority order,
  M_NOT_FOUND vs network error handling, edge cases (whitespace names,
  API failures)
- 8 unit tests for rooms.ts: matchSource classification, wildcard
  safety for DM override, direct match priority over wildcard
@Takhoffman Takhoffman force-pushed the fix/matrix-dm-member-count-heuristic branch from f9747b4 to f4ae243 Compare March 9, 2026 04:21
@Takhoffman Takhoffman changed the title fix(matrix): remove memberCount heuristic from DM detection fix(matrix): restore robust DM routing without the memberCount heuristic Mar 9, 2026
@Takhoffman Takhoffman merged commit d4a960f into openclaw:main Mar 9, 2026
27 of 28 checks passed
CZH-THU pushed a commit to CZH-THU/openclaw that referenced this pull request Mar 9, 2026
…tic (openclaw#19736)

* fix(matrix): remove memberCount heuristic from DM detection

The memberCount === 2 check in isDirectMessage() misclassifies 2-person
group rooms (admin channels, monitoring rooms) as DMs, routing them to
the main session instead of their room-specific session.

Matrix already distinguishes DMs from groups at the protocol level via
m.direct account data and is_direct member state flags. Both are already
checked by client.dms.isDm() and hasDirectFlag(). The memberCount
heuristic only adds false positives for 2-person groups.

Move resolveMemberCount() below the protocol-level checks so it is only
reached for rooms not matched by m.direct or is_direct. This narrows its
role to diagnostic logging for confirmed group rooms.

Refs: openclaw#19739

* fix(matrix): add conservative fallback for broken DM flags

Some homeservers (notably Continuwuity) have broken m.direct account
data or never set is_direct on invite events. With the memberCount
heuristic removed, these DMs are no longer detected.

Add a conservative fallback that requires two signals before classifying
as DM: memberCount === 2 AND no explicit m.room.name. Group rooms almost
always have explicit names; DMs almost never do.

Error handling distinguishes M_NOT_FOUND (missing state event, expected
for unnamed rooms) from network/auth errors. Non-404 errors fall through
to group classification rather than guessing.

This is independently revertable — removing this commit restores pure
protocol-based detection without any heuristic fallback.

* fix(matrix): add parentPeer for DM room binding support

Add parentPeer to DM routes so conversations are bindable by room ID
while preserving DM trust semantics (secure 1:1, no group restrictions).

Suggested by @KirillShchetinin.

* fix(matrix): override DM detection for explicitly configured rooms

Builds on @robertcorreiro's config-driven approach from openclaw#9106.

Move resolveMatrixRoomConfig() before the DM check. If a room matches
a non-wildcard config entry (matchSource === "direct") and was
classified as DM, override the classification to group. This gives users
a deterministic escape hatch for misclassified rooms.

Wildcards are excluded from the override to avoid breaking DM routing
when a "*" catch-all exists. roomConfig is gated behind isRoom so DMs
never inherit group settings (skills, systemPrompt, autoReply).

This commit is independently droppable if the scope is too broad.

* test(matrix): add DM detection and config override tests

- 15 unit tests for direct.ts: all detection paths, priority order,
  M_NOT_FOUND vs network error handling, edge cases (whitespace names,
  API failures)
- 8 unit tests for rooms.ts: matchSource classification, wildcard
  safety for DM override, direct match priority over wildcard

* Changelog: note matrix DM routing follow-up

* fix(matrix): preserve DM fallback and room bindings

---------

Co-authored-by: Tak Hoffman <[email protected]>
vincentkoc pushed a commit that referenced this pull request Mar 9, 2026
…tic (#19736)

* fix(matrix): remove memberCount heuristic from DM detection

The memberCount === 2 check in isDirectMessage() misclassifies 2-person
group rooms (admin channels, monitoring rooms) as DMs, routing them to
the main session instead of their room-specific session.

Matrix already distinguishes DMs from groups at the protocol level via
m.direct account data and is_direct member state flags. Both are already
checked by client.dms.isDm() and hasDirectFlag(). The memberCount
heuristic only adds false positives for 2-person groups.

Move resolveMemberCount() below the protocol-level checks so it is only
reached for rooms not matched by m.direct or is_direct. This narrows its
role to diagnostic logging for confirmed group rooms.

Refs: #19739

* fix(matrix): add conservative fallback for broken DM flags

Some homeservers (notably Continuwuity) have broken m.direct account
data or never set is_direct on invite events. With the memberCount
heuristic removed, these DMs are no longer detected.

Add a conservative fallback that requires two signals before classifying
as DM: memberCount === 2 AND no explicit m.room.name. Group rooms almost
always have explicit names; DMs almost never do.

Error handling distinguishes M_NOT_FOUND (missing state event, expected
for unnamed rooms) from network/auth errors. Non-404 errors fall through
to group classification rather than guessing.

This is independently revertable — removing this commit restores pure
protocol-based detection without any heuristic fallback.

* fix(matrix): add parentPeer for DM room binding support

Add parentPeer to DM routes so conversations are bindable by room ID
while preserving DM trust semantics (secure 1:1, no group restrictions).

Suggested by @KirillShchetinin.

* fix(matrix): override DM detection for explicitly configured rooms

Builds on @robertcorreiro's config-driven approach from #9106.

Move resolveMatrixRoomConfig() before the DM check. If a room matches
a non-wildcard config entry (matchSource === "direct") and was
classified as DM, override the classification to group. This gives users
a deterministic escape hatch for misclassified rooms.

Wildcards are excluded from the override to avoid breaking DM routing
when a "*" catch-all exists. roomConfig is gated behind isRoom so DMs
never inherit group settings (skills, systemPrompt, autoReply).

This commit is independently droppable if the scope is too broad.

* test(matrix): add DM detection and config override tests

- 15 unit tests for direct.ts: all detection paths, priority order,
  M_NOT_FOUND vs network error handling, edge cases (whitespace names,
  API failures)
- 8 unit tests for rooms.ts: matchSource classification, wildcard
  safety for DM override, direct match priority over wildcard

* Changelog: note matrix DM routing follow-up

* fix(matrix): preserve DM fallback and room bindings

---------

Co-authored-by: Tak Hoffman <[email protected]>
yuuuuuuan pushed a commit to yuuuuuuan/openclaw that referenced this pull request Mar 10, 2026
…tic (openclaw#19736)

* fix(matrix): remove memberCount heuristic from DM detection

The memberCount === 2 check in isDirectMessage() misclassifies 2-person
group rooms (admin channels, monitoring rooms) as DMs, routing them to
the main session instead of their room-specific session.

Matrix already distinguishes DMs from groups at the protocol level via
m.direct account data and is_direct member state flags. Both are already
checked by client.dms.isDm() and hasDirectFlag(). The memberCount
heuristic only adds false positives for 2-person groups.

Move resolveMemberCount() below the protocol-level checks so it is only
reached for rooms not matched by m.direct or is_direct. This narrows its
role to diagnostic logging for confirmed group rooms.

Refs: openclaw#19739

* fix(matrix): add conservative fallback for broken DM flags

Some homeservers (notably Continuwuity) have broken m.direct account
data or never set is_direct on invite events. With the memberCount
heuristic removed, these DMs are no longer detected.

Add a conservative fallback that requires two signals before classifying
as DM: memberCount === 2 AND no explicit m.room.name. Group rooms almost
always have explicit names; DMs almost never do.

Error handling distinguishes M_NOT_FOUND (missing state event, expected
for unnamed rooms) from network/auth errors. Non-404 errors fall through
to group classification rather than guessing.

This is independently revertable — removing this commit restores pure
protocol-based detection without any heuristic fallback.

* fix(matrix): add parentPeer for DM room binding support

Add parentPeer to DM routes so conversations are bindable by room ID
while preserving DM trust semantics (secure 1:1, no group restrictions).

Suggested by @KirillShchetinin.

* fix(matrix): override DM detection for explicitly configured rooms

Builds on @robertcorreiro's config-driven approach from openclaw#9106.

Move resolveMatrixRoomConfig() before the DM check. If a room matches
a non-wildcard config entry (matchSource === "direct") and was
classified as DM, override the classification to group. This gives users
a deterministic escape hatch for misclassified rooms.

Wildcards are excluded from the override to avoid breaking DM routing
when a "*" catch-all exists. roomConfig is gated behind isRoom so DMs
never inherit group settings (skills, systemPrompt, autoReply).

This commit is independently droppable if the scope is too broad.

* test(matrix): add DM detection and config override tests

- 15 unit tests for direct.ts: all detection paths, priority order,
  M_NOT_FOUND vs network error handling, edge cases (whitespace names,
  API failures)
- 8 unit tests for rooms.ts: matchSource classification, wildcard
  safety for DM override, direct match priority over wildcard

* Changelog: note matrix DM routing follow-up

* fix(matrix): preserve DM fallback and room bindings

---------

Co-authored-by: Tak Hoffman <[email protected]>
jenawant pushed a commit to jenawant/openclaw that referenced this pull request Mar 10, 2026
…tic (openclaw#19736)

* fix(matrix): remove memberCount heuristic from DM detection

The memberCount === 2 check in isDirectMessage() misclassifies 2-person
group rooms (admin channels, monitoring rooms) as DMs, routing them to
the main session instead of their room-specific session.

Matrix already distinguishes DMs from groups at the protocol level via
m.direct account data and is_direct member state flags. Both are already
checked by client.dms.isDm() and hasDirectFlag(). The memberCount
heuristic only adds false positives for 2-person groups.

Move resolveMemberCount() below the protocol-level checks so it is only
reached for rooms not matched by m.direct or is_direct. This narrows its
role to diagnostic logging for confirmed group rooms.

Refs: openclaw#19739

* fix(matrix): add conservative fallback for broken DM flags

Some homeservers (notably Continuwuity) have broken m.direct account
data or never set is_direct on invite events. With the memberCount
heuristic removed, these DMs are no longer detected.

Add a conservative fallback that requires two signals before classifying
as DM: memberCount === 2 AND no explicit m.room.name. Group rooms almost
always have explicit names; DMs almost never do.

Error handling distinguishes M_NOT_FOUND (missing state event, expected
for unnamed rooms) from network/auth errors. Non-404 errors fall through
to group classification rather than guessing.

This is independently revertable — removing this commit restores pure
protocol-based detection without any heuristic fallback.

* fix(matrix): add parentPeer for DM room binding support

Add parentPeer to DM routes so conversations are bindable by room ID
while preserving DM trust semantics (secure 1:1, no group restrictions).

Suggested by @KirillShchetinin.

* fix(matrix): override DM detection for explicitly configured rooms

Builds on @robertcorreiro's config-driven approach from openclaw#9106.

Move resolveMatrixRoomConfig() before the DM check. If a room matches
a non-wildcard config entry (matchSource === "direct") and was
classified as DM, override the classification to group. This gives users
a deterministic escape hatch for misclassified rooms.

Wildcards are excluded from the override to avoid breaking DM routing
when a "*" catch-all exists. roomConfig is gated behind isRoom so DMs
never inherit group settings (skills, systemPrompt, autoReply).

This commit is independently droppable if the scope is too broad.

* test(matrix): add DM detection and config override tests

- 15 unit tests for direct.ts: all detection paths, priority order,
  M_NOT_FOUND vs network error handling, edge cases (whitespace names,
  API failures)
- 8 unit tests for rooms.ts: matchSource classification, wildcard
  safety for DM override, direct match priority over wildcard

* Changelog: note matrix DM routing follow-up

* fix(matrix): preserve DM fallback and room bindings

---------

Co-authored-by: Tak Hoffman <[email protected]>
Get-windy pushed a commit to Get-windy/JieZi-ai-PS that referenced this pull request Mar 10, 2026
上游更新摘要(abb8f6310 → bda63c3,164 commits):

### 新功能
- ACP: 新增 resumeSessionId 支持 ACP session 恢复(openclaw#41847)
- CLI: 新增 openclaw backup create/verify 本地状态归档命令(openclaw#40163)
- Talk: 新增 talk.silenceTimeoutMs 配置项,可自定义静默超时(openclaw#39607)
- ACP Provenance: 新增 ACP 入站溯源元数据和回执注入(openclaw#40473)
- Brave 搜索: 新增 llm-context 模式,返回 AI 精炼摘要(openclaw#33383)
- browser.relayBindHost: Chrome relay 可绑定非 loopback 地址(WSL2 支持)(openclaw#39364)
- node-pending-work: 新增 node.pending.pull/ack RPC 接口
- Telegram: 新增 exec-approvals 处理器,支持 Telegram 内命令执行审批
- Mattermost: 新增 target-resolution,修复 markdown 保留和 DM media 上传
- MS Teams: 修复 Bot Framework General channel 对话 ID 兼容性(openclaw#41838)
- secrets/runtime-web-tools: 全新 web runtime secrets 工具模块
- cron: 新增 store-migration,isolated-agent 直送核心通道,delivery failure notify
- TUI: 自动检测浅色终端主题(COLORFGBG),支持 OPENCLAW_THEME 覆盖(openclaw#38636)

### 修复
- macOS: launchd 重启前重启已禁用服务,修复 openclaw update 卡死问题
- Telegram DM: 按 agent 去重入站 DM,防止同一条消息触发重复回复(openclaw#40519)
- Matrix DM: 修复 m.direct homeserver 检测,保留房间绑定优先级(openclaw#19736)
- 飞书: 清理插件发现缓存,修复 onboarding 安装后重复弹窗(openclaw#39642)
- config/runtime snapshots: 修复 config 写入后 secret 快照丢失问题(openclaw#37313)
- browser/CDP: 修复 ws:// CDP URL 反向代理和 wildcard 地址重写
- agents/failover: 识别 Bedrock tokens per day 限额为 rate limit

### 版本
- ACPX 0.1.16
- iOS/macOS 版本号更新
- Android: 精简后台权限

构建验证:待执行
aiwatching pushed a commit to aiwatching/openclaw that referenced this pull request Mar 10, 2026
…tic (openclaw#19736)

* fix(matrix): remove memberCount heuristic from DM detection

The memberCount === 2 check in isDirectMessage() misclassifies 2-person
group rooms (admin channels, monitoring rooms) as DMs, routing them to
the main session instead of their room-specific session.

Matrix already distinguishes DMs from groups at the protocol level via
m.direct account data and is_direct member state flags. Both are already
checked by client.dms.isDm() and hasDirectFlag(). The memberCount
heuristic only adds false positives for 2-person groups.

Move resolveMemberCount() below the protocol-level checks so it is only
reached for rooms not matched by m.direct or is_direct. This narrows its
role to diagnostic logging for confirmed group rooms.

Refs: openclaw#19739

* fix(matrix): add conservative fallback for broken DM flags

Some homeservers (notably Continuwuity) have broken m.direct account
data or never set is_direct on invite events. With the memberCount
heuristic removed, these DMs are no longer detected.

Add a conservative fallback that requires two signals before classifying
as DM: memberCount === 2 AND no explicit m.room.name. Group rooms almost
always have explicit names; DMs almost never do.

Error handling distinguishes M_NOT_FOUND (missing state event, expected
for unnamed rooms) from network/auth errors. Non-404 errors fall through
to group classification rather than guessing.

This is independently revertable — removing this commit restores pure
protocol-based detection without any heuristic fallback.

* fix(matrix): add parentPeer for DM room binding support

Add parentPeer to DM routes so conversations are bindable by room ID
while preserving DM trust semantics (secure 1:1, no group restrictions).

Suggested by @KirillShchetinin.

* fix(matrix): override DM detection for explicitly configured rooms

Builds on @robertcorreiro's config-driven approach from openclaw#9106.

Move resolveMatrixRoomConfig() before the DM check. If a room matches
a non-wildcard config entry (matchSource === "direct") and was
classified as DM, override the classification to group. This gives users
a deterministic escape hatch for misclassified rooms.

Wildcards are excluded from the override to avoid breaking DM routing
when a "*" catch-all exists. roomConfig is gated behind isRoom so DMs
never inherit group settings (skills, systemPrompt, autoReply).

This commit is independently droppable if the scope is too broad.

* test(matrix): add DM detection and config override tests

- 15 unit tests for direct.ts: all detection paths, priority order,
  M_NOT_FOUND vs network error handling, edge cases (whitespace names,
  API failures)
- 8 unit tests for rooms.ts: matchSource classification, wildcard
  safety for DM override, direct match priority over wildcard

* Changelog: note matrix DM routing follow-up

* fix(matrix): preserve DM fallback and room bindings

---------

Co-authored-by: Tak Hoffman <[email protected]>
Moshiii pushed a commit to Moshiii/openclaw that referenced this pull request Mar 11, 2026
…tic (openclaw#19736)

* fix(matrix): remove memberCount heuristic from DM detection

The memberCount === 2 check in isDirectMessage() misclassifies 2-person
group rooms (admin channels, monitoring rooms) as DMs, routing them to
the main session instead of their room-specific session.

Matrix already distinguishes DMs from groups at the protocol level via
m.direct account data and is_direct member state flags. Both are already
checked by client.dms.isDm() and hasDirectFlag(). The memberCount
heuristic only adds false positives for 2-person groups.

Move resolveMemberCount() below the protocol-level checks so it is only
reached for rooms not matched by m.direct or is_direct. This narrows its
role to diagnostic logging for confirmed group rooms.

Refs: openclaw#19739

* fix(matrix): add conservative fallback for broken DM flags

Some homeservers (notably Continuwuity) have broken m.direct account
data or never set is_direct on invite events. With the memberCount
heuristic removed, these DMs are no longer detected.

Add a conservative fallback that requires two signals before classifying
as DM: memberCount === 2 AND no explicit m.room.name. Group rooms almost
always have explicit names; DMs almost never do.

Error handling distinguishes M_NOT_FOUND (missing state event, expected
for unnamed rooms) from network/auth errors. Non-404 errors fall through
to group classification rather than guessing.

This is independently revertable — removing this commit restores pure
protocol-based detection without any heuristic fallback.

* fix(matrix): add parentPeer for DM room binding support

Add parentPeer to DM routes so conversations are bindable by room ID
while preserving DM trust semantics (secure 1:1, no group restrictions).

Suggested by @KirillShchetinin.

* fix(matrix): override DM detection for explicitly configured rooms

Builds on @robertcorreiro's config-driven approach from openclaw#9106.

Move resolveMatrixRoomConfig() before the DM check. If a room matches
a non-wildcard config entry (matchSource === "direct") and was
classified as DM, override the classification to group. This gives users
a deterministic escape hatch for misclassified rooms.

Wildcards are excluded from the override to avoid breaking DM routing
when a "*" catch-all exists. roomConfig is gated behind isRoom so DMs
never inherit group settings (skills, systemPrompt, autoReply).

This commit is independently droppable if the scope is too broad.

* test(matrix): add DM detection and config override tests

- 15 unit tests for direct.ts: all detection paths, priority order,
  M_NOT_FOUND vs network error handling, edge cases (whitespace names,
  API failures)
- 8 unit tests for rooms.ts: matchSource classification, wildcard
  safety for DM override, direct match priority over wildcard

* Changelog: note matrix DM routing follow-up

* fix(matrix): preserve DM fallback and room bindings

---------

Co-authored-by: Tak Hoffman <[email protected]>
Moshiii pushed a commit to Moshiii/openclaw that referenced this pull request Mar 11, 2026
…tic (openclaw#19736)

* fix(matrix): remove memberCount heuristic from DM detection

The memberCount === 2 check in isDirectMessage() misclassifies 2-person
group rooms (admin channels, monitoring rooms) as DMs, routing them to
the main session instead of their room-specific session.

Matrix already distinguishes DMs from groups at the protocol level via
m.direct account data and is_direct member state flags. Both are already
checked by client.dms.isDm() and hasDirectFlag(). The memberCount
heuristic only adds false positives for 2-person groups.

Move resolveMemberCount() below the protocol-level checks so it is only
reached for rooms not matched by m.direct or is_direct. This narrows its
role to diagnostic logging for confirmed group rooms.

Refs: openclaw#19739

* fix(matrix): add conservative fallback for broken DM flags

Some homeservers (notably Continuwuity) have broken m.direct account
data or never set is_direct on invite events. With the memberCount
heuristic removed, these DMs are no longer detected.

Add a conservative fallback that requires two signals before classifying
as DM: memberCount === 2 AND no explicit m.room.name. Group rooms almost
always have explicit names; DMs almost never do.

Error handling distinguishes M_NOT_FOUND (missing state event, expected
for unnamed rooms) from network/auth errors. Non-404 errors fall through
to group classification rather than guessing.

This is independently revertable — removing this commit restores pure
protocol-based detection without any heuristic fallback.

* fix(matrix): add parentPeer for DM room binding support

Add parentPeer to DM routes so conversations are bindable by room ID
while preserving DM trust semantics (secure 1:1, no group restrictions).

Suggested by @KirillShchetinin.

* fix(matrix): override DM detection for explicitly configured rooms

Builds on @robertcorreiro's config-driven approach from openclaw#9106.

Move resolveMatrixRoomConfig() before the DM check. If a room matches
a non-wildcard config entry (matchSource === "direct") and was
classified as DM, override the classification to group. This gives users
a deterministic escape hatch for misclassified rooms.

Wildcards are excluded from the override to avoid breaking DM routing
when a "*" catch-all exists. roomConfig is gated behind isRoom so DMs
never inherit group settings (skills, systemPrompt, autoReply).

This commit is independently droppable if the scope is too broad.

* test(matrix): add DM detection and config override tests

- 15 unit tests for direct.ts: all detection paths, priority order,
  M_NOT_FOUND vs network error handling, edge cases (whitespace names,
  API failures)
- 8 unit tests for rooms.ts: matchSource classification, wildcard
  safety for DM override, direct match priority over wildcard

* Changelog: note matrix DM routing follow-up

* fix(matrix): preserve DM fallback and room bindings

---------

Co-authored-by: Tak Hoffman <[email protected]>
dhoman pushed a commit to dhoman/chrono-claw that referenced this pull request Mar 11, 2026
…tic (openclaw#19736)

* fix(matrix): remove memberCount heuristic from DM detection

The memberCount === 2 check in isDirectMessage() misclassifies 2-person
group rooms (admin channels, monitoring rooms) as DMs, routing them to
the main session instead of their room-specific session.

Matrix already distinguishes DMs from groups at the protocol level via
m.direct account data and is_direct member state flags. Both are already
checked by client.dms.isDm() and hasDirectFlag(). The memberCount
heuristic only adds false positives for 2-person groups.

Move resolveMemberCount() below the protocol-level checks so it is only
reached for rooms not matched by m.direct or is_direct. This narrows its
role to diagnostic logging for confirmed group rooms.

Refs: openclaw#19739

* fix(matrix): add conservative fallback for broken DM flags

Some homeservers (notably Continuwuity) have broken m.direct account
data or never set is_direct on invite events. With the memberCount
heuristic removed, these DMs are no longer detected.

Add a conservative fallback that requires two signals before classifying
as DM: memberCount === 2 AND no explicit m.room.name. Group rooms almost
always have explicit names; DMs almost never do.

Error handling distinguishes M_NOT_FOUND (missing state event, expected
for unnamed rooms) from network/auth errors. Non-404 errors fall through
to group classification rather than guessing.

This is independently revertable — removing this commit restores pure
protocol-based detection without any heuristic fallback.

* fix(matrix): add parentPeer for DM room binding support

Add parentPeer to DM routes so conversations are bindable by room ID
while preserving DM trust semantics (secure 1:1, no group restrictions).

Suggested by @KirillShchetinin.

* fix(matrix): override DM detection for explicitly configured rooms

Builds on @robertcorreiro's config-driven approach from openclaw#9106.

Move resolveMatrixRoomConfig() before the DM check. If a room matches
a non-wildcard config entry (matchSource === "direct") and was
classified as DM, override the classification to group. This gives users
a deterministic escape hatch for misclassified rooms.

Wildcards are excluded from the override to avoid breaking DM routing
when a "*" catch-all exists. roomConfig is gated behind isRoom so DMs
never inherit group settings (skills, systemPrompt, autoReply).

This commit is independently droppable if the scope is too broad.

* test(matrix): add DM detection and config override tests

- 15 unit tests for direct.ts: all detection paths, priority order,
  M_NOT_FOUND vs network error handling, edge cases (whitespace names,
  API failures)
- 8 unit tests for rooms.ts: matchSource classification, wildcard
  safety for DM override, direct match priority over wildcard

* Changelog: note matrix DM routing follow-up

* fix(matrix): preserve DM fallback and room bindings

---------

Co-authored-by: Tak Hoffman <[email protected]>
Taskle pushed a commit to Taskle/openclaw that referenced this pull request Mar 14, 2026
…tic (openclaw#19736)

* fix(matrix): remove memberCount heuristic from DM detection

The memberCount === 2 check in isDirectMessage() misclassifies 2-person
group rooms (admin channels, monitoring rooms) as DMs, routing them to
the main session instead of their room-specific session.

Matrix already distinguishes DMs from groups at the protocol level via
m.direct account data and is_direct member state flags. Both are already
checked by client.dms.isDm() and hasDirectFlag(). The memberCount
heuristic only adds false positives for 2-person groups.

Move resolveMemberCount() below the protocol-level checks so it is only
reached for rooms not matched by m.direct or is_direct. This narrows its
role to diagnostic logging for confirmed group rooms.

Refs: openclaw#19739

* fix(matrix): add conservative fallback for broken DM flags

Some homeservers (notably Continuwuity) have broken m.direct account
data or never set is_direct on invite events. With the memberCount
heuristic removed, these DMs are no longer detected.

Add a conservative fallback that requires two signals before classifying
as DM: memberCount === 2 AND no explicit m.room.name. Group rooms almost
always have explicit names; DMs almost never do.

Error handling distinguishes M_NOT_FOUND (missing state event, expected
for unnamed rooms) from network/auth errors. Non-404 errors fall through
to group classification rather than guessing.

This is independently revertable — removing this commit restores pure
protocol-based detection without any heuristic fallback.

* fix(matrix): add parentPeer for DM room binding support

Add parentPeer to DM routes so conversations are bindable by room ID
while preserving DM trust semantics (secure 1:1, no group restrictions).

Suggested by @KirillShchetinin.

* fix(matrix): override DM detection for explicitly configured rooms

Builds on @robertcorreiro's config-driven approach from openclaw#9106.

Move resolveMatrixRoomConfig() before the DM check. If a room matches
a non-wildcard config entry (matchSource === "direct") and was
classified as DM, override the classification to group. This gives users
a deterministic escape hatch for misclassified rooms.

Wildcards are excluded from the override to avoid breaking DM routing
when a "*" catch-all exists. roomConfig is gated behind isRoom so DMs
never inherit group settings (skills, systemPrompt, autoReply).

This commit is independently droppable if the scope is too broad.

* test(matrix): add DM detection and config override tests

- 15 unit tests for direct.ts: all detection paths, priority order,
  M_NOT_FOUND vs network error handling, edge cases (whitespace names,
  API failures)
- 8 unit tests for rooms.ts: matchSource classification, wildcard
  safety for DM override, direct match priority over wildcard

* Changelog: note matrix DM routing follow-up

* fix(matrix): preserve DM fallback and room bindings

---------

Co-authored-by: Tak Hoffman <[email protected]>
senw-developers pushed a commit to senw-developers/va-openclaw that referenced this pull request Mar 17, 2026
…tic (openclaw#19736)

* fix(matrix): remove memberCount heuristic from DM detection

The memberCount === 2 check in isDirectMessage() misclassifies 2-person
group rooms (admin channels, monitoring rooms) as DMs, routing them to
the main session instead of their room-specific session.

Matrix already distinguishes DMs from groups at the protocol level via
m.direct account data and is_direct member state flags. Both are already
checked by client.dms.isDm() and hasDirectFlag(). The memberCount
heuristic only adds false positives for 2-person groups.

Move resolveMemberCount() below the protocol-level checks so it is only
reached for rooms not matched by m.direct or is_direct. This narrows its
role to diagnostic logging for confirmed group rooms.

Refs: openclaw#19739

* fix(matrix): add conservative fallback for broken DM flags

Some homeservers (notably Continuwuity) have broken m.direct account
data or never set is_direct on invite events. With the memberCount
heuristic removed, these DMs are no longer detected.

Add a conservative fallback that requires two signals before classifying
as DM: memberCount === 2 AND no explicit m.room.name. Group rooms almost
always have explicit names; DMs almost never do.

Error handling distinguishes M_NOT_FOUND (missing state event, expected
for unnamed rooms) from network/auth errors. Non-404 errors fall through
to group classification rather than guessing.

This is independently revertable — removing this commit restores pure
protocol-based detection without any heuristic fallback.

* fix(matrix): add parentPeer for DM room binding support

Add parentPeer to DM routes so conversations are bindable by room ID
while preserving DM trust semantics (secure 1:1, no group restrictions).

Suggested by @KirillShchetinin.

* fix(matrix): override DM detection for explicitly configured rooms

Builds on @robertcorreiro's config-driven approach from openclaw#9106.

Move resolveMatrixRoomConfig() before the DM check. If a room matches
a non-wildcard config entry (matchSource === "direct") and was
classified as DM, override the classification to group. This gives users
a deterministic escape hatch for misclassified rooms.

Wildcards are excluded from the override to avoid breaking DM routing
when a "*" catch-all exists. roomConfig is gated behind isRoom so DMs
never inherit group settings (skills, systemPrompt, autoReply).

This commit is independently droppable if the scope is too broad.

* test(matrix): add DM detection and config override tests

- 15 unit tests for direct.ts: all detection paths, priority order,
  M_NOT_FOUND vs network error handling, edge cases (whitespace names,
  API failures)
- 8 unit tests for rooms.ts: matchSource classification, wildcard
  safety for DM override, direct match priority over wildcard

* Changelog: note matrix DM routing follow-up

* fix(matrix): preserve DM fallback and room bindings

---------

Co-authored-by: Tak Hoffman <[email protected]>
V-Gutierrez pushed a commit to V-Gutierrez/openclaw-vendor that referenced this pull request Mar 17, 2026
…tic (openclaw#19736)

* fix(matrix): remove memberCount heuristic from DM detection

The memberCount === 2 check in isDirectMessage() misclassifies 2-person
group rooms (admin channels, monitoring rooms) as DMs, routing them to
the main session instead of their room-specific session.

Matrix already distinguishes DMs from groups at the protocol level via
m.direct account data and is_direct member state flags. Both are already
checked by client.dms.isDm() and hasDirectFlag(). The memberCount
heuristic only adds false positives for 2-person groups.

Move resolveMemberCount() below the protocol-level checks so it is only
reached for rooms not matched by m.direct or is_direct. This narrows its
role to diagnostic logging for confirmed group rooms.

Refs: openclaw#19739

* fix(matrix): add conservative fallback for broken DM flags

Some homeservers (notably Continuwuity) have broken m.direct account
data or never set is_direct on invite events. With the memberCount
heuristic removed, these DMs are no longer detected.

Add a conservative fallback that requires two signals before classifying
as DM: memberCount === 2 AND no explicit m.room.name. Group rooms almost
always have explicit names; DMs almost never do.

Error handling distinguishes M_NOT_FOUND (missing state event, expected
for unnamed rooms) from network/auth errors. Non-404 errors fall through
to group classification rather than guessing.

This is independently revertable — removing this commit restores pure
protocol-based detection without any heuristic fallback.

* fix(matrix): add parentPeer for DM room binding support

Add parentPeer to DM routes so conversations are bindable by room ID
while preserving DM trust semantics (secure 1:1, no group restrictions).

Suggested by @KirillShchetinin.

* fix(matrix): override DM detection for explicitly configured rooms

Builds on @robertcorreiro's config-driven approach from openclaw#9106.

Move resolveMatrixRoomConfig() before the DM check. If a room matches
a non-wildcard config entry (matchSource === "direct") and was
classified as DM, override the classification to group. This gives users
a deterministic escape hatch for misclassified rooms.

Wildcards are excluded from the override to avoid breaking DM routing
when a "*" catch-all exists. roomConfig is gated behind isRoom so DMs
never inherit group settings (skills, systemPrompt, autoReply).

This commit is independently droppable if the scope is too broad.

* test(matrix): add DM detection and config override tests

- 15 unit tests for direct.ts: all detection paths, priority order,
  M_NOT_FOUND vs network error handling, edge cases (whitespace names,
  API failures)
- 8 unit tests for rooms.ts: matchSource classification, wildcard
  safety for DM override, direct match priority over wildcard

* Changelog: note matrix DM routing follow-up

* fix(matrix): preserve DM fallback and room bindings

---------

Co-authored-by: Tak Hoffman <[email protected]>
alexey-pelykh pushed a commit to remoteclaw/remoteclaw that referenced this pull request Mar 22, 2026
…tic (openclaw#19736)

* fix(matrix): remove memberCount heuristic from DM detection

The memberCount === 2 check in isDirectMessage() misclassifies 2-person
group rooms (admin channels, monitoring rooms) as DMs, routing them to
the main session instead of their room-specific session.

Matrix already distinguishes DMs from groups at the protocol level via
m.direct account data and is_direct member state flags. Both are already
checked by client.dms.isDm() and hasDirectFlag(). The memberCount
heuristic only adds false positives for 2-person groups.

Move resolveMemberCount() below the protocol-level checks so it is only
reached for rooms not matched by m.direct or is_direct. This narrows its
role to diagnostic logging for confirmed group rooms.

Refs: openclaw#19739

* fix(matrix): add conservative fallback for broken DM flags

Some homeservers (notably Continuwuity) have broken m.direct account
data or never set is_direct on invite events. With the memberCount
heuristic removed, these DMs are no longer detected.

Add a conservative fallback that requires two signals before classifying
as DM: memberCount === 2 AND no explicit m.room.name. Group rooms almost
always have explicit names; DMs almost never do.

Error handling distinguishes M_NOT_FOUND (missing state event, expected
for unnamed rooms) from network/auth errors. Non-404 errors fall through
to group classification rather than guessing.

This is independently revertable — removing this commit restores pure
protocol-based detection without any heuristic fallback.

* fix(matrix): add parentPeer for DM room binding support

Add parentPeer to DM routes so conversations are bindable by room ID
while preserving DM trust semantics (secure 1:1, no group restrictions).

Suggested by @KirillShchetinin.

* fix(matrix): override DM detection for explicitly configured rooms

Builds on @robertcorreiro's config-driven approach from openclaw#9106.

Move resolveMatrixRoomConfig() before the DM check. If a room matches
a non-wildcard config entry (matchSource === "direct") and was
classified as DM, override the classification to group. This gives users
a deterministic escape hatch for misclassified rooms.

Wildcards are excluded from the override to avoid breaking DM routing
when a "*" catch-all exists. roomConfig is gated behind isRoom so DMs
never inherit group settings (skills, systemPrompt, autoReply).

This commit is independently droppable if the scope is too broad.

* test(matrix): add DM detection and config override tests

- 15 unit tests for direct.ts: all detection paths, priority order,
  M_NOT_FOUND vs network error handling, edge cases (whitespace names,
  API failures)
- 8 unit tests for rooms.ts: matchSource classification, wildcard
  safety for DM override, direct match priority over wildcard

* Changelog: note matrix DM routing follow-up

* fix(matrix): preserve DM fallback and room bindings

---------

Co-authored-by: Tak Hoffman <[email protected]>
(cherry picked from commit d4a960f)
alexey-pelykh pushed a commit to remoteclaw/remoteclaw that referenced this pull request Mar 22, 2026
…tic (openclaw#19736)

* fix(matrix): remove memberCount heuristic from DM detection

The memberCount === 2 check in isDirectMessage() misclassifies 2-person
group rooms (admin channels, monitoring rooms) as DMs, routing them to
the main session instead of their room-specific session.

Matrix already distinguishes DMs from groups at the protocol level via
m.direct account data and is_direct member state flags. Both are already
checked by client.dms.isDm() and hasDirectFlag(). The memberCount
heuristic only adds false positives for 2-person groups.

Move resolveMemberCount() below the protocol-level checks so it is only
reached for rooms not matched by m.direct or is_direct. This narrows its
role to diagnostic logging for confirmed group rooms.

Refs: openclaw#19739

* fix(matrix): add conservative fallback for broken DM flags

Some homeservers (notably Continuwuity) have broken m.direct account
data or never set is_direct on invite events. With the memberCount
heuristic removed, these DMs are no longer detected.

Add a conservative fallback that requires two signals before classifying
as DM: memberCount === 2 AND no explicit m.room.name. Group rooms almost
always have explicit names; DMs almost never do.

Error handling distinguishes M_NOT_FOUND (missing state event, expected
for unnamed rooms) from network/auth errors. Non-404 errors fall through
to group classification rather than guessing.

This is independently revertable — removing this commit restores pure
protocol-based detection without any heuristic fallback.

* fix(matrix): add parentPeer for DM room binding support

Add parentPeer to DM routes so conversations are bindable by room ID
while preserving DM trust semantics (secure 1:1, no group restrictions).

Suggested by @KirillShchetinin.

* fix(matrix): override DM detection for explicitly configured rooms

Builds on @robertcorreiro's config-driven approach from openclaw#9106.

Move resolveMatrixRoomConfig() before the DM check. If a room matches
a non-wildcard config entry (matchSource === "direct") and was
classified as DM, override the classification to group. This gives users
a deterministic escape hatch for misclassified rooms.

Wildcards are excluded from the override to avoid breaking DM routing
when a "*" catch-all exists. roomConfig is gated behind isRoom so DMs
never inherit group settings (skills, systemPrompt, autoReply).

This commit is independently droppable if the scope is too broad.

* test(matrix): add DM detection and config override tests

- 15 unit tests for direct.ts: all detection paths, priority order,
  M_NOT_FOUND vs network error handling, edge cases (whitespace names,
  API failures)
- 8 unit tests for rooms.ts: matchSource classification, wildcard
  safety for DM override, direct match priority over wildcard

* Changelog: note matrix DM routing follow-up

* fix(matrix): preserve DM fallback and room bindings

---------

Co-authored-by: Tak Hoffman <[email protected]>
(cherry picked from commit d4a960f)
dustin-olenslager pushed a commit to dustin-olenslager/ironclaw-supreme that referenced this pull request Mar 24, 2026
…tic (openclaw#19736)

* fix(matrix): remove memberCount heuristic from DM detection

The memberCount === 2 check in isDirectMessage() misclassifies 2-person
group rooms (admin channels, monitoring rooms) as DMs, routing them to
the main session instead of their room-specific session.

Matrix already distinguishes DMs from groups at the protocol level via
m.direct account data and is_direct member state flags. Both are already
checked by client.dms.isDm() and hasDirectFlag(). The memberCount
heuristic only adds false positives for 2-person groups.

Move resolveMemberCount() below the protocol-level checks so it is only
reached for rooms not matched by m.direct or is_direct. This narrows its
role to diagnostic logging for confirmed group rooms.

Refs: openclaw#19739

* fix(matrix): add conservative fallback for broken DM flags

Some homeservers (notably Continuwuity) have broken m.direct account
data or never set is_direct on invite events. With the memberCount
heuristic removed, these DMs are no longer detected.

Add a conservative fallback that requires two signals before classifying
as DM: memberCount === 2 AND no explicit m.room.name. Group rooms almost
always have explicit names; DMs almost never do.

Error handling distinguishes M_NOT_FOUND (missing state event, expected
for unnamed rooms) from network/auth errors. Non-404 errors fall through
to group classification rather than guessing.

This is independently revertable — removing this commit restores pure
protocol-based detection without any heuristic fallback.

* fix(matrix): add parentPeer for DM room binding support

Add parentPeer to DM routes so conversations are bindable by room ID
while preserving DM trust semantics (secure 1:1, no group restrictions).

Suggested by @KirillShchetinin.

* fix(matrix): override DM detection for explicitly configured rooms

Builds on @robertcorreiro's config-driven approach from openclaw#9106.

Move resolveMatrixRoomConfig() before the DM check. If a room matches
a non-wildcard config entry (matchSource === "direct") and was
classified as DM, override the classification to group. This gives users
a deterministic escape hatch for misclassified rooms.

Wildcards are excluded from the override to avoid breaking DM routing
when a "*" catch-all exists. roomConfig is gated behind isRoom so DMs
never inherit group settings (skills, systemPrompt, autoReply).

This commit is independently droppable if the scope is too broad.

* test(matrix): add DM detection and config override tests

- 15 unit tests for direct.ts: all detection paths, priority order,
  M_NOT_FOUND vs network error handling, edge cases (whitespace names,
  API failures)
- 8 unit tests for rooms.ts: matchSource classification, wildcard
  safety for DM override, direct match priority over wildcard

* Changelog: note matrix DM routing follow-up

* fix(matrix): preserve DM fallback and room bindings

---------

Co-authored-by: Tak Hoffman <[email protected]>
0x666c6f added a commit to 0x666c6f/openclaw that referenced this pull request Mar 26, 2026
* refactor: share Apple talk config parsing

* refactor: add canonical talk config payload

* refactor: centralize talk silence timeout defaults

* test: cover invalid talk config inputs

* test: decouple ios talk parsing coverage

* fix: resolve live config paths in status and gateway metadata (openclaw#39952)

* fix: resolve live config paths in status and gateway metadata

* fix: resolve remaining runtime config path references

* test: cover gateway config.set config path response

* fix(web-search): restore OpenRouter compatibility for Perplexity (openclaw#39937) (openclaw#39937)

* Zalo: fix provider lifecycle restarts (openclaw#39892)

* Zalo: fix provider lifecycle restarts

* Zalo: add typing indicators, smart webhook cleanup, and API type fixes

* fix review

* add allow list test secrect

* Zalo: bound webhook cleanup during shutdown

* Zalo: bound typing chat action timeout

* Zalo: use plugin-safe abort helper import

* fix(plugins): ship Feishu bundled runtime dependency (openclaw#39990)

* fix: ship feishu bundled runtime dependency

* test: align feishu bundled dependency specs

* fix(hooks): use resolveAgentIdFromSessionKey in runBeforeReset  (openclaw#39875)

Merged via squash.

Prepared head SHA: 00a2b24
Co-authored-by: rbutera <[email protected]>
Co-authored-by: altaywtf <[email protected]>
Reviewed-by: @altaywtf

* CLI: include commit hash in --version output (openclaw#39712)

* CLI: include commit hash in --version output

* fix(version): harden commit SHA resolution and keep output consistent

* CLI: keep install checks compatible with commit-tagged version output

* fix(cli): include commit hash in root version fast path

* test(cli): allow null commit-hash mocks

* Installer: share version parser across install scripts

* Installer: avoid sourcing helpers from stdin cwd

* CLI: note commit-tagged version output

* CLI: anchor commit hash resolution to module root

* CLI: harden commit hash resolution

* CLI: fix commit hash lookup edge cases

* CLI: prefer live git metadata in dev builds

* CLI: keep git lookup inside package root

* Infra: tolerate invalid moduleUrl hints

* CLI: cache baked commit metadata fallbacks

* CLI: align changelog attribution with prep gate

* CLI: restore changelog contributor credit

---------

Co-authored-by: echoVic <[email protected]>
Co-authored-by: echoVic <[email protected]>

* fix: fail closed talk provider selection

* fix: align talk config secret schemas

* refactor: require canonical talk resolved payload

* test: add talk config contract fixtures

* refactor: split talk gateway config loaders

* refactor: avoid checkout during prep head verification

* refactor: dedupe prep branch push flow

* fix: treat model api drift as baseUrl refresh

* refactor: extract pure models config merge helpers

* refactor: expand provider capability registry

* refactor: reuse one models.json read per write

* fix: publish models.json atomically

* refactor: scope prep push results to env artifacts

* refactor: fold implicit provider injection into resolver

* fix(telegram): use message previews in DMs

* CI: scope CodeQL JavaScript analysis

* feat: allow compaction model override via config (openclaw#38753)

Merged via squash.

Prepared head SHA: a3d6d6c
Co-authored-by: starbuck100 <[email protected]>
Co-authored-by: jalehman <[email protected]>
Reviewed-by: @jalehman

* fix(sessions): clear stale contextTokens on model switch (openclaw#38044)

Merged via squash.

Prepared head SHA: bac2df4
Co-authored-by: yuweuii <[email protected]>
Co-authored-by: jalehman <[email protected]>
Reviewed-by: @jalehman

* fix: prefer bundled channel plugins over npm duplicates (openclaw#40094)

* fix: prefer bundled channel plugins over npm duplicates

* fix: tighten bundled plugin review follow-ups

* fix: address check gate follow-ups

* docs: add changelog for bundled plugin install fix

* fix: align lifecycle test formatting with CI oxfmt

* docs: update Brave Search API docs for Feb 2026 plan restructuring (openclaw#40111)

Merged via squash.

Prepared head SHA: c651f07
Co-authored-by: remusao <[email protected]>
Co-authored-by: gumadeiras <[email protected]>
Reviewed-by: @gumadeiras

* Add too-many-prs override label handling

* CI: satisfy provider merge fixture typing

* Tests: reduce web search secret-scan noise

* Web search: rename Perplexity auth source helper

* Docs: use placeholder OpenRouter key in Perplexity guide

* Docs: use placeholder OpenRouter key in web tool docs

* Fixtures: normalize talk config API key placeholder

* Tests: lower entropy git commit fixtures

* Chore: widen xxxxx detect-secrets allowlist

* Chore: refresh detect-secrets baseline

* Web search: allowlist Perplexity auth source type name

* Chore: refresh detect-secrets baseline after docs line changes

* Chore: refresh detect-secrets baseline after final scan

* Chore: refresh detect-secrets baseline for Feishu docs

* CLI: set local gateway mode in setup

* Tests: format daemon lifecycle CLI coverage

* refactor: use model compat for anthropic tool payload normalization

* refactor: move bundled extension gap allowlists into manifests

* refactor: thread config runtime env through models config

* refactor: split models registry loading from persistence

* refactor: centralize transcript provider quirks

* refactor: decompose implicit provider resolution

* refactor: extract openai stream wrappers

* refactor: validate bundled extension release metadata

* refactor: extract static provider builders

* refactor: extract provider stream wrappers

* refactor: extract bundled extension manifest parser

* test: standardize hermetic provider env snapshots

* test: add implicit provider matrix coverage

* fix(acp): persist spawned child session history (openclaw#40137)

Merged via squash.

Prepared head SHA: 62de5d5
Co-authored-by: mbelinky <[email protected]>
Co-authored-by: mbelinky <[email protected]>
Reviewed-by: @mbelinky

* fix: require talk resolved payload

* refactor: dedupe android talk config parsing

* test: expand talk config contract fixtures

* refactor: split android talk voice resolution

* test: isolate plugin loader from mocked module cache

* test: isolate legacy plugin-sdk root import check

* test: isolate git commit resolution fallbacks

* refactor: simplify plugin sdk compatibility aliases

* refactor: centralize acp session resolution guards

* refactor: neutralize context engine runtime bridge

* refactor: split doctor config analysis helpers

* refactor: extract ios watch reply coordinator

* fix: restore acp session meta narrowing

* refactor: extract qmd process runner

* fix: restore gate after rebase

* docs: add Browserbase as hosted remote CDP option

Add Browserbase documentation section alongside the existing Browserless
section in the browser docs. Includes signup instructions, CDP connection
configuration, and environment variable setup for both English and Chinese
(zh-CN) translations.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* Revert "docs: add Browserbase as hosted remote CDP option"

This reverts commit c469657.

* docs: add Browserbase as hosted remote CDP option

Add Browserbase documentation section alongside the existing Browserless
section in the browser docs. Includes signup instructions, CDP connection
configuration, and environment variable setup.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* docs: fix duplicate heading lint error

Rename "Configuration" sub-heading to "Profile setup" to avoid
MD024/no-duplicate-heading conflict with the existing top-level
"Configuration" heading.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* docs: fix Browserbase section to match official docs

Browserbase requires creating a session via their API to get a CDP
connect URL, unlike Browserless which uses a static endpoint. Updated
to show the correct curl-based session creation flow, removed
unverified static WebSocket URL, and added the 5-minute connect
timeout note from official docs.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* docs: restore direct wss://connect.browserbase.com URL

Browserbase exposes a direct WebSocket connect endpoint that
auto-creates a session, similar to how Browserless works. Simplified
the section to use this static URL pattern instead of requiring
manual session creation via the API.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* docs: fact-check Browserbase section against official docs

- Fix CAPTCHA/stealth/proxy claims: these are Developer plan+ only,
  not available on free tier
- Fix free tier limits: 1 browser hour, 15-min session duration
  (not "60 minutes of monthly usage")
- Add link to pricing page for paid plan details
- Simplify structure to match Browserless section format
- Remove sub-headings to match Browserless section style

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* docs: simplify Browserbase section, drop pricing details

Restore platform-level feature description (CAPTCHA solving, stealth
mode, proxies) without plan-specific pricing gating. Keep free tier
note brief.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* feat(browser): support direct WebSocket CDP URLs for Browserbase

Browserbase uses direct WebSocket connections (wss://) rather than the
standard HTTP-based /json/version CDP discovery flow used by Browserless.
This change teaches the browser tool to accept ws:// and wss:// URLs as
cdpUrl values: when a WebSocket URL is detected, OpenClaw connects
directly instead of attempting HTTP discovery.

Changes:
- config.ts: accept ws:// and wss:// in cdpUrl validation
- cdp.helpers.ts: add isWebSocketUrl() helper
- cdp.ts: skip /json/version when cdpUrl is already a WebSocket URL
- chrome.ts: probe WSS endpoints via WebSocket handshake instead of HTTP
- cdp.test.ts: add test for direct WebSocket target creation
- docs/tools/browser.md: update Browserbase section with correct URL
  format and notes

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* test+docs: comprehensive coverage and generic framing

- Add 12 new tests covering: isWebSocketUrl detection, parseHttpUrl WSS
  acceptance/rejection, direct WS target creation with query params,
  SSRF enforcement on WS URLs, WS reachability probing bypasses HTTP
- Reframe docs section as generic "Direct WebSocket CDP providers" with
  Browserbase as one example — any WSS-based provider works
- Update security tips to mention WSS alongside HTTPS

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix(browser): update existing tests for ws/wss protocol support

Two pre-existing tests still expected ws:// URLs to be rejected by
parseHttpUrl, which now accepts them. Switch the invalid-protocol
fixture to ftp:// and tighten the assertion to match the full
"must be http(s) or ws(s)" error message.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix(browser): preserve wss:// cdpUrl in legacy default profile resolution

* chore: remove vendor-specific references from code comments

* style(browser): fix oxfmt formatting in config.ts

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* fix: preserve loopback ws cdp tab ops (openclaw#31085) (thanks @shrey150)

* fix: share context engine registry across bundled chunks (openclaw#40115)

Merged via squash.

Prepared head SHA: 6af4820
Co-authored-by: jalehman <[email protected]>
Co-authored-by: jalehman <[email protected]>
Reviewed-by: @jalehman

* fix(browser): rewrite 0.0.0.0 and [::] wildcard addresses in CDP WebSocket URLs

Containerized browsers (e.g. browserless in Docker) report
`ws://0.0.0.0:<internal-port>` in their `/json/version` response.
`normalizeCdpWsUrl` rewrites loopback WS hosts to the external
CDP host:port, but `0.0.0.0` and `[::]` were not treated as
addresses needing rewriting, causing OpenClaw to try connecting
to `ws://0.0.0.0:3000` literally — which always fails.

Fixes openclaw#17752

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: normalize wildcard remote CDP websocket URLs (openclaw#17760) (thanks @joeharouni)

* fix(browser): wait for extension tabs after relay drop (openclaw#32331)

* fix: wait for extension relay tab reconnects (openclaw#32461) (thanks @AaronWander)

* fix(infra): make browser relay bind address configurable

Add browser.relayBindHost config option so the Chrome extension relay
server can bind to a non-loopback address (e.g. 0.0.0.0 for WSL2).
Defaults to 127.0.0.1 when unset, preserving current behavior.

Closes openclaw#39214

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix(browser): add IP validation, fix upgrade handler for non-loopback bind

- Zod schema: validate relayBindHost with ipv4/ipv6 instead of bare string
- Upgrade handler: allow non-loopback connections when bindHost is explicitly
  non-loopback (e.g. 0.0.0.0 for WSL2), keeping loopback-only default
- Test: verify actual bind address via relay.bindHost instead of just checking
  reachability on 127.0.0.1 which passes regardless
- Expose bindHost on ChromeExtensionRelayServer type for inspection

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: make browser relay bind address configurable (openclaw#39364) (thanks @mvanhorn)

* docs: add WSL2 + Windows remote Chrome CDP troubleshooting (openclaw#39407) (thanks @Owlock)

* macos: add remote gateway token field for remote mode

* macos: clarify remote token placeholder text

* macos: add mode-toggle remote token sync coverage

* tests: document remote token persistence across mode toggle

* fix(macos): preserve unsupported remote gateway tokens

* docs(changelog): credit macos remote token author

* fix(macos): improve tailscale gateway discovery (openclaw#40167)

Sanitized test tailnet hostnames and re-ran the targeted macOS gateway discovery test suite before merge.

* refactor(browser): scope CDP sessions and harden stale target recovery

* feat: add local backup CLI (openclaw#40163)

Merged via squash.

Prepared head SHA: ed46625
Co-authored-by: shichangs <[email protected]>
Co-authored-by: gumadeiras <[email protected]>
Reviewed-by: @gumadeiras

* fix(ci): scope secrets scan to branch changes

* fix(ci): refresh detect-secrets baseline

* fix: harden backup verify path validation

* fix(plugin-sdk): lazily load legacy root alias

* fix(setup-podman): cd to TMPDIR before podman load to avoid cwd permission error (openclaw#39435)

* fix(setup-podman): cd to TMPDIR before podman load to avoid inherited cwd permission error

* fix(podman): safe cwd in run_as_user to prevent chdir errors

Co-Authored-By: Claude Opus 4.6  <[email protected]>
Signed-off-by: sallyom <[email protected]>

---------

Signed-off-by: sallyom <[email protected]>
Co-authored-by: sallyom <[email protected]>
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>

* fix(cron): consolidate announce delivery, fire-and-forget trigger, and minimal prompt mode (openclaw#40204)

* fix(cron): consolidate announce delivery and detach manual runs

* fix: queue detached cron runs (openclaw#40204)

* Gateway/iOS: replay queued foreground actions safely after resume (openclaw#40281)

Merged via squash.

- Local validation: `pnpm exec vitest run --config vitest.gateway.config.ts src/gateway/server-methods/nodes.invoke-wake.test.ts`
- Local validation: `pnpm build`
- mb-server validation: `pnpm exec vitest run --config vitest.gateway.config.ts src/gateway/server-methods/nodes.invoke-wake.test.ts`
- mb-server validation: `pnpm build`
- mb-server validation: `pnpm protocol:check`

* iOS: auto-load the scoped gateway canvas with safe fallback (openclaw#40282)

Merged via squash.

- mb-server validation: `swift test --package-path apps/shared/OpenClawKit --filter GatewayNodeSessionTests`
- mb-server validation: `pnpm build`
- Scope note: top-level `RootTabs` shell change was intentionally removed from this PR before merge

* Update CHANGELOG.md

* Update CHANGELOG.md

* fix(run-openclaw-podman): add SELinux :Z mount option on enforcing/permissive hosts (openclaw#39449)

* fix(run-openclaw-podman): add SELinux :Z mount option on Linux with enforcing/permissive SELinux

* fix(quadlet): add SELinux :Z label to openclaw.container.in volume mount

* fix(podman): add SELinux :Z mount option for Fedora/RHEL hosts

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Signed-off-by: sallyom <[email protected]>

---------

Signed-off-by: sallyom <[email protected]>
Co-authored-by: sallyom <[email protected]>
Co-authored-by: Claude Opus 4.6 <[email protected]>

* Docker: trim runtime image payload (openclaw#40307)

* Docker: shrink runtime image payload

* Docker: add runtime pnpm opt-in

* Docker: collapse helper entrypoint chmod layers

* Docker: restore bundled pnpm runtime

* Update CHANGELOG.md

* docs(changelog): move post-2026.3.8 entries to unreleased (openclaw#40342)

* docs(changelog): move post-2026.3.8 entries to unreleased

* Update CHANGELOG.md

* fix(tui): improve color contrast for light-background terminals (openclaw#40345)

* fix(tui): improve colour contrast for light-background terminals (openclaw#38636)

Detect light terminal backgrounds via COLORFGBG and apply a WCAG
AA-compliant light palette. Adds OPENCLAW_THEME=light|dark env var
override for terminals without auto-detection.

Uses proper sRGB linearisation and WCAG 2.1 contrast ratios to pick
whichever text palette (dark or light) has higher contrast against
the detected background colour.

Co-authored-by: ademczuk <[email protected]>

* Update CHANGELOG.md

---------

Co-authored-by: ademczuk <[email protected]>
Co-authored-by: ademczuk <[email protected]>

* fix(models): keep --all aligned with synthetic catalog rows

* test(models): refresh list assertions after main sync

* fix: normalize openai-codex gpt-5.4 transport overrides

* docs: add refactor cluster backlog

* refactor: dedupe plugin runtime stores

* refactor: share gateway argv parsing

* refactor: extract gateway port diagnostics helper

* refactor: reuse broadcast route key construction

* refactor: share multi-account config schema fragments

* test: dedupe brave llm-context rejection cases

* refactor: share channel config adapter base

* fix(agents): bootstrap runtime plugins before context-engine resolution

* docs(changelog): remove rebase marker

* refactor: harden browser relay CDP flows

* fix(config): refresh runtime snapshot from disk after write. Fixes openclaw#37175 (openclaw#37313)

Merged via squash.

Prepared head SHA: 69e1861
Co-authored-by: bbblending <[email protected]>
Co-authored-by: gumadeiras <[email protected]>
Reviewed-by: @gumadeiras

* refactor: harden browser runtime profile handling

* refactor(models): extract list row builders

* refactor(agents): extract provider model normalization

* refactor(models): split models.json planning from writes

* refactor(models): split provider discovery helpers

* chore(docs): drop refactor cleanup tracker

* gateway: fix global Control UI 404s for symlinked wrappers and bundled package roots (openclaw#40385)

Merged via squash.

Prepared head SHA: 567b3ed
Co-authored-by: velvet-shark <[email protected]>
Co-authored-by: velvet-shark <[email protected]>
Reviewed-by: @velvet-shark

* Docker: improve build cache reuse (openclaw#40351)

* Docker: improve build cache reuse

* Tests: cover Docker build cache layout

* Docker: fix sandbox cache mount continuations

* Docker: document qr-import manifest scope

* Docker: narrow e2e install inputs

* CI: cache Docker builds in workflows

* CI: route sandbox smoke through setup script

* CI: keep sandbox smoke on script path

* fix(tests): correct security check failure

* docs(changelog): correct Control UI contributor credit (openclaw#40420)

Merged via squash.

Prepared head SHA: e4295fe
Co-authored-by: velvet-shark <[email protected]>
Co-authored-by: velvet-shark <[email protected]>
Reviewed-by: @velvet-shark

* fix(models): use 1M context for openai-codex gpt-5.4 (openclaw#37876)

Merged via squash.

Prepared head SHA: c410207
Co-authored-by: yuweuii <[email protected]>
Co-authored-by: jalehman <[email protected]>
Reviewed-by: @jalehman

* fix(telegram): add download timeout to prevent polling loop hang (openclaw#40098)

Merged via squash.

Prepared head SHA: abdfa1a
Co-authored-by: tysoncung <[email protected]>
Co-authored-by: obviyus <[email protected]>
Reviewed-by: @obviyus

* ACP: add optional ingress provenance receipts (openclaw#40473)

Merged via squash.

Prepared head SHA: b63e46d
Co-authored-by: mbelinky <[email protected]>
Co-authored-by: mbelinky <[email protected]>
Reviewed-by: @mbelinky

* alphabetize web search providers (openclaw#40259)

Merged via squash.

Prepared head SHA: be6350e
Co-authored-by: kesku <[email protected]>
Co-authored-by: obviyus <[email protected]>
Reviewed-by: @obviyus

* fix(plugin-sdk): remove remaining bundled plugin src imports (openclaw#39638)

Verified:
- pnpm build
- pnpm check
- pnpm test:macmini

Co-authored-by: Kyle <[email protected]>
Co-authored-by: Tak Hoffman <[email protected]>

* test: fix android talk config contract fixture

* chore(acpx): move runtime test fixtures to test-utils (openclaw#40548)

Verified:
- pnpm install --frozen-lockfile
- pnpm build
- pnpm check
- pnpm test:macmini

* fix(agents): re-expose configured tools under restrictive profiles

* fix(media): accept reader read result type

* build(protocol): sync generated swift models

* fix: dedupe inbound Telegram DM replies per agent (openclaw#40519)

Merged via squash.

Prepared head SHA: 6e235e7
Co-authored-by: obviyus <[email protected]>
Co-authored-by: obviyus <[email protected]>
Reviewed-by: @obviyus

* fix(matrix): restore robust DM routing without the memberCount heuristic (openclaw#19736)

* fix(matrix): remove memberCount heuristic from DM detection

The memberCount === 2 check in isDirectMessage() misclassifies 2-person
group rooms (admin channels, monitoring rooms) as DMs, routing them to
the main session instead of their room-specific session.

Matrix already distinguishes DMs from groups at the protocol level via
m.direct account data and is_direct member state flags. Both are already
checked by client.dms.isDm() and hasDirectFlag(). The memberCount
heuristic only adds false positives for 2-person groups.

Move resolveMemberCount() below the protocol-level checks so it is only
reached for rooms not matched by m.direct or is_direct. This narrows its
role to diagnostic logging for confirmed group rooms.

Refs: openclaw#19739

* fix(matrix): add conservative fallback for broken DM flags

Some homeservers (notably Continuwuity) have broken m.direct account
data or never set is_direct on invite events. With the memberCount
heuristic removed, these DMs are no longer detected.

Add a conservative fallback that requires two signals before classifying
as DM: memberCount === 2 AND no explicit m.room.name. Group rooms almost
always have explicit names; DMs almost never do.

Error handling distinguishes M_NOT_FOUND (missing state event, expected
for unnamed rooms) from network/auth errors. Non-404 errors fall through
to group classification rather than guessing.

This is independently revertable — removing this commit restores pure
protocol-based detection without any heuristic fallback.

* fix(matrix): add parentPeer for DM room binding support

Add parentPeer to DM routes so conversations are bindable by room ID
while preserving DM trust semantics (secure 1:1, no group restrictions).

Suggested by @KirillShchetinin.

* fix(matrix): override DM detection for explicitly configured rooms

Builds on @robertcorreiro's config-driven approach from openclaw#9106.

Move resolveMatrixRoomConfig() before the DM check. If a room matches
a non-wildcard config entry (matchSource === "direct") and was
classified as DM, override the classification to group. This gives users
a deterministic escape hatch for misclassified rooms.

Wildcards are excluded from the override to avoid breaking DM routing
when a "*" catch-all exists. roomConfig is gated behind isRoom so DMs
never inherit group settings (skills, systemPrompt, autoReply).

This commit is independently droppable if the scope is too broad.

* test(matrix): add DM detection and config override tests

- 15 unit tests for direct.ts: all detection paths, priority order,
  M_NOT_FOUND vs network error handling, edge cases (whitespace names,
  API failures)
- 8 unit tests for rooms.ts: matchSource classification, wildcard
  safety for DM override, direct match priority over wildcard

* Changelog: note matrix DM routing follow-up

* fix(matrix): preserve DM fallback and room bindings

---------

Co-authored-by: Tak Hoffman <[email protected]>

* Fix cron text announce delivery for Telegram targets (openclaw#40575)

Merged via squash.

Prepared head SHA: 54b1513
Co-authored-by: obviyus <[email protected]>
Co-authored-by: obviyus <[email protected]>
Reviewed-by: @obviyus

* fix: clear plugin discovery cache after plugin installation (openclaw#39752)

Verified:
- pnpm build
- pnpm check
- pnpm test:macmini

Co-authored-by: GazeKingNuWu <[email protected]>
Co-authored-by: Tak Hoffman <[email protected]>

* test: fix windows secrets runtime ci

* fix(daemon): enable LaunchAgent before bootstrap on restart

restartLaunchAgent was missing the launchctl enable call that
installLaunchAgent already performs. launchd can persist a "disabled"
state after bootout, causing bootstrap to silently fail and leaving the
gateway unloaded until a manual reinstall.

Fixes openclaw#39211

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix(daemon): also enable LaunchAgent in repairLaunchAgentBootstrap

The repair/recovery path had the same missing `enable` guard as
`restartLaunchAgent`.  If launchd persists a "disabled" state after a
previous `bootout`, the `bootstrap` call in `repairLaunchAgentBootstrap`
fails silently, leaving the gateway unloaded in the recovery flow.

Add the same `enable` guard before `bootstrap` that was already applied
to `installLaunchAgent` and (in this PR) `restartLaunchAgent`.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix(gateway): exit non-zero on restart shutdown timeout

When a config-change restart hits the force-exit timeout, exit with
code 1 instead of 0 so launchd/systemd treats it as a failure and
triggers a clean process restart. Stop-timeout stays at exit(0)
since graceful stops should not cause supervisor recovery.

Closes openclaw#36822

* test(secrets): skip ACL-dependent runtime snapshot tests on windows

* fix: add changelog for restart timeout recovery (openclaw#40380) (thanks @dsantoreis)

* fix(browser): enforce redirect-hop SSRF checks

* fix(cron): restore owner-only tools for isolated runs

* test(cron): cover owner-only tool availability

* fix(msteams): enforce sender allowlists with route allowlists

* fix(gateway): validate config before restart to prevent crash + macOS permission loss (openclaw#35862)

When 'openclaw gateway restart' is run with an invalid config, the new
process crashes on startup due to config validation failure. On macOS,
this causes Full Disk Access (TCC) permissions to be lost because the
respawned process has a different PID.

Add getConfigValidationError() helper and pre-flight config validation
in both runServiceRestart() and runServiceStart(). If config is invalid,
abort with a clear error message instead of crashing.

The config watcher's hot-reload path already had this guard
(handleInvalidSnapshot), but the CLI restart/start commands did not.

AI-assisted (OpenClaw agent, fully tested)

* fix(gateway): catch startup failure in run loop to prevent process exit (openclaw#35862)

When an in-process restart (SIGUSR1) triggers a config-triggered restart
and the new config is invalid, params.start() throws and the while loop
exits, killing the process. On macOS this loses TCC permissions.

Wrap params.start() in try/catch: on failure, set server=null, log the
error, and wait for the next SIGUSR1 instead of crashing.

* test: add runServiceStart config pre-flight tests (openclaw#35862)

Address Greptile review: add test coverage for runServiceStart path.
The error message copy-paste issue was already fixed in the DRY refactor
(uses params.serviceNoun instead of hardcoded 'restart').

* fix: address bot review feedback on openclaw#35862

- Remove dead 'return false' in runServiceStart (Greptile)
- Include stack trace in run-loop crash guard error log (Greptile)
- Only catch startup errors on subsequent restarts, not initial start (Codex P1)
- Add JSDoc note about env var false positive edge case (Codex P1)

* fix: move config pre-flight before onNotLoaded in runServiceRestart (Codex P2)

The config check was positioned after onNotLoaded, which could send
SIGUSR1 to an unmanaged process before config was validated.

* fix: release gateway lock on restart failure + reply to Codex reviews

- Release gateway lock when in-process restart fails, so daemon
  restart/stop can still manage the process (Codex P2)
- P1 (env mismatch) already addressed: best-effort by design, documented
  in JSDoc

* fix(gateway): detect launchd supervision via XPC_SERVICE_NAME

On macOS, launchd sets XPC_SERVICE_NAME on managed processes but does
not set LAUNCH_JOB_LABEL or LAUNCH_JOB_NAME. Without checking
XPC_SERVICE_NAME, isLikelySupervisedProcess() returns false for
launchd-managed gateways, causing restartGatewayProcessWithFreshPid()
to fork a detached child instead of returning "supervised". The
detached child holds the gateway lock while launchd simultaneously
respawns the original process (KeepAlive=true), leading to an infinite
lock-timeout / restart loop.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: detect launchd supervision via xpc service name (openclaw#20555) (thanks @dimat)

* fix(node-host): bind bun and deno approval scripts

* fix(skills): pin validated download roots

* fix(telegram): abort in-flight getUpdates fetch on shutdown

When the gateway receives SIGTERM, runner.stop() stops the grammY polling
loop but does not abort the in-flight getUpdates HTTP request. That request
hangs for up to 30 seconds (the Telegram API timeout). If a new gateway
instance starts polling during that window, Telegram returns a 409 Conflict
error, causing message loss and requiring exponential backoff recovery.

This is especially problematic with service managers (launchd, systemd)
that restart the process immediately after SIGTERM.

Wire an AbortController into the fetch layer so every Telegram API request
(especially the long-polling getUpdates) aborts immediately on shutdown:

- bot.ts: Accept optional fetchAbortSignal in TelegramBotOptions; wrap
  the grammY fetch with AbortSignal.any() to merge the shutdown signal.
- monitor.ts: Create a per-iteration AbortController, pass its signal to
  createTelegramBot, and abort it from the SIGTERM handler, force-restart
  path, and finally block.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix(telegram): use manual signal forwarding to avoid cross-realm AbortSignal

AbortSignal.any() fails in Node.js when signals come from different module
contexts (grammY's internal signal vs local AbortController), producing:
"The signals[0] argument must be an instance of AbortSignal. Received an
instance of AbortSignal".

Replace with manual event forwarding that works across all realms.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: abort telegram getupdates on shutdown (openclaw#23950) (thanks @Gkinthecodeland)

* fix(cron): stagger missed jobs on restart to prevent gateway overload

When the gateway restarts with many overdue cron jobs, they are now
executed with staggered delays to prevent overwhelming the gateway.

- Add missedJobStaggerMs config (default 5s between jobs)
- Add maxMissedJobsPerRestart limit (default 5 jobs immediately)
- Prioritize most overdue jobs by sorting by nextRunAtMs
- Reschedule deferred jobs to fire gradually via normal timer

Fixes openclaw#18892

* fix: stagger missed cron jobs on restart (openclaw#18925) (thanks @rexlunae)

* build: update app deps except carbon

* refactor: extract telegram polling session

* refactor: split cron startup catch-up flow

* refactor: flatten supervisor marker hints

* docs: reorder 2026.3.8 changelog by impact

* build: sync pnpm lockfile

* chore: refresh secrets baseline

* docs: move 2026.3.8 entries back to unreleased

* test: fix Node 24+ test runner and subagent registry mocks

* test: fix Windows fake runtime bin fixtures

* fix: normalize windows runtime shim executables

* chore: prepare 2026.3.8-beta.1 release

* chore: update appcast for 2026.3.8-beta.1

* test: fix windows runtime and restart loop harnesses

* fix(update): re-enable launchd service before updater bootstrap

* chore: prepare 2026.3.8 npm release

* test: narrow gateway loop signal harness

* fix(launchd): harden macOS launchagent install permissions

* fix(onboard): avoid persisting talk fallback on fresh setup

* build: bump unreleased version to 2026.3.9

* fix: stabilize launchd paths and appcast secret scan

* build: sync plugin versions for 2026.3.9

* fix(ui): preserve control-ui auth across refresh (openclaw#40892)

Merged via squash.

Prepared head SHA: f9b2375
Co-authored-by: velvet-shark <[email protected]>
Co-authored-by: velvet-shark <[email protected]>
Reviewed-by: @velvet-shark

* fix(kimi-coding): fix kimi tool format: use native Anthropic tool schema instead of OpenAI … (openclaw#40008)

Verified:
- pnpm install --frozen-lockfile
- pnpm build
- pnpm check
- pnpm test:macmini

Co-authored-by: opriz <[email protected]>
Co-authored-by: Tak Hoffman <[email protected]>

* fix(swiftformat): exclude HostEnvSecurityPolicy.generated.swift from formatters (openclaw#39969)

* test(context-engine): add bundle chunk isolation tests for registry (openclaw#40460)

Merged via squash.

Prepared head SHA: 44622ab
Co-authored-by: dsantoreis <[email protected]>
Co-authored-by: jalehman <[email protected]>
Reviewed-by: @jalehman

* fix(agents): bound compaction retry wait and drain embedded runs on restart (openclaw#40324)

Merged via squash.

Prepared head SHA: cfd9956
Co-authored-by: cgdusek <[email protected]>
Co-authored-by: jalehman <[email protected]>
Reviewed-by: @jalehman

* Allow ACP sessions.patch lineage fields on ACP session keys (openclaw#40995)

Merged via squash.

Prepared head SHA: c1191ed
Co-authored-by: xaeon2026 <[email protected]>
Co-authored-by: mbelinky <[email protected]>
Reviewed-by: @mbelinky

* Update CONTRIBUTING.md

* Add Robin Waslander to maintainers

* Update CONTRIBUTING.md

* fix(acp): map error states to end_turn instead of unconditional refusal (openclaw#41187)

* fix(acp): map error states to end_turn instead of unconditional refusal

* fix: map ACP error stop reason to end_turn (openclaw#41187) (thanks @pejmanjohn)

---------

Co-authored-by: Pejman Pour-Moezzi <[email protected]>
Co-authored-by: Onur <[email protected]>

* fix(acp): propagate setSessionMode gateway errors to client (openclaw#41185)

* fix(acp): propagate setSessionMode gateway errors to client

* fix: add changelog entry for ACP setSessionMode propagation (openclaw#41185) (thanks @pejmanjohn)

---------

Co-authored-by: Pejman Pour-Moezzi <[email protected]>
Co-authored-by: Onur <[email protected]>

* plugins: harden global hook runner state (openclaw#40184)

* fix(telegram): bridge direct delivery to internal message:sent hooks (openclaw#40185)

* telegram: bridge direct delivery message hooks

* telegram: align sent hooks with command session

* Cron: enforce cron-owned delivery contract (openclaw#40998)

Merged via squash.

Prepared head SHA: 5877389
Co-authored-by: mbelinky <[email protected]>
Co-authored-by: mbelinky <[email protected]>
Reviewed-by: @mbelinky

* Agents: add embedded error observations (openclaw#41336)

Merged via squash.

Prepared head SHA: 4900042
Co-authored-by: altaywtf <[email protected]>
Co-authored-by: altaywtf <[email protected]>
Reviewed-by: @altaywtf

* Doctor: fix non-interactive cron repair gating (openclaw#41386)

* iOS: reconnect gateway on foreground return (openclaw#41384)

Merged via squash.

Prepared head SHA: 0e2e0dc
Co-authored-by: mbelinky <[email protected]>
Co-authored-by: mbelinky <[email protected]>
Reviewed-by: @mbelinky

* fix(cron): do not misclassify empty/NO_REPLY as interim acknowledgement (openclaw#41401)

* fix(cron): do not misclassify empty/NO_REPLY as interim acknowledgement

When a cron task's agent returns NO_REPLY, the payload filter strips the
silent token, leaving an empty text string. isLikelyInterimCronMessage()
previously returned true for empty input, causing the cron runner to
inject a forced rerun prompt ('Your previous response was only an
acknowledgement...').

Change the empty-string branch to return false: empty text after payload
filtering means the agent deliberately chose silent completion, not that
it sent an interim 'on it' message.

Fixes openclaw#41246

* fix(cron): do not misclassify empty/NO_REPLY as interim acknowledgement

Fixes openclaw#41246. (openclaw#41383) thanks @jackal092927.

---------

Co-authored-by: xaeon2026 <[email protected]>

* fix(auth): reset cooldown error counters on expiry to prevent infinite escalation (openclaw#41028)

Merged via squash.

Prepared head SHA: 89bd83f
Co-authored-by: zerone0x <[email protected]>
Co-authored-by: altaywtf <[email protected]>
Reviewed-by: @altaywtf

* Gateway: add pending node work primitives (openclaw#41409)

Merged via squash.

Prepared head SHA: a6d7ca9
Co-authored-by: mbelinky <[email protected]>
Co-authored-by: mbelinky <[email protected]>
Reviewed-by: @mbelinky

* Gateway: tighten node pending drain semantics (openclaw#41429)

Merged via squash.

Prepared head SHA: 361c2eb
Co-authored-by: mbelinky <[email protected]>
Co-authored-by: mbelinky <[email protected]>
Reviewed-by: @mbelinky

* acp: fail honestly in bridge mode (openclaw#41424)

Merged via squash.

Prepared head SHA: b5e6e13
Co-authored-by: mbelinky <[email protected]>
Co-authored-by: mbelinky <[email protected]>
Reviewed-by: @mbelinky

* acp: restore session context and controls (openclaw#41425)

Merged via squash.

Prepared head SHA: fcabdf7
Co-authored-by: mbelinky <[email protected]>
Co-authored-by: mbelinky <[email protected]>
Reviewed-by: @mbelinky

* Sandbox: import STATE_DIR from paths directly (openclaw#41439)

* acp: enrich streaming updates for ide clients (openclaw#41442)

Merged via squash.

Prepared head SHA: 0764368
Co-authored-by: mbelinky <[email protected]>
Co-authored-by: mbelinky <[email protected]>
Reviewed-by: @mbelinky

* acp: forward attachments into ACP runtime sessions (openclaw#41427)

Merged via squash.

Prepared head SHA: f2ac51d
Co-authored-by: mbelinky <[email protected]>
Co-authored-by: mbelinky <[email protected]>
Reviewed-by: @mbelinky

* acp: add regression coverage and smoke-test docs (openclaw#41456)

Merged via squash.

Prepared head SHA: 514d587
Co-authored-by: mbelinky <[email protected]>
Co-authored-by: mbelinky <[email protected]>
Reviewed-by: @mbelinky

* fix(agents): probe single-provider billing cooldowns (openclaw#41422)

Merged via squash.

Prepared head SHA: bbc4254
Co-authored-by: altaywtf <[email protected]>
Co-authored-by: altaywtf <[email protected]>
Reviewed-by: @altaywtf

* acp: harden follow-up reliability and attachments (openclaw#41464)

Merged via squash.

Prepared head SHA: 7d167df
Co-authored-by: mbelinky <[email protected]>
Co-authored-by: mbelinky <[email protected]>
Reviewed-by: @mbelinky

* Agents: add fallback error observations (openclaw#41337)

Merged via squash.

Prepared head SHA: 852469c
Co-authored-by: altaywtf <[email protected]>
Co-authored-by: altaywtf <[email protected]>
Reviewed-by: @altaywtf

* build(protocol): regenerate Swift models after pending node work schemas (openclaw#41477)

Merged via squash.

Prepared head SHA: cae0aaf
Co-authored-by: mbelinky <[email protected]>
Co-authored-by: mbelinky <[email protected]>
Reviewed-by: @mbelinky

* fix(discord): apply effective maxLinesPerMessage in live replies (openclaw#40133)

Merged via squash.

Prepared head SHA: 031d032
Co-authored-by: rbutera <[email protected]>
Co-authored-by: altaywtf <[email protected]>
Reviewed-by: @altaywtf

* Logging: harden probe suppression for observations (openclaw#41338)

Merged via squash.

Prepared head SHA: d18356c
Co-authored-by: altaywtf <[email protected]>
Co-authored-by: altaywtf <[email protected]>
Reviewed-by: @altaywtf

* ci(sre:PLA-760): fix smoke workflow fallbacks

* test(sre:PLA-760): allowlist secret-scan false positives

* test(sre:PLA-760): refresh secret baseline for upstream sync

* test(sre:PLA-760): update detect-secrets baseline

* ci(sre:PLA-760): exclude auto-response from zizmor

* fix(ci:PLA-760): unblock audit and bun test lane

* ci(sre:PLA-760): run linux-only ci

---------

Signed-off-by: sallyom <[email protected]>
Co-authored-by: Peter Steinberger <[email protected]>
Co-authored-by: Tak Hoffman <[email protected]>
Co-authored-by: Ayaan Zaidi <[email protected]>
Co-authored-by: darkamenosa <[email protected]>
Co-authored-by: Hermione <[email protected]>
Co-authored-by: rbutera <[email protected]>
Co-authored-by: altaywtf <[email protected]>
Co-authored-by: Altay <[email protected]>
Co-authored-by: echoVic <[email protected]>
Co-authored-by: echoVic <[email protected]>
Co-authored-by: Ayaan Zaidi <[email protected]>
Co-authored-by: Vincent Koc <[email protected]>
Co-authored-by: GitBuck <[email protected]>
Co-authored-by: starbuck100 <[email protected]>
Co-authored-by: jalehman <[email protected]>
Co-authored-by: yuweuii <[email protected]>
Co-authored-by: Rémi <[email protected]>
Co-authored-by: remusao <[email protected]>
Co-authored-by: gumadeiras <[email protected]>
Co-authored-by: Mariano <[email protected]>
Co-authored-by: Shrey Pandya <[email protected]>
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
Co-authored-by: Josh Lehman <[email protected]>
Co-authored-by: Joe Harouni <[email protected]>
Co-authored-by: AaronWander <[email protected]>
Co-authored-by: Matt Van Horn <[email protected]>
Co-authored-by: Charles Dusek <[email protected]>
Co-authored-by: Nimrod Gutman <[email protected]>
Co-authored-by: shichangs <[email protected]>
Co-authored-by: Gustavo Madeira Santana <[email protected]>
Co-authored-by: langdon <[email protected]>
Co-authored-by: sallyom <[email protected]>
Co-authored-by: Tyler Yust <[email protected]>
Co-authored-by: langdon <[email protected]>
Co-authored-by: ademczuk <[email protected]>
Co-authored-by: ademczuk <[email protected]>
Co-authored-by: Doruk Ardahan <[email protected]>
Co-authored-by: 0xsline <[email protected]>
Co-authored-by: bbblending <[email protected]>
Co-authored-by: Radek Sienkiewicz <[email protected]>
Co-authored-by: velvet-shark <[email protected]>
Co-authored-by: Tyson Cung <[email protected]>
Co-authored-by: obviyus <[email protected]>
Co-authored-by: Mariano <[email protected]>
Co-authored-by: Kesku <[email protected]>
Co-authored-by: Kyle <[email protected]>
Co-authored-by: Kyle <[email protected]>
Co-authored-by: Bronko <[email protected]>
Co-authored-by: GazeKingNuWu <[email protected]>
Co-authored-by: GazeKingNuWu <[email protected]>
Co-authored-by: scoootscooob <[email protected]>
Co-authored-by: Daniel dos Santos Reis <[email protected]>
Co-authored-by: DevMac <[email protected]>
Co-authored-by: merlin <[email protected]>
Co-authored-by: dimatu <[email protected]>
Co-authored-by: George Kalogirou <[email protected]>
Co-authored-by: rexlunae <[email protected]>
Co-authored-by: opriz <[email protected]>
Co-authored-by: opriz <[email protected]>
Co-authored-by: Joshua Lelon Mitchell <[email protected]>
Co-authored-by: dsantoreis <[email protected]>
Co-authored-by: Charles Dusek <[email protected]>
Co-authored-by: xaeon2026 <[email protected]>
Co-authored-by: xaeon2026 <[email protected]>
Co-authored-by: Robin Waslander <[email protected]>
Co-authored-by: Pejman Pour-Moezzi <[email protected]>
Co-authored-by: Pejman Pour-Moezzi <[email protected]>
Co-authored-by: Onur <[email protected]>
Co-authored-by: zerone0x <[email protected]>
Co-authored-by: zerone0x <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: matrix Channel integration: matrix size: L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Matrix] 2-person group rooms misrouted as DMs due to memberCount heuristic

9 participants