Skip to content

fix: make qemu guest sessions usable for all bundled tools - #76

Draft
xai wants to merge 3 commits into
mainfrom
fix/qemu-guest-environment
Draft

fix: make qemu guest sessions usable for all bundled tools#76
xai wants to merge 3 commits into
mainfrom
fix/qemu-guest-environment

Conversation

@xai

@xai xai commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

What it does

Fixes #75.

Three defects kept --backend qemu sessions from working with the bundled tools.

  • Bundles too large to boot. The guest has no disk: the initramfs is unpacked
    into a ramfs that becomes the rootfs, and during unpacking the kernel holds both
    the image and its contents. opencode's 1.4 GB image did not fit the fixed
    4096 MiB guest and panicked without an init. The builder now strips build-time
    caches (npm, pip, uv) and /boot, compresses the image with zstd, and records
    packed/unpacked sizes so the host derives guest memory from the actual bundle
    (never below qemuMinMemoryMiB). An undersized bundle is rejected on the host
    instead of dying inside the kernel. The compressed image is padded to a 4-byte
    boundary so the kernel still recognizes the per-run overlay archive appended to it.
  • Console reported 0x0. The emulated serial console carries no window size, so
    codex and opencode rendered one column wide and claude and pi fell back to a fixed
    80x24 box. The generated run script now seeds the console from the host terminal.
  • opencode's SQLite store failed on 9p. opencode.db runs in WAL mode inside the
    config store, and WAL shared-memory files need mmap semantics there; every query
    failed and the TUI painted an empty frame. opencode now declares
    qemuStoreCacheMmap, as codex already did. The bundle also gets a populated
    /etc/hosts — Docker bind-mounts it into the provisioning container, so the export
    left an empty placeholder and the guest answered ::1 for localhost first.

How to test

Requires Docker (bundle build) and qemu-system-x86_64. Resize your terminal to
something clearly wider than 80 columns first.

  1. enclave --backend qemu --tool opencode — the bundle builds, the guest boots, and
    the TUI comes up filling the terminal with no Failed query: PRAGMA wal_checkpoint
    errors. Send a prompt, exit, restart: the session history is still there.
  2. Same for --tool codex, --tool claude, --tool pi — each TUI uses the full
    terminal width, not one column and not 80x24.
  3. In a guest shell: stty size matches your host terminal, and
    getent hosts localhost answers 127.0.0.1.

Follow-ups

The console size is applied once at startup. Resizing the host terminal mid-session
does not reach the guest, because the console is not a controlling terminal there and
no SIGWINCH is delivered.

Breaking changes

  • This PR introduces breaking changes and has been coordinated with maintainers.

Review checklist

xai added 3 commits September 10, 2026 23:11
The opencode bundle's 1.4 GB uncompressed initramfs could not be unpacked in
the fixed 4096 MiB guest: the kernel holds the image and the ramfs it unpacks
into at the same time, so unpacking failed and the guest panicked without an
init.

Strip the npm/pip/uv caches and /boot from the rootfs, compress the image with
zstd, and derive the guest memory from the built sizes so a large bundle gets
the memory it needs. An undersized bundle is now rejected on the host instead
of dying inside the kernel.

The compressed image is padded to a 4-byte boundary and the per-run
concatenation keeps every segment aligned, because unpack_to_rootfs() only
recognizes the appended overlay archive there.
The emulated serial console carries no window size, so the guest console
reported 0x0. Terminal UIs that trust it rendered one column wide (codex,
opencode) and those with a fallback drew into a fixed 80x24 box regardless of
the real terminal (claude, pi).

Seed the console from the host terminal in the generated run script. The size
is applied once at startup: the console is not a controlling terminal in the
guest, so a later resize delivers no SIGWINCH.
opencode keeps its state in opencode.db, a WAL-mode SQLite database inside its
config store, which the qemu backend mounts over 9p. WAL shared-memory files
need mmap semantics there, so every query failed ("Failed query: PRAGMA
wal_checkpoint(PASSIVE)"), the storage layer died before the app logged
anything, and the TUI painted an empty frame. codex already declares
qemuStoreCacheMmap for the same reason.

Also populate /etc/hosts in the bundle. Docker bind-mounts it into the
provisioning container, so the export left an empty placeholder and the guest
resolved "localhost" through musl built-ins, answering ::1 first.
@xai xai added the bug Something isn't working label Sep 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

qemu backend: sessions for tools other than claude and pi are unusable

1 participant