Add AGS rollout buffer generator for SWE coding-agent RL - #1
Conversation
Use fresh adapter session IDs for each AGS attempt and isolate per-sample failures so one bad rollout does not stop the generator.
Convert Harbor SWE-style tasks into AGS prompt data while preserving the canonical verifier layout, and run eval_cmd as root so Harbor test.sh can execute unmodified in the evaluator sandbox.
Log AGS rollout calls, trajectory events, and sample payloads to Weave when W&B online logging is enabled, with an opt-in flag to decode token IDs for trace readability.
Route trajectory parsing through the configured AGS agent so CodeBuddy Code stream JSON can be traced without assuming Claude Code timestamps.
Large SWE-bench PASS_TO_PASS lists made the inline test.sh/config.json heredocs blow past AGS/E2B argv limits. Embed the files as gzip+base64 blobs and decode them with a python3 heredoc inside the sandbox instead.
Compute passrate from prompt groups and rollout attempts when rollout fan-out produces multiple training samples per attempt, and avoid failing training on mismatched fixed-shape passrate inputs.
Allow AGS adapters to publish a full public base URL so proxy paths can be used, and skip unnecessary rollout onload/update work after the final rollout when no eval remains.
Stop each rollout-buffer generation job once enough samples have been collected so colocated SGLang engines are not offloaded while AGS sandboxes can still issue generation requests. Also pause generation before releasing SGLang memory and add tests for one-epoch buffer requests and stale job writes.
Keep the two-node launcher aligned with the scalable setup by enabling checkpoint saves, shuffled chat-templated rollouts, configurable rollout steps, and run-local W&B logs.
Add lightweight adapter control endpoints so eval rollouts can reuse the already-running AGS adapter instead of binding a second service on the same port.
Pass rollout_id through the rollout-buffer payload and initialize the AGS prompt source offset from it so standalone generator runs do not repeatedly start from the first prompt groups.
Raise the two- and four-node AGS rollout defaults to the validated concurrency settings, and pass the configured WandB entity explicitly when tracking is enabled.
Keep incomplete prompt groups out of the training buffer while preserving flattened multi-segment agent outputs. Include the staged regression coverage for AGS buffering and Harbor prompt conversion.
Avoid resetting converted tasks to base_commit unless explicitly requested, so task-image compatibility patches remain available to AGS rollouts.
Support same-sandbox grading by default, stop timed-out agent process groups before evaluation, and make Harbor prompt conversion preserve image state with reproducible payloads.
Use the Harbor TCR eval set, align evaluation sampling, and reduce default sandbox resources for both launchers.
AGSPromptSource seeks RolloutDataSource to the absolute group index the trainer sends (rollout_id * rollout_batch_size), but only restored sample_offset. Once that index passed one pass over the dataset, the source stayed on epoch 0's permutation, so a resumed run drew a different prompt sequence than the uninterrupted one. Derive epoch_id from the group index and reshuffle to it when --rollout-shuffle is set.
CodeBuddy's auto-memory feature injects a `<system-reminder data-role="memory">` block into every prompt. In SWE rollouts that block is pure overhead -- there is no cross-session continuity to build up -- and it teaches the model to emit `<system-reminder>` narration of its own, which no tool-call parser can read. Verified on a 100-instance SWE-bench Verified subset: 0/100 trajectories carry memory markers afterwards, against 48/50 in the previous run. Note this does not by itself change the empty-patch rate (35.0% -> 33.0%, paired McNemar p=1.0); it is a prompt-hygiene fix, not a scoring fix. Also collapses three multi-line f-string concatenations onto single lines, as the repo formatter produces.
Some coding-agent CLIs treat a text-only final turn as a finished answer: the model stops mid-task, no tool call is parsed, the CLI exits 0, and the rollout yields an empty diff. Measured with CodeBuddy Code on SWE-bench Verified that accounts for roughly a third of all rollouts, and it is indistinguishable in the artifacts from a genuine "no change needed" outcome. empty_patch_guard.py classifies those rollouts and splits them by why they stopped (mid-task narration / claimed completion / no final text / unclassified). It reuses the existing iter_trajectory_events reader rather than adding a third trajectory parser, and it never raises: a malformed trajectory degrades to "not triggered" so classification can never cost an otherwise usable rollout. SWE_EMPTY_PATCH_GUARD selects the policy and defaults to "metrics", which only labels and counts. "abort" is deliberately not the default: an empty-patch trajectory still holds real on-policy tokens whose zero reward is correct, i.e. the negative half of a GRPO group, so masking those out at a ~33% rate would bias the group baseline toward successes. It also only requeues under fully_async_rollout -- on the rollout_buffer path the AGS scripts actually use, an aborted sample ships with its loss masked and is never retried. Verified across five runs: the empty-patch rate reproduces at 32-36%, and on a full 500-instance Claude Code run the guard fired on exactly the 3 of 42 zero-byte patches that had exit code 0, with no false positives or negatives (a nonzero exit is an honestly reported failure and needs no extra label). Also surfaces the pre-existing ill_formed flag, which had no metrics outlet.
manager_message is what re-renders as chat history on the next turn, so any field it drops stops the re-render from reproducing the ids we sampled. _SampleBuilder then sees token drift and either REALIGNs -- rewriting a real trained response as loss_mask=0 -- or forks the chain. _build_reply_parts dropped two fields: reasoning_content was never set on the leaf at all, and content was blanked whenever tool_calls were present. Measured over 150 rollouts of a 500-instance SWE-bench Verified run, that split each CodeBuddy rollout into a median of 25 samples where Claude Code produced 1, with num_samples predicted exactly by (wire responses - reasoning-only responses) in 150/150 cases. The comments justifying both omissions assumed clients rewrite history on echo. Probing the real CodeBuddy CLI over 40 turns shows it echoes text alongside tool_calls verbatim, so neither omission was buying anything. It does replay reasoning under "reasoning" rather than "reasoning_content", so _translate_messages now accepts either: the field survived the round trip all along and we were failing to read it (0/15 echoes matched the leaf before, 15/15 after). Fixing only the leaf would trade token drift for a message mismatch and lose more signal, so text and reasoning go on the wire too. This is a training-signal defect only. Scores from --debug-rollout-only runs never consume loss_mask and are unaffected; it would have corrupted GRPO advantage estimation as soon as CodeBuddy drove real training. Tests drive both real adapters over a real /generate upstream and assert one sample per clean chain plus the trained-token count, since a leaf that drops a field can keep the count at 1 while training almost nothing. They need a tokenizer whose assistant rendering a model could actually emit: FakeTokenizer renders tool calls as the opaque marker toolcall:<name>, which carries no arguments and so fabricates drift on every tool turn.
The prompt style decides whether the agent is handed the task text directly or told to go read PROBLEM_STATEMENT.md, and training and eval want different answers. Eval should measure the model solving the task, the way Harbor scores it, rather than its file-discovery turns; training may prefer it to work for the context. SWE_PROMPT_STYLE now covers training (instruction) and SWE_EVAL_PROMPT_STYLE covers periodic eval (dataset), both spelled out in the 2- and 4-node example scripts. Only "instruction" writes PROBLEM_STATEMENT.md now. Under "dataset" the prompt already carries the task text, so the file was a stray untracked artifact that only git_diff's exclude pathspec kept out of the patch, and an invitation to spend turns reading a restatement of the prompt. The style is passed to each generate() call rather than stored on the runner, because _AGSGenerateState is a singleton: the first caller's mode would otherwise stick for the life of the process and hand eval the training style, or the reverse. Drop the "inline" style, which rebuilt the prompt from the raw problem_statement and is superseded by "dataset". An explicit SWE_PROMPT_STYLE=inline now fails instead of silently changing the prompt. The converter loses --prompt-source and always uses Harbor's instruction.md, for both the prompt and metadata.problem_statement, so the two styles differ in when the agent sees the task and not in what it reads. The raw tests/config.json text moves to metadata.harbor.problem_statement: it keeps upstream CRLF on 252/500 of SWE-bench Verified, which tokenises differently, so it is provenance rather than something to feed an agent.
…s sampling Eval reached AGS through RemoteAdapterService, which only proxies an adapter that someone else already bound. The training rollout is what binds it, inside the rollout-buffer process, so eval-before-train on rollout 0 and --num-rollout 0 had nothing to talk to and every prompt died on connection refused. get_adapter_service now health-probes the control URL and reuses the training adapter when it answers -- that process owns the trajectory trees, and a second bind on the same port would fail -- otherwise it starts a local adapter on an ephemeral port. Ephemeral because the training adapter may still claim ADAPTER_PORT later in the same run. That fallback lives in the RolloutManager actor, which Ray does not pin to the head node, so it advertises the local node's own IP and the port it actually bound rather than ADAPTER_PUBLIC_HOST/ADAPTER_PUBLIC_BASE_URL -- both of which name the head and a fixed port, and would send sandboxes to the wrong address. Sampling defaults from open_session now outrank the request body. Probing the real CLIs against a recording stub: codebuddy sends temperature=1 on every /v1/chat/completions call, so --eval-temperature never reached sglang under SWE_AGENT=codebuddy_code; Claude Code sends none of temperature/top_p/top_k, which is why only the codebuddy path was affected. Body values still apply for keys the caller left unset. AdapterService stays a singleton, so a later caller's args are still ignored -- tearing down a live adapter would lose in-flight trajectories. It now records its construction inputs and warns when they differ, instead of silently serving an adapter built with another tokenizer or context budget.
Scores one Megatron checkpoint on the full SWE-bench Verified set with no training: --num-rollout 0 selects train.py's eval-only branch, so no rollout buffer is started and the eval path stands up its own AGS adapter. The non-obvious parts are commented in place -- the LR scheduler asserts that train_iters=0 triggers, why --no-load-optim is required for checkpoints written with --optimizer-cpu-offload, and the Ray port overrides these H20 nodes need. Also gate vision extraction in Dataset on multimodal_keys rather than on the mere presence of a processor. AutoProcessor returns one for any VL-capable checkpoint, so Qwen3.5-35B-A3B forced a conversation-shaped prompt on text-only data, which made --apply-chat-template mandatory and rewrote Sample.prompt into a templated "<|im_start|>user ..." string. Agent rollouts hand that prompt straight to a CLI, so they were fed the literal markers.
The examples directory now holds launchers for more than one coding agent, so move them out of claude_code_ags/ into coding_agent_rl_ags/ and prefix each script with the harness it drives (cc_).
The AGS examples only had Claude Code entry points, so scoring CodeBuddy meant hand-editing SWE_AGENT. Add run_cbc_* / eval_cbc_* wrappers that set SWE_AGENT and re-exec the harness-agnostic launcher, rather than copying ~370 lines per harness for the two copies to drift apart. Reduce each harness's env surface to EXTRA_ARGS + EXTRA_ENVS. The removed knobs (max turns, per-turn output cap, thinking, tool lists) were either properties of the harness rather than of a run, or unreachable: none of the SLIME_AGENT_CBC_* names were in the launchers' Ray env allowlist, so setting them was silently ignored. Both knobs are now applied last -- EXTRA_ARGS after the harness's own flags, EXTRA_ENVS after static_env -- so either can override a default, which relies on both CLIs taking the last occurrence of a repeated flag. Deny WebSearch/WebFetch on both harnesses. Web access makes a rollout unreproducible and lets the agent look up the fix being graded; it was denied for CodeBuddy but not for Claude Code, which then used those tools on 1-2% of instances. Do it with --disallowedTools, which the CLI enforces, not --tools, which does not restrict the surface. Cover the new contract in tests: that EXTRA_ARGS and EXTRA_ENVS are applied late enough to win, that the web tools are denied on both harnesses, and that no turn or output cap is imposed. Verified by mutation -- clearing default_flags or moving EXTRA_ARGS ahead of it turns these red.
Both CLIs clamp an absolute auto-compact window to [100k, 1M], which sits above
our 96k rollout_max_context_len -- so an absolute setting could never fire before
the adapter hard-stops a turn with finish_reason="length". Switch to the
percentage overrides instead, and tell each CLI the real window so the percentage
is of MAX_CONTEXT_LEN on both sides:
- CodeBuddy: CODEBUDDY_AUTOCOMPACT_PCT_OVERRIDE is a percentage of the model's
maxInputTokens, so CodeBuddyCodeHarness now writes that key into models.json
from SLIME_AGENT_MAX_INPUT_TOKENS. Without it resolveCompactTriggerAt() falls
back to the clamped absolute window.
- Claude Code: CLAUDE_CODE_MAX_CONTEXT_TOKENS applies directly for model names it
does not recognise as Claude models, which covers our "slime-actor" label, so
no DISABLE_COMPACT is needed. MAX_OUTPUT_TOKENS is pinned to MAX_GEN_LEN rather
than left at the 32000 default-for-unknown-ids.
Default is 60%, not the CLIs' 70%: the per-turn output reservation means a prompt
cannot grow past MAX_CONTEXT_LEN - MAX_GEN_LEN (~63k), so a 70% trigger (67200)
would never be reached. The launchers now print the resolved trigger point and
warn when it lands above that ceiling.
Also fixes a shell bug in the same block: a JSON literal used as the default in
${VAR:-{...}} is mis-parsed, because the value's own closing brace terminates the
expansion and leaks a stray "}" whenever the variable is already set. The JSON is
built in a named variable first.
Remove the extra `/wandb` subdirectory from `--wandb-dir` in training and evaluation scripts.
test_ags_generator.py was never registered, so its 45 cases only ran
locally. Registering it needed three fixes, because the CI runner invokes
`python <file>` rather than pytest:
* Add the `__main__` guard the runner relies on. Without it the file is
merely imported and exits 0 -- a green check that runs nothing.
test_ags_prompt_source.py had the same gap while already being
registered, so it has been silently passing; fixed too.
* Insert REPO_ROOT into sys.path before `from tests.test_agent._fakes`,
matching what every other agent test already does. Bare `python
<file>` does not put the repo root on the path.
* Install openai / transformers / wandb / fastapi / uvicorn for the
agent-test job. The test imports through the real slime_plugins
package, whose __init__ chain needs them; all five are in
requirements.txt but none are in the job's hardcoded install line.
Uses the template's per-job extra_pip_deps hook so the other CPU
job's install is untouched.
Verified in a clean venv built from exactly the generated dep list: all
seven tests in agent-test pass, 45 of them from this file.
CI pins 3.10, where asyncio.timeout() does not exist -- it is 3.11+. The two generate() plumbing cases hit rollout.py's `async with asyncio.timeout(...)`, whose AttributeError is then swallowed by the rollout's broad except into an abort, so the harness never runs and the tests fail on a missing capture rather than on the real cause. Shim it onto a pass-through context manager, mirroring test_agent/test_agent_rollout_cpu.py, which already does this for the same reason. The wall-clock guard never fires in these tests. Verified on a 3.10.20 venv with exactly the generated CI dep list: all seven tests in agent-test pass. The earlier local verification used 3.14, which is why this only surfaced on CI.
|
Features are basically developed per our agreed requirements, and both deliverables and outcomes have been reviewed. We suggest running an AI review to check code details. |
Tsing-git
left a comment
There was a problem hiding this comment.
Review Summary
Large PR (39 commits, +8266/−175, 50 files) that adds an AGS-backed rollout-buffer generator for training coding agents on SWE tasks, plus several important core slime fixes exposed by this new workload.
Verdict: Approve with suggestions — the code is well-tested (~2100 lines of new tests) and the commit history is unusually well-documented. A few concerns below.
Key Findings
Positives
- Excellent commit messages — every fix commit explains the root cause, how it was measured, and why the chosen fix is correct. This is a reference-quality commit log for a systems project.
- Good isolation — the AGS generator is entirely self-contained under
slime_plugins/, with clean entry points. Core slime changes are minimal and well-scoped. - Defensive engineering —
empty_patch_guard.pydefaults to metrics-only (no reward corruption),terminate_process_groupfails loudly on leak, stale job writes are rejected byrollout_job_id.
Concerns
-
PR size — 39 commits across 50 files makes review hard. This is more of a feature branch merge than a single PR. Consider splitting future work into adapter fixes, core metric fixes, and the AGS generator as separate PRs.
-
Launcher duplication —
run_cc_qwen35_35b_a3b_swe_2nodes.shand the 4-node variant share ~370 lines verbatim. The CBC wrappers partially address this, but the CC 2-node/4-node scripts themselves will drift. A shared base script sourced by both would reduce this risk. -
Sampling priority inversion (
common.py) — the fix makesopen_sessiondefaults outrank request body values, which fixes codebuddy'stemperature=1problem. But this is a semantic inversion: normally request-level params should override session defaults. The choice is correct for this use case but should be documented as intentional in the docstring, since a future adapter consumer might expect the opposite. -
compute_pass_ratesilent degradation — replacing the assert with a warning +return {}means a misconfigured run silently reports no pass@k rather than failing loudly. Consider at least logging at ERROR level so it's not missed in long training logs. -
Offload race window (
sglang_engine.py) —pause_generation()is called insiderelease_memory_occupation()but there's no guarantee in-flight requests complete before the memory release proceeds. If sglang's pause is not synchronous w.r.t. in-flight batches, this could hit a use-after-free on the KV cache. Worth confirming sglang's contract here. -
Test CI coverage —
test_ags_generator.pyhas 1170 lines and is now registered in CI, but the asyncio.timeout shim suggests it was only validated on 3.14 locally before the last commit. The CI matrix should ideally run 3.10 and 3.12+ to catch these gaps earlier.
Minor Nits
requirements.txtaddsweavewithout a version pin — this could break in a future weave release.- The
run_cc_*.shscripts hardcode paths under/data_train/ericxjzheng/as defaults — these should probably be documented as needing override, or moved to a.env.example. _REASONING_KEYSinopenai.pyis a good pattern; consider extracting the key-normalization logic into a shared utility if more adapters need it.
Overall this is solid systems work with clear measurement-driven fixes. The main ask is to keep future PRs smaller and to document the sampling-priority decision more explicitly.
A training step whose micro-batch count is not a multiple of dp_size was aligned only by splitting multi-sample bins. That is impossible in long-context runs: once a single sample fills max_tokens_per_gpu * cp_size, first-fit emits one bin per sample and there is nothing left to split, so an odd sample count raised and killed the run. This happened after the rollout phase had fully succeeded -- 48 trajectories graded, patches applied cleanly, abort rate 0 -- so a rollout's worth of sandbox compute was discarded, and no AGS metric warned of it. The sample count is unpredictable because auto-compaction lets one rollout emit several training samples (48 rollouts produced 62 to 77 samples), which makes per-step parity a coin flip. Round the count down to the nearest multiple instead, keeping every sample. Rounding down is only possible with at least align_to bins; below that the floor is 0 micro-batches, so raise there with an error naming the real cause rather than blaming the repack. Repack with longest-processing-time-first so the largest resulting bin stays as small as possible, since that bin sets peak activation memory. Merging the two smallest bins repeatedly is the intuitive rule but degrades once more than one merge is needed: [49000, 49000, 90000, 90000, 90000] merged to 3 bins peaks at 180000 tokens versus 139000, i.e. 1.88x against 1.45x a 96000 cap. The two agree at exactly one merge, which is the dp_size=2 case. Merged bins can exceed the token cap, unavoidably, because first-fit is already a maximal packing; max_tokens_per_gpu becomes a target on this path and the repack is logged. Lowering it cannot relieve the overflow -- a merged micro-batch holds whole samples, so its token count is set by the data -- so the warning points at rollout_max_context_len and the batch shape instead. Tests use the token lengths and rollout ids recovered from the run that crashed, and cover the peak-memory property and the below-threshold error.
Summary
Adds an AGS-backed rollout-buffer generator for training coding agents on SWE tasks, plus the core-slime fixes that path exposed.
The generator boots one AGS sandbox per rollout, runs a real coding-agent CLI (Claude Code or CodeBuddy Code) against slime's adapter, scores the resulting patch with the task's own eval command, and returns on-policy token-level training data. 39 commits, +8231/-175 across 50 files.
What's here
New: AGS rollout-buffer generator (
slime_plugins/rollout_buffer/generator/ags_generator/, 15 modules)rollout.py/entry.py— concurrent rollouts with per-rollout session isolation;SWE_ROLLOUT_CONCURRENCYandSWE_BOOT_CONCURRENCYbound sandbox boot and agent execution separatelyharnesses.py— harness registry for Claude Code (Anthropic adapter) and CodeBuddy Code (OpenAI adapter), each with exactly two override knobs (SLIME_AGENT_{CC,CBC}_EXTRA_{ARGS,ENVS})swe_task.py/source.py— SWE task setup, patch extraction, eval-command execution, and prompt-source progression that survives requeue and epoch rolloverwandb_metrics.py/weave_trace.py— rollout and eval metrics to W&B, plus per-agent Weave trace parsing (payloads gzip+base64-encoded)empty_patch_guard.py— classifies rollouts that exit 0 with an empty diff (a text-only final turn where no tool call was parsed). Attribution only by default: an empty-patch trajectory is still valid on-policy data whose zero reward is correct.SWE_EMPTY_PATCH_GUARD∈{off, metrics, abort}adapter_service.py— lets eval stand up its own adapter instead of requiring the training oneNew: Harbor task converter (
tools/harbor_task_to_slime_prompt_data.py)Converts Harbor SWE tasks to slime prompt-data JSONL, extracting the fields the generator consumes (
instance_id,image,workdir,problem_statement,eval_cmd). Prompt andproblem_statementare both Harbor's renderedinstruction.md— the exact string Harbor hands its own agents — with the rawtests/config.jsontext kept undermetadata.harborfor provenance.New: launchers (
examples/coding_agent_rl_ags/) — 2-node and 4-node Qwen3.5-35B-A3B SWE training for both harnesses, plus eval-only scripts for scoring a single checkpoint.Core slime changes
These are not incidental — each fixes a defect that only agentic rollout surfaces.
slime/agent/adapters/openai.pytool_calls. Droppingreasoning_contentmade the re-render stop reproducing the sampled ids, so_SampleBuildersaw token drift and either REALIGNed a real trained response toloss_mask=0or forked the trajectory — measured at ~25 samples per rollout where Claude Code produced 1. Also reads reasoning under eitherreasoning_contentorreasoning, since the spec standardised neither and CodeBuddy echoes ours back under the other key.slime/agent/adapters/common.pytemperature=1on every request, so--eval-temperaturenever reached sglang; the CLI's value now applies only whereopen_session'ssampling_defaultsleft that key unset. Adds/_slime/{open,finish,drop}_sessioncontrol routes for out-of-process session management.slime/utils/data.pymultimodal_keys, not on the mere existence of a processor.AutoProcessorreturns one for any VL-capable checkpoint (Qwen3.5-35B-A3B yields aQwen3VLProcessor), which forced a conversation-shaped prompt on text-only datasets and made--apply-chat-templatemandatory — which in turn rewroteSample.promptinto a literal<|im_start|>user ...string that agent rollouts handed straight to the CLI.slime/utils/metric_utils.py,slime/backends/megatron_utils/data.pycompute_grouped_pass_rate: agentic rollouts emit multiple train samples per attempt (one per root-to-leaf chain), and those fan-out siblings are training segments, not independent pass@k samples. Deduplicates by(group_index, rollout_id)before computing pass@k. The fixed-shape path now warns and returns{}instead of asserting.train.pyslime_plugins/rollout_buffer/buffer.pyrollout_job_id, and writes carrying a stale id are dropped so a previous job's late data can't leak into the current training rollout. ReplacesBackgroundTaskswith an explicitRolloutJobthread that can be stopped and waited on.slime/ray/rollout.pyoffload()collects handles acrossserver_groupsandray.gets them, so offload actually completes before training proceeds.slime/backends/sglang_utils/sglang_engine.pyslime/agent/sandbox.pyterminate_process_group()— SIGTERM the detachedsetsidgroup, wait out a grace period, then SIGKILL, and fail loudly if anything survives. Leaked agent background processes otherwise outlive the rollout.slime/rollout/data_source.pyn_samples_per_prompt(fan-out) instead of asserting exact equality; assert element type instead.slime/utils/arguments.py--rollout-buffer-num-epoch,--rollout-buffer-stop-timeout-sec,--enable-token2textrequirements.txtweaveTests
~2100 lines of new CPU-only tests:
test_ags_generator.py(1147),test_ags_empty_patch_guard.py(239),test_ags_prompt_source.py(152),test_harbor_task_to_slime_prompt_data.py(150),test_passrate_metrics.py(97), plus adapter test expansion (test_adapters.py+186, new_fakes.py).test_ags_prompt_source.pyandtest_ags_empty_patch_guard.pyare registered in thenum_gpus: 0CI matrix (pr-test.yml+.j2). The other new tests are not yet wired in.Testing
Not run at PR-refresh time — no pytest available in the authoring environment. The AGS generator itself has been exercised on 2- and 4-node Qwen3.5-35B-A3B SWE runs on H20, which is where the adapter, sampling, pass@k, and offload findings above were measured.
Note on the base branch
This targets
main, which currently also serves as the fork's mirror ofTHUDM/slime. Merging 39 commits into it permanently diverges the mirror and makes every future upstream sync a two-way merge. Consider retargeting to a long-lived integration branch and lettingmainstay fast-forward-only (upstream-syncnow tracksupstream/mainfor this purpose).