Chutes Build tracks xai-org/grok-build. As of 1.0.3 the relationship is a
manual port: read upstream's diff, decide per area, and carry across what is
worth carrying. Never git merge upstream/main.
Important
This reverses the model 1.0.0 adopted, and the reversal was paid for. Merging
means taking upstream's side wherever there is no conflict, and "no conflict"
is exactly where a fork's identity lives: the same file, quietly replaced. The
1.0.0 merge shipped Grok's wordmark on the splash, upstream's flag names in
--help, OAuth scopes the Chutes IdP rejects so sign-in could not complete, and
a single hardcoded context window for all thirteen models. Every one of those
compiled and passed a green gate. Three releases went to fixing them.
Upstream publishes no releases and no tags — its commits are squashed drops
named "Synced from monorepo", and its product changelog lives at
x.ai/build/changelog, off GitHub. So there is no version to "take": there is a
diff to read.
The old procedure was "port selectively, never merge upstream wholesale". It
was followed, and it still produced a fork that drifted: review records read
"nothing safe to port in isolation" and "several candidates identified, none
ported yet", so .github/upstream.json recorded a reviewed baseline of 0.2.117
while the retained crates were still at the 2026-07-17 fork point — 2075 files
and roughly 440k lines behind. Every individual port then had to be grafted into
files that were thousands of lines stale, which is why so many of them found our
copy older than the baseline rather than merely different.
Cherry-picking cannot converge on a project that adds 94k lines in five days.
So 1.0.0 re-based onto upstream's tree and re-applied the Chutes layer on top.
That layer is thin — 102 files exist only here, and only 26 consume
chutes-build-core — which is what makes the merge model viable.
git fetch upstream main
git diff --stat <last-ported-ref>..upstream/main # what moved, and whereGroup the diff by area and take one area per commit, so each can be reverted on its own. Three areas need judgement rather than a decision rule:
- The 26 seams — the files that consume
chutes-build-core: the routing and endpoint policy inxai-grok-sampler/src/client.rs, the agent config / init / models trio, the privacy and kill-switch call sites inxai-grok-shell/src/extensions/,leader/server.rs, the session and pager surfaces, andxai-grok-memory/src/storage.rs. Resolve these by hand, every time. They are the product. - Branding — re-run
python scripts/rebrand.py --apply, then--check. The script carries an explicit map and fails on any forbidden token left outside its allowlist. It also reports four ambiguous token families it will never rewrite;scripts/rebrand_files.jsonholds the per-file decisions and can be regenerated. - Deliberate divergences — listed below.
Everywhere else, judge the hunk. "Not in one of those three categories" is not a licence to take upstream's version — that reasoning is what shipped the wrong wordmark, since a logo file is in no category and conflicts with nothing. Take a change because it is worth having, not because nothing objected.
Then — and this step is not optional, it is where the 1.0.0 re-base leaked — run the seam sweep against the release you are porting from:
python scripts/seam_sweep.py --base <previous-release-ref>A clean port and a green gate do not mean the seams survived: a constant whose value came across from upstream compiles fine and its tests assert against the constant. See the sweep section in the re-base record below for what that cost.
The sweep reads source. It does not read assets, and 1.0.0 shipped upstream's Grok wordmark because both logo files came across untouched — invisible to the compiler, to the tests and to the sweep, and visible to every user at startup. After a port, compare the asset tree too:
git diff --stat upstream/main -- '**/assets/**' # anything identical is suspectA test now pins the wordmark's hash. Nothing pins the rest.
Finish with the behavioural checks the gate cannot do: --version, --help,
models, du, and a confirmation that nothing was written outside
$CHUTES_BUILD_HOME.
- Never
git add -Aafter running the test suite. It has committed live API keys to this branch once already: a test that shells out toenvwrites the whole environment somewhere, and on Windows that somewhere turned out to be the crate root. Stage deliberately, and read what you staged. See the leak record below. - No telemetry, remote error reporting, automatic upload, upstream session
sharing, remote workspace exposure or phone-home updates. These are enforced
by the compile-time constants in
crates/chutes-build-core/src/product.rsconsumed at their call sites — not by deleting the crates, which would make every future diff unreadable. - Never send ambient Chutes credentials to an endpoint that has not passed
chutes-build-core::endpoint_policy. Env-overridable base URLs (CHUTES_ROUTER_BASE_URLand friends) are the specific hazard. - Hostnames are logic, not branding.
sampling/error.rsmatches the literalgrok.com/supergrokon the way in, to recognise an upsell the upstream server sends and suppress it for API-key users. Rewriting that string passes every test and silently starts showing team users a personal-subscription pitch. - Rust identifiers are not renamed.
grok_home(),default_grok_home()and thexai-grok-*crate names stay; only the strings they return become Chutes. Renaming them makes upstream diffs unreadable for no gain. - The model catalogue is read, not assumed.
llm.chutes.ai/v1/modelspublishescontext_length,max_output_length,supported_featuresandinput_modalitiesper model; upstream's parser looks for none of those names. If a port touchesparse_remote_model_value, check those fallbacks survived.
crates/chutes-build-core/andxai-grok-tools/src/implementations/chutes/are ours outright.- Chutes routing: the virtual
model-routermodel,CHUTES_FALLBACK_MODELS, and the per-family reasoning normalisation. CHUTES_EXTRA_CA_BUNDLEsits outside theCHUTES_BUILD_*family on purpose: it configures the transport, not the product shell.cpk_in the secret redactor, and the memory privacy filter.- The
chutesnight/chutesdaythemes replace upstream's. - A vendored
protocfallback infind_protoc, becausebin/protocis a DotSlash wrapper Windows cannot execute from a build script. - The endpoint table in
xai-grok-env: the relay and gateway WebSocket URLs are loopback sentinels, not upstream hosts. Chutes Build has no vendor relay and no cloud-sandbox control plane, so those paths must fail closed rather than resolve. LEGACY_AUTH_SCOPE/LEGACY_SCOPEaredisabled::legacy-auth: the pre-OIDC relay login is disabled by pointing its scope at a key no server can mint, rather than by deleting the code path.- The changelog fetch and the update check point at a closed loopback port. The callers stay so merges stay clean; the requests fail locally and instantly.
CodingDataSharingLock::ProductPolicyand the privacy banner's floor: the coding-data row is locked with a reason instead of removed, and the banner never renders. The extension behind them already refuses the call — this is so the product never offers what it will then refuse.
Note
Superseded by the manual-port model at the top of this file. This section
reasons about merging upstream/main into a branch and squashing onto main,
which is no longer how syncs are done. It is kept because the constraint it
describes is real and would return the moment anyone reached for a merge again:
main forbids non-linear history, so a merge cannot land there, and a squash
keeps upstream's commits out of the history the next merge base is computed
from. A manual port has neither problem — it produces ordinary commits.
main currently forbids non-linear history. That rule and the merge model above
are incompatible, and the incompatibility is not cosmetic.
A squash-merge of an upstream sync produces the right tree but keeps
upstream's commits out of main's history. The next git merge upstream/main
would then still compute its merge base at the original 2026-07-17 fork point
and replay the entire divergence as conflicts — which is exactly the failure
this re-base exists to end. Squashing works once; it does not compose.
For the merge model to hold, main has to accept merge commits from
upstream/main. That is a repository-policy decision, not a code one, and it
should be made before the first sync lands. If the linear-history rule is kept
instead, the honest thing is to say so in this document and go back to
cherry-picking with eyes open, rather than discover it at the next sync.
The linear-history rule was kept, and 1.0.0 landed as one squash commit
(2ece18b) whose parent is the previous main and whose tree is the re-based tree.
Relaxing the rule to push a merge was the other option and was not taken.
So the warning above now applies to this repository as it stands: main's history
does not contain upstream's commits, and git merge upstream/main run on main
would compute its base at the 2026-07-17 fork point and replay everything as
conflicts.
The model still composes, on one condition: never merge upstream into main.
Merge it into a branch that carries upstream's history, and let main receive the
squash:
# The integration branch already contains upstream's history. Do not delete it.
git checkout rebase/upstream-1.0.0
git merge upstream/main # merge base is correct here
# ... resolve the Chutes seams, run the gate, then land the tree on main:
tree=$(git rev-parse rebase/upstream-1.0.0^{tree})
commit=$(git commit-tree "$tree" -p origin/main -F message.txt)
git push origin "$commit:refs/heads/main"rebase/upstream-1.0.0 is pushed and is that branch today. Renaming it to something
durable — integration/upstream — would say what it is for, but it must not be
deleted as a stale feature branch: it is the only place upstream's history is
reachable from, and deleting it puts the next sync back at the fork point.
Two things follow. main is a squash-only release branch, and its commits will not
correspond one-to-one with the work; CHANGELOG.md and this file are the record
instead. And the choice is still open: relaxing linear history removes the whole
dance, at the cost of merge commits in main.
Update .github/upstream.json (lastReviewedCommit, lastReviewedVersion,
reviewedAt) only after the port is complete and the gates pass — the
field means "this is the upstream this tree is", and its previous drift from
reality is what made the re-base necessary. Record user-visible changes in
CHANGELOG.md and add a record below.
The daily Upstream watch workflow reads the version from the Cargo manifest
named in versionManifest. Note that upstream publishes no GitHub releases
and no tags — both endpoints were empty even at 1.0.0, and the major bump
arrived inside an ordinary "Synced from monorepo" commit. The release/tag
comparison is not evidence that upstream has not moved.
The tree was re-based onto xai-org/grok-build 1.0.0 (afbc0fb) rather than
continuing to cherry-pick, for the reason in "Why this changed" above.
Measured before starting, HEAD against upstream/main: 102 files existed only
here, 26 consumed chutes-build-core, 860 diverged only by branding, 89 were
our own non-branding fixes, and 588 diverged purely because upstream's work had
never been taken. That last number is the one that decided it.
Sequence: the Chutes layer (77 files — the other 25 "only ours" turned out to be upstream files upstream had since restructured away), the mechanical rebrand (5944 lines / 766 files), the ambiguous tokens decided per file (2634 / 443), then the seams.
Four fixes had to come forward because upstream has no equivalent:
detect_probable_secret and the cpk_ prefix in the redactor, the
MediaArtifact output type, the vendored protoc fallback, and the proto
dependency scan writing to a temp file instead of /dev/stdout.
Three defects the compiler found that review had not:
chutes-build-pluginmatched inside the crate namexai-grok-plugin-marketplace, renaming a workspace member.- Bare
grokwas renamed in 25 files where it was a Rust identifier, producinglet chutes-build = .... The per-file classifier had read "the fork has zero occurrences" as "the fork renamed them all", when sometimes the fork had simply deleted the code they lived in. xai-grok-pager-pty-harnessimportedprocess_has_exited_without_reapunconditionally while the function and every call site are#[cfg(unix)]— an upstream bug visible only on Windows, worth sending back.
Followed upstream, dropping features it removed: project_picker, the six
bundled SKILL.md files, search_remote_sync, upload/config_files,
minimum_version, and the coordinator_lifecycle / coordinator_query split.
A pristine upstream/main worktree was built on the same Windows machine and
measured, because upstream fails a lot of its own tests on this platform and
without that baseline the failure counts here are unreadable:
| Suite | pristine upstream 1.0.0 | this tree |
|---|---|---|
xai-grok-pager --lib |
82 failures | 82 — identical sets |
xai-grok-shell --lib auth:: |
24 failures | 24 — identical sets |
Zero regressions. The table stands as measured; the conclusion drawn from it does not. "Not ours to fix" held only while nobody looked: on 2026-08-10 the pager's sixty-six were worked through and five turned out to be defects a Windows user would meet, not platform gaps — see "Closing the Windows failures" below. Inherited failures are worth reading one at a time before they are filed under the platform.
The upstream failures are Windows platform gaps (path
separators, #[cfg(unix)] helpers, shell-alias planners) and are not ours to
fix here; three of them that broke compilation rather than assertions were
fixed and are worth sending back: process_has_exited_without_reap and
parse_login_env_capture are #[cfg(unix)] while their importers and tests are
not, and the proto dependency scan writes to /dev/stdout.
clippy.toml now disallows tokio::process::Command::spawn — "an unenrolled
child outlives its session". The Chutes browser tool spawned headless Chrome
exactly that way, so every session that was not cleanly torn down left a browser
behind. It now enrols in the global process scope. This is the clearest argument
for the re-base: three weeks of cherry-picking never surfaced it.
The gate proves the tree compiles and its tests pass. It does not prove the product is Chutes Build. Running the built release binary found five things that were green in CI and wrong in the product:
--versionprintedgrok 1.0.0— the name is a literal inversion_text.Usage:printedgrok, becauseparse_clifiltersargv[0]against a known launcher allowlist and fell back to"grok"— our binary was rejected by our own allowlist.- CLI help read "Run Chutes Build without the interactive UI", "Sign in to Chutes Build". 983 occurrences of the bare product word had no rebrand rule at all.
- The model catalog offered
grok-4.5, which Chutes does not serve.default_models.jsonhad come across from upstream. - The notification hook's test wrote
CHUTES_BUILD_MESSAGE=labels while reading$CHUTES_BUILD_MESSAGE.
So the verification list below is not optional ceremony. At minimum, after every
sync: --version, --help, models, du, and a check that nothing was written
outside $CHUTES_BUILD_HOME.
Four of the bugs were in rebrand.py itself. The one worth remembering: a rule
and the identifier repair oscillated — one rewrote a path, the other reverted it
— and the script reported "0 files changed" while the tree sat in the wrong
state. A silent stable-but-wrong fixpoint is worse than a loud failure. Verify
idempotence by running the script twice and confirming the second run changes
nothing, not by trusting one clean report.
Phase 3 identified the Chutes seams by asking which files import
chutes-build-core. That question has a blind spot: a file whose structure is
upstream's and whose values are ours. scripts/seam_sweep.py asks the other
question — it compares the values the previous release shipped against the
re-based tree — and found 55 identity divergences the first question could not
see. The ones that would have shipped:
| What | Was, after the re-base | Should be |
|---|---|---|
| default inference endpoint | https://api.x.ai/v1 |
https://llm.chutes.ai/v1 |
| relay / gateway WebSocket | wss://code.grok.com/…, wss://grok.com/ws/gw/ |
loopback sentinels |
| API-key auth.json scope | xai::api_key |
chutes::api_key |
| OAuth issuer + client id | auth.x.ai, upstream's compiled-in app |
api.chutes.ai; client id must be configured |
| session update/close method | _chutes.build/session/* |
_chutes.build/session/* |
| npm package checked for updates | @xai-official/grok |
chutes-build |
Run it after every sync, in both modes:
python scripts/seam_sweep.py --base <previous-release-ref>
consts mode reads like a table and is precise. literals mode is noisy but
sees values inside struct initialisers — which is where the endpoint table hid
while consts reported the file clean. Neither mode decides anything: several of
the fork's values are simply older, and several of the re-based tree's are
deliberate improvements. The output is for a person to read.
Two lessons worth keeping:
- A wire name renamed in the handler but not the caller fails silently.
`` does not match after an underscore, so
_chutes.build/session/updatewas invisible to the pass that normalised 1034 other ACP names. - A constant's value is not covered by the test that names the constant.
Every test around
XAI_API_BASE_URL_DEFAULTasserted against the constant, so the endpoint could point anywhere and the suite stayed green.
Important
Superseded for the pager, 2026-08-10. xai-grok-pager --lib now passes on
Windows CI: sixty-six failures closed, five of them product defects rather than
test bugs — see "Closing the Windows failures" below. The comparison-against-
upstream advice no longer applies to that crate, because its baseline is zero
and any failure is a regression. The auth:: line below also does not match
measurement: this tree fails 24 there on Windows, for the shell-dialect reason
recorded in the same section.
Measured against a pristine upstream/main worktree on the same machine, so
these are not re-base damage:
xai-grok-pager --lib: 82 failures against this tree's 79, stable across four consecutive runs. One of those runs had previously produced an 80th: a quote-bar test compares a span's style against the theme's, both read from the process-globalTheme::current()that theset_themetests mutate. It holdspin_theme()now, like the layout tests that had already met this.xai-grok-shell --lib auth::: 24 — all 24 of which this tree now passes.xai-grok-shell --libcannot be run whole on Windows: it overflows a 1 MB thread stack partway through the session tests, in the debug profile only. That is why the gate runsauth::rather than the crate.agent::configwas checked separately and passes 334/334 after twelve fixtures were moved onto a first-party host — upstream's asserted thatapi.x.aiis first-party and that a session token attaches toexample.com, both of which are false underendpoint_policy. The fork had already rewritten every one of them; the re-base kept upstream's. Anything else in that crate's non-auth::tests is unmeasured here, and that is a gap, not a clean bill.xai-grok-agent --lib: upstream fails 5 here; this tree passes all 578. The five were worth fixing rather than documenting, and two of them were not test bugs:- Two skills tests compared a
Stringpath against a/-spelled suffix. Normalised, not skipped — the behaviour matters on Windows too. resolve_treats_home_git_repo_as_no_reposetHOMEand expecteddirs::home_dir()to read it. On Windows that callsknown_folder_profile, so no environment guard can redirect it. The comparison the guard performs is now a separate function, tested on every platform; the unreachable line is the lookup itself.refresh_skips_untrusted_source_outside_homewas pointing at a real hole: plugin sources are auto-trusted when under the user's home, and on Windows the system temp directory is under the home (%USERPROFILE%\AppData\Local\Temp) — so anything unpacked into temp was auto-trusted, skipping the explicit trust step. Temp is now excluded, and the five refresh-mechanics tests grant trust explicitly instead of inheriting it from wheretempfilehappened to allocate.test_encrypted_templates_not_stalefails upstream too: their checked-in encrypted prompt bytes are stale. Ours are regenerated.
- Two skills tests compared a
- Upstream 1.0.0 cannot build on Windows at all:
bin/protocis a DotSlash wrapper (not a valid Win32 application) and the proto build script writes to/dev/stdout. Both are fixed here, and the fix is what let the baseline above be measured at all.
Every check above passed with the credential flows listed as unverified. Then a
real cpk_ key went in, and the first thing it proved was that the key was not
read at all: XAI_API_KEY_ENV_VAR was still "XAI_API_KEY" while every error
message, every doc page and the fork itself said CHUTES_API_KEY. A user
following the instructions would simply never authenticate. Same for the
auth-method id xai.api_key, which is what preferred_method takes in
config.toml. 304 occurrences across 69 files.
Three reasons the sweep had missed it, each now closed:
constsmode compares constants by name. The fork had renamed the constant itself (CHUTES_API_KEY_ENV_VAR), so the name intersection was empty and there was nothing to compare.- The pattern was per-line. rustfmt had wrapped
CLI_BASE_URL_FALLBACK's value onto the next line, hiding a third update channel that pointed at a non-existent GCS bucket — soupdate --checkmade a real request tostorage.googleapis.comand reportedNoSuchBucket. Allowing whitespace, newline included, around the=is the whole fix. - I filtered the
literalsoutput by upstream hostnames.XAI_API_KEYcontains no host, so my own triage dropped it from a report that had found it.
Then 570 places told the user to run grok login, grok doctor, grok wrap …
— in help text, error messages, the bundled user guide and doc comments. An
instruction naming a binary that does not exist is worse than no instruction.
Now a rebrand rule, anchored on a following subcommand or flag so the internal
grok_home / xai-grok-* / implementations/grok_build/ names stay put.
What the live run confirms:
- API-key auth works, and the model catalogue comes from
https://llm.chutes.ai/v1/models— 15 Chutes models plus themodel-routervirtual entry. - A real turn through the Auto router completes: ~12.9 s wall clock end to end, first text event at ~13.2 s in the streaming run (a cold process start, model resolution and routing included), then steady ~80 ms token cadence.
- Only three hosts are contacted, all Chutes-ecosystem:
api.chutes.ai,llm.chutes.ai,model-router-ten.vercel.app. No call tox.aiorgrok.com. - Nothing was written outside
$CHUTES_BUILD_HOME.
One more thing the key surfaced: a variable exported from a CI secret that does
not exist arrives as the empty string, and blank counted as set — so the
product announced "You are using CHUTES_API_KEY" and then failed to
authenticate, which is the least helpful pair of messages available. Blank now
counts as unset, matching the "first set, non-blank value wins" rule the
per-model env_key list already followed.
One thing to decide, not a defect: model-router-ten.vercel.app is in
TRUSTED_HOSTS in chutes-build-core, which is ours — so it is allowed to
receive an ambient Chutes credential, and resolve_inference_base_url routes
session (OAuth) inference through it. API-key auth goes straight to
llm.chutes.ai and never touches it. Whoever operates that Vercel deployment can
see the traffic that does go there. It was the fork's own choice; it deserves a
conscious confirmation rather than inheritance.
The 89 non-branding files we had touched, decided one at a time against the new tree:
Came forward — upstream has no equivalent: detect_probable_secret and the
cpk_ prefix in the redactor; the MediaArtifact output type; the vendored
protoc fallback and the proto dependency scan writing to a temp file rather
than /dev/stdout; the checkout-independent encrypted prompt templates
(regenerated — upstream's own checked-in bytes are stale, and the rebrand changed
the template text); voice and batch STT against the Chutes whisper endpoint; the
native Advisor; API-key login inside the TUI; CHUTES_EXTRA_CA_BUNDLE; the
unified secret detection; and the memory owner-only write paths.
Correction — four of those came forward as files, not as features. This entry
originally read as above and was wrong in the same way .github/upstream.json was:
it recorded an intention as an outcome. Copying a file is not porting a feature, and
a file that no mod declaration reaches is not even compiled, so nothing failed:
- Batch STT (
pcm.rs,stt/batch.rs) was copied and never declared.SttMode,batch_api_baseand the pipeline's batch branch were not copied at all, leaving voice with only upstream's streaming transport — pointed by the rebrand at anapi.chutes.ai/v1/sttroute Chutes does not serve. - The native Advisor was one file,
slash/commands/advisor.rs, undeclared. The built-in agent, its toolset, its prompt, the config writers and the two settings actions were absent. - API-key login inside the TUI was one file,
slash/commands/apikey.rs, undeclared, and everything it calls was missing. CHUTES_EXTRA_CA_BUNDLEdid come forward, but through upstream's newxai-grok-extra-cacrate rather than the fork's module — which was left behind as dead code a reader would have taken for the live one, and is now deleted.
All four are fixed as of 1.0.0. scripts/dead_modules.py is the check: it lists
source files no mod declaration reaches, honouring #[path] and include!, and
must return nothing. Run it after any file-copying phase of a future re-base — the
"copied but unregistered" failure is silent by construction, and this re-base hit it
in four separate registries (toolsets, guide chapters, slash commands, and the
shell's reserved-name list).
Deliberately not carried: trace_classifier and the trace_classify
binary — 3278 lines of offline trace analysis with no consumer in the product.
The trace subcommand does not use them (verified: no reference from
trace_cmd.rs on either side), and laziness_classifier only mentions them in
doc comments. They remain a clean copy away in main if the analysis is wanted
again.
Dropped with the code they covered, because upstream restructured or removed
it: the Windows CI stabilisations in ptyctl and xai-crash-handler (upstream
fixed them differently), the permission/exec-risk cluster (upstream carried it
past our version), and the retry/permission work — which arrived with the re-base
itself. The toolchain pin follows upstream: the
fork had bumped to 1.94.1, upstream 1.0.0 is on 1.94.0, and the whole gate is
green on 1.94.0 — so there is nothing left to hold the extra point version
open.
Two live Chutes API keys were committed to this branch, in files nobody wrote on purpose.
The notification-hook tests interpolated an unquoted Windows path into sh -c.
Under Git Bash the backslashes were eaten as escapes, so env > C:\Users\...\env.txt
became env > CUsersMike...env.txt -- a redirect into the crate root. And
env prints the whole environment, so each of those files held whatever was
exported, including CHUTES_API_KEY. Thirty such files accumulated across the
branch; ten carried a real key. A git add -A swept them into seven commits.
output/ went the same way: 42 generated media files, one of them a latency log
with a second key, tracked on this branch only -- never on main, never
upstream.
The remediation, in order:
- The tests were fixed to quote the redirect target and hand the shell forward slashes, so no new dump can be produced.
git filter-branch --index-filterpurgedoutput/and every*env.txtfrom the 17 commits of this branch. Verified afterwards: zero commits carry acpk_-shaped string, zero of the 10869 remaining blobs do, and the diff against the pre-purge tree is exactly the purged paths and nothing else.refs/originaldeleted, reflogs expired,gc --prune=now-- the blobs are out of the local object store, not merely unreferenced..gitignorenow covers/output,CUsers*env.txtand*AppDataLocalTemp*. The tests are the fix; this is the second line of defence, because agit add -Ashould never be able to sweep up a credential.- Both keys were rotated by their owner.
The branch had never been pushed, which is the only reason this was recoverable. Two lessons that are not about git:
- A test that dumps
envis a test that dumps credentials. The bug looked cosmetic -- stray files with silly names -- and was tidied away twice before anyone asked what was in them. git add -Aon a tree known to produce stray files is not a shortcut, it is a hazard. Those files had already been noticed and deleted; that was treated as housekeeping rather than as the signal it was.
Phase 1 copied the implementations. Phase 3 reapplied the seams that consume
chutes-build-core. Neither step asked the third question — which toolset lists
these tools — and the answer was none of them. generate_media,
list_media_models, describe_media_model, browser, ocr_page,
get_chutes_usage: some six thousand lines that compiled, whose unit tests
passed, and which the model could never call.
Upstream's toolsets carried its own Imagine tools instead (image_gen,
image_to_video, reference_to_video), which talk to an xAI endpoint Chutes has
no equivalent for. Their own tier message already said so: "This legacy image tool
is unavailable. Use the native generate_media tool with a capable Chutes model."
That also explains /imagine. The pager hides a slash command whose
required_tools() are not advertised, so /imagine and /imagine-video had
quietly disappeared — they gated on tools no Chutes deployment has. Both now gate
on generate_media, and their injected instructions teach the catalog workflow
(list_media_models → describe_media_model → generate_media, composing the
payload from the cord's own example) because on Chutes the model is a catalog
entry rather than a fixed endpoint, and FLUX, Qwen-Image, Wan, LTX and the TTS
models share no schema. /imagine-video needed rewriting rather than renaming:
upstream's text opens with "there is no text-to-video tool", which is true of that
API and false here.
Restored with it: ensure_chutes_tools for agents whose toolset came from a
config file rather than a preset, and the lazy-discovery pair
(take_lazy_chutes_tools / register_lazy_chutes_tools) that keeps these tools
off the per-turn schema list while still registering them on the bridge for
use_tool — taking them off without the second half would just delete the
feature.
Two tests now assert what nothing asserted before: that the ecosystem tools are in the default presets, and that the legacy Imagine tools are not. A tool's own unit tests pass whether or not anything can reach it, which is exactly how this survived a green gate.
Verified live against Chutes with a real key: search_tool returns
chutes__list_media_models, chutes__describe_media_model,
chutes__generate_media, chutes__ocr_page and chutes__get_chutes_usage with
their real descriptions, and use_tool invoked the catalog with
{"kind":"image"} — 20 requests to api.chutes.ai, none to any xAI host. Before
this change there was no chutes server in the session at all.
Not verified: a completed generation. The headless runs end after two tool
calls — the router model narrates its plan and stops rather than carrying the
chain through to generate_media. That is a headless turn behaviour rather than
evidence about the media path, and /imagine is a TUI command in any case, but it
means the last step of this feature is unproven here and should be exercised by
hand in the TUI.
The docs were checked against the code, not proofread. That direction matters: it turns the documentation into a set of assertions about the product, and one of them was true while the code was not.
PRIVACY.md promises the ambient Chutes credential is "never reused for a custom
endpoint" and that "a custom model-catalog endpoint uses CHUTES_MODELS_API_KEY".
The re-base had dropped that variable's entire auth arm, so a custom endpoint fell
into the CHUTES_API_KEY branch: pointing CHUTES_BUILD_MODELS_LIST_URL at any
host sent it your Chutes credential during startup model discovery. The promise was
the thing that caught it.
The rest was documentation being wrong about a correct product, and worth listing because the shapes recur:
- Vendor attribution. The README presented this fork as SpaceXAI's, under their
logo;
CONTRIBUTING.mdcarried their "no external patches" policy;SECURITY.mdrouted vulnerability reports to xAI's HackerOne programme. None of these could be caught by any rebrand rule: "SpaceXAI" is upstream's own new name for itself, so noGrok-derived pattern touches it. - URLs that were assembled rather than checked. Every install instruction was
a 404, including the one
/docsitself opened. Each replacement here was verified with a request; a plausible hostname is not evidence. - Controls documented as working that are compiled out. Version pinning, the telemetry knobs, the coding-data row. Inheriting upstream's documentation for a feature this build disables produces a page that is confidently false.
- A second registry with the same gap as the tool layer. Two guide chapters sat
on disk, absent from
USER_GUIDE, so/docsnever listed them and they were never extracted to$CHUTES_BUILD_HOME/docs. Whenever the fork adds something the product exposes, the question is not "is it there" but "what lists it".
Verified live with a real API key (see the sections above): API-key auth, the
catalog from llm.chutes.ai, a complete turn through the Auto router with its
timing, the set of hosts contacted, that nothing is written outside
$CHUTES_BUILD_HOME, and that every Chutes tool is discoverable and the catalog
tool invocable.
Still unverified, and listed that way rather than assumed:
- A completed media generation. The tools are reachable and the catalog answers,
but headless runs stop after two tool calls without reaching
generate_media, and/imagineis a TUI command. Exercise it by hand in the TUI. - Voice input and OCR — both need real audio and image input.
chutes-build login: the browser OAuth handoff against the Chutes IdP, including the loopback callback and thechutes.aiCORS origin.- The usage and quota display in the status bar, and
/usage. - The pre-stream fallback chain (
CHUTES_FALLBACK_MODELS), which needs a model to be genuinely unavailable to trigger. The short-Retry-Afterwait added in 1.0.4 needs the same: a real capacity 429 carrying the header. - Making
xai-grok-toolsandxai-grok-workspacedeterministic. Their failing sets move: of 53xai-grok-workspacefailures recorded on a local Windows machine, 17 pass on the Windows runner and 2 others fail, and adaemonizetest failed on Linux in one run and not in the run 90 minutes earlier on the same commit. That is whyknown_failures.pyis a local diagnostic rather than a CI gate — a per-test baseline needs a fixed set. This is the work that would make ~188 currently unwatched tests watchable, and it is larger than the tooling that exposed it. - Why
xai-grok-tools --libnever finishes on the Windows CI runner. It ran 107 minutes, then 140, then hit a 40-minute timeout, against six minutes on Linux and about five on a local Windows machine. Every Windows step in the workflow runsimplementations::chutes::, which skipscomputer::local::terminal, so the whole suite had never run on a runner and the hang had never been seen. Not reproducible from a machine where it passes, so the way in is a throwaway Windows job running that suite with--nocaptureand--test-threads=1to see which test it stops on. Until then the Windows job gatesxai-grok-workspaceonly. - The
AuthFileStampmemo cannot distinguish two same-length rewrites within one ~15ms clock tick on Windows, where there is no inode. Judged unreachable in practice (a rotation is minutes apart, and refresh does not use this memo) and left with the reasoning in place; closing it meansGetFileInformationByHandle, sincefile_index()is still unstable in std. xai-grok-shell --libas a whole: it overflows a 1 MB thread stack on Windows in debug, so that crate's non-auth::tests are unmeasured here.agent::configandremote::clientwere checked per-module and pass.
Measured 2026-08-11 against upstream/main at b13fa526, from the recorded
baseline afbc0fb7. Three commits, 238 files, +17887/−6138.
Do not measure this with git diff HEAD upstream/main. That reports 1424 files
and 40k deletions, nearly all of which are our re-base and rebranding read
backwards — the fork is not a descendant of upstream, so the diff is symmetric
and tells you nothing about what arrived. Diff from the baseline commit instead.
What the three commits contain:
| Commit | Size | Theme |
|---|---|---|
8a14c91d |
157 files, +11148/−4949 | session replay (session/storage/replay.rs + tests, 1627 lines, new), app/session_load_barrier.rs (new), scrollback/state/layout.rs (new), .envrc/direnv support in workspace |
75e73f3d |
10 files, +273/−789 | agent/app.rs loses 740 lines to a refactor; sandbox hook_write_deny and child_net; textarea |
b13fa526 |
102 files, +6536/−470 | session rename as a user-facing feature (slash/commands/rename.rs, session_admin.rs, agent/server.rs) |
The split that makes this tractable. Intersect what upstream changed with what we changed, both measured from the same baseline:
- 147 files overlap. These need reconciling by hand, one at a time.
- 91 files are upstream-only — we never touched them, so there is nothing of ours to lose. 17 are new files; 74 already exist here unmodified.
The overlap list is the argument against merging, in concrete form: it contains
xai-grok-agent/templates/prompt.md and prompt/prompt_encrypted.rs. A merge
would take upstream's system prompt again and silently re-introduce exactly the
regression 1.0.3 fixed — the agent introducing itself as xAI's, and the
<official_chutes_sources> block with its "never put credentials in a search
query" instruction deleted a second time.
Of the 91, only 16 carry branding tokens in upstream's new version (checked
with grep -E 'GROK_|grok-build|GrokBuild|Grok Build|grok\.com|x\.ai|~/\.grok'
against upstream/main:<path>, not against ours):
app/exit_timeout.rs agent/handlers/models.rs
app/session_load_barrier.rs agent/mvp_agent/tests/session_rename_tests.rs
memory_trace_signal_topology_tests.rs session/acp_session_impl/spawn_runtime_containment_tests.rs
pty_e2e/scroll_anchor_holds_parked_… session/goal_planner.rs
pty-harness/tests/exit_timeout.rs session/goal_strategist.rs
session/goal_summarizer.rs session/storage/relocation/mod.rs
session/storage/replay.rs session/storage/replay_tests.rs
tests/test_mcp_doctor_isolation.rs workspace/src/envrc.rs
The other 75 are token-clean and can be taken verbatim, subject only to API
drift at compile time. Run scripts/rebrand.py over the 16 — it exists for
this and is re-runnable.
Order the work by area, not by commit, and land each area with its own tests green: it is the only way to tell which area broke something.
python scripts/port_assist.py --base afbc0fb7 splits the 238 files five ways
and only the last needs a person:
done 30 (12%) already ported — we hold upstream's version
new 13 ( 5%) not in our tree
clean 62 (26%) untouched by us since the baseline
mechanical 52 (21%) our divergence is exactly the rebrand
manual 81 (34%) genuine divergence — read it
mechanical is the bucket worth understanding: for those files
rebrand(upstream@base) reproduces our current version character for
character, which proves our only divergence was the rebrand and that
re-applying it to upstream's new version reproduces the work exactly.
--apply writes the three take-able buckets and leaves manual alone. It is
where the work starts, not where it ends — every area ported so far reached two
to four crates, so compile, run the suite and check known_failures.py before
believing any of it.
A useful sanity check on the tool: --path crates/codegen/xai-grok-agent
reports 100% manual. That is the crate holding templates/prompt.md, and
taking it automatically is exactly what told every model it was released by xAI.
Four areas, 28 files, on port/upstream-b13fa526. Each landed as its own commit
with its own tests green, measured against a pre-port baseline.
| Area | Files | What it brings |
|---|---|---|
xai-tty-utils |
5 | Runtime worker sizing against pids.max / RLIMIT_NPROC, plus 329 lines of tests it had none of, including EAGAIN |
| Skill-path suggestion | 6 | A failed SKILL.md read answers with where the skill is registered |
| Workspace RPC | 17 | Every RPC declares read or write (ACTIVITY); proxy client reports the server binary version |
| Sandbox write-deny | 3 | Compiled only where it enforces — Linux, plus the test arm |
Sixty-six of the ninety-one safe files remain, and all hundred and forty-seven overlapping ones.
Four things generalise to the rest.
An area is bigger than the module you meant to take. The suggestion looked
like one new module. Alone it fails clippy's dead_code, because its only
consumer is the read_file tool; read_file then needs
AsyncFileSystem::file_exists, new in computer/types.rs, with its local and
mock implementations. Six files for one feature. dead_modules.py cannot see
this — the module is reachable through mod, so it reports nothing. The
lint is the detector, which means an area is not finished until
cargo clippy -p <crate> --all-targets -- -D warnings passes, not merely until
it compiles and its own tests are green.
Measure the baseline failure set before attributing anything. This tree has
pre-existing Windows failures — 69 in xai-grok-tools alone — so a post-port
count is meaningless on its own. Stash, run, save the sorted failure names, run
again after, and comm the two lists. "73 failures" told us nothing; "these
exact four are new" told us everything.
Upstream's fixtures assume POSIX, and their CI never says otherwise. Eleven
tests across the two areas failed here and pass upstream, all for the same
reason: a leading slash on Windows is drive-relative, so
Path::new("/repo/x").is_absolute() is false and any code guarding on
is_absolute() skips the whole fixture set. The production guards were right
every time. Give the fixtures a dialect — a small abs() helper that prefixes a
drive on Windows — rather than touching the code they cover. Watch for the
second form too: a path built with join carries host separators, so an
expected string must be composed the way the implementation composes it, not
written with forward slashes.
Do not run scripts/rebrand.py --apply over the whole tree and trust it. It
used to rewrite the six sentences that name upstream on purpose — the README
attribution, SECURITY's upstream-scope paragraph, the comments explaining whose
wordmark 1.0.0 shipped, and the fork link in the getting-started guide, which it
pointed at a repository that does not exist. Those are pinned in UPSTREAM_PROSE
now and the script fails loudly if one is reworded. Still read git status after
every run: a file outside the area in hand is the signal that something was
matched that should not have been.
Note
Closed. The crash was the doctor's voice probe: collect_report enumerated
the audio device through WASAPI, and doing that twice in one process takes the
binary down with 0xc0000005. The report now skips the probe entirely when it is
not asked for, rather than suppressing its finding — the first attempt did the
latter, the probe still ran, and CI returned the identical crash. With the log no
longer truncated, the run finally produced a complete failure list, which is what
"Closing the Windows failures" below works from. The RUST_TEST_THREADS=1
localisation described here was never needed.
Rust quality (Windows) is red on main since 1.0.0 landed. The pager test binary
exits with 0xc0000005 — an access violation — after roughly 3868 of ~8200
tests, so cargo test reports no failure list at all: the process dies mid-run.
Rust quality (Windows) was green on the 0.4.x tree, so this arrives with upstream
1.0.0's code; it is not something the re-base introduced into it.
The git2 0.20.4 bump is excluded, by experiment rather than by argument. Every
Windows run between the landing commit and that bump was cancelled by a later push,
so the timeline alone could not separate them — and the crash sits at the boundary of
git_info, which is the module that calls libgit2, with the bump carrying five
libgit2 patch releases. Dispatching CI on the landing commit (2ece18b, git2 0.20.2) reproduced it exactly: same 0xc0000005, same step, 3868 tests reported
against 3869 and 3871 on the bumped runs. Nothing to look for in libgit2.
Ruled out, so nobody repeats the work:
- The escape sequences immediately before the crash are innocuous. The log shows
Kitty graphics deletes (
�_Ga=d,d=i,…) and OSC 12 cursor colours. Both are pure string construction plus a locked-stderr write —terminal::image::clear_kitty_imageandtheme::apply_cursor_color. They are interleaved output from a parallel run, not the fault. - Not the PTY tests. Every test in
pty_wrap.rsis#[cfg(unix)]. - Not
SetConsoleCtrlHandler. That FFI lives in the productionscreen_mode_relaunchpath, not in a test.
To localise it, dispatch the workflow with the Windows job running single-threaded
(RUST_TEST_THREADS=1). With one thread the harness prints each test's name before
running it, so the last name in the log is the one that died — which a parallel run
cannot tell you. Do that before guessing at the 56 unsafe blocks in the pager.
Three doctor_cmd tests also fail on Windows — non_tty_without_yes_fails_safely_before_write,
fix_preview_contains_exact_change_and_caveats, decline_is_success_and_does_not_write.
They fail on this machine too, and failed before the re-base, so they are a separate,
smaller problem from the crash.
The seams in agent/{config,init,models}.rs and extensions/billing.rs are
applied, and the memory store now writes every path through
write_secure_file / ensure_owner_only_permissions (three of the four paths
still created files with the process umask; the fourth already did not, and only
the privacy filter had been wired up). memory_files_are_owner_only covers all
of them on Unix.
main's branch protection still forbids the merge commits this model needs; see
the section above. That is a decision, not a task.
Sixty-six, from CI's own list — available complete for the first time once the
crash above stopped truncating the log. Five were product defects, and they are in
the 1.0.0 changelog: doctor fix unable to write anything, file paths in agent
prose never clickable, Tab completion in the extensions modal destroying the path,
the memory header painting \MEMORY.md, and a fixture named nul that on Windows
is the null device. The rest were fixtures describing a platform they were not
running on.
Three things are worth carrying forward more than the individual fixes.
A red step hides everything below it. The Windows job's second step — the authentication and tools tests — had never executed, because the pager step above it always failed first. The Linux job had not reached its end in twenty runs: first a formatting failure, then one flaky test, and behind them four unexecuted steps including clippy over the Chutes-owned packages. Every step cleared reveals the next, and the count of "remaining failures" is only ever a lower bound until a job runs green end to end.
The last thing hiding down there was not a test at all: a job that starts passing does more work than the one that used to fail, and can outgrow the time limit written for the broken version of itself. The Linux job's 60 minutes had been ample while it died at the formatting check; the first run that reached the end spent 59.7 minutes on steps that all passed and was killed during the cache save. That failure mode feeds itself — no cache saved means the next run starts cold, takes longer, and dies in the same place — so the limit is now 90.
Measured twice after the raise: 65 minutes, then 67. Which corrects the guess made when raising it — that a warm cache would leave the real cost well under the limit. It does not: about 45 of those minutes are test execution and 11 are clippy, so compilation is not what fills the hour, and the headroom is roughly a quarter rather than a half.
Final state, 2026-08-10: all five jobs green, which is the first complete run this
repository has had. xai-grok-pager --lib 8274 passing on Windows against 83
failures at the start, xai-grok-shell --lib auth:: 374 against 24 in a step that
had never run.
Reproduce the runner rather than trusting the local number. Local runs on the
development machine disagreed with CI in both directions, and each disagreement
had a cause worth knowing: the runner's %TEMP% is an 8.3 short path
(C:\Users\RUNNER~1\…), which a guard that rejects ~ refuses; /tmp is
drive-relative off Unix and resolves only because C:\tmp happens to exist here,
while the runner works on D:; and a terminal is attached here and not there, so
capability-driven rendering and key handling differ. Pointing %TEMP% at a
short path turned a CI-only failure into one that could be iterated on locally in
seconds.
A widened deadline is usually a misdiagnosis. The history-delivery test was
"fixed" once by taking its budget from one second to ten. It kept failing, because
the condition was tick() && result_count() == 2: tick() reports that the screen
needs repainting, not that the daemon answered, and activate takes an eager
snapshot that moves the generation counter — so when the daemon is fast, the
results are already there, no later poll reports a change, and the conjunction can
never hold however long it waits. It fails when the machine is quick, which no
amount of clock could fix.
Clearing the pager let the Windows job reach its second step for the first time —
xai-grok-shell --lib auth::, which failed 24. One cause: a provider command goes
through the platform shell, and off Unix util::subprocess::shell_c picks cmd /C
deliberately, because the contract is "exit 0 means success" and PowerShell's
-Command does not propagate a child's exit code. The fixtures were POSIX
one-liners — printf, sleep 20; printf never, $(wc -l < …), ${VAR:-0},
if [ … ]; then … fi — so they minted nothing there.
Writing each fixture twice, once per dialect, would have encoded the problem
instead of removing it: cmd has no sleep, no printf, no default-valued
expansion, and quoting JSON through it is its own nightmare. The suite now drives
crates/codegen/xai-grok-shell/src/bin/auth-provider-fixture.rs, invoked with
args, which takes the direct-exec branch where no shell interprets anything.
Three constraints that shaped it, worth knowing before touching it:
- The helper has to be built first.
CARGO_BIN_EXE_*is set for integration tests and benches, not for a lib's unit tests, andcargo test --libdoes not build a crate's binaries. Both CI steps and the local gate build it; the test panics with the exact command if it is missing. - The command-only paths cannot be quoted.
run_external_refreshandrun_external_auth_providertake a command and no args, so they still go through a shell — andcmd /Cstrips the first and last quote of the whole string, so a quoted program with quoted arguments comes apart in its hands. The helper is named bare there, with an assertion that nothing in the line contains a space. - A fixture must not abort.
[profile.dev]setspanic = "abort", so the stderr-flood mode writing througheprint!— which panics on a failed write — surfaced as exit0xc0000409, indistinguishable from the deadlock that case exists to detect.
The two lock tests were a different fault: they read the lock file through a second
handle while holding it, which Windows refuses with ERROR_LOCK_VIOLATION, since
fs2 locks a byte range there. They read through the holding handle now, which is
what holder_state does in the product. Note what that implies for production:
under real contention the PID-liveness read fails on Windows and the code falls
into the holder info unreadable branch it already has, so staleness is decided by
mtime. It degrades safely — it waits the full threshold before breaking a lock —
but the liveness probe does not run there.
374 pass, 0 fail.
Upstream moved for the first time since the fork's last two passes, and it
reached 1.0.0: six Synced from monorepo commits carried 0.2.117 to
1.0.0 (a4221165 → afbc0fb, 2026-08-03 to 2026-08-07), touching 748 files
with roughly a hundred and ten changes.
A note for future passes, because it misled this one: upstream still publishes
no GitHub releases and no tags (both endpoints return empty at 1.0.0), and
the major-version bump has no commit of its own — it landed inside an ordinary
Synced from monorepo commit alongside seven unrelated changes, one commit
after "Drop the Beta label from the product". The version exists only in the
lockstepped Cargo manifests. .github/upstream.json's versionManifest field
and the Upstream watch workflow read exactly that file, which is the correct
signal; the release/tag comparison is not, and should not be read as evidence
that upstream has not moved.
Measured against the reviewed baseline at the time of the port work
(393430ee, 0.2.121), 81 of the touched files were still byte-identical in
this fork and could be taken as-is; 491 had diverged and needed hand-merging;
57 did not exist here at all. The 1.0.0 commit adds 77 more files and does
not overlap the areas ported below.
Ported — security:
- Lexical path normalization before the permission glob match, with the
session cwd threaded through the manager and the shell-file gate. This is the
fix with real bite:
Read(src/**)was escapable assrc/../../etc/passwdbecause**consumes... Seven regression tests cover traversal escapes, deny reach, the*catch-all, and the no-cwd path; their absolute fixtures are built from a platform root, since/etc/passwdis not absolute on Windows and would otherwise be cwd-joined and stop testing the escape. NotebookEdit/NotebookReadrejected as rule prefixes instead of aliasing ontoEdit/Read.- Vendor-compat MCP kill switch actually enforced against client-forwarded servers, including the on-disk attribution loader that bypasses the import-marker cutoff, applied at all three ingress points.
- Char-safe bearer fragment. Upstream extracted it into a new
xai-grok-auth::bearer_fragment; this fork had the same byte-slicing bug in its owntoken_suffix, which now delegates to that one definition. - Sandbox deny-glob rework: the 200k visited-entry cap that refused startup on
ordinary large workspaces, symlink-target masking, and fail-closed walk
errors.
deny/glob.rswas identical to the baseline, so it was taken whole; only the caller'sOption→Resultseam and the caps struct were adapted.
Ported — inference resilience, the highest-value area for this fork because Chutes is served through the Cloudflare edge:
RetryPolicy::edge_client(429 + any 5xx minus origin-TLS 525/526), andSamplingError::is_retryablerebuilt on it. The previous hard-coded list made 521–524, 529 and 530 fatal, so a transient edge failure ended the turn.Retry-Afterclamped to a newMAX_RETRY_BACKOFFof 30s and jittered on the generic path, leaving the 429 path to wait the full value under its own attempt cap.x-should-retrycarried throughSamplingErrorInfo, so the header survives stream collection.- Clean non-200 banners. This one predates the reviewed baseline: upstream's
provider_error.rsanduser_facing_api_error_messagewere never taken at the fork point, soparse_error_bytesstill fell back to the raw body — meaning a Cloudflare HTML page went straight to the terminal. The module was added and all nine client call sites now pass the status.
Not portable: gate_preflight.rs. The module itself is small, but it is built
on upstream's GateDecision provenance rework (AskRuleMatch vs
AskFailClosed) and the manager.rs → manager/ split, neither of which this
fork has. The security value it carries — the cwd-aware evaluation — was taken
directly instead, via evaluate_with_cwd at the manager and shell-file gates.
Excluded by policy, as in previous passes: the three telemetry additions (Auto
decision, shortcut_used, model-side skill reads) and agent/otel_gate.rs;
the /feedback card changes; the "run chutes-build update before re-authenticating"
copy; the cloud sandbox provisioning types and remote/skills_client.rs.
Excluded as upstream-specific: dropping the Beta label, the chutesday /
chutesnight themes, the Finance ToolUsageCard variant, and a deprecation-doc
path fix for a file this fork does not have. Not applicable: the toolchain bump
to 1.93.0 (this fork is on 1.94.1) and the workflow-subagent cap, which needs
the upstream-only xai-workflow crate. Deliberately not taken: removing the
project-directory picker, a feature this fork keeps.
Ported — presentation and prompt:
- Narrow markdown tables reflow inside their cells. The hard split walks
grapheme boundaries (CJK, VS16 emoji, ZWJ clusters stay intact) and the span
projection uses a monotonic cursor, so a plain run repeating an earlier
substring cannot inherit a link's style or hyperlink range. All four touched
files were identical to the baseline and taken whole; the crate gains
unicode-segmentation, already a workspace dependency. - Plan-viewer scrollbar: grab zone widened to the border column, striped thumb fixed on Terminal.app.
- Session recaps follow the language of the user's own chat messages instead of always English. This fork's copy of the prompt was also older than the baseline and had lost the explicit "do not call tools" guard, which came back with it.
Arrived with 1.0.0 itself and not yet ported: guarding the in-process git
status/diff from client spam, memory traces bundled into the session trace
export (local-only here, so no privacy conflict), a tabbed usage/session-info
modal behind /usage, /session-info and /context, startupHints on
session request metadata with the headless MCP connecting reminder fixed, and a
plugin CTA debounce. Naming the Windows download Chutes Build Setup.exe is upstream
distribution and excluded.
Looked at and deliberately deferred rather than rushed: skipping nested
checkouts in file watching. The visible half is a small new checkout.rs, but
the fix that matters lands in watcher.rs, which upstream grew by 206 lines
and which has diverged here (Sapling support sits behind
CHUTES_BUILD_FSNOTIFY_SAPLING). It needs its own pass, not a hurried merge
into a live filesystem watcher.
Still open at the end of this pass, in rough value order: session-runtime work
(streamed session fork, bounded post-kill reaps, the shared search-index lock,
leader-disconnect eviction, the restored-child session registry behind
--resume), tool and agent-loop fixes (MCP images extracted before truncation,
read-only tool metadata, colliding skills, the goal evaluator, the stop gate),
the TUI batch (scrollbar, CJK selection, markdown palette and table reflow,
tmux/SSH theme detection, teardown resets, prompt-queue ordering), and the
user-facing additions that would justify a minor release (du, ACP
session/resume / session/close, conversation-only /rewind, the permission
pattern editor, the dashboard batch).
The baseline in .github/upstream.json is therefore not advanced by this
pass: the procedure above only allows it once the selected ports are complete
and the gates pass, and the selection is still open.
Upstream main was still a4221165824e5b1f5c4c10b7459f65e78dd6448d (0.2.117),
unchanged since the previous review, and upstream still publishes no releases or
tags. No new upstream work existed to review, so this pass reopened the areas the
2026-08-01 review deferred.
Ported:
- MCP config persistence. The four MCP writers parsed
config.tomlwith a fallback to an empty table and then rewrote the whole file, so an unparseable config was replaced by only the keys being saved; none took the config write lock, and all staged through a fixed temp filename. Now one helper refuses unparseable input, holds the user-config lock across the read-modify-write, writes atomically, and skips no-op writes. - Extra TLS roots via
CHUTES_EXTRA_CA_BUNDLE, for proxy-terminated TLS. Reworked rather than copied: upstream adds a crate that depends onrustlsdirectly, which would mean editing the generated workspace root, so this uses reqwest's own PEM bundle parsing insidexai-grok-http. - Cancelling an in-flight
/compact, including theis_compact_running/cancel_compact_commandstate predicates it needs.
Already present, nothing to port: background subagent cancellation (identical,
tests included), terminal resize (ptyctl differs only by a comment and an
upstream-only lint allow), and session cold-start (voice cold start, already
implemented).
Not portable: the auth_retry module behind the ACP task-lifecycle changes is
bound to upstream's AuthManager/SentCredential flow. Chutes Build
authenticates differently, so adopting it means reworking authentication, not
porting a module.
Deferred with a known reason: streaming-messages-json (NDJSON in the Messages
API wire format). Upstream implements it as a reducer split across 19 files;
this fork still has a monolithic headless.rs that branches on the output
format in twelve separate places. The port is worth doing but needs the reducer
refactor first, not a thirteenth branch.
The baseline in .github/upstream.json is unchanged because upstream itself has
not moved.
Reviewed upstream main through a4221165824e5b1f5c4c10b7459f65e78dd6448d
(0.2.117). Upstream still publishes no GitHub releases or tags, so the
versioned changelogs and main commits were the authoritative review inputs.
Ported selectively:
0.2.115: deliver the repeated-tool-call stationarity reminder only after the preceding tool results are committed, preventing duplicate tool results from corrupting chat history;0.2.116: accept/undoas an alias for/rewind; and0.2.117: require revision notes before Enter requests plan changes, with an explicitashortcut for approval.
Already covered by Chutes-specific code: Windows external-auth shell handling uses the platform-aware command builder and retains its dedicated tests.
Deferred because they are coupled to newer upstream runtime/configuration surfaces and need a separate Chutes compatibility review: MCP enable/disable persistence, streaming JSON events, additional CA-bundle handling, background subagent cancellation, ACP task lifecycle changes, terminal resize behavior, and session cold-start work. No upstream identity, billing, telemetry, or enterprise-only behavior was imported.