Skip to content

iMessage: harden echo detection against control-char prefixes#59386

Closed
Jeevan-ZGDX wants to merge 3 commits into
openclaw:mainfrom
Jeevan-ZGDX:fix-imessage-echo-loop
Closed

iMessage: harden echo detection against control-char prefixes#59386
Jeevan-ZGDX wants to merge 3 commits into
openclaw:mainfrom
Jeevan-ZGDX:fix-imessage-echo-loop

Conversation

@Jeevan-ZGDX

Copy link
Copy Markdown
  • Strip leading/trailing control characters from echo text normalization
  • Add test for control char stripping in echo cache
  • Fixes issue with reflected payloads containing \u0000 etc.

Summary

Problem: iMessage assistant outbound messages were being reflected back as inbound user messages, causing infinite echo loops in direct chats.
Root Cause: Echo detection failed to match reflected messages containing leading control characters (e.g., \u0000) prefixed to the original text.
Fix: Enhanced text normalization in echo cache to strip leading/trailing control characters, ensuring detection works even with garbled reflections.
Impact: Prevents feedback loops without affecting normal conversation flow.

  • Problem:
    -Why it matters: Echo loops waste API tokens, cause agent confusion, and disrupt user workflows by generating unwanted responses.
    -What changed: Modified normalizeEchoTextKey in echo-cache.ts to remove control characters (\u0000-\u001F, \u007F-\u009F) from text before comparison. Added unit test for control character stripping.
    -What did NOT change (scope boundary): No changes to message routing logic, permission checks, or other channels. Echo detection remains scoped to iMessage only.

Change Type (select all)

  • [ . ] Bug fix
  • [ . ] Security hardening

Scope (select all touched areas)

  • [ . ] Integrations

Linked Issue/PR

Root Cause / Regression History (if applicable)

Root cause: Echo cache normalization did not account for control characters added by iMessage reflection, causing text mismatch and allowing echoes to pass through.

Missing detection / guardrail: No stripping of non-whitespace control characters in text normalization.

Prior context: Issue started with version 2026.3.31, likely due to changes in iMessage handling or reflection behavior.

Why this regressed now: Unknown, but control-char prefixes in reflections were not anticipated in original echo logic.

What was ruled out: Not a change in is_from_me detection (code already drops all self-messages); confirmed as echo detection gap.

Regression Test Plan (if applicable)

  • Coverage level that should have caught this: Unit test
  • Target test or file: monitor-provider.echo-cache.test.ts
  • Scenario the test should lock in: Echo detection must match text even with leading/trailing control characters.
  • Why this is the smallest reliable guardrail: Unit test directly validates the normalization logic without requiring full iMessage setup.
  • Existing test that already covers this (if any): N/A
  • If no new test is added, why not: Test was added in this PR.

User-visible / Behavior Changes

  • None. This is an internal fix to echo detection; user conversations remain unchanged.

Diagram (if applicable)

N/A

Security Impact (required)

  • New permissions/capabilities? (No)
  • Secrets/tokens handling changed? (No)
  • New/changed network calls? (No)
  • Command/tool execution surface changed? (No)
  • Data access scope changed? (No)

Repro + Verification

Environment

  • OS: macOS Tahoe 26.3
  • Runtime/container: npm global install under Homebrew Node
  • Model/provider: google/gemini-3.1-flash-lite-preview, openrouter/qwen/qwen3.6-plus-preview:free
  • Integration/channel (if any): iMessage (Apple Messages)
  • Relevant config (redacted): iMessage channel enabled

Steps

  1. Send message to agent in iMessage direct chat
  2. Agent responds
  3. Agent receives its own response back as user input (with control chars)
  4. Loop continues until manually stopped

Expected : Agent does not see its own messages; smooth conversation.

Actual : Echo loop with agent replying to self.

Evidence : Session logs show self-addressed routes and control-char prefixed reflections. Local hotfix (force-drop is_from_me) resolved it.

Human Verification (required)

What you personally verified (not just CI), and how:

Verified scenarios:

Code compiles without errors.
Unit tests pass for echo cache normalization.
Logic review confirms control char stripping matches issue description.

Edge cases checked:

Empty strings, strings with only control chars, mixed content.

What you did not verify:

Full iMessage integration test (no macOS/iMessage environment available).
End-to-end echo loop reproduction.

Review Conversations

N/A (new PR)

Compatibility / Migration

  • Backward compatible? (Yes)
  • Config/env changes? (No)
  • Migration needed? (No)
  • If yes, exact upgrade steps:

Risks and Mitigations

None. This is a targeted fix to text normalization with no side effects on other logic.

- Strip leading/trailing control characters from echo text normalization
- Add test for control char stripping in echo cache
- Fixes issue with reflected payloads containing \u0000 etc.
@openclaw-barnacle openclaw-barnacle Bot added app: web-ui App: web-ui gateway Gateway runtime cli CLI command changes size: M labels Apr 2, 2026
@greptile-apps

greptile-apps Bot commented Apr 2, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR bundles two distinct areas of work: (1) the stated bug fix — hardening iMessage echo detection against control-character prefixes in reflected messages — and (2) a sizeable security hardening layer added to the gateway (CORS enforcement, WS/SSE connection caps, idle timeouts, a new SSE events endpoint, and localhost-default bindings for the Vite dev server).

Key changes:

  • echo-cache.ts: strips leading/trailing control characters (\u0000\u001F, \u007F\u009F) before text comparison; unit test added and correct.
  • ui/vite.config.ts: dev server now defaults to 127.0.0.1 instead of 0.0.0.0 — a clear security improvement.
  • ws-connection.ts: global + per-IP WS connection caps and idle timeout; resource accounting is balanced.
  • server-http.ts: origin-based CORS with Vary: Origin; correctly echoes the specific origin rather than using a wildcard with credentials.
  • events-http.ts (new file): introduces /api/events/stream (SSE) and /api/events (POST broadcast) with rate limiting — but no authentication. Every other sensitive handler in server-http.ts enforces gateway auth internally; the events handler does not. Any unauthenticated network client can subscribe to all events or inject arbitrary payloads into every connected subscriber's stream.
  • rateLimitTimestamps in events-http.ts accumulates map entries for IPs with no recent requests (stored back as empty arrays), causing unbounded map growth under high source-IP churn.

Confidence Score: 2/5

  • Not safe to merge until authentication is added to the new SSE event endpoints.
  • The core echo-cache fix is correct and well-tested. However, the new events-http.ts module introduces two HTTP endpoints with no authentication gate — any unauthenticated caller can subscribe to all SSE events or broadcast arbitrary payloads to all connected clients. This contradicts the security hardening intent of the PR and is especially risky because the same PR documents how to expose the gateway publicly. A secondary issue (rateLimitTimestamps memory growth) is lower severity but still worth addressing. The score reflects that the primary stated bug fix is sound, but new unauthenticated surface area was introduced alongside it.
  • src/gateway/server/events-http.ts requires authentication on both the GET /api/events/stream and POST /api/events endpoints before this PR is safe to merge.

Comments Outside Diff (1)

  1. src/gateway/server/ws-connection.ts, line 190-216 (link)

    P2 Per-IP limit checked after connect challenge is already sent

    The connect.challenge frame is sent to the socket at line ~191 before the per-IP count is checked. A client that exceeds maxPerIp therefore receives a challenge, is then immediately closed with 1013, and the challenge is wasted. While not a correctness bug (the connection is correctly rejected), it can be confusing to clients and leaks that the server uses a challenge-response handshake to over-limit IPs.

    Consider moving the per-IP check (and the maxConnections check) to the very top of the connection handler, before any data is sent to the socket.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: src/gateway/server/ws-connection.ts
    Line: 190-216
    
    Comment:
    **Per-IP limit checked after connect challenge is already sent**
    
    The `connect.challenge` frame is sent to the socket at line ~191 before the per-IP count is checked. A client that exceeds `maxPerIp` therefore receives a challenge, is then immediately closed with 1013, and the challenge is wasted. While not a correctness bug (the connection is correctly rejected), it can be confusing to clients and leaks that the server uses a challenge-response handshake to over-limit IPs.
    
    Consider moving the per-IP check (and the `maxConnections` check) to the very top of the `connection` handler, before any data is sent to the socket.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/gateway/server/events-http.ts
Line: 57-59

Comment:
**Module-level `rateLimitTimestamps` map is never pruned for inactive IPs**

`rateLimitTimestamps` is a module-level `Map` that accumulates one entry per unique client IP. On every call to `checkRateLimit`, the per-IP array is filtered down to recent timestamps and stored back:

```ts
rateLimitTimestamps.set(ip, recent);
```

However, when all timestamps for an IP fall outside the window, `recent` becomes an empty array and the entry is **stored back as `[]`** rather than deleted. For any IP that makes even a single POST and then goes silent, a `[]` entry remains in the map forever. In a public deployment with many different source IPs (or a scanner sweep), this is an unbounded memory leak.

After the `filter`, delete the entry when `recent` is empty instead of setting it back:

```ts
if (recent.length === 0) {
  rateLimitTimestamps.delete(ip);
} else {
  rateLimitTimestamps.set(ip, recent);
}
```

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

---

This is a comment left during a code review.
Path: src/gateway/server/ws-connection.ts
Line: 190-216

Comment:
**Per-IP limit checked after connect challenge is already sent**

The `connect.challenge` frame is sent to the socket at line ~191 before the per-IP count is checked. A client that exceeds `maxPerIp` therefore receives a challenge, is then immediately closed with 1013, and the challenge is wasted. While not a correctness bug (the connection is correctly rejected), it can be confusing to clients and leaks that the server uses a challenge-response handshake to over-limit IPs.

Consider moving the per-IP check (and the `maxConnections` check) to the very top of the `connection` handler, before any data is sent to the socket.

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

---

This is a comment left during a code review.
Path: src/gateway/server/events-http.ts
Line: 170-205

Comment:
**Event endpoints lack authentication**

`handleEventsHttpRequest` performs no credential check. Every other handler in `server-http.ts` that exposes sensitive functionality receives a resolved-auth parameter and enforces it internally (e.g. `handleToolsInvokeHttpRequest`), but the events handler is invoked without one.

As a result, any unauthenticated client that can reach the gateway can:
- Open a persistent SSE connection on `/api/events/stream` and receive all events.
- POST arbitrary JSON to `/api/events`, which the `broadcast()` function forwards verbatim to every connected SSE subscriber.

The second point is particularly impactful: any network-reachable caller can inject crafted events into the UI of every connected user. This matters most when the gateway is configured for non-loopback exposure (which this PR explicitly documents).

The handler should accept and enforce the gateway auth configuration the same way the other route handlers do.

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

Reviews (1): Last reviewed commit: "iMessage: harden echo detection against ..." | Re-trigger Greptile

Comment on lines +57 to +59
const sseClients = new Set<SseClient>();
const perIpConnections = new Map<string, number>();
const rateLimitTimestamps = new Map<string, number[]>();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Module-level rateLimitTimestamps map is never pruned for inactive IPs

rateLimitTimestamps is a module-level Map that accumulates one entry per unique client IP. On every call to checkRateLimit, the per-IP array is filtered down to recent timestamps and stored back:

rateLimitTimestamps.set(ip, recent);

However, when all timestamps for an IP fall outside the window, recent becomes an empty array and the entry is stored back as [] rather than deleted. For any IP that makes even a single POST and then goes silent, a [] entry remains in the map forever. In a public deployment with many different source IPs (or a scanner sweep), this is an unbounded memory leak.

After the filter, delete the entry when recent is empty instead of setting it back:

if (recent.length === 0) {
  rateLimitTimestamps.delete(ip);
} else {
  rateLimitTimestamps.set(ip, recent);
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/gateway/server/events-http.ts
Line: 57-59

Comment:
**Module-level `rateLimitTimestamps` map is never pruned for inactive IPs**

`rateLimitTimestamps` is a module-level `Map` that accumulates one entry per unique client IP. On every call to `checkRateLimit`, the per-IP array is filtered down to recent timestamps and stored back:

```ts
rateLimitTimestamps.set(ip, recent);
```

However, when all timestamps for an IP fall outside the window, `recent` becomes an empty array and the entry is **stored back as `[]`** rather than deleted. For any IP that makes even a single POST and then goes silent, a `[]` entry remains in the map forever. In a public deployment with many different source IPs (or a scanner sweep), this is an unbounded memory leak.

After the `filter`, delete the entry when `recent` is empty instead of setting it back:

```ts
if (recent.length === 0) {
  rateLimitTimestamps.delete(ip);
} else {
  rateLimitTimestamps.set(ip, recent);
}
```

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

Comment on lines +170 to +205
export async function handleEventsHttpRequest(
req: IncomingMessage,
res: ServerResponse,
opts: EventsHttpOptions = {},
): Promise<boolean> {
const url = new URL(req.url ?? "/", `http://${req.headers.host || "localhost"}`);
const pathname = url.pathname;

if (pathname === EVENTS_POST_PATH) {
if (req.method !== "POST") {
sendMethodNotAllowed(res, "POST");
return true;
}
const ip = resolveIp(req, opts);
const limit = checkRateLimit(ip);
if (!limit.allowed) {
if (limit.retryAfter && limit.retryAfter > 0) {
res.setHeader("Retry-After", String(limit.retryAfter));
}
sendJson(res, 429, { error: { message: "Too Many Requests", type: "rate_limited" } });
return true;
}
const body = await readJsonBody(req, EVENT_BODY_MAX_BYTES);
if (!body.ok) {
const status =
body.error === "payload too large"
? 413
: body.error === "request body timeout"
? 408
: 400;
sendJson(res, status, { ok: false, error: body.error });
return true;
}
broadcast("event", body.value);
sendJson(res, 202, { ok: true });
return true;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Event endpoints lack authentication

handleEventsHttpRequest performs no credential check. Every other handler in server-http.ts that exposes sensitive functionality receives a resolved-auth parameter and enforces it internally (e.g. handleToolsInvokeHttpRequest), but the events handler is invoked without one.

As a result, any unauthenticated client that can reach the gateway can:

  • Open a persistent SSE connection on /api/events/stream and receive all events.
  • POST arbitrary JSON to /api/events, which the broadcast() function forwards verbatim to every connected SSE subscriber.

The second point is particularly impactful: any network-reachable caller can inject crafted events into the UI of every connected user. This matters most when the gateway is configured for non-loopback exposure (which this PR explicitly documents).

The handler should accept and enforce the gateway auth configuration the same way the other route handlers do.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/gateway/server/events-http.ts
Line: 170-205

Comment:
**Event endpoints lack authentication**

`handleEventsHttpRequest` performs no credential check. Every other handler in `server-http.ts` that exposes sensitive functionality receives a resolved-auth parameter and enforces it internally (e.g. `handleToolsInvokeHttpRequest`), but the events handler is invoked without one.

As a result, any unauthenticated client that can reach the gateway can:
- Open a persistent SSE connection on `/api/events/stream` and receive all events.
- POST arbitrary JSON to `/api/events`, which the `broadcast()` function forwards verbatim to every connected SSE subscriber.

The second point is particularly impactful: any network-reachable caller can inject crafted events into the UI of every connected user. This matters most when the gateway is configured for non-loopback exposure (which this PR explicitly documents).

The handler should accept and enforce the gateway auth configuration the same way the other route handlers do.

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1a02b3dd50

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

const perIpCounts = new Map<string, number>();

wss.on("connection", (socket, upgradeReq) => {
if (clients.size >= maxConnections) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Badge Count pending sockets in global WebSocket connection limit

Use wss.clients.size (or another socket-level counter) for the global cap instead of clients.size. clients is only populated after a successful connect handshake (via setClient), so unauthenticated/pending sockets are not counted; an attacker can open many concurrent handshakes (especially across multiple IPs) and bypass MAX_WS_CONNECTIONS, defeating the overload protection this change intends to add.

Useful? React with 👍 / 👎.

Comment thread ui/vite.config.ts
host: true,
port: 5173,
host: (() => {
const raw = process.env.HOSTNAME?.trim();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Badge Avoid HOSTNAME as the default Vite bind source

Reading process.env.HOSTNAME here makes the fallback to loopback effectively unreachable in many real environments (Linux shells/containers usually export HOSTNAME automatically), so the dev server can bind to an unintended interface/hostname instead of localhost and may even fail to bind if that hostname is not a local listen address. This regresses the stated localhost-by-default behavior and can break local development unexpectedly.

Useful? React with 👍 / 👎.

const raw =
process.env.OPENCLAW_CORS_ALLOWED_ORIGINS?.trim() ||
process.env.CORS_ALLOWED_ORIGINS?.trim() ||
"http://localhost:18790";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Badge Align default CORS origin with the default control UI URL

The default CORS allowlist is hardcoded to http://localhost:18790, but the new Vite default host path serves from 127.0.0.1:18790 when no explicit host override is provided. When the UI is opened at that default URL, browser requests with Origin: http://127.0.0.1:18790 will not receive Access-Control-Allow-Origin, so cross-origin API calls are blocked despite both sides using defaults.

Useful? React with 👍 / 👎.

@fagandesign

Copy link
Copy Markdown

#59363

@openclaw-barnacle openclaw-barnacle Bot added channel: imessage Channel integration: imessage size: XL and removed size: M labels Apr 2, 2026
@Regg819

This comment was marked as spam.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 682fbebaf2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +182 to +190
const requestAuth = await authorizeGatewayHttpRequestOrReply({
req,
res,
auth: opts.auth,
trustedProxies: opts.trustedProxies,
allowRealIpFallback: opts.allowRealIpFallback,
});
if (!requestAuth) {
return true;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Badge Check /api/events path before enforcing auth

handleEventsHttpRequest authenticates the request before it checks whether the path is /api/events or /api/events/stream, and this handler is invoked for every HTTP request from createGatewayHttpServer; when gateway auth is enabled, unrelated unauthenticated routes (for example Control UI/static routes or public plugin routes) will be short-circuited with a 401 here instead of falling through to their own handlers.

Useful? React with 👍 / 👎.

Comment on lines 620 to 622
res.statusCode = 404;
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.end("Not Found");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Badge Restore gateway probe routes before 404 fallback

The request pipeline no longer dispatches /health, /healthz, /ready, or /readyz, so those probe URLs now fall through to the generic 404 response; this breaks liveness/readiness checks that previously succeeded and are used by deployment/monitoring paths.

Useful? React with 👍 / 👎.

Comment on lines +551 to +555
if (openResponsesEnabled) {
if (
await handleOpenResponsesHttpRequest(req, res, {
auth: resolvedAuth,
config: openResponsesConfig,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Badge Route /v1/models and /v1/embeddings in HTTP handler

The compat routing block now jumps directly to OpenResponses/chat-completions handling, with no call to the models or embeddings HTTP handlers, so /v1/models and /v1/embeddings requests will return 404 even when compat endpoints are enabled, breaking OpenAI-compatible clients that rely on those routes.

Useful? React with 👍 / 👎.

Comment on lines 328 to 332
const runId = dispatchAgentHook({
...normalized.value,
idempotencyKey,
sessionKey: normalizedDispatchSessionKey,
agentId: targetAgentId,
externalContentSource: "webhook",
sessionKey: sessionKey.value,
agentId: resolveHookTargetAgentId(hooksConfig, normalized.value.agentId),
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Badge Normalize hook session key to target agent namespace

When dispatching /hooks/agent, the code now forwards sessionKey.value directly; if a request targets one agent but supplies an agent:<other>:... session key, the run is scheduled under the mismatched agent-scoped namespace instead of being rebound to the target agent, which can mix isolation boundaries between agents.

Useful? React with 👍 / 👎.

Comment on lines 305 to 307
if (client?.connect?.role === "node") {
const context = buildRequestContext();
const nodeId = context.nodeRegistry.unregister(connId);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Badge Unsubscribe session listeners on every WS disconnect

Disconnect cleanup now performs registry teardown only for role === "node"; non-node clients that subscribed to session/session-message events are no longer removed on close, leaving stale connection IDs in subscriber registries and causing unbounded registry growth over time unless clients explicitly unsubscribe before disconnecting.

Useful? React with 👍 / 👎.

@clawsweeper

clawsweeper Bot commented Apr 26, 2026

Copy link
Copy Markdown
Contributor

Closing this as duplicate or superseded after Codex automated review.

Close #59386 as superseded. The linked echo-loop issue #59363 was later closed from main-branch self-chat fixes, and the remaining prefix-normalization work is now tracked by narrower open iMessage echo-cache PRs, especially #62191 for U+FFFD/C0/C1 control-prefix normalization and #63581 for NUL prefixes. This PR is not a good canonical vehicle because it bundles unrelated gateway/CORS/SSE/WebSocket changes and the submitted control-character regex is syntactically invalid as written.

Best possible solution:

Close #59386 as superseded and continue review on a focused iMessage echo-cache normalization PR, preferably #62191 or a maintainer-created successor that folds in the NUL and attributed-prefix coverage from #63581/#66169. Keep gateway hardening as separate reviewed work, not bundled with this echo fix.

What I checked:

So I’m closing this here and keeping the remaining discussion on the canonical linked item.

Codex Review notes: model gpt-5.5, reasoning high; reviewed against 4ad8b613c9b6.

@clawsweeper clawsweeper Bot closed this Apr 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app: web-ui App: web-ui channel: imessage Channel integration: imessage cli CLI command changes gateway Gateway runtime size: XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: imessage echo loop

3 participants