Compare commits

...
10 Commits
Author SHA1 Message Date
iceBear67 8128f9a1a2 Fix partial-move in Chat Completions session_cache_key mapping
CI / test (push) Canceled after 0s
CI / grok-linux-amd64 (push) Canceled after 0s
CI / grok-windows-amd64 (push) Canceled after 0s
CI / grok-macos-arm64 (push) Canceled after 0s
CI / release (push) Canceled after 0s
Copy the cache key before moving ConversationRequest fields so
ChatCompletionRequest::from compiles (E0382).
2026-08-16 03:45:51 +00:00
iceBear67 62bd461356 update readme
CI / test (push) Canceled after 0s
CI / grok-linux-amd64 (push) Canceled after 0s
CI / grok-windows-amd64 (push) Canceled after 0s
CI / grok-macos-arm64 (push) Canceled after 0s
CI / release (push) Canceled after 0s
2026-08-16 03:26:51 +00:00
iceBear67 a968ad297c Update upstream to 5163763
CI / test (push) Canceled after 0s
CI / grok-linux-amd64 (push) Canceled after 0s
CI / grok-windows-amd64 (push) Canceled after 0s
CI / grok-macos-arm64 (push) Canceled after 0s
CI / release (push) Canceled after 0s
Forward-port the six local patches onto the two new monorepo syncs
(SOURCE_REV e6a67a54 -> 84ae1223). Resolved 0001 against the runtime
full_version() injection and 0005 against the Option-wrapped
UserPromptSubmit hook payload.
2026-08-16 03:22:31 +00:00
iceBear67 c48b60d383 Add session-stable prompt_cache_key (patch 0006)
CI / test (push) Canceled after 0s
CI / grok-linux-amd64 (push) Canceled after 0s
CI / grok-windows-amd64 (push) Canceled after 0s
CI / grok-macos-arm64 (push) Canceled after 0s
CI / release (push) Canceled after 0s
Main turns now set prompt_cache_key to the session/conv id. Responses
already sent that via the conv-id fallback; Chat Completions now puts
the same value in `user`. Recap and /btw keep sharing the parent
session key — not an agent-scoped suffix.
2026-08-16 03:13:26 +00:00
iceBear67 fb0597578f Rework CI: cross Windows on Linux, build only on dispatch/tag
Regular push/PR runs the test job only. workflow_dispatch and v* tags
then build linux-amd64, windows-amd64, and macos-arm64 as separate
jobs. Windows is cargo-xwin from Ubuntu (MSVC ABI, Unix-host protoc)
instead of native windows-latest. Registry/git cache is shared on
Ubuntu; release target/ is not cached; xwin splat has its own cache.
2026-08-16 03:08:29 +00:00
iceBear67 a7e81a33ff Add builtin-tool promotion (patch 0005)
Per-turn dispatch checklist on primary user turns, plus a post-hoc bash
discipline nudge when cat/grep/find/ls (and friends) stand in for a
dedicated tool. Both blocks are stripped from the compaction summarizer
copy only.
2026-08-16 02:56:50 +00:00
iceBear67 a6bef210e9 add workflow
Build / grok-linux-amd64 (push) Canceled after 0s
Build / grok-macos-arm64 (push) Canceled after 0s
Build / grok-windows-amd64 (push) Canceled after 0s
Build / release (push) Canceled after 0s
2026-08-16 02:36:24 +00:00
iceBear67andClaude Opus 5 36f10f1256 Add scripts/paced.sh, and record what actually limits builds here
Cargo on this box was suspected of writing the disk to death. Measuring
it says otherwise: 4 CPUs, load peaks at 3.0 during a full build, while
writes peak at 30 MB/s and sit at zero for most samples. The long poles
are single-crate rustc compiles that cannot be parallelised, so the
binding constraint is CPU and `nice -n 19` is the first-line tool.

The disk itself reads at 210 MB/s and writes at 50 MB/s, identically
across block sizes and IO modes -- a volume-level write cap rather than
disk physics. A short burst reaches ~150 MB/s on what look like burst
credits; calibrating against that number is a mistake, and it is the one
that made an earlier 40 MB/s ceiling do nothing.

paced.sh is the fallback for the phase that does write hard -- linking
the 643 MB debug binary saturates writes for about 13 seconds. It
duty-cycles a process group against the observed rate in
/proc/diskstats, because every kernel-side lever is unavailable here:
/sys/fs/cgroup is read-only and cannot be remounted or re-mounted
elsewhere even under sudo (no CAP_SYS_ADMIN), /proc/sys is read-only,
and the scheduler cannot be switched to BFQ so ionice is a no-op. Each
of those is verified, not assumed; the header records them so the next
person does not re-derive it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 11:08:33 +00:00
iceBear67 95ba918629 Add /rc remote control bridge (patch 0004)
Exports the work/ commit that adds the RC bridge: the ACP tee, the outbound
glance transport with reconnect and replay, the first-answer-wins interaction
race, and the /rc slash command.
2026-08-15 09:10:07 +00:00
iceBear67 dd9b41e72f Add [remote_control] config section (patch 0003)
Exports the work/ commit adding the [remote_control] config section that the
/rc bridge reads: url, api_key, auto_start and replay_buffer, with
GROK_RC_URL / GROK_RC_API_KEY overriding the file.
2026-08-15 09:10:07 +00:00
12 changed files with 4401 additions and 179 deletions
+286
View File
@@ -0,0 +1,286 @@
# CI for the patched grok binary.
#
# Regular push / pull_request: test job only.
# workflow_dispatch or a v* tag: test, then the three release builds.
# Tags also publish the artifacts as a GitHub Release.
#
# Windows is cross-compiled from Ubuntu (cargo-xwin → x86_64-pc-windows-msvc),
# in its own job — not mixed into the native linux-amd64 build. Official proto
# codegen is Unix-host-only (/dev/stdout, Linux protoc); native windows-latest
# is the path that broke.
#
# Cache budget (GitHub's repo cap is 10 GB):
# - cargo registry/git is shared across the Ubuntu jobs (no target/)
# - test keeps a separate debug work/target (small crates only)
# - xwin's MSVC splat is cached on its own
# - release target/ is never cached (multi-GB)
name: CI
on:
push:
branches: [main]
tags: ["v*"]
pull_request:
workflow_dispatch:
concurrency:
# Distinguish test-only runs from dispatch/tag builds so a release does not
# cancel an in-flight PR test on the same branch name, and vice versa.
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}
cancel-in-progress: ${{ github.event_name == 'pull_request' || (github.event_name == 'push' && !startsWith(github.ref, 'refs/tags/')) }}
env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: "0"
RUST_BACKTRACE: "1"
permissions:
contents: read
jobs:
test:
name: test
runs-on: ubuntu-latest
timeout-minutes: 90
defaults:
run:
shell: bash
steps:
- name: Disable CRLF conversion
run: git config --global core.autocrlf false
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@1.94.0
- uses: arduino/setup-protoc@v3
with:
version: "29.3"
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Apply patches onto pinned upstream
run: ./scripts/ci-apply.sh
- name: Export PROTOC
run: echo "PROTOC=$(command -v protoc)" >> "$GITHUB_ENV"
- uses: Swatinem/rust-cache@v2
with:
workspaces: work
shared-key: ubuntu-cargo
cache-targets: false
save-if: ${{ github.ref == 'refs/heads/main' && github.event_name == 'push' }}
- name: Restore test target cache
id: test-target
uses: actions/cache/restore@v4
with:
path: work/target
key: test-target-1.94-${{ hashFiles('work/Cargo.lock') }}
restore-keys: |
test-target-1.94-
- name: Test
working-directory: work
# make test default + the proto crate (exercises protoc / build.rs).
# pager/shell stay off this job: their debug target/ would blow the
# 10 GB cache budget and the runner disk.
run: cargo test --locked -p xai-grok-version -p xai-grok-tools-api
- name: Save test target cache
if: github.ref == 'refs/heads/main' && github.event_name == 'push' && steps.test-target.outputs.cache-hit != 'true'
uses: actions/cache/save@v4
with:
path: work/target
key: test-target-1.94-${{ hashFiles('work/Cargo.lock') }}
build-linux-amd64:
name: grok-linux-amd64
if: github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/')
needs: test
runs-on: ubuntu-latest
timeout-minutes: 180
defaults:
run:
shell: bash
env:
TARGET: x86_64-unknown-linux-gnu
ARTIFACT: grok-linux-amd64
EXE: xai-grok-pager
steps:
- name: Disable CRLF conversion
run: git config --global core.autocrlf false
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@1.94.0
with:
targets: x86_64-unknown-linux-gnu
- uses: arduino/setup-protoc@v3
with:
version: "29.3"
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Apply patches onto pinned upstream
run: ./scripts/ci-apply.sh
- name: Export PROTOC
run: echo "PROTOC=$(command -v protoc)" >> "$GITHUB_ENV"
- uses: Swatinem/rust-cache@v2
with:
workspaces: work
shared-key: ubuntu-cargo
cache-targets: false
- name: Release build
working-directory: work
run: cargo build --release --locked -p xai-grok-pager-bin --target "$TARGET"
- name: Package
run: ./scripts/ci-package.sh
- uses: actions/upload-artifact@v4
with:
name: grok-linux-amd64
path: dist/*
if-no-files-found: error
build-windows-amd64:
name: grok-windows-amd64
if: github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/')
needs: test
runs-on: ubuntu-latest
timeout-minutes: 180
defaults:
run:
shell: bash
env:
TARGET: x86_64-pc-windows-msvc
ARTIFACT: grok-windows-amd64
EXE: xai-grok-pager.exe
XWIN_CACHE_DIR: ${{ github.workspace }}/.xwin-cache
steps:
- name: Disable CRLF conversion
run: git config --global core.autocrlf false
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@1.94.0
with:
targets: x86_64-pc-windows-msvc
- uses: arduino/setup-protoc@v3
with:
version: "29.3"
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Apply patches onto pinned upstream
run: ./scripts/ci-apply.sh
- name: Export PROTOC
run: echo "PROTOC=$(command -v protoc)" >> "$GITHUB_ENV"
- name: Install clang / lld / llvm
run: sudo apt-get update && sudo apt-get install -y clang lld llvm
- uses: taiki-e/install-action@v2
with:
tool: cargo-xwin
- uses: Swatinem/rust-cache@v2
with:
workspaces: work
shared-key: ubuntu-cargo
cache-targets: false
- name: Cache xwin MSVC splat
uses: actions/cache@v4
with:
path: ${{ env.XWIN_CACHE_DIR }}
key: xwin-x86_64-msvc-v1
- name: Release build (cross)
working-directory: work
run: cargo xwin build --release --locked -p xai-grok-pager-bin --target "$TARGET"
- name: Package
run: ./scripts/ci-package.sh
- uses: actions/upload-artifact@v4
with:
name: grok-windows-amd64
path: dist/*
if-no-files-found: error
build-macos-arm64:
name: grok-macos-arm64
if: github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/')
needs: test
runs-on: macos-14
timeout-minutes: 180
defaults:
run:
shell: bash
env:
TARGET: aarch64-apple-darwin
ARTIFACT: grok-macos-arm64
EXE: xai-grok-pager
steps:
- name: Disable CRLF conversion
run: git config --global core.autocrlf false
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@1.94.0
with:
targets: aarch64-apple-darwin
- uses: arduino/setup-protoc@v3
with:
version: "29.3"
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Apply patches onto pinned upstream
run: ./scripts/ci-apply.sh
- name: Export PROTOC
run: echo "PROTOC=$(command -v protoc)" >> "$GITHUB_ENV"
- uses: Swatinem/rust-cache@v2
with:
workspaces: work
key: macos-arm64
cache-targets: false
- name: Release build
working-directory: work
run: cargo build --release --locked -p xai-grok-pager-bin --target "$TARGET"
- name: Package
run: ./scripts/ci-package.sh
- uses: actions/upload-artifact@v4
with:
name: grok-macos-arm64
path: dist/*
if-no-files-found: error
release:
if: startsWith(github.ref, 'refs/tags/')
needs: [build-linux-amd64, build-windows-amd64, build-macos-arm64]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/download-artifact@v4
with:
path: dist
merge-multiple: true
- uses: softprops/action-gh-release@v2
with:
files: dist/*
generate_release_notes: true
fail_on_unmatched_files: true
+204
View File
@@ -0,0 +1,204 @@
# 协作与开发工作流
本仓库是 [`xai-org/grok-build`](https://github.com/xai-org/grok-build) 的 patch 化 fork。fork 多了哪些能力见 [README.md](README.md);这里只讲怎么改、怎么同步、怎么不把自己的活弄丢。
以 CraftBukkit / Spigot 的模型,在**不接受外部 PR 的上游**之上维护本地改动。
## 为什么是 patch,而不是 fork 后直接改
上游有两个决定性特征:
1. 它是 xAI 内部 monorepo 的**周期性单向导出**。历史是一串线性的 `Synced from monorepo`
压扁提交,根目录 `SOURCE_REV` 记录对应的内部 commit。上游随时会把整棵树重新投放一次。
2. 上游 `CONTRIBUTING.md` 明确写着**不接受任何外部 PR 或补丁**。
也就是说:改动推不回去,而上游会不断整体覆盖。直接在 fork 上改,每次同步都要打一场
merge 混战,而且没人说得清「我们到底改了什么」。
这正是 CraftBukkit 面对 Mojang 的处境。解法也一样——**把改动本身作为版本库里的唯一真相**:
> `patches/` 是源码,`work/` 是编译产物。
## 目录结构
```
newgrok/ ← 你提交的仓库
├── patches/ ★ 唯一真相:0001-*.patch,一个补丁一件事
├── scripts/ 工作流脚本
├── upstream.rev 钉住的上游 commit
├── Makefile
├── upstream/ 派生:上游的纯净 clone .gitignore
└── work/ 派生:upstream@rev + patches.gitignore
```
对照 Spigot
| 这里 | Spigot / BuildTools |
|------|---------------------|
| `upstream/` | CraftBukkit 的 clone(纯净上游) |
| `work/` | 打完补丁的工作目录 |
| `patches/` | `CraftBukkit-Patches/` |
| `scripts/apply-patches.sh` | `applyPatches.sh` |
| `scripts/rebuild-patches.sh` | `rebuildPatches.sh` |
| `upstream.rev` | `versions/*.json` 里的版本钉 |
`upstream/``work/` 都**不进 git**——它们任何时候都能从 `upstream.rev` + `patches/`
一字不差地重建。
两条规则,违反任意一条都会默默丢掉工作成果:
1. **Rust 源码改动写在 `work/`,不要手改 `patches/`。** 补丁文件是生成物,手改之后下次就合不上。
2. **`make rebuild` 是离开 `work/` 的唯一出口。** `make apply``git reset --hard` + `git clean -fdx`。没 commit 并且没 export 的改动,下次 apply 就没了。
## 日常循环
改代码永远在 `work/` 里,**一个提交 = 一个补丁**:
```sh
make apply # 拿到干净的、打好补丁的树
$EDITOR work/crates/codegen/.../foo.rs # 改
cd work && git add -A && git commit -m '...' # 提交(提交信息就是补丁标题)
cd .. && make rebuild # 写回 patches/
git add patches/ && git commit -m '...' # 提交到 fork 仓库
```
**`make rebuild` 是唯一的出口。** 没跑过它的改动,下次 `make apply` 就没了。
修改已有的补丁,用普通的 git 手段就行——`work/` 是个正常的 git 仓库,`base` 标签是上游和我们的分界:
```sh
cd work
git rebase -i base # 改写、合并、重排、删除任意补丁
cd .. && make rebuild # patches/ 会整体重新生成
```
随时看当前状态:
```sh
make status
```
## 跟进上游同步
```sh
make update-dry # 只看会拉进来什么,不动任何东西
make update # 重新钉 upstream.rev,并把补丁前移到新基线
```
`make update` 会:
1. `git fetch` 上游,列出 `upstream.rev..origin/main` 之间的新提交和 `SOURCE_REV` 变化
2.`upstream.rev` 更新为最新
3. 在新基线上重放 `patches/`
4. 干净通过则自动 `make rebuild`,让补丁上下文对齐新代码
冲突时脚本会停下并给出解决步骤——和处理 rebase 冲突完全一样:
```sh
cd work
git status # 看冲突文件
$EDITOR <冲突文件> # 消掉冲突标记
git add -A
git am --continue # 或 git am --skip / git am --abort
cd .. && make rebuild # 把前移后的结果写回去
```
补丁数量多的时候,冲突是这个模型的固有成本,也是它的价值所在:冲突精确地指出上游改动
撞上了你的哪一处改动,而不是让它悄悄消失。
## 全部命令
从仓库根目录跑 `make``make help` 列出全部目标)。脚本自己把 `~/.cargo/bin` 加进 `PATH`,交互 shell 里不必先有 `cargo`
| 命令 | 作用 |
|------|------|
| `make setup` | 安装工具链(rustup / dotslash / protoc |
| `make apply` | `upstream.rev + patches/ → work/`;加 `FORCE=1` 丢弃 `work/` 里的未提交改动 |
| `make rebuild` | `work/``base` 之上的提交 → `patches/` |
| `make update` | 跟进最新上游同步并前移补丁 |
| `make update-dry` | 预览上游差异,不做修改 |
| `make build` / `make release` | 编译 debug / release 二进制 |
| `make check` / `make clippy` / `make fmt` | 快速类型检查 / lint / 格式化 |
| `make test` | 跑测试(`PKG=<crate>` 指定 crate |
| `make run` | 编译并启动 TUI |
| `make status` | 显示当前钉住的 rev、补丁数、`work/` 状态 |
| `make clean` | 删 `work/target` |
| `make distclean` | 删 `work/``upstream/`(不动 `patches/` |
`make build` 之后的额外参数会透传给 cargo,例如 `./scripts/build.sh --release --locked`
**跑单测。** `make test` 是薄封装;要带 filter,直接走 `build.sh`,多出来的参数会原样转给 cargo:
```sh
CARGO_CMD=test PKG=xai-grok-version ./scripts/build.sh test_fork_tag
CARGO_CMD=test PKG=xai-grok-pager-bin ./scripts/build.sh version_output_writer -- --nocapture
```
## 安全网
脚本刻意不会静默吞掉工作成果:
- `work/` 有**未提交改动**时,`make apply` 拒绝执行(提示先 commit + rebuild,或 `FORCE=1`
- `work/` 的提交数和 `patches/` 的文件数**对不上**时同样拒绝——这基本意味着你忘了 `make rebuild`。故意增删补丁文件时也会触发,检查分不清这两种情况
- `git am` 进行到一半时,`make rebuild` / `make build` 会拒绝执行,而不是产出半成品
- `FORCE=1`(即 `make apply FORCE=1`)是两条 apply 护栏的逃生口,意思是「丢掉 `work/`,按现在的 `patches/` 重放」
- `make apply``git clean` 显式保留 `work/target`,否则每次打补丁都要付一次冷编译的代价
## 补丁格式
`rebuild` 用的是:
```
git format-patch --no-stat -N --zero-commit --full-index --no-signature
```
- `--zero-commit` —— 否则每次 rebase 后每个补丁的 `From <sha>` 行都会变,`git diff` 里全是噪音
- `--full-index` —— 给 `git am --3way` 留下按 blob 哈希回退的余地,上游漂移时更容易自动合上
## 构建依赖
`make setup` 会处理,这里说明它在做什么:
- **Rust 1.94.0** —— 由上游 `rust-toolchain.toml` 钉住,rustup 自动装
- **protoc** —— 只有 `xai-grok-tools-api` 需要(编译 `proto/grok-tools.proto`)。上游的解析顺序是
`$PROTOC` → 向上查找 `bin/protoc``PATH``bin/protoc` 是个
[DotSlash](https://dotslash-cli.com) wrapper,会按需下载 protoc 29.3;所以 `setup.sh`
装 dotslash,失败则回退到系统 `protobuf-compiler`
两个 git 依赖(`our-forks/async-openai``helix-editor/nucleo`)都是公开可访问的。
在这台机器上(4 核 / 7 GB),`xai-grok-pager-bin` 冷编译 debug 大约 11 分钟,`work/target` 会长到约 24 GB,二进制大约 612 MB。`cargo check``cargo build` 各有一份缓存,所以 `make build` 之后再 `make check` 并不快(冷跑大约 6 分钟)。不到真的缺盘,别跑 `make clean` / `make distclean`。迭代时尽量用单 crate`build.sh` 总会带 `-p`)。
## 上游树里该改哪
`work/` 里大约 91 个 workspace member。常动的几个:
| Crate | 角色 |
|-------|------|
| `xai-grok-pager-bin` | 组合根,产出 `xai-grok-pager` 二进制(发行名 `grok` |
| `xai-grok-pager` | TUIscrollback、prompt、modal、渲染;用户手册也在这里 |
| `xai-grok-shell` | Agent runtime + leader / stdio / headless 入口 |
| `xai-grok-tools` | 工具实现(terminal、文件编辑、搜索……) |
| `xai-grok-workspace` | 宿主文件系统、VCS、执行、checkpoint |
从上游继承的约束:
- **根目录 `Cargo.toml` 是生成的,当只读。** 改各 crate 自己的 `Cargo.toml`。它还带着 `[patch.crates-io]` 里对 `our-forks/async-openai` 的 git pin。
- **永远指定 crate`-p <crate>`)。** 全 workspace 编译慢到不现实;`build.sh` 会强制带 `-p`
- 动上游代码时,先看它已有的测试还成不成立。
## 示例补丁
`patches/0001``0002` 的提交信息标了 `[EXAMPLE PATCH]`,演示「改上游 Rust」和「新增文件」两种补丁,不需要就可以删。`0001` 同时也是 fork 品牌标记(`--version` 打出 `[newgrok]`),删之前想清楚。
删补丁文件要带 `FORCE=1`——`work/` 里还留着对应提交,删完之后 `patches/``work/` 少,安全检查会拦下来(它无法区分「你故意删的」和「你忘了 rebuild」):
```sh
rm patches/0001-*.patch patches/0002-*.patch
make apply FORCE=1
```
## 许可
上游代码为 Apache-2.0(见 `work/LICENSE`)。`patches/` 里的改动同样按 Apache-2.0 分发。
本仓库是非官方 fork,与 xAI 无关。
+42 -165
View File
@@ -1,189 +1,66 @@
# newgrok — `xai-org/grok-build` 的 patch 化开发工作流
# newgrok
以 CraftBukkit / Spigot 的模型,在**不接受外部 PR 的上游**之上维护本地改动
[`xai-org/grok-build`](https://github.com/xai-org/grok-build) 的非官方 fork。上游是 xAI 的终端 coding agent(全屏 TUI `grok`);本仓库在其上叠了一组可回放的补丁
## 为什么是 patch,而不是 fork 后直接改
本仓库与 xAI 无关。编出来的二进制会标成 `grok [newgrok] <version>`,不会和官方发行版搞混。
上游 [`xai-org/grok-build`](https://github.com/xai-org/grok-build) 有两个决定性特征:
改代码、跟进上游、补丁怎么排,见 [CONTRIBUTING.md](CONTRIBUTING.md)。
1. 它是 xAI 内部 monorepo 的**周期性单向导出**。历史是一串线性的 `Synced from monorepo`
压扁提交,根目录 `SOURCE_REV` 记录对应的内部 commit。上游随时会把整棵树重新投放一次。
2. `CONTRIBUTING.md` 明确写着**不接受任何外部 PR 或补丁**。
## 这个 fork 多了什么
也就是说:改动推不回去,而上游会不断整体覆盖。直接在 fork 上改,每次同步都要打一场
merge 混战,而且没人说得清「我们到底改了什么」。
### `/rc` — 当前会话镜像到 grok-glance
这正是 CraftBukkit 面对 Mojang 的处境。解法也一样——**把改动本身作为版本库里的唯一真相**:
> `patches/` 是源码,`work/` 是编译产物。
## 目录结构
`/rc` 把你正在用的会话经 ACP 镜像到 grok-glance 服务器。终端里的 TUI 完全不受影响:不重启、不切 headless、不把会话交出去。glance 变成同一会话的第二个视图,两端都能跟 turn 流、发 prompt、打断正在跑的 turn,以及回答权限请求、提问和 plan 批准。**两边都能答;谁先答谁赢**,另一边的对话框会自己关掉。
```
newgrok/ ← 你提交的仓库
├── patches/ ★ 唯一真相:0001-*.patch,一个补丁一件事
├── scripts/ 工作流脚本
├── upstream.rev 钉住的上游 commit
├── Makefile
├── upstream/ 派生:上游的纯净 clone .gitignore
└── work/ 派生:upstream@rev + patches.gitignore
/rc 开关
/rc on 连接(start / connect
/rc off 断开(stop / disconnect
/rc status 看链路状态
```
对照 Spigot
`~/.grok/config.toml` 里配 glance
| 这里 | Spigot / BuildTools |
|------|---------------------|
| `upstream/` | CraftBukkit 的 clone(纯净上游) |
| `work/` | 打完补丁的工作目录 |
| `patches/` | `CraftBukkit-Patches/` |
| `scripts/apply-patches.sh` | `applyPatches.sh` |
| `scripts/rebuild-patches.sh` | `rebuildPatches.sh` |
| `upstream.rev` | `versions/*.json` 里的版本钉 |
```toml
[remote_control]
url = "wss://glance.example.com/api/acp/agent"
api_key = "glance_sk_..."
auto_start = false # 启动就连,不用先敲 /rc
```
`upstream/` `work/` 都**不进 git**——它们任何时候都能从 `upstream.rev` + `patches/`
一字不差地重建。
`GROK_RC_URL` / `GROK_RC_API_KEY` / `GROK_RC_AUTO_START` 会覆盖文件里的值。API key 更适合走环境变量,不要把 bearer token 写进明文配置。服务端用 `glance apikey add <name>` 签发。
## 快速开始
链路只活在当前会话里,退出即断,什么都不落盘。glance 连不上会退避重试,`/rc status` 会说明原因——对端挂了、慢了或中途被杀,都不会拖垮本地会话。远端 cancel 记成 `Client("glance")`(和按 Esc 同一类),远端 prompt 带 `clientIdentifier`,会话日志里分得清是谁做的。
### 专用工具优先
每个主会话用户 turn 会写入一份**只写一次、之后不再改**的 dispatch checklist,让模型在动手前先考虑 `explore` / search / read / plan / `deep-research`,而不是自己开一轮宽搜索,或拿 bash 去 `cat` / `grep` / `find` / `ls`。checklist 按当前工具集渲染进用户消息,前缀对 prompt cache 是稳定的。
如果 bash 还是做了有专用工具的文件操作,工具结果后面会跟一条 **nudge**(命令已经执行过了,只提醒下次换工具)。子 agent 同样会收到这条 nudge。
两段 reminder 在送给 compaction 摘要模型之前会被剥掉,避免把 agent 内部指令写进会话摘要。
### 会话级 `prompt_cache_key`
主 turn 会显式钉上会话级的 `prompt_cache_key`(就是 conversation id)。Responses 路径以前靠 `x_grok_conv_id` 回退也能走到同一把钥匙;钉死之后,Chat Completions 的 `user` 字段、以及 recap / `/btw` 这类复用父会话前缀的旁路调用,都会共用同一个 key。
刻意不按 agent 加后缀:那样会把旁路调用设计上要蹭的前缀缓存拆开。
## 从源码构建
```sh
make setup # 装 Rust 工具链(1.94.0,由 rust-toolchain.toml 钉住)+ dotslash/protoc
make setup # 装 Rust 1.94.0 + dotslash/protoc(幂等,可重跑)
make apply # 用 upstream.rev + patches/ 生成 work/
make build # 编 work/target/debug/xai-grok-pager
make run # 直接启动 TUI
make build # 编 work/target/debug/xai-grok-pager
make run # 编完直接开 TUI
```
`make setup` 是幂等的,可以随时重跑
`make setup` 会把工具链装到 `~/.cargo`,脚本自己把它加进 `PATH`。若要在交互 shell 里直接用 `cargo`fish 执行 `source ~/.cargo/env.fish`bash 执行 `source ~/.cargo/env`
> Rust 装在 `~/.cargo`,脚本会自己把它加进 `PATH`。若要在交互 shell 里直接用 `cargo`
> fish 执行 `source ~/.cargo/env.fish`bash 执行 `source ~/.cargo/env`。
第一次启动会打开浏览器做官方登录,见上游的 [authentication guide](https://github.com/xai-org/grok-build/blob/main/crates/codegen/xai-grok-pager/docs/user-guide/02-authentication.md)。打好补丁之后,更完整的用户手册在 `work/crates/codegen/xai-grok-pager/docs/user-guide/``/rc` 写在 slash commands 那一页)。
## 日常循环
改代码永远在 `work/` 里,**一个提交 = 一个补丁**:
```sh
make apply # 拿到干净的、打好补丁的树
$EDITOR work/crates/codegen/.../foo.rs # 改
cd work && git add -A && git commit -m '...' # 提交(提交信息就是补丁标题)
cd .. && make rebuild # 写回 patches/
git add patches/ && git commit -m '...' # 提交到 fork 仓库
```
**`make rebuild` 是唯一的出口。** 没跑过它的改动,下次 `make apply` 就没了。
修改已有的补丁,用普通的 git 手段就行——`work/` 是个正常的 git 仓库:
```sh
cd work
git rebase -i base # 改写、合并、重排、删除任意补丁
cd .. && make rebuild # patches/ 会整体重新生成
```
随时看当前状态:
```sh
make status
```
## 跟进上游同步
```sh
make update-dry # 只看会拉进来什么,不动任何东西
make update # 重新钉 upstream.rev,并把补丁前移到新基线
```
`make update` 会:
1. `git fetch` 上游,列出 `upstream.rev..origin/main` 之间的新提交和 `SOURCE_REV` 变化
2.`upstream.rev` 更新为最新
3. 在新基线上重放 `patches/`
4. 干净通过则自动 `make rebuild`,让补丁上下文对齐新代码
冲突时脚本会停下并给出解决步骤——和处理 rebase 冲突完全一样:
```sh
cd work
git status # 看冲突文件
$EDITOR <冲突文件> # 消掉冲突标记
git add -A
git am --continue # 或 git am --skip / git am --abort
cd .. && make rebuild # 把前移后的结果写回去
```
补丁数量多的时候,冲突是这个模型的固有成本,也是它的价值所在:冲突精确地指出上游改动
撞上了你的哪一处改动,而不是让它悄悄消失。
## 全部命令
| 命令 | 作用 |
|------|------|
| `make setup` | 安装工具链(rustup / dotslash / protoc |
| `make apply` | `upstream.rev + patches/ → work/`;加 `FORCE=1` 丢弃 `work/` 里的未提交改动 |
| `make rebuild` | `work/``base` 之上的提交 → `patches/` |
| `make update` | 跟进最新上游同步并前移补丁 |
| `make update-dry` | 预览上游差异,不做修改 |
| `make build` / `make release` | 编译 debug / release 二进制 |
| `make check` / `make clippy` / `make fmt` | 快速类型检查 / lint / 格式化 |
| `make test` | 跑测试(`PKG=<crate>` 指定 crate |
| `make run` | 编译并启动 TUI |
| `make status` | 显示当前钉住的 rev、补丁数、`work/` 状态 |
| `make clean` | 删 `work/target` |
| `make distclean` | 删 `work/``upstream/`(不动 `patches/` |
`make build` 之后的额外参数会透传给 cargo,例如 `./scripts/build.sh --release --locked`
## 安全网
脚本刻意不会静默吞掉工作成果:
- `work/` 有**未提交改动**时,`make apply` 拒绝执行(提示先 commit + rebuild,或 `FORCE=1`
- `work/` 的提交数和 `patches/` 的文件数**对不上**时同样拒绝——这基本意味着你忘了 `make rebuild`
- `git am` 进行到一半时,`make rebuild` / `make build` 会拒绝执行,而不是产出半成品
- `make apply``git clean` 显式保留 `work/target`,否则每次打补丁都要付一次冷编译的代价
## 补丁格式
`rebuild` 用的是:
```
git format-patch --no-stat -N --zero-commit --full-index --no-signature
```
- `--zero-commit` —— 否则每次 rebase 后每个补丁的 `From <sha>` 行都会变,`git diff` 里全是噪音
- `--full-index` —— 给 `git am --3way` 留下按 blob 哈希回退的余地,上游漂移时更容易自动合上
## 构建依赖
`make setup` 会处理,这里说明它在做什么:
- **Rust 1.94.0** —— 由上游 `rust-toolchain.toml` 钉住,rustup 自动装
- **protoc** —— 只有 `xai-grok-tools-api` 需要(编译 `proto/grok-tools.proto`)。上游的解析顺序是
`$PROTOC` → 向上查找 `bin/protoc``PATH``bin/protoc` 是个
[DotSlash](https://dotslash-cli.com) wrapper,会按需下载 protoc 29.3;所以 `setup.sh`
装 dotslash,失败则回退到系统 `protobuf-compiler`
两个 git 依赖(`our-forks/async-openai``helix-editor/nucleo`)都是公开可访问的。
## 示例补丁
`patches/` 里预置了两个补丁,提交信息都标了 `[EXAMPLE PATCH]`
1. **`0001`** 改上游 Rust 源码——给 `xai-grok-version``FORK_NAME` / `fork_tag()`
并接进 pager 的版本输出,于是 `--version` 显示 `grok [newgrok] 1.0.3 (…)`
标记插在 `grok ` 之后而不是追加到末尾,因为 `main.rs`
`version_output_writer_preserves_channel_aware_contract` 断言该行仍以 channel label
(或 `)`)结尾——**改上游代码时顺手确认它的测试仍然成立**,这个补丁本身就是个例子。
2. **`0002`** 新增文件——根目录 `FORK.md`
不需要就直接删。注意要带 `FORCE=1`——`work/` 里还留着这两个补丁的提交,而删掉补丁文件后
`patches/``work/` 少,安全检查会拦下来(它无法区分「你故意删的」和「你忘了 rebuild」):
```sh
rm patches/0001-*.patch patches/0002-*.patch
make apply FORCE=1
```
日常开发循环、跟进上游、以及 `make` 全表见 [CONTRIBUTING.md](CONTRIBUTING.md)。
## 许可
上游代码为 Apache-2.0(见 `work/LICENSE`)。`patches/` 里的改动同样按 Apache-2.0 分发。
本仓库是非官方 fork,与 xAI 无关。
@@ -18,24 +18,24 @@ test asserts the line still ends with the channel label (or ")").
delete the patch file and run 'make apply'.
diff --git a/crates/codegen/xai-grok-pager-bin/src/main.rs b/crates/codegen/xai-grok-pager-bin/src/main.rs
index ac1adc4fc7800e507742d4ae4bb7067edf2959f5..19238e835bb37d8d9670c70b0402d43caf248827 100644
index 29beeff904c352b1c4f029c972312a83521f44db..0a618cf1b6331e5d9699ea4c44a99ae081fcc1bf 100644
--- a/crates/codegen/xai-grok-pager-bin/src/main.rs
+++ b/crates/codegen/xai-grok-pager-bin/src/main.rs
@@ -1788,7 +1788,8 @@ fn install_heap_profile_hooks() {
@@ -1787,7 +1787,8 @@ fn install_heap_profile_hooks() {
}
fn version_text(channel_label: &str) -> String {
format!(
- "grok {}\n",
+ "grok {} {}\n",
+ xai_grok_version::fork_tag(),
xai_grok_version::display_version_with_commit(env!("VERSION_WITH_COMMIT"), channel_label,)
)
}
xai_grok_version::display_version_with_commit(
xai_grok_version::full_version(),
channel_label,
diff --git a/crates/codegen/xai-grok-version/src/lib.rs b/crates/codegen/xai-grok-version/src/lib.rs
index 29fdfc1e64ea1df96f0ca3e1afe78c9ac37843bd..f56d96d34b828a0c10267fca407bc0b14e0c2a55 100644
index b7d224fc92d5e25ba5dae888fb60335c3c0933ad..200d31e3d27e71bae5537ad93e210cbfd86adc3e 100644
--- a/crates/codegen/xai-grok-version/src/lib.rs
+++ b/crates/codegen/xai-grok-version/src/lib.rs
@@ -9,6 +9,15 @@ pub const VERSION: &str = match option_env!("GROK_VERSION") {
@@ -11,6 +11,15 @@ pub const VERSION: &str = match option_env!("GROK_VERSION") {
None => env!("CARGO_PKG_VERSION"),
};
@@ -48,12 +48,12 @@ index 29fdfc1e64ea1df96f0ca3e1afe78c9ac37843bd..f56d96d34b828a0c10267fca407bc0b1
+ format!("[{FORK_NAME}]")
+}
+
/// [`TEST_VERSION_ENV`] override first, then [`VERSION`]. Trimmed so
/// non-semver-aware callers can pass the result straight into parsing.
pub fn installed() -> String {
@@ -72,4 +81,12 @@ mod tests {
assert_eq!(display_version(""), VERSION);
assert!(display_version(" [stable]").ends_with("[stable]"));
/// Runtime-injected `"<version> (<shortcommit>)"` string. Only the release
/// binary stamps the commit hash in its own build.rs and injects it here at
/// startup, so the big lib crates don't recompile on every commit.
@@ -100,4 +109,12 @@ mod tests {
set_full_version("second (bbbbbbb)");
assert_eq!(full_version(), "first (aaaaaaa)");
}
+
+ /// The fork marker must stay non-empty and bracketed -- the pager splices it
@@ -0,0 +1,108 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: iceBear67 <icebear67@sfclub.cc>
Date: Sat, 15 Aug 2026 06:39:14 +0000
Subject: [PATCH] Add [remote_control] config section for the /rc glance bridge
diff --git a/crates/codegen/xai-grok-shell/src/agent/config.rs b/crates/codegen/xai-grok-shell/src/agent/config.rs
index 2a2ab0491b226cee84fd968869a2daaff707f9a7..be842a31d205db6bc88ac8f46c0af8e2cecbb4ef 100644
--- a/crates/codegen/xai-grok-shell/src/agent/config.rs
+++ b/crates/codegen/xai-grok-shell/src/agent/config.rs
@@ -1023,6 +1023,79 @@ pub struct CliConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub session_picker_grouped: Option<bool>,
}
+/// `[remote_control]` section: the `/rc` bridge to a grok-glance server.
+///
+/// `/rc` mirrors the session you are already in to glance over ACP -- it does not
+/// restart it, switch it to headless, or hand it over. Both ends stay live and
+/// either can drive the session.
+///
+/// Every field has an env override, which is how the API key is normally supplied:
+/// a bearer token in a plaintext config file is a poor default.
+#[derive(Clone, Debug, Default, Serialize, Deserialize)]
+#[serde(default)]
+pub struct RemoteControlConfig {
+ /// Glance WebSocket endpoint, e.g. `wss://glance.example.com/api/acp/agent`.
+ /// Env `GROK_RC_URL`.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub url: Option<String>,
+ /// Bearer token presented on the WebSocket upgrade. Env `GROK_RC_API_KEY`.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub api_key: Option<String>,
+ /// Connect at session start instead of waiting for `/rc`. Env `GROK_RC_AUTO_START`.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub auto_start: Option<bool>,
+ /// Session updates retained for replay when glance (re)connects.
+ /// See [`RemoteControlConfig::DEFAULT_REPLAY_BUFFER`].
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub replay_buffer: Option<usize>,
+}
+impl RemoteControlConfig {
+ /// Bounded so a long session cannot grow the bridge without limit; large
+ /// enough that a browser attaching mid-session still sees useful history.
+ pub const DEFAULT_REPLAY_BUFFER: usize = 2048;
+ /// Env wins over config, and an empty value counts as unset -- an exported
+ /// but blank `GROK_RC_URL` should not mask a working config entry.
+ fn env_override(key: &str) -> Option<String> {
+ std::env::var(key)
+ .ok()
+ .map(|v| v.trim().to_owned())
+ .filter(|v| !v.is_empty())
+ }
+ pub fn resolved_url(&self) -> Option<String> {
+ Self::env_override("GROK_RC_URL").or_else(|| {
+ self.url
+ .as_deref()
+ .map(str::trim)
+ .filter(|v| !v.is_empty())
+ .map(str::to_owned)
+ })
+ }
+ pub fn resolved_api_key(&self) -> Option<String> {
+ Self::env_override("GROK_RC_API_KEY").or_else(|| {
+ self.api_key
+ .as_deref()
+ .map(str::trim)
+ .filter(|v| !v.is_empty())
+ .map(str::to_owned)
+ })
+ }
+ pub fn auto_start_enabled(&self) -> bool {
+ Self::env_override("GROK_RC_AUTO_START")
+ .map(|v| matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes" | "on"))
+ .or(self.auto_start)
+ .unwrap_or(false)
+ }
+ pub fn replay_buffer_len(&self) -> usize {
+ self.replay_buffer
+ .filter(|n| *n > 0)
+ .unwrap_or(Self::DEFAULT_REPLAY_BUFFER)
+ }
+ /// Both a URL and a key are required; `/rc` reports which one is missing
+ /// rather than dialing an endpoint that will reject it.
+ pub fn is_configured(&self) -> bool {
+ self.resolved_url().is_some() && self.resolved_api_key().is_some()
+ }
+}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct DiagnosticsConfig {
@@ -1361,6 +1434,9 @@ pub struct Config {
pub paths: PathsConfig,
#[serde(default, skip_serializing)]
pub cli: CliConfig,
+ /// `[remote_control]` section: the `/rc` bridge to grok-glance.
+ #[serde(default, skip_serializing)]
+ pub remote_control: RemoteControlConfig,
#[serde(default, skip_serializing)]
pub models: ModelsConfig,
#[serde(default, skip_serializing)]
@@ -1767,6 +1843,7 @@ impl Default for Config {
feedback: FeedbackConfig::default(),
paths: PathsConfig::default(),
cli: CliConfig::default(),
+ remote_control: RemoteControlConfig::default(),
models: ModelsConfig::default(),
harness: HarnessConfig::default(),
relay: RelayConfig::default(),
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,143 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: iceBear67 <icebear67@sfclub.cc>
Date: Sun, 16 Aug 2026 03:13:22 +0000
Subject: [PATCH] Pin a session-stable prompt_cache_key on main turns
Main turns already reached the Responses wire as prompt_cache_key via
the x_grok_conv_id fallback. Set the field explicitly so Chat Completions
can put the same session id in `user`, and so recap /btw keep sharing
one key. Do not agent-scope: that would split the prefix those side
calls are built to ride.
diff --git a/crates/codegen/xai-chat-state/src/actor/request_builder.rs b/crates/codegen/xai-chat-state/src/actor/request_builder.rs
index 64f497a0b0419b167faea18e35b32b941080620e..109a97521e590dc1f14d367c3d349600001558f8 100644
--- a/crates/codegen/xai-chat-state/src/actor/request_builder.rs
+++ b/crates/codegen/xai-chat-state/src/actor/request_builder.rs
@@ -106,7 +106,10 @@ impl ChatStateActor {
x_grok_deployment_id: None,
x_grok_user_id: None,
trace,
- prompt_cache_key: None,
+ // Same session id the Responses mapping already fell back to via
+ // `x_grok_conv_id`. Set it explicitly so Chat Completions `user`
+ // and side-calls share one key without depending on that fallback.
+ prompt_cache_key: Some(conv_id.clone()),
reasoning_effort: self.state.sampling_config.reasoning_effort,
json_schema: None,
}
diff --git a/crates/codegen/xai-chat-state/src/actor/tests.rs b/crates/codegen/xai-chat-state/src/actor/tests.rs
index 07d581d48e78a7c69e78a7f0d8d2ce1844a3190b..dee7b542755b8d97dfcc4e621d01bb809488d017 100644
--- a/crates/codegen/xai-chat-state/src/actor/tests.rs
+++ b/crates/codegen/xai-chat-state/src/actor/tests.rs
@@ -1576,6 +1576,7 @@ async fn build_request_includes_all_messages() {
assert_eq!(request.items.len(), 2);
assert_eq!(request.x_grok_conv_id, Some("conv-1".to_string()));
assert_eq!(request.x_grok_req_id, Some("req-1".to_string()));
+ assert_eq!(request.prompt_cache_key, Some("conv-1".to_string()));
}
#[tokio::test]
diff --git a/crates/codegen/xai-grok-sampling-types/src/conversation.rs b/crates/codegen/xai-grok-sampling-types/src/conversation.rs
index 7bd0cd870c5400ef4d3fb2ba6ce50cfc53ffd62c..db582daced370d89cc1158d5cbbf81b7a156ba4f 100644
--- a/crates/codegen/xai-grok-sampling-types/src/conversation.rs
+++ b/crates/codegen/xai-grok-sampling-types/src/conversation.rs
@@ -627,10 +627,29 @@ pub struct ConversationRequest {
/// JSON Schema for structured output (strict mode).
pub json_schema: Option<serde_json::Value>,
/// Sticky routing key for prompt-cache reuse; overrides `x_grok_conv_id` for routing.
+ ///
+ /// Session-scoped, not agent-scoped: recap and `/btw` replay the parent
+ /// conversation under this key. A per-agent suffix would split the prefix
+ /// cache those side-calls are built to share. Only the Responses mapping
+ /// puts this field on the wire; Chat Completions surfaces the same value
+ /// as `user` (see [`Self::session_cache_key`]).
pub prompt_cache_key: Option<String>,
}
impl ConversationRequest {
+ /// Session-stable key for prompt-cache sticky routing.
+ ///
+ /// Prefer an explicit [`Self::prompt_cache_key`], then session id, then
+ /// conv id. Callers that share a conversation prefix (main turn, recap,
+ /// `/btw`) must resolve to the same string.
+ pub fn session_cache_key(&self) -> Option<&str> {
+ self.prompt_cache_key
+ .as_deref()
+ .or(self.x_grok_session_id.as_deref())
+ .or(self.x_grok_conv_id.as_deref())
+ .filter(|s| !s.is_empty())
+ }
+
/// Strip every image; returns the stripped URLs.
pub fn strip_images(&mut self) -> Vec<Arc<str>> {
strip_images_where(&mut self.items, |_| true)
@@ -2416,6 +2435,35 @@ mod tests {
use crate::tool_overrides::*;
use assert_matches::assert_matches;
+ #[test]
+ fn session_cache_key_prefers_explicit_then_session_then_conv() {
+ let mut req = ConversationRequest {
+ x_grok_conv_id: Some("conv".into()),
+ x_grok_session_id: Some("session".into()),
+ prompt_cache_key: Some("explicit".into()),
+ ..Default::default()
+ };
+ assert_eq!(req.session_cache_key(), Some("explicit"));
+ req.prompt_cache_key = None;
+ assert_eq!(req.session_cache_key(), Some("session"));
+ req.x_grok_session_id = None;
+ assert_eq!(req.session_cache_key(), Some("conv"));
+ req.x_grok_conv_id = Some(String::new());
+ assert_eq!(req.session_cache_key(), None);
+ }
+
+ #[test]
+ fn chat_completions_user_carries_session_cache_key() {
+ let req = ConversationRequest {
+ items: vec![ConversationItem::user("hi")],
+ model: Some("test-model".into()),
+ prompt_cache_key: Some("sess-1".into()),
+ ..Default::default()
+ };
+ let mapped = ChatCompletionRequest::from(req);
+ assert_eq!(mapped.user.as_deref(), Some("sess-1"));
+ }
+
/// Keeps `forwards_prompt_cache_key()` honest against each mapping: a key that never reaches the wire looks like a 0% cache hit, not a bug.
#[test]
fn prompt_cache_key_reaches_the_wire_only_where_the_backend_claims() {
diff --git a/crates/codegen/xai-grok-sampling-types/src/conversation/chat_completions.rs b/crates/codegen/xai-grok-sampling-types/src/conversation/chat_completions.rs
index dac77d82f7f96836f5f4ed26f2e12eb13164e4f5..05a683317ae774f3ba674202ff12ae3ae50f56ec 100644
--- a/crates/codegen/xai-grok-sampling-types/src/conversation/chat_completions.rs
+++ b/crates/codegen/xai-grok-sampling-types/src/conversation/chat_completions.rs
@@ -251,6 +251,8 @@ impl From<ChatResponseMessage> for ConversationItem {
impl From<ConversationRequest> for ChatCompletionRequest {
fn from(req: ConversationRequest) -> Self {
+ // Copy before any field moves — `session_cache_key` borrows `req`.
+ let user = req.session_cache_key().map(str::to_owned);
let messages: Vec<ChatRequestMessage> = conversation_to_chat_messages(req.items);
let tools_is_empty = req.tools.is_empty();
@@ -295,7 +297,7 @@ impl From<ConversationRequest> for ChatCompletionRequest {
top_p: req.top_p,
frequency_penalty: None,
presence_penalty: None,
- user: None,
+ user,
tools,
tool_choice,
search_parameters: None,
diff --git a/crates/codegen/xai-grok-sampling-types/src/conversation/chat_completions_tests.rs b/crates/codegen/xai-grok-sampling-types/src/conversation/chat_completions_tests.rs
index 8d2afa36362471676804bfa8676d0294ca56049d..1a29113ca0e467b9a5e39faa06cd468d4f45a242 100644
--- a/crates/codegen/xai-grok-sampling-types/src/conversation/chat_completions_tests.rs
+++ b/crates/codegen/xai-grok-sampling-types/src/conversation/chat_completions_tests.rs
@@ -52,6 +52,7 @@ fn test_conversation_request_to_chat_completion() {
assert_eq!(chat_req.model, Some("grok-3".to_string()));
assert_eq!(chat_req.temperature, Some(0.7));
assert_eq!(chat_req.messages.len(), 2);
+ assert_eq!(chat_req.user, None);
}
#[test]
+29
View File
@@ -0,0 +1,29 @@
#!/usr/bin/env bash
# Shallow work/ checkout for GitHub Actions.
#
# Unlike apply-patches.sh this does not clone upstream/ (that copy is a full
# history clone for make update). CI only needs the pinned SHA + patches/.
source "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
REV="$(read_rev)"
info "Fetching upstream ${REV:0:12} -> work/"
rm -rf "$WORK_DIR"
git init "$WORK_DIR"
git -C "$WORK_DIR" remote add origin "$UPSTREAM_URL"
git -C "$WORK_DIR" fetch --depth 1 origin "$REV"
git -C "$WORK_DIR" checkout --force --detach FETCH_HEAD
git -C "$WORK_DIR" checkout -B "$WORK_BRANCH"
git -C "$WORK_DIR" tag -f "$BASE_TAG"
git -C "$WORK_DIR" config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git -C "$WORK_DIR" config user.name "github-actions[bot]"
shopt -s nullglob
patches=("$PATCHES_DIR"/*.patch)
shopt -u nullglob
if ((${#patches[@]})); then
git -C "$WORK_DIR" am --3way --keep-cr --whitespace=nowarn "${patches[@]}"
fi
info "work/ ready: upstream ${REV:0:12} + ${#patches[@]} patch(es)"
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env bash
# Copy a cargo --target release binary into dist/ with a checksum sidecar.
#
# Required env:
# TARGET rustc triple (selects work/target/$TARGET/release/)
# ARTIFACT basename without extension (grok-linux-amd64, …)
# EXE filename cargo wrote (xai-grok-pager or xai-grok-pager.exe)
source "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
: "${TARGET:?TARGET is required}"
: "${ARTIFACT:?ARTIFACT is required}"
: "${EXE:?EXE is required}"
src="$WORK_DIR/target/$TARGET/release/$EXE"
[[ -f "$src" ]] || {
echo "missing $src" >&2
ls -la "$WORK_DIR/target/$TARGET/release" >&2 || true
exit 1
}
mkdir -p "$ROOT/dist"
if [[ "$EXE" == *.exe ]]; then
dest="$ROOT/dist/${ARTIFACT}.exe"
else
dest="$ROOT/dist/${ARTIFACT}"
strip "$src" || true
fi
cp "$src" "$dest"
{
echo "artifact=$(basename "$dest")"
echo "target=${TARGET}"
echo "git=${GITHUB_SHA:-}"
echo "upstream=$(read_rev)"
echo "rustc=$(rustc --version)"
} > "$ROOT/dist/${ARTIFACT}.txt"
if command -v sha256sum >/dev/null; then
(cd "$ROOT/dist" && sha256sum "$(basename "$dest")" "${ARTIFACT}.txt" > "${ARTIFACT}.sha256")
else
(cd "$ROOT/dist" && shasum -a 256 "$(basename "$dest")" "${ARTIFACT}.txt" > "${ARTIFACT}.sha256")
fi
info "packaged $dest"
+110
View File
@@ -0,0 +1,110 @@
#!/usr/bin/env bash
# Run a command with its disk-write rate held under a ceiling.
#
# Cargo on this box can saturate the virtual disk badly enough to take the
# whole machine out. The usual levers are unavailable here and it is worth
# recording why, so nobody burns an afternoon rediscovering it:
#
# cgroup v2 io.max -- /sys/fs/cgroup is mounted ro and cannot be remounted
# or re-mounted elsewhere; the container has no
# CAP_SYS_ADMIN, so this fails even under sudo.
# vm.dirty_bytes -- /proc/sys is read-only.
# ionice -- the scheduler is mq-deadline, where IO priority is a
# no-op, and /sys/block/*/queue/scheduler is read-only
# so BFQ cannot be selected.
# nice -- CPU only. Writeback happens in kernel flusher threads
# that never see the nice value, so a niced build
# saturates the disk exactly as fast as an un-niced one.
#
# What is left is duty-cycling from userspace: sample the block device's
# written-sectors counter, and when the observed rate exceeds the ceiling,
# SIGSTOP the whole process group until it drops. Crude, but it acts on
# measured throughput rather than on a scheduler hint, which is the only
# property that actually matters here.
#
# scripts/paced.sh -- make build
# MBPS=30 scripts/paced.sh -- env CARGO_CMD=test PKG=xai-grok-pager ./scripts/build.sh --lib
set -euo pipefail
# Measured on this box, idle, 1 GB transfers:
#
# read 210 MB/s (O_DIRECT and buffered alike)
# write 50 MB/s (identical for buffered+fdatasync, O_DIRECT 1M, O_DIRECT 64k)
#
# Write is pinned at ~50 MB/s regardless of block size or IO mode, so it is a
# volume-level cap rather than disk physics, and it is 4x slower than read.
# A short burst can reach ~150 MB/s on what look like burst credits -- do not
# calibrate against that, it is the number that made an earlier 40 MB/s ceiling
# useless. 15 MB/s is under a third of sustained capacity, which leaves the rest
# of the machine usable while a build runs.
MBPS="${MBPS:-15}" # write ceiling in MB/s, averaged over one window
DEV="${DEV:-vdb}" # block device to watch; the disk, not the partition
WINDOW="${WINDOW:-1}" # sampling window in seconds
[[ "${1:-}" == "--" ]] && shift
if [[ $# -eq 0 ]]; then
echo "usage: [MBPS=60] [DEV=vdb] scripts/paced.sh -- <command...>" >&2
exit 2
fi
stats_field() {
# /proc/diskstats field 10 is sectors written, in 512-byte units.
awk -v dev="$DEV" '$3 == dev { print $10; exit }' /proc/diskstats
}
if [[ -z "$(stats_field)" ]]; then
echo "paced: no such device in /proc/diskstats: $DEV" >&2
exit 1
fi
# Its own process group, so one signal reaches cargo and every rustc it spawned.
#
# `set -m` rather than setsid: setsid only forks when it is already a group
# leader, so under `cmd &` it usually execs in place -- but when it does fork,
# $! is setsid's pid, which exits immediately. The loop below then sees the
# child "finish" instantly and reports success while the real build is still
# running. Job control gives the background job its own pgid deterministically.
set -m
"$@" &
child=$!
pgid=$child
set +m
cleanup() {
# Never leave the build stopped: a SIGSTOPped process group that outlives
# this script looks exactly like a hang.
kill -CONT -- "-$pgid" 2>/dev/null || true
kill -TERM -- "-$pgid" 2>/dev/null || true
}
trap cleanup INT TERM
ceiling_sectors=$(( MBPS * 1024 * 1024 / 512 * WINDOW ))
prev=$(stats_field)
stalled=0
while kill -0 "$child" 2>/dev/null; do
sleep "$WINDOW"
now=$(stats_field)
delta=$(( now - prev ))
prev=$now
if (( delta > ceiling_sectors )); then
# Stop for as long as the overshoot implies, capped so the build still
# makes progress even when something else on the box is writing hard.
pause=$(( delta / ceiling_sectors ))
(( pause > 4 )) && pause=4
stalled=$(( stalled + pause ))
kill -STOP -- "-$pgid" 2>/dev/null || true
sleep "$pause"
kill -CONT -- "-$pgid" 2>/dev/null || true
prev=$(stats_field)
fi
done
rc=0
wait "$child" || rc=$?
trap - INT TERM
echo "paced: done rc=$rc (throttled ${stalled}s at ${MBPS}MB/s ceiling on /dev/$DEV)" >&2
# Propagate the wrapped command's status. Swallowing it once already produced a
# confident "build finished, exit 0" for a build that had in fact been killed.
exit "$rc"
+1 -1
View File
@@ -1,3 +1,3 @@
# Pinned upstream revision of https://github.com/xai-org/grok-build.git
# Bump with: make update (never edit by hand unless you know why)
eb267feff13129e568df38fb6fdf0ceb65f735d6
5163763e703c319e4554c2f455535c5adb6e51e8