Skip to content

Commit 5dd853f

Browse files
authored
Merge pull request #1551 from jaylfc/dev
Release 1.0.0-beta.17
2 parents 26f7cad + 092f6ab commit 5dd853f

10 files changed

Lines changed: 397 additions & 63 deletions

File tree

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,16 @@ Versions follow semver beta: `1.0.0-beta.N`, bumped on each dev->master promotio
77

88
## [Unreleased]
99

10+
## [1.0.0-beta.17] - 2026-07-02
11+
12+
### Fixed
13+
- Models: a failed model download no longer looks like an instant successful install. The failure now stays on the model card with the actual cause from the backend (for example a checksum mismatch or an unreachable download host) and a Retry button, instead of the progress UI silently disappearing (#1548).
14+
- Installer: the Docker Compose v2 plugin now installs on Debian (including vendor Pi images) by trying the Debian package name (`docker-compose-plugin`) before the Ubuntu one (`docker-compose-v2`), and the engine + plugin install in separate apt transactions so a missing plugin name can no longer prevent Docker itself from installing (#1541).
15+
- Installer: install-rknpu.sh no longer aborts right after replacing librknnrt.so when `ldconfig` exits non-zero on vendor images with merged /lib layouts; the cache refresh is best-effort and rkllama now installs in the same run (#1543).
16+
- Installer: the prebuilt desktop bundle is used on re-runs again. Re-runs as root over the taos-owned checkout tripped git's dubious-ownership check, which silently disabled the prebuilt path and forced a local vite build every time; the tree check now runs as the owning user, logs say whether the bundle channel was unreachable or genuinely mismatched, and installs pinned to a release tag fall back to the bundle attached to that release (#1544).
17+
- Installer: install-rknpu.sh now installs the OpenCV runtime libraries rkllama needs (libGL, GLib, libSM, libXext); vendor Debian images ship without them and rkllama.service crashlooped on "ImportError: libGL.so.1" (#1545).
18+
- Installer: when a vendor image ships with Docker preinstalled, incus is now installed as well (it is the preferred runtime for agent containers; skip with TAOS_NO_INCUS=1). Previously the installer treated the pre-existing Docker as sufficient and only a later warning told the user to install incus by hand and re-run (#1546).
19+
1020
## [1.0.0-beta.16] - 2026-07-02
1121

1222
### Added

README.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,18 @@ curl -fsSL https://raw.githubusercontent.com/jaylfc/taOS/master/scripts/install-
7878

7979
Run without `sudo` to install as a user-mode systemd unit instead. The script is idempotent, safe to re-run on an existing install. Supports env-var overrides for install path, branch, and port.
8080

81-
Tested on WSL2 (Windows 11) with the default Ubuntu image, including the current Python 3.14 default: the installer provisions a compatible Python automatically, so a clean box needs nothing more than the one line above.
81+
### Verified installs
82+
83+
Platforms where a clean controller install has been verified end to end. Ran it somewhere else? Open an issue with your install log (the installer prints an environment banner for exactly this) and it gets added here.
84+
85+
| Hardware | OS | Verified |
86+
| --- | --- | --- |
87+
| Orange Pi 5 Plus 16GB (RK3588) | Armbian (Debian trixie base) | Runs the maintainer's stack daily, including the RK3588 NPU memory/embedding path |
88+
| Orange Pi 5 Pro (RK3588S) | Official Orange Pi Debian 12 (Bookworm) vendor image | Clean-install community report (#1540); fixes shipped in v1.0.0-beta.17, including the RK3588 NPU backend with preloaded models |
89+
| x86_64 PC | Fedora | Maintainer-verified install (also serves as a GPU cluster worker with an RTX 3060) |
90+
| x86_64 PC | Debian | Maintainer-verified install |
91+
| WSL2 on Windows 11 | Default Ubuntu image (incl. Python 3.14) | Clean install; the installer provisions a compatible Python automatically |
92+
| Mac | macOS | Installer verified in early betas; the dedicated macOS app (Apple Containerization) is a separate track |
8293

8394
**Manual / development:**
8495

desktop/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "tinyagentos-desktop",
33
"private": true,
4-
"version": "1.0.0-beta.16",
4+
"version": "1.0.0-beta.17",
55
"type": "module",
66
"scripts": {
77
"dev": "vite",
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { describe, it, expect, vi, afterEach } from "vitest";
2+
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
3+
import { ModelBrowser } from "./ModelBrowser";
4+
5+
// Pins the #1548 fix: a download that fails must stay visible with its
6+
// backend error and a Retry button, not silently vanish (which made an
7+
// instant failure look like a completed install).
8+
9+
const CATALOG = {
10+
models: [
11+
{
12+
id: "qwen-test",
13+
name: "Qwen Test",
14+
description: "test model",
15+
capabilities: ["chat"],
16+
variants: [
17+
{
18+
id: "q4",
19+
name: "Q4",
20+
size_mb: 500,
21+
compatibility: "green",
22+
downloaded: false,
23+
},
24+
],
25+
},
26+
],
27+
};
28+
29+
function mockFetch(routes: Record<string, () => Response>) {
30+
vi.stubGlobal(
31+
"fetch",
32+
vi.fn((url: string) => {
33+
for (const [prefix, make] of Object.entries(routes)) {
34+
if (url.startsWith(prefix)) return Promise.resolve(make());
35+
}
36+
return Promise.resolve(
37+
new Response("[]", {
38+
status: 200,
39+
headers: { "Content-Type": "application/json" },
40+
}),
41+
);
42+
}) as unknown as typeof fetch,
43+
);
44+
}
45+
46+
function json(body: unknown): Response {
47+
return new Response(JSON.stringify(body), {
48+
status: 200,
49+
headers: { "Content-Type": "application/json" },
50+
});
51+
}
52+
53+
describe("ModelBrowser download errors", () => {
54+
afterEach(() => {
55+
vi.unstubAllGlobals();
56+
vi.useRealTimers();
57+
});
58+
59+
it("shows the backend error and a Retry button when a download fails", async () => {
60+
mockFetch({
61+
"/api/models/downloads/": () =>
62+
json({ status: "error", percent: 0, error: "SHA256 mismatch" }),
63+
"/api/models/download": () => json({ download_id: "dl-1" }),
64+
"/api/models/loaded": () => json({ models: [] }),
65+
"/api/models": () => json(CATALOG),
66+
"/api/providers": () => json([]),
67+
});
68+
69+
render(
70+
<ModelBrowser open={true} onClose={() => {}} capability="chat" />,
71+
);
72+
73+
const btn = await screen.findByRole("button", { name: "Download" });
74+
fireEvent.click(btn);
75+
76+
// Poll interval is 2s; the error must surface, not be cleared.
77+
const alert = await screen.findByRole(
78+
"alert",
79+
{},
80+
{ timeout: 5000 },
81+
);
82+
expect(alert.textContent).toContain("SHA256 mismatch");
83+
expect(
84+
screen.getByRole("button", { name: "Retry" }),
85+
).toBeInTheDocument();
86+
}, 10000);
87+
88+
it("shows an error when the download cannot even be started", async () => {
89+
mockFetch({
90+
"/api/models/download": () => json({ error: "no download_url for variant" }),
91+
"/api/models/loaded": () => json({ models: [] }),
92+
"/api/models": () => json(CATALOG),
93+
"/api/providers": () => json([]),
94+
});
95+
96+
render(
97+
<ModelBrowser open={true} onClose={() => {}} capability="chat" />,
98+
);
99+
100+
const btn = await screen.findByRole("button", { name: "Download" });
101+
fireEvent.click(btn);
102+
103+
const alert = await screen.findByRole("alert");
104+
expect(alert.textContent).toContain("no download_url for variant");
105+
});
106+
});

desktop/src/components/ModelBrowser.tsx

Lines changed: 64 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ export function ModelBrowser({
103103
"compatible",
104104
);
105105
const [downloading, setDownloading] = useState<
106-
Record<string, { percent: number; status: string }>
106+
Record<string, { percent: number; status: string; error?: string }>
107107
>({});
108108
const [loadedModels, setLoadedModels] = useState<LoadedModel[]>([]);
109109

@@ -243,17 +243,27 @@ export function ModelBrowser({
243243
});
244244
const data = await res.json();
245245
if (!data.download_id) {
246-
setDownloading((prev) => {
247-
const n = { ...prev };
248-
delete n[key];
249-
return n;
250-
});
246+
// Keep the failure on screen: silently clearing here made a
247+
// rejected install look like an instant success (#1548).
248+
setDownloading((prev) => ({
249+
...prev,
250+
[key]: {
251+
percent: 0,
252+
status: "error",
253+
error: data.error || data.detail || "The download could not be started",
254+
},
255+
}));
251256
return;
252257
}
253258
const interval = setInterval(async () => {
254259
try {
255260
const pr = await fetch(`/api/models/downloads/${data.download_id}`);
256261
const task = await pr.json();
262+
// A proxy error page or empty body must land in the catch below
263+
// (which surfaces an error), not TypeError into a stuck spinner.
264+
if (!task || typeof task !== "object") {
265+
throw new Error("invalid poll response");
266+
}
257267
setDownloading((prev) => ({
258268
...prev,
259269
[key]: {
@@ -272,14 +282,32 @@ export function ModelBrowser({
272282
onModelDownloaded?.(modelId, variantId);
273283
} else if (task.status === "error") {
274284
clearInterval(interval);
275-
setDownloading((prev) => {
276-
const n = { ...prev };
277-
delete n[key];
278-
return n;
279-
});
285+
// A failed download must stay visible with its cause; clearing
286+
// it made an immediate failure indistinguishable from a
287+
// completed install (#1548).
288+
setDownloading((prev) => ({
289+
...prev,
290+
[key]: {
291+
percent: task.percent ?? 0,
292+
status: "error",
293+
// || not ??: the backend initialises error to an empty
294+
// string, and an empty alert helps nobody.
295+
error: task.error || "Download failed",
296+
},
297+
}));
280298
}
281299
} catch {
300+
// Losing the poll (network blip, proxy error page) must not
301+
// strand a spinner with no outcome; surface it as an error.
282302
clearInterval(interval);
303+
setDownloading((prev) => ({
304+
...prev,
305+
[key]: {
306+
percent: 0,
307+
status: "error",
308+
error: "Lost contact with the download; retry to continue",
309+
},
310+
}));
283311
}
284312
}, 2000);
285313
} catch {
@@ -515,7 +543,31 @@ export function ModelBrowser({
515543
</div>
516544
</div>
517545
<div className="shrink-0">
518-
{dl ? (
546+
{dl && dl.status === "error" ? (
547+
<div
548+
className="flex items-center gap-2 max-w-[260px]"
549+
role="alert"
550+
>
551+
<span
552+
className="text-[10px] text-red-400 flex items-center gap-1 truncate"
553+
title={dl.error}
554+
>
555+
<AlertTriangle size={10} className="shrink-0" />
556+
<span className="truncate">
557+
{dl.error || "Download failed"}
558+
</span>
559+
</span>
560+
<Button
561+
variant="outline"
562+
size="sm"
563+
onClick={() =>
564+
startDownload(model.id, variant.id)
565+
}
566+
>
567+
Retry
568+
</Button>
569+
</div>
570+
) : dl ? (
519571
<div className="flex items-center gap-2 text-xs text-shell-text-secondary">
520572
<Loader2
521573
size={12}

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "tinyagentos"
7-
version = "1.0.0-beta.16"
7+
version = "1.0.0-beta.17"
88
description = "Self-hosted AI agent memory system for low-power hardware"
99
license = { file = "LICENSE" }
1010
# Upper-capped at <3.14 because litellm (the proxy extra, the agent/model proxy

scripts/install-rknpu.sh

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,12 @@ pin_librknnrt() {
319319
# Install the new one.
320320
sudo install -m 0644 "$tmp" "$LIBRKNNRT_DEST"
321321
LIBRKNNRT_REPLACED=1
322-
sudo ldconfig
322+
# ldconfig can exit non-zero on vendor images (e.g. "/lib/librknnrt.so is
323+
# not a symbolic link" on merged-/lib layouts), which under set -e killed
324+
# the whole install right after the library was correctly replaced and
325+
# rkllama never got installed (#1543). The cache refresh is best-effort:
326+
# the library is already at its canonical path.
327+
sudo ldconfig || warn "ldconfig exited $? after installing librknnrt (its stderr above has the cause; harmless on merged /lib layouts where the runtime resolves by absolute path; continuing)"
323328

324329
# Verify the version string as a belt-and-braces check. The SHA256 above
325330
# already proved $tmp is byte-for-byte the pinned runtime, so this is
@@ -391,6 +396,34 @@ install_rkllama() {
391396
# `strings` (binutils) is used by the librknnrt version checks; minimal
392397
# Pi images can lack it (#783). Ensure it is present.
393398
command -v strings >/dev/null 2>&1 || _need+=("binutils")
399+
# rkllama imports OpenCV (cv2), which needs the OpenGL/GLib/X runtime
400+
# libs; vendor Debian images ship without them and the service then
401+
# crashloops on "ImportError: libGL.so.1" (#1545). Bookworm+ names the
402+
# GL runtime libgl1; older releases only have libgl1-mesa-glx.
403+
# GLib was renamed libglib2.0-0t64 in Ubuntu 24.04 / Debian Trixie;
404+
# probe both installed names and add whichever the repos carry.
405+
# The madison probes below read the local package lists; refresh them
406+
# first so a stale image cannot steer the name choice. Best-effort,
407+
# but say so when it fails: with stale lists the probes may pick the
408+
# wrong package name, and the operator needs that context.
409+
sudo DEBIAN_FRONTEND=noninteractive apt-get update -qq \
410+
|| warn "apt-get update failed — package lists may be stale and the library package-name detection below may be wrong"
411+
if ! dpkg-query -W libglib2.0-0 >/dev/null 2>&1 && ! dpkg-query -W libglib2.0-0t64 >/dev/null 2>&1; then
412+
if [[ -n "$(apt-cache madison libglib2.0-0 2>/dev/null)" ]]; then
413+
_need+=("libglib2.0-0")
414+
else
415+
_need+=("libglib2.0-0t64")
416+
fi
417+
fi
418+
dpkg-query -W libsm6 >/dev/null 2>&1 || _need+=("libsm6")
419+
dpkg-query -W libxext6 >/dev/null 2>&1 || _need+=("libxext6")
420+
if ! dpkg-query -W libgl1 >/dev/null 2>&1 && ! dpkg-query -W libgl1-mesa-glx >/dev/null 2>&1; then
421+
if [[ -n "$(apt-cache madison libgl1 2>/dev/null)" ]]; then
422+
_need+=("libgl1")
423+
else
424+
_need+=("libgl1-mesa-glx")
425+
fi
426+
fi
394427
if (( ${#_need[@]} )); then
395428
log "installing build deps for rkllama wheel compilation: ${_need[*]}"
396429
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq "${_need[@]}" \

0 commit comments

Comments
 (0)