Skip to content

Agent chat: inject Claude/Codex hook config at launch (live ingest goes live) - #5964

Closed
lawrencecchen wants to merge 10 commits into
feat-agentconv-codex-hooksfrom
feat-agchat-hooks-launch
Closed

lawrencecchen wants to merge 10 commits into
feat-agentconv-codex-hooksfrom
feat-agchat-hooks-launch

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jun 12, 2026 •

Copy link
Copy Markdown
Contributor

Launch side of live hook ingest for the agent conversation protocol (#5736). The daemon side (ingest socket, agent-hook-emit, Claude payload translation; Codex notify translation on #5957) was already in place, but nothing configured agents to emit, so turns, permission banners, and low-latency tool rows never flowed in real usage.

How it works

App (Swift). New Sources/AgentChat/AgentHookLaunchEnvironment.swift computes this instance's ingest socket path and exports two managed env vars into every terminal surface: CMUX_AGENT_HOOK_EMIT_BIN (staged cmuxd-remote from the checksum-verified remote-daemons cache, via AgentDaemonBinaryLocator) and CMUX_AGENT_HOOK_SOCKET. The chat surface's spawned cmuxd-remote serve --stdio child gets the same socket path pinned into its environment, so listener and emitters always agree.

Socket path scheme. Stable release: the documented default /tmp/cmuxd-agentconv-<uid>/ingest.sock. Every other variant is scoped using the control socket's bundle-id classification: /tmp/cmuxd-agentconv-<uid>-debug-<tag>/ingest.sock for tagged dev builds, -debug, -nightly[-slug], -staging[-slug] likewise, so tagged builds never cross-talk with the user's stable daemon. An explicit CMUX_AGENT_HOOK_SOCKET in the app environment overrides.

Claude. Resources/bin/cmux-claude-wrapper (the existing per-launch --settings owner) merges one agent-hook-emit entry per event (UserPromptSubmit, PreToolUse, PostToolUse, Stop, Notification, PermissionRequest, timeout 5) into the settings payload it already injects. Commands use the wrapper's \"$ENV_VAR\" hook-shell-expansion convention, so DerivedData paths with spaces need no quoting games. PreToolUse/PostToolUse are async so tool calls gain zero latency; the daemon merge layer tolerates reordering.

--settings collision handling (verified, not assumed). Claude's --settings is last-wins across repeated flags, not cumulative: on 2.1.175, claude --settings '{invalid' --settings '{}' -p hi succeeds while the reversed order fails on the invalid value. So a second --settings would clobber cmux's existing hooks payload, and the merge must happen inside the single value the wrapper owns. A user-supplied --settings passes through untouched and (being last) wins over cmux's; user settings are never clobbered, cmux's injection is lost for that launch, matching the wrapper's longstanding semantics. Injection is inline per exec, so resume/fork relaunches are idempotent with no settings files to manage.

Codex. New Resources/bin/cmux-codex-wrapper (registered in zsh/bash/fish shell integration like the Claude wrapper) injects codex -c notify=["<emit>","agent-hook-emit","--socket","<sock>","--provider","codex"] per launch. It never writes ~/.codex/config.toml and skips injection when the user already has a notifier (their own -c notify or an uncommented notify key in config.toml; cmux's persistent Codex integration never sets one). On this branch codex frames are accepted-and-dropped by the emit verb (exit 0); they start flowing when #5957 lands (argv verified against that branch's binary).

Safety gate (important). Every released cmuxd-remote predates the verb, and an old binary invoked as agent-hook-emit falls through to its CLI dispatch (reads CMUX_SOCKET_PATH, connects to the app control socket, exits non-zero), which would stall or garble every Claude hook. Injection therefore requires provenance that provably carries the verb: the explicit CMUX_REMOTE_DAEMON_BINARY dev override on any build, or, on release builds, a cached daemon at the app's exact release version (same-SHA artifacts) or newer. Debug builds inject only with the override, which dev dogfood of this feature already requires. No binary, or an old one: neither env var is set and both wrappers skip injection entirely; agent launches never depend on the feature.

Tests

  • cmuxTests/AgentHookLaunchEnvironmentTests.swift (Swift Testing, wired into project.pbxproj, lint-pbxproj-test-wiring clean): socket path scheme (stable default, tagged/untagged debug, CMUX_TAG, nightly/staging, env override), skip-when-no-binary, provenance/version gating, daemon-child socket pinning.
  • tests/test_claude_wrapper_hooks.py (extended; in ci.yml): emit entries merged with exact command/timeout/async shape, settings stay clean without the env or with a non-executable emit binary, exactly one --settings, user --settings passes through unmodified and last.
  • tests/test_codex_wrapper_notify.py (new; added to ci.yml): notify argv shape and placement before the subcommand, passthrough outside cmux / disabled / missing env or binary, user CLI and config.toml notify respected, commented notify does not block, --help untouched.
  • E2E smoke (run locally): the exact wrapper-injected command piped a fake Claude PreToolUse payload through cmuxd-remote agent-hook-emit --socket <tmp> to a listening socket from a path containing a space and produced the expected frame; the codex notify argv produced the expected Stop frame against a binary built from feat-agentconv-codex-hooks; emit with no listener exits 0 instantly.

Gates: xcodebuild ... -derivedDataPath /tmp/cmux-agchat-h build green (build-only); wrapper python suites green; normalize-pbxproj/check-pbxproj/lint-pbxproj-test-wiring clean. No user-facing strings added (env and CLI plumbing only), so no localization changes.

🤖 Generated with Claude Code


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Summary by cubic

Injects live agent hook config at launch so Claude and codex emit real-time frames into the chat surface, enabling turn events, permission banners, and low‑latency tool rows. Adds safe gating, variant‑scoped sockets, and session‑cached relay resolution while preserving cross‑instance isolation and user shell aliases.

  • New Features

    • App exports CMUX_AGENT_HOOK_EMIT_BIN (staged cmuxd-remote) and CMUX_AGENT_HOOK_SOCKET into terminals and pins the same socket in the spawned cmuxd-remote serve --stdio child; relay lookup is cached per app session with a 30s re‑probe on misses.
    • Variant‑scoped sockets: stable uses /tmp/cmuxd-agentconv-<uid>/ingest.sock; dev/nightly/staging add suffixes. A CMUX_AGENT_HOOK_SOCKET override is ignored when inherited from another cmux instance’s terminal (checked via CMUX_BUNDLE_ID).
    • Injection is gated: explicit dev override on any build, or (stable releases) a cached daemon at the app’s exact/newer version; debug/nightly/staging require the override. Missing/old binaries skip injection.
    • cmux-claude-wrapper merges one agent-hook-emit entry per event into its single --settings JSON (UserPromptSubmit, PreToolUse/PostToolUse async, Stop, Notification, PermissionRequest). A user --settings passes through and wins.
    • New cmux-codex-wrapper injects -c notify=["<emit>","agent-hook-emit","--socket","<sock>","--provider","codex"] per launch; never writes ~/.codex/config.toml, respects a user notifier (CLI or config), and skips outside cmux or when CMUX_CODEX_HOOKS_DISABLED=1. Shell integration installs the wrapper for bash/zsh/fish.
  • Bug Fixes

    • zsh integration preserves user aliases/functions for claude, grok, and codex and installs wrappers only when absent.
    • zsh: the whence guard no longer trips errexit when the command is missing.

Written for commit 55bf03b. Summary will update on new commits.

Review in cubic


Note

Medium Risk
Touches every in-cmux Claude/Codex launch and gates on daemon version provenance; mis-gating or socket isolation bugs could drop live updates or, with an old relay, stall Claude hooks—mitigated by skip-when-unsafe and extensive tests.

Overview
Wires the launch side of agent conversation hook ingest so Claude and Codex can push live frames into the chat surface via cmuxd-remote agent-hook-emit.

App (Swift). New AgentHookLaunchEnvironment derives a variant-scoped ingest socket path and, when safe, exports CMUX_AGENT_HOOK_EMIT_BIN and CMUX_AGENT_HOOK_SOCKET into terminal surfaces (session-cached relay lookup with 30s re-probe on miss). AgentDaemonBinaryLocator now returns provenance so injection is gated: explicit dev override, or stable release with a cached daemon at the same/newer version—skipping older binaries that would break Claude hooks. The chat daemon child gets the same socket via AgentDaemonClient + daemonChildEnvironment().

Wrappers & shells. cmux-claude-wrapper merges extra agent-hook-emit hook entries into its single --settings JSON when env is set (PreToolUse/PostToolUse async). New cmux-codex-wrapper adds per-launch -c notify=[...] without touching config.toml, respecting user notify overrides. Bash/zsh/fish install the Codex wrapper; zsh now skips wrapping when the user already has an alias/function.

Tests & docs. Swift and Python regression tests plus CI for Codex wrapper; docs/agent-conversation-protocol.md updated for the Swift + wrapper split.

Reviewed by Cursor Bugbot for commit 55bf03b. Bugbot is set up for automated code reviews on this repo. Configure here.

lawrencecchen and others added 2 commits June 12, 2026 01:16
Launch side of live hook ingest (docs/agent-conversation-protocol.md):

- AgentHookLaunchEnvironment computes the per-instance ingest socket path
  (tag/variant-scoped for dev, nightly, staging builds) and exports
  CMUX_AGENT_HOOK_EMIT_BIN + CMUX_AGENT_HOOK_SOCKET into terminal surfaces;
  the chat surface's spawned cmuxd-remote child gets the same socket pinned
  so listener and emitters agree.
- cmux-claude-wrapper merges one agent-hook-emit entry per hook event into
  the single --settings payload it already owns (claude's --settings is
  last-wins, verified on 2.1.175, so a second flag would clobber). Tool
  hooks are async; a user-supplied --settings passes through untouched.
- New cmux-codex-wrapper injects 'codex -c notify=[...]' per launch, never
  writing the user's config.toml and never overriding a user-configured
  notifier (CLI override or config.toml notify key).
- Injection requires an emit binary that provably carries the verb
  (explicit dev override, or exact/newer cached release version): an old
  cached daemon invoked with the unknown verb falls into its CLI dispatch
  and would fail every Claude hook.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AgentHookLaunchEnvironment.swift had been wired with AgentChatPanel.swift's
object ids, silently dropping the panel from the build; slug sanitization
lives on SocketPathMarkerFiles, not SocketControlSettings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 12, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Jun 12, 2026 10:28pm
cmux-staging Building Building Preview, Comment Jun 12, 2026 10:28pm

@coderabbitai

coderabbitai Bot commented Jun 12, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 619f570a-b6be-4acf-8db4-34504a5a97bf

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-agchat-hooks-launch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

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

Copy link
Copy Markdown

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: f21ccd1517

ℹ️ 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 +48 to +50
if let override = environment[socketEnvKey]?.trimmingCharacters(in: .whitespacesAndNewlines),
!override.isEmpty {
return override

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject inherited hook sockets from other cmux instances

When a cmux app is launched from an existing cmux terminal, that shell now inherits CMUX_AGENT_HOOK_SOCKET from the parent surface, so this unconditional override wins before the bundle/tag classification below. In that nested-launch scenario a tagged debug, nightly, or staging build will reuse the parent instance's ingest socket instead of its own variant-scoped path, letting its daemon and agent hooks feed another cmux instance and defeating the isolation this change is trying to add. Please distinguish deliberate operator overrides from inherited cmux terminal env (or validate it against this bundle variant) before returning it here.

Useful? React with 👍 / 👎.

@greptile-apps

greptile-apps Bot commented Jun 12, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR wires the launch side of the agent conversation hook ingest: a new AgentHookLaunchEnvironment type computes variant-scoped socket paths and injects CMUX_AGENT_HOOK_EMIT_BIN/CMUX_AGENT_HOOK_SOCKET into every terminal surface, while the Claude and Codex wrappers read those vars to add per-launch hook entries pointing at the staged cmuxd-remote relay. A strict provenance gate ensures only same-version-or-newer cached daemons are used, preventing old binaries from garbling Claude's hook dispatch.

  • New AgentHookLaunchEnvironment.swift derives tag-scoped ingest socket paths (stable, debug, nightly, staging variants) and caches the relay lookup for the app session (30s miss re-probe), with a CMUX_BUNDLE_ID guard that prevents cross-instance socket inheritance when cmux is launched from inside another cmux terminal.
  • cmux-claude-wrapper merges agent-hook-emit entries into its single --settings payload for UserPromptSubmit, PreToolUse/PostToolUse (async), Stop, Notification, and PermissionRequest; a second --settings flag is never added.
  • New cmux-codex-wrapper injects -c notify=[...] per launch without touching ~/.codex/config.toml, respects existing user notifiers (CLI or config), and is registered in bash/zsh/fish shell integration alongside the claude wrapper.

Confidence Score: 4/5

Safe to merge with one ordering question in the PermissionRequest hook configuration worth confirming.

The provenance gate, socket-path isolation, and passthrough-when-missing semantics are carefully designed and well-tested. The one open question is whether cmux hooks feed --source claude for PermissionRequest is blocking (waiting for user grant/deny before exiting) or fires-and-returns. If it blocks, the sync agent-hook-emit entry appended after it arrives too late to render a pre-decision banner in the chat surface. All other event hooks are ordered correctly (tool hooks are async, Stop/Notification use short-timeout sync feeds first). Confirming or fixing the PermissionRequest ordering is the only thing standing between this and a clean merge.

Resources/bin/cmux-claude-wrapper — the PermissionRequest hook group ordering needs a second look depending on whether hooks feed is blocking for that event.

Important Files Changed

Filename Overview
Resources/bin/cmux-claude-wrapper Merges agent-hook-emit entries into the single --settings payload for 6 events; PermissionRequest ordering places the sync emit after the 125s blocking feed hook, which may fire too late for a pre-decision banner.
Resources/bin/cmux-codex-wrapper New wrapper injects per-launch -c notify=[...] override; correctly gates on CMUX_SURFACE_ID, executable emit binary, and absence of user notifier (CLI and config.toml); TOML/JSON escape check skips injection for problematic paths.
Sources/AgentChat/AgentHookLaunchEnvironment.swift New type computes tag-scoped socket paths and provenance-gated emit binary URL; injectableEmitBinaryURLForLaunch calls the disk-scanning locator synchronously on @MainActor (already noted in a previous thread); the 30s miss-probe cache prevents per-surface rescans.
Sources/AgentChat/AgentDaemonClient.swift Adds optional environment parameter to pin CMUX_AGENT_HOOK_SOCKET in the daemon child; defaults to nil (inherit), so existing callers are unaffected.
Sources/GhosttyTerminalView.swift Calls injectableEmitBinaryURLForLaunch() on the main-actor surface-creation path; caching prevents per-surface rescans, but a cold miss still calls AgentDaemonBinaryLocator().locate() synchronously on the main actor (flagged in a previous thread).
Resources/shell-integration/cmux-zsh-integration.zsh Adds codex wrapper installation and fixes existing behavior to preserve user aliases/functions for claude, grok, and codex; `whence -w ...
tests/test_codex_wrapper_notify.py New test suite covers inject shape, passthrough conditions, user notifier precedence (CLI and config.toml), and help passthrough; DerivedData space path is tested.
cmuxTests/AgentHookLaunchEnvironmentTests.swift Thorough coverage of socket path scoping (stable, debug, tagged, nightly, staging), cross-instance override isolation, provenance gating, and daemon child environment pinning.

Reviews (4): Last reviewed commit: "Tests: guard the app module import for D..." | Re-trigger Greptile

Comment thread Sources/GhosttyTerminalView.swift Outdated
Comment on lines +6611 to +6621
if let emitBinaryURL = AgentHookLaunchEnvironment.injectableEmitBinaryURL(
outcome: AgentDaemonBinaryLocator().locate()
),
let agentHookEnvironment = AgentHookLaunchEnvironment.launchEnvironment(
emitBinaryURL: emitBinaryURL,
socketPath: AgentHookLaunchEnvironment.ingestSocketPath()
) {
for (key, value) in agentHookEnvironment {
setManagedEnvironmentValue(key, value)
}
}

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 Synchronous disk I/O on the main-actor surface-creation path

AgentDaemonBinaryLocator().locate() is called synchronously inside createSurface, which is called from a @MainActor context (visible from the @MainActor-annotated claudeCommandShimStateForSurface and the surrounding call sites). When the exact-version binary isn't cached, locate() falls through to newestCachedBinary(), which calls fileManager.contentsOfDirectory(atPath:) — a blocking POSIX syscall — on every terminal surface creation. On a cloud-synced home directory, NFS mount, or when the daemon cache root doesn't exist yet, this stalls the main thread and blocks terminal rendering.

The comparable call in AgentChatWebViewController.swift correctly uses await Self.locateDaemonBinary(). The same treatment belongs here: the locator result should be resolved off-main and the emit-binary URL stored (e.g., computed once and cached in TerminalSurface, similar to how claudeCommandShim uses Task.detached(priority: .utility) for installation work) so that createSurface only reads a pre-resolved value.

return "\(base)-\(variant)-\(slug)"
}

@usableFromInline static var isDebugBuild: Bool {

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 @usableFromInline is meaningful only for library modules where inlined callers in other modules need access to an internal symbol. AgentHookLaunchEnvironment lives in the cmux app target, which has no external inlining callers, so the annotation is a no-op here. It signals library semantics where none exist and will confuse future maintainers.

Suggested change
@usableFromInline static var isDebugBuild: Bool {
static var isDebugBuild: Bool {

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment thread Sources/GhosttyTerminalView.swift Outdated
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread Sources/AgentChat/AgentHookLaunchEnvironment.swift
…sion

Nightly/staging release builds share marketing versions with stable
artifacts built from different SHAs, so the exact-version check proves
nothing there; cached binaries are now injectable only on stable release
builds, everything else uses the explicit dev override. Terminal surface
creation also no longer rescans the daemon cache per surface: the relay is
resolved once per app session.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…orever

A found relay stays cached for the session; a miss is retried at most
every 30 seconds so the first cmux ssh enables injection for new
terminals without an app restart, while a workspace-restore burst still
costs a single cache scan.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
lawrencecchen and others added 2 commits June 12, 2026 13:06
… feat-agchat-hooks-launch

# Conflicts:
#	docs/agent-conversation-protocol.md
CMUX_AGENT_HOOK_SOCKET is exported into every cmux terminal for that
pane's hooks, so a cmux app launched from inside another instance's
terminal inherited that instance's socket and lost tagged/nightly/
staging isolation (the bind would fail; wrappers would emit to the
wrong app). The override now counts only when the ambient
CMUX_BUNDLE_ID is absent or matches this app's bundle id.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@lawrencecchen
lawrencecchen changed the base branch from feat-agent-normalized-layer to feat-agentconv-codex-hooks June 12, 2026 20:07

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 7b6bf08. Configure here.

Comment thread Sources/AgentChat/AgentHookLaunchEnvironment.swift
The zsh wrapper installer unconditionally unaliased and redefined the
command, unlike bash and fish which yield to a user-defined alias or
function (binary selection, env injection, default args). Mirror the
guard via whence -w; verified behaviorally in a clean zsh (user alias
preserved, wrapper installed when absent).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…is absent

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

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

Copy link
Copy Markdown

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: 50a899bf03

ℹ️ 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 +58 to +60
if ambientBundleID == nil || ambientBundleID?.isEmpty == true
|| ambientBundleID == bundleIdentifier {
return override

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Compare tags before accepting inherited hook sockets

For base debug builds whose bundle id stays com.cmuxterm.app.debug, changing only CMUX_TAG is enough to create a separate tagged instance (for example the test harnesses launch CMUX_TAG=tests-v1 .../cmux DEV). If that launch happens from another base-debug cmux terminal, the inherited CMUX_AGENT_HOOK_SOCKET is accepted here because the bundle ids match, even though the tags differ, so the new instance emits to and pins its daemon child to the parent tag's ingest socket instead of its own /tmp/cmuxd-agentconv-<uid>-debug-<tag>/ingest.sock. Please also validate the inherited/current CMUX_TAG (or otherwise reject ambient cmux terminal env) before returning the override.

Useful? React with 👍 / 👎.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@lawrencecchen lawrencecchen added the stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening. label Sep 23, 2026
@github-project-automation github-project-automation Bot moved this from Todo to Done in cmux backlog Sep 23, 2026

This branch was successfully deployed

1 active deployment
Preview – cmux — 55bf03b1 Deployed Jun 12, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants