Skip to content

fix(models): VRAM reservation TTL sweep (#1766)#1798

Merged
jaylfc merged 1 commit into
devfrom
fix/vram-reservation-1766
Jul 12, 2026
Merged

fix(models): VRAM reservation TTL sweep (#1766)#1798
jaylfc merged 1 commit into
devfrom
fix/vram-reservation-1766

Conversation

@jaylfc

@jaylfc jaylfc commented Jul 12, 2026

Copy link
Copy Markdown
Owner

Summary

Completes the remaining work for #1766 after the urgent parts landed in #1767.

Acceptance

  • No nvidia-smi: pull with real backend-level min_ram_mb proceeds with bookkeeping, no 503
  • Two concurrent large reserves on NVIDIA still hit the atomic check (only one admitted)
  • Reservation older than its TTL is reclaimed

Test plan

uv run pytest tests/test_vram_reservation.py tests/test_routes_models.py -q

Local result: 61 passed.

Closes #1766

Summary by CodeRabbit

  • Bug Fixes
    • Improved model download handling for RKLLM backends by accurately considering backend memory requirements.
    • Downloads can proceed when VRAM cannot be measured, while still maintaining reservation tracking.
    • Insufficient measurable VRAM now prevents downloads before installation begins.
    • Stale VRAM reservations are automatically reclaimed, preventing capacity from remaining blocked after interrupted installations.
  • Reliability
    • Improved handling of concurrent large-model downloads and reservation expiration.
    • Added clearer reservation status information, including the configured expiration window.

Completes the remaining #1766 work after the #1767 hotfix (fail-open on
no-probe hardware, backend-level min_ram_mb gate, asyncio.to_thread probe).

- Reclaim VramReservation entries older than a configurable TTL (default 1h)
  so a hung installer cannot hold capacity until controller restart. Sweep
  runs from reserve(), available_vram(), stats(), and public sweep_stale().
- Extract _estimated_vram_mb() so the rkllama pull gate clearly reads
  requires.backends[].min_ram_mb (max across backends, variant fallback).
- Tests: no-probe + real backend min_ram proceeds (no 503), concurrent
  large reserves on NVIDIA still atomic, TTL reclaim, and route-level
  503 when free VRAM is measurable and insufficient.

Closes #1766
@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

VRAM reservation now supports fail-open behavior without a hardware probe and TTL-based stale-entry reclamation. Model downloads estimate requirements from backend manifests, and tests cover admission, rejection, concurrency, estimation, and expiration.

Changes

VRAM admission lifecycle

Layer / File(s) Summary
Reservation fail-open and TTL lifecycle
tinyagentos/vram_reservation.py, tests/test_vram_reservation.py
Reservations record bookkeeping when VRAM probing is unavailable, stale reservations are reclaimed using configurable TTLs, and admission/accessor operations perform stale sweeps.
Backend-level model VRAM gating
tinyagentos/routes/models.py, tests/test_routes_models.py
rkllama downloads reserve the maximum backend-level min_ram_mb, falling back to the variant-level requirement when needed.
Reservation and download acceptance coverage
tests/test_vram_reservation.py, tests/test_routes_models.py
Tests cover no-probe admission, insufficient VRAM rejection, concurrent reservations, backend-level estimation, and TTL behavior.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

  • jaylfc/taOS#1658: Modifies the RKLLama download installer flow in the same route.
  • jaylfc/taOS#1725: Introduces the related atomic VRAM check-and-reserve behavior.
  • jaylfc/taOS#1767: Overlaps with backend-level VRAM gating and probe behavior.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 54.05% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: VRAM reservation TTL sweeping for models.
Linked Issues check ✅ Passed The summaries show #1766 requirements were addressed: fail-open no-probe handling, backend min_ram_mb estimation, stale TTL sweeping, and atomic reservation tests.
Out of Scope Changes check ✅ Passed The changes stay focused on the linked VRAM reservation and model-pull behavior, with no clear unrelated code introduced.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/vram-reservation-1766

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gitar-bot

gitar-bot Bot commented Jul 12, 2026

Copy link
Copy Markdown

Important

You are using the Gitar free plan. Upgrade to unlock code review, CI analysis, auto-apply, custom automations, and more.

Gitar

from :meth:`reserve` and the sync accessors; exposed for tests and
optional background sweeps.
"""
return self._sweep_stale_unlocked(now=now)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: sweep_stale() mutates shared state without holding the lock

This public method calls _sweep_stale_unlocked(), which mutates self._pending and self._reserved_vram_mb, but does not acquire self._lock. reserve() mutates the same structures under the lock and yields during await asyncio.to_thread(self._probe_vram), so a sweep can run in that window. The docstring explicitly invites "optional background sweeps"; if such a sweep is driven from a background task/thread it can race with an in-flight reserve() and corrupt accounting. Acquire self._lock in this method (or clearly document single-threaded-only usage).


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

path should already have been admitted fail-open by :meth:`reserve`
(a deny message never reports these zeros as real capacity).
"""
self._sweep_stale_unlocked()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: available_vram() is documented as a read-only accessor but now reclaims reservations

Calling _sweep_stale_unlocked() here mutates self._pending, decrements self._reserved_vram_mb, and emits an INFO log. A monitoring endpoint that polls available_vram() will silently reclaim capacity and generate logs, which can mask genuine hangs and is a surprising side effect for a getter. Consider making the sweep explicit (call sweep_stale() deliberately) rather than implicit in a read accessor.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.


def stats(self) -> dict:
"""Return a snapshot for monitoring / debug endpoints."""
self._sweep_stale_unlocked()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: stats() returns a read-only snapshot but now reclaims reservations

Like available_vram(), this accessor calls _sweep_stale_unlocked(), so fetching stats for dashboards/monitoring mutates manager state and logs INFO messages. This couples monitoring with capacity reclamation and can hide a real hung-installer signal (the stale reservation vanishes from the snapshot before anyone inspects it). Document the side effect or move the sweep off the read path.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

res.vram_mb, res.caller, rid,
now - res.created_at, self._ttl_seconds,
)
if stale_ids and self._reserved_vram_mb < 0:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: Clamping self._reserved_vram_mb to 0 can drop accounting for active reservations

The defensive if stale_ids and self._reserved_vram_mb < 0: self._reserved_vram_mb = 0 resets the entire counter when it goes negative. If an underflow is detected, accounting already drifted, but zeroing the whole counter also discards the (positive) VRAM still legitimately held by active, non-stale reservations. That can over-report free VRAM and permit over-commit. Prefer logging/asserting the inconsistency rather than silently zeroing, or only clamp the per-entry delta.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

if isinstance(b, dict)
]
candidates.append(int(variant.get("min_ram_mb", 0) or 0))
return max(candidates) if candidates else 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: Dead conditional — if candidates else 0 is unreachable

candidates always has at least one element because the variant-level min_ram_mb is appended unconditionally, so max(candidates) can never raise. The else 0 branch is dead code.

Suggested change
return max(candidates) if candidates else 0
return max(candidates)

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

# While the installer is in flight the reservation is held.
# Drain it so release runs in the finally block.
download_id = data["download_id"]
await models_app.state.download_manager._running[download_id]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: await ..._running[download_id] has no timeout

This awaits the installer task directly with no timeout. If the mocked install path ever stalls or _running[download_id] is missing, the whole test suite hangs instead of failing fast. Wrap it in asyncio.wait_for(..., timeout=...).


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@kilo-code-bot

kilo-code-bot Bot commented Jul 12, 2026

Copy link
Copy Markdown

Code Review Summary

Status: 6 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 5
Issue Details (click to expand)

WARNING

File Line Issue
tinyagentos/vram_reservation.py 187 sweep_stale() mutates shared state without the lock; can race with reserve() if driven from a background task/thread as the docstring invites

SUGGESTION

File Line Issue
tinyagentos/vram_reservation.py 214 available_vram() is a read-only accessor but now reclaims reservations and logs INFO
tinyagentos/vram_reservation.py 224 stats() snapshot now reclaims reservations, coupling monitoring with reclamation
tinyagentos/vram_reservation.py 262 Clamping self._reserved_vram_mb to 0 drops accounting for active reservations and can permit over-commit
tinyagentos/routes/models.py 167 Dead conditional if candidates else 0 is unreachable (candidates always non-empty)
tests/test_routes_models.py 423 await _running[download_id] has no timeout; a stall hangs the suite
Files Reviewed (4 files)
  • tinyagentos/vram_reservation.py - 4 issues
  • tinyagentos/routes/models.py - 1 issue
  • tests/test_routes_models.py - 1 issue
  • tests/test_vram_reservation.py - 0 issues

Fix these issues in Kilo Cloud


Reviewed by hy3-20260706:free · Input: 87.2K · Output: 15.9K · Cached: 427K

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tinyagentos/vram_reservation.py (1)

206-235: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Move the VRAM probe off the event loop tinyagentos/vram_reservation.py:206-266 still probes synchronously in available_vram()/stats(), and the only call sites are the async deny path and /api/models/vram-reservations in tinyagentos/routes/models.py. Make the accessors async (or offload the probe at those call sites) so nvidia-smi doesn’t stall the request loop; if background sweeps are planned, wrap sweep_stale() in the manager lock.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tinyagentos/vram_reservation.py` around lines 206 - 235, Make
available_vram() and stats() asynchronous and offload their synchronous
_probe_vram() calls from the event loop, then update the async deny path and
/api/models/vram-reservations handler to await them. Preserve the existing
reservation sweep and returned values; if sweep_stale() is used from background
tasks, protect it with the manager lock.
🧹 Nitpick comments (1)
tinyagentos/routes/models.py (1)

152-168: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

_estimated_vram_mb always maxes with variant-level min_ram_mb, not a strict fallback.

Docstring states the variant-level key is a fallback, but candidates.append(int(variant.get("min_ram_mb", 0) or 0)) unconditionally folds it into max() even when backend entries already supply a real figure. Today this is inert only because the rkllm catalog convention guarantees variant-level min_ram_mb == 0 (per this same docstring); if that invariant is ever violated (e.g. a backend legitimately requiring less VRAM than the variant's unrelated host-RAM figure), this silently over-reserves — the exact failure mode the adjacent TODO (lines 404-406) already calls out for host-RAM floors.

♻️ Proposed fix — true fallback semantics
 def _estimated_vram_mb(variant: dict) -> int:
     backend_reqs = (variant.get("requires") or {}).get("backends") or []
     candidates = [
         int(b.get("min_ram_mb", 0) or 0)
         for b in backend_reqs
-        if isinstance(b, dict)
+        if isinstance(b, dict) and b.get("min_ram_mb")
     ]
-    candidates.append(int(variant.get("min_ram_mb", 0) or 0))
-    return max(candidates) if candidates else 0
+    if candidates:
+        return max(candidates)
+    return int(variant.get("min_ram_mb", 0) or 0)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tinyagentos/routes/models.py` around lines 152 - 168, Update
_estimated_vram_mb so variant-level min_ram_mb is used only when no valid
backend min_ram_mb values are present; return the maximum backend requirement
otherwise. Preserve the existing integer/default handling for backend entries
and the zero result when neither source provides a value.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@tinyagentos/vram_reservation.py`:
- Around line 206-235: Make available_vram() and stats() asynchronous and
offload their synchronous _probe_vram() calls from the event loop, then update
the async deny path and /api/models/vram-reservations handler to await them.
Preserve the existing reservation sweep and returned values; if sweep_stale() is
used from background tasks, protect it with the manager lock.

---

Nitpick comments:
In `@tinyagentos/routes/models.py`:
- Around line 152-168: Update _estimated_vram_mb so variant-level min_ram_mb is
used only when no valid backend min_ram_mb values are present; return the
maximum backend requirement otherwise. Preserve the existing integer/default
handling for backend entries and the zero result when neither source provides a
value.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 6df80eef-0fa1-4afb-84f4-9524263d208d

📥 Commits

Reviewing files that changed from the base of the PR and between 5c4afbb and 747e24b.

📒 Files selected for processing (4)
  • tests/test_routes_models.py
  • tests/test_vram_reservation.py
  • tinyagentos/routes/models.py
  • tinyagentos/vram_reservation.py

@jaylfc jaylfc merged commit 9d006f4 into dev Jul 12, 2026
11 checks passed
jaylfc added a commit that referenced this pull request Jul 15, 2026
* fix(projects): show consent-flow external agents in the External section (#1784)

Approved external CLI agents (grok, kilo) were appearing in the plain Members
list instead of under "External / Connected agents" next to the other connected
agents. The Members panel only classified a member as external when its
member_id matched a registry agent's handle, but the consent flow registers
these agents with an empty handle and adds the project member row keyed by the
canonical id. So the match never fired and they fell through to the main list.

Match external registry agents by canonical id as well as handle (older
identities like the assistant reference by handle, consent-flow agents by
canonical id), and map the "grok" framework, not only "grok-build", to the
Grok label so the badge reads correctly.

* test(secrets): add coverage for the Secrets app (#1785)

Covers the mount-time /api/secrets fetch and loading state, masked value
rendering, the empty and failed-fetch fallbacks, reveal and hide via the
per-secret API, add and delete through the dialog, and category filtering.
The GitHub integration is mocked so its on-mount identity fetch does not
interfere with the secrets assertions.

* test(notes): add vitest coverage for NotesApp/TodoApp mounted behavior (#1787)

Render NotesApp and TodoApp, mock the /api/notes fetch on mount, and assert real behavior: kind filtering, empty states, detail load on select, and the create flow.

* test(chess): add vitest coverage for ChessApp (#1788)

Cover render, legal moves, turn changes, checkmate status, new game reset,
and vs-agent mode, with the on-mount agents fetch mocked.

* test(imageviewer): add vitest coverage for ImageViewerApp (#1789)

Render the app and assert real behavior: empty state, file load,
zoom in/out with min/max clamps, 90-degree rotation, reset on new
image, and object URL revocation. Stub fetch and URL.createObjectURL
so on-mount integrations do not interfere.

* fix(desktop): Registry poll no longer resets scroll (#1761) (#1786)

* fix(desktop): keep Registry panel scroll stable across 5s polls (#1761)

Quiet background polls no longer flip loading (which unmounted the list)
and setEntries is a no-op when id/content are unchanged, so scroll and
in-progress interaction are preserved. Add registryEntriesEqual helper
and vitest coverage for the poll no-op path.

* fix(desktop): guard registryEntriesEqual index access and drop em dashes

The poll no-op comparison read a[i]/b[i] without a guard, which fails the
strict noUncheckedIndexedAccess build (spa-build). Add an explicit undefined
guard, and remove the em dashes from the added comments.

* chore(deps): bump the python-deps group with 3 updates (#1790)

Updates the requirements on [uvicorn[standard]](https://github.com/Kludex/uvicorn), [croniter](https://github.com/pallets-eco/croniter) and [litellm[proxy]](https://github.com/BerriAI/litellm) to permit the latest version.

Updates `uvicorn[standard]` to 0.51.0
- [Release notes](https://github.com/Kludex/uvicorn/releases)
- [Changelog](https://github.com/Kludex/uvicorn/blob/main/docs/release-notes.md)
- [Commits](Kludex/uvicorn@0.50.0...0.51.0)

Updates `croniter` from 6.2.3 to 6.2.4
- [Release notes](https://github.com/pallets-eco/croniter/releases)
- [Changelog](https://github.com/pallets-eco/croniter/blob/main/CHANGELOG.rst)
- [Commits](pallets-eco/croniter@6.2.3...6.2.4)

Updates `litellm[proxy]` to 1.92.0
- [Release notes](https://github.com/BerriAI/litellm/releases)
- [Commits](https://github.com/BerriAI/litellm/commits)

---
updated-dependencies:
- dependency-name: uvicorn[standard]
  dependency-version: 0.51.0
  dependency-type: direct:production
  dependency-group: python-deps
- dependency-name: croniter
  dependency-version: 6.2.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: python-deps
- dependency-name: litellm[proxy]
  dependency-version: 1.92.0
  dependency-type: direct:production
  dependency-group: python-deps
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps): bump the spa-deps group in /desktop with 16 updates (#1791)

---
updated-dependencies:
- dependency-name: "@codemirror/state"
  dependency-version: 6.7.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: spa-deps
- dependency-name: "@codemirror/view"
  dependency-version: 6.43.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: spa-deps
- dependency-name: "@radix-ui/react-dialog"
  dependency-version: 1.1.19
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: spa-deps
- dependency-name: "@radix-ui/react-dropdown-menu"
  dependency-version: 2.1.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: spa-deps
- dependency-name: "@radix-ui/react-select"
  dependency-version: 2.3.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: spa-deps
- dependency-name: "@radix-ui/react-switch"
  dependency-version: 1.3.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: spa-deps
- dependency-name: "@radix-ui/react-tabs"
  dependency-version: 1.1.17
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: spa-deps
- dependency-name: "@radix-ui/react-tooltip"
  dependency-version: 1.2.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: spa-deps
- dependency-name: "@tiptap/extension-link"
  dependency-version: 3.27.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: spa-deps
- dependency-name: "@tiptap/extension-underline"
  dependency-version: 3.27.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: spa-deps
- dependency-name: "@tiptap/pm"
  dependency-version: 3.27.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: spa-deps
- dependency-name: "@tiptap/react"
  dependency-version: 3.27.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: spa-deps
- dependency-name: "@tiptap/starter-kit"
  dependency-version: 3.27.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: spa-deps
- dependency-name: "@types/three"
  dependency-version: 0.185.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: spa-deps
- dependency-name: vite
  dependency-version: 8.1.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: spa-deps
- dependency-name: vitest
  dependency-version: 4.1.10
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: spa-deps
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* docs(design): account model, free username plus paid chosen subdomains (#1792)

* docs(design): Hailo-10H LLM backend, zero-touch install parity with RK3588 (#1793)

* docs(design): hub.taos.my local-first P2P social network foundation (#1794)

* feat(hailo): reserve port 7836 and map hailo-ollama llm-chat capability (#1795)

Slice 1 of the Hailo-10H LLM backend design (docs/design/hailo-llm-backend.md):
add 7836 to RESERVED_PORTS so apps cannot squat the NPU backend port, and
register hailo-ollama with llm-chat in BACKEND_CAPABILITIES. Closes the first
shippable piece of #1771.

* feat(account): controller proxy actions for subdomain check/claim/release (slice 3) (#1796)

* feat(account): proxy subdomain check/claim/release actions (slice 3)

Add /api/account/subdomains/{check,claim,release} routes to account_proxy.py
that forward to the taos.my subdomain claims service with the session cookie
passthrough. The name field is validated rid-style before it can reach the
upstream URL, so a crafted name cannot inject path/query (SSRF/path-traversal
guard). Implements account design doc slice 3.

* test(account): cover subdomain proxy forwarding, 503, and name validation

Add tests alongside the existing account_proxy suite: forwarding of check
(query name) and claim/release (body name) with cookie passthrough, 503 when
the account service is unconfigured, and 400 on an invalid name with no
upstream call.

* docs(design): Projects app nested elements, one project with typed elements (#1797)

* fix(models): VRAM reservation TTL sweep + #1766 acceptance coverage (#1798)

Completes the remaining #1766 work after the #1767 hotfix (fail-open on
no-probe hardware, backend-level min_ram_mb gate, asyncio.to_thread probe).

- Reclaim VramReservation entries older than a configurable TTL (default 1h)
  so a hung installer cannot hold capacity until controller restart. Sweep
  runs from reserve(), available_vram(), stats(), and public sweep_stale().
- Extract _estimated_vram_mb() so the rkllama pull gate clearly reads
  requires.backends[].min_ram_mb (max across backends, variant fallback).
- Tests: no-probe + real backend min_ram proceeds (no 503), concurrent
  large reserves on NVIDIA still atomic, TTL reclaim, and route-level
  503 when free VRAM is measurable and insufficient.

Closes #1766

* feat(account): frontend types + Account panel split for username/subdomains (slice 4) (#1801)

Implements slice 4 of docs/design/account-username-subdomain-model.md.

- account-client: add SubdomainClaim/SubdomainCheck types, Account.username
  and Account.subdomains, deprecate Account.handle; add checkSubdomain,
  claimSubdomain, releaseSubdomain helpers with the same degrade-to-state
  (AuthError, never throw) error style as the auth actions.
- AccountPanel: split the old ReserveHandleCard into a free UsernameCard
  (no taOSgo mention, no .taos.my suffix) and a SubdomainsCard (claim list
  with active/grace badges, inline availability check, release, disabled claim
  UI when unsubscribed). Update the section intro copy.
- Tests: account-client subdomain helper coverage (mocked website endpoints);
  AccountPanel coverage for free-username copy, claim list rendering, disabled
  claim when unsubscribed, and grace badge.

* feat(hailo): slice 2 hailo-ollama installer (#1771) (#1803)

* feat(hailo): slice 2 hailo-ollama installer (#1771)

Implements docs/design/hailo-llm-backend.md slice S2 (section C):
scripts/install-hailo.sh mirrors scripts/install-rknpu.sh structure and
safety contract. Detects Hailo-10H via /dev/hailo0 + lspci/hailortcli,
installs HailoRT (>= 5.1.0 firmware floor) on Raspberry Pi OS, clones
hailo-ollama at a pinned ref remapped to port 7836, installs a systemd
unit with orphan-reap ExecStartPre, and health-waits on /api/tags.

Verification: bash -n, shellcheck, and a non-Hailo host prints the
no-detection notice and exits 0 without touching the system.

* chore(hailo): doc-gate trailer for install-hailo.sh

The installer is specified line by line in docs/design/hailo-llm-backend.md
section C (slice S2), which is already merged on dev.

Docs-Reviewed: implements the merged design doc docs/design/hailo-llm-backend.md section C, no separate doc change needed

* feat(hailo): slice 3 hailo-ollama managed service manifest (#1804)

* feat(hailo): slice 3 hailo-ollama managed service manifest

Add app-catalog/services/hailo-ollama/manifest.yaml per the managed-backend
contract (lifecycle.auto_manage, unit, scope=system, health on 7836). The
backend flows through load_managed_backends() with no new plumbing.

Adds a unit test that loads the real manifest and asserts the backend is
returned by load_managed_backends().

Docs-Reviewed: implements the merged design doc docs/design/hailo-llm-backend.md

* fix(hailo): declare the proprietary license in the hailo-ollama manifest

test_services_manifests requires a license key on every service manifest.
The runtime is Hailo proprietary software behind an install-time EULA
acceptance, so the manifest now says exactly that.

Docs-Reviewed: license posture is specified in docs/design/hailo-llm-backend.md security and licensing section

* feat(account): onboarding free username claim step (slice 5) (#1805)

Adds a free taOS username claim step to OnboardingScreen, shown right
after local account creation. Clearly labeled free, never gated behind
taOSgo, and the user can always finish (claim is optional and failures
degrade to a Settings pointer). Public subdomain publishing is deferred
to Settings so onboarding never dead-ends on the paid path.

Bounded to OnboardingScreen.tsx plus its test, per the slice plan.

* feat(hub): identity keypair keystore + directory registration proxy (slice 1) (#1806)

Implements slice 1 of the hub.taos.my own-your-posts social network design
(docs/design/hub-social-network-foundation.md).

Controller side, the taos.my directory endpoints are the contract (mocked in
tests):

- tinyagentos/hub/identity.py: node keystore that mints an Ed25519 signing key
  and an X25519 encryption key on first use, persists them 0600 under
  <data_dir>/hub/identity.json (mesh_credentials.py pattern: atomic write,
  allowlisted fields, TAOS_DATA_DIR override), and exposes the registerable
  public view, the SHA-256 author fingerprint, and a challenge-proof signer plus
  verifier.
- account_proxy.py: _ACTIONS additions and same-origin routes for hub identity
  register / lookup / rotate, forwarding to /api/hub/identity/* with session
  cookie pass-through; lookup validates the username as an rid-style token before
  it can reach the upstream URL.

Tests: keystore round-trip + 0600 perms + stable fingerprint, proxy forwarding +
503 (unconfigured and unreachable), challenge proof rejects a wrong key, lookup
relays the append-only key log.

* feat(hailo): slice 4 install-time gates for Hailo-10H (per design doc) (#1807)

Mirror the Rockchip RKNPU install-time gates for the Hailo-10H NPU across
install.sh, scripts/install-server.sh and scripts/install-worker.sh:
detect /dev/hailo0 with 10H vs 8L discrimination (lspci/hailortcli, with
TAOS_FORCE_HAILO override), chain into scripts/install-hailo.sh under
TAOS_HAILO_SETUP=1, fail-soft on chain failure, and document the new env
vars in each script header. Part of #1771.

Docs-Reviewed: implements the merged design doc (doc-gate requires it)

* feat(hailo): slice 5 runtime detection + provider adapter for Hailo-10H (per design doc) (#1808)

Add the hailo-ollama backend end to end with no new hardware needed for CI:
- worker probe candidate on the taOS remap port 7836, Ollama-compatible
- OllamaCompatAdapter entry for hailo-ollama
- litellm_config ollama-compat membership extended to hailo-ollama
- provider type registered so auto_register_from_manifest seeds
  local-hailo-ollama on Hailo-10H hardware (mirrors rkllama local-rkllama)
- tests for detection, LiteLLM ollama/<model> prefix, and seed

* feat(hub): profile object + local hub store (slice 2) (#1809)

* feat(hub): profile object + local hub store (slice 2)

Implements slice 2 of the hub.taos.my own-your-posts social network design
(docs/design/hub-social-network-foundation.md).

- tinyagentos/hub/store.py: canonical-JSON encode/hash/sign/verify helpers and
  a SQLite HubStore (objects, blobs, authors tables). Objects are canonical-JSON
  encoded, content-addressed by SHA-256 of the canonical bytes excluding the
  signature, and signed by the slice-1 Ed25519 keystore. Profiles are the one
  mutable object with highest-version-wins semantics; put_profile ignores a
  stale (lower-or-equal version) replica so it can never clobber a newer one.
- tinyagentos/routes/hub.py: local API the Hub app consumes. Render the node's
  own profile and create/update it with a version bump, each response carrying
  an explicit degrade state (no-identity / no-profile / ok). The store is opened
  lazily and colocated with the identity keystore under the data dir. Wired into
  routes/__init__.py. No peer networking; directory calls stay in account_proxy.

Tests: canonicalization vectors, sign/verify (good sig verifies, tamper and
wrong key do not), version-wins, object/blob/author store round-trip, and the
profile routes end to end (degrade states, create + version bump, kind
validation, signature check).

* chore(hub): doc-gate trailer for the hub store slice

The profile object and local hub store are specified in
docs/design/hub-social-network-foundation.md (slice 2), merged on dev.

Docs-Reviewed: implements the merged design doc docs/design/hub-social-network-foundation.md slice 2, no separate doc change needed

* feat(hub): follow / friend / circle model + request brokering (slice 3) (#1810)

Implement hub social slice 3 from docs/design/hub-social-network-foundation.md:
- signed follow and cache-grant statements (cache-grant stored, not yet
  acted on; the cache worker lands in slice 6),
- friend-request send/accept/decline flows that broker through the
  directory and record the local accepted edge,
- local block (severs every edge to the peer and asks the hub to revoke
  the server-side edge) and mute operations,
- a presence gate that denies lookup without an accepted edge,
- directory proxy entries for requests, presence, and edge revoke.

Tests cover edge authorization (presence denied without an accepted edge),
rate-limit behavior, and block severing the edge.

Docs-Reviewed: implements the merged design doc (doc-gate requires it)

* feat(projects): nested element store, CRUD routes, and task element tags (slice 1) (#1811)

* feat(projects): implement nested element store, CRUD routes, and task element tags (slice 1)

Adds project elements (one level of nesting per the design) with a dedicated
store and owner-gated CRUD routes, an element_id tag on tasks with create and
update validation plus list and ready filtering, and the Beads snapshot
carrying the tag. Group/promote and assignment are later slices.

Docs-Reviewed: implements the merged design doc docs/design/projects-nested-elements.md slice 1.

* test(projects): slice 1 element store, CRUD route, and task tag coverage

Adds the element store unit tests and route-level coverage for element CRUD,
tag validation, the 409 delete guard, untag mode, and element_id filtering on
list/ready. Proves an external agent token filters by element with no auth
change.

Docs-Reviewed: implements the merged design doc docs/design/projects-nested-elements.md slice 1.

* feat(projects): kanban element filter bar (slice 2) (#1812)

Add a persistent element axis to the kanban board: element client API and
element_id on task types, a pure element filter in boardFiltering, a new
ElementFilterBar rendered from the toolbar (All | element chips | Project-
level), an element badge on cards when the board is unfiltered, and element
fetching wired through useBoardData. Zero-element projects stay untouched
(the bar does not render). Tests added and existing board tests updated.

Docs-Reviewed: implements the merged design doc (doc-gate requires it for scripts/, app-catalog/, tinyagentos/ changes)

* feat(hub): post objects, chain logic, image ingest, composer + own-timeline (slice 4) (#1813)

Implements slice 4 of docs/design/hub-social-network-foundation.md: a per-author
hash chain (seq/prev), signed append plus verify and tamper detection, signed
tombstones that drop content while keeping the chain verifiable, and image
ingest that re-encodes and strips EXIF. Adds the local post, timeline, and
delete routes plus the Hub app: a composer with a loud friends-only-by-default
visibility switch and an own-timeline read from the local store. No peer sync
yet (that is slice 5).

Tests cover chain append/verify, tamper detection, tombstone drops content and
keeps the chain verifiable, and EXIF stripped.

Docs-Reviewed: implements the merged design doc (doc-gate requires it for scripts/, app-catalog/, tinyagentos/ changes)

* fix: map violet and red note colors to valid tldraw palette names (#1815)

Canvas notes with payload.color violet or red fell back to yellow
because the tldraw COLOR_MAP in NoteShape.tsx was missing those
entries. Added violet, red, and light-blue mappings so the note
shape renders with the correct background color. Added tests to
verify the color strings survive element-to-shape coercion and
that COLOR_MAP has the expected entries.

* docs(readme): External Coding Agents section (bring your own AI team) (#1816)

* docs(readme): External Coding Agents section (registry, consent onboarding, kanban + a2a work loop)

The external-agent collaboration flow (access requests approved from the
phone, scoped registry identities, board claim/PR/close loop, a2a
coordination) had no README presence despite being live and proven.

Docs-Reviewed: readme-only change describing the shipped flow documented in docs/design/external-agent-onboarding.md

* docs(readme): reference only docs that exist on dev

doc-gate verifies every mentioned path exists; the project-invite design
doc lives on a branch, so the section links only the onboarding doc.

Docs-Reviewed: readme-only change describing the shipped flow documented in docs/design/external-agent-onboarding.md

* fix(hub): serialize chain appends so racing posts are not orphaned (#1817)

next_chain_position read the chain head and put_chain_object inserted with
INSERT OR IGNORE, so two concurrent appends for the same author computed the
same seq and the loser was silently dropped from the chain index while its
body stayed in hub_objects. A per-store asyncio lock now serializes the
read-position-then-insert section in append_post and delete_post; the local
node is the only writer of its own chain, so this closes the race. Regression
test proves three concurrent appends land as seq 1,2,3.

Docs-Reviewed: hardening of the merged design doc docs/design/hub-social-network-foundation.md slice 4, no doc change needed

* fix(canvas): map text elements to visible taos-text shapes (#1819)

* fix(canvas): map text elements to visible taos-text shapes

element-to-shape.ts only mapped note/link/image to custom shape types;
text (and mermaid label) fell through to taos-generic whose props only
carry geometry, so the payload never reached a visible label. Add a
taos-text shape util and map kind=text to it, coercing payload.text to
a string with empty-string default so imperfect agent writes still render.

Add tests for text kind mapping, payload coercion, and fallback behavior.

* chore(canvas): doc-gate trailer for text shape

New desktop source (TextShape.tsx) rendering the canvas text kind; no
behavioral doc needed, the canvas element kinds are covered by the design.

Docs-Reviewed: frontend-only canvas fix for the boarded canvas text render bug, no doc change needed

* feat(projects): element overview grid, creation flow, and drill-in navigation (slice 3) (#1820)

* feat(projects): element overview grid, creation flow, and drill-in navigation (slice 3)

Implements slice 3 of docs/design/projects-nested-elements.md on the frontend:

- New elements/ registry (types.ts) with the seven known types, their
  icons, and type-driven landing-tab order.
- ElementGrid: overview grid shown when a project has elements, with a
  fixed Project card and an Add element tile; zero-element projects keep
  today's workspace pane untouched (back-compat invariant).
- ElementCard: type icon, name, type label, open/total task counts, owner
  chip, and a recent-activity line.
- ElementCreateDialog: create a single nested element (name, slug, type,
  optional owner) within an existing project.
- CreateProjectDialog: optional second step to seed nested elements;
  skipping yields a project identical to today's.
- ProjectWorkspace: element drill-in scopes the board to the element and
  lands on the type's preferred tab, with a breadcrumb and the element id
  carried on the URL for deep links.
- ProjectBoard/BoardToolbar: accept a scoped element id and hide the
  element filter bar while scoped.

Tests cover the grid, card, create dialog, the zero-element regression,
drill-in with breadcrumb, and the creation-flow step two.

* chore(projects): doc-gate trailer for elements slice 3 UI

New desktop sources under ProjectsApp/elements/ implementing slice 3 of the
merged nested-elements design; frontend-only, no behavioral doc change.

Docs-Reviewed: implements the merged design doc docs/design/projects-nested-elements.md slice 3

* Add confirm guard before video delete (#1821)

Docs-Reviewed: frontend change to the shipped Video Studio surface.

* Projects elements slice 4: element-scoped canvas + files (#1822)

Add an element_id tag to canvas items (store column + ALTER migration,
element-filtered list and create on the canvas routes), an adopt-existing
element files subfolder helper, untag-on-delete for canvas items, and wire
the frontend so the canvas honors the active element filter and the Files
tab mounts the element subfolder.

Docs-Reviewed: implements the boarded task, frontend/backend change to shipped surface.

* feat(projects): doc-review stamp store + routes (#1802 slice 3)

Add per-document review_state machine (awaiting_review/approved/changes_requested)
with actor recording and timestamps, project-scoped agent-token gated routes, and
the desktop stamp badge column plus typed API client.

Docs-Reviewed: doc-review stamp store + routes slice per document-review-surface.md

* feat(projects): add doc-review stamp badge to FilesApp and fix projects.ts types

- Add ReviewBadge component with onClick support for cycling review state
- Add cycleDocReview callback for in-place state transitions
- Wire badge into FileRow (list view) and grid cards (project: locations only)
- Fetch review states on project-location navigation via projectsApi.docReviews.list
- Remove duplicate DocReviewState/DocReview type definitions from projects.ts
- Remove duplicate docReview API block; keep single canonical docReviews namespace
- Use DocReview | DocReviewMissing union for GET response type
- Apply encodeURIComponent to state filter query parameter

Docs-Reviewed: doc-review stamp store + routes slice per document-review-surface.md

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant