From 70dd2f68d7248a436db4461b1410e6ceb865440c Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 31 Aug 2026 16:21:37 +0000 Subject: [PATCH 1/2] Unstick the upstream sync automation and make validated bumps land themselves MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three failures had quietly stalled the pipeline: - Every llama.cpp release since ~b10651 requires a mtmd_helper_init_opt argument to mtmd_helper_bitmap_init_from_buf, so the daily pin workflow's macOS link gate rejected every bump for five straight days. Fix the call site in LlamaSession.swift and bump the pin to b10721 in the same change (the source can only match one side of the break). - The weekly wasm sync died in git ("shallow file has changed since we read it") from fetching a second ref into a fresh shallow submodule clone; clone the pinned tag directly instead. Past that, the wllama glue no longer compiles against current llama.cpp (use_mmap/use_mlock became llama_load_mode, json became common_json, eval_llama_cmpl_schema dropped its n_ctx argument, server-queue callbacks grew an is_yielding flag). Ship those fixes as tool/wllama-patches/, which the workflow now applies on top of the fork clone — compat fixes land here through a reviewable PR instead of pushes to the fork. - Validated pin PRs never merged: CI runs on GITHUB_TOKEN-pushed PRs sit at "action_required" forever, so chore/llama-cpp-pin idled open for a month while main drifted ~500 releases. The workflow's own gates are the validation, so merge the PR as soon as they pass. Scheduled failures now also file/update a GitHub issue instead of only emailing the owner, and CI gets concurrency cancellation for superseded PR pushes, job timeouts, and Flutter/pub caching. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_016MXK55KkWfy3KzVFXJ8D4A --- .github/workflows/ci.yml | 11 +++ .github/workflows/deploy-pages.yml | 1 + .github/workflows/sync-wllama-wasm.yml | 56 +++++++++++++- .github/workflows/update-llama-pin.yml | 50 ++++++++++++- CLAUDE.md | 32 ++++++-- darwin/Classes/LlamaSession.swift | 8 +- tool/versions.env | 4 +- .../0001-llama-b10721-server-api.patch | 74 +++++++++++++++++++ 8 files changed, 220 insertions(+), 16 deletions(-) create mode 100644 tool/wllama-patches/0001-llama-b10721-server-api.patch diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d1bb739..25a5fa7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,14 +5,22 @@ on: push: branches: [main] +# Superseded pushes to the same PR don't need their runs finished; main +# builds are kept so a merge never cancels its own validation. +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + jobs: analyze-test: runs-on: ubuntu-latest + timeout-minutes: 30 steps: - uses: actions/checkout@v4 - uses: subosito/flutter-action@v2 with: channel: stable + cache: true - run: flutter pub get - run: flutter analyze - run: flutter test @@ -31,6 +39,7 @@ jobs: # longer declares what darwin/Classes/LlamaExtShim.cpp re-declares. abi-check: runs-on: ubuntu-latest + timeout-minutes: 10 steps: - uses: actions/checkout@v4 - run: ./tool/check_llama_ext_abi.sh @@ -39,11 +48,13 @@ jobs: # app link resolves every llama.cpp symbol the plugin references. build-macos: runs-on: macos-latest + timeout-minutes: 45 steps: - uses: actions/checkout@v4 - uses: subosito/flutter-action@v2 with: channel: stable + cache: true - run: flutter pub get - run: flutter build macos --debug working-directory: example diff --git a/.github/workflows/deploy-pages.yml b/.github/workflows/deploy-pages.yml index c4a69c2..80890ee 100644 --- a/.github/workflows/deploy-pages.yml +++ b/.github/workflows/deploy-pages.yml @@ -29,6 +29,7 @@ jobs: - uses: subosito/flutter-action@v2 with: channel: stable + cache: true - uses: actions/configure-pages@v5 with: enablement: true diff --git a/.github/workflows/sync-wllama-wasm.yml b/.github/workflows/sync-wllama-wasm.yml index 0b34e0f..5f7b07c 100644 --- a/.github/workflows/sync-wllama-wasm.yml +++ b/.github/workflows/sync-wllama-wasm.yml @@ -21,6 +21,12 @@ name: Sync wllama wasm # loudly, and leaves the last known-good synced wasm in place. # tool/update_wllama.sh remains the manual/fallback path when you explicitly # want the npm artifact. +# +# Compat patches for upstream API drift live in tool/wllama-patches/ in THIS +# repo and are applied on top of the fork branch before building, so a drift +# fix can land here through a reviewable PR without pushing to the fork. A +# patch that stops applying (usually because the fork branch absorbed it) +# fails the run loudly — delete or regenerate it. on: schedule: @@ -30,6 +36,7 @@ on: permissions: contents: write pull-requests: write + issues: write concurrency: group: sync-wllama-wasm @@ -64,10 +71,19 @@ jobs: git clone --branch "${{ steps.pins.outputs.sync_ref }}" \ --depth 1 "https://github.com/${{ steps.pins.outputs.sync_repo }}" /tmp/wllama cd /tmp/wllama - git submodule update --init --depth 1 llama.cpp - git -C llama.cpp fetch --depth 1 origin \ - tag "${{ steps.pins.outputs.llama_tag }}" - git -C llama.cpp checkout "${{ steps.pins.outputs.llama_tag }}" + shopt -s nullglob + for patch in "$GITHUB_WORKSPACE"/tool/wllama-patches/*.patch; do + echo "Applying $(basename "$patch")" + git apply --verbose "$patch" + done + # Clone the pinned tag directly rather than initialising the + # submodule and fetching a second ref into its fresh shallow clone: + # that dance intermittently dies with "fatal: shallow file has + # changed since we read it" (it killed every scheduled run in + # Aug 2026). + rm -rf llama.cpp + git clone --branch "${{ steps.pins.outputs.llama_tag }}" --depth 1 \ + https://github.com/ggml-org/llama.cpp llama.cpp SKIP_COMPAT=1 bash scripts/build_wasm.sh test -f src/wasm/wllama.wasm @@ -120,6 +136,7 @@ jobs: - uses: subosito/flutter-action@v2 with: channel: stable + cache: true - run: flutter pub get - run: flutter analyze - run: flutter test @@ -174,3 +191,34 @@ jobs: else gh pr create --head "$BRANCH" --title "$TITLE" --body-file /tmp/pr-body.md fi + + # A red scheduled run otherwise only emails the owner. Keep one open issue + # as the visible, accumulating record; close it when the sync is green. + report-failure: + needs: [build, validate, open-pr] + if: always() && contains(needs.*.result, 'failure') && github.event_name == 'schedule' + runs-on: ubuntu-latest + steps: + - name: File or update the failure issue + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + TITLE='sync-wllama-wasm: scheduled run failing' + cat > /tmp/issue-body.md < /tmp/pr-body.md < \`$TAG\`. - Validated before this PR was opened: + Validated before this PR was opened (and auto-merged on that basis): - staging-ABI check (\`tool/check_llama_ext_abi.sh $TAG\`) - macOS link check (example app builds against the new xcframework) @@ -154,3 +166,35 @@ jobs: else gh pr create --head "$BRANCH" --title "$TITLE" --body-file /tmp/pr-body.md fi + gh pr merge "$BRANCH" --squash --delete-branch + + # A red scheduled run otherwise only emails the owner, which is how the + # gates stayed red for five straight days once. Keep one open issue as the + # visible, accumulating record; close it when the run is green again. + report-failure: + needs: [bump, abi-check, build-macos, open-pr] + if: always() && contains(needs.*.result, 'failure') && github.event_name == 'schedule' + runs-on: ubuntu-latest + steps: + - name: File or update the failure issue + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + TITLE='update-llama-pin: scheduled run failing' + cat > /tmp/issue-body.md <-xcframework.zip. -LLAMA_CPP_TAG=b10165 -LLAMA_XCFRAMEWORK_ZIP_SHA256=91f9107451bbd7939a0b8a9d43c1ed1658c59715a3bb12d77d178585d55e347d +LLAMA_CPP_TAG=b10721 +LLAMA_XCFRAMEWORK_ZIP_SHA256=a87958cbcf7c4e621e553c15d406d224db1fa3f6df5671f96f3b2d87a0081b20 WLLAMA_VERSION=3.5.1 WLLAMA_WASM_SOURCE=jamiewest/wllama@flutter-sync+llama.cpp-b10091 WLLAMA_WASM_SHA256=8bfd16b0a720243e2be4595d8dd4dece0c4958812ae0d59c9c1c8cbc2ab6d53f diff --git a/tool/wllama-patches/0001-llama-b10721-server-api.patch b/tool/wllama-patches/0001-llama-b10721-server-api.patch new file mode 100644 index 0000000..d6d3c2c --- /dev/null +++ b/tool/wllama-patches/0001-llama-b10721-server-api.patch @@ -0,0 +1,74 @@ +diff --git a/cpp/wllama-context.h b/cpp/wllama-context.h +index fa2cc7a..755d88b 100644 +--- a/cpp/wllama-context.h ++++ b/cpp/wllama-context.h +@@ -302,11 +302,11 @@ struct wllama_context + task.id = rd->get_new_id(); + task.index = 0; + // llama.cpp moved server_task::params_from_json_cmpl to a free +- // function (same argument list) in the b10xxx server refactor. ++ // function in the b10xxx server refactor; the slot n_ctx argument was ++ // dropped again around b10700. + task.params = server_schema::eval_llama_cmpl_schema( + vocab, + params, +- meta->slot_n_ctx, + meta->logit_bias_eog, + body); + task.params.res_type = res_type; +@@ -402,10 +402,16 @@ struct wllama_context + params.image_max_tokens = req.image_max_tokens.value; + + // model params +- if (req.use_mmap.not_null()) +- params.use_mmap = req.use_mmap.value; +- if (req.use_mlock.not_null()) +- params.use_mlock = req.use_mlock.value; ++ // llama.cpp replaced common_params.use_mmap/use_mlock with a single ++ // llama_load_mode enum around b10700; keep the boolean JS API and map it. ++ if (req.use_mmap.not_null() || req.use_mlock.not_null()) ++ { ++ const bool use_mmap = req.use_mmap.not_null() ? req.use_mmap.value : true; ++ const bool use_mlock = req.use_mlock.not_null() ? req.use_mlock.value : false; ++ params.load_mode = use_mmap ++ ? (use_mlock ? LLAMA_LOAD_MODE_MMAP_MLOCK : LLAMA_LOAD_MODE_MMAP) ++ : (use_mlock ? LLAMA_LOAD_MODE_MLOCK : LLAMA_LOAD_MODE_NONE); ++ } + if (req.n_gpu_layers.not_null()) + params.n_gpu_layers = req.n_gpu_layers.value; + if (req.model_alias.not_null()) +@@ -669,10 +675,11 @@ struct wllama_context + int embd_normalize = 2; + if (body.count("embd_normalize") != 0) + { +- embd_normalize = body.at("embd_normalize"); ++ embd_normalize = body.at("embd_normalize").get(); + } + +- auto tokenized_prompts = tokenize_input_prompts(vocab, nullptr, prompt, true, true); ++ auto tokenized_prompts = tokenize_input_prompts( ++ vocab, nullptr, prompt, true, true, mtmd_helper_init_opt_default()); + for (const auto &tokens : tokenized_prompts) + { + if (tokens.empty()) +@@ -728,7 +735,8 @@ struct wllama_context + rd = std::make_unique(ctx_server.get_response_reader()); + last_error = ""; + +- auto tokens = format_prompt_rerank(model, vocab, nullptr, query, document); ++ auto tokens = format_prompt_rerank( ++ model, vocab, nullptr, query, document, mtmd_helper_init_opt_default()); + server_task task = server_task(SERVER_TASK_TYPE_RERANK); + task.id = rd->get_new_id(); + task.index = 0; +@@ -943,7 +951,9 @@ void server_queue::start_loop(int64_t idle_sleep_ms) + queue_tasks.pop_front(); + + LOG_DBG("processing task, id = %d\n", task.id); +- callback_new_task(std::move(task)); ++ // The single-threaded wasm loop never yields, so the task can't be ++ // declined (upstream asserts declines only happen while yielding). ++ callback_new_task(std::move(task), /*is_yielding=*/false); + } + // all tasks in the current loop is processed, slots data is now ready + LOG_DBG("%s", "update slots\n"); From 6c4f5eb1c9736c856cf4fe8aff08f06a64e19947 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 31 Aug 2026 16:29:02 +0000 Subject: [PATCH 2/2] Await the 416-restart retry so its errors surface through the enclosing try Flutter stable moved to 3.47.2 (Dart 3.10), whose new unawaited_return_in_try_block lint fails 'flutter analyze' on the previously unawaited recursive download() retry. Awaiting it is the lint's own suggested fix; the retry opens its own HttpClient, so this client staying open until the retry finishes is benign. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_016MXK55KkWfy3KzVFXJ8D4A --- lib/src/runtime/model_downloader_io.dart | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/src/runtime/model_downloader_io.dart b/lib/src/runtime/model_downloader_io.dart index 0663b2f..6742b58 100644 --- a/lib/src/runtime/model_downloader_io.dart +++ b/lib/src/runtime/model_downloader_io.dart @@ -110,7 +110,14 @@ final class ModelDownloader { // between the final write and the rename). Restart cleanly so the // retry can't wedge on 416 forever. await part.delete(); - return download(url, directory: directory, onProgress: onProgress); + // Awaited so the retry's errors surface through this try (and the + // Dart 3.10 unawaited_return_in_try_block lint); the retry opens + // its own client, so holding this one until it finishes is benign. + return await download( + url, + directory: directory, + onProgress: onProgress, + ); } throw HttpException( 'The model download failed with HTTP ${response.statusCode}.',