fix(models): VRAM reservation TTL sweep (#1766)#1798
Conversation
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 reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
📝 WalkthroughWalkthroughVRAM 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. ChangesVRAM admission lifecycle
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
| from :meth:`reserve` and the sync accessors; exposed for tests and | ||
| optional background sweeps. | ||
| """ | ||
| return self._sweep_stale_unlocked(now=now) |
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
| 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] |
There was a problem hiding this comment.
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.
Code Review SummaryStatus: 6 Issues Found | Recommendation: Address before merge Overview
Issue Details (click to expand)WARNING
SUGGESTION
Files Reviewed (4 files)
Fix these issues in Kilo Cloud Reviewed by hy3-20260706:free · Input: 87.2K · Output: 15.9K · Cached: 427K |
There was a problem hiding this comment.
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 winMove the VRAM probe off the event loop
tinyagentos/vram_reservation.py:206-266still probes synchronously inavailable_vram()/stats(), and the only call sites are the async deny path and/api/models/vram-reservationsintinyagentos/routes/models.py. Make the accessors async (or offload the probe at those call sites) sonvidia-smidoesn’t stall the request loop; if background sweeps are planned, wrapsweep_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_mbalways maxes with variant-levelmin_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 intomax()even when backend entries already supply a real figure. Today this is inert only because the rkllm catalog convention guarantees variant-levelmin_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
📒 Files selected for processing (4)
tests/test_routes_models.pytests/test_vram_reservation.pytinyagentos/routes/models.pytinyagentos/vram_reservation.py
* 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>
Summary
Completes the remaining work for #1766 after the urgent parts landed in #1767.
_probe_vram()returnsNonewhen nvidia-smi is absent;reserve()fails OPEN with bookkeeping only (matchesclaim_leasewhenfree_vram_mb is None). Atomic deny still applies when VRAM is measurable.requires.backends[].min_ram_mb(max across backends, variant fallback) via_estimated_vram_mb().VramReservationentries (default 1h) so a hung installer cannot hold capacity until controller restart. Sweep runs fromreserve(),available_vram(),stats(), and publicsweep_stale(). Probe under the async lock already usesasyncio.to_thread(from fix(worker,models): audit hotfixes - manifest crash, VRAM fail-open, real min_ram_mb key #1767).Acceptance
min_ram_mbproceeds with bookkeeping, no 503Test plan
Local result: 61 passed.
Closes #1766
Summary by CodeRabbit