Skip to content

Windows packaged app crashes on startup and bundled NekoQA LoRA adapter is a Git LFS pointer instead of a valid GGUF #16

Description

@LeoooKang

Windows packaged app crashes on startup and bundled NekoQA LoRA adapter is a Git LFS pointer instead of a valid GGUF

Summary

On Windows, the packaged MiniCPM Desk Pet app can fail to launch / flash-crash during normal startup when the MiniCPM sidecar is warmed up.

After debugging the installed app, I found two separate issues:

  1. Startup / sidecar crash issue

    • The packaged Electron app spawns minicpm-sidecar.exe.
    • The sidecar's Windows watchdog path uses os.kill(pid, 0) as a process-liveness probe.
    • On this Windows environment, that code path can raise OSError: [WinError 87] 参数错误 and then a SystemError.
    • The app exits shortly after sidecar startup.
  2. Bundled NekoQA / 猫娘 adapter issue

    • The bundled adapter_model.f16.gguf is not a real GGUF file.
    • It is a 133-byte Git LFS pointer file.
    • llama.cpp fails to load it with:
      • invalid magic characters: 'vers', expected 'GGUF'
    • This makes the 猫娘 / NekoQA persona unusable in the installed app.

There is also an additional local-network/proxy issue:

  1. Gateway → llama-server requests can be affected by inherited proxy environment
    • The sidecar gateway uses httpx to call local llama-server at 127.0.0.1.
    • With inherited system proxy settings, gateway requests to local llama-server returned HTTP 502.
    • Reproducing the same httpx request with trust_env=False worked.
    • Adding NO_PROXY=* to the sidecar process environment fixed the packaged runtime locally.

Environment

  • OS: Windows 11 Pro
  • Installed app path:
C:\Users\27437\AppData\Local\Programs\MiniCPM Desk Pet\MiniCPM Desk Pet.exe
  • Packaged app archive:
C:\Users\27437\AppData\Local\Programs\MiniCPM Desk Pet\resources\app.asar
  • Sidecar binary:
C:\Users\27437\AppData\Local\Programs\MiniCPM Desk Pet\resources\sidecar-bin\minicpm-sidecar.exe
  • llama-server binary:
C:\Users\27437\AppData\Local\Programs\MiniCPM Desk Pet\resources\sidecar-bin\llama-server.exe
  • User data directory:
C:\Users\27437\AppData\Roaming\minicpm-desk-pet
  • Model file:
C:\Users\27437\AppData\Roaming\minicpm-desk-pet\models\MiniCPM5-1B-Q8_0.gguf

User-visible symptoms

Symptom 1: App cannot stay open

The app appeared to launch, but then quickly exited / flash-crashed.

Observed behavior:

  • Onboarding could reach the model settings page, then the app exited.
  • When a model was already configured, normal startup triggered sidecar warmup and the app exited shortly after.
  • Windows Event Viewer did not show a native crash entry for MiniCPM / Electron.
  • Disabling GPU did not fix it.
  • Disabling tray / integrations / permission bubbles did not fix it.
  • Forcing a missing model so the app entered onboarding made the app stay alive, which suggested the normal model-present sidecar warmup path was involved.

Symptom 2: 猫娘 / NekoQA persona cannot be used

The app showed a bundled 猫娘 adapter entry, but attempting to load it caused llama-server failure.


Relevant packaged source code / behavior

Electron starts the sidecar and passes MINICPM_PARENT_PID

The Electron sidecar manager builds the sidecar environment approximately like this:

const env = {
  ...process.env,
  PYTHONUNBUFFERED: "1",
  MINICPM_LOG_DIR: this.logFile ? path.dirname(this.logFile) : (process.env.MINICPM_LOG_DIR || ""),
  MINICPM_ADAPTER_DIR: this.adapterDir || process.env.MINICPM_ADAPTER_DIR || "",
  MINICPM_ACTIVE_ADAPTER: this.activeAdapterPath || process.env.MINICPM_ACTIVE_ADAPTER || "",
  MINICPM_PARENT_PID: String(process.pid),
};

Then it spawns:

proc = spawn(this.sidecarBin, argsCommon, {
  cwd: path.dirname(this.sidecarBin),
  env,
});

Sidecar resolves parent PID

In the packaged sidecar source:

def _resolve_parent_pid() -> int:
    raw = (os.environ.get("MINICPM_PARENT_PID") or "").strip()
    if raw:
        try:
            return int(raw)
        except ValueError:
            pass
    return os.getppid()

Then:

parent_pid = _resolve_parent_pid()
ParentWatchdog(parent_pid).start()

Sidecar watchdog uses _pid_alive

In gateway/lifecycle.py:

def _pid_alive(pid: int) -> bool:
    if pid <= 0:
        return False
    try:
        os.kill(pid, 0)
        return True
    except ProcessLookupError:
        return False
    except PermissionError:
        return True
    except OSError:
        return False

ParentWatchdog.start() calls _pid_alive():

def start(self) -> None:
    log = get_logger()
    if self.target_pid <= 1:
        log.info("parent watchdog disabled (target_pid=%d)", self.target_pid)
        return
    if not _pid_alive(self.target_pid):
        ...

Crash / error evidence

When the sidecar received an empty / invalid / unsuitable parent PID, it crashed with:

OSError: [WinError 87] 参数错误

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "__main__.py", line 127, in <module>
  File "__main__.py", line 100, in main
  File "gateway\lifecycle.py", line 85, in start
  File "gateway\lifecycle.py", line 160, in _pid_alive
SystemError: <built-in function kill> returned a result with an exception set
[PYI-20276:ERROR] Failed to execute script '__main__' due to unhandled exception!

The key failing line is:

os.kill(pid, 0)

Observed in logs:

[minicpm-chat] spawn binary C:\Users\27437\AppData\Local\Programs\MiniCPM Desk Pet\resources\sidecar-bin\minicpm-sidecar.exe --port 18765
[sidecar! ] 2026-05-28 10:12:18 INFO [minicpm.gateway] file logger -> C:\Users\27437\AppData\Roaming\minicpm-desk-pet\logs\sidecar-internal.log
[sidecar! ] OSError: [WinError 87] 参数错误
...
SystemError: <built-in function kill> returned a result with an exception set
[PYI-20276:ERROR] Failed to execute script '__main__' due to unhandled exception!
[minicpm-chat] sidecar exited code=1 signal=null

Debugging findings

1. Startup warmup triggers the bad path

Normal startup path includes sidecar warmup:

if (!(_minicpmOnboarding && _minicpmOnboarding.shouldShow())) {
  setTimeout(() => {
    if (_minicpmChat && typeof _minicpmChat.warmup === "function") {
      _minicpmChat.warmup();
    }
  }, 500);
}

Temporarily replacing:

_minicpmChat.warmup();

with a no-op made the Electron app stay alive.

That showed the app/window itself was not the problem; the failure was tied to starting the sidecar.

2. Setting MINICPM_PARENT_PID=0 prevents the watchdog crash

Because ParentWatchdog.start() already disables itself for target_pid <= 1, setting:

MINICPM_PARENT_PID: "0"

made the sidecar log:

parent watchdog disabled (target_pid=0)

and the sidecar could start instead of crashing in _pid_alive().

3. A stale llama-server PID file can trigger the same _pid_alive() problem

Even after disabling the parent watchdog, the same _pid_alive() function is also used by stale llama-server cleanup:

if not _pid_alive(pid):
    clear_pid_file(pid_file)
    return

If llama-server.pid points to a stale PID, this path can also raise the same Windows os.kill(pid, 0) error.

Example log:

2026-05-28 11:41:10 ERROR [minicpm.gateway] initial llama-server start failed: <built-in function kill> returned a result with an exception set
OSError: [WinError 87] 参数错误

Traceback (most recent call last):
  File "gateway\server.py", line 321, in lifespan
  File "gateway\llama_client.py", line 237, in start
  File "gateway\lifecycle.py", line 259, in cleanup_stale_llama_server
  File "gateway\lifecycle.py", line 160, in _pid_alive
SystemError: <built-in function kill> returned a result with an exception set

Deleting the stale PID file allowed llama-server startup to proceed.

PID file path:

C:\Users\27437\AppData\Roaming\minicpm-desk-pet\logs\llama-server.pid

4. httpx inherited proxy settings and returned 502 for local llama-server

After sidecar and llama-server were alive, /api/chat through the gateway failed with:

llama-server /v1/chat/completions HTTP 502:

Direct calls to llama-server with urllib succeeded.

The same call with httpx failed by default:

async with httpx.AsyncClient(...) as client:
    async with client.stream(
        "POST",
        "http://127.0.0.1:18766/v1/chat/completions",
        json=body,
    ) as resp:
        print(resp.status_code)

Result:

502

But the same request succeeded with:

httpx.AsyncClient(..., trust_env=False)

or when the sidecar process environment included:

NO_PROXY=*

This suggests the gateway should either:

  • create the internal llama-server client with trust_env=False, or
  • ensure 127.0.0.1 / localhost bypass proxy via environment.

Local workaround that restored basic functionality

I patched the installed app.asar in place using same-length byte replacements.

Final environment field became:

MINICPM_PARENT_PID:"0",NO_PROXY:"*",

This was an equal-length replacement for the previous parent PID field in the ASAR, to avoid changing ASAR offsets.

After this local patch and clearing stale llama-server.pid, clean verification passed:

/state -> 200
/api/health -> 200
/api/chat -> start / delta / end

Example health response after fix:

{
  "ok": true,
  "alive": true,
  "backend": "llama.cpp",
  "accel": "cpu",
  "device": "cpu",
  "dtype": "gguf",
  "model_dir": "C:\\Users\\27437\\AppData\\Roaming\\minicpm-desk-pet\\models\\MiniCPM5-1B-Q8_0.gguf",
  "model_name": "MiniCPM5-1B-Q8_0.gguf",
  "adapter": null,
  "persona": "default",
  "llama_server": {
    "status": "ok"
  },
  "port": 18766
}

Streaming chat also worked:

data: {"event": "start"}

data: {"event": "delta", "content": "..."}

data: {"event": "end"}

NekoQA / 猫娘 adapter issue

The bundled NekoQA adapter file path is:

C:\Users\27437\AppData\Roaming\minicpm-desk-pet\adapters\lora_nekoqa_v2_fixedbase_adapter_20260524_0959\adapter_model.f16.gguf

The app manifest points to it:

{
  "id": "preset:nekoqa",
  "path": "C:\\Users\\27437\\AppData\\Roaming\\minicpm-desk-pet\\adapters\\lora_nekoqa_v2_fixedbase_adapter_20260524_0959\\adapter_model.f16.gguf",
  "displayName": "猫娘",
  "aliases": ["猫娘", "宝宝", "neko"],
  "persona": "neko",
  "source": "bundled"
}

But the file was only 133 bytes and contained:

version https://git-lfs.github.com/spec/v1
oid sha256:49bbcc128053da67161feeac0acfbee6d09b883f2f48f76b1e7401fbb6708e99
size 22436736

So it is a Git LFS pointer, not the real GGUF.

llama.cpp failed with:

gguf_init_from_file_ptr: invalid magic characters: 'vers', expected 'GGUF'
llama_adapter_lora_init: failed to apply lora adapter: failed to load lora adapter file from ...\adapter_model.f16.gguf
common_init_result: failed to load lora adapter ...
common_init_from_params: failed to create context with model ...

This caused /api/load-adapter to fail with HTTP 500 and left llama-server not running.


Local workaround that fixed 猫娘

The bundled USAGE.md points to the public GGUF repo:

https://huggingface.co/DennisHuang648/MiniCPM5-1B-NekoQA-v2-LoRA-GGUF

I downloaded the real file:

adapter_model.f16.gguf

Expected size:

22436736 bytes

Expected magic:

GGUF

Then replaced the LFS pointer file in both locations:

C:\Users\27437\AppData\Roaming\minicpm-desk-pet\adapters\lora_nekoqa_v2_fixedbase_adapter_20260524_0959\adapter_model.f16.gguf

and:

C:\Users\27437\AppData\Local\Programs\MiniCPM Desk Pet\resources\adapters\lora_nekoqa_v2_fixedbase_adapter_20260524_0959\adapter_model.f16.gguf

I backed up the pointer files as:

adapter_model.f16.gguf.lfs-pointer.bak

After replacing with the real GGUF, /api/load-adapter succeeded:

{
  "ok": true,
  "adapter": "C:\\Users\\27437\\AppData\\Roaming\\minicpm-desk-pet\\adapters\\lora_nekoqa_v2_fixedbase_adapter_20260524_0959\\adapter_model.f16.gguf",
  "persona": "neko"
}

Health then showed:

{
  "ok": true,
  "alive": true,
  "adapter": "C:\\Users\\27437\\AppData\\Roaming\\minicpm-desk-pet\\adapters\\lora_nekoqa_v2_fixedbase_adapter_20260524_0959\\adapter_model.f16.gguf",
  "persona": "neko",
  "llama_server": {
    "status": "ok"
  },
  "port": 18766
}

I also set:

{
  "active_adapter_id": "preset:nekoqa"
}

in:

C:\Users\27437\AppData\Roaming\minicpm-desk-pet\minicpm-prefs.json

After restarting the app, the sidecar automatically loaded the NekoQA adapter:

"persona": "neko"

and chat returned streamed delta events successfully.


Expected behavior

  1. The Windows packaged app should start normally and remain open.
  2. Starting sidecar warmup should not terminate Electron.
  3. Parent watchdog and stale PID cleanup should not crash on Windows.
  4. Local gateway → llama-server calls should not be affected by system proxy settings.
  5. The bundled 猫娘 / NekoQA adapter should be a valid GGUF file, not a Git LFS pointer.
  6. Loading the bundled 猫娘 adapter should not crash llama-server.

Actual behavior

  1. App can flash-crash / exit during startup sidecar warmup.
  2. Sidecar can fail in gateway.lifecycle._pid_alive.
  3. httpx calls from gateway to local llama-server can return HTTP 502 when proxy env is inherited.
  4. Bundled 猫娘 adapter file is a Git LFS pointer.
  5. Loading the 猫娘 adapter causes llama.cpp to fail with invalid magic characters: 'vers', expected 'GGUF'.

Suggested fixes

A. Fix Windows process liveness probing

Avoid using os.kill(pid, 0) as the Windows PID-liveness implementation.

Possible alternatives:

  • Use Windows APIs via ctypes:
    • OpenProcess
    • GetExitCodeProcess
    • CloseHandle
  • Or use psutil.pid_exists(pid) if adding psutil is acceptable.
  • Or guard the current implementation more defensively so SystemError cannot escape.

Suggested shape:

def _pid_alive(pid: int) -> bool:
    if pid <= 0:
        return False

    if platform.system() == "Windows":
        return _pid_alive_windows(pid)

    try:
        os.kill(pid, 0)
        return True
    except ProcessLookupError:
        return False
    except PermissionError:
        return True
    except OSError:
        return False
    except SystemError:
        return False

And for Windows, use a real process-handle probe instead of os.kill(pid, 0).

B. Make stale PID cleanup robust

cleanup_stale_llama_server() should never prevent gateway startup just because stale PID probing fails.

The module comment says lifecycle helper exceptions should be swallowed:

All three are cooperative and tolerate failure: any exception is logged
and swallowed, never propagated, so a buggy lifecycle helper can't keep
the gateway from booting.

But the observed SystemError from _pid_alive() did propagate and blocked initial llama-server start.

At minimum, catch SystemError wherever _pid_alive() is used or inside _pid_alive() itself.

C. Avoid proxy env for localhost llama-server calls

In LlamaServer.start(), the internal httpx.AsyncClient could be created with:

self._client = httpx.AsyncClient(
    base_url=f"http://{self.host}:{self.port}",
    timeout=httpx.Timeout(connect=5.0, read=None, write=30.0, pool=5.0),
    trust_env=False,
)

This avoids system proxy variables affecting local 127.0.0.1 calls.

Alternatively, when spawning the sidecar, set a more specific no-proxy value:

NO_PROXY=127.0.0.1,localhost

or preserve existing NO_PROXY while appending localhost.

D. Fix packaged NekoQA adapter asset

The packaged app should include the actual GGUF file instead of the Git LFS pointer.

Current bad file:

adapter_model.f16.gguf
size: 133 bytes
contents: Git LFS pointer

Expected real file:

adapter_model.f16.gguf
size: 22436736 bytes
magic: GGUF

The build / packaging process probably needs to fetch Git LFS files before packaging, e.g.:

git lfs install
git lfs pull

or validate packaged adapter files before release.

Suggested release validation:

  • Check every bundled .gguf starts with GGUF.
  • Reject package if any .gguf starts with version https://git-lfs.github.com/spec/v1.

E. Validate adapters before attempting llama-server restart

Before calling llama-server with --lora <path>, validate the adapter file header.

For example:

def is_gguf(path: Path) -> bool:
    try:
        with path.open("rb") as f:
            return f.read(4) == b"GGUF"
    except OSError:
        return False

If invalid, return a clear user-facing error like:

Adapter file is not a valid GGUF file. It may be a Git LFS pointer or incomplete download.

This would avoid crashing / stopping the existing base llama-server.


Verification after local workaround

After local patching, I verified:

Base mode

GET http://127.0.0.1:23334/state
-> 200
GET http://127.0.0.1:18765/api/health
-> 200

Health showed:

{
  "ok": true,
  "alive": true,
  "adapter": null,
  "persona": "default",
  "llama_server": {
    "status": "ok"
  },
  "port": 18766
}

Chat:

POST http://127.0.0.1:18765/api/chat
-> stream returned start / delta / end

Neko mode

After replacing the Git LFS pointer with the real GGUF:

POST http://127.0.0.1:18765/api/load-adapter
-> 200

Response:

{
  "ok": true,
  "adapter": "C:\\Users\\27437\\AppData\\Roaming\\minicpm-desk-pet\\adapters\\lora_nekoqa_v2_fixedbase_adapter_20260524_0959\\adapter_model.f16.gguf",
  "persona": "neko"
}

Health:

{
  "ok": true,
  "alive": true,
  "adapter": "C:\\Users\\27437\\AppData\\Roaming\\minicpm-desk-pet\\adapters\\lora_nekoqa_v2_fixedbase_adapter_20260524_0959\\adapter_model.f16.gguf",
  "persona": "neko",
  "llama_server": {
    "status": "ok"
  },
  "port": 18766
}

Chat:

POST http://127.0.0.1:18765/api/chat
-> stream returned start / delta / end

After setting:

"active_adapter_id": "preset:nekoqa"

and restarting, health automatically showed:

"persona": "neko"

Impact

This affects first-run / normal startup and bundled persona functionality on Windows.

Without local workaround:

  • App may appear broken because it exits during startup.
  • Sidecar may crash or fail to start.
  • Base chat may fail due to local proxy inheritance.
  • Bundled 猫娘 persona cannot work because the packaged adapter is not a valid GGUF.

With the local fixes above, the app works normally in both base mode and NekoQA mode.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions