Skip to content

fix(subagent): resolve runtime model from subagent default instead of parent primary#72984

Open
joeykrug wants to merge 25 commits into
openclaw:mainfrom
joeykrug:fix/subagent-model-resolution
Open

fix(subagent): resolve runtime model from subagent default instead of parent primary#72984
joeykrug wants to merge 25 commits into
openclaw:mainfrom
joeykrug:fix/subagent-model-resolution

Conversation

@joeykrug

@joeykrug joeykrug commented Apr 27, 2026

Copy link
Copy Markdown
Contributor

Summary

Two-part fix for subagent model resolution:

  • Commit 1 — read-side fallback (defense in depth, fbbd3c2d5f): resolveSessionModelRef had no subagent awareness. When entry.model was empty (race between the spawn-side write and the gateway-side read at run-start), it fell through to resolveDefaultModelForAgent, returning the parent agent's primary model instead of the configured subagent default. Also reorders the ?? chain in resolveSubagentConfiguredModelSelection so global agents.defaults.subagents.model takes precedence over the spawning agent's own model field.
  • Commit 2 — Pi-harness write-side fix (production path, this commit): getReplyFromConfig resolved the run model via resolveDefaultModel (which goes through resolveDefaultModelForAgent and is not subagent-aware), and post-run persistence in updateSessionStoreAfterAgentRun then wrote that model back to entry.model, silently flipping every subagent run onto the parent agent's primary. The new resolveSubagentSessionDefaultModel helper performs a one-time override after sessionEntry is loaded so the Pi runtime boots on the configured subagent default and the post-run write preserves it.

Failure mode (before)

Config:

agents.list[main].model.primary = anthropic/claude-opus-4-7
agents.defaults.subagents.model.primary = openai-codex/gpt-5.5
agents.list[main].subagents.model.primary = openai-codex/gpt-5.5

Spawn flow:

  1. Parent calls sessions_spawn(task=..., agentId=main) with no explicit model:.
  2. Spawn-side computes the right ref (gpt-5.5) and writes entry.model / entry.modelProvider via persistInitialChildSessionRuntimeModel.
  3. The Pi runtime starts the run via getReplyFromConfig, which resolves defaultProvider/defaultModel via resolveDefaultModelForAgent — returning Opus (parent primary) — and uses that as the run's provider/model.
  4. Subagent runs on Opus instead of gpt-5.5.
  5. Post-run, updateSessionStoreAfterAgentRun → setSessionRuntimeModel writes Opus back to entry.model, clobbering the spawn-time gpt-5.5.

Result on disk: ~/.openclaw/agents/<agent>/sessions/sessions.json shows model: "claude-opus-4-7" for entries with subagentRole: "orchestrator", spawnDepth: 1, even though ~/.openclaw/subagents/runs.json correctly recorded gpt-5.5. The read-side fallback in commit 1 only kicks in while entry.model is empty — once the post-run write fires, entry.model is non-empty and the fallback is skipped.

Fix

  • Commit 1 — resolveSessionModelRef: when entry.spawnDepth >= 1 or entry.subagentRole is set, fall back to the configured subagent default for this agent before falling through to the agent's primary.
  • Commit 1 — resolveSubagentConfiguredModelSelection: reorder ?? chain to agentConfig.subagents.modelagents.defaults.subagents.modelagentConfig.model.
  • Commit 2 — resolveSubagentSessionDefaultModel: new helper in directive-handling.defaults.ts that, given the loaded sessionEntry, checks the same subagent shape (subagentRole set or spawnDepth >= 1) and resolves the configured subagent default. getReplyFromConfig invokes it once after sessionEntry is loaded and overrides defaultProvider/defaultModel/provider/model so the Pi runtime boots and post-run-persists on the right model.

Tests

  • Read-side fallback tests in src/gateway/session-utils.test.ts cover the runtime subagent fallback (with and without entry.spawnDepth).
  • Spawn-side chain-order tests cover the case where agentConfig.subagents.model is unset, agents.defaults.subagents.model is set, and agentConfig.model is set — we now return the global default instead of the agent's own primary.
  • New tests in src/auto-reply/reply/directive-handling.defaults.subagent.test.ts cover the Pi-harness write-side helper: subagent sessions resolve to the configured subagent default; non-subagent sessions return null so the parent's primary is preserved.
  • Existing get-reply test mocks updated to expose the new export.

Scope

Commit 1: src/gateway/session-utils.ts, src/agents/model-selection.ts, and their tests.
Commit 2: src/auto-reply/reply/directive-handling.defaults.ts, src/auto-reply/reply/get-reply.ts, and one new test plus two existing test-mock updates.
No drive-by changes.

Real behavior proof

  • Behavior or issue addressed: Spawned subagent runs persisted the parent agent's primary model (e.g. anthropic/claude-opus-4-7) into ~/.openclaw/agents/<agent>/sessions/sessions.json even though the configured subagent default was openai-codex/gpt-5.5 and the spawn-side resolver had already written the correct ref. Subsequent turns then ran on the wrong model.
  • Real environment tested: Local OpenClaw install on macOS, Node 22, single-agent config with agents.list[main].model.primary = anthropic/claude-opus-4-7, agents.defaults.subagents.model.primary = openai-codex/gpt-5.5, and agents.list[main].subagents.model.primary = openai-codex/gpt-5.5. Subagent spawned via sessions_spawn(task=..., agentId=main) from a parent Telegram chat with no explicit model: argument.
  • Exact steps or command run after this patch:
    1. pnpm openclaw doctor to confirm config parses cleanly.
    2. Start gateway: pnpm openclaw gateway start.
    3. From the active chat, ask the agent to spawn a subagent with no model override.
    4. While the subagent is running and again after it completes, inspect ~/.openclaw/agents/main/sessions/sessions.json and ~/.openclaw/subagents/runs.json.
  • Evidence after fix: Inspected ~/.openclaw/agents/main/sessions/sessions.json directly with jq — entries with subagentRole: "orchestrator" and spawnDepth: 1 now persist model: "gpt-5.5" and modelProvider: "openai-codex" instead of claude-opus-4-7. ~/.openclaw/subagents/runs.json continues to record gpt-5.5 as before, and the gateway run trace prints the openai-codex/gpt-5.5 provider/model pair when the run starts. Before the fix the same inspection printed model: "claude-opus-4-7" for those subagent entries even though the runs file recorded gpt-5.5.
  • Observed result after fix: Subagent runs boot on openai-codex/gpt-5.5 (visible in CLI run output and provider request logs), and the post-run write preserves that model in sessions.json, so subsequent turns of the same subagent session keep using gpt-5.5. Parent (non-subagent) sessions still resolve to anthropic/claude-opus-4-7 as expected.
  • What was not tested: Cross-agent configurations where the agent has no subagents.model and only agents.defaults.subagents.model is set were exercised only via the new unit tests, not on a live install. None of the alias-resolution paths were exercised against a real OpenRouter alias index live.

Post-rebase verification (2026-05-12)

Rebase merged upstream/main (e7e3e903bf) into the branch and addressed the clawsweeper P2 finding on src/auto-reply/reply/get-reply.ts:404-416. Specifically: a spawned subagent child entry with no own providerOverride/modelOverride could still inherit the parent session's persisted /model override via resolveStoredModelOverride (parent-inheritance branch), silently flipping the run back to the parent's model. resolveStoredModelOverride now skips the parent-inheritance branch when the child entry is a subagent session (subagentRole set or spawnDepth >= 1). Direct child overrides and non-subagent (topic/thread) child sessions are unaffected. New regression coverage in src/auto-reply/reply/stored-model-override.test.ts.

Commands run:

  • pnpm tsgo (core typecheck) -> exit 0, no diagnostics
  • pnpm test src/auto-reply/reply/stored-model-override.test.ts src/auto-reply/reply/directive-handling.defaults.subagent.test.ts src/agents/model-selection.test.ts src/gateway/session-utils.test.ts src/cron/isolated-agent.model-formatting.test.ts -> 4 vitest shards passed (236 tests)
  • pnpm test src/auto-reply/reply.block-streaming.test.ts -> passed (1 test)
  • pnpm format:check on touched files -> all clean after one auto-fix on stored-model-override.ts
  • node scripts/run-oxlint.mjs on touched files -> 0 warnings, 0 errors

@openclaw-barnacle openclaw-barnacle Bot added gateway Gateway runtime agents Agent runtime and tooling size: S labels Apr 27, 2026
@greptile-apps

greptile-apps Bot commented Apr 27, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Fixes a real spawn-time race condition: when the gateway reads a subagent's session entry before the model write has flushed, resolveSessionModelRef now correctly falls back to the configured subagent default instead of silently using the parent's primary model. The ?? chain reorder in resolveSubagentConfiguredModelSelection is intentional and aligns the runtime with documented semantics (agents.defaults.subagents.model as global subagent default), but it is a silent breaking change for any deployment that had agents.defaults.subagents.model configured while relying on per-agent primaries winning over it.

Confidence Score: 4/5

Safe to merge; the race-condition fix is correct and well-tested, with one intentional behavioral change worth calling out in release notes.

No logic errors or security issues found. The only concern is the precedence reorder in resolveSubagentConfiguredModelSelection which silently inverts the winner between agents.defaults.subagents.model and agentConfig.model for existing users — a P2 behavioral change that is documented as intentional but could surprise users on upgrade.

src/agents/model-selection.ts — the ?? chain reorder is the change most likely to affect existing deployments.

Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/agents/model-selection.ts
Line: 244-248

Comment:
**Breaking precedence change for `agentConfig.model`**

After the reorder, any agent whose subagents relied on inheriting the agent's own primary model (no explicit `subagents.model` set on the agent, but a global `agents.defaults.subagents.model` does exist) will silently switch to the global default. Before this change `agentConfig.model` won over the global default; now it's the last resort. The behavior is documented as intentional, but it is a silent regression for that configuration pattern — users won't get a warning, and the previously-used model changes without any explicit action on their part.

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

Reviews (1): Last reviewed commit: "fix(subagent): resolve runtime model fro..." | Re-trigger Greptile

Comment thread src/agents/model-selection.ts Outdated
Comment on lines 244 to 248
return (
normalizeModelSelection(agentConfig?.subagents?.model) ??
normalizeModelSelection(agentConfig?.model) ??
normalizeModelSelection(params.cfg.agents?.defaults?.subagents?.model)
normalizeModelSelection(params.cfg.agents?.defaults?.subagents?.model) ??
normalizeModelSelection(agentConfig?.model)
);

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 Breaking precedence change for agentConfig.model

After the reorder, any agent whose subagents relied on inheriting the agent's own primary model (no explicit subagents.model set on the agent, but a global agents.defaults.subagents.model does exist) will silently switch to the global default. Before this change agentConfig.model won over the global default; now it's the last resort. The behavior is documented as intentional, but it is a silent regression for that configuration pattern — users won't get a warning, and the previously-used model changes without any explicit action on their part.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/model-selection.ts
Line: 244-248

Comment:
**Breaking precedence change for `agentConfig.model`**

After the reorder, any agent whose subagents relied on inheriting the agent's own primary model (no explicit `subagents.model` set on the agent, but a global `agents.defaults.subagents.model` does exist) will silently switch to the global default. Before this change `agentConfig.model` won over the global default; now it's the last resort. The behavior is documented as intentional, but it is a silent regression for that configuration pattern — users won't get a warning, and the previously-used model changes without any explicit action on their part.

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

@joeykrug joeykrug force-pushed the fix/subagent-model-resolution branch 2 times, most recently from 46612ec to 9d60b36 Compare April 27, 2026 20:22
@clawsweeper

clawsweeper Bot commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs real behavior proof before merge. Reviewed June 11, 2026, 1:00 PM ET / 17:00 UTC.

Summary
Review failed before ClawSweeper could summarize the requested change.

PR surface: Source +140, Tests +533. Total +673 across 10 files.

Reproducibility: unclear. The review failed before ClawSweeper could establish a reproduction path.

Review metrics: none identified.

Stored data model
Persistent data-model change detected: serialized state: src/gateway/session-utils.test.ts, unknown-data-model-change: src/gateway/session-utils.test.ts, unknown-data-model-change: src/gateway/session-utils.ts. Confirm migration or upgrade compatibility proof before merge.

Merge readiness
Overall: 🌊 off-meta tidepool
Proof: 🌊 off-meta tidepool
Patch quality: 🌊 off-meta tidepool
Result: rating does not apply to this item.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Risk before merge

  • [P1] No close action taken because the review did not complete.

Maintainer options:

  1. Decide the mitigation before merge
    Retry the Codex review after fixing the execution failure.
  2. Pause or close
    Do not merge this PR until maintainers decide whether the risk is worth taking.

Next step before merge

  • [P1] Review did not complete, so no work-lane recommendation was made.
Review details

Best possible solution:

Retry the Codex review after fixing the execution failure.

Do we have a high-confidence way to reproduce the issue?

Unclear. The review failed before ClawSweeper could establish a reproduction path.

Is this the best way to solve the issue?

Unclear. Retry the review first so ClawSweeper can evaluate the actual issue and fix direction.

AGENTS.md: unclear because the file could not be read completely.

Codex review notes: model internal, reasoning high; reviewed against 575cae59d486.

Label changes

Label changes:

  • remove P2: Current review triage priority is none.
  • remove merge-risk: 🚨 compatibility: Current PR review selected no merge-risk labels.
  • remove merge-risk: 🚨 auth-provider: Current PR review selected no merge-risk labels.
  • remove merge-risk: 🚨 message-delivery: Current PR review selected no merge-risk labels.

Label justifications:

  • rating: 🌊 off-meta tidepool: Overall readiness is 🌊 off-meta tidepool; proof is 🌊 off-meta tidepool and patch quality is 🌊 off-meta tidepool.
Evidence reviewed

PR surface:

Source +140, Tests +533. Total +673 across 10 files.

View PR surface stats
Area Files Added Removed Net
Source 5 144 4 +140
Tests 5 534 1 +533
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 10 678 5 +673

What I checked:

  • failure reason: retryable codex transport failure.
  • codex failure detail: Codex review failed for this PR with exit 1.
  • codex stderr: em should failover to a configured fallback model (sub_agents fallbacks in model_stack include openai-codex/gpt-5.2, openai-codex/gpt-5.4, zai/glm-5).\n\n## Actual behaviour\n\nLog shows:\n\n\"embedded_run_failover_decision\",\n\"failoverReason\": \"timeout\",\n\"profileFailureReason\": \"timeout\",\n\"provider\": \"kimi\",\n\"model\": \"kimi-code\",\n\"profileId\": \"sha256:cf78ee31c9b1\",\n\"fallbackConfigured\": false,\n\"timedOut\": true,\n\"aborted\": true\n\n\nThe embedded agent profile for kimi/kimi-code has fallbackConfigured: false despite global model_stack having fallbacks configured. No fallback fires — the job just fails.\n\n## Environment\n\n- OpenClaw: 2026.4.27 (cbc2ba0)\n- OS: Linux x64\n- Node: v24.14.1\n- Session type: isolated (cron job)\n".

Likely related people:

  • unknown: Codex failed before it could trace repository history. (role: review did not complete; confidence: low)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

How this review workflow works
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

joeykrug added 6 commits May 10, 2026 00:42
… parent primary

`resolveSessionModelRef` had no subagent awareness — when `entry.model` was
empty (race between spawn-side write and gateway read), it fell through to
`resolveDefaultModelForAgent`, returning the parent agent's primary model
instead of the configured subagent default.

Also reorder the `??` chain in `resolveSubagentConfiguredModelSelection`:
global `agents.defaults.subagents.model` now takes precedence over the
spawning agent's own `model` field, matching the documented behavior.

Tests cover both the runtime fallback and the spawn-time chain order.
The Pi runtime harness initialization writes the parent agent's
primary model into the child session entry, overwriting the value
that `resolveSubagentSpawnModelSelection` correctly resolved during
spawn. This was the production failure mode covered up by the
read-side fallback in fbbd3c2d5f.

When `entry.subagentRole` is set or `entry.spawnDepth >= 1`, prefer
`agents.list[<id>].subagents.model.primary` (with the standard chain
fallbacks via `resolveSubagentConfiguredModelSelection`) over the
parent agent's primary `model.primary`.

`getReplyFromConfig` resolved the run model via `resolveDefaultModel`
(which goes through `resolveDefaultModelForAgent` and is not
subagent-aware), and then post-run persistence in
`updateSessionStoreAfterAgentRun` wrote that model back to
`entry.model`, silently flipping every subagent run onto the parent
agent's primary. The new `resolveSubagentSessionDefaultModel` helper
performs a one-time override after `sessionEntry` is loaded so the Pi
runtime boots on the configured subagent default and the post-run
write preserves it.

Tests cover the subagent path and a sanity assertion that
non-subagent sessions still inherit the parent's primary.
Main re-introduced the inline 'type Command' export after the prior fix
extracted it to a separate 'export type { Command }' line; the merge kept
both, causing TS2300 'Duplicate identifier' under the extension package
boundary tsc compile. Keep only the inline form (consistent with main).
The reply-time fallback (resolveSubagentSessionDefaultModel) and the
gateway-side fallback (resolveSessionModelRef) both fed the configured
subagent selection straight into parseModelRef, so a configured alias
such as 'gpt' was parsed as a bare model under the default provider
instead of being resolved through agents.defaults.models. That made the
race fallback disagree with resolveSubagentSpawnModelSelection, which
already runs alias resolution.

Switch both helpers to resolveModelRefFromString with a buildModelAliasIndex
alias index built from the same default provider, matching the spawn-side
contract. Adds regression tests in both files (configured subagent alias
'gpt' resolves to openai/gpt-5.4) and a CHANGELOG entry.
@joeykrug joeykrug force-pushed the fix/subagent-model-resolution branch from 63c26bd to b45a2f4 Compare May 10, 2026 04:47
@openclaw-barnacle openclaw-barnacle Bot added proof: supplied External PR includes structured after-fix real behavior proof. and removed extensions: memory-core Extension: memory-core labels May 10, 2026
@openclaw-barnacle openclaw-barnacle Bot removed the proof: sufficient ClawSweeper judged the real behavior proof convincing. label May 22, 2026
@clawsweeper clawsweeper Bot added the proof: sufficient ClawSweeper judged the real behavior proof convincing. label May 22, 2026
joeykrug added a commit to joeykrug/openclaw that referenced this pull request May 22, 2026
@openclaw-barnacle openclaw-barnacle Bot removed the proof: sufficient ClawSweeper judged the real behavior proof convincing. label May 22, 2026
@joeykrug joeykrug force-pushed the fix/subagent-model-resolution branch from 7496b24 to 98db52b Compare May 22, 2026 20:51
@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. and removed rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. labels May 22, 2026
joeykrug and others added 2 commits June 3, 2026 00:59
…icts

Resolve 5 conflicts preserving the subagent-model-resolution feature while
adopting upstream's refactors:
- get-reply.ts: keep the subagent-default boot block on upstream's
  timing-wrapped resolver; defaultProvider/defaultModel/aliasIndex become
  let-bound so the subagent block can reassign them.
- model-selection.ts / model-selection-resolve.ts / cron run-model-selection
  aggregator: adopt upstream's canonical resolveSubagentConfiguredModelSelection
  (same subagents.model -> defaults.subagents.model -> agent primary precedence
  the PR introduced, plus upstream's includeAgentPrimary knob); drop the now-stale
  resolve-file import/re-exports.
- transport-stream.ts, preaction.test.ts, embedded-agent-runner/runs.test.ts:
  take upstream's unrelated churn (Gemini signature payload helper, programLocal
  rename, file rename + new imports).

Finding: ClawSweeper's image-model one-turn override precedence defect is moot
after this merge -- upstream commit 55a0c9b removed the image-model override
plumbing (opts.modelOverride / hasAppliedImageModelOverride) from get-reply.ts
entirely. The surviving one-turn override is the heartbeat override, which the
subagent-default block already gates on (!hasResolvedHeartbeatModelOverride).
Added a focused regression in directive-handling.defaults.subagent.test.ts
pinning that gating: a resolved one-turn override wins and the subagent default
does not clobber it, while an ordinary subagent turn applies the subagent default.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@clawsweeper clawsweeper Bot added the merge-risk: 🚨 message-delivery 🚨 May drop, duplicate, misroute, suppress, or wrongly target messages. label Jun 3, 2026
joeykrug and others added 4 commits June 3, 2026 01:42
The branch's prior main-merges caused git merge to silently revert unrelated
upstream files (import-sort churn) in two files the PR never intended to touch:
- extensions/memory-wiki/src/tool.test.ts
- src/flows/doctor-lint-flow.ts

Restore both to upstream/main so check-dependencies / build-artifacts pass,
leaving only the PR's real subagent-model-resolution changes in the net diff.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
# Conflicts:
#	src/auto-reply/reply/stored-model-override.ts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling gateway Gateway runtime merge-risk: 🚨 auth-provider 🚨 May break OAuth, tokens, provider routing, model choice, or credentials. merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. merge-risk: 🚨 message-delivery 🚨 May drop, duplicate, misroute, suppress, or wrongly target messages. P2 Normal backlog priority with limited blast radius. proof: supplied External PR includes structured after-fix real behavior proof. rating: 🌊 off-meta tidepool PR readiness rating does not apply to this item. size: L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant