Skip to content

Stop the nightly translation re-translating everything, most days - #685

Open
chhhee10 wants to merge 4 commits into
mainfrom
fix/translate-cache-eviction
Open

Stop the nightly translation re-translating everything, most days#685
chhhee10 wants to merge 4 commits into
mainfrom
fix/translate-cache-eviction

Conversation

@chhhee10

@chhhee10 chhhee10 commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

The Translate Docs cron cost 4 minutes on Aug 3-5 and 118-136 minutes
every day from Aug 6-11 — ~750 wasted runner-minutes and six full-corpus passes
through the LLM gateway in six days.

Aug 3,4,5     4 min   cache survived
Aug 6       125 min   FAIL
Aug 7       136 min   FAIL
Aug 8       122 min   ok    <- saved successfully, and Aug 9 was still slow
Aug 9       118 min   ok
Aug 10      123 min   ok
Aug 11      134 min   ok
Aug 12        5 min   FAIL  <- cache HIT, and the run failed *because* of it

Three causes compound. None of them is the translation cache's own logic, which
is sound — isCached validates per entry against the English source hash, and
the top-level sourceHash field is vestigial (written in three places, read in
none).

1. The cache was evicted between runs — dominant, ~105 min/day

ci.yml cached target/ alongside the cargo registry under a combined
actions/cache@v6, which writes a ref-scoped copy from every branch that
misses the exact key. With build output included each copy is 1.5-2.3 GiB, and
five were live at once — PR refs 677, 679, 680, 681 and main — putting the repo
at 11.56 GiB against GitHub's 10 GiB cap, so the store sits permanently in
LRU eviction.

What that evicted was the 13 KB translation cache, read once every 24 hours
and therefore always the least-recently-used entry in the store. Aug 9's jobs
say so directly: Cache not found for input keys: translation-cache-, translation-cache-, the morning after Aug 8 logged Cache saved with key: translation-cache-1adebf3f….

Fixed with the restore/save split build-daemon.yml:117-144 already uses, whose
comment gives the other reason to want it (a PR branch can otherwise write a
poisoned target/ that a release run restores into a published binary).

2. The cache was saved once, at the end of a serial pipeline

The only save sat in consolidate, downstream of both the matrix gate and
mintlify validate. One page failing in one language discarded all fourteen —
Aug 6 lost ~110 minutes of finished translation to a single ko page. Each
language now saves its own fragment in the job that produced it, right after the
step that proved it good. Each fragment is already authoritative for its own
language, so nothing needs merging first; consolidate's merged save stays as the
cross-language fallback.

A miss is also visible now. The old restore key always evaluated to the bare
literal translation-cache- — the file is gitignored, so it is absent at
checkout and hashFiles returns "" for a path matching nothing. Every restore
that ever worked was a restore-keys prefix match, and a total miss looked
exactly like a hit: nothing failed, nothing warned, the job just spent nine
minutes and a full LLM pass.

3. A cache HIT never checked the file exists — this is Aug 12, and it deadlocks

isCached records that a page was translated once, not that it is on disk
now. Translations land on an auto-translate PR branch; while that sits
unmerged, main lacks the files and the cache still says done, so they are never
regenerated — while --update-nav reads the English tree and emits nav
entries for them. mintlify validate then fails on 28 missing files.

Verified live rather than inferred: docs/cli/update.mdx and
docs/cli/migrate.mdx are on main, docs/zh/cli/ has neither, and #682
carrying them is still open.

That is non-convergent: a cache hit writes nothing → validation fails → the
cache is never saved; a full cache miss spends 120 minutes → goes green. There
was no path to a cheap success while #682 stayed open.

⚠️ This one changes translation OUTPUT, not just caching. The first run
re-sends those 28 pages to the model, so the text will not be byte-identical to
what is on #682 — expect one noisy diff there. ~10 minutes, once.

Guarded at all four call sites. cli.ts is the load-bearing one: the batch
path sorts pages into cached/uncached itself and never calls translateMdxPage
for a cached page, so guarding only the translator would have fixed nothing.

Testing

Beyond the suite, the deadlock was reproduced against the real repo with a
realistically seeded cache (every English page marked translated for zh at its
current hash, as a successful Aug 11 run would have left it):

Cached Need translation Outcome
Without fix 48 0 writes nothing → nav points at 2 missing files → validate fails
With fix 46 2 regenerates exactly the two missing pages

The new unit tests pin both directions — a missing output re-translates, a
present one is still skipped. The second matters as much as the first: without
it a later refactor could satisfy this PR by turning the guard into a cache
bypass, and every run would be a full re-translation with the suite still green.
I also confirmed the first test fails on the pre-fix code rather than
assuming it would.

  • bun run test:run — 3458 pass, 10 skipped
  • tsc --noEmit — clean
  • eslint on every changed file — clean
  • both workflow YAMLs re-parsed after editing

Deliberately not included

  • Reordering consolidate's validation so the cache save precedes it. Fix 2
    already makes the cache survive that failure, so this is reorder risk for
    little return.
  • A delete channel in the merge script. Correctness only, zero minutes.

Expected effect

Steady state returns to the ~4-minute cache-hit day. The first run after merge
re-translates the 28 pages main is missing (~10 minutes, once), and the repo
cache should fall back under the 10 GiB cap as the four PR-scoped cargo copies
age out.

🤖 Generated with Claude Code

https://claude.ai/code/session_01HEwcerc9jE7ZBkfBiYRbep

Hermes review

Field Value
Status Approved
Reviewed commit d960a1d2532a0b648cfb12ce0fdfc7da5718e9cc
Policy revision 1d8f31d926828f3bae215c58f5b35baa44acbff0
Model gpt-5.6-terra
Duration 430s
Updated 2026-08-12T12:41:04.252759059+00:00

Summary

No actionable correctness, security, data-safety, or compatibility defects found in the PR diff. The relevant cache, workflow, batch, and single-page paths were inspected.

Changes

  • Splits Rust build-cache restore/save so only main writes large Cargo artifacts.
  • Adds per-language translation-cache restore/save and cache-miss visibility.
  • Treats a translation cache entry as valid only when its output file exists.

Validation

None configured.

Findings

None.

Open questions

None.

Policy overrides

None.

Summary by CodeRabbit

  • Bug Fixes

    • Improved translation caching by detecting missing translated files and regenerating them automatically.
    • Added clearer warnings when translation caches are unavailable.
    • Improved reliability of translated documentation and README generation.
  • Chores

    • Updated CI caching to preserve translation and build results more reliably.
    • Extended translation artifact retention to seven days.
  • Documentation

    • Added release notes for version 1.0.1-beta.0.
  • Tests

    • Added coverage for missing and existing translation outputs to verify cache behavior.

chhhee10 and others added 2 commits August 12, 2026 17:53
`rust-quality` cached `~/.cargo` AND `target/` under a combined
`actions/cache@v6`, which writes a ref-scoped copy from every branch that misses
the exact key. Because the entry carries build output, each copy is 1.5-2.3 GiB,
and five were live at once — PR refs 677, 679, 680, 681 and main — putting the
repository at 11.56 GiB against GitHub's 10 GiB cap and therefore permanently in
LRU eviction.

The thing being evicted was not another cargo build. It was the 13 KB doc
translation cache, read once every 24 hours by the nightly `translate-docs` run
and so always the least-recently-used entry in the store. Losing it re-translated
48 pages into 14 languages the next morning: ~125 runner-minutes and a full LLM
pass per language, against a 4-minute baseline when it survives. Six consecutive
days of that, Aug 6-11, cost ~750 runner-minutes and six full-corpus passes
through the gateway.

Restore on every run, save only on a push to main — the split `build-daemon.yml`
already uses, whose comment gives the other reason to want it (a PR branch can
otherwise write a poisoned `target/` that a later release run restores straight
into a published binary). `cache-hit != 'true'` keeps a run that changed nothing
from re-uploading 2 GiB.

What a PR gives up: one whose `Cargo.lock` moved rebuilds from a
stale-but-close main cache. That is already what `restore-keys` hands it today.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The only save sat in `consolidate`, downstream of both the matrix gate
(`if: needs.translate.result == 'success'`) and `mintlify validate`. So the
day's cache was contingent on fourteen languages and a nav check all succeeding:
Aug 6 discarded ~110 minutes of completed translation because one `ko` page
failed validation, and Aug 12 discarded a full run because consolidate's
validation failed. In both cases every language had finished its work and
uploaded its fragment; the cache was thrown away anyway.

Each fragment is already authoritative for its own language, so nothing has to
be merged before it can be stored. Each language now saves its own, in the job
that produced it, immediately after the step that proved it good. Consolidate's
merged save stays as the cross-language fallback.

The restore key changes for a related reason. It read
`translation-cache-${{ hashFiles('scripts/translate-docs/.translation-cache.json') }}`,
which ALWAYS evaluated to the bare literal `translation-cache-`: the file is
gitignored, so it is absent at checkout and `hashFiles` returns "" for a path
that matches nothing. Every restore that ever worked was a `restore-keys` prefix
match. That is not a bug on its own — but it means a total miss and a hit are
indistinguishable, so the expensive case was silent. It is now a per-language
key with the merged entry as fallback, and a miss emits a `::warning` naming
what it is about to cost.

Artifact retention 1 → 7 days, so a run that dies mid-pipeline leaves a human a
recovery path rather than expiring overnight.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@chhhee10
chhhee10 force-pushed the fix/translate-cache-eviction branch from 58a0a80 to 06833d8 Compare August 12, 2026 12:27
`isCached` is a pure function of the ENGLISH source hash. It records that a page
was translated once — never that the translation is on disk now — and those two
facts came apart in production.

Translations land on an auto-translate PR branch. While that branch sits
unmerged, `main` lacks the files and the cache still reports them done, so they
are never regenerated. Meanwhile `--update-nav` reads the ENGLISH tree and emits
nav entries for them, and `mintlify validate` fails on entries pointing at files
that are not there. Verified on the live repo: `docs/cli/{update,migrate}.mdx`
exist on main, `docs/zh/cli/` has neither, and PR #682 carrying them is still
open — 28 missing files across 14 locales.

That is non-convergent, which is what makes it worth a code change rather than a
merge. A cache HIT writes nothing, so validation fails and the cache is never
saved; a full cache MISS spends 120 runner-minutes and goes green. The pipeline
had no path to a cheap success while #682 stayed open, and Aug 12 is exactly
that: all 14 languages finished in ~20 seconds each, and consolidate failed.

Statting the output makes the cache self-healing against any "translated once,
never landed" gap, whatever opened it — an unmerged PR, a hand-reverted file, a
locale added to the matrix after the fact.

Guarded at all four sites rather than one. `cli.ts` is load-bearing: the batch
path sorts pages into cached/uncached itself and never calls `translateMdxPage`
for a cached one, so guarding only the translator would have fixed nothing. The
two single-page paths are guarded too, or they and the batch path disagree about
what "cached" means.

The tests pin both directions — a missing output re-translates, a present one is
still skipped. The second matters as much as the first: without it a later
refactor could satisfy this commit by making the guard a cache bypass, and every
run would be a full re-translation with the suite still green. Confirmed the
first test fails on the pre-fix code rather than assuming it would.

NOTE: this changes translation OUTPUT, not just caching. The first run after it
lands re-sends those 28 pages to the model, so the text will not be byte-identical
to what sits on #682 — expect a noisy diff there once. ~10 minutes, once.

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

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 54543b02-e8d7-43b5-9cca-be9ddb07357e

📥 Commits

Reviewing files that changed from the base of the PR and between b20ebad and d960a1d.

📒 Files selected for processing (1)
  • .github/workflows/translate-docs.yml
🚧 Files skipped from review as they are similar to previous changes (1)
  • .github/workflows/translate-docs.yml

📝 Walkthrough

Walkthrough

Translation cache hits now require existing output files. CI and translation workflows use explicit cache restore and save steps. Release notes document these changes, and tests cover missing and present cached outputs.

Changes

Translation cache reliability

Layer / File(s) Summary
Validate cached translation outputs
scripts/translate-docs/..., __tests__/scripts/translate-docs/mdx-translator.test.ts
MDX and README translations regenerate when cached output files are missing. Tests cover regeneration and cache reuse.
Control workflow cache persistence
.github/workflows/ci.yml, .github/workflows/translate-docs.yml, CHANGELOG.md
Cargo caches use explicit restore and save steps. Translation caches use per-language keys, cache-miss warnings, per-language saves, and seven-day artifact retention. The changelog records the release fixes.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Suggested labels: bug

Suggested reviewers: hermes-exosphere

Poem

I checked each cached page with care,
And found missing files hiding there.
Fresh translations now hop through,
While saved caches stay clear and true.
— A rabbit with a tidy burrow 🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the primary change: preventing nightly translations from reprocessing the full corpus.
Description check ✅ Passed The description clearly explains the problem, implementation, testing, and expected effect, but omits the template's Type of Change and Checklist sections.

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.

@chhhee10
chhhee10 force-pushed the fix/translate-cache-eviction branch from 06833d8 to b20ebad Compare August 12, 2026 12:28
@hermes-exosphere

hermes-exosphere commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Hermes

Status Reviewed
Verdict Approved
Head d960a1d2532a
Rounds 0 of 5

No actionable correctness, security, data-safety, or compatibility defects found in the PR diff. The relevant cache, workflow, batch, and single-page paths were inspected.

What this changes

flowchart LR
    n0RustCIcache["~ Rust CI cache"]
    n1Translationworkflow["~ Translation workflow"]
    n2Translationcache["Translation cache"]
    n3TranslationbatchCLI["~ Translation batch CLI"]
    n4MDXtranslator["~ MDX translator"]
    n5READMEtranslator["~ README translator"]
    n6Translationtests["~ Translation tests"]
    n1Translationworkflow -- "restores and saves cache fragments" --> n2Translationcache
    n1Translationworkflow -- "starts per-language translation" --> n3TranslationbatchCLI
    n3TranslationbatchCLI -- "dispatches uncached MDX tasks" --> n4MDXtranslator
    n3TranslationbatchCLI -- "dispatches uncached README tasks" --> n5READMEtranslator
    n3TranslationbatchCLI -- "checks hashes and output existence" --> n2Translationcache
    n4MDXtranslator -- "validates cached output existence" --> n2Translationcache
    n5READMEtranslator -- "validates cached output existence" --> n2Translationcache
    n6Translationtests -- "exercises cache-hit behavior" --> n4MDXtranslator
Loading

Rounds

Round Reviewed Commits in this round Verdict
0 d960a1d2532a a05bab4206aa dcfa1c5c060b b20ebadb7e09 d960a1d2532a Approved

Findings

Nothing raised yet.


@hermes-exosphere help lists every command. This comment is maintained in place — I rewrite it after each review rather than posting a new one.

@coderabbitai coderabbitai Bot added the bug Something isn't working label Aug 12, 2026
The save key embeds `github.run_id`, and GitHub REUSES that id when someone
re-runs a failed job. On the second attempt the primary key already exists, the
restore scores an exact hit, and the save collides with itself.

Found by running it rather than reasoning about it. A throwaway workflow
exercising the same key shapes showed all four cases:

  - first run:  cache-matched-key='',              save proceeds
  - next run:   restored from probe-zh-<prev_id>,  cache-hit='false'
  - re-run:     cache-hit='true'                   <- the collision
  - languages stayed isolated: ja restored ja's payload, zh restored zh's

The same `cache-hit != 'true'` guard `build-daemon.yml:137` carries. With it the
re-run skips the save and stays green.

That probe also confirmed the two things the rest of this branch assumes and
could not otherwise check: `cache-matched-key` really is empty on a total miss —
so the new warning fires exactly when a language is about to re-translate
everything, and stays silent on the prefix hits that are the normal case — and
the `restore-keys` prefix genuinely carries the previous run's file across, which
is the whole mechanism by which tomorrow's run inherits today's cache.

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

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/translate-docs.yml:
- Around line 105-108: Update the workflow’s prepare logic to validate every
language from inputs.languages against the supported language allowlist and
reject any invalid value before shell execution. Pass only the validated
language value to translation steps through an environment variable, and quote
that variable wherever the translation command uses it so shell metacharacters
cannot execute commands.

In `@scripts/translate-docs/readme-translator.ts`:
- Around line 223-225: Add direct tests for translateReadme covering both
cache-condition outcomes: regenerate the README translation when
docs/i18n/README.<lang>.md is missing, and skip generation when that file
exists. Add these cases to
__tests__/scripts/translate-docs/readme-translator.test.ts, using the existing
test setup and mocks to verify translation is invoked only for the missing-file
case.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b204146e-eeac-40f8-8d0c-3ec775e03b57

📥 Commits

Reviewing files that changed from the base of the PR and between e022752 and b20ebad.

📒 Files selected for processing (7)
  • .github/workflows/ci.yml
  • .github/workflows/translate-docs.yml
  • CHANGELOG.md
  • __tests__/scripts/translate-docs/mdx-translator.test.ts
  • scripts/translate-docs/cli.ts
  • scripts/translate-docs/mdx-translator.ts
  • scripts/translate-docs/readme-translator.ts

Comment on lines +105 to +108
- name: Warn on translation cache miss
if: steps.restore-cache.outputs.cache-matched-key == ''
run: |
echo "::warning title=Translation cache MISS::${{ matrix.lang }} will re-translate every page (~9 runner-minutes and one full LLM pass)"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,125p' .github/workflows/translate-docs.yml
rg -n -C 4 'translate-docs\.yml|uses:.*translate-docs|inputs:\s*$|languages:' .github/workflows

Repository: FailproofAI/failproofai

Length of output: 9116


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- workflow remainder ---'
sed -n '120,260p' .github/workflows/translate-docs.yml

printf '%s\n' '--- language-related implementation ---'
rg -n -C 5 'languages|language|lang' scripts/translate-docs package.json .github/workflows/translate-docs.yml \
  -g '*.ts' -g '*.js' -g '*.json' -g '*.yml' -g '*.yaml' -g 'package.json' \
  | head -n 500

printf '%s\n' '--- workflow references and permissions ---'
rg -n -C 5 'translate-docs|workflow_call|workflow_dispatch|permissions:|ANTHROPIC_API_KEY|ANTHROPIC_AUTH_TOKEN' .github README.md scripts \
  -g '*.yml' -g '*.yaml' -g '*.md' -g '*.ts' -g '*.js' -g 'package.json' \
  | head -n 500

printf '%s\n' '--- candidate files ---'
git ls-files scripts/translate-docs .github/workflows/translate-docs.yml

Repository: FailproofAI/failproofai

Length of output: 50379


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

workflow = Path(".github/workflows/translate-docs.yml").read_text()
cases = [
    'en',
    'en; echo INJECTED',
    "en' ; echo INJECTED; #",
    'en", "ja"]',
]
needles = [
    "if [ -n \"${{ inputs.languages }}\" ]; then",
    "echo '${{ inputs.languages }}' | jq -Rc",
    "echo \"::warning title=Translation cache MISS::${{ matrix.lang }}",
    "bun run translate --languages ${{ matrix.lang }}",
]
for value in cases:
    print(f"=== input: {value!r} ===")
    expanded = workflow.replace("${{ inputs.languages }}", value).replace("${{ matrix.lang }}", value)
    for needle in needles:
        pos = expanded.find(needle.split("${{")[0])
        if pos >= 0:
            print(expanded[pos:expanded.find("\n", pos)])
PY

Repository: FailproofAI/failproofai

Length of output: 1652


Validate and quote inputs.languages before shell use

A workflow_dispatch user can set inputs.languages. The value reaches prepare shell source and the unquoted translation command. For example, en;id runs id as a separate command while ANTHROPIC_API_KEY is set. Validate each language against the supported allowlist in prepare, then pass the validated value through a quoted environment variable.

🧰 Tools
🪛 zizmor (1.29.0)

[warning] 108-108: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/translate-docs.yml around lines 105 - 108, Update the
workflow’s prepare logic to validate every language from inputs.languages
against the supported language allowlist and reject any invalid value before
shell execution. Pass only the validated language value to translation steps
through an environment variable, and quote that variable wherever the
translation command uses it so shell metacharacters cannot execute commands.

Source: Linters/SAST tools

Comment on lines +223 to +225
// `&& existsSync(outputPath)` — see the MDX path. Cached records that a
// translation was produced, not that the file is there now.
if (isCached(cache, "README.md", lang, sourceContent) && existsSync(outputPath)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Add direct tests for the README cache condition.

The supplied tests invoke translateMdxPage only. They do not exercise translateReadme.

Add tests in __tests__/scripts/translate-docs/readme-translator.test.ts for both cases: regenerate when docs/i18n/README.<lang>.md is absent, and skip generation when it exists. As per coding guidelines: “When you add or change logic, add a corresponding test in __tests__/.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/translate-docs/readme-translator.ts` around lines 223 - 225, Add
direct tests for translateReadme covering both cache-condition outcomes:
regenerate the README translation when docs/i18n/README.<lang>.md is missing,
and skip generation when that file exists. Add these cases to
__tests__/scripts/translate-docs/readme-translator.test.ts, using the existing
test setup and mocks to verify translation is invoked only for the missing-file
case.

Source: Coding guidelines

@hermes-exosphere hermes-exosphere 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.

Hermes found no blocking issues in this revision.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants