[Release] 2026-09-27: image tools, sd-server install, drop on Linux, clean exit - #315
Merged
Merged
Conversation
models.entries.qwen38-27b-cnq for Qwen3.8-27B-CNQ4.5.cnq (crow-nest serve, port 8099, not booted from here). Checked live against the running serve: model_key_for -> qwen38-27b-cnq, reasoning fixed high, no menu, sampling 1.0 / 0.95 / 20 / 0.0 / 0.0 (the 27B card's thinking row). Reasoning levels measured on that serve (#74 method): none/low/medium/high 200, max/minimal/off/High/empty 400 - as Flash-Next: the 27B's template and tokenizer files are byte-identical to Flash-Next's. check_operating_point 10/10; test_crow_core 1529 OK, test_crow 476 OK, test_crow_remote 53 OK; test_crow_gui 915 with the same 6 folder-picker errors as before this change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LyBV1tX2JWocGLTjFkNYMh
Stop set INTERRUPT, but nothing read it while a tool ran: the command ran to its end, the round's remaining calls still ran, and one more chat request went to serve before the stream reader dropped it. - _bounded_run: INTERRUPT is a third stop reason beside the clock and the cap, read every 0.25 s tick. Stop sends SIGTERM to the group and the scope, waits up to STOP_GRACE (2 s), then the existing SIGKILL sweep. The output so far is kept. Windows: taskkill /T /F (kill_tree) for the clock, the cap and Stop. - tool_run_command: "error: stopped by the user after <N>s -- ...". - run_turn: a call after the Stop does not run (STOPPED + " -- not run"); a stopped call is booked like a declined one; the turn ends after the round and at the head of the next round, so no further request is sent. - Background jobs of earlier calls are not touched (#218 contract). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LyBV1tX2JWocGLTjFkNYMh
…said (#309) The default command scope drops MemoryHigh=7G and keeps MemoryMax=8G, MemorySwapMax=0, OOMPolicy=kill -- the shape CROW_COMMAND_MEMORY_MAX already had. memory.high throttles and never kills; with no swap an over-budget job sat at 7.40 GiB for 10 min 47 s (full avg60=88.13) and never reached the 8G kill. A scope that still holds a process when run_command returns is recorded. run_tool_cached prefixes every later tool result with a note when that job was killed at its ceiling (session.slice oom_kill rose while the scope vanished) or its memory.pressure reads full avg60 >= 50. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LyBV1tX2JWocGLTjFkNYMh
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LyBV1tX2JWocGLTjFkNYMh
…e X (#312, #313) #312: on Linux the drop's path came from pywebview's GTK drag handler, which reads the drag data only as text; robin's drop from Nautilus reached no chip and no request. The page now reads every dropped picture with FileReader and hands it to the new Api.stage_image_data, which writes it like a paste and stages it under the dropped name; the path, where pywebview delivers it, is skipped for a picture already read. Classified `upload` for the phone mirror. One [drop] line per drop in crow.log. Harness: an 11,528,977-byte PNG dropped synthetically into WebKitGTK arrives in Python byte-identical. #313: after the window's X the process could stay (pid 34193, main thread in a futex wait, SIGINT ignored), and from a terminal Ctrl+C was always needed. arm_exit_watchdog() after webview.start: 5 s later a process still alive logs its threads and stacks to crow.log, stops the MCP servers and exits. The blocker is not identified yet. test_crow_gui 921 OK (2 skipped; +6, 5 red without the fix), test_crow_core + test_crow 2020 OK, test_crow_remote 53 OK, ruff clean, check_shared_core 83/83, check_operating_point 10/10. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LyBV1tX2JWocGLTjFkNYMh
… hold the exit (#312, #313) #312, measured on robin's retest (crow.log 09:38:03, 09:38:47): WebKitGTK handed the page a file-manager drop with zero File objects. The page now reads the drag's text/uri-list when there are no files (dropUriPaths: file:// URIs -> local paths -> the normal drop route) and logs what each drop carried (drop_seen: types, file count, uri length). #313, cause measured with the exit watchdog's stacks on robin's close (crow.log 09:39:45): MainThread in threading._shutdown joining pywebview's JS-bridge thread Thread-14 (_call), stuck in evaluate_js waiting for WebKit's reply to an answer in flight when the window went (webview/util.py:335 non-daemon, platforms/gtk.py:693-695 no timeout). daemon_bridge_threads() makes those threads daemon before the window opens. Harness with one answer in flight at the close: 2/2 hung without, 2/2 ended in 1-2 s with it (crow_gui.daemon_bridge_threads itself). test_crow_gui 924 OK (2 skipped; the three new tests red on 0d9a092), ruff clean. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LyBV1tX2JWocGLTjFkNYMh
Second retest (crow.log 10:00:46, 10:00:49): the page saw types text/uri-list and text/html, zero files and an EMPTY uri text, and pywebview's GTK handler reads the drag data with get_text(), which is None for a uri-list. Crow now connects the WebKit widget's drag-data-received on the GTK main thread, keeps the non-empty SelectionData.get_uris(), and on_drop takes them once (30 s fresh) as paths for the normal drop route; the [drop] line names the source. Checked in a real pywebview window: the signal connects on WebKit2 WebView. test_crow_gui 927 OK (3 new), ruff clean. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LyBV1tX2JWocGLTjFkNYMh
…ghtbox (#308, #311) #308: Turn.tool_progress pushes {"k":"imgjob"} (throttled to 4/s per job; the first line, a phase change and every end pass). The page draws one figure per job in the chat flow (its own turn, so a folded round cannot hide it): the image's aspect ratio, a flow animation from --accent, --bevel and the surfaces only (keyframes move positions, never colours), still under prefers-reduced-motion, a phase line; error = red tile, stopped = still tile. One band mark per job; a replay draws a job that neither runs nor got its picture as "interrupted", never animating. #311: Turn.image_created vets (regular file, IMAGE_TYPES extension with matching magic bytes, <= IMAGE_MAX_BYTES, working area or an approved outside path) and pushes {"k":"image"}; the card replaces its job's tile in place (512-px GdkPixbuf preview; Windows shrinks in a canvas). The lightbox is a modal <dialog>: Show in folder (FileManager1.ShowItems, else reveal_command), i (image_info), Copy image, Copy path, Save as... (a phone gets Download: <a download> on the data URL), Open in viewer, Move to trash (two clicks, Gio trash / SHFileOperation+ALLOWUNDO, never unlinked), Close; Esc closes. Every image_* method re-vets the path against the images this window announced. Phone: thumb/full/info proxied (full is a data URL there), reveal/copy/open/trash desktop, save_as "download". The window warms the image server when the core offers image_tools_available/image_server_warm. test_crow_gui + test_crow_remote 1026 OK (3 skipped, pre-existing); 45 new tests, 35 red against ee4261f's crow_gui.py, 14 targeted mutations all red. ruff clean, node --check clean on both page variants. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LyBV1tX2JWocGLTjFkNYMh
, #308, #311) - generate_image(prompt, aspect_ratio, seed) and edit_image(images, instruction, description, aspect_ratio): Qwen-Image 2.1 over sd-server's async job API (img_gen + jobs), started on the first call, kept loaded, stopped at exit; image_server_warm() for surfaces. - edit runs B5 in one call: a ~1 MP edit (refs stretched by the server to calculate_dimensions(1024^2), vae_input_max_pixels=1048576), then img2img at full size, strength 0.25, the full description, no refs. - SdProgress reads the server's log (#308's table); TurnEvents gains tool_progress(name, state) and image_created(path, source, job), fed by a thread-local sink armed around each call in run_turn. - PNGs land in <root>/images/, never overwritten, and ride to the model like read_image's; a blind server keeps the result as a success. - "image" and "imgjob" session notes keep facts only. - Terminal: one redrawn line per call, "image: <path> (WxH, N KB)". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LyBV1tX2JWocGLTjFkNYMh
…#300, #308, #311) - push: "imgjob" is a session note kind since the core half, so the plain note append added a second mark per progress line next to the in-place update; it is skipped there (TheImageBandReplaysTests red on the merge). - image_model_dir: without CROW_IMAGE_MODEL_DIR, look beside the target of a linked <install>/models when the plain place is empty (install.sh's layout; robin's ~/Projects/models/qwen-image-2.1). New test. - test_crow_gui: a concrete /home path from the #312 uri test replaced (test_no_concrete_home_directory_is_written_down). - CHANGELOG Unreleased: the image tools, the chat tile, the lightbox. test_crow_core + test_crow 2074 OK (5 skipped), test_crow_gui + test_crow_remote 1026 OK (3 skipped), ruff clean, check_shared_core 85/85. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LyBV1tX2JWocGLTjFkNYMh
AnImageJobThatDoesNotFinishSaysWhyTests asserted image_tools_available() is False after leaving the patch that hides the binary; since 8eb5247 finds the real model beside the linked tree, the machine's real install answered True. 8eb5247's message claimed test_crow_core OK -- it was FAILED (failures=1) on exactly this test; corrected here. test_crow_core + test_crow: see below in the ticket comment. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LyBV1tX2JWocGLTjFkNYMh
…_core (#300) - tools/readme_image.py: the tool count is len(crow_core.BUILTIN_TOOLS) (30, was a hard-coded 28); the tool table must name every built-in tool or the script stops. - New "Images" section (desktop and mobile): generate_image 2752x1536 155.2 s warm / 175.3 s cold, edit_image two stages 105.7 + 55.3 s, measured beside crow-nest's 27B on one RTX 5090 (docs/reference/tools.md); the chat's animated placeholder. - Features: Images and Lightbox added; Vision names the drop/paste and the 27B. - Intro: one line for the images. Section rule no longer runs into long titles. - README.md alt text follows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LyBV1tX2JWocGLTjFkNYMh
tools/build-sd-server.sh builds sd-server and sd-cli from stable-diffusion.cpp 2f88688 (master-920-2f88688, submodule pins recorded and verified) with the CUDA 13.3 prefix and cmake/ninja that build-llama-server.sh already keeps under $CROW_HOME; it sources that script for ensure_cuda/ensure_cmake_ninja, which now runs main only when executed. SD_CUDA=ON, sm_120 (-> 120a), frontend off, DT_RPATH to $CROW_HOME/cuda/lib, ldd and --version checked, idempotent, disk guard; a binary of another commit in bin/ is kept as <name>-<commit>. install.sh --build-image-server / CROW_BUILD_IMAGE_SERVER=1 runs it and reports where the core looks for Qwen-Image 2.1. pack-release.ps1 -SdBuildDir adds sd-server.exe/sd-cli.exe with their DLLs resolved like llama-server's. Docs: Linux build, Windows recipe, tools reference. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LyBV1tX2JWocGLTjFkNYMh
…#300, #308, #311, #314) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LyBV1tX2JWocGLTjFkNYMh
Version 2.8.0 in cli/crow.py, install.ps1 and the operating-point manifest; CHANGELOG 2.8.0 with the release summary and a Changed section (#307's image README, install.ps1 seeing a running sd-server for #314); the README image regenerated (version pill). install.ps1's change is not run: no PowerShell on the Linux release machine. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LyBV1tX2JWocGLTjFkNYMh
…platform (#300) Windows CI on the release PR #315: test_the_file_lands_in_images_and_the_result_names_it expected 'images/…' and got 'images\…'. The relative path in the result now uses '/'. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LyBV1tX2JWocGLTjFkNYMh
This was referenced Sep 27, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
22 commits since origin/main (
9f56600), Crow 2.8.0.install.sh --build-image-serverbuilds sd-server + sd-cli (stable-diffusion.cpp 2f88688, CUDA);pack-release.ps1 -SdBuildDirfor the Windows zip (The installer does not provide sd-server #314)get_uris(), bytes from the page, phone mirror included) (An image dropped into the window on Linux is lost #312)Scope
Resolved issues (close on merge)
Closes #314 — The installer does not provide sd-server (Linux build measured: 3 min 10 s, rerun 1.4 s,
lddon<install>/cuda/lib; Windows-SdBuildDirnot run)Closes #310 — Stop does not interrupt a running run_command (live check pending: during a
sleep 110run_command press Stop -> the turn ends within ~2 s)Closes #309 — run_command ceiling thrashes big jobs instead of failing (live check pending: see the implementation comment)
Resolved, pending live verification
Issues shipped (already closed)
Checks
c54385a: test_crow_core + test_crow 2074 OK (5 skipped), test_crow_gui + test_crow_remote 1026 OK (3 skipped), ruff clean, check_shared_core 85/85, check_operating_point 10/10,install.sh --selftest51 checks, 0 failedsd-server-8097.log/ engine.log)9f56600: Windows CI caught the image result's relative path written with\; now/on every platform. CI green on ubuntu-latest and windows-latest.install.ps1,pack-release.ps1 -SdBuildDir), WindowsCHANGELOG
2.8.0 — 2026-09-27
Crow makes and edits images, and the 27B on crow-nest sees them.
generate_imageandedit_imagerunQwen-Image 2.1 on a resident sd-server beside crow-nest's dense 27B on one 32 GB card: 2752×1536 in about
2.6 min, an edit in two stages (1 MP edit, full-size redraw) because a 4 MP edit comes out grainy (#300, #308).
In the chat an animated square in the theme's colours becomes the picture; a lightbox opens it with folder,
download and info (#311), on the phone mirror too.
install.sh --build-image-serverbuilds sd-server (#314).Images dropped from the file manager reach the model on Linux (#312), the window's X ends the process (#313),
Stop interrupts a running command (#310), and a command over its memory ceiling dies instead of crawling (#309).
20 commits since v2.7.0.
Added
Crow makes and edits images ([meta] crow-nest toward a model-agnostic engine: 27B CNQ, images #300 phase 3, Image generation shows no progress in the window #308, Generated images are not shown in the chat window #311, 2026-09-27). Two tools on a resident
sd-server(stable-diffusion.cpp 2f88688, Qwen-Image 2.1, BF16 DiT streamed,--max-vram 7, text encoder on the CPU), started on first use or warmed at window start, stopped with Crow:generate_image(prompt, aspect_ratio): the model card's sizes (16:9 = 2752×1536). Measured beside crow-nest's 27B (8.15 GiB free): 155 s warm, 175 s cold, card peak 31,338 MiB, the 27B's output unchanged.edit_image(images, instruction, description): two stages in one call, because a 4 MP edit comes out grainy (measured E1/E2). Stage 1 edits at ~1 MP, the reference pipeline's size; stage 2 redraws the result at full size (img2img without references, strength 0.25). Measured 105.7 s + 55.3 s for 2752×1536, clean; chosen by robin from a side-by-side (crow-nest/decode_out/p3-img/compare-img2img.png).<working root>/images/; the model sees its own result (vision).install.sh --build-image-server(The installer does not provide sd-server #314, 2026-09-27): buildssd-serverandsd-cli(stable-diffusion.cpp 2f88688 with its four submodule pins, CUDA, sm_120) intobin/withtools/build-sd-server.sh, reusing the llama-server builder's CUDA toolkit; no web UI. Measured on the RTX 5090 box: 3 min 10 s (8 jobs), a rerun 1.4 s with nothing to do,lddresolving CUDA from<install>/cuda/lib. An oldersd-cliis kept assd-cli-<commit>. Windows:tools/pack-release.ps1 -SdBuildDirputssd-server.exeand its DLLs into the release zip, recipe indocs/user-guide/install.md; not run yet (needs Windows).Crow knows crow-nest's dense Qwen3.8-27B container ([meta] crow-nest toward a model-agnostic engine: 27B CNQ, images #300, 2026-09-27). New
models.entries.qwen38-27b-cnqforQwen3.8-27B-CNQ4.5.cnq, the container crow-nest'sserveopens on port 8099 (not booted from Crow, likeflash-next-cnq45-m).model_key_forpairs it with what the running serve reports (checked live: keyqwen38-27b-cnq), so a request to it carries the 27B card's rows (thinking 1.0 / 0.95 / 20 / 0.0 / 0.0, non-thinking 0.7 / 0.80 / 20 / 0.0 / 1.5), thinking fixed athigh(= the template's xhigh) and the 1024 reasoning cap, the same policy as the crow-nest Flash-Next point. Reasoning levels measured on the running 27B serve (The operating point is spelled in one file and every copy is checked against it #74 method): none / low / medium / high accepted, max / minimal / off / High / empty refused with 400; identical to Flash-Next's, since the 27B's chat template and tokenizer files are byte-identical to Flash-Next's.docs/operating-points.mdhas its row. Not measured: answer quality in Crow on this point.Changed
4664c3a,f1c941d): one picture per GitHub theme and a one-column variant for phones (docs/images/readme/, drawn bytools/readme_image.py), then the install lines, the docs links, the licence and Ko-fi; the old text front page lives indocs/archive/README-v2.7.0.md. For 2.8.0 the picture shows the image tools and takes the tool count fromcrow_core.BUILTIN_TOOLS(30, was drawn as 28) (0c08fb1).install.ps1sees a runningsd-server.exeholdingbin\during an update, as it did llama-server (The installer does not provide sd-server #314). Not run: no PowerShell on the Linux release machine.Fixed
An image dropped into the window on Linux is lost (An image dropped into the window on Linux is lost #312, 2026-09-27). The drop took the file's path from pywebview's GTK drag handler; robin's drops from the file manager reached no chip and no request. Measured on the retest (
crow.log09:38:03, 09:38:47): WebKitGTK handed the page the drop with zero File objects, so neither the path nor the bytes existed. The page now takes a picture's bytes when the drop carries files (FileReader→stage_image_data→ written like a paste → staged under its own name, also on the phone mirror), and otherwise reads the file manager's URIs where GTK has them: the second retest (crow.log10:00:46) showed the page typestext/uri-listandtext/htmlwith zero files and an EMPTY uri text, and pywebview reads the GTK drag data withget_text()(None for a uri-list), so Crow connects the WebKit widget'sdrag-data-receiveditself, keepsGtk.SelectionData.get_uris(), andon_dropturns them into paths for the normal drop route (drag_uri_path; the page-sidedropUriPathsstays for backends that expose the text). Every drop leaves[drop]lines incrow.log(types, file count, uri length). Harness: an 11,528,977-byte PNG dropped synthetically into WebKitGTK arrives byte-identical. Tests red without the fix; a real file-manager drop is robin's live check.The process stays alive after the window's X (The process stays alive after the window's X #313, 2026-09-27). robin's closed window left the process running (pid 34193, SIGINT ignored), and from a terminal Ctrl+C was always needed. Cause, measured with the new exit watchdog's stacks on robin's next close (
crow.log09:39:45): the main thread inthreading._shutdown, joining pywebview's JS-bridge threadThread-14 (_call), which waited inevaluate_jsfor WebKit's reply to an answer still in flight when the window went. pywebview runs bridge calls in non-daemon threads and waits on GTK with no timeout (webview/util.py:335,platforms/gtk.py:693-695). Crow now makes those threads daemon threads (daemon_bridge_threads) before the window opens. A harness with one answer in flight at the close hung 2 of 2 without it (killed after 15 s) and ended 2 of 2 in 1-2 s with it. The exit watchdog stays as the backstop: 5 s after the window loop returns, a process still alive logs its threads and stacks, stops the MCP servers and exits.Stop interrupts a running
run_command(Stop does not interrupt a running run_command #310, 2026-09-27). Before, Stop set a flag that nothing read while a tool ran: asleep 110poll ran to its end, every remaining call of the round still ran, and one more chat request went to serve before it was dropped (engine.log 2026-09-27 00:27:39, 535 tokens prefilled for nothing). Now Stop ends the running command within ~2 s: SIGTERM to its process group and its scope, up to 2 s for a cleanup, then the existing SIGKILL sweep; on Windowstaskkill /T /Fon the tree, for the clock and the cap too. The model readserror: stopped by the user after <N>s -- the command and everything it started in this call were ended: <command>plus the output so far, booked like a declined call, not a failure. The round's later calls do not run (error: stopped by the user -- not run), and no further request goes to the server. Jobs an earlier call put in the background (nohup … &) survive, as documented.build_bundleand the syntax check stop the same way. Not verified: the GUI click end to end and Windows.A command over its memory ceiling dies at the ceiling instead of crawling, and a background job's kill or thrash is said (run_command ceiling thrashes big jobs instead of failing #309, 2026-09-27).
run_command's scope dropsMemoryHigh=7G: the default isMemoryMax=8G,MemorySwapMax=0,OOMPolicy=kill, the shapeCROW_COMMAND_MEMORY_MAXalready had. memory.high throttles and never kills, and with no swap the anonymous part cannot be reclaimed.nohup sd-cli … &sat at 7.40 GiB of the 7G throttle for 10 min 47 s, in D state,memory.pressurefull avg60 = 88.13, loading at ~11–22 MB/s instead of ~5 GB/s, and never reached the 8G kill. The same argv under a 40G ceiling loaded all 397 tensors in 2.58 s.MemoryHigh=150M+MemoryMax=200Mwas still throttled whentimeout 20ended it (21.8 s);MemoryMax=200Malone killed it in 42 ms.note:(after the first line of anerror:result) when that job was killed at its ceiling (the kernel'soom_killcount forsession.slicerose while its scope vanished) or when its scope'smemory.pressurereadsfull avg60≥ 50 %. Before, the model polled the starving job 7 times and read "RUNNING".run_command(the ≤ 15 s threshold), and the live sd-cli case with the note (needs the GPU and serve's slot). A job that really needs more than 8G still needsCROW_COMMAND_MEMORY_MAXat GUI launch.🤖 Generated with Claude Code
https://claude.ai/code/session_01LyBV1tX2JWocGLTjFkNYMh